Repository: exercism/cli (go)

Issue:
Allow submitting solution without specifying files
One of the most requested features for the CLI is to be able to submit a solution without having to explicitly specify the files to submit. With Exercism v3, each exercise will have a `.meta/config.json` file (see https://github.com/exercism/docs/blob/main/building/tracks/concept-exercises.md#file-metaconfigjson and https://github.com/exercism/docs/blob/main/building/tracks/practice-exercises.md#file-metaconfigjson). This file contains a `files` property with a nested a `solution` string array property. The values in the `solution` array are the names of the files that the student should work on. In other words: these file names can be used to determine the files that should be submitted, provided no files were specified by the student.

As an example, consider the following `.meta/config.json` file:

```json
{
  "files": {
    "solution": ["Lasagna.fs"],
    "test": ["LasagnaTests.fs"],
    "exemplar": [".meta/Exemplar.fs"]
  }
}
```

If the student runs `exercism submit` (no file names specified), the CLI should then interpret this as if the student ran `exercism submit Lasagna.fs`.

Note that if a student _does_ specify files, the CLI should use those and ignore the `.meta/config.json` file.

Also note that the `solution` property is an array, so it can contain multiple files (which should all be submitted).

Pull request description:
Submit without specifying files
Fixes #991

Adds the possibility of submitting without specifying files, with the "algorithm" specified in https://github.com/exercism/cli/issues/991#issuecomment-939768772

Other than the tests in the PR, I also tried to submit solutions using this version of the program, including messing up the exercise directory and everything went as expected :)

Patch:
diff --git a/cmd/submit.go b/cmd/submit.go
index 4f2a8df..aaf3a93 100644
--- a/cmd/submit.go
+++ b/cmd/submit.go
@@ -26,7 +26,6 @@ var submitCmd = &cobra.Command{
 
     Call the command with the list of files you want to submit.
 `,
-	Args: cobra.MinimumNArgs(1),
 	RunE: func(cmd *cobra.Command, args []string) error {
 		cfg := config.NewConfig()
 
@@ -45,6 +44,14 @@ var submitCmd = &cobra.Command{
 		// Ignore error. If the file doesn't exist, that is fine.
 		_ = v.ReadInConfig()
 
+		if len(args) == 0 {
+			files, err := getExerciseSolutionFiles(".")
+			if err != nil {
+				return err
+			}
+			args = files
+		}
+
 		return runSubmit(cfg, cmd.Flags(), args)
 	},
 }
@@ -114,6 +121,23 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error {
 	return nil
 }
 
+func getExerciseSolutionFiles(baseDir string) ([]string, error) {
+	v := viper.New()
+	v.AddConfigPath(filepath.Join(baseDir, ".exercism"))
+	v.SetConfigName("config")
+	v.SetConfigType("json")
+	err := v.ReadInConfig()
+	if err != nil {
+		return nil, errors.New("no files to submit")
+	}
+	solutionFiles := v.GetStringSlice("files.solution")
+	if len(solutionFiles) == 0 {
+		return nil, errors.New("no files to submit")
+	}
+
+	return solutionFiles, nil
+}
+
 type submitCmdContext struct {
 	usrCfg    *viper.Viper
 	flags     *pflag.FlagSet


Test patch:
diff --git a/cmd/submit_test.go b/cmd/submit_test.go
index c20c477..7286a19 100644
--- a/cmd/submit_test.go
+++ b/cmd/submit_test.go
@@ -108,6 +108,45 @@ func TestSubmitExerciseWithoutMetadataFile(t *testing.T) {
 	}
 }
 
+func TestGetExerciseSolutionFiles(t *testing.T) {
+
+	tmpDir, err := ioutil.TempDir("", "dir-with-no-metadata")
+	defer os.RemoveAll(tmpDir)
+	assert.NoError(t, err)
+
+	_, err = getExerciseSolutionFiles(tmpDir)
+	if assert.Error(t, err) {
+		assert.Regexp(t, "no files to submit", err.Error())
+	}
+
+	validTmpDir, err := ioutil.TempDir("", "dir-with-valid-metadata")
+	defer os.RemoveAll(validTmpDir)
+	assert.NoError(t, err)
+
+	metadataDir := filepath.Join(validTmpDir, ".exercism")
+	err = os.MkdirAll(metadataDir, os.FileMode(0755))
+	assert.NoError(t, err)
+
+	err = ioutil.WriteFile(
+		filepath.Join(metadataDir, "config.json"),
+		[]byte(`
+{
+	"files": {
+		"solution": [
+		  "expenses.go"
+		]
+  	}
+}
+`), os.FileMode(0755))
+	assert.NoError(t, err)
+
+	files, err := getExerciseSolutionFiles(validTmpDir)
+	assert.NoError(t, err)
+	if assert.Equal(t, len(files), 1) {
+		assert.Equal(t, files[0], "expenses.go")
+	}
+}
+
 func TestSubmitFilesAndDir(t *testing.T) {
 	tmpDir, err := ioutil.TempDir("", "submit-no-such-file")
 	defer os.RemoveAll(tmpDir)