Repository: exercism/cli
Pull request #705 (2018-08-11 22:21:43)

Distinguish between path and filepath
Use a normalized path relative to the exercise directory and using forward slashes when submitting to the server. This is converted to a proper filepath on the file system, using `/` or `\` depending on the operating system.

This builds on top of the pull request in https://github.com/exercism/cli/pull/703 -- too look at just the difference between the two branches, compare [no-leading-slash to path-vs-filepath](https://github.com/exercism/cli/compare/no-leading-slash...path-vs-filepath?expand=1).

Closes https://github.com/exercism/cli/issues/698
Closes https://github.com/exercism/cli/issues/665


Patch:
diff --git a/cmd/submit.go b/cmd/submit.go
index e468fe7..9952b5f 100644
--- a/cmd/submit.go
+++ b/cmd/submit.go
@@ -8,7 +8,6 @@ import (
 	"mime/multipart"
 	"os"
 	"path/filepath"
-	"strings"
 
 	"github.com/exercism/cli/api"
 	"github.com/exercism/cli/config"
@@ -125,6 +124,8 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error {
 		exerciseDir = dir
 	}
 
+	exercise := workspace.NewExerciseFromDir(exerciseDir)
+
 	solution, err := workspace.NewSolution(exerciseDir)
 	if err != nil {
 		return err
@@ -143,7 +144,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error {
 		return fmt.Errorf(msg, BinaryName, solution.Exercise, solution.Track)
 	}
 
-	paths := make([]string, 0, len(args))
+	exercise.Documents = make([]workspace.Document, 0, len(args))
 	for _, file := range args {
 		// Don't submit empty files
 		info, err := os.Stat(file)
@@ -161,10 +162,14 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error {
 			fmt.Fprintf(Err, msg, file)
 			continue
 		}
-		paths = append(paths, file)
+		doc, err := workspace.NewDocument(exercise.Filepath(), file)
+		if err != nil {
+			return err
+		}
+		exercise.Documents = append(exercise.Documents, doc)
 	}
 
-	if len(paths) == 0 {
+	if len(exercise.Documents) == 0 {
 		msg := `
 
     No files found to submit.
@@ -176,18 +181,14 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error {
 	body := &bytes.Buffer{}
 	writer := multipart.NewWriter(body)
 
-	for _, path := range paths {
-		file, err := os.Open(path)
+	for _, doc := range exercise.Documents {
+		file, err := os.Open(doc.Filepath())
 		if err != nil {
 			return err
 		}
 		defer file.Close()
 
-		dirname := fmt.Sprintf("%s%s%s", string(os.PathSeparator), solution.Exercise, string(os.PathSeparator))
-		pieces := strings.Split(path, dirname)
-		filename := pieces[len(pieces)-1]
-
-		part, err := writer.CreateFormFile("files[]", filename)
+		part, err := writer.CreateFormFile("files[]", doc.Path())
 		if err != nil {
 			return err
 		}
diff --git a/workspace/document.go b/workspace/document.go
new file mode 100644
index 0000000..52b1264
--- /dev/null
+++ b/workspace/document.go
@@ -0,0 +1,34 @@
+package workspace
+
+import "path/filepath"
+
+// Document is a file in a directory.
+type Document struct {
+	Root         string
+	RelativePath string
+}
+
+// NewDocument creates a document from the filepath.
+// The root is typically the root of the exercise, and
+// path is the absolute path to the file.
+func NewDocument(root, path string) (Document, error) {
+	path, err := filepath.Rel(root, path)
+	if err != nil {
+		return Document{}, err
+	}
+	return Document{
+		Root:         root,
+		RelativePath: path,
+	}, nil
+}
+
+// Filepath is the absolute path to the document on the filesystem.
+func (doc Document) Filepath() string {
+	return filepath.Join(doc.Root, doc.RelativePath)
+}
+
+// Path is the normalized path.
+// It uses forward slashes regardless of the operating system.
+func (doc Document) Path() string {
+	return filepath.ToSlash(doc.RelativePath)
+}
diff --git a/workspace/exercise.go b/workspace/exercise.go
index 246cf36..9c3815e 100644
--- a/workspace/exercise.go
+++ b/workspace/exercise.go
@@ -8,9 +8,19 @@ import (
 
 // Exercise is an implementation of a problem in a track.
 type Exercise struct {
-	Root  string
-	Track string
-	Slug  string
+	Root      string
+	Track     string
+	Slug      string
+	Documents []Document
+}
+
+// NewExerciseFromDir constructs an exercise given the exercise directory.
+func NewExerciseFromDir(dir string) Exercise {
+	slug := filepath.Base(dir)
+	dir = filepath.Dir(dir)
+	track := filepath.Base(dir)
+	root := filepath.Dir(dir)
+	return Exercise{Root: root, Track: track, Slug: slug}
 }
 
 // Path is the normalized relative path.


Test patch:
diff --git a/cmd/submit_test.go b/cmd/submit_test.go
index f4b2e78..556c3d3 100644
--- a/cmd/submit_test.go
+++ b/cmd/submit_test.go
@@ -180,7 +180,7 @@ func TestSubmitFiles(t *testing.T) {
 	assert.Equal(t, 3, len(submittedFiles))
 
 	assert.Equal(t, "This is file 1.", submittedFiles["file-1.txt"])
-	assert.Equal(t, "This is file 2.", submittedFiles[filepath.Join("subdir", "file-2.txt")])
+	assert.Equal(t, "This is file 2.", submittedFiles["subdir/file-2.txt"])
 	assert.Equal(t, "This is the readme.", submittedFiles["README.md"])
 }
 
@@ -278,7 +278,7 @@ func TestSubmitFilesForTeamExercise(t *testing.T) {
 	assert.Equal(t, 2, len(submittedFiles))
 
 	assert.Equal(t, "This is file 1.", submittedFiles["file-1.txt"])
-	assert.Equal(t, "This is file 2.", submittedFiles[filepath.Join("subdir", "file-2.txt")])
+	assert.Equal(t, "This is file 2.", submittedFiles["subdir/file-2.txt"])
 }
 
 func TestSubmitOnlyEmptyFile(t *testing.T) {
diff --git a/workspace/document_test.go b/workspace/document_test.go
new file mode 100644
index 0000000..4e055df
--- /dev/null
+++ b/workspace/document_test.go
@@ -0,0 +1,43 @@
+package workspace
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNormalizedDocumentPath(t *testing.T) {
+	root, err := ioutil.TempDir("", "docpath")
+	assert.NoError(t, err)
+	defer os.RemoveAll(root)
+
+	err = os.MkdirAll(filepath.Join(root, "subdirectory"), os.FileMode(0755))
+	assert.NoError(t, err)
+
+	testCases := []struct {
+		filepath string
+		path     string
+	}{
+		{
+			filepath: filepath.Join(root, "file.txt"),
+			path:     "file.txt",
+		},
+		{
+			filepath: filepath.Join(root, "subdirectory", "file.txt"),
+			path:     "subdirectory/file.txt",
+		},
+	}
+
+	for _, tc := range testCases {
+		err = ioutil.WriteFile(tc.filepath, []byte("a file"), os.FileMode(0600))
+		assert.NoError(t, err)
+
+		doc, err := NewDocument(root, tc.filepath)
+		assert.NoError(t, err)
+
+		assert.Equal(t, doc.Path(), tc.path)
+	}
+}
diff --git a/workspace/exercise_test.go b/workspace/exercise_test.go
index 83b7940..c3d54a5 100644
--- a/workspace/exercise_test.go
+++ b/workspace/exercise_test.go
@@ -33,3 +33,12 @@ func TestHasMetadata(t *testing.T) {
 	assert.NoError(t, err)
 	assert.False(t, ok)
 }
+
+func TestNewFromDir(t *testing.T) {
+	dir := filepath.Join("something", "another", "whatever", "the-track", "the-exercise")
+
+	exercise := NewExerciseFromDir(dir)
+	assert.Equal(t, filepath.Join("something", "another", "whatever"), exercise.Root)
+	assert.Equal(t, "the-track", exercise.Track)
+	assert.Equal(t, "the-exercise", exercise.Slug)
+}