Repository: exercism/cli
Pull request #453 (2017-08-18 05:58:44)

[nextercism] Determine the root directory of a solution.
Given a file or a directory, figure out what directory is the exercise's root directory.
This might be the given directory, or it could be a parent any number of
levels up in the directory tree.

We need this for the new submit command, since we're being a lot more flexible about how people specify the solution to be submitted. They might tell us the path to a directory. Or they might give us a list of files.

In order to submit a solution, we need to check the metadata, which is stored in the root of the exercise directory—so if they're submitting a list of specific files, we need to figure out what the common root is. Since we allow arbitrarily nested subdirectories, we need to check all the parent directories until we get to one that has a solution metadata file. If we find multiple parents, then we bail with an error.


Patch:
diff --git a/fixtures/solution-dir/file.txt b/fixtures/solution-dir/file.txt
new file mode 100644
index 0000000..e69de29
diff --git a/fixtures/solution-dir/workspace/exercise/.solution.json b/fixtures/solution-dir/workspace/exercise/.solution.json
new file mode 100644
index 0000000..e69de29
diff --git a/fixtures/solution-dir/workspace/exercise/file.txt b/fixtures/solution-dir/workspace/exercise/file.txt
new file mode 100644
index 0000000..e69de29
diff --git a/fixtures/solution-dir/workspace/exercise/in/a/subdir/file.txt b/fixtures/solution-dir/workspace/exercise/in/a/subdir/file.txt
new file mode 100644
index 0000000..e69de29
diff --git a/fixtures/solution-dir/workspace/not-exercise/file.txt b/fixtures/solution-dir/workspace/not-exercise/file.txt
new file mode 100644
index 0000000..e69de29
diff --git a/workspace/workspace.go b/workspace/workspace.go
index ac6f072..f7f3d69 100644
--- a/workspace/workspace.go
+++ b/workspace/workspace.go
@@ -1,6 +1,7 @@
 package workspace
 
 import (
+	"errors"
 	"fmt"
 	"os"
 	"path/filepath"
@@ -162,3 +163,26 @@ func (ws Workspace) ResolveSolutionPath(paths []string, exercise, solutionID str
 		path = fmt.Sprintf("%s-%d", root, suffix)
 	}
 }
+
+// SolutionDir determines the root directory of a solution.
+// This is the directory that contains the solution metadata file.
+func (ws Workspace) SolutionDir(s string) (string, error) {
+	if !strings.HasPrefix(s, ws.Dir) {
+		return "", errors.New("not in workspace")
+	}
+
+	path := s
+	for {
+		if path == ws.Dir {
+			return "", errors.New("couldn't find it")
+		}
+		if _, err := os.Lstat(path); os.IsNotExist(err) {
+			return "", err
+		}
+		if _, err := os.Lstat(filepath.Join(path, solutionFilename)); err == nil {
+			return path, nil
+		}
+		path = filepath.Dir(path)
+	}
+	return "", nil
+}


Test patch:
diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go
index 6c3b861..608ba71 100644
--- a/workspace/workspace_test.go
+++ b/workspace/workspace_test.go
@@ -279,3 +279,53 @@ func TestResolveSolutionPath(t *testing.T) {
 		assert.Equal(t, test.expected, path, test.desc)
 	}
 }
+
+func TestSolutionDir(t *testing.T) {
+	_, cwd, _, _ := runtime.Caller(0)
+	root := filepath.Join(cwd, "..", "..", "fixtures", "solution-dir")
+
+	ws := New(filepath.Join(root, "workspace"))
+
+	tests := []struct {
+		path string
+		ok   bool
+	}{
+		{
+			path: filepath.Join(ws.Dir, "exercise"),
+			ok:   true,
+		},
+		{
+			path: filepath.Join(ws.Dir, "exercise", "file.txt"),
+			ok:   true,
+		},
+		{
+			path: filepath.Join(ws.Dir, "exercise", "in", "a", "subdir", "file.txt"),
+			ok:   true,
+		},
+		{
+			path: filepath.Join(ws.Dir, "exercise", "in", "a"),
+			ok:   true,
+		},
+		{
+			path: filepath.Join(ws.Dir, "not-exercise", "file.txt"),
+			ok:   false,
+		},
+		{
+			path: filepath.Join(root, "file.txt"),
+			ok:   false,
+		},
+		{
+			path: filepath.Join(ws.Dir, "exercise", "no-such-file.txt"),
+			ok:   false,
+		},
+	}
+
+	for _, test := range tests {
+		dir, err := ws.SolutionDir(test.path)
+		if !test.ok {
+			assert.Error(t, err, test.path)
+			continue
+		}
+		assert.Equal(t, filepath.Join(ws.Dir, "exercise"), dir, test.path)
+	}
+}