From 3e0c96e379c86db21bc5a357988fa132d5387f5e Mon Sep 17 00:00:00 2001 From: Fabien Tregan Date: Wed, 13 Jun 2018 15:12:03 +0200 Subject: [PATCH 001/544] Avoids ending Println parameter with new lines. --- debug/debug.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/debug/debug.go b/debug/debug.go index 1a0eb417a..e337d12b5 100644 --- a/debug/debug.go +++ b/debug/debug.go @@ -47,7 +47,8 @@ func DumpRequest(req *http.Request) { Println("\n========================= BEGIN DumpRequest =========================") Println(string(dump)) - Println("========================= END DumpRequest =========================\n") + Println("========================= END DumpRequest =========================") + Println("") req.Body = ioutil.NopCloser(&bodyCopy) } @@ -68,7 +69,8 @@ func DumpResponse(res *http.Response) { Println("\n========================= BEGIN DumpResponse =========================") Println(string(dump)) - Println("========================= END DumpResponse =========================\n") + Println("========================= END DumpResponse =========================") + Println("") res.Body = ioutil.NopCloser(&bodyCopy) } From eb19ecd50ea27b7943b42f768227527ce4878f82 Mon Sep 17 00:00:00 2001 From: Fabien Tregan <2663407+FabienTregan@users.noreply.github.com> Date: Tue, 19 Jun 2018 13:18:56 +0200 Subject: [PATCH 002/544] Remove trailing carriage returns from answers (#559) `comms.Question` removes the tailing '\n' (added when pressing enter), hence callers expect a result where the result of pressing "enter" when answering is removed. However, on Windows, pressing enter adds not only `\n` but `\r\n`, causing the code that compare the output with a given string work only on unix and ios. (causing, e.g. #558) --- comms/question.go | 2 +- comms/question_test.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/comms/question.go b/comms/question.go index 494045b5e..f9afd6a1d 100644 --- a/comms/question.go +++ b/comms/question.go @@ -22,7 +22,7 @@ func (q Question) Read(r io.Reader) (string, error) { if err != nil { return "", err } - s = strings.Trim(s, "\n") + s = strings.TrimSpace(s) if s == "" { return q.DefaultValue, nil } diff --git a/comms/question_test.go b/comms/question_test.go index f7721ae0d..f7746eab8 100644 --- a/comms/question_test.go +++ b/comms/question_test.go @@ -17,6 +17,9 @@ func TestQuestion(t *testing.T) { }{ {"records interactive response", "hello\n", "", "hello"}, {"responds with default if response is empty", "\n", "Fine.", "Fine."}, + {"removes trailing \\r in addition to trailing \\", "hello\r\n", "Fine.", "hello"}, + {"removes trailing white spaces", "hello \n", "Fine.", "hello"}, + {"falls back to default value", " \n", "Default", "Default"}, } for _, test := range tests { q := &Question{ From 818bc53b296ff6c1f50d95d1b8029c2715150f0b Mon Sep 17 00:00:00 2001 From: Fabien Tregan <2663407+FabienTregan@users.noreply.github.com> Date: Thu, 21 Jun 2018 15:18:26 +0200 Subject: [PATCH 003/544] Break out test into subtests. (#540) This allow the test tools to show individuals reports. * Breaks out TestConfigure into subtests. * Breaks out TestPathType into subtests. * Breaks out TestNewRequestSetsDefaultHeaders into subtests. * Breaks out TestIsUpToDate into subtests. * Breaks out TestUpgrade into subtests. * Breaks out TestVersionUpdateCheck into subtests. * Breaks out TestQuestion into subtests. * Breaks out TestSelectionPick into subtests. * Breaks out TestTrackIgnoreString into subtests. * Breaks out TestNormalizeWorkspace into subtests. * Breaks out TestSolutionString into subtests There is currently no description for the tests: only in and expected out values, * Breaks out TestNewTransmission into subtests. * Breaks out TestLocateErrors into subtests. * Breaks out TestDownload into subtests. * Refactors TestDownload. --- api/client_test.go | 21 ++-- cli/cli_test.go | 29 +++--- cmd/configure_test.go | 59 ++++++----- cmd/download_test.go | 157 ++++++++++++++++------------- cmd/upgrade_test.go | 14 +-- cmd/version_test.go | 27 ++--- comms/question_test.go | 24 +++-- comms/selection_test.go | 26 ++--- config/track_test.go | 20 +++- config/user_config_test.go | 13 ++- config/user_config_windows_test.go | 14 ++- workspace/path_type_test.go | 29 ++++-- workspace/solution_test.go | 18 ++-- workspace/transmission_test.go | 26 ++--- workspace/workspace_test.go | 10 +- 15 files changed, 281 insertions(+), 206 deletions(-) diff --git a/api/client_test.go b/api/client_test.go index 4f3c6f9e7..ba069b738 100644 --- a/api/client_test.go +++ b/api/client_test.go @@ -18,19 +18,20 @@ func TestNewRequestSetsDefaultHeaders(t *testing.T) { UserAgent = "BogusAgent" - tests := []struct { + testCases := []struct { + desc string client *Client auth string contentType string }{ { - // Use defaults. + desc: "User defaults", client: &Client{}, auth: "", contentType: "application/json", }, { - // Override defaults. + desc: "Override defaults", client: &Client{ UserConfig: &config.UserConfig{Token: "abc123"}, ContentType: "bogus", @@ -40,12 +41,14 @@ func TestNewRequestSetsDefaultHeaders(t *testing.T) { }, } - for _, test := range tests { - req, err := test.client.NewRequest("GET", ts.URL, nil) - assert.NoError(t, err) - assert.Equal(t, "BogusAgent", req.Header.Get("User-Agent")) - assert.Equal(t, test.contentType, req.Header.Get("Content-Type")) - assert.Equal(t, test.auth, req.Header.Get("Authorization")) + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + req, err := tc.client.NewRequest("GET", ts.URL, nil) + assert.NoError(t, err) + assert.Equal(t, "BogusAgent", req.Header.Get("User-Agent")) + assert.Equal(t, tc.contentType, req.Header.Get("Content-Type")) + assert.Equal(t, tc.auth, req.Header.Get("Authorization")) + }) } } diff --git a/cli/cli_test.go b/cli/cli_test.go index b7968a08c..c9e71e37c 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -10,46 +10,49 @@ import ( ) func TestIsUpToDate(t *testing.T) { - tests := []struct { + testCases := []struct { + desc string cliVersion string releaseTag string ok bool }{ { - // It returns false for versions less than release. + desc: "It returns false for versions less than release.", cliVersion: "1.0.0", releaseTag: "v1.0.1", ok: false, }, { - // It returns false for pre-release versions of release. + desc: "It returns false for pre-release versions of release.", cliVersion: "1.0.1-alpha.1", releaseTag: "v1.0.1", ok: false, }, { - // It returns true for versions equal to release. + desc: "It returns true for versions equal to release.", cliVersion: "2.0.1", releaseTag: "v2.0.1", ok: true, }, { - // It returns true for versions greater than release. + desc: "It returns true for versions greater than release.", cliVersion: "2.0.2", releaseTag: "v2.0.1", ok: true, }, } - for _, test := range tests { - c := &CLI{ - Version: test.cliVersion, - LatestRelease: &Release{TagName: test.releaseTag}, - } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + c := &CLI{ + Version: tc.cliVersion, + LatestRelease: &Release{TagName: tc.releaseTag}, + } - ok, err := c.IsUpToDate() - assert.NoError(t, err) - assert.Equal(t, test.ok, ok, test.cliVersion) + ok, err := c.IsUpToDate() + assert.NoError(t, err) + assert.Equal(t, tc.ok, ok, tc.cliVersion) + }) } } diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 46bb326e7..d1d81ed99 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -7,16 +7,18 @@ import ( "github.com/stretchr/testify/assert" ) +type testCase struct { + desc string + args []string + existingUsrCfg *config.UserConfig + expectedUsrCfg *config.UserConfig + existingAPICfg *config.APIConfig + expectedAPICfg *config.APIConfig +} + func TestConfigure(t *testing.T) { - tests := []struct { - desc string - args []string - existingUsrCfg *config.UserConfig - expectedUsrCfg *config.UserConfig - existingAPICfg *config.APIConfig - expectedAPICfg *config.APIConfig - }{ - { + testCases := []testCase{ + testCase{ desc: "It writes the flags when there is no config file.", args: []string{"fakeapp", "configure", "--token", "a", "--workspace", "/a", "--api", "http://example.com"}, existingUsrCfg: nil, @@ -24,7 +26,7 @@ func TestConfigure(t *testing.T) { existingAPICfg: nil, expectedAPICfg: &config.APIConfig{BaseURL: "http://example.com"}, }, - { + testCase{ desc: "It overwrites the flags in the config file.", args: []string{"fakeapp", "configure", "--token", "b", "--workspace", "/b", "--api", "http://example.com/v2"}, existingUsrCfg: &config.UserConfig{Token: "token-b", Workspace: "/workspace-b"}, @@ -32,13 +34,13 @@ func TestConfigure(t *testing.T) { existingAPICfg: &config.APIConfig{BaseURL: "http://example.com/v1"}, expectedAPICfg: &config.APIConfig{BaseURL: "http://example.com/v2"}, }, - { + testCase{ desc: "It overwrites the flags that are passed, without losing the ones that are not.", args: []string{"fakeapp", "configure", "--token", "c"}, existingUsrCfg: &config.UserConfig{Token: "token-c", Workspace: "/workspace-c"}, expectedUsrCfg: &config.UserConfig{Token: "c", Workspace: "/workspace-c"}, }, - { + testCase{ desc: "It gets the default API base URL.", args: []string{"fakeapp", "configure"}, existingAPICfg: &config.APIConfig{}, @@ -46,37 +48,44 @@ func TestConfigure(t *testing.T) { }, } - for _, test := range tests { + for _, tc := range testCases { + t.Run(tc.desc, makeTest(tc)) + } +} + +func makeTest(tc testCase) func(*testing.T) { + + return func(t *testing.T) { cmdTest := &CommandTest{ Cmd: configureCmd, InitFn: initConfigureCmd, - Args: test.args, + Args: tc.args, } cmdTest.Setup(t) defer cmdTest.Teardown(t) - if test.existingUsrCfg != nil { + if tc.existingUsrCfg != nil { // Write a fake config. cfg := config.NewEmptyUserConfig() - cfg.Token = test.existingUsrCfg.Token - cfg.Workspace = test.existingUsrCfg.Workspace + cfg.Token = tc.existingUsrCfg.Token + cfg.Workspace = tc.existingUsrCfg.Workspace err := cfg.Write() - assert.NoError(t, err, test.desc) + assert.NoError(t, err, tc.desc) } cmdTest.App.Execute() - if test.expectedUsrCfg != nil { + if tc.expectedUsrCfg != nil { usrCfg, err := config.NewUserConfig() - assert.NoError(t, err, test.desc) - assert.Equal(t, test.expectedUsrCfg.Token, usrCfg.Token, test.desc) - assert.Equal(t, test.expectedUsrCfg.Workspace, usrCfg.Workspace, test.desc) + assert.NoError(t, err, tc.desc) + assert.Equal(t, tc.expectedUsrCfg.Token, usrCfg.Token, tc.desc) + assert.Equal(t, tc.expectedUsrCfg.Workspace, usrCfg.Workspace, tc.desc) } - if test.expectedAPICfg != nil { + if tc.expectedAPICfg != nil { apiCfg, err := config.NewAPIConfig() - assert.NoError(t, err, test.desc) - assert.Equal(t, test.expectedAPICfg.BaseURL, apiCfg.BaseURL, test.desc) + assert.NoError(t, err, tc.desc) + assert.Equal(t, tc.expectedAPICfg.BaseURL, apiCfg.BaseURL, tc.desc) } } } diff --git a/cmd/download_test.go b/cmd/download_test.go index 5129de6ea..406fad628 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -13,9 +13,37 @@ import ( "github.com/stretchr/testify/assert" ) +const payloadTemplate = ` +{ + "solution": { + "id": "bogus-id", + "user": { + "handle": "alice", + "is_requester": true + }, + "exercise": { + "id": "bogus-exercise", + "instructions_url": "http://example.com/bogus-exercise", + "auto_approve": false, + "track": { + "id": "bogus-track", + "language": "Bogus Language" + } + }, + "file_download_base_url": "%s", + "files": [ + "%s", + "%s", + "%s" + ], + "iteration": { + "submitted_at": "2017-08-21t10:11:12.130z" + } + } +} +` + func TestDownload(t *testing.T) { - // Let's not actually print to standard out while testing. - Out = ioutil.Discard cmdTest := &CommandTest{ Cmd: downloadCmd, @@ -25,45 +53,66 @@ func TestDownload(t *testing.T) { cmdTest.Setup(t) defer cmdTest.Teardown(t) - // Write a fake user config setting the workspace to the temp dir. - userCfg := config.NewEmptyUserConfig() - userCfg.Workspace = cmdTest.TmpDir - err := userCfg.Write() + mockServer := makeMockServer() + defer mockServer.Close() + + err := writeFakeUserConfigSetting(cmdTest.TmpDir) + assert.NoError(t, err) + err = writeFakeAPIConfigSetting(mockServer.URL) assert.NoError(t, err) - payloadBody := ` - { - "solution": { - "id": "bogus-id", - "user": { - "handle": "alice", - "is_requester": true - }, - "exercise": { - "id": "bogus-exercise", - "instructions_url": "http://example.com/bogus-exercise", - "auto_approve": false, - "track": { - "id": "bogus-track", - "language": "Bogus Language" - } - }, - "file_download_base_url": "%s", - "files": [ - "%s", - "%s", - "%s" - ], - "iteration": { - "submitted_at": "2017-08-21t10:11:12.130z" - } - } + testCases := []struct { + desc string + path string + contents string + }{ + { + desc: "It should download a file.", + path: filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", "file-1.txt"), + contents: "this is file 1", + }, + { + desc: "It should download a file in a subdir.", + path: filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", "subdir", "file-2.txt"), + contents: "this is file 2", + }, + { + desc: "It creates the .solution.json file.", + path: filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", ".solution.json"), + contents: `{"track":"bogus-track","exercise":"bogus-exercise","id":"bogus-id","url":"","handle":"alice","is_requester":true,"auto_approve":false}`, + }, + } + + cmdTest.App.Execute() + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + b, err := ioutil.ReadFile(tc.path) + assert.NoError(t, err) + assert.Equal(t, tc.contents, string(b)) + }) } - ` + path := filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", "file-3.txt") + _, err = os.Lstat(path) + assert.True(t, os.IsNotExist(err), "It should not write the file if empty.") +} + +func writeFakeUserConfigSetting(tmpDirPath string) error { + userCfg := config.NewEmptyUserConfig() + userCfg.Workspace = tmpDirPath + return userCfg.Write() +} + +func writeFakeAPIConfigSetting(serverURL string) error { + apiCfg := config.NewEmptyAPIConfig() + apiCfg.BaseURL = serverURL + return apiCfg.Write() +} + +func makeMockServer() *httptest.Server { mux := http.NewServeMux() server := httptest.NewServer(mux) - defer server.Close() path1 := "file-1.txt" mux.HandleFunc("/"+path1, func(w http.ResponseWriter, r *http.Request) { @@ -80,45 +129,11 @@ func TestDownload(t *testing.T) { fmt.Fprint(w, "") }) - payloadBody = fmt.Sprintf(payloadBody, server.URL+"/", path1, path2, path3) + payloadBody := fmt.Sprintf(payloadTemplate, server.URL+"/", path1, path2, path3) mux.HandleFunc("/solutions/latest", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, payloadBody) }) - // Write a fake api config setting the base url to the test server. - apiCfg := config.NewEmptyAPIConfig() - apiCfg.BaseURL = server.URL - err = apiCfg.Write() - assert.NoError(t, err) - - tests := []struct { - path string - contents string - }{ - { - path: filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", "file-1.txt"), - contents: "this is file 1", - }, - { - path: filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", "subdir", "file-2.txt"), - contents: "this is file 2", - }, - { - path: filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", ".solution.json"), - contents: `{"track":"bogus-track","exercise":"bogus-exercise","id":"bogus-id","url":"","handle":"alice","is_requester":true,"auto_approve":false}`, - }, - } - - cmdTest.App.Execute() - - for _, test := range tests { - b, err := ioutil.ReadFile(test.path) - assert.NoError(t, err) - assert.Equal(t, test.contents, string(b)) - } + return server - // It doesn't write the empty file. - path := filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", path3) - _, err = os.Lstat(path) - assert.True(t, os.IsNotExist(err)) } diff --git a/cmd/upgrade_test.go b/cmd/upgrade_test.go index 6c3459650..e2fd09496 100644 --- a/cmd/upgrade_test.go +++ b/cmd/upgrade_test.go @@ -26,7 +26,7 @@ func TestUpgrade(t *testing.T) { Out = ioutil.Discard defer func() { Out = oldOut }() - tests := []struct { + testCases := []struct { desc string upToDate bool expected bool @@ -43,11 +43,13 @@ func TestUpgrade(t *testing.T) { }, } - for _, test := range tests { - fc := &fakeCLI{UpToDate: test.upToDate} + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + fc := &fakeCLI{UpToDate: tc.upToDate} - err := updateCLI(fc) - assert.NoError(t, err) - assert.Equal(t, test.expected, fc.UpgradeCalled, test.desc) + err := updateCLI(fc) + assert.NoError(t, err) + assert.Equal(t, tc.expected, fc.UpgradeCalled) + }) } } diff --git a/cmd/version_test.go b/cmd/version_test.go index e2a55a141..6c8916282 100644 --- a/cmd/version_test.go +++ b/cmd/version_test.go @@ -25,36 +25,39 @@ func TestVersionUpdateCheck(t *testing.T) { defer ts.Close() cli.ReleaseURL = ts.URL - tests := []struct { + testCases := []struct { + desc string version string expected string }{ { - // It returns new version available for versions older than latest. + desc: "It returns new version available for versions older than latest.", version: "1.0.0", expected: "A new CLI version is available. Run `exercism upgrade` to update to 2.0.0", }, { - // It returns up to date for versions matching latest. + desc: "It returns up to date for versions matching latest.", version: "2.0.0", expected: "Your CLI version is up to date.", }, { - // It returns up to date for versions newer than latest. + desc: "It returns up to date for versions newer than latest.", version: "2.0.1", expected: "Your CLI version is up to date.", }, } - for _, test := range tests { - c := &cli.CLI{ - Version: test.version, - } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + c := &cli.CLI{ + Version: tc.version, + } - actual, err := checkForUpdate(c) + actual, err := checkForUpdate(c) - assert.NoError(t, err) - assert.NotEmpty(t, actual) - assert.Equal(t, test.expected, actual) + assert.NoError(t, err) + assert.NotEmpty(t, actual) + assert.Equal(t, tc.expected, actual) + }) } } diff --git a/comms/question_test.go b/comms/question_test.go index f7746eab8..1b77dad60 100644 --- a/comms/question_test.go +++ b/comms/question_test.go @@ -9,7 +9,7 @@ import ( ) func TestQuestion(t *testing.T) { - tests := []struct { + testCases := []struct { desc string given string fallback string @@ -21,16 +21,18 @@ func TestQuestion(t *testing.T) { {"removes trailing white spaces", "hello \n", "Fine.", "hello"}, {"falls back to default value", " \n", "Default", "Default"}, } - for _, test := range tests { - q := &Question{ - Reader: strings.NewReader(test.given), - Writer: ioutil.Discard, - Prompt: "Say something: ", - DefaultValue: test.fallback, - } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + q := &Question{ + Reader: strings.NewReader(tc.given), + Writer: ioutil.Discard, + Prompt: "Say something: ", + DefaultValue: tc.fallback, + } - answer, err := q.Ask() - assert.NoError(t, err) - assert.Equal(t, answer, test.expected, test.desc) + answer, err := q.Ask() + assert.NoError(t, err) + assert.Equal(t, answer, tc.expected) + }) } } diff --git a/comms/selection_test.go b/comms/selection_test.go index ad6bc552a..bbf2ce16e 100644 --- a/comms/selection_test.go +++ b/comms/selection_test.go @@ -78,7 +78,7 @@ func TestSelectionRead(t *testing.T) { } func TestSelectionPick(t *testing.T) { - tests := []struct { + testCases := []struct { desc string selection Selection things []thing @@ -111,16 +111,18 @@ func TestSelectionPick(t *testing.T) { }, } - for _, test := range tests { - test.selection.Writer = ioutil.Discard - for _, th := range test.things { - test.selection.Items = append(test.selection.Items, th) - } - - item, err := test.selection.Pick("which one? %s") - assert.NoError(t, err) - th, ok := item.(thing) - assert.True(t, ok) - assert.Equal(t, test.expected, th.name) + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + tc.selection.Writer = ioutil.Discard + for _, th := range tc.things { + tc.selection.Items = append(tc.selection.Items, th) + } + + item, err := tc.selection.Pick("which one? %s") + assert.NoError(t, err) + th, ok := item.(thing) + assert.True(t, ok) + assert.Equal(t, tc.expected, th.name) + }) } } diff --git a/config/track_test.go b/config/track_test.go index 8440e4866..55f79ecbb 100644 --- a/config/track_test.go +++ b/config/track_test.go @@ -14,16 +14,26 @@ func TestTrackIgnoreString(t *testing.T) { }, } - tests := map[string]bool{ + testCases := map[string]bool{ "falcon.txt": false, "beacon|txt": true, "beacon.ext": true, "proof": false, } - for name, acceptable := range tests { - ok, err := track.AcceptFilename(name) - assert.NoError(t, err, name) - assert.Equal(t, acceptable, ok, name) + for name, acceptable := range testCases { + testName := name + " should " + notIfNeeded(acceptable) + "be an acceptable name." + t.Run(testName, func(t *testing.T) { + ok, err := track.AcceptFilename(name) + assert.NoError(t, err, name) + assert.Equal(t, acceptable, ok, testName) + }) } } + +func notIfNeeded(b bool) string { + if !b { + return "not " + } + return "" +} diff --git a/config/user_config_test.go b/config/user_config_test.go index 9d65aa532..b3ce81c77 100644 --- a/config/user_config_test.go +++ b/config/user_config_test.go @@ -42,7 +42,7 @@ func TestNormalizeWorkspace(t *testing.T) { assert.NoError(t, err) cfg := &UserConfig{Home: "/home/alice"} - tests := []struct { + testCases := []struct { in, out string }{ {"", ""}, // don't make wild guesses @@ -54,9 +54,12 @@ func TestNormalizeWorkspace(t *testing.T) { {"relative///path", filepath.Join(cwd, "relative", "path")}, } - for _, test := range tests { - cfg.Workspace = test.in - cfg.Normalize() - assert.Equal(t, test.out, cfg.Workspace) + for _, tc := range testCases { + testName := "'" + tc.in + "' should be normalized as '" + tc.out + "'" + t.Run(testName, func(t *testing.T) { + cfg.Workspace = tc.in + cfg.Normalize() + assert.Equal(t, tc.out, cfg.Workspace, testName) + }) } } diff --git a/config/user_config_windows_test.go b/config/user_config_windows_test.go index 62c8255e9..5ea9a3e45 100644 --- a/config/user_config_windows_test.go +++ b/config/user_config_windows_test.go @@ -40,7 +40,7 @@ func TestNormalizeWorkspace(t *testing.T) { assert.NoError(t, err) cfg := &UserConfig{Home: "C:\\Users\\alice"} - tests := []struct { + testCases := []struct { in, out string }{ {"", ""}, // don't make wild guesses @@ -54,9 +54,13 @@ func TestNormalizeWorkspace(t *testing.T) { {"relative///path", filepath.Join(cwd, "relative", "path")}, } - for _, test := range tests { - cfg.Workspace = test.in - cfg.Normalize() - assert.Equal(t, test.out, cfg.Workspace) + for _, tc := range testCases { + testName := "'" + tc.in + "' should be normalized as '" + tc.out + "'" + + t.Run(testName, func(t *testing.T) { + cfg.Workspace = tc.in + cfg.Normalize() + assert.Equal(t, tc.out, cfg.Workspace, testName) + }) } } diff --git a/workspace/path_type_test.go b/workspace/path_type_test.go index 232634ca4..c233bcfe3 100644 --- a/workspace/path_type_test.go +++ b/workspace/path_type_test.go @@ -8,16 +8,18 @@ import ( "github.com/stretchr/testify/assert" ) +type testCase struct { + desc string + path string + pt PathType +} + func TestDetectPathType(t *testing.T) { _, cwd, _, _ := runtime.Caller(0) root := filepath.Join(cwd, "..", "..", "fixtures", "detect-path-type") - tests := []struct { - desc string - path string - pt PathType - }{ - { + testCases := []testCase{ + testCase{ desc: "absolute dir", path: filepath.Join(root, "a-dir"), pt: TypeDir, @@ -54,9 +56,16 @@ func TestDetectPathType(t *testing.T) { }, } - for _, test := range tests { - pt, err := DetectPathType(test.path) - assert.NoError(t, err, test.desc) - assert.Equal(t, test.pt, pt, test.desc) + for _, tc := range testCases { + t.Run(tc.desc, makeTest(tc)) + + } +} + +func makeTest(tc testCase) func(*testing.T) { + return func(t *testing.T) { + pt, err := DetectPathType(tc.path) + assert.NoError(t, err, tc.desc) + assert.Equal(t, tc.pt, pt, tc.desc) } } diff --git a/workspace/solution_test.go b/workspace/solution_test.go index fb621099f..7c1d1e064 100644 --- a/workspace/solution_test.go +++ b/workspace/solution_test.go @@ -43,7 +43,7 @@ func TestSolution(t *testing.T) { } func TestSuffix(t *testing.T) { - tests := []struct { + testCases := []struct { solution Solution suffix string }{ @@ -77,13 +77,16 @@ func TestSuffix(t *testing.T) { }, } - for _, test := range tests { - assert.Equal(t, test.suffix, test.solution.Suffix()) + for _, tc := range testCases { + testName := "Suffix of '" + tc.solution.Dir + "' should be " + tc.suffix + t.Run(testName, func(t *testing.T) { + assert.Equal(t, tc.suffix, tc.solution.Suffix(), testName) + }) } } func TestSolutionString(t *testing.T) { - tests := []struct { + testCases := []struct { solution Solution desc string }{ @@ -136,7 +139,10 @@ func TestSolutionString(t *testing.T) { }, } - for _, test := range tests { - assert.Equal(t, test.desc, test.solution.String()) + for _, tc := range testCases { + testName := "should stringify to '" + tc.desc + "'" + t.Run(testName, func(t *testing.T) { + assert.Equal(t, tc.desc, tc.solution.String()) + }) } } diff --git a/workspace/transmission_test.go b/workspace/transmission_test.go index 828d40a66..9faf7d69a 100644 --- a/workspace/transmission_test.go +++ b/workspace/transmission_test.go @@ -18,7 +18,7 @@ func TestNewTransmission(t *testing.T) { fileBird := filepath.Join(dirBird, "hummingbird.txt") fileSugar := filepath.Join(dirFeeder, "sugar.txt") - tests := []struct { + testCases := []struct { desc string args []string ok bool @@ -65,18 +65,20 @@ func TestNewTransmission(t *testing.T) { }, } - for _, test := range tests { - tx, err := NewTransmission(root, test.args) - if test.ok { - assert.NoError(t, err, test.desc) - } else { - assert.Error(t, err, test.desc) - } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + tx, err := NewTransmission(root, tc.args) + if tc.ok { + assert.NoError(t, err, tc.desc) + } else { + assert.Error(t, err, tc.desc) + } - if test.tx != nil { - assert.Equal(t, test.tx.Files, tx.Files, test.desc) - assert.Equal(t, test.tx.Dir, tx.Dir, test.desc) - } + if tc.tx != nil { + assert.Equal(t, tc.tx.Files, tx.Files) + assert.Equal(t, tc.tx.Dir, tx.Dir) + } + }) } } diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go index 608ba7173..e183dc0e5 100644 --- a/workspace/workspace_test.go +++ b/workspace/workspace_test.go @@ -16,7 +16,7 @@ func TestLocateErrors(t *testing.T) { ws := New(filepath.Join(root, "workspace")) - tests := []struct { + testCases := []struct { desc, arg string errFn func(error) bool }{ @@ -47,9 +47,11 @@ func TestLocateErrors(t *testing.T) { }, } - for _, test := range tests { - _, err := ws.Locate(test.arg) - assert.True(t, test.errFn(err), fmt.Sprintf("test: %s (arg: %s), %#v", test.desc, test.arg, err)) + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + _, err := ws.Locate(tc.arg) + assert.True(t, tc.errFn(err), fmt.Sprintf("test: %s (arg: %s), %#v", tc.desc, tc.arg, err)) + }) } } From 19ae7d5f3c7f6bffdfb3f4024491bfdd2aaeb64b Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 21 Jun 2018 19:21:11 -0600 Subject: [PATCH 004/544] Use the binary name as the config subdirectory name We need to be able to have multiple clients all working against different environments at the same time. If they all use a directory named 'exercism' for the config files, then running the configure command on any client will clobber the values of the other client. --- cmd/root.go | 2 ++ config/dir.go | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 48d21157f..214d53ef4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,6 +7,7 @@ import ( "runtime" "github.com/exercism/cli/api" + "github.com/exercism/cli/config" "github.com/exercism/cli/debug" "github.com/spf13/cobra" ) @@ -48,6 +49,7 @@ func Execute() { func init() { BinaryName = os.Args[0] + config.SubdirectoryName = BinaryName Out = os.Stdout In = os.Stdin api.UserAgent = fmt.Sprintf("github.com/exercism/cli v%s (%s/%s)", Version, runtime.GOOS, runtime.GOARCH) diff --git a/config/dir.go b/config/dir.go index 98cf75524..c4dddf220 100644 --- a/config/dir.go +++ b/config/dir.go @@ -6,6 +6,10 @@ import ( "runtime" ) +var ( + SubdirectoryName = "exercism" +) + // Dir is the configured config home directory. // All the cli-related config files live in this directory. func Dir() string { @@ -13,7 +17,7 @@ func Dir() string { if runtime.GOOS == "windows" { dir = os.Getenv("APPDATA") if dir != "" { - return filepath.Join(dir, "exercism") + return filepath.Join(dir, SubdirectoryName) } } else { dir := os.Getenv("EXERCISM_CONFIG_HOME") @@ -25,7 +29,7 @@ func Dir() string { dir = filepath.Join(os.Getenv("HOME"), ".config") } if dir != "" { - return filepath.Join(dir, "exercism") + return filepath.Join(dir, SubdirectoryName) } } // If all else fails, use the current directory. From df373f8d45b860760729ff7023f035e65ed5d922 Mon Sep 17 00:00:00 2001 From: Fabien Tregan Date: Sat, 9 Jun 2018 14:05:03 +0200 Subject: [PATCH 005/544] Calls Normalize() on expected result in configure_test so tests passes on windows. --- cmd/configure_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/configure_test.go b/cmd/configure_test.go index d1d81ed99..c94680e1d 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -1,6 +1,7 @@ package cmd import ( + "runtime" "testing" "github.com/exercism/cli/config" @@ -76,7 +77,12 @@ func makeTest(tc testCase) func(*testing.T) { cmdTest.App.Execute() if tc.expectedUsrCfg != nil { + if runtime.GOOS == "windows" { + tc.expectedUsrCfg.Normalize() + } + usrCfg, err := config.NewUserConfig() + assert.NoError(t, err, tc.desc) assert.Equal(t, tc.expectedUsrCfg.Token, usrCfg.Token, tc.desc) assert.Equal(t, tc.expectedUsrCfg.Workspace, usrCfg.Workspace, tc.desc) From 332a6733f1f926cb22a90083e3c43fdc0e4f8fc6 Mon Sep 17 00:00:00 2001 From: Fabien Tregan Date: Sat, 9 Jun 2018 14:05:53 +0200 Subject: [PATCH 006/544] Isolates the symlinks tests and ignores them under windows. --- workspace/path_type_symlinks_test.go | 31 ++++++++++++++++++++++++++++ workspace/path_type_test.go | 29 +++++++------------------- 2 files changed, 39 insertions(+), 21 deletions(-) create mode 100644 workspace/path_type_symlinks_test.go diff --git a/workspace/path_type_symlinks_test.go b/workspace/path_type_symlinks_test.go new file mode 100644 index 000000000..0b8421019 --- /dev/null +++ b/workspace/path_type_symlinks_test.go @@ -0,0 +1,31 @@ +// +build !windows + +package workspace + +import ( + "path/filepath" + "runtime" + "testing" +) + +func TestDetectPathTypeSymlink(t *testing.T) { + _, cwd, _, _ := runtime.Caller(0) + root := filepath.Join(cwd, "..", "..", "fixtures", "detect-path-type") + + testCases := []detectPathTestCase{ + { + desc: "symlinked dir", + path: filepath.Join(root, "symlinked-dir"), + pt: TypeDir, + }, + { + desc: "symlinked file", + path: filepath.Join(root, "symlinked-file.txt"), + pt: TypeFile, + }, + } + + for _, tc := range testCases { + testDetectPathType(t, tc) + } +} diff --git a/workspace/path_type_test.go b/workspace/path_type_test.go index c233bcfe3..28f84c9d3 100644 --- a/workspace/path_type_test.go +++ b/workspace/path_type_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) -type testCase struct { +type detectPathTestCase struct { desc string path string pt PathType @@ -18,8 +18,8 @@ func TestDetectPathType(t *testing.T) { _, cwd, _, _ := runtime.Caller(0) root := filepath.Join(cwd, "..", "..", "fixtures", "detect-path-type") - testCases := []testCase{ - testCase{ + testCases := []detectPathTestCase{ + detectPathTestCase{ desc: "absolute dir", path: filepath.Join(root, "a-dir"), pt: TypeDir, @@ -29,11 +29,6 @@ func TestDetectPathType(t *testing.T) { path: filepath.Join("..", "fixtures", "detect-path-type", "a-dir"), pt: TypeDir, }, - { - desc: "symlinked dir", - path: filepath.Join(root, "symlinked-dir"), - pt: TypeDir, - }, { desc: "absolute file", path: filepath.Join(root, "a-file.txt"), @@ -44,11 +39,6 @@ func TestDetectPathType(t *testing.T) { path: filepath.Join("..", "fixtures", "detect-path-type", "a-file.txt"), pt: TypeFile, }, - { - desc: "symlinked file", - path: filepath.Join(root, "symlinked-file.txt"), - pt: TypeFile, - }, { desc: "exercise ID", path: "a-file", @@ -57,15 +47,12 @@ func TestDetectPathType(t *testing.T) { } for _, tc := range testCases { - t.Run(tc.desc, makeTest(tc)) - + testDetectPathType(t, tc) } } -func makeTest(tc testCase) func(*testing.T) { - return func(t *testing.T) { - pt, err := DetectPathType(tc.path) - assert.NoError(t, err, tc.desc) - assert.Equal(t, tc.pt, pt, tc.desc) - } +func testDetectPathType(t *testing.T, tc detectPathTestCase) { + pt, err := DetectPathType(tc.path) + assert.NoError(t, err, tc.desc) + assert.Equal(t, tc.pt, pt, tc.desc) } From 88e8ffb43940e31542ca830d314da023d1394c60 Mon Sep 17 00:00:00 2001 From: Fabien Tregan Date: Sat, 9 Jun 2018 20:33:39 +0200 Subject: [PATCH 007/544] Delete config files between tests in TestConfigure. Some tests save configuration files. This configuration file prevented the default values from being loaded in subsequent tests, making one of them fail. --- cmd/configure_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/configure_test.go b/cmd/configure_test.go index c94680e1d..c8c35d489 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -1,6 +1,7 @@ package cmd import ( + "os" "runtime" "testing" @@ -92,6 +93,7 @@ func makeTest(tc testCase) func(*testing.T) { apiCfg, err := config.NewAPIConfig() assert.NoError(t, err, tc.desc) assert.Equal(t, tc.expectedAPICfg.BaseURL, apiCfg.BaseURL, tc.desc) + os.Remove(apiCfg.File()) } } } From 94720f7fe5d04bcfd79039edf9cafa8c621067ad Mon Sep 17 00:00:00 2001 From: Fabien Tregan Date: Sun, 10 Jun 2018 17:28:45 +0200 Subject: [PATCH 008/544] Delete configuration file before writing it in TestPrepareTrack so the filter values do not accumulate. --- cmd/prepare_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/prepare_test.go b/cmd/prepare_test.go index 93efd538d..ad4c239dc 100644 --- a/cmd/prepare_test.go +++ b/cmd/prepare_test.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "os" "testing" "github.com/exercism/cli/config" @@ -43,6 +44,7 @@ func TestPrepareTrack(t *testing.T) { cmdTest.App.Execute() cliCfg, err := config.NewCLIConfig() + os.Remove(cliCfg.File()) assert.NoError(t, err) expected := []string{ From b9cab7d3397a1b3b023760ef9c5b0277c8d11000 Mon Sep 17 00:00:00 2001 From: Fabien Tregan Date: Sun, 10 Jun 2018 18:07:31 +0200 Subject: [PATCH 009/544] Makes TestTransmissionWithRelativePath pass. Cleans the path to current working directory (expected value) in TestTransmissionWithRelativePath. This is required because runtime.Caller(0)->pwd won't return `C:\cwd\` but `C:/CWD/` when the tests are run from gitbash / mingw under Windows, making the test fail. --- workspace/transmission_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspace/transmission_test.go b/workspace/transmission_test.go index 9faf7d69a..2b874e9ad 100644 --- a/workspace/transmission_test.go +++ b/workspace/transmission_test.go @@ -93,6 +93,6 @@ func TestTransmissionWithRelativePath(t *testing.T) { file := filepath.Base(cwd) tx, err := NewTransmission(dir, []string{file}) if assert.NoError(t, err) { - assert.Equal(t, cwd, tx.Files[0]) + assert.Equal(t, filepath.Clean(cwd), tx.Files[0]) } } From 6a987da2a60663c495f8463e14d1813513b15290 Mon Sep 17 00:00:00 2001 From: Fabien Tregan Date: Wed, 13 Jun 2018 14:06:40 +0200 Subject: [PATCH 010/544] Avoids running symlinks related parts of TestWorkspace on Windows. --- workspace/workspace_symlinks_test.go | 49 ++++++++++++++++++++++++ workspace/workspace_test.go | 56 +++++++++------------------- 2 files changed, 66 insertions(+), 39 deletions(-) create mode 100644 workspace/workspace_symlinks_test.go diff --git a/workspace/workspace_symlinks_test.go b/workspace/workspace_symlinks_test.go new file mode 100644 index 000000000..370106133 --- /dev/null +++ b/workspace/workspace_symlinks_test.go @@ -0,0 +1,49 @@ +// +build !windows + +package workspace + +import ( + "path/filepath" + "runtime" + "testing" +) + +func TestLocateSymlinks(t *testing.T) { + _, cwd, _, _ := runtime.Caller(0) + root := filepath.Join(cwd, "..", "..", "fixtures", "locate-exercise") + + wsSymbolic := New(filepath.Join(root, "symlinked-workspace")) + wsPrimary := New(filepath.Join(root, "workspace")) + + testCases := []locateTestCase{ + { + desc: "find absolute path within symlinked workspace", + workspace: wsSymbolic, + in: filepath.Join(wsSymbolic.Dir, "creatures", "horse"), + out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, + }, + { + desc: "find by name in a symlinked workspace", + workspace: wsSymbolic, + in: "horse", + out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, + }, + { + desc: "don't be confused by a symlinked file named the same as an exercise", + workspace: wsPrimary, + in: "date", + out: []string{filepath.Join(wsPrimary.Dir, "actions", "date")}, + }, + { + desc: "find exercises that are symlinks", + workspace: wsPrimary, + in: "squash", + out: []string{ + filepath.Join(wsPrimary.Dir, "..", "food", "squash"), + filepath.Join(wsPrimary.Dir, "actions", "squash"), + }, + }, + } + + testLocate(testCases, t) +} diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go index e183dc0e5..81f6f7be8 100644 --- a/workspace/workspace_test.go +++ b/workspace/workspace_test.go @@ -55,31 +55,26 @@ func TestLocateErrors(t *testing.T) { } } +type locateTestCase struct { + desc string + workspace Workspace + in string + out []string +} + func TestLocate(t *testing.T) { _, cwd, _, _ := runtime.Caller(0) root := filepath.Join(cwd, "..", "..", "fixtures", "locate-exercise") wsPrimary := New(filepath.Join(root, "workspace")) - wsSymbolic := New(filepath.Join(root, "symlinked-workspace")) - tests := []struct { - desc string - workspace Workspace - in string - out []string - }{ + testCases := []locateTestCase{ { desc: "find absolute path within workspace", workspace: wsPrimary, in: filepath.Join(wsPrimary.Dir, "creatures", "horse"), out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, }, - { - desc: "find absolute path within symlinked workspace", - workspace: wsSymbolic, - in: filepath.Join(wsSymbolic.Dir, "creatures", "horse"), - out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, - }, { desc: "find relative path within workspace", workspace: wsPrimary, @@ -92,12 +87,6 @@ func TestLocate(t *testing.T) { in: "horse", out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, }, - { - desc: "find by name in a symlinked workspace", - workspace: wsSymbolic, - in: "horse", - out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, - }, { desc: "find by name in a subtree", workspace: wsPrimary, @@ -110,12 +99,6 @@ func TestLocate(t *testing.T) { in: "duck", out: []string{filepath.Join(wsPrimary.Dir, "creatures", "duck")}, }, - { - desc: "don't be confused by a symlinked file named the same as an exercise", - workspace: wsPrimary, - in: "date", - out: []string{filepath.Join(wsPrimary.Dir, "actions", "date")}, - }, { desc: "find all the exercises with the same name", workspace: wsPrimary, @@ -134,25 +117,20 @@ func TestLocate(t *testing.T) { filepath.Join(wsPrimary.Dir, "creatures", "crane-2"), }, }, - { - desc: "find exercises that are symlinks", - workspace: wsPrimary, - in: "squash", - out: []string{ - filepath.Join(wsPrimary.Dir, "..", "food", "squash"), - filepath.Join(wsPrimary.Dir, "actions", "squash"), - }, - }, } - for _, test := range tests { - dirs, err := test.workspace.Locate(test.in) + testLocate(testCases, t) +} + +func testLocate(testCases []locateTestCase, t *testing.T) { + for _, tc := range testCases { + dirs, err := tc.workspace.Locate(tc.in) sort.Strings(dirs) - sort.Strings(test.out) + sort.Strings(tc.out) - assert.NoError(t, err, test.desc) - assert.Equal(t, test.out, dirs, test.desc) + assert.NoError(t, err, tc.desc) + assert.Equal(t, tc.out, dirs, tc.desc) } } From 43b5da224de82af156b57323bd265a5646fff478 Mon Sep 17 00:00:00 2001 From: Fabien Tregan Date: Sat, 16 Jun 2018 23:25:54 +0200 Subject: [PATCH 011/544] Unhides the solution file before writting to it. Hidden files can't easily be written to from go under widnows*. This quick fix allowas two more tests to pass on windows. * : trying to investigate the problem with people on gophers slack, I was hinted this : Kale Blankenship [22 h 37] @Fabien I think this is due to how the Windows API works. > If CREATE_ALWAYS and FILE_ATTRIBUTE_NORMAL are specified, CreateFile > fails and sets the last error to ERROR_ACCESS_DENIED if the file exists > and has the FILE_ATTRIBUTE_HIDDEN or FILE_ATTRIBUTE_SYSTEM attribute. To > avoid the error, specify the same attributes as the existing file. https://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx The typical path sets `FILE_ATTRIBUTE_NORMAL` (https://github.com/golang/go/blob/master/src/syscall/syscall_windows.go#L289), which is probably a reasonable thing to do. From my brief reading of the docs, the way around it would be to first read the existing attributes before opening. Conceivably, you could work around it using `syscall.CreateFile` and `os.NewFile` yourself . But the flags to File.Open ( line 63 in https://golang.org/src/os/file.go ) do not seem to contain a bit for visibility. So for now I went with this hacky solution. The tests all pass on windows now. --- visibility/hide_file.go | 5 +++++ visibility/hide_file_windows.go | 16 +++++++++++++++- workspace/solution.go | 7 ++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/visibility/hide_file.go b/visibility/hide_file.go index 7d1687833..7fc015772 100644 --- a/visibility/hide_file.go +++ b/visibility/hide_file.go @@ -6,3 +6,8 @@ package visibility func HideFile(string) error { return nil } + +// ShowFile is a no-op for non-Windows systems. +func ShowFile(path string) error { + return nil +} diff --git a/visibility/hide_file_windows.go b/visibility/hide_file_windows.go index ab69889e3..c565e5703 100644 --- a/visibility/hide_file_windows.go +++ b/visibility/hide_file_windows.go @@ -6,6 +6,15 @@ import "syscall" // This is the equivalent of giving a filename on // Linux or MacOS a leading dot (e.g. .bash_rc). func HideFile(path string) error { + return setVisibility(path, false) +} + +// ShowFile unsets a Windows file's 'hidden' attribute. +func ShowFile(path string) error { + return setVisibility(path, true) +} + +func setVisibility(path string, visible bool) error { // This is based on the discussion in // https://www.reddit.com/r/golang/comments/5t3ezd/hidden_files_directories/ // but instead of duplicating all the effort to write the file, this takes @@ -22,6 +31,11 @@ func HideFile(path string) error { return err } - attributes |= syscall.FILE_ATTRIBUTE_HIDDEN + if visible { + attributes &^= syscall.FILE_ATTRIBUTE_HIDDEN + } else { + attributes |= syscall.FILE_ATTRIBUTE_HIDDEN + } + return syscall.SetFileAttributes(ptr, attributes) } diff --git a/workspace/solution.go b/workspace/solution.go index cf2656073..e0f8bbfa3 100644 --- a/workspace/solution.go +++ b/workspace/solution.go @@ -66,8 +66,13 @@ func (s *Solution) Write(dir string) error { if err != nil { return err } + path := filepath.Join(dir, solutionFilename) - if err := ioutil.WriteFile(path, b, os.FileMode(0644)); err != nil { + + // Hack because ioutil.WriteFile fails on hidden files + visibility.ShowFile(path) + + if err := ioutil.WriteFile(path, b, os.FileMode(0600)); err != nil { return err } s.Dir = dir From 232c712ebdb5c60cf8c51f29c1ecebf22741696c Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 21 Jun 2018 20:27:49 -0600 Subject: [PATCH 012/544] Add a method for inferring the site URL based on the API URL --- config/config.go | 11 +++++++++++ config/config_test.go | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/config/config.go b/config/config.go index 9318b9bda..d71f0da6d 100644 --- a/config/config.go +++ b/config/config.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path/filepath" + "regexp" "github.com/spf13/viper" ) @@ -60,3 +61,13 @@ func ensureDir(f filer) error { } return err } + +// InferSiteURL guesses what the website URL is. +// The basis for the guess is which API we're submitting to. +func InferSiteURL(apiURL string) string { + if apiURL == "https://api.exercism.io/v1" { + return "https://exercism.io" + } + re := regexp.MustCompile("^(https?://[^/]*).*") + return re.ReplaceAllString(apiURL, "$1") +} diff --git a/config/config_test.go b/config/config_test.go index 1fe086dfe..44808d50c 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -95,3 +95,19 @@ func TestFakeConfig(t *testing.T) { assert.Equal(t, "b", cfg.Letter) assert.Equal(t, 1, cfg.Number) } + +func TestInferSiteURL(t *testing.T) { + testCases := []struct { + api, url string + }{ + {"https://api.exercism.io/v1", "https://exercism.io"}, + {"https://v2.exercism.io/api/v1", "https://v2.exercism.io"}, + {"https://mentors-beta.exercism.io/api/v1", "https://mentors-beta.exercism.io"}, + {"http://localhost:3000/api/v1", "http://localhost:3000"}, + {"http://whatever", "http://whatever"}, // you're on your own, pal + } + + for _, tc := range testCases { + assert.Equal(t, InferSiteURL(tc.api), tc.url) + } +} From 4215d207ce5d4eabe32435a81740c6a1c037f180 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 21 Jun 2018 20:56:20 -0600 Subject: [PATCH 013/544] Use inferred site URL instead of hard-coding v2 site We need to be able to use the CLI in multiple environments (dev, staging, production). This is hacky, but I anticipate rewriting all the config logic shortly, so I'm OK with it for now. --- api/client.go | 3 ++- cli/status.go | 20 ++++++++++++++++---- cmd/configure.go | 3 ++- cmd/download.go | 4 ++-- cmd/root.go | 2 +- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/api/client.go b/api/client.go index 39671ac15..e71a291c4 100644 --- a/api/client.go +++ b/api/client.go @@ -87,7 +87,8 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { // TODO: if it's json, and it has an error key, print the message. return nil, fmt.Errorf("%s", res.Status) case http.StatusUnauthorized: - return nil, fmt.Errorf("unauthorized request. Please run the configure command to set the api token found at https://v2.exercism.io/my/settings") + siteURL := config.InferSiteURL(c.APIConfig.BaseURL) + return nil, fmt.Errorf("unauthorized request. Please run the configure command. You can find your API token at %s/my/settings", siteURL) default: if v != nil { defer res.Body.Close() diff --git a/cli/status.go b/cli/status.go index b9eded6b2..170becf14 100644 --- a/cli/status.go +++ b/cli/status.go @@ -69,7 +69,13 @@ func NewStatus(c *CLI, uc config.UserConfig) Status { func (status *Status) Check() (string, error) { status.Version = newVersionStatus(status.cli) status.System = newSystemStatus() - status.Configuration = newConfigurationStatus(status) + + cs, err := newConfigurationStatus(status) + if err != nil { + return "", err + } + status.Configuration = cs + ar, err := newAPIReachabilityStatus() if err != nil { return "", err @@ -94,6 +100,7 @@ func newAPIReachabilityStatus() (apiReachabilityStatus, error) { if err != nil { return apiReachabilityStatus{}, nil } + apiCfg.SetDefaults() ar := apiReachabilityStatus{ Services: []*apiPing{ {Service: "GitHub", URL: "https://api.github.com"}, @@ -137,18 +144,23 @@ func newSystemStatus() systemStatus { return ss } -func newConfigurationStatus(status *Status) configurationStatus { +func newConfigurationStatus(status *Status) (configurationStatus, error) { + apiCfg, err := config.NewAPIConfig() + if err != nil { + return configurationStatus{}, err + } + apiCfg.SetDefaults() cs := configurationStatus{ Home: status.cfg.Home, Workspace: status.cfg.Workspace, File: status.cfg.File(), Token: status.cfg.Token, - TokenURL: "http://exercism.io/account/key", + TokenURL: config.InferSiteURL(apiCfg.BaseURL) + "/my/settings", } if status.Censor && status.cfg.Token != "" { cs.Token = redactToken(status.cfg.Token) } - return cs + return cs, nil } func (ping *apiPing) Call(wg *sync.WaitGroup) { diff --git a/cmd/configure.go b/cmd/configure.go index fa9abd4c5..33d863063 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -39,6 +39,7 @@ You can also override certain default settings to suit your preferences. if err != nil { return err } + apiCfg.SetDefaults() show, err := cmd.Flags().GetBool("show") if err != nil { @@ -67,7 +68,7 @@ You can also override certain default settings to suit your preferences. } func initConfigureCmd() { - configureCmd.Flags().StringP("token", "t", "", "authentication token used to connect to exercism.io") + configureCmd.Flags().StringP("token", "t", "", "authentication token used to connect to the site") configureCmd.Flags().StringP("workspace", "w", "", "directory for exercism exercises") configureCmd.Flags().StringP("api", "a", "", "API base url") configureCmd.Flags().BoolP("show", "s", false, "show the current configuration") diff --git a/cmd/download.go b/cmd/download.go index 2a5da68d4..1ada384c7 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -3,11 +3,11 @@ package cmd import ( "errors" "fmt" - "strings" "io" "net/http" "os" "path/filepath" + "strings" "github.com/exercism/cli/api" "github.com/exercism/cli/config" @@ -210,7 +210,7 @@ type downloadPayload struct { func initDownloadCmd() { downloadCmd.Flags().StringP("uuid", "u", "", "the solution UUID") downloadCmd.Flags().StringP("track", "t", "", "the track ID") - downloadCmd.Flags().StringP("token", "k", "", "authentication token used to connect to exercism.io") + downloadCmd.Flags().StringP("token", "k", "", "authentication token used to connect to the site") } func init() { diff --git a/cmd/root.go b/cmd/root.go index 214d53ef4..17c13016e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -29,7 +29,7 @@ var ( var RootCmd = &cobra.Command{ Use: BinaryName, Short: "A friendly command-line interface to Exercism.", - Long: `A command-line interface for https://v2.exercism.io. + Long: `A command-line interface for the v2 redesign of Exercism. Download exercises and submit your solutions.`, SilenceUsage: true, From 248a90c815ee0d421e753d4c0a949dd07d26c726 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 21 Jun 2018 21:09:10 -0600 Subject: [PATCH 014/544] Use the binary name to determine the default workspace This is temporary, to unblock beta testing. To be fixed properly by https://github.com/exercism/cli/issues/541 --- cmd/configure.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/configure.go b/cmd/configure.go index 33d863063..e8ecb45aa 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -33,6 +33,9 @@ You can also override certain default settings to suit your preferences. if err != nil { return err } + if usrCfg.Workspace == "" { + fmt.Println(usrCfg.Home + BinaryName) + } apiCfg := config.NewEmptyAPIConfig() err = apiCfg.Load(viperAPIConfig) From c0f4bf0a73d00f428c64a79c9213aa7d837e68df Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 21 Jun 2018 21:51:57 -0600 Subject: [PATCH 015/544] Actually set the default workspace I messed up in #565 (it's getting late, I should not be doing this). This will be fixed properly when I address #541. --- cmd/configure.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/configure.go b/cmd/configure.go index e8ecb45aa..e143d49e5 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "path" "text/tabwriter" "github.com/exercism/cli/config" @@ -34,7 +35,7 @@ You can also override certain default settings to suit your preferences. return err } if usrCfg.Workspace == "" { - fmt.Println(usrCfg.Home + BinaryName) + usrCfg.Workspace = path.Join(usrCfg.Home, path.Base(BinaryName)) } apiCfg := config.NewEmptyAPIConfig() From a60713514053225fc4878c860a1e621cdc739596 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Fri, 22 Jun 2018 16:49:54 -0600 Subject: [PATCH 016/544] Avoid conflict between binary location and workspace Don't try to create the workspace directory on top of the binary, if the binary happens to be downloaded to the default workspace directory. --- cmd/configure.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cmd/configure.go b/cmd/configure.go index e143d49e5..34a718193 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "os" "path" "text/tabwriter" @@ -35,7 +36,14 @@ You can also override certain default settings to suit your preferences. return err } if usrCfg.Workspace == "" { - usrCfg.Workspace = path.Join(usrCfg.Home, path.Base(BinaryName)) + dirName := path.Base(BinaryName) + defaultWorkspace := path.Join(usrCfg.Home, dirName) + _, err := os.Stat(defaultWorkspace) + // Sorry about the double negative. + if !os.IsNotExist(err) { + defaultWorkspace = fmt.Sprintf("%s-1", defaultWorkspace) + } + usrCfg.Workspace = defaultWorkspace } apiCfg := config.NewEmptyAPIConfig() From 5bbf9363662ae1dcfe58aab417692ca810d464e4 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Fri, 22 Jun 2018 16:50:59 -0600 Subject: [PATCH 017/544] Place default workspace in home directory The default config had an empty string as Home, this writes Home first, so that the workspace goes there instead of in the current directory. --- cmd/configure.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/configure.go b/cmd/configure.go index 34a718193..a9f392371 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -35,6 +35,7 @@ You can also override certain default settings to suit your preferences. if err != nil { return err } + usrCfg.Normalize() if usrCfg.Workspace == "" { dirName := path.Base(BinaryName) defaultWorkspace := path.Join(usrCfg.Home, dirName) From ff73e58a2cb97be6a17c10b912da9aa44b5911f8 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Fri, 22 Jun 2018 16:51:23 -0600 Subject: [PATCH 018/544] Trim the suffix from the binary on Windows --- cmd/configure.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/configure.go b/cmd/configure.go index a9f392371..a9ac5c692 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path" + "strings" "text/tabwriter" "github.com/exercism/cli/config" @@ -37,7 +38,7 @@ You can also override certain default settings to suit your preferences. } usrCfg.Normalize() if usrCfg.Workspace == "" { - dirName := path.Base(BinaryName) + dirName := strings.Replace(path.Base(BinaryName), ".exe", "", 1) defaultWorkspace := path.Join(usrCfg.Home, dirName) _, err := os.Stat(defaultWorkspace) // Sorry about the double negative. From e46def4720e1c679ba215b0dac5633893d28919d Mon Sep 17 00:00:00 2001 From: Rob Jackson Date: Sat, 23 Jun 2018 16:55:57 +0100 Subject: [PATCH 019/544] Fix panic in workspace.Locate when encountering filesystem errors (#569) --- workspace/workspace.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/workspace/workspace.go b/workspace/workspace.go index f7f3d69c0..9430d3dae 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -61,6 +61,10 @@ func (ws Workspace) Locate(exercise string) ([]string, error) { var paths []string // Look through the entire workspace tree to find any matches. walkFn := func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + // If it's a symlink, follow it, then get the file info of the target. if info.Mode()&os.ModeSymlink == os.ModeSymlink { src, err := filepath.EvalSymlinks(path) From b2472c2f668b3ec2538109f84e196978eeeb291f Mon Sep 17 00:00:00 2001 From: ccallergard Date: Sat, 16 Jun 2018 01:37:53 +0200 Subject: [PATCH 020/544] Implements token validation Can now check if token is: - an UUID, i.e seems right - accepted by the exercism.io API. Changes configure command to do this, and it basically works. Bad tokens are not saved. Needs improvement: - Properly build request URL from client config. Endpoint? - Check client.Do() error - better message --- api/token.go | 43 +++++++++++++++++++++++++++++++++++++++++++ cmd/configure.go | 11 +++++++++++ 2 files changed, 54 insertions(+) create mode 100644 api/token.go diff --git a/api/token.go b/api/token.go new file mode 100644 index 000000000..36a893485 --- /dev/null +++ b/api/token.go @@ -0,0 +1,43 @@ +package api + +import ( + "fmt" + "regexp" +) + +// ValidateToken checks if token is a valid UUID, +// and optionally verifies that the remote API accepts it. +func ValidateToken(token string, skipAuthCheck bool) error { + tokenIsUUID, err := regexp.MatchString("^[[:alnum:]]{8}-([[:alnum:]]{4}-){3}[[:alnum:]]{12}$", token) + if err != nil { + return err + } + + if !tokenIsUUID { + return fmt.Errorf("the token \"%s\" doesn't look like a valid token", token) + } + + if skipAuthCheck { + return nil + } + + client, err := NewClient() + if err != nil { + return err + } + client.UserConfig.Token = token + + return client.checkAuthorization() +} + +// checkAuthorization calls the API to check if +// the client is authorized. +func (client *Client) checkAuthorization() error { + req, err := client.NewRequest("GET", "https://v2.exercism.io/api/v1/solutions/latest?exercise_id=hello-world&track_id=go", nil) + if err != nil { + return err + } + _, err = client.Do(req, nil) + + return err +} diff --git a/cmd/configure.go b/cmd/configure.go index a9ac5c692..77369e2be 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -7,6 +7,7 @@ import ( "strings" "text/tabwriter" + "github.com/exercism/cli/api" "github.com/exercism/cli/config" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -55,6 +56,15 @@ You can also override certain default settings to suit your preferences. } apiCfg.SetDefaults() + tokenFlag := cmd.Flags().Lookup("token") + if tokenFlag.Changed { + skipAuth, _ := cmd.Flags().GetBool("skip-auth") + err = api.ValidateToken(usrCfg.Token, skipAuth) + if err != nil { + return err + } + } + show, err := cmd.Flags().GetBool("show") if err != nil { return err @@ -86,6 +96,7 @@ func initConfigureCmd() { configureCmd.Flags().StringP("workspace", "w", "", "directory for exercism exercises") configureCmd.Flags().StringP("api", "a", "", "API base url") configureCmd.Flags().BoolP("show", "s", false, "show the current configuration") + configureCmd.Flags().BoolP("skip-auth", "", false, "skip online token validation") viperUserConfig = viper.New() viperUserConfig.BindPFlag("token", configureCmd.Flags().Lookup("token")) From b18bebe419e5a2025c063805b30df3315811cccb Mon Sep 17 00:00:00 2001 From: ccallergard Date: Sun, 17 Jun 2018 19:15:32 +0200 Subject: [PATCH 021/544] Use /validate_token endpoint --- api/token.go | 3 ++- config/api_config.go | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/api/token.go b/api/token.go index 36a893485..3a085d53b 100644 --- a/api/token.go +++ b/api/token.go @@ -33,7 +33,8 @@ func ValidateToken(token string, skipAuthCheck bool) error { // checkAuthorization calls the API to check if // the client is authorized. func (client *Client) checkAuthorization() error { - req, err := client.NewRequest("GET", "https://v2.exercism.io/api/v1/solutions/latest?exercise_id=hello-world&track_id=go", nil) + url := client.APIConfig.URL("validate") + req, err := client.NewRequest("GET", url, nil) if err != nil { return err } diff --git a/config/api_config.go b/config/api_config.go index cf7043531..f3a4799a8 100644 --- a/config/api_config.go +++ b/config/api_config.go @@ -14,6 +14,7 @@ var ( "submit": "/solutions/%s", "prepare-track": "/tracks/%s", "ping": "/ping", + "validate": "/validate_token", } ) From 22a964c82776c2415b3dccbaea7ac12af638d4e6 Mon Sep 17 00:00:00 2001 From: ccallergard Date: Mon, 18 Jun 2018 11:27:06 +0200 Subject: [PATCH 022/544] Refectors, thinks --- api/client.go | 13 +++++++++++++ api/token.go | 19 +++---------------- cmd/configure.go | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/api/client.go b/api/client.go index e71a291c4..6ca59f468 100644 --- a/api/client.go +++ b/api/client.go @@ -101,3 +101,16 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { return res, nil } + +// checkAuthorization calls the API to check if +// the client is authorized. +func (c *Client) checkAuthorization() error { + url := c.APIConfig.URL("validate") + req, err := c.NewRequest("GET", url, nil) + if err != nil { + return err + } + _, err = c.Do(req, nil) + + return err +} diff --git a/api/token.go b/api/token.go index 3a085d53b..1272c6c3d 100644 --- a/api/token.go +++ b/api/token.go @@ -6,8 +6,8 @@ import ( ) // ValidateToken checks if token is a valid UUID, -// and optionally verifies that the remote API accepts it. -func ValidateToken(token string, skipAuthCheck bool) error { +// and verifies that the remote API accepts it, unless opted out. +func ValidateToken(token string, noAuthCheck bool) error { tokenIsUUID, err := regexp.MatchString("^[[:alnum:]]{8}-([[:alnum:]]{4}-){3}[[:alnum:]]{12}$", token) if err != nil { return err @@ -17,7 +17,7 @@ func ValidateToken(token string, skipAuthCheck bool) error { return fmt.Errorf("the token \"%s\" doesn't look like a valid token", token) } - if skipAuthCheck { + if noAuthCheck { return nil } @@ -29,16 +29,3 @@ func ValidateToken(token string, skipAuthCheck bool) error { return client.checkAuthorization() } - -// checkAuthorization calls the API to check if -// the client is authorized. -func (client *Client) checkAuthorization() error { - url := client.APIConfig.URL("validate") - req, err := client.NewRequest("GET", url, nil) - if err != nil { - return err - } - _, err = client.Do(req, nil) - - return err -} diff --git a/cmd/configure.go b/cmd/configure.go index 77369e2be..46a43e985 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -96,7 +96,7 @@ func initConfigureCmd() { configureCmd.Flags().StringP("workspace", "w", "", "directory for exercism exercises") configureCmd.Flags().StringP("api", "a", "", "API base url") configureCmd.Flags().BoolP("show", "s", false, "show the current configuration") - configureCmd.Flags().BoolP("skip-auth", "", false, "skip online token validation") + configureCmd.Flags().BoolP("skip-auth", "", false, "skip online token authorization check") viperUserConfig = viper.New() viperUserConfig.BindPFlag("token", configureCmd.Flags().Lookup("token")) From c7ad935bd13f9cb7689ed3c71fbe9ac849f3c4f6 Mon Sep 17 00:00:00 2001 From: ccallergard Date: Mon, 18 Jun 2018 11:58:20 +0200 Subject: [PATCH 023/544] Defer configure printing so --show always prints what's actually on disk --- cmd/configure.go | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 46a43e985..b92d540f7 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -56,6 +56,14 @@ You can also override certain default settings to suit your preferences. } apiCfg.SetDefaults() + show, err := cmd.Flags().GetBool("show") + if err != nil { + return err + } + if show { + defer printCurrentConfig() + } + tokenFlag := cmd.Flags().Lookup("token") if tokenFlag.Changed { skipAuth, _ := cmd.Flags().GetBool("skip-auth") @@ -65,23 +73,6 @@ You can also override certain default settings to suit your preferences. } } - show, err := cmd.Flags().GetBool("show") - if err != nil { - return err - } - - if show { - w := tabwriter.NewWriter(Out, 0, 0, 2, ' ', 0) - defer w.Flush() - - fmt.Fprintln(w, "") - fmt.Fprintln(w, fmt.Sprintf("Config dir:\t%s", config.Dir())) - fmt.Fprintln(w, fmt.Sprintf("-t, --token\t%s", usrCfg.Token)) - fmt.Fprintln(w, fmt.Sprintf("-w, --workspace\t%s", usrCfg.Workspace)) - fmt.Fprintln(w, fmt.Sprintf("-a, --api\t%s", apiCfg.BaseURL)) - return nil - } - err = usrCfg.Write() if err != nil { return err @@ -91,6 +82,25 @@ You can also override certain default settings to suit your preferences. }, } +func printCurrentConfig() { + usrCfg, err := config.NewUserConfig() + if err != nil { + return + } + apiCfg, err := config.NewAPIConfig() + if err != nil { + return + } + w := tabwriter.NewWriter(Out, 0, 0, 2, ' ', 0) + defer w.Flush() + + fmt.Fprintln(w, fmt.Sprintf("Config dir:\t%s", config.Dir())) + fmt.Fprintln(w, fmt.Sprintf("-t, --token\t%s", usrCfg.Token)) + fmt.Fprintln(w, fmt.Sprintf("-w, --workspace\t%s", usrCfg.Workspace)) + fmt.Fprintln(w, fmt.Sprintf("-a, --api\t%s", apiCfg.BaseURL)) + fmt.Fprintln(w, "") +} + func initConfigureCmd() { configureCmd.Flags().StringP("token", "t", "", "authentication token used to connect to the site") configureCmd.Flags().StringP("workspace", "w", "", "directory for exercism exercises") From 323d7374713816c9fc3b7762250b1041defb19e4 Mon Sep 17 00:00:00 2001 From: ccallergard Date: Mon, 18 Jun 2018 15:27:58 +0200 Subject: [PATCH 024/544] Handle existing-token and no-token cases on configure without token flag --- cmd/configure.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index b92d540f7..0d3a5eed1 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -64,13 +64,26 @@ You can also override certain default settings to suit your preferences. defer printCurrentConfig() } - tokenFlag := cmd.Flags().Lookup("token") - if tokenFlag.Changed { + switch { + case cmd.Flags().Lookup("token").Changed: + // User set new token skipAuth, _ := cmd.Flags().GetBool("skip-auth") err = api.ValidateToken(usrCfg.Token, skipAuth) if err != nil { return err } + case usrCfg.Token == "": + fmt.Fprintln(Out, "There is no token configured, please set it using --token.") + default: + // Validate existing token + skipAuth, _ := cmd.Flags().GetBool("skip-auth") + err = api.ValidateToken(usrCfg.Token, skipAuth) + if err != nil { + if !show { + defer printCurrentConfig() + } + fmt.Fprintln(Out, err) + } } err = usrCfg.Write() @@ -94,6 +107,7 @@ func printCurrentConfig() { w := tabwriter.NewWriter(Out, 0, 0, 2, ' ', 0) defer w.Flush() + fmt.Fprintln(w, "") fmt.Fprintln(w, fmt.Sprintf("Config dir:\t%s", config.Dir())) fmt.Fprintln(w, fmt.Sprintf("-t, --token\t%s", usrCfg.Token)) fmt.Fprintln(w, fmt.Sprintf("-w, --workspace\t%s", usrCfg.Workspace)) From f61a51c7d9147fdf77b884c1fee464ac4e3d896d Mon Sep 17 00:00:00 2001 From: ccallergard Date: Mon, 18 Jun 2018 15:57:35 +0200 Subject: [PATCH 025/544] Printouts on token acceptance --- cmd/configure.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 0d3a5eed1..c5de07922 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -65,6 +65,8 @@ You can also override certain default settings to suit your preferences. } switch { + case usrCfg.Token == "": + fmt.Fprintln(Out, "There is no token configured, please set it using --token.") case cmd.Flags().Lookup("token").Changed: // User set new token skipAuth, _ := cmd.Flags().GetBool("skip-auth") @@ -72,17 +74,18 @@ You can also override certain default settings to suit your preferences. if err != nil { return err } - case usrCfg.Token == "": - fmt.Fprintln(Out, "There is no token configured, please set it using --token.") + fmt.Fprintln(Out, "Token accepted") default: // Validate existing token + if !show { + defer printCurrentConfig() + } skipAuth, _ := cmd.Flags().GetBool("skip-auth") err = api.ValidateToken(usrCfg.Token, skipAuth) if err != nil { - if !show { - defer printCurrentConfig() - } fmt.Fprintln(Out, err) + } else { + fmt.Fprintln(Out, "Token accepted") } } From c073d59df0b8731c3423f77def2375c5c0bb73b6 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 24 Jun 2018 08:38:02 -0600 Subject: [PATCH 026/544] Do not validate token format If we decided to change the format of the tokens in the API, the CLI should accept that change transparently. --- api/token.go | 25 ++----------------------- cmd/configure.go | 22 +++++++++++++--------- cmd/configure_test.go | 8 ++++---- 3 files changed, 19 insertions(+), 36 deletions(-) diff --git a/api/token.go b/api/token.go index 1272c6c3d..95edf239a 100644 --- a/api/token.go +++ b/api/token.go @@ -1,31 +1,10 @@ package api -import ( - "fmt" - "regexp" -) - -// ValidateToken checks if token is a valid UUID, -// and verifies that the remote API accepts it, unless opted out. -func ValidateToken(token string, noAuthCheck bool) error { - tokenIsUUID, err := regexp.MatchString("^[[:alnum:]]{8}-([[:alnum:]]{4}-){3}[[:alnum:]]{12}$", token) - if err != nil { - return err - } - - if !tokenIsUUID { - return fmt.Errorf("the token \"%s\" doesn't look like a valid token", token) - } - - if noAuthCheck { - return nil - } - +// ValidateToken calls the API to determine whether the token is valid. +func ValidateToken() error { client, err := NewClient() if err != nil { return err } - client.UserConfig.Token = token - return client.checkAuthorization() } diff --git a/cmd/configure.go b/cmd/configure.go index c5de07922..3cbe809ce 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -70,22 +70,26 @@ You can also override certain default settings to suit your preferences. case cmd.Flags().Lookup("token").Changed: // User set new token skipAuth, _ := cmd.Flags().GetBool("skip-auth") - err = api.ValidateToken(usrCfg.Token, skipAuth) - if err != nil { - return err + if !skipAuth { + err = api.ValidateToken() + if err != nil { + return err + } + fmt.Fprintln(Out, "Token accepted") } - fmt.Fprintln(Out, "Token accepted") default: // Validate existing token if !show { defer printCurrentConfig() } skipAuth, _ := cmd.Flags().GetBool("skip-auth") - err = api.ValidateToken(usrCfg.Token, skipAuth) - if err != nil { - fmt.Fprintln(Out, err) - } else { - fmt.Fprintln(Out, "Token accepted") + if !skipAuth { + err = api.ValidateToken() + if err != nil { + fmt.Fprintln(Out, err) + } else { + fmt.Fprintln(Out, "Token accepted") + } } } diff --git a/cmd/configure_test.go b/cmd/configure_test.go index c8c35d489..38a2dbc4f 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -22,7 +22,7 @@ func TestConfigure(t *testing.T) { testCases := []testCase{ testCase{ desc: "It writes the flags when there is no config file.", - args: []string{"fakeapp", "configure", "--token", "a", "--workspace", "/a", "--api", "http://example.com"}, + args: []string{"fakeapp", "configure", "--token", "a", "--workspace", "/a", "--api", "http://example.com", "--skip-auth"}, existingUsrCfg: nil, expectedUsrCfg: &config.UserConfig{Token: "a", Workspace: "/a"}, existingAPICfg: nil, @@ -30,7 +30,7 @@ func TestConfigure(t *testing.T) { }, testCase{ desc: "It overwrites the flags in the config file.", - args: []string{"fakeapp", "configure", "--token", "b", "--workspace", "/b", "--api", "http://example.com/v2"}, + args: []string{"fakeapp", "configure", "--token", "b", "--workspace", "/b", "--api", "http://example.com/v2", "--skip-auth"}, existingUsrCfg: &config.UserConfig{Token: "token-b", Workspace: "/workspace-b"}, expectedUsrCfg: &config.UserConfig{Token: "b", Workspace: "/b"}, existingAPICfg: &config.APIConfig{BaseURL: "http://example.com/v1"}, @@ -38,13 +38,13 @@ func TestConfigure(t *testing.T) { }, testCase{ desc: "It overwrites the flags that are passed, without losing the ones that are not.", - args: []string{"fakeapp", "configure", "--token", "c"}, + args: []string{"fakeapp", "configure", "--token", "c", "--skip-auth"}, existingUsrCfg: &config.UserConfig{Token: "token-c", Workspace: "/workspace-c"}, expectedUsrCfg: &config.UserConfig{Token: "c", Workspace: "/workspace-c"}, }, testCase{ desc: "It gets the default API base URL.", - args: []string{"fakeapp", "configure"}, + args: []string{"fakeapp", "configure", "--skip-auth"}, existingAPICfg: &config.APIConfig{}, expectedAPICfg: &config.APIConfig{BaseURL: "https://v2.exercism.io/api/v1"}, }, From 57720aa5ba88d5b2fff6024bedfa570733aba0ad Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 24 Jun 2018 08:39:14 -0600 Subject: [PATCH 027/544] Don't print output if token is valid Assume that it's valid, and only complain if it's not. --- cmd/configure.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 3cbe809ce..ad0c0536c 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -75,7 +75,6 @@ You can also override certain default settings to suit your preferences. if err != nil { return err } - fmt.Fprintln(Out, "Token accepted") } default: // Validate existing token @@ -87,8 +86,6 @@ You can also override certain default settings to suit your preferences. err = api.ValidateToken() if err != nil { fmt.Fprintln(Out, err) - } else { - fmt.Fprintln(Out, "Token accepted") } } } From 1d98b690df79eca291fdad883fb67bf98ed7b177 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 24 Jun 2018 16:38:59 -0500 Subject: [PATCH 028/544] Remove unnecessary indirection in token validation --- api/client.go | 5 ++--- api/token.go | 10 ---------- cmd/configure.go | 8 ++++++-- 3 files changed, 8 insertions(+), 15 deletions(-) delete mode 100644 api/token.go diff --git a/api/client.go b/api/client.go index 6ca59f468..693a5b1cc 100644 --- a/api/client.go +++ b/api/client.go @@ -102,9 +102,8 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { return res, nil } -// checkAuthorization calls the API to check if -// the client is authorized. -func (c *Client) checkAuthorization() error { +// ValidateToken calls the API to determine whether the token is valid. +func (c *Client) ValidateToken() error { url := c.APIConfig.URL("validate") req, err := c.NewRequest("GET", url, nil) if err != nil { diff --git a/api/token.go b/api/token.go deleted file mode 100644 index 95edf239a..000000000 --- a/api/token.go +++ /dev/null @@ -1,10 +0,0 @@ -package api - -// ValidateToken calls the API to determine whether the token is valid. -func ValidateToken() error { - client, err := NewClient() - if err != nil { - return err - } - return client.checkAuthorization() -} diff --git a/cmd/configure.go b/cmd/configure.go index ad0c0536c..81b0c110b 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -63,6 +63,10 @@ You can also override certain default settings to suit your preferences. if show { defer printCurrentConfig() } + client, err := api.NewClient() + if err != nil { + return err + } switch { case usrCfg.Token == "": @@ -71,7 +75,7 @@ You can also override certain default settings to suit your preferences. // User set new token skipAuth, _ := cmd.Flags().GetBool("skip-auth") if !skipAuth { - err = api.ValidateToken() + err = client.ValidateToken() if err != nil { return err } @@ -83,7 +87,7 @@ You can also override certain default settings to suit your preferences. } skipAuth, _ := cmd.Flags().GetBool("skip-auth") if !skipAuth { - err = api.ValidateToken() + err = client.ValidateToken() if err != nil { fmt.Fprintln(Out, err) } From 90367abdf9efc2790333c5d74b4e512fe737efb8 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 24 Jun 2018 14:57:21 -0500 Subject: [PATCH 029/544] Rename Normalize to SetDefaults on user config for clarity --- cmd/configure.go | 2 +- cmd/configure_test.go | 2 +- config/user_config.go | 6 +++--- config/user_config_test.go | 4 ++-- config/user_config_windows_test.go | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 81b0c110b..6d8cd53ff 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -37,7 +37,7 @@ You can also override certain default settings to suit your preferences. if err != nil { return err } - usrCfg.Normalize() + usrCfg.SetDefaults() if usrCfg.Workspace == "" { dirName := strings.Replace(path.Base(BinaryName), ".exe", "", 1) defaultWorkspace := path.Join(usrCfg.Home, dirName) diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 38a2dbc4f..bb72197d9 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -79,7 +79,7 @@ func makeTest(tc testCase) func(*testing.T) { if tc.expectedUsrCfg != nil { if runtime.GOOS == "windows" { - tc.expectedUsrCfg.Normalize() + tc.expectedUsrCfg.SetDefaults() } usrCfg, err := config.NewUserConfig() diff --git a/config/user_config.go b/config/user_config.go index 4940dde09..57d77c618 100644 --- a/config/user_config.go +++ b/config/user_config.go @@ -35,8 +35,8 @@ func NewEmptyUserConfig() *UserConfig { } } -// Normalize ensures that we have proper values where possible. -func (cfg *UserConfig) Normalize() { +// SetDefaults ensures that we have proper values where possible. +func (cfg *UserConfig) SetDefaults() { if cfg.Home == "" { cfg.Home = userHome() } @@ -45,7 +45,7 @@ func (cfg *UserConfig) Normalize() { // Write stores the config to disk. func (cfg *UserConfig) Write() error { - cfg.Normalize() + cfg.SetDefaults() return Write(cfg) } diff --git a/config/user_config_test.go b/config/user_config_test.go index b3ce81c77..204bc48f4 100644 --- a/config/user_config_test.go +++ b/config/user_config_test.go @@ -37,7 +37,7 @@ func TestUserConfig(t *testing.T) { assert.Equal(t, "/a", cfg.Workspace) } -func TestNormalizeWorkspace(t *testing.T) { +func TestSetDefaultWorkspace(t *testing.T) { cwd, err := os.Getwd() assert.NoError(t, err) @@ -58,7 +58,7 @@ func TestNormalizeWorkspace(t *testing.T) { testName := "'" + tc.in + "' should be normalized as '" + tc.out + "'" t.Run(testName, func(t *testing.T) { cfg.Workspace = tc.in - cfg.Normalize() + cfg.SetDefaults() assert.Equal(t, tc.out, cfg.Workspace, testName) }) } diff --git a/config/user_config_windows_test.go b/config/user_config_windows_test.go index 5ea9a3e45..49af6be5c 100644 --- a/config/user_config_windows_test.go +++ b/config/user_config_windows_test.go @@ -35,7 +35,7 @@ func TestUserConfig(t *testing.T) { assert.Equal(t, filepath.Join(cfg.Home, "a"), cfg.Workspace) } -func TestNormalizeWorkspace(t *testing.T) { +func TestSetDefaultWorkspace(t *testing.T) { cwd, err := os.Getwd() assert.NoError(t, err) @@ -59,7 +59,7 @@ func TestNormalizeWorkspace(t *testing.T) { t.Run(testName, func(t *testing.T) { cfg.Workspace = tc.in - cfg.Normalize() + cfg.SetDefaults() assert.Equal(t, tc.out, cfg.Workspace, testName) }) } From f5bc41784550804bf038147f02b09a299a5d8cd4 Mon Sep 17 00:00:00 2001 From: Sebastian Borza Date: Sun, 24 Jun 2018 15:32:19 -0500 Subject: [PATCH 030/544] moving debug response parser to use the body reader copy --- debug/debug.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/debug/debug.go b/debug/debug.go index e337d12b5..0fdb96ebd 100644 --- a/debug/debug.go +++ b/debug/debug.go @@ -31,6 +31,7 @@ func Printf(format string, args ...interface{}) { } } +// DumpRequest dumps out the provided http.Request func DumpRequest(req *http.Request) { if !Verbose { return @@ -53,6 +54,7 @@ func DumpRequest(req *http.Request) { req.Body = ioutil.NopCloser(&bodyCopy) } +// DumpResponse dumps out the provided http.Response func DumpResponse(res *http.Response) { if !Verbose { return @@ -72,5 +74,5 @@ func DumpResponse(res *http.Response) { Println("========================= END DumpResponse =========================") Println("") - res.Body = ioutil.NopCloser(&bodyCopy) + res.Body = ioutil.NopCloser(body) } From 2561fda0a3808e813708b9cc490cd1c6e510ec29 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 24 Jun 2018 23:04:24 -0400 Subject: [PATCH 031/544] Move path resolving to function on config package --- config/config.go | 23 +++++++++++++++++++++++ config/config_test.go | 24 ++++++++++++++++++++++++ config/user_config.go | 26 +------------------------- config/user_config_test.go | 28 ---------------------------- 4 files changed, 48 insertions(+), 53 deletions(-) diff --git a/config/config.go b/config/config.go index d71f0da6d..f19664f89 100644 --- a/config/config.go +++ b/config/config.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "regexp" + "strings" "github.com/spf13/viper" ) @@ -71,3 +72,25 @@ func InferSiteURL(apiURL string) string { re := regexp.MustCompile("^(https?://[^/]*).*") return re.ReplaceAllString(apiURL, "$1") } + +func Resolve(path, home string) string { + if path == "" { + return "" + } + if strings.HasPrefix(path, "~/") { + path = strings.Replace(path, "~/", "", 1) + return filepath.Join(home, path) + } + if filepath.IsAbs(path) { + return filepath.Clean(path) + } + // if using "/dir" on Windows + if strings.HasPrefix(path, "/") { + return filepath.Join(home, filepath.Clean(path)) + } + cwd, err := os.Getwd() + if err != nil { + return path + } + return filepath.Join(cwd, path) +} diff --git a/config/config_test.go b/config/config_test.go index 44808d50c..5b1a16ddb 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -111,3 +111,27 @@ func TestInferSiteURL(t *testing.T) { assert.Equal(t, InferSiteURL(tc.api), tc.url) } } + +func TestResolve(t *testing.T) { + cwd, err := os.Getwd() + assert.NoError(t, err) + + testCases := []struct { + in, out string + }{ + {"", ""}, // don't make wild guesses + {"/home/alice///foobar", "/home/alice/foobar"}, + {"~/foobar", "/home/alice/foobar"}, + {"/foobar/~/noexpand", "/foobar/~/noexpand"}, + {"/no/modification", "/no/modification"}, + {"relative", filepath.Join(cwd, "relative")}, + {"relative///path", filepath.Join(cwd, "relative", "path")}, + } + + for _, tc := range testCases { + testName := "'" + tc.in + "' should be normalized as '" + tc.out + "'" + t.Run(testName, func(t *testing.T) { + assert.Equal(t, tc.out, Resolve(tc.in, "/home/alice"), testName) + }) + } +} diff --git a/config/user_config.go b/config/user_config.go index 57d77c618..626819291 100644 --- a/config/user_config.go +++ b/config/user_config.go @@ -2,9 +2,7 @@ package config import ( "os" - "path/filepath" "runtime" - "strings" "github.com/spf13/viper" ) @@ -40,7 +38,7 @@ func (cfg *UserConfig) SetDefaults() { if cfg.Home == "" { cfg.Home = userHome() } - cfg.Workspace = cfg.resolve(cfg.Workspace) + cfg.Workspace = Resolve(cfg.Workspace, cfg.Home) } // Write stores the config to disk. @@ -76,25 +74,3 @@ func userHome() string { dir, _ = os.Getwd() return dir } - -func (cfg *UserConfig) resolve(path string) string { - if path == "" { - return "" - } - if strings.HasPrefix(path, "~/") { - path = strings.Replace(path, "~/", "", 1) - return filepath.Join(cfg.Home, path) - } - if filepath.IsAbs(path) { - return filepath.Clean(path) - } - // if using "/dir" on Windows - if strings.HasPrefix(path, "/") { - return filepath.Join(cfg.Home, filepath.Clean(path)) - } - cwd, err := os.Getwd() - if err != nil { - return path - } - return filepath.Join(cwd, path) -} diff --git a/config/user_config_test.go b/config/user_config_test.go index 204bc48f4..2408e677d 100644 --- a/config/user_config_test.go +++ b/config/user_config_test.go @@ -5,7 +5,6 @@ package config import ( "io/ioutil" "os" - "path/filepath" "testing" "github.com/spf13/viper" @@ -36,30 +35,3 @@ func TestUserConfig(t *testing.T) { assert.Equal(t, "a", cfg.Token) assert.Equal(t, "/a", cfg.Workspace) } - -func TestSetDefaultWorkspace(t *testing.T) { - cwd, err := os.Getwd() - assert.NoError(t, err) - - cfg := &UserConfig{Home: "/home/alice"} - testCases := []struct { - in, out string - }{ - {"", ""}, // don't make wild guesses - {"/home/alice///foobar", "/home/alice/foobar"}, - {"~/foobar", "/home/alice/foobar"}, - {"/foobar/~/noexpand", "/foobar/~/noexpand"}, - {"/no/modification", "/no/modification"}, - {"relative", filepath.Join(cwd, "relative")}, - {"relative///path", filepath.Join(cwd, "relative", "path")}, - } - - for _, tc := range testCases { - testName := "'" + tc.in + "' should be normalized as '" + tc.out + "'" - t.Run(testName, func(t *testing.T) { - cfg.Workspace = tc.in - cfg.SetDefaults() - assert.Equal(t, tc.out, cfg.Workspace, testName) - }) - } -} From 726ccff4efe6dd0199cf945acc9a8611eae849f9 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 24 Jun 2018 23:06:25 -0400 Subject: [PATCH 032/544] Only resolve user-passed workspace If we set the default it doesn't need to be resolved. --- cmd/configure.go | 1 + config/user_config.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/configure.go b/cmd/configure.go index 6d8cd53ff..a951f3b69 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -37,6 +37,7 @@ You can also override certain default settings to suit your preferences. if err != nil { return err } + usrCfg.Workspace = config.Resolve(usrCfg.Workspace, usrCfg.Home) usrCfg.SetDefaults() if usrCfg.Workspace == "" { dirName := strings.Replace(path.Base(BinaryName), ".exe", "", 1) diff --git a/config/user_config.go b/config/user_config.go index 626819291..d29bd870b 100644 --- a/config/user_config.go +++ b/config/user_config.go @@ -38,7 +38,6 @@ func (cfg *UserConfig) SetDefaults() { if cfg.Home == "" { cfg.Home = userHome() } - cfg.Workspace = Resolve(cfg.Workspace, cfg.Home) } // Write stores the config to disk. From 847a9b19a45d236abd344bd65a5d7c6046e9b36d Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 24 Jun 2018 14:47:04 -0500 Subject: [PATCH 033/544] Remove indirection for API URLs --- api/client.go | 2 +- cli/status.go | 2 +- cmd/download.go | 2 +- cmd/prepare.go | 2 +- cmd/prepare_test.go | 1 - cmd/submit.go | 3 ++- cmd/submit_test.go | 1 - config/api_config.go | 32 ++------------------------------ config/api_config_test.go | 34 ---------------------------------- 9 files changed, 8 insertions(+), 71 deletions(-) diff --git a/api/client.go b/api/client.go index 693a5b1cc..171978112 100644 --- a/api/client.go +++ b/api/client.go @@ -104,7 +104,7 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { // ValidateToken calls the API to determine whether the token is valid. func (c *Client) ValidateToken() error { - url := c.APIConfig.URL("validate") + url := fmt.Sprintf("%s/validate_token", c.APIConfig.BaseURL) req, err := c.NewRequest("GET", url, nil) if err != nil { return err diff --git a/cli/status.go b/cli/status.go index 170becf14..7b0a52834 100644 --- a/cli/status.go +++ b/cli/status.go @@ -104,7 +104,7 @@ func newAPIReachabilityStatus() (apiReachabilityStatus, error) { ar := apiReachabilityStatus{ Services: []*apiPing{ {Service: "GitHub", URL: "https://api.github.com"}, - {Service: "Exercism", URL: apiCfg.URL("ping")}, + {Service: "Exercism", URL: fmt.Sprintf("%s/ping", apiCfg.BaseURL)}, }, } var wg sync.WaitGroup diff --git a/cmd/download.go b/cmd/download.go index 1ada384c7..8becdaeaf 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -59,7 +59,7 @@ Download other people's solutions by providing the UUID. } else { slug = uuid } - url := apiCfg.URL("download", slug) + url := fmt.Sprintf("%s/solutions/%s", apiCfg.BaseURL, slug) client, err := api.NewClient() if err != nil { diff --git a/cmd/prepare.go b/cmd/prepare.go index d604ac41d..9504b66e1 100644 --- a/cmd/prepare.go +++ b/cmd/prepare.go @@ -52,7 +52,7 @@ func prepareTrack(id string) error { if err != nil { return err } - url := apiCfg.URL("prepare-track", id) + url := fmt.Sprintf("%s/tracks/%s", apiCfg.BaseURL, id) req, err := client.NewRequest("GET", url, nil) if err != nil { diff --git a/cmd/prepare_test.go b/cmd/prepare_test.go index ad4c239dc..82a515808 100644 --- a/cmd/prepare_test.go +++ b/cmd/prepare_test.go @@ -37,7 +37,6 @@ func TestPrepareTrack(t *testing.T) { apiCfg := config.NewEmptyAPIConfig() apiCfg.BaseURL = ts.URL - apiCfg.Endpoints = map[string]string{"prepare-track": "?%s"} err := apiCfg.Write() assert.NoError(t, err) diff --git a/cmd/submit.go b/cmd/submit.go index 850b1078c..d640e877e 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -212,7 +212,8 @@ figuring things out if necessary. if err != nil { return err } - req, err := client.NewRequest("PATCH", apiCfg.URL("submit", solution.ID), body) + url := fmt.Sprintf("%s/solutions/%s", apiCfg.BaseURL, solution.ID) + req, err := client.NewRequest("PATCH", url, body) if err != nil { return err } diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 180ed1b43..d7193e243 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -112,7 +112,6 @@ func TestSubmit(t *testing.T) { apiCfg, err := config.NewAPIConfig() assert.NoError(t, err) apiCfg.BaseURL = ts.URL - apiCfg.Endpoints["submit"] = "?%s" err = apiCfg.Write() assert.NoError(t, err) diff --git a/config/api_config.go b/config/api_config.go index f3a4799a8..a3f91f98e 100644 --- a/config/api_config.go +++ b/config/api_config.go @@ -1,28 +1,19 @@ package config import ( - "fmt" "strings" "github.com/spf13/viper" ) var ( - defaultBaseURL = "https://v2.exercism.io/api/v1" - defaultEndpoints = map[string]string{ - "download": "/solutions/%s", - "submit": "/solutions/%s", - "prepare-track": "/tracks/%s", - "ping": "/ping", - "validate": "/validate_token", - } + defaultBaseURL = "https://v2.exercism.io/api/v1" ) // APIConfig provides API-specific configuration values. type APIConfig struct { *Config - BaseURL string - Endpoints map[string]string + BaseURL string } // NewAPIConfig loads the config file in the config directory. @@ -43,25 +34,6 @@ func (cfg *APIConfig) SetDefaults() { if cfg.BaseURL == "" { cfg.BaseURL = defaultBaseURL } - if cfg.Endpoints == nil { - cfg.Endpoints = defaultEndpoints - return - } - - for key, endpoint := range defaultEndpoints { - if cfg.Endpoints[key] == "" { - cfg.Endpoints[key] = endpoint - } - } -} - -// URL provides the API URL for a given endpoint key. -func (cfg *APIConfig) URL(key string, args ...interface{}) string { - pattern := fmt.Sprintf("%s%s", cfg.BaseURL, cfg.Endpoints[key]) - if args == nil { - return pattern - } - return fmt.Sprintf(pattern, args...) } // NewEmptyAPIConfig doesn't load the config from file or set default values. diff --git a/config/api_config_test.go b/config/api_config_test.go index a27c2e2a4..5b87a662d 100644 --- a/config/api_config_test.go +++ b/config/api_config_test.go @@ -17,10 +17,6 @@ func TestAPIConfig(t *testing.T) { cfg := &APIConfig{ Config: New(dir, "api"), BaseURL: "http://example.com/v1", - Endpoints: map[string]string{ - "a": "/a", - "b": "/b", - }, } // write it @@ -34,8 +30,6 @@ func TestAPIConfig(t *testing.T) { err = cfg.Load(viper.New()) assert.NoError(t, err) assert.Equal(t, "http://example.com/v1", cfg.BaseURL) - assert.Equal(t, "/a", cfg.Endpoints["a"]) - assert.Equal(t, "/b", cfg.Endpoints["b"]) } func TestAPIConfigSetDefaults(t *testing.T) { @@ -43,8 +37,6 @@ func TestAPIConfigSetDefaults(t *testing.T) { cfg := &APIConfig{} cfg.SetDefaults() assert.Equal(t, "https://v2.exercism.io/api/v1", cfg.BaseURL) - assert.Equal(t, "/solutions/%s", cfg.Endpoints["download"]) - assert.Equal(t, "/solutions/%s", cfg.Endpoints["submit"]) // Override just the base url. cfg = &APIConfig{ @@ -52,30 +44,4 @@ func TestAPIConfigSetDefaults(t *testing.T) { } cfg.SetDefaults() assert.Equal(t, "http://example.com/v1", cfg.BaseURL) - assert.Equal(t, "/solutions/%s", cfg.Endpoints["download"]) - assert.Equal(t, "/solutions/%s", cfg.Endpoints["submit"]) - - // Override just one of the endpoints. - cfg = &APIConfig{ - Endpoints: map[string]string{ - "download": "/download/%d", - }, - } - cfg.SetDefaults() - assert.Equal(t, "https://v2.exercism.io/api/v1", cfg.BaseURL) - assert.Equal(t, "/download/%d", cfg.Endpoints["download"]) - assert.Equal(t, "/solutions/%s", cfg.Endpoints["submit"]) -} - -func TestAPIConfigURL(t *testing.T) { - cfg := &APIConfig{ - Endpoints: map[string]string{ - "a": "a/%s/a", - "b": "b/%s/%d", - "c": "c/%s/%s/%s", - }, - } - assert.Equal(t, "a/apple/a", cfg.URL("a", "apple")) - assert.Equal(t, "b/banana/2", cfg.URL("b", "banana", 2)) - assert.Equal(t, "c/cherry/coca/cola", cfg.URL("c", "cherry", "coca", "cola")) } From 0079b85f3abea23e3913b733a35b035ae403e90d Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 24 Jun 2018 23:53:50 -0400 Subject: [PATCH 034/544] Pass only needed primitives to api.Client The client doesn't need the entire config objects, it just needs a couple of strings. --- api/client.go | 29 ++++++++++------------------- api/client_test.go | 3 +-- cmd/configure.go | 2 +- cmd/download.go | 11 ++++++++--- cmd/prepare.go | 6 +++++- cmd/submit.go | 6 +++--- 6 files changed, 28 insertions(+), 29 deletions(-) diff --git a/api/client.go b/api/client.go index 171978112..7ad675762 100644 --- a/api/client.go +++ b/api/client.go @@ -23,26 +23,17 @@ var ( // Client is an http client that is configured for Exercism. type Client struct { *http.Client - APIConfig *config.APIConfig - UserConfig *config.UserConfig ContentType string + Token string + BaseURL string } // NewClient returns an Exercism API client. -func NewClient() (*Client, error) { - apiCfg, err := config.NewAPIConfig() - if err != nil { - return nil, err - } - userCfg, err := config.NewUserConfig() - if err != nil { - return nil, err - } - +func NewClient(token, baseURL string) (*Client, error) { return &Client{ - Client: DefaultHTTPClient, - APIConfig: apiCfg, - UserConfig: userCfg, + Client: DefaultHTTPClient, + Token: token, + BaseURL: baseURL, }, nil } @@ -63,8 +54,8 @@ func (c *Client) NewRequest(method, url string, body io.Reader) (*http.Request, } else { req.Header.Set("Content-Type", c.ContentType) } - if c.UserConfig != nil && c.UserConfig.Token != "" { - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.UserConfig.Token)) + if c.Token != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.Token)) } return req, nil @@ -87,7 +78,7 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { // TODO: if it's json, and it has an error key, print the message. return nil, fmt.Errorf("%s", res.Status) case http.StatusUnauthorized: - siteURL := config.InferSiteURL(c.APIConfig.BaseURL) + siteURL := config.InferSiteURL(c.BaseURL) return nil, fmt.Errorf("unauthorized request. Please run the configure command. You can find your API token at %s/my/settings", siteURL) default: if v != nil { @@ -104,7 +95,7 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { // ValidateToken calls the API to determine whether the token is valid. func (c *Client) ValidateToken() error { - url := fmt.Sprintf("%s/validate_token", c.APIConfig.BaseURL) + url := fmt.Sprintf("%s/validate_token", c.BaseURL) req, err := c.NewRequest("GET", url, nil) if err != nil { return err diff --git a/api/client_test.go b/api/client_test.go index ba069b738..c73023622 100644 --- a/api/client_test.go +++ b/api/client_test.go @@ -6,7 +6,6 @@ import ( "net/http/httptest" "testing" - "github.com/exercism/cli/config" "github.com/stretchr/testify/assert" ) @@ -33,8 +32,8 @@ func TestNewRequestSetsDefaultHeaders(t *testing.T) { { desc: "Override defaults", client: &Client{ - UserConfig: &config.UserConfig{Token: "abc123"}, ContentType: "bogus", + Token: "abc123", }, auth: "Bearer abc123", contentType: "bogus", diff --git a/cmd/configure.go b/cmd/configure.go index a951f3b69..ff150e46c 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -64,7 +64,7 @@ You can also override certain default settings to suit your preferences. if show { defer printCurrentConfig() } - client, err := api.NewClient() + client, err := api.NewClient(usrCfg.Token, apiCfg.BaseURL) if err != nil { return err } diff --git a/cmd/download.go b/cmd/download.go index 8becdaeaf..e8b9824d1 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -48,6 +48,11 @@ Download other people's solutions by providing the UUID. // TODO: usage return errors.New("need an exercise name or a solution --uuid") } + usrCfg, err := config.NewUserConfig() + if err != nil { + return err + } + apiCfg, err := config.NewAPIConfig() if err != nil { return err @@ -61,7 +66,7 @@ Download other people's solutions by providing the UUID. } url := fmt.Sprintf("%s/solutions/%s", apiCfg.BaseURL, slug) - client, err := api.NewClient() + client, err := api.NewClient(usrCfg.Token, apiCfg.BaseURL) if err != nil { return err } @@ -116,9 +121,9 @@ Download other people's solutions by providing the UUID. var ws workspace.Workspace if solution.IsRequester { - ws = workspace.New(filepath.Join(client.UserConfig.Workspace, solution.Track)) + ws = workspace.New(filepath.Join(usrCfg.Workspace, solution.Track)) } else { - ws = workspace.New(filepath.Join(client.UserConfig.Workspace, "users", solution.Handle, solution.Track)) + ws = workspace.New(filepath.Join(usrCfg.Workspace, "users", solution.Handle, solution.Track)) } os.MkdirAll(ws.Dir, os.FileMode(0755)) diff --git a/cmd/prepare.go b/cmd/prepare.go index 9504b66e1..f24039434 100644 --- a/cmd/prepare.go +++ b/cmd/prepare.go @@ -43,12 +43,16 @@ To customize the CLI to suit your own preferences, use the configure command. } func prepareTrack(id string) error { + usrCfg, err := config.NewUserConfig() + if err != nil { + return err + } apiCfg, err := config.NewAPIConfig() if err != nil { return err } - client, err := api.NewClient() + client, err := api.NewClient(usrCfg.Token, apiCfg.BaseURL) if err != nil { return err } diff --git a/cmd/submit.go b/cmd/submit.go index d640e877e..cd3994cf2 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -208,7 +208,7 @@ figuring things out if necessary. return err } - client, err := api.NewClient() + client, err := api.NewClient(usrCfg.Token, apiCfg.BaseURL) if err != nil { return err } @@ -232,8 +232,8 @@ figuring things out if necessary. } if solution.AutoApprove == true { - fmt.Fprintf(Out, "Your solution has been submitted " + - "successfully and has been auto-approved. You can complete " + + fmt.Fprintf(Out, "Your solution has been submitted "+ + "successfully and has been auto-approved. You can complete "+ "the exercise and unlock the next core exercise at %s\n", solution.URL) } else { From f7ee7ae29aaa49aae3a4e02a29215e02a2bded78 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 25 Jun 2018 00:16:47 -0400 Subject: [PATCH 035/544] Move logic for default workspace into config package --- cmd/configure.go | 13 ------------- cmd/root.go | 2 +- config/dir.go | 14 +++++++++++--- config/user_config.go | 15 +++++++++++++++ 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index ff150e46c..61e8011c9 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -2,9 +2,6 @@ package cmd import ( "fmt" - "os" - "path" - "strings" "text/tabwriter" "github.com/exercism/cli/api" @@ -39,16 +36,6 @@ You can also override certain default settings to suit your preferences. } usrCfg.Workspace = config.Resolve(usrCfg.Workspace, usrCfg.Home) usrCfg.SetDefaults() - if usrCfg.Workspace == "" { - dirName := strings.Replace(path.Base(BinaryName), ".exe", "", 1) - defaultWorkspace := path.Join(usrCfg.Home, dirName) - _, err := os.Stat(defaultWorkspace) - // Sorry about the double negative. - if !os.IsNotExist(err) { - defaultWorkspace = fmt.Sprintf("%s-1", defaultWorkspace) - } - usrCfg.Workspace = defaultWorkspace - } apiCfg := config.NewEmptyAPIConfig() err = apiCfg.Load(viperAPIConfig) diff --git a/cmd/root.go b/cmd/root.go index 17c13016e..8e26fb568 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -49,7 +49,7 @@ func Execute() { func init() { BinaryName = os.Args[0] - config.SubdirectoryName = BinaryName + config.SetDefaultDirName(BinaryName) Out = os.Stdout In = os.Stdin api.UserAgent = fmt.Sprintf("github.com/exercism/cli v%s (%s/%s)", Version, runtime.GOOS, runtime.GOARCH) diff --git a/config/dir.go b/config/dir.go index c4dddf220..40654c2bf 100644 --- a/config/dir.go +++ b/config/dir.go @@ -2,14 +2,22 @@ package config import ( "os" + "path" "path/filepath" "runtime" + "strings" ) var ( - SubdirectoryName = "exercism" + // DefaultDirName is the default name used for config and workspace directories. + DefaultDirName string ) +// SetDefaultDirName configures the default directory name based on the name of the binary. +func SetDefaultDirName(binaryName string) { + DefaultDirName = strings.Replace(path.Base(binaryName), ".exe", "", 1) +} + // Dir is the configured config home directory. // All the cli-related config files live in this directory. func Dir() string { @@ -17,7 +25,7 @@ func Dir() string { if runtime.GOOS == "windows" { dir = os.Getenv("APPDATA") if dir != "" { - return filepath.Join(dir, SubdirectoryName) + return filepath.Join(dir, DefaultDirName) } } else { dir := os.Getenv("EXERCISM_CONFIG_HOME") @@ -29,7 +37,7 @@ func Dir() string { dir = filepath.Join(os.Getenv("HOME"), ".config") } if dir != "" { - return filepath.Join(dir, SubdirectoryName) + return filepath.Join(dir, DefaultDirName) } } // If all else fails, use the current directory. diff --git a/config/user_config.go b/config/user_config.go index d29bd870b..30a4e0386 100644 --- a/config/user_config.go +++ b/config/user_config.go @@ -1,7 +1,9 @@ package config import ( + "fmt" "os" + "path" "runtime" "github.com/spf13/viper" @@ -38,6 +40,9 @@ func (cfg *UserConfig) SetDefaults() { if cfg.Home == "" { cfg.Home = userHome() } + if cfg.Workspace == "" { + cfg.Workspace = defaultWorkspace(cfg.Home) + } } // Write stores the config to disk. @@ -73,3 +78,13 @@ func userHome() string { dir, _ = os.Getwd() return dir } + +func defaultWorkspace(home string) string { + dir := path.Join(home, DefaultDirName) + _, err := os.Stat(dir) + // Sorry about the double negative. + if !os.IsNotExist(err) { + dir = fmt.Sprintf("%s-1", dir) + } + return dir +} From 29a47b729bdd8872f1043150d3af67b6aee62ffa Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 24 Jun 2018 14:52:25 -0500 Subject: [PATCH 036/544] Add support for API base URL on user config This will let us simplify, getting rid of the API config altogether. --- cmd/configure_test.go | 1 + config/api_config.go | 4 ---- config/user_config.go | 14 +++++++++++--- config/user_config_test.go | 2 ++ 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/cmd/configure_test.go b/cmd/configure_test.go index bb72197d9..58c4caf54 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -71,6 +71,7 @@ func makeTest(tc testCase) func(*testing.T) { cfg := config.NewEmptyUserConfig() cfg.Token = tc.existingUsrCfg.Token cfg.Workspace = tc.existingUsrCfg.Workspace + cfg.APIBaseURL = tc.existingUsrCfg.APIBaseURL err := cfg.Write() assert.NoError(t, err, tc.desc) } diff --git a/config/api_config.go b/config/api_config.go index a3f91f98e..28901cde7 100644 --- a/config/api_config.go +++ b/config/api_config.go @@ -6,10 +6,6 @@ import ( "github.com/spf13/viper" ) -var ( - defaultBaseURL = "https://v2.exercism.io/api/v1" -) - // APIConfig provides API-specific configuration values. type APIConfig struct { *Config diff --git a/config/user_config.go b/config/user_config.go index 30a4e0386..d955583a5 100644 --- a/config/user_config.go +++ b/config/user_config.go @@ -9,12 +9,17 @@ import ( "github.com/spf13/viper" ) +var ( + defaultBaseURL = "https://v2.exercism.io/api/v1" +) + // UserConfig contains user-specific settings. type UserConfig struct { *Config - Workspace string - Token string - Home string + Workspace string + Token string + Home string + APIBaseURL string } // NewUserConfig loads a user configuration if it exists. @@ -40,6 +45,9 @@ func (cfg *UserConfig) SetDefaults() { if cfg.Home == "" { cfg.Home = userHome() } + if cfg.APIBaseURL == "" { + cfg.APIBaseURL = defaultBaseURL + } if cfg.Workspace == "" { cfg.Workspace = defaultWorkspace(cfg.Home) } diff --git a/config/user_config_test.go b/config/user_config_test.go index 2408e677d..fe4d2bca4 100644 --- a/config/user_config_test.go +++ b/config/user_config_test.go @@ -21,6 +21,7 @@ func TestUserConfig(t *testing.T) { } cfg.Token = "a" cfg.Workspace = "/a" + cfg.APIBaseURL = "http://example.com" // write it err = cfg.Write() @@ -34,4 +35,5 @@ func TestUserConfig(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "a", cfg.Token) assert.Equal(t, "/a", cfg.Workspace) + assert.Equal(t, "http://example.com", cfg.APIBaseURL) } From da4ff277820d5e82093abc8f4631292fee26b2b5 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 25 Jun 2018 00:38:20 -0400 Subject: [PATCH 037/544] Swap configure command over to use only UserConfig The code copies over the value into the API config and writes it, for the other commands that still use the APIConfig. --- cmd/configure.go | 6 +++--- cmd/configure_test.go | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 61e8011c9..2dafef264 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -42,7 +42,7 @@ You can also override certain default settings to suit your preferences. if err != nil { return err } - apiCfg.SetDefaults() + apiCfg.BaseURL = usrCfg.APIBaseURL show, err := cmd.Flags().GetBool("show") if err != nil { @@ -51,7 +51,7 @@ You can also override certain default settings to suit your preferences. if show { defer printCurrentConfig() } - client, err := api.NewClient(usrCfg.Token, apiCfg.BaseURL) + client, err := api.NewClient(usrCfg.Token, usrCfg.APIBaseURL) if err != nil { return err } @@ -121,9 +121,9 @@ func initConfigureCmd() { viperUserConfig = viper.New() viperUserConfig.BindPFlag("token", configureCmd.Flags().Lookup("token")) viperUserConfig.BindPFlag("workspace", configureCmd.Flags().Lookup("workspace")) + viperUserConfig.BindPFlag("apibaseurl", configureCmd.Flags().Lookup("api")) viperAPIConfig = viper.New() - viperAPIConfig.BindPFlag("baseurl", configureCmd.Flags().Lookup("api")) } func init() { diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 58c4caf54..fd4f74413 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -24,15 +24,15 @@ func TestConfigure(t *testing.T) { desc: "It writes the flags when there is no config file.", args: []string{"fakeapp", "configure", "--token", "a", "--workspace", "/a", "--api", "http://example.com", "--skip-auth"}, existingUsrCfg: nil, - expectedUsrCfg: &config.UserConfig{Token: "a", Workspace: "/a"}, + expectedUsrCfg: &config.UserConfig{Token: "a", Workspace: "/a", APIBaseURL: "http://example.com"}, existingAPICfg: nil, expectedAPICfg: &config.APIConfig{BaseURL: "http://example.com"}, }, testCase{ desc: "It overwrites the flags in the config file.", args: []string{"fakeapp", "configure", "--token", "b", "--workspace", "/b", "--api", "http://example.com/v2", "--skip-auth"}, - existingUsrCfg: &config.UserConfig{Token: "token-b", Workspace: "/workspace-b"}, - expectedUsrCfg: &config.UserConfig{Token: "b", Workspace: "/b"}, + existingUsrCfg: &config.UserConfig{Token: "token-b", Workspace: "/workspace-b", APIBaseURL: "http://example.com/v1"}, + expectedUsrCfg: &config.UserConfig{Token: "b", Workspace: "/b", APIBaseURL: "http://example.com/v2"}, existingAPICfg: &config.APIConfig{BaseURL: "http://example.com/v1"}, expectedAPICfg: &config.APIConfig{BaseURL: "http://example.com/v2"}, }, @@ -45,6 +45,8 @@ func TestConfigure(t *testing.T) { testCase{ desc: "It gets the default API base URL.", args: []string{"fakeapp", "configure", "--skip-auth"}, + existingUsrCfg: &config.UserConfig{Workspace: "/workspace-c"}, + expectedUsrCfg: &config.UserConfig{Workspace: "/workspace-c", APIBaseURL: "https://v2.exercism.io/api/v1"}, existingAPICfg: &config.APIConfig{}, expectedAPICfg: &config.APIConfig{BaseURL: "https://v2.exercism.io/api/v1"}, }, From de8fc6e3883f6213b8f76e3249aa2d6d93e016fb Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 25 Jun 2018 00:42:56 -0400 Subject: [PATCH 038/544] Switch download command over to using user config --- cmd/download.go | 9 ++------- cmd/download_test.go | 13 +++---------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index e8b9824d1..78f3a9fd0 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -53,20 +53,15 @@ Download other people's solutions by providing the UUID. return err } - apiCfg, err := config.NewAPIConfig() - if err != nil { - return err - } - var slug string if uuid == "" { slug = "latest" } else { slug = uuid } - url := fmt.Sprintf("%s/solutions/%s", apiCfg.BaseURL, slug) + url := fmt.Sprintf("%s/solutions/%s", usrCfg.APIBaseURL, slug) - client, err := api.NewClient(usrCfg.Token, apiCfg.BaseURL) + client, err := api.NewClient(usrCfg.Token, usrCfg.APIBaseURL) if err != nil { return err } diff --git a/cmd/download_test.go b/cmd/download_test.go index 406fad628..1c5b8e5a8 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -56,9 +56,7 @@ func TestDownload(t *testing.T) { mockServer := makeMockServer() defer mockServer.Close() - err := writeFakeUserConfigSetting(cmdTest.TmpDir) - assert.NoError(t, err) - err = writeFakeAPIConfigSetting(mockServer.URL) + err := writeFakeUserConfigSettings(cmdTest.TmpDir, mockServer.URL) assert.NoError(t, err) testCases := []struct { @@ -98,18 +96,13 @@ func TestDownload(t *testing.T) { assert.True(t, os.IsNotExist(err), "It should not write the file if empty.") } -func writeFakeUserConfigSetting(tmpDirPath string) error { +func writeFakeUserConfigSettings(tmpDirPath, serverURL string) error { userCfg := config.NewEmptyUserConfig() userCfg.Workspace = tmpDirPath + userCfg.APIBaseURL = serverURL return userCfg.Write() } -func writeFakeAPIConfigSetting(serverURL string) error { - apiCfg := config.NewEmptyAPIConfig() - apiCfg.BaseURL = serverURL - return apiCfg.Write() -} - func makeMockServer() *httptest.Server { mux := http.NewServeMux() server := httptest.NewServer(mux) From 578b11ead4b35e5011053742e6baf31c10c9f57d Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 25 Jun 2018 00:44:48 -0400 Subject: [PATCH 039/544] Swap prepare command over to using user config --- cmd/prepare.go | 8 ++------ cmd/prepare_test.go | 6 +++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/cmd/prepare.go b/cmd/prepare.go index f24039434..b3247c77a 100644 --- a/cmd/prepare.go +++ b/cmd/prepare.go @@ -47,16 +47,12 @@ func prepareTrack(id string) error { if err != nil { return err } - apiCfg, err := config.NewAPIConfig() - if err != nil { - return err - } - client, err := api.NewClient(usrCfg.Token, apiCfg.BaseURL) + client, err := api.NewClient(usrCfg.Token, usrCfg.APIBaseURL) if err != nil { return err } - url := fmt.Sprintf("%s/tracks/%s", apiCfg.BaseURL, id) + url := fmt.Sprintf("%s/tracks/%s", usrCfg.APIBaseURL, id) req, err := client.NewRequest("GET", url, nil) if err != nil { diff --git a/cmd/prepare_test.go b/cmd/prepare_test.go index 82a515808..a07d48e8a 100644 --- a/cmd/prepare_test.go +++ b/cmd/prepare_test.go @@ -35,9 +35,9 @@ func TestPrepareTrack(t *testing.T) { ts := httptest.NewServer(fakeEndpoint) defer ts.Close() - apiCfg := config.NewEmptyAPIConfig() - apiCfg.BaseURL = ts.URL - err := apiCfg.Write() + cfg := config.NewEmptyUserConfig() + cfg.APIBaseURL = ts.URL + err := cfg.Write() assert.NoError(t, err) cmdTest.App.Execute() From b34523ce97998a7ac95b7d0547b295665e5707e2 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 25 Jun 2018 00:46:13 -0400 Subject: [PATCH 040/544] Switch submit command over to using user config --- cmd/submit.go | 9 ++------- cmd/submit_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index cd3994cf2..c99d0aa71 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -203,16 +203,11 @@ figuring things out if necessary. return err } - apiCfg, err := config.NewAPIConfig() + client, err := api.NewClient(usrCfg.Token, usrCfg.APIBaseURL) if err != nil { return err } - - client, err := api.NewClient(usrCfg.Token, apiCfg.BaseURL) - if err != nil { - return err - } - url := fmt.Sprintf("%s/solutions/%s", apiCfg.BaseURL, solution.ID) + url := fmt.Sprintf("%s/solutions/%s", usrCfg.APIBaseURL, solution.ID) req, err := client.NewRequest("PATCH", url, body) if err != nil { return err diff --git a/cmd/submit_test.go b/cmd/submit_test.go index d7193e243..a20cd5a65 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -108,11 +108,11 @@ func TestSubmit(t *testing.T) { err = cliCfg.Write() assert.NoError(t, err) - // Create a fake API config. - apiCfg, err := config.NewAPIConfig() + // Create a fake config. + cfg, err := config.NewUserConfig() assert.NoError(t, err) - apiCfg.BaseURL = ts.URL - err = apiCfg.Write() + cfg.APIBaseURL = ts.URL + err = cfg.Write() assert.NoError(t, err) // Write mock interactive input to In for the CLI command. From 342c4e43d96752229a2afd0c11f31098f3084ef7 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 25 Jun 2018 00:58:53 -0400 Subject: [PATCH 041/544] Switch status check to only use user config --- cli/status.go | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/cli/status.go b/cli/status.go index 7b0a52834..f79f410eb 100644 --- a/cli/status.go +++ b/cli/status.go @@ -76,7 +76,7 @@ func (status *Status) Check() (string, error) { } status.Configuration = cs - ar, err := newAPIReachabilityStatus() + ar, err := newAPIReachabilityStatus(status.cfg.APIBaseURL) if err != nil { return "", err } @@ -95,16 +95,11 @@ func (status *Status) compile() (string, error) { return bb.String(), nil } -func newAPIReachabilityStatus() (apiReachabilityStatus, error) { - apiCfg, err := config.NewAPIConfig() - if err != nil { - return apiReachabilityStatus{}, nil - } - apiCfg.SetDefaults() +func newAPIReachabilityStatus(baseURL string) (apiReachabilityStatus, error) { ar := apiReachabilityStatus{ Services: []*apiPing{ {Service: "GitHub", URL: "https://api.github.com"}, - {Service: "Exercism", URL: fmt.Sprintf("%s/ping", apiCfg.BaseURL)}, + {Service: "Exercism", URL: fmt.Sprintf("%s/ping", baseURL)}, }, } var wg sync.WaitGroup @@ -145,17 +140,12 @@ func newSystemStatus() systemStatus { } func newConfigurationStatus(status *Status) (configurationStatus, error) { - apiCfg, err := config.NewAPIConfig() - if err != nil { - return configurationStatus{}, err - } - apiCfg.SetDefaults() cs := configurationStatus{ Home: status.cfg.Home, Workspace: status.cfg.Workspace, File: status.cfg.File(), Token: status.cfg.Token, - TokenURL: config.InferSiteURL(apiCfg.BaseURL) + "/my/settings", + TokenURL: config.InferSiteURL(status.cfg.APIBaseURL) + "/my/settings", } if status.Censor && status.cfg.Token != "" { cs.Token = redactToken(status.cfg.Token) From 7ecb7384ef03b3bfec850a14f8e9168cd6b554fd Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 25 Jun 2018 00:47:55 -0400 Subject: [PATCH 042/544] Get rid of api config in configure command --- cmd/configure.go | 23 ++--------------------- cmd/configure_test.go | 16 ---------------- 2 files changed, 2 insertions(+), 37 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 2dafef264..6901e15ce 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -12,7 +12,6 @@ import ( var ( viperUserConfig *viper.Viper - viperAPIConfig *viper.Viper ) // configureCmd configures the command-line client with user-specific settings. @@ -37,13 +36,6 @@ You can also override certain default settings to suit your preferences. usrCfg.Workspace = config.Resolve(usrCfg.Workspace, usrCfg.Home) usrCfg.SetDefaults() - apiCfg := config.NewEmptyAPIConfig() - err = apiCfg.Load(viperAPIConfig) - if err != nil { - return err - } - apiCfg.BaseURL = usrCfg.APIBaseURL - show, err := cmd.Flags().GetBool("show") if err != nil { return err @@ -82,12 +74,7 @@ You can also override certain default settings to suit your preferences. } } - err = usrCfg.Write() - if err != nil { - return err - } - - return apiCfg.Write() + return usrCfg.Write() }, } @@ -96,10 +83,6 @@ func printCurrentConfig() { if err != nil { return } - apiCfg, err := config.NewAPIConfig() - if err != nil { - return - } w := tabwriter.NewWriter(Out, 0, 0, 2, ' ', 0) defer w.Flush() @@ -107,7 +90,7 @@ func printCurrentConfig() { fmt.Fprintln(w, fmt.Sprintf("Config dir:\t%s", config.Dir())) fmt.Fprintln(w, fmt.Sprintf("-t, --token\t%s", usrCfg.Token)) fmt.Fprintln(w, fmt.Sprintf("-w, --workspace\t%s", usrCfg.Workspace)) - fmt.Fprintln(w, fmt.Sprintf("-a, --api\t%s", apiCfg.BaseURL)) + fmt.Fprintln(w, fmt.Sprintf("-a, --api\t%s", usrCfg.APIBaseURL)) fmt.Fprintln(w, "") } @@ -122,8 +105,6 @@ func initConfigureCmd() { viperUserConfig.BindPFlag("token", configureCmd.Flags().Lookup("token")) viperUserConfig.BindPFlag("workspace", configureCmd.Flags().Lookup("workspace")) viperUserConfig.BindPFlag("apibaseurl", configureCmd.Flags().Lookup("api")) - - viperAPIConfig = viper.New() } func init() { diff --git a/cmd/configure_test.go b/cmd/configure_test.go index fd4f74413..052fba7b2 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -1,7 +1,6 @@ package cmd import ( - "os" "runtime" "testing" @@ -14,8 +13,6 @@ type testCase struct { args []string existingUsrCfg *config.UserConfig expectedUsrCfg *config.UserConfig - existingAPICfg *config.APIConfig - expectedAPICfg *config.APIConfig } func TestConfigure(t *testing.T) { @@ -25,16 +22,12 @@ func TestConfigure(t *testing.T) { args: []string{"fakeapp", "configure", "--token", "a", "--workspace", "/a", "--api", "http://example.com", "--skip-auth"}, existingUsrCfg: nil, expectedUsrCfg: &config.UserConfig{Token: "a", Workspace: "/a", APIBaseURL: "http://example.com"}, - existingAPICfg: nil, - expectedAPICfg: &config.APIConfig{BaseURL: "http://example.com"}, }, testCase{ desc: "It overwrites the flags in the config file.", args: []string{"fakeapp", "configure", "--token", "b", "--workspace", "/b", "--api", "http://example.com/v2", "--skip-auth"}, existingUsrCfg: &config.UserConfig{Token: "token-b", Workspace: "/workspace-b", APIBaseURL: "http://example.com/v1"}, expectedUsrCfg: &config.UserConfig{Token: "b", Workspace: "/b", APIBaseURL: "http://example.com/v2"}, - existingAPICfg: &config.APIConfig{BaseURL: "http://example.com/v1"}, - expectedAPICfg: &config.APIConfig{BaseURL: "http://example.com/v2"}, }, testCase{ desc: "It overwrites the flags that are passed, without losing the ones that are not.", @@ -47,8 +40,6 @@ func TestConfigure(t *testing.T) { args: []string{"fakeapp", "configure", "--skip-auth"}, existingUsrCfg: &config.UserConfig{Workspace: "/workspace-c"}, expectedUsrCfg: &config.UserConfig{Workspace: "/workspace-c", APIBaseURL: "https://v2.exercism.io/api/v1"}, - existingAPICfg: &config.APIConfig{}, - expectedAPICfg: &config.APIConfig{BaseURL: "https://v2.exercism.io/api/v1"}, }, } @@ -91,12 +82,5 @@ func makeTest(tc testCase) func(*testing.T) { assert.Equal(t, tc.expectedUsrCfg.Token, usrCfg.Token, tc.desc) assert.Equal(t, tc.expectedUsrCfg.Workspace, usrCfg.Workspace, tc.desc) } - - if tc.expectedAPICfg != nil { - apiCfg, err := config.NewAPIConfig() - assert.NoError(t, err, tc.desc) - assert.Equal(t, tc.expectedAPICfg.BaseURL, apiCfg.BaseURL, tc.desc) - os.Remove(apiCfg.File()) - } } } From ba89219b7b5d82e15de8ecd20999282b8e5cb835 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 25 Jun 2018 00:53:08 -0400 Subject: [PATCH 043/544] Rename config variables in configure command The command used to use both a user config and an API config. It no longer needs to distinguish between the two, so this simplifies a bit. --- cmd/configure.go | 32 ++++++++++++++++---------------- cmd/configure_test.go | 6 +++--- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 6901e15ce..f0a87e431 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -11,7 +11,7 @@ import ( ) var ( - viperUserConfig *viper.Viper + viperConfig *viper.Viper ) // configureCmd configures the command-line client with user-specific settings. @@ -28,13 +28,13 @@ places. You can also override certain default settings to suit your preferences. `, RunE: func(cmd *cobra.Command, args []string) error { - usrCfg := config.NewEmptyUserConfig() - err := usrCfg.Load(viperUserConfig) + cfg := config.NewEmptyUserConfig() + err := cfg.Load(viperConfig) if err != nil { return err } - usrCfg.Workspace = config.Resolve(usrCfg.Workspace, usrCfg.Home) - usrCfg.SetDefaults() + cfg.Workspace = config.Resolve(cfg.Workspace, cfg.Home) + cfg.SetDefaults() show, err := cmd.Flags().GetBool("show") if err != nil { @@ -43,13 +43,13 @@ You can also override certain default settings to suit your preferences. if show { defer printCurrentConfig() } - client, err := api.NewClient(usrCfg.Token, usrCfg.APIBaseURL) + client, err := api.NewClient(cfg.Token, cfg.APIBaseURL) if err != nil { return err } switch { - case usrCfg.Token == "": + case cfg.Token == "": fmt.Fprintln(Out, "There is no token configured, please set it using --token.") case cmd.Flags().Lookup("token").Changed: // User set new token @@ -74,12 +74,12 @@ You can also override certain default settings to suit your preferences. } } - return usrCfg.Write() + return cfg.Write() }, } func printCurrentConfig() { - usrCfg, err := config.NewUserConfig() + cfg, err := config.NewUserConfig() if err != nil { return } @@ -88,9 +88,9 @@ func printCurrentConfig() { fmt.Fprintln(w, "") fmt.Fprintln(w, fmt.Sprintf("Config dir:\t%s", config.Dir())) - fmt.Fprintln(w, fmt.Sprintf("-t, --token\t%s", usrCfg.Token)) - fmt.Fprintln(w, fmt.Sprintf("-w, --workspace\t%s", usrCfg.Workspace)) - fmt.Fprintln(w, fmt.Sprintf("-a, --api\t%s", usrCfg.APIBaseURL)) + fmt.Fprintln(w, fmt.Sprintf("-t, --token\t%s", cfg.Token)) + fmt.Fprintln(w, fmt.Sprintf("-w, --workspace\t%s", cfg.Workspace)) + fmt.Fprintln(w, fmt.Sprintf("-a, --api\t%s", cfg.APIBaseURL)) fmt.Fprintln(w, "") } @@ -101,10 +101,10 @@ func initConfigureCmd() { configureCmd.Flags().BoolP("show", "s", false, "show the current configuration") configureCmd.Flags().BoolP("skip-auth", "", false, "skip online token authorization check") - viperUserConfig = viper.New() - viperUserConfig.BindPFlag("token", configureCmd.Flags().Lookup("token")) - viperUserConfig.BindPFlag("workspace", configureCmd.Flags().Lookup("workspace")) - viperUserConfig.BindPFlag("apibaseurl", configureCmd.Flags().Lookup("api")) + viperConfig = viper.New() + viperConfig.BindPFlag("token", configureCmd.Flags().Lookup("token")) + viperConfig.BindPFlag("workspace", configureCmd.Flags().Lookup("workspace")) + viperConfig.BindPFlag("apibaseurl", configureCmd.Flags().Lookup("api")) } func init() { diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 052fba7b2..78d6f8055 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -76,11 +76,11 @@ func makeTest(tc testCase) func(*testing.T) { tc.expectedUsrCfg.SetDefaults() } - usrCfg, err := config.NewUserConfig() + cfg, err := config.NewUserConfig() assert.NoError(t, err, tc.desc) - assert.Equal(t, tc.expectedUsrCfg.Token, usrCfg.Token, tc.desc) - assert.Equal(t, tc.expectedUsrCfg.Workspace, usrCfg.Workspace, tc.desc) + assert.Equal(t, tc.expectedUsrCfg.Token, cfg.Token, tc.desc) + assert.Equal(t, tc.expectedUsrCfg.Workspace, cfg.Workspace, tc.desc) } } } From ceacc3d716eb8c9a65a6329bbf4a85a124280c88 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 25 Jun 2018 00:59:52 -0400 Subject: [PATCH 044/544] Delete the (finally) superfluous API config --- config/api_config.go | 54 --------------------------------------- config/api_config_test.go | 47 ---------------------------------- 2 files changed, 101 deletions(-) delete mode 100644 config/api_config.go delete mode 100644 config/api_config_test.go diff --git a/config/api_config.go b/config/api_config.go deleted file mode 100644 index 28901cde7..000000000 --- a/config/api_config.go +++ /dev/null @@ -1,54 +0,0 @@ -package config - -import ( - "strings" - - "github.com/spf13/viper" -) - -// APIConfig provides API-specific configuration values. -type APIConfig struct { - *Config - BaseURL string -} - -// NewAPIConfig loads the config file in the config directory. -func NewAPIConfig() (*APIConfig, error) { - cfg := NewEmptyAPIConfig() - - if err := cfg.Load(viper.New()); err != nil { - return nil, err - } - - cfg.SetDefaults() - - return cfg, nil -} - -// SetDefaults ensures that we have all the necessary settings for the API. -func (cfg *APIConfig) SetDefaults() { - if cfg.BaseURL == "" { - cfg.BaseURL = defaultBaseURL - } -} - -// NewEmptyAPIConfig doesn't load the config from file or set default values. -func NewEmptyAPIConfig() *APIConfig { - return &APIConfig{ - Config: New(Dir(), "api"), - } -} - -// Write stores the config to disk. -func (cfg *APIConfig) Write() error { - cfg.BaseURL = strings.Trim(cfg.BaseURL, "/") - cfg.SetDefaults() - - return Write(cfg) -} - -// Load reads a viper configuration into the config. -func (cfg *APIConfig) Load(v *viper.Viper) error { - cfg.readIn(v) - return v.Unmarshal(&cfg) -} diff --git a/config/api_config_test.go b/config/api_config_test.go deleted file mode 100644 index 5b87a662d..000000000 --- a/config/api_config_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package config - -import ( - "io/ioutil" - "os" - "testing" - - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" -) - -func TestAPIConfig(t *testing.T) { - dir, err := ioutil.TempDir("", "api-config") - assert.NoError(t, err) - defer os.RemoveAll(dir) - - cfg := &APIConfig{ - Config: New(dir, "api"), - BaseURL: "http://example.com/v1", - } - - // write it - err = cfg.Write() - assert.NoError(t, err) - - // reload it - cfg = &APIConfig{ - Config: New(dir, "api"), - } - err = cfg.Load(viper.New()) - assert.NoError(t, err) - assert.Equal(t, "http://example.com/v1", cfg.BaseURL) -} - -func TestAPIConfigSetDefaults(t *testing.T) { - // All defaults. - cfg := &APIConfig{} - cfg.SetDefaults() - assert.Equal(t, "https://v2.exercism.io/api/v1", cfg.BaseURL) - - // Override just the base url. - cfg = &APIConfig{ - BaseURL: "http://example.com/v1", - } - cfg.SetDefaults() - assert.Equal(t, "http://example.com/v1", cfg.BaseURL) -} From b3723a1de00ae678c524167e0f08b0c86ffe58eb Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 25 Jun 2018 01:22:45 -0400 Subject: [PATCH 045/544] Move status logic into troubleshoot command file The Status is used only in the troubleshoot command. --- cli/status.go | 223 ------------------ cmd/troubleshoot.go | 207 +++++++++++++++- .../troubleshoot_test.go | 5 +- 3 files changed, 209 insertions(+), 226 deletions(-) delete mode 100644 cli/status.go rename cli/status_test.go => cmd/troubleshoot_test.go (87%) diff --git a/cli/status.go b/cli/status.go deleted file mode 100644 index f79f410eb..000000000 --- a/cli/status.go +++ /dev/null @@ -1,223 +0,0 @@ -package cli - -import ( - "bytes" - "fmt" - "html/template" - "runtime" - "strings" - "sync" - "time" - - "github.com/exercism/cli/config" -) - -// Status represents the results of a CLI self test. -type Status struct { - Censor bool - Version versionStatus - System systemStatus - Configuration configurationStatus - APIReachability apiReachabilityStatus - cli *CLI - cfg config.UserConfig -} - -type versionStatus struct { - Current string - Latest string - Status string - Error error - UpToDate bool -} - -type systemStatus struct { - OS string - Architecture string - Build string -} - -type configurationStatus struct { - Home string - Workspace string - File string - Token string - TokenURL string -} - -type apiReachabilityStatus struct { - Services []*apiPing -} - -type apiPing struct { - Service string - URL string - Status string - Latency time.Duration -} - -// NewStatus prepares a value to perform a diagnostic self-check. -func NewStatus(c *CLI, uc config.UserConfig) Status { - status := Status{ - cli: c, - cfg: uc, - } - return status -} - -// Check runs the CLI's diagnostic self-check. -func (status *Status) Check() (string, error) { - status.Version = newVersionStatus(status.cli) - status.System = newSystemStatus() - - cs, err := newConfigurationStatus(status) - if err != nil { - return "", err - } - status.Configuration = cs - - ar, err := newAPIReachabilityStatus(status.cfg.APIBaseURL) - if err != nil { - return "", err - } - status.APIReachability = ar - - return status.compile() -} -func (status *Status) compile() (string, error) { - t, err := template.New("self-test").Parse(tmplSelfTest) - if err != nil { - return "", err - } - - var bb bytes.Buffer - t.Execute(&bb, status) - return bb.String(), nil -} - -func newAPIReachabilityStatus(baseURL string) (apiReachabilityStatus, error) { - ar := apiReachabilityStatus{ - Services: []*apiPing{ - {Service: "GitHub", URL: "https://api.github.com"}, - {Service: "Exercism", URL: fmt.Sprintf("%s/ping", baseURL)}, - }, - } - var wg sync.WaitGroup - wg.Add(len(ar.Services)) - for _, service := range ar.Services { - go service.Call(&wg) - } - wg.Wait() - return ar, nil -} - -func newVersionStatus(cli *CLI) versionStatus { - vs := versionStatus{ - Current: cli.Version, - } - ok, err := cli.IsUpToDate() - if err == nil { - vs.Latest = cli.LatestRelease.Version() - } else { - vs.Error = fmt.Errorf("Error: %s", err) - } - vs.UpToDate = ok - return vs -} - -func newSystemStatus() systemStatus { - ss := systemStatus{ - OS: runtime.GOOS, - Architecture: runtime.GOARCH, - } - if BuildOS != "" && BuildARCH != "" { - ss.Build = fmt.Sprintf("%s/%s", BuildOS, BuildARCH) - } - if BuildARM != "" { - ss.Build = fmt.Sprintf("%s ARMv%s", ss.Build, BuildARM) - } - return ss -} - -func newConfigurationStatus(status *Status) (configurationStatus, error) { - cs := configurationStatus{ - Home: status.cfg.Home, - Workspace: status.cfg.Workspace, - File: status.cfg.File(), - Token: status.cfg.Token, - TokenURL: config.InferSiteURL(status.cfg.APIBaseURL) + "/my/settings", - } - if status.Censor && status.cfg.Token != "" { - cs.Token = redactToken(status.cfg.Token) - } - return cs, nil -} - -func (ping *apiPing) Call(wg *sync.WaitGroup) { - defer wg.Done() - - now := time.Now() - res, err := HTTPClient.Get(ping.URL) - delta := time.Since(now) - ping.Latency = delta - if err != nil { - ping.Status = err.Error() - return - } - res.Body.Close() - ping.Status = "connected" -} - -func redactToken(token string) string { - str := token[4 : len(token)-3] - redaction := strings.Repeat("*", len(str)) - return string(token[:4]) + redaction + string(token[len(token)-3:]) -} - -const tmplSelfTest = ` -Troubleshooting Information -=========================== - -Version ----------------- -Current: {{ .Version.Current }} -Latest: {{ with .Version.Latest }}{{ . }}{{ else }}{{ end }} -{{ with .Version.Error }} -{{ . }} -{{ end -}} -{{ if not .Version.UpToDate }} -Call 'exercism upgrade' to get the latest version. -See the release notes at https://github.com/exercism/cli/releases/tag/{{ .Version.Latest }} for details. -{{ end }} - -Operating System ----------------- -OS: {{ .System.OS }} -Architecture: {{ .System.Architecture }} -{{ with .System.Build }} -Build: {{ . }} -{{ end }} - -Configuration ----------------- -Home: {{ .Configuration.Home }} -Workspace: {{ .Configuration.Workspace }} -Config: {{ .Configuration.File }} -API key: {{ with .Configuration.Token }}{{ . }}{{ else }} -Find your API key at {{ .Configuration.TokenURL }}{{ end }} - -API Reachability ----------------- -{{ range .APIReachability.Services }} -{{ .Service }}: - * {{ .URL }} - * [{{ .Status }}] - * {{ .Latency }} -{{ end }} - -If you are having trouble please file a GitHub issue at -https://github.com/exercism/exercism.io/issues and include -this information. -{{ if not .Censor }} -Don't share your API key. Keep that private. -{{ end }}` diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go index 9b72042ac..515e564ba 100644 --- a/cmd/troubleshoot.go +++ b/cmd/troubleshoot.go @@ -1,8 +1,13 @@ package cmd import ( + "bytes" "fmt" + "html/template" "net/http" + "runtime" + "strings" + "sync" "time" "github.com/exercism/cli/cli" @@ -32,7 +37,7 @@ command into a GitHub issue so we can help figure out what's going on. return err } - status := cli.NewStatus(c, *cfg) + status := NewStatus(c, *cfg) status.Censor = !fullAPIKey s, err := status.Check() if err != nil { @@ -44,6 +49,206 @@ command into a GitHub issue so we can help figure out what's going on. }, } +// Status represents the results of a CLI self test. +type Status struct { + Censor bool + Version versionStatus + System systemStatus + Configuration configurationStatus + APIReachability apiReachabilityStatus + cli *cli.CLI + cfg config.UserConfig +} + +type versionStatus struct { + Current string + Latest string + Status string + Error error + UpToDate bool +} + +type systemStatus struct { + OS string + Architecture string + Build string +} + +type configurationStatus struct { + Home string + Workspace string + File string + Token string + TokenURL string +} + +type apiReachabilityStatus struct { + Services []*apiPing +} + +type apiPing struct { + Service string + URL string + Status string + Latency time.Duration +} + +// NewStatus prepares a value to perform a diagnostic self-check. +func NewStatus(c *cli.CLI, uc config.UserConfig) Status { + status := Status{ + cli: c, + cfg: uc, + } + return status +} + +// Check runs the CLI's diagnostic self-check. +func (status *Status) Check() (string, error) { + status.Version = newVersionStatus(status.cli) + status.System = newSystemStatus() + status.Configuration = newConfigurationStatus(status) + status.APIReachability = newAPIReachabilityStatus(status.cfg.APIBaseURL) + + return status.compile() +} +func (status *Status) compile() (string, error) { + t, err := template.New("self-test").Parse(tmplSelfTest) + if err != nil { + return "", err + } + + var bb bytes.Buffer + t.Execute(&bb, status) + return bb.String(), nil +} + +func newAPIReachabilityStatus(baseURL string) apiReachabilityStatus { + ar := apiReachabilityStatus{ + Services: []*apiPing{ + {Service: "GitHub", URL: "https://api.github.com"}, + {Service: "Exercism", URL: fmt.Sprintf("%s/ping", baseURL)}, + }, + } + var wg sync.WaitGroup + wg.Add(len(ar.Services)) + for _, service := range ar.Services { + go service.Call(&wg) + } + wg.Wait() + return ar +} + +func newVersionStatus(c *cli.CLI) versionStatus { + vs := versionStatus{ + Current: c.Version, + } + ok, err := c.IsUpToDate() + if err == nil { + vs.Latest = c.LatestRelease.Version() + } else { + vs.Error = fmt.Errorf("Error: %s", err) + } + vs.UpToDate = ok + return vs +} + +func newSystemStatus() systemStatus { + ss := systemStatus{ + OS: runtime.GOOS, + Architecture: runtime.GOARCH, + } + if cli.BuildOS != "" && cli.BuildARCH != "" { + ss.Build = fmt.Sprintf("%s/%s", cli.BuildOS, cli.BuildARCH) + } + if cli.BuildARM != "" { + ss.Build = fmt.Sprintf("%s ARMv%s", ss.Build, cli.BuildARM) + } + return ss +} + +func newConfigurationStatus(status *Status) configurationStatus { + cs := configurationStatus{ + Home: status.cfg.Home, + Workspace: status.cfg.Workspace, + File: status.cfg.File(), + Token: status.cfg.Token, + TokenURL: config.InferSiteURL(status.cfg.APIBaseURL) + "/my/settings", + } + if status.Censor && status.cfg.Token != "" { + cs.Token = redactToken(status.cfg.Token) + } + return cs +} + +func (ping *apiPing) Call(wg *sync.WaitGroup) { + defer wg.Done() + + now := time.Now() + res, err := cli.HTTPClient.Get(ping.URL) + delta := time.Since(now) + ping.Latency = delta + if err != nil { + ping.Status = err.Error() + return + } + res.Body.Close() + ping.Status = "connected" +} + +func redactToken(token string) string { + str := token[4 : len(token)-3] + redaction := strings.Repeat("*", len(str)) + return string(token[:4]) + redaction + string(token[len(token)-3:]) +} + +const tmplSelfTest = ` +Troubleshooting Information +=========================== + +Version +---------------- +Current: {{ .Version.Current }} +Latest: {{ with .Version.Latest }}{{ . }}{{ else }}{{ end }} +{{ with .Version.Error }} +{{ . }} +{{ end -}} +{{ if not .Version.UpToDate }} +Call 'exercism upgrade' to get the latest version. +See the release notes at https://github.com/exercism/cli/releases/tag/{{ .Version.Latest }} for details. +{{ end }} + +Operating System +---------------- +OS: {{ .System.OS }} +Architecture: {{ .System.Architecture }} +{{ with .System.Build }} +Build: {{ . }} +{{ end }} + +Configuration +---------------- +Home: {{ .Configuration.Home }} +Workspace: {{ .Configuration.Workspace }} +Config: {{ .Configuration.File }} +API key: {{ with .Configuration.Token }}{{ . }}{{ else }} +Find your API key at {{ .Configuration.TokenURL }}{{ end }} + +API Reachability +---------------- +{{ range .APIReachability.Services }} +{{ .Service }}: + * {{ .URL }} + * [{{ .Status }}] + * {{ .Latency }} +{{ end }} + +If you are having trouble please file a GitHub issue at +https://github.com/exercism/exercism.io/issues and include +this information. +{{ if not .Censor }} +Don't share your API key. Keep that private. +{{ end }}` + func init() { RootCmd.AddCommand(troubleshootCmd) troubleshootCmd.Flags().BoolVarP(&fullAPIKey, "full-api-key", "f", false, "display the user's full API key, censored by default") diff --git a/cli/status_test.go b/cmd/troubleshoot_test.go similarity index 87% rename from cli/status_test.go rename to cmd/troubleshoot_test.go index 07b901133..95410f1cd 100644 --- a/cli/status_test.go +++ b/cmd/troubleshoot_test.go @@ -1,8 +1,9 @@ -package cli +package cmd import ( "testing" + "github.com/exercism/cli/cli" "github.com/exercism/cli/config" "github.com/stretchr/testify/assert" ) @@ -11,7 +12,7 @@ func TestCensor(t *testing.T) { fakeToken := "1a11111aaaa111aa1a11111a11111aa1" expected := "1a11*************************aa1" - c := New("") + c := cli.New("") cfg := config.NewEmptyUserConfig() cfg.Token = fakeToken From 4de3e37ef89480cbb54a7f50b773a92c1b9b1249 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 25 Jun 2018 01:26:51 -0400 Subject: [PATCH 046/544] Simplify troubleshoot test We don't need all the setup, we're just checking that we can redact the token. --- cmd/troubleshoot.go | 4 ++-- cmd/troubleshoot_test.go | 15 ++------------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go index 515e564ba..434ca0cac 100644 --- a/cmd/troubleshoot.go +++ b/cmd/troubleshoot.go @@ -175,7 +175,7 @@ func newConfigurationStatus(status *Status) configurationStatus { TokenURL: config.InferSiteURL(status.cfg.APIBaseURL) + "/my/settings", } if status.Censor && status.cfg.Token != "" { - cs.Token = redactToken(status.cfg.Token) + cs.Token = redact(status.cfg.Token) } return cs } @@ -195,7 +195,7 @@ func (ping *apiPing) Call(wg *sync.WaitGroup) { ping.Status = "connected" } -func redactToken(token string) string { +func redact(token string) string { str := token[4 : len(token)-3] redaction := strings.Repeat("*", len(str)) return string(token[:4]) + redaction + string(token[len(token)-3:]) diff --git a/cmd/troubleshoot_test.go b/cmd/troubleshoot_test.go index 95410f1cd..e2446022c 100644 --- a/cmd/troubleshoot_test.go +++ b/cmd/troubleshoot_test.go @@ -3,23 +3,12 @@ package cmd import ( "testing" - "github.com/exercism/cli/cli" - "github.com/exercism/cli/config" "github.com/stretchr/testify/assert" ) -func TestCensor(t *testing.T) { +func TestRedact(t *testing.T) { fakeToken := "1a11111aaaa111aa1a11111a11111aa1" expected := "1a11*************************aa1" - c := cli.New("") - - cfg := config.NewEmptyUserConfig() - cfg.Token = fakeToken - - status := NewStatus(c, *cfg) - status.Censor = true - status.Check() - - assert.Equal(t, expected, status.Configuration.Token) + assert.Equal(t, expected, redact(fakeToken)) } From 9d2ef16ed2c49a9bcbd57cabdac7c7bc395c9565 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 25 Jun 2018 01:28:22 -0400 Subject: [PATCH 047/544] Unexport the status stuff in the troubleshoot command --- cmd/troubleshoot.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go index 434ca0cac..b03242a90 100644 --- a/cmd/troubleshoot.go +++ b/cmd/troubleshoot.go @@ -37,9 +37,9 @@ command into a GitHub issue so we can help figure out what's going on. return err } - status := NewStatus(c, *cfg) + status := newStatus(c, *cfg) status.Censor = !fullAPIKey - s, err := status.Check() + s, err := status.check() if err != nil { return err } @@ -93,8 +93,8 @@ type apiPing struct { Latency time.Duration } -// NewStatus prepares a value to perform a diagnostic self-check. -func NewStatus(c *cli.CLI, uc config.UserConfig) Status { +// newStatus prepares a value to perform a diagnostic self-check. +func newStatus(c *cli.CLI, uc config.UserConfig) Status { status := Status{ cli: c, cfg: uc, @@ -102,8 +102,8 @@ func NewStatus(c *cli.CLI, uc config.UserConfig) Status { return status } -// Check runs the CLI's diagnostic self-check. -func (status *Status) Check() (string, error) { +// check runs the CLI's diagnostic self-check. +func (status *Status) check() (string, error) { status.Version = newVersionStatus(status.cli) status.System = newSystemStatus() status.Configuration = newConfigurationStatus(status) From 250319eb712288561aa02b6fae626e80baf27684 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 24 Jun 2018 17:20:54 -0500 Subject: [PATCH 048/544] Inline error handling into download command All the error messages should be crafted in the command layer. --- cmd/download.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index 78f3a9fd0..8d57bb978 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -1,6 +1,7 @@ package cmd import ( + "encoding/json" "errors" "fmt" "io" @@ -89,12 +90,22 @@ Download other people's solutions by providing the UUID. req.URL.RawQuery = q.Encode() } - payload := &downloadPayload{} - res, err := client.Do(req, payload) + res, err := client.Do(req, nil) if err != nil { return err } + payload := &downloadPayload{} + defer res.Body.Close() + if err := json.NewDecoder(res.Body).Decode(payload); err != nil { + return fmt.Errorf("unable to parse API response - %s", err) + } + + if res.StatusCode == http.StatusUnauthorized { + siteURL := config.InferSiteURL(usrCfg.APIBaseURL) + return fmt.Errorf("unauthorized request. Please run the configure command. You can find your API token at %s/my/settings", siteURL) + } + if res.StatusCode != http.StatusOK { switch payload.Error.Type { case "track_ambiguous": From 68e7340864da6ad06782b8ee9a0901b8bc518f3e Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 24 Jun 2018 17:27:06 -0500 Subject: [PATCH 049/544] Inline http error handling into prepare command --- cmd/prepare.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/cmd/prepare.go b/cmd/prepare.go index b3247c77a..041cb2f41 100644 --- a/cmd/prepare.go +++ b/cmd/prepare.go @@ -1,6 +1,7 @@ package cmd import ( + "encoding/json" "errors" "fmt" "net/http" @@ -59,14 +60,20 @@ func prepareTrack(id string) error { return err } - payload := &prepareTrackPayload{} - res, err := client.Do(req, payload) + res, err := client.Do(req, nil) if err != nil { return err } + defer res.Body.Close() + + payload := &prepareTrackPayload{} + + if err := json.NewDecoder(res.Body).Decode(payload); err != nil { + return fmt.Errorf("unable to parse API response - %s", err) + } if res.StatusCode != http.StatusOK { - return errors.New("api call failed") + return errors.New(payload.Error.Message) } cliCfg, err := config.NewCLIConfig() @@ -92,6 +99,10 @@ type prepareTrackPayload struct { Language string `json:"language"` TestPattern string `json:"test_pattern"` } `json:"track"` + Error struct { + Type string `json:"type"` + Message string `json:"message"` + } `json:"error,omitempty"` } func initPrepareCmd() { From aa5e0bd42042b6aae1fae50ca6251f230dd3aa93 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 24 Jun 2018 17:28:57 -0500 Subject: [PATCH 050/544] Remove optional unmarshal in client.Do() If we unmarshal here we have to handle error messages, but the error messages will need to be customized based on the user's configuration, and also based on error types that the API returns. The api package should be a very thin wrapper around http. It shouldn't have to know about the implementation details of the API error messages and CLI error handling. --- api/client.go | 26 ++------------------------ api/client_test.go | 7 ++++++- cmd/download.go | 4 ++-- cmd/prepare.go | 2 +- cmd/submit.go | 2 +- 5 files changed, 12 insertions(+), 29 deletions(-) diff --git a/api/client.go b/api/client.go index 7ad675762..f91eeca36 100644 --- a/api/client.go +++ b/api/client.go @@ -1,13 +1,11 @@ package api import ( - "encoding/json" "fmt" "io" "net/http" "time" - "github.com/exercism/cli/config" "github.com/exercism/cli/debug" ) @@ -62,7 +60,7 @@ func (c *Client) NewRequest(method, url string, body io.Reader) (*http.Request, } // Do performs an http.Request and optionally parses the response body into the given interface. -func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { +func (c *Client) Do(req *http.Request) (*http.Response, error) { debug.DumpRequest(req) res, err := c.Client.Do(req) @@ -70,26 +68,6 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { return nil, err } debug.DumpResponse(res) - - switch res.StatusCode { - case http.StatusNoContent: - return res, nil - case http.StatusInternalServerError: - // TODO: if it's json, and it has an error key, print the message. - return nil, fmt.Errorf("%s", res.Status) - case http.StatusUnauthorized: - siteURL := config.InferSiteURL(c.BaseURL) - return nil, fmt.Errorf("unauthorized request. Please run the configure command. You can find your API token at %s/my/settings", siteURL) - default: - if v != nil { - defer res.Body.Close() - - if err := json.NewDecoder(res.Body).Decode(v); err != nil { - return nil, fmt.Errorf("unable to parse API response - %s", err) - } - } - } - return res, nil } @@ -100,7 +78,7 @@ func (c *Client) ValidateToken() error { if err != nil { return err } - _, err = c.Do(req, nil) + _, err = c.Do(req) return err } diff --git a/api/client_test.go b/api/client_test.go index c73023622..948500f8f 100644 --- a/api/client_test.go +++ b/api/client_test.go @@ -1,6 +1,7 @@ package api import ( + "encoding/json" "fmt" "net/http" "net/http/httptest" @@ -68,8 +69,12 @@ func TestDo(t *testing.T) { req, err := client.NewRequest("GET", ts.URL, nil) assert.NoError(t, err) + res, err := client.Do(req) + assert.NoError(t, err) + var body payload - _, err = client.Do(req, &body) + err = json.NewDecoder(res.Body).Decode(&body) assert.NoError(t, err) + assert.Equal(t, "world", body.Hello) } diff --git a/cmd/download.go b/cmd/download.go index 8d57bb978..31f2635cb 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -90,7 +90,7 @@ Download other people's solutions by providing the UUID. req.URL.RawQuery = q.Encode() } - res, err := client.Do(req, nil) + res, err := client.Do(req) if err != nil { return err } @@ -152,7 +152,7 @@ Download other people's solutions by providing the UUID. return err } - res, err := client.Do(req, nil) + res, err := client.Do(req) if err != nil { return err } diff --git a/cmd/prepare.go b/cmd/prepare.go index 041cb2f41..32c6a33d2 100644 --- a/cmd/prepare.go +++ b/cmd/prepare.go @@ -60,7 +60,7 @@ func prepareTrack(id string) error { return err } - res, err := client.Do(req, nil) + res, err := client.Do(req) if err != nil { return err } diff --git a/cmd/submit.go b/cmd/submit.go index c99d0aa71..c63891067 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -214,7 +214,7 @@ figuring things out if necessary. } req.Header.Set("Content-Type", writer.FormDataContentType()) - resp, err := client.Do(req, nil) + resp, err := client.Do(req) if err != nil { return err } From dbf5fff91ff3e3bde9d836b4066babbe41bac711 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 26 Jun 2018 23:38:36 -0400 Subject: [PATCH 051/544] Tweak payload assignments --- cmd/download.go | 4 ++-- cmd/prepare.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index 31f2635cb..a25457917 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -95,9 +95,9 @@ Download other people's solutions by providing the UUID. return err } - payload := &downloadPayload{} + var payload downloadPayload defer res.Body.Close() - if err := json.NewDecoder(res.Body).Decode(payload); err != nil { + if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { return fmt.Errorf("unable to parse API response - %s", err) } diff --git a/cmd/prepare.go b/cmd/prepare.go index 32c6a33d2..699f06472 100644 --- a/cmd/prepare.go +++ b/cmd/prepare.go @@ -66,9 +66,9 @@ func prepareTrack(id string) error { } defer res.Body.Close() - payload := &prepareTrackPayload{} + var payload prepareTrackPayload - if err := json.NewDecoder(res.Body).Decode(payload); err != nil { + if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { return fmt.Errorf("unable to parse API response - %s", err) } From d89d2b92c38c4b37a28efa6604c3869623f4aa9c Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 24 Jun 2018 17:28:57 -0500 Subject: [PATCH 052/544] Remove optional unmarshal in client.Do() If we unmarshal here we have to handle error messages, but the error messages will need to be customized based on the user's configuration, and also based on error types that the API returns. The api package should be a very thin wrapper around http. It shouldn't have to know about the implementation details of the API error messages and CLI error handling. --- api/client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/client.go b/api/client.go index f91eeca36..a75311d97 100644 --- a/api/client.go +++ b/api/client.go @@ -67,6 +67,7 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) { if err != nil { return nil, err } + debug.DumpResponse(res) return res, nil } From 10f4f4f4ba7f4af7fcefac5c893d6373d63ccbec Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 24 Jun 2018 16:22:24 -0500 Subject: [PATCH 053/544] Combine API and User configs --- cmd/configure_test.go | 7 ++++--- cmd/prepare.go | 6 +++--- cmd/prepare_test.go | 6 +++--- cmd/submit_test.go | 8 +------- config/config.go | 4 ++++ config/user_config.go | 4 ---- 6 files changed, 15 insertions(+), 20 deletions(-) diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 78d6f8055..cc78dd189 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -32,11 +32,11 @@ func TestConfigure(t *testing.T) { testCase{ desc: "It overwrites the flags that are passed, without losing the ones that are not.", args: []string{"fakeapp", "configure", "--token", "c", "--skip-auth"}, - existingUsrCfg: &config.UserConfig{Token: "token-c", Workspace: "/workspace-c"}, - expectedUsrCfg: &config.UserConfig{Token: "c", Workspace: "/workspace-c"}, + existingUsrCfg: &config.UserConfig{Token: "token-c", Workspace: "/workspace-c", APIBaseURL: "http://example.com"}, + expectedUsrCfg: &config.UserConfig{Token: "c", Workspace: "/workspace-c", APIBaseURL: "http://example.com"}, }, testCase{ - desc: "It gets the default API base URL.", + desc: "It gets the default API base url.", args: []string{"fakeapp", "configure", "--skip-auth"}, existingUsrCfg: &config.UserConfig{Workspace: "/workspace-c"}, expectedUsrCfg: &config.UserConfig{Workspace: "/workspace-c", APIBaseURL: "https://v2.exercism.io/api/v1"}, @@ -81,6 +81,7 @@ func makeTest(tc testCase) func(*testing.T) { assert.NoError(t, err, tc.desc) assert.Equal(t, tc.expectedUsrCfg.Token, cfg.Token, tc.desc) assert.Equal(t, tc.expectedUsrCfg.Workspace, cfg.Workspace, tc.desc) + assert.Equal(t, tc.expectedUsrCfg.APIBaseURL, cfg.APIBaseURL, tc.desc) } } } diff --git a/cmd/prepare.go b/cmd/prepare.go index 699f06472..6992a022e 100644 --- a/cmd/prepare.go +++ b/cmd/prepare.go @@ -44,16 +44,16 @@ To customize the CLI to suit your own preferences, use the configure command. } func prepareTrack(id string) error { - usrCfg, err := config.NewUserConfig() + cfg, err := config.NewUserConfig() if err != nil { return err } - client, err := api.NewClient(usrCfg.Token, usrCfg.APIBaseURL) + client, err := api.NewClient(cfg.Token, cfg.APIBaseURL) if err != nil { return err } - url := fmt.Sprintf("%s/tracks/%s", usrCfg.APIBaseURL, id) + url := fmt.Sprintf("%s/tracks/%s", cfg.APIBaseURL, id) req, err := client.NewRequest("GET", url, nil) if err != nil { diff --git a/cmd/prepare_test.go b/cmd/prepare_test.go index a07d48e8a..97930660b 100644 --- a/cmd/prepare_test.go +++ b/cmd/prepare_test.go @@ -35,9 +35,9 @@ func TestPrepareTrack(t *testing.T) { ts := httptest.NewServer(fakeEndpoint) defer ts.Close() - cfg := config.NewEmptyUserConfig() - cfg.APIBaseURL = ts.URL - err := cfg.Write() + usrCfg := config.NewEmptyUserConfig() + usrCfg.APIBaseURL = ts.URL + err := usrCfg.Write() assert.NoError(t, err) cmdTest.App.Execute() diff --git a/cmd/submit_test.go b/cmd/submit_test.go index a20cd5a65..cc0d6f744 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -98,6 +98,7 @@ func TestSubmit(t *testing.T) { // Create a fake user config. usrCfg := config.NewEmptyUserConfig() usrCfg.Workspace = cmdTest.TmpDir + usrCfg.APIBaseURL = ts.URL err = usrCfg.Write() assert.NoError(t, err) @@ -108,13 +109,6 @@ func TestSubmit(t *testing.T) { err = cliCfg.Write() assert.NoError(t, err) - // Create a fake config. - cfg, err := config.NewUserConfig() - assert.NoError(t, err) - cfg.APIBaseURL = ts.URL - err = cfg.Write() - assert.NoError(t, err) - // Write mock interactive input to In for the CLI command. In = strings.NewReader(cmdTest.MockInteractiveResponse) diff --git a/config/config.go b/config/config.go index f19664f89..4be008777 100644 --- a/config/config.go +++ b/config/config.go @@ -12,6 +12,10 @@ import ( "github.com/spf13/viper" ) +var ( + defaultBaseURL = "https://v2.exercism.io/api/v1" +) + // Config is a wrapper around a viper configuration. type Config struct { dir string diff --git a/config/user_config.go b/config/user_config.go index d955583a5..658062d48 100644 --- a/config/user_config.go +++ b/config/user_config.go @@ -9,10 +9,6 @@ import ( "github.com/spf13/viper" ) -var ( - defaultBaseURL = "https://v2.exercism.io/api/v1" -) - // UserConfig contains user-specific settings. type UserConfig struct { *Config From b9e64e8320b46d126b22e39a00c14f1f034165ea Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 24 Jun 2018 17:00:04 -0500 Subject: [PATCH 054/544] Pass token and base url directly to API client It should not have to know the structure of the config. --- api/client.go | 10 +++++----- api/client_test.go | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/api/client.go b/api/client.go index a75311d97..b81e8a527 100644 --- a/api/client.go +++ b/api/client.go @@ -23,15 +23,15 @@ type Client struct { *http.Client ContentType string Token string - BaseURL string + APIBaseURL string } // NewClient returns an Exercism API client. func NewClient(token, baseURL string) (*Client, error) { return &Client{ - Client: DefaultHTTPClient, - Token: token, - BaseURL: baseURL, + Client: DefaultHTTPClient, + Token: token, + APIBaseURL: baseURL, }, nil } @@ -74,7 +74,7 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) { // ValidateToken calls the API to determine whether the token is valid. func (c *Client) ValidateToken() error { - url := fmt.Sprintf("%s/validate_token", c.BaseURL) + url := fmt.Sprintf("%s/validate_token", c.APIBaseURL) req, err := c.NewRequest("GET", url, nil) if err != nil { return err diff --git a/api/client_test.go b/api/client_test.go index 948500f8f..efb1c3c14 100644 --- a/api/client_test.go +++ b/api/client_test.go @@ -33,8 +33,9 @@ func TestNewRequestSetsDefaultHeaders(t *testing.T) { { desc: "Override defaults", client: &Client{ - ContentType: "bogus", Token: "abc123", + APIBaseURL: "http://example.com", + ContentType: "bogus", }, auth: "Bearer abc123", contentType: "bogus", From ec64d3ba0b33775304cdefcba0fca589612534d0 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 7 Jul 2018 08:24:46 +0100 Subject: [PATCH 055/544] Move default base URL into user config file --- config/config.go | 4 ---- config/user_config.go | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/config.go b/config/config.go index 4be008777..f19664f89 100644 --- a/config/config.go +++ b/config/config.go @@ -12,10 +12,6 @@ import ( "github.com/spf13/viper" ) -var ( - defaultBaseURL = "https://v2.exercism.io/api/v1" -) - // Config is a wrapper around a viper configuration. type Config struct { dir string diff --git a/config/user_config.go b/config/user_config.go index 658062d48..d955583a5 100644 --- a/config/user_config.go +++ b/config/user_config.go @@ -9,6 +9,10 @@ import ( "github.com/spf13/viper" ) +var ( + defaultBaseURL = "https://v2.exercism.io/api/v1" +) + // UserConfig contains user-specific settings. type UserConfig struct { *Config From 5df49d506aa5fb672da0d8b591e131c383ec285e Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 7 Jul 2018 08:35:42 +0100 Subject: [PATCH 056/544] Tweak test values in config command test for clarity Use test values that help clarify that each test case is independent of each other test case, and also use descriptive values that help provide better symmetry (new vs old, original vs replacement, unmodified). --- cmd/configure_test.go | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/cmd/configure_test.go b/cmd/configure_test.go index cc78dd189..1f0d1c123 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -18,28 +18,41 @@ type testCase struct { func TestConfigure(t *testing.T) { testCases := []testCase{ testCase{ - desc: "It writes the flags when there is no config file.", - args: []string{"fakeapp", "configure", "--token", "a", "--workspace", "/a", "--api", "http://example.com", "--skip-auth"}, + desc: "It writes the flags when there is no config file.", + args: []string{ + "fakeapp", "configure", "--skip-auth", + "--token", "abc123", + "--workspace", "/workspace", + "--api", "http://api.example.com", + }, existingUsrCfg: nil, - expectedUsrCfg: &config.UserConfig{Token: "a", Workspace: "/a", APIBaseURL: "http://example.com"}, + expectedUsrCfg: &config.UserConfig{Token: "abc123", Workspace: "/workspace", APIBaseURL: "http://api.example.com"}, }, testCase{ - desc: "It overwrites the flags in the config file.", - args: []string{"fakeapp", "configure", "--token", "b", "--workspace", "/b", "--api", "http://example.com/v2", "--skip-auth"}, - existingUsrCfg: &config.UserConfig{Token: "token-b", Workspace: "/workspace-b", APIBaseURL: "http://example.com/v1"}, - expectedUsrCfg: &config.UserConfig{Token: "b", Workspace: "/b", APIBaseURL: "http://example.com/v2"}, + desc: "It overwrites the flags in the config file.", + args: []string{ + "fakeapp", "configure", "--skip-auth", + "--token", "new-token", + "--workspace", "/new-workspace", + "--api", "http://new.example.com", + }, + existingUsrCfg: &config.UserConfig{Token: "old-token", Workspace: "/old-workspace", APIBaseURL: "http://old.example.com"}, + expectedUsrCfg: &config.UserConfig{Token: "new-token", Workspace: "/new-workspace", APIBaseURL: "http://new.example.com"}, }, testCase{ - desc: "It overwrites the flags that are passed, without losing the ones that are not.", - args: []string{"fakeapp", "configure", "--token", "c", "--skip-auth"}, - existingUsrCfg: &config.UserConfig{Token: "token-c", Workspace: "/workspace-c", APIBaseURL: "http://example.com"}, - expectedUsrCfg: &config.UserConfig{Token: "c", Workspace: "/workspace-c", APIBaseURL: "http://example.com"}, + desc: "It overwrites the flags that are passed, without losing the ones that are not.", + args: []string{ + "fakeapp", "configure", "--skip-auth", + "--token", "replacement-token", + }, + existingUsrCfg: &config.UserConfig{Token: "original-token", Workspace: "/unmodified", APIBaseURL: "http://unmodified.example.com"}, + expectedUsrCfg: &config.UserConfig{Token: "replacement-token", Workspace: "/unmodified", APIBaseURL: "http://unmodified.example.com"}, }, testCase{ desc: "It gets the default API base url.", args: []string{"fakeapp", "configure", "--skip-auth"}, - existingUsrCfg: &config.UserConfig{Workspace: "/workspace-c"}, - expectedUsrCfg: &config.UserConfig{Workspace: "/workspace-c", APIBaseURL: "https://v2.exercism.io/api/v1"}, + existingUsrCfg: &config.UserConfig{Workspace: "/configured-workspace"}, + expectedUsrCfg: &config.UserConfig{Workspace: "/configured-workspace", APIBaseURL: "https://v2.exercism.io/api/v1"}, }, } From 3a1946ad6b0ef0aa653ed49ca84448297d737b8a Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 7 Jul 2018 12:24:37 +0100 Subject: [PATCH 057/544] Tweak config track test for readability When naming a method that contains a small conditional, I find it easier to name if the method contains both the exception and the rule. The notIfNeeded method is naming only the case where it's not needed. This changes the logic so that it contains two variations on the same concept ("how acceptable is this"). --- config/track_test.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/config/track_test.go b/config/track_test.go index 55f79ecbb..0f122f763 100644 --- a/config/track_test.go +++ b/config/track_test.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -21,19 +22,19 @@ func TestTrackIgnoreString(t *testing.T) { "proof": false, } - for name, acceptable := range testCases { - testName := name + " should " + notIfNeeded(acceptable) + "be an acceptable name." + for name, ok := range testCases { + testName := fmt.Sprintf("%s is %s", name, acceptability(ok)) t.Run(testName, func(t *testing.T) { - ok, err := track.AcceptFilename(name) + acceptable, err := track.AcceptFilename(name) assert.NoError(t, err, name) - assert.Equal(t, acceptable, ok, testName) + assert.Equal(t, ok, acceptable, testName) }) } } -func notIfNeeded(b bool) string { - if !b { - return "not " +func acceptability(ok bool) string { + if ok { + return "fine" } - return "" + return "not acceptable" } From a4a50f8a69a72689b1700e4a1f9031e8ed9dab3d Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 7 Jul 2018 15:39:59 +0100 Subject: [PATCH 058/544] Remove unnecessary duplication in track test Differentiate between the test name and the failure message. --- config/track_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/config/track_test.go b/config/track_test.go index 0f122f763..aaf392301 100644 --- a/config/track_test.go +++ b/config/track_test.go @@ -23,11 +23,10 @@ func TestTrackIgnoreString(t *testing.T) { } for name, ok := range testCases { - testName := fmt.Sprintf("%s is %s", name, acceptability(ok)) - t.Run(testName, func(t *testing.T) { + t.Run(name, func(t *testing.T) { acceptable, err := track.AcceptFilename(name) assert.NoError(t, err, name) - assert.Equal(t, ok, acceptable, testName) + assert.Equal(t, ok, acceptable, fmt.Sprintf("%s is %s", name, acceptability(ok))) }) } } From 66e605e248c6d3d4ae706eb63822681681c60591 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 04:04:51 +0100 Subject: [PATCH 059/544] Use filepath rather than path for manipulating paths The documentation for the path package says: > The path package should only be used for paths separated by forward > slashes, such as the paths in URLs. This package does not deal with > Windows paths with drive letters or backslashes; to manipulate operating > system paths, use the path/filepath package. --- config/dir.go | 3 +-- config/user_config.go | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/config/dir.go b/config/dir.go index 40654c2bf..1cccc8745 100644 --- a/config/dir.go +++ b/config/dir.go @@ -2,7 +2,6 @@ package config import ( "os" - "path" "path/filepath" "runtime" "strings" @@ -15,7 +14,7 @@ var ( // SetDefaultDirName configures the default directory name based on the name of the binary. func SetDefaultDirName(binaryName string) { - DefaultDirName = strings.Replace(path.Base(binaryName), ".exe", "", 1) + DefaultDirName = strings.Replace(filepath.Base(binaryName), ".exe", "", 1) } // Dir is the configured config home directory. diff --git a/config/user_config.go b/config/user_config.go index d955583a5..9e9013fe0 100644 --- a/config/user_config.go +++ b/config/user_config.go @@ -3,7 +3,7 @@ package config import ( "fmt" "os" - "path" + "path/filepath" "runtime" "github.com/spf13/viper" @@ -88,7 +88,7 @@ func userHome() string { } func defaultWorkspace(home string) string { - dir := path.Join(home, DefaultDirName) + dir := filepath.Join(home, DefaultDirName) _, err := os.Stat(dir) // Sorry about the double negative. if !os.IsNotExist(err) { From 601a875fa190570eb0ec54653418c44feb725e3b Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 06:36:00 +0100 Subject: [PATCH 060/544] Silence test output in download command --- cmd/download_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/download_test.go b/cmd/download_test.go index 1c5b8e5a8..30749a78c 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -44,6 +44,9 @@ const payloadTemplate = ` ` func TestDownload(t *testing.T) { + oldOut := Out + Out = ioutil.Discard + defer func() { Out = oldOut }() cmdTest := &CommandTest{ Cmd: downloadCmd, From 42fe0d030cfe8dee16ed83f322963ac0daa6b01d Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 07:31:32 +0100 Subject: [PATCH 061/544] Distinguish between STDOUT and STDERR We are currently writing everything to STDOUT. We want to be able to do two things: - write certain things to STDERR and other things to STDOUT - overwrite the default streams in the tests to not pollute them This provides a variable for writing to the error stream. It defaults to STDERR, but can be overwritten in tests. See https://www.jstorimer.com/blogs/workingwithcode/7766119-when-to-use-stderr-instead-of-stdout for some useful details about the distinction between the two streams. --- cmd/root.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index 8e26fb568..1b0d9a139 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -19,8 +19,10 @@ var ( // The usage examples and help strings should reflect // the actual name of the binary. BinaryName string - // Out is used to write to the required writer. + // Out is used to write to information. Out io.Writer + // Err is used to write errors. + Err io.Writer // In is used to provide mocked test input (i.e. for prompts). In io.Reader ) @@ -51,6 +53,7 @@ func init() { BinaryName = os.Args[0] config.SetDefaultDirName(BinaryName) Out = os.Stdout + Err = os.Stderr In = os.Stdin api.UserAgent = fmt.Sprintf("github.com/exercism/cli v%s (%s/%s)", Version, runtime.GOOS, runtime.GOARCH) RootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output") From 03acffa2bdf8817279e082d12e22a8abeacda941 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 07:45:59 +0100 Subject: [PATCH 062/544] Clean up output streams in submit command Separate out content that is pipable from content that is not. Ensure that the tests do not print to standard out. --- cmd/submit.go | 18 ++++++++++-------- cmd/submit_test.go | 10 +++++++--- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index c63891067..dcc3ef1d7 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -100,7 +100,7 @@ figuring things out if necessary. } s, ok := option.(*workspace.Solution) if !ok { - fmt.Fprintf(Out, "something went wrong trying to pick that solution, not sure what happened") + fmt.Fprintf(Err, "something went wrong trying to pick that solution, not sure what happened") continue } solution = s @@ -162,10 +162,10 @@ figuring things out if necessary. return err } if strings.ToLower(answer) != "y" { - fmt.Fprintf(Out, "Submit cancelled.\nTry submitting individually instead.") + fmt.Fprintf(Err, "Submit cancelled.\nTry submitting individually instead.") return nil } - fmt.Fprintf(Out, "Submitting files now...") + fmt.Fprintf(Err, "Submitting files now...") } for _, path := range paths { @@ -227,13 +227,15 @@ figuring things out if necessary. } if solution.AutoApprove == true { - fmt.Fprintf(Out, "Your solution has been submitted "+ - "successfully and has been auto-approved. You can complete "+ - "the exercise and unlock the next core exercise at %s\n", - solution.URL) + msg := `Your solution has been submitted successfully and has been auto-approved. +You can complete the exercise and unlock the next core exercise at: +` + fmt.Fprintf(Err, msg) } else { - //TODO + msg := "Your solution has been submitted successfully. View it at:\n" + fmt.Fprintf(Err, msg) } + fmt.Fprintf(Out, "%s\n", solution.URL) return nil }, diff --git a/cmd/submit_test.go b/cmd/submit_test.go index cc0d6f744..4d2cc3b05 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -16,11 +16,15 @@ import ( func TestSubmit(t *testing.T) { oldOut := Out + oldErr := Err oldIn := In Out = ioutil.Discard - - defer func() { Out = oldOut }() - defer func() { In = oldIn }() + Err = ioutil.Discard + defer func() { + Out = oldOut + Err = oldErr + In = oldIn + }() type file struct { relativePath string From e2ace86a2c646c7219190945b3d65c8b3b709948 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 07:47:02 +0100 Subject: [PATCH 063/544] Clean up output streams in configure command Print errors to error stream and info to out stream. Ensure that the tests aren't polluting stdout and stderr. --- cmd/configure.go | 4 ++-- cmd/configure_test.go | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index f0a87e431..2900bdfd9 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -50,7 +50,7 @@ You can also override certain default settings to suit your preferences. switch { case cfg.Token == "": - fmt.Fprintln(Out, "There is no token configured, please set it using --token.") + fmt.Fprintln(Err, "There is no token configured, please set it using --token.") case cmd.Flags().Lookup("token").Changed: // User set new token skipAuth, _ := cmd.Flags().GetBool("skip-auth") @@ -69,7 +69,7 @@ You can also override certain default settings to suit your preferences. if !skipAuth { err = client.ValidateToken() if err != nil { - fmt.Fprintln(Out, err) + fmt.Fprintln(Err, err) } } } diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 1f0d1c123..3e4403bec 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -1,6 +1,7 @@ package cmd import ( + "io/ioutil" "runtime" "testing" @@ -16,6 +17,15 @@ type testCase struct { } func TestConfigure(t *testing.T) { + oldOut := Out + oldErr := Err + Out = ioutil.Discard + Err = ioutil.Discard + defer func() { + Out = oldOut + Err = oldErr + }() + testCases := []testCase{ testCase{ desc: "It writes the flags when there is no config file.", From 00bb36128d8209611fd5f2ca6457a810e9c11616 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 07:47:55 +0100 Subject: [PATCH 064/544] Clean up output streams in download command Stick non-pipeable output onto error stream. Ensure that tests are not polluting stdout and stderr. --- cmd/download.go | 3 ++- cmd/download_test.go | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index a25457917..d4b3904d3 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -183,7 +183,8 @@ Download other people's solutions by providing the UUID. return err } } - fmt.Fprintf(Out, "\nDownloaded to\n%s\n", solution.Dir) + fmt.Fprintf(Err, "\nDownloaded to\n") + fmt.Fprintf(Out, "%s\n", solution.Dir) return nil }, } diff --git a/cmd/download_test.go b/cmd/download_test.go index 30749a78c..c86e9ee5a 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -45,8 +45,13 @@ const payloadTemplate = ` func TestDownload(t *testing.T) { oldOut := Out + oldErr := Err Out = ioutil.Discard - defer func() { Out = oldOut }() + Err = ioutil.Discard + defer func() { + Out = oldOut + Err = oldErr + }() cmdTest := &CommandTest{ Cmd: downloadCmd, From 0bafe24db7f644bf999098beb57f5cd069960829 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 08:55:43 +0100 Subject: [PATCH 065/544] Fix token validation logic The configure command was calling the token validation endpoint, but was not actually bailing or providing an error in the case of an invalid token. We will need to improve the error message to include the link to the url where the token can be found. Also, we should probably not print the current configuration after printing the error message. --- api/client.go | 14 ++++++++------ cmd/configure.go | 17 +++++++++++------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/api/client.go b/api/client.go index b81e8a527..d7a8385ac 100644 --- a/api/client.go +++ b/api/client.go @@ -72,14 +72,16 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) { return res, nil } -// ValidateToken calls the API to determine whether the token is valid. -func (c *Client) ValidateToken() error { +// TokenIsValid calls the API to determine whether the token is valid. +func (c *Client) TokenIsValid() (bool, error) { url := fmt.Sprintf("%s/validate_token", c.APIBaseURL) req, err := c.NewRequest("GET", url, nil) if err != nil { - return err + return false, err } - _, err = c.Do(req) - - return err + resp, err := c.Do(req) + if err != nil { + return false, err + } + return resp.StatusCode == http.StatusOK, nil } diff --git a/cmd/configure.go b/cmd/configure.go index 2900bdfd9..c4481a0d9 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -55,22 +55,27 @@ You can also override certain default settings to suit your preferences. // User set new token skipAuth, _ := cmd.Flags().GetBool("skip-auth") if !skipAuth { - err = client.ValidateToken() + ok, err := client.TokenIsValid() if err != nil { return err } + if !ok { + fmt.Fprintln(Err, "The token is invalid.") + } } default: // Validate existing token - if !show { - defer printCurrentConfig() - } skipAuth, _ := cmd.Flags().GetBool("skip-auth") if !skipAuth { - err = client.ValidateToken() + ok, err := client.TokenIsValid() if err != nil { - fmt.Fprintln(Err, err) + return err } + if !ok { + fmt.Fprintln(Err, "The token is invalid.") + } + + defer printCurrentConfig() } } From cb740601c6188c3175adcff58c37fbf430546bfa Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 10:05:49 +0100 Subject: [PATCH 066/544] Print errors before exiting We are detecting errors when executing commands, but we are exiting with an error code directly, without printing the error. This prints the error to the Err stream. --- cmd/root.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/root.go b/cmd/root.go index 1b0d9a139..d6cbca01c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -45,6 +45,7 @@ Download exercises and submit your solutions.`, // Execute adds all child commands to the root command. func Execute() { if err := RootCmd.Execute(); err != nil { + fmt.Fprintf(Err, err.Error()) os.Exit(-1) } } From 4dbd666ed34bf7c24680fd5cd1e84d0961982385 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 13:42:46 +0100 Subject: [PATCH 067/544] Add a newline to the printed error before exiting --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index d6cbca01c..e8402d03b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -45,7 +45,7 @@ Download exercises and submit your solutions.`, // Execute adds all child commands to the root command. func Execute() { if err := RootCmd.Execute(); err != nil { - fmt.Fprintf(Err, err.Error()) + fmt.Fprintf(Err, "%s\n", err.Error()) os.Exit(-1) } } From 3f37792b1335cf3e0a31f55f3b33840d6c760688 Mon Sep 17 00:00:00 2001 From: nywilken Date: Sun, 8 Jul 2018 10:15:26 -0400 Subject: [PATCH 068/544] Update test cases in track This renames TestTrackIngoreString to TestAcceptFilename. This refactor also changes the name of the subtests to give a better understanding of why the test is failing. --- config/track_test.go | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/config/track_test.go b/config/track_test.go index aaf392301..e6f5911f7 100644 --- a/config/track_test.go +++ b/config/track_test.go @@ -7,33 +7,32 @@ import ( "github.com/stretchr/testify/assert" ) -func TestTrackIgnoreString(t *testing.T) { +func TestAcceptFilename(t *testing.T) { + + testCases := []struct { + desc string + filenames []string + expected bool + }{ + + {"allowed filename", []string{"beacon.ext", "falcon.zip"}, true}, + {"ignored filename", []string{"beacon|txt", "falcon.txt", "proof"}, false}, + } + track := &Track{ IgnorePatterns: []string{ - "con[.]txt", + "con[|.]txt", "pro.f", }, } - testCases := map[string]bool{ - "falcon.txt": false, - "beacon|txt": true, - "beacon.ext": true, - "proof": false, - } - - for name, ok := range testCases { - t.Run(name, func(t *testing.T) { - acceptable, err := track.AcceptFilename(name) - assert.NoError(t, err, name) - assert.Equal(t, ok, acceptable, fmt.Sprintf("%s is %s", name, acceptability(ok))) - }) - } -} - -func acceptability(ok bool) string { - if ok { - return "fine" + for _, tc := range testCases { + for _, filename := range tc.filenames { + t.Run(fmt.Sprintf("%s %s", tc.desc, filename), func(t *testing.T) { + got, err := track.AcceptFilename(filename) + assert.NoError(t, err, fmt.Sprintf("%s %s", tc.desc, filename)) + assert.Equal(t, tc.expected, got, fmt.Sprintf("should return %t for %s, but got %t", tc.expected, tc.desc, got)) + }) + } } - return "not acceptable" } From cf4b4f4f52d357c26c5dace569a52f231e16a25c Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 15:32:40 +0100 Subject: [PATCH 069/544] Do not double-print the command errors I somehow was getting no errors, then when I printed them I got double errors, then going back to how it was before, I'm getting exactly one error printed ... which is what I wanted in the first place. I have no idea what I was doing. --- cmd/root.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index e8402d03b..1b0d9a139 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -45,7 +45,6 @@ Download exercises and submit your solutions.`, // Execute adds all child commands to the root command. func Execute() { if err := RootCmd.Execute(); err != nil { - fmt.Fprintf(Err, "%s\n", err.Error()) os.Exit(-1) } } From 25c68451eea87f0313ab7b6dfd8a4ff767f821fa Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 9 Jul 2018 13:52:35 +0100 Subject: [PATCH 070/544] Add appveyor config file AppVeyor is CI on Windows. --- appveyor.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..6d3991db4 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,21 @@ +--- +version: "{build}" + +clone_folder: c:\gopath\src\github.com\exercism\cli + +environment: + GOPATH: c:\gopath + +init: + - git config --global core.autocrlf input + +install: + - echo %PATH% + - echo %GOPATH% + - go version + - go env + - go get -u github.com/golang/dep/... + - c:\gopath\bin\dep.exe ensure + +build_script: + - for /f "" %%G in ('go list github.com/exercism/cli/... ^| find /i /v "/vendor/"') do ( go test %%G & IF ERRORLEVEL == 1 EXIT 1) From bcfb22b9c47eb96ff67eac48df717f6a5008ccd9 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 9 Jul 2018 14:02:42 +0100 Subject: [PATCH 071/544] Use conditional compilation for failing windows tests **Configure Command** The tests for the configure command are all being rewritten in a separate branch. We will ensure that the new tests are passing. The current ones are not worth fixing since they're going away. **Default Workspace** The default workspace logic is being rewritten in a separate branch. We will make sure we have windows-specific tests there instead of fixing these only to rewrite them later. **Resolve Function** The Resolve function will need to be fixed to be meaningful on Windows. **Locate** We need to simplify the workspace logic. As part of that work (later this week), we will ensure that it works for windows as well. --- cmd/configure_test.go | 2 + config/config.go | 23 ----- config/config_test.go | 24 ----- config/resolve.go | 30 +++++++ config/resolve_notwin_test.go | 35 ++++++++ config/resolve_windows.go | 32 +++++++ config/user_config_windows_test.go | 66 -------------- workspace/workspace_locate_test.go | 137 +++++++++++++++++++++++++++++ workspace/workspace_test.go | 126 -------------------------- 9 files changed, 236 insertions(+), 239 deletions(-) create mode 100644 config/resolve.go create mode 100644 config/resolve_notwin_test.go create mode 100644 config/resolve_windows.go delete mode 100644 config/user_config_windows_test.go create mode 100644 workspace/workspace_locate_test.go diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 3e4403bec..b5abee695 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -1,3 +1,5 @@ +// +build !windows + package cmd import ( diff --git a/config/config.go b/config/config.go index f19664f89..d71f0da6d 100644 --- a/config/config.go +++ b/config/config.go @@ -7,7 +7,6 @@ import ( "os" "path/filepath" "regexp" - "strings" "github.com/spf13/viper" ) @@ -72,25 +71,3 @@ func InferSiteURL(apiURL string) string { re := regexp.MustCompile("^(https?://[^/]*).*") return re.ReplaceAllString(apiURL, "$1") } - -func Resolve(path, home string) string { - if path == "" { - return "" - } - if strings.HasPrefix(path, "~/") { - path = strings.Replace(path, "~/", "", 1) - return filepath.Join(home, path) - } - if filepath.IsAbs(path) { - return filepath.Clean(path) - } - // if using "/dir" on Windows - if strings.HasPrefix(path, "/") { - return filepath.Join(home, filepath.Clean(path)) - } - cwd, err := os.Getwd() - if err != nil { - return path - } - return filepath.Join(cwd, path) -} diff --git a/config/config_test.go b/config/config_test.go index 5b1a16ddb..44808d50c 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -111,27 +111,3 @@ func TestInferSiteURL(t *testing.T) { assert.Equal(t, InferSiteURL(tc.api), tc.url) } } - -func TestResolve(t *testing.T) { - cwd, err := os.Getwd() - assert.NoError(t, err) - - testCases := []struct { - in, out string - }{ - {"", ""}, // don't make wild guesses - {"/home/alice///foobar", "/home/alice/foobar"}, - {"~/foobar", "/home/alice/foobar"}, - {"/foobar/~/noexpand", "/foobar/~/noexpand"}, - {"/no/modification", "/no/modification"}, - {"relative", filepath.Join(cwd, "relative")}, - {"relative///path", filepath.Join(cwd, "relative", "path")}, - } - - for _, tc := range testCases { - testName := "'" + tc.in + "' should be normalized as '" + tc.out + "'" - t.Run(testName, func(t *testing.T) { - assert.Equal(t, tc.out, Resolve(tc.in, "/home/alice"), testName) - }) - } -} diff --git a/config/resolve.go b/config/resolve.go new file mode 100644 index 000000000..93c1645bc --- /dev/null +++ b/config/resolve.go @@ -0,0 +1,30 @@ +package config + +import ( + "os" + "path/filepath" + "strings" +) + +// Resolve cleans up filesystem paths. +func Resolve(path, home string) string { + if path == "" { + return "" + } + if strings.HasPrefix(path, "~/") { + path = strings.Replace(path, "~/", "", 1) + return filepath.Join(home, path) + } + if filepath.IsAbs(path) { + return filepath.Clean(path) + } + // if using "/dir" on Windows + if strings.HasPrefix(path, "/") { + return filepath.Join(home, filepath.Clean(path)) + } + cwd, err := os.Getwd() + if err != nil { + return path + } + return filepath.Join(cwd, path) +} diff --git a/config/resolve_notwin_test.go b/config/resolve_notwin_test.go new file mode 100644 index 000000000..d44b73714 --- /dev/null +++ b/config/resolve_notwin_test.go @@ -0,0 +1,35 @@ +// +build !windows + +package config + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResolve(t *testing.T) { + cwd, err := os.Getwd() + assert.NoError(t, err) + + testCases := []struct { + in, out string + }{ + {"", ""}, // don't make wild guesses + {"/home/alice///foobar", "/home/alice/foobar"}, + {"~/foobar", "/home/alice/foobar"}, + {"/foobar/~/noexpand", "/foobar/~/noexpand"}, + {"/no/modification", "/no/modification"}, + {"relative", filepath.Join(cwd, "relative")}, + {"relative///path", filepath.Join(cwd, "relative", "path")}, + } + + for _, tc := range testCases { + testName := "'" + tc.in + "' should be normalized as '" + tc.out + "'" + t.Run(testName, func(t *testing.T) { + assert.Equal(t, tc.out, Resolve(tc.in, "/home/alice"), testName) + }) + } +} diff --git a/config/resolve_windows.go b/config/resolve_windows.go new file mode 100644 index 000000000..20035a312 --- /dev/null +++ b/config/resolve_windows.go @@ -0,0 +1,32 @@ +package config + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResolve(t *testing.T) { + cwd, err := os.Getwd() + assert.NoError(t, err) + + testCases := []struct { + in, out string + }{ + {"", ""}, // don't make wild guesses + {"C:\\alice\\\\foobar", "C:\\alice\\\\foobar"}, + {"\\foobar\\~\\noexpand", "\\foobar\\~\\noexpand"}, + {"\\no\\modification", "\\no\\modification"}, + {"relative", filepath.Join(cwd, "relative")}, + {"relative\\path", filepath.Join(cwd, "relative", "path")}, + } + + for _, tc := range testCases { + t.Run(tc.in, func(t *testing.T) { + desc := "'" + tc.in + "' should be normalized as '" + tc.out + "'" + assert.Equal(t, tc.out, Resolve(tc.in, ""), desc) + }) + } +} diff --git a/config/user_config_windows_test.go b/config/user_config_windows_test.go deleted file mode 100644 index 49af6be5c..000000000 --- a/config/user_config_windows_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package config - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" -) - -func TestUserConfig(t *testing.T) { - dir, err := ioutil.TempDir("", "user-config") - assert.NoError(t, err) - defer os.RemoveAll(dir) - - cfg := &UserConfig{ - Config: New(dir, "user"), - } - cfg.Token = "a" - cfg.Workspace = "/a" - - // write it - err = cfg.Write() - assert.NoError(t, err) - - // reload it - cfg = &UserConfig{ - Config: New(dir, "user"), - } - err = cfg.Load(viper.New()) - assert.NoError(t, err) - assert.Equal(t, "a", cfg.Token) - assert.Equal(t, filepath.Join(cfg.Home, "a"), cfg.Workspace) -} - -func TestSetDefaultWorkspace(t *testing.T) { - cwd, err := os.Getwd() - assert.NoError(t, err) - - cfg := &UserConfig{Home: "C:\\Users\\alice"} - testCases := []struct { - in, out string - }{ - {"", ""}, // don't make wild guesses - {"C:\\Users\\alice\\\\\\foobar", "C:\\Users\\alice\\foobar"}, - {"C:\\\\\\Users\\alice\\foobar", "C:\\Users\\alice\\foobar"}, - {"/foobar", "C:\\Users\\alice\\foobar"}, - {"~/foobar", "C:\\Users\\alice\\foobar"}, - {"C:\\foobar\\~\\noexpand", "C:\\foobar\\~\\noexpand"}, - {"C:\\no\\modification", "C:\\no\\modification"}, - {"relative", filepath.Join(cwd, "relative")}, - {"relative///path", filepath.Join(cwd, "relative", "path")}, - } - - for _, tc := range testCases { - testName := "'" + tc.in + "' should be normalized as '" + tc.out + "'" - - t.Run(testName, func(t *testing.T) { - cfg.Workspace = tc.in - cfg.SetDefaults() - assert.Equal(t, tc.out, cfg.Workspace, testName) - }) - } -} diff --git a/workspace/workspace_locate_test.go b/workspace/workspace_locate_test.go new file mode 100644 index 000000000..5affc3e94 --- /dev/null +++ b/workspace/workspace_locate_test.go @@ -0,0 +1,137 @@ +// +build !windows + +package workspace + +import ( + "fmt" + "path/filepath" + "runtime" + "sort" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLocateErrors(t *testing.T) { + _, cwd, _, _ := runtime.Caller(0) + root := filepath.Join(cwd, "..", "..", "fixtures", "locate-exercise") + + ws := New(filepath.Join(root, "workspace")) + + testCases := []struct { + desc, arg string + errFn func(error) bool + }{ + { + desc: "absolute path outside of workspace", + arg: filepath.Join(root, "equipment", "bat"), + errFn: IsNotInWorkspace, + }, + { + desc: "absolute path in workspace not found", + arg: filepath.Join(ws.Dir, "creatures", "pig"), + errFn: IsNotExist, + }, + { + desc: "relative path is outside of workspace", + arg: filepath.Join("..", "fixtures", "locate-exercise", "equipment", "bat"), + errFn: IsNotInWorkspace, + }, + { + desc: "relative path in workspace not found", + arg: filepath.Join("..", "fixtures", "locate-exercise", "workspace", "creatures", "pig"), + errFn: IsNotExist, + }, + { + desc: "exercise name not found in workspace", + arg: "pig", + errFn: IsNotExist, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + _, err := ws.Locate(tc.arg) + assert.True(t, tc.errFn(err), fmt.Sprintf("test: %s (arg: %s), %#v", tc.desc, tc.arg, err)) + }) + } +} + +type locateTestCase struct { + desc string + workspace Workspace + in string + out []string +} + +func TestLocate(t *testing.T) { + _, cwd, _, _ := runtime.Caller(0) + root := filepath.Join(cwd, "..", "..", "fixtures", "locate-exercise") + + wsPrimary := New(filepath.Join(root, "workspace")) + + testCases := []locateTestCase{ + { + desc: "find absolute path within workspace", + workspace: wsPrimary, + in: filepath.Join(wsPrimary.Dir, "creatures", "horse"), + out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, + }, + { + desc: "find relative path within workspace", + workspace: wsPrimary, + in: filepath.Join("..", "fixtures", "locate-exercise", "workspace", "creatures", "horse"), + out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, + }, + { + desc: "find by name in default location", + workspace: wsPrimary, + in: "horse", + out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, + }, + { + desc: "find by name in a subtree", + workspace: wsPrimary, + in: "fly", + out: []string{filepath.Join(wsPrimary.Dir, "friends", "alice", "creatures", "fly")}, + }, + { + desc: "don't be confused by a file named the same as an exercise", + workspace: wsPrimary, + in: "duck", + out: []string{filepath.Join(wsPrimary.Dir, "creatures", "duck")}, + }, + { + desc: "find all the exercises with the same name", + workspace: wsPrimary, + in: "bat", + out: []string{ + filepath.Join(wsPrimary.Dir, "creatures", "bat"), + filepath.Join(wsPrimary.Dir, "friends", "alice", "creatures", "bat"), + }, + }, + { + desc: "find copies of exercise with suffix", + workspace: wsPrimary, + in: "crane", + out: []string{ + filepath.Join(wsPrimary.Dir, "creatures", "crane"), + filepath.Join(wsPrimary.Dir, "creatures", "crane-2"), + }, + }, + } + + testLocate(testCases, t) +} + +func testLocate(testCases []locateTestCase, t *testing.T) { + for _, tc := range testCases { + dirs, err := tc.workspace.Locate(tc.in) + + sort.Strings(dirs) + sort.Strings(tc.out) + + assert.NoError(t, err, tc.desc) + assert.Equal(t, tc.out, dirs, tc.desc) + } +} diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go index 81f6f7be8..3c733663b 100644 --- a/workspace/workspace_test.go +++ b/workspace/workspace_test.go @@ -1,139 +1,13 @@ package workspace import ( - "fmt" "path/filepath" "runtime" - "sort" "testing" "github.com/stretchr/testify/assert" ) -func TestLocateErrors(t *testing.T) { - _, cwd, _, _ := runtime.Caller(0) - root := filepath.Join(cwd, "..", "..", "fixtures", "locate-exercise") - - ws := New(filepath.Join(root, "workspace")) - - testCases := []struct { - desc, arg string - errFn func(error) bool - }{ - { - desc: "absolute path outside of workspace", - arg: filepath.Join(root, "equipment", "bat"), - errFn: IsNotInWorkspace, - }, - { - desc: "absolute path in workspace not found", - arg: filepath.Join(ws.Dir, "creatures", "pig"), - errFn: IsNotExist, - }, - { - desc: "relative path is outside of workspace", - arg: filepath.Join("..", "fixtures", "locate-exercise", "equipment", "bat"), - errFn: IsNotInWorkspace, - }, - { - desc: "relative path in workspace not found", - arg: filepath.Join("..", "fixtures", "locate-exercise", "workspace", "creatures", "pig"), - errFn: IsNotExist, - }, - { - desc: "exercise name not found in workspace", - arg: "pig", - errFn: IsNotExist, - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - _, err := ws.Locate(tc.arg) - assert.True(t, tc.errFn(err), fmt.Sprintf("test: %s (arg: %s), %#v", tc.desc, tc.arg, err)) - }) - } -} - -type locateTestCase struct { - desc string - workspace Workspace - in string - out []string -} - -func TestLocate(t *testing.T) { - _, cwd, _, _ := runtime.Caller(0) - root := filepath.Join(cwd, "..", "..", "fixtures", "locate-exercise") - - wsPrimary := New(filepath.Join(root, "workspace")) - - testCases := []locateTestCase{ - { - desc: "find absolute path within workspace", - workspace: wsPrimary, - in: filepath.Join(wsPrimary.Dir, "creatures", "horse"), - out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, - }, - { - desc: "find relative path within workspace", - workspace: wsPrimary, - in: filepath.Join("..", "fixtures", "locate-exercise", "workspace", "creatures", "horse"), - out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, - }, - { - desc: "find by name in default location", - workspace: wsPrimary, - in: "horse", - out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, - }, - { - desc: "find by name in a subtree", - workspace: wsPrimary, - in: "fly", - out: []string{filepath.Join(wsPrimary.Dir, "friends", "alice", "creatures", "fly")}, - }, - { - desc: "don't be confused by a file named the same as an exercise", - workspace: wsPrimary, - in: "duck", - out: []string{filepath.Join(wsPrimary.Dir, "creatures", "duck")}, - }, - { - desc: "find all the exercises with the same name", - workspace: wsPrimary, - in: "bat", - out: []string{ - filepath.Join(wsPrimary.Dir, "creatures", "bat"), - filepath.Join(wsPrimary.Dir, "friends", "alice", "creatures", "bat"), - }, - }, - { - desc: "find copies of exercise with suffix", - workspace: wsPrimary, - in: "crane", - out: []string{ - filepath.Join(wsPrimary.Dir, "creatures", "crane"), - filepath.Join(wsPrimary.Dir, "creatures", "crane-2"), - }, - }, - } - - testLocate(testCases, t) -} - -func testLocate(testCases []locateTestCase, t *testing.T) { - for _, tc := range testCases { - dirs, err := tc.workspace.Locate(tc.in) - - sort.Strings(dirs) - sort.Strings(tc.out) - - assert.NoError(t, err, tc.desc) - assert.Equal(t, tc.out, dirs, tc.desc) - } -} - func TestSolutionPath(t *testing.T) { root := filepath.Join("..", "fixtures", "solution-path", "creatures") ws := New(root) From b156330134186fa5470852a72d400cac64643320 Mon Sep 17 00:00:00 2001 From: "A.L. Drake" Date: Mon, 18 Jun 2018 21:59:04 -0400 Subject: [PATCH 072/544] Update test to reflect new submit cmd requirements --- cmd/submit_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 4d2cc3b05..1b0e14259 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -48,7 +48,7 @@ func TestSubmit(t *testing.T) { Cmd: submitCmd, InitFn: initSubmitCmd, MockInteractiveResponse: "\n", - Args: []string{"fakeapp", "submit", "bogus-exercise"}, + Args: []string{"fakeapp", "submit", "-e", "bogus-exercise", "-t", "bogus-track"}, } cmdTest.Setup(t) defer cmdTest.Teardown(t) From b0d5928a74e2ca5f5b6f21a20d6bce755a14902a Mon Sep 17 00:00:00 2001 From: "A.L. Drake" Date: Mon, 18 Jun 2018 22:35:49 -0400 Subject: [PATCH 073/544] Handle track and exercise flags in submit command --- cmd/submit.go | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index dcc3ef1d7..9eb0b11d4 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -36,23 +36,35 @@ If called with the name of an exercise, it will work out which track it is on and submit it. The command will ask for help figuring things out if necessary. `, + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - usrCfg, err := config.NewUserConfig() + // check input before doing any other work + exercise, err := cmd.Flags().GetString("exercise") if err != nil { return err } - cliCfg, err := config.NewCLIConfig() + trackId, err := cmd.Flags().GetString("track") if err != nil { return err } - if len(args) == 0 { - cwd, err := os.Getwd() - if err != nil { - return err - } - args = []string{cwd} + if len(args) == 0 && !(exercise != "" && trackId != "") { + return errors.New("must use --exercise and --track together with no args") + } + + if len(args) > 0 && (exercise != "" || trackId != "") { + return errors.New("can't use flags and arguments together") + } + + usrCfg, err := config.NewUserConfig() + if err != nil { + return err + } + + cliCfg, err := config.NewCLIConfig() + if err != nil { + return err } // TODO: make sure we get the workspace configured. @@ -65,16 +77,20 @@ figuring things out if necessary. } ws := workspace.New(usrCfg.Workspace) + + // create directory from track and exercise slugs if needed + if (trackId != "" && exercise != "") { + args = []string{filepath.Join(ws.Dir, trackId, exercise)} + } + tx, err := workspace.NewTransmission(ws.Dir, args) if err != nil { return err } - dirs, err := ws.Locate(tx.Dir) if err != nil { return err } - sx, err := workspace.NewSolutions(dirs) if err != nil { return err @@ -110,6 +126,7 @@ figuring things out if necessary. if !solution.IsRequester { return errors.New("not your solution") } + track := cliCfg.Tracks[solution.Track] if track == nil { err := prepareTrack(solution.Track) @@ -242,7 +259,8 @@ You can complete the exercise and unlock the next core exercise at: } func initSubmitCmd() { - // TODO + submitCmd.Flags().StringP("track", "t", "", "the track ID") + submitCmd.Flags().StringP("exercise", "e", "", "the exercise ID") } func init() { From 88055905e007e78049c74f8b475befc2e641849c Mon Sep 17 00:00:00 2001 From: "A.L. Drake" Date: Mon, 18 Jun 2018 22:55:02 -0400 Subject: [PATCH 074/544] Refactor submit command tests to use table tests --- cmd/submit_test.go | 147 +++++++++++++++++++++++++-------------------- 1 file changed, 82 insertions(+), 65 deletions(-) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 1b0e14259..55d359d35 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -43,86 +43,103 @@ func TestSubmit(t *testing.T) { relativePath: "README.md", contents: "The readme.", } - - cmdTest := &CommandTest{ + // make a list of tests + cmdTestFlags := &CommandTest{ Cmd: submitCmd, InitFn: initSubmitCmd, MockInteractiveResponse: "\n", Args: []string{"fakeapp", "submit", "-e", "bogus-exercise", "-t", "bogus-track"}, } - cmdTest.Setup(t) - defer cmdTest.Teardown(t) - - // Create a temp dir for the config and the exercise files. - dir := filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise") - os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) - - solution := &workspace.Solution{ - ID: "bogus-solution-uuid", - Track: "bogus-track", - Exercise: "bogus-exercise", - IsRequester: true, + cmdTestRelativeDir := &CommandTest{ + Cmd: submitCmd, + InitFn: initSubmitCmd, + MockInteractiveResponse: "\n", + Args: []string{"fakeapp", "submit", filepath.Join("bogus-track", "bogus-exercise")}, } - err := solution.Write(dir) - assert.NoError(t, err) - - for _, file := range []file{file1, file2, file3} { - err := ioutil.WriteFile(filepath.Join(dir, file.relativePath), []byte(file.contents), os.FileMode(0755)) - assert.NoError(t, err) + tests := []*CommandTest{ + cmdTestFlags, + cmdTestRelativeDir, } + for _, cmdTest := range tests { + cmdTest.Setup(t) + defer cmdTest.Teardown(t) - // The fake endpoint will populate this when it receives the call from the command. - submittedFiles := map[string]string{} + // handle case when directory to submit needs the tmp dir prefix + if len(cmdTest.Args) == 3 { + cmdTest.Args[2] = filepath.Join(cmdTest.TmpDir, cmdTest.Args[2]) + } + + // Create a temp dir for the config and the exercise files. + dir := filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) + + solution := &workspace.Solution{ + ID: "bogus-solution-uuid", + Track: "bogus-track", + Exercise: "bogus-exercise", + IsRequester: true, + } + err := solution.Write(dir) + assert.NoError(t, err) - // Set up the test server. - fakeEndpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - err := r.ParseMultipartForm(2 << 10) - if err != nil { - t.Fatal(err) + for _, file := range []file{file1, file2, file3} { + err := ioutil.WriteFile(filepath.Join(dir, file.relativePath), []byte(file.contents), os.FileMode(0755)) + assert.NoError(t, err) } - mf := r.MultipartForm - files := mf.File["files[]"] - for _, fileHeader := range files { - file, err := fileHeader.Open() + // The fake endpoint will populate this when it receives the call from the command. + submittedFiles := map[string]string{} + + // Set up the test server. + fakeEndpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := r.ParseMultipartForm(2 << 10) if err != nil { t.Fatal(err) } - defer file.Close() - body, err := ioutil.ReadAll(file) - if err != nil { - t.Fatal(err) + mf := r.MultipartForm + + files := mf.File["files[]"] + for _, fileHeader := range files { + file, err := fileHeader.Open() + if err != nil { + t.Fatal(err) + } + defer file.Close() + body, err := ioutil.ReadAll(file) + if err != nil { + t.Fatal(err) + } + submittedFiles[fileHeader.Filename] = string(body) } - submittedFiles[fileHeader.Filename] = string(body) + }) + ts := httptest.NewServer(fakeEndpoint) + defer ts.Close() + + // Create a fake user config. + usrCfg := config.NewEmptyUserConfig() + usrCfg.Workspace = cmdTest.TmpDir + usrCfg.APIBaseURL = ts.URL + err = usrCfg.Write() + assert.NoError(t, err) + + // Create a fake CLI config. + cliCfg, err := config.NewCLIConfig() + assert.NoError(t, err) + cliCfg.Tracks["bogus-track"] = config.NewTrack("bogus-track") + err = cliCfg.Write() + assert.NoError(t, err) + + // Write mock interactive input to In for the CLI command. + In = strings.NewReader(cmdTest.MockInteractiveResponse) + + // Execute the command! + cmdTest.App.Execute() + + // We got only the file we expected. + assert.Equal(t, 2, len(submittedFiles)) + for _, file := range []file{file1, file2} { + path := string(os.PathSeparator) + file.relativePath + assert.Equal(t, file.contents, submittedFiles[path]) } - }) - ts := httptest.NewServer(fakeEndpoint) - defer ts.Close() - - // Create a fake user config. - usrCfg := config.NewEmptyUserConfig() - usrCfg.Workspace = cmdTest.TmpDir - usrCfg.APIBaseURL = ts.URL - err = usrCfg.Write() - assert.NoError(t, err) - - // Create a fake CLI config. - cliCfg, err := config.NewCLIConfig() - assert.NoError(t, err) - cliCfg.Tracks["bogus-track"] = config.NewTrack("bogus-track") - err = cliCfg.Write() - assert.NoError(t, err) - - // Write mock interactive input to In for the CLI command. - In = strings.NewReader(cmdTest.MockInteractiveResponse) - - // Execute the command! - cmdTest.App.Execute() - - // We got only the file we expected. - assert.Equal(t, 2, len(submittedFiles)) - for _, file := range []file{file1, file2} { - path := string(os.PathSeparator) + file.relativePath - assert.Equal(t, file.contents, submittedFiles[path]) } } From d5756b426224c0ef360e3fd020b8f3b3fe73473c Mon Sep 17 00:00:00 2001 From: "A.L. Drake" Date: Thu, 21 Jun 2018 22:49:06 -0400 Subject: [PATCH 075/544] Implement support for files flag on submit command --- cmd/submit.go | 23 ++++++++++++++++++----- cmd/submit_test.go | 19 +++++++++++++++++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 9eb0b11d4..823b4688e 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -49,12 +49,22 @@ figuring things out if necessary. return err } - if len(args) == 0 && !(exercise != "" && trackId != "") { - return errors.New("must use --exercise and --track together with no args") + files, err := cmd.Flags().GetStringSlice("files") + if err != nil { + return err + } + + // Verify correct usage of flags/args + if len(args) == 0 && len(files) == 0 && !(exercise != "" && trackId != "") { + return errors.New("must use --exercise and --track together") + } + + if len(args) > 0 && (exercise != "" || trackId != "" || len(files) > 0) { + return errors.New("can't use flags and directory arguments together") } - if len(args) > 0 && (exercise != "" || trackId != "") { - return errors.New("can't use flags and arguments together") + if len(files) > 0 && len(args) > 0 { + return errors.New("can't submit files and a directory together") } usrCfg, err := config.NewUserConfig() @@ -79,8 +89,10 @@ figuring things out if necessary. ws := workspace.New(usrCfg.Workspace) // create directory from track and exercise slugs if needed - if (trackId != "" && exercise != "") { + if trackId != "" && exercise != "" { args = []string{filepath.Join(ws.Dir, trackId, exercise)} + } else if len(files) > 0 { + args = files } tx, err := workspace.NewTransmission(ws.Dir, args) @@ -261,6 +273,7 @@ You can complete the exercise and unlock the next core exercise at: func initSubmitCmd() { submitCmd.Flags().StringP("track", "t", "", "the track ID") submitCmd.Flags().StringP("exercise", "e", "", "the exercise ID") + submitCmd.Flags().StringSliceP("files", "f", make([]string, 0), "files to submit") } func init() { diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 55d359d35..74fc112a3 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" "io/ioutil" "net/http" "net/http/httptest" @@ -56,19 +57,33 @@ func TestSubmit(t *testing.T) { MockInteractiveResponse: "\n", Args: []string{"fakeapp", "submit", filepath.Join("bogus-track", "bogus-exercise")}, } + cmdTestFilesFlag := &CommandTest{ + Cmd: submitCmd, + InitFn: initSubmitCmd, + MockInteractiveResponse: "\n", + Args: []string{"fakeapp", "submit", "--files"}, + } tests := []*CommandTest{ cmdTestFlags, cmdTestRelativeDir, + cmdTestFilesFlag, } for _, cmdTest := range tests { cmdTest.Setup(t) defer cmdTest.Teardown(t) // handle case when directory to submit needs the tmp dir prefix - if len(cmdTest.Args) == 3 { + if cmdTest.Args[2] == "--files" { + // prefix each file + filenames := make([]string, 2) + for i, file := range []file{file1, file2} { + filenames[i] = filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", file.relativePath) + } + filenameString := strings.Join(filenames, ",") + cmdTest.Args[2] = fmt.Sprintf("-f=%s", filenameString) + } else if len(cmdTest.Args) == 3 { cmdTest.Args[2] = filepath.Join(cmdTest.TmpDir, cmdTest.Args[2]) } - // Create a temp dir for the config and the exercise files. dir := filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise") os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) From 8182cdd9b1714bb5f04d1e9e010f3c7ae220fa57 Mon Sep 17 00:00:00 2001 From: "A.L. Drake" Date: Sun, 1 Jul 2018 22:15:43 -0400 Subject: [PATCH 076/544] Rewrite comments in submit command to be more idiomatic --- cmd/submit.go | 6 ++++-- cmd/submit_test.go | 5 ++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 823b4688e..e55a055d0 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -38,7 +38,7 @@ figuring things out if necessary. `, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - // check input before doing any other work + // Validate input before doing any other work exercise, err := cmd.Flags().GetString("exercise") if err != nil { return err @@ -88,7 +88,7 @@ figuring things out if necessary. ws := workspace.New(usrCfg.Workspace) - // create directory from track and exercise slugs if needed + // Create directory from track and exercise slugs if needed if trackId != "" && exercise != "" { args = []string{filepath.Join(ws.Dir, trackId, exercise)} } else if len(files) > 0 { @@ -99,10 +99,12 @@ figuring things out if necessary. if err != nil { return err } + dirs, err := ws.Locate(tx.Dir) if err != nil { return err } + sx, err := workspace.NewSolutions(dirs) if err != nil { return err diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 74fc112a3..a5306ebc0 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -44,7 +44,7 @@ func TestSubmit(t *testing.T) { relativePath: "README.md", contents: "The readme.", } - // make a list of tests + // Make a list of test commands cmdTestFlags := &CommandTest{ Cmd: submitCmd, InitFn: initSubmitCmd, @@ -72,9 +72,8 @@ func TestSubmit(t *testing.T) { cmdTest.Setup(t) defer cmdTest.Teardown(t) - // handle case when directory to submit needs the tmp dir prefix + // Prefix submitted filenames with correct temporary directory if cmdTest.Args[2] == "--files" { - // prefix each file filenames := make([]string, 2) for i, file := range []file{file1, file2} { filenames[i] = filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", file.relativePath) From 85b47ac1fede719347f7f868f3ff35517613df18 Mon Sep 17 00:00:00 2001 From: "A.L. Drake" Date: Thu, 5 Jul 2018 21:23:11 -0400 Subject: [PATCH 077/544] Tweak submit command for clarity - return more informative error messages - rename trackId to trackID to be idiomatic - handle missing flags explicitly --- cmd/submit.go | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index e55a055d0..8ecb8050c 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -44,7 +44,7 @@ figuring things out if necessary. return err } - trackId, err := cmd.Flags().GetString("track") + trackID, err := cmd.Flags().GetString("track") if err != nil { return err } @@ -54,17 +54,28 @@ figuring things out if necessary. return err } - // Verify correct usage of flags/args - if len(args) == 0 && len(files) == 0 && !(exercise != "" && trackId != "") { - return errors.New("must use --exercise and --track together") + // Verify that both --track and --exercise are used together + if len(args) == 0 && len(files) == 0 && !(exercise != "" && trackID != "") { + // Are they both missing? + if exercise == "" && trackID == "" { + return errors.New("Please use the --exercise/--trackID flags to submit without an explicit directory or files.") + } + // Guess that --trackID is missing, unless it's not + present, missing := "--exercise", "--track" + if trackID != "" { + present, missing = missing, present + } + // Help user correct CLI command + missingFlagMessage := fmt.Sprintf("You specified %s, please also include %s.", present, missing) + return errors.New(missingFlagMessage) } - if len(args) > 0 && (exercise != "" || trackId != "" || len(files) > 0) { - return errors.New("can't use flags and directory arguments together") + if len(args) > 0 && (exercise != "" || trackID != "") { + return errors.New("You are submitting a directory. We will infer the track and exercise from that. Please re-run the submit command without the flags.") } if len(files) > 0 && len(args) > 0 { - return errors.New("can't submit files and a directory together") + return errors.New("You can submit either a list of files, or a directory, but not both.") } usrCfg, err := config.NewUserConfig() @@ -89,8 +100,8 @@ figuring things out if necessary. ws := workspace.New(usrCfg.Workspace) // Create directory from track and exercise slugs if needed - if trackId != "" && exercise != "" { - args = []string{filepath.Join(ws.Dir, trackId, exercise)} + if trackID != "" && exercise != "" { + args = []string{filepath.Join(ws.Dir, trackID, exercise)} } else if len(files) > 0 { args = files } From b8b8e0dbc657124e2b7603a68ac2cf9ffaecc86e Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 04:33:07 +0100 Subject: [PATCH 078/544] Add a configuration object to inject into commands This is part of a larger refactoring that is attempting to tease apart some really painful coupling that I introduced before I really understood how to use viper. --- config/configuration.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 config/configuration.go diff --git a/config/configuration.go b/config/configuration.go new file mode 100644 index 000000000..8e53d9832 --- /dev/null +++ b/config/configuration.go @@ -0,0 +1,30 @@ +package config + +import "github.com/spf13/viper" + +// Configuration lets us inject configuration options into commands. +// Note that we are slowly working towards getting rid of the +// config.Config, config.UserConfig, and config.CLIConfig types. +// Once we do, we can rename this type to Config, and get rid of the +// User and CLI fields. +type Configuration struct { + Home string + Dir string + DefaultBaseURL string + DefaultWorkspaceDir string + UserViperConfig *viper.Viper + UserConfig *UserConfig + CLI *CLIConfig +} + +// NewConfiguration provides a configuration with default values. +func NewConfiguration() Configuration { + home := userHome() + + return Configuration{ + Dir: Dir(), + Home: home, + DefaultBaseURL: defaultBaseURL, + DefaultWorkspaceDir: defaultWorkspace(home), + } +} From 80d6fa4cb4b26b94dec4980aa23e963fd29c6cec Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 9 Jul 2018 09:29:49 +0100 Subject: [PATCH 079/544] Consolidate configuration-related methods The new Configuration type (which will eventually be renamed to Config, once we get rid of the complicated Config and UserConfig stuff) seems to be a better place for the Dir, userHome, defaultWorkspace, and SetDefaultDirName functions. --- config/configuration.go | 79 ++++++++++++++++++++++++++++++++++++++++- config/dir.go | 45 ----------------------- config/user_config.go | 41 +-------------------- 3 files changed, 79 insertions(+), 86 deletions(-) delete mode 100644 config/dir.go diff --git a/config/configuration.go b/config/configuration.go index 8e53d9832..776a77ee4 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -1,6 +1,19 @@ package config -import "github.com/spf13/viper" +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/spf13/viper" +) + +var ( + // DefaultDirName is the default name used for config and workspace directories. + DefaultDirName string +) // Configuration lets us inject configuration options into commands. // Note that we are slowly working towards getting rid of the @@ -28,3 +41,67 @@ func NewConfiguration() Configuration { DefaultWorkspaceDir: defaultWorkspace(home), } } + +// SetDefaultDirName configures the default directory name based on the name of the binary. +func SetDefaultDirName(binaryName string) { + DefaultDirName = strings.Replace(filepath.Base(binaryName), ".exe", "", 1) +} + +// Dir is the configured config home directory. +// All the cli-related config files live in this directory. +func Dir() string { + var dir string + if runtime.GOOS == "windows" { + dir = os.Getenv("APPDATA") + if dir != "" { + return filepath.Join(dir, DefaultDirName) + } + } else { + dir := os.Getenv("EXERCISM_CONFIG_HOME") + if dir != "" { + return dir + } + dir = os.Getenv("XDG_CONFIG_HOME") + if dir == "" { + dir = filepath.Join(os.Getenv("HOME"), ".config") + } + if dir != "" { + return filepath.Join(dir, DefaultDirName) + } + } + // If all else fails, use the current directory. + dir, _ = os.Getwd() + return dir +} + +func userHome() string { + var dir string + if runtime.GOOS == "windows" { + dir = os.Getenv("USERPROFILE") + if dir != "" { + return dir + } + dir = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + if dir != "" { + return dir + } + } else { + dir = os.Getenv("HOME") + if dir != "" { + return dir + } + } + // If all else fails, use the current directory. + dir, _ = os.Getwd() + return dir +} + +func defaultWorkspace(home string) string { + dir := filepath.Join(home, DefaultDirName) + _, err := os.Stat(dir) + // Sorry about the double negative. + if !os.IsNotExist(err) { + dir = fmt.Sprintf("%s-1", dir) + } + return dir +} diff --git a/config/dir.go b/config/dir.go deleted file mode 100644 index 1cccc8745..000000000 --- a/config/dir.go +++ /dev/null @@ -1,45 +0,0 @@ -package config - -import ( - "os" - "path/filepath" - "runtime" - "strings" -) - -var ( - // DefaultDirName is the default name used for config and workspace directories. - DefaultDirName string -) - -// SetDefaultDirName configures the default directory name based on the name of the binary. -func SetDefaultDirName(binaryName string) { - DefaultDirName = strings.Replace(filepath.Base(binaryName), ".exe", "", 1) -} - -// Dir is the configured config home directory. -// All the cli-related config files live in this directory. -func Dir() string { - var dir string - if runtime.GOOS == "windows" { - dir = os.Getenv("APPDATA") - if dir != "" { - return filepath.Join(dir, DefaultDirName) - } - } else { - dir := os.Getenv("EXERCISM_CONFIG_HOME") - if dir != "" { - return dir - } - dir = os.Getenv("XDG_CONFIG_HOME") - if dir == "" { - dir = filepath.Join(os.Getenv("HOME"), ".config") - } - if dir != "" { - return filepath.Join(dir, DefaultDirName) - } - } - // If all else fails, use the current directory. - dir, _ = os.Getwd() - return dir -} diff --git a/config/user_config.go b/config/user_config.go index 9e9013fe0..99a6eb991 100644 --- a/config/user_config.go +++ b/config/user_config.go @@ -1,13 +1,6 @@ package config -import ( - "fmt" - "os" - "path/filepath" - "runtime" - - "github.com/spf13/viper" -) +import "github.com/spf13/viper" var ( defaultBaseURL = "https://v2.exercism.io/api/v1" @@ -64,35 +57,3 @@ func (cfg *UserConfig) Load(v *viper.Viper) error { cfg.readIn(v) return v.Unmarshal(&cfg) } - -func userHome() string { - var dir string - if runtime.GOOS == "windows" { - dir = os.Getenv("USERPROFILE") - if dir != "" { - return dir - } - dir = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") - if dir != "" { - return dir - } - } else { - dir = os.Getenv("HOME") - if dir != "" { - return dir - } - } - // If all else fails, use the current directory. - dir, _ = os.Getwd() - return dir -} - -func defaultWorkspace(home string) string { - dir := filepath.Join(home, DefaultDirName) - _, err := os.Stat(dir) - // Sorry about the double negative. - if !os.IsNotExist(err) { - dir = fmt.Sprintf("%s-1", dir) - } - return dir -} From 190116f9bcb0873f6b61eb5fb7200f736e33ee9d Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 9 Jul 2018 09:30:59 +0100 Subject: [PATCH 080/544] Add OS to configuration type --- config/configuration.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/configuration.go b/config/configuration.go index 776a77ee4..bb06bd74e 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -21,6 +21,7 @@ var ( // Once we do, we can rename this type to Config, and get rid of the // User and CLI fields. type Configuration struct { + OS string Home string Dir string DefaultBaseURL string @@ -35,6 +36,7 @@ func NewConfiguration() Configuration { home := userHome() return Configuration{ + OS: runtime.GOOS, Dir: Dir(), Home: home, DefaultBaseURL: defaultBaseURL, From 45b256f8c898692ea06323391a9efbfe71ca76b5 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 9 Jul 2018 09:36:48 +0100 Subject: [PATCH 081/544] Add default dir name to the configuration type --- config/configuration.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/configuration.go b/config/configuration.go index bb06bd74e..2f0758cff 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -26,6 +26,7 @@ type Configuration struct { Dir string DefaultBaseURL string DefaultWorkspaceDir string + DefaultDirName string UserViperConfig *viper.Viper UserConfig *UserConfig CLI *CLIConfig @@ -41,6 +42,7 @@ func NewConfiguration() Configuration { Home: home, DefaultBaseURL: defaultBaseURL, DefaultWorkspaceDir: defaultWorkspace(home), + DefaultDirName: DefaultDirName, } } From ee66a21556384c3f8005b9865f345def8b8c803d Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 9 Jul 2018 11:06:28 +0100 Subject: [PATCH 082/544] Determine sensible os-specific defaults for workspace name --- config/configuration.go | 8 ++++++++ config/configuration_notwin_test.go | 30 ++++++++++++++++++++++++++++ config/configuration_windows_test.go | 14 +++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 config/configuration_notwin_test.go create mode 100644 config/configuration_windows_test.go diff --git a/config/configuration.go b/config/configuration.go index 2f0758cff..8ba878492 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -100,6 +100,14 @@ func userHome() string { return dir } +func DefaultWorkspaceDir(cfg Configuration) string { + dir := cfg.DefaultDirName + if cfg.OS != "linux" { + dir = strings.Title(dir) + } + return filepath.Join(cfg.Home, dir) +} + func defaultWorkspace(home string) string { dir := filepath.Join(home, DefaultDirName) _, err := os.Stat(dir) diff --git a/config/configuration_notwin_test.go b/config/configuration_notwin_test.go new file mode 100644 index 000000000..ff64b495a --- /dev/null +++ b/config/configuration_notwin_test.go @@ -0,0 +1,30 @@ +// +build !windows + +package config + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultWorkspaceDir(t *testing.T) { + testCases := []struct { + cfg Configuration + expected string + }{ + { + cfg: Configuration{OS: "darwin", Home: "/User/charlie", DefaultDirName: "apple"}, + expected: "/User/charlie/Apple", + }, + { + cfg: Configuration{OS: "linux", Home: "/home/bob", DefaultDirName: "banana"}, + expected: "/home/bob/banana", + }, + } + + for _, tc := range testCases { + assert.Equal(t, tc.expected, DefaultWorkspaceDir(tc.cfg), fmt.Sprintf("Operating System: %s", tc.cfg.OS)) + } +} diff --git a/config/configuration_windows_test.go b/config/configuration_windows_test.go new file mode 100644 index 000000000..b07ccab3f --- /dev/null +++ b/config/configuration_windows_test.go @@ -0,0 +1,14 @@ +// +build windows + +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultWindowsWorkspaceDir(t *testing.T) { + cfg := Configuration{OS: "windows", Home: "C:\\Something", DefaultDirName: "basename"} + assert.Equal(t, "C:\\Something\\Basename", DefaultWorkspaceDir(cfg)) +} From be24896baa4dd3e4cd2335061f1ab324edefea9b Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 9 Jul 2018 11:14:29 +0100 Subject: [PATCH 083/544] Use sensible defaults for Workspace dir We are phasing out the UserConfig type, but until we do, we are hooking into the new sensible os-specific path default for workspace. This moves the "do not clobber" logic into the user config type's SetDefaults method. As we overhaul the verification logic in the 'configure' command, we will likely move that out of config and into cmd. --- config/configuration.go | 39 +++++++++++++-------------------------- config/user_config.go | 20 +++++++++++++++++--- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/config/configuration.go b/config/configuration.go index 8ba878492..1761d4665 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -1,7 +1,6 @@ package config import ( - "fmt" "os" "path/filepath" "runtime" @@ -21,15 +20,14 @@ var ( // Once we do, we can rename this type to Config, and get rid of the // User and CLI fields. type Configuration struct { - OS string - Home string - Dir string - DefaultBaseURL string - DefaultWorkspaceDir string - DefaultDirName string - UserViperConfig *viper.Viper - UserConfig *UserConfig - CLI *CLIConfig + OS string + Home string + Dir string + DefaultBaseURL string + DefaultDirName string + UserViperConfig *viper.Viper + UserConfig *UserConfig + CLI *CLIConfig } // NewConfiguration provides a configuration with default values. @@ -37,12 +35,11 @@ func NewConfiguration() Configuration { home := userHome() return Configuration{ - OS: runtime.GOOS, - Dir: Dir(), - Home: home, - DefaultBaseURL: defaultBaseURL, - DefaultWorkspaceDir: defaultWorkspace(home), - DefaultDirName: DefaultDirName, + OS: runtime.GOOS, + Dir: Dir(), + Home: home, + DefaultBaseURL: defaultBaseURL, + DefaultDirName: DefaultDirName, } } @@ -107,13 +104,3 @@ func DefaultWorkspaceDir(cfg Configuration) string { } return filepath.Join(cfg.Home, dir) } - -func defaultWorkspace(home string) string { - dir := filepath.Join(home, DefaultDirName) - _, err := os.Stat(dir) - // Sorry about the double negative. - if !os.IsNotExist(err) { - dir = fmt.Sprintf("%s-1", dir) - } - return dir -} diff --git a/config/user_config.go b/config/user_config.go index 99a6eb991..ef5b9b1a8 100644 --- a/config/user_config.go +++ b/config/user_config.go @@ -1,6 +1,11 @@ package config -import "github.com/spf13/viper" +import ( + "fmt" + "os" + + "github.com/spf13/viper" +) var ( defaultBaseURL = "https://v2.exercism.io/api/v1" @@ -13,6 +18,7 @@ type UserConfig struct { Token string Home string APIBaseURL string + settings Configuration } // NewUserConfig loads a user configuration if it exists. @@ -29,7 +35,8 @@ func NewUserConfig() (*UserConfig, error) { // NewEmptyUserConfig creates a user configuration without loading it. func NewEmptyUserConfig() *UserConfig { return &UserConfig{ - Config: New(Dir(), "user"), + Config: New(Dir(), "user"), + settings: NewConfiguration(), } } @@ -42,7 +49,14 @@ func (cfg *UserConfig) SetDefaults() { cfg.APIBaseURL = defaultBaseURL } if cfg.Workspace == "" { - cfg.Workspace = defaultWorkspace(cfg.Home) + dir := DefaultWorkspaceDir(cfg.settings) + + _, err := os.Stat(dir) + // Sorry about the double negative. + if !os.IsNotExist(err) { + dir = fmt.Sprintf("%s-1", dir) + } + cfg.Workspace = dir } } From 81e9c741611cdc0fca9be8bd02524100dcdf5f4b Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 9 Jul 2018 15:01:32 +0100 Subject: [PATCH 084/544] Tweak path logic for Windows home directory This will use filepath's clean to get the details right. --- config/configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/configuration.go b/config/configuration.go index 1761d4665..1cc00d8b5 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -82,7 +82,7 @@ func userHome() string { if dir != "" { return dir } - dir = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + dir = filepath.Join(os.Getenv("HOMEDRIVE"), os.Getenv("HOMEPATH")) if dir != "" { return dir } From 94d1bcac2de1aae3a7763f9f71caa95f25f57acf Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 9 Jul 2018 15:03:02 +0100 Subject: [PATCH 085/544] Rename file to follow conventions The configuration test is conditionally compiled for not-Windows. --- config/{configuration_notwin_test.go => configuration_test.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename config/{configuration_notwin_test.go => configuration_test.go} (100%) diff --git a/config/configuration_notwin_test.go b/config/configuration_test.go similarity index 100% rename from config/configuration_notwin_test.go rename to config/configuration_test.go From 233d2de2b47542c27e5e2605d375172392bd94f8 Mon Sep 17 00:00:00 2001 From: Aaron Strick Date: Thu, 21 Jun 2018 18:27:38 -0700 Subject: [PATCH 086/544] update test to reflect new UI --- cmd/download_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index c86e9ee5a..ea90b51f0 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -56,7 +56,7 @@ func TestDownload(t *testing.T) { cmdTest := &CommandTest{ Cmd: downloadCmd, InitFn: initDownloadCmd, - Args: []string{"fakeapp", "download", "bogus-exercise"}, + Args: []string{"fakeapp", "download", "--exercise=bogus-exercise"}, } cmdTest.Setup(t) defer cmdTest.Teardown(t) From dd3c510dc83910c4d87d28a2a3170c31b52bc69d Mon Sep 17 00:00:00 2001 From: Aaron Strick Date: Thu, 21 Jun 2018 18:29:56 -0700 Subject: [PATCH 087/544] Read exercise from a flag instead of as an arg --- cmd/download.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index d4b3904d3..83fa4433a 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -29,7 +29,7 @@ latest solution. Download other people's solutions by providing the UUID. `, - Args: cobra.ExactArgs(1), + Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { token, err := cmd.Flags().GetString("token") if err != nil { @@ -45,9 +45,12 @@ Download other people's solutions by providing the UUID. if err != nil { return err } - if uuid == "" && len(args) == 0 { - // TODO: usage - return errors.New("need an exercise name or a solution --uuid") + exercise, err := cmd.Flags().GetString("exercise") + if err != nil { + return err + } + if uuid == "" && exercise == "" { + return errors.New("need an --exercise name or a solution --uuid") } usrCfg, err := config.NewUserConfig() if err != nil { @@ -76,10 +79,6 @@ Download other people's solutions by providing the UUID. if err != nil { return err } - var exercise string - if len(args) > 0 { - exercise = args[0] - } if uuid == "" { q := req.URL.Query() @@ -222,6 +221,7 @@ type downloadPayload struct { func initDownloadCmd() { downloadCmd.Flags().StringP("uuid", "u", "", "the solution UUID") downloadCmd.Flags().StringP("track", "t", "", "the track ID") + downloadCmd.Flags().StringP("exercise", "e", "", "the exercise ID") downloadCmd.Flags().StringP("token", "k", "", "authentication token used to connect to the site") } From 0df47521c1e10a909180436efc51f48ea7bd18c8 Mon Sep 17 00:00:00 2001 From: Aaron Strick Date: Sat, 30 Jun 2018 14:48:07 -0400 Subject: [PATCH 088/544] Match naming convention for exercise identifiers (use 'slug') --- cmd/download.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/download.go b/cmd/download.go index 83fa4433a..132c31a8a 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -221,7 +221,7 @@ type downloadPayload struct { func initDownloadCmd() { downloadCmd.Flags().StringP("uuid", "u", "", "the solution UUID") downloadCmd.Flags().StringP("track", "t", "", "the track ID") - downloadCmd.Flags().StringP("exercise", "e", "", "the exercise ID") + downloadCmd.Flags().StringP("exercise", "e", "", "the exercise slug") downloadCmd.Flags().StringP("token", "k", "", "authentication token used to connect to the site") } From 88da10a0c0bef9092dffafca40a0e9d4f6ae8c74 Mon Sep 17 00:00:00 2001 From: Aaron Strick Date: Sat, 30 Jun 2018 14:55:11 -0400 Subject: [PATCH 089/544] Provide custom error when an argument is given instead of the default cobra one --- cmd/download.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/download.go b/cmd/download.go index 132c31a8a..735f438bd 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -29,7 +29,6 @@ latest solution. Download other people's solutions by providing the UUID. `, - Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { token, err := cmd.Flags().GetString("token") if err != nil { From 64cf88c1365b5c4f7605c8ab73b9827ccac5f856 Mon Sep 17 00:00:00 2001 From: Aaron Strick Date: Sat, 30 Jun 2018 15:43:27 -0400 Subject: [PATCH 090/544] Assert friendly error appears when user provides no flags --- cmd/download_test.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/cmd/download_test.go b/cmd/download_test.go index ea90b51f0..977bf3722 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -138,3 +138,32 @@ func makeMockServer() *httptest.Server { return server } + +func TestDownloadArgs(t *testing.T) { + tests := []struct { + args []string + expectedError string + }{ + { + args: []string{"bogus"}, // providing just an exercise slug without the flag + expectedError: "need an --exercise name or a solution --uuid", + }, + { + args: []string{""}, // providing no args + expectedError: "need an --exercise name or a solution --uuid", + }, + } + + for _, test := range tests { + cmdTest := &CommandTest{ + Cmd: downloadCmd, + InitFn: initDownloadCmd, + Args: append([]string{"fakeapp", "download"}, test.args...), + } + cmdTest.Setup(t) + defer cmdTest.Teardown(t) + err := cmdTest.App.Execute() + + assert.EqualError(t, err, test.expectedError) + } +} From cde88ea539ec26677b1b5d7e62ba8f4df3e16df8 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 10 Jul 2018 09:53:52 +0100 Subject: [PATCH 091/544] Silence test errors for download command --- cmd/download_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/download_test.go b/cmd/download_test.go index 977bf3722..c3865b937 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -161,6 +161,7 @@ func TestDownloadArgs(t *testing.T) { Args: append([]string{"fakeapp", "download"}, test.args...), } cmdTest.Setup(t) + cmdTest.App.SetOutput(ioutil.Discard) defer cmdTest.Teardown(t) err := cmdTest.App.Execute() From 1f112a4f0649ef8d499e05eb4cf737d1eb6583d2 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 24 Jun 2018 12:06:11 -0500 Subject: [PATCH 092/544] Update viper dependency They implemented a WriteConfig function which I'd like to use. It is not quite working right yet, but there's a WriteConfigAs that makes a reasonable workaround until WriteConfig is fixed. --- Gopkg.lock | 55 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index a25ce673b..91ab68269 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -22,13 +22,28 @@ [[projects]] branch = "master" name = "github.com/hashicorp/hcl" - packages = [".","hcl/ast","hcl/parser","hcl/scanner","hcl/strconv","hcl/token","json/parser","json/scanner","json/token"] + packages = [ + ".", + "hcl/ast", + "hcl/parser", + "hcl/printer", + "hcl/scanner", + "hcl/strconv", + "hcl/token", + "json/parser", + "json/scanner", + "json/token" + ] revision = "392dba7d905ed5d04a5794ba89f558b27e2ba1ca" [[projects]] branch = "master" name = "github.com/inconshreveable/go-update" - packages = [".","internal/binarydist","internal/osext"] + packages = [ + ".", + "internal/binarydist", + "internal/osext" + ] revision = "8152e7eb6ccf8679a64582a66b78519688d156ad" [[projects]] @@ -70,7 +85,10 @@ [[projects]] branch = "master" name = "github.com/spf13/afero" - packages = [".","mem"] + packages = [ + ".", + "mem" + ] revision = "9be650865eab0c12963d8753212f4f9c66cdcf12" [[projects]] @@ -101,7 +119,7 @@ branch = "master" name = "github.com/spf13/viper" packages = ["."] - revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7" + revision = "15738813a09db5c8e5b60a19d67d3f9bd38da3a4" [[projects]] name = "github.com/stretchr/testify" @@ -112,7 +130,11 @@ [[projects]] branch = "master" name = "golang.org/x/net" - packages = ["html","html/atom","html/charset"] + packages = [ + "html", + "html/atom", + "html/charset" + ] revision = "f5079bd7f6f74e23c4d65efa0f4ce14cbd6a3c0f" [[projects]] @@ -124,7 +146,28 @@ [[projects]] branch = "master" name = "golang.org/x/text" - packages = ["encoding","encoding/charmap","encoding/htmlindex","encoding/internal","encoding/internal/identifier","encoding/japanese","encoding/korean","encoding/simplifiedchinese","encoding/traditionalchinese","encoding/unicode","internal/gen","internal/tag","internal/triegen","internal/ucd","internal/utf8internal","language","runes","transform","unicode/cldr","unicode/norm"] + packages = [ + "encoding", + "encoding/charmap", + "encoding/htmlindex", + "encoding/internal", + "encoding/internal/identifier", + "encoding/japanese", + "encoding/korean", + "encoding/simplifiedchinese", + "encoding/traditionalchinese", + "encoding/unicode", + "internal/gen", + "internal/tag", + "internal/triegen", + "internal/ucd", + "internal/utf8internal", + "language", + "runes", + "transform", + "unicode/cldr", + "unicode/norm" + ] revision = "3bd178b88a8180be2df394a1fbb81313916f0e7b" [[projects]] From 738a62d4c913a90451ebc8c59b057b110fc384cd Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 04:35:26 +0100 Subject: [PATCH 093/544] Add indirection to configure command This splits up the configure command so that we can inject some configuration values. Right now there's a lot of pain in test setup where we need to put together entire cobra commands and temporary directories and environment variables. This is slowly moving us in the direction of being able to inject defaults and therefore test the command without manipulating the environment so heavily. --- cmd/configure.go | 90 ++++++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index c4481a0d9..094d1dc28 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -7,6 +7,7 @@ import ( "github.com/exercism/cli/api" "github.com/exercism/cli/config" "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/spf13/viper" ) @@ -33,54 +34,59 @@ You can also override certain default settings to suit your preferences. if err != nil { return err } - cfg.Workspace = config.Resolve(cfg.Workspace, cfg.Home) - cfg.SetDefaults() + configuration := config.NewConfiguration() + configuration.UserConfig = cfg + return runConfigure(configuration, cmd.Flags()) + }, +} - show, err := cmd.Flags().GetBool("show") - if err != nil { - return err - } - if show { - defer printCurrentConfig() - } - client, err := api.NewClient(cfg.Token, cfg.APIBaseURL) - if err != nil { - return err - } +func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) error { + cfg := configuration.UserConfig + cfg.Workspace = config.Resolve(cfg.Workspace, cfg.Home) + cfg.SetDefaults() + + show, err := flags.GetBool("show") + if err != nil { + return err + } + if show { + defer printCurrentConfig() + } + client, err := api.NewClient(cfg.Token, cfg.APIBaseURL) + if err != nil { + return err + } - switch { - case cfg.Token == "": - fmt.Fprintln(Err, "There is no token configured, please set it using --token.") - case cmd.Flags().Lookup("token").Changed: - // User set new token - skipAuth, _ := cmd.Flags().GetBool("skip-auth") - if !skipAuth { - ok, err := client.TokenIsValid() - if err != nil { - return err - } - if !ok { - fmt.Fprintln(Err, "The token is invalid.") - } + switch { + case cfg.Token == "": + fmt.Fprintln(Err, "There is no token configured, please set it using --token.") + case flags.Lookup("token").Changed: + // User set new token + skipAuth, _ := flags.GetBool("skip-auth") + if !skipAuth { + ok, err := client.TokenIsValid() + if err != nil { + return err } - default: - // Validate existing token - skipAuth, _ := cmd.Flags().GetBool("skip-auth") - if !skipAuth { - ok, err := client.TokenIsValid() - if err != nil { - return err - } - if !ok { - fmt.Fprintln(Err, "The token is invalid.") - } - - defer printCurrentConfig() + if !ok { + fmt.Fprintln(Err, "The token is invalid.") } } + default: + // Validate existing token + skipAuth, _ := flags.GetBool("skip-auth") + if !skipAuth { + ok, err := client.TokenIsValid() + if err != nil { + return err + } + if !ok { + fmt.Fprintln(Err, "The token is invalid.") + } + } + } - return cfg.Write() - }, + return cfg.Write() } func printCurrentConfig() { From e9299ffc8567fe4271e92b7bf5048513bb6ddc87 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 04:42:19 +0100 Subject: [PATCH 094/544] Inline configure test This isolates all the existing complicated configuration testing so that we can start fresh without breaking the existing behavior. --- cmd/configure_test.go | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/cmd/configure_test.go b/cmd/configure_test.go index b5abee695..1f6d29e36 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -11,13 +11,6 @@ import ( "github.com/stretchr/testify/assert" ) -type testCase struct { - desc string - args []string - existingUsrCfg *config.UserConfig - expectedUsrCfg *config.UserConfig -} - func TestConfigure(t *testing.T) { oldOut := Out oldErr := Err @@ -28,6 +21,13 @@ func TestConfigure(t *testing.T) { Err = oldErr }() + type testCase struct { + desc string + args []string + existingUsrCfg *config.UserConfig + expectedUsrCfg *config.UserConfig + } + testCases := []testCase{ testCase{ desc: "It writes the flags when there is no config file.", @@ -69,13 +69,6 @@ func TestConfigure(t *testing.T) { } for _, tc := range testCases { - t.Run(tc.desc, makeTest(tc)) - } -} - -func makeTest(tc testCase) func(*testing.T) { - - return func(t *testing.T) { cmdTest := &CommandTest{ Cmd: configureCmd, InitFn: initConfigureCmd, From 3642edd0473b2eb5600d0c5171cd4432a161fe57 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 05:19:13 +0100 Subject: [PATCH 095/544] Get rid of user config type in configure command Use the viper value directly. At the moment this is a little bit verbose, but we will be able to fix that later. --- cmd/configure.go | 69 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 094d1dc28..6285715a7 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -2,6 +2,8 @@ package cmd import ( "fmt" + "os" + "path/filepath" "text/tabwriter" "github.com/exercism/cli/api" @@ -29,36 +31,45 @@ places. You can also override certain default settings to suit your preferences. `, RunE: func(cmd *cobra.Command, args []string) error { - cfg := config.NewEmptyUserConfig() - err := cfg.Load(viperConfig) - if err != nil { - return err - } configuration := config.NewConfiguration() - configuration.UserConfig = cfg + + viperConfig.AddConfigPath(configuration.Dir) + viperConfig.SetConfigName("user") + viperConfig.SetConfigType("json") + // Ignore error. If the file doesn't exist, that is fine. + _ = viperConfig.ReadInConfig() + configuration.UserViperConfig = viperConfig + return runConfigure(configuration, cmd.Flags()) }, } func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) error { - cfg := configuration.UserConfig - cfg.Workspace = config.Resolve(cfg.Workspace, cfg.Home) - cfg.SetDefaults() + cfg := configuration.UserViperConfig + + cfg.Set("workspace", config.Resolve(viperConfig.GetString("workspace"), configuration.Home)) + + if cfg.GetString("apibaseurl") == "" { + cfg.Set("apibaseurl", configuration.DefaultBaseURL) + } + if cfg.GetString("workspace") == "" { + cfg.Set("workspace", configuration.DefaultWorkspaceDir) + } show, err := flags.GetBool("show") if err != nil { return err } if show { - defer printCurrentConfig() + defer printCurrentConfig(configuration) } - client, err := api.NewClient(cfg.Token, cfg.APIBaseURL) + client, err := api.NewClient(cfg.GetString("token"), cfg.GetString("apibaseurl")) if err != nil { return err } switch { - case cfg.Token == "": + case cfg.GetString("token") == "": fmt.Fprintln(Err, "There is no token configured, please set it using --token.") case flags.Lookup("token").Changed: // User set new token @@ -83,25 +94,39 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro if !ok { fmt.Fprintln(Err, "The token is invalid.") } + defer printCurrentConfig(configuration) } } - return cfg.Write() -} + viperConfig.SetConfigType("json") + viperConfig.AddConfigPath(configuration.Dir) + viperConfig.SetConfigName("user") -func printCurrentConfig() { - cfg, err := config.NewUserConfig() - if err != nil { - return + if _, err := os.Stat(configuration.Dir); os.IsNotExist(err) { + if err := os.MkdirAll(configuration.Dir, os.FileMode(0755)); err != nil { + return err + } } + // WriteConfig is broken. + // Someone proposed a fix in https://github.com/spf13/viper/pull/503, + // but the fix doesn't work yet. + // When it's fixed and merged we can get rid of `path` + // and use viperConfig.WriteConfig() directly. + path := filepath.Join(configuration.Dir, "user.json") + return viperConfig.WriteConfigAs(path) +} + +func printCurrentConfig(configuration config.Configuration) { w := tabwriter.NewWriter(Out, 0, 0, 2, ' ', 0) defer w.Flush() + v := configuration.UserViperConfig + fmt.Fprintln(w, "") - fmt.Fprintln(w, fmt.Sprintf("Config dir:\t%s", config.Dir())) - fmt.Fprintln(w, fmt.Sprintf("-t, --token\t%s", cfg.Token)) - fmt.Fprintln(w, fmt.Sprintf("-w, --workspace\t%s", cfg.Workspace)) - fmt.Fprintln(w, fmt.Sprintf("-a, --api\t%s", cfg.APIBaseURL)) + fmt.Fprintln(w, fmt.Sprintf("Config dir:\t%s", configuration.Dir)) + fmt.Fprintln(w, fmt.Sprintf("-t, --token\t%s", v.GetString("token"))) + fmt.Fprintln(w, fmt.Sprintf("-w, --workspace\t%s", v.GetString("workspace"))) + fmt.Fprintln(w, fmt.Sprintf("-a, --api\t%s", v.GetString("apibaseurl"))) fmt.Fprintln(w, "") } From 929291c4017059e6792692f1bf6870648616a6e4 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 05:42:02 +0100 Subject: [PATCH 096/544] Push save into persister in configure command This is the next step in allowing us to decouple the configure command tests from the filesystem. It creates two implementations of persister, the default one persists to the filesystem, and the other one does nothing (a.k.a. persists in memory, where it already is). --- cmd/configure.go | 22 ++----------------- config/configuration.go | 7 ++++++ config/persister.go | 48 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 20 deletions(-) create mode 100644 config/persister.go diff --git a/cmd/configure.go b/cmd/configure.go index 6285715a7..02a9a5ad3 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -2,8 +2,6 @@ package cmd import ( "fmt" - "os" - "path/filepath" "text/tabwriter" "github.com/exercism/cli/api" @@ -53,7 +51,7 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro cfg.Set("apibaseurl", configuration.DefaultBaseURL) } if cfg.GetString("workspace") == "" { - cfg.Set("workspace", configuration.DefaultWorkspaceDir) + cfg.Set("workspace", config.DefaultWorkspaceDir(configuration)) } show, err := flags.GetBool("show") @@ -97,23 +95,7 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro defer printCurrentConfig(configuration) } } - - viperConfig.SetConfigType("json") - viperConfig.AddConfigPath(configuration.Dir) - viperConfig.SetConfigName("user") - - if _, err := os.Stat(configuration.Dir); os.IsNotExist(err) { - if err := os.MkdirAll(configuration.Dir, os.FileMode(0755)); err != nil { - return err - } - } - // WriteConfig is broken. - // Someone proposed a fix in https://github.com/spf13/viper/pull/503, - // but the fix doesn't work yet. - // When it's fixed and merged we can get rid of `path` - // and use viperConfig.WriteConfig() directly. - path := filepath.Join(configuration.Dir, "user.json") - return viperConfig.WriteConfigAs(path) + return configuration.Save("user") } func printCurrentConfig(configuration config.Configuration) { diff --git a/config/configuration.go b/config/configuration.go index 1cc00d8b5..54adf6e05 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -28,11 +28,13 @@ type Configuration struct { UserViperConfig *viper.Viper UserConfig *UserConfig CLI *CLIConfig + Persister Persister } // NewConfiguration provides a configuration with default values. func NewConfiguration() Configuration { home := userHome() + dir := Dir() return Configuration{ OS: runtime.GOOS, @@ -40,6 +42,7 @@ func NewConfiguration() Configuration { Home: home, DefaultBaseURL: defaultBaseURL, DefaultDirName: DefaultDirName, + Persister: FilePersister{Dir: dir}, } } @@ -104,3 +107,7 @@ func DefaultWorkspaceDir(cfg Configuration) string { } return filepath.Join(cfg.Home, dir) } + +func (c Configuration) Save(basename string) error { + return c.Persister.Save(c.UserViperConfig, basename) +} diff --git a/config/persister.go b/config/persister.go new file mode 100644 index 000000000..1ad046f0d --- /dev/null +++ b/config/persister.go @@ -0,0 +1,48 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/viper" +) + +// Persister saves viper configs. +type Persister interface { + Save(*viper.Viper, string) error +} + +// FilePersister saves viper configs to the file system. +type FilePersister struct { + Dir string +} + +// Save writes the viper config to the target location on the filesystem. +func (p FilePersister) Save(v *viper.Viper, basename string) error { + v.SetConfigType("json") + v.AddConfigPath(p.Dir) + v.SetConfigName(basename) + + if _, err := os.Stat(p.Dir); os.IsNotExist(err) { + if err := os.MkdirAll(p.Dir, os.FileMode(0755)); err != nil { + return err + } + } + + // WriteConfig is broken. + // Someone proposed a fix in https://github.com/spf13/viper/pull/503, + // but the fix doesn't work yet. + // When it's fixed and merged we can get rid of `path` + // and use viperConfig.WriteConfig() directly. + path := filepath.Join(p.Dir, fmt.Sprintf("%s.json", basename)) + return v.WriteConfigAs(path) +} + +// InMemoryPersister is a noop persister for use in unit tests. +type InMemoryPersister struct{} + +// Save does nothing. +func (p InMemoryPersister) Save(*viper.Viper, string) error { + return nil +} From 9c1bdeeafe1bb42bdb11c0b311f3fcc231b30346 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 06:12:16 +0100 Subject: [PATCH 097/544] Decouple cobra command from flag setup in configure command We need to be able to set up flags for testing more easily. Right now the flags come directly from the cobra command. This lets us inject a fresh flag set into the runConfigure function without first creating a cobra command. --- cmd/configure.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 02a9a5ad3..c3da2af73 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -113,16 +113,20 @@ func printCurrentConfig(configuration config.Configuration) { } func initConfigureCmd() { - configureCmd.Flags().StringP("token", "t", "", "authentication token used to connect to the site") - configureCmd.Flags().StringP("workspace", "w", "", "directory for exercism exercises") - configureCmd.Flags().StringP("api", "a", "", "API base url") - configureCmd.Flags().BoolP("show", "s", false, "show the current configuration") - configureCmd.Flags().BoolP("skip-auth", "", false, "skip online token authorization check") - viperConfig = viper.New() - viperConfig.BindPFlag("token", configureCmd.Flags().Lookup("token")) - viperConfig.BindPFlag("workspace", configureCmd.Flags().Lookup("workspace")) - viperConfig.BindPFlag("apibaseurl", configureCmd.Flags().Lookup("api")) + setupConfigureFlags(configureCmd.Flags(), viperConfig) +} + +func setupConfigureFlags(flags *pflag.FlagSet, v *viper.Viper) { + flags.StringP("token", "t", "", "authentication token used to connect to the site") + flags.StringP("workspace", "w", "", "directory for exercism exercises") + flags.StringP("api", "a", "", "API base url") + flags.BoolP("show", "s", false, "show the current configuration") + flags.BoolP("skip-auth", "", false, "skip online token authorization check") + + v.BindPFlag("token", configureCmd.Flags().Lookup("token")) + v.BindPFlag("workspace", configureCmd.Flags().Lookup("workspace")) + v.BindPFlag("apibaseurl", configureCmd.Flags().Lookup("api")) } func init() { From 5d94042a5aafb6fa29f4803fd75e43933f43f34c Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 11:08:58 +0100 Subject: [PATCH 098/544] Silence configure command --- cmd/cmd_test.go | 1 + cmd/configure.go | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 1a4d7ebcd..4d6fb4c6f 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -76,6 +76,7 @@ func (test *CommandTest) Setup(t *testing.T) { test.App = &cobra.Command{} test.App.AddCommand(test.Cmd) + test.App.SetOutput(Err) } // Teardown puts the environment back the way it was before the test. diff --git a/cmd/configure.go b/cmd/configure.go index c3da2af73..3ef234575 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "fmt" "text/tabwriter" @@ -68,7 +69,7 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro switch { case cfg.GetString("token") == "": - fmt.Fprintln(Err, "There is no token configured, please set it using --token.") + return errors.New("There is no token configured, please set it using --token.") case flags.Lookup("token").Changed: // User set new token skipAuth, _ := flags.GetBool("skip-auth") @@ -78,7 +79,7 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro return err } if !ok { - fmt.Fprintln(Err, "The token is invalid.") + return errors.New("The token is invalid.") } } default: @@ -90,7 +91,7 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro return err } if !ok { - fmt.Fprintln(Err, "The token is invalid.") + return errors.New("The token is invalid.") } defer printCurrentConfig(configuration) } From 2e3a9da266d1a78ea8779adba0a2dcac3eb82adf Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 11:37:57 +0100 Subject: [PATCH 099/544] Rework token logic in configure command. - Improve error messages so they explain how to fix the problem. - Do not print configuration if there's an error. This adds tests to ensure that we are handling all the combinations of configured tokens and tokens passed via a flag. It also adds a test to ensure that we are verifying the token correctly. --- cmd/configure.go | 73 +++++++++++++++--------------- cmd/configure_test.go | 100 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 35 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 3ef234575..23ff8ef42 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -46,8 +46,6 @@ You can also override certain default settings to suit your preferences. func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) error { cfg := configuration.UserViperConfig - cfg.Set("workspace", config.Resolve(viperConfig.GetString("workspace"), configuration.Home)) - if cfg.GetString("apibaseurl") == "" { cfg.Set("apibaseurl", configuration.DefaultBaseURL) } @@ -55,47 +53,53 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro cfg.Set("workspace", config.DefaultWorkspaceDir(configuration)) } - show, err := flags.GetBool("show") + token, err := flags.GetString("token") if err != nil { return err } - if show { - defer printCurrentConfig(configuration) + if token == "" { + token = cfg.GetString("token") + } + + tokenURL := config.InferSiteURL(cfg.GetString("apibaseurl")) + "/my/settings" + if token == "" { + return fmt.Errorf("There is no token configured. Find your token on %s, and call this command again with --token=.", tokenURL) } - client, err := api.NewClient(cfg.GetString("token"), cfg.GetString("apibaseurl")) + + skipAuth, err := flags.GetBool("skip-auth") if err != nil { return err } - switch { - case cfg.GetString("token") == "": - return errors.New("There is no token configured, please set it using --token.") - case flags.Lookup("token").Changed: - // User set new token - skipAuth, _ := flags.GetBool("skip-auth") - if !skipAuth { - ok, err := client.TokenIsValid() - if err != nil { - return err - } - if !ok { - return errors.New("The token is invalid.") - } + if !skipAuth { + client, err := api.NewClient(cfg.GetString("token"), cfg.GetString("apibaseurl")) + if err != nil { + return err + } + ok, err := client.TokenIsValid() + if err != nil { + return err } - default: - // Validate existing token - skipAuth, _ := flags.GetBool("skip-auth") - if !skipAuth { - ok, err := client.TokenIsValid() - if err != nil { - return err - } - if !ok { - return errors.New("The token is invalid.") - } - defer printCurrentConfig(configuration) + if !ok { + msg := fmt.Sprintf("The token '%s' is invalid. Find your token on %s.", token, tokenURL) + return errors.New(msg) } } + cfg.Set("token", token) + + cfg.Set("workspace", config.Resolve(viperConfig.GetString("workspace"), configuration.Home)) + + if cfg.GetString("workspace") == "" { + cfg.Set("workspace", config.DefaultWorkspaceDir(configuration)) + } + + show, err := flags.GetBool("show") + if err != nil { + return err + } + if show { + defer printCurrentConfig(configuration) + } return configuration.Save("user") } @@ -125,9 +129,8 @@ func setupConfigureFlags(flags *pflag.FlagSet, v *viper.Viper) { flags.BoolP("show", "s", false, "show the current configuration") flags.BoolP("skip-auth", "", false, "skip online token authorization check") - v.BindPFlag("token", configureCmd.Flags().Lookup("token")) - v.BindPFlag("workspace", configureCmd.Flags().Lookup("workspace")) - v.BindPFlag("apibaseurl", configureCmd.Flags().Lookup("api")) + v.BindPFlag("workspace", flags.Lookup("workspace")) + v.BindPFlag("apibaseurl", flags.Lookup("api")) } func init() { diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 1f6d29e36..4c1216a7c 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -3,14 +3,114 @@ package cmd import ( + "bytes" "io/ioutil" + "net/http" + "net/http/httptest" "runtime" "testing" "github.com/exercism/cli/config" + "github.com/spf13/pflag" + "github.com/spf13/viper" "github.com/stretchr/testify/assert" ) +func TestConfigureToken(t *testing.T) { + testCases := []struct { + desc string + configured string + args []string + expected string + message string + err bool + }{ + { + desc: "It doesn't lose a configured value", + configured: "existing-token", + args: []string{"--skip-auth"}, + expected: "existing-token", + }, + { + desc: "It writes a token when passed as a flag", + configured: "", + args: []string{"--skip-auth", "--token", "a-token"}, + expected: "a-token", + }, + { + desc: "It overwrites the token", + configured: "old-token", + args: []string{"--skip-auth", "--token", "replacement-token"}, + expected: "replacement-token", + }, + { + desc: "It complains when token is neither configured nor passed", + configured: "", + args: []string{"--skip-auth"}, + expected: "", + err: true, + message: "no token configured", + }, + { + desc: "It validates the existing token if we're not skipping auth", + configured: "configured-token", + args: []string{}, + expected: "configured-token", + err: true, + message: "token.*invalid", + }, + { + desc: "It validates the replacement token if we're not skipping auth", + configured: "", + args: []string{"--token", "invalid-token"}, + expected: "", + err: true, + message: "token.*invalid", + }, + } + + endpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/validate_token" { + w.WriteHeader(http.StatusUnauthorized) + } + }) + ts := httptest.NewServer(endpoint) + defer ts.Close() + + oldOut := Out + oldErr := Err + Out = ioutil.Discard + defer func() { + Out = oldOut + Err = oldErr + }() + + for _, tc := range testCases { + var buf bytes.Buffer + Err = &buf + + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + v := viper.New() + v.Set("token", tc.configured) + setupConfigureFlags(flags, v) + + err := flags.Parse(tc.args) + assert.NoError(t, err) + + cfg := config.Configuration{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: ts.URL, + } + + err = runConfigure(cfg, flags) + if err != nil || tc.err { + assert.Regexp(t, tc.message, err.Error(), tc.desc) + } + assert.Equal(t, tc.expected, cfg.UserViperConfig.GetString("token"), tc.desc) + } +} + func TestConfigure(t *testing.T) { oldOut := Out oldErr := Err From 3522a9053a749325e84f0a30e36b708350fb75eb Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 12:58:13 +0100 Subject: [PATCH 100/544] Rename skip-auth flag on configure command We need to verify both the API base URL and the token. The name 'skip auth' is only applicable to the token. To not be specific to auth, I went with the idea of 'verification'. I chose 'no-verify' as the flag name, as this will be familiar to people who use git's no-verify flag to skip hooks. --- cmd/configure.go | 6 +++--- cmd/configure_test.go | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 23ff8ef42..912736547 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -66,12 +66,12 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro return fmt.Errorf("There is no token configured. Find your token on %s, and call this command again with --token=.", tokenURL) } - skipAuth, err := flags.GetBool("skip-auth") + skipVerification, err := flags.GetBool("no-verify") if err != nil { return err } - if !skipAuth { + if !skipVerification { client, err := api.NewClient(cfg.GetString("token"), cfg.GetString("apibaseurl")) if err != nil { return err @@ -127,7 +127,7 @@ func setupConfigureFlags(flags *pflag.FlagSet, v *viper.Viper) { flags.StringP("workspace", "w", "", "directory for exercism exercises") flags.StringP("api", "a", "", "API base url") flags.BoolP("show", "s", false, "show the current configuration") - flags.BoolP("skip-auth", "", false, "skip online token authorization check") + flags.BoolP("no-verify", "", false, "skip online token authorization check") v.BindPFlag("workspace", flags.Lookup("workspace")) v.BindPFlag("apibaseurl", flags.Lookup("api")) diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 4c1216a7c..7c1276b5e 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -28,31 +28,31 @@ func TestConfigureToken(t *testing.T) { { desc: "It doesn't lose a configured value", configured: "existing-token", - args: []string{"--skip-auth"}, + args: []string{"--no-verify"}, expected: "existing-token", }, { desc: "It writes a token when passed as a flag", configured: "", - args: []string{"--skip-auth", "--token", "a-token"}, + args: []string{"--no-verify", "--token", "a-token"}, expected: "a-token", }, { desc: "It overwrites the token", configured: "old-token", - args: []string{"--skip-auth", "--token", "replacement-token"}, + args: []string{"--no-verify", "--token", "replacement-token"}, expected: "replacement-token", }, { desc: "It complains when token is neither configured nor passed", configured: "", - args: []string{"--skip-auth"}, + args: []string{"--no-verify"}, expected: "", err: true, message: "no token configured", }, { - desc: "It validates the existing token if we're not skipping auth", + desc: "It validates the existing token if we're not skipping validations", configured: "configured-token", args: []string{}, expected: "configured-token", @@ -60,7 +60,7 @@ func TestConfigureToken(t *testing.T) { message: "token.*invalid", }, { - desc: "It validates the replacement token if we're not skipping auth", + desc: "It validates the replacement token if we're not skipping validations", configured: "", args: []string{"--token", "invalid-token"}, expected: "", @@ -132,7 +132,7 @@ func TestConfigure(t *testing.T) { testCase{ desc: "It writes the flags when there is no config file.", args: []string{ - "fakeapp", "configure", "--skip-auth", + "fakeapp", "configure", "--no-verify", "--token", "abc123", "--workspace", "/workspace", "--api", "http://api.example.com", @@ -143,7 +143,7 @@ func TestConfigure(t *testing.T) { testCase{ desc: "It overwrites the flags in the config file.", args: []string{ - "fakeapp", "configure", "--skip-auth", + "fakeapp", "configure", "--no-verify", "--token", "new-token", "--workspace", "/new-workspace", "--api", "http://new.example.com", @@ -154,7 +154,7 @@ func TestConfigure(t *testing.T) { testCase{ desc: "It overwrites the flags that are passed, without losing the ones that are not.", args: []string{ - "fakeapp", "configure", "--skip-auth", + "fakeapp", "configure", "--no-verify", "--token", "replacement-token", }, existingUsrCfg: &config.UserConfig{Token: "original-token", Workspace: "/unmodified", APIBaseURL: "http://unmodified.example.com"}, @@ -162,7 +162,7 @@ func TestConfigure(t *testing.T) { }, testCase{ desc: "It gets the default API base url.", - args: []string{"fakeapp", "configure", "--skip-auth"}, + args: []string{"fakeapp", "configure", "--no-verify"}, existingUsrCfg: &config.UserConfig{Workspace: "/configured-workspace"}, expectedUsrCfg: &config.UserConfig{Workspace: "/configured-workspace", APIBaseURL: "https://v2.exercism.io/api/v1"}, }, From d70beea7d94a131832c36fc139675b7b983ce669 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 15:35:55 +0100 Subject: [PATCH 101/544] Add method to verify the base API url When configuring the base API URL we need to call the ping endpoint to see if the URL is valid. --- api/client.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/api/client.go b/api/client.go index d7a8385ac..782bae3f6 100644 --- a/api/client.go +++ b/api/client.go @@ -85,3 +85,17 @@ func (c *Client) TokenIsValid() (bool, error) { } return resp.StatusCode == http.StatusOK, nil } + +// IsPingable calls the API /ping to determine whether the API can be reached. +func (c *Client) IsPingable() (bool, error) { + url := fmt.Sprintf("%s/ping", c.APIBaseURL) + req, err := c.NewRequest("GET", url, nil) + if err != nil { + return false, err + } + resp, err := c.Do(req) + if err != nil { + return false, err + } + return resp.StatusCode == http.StatusOK, nil +} From dd15bfdbd3e373dbb6adbe2ec847c339082cbe78 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 15:39:37 +0100 Subject: [PATCH 102/544] Rework api base url logic in configure command This lays out the logic a bit more clearly, and adds verification logic to actually ping the newly configured API, and adds tests for the entire thing. --- cmd/configure.go | 42 ++++++++++++++------- cmd/configure_test.go | 87 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 14 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 912736547..21383cf3a 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -1,7 +1,6 @@ package cmd import ( - "errors" "fmt" "text/tabwriter" @@ -46,13 +45,35 @@ You can also override certain default settings to suit your preferences. func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) error { cfg := configuration.UserViperConfig - if cfg.GetString("apibaseurl") == "" { - cfg.Set("apibaseurl", configuration.DefaultBaseURL) + baseURL, err := flags.GetString("api") + if err != nil { + return err } - if cfg.GetString("workspace") == "" { - cfg.Set("workspace", config.DefaultWorkspaceDir(configuration)) + if baseURL == "" { + baseURL = cfg.GetString("apibaseurl") + } + if baseURL == "" { + baseURL = configuration.DefaultBaseURL + } + + skipVerification, err := flags.GetBool("no-verify") + if err != nil { + return err } + if !skipVerification { + client, err := api.NewClient("", baseURL) + if err != nil { + return err + } + + ok, err := client.IsPingable() + if !ok || err != nil { + return fmt.Errorf("The base API URL '%s' cannot be reached.\n\n%s", baseURL, err) + } + } + cfg.Set("apibaseurl", baseURL) + token, err := flags.GetString("token") if err != nil { return err @@ -66,13 +87,8 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro return fmt.Errorf("There is no token configured. Find your token on %s, and call this command again with --token=.", tokenURL) } - skipVerification, err := flags.GetBool("no-verify") - if err != nil { - return err - } - if !skipVerification { - client, err := api.NewClient(cfg.GetString("token"), cfg.GetString("apibaseurl")) + client, err := api.NewClient(token, baseURL) if err != nil { return err } @@ -81,8 +97,7 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro return err } if !ok { - msg := fmt.Sprintf("The token '%s' is invalid. Find your token on %s.", token, tokenURL) - return errors.New(msg) + return fmt.Errorf("The token '%s' is invalid. Find your token on %s.", token, tokenURL) } } cfg.Set("token", token) @@ -130,7 +145,6 @@ func setupConfigureFlags(flags *pflag.FlagSet, v *viper.Viper) { flags.BoolP("no-verify", "", false, "skip online token authorization check") v.BindPFlag("workspace", flags.Lookup("workspace")) - v.BindPFlag("apibaseurl", flags.Lookup("api")) } func init() { diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 7c1276b5e..d917dd0d1 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -111,6 +111,93 @@ func TestConfigureToken(t *testing.T) { } } +func TestConfigureAPIBaseURL(t *testing.T) { + endpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/ping" { + w.WriteHeader(http.StatusNotFound) + } + }) + ts := httptest.NewServer(endpoint) + defer ts.Close() + + testCases := []struct { + desc string + configured string + args []string + expected string + message string + err bool + }{ + { + desc: "It doesn't lose a configured value", + configured: "http://example.com", + args: []string{"--no-verify"}, + expected: "http://example.com", + }, + { + desc: "It writes a base url when passed as a flag", + configured: "", + args: []string{"--no-verify", "--api", "http://api.example.com"}, + expected: "http://api.example.com", + }, + { + desc: "It overwrites the base url", + configured: "http://old.example.com", + args: []string{"--no-verify", "--api", "http://replacement.example.com"}, + expected: "http://replacement.example.com", + }, + { + desc: "It validates the existing base url if we're not skipping validations", + configured: ts.URL, + args: []string{}, + expected: ts.URL, + err: true, + message: "API.*cannot be reached", + }, + { + desc: "It validates the replacement base URL if we're not skipping validations", + configured: "", + args: []string{"--api", ts.URL}, + expected: "", + err: true, + message: "API.*cannot be reached", + }, + } + + oldOut := Out + oldErr := Err + Out = ioutil.Discard + defer func() { + Out = oldOut + Err = oldErr + }() + + for _, tc := range testCases { + var buf bytes.Buffer + Err = &buf + + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + v := viper.New() + v.Set("apibaseurl", tc.configured) + setupConfigureFlags(flags, v) + + err := flags.Parse(tc.args) + assert.NoError(t, err) + + cfg := config.Configuration{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: ts.URL, + } + + err = runConfigure(cfg, flags) + if err != nil || tc.err { + assert.Regexp(t, tc.message, err.Error(), tc.desc) + } + assert.Equal(t, tc.expected, cfg.UserViperConfig.GetString("apibaseurl"), tc.desc) + } +} + func TestConfigure(t *testing.T) { oldOut := Out oldErr := Err From 9488c875cd92bf3abbeffce1f6a86a8d3f9d7629 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 16:16:41 +0100 Subject: [PATCH 103/544] Rework logic for show flag in configure command --- cmd/configure.go | 19 ++++++++++-------- cmd/configure_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 21383cf3a..8eeee0158 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -45,6 +45,16 @@ You can also override certain default settings to suit your preferences. func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) error { cfg := configuration.UserViperConfig + show, err := flags.GetBool("show") + if err != nil { + return err + } + + if show { + printCurrentConfig(configuration) + return nil + } + baseURL, err := flags.GetString("api") if err != nil { return err @@ -108,18 +118,11 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro cfg.Set("workspace", config.DefaultWorkspaceDir(configuration)) } - show, err := flags.GetBool("show") - if err != nil { - return err - } - if show { - defer printCurrentConfig(configuration) - } return configuration.Save("user") } func printCurrentConfig(configuration config.Configuration) { - w := tabwriter.NewWriter(Out, 0, 0, 2, ' ', 0) + w := tabwriter.NewWriter(Err, 0, 0, 2, ' ', 0) defer w.Flush() v := configuration.UserViperConfig diff --git a/cmd/configure_test.go b/cmd/configure_test.go index d917dd0d1..f84ee7510 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -16,6 +16,51 @@ import ( "github.com/stretchr/testify/assert" ) +func TestConfigureShow(t *testing.T) { + oldErr := Err + defer func() { + Err = oldErr + }() + + var buf bytes.Buffer + Err = &buf + + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + v := viper.New() + v.Set("token", "configured-token") + v.Set("workspace", "configured-workspace") + v.Set("apibaseurl", "http://configured.example.com") + + setupConfigureFlags(flags, v) + + // it will ignore any flags + args := []string{ + "--show", + "--api", "http://override.example.com", + "--token", "token-override", + "--workspace", "workspace-override", + } + err := flags.Parse(args) + assert.NoError(t, err) + + cfg := config.Configuration{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + } + + err = runConfigure(cfg, flags) + assert.NoError(t, err) + + assert.Regexp(t, "configured.example", Err) + assert.NotRegexp(t, "override.example", Err) + + assert.Regexp(t, "configured-token", Err) + assert.NotRegexp(t, "token-overrid", Err) + + assert.Regexp(t, "configured-workspace", Err) + assert.NotRegexp(t, "workspace-override", Err) +} + func TestConfigureToken(t *testing.T) { testCases := []struct { desc string From 939f7de9066bf7dbb11e63d786cde1655ccbe8a2 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 16:42:38 +0100 Subject: [PATCH 104/544] Rework behavior of bare configure command When called without any flags, bail early with an error message if there is no token configured. --- cmd/configure.go | 10 +++++++++- cmd/configure_test.go | 27 ++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 8eeee0158..25a79c2a7 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -49,12 +49,20 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro if err != nil { return err } - if show { printCurrentConfig(configuration) return nil } + if flags.NFlag() == 0 && cfg.GetString("token") == "" { + baseURL := cfg.GetString("apibaseurl") + if baseURL != "" { + tokenURL := config.InferSiteURL(baseURL) + "/my/settings" + return fmt.Errorf("There is no token configured. Find your token on %s, and call this command again with --token=.", tokenURL) + } + return fmt.Errorf("There is no token configured. Find your token in your settings on the website, and call this command again with --token=.") + } + baseURL, err := flags.GetString("api") if err != nil { return err diff --git a/cmd/configure_test.go b/cmd/configure_test.go index f84ee7510..9d0b0ca24 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -16,6 +16,31 @@ import ( "github.com/stretchr/testify/assert" ) +func TestBareConfigure(t *testing.T) { + oldErr := Err + defer func() { + Err = oldErr + }() + + var buf bytes.Buffer + Err = &buf + + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + v := viper.New() + setupConfigureFlags(flags, v) + err := flags.Parse([]string{}) + assert.NoError(t, err) + + cfg := config.Configuration{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: "http://example.com", + } + + err = runConfigure(cfg, flags) + assert.Regexp(t, "no token configured", err.Error()) +} + func TestConfigureShow(t *testing.T) { oldErr := Err defer func() { @@ -194,7 +219,7 @@ func TestConfigureAPIBaseURL(t *testing.T) { { desc: "It validates the existing base url if we're not skipping validations", configured: ts.URL, - args: []string{}, + args: []string{"--token", "some-token"}, // need to bypass the error message on "bare configure" expected: ts.URL, err: true, message: "API.*cannot be reached", From 00d5fb0e45512fedd49412a298b645ff42dee749 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 22:25:10 +0100 Subject: [PATCH 105/544] Rework workspace logic in configure command Add tests for all the permutations. This does not deal with error handling in the case where there is a file in the workspace location, or where the directory already exists. --- cmd/configure.go | 21 ++++++---- cmd/configure_test.go | 93 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 101 insertions(+), 13 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 25a79c2a7..5b5194e83 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -120,11 +120,18 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro } cfg.Set("token", token) - cfg.Set("workspace", config.Resolve(viperConfig.GetString("workspace"), configuration.Home)) - - if cfg.GetString("workspace") == "" { - cfg.Set("workspace", config.DefaultWorkspaceDir(configuration)) + workspace, err := flags.GetString("workspace") + if err != nil { + return err } + if workspace == "" { + workspace = cfg.GetString("workspace") + } + workspace = config.Resolve(workspace, configuration.Home) + if workspace == "" { + workspace = config.DefaultWorkspaceDir(configuration) + } + cfg.Set("workspace", workspace) return configuration.Save("user") } @@ -145,17 +152,15 @@ func printCurrentConfig(configuration config.Configuration) { func initConfigureCmd() { viperConfig = viper.New() - setupConfigureFlags(configureCmd.Flags(), viperConfig) + setupConfigureFlags(configureCmd.Flags()) } -func setupConfigureFlags(flags *pflag.FlagSet, v *viper.Viper) { +func setupConfigureFlags(flags *pflag.FlagSet) { flags.StringP("token", "t", "", "authentication token used to connect to the site") flags.StringP("workspace", "w", "", "directory for exercism exercises") flags.StringP("api", "a", "", "API base url") flags.BoolP("show", "s", false, "show the current configuration") flags.BoolP("no-verify", "", false, "skip online token authorization check") - - v.BindPFlag("workspace", flags.Lookup("workspace")) } func init() { diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 9d0b0ca24..222dfc246 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -26,8 +26,9 @@ func TestBareConfigure(t *testing.T) { Err = &buf flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupConfigureFlags(flags) + v := viper.New() - setupConfigureFlags(flags, v) err := flags.Parse([]string{}) assert.NoError(t, err) @@ -51,13 +52,13 @@ func TestConfigureShow(t *testing.T) { Err = &buf flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupConfigureFlags(flags) + v := viper.New() v.Set("token", "configured-token") v.Set("workspace", "configured-workspace") v.Set("apibaseurl", "http://configured.example.com") - setupConfigureFlags(flags, v) - // it will ignore any flags args := []string{ "--show", @@ -160,9 +161,10 @@ func TestConfigureToken(t *testing.T) { Err = &buf flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupConfigureFlags(flags) + v := viper.New() v.Set("token", tc.configured) - setupConfigureFlags(flags, v) err := flags.Parse(tc.args) assert.NoError(t, err) @@ -247,9 +249,10 @@ func TestConfigureAPIBaseURL(t *testing.T) { Err = &buf flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupConfigureFlags(flags) + v := viper.New() v.Set("apibaseurl", tc.configured) - setupConfigureFlags(flags, v) err := flags.Parse(tc.args) assert.NoError(t, err) @@ -268,6 +271,86 @@ func TestConfigureAPIBaseURL(t *testing.T) { } } +func TestConfigureWorkspace(t *testing.T) { + testCases := []struct { + desc string + configured string + args []string + expected string + message string + err bool + }{ + { + desc: "It doesn't lose a configured value", + configured: "/the-workspace", + args: []string{"--no-verify"}, + expected: "/the-workspace", + }, + { + desc: "It writes a workspace when passed as a flag", + configured: "", + args: []string{"--no-verify", "--workspace", "/new-workspace"}, + expected: "/new-workspace", + }, + { + desc: "It overwrites the configured workspace", + configured: "/configured-workspace", + args: []string{"--no-verify", "--workspace", "/replacement-workspace"}, + expected: "/replacement-workspace", + }, + { + desc: "It gets the default workspace when neither configured nor passed as a flag", + configured: "", + args: []string{"--token", "some-token"}, // need to bypass the error message on "bare configure" + expected: "/home/default-workspace", + }, + { + desc: "It resolves the passed workspace to expand ~", + configured: "", + args: []string{"--workspace", "~/workspace-dir"}, + expected: "/home/workspace-dir", + }, + + { + desc: "It resolves the configured workspace to expand ~", + configured: "~/configured-dir", + args: []string{"--token", "some-token"}, // need to bypass the error message on "bare configure" + expected: "/home/configured-dir", // The configuration object hard-codes the home directory below + }, + } + + endpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 200 OK by default. Ping and TokenAuth will both pass. + }) + ts := httptest.NewServer(endpoint) + defer ts.Close() + + for _, tc := range testCases { + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupConfigureFlags(flags) + + v := viper.New() + v.Set("token", "abc123") // set a token so we get past the no token configured logic + v.Set("workspace", tc.configured) + + err := flags.Parse(tc.args) + assert.NoError(t, err) + + cfg := config.Configuration{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: ts.URL, + DefaultDirName: "default-workspace", + Home: "/home", + OS: "linux", + } + + err = runConfigure(cfg, flags) + assert.NoError(t, err, tc.desc) + assert.Equal(t, tc.expected, cfg.UserViperConfig.GetString("workspace"), tc.desc) + } +} + func TestConfigure(t *testing.T) { oldOut := Out oldErr := Err From 39f2b16bf9f1a77a853c7b1760aba63e1f0a88da Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 22:26:40 +0100 Subject: [PATCH 106/544] Delete redundant tests for configure command The original tests go all the way through the top of the cobra command and use the file system. The logic remaining in the RunE function itself is now minimal, and all the complexity has been isolated ot the runConfigure function, which we've now decoupled enough to test it without the filesystem. --- cmd/configure_test.go | 94 ------------------------------------------- 1 file changed, 94 deletions(-) diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 222dfc246..98aaa3d01 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -7,7 +7,6 @@ import ( "io/ioutil" "net/http" "net/http/httptest" - "runtime" "testing" "github.com/exercism/cli/config" @@ -350,96 +349,3 @@ func TestConfigureWorkspace(t *testing.T) { assert.Equal(t, tc.expected, cfg.UserViperConfig.GetString("workspace"), tc.desc) } } - -func TestConfigure(t *testing.T) { - oldOut := Out - oldErr := Err - Out = ioutil.Discard - Err = ioutil.Discard - defer func() { - Out = oldOut - Err = oldErr - }() - - type testCase struct { - desc string - args []string - existingUsrCfg *config.UserConfig - expectedUsrCfg *config.UserConfig - } - - testCases := []testCase{ - testCase{ - desc: "It writes the flags when there is no config file.", - args: []string{ - "fakeapp", "configure", "--no-verify", - "--token", "abc123", - "--workspace", "/workspace", - "--api", "http://api.example.com", - }, - existingUsrCfg: nil, - expectedUsrCfg: &config.UserConfig{Token: "abc123", Workspace: "/workspace", APIBaseURL: "http://api.example.com"}, - }, - testCase{ - desc: "It overwrites the flags in the config file.", - args: []string{ - "fakeapp", "configure", "--no-verify", - "--token", "new-token", - "--workspace", "/new-workspace", - "--api", "http://new.example.com", - }, - existingUsrCfg: &config.UserConfig{Token: "old-token", Workspace: "/old-workspace", APIBaseURL: "http://old.example.com"}, - expectedUsrCfg: &config.UserConfig{Token: "new-token", Workspace: "/new-workspace", APIBaseURL: "http://new.example.com"}, - }, - testCase{ - desc: "It overwrites the flags that are passed, without losing the ones that are not.", - args: []string{ - "fakeapp", "configure", "--no-verify", - "--token", "replacement-token", - }, - existingUsrCfg: &config.UserConfig{Token: "original-token", Workspace: "/unmodified", APIBaseURL: "http://unmodified.example.com"}, - expectedUsrCfg: &config.UserConfig{Token: "replacement-token", Workspace: "/unmodified", APIBaseURL: "http://unmodified.example.com"}, - }, - testCase{ - desc: "It gets the default API base url.", - args: []string{"fakeapp", "configure", "--no-verify"}, - existingUsrCfg: &config.UserConfig{Workspace: "/configured-workspace"}, - expectedUsrCfg: &config.UserConfig{Workspace: "/configured-workspace", APIBaseURL: "https://v2.exercism.io/api/v1"}, - }, - } - - for _, tc := range testCases { - cmdTest := &CommandTest{ - Cmd: configureCmd, - InitFn: initConfigureCmd, - Args: tc.args, - } - cmdTest.Setup(t) - defer cmdTest.Teardown(t) - - if tc.existingUsrCfg != nil { - // Write a fake config. - cfg := config.NewEmptyUserConfig() - cfg.Token = tc.existingUsrCfg.Token - cfg.Workspace = tc.existingUsrCfg.Workspace - cfg.APIBaseURL = tc.existingUsrCfg.APIBaseURL - err := cfg.Write() - assert.NoError(t, err, tc.desc) - } - - cmdTest.App.Execute() - - if tc.expectedUsrCfg != nil { - if runtime.GOOS == "windows" { - tc.expectedUsrCfg.SetDefaults() - } - - cfg, err := config.NewUserConfig() - - assert.NoError(t, err, tc.desc) - assert.Equal(t, tc.expectedUsrCfg.Token, cfg.Token, tc.desc) - assert.Equal(t, tc.expectedUsrCfg.Workspace, cfg.Workspace, tc.desc) - assert.Equal(t, tc.expectedUsrCfg.APIBaseURL, cfg.APIBaseURL, tc.desc) - } - } -} From 1190e8e33ddb6b747eed88e9b81b486fc3c53ced Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 22:38:29 +0100 Subject: [PATCH 107/544] Add commentary to the configure command --- cmd/configure.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/cmd/configure.go b/cmd/configure.go index 5b5194e83..5c5ce84b8 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -45,6 +45,7 @@ You can also override certain default settings to suit your preferences. func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) error { cfg := configuration.UserViperConfig + // Show the existing configuration and exit. show, err := flags.GetBool("show") if err != nil { return err @@ -54,15 +55,20 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro return nil } + // If the command is run 'bare' and we have no token, + // explain how to set the token. if flags.NFlag() == 0 && cfg.GetString("token") == "" { baseURL := cfg.GetString("apibaseurl") if baseURL != "" { + // If we have a base URL, then give the exact link. tokenURL := config.InferSiteURL(baseURL) + "/my/settings" return fmt.Errorf("There is no token configured. Find your token on %s, and call this command again with --token=.", tokenURL) } + // If we don't, then do our best. return fmt.Errorf("There is no token configured. Find your token in your settings on the website, and call this command again with --token=.") } + // Determine the base API URL. baseURL, err := flags.GetString("api") if err != nil { return err @@ -74,11 +80,15 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro baseURL = configuration.DefaultBaseURL } + // By default we verify that + // - the configured API URL is reachable. + // - the configured token is valid. skipVerification, err := flags.GetBool("no-verify") if err != nil { return err } + // Is the API URL reachable? if !skipVerification { client, err := api.NewClient("", baseURL) if err != nil { @@ -90,8 +100,10 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro return fmt.Errorf("The base API URL '%s' cannot be reached.\n\n%s", baseURL, err) } } + // Finally, configure the URL. cfg.Set("apibaseurl", baseURL) + // Determine the token. token, err := flags.GetString("token") if err != nil { return err @@ -100,11 +112,15 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro token = cfg.GetString("token") } + // Infer the URL where the token can be found. tokenURL := config.InferSiteURL(cfg.GetString("apibaseurl")) + "/my/settings" + + // If we don't have a token then explain how to set it and bail. if token == "" { return fmt.Errorf("There is no token configured. Find your token on %s, and call this command again with --token=.", tokenURL) } + // Verify that the token is valid. if !skipVerification { client, err := api.NewClient(token, baseURL) if err != nil { @@ -118,8 +134,11 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro return fmt.Errorf("The token '%s' is invalid. Find your token on %s.", token, tokenURL) } } + + // Finally, configure the token. cfg.Set("token", token) + // Determine the workspace. workspace, err := flags.GetString("workspace") if err != nil { return err @@ -131,8 +150,10 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro if workspace == "" { workspace = config.DefaultWorkspaceDir(configuration) } + // Configure the workspace. cfg.Set("workspace", workspace) + // Persist the new configuration. return configuration.Save("user") } From f62c0089cc194eeadf3c106b95b0b66c852773ce Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 8 Jul 2018 22:41:00 +0100 Subject: [PATCH 108/544] Print the new configuration after saving it --- cmd/configure.go | 6 +++++- cmd/configure_test.go | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cmd/configure.go b/cmd/configure.go index 5c5ce84b8..d45bf24e2 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -154,7 +154,11 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro cfg.Set("workspace", workspace) // Persist the new configuration. - return configuration.Save("user") + if err := configuration.Save("user"); err != nil { + return err + } + printCurrentConfig(configuration) + return nil } func printCurrentConfig(configuration config.Configuration) { diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 98aaa3d01..93e886b45 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -271,6 +271,12 @@ func TestConfigureAPIBaseURL(t *testing.T) { } func TestConfigureWorkspace(t *testing.T) { + oldErr := Err + Err = ioutil.Discard + defer func() { + Err = oldErr + }() + testCases := []struct { desc string configured string From d22cd5c0030d048020d85f78687531f04a62bc21 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 10 Jul 2018 10:19:19 +0100 Subject: [PATCH 109/544] Do not clobber directory when configuring default workspace If you explicitly pass a --workspace flag, then trust that the user knows what they're doing. If they're not explicitly choosing a workspace, then don't automatically set the workspace to an existing directory. --- cmd/configure.go | 30 +++++++++++++++++++++++ cmd/configure_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/cmd/configure.go b/cmd/configure.go index d45bf24e2..774478638 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -1,7 +1,10 @@ package cmd import ( + "errors" "fmt" + "os" + "strings" "text/tabwriter" "github.com/exercism/cli/api" @@ -149,6 +152,22 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro workspace = config.Resolve(workspace, configuration.Home) if workspace == "" { workspace = config.DefaultWorkspaceDir(configuration) + + // If it already exists don't clobber it with the default. + if _, err := os.Lstat(workspace); !os.IsNotExist(err) { + msg := ` + The default Exercism workspace is + + %s + + There is already a directory there. + You can choose the workspace location by rerunning this command with the --workspace flag. + + %s configure %s --workspace=%s + ` + + return errors.New(fmt.Sprintf(msg, workspace, BinaryName, commandify(flags), workspace)) + } } // Configure the workspace. cfg.Set("workspace", workspace) @@ -175,6 +194,17 @@ func printCurrentConfig(configuration config.Configuration) { fmt.Fprintln(w, "") } +func commandify(flags *pflag.FlagSet) string { + var cmd string + fn := func(f *pflag.Flag) { + if f.Changed { + cmd = fmt.Sprintf("%s --%s=%s", cmd, f.Name, f.Value.String()) + } + } + flags.VisitAll(fn) + return strings.TrimLeft(cmd, " ") +} + func initConfigureCmd() { viperConfig = viper.New() setupConfigureFlags(configureCmd.Flags()) diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 93e886b45..a029f892d 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "os" "testing" "github.com/exercism/cli/config" @@ -355,3 +356,57 @@ func TestConfigureWorkspace(t *testing.T) { assert.Equal(t, tc.expected, cfg.UserViperConfig.GetString("workspace"), tc.desc) } } + +func TestConfigureDefaultWorkspaceWithoutClobbering(t *testing.T) { + oldOut := Out + oldErr := Err + Out = ioutil.Discard + Err = ioutil.Discard + defer func() { + Out = oldOut + Err = oldErr + }() + + // Stub server to always be 200 OK + endpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + ts := httptest.NewServer(endpoint) + defer ts.Close() + + tmpDir, err := ioutil.TempDir("", "no-clobber") + assert.NoError(t, err) + + cfg := config.Configuration{ + OS: "linux", + DefaultDirName: "workspace", + Home: tmpDir, + Dir: tmpDir, + DefaultBaseURL: ts.URL, + UserViperConfig: viper.New(), + Persister: config.InMemoryPersister{}, + } + + // Create a directory at the workspace directory's location + // so that it's already present. + err = os.MkdirAll(config.DefaultWorkspaceDir(cfg), os.FileMode(0755)) + assert.NoError(t, err) + + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupConfigureFlags(flags) + err = flags.Parse([]string{"--token", "abc123"}) + assert.NoError(t, err) + + err = runConfigure(cfg, flags) + assert.Error(t, err) + assert.Regexp(t, "already a directory", err.Error()) +} + +func TestCommandifyFlagSet(t *testing.T) { + flags := pflag.NewFlagSet("primitives", pflag.PanicOnError) + flags.StringP("word", "w", "", "a word") + flags.BoolP("yes", "y", false, "just do it") + flags.IntP("number", "n", 1, "count to one") + + err := flags.Parse([]string{"--word", "banana", "--yes"}) + assert.NoError(t, err) + assert.Equal(t, commandify(flags), "--word=banana --yes=true") +} From fb8b8f098e7951cce7d4e93095149ef80b28a70c Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 10 Jul 2018 13:53:48 +0100 Subject: [PATCH 110/544] Add indirection to the submit command The test setup/teardown is quite arduous right now. This splits apart the submit command so that we can set up all the environment and configuration in the RunE function, and then pass the configuration value along with flags and arguments to the runSubmit function. This lets us ignore the cobra.Command setup/teardown in the command tests. --- cmd/submit.go | 399 +++++++++++++++++++++++++------------------------- 1 file changed, 202 insertions(+), 197 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 8ecb8050c..fb6b17a77 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -15,6 +15,7 @@ import ( "github.com/exercism/cli/config" "github.com/exercism/cli/workspace" "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/spf13/viper" ) @@ -38,249 +39,253 @@ figuring things out if necessary. `, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - // Validate input before doing any other work - exercise, err := cmd.Flags().GetString("exercise") - if err != nil { - return err - } - - trackID, err := cmd.Flags().GetString("track") - if err != nil { - return err - } - - files, err := cmd.Flags().GetStringSlice("files") - if err != nil { - return err - } - - // Verify that both --track and --exercise are used together - if len(args) == 0 && len(files) == 0 && !(exercise != "" && trackID != "") { - // Are they both missing? - if exercise == "" && trackID == "" { - return errors.New("Please use the --exercise/--trackID flags to submit without an explicit directory or files.") - } - // Guess that --trackID is missing, unless it's not - present, missing := "--exercise", "--track" - if trackID != "" { - present, missing = missing, present - } - // Help user correct CLI command - missingFlagMessage := fmt.Sprintf("You specified %s, please also include %s.", present, missing) - return errors.New(missingFlagMessage) - } + return runSubmit(config.Configuration{}, cmd.Flags(), args) + }, +} - if len(args) > 0 && (exercise != "" || trackID != "") { - return errors.New("You are submitting a directory. We will infer the track and exercise from that. Please re-run the submit command without the flags.") +func runSubmit(configuration config.Configuration, flags *pflag.FlagSet, args []string) error { + // Validate input before doing any other work + exercise, err := flags.GetString("exercise") + if err != nil { + return err + } + + trackID, err := flags.GetString("track") + if err != nil { + return err + } + + files, err := flags.GetStringSlice("files") + if err != nil { + return err + } + + // Verify that both --track and --exercise are used together + if len(args) == 0 && len(files) == 0 && !(exercise != "" && trackID != "") { + // Are they both missing? + if exercise == "" && trackID == "" { + return errors.New("Please use the --exercise/--trackID flags to submit without an explicit directory or files.") } - - if len(files) > 0 && len(args) > 0 { - return errors.New("You can submit either a list of files, or a directory, but not both.") + // Guess that --trackID is missing, unless it's not + present, missing := "--exercise", "--track" + if trackID != "" { + present, missing = missing, present } - - usrCfg, err := config.NewUserConfig() + // Help user correct CLI command + missingFlagMessage := fmt.Sprintf("You specified %s, please also include %s.", present, missing) + return errors.New(missingFlagMessage) + } + + if len(args) > 0 && (exercise != "" || trackID != "") { + return errors.New("You are submitting a directory. We will infer the track and exercise from that. Please re-run the submit command without the flags.") + } + + if len(files) > 0 && len(args) > 0 { + return errors.New("You can submit either a list of files, or a directory, but not both.") + } + + usrCfg, err := config.NewUserConfig() + if err != nil { + return err + } + + cliCfg, err := config.NewCLIConfig() + if err != nil { + return err + } + + // TODO: make sure we get the workspace configured. + if usrCfg.Workspace == "" { + cwd, err := os.Getwd() if err != nil { return err } + usrCfg.Workspace = filepath.Dir(filepath.Dir(cwd)) + } + + ws := workspace.New(usrCfg.Workspace) + + // Create directory from track and exercise slugs if needed + if trackID != "" && exercise != "" { + args = []string{filepath.Join(ws.Dir, trackID, exercise)} + } else if len(files) > 0 { + args = files + } + + tx, err := workspace.NewTransmission(ws.Dir, args) + if err != nil { + return err + } + + dirs, err := ws.Locate(tx.Dir) + if err != nil { + return err + } + + sx, err := workspace.NewSolutions(dirs) + if err != nil { + return err + } + + var solution *workspace.Solution + + selection := comms.NewSelection() + for _, s := range sx { + selection.Items = append(selection.Items, s) + } + + for { + prompt := ` + We found more than one. Which one did you mean? + Type the number of the one you want to select. - cliCfg, err := config.NewCLIConfig() + %s + > ` + option, err := selection.Pick(prompt) if err != nil { return err } - - // TODO: make sure we get the workspace configured. - if usrCfg.Workspace == "" { - cwd, err := os.Getwd() - if err != nil { - return err - } - usrCfg.Workspace = filepath.Dir(filepath.Dir(cwd)) - } - - ws := workspace.New(usrCfg.Workspace) - - // Create directory from track and exercise slugs if needed - if trackID != "" && exercise != "" { - args = []string{filepath.Join(ws.Dir, trackID, exercise)} - } else if len(files) > 0 { - args = files - } - - tx, err := workspace.NewTransmission(ws.Dir, args) - if err != nil { - return err + s, ok := option.(*workspace.Solution) + if !ok { + fmt.Fprintf(Err, "something went wrong trying to pick that solution, not sure what happened") + continue } + solution = s + break + } - dirs, err := ws.Locate(tx.Dir) - if err != nil { - return err - } + if !solution.IsRequester { + return errors.New("not your solution") + } - sx, err := workspace.NewSolutions(dirs) + track := cliCfg.Tracks[solution.Track] + if track == nil { + err := prepareTrack(solution.Track) if err != nil { return err } - - var solution *workspace.Solution - - selection := comms.NewSelection() - for _, s := range sx { - selection.Items = append(selection.Items, s) - } - - for { - prompt := ` - We found more than one. Which one did you mean? - Type the number of the one you want to select. - - %s - > ` - option, err := selection.Pick(prompt) - if err != nil { + cliCfg.Load(viper.New()) + track = cliCfg.Tracks[solution.Track] + } + + paths := tx.Files + if len(paths) == 0 { + walkFn := func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { return err } - s, ok := option.(*workspace.Solution) - if !ok { - fmt.Fprintf(Err, "something went wrong trying to pick that solution, not sure what happened") - continue - } - solution = s - break - } - - if !solution.IsRequester { - return errors.New("not your solution") - } - - track := cliCfg.Tracks[solution.Track] - if track == nil { - err := prepareTrack(solution.Track) - if err != nil { + ok, err := track.AcceptFilename(path) + if err != nil || !ok { return err } - cliCfg.Load(viper.New()) - track = cliCfg.Tracks[solution.Track] + paths = append(paths, path) + return nil } + filepath.Walk(solution.Dir, walkFn) + } - paths := tx.Files - if len(paths) == 0 { - walkFn := func(path string, info os.FileInfo, err error) error { - if err != nil || info.IsDir() { - return err - } - ok, err := track.AcceptFilename(path) - if err != nil || !ok { - return err - } - paths = append(paths, path) - return nil - } - filepath.Walk(solution.Dir, walkFn) - } - - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - if len(paths) == 0 { - return errors.New("no files found to submit") - } + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) - // If the user submits a directory, confirm the list of files. - if len(tx.ArgDirs) > 0 { - prompt := "You specified a directory, which contains these files:\n" - for i, path := range paths { - prompt += fmt.Sprintf(" [%d] %s\n", i+1, path) - } - prompt += "\nPress ENTER to submit, or control + c to cancel: " + if len(paths) == 0 { + return errors.New("no files found to submit") + } - confirmQuestion := &comms.Question{ - Prompt: prompt, - DefaultValue: "y", - Reader: In, - Writer: Out, - } - answer, err := confirmQuestion.Ask() - if err != nil { - return err - } - if strings.ToLower(answer) != "y" { - fmt.Fprintf(Err, "Submit cancelled.\nTry submitting individually instead.") - return nil - } - fmt.Fprintf(Err, "Submitting files now...") + // If the user submits a directory, confirm the list of files. + if len(tx.ArgDirs) > 0 { + prompt := "You specified a directory, which contains these files:\n" + for i, path := range paths { + prompt += fmt.Sprintf(" [%d] %s\n", i+1, path) } + prompt += "\nPress ENTER to submit, or control + c to cancel: " - for _, path := range paths { - // Don't submit empty files - info, err := os.Stat(path) - if err != nil { - return err - } - if info.Size() == 0 { - fmt.Printf("Warning: file %s was empty, skipping...", path) - continue - } - file, err := os.Open(path) - 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 := fmt.Sprintf("%s%s", string(os.PathSeparator), pieces[len(pieces)-1]) - - part, err := writer.CreateFormFile("files[]", filename) - if err != nil { - return err - } - _, err = io.Copy(part, file) - if err != nil { - return err - } + confirmQuestion := &comms.Question{ + Prompt: prompt, + DefaultValue: "y", + Reader: In, + Writer: Out, } - - err = writer.Close() + answer, err := confirmQuestion.Ask() if err != nil { return err } + if strings.ToLower(answer) != "y" { + fmt.Fprintf(Err, "Submit cancelled.\nTry submitting individually instead.") + return nil + } + fmt.Fprintf(Err, "Submitting files now...") + } - client, err := api.NewClient(usrCfg.Token, usrCfg.APIBaseURL) + for _, path := range paths { + // Don't submit empty files + info, err := os.Stat(path) if err != nil { return err } - url := fmt.Sprintf("%s/solutions/%s", usrCfg.APIBaseURL, solution.ID) - req, err := client.NewRequest("PATCH", url, body) + if info.Size() == 0 { + fmt.Printf("Warning: file %s was empty, skipping...", path) + continue + } + file, err := os.Open(path) if err != nil { return err } - req.Header.Set("Content-Type", writer.FormDataContentType()) + defer file.Close() + + dirname := fmt.Sprintf("%s%s%s", string(os.PathSeparator), solution.Exercise, string(os.PathSeparator)) + pieces := strings.Split(path, dirname) + filename := fmt.Sprintf("%s%s", string(os.PathSeparator), pieces[len(pieces)-1]) - resp, err := client.Do(req) + part, err := writer.CreateFormFile("files[]", filename) if err != nil { return err } - defer resp.Body.Close() - - bb := &bytes.Buffer{} - _, err = bb.ReadFrom(resp.Body) + _, err = io.Copy(part, file) if err != nil { return err } - - if solution.AutoApprove == true { - msg := `Your solution has been submitted successfully and has been auto-approved. + } + + err = writer.Close() + if err != nil { + return err + } + + client, err := api.NewClient(usrCfg.Token, usrCfg.APIBaseURL) + if err != nil { + return err + } + url := fmt.Sprintf("%s/solutions/%s", usrCfg.APIBaseURL, solution.ID) + req, err := client.NewRequest("PATCH", url, body) + if err != nil { + return err + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + bb := &bytes.Buffer{} + _, err = bb.ReadFrom(resp.Body) + if err != nil { + return err + } + + if solution.AutoApprove == true { + msg := `Your solution has been submitted successfully and has been auto-approved. You can complete the exercise and unlock the next core exercise at: ` - fmt.Fprintf(Err, msg) - } else { - msg := "Your solution has been submitted successfully. View it at:\n" - fmt.Fprintf(Err, msg) - } - fmt.Fprintf(Out, "%s\n", solution.URL) - - return nil - }, + fmt.Fprintf(Err, msg) + } else { + msg := "Your solution has been submitted successfully. View it at:\n" + fmt.Fprintf(Err, msg) + } + fmt.Fprintf(Out, "%s\n", solution.URL) + + return nil } func initSubmitCmd() { From f6b632dbcbb0e0d83029e810d88328a3164decf9 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 10 Jul 2018 14:00:15 +0100 Subject: [PATCH 111/544] Inject configuration into submit command This will allow us to simplify the tests to use an in-memory configuration persistence strategy. --- cmd/submit.go | 29 ++++++++++++++++++----------- config/configuration.go | 2 +- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index fb6b17a77..33c1f318e 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -39,11 +39,25 @@ figuring things out if necessary. `, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runSubmit(config.Configuration{}, cmd.Flags(), args) + usrCfg, err := config.NewUserConfig() + if err != nil { + return err + } + + cliCfg, err := config.NewCLIConfig() + if err != nil { + return err + } + + cfg := config.NewConfiguration() + cfg.UserConfig = usrCfg + cfg.CLIConfig = cliCfg + + return runSubmit(cfg, cmd.Flags(), args) }, } -func runSubmit(configuration config.Configuration, flags *pflag.FlagSet, args []string) error { +func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) error { // Validate input before doing any other work exercise, err := flags.GetString("exercise") if err != nil { @@ -84,15 +98,8 @@ func runSubmit(configuration config.Configuration, flags *pflag.FlagSet, args [] return errors.New("You can submit either a list of files, or a directory, but not both.") } - usrCfg, err := config.NewUserConfig() - if err != nil { - return err - } - - cliCfg, err := config.NewCLIConfig() - if err != nil { - return err - } + usrCfg := cfg.UserConfig + cliCfg := cfg.CLIConfig // TODO: make sure we get the workspace configured. if usrCfg.Workspace == "" { diff --git a/config/configuration.go b/config/configuration.go index 54adf6e05..8f3ca3f42 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -27,7 +27,7 @@ type Configuration struct { DefaultDirName string UserViperConfig *viper.Viper UserConfig *UserConfig - CLI *CLIConfig + CLIConfig *CLIConfig Persister Persister } From 3401b22117d0f530388da1f8501293fde8031983 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 10 Jul 2018 14:17:27 +0100 Subject: [PATCH 112/544] Use viper config rather than UserConfig in submit command --- cmd/submit.go | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 33c1f318e..1117d4da5 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -39,18 +39,20 @@ figuring things out if necessary. `, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - usrCfg, err := config.NewUserConfig() - if err != nil { - return err - } + cfg := config.NewConfiguration() + + usrCfg := viper.New() + usrCfg.AddConfigPath(cfg.Dir) + usrCfg.SetConfigName("user") + usrCfg.SetConfigType("json") + // Ignore error. If the file doesn't exist, that is fine. + _ = usrCfg.ReadInConfig() + cfg.UserViperConfig = usrCfg cliCfg, err := config.NewCLIConfig() if err != nil { return err } - - cfg := config.NewConfiguration() - cfg.UserConfig = usrCfg cfg.CLIConfig = cliCfg return runSubmit(cfg, cmd.Flags(), args) @@ -98,19 +100,19 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er return errors.New("You can submit either a list of files, or a directory, but not both.") } - usrCfg := cfg.UserConfig + usrCfg := cfg.UserViperConfig cliCfg := cfg.CLIConfig // TODO: make sure we get the workspace configured. - if usrCfg.Workspace == "" { + if usrCfg.GetString("workspace") == "" { cwd, err := os.Getwd() if err != nil { return err } - usrCfg.Workspace = filepath.Dir(filepath.Dir(cwd)) + usrCfg.Set("workspace", filepath.Dir(filepath.Dir(cwd))) } - ws := workspace.New(usrCfg.Workspace) + ws := workspace.New(usrCfg.GetString("workspace")) // Create directory from track and exercise slugs if needed if trackID != "" && exercise != "" { @@ -258,11 +260,11 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er return err } - client, err := api.NewClient(usrCfg.Token, usrCfg.APIBaseURL) + client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl")) if err != nil { return err } - url := fmt.Sprintf("%s/solutions/%s", usrCfg.APIBaseURL, solution.ID) + url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), solution.ID) req, err := client.NewRequest("PATCH", url, body) if err != nil { return err From 1a17c4893446e5bdd34a58bafd830374445598d3 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 10 Jul 2018 14:27:10 +0100 Subject: [PATCH 113/544] Make CLIConfig loading explicit in submit command We're working to get rid of the embedded viper config pattern that we currently use for the UserConfig and CLIConfig types, as these rely on a lot of implicit context which makes testing much more difficult. We still have an explicit Load() in the runSubmit command which we will need to get rid of, but this will require clarifying the behavior around the track preparation. We should be able to do this in such a way that we don't have to load a new configuration off the filesystem. --- cmd/submit.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 1117d4da5..e5632158a 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -49,11 +49,18 @@ figuring things out if necessary. _ = usrCfg.ReadInConfig() cfg.UserViperConfig = usrCfg - cliCfg, err := config.NewCLIConfig() - if err != nil { + v := viper.New() + v.AddConfigPath(cfg.Dir) + v.SetConfigName("cli") + v.SetConfigType("json") + // Ignore error. If the file doesn't exist, that is fine. + _ = v.ReadInConfig() + + cliCfg := config.CLIConfig{Tracks: config.Tracks{}} + if err := v.Unmarshal(&cliCfg); err != nil { return err } - cfg.CLIConfig = cliCfg + cfg.CLIConfig = &cliCfg return runSubmit(cfg, cmd.Flags(), args) }, From f5d17a5381a4ae84aa34f5c60775a58b9f2daa22 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 10 Jul 2018 20:02:46 +0100 Subject: [PATCH 114/544] Move flag setup in submit command into its own method This will let us write tests against runSubmit() instead of submitCmd. --- cmd/submit.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index e5632158a..a43e03594 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -305,9 +305,13 @@ You can complete the exercise and unlock the next core exercise at: } func initSubmitCmd() { - submitCmd.Flags().StringP("track", "t", "", "the track ID") - submitCmd.Flags().StringP("exercise", "e", "", "the exercise ID") - submitCmd.Flags().StringSliceP("files", "f", make([]string, 0), "files to submit") + setupSubmitFlags(submitCmd.Flags()) +} + +func setupSubmitFlags(flags *pflag.FlagSet) { + flags.StringP("track", "t", "", "the track ID") + flags.StringP("exercise", "e", "", "the exercise ID") + flags.StringSliceP("files", "f", make([]string, 0), "files to submit") } func init() { From 7d96c68a1ab468a2754aac384c74b15641a05b16 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 11 Jul 2018 07:55:44 +0100 Subject: [PATCH 115/544] Remove interactive prompt for submitting files in directory For launch we are not going to support submitting directories. As a first step, just assume that when submitting a directory you want to submit everything in it. --- cmd/submit.go | 25 ------------------------- cmd/submit_test.go | 24 +++++++++--------------- 2 files changed, 9 insertions(+), 40 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index a43e03594..02ca63e59 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -207,31 +207,6 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er return errors.New("no files found to submit") } - // If the user submits a directory, confirm the list of files. - if len(tx.ArgDirs) > 0 { - prompt := "You specified a directory, which contains these files:\n" - for i, path := range paths { - prompt += fmt.Sprintf(" [%d] %s\n", i+1, path) - } - prompt += "\nPress ENTER to submit, or control + c to cancel: " - - confirmQuestion := &comms.Question{ - Prompt: prompt, - DefaultValue: "y", - Reader: In, - Writer: Out, - } - answer, err := confirmQuestion.Ask() - if err != nil { - return err - } - if strings.ToLower(answer) != "y" { - fmt.Fprintf(Err, "Submit cancelled.\nTry submitting individually instead.") - return nil - } - fmt.Fprintf(Err, "Submitting files now...") - } - for _, path := range paths { // Don't submit empty files info, err := os.Stat(path) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index a5306ebc0..0b4bbd942 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -46,22 +46,19 @@ func TestSubmit(t *testing.T) { } // Make a list of test commands cmdTestFlags := &CommandTest{ - Cmd: submitCmd, - InitFn: initSubmitCmd, - MockInteractiveResponse: "\n", - Args: []string{"fakeapp", "submit", "-e", "bogus-exercise", "-t", "bogus-track"}, + Cmd: submitCmd, + InitFn: initSubmitCmd, + Args: []string{"fakeapp", "submit", "-e", "bogus-exercise", "-t", "bogus-track"}, } cmdTestRelativeDir := &CommandTest{ - Cmd: submitCmd, - InitFn: initSubmitCmd, - MockInteractiveResponse: "\n", - Args: []string{"fakeapp", "submit", filepath.Join("bogus-track", "bogus-exercise")}, + Cmd: submitCmd, + InitFn: initSubmitCmd, + Args: []string{"fakeapp", "submit", filepath.Join("bogus-track", "bogus-exercise")}, } cmdTestFilesFlag := &CommandTest{ - Cmd: submitCmd, - InitFn: initSubmitCmd, - MockInteractiveResponse: "\n", - Args: []string{"fakeapp", "submit", "--files"}, + Cmd: submitCmd, + InitFn: initSubmitCmd, + Args: []string{"fakeapp", "submit", "--files"}, } tests := []*CommandTest{ cmdTestFlags, @@ -143,9 +140,6 @@ func TestSubmit(t *testing.T) { err = cliCfg.Write() assert.NoError(t, err) - // Write mock interactive input to In for the CLI command. - In = strings.NewReader(cmdTest.MockInteractiveResponse) - // Execute the command! cmdTest.App.Execute() From 0cfe3372d5f78e92dbe6feca398395dc01f315c1 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 11 Jul 2018 09:54:46 +0100 Subject: [PATCH 116/544] Delete tests for submit functionality that is going away We're temporarily going to _only_ support explicit file paths. Not directories. Not implicit directories in the form of an exercise and track. --- cmd/submit_test.go | 154 ++++++++++++++++++++------------------------- 1 file changed, 68 insertions(+), 86 deletions(-) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 0b4bbd942..20fbccf85 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -44,110 +44,92 @@ func TestSubmit(t *testing.T) { relativePath: "README.md", contents: "The readme.", } - // Make a list of test commands - cmdTestFlags := &CommandTest{ - Cmd: submitCmd, - InitFn: initSubmitCmd, - Args: []string{"fakeapp", "submit", "-e", "bogus-exercise", "-t", "bogus-track"}, - } - cmdTestRelativeDir := &CommandTest{ - Cmd: submitCmd, - InitFn: initSubmitCmd, - Args: []string{"fakeapp", "submit", filepath.Join("bogus-track", "bogus-exercise")}, - } - cmdTestFilesFlag := &CommandTest{ + cmdTest := &CommandTest{ Cmd: submitCmd, InitFn: initSubmitCmd, Args: []string{"fakeapp", "submit", "--files"}, } - tests := []*CommandTest{ - cmdTestFlags, - cmdTestRelativeDir, - cmdTestFilesFlag, - } - for _, cmdTest := range tests { - cmdTest.Setup(t) - defer cmdTest.Teardown(t) + cmdTest.Setup(t) + defer cmdTest.Teardown(t) - // Prefix submitted filenames with correct temporary directory - if cmdTest.Args[2] == "--files" { - filenames := make([]string, 2) - for i, file := range []file{file1, file2} { - filenames[i] = filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", file.relativePath) - } - filenameString := strings.Join(filenames, ",") - cmdTest.Args[2] = fmt.Sprintf("-f=%s", filenameString) - } else if len(cmdTest.Args) == 3 { - cmdTest.Args[2] = filepath.Join(cmdTest.TmpDir, cmdTest.Args[2]) + // Prefix submitted filenames with correct temporary directory + if cmdTest.Args[2] == "--files" { + filenames := make([]string, 2) + for i, file := range []file{file1, file2} { + filenames[i] = filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", file.relativePath) } - // Create a temp dir for the config and the exercise files. - dir := filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise") - os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) + filenameString := strings.Join(filenames, ",") + cmdTest.Args[2] = fmt.Sprintf("-f=%s", filenameString) + } else if len(cmdTest.Args) == 3 { + cmdTest.Args[2] = filepath.Join(cmdTest.TmpDir, cmdTest.Args[2]) + } + // Create a temp dir for the config and the exercise files. + dir := filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) - solution := &workspace.Solution{ - ID: "bogus-solution-uuid", - Track: "bogus-track", - Exercise: "bogus-exercise", - IsRequester: true, - } - err := solution.Write(dir) + solution := &workspace.Solution{ + ID: "bogus-solution-uuid", + Track: "bogus-track", + Exercise: "bogus-exercise", + IsRequester: true, + } + err := solution.Write(dir) + assert.NoError(t, err) + + for _, file := range []file{file1, file2, file3} { + err := ioutil.WriteFile(filepath.Join(dir, file.relativePath), []byte(file.contents), os.FileMode(0755)) assert.NoError(t, err) + } - for _, file := range []file{file1, file2, file3} { - err := ioutil.WriteFile(filepath.Join(dir, file.relativePath), []byte(file.contents), os.FileMode(0755)) - assert.NoError(t, err) - } + // The fake endpoint will populate this when it receives the call from the command. + submittedFiles := map[string]string{} - // The fake endpoint will populate this when it receives the call from the command. - submittedFiles := map[string]string{} + // Set up the test server. + fakeEndpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := r.ParseMultipartForm(2 << 10) + if err != nil { + t.Fatal(err) + } + mf := r.MultipartForm - // Set up the test server. - fakeEndpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - err := r.ParseMultipartForm(2 << 10) + files := mf.File["files[]"] + for _, fileHeader := range files { + file, err := fileHeader.Open() if err != nil { t.Fatal(err) } - mf := r.MultipartForm - - files := mf.File["files[]"] - for _, fileHeader := range files { - file, err := fileHeader.Open() - if err != nil { - t.Fatal(err) - } - defer file.Close() - body, err := ioutil.ReadAll(file) - if err != nil { - t.Fatal(err) - } - submittedFiles[fileHeader.Filename] = string(body) + defer file.Close() + body, err := ioutil.ReadAll(file) + if err != nil { + t.Fatal(err) } - }) - ts := httptest.NewServer(fakeEndpoint) - defer ts.Close() + submittedFiles[fileHeader.Filename] = string(body) + } + }) + ts := httptest.NewServer(fakeEndpoint) + defer ts.Close() - // Create a fake user config. - usrCfg := config.NewEmptyUserConfig() - usrCfg.Workspace = cmdTest.TmpDir - usrCfg.APIBaseURL = ts.URL - err = usrCfg.Write() - assert.NoError(t, err) + // Create a fake user config. + usrCfg := config.NewEmptyUserConfig() + usrCfg.Workspace = cmdTest.TmpDir + usrCfg.APIBaseURL = ts.URL + err = usrCfg.Write() + assert.NoError(t, err) - // Create a fake CLI config. - cliCfg, err := config.NewCLIConfig() - assert.NoError(t, err) - cliCfg.Tracks["bogus-track"] = config.NewTrack("bogus-track") - err = cliCfg.Write() - assert.NoError(t, err) + // Create a fake CLI config. + cliCfg, err := config.NewCLIConfig() + assert.NoError(t, err) + cliCfg.Tracks["bogus-track"] = config.NewTrack("bogus-track") + err = cliCfg.Write() + assert.NoError(t, err) - // Execute the command! - cmdTest.App.Execute() + // Execute the command! + cmdTest.App.Execute() - // We got only the file we expected. - assert.Equal(t, 2, len(submittedFiles)) - for _, file := range []file{file1, file2} { - path := string(os.PathSeparator) + file.relativePath - assert.Equal(t, file.contents, submittedFiles[path]) - } + // We got only the file we expected. + assert.Equal(t, 2, len(submittedFiles)) + for _, file := range []file{file1, file2} { + path := string(os.PathSeparator) + file.relativePath + assert.Equal(t, file.contents, submittedFiles[path]) } } From 77ba13c0b184b4b1cfe05f518b95d92d23b9f978 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 11 Jul 2018 10:58:54 +0100 Subject: [PATCH 117/544] Rewrite the submit files test This tests through the runSubmit() function rather than the RunE function. The test is a lot more straight-forward than the previous one, so we can start deleting functionality from the command and understand what is going on. --- cmd/submit_test.go | 144 +++++++++++++++++++++++---------------------- 1 file changed, 73 insertions(+), 71 deletions(-) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 20fbccf85..6e50b8b6a 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -1,7 +1,6 @@ package cmd import ( - "fmt" "io/ioutil" "net/http" "net/http/httptest" @@ -12,75 +11,20 @@ import ( "github.com/exercism/cli/config" "github.com/exercism/cli/workspace" + "github.com/spf13/pflag" + "github.com/spf13/viper" "github.com/stretchr/testify/assert" ) -func TestSubmit(t *testing.T) { +func TestSubmitFiles(t *testing.T) { oldOut := Out oldErr := Err - oldIn := In Out = ioutil.Discard Err = ioutil.Discard defer func() { Out = oldOut Err = oldErr - In = oldIn }() - - type file struct { - relativePath string - contents string - } - - file1 := file{ - relativePath: "file-1.txt", - contents: "This is file 1.", - } - file2 := file{ - relativePath: filepath.Join("subdir", "file-2.txt"), - contents: "This is file 2.", - } - file3 := file{ - relativePath: "README.md", - contents: "The readme.", - } - cmdTest := &CommandTest{ - Cmd: submitCmd, - InitFn: initSubmitCmd, - Args: []string{"fakeapp", "submit", "--files"}, - } - cmdTest.Setup(t) - defer cmdTest.Teardown(t) - - // Prefix submitted filenames with correct temporary directory - if cmdTest.Args[2] == "--files" { - filenames := make([]string, 2) - for i, file := range []file{file1, file2} { - filenames[i] = filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", file.relativePath) - } - filenameString := strings.Join(filenames, ",") - cmdTest.Args[2] = fmt.Sprintf("-f=%s", filenameString) - } else if len(cmdTest.Args) == 3 { - cmdTest.Args[2] = filepath.Join(cmdTest.TmpDir, cmdTest.Args[2]) - } - // Create a temp dir for the config and the exercise files. - dir := filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise") - os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) - - solution := &workspace.Solution{ - ID: "bogus-solution-uuid", - Track: "bogus-track", - Exercise: "bogus-exercise", - IsRequester: true, - } - err := solution.Write(dir) - assert.NoError(t, err) - - for _, file := range []file{file1, file2, file3} { - err := ioutil.WriteFile(filepath.Join(dir, file.relativePath), []byte(file.contents), os.FileMode(0755)) - assert.NoError(t, err) - } - // The fake endpoint will populate this when it receives the call from the command. submittedFiles := map[string]string{} @@ -109,26 +53,84 @@ func TestSubmit(t *testing.T) { ts := httptest.NewServer(fakeEndpoint) defer ts.Close() - // Create a fake user config. - usrCfg := config.NewEmptyUserConfig() - usrCfg.Workspace = cmdTest.TmpDir - usrCfg.APIBaseURL = ts.URL - err = usrCfg.Write() + tmpDir, err := ioutil.TempDir("", "submit-files") + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) + + type file struct { + relativePath string + contents string + } + + file1 := file{ + relativePath: "file-1.txt", + contents: "This is file 1.", + } + file2 := file{ + relativePath: filepath.Join("subdir", "file-2.txt"), + contents: "This is file 2.", + } + file3 := file{ + relativePath: "README.md", + contents: "The readme.", + } + + filenames := make([]string, 0, 3) + for _, file := range []file{file1, file2, file3} { + path := filepath.Join(dir, file.relativePath) + filenames = append(filenames, path) + err := ioutil.WriteFile(path, []byte(file.contents), os.FileMode(0755)) + assert.NoError(t, err) + } + + solution := &workspace.Solution{ + ID: "bogus-solution-uuid", + Track: "bogus-track", + Exercise: "bogus-exercise", + IsRequester: true, + } + err = solution.Write(dir) assert.NoError(t, err) - // Create a fake CLI config. - cliCfg, err := config.NewCLIConfig() + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupSubmitFlags(flags) + flagArgs := []string{ + "--files", + strings.Join(filenames, ","), + } + err = flags.Parse(flagArgs) assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cliCfg := &config.CLIConfig{ + Config: config.New(tmpDir, "cli"), + Tracks: config.Tracks{}, + } cliCfg.Tracks["bogus-track"] = config.NewTrack("bogus-track") err = cliCfg.Write() assert.NoError(t, err) - // Execute the command! - cmdTest.App.Execute() + cfg := config.Configuration{ + Persister: config.InMemoryPersister{}, + Dir: tmpDir, + UserViperConfig: v, + CLIConfig: cliCfg, + } - // We got only the file we expected. - assert.Equal(t, 2, len(submittedFiles)) - for _, file := range []file{file1, file2} { + err = runSubmit(cfg, flags, []string{}) + assert.NoError(t, err) + + // We currently have a bug, and we're not filtering anything. + // Fix that in a separate commit. + assert.Equal(t, 3, len(submittedFiles)) + + for _, file := range []file{file1, file2, file3} { path := string(os.PathSeparator) + file.relativePath assert.Equal(t, file.contents, submittedFiles[path]) } From ebb0a08f7a2976250f0d5b198c1764ed45ca9421 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 11 Jul 2018 11:58:11 +0100 Subject: [PATCH 118/544] Rip out dir-related submit-functionality We're only going to accept explicit file paths for the launch. We will add back dir-related functionality when we've gotten the basics right. --- cmd/submit.go | 99 +++++++-------------------------------------------- 1 file changed, 13 insertions(+), 86 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 02ca63e59..e6b20c2f6 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -11,7 +11,6 @@ import ( "strings" "github.com/exercism/cli/api" - "github.com/exercism/cli/comms" "github.com/exercism/cli/config" "github.com/exercism/cli/workspace" "github.com/spf13/cobra" @@ -67,68 +66,17 @@ figuring things out if necessary. } func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) error { - // Validate input before doing any other work - exercise, err := flags.GetString("exercise") - if err != nil { - return err - } - - trackID, err := flags.GetString("track") - if err != nil { - return err - } - files, err := flags.GetStringSlice("files") if err != nil { return err } - // Verify that both --track and --exercise are used together - if len(args) == 0 && len(files) == 0 && !(exercise != "" && trackID != "") { - // Are they both missing? - if exercise == "" && trackID == "" { - return errors.New("Please use the --exercise/--trackID flags to submit without an explicit directory or files.") - } - // Guess that --trackID is missing, unless it's not - present, missing := "--exercise", "--track" - if trackID != "" { - present, missing = missing, present - } - // Help user correct CLI command - missingFlagMessage := fmt.Sprintf("You specified %s, please also include %s.", present, missing) - return errors.New(missingFlagMessage) - } - - if len(args) > 0 && (exercise != "" || trackID != "") { - return errors.New("You are submitting a directory. We will infer the track and exercise from that. Please re-run the submit command without the flags.") - } - - if len(files) > 0 && len(args) > 0 { - return errors.New("You can submit either a list of files, or a directory, but not both.") - } - usrCfg := cfg.UserViperConfig cliCfg := cfg.CLIConfig - // TODO: make sure we get the workspace configured. - if usrCfg.GetString("workspace") == "" { - cwd, err := os.Getwd() - if err != nil { - return err - } - usrCfg.Set("workspace", filepath.Dir(filepath.Dir(cwd))) - } - ws := workspace.New(usrCfg.GetString("workspace")) - // Create directory from track and exercise slugs if needed - if trackID != "" && exercise != "" { - args = []string{filepath.Join(ws.Dir, trackID, exercise)} - } else if len(files) > 0 { - args = files - } - - tx, err := workspace.NewTransmission(ws.Dir, args) + tx, err := workspace.NewTransmission(ws.Dir, files) if err != nil { return err } @@ -142,46 +90,25 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er if err != nil { return err } - - var solution *workspace.Solution - - selection := comms.NewSelection() - for _, s := range sx { - selection.Items = append(selection.Items, s) + if len(sx) == 0 { + // TODO: add test + return errors.New("can't find a solution metadata file. (todo: explain how to fix it)") } - - for { - prompt := ` - We found more than one. Which one did you mean? - Type the number of the one you want to select. - - %s - > ` - option, err := selection.Pick(prompt) - if err != nil { - return err - } - s, ok := option.(*workspace.Solution) - if !ok { - fmt.Fprintf(Err, "something went wrong trying to pick that solution, not sure what happened") - continue - } - solution = s - break + if len(sx) > 1 { + // TODO: add test + return errors.New("files from multiple solutions. Can only submit one solution at a time. (todo: fix error message)") } + solution := sx[0] if !solution.IsRequester { - return errors.New("not your solution") + // TODO: add test + return errors.New("not your solution. todo: fix error message") } track := cliCfg.Tracks[solution.Track] if track == nil { - err := prepareTrack(solution.Track) - if err != nil { - return err - } - cliCfg.Load(viper.New()) - track = cliCfg.Tracks[solution.Track] + track = config.NewTrack(solution.Track) + track.SetDefaults() } paths := tx.Files @@ -204,7 +131,7 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er writer := multipart.NewWriter(body) if len(paths) == 0 { - return errors.New("no files found to submit") + return errors.New("no files found to submit. TODO: fix error messages") } for _, path := range paths { From ebd79cc2ba06d239f6dd0172521e01c878b2b126 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 11 Jul 2018 12:11:12 +0100 Subject: [PATCH 119/544] Guard against submitting without a token --- cmd/submit.go | 10 +++++++--- cmd/submit_test.go | 13 +++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index e6b20c2f6..4c12fdba7 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -66,14 +66,18 @@ figuring things out if necessary. } func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) error { + usrCfg := cfg.UserViperConfig + cliCfg := cfg.CLIConfig + + if usrCfg.GetString("token") == "" { + return errors.New("TODO: Welcome to Exercism this is how you use this") + } + files, err := flags.GetStringSlice("files") if err != nil { return err } - usrCfg := cfg.UserViperConfig - cliCfg := cfg.CLIConfig - ws := workspace.New(usrCfg.GetString("workspace")) tx, err := workspace.NewTransmission(ws.Dir, files) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 6e50b8b6a..8b5f8c90c 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -16,6 +16,19 @@ import ( "github.com/stretchr/testify/assert" ) +func TestSubmitWithoutToken(t *testing.T) { + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + + cfg := config.Configuration{ + Persister: config.InMemoryPersister{}, + UserViperConfig: viper.New(), + DefaultBaseURL: "http://example.com", + } + + err := runSubmit(cfg, flags, []string{}) + assert.Regexp(t, "Welcome to Exercism", err.Error()) +} + func TestSubmitFiles(t *testing.T) { oldOut := Out oldErr := Err From d337ba73b09b060ad4a4349e65556ef2c8a774ee Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 11 Jul 2018 12:12:20 +0100 Subject: [PATCH 120/544] Guard against submitting without configured workspace --- cmd/submit.go | 4 ++++ cmd/submit_test.go | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/cmd/submit.go b/cmd/submit.go index 4c12fdba7..2b496b61d 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -73,6 +73,10 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er return errors.New("TODO: Welcome to Exercism this is how you use this") } + if usrCfg.GetString("workspace") == "" { + return errors.New("TODO: run configure first") + } + files, err := flags.GetStringSlice("files") if err != nil { return err diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 8b5f8c90c..da1840dbc 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -29,6 +29,22 @@ func TestSubmitWithoutToken(t *testing.T) { assert.Regexp(t, "Welcome to Exercism", err.Error()) } +func TestSubmitWithoutWorkspace(t *testing.T) { + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + + v := viper.New() + v.Set("token", "abc123") + + cfg := config.Configuration{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: "http://example.com", + } + + err := runSubmit(cfg, flags, []string{}) + assert.Regexp(t, "run configure", err.Error()) +} + func TestSubmitFiles(t *testing.T) { oldOut := Out oldErr := Err From 0d62d961de4fce66b787ee80f02b896bb2c03423 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 11 Jul 2018 12:16:45 +0100 Subject: [PATCH 121/544] Update submit command to take files as arguments We decided to restructure the UI for the submit command (again). exercism submit file1 file2 file3 When we re-introduce the ability to submit a directory we will do exercism submit dir Then we check if there are multiple arguments. If there are, then they're submitting files and we fail for all the usual reasons. If there is just one argument, then we check first if it's a file or a directory and deal with it appropriately. But for now: just files. --- cmd/submit.go | 7 +------ cmd/submit_test.go | 9 +-------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 2b496b61d..650f7d71d 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -77,14 +77,9 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er return errors.New("TODO: run configure first") } - files, err := flags.GetStringSlice("files") - if err != nil { - return err - } - ws := workspace.New(usrCfg.GetString("workspace")) - tx, err := workspace.NewTransmission(ws.Dir, files) + tx, err := workspace.NewTransmission(ws.Dir, args) if err != nil { return err } diff --git a/cmd/submit_test.go b/cmd/submit_test.go index da1840dbc..067f098cc 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -6,7 +6,6 @@ import ( "net/http/httptest" "os" "path/filepath" - "strings" "testing" "github.com/exercism/cli/config" @@ -125,12 +124,6 @@ func TestSubmitFiles(t *testing.T) { flags := pflag.NewFlagSet("fake", pflag.PanicOnError) setupSubmitFlags(flags) - flagArgs := []string{ - "--files", - strings.Join(filenames, ","), - } - err = flags.Parse(flagArgs) - assert.NoError(t, err) v := viper.New() v.Set("token", "abc123") @@ -152,7 +145,7 @@ func TestSubmitFiles(t *testing.T) { CLIConfig: cliCfg, } - err = runSubmit(cfg, flags, []string{}) + err = runSubmit(cfg, flags, filenames) assert.NoError(t, err) // We currently have a bug, and we're not filtering anything. From 7f55a6428fefb861f40f871c8148cdf18dedc2d9 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 11 Jul 2018 12:31:09 +0100 Subject: [PATCH 122/544] Do not try to submit non-existent files --- cmd/submit.go | 9 +++++++++ cmd/submit_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/cmd/submit.go b/cmd/submit.go index 650f7d71d..eea86b251 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -77,6 +77,15 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er return errors.New("TODO: run configure first") } + for _, arg := range args { + if _, err := os.Stat(arg); err != nil { + if os.IsNotExist(err) { + return errors.New("TODO: explain that there is no such file") + } + return err + } + } + ws := workspace.New(usrCfg.GetString("workspace")) tx, err := workspace.NewTransmission(ws.Dir, args) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 067f098cc..ecad44b37 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -44,6 +44,32 @@ func TestSubmitWithoutWorkspace(t *testing.T) { assert.Regexp(t, "run configure", err.Error()) } +func TestSubmitNonExistentFile(t *testing.T) { + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + + tmpDir, err := ioutil.TempDir("", "submit-no-such-file") + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + + cfg := config.Configuration{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: "http://example.com", + } + + err = ioutil.WriteFile(filepath.Join(tmpDir, "file-1.txt"), []byte("This is file 1"), os.FileMode(0755)) + assert.NoError(t, err) + + err = ioutil.WriteFile(filepath.Join(tmpDir, "file-2.txt"), []byte("This is file 2"), os.FileMode(0755)) + assert.NoError(t, err) + + err = runSubmit(cfg, flags, []string{filepath.Join(tmpDir, "file-1.txt"), "no-such-file.txt", filepath.Join(tmpDir, "file-2.txt")}) + assert.Regexp(t, "no such file", err.Error()) +} + func TestSubmitFiles(t *testing.T) { oldOut := Out oldErr := Err From a8e2afc510c5e03e974b34dc19fb4e15381b3ba5 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 11 Jul 2018 13:01:53 +0100 Subject: [PATCH 123/544] Guard against submitting directories For the moment we're simplifying, and only handling explicit lists of files. Once we've got the basic user experience worked out, we'll expand to accept directories. That said, we'll never accept a combination of files and directories, so this test will still mostly be relevant (with minor tweaks). --- cmd/submit.go | 6 +++++- cmd/submit_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/cmd/submit.go b/cmd/submit.go index eea86b251..f33a3542e 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -78,12 +78,16 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er } for _, arg := range args { - if _, err := os.Stat(arg); err != nil { + info, err := os.Lstat(arg) + if err != nil { if os.IsNotExist(err) { return errors.New("TODO: explain that there is no such file") } return err } + if info.IsDir() { + return errors.New("TODO: it is a directory and we cannot handle that") + } } ws := workspace.New(usrCfg.GetString("workspace")) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index ecad44b37..2c68f5f88 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -70,6 +70,32 @@ func TestSubmitNonExistentFile(t *testing.T) { assert.Regexp(t, "no such file", err.Error()) } +func TestSubmitFilesAndDir(t *testing.T) { + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + + tmpDir, err := ioutil.TempDir("", "submit-no-such-file") + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + + cfg := config.Configuration{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: "http://example.com", + } + + err = ioutil.WriteFile(filepath.Join(tmpDir, "file-1.txt"), []byte("This is file 1"), os.FileMode(0755)) + assert.NoError(t, err) + + err = ioutil.WriteFile(filepath.Join(tmpDir, "file-2.txt"), []byte("This is file 2"), os.FileMode(0755)) + assert.NoError(t, err) + + err = runSubmit(cfg, flags, []string{filepath.Join(tmpDir, "file-1.txt"), tmpDir, filepath.Join(tmpDir, "file-2.txt")}) + assert.Regexp(t, "is a directory", err.Error()) +} + func TestSubmitFiles(t *testing.T) { oldOut := Out oldErr := Err From 9d609ea064f7babe9af8848d4d42f82158a206bd Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 11 Jul 2018 13:42:31 +0100 Subject: [PATCH 124/544] Extract submit test server The fake backend for the submit is not specific to a given test, it just needs to accept files. --- cmd/submit_test.go | 50 ++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 2c68f5f88..a6eb10fc0 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -107,30 +107,7 @@ func TestSubmitFiles(t *testing.T) { }() // The fake endpoint will populate this when it receives the call from the command. submittedFiles := map[string]string{} - - // Set up the test server. - fakeEndpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - err := r.ParseMultipartForm(2 << 10) - if err != nil { - t.Fatal(err) - } - mf := r.MultipartForm - - files := mf.File["files[]"] - for _, fileHeader := range files { - file, err := fileHeader.Open() - if err != nil { - t.Fatal(err) - } - defer file.Close() - body, err := ioutil.ReadAll(file) - if err != nil { - t.Fatal(err) - } - submittedFiles[fileHeader.Filename] = string(body) - } - }) - ts := httptest.NewServer(fakeEndpoint) + ts := fakeSubmitServer(t, submittedFiles) defer ts.Close() tmpDir, err := ioutil.TempDir("", "submit-files") @@ -209,3 +186,28 @@ func TestSubmitFiles(t *testing.T) { assert.Equal(t, file.contents, submittedFiles[path]) } } + +func fakeSubmitServer(t *testing.T, submittedFiles map[string]string) *httptest.Server { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := r.ParseMultipartForm(2 << 10) + if err != nil { + t.Fatal(err) + } + mf := r.MultipartForm + + files := mf.File["files[]"] + for _, fileHeader := range files { + file, err := fileHeader.Open() + if err != nil { + t.Fatal(err) + } + defer file.Close() + body, err := ioutil.ReadAll(file) + if err != nil { + t.Fatal(err) + } + submittedFiles[fileHeader.Filename] = string(body) + } + }) + return httptest.NewServer(handler) +} From aec49274b696ab45c8eae61c55e87361ff6716c5 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 11 Jul 2018 14:04:13 +0100 Subject: [PATCH 125/544] Extract fake solution helper in submit command test This will make it easier to set up more tests for the command. --- cmd/submit_test.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index a6eb10fc0..d4de03baa 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -142,14 +142,7 @@ func TestSubmitFiles(t *testing.T) { assert.NoError(t, err) } - solution := &workspace.Solution{ - ID: "bogus-solution-uuid", - Track: "bogus-track", - Exercise: "bogus-exercise", - IsRequester: true, - } - err = solution.Write(dir) - assert.NoError(t, err) + writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") flags := pflag.NewFlagSet("fake", pflag.PanicOnError) setupSubmitFlags(flags) @@ -211,3 +204,14 @@ func fakeSubmitServer(t *testing.T, submittedFiles map[string]string) *httptest. }) return httptest.NewServer(handler) } + +func writeFakeSolution(t *testing.T, dir, trackID, exerciseSlug string) { + solution := &workspace.Solution{ + ID: "bogus-solution-uuid", + Track: trackID, + Exercise: exerciseSlug, + IsRequester: true, + } + err := solution.Write(dir) + assert.NoError(t, err) +} From dfcb9565e289bf412d8e45ca93f96d662429d708 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 11 Jul 2018 15:31:11 +0100 Subject: [PATCH 126/544] Handle submitting relative paths to files To test relative paths we change directory into the temporary directory and then pass the relative directory as an argument. On the Mac, temporary directories are created within /var which is a symlinked directory under /private: $ ls -l /var lrwxr-xr-x@ 1 root wheel 11 Dec 17 2017 /var -> private/var Temporary directories are returned using the path within the symlinked path (/var/folders/...) rather than the source path (/private/var/folders/...). To test the relative path change directory into the temporary directory, then pass the relative path as an argument, which then gets resolved into an absolute path. Before submitting, we make sure that the file is within the configured workspace. However, this now causes a mismatch, because the workspace path is the one with the symlink, whereas the resolved absolute path in this case is the source path. To fix this, we evaluate the symlinks in the workspace path and resolve these before comparing them to the submitted files. The Windows build fails with this test, so I've separated the relevant tests out into a conditional build. Once we get it fixed it may make more sense to inline it into the main submit test and do a little bit of conditional setup. --- cmd/download.go | 16 +++-- cmd/open.go | 5 +- cmd/submit.go | 13 +++- cmd/submit_relative_path_test.go | 68 +++++++++++++++++++ cmd/submit_relative_path_windows_test.go | 68 +++++++++++++++++++ cmd/submit_test.go | 1 + workspace/transmission.go | 5 +- workspace/transmission_test.go | 2 + workspace/transmission_windows_test.go | 83 ++++++++++++++++++++++++ workspace/workspace.go | 12 +++- workspace/workspace_locate_test.go | 6 +- workspace/workspace_symlinks_test.go | 8 ++- workspace/workspace_test.go | 11 +++- 13 files changed, 281 insertions(+), 17 deletions(-) create mode 100644 cmd/submit_relative_path_test.go create mode 100644 cmd/submit_relative_path_windows_test.go create mode 100644 workspace/transmission_windows_test.go diff --git a/cmd/download.go b/cmd/download.go index 735f438bd..cd76805c7 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -123,15 +123,23 @@ Download other people's solutions by providing the UUID. IsRequester: payload.Solution.User.IsRequester, } + dir := filepath.Join(usrCfg.Workspace, solution.Track) + os.MkdirAll(dir, os.FileMode(0755)) + var ws workspace.Workspace if solution.IsRequester { - ws = workspace.New(filepath.Join(usrCfg.Workspace, solution.Track)) + ws, err = workspace.New(dir) + if err != nil { + return err + } } else { - ws = workspace.New(filepath.Join(usrCfg.Workspace, "users", solution.Handle, solution.Track)) + ws, err = workspace.New(filepath.Join(usrCfg.Workspace, "users", solution.Handle, solution.Track)) + if err != nil { + return err + } } - os.MkdirAll(ws.Dir, os.FileMode(0755)) - dir, err := ws.SolutionPath(solution.Exercise, solution.ID) + dir, err = ws.SolutionPath(solution.Exercise, solution.ID) if err != nil { return err } diff --git a/cmd/open.go b/cmd/open.go index 2de0dcec6..e3a432720 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -27,7 +27,10 @@ the solution you want to see on the website. if err != nil { return err } - ws := workspace.New(cfg.Workspace) + ws, err := workspace.New(cfg.Workspace) + if err != nil { + return err + } paths, err := ws.Locate(args[0]) if err != nil { diff --git a/cmd/submit.go b/cmd/submit.go index f33a3542e..2b7b248e3 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -77,7 +77,7 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er return errors.New("TODO: run configure first") } - for _, arg := range args { + for i, arg := range args { info, err := os.Lstat(arg) if err != nil { if os.IsNotExist(err) { @@ -88,9 +88,18 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er if info.IsDir() { return errors.New("TODO: it is a directory and we cannot handle that") } + + src, err := filepath.EvalSymlinks(arg) + if err != nil { + return err + } + args[i] = src } - ws := workspace.New(usrCfg.GetString("workspace")) + ws, err := workspace.New(usrCfg.GetString("workspace")) + if err != nil { + return err + } tx, err := workspace.NewTransmission(ws.Dir, args) if err != nil { diff --git a/cmd/submit_relative_path_test.go b/cmd/submit_relative_path_test.go new file mode 100644 index 000000000..b0e6816e1 --- /dev/null +++ b/cmd/submit_relative_path_test.go @@ -0,0 +1,68 @@ +// +build !windows + +package cmd + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/exercism/cli/config" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestSubmitRelativePath(t *testing.T) { + oldOut := Out + oldErr := Err + Out = ioutil.Discard + Err = ioutil.Discard + defer func() { + Out = oldOut + Err = oldErr + }() + // The fake endpoint will populate this when it receives the call from the command. + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := ioutil.TempDir("", "relative-path") + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(dir, os.FileMode(0755)) + + writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cliCfg := &config.CLIConfig{ + Config: config.New(tmpDir, "cli"), + Tracks: config.Tracks{}, + } + cliCfg.Tracks["bogus-track"] = config.NewTrack("bogus-track") + err = cliCfg.Write() + assert.NoError(t, err) + + cfg := config.Configuration{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + CLIConfig: cliCfg, + } + + err = ioutil.WriteFile(filepath.Join(dir, "file.txt"), []byte("This is a file."), os.FileMode(0755)) + + err = os.Chdir(dir) + assert.NoError(t, err) + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{"file.txt"}) + assert.NoError(t, err) + + assert.Equal(t, 1, len(submittedFiles)) + assert.Equal(t, "This is a file.", submittedFiles["/file.txt"]) +} diff --git a/cmd/submit_relative_path_windows_test.go b/cmd/submit_relative_path_windows_test.go new file mode 100644 index 000000000..889682591 --- /dev/null +++ b/cmd/submit_relative_path_windows_test.go @@ -0,0 +1,68 @@ +package cmd + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/exercism/cli/config" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestSubmitRelativePath(t *testing.T) { + t.Skip("The Windows build is failing and needs to be debugged.\nSee https://ci.appveyor.com/project/kytrinyx/cli/build/110") + + oldOut := Out + oldErr := Err + Out = ioutil.Discard + Err = ioutil.Discard + defer func() { + Out = oldOut + Err = oldErr + }() + // The fake endpoint will populate this when it receives the call from the command. + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := ioutil.TempDir("", "relative-path") + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(dir, os.FileMode(0755)) + + writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cliCfg := &config.CLIConfig{ + Config: config.New(tmpDir, "cli"), + Tracks: config.Tracks{}, + } + cliCfg.Tracks["bogus-track"] = config.NewTrack("bogus-track") + err = cliCfg.Write() + assert.NoError(t, err) + + cfg := config.Configuration{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + CLIConfig: cliCfg, + } + + err = ioutil.WriteFile(filepath.Join(dir, "file.txt"), []byte("This is a file."), os.FileMode(0755)) + + err = os.Chdir(dir) + assert.NoError(t, err) + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{"file.txt"}) + assert.NoError(t, err) + + assert.Equal(t, 1, len(submittedFiles)) + assert.Equal(t, "This is a file.", submittedFiles["/file.txt"]) +} diff --git a/cmd/submit_test.go b/cmd/submit_test.go index d4de03baa..b3fca1a6c 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -210,6 +210,7 @@ func writeFakeSolution(t *testing.T, dir, trackID, exerciseSlug string) { ID: "bogus-solution-uuid", Track: trackID, Exercise: exerciseSlug, + URL: "http://example.com/bogus-url", IsRequester: true, } err := solution.Write(dir) diff --git a/workspace/transmission.go b/workspace/transmission.go index 0f1f63e65..7ea77faa4 100644 --- a/workspace/transmission.go +++ b/workspace/transmission.go @@ -38,7 +38,10 @@ func NewTransmission(root string, args []string) (*Transmission, error) { return nil, errors.New("mixing files and dirs") } if len(tx.Files) > 0 { - ws := New(root) + ws, err := New(root) + if err != nil { + return nil, err + } parents := map[string]bool{} for _, file := range tx.Files { dir, err := ws.SolutionDir(file) diff --git a/workspace/transmission_test.go b/workspace/transmission_test.go index 2b874e9ad..f379e6ffe 100644 --- a/workspace/transmission_test.go +++ b/workspace/transmission_test.go @@ -1,3 +1,5 @@ +// +build !windows + package workspace import ( diff --git a/workspace/transmission_windows_test.go b/workspace/transmission_windows_test.go new file mode 100644 index 000000000..91dabc8ca --- /dev/null +++ b/workspace/transmission_windows_test.go @@ -0,0 +1,83 @@ +package workspace + +import ( + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewTransmission(t *testing.T) { + t.Skip("This panics on Windows. Once debugged, this can likely be inlined back into the main transmission test.") + + _, cwd, _, _ := runtime.Caller(0) + root := filepath.Join(cwd, "..", "..", "fixtures", "transmission") + dirBird := filepath.Join(root, "creatures", "hummingbird") + dirFeeder := filepath.Join(dirBird, "feeder") + fileBird := filepath.Join(dirBird, "hummingbird.txt") + fileSugar := filepath.Join(dirFeeder, "sugar.txt") + + testCases := []struct { + desc string + args []string + ok bool + tx *Transmission + }{ + { + desc: "more than one dir", + args: []string{dirBird, dirFeeder}, + ok: false, + }, + { + desc: "a file and a dir", + args: []string{dirBird, fileBird}, + ok: false, + }, + { + desc: "just one file", + args: []string{fileBird}, + ok: true, + tx: &Transmission{Files: []string{fileBird}, Dir: dirBird}, + }, + { + desc: "multiple files", + args: []string{fileBird, fileSugar}, + ok: true, + tx: &Transmission{Files: []string{fileBird, fileSugar}, Dir: dirBird}, + }, + { + desc: "one dir", + args: []string{dirBird}, + ok: true, + tx: &Transmission{Files: nil, Dir: dirBird}, + }, + { + desc: "multiple exercise names", + args: []string{"hummingbird", "bear"}, + ok: false, + }, + { + desc: "one exercise name", + args: []string{"hummingbird"}, + ok: true, + tx: &Transmission{Files: nil, Dir: "hummingbird"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + tx, err := NewTransmission(root, tc.args) + if tc.ok { + assert.NoError(t, err, tc.desc) + } else { + assert.Error(t, err, tc.desc) + } + + if tc.tx != nil { + assert.Equal(t, tc.tx.Files, tx.Files) + assert.Equal(t, tc.tx.Dir, tx.Dir) + } + }) + } +} diff --git a/workspace/workspace.go b/workspace/workspace.go index 9430d3dae..628cee01a 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -19,8 +19,16 @@ type Workspace struct { } // New returns a configured workspace. -func New(dir string) Workspace { - return Workspace{Dir: dir} +func New(dir string) (Workspace, error) { + _, err := os.Lstat(dir) + if err != nil { + return Workspace{}, err + } + dir, err = filepath.EvalSymlinks(dir) + if err != nil { + return Workspace{}, err + } + return Workspace{Dir: dir}, nil } // Locate the matching directories within the workspace. diff --git a/workspace/workspace_locate_test.go b/workspace/workspace_locate_test.go index 5affc3e94..837657b97 100644 --- a/workspace/workspace_locate_test.go +++ b/workspace/workspace_locate_test.go @@ -16,7 +16,8 @@ func TestLocateErrors(t *testing.T) { _, cwd, _, _ := runtime.Caller(0) root := filepath.Join(cwd, "..", "..", "fixtures", "locate-exercise") - ws := New(filepath.Join(root, "workspace")) + ws, err := New(filepath.Join(root, "workspace")) + assert.NoError(t, err) testCases := []struct { desc, arg string @@ -68,7 +69,8 @@ func TestLocate(t *testing.T) { _, cwd, _, _ := runtime.Caller(0) root := filepath.Join(cwd, "..", "..", "fixtures", "locate-exercise") - wsPrimary := New(filepath.Join(root, "workspace")) + wsPrimary, err := New(filepath.Join(root, "workspace")) + assert.NoError(t, err) testCases := []locateTestCase{ { diff --git a/workspace/workspace_symlinks_test.go b/workspace/workspace_symlinks_test.go index 370106133..83718369c 100644 --- a/workspace/workspace_symlinks_test.go +++ b/workspace/workspace_symlinks_test.go @@ -6,14 +6,18 @@ import ( "path/filepath" "runtime" "testing" + + "github.com/stretchr/testify/assert" ) func TestLocateSymlinks(t *testing.T) { _, cwd, _, _ := runtime.Caller(0) root := filepath.Join(cwd, "..", "..", "fixtures", "locate-exercise") - wsSymbolic := New(filepath.Join(root, "symlinked-workspace")) - wsPrimary := New(filepath.Join(root, "workspace")) + wsSymbolic, err := New(filepath.Join(root, "symlinked-workspace")) + assert.NoError(t, err) + wsPrimary, err := New(filepath.Join(root, "workspace")) + assert.NoError(t, err) testCases := []locateTestCase{ { diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go index 3c733663b..f4b5b2971 100644 --- a/workspace/workspace_test.go +++ b/workspace/workspace_test.go @@ -1,6 +1,7 @@ package workspace import ( + "io/ioutil" "path/filepath" "runtime" "testing" @@ -10,7 +11,8 @@ import ( func TestSolutionPath(t *testing.T) { root := filepath.Join("..", "fixtures", "solution-path", "creatures") - ws := New(root) + ws, err := New(root) + assert.NoError(t, err) // An existing exercise. path, err := ws.SolutionPath("gazelle", "ccc") @@ -48,7 +50,9 @@ func TestIsSolutionPath(t *testing.T) { } func TestResolveSolutionPath(t *testing.T) { - ws := New("tmp") + tmpDir, err := ioutil.TempDir("", "resolve-solution-path") + ws, err := New(tmpDir) + assert.NoError(t, err) existsFn := func(solutionID, path string) (bool, error) { pathToSolutionID := map[string]string{ @@ -138,7 +142,8 @@ func TestSolutionDir(t *testing.T) { _, cwd, _, _ := runtime.Caller(0) root := filepath.Join(cwd, "..", "..", "fixtures", "solution-dir") - ws := New(filepath.Join(root, "workspace")) + ws, err := New(filepath.Join(root, "workspace")) + assert.NoError(t, err) tests := []struct { path string From 24ac47bb5d52bbc6cfb05b6f85ca66ec7bdc095f Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 11 Jul 2018 19:52:40 +0100 Subject: [PATCH 127/544] Remove incorrect comment We've been back and forth on what to filter out and when. Recently we decided that if you explicitly pass files, then we will submit them, regardless of filtering rules. --- cmd/submit_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index b3fca1a6c..e55da3464 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -129,6 +129,7 @@ func TestSubmitFiles(t *testing.T) { relativePath: filepath.Join("subdir", "file-2.txt"), contents: "This is file 2.", } + // We don't filter *.md files if you explicitly pass the file path. file3 := file{ relativePath: "README.md", contents: "The readme.", @@ -170,8 +171,6 @@ func TestSubmitFiles(t *testing.T) { err = runSubmit(cfg, flags, filenames) assert.NoError(t, err) - // We currently have a bug, and we're not filtering anything. - // Fix that in a separate commit. assert.Equal(t, 3, len(submittedFiles)) for _, file := range []file{file1, file2, file3} { From 865be262666b010de4e4717bbb35701a6c1242d4 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 11 Jul 2018 20:28:11 +0100 Subject: [PATCH 128/544] Handle submitting files in a symlinked path --- cmd/submit_symlink_test.go | 73 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 cmd/submit_symlink_test.go diff --git a/cmd/submit_symlink_test.go b/cmd/submit_symlink_test.go new file mode 100644 index 000000000..a43a4305a --- /dev/null +++ b/cmd/submit_symlink_test.go @@ -0,0 +1,73 @@ +// +build !windows + +package cmd + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/exercism/cli/config" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestSubmitFilesInSymlinkedPath(t *testing.T) { + oldOut := Out + oldErr := Err + Out = ioutil.Discard + Err = ioutil.Discard + defer func() { + Out = oldOut + Err = oldErr + }() + + // The fake endpoint will populate this when it receives the call from the command. + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := ioutil.TempDir("", "symlink-destination") + assert.NoError(t, err) + dstDir := filepath.Join(tmpDir, "workspace") + + srcDir, err := ioutil.TempDir("", "symlink-source") + assert.NoError(t, err) + + err = os.Symlink(srcDir, dstDir) + assert.NoError(t, err) + + dir := filepath.Join(dstDir, "bogus-track", "bogus-exercise") + os.MkdirAll(dir, os.FileMode(0755)) + + cliCfg := &config.CLIConfig{ + Config: config.New(tmpDir, "cli"), + Tracks: config.Tracks{}, + } + cliCfg.Tracks["bogus-track"] = config.NewTrack("bogus-track") + + writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", dstDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Configuration{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + CLIConfig: cliCfg, + } + + file := filepath.Join(dir, "file.txt") + err = ioutil.WriteFile(filepath.Join(dir, "file.txt"), []byte("This is a file."), os.FileMode(0755)) + assert.NoError(t, err) + + err = runSubmit(cfg, pflag.NewFlagSet("symlinks", pflag.PanicOnError), []string{file}) + assert.NoError(t, err) + + assert.Equal(t, 1, len(submittedFiles)) + assert.Equal(t, "This is a file.", submittedFiles["/file.txt"]) +} From 195130e090258a9eeee6d275b6fa92c6d746319c Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 11 Jul 2018 20:43:52 +0100 Subject: [PATCH 129/544] Add test to demonstrate submitting incompatible files You can submit an explicit list of files, but they must all belong to the same solution. --- cmd/submit.go | 1 - cmd/submit_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/cmd/submit.go b/cmd/submit.go index 2b7b248e3..bef63fb5d 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -120,7 +120,6 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er return errors.New("can't find a solution metadata file. (todo: explain how to fix it)") } if len(sx) > 1 { - // TODO: add test return errors.New("files from multiple solutions. Can only submit one solution at a time. (todo: fix error message)") } solution := sx[0] diff --git a/cmd/submit_test.go b/cmd/submit_test.go index e55da3464..9abc610da 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -179,6 +179,50 @@ func TestSubmitFiles(t *testing.T) { } } +func TestSubmitFilesFromDifferentSolutions(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "dir-1-submit") + assert.NoError(t, err) + + dir1 := filepath.Join(tmpDir, "bogus-track", "bogus-exercise-1") + os.MkdirAll(dir1, os.FileMode(0755)) + writeFakeSolution(t, dir1, "bogus-track", "bogus-exercise-1") + + dir2 := filepath.Join(tmpDir, "bogus-track", "bogus-exercise-2") + os.MkdirAll(dir2, os.FileMode(0755)) + writeFakeSolution(t, dir2, "bogus-track", "bogus-exercise-2") + + file1 := filepath.Join(dir1, "file-1.txt") + err = ioutil.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) + assert.NoError(t, err) + + file2 := filepath.Join(dir2, "file-2.txt") + err = ioutil.WriteFile(file2, []byte("This is file 2."), os.FileMode(0755)) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + + cliCfg := &config.CLIConfig{ + Config: config.New(tmpDir, "cli"), + Tracks: config.Tracks{}, + } + cliCfg.Tracks["bogus-track"] = config.NewTrack("bogus-track") + err = cliCfg.Write() + assert.NoError(t, err) + + cfg := config.Configuration{ + Persister: config.InMemoryPersister{}, + Dir: tmpDir, + UserViperConfig: v, + CLIConfig: cliCfg, + } + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file1, file2}) + assert.Error(t, err) + assert.Regexp(t, "more than one solution", err.Error()) +} + func fakeSubmitServer(t *testing.T, submittedFiles map[string]string) *httptest.Server { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := r.ParseMultipartForm(2 << 10) From 3458de3c4cbad85f3c2bfef4d0ff475532fd5ccf Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 11 Jul 2018 20:57:59 +0100 Subject: [PATCH 130/544] Inline flagsets in submit tests --- cmd/submit_test.go | 80 +++++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 48 deletions(-) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 9abc610da..b10e80a48 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -16,21 +16,17 @@ import ( ) func TestSubmitWithoutToken(t *testing.T) { - flags := pflag.NewFlagSet("fake", pflag.PanicOnError) - cfg := config.Configuration{ Persister: config.InMemoryPersister{}, UserViperConfig: viper.New(), DefaultBaseURL: "http://example.com", } - err := runSubmit(cfg, flags, []string{}) + err := runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) assert.Regexp(t, "Welcome to Exercism", err.Error()) } func TestSubmitWithoutWorkspace(t *testing.T) { - flags := pflag.NewFlagSet("fake", pflag.PanicOnError) - v := viper.New() v.Set("token", "abc123") @@ -40,13 +36,11 @@ func TestSubmitWithoutWorkspace(t *testing.T) { DefaultBaseURL: "http://example.com", } - err := runSubmit(cfg, flags, []string{}) + err := runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) assert.Regexp(t, "run configure", err.Error()) } func TestSubmitNonExistentFile(t *testing.T) { - flags := pflag.NewFlagSet("fake", pflag.PanicOnError) - tmpDir, err := ioutil.TempDir("", "submit-no-such-file") assert.NoError(t, err) @@ -65,14 +59,16 @@ func TestSubmitNonExistentFile(t *testing.T) { err = ioutil.WriteFile(filepath.Join(tmpDir, "file-2.txt"), []byte("This is file 2"), os.FileMode(0755)) assert.NoError(t, err) - - err = runSubmit(cfg, flags, []string{filepath.Join(tmpDir, "file-1.txt"), "no-such-file.txt", filepath.Join(tmpDir, "file-2.txt")}) + files := []string{ + filepath.Join(tmpDir, "file-1.txt"), + "no-such-file.txt", + filepath.Join(tmpDir, "file-2.txt"), + } + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) assert.Regexp(t, "no such file", err.Error()) } func TestSubmitFilesAndDir(t *testing.T) { - flags := pflag.NewFlagSet("fake", pflag.PanicOnError) - tmpDir, err := ioutil.TempDir("", "submit-no-such-file") assert.NoError(t, err) @@ -91,8 +87,12 @@ func TestSubmitFilesAndDir(t *testing.T) { err = ioutil.WriteFile(filepath.Join(tmpDir, "file-2.txt"), []byte("This is file 2"), os.FileMode(0755)) assert.NoError(t, err) - - err = runSubmit(cfg, flags, []string{filepath.Join(tmpDir, "file-1.txt"), tmpDir, filepath.Join(tmpDir, "file-2.txt")}) + files := []string{ + filepath.Join(tmpDir, "file-1.txt"), + tmpDir, + filepath.Join(tmpDir, "file-2.txt"), + } + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) assert.Regexp(t, "is a directory", err.Error()) } @@ -115,38 +115,20 @@ func TestSubmitFiles(t *testing.T) { dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) + writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") - type file struct { - relativePath string - contents string - } - - file1 := file{ - relativePath: "file-1.txt", - contents: "This is file 1.", - } - file2 := file{ - relativePath: filepath.Join("subdir", "file-2.txt"), - contents: "This is file 2.", - } - // We don't filter *.md files if you explicitly pass the file path. - file3 := file{ - relativePath: "README.md", - contents: "The readme.", - } - - filenames := make([]string, 0, 3) - for _, file := range []file{file1, file2, file3} { - path := filepath.Join(dir, file.relativePath) - filenames = append(filenames, path) - err := ioutil.WriteFile(path, []byte(file.contents), os.FileMode(0755)) - assert.NoError(t, err) - } + file1 := filepath.Join(dir, "file-1.txt") + err = ioutil.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) + assert.NoError(t, err) - writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") + file2 := filepath.Join(dir, "subdir", "file-2.txt") + err = ioutil.WriteFile(file2, []byte("This is file 2."), os.FileMode(0755)) + assert.NoError(t, err) - flags := pflag.NewFlagSet("fake", pflag.PanicOnError) - setupSubmitFlags(flags) + // We don't filter *.md files if you explicitly pass the file path. + readme := filepath.Join(dir, "README.md") + err = ioutil.WriteFile(readme, []byte("This is the readme."), os.FileMode(0755)) + assert.NoError(t, err) v := viper.New() v.Set("token", "abc123") @@ -168,15 +150,17 @@ func TestSubmitFiles(t *testing.T) { CLIConfig: cliCfg, } - err = runSubmit(cfg, flags, filenames) + files := []string{ + file1, file2, readme, + } + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) assert.NoError(t, err) assert.Equal(t, 3, len(submittedFiles)) - for _, file := range []file{file1, file2, file3} { - path := string(os.PathSeparator) + file.relativePath - assert.Equal(t, file.contents, submittedFiles[path]) - } + assert.Equal(t, "This is file 1.", submittedFiles[string(os.PathSeparator)+"file-1.txt"]) + assert.Equal(t, "This is file 2.", submittedFiles[string(os.PathSeparator)+filepath.Join("subdir", "file-2.txt")]) + assert.Equal(t, "This is the readme.", submittedFiles[string(os.PathSeparator)+"README.md"]) } func TestSubmitFilesFromDifferentSolutions(t *testing.T) { From 3a8ed3149722cdf646e3a28da9d2a804dfc893ce Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 12 Jul 2018 10:52:35 +0100 Subject: [PATCH 131/544] Do not submit empty files If all the files were empty and we're left with no files, don't submit at all. --- cmd/submit.go | 39 +++++++------------ cmd/submit_test.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 25 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index bef63fb5d..bbc5e9a74 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -135,39 +135,28 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er track.SetDefaults() } - paths := tx.Files - if len(paths) == 0 { - walkFn := func(path string, info os.FileInfo, err error) error { - if err != nil || info.IsDir() { - return err - } - ok, err := track.AcceptFilename(path) - if err != nil || !ok { - return err - } - paths = append(paths, path) - return nil + paths := make([]string, 0, len(tx.Files)) + for _, file := range tx.Files { + // Don't submit empty files + info, err := os.Stat(file) + if err != nil { + return err } - filepath.Walk(solution.Dir, walkFn) + if info.Size() == 0 { + fmt.Fprintf(Err, "(TODO) Warning: file %s was empty, skipping...", file) + continue + } + paths = append(paths, file) } - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - if len(paths) == 0 { return errors.New("no files found to submit. TODO: fix error messages") } + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + for _, path := range paths { - // Don't submit empty files - info, err := os.Stat(path) - if err != nil { - return err - } - if info.Size() == 0 { - fmt.Printf("Warning: file %s was empty, skipping...", path) - continue - } file, err := os.Open(path) if err != nil { return err diff --git a/cmd/submit_test.go b/cmd/submit_test.go index b10e80a48..8aa274f83 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -163,6 +163,100 @@ func TestSubmitFiles(t *testing.T) { assert.Equal(t, "This is the readme.", submittedFiles[string(os.PathSeparator)+"README.md"]) } +func TestSubmitWithEmptyFile(t *testing.T) { + oldOut := Out + oldErr := Err + Out = ioutil.Discard + Err = ioutil.Discard + defer func() { + Out = oldOut + Err = oldErr + }() + + // The fake endpoint will populate this when it receives the call from the command. + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := ioutil.TempDir("", "empty-file") + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(dir, os.FileMode(0755)) + + cliCfg := &config.CLIConfig{ + Config: config.New(tmpDir, "cli"), + Tracks: config.Tracks{}, + } + cliCfg.Tracks["bogus-track"] = config.NewTrack("bogus-track") + + writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Configuration{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + CLIConfig: cliCfg, + } + + file1 := filepath.Join(dir, "file-1.txt") + err = ioutil.WriteFile(file1, []byte(""), os.FileMode(0755)) + file2 := filepath.Join(dir, "file-2.txt") + err = ioutil.WriteFile(file2, []byte("This is file 2."), os.FileMode(0755)) + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file1, file2}) + assert.NoError(t, err) + + assert.Equal(t, 1, len(submittedFiles)) + assert.Equal(t, "This is file 2.", submittedFiles[string(os.PathSeparator)+"file-2.txt"]) +} + +func TestSubmitOnlyEmptyFile(t *testing.T) { + oldOut := Out + oldErr := Err + Out = ioutil.Discard + Err = ioutil.Discard + defer func() { + Out = oldOut + Err = oldErr + }() + + tmpDir, err := ioutil.TempDir("", "just-an-empty-file") + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(dir, os.FileMode(0755)) + + cliCfg := &config.CLIConfig{ + Config: config.New(tmpDir, "cli"), + Tracks: config.Tracks{}, + } + cliCfg.Tracks["bogus-track"] = config.NewTrack("bogus-track") + + writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + + cfg := config.Configuration{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + CLIConfig: cliCfg, + } + + file := filepath.Join(dir, "file.txt") + err = ioutil.WriteFile(file, []byte(""), os.FileMode(0755)) + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file}) + assert.Error(t, err) + assert.Regexp(t, "no files", err.Error()) +} + func TestSubmitFilesFromDifferentSolutions(t *testing.T) { tmpDir, err := ioutil.TempDir("", "dir-1-submit") assert.NoError(t, err) From 8dc0034fce4005d818f743d0308584c9fdaafbf3 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 11 Jul 2018 21:23:08 +0100 Subject: [PATCH 132/544] Delete CLIConfig from submit command This is used for filtering, which we don't need for straight-up file submissions. When we re-introduce directories as valid arguments to the submit command we can add it back in. --- cmd/submit.go | 13 ---------- cmd/submit_relative_path_test.go | 9 ------- cmd/submit_relative_path_windows_test.go | 9 ------- cmd/submit_symlink_test.go | 7 ------ cmd/submit_test.go | 32 ------------------------ 5 files changed, 70 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index bbc5e9a74..d1e898b15 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -55,19 +55,12 @@ figuring things out if necessary. // Ignore error. If the file doesn't exist, that is fine. _ = v.ReadInConfig() - cliCfg := config.CLIConfig{Tracks: config.Tracks{}} - if err := v.Unmarshal(&cliCfg); err != nil { - return err - } - cfg.CLIConfig = &cliCfg - return runSubmit(cfg, cmd.Flags(), args) }, } func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) error { usrCfg := cfg.UserViperConfig - cliCfg := cfg.CLIConfig if usrCfg.GetString("token") == "" { return errors.New("TODO: Welcome to Exercism this is how you use this") @@ -129,12 +122,6 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er return errors.New("not your solution. todo: fix error message") } - track := cliCfg.Tracks[solution.Track] - if track == nil { - track = config.NewTrack(solution.Track) - track.SetDefaults() - } - paths := make([]string, 0, len(tx.Files)) for _, file := range tx.Files { // Don't submit empty files diff --git a/cmd/submit_relative_path_test.go b/cmd/submit_relative_path_test.go index b0e6816e1..f8d62cbe8 100644 --- a/cmd/submit_relative_path_test.go +++ b/cmd/submit_relative_path_test.go @@ -41,18 +41,9 @@ func TestSubmitRelativePath(t *testing.T) { v.Set("workspace", tmpDir) v.Set("apibaseurl", ts.URL) - cliCfg := &config.CLIConfig{ - Config: config.New(tmpDir, "cli"), - Tracks: config.Tracks{}, - } - cliCfg.Tracks["bogus-track"] = config.NewTrack("bogus-track") - err = cliCfg.Write() - assert.NoError(t, err) - cfg := config.Configuration{ Persister: config.InMemoryPersister{}, UserViperConfig: v, - CLIConfig: cliCfg, } err = ioutil.WriteFile(filepath.Join(dir, "file.txt"), []byte("This is a file."), os.FileMode(0755)) diff --git a/cmd/submit_relative_path_windows_test.go b/cmd/submit_relative_path_windows_test.go index 889682591..f2512b03a 100644 --- a/cmd/submit_relative_path_windows_test.go +++ b/cmd/submit_relative_path_windows_test.go @@ -41,18 +41,9 @@ func TestSubmitRelativePath(t *testing.T) { v.Set("workspace", tmpDir) v.Set("apibaseurl", ts.URL) - cliCfg := &config.CLIConfig{ - Config: config.New(tmpDir, "cli"), - Tracks: config.Tracks{}, - } - cliCfg.Tracks["bogus-track"] = config.NewTrack("bogus-track") - err = cliCfg.Write() - assert.NoError(t, err) - cfg := config.Configuration{ Persister: config.InMemoryPersister{}, UserViperConfig: v, - CLIConfig: cliCfg, } err = ioutil.WriteFile(filepath.Join(dir, "file.txt"), []byte("This is a file."), os.FileMode(0755)) diff --git a/cmd/submit_symlink_test.go b/cmd/submit_symlink_test.go index a43a4305a..da9fa3090 100644 --- a/cmd/submit_symlink_test.go +++ b/cmd/submit_symlink_test.go @@ -42,12 +42,6 @@ func TestSubmitFilesInSymlinkedPath(t *testing.T) { dir := filepath.Join(dstDir, "bogus-track", "bogus-exercise") os.MkdirAll(dir, os.FileMode(0755)) - cliCfg := &config.CLIConfig{ - Config: config.New(tmpDir, "cli"), - Tracks: config.Tracks{}, - } - cliCfg.Tracks["bogus-track"] = config.NewTrack("bogus-track") - writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") v := viper.New() @@ -58,7 +52,6 @@ func TestSubmitFilesInSymlinkedPath(t *testing.T) { cfg := config.Configuration{ Persister: config.InMemoryPersister{}, UserViperConfig: v, - CLIConfig: cliCfg, } file := filepath.Join(dir, "file.txt") diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 8aa274f83..999876154 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -135,19 +135,10 @@ func TestSubmitFiles(t *testing.T) { v.Set("workspace", tmpDir) v.Set("apibaseurl", ts.URL) - cliCfg := &config.CLIConfig{ - Config: config.New(tmpDir, "cli"), - Tracks: config.Tracks{}, - } - cliCfg.Tracks["bogus-track"] = config.NewTrack("bogus-track") - err = cliCfg.Write() - assert.NoError(t, err) - cfg := config.Configuration{ Persister: config.InMemoryPersister{}, Dir: tmpDir, UserViperConfig: v, - CLIConfig: cliCfg, } files := []string{ @@ -184,12 +175,6 @@ func TestSubmitWithEmptyFile(t *testing.T) { dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") os.MkdirAll(dir, os.FileMode(0755)) - cliCfg := &config.CLIConfig{ - Config: config.New(tmpDir, "cli"), - Tracks: config.Tracks{}, - } - cliCfg.Tracks["bogus-track"] = config.NewTrack("bogus-track") - writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") v := viper.New() @@ -200,7 +185,6 @@ func TestSubmitWithEmptyFile(t *testing.T) { cfg := config.Configuration{ Persister: config.InMemoryPersister{}, UserViperConfig: v, - CLIConfig: cliCfg, } file1 := filepath.Join(dir, "file-1.txt") @@ -231,12 +215,6 @@ func TestSubmitOnlyEmptyFile(t *testing.T) { dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") os.MkdirAll(dir, os.FileMode(0755)) - cliCfg := &config.CLIConfig{ - Config: config.New(tmpDir, "cli"), - Tracks: config.Tracks{}, - } - cliCfg.Tracks["bogus-track"] = config.NewTrack("bogus-track") - writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") v := viper.New() @@ -246,7 +224,6 @@ func TestSubmitOnlyEmptyFile(t *testing.T) { cfg := config.Configuration{ Persister: config.InMemoryPersister{}, UserViperConfig: v, - CLIConfig: cliCfg, } file := filepath.Join(dir, "file.txt") @@ -281,19 +258,10 @@ func TestSubmitFilesFromDifferentSolutions(t *testing.T) { v.Set("token", "abc123") v.Set("workspace", tmpDir) - cliCfg := &config.CLIConfig{ - Config: config.New(tmpDir, "cli"), - Tracks: config.Tracks{}, - } - cliCfg.Tracks["bogus-track"] = config.NewTrack("bogus-track") - err = cliCfg.Write() - assert.NoError(t, err) - cfg := config.Configuration{ Persister: config.InMemoryPersister{}, Dir: tmpDir, UserViperConfig: v, - CLIConfig: cliCfg, } err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file1, file2}) From 3081493c6e90d1334ca42697376c8acecfe54f41 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 12 Jul 2018 12:12:38 +0100 Subject: [PATCH 133/544] Reword error messages in submit command --- cmd/submit.go | 107 ++++++++++++++++++++++++++++++++++++++------- cmd/submit_test.go | 8 ++-- 2 files changed, 94 insertions(+), 21 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index d1e898b15..a108b2884 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -63,23 +63,64 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er usrCfg := cfg.UserViperConfig if usrCfg.GetString("token") == "" { - return errors.New("TODO: Welcome to Exercism this is how you use this") + tokenURL := config.InferSiteURL(usrCfg.GetString("apibaseurl")) + "/my/settings" + msg := ` + + Welcome to Exercism! + + To get started, you need to configure the the tool with your API token. + Find your token at + + %s + + Then run the configure command: + + + %s configure --token=YOUR_TOKEN + + ` + return fmt.Errorf(msg, tokenURL, BinaryName) } if usrCfg.GetString("workspace") == "" { - return errors.New("TODO: run configure first") + // Running configure without any arguments will attempt to + // set the default workspace. If the default workspace directory + // risks clobbering an existing directory, it will print an + // error message that explains how to proceed. + msg := ` + + Please re-run the configure command to define where + to download the exercises. + + %s configure + ` + return fmt.Errorf(msg, BinaryName) } for i, arg := range args { info, err := os.Lstat(arg) if err != nil { if os.IsNotExist(err) { - return errors.New("TODO: explain that there is no such file") + msg := ` + + The file you are trying to submit cannot be found. + + %s + + ` + return fmt.Errorf(msg, arg) } return err } if info.IsDir() { - return errors.New("TODO: it is a directory and we cannot handle that") + msg := ` + + You are submitting a directory, which is not currently supported. + + %s + + ` + return fmt.Errorf(msg, arg) } src, err := filepath.EvalSymlinks(arg) @@ -110,16 +151,36 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er } if len(sx) == 0 { // TODO: add test - return errors.New("can't find a solution metadata file. (todo: explain how to fix it)") + msg := ` + + The exercise you are submitting doesn't have the necessary metadata. + Please see https://exercism.io/cli-v1-to-v2 for instructions on how to fix it. + + ` + return errors.New(msg) } if len(sx) > 1 { - return errors.New("files from multiple solutions. Can only submit one solution at a time. (todo: fix error message)") + msg := ` + + You are submitting files belonging to different solutions. + Please submit the files for one solution at a time. + + ` + return errors.New(msg) } solution := sx[0] if !solution.IsRequester { // TODO: add test - return errors.New("not your solution. todo: fix error message") + msg := ` + + The solution you are submitting is not connected to your account. + Please re-download the exercise to make sure it has the data it needs. + + %s download --exercise=%s --track=%s + + ` + return fmt.Errorf(msg, BinaryName, solution.Exercise, solution.Track) } paths := make([]string, 0, len(tx.Files)) @@ -130,14 +191,26 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er return err } if info.Size() == 0 { - fmt.Fprintf(Err, "(TODO) Warning: file %s was empty, skipping...", file) + + msg := ` + + WARNING: Skipping empty file + %s + + ` + fmt.Fprintf(Err, msg, file) continue } paths = append(paths, file) } if len(paths) == 0 { - return errors.New("no files found to submit. TODO: fix error messages") + msg := ` + + No files found to submit. + + ` + return errors.New(msg) } body := &bytes.Buffer{} @@ -192,17 +265,17 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er return err } - if solution.AutoApprove == true { - msg := `Your solution has been submitted successfully and has been auto-approved. -You can complete the exercise and unlock the next core exercise at: + msg := ` + + Your solution has been submitted successfully. + %s ` - fmt.Fprintf(Err, msg) - } else { - msg := "Your solution has been submitted successfully. View it at:\n" - fmt.Fprintf(Err, msg) + suffix := "View it at:" + if solution.AutoApprove { + suffix = "You can complete the exercise and unlock the next core exercise at:" } + fmt.Fprintf(Err, msg, suffix) fmt.Fprintf(Out, "%s\n", solution.URL) - return nil } diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 999876154..ab35eaef3 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -37,7 +37,7 @@ func TestSubmitWithoutWorkspace(t *testing.T) { } err := runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) - assert.Regexp(t, "run configure", err.Error()) + assert.Regexp(t, "re-run the configure", err.Error()) } func TestSubmitNonExistentFile(t *testing.T) { @@ -65,7 +65,7 @@ func TestSubmitNonExistentFile(t *testing.T) { filepath.Join(tmpDir, "file-2.txt"), } err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) - assert.Regexp(t, "no such file", err.Error()) + assert.Regexp(t, "cannot be found", err.Error()) } func TestSubmitFilesAndDir(t *testing.T) { @@ -93,7 +93,7 @@ func TestSubmitFilesAndDir(t *testing.T) { filepath.Join(tmpDir, "file-2.txt"), } err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) - assert.Regexp(t, "is a directory", err.Error()) + assert.Regexp(t, "submitting a directory", err.Error()) } func TestSubmitFiles(t *testing.T) { @@ -231,7 +231,7 @@ func TestSubmitOnlyEmptyFile(t *testing.T) { err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file}) assert.Error(t, err) - assert.Regexp(t, "no files", err.Error()) + assert.Regexp(t, "No files found", err.Error()) } func TestSubmitFilesFromDifferentSolutions(t *testing.T) { From 05ac7fdff34be0d6341e7e4be23224cb72c1e75d Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 12 Jul 2018 13:10:25 +0100 Subject: [PATCH 134/544] Do not clobber when configuring workspace We already make sure we're not clobbering anything when setting the default workspace, however if you pass the --workspace flag explicitly, we have just been trusting the input. There is one case where we don't want to trust the input, and that is if they downloaded the CLI to the exact same place as the workspace that they pass as a flag. This will typically happen if: 1. They downloaded the CLI to their home directory 2. Extracted the archive right there 3. Followed the default instructions for configuring 4. At this point, the error message will - explain what is wrong, and - show how to use --workspace to explicitly configure it 5. If they then use --workspace with the default it blows up Instead of trying to overwrite the CLI binary with a directory, this adds an extra check to say that if there's a directory at the location, it's fine, but if it's anything else, exit with an error message. --- cmd/configure.go | 24 +++++++++++++++++++++++- cmd/configure_test.go | 43 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 774478638..6e0801625 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -150,6 +150,25 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro workspace = cfg.GetString("workspace") } workspace = config.Resolve(workspace, configuration.Home) + + if workspace != "" { + // If there is a non-directory here, then we cannot proceed. + if info, err := os.Lstat(workspace); !os.IsNotExist(err) && !info.IsDir() { + msg := ` + + There is already something at the workspace location you are configuring: + + %s + + Please rename it, or set a different workspace location: + + %s configure %s --workspace=PATH_TO_DIFFERENT_FOLDER + ` + + return errors.New(fmt.Sprintf(msg, workspace, BinaryName, commandify(flags))) + } + } + if workspace == "" { workspace = config.DefaultWorkspaceDir(configuration) @@ -160,7 +179,10 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro %s - There is already a directory there. + There is already something there. + If it's a directory, that might be fine. If it's a file, you will need to move it first, + or choose a different location for the workspace. + You can choose the workspace location by rerunning this command with the --workspace flag. %s configure %s --workspace=%s diff --git a/cmd/configure_test.go b/cmd/configure_test.go index a029f892d..adf30b705 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "testing" "github.com/exercism/cli/config" @@ -397,7 +398,47 @@ func TestConfigureDefaultWorkspaceWithoutClobbering(t *testing.T) { err = runConfigure(cfg, flags) assert.Error(t, err) - assert.Regexp(t, "already a directory", err.Error()) + assert.Regexp(t, "already something", err.Error()) +} + +func TestConfigureExplicitWorkspaceWithoutClobberingNonDirectory(t *testing.T) { + oldOut := Out + oldErr := Err + Out = ioutil.Discard + Err = ioutil.Discard + defer func() { + Out = oldOut + Err = oldErr + }() + + tmpDir, err := ioutil.TempDir("", "no-clobber") + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + + cfg := config.Configuration{ + OS: "linux", + DefaultDirName: "workspace", + Home: tmpDir, + Dir: tmpDir, + UserViperConfig: v, + Persister: config.InMemoryPersister{}, + } + + // Create a file at the workspace directory's location + err = ioutil.WriteFile(filepath.Join(tmpDir, "workspace"), []byte("This is not a directory"), os.FileMode(0755)) + assert.NoError(t, err) + + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupConfigureFlags(flags) + err = flags.Parse([]string{"--no-verify", "--workspace", config.DefaultWorkspaceDir(cfg)}) + assert.NoError(t, err) + + err = runConfigure(cfg, flags) + if assert.Error(t, err) { + assert.Regexp(t, "set a different workspace", err.Error()) + } } func TestCommandifyFlagSet(t *testing.T) { From 8dae35782eb95b876881674ef108fb2ae6576a80 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Fri, 13 Jul 2018 12:29:20 +0700 Subject: [PATCH 135/544] Remove unreachable code `go vet ./workspace` workspace/workspace.go:199: unreachable code --- workspace/workspace.go | 1 - 1 file changed, 1 deletion(-) diff --git a/workspace/workspace.go b/workspace/workspace.go index 628cee01a..3952c5482 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -196,5 +196,4 @@ func (ws Workspace) SolutionDir(s string) (string, error) { } path = filepath.Dir(path) } - return "", nil } From bd566b5fb96d882dd3f04e48e5da79cae9057f84 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Fri, 13 Jul 2018 15:18:12 +0700 Subject: [PATCH 136/544] Add shell completions and README to build script Completion scripts currently live in the CLI website repository which is deemed inappropriate Update build script to add shell completions and README to tar/zips --- BUILD.md | 30 ++++++++++++++++++++++++++++++ bin/build-all | 17 +++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 BUILD.md diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 000000000..93da1e16d --- /dev/null +++ b/BUILD.md @@ -0,0 +1,30 @@ +## Executable +Unpack the archive relevant to your machine and place in $PATH + +## Shell Completion Scripts + +### Bash + + mkdir -p ~/.config/exercism + mv ./exercism_completion.bash ~/.config/exercism/exercism\exercism_completion.bash + +Load the completion in your `.bashrc`, `.bash_profile` or `.profile` by +adding the following snippet: + + if [ -f ~/.config/exercism/exercism_completion.bash ]; then + source ~/.config/exercism/exercism_completion.bash + fi + +### Zsh + + mkdir -p ~/.config/exercism + mv ./exercism_completion.zsh ~/.config/exercism/exercism_completion.zsh + +Load up the completion in your `.zshrc`, `.zsh_profile` or `.profile` by adding +the following snippet + + if [ -f ~/.config/exercism/exercism_completion.zsh ]; then + source ~/.config/exercism/exercism_completion.zsh + fi + +**Note:** If you are using the popular [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) framework to manage your zsh plugins, you don't need to add the above snippet, all you need to do is create a file `exercism_completion.zsh` inside the `~/.oh-my-zsh/custom`. diff --git a/bin/build-all b/bin/build-all index 4c87436fd..17358f4c2 100755 --- a/bin/build-all +++ b/bin/build-all @@ -13,6 +13,15 @@ ARMVAR=github.com/exercism/cli/cmd.BuildARM # handle alternate binary name for pre-releases BINNAME=${NAME:-exercism} +get_shell_completions() { + SHELLCOMP_BASEURL=http://cli.exercism.io/shell/exercism_completion + curl -O $SHELLCOMP_BASEURL.bash -O $SHELLCOMP_BASEURL.zsh +} + +cleanup() { + rm -f exercism_completion.bash exercism_completion.zsh +} + createRelease() { os=$1 arch=$2 @@ -66,13 +75,15 @@ createRelease() { if [ "$osname" = windows ] then - zip "$relname.zip" "$binname" + zip "$relname.zip" "$binname" ../exercism_completion* ../BUILD.md else - tar cvzf "$relname.tgz" "$binname" + tar cvzf "$relname.tgz" "$binname" ../exercism_completion* ../BUILD.md fi cd .. } +get_shell_completions + # Mac Releases createRelease darwin 386 createRelease darwin amd64 @@ -101,3 +112,5 @@ createRelease linux arm64 # Windows Releases createRelease windows 386 createRelease windows amd64 + +cleanup From 077f4f65b7bb8c2f02122412be3c37ba2f36f6dc Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Fri, 13 Jul 2018 10:52:26 +0100 Subject: [PATCH 137/544] Fix linting errors --- cmd/configure.go | 5 ++--- config/configuration.go | 5 +++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 6e0801625..673db976a 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -1,7 +1,6 @@ package cmd import ( - "errors" "fmt" "os" "strings" @@ -165,7 +164,7 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro %s configure %s --workspace=PATH_TO_DIFFERENT_FOLDER ` - return errors.New(fmt.Sprintf(msg, workspace, BinaryName, commandify(flags))) + return fmt.Errorf(msg, workspace, BinaryName, commandify(flags)) } } @@ -188,7 +187,7 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro %s configure %s --workspace=%s ` - return errors.New(fmt.Sprintf(msg, workspace, BinaryName, commandify(flags), workspace)) + return fmt.Errorf(msg, workspace, BinaryName, commandify(flags), workspace) } } // Configure the workspace. diff --git a/config/configuration.go b/config/configuration.go index 8f3ca3f42..7dffe58ec 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -100,6 +100,10 @@ func userHome() string { return dir } +// DefaultWorkspaceDir provides a sensible default for the Exercism workspace. +// The default is different depending on the platform, in order to best match +// the conventions for that platform. +// It places the directory in the user's home path. func DefaultWorkspaceDir(cfg Configuration) string { dir := cfg.DefaultDirName if cfg.OS != "linux" { @@ -108,6 +112,7 @@ func DefaultWorkspaceDir(cfg Configuration) string { return filepath.Join(cfg.Home, dir) } +// Save persists a viper config of the base name. func (c Configuration) Save(basename string) error { return c.Persister.Save(c.UserViperConfig, basename) } From f6880d9d1f5679da2ab735a8a2d169d9df86453f Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Fri, 13 Jul 2018 17:42:25 +0700 Subject: [PATCH 138/544] Add shell completion scripts directly to repo --- BUILD.md | 4 +- bin/build-all | 17 +------- shell/exercism_completion.bash | 71 ++++++++++++++++++++++++++++++++++ shell/exercism_completion.zsh | 38 ++++++++++++++++++ 4 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 shell/exercism_completion.bash create mode 100644 shell/exercism_completion.zsh diff --git a/BUILD.md b/BUILD.md index 93da1e16d..b5ee140a2 100644 --- a/BUILD.md +++ b/BUILD.md @@ -6,7 +6,7 @@ Unpack the archive relevant to your machine and place in $PATH ### Bash mkdir -p ~/.config/exercism - mv ./exercism_completion.bash ~/.config/exercism/exercism\exercism_completion.bash + mv ../shell/exercism_completion.bash ~/.config/exercism/exercism\exercism_completion.bash Load the completion in your `.bashrc`, `.bash_profile` or `.profile` by adding the following snippet: @@ -18,7 +18,7 @@ adding the following snippet: ### Zsh mkdir -p ~/.config/exercism - mv ./exercism_completion.zsh ~/.config/exercism/exercism_completion.zsh + mv ../shell/exercism_completion.zsh ~/.config/exercism/exercism_completion.zsh Load up the completion in your `.zshrc`, `.zsh_profile` or `.profile` by adding the following snippet diff --git a/bin/build-all b/bin/build-all index 17358f4c2..1e8596a34 100755 --- a/bin/build-all +++ b/bin/build-all @@ -13,15 +13,6 @@ ARMVAR=github.com/exercism/cli/cmd.BuildARM # handle alternate binary name for pre-releases BINNAME=${NAME:-exercism} -get_shell_completions() { - SHELLCOMP_BASEURL=http://cli.exercism.io/shell/exercism_completion - curl -O $SHELLCOMP_BASEURL.bash -O $SHELLCOMP_BASEURL.zsh -} - -cleanup() { - rm -f exercism_completion.bash exercism_completion.zsh -} - createRelease() { os=$1 arch=$2 @@ -75,15 +66,13 @@ createRelease() { if [ "$osname" = windows ] then - zip "$relname.zip" "$binname" ../exercism_completion* ../BUILD.md + zip "$relname.zip" "$binname" ../shell ../BUILD.md else - tar cvzf "$relname.tgz" "$binname" ../exercism_completion* ../BUILD.md + tar cvzf "$relname.tgz" "$binname" ../shell ../BUILD.md fi cd .. } -get_shell_completions - # Mac Releases createRelease darwin 386 createRelease darwin amd64 @@ -112,5 +101,3 @@ createRelease linux arm64 # Windows Releases createRelease windows 386 createRelease windows amd64 - -cleanup diff --git a/shell/exercism_completion.bash b/shell/exercism_completion.bash new file mode 100644 index 000000000..aa383d4b8 --- /dev/null +++ b/shell/exercism_completion.bash @@ -0,0 +1,71 @@ +_exercism () { + local cur prev + + COMPREPLY=() # Array variable storing the possible completions. + cur=${COMP_WORDS[COMP_CWORD]} + prev=${COMP_WORDS[COMP_CWORD-1]} + + commands="configure debug download fetch list open + restore skip status submit tracks unsubmit + upgrade help" + tracks="csharp cpp clojure coffeescript lisp crystal + dlang ecmascript elixir elm elisp erlang + fsharp go haskell java javascript kotlin + lfe lua mips ocaml objective-c php + plsql perl5 python racket ruby rust scala + scheme swift typescript bash c ceylon + coldfusion delphi factor groovy haxe + idris julia nim perl6 pony prolog + purescript r sml vbnet powershell" + config_opts="--dir --host --key --api" + submit_opts="--test --comment" + + if [ "${#COMP_WORDS[@]}" -eq 2 ]; then + COMPREPLY=( $( compgen -W "${commands}" "${cur}" ) ) + return 0 + fi + + if [ "${#COMP_WORDS[@]}" -eq 3 ]; then + case "${prev}" in + configure) + COMPREPLY=( $( compgen -W "${config_opts}" -- "${cur}" ) ) + return 0 + ;; + fetch) + COMPREPLY=( $( compgen -W "${tracks}" "${cur}" ) ) + return 0 + ;; + list) + COMPREPLY=( $( compgen -W "${tracks}" "${cur}" ) ) + return 0 + ;; + open) + COMPREPLY=( $( compgen -W "${tracks}" "${cur}" ) ) + return 0 + ;; + skip) + COMPREPLY=( $( compgen -W "${tracks}" "${cur}" ) ) + return 0 + ;; + status) + COMPREPLY=( $( compgen -W "${tracks}" "${cur}" ) ) + return 0 + ;; + submit) + COMPREPLY=( $( compgen -W "${submit_opts}" -- "${cur}" ) ) + return 0 + ;; + help) + COMPREPLY=( $( compgen -W "${commands}" "${cur}" ) ) + return 0 + ;; + *) + return 0 + ;; + esac + fi + + return 0 +} + +complete -F _exercism exercism diff --git a/shell/exercism_completion.zsh b/shell/exercism_completion.zsh new file mode 100644 index 000000000..44a9cc6e2 --- /dev/null +++ b/shell/exercism_completion.zsh @@ -0,0 +1,38 @@ +_exercism() { + local curcontext="$curcontext" state line + typeset -A opt_args + + local -a options + options=(debug:"Outputs useful debug information." + configure:"Writes config values to a JSON file." + demo:"Fetches a demo problem for each language track on exercism.io." + fetch:"Fetches your current problems on exercism.ios well as the next unstarted problem in each language." + restore:"Restores completed and current problems on from exercism.iolong with your most recent iteration for each." + submit:"Submits a new iteration to a problem on exercism.io." + unsubmit:"Deletes the most recently submitted iteration." + tracks:"List the available language tracks" + download:"Downloads and saves a specified submission into the local system" + help:"Shows a list of commands or help for one command") + + _arguments -s -S \ + {-c,--config}"[path to config file]:file:_files" \ + {-d,--debug}"[turn on verbose logging]" \ + {-h,--help}"[show help]" \ + {-v,--version}"[print the version]" \ + '(-): :->command' \ + '(-)*:: :->option-or-argument' \ + && return 0; + + case $state in + (command) + _describe 'commands' options ;; + (option-or-argument) + case $words[1] in + s*) + _files + ;; + esac + esac +} + +compdef '_exercism' exercism From dbd717c5b2049f39d59ea08b4c7b5599df29f5d2 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Fri, 13 Jul 2018 12:40:16 +0100 Subject: [PATCH 139/544] Get rid of transmission type This inlines the little bit of logic that we needed from the workspace.Transition and deletes the type. We may find that we want to re-introduce an abstraction for some of this, but I want to write it all down as simply as possible first and then go from there. --- cmd/submit.go | 31 ++++++-- cmd/submit_test.go | 2 +- workspace/transmission.go | 62 --------------- workspace/transmission_test.go | 100 ------------------------- workspace/transmission_windows_test.go | 83 -------------------- 5 files changed, 26 insertions(+), 252 deletions(-) delete mode 100644 workspace/transmission.go delete mode 100644 workspace/transmission_test.go delete mode 100644 workspace/transmission_windows_test.go diff --git a/cmd/submit.go b/cmd/submit.go index a108b2884..6b851bb7a 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -98,6 +98,12 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er } for i, arg := range args { + var err error + arg, err = filepath.Abs(arg) + if err != nil { + return err + } + info, err := os.Lstat(arg) if err != nil { if os.IsNotExist(err) { @@ -135,12 +141,25 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er return err } - tx, err := workspace.NewTransmission(ws.Dir, args) - if err != nil { - return err + var exerciseDir string + for _, arg := range args { + dir, err := ws.SolutionDir(arg) + if err != nil { + return err + } + if exerciseDir != "" && dir != exerciseDir { + msg := ` + + You are submitting files belonging to different solutions. + Please submit the files for one solution at a time. + + ` + return errors.New(msg) + } + exerciseDir = dir } - dirs, err := ws.Locate(tx.Dir) + dirs, err := ws.Locate(exerciseDir) if err != nil { return err } @@ -183,8 +202,8 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er return fmt.Errorf(msg, BinaryName, solution.Exercise, solution.Track) } - paths := make([]string, 0, len(tx.Files)) - for _, file := range tx.Files { + paths := make([]string, 0, len(args)) + for _, file := range args { // Don't submit empty files info, err := os.Stat(file) if err != nil { diff --git a/cmd/submit_test.go b/cmd/submit_test.go index ab35eaef3..3b9650600 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -266,7 +266,7 @@ func TestSubmitFilesFromDifferentSolutions(t *testing.T) { err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file1, file2}) assert.Error(t, err) - assert.Regexp(t, "more than one solution", err.Error()) + assert.Regexp(t, "different solutions", err.Error()) } func fakeSubmitServer(t *testing.T, submittedFiles map[string]string) *httptest.Server { diff --git a/workspace/transmission.go b/workspace/transmission.go deleted file mode 100644 index 7ea77faa4..000000000 --- a/workspace/transmission.go +++ /dev/null @@ -1,62 +0,0 @@ -package workspace - -import ( - "errors" - "path/filepath" -) - -// Transmission is the data necessary to submit a solution. -type Transmission struct { - Files []string - Dir string - ArgDirs []string -} - -// NewTransmission processes the arguments to the submit command to prep a submission. -func NewTransmission(root string, args []string) (*Transmission, error) { - tx := &Transmission{} - for _, arg := range args { - pt, err := DetectPathType(arg) - if err != nil { - return nil, err - } - if pt == TypeFile { - arg, err = filepath.Abs(arg) - if err != nil { - return nil, err - } - tx.Files = append(tx.Files, arg) - continue - } - // For our purposes, if it's not a file then it's a directory. - tx.ArgDirs = append(tx.ArgDirs, arg) - } - if len(tx.ArgDirs) > 1 { - return nil, errors.New("more than one dir") - } - if len(tx.ArgDirs) > 0 && len(tx.Files) > 0 { - return nil, errors.New("mixing files and dirs") - } - if len(tx.Files) > 0 { - ws, err := New(root) - if err != nil { - return nil, err - } - parents := map[string]bool{} - for _, file := range tx.Files { - dir, err := ws.SolutionDir(file) - if err != nil { - return nil, err - } - parents[dir] = true - tx.Dir = dir - } - if len(parents) > 1 { - return nil, errors.New("files are from more than one solution") - } - } - if len(tx.ArgDirs) == 1 { - tx.Dir = tx.ArgDirs[0] - } - return tx, nil -} diff --git a/workspace/transmission_test.go b/workspace/transmission_test.go deleted file mode 100644 index f379e6ffe..000000000 --- a/workspace/transmission_test.go +++ /dev/null @@ -1,100 +0,0 @@ -// +build !windows - -package workspace - -import ( - "io/ioutil" - "os" - "path/filepath" - "runtime" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNewTransmission(t *testing.T) { - _, cwd, _, _ := runtime.Caller(0) - root := filepath.Join(cwd, "..", "..", "fixtures", "transmission") - dirBird := filepath.Join(root, "creatures", "hummingbird") - dirFeeder := filepath.Join(dirBird, "feeder") - fileBird := filepath.Join(dirBird, "hummingbird.txt") - fileSugar := filepath.Join(dirFeeder, "sugar.txt") - - testCases := []struct { - desc string - args []string - ok bool - tx *Transmission - }{ - { - desc: "more than one dir", - args: []string{dirBird, dirFeeder}, - ok: false, - }, - { - desc: "a file and a dir", - args: []string{dirBird, fileBird}, - ok: false, - }, - { - desc: "just one file", - args: []string{fileBird}, - ok: true, - tx: &Transmission{Files: []string{fileBird}, Dir: dirBird}, - }, - { - desc: "multiple files", - args: []string{fileBird, fileSugar}, - ok: true, - tx: &Transmission{Files: []string{fileBird, fileSugar}, Dir: dirBird}, - }, - { - desc: "one dir", - args: []string{dirBird}, - ok: true, - tx: &Transmission{Files: nil, Dir: dirBird}, - }, - { - desc: "multiple exercise names", - args: []string{"hummingbird", "bear"}, - ok: false, - }, - { - desc: "one exercise name", - args: []string{"hummingbird"}, - ok: true, - tx: &Transmission{Files: nil, Dir: "hummingbird"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - tx, err := NewTransmission(root, tc.args) - if tc.ok { - assert.NoError(t, err, tc.desc) - } else { - assert.Error(t, err, tc.desc) - } - - if tc.tx != nil { - assert.Equal(t, tc.tx.Files, tx.Files) - assert.Equal(t, tc.tx.Dir, tx.Dir) - } - }) - } -} - -func TestTransmissionWithRelativePath(t *testing.T) { - // This is really dirty, but I need to make sure that we turn relative paths into absolute paths. - err := ioutil.WriteFile(".solution.json", []byte("{}"), os.FileMode(0755)) - assert.NoError(t, err) - defer os.Remove(".solution.json") - - _, cwd, _, _ := runtime.Caller(0) - dir := filepath.Dir(filepath.Dir(cwd)) - file := filepath.Base(cwd) - tx, err := NewTransmission(dir, []string{file}) - if assert.NoError(t, err) { - assert.Equal(t, filepath.Clean(cwd), tx.Files[0]) - } -} diff --git a/workspace/transmission_windows_test.go b/workspace/transmission_windows_test.go deleted file mode 100644 index 91dabc8ca..000000000 --- a/workspace/transmission_windows_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package workspace - -import ( - "path/filepath" - "runtime" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNewTransmission(t *testing.T) { - t.Skip("This panics on Windows. Once debugged, this can likely be inlined back into the main transmission test.") - - _, cwd, _, _ := runtime.Caller(0) - root := filepath.Join(cwd, "..", "..", "fixtures", "transmission") - dirBird := filepath.Join(root, "creatures", "hummingbird") - dirFeeder := filepath.Join(dirBird, "feeder") - fileBird := filepath.Join(dirBird, "hummingbird.txt") - fileSugar := filepath.Join(dirFeeder, "sugar.txt") - - testCases := []struct { - desc string - args []string - ok bool - tx *Transmission - }{ - { - desc: "more than one dir", - args: []string{dirBird, dirFeeder}, - ok: false, - }, - { - desc: "a file and a dir", - args: []string{dirBird, fileBird}, - ok: false, - }, - { - desc: "just one file", - args: []string{fileBird}, - ok: true, - tx: &Transmission{Files: []string{fileBird}, Dir: dirBird}, - }, - { - desc: "multiple files", - args: []string{fileBird, fileSugar}, - ok: true, - tx: &Transmission{Files: []string{fileBird, fileSugar}, Dir: dirBird}, - }, - { - desc: "one dir", - args: []string{dirBird}, - ok: true, - tx: &Transmission{Files: nil, Dir: dirBird}, - }, - { - desc: "multiple exercise names", - args: []string{"hummingbird", "bear"}, - ok: false, - }, - { - desc: "one exercise name", - args: []string{"hummingbird"}, - ok: true, - tx: &Transmission{Files: nil, Dir: "hummingbird"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - tx, err := NewTransmission(root, tc.args) - if tc.ok { - assert.NoError(t, err, tc.desc) - } else { - assert.Error(t, err, tc.desc) - } - - if tc.tx != nil { - assert.Equal(t, tc.tx.Files, tx.Files) - assert.Equal(t, tc.tx.Dir, tx.Dir) - } - }) - } -} From 22602f36f31238ab88ce7ca4a9a04dfa897eef5d Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Fri, 13 Jul 2018 19:01:53 +0700 Subject: [PATCH 140/544] Move build readme to shell --- bin/build-all | 4 ++-- BUILD.md => shell/README.md | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename BUILD.md => shell/README.md (100%) diff --git a/bin/build-all b/bin/build-all index 1e8596a34..3a728886c 100755 --- a/bin/build-all +++ b/bin/build-all @@ -66,9 +66,9 @@ createRelease() { if [ "$osname" = windows ] then - zip "$relname.zip" "$binname" ../shell ../BUILD.md + zip "$relname.zip" "$binname" ../shell else - tar cvzf "$relname.tgz" "$binname" ../shell ../BUILD.md + tar cvzf "$relname.tgz" "$binname" ../shell fi cd .. } diff --git a/BUILD.md b/shell/README.md similarity index 100% rename from BUILD.md rename to shell/README.md From fe942cf1d8a2522d58aa77957194522fb788eb28 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Fri, 13 Jul 2018 13:30:39 +0100 Subject: [PATCH 141/544] Set default API base URL to api.exercism.io --- config/user_config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/user_config.go b/config/user_config.go index ef5b9b1a8..cd61633bc 100644 --- a/config/user_config.go +++ b/config/user_config.go @@ -8,7 +8,7 @@ import ( ) var ( - defaultBaseURL = "https://v2.exercism.io/api/v1" + defaultBaseURL = "https://api.exercism.io/v1" ) // UserConfig contains user-specific settings. From 23909a16df29496df43b6a9a55089342a99cffc9 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Fri, 13 Jul 2018 14:48:02 +0100 Subject: [PATCH 142/544] Tweak submission feedback message --- cmd/submit.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 6b851bb7a..f8f1c1730 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -287,14 +287,14 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er msg := ` Your solution has been submitted successfully. - %s + %s ` - suffix := "View it at:" + suffix := "View it at:\n\n " if solution.AutoApprove { - suffix = "You can complete the exercise and unlock the next core exercise at:" + suffix = "You can complete the exercise and unlock the next core exercise at:\n" } fmt.Fprintf(Err, msg, suffix) - fmt.Fprintf(Out, "%s\n", solution.URL) + fmt.Fprintf(Out, " %s\n\n", solution.URL) return nil } From 7e403507273814f5d34628dc6afa483d13f744e9 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Fri, 13 Jul 2018 13:33:40 +0100 Subject: [PATCH 143/544] Bump to v3.0.0 --- cmd/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/version.go b/cmd/version.go index 9a9d47899..4aabc5e37 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.0.0-alpha.4" +const Version = "3.0.0" // checkLatest flag for version command. var checkLatest bool From 86c68d11bbbc363f53b47d19a591d960d895580f Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Fri, 13 Jul 2018 20:54:08 +0700 Subject: [PATCH 144/544] Fix path issues related to tar/zip creation Avoid including 'out' directory in generated archive tar and zip differ in how to achieve this tar -C will cd to dir before adding files. zip cd subshell accomplishes the same thing. --- bin/build-all | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/bin/build-all b/bin/build-all index 3a728886c..36ebe54fb 100755 --- a/bin/build-all +++ b/bin/build-all @@ -52,7 +52,6 @@ createRelease() { binname="$binname.exe" fi - relname="../release/$BINNAME-$osname-$osarch" echo "Creating $os/$arch binary..." if [ "$arm" ] @@ -62,15 +61,12 @@ createRelease() { GOOS=$os GOARCH=$arch go build -ldflags "$ldflags" -o "out/$binname" exercism/main.go fi - cd out - - if [ "$osname" = windows ] - then - zip "$relname.zip" "$binname" ../shell + release_name="release/$BINNAME-$osname-$osarch" + if [ "$osname" = windows ]; then + (cd out && zip "../$release_name.zip" ../shell/* "./$binname") else - tar cvzf "$relname.tgz" "$binname" ../shell + tar cvzf "$release_name.tgz" shell -C out "./$binname" fi - cd .. } # Mac Releases From 413f3de517b9f4e4666f1812c78972170adfc51a Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Fri, 13 Jul 2018 17:57:34 +0100 Subject: [PATCH 145/544] Make the successfully configured message friendlier The configure output looked like it could be an error message. This adds a bit of context to say that the configuration was saved, and clarifies the output to make it easier to understand that it's showing you what the configuration settings that were saved are. --- cmd/configure.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 673db976a..d6dc0e2ae 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -197,6 +197,7 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro if err := configuration.Save("user"); err != nil { return err } + fmt.Fprintln(Err, "\nYou have configured the Exercism command-line client:") printCurrentConfig(configuration) return nil } @@ -208,10 +209,10 @@ func printCurrentConfig(configuration config.Configuration) { v := configuration.UserViperConfig fmt.Fprintln(w, "") - fmt.Fprintln(w, fmt.Sprintf("Config dir:\t%s", configuration.Dir)) - fmt.Fprintln(w, fmt.Sprintf("-t, --token\t%s", v.GetString("token"))) - fmt.Fprintln(w, fmt.Sprintf("-w, --workspace\t%s", v.GetString("workspace"))) - fmt.Fprintln(w, fmt.Sprintf("-a, --api\t%s", v.GetString("apibaseurl"))) + fmt.Fprintln(w, fmt.Sprintf("Config dir:\t\t%s", configuration.Dir)) + fmt.Fprintln(w, fmt.Sprintf("Token:\t(-t, --token)\t%s", v.GetString("token"))) + fmt.Fprintln(w, fmt.Sprintf("Workspace:\t(-w, --workspace)\t%s", v.GetString("workspace"))) + fmt.Fprintln(w, fmt.Sprintf("API Base URL:\t(-a, --api)\t%s", v.GetString("apibaseurl"))) fmt.Fprintln(w, "") } From f7832764e0347f75963419e4960efa0fd9327c5a Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Fri, 13 Jul 2018 18:16:50 +0100 Subject: [PATCH 146/544] Bump to v3.0.1 --- cmd/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/version.go b/cmd/version.go index 4aabc5e37..6331a33ad 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.0.0" +const Version = "3.0.1" // checkLatest flag for version command. var checkLatest bool From 03fc483b7f8262047bfcb7fb594d8b2f61aef04b Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Fri, 13 Jul 2018 22:26:07 +0100 Subject: [PATCH 147/544] Fix multi-file submission bug When I rewrote the tests to go through the runSubmit function so I can inject stuff, I forgot to actually consider what the command setup was configuring. In this case we had left a strict requirement that the submit command only accept one single argument. I've deleted the restriction, and tested manually. I'll add a proper test when things slow down a bit. --- cmd/submit.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/submit.go b/cmd/submit.go index f8f1c1730..3a8d01455 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -36,7 +36,6 @@ If called with the name of an exercise, it will work out which track it is on and submit it. The command will ask for help figuring things out if necessary. `, - Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cfg := config.NewConfiguration() From 60f54e0a12abe4e42d9e169727816a91dc55009e Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Fri, 13 Jul 2018 22:35:21 +0100 Subject: [PATCH 148/544] Bump version to v3.0.2 --- cmd/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/version.go b/cmd/version.go index 6331a33ad..698a8acbb 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.0.1" +const Version = "3.0.2" // checkLatest flag for version command. var checkLatest bool From 2055ee46f4871278bc17b32c45a14834d76ea23d Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Fri, 13 Jul 2018 23:06:48 +0100 Subject: [PATCH 149/544] Fix incorrect long description of submit command The documentation was still talking about all the guessing the command used to do to figure out what to submit. --- cmd/submit.go | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 3a8d01455..3342b10ba 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -25,16 +25,7 @@ var submitCmd = &cobra.Command{ Short: "Submit your solution to an exercise.", Long: `Submit your solution to an Exercism exercise. -The CLI will do its best to figure out what to submit. - -If you call the command without any arguments, it will -submit the exercise contained in the current directory. - -If called with the path to a directory, it will submit it. - -If called with the name of an exercise, it will work out which -track it is on and submit it. The command will ask for help -figuring things out if necessary. + Call the command with the list of files you want to submit. `, RunE: func(cmd *cobra.Command, args []string) error { cfg := config.NewConfiguration() From 72ab762018fecd51e19280e422230cc2c2b750b7 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Fri, 13 Jul 2018 23:20:42 +0100 Subject: [PATCH 150/544] Fix tabs vs spaces in big text-heavy output The output messages are using literal strings, which were automatically being formatted with tabs. Lots of tabs. This caused the entire message to be indented by 125 spaces. Or something. That's too much. --- cmd/configure.go | 28 +++++++++++++++------------- cmd/submit.go | 6 +++--- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index d6dc0e2ae..7840962b2 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -155,14 +155,14 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro if info, err := os.Lstat(workspace); !os.IsNotExist(err) && !info.IsDir() { msg := ` - There is already something at the workspace location you are configuring: + There is already something at the workspace location you are configuring: - %s + %s - Please rename it, or set a different workspace location: + Please rename it, or set a different workspace location: - %s configure %s --workspace=PATH_TO_DIFFERENT_FOLDER - ` + %s configure %s --workspace=PATH_TO_DIFFERENT_FOLDER + ` return fmt.Errorf(msg, workspace, BinaryName, commandify(flags)) } @@ -174,18 +174,20 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro // If it already exists don't clobber it with the default. if _, err := os.Lstat(workspace); !os.IsNotExist(err) { msg := ` - The default Exercism workspace is + The default Exercism workspace is - %s + %s - There is already something there. - If it's a directory, that might be fine. If it's a file, you will need to move it first, - or choose a different location for the workspace. + There is already something there. + If it's a directory, that might be fine. + If it's a file, you will need to move it first, or choose a + different location for the workspace. - You can choose the workspace location by rerunning this command with the --workspace flag. + You can choose the workspace location by rerunning this command + with the --workspace flag. - %s configure %s --workspace=%s - ` + %s configure %s --workspace=%s + ` return fmt.Errorf(msg, workspace, BinaryName, commandify(flags), workspace) } diff --git a/cmd/submit.go b/cmd/submit.go index 3a8d01455..0feed8806 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -77,7 +77,7 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er %s configure --token=YOUR_TOKEN - ` + ` return fmt.Errorf(msg, tokenURL, BinaryName) } @@ -212,7 +212,7 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er msg := ` - WARNING: Skipping empty file + WARNING: Skipping empty file %s ` @@ -225,7 +225,7 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er if len(paths) == 0 { msg := ` - No files found to submit. + No files found to submit. ` return errors.New(msg) From 90c726f20bb925037c334dd90fb2d40c80957972 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 00:30:51 +0100 Subject: [PATCH 151/544] Reorder tests for download command Put the helper methods and fixtures at the bottom of the file. --- cmd/download_test.go | 109 +++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index c3865b937..18e139002 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -13,36 +13,6 @@ import ( "github.com/stretchr/testify/assert" ) -const payloadTemplate = ` -{ - "solution": { - "id": "bogus-id", - "user": { - "handle": "alice", - "is_requester": true - }, - "exercise": { - "id": "bogus-exercise", - "instructions_url": "http://example.com/bogus-exercise", - "auto_approve": false, - "track": { - "id": "bogus-track", - "language": "Bogus Language" - } - }, - "file_download_base_url": "%s", - "files": [ - "%s", - "%s", - "%s" - ], - "iteration": { - "submitted_at": "2017-08-21t10:11:12.130z" - } - } -} -` - func TestDownload(t *testing.T) { oldOut := Out oldErr := Err @@ -104,6 +74,36 @@ func TestDownload(t *testing.T) { assert.True(t, os.IsNotExist(err), "It should not write the file if empty.") } +func TestDownloadArgs(t *testing.T) { + tests := []struct { + args []string + expectedError string + }{ + { + args: []string{"bogus"}, // providing just an exercise slug without the flag + expectedError: "need an --exercise name or a solution --uuid", + }, + { + args: []string{""}, // providing no args + expectedError: "need an --exercise name or a solution --uuid", + }, + } + + for _, test := range tests { + cmdTest := &CommandTest{ + Cmd: downloadCmd, + InitFn: initDownloadCmd, + Args: append([]string{"fakeapp", "download"}, test.args...), + } + cmdTest.Setup(t) + cmdTest.App.SetOutput(ioutil.Discard) + defer cmdTest.Teardown(t) + err := cmdTest.App.Execute() + + assert.EqualError(t, err, test.expectedError) + } +} + func writeFakeUserConfigSettings(tmpDirPath, serverURL string) error { userCfg := config.NewEmptyUserConfig() userCfg.Workspace = tmpDirPath @@ -136,35 +136,34 @@ func makeMockServer() *httptest.Server { }) return server - } -func TestDownloadArgs(t *testing.T) { - tests := []struct { - args []string - expectedError string - }{ - { - args: []string{"bogus"}, // providing just an exercise slug without the flag - expectedError: "need an --exercise name or a solution --uuid", +const payloadTemplate = ` +{ + "solution": { + "id": "bogus-id", + "user": { + "handle": "alice", + "is_requester": true }, - { - args: []string{""}, // providing no args - expectedError: "need an --exercise name or a solution --uuid", + "exercise": { + "id": "bogus-exercise", + "instructions_url": "http://example.com/bogus-exercise", + "auto_approve": false, + "track": { + "id": "bogus-track", + "language": "Bogus Language" + } }, - } - - for _, test := range tests { - cmdTest := &CommandTest{ - Cmd: downloadCmd, - InitFn: initDownloadCmd, - Args: append([]string{"fakeapp", "download"}, test.args...), + "file_download_base_url": "%s", + "files": [ + "%s", + "%s", + "%s" + ], + "iteration": { + "submitted_at": "2017-08-21t10:11:12.130z" } - cmdTest.Setup(t) - cmdTest.App.SetOutput(ioutil.Discard) - defer cmdTest.Teardown(t) - err := cmdTest.App.Execute() - - assert.EqualError(t, err, test.expectedError) } } +` From 463a4addf2ccac53243346dc309c5cd3f1ffc62a Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 10:02:18 +0100 Subject: [PATCH 152/544] Rename test helper for clarity and consistency The various command tests need different types of mock servers. These all live in the same namespace, despite being defined in different files. The download command test had a 'makeMockServer' function, which in the scope of the entire cmd package is not specific enough. This renames it to fakeDownloadServer which follows the pattern used in the submit test (fakeSubmitServer). I prefer that the name reflect _what it returns_ rather than _what you are asking it to do_. So for something that returns a _thing_, I'd rather it be named _thing_ than _make thing_. --- cmd/download_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index 18e139002..be7a380e9 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -31,10 +31,10 @@ func TestDownload(t *testing.T) { cmdTest.Setup(t) defer cmdTest.Teardown(t) - mockServer := makeMockServer() - defer mockServer.Close() + ts := fakeDownloadServer() + defer ts.Close() - err := writeFakeUserConfigSettings(cmdTest.TmpDir, mockServer.URL) + err := writeFakeUserConfigSettings(cmdTest.TmpDir, ts.URL) assert.NoError(t, err) testCases := []struct { @@ -111,7 +111,7 @@ func writeFakeUserConfigSettings(tmpDirPath, serverURL string) error { return userCfg.Write() } -func makeMockServer() *httptest.Server { +func fakeDownloadServer() *httptest.Server { mux := http.NewServeMux() server := httptest.NewServer(mux) From f230330a571d7fcab61ca47087b22a1e1def0e64 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 10:11:12 +0100 Subject: [PATCH 153/544] Do not accept token flag on download command --- cmd/download.go | 11 ----------- cmd/download_test.go | 1 + 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index cd76805c7..f6c1c947c 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -30,16 +30,6 @@ latest solution. Download other people's solutions by providing the UUID. `, RunE: func(cmd *cobra.Command, args []string) error { - token, err := cmd.Flags().GetString("token") - if err != nil { - return err - } - if token != "" { - RootCmd.SetArgs([]string{"configure", "--token", token}) - if err := RootCmd.Execute(); err != nil { - return err - } - } uuid, err := cmd.Flags().GetString("uuid") if err != nil { return err @@ -229,7 +219,6 @@ func initDownloadCmd() { downloadCmd.Flags().StringP("uuid", "u", "", "the solution UUID") downloadCmd.Flags().StringP("track", "t", "", "the track ID") downloadCmd.Flags().StringP("exercise", "e", "", "the exercise slug") - downloadCmd.Flags().StringP("token", "k", "", "authentication token used to connect to the site") } func init() { diff --git a/cmd/download_test.go b/cmd/download_test.go index be7a380e9..7ee5cf940 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -108,6 +108,7 @@ func writeFakeUserConfigSettings(tmpDirPath, serverURL string) error { userCfg := config.NewEmptyUserConfig() userCfg.Workspace = tmpDirPath userCfg.APIBaseURL = serverURL + userCfg.Token = "abc123" return userCfg.Write() } From 7a201d4956490fd55dc01d2e5dc9064fdae348c8 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 10:25:04 +0100 Subject: [PATCH 154/544] Complain if downloading without a configured token We need to know who you are in order to download the correct exercise with all the requisite metadata. Originally the download command allowed you to pass a token on the fly, during download. That added a lot of complexity, so instead we bail with a helpful error message that explains how to configure the client. --- cmd/download.go | 28 ++++++++++++++++++++++++---- cmd/download_test.go | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index f6c1c947c..a4e55b43b 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -30,6 +30,30 @@ latest solution. Download other people's solutions by providing the UUID. `, RunE: func(cmd *cobra.Command, args []string) error { + usrCfg, err := config.NewUserConfig() + if err != nil { + return err + } + if usrCfg.Token == "" { + tokenURL := config.InferSiteURL(usrCfg.APIBaseURL) + "/my/settings" + msg := ` + + Welcome to Exercism! + + To get started, you need to configure the the tool with your API token. + Find your token at + + %s + + Then run the configure command: + + + %s configure --token=YOUR_TOKEN + + ` + return fmt.Errorf(msg, tokenURL, BinaryName) + } + uuid, err := cmd.Flags().GetString("uuid") if err != nil { return err @@ -41,10 +65,6 @@ Download other people's solutions by providing the UUID. if uuid == "" && exercise == "" { return errors.New("need an --exercise name or a solution --uuid") } - usrCfg, err := config.NewUserConfig() - if err != nil { - return err - } var slug string if uuid == "" { diff --git a/cmd/download_test.go b/cmd/download_test.go index 7ee5cf940..ebb8f8179 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -13,6 +13,39 @@ import ( "github.com/stretchr/testify/assert" ) +func TestDownloadWithoutToken(t *testing.T) { + oldOut := Out + oldErr := Err + Out = ioutil.Discard + Err = ioutil.Discard + defer func() { + Out = oldOut + Err = oldErr + }() + + cmdTest := &CommandTest{ + Cmd: downloadCmd, + InitFn: initDownloadCmd, + Args: []string{"fakeapp", "download", "--exercise=bogus-exercise"}, + } + cmdTest.Setup(t) + defer cmdTest.Teardown(t) + + ts := fakeDownloadServer() + defer ts.Close() + + userCfg := config.NewEmptyUserConfig() + userCfg.Workspace = cmdTest.TmpDir + userCfg.APIBaseURL = ts.URL + err := userCfg.Write() + assert.NoError(t, err) + + err = cmdTest.App.Execute() + if assert.Error(t, err) { + assert.Regexp(t, "Welcome to Exercism", err.Error()) + } +} + func TestDownload(t *testing.T) { oldOut := Out oldErr := Err @@ -96,9 +129,16 @@ func TestDownloadArgs(t *testing.T) { Args: append([]string{"fakeapp", "download"}, test.args...), } cmdTest.Setup(t) + userCfg := config.NewEmptyUserConfig() + userCfg.Workspace = cmdTest.TmpDir + userCfg.APIBaseURL = "http://example.com" + userCfg.Token = "abc123" + err := userCfg.Write() + assert.NoError(t, err) + cmdTest.App.SetOutput(ioutil.Discard) defer cmdTest.Teardown(t) - err := cmdTest.App.Execute() + err = cmdTest.App.Execute() assert.EqualError(t, err, test.expectedError) } From 4d6f29243b61ff494e8cbd21241d7954b650ed35 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 10:33:39 +0100 Subject: [PATCH 155/544] Move duplicated welcome message to constant --- cmd/cmd.go | 16 ++++++++++++++++ cmd/download.go | 17 +---------------- cmd/submit.go | 17 +---------------- 3 files changed, 18 insertions(+), 32 deletions(-) create mode 100644 cmd/cmd.go diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 000000000..c7f97fe5c --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,16 @@ +package cmd + +const msgWelcomePleaseConfigure = ` + + Welcome to Exercism! + + To get started, you need to configure the the tool with your API token. + Find your token at + + %s + + Then run the configure command: + + %s configure --token=YOUR_TOKEN + +` diff --git a/cmd/download.go b/cmd/download.go index a4e55b43b..e95564a00 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -36,22 +36,7 @@ Download other people's solutions by providing the UUID. } if usrCfg.Token == "" { tokenURL := config.InferSiteURL(usrCfg.APIBaseURL) + "/my/settings" - msg := ` - - Welcome to Exercism! - - To get started, you need to configure the the tool with your API token. - Find your token at - - %s - - Then run the configure command: - - - %s configure --token=YOUR_TOKEN - - ` - return fmt.Errorf(msg, tokenURL, BinaryName) + return fmt.Errorf(msgWelcomePleaseConfigure, tokenURL, BinaryName) } uuid, err := cmd.Flags().GetString("uuid") diff --git a/cmd/submit.go b/cmd/submit.go index 0feed8806..5b85aea5a 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -63,22 +63,7 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er if usrCfg.GetString("token") == "" { tokenURL := config.InferSiteURL(usrCfg.GetString("apibaseurl")) + "/my/settings" - msg := ` - - Welcome to Exercism! - - To get started, you need to configure the the tool with your API token. - Find your token at - - %s - - Then run the configure command: - - - %s configure --token=YOUR_TOKEN - - ` - return fmt.Errorf(msg, tokenURL, BinaryName) + return fmt.Errorf(msgWelcomePleaseConfigure, tokenURL, BinaryName) } if usrCfg.GetString("workspace") == "" { From 4672b493a7130a8f28272aeea2d72f4d27b35d76 Mon Sep 17 00:00:00 2001 From: nywilken Date: Fri, 13 Jul 2018 07:48:39 -0400 Subject: [PATCH 156/544] Fix relative path test on Windows This changes fixes the one test for Windows, but I am not entirely confident that the test passing means that it will work 100% of the time on Windows. I am still wrapping my head around how the submission stuff should play out. --- cmd/submit_relative_path_windows_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/submit_relative_path_windows_test.go b/cmd/submit_relative_path_windows_test.go index f2512b03a..9a0f2a978 100644 --- a/cmd/submit_relative_path_windows_test.go +++ b/cmd/submit_relative_path_windows_test.go @@ -13,7 +13,7 @@ import ( ) func TestSubmitRelativePath(t *testing.T) { - t.Skip("The Windows build is failing and needs to be debugged.\nSee https://ci.appveyor.com/project/kytrinyx/cli/build/110") + //t.Skip("The Windows build is failing and needs to be debugged.\nSee https://ci.appveyor.com/project/kytrinyx/cli/build/110") oldOut := Out oldErr := Err @@ -30,6 +30,7 @@ func TestSubmitRelativePath(t *testing.T) { tmpDir, err := ioutil.TempDir("", "relative-path") assert.NoError(t, err) + defer os.RemoveAll(tmpDir) dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") os.MkdirAll(dir, os.FileMode(0755)) @@ -55,5 +56,5 @@ func TestSubmitRelativePath(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 1, len(submittedFiles)) - assert.Equal(t, "This is a file.", submittedFiles["/file.txt"]) + assert.Equal(t, "This is a file.", submittedFiles["\\file.txt"]) } From 89c39fbd2b0735281d748ac3a2fd7456dce674bd Mon Sep 17 00:00:00 2001 From: Sonia Hamilton Date: Sun, 15 Jul 2018 03:43:56 +1000 Subject: [PATCH 157/544] Improve setup instructions for contributors (#628) The current instructions don't demonstrate how a contributor would track the main repo while submitting pull requests. These updated instructions allow contributors to pull from upstream but not push, whilst being able to push to their repo and submit PR's. --- CONTRIBUTING.md | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f483d15a..df933fa71 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,15 +17,19 @@ The TL;DR is: **don't clone your fork**, and it matters where on your filesystem If you don't care how and why and just want something that works, follow these steps: -1. [fork this repo][fork] +1. [fork this repo on the GitHub webpage][fork] 1. `go get github.com/exercism/cli/exercism` 1. `cd $GOPATH/src/github.com/exercism/cli` (or `cd %GOPATH%/src/github.com/exercism/cli` on Windows) -1. `git remote set-url origin https://github.com//cli` +1. `git remote rename origin upstream` +1. `git remote add origin git@github.com:/cli.git` +1. `git checkout -b development` +1. `git push -u origin development` (setup where you push to, check it works) 1. `go get -u github.com/golang/dep/cmd/dep` * depending on your setup, you may need to install `dep` by following the instructions in the [`dep` repo](https://github.com/golang/dep) 1. `dep ensure` +1. `git update-index --assume-unchanged Gopkg.lock` (prevent your dep changes being committed) -Then make the change as usual, and submit a pull request. Please provide tests for the changes where possible. +Then make changes as usual and submit a pull request. Please provide tests for the changes where possible. If you care about the details, check out the blog post [Contributing to Open Source Repositories in Go][contrib-blog] on the Splice blog. @@ -47,24 +51,22 @@ As of Go 1.9 this is simplified to `go test ./...`. ## Manual Testing against Exercism -You can build whatever is in your local, working copy of the CLI without overwriting your existing Exercism -CLI installation by using the `go build` command: +To test your changes while doing everyday Exercism work you +can build using the following instructions. Any name may be used for the +binary (e.g. `testercism`) - by using a name other than `exercism` you +can have different profiles under `~/.config` and avoid possibly +damaging your real Exercism submissions, or test different tokens, etc. -``` -go build -o testercism exercism/main.go -``` - -This assumes that you are standing at the root of the exercism/cli repository checked out locally, and it will put a binary named `testercism` in your current working directory. - -You can call it whatever you like, but `exercism` would conflict with the directory that is already there. - -Then you call it with `./testercism`. +On Unices: -You can always put this in your path if you want to run it from elsewhere on your system. +- `cd $GOPATH/src/github.com/exercism/cli/exercism && go build -o testercism main.go` +- `./testercism -h` -We highly recommend spinning up a local copy of Exercism to test against so that you can mess with the database (and so you don't accidentally break stuff for yourself in production). +On Windows: -[TODO: link to the nextercism repo installation instructions, and explain how to reconfigure the CLI] +- `cd /d %GOPATH%\src\github.com\exercism\cli` +- `go build -o testercism.exe exercism\main.go` +- `testercism.exe —help` ### Building for All Platforms From e2e2cd12734d6c677650ae5ef3d5f33f5c485d38 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 12:08:47 +0100 Subject: [PATCH 158/544] Make settings injectable in download command This changes the download command to rely on the injectable config.Configuration type, which will let us simplify the configuration and the tests. --- cmd/download.go | 263 +++++++++++++++++++++++++----------------------- 1 file changed, 138 insertions(+), 125 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index e95564a00..658698e39 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -14,6 +14,8 @@ import ( "github.com/exercism/cli/config" "github.com/exercism/cli/workspace" "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" ) // downloadCmd represents the download command @@ -30,164 +32,175 @@ latest solution. Download other people's solutions by providing the UUID. `, RunE: func(cmd *cobra.Command, args []string) error { - usrCfg, err := config.NewUserConfig() - if err != nil { - return err - } - if usrCfg.Token == "" { - tokenURL := config.InferSiteURL(usrCfg.APIBaseURL) + "/my/settings" - return fmt.Errorf(msgWelcomePleaseConfigure, tokenURL, BinaryName) - } + cfg := config.NewConfiguration() + + v := viper.New() + v.AddConfigPath(cfg.Dir) + v.SetConfigName("user") + v.SetConfigType("json") + // Ignore error. If the file doesn't exist, that is fine. + _ = v.ReadInConfig() + cfg.UserViperConfig = v - uuid, err := cmd.Flags().GetString("uuid") + return runDownload(cfg, cmd.Flags(), args) + }, +} + +func runDownload(cfg config.Configuration, flags *pflag.FlagSet, args []string) error { + usrCfg := cfg.UserViperConfig + if usrCfg.GetString("token") == "" { + tokenURL := config.InferSiteURL(usrCfg.GetString("apibaseurl")) + "/my/settings" + return fmt.Errorf(msgWelcomePleaseConfigure, tokenURL, BinaryName) + } + + uuid, err := flags.GetString("uuid") + if err != nil { + return err + } + exercise, err := flags.GetString("exercise") + if err != nil { + return err + } + if uuid == "" && exercise == "" { + return errors.New("need an --exercise name or a solution --uuid") + } + + var slug string + if uuid == "" { + slug = "latest" + } else { + slug = uuid + } + url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), slug) + + client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl")) + if err != nil { + return err + } + + req, err := client.NewRequest("GET", url, nil) + if err != nil { + return err + } + + track, err := flags.GetString("track") + if err != nil { + return err + } + + if uuid == "" { + q := req.URL.Query() + q.Add("exercise_id", exercise) + if track != "" { + q.Add("track_id", track) + } + req.URL.RawQuery = q.Encode() + } + + res, err := client.Do(req) + if err != nil { + return err + } + + var payload downloadPayload + defer res.Body.Close() + if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { + return fmt.Errorf("unable to parse API response - %s", err) + } + + if res.StatusCode == http.StatusUnauthorized { + siteURL := config.InferSiteURL(usrCfg.GetString("apibaseurl")) + return fmt.Errorf("unauthorized request. Please run the configure command. You can find your API token at %s/my/settings", siteURL) + } + + if res.StatusCode != http.StatusOK { + switch payload.Error.Type { + case "track_ambiguous": + return fmt.Errorf("%s: %s", payload.Error.Message, strings.Join(payload.Error.PossibleTrackIDs, ", ")) + default: + return errors.New(payload.Error.Message) + } + } + + solution := workspace.Solution{ + AutoApprove: payload.Solution.Exercise.AutoApprove, + Track: payload.Solution.Exercise.Track.ID, + Exercise: payload.Solution.Exercise.ID, + ID: payload.Solution.ID, + URL: payload.Solution.URL, + Handle: payload.Solution.User.Handle, + IsRequester: payload.Solution.User.IsRequester, + } + + dir := filepath.Join(usrCfg.GetString("workspace"), solution.Track) + os.MkdirAll(dir, os.FileMode(0755)) + + var ws workspace.Workspace + if solution.IsRequester { + ws, err = workspace.New(dir) if err != nil { return err } - exercise, err := cmd.Flags().GetString("exercise") + } else { + ws, err = workspace.New(filepath.Join(usrCfg.GetString("workspace"), "users", solution.Handle, solution.Track)) if err != nil { return err } - if uuid == "" && exercise == "" { - return errors.New("need an --exercise name or a solution --uuid") - } + } - var slug string - if uuid == "" { - slug = "latest" - } else { - slug = uuid - } - url := fmt.Sprintf("%s/solutions/%s", usrCfg.APIBaseURL, slug) + dir, err = ws.SolutionPath(solution.Exercise, solution.ID) + if err != nil { + return err + } - client, err := api.NewClient(usrCfg.Token, usrCfg.APIBaseURL) - if err != nil { - return err - } + os.MkdirAll(dir, os.FileMode(0755)) - req, err := client.NewRequest("GET", url, nil) - if err != nil { - return err - } + err = solution.Write(dir) + if err != nil { + return err + } - track, err := cmd.Flags().GetString("track") + for _, file := range payload.Solution.Files { + url := fmt.Sprintf("%s%s", payload.Solution.FileDownloadBaseURL, file) + req, err := client.NewRequest("GET", url, nil) if err != nil { return err } - if uuid == "" { - q := req.URL.Query() - q.Add("exercise_id", exercise) - if track != "" { - q.Add("track_id", track) - } - req.URL.RawQuery = q.Encode() - } - res, err := client.Do(req) if err != nil { return err } - - var payload downloadPayload defer res.Body.Close() - if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { - return fmt.Errorf("unable to parse API response - %s", err) - } - - if res.StatusCode == http.StatusUnauthorized { - siteURL := config.InferSiteURL(usrCfg.APIBaseURL) - return fmt.Errorf("unauthorized request. Please run the configure command. You can find your API token at %s/my/settings", siteURL) - } if res.StatusCode != http.StatusOK { - switch payload.Error.Type { - case "track_ambiguous": - return fmt.Errorf("%s: %s", payload.Error.Message, strings.Join(payload.Error.PossibleTrackIDs, ", ")) - default: - return errors.New(payload.Error.Message) - } + // TODO: deal with it + continue } - - solution := workspace.Solution{ - AutoApprove: payload.Solution.Exercise.AutoApprove, - Track: payload.Solution.Exercise.Track.ID, - Exercise: payload.Solution.Exercise.ID, - ID: payload.Solution.ID, - URL: payload.Solution.URL, - Handle: payload.Solution.User.Handle, - IsRequester: payload.Solution.User.IsRequester, + // Don't bother with empty files. + if res.Header.Get("Content-Length") == "0" { + continue } - dir := filepath.Join(usrCfg.Workspace, solution.Track) + // TODO: if there's a collision, interactively resolve (show diff, ask if overwrite). + // TODO: handle --force flag to overwrite without asking. + relativePath := filepath.FromSlash(file) + dir := filepath.Join(solution.Dir, filepath.Dir(relativePath)) os.MkdirAll(dir, os.FileMode(0755)) - var ws workspace.Workspace - if solution.IsRequester { - ws, err = workspace.New(dir) - if err != nil { - return err - } - } else { - ws, err = workspace.New(filepath.Join(usrCfg.Workspace, "users", solution.Handle, solution.Track)) - if err != nil { - return err - } - } - - dir, err = ws.SolutionPath(solution.Exercise, solution.ID) + f, err := os.Create(filepath.Join(solution.Dir, relativePath)) if err != nil { return err } - - os.MkdirAll(dir, os.FileMode(0755)) - - err = solution.Write(dir) + defer f.Close() + _, err = io.Copy(f, res.Body) if err != nil { return err } - - for _, file := range payload.Solution.Files { - url := fmt.Sprintf("%s%s", payload.Solution.FileDownloadBaseURL, file) - req, err := client.NewRequest("GET", url, nil) - if err != nil { - return err - } - - res, err := client.Do(req) - if err != nil { - return err - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - // TODO: deal with it - continue - } - // Don't bother with empty files. - if res.Header.Get("Content-Length") == "0" { - continue - } - - // TODO: if there's a collision, interactively resolve (show diff, ask if overwrite). - // TODO: handle --force flag to overwrite without asking. - relativePath := filepath.FromSlash(file) - dir := filepath.Join(solution.Dir, filepath.Dir(relativePath)) - os.MkdirAll(dir, os.FileMode(0755)) - - f, err := os.Create(filepath.Join(solution.Dir, relativePath)) - if err != nil { - return err - } - defer f.Close() - _, err = io.Copy(f, res.Body) - if err != nil { - return err - } - } - fmt.Fprintf(Err, "\nDownloaded to\n") - fmt.Fprintf(Out, "%s\n", solution.Dir) - return nil - }, + } + fmt.Fprintf(Err, "\nDownloaded to\n") + fmt.Fprintf(Out, "%s\n", solution.Dir) + return nil } type downloadPayload struct { From 650e7b6d45fee15ffd03152e46c7e11e148d70da Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 16:39:44 +0100 Subject: [PATCH 159/544] Simplify download command tests Now that the download command has a runDownload function, we can use it to pass the configuration we want directly, rather than looking up the configuration directory in the environment and then reading the configuration from a file. This means significantly less setup in the tests. --- cmd/download.go | 10 ++-- cmd/download_test.go | 137 +++++++++++++++---------------------------- 2 files changed, 52 insertions(+), 95 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index 658698e39..187c4a8d4 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -233,13 +233,13 @@ type downloadPayload struct { } `json:"error,omitempty"` } -func initDownloadCmd() { - downloadCmd.Flags().StringP("uuid", "u", "", "the solution UUID") - downloadCmd.Flags().StringP("track", "t", "", "the track ID") - downloadCmd.Flags().StringP("exercise", "e", "", "the exercise slug") +func setupDownloadFlags(flags *pflag.FlagSet) { + flags.StringP("uuid", "u", "", "the solution UUID") + flags.StringP("track", "t", "", "the track ID") + flags.StringP("exercise", "e", "", "the exercise slug") } func init() { RootCmd.AddCommand(downloadCmd) - initDownloadCmd() + setupDownloadFlags(downloadCmd.Flags()) } diff --git a/cmd/download_test.go b/cmd/download_test.go index ebb8f8179..132f40e40 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -10,39 +10,36 @@ import ( "testing" "github.com/exercism/cli/config" + "github.com/spf13/pflag" + "github.com/spf13/viper" "github.com/stretchr/testify/assert" ) func TestDownloadWithoutToken(t *testing.T) { - oldOut := Out - oldErr := Err - Out = ioutil.Discard - Err = ioutil.Discard - defer func() { - Out = oldOut - Err = oldErr - }() + cfg := config.Configuration{ + UserViperConfig: viper.New(), + } - cmdTest := &CommandTest{ - Cmd: downloadCmd, - InitFn: initDownloadCmd, - Args: []string{"fakeapp", "download", "--exercise=bogus-exercise"}, + err := runDownload(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) + if assert.Error(t, err) { + assert.Regexp(t, "Welcome to Exercism", err.Error()) } - cmdTest.Setup(t) - defer cmdTest.Teardown(t) +} - ts := fakeDownloadServer() - defer ts.Close() +func TestDownloadWithoutFlags(t *testing.T) { + v := viper.New() + v.Set("token", "abc123") - userCfg := config.NewEmptyUserConfig() - userCfg.Workspace = cmdTest.TmpDir - userCfg.APIBaseURL = ts.URL - err := userCfg.Write() - assert.NoError(t, err) + cfg := config.Configuration{ + UserViperConfig: v, + } + + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupDownloadFlags(flags) - err = cmdTest.App.Execute() + err := runDownload(cfg, flags, []string{}) if assert.Error(t, err) { - assert.Regexp(t, "Welcome to Exercism", err.Error()) + assert.Regexp(t, "need an --exercise name or a solution --uuid", err.Error()) } } @@ -56,102 +53,62 @@ func TestDownload(t *testing.T) { Err = oldErr }() - cmdTest := &CommandTest{ - Cmd: downloadCmd, - InitFn: initDownloadCmd, - Args: []string{"fakeapp", "download", "--exercise=bogus-exercise"}, - } - cmdTest.Setup(t) - defer cmdTest.Teardown(t) + tmpDir, err := ioutil.TempDir("", "download-cmd") + assert.NoError(t, err) ts := fakeDownloadServer() defer ts.Close() - err := writeFakeUserConfigSettings(cmdTest.TmpDir, ts.URL) + v := viper.New() + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + v.Set("token", "abc123") + + cfg := config.Configuration{ + UserViperConfig: v, + } + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupDownloadFlags(flags) + flags.Set("exercise", "bogus-exercise") + + err = runDownload(cfg, flags, []string{}) assert.NoError(t, err) - testCases := []struct { + expectedFiles := []struct { desc string path string contents string }{ { - desc: "It should download a file.", - path: filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", "file-1.txt"), + desc: "a file in the exercise root directory", + path: filepath.Join(tmpDir, "bogus-track", "bogus-exercise", "file-1.txt"), contents: "this is file 1", }, { - desc: "It should download a file in a subdir.", - path: filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", "subdir", "file-2.txt"), + desc: "a file in a subdirectory", + path: filepath.Join(tmpDir, "bogus-track", "bogus-exercise", "subdir", "file-2.txt"), contents: "this is file 2", }, { - desc: "It creates the .solution.json file.", - path: filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", ".solution.json"), + desc: "the solution metadata file", + path: filepath.Join(tmpDir, "bogus-track", "bogus-exercise", ".solution.json"), contents: `{"track":"bogus-track","exercise":"bogus-exercise","id":"bogus-id","url":"","handle":"alice","is_requester":true,"auto_approve":false}`, }, } - cmdTest.App.Execute() - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - b, err := ioutil.ReadFile(tc.path) + for _, file := range expectedFiles { + t.Run(file.desc, func(t *testing.T) { + b, err := ioutil.ReadFile(file.path) assert.NoError(t, err) - assert.Equal(t, tc.contents, string(b)) + assert.Equal(t, file.contents, string(b)) }) } - path := filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", "file-3.txt") + path := filepath.Join(tmpDir, "bogus-track", "bogus-exercise", "file-3.txt") _, err = os.Lstat(path) assert.True(t, os.IsNotExist(err), "It should not write the file if empty.") } -func TestDownloadArgs(t *testing.T) { - tests := []struct { - args []string - expectedError string - }{ - { - args: []string{"bogus"}, // providing just an exercise slug without the flag - expectedError: "need an --exercise name or a solution --uuid", - }, - { - args: []string{""}, // providing no args - expectedError: "need an --exercise name or a solution --uuid", - }, - } - - for _, test := range tests { - cmdTest := &CommandTest{ - Cmd: downloadCmd, - InitFn: initDownloadCmd, - Args: append([]string{"fakeapp", "download"}, test.args...), - } - cmdTest.Setup(t) - userCfg := config.NewEmptyUserConfig() - userCfg.Workspace = cmdTest.TmpDir - userCfg.APIBaseURL = "http://example.com" - userCfg.Token = "abc123" - err := userCfg.Write() - assert.NoError(t, err) - - cmdTest.App.SetOutput(ioutil.Discard) - defer cmdTest.Teardown(t) - err = cmdTest.App.Execute() - - assert.EqualError(t, err, test.expectedError) - } -} - -func writeFakeUserConfigSettings(tmpDirPath, serverURL string) error { - userCfg := config.NewEmptyUserConfig() - userCfg.Workspace = tmpDirPath - userCfg.APIBaseURL = serverURL - userCfg.Token = "abc123" - return userCfg.Write() -} - func fakeDownloadServer() *httptest.Server { mux := http.NewServeMux() server := httptest.NewServer(mux) From 68a3e2ddc1461b285475b397252adf6b7922a0c2 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 16:45:53 +0100 Subject: [PATCH 160/544] Get rid of user config in open command --- cmd/open.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/cmd/open.go b/cmd/open.go index e3a432720..a18722701 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -9,6 +9,7 @@ import ( "github.com/exercism/cli/config" "github.com/exercism/cli/workspace" "github.com/spf13/cobra" + "github.com/spf13/viper" ) // openCmd opens the designated exercise in the browser. @@ -23,11 +24,16 @@ the solution you want to see on the website. `, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - cfg, err := config.NewUserConfig() - if err != nil { - return err - } - ws, err := workspace.New(cfg.Workspace) + cfg := config.NewConfiguration() + + v := viper.New() + v.AddConfigPath(cfg.Dir) + v.SetConfigName("user") + v.SetConfigType("json") + // Ignore error. If the file doesn't exist, that is fine. + _ = v.ReadInConfig() + + ws, err := workspace.New(v.GetString("workspace")) if err != nil { return err } From d93894c28e828abead5d48ef2279f143f32f5757 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 16:47:55 +0100 Subject: [PATCH 161/544] Get rid of user config in workspace command --- cmd/workspace.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/cmd/workspace.go b/cmd/workspace.go index 61872bc5c..03c1ffef4 100644 --- a/cmd/workspace.go +++ b/cmd/workspace.go @@ -5,6 +5,7 @@ import ( "github.com/exercism/cli/config" "github.com/spf13/cobra" + "github.com/spf13/viper" ) // workspaceCmd outputs the path to the person's workspace directory. @@ -26,11 +27,16 @@ need to be on the same drive as your workspace directory. Otherwise nothing will happen. `, RunE: func(cmd *cobra.Command, args []string) error { - usrCfg, err := config.NewUserConfig() - if err != nil { - return err - } - fmt.Println(usrCfg.Workspace) + cfg := config.NewConfiguration() + + v := viper.New() + v.AddConfigPath(cfg.Dir) + v.SetConfigName("user") + v.SetConfigType("json") + // Ignore error. If the file doesn't exist, that is fine. + _ = v.ReadInConfig() + + fmt.Fprintf(Out, "%s\n", v.GetString("workspace")) return nil }, } From a9ffb27a23f74e24aed91567a9b6f5563b888ab2 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 16:50:58 +0100 Subject: [PATCH 162/544] Remove UserConfig from troubleshoot command --- cmd/troubleshoot.go | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go index b03242a90..3e22920c4 100644 --- a/cmd/troubleshoot.go +++ b/cmd/troubleshoot.go @@ -13,6 +13,7 @@ import ( "github.com/exercism/cli/cli" "github.com/exercism/cli/config" "github.com/spf13/cobra" + "github.com/spf13/viper" ) // fullAPIKey flag for troubleshoot command. @@ -32,12 +33,16 @@ command into a GitHub issue so we can help figure out what's going on. cli.HTTPClient = &http.Client{Timeout: 20 * time.Second} c := cli.New(Version) - cfg, err := config.NewUserConfig() - if err != nil { - return err - } + cfg := config.NewConfiguration() + + v := viper.New() + v.AddConfigPath(cfg.Dir) + v.SetConfigName("user") + v.SetConfigType("json") + // Ignore error. If the file doesn't exist, that is fine. + _ = v.ReadInConfig() - status := newStatus(c, *cfg) + status := newStatus(c, v) status.Censor = !fullAPIKey s, err := status.check() if err != nil { @@ -57,7 +62,7 @@ type Status struct { Configuration configurationStatus APIReachability apiReachabilityStatus cli *cli.CLI - cfg config.UserConfig + cfg *viper.Viper } type versionStatus struct { @@ -94,10 +99,10 @@ type apiPing struct { } // newStatus prepares a value to perform a diagnostic self-check. -func newStatus(c *cli.CLI, uc config.UserConfig) Status { +func newStatus(c *cli.CLI, v *viper.Viper) Status { status := Status{ cli: c, - cfg: uc, + cfg: v, } return status } @@ -107,7 +112,7 @@ func (status *Status) check() (string, error) { status.Version = newVersionStatus(status.cli) status.System = newSystemStatus() status.Configuration = newConfigurationStatus(status) - status.APIReachability = newAPIReachabilityStatus(status.cfg.APIBaseURL) + status.APIReachability = newAPIReachabilityStatus(status.cfg.GetString("apibaseurl")) return status.compile() } @@ -167,15 +172,16 @@ func newSystemStatus() systemStatus { } func newConfigurationStatus(status *Status) configurationStatus { + token := status.cfg.GetString("token") cs := configurationStatus{ - Home: status.cfg.Home, - Workspace: status.cfg.Workspace, - File: status.cfg.File(), - Token: status.cfg.Token, - TokenURL: config.InferSiteURL(status.cfg.APIBaseURL) + "/my/settings", + Home: status.cfg.GetString("home"), + Workspace: status.cfg.GetString("workspace"), + File: status.cfg.ConfigFileUsed(), + Token: token, + TokenURL: config.InferSiteURL(status.cfg.GetString("apibaseurl")) + "/my/settings", } - if status.Censor && status.cfg.Token != "" { - cs.Token = redact(status.cfg.Token) + if status.Censor && token != "" { + cs.Token = redact(token) } return cs } From c3a79d26d8cd14b7371a35986f69ec28548499bd Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 7 Jul 2018 16:32:30 +0100 Subject: [PATCH 163/544] Inject user config into prepare track method I'm preparing to swap out the user config with the viper config. This will let us simplify the test. --- cmd/prepare.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/cmd/prepare.go b/cmd/prepare.go index 6992a022e..c93b9b63a 100644 --- a/cmd/prepare.go +++ b/cmd/prepare.go @@ -39,16 +39,15 @@ To customize the CLI to suit your own preferences, use the configure command. fmt.Println("prepare called") return nil } - return prepareTrack(track) + cfg, err := config.NewUserConfig() + if err != nil { + return err + } + return prepareTrack(track, cfg) }, } -func prepareTrack(id string) error { - cfg, err := config.NewUserConfig() - if err != nil { - return err - } - +func prepareTrack(id string, cfg *config.UserConfig) error { client, err := api.NewClient(cfg.Token, cfg.APIBaseURL) if err != nil { return err From 97bb7800222e3ffe4a2cb31383abbe2b0a095f53 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 17:03:18 +0100 Subject: [PATCH 164/544] Normalize prepare command The submit, configure, and download commands all have a runXYZ command that takes a configuration value, flags, and arguments. The prepare command had a function it was passing work to, but it didn't follow this format. --- cmd/prepare.go | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/cmd/prepare.go b/cmd/prepare.go index c93b9b63a..02b7b239a 100644 --- a/cmd/prepare.go +++ b/cmd/prepare.go @@ -9,6 +9,7 @@ import ( "github.com/exercism/cli/api" "github.com/exercism/cli/config" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) // prepareCmd does necessary setup for Exercism and its tracks. @@ -18,10 +19,6 @@ var prepareCmd = &cobra.Command{ Short: "Prepare does setup for Exercism and its tracks.", Long: `Prepare downloads settings and dependencies for Exercism and the language tracks. -When called without any arguments, this downloads all the copy for the CLI so we -know what to say in all the various situations. It also provides an up-to-date list -of the API endpoints to use. - When called with a track ID, it will do specific setup for that track. This might include downloading the files that the track maintainers have said are necessary for the track in general. Any files that are only necessary for a specific @@ -30,29 +27,32 @@ exercise will be downloaded along with the exercise. To customize the CLI to suit your own preferences, use the configure command. `, RunE: func(cmd *cobra.Command, args []string) error { - track, err := cmd.Flags().GetString("track") - if err != nil { - return err - } - - if track == "" { - fmt.Println("prepare called") - return nil - } - cfg, err := config.NewUserConfig() + cfg := config.NewConfiguration() + usrCfg, err := config.NewUserConfig() if err != nil { return err } - return prepareTrack(track, cfg) + cfg.UserConfig = usrCfg + return runPrepare(cfg, cmd.Flags(), args) }, } -func prepareTrack(id string, cfg *config.UserConfig) error { - client, err := api.NewClient(cfg.Token, cfg.APIBaseURL) +func runPrepare(cfg config.Configuration, flags *pflag.FlagSet, args []string) error { + usrCfg := cfg.UserConfig + + track, err := flags.GetString("track") + if err != nil { + return err + } + + if track == "" { + return nil + } + client, err := api.NewClient(usrCfg.Token, usrCfg.APIBaseURL) if err != nil { return err } - url := fmt.Sprintf("%s/tracks/%s", cfg.APIBaseURL, id) + url := fmt.Sprintf("%s/tracks/%s", usrCfg.APIBaseURL, track) req, err := client.NewRequest("GET", url, nil) if err != nil { @@ -80,14 +80,14 @@ func prepareTrack(id string, cfg *config.UserConfig) error { return err } - t, ok := cliCfg.Tracks[id] + t, ok := cliCfg.Tracks[track] if !ok { - t = config.NewTrack(id) + t = config.NewTrack(track) } if payload.Track.TestPattern != "" { t.IgnorePatterns = append(t.IgnorePatterns, payload.Track.TestPattern) } - cliCfg.Tracks[id] = t + cliCfg.Tracks[track] = t return cliCfg.Write() } From def58a5d40bdcbdde2d9b33871062bef88115f24 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 17:05:13 +0100 Subject: [PATCH 165/544] Remove user config from prepare command --- cmd/prepare.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/cmd/prepare.go b/cmd/prepare.go index 02b7b239a..4c29a5a5c 100644 --- a/cmd/prepare.go +++ b/cmd/prepare.go @@ -10,6 +10,7 @@ import ( "github.com/exercism/cli/config" "github.com/spf13/cobra" "github.com/spf13/pflag" + "github.com/spf13/viper" ) // prepareCmd does necessary setup for Exercism and its tracks. @@ -28,17 +29,21 @@ To customize the CLI to suit your own preferences, use the configure command. `, RunE: func(cmd *cobra.Command, args []string) error { cfg := config.NewConfiguration() - usrCfg, err := config.NewUserConfig() - if err != nil { - return err - } - cfg.UserConfig = usrCfg + + v := viper.New() + v.AddConfigPath(cfg.Dir) + v.SetConfigName("user") + v.SetConfigType("json") + // Ignore error. If the file doesn't exist, that is fine. + _ = v.ReadInConfig() + cfg.UserViperConfig = v + return runPrepare(cfg, cmd.Flags(), args) }, } func runPrepare(cfg config.Configuration, flags *pflag.FlagSet, args []string) error { - usrCfg := cfg.UserConfig + v := cfg.UserViperConfig track, err := flags.GetString("track") if err != nil { @@ -48,11 +53,11 @@ func runPrepare(cfg config.Configuration, flags *pflag.FlagSet, args []string) e if track == "" { return nil } - client, err := api.NewClient(usrCfg.Token, usrCfg.APIBaseURL) + client, err := api.NewClient(v.GetString("token"), v.GetString("apibaseurl")) if err != nil { return err } - url := fmt.Sprintf("%s/tracks/%s", usrCfg.APIBaseURL, track) + url := fmt.Sprintf("%s/tracks/%s", v.GetString("apibaseurl"), track) req, err := client.NewRequest("GET", url, nil) if err != nil { From b724e478389fe8df0153be29bb50857551cb5f68 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 17:27:28 +0100 Subject: [PATCH 166/544] Get rid of user config in prepare command Rework test to go straight to runPrepare instead of all the way through the command interface. --- cmd/prepare.go | 6 +++++- cmd/prepare_test.go | 35 +++++++++++++++++++++++------------ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/cmd/prepare.go b/cmd/prepare.go index 4c29a5a5c..4c0557f1f 100644 --- a/cmd/prepare.go +++ b/cmd/prepare.go @@ -110,7 +110,11 @@ type prepareTrackPayload struct { } func initPrepareCmd() { - prepareCmd.Flags().StringP("track", "t", "", "the track you want to prepare") + setupPrepareFlags(prepareCmd.Flags()) +} + +func setupPrepareFlags(flags *pflag.FlagSet) { + flags.StringP("track", "t", "", "the track you want to prepare") } func init() { diff --git a/cmd/prepare_test.go b/cmd/prepare_test.go index 97930660b..8d308d1b5 100644 --- a/cmd/prepare_test.go +++ b/cmd/prepare_test.go @@ -2,24 +2,19 @@ package cmd import ( "fmt" + "io/ioutil" "net/http" "net/http/httptest" "os" "testing" "github.com/exercism/cli/config" + "github.com/spf13/pflag" + "github.com/spf13/viper" "github.com/stretchr/testify/assert" ) func TestPrepareTrack(t *testing.T) { - cmdTest := &CommandTest{ - Cmd: prepareCmd, - InitFn: initPrepareCmd, - Args: []string{"fakeapp", "prepare", "--track", "bogus"}, - } - cmdTest.Setup(t) - defer cmdTest.Teardown(t) - fakeEndpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { payload := ` { @@ -35,12 +30,28 @@ func TestPrepareTrack(t *testing.T) { ts := httptest.NewServer(fakeEndpoint) defer ts.Close() - usrCfg := config.NewEmptyUserConfig() - usrCfg.APIBaseURL = ts.URL - err := usrCfg.Write() + tmpDir, err := ioutil.TempDir("", "prepare-track") assert.NoError(t, err) + defer os.Remove(tmpDir) + + // Until we can decouple CLIConfig from filesystem, overwrite config dir. + originalConfigDir := os.Getenv(cfgHomeKey) + os.Setenv(cfgHomeKey, tmpDir) + defer os.Setenv(cfgHomeKey, originalConfigDir) + + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupPrepareFlags(flags) + flags.Set("track", "bogus") - cmdTest.App.Execute() + v := viper.New() + v.Set("apibaseurl", ts.URL) + + cfg := config.Configuration{ + UserViperConfig: v, + } + + err = runPrepare(cfg, flags, []string{}) + assert.NoError(t, err) cliCfg, err := config.NewCLIConfig() os.Remove(cliCfg.File()) From b80c97ce272c1668e8ffa76bf906110f3249194a Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 17:29:43 +0100 Subject: [PATCH 167/544] Delete the UserConfig type It is no longer used anywhere. --- config/config.go | 4 +++ config/configuration.go | 5 --- config/user_config.go | 73 -------------------------------------- config/user_config_test.go | 39 -------------------- 4 files changed, 4 insertions(+), 117 deletions(-) delete mode 100644 config/user_config.go delete mode 100644 config/user_config_test.go diff --git a/config/config.go b/config/config.go index d71f0da6d..b7a07f190 100644 --- a/config/config.go +++ b/config/config.go @@ -11,6 +11,10 @@ import ( "github.com/spf13/viper" ) +var ( + defaultBaseURL = "https://api.exercism.io/v1" +) + // Config is a wrapper around a viper configuration. type Config struct { dir string diff --git a/config/configuration.go b/config/configuration.go index 7dffe58ec..325eeb2a0 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -15,10 +15,6 @@ var ( ) // Configuration lets us inject configuration options into commands. -// Note that we are slowly working towards getting rid of the -// config.Config, config.UserConfig, and config.CLIConfig types. -// Once we do, we can rename this type to Config, and get rid of the -// User and CLI fields. type Configuration struct { OS string Home string @@ -26,7 +22,6 @@ type Configuration struct { DefaultBaseURL string DefaultDirName string UserViperConfig *viper.Viper - UserConfig *UserConfig CLIConfig *CLIConfig Persister Persister } diff --git a/config/user_config.go b/config/user_config.go deleted file mode 100644 index cd61633bc..000000000 --- a/config/user_config.go +++ /dev/null @@ -1,73 +0,0 @@ -package config - -import ( - "fmt" - "os" - - "github.com/spf13/viper" -) - -var ( - defaultBaseURL = "https://api.exercism.io/v1" -) - -// UserConfig contains user-specific settings. -type UserConfig struct { - *Config - Workspace string - Token string - Home string - APIBaseURL string - settings Configuration -} - -// NewUserConfig loads a user configuration if it exists. -func NewUserConfig() (*UserConfig, error) { - cfg := NewEmptyUserConfig() - - if err := cfg.Load(viper.New()); err != nil { - return nil, err - } - - return cfg, nil -} - -// NewEmptyUserConfig creates a user configuration without loading it. -func NewEmptyUserConfig() *UserConfig { - return &UserConfig{ - Config: New(Dir(), "user"), - settings: NewConfiguration(), - } -} - -// SetDefaults ensures that we have proper values where possible. -func (cfg *UserConfig) SetDefaults() { - if cfg.Home == "" { - cfg.Home = userHome() - } - if cfg.APIBaseURL == "" { - cfg.APIBaseURL = defaultBaseURL - } - if cfg.Workspace == "" { - dir := DefaultWorkspaceDir(cfg.settings) - - _, err := os.Stat(dir) - // Sorry about the double negative. - if !os.IsNotExist(err) { - dir = fmt.Sprintf("%s-1", dir) - } - cfg.Workspace = dir - } -} - -// Write stores the config to disk. -func (cfg *UserConfig) Write() error { - cfg.SetDefaults() - return Write(cfg) -} - -// Load reads a viper configuration into the config. -func (cfg *UserConfig) Load(v *viper.Viper) error { - cfg.readIn(v) - return v.Unmarshal(&cfg) -} diff --git a/config/user_config_test.go b/config/user_config_test.go deleted file mode 100644 index fe4d2bca4..000000000 --- a/config/user_config_test.go +++ /dev/null @@ -1,39 +0,0 @@ -// +build !windows - -package config - -import ( - "io/ioutil" - "os" - "testing" - - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" -) - -func TestUserConfig(t *testing.T) { - dir, err := ioutil.TempDir("", "user-config") - assert.NoError(t, err) - defer os.RemoveAll(dir) - - cfg := &UserConfig{ - Config: New(dir, "user"), - } - cfg.Token = "a" - cfg.Workspace = "/a" - cfg.APIBaseURL = "http://example.com" - - // write it - err = cfg.Write() - assert.NoError(t, err) - - // reload it - cfg = &UserConfig{ - Config: New(dir, "user"), - } - err = cfg.Load(viper.New()) - assert.NoError(t, err) - assert.Equal(t, "a", cfg.Token) - assert.Equal(t, "/a", cfg.Workspace) - assert.Equal(t, "http://example.com", cfg.APIBaseURL) -} From ab253b0b298cc6e24bcea2caaee31ada3853c58f Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 19:33:16 +0100 Subject: [PATCH 168/544] Extract downloaded files assertion The fake download server hardcodes responses, effectively hiding information. Since the setup in the test doesn't obviously create the data, it makes some sort of sense to hide the filenames in the assertion. I don't massively like it, but I think it kind of makes sense for now. --- cmd/download_test.go | 73 ++++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index 132f40e40..563bdad29 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -56,7 +56,7 @@ func TestDownload(t *testing.T) { tmpDir, err := ioutil.TempDir("", "download-cmd") assert.NoError(t, err) - ts := fakeDownloadServer() + ts := fakeDownloadServer(requestorSelf) defer ts.Close() v := viper.New() @@ -74,6 +74,37 @@ func TestDownload(t *testing.T) { err = runDownload(cfg, flags, []string{}) assert.NoError(t, err) + assertDownloadedCorrectFiles(t, tmpDir) +} + +func fakeDownloadServer(requestor string) *httptest.Server { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + + path1 := "file-1.txt" + mux.HandleFunc("/"+path1, func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "this is file 1") + }) + + path2 := "subdir/file-2.txt" + mux.HandleFunc("/"+path2, func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "this is file 2") + }) + + path3 := "file-3.txt" + mux.HandleFunc("/"+path3, func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "") + }) + + payloadBody := fmt.Sprintf(payloadTemplate, requestor, server.URL+"/", path1, path2, path3) + mux.HandleFunc("/solutions/latest", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, payloadBody) + }) + + return server +} + +func assertDownloadedCorrectFiles(t *testing.T, targetDir string) { expectedFiles := []struct { desc string path string @@ -81,17 +112,17 @@ func TestDownload(t *testing.T) { }{ { desc: "a file in the exercise root directory", - path: filepath.Join(tmpDir, "bogus-track", "bogus-exercise", "file-1.txt"), + path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "file-1.txt"), contents: "this is file 1", }, { desc: "a file in a subdirectory", - path: filepath.Join(tmpDir, "bogus-track", "bogus-exercise", "subdir", "file-2.txt"), + path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "subdir", "file-2.txt"), contents: "this is file 2", }, { desc: "the solution metadata file", - path: filepath.Join(tmpDir, "bogus-track", "bogus-exercise", ".solution.json"), + path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", ".solution.json"), contents: `{"track":"bogus-track","exercise":"bogus-exercise","id":"bogus-id","url":"","handle":"alice","is_requester":true,"auto_approve":false}`, }, } @@ -104,37 +135,13 @@ func TestDownload(t *testing.T) { }) } - path := filepath.Join(tmpDir, "bogus-track", "bogus-exercise", "file-3.txt") - _, err = os.Lstat(path) + path := filepath.Join(targetDir, "bogus-track", "bogus-exercise", "file-3.txt") + _, err := os.Lstat(path) assert.True(t, os.IsNotExist(err), "It should not write the file if empty.") } -func fakeDownloadServer() *httptest.Server { - mux := http.NewServeMux() - server := httptest.NewServer(mux) - - path1 := "file-1.txt" - mux.HandleFunc("/"+path1, func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, "this is file 1") - }) - - path2 := "subdir/file-2.txt" - mux.HandleFunc("/"+path2, func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, "this is file 2") - }) - - path3 := "file-3.txt" - mux.HandleFunc("/"+path3, func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, "") - }) - - payloadBody := fmt.Sprintf(payloadTemplate, server.URL+"/", path1, path2, path3) - mux.HandleFunc("/solutions/latest", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, payloadBody) - }) - - return server -} +const requestorSelf = "true" +const requestorOther = "false" const payloadTemplate = ` { @@ -142,7 +149,7 @@ const payloadTemplate = ` "id": "bogus-id", "user": { "handle": "alice", - "is_requester": true + "is_requester": %s }, "exercise": { "id": "bogus-exercise", From 1bcbae6e74117c1626a79c99fd7794533a878973 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 19:45:54 +0100 Subject: [PATCH 169/544] Ensure download own by uuid works You should be able to download by an exercise name or a uuid. This adds a test for the uuid case. --- cmd/download_test.go | 48 ++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index 563bdad29..b7d6cbf2c 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -53,28 +53,39 @@ func TestDownload(t *testing.T) { Err = oldErr }() - tmpDir, err := ioutil.TempDir("", "download-cmd") - assert.NoError(t, err) + testCases := []struct { + requestor string + expectedDir string + flag, flagValue string + }{ + {requestorSelf, "", "exercise", "bogus-exercise"}, + {requestorSelf, "", "uuid", "bogus-id"}, + } - ts := fakeDownloadServer(requestorSelf) - defer ts.Close() + for _, tc := range testCases { + tmpDir, err := ioutil.TempDir("", "download-cmd") + assert.NoError(t, err) - v := viper.New() - v.Set("workspace", tmpDir) - v.Set("apibaseurl", ts.URL) - v.Set("token", "abc123") + ts := fakeDownloadServer(tc.requestor) + defer ts.Close() - cfg := config.Configuration{ - UserViperConfig: v, - } - flags := pflag.NewFlagSet("fake", pflag.PanicOnError) - setupDownloadFlags(flags) - flags.Set("exercise", "bogus-exercise") + v := viper.New() + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + v.Set("token", "abc123") + + cfg := config.Configuration{ + UserViperConfig: v, + } + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupDownloadFlags(flags) + flags.Set(tc.flag, tc.flagValue) - err = runDownload(cfg, flags, []string{}) - assert.NoError(t, err) + err = runDownload(cfg, flags, []string{}) + assert.NoError(t, err) - assertDownloadedCorrectFiles(t, tmpDir) + assertDownloadedCorrectFiles(t, filepath.Join(tmpDir, tc.expectedDir)) + } } func fakeDownloadServer(requestor string) *httptest.Server { @@ -100,6 +111,9 @@ func fakeDownloadServer(requestor string) *httptest.Server { mux.HandleFunc("/solutions/latest", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, payloadBody) }) + mux.HandleFunc("/solutions/bogus-id", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, payloadBody) + }) return server } From a59e028d9d18c4d9861bc9d9e2791aa09faaa437 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 20:02:00 +0100 Subject: [PATCH 170/544] Fix download other person's solution by uuid When downloading someone else's solution, we weren't creating the necessary directories before trying to write the file to the target directory. --- cmd/download.go | 22 +++++++++------------- cmd/download_test.go | 10 ++++++---- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index 187c4a8d4..11650956b 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -132,20 +132,16 @@ func runDownload(cfg config.Configuration, flags *pflag.FlagSet, args []string) IsRequester: payload.Solution.User.IsRequester, } - dir := filepath.Join(usrCfg.GetString("workspace"), solution.Track) - os.MkdirAll(dir, os.FileMode(0755)) + dir := usrCfg.GetString("workspace") + if !solution.IsRequester { + dir = filepath.Join(dir, "users", solution.Handle) + } + dir = filepath.Join(dir, solution.Track) - var ws workspace.Workspace - if solution.IsRequester { - ws, err = workspace.New(dir) - if err != nil { - return err - } - } else { - ws, err = workspace.New(filepath.Join(usrCfg.GetString("workspace"), "users", solution.Handle, solution.Track)) - if err != nil { - return err - } + os.MkdirAll(dir, os.FileMode(0755)) + ws, err := workspace.New(dir) + if err != nil { + return err } dir, err = ws.SolutionPath(solution.Exercise, solution.ID) diff --git a/cmd/download_test.go b/cmd/download_test.go index b7d6cbf2c..92106a7c2 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -43,7 +43,7 @@ func TestDownloadWithoutFlags(t *testing.T) { } } -func TestDownload(t *testing.T) { +func TestDownloadTHISONE(t *testing.T) { oldOut := Out oldErr := Err Out = ioutil.Discard @@ -60,6 +60,7 @@ func TestDownload(t *testing.T) { }{ {requestorSelf, "", "exercise", "bogus-exercise"}, {requestorSelf, "", "uuid", "bogus-id"}, + {requestorOther, filepath.Join("users", "alice"), "uuid", "bogus-id"}, } for _, tc := range testCases { @@ -84,7 +85,7 @@ func TestDownload(t *testing.T) { err = runDownload(cfg, flags, []string{}) assert.NoError(t, err) - assertDownloadedCorrectFiles(t, filepath.Join(tmpDir, tc.expectedDir)) + assertDownloadedCorrectFiles(t, filepath.Join(tmpDir, tc.expectedDir), tc.requestor) } } @@ -118,7 +119,8 @@ func fakeDownloadServer(requestor string) *httptest.Server { return server } -func assertDownloadedCorrectFiles(t *testing.T, targetDir string) { +func assertDownloadedCorrectFiles(t *testing.T, targetDir, requestor string) { + metadata := `{"track":"bogus-track","exercise":"bogus-exercise","id":"bogus-id","url":"","handle":"alice","is_requester":%s,"auto_approve":false}` expectedFiles := []struct { desc string path string @@ -137,7 +139,7 @@ func assertDownloadedCorrectFiles(t *testing.T, targetDir string) { { desc: "the solution metadata file", path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", ".solution.json"), - contents: `{"track":"bogus-track","exercise":"bogus-exercise","id":"bogus-id","url":"","handle":"alice","is_requester":true,"auto_approve":false}`, + contents: fmt.Sprintf(metadata, requestor), }, } From 60207eaa5def2bb2f0fbc692488ed0d113f566bc Mon Sep 17 00:00:00 2001 From: Wilken Rivera Date: Sat, 14 Jul 2018 19:17:11 -0400 Subject: [PATCH 171/544] Update minimum supported Go version to 1.9 (#635) This change drops support for Go1.8 and sets the minimum to Go1.9. By making this change we are able to use the same testing steps on *nix and Windows. Travis and Appveyor build scripts have been updated to reflect the said testing step. --- .travis.yml | 6 +++--- CONTRIBUTING.md | 18 +++++------------- appveyor.yml | 2 +- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/.travis.yml b/.travis.yml index 733e8e83c..220880371 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,8 @@ language: go sudo: false go: - - 1.8 - - 1.9 + - "1.9" + - "1.10.x" - tip install: @@ -12,4 +12,4 @@ install: - dep ensure script: - - go test $(go list ./... | grep -v /vendor/) + - go test -cover ./... diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index df933fa71..983bc1389 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ Exercism would be impossible without people like you being willing to spend time ## Dependencies -You'll need Go version 1.8 or higher. Follow the directions on http://golang.org/doc/install +You'll need Go version 1.9 or higher. Follow the directions on http://golang.org/doc/install You will also need `dep`, the Go dependency management tool. Follow the directions on https://golang.github.io/dep/docs/installation.html @@ -19,7 +19,7 @@ If you don't care how and why and just want something that works, follow these s 1. [fork this repo on the GitHub webpage][fork] 1. `go get github.com/exercism/cli/exercism` -1. `cd $GOPATH/src/github.com/exercism/cli` (or `cd %GOPATH%/src/github.com/exercism/cli` on Windows) +1. `cd $GOPATH/src/github.com/exercism/cli` (or `cd %GOPATH%\src\github.com\exercism\cli` on Windows) 1. `git remote rename origin upstream` 1. `git remote add origin git@github.com:/cli.git` 1. `git checkout -b development` @@ -35,20 +35,12 @@ If you care about the details, check out the blog post [Contributing to Open Sou ## Running the Tests -To run the tests locally on Linux or MacOS, use +To run the tests locally ``` -go test $(go list ./... | grep -v vendor) +go test ./... ``` -On Windows, the command is more painful (sorry!): - -``` -for /f "" %G in ('go list ./... ^| find /i /v "/vendor/"') do @go test %G -``` - -As of Go 1.9 this is simplified to `go test ./...`. - ## Manual Testing against Exercism To test your changes while doing everyday Exercism work you @@ -66,7 +58,7 @@ On Windows: - `cd /d %GOPATH%\src\github.com\exercism\cli` - `go build -o testercism.exe exercism\main.go` -- `testercism.exe —help` +- `testercism.exe —h` ### Building for All Platforms diff --git a/appveyor.yml b/appveyor.yml index 6d3991db4..1f3ddc3ca 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,4 +18,4 @@ install: - c:\gopath\bin\dep.exe ensure build_script: - - for /f "" %%G in ('go list github.com/exercism/cli/... ^| find /i /v "/vendor/"') do ( go test %%G & IF ERRORLEVEL == 1 EXIT 1) + - go test -cover ./... From fd4b233d989a00d41ab4050501226778430ae8ac Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 20:05:53 +0100 Subject: [PATCH 172/544] Move error message into constant We need this same error message for the download command. --- cmd/cmd.go | 12 ++++++++++++ cmd/submit.go | 13 +------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index c7f97fe5c..cb82335a9 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -14,3 +14,15 @@ const msgWelcomePleaseConfigure = ` %s configure --token=YOUR_TOKEN ` + +// Running configure without any arguments will attempt to +// set the default workspace. If the default workspace directory +// risks clobbering an existing directory, it will print an +// error message that explains how to proceed. +const msgRerunConfigure = ` + + Please re-run the configure command to define where + to download the exercises. + + %s configure +` diff --git a/cmd/submit.go b/cmd/submit.go index 9e8b21311..05990ec7a 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -58,18 +58,7 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er } if usrCfg.GetString("workspace") == "" { - // Running configure without any arguments will attempt to - // set the default workspace. If the default workspace directory - // risks clobbering an existing directory, it will print an - // error message that explains how to proceed. - msg := ` - - Please re-run the configure command to define where - to download the exercises. - - %s configure - ` - return fmt.Errorf(msg, BinaryName) + return fmt.Errorf(msgRerunConfigure, BinaryName) } for i, arg := range args { From f80a8bdd63e7cc936df749348afd24c4a357cd07 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 20:09:31 +0100 Subject: [PATCH 173/544] Add guard against unconfigured workspace to download If you're going to download an exercise, we need to know where to download it to. This will ask the user to re-run the configure command if the workspace isn't configured. --- cmd/download.go | 3 +++ cmd/download_test.go | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/cmd/download.go b/cmd/download.go index 11650956b..2ab6960ac 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -52,6 +52,9 @@ func runDownload(cfg config.Configuration, flags *pflag.FlagSet, args []string) tokenURL := config.InferSiteURL(usrCfg.GetString("apibaseurl")) + "/my/settings" return fmt.Errorf(msgWelcomePleaseConfigure, tokenURL, BinaryName) } + if usrCfg.GetString("workspace") == "" { + return fmt.Errorf(msgRerunConfigure, BinaryName) + } uuid, err := flags.GetString("uuid") if err != nil { diff --git a/cmd/download_test.go b/cmd/download_test.go index 92106a7c2..1e79bd1b2 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -26,9 +26,23 @@ func TestDownloadWithoutToken(t *testing.T) { } } +func TestDownloadWithoutWorkspace(t *testing.T) { + v := viper.New() + v.Set("token", "abc123") + cfg := config.Configuration{ + UserViperConfig: v, + } + + err := runDownload(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) + if assert.Error(t, err) { + assert.Regexp(t, "re-run the configure", err.Error()) + } +} + func TestDownloadWithoutFlags(t *testing.T) { v := viper.New() v.Set("token", "abc123") + v.Set("workspace", "/home/username") cfg := config.Configuration{ UserViperConfig: v, From 7d1a115d5e2e6225211267f51374008462745124 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 20:13:01 +0100 Subject: [PATCH 174/544] Add guard for missing API URL in download command --- cmd/download.go | 2 +- cmd/download_test.go | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/cmd/download.go b/cmd/download.go index 2ab6960ac..877ab81b6 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -52,7 +52,7 @@ func runDownload(cfg config.Configuration, flags *pflag.FlagSet, args []string) tokenURL := config.InferSiteURL(usrCfg.GetString("apibaseurl")) + "/my/settings" return fmt.Errorf(msgWelcomePleaseConfigure, tokenURL, BinaryName) } - if usrCfg.GetString("workspace") == "" { + if usrCfg.GetString("workspace") == "" || usrCfg.GetString("apibaseurl") == "" { return fmt.Errorf(msgRerunConfigure, BinaryName) } diff --git a/cmd/download_test.go b/cmd/download_test.go index 1e79bd1b2..803ac9501 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -39,10 +39,25 @@ func TestDownloadWithoutWorkspace(t *testing.T) { } } +func TestDownloadWithoutBaseURL(t *testing.T) { + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", "/home/whatever") + cfg := config.Configuration{ + UserViperConfig: v, + } + + err := runDownload(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) + if assert.Error(t, err) { + assert.Regexp(t, "re-run the configure", err.Error()) + } +} + func TestDownloadWithoutFlags(t *testing.T) { v := viper.New() v.Set("token", "abc123") v.Set("workspace", "/home/username") + v.Set("apibaseurl", "http://example.com") cfg := config.Configuration{ UserViperConfig: v, From 10c726bdada8e078b937b1bd2487d31b1cf81ca2 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 18:42:12 -0500 Subject: [PATCH 175/544] Update changelog for 3.x releases --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d66ad0fb1..e9a5965e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,25 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ---------------- ## Next Release +[#642](https://github.com/exercism/cli/pull/642) Add better error messages when configuration is needed before download - [@kytrinyx] +[#641](https://github.com/exercism/cli/pull/641) Fix broken download for uuid flag - [@kytrinyx] +[#618](https://github.com/exercism/cli/pull/618) Fix broken test in Windows build for relative paths - [@nywilken] +[#631](https://github.com/exercism/cli/pull/631) Stop accepting token flag on download command - [@kytrinyx] +[#616](https://github.com/exercism/cli/pull/616) Add shell completion scripts to build artifacts - [@jdsutherland] +[#624](https://github.com/exercism/cli/pull/624) Tweak command documentation to reflect reality - [@kytrinyx] +[#625](https://github.com/exercism/cli/pull/625) Fix wildly excessive whitespace in error messages - [@kytrinyx] * **Your contribution here** +## v3.0.2 (2018-07-13) +* [#622](https://github.com/exercism/cli/pull/622) Fix bug with multi-file submission - [@kytrinyx] + +## v3.0.1 (2018-07-13) +* [#619](https://github.com/exercism/cli/pull/619) Improve error message for successful configuration - [@kytrinyx] + +## v3.0.0 (2018-07-13) + +This is a complete rewrite from the ground up to work against the new https://exercism.io site. + ## v2.4.1 (2017-07-01) * [#385](https://github.com/exercism/cli/pull/385) Fix broken upgrades for Windows - [@Tonkpils] @@ -365,6 +382,7 @@ All changes by [@msgehard] [@harimp]: https://github.com/harimp [@hjljo]: https://github.com/hjljo [@isbadawi]: https://github.com/isbadawi +[@jdsutherland]: https://github.com/jdsutherland [@jgsqware]: https://github.com/jgsqware [@jish]: https://github.com/jish [@jppunnett]: https://github.com/jppunnett From 7ce822f1103dfe6709b38f58ebe32897ef263380 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 14 Jul 2018 18:43:05 -0500 Subject: [PATCH 176/544] Bump to version v3.0.3 --- CHANGELOG.md | 4 +++- cmd/version.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9a5965e2..d3d656fa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ---------------- ## Next Release +* **Your contribution here** + +## v3.0.3 (2018-07-14) [#642](https://github.com/exercism/cli/pull/642) Add better error messages when configuration is needed before download - [@kytrinyx] [#641](https://github.com/exercism/cli/pull/641) Fix broken download for uuid flag - [@kytrinyx] [#618](https://github.com/exercism/cli/pull/618) Fix broken test in Windows build for relative paths - [@nywilken] @@ -12,7 +15,6 @@ The exercism CLI follows [semantic versioning](http://semver.org/). [#616](https://github.com/exercism/cli/pull/616) Add shell completion scripts to build artifacts - [@jdsutherland] [#624](https://github.com/exercism/cli/pull/624) Tweak command documentation to reflect reality - [@kytrinyx] [#625](https://github.com/exercism/cli/pull/625) Fix wildly excessive whitespace in error messages - [@kytrinyx] -* **Your contribution here** ## v3.0.2 (2018-07-13) * [#622](https://github.com/exercism/cli/pull/622) Fix bug with multi-file submission - [@kytrinyx] diff --git a/cmd/version.go b/cmd/version.go index 698a8acbb..78e3f9d97 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.0.2" +const Version = "3.0.3" // checkLatest flag for version command. var checkLatest bool From 53bb62de80806f504109901e95e931ea956884df Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 15 Jul 2018 12:45:24 -0600 Subject: [PATCH 177/544] Add proper error message when solution metadata is not found --- cmd/cmd.go | 7 +++++++ cmd/submit.go | 13 +++---------- cmd/submit_test.go | 26 ++++++++++++++++++++++++++ workspace/workspace.go | 9 ++++++++- 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index cb82335a9..a87bf96bd 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -26,3 +26,10 @@ const msgRerunConfigure = ` %s configure ` + +const msgMissingMetadata = ` + + The exercise you are submitting doesn't have the necessary metadata. + Please see https://exercism.io/cli-v1-to-v2 for instructions on how to fix it. + +` diff --git a/cmd/submit.go b/cmd/submit.go index 05990ec7a..543afb9c3 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -109,6 +109,9 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er for _, arg := range args { dir, err := ws.SolutionDir(arg) if err != nil { + if workspace.IsMissingMetadata(err) { + return errors.New(msgMissingMetadata) + } return err } if exerciseDir != "" && dir != exerciseDir { @@ -132,16 +135,6 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er if err != nil { return err } - if len(sx) == 0 { - // TODO: add test - msg := ` - - The exercise you are submitting doesn't have the necessary metadata. - Please see https://exercism.io/cli-v1-to-v2 for instructions on how to fix it. - - ` - return errors.New(msg) - } if len(sx) > 1 { msg := ` diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 3b9650600..021c7409e 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -68,6 +68,32 @@ func TestSubmitNonExistentFile(t *testing.T) { assert.Regexp(t, "cannot be found", err.Error()) } +func TestSubmitExerciseWithoutSolutionMetadataFile(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "no-metadata-file") + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(dir, os.FileMode(0755)) + + file := filepath.Join(dir, "file.txt") + err = ioutil.WriteFile(file, []byte("This is a file."), os.FileMode(0755)) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + + cfg := config.Configuration{ + Persister: config.InMemoryPersister{}, + Dir: tmpDir, + UserViperConfig: v, + } + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file}) + assert.Error(t, err) + assert.Regexp(t, "doesn't have the necessary metadata", err.Error()) +} + func TestSubmitFilesAndDir(t *testing.T) { tmpDir, err := ioutil.TempDir("", "submit-no-such-file") assert.NoError(t, err) diff --git a/workspace/workspace.go b/workspace/workspace.go index 3952c5482..c05a0c740 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -9,6 +9,13 @@ import ( "strings" ) +var errMissingMetadata = errors.New("no solution metadata file found") + +// IsMissingMetadata verifies the type of error. +func IsMissingMetadata(err error) bool { + return err == errMissingMetadata +} + var rgxSerialSuffix = regexp.MustCompile(`-\d*$`) // Workspace represents a user's Exercism workspace. @@ -186,7 +193,7 @@ func (ws Workspace) SolutionDir(s string) (string, error) { path := s for { if path == ws.Dir { - return "", errors.New("couldn't find it") + return "", errMissingMetadata } if _, err := os.Lstat(path); os.IsNotExist(err) { return "", err From cfd008a4237d49d86d4920fc28062fcfba3f8a98 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 15 Jul 2018 13:00:51 -0600 Subject: [PATCH 178/544] Bump to v3.0.4 --- CHANGELOG.md | 3 +++ cmd/version.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3d656fa1..ade69e984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ## Next Release * **Your contribution here** +## v3.0.4 (2018-07-15) +[#644](https://github.com/exercism/cli/pull/644) Add better error messages when solution metadata is missing - [@kytrinyx] + ## v3.0.3 (2018-07-14) [#642](https://github.com/exercism/cli/pull/642) Add better error messages when configuration is needed before download - [@kytrinyx] [#641](https://github.com/exercism/cli/pull/641) Fix broken download for uuid flag - [@kytrinyx] diff --git a/cmd/version.go b/cmd/version.go index 78e3f9d97..c23280971 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.0.3" +const Version = "3.0.4" // checkLatest flag for version command. var checkLatest bool From 62df39d3045ed475dfe06c55024f30f9bc46c7d9 Mon Sep 17 00:00:00 2001 From: Wilken Rivera Date: Mon, 16 Jul 2018 22:41:07 -0400 Subject: [PATCH 179/544] Fix issue with upgrading on Windows (#646) This changes ensures that the upgrade process does not try to replace the exercism binary with a non executable file. There is probably a more extensive check we can do to ensure we have the correct file. I did not choose to use the full name as Configlet is also using the cli pkg for upgrading its binary. --- cli/cli.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cli/cli.go b/cli/cli.go index c374fd040..b20e1ebbe 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -164,6 +164,10 @@ func extractBinary(source *bytes.Reader, os string) (binary io.ReadCloser, err e } for _, f := range zr.File { + info := f.FileInfo() + if info.IsDir() || !strings.HasSuffix(f.Name, ".exe") { + continue + } return f.Open() } } else { From 8e8695b0a3f28447fa1c02723276fbb64280e9e6 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 17 Jul 2018 18:29:50 -0600 Subject: [PATCH 180/544] Bump version to v3.0.5 --- CHANGELOG.md | 19 +++++++++++-------- cmd/version.go | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ade69e984..6372d9cd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,17 +7,20 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ## Next Release * **Your contribution here** +## v3.0.5 (2018-07-17) +* [#646](https://github.com/exercism/cli/pull/646) Fix issue with upgrading on Windows - [@nywilken] + ## v3.0.4 (2018-07-15) -[#644](https://github.com/exercism/cli/pull/644) Add better error messages when solution metadata is missing - [@kytrinyx] +* [#644](https://github.com/exercism/cli/pull/644) Add better error messages when solution metadata is missing - [@kytrinyx] ## v3.0.3 (2018-07-14) -[#642](https://github.com/exercism/cli/pull/642) Add better error messages when configuration is needed before download - [@kytrinyx] -[#641](https://github.com/exercism/cli/pull/641) Fix broken download for uuid flag - [@kytrinyx] -[#618](https://github.com/exercism/cli/pull/618) Fix broken test in Windows build for relative paths - [@nywilken] -[#631](https://github.com/exercism/cli/pull/631) Stop accepting token flag on download command - [@kytrinyx] -[#616](https://github.com/exercism/cli/pull/616) Add shell completion scripts to build artifacts - [@jdsutherland] -[#624](https://github.com/exercism/cli/pull/624) Tweak command documentation to reflect reality - [@kytrinyx] -[#625](https://github.com/exercism/cli/pull/625) Fix wildly excessive whitespace in error messages - [@kytrinyx] +* [#642](https://github.com/exercism/cli/pull/642) Add better error messages when configuration is needed before download - [@kytrinyx] +* [#641](https://github.com/exercism/cli/pull/641) Fix broken download for uuid flag - [@kytrinyx] +* [#618](https://github.com/exercism/cli/pull/618) Fix broken test in Windows build for relative paths - [@nywilken] +* [#631](https://github.com/exercism/cli/pull/631) Stop accepting token flag on download command - [@kytrinyx] +* [#616](https://github.com/exercism/cli/pull/616) Add shell completion scripts to build artifacts - [@jdsutherland] +* [#624](https://github.com/exercism/cli/pull/624) Tweak command documentation to reflect reality - [@kytrinyx] +* [#625](https://github.com/exercism/cli/pull/625) Fix wildly excessive whitespace in error messages - [@kytrinyx] ## v3.0.2 (2018-07-13) * [#622](https://github.com/exercism/cli/pull/622) Fix bug with multi-file submission - [@kytrinyx] diff --git a/cmd/version.go b/cmd/version.go index c23280971..617f2da07 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.0.4" +const Version = "3.0.5" // checkLatest flag for version command. var checkLatest bool From 6f458000cded661c34ed0c821fecb276a408c1c4 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 17 Jul 2018 18:58:31 -0600 Subject: [PATCH 181/544] Delete stray test suffix This was never supposed to have gone into master, and I thought I had deleted it. Alas. --- cmd/download_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index 803ac9501..1645fb87a 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -72,7 +72,7 @@ func TestDownloadWithoutFlags(t *testing.T) { } } -func TestDownloadTHISONE(t *testing.T) { +func TestDownload(t *testing.T) { oldOut := Out oldErr := Err Out = ioutil.Discard From 2fb659639a72aa3a42d64f4a993359f8f60d4647 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 19 Jul 2018 07:30:23 -0600 Subject: [PATCH 182/544] Delete unnecessary flags in submit command (#654) Since we're operating with explicitly passed files, we detect the solution directory by looking for the metadata file, and then use that to determine what we're submitting. The flags were left over from a previous implementation where we were using them to determine what the solution directory was. --- cmd/submit.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 543afb9c3..2ee7a8350 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -255,16 +255,6 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er return nil } -func initSubmitCmd() { - setupSubmitFlags(submitCmd.Flags()) -} - -func setupSubmitFlags(flags *pflag.FlagSet) { - flags.StringP("track", "t", "", "the track ID") - flags.StringP("exercise", "e", "", "the exercise ID") - flags.StringSliceP("files", "f", make([]string, 0), "files to submit") -} - func init() { RootCmd.AddCommand(submitCmd) } From 0762dfe2790a46f8ae3e6e8f53e016b41bdae20e Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 19 Jul 2018 07:34:15 -0600 Subject: [PATCH 183/544] Fold submit relative path back into main submit tests (#653) This was split out to a conditional windows build because I was stumped on a failing test. Once nywilken fixed it, I realized we could bring it all back into the same file. --- cmd/submit_relative_path_test.go | 59 ----------------------- cmd/submit_relative_path_windows_test.go | 60 ------------------------ cmd/submit_test.go | 44 +++++++++++++++++ 3 files changed, 44 insertions(+), 119 deletions(-) delete mode 100644 cmd/submit_relative_path_test.go delete mode 100644 cmd/submit_relative_path_windows_test.go diff --git a/cmd/submit_relative_path_test.go b/cmd/submit_relative_path_test.go deleted file mode 100644 index f8d62cbe8..000000000 --- a/cmd/submit_relative_path_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// +build !windows - -package cmd - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/exercism/cli/config" - "github.com/spf13/pflag" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" -) - -func TestSubmitRelativePath(t *testing.T) { - oldOut := Out - oldErr := Err - Out = ioutil.Discard - Err = ioutil.Discard - defer func() { - Out = oldOut - Err = oldErr - }() - // The fake endpoint will populate this when it receives the call from the command. - submittedFiles := map[string]string{} - ts := fakeSubmitServer(t, submittedFiles) - defer ts.Close() - - tmpDir, err := ioutil.TempDir("", "relative-path") - assert.NoError(t, err) - - dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") - os.MkdirAll(dir, os.FileMode(0755)) - - writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") - - v := viper.New() - v.Set("token", "abc123") - v.Set("workspace", tmpDir) - v.Set("apibaseurl", ts.URL) - - cfg := config.Configuration{ - Persister: config.InMemoryPersister{}, - UserViperConfig: v, - } - - err = ioutil.WriteFile(filepath.Join(dir, "file.txt"), []byte("This is a file."), os.FileMode(0755)) - - err = os.Chdir(dir) - assert.NoError(t, err) - - err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{"file.txt"}) - assert.NoError(t, err) - - assert.Equal(t, 1, len(submittedFiles)) - assert.Equal(t, "This is a file.", submittedFiles["/file.txt"]) -} diff --git a/cmd/submit_relative_path_windows_test.go b/cmd/submit_relative_path_windows_test.go deleted file mode 100644 index 9a0f2a978..000000000 --- a/cmd/submit_relative_path_windows_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package cmd - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/exercism/cli/config" - "github.com/spf13/pflag" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" -) - -func TestSubmitRelativePath(t *testing.T) { - //t.Skip("The Windows build is failing and needs to be debugged.\nSee https://ci.appveyor.com/project/kytrinyx/cli/build/110") - - oldOut := Out - oldErr := Err - Out = ioutil.Discard - Err = ioutil.Discard - defer func() { - Out = oldOut - Err = oldErr - }() - // The fake endpoint will populate this when it receives the call from the command. - submittedFiles := map[string]string{} - ts := fakeSubmitServer(t, submittedFiles) - defer ts.Close() - - tmpDir, err := ioutil.TempDir("", "relative-path") - assert.NoError(t, err) - defer os.RemoveAll(tmpDir) - - dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") - os.MkdirAll(dir, os.FileMode(0755)) - - writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") - - v := viper.New() - v.Set("token", "abc123") - v.Set("workspace", tmpDir) - v.Set("apibaseurl", ts.URL) - - cfg := config.Configuration{ - Persister: config.InMemoryPersister{}, - UserViperConfig: v, - } - - err = ioutil.WriteFile(filepath.Join(dir, "file.txt"), []byte("This is a file."), os.FileMode(0755)) - - err = os.Chdir(dir) - assert.NoError(t, err) - - err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{"file.txt"}) - assert.NoError(t, err) - - assert.Equal(t, 1, len(submittedFiles)) - assert.Equal(t, "This is a file.", submittedFiles["\\file.txt"]) -} diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 021c7409e..c134375d9 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -320,6 +320,50 @@ func fakeSubmitServer(t *testing.T, submittedFiles map[string]string) *httptest. return httptest.NewServer(handler) } +func TestSubmitRelativePath(t *testing.T) { + oldOut := Out + oldErr := Err + Out = ioutil.Discard + Err = ioutil.Discard + defer func() { + Out = oldOut + Err = oldErr + }() + // The fake endpoint will populate this when it receives the call from the command. + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := ioutil.TempDir("", "relative-path") + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(dir, os.FileMode(0755)) + + writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Configuration{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + } + + err = ioutil.WriteFile(filepath.Join(dir, "file.txt"), []byte("This is a file."), os.FileMode(0755)) + + err = os.Chdir(dir) + assert.NoError(t, err) + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{"file.txt"}) + assert.NoError(t, err) + + assert.Equal(t, 1, len(submittedFiles)) + assert.Equal(t, "This is a file.", submittedFiles[string(os.PathSeparator)+"file.txt"]) +} + func writeFakeSolution(t *testing.T, dir, trackID, exerciseSlug string) { solution := &workspace.Solution{ ID: "bogus-solution-uuid", From ed8fbf4422cbe8a6e3d282d25f33b9e03cac383f Mon Sep 17 00:00:00 2001 From: sue spence Date: Thu, 19 Jul 2018 22:48:50 +0100 Subject: [PATCH 184/544] Replace accidental \ in path to /. (#655) * Replace accidental \ in path to /. * Remove extra directory level in mv command --- shell/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/README.md b/shell/README.md index b5ee140a2..75e707366 100644 --- a/shell/README.md +++ b/shell/README.md @@ -6,7 +6,7 @@ Unpack the archive relevant to your machine and place in $PATH ### Bash mkdir -p ~/.config/exercism - mv ../shell/exercism_completion.bash ~/.config/exercism/exercism\exercism_completion.bash + mv ../shell/exercism_completion.bash ~/.config/exercism/exercism_completion.bash Load the completion in your `.bashrc`, `.bash_profile` or `.profile` by adding the following snippet: From c4ccd23a27bb0af8284568d4fa66a09503df13f4 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 19 Jul 2018 18:17:43 -0600 Subject: [PATCH 185/544] Use explicit struct fields in download test We're going to expand the test case to include more fields, and this reformats it so it doesn't become impossible to read once we do. --- cmd/download_test.go | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index 1645fb87a..d6186ed89 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -87,9 +87,24 @@ func TestDownload(t *testing.T) { expectedDir string flag, flagValue string }{ - {requestorSelf, "", "exercise", "bogus-exercise"}, - {requestorSelf, "", "uuid", "bogus-id"}, - {requestorOther, filepath.Join("users", "alice"), "uuid", "bogus-id"}, + { + requestor: requestorSelf, + expectedDir: "", + flag: "exercise", + flagValue: "bogus-exercise", + }, + { + requestor: requestorSelf, + expectedDir: "", + flag: "uuid", + flagValue: "bogus-id", + }, + { + requestor: requestorOther, + expectedDir: filepath.Join("users", "alice"), + flag: "uuid", + flagValue: "bogus-id", + }, } for _, tc := range testCases { From 7ea16b1140de392e96cb434fb48cebd25f9f3785 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 22 Jul 2018 06:01:33 -0600 Subject: [PATCH 186/544] Tweak flag setup in download test (#660) Use a map instead of two independent strings for the flag name and flag value. This is cleaner, and also will let us set more than one flag. --- cmd/download_test.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index d6186ed89..923f24cb2 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -83,27 +83,24 @@ func TestDownload(t *testing.T) { }() testCases := []struct { - requestor string - expectedDir string - flag, flagValue string + requestor string + expectedDir string + flags map[string]string }{ { requestor: requestorSelf, expectedDir: "", - flag: "exercise", - flagValue: "bogus-exercise", + flags: map[string]string{"exercise": "bogus-exercise"}, }, { requestor: requestorSelf, expectedDir: "", - flag: "uuid", - flagValue: "bogus-id", + flags: map[string]string{"uuid": "bogus-id"}, }, { requestor: requestorOther, expectedDir: filepath.Join("users", "alice"), - flag: "uuid", - flagValue: "bogus-id", + flags: map[string]string{"uuid": "bogus-id"}, }, } @@ -124,7 +121,9 @@ func TestDownload(t *testing.T) { } flags := pflag.NewFlagSet("fake", pflag.PanicOnError) setupDownloadFlags(flags) - flags.Set(tc.flag, tc.flagValue) + for name, value := range tc.flags { + flags.Set(name, value) + } err = runDownload(cfg, flags, []string{}) assert.NoError(t, err) From 625008c5c8455d3c8fcf471375538007aaaae143 Mon Sep 17 00:00:00 2001 From: Ryan Cook Date: Sun, 22 Jul 2018 21:30:33 -0600 Subject: [PATCH 187/544] Update Bash completion script with strategy found in Git's Bash completion library which ensures that default tab completion still functions. --- shell/exercism_completion.bash | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shell/exercism_completion.bash b/shell/exercism_completion.bash index aa383d4b8..f4a17e808 100644 --- a/shell/exercism_completion.bash +++ b/shell/exercism_completion.bash @@ -68,4 +68,5 @@ _exercism () { return 0 } -complete -F _exercism exercism +complete -o bashdefault -o default -o nospace -F _exercism exercism 2>/dev/null \ + || complete -o default -o nospace -F _exercism exercism From e4ce40e8803edd0d4743311ed99386fc012f8c3f Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 17 Jul 2018 18:51:08 -0600 Subject: [PATCH 188/544] Ensure welcome message has full link to settings If the base API URL is unconfigured, then we end up with a stray '/my/settings' link for people to go to. This assumes that if you've not configured the base URL, the default will work just fine. This fixes both the download and submit commands. --- cmd/download.go | 6 +++++- cmd/download_test.go | 3 +++ cmd/submit.go | 6 +++++- cmd/submit_test.go | 3 ++- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index 877ab81b6..1a8bd3295 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -49,7 +49,11 @@ Download other people's solutions by providing the UUID. func runDownload(cfg config.Configuration, flags *pflag.FlagSet, args []string) error { usrCfg := cfg.UserViperConfig if usrCfg.GetString("token") == "" { - tokenURL := config.InferSiteURL(usrCfg.GetString("apibaseurl")) + "/my/settings" + apiURL := usrCfg.GetString("apibaseurl") + if apiURL == "" { + apiURL = cfg.DefaultBaseURL + } + tokenURL := fmt.Sprintf("%s/my/settings", config.InferSiteURL(apiURL)) return fmt.Errorf(msgWelcomePleaseConfigure, tokenURL, BinaryName) } if usrCfg.GetString("workspace") == "" || usrCfg.GetString("apibaseurl") == "" { diff --git a/cmd/download_test.go b/cmd/download_test.go index 923f24cb2..91676e565 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -18,11 +18,14 @@ import ( func TestDownloadWithoutToken(t *testing.T) { cfg := config.Configuration{ UserViperConfig: viper.New(), + DefaultBaseURL: "http://fake.exercism.io", } err := runDownload(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) if assert.Error(t, err) { assert.Regexp(t, "Welcome to Exercism", err.Error()) + // It uses the default base API url to infer the host + assert.Regexp(t, "fake.exercism.io/my/settings", err.Error()) } } diff --git a/cmd/submit.go b/cmd/submit.go index 2ee7a8350..1c07137c2 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -53,7 +53,11 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er usrCfg := cfg.UserViperConfig if usrCfg.GetString("token") == "" { - tokenURL := config.InferSiteURL(usrCfg.GetString("apibaseurl")) + "/my/settings" + apiURL := usrCfg.GetString("apibaseurl") + if apiURL == "" { + apiURL = cfg.DefaultBaseURL + } + tokenURL := fmt.Sprintf("%s/my/settings", config.InferSiteURL(apiURL)) return fmt.Errorf(msgWelcomePleaseConfigure, tokenURL, BinaryName) } diff --git a/cmd/submit_test.go b/cmd/submit_test.go index c134375d9..c0deb8bf4 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -19,11 +19,12 @@ func TestSubmitWithoutToken(t *testing.T) { cfg := config.Configuration{ Persister: config.InMemoryPersister{}, UserViperConfig: viper.New(), - DefaultBaseURL: "http://example.com", + DefaultBaseURL: "http://unconfigured.exercism.io", } err := runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) assert.Regexp(t, "Welcome to Exercism", err.Error()) + assert.Regexp(t, "http://unconfigured.exercism.io/my/settings", err.Error()) } func TestSubmitWithoutWorkspace(t *testing.T) { From 3e98e16fee431e8bb9440bccf4b4baa78c5532ca Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 19 Jul 2018 20:36:55 -0600 Subject: [PATCH 189/544] Infer site URL more cleverly If we're trying to guess at the site URL and the CLI is unconfigured, then fall back to using the default API URL to guess from. --- config/config.go | 3 +++ config/config_test.go | 1 + 2 files changed, 4 insertions(+) diff --git a/config/config.go b/config/config.go index b7a07f190..10c968f3f 100644 --- a/config/config.go +++ b/config/config.go @@ -69,6 +69,9 @@ func ensureDir(f filer) error { // InferSiteURL guesses what the website URL is. // The basis for the guess is which API we're submitting to. func InferSiteURL(apiURL string) string { + if apiURL == "" { + apiURL = defaultBaseURL + } if apiURL == "https://api.exercism.io/v1" { return "https://exercism.io" } diff --git a/config/config_test.go b/config/config_test.go index 44808d50c..6304bf67e 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -104,6 +104,7 @@ func TestInferSiteURL(t *testing.T) { {"https://v2.exercism.io/api/v1", "https://v2.exercism.io"}, {"https://mentors-beta.exercism.io/api/v1", "https://mentors-beta.exercism.io"}, {"http://localhost:3000/api/v1", "http://localhost:3000"}, + {"", "https://exercism.io"}, // use the default {"http://whatever", "http://whatever"}, // you're on your own, pal } From 1efeefe3ec33ab5338077125bb21ed60e60e0528 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 19 Jul 2018 20:42:48 -0600 Subject: [PATCH 190/544] Rely on default base url in welcome message The InferSiteURL function will fall back to the default base URL if no URL is provided. The download and submit commands can therefore stop jumping through hoops to provide one in the case where the client is unconfigured. --- cmd/download.go | 3 --- cmd/download_test.go | 3 +-- cmd/submit.go | 3 --- cmd/submit_test.go | 3 +-- 4 files changed, 2 insertions(+), 10 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index 1a8bd3295..4ecbf4a35 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -50,9 +50,6 @@ func runDownload(cfg config.Configuration, flags *pflag.FlagSet, args []string) usrCfg := cfg.UserViperConfig if usrCfg.GetString("token") == "" { apiURL := usrCfg.GetString("apibaseurl") - if apiURL == "" { - apiURL = cfg.DefaultBaseURL - } tokenURL := fmt.Sprintf("%s/my/settings", config.InferSiteURL(apiURL)) return fmt.Errorf(msgWelcomePleaseConfigure, tokenURL, BinaryName) } diff --git a/cmd/download_test.go b/cmd/download_test.go index 91676e565..f6749203f 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -18,14 +18,13 @@ import ( func TestDownloadWithoutToken(t *testing.T) { cfg := config.Configuration{ UserViperConfig: viper.New(), - DefaultBaseURL: "http://fake.exercism.io", } err := runDownload(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) if assert.Error(t, err) { assert.Regexp(t, "Welcome to Exercism", err.Error()) // It uses the default base API url to infer the host - assert.Regexp(t, "fake.exercism.io/my/settings", err.Error()) + assert.Regexp(t, "exercism.io/my/settings", err.Error()) } } diff --git a/cmd/submit.go b/cmd/submit.go index 1c07137c2..a6ec01953 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -54,9 +54,6 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er if usrCfg.GetString("token") == "" { apiURL := usrCfg.GetString("apibaseurl") - if apiURL == "" { - apiURL = cfg.DefaultBaseURL - } tokenURL := fmt.Sprintf("%s/my/settings", config.InferSiteURL(apiURL)) return fmt.Errorf(msgWelcomePleaseConfigure, tokenURL, BinaryName) } diff --git a/cmd/submit_test.go b/cmd/submit_test.go index c0deb8bf4..0eb68a5fc 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -19,12 +19,11 @@ func TestSubmitWithoutToken(t *testing.T) { cfg := config.Configuration{ Persister: config.InMemoryPersister{}, UserViperConfig: viper.New(), - DefaultBaseURL: "http://unconfigured.exercism.io", } err := runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) assert.Regexp(t, "Welcome to Exercism", err.Error()) - assert.Regexp(t, "http://unconfigured.exercism.io/my/settings", err.Error()) + assert.Regexp(t, "exercism.io/my/settings", err.Error()) } func TestSubmitWithoutWorkspace(t *testing.T) { From d4d55b8fbbb7412d40c3c9045447286a00ccbf1c Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 19 Jul 2018 20:49:45 -0600 Subject: [PATCH 191/544] Provide helper method for settings URL --- cmd/download.go | 4 +--- cmd/submit.go | 4 +--- config/config.go | 5 +++++ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index 4ecbf4a35..d02f08066 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -49,9 +49,7 @@ Download other people's solutions by providing the UUID. func runDownload(cfg config.Configuration, flags *pflag.FlagSet, args []string) error { usrCfg := cfg.UserViperConfig if usrCfg.GetString("token") == "" { - apiURL := usrCfg.GetString("apibaseurl") - tokenURL := fmt.Sprintf("%s/my/settings", config.InferSiteURL(apiURL)) - return fmt.Errorf(msgWelcomePleaseConfigure, tokenURL, BinaryName) + return fmt.Errorf(msgWelcomePleaseConfigure, config.SettingsURL(usrCfg.GetString("apibaseurl")), BinaryName) } if usrCfg.GetString("workspace") == "" || usrCfg.GetString("apibaseurl") == "" { return fmt.Errorf(msgRerunConfigure, BinaryName) diff --git a/cmd/submit.go b/cmd/submit.go index a6ec01953..930311cee 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -53,9 +53,7 @@ func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) er usrCfg := cfg.UserViperConfig if usrCfg.GetString("token") == "" { - apiURL := usrCfg.GetString("apibaseurl") - tokenURL := fmt.Sprintf("%s/my/settings", config.InferSiteURL(apiURL)) - return fmt.Errorf(msgWelcomePleaseConfigure, tokenURL, BinaryName) + return fmt.Errorf(msgWelcomePleaseConfigure, config.SettingsURL(usrCfg.GetString("apibaseurl")), BinaryName) } if usrCfg.GetString("workspace") == "" { diff --git a/config/config.go b/config/config.go index 10c968f3f..7767c9f3e 100644 --- a/config/config.go +++ b/config/config.go @@ -78,3 +78,8 @@ func InferSiteURL(apiURL string) string { re := regexp.MustCompile("^(https?://[^/]*).*") return re.ReplaceAllString(apiURL, "$1") } + +// SettingsURL provides a link to where the user can find their API token. +func SettingsURL(apiURL string) string { + return fmt.Sprintf("%s%s", InferSiteURL(apiURL), "/my/settings") +} From 5d367aa9c248a558c093d331b16d413e194802f8 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 23 Jul 2018 12:23:55 -0700 Subject: [PATCH 192/544] Update configure command to use SettingsURL --- cmd/configure.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/cmd/configure.go b/cmd/configure.go index 7840962b2..8a6b19221 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -60,14 +60,8 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro // If the command is run 'bare' and we have no token, // explain how to set the token. if flags.NFlag() == 0 && cfg.GetString("token") == "" { - baseURL := cfg.GetString("apibaseurl") - if baseURL != "" { - // If we have a base URL, then give the exact link. - tokenURL := config.InferSiteURL(baseURL) + "/my/settings" - return fmt.Errorf("There is no token configured. Find your token on %s, and call this command again with --token=.", tokenURL) - } - // If we don't, then do our best. - return fmt.Errorf("There is no token configured. Find your token in your settings on the website, and call this command again with --token=.") + tokenURL := config.SettingsURL(cfg.GetString("apibaseurl")) + return fmt.Errorf("There is no token configured. Find your token on %s, and call this command again with --token=.", tokenURL) } // Determine the base API URL. @@ -114,8 +108,7 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro token = cfg.GetString("token") } - // Infer the URL where the token can be found. - tokenURL := config.InferSiteURL(cfg.GetString("apibaseurl")) + "/my/settings" + tokenURL := config.SettingsURL(cfg.GetString("apibaseurl")) // If we don't have a token then explain how to set it and bail. if token == "" { From f81bc67d482defdd39f82bf25c8c57b7c6b23f18 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 23 Jul 2018 16:09:19 -0600 Subject: [PATCH 193/544] Move assertion for solution metadata out of helper (#659) * Move assertion for solution metadata out of helper The solution metadata varies on a test-by-test basis. I don't like hiding that away in a helper. * Delete tmpdir after download test completes * Compact the JSON in the download test for readability --- cmd/download_test.go | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index f6749203f..965ff5d41 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -1,6 +1,8 @@ package cmd import ( + "bytes" + "encoding/json" "fmt" "io/ioutil" "net/http" @@ -108,6 +110,7 @@ func TestDownload(t *testing.T) { for _, tc := range testCases { tmpDir, err := ioutil.TempDir("", "download-cmd") + defer os.RemoveAll(tmpDir) assert.NoError(t, err) ts := fakeDownloadServer(tc.requestor) @@ -130,7 +133,25 @@ func TestDownload(t *testing.T) { err = runDownload(cfg, flags, []string{}) assert.NoError(t, err) - assertDownloadedCorrectFiles(t, filepath.Join(tmpDir, tc.expectedDir), tc.requestor) + targetDir := filepath.Join(tmpDir, tc.expectedDir) + assertDownloadedCorrectFiles(t, targetDir, tc.requestor) + + metadata := `{ + "track": "bogus-track", + "exercise":"bogus-exercise", + "id":"bogus-id", + "url":"", + "handle":"alice", + "is_requester":%s, + "auto_approve":false + }` + metadata = fmt.Sprintf(metadata, tc.requestor) + metadata = compact(t, metadata) + + path := filepath.Join(targetDir, "bogus-track", "bogus-exercise", ".solution.json") + b, err := ioutil.ReadFile(path) + assert.NoError(t, err) + assert.Equal(t, metadata, string(b), "the solution metadata file") } } @@ -165,7 +186,6 @@ func fakeDownloadServer(requestor string) *httptest.Server { } func assertDownloadedCorrectFiles(t *testing.T, targetDir, requestor string) { - metadata := `{"track":"bogus-track","exercise":"bogus-exercise","id":"bogus-id","url":"","handle":"alice","is_requester":%s,"auto_approve":false}` expectedFiles := []struct { desc string path string @@ -181,11 +201,6 @@ func assertDownloadedCorrectFiles(t *testing.T, targetDir, requestor string) { path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "subdir", "file-2.txt"), contents: "this is file 2", }, - { - desc: "the solution metadata file", - path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", ".solution.json"), - contents: fmt.Sprintf(metadata, requestor), - }, } for _, file := range expectedFiles { @@ -233,3 +248,10 @@ const payloadTemplate = ` } } ` + +func compact(t *testing.T, s string) string { + buffer := new(bytes.Buffer) + err := json.Compact(buffer, []byte(s)) + assert.NoError(t, err) + return buffer.String() +} From 8572e980e811aff3249d980fe04644a51ee5944c Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 23 Jul 2018 15:37:56 -0700 Subject: [PATCH 194/544] Don't pass files to payload template in download test The fake download server hard-codes three files. We were passing these three files into the fake payload, which doesn't really make sense. --- cmd/download_test.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index 965ff5d41..fbb315bb3 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -159,22 +159,19 @@ func fakeDownloadServer(requestor string) *httptest.Server { mux := http.NewServeMux() server := httptest.NewServer(mux) - path1 := "file-1.txt" - mux.HandleFunc("/"+path1, func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/file-1.txt", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "this is file 1") }) - path2 := "subdir/file-2.txt" - mux.HandleFunc("/"+path2, func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/subdir/file-2.txt", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "this is file 2") }) - path3 := "file-3.txt" - mux.HandleFunc("/"+path3, func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/file-3.txt", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "") }) - payloadBody := fmt.Sprintf(payloadTemplate, requestor, server.URL+"/", path1, path2, path3) + payloadBody := fmt.Sprintf(payloadTemplate, requestor, server.URL+"/") mux.HandleFunc("/solutions/latest", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, payloadBody) }) @@ -238,9 +235,9 @@ const payloadTemplate = ` }, "file_download_base_url": "%s", "files": [ - "%s", - "%s", - "%s" + "/file-1.txt", + "/subdir/file-2.txt", + "/file-3.txt" ], "iteration": { "submitted_at": "2017-08-21t10:11:12.130z" From 202ce7601ad6901c1484bb6e1877fde6c6dce2d5 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 24 Jul 2018 16:09:07 -0700 Subject: [PATCH 195/544] Remove unnecessary argument to helper method --- cmd/download_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index fbb315bb3..2a4b34dd2 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -134,7 +134,7 @@ func TestDownload(t *testing.T) { assert.NoError(t, err) targetDir := filepath.Join(tmpDir, tc.expectedDir) - assertDownloadedCorrectFiles(t, targetDir, tc.requestor) + assertDownloadedCorrectFiles(t, targetDir) metadata := `{ "track": "bogus-track", @@ -182,7 +182,7 @@ func fakeDownloadServer(requestor string) *httptest.Server { return server } -func assertDownloadedCorrectFiles(t *testing.T, targetDir, requestor string) { +func assertDownloadedCorrectFiles(t *testing.T, targetDir string) { expectedFiles := []struct { desc string path string From 9afb51b242aed268dcb5c092b908e92348fbeac5 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 24 Jul 2018 16:26:43 -0700 Subject: [PATCH 196/544] Add debug as alias for troubleshoot (#669) A lot of exercise READMEs have instructions to run the 'debug' command if you get stuck. E.g. every exercise in Python: https://github.com/exercism/python/blob/a83f53697a1a213967838a78b86f2b358727d76f/exercises/etl/README.md#submitting-exercises We can update these, however in the website we pin people's solution to the HEAD SHA of the track repo at the time when they unlock the solution. So only people who have not already unlocked an exercise with the incorrect command would get the update. Adding 'debug' as an alias makes this 'just work'. --- cmd/troubleshoot.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go index 3e22920c4..c491108a6 100644 --- a/cmd/troubleshoot.go +++ b/cmd/troubleshoot.go @@ -22,7 +22,7 @@ var fullAPIKey bool // troubleshootCmd does a diagnostic self-check. var troubleshootCmd = &cobra.Command{ Use: "troubleshoot", - Aliases: []string{"t"}, + Aliases: []string{"t", "debug"}, Short: "Troubleshoot does a diagnostic self-check.", Long: `Provides output to help with troubleshooting. From 0c3ac4d3d6eb086ee278aef43fc359feac8c0261 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 24 Jul 2018 16:11:19 -0700 Subject: [PATCH 197/544] Check downloaded metadata by verifying fields We don't need to check the entire downloaded JSON, we just want to make sure it got downloaded and has the crucial bits of data that we passed to the server. --- cmd/download_test.go | 42 +++++++++++++----------------------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index 2a4b34dd2..e9aca175c 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -1,7 +1,6 @@ package cmd import ( - "bytes" "encoding/json" "fmt" "io/ioutil" @@ -9,9 +8,11 @@ import ( "net/http/httptest" "os" "path/filepath" + "strconv" "testing" "github.com/exercism/cli/config" + "github.com/exercism/cli/workspace" "github.com/spf13/pflag" "github.com/spf13/viper" "github.com/stretchr/testify/assert" @@ -87,22 +88,22 @@ func TestDownload(t *testing.T) { }() testCases := []struct { - requestor string + requester bool expectedDir string flags map[string]string }{ { - requestor: requestorSelf, + requester: true, expectedDir: "", flags: map[string]string{"exercise": "bogus-exercise"}, }, { - requestor: requestorSelf, + requester: true, expectedDir: "", flags: map[string]string{"uuid": "bogus-id"}, }, { - requestor: requestorOther, + requester: false, expectedDir: filepath.Join("users", "alice"), flags: map[string]string{"uuid": "bogus-id"}, }, @@ -113,7 +114,7 @@ func TestDownload(t *testing.T) { defer os.RemoveAll(tmpDir) assert.NoError(t, err) - ts := fakeDownloadServer(tc.requestor) + ts := fakeDownloadServer(strconv.FormatBool(tc.requester)) defer ts.Close() v := viper.New() @@ -136,22 +137,15 @@ func TestDownload(t *testing.T) { targetDir := filepath.Join(tmpDir, tc.expectedDir) assertDownloadedCorrectFiles(t, targetDir) - metadata := `{ - "track": "bogus-track", - "exercise":"bogus-exercise", - "id":"bogus-id", - "url":"", - "handle":"alice", - "is_requester":%s, - "auto_approve":false - }` - metadata = fmt.Sprintf(metadata, tc.requestor) - metadata = compact(t, metadata) - path := filepath.Join(targetDir, "bogus-track", "bogus-exercise", ".solution.json") b, err := ioutil.ReadFile(path) + var s workspace.Solution + err = json.Unmarshal(b, &s) assert.NoError(t, err) - assert.Equal(t, metadata, string(b), "the solution metadata file") + + assert.Equal(t, "bogus-track", s.Track) + assert.Equal(t, "bogus-exercise", s.Exercise) + assert.Equal(t, tc.requester, s.IsRequester) } } @@ -213,9 +207,6 @@ func assertDownloadedCorrectFiles(t *testing.T, targetDir string) { assert.True(t, os.IsNotExist(err), "It should not write the file if empty.") } -const requestorSelf = "true" -const requestorOther = "false" - const payloadTemplate = ` { "solution": { @@ -245,10 +236,3 @@ const payloadTemplate = ` } } ` - -func compact(t *testing.T, s string) string { - buffer := new(bytes.Buffer) - err := json.Compact(buffer, []byte(s)) - assert.NoError(t, err) - return buffer.String() -} From f6344490c453823aabd298528e1a4e20f012a9b7 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Wed, 25 Jul 2018 13:08:01 +0700 Subject: [PATCH 198/544] Add missing teardown for test artifacts Many tests call TempDir but don't perform any teardown. This change attempts to add the teardown in all locations where it is lacking. --- cmd/cmd_test.go | 1 + cmd/configure_test.go | 2 ++ cmd/submit_symlink_test.go | 2 ++ cmd/submit_test.go | 8 ++++++++ workspace/workspace_test.go | 2 ++ 5 files changed, 15 insertions(+) diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 4d6fb4c6f..da7d1b2bc 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -61,6 +61,7 @@ type CommandTest struct { // fail the test if the creation of the temporary directory fails. func (test *CommandTest) Setup(t *testing.T) { dir, err := ioutil.TempDir("", "command-test") + defer os.RemoveAll(dir) assert.NoError(t, err) test.TmpDir = dir diff --git a/cmd/configure_test.go b/cmd/configure_test.go index adf30b705..bdd565fa7 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -374,6 +374,7 @@ func TestConfigureDefaultWorkspaceWithoutClobbering(t *testing.T) { defer ts.Close() tmpDir, err := ioutil.TempDir("", "no-clobber") + defer os.RemoveAll(tmpDir) assert.NoError(t, err) cfg := config.Configuration{ @@ -412,6 +413,7 @@ func TestConfigureExplicitWorkspaceWithoutClobberingNonDirectory(t *testing.T) { }() tmpDir, err := ioutil.TempDir("", "no-clobber") + defer os.RemoveAll(tmpDir) assert.NoError(t, err) v := viper.New() diff --git a/cmd/submit_symlink_test.go b/cmd/submit_symlink_test.go index da9fa3090..a34b1f8b5 100644 --- a/cmd/submit_symlink_test.go +++ b/cmd/submit_symlink_test.go @@ -30,10 +30,12 @@ func TestSubmitFilesInSymlinkedPath(t *testing.T) { defer ts.Close() tmpDir, err := ioutil.TempDir("", "symlink-destination") + defer os.RemoveAll(tmpDir) assert.NoError(t, err) dstDir := filepath.Join(tmpDir, "workspace") srcDir, err := ioutil.TempDir("", "symlink-source") + defer os.RemoveAll(srcDir) assert.NoError(t, err) err = os.Symlink(srcDir, dstDir) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 0eb68a5fc..bbd05de0a 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -42,6 +42,7 @@ func TestSubmitWithoutWorkspace(t *testing.T) { func TestSubmitNonExistentFile(t *testing.T) { tmpDir, err := ioutil.TempDir("", "submit-no-such-file") + defer os.RemoveAll(tmpDir) assert.NoError(t, err) v := viper.New() @@ -70,6 +71,7 @@ func TestSubmitNonExistentFile(t *testing.T) { func TestSubmitExerciseWithoutSolutionMetadataFile(t *testing.T) { tmpDir, err := ioutil.TempDir("", "no-metadata-file") + defer os.RemoveAll(tmpDir) assert.NoError(t, err) dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") @@ -96,6 +98,7 @@ func TestSubmitExerciseWithoutSolutionMetadataFile(t *testing.T) { func TestSubmitFilesAndDir(t *testing.T) { tmpDir, err := ioutil.TempDir("", "submit-no-such-file") + defer os.RemoveAll(tmpDir) assert.NoError(t, err) v := viper.New() @@ -137,6 +140,7 @@ func TestSubmitFiles(t *testing.T) { defer ts.Close() tmpDir, err := ioutil.TempDir("", "submit-files") + defer os.RemoveAll(tmpDir) assert.NoError(t, err) dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") @@ -196,6 +200,7 @@ func TestSubmitWithEmptyFile(t *testing.T) { defer ts.Close() tmpDir, err := ioutil.TempDir("", "empty-file") + defer os.RemoveAll(tmpDir) assert.NoError(t, err) dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") @@ -236,6 +241,7 @@ func TestSubmitOnlyEmptyFile(t *testing.T) { }() tmpDir, err := ioutil.TempDir("", "just-an-empty-file") + defer os.RemoveAll(tmpDir) assert.NoError(t, err) dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") @@ -262,6 +268,7 @@ func TestSubmitOnlyEmptyFile(t *testing.T) { func TestSubmitFilesFromDifferentSolutions(t *testing.T) { tmpDir, err := ioutil.TempDir("", "dir-1-submit") + defer os.RemoveAll(tmpDir) assert.NoError(t, err) dir1 := filepath.Join(tmpDir, "bogus-track", "bogus-exercise-1") @@ -335,6 +342,7 @@ func TestSubmitRelativePath(t *testing.T) { defer ts.Close() tmpDir, err := ioutil.TempDir("", "relative-path") + defer os.RemoveAll(tmpDir) assert.NoError(t, err) dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go index f4b5b2971..8b041e05a 100644 --- a/workspace/workspace_test.go +++ b/workspace/workspace_test.go @@ -2,6 +2,7 @@ package workspace import ( "io/ioutil" + "os" "path/filepath" "runtime" "testing" @@ -51,6 +52,7 @@ func TestIsSolutionPath(t *testing.T) { func TestResolveSolutionPath(t *testing.T) { tmpDir, err := ioutil.TempDir("", "resolve-solution-path") + defer os.RemoveAll(tmpDir) ws, err := New(tmpDir) assert.NoError(t, err) From 239f30367d46c642c034215e52c81d617dbe5b39 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 25 Jul 2018 07:53:47 -0700 Subject: [PATCH 199/544] Improve the API ping in configure command This distinguishes between an error in the HTTP request (e.g. no service at URL), and an error returned from the API (e.g. 500 error). --- api/client.go | 11 +++++++---- cmd/configure.go | 3 +-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/api/client.go b/api/client.go index 782bae3f6..22e483419 100644 --- a/api/client.go +++ b/api/client.go @@ -87,15 +87,18 @@ func (c *Client) TokenIsValid() (bool, error) { } // IsPingable calls the API /ping to determine whether the API can be reached. -func (c *Client) IsPingable() (bool, error) { +func (c *Client) IsPingable() error { url := fmt.Sprintf("%s/ping", c.APIBaseURL) req, err := c.NewRequest("GET", url, nil) if err != nil { - return false, err + return err } resp, err := c.Do(req) if err != nil { - return false, err + return err } - return resp.StatusCode == http.StatusOK, nil + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("API returned %s", resp.Status) + } + return nil } diff --git a/cmd/configure.go b/cmd/configure.go index 8a6b19221..2dbcb5146 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -91,8 +91,7 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro return err } - ok, err := client.IsPingable() - if !ok || err != nil { + if err := client.IsPingable(); err != nil { return fmt.Errorf("The base API URL '%s' cannot be reached.\n\n%s", baseURL, err) } } From 0c661299e573831c11a64a482073c60316b5506a Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 24 Jul 2018 15:56:22 -0700 Subject: [PATCH 200/544] Set up download test to handle teams This updates the fake server to fill in the teams in the payload if the team flag is passed. Nothing is passing the team flag yet. --- cmd/download_test.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index e9aca175c..53ed42ee5 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -114,7 +114,7 @@ func TestDownload(t *testing.T) { defer os.RemoveAll(tmpDir) assert.NoError(t, err) - ts := fakeDownloadServer(strconv.FormatBool(tc.requester)) + ts := fakeDownloadServer(strconv.FormatBool(tc.requester), tc.flags["team"]) defer ts.Close() v := viper.New() @@ -149,7 +149,7 @@ func TestDownload(t *testing.T) { } } -func fakeDownloadServer(requestor string) *httptest.Server { +func fakeDownloadServer(requestor, teamSlug string) *httptest.Server { mux := http.NewServeMux() server := httptest.NewServer(mux) @@ -165,11 +165,16 @@ func fakeDownloadServer(requestor string) *httptest.Server { fmt.Fprint(w, "") }) - payloadBody := fmt.Sprintf(payloadTemplate, requestor, server.URL+"/") mux.HandleFunc("/solutions/latest", func(w http.ResponseWriter, r *http.Request) { + team := "null" + if teamSlug := r.FormValue("team_id"); teamSlug != "" { + team = fmt.Sprintf(`{"name": "Bogus Team", "slug": "%s"}`, teamSlug) + } + payloadBody := fmt.Sprintf(payloadTemplate, requestor, team, server.URL+"/") fmt.Fprint(w, payloadBody) }) mux.HandleFunc("/solutions/bogus-id", func(w http.ResponseWriter, r *http.Request) { + payloadBody := fmt.Sprintf(payloadTemplate, requestor, "null", server.URL+"/") fmt.Fprint(w, payloadBody) }) @@ -215,6 +220,7 @@ const payloadTemplate = ` "handle": "alice", "is_requester": %s }, + "team": %s, "exercise": { "id": "bogus-exercise", "instructions_url": "http://example.com/bogus-exercise", @@ -226,9 +232,9 @@ const payloadTemplate = ` }, "file_download_base_url": "%s", "files": [ - "/file-1.txt", - "/subdir/file-2.txt", - "/file-3.txt" + "/file-1.txt", + "/subdir/file-2.txt", + "/file-3.txt" ], "iteration": { "submitted_at": "2017-08-21t10:11:12.130z" From c5c821c6937a8a1dc33f815a2c692072f0caa7a0 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 24 Jul 2018 16:24:30 -0700 Subject: [PATCH 201/544] Potentially store a team in the solution metadata --- workspace/solution.go | 1 + 1 file changed, 1 insertion(+) diff --git a/workspace/solution.go b/workspace/solution.go index e0f8bbfa3..518f819f3 100644 --- a/workspace/solution.go +++ b/workspace/solution.go @@ -19,6 +19,7 @@ type Solution struct { Track string `json:"track"` Exercise string `json:"exercise"` ID string `json:"id"` + Team string `json:"team,omitempty"` URL string `json:"url"` Handle string `json:"handle"` IsRequester bool `json:"is_requester"` From ad5e545ecfbc1ddd3466cb4cc49aad34714ea98e Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 24 Jul 2018 16:26:49 -0700 Subject: [PATCH 202/544] Implement team flag for download command --- cmd/download.go | 17 +++++++++++++++++ cmd/download_test.go | 5 +++++ 2 files changed, 22 insertions(+) diff --git a/cmd/download.go b/cmd/download.go index d02f08066..2b9dc3052 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -90,12 +90,20 @@ func runDownload(cfg config.Configuration, flags *pflag.FlagSet, args []string) return err } + team, err := flags.GetString("team") + if err != nil { + return err + } + if uuid == "" { q := req.URL.Query() q.Add("exercise_id", exercise) if track != "" { q.Add("track_id", track) } + if team != "" { + q.Add("team_id", team) + } req.URL.RawQuery = q.Encode() } @@ -127,6 +135,7 @@ func runDownload(cfg config.Configuration, flags *pflag.FlagSet, args []string) solution := workspace.Solution{ AutoApprove: payload.Solution.Exercise.AutoApprove, Track: payload.Solution.Exercise.Track.ID, + Team: payload.Solution.Team.Slug, Exercise: payload.Solution.Exercise.ID, ID: payload.Solution.ID, URL: payload.Solution.URL, @@ -135,6 +144,9 @@ func runDownload(cfg config.Configuration, flags *pflag.FlagSet, args []string) } dir := usrCfg.GetString("workspace") + if solution.Team != "" { + dir = filepath.Join(dir, "teams", solution.Team) + } if !solution.IsRequester { dir = filepath.Join(dir, "users", solution.Handle) } @@ -205,6 +217,10 @@ type downloadPayload struct { Solution struct { ID string `json:"id"` URL string `json:"url"` + Team struct { + Name string `json:"name"` + Slug string `json:"slug"` + } `json:"team"` User struct { Handle string `json:"handle"` IsRequester bool `json:"is_requester"` @@ -235,6 +251,7 @@ func setupDownloadFlags(flags *pflag.FlagSet) { flags.StringP("uuid", "u", "", "the solution UUID") flags.StringP("track", "t", "", "the track ID") flags.StringP("exercise", "e", "", "the exercise slug") + flags.StringP("team", "T", "", "the team slug") } func init() { diff --git a/cmd/download_test.go b/cmd/download_test.go index 53ed42ee5..883372a58 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -107,6 +107,11 @@ func TestDownload(t *testing.T) { expectedDir: filepath.Join("users", "alice"), flags: map[string]string{"uuid": "bogus-id"}, }, + { + requester: true, + expectedDir: filepath.Join("teams", "bogus-team"), + flags: map[string]string{"exercise": "bogus-exercise", "track": "bogus-track", "team": "bogus-team"}, + }, } for _, tc := range testCases { From 37f050701a581b71a5e70885cb306c1fee6e7969 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 25 Jul 2018 20:09:43 -0700 Subject: [PATCH 203/544] Delete transmission fixtures (#681) The transmission stuff was deleted, these fixtures got left behind. --- fixtures/transmission/creatures/hummingbird/.solution.json | 0 fixtures/transmission/creatures/hummingbird/feeder/sugar.txt | 0 fixtures/transmission/creatures/hummingbird/hummingbird.txt | 0 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 fixtures/transmission/creatures/hummingbird/.solution.json delete mode 100644 fixtures/transmission/creatures/hummingbird/feeder/sugar.txt delete mode 100644 fixtures/transmission/creatures/hummingbird/hummingbird.txt diff --git a/fixtures/transmission/creatures/hummingbird/.solution.json b/fixtures/transmission/creatures/hummingbird/.solution.json deleted file mode 100644 index e69de29bb..000000000 diff --git a/fixtures/transmission/creatures/hummingbird/feeder/sugar.txt b/fixtures/transmission/creatures/hummingbird/feeder/sugar.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/fixtures/transmission/creatures/hummingbird/hummingbird.txt b/fixtures/transmission/creatures/hummingbird/hummingbird.txt deleted file mode 100644 index e69de29bb..000000000 From 41999e757e87a7376aee36a5f054173bf7cf496b Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 25 Jul 2018 20:16:21 -0700 Subject: [PATCH 204/544] Tweak output of troubleshoot command when the CLI is unconfigured (#675) * Pass full configuration value to troubleshooting status Instead of passing just the viper config, which could be empty, pass the full config.Configuration value, which has access to defaults. This will let us give better debugging output. * Fix output for 'home' in troubleshoot command The home directory is never written to the viper config, we get it from the environment, which is stored in the config.Configuration value. * Output default workspace in troubleshoot if unconfigured Rather than providing an empty string for the workspace in the troubleshoot command when the CLI is unconfigured, this now says Workspace: /the/actual/path (default) * Use config dir, not file, in troubleshoot command In the old CLI we had a single config file. Now we potentially have several. They will all live in the config directory. The troubleshoot command now outputs the directory instead of the file. If the troubleshoot command is unconfigured, this would previously have given an empty string for the config file. Now it will always print the default config directory, unless the user has specifically defined an override using environment variables. * Inline token in troubleshoot config status It was unnecessary to have an additional local variable. * Use proper helper method for settings URL in troubleshoot command The config package has logic for the settings URL, we shouldn't be hard-coding it here. * Fix ping when troubleshoot is unconfigured --- cmd/troubleshoot.go | 46 ++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go index c491108a6..66c3e2a9d 100644 --- a/cmd/troubleshoot.go +++ b/cmd/troubleshoot.go @@ -42,7 +42,9 @@ command into a GitHub issue so we can help figure out what's going on. // Ignore error. If the file doesn't exist, that is fine. _ = v.ReadInConfig() - status := newStatus(c, v) + cfg.UserViperConfig = v + + status := newStatus(c, cfg) status.Censor = !fullAPIKey s, err := status.check() if err != nil { @@ -61,8 +63,8 @@ type Status struct { System systemStatus Configuration configurationStatus APIReachability apiReachabilityStatus + cfg config.Configuration cli *cli.CLI - cfg *viper.Viper } type versionStatus struct { @@ -82,7 +84,7 @@ type systemStatus struct { type configurationStatus struct { Home string Workspace string - File string + Dir string Token string TokenURL string } @@ -99,10 +101,10 @@ type apiPing struct { } // newStatus prepares a value to perform a diagnostic self-check. -func newStatus(c *cli.CLI, v *viper.Viper) Status { +func newStatus(cli *cli.CLI, cfg config.Configuration) Status { status := Status{ - cli: c, - cfg: v, + cfg: cfg, + cli: cli, } return status } @@ -112,7 +114,7 @@ func (status *Status) check() (string, error) { status.Version = newVersionStatus(status.cli) status.System = newSystemStatus() status.Configuration = newConfigurationStatus(status) - status.APIReachability = newAPIReachabilityStatus(status.cfg.GetString("apibaseurl")) + status.APIReachability = newAPIReachabilityStatus(status.cfg) return status.compile() } @@ -127,7 +129,11 @@ func (status *Status) compile() (string, error) { return bb.String(), nil } -func newAPIReachabilityStatus(baseURL string) apiReachabilityStatus { +func newAPIReachabilityStatus(cfg config.Configuration) apiReachabilityStatus { + baseURL := cfg.UserViperConfig.GetString("apibaseurl") + if baseURL == "" { + baseURL = cfg.DefaultBaseURL + } ar := apiReachabilityStatus{ Services: []*apiPing{ {Service: "GitHub", URL: "https://api.github.com"}, @@ -172,16 +178,22 @@ func newSystemStatus() systemStatus { } func newConfigurationStatus(status *Status) configurationStatus { - token := status.cfg.GetString("token") + v := status.cfg.UserViperConfig + + workspace := v.GetString("workspace") + if workspace == "" { + workspace = fmt.Sprintf("%s (default)", config.DefaultWorkspaceDir(status.cfg)) + } + cs := configurationStatus{ - Home: status.cfg.GetString("home"), - Workspace: status.cfg.GetString("workspace"), - File: status.cfg.ConfigFileUsed(), - Token: token, - TokenURL: config.InferSiteURL(status.cfg.GetString("apibaseurl")) + "/my/settings", + Home: status.cfg.Home, + Workspace: workspace, + Dir: status.cfg.Dir, + Token: v.GetString("token"), + TokenURL: config.SettingsURL(v.GetString("apibaseurl")), } - if status.Censor && token != "" { - cs.Token = redact(token) + if status.Censor && cs.Token != "" { + cs.Token = redact(cs.Token) } return cs } @@ -235,7 +247,7 @@ Configuration ---------------- Home: {{ .Configuration.Home }} Workspace: {{ .Configuration.Workspace }} -Config: {{ .Configuration.File }} +Config: {{ .Configuration.Dir }} API key: {{ with .Configuration.Token }}{{ . }}{{ else }} Find your API key at {{ .Configuration.TokenURL }}{{ end }} From d2081f214c2a7272d6b971fae4cf0340ef2d8beb Mon Sep 17 00:00:00 2001 From: Guillaume Date: Thu, 26 Jul 2018 16:29:18 +0100 Subject: [PATCH 205/544] fix typo in welcome message (#683) --- cmd/cmd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index a87bf96bd..f881a3344 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -4,7 +4,7 @@ const msgWelcomePleaseConfigure = ` Welcome to Exercism! - To get started, you need to configure the the tool with your API token. + To get started, you need to configure the tool with your API token. Find your token at %s From fc64c80130a926c105f131c746a1b412f1a18b61 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 26 Jul 2018 10:33:57 -0700 Subject: [PATCH 206/544] Delete speculative implementation of prepare command (#677) When I first started writing the new CLI I had some grand ideas about how submit should work, which required all sorts of complicated guessing, which in turn required a command to store settings on a per-track basis. We may still need to prepare things on a per-track basis, but the current implementation was highly speculative, and I decided that it would be better to delete it and start fresh when we know exactly what we need. --- cmd/prepare.go | 108 +------------------------------------- cmd/prepare_test.go | 70 ------------------------ config/cli_config.go | 62 ---------------------- config/cli_config_test.go | 89 ------------------------------- config/config.go | 57 -------------------- config/config_test.go | 90 ------------------------------- config/configuration.go | 1 - config/track.go | 71 ------------------------- config/track_test.go | 38 -------------- config/tracks.go | 4 -- 10 files changed, 2 insertions(+), 588 deletions(-) delete mode 100644 cmd/prepare_test.go delete mode 100644 config/cli_config.go delete mode 100644 config/cli_config_test.go delete mode 100644 config/track.go delete mode 100644 config/track_test.go delete mode 100644 config/tracks.go diff --git a/cmd/prepare.go b/cmd/prepare.go index 4c0557f1f..6e6a13811 100644 --- a/cmd/prepare.go +++ b/cmd/prepare.go @@ -1,17 +1,6 @@ package cmd -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - - "github.com/exercism/cli/api" - "github.com/exercism/cli/config" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/spf13/viper" -) +import "github.com/spf13/cobra" // prepareCmd does necessary setup for Exercism and its tracks. var prepareCmd = &cobra.Command{ @@ -19,105 +8,12 @@ var prepareCmd = &cobra.Command{ Aliases: []string{"p"}, Short: "Prepare does setup for Exercism and its tracks.", Long: `Prepare downloads settings and dependencies for Exercism and the language tracks. - -When called with a track ID, it will do specific setup for that track. This -might include downloading the files that the track maintainers have said are -necessary for the track in general. Any files that are only necessary for a specific -exercise will be downloaded along with the exercise. - -To customize the CLI to suit your own preferences, use the configure command. `, RunE: func(cmd *cobra.Command, args []string) error { - cfg := config.NewConfiguration() - - v := viper.New() - v.AddConfigPath(cfg.Dir) - v.SetConfigName("user") - v.SetConfigType("json") - // Ignore error. If the file doesn't exist, that is fine. - _ = v.ReadInConfig() - cfg.UserViperConfig = v - - return runPrepare(cfg, cmd.Flags(), args) - }, -} - -func runPrepare(cfg config.Configuration, flags *pflag.FlagSet, args []string) error { - v := cfg.UserViperConfig - - track, err := flags.GetString("track") - if err != nil { - return err - } - - if track == "" { return nil - } - client, err := api.NewClient(v.GetString("token"), v.GetString("apibaseurl")) - if err != nil { - return err - } - url := fmt.Sprintf("%s/tracks/%s", v.GetString("apibaseurl"), track) - - req, err := client.NewRequest("GET", url, nil) - if err != nil { - return err - } - - res, err := client.Do(req) - if err != nil { - return err - } - defer res.Body.Close() - - var payload prepareTrackPayload - - if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { - return fmt.Errorf("unable to parse API response - %s", err) - } - - if res.StatusCode != http.StatusOK { - return errors.New(payload.Error.Message) - } - - cliCfg, err := config.NewCLIConfig() - if err != nil { - return err - } - - t, ok := cliCfg.Tracks[track] - if !ok { - t = config.NewTrack(track) - } - if payload.Track.TestPattern != "" { - t.IgnorePatterns = append(t.IgnorePatterns, payload.Track.TestPattern) - } - cliCfg.Tracks[track] = t - - return cliCfg.Write() -} - -type prepareTrackPayload struct { - Track struct { - ID string `json:"id"` - Language string `json:"language"` - TestPattern string `json:"test_pattern"` - } `json:"track"` - Error struct { - Type string `json:"type"` - Message string `json:"message"` - } `json:"error,omitempty"` -} - -func initPrepareCmd() { - setupPrepareFlags(prepareCmd.Flags()) -} - -func setupPrepareFlags(flags *pflag.FlagSet) { - flags.StringP("track", "t", "", "the track you want to prepare") + }, } func init() { RootCmd.AddCommand(prepareCmd) - initPrepareCmd() } diff --git a/cmd/prepare_test.go b/cmd/prepare_test.go deleted file mode 100644 index 8d308d1b5..000000000 --- a/cmd/prepare_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package cmd - -import ( - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "testing" - - "github.com/exercism/cli/config" - "github.com/spf13/pflag" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" -) - -func TestPrepareTrack(t *testing.T) { - fakeEndpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - payload := ` - { - "track": { - "id": "bogus", - "language": "Bogus", - "test_pattern": "_spec[.]ext$" - } - } - ` - fmt.Fprintln(w, payload) - }) - ts := httptest.NewServer(fakeEndpoint) - defer ts.Close() - - tmpDir, err := ioutil.TempDir("", "prepare-track") - assert.NoError(t, err) - defer os.Remove(tmpDir) - - // Until we can decouple CLIConfig from filesystem, overwrite config dir. - originalConfigDir := os.Getenv(cfgHomeKey) - os.Setenv(cfgHomeKey, tmpDir) - defer os.Setenv(cfgHomeKey, originalConfigDir) - - flags := pflag.NewFlagSet("fake", pflag.PanicOnError) - setupPrepareFlags(flags) - flags.Set("track", "bogus") - - v := viper.New() - v.Set("apibaseurl", ts.URL) - - cfg := config.Configuration{ - UserViperConfig: v, - } - - err = runPrepare(cfg, flags, []string{}) - assert.NoError(t, err) - - cliCfg, err := config.NewCLIConfig() - os.Remove(cliCfg.File()) - assert.NoError(t, err) - - expected := []string{ - ".*[.]md", - "[.]solution[.]json", - "_spec[.]ext$", - } - track := cliCfg.Tracks["bogus"] - if track == nil { - t.Fatal("track missing from config") - } - assert.Equal(t, expected, track.IgnorePatterns) -} diff --git a/config/cli_config.go b/config/cli_config.go deleted file mode 100644 index 79838fabe..000000000 --- a/config/cli_config.go +++ /dev/null @@ -1,62 +0,0 @@ -package config - -import "github.com/spf13/viper" - -// CLIConfig contains settings specific to the behavior of the CLI. -type CLIConfig struct { - *Config - Tracks Tracks -} - -// NewCLIConfig loads the config file in the config directory. -func NewCLIConfig() (*CLIConfig, error) { - cfg := NewEmptyCLIConfig() - - if err := cfg.Load(viper.New()); err != nil { - return nil, err - } - cfg.SetDefaults() - - return cfg, nil -} - -// NewEmptyCLIConfig doesn't load the config from file or set default values. -func NewEmptyCLIConfig() *CLIConfig { - return &CLIConfig{ - Config: New(Dir(), "cli"), - Tracks: Tracks{}, - } -} - -// Write stores the config to disk. -func (cfg *CLIConfig) Write() error { - cfg.SetDefaults() - if err := cfg.Validate(); err != nil { - return err - } - return Write(cfg) -} - -// Validate ensures that the config is valid. -// This is called before writing it. -func (cfg *CLIConfig) Validate() error { - for _, track := range cfg.Tracks { - if err := track.CompileRegexes(); err != nil { - return err - } - } - return nil -} - -// SetDefaults ensures that we have all the necessary settings for the CLI. -func (cfg *CLIConfig) SetDefaults() { - for _, track := range cfg.Tracks { - track.SetDefaults() - } -} - -// Load reads a viper configuration into the config. -func (cfg *CLIConfig) Load(v *viper.Viper) error { - cfg.readIn(v) - return v.Unmarshal(&cfg) -} diff --git a/config/cli_config_test.go b/config/cli_config_test.go deleted file mode 100644 index ad78d852b..000000000 --- a/config/cli_config_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package config - -import ( - "io/ioutil" - "os" - "sort" - "testing" - - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" -) - -func TestCLIConfig(t *testing.T) { - dir, err := ioutil.TempDir("", "cli-config") - assert.NoError(t, err) - defer os.RemoveAll(dir) - - cfg := &CLIConfig{ - Config: New(dir, "cli"), - Tracks: Tracks{ - "bogus": &Track{ID: "bogus"}, - "fake": &Track{ID: "fake", IgnorePatterns: []string{"c", "b", "a"}}, - }, - } - - // write it - err = cfg.Write() - assert.NoError(t, err) - - // reload it - cfg = &CLIConfig{ - Config: New(dir, "cli"), - } - err = cfg.Load(viper.New()) - assert.NoError(t, err) - assert.Equal(t, "bogus", cfg.Tracks["bogus"].ID) - assert.Equal(t, "fake", cfg.Tracks["fake"].ID) - - // The ignore patterns got sorted. - expected := append(defaultIgnorePatterns, "a", "b", "c") - sort.Strings(expected) - assert.Equal(t, expected, cfg.Tracks["fake"].IgnorePatterns) -} - -func TestCLIConfigValidate(t *testing.T) { - cfg := &CLIConfig{ - Tracks: Tracks{ - "fake": &Track{ - ID: "fake", - IgnorePatterns: []string{"(?=re)"}, // not a valid regex - }, - }, - } - - err := cfg.Validate() - assert.Error(t, err) -} - -func TestCLIConfigSetDefaults(t *testing.T) { - // No tracks, no defaults. - cfg := &CLIConfig{} - cfg.SetDefaults() - assert.Equal(t, &CLIConfig{}, cfg) - - // With a track, gets defaults. - cfg = &CLIConfig{ - Tracks: map[string]*Track{ - "bogus": { - ID: "bogus", - }, - }, - } - cfg.SetDefaults() - assert.Equal(t, defaultIgnorePatterns, cfg.Tracks["bogus"].IgnorePatterns) - - // With partial defaults and extras, gets everything. - cfg = &CLIConfig{ - Tracks: map[string]*Track{ - "bogus": { - ID: "bogus", - IgnorePatterns: []string{"[.]solution[.]json", "_spec[.]ext$"}, - }, - }, - } - cfg.SetDefaults() - expected := append(defaultIgnorePatterns, "_spec[.]ext$") - sort.Strings(expected) - assert.Equal(t, expected, cfg.Tracks["bogus"].IgnorePatterns) -} diff --git a/config/config.go b/config/config.go index 7767c9f3e..c072f5c37 100644 --- a/config/config.go +++ b/config/config.go @@ -1,71 +1,14 @@ package config import ( - "encoding/json" "fmt" - "io/ioutil" - "os" - "path/filepath" "regexp" - - "github.com/spf13/viper" ) var ( defaultBaseURL = "https://api.exercism.io/v1" ) -// Config is a wrapper around a viper configuration. -type Config struct { - dir string - name string -} - -// New creates a default config value for the given directory. -func New(dir, name string) *Config { - return &Config{ - dir: dir, - name: name, - } -} - -// File is the full path to the config file. -func (cfg *Config) File() string { - return filepath.Join(cfg.dir, fmt.Sprintf("%s.json", cfg.name)) -} - -func (cfg *Config) readIn(v *viper.Viper) { - v.AddConfigPath(cfg.dir) - v.SetConfigName(cfg.name) - v.SetConfigType("json") - v.ReadInConfig() -} - -type filer interface { - File() string -} - -// Write stores the config into a file. -func Write(f filer) error { - b, err := json.Marshal(f) - if err != nil { - return err - } - if err := ensureDir(f); err != nil { - return err - } - return ioutil.WriteFile(f.File(), b, os.FileMode(0644)) -} - -func ensureDir(f filer) error { - dir := filepath.Dir(f.File()) - _, err := os.Stat(dir) - if os.IsNotExist(err) { - return os.MkdirAll(dir, os.FileMode(0755)) - } - return err -} - // InferSiteURL guesses what the website URL is. // The basis for the guess is which API we're submitting to. func InferSiteURL(apiURL string) string { diff --git a/config/config_test.go b/config/config_test.go index 6304bf67e..e9d29f97f 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,101 +1,11 @@ package config import ( - "io/ioutil" - "os" - "path/filepath" "testing" - "github.com/spf13/pflag" - "github.com/spf13/viper" "github.com/stretchr/testify/assert" ) -type fakeConfig struct { - *Config - Letter string - Number int -} - -func (cfg *fakeConfig) write() error { - return Write(cfg) -} - -func (cfg *fakeConfig) load(v *viper.Viper) error { - cfg.readIn(v) - return v.Unmarshal(&cfg) -} - -func TestFakeConfig(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "fake-config") - assert.NoError(t, err) - defer os.RemoveAll(tmpDir) - - // Set the config directory to a directory that doesn't exist. - dir := filepath.Join(tmpDir, "exercism") - - // It has access to the embedded fields. - cfg := &fakeConfig{ - Config: New(dir, "fake"), - } - assert.Equal(t, dir, cfg.dir) - assert.Equal(t, "fake", cfg.name) - - // We're going to load up a viper that is bound to some command-line flags. - // First we need flags. - flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) - flagSet.IntP("number", "n", 0, "pick a number, any number") - flagSet.StringP("letter", "l", "", "something from a nice alphabet") - flagSet.Set("number", "1") - flagSet.Set("letter", "a") - - // Bind the flags to a new viper value. - v := viper.New() - v.BindPFlag("number", flagSet.Lookup("number")) - v.BindPFlag("letter", flagSet.Lookup("letter")) - - // Binding the flags loaded the values into viper. - assert.Equal(t, 1, v.Get("number")) - assert.Equal(t, "a", v.Get("letter")) - - // Load viper into the config value. - err = cfg.load(v) - assert.NoError(t, err) - - // The original flag values have been loaded into the struct value. - assert.Equal(t, 1, cfg.Number) - assert.Equal(t, "a", cfg.Letter) - - // Write the file. - err = cfg.write() - assert.NoError(t, err) - - // Reload it. - cfg = &fakeConfig{ - Config: New(dir, "fake"), - } - err = cfg.load(viper.New()) - assert.NoError(t, err) - - // It wrote the non-embedded fields. - assert.Equal(t, "a", cfg.Letter) - assert.Equal(t, 1, cfg.Number) - - // Update the config. - cfg.Letter = "b" - err = cfg.write() - assert.NoError(t, err) - - // Updating the config selectively overwrote the values. - cfg = &fakeConfig{ - Config: New(dir, "fake"), - } - err = cfg.load(viper.New()) - assert.NoError(t, err) - assert.Equal(t, "b", cfg.Letter) - assert.Equal(t, 1, cfg.Number) -} - func TestInferSiteURL(t *testing.T) { testCases := []struct { api, url string diff --git a/config/configuration.go b/config/configuration.go index 325eeb2a0..bf92c5b9d 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -22,7 +22,6 @@ type Configuration struct { DefaultBaseURL string DefaultDirName string UserViperConfig *viper.Viper - CLIConfig *CLIConfig Persister Persister } diff --git a/config/track.go b/config/track.go deleted file mode 100644 index 414340a1d..000000000 --- a/config/track.go +++ /dev/null @@ -1,71 +0,0 @@ -package config - -import ( - "regexp" - "sort" -) - -var defaultIgnorePatterns = []string{ - ".*[.]md", - "[.]solution[.]json", -} - -// Track holds the CLI-related settings for a track. -type Track struct { - ID string - IgnorePatterns []string - ignoreRegexes []*regexp.Regexp -} - -// NewTrack provides a track configured with default values. -func NewTrack(id string) *Track { - t := &Track{ - ID: id, - } - t.SetDefaults() - return t -} - -// SetDefaults configures a track with default values. -func (t *Track) SetDefaults() { - m := map[string]bool{} - for _, pattern := range t.IgnorePatterns { - m[pattern] = true - } - for _, pattern := range defaultIgnorePatterns { - if !m[pattern] { - t.IgnorePatterns = append(t.IgnorePatterns, pattern) - } - } - sort.Strings(t.IgnorePatterns) -} - -// AcceptFilename judges a files admissability based on the name. -func (t *Track) AcceptFilename(f string) (bool, error) { - if err := t.CompileRegexes(); err != nil { - return false, err - } - - for _, re := range t.ignoreRegexes { - if re.MatchString(f) { - return false, nil - } - } - return true, nil -} - -// CompileRegexes precompiles the ignore patterns. -func (t *Track) CompileRegexes() error { - if len(t.ignoreRegexes) == len(t.IgnorePatterns) { - return nil - } - - for _, pattern := range t.IgnorePatterns { - re, err := regexp.Compile(pattern) - if err != nil { - return err - } - t.ignoreRegexes = append(t.ignoreRegexes, re) - } - return nil -} diff --git a/config/track_test.go b/config/track_test.go deleted file mode 100644 index e6f5911f7..000000000 --- a/config/track_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package config - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestAcceptFilename(t *testing.T) { - - testCases := []struct { - desc string - filenames []string - expected bool - }{ - - {"allowed filename", []string{"beacon.ext", "falcon.zip"}, true}, - {"ignored filename", []string{"beacon|txt", "falcon.txt", "proof"}, false}, - } - - track := &Track{ - IgnorePatterns: []string{ - "con[|.]txt", - "pro.f", - }, - } - - for _, tc := range testCases { - for _, filename := range tc.filenames { - t.Run(fmt.Sprintf("%s %s", tc.desc, filename), func(t *testing.T) { - got, err := track.AcceptFilename(filename) - assert.NoError(t, err, fmt.Sprintf("%s %s", tc.desc, filename)) - assert.Equal(t, tc.expected, got, fmt.Sprintf("should return %t for %s, but got %t", tc.expected, tc.desc, got)) - }) - } - } -} diff --git a/config/tracks.go b/config/tracks.go deleted file mode 100644 index 8a964df53..000000000 --- a/config/tracks.go +++ /dev/null @@ -1,4 +0,0 @@ -package config - -// Tracks is a collection of track-specific settings. -type Tracks map[string]*Track From a9ecf3ce305aa82cf0f364919abe9d6cab8d73cb Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 18 Jul 2018 21:27:21 -0600 Subject: [PATCH 207/544] Add test to show that submit works for team solutions There are refactorings that I'm going to want to do, but this shows that we at least handle the use case, so that we can launch the teams feature now if we decide to. --- cmd/submit_test.go | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index bbd05de0a..d40a55c5d 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -230,6 +230,57 @@ func TestSubmitWithEmptyFile(t *testing.T) { assert.Equal(t, "This is file 2.", submittedFiles[string(os.PathSeparator)+"file-2.txt"]) } +func TestSubmitFilesForTeamExercise(t *testing.T) { + oldOut := Out + oldErr := Err + Out = ioutil.Discard + Err = ioutil.Discard + defer func() { + Out = oldOut + Err = oldErr + }() + // The fake endpoint will populate this when it receives the call from the command. + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := ioutil.TempDir("", "submit-files") + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "teams", "bogus-team", "bogus-track", "bogus-exercise") + os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) + writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") + + file1 := filepath.Join(dir, "file-1.txt") + err = ioutil.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) + assert.NoError(t, err) + + file2 := filepath.Join(dir, "subdir", "file-2.txt") + err = ioutil.WriteFile(file2, []byte("This is file 2."), os.FileMode(0755)) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Configuration{ + Dir: tmpDir, + UserViperConfig: v, + } + + files := []string{ + file1, file2, + } + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) + assert.NoError(t, err) + + assert.Equal(t, 2, len(submittedFiles)) + + assert.Equal(t, "This is file 1.", submittedFiles[string(os.PathSeparator)+"file-1.txt"]) + assert.Equal(t, "This is file 2.", submittedFiles[string(os.PathSeparator)+filepath.Join("subdir", "file-2.txt")]) +} + func TestSubmitOnlyEmptyFile(t *testing.T) { oldOut := Out oldErr := Err From 0f1402afec188856459eed716802ed30991f332d Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 26 Jul 2018 18:26:20 -0600 Subject: [PATCH 208/544] Bump version to v3.0.6 --- CHANGELOG.md | 11 +++++++++++ cmd/version.go | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6372d9cd5..9726cd86d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ## Next Release * **Your contribution here** +## v3.0.6 (2018-07-17) +* [#652](https://github.com/exercism/cli/pull/652) Add support for teams feature - [@kytrinyx] +* [#683](https://github.com/exercism/cli/pull/683) Fix typo in welcome message - [@glebedel] +* [#675](https://github.com/exercism/cli/pull/675) Improve output of troubleshoot command when CLI is unconfigured - [@kytrinyx] +* [#679](https://github.com/exercism/cli/pull/679) Improve error message for failed /ping on configure - [@kytrinyx] +* [#669](https://github.com/exercism/cli/pull/669) Add debug as alias for troubleshoot - [@kytrinyx] +* [#647](https://github.com/exercism/cli/pull/647) Ensure welcome message has full link to settings page - [@kytrinyx] +* [#667](https://github.com/exercism/cli/pull/667) Improve bash completion script - [@cookrn] + ## v3.0.5 (2018-07-17) * [#646](https://github.com/exercism/cli/pull/646) Fix issue with upgrading on Windows - [@nywilken] @@ -378,6 +387,7 @@ All changes by [@msgehard] [@blackerby]: https://github.com/blackerby [@broady]: https://github.com/broady [@ccnp123]: https://github.com/ccnp123 +[@cookrn]: https://github.com/cookrn [@daveyarwood]: https://github.com/daveyarwood [@devonestes]: https://github.com/devonestes [@djquan]: https://github.com/djquan @@ -387,6 +397,7 @@ All changes by [@msgehard] [@ebautistabar]: https://github.com/ebautistabar [@elimisteve]: https://github.com/elimisteve [@ests]: https://github.com/ests +[@glebedel]: https://github.com/glebedel [@harimp]: https://github.com/harimp [@hjljo]: https://github.com/hjljo [@isbadawi]: https://github.com/isbadawi diff --git a/cmd/version.go b/cmd/version.go index 617f2da07..3bcb893de 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.0.5" +const Version = "3.0.6" // checkLatest flag for version command. var checkLatest bool From d48268d07e5d3902e64b6518fa8291fda1025e64 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 26 Jul 2018 18:40:48 -0600 Subject: [PATCH 209/544] Tweak release documentation --- RELEASE.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 9c53bdb6f..874f40dee 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -66,7 +66,7 @@ You will need to configure the CLI with your [Exercism API Key](http://exercism. For more detailed instructions, see the [CLI page on Exercism](http://exercism.io/cli). -#### Recent Changes +#### Recent changes * ABC... * XYZ... @@ -99,9 +99,9 @@ brew bump-formula-pr --strict exercism --url=https://github.com/exercism/cli/arc For more information see their [contribution guidelines](https://github.com/Homebrew/homebrew/blob/master/share/doc/homebrew/How-To-Open-a-Homebrew-Pull-Request-(and-get-it-merged).md#how-to-open-a-homebrew-pull-request-and-get-it-merged). -## Update the Docs Site +## Update the docs site If there are any significant changes, we should describe them on -[cli.exercism.io](http://cli.exercism.io/). +[exercism.io/cli]([https://exercism.io/cli). -The codebase lives at [exercism/cli-www](https://github.com/exercism/cli-www). +The codebase lives at [exercism/website-copy](https://github.com/exercism/website-copy) in `pages/cli.md`. From 09a5dcf08a622ddc6c5a4cceb3b383f9868af866 Mon Sep 17 00:00:00 2001 From: jdsutherland Date: Tue, 31 Jul 2018 07:11:40 +0700 Subject: [PATCH 210/544] Add reference to docs repo in CONTRIBUTING.md (#687) --- CONTRIBUTING.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 983bc1389..33e910478 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,6 +3,11 @@ First, thank you! :tada: Exercism would be impossible without people like you being willing to spend time and effort making things better. +## Documentation +* [Exercism Documentation Repository](https://github.com/exercism/docs) +* [Exercism Glossary](https://github.com/exercism/docs/blob/master/about/glossary.md) +* [Exercism Architecture](https://github.com/exercism/docs/blob/master/about/architecture.md) + ## Dependencies You'll need Go version 1.9 or higher. Follow the directions on http://golang.org/doc/install From 9e1285b62502f3f5a4a896a44e540ee1bee5c1bf Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 24 Jul 2018 22:57:09 -0700 Subject: [PATCH 211/544] Rename config.Configuration to config.Config Now that we got rid of the complicated viper wrapper, we can name this Config, which is a bit more idiomatic. --- cmd/configure.go | 6 +- cmd/configure_test.go | 14 +-- cmd/download.go | 4 +- cmd/download_test.go | 10 +- cmd/open.go | 2 +- cmd/submit.go | 4 +- cmd/submit_symlink_test.go | 2 +- cmd/submit_test.go | 22 ++-- cmd/troubleshoot.go | 8 +- cmd/workspace.go | 2 +- config/config.go | 106 +++++++++++++++++ ...guration_test.go => config_notwin_test.go} | 6 +- ...windows_test.go => config_windows_test.go} | 2 +- config/configuration.go | 112 ------------------ 14 files changed, 147 insertions(+), 153 deletions(-) rename config/{configuration_test.go => config_notwin_test.go} (68%) rename config/{configuration_windows_test.go => config_windows_test.go} (71%) delete mode 100644 config/configuration.go diff --git a/cmd/configure.go b/cmd/configure.go index 2dbcb5146..dd1c47ed8 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -31,7 +31,7 @@ places. You can also override certain default settings to suit your preferences. `, RunE: func(cmd *cobra.Command, args []string) error { - configuration := config.NewConfiguration() + configuration := config.NewConfig() viperConfig.AddConfigPath(configuration.Dir) viperConfig.SetConfigName("user") @@ -44,7 +44,7 @@ You can also override certain default settings to suit your preferences. }, } -func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) error { +func runConfigure(configuration config.Config, flags *pflag.FlagSet) error { cfg := configuration.UserViperConfig // Show the existing configuration and exit. @@ -196,7 +196,7 @@ func runConfigure(configuration config.Configuration, flags *pflag.FlagSet) erro return nil } -func printCurrentConfig(configuration config.Configuration) { +func printCurrentConfig(configuration config.Config) { w := tabwriter.NewWriter(Err, 0, 0, 2, ' ', 0) defer w.Flush() diff --git a/cmd/configure_test.go b/cmd/configure_test.go index bdd565fa7..99e6472be 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -33,7 +33,7 @@ func TestBareConfigure(t *testing.T) { err := flags.Parse([]string{}) assert.NoError(t, err) - cfg := config.Configuration{ + cfg := config.Config{ Persister: config.InMemoryPersister{}, UserViperConfig: v, DefaultBaseURL: "http://example.com", @@ -70,7 +70,7 @@ func TestConfigureShow(t *testing.T) { err := flags.Parse(args) assert.NoError(t, err) - cfg := config.Configuration{ + cfg := config.Config{ Persister: config.InMemoryPersister{}, UserViperConfig: v, } @@ -170,7 +170,7 @@ func TestConfigureToken(t *testing.T) { err := flags.Parse(tc.args) assert.NoError(t, err) - cfg := config.Configuration{ + cfg := config.Config{ Persister: config.InMemoryPersister{}, UserViperConfig: v, DefaultBaseURL: ts.URL, @@ -258,7 +258,7 @@ func TestConfigureAPIBaseURL(t *testing.T) { err := flags.Parse(tc.args) assert.NoError(t, err) - cfg := config.Configuration{ + cfg := config.Config{ Persister: config.InMemoryPersister{}, UserViperConfig: v, DefaultBaseURL: ts.URL, @@ -343,7 +343,7 @@ func TestConfigureWorkspace(t *testing.T) { err := flags.Parse(tc.args) assert.NoError(t, err) - cfg := config.Configuration{ + cfg := config.Config{ Persister: config.InMemoryPersister{}, UserViperConfig: v, DefaultBaseURL: ts.URL, @@ -377,7 +377,7 @@ func TestConfigureDefaultWorkspaceWithoutClobbering(t *testing.T) { defer os.RemoveAll(tmpDir) assert.NoError(t, err) - cfg := config.Configuration{ + cfg := config.Config{ OS: "linux", DefaultDirName: "workspace", Home: tmpDir, @@ -419,7 +419,7 @@ func TestConfigureExplicitWorkspaceWithoutClobberingNonDirectory(t *testing.T) { v := viper.New() v.Set("token", "abc123") - cfg := config.Configuration{ + cfg := config.Config{ OS: "linux", DefaultDirName: "workspace", Home: tmpDir, diff --git a/cmd/download.go b/cmd/download.go index 2b9dc3052..206ee9386 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -32,7 +32,7 @@ latest solution. Download other people's solutions by providing the UUID. `, RunE: func(cmd *cobra.Command, args []string) error { - cfg := config.NewConfiguration() + cfg := config.NewConfig() v := viper.New() v.AddConfigPath(cfg.Dir) @@ -46,7 +46,7 @@ Download other people's solutions by providing the UUID. }, } -func runDownload(cfg config.Configuration, flags *pflag.FlagSet, args []string) error { +func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { usrCfg := cfg.UserViperConfig if usrCfg.GetString("token") == "" { return fmt.Errorf(msgWelcomePleaseConfigure, config.SettingsURL(usrCfg.GetString("apibaseurl")), BinaryName) diff --git a/cmd/download_test.go b/cmd/download_test.go index 883372a58..2c3200c2e 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -19,7 +19,7 @@ import ( ) func TestDownloadWithoutToken(t *testing.T) { - cfg := config.Configuration{ + cfg := config.Config{ UserViperConfig: viper.New(), } @@ -34,7 +34,7 @@ func TestDownloadWithoutToken(t *testing.T) { func TestDownloadWithoutWorkspace(t *testing.T) { v := viper.New() v.Set("token", "abc123") - cfg := config.Configuration{ + cfg := config.Config{ UserViperConfig: v, } @@ -48,7 +48,7 @@ func TestDownloadWithoutBaseURL(t *testing.T) { v := viper.New() v.Set("token", "abc123") v.Set("workspace", "/home/whatever") - cfg := config.Configuration{ + cfg := config.Config{ UserViperConfig: v, } @@ -64,7 +64,7 @@ func TestDownloadWithoutFlags(t *testing.T) { v.Set("workspace", "/home/username") v.Set("apibaseurl", "http://example.com") - cfg := config.Configuration{ + cfg := config.Config{ UserViperConfig: v, } @@ -127,7 +127,7 @@ func TestDownload(t *testing.T) { v.Set("apibaseurl", ts.URL) v.Set("token", "abc123") - cfg := config.Configuration{ + cfg := config.Config{ UserViperConfig: v, } flags := pflag.NewFlagSet("fake", pflag.PanicOnError) diff --git a/cmd/open.go b/cmd/open.go index a18722701..0da33b07b 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -24,7 +24,7 @@ the solution you want to see on the website. `, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - cfg := config.NewConfiguration() + cfg := config.NewConfig() v := viper.New() v.AddConfigPath(cfg.Dir) diff --git a/cmd/submit.go b/cmd/submit.go index 930311cee..2028c940f 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -28,7 +28,7 @@ var submitCmd = &cobra.Command{ Call the command with the list of files you want to submit. `, RunE: func(cmd *cobra.Command, args []string) error { - cfg := config.NewConfiguration() + cfg := config.NewConfig() usrCfg := viper.New() usrCfg.AddConfigPath(cfg.Dir) @@ -49,7 +49,7 @@ var submitCmd = &cobra.Command{ }, } -func runSubmit(cfg config.Configuration, flags *pflag.FlagSet, args []string) error { +func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { usrCfg := cfg.UserViperConfig if usrCfg.GetString("token") == "" { diff --git a/cmd/submit_symlink_test.go b/cmd/submit_symlink_test.go index a34b1f8b5..8b5781d55 100644 --- a/cmd/submit_symlink_test.go +++ b/cmd/submit_symlink_test.go @@ -51,7 +51,7 @@ func TestSubmitFilesInSymlinkedPath(t *testing.T) { v.Set("workspace", dstDir) v.Set("apibaseurl", ts.URL) - cfg := config.Configuration{ + cfg := config.Config{ Persister: config.InMemoryPersister{}, UserViperConfig: v, } diff --git a/cmd/submit_test.go b/cmd/submit_test.go index d40a55c5d..aae0f1f49 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -16,7 +16,7 @@ import ( ) func TestSubmitWithoutToken(t *testing.T) { - cfg := config.Configuration{ + cfg := config.Config{ Persister: config.InMemoryPersister{}, UserViperConfig: viper.New(), } @@ -30,7 +30,7 @@ func TestSubmitWithoutWorkspace(t *testing.T) { v := viper.New() v.Set("token", "abc123") - cfg := config.Configuration{ + cfg := config.Config{ Persister: config.InMemoryPersister{}, UserViperConfig: v, DefaultBaseURL: "http://example.com", @@ -49,7 +49,7 @@ func TestSubmitNonExistentFile(t *testing.T) { v.Set("token", "abc123") v.Set("workspace", tmpDir) - cfg := config.Configuration{ + cfg := config.Config{ Persister: config.InMemoryPersister{}, UserViperConfig: v, DefaultBaseURL: "http://example.com", @@ -85,7 +85,7 @@ func TestSubmitExerciseWithoutSolutionMetadataFile(t *testing.T) { v.Set("token", "abc123") v.Set("workspace", tmpDir) - cfg := config.Configuration{ + cfg := config.Config{ Persister: config.InMemoryPersister{}, Dir: tmpDir, UserViperConfig: v, @@ -105,7 +105,7 @@ func TestSubmitFilesAndDir(t *testing.T) { v.Set("token", "abc123") v.Set("workspace", tmpDir) - cfg := config.Configuration{ + cfg := config.Config{ Persister: config.InMemoryPersister{}, UserViperConfig: v, DefaultBaseURL: "http://example.com", @@ -165,7 +165,7 @@ func TestSubmitFiles(t *testing.T) { v.Set("workspace", tmpDir) v.Set("apibaseurl", ts.URL) - cfg := config.Configuration{ + cfg := config.Config{ Persister: config.InMemoryPersister{}, Dir: tmpDir, UserViperConfig: v, @@ -213,7 +213,7 @@ func TestSubmitWithEmptyFile(t *testing.T) { v.Set("workspace", tmpDir) v.Set("apibaseurl", ts.URL) - cfg := config.Configuration{ + cfg := config.Config{ Persister: config.InMemoryPersister{}, UserViperConfig: v, } @@ -264,7 +264,7 @@ func TestSubmitFilesForTeamExercise(t *testing.T) { v.Set("workspace", tmpDir) v.Set("apibaseurl", ts.URL) - cfg := config.Configuration{ + cfg := config.Config{ Dir: tmpDir, UserViperConfig: v, } @@ -304,7 +304,7 @@ func TestSubmitOnlyEmptyFile(t *testing.T) { v.Set("token", "abc123") v.Set("workspace", tmpDir) - cfg := config.Configuration{ + cfg := config.Config{ Persister: config.InMemoryPersister{}, UserViperConfig: v, } @@ -342,7 +342,7 @@ func TestSubmitFilesFromDifferentSolutions(t *testing.T) { v.Set("token", "abc123") v.Set("workspace", tmpDir) - cfg := config.Configuration{ + cfg := config.Config{ Persister: config.InMemoryPersister{}, Dir: tmpDir, UserViperConfig: v, @@ -406,7 +406,7 @@ func TestSubmitRelativePath(t *testing.T) { v.Set("workspace", tmpDir) v.Set("apibaseurl", ts.URL) - cfg := config.Configuration{ + cfg := config.Config{ Persister: config.InMemoryPersister{}, UserViperConfig: v, } diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go index 66c3e2a9d..a205a6783 100644 --- a/cmd/troubleshoot.go +++ b/cmd/troubleshoot.go @@ -33,7 +33,7 @@ command into a GitHub issue so we can help figure out what's going on. cli.HTTPClient = &http.Client{Timeout: 20 * time.Second} c := cli.New(Version) - cfg := config.NewConfiguration() + cfg := config.NewConfig() v := viper.New() v.AddConfigPath(cfg.Dir) @@ -63,7 +63,7 @@ type Status struct { System systemStatus Configuration configurationStatus APIReachability apiReachabilityStatus - cfg config.Configuration + cfg config.Config cli *cli.CLI } @@ -101,7 +101,7 @@ type apiPing struct { } // newStatus prepares a value to perform a diagnostic self-check. -func newStatus(cli *cli.CLI, cfg config.Configuration) Status { +func newStatus(cli *cli.CLI, cfg config.Config) Status { status := Status{ cfg: cfg, cli: cli, @@ -129,7 +129,7 @@ func (status *Status) compile() (string, error) { return bb.String(), nil } -func newAPIReachabilityStatus(cfg config.Configuration) apiReachabilityStatus { +func newAPIReachabilityStatus(cfg config.Config) apiReachabilityStatus { baseURL := cfg.UserViperConfig.GetString("apibaseurl") if baseURL == "" { baseURL = cfg.DefaultBaseURL diff --git a/cmd/workspace.go b/cmd/workspace.go index 03c1ffef4..673ab367e 100644 --- a/cmd/workspace.go +++ b/cmd/workspace.go @@ -27,7 +27,7 @@ need to be on the same drive as your workspace directory. Otherwise nothing will happen. `, RunE: func(cmd *cobra.Command, args []string) error { - cfg := config.NewConfiguration() + cfg := config.NewConfig() v := viper.New() v.AddConfigPath(cfg.Dir) diff --git a/config/config.go b/config/config.go index c072f5c37..b9b1883ba 100644 --- a/config/config.go +++ b/config/config.go @@ -2,13 +2,119 @@ package config import ( "fmt" + "os" + "path/filepath" "regexp" + "runtime" + "strings" + + "github.com/spf13/viper" ) var ( defaultBaseURL = "https://api.exercism.io/v1" + + // DefaultDirName is the default name used for config and workspace directories. + DefaultDirName string ) +// Config lets us inject configuration options into commands. +type Config struct { + OS string + Home string + Dir string + DefaultBaseURL string + DefaultDirName string + UserViperConfig *viper.Viper + Persister Persister +} + +// NewConfig provides a configuration with default values. +func NewConfig() Config { + home := userHome() + dir := Dir() + + return Config{ + OS: runtime.GOOS, + Dir: Dir(), + Home: home, + DefaultBaseURL: defaultBaseURL, + DefaultDirName: DefaultDirName, + Persister: FilePersister{Dir: dir}, + } +} + +// SetDefaultDirName configures the default directory name based on the name of the binary. +func SetDefaultDirName(binaryName string) { + DefaultDirName = strings.Replace(filepath.Base(binaryName), ".exe", "", 1) +} + +// Dir is the configured config home directory. +// All the cli-related config files live in this directory. +func Dir() string { + var dir string + if runtime.GOOS == "windows" { + dir = os.Getenv("APPDATA") + if dir != "" { + return filepath.Join(dir, DefaultDirName) + } + } else { + dir := os.Getenv("EXERCISM_CONFIG_HOME") + if dir != "" { + return dir + } + dir = os.Getenv("XDG_CONFIG_HOME") + if dir == "" { + dir = filepath.Join(os.Getenv("HOME"), ".config") + } + if dir != "" { + return filepath.Join(dir, DefaultDirName) + } + } + // If all else fails, use the current directory. + dir, _ = os.Getwd() + return dir +} + +func userHome() string { + var dir string + if runtime.GOOS == "windows" { + dir = os.Getenv("USERPROFILE") + if dir != "" { + return dir + } + dir = filepath.Join(os.Getenv("HOMEDRIVE"), os.Getenv("HOMEPATH")) + if dir != "" { + return dir + } + } else { + dir = os.Getenv("HOME") + if dir != "" { + return dir + } + } + // If all else fails, use the current directory. + dir, _ = os.Getwd() + return dir +} + +// DefaultWorkspaceDir provides a sensible default for the Exercism workspace. +// The default is different depending on the platform, in order to best match +// the conventions for that platform. +// It places the directory in the user's home path. +func DefaultWorkspaceDir(cfg Config) string { + dir := cfg.DefaultDirName + if cfg.OS != "linux" { + dir = strings.Title(dir) + } + return filepath.Join(cfg.Home, dir) +} + +// Save persists a viper config of the base name. +func (c Config) Save(basename string) error { + return c.Persister.Save(c.UserViperConfig, basename) +} + // InferSiteURL guesses what the website URL is. // The basis for the guess is which API we're submitting to. func InferSiteURL(apiURL string) string { diff --git a/config/configuration_test.go b/config/config_notwin_test.go similarity index 68% rename from config/configuration_test.go rename to config/config_notwin_test.go index ff64b495a..f7e4564da 100644 --- a/config/configuration_test.go +++ b/config/config_notwin_test.go @@ -11,15 +11,15 @@ import ( func TestDefaultWorkspaceDir(t *testing.T) { testCases := []struct { - cfg Configuration + cfg Config expected string }{ { - cfg: Configuration{OS: "darwin", Home: "/User/charlie", DefaultDirName: "apple"}, + cfg: Config{OS: "darwin", Home: "/User/charlie", DefaultDirName: "apple"}, expected: "/User/charlie/Apple", }, { - cfg: Configuration{OS: "linux", Home: "/home/bob", DefaultDirName: "banana"}, + cfg: Config{OS: "linux", Home: "/home/bob", DefaultDirName: "banana"}, expected: "/home/bob/banana", }, } diff --git a/config/configuration_windows_test.go b/config/config_windows_test.go similarity index 71% rename from config/configuration_windows_test.go rename to config/config_windows_test.go index b07ccab3f..e4374def3 100644 --- a/config/configuration_windows_test.go +++ b/config/config_windows_test.go @@ -9,6 +9,6 @@ import ( ) func TestDefaultWindowsWorkspaceDir(t *testing.T) { - cfg := Configuration{OS: "windows", Home: "C:\\Something", DefaultDirName: "basename"} + cfg := Config{OS: "windows", Home: "C:\\Something", DefaultDirName: "basename"} assert.Equal(t, "C:\\Something\\Basename", DefaultWorkspaceDir(cfg)) } diff --git a/config/configuration.go b/config/configuration.go deleted file mode 100644 index bf92c5b9d..000000000 --- a/config/configuration.go +++ /dev/null @@ -1,112 +0,0 @@ -package config - -import ( - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/spf13/viper" -) - -var ( - // DefaultDirName is the default name used for config and workspace directories. - DefaultDirName string -) - -// Configuration lets us inject configuration options into commands. -type Configuration struct { - OS string - Home string - Dir string - DefaultBaseURL string - DefaultDirName string - UserViperConfig *viper.Viper - Persister Persister -} - -// NewConfiguration provides a configuration with default values. -func NewConfiguration() Configuration { - home := userHome() - dir := Dir() - - return Configuration{ - OS: runtime.GOOS, - Dir: Dir(), - Home: home, - DefaultBaseURL: defaultBaseURL, - DefaultDirName: DefaultDirName, - Persister: FilePersister{Dir: dir}, - } -} - -// SetDefaultDirName configures the default directory name based on the name of the binary. -func SetDefaultDirName(binaryName string) { - DefaultDirName = strings.Replace(filepath.Base(binaryName), ".exe", "", 1) -} - -// Dir is the configured config home directory. -// All the cli-related config files live in this directory. -func Dir() string { - var dir string - if runtime.GOOS == "windows" { - dir = os.Getenv("APPDATA") - if dir != "" { - return filepath.Join(dir, DefaultDirName) - } - } else { - dir := os.Getenv("EXERCISM_CONFIG_HOME") - if dir != "" { - return dir - } - dir = os.Getenv("XDG_CONFIG_HOME") - if dir == "" { - dir = filepath.Join(os.Getenv("HOME"), ".config") - } - if dir != "" { - return filepath.Join(dir, DefaultDirName) - } - } - // If all else fails, use the current directory. - dir, _ = os.Getwd() - return dir -} - -func userHome() string { - var dir string - if runtime.GOOS == "windows" { - dir = os.Getenv("USERPROFILE") - if dir != "" { - return dir - } - dir = filepath.Join(os.Getenv("HOMEDRIVE"), os.Getenv("HOMEPATH")) - if dir != "" { - return dir - } - } else { - dir = os.Getenv("HOME") - if dir != "" { - return dir - } - } - // If all else fails, use the current directory. - dir, _ = os.Getwd() - return dir -} - -// DefaultWorkspaceDir provides a sensible default for the Exercism workspace. -// The default is different depending on the platform, in order to best match -// the conventions for that platform. -// It places the directory in the user's home path. -func DefaultWorkspaceDir(cfg Configuration) string { - dir := cfg.DefaultDirName - if cfg.OS != "linux" { - dir = strings.Title(dir) - } - return filepath.Join(cfg.Home, dir) -} - -// Save persists a viper config of the base name. -func (c Configuration) Save(basename string) error { - return c.Persister.Save(c.UserViperConfig, basename) -} From 8f2e271aecd70ab24d830783687528e0cdf28209 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 25 Jul 2018 13:15:18 -0700 Subject: [PATCH 212/544] Implement a simple Exercise type I expect this to accrue more behavior over time, but for now it's a way to handle simple exercise filepaths within the workspace. --- workspace/exercise.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 workspace/exercise.go diff --git a/workspace/exercise.go b/workspace/exercise.go new file mode 100644 index 000000000..53a7d5000 --- /dev/null +++ b/workspace/exercise.go @@ -0,0 +1,25 @@ +package workspace + +import ( + "path" + "path/filepath" +) + +// Exercise is an implementation of a problem in a track. +type Exercise struct { + Root string + Track string + Slug string +} + +// Path is the normalized relative path. +// It always has forward slashes, regardless +// of the operating system. +func (e Exercise) Path() string { + return path.Join(e.Track, e.Slug) +} + +// Filepath is the absolute path on the filesystem. +func (e Exercise) Filepath() string { + return filepath.Join(e.Root, e.Track, e.Slug) +} From a8891eee87012585334334582ee81955022686e6 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 25 Jul 2018 16:34:57 -0600 Subject: [PATCH 213/544] Provide a way of detecting all potential exercises in a workspace --- workspace/workspace.go | 40 +++++++++++++++++++++++++++++++++++++ workspace/workspace_test.go | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/workspace/workspace.go b/workspace/workspace.go index c05a0c740..09ddfbf4f 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -3,6 +3,7 @@ package workspace import ( "errors" "fmt" + "io/ioutil" "os" "path/filepath" "regexp" @@ -38,6 +39,45 @@ func New(dir string) (Workspace, error) { return Workspace{Dir: dir}, nil } +// PotentialExercises are a first-level guess at the user's exercises. +// It looks at the workspace structurally, and guesses based on +// the location of the directory. E.g. any top level directory +// within the workspace (except 'users') is assumed to be a +// track, and any directory within there again is assumed to +// be an exercise. +func (ws Workspace) PotentialExercises() ([]Exercise, error) { + exercises := []Exercise{} + + topInfos, err := ioutil.ReadDir(ws.Dir) + if err != nil { + return nil, err + } + for _, topInfo := range topInfos { + if !topInfo.IsDir() { + continue + } + + if topInfo.Name() == "users" { + continue + } + + subInfos, err := ioutil.ReadDir(filepath.Join(ws.Dir, topInfo.Name())) + if err != nil { + return nil, err + } + + for _, subInfo := range subInfos { + if !subInfo.IsDir() { + continue + } + + exercises = append(exercises, Exercise{Track: topInfo.Name(), Slug: subInfo.Name(), Root: ws.Dir}) + } + } + + return exercises, nil +} + // Locate the matching directories within the workspace. // This will look for an exact match on absolute or relative paths. // If given the base name of a directory with no path information it diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go index 8b041e05a..66dfc9ee6 100644 --- a/workspace/workspace_test.go +++ b/workspace/workspace_test.go @@ -5,11 +5,50 @@ import ( "os" "path/filepath" "runtime" + "sort" "testing" "github.com/stretchr/testify/assert" ) +func TestWorkspacePotentialExercises(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "walk") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + a1 := filepath.Join(tmpDir, "track-a", "exercise-one") + b1 := filepath.Join(tmpDir, "track-b", "exercise-one") + b2 := filepath.Join(tmpDir, "track-b", "exercise-two") + + // It should ignore other people's exercises. + alice := filepath.Join(tmpDir, "users", "alice", "track-a", "exercise-one") + + // It should ignore nested dirs within exercises. + nested := filepath.Join(a1, "subdir", "deeper-dir", "another-deep-dir") + + for _, path := range []string{a1, b1, b2, alice, nested} { + err := os.MkdirAll(path, os.FileMode(0755)) + assert.NoError(t, err) + } + + ws, err := New(tmpDir) + assert.NoError(t, err) + + exercises, err := ws.PotentialExercises() + assert.NoError(t, err) + if assert.Equal(t, 3, len(exercises)) { + paths := make([]string, len(exercises)) + for i, e := range exercises { + paths[i] = e.Path() + } + + sort.Strings(paths) + assert.Equal(t, paths[0], "track-a/exercise-one") + assert.Equal(t, paths[1], "track-b/exercise-one") + assert.Equal(t, paths[2], "track-b/exercise-two") + } +} + func TestSolutionPath(t *testing.T) { root := filepath.Join("..", "fixtures", "solution-path", "creatures") ws, err := New(root) From 48f85fbaee5f78741fca20d0299b02e55f023747 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Fri, 3 Aug 2018 19:14:53 -0600 Subject: [PATCH 214/544] Add method on exercise to check for metadata --- workspace/exercise.go | 20 ++++++++++++++++++++ workspace/exercise_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 workspace/exercise_test.go diff --git a/workspace/exercise.go b/workspace/exercise.go index 53a7d5000..e8be3eca0 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -1,6 +1,7 @@ package workspace import ( + "os" "path" "path/filepath" ) @@ -23,3 +24,22 @@ func (e Exercise) Path() string { func (e Exercise) Filepath() string { return filepath.Join(e.Root, e.Track, e.Slug) } + +// MetadataFilepath is the absolute path to the exercise metadata. +func (e Exercise) MetadataFilepath() string { + return filepath.Join(e.Filepath(), solutionFilename) +} + +// HasMetadata checks for the presence of an exercise metadata file. +// If there is no such file, this may be a legacy exercise. +// It could also be an unrelated directory. +func (e Exercise) HasMetadata() (bool, error) { + _, err := os.Lstat(e.MetadataFilepath()) + if os.IsNotExist(err) { + return false, nil + } + if err == nil { + return true, nil + } + return false, err +} diff --git a/workspace/exercise_test.go b/workspace/exercise_test.go new file mode 100644 index 000000000..83b794064 --- /dev/null +++ b/workspace/exercise_test.go @@ -0,0 +1,35 @@ +package workspace + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHasMetadata(t *testing.T) { + ws, err := ioutil.TempDir("", "fake-workspace") + defer os.RemoveAll(ws) + assert.NoError(t, err) + + exerciseA := Exercise{Root: ws, Track: "bogus-track", Slug: "apple"} + exerciseB := Exercise{Root: ws, Track: "bogus-track", Slug: "banana"} + + err = os.MkdirAll(filepath.Join(exerciseA.Filepath()), os.FileMode(0755)) + assert.NoError(t, err) + err = os.MkdirAll(filepath.Join(exerciseB.Filepath()), os.FileMode(0755)) + assert.NoError(t, err) + + err = ioutil.WriteFile(exerciseA.MetadataFilepath(), []byte{}, os.FileMode(0600)) + assert.NoError(t, err) + + ok, err := exerciseA.HasMetadata() + assert.NoError(t, err) + assert.True(t, ok) + + ok, err = exerciseB.HasMetadata() + assert.NoError(t, err) + assert.False(t, ok) +} From f6a2c436f5130858120134656987efd3b35ef09d Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Fri, 3 Aug 2018 19:26:08 -0600 Subject: [PATCH 215/544] Add method for detecting actual exercises in a workspace --- workspace/workspace.go | 21 ++++++++++++++++++++ workspace/workspace_test.go | 38 +++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/workspace/workspace.go b/workspace/workspace.go index 09ddfbf4f..b6a378543 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -78,6 +78,27 @@ func (ws Workspace) PotentialExercises() ([]Exercise, error) { return exercises, nil } +// Exercises returns the user's exercises within the workspace. +// This doesn't find legacy exercises where the metadata is missing. +func (ws Workspace) Exercises() ([]Exercise, error) { + candidates, err := ws.PotentialExercises() + if err != nil { + return nil, err + } + + exercises := make([]Exercise, 0, len(candidates)) + for _, candidate := range candidates { + ok, err := candidate.HasMetadata() + if err != nil { + return nil, err + } + if ok { + exercises = append(exercises, candidate) + } + } + return exercises, nil +} + // Locate the matching directories within the workspace. // This will look for an exact match on absolute or relative paths. // If given the base name of a directory with no path information it diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go index 66dfc9ee6..31097b617 100644 --- a/workspace/workspace_test.go +++ b/workspace/workspace_test.go @@ -49,6 +49,44 @@ func TestWorkspacePotentialExercises(t *testing.T) { } } +func TestWorkspaceExercises(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "walk-with-metadata") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + a1 := filepath.Join(tmpDir, "track-a", "exercise-one") + a2 := filepath.Join(tmpDir, "track-a", "exercise-two") // no metadata + b1 := filepath.Join(tmpDir, "track-b", "exercise-one") + b2 := filepath.Join(tmpDir, "track-b", "exercise-two") + + for _, path := range []string{a1, a2, b1, b2} { + err := os.MkdirAll(path, os.FileMode(0755)) + assert.NoError(t, err) + + if path != a2 { + err = ioutil.WriteFile(filepath.Join(path, solutionFilename), []byte{}, os.FileMode(0600)) + assert.NoError(t, err) + } + } + + ws, err := New(tmpDir) + assert.NoError(t, err) + + exercises, err := ws.Exercises() + assert.NoError(t, err) + if assert.Equal(t, 3, len(exercises)) { + paths := make([]string, len(exercises)) + for i, e := range exercises { + paths[i] = e.Path() + } + + sort.Strings(paths) + assert.Equal(t, paths[0], "track-a/exercise-one") + assert.Equal(t, paths[1], "track-b/exercise-one") + assert.Equal(t, paths[2], "track-b/exercise-two") + } +} + func TestSolutionPath(t *testing.T) { root := filepath.Join("..", "fixtures", "solution-path", "creatures") ws, err := New(root) From 07f6e9def270c2fd5370f07e3bbe310f1a2ec9a3 Mon Sep 17 00:00:00 2001 From: William Andrade Date: Tue, 17 Jul 2018 13:12:17 -0300 Subject: [PATCH 216/544] Fix the encode problem --- cmd/download.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/cmd/download.go b/cmd/download.go index 206ee9386..0e782d40a 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + netURL "net/url" "os" "path/filepath" "strings" @@ -171,7 +172,15 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { } for _, file := range payload.Solution.Files { - url := fmt.Sprintf("%s%s", payload.Solution.FileDownloadBaseURL, file) + unparsedUrl := fmt.Sprintf("%s%s", payload.Solution.FileDownloadBaseURL, file) + parsedUrl, err := netURL.ParseRequestURI(unparsedUrl) + + if err != nil { + return err + } + + url := parsedUrl.String() + req, err := client.NewRequest("GET", url, nil) if err != nil { return err From cd03d2eddcaa901f16a37b84e83fa20094b1f111 Mon Sep 17 00:00:00 2001 From: William Andrade Date: Wed, 25 Jul 2018 11:07:57 -0300 Subject: [PATCH 217/544] Adding a special character to a file for the test case --- cmd/download_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index 2c3200c2e..97f0d9e4f 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -162,7 +162,7 @@ func fakeDownloadServer(requestor, teamSlug string) *httptest.Server { fmt.Fprint(w, "this is file 1") }) - mux.HandleFunc("/subdir/file-2.txt", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/subdir/file-2#.txt", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "this is file 2") }) @@ -199,7 +199,7 @@ func assertDownloadedCorrectFiles(t *testing.T, targetDir string) { }, { desc: "a file in a subdirectory", - path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "subdir", "file-2.txt"), + path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "subdir", "file-2#.txt"), contents: "this is file 2", }, } From 1252e1b06231a2458df08b4c0154932f607e6a2f Mon Sep 17 00:00:00 2001 From: William Andrade Date: Tue, 31 Jul 2018 18:08:11 -0300 Subject: [PATCH 218/544] change the test because of the commit 8572e980e811aff3249d980fe04644a51ee5944c --- cmd/download_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index 97f0d9e4f..4aaefa0d8 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -238,7 +238,7 @@ const payloadTemplate = ` "file_download_base_url": "%s", "files": [ "/file-1.txt", - "/subdir/file-2.txt", + "/subdir/file-2#.txt", "/file-3.txt" ], "iteration": { From 75a9c5079d94e4ee5946dfed1de6c55e51cbba24 Mon Sep 17 00:00:00 2001 From: nywilken Date: Sun, 5 Aug 2018 09:28:27 -0400 Subject: [PATCH 219/544] add specific test for URL encoded filenames --- cmd/download_test.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index 4aaefa0d8..140c8d010 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -162,10 +162,14 @@ func fakeDownloadServer(requestor, teamSlug string) *httptest.Server { fmt.Fprint(w, "this is file 1") }) - mux.HandleFunc("/subdir/file-2#.txt", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/subdir/file-2.txt", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "this is file 2") }) + mux.HandleFunc("/special-char-filename#.txt", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "this is a special file") + }) + mux.HandleFunc("/file-3.txt", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "") }) @@ -199,9 +203,14 @@ func assertDownloadedCorrectFiles(t *testing.T, targetDir string) { }, { desc: "a file in a subdirectory", - path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "subdir", "file-2#.txt"), + path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "subdir", "file-2.txt"), contents: "this is file 2", }, + { + desc: "a file that requires URL encoding", + path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "special-char-filename#.txt"), + contents: "this is a special file", + }, } for _, file := range expectedFiles { @@ -238,7 +247,8 @@ const payloadTemplate = ` "file_download_base_url": "%s", "files": [ "/file-1.txt", - "/subdir/file-2#.txt", + "/subdir/file-2.txt", + "/special-char-filename#.txt", "/file-3.txt" ], "iteration": { From 1bb612f326ba3b87588c7c19ff0128eb539c922d Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 7 Aug 2018 16:19:10 -0600 Subject: [PATCH 220/544] Get rid of the workspace.Locate behavior and comms package (#696) * Simplify open command This no longer accepts just an exercise name. It mirrors the behavior of the submit command where you pass the path to the exercise you want to open in the browser. In the future we can expand this to take --exercise, --track, and --team. * Simplify submit command We determine the exact exercise directory based on the filepaths. Since we have the directory, we don't need to do complicated guessing, we can just load in the exercise metadata directly. * Rename slug to param in download command for clarity * Rename exercise to slug in download for clarity * Rename dir variable in download command for clarity We were overloading the variable, which makes it confusing. * Add missing error handling in download command * Add metadata dir method to exercise type We're going to want to ensure that the directory exists. For the moment, this directory is the same as the exercise directory, but we're working towards putting this in a subdirectory to match common industry conventions. * Simplify download command to rely on Exercise type * Get rid of solution path abstraction in workspace We've moved the idea of the exercise metadata path onto the Exercise type. * Delete complicated Locate behavior in workspace It's no longer used. * Delete unused comms package * Tweak download command for clarity Simplify a conditional. --- cmd/download.go | 38 ++++--- cmd/open.go | 72 +------------ cmd/submit.go | 17 +--- comms/question.go | 36 ------- comms/question_test.go | 38 ------- comms/selection.go | 78 -------------- comms/selection_test.go | 128 ----------------------- workspace/exercise.go | 6 ++ workspace/workspace.go | 146 --------------------------- workspace/workspace_locate_test.go | 139 ------------------------- workspace/workspace_symlinks_test.go | 53 ---------- workspace/workspace_test.go | 130 ------------------------ 12 files changed, 28 insertions(+), 853 deletions(-) delete mode 100644 comms/question.go delete mode 100644 comms/question_test.go delete mode 100644 comms/selection.go delete mode 100644 comms/selection_test.go delete mode 100644 workspace/workspace_locate_test.go delete mode 100644 workspace/workspace_symlinks_test.go diff --git a/cmd/download.go b/cmd/download.go index 0e782d40a..031ba877e 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -60,21 +60,19 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { if err != nil { return err } - exercise, err := flags.GetString("exercise") + slug, err := flags.GetString("exercise") if err != nil { return err } - if uuid == "" && exercise == "" { + if uuid == "" && slug == "" { return errors.New("need an --exercise name or a solution --uuid") } - var slug string - if uuid == "" { - slug = "latest" - } else { - slug = uuid + param := "latest" + if param == "" { + param = uuid } - url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), slug) + url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), param) client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl")) if err != nil { @@ -98,7 +96,7 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { if uuid == "" { q := req.URL.Query() - q.Add("exercise_id", exercise) + q.Add("exercise_id", slug) if track != "" { q.Add("track_id", track) } @@ -144,28 +142,26 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { IsRequester: payload.Solution.User.IsRequester, } - dir := usrCfg.GetString("workspace") + root := usrCfg.GetString("workspace") if solution.Team != "" { - dir = filepath.Join(dir, "teams", solution.Team) + root = filepath.Join(root, "teams", solution.Team) } if !solution.IsRequester { - dir = filepath.Join(dir, "users", solution.Handle) + root = filepath.Join(root, "users", solution.Handle) } - dir = filepath.Join(dir, solution.Track) - os.MkdirAll(dir, os.FileMode(0755)) - ws, err := workspace.New(dir) - if err != nil { - return err + exercise := workspace.Exercise{ + Root: root, + Track: solution.Track, + Slug: solution.Exercise, } - dir, err = ws.SolutionPath(solution.Exercise, solution.ID) - if err != nil { + dir := exercise.MetadataDir() + + if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { return err } - os.MkdirAll(dir, os.FileMode(0755)) - err = solution.Write(dir) if err != nil { return err diff --git a/cmd/open.go b/cmd/open.go index 0da33b07b..15166b255 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -1,15 +1,9 @@ package cmd import ( - "errors" - "fmt" - "github.com/exercism/cli/browser" - "github.com/exercism/cli/comms" - "github.com/exercism/cli/config" "github.com/exercism/cli/workspace" "github.com/spf13/cobra" - "github.com/spf13/viper" ) // openCmd opens the designated exercise in the browser. @@ -19,74 +13,16 @@ var openCmd = &cobra.Command{ Short: "Open an exercise on the website.", Long: `Open the specified exercise to the solution page on the Exercism website. -Pass either the name of an exercise, or the path to the directory that contains -the solution you want to see on the website. +Pass the path to the directory that contains the solution you want to see on the website. `, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - cfg := config.NewConfig() - - v := viper.New() - v.AddConfigPath(cfg.Dir) - v.SetConfigName("user") - v.SetConfigType("json") - // Ignore error. If the file doesn't exist, that is fine. - _ = v.ReadInConfig() - - ws, err := workspace.New(v.GetString("workspace")) - if err != nil { - return err - } - - paths, err := ws.Locate(args[0]) + solution, err := workspace.NewSolution(args[0]) if err != nil { return err } - - solutions, err := workspace.NewSolutions(paths) - if err != nil { - return err - } - - if len(solutions) == 0 { - return nil - } - - if len(solutions) > 1 { - var mine []*workspace.Solution - for _, s := range solutions { - if s.IsRequester { - mine = append(mine, s) - } - } - solutions = mine - } - - selection := comms.NewSelection() - for _, solution := range solutions { - selection.Items = append(selection.Items, solution) - } - for { - prompt := ` -We found more than one. Which one did you mean? -Type the number of the one you want to select. - -%s -> ` - option, err := selection.Pick(prompt) - if err != nil { - fmt.Println(err) - continue - } - solution, ok := option.(*workspace.Solution) - if ok { - browser.Open(solution.URL) - return nil - } - if err != nil { - return errors.New("should never happen") - } - } + browser.Open(solution.URL) + return nil }, } diff --git a/cmd/submit.go b/cmd/submit.go index 2028c940f..507808408 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -125,26 +125,11 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { exerciseDir = dir } - dirs, err := ws.Locate(exerciseDir) + solution, err := workspace.NewSolution(exerciseDir) if err != nil { return err } - sx, err := workspace.NewSolutions(dirs) - if err != nil { - return err - } - if len(sx) > 1 { - msg := ` - - You are submitting files belonging to different solutions. - Please submit the files for one solution at a time. - - ` - return errors.New(msg) - } - solution := sx[0] - if !solution.IsRequester { // TODO: add test msg := ` diff --git a/comms/question.go b/comms/question.go deleted file mode 100644 index f9afd6a1d..000000000 --- a/comms/question.go +++ /dev/null @@ -1,36 +0,0 @@ -package comms - -import ( - "bufio" - "fmt" - "io" - "strings" -) - -// Question provides an interactive session. -type Question struct { - Reader io.Reader - Writer io.Writer - Prompt string - DefaultValue string -} - -// Read reads the user's input. -func (q Question) Read(r io.Reader) (string, error) { - reader := bufio.NewReader(r) - s, err := reader.ReadString('\n') - if err != nil { - return "", err - } - s = strings.TrimSpace(s) - if s == "" { - return q.DefaultValue, nil - } - return s, nil -} - -// Ask displays the prompt, then records the response. -func (q *Question) Ask() (string, error) { - fmt.Fprintf(q.Writer, q.Prompt) - return q.Read(q.Reader) -} diff --git a/comms/question_test.go b/comms/question_test.go deleted file mode 100644 index 1b77dad60..000000000 --- a/comms/question_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package comms - -import ( - "io/ioutil" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestQuestion(t *testing.T) { - testCases := []struct { - desc string - given string - fallback string - expected string - }{ - {"records interactive response", "hello\n", "", "hello"}, - {"responds with default if response is empty", "\n", "Fine.", "Fine."}, - {"removes trailing \\r in addition to trailing \\", "hello\r\n", "Fine.", "hello"}, - {"removes trailing white spaces", "hello \n", "Fine.", "hello"}, - {"falls back to default value", " \n", "Default", "Default"}, - } - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - q := &Question{ - Reader: strings.NewReader(tc.given), - Writer: ioutil.Discard, - Prompt: "Say something: ", - DefaultValue: tc.fallback, - } - - answer, err := q.Ask() - assert.NoError(t, err) - assert.Equal(t, answer, tc.expected) - }) - } -} diff --git a/comms/selection.go b/comms/selection.go deleted file mode 100644 index 5d760864f..000000000 --- a/comms/selection.go +++ /dev/null @@ -1,78 +0,0 @@ -package comms - -import ( - "bufio" - "errors" - "fmt" - "io" - "os" - "strconv" - "strings" -) - -// Selection wraps a list of items. -// It is used for interactive communication. -type Selection struct { - Items []fmt.Stringer - Reader io.Reader - Writer io.Writer -} - -// NewSelection prepares an empty collection for interactive input. -func NewSelection() Selection { - return Selection{ - Reader: os.Stdin, - Writer: os.Stdout, - } -} - -// Pick lets a user interactively select an option from a list. -func (sel Selection) Pick(prompt string) (fmt.Stringer, error) { - // If there's just one, then we're done here. - if len(sel.Items) == 1 { - return sel.Items[0], nil - } - - fmt.Fprintf(sel.Writer, prompt, sel.Display()) - - n, err := sel.Read(sel.Reader) - if err != nil { - return nil, err - } - - o, err := sel.Get(n) - if err != nil { - return nil, err - } - return o, nil -} - -// Display shows a numbered list of the solutions to choose from. -// The list starts at 1, since that seems better in a user interface. -func (sel Selection) Display() string { - str := "" - for i, item := range sel.Items { - str += fmt.Sprintf(" [%d] %s\n", i+1, item) - } - return str -} - -// Read reads the user's selection and converts it to a number. -func (sel Selection) Read(r io.Reader) (int, error) { - reader := bufio.NewReader(r) - text, _ := reader.ReadString('\n') - n, err := strconv.Atoi(strings.TrimSpace(text)) - if err != nil { - return 0, err - } - return n, nil -} - -// Get returns the solution corresponding to the number. -// The list starts at 1, since that seems better in a user interface. -func (sel Selection) Get(n int) (fmt.Stringer, error) { - if n <= 0 || n > len(sel.Items) { - return nil, errors.New("we don't have that one") - } - return sel.Items[n-1], nil -} diff --git a/comms/selection_test.go b/comms/selection_test.go deleted file mode 100644 index bbf2ce16e..000000000 --- a/comms/selection_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package comms - -import ( - "fmt" - "io/ioutil" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -type thing struct { - name string - rating int -} - -func (t thing) String() string { - return fmt.Sprintf("%s (+%d)", t.name, t.rating) -} - -var ( - things = []thing{ - {name: "water", rating: 10}, - {name: "food", rating: 3}, - {name: "music", rating: 0}, - } -) - -func TestSelectionDisplay(t *testing.T) { - // We have to manually add each thing to the options collection. - var sel Selection - for _, thing := range things { - sel.Items = append(sel.Items, thing) - } - - display := " [1] water (+10)\n [2] food (+3)\n [3] music (+0)\n" - assert.Equal(t, display, sel.Display()) -} - -func TestSelectionGet(t *testing.T) { - var sel Selection - for _, thing := range things { - sel.Items = append(sel.Items, thing) - } - - _, err := sel.Get(0) - assert.Error(t, err) - - o, err := sel.Get(1) - assert.NoError(t, err) - // We need to do a type assertion to access - // any non-stringer stuff. - t1 := o.(thing) - assert.Equal(t, "water", t1.name) - - o, err = sel.Get(2) - assert.NoError(t, err) - t2 := o.(thing) - assert.Equal(t, "food", t2.name) - - o, err = sel.Get(3) - assert.NoError(t, err) - t3 := o.(thing) - assert.Equal(t, "music", t3.name) - - _, err = sel.Get(4) - assert.Error(t, err) -} - -func TestSelectionRead(t *testing.T) { - var sel Selection - n, err := sel.Read(strings.NewReader("5")) - assert.NoError(t, err) - assert.Equal(t, 5, n) - - _, err = sel.Read(strings.NewReader("abc")) - assert.Error(t, err) -} - -func TestSelectionPick(t *testing.T) { - testCases := []struct { - desc string - selection Selection - things []thing - expected string - }{ - { - desc: "autoselect the only one", - selection: Selection{ - // it never hits the error, - // because it doesn't actually do - // the prompt and read response. - Reader: strings.NewReader("BOOM!"), - }, - things: []thing{ - {"hugs", 100}, - }, - expected: "hugs", - }, - { - desc: "it picks the one corresponding to the selection", - selection: Selection{ - Reader: strings.NewReader("2"), - }, - things: []thing{ - {"food", 10}, - {"water", 3}, - {"music", 0}, - }, - expected: "water", - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - tc.selection.Writer = ioutil.Discard - for _, th := range tc.things { - tc.selection.Items = append(tc.selection.Items, th) - } - - item, err := tc.selection.Pick("which one? %s") - assert.NoError(t, err) - th, ok := item.(thing) - assert.True(t, ok) - assert.Equal(t, tc.expected, th.name) - }) - } -} diff --git a/workspace/exercise.go b/workspace/exercise.go index e8be3eca0..246cf364f 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -30,6 +30,12 @@ func (e Exercise) MetadataFilepath() string { return filepath.Join(e.Filepath(), solutionFilename) } +// MetadataDir returns the directory that the exercise metadata lives in. +// For now this is the exercise directory. +func (e Exercise) MetadataDir() string { + return e.Filepath() +} + // HasMetadata checks for the presence of an exercise metadata file. // If there is no such file, this may be a legacy exercise. // It could also be an unrelated directory. diff --git a/workspace/workspace.go b/workspace/workspace.go index b6a378543..0e9092e41 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -2,7 +2,6 @@ package workspace import ( "errors" - "fmt" "io/ioutil" "os" "path/filepath" @@ -99,151 +98,6 @@ func (ws Workspace) Exercises() ([]Exercise, error) { return exercises, nil } -// Locate the matching directories within the workspace. -// This will look for an exact match on absolute or relative paths. -// If given the base name of a directory with no path information it -// It will look for all directories with that name, or that are -// named with a numerical suffix. -func (ws Workspace) Locate(exercise string) ([]string, error) { - // First assume it's a path. - dir := exercise - - // If it's not an absolute path, make it one. - if !filepath.IsAbs(dir) { - var err error - dir, err = filepath.Abs(dir) - if err != nil { - return nil, err - } - } - - // If it exists, we were right. It's a path. - if _, err := os.Stat(dir); err == nil { - if !strings.HasPrefix(dir, ws.Dir) { - return nil, ErrNotInWorkspace(exercise) - } - - src, err := filepath.EvalSymlinks(dir) - if err == nil { - return []string{src}, nil - } - } - - // If the argument is a path, then we should have found it by now. - if strings.Contains(exercise, string(os.PathSeparator)) { - return nil, ErrNotExist(exercise) - } - - var paths []string - // Look through the entire workspace tree to find any matches. - walkFn := func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - // If it's a symlink, follow it, then get the file info of the target. - if info.Mode()&os.ModeSymlink == os.ModeSymlink { - src, err := filepath.EvalSymlinks(path) - if err == nil { - path = src - } - info, err = os.Lstat(path) - if err != nil { - return err - } - } - - if !info.IsDir() { - return nil - } - - if strings.HasPrefix(filepath.Base(path), exercise) { - // We're trying to find any directories that match either the exact name - // or the name with a numeric suffix. - // E.g. if passed 'bat', then we should match 'bat', 'bat-2', 'bat-200', - // but not 'batten'. - suffix := strings.Replace(filepath.Base(path), exercise, "", 1) - if suffix == "" || rgxSerialSuffix.MatchString(suffix) { - paths = append(paths, path) - } - } - return nil - } - - // If the workspace directory is a symlink, resolve that first. - root := ws.Dir - src, err := filepath.EvalSymlinks(root) - if err == nil { - root = src - } - - filepath.Walk(root, walkFn) - - if len(paths) == 0 { - return nil, ErrNotExist(exercise) - } - return paths, nil -} - -// SolutionPath returns the full path where the exercise will be stored. -// By default this the directory name matches that of the exercise, but if -// a different solution already exists, then a numeric suffix will be added -// to the name. -func (ws Workspace) SolutionPath(exercise, solutionID string) (string, error) { - paths, err := ws.Locate(exercise) - if !IsNotExist(err) && err != nil { - return "", err - } - - return ws.ResolveSolutionPath(paths, exercise, solutionID, IsSolutionPath) -} - -// IsSolutionPath checks whether the given path contains the solution with the given ID. -func IsSolutionPath(solutionID, path string) (bool, error) { - s, err := NewSolution(path) - if os.IsNotExist(err) { - return false, nil - } - if err != nil { - return false, err - } - return s.ID == solutionID, nil -} - -// ResolveSolutionPath determines the path for the given exercise solution. -// It will locate an existing path, or indicate the name of a new path, if this is a new solution. -func (ws Workspace) ResolveSolutionPath(paths []string, exercise, solutionID string, existsFn func(string, string) (bool, error)) (string, error) { - // Do we already have a directory for this solution? - for _, path := range paths { - ok, err := existsFn(solutionID, path) - if err != nil { - return "", err - } - if ok { - return path, nil - } - } - // If we didn't find the solution in one of the paths that - // were passed in, we're going to construct some new ones - // using a numeric suffix. Create a lookup table so we can - // reject constructed paths if they match existing ones. - m := map[string]bool{} - for _, path := range paths { - m[path] = true - } - suffix := 1 - root := filepath.Join(ws.Dir, exercise) - path := root - for { - exists := m[path] - if !exists { - return path, nil - } - suffix++ - 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) { diff --git a/workspace/workspace_locate_test.go b/workspace/workspace_locate_test.go deleted file mode 100644 index 837657b97..000000000 --- a/workspace/workspace_locate_test.go +++ /dev/null @@ -1,139 +0,0 @@ -// +build !windows - -package workspace - -import ( - "fmt" - "path/filepath" - "runtime" - "sort" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLocateErrors(t *testing.T) { - _, cwd, _, _ := runtime.Caller(0) - root := filepath.Join(cwd, "..", "..", "fixtures", "locate-exercise") - - ws, err := New(filepath.Join(root, "workspace")) - assert.NoError(t, err) - - testCases := []struct { - desc, arg string - errFn func(error) bool - }{ - { - desc: "absolute path outside of workspace", - arg: filepath.Join(root, "equipment", "bat"), - errFn: IsNotInWorkspace, - }, - { - desc: "absolute path in workspace not found", - arg: filepath.Join(ws.Dir, "creatures", "pig"), - errFn: IsNotExist, - }, - { - desc: "relative path is outside of workspace", - arg: filepath.Join("..", "fixtures", "locate-exercise", "equipment", "bat"), - errFn: IsNotInWorkspace, - }, - { - desc: "relative path in workspace not found", - arg: filepath.Join("..", "fixtures", "locate-exercise", "workspace", "creatures", "pig"), - errFn: IsNotExist, - }, - { - desc: "exercise name not found in workspace", - arg: "pig", - errFn: IsNotExist, - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - _, err := ws.Locate(tc.arg) - assert.True(t, tc.errFn(err), fmt.Sprintf("test: %s (arg: %s), %#v", tc.desc, tc.arg, err)) - }) - } -} - -type locateTestCase struct { - desc string - workspace Workspace - in string - out []string -} - -func TestLocate(t *testing.T) { - _, cwd, _, _ := runtime.Caller(0) - root := filepath.Join(cwd, "..", "..", "fixtures", "locate-exercise") - - wsPrimary, err := New(filepath.Join(root, "workspace")) - assert.NoError(t, err) - - testCases := []locateTestCase{ - { - desc: "find absolute path within workspace", - workspace: wsPrimary, - in: filepath.Join(wsPrimary.Dir, "creatures", "horse"), - out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, - }, - { - desc: "find relative path within workspace", - workspace: wsPrimary, - in: filepath.Join("..", "fixtures", "locate-exercise", "workspace", "creatures", "horse"), - out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, - }, - { - desc: "find by name in default location", - workspace: wsPrimary, - in: "horse", - out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, - }, - { - desc: "find by name in a subtree", - workspace: wsPrimary, - in: "fly", - out: []string{filepath.Join(wsPrimary.Dir, "friends", "alice", "creatures", "fly")}, - }, - { - desc: "don't be confused by a file named the same as an exercise", - workspace: wsPrimary, - in: "duck", - out: []string{filepath.Join(wsPrimary.Dir, "creatures", "duck")}, - }, - { - desc: "find all the exercises with the same name", - workspace: wsPrimary, - in: "bat", - out: []string{ - filepath.Join(wsPrimary.Dir, "creatures", "bat"), - filepath.Join(wsPrimary.Dir, "friends", "alice", "creatures", "bat"), - }, - }, - { - desc: "find copies of exercise with suffix", - workspace: wsPrimary, - in: "crane", - out: []string{ - filepath.Join(wsPrimary.Dir, "creatures", "crane"), - filepath.Join(wsPrimary.Dir, "creatures", "crane-2"), - }, - }, - } - - testLocate(testCases, t) -} - -func testLocate(testCases []locateTestCase, t *testing.T) { - for _, tc := range testCases { - dirs, err := tc.workspace.Locate(tc.in) - - sort.Strings(dirs) - sort.Strings(tc.out) - - assert.NoError(t, err, tc.desc) - assert.Equal(t, tc.out, dirs, tc.desc) - } -} diff --git a/workspace/workspace_symlinks_test.go b/workspace/workspace_symlinks_test.go deleted file mode 100644 index 83718369c..000000000 --- a/workspace/workspace_symlinks_test.go +++ /dev/null @@ -1,53 +0,0 @@ -// +build !windows - -package workspace - -import ( - "path/filepath" - "runtime" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLocateSymlinks(t *testing.T) { - _, cwd, _, _ := runtime.Caller(0) - root := filepath.Join(cwd, "..", "..", "fixtures", "locate-exercise") - - wsSymbolic, err := New(filepath.Join(root, "symlinked-workspace")) - assert.NoError(t, err) - wsPrimary, err := New(filepath.Join(root, "workspace")) - assert.NoError(t, err) - - testCases := []locateTestCase{ - { - desc: "find absolute path within symlinked workspace", - workspace: wsSymbolic, - in: filepath.Join(wsSymbolic.Dir, "creatures", "horse"), - out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, - }, - { - desc: "find by name in a symlinked workspace", - workspace: wsSymbolic, - in: "horse", - out: []string{filepath.Join(wsPrimary.Dir, "creatures", "horse")}, - }, - { - desc: "don't be confused by a symlinked file named the same as an exercise", - workspace: wsPrimary, - in: "date", - out: []string{filepath.Join(wsPrimary.Dir, "actions", "date")}, - }, - { - desc: "find exercises that are symlinks", - workspace: wsPrimary, - in: "squash", - out: []string{ - filepath.Join(wsPrimary.Dir, "..", "food", "squash"), - filepath.Join(wsPrimary.Dir, "actions", "squash"), - }, - }, - } - - testLocate(testCases, t) -} diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go index 31097b617..b48f3cf35 100644 --- a/workspace/workspace_test.go +++ b/workspace/workspace_test.go @@ -87,136 +87,6 @@ func TestWorkspaceExercises(t *testing.T) { } } -func TestSolutionPath(t *testing.T) { - root := filepath.Join("..", "fixtures", "solution-path", "creatures") - ws, err := New(root) - assert.NoError(t, err) - - // An existing exercise. - path, err := ws.SolutionPath("gazelle", "ccc") - assert.NoError(t, err) - assert.Equal(t, filepath.Join(root, "gazelle-3"), path) - - path, err = ws.SolutionPath("gazelle", "abc") - assert.NoError(t, err) - assert.Equal(t, filepath.Join(root, "gazelle-4"), path) - - // A new exercise. - path, err = ws.SolutionPath("lizard", "abc") - assert.NoError(t, err) - assert.Equal(t, filepath.Join(root, "lizard"), path) -} - -func TestIsSolutionPath(t *testing.T) { - root := filepath.Join("..", "fixtures", "is-solution-path") - - ok, err := IsSolutionPath("abc", filepath.Join(root, "yepp")) - assert.NoError(t, err) - assert.True(t, ok) - - // The ID has to actually match. - ok, err = IsSolutionPath("xxx", filepath.Join(root, "yepp")) - assert.NoError(t, err) - assert.False(t, ok) - - ok, err = IsSolutionPath("abc", filepath.Join(root, "nope")) - assert.NoError(t, err) - assert.False(t, ok) - - _, err = IsSolutionPath("abc", filepath.Join(root, "broken")) - assert.Error(t, err) -} - -func TestResolveSolutionPath(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "resolve-solution-path") - defer os.RemoveAll(tmpDir) - ws, err := New(tmpDir) - assert.NoError(t, err) - - existsFn := func(solutionID, path string) (bool, error) { - pathToSolutionID := map[string]string{ - filepath.Join(ws.Dir, "pig"): "xxx", - filepath.Join(ws.Dir, "gecko"): "aaa", - filepath.Join(ws.Dir, "gecko-2"): "xxx", - filepath.Join(ws.Dir, "gecko-3"): "ccc", - filepath.Join(ws.Dir, "bat"): "aaa", - filepath.Join(ws.Dir, "dog"): "aaa", - filepath.Join(ws.Dir, "dog-2"): "bbb", - filepath.Join(ws.Dir, "dog-3"): "ccc", - filepath.Join(ws.Dir, "rabbit"): "aaa", - filepath.Join(ws.Dir, "rabbit-2"): "bbb", - filepath.Join(ws.Dir, "rabbit-4"): "ccc", - } - return pathToSolutionID[path] == solutionID, nil - } - - tests := []struct { - desc string - paths []string - exercise string - expected string - }{ - { - desc: "If we don't have that exercise yet, it gets the default name.", - exercise: "duck", - paths: []string{}, - expected: filepath.Join(ws.Dir, "duck"), - }, - { - desc: "If we already have a directory for the solution in question, return it.", - exercise: "pig", - paths: []string{ - filepath.Join(ws.Dir, "pig"), - }, - expected: filepath.Join(ws.Dir, "pig"), - }, - { - desc: "If we already have multiple solutions, and this is one of them, find it.", - exercise: "gecko", - paths: []string{ - filepath.Join(ws.Dir, "gecko"), - filepath.Join(ws.Dir, "gecko-2"), - filepath.Join(ws.Dir, "gecko-3"), - }, - expected: filepath.Join(ws.Dir, "gecko-2"), - }, - { - desc: "If we already have a solution, but this is a new one, add a suffix.", - exercise: "bat", - paths: []string{ - filepath.Join(ws.Dir, "bat"), - }, - expected: filepath.Join(ws.Dir, "bat-2"), - }, - { - desc: "If we already have multiple solutions, but this is a new one, add a new suffix.", - exercise: "dog", - paths: []string{ - filepath.Join(ws.Dir, "dog"), - filepath.Join(ws.Dir, "dog-2"), - filepath.Join(ws.Dir, "dog-3"), - }, - expected: filepath.Join(ws.Dir, "dog-4"), - }, - { - desc: "Use the first available suffix.", - exercise: "rabbit", - paths: []string{ - filepath.Join(ws.Dir, "rabbit"), - filepath.Join(ws.Dir, "rabbit-2"), - filepath.Join(ws.Dir, "rabbit-4"), - }, - expected: filepath.Join(ws.Dir, "rabbit-3"), - }, - } - - for _, test := range tests { - path, err := ws.ResolveSolutionPath(test.paths, test.exercise, "xxx", existsFn) - assert.NoError(t, err, test.desc) - assert.Equal(t, test.expected, path, test.desc) - } -} - func TestSolutionDir(t *testing.T) { _, cwd, _, _ := runtime.Caller(0) root := filepath.Join(cwd, "..", "..", "fixtures", "solution-dir") From da2ba6447b0c927c6481db7e58e15cd31da4ebeb Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 12 Aug 2018 06:05:44 -0600 Subject: [PATCH 221/544] Fix linting errors in cmd package (#704) --- cmd/download.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index 031ba877e..4d6d02e59 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -168,14 +168,14 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { } for _, file := range payload.Solution.Files { - unparsedUrl := fmt.Sprintf("%s%s", payload.Solution.FileDownloadBaseURL, file) - parsedUrl, err := netURL.ParseRequestURI(unparsedUrl) + unparsedURL := fmt.Sprintf("%s%s", payload.Solution.FileDownloadBaseURL, file) + parsedURL, err := netURL.ParseRequestURI(unparsedURL) if err != nil { return err } - url := parsedUrl.String() + url := parsedURL.String() req, err := client.NewRequest("GET", url, nil) if err != nil { From 02810c4237a930922ac7a60065a0632d2be24615 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 12 Aug 2018 09:19:51 -0600 Subject: [PATCH 222/544] Omit the leading slash on the relative path of exercise files (#703) * Update download test to omit leading slash on files This shows that the download command correctly handles filenames that don't start with a leading slash. It also shows that it still handles files with a leading slash correctly. * Get rid of leading slash on submit --- cmd/download_test.go | 18 ++++++++++++++---- cmd/submit.go | 2 +- cmd/submit_symlink_test.go | 2 +- cmd/submit_test.go | 14 +++++++------- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index 140c8d010..865e3e3b9 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -170,6 +170,10 @@ func fakeDownloadServer(requestor, teamSlug string) *httptest.Server { fmt.Fprint(w, "this is a special file") }) + mux.HandleFunc("/with-leading-slash.txt", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "this has a slash") + }) + mux.HandleFunc("/file-3.txt", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "") }) @@ -211,6 +215,11 @@ func assertDownloadedCorrectFiles(t *testing.T, targetDir string) { path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "special-char-filename#.txt"), contents: "this is a special file", }, + { + desc: "a file that has a leading slash", + path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "with-leading-slash.txt"), + contents: "this has a slash", + }, } for _, file := range expectedFiles { @@ -246,10 +255,11 @@ const payloadTemplate = ` }, "file_download_base_url": "%s", "files": [ - "/file-1.txt", - "/subdir/file-2.txt", - "/special-char-filename#.txt", - "/file-3.txt" + "file-1.txt", + "subdir/file-2.txt", + "special-char-filename#.txt", + "/with-leading-slash.txt", + "file-3.txt" ], "iteration": { "submitted_at": "2017-08-21t10:11:12.130z" diff --git a/cmd/submit.go b/cmd/submit.go index 507808408..e468fe7d8 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -185,7 +185,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { dirname := fmt.Sprintf("%s%s%s", string(os.PathSeparator), solution.Exercise, string(os.PathSeparator)) pieces := strings.Split(path, dirname) - filename := fmt.Sprintf("%s%s", string(os.PathSeparator), pieces[len(pieces)-1]) + filename := pieces[len(pieces)-1] part, err := writer.CreateFormFile("files[]", filename) if err != nil { diff --git a/cmd/submit_symlink_test.go b/cmd/submit_symlink_test.go index 8b5781d55..ccf387827 100644 --- a/cmd/submit_symlink_test.go +++ b/cmd/submit_symlink_test.go @@ -64,5 +64,5 @@ func TestSubmitFilesInSymlinkedPath(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 1, len(submittedFiles)) - assert.Equal(t, "This is a file.", submittedFiles["/file.txt"]) + assert.Equal(t, "This is a file.", submittedFiles["file.txt"]) } diff --git a/cmd/submit_test.go b/cmd/submit_test.go index aae0f1f49..f4b2e781e 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -179,9 +179,9 @@ func TestSubmitFiles(t *testing.T) { assert.Equal(t, 3, len(submittedFiles)) - assert.Equal(t, "This is file 1.", submittedFiles[string(os.PathSeparator)+"file-1.txt"]) - assert.Equal(t, "This is file 2.", submittedFiles[string(os.PathSeparator)+filepath.Join("subdir", "file-2.txt")]) - assert.Equal(t, "This is the readme.", submittedFiles[string(os.PathSeparator)+"README.md"]) + 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 the readme.", submittedFiles["README.md"]) } func TestSubmitWithEmptyFile(t *testing.T) { @@ -227,7 +227,7 @@ func TestSubmitWithEmptyFile(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 1, len(submittedFiles)) - assert.Equal(t, "This is file 2.", submittedFiles[string(os.PathSeparator)+"file-2.txt"]) + assert.Equal(t, "This is file 2.", submittedFiles["file-2.txt"]) } func TestSubmitFilesForTeamExercise(t *testing.T) { @@ -277,8 +277,8 @@ func TestSubmitFilesForTeamExercise(t *testing.T) { assert.Equal(t, 2, len(submittedFiles)) - assert.Equal(t, "This is file 1.", submittedFiles[string(os.PathSeparator)+"file-1.txt"]) - assert.Equal(t, "This is file 2.", submittedFiles[string(os.PathSeparator)+filepath.Join("subdir", "file-2.txt")]) + 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")]) } func TestSubmitOnlyEmptyFile(t *testing.T) { @@ -420,7 +420,7 @@ func TestSubmitRelativePath(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 1, len(submittedFiles)) - assert.Equal(t, "This is a file.", submittedFiles[string(os.PathSeparator)+"file.txt"]) + assert.Equal(t, "This is a file.", submittedFiles["file.txt"]) } func writeFakeSolution(t *testing.T, dir, trackID, exerciseSlug string) { From 556b3404e9b5805c4aef9e552cd89ddd61a4f7e0 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 11 Aug 2018 10:42:56 -0600 Subject: [PATCH 223/544] Create exercise from path to exercise directory Given the path to an exercise directory on the filesystem, determine the root, the track, and the exercise slug. --- workspace/exercise.go | 9 +++++++++ workspace/exercise_test.go | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/workspace/exercise.go b/workspace/exercise.go index 246cf364f..2c5cf6070 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -13,6 +13,15 @@ type Exercise struct { Slug string } +// 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. // It always has forward slashes, regardless // of the operating system. diff --git a/workspace/exercise_test.go b/workspace/exercise_test.go index 83b794064..c3d54a5d7 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) +} From 49c6c727b9d4be8412ee255744e3ac62d05423e4 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 11 Aug 2018 15:44:30 -0600 Subject: [PATCH 224/544] Add documents to Exercise type --- workspace/document.go | 29 +++++++++++++++++++++++++++++ workspace/document_test.go | 30 ++++++++++++++++++++++++++++++ workspace/exercise.go | 7 ++++--- 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 workspace/document.go create mode 100644 workspace/document_test.go diff --git a/workspace/document.go b/workspace/document.go new file mode 100644 index 000000000..b44c13455 --- /dev/null +++ b/workspace/document.go @@ -0,0 +1,29 @@ +package workspace + +import ( + "os" + "path/filepath" + "strings" +) + +// Document is a file in a directory. +type Document struct { + Root string + Filepath string +} + +// NewDocument creates a document from a filepath. +func NewDocument(root, file string) Document { + return Document{ + Root: root, + Filepath: file, + } +} + +// Path is the normalized path. +// It uses forward slashes regardless of the operating system. +func (doc Document) Path() string { + path := strings.Replace(doc.Filepath, doc.Root, "", 1) + path = strings.TrimLeft(path, string(os.PathSeparator)) + return filepath.ToSlash(path) +} diff --git a/workspace/document_test.go b/workspace/document_test.go new file mode 100644 index 000000000..b6882f3a8 --- /dev/null +++ b/workspace/document_test.go @@ -0,0 +1,30 @@ +package workspace + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNormalizedDocumentPath(t *testing.T) { + root := filepath.Join("the", "root", "path", "the-track", "the-exercise") + 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 { + doc := NewDocument(root, tc.filepath) + assert.Equal(t, tc.path, doc.Path()) + } +} diff --git a/workspace/exercise.go b/workspace/exercise.go index 2c5cf6070..9c3815e66 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -8,9 +8,10 @@ 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. From 0385ab89526cd4e028d6d16177001d71db954a1e Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sat, 11 Aug 2018 16:06:36 -0600 Subject: [PATCH 225/544] Use Exercise type in submit command This normalizes the file sent to the server to use forward slashes. --- cmd/submit.go | 19 ++++++++----------- cmd/submit_test.go | 4 ++-- workspace/exercise.go | 5 +++++ 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index e468fe7d8..fc0c13775 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,10 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { fmt.Fprintf(Err, msg, file) continue } - paths = append(paths, file) + exercise.Documents = append(exercise.Documents, exercise.NewDocument(file)) } - if len(paths) == 0 { + if len(exercise.Documents) == 0 { msg := ` No files found to submit. @@ -176,18 +177,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/cmd/submit_test.go b/cmd/submit_test.go index f4b2e781e..556c3d390 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/exercise.go b/workspace/exercise.go index 9c3815e66..469b97751 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -59,3 +59,8 @@ func (e Exercise) HasMetadata() (bool, error) { } return false, err } + +// NewDocument creates a document relative to the exercise. +func (e Exercise) NewDocument(file string) Document { + return NewDocument(e.Filepath(), file) +} From 5ed77dd5d593265c227c1a232f739aeef6254880 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 14 Aug 2018 17:18:27 -0600 Subject: [PATCH 226/544] Remove unnecessary indirection on Exercise type --- cmd/submit.go | 2 +- workspace/exercise.go | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index fc0c13775..ed54202ec 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -162,7 +162,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { fmt.Fprintf(Err, msg, file) continue } - exercise.Documents = append(exercise.Documents, exercise.NewDocument(file)) + exercise.Documents = append(exercise.Documents, workspace.NewDocument(exercise.Filepath(), file)) } if len(exercise.Documents) == 0 { diff --git a/workspace/exercise.go b/workspace/exercise.go index 469b97751..9c3815e66 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -59,8 +59,3 @@ func (e Exercise) HasMetadata() (bool, error) { } return false, err } - -// NewDocument creates a document relative to the exercise. -func (e Exercise) NewDocument(file string) Document { - return NewDocument(e.Filepath(), file) -} From efe66bdae49b716202fa651458d271c286593f1a Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 14 Aug 2018 17:32:08 -0600 Subject: [PATCH 227/544] Clarify doc comment for NewDocument --- workspace/document.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/workspace/document.go b/workspace/document.go index b44c13455..86ea5df1f 100644 --- a/workspace/document.go +++ b/workspace/document.go @@ -13,10 +13,12 @@ type Document struct { } // NewDocument creates a document from a filepath. -func NewDocument(root, file string) Document { +// The root is typically the root of the exercise, and +// path is the relative path to the file within the root directory. +func NewDocument(root, path string) Document { return Document{ Root: root, - Filepath: file, + Filepath: path, } } From e5a7345e1aff698d74d9ce095050b4788d713a64 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 14 Aug 2018 17:35:42 -0600 Subject: [PATCH 228/544] Replace gnarly logic with standard library call --- cmd/submit.go | 8 ++++++-- workspace/document.go | 33 ++++++++++++++++++--------------- workspace/document_test.go | 19 ++++++++++++++++--- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index ed54202ec..9952b5fdb 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -162,7 +162,11 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { fmt.Fprintf(Err, msg, file) continue } - exercise.Documents = append(exercise.Documents, workspace.NewDocument(exercise.Filepath(), file)) + doc, err := workspace.NewDocument(exercise.Filepath(), file) + if err != nil { + return err + } + exercise.Documents = append(exercise.Documents, doc) } if len(exercise.Documents) == 0 { @@ -178,7 +182,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { writer := multipart.NewWriter(body) for _, doc := range exercise.Documents { - file, err := os.Open(doc.Filepath) + file, err := os.Open(doc.Filepath()) if err != nil { return err } diff --git a/workspace/document.go b/workspace/document.go index 86ea5df1f..4b2915138 100644 --- a/workspace/document.go +++ b/workspace/document.go @@ -1,31 +1,34 @@ package workspace -import ( - "os" - "path/filepath" - "strings" -) +import "path/filepath" // Document is a file in a directory. type Document struct { - Root string - Filepath string + Root string + RelativePath string } -// NewDocument creates a document from a filepath. +// NewDocument creates a document from a relative filepath. // The root is typically the root of the exercise, and // path is the relative path to the file within the root directory. -func NewDocument(root, path string) Document { - return Document{ - Root: root, - Filepath: path, +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 { - path := strings.Replace(doc.Filepath, doc.Root, "", 1) - path = strings.TrimLeft(path, string(os.PathSeparator)) - return filepath.ToSlash(path) + return filepath.ToSlash(doc.RelativePath) } diff --git a/workspace/document_test.go b/workspace/document_test.go index b6882f3a8..4e055df2f 100644 --- a/workspace/document_test.go +++ b/workspace/document_test.go @@ -1,6 +1,8 @@ package workspace import ( + "io/ioutil" + "os" "path/filepath" "testing" @@ -8,7 +10,13 @@ import ( ) func TestNormalizedDocumentPath(t *testing.T) { - root := filepath.Join("the", "root", "path", "the-track", "the-exercise") + 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 @@ -24,7 +32,12 @@ func TestNormalizedDocumentPath(t *testing.T) { } for _, tc := range testCases { - doc := NewDocument(root, tc.filepath) - assert.Equal(t, tc.path, doc.Path()) + 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) } } From 60b6ed7bf8c22107e407983043adfd59c1a7e5ab Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 20 Aug 2018 13:13:44 -0600 Subject: [PATCH 229/544] Fix incorrect doc comment for NewDocument --- workspace/document.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workspace/document.go b/workspace/document.go index 4b2915138..52b1264d8 100644 --- a/workspace/document.go +++ b/workspace/document.go @@ -8,9 +8,9 @@ type Document struct { RelativePath string } -// NewDocument creates a document from a relative filepath. +// NewDocument creates a document from the filepath. // The root is typically the root of the exercise, and -// path is the relative path to the file within the root directory. +// path is the absolute path to the file. func NewDocument(root, path string) (Document, error) { path, err := filepath.Rel(root, path) if err != nil { From a07fb1b1af9c11b711b19e4451ec42f88b23ba73 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 21 Aug 2018 19:57:58 -0600 Subject: [PATCH 230/544] Bump version to v3.0.7 --- CHANGELOG.md | 5 +++++ cmd/version.go | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9726cd86d..1b72e9f8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ## Next Release * **Your contribution here** +## v3.0.7 (2018-08-21) +* [#705](https://github.com/exercism/cli/pull/705) Fix confusion about path and filepath - [@kytrinyx] +* [#650](https://github.com/exercism/cli/pull/650) Fix encoding problem in filenames - [@williandrade] + ## v3.0.6 (2018-07-17) * [#652](https://github.com/exercism/cli/pull/652) Add support for teams feature - [@kytrinyx] * [#683](https://github.com/exercism/cli/pull/683) Fix typo in welcome message - [@glebedel] @@ -421,4 +425,5 @@ All changes by [@msgehard] [@rprouse]: https://github.com/rprouse [@simonjefford]: https://github.com/simonjefford [@srt32]: https://github.com/srt32 +[@williandrade]: https://github.com/williandrade [@zabawaba99]: https://github.com/zabawaba99 diff --git a/cmd/version.go b/cmd/version.go index 3bcb893de..482153790 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.0.6" +const Version = "3.0.7" // checkLatest flag for version command. var checkLatest bool From 20a0ed8b5ee814fb598848bead7dd33f39c13d78 Mon Sep 17 00:00:00 2001 From: nywilken Date: Wed, 22 Aug 2018 07:16:38 -0400 Subject: [PATCH 231/544] download: fix support for uuid flag --- cmd/download.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/download.go b/cmd/download.go index 4d6d02e59..387d37257 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -69,7 +69,7 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { } param := "latest" - if param == "" { + if uuid != "" { param = uuid } url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), param) From 28411025d0856c9bbf0ddb79067276ff1786cc71 Mon Sep 17 00:00:00 2001 From: nywilken Date: Wed, 22 Aug 2018 07:25:03 -0400 Subject: [PATCH 232/544] download: add mutual exclusive flag check for exercise and uuid --- cmd/download.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index 387d37257..dc2091899 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -64,32 +64,32 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { if err != nil { return err } - if uuid == "" && slug == "" { + if uuid != "" && slug != "" || uuid == slug { return errors.New("need an --exercise name or a solution --uuid") } - param := "latest" - if uuid != "" { - param = uuid - } - url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), param) - - client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl")) + track, err := flags.GetString("track") if err != nil { return err } - req, err := client.NewRequest("GET", url, nil) + team, err := flags.GetString("team") if err != nil { return err } - track, err := flags.GetString("track") + param := "latest" + if uuid != "" { + param = uuid + } + url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), param) + + client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl")) if err != nil { return err } - team, err := flags.GetString("team") + req, err := client.NewRequest("GET", url, nil) if err != nil { return err } From 536c25f0fa4a82422ee06ef11d6e058f670d8d4e Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 22 Aug 2018 20:05:31 -0600 Subject: [PATCH 233/544] Bump to v3.0.8 --- CHANGELOG.md | 3 +++ cmd/version.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b72e9f8a..d78781a14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ## Next Release * **Your contribution here** +## v3.0.8 (2018-08-22) +* [#713](https://github.com/exercism/cli/pull/713) Fix broken support for uuid flag on download command - [@nywilken] + ## v3.0.7 (2018-08-21) * [#705](https://github.com/exercism/cli/pull/705) Fix confusion about path and filepath - [@kytrinyx] * [#650](https://github.com/exercism/cli/pull/650) Fix encoding problem in filenames - [@williandrade] diff --git a/cmd/version.go b/cmd/version.go index 482153790..f65b5e5a7 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.0.7" +const Version = "3.0.8" // checkLatest flag for version command. var checkLatest bool From ca424d5d5ed86fc308b2df3627fbc5ea01b6c04b Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 26 Aug 2018 11:59:12 -0600 Subject: [PATCH 234/544] Rename default http client in api package for consistency The cli package uses just HTTPClient for this. --- api/client.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/client.go b/api/client.go index 22e483419..af2cffa2b 100644 --- a/api/client.go +++ b/api/client.go @@ -14,8 +14,8 @@ var ( // It's overridden from the root command so that we can set the version. UserAgent = "github.com/exercism/cli" - // DefaultHTTPClient configures a timeout to use by default. - DefaultHTTPClient = &http.Client{Timeout: 10 * time.Second} + // HTTPClient configures a timeout to use by default. + HTTPClient = &http.Client{Timeout: 10 * time.Second} ) // Client is an http client that is configured for Exercism. @@ -29,7 +29,7 @@ type Client struct { // NewClient returns an Exercism API client. func NewClient(token, baseURL string) (*Client, error) { return &Client{ - Client: DefaultHTTPClient, + Client: HTTPClient, Token: token, APIBaseURL: baseURL, }, nil @@ -38,7 +38,7 @@ func NewClient(token, baseURL string) (*Client, error) { // NewRequest returns an http.Request with information for the Exercism API. func (c *Client) NewRequest(method, url string, body io.Reader) (*http.Request, error) { if c.Client == nil { - c.Client = DefaultHTTPClient + c.Client = HTTPClient } req, err := http.NewRequest(method, url, body) From 12c3d2fb2c30ff0ec129f6a874650867bd8dbf9d Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 26 Aug 2018 12:01:47 -0600 Subject: [PATCH 235/544] Make HTTP timeout configurable --- api/client.go | 6 ++++-- cli/cli.go | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/api/client.go b/api/client.go index af2cffa2b..5fc67d1e3 100644 --- a/api/client.go +++ b/api/client.go @@ -14,8 +14,10 @@ var ( // It's overridden from the root command so that we can set the version. UserAgent = "github.com/exercism/cli" - // HTTPClient configures a timeout to use by default. - HTTPClient = &http.Client{Timeout: 10 * time.Second} + // TimeoutInSeconds is the timeout the default HTTP client will use. + TimeoutInSeconds = 10 + // HTTPClient is the client used to make HTTP calls in the cli package. + HTTPClient = &http.Client{Timeout: time.Duration(TimeoutInSeconds) * time.Second} ) // Client is an http client that is configured for Exercism. diff --git a/cli/cli.go b/cli/cli.go index b20e1ebbe..da6601b1c 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -43,8 +43,10 @@ var ( ) var ( + // TimeoutInSeconds is the timeout the default HTTP client will use. + TimeoutInSeconds = 10 // HTTPClient is the client used to make HTTP calls in the cli package. - HTTPClient = &http.Client{Timeout: 10 * time.Second} + HTTPClient = &http.Client{Timeout: time.Duration(TimeoutInSeconds) * time.Second} // ReleaseURL is the endpoint that provides information about cli releases. ReleaseURL = "https://api.github.com/repos/exercism/cli/releases" ) From 5673f90ab2cc3181e51e98a59721554876b66b95 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 26 Aug 2018 12:02:24 -0600 Subject: [PATCH 236/544] Configure custom HTTP timeout in troubleshoot command This configures the timeout on the cli package rather than making a whole new client. --- cmd/troubleshoot.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go index a205a6783..363e22559 100644 --- a/cmd/troubleshoot.go +++ b/cmd/troubleshoot.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "html/template" - "net/http" "runtime" "strings" "sync" @@ -30,7 +29,7 @@ If you're running into trouble, copy and paste the output from the troubleshoot command into a GitHub issue so we can help figure out what's going on. `, RunE: func(cmd *cobra.Command, args []string) error { - cli.HTTPClient = &http.Client{Timeout: 20 * time.Second} + cli.TimeoutInSeconds = 20 c := cli.New(Version) cfg := config.NewConfig() From 9c591874d5ab8fe7d2c24223c627cec6361e29f6 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 26 Aug 2018 12:14:25 -0600 Subject: [PATCH 237/544] Add persistent flag for timeout override In the root command, configure a persistent timeout flag, and check it in the 'prerun' logic, overriding the default value if it isn't the zerovalue for the flag. --- cmd/root.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/root.go b/cmd/root.go index 1b0d9a139..e2b09a423 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,6 +7,7 @@ import ( "runtime" "github.com/exercism/cli/api" + "github.com/exercism/cli/cli" "github.com/exercism/cli/config" "github.com/exercism/cli/debug" "github.com/spf13/cobra" @@ -39,6 +40,10 @@ Download exercises and submit your solutions.`, if verbose, _ := cmd.Flags().GetBool("verbose"); verbose { debug.Verbose = verbose } + if timeout, _ := cmd.Flags().GetInt("timeout"); timeout > 0 { + cli.TimeoutInSeconds = timeout + api.TimeoutInSeconds = timeout + } }, } @@ -57,4 +62,5 @@ func init() { In = os.Stdin api.UserAgent = fmt.Sprintf("github.com/exercism/cli v%s (%s/%s)", Version, runtime.GOOS, runtime.GOARCH) RootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output") + RootCmd.PersistentFlags().IntP("timeout", "", 0, "override the default HTTP timeout (seconds)") } From 65529d35b61a1e048c31147973220b06a9a1a942 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 26 Aug 2018 13:15:57 -0600 Subject: [PATCH 238/544] Fix windows filepaths that accidentally got submitted to the server Some of the older CLIs did not submit a normalized path for the file, but rather submitted the filepath. This means that we sometimes had forward slashes and sometimes had backslashes. The CLI now always submits the normalized path with forward slashes. This ensures that when we receive files submitted with a buggy client we rewrite the paths correctly. --- cmd/download.go | 4 ++++ cmd/download_test.go | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/cmd/download.go b/cmd/download.go index dc2091899..a7491c4fe 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -199,6 +199,10 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { // TODO: if there's a collision, interactively resolve (show diff, ask if overwrite). // TODO: handle --force flag to overwrite without asking. + + // Rewrite paths submitted with an older, buggy client where the Windows path is being treated as part of the filename. + file = strings.Replace(file, "\\", "/", -1) + relativePath := filepath.FromSlash(file) dir := filepath.Join(solution.Dir, filepath.Dir(relativePath)) os.MkdirAll(dir, os.FileMode(0755)) diff --git a/cmd/download_test.go b/cmd/download_test.go index 865e3e3b9..401eb6d77 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -170,6 +170,13 @@ func fakeDownloadServer(requestor, teamSlug string) *httptest.Server { fmt.Fprint(w, "this is a special file") }) + mux.HandleFunc("/\\with-leading-backslash.txt", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "with backslash in name") + }) + mux.HandleFunc("/\\with\\backslashes\\in\\path.txt", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "with backslash in path") + }) + mux.HandleFunc("/with-leading-slash.txt", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "this has a slash") }) @@ -220,6 +227,16 @@ func assertDownloadedCorrectFiles(t *testing.T, targetDir string) { path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "with-leading-slash.txt"), contents: "this has a slash", }, + { + desc: "a file with a leading backslash", + path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "with-leading-backslash.txt"), + contents: "with backslash in name", + }, + { + desc: "a file with backslashes in path", + path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "with", "backslashes", "in", "path.txt"), + contents: "with backslash in path", + }, } for _, file := range expectedFiles { @@ -259,6 +276,8 @@ const payloadTemplate = ` "subdir/file-2.txt", "special-char-filename#.txt", "/with-leading-slash.txt", + "\\with-leading-backslash.txt", + "\\with\\backslashes\\in\\path.txt", "file-3.txt" ], "iteration": { From bf809ab45d7e7ce223d095afc6d02702760fe928 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 27 Aug 2018 19:55:22 -0600 Subject: [PATCH 239/544] Tweak timeouts based on feedback from code review --- api/client.go | 2 +- cli/cli.go | 2 +- cmd/troubleshoot.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/client.go b/api/client.go index 5fc67d1e3..103df119e 100644 --- a/api/client.go +++ b/api/client.go @@ -15,7 +15,7 @@ var ( UserAgent = "github.com/exercism/cli" // TimeoutInSeconds is the timeout the default HTTP client will use. - TimeoutInSeconds = 10 + TimeoutInSeconds = 60 // HTTPClient is the client used to make HTTP calls in the cli package. HTTPClient = &http.Client{Timeout: time.Duration(TimeoutInSeconds) * time.Second} ) diff --git a/cli/cli.go b/cli/cli.go index da6601b1c..29f6d64a3 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -44,7 +44,7 @@ var ( var ( // TimeoutInSeconds is the timeout the default HTTP client will use. - TimeoutInSeconds = 10 + TimeoutInSeconds = 60 // HTTPClient is the client used to make HTTP calls in the cli package. HTTPClient = &http.Client{Timeout: time.Duration(TimeoutInSeconds) * time.Second} // ReleaseURL is the endpoint that provides information about cli releases. diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go index 363e22559..12f2277e3 100644 --- a/cmd/troubleshoot.go +++ b/cmd/troubleshoot.go @@ -29,7 +29,7 @@ If you're running into trouble, copy and paste the output from the troubleshoot command into a GitHub issue so we can help figure out what's going on. `, RunE: func(cmd *cobra.Command, args []string) error { - cli.TimeoutInSeconds = 20 + cli.TimeoutInSeconds = cli.TimeoutInSeconds * 2 c := cli.New(Version) cfg := config.NewConfig() From 93aaf80e531457bd0230188e8bca06d7154d7fa3 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 27 Aug 2018 09:48:23 +0700 Subject: [PATCH 240/544] Remove hard-coded metadata filename references --- cmd/download_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index 865e3e3b9..b79650ae2 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -142,8 +142,8 @@ func TestDownload(t *testing.T) { targetDir := filepath.Join(tmpDir, tc.expectedDir) assertDownloadedCorrectFiles(t, targetDir) - path := filepath.Join(targetDir, "bogus-track", "bogus-exercise", ".solution.json") - b, err := ioutil.ReadFile(path) + path := filepath.Join(targetDir, "bogus-track", "bogus-exercise") + b, err := ioutil.ReadFile(workspace.NewExerciseFromDir(path).MetadataFilepath()) var s workspace.Solution err = json.Unmarshal(b, &s) assert.NoError(t, err) From 0b763e39afc748c3807affdc3524fd760335b63a Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 27 Aug 2018 13:29:43 +0700 Subject: [PATCH 241/544] WIP move metadata file to hidden subdir Passing --- .../solution.json} | 0 .../solution.json} | 0 .../solution.json} | 0 .../solution.json} | 0 .../solution.json} | 0 .../solution.json} | 0 .../solution.json} | 0 .../solution.json} | 0 .../solution.json} | 0 workspace/exercise.go | 2 +- workspace/exercise_test.go | 4 +-- workspace/solution.go | 35 ++++++++++++------- workspace/workspace.go | 2 +- workspace/workspace_test.go | 3 +- 14 files changed, 29 insertions(+), 17 deletions(-) rename fixtures/is-solution-path/broken/{.solution.json => .exercism/solution.json} (100%) rename fixtures/is-solution-path/yepp/{.solution.json => .exercism/solution.json} (100%) rename fixtures/solution-dir/workspace/exercise/{.solution.json => .exercism/solution.json} (100%) rename fixtures/solution-path/creatures/gazelle-2/{.solution.json => .exercism/solution.json} (100%) rename fixtures/solution-path/creatures/gazelle-3/{.solution.json => .exercism/solution.json} (100%) rename fixtures/solution-path/creatures/gazelle/{.solution.json => .exercism/solution.json} (100%) rename fixtures/solutions/alpha/{.solution.json => .exercism/solution.json} (100%) rename fixtures/solutions/bravo/{.solution.json => .exercism/solution.json} (100%) rename fixtures/solutions/charlie/{.solution.json => .exercism/solution.json} (100%) diff --git a/fixtures/is-solution-path/broken/.solution.json b/fixtures/is-solution-path/broken/.exercism/solution.json similarity index 100% rename from fixtures/is-solution-path/broken/.solution.json rename to fixtures/is-solution-path/broken/.exercism/solution.json diff --git a/fixtures/is-solution-path/yepp/.solution.json b/fixtures/is-solution-path/yepp/.exercism/solution.json similarity index 100% rename from fixtures/is-solution-path/yepp/.solution.json rename to fixtures/is-solution-path/yepp/.exercism/solution.json diff --git a/fixtures/solution-dir/workspace/exercise/.solution.json b/fixtures/solution-dir/workspace/exercise/.exercism/solution.json similarity index 100% rename from fixtures/solution-dir/workspace/exercise/.solution.json rename to fixtures/solution-dir/workspace/exercise/.exercism/solution.json diff --git a/fixtures/solution-path/creatures/gazelle-2/.solution.json b/fixtures/solution-path/creatures/gazelle-2/.exercism/solution.json similarity index 100% rename from fixtures/solution-path/creatures/gazelle-2/.solution.json rename to fixtures/solution-path/creatures/gazelle-2/.exercism/solution.json diff --git a/fixtures/solution-path/creatures/gazelle-3/.solution.json b/fixtures/solution-path/creatures/gazelle-3/.exercism/solution.json similarity index 100% rename from fixtures/solution-path/creatures/gazelle-3/.solution.json rename to fixtures/solution-path/creatures/gazelle-3/.exercism/solution.json diff --git a/fixtures/solution-path/creatures/gazelle/.solution.json b/fixtures/solution-path/creatures/gazelle/.exercism/solution.json similarity index 100% rename from fixtures/solution-path/creatures/gazelle/.solution.json rename to fixtures/solution-path/creatures/gazelle/.exercism/solution.json diff --git a/fixtures/solutions/alpha/.solution.json b/fixtures/solutions/alpha/.exercism/solution.json similarity index 100% rename from fixtures/solutions/alpha/.solution.json rename to fixtures/solutions/alpha/.exercism/solution.json diff --git a/fixtures/solutions/bravo/.solution.json b/fixtures/solutions/bravo/.exercism/solution.json similarity index 100% rename from fixtures/solutions/bravo/.solution.json rename to fixtures/solutions/bravo/.exercism/solution.json diff --git a/fixtures/solutions/charlie/.solution.json b/fixtures/solutions/charlie/.exercism/solution.json similarity index 100% rename from fixtures/solutions/charlie/.solution.json rename to fixtures/solutions/charlie/.exercism/solution.json diff --git a/workspace/exercise.go b/workspace/exercise.go index 9c3815e66..c3fb1aa77 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -37,7 +37,7 @@ func (e Exercise) Filepath() string { // MetadataFilepath is the absolute path to the exercise metadata. func (e Exercise) MetadataFilepath() string { - return filepath.Join(e.Filepath(), solutionFilename) + return filepath.Join(e.Filepath(), ignoreSubdirMetadataFilepath()) } // MetadataDir returns the directory that the exercise metadata lives in. diff --git a/workspace/exercise_test.go b/workspace/exercise_test.go index c3d54a5d7..ae4776424 100644 --- a/workspace/exercise_test.go +++ b/workspace/exercise_test.go @@ -17,9 +17,9 @@ func TestHasMetadata(t *testing.T) { exerciseA := Exercise{Root: ws, Track: "bogus-track", Slug: "apple"} exerciseB := Exercise{Root: ws, Track: "bogus-track", Slug: "banana"} - err = os.MkdirAll(filepath.Join(exerciseA.Filepath()), os.FileMode(0755)) + err = os.MkdirAll(filepath.Join(exerciseA.Filepath(), ignoreSubdir), os.FileMode(0755)) assert.NoError(t, err) - err = os.MkdirAll(filepath.Join(exerciseB.Filepath()), os.FileMode(0755)) + err = os.MkdirAll(filepath.Join(exerciseB.Filepath(), ignoreSubdir), os.FileMode(0755)) assert.NoError(t, err) err = ioutil.WriteFile(exerciseA.MetadataFilepath(), []byte{}, os.FileMode(0600)) diff --git a/workspace/solution.go b/workspace/solution.go index 518f819f3..15797ccf8 100644 --- a/workspace/solution.go +++ b/workspace/solution.go @@ -8,11 +8,10 @@ import ( "path/filepath" "strings" "time" - - "github.com/exercism/cli/visibility" ) -const solutionFilename = ".solution.json" +const solutionFilename = "solution.json" +const ignoreSubdir = ".exercism" // Solution contains metadata about a user's solution. type Solution struct { @@ -30,7 +29,7 @@ type Solution struct { // NewSolution reads solution metadata from a file in the given directory. func NewSolution(dir string) (*Solution, error) { - path := filepath.Join(dir, solutionFilename) + path := filepath.Join(dir, ignoreSubdirMetadataFilepath()) b, err := ioutil.ReadFile(path) if err != nil { return &Solution{}, err @@ -67,17 +66,15 @@ func (s *Solution) Write(dir string) error { if err != nil { return err } - - path := filepath.Join(dir, solutionFilename) - - // Hack because ioutil.WriteFile fails on hidden files - visibility.ShowFile(path) - - if err := ioutil.WriteFile(path, b, os.FileMode(0600)); err != nil { + if err = createIgnoreSubdir(dir); err != nil { + return err + } + exercise := NewExerciseFromDir(dir) + if err = ioutil.WriteFile(exercise.MetadataFilepath(), b, os.FileMode(0600)); err != nil { return err } s.Dir = dir - return visibility.HideFile(path) + return nil } // PathToParent is the relative path from the workspace to the parent dir. @@ -88,3 +85,17 @@ func (s *Solution) PathToParent() string { } return filepath.Join(dir, s.Track) } + +func ignoreSubdirMetadataFilepath() string { + return filepath.Join(ignoreSubdir, solutionFilename) +} + +func createIgnoreSubdir(path string) error { + path = filepath.Join(path, ignoreSubdir) + if _, err := os.Stat(path); os.IsNotExist(err) { + if err := os.Mkdir(path, os.FileMode(0755)); err != nil { + return err + } + } + return nil +} diff --git a/workspace/workspace.go b/workspace/workspace.go index 0e9092e41..37b8bef2f 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -113,7 +113,7 @@ func (ws Workspace) SolutionDir(s string) (string, error) { if _, err := os.Lstat(path); os.IsNotExist(err) { return "", err } - if _, err := os.Lstat(filepath.Join(path, solutionFilename)); err == nil { + if _, err := os.Lstat(filepath.Join(path, ignoreSubdirMetadataFilepath())); err == nil { return path, nil } path = filepath.Dir(path) diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go index b48f3cf35..2d971d73b 100644 --- a/workspace/workspace_test.go +++ b/workspace/workspace_test.go @@ -60,10 +60,11 @@ func TestWorkspaceExercises(t *testing.T) { b2 := filepath.Join(tmpDir, "track-b", "exercise-two") for _, path := range []string{a1, a2, b1, b2} { + path := filepath.Join(path, ignoreSubdir) err := os.MkdirAll(path, os.FileMode(0755)) assert.NoError(t, err) - if path != a2 { + if path != filepath.Join(a2, ignoreSubdir) { err = ioutil.WriteFile(filepath.Join(path, solutionFilename), []byte{}, os.FileMode(0600)) assert.NoError(t, err) } From 0597c943fc7ed2f80f437a174e3525130bc9f27c Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 27 Aug 2018 14:42:08 +0700 Subject: [PATCH 242/544] WIP add legacy metadata migration --- cmd/submit_test.go | 65 ++++++++++++++++++++++++++++++++++++++++++ workspace/exercise.go | 5 ++++ workspace/solution.go | 20 +++++++++++++ workspace/workspace.go | 15 +++++++++- 4 files changed, 104 insertions(+), 1 deletion(-) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 556c3d390..6f2a1d7bd 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -1,6 +1,7 @@ package cmd import ( + "encoding/json" "io/ioutil" "net/http" "net/http/httptest" @@ -184,6 +185,54 @@ func TestSubmitFiles(t *testing.T) { assert.Equal(t, "This is the readme.", submittedFiles["README.md"]) } +func TestLegacySolutionMetadataMigration(t *testing.T) { + oldOut := Out + oldErr := Err + Out = ioutil.Discard + Err = ioutil.Discard + defer func() { + Out = oldOut + Err = oldErr + }() + // The fake endpoint will populate this when it receives the call from the command. + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := ioutil.TempDir("", "legacy-metadata-file") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(dir, os.FileMode(0755)) + writeFakeLegacySolution(t, dir, "bogus-track", "bogus-exercise") + + file := filepath.Join(dir, "file.txt") + err = ioutil.WriteFile(file, []byte("This is a file."), os.FileMode(0755)) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + Dir: tmpDir, + UserViperConfig: v, + } + exercise := workspace.NewExerciseFromDir(dir) + expectedPathAfterMigration := exercise.MetadataFilepath() + _, err = os.Stat(expectedPathAfterMigration) + assert.Error(t, err) + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file}) + assert.NoError(t, err) + assert.Equal(t, "This is a file.", submittedFiles["file.txt"]) + + _, err = os.Stat(expectedPathAfterMigration) + assert.NoError(t, err) +} + func TestSubmitWithEmptyFile(t *testing.T) { oldOut := Out oldErr := Err @@ -434,3 +483,19 @@ func writeFakeSolution(t *testing.T, dir, trackID, exerciseSlug string) { err := solution.Write(dir) assert.NoError(t, err) } + +func writeFakeLegacySolution(t *testing.T, dir, trackID, exerciseSlug string) { + solution := &workspace.Solution{ + ID: "bogus-solution-uuid", + Track: trackID, + Exercise: exerciseSlug, + URL: "http://example.com/bogus-url", + IsRequester: true, + } + b, err := json.Marshal(solution) + assert.NoError(t, err) + + exercise := workspace.NewExerciseFromDir(dir) + err = ioutil.WriteFile(exercise.LegacyMetadataFilepath(), b, os.FileMode(0600)) + assert.NoError(t, err) +} diff --git a/workspace/exercise.go b/workspace/exercise.go index c3fb1aa77..f150a5011 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -40,6 +40,11 @@ func (e Exercise) MetadataFilepath() string { return filepath.Join(e.Filepath(), ignoreSubdirMetadataFilepath()) } +// LegacyMetadataFilepath is the absolute path to the legacy exercise metadata. +func (e Exercise) LegacyMetadataFilepath() string { + return filepath.Join(e.Filepath(), legacySolutionFilename) +} + // MetadataDir returns the directory that the exercise metadata lives in. // For now this is the exercise directory. func (e Exercise) MetadataDir() string { diff --git a/workspace/solution.go b/workspace/solution.go index 15797ccf8..23e0c7174 100644 --- a/workspace/solution.go +++ b/workspace/solution.go @@ -11,6 +11,7 @@ import ( ) const solutionFilename = "solution.json" +const legacySolutionFilename = ".solution.json" const ignoreSubdir = ".exercism" // Solution contains metadata about a user's solution. @@ -99,3 +100,22 @@ func createIgnoreSubdir(path string) error { } return nil } + +func migrateLegacySolutionFile(legacyMetadataPath string, metadataPath string) error { + if _, err := os.Lstat(legacyMetadataPath); err != nil { + return err + } + if err := createIgnoreSubdir(filepath.Dir(legacyMetadataPath)); err != nil { + return err + } + if _, err := os.Lstat(metadataPath); err != nil { + if err := os.Rename(legacyMetadataPath, metadataPath); err != nil { + return err + } + fmt.Fprintf(os.Stderr, "\nMigrated solution metadata to %s\n", metadataPath) + } else { + // TODO: decide how to handle case where both legacy and modern metadata files exist + fmt.Fprintf(os.Stderr, "\nAttempted to migrate solution metadata to %s but file already exists\n", metadataPath) + } + return nil +} diff --git a/workspace/workspace.go b/workspace/workspace.go index 37b8bef2f..48a03816a 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -113,9 +113,22 @@ func (ws Workspace) SolutionDir(s string) (string, error) { if _, err := os.Lstat(path); os.IsNotExist(err) { return "", err } - if _, err := os.Lstat(filepath.Join(path, ignoreSubdirMetadataFilepath())); err == nil { + if err := checkMetadataFile(path); err == nil { return path, nil } path = filepath.Dir(path) } } + +func checkMetadataFile(path string) error { + metadataPath := filepath.Join(path, ignoreSubdirMetadataFilepath()) + legacyMetadataPath := filepath.Join(path, legacySolutionFilename) + + var err error + if _, err = os.Lstat(metadataPath); err == nil { + return nil + } else if _, err2 := os.Lstat(legacyMetadataPath); err2 == nil { + return migrateLegacySolutionFile(legacyMetadataPath, metadataPath) + } + return err +} From fda69bfb5f580a9fa11eec9f81c6568c4482ca47 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 27 Aug 2018 19:39:49 +0700 Subject: [PATCH 243/544] Remove visibility package --- visibility/hide_file.go | 13 ----------- visibility/hide_file_windows.go | 41 --------------------------------- 2 files changed, 54 deletions(-) delete mode 100644 visibility/hide_file.go delete mode 100644 visibility/hide_file_windows.go diff --git a/visibility/hide_file.go b/visibility/hide_file.go deleted file mode 100644 index 7fc015772..000000000 --- a/visibility/hide_file.go +++ /dev/null @@ -1,13 +0,0 @@ -// +build !windows - -package visibility - -// HideFile is a no-op for non-Windows systems. -func HideFile(string) error { - return nil -} - -// ShowFile is a no-op for non-Windows systems. -func ShowFile(path string) error { - return nil -} diff --git a/visibility/hide_file_windows.go b/visibility/hide_file_windows.go deleted file mode 100644 index c565e5703..000000000 --- a/visibility/hide_file_windows.go +++ /dev/null @@ -1,41 +0,0 @@ -package visibility - -import "syscall" - -// HideFile sets a Windows file's 'hidden' attribute. -// This is the equivalent of giving a filename on -// Linux or MacOS a leading dot (e.g. .bash_rc). -func HideFile(path string) error { - return setVisibility(path, false) -} - -// ShowFile unsets a Windows file's 'hidden' attribute. -func ShowFile(path string) error { - return setVisibility(path, true) -} - -func setVisibility(path string, visible bool) error { - // This is based on the discussion in - // https://www.reddit.com/r/golang/comments/5t3ezd/hidden_files_directories/ - // but instead of duplicating all the effort to write the file, this takes - // the path of a written file and then flips the bit on the relevant attribute. - // The attributes are a bitmask (uint32), so we can't call - // SetFileAttributes(ptr, syscall.File_ATTRIBUTE_HIDDEN) as suggested, since - // that would wipe out any existing attributes. - ptr, err := syscall.UTF16PtrFromString(path) - if err != nil { - return err - } - attributes, err := syscall.GetFileAttributes(ptr) - if err != nil { - return err - } - - if visible { - attributes &^= syscall.FILE_ATTRIBUTE_HIDDEN - } else { - attributes |= syscall.FILE_ATTRIBUTE_HIDDEN - } - - return syscall.SetFileAttributes(ptr, attributes) -} From 2cf12a2b03a921fc8350199c820063f350f4ce1d Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 28 Aug 2018 09:16:44 +0700 Subject: [PATCH 244/544] Refactor tests using encapsulated metadata path --- workspace/exercise_test.go | 4 ++-- workspace/workspace_test.go | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/workspace/exercise_test.go b/workspace/exercise_test.go index ae4776424..4bd2abb1b 100644 --- a/workspace/exercise_test.go +++ b/workspace/exercise_test.go @@ -17,9 +17,9 @@ func TestHasMetadata(t *testing.T) { exerciseA := Exercise{Root: ws, Track: "bogus-track", Slug: "apple"} exerciseB := Exercise{Root: ws, Track: "bogus-track", Slug: "banana"} - err = os.MkdirAll(filepath.Join(exerciseA.Filepath(), ignoreSubdir), os.FileMode(0755)) + err = os.MkdirAll(filepath.Dir(exerciseA.MetadataFilepath()), os.FileMode(0755)) assert.NoError(t, err) - err = os.MkdirAll(filepath.Join(exerciseB.Filepath(), ignoreSubdir), os.FileMode(0755)) + err = os.MkdirAll(filepath.Dir(exerciseB.MetadataFilepath()), os.FileMode(0755)) assert.NoError(t, err) err = ioutil.WriteFile(exerciseA.MetadataFilepath(), []byte{}, os.FileMode(0600)) diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go index 2d971d73b..8231ba94f 100644 --- a/workspace/workspace_test.go +++ b/workspace/workspace_test.go @@ -6,6 +6,7 @@ import ( "path/filepath" "runtime" "sort" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -60,11 +61,11 @@ func TestWorkspaceExercises(t *testing.T) { b2 := filepath.Join(tmpDir, "track-b", "exercise-two") for _, path := range []string{a1, a2, b1, b2} { - path := filepath.Join(path, ignoreSubdir) + path := filepath.Dir(NewExerciseFromDir(path).MetadataFilepath()) err := os.MkdirAll(path, os.FileMode(0755)) assert.NoError(t, err) - if path != filepath.Join(a2, ignoreSubdir) { + if !strings.HasPrefix(path, a2) { err = ioutil.WriteFile(filepath.Join(path, solutionFilename), []byte{}, os.FileMode(0600)) assert.NoError(t, err) } From 66391f47d825e233dde70c822ea31bb3b0a6d218 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 28 Aug 2018 09:32:16 +0700 Subject: [PATCH 245/544] Refactor TestLegacySolutionMetadataMigration Inline creating fake legacy metadata and assert legacy location DNE after migration --- cmd/submit_test.go | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 6f2a1d7bd..c26168248 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -205,7 +205,20 @@ func TestLegacySolutionMetadataMigration(t *testing.T) { dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") os.MkdirAll(dir, os.FileMode(0755)) - writeFakeLegacySolution(t, dir, "bogus-track", "bogus-exercise") + + // Write fake legacy solution + solution := &workspace.Solution{ + ID: "bogus-solution-uuid", + Track: "bogus-track", + Exercise: "bogus-exercise", + URL: "http://example.com/bogus-url", + IsRequester: true, + } + b, err := json.Marshal(solution) + assert.NoError(t, err) + exercise := workspace.NewExerciseFromDir(dir) + err = ioutil.WriteFile(exercise.LegacyMetadataFilepath(), b, os.FileMode(0600)) + assert.NoError(t, err) file := filepath.Join(dir, "file.txt") err = ioutil.WriteFile(file, []byte("This is a file."), os.FileMode(0755)) @@ -220,7 +233,6 @@ func TestLegacySolutionMetadataMigration(t *testing.T) { Dir: tmpDir, UserViperConfig: v, } - exercise := workspace.NewExerciseFromDir(dir) expectedPathAfterMigration := exercise.MetadataFilepath() _, err = os.Stat(expectedPathAfterMigration) assert.Error(t, err) @@ -231,6 +243,8 @@ func TestLegacySolutionMetadataMigration(t *testing.T) { _, err = os.Stat(expectedPathAfterMigration) assert.NoError(t, err) + _, err = os.Stat(exercise.LegacyMetadataFilepath()) + assert.Error(t, err) } func TestSubmitWithEmptyFile(t *testing.T) { @@ -483,19 +497,3 @@ func writeFakeSolution(t *testing.T, dir, trackID, exerciseSlug string) { err := solution.Write(dir) assert.NoError(t, err) } - -func writeFakeLegacySolution(t *testing.T, dir, trackID, exerciseSlug string) { - solution := &workspace.Solution{ - ID: "bogus-solution-uuid", - Track: trackID, - Exercise: exerciseSlug, - URL: "http://example.com/bogus-url", - IsRequester: true, - } - b, err := json.Marshal(solution) - assert.NoError(t, err) - - exercise := workspace.NewExerciseFromDir(dir) - err = ioutil.WriteFile(exercise.LegacyMetadataFilepath(), b, os.FileMode(0600)) - assert.NoError(t, err) -} From e0e865efab5133e3b9b085aeb2cf44688f51db9c Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 28 Aug 2018 10:58:57 +0700 Subject: [PATCH 246/544] WIP refactor MigrateLegacyMetadataFile() Passing TODO: * if legacy & modern metadata exist, handle deletion * unit tests for migration --- cmd/submit.go | 4 +++- workspace/exercise.go | 26 ++++++++++++++++++++++++++ workspace/exercise_test.go | 16 ++++++++++++++++ workspace/solution.go | 19 ------------------- workspace/workspace.go | 18 ++++-------------- 5 files changed, 49 insertions(+), 34 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 9952b5fdb..6e69b5ff0 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -125,7 +125,9 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { } exercise := workspace.NewExerciseFromDir(exerciseDir) - + if err := exercise.MigrateLegacyMetadataFile(); err != nil { + return err + } solution, err := workspace.NewSolution(exerciseDir) if err != nil { return err diff --git a/workspace/exercise.go b/workspace/exercise.go index f150a5011..e407b0a99 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -1,6 +1,7 @@ package workspace import ( + "fmt" "os" "path" "path/filepath" @@ -64,3 +65,28 @@ func (e Exercise) HasMetadata() (bool, error) { } return false, err } + +// MigrateLegacyMetadataFile migrates a legacy metadata to the modern location. +// This is a noop if the metadata file isn't legacy. +// If both legacy and modern metadata files exist, the legacy file will be deleted. +func (e Exercise) MigrateLegacyMetadataFile() error { + legacyMetadataFilepath := e.LegacyMetadataFilepath() + metadataFilepath := e.MetadataFilepath() + + if _, err := os.Lstat(legacyMetadataFilepath); err != nil { + return nil + } + if err := createIgnoreSubdir(filepath.Dir(legacyMetadataFilepath)); err != nil { + return err + } + if _, err := os.Lstat(metadataFilepath); err != nil { + if err := os.Rename(legacyMetadataFilepath, metadataFilepath); err != nil { + return err + } + fmt.Fprintf(os.Stderr, "\nMigrated solution metadata to %s\n", metadataFilepath) + } else { + // TODO: decide how to handle case where both legacy and modern metadata files exist + fmt.Fprintf(os.Stderr, "\nAttempted to migrate solution metadata to %s but file already exists\n", metadataFilepath) + } + return nil +} diff --git a/workspace/exercise_test.go b/workspace/exercise_test.go index 4bd2abb1b..bc6b78c3e 100644 --- a/workspace/exercise_test.go +++ b/workspace/exercise_test.go @@ -42,3 +42,19 @@ func TestNewFromDir(t *testing.T) { assert.Equal(t, "the-track", exercise.Track) assert.Equal(t, "the-exercise", exercise.Slug) } + +func TestMigrateLegacyMetadataFile(t *testing.T) { + ws, err := ioutil.TempDir("", "fake-workspace") + defer os.RemoveAll(ws) + assert.NoError(t, err) + + exerciseLegacy := Exercise{Root: ws, Track: "bogus-track", Slug: "legacy"} + exerciseModern := Exercise{Root: ws, Track: "bogus-track", Slug: "modern"} + + err = os.MkdirAll(filepath.Dir(exerciseLegacy.LegacyMetadataFilepath()), os.FileMode(0755)) + assert.NoError(t, err) + err = os.MkdirAll(filepath.Dir(exerciseModern.MetadataFilepath()), os.FileMode(0755)) + assert.NoError(t, err) + + // TODO +} diff --git a/workspace/solution.go b/workspace/solution.go index 23e0c7174..bcdc17fb4 100644 --- a/workspace/solution.go +++ b/workspace/solution.go @@ -100,22 +100,3 @@ func createIgnoreSubdir(path string) error { } return nil } - -func migrateLegacySolutionFile(legacyMetadataPath string, metadataPath string) error { - if _, err := os.Lstat(legacyMetadataPath); err != nil { - return err - } - if err := createIgnoreSubdir(filepath.Dir(legacyMetadataPath)); err != nil { - return err - } - if _, err := os.Lstat(metadataPath); err != nil { - if err := os.Rename(legacyMetadataPath, metadataPath); err != nil { - return err - } - fmt.Fprintf(os.Stderr, "\nMigrated solution metadata to %s\n", metadataPath) - } else { - // TODO: decide how to handle case where both legacy and modern metadata files exist - fmt.Fprintf(os.Stderr, "\nAttempted to migrate solution metadata to %s but file already exists\n", metadataPath) - } - return nil -} diff --git a/workspace/workspace.go b/workspace/workspace.go index 48a03816a..b031a8631 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -113,22 +113,12 @@ func (ws Workspace) SolutionDir(s string) (string, error) { if _, err := os.Lstat(path); os.IsNotExist(err) { return "", err } - if err := checkMetadataFile(path); err == nil { + if _, err := os.Lstat(filepath.Join(path, ignoreSubdirMetadataFilepath())); err == nil { + return path, nil + } + if _, err := os.Lstat(filepath.Join(path, legacySolutionFilename)); err == nil { return path, nil } path = filepath.Dir(path) } } - -func checkMetadataFile(path string) error { - metadataPath := filepath.Join(path, ignoreSubdirMetadataFilepath()) - legacyMetadataPath := filepath.Join(path, legacySolutionFilename) - - var err error - if _, err = os.Lstat(metadataPath); err == nil { - return nil - } else if _, err2 := os.Lstat(legacyMetadataPath); err2 == nil { - return migrateLegacySolutionFile(legacyMetadataPath, metadataPath) - } - return err -} From 9752d628c38fb0c37c75d4be220ade1cc72e462d Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 28 Aug 2018 11:11:36 +0700 Subject: [PATCH 247/544] Refactor relative path metadata func to var --- workspace/exercise.go | 10 ++++++---- workspace/solution.go | 8 +++----- workspace/workspace.go | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/workspace/exercise.go b/workspace/exercise.go index e407b0a99..f40ada049 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -38,7 +38,7 @@ func (e Exercise) Filepath() string { // MetadataFilepath is the absolute path to the exercise metadata. func (e Exercise) MetadataFilepath() string { - return filepath.Join(e.Filepath(), ignoreSubdirMetadataFilepath()) + return filepath.Join(e.Filepath(), metadataFilepath) } // LegacyMetadataFilepath is the absolute path to the legacy exercise metadata. @@ -83,10 +83,12 @@ func (e Exercise) MigrateLegacyMetadataFile() error { if err := os.Rename(legacyMetadataFilepath, metadataFilepath); err != nil { return err } - fmt.Fprintf(os.Stderr, "\nMigrated solution metadata to %s\n", metadataFilepath) + fmt.Fprintf(os.Stderr, "\nMigrated metadata to %s\n", metadataFilepath) } else { - // TODO: decide how to handle case where both legacy and modern metadata files exist - fmt.Fprintf(os.Stderr, "\nAttempted to migrate solution metadata to %s but file already exists\n", metadataFilepath) + if err := os.Remove(legacyMetadataFilepath); err != nil { + return err + } + fmt.Fprintf(os.Stderr, "\nRemoved legacy metadata: %s\n", legacyMetadataFilepath) } return nil } diff --git a/workspace/solution.go b/workspace/solution.go index bcdc17fb4..c5b62e911 100644 --- a/workspace/solution.go +++ b/workspace/solution.go @@ -14,6 +14,8 @@ const solutionFilename = "solution.json" const legacySolutionFilename = ".solution.json" const ignoreSubdir = ".exercism" +var metadataFilepath = filepath.Join(ignoreSubdir, solutionFilename) + // Solution contains metadata about a user's solution. type Solution struct { Track string `json:"track"` @@ -30,7 +32,7 @@ type Solution struct { // NewSolution reads solution metadata from a file in the given directory. func NewSolution(dir string) (*Solution, error) { - path := filepath.Join(dir, ignoreSubdirMetadataFilepath()) + path := filepath.Join(dir, metadataFilepath) b, err := ioutil.ReadFile(path) if err != nil { return &Solution{}, err @@ -87,10 +89,6 @@ func (s *Solution) PathToParent() string { return filepath.Join(dir, s.Track) } -func ignoreSubdirMetadataFilepath() string { - return filepath.Join(ignoreSubdir, solutionFilename) -} - func createIgnoreSubdir(path string) error { path = filepath.Join(path, ignoreSubdir) if _, err := os.Stat(path); os.IsNotExist(err) { diff --git a/workspace/workspace.go b/workspace/workspace.go index b031a8631..30b624a83 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -113,7 +113,7 @@ func (ws Workspace) SolutionDir(s string) (string, error) { if _, err := os.Lstat(path); os.IsNotExist(err) { return "", err } - if _, err := os.Lstat(filepath.Join(path, ignoreSubdirMetadataFilepath())); err == nil { + if _, err := os.Lstat(filepath.Join(path, metadataFilepath)); err == nil { return path, nil } if _, err := os.Lstat(filepath.Join(path, legacySolutionFilename)); err == nil { From 05545c5d48680cc0aaeb18171bc2c1a139b58f43 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 28 Aug 2018 19:12:36 +0700 Subject: [PATCH 248/544] WIP add unit tests for MigrateLegacyMetadataFile --- cmd/submit.go | 2 +- workspace/exercise.go | 32 ++++++++++++++++------ workspace/exercise_test.go | 55 ++++++++++++++++++++++++++++++++++---- 3 files changed, 75 insertions(+), 14 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 6e69b5ff0..db6d74951 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -125,7 +125,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { } exercise := workspace.NewExerciseFromDir(exerciseDir) - if err := exercise.MigrateLegacyMetadataFile(); err != nil { + if _, err := exercise.MigrateLegacyMetadataFile(); err != nil { return err } solution, err := workspace.NewSolution(exerciseDir) diff --git a/workspace/exercise.go b/workspace/exercise.go index f40ada049..b632a0fdc 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -66,29 +66,45 @@ func (e Exercise) HasMetadata() (bool, error) { return false, err } +// HasLegacyMetadata checks for the presence of a legacy exercise metadata file. +// If there is no such file, it could also be an unrelated directory. +func (e Exercise) HasLegacyMetadata() (bool, error) { + _, err := os.Lstat(e.LegacyMetadataFilepath()) + if os.IsNotExist(err) { + return false, nil + } + if err == nil { + return true, nil + } + return false, err +} + // MigrateLegacyMetadataFile migrates a legacy metadata to the modern location. // This is a noop if the metadata file isn't legacy. // If both legacy and modern metadata files exist, the legacy file will be deleted. -func (e Exercise) MigrateLegacyMetadataFile() error { +func (e Exercise) MigrateLegacyMetadataFile() (string, error) { + var str string legacyMetadataFilepath := e.LegacyMetadataFilepath() metadataFilepath := e.MetadataFilepath() if _, err := os.Lstat(legacyMetadataFilepath); err != nil { - return nil + return "", nil } if err := createIgnoreSubdir(filepath.Dir(legacyMetadataFilepath)); err != nil { - return err + return "", err } if _, err := os.Lstat(metadataFilepath); err != nil { if err := os.Rename(legacyMetadataFilepath, metadataFilepath); err != nil { - return err + return "", err } - fmt.Fprintf(os.Stderr, "\nMigrated metadata to %s\n", metadataFilepath) + str = fmt.Sprintf("\nMigrated metadata to %s\n", metadataFilepath) + fmt.Fprintf(os.Stderr, str) } else { if err := os.Remove(legacyMetadataFilepath); err != nil { - return err + return "", err } - fmt.Fprintf(os.Stderr, "\nRemoved legacy metadata: %s\n", legacyMetadataFilepath) + str = fmt.Sprintf("\nRemoved legacy metadata: %s\n", legacyMetadataFilepath) + fmt.Fprintf(os.Stderr, str) } - return nil + return str, nil } diff --git a/workspace/exercise_test.go b/workspace/exercise_test.go index bc6b78c3e..a38a2d13a 100644 --- a/workspace/exercise_test.go +++ b/workspace/exercise_test.go @@ -1,6 +1,7 @@ package workspace import ( + "fmt" "io/ioutil" "os" "path/filepath" @@ -44,17 +45,61 @@ func TestNewFromDir(t *testing.T) { } func TestMigrateLegacyMetadataFile(t *testing.T) { + var str string ws, err := ioutil.TempDir("", "fake-workspace") defer os.RemoveAll(ws) assert.NoError(t, err) - exerciseLegacy := Exercise{Root: ws, Track: "bogus-track", Slug: "legacy"} - exerciseModern := Exercise{Root: ws, Track: "bogus-track", Slug: "modern"} + exercise := Exercise{Root: ws, Track: "bogus-track", Slug: "migration"} + metadataFilepath := exercise.MetadataFilepath() + legacyMetadataFilepath := exercise.LegacyMetadataFilepath() - err = os.MkdirAll(filepath.Dir(exerciseLegacy.LegacyMetadataFilepath()), os.FileMode(0755)) + err = os.MkdirAll(filepath.Dir(legacyMetadataFilepath), os.FileMode(0755)) assert.NoError(t, err) - err = os.MkdirAll(filepath.Dir(exerciseModern.MetadataFilepath()), os.FileMode(0755)) + err = os.MkdirAll(filepath.Dir(metadataFilepath), os.FileMode(0755)) assert.NoError(t, err) - // TODO + // returns nil if not legacy + err = ioutil.WriteFile(metadataFilepath, []byte{}, os.FileMode(0600)) + assert.NoError(t, err) + ok, _ := exercise.HasMetadata() + assert.True(t, ok) + _, err = exercise.MigrateLegacyMetadataFile() + assert.Nil(t, err) + ok, _ = exercise.HasMetadata() + assert.True(t, ok) + + // legacy metadata only => gets renamed + os.Remove(metadataFilepath) + err = ioutil.WriteFile(legacyMetadataFilepath, []byte{}, os.FileMode(0600)) + assert.NoError(t, err) + ok, _ = exercise.HasLegacyMetadata() + assert.True(t, ok) + ok, _ = exercise.HasMetadata() + assert.False(t, ok) + str, err = exercise.MigrateLegacyMetadataFile() + assert.Equal(t, fmt.Sprintf("\nMigrated metadata to %s\n", metadataFilepath), str) + assert.NoError(t, err) + ok, _ = exercise.HasLegacyMetadata() + assert.False(t, ok) + ok, _ = exercise.HasMetadata() + assert.True(t, ok) + + // both legacy and modern metadata files exist => legacy gets deleted + err = ioutil.WriteFile(legacyMetadataFilepath, []byte{}, os.FileMode(0600)) + assert.NoError(t, err) + err = ioutil.WriteFile(metadataFilepath, []byte{}, os.FileMode(0600)) + assert.NoError(t, err) + ok, _ = exercise.HasLegacyMetadata() + assert.True(t, ok) + ok, _ = exercise.HasMetadata() + assert.True(t, ok) + str, err = exercise.MigrateLegacyMetadataFile() + assert.Equal(t, fmt.Sprintf("\nRemoved legacy metadata: %s\n", legacyMetadataFilepath), str) + assert.NoError(t, err) + ok, _ = exercise.HasLegacyMetadata() + assert.False(t, ok) + ok, _ = exercise.HasMetadata() + assert.True(t, ok) + } From 8849c1998b76b45e3ec0d92392a83a075c1a64c6 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 28 Aug 2018 19:27:58 +0700 Subject: [PATCH 249/544] Refactor MigrateLegacyMetadataFile to separate tests --- workspace/exercise_test.go | 61 ++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/workspace/exercise_test.go b/workspace/exercise_test.go index a38a2d13a..bc87f7e24 100644 --- a/workspace/exercise_test.go +++ b/workspace/exercise_test.go @@ -44,62 +44,85 @@ func TestNewFromDir(t *testing.T) { assert.Equal(t, "the-exercise", exercise.Slug) } -func TestMigrateLegacyMetadataFile(t *testing.T) { - var str string +func TestMigrateLegacyMetadataFileWithoutLegacy(t *testing.T) { ws, err := ioutil.TempDir("", "fake-workspace") defer os.RemoveAll(ws) assert.NoError(t, err) - exercise := Exercise{Root: ws, Track: "bogus-track", Slug: "migration"} + exercise := Exercise{Root: ws, Track: "bogus-track", Slug: "no-legacy"} metadataFilepath := exercise.MetadataFilepath() - legacyMetadataFilepath := exercise.LegacyMetadataFilepath() - - err = os.MkdirAll(filepath.Dir(legacyMetadataFilepath), os.FileMode(0755)) - assert.NoError(t, err) err = os.MkdirAll(filepath.Dir(metadataFilepath), os.FileMode(0755)) assert.NoError(t, err) - // returns nil if not legacy err = ioutil.WriteFile(metadataFilepath, []byte{}, os.FileMode(0600)) assert.NoError(t, err) ok, _ := exercise.HasMetadata() assert.True(t, ok) - _, err = exercise.MigrateLegacyMetadataFile() + + stderr, err := exercise.MigrateLegacyMetadataFile() + assert.Nil(t, err) + assert.Equal(t, "", stderr) ok, _ = exercise.HasMetadata() assert.True(t, ok) +} + +func TestMigrateLegacyMetadataFileWithLegacy(t *testing.T) { + ws, err := ioutil.TempDir("", "fake-workspace") + defer os.RemoveAll(ws) + assert.NoError(t, err) + + exercise := Exercise{Root: ws, Track: "bogus-track", Slug: "legacy"} + metadataFilepath := exercise.MetadataFilepath() + legacyMetadataFilepath := exercise.LegacyMetadataFilepath() + err = os.MkdirAll(filepath.Dir(legacyMetadataFilepath), os.FileMode(0755)) + assert.NoError(t, err) - // legacy metadata only => gets renamed - os.Remove(metadataFilepath) err = ioutil.WriteFile(legacyMetadataFilepath, []byte{}, os.FileMode(0600)) assert.NoError(t, err) - ok, _ = exercise.HasLegacyMetadata() + ok, _ := exercise.HasLegacyMetadata() assert.True(t, ok) ok, _ = exercise.HasMetadata() assert.False(t, ok) - str, err = exercise.MigrateLegacyMetadataFile() - assert.Equal(t, fmt.Sprintf("\nMigrated metadata to %s\n", metadataFilepath), str) + + stderr, err := exercise.MigrateLegacyMetadataFile() + + assert.Equal(t, fmt.Sprintf("\nMigrated metadata to %s\n", metadataFilepath), stderr) assert.NoError(t, err) ok, _ = exercise.HasLegacyMetadata() assert.False(t, ok) ok, _ = exercise.HasMetadata() assert.True(t, ok) +} + +func TestMigrateLegacyMetadataFileWithLegacyAndModern(t *testing.T) { + ws, err := ioutil.TempDir("", "fake-workspace") + defer os.RemoveAll(ws) + assert.NoError(t, err) + + exercise := Exercise{Root: ws, Track: "bogus-track", Slug: "both-legacy-and-modern"} + metadataFilepath := exercise.MetadataFilepath() + legacyMetadataFilepath := exercise.LegacyMetadataFilepath() + err = os.MkdirAll(filepath.Dir(legacyMetadataFilepath), os.FileMode(0755)) + assert.NoError(t, err) + err = os.MkdirAll(filepath.Dir(metadataFilepath), os.FileMode(0755)) + assert.NoError(t, err) - // both legacy and modern metadata files exist => legacy gets deleted err = ioutil.WriteFile(legacyMetadataFilepath, []byte{}, os.FileMode(0600)) assert.NoError(t, err) err = ioutil.WriteFile(metadataFilepath, []byte{}, os.FileMode(0600)) assert.NoError(t, err) - ok, _ = exercise.HasLegacyMetadata() + ok, _ := exercise.HasLegacyMetadata() assert.True(t, ok) ok, _ = exercise.HasMetadata() assert.True(t, ok) - str, err = exercise.MigrateLegacyMetadataFile() - assert.Equal(t, fmt.Sprintf("\nRemoved legacy metadata: %s\n", legacyMetadataFilepath), str) + + stderr, err := exercise.MigrateLegacyMetadataFile() + + assert.Equal(t, fmt.Sprintf("\nRemoved legacy metadata: %s\n", legacyMetadataFilepath), stderr) assert.NoError(t, err) ok, _ = exercise.HasLegacyMetadata() assert.False(t, ok) ok, _ = exercise.HasMetadata() assert.True(t, ok) - } From 49178ce67c69bcf960f05ca71ff32aa5ce115438 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 28 Aug 2018 19:32:28 +0700 Subject: [PATCH 250/544] Add test for HasLegacyMetadata --- workspace/exercise_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/workspace/exercise_test.go b/workspace/exercise_test.go index bc87f7e24..14bd784a9 100644 --- a/workspace/exercise_test.go +++ b/workspace/exercise_test.go @@ -35,6 +35,31 @@ func TestHasMetadata(t *testing.T) { assert.False(t, ok) } +func TestHasLegacyMetadata(t *testing.T) { + ws, err := ioutil.TempDir("", "fake-workspace") + defer os.RemoveAll(ws) + assert.NoError(t, err) + + exerciseA := Exercise{Root: ws, Track: "bogus-track", Slug: "apple"} + exerciseB := Exercise{Root: ws, Track: "bogus-track", Slug: "banana"} + + err = os.MkdirAll(filepath.Dir(exerciseA.LegacyMetadataFilepath()), os.FileMode(0755)) + assert.NoError(t, err) + err = os.MkdirAll(filepath.Dir(exerciseB.LegacyMetadataFilepath()), os.FileMode(0755)) + assert.NoError(t, err) + + err = ioutil.WriteFile(exerciseA.LegacyMetadataFilepath(), []byte{}, os.FileMode(0600)) + assert.NoError(t, err) + + ok, err := exerciseA.HasLegacyMetadata() + assert.NoError(t, err) + assert.True(t, ok) + + ok, err = exerciseB.HasLegacyMetadata() + assert.NoError(t, err) + assert.False(t, ok) +} + func TestNewFromDir(t *testing.T) { dir := filepath.Join("something", "another", "whatever", "the-track", "the-exercise") From 9b0f559a37994d3c16e98ae8bc1e0a8748cb0032 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 26 Aug 2018 13:48:01 -0600 Subject: [PATCH 251/544] Delete unused constant in workspace package --- workspace/workspace.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/workspace/workspace.go b/workspace/workspace.go index 0e9092e41..843953013 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -5,7 +5,6 @@ import ( "io/ioutil" "os" "path/filepath" - "regexp" "strings" ) @@ -16,8 +15,6 @@ func IsMissingMetadata(err error) bool { return err == errMissingMetadata } -var rgxSerialSuffix = regexp.MustCompile(`-\d*$`) - // Workspace represents a user's Exercism workspace. // It may contain a user's own exercises, and other people's // exercises that they've downloaded to look at or run locally. From 3ed08d2c67cfe2aafbb191aa3ac08a1cf4e1ec48 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 26 Aug 2018 13:51:01 -0600 Subject: [PATCH 252/544] Handle exercise directories with numeric suffixes An early design decision was to allow people to work on multiple solutions to the same exercise at the same time. We later decided not to do this, but the CLI still had logic that supported it. This caused us to (incorrectly) create exercise directories that were named with a numeric suffix, which would then get submitted to the backend server, which doesn't care about filepaths. Because the exercise with the numeric suffix doesn't match the expected path, we did not correctly trim off any leading directories on submit, which further caused the download command to put the solution in a weirdly and deeply nested directory, making the solution hard to find and review. --- cmd/download.go | 10 ++++++++++ cmd/download_test.go | 12 +++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/cmd/download.go b/cmd/download.go index a7491c4fe..c0139e80e 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -9,6 +9,7 @@ import ( netURL "net/url" "os" "path/filepath" + "regexp" "strings" "github.com/exercism/cli/api" @@ -200,6 +201,15 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { // TODO: if there's a collision, interactively resolve (show diff, ask if overwrite). // TODO: handle --force flag to overwrite without asking. + // Work around a path bug due to an early design decision (later reversed) to + // allow numeric suffixes for exercise directories, allowing people to have + // multiple parallel versions of an exercise. + pattern := fmt.Sprintf(`\A.*[/\\]%s-\d*/`, solution.Exercise) + rgxNumericSuffix := regexp.MustCompile(pattern) + if rgxNumericSuffix.MatchString(file) { + file = string(rgxNumericSuffix.ReplaceAll([]byte(file), []byte(""))) + } + // Rewrite paths submitted with an older, buggy client where the Windows path is being treated as part of the filename. file = strings.Replace(file, "\\", "/", -1) diff --git a/cmd/download_test.go b/cmd/download_test.go index 401eb6d77..7beb26ae1 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -166,6 +166,10 @@ func fakeDownloadServer(requestor, teamSlug string) *httptest.Server { fmt.Fprint(w, "this is file 2") }) + mux.HandleFunc("/full/path/with/numeric-suffix/bogus-track/bogus-exercise-12345/subdir/numeric.txt", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "with numeric suffix") + }) + mux.HandleFunc("/special-char-filename#.txt", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "this is a special file") }) @@ -217,6 +221,11 @@ func assertDownloadedCorrectFiles(t *testing.T, targetDir string) { path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "subdir", "file-2.txt"), contents: "this is file 2", }, + { + desc: "a path with a numeric suffix", + path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "subdir", "numeric.txt"), + contents: "with numeric suffix", + }, { desc: "a file that requires URL encoding", path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "special-char-filename#.txt"), @@ -278,7 +287,8 @@ const payloadTemplate = ` "/with-leading-slash.txt", "\\with-leading-backslash.txt", "\\with\\backslashes\\in\\path.txt", - "file-3.txt" + "file-3.txt", + "/full/path/with/numeric-suffix/bogus-track/bogus-exercise-12345/subdir/numeric.txt" ], "iteration": { "submitted_at": "2017-08-21t10:11:12.130z" From c8de6100cba1f17ed401e6a29f42a62a98a4bcd0 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Wed, 29 Aug 2018 09:33:56 +0700 Subject: [PATCH 253/544] Refactor inline ignore subdirectory location --- workspace/exercise.go | 3 ++- workspace/solution.go | 12 +----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/workspace/exercise.go b/workspace/exercise.go index b632a0fdc..e1c66a636 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -90,7 +90,8 @@ func (e Exercise) MigrateLegacyMetadataFile() (string, error) { if _, err := os.Lstat(legacyMetadataFilepath); err != nil { return "", nil } - if err := createIgnoreSubdir(filepath.Dir(legacyMetadataFilepath)); err != nil { + dir := filepath.Join(filepath.Dir(legacyMetadataFilepath), ignoreSubdir) + if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { return "", err } if _, err := os.Lstat(metadataFilepath); err != nil { diff --git a/workspace/solution.go b/workspace/solution.go index c5b62e911..dffd1919a 100644 --- a/workspace/solution.go +++ b/workspace/solution.go @@ -69,7 +69,7 @@ func (s *Solution) Write(dir string) error { if err != nil { return err } - if err = createIgnoreSubdir(dir); err != nil { + if err = os.MkdirAll(filepath.Join(dir, ignoreSubdir), os.FileMode(0755)); err != nil { return err } exercise := NewExerciseFromDir(dir) @@ -88,13 +88,3 @@ func (s *Solution) PathToParent() string { } return filepath.Join(dir, s.Track) } - -func createIgnoreSubdir(path string) error { - path = filepath.Join(path, ignoreSubdir) - if _, err := os.Stat(path); os.IsNotExist(err) { - if err := os.Mkdir(path, os.FileMode(0755)); err != nil { - return err - } - } - return nil -} From f244b57efb12167dbce027117279d00253d482ca Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Wed, 29 Aug 2018 09:49:45 +0700 Subject: [PATCH 254/544] Refactor remove printing from migration WIP - will replace with status --- cmd/submit.go | 2 +- workspace/exercise.go | 18 ++++++------------ workspace/exercise_test.go | 26 ++++++++++++++------------ 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index db6d74951..5812cc42e 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -125,7 +125,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { } exercise := workspace.NewExerciseFromDir(exerciseDir) - if _, err := exercise.MigrateLegacyMetadataFile(); err != nil { + if err = exercise.MigrateLegacyMetadataFile(); err != nil { return err } solution, err := workspace.NewSolution(exerciseDir) diff --git a/workspace/exercise.go b/workspace/exercise.go index e1c66a636..cde90754e 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -1,7 +1,6 @@ package workspace import ( - "fmt" "os" "path" "path/filepath" @@ -82,30 +81,25 @@ func (e Exercise) HasLegacyMetadata() (bool, error) { // MigrateLegacyMetadataFile migrates a legacy metadata to the modern location. // This is a noop if the metadata file isn't legacy. // If both legacy and modern metadata files exist, the legacy file will be deleted. -func (e Exercise) MigrateLegacyMetadataFile() (string, error) { - var str string +func (e Exercise) MigrateLegacyMetadataFile() error { legacyMetadataFilepath := e.LegacyMetadataFilepath() metadataFilepath := e.MetadataFilepath() if _, err := os.Lstat(legacyMetadataFilepath); err != nil { - return "", nil + return nil } dir := filepath.Join(filepath.Dir(legacyMetadataFilepath), ignoreSubdir) if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { - return "", err + return err } if _, err := os.Lstat(metadataFilepath); err != nil { if err := os.Rename(legacyMetadataFilepath, metadataFilepath); err != nil { - return "", err + return err } - str = fmt.Sprintf("\nMigrated metadata to %s\n", metadataFilepath) - fmt.Fprintf(os.Stderr, str) } else { if err := os.Remove(legacyMetadataFilepath); err != nil { - return "", err + return err } - str = fmt.Sprintf("\nRemoved legacy metadata: %s\n", legacyMetadataFilepath) - fmt.Fprintf(os.Stderr, str) } - return str, nil + return nil } diff --git a/workspace/exercise_test.go b/workspace/exercise_test.go index 14bd784a9..993140196 100644 --- a/workspace/exercise_test.go +++ b/workspace/exercise_test.go @@ -1,7 +1,6 @@ package workspace import ( - "fmt" "io/ioutil" "os" "path/filepath" @@ -81,13 +80,17 @@ func TestMigrateLegacyMetadataFileWithoutLegacy(t *testing.T) { err = ioutil.WriteFile(metadataFilepath, []byte{}, os.FileMode(0600)) assert.NoError(t, err) - ok, _ := exercise.HasMetadata() - assert.True(t, ok) - stderr, err := exercise.MigrateLegacyMetadataFile() + ok, _ := exercise.HasLegacyMetadata() + assert.False(t, ok) + ok, _ = exercise.HasMetadata() + assert.True(t, ok) + err = exercise.MigrateLegacyMetadataFile() assert.Nil(t, err) - assert.Equal(t, "", stderr) + + ok, _ = exercise.HasLegacyMetadata() + assert.False(t, ok) ok, _ = exercise.HasMetadata() assert.True(t, ok) } @@ -98,22 +101,21 @@ func TestMigrateLegacyMetadataFileWithLegacy(t *testing.T) { assert.NoError(t, err) exercise := Exercise{Root: ws, Track: "bogus-track", Slug: "legacy"} - metadataFilepath := exercise.MetadataFilepath() legacyMetadataFilepath := exercise.LegacyMetadataFilepath() err = os.MkdirAll(filepath.Dir(legacyMetadataFilepath), os.FileMode(0755)) assert.NoError(t, err) err = ioutil.WriteFile(legacyMetadataFilepath, []byte{}, os.FileMode(0600)) assert.NoError(t, err) + ok, _ := exercise.HasLegacyMetadata() assert.True(t, ok) ok, _ = exercise.HasMetadata() assert.False(t, ok) - stderr, err := exercise.MigrateLegacyMetadataFile() - - assert.Equal(t, fmt.Sprintf("\nMigrated metadata to %s\n", metadataFilepath), stderr) + err = exercise.MigrateLegacyMetadataFile() assert.NoError(t, err) + ok, _ = exercise.HasLegacyMetadata() assert.False(t, ok) ok, _ = exercise.HasMetadata() @@ -137,15 +139,15 @@ func TestMigrateLegacyMetadataFileWithLegacyAndModern(t *testing.T) { assert.NoError(t, err) err = ioutil.WriteFile(metadataFilepath, []byte{}, os.FileMode(0600)) assert.NoError(t, err) + ok, _ := exercise.HasLegacyMetadata() assert.True(t, ok) ok, _ = exercise.HasMetadata() assert.True(t, ok) - stderr, err := exercise.MigrateLegacyMetadataFile() - - assert.Equal(t, fmt.Sprintf("\nRemoved legacy metadata: %s\n", legacyMetadataFilepath), stderr) + err = exercise.MigrateLegacyMetadataFile() assert.NoError(t, err) + ok, _ = exercise.HasLegacyMetadata() assert.False(t, ok) ok, _ = exercise.HasMetadata() From a79167bab87a390b3d6dc67e11e02a344e35f7ce Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Wed, 29 Aug 2018 10:04:50 +0700 Subject: [PATCH 255/544] Address metadata file checking review concern The logic for checking metadata files in the Migration method exists in Exercise's HasMetadata(). I previously avoided using these methods because this adds duplicate calls to MetadataFilepath() but this might be worth trading for DRYing up the file checking logic. --- workspace/exercise.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/workspace/exercise.go b/workspace/exercise.go index cde90754e..7a7d8104d 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -82,18 +82,17 @@ func (e Exercise) HasLegacyMetadata() (bool, error) { // This is a noop if the metadata file isn't legacy. // If both legacy and modern metadata files exist, the legacy file will be deleted. func (e Exercise) MigrateLegacyMetadataFile() error { - legacyMetadataFilepath := e.LegacyMetadataFilepath() - metadataFilepath := e.MetadataFilepath() - - if _, err := os.Lstat(legacyMetadataFilepath); err != nil { + if ok, _ := e.HasLegacyMetadata(); !ok { return nil } - dir := filepath.Join(filepath.Dir(legacyMetadataFilepath), ignoreSubdir) - if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { + legacyMetadataFilepath := e.LegacyMetadataFilepath() + if err := os.MkdirAll( + filepath.Join(filepath.Dir(legacyMetadataFilepath), ignoreSubdir), + os.FileMode(0755)); err != nil { return err } - if _, err := os.Lstat(metadataFilepath); err != nil { - if err := os.Rename(legacyMetadataFilepath, metadataFilepath); err != nil { + if ok, _ := e.HasMetadata(); !ok { + if err := os.Rename(legacyMetadataFilepath, e.MetadataFilepath()); err != nil { return err } } else { From c770724dbe539a80e6f863d77fcb657ab85f7285 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Wed, 29 Aug 2018 10:31:57 +0700 Subject: [PATCH 256/544] Refactor migration else path to be more explicit Else path is when both legacy and modern metadata files exist --- workspace/exercise.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/workspace/exercise.go b/workspace/exercise.go index 7a7d8104d..976df56ab 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -95,7 +95,8 @@ func (e Exercise) MigrateLegacyMetadataFile() error { if err := os.Rename(legacyMetadataFilepath, e.MetadataFilepath()); err != nil { return err } - } else { + } + if ok, _ := e.HasLegacyMetadata(); ok { if err := os.Remove(legacyMetadataFilepath); err != nil { return err } From 9cd6e4c25bfff128a87d8ddc56e47b17213f54fd Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Wed, 29 Aug 2018 10:53:46 +0700 Subject: [PATCH 257/544] Add state type return to MigrateLegacyMetadataFile Rather than printing from inside the migration, return a status type that can be inferred. --- cmd/submit.go | 2 +- workspace/exercise.go | 28 ++++++++++++++++++++++------ workspace/exercise_test.go | 9 ++++++--- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 5812cc42e..af5f7fc7a 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -125,7 +125,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { } exercise := workspace.NewExerciseFromDir(exerciseDir) - if err = exercise.MigrateLegacyMetadataFile(); err != nil { + if _, err = exercise.MigrateLegacyMetadataFile(); err != nil { return err } solution, err := workspace.NewSolution(exerciseDir) diff --git a/workspace/exercise.go b/workspace/exercise.go index 976df56ab..1fb5c3d32 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -78,28 +78,44 @@ func (e Exercise) HasLegacyMetadata() (bool, error) { return false, err } +// MigrationStatus represents the result of migrating a legacy metadata file. +type MigrationStatus int + +// MigrationStatus +const ( + MigrationStatusMkdirError MigrationStatus = iota + MigrationStatusRenameError + MigrationStatusRemoveError + MigrationStatusNoop + MigrationStatusMigrated + MigrationStatusRemoved +) + // MigrateLegacyMetadataFile migrates a legacy metadata to the modern location. // This is a noop if the metadata file isn't legacy. // If both legacy and modern metadata files exist, the legacy file will be deleted. -func (e Exercise) MigrateLegacyMetadataFile() error { +func (e Exercise) MigrateLegacyMetadataFile() (MigrationStatus, error) { if ok, _ := e.HasLegacyMetadata(); !ok { - return nil + return MigrationStatusNoop, nil } + var status MigrationStatus legacyMetadataFilepath := e.LegacyMetadataFilepath() if err := os.MkdirAll( filepath.Join(filepath.Dir(legacyMetadataFilepath), ignoreSubdir), os.FileMode(0755)); err != nil { - return err + return MigrationStatusMkdirError, err } if ok, _ := e.HasMetadata(); !ok { if err := os.Rename(legacyMetadataFilepath, e.MetadataFilepath()); err != nil { - return err + return MigrationStatusRenameError, err } + status = MigrationStatusMigrated } if ok, _ := e.HasLegacyMetadata(); ok { if err := os.Remove(legacyMetadataFilepath); err != nil { - return err + return MigrationStatusRemoveError, err } + status = MigrationStatusRemoved } - return nil + return status, nil } diff --git a/workspace/exercise_test.go b/workspace/exercise_test.go index 993140196..c1cfac351 100644 --- a/workspace/exercise_test.go +++ b/workspace/exercise_test.go @@ -86,7 +86,8 @@ func TestMigrateLegacyMetadataFileWithoutLegacy(t *testing.T) { ok, _ = exercise.HasMetadata() assert.True(t, ok) - err = exercise.MigrateLegacyMetadataFile() + status, err := exercise.MigrateLegacyMetadataFile() + assert.Equal(t, MigrationStatus(MigrationStatusNoop), status) assert.Nil(t, err) ok, _ = exercise.HasLegacyMetadata() @@ -113,7 +114,8 @@ func TestMigrateLegacyMetadataFileWithLegacy(t *testing.T) { ok, _ = exercise.HasMetadata() assert.False(t, ok) - err = exercise.MigrateLegacyMetadataFile() + status, err := exercise.MigrateLegacyMetadataFile() + assert.Equal(t, MigrationStatus(MigrationStatusMigrated), status) assert.NoError(t, err) ok, _ = exercise.HasLegacyMetadata() @@ -145,7 +147,8 @@ func TestMigrateLegacyMetadataFileWithLegacyAndModern(t *testing.T) { ok, _ = exercise.HasMetadata() assert.True(t, ok) - err = exercise.MigrateLegacyMetadataFile() + status, err := exercise.MigrateLegacyMetadataFile() + assert.Equal(t, MigrationStatus(MigrationStatusRemoved), status) assert.NoError(t, err) ok, _ = exercise.HasLegacyMetadata() From b555a3a8d69068cf89cbfc28686b9bfb698edff2 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Wed, 29 Aug 2018 15:07:00 +0700 Subject: [PATCH 258/544] Refactor simplify migration logic --- workspace/exercise.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/workspace/exercise.go b/workspace/exercise.go index 1fb5c3d32..6d4ed31b9 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -98,7 +98,6 @@ func (e Exercise) MigrateLegacyMetadataFile() (MigrationStatus, error) { if ok, _ := e.HasLegacyMetadata(); !ok { return MigrationStatusNoop, nil } - var status MigrationStatus legacyMetadataFilepath := e.LegacyMetadataFilepath() if err := os.MkdirAll( filepath.Join(filepath.Dir(legacyMetadataFilepath), ignoreSubdir), @@ -109,13 +108,10 @@ func (e Exercise) MigrateLegacyMetadataFile() (MigrationStatus, error) { if err := os.Rename(legacyMetadataFilepath, e.MetadataFilepath()); err != nil { return MigrationStatusRenameError, err } - status = MigrationStatusMigrated + return MigrationStatusMigrated, nil } - if ok, _ := e.HasLegacyMetadata(); ok { - if err := os.Remove(legacyMetadataFilepath); err != nil { - return MigrationStatusRemoveError, err - } - status = MigrationStatusRemoved + if err := os.Remove(legacyMetadataFilepath); err != nil { + return MigrationStatusRemoveError, err } - return status, nil + return MigrationStatusRemoved, nil } From c7885bfb8e14aa05ee5da7abc26c939bd5afdc63 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Wed, 29 Aug 2018 15:25:44 +0700 Subject: [PATCH 259/544] Refactor idiomatic enum-style naming for migration --- workspace/exercise.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/workspace/exercise.go b/workspace/exercise.go index 6d4ed31b9..4177d1d19 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -83,9 +83,9 @@ type MigrationStatus int // MigrationStatus const ( - MigrationStatusMkdirError MigrationStatus = iota - MigrationStatusRenameError - MigrationStatusRemoveError + MigrationStatusErrorMkdir MigrationStatus = iota + MigrationStatusErrorRename + MigrationStatusErrorRemove MigrationStatusNoop MigrationStatusMigrated MigrationStatusRemoved @@ -102,16 +102,16 @@ func (e Exercise) MigrateLegacyMetadataFile() (MigrationStatus, error) { if err := os.MkdirAll( filepath.Join(filepath.Dir(legacyMetadataFilepath), ignoreSubdir), os.FileMode(0755)); err != nil { - return MigrationStatusMkdirError, err + return MigrationStatusErrorMkdir, err } if ok, _ := e.HasMetadata(); !ok { if err := os.Rename(legacyMetadataFilepath, e.MetadataFilepath()); err != nil { - return MigrationStatusRenameError, err + return MigrationStatusErrorRename, err } return MigrationStatusMigrated, nil } if err := os.Remove(legacyMetadataFilepath); err != nil { - return MigrationStatusRemoveError, err + return MigrationStatusErrorRemove, err } return MigrationStatusRemoved, nil } From b6fa515d421d57877ec65a8f8906ac7efa18235b Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Wed, 29 Aug 2018 17:32:40 +0700 Subject: [PATCH 260/544] Refactor migration target directory creation Avoid unnecessary coupling explicitly to the ignore subdirectory by using the non-legacy MetadataFilepath() --- workspace/exercise.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/workspace/exercise.go b/workspace/exercise.go index 4177d1d19..a034b29be 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -99,13 +99,12 @@ func (e Exercise) MigrateLegacyMetadataFile() (MigrationStatus, error) { return MigrationStatusNoop, nil } legacyMetadataFilepath := e.LegacyMetadataFilepath() - if err := os.MkdirAll( - filepath.Join(filepath.Dir(legacyMetadataFilepath), ignoreSubdir), - os.FileMode(0755)); err != nil { + metadataFilepath := e.MetadataFilepath() + if err := os.MkdirAll(filepath.Dir(metadataFilepath), os.FileMode(0755)); err != nil { return MigrationStatusErrorMkdir, err } if ok, _ := e.HasMetadata(); !ok { - if err := os.Rename(legacyMetadataFilepath, e.MetadataFilepath()); err != nil { + if err := os.Rename(legacyMetadataFilepath, metadataFilepath); err != nil { return MigrationStatusErrorRename, err } return MigrationStatusMigrated, nil From 78f3604b2521e70bf2c3757c5fc026babacb69dc Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Wed, 29 Aug 2018 17:51:05 +0700 Subject: [PATCH 261/544] Fix missing word from godoc --- workspace/exercise.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspace/exercise.go b/workspace/exercise.go index a034b29be..73e214c6b 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -91,7 +91,7 @@ const ( MigrationStatusRemoved ) -// MigrateLegacyMetadataFile migrates a legacy metadata to the modern location. +// MigrateLegacyMetadataFile migrates a legacy metadata file to the modern location. // This is a noop if the metadata file isn't legacy. // If both legacy and modern metadata files exist, the legacy file will be deleted. func (e Exercise) MigrateLegacyMetadataFile() (MigrationStatus, error) { From 6320d957348fb6f1ee9cba918588b746babadf19 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 29 Aug 2018 16:07:07 -0600 Subject: [PATCH 262/544] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d78781a14..0e7ad52e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ## Next Release * **Your contribution here** +* [#720](https://github.com/exercism/cli/pull/720) Make the timeout configurable globally - [@kytrinyx] +* [#721](https://github.com/exercism/cli/pull/721) Handle windows filepaths that accidentally got submitted to the server - [@kytrinyx] +* [#722](https://github.com/exercism/cli/pull/722) Handle exercise directories with numeric suffixes - [@kytrinyx] ## v3.0.8 (2018-08-22) * [#713](https://github.com/exercism/cli/pull/713) Fix broken support for uuid flag on download command - [@nywilken] From e21aa6586f2cae94dfbdd90beeca942ad12f31bd Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 29 Aug 2018 16:07:40 -0600 Subject: [PATCH 263/544] Bump version to v3.0.9 --- CHANGELOG.md | 2 ++ cmd/version.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e7ad52e1..8c9d73775 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ## Next Release * **Your contribution here** + +## v3.0.9 (2018-08-29) * [#720](https://github.com/exercism/cli/pull/720) Make the timeout configurable globally - [@kytrinyx] * [#721](https://github.com/exercism/cli/pull/721) Handle windows filepaths that accidentally got submitted to the server - [@kytrinyx] * [#722](https://github.com/exercism/cli/pull/722) Handle exercise directories with numeric suffixes - [@kytrinyx] diff --git a/cmd/version.go b/cmd/version.go index f65b5e5a7..f83413265 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.0.8" +const Version = "3.0.9" // checkLatest flag for version command. var checkLatest bool From 0ae92f5940b1a96f0676848acdbb212b53a9847a Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Thu, 30 Aug 2018 07:33:05 +0700 Subject: [PATCH 264/544] Remove migration cache vars and useless casting --- workspace/exercise.go | 14 ++++++-------- workspace/exercise_test.go | 6 +++--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/workspace/exercise.go b/workspace/exercise.go index 73e214c6b..542dc4f60 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -98,19 +98,17 @@ func (e Exercise) MigrateLegacyMetadataFile() (MigrationStatus, error) { if ok, _ := e.HasLegacyMetadata(); !ok { return MigrationStatusNoop, nil } - legacyMetadataFilepath := e.LegacyMetadataFilepath() - metadataFilepath := e.MetadataFilepath() - if err := os.MkdirAll(filepath.Dir(metadataFilepath), os.FileMode(0755)); err != nil { - return MigrationStatusErrorMkdir, err + if err := os.MkdirAll(filepath.Dir(e.MetadataFilepath()), os.FileMode(0755)); err != nil { + return MigrationStatusNoop, err } if ok, _ := e.HasMetadata(); !ok { - if err := os.Rename(legacyMetadataFilepath, metadataFilepath); err != nil { - return MigrationStatusErrorRename, err + if err := os.Rename(e.LegacyMetadataFilepath(), e.MetadataFilepath()); err != nil { + return MigrationStatusNoop, err } return MigrationStatusMigrated, nil } - if err := os.Remove(legacyMetadataFilepath); err != nil { - return MigrationStatusErrorRemove, err + if err := os.Remove(e.LegacyMetadataFilepath()); err != nil { + return MigrationStatusNoop, err } return MigrationStatusRemoved, nil } diff --git a/workspace/exercise_test.go b/workspace/exercise_test.go index c1cfac351..00689c584 100644 --- a/workspace/exercise_test.go +++ b/workspace/exercise_test.go @@ -87,7 +87,7 @@ func TestMigrateLegacyMetadataFileWithoutLegacy(t *testing.T) { assert.True(t, ok) status, err := exercise.MigrateLegacyMetadataFile() - assert.Equal(t, MigrationStatus(MigrationStatusNoop), status) + assert.Equal(t, MigrationStatusNoop, status) assert.Nil(t, err) ok, _ = exercise.HasLegacyMetadata() @@ -115,7 +115,7 @@ func TestMigrateLegacyMetadataFileWithLegacy(t *testing.T) { assert.False(t, ok) status, err := exercise.MigrateLegacyMetadataFile() - assert.Equal(t, MigrationStatus(MigrationStatusMigrated), status) + assert.Equal(t, MigrationStatusMigrated, status) assert.NoError(t, err) ok, _ = exercise.HasLegacyMetadata() @@ -148,7 +148,7 @@ func TestMigrateLegacyMetadataFileWithLegacyAndModern(t *testing.T) { assert.True(t, ok) status, err := exercise.MigrateLegacyMetadataFile() - assert.Equal(t, MigrationStatus(MigrationStatusRemoved), status) + assert.Equal(t, MigrationStatusRemoved, status) assert.NoError(t, err) ok, _ = exercise.HasLegacyMetadata() From 5faa58563bbdb244f7e5103ae2066fe95da81e3a Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Thu, 30 Aug 2018 07:41:13 +0700 Subject: [PATCH 265/544] Add migration status printing in verbose mode --- cmd/submit.go | 6 +++++- workspace/exercise.go | 17 +++++++++++++---- workspace/exercise_test.go | 12 ++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index af5f7fc7a..9567e9623 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -125,9 +125,13 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { } exercise := workspace.NewExerciseFromDir(exerciseDir) - if _, err = exercise.MigrateLegacyMetadataFile(); err != nil { + migrationStatus, err := exercise.MigrateLegacyMetadataFile() + if err != nil { return err } + if verbose, _ := flags.GetBool("verbose"); verbose { + fmt.Fprintf(os.Stderr, migrationStatus.String(exercise)) + } solution, err := workspace.NewSolution(exerciseDir) if err != nil { return err diff --git a/workspace/exercise.go b/workspace/exercise.go index 542dc4f60..cdc920d53 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -1,6 +1,7 @@ package workspace import ( + "fmt" "os" "path" "path/filepath" @@ -83,14 +84,22 @@ type MigrationStatus int // MigrationStatus const ( - MigrationStatusErrorMkdir MigrationStatus = iota - MigrationStatusErrorRename - MigrationStatusErrorRemove - MigrationStatusNoop + MigrationStatusNoop MigrationStatus = iota MigrationStatusMigrated MigrationStatusRemoved ) +func (m MigrationStatus) String(e Exercise) string { + switch m { + case MigrationStatusMigrated: + return fmt.Sprintf("\nMigrated metadata to %s\n", e.MetadataFilepath()) + case MigrationStatusRemoved: + return fmt.Sprintf("\nRemoved legacy metadata at %s\n", e.LegacyMetadataFilepath()) + default: + return "" + } +} + // MigrateLegacyMetadataFile migrates a legacy metadata file to the modern location. // This is a noop if the metadata file isn't legacy. // If both legacy and modern metadata files exist, the legacy file will be deleted. diff --git a/workspace/exercise_test.go b/workspace/exercise_test.go index 00689c584..766e77dc4 100644 --- a/workspace/exercise_test.go +++ b/workspace/exercise_test.go @@ -1,6 +1,7 @@ package workspace import ( + "fmt" "io/ioutil" "os" "path/filepath" @@ -68,6 +69,17 @@ func TestNewFromDir(t *testing.T) { assert.Equal(t, "the-exercise", exercise.Slug) } +func TestMigrationStatusString(t *testing.T) { + exercise := Exercise{Root: "", Track: "bogus-track", Slug: "banana"} + + assert.Equal(t, fmt.Sprintf("\nMigrated metadata to %s\n", exercise.MetadataFilepath()), + MigrationStatusMigrated.String(exercise)) + assert.Equal(t, fmt.Sprintf("\nRemoved legacy metadata at %s\n", exercise.LegacyMetadataFilepath()), + MigrationStatusRemoved.String(exercise)) + assert.Equal(t, "", MigrationStatusNoop.String(exercise)) + assert.Equal(t, "", MigrationStatus(-1).String(exercise)) +} + func TestMigrateLegacyMetadataFileWithoutLegacy(t *testing.T) { ws, err := ioutil.TempDir("", "fake-workspace") defer os.RemoveAll(ws) From 305edf2afb7a2d5c3ea0724e83e37ef545ad79ef Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Thu, 30 Aug 2018 10:57:28 +0700 Subject: [PATCH 266/544] Refactor inline unnecessary vars --- cmd/submit_test.go | 6 ++---- workspace/solution.go | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index c26168248..5cb22260f 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -206,7 +206,6 @@ func TestLegacySolutionMetadataMigration(t *testing.T) { dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") os.MkdirAll(dir, os.FileMode(0755)) - // Write fake legacy solution solution := &workspace.Solution{ ID: "bogus-solution-uuid", Track: "bogus-track", @@ -233,15 +232,14 @@ func TestLegacySolutionMetadataMigration(t *testing.T) { Dir: tmpDir, UserViperConfig: v, } - expectedPathAfterMigration := exercise.MetadataFilepath() - _, err = os.Stat(expectedPathAfterMigration) + _, err = os.Stat(exercise.MetadataFilepath()) assert.Error(t, err) err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file}) assert.NoError(t, err) assert.Equal(t, "This is a file.", submittedFiles["file.txt"]) - _, err = os.Stat(expectedPathAfterMigration) + _, err = os.Stat(exercise.MetadataFilepath()) assert.NoError(t, err) _, err = os.Stat(exercise.LegacyMetadataFilepath()) assert.Error(t, err) diff --git a/workspace/solution.go b/workspace/solution.go index dffd1919a..024a8caf1 100644 --- a/workspace/solution.go +++ b/workspace/solution.go @@ -72,8 +72,8 @@ func (s *Solution) Write(dir string) error { if err = os.MkdirAll(filepath.Join(dir, ignoreSubdir), os.FileMode(0755)); err != nil { return err } - exercise := NewExerciseFromDir(dir) - if err = ioutil.WriteFile(exercise.MetadataFilepath(), b, os.FileMode(0600)); err != nil { + if err = ioutil.WriteFile(NewExerciseFromDir(dir).MetadataFilepath(), b, + os.FileMode(0600)); err != nil { return err } s.Dir = dir From 5d4e34b13f9d7e69e0392e0b5e19c22ae191f465 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Thu, 30 Aug 2018 10:59:56 +0700 Subject: [PATCH 267/544] Make migration submit test consistent w/ unit test Unify the assertions on metadata existing --- cmd/submit_test.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 5cb22260f..6c8347abb 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -232,17 +232,20 @@ func TestLegacySolutionMetadataMigration(t *testing.T) { Dir: tmpDir, UserViperConfig: v, } - _, err = os.Stat(exercise.MetadataFilepath()) - assert.Error(t, err) + + ok, _ := exercise.HasLegacyMetadata() + assert.True(t, ok) + ok, _ = exercise.HasMetadata() + assert.False(t, ok) err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file}) assert.NoError(t, err) assert.Equal(t, "This is a file.", submittedFiles["file.txt"]) - _, err = os.Stat(exercise.MetadataFilepath()) - assert.NoError(t, err) - _, err = os.Stat(exercise.LegacyMetadataFilepath()) - assert.Error(t, err) + ok, _ = exercise.HasLegacyMetadata() + assert.False(t, ok) + ok, _ = exercise.HasMetadata() + assert.True(t, ok) } func TestSubmitWithEmptyFile(t *testing.T) { From 52b740551eaf7addc759da4821d580aa75e5bebe Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Thu, 30 Aug 2018 11:17:12 +0700 Subject: [PATCH 268/544] Refactor improve var name 'dir' makes more sense now using Exercise.NewExerciseFromDir --- cmd/download_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index b79650ae2..4a9054803 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -142,8 +142,8 @@ func TestDownload(t *testing.T) { targetDir := filepath.Join(tmpDir, tc.expectedDir) assertDownloadedCorrectFiles(t, targetDir) - path := filepath.Join(targetDir, "bogus-track", "bogus-exercise") - b, err := ioutil.ReadFile(workspace.NewExerciseFromDir(path).MetadataFilepath()) + dir := filepath.Join(targetDir, "bogus-track", "bogus-exercise") + b, err := ioutil.ReadFile(workspace.NewExerciseFromDir(dir).MetadataFilepath()) var s workspace.Solution err = json.Unmarshal(b, &s) assert.NoError(t, err) From f074b6c506febdf8c57a31cc764a32e122296c44 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Thu, 30 Aug 2018 11:40:52 +0700 Subject: [PATCH 269/544] Refactor simplify solution.Write Replace knowledge of ignoreSubdir with encapsulated Exercise --- workspace/solution.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/workspace/solution.go b/workspace/solution.go index 024a8caf1..9ce1b9a24 100644 --- a/workspace/solution.go +++ b/workspace/solution.go @@ -69,11 +69,11 @@ func (s *Solution) Write(dir string) error { if err != nil { return err } - if err = os.MkdirAll(filepath.Join(dir, ignoreSubdir), os.FileMode(0755)); err != nil { + metadataAbsoluteFilepath := NewExerciseFromDir(dir).MetadataFilepath() + if err = os.MkdirAll(filepath.Dir(metadataAbsoluteFilepath), os.FileMode(0755)); err != nil { return err } - if err = ioutil.WriteFile(NewExerciseFromDir(dir).MetadataFilepath(), b, - os.FileMode(0600)); err != nil { + if err = ioutil.WriteFile(metadataAbsoluteFilepath, b, os.FileMode(0600)); err != nil { return err } s.Dir = dir From 8dbfcb5a01adad515ecf4e7e92b267c19cf3ad34 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Thu, 30 Aug 2018 11:53:21 +0700 Subject: [PATCH 270/544] Make migration test assertion consistent --- workspace/exercise_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspace/exercise_test.go b/workspace/exercise_test.go index 766e77dc4..0555528ed 100644 --- a/workspace/exercise_test.go +++ b/workspace/exercise_test.go @@ -100,7 +100,7 @@ func TestMigrateLegacyMetadataFileWithoutLegacy(t *testing.T) { status, err := exercise.MigrateLegacyMetadataFile() assert.Equal(t, MigrationStatusNoop, status) - assert.Nil(t, err) + assert.NoError(t, err) ok, _ = exercise.HasLegacyMetadata() assert.False(t, ok) From e646bc8fc1fbe2cac13b393596d39c190632bb3a Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Thu, 30 Aug 2018 12:09:43 +0700 Subject: [PATCH 271/544] Refactor inline temp --- workspace/solution.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/workspace/solution.go b/workspace/solution.go index 9ce1b9a24..d6a16870f 100644 --- a/workspace/solution.go +++ b/workspace/solution.go @@ -32,8 +32,7 @@ type Solution struct { // NewSolution reads solution metadata from a file in the given directory. func NewSolution(dir string) (*Solution, error) { - path := filepath.Join(dir, metadataFilepath) - b, err := ioutil.ReadFile(path) + b, err := ioutil.ReadFile(filepath.Join(dir, metadataFilepath)) if err != nil { return &Solution{}, err } From cdb407ec878eadcb75fc415e3a9b54224f2fad16 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 30 Aug 2018 21:14:55 -0400 Subject: [PATCH 272/544] submit: Update error message when submitting a directory (#724) * Update error message for submision of directory * Update test to check for helpful error * Updating message format to use BinaryName * Update error message format further * Fix test by double escaping --- cmd/submit.go | 6 +++++- cmd/submit_test.go | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/cmd/submit.go b/cmd/submit.go index 9952b5fdb..29d7eb440 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -87,8 +87,12 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { %s + Please change into the directory and provide the path to the file(s) you wish to submit + + %s submit FILENAME + ` - return fmt.Errorf(msg, arg) + return fmt.Errorf(msg, arg, BinaryName) } src, err := filepath.EvalSymlinks(arg) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 556c3d390..ca1147e32 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -123,6 +123,7 @@ func TestSubmitFilesAndDir(t *testing.T) { } err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) assert.Regexp(t, "submitting a directory", err.Error()) + assert.Regexp(t, "Please change into the directory and provide the path to the file\\(s\\) you wish to submit", err.Error()) } func TestSubmitFiles(t *testing.T) { From 839a2d4ad1d57450bb77985addc546b44b3e26d6 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Fri, 31 Aug 2018 10:10:47 +0700 Subject: [PATCH 273/544] Refactor simplify Solution.Write path knowledge Knowledge of metadata filepath doesn't require asking Exercise --- workspace/solution.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspace/solution.go b/workspace/solution.go index d6a16870f..a5722d6c9 100644 --- a/workspace/solution.go +++ b/workspace/solution.go @@ -68,7 +68,7 @@ func (s *Solution) Write(dir string) error { if err != nil { return err } - metadataAbsoluteFilepath := NewExerciseFromDir(dir).MetadataFilepath() + metadataAbsoluteFilepath := filepath.Join(dir, metadataFilepath) if err = os.MkdirAll(filepath.Dir(metadataAbsoluteFilepath), os.FileMode(0755)); err != nil { return err } From c6bff35fbea576dcb02a461c8928de69b01ad7a9 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Fri, 31 Aug 2018 10:26:13 +0700 Subject: [PATCH 274/544] Refactor simplify TestWorkspaceExercises Reduce needless complexity --- workspace/workspace_test.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go index 8231ba94f..322f04ebb 100644 --- a/workspace/workspace_test.go +++ b/workspace/workspace_test.go @@ -6,7 +6,6 @@ import ( "path/filepath" "runtime" "sort" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -61,12 +60,12 @@ func TestWorkspaceExercises(t *testing.T) { b2 := filepath.Join(tmpDir, "track-b", "exercise-two") for _, path := range []string{a1, a2, b1, b2} { - path := filepath.Dir(NewExerciseFromDir(path).MetadataFilepath()) - err := os.MkdirAll(path, os.FileMode(0755)) + metadataAbsoluteFilepath := filepath.Join(path, metadataFilepath) + err := os.MkdirAll(filepath.Dir(metadataAbsoluteFilepath), os.FileMode(0755)) assert.NoError(t, err) - if !strings.HasPrefix(path, a2) { - err = ioutil.WriteFile(filepath.Join(path, solutionFilename), []byte{}, os.FileMode(0600)) + if path != a2 { + err = ioutil.WriteFile(metadataAbsoluteFilepath, []byte{}, os.FileMode(0600)) assert.NoError(t, err) } } From 09b6115edd6f62147e8496681c43a21a515b8e34 Mon Sep 17 00:00:00 2001 From: Katrin Leinweber <9948149+katrinleinweber@users.noreply.github.com> Date: Sun, 2 Sep 2018 16:38:34 +0200 Subject: [PATCH 275/544] Update Oh My Zsh instructions --- shell/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/README.md b/shell/README.md index 75e707366..861325630 100644 --- a/shell/README.md +++ b/shell/README.md @@ -27,4 +27,4 @@ the following snippet source ~/.config/exercism/exercism_completion.zsh fi -**Note:** If you are using the popular [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) framework to manage your zsh plugins, you don't need to add the above snippet, all you need to do is create a file `exercism_completion.zsh` inside the `~/.oh-my-zsh/custom`. +**Note:** If you are using the popular [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) framework to manage your zsh plugins, move the file `exercism_completion.zsh` into `~/.oh-my-zsh/custom`. From 244846460989b5b8bd6baf21704ba066e73c657b Mon Sep 17 00:00:00 2001 From: Katrin Leinweber <9948149+katrinleinweber@users.noreply.github.com> Date: Mon, 3 Sep 2018 21:40:07 +0200 Subject: [PATCH 276/544] Convert note into section --- shell/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shell/README.md b/shell/README.md index 861325630..e8a258d65 100644 --- a/shell/README.md +++ b/shell/README.md @@ -27,4 +27,7 @@ the following snippet source ~/.config/exercism/exercism_completion.zsh fi -**Note:** If you are using the popular [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) framework to manage your zsh plugins, move the file `exercism_completion.zsh` into `~/.oh-my-zsh/custom`. + +#### Oh my Zsh + +If you are using the popular [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) framework to manage your zsh plugins, move the file `exercism_completion.zsh` into `~/.oh-my-zsh/custom`. From 998b67cedeaa40d1403eb70e5869cfcacaba5336 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 4 Sep 2018 06:44:53 +0700 Subject: [PATCH 277/544] MigrationStatus.String() doesn't take an Exercise --- cmd/submit.go | 2 +- workspace/exercise.go | 6 +++--- workspace/exercise_test.go | 12 ++++-------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 9567e9623..22e645c6a 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -130,7 +130,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } if verbose, _ := flags.GetBool("verbose"); verbose { - fmt.Fprintf(os.Stderr, migrationStatus.String(exercise)) + fmt.Fprintf(os.Stderr, migrationStatus.String()) } solution, err := workspace.NewSolution(exerciseDir) if err != nil { diff --git a/workspace/exercise.go b/workspace/exercise.go index cdc920d53..3e60fa7b5 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -89,12 +89,12 @@ const ( MigrationStatusRemoved ) -func (m MigrationStatus) String(e Exercise) string { +func (m MigrationStatus) String() string { switch m { case MigrationStatusMigrated: - return fmt.Sprintf("\nMigrated metadata to %s\n", e.MetadataFilepath()) + return fmt.Sprintf("\nMigrated metadata\n") case MigrationStatusRemoved: - return fmt.Sprintf("\nRemoved legacy metadata at %s\n", e.LegacyMetadataFilepath()) + return fmt.Sprintf("\nRemoved legacy metadata\n") default: return "" } diff --git a/workspace/exercise_test.go b/workspace/exercise_test.go index 0555528ed..7df164787 100644 --- a/workspace/exercise_test.go +++ b/workspace/exercise_test.go @@ -70,14 +70,10 @@ func TestNewFromDir(t *testing.T) { } func TestMigrationStatusString(t *testing.T) { - exercise := Exercise{Root: "", Track: "bogus-track", Slug: "banana"} - - assert.Equal(t, fmt.Sprintf("\nMigrated metadata to %s\n", exercise.MetadataFilepath()), - MigrationStatusMigrated.String(exercise)) - assert.Equal(t, fmt.Sprintf("\nRemoved legacy metadata at %s\n", exercise.LegacyMetadataFilepath()), - MigrationStatusRemoved.String(exercise)) - assert.Equal(t, "", MigrationStatusNoop.String(exercise)) - assert.Equal(t, "", MigrationStatus(-1).String(exercise)) + assert.Equal(t, fmt.Sprintf("\nMigrated metadata\n"), MigrationStatusMigrated.String()) + assert.Equal(t, fmt.Sprintf("\nRemoved legacy metadata\n"), MigrationStatusRemoved.String()) + assert.Equal(t, "", MigrationStatusNoop.String()) + assert.Equal(t, "", MigrationStatus(-1).String()) } func TestMigrateLegacyMetadataFileWithoutLegacy(t *testing.T) { From dc2feb5f1113f78f4dd97c5c50d98977b1791f2d Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 4 Sep 2018 08:44:14 +0700 Subject: [PATCH 278/544] Remove unnecessary Sprintf --- workspace/exercise.go | 5 ++--- workspace/exercise_test.go | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/workspace/exercise.go b/workspace/exercise.go index 3e60fa7b5..34dc31596 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -1,7 +1,6 @@ package workspace import ( - "fmt" "os" "path" "path/filepath" @@ -92,9 +91,9 @@ const ( func (m MigrationStatus) String() string { switch m { case MigrationStatusMigrated: - return fmt.Sprintf("\nMigrated metadata\n") + return "\nMigrated metadata\n" case MigrationStatusRemoved: - return fmt.Sprintf("\nRemoved legacy metadata\n") + return "\nRemoved legacy metadata\n" default: return "" } diff --git a/workspace/exercise_test.go b/workspace/exercise_test.go index 7df164787..0a6d1d8fa 100644 --- a/workspace/exercise_test.go +++ b/workspace/exercise_test.go @@ -1,7 +1,6 @@ package workspace import ( - "fmt" "io/ioutil" "os" "path/filepath" @@ -70,8 +69,8 @@ func TestNewFromDir(t *testing.T) { } func TestMigrationStatusString(t *testing.T) { - assert.Equal(t, fmt.Sprintf("\nMigrated metadata\n"), MigrationStatusMigrated.String()) - assert.Equal(t, fmt.Sprintf("\nRemoved legacy metadata\n"), MigrationStatusRemoved.String()) + assert.Equal(t, "\nMigrated metadata\n", MigrationStatusMigrated.String()) + assert.Equal(t, "\nRemoved legacy metadata\n", MigrationStatusRemoved.String()) assert.Equal(t, "", MigrationStatusNoop.String()) assert.Equal(t, "", MigrationStatus(-1).String()) } From beef6187b9318fc9c7fe63faddf7c929d287b784 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 5 Sep 2018 12:51:41 -0400 Subject: [PATCH 279/544] Prevent enormous files from being submitted (#725) closes #717 --- cmd/submit.go | 9 +++++++++ cmd/submit_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/cmd/submit.go b/cmd/submit.go index 8e364427a..6655c3863 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -161,6 +161,15 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { if err != nil { return err } + const maxFileSize int64 = 65535 + if info.Size() >= maxFileSize { + msg :=` + + The submitted file is larger than the max allowed file size of %d bytes. Please reduce the size of the file and try again. + + ` + return fmt.Errorf(msg, maxFileSize) + } if info.Size() == 0 { msg := ` diff --git a/cmd/submit_test.go b/cmd/submit_test.go index c651a2ce3..956633fe9 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -295,6 +295,52 @@ func TestSubmitWithEmptyFile(t *testing.T) { assert.Equal(t, "This is file 2.", submittedFiles["file-2.txt"]) } +func TestSubmitWithEnormousFile(t *testing.T) { + oldOut := Out + oldErr := Err + Out = ioutil.Discard + Err = ioutil.Discard + defer func() { + Out = oldOut + Err = oldErr + }() + + // The fake endpoint will populate this when it receives the call from the command. + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := ioutil.TempDir("", "enormous-file") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(dir, os.FileMode(0755)) + + writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + } + + file := filepath.Join(dir, "file.txt") + err = ioutil.WriteFile(file, make([]byte, 65535), os.FileMode(0755)) + if err != nil { + t.Fatal(err) + } + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file}) + + assert.Error(t, err) + assert.Regexp(t, "Please reduce the size of the file and try again.", err.Error()) +} + func TestSubmitFilesForTeamExercise(t *testing.T) { oldOut := Out oldErr := Err From 4a463a2f290a099c1cf40560c0c264cc9337afac Mon Sep 17 00:00:00 2001 From: jdsutherland Date: Tue, 18 Sep 2018 08:30:43 +0700 Subject: [PATCH 280/544] Remove unused solutions type (#737) --- workspace/solutions.go | 18 ------------------ workspace/solutions_test.go | 36 ------------------------------------ 2 files changed, 54 deletions(-) delete mode 100644 workspace/solutions.go delete mode 100644 workspace/solutions_test.go diff --git a/workspace/solutions.go b/workspace/solutions.go deleted file mode 100644 index 20228ab58..000000000 --- a/workspace/solutions.go +++ /dev/null @@ -1,18 +0,0 @@ -package workspace - -// Solutions is a collection of solutions to interactively choose from. -type Solutions []*Solution - -// NewSolutions loads up the solution metadata for each of the provided paths. -func NewSolutions(paths []string) (Solutions, error) { - var solutions []*Solution - - for _, path := range paths { - solution, err := NewSolution(path) - if err != nil { - return []*Solution{}, err - } - solutions = append(solutions, solution) - } - return solutions, nil -} diff --git a/workspace/solutions_test.go b/workspace/solutions_test.go deleted file mode 100644 index 65fdf6a80..000000000 --- a/workspace/solutions_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package workspace - -import ( - "path/filepath" - "runtime" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNewSolutions(t *testing.T) { - _, cwd, _, _ := runtime.Caller(0) - root := filepath.Join(cwd, "..", "..", "fixtures", "solutions") - - paths := []string{ - filepath.Join(root, "alpha"), - filepath.Join(root, "bravo"), - filepath.Join(root, "charlie"), - } - sx, err := NewSolutions(paths) - assert.NoError(t, err) - - if assert.Equal(t, 3, len(sx)) { - assert.Equal(t, "alpha", sx[0].ID) - assert.Equal(t, "bravo", sx[1].ID) - assert.Equal(t, "charlie", sx[2].ID) - } - - paths = []string{ - filepath.Join(root, "alpha"), - filepath.Join(root, "delta"), - filepath.Join(root, "bravo"), - } - _, err = NewSolutions(paths) - assert.Error(t, err) -} From 2c1e6c8d7c6b899617690bc48c25bb6b0fb641d0 Mon Sep 17 00:00:00 2001 From: Wilken Rivera Date: Tue, 18 Sep 2018 18:12:55 -0400 Subject: [PATCH 281/544] add missing contributor URLs to CHANGELOG (#738) --- CHANGELOG.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c9d73775..fc401eb0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -389,19 +389,19 @@ All changes by [@msgehard] * Implement login and logout * Build on Travis -[@Dparker1990]: https://github.com/Dparker1990 -[@LegalizeAdulthood]: https://github.com/LegalizeAdulthood -[@Tonkpils]: https://github.com/Tonkpils -[@TrevorBramble]: https://github.com/TrevorBramble +[@alebaffa]: https://github.com/alebaffa +[@AlexWheeler]: https://github.com/AlexWheeler [@ambroff]: https://github.com/ambroff [@andrewsardone]: https://github.com/andrewsardone [@anxiousmodernman]: https://github.com/anxiousmodernman +[@beanieboi]: https://github.com/beanieboi [@blackerby]: https://github.com/blackerby [@broady]: https://github.com/broady [@ccnp123]: https://github.com/ccnp123 [@cookrn]: https://github.com/cookrn [@daveyarwood]: https://github.com/daveyarwood [@devonestes]: https://github.com/devonestes +[@Dparker1990]: https://github.com/Dparker1990 [@djquan]: https://github.com/djquan [@dmmulroy]: https://github.com/dmmulroy [@dpritchett]: https://github.com/dpritchett @@ -414,18 +414,22 @@ All changes by [@msgehard] [@hjljo]: https://github.com/hjljo [@isbadawi]: https://github.com/isbadawi [@jdsutherland]: https://github.com/jdsutherland +[@jbaiter]: https://github.com/jbaiter [@jgsqware]: https://github.com/jgsqware [@jish]: https://github.com/jish [@jppunnett]: https://github.com/jppunnett [@kytrinyx]: https://github.com/kytrinyx [@lcowell]: https://github.com/lcowell +[@LegalizeAdulthood]: https://github.com/LegalizeAdulthood [@manusajith]: https://github.com/manusajith [@morphatic]: https://github.com/morphatic +[@mrageh]: https://github.com/mrageh [@msgehard]: https://github.com/msgehard [@narqo]: https://github.com/narqo [@neslom]: https://github.com/neslom [@nf]: https://github.com/nf [@nilbus]: https://github.com/nilbus +[@nywilken]: https://github.com/nywilken [@petertseng]: https://github.com/petertseng [@pminten]: https://github.com/pminten [@queuebit]: https://github.com/queuebit @@ -433,5 +437,7 @@ All changes by [@msgehard] [@rprouse]: https://github.com/rprouse [@simonjefford]: https://github.com/simonjefford [@srt32]: https://github.com/srt32 +[@Tonkpils]: https://github.com/Tonkpils +[@TrevorBramble]: https://github.com/TrevorBramble [@williandrade]: https://github.com/williandrade [@zabawaba99]: https://github.com/zabawaba99 From 2fbd8d7135ff1730718edb0dc1bb917dc37b6f0b Mon Sep 17 00:00:00 2001 From: jdsutherland Date: Sat, 22 Sep 2018 09:08:42 +0700 Subject: [PATCH 282/544] Rename 'solution metadata' to 'exercise metadata' (#736) * Rename solution.json related refs to metadata.json * Rename Metadata to ExerciseMetadata for clarity --- CHANGELOG.md | 2 +- cmd/download.go | 24 ++++---- cmd/download_test.go | 10 ++-- cmd/open.go | 4 +- cmd/submit.go | 14 ++--- cmd/submit_symlink_test.go | 2 +- cmd/submit_test.go | 30 +++++----- .../{solution.json => metadata.json} | 0 .../{solution.json => metadata.json} | 0 .../{solution.json => metadata.json} | 0 .../{solution.json => metadata.json} | 0 .../{solution.json => metadata.json} | 0 .../{solution.json => metadata.json} | 0 .../{solution.json => metadata.json} | 0 .../{solution.json => metadata.json} | 0 .../{solution.json => metadata.json} | 0 workspace/exercise.go | 2 +- .../{solution.go => exercise_metadata.go} | 56 +++++++++---------- ...tion_test.go => exercise_metadata_test.go} | 50 ++++++++--------- workspace/workspace.go | 10 ++-- workspace/workspace_test.go | 4 +- 21 files changed, 104 insertions(+), 104 deletions(-) rename fixtures/is-solution-path/broken/.exercism/{solution.json => metadata.json} (100%) rename fixtures/is-solution-path/yepp/.exercism/{solution.json => metadata.json} (100%) rename fixtures/solution-dir/workspace/exercise/.exercism/{solution.json => metadata.json} (100%) rename fixtures/solution-path/creatures/gazelle-2/.exercism/{solution.json => metadata.json} (100%) rename fixtures/solution-path/creatures/gazelle-3/.exercism/{solution.json => metadata.json} (100%) rename fixtures/solution-path/creatures/gazelle/.exercism/{solution.json => metadata.json} (100%) rename fixtures/solutions/alpha/.exercism/{solution.json => metadata.json} (100%) rename fixtures/solutions/bravo/.exercism/{solution.json => metadata.json} (100%) rename fixtures/solutions/charlie/.exercism/{solution.json => metadata.json} (100%) rename workspace/{solution.go => exercise_metadata.go} (52%) rename workspace/{solution_test.go => exercise_metadata_test.go} (70%) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc401eb0d..fdefa2138 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ---------------- ## Next Release -* **Your contribution here** +* [#736](https://github.com/exercism/cli/pull/736) Metadata file .solution.json renamed to metadata.json - [@jdsutherland] ## v3.0.9 (2018-08-29) * [#720](https://github.com/exercism/cli/pull/720) Make the timeout configurable globally - [@kytrinyx] diff --git a/cmd/download.go b/cmd/download.go index c0139e80e..fd54052b2 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -132,7 +132,7 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { } } - solution := workspace.Solution{ + metadata := workspace.ExerciseMetadata{ AutoApprove: payload.Solution.Exercise.AutoApprove, Track: payload.Solution.Exercise.Track.ID, Team: payload.Solution.Team.Slug, @@ -144,17 +144,17 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { } root := usrCfg.GetString("workspace") - if solution.Team != "" { - root = filepath.Join(root, "teams", solution.Team) + if metadata.Team != "" { + root = filepath.Join(root, "teams", metadata.Team) } - if !solution.IsRequester { - root = filepath.Join(root, "users", solution.Handle) + if !metadata.IsRequester { + root = filepath.Join(root, "users", metadata.Handle) } exercise := workspace.Exercise{ Root: root, - Track: solution.Track, - Slug: solution.Exercise, + Track: metadata.Track, + Slug: metadata.Exercise, } dir := exercise.MetadataDir() @@ -163,7 +163,7 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - err = solution.Write(dir) + err = metadata.Write(dir) if err != nil { return err } @@ -204,7 +204,7 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { // Work around a path bug due to an early design decision (later reversed) to // allow numeric suffixes for exercise directories, allowing people to have // multiple parallel versions of an exercise. - pattern := fmt.Sprintf(`\A.*[/\\]%s-\d*/`, solution.Exercise) + pattern := fmt.Sprintf(`\A.*[/\\]%s-\d*/`, metadata.Exercise) rgxNumericSuffix := regexp.MustCompile(pattern) if rgxNumericSuffix.MatchString(file) { file = string(rgxNumericSuffix.ReplaceAll([]byte(file), []byte(""))) @@ -214,10 +214,10 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { file = strings.Replace(file, "\\", "/", -1) relativePath := filepath.FromSlash(file) - dir := filepath.Join(solution.Dir, filepath.Dir(relativePath)) + dir := filepath.Join(metadata.Dir, filepath.Dir(relativePath)) os.MkdirAll(dir, os.FileMode(0755)) - f, err := os.Create(filepath.Join(solution.Dir, relativePath)) + f, err := os.Create(filepath.Join(metadata.Dir, relativePath)) if err != nil { return err } @@ -228,7 +228,7 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { } } fmt.Fprintf(Err, "\nDownloaded to\n") - fmt.Fprintf(Out, "%s\n", solution.Dir) + fmt.Fprintf(Out, "%s\n", metadata.Dir) return nil } diff --git a/cmd/download_test.go b/cmd/download_test.go index f9aeacebe..0f08da5b9 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -144,13 +144,13 @@ func TestDownload(t *testing.T) { dir := filepath.Join(targetDir, "bogus-track", "bogus-exercise") b, err := ioutil.ReadFile(workspace.NewExerciseFromDir(dir).MetadataFilepath()) - var s workspace.Solution - err = json.Unmarshal(b, &s) + var metadata workspace.ExerciseMetadata + err = json.Unmarshal(b, &metadata) assert.NoError(t, err) - assert.Equal(t, "bogus-track", s.Track) - assert.Equal(t, "bogus-exercise", s.Exercise) - assert.Equal(t, tc.requester, s.IsRequester) + assert.Equal(t, "bogus-track", metadata.Track) + assert.Equal(t, "bogus-exercise", metadata.Exercise) + assert.Equal(t, tc.requester, metadata.IsRequester) } } diff --git a/cmd/open.go b/cmd/open.go index 15166b255..4d04245e8 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -17,11 +17,11 @@ Pass the path to the directory that contains the solution you want to see on the `, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - solution, err := workspace.NewSolution(args[0]) + metadata, err := workspace.NewExerciseMetadata(args[0]) if err != nil { return err } - browser.Open(solution.URL) + browser.Open(metadata.URL) return nil }, } diff --git a/cmd/submit.go b/cmd/submit.go index 6655c3863..bfd55f1c9 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -109,7 +109,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { var exerciseDir string for _, arg := range args { - dir, err := ws.SolutionDir(arg) + dir, err := ws.ExerciseDir(arg) if err != nil { if workspace.IsMissingMetadata(err) { return errors.New(msgMissingMetadata) @@ -136,12 +136,12 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { if verbose, _ := flags.GetBool("verbose"); verbose { fmt.Fprintf(os.Stderr, migrationStatus.String()) } - solution, err := workspace.NewSolution(exerciseDir) + metadata, err := workspace.NewExerciseMetadata(exerciseDir) if err != nil { return err } - if !solution.IsRequester { + if !metadata.IsRequester { // TODO: add test msg := ` @@ -151,7 +151,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { %s download --exercise=%s --track=%s ` - return fmt.Errorf(msg, BinaryName, solution.Exercise, solution.Track) + return fmt.Errorf(msg, BinaryName, metadata.Exercise, metadata.Track) } exercise.Documents = make([]workspace.Document, 0, len(args)) @@ -226,7 +226,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { if err != nil { return err } - url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), solution.ID) + url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), metadata.ID) req, err := client.NewRequest("PATCH", url, body) if err != nil { return err @@ -251,11 +251,11 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { %s ` suffix := "View it at:\n\n " - if solution.AutoApprove { + if metadata.AutoApprove { suffix = "You can complete the exercise and unlock the next core exercise at:\n" } fmt.Fprintf(Err, msg, suffix) - fmt.Fprintf(Out, " %s\n\n", solution.URL) + fmt.Fprintf(Out, " %s\n\n", metadata.URL) return nil } diff --git a/cmd/submit_symlink_test.go b/cmd/submit_symlink_test.go index ccf387827..8d193cd6c 100644 --- a/cmd/submit_symlink_test.go +++ b/cmd/submit_symlink_test.go @@ -44,7 +44,7 @@ func TestSubmitFilesInSymlinkedPath(t *testing.T) { dir := filepath.Join(dstDir, "bogus-track", "bogus-exercise") os.MkdirAll(dir, os.FileMode(0755)) - writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") v := viper.New() v.Set("token", "abc123") diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 956633fe9..b2e38d9b6 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -70,7 +70,7 @@ func TestSubmitNonExistentFile(t *testing.T) { assert.Regexp(t, "cannot be found", err.Error()) } -func TestSubmitExerciseWithoutSolutionMetadataFile(t *testing.T) { +func TestSubmitExerciseWithoutMetadataFile(t *testing.T) { tmpDir, err := ioutil.TempDir("", "no-metadata-file") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -147,7 +147,7 @@ func TestSubmitFiles(t *testing.T) { dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) - writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") file1 := filepath.Join(dir, "file-1.txt") err = ioutil.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) @@ -186,7 +186,7 @@ func TestSubmitFiles(t *testing.T) { assert.Equal(t, "This is the readme.", submittedFiles["README.md"]) } -func TestLegacySolutionMetadataMigration(t *testing.T) { +func TestLegacyMetadataMigration(t *testing.T) { oldOut := Out oldErr := Err Out = ioutil.Discard @@ -207,14 +207,14 @@ func TestLegacySolutionMetadataMigration(t *testing.T) { dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") os.MkdirAll(dir, os.FileMode(0755)) - solution := &workspace.Solution{ + metadata := &workspace.ExerciseMetadata{ ID: "bogus-solution-uuid", Track: "bogus-track", Exercise: "bogus-exercise", URL: "http://example.com/bogus-url", IsRequester: true, } - b, err := json.Marshal(solution) + b, err := json.Marshal(metadata) assert.NoError(t, err) exercise := workspace.NewExerciseFromDir(dir) err = ioutil.WriteFile(exercise.LegacyMetadataFilepath(), b, os.FileMode(0600)) @@ -271,7 +271,7 @@ func TestSubmitWithEmptyFile(t *testing.T) { dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") os.MkdirAll(dir, os.FileMode(0755)) - writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") v := viper.New() v.Set("token", "abc123") @@ -317,7 +317,7 @@ func TestSubmitWithEnormousFile(t *testing.T) { dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") os.MkdirAll(dir, os.FileMode(0755)) - writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") v := viper.New() v.Set("token", "abc123") @@ -360,7 +360,7 @@ func TestSubmitFilesForTeamExercise(t *testing.T) { dir := filepath.Join(tmpDir, "teams", "bogus-team", "bogus-track", "bogus-exercise") os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) - writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") file1 := filepath.Join(dir, "file-1.txt") err = ioutil.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) @@ -409,7 +409,7 @@ func TestSubmitOnlyEmptyFile(t *testing.T) { dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") os.MkdirAll(dir, os.FileMode(0755)) - writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") v := viper.New() v.Set("token", "abc123") @@ -435,11 +435,11 @@ func TestSubmitFilesFromDifferentSolutions(t *testing.T) { dir1 := filepath.Join(tmpDir, "bogus-track", "bogus-exercise-1") os.MkdirAll(dir1, os.FileMode(0755)) - writeFakeSolution(t, dir1, "bogus-track", "bogus-exercise-1") + writeFakeMetadata(t, dir1, "bogus-track", "bogus-exercise-1") dir2 := filepath.Join(tmpDir, "bogus-track", "bogus-exercise-2") os.MkdirAll(dir2, os.FileMode(0755)) - writeFakeSolution(t, dir2, "bogus-track", "bogus-exercise-2") + writeFakeMetadata(t, dir2, "bogus-track", "bogus-exercise-2") file1 := filepath.Join(dir1, "file-1.txt") err = ioutil.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) @@ -510,7 +510,7 @@ func TestSubmitRelativePath(t *testing.T) { dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") os.MkdirAll(dir, os.FileMode(0755)) - writeFakeSolution(t, dir, "bogus-track", "bogus-exercise") + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") v := viper.New() v.Set("token", "abc123") @@ -534,14 +534,14 @@ func TestSubmitRelativePath(t *testing.T) { assert.Equal(t, "This is a file.", submittedFiles["file.txt"]) } -func writeFakeSolution(t *testing.T, dir, trackID, exerciseSlug string) { - solution := &workspace.Solution{ +func writeFakeMetadata(t *testing.T, dir, trackID, exerciseSlug string) { + metadata := &workspace.ExerciseMetadata{ ID: "bogus-solution-uuid", Track: trackID, Exercise: exerciseSlug, URL: "http://example.com/bogus-url", IsRequester: true, } - err := solution.Write(dir) + err := metadata.Write(dir) assert.NoError(t, err) } diff --git a/fixtures/is-solution-path/broken/.exercism/solution.json b/fixtures/is-solution-path/broken/.exercism/metadata.json similarity index 100% rename from fixtures/is-solution-path/broken/.exercism/solution.json rename to fixtures/is-solution-path/broken/.exercism/metadata.json diff --git a/fixtures/is-solution-path/yepp/.exercism/solution.json b/fixtures/is-solution-path/yepp/.exercism/metadata.json similarity index 100% rename from fixtures/is-solution-path/yepp/.exercism/solution.json rename to fixtures/is-solution-path/yepp/.exercism/metadata.json diff --git a/fixtures/solution-dir/workspace/exercise/.exercism/solution.json b/fixtures/solution-dir/workspace/exercise/.exercism/metadata.json similarity index 100% rename from fixtures/solution-dir/workspace/exercise/.exercism/solution.json rename to fixtures/solution-dir/workspace/exercise/.exercism/metadata.json diff --git a/fixtures/solution-path/creatures/gazelle-2/.exercism/solution.json b/fixtures/solution-path/creatures/gazelle-2/.exercism/metadata.json similarity index 100% rename from fixtures/solution-path/creatures/gazelle-2/.exercism/solution.json rename to fixtures/solution-path/creatures/gazelle-2/.exercism/metadata.json diff --git a/fixtures/solution-path/creatures/gazelle-3/.exercism/solution.json b/fixtures/solution-path/creatures/gazelle-3/.exercism/metadata.json similarity index 100% rename from fixtures/solution-path/creatures/gazelle-3/.exercism/solution.json rename to fixtures/solution-path/creatures/gazelle-3/.exercism/metadata.json diff --git a/fixtures/solution-path/creatures/gazelle/.exercism/solution.json b/fixtures/solution-path/creatures/gazelle/.exercism/metadata.json similarity index 100% rename from fixtures/solution-path/creatures/gazelle/.exercism/solution.json rename to fixtures/solution-path/creatures/gazelle/.exercism/metadata.json diff --git a/fixtures/solutions/alpha/.exercism/solution.json b/fixtures/solutions/alpha/.exercism/metadata.json similarity index 100% rename from fixtures/solutions/alpha/.exercism/solution.json rename to fixtures/solutions/alpha/.exercism/metadata.json diff --git a/fixtures/solutions/bravo/.exercism/solution.json b/fixtures/solutions/bravo/.exercism/metadata.json similarity index 100% rename from fixtures/solutions/bravo/.exercism/solution.json rename to fixtures/solutions/bravo/.exercism/metadata.json diff --git a/fixtures/solutions/charlie/.exercism/solution.json b/fixtures/solutions/charlie/.exercism/metadata.json similarity index 100% rename from fixtures/solutions/charlie/.exercism/solution.json rename to fixtures/solutions/charlie/.exercism/metadata.json diff --git a/workspace/exercise.go b/workspace/exercise.go index 34dc31596..d87aa6178 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -42,7 +42,7 @@ func (e Exercise) MetadataFilepath() string { // LegacyMetadataFilepath is the absolute path to the legacy exercise metadata. func (e Exercise) LegacyMetadataFilepath() string { - return filepath.Join(e.Filepath(), legacySolutionFilename) + return filepath.Join(e.Filepath(), legacyMetadataFilename) } // MetadataDir returns the directory that the exercise metadata lives in. diff --git a/workspace/solution.go b/workspace/exercise_metadata.go similarity index 52% rename from workspace/solution.go rename to workspace/exercise_metadata.go index a5722d6c9..48d6c9347 100644 --- a/workspace/solution.go +++ b/workspace/exercise_metadata.go @@ -10,14 +10,14 @@ import ( "time" ) -const solutionFilename = "solution.json" -const legacySolutionFilename = ".solution.json" +const metadataFilename = "metadata.json" +const legacyMetadataFilename = ".solution.json" const ignoreSubdir = ".exercism" -var metadataFilepath = filepath.Join(ignoreSubdir, solutionFilename) +var metadataFilepath = filepath.Join(ignoreSubdir, metadataFilename) -// Solution contains metadata about a user's solution. -type Solution struct { +// ExerciseMetadata contains metadata about a user's exercise. +type ExerciseMetadata struct { Track string `json:"track"` Exercise string `json:"exercise"` ID string `json:"id"` @@ -30,41 +30,41 @@ type Solution struct { AutoApprove bool `json:"auto_approve"` } -// NewSolution reads solution metadata from a file in the given directory. -func NewSolution(dir string) (*Solution, error) { +// NewExerciseMetadata reads exercise metadata from a file in the given directory. +func NewExerciseMetadata(dir string) (*ExerciseMetadata, error) { b, err := ioutil.ReadFile(filepath.Join(dir, metadataFilepath)) if err != nil { - return &Solution{}, err + return nil, err } - var s Solution - if err := json.Unmarshal(b, &s); err != nil { - return &Solution{}, err + var metadata ExerciseMetadata + if err := json.Unmarshal(b, &metadata); err != nil { + return nil, err } - s.Dir = dir - return &s, nil + metadata.Dir = dir + return &metadata, nil } // Suffix is the serial numeric value appended to an exercise directory. // This is appended to avoid name conflicts, and does not indicate a particular // iteration. -func (s *Solution) Suffix() string { - return strings.Trim(strings.Replace(filepath.Base(s.Dir), s.Exercise, "", 1), "-.") +func (em *ExerciseMetadata) Suffix() string { + return strings.Trim(strings.Replace(filepath.Base(em.Dir), em.Exercise, "", 1), "-.") } -func (s *Solution) String() string { - str := fmt.Sprintf("%s/%s", s.Track, s.Exercise) - if s.Suffix() != "" { - str = fmt.Sprintf("%s (%s)", str, s.Suffix()) +func (em *ExerciseMetadata) String() string { + str := fmt.Sprintf("%s/%s", em.Track, em.Exercise) + if em.Suffix() != "" { + str = fmt.Sprintf("%s (%s)", str, em.Suffix()) } - if !s.IsRequester && s.Handle != "" { - str = fmt.Sprintf("%s by @%s", str, s.Handle) + if !em.IsRequester && em.Handle != "" { + str = fmt.Sprintf("%s by @%s", str, em.Handle) } return str } -// Write stores solution metadata to a file. -func (s *Solution) Write(dir string) error { - b, err := json.Marshal(s) +// Write stores exercise metadata to a file. +func (em *ExerciseMetadata) Write(dir string) error { + b, err := json.Marshal(em) if err != nil { return err } @@ -75,15 +75,15 @@ func (s *Solution) Write(dir string) error { if err = ioutil.WriteFile(metadataAbsoluteFilepath, b, os.FileMode(0600)); err != nil { return err } - s.Dir = dir + em.Dir = dir return nil } // PathToParent is the relative path from the workspace to the parent dir. -func (s *Solution) PathToParent() string { +func (em *ExerciseMetadata) PathToParent() string { var dir string - if !s.IsRequester { + if !em.IsRequester { dir = filepath.Join("users") } - return filepath.Join(dir, s.Track) + return filepath.Join(dir, em.Track) } diff --git a/workspace/solution_test.go b/workspace/exercise_metadata_test.go similarity index 70% rename from workspace/solution_test.go rename to workspace/exercise_metadata_test.go index 7c1d1e064..75fe819d8 100644 --- a/workspace/solution_test.go +++ b/workspace/exercise_metadata_test.go @@ -9,12 +9,12 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSolution(t *testing.T) { +func TestExerciseMetadata(t *testing.T) { dir, err := ioutil.TempDir("", "solution") assert.NoError(t, err) defer os.RemoveAll(dir) - s1 := &Solution{ + em1 := &ExerciseMetadata{ Track: "a-track", Exercise: "bogus-exercise", ID: "abc", @@ -23,53 +23,53 @@ func TestSolution(t *testing.T) { IsRequester: true, Dir: dir, } - err = s1.Write(dir) + err = em1.Write(dir) assert.NoError(t, err) - s2, err := NewSolution(dir) + em2, err := NewExerciseMetadata(dir) assert.NoError(t, err) - assert.Nil(t, s2.SubmittedAt) - assert.Equal(t, s1, s2) + assert.Nil(t, em2.SubmittedAt) + assert.Equal(t, em1, em2) ts := time.Date(2000, 1, 2, 3, 4, 5, 6, time.UTC) - s2.SubmittedAt = &ts + em2.SubmittedAt = &ts - err = s2.Write(dir) + err = em2.Write(dir) assert.NoError(t, err) - s3, err := NewSolution(dir) + em3, err := NewExerciseMetadata(dir) assert.NoError(t, err) - assert.Equal(t, s2, s3) + assert.Equal(t, em2, em3) } func TestSuffix(t *testing.T) { testCases := []struct { - solution Solution + metadata ExerciseMetadata suffix string }{ { - solution: Solution{ + metadata: ExerciseMetadata{ Exercise: "bat", Dir: "", }, suffix: "", }, { - solution: Solution{ + metadata: ExerciseMetadata{ Exercise: "bat", Dir: "/path/to/bat", }, suffix: "", }, { - solution: Solution{ + metadata: ExerciseMetadata{ Exercise: "bat", Dir: "/path/to/bat-2", }, suffix: "2", }, { - solution: Solution{ + metadata: ExerciseMetadata{ Exercise: "bat", Dir: "/path/to/bat-200", }, @@ -78,20 +78,20 @@ func TestSuffix(t *testing.T) { } for _, tc := range testCases { - testName := "Suffix of '" + tc.solution.Dir + "' should be " + tc.suffix + testName := "Suffix of '" + tc.metadata.Dir + "' should be " + tc.suffix t.Run(testName, func(t *testing.T) { - assert.Equal(t, tc.suffix, tc.solution.Suffix(), testName) + assert.Equal(t, tc.suffix, tc.metadata.Suffix(), testName) }) } } -func TestSolutionString(t *testing.T) { +func TestExerciseMetadataString(t *testing.T) { testCases := []struct { - solution Solution + metadata ExerciseMetadata desc string }{ { - solution: Solution{ + metadata: ExerciseMetadata{ Track: "elixir", Exercise: "secret-handshake", Handle: "", @@ -100,7 +100,7 @@ func TestSolutionString(t *testing.T) { desc: "elixir/secret-handshake", }, { - solution: Solution{ + metadata: ExerciseMetadata{ Track: "cpp", Exercise: "clock", Handle: "alice", @@ -109,7 +109,7 @@ func TestSolutionString(t *testing.T) { desc: "cpp/clock", }, { - solution: Solution{ + metadata: ExerciseMetadata{ Track: "cpp", Exercise: "clock", Handle: "alice", @@ -119,7 +119,7 @@ func TestSolutionString(t *testing.T) { desc: "cpp/clock (2)", }, { - solution: Solution{ + metadata: ExerciseMetadata{ Track: "fsharp", Exercise: "hello-world", Handle: "bob", @@ -128,7 +128,7 @@ func TestSolutionString(t *testing.T) { desc: "fsharp/hello-world by @bob", }, { - solution: Solution{ + metadata: ExerciseMetadata{ Track: "haskell", Exercise: "allergies", Handle: "charlie", @@ -142,7 +142,7 @@ func TestSolutionString(t *testing.T) { for _, tc := range testCases { testName := "should stringify to '" + tc.desc + "'" t.Run(testName, func(t *testing.T) { - assert.Equal(t, tc.desc, tc.solution.String()) + assert.Equal(t, tc.desc, tc.metadata.String()) }) } } diff --git a/workspace/workspace.go b/workspace/workspace.go index 78c20b65b..875f902c4 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -8,7 +8,7 @@ import ( "strings" ) -var errMissingMetadata = errors.New("no solution metadata file found") +var errMissingMetadata = errors.New("no exercise metadata file found") // IsMissingMetadata verifies the type of error. func IsMissingMetadata(err error) bool { @@ -95,9 +95,9 @@ func (ws Workspace) Exercises() ([]Exercise, error) { return exercises, nil } -// 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) { +// ExerciseDir determines the root directory of an exercise. +// This is the directory that contains the exercise metadata file. +func (ws Workspace) ExerciseDir(s string) (string, error) { if !strings.HasPrefix(s, ws.Dir) { return "", errors.New("not in workspace") } @@ -113,7 +113,7 @@ func (ws Workspace) SolutionDir(s string) (string, error) { if _, err := os.Lstat(filepath.Join(path, metadataFilepath)); err == nil { return path, nil } - if _, err := os.Lstat(filepath.Join(path, legacySolutionFilename)); err == nil { + if _, err := os.Lstat(filepath.Join(path, legacyMetadataFilename)); err == nil { return path, nil } path = filepath.Dir(path) diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go index 322f04ebb..59c76a8c2 100644 --- a/workspace/workspace_test.go +++ b/workspace/workspace_test.go @@ -88,7 +88,7 @@ func TestWorkspaceExercises(t *testing.T) { } } -func TestSolutionDir(t *testing.T) { +func TestExerciseDir(t *testing.T) { _, cwd, _, _ := runtime.Caller(0) root := filepath.Join(cwd, "..", "..", "fixtures", "solution-dir") @@ -130,7 +130,7 @@ func TestSolutionDir(t *testing.T) { } for _, test := range tests { - dir, err := ws.SolutionDir(test.path) + dir, err := ws.ExerciseDir(test.path) if !test.ok { assert.Error(t, err, test.path) continue From 39ec866e03c9d0c8cc4bc941dda776950cabb9a1 Mon Sep 17 00:00:00 2001 From: Wilken Rivera Date: Fri, 21 Sep 2018 22:10:18 -0400 Subject: [PATCH 283/544] update maxFileSize error to include filename (#739) --- cmd/submit.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index bfd55f1c9..bc031f05e 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -163,12 +163,13 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { } const maxFileSize int64 = 65535 if info.Size() >= maxFileSize { - msg :=` + msg := ` - The submitted file is larger than the max allowed file size of %d bytes. Please reduce the size of the file and try again. + The submitted file '%s' is larger than the max allowed file size of %d bytes. + Please reduce the size of the file and try again. ` - return fmt.Errorf(msg, maxFileSize) + return fmt.Errorf(msg, file, maxFileSize) } if info.Size() == 0 { From 0cd96e0c880e5df9f9a484c85f75512e20c8f7a0 Mon Sep 17 00:00:00 2001 From: nywilken Date: Tue, 18 Sep 2018 21:55:26 -0400 Subject: [PATCH 284/544] Bump version to v3.0.10-alpha.1 --- CHANGELOG.md | 12 ++++++++++++ cmd/version.go | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdefa2138..c044834af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,17 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ---------------- ## Next Release +* **Your contribution here** + +## v3.0.10-alpha.1 (2018-09-21) +* [#739](https://github.com/exercism/cli/pull/739) update maxFileSize error to include filename - [@nywilken] * [#736](https://github.com/exercism/cli/pull/736) Metadata file .solution.json renamed to metadata.json - [@jdsutherland] +* [#738](https://github.com/exercism/cli/pull/738) Add missing contributor URLs to CHANGELOG - [@nywilken] +* [#737](https://github.com/exercism/cli/pull/737) Remove unused solutions type - [@jdsutherland] +* [#729](https://github.com/exercism/cli/pull/729) Update Oh My Zsh instructions - [@katrinleinweber] +* [#725](https://github.com/exercism/cli/pull/725) Do not allow submission of enormous files - [@sfairchild] +* [#724](https://github.com/exercism/cli/pull/724) Update submit error message when submitting a directory - [@sfairchild] +* [#723](https://github.com/exercism/cli/pull/720) Move .solution.json to hidden subdirectory - [@jdsutherland] ## v3.0.9 (2018-08-29) * [#720](https://github.com/exercism/cli/pull/720) Make the timeout configurable globally - [@kytrinyx] @@ -419,6 +429,7 @@ All changes by [@msgehard] [@jish]: https://github.com/jish [@jppunnett]: https://github.com/jppunnett [@kytrinyx]: https://github.com/kytrinyx +[@katrinleinweber]: https://github.com/katrinleinweber [@lcowell]: https://github.com/lcowell [@LegalizeAdulthood]: https://github.com/LegalizeAdulthood [@manusajith]: https://github.com/manusajith @@ -435,6 +446,7 @@ All changes by [@msgehard] [@queuebit]: https://github.com/queuebit [@rcode5]: https://github.com/rcode5 [@rprouse]: https://github.com/rprouse +[@sfairchild]: https://github.com/sfairchild [@simonjefford]: https://github.com/simonjefford [@srt32]: https://github.com/srt32 [@Tonkpils]: https://github.com/Tonkpils diff --git a/cmd/version.go b/cmd/version.go index f83413265..155f777ba 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.0.9" +const Version = "3.0.10-alpha.1" // checkLatest flag for version command. var checkLatest bool From 7c1f4838a1b08b76ed80d35ea466d12f0a86cea7 Mon Sep 17 00:00:00 2001 From: nywilken Date: Wed, 3 Oct 2018 18:06:55 -0400 Subject: [PATCH 285/544] Bump version to v3.0.10 --- CHANGELOG.md | 3 +++ cmd/version.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c044834af..aaf1c96df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ## Next Release * **Your contribution here** +## v3.0.10 (2018-10-03) +* official release of v3.0.10-alpha.1 - [@nywilken] + ## v3.0.10-alpha.1 (2018-09-21) * [#739](https://github.com/exercism/cli/pull/739) update maxFileSize error to include filename - [@nywilken] * [#736](https://github.com/exercism/cli/pull/736) Metadata file .solution.json renamed to metadata.json - [@jdsutherland] diff --git a/cmd/version.go b/cmd/version.go index 155f777ba..5005821a7 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.0.10-alpha.1" +const Version = "3.0.10" // checkLatest flag for version command. var checkLatest bool From fd79bb33b4e63adb11dc67377c7f77eafb5ad34e Mon Sep 17 00:00:00 2001 From: Katrin Leinweber <9948149+katrinleinweber@users.noreply.github.com> Date: Sat, 20 Oct 2018 06:31:49 +0200 Subject: [PATCH 286/544] exercism_completion.zsh: Remove unsupported commands (#746) * Fix typo * Attempt to remove no-longer-supported commands --- shell/exercism_completion.zsh | 5 ----- 1 file changed, 5 deletions(-) diff --git a/shell/exercism_completion.zsh b/shell/exercism_completion.zsh index 44a9cc6e2..605069947 100644 --- a/shell/exercism_completion.zsh +++ b/shell/exercism_completion.zsh @@ -5,12 +5,7 @@ _exercism() { local -a options options=(debug:"Outputs useful debug information." configure:"Writes config values to a JSON file." - demo:"Fetches a demo problem for each language track on exercism.io." - fetch:"Fetches your current problems on exercism.ios well as the next unstarted problem in each language." - restore:"Restores completed and current problems on from exercism.iolong with your most recent iteration for each." submit:"Submits a new iteration to a problem on exercism.io." - unsubmit:"Deletes the most recently submitted iteration." - tracks:"List the available language tracks" download:"Downloads and saves a specified submission into the local system" help:"Shows a list of commands or help for one command") From ed6dc02bdeafc485cff869a49244180fbbe2ef99 Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Thu, 25 Oct 2018 04:12:48 +0200 Subject: [PATCH 287/544] Bump Travis versions (#750) --- .travis.yml | 2 +- CONTRIBUTING.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 220880371..29cb3ec73 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,8 @@ language: go sudo: false go: - - "1.9" - "1.10.x" + - "1.11.x" - tip install: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 33e910478..0b2eaf204 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ Exercism would be impossible without people like you being willing to spend time ## Dependencies -You'll need Go version 1.9 or higher. Follow the directions on http://golang.org/doc/install +You'll need Go version 1.10 or higher. Follow the directions on http://golang.org/doc/install You will also need `dep`, the Go dependency management tool. Follow the directions on https://golang.github.io/dep/docs/installation.html From 97d69cc960d16f001bc46396e55e21041ccf4df9 Mon Sep 17 00:00:00 2001 From: Jake Faris Date: Fri, 2 Nov 2018 12:58:32 -0400 Subject: [PATCH 288/544] Add Descriptive Error Message to Upgrade Command (#752) * Add Descriptive Error Message to Upgrade Command * Reword Error Message --- cmd/upgrade.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/cmd/upgrade.go b/cmd/upgrade.go index d1e6376bd..4b78d4b6f 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -23,7 +23,18 @@ You can always delete this file. `, RunE: func(cmd *cobra.Command, args []string) error { c := cli.New(Version) - return updateCLI(c) + err := updateCLI(c) + if err != nil { + return fmt.Errorf(` + +We were not able to upgrade the cli because we encountered an error: +%s + +Please check the FAQ for solutions to common upgrading issues. + +https://exercism.io/faqs`, err) + } + return nil }, } From e98310fb09c85e1813d8078a4af6e162f6911025 Mon Sep 17 00:00:00 2001 From: nywilken Date: Tue, 30 Oct 2018 21:32:04 -0400 Subject: [PATCH 289/544] Update shell tab completion for exercism cli resolves #666 --- shell/exercism_completion.bash | 42 +++++----------------------------- shell/exercism_completion.zsh | 15 +++++++----- 2 files changed, 15 insertions(+), 42 deletions(-) diff --git a/shell/exercism_completion.bash b/shell/exercism_completion.bash index f4a17e808..e53118d91 100644 --- a/shell/exercism_completion.bash +++ b/shell/exercism_completion.bash @@ -5,20 +5,10 @@ _exercism () { cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} - commands="configure debug download fetch list open - restore skip status submit tracks unsubmit - upgrade help" - tracks="csharp cpp clojure coffeescript lisp crystal - dlang ecmascript elixir elm elisp erlang - fsharp go haskell java javascript kotlin - lfe lua mips ocaml objective-c php - plsql perl5 python racket ruby rust scala - scheme swift typescript bash c ceylon - coldfusion delphi factor groovy haxe - idris julia nim perl6 pony prolog - purescript r sml vbnet powershell" - config_opts="--dir --host --key --api" - submit_opts="--test --comment" + commands="configure download open + submit troubleshoot upgrade version workspace help" + config_opts="--show" + version_opts="--latest" if [ "${#COMP_WORDS[@]}" -eq 2 ]; then COMPREPLY=( $( compgen -W "${commands}" "${cur}" ) ) @@ -31,28 +21,8 @@ _exercism () { COMPREPLY=( $( compgen -W "${config_opts}" -- "${cur}" ) ) return 0 ;; - fetch) - COMPREPLY=( $( compgen -W "${tracks}" "${cur}" ) ) - return 0 - ;; - list) - COMPREPLY=( $( compgen -W "${tracks}" "${cur}" ) ) - return 0 - ;; - open) - COMPREPLY=( $( compgen -W "${tracks}" "${cur}" ) ) - return 0 - ;; - skip) - COMPREPLY=( $( compgen -W "${tracks}" "${cur}" ) ) - return 0 - ;; - status) - COMPREPLY=( $( compgen -W "${tracks}" "${cur}" ) ) - return 0 - ;; - submit) - COMPREPLY=( $( compgen -W "${submit_opts}" -- "${cur}" ) ) + version) + COMPREPLY=( $( compgen -W "${version_opts}" -- "${cur}" ) ) return 0 ;; help) diff --git a/shell/exercism_completion.zsh b/shell/exercism_completion.zsh index 605069947..f21553064 100644 --- a/shell/exercism_completion.zsh +++ b/shell/exercism_completion.zsh @@ -3,17 +3,20 @@ _exercism() { typeset -A opt_args local -a options - options=(debug:"Outputs useful debug information." - configure:"Writes config values to a JSON file." - submit:"Submits a new iteration to a problem on exercism.io." + options=(configure:"Writes config values to a JSON file." download:"Downloads and saves a specified submission into the local system" + open:"Opens a browser to exercism.io for the specified submission." + submit:"Submits a new iteration to a problem on exercism.io." + troubleshoot:"Outputs useful debug information." + upgrade:"Upgrades to the latest available version." + version:"Outputs version information." + workspace:"Outputs the root directory for Exercism exercises." help:"Shows a list of commands or help for one command") _arguments -s -S \ - {-c,--config}"[path to config file]:file:_files" \ - {-d,--debug}"[turn on verbose logging]" \ {-h,--help}"[show help]" \ - {-v,--version}"[print the version]" \ + {-t,--timeout}"[override default HTTP timeout]" \ + {-v,--verbose}"[turn on verbose logging]" \ '(-): :->command' \ '(-)*:: :->option-or-argument' \ && return 0; From 517f301b1f799cab3e8f482f35957db16ef728c0 Mon Sep 17 00:00:00 2001 From: nywilken Date: Sat, 3 Nov 2018 21:02:33 -0400 Subject: [PATCH 290/544] Add bash completion for exercism opts --- shell/exercism_completion.bash | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/shell/exercism_completion.bash b/shell/exercism_completion.bash index e53118d91..f5f4e9010 100644 --- a/shell/exercism_completion.bash +++ b/shell/exercism_completion.bash @@ -4,6 +4,7 @@ _exercism () { COMPREPLY=() # Array variable storing the possible completions. cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} + opts="--verbose --timeout" commands="configure download open submit troubleshoot upgrade version workspace help" @@ -11,8 +12,16 @@ _exercism () { version_opts="--latest" if [ "${#COMP_WORDS[@]}" -eq 2 ]; then - COMPREPLY=( $( compgen -W "${commands}" "${cur}" ) ) - return 0 + case "${cur}" in + -*) + COMPREPLY=( $( compgen -W "${opts}" -- "${cur}" ) ) + return 0 + ;; + *) + COMPREPLY=( $( compgen -W "${commands}" "${cur}" ) ) + return 0 + ;; + esac fi if [ "${#COMP_WORDS[@]}" -eq 3 ]; then From 6b1b8ca8caa3995922d5ec9c6b0f8cab41364977 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Thu, 8 Nov 2018 10:24:36 +0700 Subject: [PATCH 291/544] Require gofmt on Travis build --- .travis.gofmt.sh | 7 +++++++ .travis.yml | 1 + 2 files changed, 8 insertions(+) create mode 100755 .travis.gofmt.sh diff --git a/.travis.gofmt.sh b/.travis.gofmt.sh new file mode 100755 index 000000000..2902ab797 --- /dev/null +++ b/.travis.gofmt.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +cd "$(dirname $0)" +if [ -n "$(go fmt ./...)" ]; then + echo "Go code is not formatted, run 'go fmt github.com/exercism/cli/...'" >&2 + exit 1 +fi diff --git a/.travis.yml b/.travis.yml index 29cb3ec73..282db173e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,4 +12,5 @@ install: - dep ensure script: + - ./.travis.gofmt.sh - go test -cover ./... From e4657c923a61284af5dd71a681b0a15446dd68ec Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Thu, 8 Nov 2018 11:10:36 +0700 Subject: [PATCH 292/544] Add gofmt failure to prove travis fails --- cmd/download.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/download.go b/cmd/download.go index fd54052b2..b23eb0ffb 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -18,6 +18,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" + "github.com/exercism/cli/debug" ) // downloadCmd represents the download command @@ -66,6 +67,7 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } if uuid != "" && slug != "" || uuid == slug { + debug.Printf("need to go in exercism website ,open the track and choose the exercise, \nthen copy the available link for download.\n ") return errors.New("need an --exercise name or a solution --uuid") } From 56d24b4807b02a8b9f65c560999cedfda2534952 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Thu, 8 Nov 2018 11:27:41 +0700 Subject: [PATCH 293/544] Revert "Add gofmt failure to prove travis fails" This reverts commit e4657c923a61284af5dd71a681b0a15446dd68ec. --- cmd/download.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index b23eb0ffb..fd54052b2 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -18,7 +18,6 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" - "github.com/exercism/cli/debug" ) // downloadCmd represents the download command @@ -67,7 +66,6 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } if uuid != "" && slug != "" || uuid == slug { - debug.Printf("need to go in exercism website ,open the track and choose the exercise, \nthen copy the available link for download.\n ") return errors.New("need an --exercise name or a solution --uuid") } From 8775d0fa614844c5a18874621e92101d11446cae Mon Sep 17 00:00:00 2001 From: Stucki Date: Tue, 13 Nov 2018 08:42:37 -0600 Subject: [PATCH 294/544] Update submit usage string --- cmd/submit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/submit.go b/cmd/submit.go index bc031f05e..2c4a69450 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -19,7 +19,7 @@ import ( // submitCmd lets people upload a solution to the website. var submitCmd = &cobra.Command{ - Use: "submit", + Use: "submit FILE1 [FILE2 ...]", Aliases: []string{"s"}, Short: "Submit your solution to an exercise.", Long: `Submit your solution to an Exercism exercise. From 38f0d5f69864d54a9c4ab45b80c6d95d14ec0830 Mon Sep 17 00:00:00 2001 From: John Goff Date: Tue, 13 Nov 2018 23:03:06 -0500 Subject: [PATCH 295/544] create fish completions for exercism --- shell/exercism.fish | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 shell/exercism.fish diff --git a/shell/exercism.fish b/shell/exercism.fish new file mode 100644 index 000000000..330f2fdfe --- /dev/null +++ b/shell/exercism.fish @@ -0,0 +1,12 @@ +complete -f -c exercism -a "configure" -d "Writes config values to a JSON file." +complete -f -c exercism -a "download" -d "Downloads and saves a specified submission into the local system" +complete -f -c exercism -a "help" -d "Shows a list of commands or help for one command" +complete -f -c exercism -a "open" -d "Opens a browser to exercism.io for the specified submission." +complete -f -c exercism -a "submit" -d "Submits a new iteration to a problem on exercism.io." +complete -f -c exercism -a "troubleshoot" -d "Outputs useful debug information." +complete -f -c exercism -a "upgrade" -d "Upgrades to the latest available version." +complete -f -c exercism -a "version" -d "Outputs version information." +complete -f -c exercism -a "workspace" -d "Outputs the root directory for Exercism exercises." +complete -f -c exercism -s h -l help -d "show help" +complete -f -c exercism -l timeout -a "(seq 0 100 100000)" -d "override default HTTP timeout" +complete -f -c exercism -s v -l verbose -d "turn on verbose logging" From 9816c9204287215e1907416d4da69142748cbcc7 Mon Sep 17 00:00:00 2001 From: John Goff Date: Tue, 13 Nov 2018 23:06:13 -0500 Subject: [PATCH 296/544] add fish to shell readme --- shell/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/shell/README.md b/shell/README.md index e8a258d65..35b1e623c 100644 --- a/shell/README.md +++ b/shell/README.md @@ -31,3 +31,10 @@ the following snippet #### Oh my Zsh If you are using the popular [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) framework to manage your zsh plugins, move the file `exercism_completion.zsh` into `~/.oh-my-zsh/custom`. + +### Fish + +Completions must go in the user defined `$fish_complete_path`. By default, this is `~/.config/fish/completions` + + mv ../shell/exercism.fish ~/.config/fish/exercism.fish + From 9f4ab99117f54389ad61d615e2aeee008cc736e2 Mon Sep 17 00:00:00 2001 From: John Goff Date: Tue, 13 Nov 2018 23:42:24 -0500 Subject: [PATCH 297/544] improve completions --- shell/exercism.fish | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/shell/exercism.fish b/shell/exercism.fish index 330f2fdfe..637b0cf12 100644 --- a/shell/exercism.fish +++ b/shell/exercism.fish @@ -1,12 +1,49 @@ +# Configure complete -f -c exercism -a "configure" -d "Writes config values to a JSON file." +complete -f -c exercism -n "__fish_seen_subcommand_from configure" -s t -l token -d "Set token" +complete -f -c exercism -n "__fish_seen_subcommand_from configure" -s w -l workspace -d "Set workspace" +complete -f -c exercism -n "__fish_seen_subcommand_from configure" -s a -l api -d "set API base url" +complete -f -c exercism -n "__fish_seen_subcommand_from configure" -s s -l show -d "show settings" + +# Download complete -f -c exercism -a "download" -d "Downloads and saves a specified submission into the local system" +complete -f -c exercism -n "__fish_seen_subcommand_from download" -s e -l exercise -d "the exercise slug" +complete -f -c exercism -n "__fish_seen_subcommand_from download" -s h -l help -d "help for download" +complete -f -c exercism -n "__fish_seen_subcommand_from download" -s T -l team -d "the team slug" +complete -f -c exercism -n "__fish_seen_subcommand_from download" -s t -l track -d "the track ID" +complete -f -c exercism -n "__fish_seen_subcommand_from download" -s u -l uuid -d "the solution UUID" + +# Help complete -f -c exercism -a "help" -d "Shows a list of commands or help for one command" +complete -f -c exercism -n "__fish_seen_subcommand_from help" -a "configure download help open submit troubleshoot upgrade version workspace" + +# Open complete -f -c exercism -a "open" -d "Opens a browser to exercism.io for the specified submission." +complete -f -c exercism -n "__fish_seen_subcommand_from open" -s h -l help -d "help for open" + +# Submit complete -f -c exercism -a "submit" -d "Submits a new iteration to a problem on exercism.io." +complete -f -c exercism -n "__fish_seen_subcommand_from submit" -s h -l help -d "help for submit" + +# Troubleshoot complete -f -c exercism -a "troubleshoot" -d "Outputs useful debug information." +complete -f -c exercism -n "__fish_seen_subcommand_from troubleshoot" -s f -l full-api-key -d "display full API key (censored by default)" +complete -f -c exercism -n "__fish_seen_subcommand_from troubleshoot" -s h -l help -d "help for troubleshoot" + +# Upgrade complete -f -c exercism -a "upgrade" -d "Upgrades to the latest available version." +complete -f -c exercism -n "__fish_seen_subcommand_from help" -s h -l help -d "help for help" + +# Version complete -f -c exercism -a "version" -d "Outputs version information." +complete -f -c exercism -n "__fish_seen_subcommand_from version" -s l -l latest -d "check latest available version" +complete -f -c exercism -n "__fish_seen_subcommand_from version" -s h -l help -d "help for version" + +# Workspace complete -f -c exercism -a "workspace" -d "Outputs the root directory for Exercism exercises." +complete -f -c exercism -n "__fish_seen_subcommand_from workspace" -s h -l help -d "help for workspace" + +# Options complete -f -c exercism -s h -l help -d "show help" complete -f -c exercism -l timeout -a "(seq 0 100 100000)" -d "override default HTTP timeout" complete -f -c exercism -s v -l verbose -d "turn on verbose logging" From 965a3ac37a50f87232d248a54593e937a4485b76 Mon Sep 17 00:00:00 2001 From: John Goff Date: Tue, 13 Nov 2018 23:53:46 -0500 Subject: [PATCH 298/544] prevent already matched arguments from being matched repeatedly --- shell/exercism.fish | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/shell/exercism.fish b/shell/exercism.fish index 637b0cf12..1b4821eba 100644 --- a/shell/exercism.fish +++ b/shell/exercism.fish @@ -1,12 +1,12 @@ # Configure -complete -f -c exercism -a "configure" -d "Writes config values to a JSON file." +complete -f -c exercism -n "__fish_use_subcommand" -a "configure" -d "Writes config values to a JSON file." complete -f -c exercism -n "__fish_seen_subcommand_from configure" -s t -l token -d "Set token" complete -f -c exercism -n "__fish_seen_subcommand_from configure" -s w -l workspace -d "Set workspace" complete -f -c exercism -n "__fish_seen_subcommand_from configure" -s a -l api -d "set API base url" complete -f -c exercism -n "__fish_seen_subcommand_from configure" -s s -l show -d "show settings" # Download -complete -f -c exercism -a "download" -d "Downloads and saves a specified submission into the local system" +complete -f -c exercism -n "__fish_use_subcommand" -a "download" -d "Downloads and saves a specified submission into the local system" complete -f -c exercism -n "__fish_seen_subcommand_from download" -s e -l exercise -d "the exercise slug" complete -f -c exercism -n "__fish_seen_subcommand_from download" -s h -l help -d "help for download" complete -f -c exercism -n "__fish_seen_subcommand_from download" -s T -l team -d "the team slug" @@ -14,33 +14,33 @@ complete -f -c exercism -n "__fish_seen_subcommand_from download" -s t -l track complete -f -c exercism -n "__fish_seen_subcommand_from download" -s u -l uuid -d "the solution UUID" # Help -complete -f -c exercism -a "help" -d "Shows a list of commands or help for one command" +complete -f -c exercism -n "__fish_use_subcommand" -a "help" -d "Shows a list of commands or help for one command" complete -f -c exercism -n "__fish_seen_subcommand_from help" -a "configure download help open submit troubleshoot upgrade version workspace" # Open -complete -f -c exercism -a "open" -d "Opens a browser to exercism.io for the specified submission." +complete -f -c exercism -n "__fish_use_subcommand" -a "open" -d "Opens a browser to exercism.io for the specified submission." complete -f -c exercism -n "__fish_seen_subcommand_from open" -s h -l help -d "help for open" # Submit -complete -f -c exercism -a "submit" -d "Submits a new iteration to a problem on exercism.io." +complete -f -c exercism -n "__fish_use_subcommand" -a "submit" -d "Submits a new iteration to a problem on exercism.io." complete -f -c exercism -n "__fish_seen_subcommand_from submit" -s h -l help -d "help for submit" # Troubleshoot -complete -f -c exercism -a "troubleshoot" -d "Outputs useful debug information." +complete -f -c exercism -n "__fish_use_subcommand" -a "troubleshoot" -d "Outputs useful debug information." complete -f -c exercism -n "__fish_seen_subcommand_from troubleshoot" -s f -l full-api-key -d "display full API key (censored by default)" complete -f -c exercism -n "__fish_seen_subcommand_from troubleshoot" -s h -l help -d "help for troubleshoot" # Upgrade -complete -f -c exercism -a "upgrade" -d "Upgrades to the latest available version." +complete -f -c exercism -n "__fish_use_subcommand" -a "upgrade" -d "Upgrades to the latest available version." complete -f -c exercism -n "__fish_seen_subcommand_from help" -s h -l help -d "help for help" # Version -complete -f -c exercism -a "version" -d "Outputs version information." +complete -f -c exercism -n "__fish_use_subcommand" -a "version" -d "Outputs version information." complete -f -c exercism -n "__fish_seen_subcommand_from version" -s l -l latest -d "check latest available version" complete -f -c exercism -n "__fish_seen_subcommand_from version" -s h -l help -d "help for version" # Workspace -complete -f -c exercism -a "workspace" -d "Outputs the root directory for Exercism exercises." +complete -f -c exercism -n "__fish_use_subcommand" -a "workspace" -d "Outputs the root directory for Exercism exercises." complete -f -c exercism -n "__fish_seen_subcommand_from workspace" -s h -l help -d "help for workspace" # Options From 1843bc035499ddcbcf939de5b35c59c88345527b Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 18 Nov 2018 12:31:42 -0700 Subject: [PATCH 299/544] Avoid 'core' terminology in team context The teams site does not have core exercises. This will only show the 'core' messaging for auto-approvable exercises on the main site. --- cmd/submit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/submit.go b/cmd/submit.go index 2c4a69450..f7152ef25 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -252,7 +252,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { %s ` suffix := "View it at:\n\n " - if metadata.AutoApprove { + if metadata.AutoApprove && metadata.Team == "" { suffix = "You can complete the exercise and unlock the next core exercise at:\n" } fmt.Fprintf(Err, msg, suffix) From 7d898f7e13e31f28d76120821c9c78dbb160e7e5 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 18 Nov 2018 13:43:04 -0700 Subject: [PATCH 300/544] Add compare link to release instructions This isn't necessary, just helpful as it can be hard to find it on the GitHub site. --- RELEASE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASE.md b/RELEASE.md index 874f40dee..eb5b09fa8 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -24,6 +24,9 @@ $ sudo GCO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 ./make.bash --no-clean Make sure all the recent changes are reflected in the "next release" section of the Changelog. Make this a separate commit from bumping the version. +You can view changes using the /compare/ view: +https://github.com/exercism/cli/compare/$PREVIOUS_RELEASE...master + ## Bump the version Edit the `Version` constant in `exercism/main.go`, and edit the Changelog. From 0ac0002c00d3cf6df592eb9cb88db2ab262798e5 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 18 Nov 2018 13:43:17 -0700 Subject: [PATCH 301/544] Add changelog for v3.0.11 release --- CHANGELOG.md | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aaf1c96df..b5c2350e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ## Next Release * **Your contribution here** +## v3.0.11 (2018-11-18) +* [#752](https://github.com/exercism/cli/pull/752) Improve error message on upgrade command - [@farisj] +* [#759](https://github.com/exercism/cli/pull/759) Update shell tab completion for bash and zsh - [@nywilken] +* [#762](https://github.com/exercism/cli/pull/762) Improve usage documentation - [@Smarticles101] +* [#766](https://github.com/exercism/cli/pull/766) Tweak messaging to work for teams edition - [@kytrinyx] + ## v3.0.10 (2018-10-03) * official release of v3.0.10-alpha.1 - [@nywilken] @@ -402,8 +408,12 @@ All changes by [@msgehard] * Implement login and logout * Build on Travis -[@alebaffa]: https://github.com/alebaffa [@AlexWheeler]: https://github.com/AlexWheeler +[@Dparker1990]: https://github.com/Dparker1990 +[@LegalizeAdulthood]: https://github.com/LegalizeAdulthood +[@Tonkpils]: https://github.com/Tonkpils +[@TrevorBramble]: https://github.com/TrevorBramble +[@alebaffa]: https://github.com/alebaffa [@ambroff]: https://github.com/ambroff [@andrewsardone]: https://github.com/andrewsardone [@anxiousmodernman]: https://github.com/anxiousmodernman @@ -414,7 +424,6 @@ All changes by [@msgehard] [@cookrn]: https://github.com/cookrn [@daveyarwood]: https://github.com/daveyarwood [@devonestes]: https://github.com/devonestes -[@Dparker1990]: https://github.com/Dparker1990 [@djquan]: https://github.com/djquan [@dmmulroy]: https://github.com/dmmulroy [@dpritchett]: https://github.com/dpritchett @@ -422,19 +431,19 @@ All changes by [@msgehard] [@ebautistabar]: https://github.com/ebautistabar [@elimisteve]: https://github.com/elimisteve [@ests]: https://github.com/ests +[@farisj]: https://github.com/farisj [@glebedel]: https://github.com/glebedel [@harimp]: https://github.com/harimp [@hjljo]: https://github.com/hjljo [@isbadawi]: https://github.com/isbadawi -[@jdsutherland]: https://github.com/jdsutherland [@jbaiter]: https://github.com/jbaiter +[@jdsutherland]: https://github.com/jdsutherland [@jgsqware]: https://github.com/jgsqware [@jish]: https://github.com/jish [@jppunnett]: https://github.com/jppunnett -[@kytrinyx]: https://github.com/kytrinyx [@katrinleinweber]: https://github.com/katrinleinweber +[@kytrinyx]: https://github.com/kytrinyx [@lcowell]: https://github.com/lcowell -[@LegalizeAdulthood]: https://github.com/LegalizeAdulthood [@manusajith]: https://github.com/manusajith [@morphatic]: https://github.com/morphatic [@mrageh]: https://github.com/mrageh @@ -452,7 +461,6 @@ All changes by [@msgehard] [@sfairchild]: https://github.com/sfairchild [@simonjefford]: https://github.com/simonjefford [@srt32]: https://github.com/srt32 -[@Tonkpils]: https://github.com/Tonkpils -[@TrevorBramble]: https://github.com/TrevorBramble [@williandrade]: https://github.com/williandrade [@zabawaba99]: https://github.com/zabawaba99 +[@Smarticles101]: https://github.com/Smarticles101 From 6067431dc195fd2e1050b95ae7aa597839d4622a Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 18 Nov 2018 13:43:35 -0700 Subject: [PATCH 302/544] Bump version to 3.0.11 --- cmd/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/version.go b/cmd/version.go index 5005821a7..970e46c06 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.0.10" +const Version = "3.0.11" // checkLatest flag for version command. var checkLatest bool From 4e3211233546a4d4bf374dfd6a2cd8d35dd9cef6 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 18 Nov 2018 13:46:12 -0700 Subject: [PATCH 303/544] Fix incorrect version instructions in release docs --- RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index eb5b09fa8..7e86ebd23 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -29,7 +29,7 @@ https://github.com/exercism/cli/compare/$PREVIOUS_RELEASE...master ## Bump the version -Edit the `Version` constant in `exercism/main.go`, and edit the Changelog. +Edit the `Version` constant in `cmd/version.go`, and edit the Changelog. All the changes in the "next release" section should be moved to a new section that describes the version number, and gives it a date. From c8f62652336a5356a0d750b41ffcc289b68d0466 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 18 Nov 2018 13:57:21 -0700 Subject: [PATCH 304/544] Tweak instructions for release notes on the GitHub release --- RELEASE.md | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 7e86ebd23..71aecd0a3 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -54,25 +54,15 @@ VERSION matches the value of the `Version` constant. Upload all the binaries from `release/*`. -Paste the release text and describe the new changes (`tail -n +57 RELEASE.md | head -n 16 | pbcopy`): +Paste the release text and describe the new changes. ``` -### Exercism Command-Line Interface (CLI) +To install, follow the interactive installation instructions at https://exercism.io/cli-walkthrough -Exercism takes place in two places: the discussions happen on the website, and you work on exercises locally. The CLI bridges the gap, allowing you to fetch exercises and submit solutions to the site. +--- -This is a stand-alone binary, which means that you don't need to install any particular language or environment in order to use it. +[describe changes in this release] -To install, download the archive that matches your operating system and architecture, unpack the archive, and put the binary somewhere on your path. - -You will need to configure the CLI with your [Exercism API Key](http://exercism.io/account/key) before submitting. - -For more detailed instructions, see the [CLI page on Exercism](http://exercism.io/cli). - -#### Recent changes - -* ABC... -* XYZ... ``` ## Update Homebrew From 251a261d792c0d02c5549ff620c781833c64b9a8 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 20 Nov 2018 11:22:51 +0700 Subject: [PATCH 305/544] Fix typo missed word ending --- cmd/configure_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 99e6472be..ffe6b4d54 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -82,7 +82,7 @@ func TestConfigureShow(t *testing.T) { assert.NotRegexp(t, "override.example", Err) assert.Regexp(t, "configured-token", Err) - assert.NotRegexp(t, "token-overrid", Err) + assert.NotRegexp(t, "token-override", Err) assert.Regexp(t, "configured-workspace", Err) assert.NotRegexp(t, "workspace-override", Err) From 0e00b893cd8a1084385f6249f399bd27a63750df Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 20 Nov 2018 11:24:20 +0700 Subject: [PATCH 306/544] Remove dead code with faked output --- cmd/configure_test.go | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/cmd/configure_test.go b/cmd/configure_test.go index ffe6b4d54..b5293d438 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -18,14 +18,6 @@ import ( ) func TestBareConfigure(t *testing.T) { - oldErr := Err - defer func() { - Err = oldErr - }() - - var buf bytes.Buffer - Err = &buf - flags := pflag.NewFlagSet("fake", pflag.PanicOnError) setupConfigureFlags(flags) @@ -150,17 +142,12 @@ func TestConfigureToken(t *testing.T) { defer ts.Close() oldOut := Out - oldErr := Err Out = ioutil.Discard defer func() { Out = oldOut - Err = oldErr }() for _, tc := range testCases { - var buf bytes.Buffer - Err = &buf - flags := pflag.NewFlagSet("fake", pflag.PanicOnError) setupConfigureFlags(flags) @@ -238,17 +225,12 @@ func TestConfigureAPIBaseURL(t *testing.T) { } oldOut := Out - oldErr := Err Out = ioutil.Discard defer func() { Out = oldOut - Err = oldErr }() for _, tc := range testCases { - var buf bytes.Buffer - Err = &buf - flags := pflag.NewFlagSet("fake", pflag.PanicOnError) setupConfigureFlags(flags) From dacfa650853d97154df08e231391b595b8d7d1ed Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 20 Nov 2018 11:37:04 +0700 Subject: [PATCH 307/544] Inline buffer init --- cmd/configure_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/configure_test.go b/cmd/configure_test.go index b5293d438..3bf1b2de4 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -41,8 +41,7 @@ func TestConfigureShow(t *testing.T) { Err = oldErr }() - var buf bytes.Buffer - Err = &buf + Err = &bytes.Buffer{} flags := pflag.NewFlagSet("fake", pflag.PanicOnError) setupConfigureFlags(flags) From 1aec7abb87b70c04664e81c7bb9333bff4722284 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Wed, 21 Nov 2018 13:40:48 +0700 Subject: [PATCH 308/544] Add test for migration output Previously printing referenced `os.Stderr` instead of `Err` as done elsewhere Having fixed this, it seems fit to verify migration printing output --- cmd/submit.go | 2 +- cmd/submit_test.go | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index f7152ef25..e5f8cfd01 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -134,7 +134,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } if verbose, _ := flags.GetBool("verbose"); verbose { - fmt.Fprintf(os.Stderr, migrationStatus.String()) + fmt.Fprintf(Err, migrationStatus.String()) } metadata, err := workspace.NewExerciseMetadata(exerciseDir) if err != nil { diff --git a/cmd/submit_test.go b/cmd/submit_test.go index b2e38d9b6..4027e2cba 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -1,6 +1,7 @@ package cmd import ( + "bytes" "encoding/json" "io/ioutil" "net/http" @@ -187,15 +188,12 @@ func TestSubmitFiles(t *testing.T) { } func TestLegacyMetadataMigration(t *testing.T) { - oldOut := Out oldErr := Err - Out = ioutil.Discard - Err = ioutil.Discard defer func() { - Out = oldOut Err = oldErr }() - // The fake endpoint will populate this when it receives the call from the command. + Err = &bytes.Buffer{} + submittedFiles := map[string]string{} ts := fakeSubmitServer(t, submittedFiles) defer ts.Close() @@ -239,7 +237,10 @@ func TestLegacyMetadataMigration(t *testing.T) { ok, _ = exercise.HasMetadata() assert.False(t, ok) - err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file}) + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + flags.Bool("verbose", true, "") + + err = runSubmit(cfg, flags, []string{file}) assert.NoError(t, err) assert.Equal(t, "This is a file.", submittedFiles["file.txt"]) @@ -247,6 +248,7 @@ func TestLegacyMetadataMigration(t *testing.T) { assert.False(t, ok) ok, _ = exercise.HasMetadata() assert.True(t, ok) + assert.Regexp(t, "Migrated metadata", Err) } func TestSubmitWithEmptyFile(t *testing.T) { From eb097f64b105b53cd457a3581434fedaecc957b6 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Sun, 9 Dec 2018 11:54:53 +0700 Subject: [PATCH 309/544] Fix test failure panics when err is nil (#776) Currently several tests use `assert.Regexp(.. err.Error())` where err is unchecked. If there is no error (it's nil) then the tests blow up and we don't get a useful failure. --- cmd/configure_test.go | 9 ++++++--- cmd/submit_test.go | 40 ++++++++++++++++++++++++++-------------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 3bf1b2de4..0140b9b68 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -32,7 +32,9 @@ func TestBareConfigure(t *testing.T) { } err = runConfigure(cfg, flags) - assert.Regexp(t, "no token configured", err.Error()) + if assert.Error(t, err) { + assert.Regexp(t, "no token configured", err.Error()) + } } func TestConfigureShow(t *testing.T) { @@ -379,8 +381,9 @@ func TestConfigureDefaultWorkspaceWithoutClobbering(t *testing.T) { assert.NoError(t, err) err = runConfigure(cfg, flags) - assert.Error(t, err) - assert.Regexp(t, "already something", err.Error()) + if assert.Error(t, err) { + assert.Regexp(t, "already something", err.Error()) + } } func TestConfigureExplicitWorkspaceWithoutClobberingNonDirectory(t *testing.T) { diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 4027e2cba..1fab4b243 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -24,8 +24,10 @@ func TestSubmitWithoutToken(t *testing.T) { } err := runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) - assert.Regexp(t, "Welcome to Exercism", err.Error()) - assert.Regexp(t, "exercism.io/my/settings", err.Error()) + if assert.Error(t, err) { + assert.Regexp(t, "Welcome to Exercism", err.Error()) + assert.Regexp(t, "exercism.io/my/settings", err.Error()) + } } func TestSubmitWithoutWorkspace(t *testing.T) { @@ -39,7 +41,9 @@ func TestSubmitWithoutWorkspace(t *testing.T) { } err := runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) - assert.Regexp(t, "re-run the configure", err.Error()) + if assert.Error(t, err) { + assert.Regexp(t, "re-run the configure", err.Error()) + } } func TestSubmitNonExistentFile(t *testing.T) { @@ -68,7 +72,9 @@ func TestSubmitNonExistentFile(t *testing.T) { filepath.Join(tmpDir, "file-2.txt"), } err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) - assert.Regexp(t, "cannot be found", err.Error()) + if assert.Error(t, err) { + assert.Regexp(t, "cannot be found", err.Error()) + } } func TestSubmitExerciseWithoutMetadataFile(t *testing.T) { @@ -94,8 +100,9 @@ func TestSubmitExerciseWithoutMetadataFile(t *testing.T) { } err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file}) - assert.Error(t, err) - assert.Regexp(t, "doesn't have the necessary metadata", err.Error()) + if assert.Error(t, err) { + assert.Regexp(t, "doesn't have the necessary metadata", err.Error()) + } } func TestSubmitFilesAndDir(t *testing.T) { @@ -124,8 +131,10 @@ func TestSubmitFilesAndDir(t *testing.T) { filepath.Join(tmpDir, "file-2.txt"), } err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) - assert.Regexp(t, "submitting a directory", err.Error()) - assert.Regexp(t, "Please change into the directory and provide the path to the file\\(s\\) you wish to submit", err.Error()) + if assert.Error(t, err) { + assert.Regexp(t, "submitting a directory", err.Error()) + assert.Regexp(t, "Please change into the directory and provide the path to the file\\(s\\) you wish to submit", err.Error()) + } } func TestSubmitFiles(t *testing.T) { @@ -339,8 +348,9 @@ func TestSubmitWithEnormousFile(t *testing.T) { err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file}) - assert.Error(t, err) - assert.Regexp(t, "Please reduce the size of the file and try again.", err.Error()) + if assert.Error(t, err) { + assert.Regexp(t, "Please reduce the size of the file and try again.", err.Error()) + } } func TestSubmitFilesForTeamExercise(t *testing.T) { @@ -426,8 +436,9 @@ func TestSubmitOnlyEmptyFile(t *testing.T) { err = ioutil.WriteFile(file, []byte(""), os.FileMode(0755)) err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file}) - assert.Error(t, err) - assert.Regexp(t, "No files found", err.Error()) + if assert.Error(t, err) { + assert.Regexp(t, "No files found", err.Error()) + } } func TestSubmitFilesFromDifferentSolutions(t *testing.T) { @@ -462,8 +473,9 @@ func TestSubmitFilesFromDifferentSolutions(t *testing.T) { } err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file1, file2}) - assert.Error(t, err) - assert.Regexp(t, "different solutions", err.Error()) + if assert.Error(t, err) { + assert.Regexp(t, "different solutions", err.Error()) + } } func fakeSubmitServer(t *testing.T, submittedFiles map[string]string) *httptest.Server { From 0500cf59094dafc1206c9e38bb5ce021eeb8625f Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Wed, 12 Dec 2018 08:33:29 +0700 Subject: [PATCH 310/544] Exercise dirname matches metadata exercise slug (#774) * Exercise dirname matches metadata exercise slug * Avoid panic when err nil on test failure * Make error message actionable --- cmd/submit.go | 14 ++++++++++++++ cmd/submit_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/cmd/submit.go b/cmd/submit.go index e5f8cfd01..d0a34bcef 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -141,6 +141,20 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } + if exercise.Slug != metadata.Exercise { + // TODO: error msg should suggest running future doctor command + msg := ` + + The exercise directory does not match exercise slug in metadata: + + expected '%[1]s' but got '%[2]s' + + Please rename the directory '%[1]s' to '%[2]s' and try again. + + ` + return fmt.Errorf(msg, exercise.Slug, metadata.Exercise) + } + if !metadata.IsRequester { // TODO: add test msg := ` diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 1fab4b243..ca74b2b06 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -548,6 +548,40 @@ func TestSubmitRelativePath(t *testing.T) { assert.Equal(t, "This is a file.", submittedFiles["file.txt"]) } +func TestExerciseDirnameMatchesMetadataSlug(t *testing.T) { + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := ioutil.TempDir("", "submit-files") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise-doesnt-match-metadata-slug") + os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") + + file1 := filepath.Join(dir, "file-1.txt") + err = ioutil.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + Dir: tmpDir, + UserViperConfig: v, + } + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file1}) + if assert.Error(t, err) { + assert.Regexp(t, "directory does not match exercise slug", err.Error()) + } +} + func writeFakeMetadata(t *testing.T, dir, trackID, exerciseSlug string) { metadata := &workspace.ExerciseMetadata{ ID: "bogus-solution-uuid", From c39243d0b777ca532841b555540cc20271b1991b Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Sat, 15 Dec 2018 23:01:09 +0700 Subject: [PATCH 311/544] Add missing test when metadata not requester (#777) --- cmd/submit.go | 1 - cmd/submit_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/cmd/submit.go b/cmd/submit.go index d0a34bcef..da2de03c3 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -156,7 +156,6 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { } if !metadata.IsRequester { - // TODO: add test msg := ` The solution you are submitting is not connected to your account. diff --git a/cmd/submit_test.go b/cmd/submit_test.go index ca74b2b06..5394bd182 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -548,6 +548,49 @@ func TestSubmitRelativePath(t *testing.T) { assert.Equal(t, "This is a file.", submittedFiles["file.txt"]) } +func TestSubmissionNotConnectedToRequesterAccount(t *testing.T) { + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := ioutil.TempDir("", "submit-files") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) + + metadata := &workspace.ExerciseMetadata{ + ID: "bogus-solution-uuid", + Track: "bogus-track", + Exercise: "bogus-exercise", + URL: "http://example.com/bogus-url", + IsRequester: false, + } + err = metadata.Write(dir) + assert.NoError(t, err) + + file1 := filepath.Join(dir, "file-1.txt") + err = ioutil.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + Dir: tmpDir, + UserViperConfig: v, + } + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file1}) + if assert.Error(t, err) { + assert.Regexp(t, "not connected to your account", err.Error()) + } +} + func TestExerciseDirnameMatchesMetadataSlug(t *testing.T) { submittedFiles := map[string]string{} ts := fakeSubmitServer(t, submittedFiles) From 2d2d9c626441451e1e6c5dac011618f643259535 Mon Sep 17 00:00:00 2001 From: John-Goff <33040621+John-Goff@users.noreply.github.com> Date: Wed, 19 Dec 2018 13:59:00 -0500 Subject: [PATCH 312/544] Shorten possible timeout ranges in tab completion --- shell/exercism.fish | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/exercism.fish b/shell/exercism.fish index 1b4821eba..f246ff4a6 100644 --- a/shell/exercism.fish +++ b/shell/exercism.fish @@ -45,5 +45,5 @@ complete -f -c exercism -n "__fish_seen_subcommand_from workspace" -s h -l help # Options complete -f -c exercism -s h -l help -d "show help" -complete -f -c exercism -l timeout -a "(seq 0 100 100000)" -d "override default HTTP timeout" +complete -f -c exercism -l timeout -a "(seq 0 1000 10000)" -d "override default HTTP timeout" complete -f -c exercism -s v -l verbose -d "turn on verbose logging" From 05096a3e323f89c2f4aefa62c88191ebe4cb4eed Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Wed, 12 Dec 2018 15:25:41 +0700 Subject: [PATCH 313/544] Extract common validate user config helper method This logic occurs in several of the commands. --- cmd/cmd.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/cmd/cmd.go b/cmd/cmd.go index f881a3344..b37c8a20e 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -1,5 +1,12 @@ package cmd +import ( + "fmt" + + "github.com/exercism/cli/config" + "github.com/spf13/viper" +) + const msgWelcomePleaseConfigure = ` Welcome to Exercism! @@ -33,3 +40,18 @@ const msgMissingMetadata = ` Please see https://exercism.io/cli-v1-to-v2 for instructions on how to fix it. ` + +// validateUserConfig validates the presense of required user config values +func validateUserConfig(cfg *viper.Viper) error { + if cfg.GetString("token") == "" { + return fmt.Errorf( + msgWelcomePleaseConfigure, + config.SettingsURL(cfg.GetString("apibaseurl")), + BinaryName, + ) + } + if cfg.GetString("workspace") == "" || cfg.GetString("apibaseurl") == "" { + return fmt.Errorf(msgRerunConfigure, BinaryName) + } + return nil +} From 33cd3c03ff869c154e16151ad241c2c9f194ef7a Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 26 Dec 2018 13:17:44 -0800 Subject: [PATCH 314/544] Ensure user config is complete in submit tests The submit command tests would fail spuriously if we validate that the api url is set (i.e. that the CLI is configured). --- cmd/submit_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 5394bd182..048428488 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -54,6 +54,7 @@ func TestSubmitNonExistentFile(t *testing.T) { v := viper.New() v.Set("token", "abc123") v.Set("workspace", tmpDir) + v.Set("apibaseurl", "http://api.example.com") cfg := config.Config{ Persister: config.InMemoryPersister{}, @@ -92,6 +93,7 @@ func TestSubmitExerciseWithoutMetadataFile(t *testing.T) { v := viper.New() v.Set("token", "abc123") v.Set("workspace", tmpDir) + v.Set("apibaseurl", "http://api.example.com") cfg := config.Config{ Persister: config.InMemoryPersister{}, @@ -113,6 +115,7 @@ func TestSubmitFilesAndDir(t *testing.T) { v := viper.New() v.Set("token", "abc123") v.Set("workspace", tmpDir) + v.Set("apibaseurl", "http://api.example.com") cfg := config.Config{ Persister: config.InMemoryPersister{}, @@ -426,6 +429,7 @@ func TestSubmitOnlyEmptyFile(t *testing.T) { v := viper.New() v.Set("token", "abc123") v.Set("workspace", tmpDir) + v.Set("apibaseurl", "http://api.example.com") cfg := config.Config{ Persister: config.InMemoryPersister{}, @@ -465,6 +469,7 @@ func TestSubmitFilesFromDifferentSolutions(t *testing.T) { v := viper.New() v.Set("token", "abc123") v.Set("workspace", tmpDir) + v.Set("apibaseurl", "http://api.example.com") cfg := config.Config{ Persister: config.InMemoryPersister{}, From 483206e0b7fdd50faec4ae8b5935291ab2c35dec Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 26 Dec 2018 13:19:16 -0800 Subject: [PATCH 315/544] Normalize user config validation in submit and download commands --- cmd/download.go | 7 ++----- cmd/submit.go | 8 ++------ 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index fd54052b2..832019e33 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -50,11 +50,8 @@ Download other people's solutions by providing the UUID. func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { usrCfg := cfg.UserViperConfig - if usrCfg.GetString("token") == "" { - return fmt.Errorf(msgWelcomePleaseConfigure, config.SettingsURL(usrCfg.GetString("apibaseurl")), BinaryName) - } - if usrCfg.GetString("workspace") == "" || usrCfg.GetString("apibaseurl") == "" { - return fmt.Errorf(msgRerunConfigure, BinaryName) + if err := validateUserConfig(usrCfg); err != nil { + return err } uuid, err := flags.GetString("uuid") diff --git a/cmd/submit.go b/cmd/submit.go index da2de03c3..4c3bf982b 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -51,12 +51,8 @@ var submitCmd = &cobra.Command{ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { usrCfg := cfg.UserViperConfig - if usrCfg.GetString("token") == "" { - return fmt.Errorf(msgWelcomePleaseConfigure, config.SettingsURL(usrCfg.GetString("apibaseurl")), BinaryName) - } - - if usrCfg.GetString("workspace") == "" { - return fmt.Errorf(msgRerunConfigure, BinaryName) + if err := validateUserConfig(usrCfg); err != nil { + return err } for i, arg := range args { From 099a54c76a6cdaadec807078c8c45c77bff8239f Mon Sep 17 00:00:00 2001 From: Stucki Date: Thu, 27 Dec 2018 14:49:31 -0600 Subject: [PATCH 316/544] Submit prints out api error messages --- cmd/submit.go | 18 ++++++++++++++++++ cmd/submit_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/cmd/submit.go b/cmd/submit.go index 4c3bf982b..83dca5a65 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -2,10 +2,12 @@ package cmd import ( "bytes" + "encoding/json" "errors" "fmt" "io" "mime/multipart" + "net/http" "os" "path/filepath" @@ -249,6 +251,15 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { } defer resp.Body.Close() + if resp.StatusCode == http.StatusBadRequest { + var jsonErrBody apiErrorMessage + if err := json.NewDecoder(resp.Body).Decode(&jsonErrBody); err != nil { + return fmt.Errorf("failed to parse error response - %s", err) + } + + return fmt.Errorf(jsonErrBody.Error.Message) + } + bb := &bytes.Buffer{} _, err = bb.ReadFrom(resp.Body) if err != nil { @@ -272,3 +283,10 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { func init() { RootCmd.AddCommand(submitCmd) } + +type apiErrorMessage struct { + Error struct { + Type string `json:"type"` + Message string `json:"message"` + } `json:"error,omitempty"` +} diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 048428488..9a890b2e1 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -3,6 +3,7 @@ package cmd import ( "bytes" "encoding/json" + "fmt" "io/ioutil" "net/http" "net/http/httptest" @@ -504,6 +505,8 @@ func fakeSubmitServer(t *testing.T, submittedFiles map[string]string) *httptest. } submittedFiles[fileHeader.Filename] = string(body) } + + fmt.Fprint(w, "{}") }) return httptest.NewServer(handler) } @@ -553,6 +556,46 @@ func TestSubmitRelativePath(t *testing.T) { assert.Equal(t, "This is a file.", submittedFiles["file.txt"]) } +func TestSubmitServerErr(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, `{"error": {"type": "error", "message": "test error"}}`) + }) + + ts := httptest.NewServer(handler) + defer ts.Close() + + tmpDir, err := ioutil.TempDir("", "submit-err-tmp-dir") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: "http://example.com", + } + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") + + err = ioutil.WriteFile(filepath.Join(dir, "file-1.txt"), []byte("This is file 1"), os.FileMode(0755)) + assert.NoError(t, err) + + files := []string{ + filepath.Join(dir, "file-1.txt"), + } + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) + + assert.Regexp(t, "test error", err.Error()) +} + func TestSubmissionNotConnectedToRequesterAccount(t *testing.T) { submittedFiles := map[string]string{} ts := fakeSubmitServer(t, submittedFiles) From 812ae3b1fd33549f4267d1fcf487be8efb2a1ca7 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 26 Dec 2018 12:02:27 -0800 Subject: [PATCH 317/544] Move shared package variables to cmd helper file The Root command is just a command (though admittedly a somewhat special one). The cmd.go file is where we're putting shared stuff. --- cmd/cmd.go | 17 +++++++++++++++++ cmd/root.go | 16 ---------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index b37c8a20e..d073aa1ff 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -3,10 +3,27 @@ package cmd import ( "fmt" + "io" + "github.com/exercism/cli/config" "github.com/spf13/viper" ) +var ( + // BinaryName is the name of the app. + // By default this is exercism, but people + // are free to name this however they want. + // The usage examples and help strings should reflect + // the actual name of the binary. + BinaryName string + // Out is used to write to information. + Out io.Writer + // Err is used to write errors. + Err io.Writer + // In is used to provide mocked test input (i.e. for prompts). + In io.Reader +) + const msgWelcomePleaseConfigure = ` Welcome to Exercism! diff --git a/cmd/root.go b/cmd/root.go index e2b09a423..d4cf12acb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "io" "os" "runtime" @@ -13,21 +12,6 @@ import ( "github.com/spf13/cobra" ) -var ( - // BinaryName is the name of the app. - // By default this is exercism, but people - // are free to name this however they want. - // The usage examples and help strings should reflect - // the actual name of the binary. - BinaryName string - // Out is used to write to information. - Out io.Writer - // Err is used to write errors. - Err io.Writer - // In is used to provide mocked test input (i.e. for prompts). - In io.Reader -) - // RootCmd represents the base command when called without any subcommands. var RootCmd = &cobra.Command{ Use: BinaryName, From 24da940e2e08bcace3c857e98a3e0f144916351e Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 26 Dec 2018 12:32:27 -0800 Subject: [PATCH 318/544] Get rid of obsolete package variable We no longer have any interactive commands. --- cmd/cmd.go | 2 -- cmd/root.go | 1 - 2 files changed, 3 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index d073aa1ff..e71679c1b 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -20,8 +20,6 @@ var ( Out io.Writer // Err is used to write errors. Err io.Writer - // In is used to provide mocked test input (i.e. for prompts). - In io.Reader ) const msgWelcomePleaseConfigure = ` diff --git a/cmd/root.go b/cmd/root.go index d4cf12acb..5014c1e30 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -43,7 +43,6 @@ func init() { config.SetDefaultDirName(BinaryName) Out = os.Stdout Err = os.Stderr - In = os.Stdin api.UserAgent = fmt.Sprintf("github.com/exercism/cli v%s (%s/%s)", Version, runtime.GOOS, runtime.GOARCH) RootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output") RootCmd.PersistentFlags().IntP("timeout", "", 0, "override the default HTTP timeout (seconds)") From b055290085529ce6c6c37abe201f8621b4a60770 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 26 Dec 2018 12:47:50 -0800 Subject: [PATCH 319/544] Create helper type to better override streams in command tests --- cmd/cmd_test.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index da7d1b2bc..76ed0634e 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -1,6 +1,7 @@ package cmd import ( + "io" "io/ioutil" "os" "testing" @@ -90,3 +91,30 @@ func (test *CommandTest) Teardown(t *testing.T) { t.Fatal(err) } } + +// capturedOutput lets us more easily redirect streams in the tests. +type capturedOutput struct { + oldOut, oldErr, newOut, newErr io.Writer +} + +// newCapturedOutput creates a new value to override the streams. +func newCapturedOutput() capturedOutput { + return capturedOutput{ + oldOut: Out, + oldErr: Err, + newOut: ioutil.Discard, + newErr: ioutil.Discard, + } +} + +// override sets the package variables to the fake streams. +func (co capturedOutput) override() { + Out = co.newOut + Err = co.newErr +} + +// reset puts back the original streams for the commands to write to. +func (co capturedOutput) reset() { + Out = co.oldOut + Err = co.oldErr +} From 133e403a4c48ab0610a5a317e7d76890d1fd7319 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 26 Dec 2018 12:58:36 -0800 Subject: [PATCH 320/544] Replace individual overrides for streams in tests The tests were not consistent in how they were overriding STDOUT and STDERR. This caused several of the tests to print output to the terminal during a test run if you ran only the tests for the cmd package. --- cmd/configure_test.go | 60 ++++++++++------------------- cmd/download_test.go | 11 ++---- cmd/submit_symlink_test.go | 11 ++---- cmd/submit_test.go | 78 ++++++++++++-------------------------- cmd/upgrade_test.go | 7 ++-- 5 files changed, 55 insertions(+), 112 deletions(-) diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 0140b9b68..8d3643522 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -38,12 +38,10 @@ func TestBareConfigure(t *testing.T) { } func TestConfigureShow(t *testing.T) { - oldErr := Err - defer func() { - Err = oldErr - }() - - Err = &bytes.Buffer{} + co := newCapturedOutput() + co.newErr = &bytes.Buffer{} + co.override() + defer co.reset() flags := pflag.NewFlagSet("fake", pflag.PanicOnError) setupConfigureFlags(flags) @@ -82,6 +80,10 @@ func TestConfigureShow(t *testing.T) { } func TestConfigureToken(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + testCases := []struct { desc string configured string @@ -142,12 +144,6 @@ func TestConfigureToken(t *testing.T) { ts := httptest.NewServer(endpoint) defer ts.Close() - oldOut := Out - Out = ioutil.Discard - defer func() { - Out = oldOut - }() - for _, tc := range testCases { flags := pflag.NewFlagSet("fake", pflag.PanicOnError) setupConfigureFlags(flags) @@ -173,6 +169,10 @@ func TestConfigureToken(t *testing.T) { } func TestConfigureAPIBaseURL(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + endpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/ping" { w.WriteHeader(http.StatusNotFound) @@ -225,12 +225,6 @@ func TestConfigureAPIBaseURL(t *testing.T) { }, } - oldOut := Out - Out = ioutil.Discard - defer func() { - Out = oldOut - }() - for _, tc := range testCases { flags := pflag.NewFlagSet("fake", pflag.PanicOnError) setupConfigureFlags(flags) @@ -256,11 +250,9 @@ func TestConfigureAPIBaseURL(t *testing.T) { } func TestConfigureWorkspace(t *testing.T) { - oldErr := Err - Err = ioutil.Discard - defer func() { - Err = oldErr - }() + co := newCapturedOutput() + co.override() + defer co.reset() testCases := []struct { desc string @@ -342,14 +334,9 @@ func TestConfigureWorkspace(t *testing.T) { } func TestConfigureDefaultWorkspaceWithoutClobbering(t *testing.T) { - oldOut := Out - oldErr := Err - Out = ioutil.Discard - Err = ioutil.Discard - defer func() { - Out = oldOut - Err = oldErr - }() + co := newCapturedOutput() + co.override() + defer co.reset() // Stub server to always be 200 OK endpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) @@ -387,14 +374,9 @@ func TestConfigureDefaultWorkspaceWithoutClobbering(t *testing.T) { } func TestConfigureExplicitWorkspaceWithoutClobberingNonDirectory(t *testing.T) { - oldOut := Out - oldErr := Err - Out = ioutil.Discard - Err = ioutil.Discard - defer func() { - Out = oldOut - Err = oldErr - }() + co := newCapturedOutput() + co.override() + defer co.reset() tmpDir, err := ioutil.TempDir("", "no-clobber") defer os.RemoveAll(tmpDir) diff --git a/cmd/download_test.go b/cmd/download_test.go index 0f08da5b9..324f8733d 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -78,14 +78,9 @@ func TestDownloadWithoutFlags(t *testing.T) { } func TestDownload(t *testing.T) { - oldOut := Out - oldErr := Err - Out = ioutil.Discard - Err = ioutil.Discard - defer func() { - Out = oldOut - Err = oldErr - }() + co := newCapturedOutput() + co.override() + defer co.reset() testCases := []struct { requester bool diff --git a/cmd/submit_symlink_test.go b/cmd/submit_symlink_test.go index 8d193cd6c..b2d30ac4c 100644 --- a/cmd/submit_symlink_test.go +++ b/cmd/submit_symlink_test.go @@ -15,14 +15,9 @@ import ( ) func TestSubmitFilesInSymlinkedPath(t *testing.T) { - oldOut := Out - oldErr := Err - Out = ioutil.Discard - Err = ioutil.Discard - defer func() { - Out = oldOut - Err = oldErr - }() + co := newCapturedOutput() + co.override() + defer co.reset() // The fake endpoint will populate this when it receives the call from the command. submittedFiles := map[string]string{} diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 9a890b2e1..06e7caf55 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -142,14 +142,10 @@ func TestSubmitFilesAndDir(t *testing.T) { } func TestSubmitFiles(t *testing.T) { - oldOut := Out - oldErr := Err - Out = ioutil.Discard - Err = ioutil.Discard - defer func() { - Out = oldOut - Err = oldErr - }() + co := newCapturedOutput() + co.override() + defer co.reset() + // The fake endpoint will populate this when it receives the call from the command. submittedFiles := map[string]string{} ts := fakeSubmitServer(t, submittedFiles) @@ -201,11 +197,10 @@ func TestSubmitFiles(t *testing.T) { } func TestLegacyMetadataMigration(t *testing.T) { - oldErr := Err - defer func() { - Err = oldErr - }() - Err = &bytes.Buffer{} + co := newCapturedOutput() + co.newErr = &bytes.Buffer{} + co.override() + defer co.reset() submittedFiles := map[string]string{} ts := fakeSubmitServer(t, submittedFiles) @@ -265,14 +260,9 @@ func TestLegacyMetadataMigration(t *testing.T) { } func TestSubmitWithEmptyFile(t *testing.T) { - oldOut := Out - oldErr := Err - Out = ioutil.Discard - Err = ioutil.Discard - defer func() { - Out = oldOut - Err = oldErr - }() + co := newCapturedOutput() + co.override() + defer co.reset() // The fake endpoint will populate this when it receives the call from the command. submittedFiles := map[string]string{} @@ -311,14 +301,9 @@ func TestSubmitWithEmptyFile(t *testing.T) { } func TestSubmitWithEnormousFile(t *testing.T) { - oldOut := Out - oldErr := Err - Out = ioutil.Discard - Err = ioutil.Discard - defer func() { - Out = oldOut - Err = oldErr - }() + co := newCapturedOutput() + co.override() + defer co.reset() // The fake endpoint will populate this when it receives the call from the command. submittedFiles := map[string]string{} @@ -358,14 +343,10 @@ func TestSubmitWithEnormousFile(t *testing.T) { } func TestSubmitFilesForTeamExercise(t *testing.T) { - oldOut := Out - oldErr := Err - Out = ioutil.Discard - Err = ioutil.Discard - defer func() { - Out = oldOut - Err = oldErr - }() + co := newCapturedOutput() + co.override() + defer co.reset() + // The fake endpoint will populate this when it receives the call from the command. submittedFiles := map[string]string{} ts := fakeSubmitServer(t, submittedFiles) @@ -409,14 +390,9 @@ func TestSubmitFilesForTeamExercise(t *testing.T) { } func TestSubmitOnlyEmptyFile(t *testing.T) { - oldOut := Out - oldErr := Err - Out = ioutil.Discard - Err = ioutil.Discard - defer func() { - Out = oldOut - Err = oldErr - }() + co := newCapturedOutput() + co.override() + defer co.reset() tmpDir, err := ioutil.TempDir("", "just-an-empty-file") defer os.RemoveAll(tmpDir) @@ -512,14 +488,10 @@ func fakeSubmitServer(t *testing.T, submittedFiles map[string]string) *httptest. } func TestSubmitRelativePath(t *testing.T) { - oldOut := Out - oldErr := Err - Out = ioutil.Discard - Err = ioutil.Discard - defer func() { - Out = oldOut - Err = oldErr - }() + co := newCapturedOutput() + co.override() + defer co.reset() + // The fake endpoint will populate this when it receives the call from the command. submittedFiles := map[string]string{} ts := fakeSubmitServer(t, submittedFiles) diff --git a/cmd/upgrade_test.go b/cmd/upgrade_test.go index e2fd09496..b2489ea83 100644 --- a/cmd/upgrade_test.go +++ b/cmd/upgrade_test.go @@ -1,7 +1,6 @@ package cmd import ( - "io/ioutil" "testing" "github.com/stretchr/testify/assert" @@ -22,9 +21,9 @@ func (fc *fakeCLI) Upgrade() error { } func TestUpgrade(t *testing.T) { - oldOut := Out - Out = ioutil.Discard - defer func() { Out = oldOut }() + co := newCapturedOutput() + co.override() + defer co.reset() testCases := []struct { desc string From 61d0998632c2d599b8ef4bae2f9b4297a3859fcb Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Sat, 8 Dec 2018 13:38:32 +0700 Subject: [PATCH 321/544] Extract methods of submit cmd These changes attempt to make submit easier to work with by composing methods for submit. --- cmd/submit.go | 211 +++++++++++++++++++++++++++++++------------------- 1 file changed, 131 insertions(+), 80 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 83dca5a65..79bc1cadd 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -57,6 +57,66 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } + if err := sanitizeArgs(args); err != nil { + return err + } + + ws, err := workspace.New(usrCfg.GetString("workspace")) + if err != nil { + return err + } + + exerciseDir, err := findExerciseDir(ws, args) + if err != nil { + return err + } + + exercise := workspace.NewExerciseFromDir(exerciseDir) + + migrationStatus, err := exercise.MigrateLegacyMetadataFile() + if err != nil { + return err + } + if verbose, _ := flags.GetBool("verbose"); verbose { + fmt.Fprintf(Err, migrationStatus.String()) + } + + metadata, err := metadata(exerciseDir) + if err != nil { + return err + } + + exercise.Documents, err = documents(exercise, args) + if err != nil { + return err + } + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + if err := writeFormFiles(writer, exercise); err != nil { + return err + } + + if err := submitRequest(usrCfg, metadata, writer, body); err != nil { + return err + } + + msg := ` + + Your solution has been submitted successfully. + %s +` + suffix := "View it at:\n\n " + if metadata.AutoApprove && metadata.Team == "" { + suffix = "You can complete the exercise and unlock the next core exercise at:\n" + } + fmt.Fprintf(Err, msg, suffix) + fmt.Fprintf(Out, " %s\n\n", metadata.URL) + return nil +} + +func sanitizeArgs(args []string) error { for i, arg := range args { var err error arg, err = filepath.Abs(arg) @@ -69,9 +129,9 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { if os.IsNotExist(err) { msg := ` - The file you are trying to submit cannot be found. + The file you are trying to submit cannot be found. - %s + %s ` return fmt.Errorf(msg, arg) @@ -81,13 +141,13 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { if info.IsDir() { msg := ` - You are submitting a directory, which is not currently supported. + You are submitting a directory, which is not currently supported. - %s + %s - Please change into the directory and provide the path to the file(s) you wish to submit + Please change into the directory and provide the path to the file(s) you wish to submit - %s submit FILENAME + %s submit FILENAME ` return fmt.Errorf(msg, arg, BinaryName) @@ -99,78 +159,40 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { } args[i] = src } + return nil +} - ws, err := workspace.New(usrCfg.GetString("workspace")) - if err != nil { - return err - } - +func findExerciseDir(ws workspace.Workspace, args []string) (string, error) { var exerciseDir string for _, arg := range args { dir, err := ws.ExerciseDir(arg) if err != nil { if workspace.IsMissingMetadata(err) { - return errors.New(msgMissingMetadata) + return "", errors.New(msgMissingMetadata) } - return err + return "", err } if exerciseDir != "" && dir != exerciseDir { msg := ` - You are submitting files belonging to different solutions. - Please submit the files for one solution at a time. + You are submitting files belonging to different solutions. + Please submit the files for one solution at a time. ` - return errors.New(msg) + return "", errors.New(msg) } exerciseDir = dir } + return exerciseDir, nil +} - exercise := workspace.NewExerciseFromDir(exerciseDir) - migrationStatus, err := exercise.MigrateLegacyMetadataFile() - if err != nil { - return err - } - if verbose, _ := flags.GetBool("verbose"); verbose { - fmt.Fprintf(Err, migrationStatus.String()) - } - metadata, err := workspace.NewExerciseMetadata(exerciseDir) - if err != nil { - return err - } - - if exercise.Slug != metadata.Exercise { - // TODO: error msg should suggest running future doctor command - msg := ` - - The exercise directory does not match exercise slug in metadata: - - expected '%[1]s' but got '%[2]s' - - Please rename the directory '%[1]s' to '%[2]s' and try again. - - ` - return fmt.Errorf(msg, exercise.Slug, metadata.Exercise) - } - - if !metadata.IsRequester { - msg := ` - - The solution you are submitting is not connected to your account. - Please re-download the exercise to make sure it has the data it needs. - - %s download --exercise=%s --track=%s - - ` - return fmt.Errorf(msg, BinaryName, metadata.Exercise, metadata.Track) - } - - exercise.Documents = make([]workspace.Document, 0, len(args)) +func documents(exercise workspace.Exercise, args []string) ([]workspace.Document, error) { + docs := make([]workspace.Document, 0, len(args)) for _, file := range args { // Don't submit empty files info, err := os.Stat(file) if err != nil { - return err + return nil, err } const maxFileSize int64 = 65535 if info.Size() >= maxFileSize { @@ -180,7 +202,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { Please reduce the size of the file and try again. ` - return fmt.Errorf(msg, file, maxFileSize) + return nil, fmt.Errorf(msg, file, maxFileSize) } if info.Size() == 0 { @@ -195,23 +217,58 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { } doc, err := workspace.NewDocument(exercise.Filepath(), file) if err != nil { - return err + return nil, err } - exercise.Documents = append(exercise.Documents, doc) + docs = append(docs, doc) } + if len(docs) == 0 { + msg := ` + + No files found to submit. + + ` + return nil, errors.New(msg) + } + return docs, nil +} - if len(exercise.Documents) == 0 { +func metadata(exerciseDir string) (*workspace.ExerciseMetadata, error) { + metadata, err := workspace.NewExerciseMetadata(exerciseDir) + if err != nil { + return nil, err + } + + exercise := workspace.NewExerciseFromDir(exerciseDir) + if exercise.Slug != metadata.Exercise { + // TODO: error msg should suggest running future doctor command msg := ` - No files found to submit. + The exercise directory does not match exercise slug in metadata: + + expected '%[1]s' but got '%[2]s' + + Please rename the directory '%[1]s' to '%[2]s' and try again. ` - return errors.New(msg) + return nil, fmt.Errorf(msg, exercise.Slug, metadata.Exercise) } - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) + if !metadata.IsRequester { + // TODO: add test + msg := ` + + The solution you are submitting is not connected to your account. + Please re-download the exercise to make sure it has the data it needs. + + %s download --exercise=%s --track=%s + + ` + return nil, fmt.Errorf(msg, BinaryName, metadata.Exercise, metadata.Track) + } + return metadata, nil +} +func writeFormFiles(writer *multipart.Writer, exercise workspace.Exercise) error { for _, doc := range exercise.Documents { file, err := os.Open(doc.Filepath()) if err != nil { @@ -228,12 +285,18 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } } - - err = writer.Close() - if err != nil { + if err := writer.Close(); err != nil { return err } + return nil +} +func submitRequest( + usrCfg *viper.Viper, + metadata *workspace.ExerciseMetadata, + writer *multipart.Writer, + body *bytes.Buffer, +) error { client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl")) if err != nil { return err @@ -265,18 +328,6 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { if err != nil { return err } - - msg := ` - - Your solution has been submitted successfully. - %s -` - suffix := "View it at:\n\n " - if metadata.AutoApprove && metadata.Team == "" { - suffix = "You can complete the exercise and unlock the next core exercise at:\n" - } - fmt.Fprintf(Err, msg, suffix) - fmt.Fprintf(Out, " %s\n\n", metadata.URL) return nil } From 09a33491dd685dafc82efe3394ef289f0186f61d Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Sat, 8 Dec 2018 16:09:20 +0700 Subject: [PATCH 322/544] Refactor functions to methods `submitContext` isn't doing much but it avoids namespace conflicts for methods like `metadata` --- cmd/submit.go | 125 +++++++++++++++++++++++++------------------------- 1 file changed, 63 insertions(+), 62 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 79bc1cadd..c690e957e 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -50,23 +50,29 @@ var submitCmd = &cobra.Command{ }, } +type submitContext struct { + args []string + usrCfg *viper.Viper +} + func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { usrCfg := cfg.UserViperConfig if err := validateUserConfig(usrCfg); err != nil { return err } - - if err := sanitizeArgs(args); err != nil { + ws, err := workspace.New(usrCfg.GetString("workspace")) + if err != nil { return err } - ws, err := workspace.New(usrCfg.GetString("workspace")) - if err != nil { + ctx := &submitContext{args: args, usrCfg: usrCfg} + + if err := ctx.sanitizeArgs(); err != nil { return err } - exerciseDir, err := findExerciseDir(ws, args) + exerciseDir, err := ctx.exerciseDir(ws) if err != nil { return err } @@ -81,12 +87,12 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { fmt.Fprintf(Err, migrationStatus.String()) } - metadata, err := metadata(exerciseDir) + metadata, err := ctx.metadata(exerciseDir) if err != nil { return err } - exercise.Documents, err = documents(exercise, args) + exercise.Documents, err = ctx.documents(exercise) if err != nil { return err } @@ -94,11 +100,11 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { body := &bytes.Buffer{} writer := multipart.NewWriter(body) - if err := writeFormFiles(writer, exercise); err != nil { + if err := ctx.writeFormFiles(writer, exercise); err != nil { return err } - if err := submitRequest(usrCfg, metadata, writer, body); err != nil { + if err := ctx.submitRequest(metadata, writer, body); err != nil { return err } @@ -116,8 +122,8 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return nil } -func sanitizeArgs(args []string) error { - for i, arg := range args { +func (ctx *submitContext) sanitizeArgs() error { + for i, arg := range ctx.args { var err error arg, err = filepath.Abs(arg) if err != nil { @@ -157,14 +163,14 @@ func sanitizeArgs(args []string) error { if err != nil { return err } - args[i] = src + ctx.args[i] = src } return nil } -func findExerciseDir(ws workspace.Workspace, args []string) (string, error) { +func (ctx *submitContext) exerciseDir(ws workspace.Workspace) (string, error) { var exerciseDir string - for _, arg := range args { + for _, arg := range ctx.args { dir, err := ws.ExerciseDir(arg) if err != nil { if workspace.IsMissingMetadata(err) { @@ -186,9 +192,45 @@ func findExerciseDir(ws workspace.Workspace, args []string) (string, error) { return exerciseDir, nil } -func documents(exercise workspace.Exercise, args []string) ([]workspace.Document, error) { - docs := make([]workspace.Document, 0, len(args)) - for _, file := range args { +func (ctx *submitContext) metadata(exerciseDir string) (*workspace.ExerciseMetadata, error) { + metadata, err := workspace.NewExerciseMetadata(exerciseDir) + if err != nil { + return nil, err + } + + exercise := workspace.NewExerciseFromDir(exerciseDir) + if exercise.Slug != metadata.Exercise { + // TODO: error msg should suggest running future doctor command + msg := ` + + The exercise directory does not match exercise slug in metadata: + + expected '%[1]s' but got '%[2]s' + + Please rename the directory '%[1]s' to '%[2]s' and try again. + + ` + return nil, fmt.Errorf(msg, exercise.Slug, metadata.Exercise) + } + + if !metadata.IsRequester { + // TODO: add test + msg := ` + + The solution you are submitting is not connected to your account. + Please re-download the exercise to make sure it has the data it needs. + + %s download --exercise=%s --track=%s + + ` + return nil, fmt.Errorf(msg, BinaryName, metadata.Exercise, metadata.Track) + } + return metadata, nil +} + +func (ctx *submitContext) documents(exercise workspace.Exercise) ([]workspace.Document, error) { + docs := make([]workspace.Document, 0, len(ctx.args)) + for _, file := range ctx.args { // Don't submit empty files info, err := os.Stat(file) if err != nil { @@ -232,43 +274,7 @@ func documents(exercise workspace.Exercise, args []string) ([]workspace.Document return docs, nil } -func metadata(exerciseDir string) (*workspace.ExerciseMetadata, error) { - metadata, err := workspace.NewExerciseMetadata(exerciseDir) - if err != nil { - return nil, err - } - - exercise := workspace.NewExerciseFromDir(exerciseDir) - if exercise.Slug != metadata.Exercise { - // TODO: error msg should suggest running future doctor command - msg := ` - - The exercise directory does not match exercise slug in metadata: - - expected '%[1]s' but got '%[2]s' - - Please rename the directory '%[1]s' to '%[2]s' and try again. - - ` - return nil, fmt.Errorf(msg, exercise.Slug, metadata.Exercise) - } - - if !metadata.IsRequester { - // TODO: add test - msg := ` - - The solution you are submitting is not connected to your account. - Please re-download the exercise to make sure it has the data it needs. - - %s download --exercise=%s --track=%s - - ` - return nil, fmt.Errorf(msg, BinaryName, metadata.Exercise, metadata.Track) - } - return metadata, nil -} - -func writeFormFiles(writer *multipart.Writer, exercise workspace.Exercise) error { +func (ctx *submitContext) writeFormFiles(writer *multipart.Writer, exercise workspace.Exercise) error { for _, doc := range exercise.Documents { file, err := os.Open(doc.Filepath()) if err != nil { @@ -291,17 +297,12 @@ func writeFormFiles(writer *multipart.Writer, exercise workspace.Exercise) error return nil } -func submitRequest( - usrCfg *viper.Viper, - metadata *workspace.ExerciseMetadata, - writer *multipart.Writer, - body *bytes.Buffer, -) error { - client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl")) +func (ctx *submitContext) submitRequest(metadata *workspace.ExerciseMetadata, writer *multipart.Writer, body *bytes.Buffer) error { + client, err := api.NewClient(ctx.usrCfg.GetString("token"), ctx.usrCfg.GetString("apibaseurl")) if err != nil { return err } - url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), metadata.ID) + url := fmt.Sprintf("%s/solutions/%s", ctx.usrCfg.GetString("apibaseurl"), metadata.ID) req, err := client.NewRequest("PATCH", url, body) if err != nil { return err From 78add7246c10207040eceead2dcc6dcb3ae433f2 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Sat, 8 Dec 2018 17:17:18 +0700 Subject: [PATCH 323/544] Replace param with query --- cmd/submit.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index c690e957e..cc6f84c13 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -61,10 +61,6 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { if err := validateUserConfig(usrCfg); err != nil { return err } - ws, err := workspace.New(usrCfg.GetString("workspace")) - if err != nil { - return err - } ctx := &submitContext{args: args, usrCfg: usrCfg} @@ -72,7 +68,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - exerciseDir, err := ctx.exerciseDir(ws) + exerciseDir, err := ctx.exerciseDir() if err != nil { return err } @@ -168,7 +164,12 @@ func (ctx *submitContext) sanitizeArgs() error { return nil } -func (ctx *submitContext) exerciseDir(ws workspace.Workspace) (string, error) { +func (ctx *submitContext) exerciseDir() (string, error) { + ws, err := workspace.New(ctx.usrCfg.GetString("workspace")) + if err != nil { + return "", err + } + var exerciseDir string for _, arg := range ctx.args { dir, err := ws.ExerciseDir(arg) From 0051d7463b45442fbe5549a38914cfd7c6c4d2c5 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Sat, 8 Dec 2018 18:21:59 +0700 Subject: [PATCH 324/544] Prefer field params rather than entire object --- cmd/submit.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index cc6f84c13..fbd65b247 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -96,11 +96,11 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { body := &bytes.Buffer{} writer := multipart.NewWriter(body) - if err := ctx.writeFormFiles(writer, exercise); err != nil { + if err := ctx.writeFormFiles(writer, exercise.Documents); err != nil { return err } - if err := ctx.submitRequest(metadata, writer, body); err != nil { + if err := ctx.submitRequest(metadata.ID, writer, body); err != nil { return err } @@ -275,8 +275,8 @@ func (ctx *submitContext) documents(exercise workspace.Exercise) ([]workspace.Do return docs, nil } -func (ctx *submitContext) writeFormFiles(writer *multipart.Writer, exercise workspace.Exercise) error { - for _, doc := range exercise.Documents { +func (ctx *submitContext) writeFormFiles(writer *multipart.Writer, docs []workspace.Document) error { + for _, doc := range docs { file, err := os.Open(doc.Filepath()) if err != nil { return err @@ -298,12 +298,12 @@ func (ctx *submitContext) writeFormFiles(writer *multipart.Writer, exercise work return nil } -func (ctx *submitContext) submitRequest(metadata *workspace.ExerciseMetadata, writer *multipart.Writer, body *bytes.Buffer) error { +func (ctx *submitContext) submitRequest(id string, writer *multipart.Writer, body *bytes.Buffer) error { client, err := api.NewClient(ctx.usrCfg.GetString("token"), ctx.usrCfg.GetString("apibaseurl")) if err != nil { return err } - url := fmt.Sprintf("%s/solutions/%s", ctx.usrCfg.GetString("apibaseurl"), metadata.ID) + url := fmt.Sprintf("%s/solutions/%s", ctx.usrCfg.GetString("apibaseurl"), id) req, err := client.NewRequest("PATCH", url, body) if err != nil { return err From e6f67efa4615e180e41d8f9f8b39bb16ff0d952b Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Sat, 8 Dec 2018 18:41:03 +0700 Subject: [PATCH 325/544] Simplify documents param Already have the exercise dir --- cmd/submit.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index fbd65b247..1ac7f4bb5 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -88,7 +88,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - exercise.Documents, err = ctx.documents(exercise) + exercise.Documents, err = ctx.documents(exerciseDir) if err != nil { return err } @@ -229,7 +229,7 @@ func (ctx *submitContext) metadata(exerciseDir string) (*workspace.ExerciseMetad return metadata, nil } -func (ctx *submitContext) documents(exercise workspace.Exercise) ([]workspace.Document, error) { +func (ctx *submitContext) documents(exerciseDir string) ([]workspace.Document, error) { docs := make([]workspace.Document, 0, len(ctx.args)) for _, file := range ctx.args { // Don't submit empty files @@ -258,7 +258,7 @@ func (ctx *submitContext) documents(exercise workspace.Exercise) ([]workspace.Do fmt.Fprintf(Err, msg, file) continue } - doc, err := workspace.NewDocument(exercise.Filepath(), file) + doc, err := workspace.NewDocument(exerciseDir, file) if err != nil { return nil, err } From 9e94e35cf661219f8ac4207ed74378a359084f87 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Sat, 8 Dec 2018 19:21:16 +0700 Subject: [PATCH 326/544] Add error checking to critical params --- cmd/submit.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cmd/submit.go b/cmd/submit.go index 1ac7f4bb5..e20ee6131 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -276,6 +276,13 @@ func (ctx *submitContext) documents(exerciseDir string) ([]workspace.Document, e } func (ctx *submitContext) writeFormFiles(writer *multipart.Writer, docs []workspace.Document) error { + if writer == nil { + return errors.New("writer is empty") + } + if len(docs) == 0 { + return errors.New("docs is empty") + } + for _, doc := range docs { file, err := os.Open(doc.Filepath()) if err != nil { @@ -299,6 +306,13 @@ func (ctx *submitContext) writeFormFiles(writer *multipart.Writer, docs []worksp } func (ctx *submitContext) submitRequest(id string, writer *multipart.Writer, body *bytes.Buffer) error { + if writer == nil { + return errors.New("writer is empty") + } + if body.Len() == 0 { + return errors.New("body is empty") + } + client, err := api.NewClient(ctx.usrCfg.GetString("token"), ctx.usrCfg.GetString("apibaseurl")) if err != nil { return err From 4a392b838557f7d363a4a47bd57fa8b7d6335182 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Sun, 9 Dec 2018 06:51:53 +0700 Subject: [PATCH 327/544] Merge related methods Previously, `submitRequest` depended directly on state of writer from `writeFormFiles`. These methods are related enough that they can be merged, avoiding potential maintenance problems and resulting in a more simple API. --- cmd/submit.go | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index e20ee6131..969ed0900 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -93,14 +93,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - if err := ctx.writeFormFiles(writer, exercise.Documents); err != nil { - return err - } - - if err := ctx.submitRequest(metadata.ID, writer, body); err != nil { + if err := ctx.submitRequest(metadata.ID, exercise.Documents); err != nil { return err } @@ -275,14 +268,14 @@ func (ctx *submitContext) documents(exerciseDir string) ([]workspace.Document, e return docs, nil } -func (ctx *submitContext) writeFormFiles(writer *multipart.Writer, docs []workspace.Document) error { - if writer == nil { - return errors.New("writer is empty") - } +func (ctx *submitContext) submitRequest(id string, docs []workspace.Document) error { if len(docs) == 0 { return errors.New("docs is empty") } + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + for _, doc := range docs { file, err := os.Open(doc.Filepath()) if err != nil { @@ -302,16 +295,6 @@ func (ctx *submitContext) writeFormFiles(writer *multipart.Writer, docs []worksp if err := writer.Close(); err != nil { return err } - return nil -} - -func (ctx *submitContext) submitRequest(id string, writer *multipart.Writer, body *bytes.Buffer) error { - if writer == nil { - return errors.New("writer is empty") - } - if body.Len() == 0 { - return errors.New("body is empty") - } client, err := api.NewClient(ctx.usrCfg.GetString("token"), ctx.usrCfg.GetString("apibaseurl")) if err != nil { From 0dc46fb538bd904189d39655f02edafcfbc88846 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 10 Dec 2018 06:51:17 +0700 Subject: [PATCH 328/544] Move to exercise method --- cmd/submit.go | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 969ed0900..dfcb0ad30 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -68,13 +68,11 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - exerciseDir, err := ctx.exerciseDir() + exercise, err := ctx.exercise() if err != nil { return err } - exercise := workspace.NewExerciseFromDir(exerciseDir) - migrationStatus, err := exercise.MigrateLegacyMetadataFile() if err != nil { return err @@ -83,12 +81,12 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { fmt.Fprintf(Err, migrationStatus.String()) } - metadata, err := ctx.metadata(exerciseDir) + metadata, err := ctx.metadata(exercise.Filepath()) if err != nil { return err } - exercise.Documents, err = ctx.documents(exerciseDir) + exercise.Documents, err = ctx.documents(exercise.Filepath()) if err != nil { return err } @@ -157,10 +155,10 @@ func (ctx *submitContext) sanitizeArgs() error { return nil } -func (ctx *submitContext) exerciseDir() (string, error) { +func (ctx *submitContext) exercise() (workspace.Exercise, error) { ws, err := workspace.New(ctx.usrCfg.GetString("workspace")) if err != nil { - return "", err + return workspace.Exercise{}, err } var exerciseDir string @@ -168,9 +166,9 @@ func (ctx *submitContext) exerciseDir() (string, error) { dir, err := ws.ExerciseDir(arg) if err != nil { if workspace.IsMissingMetadata(err) { - return "", errors.New(msgMissingMetadata) + return workspace.Exercise{}, errors.New(msgMissingMetadata) } - return "", err + return workspace.Exercise{}, err } if exerciseDir != "" && dir != exerciseDir { msg := ` @@ -179,11 +177,12 @@ func (ctx *submitContext) exerciseDir() (string, error) { Please submit the files for one solution at a time. ` - return "", errors.New(msg) + return workspace.Exercise{}, errors.New(msg) } exerciseDir = dir } - return exerciseDir, nil + + return workspace.NewExerciseFromDir(exerciseDir), nil } func (ctx *submitContext) metadata(exerciseDir string) (*workspace.ExerciseMetadata, error) { From 021c0b3b90916b06cf07c0ec03e7127a9f1159ea Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 10 Dec 2018 12:02:13 +0700 Subject: [PATCH 329/544] Replace string literals tabs w spaces Previously string literals use spaces instead of tabs. My editor seems to default them to tabs in the process of refactoring. These changes make them consistent. --- cmd/submit.go | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index dfcb0ad30..d9e5c6056 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -26,7 +26,7 @@ var submitCmd = &cobra.Command{ Short: "Submit your solution to an exercise.", Long: `Submit your solution to an Exercism exercise. - Call the command with the list of files you want to submit. + Call the command with the list of files you want to submit. `, RunE: func(cmd *cobra.Command, args []string) error { cfg := config.NewConfig() @@ -97,8 +97,8 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { msg := ` - Your solution has been submitted successfully. - %s + Your solution has been submitted successfully. + %s ` suffix := "View it at:\n\n " if metadata.AutoApprove && metadata.Team == "" { @@ -122,11 +122,11 @@ func (ctx *submitContext) sanitizeArgs() error { if os.IsNotExist(err) { msg := ` - The file you are trying to submit cannot be found. + The file you are trying to submit cannot be found. - %s + %s - ` + ` return fmt.Errorf(msg, arg) } return err @@ -134,15 +134,15 @@ func (ctx *submitContext) sanitizeArgs() error { if info.IsDir() { msg := ` - You are submitting a directory, which is not currently supported. + You are submitting a directory, which is not currently supported. - %s + %s - Please change into the directory and provide the path to the file(s) you wish to submit + Please change into the directory and provide the path to the file(s) you wish to submit - %s submit FILENAME + %s submit FILENAME - ` + ` return fmt.Errorf(msg, arg, BinaryName) } @@ -173,10 +173,10 @@ func (ctx *submitContext) exercise() (workspace.Exercise, error) { if exerciseDir != "" && dir != exerciseDir { msg := ` - You are submitting files belonging to different solutions. - Please submit the files for one solution at a time. + You are submitting files belonging to different solutions. + Please submit the files for one solution at a time. - ` + ` return workspace.Exercise{}, errors.New(msg) } exerciseDir = dir @@ -210,12 +210,12 @@ func (ctx *submitContext) metadata(exerciseDir string) (*workspace.ExerciseMetad // TODO: add test msg := ` - The solution you are submitting is not connected to your account. - Please re-download the exercise to make sure it has the data it needs. + The solution you are submitting is not connected to your account. + Please re-download the exercise to make sure it has the data it needs. - %s download --exercise=%s --track=%s + %s download --exercise=%s --track=%s - ` + ` return nil, fmt.Errorf(msg, BinaryName, metadata.Exercise, metadata.Track) } return metadata, nil @@ -236,7 +236,7 @@ func (ctx *submitContext) documents(exerciseDir string) ([]workspace.Document, e The submitted file '%s' is larger than the max allowed file size of %d bytes. Please reduce the size of the file and try again. - ` + ` return nil, fmt.Errorf(msg, file, maxFileSize) } if info.Size() == 0 { @@ -246,7 +246,7 @@ func (ctx *submitContext) documents(exerciseDir string) ([]workspace.Document, e WARNING: Skipping empty file %s - ` + ` fmt.Fprintf(Err, msg, file) continue } @@ -259,9 +259,9 @@ func (ctx *submitContext) documents(exerciseDir string) ([]workspace.Document, e if len(docs) == 0 { msg := ` - No files found to submit. + No files found to submit. - ` + ` return nil, errors.New(msg) } return docs, nil From 400b286ce63c711e38f5fc84409358ca9f9440c3 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 10 Dec 2018 14:34:15 +0700 Subject: [PATCH 330/544] Add blank validation on submission ID --- cmd/submit.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/submit.go b/cmd/submit.go index d9e5c6056..f6ac03f8b 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -268,6 +268,9 @@ func (ctx *submitContext) documents(exerciseDir string) ([]workspace.Document, e } func (ctx *submitContext) submitRequest(id string, docs []workspace.Document) error { + if id == "" { + return errors.New("id is empty") + } if len(docs) == 0 { return errors.New("docs is empty") } From 7510e3a4981d3d384cc66829a2f0e087ba91386c Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 10 Dec 2018 18:29:29 +0700 Subject: [PATCH 331/544] Extract user config validation to method --- cmd/submit.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cmd/submit.go b/cmd/submit.go index f6ac03f8b..7c83824ab 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -109,6 +109,16 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return nil } +func (ctx *submitContext) validateUserConfig() error { + if ctx.usrCfg.GetString("token") == "" { + return fmt.Errorf(msgWelcomePleaseConfigure, config.SettingsURL(ctx.usrCfg.GetString("apibaseurl")), BinaryName) + } + if ctx.usrCfg.GetString("workspace") == "" { + return fmt.Errorf(msgRerunConfigure, BinaryName) + } + return nil +} + func (ctx *submitContext) sanitizeArgs() error { for i, arg := range ctx.args { var err error From 7242f777c6f5ccd5da9bb59054d5a4fa251bc2bc Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 10 Dec 2018 18:33:01 +0700 Subject: [PATCH 332/544] Extract print method --- cmd/submit.go | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 7c83824ab..8e94e4d08 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -95,17 +95,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - msg := ` - - Your solution has been submitted successfully. - %s -` - suffix := "View it at:\n\n " - if metadata.AutoApprove && metadata.Team == "" { - suffix = "You can complete the exercise and unlock the next core exercise at:\n" - } - fmt.Fprintf(Err, msg, suffix) - fmt.Fprintf(Out, " %s\n\n", metadata.URL) + ctx.printResult(metadata) return nil } @@ -342,6 +332,20 @@ func (ctx *submitContext) submitRequest(id string, docs []workspace.Document) er return nil } +func (ctx *submitContext) printResult(metadata *workspace.ExerciseMetadata) { + msg := ` + + Your solution has been submitted successfully. + %s +` + suffix := "View it at:\n\n " + if metadata.AutoApprove && metadata.Team == "" { + suffix = "You can complete the exercise and unlock the next core exercise at:\n" + } + fmt.Fprintf(Err, msg, suffix) + fmt.Fprintf(Out, " %s\n\n", metadata.URL) +} + func init() { RootCmd.AddCommand(submitCmd) } From 57f384d5986aa38ab2de247523228ad3774df51f Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 10 Dec 2018 19:08:25 +0700 Subject: [PATCH 333/544] Rename char receiver names --- cmd/submit.go | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 8e94e4d08..b84038067 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -99,18 +99,18 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return nil } -func (ctx *submitContext) validateUserConfig() error { - if ctx.usrCfg.GetString("token") == "" { - return fmt.Errorf(msgWelcomePleaseConfigure, config.SettingsURL(ctx.usrCfg.GetString("apibaseurl")), BinaryName) +func (s *submitContext) validateUserConfig() error { + if s.usrCfg.GetString("token") == "" { + return fmt.Errorf(msgWelcomePleaseConfigure, config.SettingsURL(s.usrCfg.GetString("apibaseurl")), BinaryName) } - if ctx.usrCfg.GetString("workspace") == "" { + if s.usrCfg.GetString("workspace") == "" { return fmt.Errorf(msgRerunConfigure, BinaryName) } return nil } -func (ctx *submitContext) sanitizeArgs() error { - for i, arg := range ctx.args { +func (s *submitContext) sanitizeArgs() error { + for i, arg := range s.args { var err error arg, err = filepath.Abs(arg) if err != nil { @@ -150,19 +150,19 @@ func (ctx *submitContext) sanitizeArgs() error { if err != nil { return err } - ctx.args[i] = src + s.args[i] = src } return nil } -func (ctx *submitContext) exercise() (workspace.Exercise, error) { - ws, err := workspace.New(ctx.usrCfg.GetString("workspace")) +func (s *submitContext) exercise() (workspace.Exercise, error) { + ws, err := workspace.New(s.usrCfg.GetString("workspace")) if err != nil { return workspace.Exercise{}, err } var exerciseDir string - for _, arg := range ctx.args { + for _, arg := range s.args { dir, err := ws.ExerciseDir(arg) if err != nil { if workspace.IsMissingMetadata(err) { @@ -185,7 +185,7 @@ func (ctx *submitContext) exercise() (workspace.Exercise, error) { return workspace.NewExerciseFromDir(exerciseDir), nil } -func (ctx *submitContext) metadata(exerciseDir string) (*workspace.ExerciseMetadata, error) { +func (s *submitContext) metadata(exerciseDir string) (*workspace.ExerciseMetadata, error) { metadata, err := workspace.NewExerciseMetadata(exerciseDir) if err != nil { return nil, err @@ -221,9 +221,9 @@ func (ctx *submitContext) metadata(exerciseDir string) (*workspace.ExerciseMetad return metadata, nil } -func (ctx *submitContext) documents(exerciseDir string) ([]workspace.Document, error) { - docs := make([]workspace.Document, 0, len(ctx.args)) - for _, file := range ctx.args { +func (s *submitContext) documents(exerciseDir string) ([]workspace.Document, error) { + docs := make([]workspace.Document, 0, len(s.args)) + for _, file := range s.args { // Don't submit empty files info, err := os.Stat(file) if err != nil { @@ -267,7 +267,7 @@ func (ctx *submitContext) documents(exerciseDir string) ([]workspace.Document, e return docs, nil } -func (ctx *submitContext) submitRequest(id string, docs []workspace.Document) error { +func (s *submitContext) submitRequest(id string, docs []workspace.Document) error { if id == "" { return errors.New("id is empty") } @@ -298,11 +298,11 @@ func (ctx *submitContext) submitRequest(id string, docs []workspace.Document) er return err } - client, err := api.NewClient(ctx.usrCfg.GetString("token"), ctx.usrCfg.GetString("apibaseurl")) + client, err := api.NewClient(s.usrCfg.GetString("token"), s.usrCfg.GetString("apibaseurl")) if err != nil { return err } - url := fmt.Sprintf("%s/solutions/%s", ctx.usrCfg.GetString("apibaseurl"), id) + url := fmt.Sprintf("%s/solutions/%s", s.usrCfg.GetString("apibaseurl"), id) req, err := client.NewRequest("PATCH", url, body) if err != nil { return err @@ -332,7 +332,7 @@ func (ctx *submitContext) submitRequest(id string, docs []workspace.Document) er return nil } -func (ctx *submitContext) printResult(metadata *workspace.ExerciseMetadata) { +func (s *submitContext) printResult(metadata *workspace.ExerciseMetadata) { msg := ` Your solution has been submitted successfully. From c296db4d9562ddc4955dad0de408f18129c645e6 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 10 Dec 2018 19:20:46 +0700 Subject: [PATCH 334/544] Add submitContext constructor & move validation --- cmd/submit.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index b84038067..5ff10fd16 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -51,8 +51,8 @@ var submitCmd = &cobra.Command{ } type submitContext struct { - args []string usrCfg *viper.Viper + args []string } func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { @@ -99,14 +99,18 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return nil } -func (s *submitContext) validateUserConfig() error { - if s.usrCfg.GetString("token") == "" { - return fmt.Errorf(msgWelcomePleaseConfigure, config.SettingsURL(s.usrCfg.GetString("apibaseurl")), BinaryName) +func newSubmitContext(usrCfg *viper.Viper, args []string) (*submitContext, error) { + if usrCfg.GetString("token") == "" { + return nil, fmt.Errorf( + msgWelcomePleaseConfigure, + config.SettingsURL(usrCfg.GetString("apibaseurl")), + BinaryName, + ) } - if s.usrCfg.GetString("workspace") == "" { - return fmt.Errorf(msgRerunConfigure, BinaryName) + if usrCfg.GetString("workspace") == "" { + return nil, fmt.Errorf(msgRerunConfigure, BinaryName) } - return nil + return &submitContext{args: args, usrCfg: usrCfg}, nil } func (s *submitContext) sanitizeArgs() error { From 68143a62e740c7e4e71b891585b4f4d31b167eb5 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 10 Dec 2018 20:36:57 +0700 Subject: [PATCH 335/544] Remove Exercise.Documents Unless we have good reason to keep it, these changes seem to show it's not needed. --- cmd/submit.go | 4 ++-- workspace/exercise.go | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 5ff10fd16..4719f1a3d 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -86,12 +86,12 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - exercise.Documents, err = ctx.documents(exercise.Filepath()) + documents, err := ctx.documents(exercise.Filepath()) if err != nil { return err } - if err := ctx.submitRequest(metadata.ID, exercise.Documents); err != nil { + if err := ctx.submitRequest(metadata.ID, documents); err != nil { return err } diff --git a/workspace/exercise.go b/workspace/exercise.go index d87aa6178..126662b12 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -11,7 +11,6 @@ type Exercise struct { Root string Track string Slug string - Documents []Document } // NewExerciseFromDir constructs an exercise given the exercise directory. From 0e2c8bdff40bd4981823d6e98f831da6fbe21374 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 10 Dec 2018 20:43:54 +0700 Subject: [PATCH 336/544] Metadata depends on Exercise We know `metadata()` will require an Exercise to check that the slug matches so this will make merging more smooth. --- cmd/submit.go | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 4719f1a3d..221bd98e1 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -52,6 +52,7 @@ var submitCmd = &cobra.Command{ type submitContext struct { usrCfg *viper.Viper + flags *pflag.FlagSet args []string } @@ -62,7 +63,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - ctx := &submitContext{args: args, usrCfg: usrCfg} + ctx := &submitContext{args: args, flags: flags, usrCfg: usrCfg} if err := ctx.sanitizeArgs(); err != nil { return err @@ -81,7 +82,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { fmt.Fprintf(Err, migrationStatus.String()) } - metadata, err := ctx.metadata(exercise.Filepath()) + metadata, err := ctx.metadata(exercise) if err != nil { return err } @@ -189,13 +190,24 @@ func (s *submitContext) exercise() (workspace.Exercise, error) { return workspace.NewExerciseFromDir(exerciseDir), nil } -func (s *submitContext) metadata(exerciseDir string) (*workspace.ExerciseMetadata, error) { - metadata, err := workspace.NewExerciseMetadata(exerciseDir) +func (s *submitContext) migrateLegacyMetadata(exercise workspace.Exercise) error { + migrationStatus, err := exercise.MigrateLegacyMetadataFile() + if err != nil { + return err + } + if verbose, _ := s.flags.GetBool("verbose"); verbose { + fmt.Fprintf(Err, migrationStatus.String()) + } + return nil +} + +func (s *submitContext) metadata(exercise workspace.Exercise) (*workspace.ExerciseMetadata, error) { + metadata, err := workspace.NewExerciseMetadata(exercise.Filepath()) + if err != nil { return nil, err } - exercise := workspace.NewExerciseFromDir(exerciseDir) if exercise.Slug != metadata.Exercise { // TODO: error msg should suggest running future doctor command msg := ` From 9b17286ef59eacb13da0eb84ab45aacddfbdaa39 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 10 Dec 2018 20:46:06 +0700 Subject: [PATCH 337/544] gofmt --- workspace/exercise.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/workspace/exercise.go b/workspace/exercise.go index 126662b12..af462832c 100644 --- a/workspace/exercise.go +++ b/workspace/exercise.go @@ -8,9 +8,9 @@ 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 } // NewExerciseFromDir constructs an exercise given the exercise directory. From 381a1cd61b823cd2c5af364ad2f566e56905cdcd Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 11 Dec 2018 11:10:56 +0700 Subject: [PATCH 338/544] Fix shadow err vars --- cmd/submit.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 221bd98e1..ed17ffdc6 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -74,13 +74,9 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - migrationStatus, err := exercise.MigrateLegacyMetadataFile() - if err != nil { + if err = ctx.migrateLegacyMetadata(exercise); err != nil { return err } - if verbose, _ := flags.GetBool("verbose"); verbose { - fmt.Fprintf(Err, migrationStatus.String()) - } metadata, err := ctx.metadata(exercise) if err != nil { From 17b34e47294a7059fffb3a89300ad41de8332311 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 11 Dec 2018 11:15:19 +0700 Subject: [PATCH 339/544] Documents takes an Exercise `documents()` is more flexible and is more encapsulated by not requiring consumers to know that they need to call `exercise.Filepath()` --- cmd/submit.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index ed17ffdc6..94fa17e98 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -83,7 +83,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - documents, err := ctx.documents(exercise.Filepath()) + documents, err := ctx.documents(exercise) if err != nil { return err } @@ -233,7 +233,7 @@ func (s *submitContext) metadata(exercise workspace.Exercise) (*workspace.Exerci return metadata, nil } -func (s *submitContext) documents(exerciseDir string) ([]workspace.Document, error) { +func (s *submitContext) documents(exercise workspace.Exercise) ([]workspace.Document, error) { docs := make([]workspace.Document, 0, len(s.args)) for _, file := range s.args { // Don't submit empty files @@ -262,7 +262,7 @@ func (s *submitContext) documents(exerciseDir string) ([]workspace.Document, err fmt.Fprintf(Err, msg, file) continue } - doc, err := workspace.NewDocument(exerciseDir, file) + doc, err := workspace.NewDocument(exercise.Filepath(), file) if err != nil { return nil, err } From 8a9c2f3bae6f17b59248d8ee3d9180c7f1fd8a4c Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 11 Dec 2018 11:24:59 +0700 Subject: [PATCH 340/544] submitRequest depends on metadata Rather than requiring just an ID, submitRequest is more flexible by depending on the entire metadata. It seems possible that changes may require additional info from metadata. Also, currently ID is entirely dependent on metadata so this better encapsulates that knowledge. --- cmd/submit.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 94fa17e98..a2f4fa8b0 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -88,7 +88,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - if err := ctx.submitRequest(metadata.ID, documents); err != nil { + if err := ctx.submitRequest(metadata, documents); err != nil { return err } @@ -279,8 +279,8 @@ func (s *submitContext) documents(exercise workspace.Exercise) ([]workspace.Docu return docs, nil } -func (s *submitContext) submitRequest(id string, docs []workspace.Document) error { - if id == "" { +func (s *submitContext) submitRequest(metadata *workspace.ExerciseMetadata, docs []workspace.Document) error { + if metadata.ID == "" { return errors.New("id is empty") } if len(docs) == 0 { @@ -314,7 +314,7 @@ func (s *submitContext) submitRequest(id string, docs []workspace.Document) erro if err != nil { return err } - url := fmt.Sprintf("%s/solutions/%s", s.usrCfg.GetString("apibaseurl"), id) + url := fmt.Sprintf("%s/solutions/%s", s.usrCfg.GetString("apibaseurl"), metadata.ID) req, err := client.NewRequest("PATCH", url, body) if err != nil { return err From c686c3c34129fb2456b8c4fcf69339c1acebfbd7 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 11 Dec 2018 11:40:30 +0700 Subject: [PATCH 341/544] Ensure submit prints on success --- cmd/submit_test.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 06e7caf55..2dae04117 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -145,6 +145,7 @@ func TestSubmitFiles(t *testing.T) { co := newCapturedOutput() co.override() defer co.reset() + Err = &bytes.Buffer{} // The fake endpoint will populate this when it receives the call from the command. submittedFiles := map[string]string{} @@ -186,14 +187,16 @@ func TestSubmitFiles(t *testing.T) { files := []string{ file1, file2, readme, } - err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) - assert.NoError(t, err) - assert.Equal(t, 3, len(submittedFiles)) + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) - assert.Equal(t, "This is file 1.", submittedFiles["file-1.txt"]) - assert.Equal(t, "This is file 2.", submittedFiles["subdir/file-2.txt"]) - assert.Equal(t, "This is the readme.", submittedFiles["README.md"]) + if assert.NoError(t, err) { + 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["subdir/file-2.txt"]) + assert.Equal(t, "This is the readme.", submittedFiles["README.md"]) + assert.Regexp(t, "submitted successfully", Err) + } } func TestLegacyMetadataMigration(t *testing.T) { From 2a879b55b16c1d59f0939d8924608e492ba60f8c Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 11 Dec 2018 14:23:19 +0700 Subject: [PATCH 342/544] Move sanitizeArgs to constructor sanitizeArgs is more of an implementation detail than a public API, so it is better encapsulated in the constructor. --- cmd/submit.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index a2f4fa8b0..a156d845b 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -57,15 +57,8 @@ type submitContext struct { } func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { - usrCfg := cfg.UserViperConfig - - if err := validateUserConfig(usrCfg); err != nil { - return err - } - - ctx := &submitContext{args: args, flags: flags, usrCfg: usrCfg} - - if err := ctx.sanitizeArgs(); err != nil { + ctx, err := newSubmitContext(cfg.UserViperConfig, flags, args) + if err != nil { return err } @@ -96,7 +89,8 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return nil } -func newSubmitContext(usrCfg *viper.Viper, args []string) (*submitContext, error) { +// newSubmitContext creates a submitContext and sanitizes the arguments. +func newSubmitContext(usrCfg *viper.Viper, flags *pflag.FlagSet, args []string) (*submitContext, error) { if usrCfg.GetString("token") == "" { return nil, fmt.Errorf( msgWelcomePleaseConfigure, @@ -107,7 +101,9 @@ func newSubmitContext(usrCfg *viper.Viper, args []string) (*submitContext, error if usrCfg.GetString("workspace") == "" { return nil, fmt.Errorf(msgRerunConfigure, BinaryName) } - return &submitContext{args: args, usrCfg: usrCfg}, nil + + ctx := &submitContext{usrCfg: usrCfg, flags: flags, args: args} + return ctx, ctx.sanitizeArgs() } func (s *submitContext) sanitizeArgs() error { From e5c47feb1e1586f34e484edf4d55693ae02a7cff Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 11 Dec 2018 14:32:55 +0700 Subject: [PATCH 343/544] Improve godoc --- cmd/submit.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/submit.go b/cmd/submit.go index a156d845b..c75834e04 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -50,6 +50,7 @@ var submitCmd = &cobra.Command{ }, } +// submitContext is a context for submitting solutions to the API. type submitContext struct { usrCfg *viper.Viper flags *pflag.FlagSet @@ -89,7 +90,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return nil } -// newSubmitContext creates a submitContext and sanitizes the arguments. +// newSubmitContext creates a submitContext, sanitizing given args. func newSubmitContext(usrCfg *viper.Viper, flags *pflag.FlagSet, args []string) (*submitContext, error) { if usrCfg.GetString("token") == "" { return nil, fmt.Errorf( @@ -106,6 +107,7 @@ func newSubmitContext(usrCfg *viper.Viper, flags *pflag.FlagSet, args []string) return ctx, ctx.sanitizeArgs() } +// sanitizeArgs validates args and replaces with evaluated symlink paths. func (s *submitContext) sanitizeArgs() error { for i, arg := range s.args { var err error @@ -229,6 +231,7 @@ func (s *submitContext) metadata(exercise workspace.Exercise) (*workspace.Exerci return metadata, nil } +// documents creates a document for each internal arg upon validation, returning the collection. func (s *submitContext) documents(exercise workspace.Exercise) ([]workspace.Document, error) { docs := make([]workspace.Document, 0, len(s.args)) for _, file := range s.args { @@ -275,6 +278,7 @@ func (s *submitContext) documents(exercise workspace.Exercise) ([]workspace.Docu return docs, nil } +// submitRequest submits an HTTP request for each document. func (s *submitContext) submitRequest(metadata *workspace.ExerciseMetadata, docs []workspace.Document) error { if metadata.ID == "" { return errors.New("id is empty") From ff65f768bd384c2fe0c8b0c16195ecf144a718ba Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Wed, 12 Dec 2018 15:25:41 +0700 Subject: [PATCH 344/544] Extract common validate user config Fix test errors and non-checked nil error return vals --- cmd/submit.go | 15 ++++----------- cmd/submit_test.go | 1 + 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index c75834e04..ad29bab0e 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -58,6 +58,10 @@ type submitContext struct { } func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { + if err := validateUserConfig(cfg.UserViperConfig); err != nil { + return err + } + ctx, err := newSubmitContext(cfg.UserViperConfig, flags, args) if err != nil { return err @@ -92,17 +96,6 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { // newSubmitContext creates a submitContext, sanitizing given args. func newSubmitContext(usrCfg *viper.Viper, flags *pflag.FlagSet, args []string) (*submitContext, error) { - if usrCfg.GetString("token") == "" { - return nil, fmt.Errorf( - msgWelcomePleaseConfigure, - config.SettingsURL(usrCfg.GetString("apibaseurl")), - BinaryName, - ) - } - if usrCfg.GetString("workspace") == "" { - return nil, fmt.Errorf(msgRerunConfigure, BinaryName) - } - ctx := &submitContext{usrCfg: usrCfg, flags: flags, args: args} return ctx, ctx.sanitizeArgs() } diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 2dae04117..4dc9b2b1d 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -134,6 +134,7 @@ func TestSubmitFilesAndDir(t *testing.T) { tmpDir, filepath.Join(tmpDir, "file-2.txt"), } + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) if assert.Error(t, err) { assert.Regexp(t, "submitting a directory", err.Error()) From 4bf220037f7cdb7c2cc76bc21e07392d6ea5bfb9 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Wed, 12 Dec 2018 17:39:56 +0700 Subject: [PATCH 345/544] Clarify naming and godocs --- cmd/submit.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index ad29bab0e..d4c8e3ee8 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -86,7 +86,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - if err := ctx.submitRequest(metadata, documents); err != nil { + if err := ctx.submitDocuments(metadata, documents); err != nil { return err } @@ -100,7 +100,7 @@ func newSubmitContext(usrCfg *viper.Viper, flags *pflag.FlagSet, args []string) return ctx, ctx.sanitizeArgs() } -// sanitizeArgs validates args and replaces with evaluated symlink paths. +// sanitizeArgs validates args and swaps with evaluated symlink paths. func (s *submitContext) sanitizeArgs() error { for i, arg := range s.args { var err error @@ -224,7 +224,6 @@ func (s *submitContext) metadata(exercise workspace.Exercise) (*workspace.Exerci return metadata, nil } -// documents creates a document for each internal arg upon validation, returning the collection. func (s *submitContext) documents(exercise workspace.Exercise) ([]workspace.Document, error) { docs := make([]workspace.Document, 0, len(s.args)) for _, file := range s.args { @@ -271,8 +270,8 @@ func (s *submitContext) documents(exercise workspace.Exercise) ([]workspace.Docu return docs, nil } -// submitRequest submits an HTTP request for each document. -func (s *submitContext) submitRequest(metadata *workspace.ExerciseMetadata, docs []workspace.Document) error { +// submitDocuments submits the documents to the API via HTTP. +func (s *submitContext) submitDocuments(metadata *workspace.ExerciseMetadata, docs []workspace.Document) error { if metadata.ID == "" { return errors.New("id is empty") } From a5e681455f25b5ff2774534add09d14240869e8b Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 17 Dec 2018 06:25:06 +0700 Subject: [PATCH 346/544] Encapsulate submission data in constructor Previously, `runSubmit` required calling `getter` type methods and passing them back to the same type that gets them, resulting in an awkward API. These changes help to reduce this burden, simplifying the API and helping to make the submit cmd at a more appropriate level of abstraction. --- cmd/submit.go | 88 +++++++++++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index d4c8e3ee8..479e351f5 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -50,11 +50,18 @@ var submitCmd = &cobra.Command{ }, } +type submission struct { + exercise workspace.Exercise + metadata *workspace.ExerciseMetadata + documents []workspace.Document +} + // submitContext is a context for submitting solutions to the API. type submitContext struct { usrCfg *viper.Viper flags *pflag.FlagSet args []string + submission } func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { @@ -67,37 +74,45 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - exercise, err := ctx.exercise() - if err != nil { + if err := ctx.submitDocuments(); err != nil { return err } - if err = ctx.migrateLegacyMetadata(exercise); err != nil { - return err + ctx.printResult() + return nil +} + +// newSubmitContext creates a submitContext. +func newSubmitContext(usrCfg *viper.Viper, flags *pflag.FlagSet, args []string) (*submitContext, error) { + ctx := &submitContext{usrCfg: usrCfg, flags: flags, args: args} + + if err := ctx.sanitizeArgs(); err != nil { + return nil, err } - metadata, err := ctx.metadata(exercise) + exercise, err := ctx._exercise() if err != nil { - return err + return nil, err } + ctx.exercise = exercise - documents, err := ctx.documents(exercise) - if err != nil { - return err + if err = ctx.migrateLegacyMetadata(); err != nil { + return nil, err } - if err := ctx.submitDocuments(metadata, documents); err != nil { - return err + metadata, err := ctx._metadata() + if err != nil { + return nil, err } + ctx.metadata = metadata - ctx.printResult(metadata) - return nil -} + documents, err := ctx._documents() + if err != nil { + return nil, err + } + ctx.documents = documents -// newSubmitContext creates a submitContext, sanitizing given args. -func newSubmitContext(usrCfg *viper.Viper, flags *pflag.FlagSet, args []string) (*submitContext, error) { - ctx := &submitContext{usrCfg: usrCfg, flags: flags, args: args} - return ctx, ctx.sanitizeArgs() + return ctx, nil } // sanitizeArgs validates args and swaps with evaluated symlink paths. @@ -147,7 +162,7 @@ func (s *submitContext) sanitizeArgs() error { return nil } -func (s *submitContext) exercise() (workspace.Exercise, error) { +func (s *submitContext) _exercise() (workspace.Exercise, error) { ws, err := workspace.New(s.usrCfg.GetString("workspace")) if err != nil { return workspace.Exercise{}, err @@ -177,8 +192,8 @@ func (s *submitContext) exercise() (workspace.Exercise, error) { return workspace.NewExerciseFromDir(exerciseDir), nil } -func (s *submitContext) migrateLegacyMetadata(exercise workspace.Exercise) error { - migrationStatus, err := exercise.MigrateLegacyMetadataFile() +func (s *submitContext) migrateLegacyMetadata() error { + migrationStatus, err := s.exercise.MigrateLegacyMetadataFile() if err != nil { return err } @@ -188,14 +203,13 @@ func (s *submitContext) migrateLegacyMetadata(exercise workspace.Exercise) error return nil } -func (s *submitContext) metadata(exercise workspace.Exercise) (*workspace.ExerciseMetadata, error) { - metadata, err := workspace.NewExerciseMetadata(exercise.Filepath()) - +func (s *submitContext) _metadata() (*workspace.ExerciseMetadata, error) { + metadata, err := workspace.NewExerciseMetadata(s.exercise.Filepath()) if err != nil { return nil, err } - if exercise.Slug != metadata.Exercise { + if metadata.Exercise != s.exercise.Slug { // TODO: error msg should suggest running future doctor command msg := ` @@ -206,7 +220,7 @@ func (s *submitContext) metadata(exercise workspace.Exercise) (*workspace.Exerci Please rename the directory '%[1]s' to '%[2]s' and try again. ` - return nil, fmt.Errorf(msg, exercise.Slug, metadata.Exercise) + return nil, fmt.Errorf(msg, s.exercise.Slug, metadata.Exercise) } if !metadata.IsRequester { @@ -224,7 +238,7 @@ func (s *submitContext) metadata(exercise workspace.Exercise) (*workspace.Exerci return metadata, nil } -func (s *submitContext) documents(exercise workspace.Exercise) ([]workspace.Document, error) { +func (s *submitContext) _documents() ([]workspace.Document, error) { docs := make([]workspace.Document, 0, len(s.args)) for _, file := range s.args { // Don't submit empty files @@ -253,7 +267,7 @@ func (s *submitContext) documents(exercise workspace.Exercise) ([]workspace.Docu fmt.Fprintf(Err, msg, file) continue } - doc, err := workspace.NewDocument(exercise.Filepath(), file) + doc, err := workspace.NewDocument(s.exercise.Filepath(), file) if err != nil { return nil, err } @@ -271,18 +285,18 @@ func (s *submitContext) documents(exercise workspace.Exercise) ([]workspace.Docu } // submitDocuments submits the documents to the API via HTTP. -func (s *submitContext) submitDocuments(metadata *workspace.ExerciseMetadata, docs []workspace.Document) error { - if metadata.ID == "" { +func (s *submitContext) submitDocuments() error { + if s.metadata.ID == "" { return errors.New("id is empty") } - if len(docs) == 0 { - return errors.New("docs is empty") + if len(s.documents) == 0 { + return errors.New("documents is empty") } body := &bytes.Buffer{} writer := multipart.NewWriter(body) - for _, doc := range docs { + for _, doc := range s.documents { file, err := os.Open(doc.Filepath()) if err != nil { return err @@ -306,7 +320,7 @@ func (s *submitContext) submitDocuments(metadata *workspace.ExerciseMetadata, do if err != nil { return err } - url := fmt.Sprintf("%s/solutions/%s", s.usrCfg.GetString("apibaseurl"), metadata.ID) + url := fmt.Sprintf("%s/solutions/%s", s.usrCfg.GetString("apibaseurl"), s.metadata.ID) req, err := client.NewRequest("PATCH", url, body) if err != nil { return err @@ -336,18 +350,18 @@ func (s *submitContext) submitDocuments(metadata *workspace.ExerciseMetadata, do return nil } -func (s *submitContext) printResult(metadata *workspace.ExerciseMetadata) { +func (s *submitContext) printResult() { msg := ` Your solution has been submitted successfully. %s ` suffix := "View it at:\n\n " - if metadata.AutoApprove && metadata.Team == "" { + if s.metadata.AutoApprove && s.metadata.Team == "" { suffix = "You can complete the exercise and unlock the next core exercise at:\n" } fmt.Fprintf(Err, msg, suffix) - fmt.Fprintf(Out, " %s\n\n", metadata.URL) + fmt.Fprintf(Out, " %s\n\n", s.metadata.URL) } func init() { From f021f71d76e100198402e6ce7d8615df5d40592f Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 17 Dec 2018 07:51:14 +0700 Subject: [PATCH 347/544] Make dependencies explicit --- cmd/submit.go | 109 ++++++++++++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 56 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 479e351f5..8a7a70461 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -51,16 +51,14 @@ var submitCmd = &cobra.Command{ } type submission struct { - exercise workspace.Exercise - metadata *workspace.ExerciseMetadata documents []workspace.Document + metadata *workspace.ExerciseMetadata } // submitContext is a context for submitting solutions to the API. type submitContext struct { usrCfg *viper.Viper flags *pflag.FlagSet - args []string submission } @@ -74,7 +72,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - if err := ctx.submitDocuments(); err != nil { + if err := submitDocuments(cfg.UserViperConfig, ctx.metadata, ctx.documents); err != nil { return err } @@ -84,44 +82,43 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { // newSubmitContext creates a submitContext. func newSubmitContext(usrCfg *viper.Viper, flags *pflag.FlagSet, args []string) (*submitContext, error) { - ctx := &submitContext{usrCfg: usrCfg, flags: flags, args: args} + ctx := &submitContext{usrCfg: usrCfg, flags: flags} - if err := ctx.sanitizeArgs(); err != nil { + filepaths, err := ctx.sanitizeArgs(args) + if err != nil { return nil, err } - exercise, err := ctx._exercise() + exercise, err := ctx.exercise(filepaths) if err != nil { return nil, err } - ctx.exercise = exercise - if err = ctx.migrateLegacyMetadata(); err != nil { + if err = ctx.migrateLegacyMetadata(exercise); err != nil { return nil, err } - metadata, err := ctx._metadata() + documents, err := ctx._documents(filepaths, exercise) if err != nil { return nil, err } - ctx.metadata = metadata - documents, err := ctx._documents() + metadata, err := ctx._metadata(exercise) if err != nil { return nil, err } - ctx.documents = documents + ctx.submission = submission{documents: documents, metadata: metadata} return ctx, nil } // sanitizeArgs validates args and swaps with evaluated symlink paths. -func (s *submitContext) sanitizeArgs() error { - for i, arg := range s.args { +func (s *submitContext) sanitizeArgs(args []string) ([]string, error) { + for i, arg := range args { var err error arg, err = filepath.Abs(arg) if err != nil { - return err + return nil, err } info, err := os.Lstat(arg) @@ -134,9 +131,9 @@ func (s *submitContext) sanitizeArgs() error { %s ` - return fmt.Errorf(msg, arg) + return nil, fmt.Errorf(msg, arg) } - return err + return nil, err } if info.IsDir() { msg := ` @@ -150,27 +147,27 @@ func (s *submitContext) sanitizeArgs() error { %s submit FILENAME ` - return fmt.Errorf(msg, arg, BinaryName) + return nil, fmt.Errorf(msg, arg, BinaryName) } src, err := filepath.EvalSymlinks(arg) if err != nil { - return err + return nil, err } - s.args[i] = src + args[i] = src } - return nil + return args, nil } -func (s *submitContext) _exercise() (workspace.Exercise, error) { +func (s *submitContext) exercise(filepaths []string) (workspace.Exercise, error) { ws, err := workspace.New(s.usrCfg.GetString("workspace")) if err != nil { return workspace.Exercise{}, err } var exerciseDir string - for _, arg := range s.args { - dir, err := ws.ExerciseDir(arg) + for _, f := range filepaths { + dir, err := ws.ExerciseDir(f) if err != nil { if workspace.IsMissingMetadata(err) { return workspace.Exercise{}, errors.New(msgMissingMetadata) @@ -192,8 +189,8 @@ func (s *submitContext) _exercise() (workspace.Exercise, error) { return workspace.NewExerciseFromDir(exerciseDir), nil } -func (s *submitContext) migrateLegacyMetadata() error { - migrationStatus, err := s.exercise.MigrateLegacyMetadataFile() +func (s *submitContext) migrateLegacyMetadata(exercise workspace.Exercise) error { + migrationStatus, err := exercise.MigrateLegacyMetadataFile() if err != nil { return err } @@ -203,13 +200,13 @@ func (s *submitContext) migrateLegacyMetadata() error { return nil } -func (s *submitContext) _metadata() (*workspace.ExerciseMetadata, error) { - metadata, err := workspace.NewExerciseMetadata(s.exercise.Filepath()) +func (s *submitContext) _metadata(exercise workspace.Exercise) (*workspace.ExerciseMetadata, error) { + metadata, err := workspace.NewExerciseMetadata(exercise.Filepath()) if err != nil { return nil, err } - if metadata.Exercise != s.exercise.Slug { + if metadata.Exercise != exercise.Slug { // TODO: error msg should suggest running future doctor command msg := ` @@ -220,7 +217,7 @@ func (s *submitContext) _metadata() (*workspace.ExerciseMetadata, error) { Please rename the directory '%[1]s' to '%[2]s' and try again. ` - return nil, fmt.Errorf(msg, s.exercise.Slug, metadata.Exercise) + return nil, fmt.Errorf(msg, exercise.Slug, metadata.Exercise) } if !metadata.IsRequester { @@ -238,9 +235,9 @@ func (s *submitContext) _metadata() (*workspace.ExerciseMetadata, error) { return metadata, nil } -func (s *submitContext) _documents() ([]workspace.Document, error) { - docs := make([]workspace.Document, 0, len(s.args)) - for _, file := range s.args { +func (s *submitContext) _documents(filepaths []string, exercise workspace.Exercise) ([]workspace.Document, error) { + docs := make([]workspace.Document, 0, len(filepaths)) + for _, file := range filepaths { // Don't submit empty files info, err := os.Stat(file) if err != nil { @@ -267,7 +264,7 @@ func (s *submitContext) _documents() ([]workspace.Document, error) { fmt.Fprintf(Err, msg, file) continue } - doc, err := workspace.NewDocument(s.exercise.Filepath(), file) + doc, err := workspace.NewDocument(exercise.Filepath(), file) if err != nil { return nil, err } @@ -284,19 +281,33 @@ func (s *submitContext) _documents() ([]workspace.Document, error) { return docs, nil } -// submitDocuments submits the documents to the API via HTTP. -func (s *submitContext) submitDocuments() error { - if s.metadata.ID == "" { +func (s *submitContext) printResult() { + msg := ` + + Your solution has been submitted successfully. + %s +` + suffix := "View it at:\n\n " + if s.metadata.AutoApprove && s.metadata.Team == "" { + suffix = "You can complete the exercise and unlock the next core exercise at:\n" + } + fmt.Fprintf(Err, msg, suffix) + fmt.Fprintf(Out, " %s\n\n", s.metadata.URL) +} + +// submitDocuments submits the documents to the Exercism API. +func submitDocuments(usrCfg *viper.Viper, metadata *workspace.ExerciseMetadata, docs []workspace.Document) error { + if metadata.ID == "" { return errors.New("id is empty") } - if len(s.documents) == 0 { + if len(docs) == 0 { return errors.New("documents is empty") } body := &bytes.Buffer{} writer := multipart.NewWriter(body) - for _, doc := range s.documents { + for _, doc := range docs { file, err := os.Open(doc.Filepath()) if err != nil { return err @@ -316,11 +327,11 @@ func (s *submitContext) submitDocuments() error { return err } - client, err := api.NewClient(s.usrCfg.GetString("token"), s.usrCfg.GetString("apibaseurl")) + client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl")) if err != nil { return err } - url := fmt.Sprintf("%s/solutions/%s", s.usrCfg.GetString("apibaseurl"), s.metadata.ID) + url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), metadata.ID) req, err := client.NewRequest("PATCH", url, body) if err != nil { return err @@ -350,20 +361,6 @@ func (s *submitContext) submitDocuments() error { return nil } -func (s *submitContext) printResult() { - msg := ` - - Your solution has been submitted successfully. - %s -` - suffix := "View it at:\n\n " - if s.metadata.AutoApprove && s.metadata.Team == "" { - suffix = "You can complete the exercise and unlock the next core exercise at:\n" - } - fmt.Fprintf(Err, msg, suffix) - fmt.Fprintf(Out, " %s\n\n", s.metadata.URL) -} - func init() { RootCmd.AddCommand(submitCmd) } From 65848da0c2c468dba5286b3237f6dafdd6327481 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 17 Dec 2018 09:30:41 +0700 Subject: [PATCH 348/544] Move methods to submit type This makes submit testable in isolation. --- cmd/submit.go | 75 ++++++++++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 8a7a70461..34a84cb07 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -50,18 +50,6 @@ var submitCmd = &cobra.Command{ }, } -type submission struct { - documents []workspace.Document - metadata *workspace.ExerciseMetadata -} - -// submitContext is a context for submitting solutions to the API. -type submitContext struct { - usrCfg *viper.Viper - flags *pflag.FlagSet - submission -} - func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { if err := validateUserConfig(cfg.UserViperConfig); err != nil { return err @@ -72,7 +60,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - if err := submitDocuments(cfg.UserViperConfig, ctx.metadata, ctx.documents); err != nil { + if err := ctx.submit(cfg.UserViperConfig); err != nil { return err } @@ -80,6 +68,13 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return nil } +// submitContext is a context for submitting solutions to the API. +type submitContext struct { + usrCfg *viper.Viper + flags *pflag.FlagSet + submission +} + // newSubmitContext creates a submitContext. func newSubmitContext(usrCfg *viper.Viper, flags *pflag.FlagSet, args []string) (*submitContext, error) { ctx := &submitContext{usrCfg: usrCfg, flags: flags} @@ -281,33 +276,21 @@ func (s *submitContext) _documents(filepaths []string, exercise workspace.Exerci return docs, nil } -func (s *submitContext) printResult() { - msg := ` - - Your solution has been submitted successfully. - %s -` - suffix := "View it at:\n\n " - if s.metadata.AutoApprove && s.metadata.Team == "" { - suffix = "You can complete the exercise and unlock the next core exercise at:\n" - } - fmt.Fprintf(Err, msg, suffix) - fmt.Fprintf(Out, " %s\n\n", s.metadata.URL) +type submission struct { + documents []workspace.Document + metadata *workspace.ExerciseMetadata } -// submitDocuments submits the documents to the Exercism API. -func submitDocuments(usrCfg *viper.Viper, metadata *workspace.ExerciseMetadata, docs []workspace.Document) error { - if metadata.ID == "" { - return errors.New("id is empty") - } - if len(docs) == 0 { - return errors.New("documents is empty") +// submit submits the documents to the Exercism API. +func (s submission) submit(usrCfg *viper.Viper) error { + if err := s.validate(); err != nil { + return err } body := &bytes.Buffer{} writer := multipart.NewWriter(body) - for _, doc := range docs { + for _, doc := range s.documents { file, err := os.Open(doc.Filepath()) if err != nil { return err @@ -331,7 +314,7 @@ func submitDocuments(usrCfg *viper.Viper, metadata *workspace.ExerciseMetadata, if err != nil { return err } - url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), metadata.ID) + url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), s.metadata.ID) req, err := client.NewRequest("PATCH", url, body) if err != nil { return err @@ -361,6 +344,30 @@ func submitDocuments(usrCfg *viper.Viper, metadata *workspace.ExerciseMetadata, return nil } +func (s submission) printResult() { + msg := ` + + Your solution has been submitted successfully. + %s +` + suffix := "View it at:\n\n " + if s.metadata.AutoApprove && s.metadata.Team == "" { + suffix = "You can complete the exercise and unlock the next core exercise at:\n" + } + fmt.Fprintf(Err, msg, suffix) + fmt.Fprintf(Out, " %s\n\n", s.metadata.URL) +} + +func (s submission) validate() error { + if s.metadata.ID == "" { + return errors.New("id is empty") + } + if len(s.documents) == 0 { + return errors.New("documents is empty") + } + return nil +} + func init() { RootCmd.AddCommand(submitCmd) } From d0afa9ff5828b776fe463778fcc0a00a23f6b9a6 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 17 Dec 2018 16:01:35 +0700 Subject: [PATCH 349/544] Rename submitContext => submitCmdContext Distinguish that this is a context for the submit cmd itself. --- cmd/submit.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 34a84cb07..ba879f760 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -55,7 +55,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - ctx, err := newSubmitContext(cfg.UserViperConfig, flags, args) + ctx, err := newSubmitCmdContext(cfg.UserViperConfig, flags, args) if err != nil { return err } @@ -68,16 +68,16 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return nil } -// submitContext is a context for submitting solutions to the API. -type submitContext struct { +// submitCmdContext represents the context for the submit cmd. +type submitCmdContext struct { usrCfg *viper.Viper flags *pflag.FlagSet submission } -// newSubmitContext creates a submitContext. -func newSubmitContext(usrCfg *viper.Viper, flags *pflag.FlagSet, args []string) (*submitContext, error) { - ctx := &submitContext{usrCfg: usrCfg, flags: flags} +// newSubmitCmdContext sets up a context to initiate a submission. +func newSubmitCmdContext(usrCfg *viper.Viper, flags *pflag.FlagSet, args []string) (*submitCmdContext, error) { + ctx := &submitCmdContext{usrCfg: usrCfg, flags: flags} filepaths, err := ctx.sanitizeArgs(args) if err != nil { @@ -108,7 +108,7 @@ func newSubmitContext(usrCfg *viper.Viper, flags *pflag.FlagSet, args []string) } // sanitizeArgs validates args and swaps with evaluated symlink paths. -func (s *submitContext) sanitizeArgs(args []string) ([]string, error) { +func (s *submitCmdContext) sanitizeArgs(args []string) ([]string, error) { for i, arg := range args { var err error arg, err = filepath.Abs(arg) @@ -154,7 +154,7 @@ func (s *submitContext) sanitizeArgs(args []string) ([]string, error) { return args, nil } -func (s *submitContext) exercise(filepaths []string) (workspace.Exercise, error) { +func (s *submitCmdContext) exercise(filepaths []string) (workspace.Exercise, error) { ws, err := workspace.New(s.usrCfg.GetString("workspace")) if err != nil { return workspace.Exercise{}, err @@ -184,7 +184,7 @@ func (s *submitContext) exercise(filepaths []string) (workspace.Exercise, error) return workspace.NewExerciseFromDir(exerciseDir), nil } -func (s *submitContext) migrateLegacyMetadata(exercise workspace.Exercise) error { +func (s *submitCmdContext) migrateLegacyMetadata(exercise workspace.Exercise) error { migrationStatus, err := exercise.MigrateLegacyMetadataFile() if err != nil { return err @@ -195,7 +195,7 @@ func (s *submitContext) migrateLegacyMetadata(exercise workspace.Exercise) error return nil } -func (s *submitContext) _metadata(exercise workspace.Exercise) (*workspace.ExerciseMetadata, error) { +func (s *submitCmdContext) _metadata(exercise workspace.Exercise) (*workspace.ExerciseMetadata, error) { metadata, err := workspace.NewExerciseMetadata(exercise.Filepath()) if err != nil { return nil, err @@ -230,7 +230,7 @@ func (s *submitContext) _metadata(exercise workspace.Exercise) (*workspace.Exerc return metadata, nil } -func (s *submitContext) _documents(filepaths []string, exercise workspace.Exercise) ([]workspace.Document, error) { +func (s *submitCmdContext) _documents(filepaths []string, exercise workspace.Exercise) ([]workspace.Document, error) { docs := make([]workspace.Document, 0, len(filepaths)) for _, file := range filepaths { // Don't submit empty files From 9dd0499e0db5682df79872c56e9aab10b40f7294 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 18 Dec 2018 07:36:00 +0700 Subject: [PATCH 350/544] Move printResult to submitCmdContext printing really depends on the submitCmd rather than the submission itself --- cmd/submit.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index ba879f760..bcc7c72b1 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -276,6 +276,20 @@ func (s *submitCmdContext) _documents(filepaths []string, exercise workspace.Exe return docs, nil } +func (s *submitCmdContext) printResult() { + msg := ` + + Your solution has been submitted successfully. + %s +` + suffix := "View it at:\n\n " + if s.metadata.AutoApprove && s.metadata.Team == "" { + suffix = "You can complete the exercise and unlock the next core exercise at:\n" + } + fmt.Fprintf(Err, msg, suffix) + fmt.Fprintf(Out, " %s\n\n", s.metadata.URL) +} + type submission struct { documents []workspace.Document metadata *workspace.ExerciseMetadata @@ -344,20 +358,6 @@ func (s submission) submit(usrCfg *viper.Viper) error { return nil } -func (s submission) printResult() { - msg := ` - - Your solution has been submitted successfully. - %s -` - suffix := "View it at:\n\n " - if s.metadata.AutoApprove && s.metadata.Team == "" { - suffix = "You can complete the exercise and unlock the next core exercise at:\n" - } - fmt.Fprintf(Err, msg, suffix) - fmt.Fprintf(Out, " %s\n\n", s.metadata.URL) -} - func (s submission) validate() error { if s.metadata.ID == "" { return errors.New("id is empty") From d2966363471a6d9ae8cee5497eb86ff12d3fefb5 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 18 Dec 2018 07:42:11 +0700 Subject: [PATCH 351/544] Submission uses pointer receivers Size can get large if submitting many docs --- cmd/submit.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index bcc7c72b1..66785778c 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -296,7 +296,7 @@ type submission struct { } // submit submits the documents to the Exercism API. -func (s submission) submit(usrCfg *viper.Viper) error { +func (s *submission) submit(usrCfg *viper.Viper) error { if err := s.validate(); err != nil { return err } @@ -358,7 +358,10 @@ func (s submission) submit(usrCfg *viper.Viper) error { return nil } -func (s submission) validate() error { +func (s *submission) validate() error { + if s == nil { + return errors.New("submission is empty") + } if s.metadata.ID == "" { return errors.New("id is empty") } From 23afc489c0e4705f6e568994c8eccf10c3299c08 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Tue, 18 Dec 2018 07:42:57 +0700 Subject: [PATCH 352/544] godoc --- cmd/submit.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/submit.go b/cmd/submit.go index 66785778c..00249e7f6 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -290,6 +290,7 @@ func (s *submitCmdContext) printResult() { fmt.Fprintf(Out, " %s\n\n", s.metadata.URL) } +// submission is a submission to the Excercism API. type submission struct { documents []workspace.Document metadata *workspace.ExerciseMetadata From 34913282c966c25cee7c598fb369eea423d99f7c Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Wed, 2 Jan 2019 16:49:19 +0800 Subject: [PATCH 353/544] Make runSubmit more explicit Remove submit type and move methods to submitCmdContext --- cmd/submit.go | 109 +++++++++++++++++--------------------------------- 1 file changed, 37 insertions(+), 72 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 00249e7f6..4ab2006ec 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -55,56 +55,44 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - ctx, err := newSubmitCmdContext(cfg.UserViperConfig, flags, args) - if err != nil { - return err - } - - if err := ctx.submit(cfg.UserViperConfig); err != nil { - return err - } - - ctx.printResult() - return nil -} - -// submitCmdContext represents the context for the submit cmd. -type submitCmdContext struct { - usrCfg *viper.Viper - flags *pflag.FlagSet - submission -} - -// newSubmitCmdContext sets up a context to initiate a submission. -func newSubmitCmdContext(usrCfg *viper.Viper, flags *pflag.FlagSet, args []string) (*submitCmdContext, error) { - ctx := &submitCmdContext{usrCfg: usrCfg, flags: flags} + ctx := &submitCmdContext{usrCfg: cfg.UserViperConfig, flags: flags} filepaths, err := ctx.sanitizeArgs(args) if err != nil { - return nil, err + return err } exercise, err := ctx.exercise(filepaths) if err != nil { - return nil, err + return err } if err = ctx.migrateLegacyMetadata(exercise); err != nil { - return nil, err + return err } - documents, err := ctx._documents(filepaths, exercise) + documents, err := ctx.documents(filepaths, exercise) if err != nil { - return nil, err + return err } - metadata, err := ctx._metadata(exercise) + metadata, err := ctx.metadata(exercise) if err != nil { - return nil, err + return err + } + + if err := ctx.submit(metadata, documents); err != nil { + return err } - ctx.submission = submission{documents: documents, metadata: metadata} - return ctx, nil + ctx.printResult(metadata) + return nil +} + +// submitCmdContext represents the context for the submit cmd. +type submitCmdContext struct { + usrCfg *viper.Viper + flags *pflag.FlagSet } // sanitizeArgs validates args and swaps with evaluated symlink paths. @@ -195,7 +183,7 @@ func (s *submitCmdContext) migrateLegacyMetadata(exercise workspace.Exercise) er return nil } -func (s *submitCmdContext) _metadata(exercise workspace.Exercise) (*workspace.ExerciseMetadata, error) { +func (s *submitCmdContext) metadata(exercise workspace.Exercise) (*workspace.ExerciseMetadata, error) { metadata, err := workspace.NewExerciseMetadata(exercise.Filepath()) if err != nil { return nil, err @@ -230,7 +218,7 @@ func (s *submitCmdContext) _metadata(exercise workspace.Exercise) (*workspace.Ex return metadata, nil } -func (s *submitCmdContext) _documents(filepaths []string, exercise workspace.Exercise) ([]workspace.Document, error) { +func (s *submitCmdContext) documents(filepaths []string, exercise workspace.Exercise) ([]workspace.Document, error) { docs := make([]workspace.Document, 0, len(filepaths)) for _, file := range filepaths { // Don't submit empty files @@ -276,36 +264,12 @@ func (s *submitCmdContext) _documents(filepaths []string, exercise workspace.Exe return docs, nil } -func (s *submitCmdContext) printResult() { - msg := ` - - Your solution has been submitted successfully. - %s -` - suffix := "View it at:\n\n " - if s.metadata.AutoApprove && s.metadata.Team == "" { - suffix = "You can complete the exercise and unlock the next core exercise at:\n" - } - fmt.Fprintf(Err, msg, suffix) - fmt.Fprintf(Out, " %s\n\n", s.metadata.URL) -} - -// submission is a submission to the Excercism API. -type submission struct { - documents []workspace.Document - metadata *workspace.ExerciseMetadata -} - // submit submits the documents to the Exercism API. -func (s *submission) submit(usrCfg *viper.Viper) error { - if err := s.validate(); err != nil { - return err - } - +func (s *submitCmdContext) submit(metadata *workspace.ExerciseMetadata, docs []workspace.Document) error { body := &bytes.Buffer{} writer := multipart.NewWriter(body) - for _, doc := range s.documents { + for _, doc := range docs { file, err := os.Open(doc.Filepath()) if err != nil { return err @@ -325,11 +289,11 @@ func (s *submission) submit(usrCfg *viper.Viper) error { return err } - client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl")) + client, err := api.NewClient(s.usrCfg.GetString("token"), s.usrCfg.GetString("apibaseurl")) if err != nil { return err } - url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), s.metadata.ID) + url := fmt.Sprintf("%s/solutions/%s", s.usrCfg.GetString("apibaseurl"), metadata.ID) req, err := client.NewRequest("PATCH", url, body) if err != nil { return err @@ -359,17 +323,18 @@ func (s *submission) submit(usrCfg *viper.Viper) error { return nil } -func (s *submission) validate() error { - if s == nil { - return errors.New("submission is empty") - } - if s.metadata.ID == "" { - return errors.New("id is empty") - } - if len(s.documents) == 0 { - return errors.New("documents is empty") +func (s *submitCmdContext) printResult(metadata *workspace.ExerciseMetadata) { + msg := ` + + Your solution has been submitted successfully. + %s +` + suffix := "View it at:\n\n " + if metadata.AutoApprove && metadata.Team == "" { + suffix = "You can complete the exercise and unlock the next core exercise at:\n" } - return nil + fmt.Fprintf(Err, msg, suffix) + fmt.Fprintf(Out, " %s\n\n", metadata.URL) } func init() { From 5266925ee060dd20a95acdd2f9c284540e2d37bc Mon Sep 17 00:00:00 2001 From: John Goff Date: Thu, 3 Jan 2019 12:12:38 -0500 Subject: [PATCH 354/544] label common timeout values --- shell/exercism.fish | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shell/exercism.fish b/shell/exercism.fish index f246ff4a6..dc20fdce0 100644 --- a/shell/exercism.fish +++ b/shell/exercism.fish @@ -45,5 +45,10 @@ complete -f -c exercism -n "__fish_seen_subcommand_from workspace" -s h -l help # Options complete -f -c exercism -s h -l help -d "show help" -complete -f -c exercism -l timeout -a "(seq 0 1000 10000)" -d "override default HTTP timeout" +complete -f -c exercism -l timeout -a "10" -d "10 seconds" +complete -f -c exercism -l timeout -a "30" -d "30 seconds" +complete -f -c exercism -l timeout -a "60" -d "1 minute" +complete -f -c exercism -l timeout -a "300" -d "5 minutes" +complete -f -c exercism -l timeout -a "600" -d "10 minutes" +complete -f -c exercism -l timeout -a "" -d "override default HTTP timeout" complete -f -c exercism -s v -l verbose -d "turn on verbose logging" From 30231094cbc3708688738ba1d653e36375649daf Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Sat, 5 Jan 2019 11:32:46 +0700 Subject: [PATCH 355/544] Extract validations to new submitValidator type This separates validations from the getter-like methods for a better separations of concerns. Each rule is explicitly defined under submitValidator. --- cmd/submit.go | 328 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 208 insertions(+), 120 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 4ab2006ec..1f32bda21 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -55,14 +55,22 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - ctx := &submitCmdContext{usrCfg: cfg.UserViperConfig, flags: flags} + ctx := newSubmitCmdContext(cfg.UserViperConfig, flags) - filepaths, err := ctx.sanitizeArgs(args) + if err := ctx.validator.filesExistAndNotADir(args); err != nil { + return err + } + + submitPaths, err := ctx.evaluatedSymlinks(args) if err != nil { return err } - exercise, err := ctx.exercise(filepaths) + if err = ctx.validator.filesBelongToSameExercise(submitPaths); err != nil { + return err + } + + exercise, err := ctx.exercise(submitPaths) if err != nil { return err } @@ -71,16 +79,32 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - documents, err := ctx.documents(filepaths, exercise) + if err = ctx.validator.fileSizesWithinMax(submitPaths); err != nil { + return err + } + + documents, err := ctx.documents(submitPaths, exercise) if err != nil { return err } + if err = ctx.validator.submissionNotEmpty(documents); err != nil { + return err + } + metadata, err := ctx.metadata(exercise) if err != nil { return err } + if err := ctx.validator.metadataMatchesExercise(metadata, exercise); err != nil { + return err + } + + if err := ctx.validator.isRequestor(metadata); err != nil { + return err + } + if err := ctx.submit(metadata, documents); err != nil { return err } @@ -89,87 +113,51 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return nil } -// submitCmdContext represents the context for the submit cmd. type submitCmdContext struct { - usrCfg *viper.Viper - flags *pflag.FlagSet + usrCfg *viper.Viper + flags *pflag.FlagSet + validator submitValidator +} + +func newSubmitCmdContext(usrCfg *viper.Viper, flags *pflag.FlagSet) *submitCmdContext { + return &submitCmdContext{ + usrCfg: usrCfg, + flags: flags, + validator: submitValidator{usrCfg: usrCfg}, + } } -// sanitizeArgs validates args and swaps with evaluated symlink paths. -func (s *submitCmdContext) sanitizeArgs(args []string) ([]string, error) { - for i, arg := range args { +// evaluatedSymlinks returns a slice of submit paths where each path's symlink has been evaluated. +func (s *submitCmdContext) evaluatedSymlinks(submitPaths []string) ([]string, error) { + evalSymlinkSubmitPaths := make([]string, 0, len(submitPaths)) + for _, path := range submitPaths { var err error - arg, err = filepath.Abs(arg) + path, err = filepath.Abs(path) if err != nil { return nil, err } - info, err := os.Lstat(arg) - if err != nil { - if os.IsNotExist(err) { - msg := ` - - The file you are trying to submit cannot be found. - - %s - - ` - return nil, fmt.Errorf(msg, arg) - } - return nil, err - } - if info.IsDir() { - msg := ` - - You are submitting a directory, which is not currently supported. - - %s - - Please change into the directory and provide the path to the file(s) you wish to submit - - %s submit FILENAME - - ` - return nil, fmt.Errorf(msg, arg, BinaryName) - } - - src, err := filepath.EvalSymlinks(arg) + src, err := filepath.EvalSymlinks(path) if err != nil { return nil, err } - args[i] = src + evalSymlinkSubmitPaths = append(evalSymlinkSubmitPaths, src) } - return args, nil + return evalSymlinkSubmitPaths, nil } -func (s *submitCmdContext) exercise(filepaths []string) (workspace.Exercise, error) { +// exercise returns an Exercise using the directory of the submitted paths. +func (s *submitCmdContext) exercise(submitPaths []string) (workspace.Exercise, error) { ws, err := workspace.New(s.usrCfg.GetString("workspace")) if err != nil { return workspace.Exercise{}, err } - var exerciseDir string - for _, f := range filepaths { - dir, err := ws.ExerciseDir(f) - if err != nil { - if workspace.IsMissingMetadata(err) { - return workspace.Exercise{}, errors.New(msgMissingMetadata) - } - return workspace.Exercise{}, err - } - if exerciseDir != "" && dir != exerciseDir { - msg := ` - - You are submitting files belonging to different solutions. - Please submit the files for one solution at a time. - - ` - return workspace.Exercise{}, errors.New(msg) - } - exerciseDir = dir + dir, err := ws.ExerciseDir(submitPaths[0]) + if err != nil { + return workspace.Exercise{}, err } - - return workspace.NewExerciseFromDir(exerciseDir), nil + return workspace.NewExerciseFromDir(dir), nil } func (s *submitCmdContext) migrateLegacyMetadata(exercise workspace.Exercise) error { @@ -183,59 +171,16 @@ func (s *submitCmdContext) migrateLegacyMetadata(exercise workspace.Exercise) er return nil } -func (s *submitCmdContext) metadata(exercise workspace.Exercise) (*workspace.ExerciseMetadata, error) { - metadata, err := workspace.NewExerciseMetadata(exercise.Filepath()) - if err != nil { - return nil, err - } - - if metadata.Exercise != exercise.Slug { - // TODO: error msg should suggest running future doctor command - msg := ` - - The exercise directory does not match exercise slug in metadata: - - expected '%[1]s' but got '%[2]s' - - Please rename the directory '%[1]s' to '%[2]s' and try again. - - ` - return nil, fmt.Errorf(msg, exercise.Slug, metadata.Exercise) - } - - if !metadata.IsRequester { - // TODO: add test - msg := ` - - The solution you are submitting is not connected to your account. - Please re-download the exercise to make sure it has the data it needs. - - %s download --exercise=%s --track=%s - - ` - return nil, fmt.Errorf(msg, BinaryName, metadata.Exercise, metadata.Track) - } - return metadata, nil -} - -func (s *submitCmdContext) documents(filepaths []string, exercise workspace.Exercise) ([]workspace.Document, error) { - docs := make([]workspace.Document, 0, len(filepaths)) - for _, file := range filepaths { +// documents returns a slice of documents to be submitted. +// empty files are skipped and a warning is printed. +func (s *submitCmdContext) documents(submitPaths []string, exercise workspace.Exercise) ([]workspace.Document, error) { + docs := make([]workspace.Document, 0, len(submitPaths)) + for _, file := range submitPaths { // Don't submit empty files info, err := os.Stat(file) if err != nil { return nil, err } - const maxFileSize int64 = 65535 - if info.Size() >= maxFileSize { - msg := ` - - The submitted file '%s' is larger than the max allowed file size of %d bytes. - Please reduce the size of the file and try again. - - ` - return nil, fmt.Errorf(msg, file, maxFileSize) - } if info.Size() == 0 { msg := ` @@ -253,15 +198,15 @@ func (s *submitCmdContext) documents(filepaths []string, exercise workspace.Exer } docs = append(docs, doc) } - if len(docs) == 0 { - msg := ` - - No files found to submit. + return docs, nil +} - ` - return nil, errors.New(msg) +func (s *submitCmdContext) metadata(exercise workspace.Exercise) (*workspace.ExerciseMetadata, error) { + metadata, err := workspace.NewExerciseMetadata(exercise.Filepath()) + if err != nil { + return nil, err } - return docs, nil + return metadata, nil } // submit submits the documents to the Exercism API. @@ -337,6 +282,149 @@ func (s *submitCmdContext) printResult(metadata *workspace.ExerciseMetadata) { fmt.Fprintf(Out, " %s\n\n", metadata.URL) } +// submitValidator contains the validation rules for a submission. +type submitValidator struct { + usrCfg *viper.Viper +} + +// filesExistAndNotADir checks that each file exists and is not a directory. +func (s submitValidator) filesExistAndNotADir(submitPaths []string) error { + for _, path := range submitPaths { + path, err := filepath.Abs(path) + if err != nil { + return err + } + + info, err := os.Lstat(path) + if err != nil { + if os.IsNotExist(err) { + msg := ` + + The file you are trying to submit cannot be found. + + %s + + ` + return fmt.Errorf(msg, path) + } + return err + } + if info.IsDir() { + msg := ` + + You are submitting a directory, which is not currently supported. + + %s + + Please change into the directory and provide the path to the file(s) you wish to submit + + %s submit FILENAME + + ` + return fmt.Errorf(msg, path, BinaryName) + } + } + return nil +} + +// filesBelongToSameExercise checks that each file belongs to the same exercise. +func (s submitValidator) filesBelongToSameExercise(submitPaths []string) error { + ws, err := workspace.New(s.usrCfg.GetString("workspace")) + if err != nil { + return err + } + + var exerciseDir string + for _, f := range submitPaths { + dir, err := ws.ExerciseDir(f) + if err != nil { + if workspace.IsMissingMetadata(err) { + return errors.New(msgMissingMetadata) + } + return err + } + if exerciseDir != "" && dir != exerciseDir { + msg := ` + + You are submitting files belonging to different solutions. + Please submit the files for one solution at a time. + + ` + return errors.New(msg) + } + exerciseDir = dir + } + return nil +} + +// fileSizesWithinMax checks that each file does not exceed the max allowed size. +func (s submitValidator) fileSizesWithinMax(submitPaths []string) error { + for _, file := range submitPaths { + info, err := os.Stat(file) + if err != nil { + return err + } + const maxFileSize int64 = 65535 + if info.Size() >= maxFileSize { + msg := ` + + The submitted file '%s' is larger than the max allowed file size of %d bytes. + Please reduce the size of the file and try again. + + ` + return fmt.Errorf(msg, file, maxFileSize) + } + } + return nil +} + +// submissionNotEmpty checks that there is at least one file to submit. +func (s submitValidator) submissionNotEmpty(docs []workspace.Document) error { + if len(docs) == 0 { + msg := ` + + No files found to submit. + + ` + return errors.New(msg) + } + return nil +} + +// metadataMatchesExercise checks that the metadata refers to the exercise being submitted. +func (s submitValidator) metadataMatchesExercise(metadata *workspace.ExerciseMetadata, exercise workspace.Exercise) error { + if metadata.Exercise != exercise.Slug { + // TODO: error msg should suggest running future doctor command + msg := ` + + The exercise directory does not match exercise slug in metadata: + + expected '%[1]s' but got '%[2]s' + + Please rename the directory '%[1]s' to '%[2]s' and try again. + + ` + return fmt.Errorf(msg, exercise.Slug, metadata.Exercise) + } + return nil +} + +// isRequestor checks that the submission requestor is listed as the author in the metadata. +func (s submitValidator) isRequestor(metadata *workspace.ExerciseMetadata) error { + if !metadata.IsRequester { + msg := ` + + The solution you are submitting is not connected to your account. + Please re-download the exercise to make sure it has the data it needs. + + %s download --exercise=%s --track=%s + + ` + return fmt.Errorf(msg, BinaryName, metadata.Exercise, metadata.Track) + } + return nil +} + func init() { RootCmd.AddCommand(submitCmd) } From a3c2bcedd5735df821eb7f806a78cc1c1a6f46b1 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Sat, 5 Jan 2019 14:54:25 +0700 Subject: [PATCH 356/544] exercise has tacit dep on filesBelongToSameExercise exercise() uses ws.ExerciseDir() to find the root dir of an exercise. Since ws.ExerciseDir requires a single path, this tacitly assumes that the submitted paths are all part of the same exercise. There is a validation for this called `filesBelongToSameExercise`. This isn't an issue from the point of view of `runSubmit` but it might be an issue if `exercise()` gets moved around in the future as this isn't explicitly stated. These changes attempt to make it more clear by stating it in the godoc. --- cmd/submit.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 1f32bda21..147d904ea 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -70,7 +70,7 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - exercise, err := ctx.exercise(submitPaths) + exercise, err := ctx.exercise(submitPaths[0]) if err != nil { return err } @@ -146,14 +146,15 @@ func (s *submitCmdContext) evaluatedSymlinks(submitPaths []string) ([]string, er return evalSymlinkSubmitPaths, nil } -// exercise returns an Exercise using the directory of the submitted paths. -func (s *submitCmdContext) exercise(submitPaths []string) (workspace.Exercise, error) { +// exercise creates an exercise using one of the submitted filepaths. +// This assumes prior verification that submit paths belong to the same exercise. +func (s *submitCmdContext) exercise(aSubmitPath string) (workspace.Exercise, error) { ws, err := workspace.New(s.usrCfg.GetString("workspace")) if err != nil { return workspace.Exercise{}, err } - dir, err := ws.ExerciseDir(submitPaths[0]) + dir, err := ws.ExerciseDir(aSubmitPath) if err != nil { return workspace.Exercise{}, err } From e98d45715ec58b2bc8447196bd469757630850f4 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Sun, 6 Jan 2019 19:06:53 +0700 Subject: [PATCH 357/544] godoc --- cmd/submit.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 147d904ea..614116039 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -127,7 +127,7 @@ func newSubmitCmdContext(usrCfg *viper.Viper, flags *pflag.FlagSet) *submitCmdCo } } -// evaluatedSymlinks returns a slice of submit paths where each path's symlink has been evaluated. +// evaluatedSymlinks returns the submit paths with evaluated symlinks. func (s *submitCmdContext) evaluatedSymlinks(submitPaths []string) ([]string, error) { evalSymlinkSubmitPaths := make([]string, 0, len(submitPaths)) for _, path := range submitPaths { @@ -172,8 +172,8 @@ func (s *submitCmdContext) migrateLegacyMetadata(exercise workspace.Exercise) er return nil } -// documents returns a slice of documents to be submitted. -// empty files are skipped and a warning is printed. +// documents builds the documents that get submitted. +// Empty files are skipped, printing a warning. func (s *submitCmdContext) documents(submitPaths []string, exercise workspace.Exercise) ([]workspace.Document, error) { docs := make([]workspace.Document, 0, len(submitPaths)) for _, file := range submitPaths { From 6f8b1becff670186e1e3ee4cac2b9998068a816f Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 21 Jan 2019 09:59:30 +0700 Subject: [PATCH 358/544] Fix panic when submit not given args Previously, submit panics when not given args due when trying to index an empty array: exercise, err := ctx.exercise(submitPaths[0]) --- cmd/submit.go | 3 +++ cmd/submit_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/cmd/submit.go b/cmd/submit.go index 614116039..7fbc235b2 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -290,6 +290,9 @@ type submitValidator struct { // filesExistAndNotADir checks that each file exists and is not a directory. func (s submitValidator) filesExistAndNotADir(submitPaths []string) error { + if len(submitPaths) == 0 { + return fmt.Errorf("usage: %s submit FILE1 [FILE2 ...]", BinaryName) + } for _, path := range submitPaths { path, err := filepath.Abs(path) if err != nil { diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 4dc9b2b1d..6a8ba3908 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -47,6 +47,30 @@ func TestSubmitWithoutWorkspace(t *testing.T) { } } +func TestSubmitWithoutArgs(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "submit-no-args") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", "http://api.example.com") + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: "http://example.com", + } + + var noCLIArguments []string + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), noCLIArguments) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "usage") + } +} + func TestSubmitNonExistentFile(t *testing.T) { tmpDir, err := ioutil.TempDir("", "submit-no-such-file") defer os.RemoveAll(tmpDir) From e0aec26203e49ecded39f79ed327277c7bb0c9d1 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 21 Jan 2019 10:26:35 +0700 Subject: [PATCH 359/544] Revert "Fix panic when submit not given args" This reverts commit 6f8b1becff670186e1e3ee4cac2b9998068a816f. --- cmd/submit.go | 3 --- cmd/submit_test.go | 24 ------------------------ 2 files changed, 27 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index 7fbc235b2..614116039 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -290,9 +290,6 @@ type submitValidator struct { // filesExistAndNotADir checks that each file exists and is not a directory. func (s submitValidator) filesExistAndNotADir(submitPaths []string) error { - if len(submitPaths) == 0 { - return fmt.Errorf("usage: %s submit FILE1 [FILE2 ...]", BinaryName) - } for _, path := range submitPaths { path, err := filepath.Abs(path) if err != nil { diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 6a8ba3908..4dc9b2b1d 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -47,30 +47,6 @@ func TestSubmitWithoutWorkspace(t *testing.T) { } } -func TestSubmitWithoutArgs(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "submit-no-args") - defer os.RemoveAll(tmpDir) - assert.NoError(t, err) - - v := viper.New() - v.Set("token", "abc123") - v.Set("workspace", tmpDir) - v.Set("apibaseurl", "http://api.example.com") - - cfg := config.Config{ - Persister: config.InMemoryPersister{}, - UserViperConfig: v, - DefaultBaseURL: "http://example.com", - } - - var noCLIArguments []string - - err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), noCLIArguments) - if assert.Error(t, err) { - assert.Contains(t, err.Error(), "usage") - } -} - func TestSubmitNonExistentFile(t *testing.T) { tmpDir, err := ioutil.TempDir("", "submit-no-such-file") defer os.RemoveAll(tmpDir) From 51f1668ac5e3580977abadfe0d583dbbd596f8c8 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 21 Jan 2019 10:27:10 +0700 Subject: [PATCH 360/544] Use built-in cobra validator to handle empty args --- cmd/submit.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/submit.go b/cmd/submit.go index 614116039..2f9fd9659 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -28,6 +28,7 @@ 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() From 70ee5185a065aaf943eeca4accff02464ebd5589 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sun, 10 Feb 2019 03:28:56 -0500 Subject: [PATCH 361/544] Make zsh completion work on $fpath. This allows the file to be placed, e.g., in a system-wide `site-functions` directory and just work automatically. --- shell/README.md | 14 ++++++++------ shell/exercism_completion.zsh | 4 +++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/shell/README.md b/shell/README.md index 35b1e623c..a1a2161f4 100644 --- a/shell/README.md +++ b/shell/README.md @@ -17,15 +17,17 @@ adding the following snippet: ### Zsh +Load up the completion by placing the `exercism_completion.zsh` somewhere on +your `$fpath` as `_exercism`. For example: + mkdir -p ~/.config/exercism - mv ../shell/exercism_completion.zsh ~/.config/exercism/exercism_completion.zsh + mv ../shell/exercism_completion.zsh ~/.config/exercism/_exercism -Load up the completion in your `.zshrc`, `.zsh_profile` or `.profile` by adding -the following snippet +and then add the directory to your `$fpath` in your `.zshrc`, `.zsh_profile` or +`.profile` before running `compinit`: - if [ -f ~/.config/exercism/exercism_completion.zsh ]; then - source ~/.config/exercism/exercism_completion.zsh - fi + export fpath=(~/.config/exercism $fpath) + autoload -U compinit && compinit #### Oh my Zsh diff --git a/shell/exercism_completion.zsh b/shell/exercism_completion.zsh index f21553064..2c1311240 100644 --- a/shell/exercism_completion.zsh +++ b/shell/exercism_completion.zsh @@ -1,3 +1,5 @@ +#compdef exercism + _exercism() { local curcontext="$curcontext" state line typeset -A opt_args @@ -33,4 +35,4 @@ _exercism() { esac } -compdef '_exercism' exercism +_exercism "$@" From 890edd978f560fdfcdb3a27f911fa77512f2f948 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sun, 24 Feb 2019 19:48:45 -0500 Subject: [PATCH 362/544] Remove extra function around zsh completion script. --- shell/exercism_completion.zsh | 62 ++++++++++++++++------------------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/shell/exercism_completion.zsh b/shell/exercism_completion.zsh index 2c1311240..424b24e67 100644 --- a/shell/exercism_completion.zsh +++ b/shell/exercism_completion.zsh @@ -1,38 +1,34 @@ #compdef exercism -_exercism() { - local curcontext="$curcontext" state line - typeset -A opt_args +local curcontext="$curcontext" state line +typeset -A opt_args - local -a options - options=(configure:"Writes config values to a JSON file." - download:"Downloads and saves a specified submission into the local system" - open:"Opens a browser to exercism.io for the specified submission." - submit:"Submits a new iteration to a problem on exercism.io." - troubleshoot:"Outputs useful debug information." - upgrade:"Upgrades to the latest available version." - version:"Outputs version information." - workspace:"Outputs the root directory for Exercism exercises." - help:"Shows a list of commands or help for one command") +local -a options +options=(configure:"Writes config values to a JSON file." + download:"Downloads and saves a specified submission into the local system" + open:"Opens a browser to exercism.io for the specified submission." + submit:"Submits a new iteration to a problem on exercism.io." + troubleshoot:"Outputs useful debug information." + upgrade:"Upgrades to the latest available version." + version:"Outputs version information." + workspace:"Outputs the root directory for Exercism exercises." + help:"Shows a list of commands or help for one command") - _arguments -s -S \ - {-h,--help}"[show help]" \ - {-t,--timeout}"[override default HTTP timeout]" \ - {-v,--verbose}"[turn on verbose logging]" \ - '(-): :->command' \ - '(-)*:: :->option-or-argument' \ - && return 0; +_arguments -s -S \ + {-h,--help}"[show help]" \ + {-t,--timeout}"[override default HTTP timeout]" \ + {-v,--verbose}"[turn on verbose logging]" \ + '(-): :->command' \ + '(-)*:: :->option-or-argument' \ + && return 0; - case $state in - (command) - _describe 'commands' options ;; - (option-or-argument) - case $words[1] in - s*) - _files - ;; - esac - esac -} - -_exercism "$@" +case $state in + (command) + _describe 'commands' options ;; + (option-or-argument) + case $words[1] in + s*) + _files + ;; + esac +esac From d431fd86dcb31bb84c31987917e7c0c0f0df11f4 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sun, 24 Feb 2019 19:57:12 -0500 Subject: [PATCH 363/544] doc: Use more idiomatic zsh fpath. --- shell/README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/shell/README.md b/shell/README.md index a1a2161f4..b4c1509ce 100644 --- a/shell/README.md +++ b/shell/README.md @@ -20,13 +20,13 @@ adding the following snippet: Load up the completion by placing the `exercism_completion.zsh` somewhere on your `$fpath` as `_exercism`. For example: - mkdir -p ~/.config/exercism - mv ../shell/exercism_completion.zsh ~/.config/exercism/_exercism + mkdir -p ~/.zsh/functions + mv ../shell/exercism_completion.zsh ~/.zsh/functions/_exercism and then add the directory to your `$fpath` in your `.zshrc`, `.zsh_profile` or `.profile` before running `compinit`: - export fpath=(~/.config/exercism $fpath) + export fpath=(~/.zsh/functions $fpath) autoload -U compinit && compinit @@ -39,4 +39,3 @@ If you are using the popular [oh-my-zsh](https://github.com/robbyrussell/oh-my-z Completions must go in the user defined `$fish_complete_path`. By default, this is `~/.config/fish/completions` mv ../shell/exercism.fish ~/.config/fish/exercism.fish - From 63c1d93924c6cb9d91cfc0b254ae551cefe9a334 Mon Sep 17 00:00:00 2001 From: ZapAnton Date: Mon, 25 Feb 2019 15:27:44 +0300 Subject: [PATCH 364/544] Initialized a new go module Via the following command from the project root: go mod init --- go.mod | 26 ++++++++++++++++++++++++++ go.sum | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 go.mod create mode 100644 go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..c0d986d0f --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module github.com/exercism/cli + +require ( + github.com/blang/semver v3.5.1+incompatible + github.com/davecgh/go-spew v1.1.0 + github.com/fsnotify/fsnotify v1.4.2 + github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e + github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf + github.com/inconshreveable/mousetrap v1.0.0 + github.com/magiconair/properties v1.7.3 + github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992 + github.com/pelletier/go-buffruneio v0.2.0 + github.com/pelletier/go-toml v1.0.0 + github.com/pmezard/go-difflib v1.0.0 + github.com/spf13/afero v0.0.0-20170217164146-9be650865eab + github.com/spf13/cast v1.1.0 + github.com/spf13/cobra v0.0.0-20170731170427-b26b538f6930 + github.com/spf13/jwalterweatherman v0.0.0-20170523133247-0efa5202c046 + github.com/spf13/pflag v1.0.0 + github.com/spf13/viper v0.0.0-20180507071007-15738813a09d + github.com/stretchr/testify v1.1.4 + golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7 + golang.org/x/sys v0.0.0-20170803140359-d8f5ea21b929 + golang.org/x/text v0.0.0-20170730040918-3bd178b88a81 + gopkg.in/yaml.v2 v2.0.0-20170721122051-25c4ec802a7d +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..0a9864259 --- /dev/null +++ b/go.sum @@ -0,0 +1,43 @@ +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.2 h1:v5tKwtf2hNhBV24eNYfQ5UmvFOGlOCmRqk7/P1olxtk= +github.com/fsnotify/fsnotify v1.4.2/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e h1:KJWs1uTCkN3E/J5ofCH9Pf8KKsibTFc3fv0CA9+WsVo= +github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8= +github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/magiconair/properties v1.7.3 h1:6AOjgCKyZFMG/1yfReDPDz3CJZPxnYk7DGmj2HtyF24= +github.com/magiconair/properties v1.7.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992 h1:W7VHAEVflA5/eTyRvQ53Lz5j8bhRd1myHZlI/IZFvbU= +github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA= +github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= +github.com/pelletier/go-toml v1.0.0 h1:QFDlmAXZrfPXEF6c9+15fMqhQIS3O0pxszhnk936vg4= +github.com/pelletier/go-toml v1.0.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/afero v0.0.0-20170217164146-9be650865eab h1:IVAbBHQR8rXL2Fc8Zba/lMF7KOnTi70lqdx91UTuAwQ= +github.com/spf13/afero v0.0.0-20170217164146-9be650865eab/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.1.0 h1:0Rhw4d6C8J9VPu6cjZLIhZ8+aAOHcDvGeKn+cq5Aq3k= +github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= +github.com/spf13/cobra v0.0.0-20170731170427-b26b538f6930 h1:uJND9FKkf5s8kdTQX1jDygtp/zV4BJQpYvOmXPCYWgc= +github.com/spf13/cobra v0.0.0-20170731170427-b26b538f6930/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/jwalterweatherman v0.0.0-20170523133247-0efa5202c046 h1:RpxSq53NruItMGgp6q5MsDYoZynisJgEpisQdWJ7PyM= +github.com/spf13/jwalterweatherman v0.0.0-20170523133247-0efa5202c046/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.0 h1:oaPbdDe/x0UncahuwiPxW1GYJyilRAdsPnq3e1yaPcI= +github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v0.0.0-20180507071007-15738813a09d h1:pIz+bbPLk78K39d3u77IlNpJvpS/f0ao8n3sdy82eCs= +github.com/spf13/viper v0.0.0-20180507071007-15738813a09d/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/stretchr/testify v1.1.4 h1:ToftOQTytwshuOSj6bDSolVUa3GINfJP/fg3OkkOzQQ= +github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7 h1:1Pw+ZX4dmGORIwGkTwnUr7RFuMhfpCYHXRZNF04XPYs= +golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sys v0.0.0-20170803140359-d8f5ea21b929 h1:M4VPQYSW/nB4Bcg1XMD4yW2sprnwerD3Kb6apRphtZw= +golang.org/x/sys v0.0.0-20170803140359-d8f5ea21b929/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.0.0-20170730040918-3bd178b88a81 h1:7aXI3TQ9sZ4JdDoIDGjxL6G2mQxlsPy9dySnJaL6Bdk= +golang.org/x/text v0.0.0-20170730040918-3bd178b88a81/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/yaml.v2 v2.0.0-20170721122051-25c4ec802a7d h1:2DX7x6HUDGZUyuEDAhUsQQNqkb1zvDyKTjVoTdzaEzo= +gopkg.in/yaml.v2 v2.0.0-20170721122051-25c4ec802a7d/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= From 7a114b3b10990b4ff5aa77fb5eb19296c03fa3c9 Mon Sep 17 00:00:00 2001 From: ZapAnton Date: Mon, 25 Feb 2019 15:30:37 +0300 Subject: [PATCH 365/544] Removed the 'Gopkg' files --- Gopkg.lock | 184 ----------------------------------------------------- Gopkg.toml | 50 --------------- 2 files changed, 234 deletions(-) delete mode 100644 Gopkg.lock delete mode 100644 Gopkg.toml diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index 91ab68269..000000000 --- a/Gopkg.lock +++ /dev/null @@ -1,184 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - name = "github.com/blang/semver" - packages = ["."] - revision = "2ee87856327ba09384cabd113bc6b5d174e9ec0f" - version = "v3.5.1" - -[[projects]] - name = "github.com/davecgh/go-spew" - packages = ["spew"] - revision = "346938d642f2ec3594ed81d874461961cd0faa76" - version = "v1.1.0" - -[[projects]] - name = "github.com/fsnotify/fsnotify" - packages = ["."] - revision = "629574ca2a5df945712d3079857300b5e4da0236" - version = "v1.4.2" - -[[projects]] - branch = "master" - name = "github.com/hashicorp/hcl" - packages = [ - ".", - "hcl/ast", - "hcl/parser", - "hcl/printer", - "hcl/scanner", - "hcl/strconv", - "hcl/token", - "json/parser", - "json/scanner", - "json/token" - ] - revision = "392dba7d905ed5d04a5794ba89f558b27e2ba1ca" - -[[projects]] - branch = "master" - name = "github.com/inconshreveable/go-update" - packages = [ - ".", - "internal/binarydist", - "internal/osext" - ] - revision = "8152e7eb6ccf8679a64582a66b78519688d156ad" - -[[projects]] - name = "github.com/inconshreveable/mousetrap" - packages = ["."] - revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" - version = "v1.0" - -[[projects]] - name = "github.com/magiconair/properties" - packages = ["."] - revision = "be5ece7dd465ab0765a9682137865547526d1dfb" - version = "v1.7.3" - -[[projects]] - branch = "master" - name = "github.com/mitchellh/mapstructure" - packages = ["."] - revision = "d0303fe809921458f417bcf828397a65db30a7e4" - -[[projects]] - name = "github.com/pelletier/go-buffruneio" - packages = ["."] - revision = "c37440a7cf42ac63b919c752ca73a85067e05992" - version = "v0.2.0" - -[[projects]] - name = "github.com/pelletier/go-toml" - packages = ["."] - revision = "5ccdfb18c776b740aecaf085c4d9a2779199c279" - version = "v1.0.0" - -[[projects]] - name = "github.com/pmezard/go-difflib" - packages = ["difflib"] - revision = "792786c7400a136282c1664665ae0a8db921c6c2" - version = "v1.0.0" - -[[projects]] - branch = "master" - name = "github.com/spf13/afero" - packages = [ - ".", - "mem" - ] - revision = "9be650865eab0c12963d8753212f4f9c66cdcf12" - -[[projects]] - name = "github.com/spf13/cast" - packages = ["."] - revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4" - version = "v1.1.0" - -[[projects]] - branch = "master" - name = "github.com/spf13/cobra" - packages = ["."] - revision = "b26b538f693051ac6518e65672de3144ce3fbedc" - -[[projects]] - branch = "master" - name = "github.com/spf13/jwalterweatherman" - packages = ["."] - revision = "0efa5202c04663c757d84f90f5219c1250baf94f" - -[[projects]] - name = "github.com/spf13/pflag" - packages = ["."] - revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" - version = "v1.0.0" - -[[projects]] - branch = "master" - name = "github.com/spf13/viper" - packages = ["."] - revision = "15738813a09db5c8e5b60a19d67d3f9bd38da3a4" - -[[projects]] - name = "github.com/stretchr/testify" - packages = ["assert"] - revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0" - version = "v1.1.4" - -[[projects]] - branch = "master" - name = "golang.org/x/net" - packages = [ - "html", - "html/atom", - "html/charset" - ] - revision = "f5079bd7f6f74e23c4d65efa0f4ce14cbd6a3c0f" - -[[projects]] - branch = "master" - name = "golang.org/x/sys" - packages = ["unix"] - revision = "d8f5ea21b9295e315e612b4bcf4bedea93454d4d" - -[[projects]] - branch = "master" - name = "golang.org/x/text" - packages = [ - "encoding", - "encoding/charmap", - "encoding/htmlindex", - "encoding/internal", - "encoding/internal/identifier", - "encoding/japanese", - "encoding/korean", - "encoding/simplifiedchinese", - "encoding/traditionalchinese", - "encoding/unicode", - "internal/gen", - "internal/tag", - "internal/triegen", - "internal/ucd", - "internal/utf8internal", - "language", - "runes", - "transform", - "unicode/cldr", - "unicode/norm" - ] - revision = "3bd178b88a8180be2df394a1fbb81313916f0e7b" - -[[projects]] - branch = "v2" - name = "gopkg.in/yaml.v2" - packages = ["."] - revision = "25c4ec802a7d637f88d584ab26798e94ad14c13b" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - inputs-digest = "158b0077955d6d252f0b332b791bc8fb744b7620871ee86018e115c41e111dc6" - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml deleted file mode 100644 index d97af6f18..000000000 --- a/Gopkg.toml +++ /dev/null @@ -1,50 +0,0 @@ - -# Gopkg.toml example -# -# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md -# for detailed Gopkg.toml documentation. -# -# required = ["github.com/user/thing/cmd/thing"] -# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] -# -# [[constraint]] -# name = "github.com/user/project" -# version = "1.0.0" -# -# [[constraint]] -# name = "github.com/user/project2" -# branch = "dev" -# source = "github.com/myfork/project2" -# -# [[override]] -# name = "github.com/x/y" -# version = "2.4.0" - - -[[constraint]] - name = "github.com/blang/semver" - version = "3.5.1" - -[[constraint]] - branch = "master" - name = "github.com/inconshreveable/go-update" - -[[constraint]] - branch = "master" - name = "github.com/spf13/cobra" - -[[constraint]] - branch = "master" - name = "github.com/spf13/viper" - -[[constraint]] - name = "github.com/stretchr/testify" - version = "1.1.4" - -[[constraint]] - branch = "master" - name = "golang.org/x/net" - -[[constraint]] - branch = "master" - name = "golang.org/x/text" From 38b9d07ac116e82dc9de3f12280f618190758110 Mon Sep 17 00:00:00 2001 From: ZapAnton Date: Mon, 25 Feb 2019 15:36:52 +0300 Subject: [PATCH 366/544] appveyor: Removed the go deps installation --- appveyor.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 1f3ddc3ca..114035734 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,6 +5,7 @@ clone_folder: c:\gopath\src\github.com\exercism\cli environment: GOPATH: c:\gopath + GO111MODULE: on init: - git config --global core.autocrlf input @@ -14,8 +15,6 @@ install: - echo %GOPATH% - go version - go env - - go get -u github.com/golang/dep/... - - c:\gopath\bin\dep.exe ensure build_script: - go test -cover ./... From ef52081d2d406a0d5984ebb3f7650988b2648bee Mon Sep 17 00:00:00 2001 From: ZapAnton Date: Mon, 25 Feb 2019 15:46:29 +0300 Subject: [PATCH 367/544] travis: Removed the go deps installation --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 282db173e..cb9eced50 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,6 @@ go: - "1.11.x" - tip -install: - - go get -u github.com/golang/dep/... - - dep ensure - script: - ./.travis.gofmt.sh - go test -cover ./... From 2f5ff0a717680b84ad07b14ccb7d70166c8833bc Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 26 Feb 2019 14:40:57 -0700 Subject: [PATCH 368/544] Fix typo in doc comment --- cmd/cmd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index e71679c1b..5f0c70284 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -56,7 +56,7 @@ const msgMissingMetadata = ` ` -// validateUserConfig validates the presense of required user config values +// validateUserConfig validates the presence of required user config values func validateUserConfig(cfg *viper.Viper) error { if cfg.GetString("token") == "" { return fmt.Errorf( From 8071dc29d476413594fa5dba08e170100129b94f Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Wed, 20 Feb 2019 17:11:13 +0700 Subject: [PATCH 369/544] Extract API error logic to helper The submit command handles API errors correctly. We need to be able to share this logic so we can stop swallowing errors in other commands. --- cmd/cmd.go | 20 ++++++++++++++++++++ cmd/submit.go | 15 +-------------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index 5f0c70284..36e6ef530 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -1,7 +1,9 @@ package cmd import ( + "encoding/json" "fmt" + "net/http" "io" @@ -70,3 +72,21 @@ func validateUserConfig(cfg *viper.Viper) error { } return nil } + +// decodedAPIError decodes and returns the error message from the API response. +// If the message is blank, it returns a fallback message with the status code. +func decodedAPIError(resp *http.Response) error { + var apiError struct { + Error struct { + Type string `json:"type"` + Message string `json:"message"` + } `json:"error,omitempty"` + } + if err := json.NewDecoder(resp.Body).Decode(&apiError); err != nil { + return fmt.Errorf("failed to parse API error response: %s", err) + } + if apiError.Error.Message != "" { + return fmt.Errorf(apiError.Error.Message) + } + return fmt.Errorf("unexpected API response: %d", resp.StatusCode) +} diff --git a/cmd/submit.go b/cmd/submit.go index 2f9fd9659..d52d83520 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -2,7 +2,6 @@ package cmd import ( "bytes" - "encoding/json" "errors" "fmt" "io" @@ -254,12 +253,7 @@ func (s *submitCmdContext) submit(metadata *workspace.ExerciseMetadata, docs []w defer resp.Body.Close() if resp.StatusCode == http.StatusBadRequest { - var jsonErrBody apiErrorMessage - if err := json.NewDecoder(resp.Body).Decode(&jsonErrBody); err != nil { - return fmt.Errorf("failed to parse error response - %s", err) - } - - return fmt.Errorf(jsonErrBody.Error.Message) + return decodedAPIError(resp) } bb := &bytes.Buffer{} @@ -430,10 +424,3 @@ func (s submitValidator) isRequestor(metadata *workspace.ExerciseMetadata) error func init() { RootCmd.AddCommand(submitCmd) } - -type apiErrorMessage struct { - Error struct { - Type string `json:"type"` - Message string `json:"message"` - } `json:"error,omitempty"` -} From 3e9d5fecc38fb2981789643b59853c52d232890d Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 26 Feb 2019 15:23:50 -0700 Subject: [PATCH 370/544] Rename Exercise field on metadata type We are going to want to move some behavior onto the ExerciseMetadata type, and the naming is going to crash. ExerciseSlug, while more verbose, is also more correct. --- cmd/download.go | 20 ++++---- cmd/download_test.go | 2 +- cmd/submit.go | 6 +-- cmd/submit_test.go | 30 ++++++------ workspace/exercise_metadata.go | 24 +++++----- workspace/exercise_metadata_test.go | 74 ++++++++++++++--------------- 6 files changed, 78 insertions(+), 78 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index 832019e33..0cd551783 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -130,14 +130,14 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { } metadata := workspace.ExerciseMetadata{ - AutoApprove: payload.Solution.Exercise.AutoApprove, - Track: payload.Solution.Exercise.Track.ID, - Team: payload.Solution.Team.Slug, - Exercise: payload.Solution.Exercise.ID, - ID: payload.Solution.ID, - URL: payload.Solution.URL, - Handle: payload.Solution.User.Handle, - IsRequester: payload.Solution.User.IsRequester, + AutoApprove: payload.Solution.Exercise.AutoApprove, + Track: payload.Solution.Exercise.Track.ID, + Team: payload.Solution.Team.Slug, + ExerciseSlug: payload.Solution.Exercise.ID, + ID: payload.Solution.ID, + URL: payload.Solution.URL, + Handle: payload.Solution.User.Handle, + IsRequester: payload.Solution.User.IsRequester, } root := usrCfg.GetString("workspace") @@ -151,7 +151,7 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { exercise := workspace.Exercise{ Root: root, Track: metadata.Track, - Slug: metadata.Exercise, + Slug: metadata.ExerciseSlug, } dir := exercise.MetadataDir() @@ -201,7 +201,7 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { // Work around a path bug due to an early design decision (later reversed) to // allow numeric suffixes for exercise directories, allowing people to have // multiple parallel versions of an exercise. - pattern := fmt.Sprintf(`\A.*[/\\]%s-\d*/`, metadata.Exercise) + pattern := fmt.Sprintf(`\A.*[/\\]%s-\d*/`, metadata.ExerciseSlug) rgxNumericSuffix := regexp.MustCompile(pattern) if rgxNumericSuffix.MatchString(file) { file = string(rgxNumericSuffix.ReplaceAll([]byte(file), []byte(""))) diff --git a/cmd/download_test.go b/cmd/download_test.go index 324f8733d..f76b4eca9 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -144,7 +144,7 @@ func TestDownload(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "bogus-track", metadata.Track) - assert.Equal(t, "bogus-exercise", metadata.Exercise) + assert.Equal(t, "bogus-exercise", metadata.ExerciseSlug) assert.Equal(t, tc.requester, metadata.IsRequester) } } diff --git a/cmd/submit.go b/cmd/submit.go index d52d83520..9f72b0a55 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -389,7 +389,7 @@ func (s submitValidator) submissionNotEmpty(docs []workspace.Document) error { // metadataMatchesExercise checks that the metadata refers to the exercise being submitted. func (s submitValidator) metadataMatchesExercise(metadata *workspace.ExerciseMetadata, exercise workspace.Exercise) error { - if metadata.Exercise != exercise.Slug { + if metadata.ExerciseSlug != exercise.Slug { // TODO: error msg should suggest running future doctor command msg := ` @@ -400,7 +400,7 @@ func (s submitValidator) metadataMatchesExercise(metadata *workspace.ExerciseMet Please rename the directory '%[1]s' to '%[2]s' and try again. ` - return fmt.Errorf(msg, exercise.Slug, metadata.Exercise) + return fmt.Errorf(msg, exercise.Slug, metadata.ExerciseSlug) } return nil } @@ -416,7 +416,7 @@ func (s submitValidator) isRequestor(metadata *workspace.ExerciseMetadata) error %s download --exercise=%s --track=%s ` - return fmt.Errorf(msg, BinaryName, metadata.Exercise, metadata.Track) + return fmt.Errorf(msg, BinaryName, metadata.ExerciseSlug, metadata.Track) } return nil } diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 4dc9b2b1d..7556ab0c3 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -218,11 +218,11 @@ func TestLegacyMetadataMigration(t *testing.T) { os.MkdirAll(dir, os.FileMode(0755)) metadata := &workspace.ExerciseMetadata{ - ID: "bogus-solution-uuid", - Track: "bogus-track", - Exercise: "bogus-exercise", - URL: "http://example.com/bogus-url", - IsRequester: true, + ID: "bogus-solution-uuid", + Track: "bogus-track", + ExerciseSlug: "bogus-exercise", + URL: "http://example.com/bogus-url", + IsRequester: true, } b, err := json.Marshal(metadata) assert.NoError(t, err) @@ -585,11 +585,11 @@ func TestSubmissionNotConnectedToRequesterAccount(t *testing.T) { os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) metadata := &workspace.ExerciseMetadata{ - ID: "bogus-solution-uuid", - Track: "bogus-track", - Exercise: "bogus-exercise", - URL: "http://example.com/bogus-url", - IsRequester: false, + ID: "bogus-solution-uuid", + Track: "bogus-track", + ExerciseSlug: "bogus-exercise", + URL: "http://example.com/bogus-url", + IsRequester: false, } err = metadata.Write(dir) assert.NoError(t, err) @@ -651,11 +651,11 @@ func TestExerciseDirnameMatchesMetadataSlug(t *testing.T) { func writeFakeMetadata(t *testing.T, dir, trackID, exerciseSlug string) { metadata := &workspace.ExerciseMetadata{ - ID: "bogus-solution-uuid", - Track: trackID, - Exercise: exerciseSlug, - URL: "http://example.com/bogus-url", - IsRequester: true, + ID: "bogus-solution-uuid", + Track: trackID, + ExerciseSlug: exerciseSlug, + URL: "http://example.com/bogus-url", + IsRequester: true, } err := metadata.Write(dir) assert.NoError(t, err) diff --git a/workspace/exercise_metadata.go b/workspace/exercise_metadata.go index 48d6c9347..dbfda9290 100644 --- a/workspace/exercise_metadata.go +++ b/workspace/exercise_metadata.go @@ -18,16 +18,16 @@ var metadataFilepath = filepath.Join(ignoreSubdir, metadataFilename) // ExerciseMetadata contains metadata about a user's exercise. type ExerciseMetadata struct { - Track string `json:"track"` - Exercise string `json:"exercise"` - ID string `json:"id"` - Team string `json:"team,omitempty"` - URL string `json:"url"` - Handle string `json:"handle"` - IsRequester bool `json:"is_requester"` - SubmittedAt *time.Time `json:"submitted_at,omitempty"` - Dir string `json:"-"` - AutoApprove bool `json:"auto_approve"` + Track string `json:"track"` + ExerciseSlug string `json:"exercise"` + ID string `json:"id"` + Team string `json:"team,omitempty"` + URL string `json:"url"` + Handle string `json:"handle"` + IsRequester bool `json:"is_requester"` + SubmittedAt *time.Time `json:"submitted_at,omitempty"` + Dir string `json:"-"` + AutoApprove bool `json:"auto_approve"` } // NewExerciseMetadata reads exercise metadata from a file in the given directory. @@ -48,11 +48,11 @@ func NewExerciseMetadata(dir string) (*ExerciseMetadata, error) { // This is appended to avoid name conflicts, and does not indicate a particular // iteration. func (em *ExerciseMetadata) Suffix() string { - return strings.Trim(strings.Replace(filepath.Base(em.Dir), em.Exercise, "", 1), "-.") + return strings.Trim(strings.Replace(filepath.Base(em.Dir), em.ExerciseSlug, "", 1), "-.") } func (em *ExerciseMetadata) String() string { - str := fmt.Sprintf("%s/%s", em.Track, em.Exercise) + str := fmt.Sprintf("%s/%s", em.Track, em.ExerciseSlug) if em.Suffix() != "" { str = fmt.Sprintf("%s (%s)", str, em.Suffix()) } diff --git a/workspace/exercise_metadata_test.go b/workspace/exercise_metadata_test.go index 75fe819d8..e05ffeac3 100644 --- a/workspace/exercise_metadata_test.go +++ b/workspace/exercise_metadata_test.go @@ -15,13 +15,13 @@ func TestExerciseMetadata(t *testing.T) { defer os.RemoveAll(dir) em1 := &ExerciseMetadata{ - Track: "a-track", - Exercise: "bogus-exercise", - ID: "abc", - URL: "http://example.com", - Handle: "alice", - IsRequester: true, - Dir: dir, + Track: "a-track", + ExerciseSlug: "bogus-exercise", + ID: "abc", + URL: "http://example.com", + Handle: "alice", + IsRequester: true, + Dir: dir, } err = em1.Write(dir) assert.NoError(t, err) @@ -49,29 +49,29 @@ func TestSuffix(t *testing.T) { }{ { metadata: ExerciseMetadata{ - Exercise: "bat", - Dir: "", + ExerciseSlug: "bat", + Dir: "", }, suffix: "", }, { metadata: ExerciseMetadata{ - Exercise: "bat", - Dir: "/path/to/bat", + ExerciseSlug: "bat", + Dir: "/path/to/bat", }, suffix: "", }, { metadata: ExerciseMetadata{ - Exercise: "bat", - Dir: "/path/to/bat-2", + ExerciseSlug: "bat", + Dir: "/path/to/bat-2", }, suffix: "2", }, { metadata: ExerciseMetadata{ - Exercise: "bat", - Dir: "/path/to/bat-200", + ExerciseSlug: "bat", + Dir: "/path/to/bat-200", }, suffix: "200", }, @@ -92,48 +92,48 @@ func TestExerciseMetadataString(t *testing.T) { }{ { metadata: ExerciseMetadata{ - Track: "elixir", - Exercise: "secret-handshake", - Handle: "", - Dir: "", + Track: "elixir", + ExerciseSlug: "secret-handshake", + Handle: "", + Dir: "", }, desc: "elixir/secret-handshake", }, { metadata: ExerciseMetadata{ - Track: "cpp", - Exercise: "clock", - Handle: "alice", - IsRequester: true, + Track: "cpp", + ExerciseSlug: "clock", + Handle: "alice", + IsRequester: true, }, desc: "cpp/clock", }, { metadata: ExerciseMetadata{ - Track: "cpp", - Exercise: "clock", - Handle: "alice", - IsRequester: true, - Dir: "/path/to/clock-2", + Track: "cpp", + ExerciseSlug: "clock", + Handle: "alice", + IsRequester: true, + Dir: "/path/to/clock-2", }, desc: "cpp/clock (2)", }, { metadata: ExerciseMetadata{ - Track: "fsharp", - Exercise: "hello-world", - Handle: "bob", - IsRequester: false, + Track: "fsharp", + ExerciseSlug: "hello-world", + Handle: "bob", + IsRequester: false, }, desc: "fsharp/hello-world by @bob", }, { metadata: ExerciseMetadata{ - Track: "haskell", - Exercise: "allergies", - Handle: "charlie", - IsRequester: false, - Dir: "/path/to/allergies-2", + Track: "haskell", + ExerciseSlug: "allergies", + Handle: "charlie", + IsRequester: false, + Dir: "/path/to/allergies-2", }, desc: "haskell/allergies (2) by @charlie", }, From c6a395d9d67552e4fbf905ea7c0c5fb11c087493 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 26 Feb 2019 15:08:02 -0700 Subject: [PATCH 371/544] Enhance downloadPayload with helper to return exercise metadata co-authored-by: Jeff Sutherland --- cmd/download.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index 0cd551783..707589f65 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -129,16 +129,7 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { } } - metadata := workspace.ExerciseMetadata{ - AutoApprove: payload.Solution.Exercise.AutoApprove, - Track: payload.Solution.Exercise.Track.ID, - Team: payload.Solution.Team.Slug, - ExerciseSlug: payload.Solution.Exercise.ID, - ID: payload.Solution.ID, - URL: payload.Solution.URL, - Handle: payload.Solution.User.Handle, - IsRequester: payload.Solution.User.IsRequester, - } + metadata := payload.metadata() root := usrCfg.GetString("workspace") if metadata.Team != "" { @@ -263,6 +254,19 @@ type downloadPayload struct { } `json:"error,omitempty"` } +func (dp downloadPayload) metadata() workspace.ExerciseMetadata { + return workspace.ExerciseMetadata{ + AutoApprove: dp.Solution.Exercise.AutoApprove, + Track: dp.Solution.Exercise.Track.ID, + Team: dp.Solution.Team.Slug, + ExerciseSlug: dp.Solution.Exercise.ID, + ID: dp.Solution.ID, + URL: dp.Solution.URL, + Handle: dp.Solution.User.Handle, + IsRequester: dp.Solution.User.IsRequester, + } +} + func setupDownloadFlags(flags *pflag.FlagSet) { flags.StringP("uuid", "u", "", "the solution UUID") flags.StringP("track", "t", "", "the track ID") From c1fac1031a3ebe425172e0da8d67db6d7253e8f4 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 26 Feb 2019 16:22:36 -0700 Subject: [PATCH 372/544] Create workspace exercise from metadata Enhance the ExerciseMetadata type to have a method that creates an exercise given the path to the user's workspace directory. --- workspace/exercise_metadata.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/workspace/exercise_metadata.go b/workspace/exercise_metadata.go index dbfda9290..0a0305576 100644 --- a/workspace/exercise_metadata.go +++ b/workspace/exercise_metadata.go @@ -87,3 +87,23 @@ func (em *ExerciseMetadata) PathToParent() string { } return filepath.Join(dir, em.Track) } + +// Exercise is an implementation of a problem on disk. +func (em *ExerciseMetadata) Exercise(workspace string) Exercise { + return Exercise{ + Root: em.root(workspace), + Track: em.Track, + Slug: em.ExerciseSlug, + } +} + +// root represents the root of the exercise. +func (em *ExerciseMetadata) root(workspace string) string { + if em.Team != "" { + return filepath.Join(workspace, "teams", em.Team) + } + if !em.IsRequester { + return filepath.Join(workspace, "users", em.Handle) + } + return workspace +} From 3700417d551ea71fca80d6f6477a062cababe7fc Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 26 Feb 2019 16:22:59 -0700 Subject: [PATCH 373/544] Refactor download to use new workspace behavior --- cmd/download.go | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index 707589f65..f33d3c7c1 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -130,22 +130,7 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { } metadata := payload.metadata() - - root := usrCfg.GetString("workspace") - if metadata.Team != "" { - root = filepath.Join(root, "teams", metadata.Team) - } - if !metadata.IsRequester { - root = filepath.Join(root, "users", metadata.Handle) - } - - exercise := workspace.Exercise{ - Root: root, - Track: metadata.Track, - Slug: metadata.ExerciseSlug, - } - - dir := exercise.MetadataDir() + dir := metadata.Exercise(usrCfg.GetString("workspace")).MetadataDir() if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { return err From 7c6cd9db88541d870f304f63f138d21604467c47 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 26 Feb 2019 16:49:14 -0700 Subject: [PATCH 374/544] Extract logic for the URI variable identifying the exercise to download There is very likely a better abstraction hiding close by. This is a minimal, cohesive change, which I think is fairly uncontroversial. If we do find the better abstraction, it's likely that we'll be able to move this one to it wholesale. I've left a bit of duplication in place for now, rather than make a big guess about the abstraction. --- cmd/download.go | 47 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index f33d3c7c1..4a60b61e9 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -54,44 +54,40 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - uuid, err := flags.GetString("uuid") + track, err := flags.GetString("track") if err != nil { return err } - slug, err := flags.GetString("exercise") + + team, err := flags.GetString("team") if err != nil { return err } - if uuid != "" && slug != "" || uuid == slug { - return errors.New("need an --exercise name or a solution --uuid") - } - track, err := flags.GetString("track") + identifier, err := downloadIdentifier(flags) if err != nil { return err } + url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), identifier) - team, err := flags.GetString("team") + client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl")) if err != nil { return err } - param := "latest" - if uuid != "" { - param = uuid + req, err := client.NewRequest("GET", url, nil) + if err != nil { + return err } - url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), param) - client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl")) + uuid, err := flags.GetString("uuid") if err != nil { return err } - - req, err := client.NewRequest("GET", url, nil) + slug, err := flags.GetString("exercise") if err != nil { return err } - if uuid == "" { q := req.URL.Query() q.Add("exercise_id", slug) @@ -252,6 +248,27 @@ func (dp downloadPayload) metadata() workspace.ExerciseMetadata { } } +// downloadIdentifier is the variable for the URI to initiate an exercise download. +func downloadIdentifier(flags *pflag.FlagSet) (string, error) { + uuid, err := flags.GetString("uuid") + if err != nil { + return "", err + } + slug, err := flags.GetString("exercise") + if err != nil { + return "", err + } + if uuid != "" && slug != "" || uuid == slug { + return "", errors.New("need an --exercise name or a solution --uuid") + } + + identifier := "latest" + if uuid != "" { + identifier = uuid + } + return identifier, nil +} + func setupDownloadFlags(flags *pflag.FlagSet) { flags.StringP("uuid", "u", "", "the solution UUID") flags.StringP("track", "t", "", "the track ID") From 7513bcb245285b4e9aac5d72be02b1b291c4631d Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 26 Feb 2019 17:34:52 -0700 Subject: [PATCH 375/544] Extract helper for download request query params There's a proper abstraction lurking close by, but I'm not convinced I'm seeing it. Instead of jumping to conclusions, this extracts a cohesive helper method which can be moved onto the abstraction once it's defined. --- cmd/download.go | 60 ++++++++++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index 4a60b61e9..ca8bda2d3 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -54,16 +54,6 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - track, err := flags.GetString("track") - if err != nil { - return err - } - - team, err := flags.GetString("team") - if err != nil { - return err - } - identifier, err := downloadIdentifier(flags) if err != nil { return err @@ -80,25 +70,10 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - uuid, err := flags.GetString("uuid") + req, err = addQueryToDownloadRequest(flags, req) if err != nil { return err } - slug, err := flags.GetString("exercise") - if err != nil { - return err - } - if uuid == "" { - q := req.URL.Query() - q.Add("exercise_id", slug) - if track != "" { - q.Add("track_id", track) - } - if team != "" { - q.Add("team_id", team) - } - req.URL.RawQuery = q.Encode() - } res, err := client.Do(req) if err != nil { @@ -269,6 +244,39 @@ func downloadIdentifier(flags *pflag.FlagSet) (string, error) { return identifier, nil } +func addQueryToDownloadRequest(flags *pflag.FlagSet, req *http.Request) (*http.Request, error) { + uuid, err := flags.GetString("uuid") + if err != nil { + return req, err + } + slug, err := flags.GetString("exercise") + if err != nil { + return req, err + } + track, err := flags.GetString("track") + if err != nil { + return req, err + } + + team, err := flags.GetString("team") + if err != nil { + return req, err + } + + if uuid == "" { + q := req.URL.Query() + q.Add("exercise_id", slug) + if track != "" { + q.Add("track_id", track) + } + if team != "" { + q.Add("team_id", team) + } + req.URL.RawQuery = q.Encode() + } + return req, nil +} + func setupDownloadFlags(flags *pflag.FlagSet) { flags.StringP("uuid", "u", "", "the solution UUID") flags.StringP("track", "t", "", "the track ID") From 8da44ec7558361439283007ea485a7b537cc577f Mon Sep 17 00:00:00 2001 From: ZapAnton Date: Wed, 27 Feb 2019 10:42:52 +0300 Subject: [PATCH 376/544] CONTRIBUTING: Updated the doc to match the go 'modules' development workflow. --- CONTRIBUTING.md | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b2eaf204..3dbcbd5d3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,33 +10,28 @@ Exercism would be impossible without people like you being willing to spend time ## Dependencies -You'll need Go version 1.10 or higher. Follow the directions on http://golang.org/doc/install +You'll need Go version 1.11 or higher. Follow the directions on http://golang.org/doc/install -You will also need `dep`, the Go dependency management tool. Follow the directions on https://golang.github.io/dep/docs/installation.html +You will also need to be familiar with the Go `modules` dependency management system. Refer to the [modules wiki page](https://github.com/golang/go/wiki/Modules) to learn more. ## Development -If you've never contributed to a Go project before this is going to feel a little bit foreign. - -The TL;DR is: **don't clone your fork**, and it matters where on your filesystem the project gets cloned to. - -If you don't care how and why and just want something that works, follow these steps: +A typical development workflow looks like this: 1. [fork this repo on the GitHub webpage][fork] -1. `go get github.com/exercism/cli/exercism` -1. `cd $GOPATH/src/github.com/exercism/cli` (or `cd %GOPATH%\src\github.com\exercism\cli` on Windows) -1. `git remote rename origin upstream` -1. `git remote add origin git@github.com:/cli.git` -1. `git checkout -b development` -1. `git push -u origin development` (setup where you push to, check it works) -1. `go get -u github.com/golang/dep/cmd/dep` - * depending on your setup, you may need to install `dep` by following the instructions in the [`dep` repo](https://github.com/golang/dep) -1. `dep ensure` -1. `git update-index --assume-unchanged Gopkg.lock` (prevent your dep changes being committed) +1. `cd /path/to/the/development/directory` +1. `git clone https://github.com//cli.git` +1. `cd cli` +1. `git remote add upstream https://github.com/exercism/cli.git` +1. Optionally: `git config user.name ` and `git config user.email ` +1. `git checkout -b ` +1. `git push -u origin ` (setup where you push to, check it works) + +Then make your desired changes and submit a pull request. Please provide tests for the changes where possible. -Then make changes as usual and submit a pull request. Please provide tests for the changes where possible. +Please note that if your development directory is located inside the `GOPATH`, you would need to set the `GO111MODULE=on` environment variable, in order to be able to use the `modules` system. -If you care about the details, check out the blog post [Contributing to Open Source Repositories in Go][contrib-blog] on the Splice blog. +If you wish to learn how to contribute to the Go projects without the `modules`, check out the blog post [Contributing to Open Source Repositories in Go][contrib-blog] on the Splice blog. ## Running the Tests @@ -56,12 +51,12 @@ damaging your real Exercism submissions, or test different tokens, etc. On Unices: -- `cd $GOPATH/src/github.com/exercism/cli/exercism && go build -o testercism main.go` +- `cd /path/to/the/development/directory/cli && go build -o testercism main.go` - `./testercism -h` On Windows: -- `cd /d %GOPATH%\src\github.com\exercism\cli` +- `cd /d \path\to\the\development\directory\cli` - `go build -o testercism.exe exercism\main.go` - `testercism.exe —h` From 6fde6d5a6d603e4e7c6f5a50a275e9e0e53bd96c Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Wed, 27 Feb 2019 22:03:42 -0700 Subject: [PATCH 377/544] Simplify development instructions --- CONTRIBUTING.md | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3dbcbd5d3..e170e9da2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,26 +12,15 @@ Exercism would be impossible without people like you being willing to spend time You'll need Go version 1.11 or higher. Follow the directions on http://golang.org/doc/install -You will also need to be familiar with the Go `modules` dependency management system. Refer to the [modules wiki page](https://github.com/golang/go/wiki/Modules) to learn more. - ## Development -A typical development workflow looks like this: - -1. [fork this repo on the GitHub webpage][fork] -1. `cd /path/to/the/development/directory` -1. `git clone https://github.com//cli.git` -1. `cd cli` -1. `git remote add upstream https://github.com/exercism/cli.git` -1. Optionally: `git config user.name ` and `git config user.email ` -1. `git checkout -b ` -1. `git push -u origin ` (setup where you push to, check it works) - -Then make your desired changes and submit a pull request. Please provide tests for the changes where possible. +This project uses Go's [`modules` dependency management](https://github.com/golang/go/wiki/Modules) system. -Please note that if your development directory is located inside the `GOPATH`, you would need to set the `GO111MODULE=on` environment variable, in order to be able to use the `modules` system. +To contribute [fork this repo on the GitHub webpage][fork] and clone your fork. +Make your desired changes and submit a pull request. +Please provide tests for the changes where possible. -If you wish to learn how to contribute to the Go projects without the `modules`, check out the blog post [Contributing to Open Source Repositories in Go][contrib-blog] on the Splice blog. +Please note that if your development directory is located inside the `GOPATH`, you need to set the `GO111MODULE=on` environment variable. ## Running the Tests @@ -66,4 +55,3 @@ In order to cross-compile for all platforms, run `bin/build-all`. The binaries will be built into the `release` directory. [fork]: https://github.com/exercism/cli/fork -[contrib-blog]: https://splice.com/blog/contributing-open-source-git-repositories-go/ From d5ffa730e5e574ffd989805b9437d029275c1719 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Thu, 28 Feb 2019 13:40:24 +0700 Subject: [PATCH 378/544] Add download type --- cmd/download.go | 261 ++++++++++++++++++++++++++++++------------------ 1 file changed, 162 insertions(+), 99 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index ca8bda2d3..46c8027bc 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -54,66 +54,29 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - identifier, err := downloadIdentifier(flags) + download, err := newDownload(flags, usrCfg) if err != nil { return err } - url := fmt.Sprintf("%s/solutions/%s", usrCfg.GetString("apibaseurl"), identifier) - client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl")) - if err != nil { - return err - } - - req, err := client.NewRequest("GET", url, nil) - if err != nil { - return err - } - - req, err = addQueryToDownloadRequest(flags, req) - if err != nil { - return err - } + metadata := download.payload.metadata() + dir := metadata.Exercise(usrCfg.GetString("workspace")).MetadataDir() - res, err := client.Do(req) - if err != nil { + if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { return err } - var payload downloadPayload - defer res.Body.Close() - if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { - return fmt.Errorf("unable to parse API response - %s", err) - } - - if res.StatusCode == http.StatusUnauthorized { - siteURL := config.InferSiteURL(usrCfg.GetString("apibaseurl")) - return fmt.Errorf("unauthorized request. Please run the configure command. You can find your API token at %s/my/settings", siteURL) - } - - if res.StatusCode != http.StatusOK { - switch payload.Error.Type { - case "track_ambiguous": - return fmt.Errorf("%s: %s", payload.Error.Message, strings.Join(payload.Error.PossibleTrackIDs, ", ")) - default: - return errors.New(payload.Error.Message) - } - } - - metadata := payload.metadata() - dir := metadata.Exercise(usrCfg.GetString("workspace")).MetadataDir() - - if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { + if err := metadata.Write(dir); err != nil { return err } - err = metadata.Write(dir) + client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl")) if err != nil { return err } - for _, file := range payload.Solution.Files { - unparsedURL := fmt.Sprintf("%s%s", payload.Solution.FileDownloadBaseURL, file) + for _, file := range download.payload.Solution.Files { + unparsedURL := fmt.Sprintf("%s%s", download.payload.Solution.FileDownloadBaseURL, file) parsedURL, err := netURL.ParseRequestURI(unparsedURL) if err != nil { @@ -176,6 +139,160 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { return nil } +type download struct { + // either/or + slug, uuid string + + // user config + token, apibaseurl, workspace string + + // optional + track, team string + + payload *downloadPayload +} + +func newDownload(flags *pflag.FlagSet, usrCfg *viper.Viper) (*download, error) { + var err error + d := &download{} + d.uuid, err = flags.GetString("uuid") + if err != nil { + return nil, err + } + d.slug, err = flags.GetString("exercise") + if err != nil { + return nil, err + } + d.track, err = flags.GetString("track") + if err != nil { + return nil, err + } + d.team, err = flags.GetString("team") + if err != nil { + return nil, err + } + + d.token = usrCfg.GetString("token") + d.apibaseurl = usrCfg.GetString("apibaseurl") + d.workspace = usrCfg.GetString("workspace") + + if err = d.needsSlugXorUUID(); err != nil { + return nil, err + } + if err = d.needsUserConfigValues(); err != nil { + return nil, err + } + if err = d.needsSlugWhenGivenTrackOrTeam(); err != nil { + return nil, err + } + + client, err := api.NewClient(d.token, d.apibaseurl) + if err != nil { + return nil, err + } + + req, err := client.NewRequest("GET", d.url(), nil) + if err != nil { + return nil, err + } + d.buildQueryParams(req.URL) + + res, err := client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if err := json.NewDecoder(res.Body).Decode(&d.payload); err != nil { + return nil, fmt.Errorf("unable to parse API response - %s", err) + } + + if res.StatusCode == http.StatusUnauthorized { + return nil, fmt.Errorf( + "unauthorized request. Please run the configure command. You can find your API token at %s/my/settings", + config.InferSiteURL(d.apibaseurl), + ) + } + if res.StatusCode != http.StatusOK { + switch d.payload.Error.Type { + case "track_ambiguous": + return nil, fmt.Errorf( + "%s: %s", + d.payload.Error.Message, + strings.Join(d.payload.Error.PossibleTrackIDs, ", "), + ) + default: + return nil, errors.New(d.payload.Error.Message) + } + } + + return d, d.validate() +} + +func (d download) url() string { + id := "latest" + if d.uuid != "" { + id = d.uuid + } + return fmt.Sprintf("%s/solutions/%s", d.apibaseurl, id) +} + +func (d download) buildQueryParams(url *netURL.URL) { + query := url.Query() + if d.slug != "" { + query.Add("exercise_id", d.slug) + if d.track != "" { + query.Add("track_id", d.track) + } + if d.team != "" { + query.Add("team_id", d.team) + } + } + url.RawQuery = query.Encode() +} + +func (d download) validate() error { + if d.payload.Solution.ID == "" { + return errors.New("download missing ID") + } + if d.payload.Error.Message != "" { + return errors.New(d.payload.Error.Message) + } + return nil +} + +// needsSlugXorUUID checks the presence of slug XOR uuid. +func (d download) needsSlugXorUUID() error { + if d.slug != "" && d.uuid != "" || d.uuid == d.slug { + return errors.New("need an --exercise name or a solution --uuid") + } + return nil +} + +// needsUserConfigValues checks the presence of required values from the user config. +func (d download) needsUserConfigValues() error { + errMsg := "missing required user config: '%s'" + if d.token == "" { + return fmt.Errorf(errMsg, "token") + } + if d.apibaseurl == "" { + return fmt.Errorf(errMsg, "apibaseurl") + } + if d.workspace == "" { + return fmt.Errorf(errMsg, "workspace") + } + return nil +} + +// needsSlugWhenGivenTrackOrTeam ensures that track/team arguments are also given with a slug. +// (track/team meaningless when given a uuid). +func (d download) needsSlugWhenGivenTrackOrTeam() error { + if (d.team != "" || d.track != "") && d.slug == "" { + return errors.New("--track or --team requires --exercise (not --uuid)") + } + return nil +} + type downloadPayload struct { Solution struct { ID string `json:"id"` @@ -223,60 +340,6 @@ func (dp downloadPayload) metadata() workspace.ExerciseMetadata { } } -// downloadIdentifier is the variable for the URI to initiate an exercise download. -func downloadIdentifier(flags *pflag.FlagSet) (string, error) { - uuid, err := flags.GetString("uuid") - if err != nil { - return "", err - } - slug, err := flags.GetString("exercise") - if err != nil { - return "", err - } - if uuid != "" && slug != "" || uuid == slug { - return "", errors.New("need an --exercise name or a solution --uuid") - } - - identifier := "latest" - if uuid != "" { - identifier = uuid - } - return identifier, nil -} - -func addQueryToDownloadRequest(flags *pflag.FlagSet, req *http.Request) (*http.Request, error) { - uuid, err := flags.GetString("uuid") - if err != nil { - return req, err - } - slug, err := flags.GetString("exercise") - if err != nil { - return req, err - } - track, err := flags.GetString("track") - if err != nil { - return req, err - } - - team, err := flags.GetString("team") - if err != nil { - return req, err - } - - if uuid == "" { - q := req.URL.Query() - q.Add("exercise_id", slug) - if track != "" { - q.Add("track_id", track) - } - if team != "" { - q.Add("team_id", team) - } - req.URL.RawQuery = q.Encode() - } - return req, nil -} - func setupDownloadFlags(flags *pflag.FlagSet) { flags.StringP("uuid", "u", "", "the solution UUID") flags.StringP("track", "t", "", "the track ID") From b336b6a1a8a520d22ffbb4d786014071185fbefa Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Thu, 28 Feb 2019 18:17:01 +0700 Subject: [PATCH 379/544] Remove validate Unneeded as these checks already happen in the constructor and it's potentially confusing because there are other needsX validation methods --- cmd/download.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index 46c8027bc..982fd7de1 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -226,7 +226,7 @@ func newDownload(flags *pflag.FlagSet, usrCfg *viper.Viper) (*download, error) { } } - return d, d.validate() + return d, nil } func (d download) url() string { @@ -251,16 +251,6 @@ func (d download) buildQueryParams(url *netURL.URL) { url.RawQuery = query.Encode() } -func (d download) validate() error { - if d.payload.Solution.ID == "" { - return errors.New("download missing ID") - } - if d.payload.Error.Message != "" { - return errors.New(d.payload.Error.Message) - } - return nil -} - // needsSlugXorUUID checks the presence of slug XOR uuid. func (d download) needsSlugXorUUID() error { if d.slug != "" && d.uuid != "" || d.uuid == d.slug { From b0d7ee03f9901f60f77052d5f81ec85909dabb6a Mon Sep 17 00:00:00 2001 From: Stucki Date: Thu, 28 Feb 2019 09:10:56 -0600 Subject: [PATCH 380/544] add issue template --- .github/ISSUE_TEMPLATE.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..04f539626 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,7 @@ + From 8ebb29af5f565a554ab9e954d9ce4ad3bf0d18da Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Fri, 1 Mar 2019 08:16:25 +0700 Subject: [PATCH 381/544] Download response uses shared error parser --- cmd/cmd.go | 13 +++++++++++-- cmd/download.go | 21 +-------------------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index 36e6ef530..9fa48acdc 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "strings" "io" @@ -78,14 +79,22 @@ func validateUserConfig(cfg *viper.Viper) error { func decodedAPIError(resp *http.Response) error { var apiError struct { Error struct { - Type string `json:"type"` - Message string `json:"message"` + Type string `json:"type"` + Message string `json:"message"` + PossibleTrackIDs []string `json:"possible_track_ids"` } `json:"error,omitempty"` } if err := json.NewDecoder(resp.Body).Decode(&apiError); err != nil { return fmt.Errorf("failed to parse API error response: %s", err) } if apiError.Error.Message != "" { + if apiError.Error.Type == "track_ambiguous" { + return fmt.Errorf( + "%s: %s", + apiError.Error.Message, + strings.Join(apiError.Error.PossibleTrackIDs, ", "), + ) + } return fmt.Errorf(apiError.Error.Message) } return fmt.Errorf("unexpected API response: %d", resp.StatusCode) diff --git a/cmd/download.go b/cmd/download.go index 982fd7de1..c6c3cf628 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -204,26 +204,7 @@ func newDownload(flags *pflag.FlagSet, usrCfg *viper.Viper) (*download, error) { defer res.Body.Close() if err := json.NewDecoder(res.Body).Decode(&d.payload); err != nil { - return nil, fmt.Errorf("unable to parse API response - %s", err) - } - - if res.StatusCode == http.StatusUnauthorized { - return nil, fmt.Errorf( - "unauthorized request. Please run the configure command. You can find your API token at %s/my/settings", - config.InferSiteURL(d.apibaseurl), - ) - } - if res.StatusCode != http.StatusOK { - switch d.payload.Error.Type { - case "track_ambiguous": - return nil, fmt.Errorf( - "%s: %s", - d.payload.Error.Message, - strings.Join(d.payload.Error.PossibleTrackIDs, ", "), - ) - default: - return nil, errors.New(d.payload.Error.Message) - } + return nil, decodedAPIError(res) } return d, nil From 5de12130a3f042a9ed7a2eb536ec7e743760e379 Mon Sep 17 00:00:00 2001 From: Stucki Date: Fri, 1 Mar 2019 08:39:50 -0600 Subject: [PATCH 382/544] add teams support to workspace.PotentialExercises --- workspace/workspace.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/workspace/workspace.go b/workspace/workspace.go index 875f902c4..71036f511 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -57,6 +57,28 @@ func (ws Workspace) PotentialExercises() ([]Exercise, error) { continue } + if topInfo.Name() == "teams" { + subInfos, err := ioutil.ReadDir(filepath.Join(ws.Dir, "teams")) + if err != nil { + return nil, err + } + + for _, subInfo := range subInfos { + teamWs, err := New(filepath.Join(ws.Dir, "teams", subInfo.Name())) + if err != nil { + return nil, err + } + + teamExercises, err := teamWs.PotentialExercises() + if err != nil { + return nil, err + } + + exercises = append(exercises, teamExercises...) + } + continue + } + subInfos, err := ioutil.ReadDir(filepath.Join(ws.Dir, topInfo.Name())) if err != nil { return nil, err From 61382df521b6751fb918d0901646c53dc0b1eaa2 Mon Sep 17 00:00:00 2001 From: Stucki Date: Fri, 1 Mar 2019 09:04:34 -0600 Subject: [PATCH 383/544] add test for team exercises in workspace --- workspace/workspace_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go index 59c76a8c2..160c7cd0e 100644 --- a/workspace/workspace_test.go +++ b/workspace/workspace_test.go @@ -20,13 +20,16 @@ func TestWorkspacePotentialExercises(t *testing.T) { b1 := filepath.Join(tmpDir, "track-b", "exercise-one") b2 := filepath.Join(tmpDir, "track-b", "exercise-two") + // It should find teams exercises + team := filepath.Join(tmpDir, "teams", "some-team", "track-c", "exercise-one") + // It should ignore other people's exercises. alice := filepath.Join(tmpDir, "users", "alice", "track-a", "exercise-one") // It should ignore nested dirs within exercises. nested := filepath.Join(a1, "subdir", "deeper-dir", "another-deep-dir") - for _, path := range []string{a1, b1, b2, alice, nested} { + for _, path := range []string{a1, b1, b2, team, alice, nested} { err := os.MkdirAll(path, os.FileMode(0755)) assert.NoError(t, err) } @@ -36,7 +39,7 @@ func TestWorkspacePotentialExercises(t *testing.T) { exercises, err := ws.PotentialExercises() assert.NoError(t, err) - if assert.Equal(t, 3, len(exercises)) { + if assert.Equal(t, 4, len(exercises)) { paths := make([]string, len(exercises)) for i, e := range exercises { paths[i] = e.Path() @@ -46,6 +49,7 @@ func TestWorkspacePotentialExercises(t *testing.T) { assert.Equal(t, paths[0], "track-a/exercise-one") assert.Equal(t, paths[1], "track-b/exercise-one") assert.Equal(t, paths[2], "track-b/exercise-two") + assert.Equal(t, paths[3], "track-c/exercise-one") } } From 95369d5710c649cb5648535cab9a5dc0207d51ef Mon Sep 17 00:00:00 2001 From: Berin Larson Date: Sat, 2 Mar 2019 03:50:47 +0530 Subject: [PATCH 384/544] Remove duplicate files before submitting Closes #805 --- cmd/submit.go | 16 ++++++++++++++++ cmd/submit_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/cmd/submit.go b/cmd/submit.go index 9f72b0a55..de9b98e15 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -66,6 +66,8 @@ func runSubmit(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } + submitPaths = ctx.removeDuplicatePaths(submitPaths) + if err = ctx.validator.filesBelongToSameExercise(submitPaths); err != nil { return err } @@ -146,6 +148,20 @@ func (s *submitCmdContext) evaluatedSymlinks(submitPaths []string) ([]string, er return evalSymlinkSubmitPaths, nil } +func (s *submitCmdContext) removeDuplicatePaths(submitPaths []string) []string { + seen := make(map[string]bool) + result := make([]string, 0, len(submitPaths)) + + for _, val := range submitPaths { + if _, ok := seen[val]; !ok { + seen[val] = true + result = append(result, val) + } + } + + return result +} + // exercise creates an exercise using one of the submitted filepaths. // This assumes prior verification that submit paths belong to the same exercise. func (s *submitCmdContext) exercise(aSubmitPath string) (workspace.Exercise, error) { diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 7556ab0c3..a79b08ee5 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -142,6 +142,45 @@ func TestSubmitFilesAndDir(t *testing.T) { } } +func TestDuplicateFiles(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + + // The fake endpoint will populate this when it receives the call from the command. + submittedFiles := map[string]string{} + ts := fakeSubmitServer(t, submittedFiles) + defer ts.Close() + + tmpDir, err := ioutil.TempDir("", "duplicate-files") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(dir, os.FileMode(0755)) + + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + } + + file1 := filepath.Join(dir, "file-1.txt") + err = ioutil.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file1, file1}) + assert.NoError(t, err) + + assert.Equal(t, 1, len(submittedFiles)) + assert.Equal(t, "This is file 1.", submittedFiles["file-1.txt"]) +} + func TestSubmitFiles(t *testing.T) { co := newCapturedOutput() co.override() From 33537343e75c9cdbaa79729253c6cc08dc48d94a Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Fri, 11 Jan 2019 07:41:54 +0700 Subject: [PATCH 385/544] Submit handles non 2xx responses Previously, 404s silently continue to misleading success output --- cmd/submit.go | 4 ++++ cmd/submit_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/cmd/submit.go b/cmd/submit.go index de9b98e15..aac8a0615 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -272,6 +272,10 @@ func (s *submitCmdContext) submit(metadata *workspace.ExerciseMetadata, docs []w return decodedAPIError(resp) } + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return fmt.Errorf("submission unsuccessful (%s)", resp.Status) + } + bb := &bytes.Buffer{} _, err = bb.ReadFrom(resp.Body) if err != nil { diff --git a/cmd/submit_test.go b/cmd/submit_test.go index a79b08ee5..7bdad253f 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -611,6 +611,45 @@ func TestSubmitServerErr(t *testing.T) { assert.Regexp(t, "test error", err.Error()) } +func TestHandle404Response(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(404) + }) + + ts := httptest.NewServer(handler) + defer ts.Close() + + tmpDir, err := ioutil.TempDir("", "submit-nonsuccess") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: "http://example.com", + } + + dir := filepath.Join(tmpDir, "bogus-track", "bogus-exercise") + os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) + writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") + + err = ioutil.WriteFile(filepath.Join(dir, "file-1.txt"), []byte("This is file 1"), os.FileMode(0755)) + assert.NoError(t, err) + + files := []string{ + filepath.Join(dir, "file-1.txt"), + } + + err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "unsuccessful") + } +} func TestSubmissionNotConnectedToRequesterAccount(t *testing.T) { submittedFiles := map[string]string{} ts := fakeSubmitServer(t, submittedFiles) From 10aea95a9ef21b81390dbde89b5c4693eae05493 Mon Sep 17 00:00:00 2001 From: Jeff Sutherland Date: Mon, 4 Mar 2019 09:05:20 +0700 Subject: [PATCH 386/544] Use api error helper --- cmd/submit.go | 7 +------ cmd/submit_test.go | 7 +++---- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/cmd/submit.go b/cmd/submit.go index aac8a0615..4f2a8df72 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "mime/multipart" - "net/http" "os" "path/filepath" @@ -268,12 +267,8 @@ func (s *submitCmdContext) submit(metadata *workspace.ExerciseMetadata, docs []w } defer resp.Body.Close() - if resp.StatusCode == http.StatusBadRequest { - return decodedAPIError(resp) - } - if resp.StatusCode < 200 || resp.StatusCode > 299 { - return fmt.Errorf("submission unsuccessful (%s)", resp.Status) + return decodedAPIError(resp) } bb := &bytes.Buffer{} diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 7bdad253f..c20c47748 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -611,7 +611,7 @@ func TestSubmitServerErr(t *testing.T) { assert.Regexp(t, "test error", err.Error()) } -func TestHandle404Response(t *testing.T) { +func TestHandleErrorResponse(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) }) @@ -646,10 +646,9 @@ func TestHandle404Response(t *testing.T) { } err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) - if assert.Error(t, err) { - assert.Contains(t, err.Error(), "unsuccessful") - } + assert.Error(t, err) } + func TestSubmissionNotConnectedToRequesterAccount(t *testing.T) { submittedFiles := map[string]string{} ts := fakeSubmitServer(t, submittedFiles) From 8cd67be5deab64a6d27b49cd0ef4dbb0ab6ede27 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 3 Mar 2019 09:13:57 -0700 Subject: [PATCH 387/544] Isolate file logic from http and filesystem logic in download --- cmd/download.go | 75 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index c6c3cf628..9deeb0aa0 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -75,16 +75,12 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - for _, file := range download.payload.Solution.Files { - unparsedURL := fmt.Sprintf("%s%s", download.payload.Solution.FileDownloadBaseURL, file) - parsedURL, err := netURL.ParseRequestURI(unparsedURL) - + for _, sf := range download.payload.files() { + url, err := sf.url() if err != nil { return err } - url := parsedURL.String() - req, err := client.NewRequest("GET", url, nil) if err != nil { return err @@ -105,26 +101,12 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { continue } - // TODO: if there's a collision, interactively resolve (show diff, ask if overwrite). - // TODO: handle --force flag to overwrite without asking. - - // Work around a path bug due to an early design decision (later reversed) to - // allow numeric suffixes for exercise directories, allowing people to have - // multiple parallel versions of an exercise. - pattern := fmt.Sprintf(`\A.*[/\\]%s-\d*/`, metadata.ExerciseSlug) - rgxNumericSuffix := regexp.MustCompile(pattern) - if rgxNumericSuffix.MatchString(file) { - file = string(rgxNumericSuffix.ReplaceAll([]byte(file), []byte(""))) - } - - // Rewrite paths submitted with an older, buggy client where the Windows path is being treated as part of the filename. - file = strings.Replace(file, "\\", "/", -1) - - relativePath := filepath.FromSlash(file) - dir := filepath.Join(metadata.Dir, filepath.Dir(relativePath)) + // TODO: handle collisions + path := sf.relativePath() + dir := filepath.Join(metadata.Dir, filepath.Dir(path)) os.MkdirAll(dir, os.FileMode(0755)) - f, err := os.Create(filepath.Join(metadata.Dir, relativePath)) + f, err := os.Create(filepath.Join(metadata.Dir, path)) if err != nil { return err } @@ -311,6 +293,51 @@ func (dp downloadPayload) metadata() workspace.ExerciseMetadata { } } +func (dp downloadPayload) files() []solutionFile { + fx := make([]solutionFile, 0, len(dp.Solution.Files)) + for _, file := range dp.Solution.Files { + f := solutionFile{ + path: file, + baseURL: dp.Solution.FileDownloadBaseURL, + slug: dp.Solution.Exercise.ID, + } + fx = append(fx, f) + } + return fx +} + +type solutionFile struct { + path, baseURL, slug string +} + +func (sf solutionFile) url() (string, error) { + url, err := netURL.ParseRequestURI(fmt.Sprintf("%s%s", sf.baseURL, sf.path)) + + if err != nil { + return "", err + } + + return url.String(), nil +} + +func (sf solutionFile) relativePath() string { + file := sf.path + + // Work around a path bug due to an early design decision (later reversed) to + // allow numeric suffixes for exercise directories, letting people have + // multiple parallel versions of an exercise. + pattern := fmt.Sprintf(`\A.*[/\\]%s-\d*/`, sf.slug) + rgxNumericSuffix := regexp.MustCompile(pattern) + if rgxNumericSuffix.MatchString(sf.path) { + file = string(rgxNumericSuffix.ReplaceAll([]byte(sf.path), []byte(""))) + } + + // Rewrite paths submitted with an older, buggy client where the Windows path is being treated as part of the filename. + file = strings.Replace(file, "\\", "/", -1) + + return filepath.FromSlash(file) +} + func setupDownloadFlags(flags *pflag.FlagSet) { flags.StringP("uuid", "u", "", "the solution UUID") flags.StringP("track", "t", "", "the track ID") From 46fe28a350777191e22ce9657a21e9f34854605c Mon Sep 17 00:00:00 2001 From: Berin Larson Date: Sat, 9 Mar 2019 05:07:40 +0530 Subject: [PATCH 388/544] Add test for Solution File Type --- cmd/download_test.go | 110 +++++++++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 50 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index f76b4eca9..0e88c0320 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -77,6 +77,65 @@ func TestDownloadWithoutFlags(t *testing.T) { } } +func TestSolutionFile(t *testing.T) { + testCases := []struct { + name, file, expectedPath, expectedURL string + }{ + { + name: "filename with special character", + file: "special-char-filename#.txt", + expectedPath: "special-char-filename#.txt", + expectedURL: "http://www.example.com/special-char-filename%23.txt", + }, + { + name: "filename with leading slash", + file: "/with-leading-slash.txt", + expectedPath: fmt.Sprintf("%cwith-leading-slash.txt", os.PathSeparator), + expectedURL: "http://www.example.com//with-leading-slash.txt", + }, + { + name: "filename with leading backslash", + file: "\\with-leading-backslash.txt", + expectedPath: fmt.Sprintf("%cwith-leading-backslash.txt", os.PathSeparator), + expectedURL: "http://www.example.com/%5Cwith-leading-backslash.txt", + }, + { + name: "filename with backslashes in path", + file: "\\backslashes\\in-path.txt", + expectedPath: fmt.Sprintf("%[1]cbackslashes%[1]cin-path.txt", os.PathSeparator), + expectedURL: "http://www.example.com/%5Cbackslashes%5Cin-path.txt", + }, + { + name: "path with a numeric suffix", + file: "/bogus-exercise-12345/numeric.txt", + expectedPath: fmt.Sprintf("%[1]cbogus-exercise-12345%[1]cnumeric.txt", os.PathSeparator), + expectedURL: "http://www.example.com//bogus-exercise-12345/numeric.txt", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sf := solutionFile{ + path: tc.file, + baseURL: "http://www.example.com/", + } + + if sf.relativePath() != tc.expectedPath { + t.Fatalf("Expected path '%s', got '%s'", tc.expectedPath, sf.relativePath()) + } + + url, err := sf.url() + if err != nil { + t.Fatal(err) + } + + if url != tc.expectedURL { + t.Fatalf("Expected URL '%s', got '%s'", tc.expectedURL, url) + } + }) + } +} + func TestDownload(t *testing.T) { co := newCapturedOutput() co.override() @@ -161,25 +220,6 @@ func fakeDownloadServer(requestor, teamSlug string) *httptest.Server { fmt.Fprint(w, "this is file 2") }) - mux.HandleFunc("/full/path/with/numeric-suffix/bogus-track/bogus-exercise-12345/subdir/numeric.txt", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, "with numeric suffix") - }) - - mux.HandleFunc("/special-char-filename#.txt", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, "this is a special file") - }) - - mux.HandleFunc("/\\with-leading-backslash.txt", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, "with backslash in name") - }) - mux.HandleFunc("/\\with\\backslashes\\in\\path.txt", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, "with backslash in path") - }) - - mux.HandleFunc("/with-leading-slash.txt", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, "this has a slash") - }) - mux.HandleFunc("/file-3.txt", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "") }) @@ -216,31 +256,6 @@ func assertDownloadedCorrectFiles(t *testing.T, targetDir string) { path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "subdir", "file-2.txt"), contents: "this is file 2", }, - { - desc: "a path with a numeric suffix", - path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "subdir", "numeric.txt"), - contents: "with numeric suffix", - }, - { - desc: "a file that requires URL encoding", - path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "special-char-filename#.txt"), - contents: "this is a special file", - }, - { - desc: "a file that has a leading slash", - path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "with-leading-slash.txt"), - contents: "this has a slash", - }, - { - desc: "a file with a leading backslash", - path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "with-leading-backslash.txt"), - contents: "with backslash in name", - }, - { - desc: "a file with backslashes in path", - path: filepath.Join(targetDir, "bogus-track", "bogus-exercise", "with", "backslashes", "in", "path.txt"), - contents: "with backslash in path", - }, } for _, file := range expectedFiles { @@ -278,12 +293,7 @@ const payloadTemplate = ` "files": [ "file-1.txt", "subdir/file-2.txt", - "special-char-filename#.txt", - "/with-leading-slash.txt", - "\\with-leading-backslash.txt", - "\\with\\backslashes\\in\\path.txt", - "file-3.txt", - "/full/path/with/numeric-suffix/bogus-track/bogus-exercise-12345/subdir/numeric.txt" + "file-3.txt" ], "iteration": { "submitted_at": "2017-08-21t10:11:12.130z" From efc58b6a8e982396322485d7af011e1221ec36ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Lipt=C3=A1k?= Date: Sun, 17 Mar 2019 10:07:00 -0400 Subject: [PATCH 389/544] Add GoReportCard badge to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index da7f8a80d..fa1c45db8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Exercism Command-line Interface (CLI) [![Build Status](https://travis-ci.org/exercism/cli.svg?branch=master)](https://travis-ci.org/exercism/cli) +[![Go Report Card](https://goreportcard.com/badge/github.com/exercism/cli)](https://goreportcard.com/report/github.com/exercism/cli) The CLI is the link between the [Exercism][exercism] website and your local work environment. It lets you download exercises and submit your solution to the site. From 211bde5ec12876265a78055063d3a1ad0b02214f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Lipt=C3=A1k?= Date: Sun, 17 Mar 2019 10:09:36 -0400 Subject: [PATCH 390/544] Correct ineffassign --- cmd/download_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/download_test.go b/cmd/download_test.go index f76b4eca9..0f9067b1b 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -139,6 +139,7 @@ func TestDownload(t *testing.T) { dir := filepath.Join(targetDir, "bogus-track", "bogus-exercise") b, err := ioutil.ReadFile(workspace.NewExerciseFromDir(dir).MetadataFilepath()) + assert.NoError(t, err) var metadata workspace.ExerciseMetadata err = json.Unmarshal(b, &metadata) assert.NoError(t, err) From 815805e977b6ae0d38042a991b82a378357d2045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Lipt=C3=A1k?= Date: Fri, 22 Mar 2019 13:09:20 -0400 Subject: [PATCH 391/544] Bring Go current in Travis --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cb9eced50..a30fc6eca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,10 +3,14 @@ language: go sudo: false go: - - "1.10.x" - "1.11.x" + - "1.12.x" - tip +matrix: + allow_failures: + - go: tip + script: - ./.travis.gofmt.sh - go test -cover ./... From 6c8f99a2c916345591863ca736aebe52e9f95a5d Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 25 Apr 2019 09:55:40 -0600 Subject: [PATCH 392/544] Update copyright in license This ensures that the year is up to date, and that the copyright is assigned to the not-for-profit Exercism, rather than the (now-defunct) Delaware Corporation that we had in place as a temporary measure for legal protection until we could figure out how to structure things properly. Also, some very old repos still listed me as the copyright holder. I'm assigning all copyright to Exercism, even for those old repos. --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 449be2455..986629c33 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013 Mike Gehard, Katrina Owen +Copyright (c) 2013 Mike Gehard, Exercism Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From 11a3f1f592110d98f4568f5b037d3f6f844e2df6 Mon Sep 17 00:00:00 2001 From: bcmmbaga Date: Wed, 22 May 2019 23:37:44 +0300 Subject: [PATCH 393/544] ensure closing response body --- api/client.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/client.go b/api/client.go index 103df119e..b5b45b117 100644 --- a/api/client.go +++ b/api/client.go @@ -69,6 +69,7 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) { if err != nil { return nil, err } + defer res.Body.Close() debug.DumpResponse(res) return res, nil @@ -85,6 +86,8 @@ func (c *Client) TokenIsValid() (bool, error) { if err != nil { return false, err } + defer resp.Body.Close() + return resp.StatusCode == http.StatusOK, nil } @@ -99,6 +102,8 @@ func (c *Client) IsPingable() error { if err != nil { return err } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { return fmt.Errorf("API returned %s", resp.Status) } From 7ed7afed1c92ae57d34a7b7933631fbf19fc121b Mon Sep 17 00:00:00 2001 From: bcmmbaga Date: Thu, 23 May 2019 00:13:36 +0300 Subject: [PATCH 394/544] fix closing wrong response body --- api/client.go | 1 - 1 file changed, 1 deletion(-) diff --git a/api/client.go b/api/client.go index b5b45b117..a65cd3235 100644 --- a/api/client.go +++ b/api/client.go @@ -69,7 +69,6 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) { if err != nil { return nil, err } - defer res.Body.Close() debug.DumpResponse(res) return res, nil From 7d92475ecfe4fcda78946ebb7198b46b13690dac Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 7 Jul 2019 08:08:04 -0600 Subject: [PATCH 395/544] Update changelog to reflect changes since previous release --- CHANGELOG.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5c2350e7..658e78738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ## Next Release * **Your contribution here** +* [#770](https://github.com/exercism/cli/pull/770) Print API error messages in submit command - [@Smarticles101] +* [#763](https://github.com/exercism/cli/pull/763) Add Fish shell tab completions - [@John-Goff] +* [#806](https://github.com/exercism/cli/pull/806) Make Zsh shell tab completions work on $fpath - [@QuLogic] +* [#797](https://github.com/exercism/cli/pull/797) Fix panic when submit command is not given args - [@jdsutherland] +* [#828](https://github.com/exercism/cli/pull/828) Remove duplicate files before submitting - [@larson004] +* [#793](https://github.com/exercism/cli/pull/793) Submit handles non 2xx responses - [@jdsutherland] ## v3.0.11 (2018-11-18) * [#752](https://github.com/exercism/cli/pull/752) Improve error message on upgrade command - [@farisj] @@ -410,7 +416,10 @@ All changes by [@msgehard] [@AlexWheeler]: https://github.com/AlexWheeler [@Dparker1990]: https://github.com/Dparker1990 +[@John-Goff]: https://github.com/John-Goff [@LegalizeAdulthood]: https://github.com/LegalizeAdulthood +[@QuLogic]: https://github.com/QuLogic +[@Smarticles101]: https://github.com/Smarticles101 [@Tonkpils]: https://github.com/Tonkpils [@TrevorBramble]: https://github.com/TrevorBramble [@alebaffa]: https://github.com/alebaffa @@ -443,6 +452,7 @@ All changes by [@msgehard] [@jppunnett]: https://github.com/jppunnett [@katrinleinweber]: https://github.com/katrinleinweber [@kytrinyx]: https://github.com/kytrinyx +[@larson004]: https://github.com/larson004 [@lcowell]: https://github.com/lcowell [@manusajith]: https://github.com/manusajith [@morphatic]: https://github.com/morphatic @@ -463,4 +473,3 @@ All changes by [@msgehard] [@srt32]: https://github.com/srt32 [@williandrade]: https://github.com/williandrade [@zabawaba99]: https://github.com/zabawaba99 -[@Smarticles101]: https://github.com/Smarticles101 From b31af6ce8cb0250850a118878562b90deff905e7 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Sun, 7 Jul 2019 08:08:42 -0600 Subject: [PATCH 396/544] Bump version to v3.0.12 --- CHANGELOG.md | 2 ++ cmd/version.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 658e78738..724403b8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ## Next Release * **Your contribution here** + +## v3.0.12 (2019-07-07) * [#770](https://github.com/exercism/cli/pull/770) Print API error messages in submit command - [@Smarticles101] * [#763](https://github.com/exercism/cli/pull/763) Add Fish shell tab completions - [@John-Goff] * [#806](https://github.com/exercism/cli/pull/806) Make Zsh shell tab completions work on $fpath - [@QuLogic] diff --git a/cmd/version.go b/cmd/version.go index 970e46c06..45c88ff55 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.0.11" +const Version = "3.0.12" // checkLatest flag for version command. var checkLatest bool From 91192386a75d387b921a35ea4d0762e4690ab479 Mon Sep 17 00:00:00 2001 From: Rafael Date: Wed, 31 Jul 2019 10:23:16 -0300 Subject: [PATCH 397/544] Fix Oh my Zsh correct fpath Every time I open the terminal show this: ```/.oh-my-zsh/custom/exercism_completion.zsh:local:6: options: can't change type of autoloaded parameter``` You need to put the file in any `$fpath`, but `~/.oh-my-zsh/custom` is not one of them. Look this: https://github.com/beetbox/beets/issues/1731 --- shell/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/shell/README.md b/shell/README.md index b4c1509ce..8be23f708 100644 --- a/shell/README.md +++ b/shell/README.md @@ -32,7 +32,11 @@ and then add the directory to your `$fpath` in your `.zshrc`, `.zsh_profile` or #### Oh my Zsh -If you are using the popular [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) framework to manage your zsh plugins, move the file `exercism_completion.zsh` into `~/.oh-my-zsh/custom`. +If you are using the popular [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) framework to manage your zsh plugins, follow these steps: + + mkdir -p ~/.oh-my-zsh/completions + mv ../shell/exercism_completion.zsh ~/.oh-my-zsh/completions + ### Fish From 52e2906c15fa2539f766fb09831aa31d8f1d3bf7 Mon Sep 17 00:00:00 2001 From: Jamario Rankins Date: Fri, 2 Aug 2019 09:28:39 -0600 Subject: [PATCH 398/544] Inital Commit --- utils/utils.go | 9 +++++++++ utils/utils_test.go | 15 +++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 utils/utils.go create mode 100644 utils/utils_test.go diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 000000000..d6dd42e05 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,9 @@ +package utils + +import "strings" + +func Redact(token string) string { + str := token[4 : len(token)-3] + redaction := strings.Repeat("*", len(str)) + return string(token[:4]) + redaction + string(token[len(token)-3:]) +} diff --git a/utils/utils_test.go b/utils/utils_test.go new file mode 100644 index 000000000..94fedd091 --- /dev/null +++ b/utils/utils_test.go @@ -0,0 +1,15 @@ +package utils + +import ( + "testing" + + "github.com/exercism/cli/utils" + "github.com/stretchr/testify/assert" +) + +func TestRedact(t *testing.T) { + fakeToken := "1a11111aaaa111aa1a11111a11111aa1" + expected := "1a11*************************aa1" + + assert.Equal(t, expected, utils.Redact(fakeToken)) +} From 867ff5e9a34817d253d247b424ecb602ddfa7b5c Mon Sep 17 00:00:00 2001 From: Jamario Rankins Date: Fri, 2 Aug 2019 09:30:37 -0600 Subject: [PATCH 399/544] Inital Commit --- cmd/root.go | 4 ++++ cmd/troubleshoot.go | 10 ++-------- cmd/troubleshoot_test.go | 14 -------------- debug/debug.go | 15 +++++++++++++-- go.mod | 31 +++++++++++++++++-------------- go.sum | 11 +++++++++++ 6 files changed, 47 insertions(+), 38 deletions(-) delete mode 100644 cmd/troubleshoot_test.go diff --git a/cmd/root.go b/cmd/root.go index 5014c1e30..745338200 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -24,6 +24,9 @@ Download exercises and submit your solutions.`, if verbose, _ := cmd.Flags().GetBool("verbose"); verbose { debug.Verbose = verbose } + if unmask, _ := cmd.Flags().GetBool("unmask-token"); unmask { + debug.UnmaskAPIKey = unmask + } if timeout, _ := cmd.Flags().GetInt("timeout"); timeout > 0 { cli.TimeoutInSeconds = timeout api.TimeoutInSeconds = timeout @@ -46,4 +49,5 @@ func init() { api.UserAgent = fmt.Sprintf("github.com/exercism/cli v%s (%s/%s)", Version, runtime.GOOS, runtime.GOARCH) RootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output") RootCmd.PersistentFlags().IntP("timeout", "", 0, "override the default HTTP timeout (seconds)") + RootCmd.PersistentFlags().BoolP("unmask-token", "", false, "will unmask the API duruing a request/response dump") } diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go index 12f2277e3..997fcb19a 100644 --- a/cmd/troubleshoot.go +++ b/cmd/troubleshoot.go @@ -5,12 +5,12 @@ import ( "fmt" "html/template" "runtime" - "strings" "sync" "time" "github.com/exercism/cli/cli" "github.com/exercism/cli/config" + "github.com/exercism/cli/utils" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -192,7 +192,7 @@ func newConfigurationStatus(status *Status) configurationStatus { TokenURL: config.SettingsURL(v.GetString("apibaseurl")), } if status.Censor && cs.Token != "" { - cs.Token = redact(cs.Token) + cs.Token = utils.Redact(cs.Token) } return cs } @@ -212,12 +212,6 @@ func (ping *apiPing) Call(wg *sync.WaitGroup) { ping.Status = "connected" } -func redact(token string) string { - str := token[4 : len(token)-3] - redaction := strings.Repeat("*", len(str)) - return string(token[:4]) + redaction + string(token[len(token)-3:]) -} - const tmplSelfTest = ` Troubleshooting Information =========================== diff --git a/cmd/troubleshoot_test.go b/cmd/troubleshoot_test.go deleted file mode 100644 index e2446022c..000000000 --- a/cmd/troubleshoot_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package cmd - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRedact(t *testing.T) { - fakeToken := "1a11111aaaa111aa1a11111a11111aa1" - expected := "1a11*************************aa1" - - assert.Equal(t, expected, redact(fakeToken)) -} diff --git a/debug/debug.go b/debug/debug.go index 0fdb96ebd..e15db3b3e 100644 --- a/debug/debug.go +++ b/debug/debug.go @@ -9,12 +9,16 @@ import ( "net/http" "net/http/httputil" "os" + "strings" + + "github.com/exercism/cli/utils" ) var ( // Verbose determines if debugging output is displayed to the user - Verbose bool - output io.Writer = os.Stderr + Verbose bool + output io.Writer = os.Stderr + UnmaskAPIKey bool ) // Println conditionally outputs a message to Stderr @@ -41,6 +45,12 @@ func DumpRequest(req *http.Request) { body := io.TeeReader(req.Body, &bodyCopy) req.Body = ioutil.NopCloser(body) + temp := req.Header.Get("Authorization") + + if !UnmaskAPIKey { + req.Header.Set("Authorization", "Bearer "+utils.Redact(strings.Split(temp, " ")[1])) + } + dump, err := httputil.DumpRequest(req, req.ContentLength > 0) if err != nil { log.Fatal(err) @@ -51,6 +61,7 @@ func DumpRequest(req *http.Request) { Println("========================= END DumpRequest =========================") Println("") + req.Header.Set("Authorization", temp) req.Body = ioutil.NopCloser(&bodyCopy) } diff --git a/go.mod b/go.mod index c0d986d0f..79dab5ab7 100644 --- a/go.mod +++ b/go.mod @@ -2,25 +2,28 @@ module github.com/exercism/cli require ( github.com/blang/semver v3.5.1+incompatible - github.com/davecgh/go-spew v1.1.0 - github.com/fsnotify/fsnotify v1.4.2 - github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/fsnotify/fsnotify v1.4.2 // indirect + github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e // indirect github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf - github.com/inconshreveable/mousetrap v1.0.0 - github.com/magiconair/properties v1.7.3 - github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992 - github.com/pelletier/go-buffruneio v0.2.0 - github.com/pelletier/go-toml v1.0.0 - github.com/pmezard/go-difflib v1.0.0 - github.com/spf13/afero v0.0.0-20170217164146-9be650865eab - github.com/spf13/cast v1.1.0 + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/kr/pretty v0.1.0 // indirect + github.com/ld9999999999/go-interfacetools v0.0.0-20151014172923-5c708af5db62 + github.com/magiconair/properties v1.7.3 // indirect + github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992 // indirect + github.com/pelletier/go-buffruneio v0.2.0 // indirect + github.com/pelletier/go-toml v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/afero v0.0.0-20170217164146-9be650865eab // indirect + github.com/spf13/cast v1.1.0 // indirect github.com/spf13/cobra v0.0.0-20170731170427-b26b538f6930 - github.com/spf13/jwalterweatherman v0.0.0-20170523133247-0efa5202c046 + github.com/spf13/jwalterweatherman v0.0.0-20170523133247-0efa5202c046 // indirect github.com/spf13/pflag v1.0.0 github.com/spf13/viper v0.0.0-20180507071007-15738813a09d github.com/stretchr/testify v1.1.4 golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7 - golang.org/x/sys v0.0.0-20170803140359-d8f5ea21b929 + golang.org/x/sys v0.0.0-20170803140359-d8f5ea21b929 // indirect golang.org/x/text v0.0.0-20170730040918-3bd178b88a81 - gopkg.in/yaml.v2 v2.0.0-20170721122051-25c4ec802a7d + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/yaml.v2 v2.0.0-20170721122051-25c4ec802a7d // indirect ) diff --git a/go.sum b/go.sum index 0a9864259..cf2df5dde 100644 --- a/go.sum +++ b/go.sum @@ -2,13 +2,22 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/exercism/c v0.0.0-20190801120910-35035966d3ef h1:DIBUBpXo2Zot+UiaPmuwPYdmqg8pYqB5thsNWINiPD4= github.com/fsnotify/fsnotify v1.4.2 h1:v5tKwtf2hNhBV24eNYfQ5UmvFOGlOCmRqk7/P1olxtk= github.com/fsnotify/fsnotify v1.4.2/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e h1:KJWs1uTCkN3E/J5ofCH9Pf8KKsibTFc3fv0CA9+WsVo= github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/ld9999999999/go-interfacetools v0.0.0-20151014172923-5c708af5db62 h1:Yxmgqjdx/bnDhp8TSw9CENvRrMiJklKYDfruUMROaFk= +github.com/ld9999999999/go-interfacetools v0.0.0-20151014172923-5c708af5db62/go.mod h1:HaEcNiqo/Y5y/ZpHKx6bWRvJJ+HXelkjSCpk0MEf19k= github.com/magiconair/properties v1.7.3 h1:6AOjgCKyZFMG/1yfReDPDz3CJZPxnYk7DGmj2HtyF24= github.com/magiconair/properties v1.7.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992 h1:W7VHAEVflA5/eTyRvQ53Lz5j8bhRd1myHZlI/IZFvbU= @@ -39,5 +48,7 @@ golang.org/x/sys v0.0.0-20170803140359-d8f5ea21b929 h1:M4VPQYSW/nB4Bcg1XMD4yW2sp golang.org/x/sys v0.0.0-20170803140359-d8f5ea21b929/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.0.0-20170730040918-3bd178b88a81 h1:7aXI3TQ9sZ4JdDoIDGjxL6G2mQxlsPy9dySnJaL6Bdk= golang.org/x/text v0.0.0-20170730040918-3bd178b88a81/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.0.0-20170721122051-25c4ec802a7d h1:2DX7x6HUDGZUyuEDAhUsQQNqkb1zvDyKTjVoTdzaEzo= gopkg.in/yaml.v2 v2.0.0-20170721122051-25c4ec802a7d/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= From 9b9ff08be25efd5b104725957ed0fecf96d62db6 Mon Sep 17 00:00:00 2001 From: Jamario Rankins Date: Fri, 2 Aug 2019 09:33:53 -0600 Subject: [PATCH 400/544] Added doc for Redact and Added empty string case for header --- debug/debug.go | 4 +++- utils/utils.go | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/debug/debug.go b/debug/debug.go index e15db3b3e..86849dbb6 100644 --- a/debug/debug.go +++ b/debug/debug.go @@ -48,7 +48,9 @@ func DumpRequest(req *http.Request) { temp := req.Header.Get("Authorization") if !UnmaskAPIKey { - req.Header.Set("Authorization", "Bearer "+utils.Redact(strings.Split(temp, " ")[1])) + if token := strings.Split(temp, " ")[1]; token != "" { + req.Header.Set("Authorization", "Bearer "+utils.Redact(token)) + } } dump, err := httputil.DumpRequest(req, req.ContentLength > 0) diff --git a/utils/utils.go b/utils/utils.go index d6dd42e05..9823ce1c8 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -2,6 +2,9 @@ package utils import "strings" +/* +Redact masks the given token by replacing part of the string with * +*/ func Redact(token string) string { str := token[4 : len(token)-3] redaction := strings.Repeat("*", len(str)) From a81e36b348e111641710508877896702aba26753 Mon Sep 17 00:00:00 2001 From: Jamario Rankins Date: Fri, 2 Aug 2019 09:44:47 -0600 Subject: [PATCH 401/544] Fixing test --- utils/utils_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/utils/utils_test.go b/utils/utils_test.go index 94fedd091..4d74ebc6c 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -3,7 +3,6 @@ package utils import ( "testing" - "github.com/exercism/cli/utils" "github.com/stretchr/testify/assert" ) @@ -11,5 +10,5 @@ func TestRedact(t *testing.T) { fakeToken := "1a11111aaaa111aa1a11111a11111aa1" expected := "1a11*************************aa1" - assert.Equal(t, expected, utils.Redact(fakeToken)) + assert.Equal(t, expected, Redact(fakeToken)) } From f9378263689168855c613bafdb6fa50965dd5061 Mon Sep 17 00:00:00 2001 From: Jamario Rankins Date: Fri, 2 Aug 2019 09:52:31 -0600 Subject: [PATCH 402/544] Update cmd/root.go Co-Authored-By: Victor Goff --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index 745338200..aa97c4670 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -49,5 +49,5 @@ func init() { api.UserAgent = fmt.Sprintf("github.com/exercism/cli v%s (%s/%s)", Version, runtime.GOOS, runtime.GOARCH) RootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output") RootCmd.PersistentFlags().IntP("timeout", "", 0, "override the default HTTP timeout (seconds)") - RootCmd.PersistentFlags().BoolP("unmask-token", "", false, "will unmask the API duruing a request/response dump") + RootCmd.PersistentFlags().BoolP("unmask-token", "", false, "will unmask the API during a request/response dump") } From 3559f2055a049cc85012641c04f50b2431888160 Mon Sep 17 00:00:00 2001 From: Jamario Rankins Date: Sun, 4 Aug 2019 12:27:39 -0600 Subject: [PATCH 403/544] Reverting go.mod and go.sum --- go.mod | 31 ++++++++++++++----------------- go.sum | 11 ----------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/go.mod b/go.mod index 79dab5ab7..c0d986d0f 100644 --- a/go.mod +++ b/go.mod @@ -2,28 +2,25 @@ module github.com/exercism/cli require ( github.com/blang/semver v3.5.1+incompatible - github.com/davecgh/go-spew v1.1.0 // indirect - github.com/fsnotify/fsnotify v1.4.2 // indirect - github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e // indirect + github.com/davecgh/go-spew v1.1.0 + github.com/fsnotify/fsnotify v1.4.2 + github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf - github.com/inconshreveable/mousetrap v1.0.0 // indirect - github.com/kr/pretty v0.1.0 // indirect - github.com/ld9999999999/go-interfacetools v0.0.0-20151014172923-5c708af5db62 - github.com/magiconair/properties v1.7.3 // indirect - github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992 // indirect - github.com/pelletier/go-buffruneio v0.2.0 // indirect - github.com/pelletier/go-toml v1.0.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/spf13/afero v0.0.0-20170217164146-9be650865eab // indirect - github.com/spf13/cast v1.1.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 + github.com/magiconair/properties v1.7.3 + github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992 + github.com/pelletier/go-buffruneio v0.2.0 + github.com/pelletier/go-toml v1.0.0 + github.com/pmezard/go-difflib v1.0.0 + github.com/spf13/afero v0.0.0-20170217164146-9be650865eab + github.com/spf13/cast v1.1.0 github.com/spf13/cobra v0.0.0-20170731170427-b26b538f6930 - github.com/spf13/jwalterweatherman v0.0.0-20170523133247-0efa5202c046 // indirect + github.com/spf13/jwalterweatherman v0.0.0-20170523133247-0efa5202c046 github.com/spf13/pflag v1.0.0 github.com/spf13/viper v0.0.0-20180507071007-15738813a09d github.com/stretchr/testify v1.1.4 golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7 - golang.org/x/sys v0.0.0-20170803140359-d8f5ea21b929 // indirect + golang.org/x/sys v0.0.0-20170803140359-d8f5ea21b929 golang.org/x/text v0.0.0-20170730040918-3bd178b88a81 - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect - gopkg.in/yaml.v2 v2.0.0-20170721122051-25c4ec802a7d // indirect + gopkg.in/yaml.v2 v2.0.0-20170721122051-25c4ec802a7d ) diff --git a/go.sum b/go.sum index cf2df5dde..0a9864259 100644 --- a/go.sum +++ b/go.sum @@ -2,22 +2,13 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/exercism/c v0.0.0-20190801120910-35035966d3ef h1:DIBUBpXo2Zot+UiaPmuwPYdmqg8pYqB5thsNWINiPD4= github.com/fsnotify/fsnotify v1.4.2 h1:v5tKwtf2hNhBV24eNYfQ5UmvFOGlOCmRqk7/P1olxtk= github.com/fsnotify/fsnotify v1.4.2/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e h1:KJWs1uTCkN3E/J5ofCH9Pf8KKsibTFc3fv0CA9+WsVo= github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/ld9999999999/go-interfacetools v0.0.0-20151014172923-5c708af5db62 h1:Yxmgqjdx/bnDhp8TSw9CENvRrMiJklKYDfruUMROaFk= -github.com/ld9999999999/go-interfacetools v0.0.0-20151014172923-5c708af5db62/go.mod h1:HaEcNiqo/Y5y/ZpHKx6bWRvJJ+HXelkjSCpk0MEf19k= github.com/magiconair/properties v1.7.3 h1:6AOjgCKyZFMG/1yfReDPDz3CJZPxnYk7DGmj2HtyF24= github.com/magiconair/properties v1.7.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992 h1:W7VHAEVflA5/eTyRvQ53Lz5j8bhRd1myHZlI/IZFvbU= @@ -48,7 +39,5 @@ golang.org/x/sys v0.0.0-20170803140359-d8f5ea21b929 h1:M4VPQYSW/nB4Bcg1XMD4yW2sp golang.org/x/sys v0.0.0-20170803140359-d8f5ea21b929/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.0.0-20170730040918-3bd178b88a81 h1:7aXI3TQ9sZ4JdDoIDGjxL6G2mQxlsPy9dySnJaL6Bdk= golang.org/x/text v0.0.0-20170730040918-3bd178b88a81/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.0.0-20170721122051-25c4ec802a7d h1:2DX7x6HUDGZUyuEDAhUsQQNqkb1zvDyKTjVoTdzaEzo= gopkg.in/yaml.v2 v2.0.0-20170721122051-25c4ec802a7d/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= From 332b16d4cb15637db3ffe118019ad2459c849e04 Mon Sep 17 00:00:00 2001 From: Jamario Rankins Date: Sun, 4 Aug 2019 12:33:04 -0600 Subject: [PATCH 404/544] Changed function comment to match codebase --- utils/utils.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/utils/utils.go b/utils/utils.go index 9823ce1c8..d2599f3c7 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -2,9 +2,7 @@ package utils import "strings" -/* -Redact masks the given token by replacing part of the string with * -*/ +// Redact masks the given token by replacing part of the string with * func Redact(token string) string { str := token[4 : len(token)-3] redaction := strings.Repeat("*", len(str)) From 7f4bb2787e4ae49ac8021e5385f3f884e85f7ff3 Mon Sep 17 00:00:00 2001 From: Jamario Rankins Date: Sun, 4 Aug 2019 12:55:54 -0600 Subject: [PATCH 405/544] Added Comment for UnamskAPIKey --- debug/debug.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/debug/debug.go b/debug/debug.go index 86849dbb6..fd4908584 100644 --- a/debug/debug.go +++ b/debug/debug.go @@ -16,8 +16,9 @@ import ( var ( // Verbose determines if debugging output is displayed to the user - Verbose bool - output io.Writer = os.Stderr + Verbose bool + output io.Writer = os.Stderr + // UnmaskAPIKey determines if the API key should de displayed during a dump UnmaskAPIKey bool ) From c7bab26d77ff8fd25072ce9f29b67e5f4f801b8e Mon Sep 17 00:00:00 2001 From: Jamario Rankins Date: Wed, 7 Aug 2019 08:42:19 -0600 Subject: [PATCH 406/544] Removing utils package and moving Redact to the debug package --- cmd/troubleshoot.go | 4 ++-- debug/debug.go | 11 ++++++++--- debug/debug_test.go | 9 +++++++++ utils/utils.go | 10 ---------- utils/utils_test.go | 14 -------------- 5 files changed, 19 insertions(+), 29 deletions(-) delete mode 100644 utils/utils.go delete mode 100644 utils/utils_test.go diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go index 997fcb19a..f573f2523 100644 --- a/cmd/troubleshoot.go +++ b/cmd/troubleshoot.go @@ -10,7 +10,7 @@ import ( "github.com/exercism/cli/cli" "github.com/exercism/cli/config" - "github.com/exercism/cli/utils" + "github.com/exercism/cli/debug" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -192,7 +192,7 @@ func newConfigurationStatus(status *Status) configurationStatus { TokenURL: config.SettingsURL(v.GetString("apibaseurl")), } if status.Censor && cs.Token != "" { - cs.Token = utils.Redact(cs.Token) + cs.Token = debug.Redact(cs.Token) } return cs } diff --git a/debug/debug.go b/debug/debug.go index fd4908584..dad0b629c 100644 --- a/debug/debug.go +++ b/debug/debug.go @@ -10,8 +10,6 @@ import ( "net/http/httputil" "os" "strings" - - "github.com/exercism/cli/utils" ) var ( @@ -50,7 +48,7 @@ func DumpRequest(req *http.Request) { if !UnmaskAPIKey { if token := strings.Split(temp, " ")[1]; token != "" { - req.Header.Set("Authorization", "Bearer "+utils.Redact(token)) + req.Header.Set("Authorization", "Bearer "+Redact(token)) } } @@ -90,3 +88,10 @@ func DumpResponse(res *http.Response) { res.Body = ioutil.NopCloser(body) } + +// Redact masks the given token by replacing part of the string with * +func Redact(token string) string { + str := token[4 : len(token)-3] + redaction := strings.Repeat("*", len(str)) + return string(token[:4]) + redaction + string(token[len(token)-3:]) +} diff --git a/debug/debug_test.go b/debug/debug_test.go index d4adf3f43..8bbe3a857 100644 --- a/debug/debug_test.go +++ b/debug/debug_test.go @@ -3,6 +3,8 @@ package debug import ( "bytes" "testing" + + "github.com/stretchr/testify/assert" ) func TestVerboseEnabled(t *testing.T) { @@ -26,3 +28,10 @@ func TestVerboseDisabled(t *testing.T) { t.Error("expected '' got", b.String()) } } + +func TestRedact(t *testing.T) { + fakeToken := "1a11111aaaa111aa1a11111a11111aa1" + expected := "1a11*************************aa1" + + assert.Equal(t, expected, Redact(fakeToken)) +} diff --git a/utils/utils.go b/utils/utils.go deleted file mode 100644 index d2599f3c7..000000000 --- a/utils/utils.go +++ /dev/null @@ -1,10 +0,0 @@ -package utils - -import "strings" - -// Redact masks the given token by replacing part of the string with * -func Redact(token string) string { - str := token[4 : len(token)-3] - redaction := strings.Repeat("*", len(str)) - return string(token[:4]) + redaction + string(token[len(token)-3:]) -} diff --git a/utils/utils_test.go b/utils/utils_test.go deleted file mode 100644 index 4d74ebc6c..000000000 --- a/utils/utils_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package utils - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRedact(t *testing.T) { - fakeToken := "1a11111aaaa111aa1a11111a11111aa1" - expected := "1a11*************************aa1" - - assert.Equal(t, expected, Redact(fakeToken)) -} From 4ad01b1562f320ac6c75b9463827b5d5857f4f03 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 12 Aug 2019 08:30:09 -0600 Subject: [PATCH 407/544] Revert change to custom oh my zsh fpath This reverts commit 91192386a75d387b921a35ea4d0762e4690ab479. --- shell/README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/shell/README.md b/shell/README.md index 8be23f708..b4c1509ce 100644 --- a/shell/README.md +++ b/shell/README.md @@ -32,11 +32,7 @@ and then add the directory to your `$fpath` in your `.zshrc`, `.zsh_profile` or #### Oh my Zsh -If you are using the popular [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) framework to manage your zsh plugins, follow these steps: - - mkdir -p ~/.oh-my-zsh/completions - mv ../shell/exercism_completion.zsh ~/.oh-my-zsh/completions - +If you are using the popular [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) framework to manage your zsh plugins, move the file `exercism_completion.zsh` into `~/.oh-my-zsh/custom`. ### Fish From 2bfd6c8f0b9269a074702cbd0caf43ae46411bcb Mon Sep 17 00:00:00 2001 From: Jamario Rankins Date: Tue, 13 Aug 2019 17:23:17 -0600 Subject: [PATCH 408/544] Error message returned if the track is locked --- cmd/download.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/download.go b/cmd/download.go index 9deeb0aa0..febd95fcc 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -188,6 +188,9 @@ func newDownload(flags *pflag.FlagSet, usrCfg *viper.Viper) (*download, error) { if err := json.NewDecoder(res.Body).Decode(&d.payload); err != nil { return nil, decodedAPIError(res) } + if d.payload.Error.Message != "" { + return nil, errors.New(d.payload.Error.Message) + } return d, nil } From 08d56c1e9685417e71880d71a0163253af25f69e Mon Sep 17 00:00:00 2001 From: Jamario Rankins Date: Wed, 14 Aug 2019 15:34:16 -0600 Subject: [PATCH 409/544] Error message returned if the track is locked --- cmd/download.go | 10 ++++++---- cmd/download_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index febd95fcc..8966c6ab0 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -1,10 +1,12 @@ package cmd import ( + "bytes" "encoding/json" "errors" "fmt" "io" + "io/ioutil" "net/http" netURL "net/url" "os" @@ -185,12 +187,12 @@ func newDownload(flags *pflag.FlagSet, usrCfg *viper.Viper) (*download, error) { } defer res.Body.Close() - if err := json.NewDecoder(res.Body).Decode(&d.payload); err != nil { + body, err := ioutil.ReadAll(res.Body) + res.Body = ioutil.NopCloser(bytes.NewReader(body)) + + if err := json.Unmarshal(body, &d.payload); err != nil || res.StatusCode < 200 || res.StatusCode > 299 { return nil, decodedAPIError(res) } - if d.payload.Error.Message != "" { - return nil, errors.New(d.payload.Error.Message) - } return d, nil } diff --git a/cmd/download_test.go b/cmd/download_test.go index 9410f2473..69e8f90a1 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -272,6 +272,42 @@ func assertDownloadedCorrectFiles(t *testing.T, targetDir string) { assert.True(t, os.IsNotExist(err), "It should not write the file if empty.") } +func TestDownloadError(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, `{"error": {"type": "error", "message": "test error"}}`) + }) + + ts := httptest.NewServer(handler) + defer ts.Close() + + tmpDir, err := ioutil.TempDir("", "submit-err-tmp-dir") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + v := viper.New() + v.Set("token", "abc123") + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + + cfg := config.Config{ + Persister: config.InMemoryPersister{}, + UserViperConfig: v, + DefaultBaseURL: "http://example.com", + } + + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupDownloadFlags(flags) + flags.Set("uuid", "value") + + err = runDownload(cfg, flags, []string{}) + + fmt.Println(err) + + assert.Regexp(t, "test error", err.Error()) + +} + const payloadTemplate = ` { "solution": { From 31c4c106ea5d4b1b30b0772b07f327959427a0a0 Mon Sep 17 00:00:00 2001 From: Jamario Rankins Date: Thu, 15 Aug 2019 15:58:27 -0600 Subject: [PATCH 410/544] Allowed Unmarshal to handle the error and removed debug output --- cmd/download.go | 8 ++++++-- cmd/download_test.go | 4 +--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index 8966c6ab0..2d2935f24 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -187,10 +187,14 @@ func newDownload(flags *pflag.FlagSet, usrCfg *viper.Viper) (*download, error) { } defer res.Body.Close() - body, err := ioutil.ReadAll(res.Body) + if res.StatusCode < 200 || res.StatusCode > 299 { + return nil, decodedAPIError(res) + } + + body, _ := ioutil.ReadAll(res.Body) res.Body = ioutil.NopCloser(bytes.NewReader(body)) - if err := json.Unmarshal(body, &d.payload); err != nil || res.StatusCode < 200 || res.StatusCode > 299 { + if err := json.Unmarshal(body, &d.payload); err != nil { return nil, decodedAPIError(res) } diff --git a/cmd/download_test.go b/cmd/download_test.go index 69e8f90a1..2eb0418df 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -302,9 +302,7 @@ func TestDownloadError(t *testing.T) { err = runDownload(cfg, flags, []string{}) - fmt.Println(err) - - assert.Regexp(t, "test error", err.Error()) + assert.Equal(t, "test error", err.Error()) } From 96a994c49fe22ee11818af691b04f0ac98fd4421 Mon Sep 17 00:00:00 2001 From: Aleksei Vegner Date: Sat, 17 Aug 2019 22:25:23 +0300 Subject: [PATCH 411/544] Make all errors in cmd package checked --- cmd/download.go | 4 +++- cmd/open.go | 3 +-- cmd/troubleshoot.go | 4 +++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index 9deeb0aa0..c0fe8dff6 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -104,7 +104,9 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { // TODO: handle collisions path := sf.relativePath() dir := filepath.Join(metadata.Dir, filepath.Dir(path)) - os.MkdirAll(dir, os.FileMode(0755)) + if err = os.MkdirAll(dir, os.FileMode(0755)); err != nil { + return err + } f, err := os.Create(filepath.Join(metadata.Dir, path)) if err != nil { diff --git a/cmd/open.go b/cmd/open.go index 4d04245e8..dd41a3cf6 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -21,8 +21,7 @@ Pass the path to the directory that contains the solution you want to see on the if err != nil { return err } - browser.Open(metadata.URL) - return nil + return browser.Open(metadata.URL) }, } diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go index f573f2523..5fd134d3c 100644 --- a/cmd/troubleshoot.go +++ b/cmd/troubleshoot.go @@ -124,7 +124,9 @@ func (status *Status) compile() (string, error) { } var bb bytes.Buffer - t.Execute(&bb, status) + if err = t.Execute(&bb, status); err != nil { + return "", err + } return bb.String(), nil } From c19d34c002bb8f3af5aa1cc940a247e6f2db379e Mon Sep 17 00:00:00 2001 From: Eric Kingery Date: Fri, 23 Aug 2019 23:31:56 -0500 Subject: [PATCH 412/544] Added GoReleaser config, updated docs, made archive naming adjustments --- .gitignore | 1 + .goreleaser.yml | 203 ++++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 8 +- RELEASE.md | 76 ++++++++++-------- bin/build-all | 99 ----------------------- cli/cli.go | 9 ++- 6 files changed, 257 insertions(+), 139 deletions(-) create mode 100644 .goreleaser.yml delete mode 100755 bin/build-all diff --git a/.gitignore b/.gitignore index 76a9fc205..1f3baf919 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ _obj _test vendor/ +dist/ # Architecture specific extensions/prefixes *.[568vq] diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 000000000..572e09681 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,203 @@ +# You can find the GoReleaser documentation at http://goreleaser.com +project_name: exercism + +builds: +- env: + - CGO_ENABLED=0 + main: ./exercism/main.go + goos: + - darwin + - linux + - windows + - freebsd + - openbsd + goarch: + - amd64 + - 386 + - arm + - ppc64 + goarm: + - 5 + - 6 + ignore: + - goos: openbsd + goarch: arm + - goos: freebsd + goarch: arm + +checksum: + name_template: '{{ .ProjectName }}_checksums.txt' + +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + +archives: + - name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" + replacements: + amd64: x86_64 + 386: i386 + format_overrides: + - goos: windows + format: zip + files: + - shell/**/* + - LICENSE + - README.md + +signs: +- artifacts: checksum + +release: + # Repo in which the release will be created. + # Default is extracted from the origin remote URL. + github: + name: cli + + # If set to true, will not auto-publish the release. + # Default is false. + draft: true + + # If set to auto, will mark the release as not ready for production + # in case there is an indicator for this in the tag e.g. v1.0.0-rc1 + # If set to true, will mark the release as not ready for production. + # Default is false. + prerelease: auto + + # You can change the name of the GitHub release. + # Default is `{{.Tag}}` + name_template: "{{.ProjectName}}-v{{.Version}} {{.Env.USER}}" + +snapcrafts: + - + name: exercism-cli + license: MIT + # Whether to publish the snap to the snapcraft store. + # Remember you need to `snapcraft login` first. + # Defaults to false. + # publish: true + summary: Command-line client for https://exercism.io + # https://snapcraft.io/docs/reference/confinement + confinement: strict + # A snap of type base to be used as the execution environment for this snap. + base: core18 + # https://snapcraft.io/docs/reference/channels + grade: stable + description: Exercism is an online platform designed to help you improve your coding skills through practice and mentorship. Exercism provides you with thousands of exercises spread across numerous language tracks. Each one is a fun and interesting challenge designed to teach you a little more about the features of a language. + name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" + replacements: + amd64: x86_64 + 386: i386 + apps: + exercism: + plugs: ["home", "network", "removable-media"] + + +# [TODO] +# brews: +# - +# # Name template of the recipe +# # Default to project name +# name: myproject +# +# # IDs of the archives to use. +# # Defaults to all. +# ids: +# - foo +# - bar +# +# +# # NOTE: make sure the url_template, the token and given repo (github or gitlab) owner and name are from the +# # same kind. We will probably unify this in the next major version like it is done with scoop. +# +# # Github repository to push the tap to. +# github: +# owner: github-user +# name: homebrew-tap +# +# # OR Gitlab +# # gitlab: +# # owner: gitlab-user +# # name: homebrew-tap +# +# # Template for the url which is determined by the given Token (github or gitlab) +# # Default for github is "https://github.com///releases/download/{{ .Tag }}/{{ .ArtifactName }}" +# # Default for gitlab is "https://gitlab.com///uploads/{{ .ArtifactUploadHash }}/{{ .ArtifactName }}" +# url_template: "http://github.mycompany.com/foo/bar/releases/{{ .Tag }}/{{ .ArtifactName }}" +# +# # Allows you to set a custom download strategy. +# # Default is empty. +# download_strategy: GitHubPrivateRepositoryReleaseDownloadStrategy +# +# # Allows you to add a custom require_relative at the top of the formula template +# # Default is empty +# custom_require: custom_download_strategy +# +# # Git author used to commit to the repository. +# # Defaults are shown. +# commit_author: +# name: goreleaserbot +# email: goreleaser@carlosbecker.com +# +# # Folder inside the repository to put the formula. +# # Default is the root folder. +# folder: Formula +# +# # Caveats for the user of your binary. +# # Default is empty. +# caveats: "How to use this binary" +# +# # Your app's homepage. +# # Default is empty. +# homepage: "https://example.com/" +# +# # Your app's description. +# # Default is empty. +# description: "Software to create fast and easy drum rolls." +# +# # Setting this will prevent goreleaser to actually try to commit the updated +# # formula - instead, the formula file will be stored on the dist folder only, +# # leaving the responsibility of publishing it to the user. +# # If set to auto, the release will not be uploaded to the homebrew tap +# # in case there is an indicator for prerelease in the tag e.g. v1.0.0-rc1 +# # Default is false. +# skip_upload: true +# +# # Custom block for brew. +# # Can be used to specify alternate downloads for devel or head releases. +# # Default is empty. +# custom_block: | +# head "https://github.com/some/package.git" +# ... +# +# # Packages your package depends on. +# dependencies: +# - git +# - zsh +# +# # Packages that conflict with your package. +# conflicts: +# - svn +# - bash +# +# # Specify for packages that run as a service. +# # Default is empty. +# plist: | +# +# ... +# +# # So you can `brew test` your formula. +# # Default is empty. +# test: | +# system "#{bin}/program --version" +# ... +# +# # Custom install script for brew. +# # Default is 'bin.install "program"'. +# install: | +# bin.install "program" +# ... +# diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e170e9da2..df85ab2cf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,9 +49,5 @@ On Windows: - `go build -o testercism.exe exercism\main.go` - `testercism.exe —h` -### Building for All Platforms - -In order to cross-compile for all platforms, run `bin/build-all`. The binaries -will be built into the `release` directory. - -[fork]: https://github.com/exercism/cli/fork +### Releasing a new CLI version +Consult the [release documentation](https://github.com/exercism/cli/master/RELEASE.md). diff --git a/RELEASE.md b/RELEASE.md index 71aecd0a3..f13a02357 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,25 +1,49 @@ # Cutting a CLI Release -## Bootstrap Cross-Compilation for Go - -**This only has to be done once.** - -Change directory to the go source. Then run the bootstrap command for -each operating system and architecture. - -```plain -$ cd `which go`/../../src -$ sudo GCO_ENABLED=0 GOOS=windows GOARCH=386 ./make.bash --no-clean -$ sudo GCO_ENABLED=0 GOOS=darwin GOARCH=386 ./make.bash --no-clean -$ sudo GCO_ENABLED=0 GOOS=linux GOARCH=386 ./make.bash --no-clean -$ sudo GCO_ENABLED=0 GOOS=windows GOARCH=amd64 ./make.bash --no-clean -$ sudo GCO_ENABLED=0 GOOS=darwin GOARCH=amd64 ./make.bash --no-clean -$ sudo GCO_ENABLED=0 GOOS=linux GOARCH=amd64 ./make.bash --no-clean -$ sudo GCO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 ./make.bash --no-clean -$ sudo GCO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 ./make.bash --no-clean +The Exercism CLI uses [GoReleaser](https://goreleaser.com) to automate the +release process. + +## Requirements + +1. [Install GoReleaser](https://goreleaser.com/install/) +1. [Install snapcraft](https://snapcraft.io/docs/snapcraft-overview) +1. [Setup GitHub token](https://goreleaser.com/environment/#github-token) +1. Have a gpg key installed on your machine - it is [used for signing the artifacts](https://goreleaser.com/sign/) + +## Cut a release + +```bash + +# Test run +goreleaser --skip-publish --snapshot --rm-dist + +# Commit any changes, then create a new tag and push it +git tag -a v3.0.16 -m "Trying out GoReleaser" +git push origin v3.0.16 + +# Build and release +goreleaser --rm-dist + +# Remember to update cmd/version.go in the code +# (until we use: https://goreleaser.com/environment/#using-the-main-version) + +# You must be logged into snapcraft to publish a new snap +snapcraft login + +# Push to snapcraft +for f in `ls dist/*.snap`; do snapcraft push --release=stable $f; done + +# [TODO] Push to homebrew + +# Run [exercism-cp-archive-hack.sh](https://gist.github.com/ekingery/961650fca4e2233098c8320f32736836) which takes the new archive files and renames them to match the old naming scheme for backward compatibility. ``` -## Update the Changelog +Lastly, head to [the release page](https://github.com/exercism/cli/releases) to test and publish the draft. Until mid to late 2020, we will need to manually upload the backward-compatible archive files generated in `/tmp/exercism_tmp_upload` by the shell script referenced above. + + +## Confirm / Update the Changelog + +GoReleaser supports [auto generation of changelog](https://goreleaser.com/customization/#customize-the-changelog) that we will want to customize to meet our standards (not including refactors, test updates, etc). We should also consider [the release notes](https://goreleaser.com/customization/#custom-release-notes). Make sure all the recent changes are reflected in the "next release" section of the Changelog. Make this a separate commit from bumping the version. @@ -27,6 +51,7 @@ of the Changelog. Make this a separate commit from bumping the version. You can view changes using the /compare/ view: https://github.com/exercism/cli/compare/$PREVIOUS_RELEASE...master + ## Bump the version Edit the `Version` constant in `cmd/version.go`, and edit the Changelog. @@ -38,27 +63,16 @@ The "next release" section should contain only "Your contribution here". _Note: It's useful to add the version to the commit message when you bump it: e.g. `Bump version to v2.3.4`._ -## Generate the Binaries - -```plain -$ rm release/* -$ CGO_ENABLED=0 bin/build-all -``` +In the future we will probably want to replace the hardcoded `Version` constant with [main.version](https://goreleaser.com/environment/#using-the-main-version). Here is a [stack overflow post on injecting to cmd/version.go](https://stackoverflow.com/a/47510909). ## Cut Release on GitHub Go to [the exercism/cli "new release" page](https://github.com/exercism/cli/releases/new). -Describe the release, select a specific commit to target, name the version `v{VERSION}`, where -VERSION matches the value of the `Version` constant. - -Upload all the binaries from `release/*`. - -Paste the release text and describe the new changes. +A draft will have been auto-generated by GoReleaser. Describe the release, select a specific commit to target, paste the release text and describe the new changes. ``` To install, follow the interactive installation instructions at https://exercism.io/cli-walkthrough - --- [describe changes in this release] diff --git a/bin/build-all b/bin/build-all deleted file mode 100755 index 36ebe54fb..000000000 --- a/bin/build-all +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash - -set -e -x - -echo "Creating release dir..." -mkdir -p release - -# variables as defined by "go tool nm" -OSVAR=github.com/exercism/cli/cmd.BuildOS -ARCHVAR=github.com/exercism/cli/cmd.BuildARCH -ARMVAR=github.com/exercism/cli/cmd.BuildARM - -# handle alternate binary name for pre-releases -BINNAME=${NAME:-exercism} - -createRelease() { - os=$1 - arch=$2 - arm=$3 - - if [ "$os" = darwin ] - then - osname='mac' - else - osname=$os - fi - if [ "$arch" = amd64 ] - then - osarch=64bit - elif [ "$os" = linux ] && [ "$arch" = ppc64 ] - then - osarch=ppc64 - else - osarch=32bit - - fi - - ldflags="-s -w -X $OSVAR=$os -X $ARCHVAR=$arch" - if [ "$arm" ] - then - osarch=arm-v$arm - ldflags="$ldflags -X $ARMVAR=$arm" - elif [ "$arch" = arm64 ] - then - osarch=arm-v8 - ldflags="$ldflags -X $ARMVAR=8" - fi - - binname=$BINNAME - if [ "$osname" = windows ] - then - binname="$binname.exe" - fi - - echo "Creating $os/$arch binary..." - - if [ "$arm" ] - then - GOOS=$os GOARCH=$arch GOARM=$arm go build -ldflags "$ldflags" -o "out/$binname" exercism/main.go - else - GOOS=$os GOARCH=$arch go build -ldflags "$ldflags" -o "out/$binname" exercism/main.go - fi - - release_name="release/$BINNAME-$osname-$osarch" - if [ "$osname" = windows ]; then - (cd out && zip "../$release_name.zip" ../shell/* "./$binname") - else - tar cvzf "$release_name.tgz" shell -C out "./$binname" - fi -} - -# Mac Releases -createRelease darwin 386 -createRelease darwin amd64 - -# PowerPC Releases -createRelease linux ppc64 - -# Linux Releases -createRelease linux 386 -createRelease linux amd64 - -# FreeBSD Releases -createRelease freebsd 386 -createRelease freebsd amd64 - -# OpenBSD Releases -createRelease openbsd 386 -createRelease openbsd amd64 - -# ARM Releases -createRelease linux arm 5 -createRelease linux arm 6 -createRelease linux arm 7 -createRelease linux arm64 - -# Windows Releases -createRelease windows 386 -createRelease windows amd64 diff --git a/cli/cli.go b/cli/cli.go index 29f6d64a3..4312eb018 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -30,15 +30,18 @@ var ( var ( osMap = map[string]string{ - "darwin": "mac", + "darwin": "darwin", + "freebsd": "freebsd", "linux": "linux", + "openbsd": "openbsd", "windows": "windows", } archMap = map[string]string{ - "amd64": "64bit", - "386": "32bit", + "386": "i386", + "amd64": "x86_64", "arm": "arm", + "ppc64": "ppc64", } ) From a9e53b680c1768a2463ee08c33e74a0c39c231d1 Mon Sep 17 00:00:00 2001 From: Eric Kingery Date: Thu, 24 Oct 2019 09:40:50 -0500 Subject: [PATCH 413/544] Updating goreleaser github repo config and release documentation --- .goreleaser.yml | 1 + RELEASE.md | 62 ++++++++++++++++++++----------------------------- 2 files changed, 26 insertions(+), 37 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 572e09681..6ba014c64 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -55,6 +55,7 @@ release: # Repo in which the release will be created. # Default is extracted from the origin remote URL. github: + owner: exercism name: cli # If set to true, will not auto-publish the release. diff --git a/RELEASE.md b/RELEASE.md index f13a02357..e72ed7400 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -10,23 +10,38 @@ release process. 1. [Setup GitHub token](https://goreleaser.com/environment/#github-token) 1. Have a gpg key installed on your machine - it is [used for signing the artifacts](https://goreleaser.com/sign/) +## Confirm / Update the Changelog + +Make sure all the recent changes are reflected in the "next release" section of the CHANGELOG.md file. All the changes in the "next release" section should be moved to a new section that describes the version number, and gives it a date. + +You can view changes using the /compare/ view: +https://github.com/exercism/cli/compare/$PREVIOUS_RELEASE...master + +GoReleaser supports the [auto generation of a changelog](https://goreleaser.com/customization/#customize-the-changelog) we will want to customize to meet our standards (not including refactors, test updates, etc). We should also consider using [the release notes feature](https://goreleaser.com/customization/#custom-release-notes). + +## Bump the version + +Edit the `Version` constant in `cmd/version.go` + +_Note: It's useful to add the version to the commit message when you bump it: e.g. `Bump version to v2.3.4`._ + +In the future we will probably want to replace the hardcoded `Version` constant with [main.version](https://goreleaser.com/environment/#using-the-main-version). Here is a [stack overflow post on injecting to cmd/version.go](https://stackoverflow.com/a/47510909). + +Commit this change on a branch along with the CHANGELOG updates in a single commit, and create a PR for merge to master. + ## Cut a release ```bash - # Test run goreleaser --skip-publish --snapshot --rm-dist -# Commit any changes, then create a new tag and push it +# Create a new tag on the master branch and push it git tag -a v3.0.16 -m "Trying out GoReleaser" git push origin v3.0.16 # Build and release goreleaser --rm-dist -# Remember to update cmd/version.go in the code -# (until we use: https://goreleaser.com/environment/#using-the-main-version) - # You must be logged into snapcraft to publish a new snap snapcraft login @@ -34,51 +49,24 @@ snapcraft login for f in `ls dist/*.snap`; do snapcraft push --release=stable $f; done # [TODO] Push to homebrew - -# Run [exercism-cp-archive-hack.sh](https://gist.github.com/ekingery/961650fca4e2233098c8320f32736836) which takes the new archive files and renames them to match the old naming scheme for backward compatibility. ``` -Lastly, head to [the release page](https://github.com/exercism/cli/releases) to test and publish the draft. Until mid to late 2020, we will need to manually upload the backward-compatible archive files generated in `/tmp/exercism_tmp_upload` by the shell script referenced above. - - -## Confirm / Update the Changelog - -GoReleaser supports [auto generation of changelog](https://goreleaser.com/customization/#customize-the-changelog) that we will want to customize to meet our standards (not including refactors, test updates, etc). We should also consider [the release notes](https://goreleaser.com/customization/#custom-release-notes). - -Make sure all the recent changes are reflected in the "next release" section -of the Changelog. Make this a separate commit from bumping the version. - -You can view changes using the /compare/ view: -https://github.com/exercism/cli/compare/$PREVIOUS_RELEASE...master - - -## Bump the version - -Edit the `Version` constant in `cmd/version.go`, and edit the Changelog. - -All the changes in the "next release" section should be moved to a new section -that describes the version number, and gives it a date. - -The "next release" section should contain only "Your contribution here". - -_Note: It's useful to add the version to the commit message when you bump it: e.g. `Bump version to v2.3.4`._ - -In the future we will probably want to replace the hardcoded `Version` constant with [main.version](https://goreleaser.com/environment/#using-the-main-version). Here is a [stack overflow post on injecting to cmd/version.go](https://stackoverflow.com/a/47510909). - ## Cut Release on GitHub -Go to [the exercism/cli "new release" page](https://github.com/exercism/cli/releases/new). +Run [exercism-cp-archive-hack.sh](https://gist.github.com/ekingery/961650fca4e2233098c8320f32736836) which takes the new archive files and renames them to match the old naming scheme for backward compatibility. Until mid to late 2020, we will need to manually upload the backward-compatible archive files generated in `/tmp/exercism_tmp_upload`. -A draft will have been auto-generated by GoReleaser. Describe the release, select a specific commit to target, paste the release text and describe the new changes. +The generated archive files should be uploaded to the [draft release page created by GoReleaser](https://github.com/exercism/cli/releases). Describe the release, select a specific commit to target, paste the following release text, and describe the new changes. ``` To install, follow the interactive installation instructions at https://exercism.io/cli-walkthrough --- [describe changes in this release] - ``` + Lastly, test and publish the draft + + ## Update Homebrew This is helpful for the (many) Mac OS X users. From 978ee143c0e4bc0dfd78fda606722e96296e6334 Mon Sep 17 00:00:00 2001 From: Eric Kingery Date: Thu, 24 Oct 2019 09:36:27 -0500 Subject: [PATCH 414/544] Bumping version to v3.0.13 --- CHANGELOG.md | 6 ++++++ cmd/version.go | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 724403b8c..7c8982c2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ## Next Release * **Your contribution here** +## v3.0.13 (2019-10-23) +* [#866](https://github.com/exercism/cli/pull/866) The API token outputted during verbose will now be masked by default - [@Jrank2013] +* [#873](https://github.com/exercism/cli/pull/873) Make all errors in cmd package checked - [@avegner] +* [#871](https://github.com/exercism/cli/pull/871) Error message returned if the track is locked - [@Jrank2013] +* [#886](https://github.com/exercism/cli/pull/886) Added GoReleaser config, updated docs, made archive naming adjustments - [@ekingery] + ## v3.0.12 (2019-07-07) * [#770](https://github.com/exercism/cli/pull/770) Print API error messages in submit command - [@Smarticles101] * [#763](https://github.com/exercism/cli/pull/763) Add Fish shell tab completions - [@John-Goff] diff --git a/cmd/version.go b/cmd/version.go index 45c88ff55..3b81f5897 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.0.12" +const Version = "3.0.13" // checkLatest flag for version command. var checkLatest bool From f17637d92f46e23724b946583ee4293b691589a4 Mon Sep 17 00:00:00 2001 From: Eric Kingery Date: Thu, 24 Oct 2019 13:23:11 -0500 Subject: [PATCH 415/544] Fixed links in changelog, updated goreleaser config to fix snapcraft name and improve / cleanup comments --- .goreleaser.yml | 112 ++---------------------------------------------- CHANGELOG.md | 3 ++ RELEASE.md | 2 +- 3 files changed, 8 insertions(+), 109 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 6ba014c64..564944e1a 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -72,9 +72,12 @@ release: # Default is `{{.Tag}}` name_template: "{{.ProjectName}}-v{{.Version}} {{.Env.USER}}" +# brews: +# We do not use the brew config, which is for taps, not core forumulas. + snapcrafts: - - name: exercism-cli + name: exercism license: MIT # Whether to publish the snap to the snapcraft store. # Remember you need to `snapcraft login` first. @@ -95,110 +98,3 @@ snapcrafts: apps: exercism: plugs: ["home", "network", "removable-media"] - - -# [TODO] -# brews: -# - -# # Name template of the recipe -# # Default to project name -# name: myproject -# -# # IDs of the archives to use. -# # Defaults to all. -# ids: -# - foo -# - bar -# -# -# # NOTE: make sure the url_template, the token and given repo (github or gitlab) owner and name are from the -# # same kind. We will probably unify this in the next major version like it is done with scoop. -# -# # Github repository to push the tap to. -# github: -# owner: github-user -# name: homebrew-tap -# -# # OR Gitlab -# # gitlab: -# # owner: gitlab-user -# # name: homebrew-tap -# -# # Template for the url which is determined by the given Token (github or gitlab) -# # Default for github is "https://github.com///releases/download/{{ .Tag }}/{{ .ArtifactName }}" -# # Default for gitlab is "https://gitlab.com///uploads/{{ .ArtifactUploadHash }}/{{ .ArtifactName }}" -# url_template: "http://github.mycompany.com/foo/bar/releases/{{ .Tag }}/{{ .ArtifactName }}" -# -# # Allows you to set a custom download strategy. -# # Default is empty. -# download_strategy: GitHubPrivateRepositoryReleaseDownloadStrategy -# -# # Allows you to add a custom require_relative at the top of the formula template -# # Default is empty -# custom_require: custom_download_strategy -# -# # Git author used to commit to the repository. -# # Defaults are shown. -# commit_author: -# name: goreleaserbot -# email: goreleaser@carlosbecker.com -# -# # Folder inside the repository to put the formula. -# # Default is the root folder. -# folder: Formula -# -# # Caveats for the user of your binary. -# # Default is empty. -# caveats: "How to use this binary" -# -# # Your app's homepage. -# # Default is empty. -# homepage: "https://example.com/" -# -# # Your app's description. -# # Default is empty. -# description: "Software to create fast and easy drum rolls." -# -# # Setting this will prevent goreleaser to actually try to commit the updated -# # formula - instead, the formula file will be stored on the dist folder only, -# # leaving the responsibility of publishing it to the user. -# # If set to auto, the release will not be uploaded to the homebrew tap -# # in case there is an indicator for prerelease in the tag e.g. v1.0.0-rc1 -# # Default is false. -# skip_upload: true -# -# # Custom block for brew. -# # Can be used to specify alternate downloads for devel or head releases. -# # Default is empty. -# custom_block: | -# head "https://github.com/some/package.git" -# ... -# -# # Packages your package depends on. -# dependencies: -# - git -# - zsh -# -# # Packages that conflict with your package. -# conflicts: -# - svn -# - bash -# -# # Specify for packages that run as a service. -# # Default is empty. -# plist: | -# -# ... -# -# # So you can `brew test` your formula. -# # Default is empty. -# test: | -# system "#{bin}/program --version" -# ... -# -# # Custom install script for brew. -# # Default is 'bin.install "program"'. -# install: | -# bin.install "program" -# ... -# diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c8982c2e..ccec41b11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -423,6 +423,7 @@ All changes by [@msgehard] * Build on Travis [@AlexWheeler]: https://github.com/AlexWheeler +[@avegner]: https://github.com/avegner [@Dparker1990]: https://github.com/Dparker1990 [@John-Goff]: https://github.com/John-Goff [@LegalizeAdulthood]: https://github.com/LegalizeAdulthood @@ -446,6 +447,7 @@ All changes by [@msgehard] [@dpritchett]: https://github.com/dpritchett [@eToThePiIPower]: https://github.com/eToThePiIPower [@ebautistabar]: https://github.com/ebautistabar +[@ekingery]: https://github.com/ekingery [@elimisteve]: https://github.com/elimisteve [@ests]: https://github.com/ests [@farisj]: https://github.com/farisj @@ -457,6 +459,7 @@ All changes by [@msgehard] [@jdsutherland]: https://github.com/jdsutherland [@jgsqware]: https://github.com/jgsqware [@jish]: https://github.com/jish +[@Jrank2013]: https://github.com/Jrank2013 [@jppunnett]: https://github.com/jppunnett [@katrinleinweber]: https://github.com/katrinleinweber [@kytrinyx]: https://github.com/kytrinyx diff --git a/RELEASE.md b/RELEASE.md index e72ed7400..c8c1c92f0 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -92,7 +92,7 @@ brew update brew bump-formula-pr --strict exercism --url=https://github.com/exercism/cli/archive/vX.Y.Z.tar.gz --sha256=$SHA ``` -For more information see their [contribution guidelines](https://github.com/Homebrew/homebrew/blob/master/share/doc/homebrew/How-To-Open-a-Homebrew-Pull-Request-(and-get-it-merged).md#how-to-open-a-homebrew-pull-request-and-get-it-merged). +For more information see [How To Open a Homebrew Pull Request](https://docs.brew.sh/How-To-Open-a-Homebrew-Pull-Request). ## Update the docs site From 01cb5bc0df991af80ad871d06dad6a4ea6148cc8 Mon Sep 17 00:00:00 2001 From: Shirley Leu Date: Tue, 9 Feb 2021 16:08:27 +0100 Subject: [PATCH 416/544] Improve error message 'not in workspace' for MacOS (#968) * Improve error message 'not in workspace' for MacOS Default installation of MacOS allows user to change directory insensitive of case. This commit wraps a supplemental error message for Mac users, including prints of the workspace and submit path. * go fmt * Update go version in travis.yml Authored-by: Shirley Leu --- .travis.yml | 3 +-- go.mod | 2 ++ workspace/workspace.go | 9 ++++++++- workspace/workspace_darwin_test.go | 25 +++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 workspace/workspace_darwin_test.go diff --git a/.travis.yml b/.travis.yml index a30fc6eca..a195a1099 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,7 @@ language: go sudo: false go: - - "1.11.x" - - "1.12.x" + - "1.15.x" - tip matrix: diff --git a/go.mod b/go.mod index c0d986d0f..a3723bfad 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,7 @@ module github.com/exercism/cli +go 1.15 + require ( github.com/blang/semver v3.5.1+incompatible github.com/davecgh/go-spew v1.1.0 diff --git a/workspace/workspace.go b/workspace/workspace.go index 71036f511..eb9875e8a 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -2,9 +2,11 @@ package workspace import ( "errors" + "fmt" "io/ioutil" "os" "path/filepath" + "runtime" "strings" ) @@ -121,7 +123,12 @@ func (ws Workspace) Exercises() ([]Exercise, error) { // This is the directory that contains the exercise metadata file. func (ws Workspace) ExerciseDir(s string) (string, error) { if !strings.HasPrefix(s, ws.Dir) { - return "", errors.New("not in workspace") + var err = fmt.Errorf("not in workspace") + if runtime.GOOS == "darwin" { + err = fmt.Errorf("%w: directory location may be case sensitive: workspace directory: %s, "+ + "submit path: %s", err, ws.Dir, s) + } + return "", err } path := s diff --git a/workspace/workspace_darwin_test.go b/workspace/workspace_darwin_test.go new file mode 100644 index 000000000..03794a795 --- /dev/null +++ b/workspace/workspace_darwin_test.go @@ -0,0 +1,25 @@ +package workspace + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "path/filepath" + "runtime" + "strings" + "testing" +) + +func TestExerciseDir_case_insensitive(t *testing.T) { + _, cwd, _, _ := runtime.Caller(0) + root := filepath.Join(cwd, "..", "..", "fixtures", "solution-dir") + // configuration file was set with "workspace" - the directory that exists + configured := Workspace{Dir: filepath.Join(root, "workspace")} + // user changes into directory with "bad" case - "Workspace" + userPath := strings.Replace(configured.Dir, "workspace", "Workspace", 1) + + _, err := configured.ExerciseDir(filepath.Join(userPath, "exercise", "file.txt")) + + assert.Error(t, err) + assert.Equal(t, fmt.Sprintf("not in workspace: directory location may be case sensitive: "+ + "workspace directory: %s, submit path: %s/exercise/file.txt", configured.Dir, userPath), err.Error()) +} From aa9dcfaa6ba43a69e10a3792af43f9f313f4284a Mon Sep 17 00:00:00 2001 From: Eric Kingery Date: Tue, 9 Feb 2021 09:55:04 -0600 Subject: [PATCH 417/544] Allow read-write access to configuration and download directories when installed as a Snap (#980) * Allow read-write access to configuration and download directories. We need write access to the $HOME/.config/exercism/ directory for `exercism config` and $HOME/exercism is the default download directory so we need write access there as well. This should resolve issue #945 * Permit writes to any files in user's home directory Authored-by: Richard Neish --- .goreleaser.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 564944e1a..19fb72b2a 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -97,4 +97,8 @@ snapcrafts: 386: i386 apps: exercism: - plugs: ["home", "network", "removable-media"] + plugs: ["home", "network", "removable-media","personal-files"] + plugs: + personal-files: + write: + - $HOME/ From ce8f497d2439a8094ee39e15aa72d5d62a54c759 Mon Sep 17 00:00:00 2001 From: Ali A <1076+haguro@users.noreply.github.com> Date: Wed, 10 Feb 2021 03:45:17 +1100 Subject: [PATCH 418/544] Protect existing solutions from being overwritten by 'download' (#979) Add an optional `--force` flag to the download command to overwrite an existing exercise directory. --- cmd/download.go | 14 +++++- cmd/download_test.go | 102 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index e4c19377b..7bf278bff 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -64,6 +64,10 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { metadata := download.payload.metadata() dir := metadata.Exercise(usrCfg.GetString("workspace")).MetadataDir() + if _, err = os.Stat(dir); !download.forceoverwrite && err == nil { + return fmt.Errorf("directory '%s' already exists, use --force to overwrite", dir) + } + if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { return err } @@ -103,7 +107,6 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { continue } - // TODO: handle collisions path := sf.relativePath() dir := filepath.Join(metadata.Dir, filepath.Dir(path)) if err = os.MkdirAll(dir, os.FileMode(0755)); err != nil { @@ -133,7 +136,8 @@ type download struct { token, apibaseurl, workspace string // optional - track, team string + track, team string + forceoverwrite bool payload *downloadPayload } @@ -158,6 +162,11 @@ func newDownload(flags *pflag.FlagSet, usrCfg *viper.Viper) (*download, error) { return nil, err } + d.forceoverwrite, err = flags.GetBool("force") + if err != nil { + return nil, err + } + d.token = usrCfg.GetString("token") d.apibaseurl = usrCfg.GetString("apibaseurl") d.workspace = usrCfg.GetString("workspace") @@ -354,6 +363,7 @@ func setupDownloadFlags(flags *pflag.FlagSet) { flags.StringP("track", "t", "", "the track ID") flags.StringP("exercise", "e", "", "the exercise slug") flags.StringP("team", "T", "", "the team slug") + flags.BoolP("force", "F", false, "overwrite existing exercise directory") } func init() { diff --git a/cmd/download_test.go b/cmd/download_test.go index 2eb0418df..676d90518 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -209,6 +209,108 @@ func TestDownload(t *testing.T) { } } +func TestDownloadToExistingDirectory(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + + testCases := []struct { + exerciseDir string + flags map[string]string + }{ + { + exerciseDir: filepath.Join("bogus-track", "bogus-exercise"), + flags: map[string]string{"exercise": "bogus-exercise", "track": "bogus-track"}, + }, + { + exerciseDir: filepath.Join("teams", "bogus-team", "bogus-track", "bogus-exercise"), + flags: map[string]string{"exercise": "bogus-exercise", "track": "bogus-track", "team": "bogus-team"}, + }, + } + + for _, tc := range testCases { + tmpDir, err := ioutil.TempDir("", "download-cmd") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + err = os.MkdirAll(filepath.Join(tmpDir, tc.exerciseDir), os.FileMode(0755)) + assert.NoError(t, err) + + ts := fakeDownloadServer("true", "") + defer ts.Close() + + v := viper.New() + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + v.Set("token", "abc123") + + cfg := config.Config{ + UserViperConfig: v, + } + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupDownloadFlags(flags) + for name, value := range tc.flags { + flags.Set(name, value) + } + + err = runDownload(cfg, flags, []string{}) + + if assert.Error(t, err) { + assert.Regexp(t, "directory '.+' already exists", err.Error()) + } + } +} + +func TestDownloadToExistingDirectoryWithForce(t *testing.T) { + co := newCapturedOutput() + co.override() + defer co.reset() + + testCases := []struct { + exerciseDir string + flags map[string]string + }{ + { + exerciseDir: filepath.Join("bogus-track", "bogus-exercise"), + flags: map[string]string{"exercise": "bogus-exercise", "track": "bogus-track"}, + }, + { + exerciseDir: filepath.Join("teams", "bogus-team", "bogus-track", "bogus-exercise"), + flags: map[string]string{"exercise": "bogus-exercise", "track": "bogus-track", "team": "bogus-team"}, + }, + } + + for _, tc := range testCases { + tmpDir, err := ioutil.TempDir("", "download-cmd") + defer os.RemoveAll(tmpDir) + assert.NoError(t, err) + + err = os.MkdirAll(filepath.Join(tmpDir, tc.exerciseDir), os.FileMode(0755)) + assert.NoError(t, err) + + ts := fakeDownloadServer("true", "") + defer ts.Close() + + v := viper.New() + v.Set("workspace", tmpDir) + v.Set("apibaseurl", ts.URL) + v.Set("token", "abc123") + + cfg := config.Config{ + UserViperConfig: v, + } + flags := pflag.NewFlagSet("fake", pflag.PanicOnError) + setupDownloadFlags(flags) + for name, value := range tc.flags { + flags.Set(name, value) + } + flags.Set("force", "true") + + err = runDownload(cfg, flags, []string{}) + assert.NoError(t, err) + } +} + func fakeDownloadServer(requestor, teamSlug string) *httptest.Server { mux := http.NewServeMux() server := httptest.NewServer(mux) From 09344267e352fd90b2be07f7f665b8f1502bf733 Mon Sep 17 00:00:00 2001 From: Ali A <1076+haguro@users.noreply.github.com> Date: Sun, 14 Feb 2021 10:20:29 +1100 Subject: [PATCH 419/544] Check if authorisation header is set before attempting to extract token (#981) * Check if authorisation header is set before attempting to extract token * Add unit tests for debug.DumpRequest and debug.DumpResponse. --- debug/debug.go | 8 ++--- debug/debug_test.go | 71 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/debug/debug.go b/debug/debug.go index dad0b629c..133681439 100644 --- a/debug/debug.go +++ b/debug/debug.go @@ -44,10 +44,10 @@ func DumpRequest(req *http.Request) { body := io.TeeReader(req.Body, &bodyCopy) req.Body = ioutil.NopCloser(body) - temp := req.Header.Get("Authorization") + authHeader := req.Header.Get("Authorization") - if !UnmaskAPIKey { - if token := strings.Split(temp, " ")[1]; token != "" { + if authParts := strings.Split(authHeader, " "); len(authParts) > 1 && !UnmaskAPIKey { + if token := authParts[1]; token != "" { req.Header.Set("Authorization", "Bearer "+Redact(token)) } } @@ -62,7 +62,7 @@ func DumpRequest(req *http.Request) { Println("========================= END DumpRequest =========================") Println("") - req.Header.Set("Authorization", temp) + req.Header.Set("Authorization", authHeader) req.Body = ioutil.NopCloser(&bodyCopy) } diff --git a/debug/debug_test.go b/debug/debug_test.go index 8bbe3a857..f9be5f864 100644 --- a/debug/debug_test.go +++ b/debug/debug_test.go @@ -2,6 +2,7 @@ package debug import ( "bytes" + "net/http" "testing" "github.com/stretchr/testify/assert" @@ -29,6 +30,76 @@ func TestVerboseDisabled(t *testing.T) { } } +func TestDumpRequest(t *testing.T) { + testCases := []struct { + desc string + auth string + verbose bool + unmask bool + }{ + { + desc: "Do not attempt to dump request if 'Verbose' is set to false", + auth: "", + verbose: false, + unmask: false, + }, + { + desc: "Dump request without authorization header", + auth: "", //not set + verbose: true, + unmask: false, + }, + { + desc: "Dump request with malformed 'Authorization' header", + auth: "malformed", + verbose: true, + unmask: true, + }, + { + desc: "Dump request with properly formed 'Authorization' header", + auth: "Bearer abc12-345abcde1234-5abc12", + verbose: true, + unmask: false, + }, + } + + b := &bytes.Buffer{} + output = b + for _, tc := range testCases { + Verbose = tc.verbose + UnmaskAPIKey = tc.unmask + r, _ := http.NewRequest("GET", "https://api.example.com/bogus", nil) + if tc.auth != "" { + r.Header.Set("Authorization", tc.auth) + } + + DumpRequest(r) + if tc.verbose { + assert.Regexp(t, "GET /bogus", b.String(), tc.desc) + assert.Equal(t, tc.auth, r.Header.Get("Authorization"), tc.desc) + if tc.unmask { + assert.Regexp(t, "Authorization: "+tc.auth, b.String(), tc.desc) + } + } else { + assert.NotRegexp(t, "GET /bogus", b.String(), tc.desc) + } + } +} + +func TestDumpResponse(t *testing.T) { + b := &bytes.Buffer{} + output = b + Verbose = true + r := &http.Response{ + StatusCode: 200, + ProtoMajor: 1, + ProtoMinor: 1, + } + + DumpResponse(r) + assert.Regexp(t, "HTTP/1.1 200 OK", b.String()) +} + func TestRedact(t *testing.T) { fakeToken := "1a11111aaaa111aa1a11111a11111aa1" expected := "1a11*************************aa1" From 7cc4e6cda6a85e355520dbe81f2291f2ccbf36b1 Mon Sep 17 00:00:00 2001 From: Stig Johan Berggren Date: Tue, 16 Mar 2021 13:07:48 +0100 Subject: [PATCH 420/544] Fix instructions for completion in Oh My Zsh (#881) * Fix instructions for completion in Oh My Zsh * Use the $ZSH_CUSTOM environment variable * Apply suggestions * Updated link to OMZ repository * title case in section title --- shell/README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/shell/README.md b/shell/README.md index b4c1509ce..beee3c47a 100644 --- a/shell/README.md +++ b/shell/README.md @@ -30,9 +30,22 @@ and then add the directory to your `$fpath` in your `.zshrc`, `.zsh_profile` or autoload -U compinit && compinit -#### Oh my Zsh +#### Oh My Zsh -If you are using the popular [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) framework to manage your zsh plugins, move the file `exercism_completion.zsh` into `~/.oh-my-zsh/custom`. +If you are using the popular [Oh My Zsh][oh-my-zsh] framework to manage your +zsh plugins, you need to move the file `exercism_completion.zsh` to a new +custom plugin: + +[oh-my-zsh]: https://github.com/ohmyzsh/ohmyzsh + + mkdir -p $ZSH_CUSTOM/plugins/exercism + cp exercism_completion.zsh $ZSH_CUSTOM/plugins/exercism/_exercism + +Then edit the file `~/.zshrc` to include `exercism` in the list of plugins. +Completions will be activated the next time you open a new shell. If the +completions do not work, you should update Oh My Zsh to the latest version with +`omz update`. Oh My Zsh now checks whether the plugin list has changed (more +accurately, `$fpath`) and resets the `zcompdump` file. ### Fish From 6d5db1597b0938b6a730d1e023a30a800328ba0d Mon Sep 17 00:00:00 2001 From: Eric Kingery Date: Mon, 17 May 2021 09:09:04 -0500 Subject: [PATCH 421/544] Fixed broken link to release documentation (#993) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index df85ab2cf..cf13f1181 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,4 +50,4 @@ On Windows: - `testercism.exe —h` ### Releasing a new CLI version -Consult the [release documentation](https://github.com/exercism/cli/master/RELEASE.md). +Consult the [release documentation](RELEASE.md). From 6d398de33519fe3b5981432af06ad9f17c25839f Mon Sep 17 00:00:00 2001 From: Exercism Bot <66069679+exercism-bot@users.noreply.github.com> Date: Tue, 6 Jul 2021 12:19:19 +0100 Subject: [PATCH 422/544] =?UTF-8?q?=F0=9F=A4=96=20Sync=20org-wide=20files?= =?UTF-8?q?=20to=20upstream=20repo=20(#999)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit More info: https://github.com/exercism/org-wide-files/commit/53f167009e30c254dcf27cfd60a86c912088e36f --- .github/labels.yml | 140 ++++++++++++++++++++++++++++++ .github/workflows/sync-labels.yml | 21 +++++ 2 files changed, 161 insertions(+) create mode 100644 .github/labels.yml create mode 100644 .github/workflows/sync-labels.yml diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 000000000..1e684c04d --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,140 @@ +# --------------------------------------------------------------- # +# This is an auto-generated file - Do not manually edit this file # +# --------------------------------------------------------------- # + +# This file is automatically generated by concatenating two files: +# +# 1. The Exercism-wide labels: defined in https://github.com/exercism/org-wide-files/blob/main/global-files/.github/labels.yml +# 2. The repository-specific labels: defined in the `.appends/.github/labels.yml` file within this repository. +# +# If any of these two files change, a pull request is automatically containing a re-generated version of this file. +# Consequently, to change repository-specific labels you should update the `.appends/.github/labels.yml` file and _not_ this file. +# +# When the pull request has been merged, the GitHub labels will be automatically updated by the "Sync labels" workflow. +# This typically takes 5-10 minutes. + +# --------------------------------------------------------------------- # +# These are the Exercism-wide labels which are shared across all repos. # +# --------------------------------------------------------------------- # + +# The following Exercism-wide labels are used to show "tasks" on the website, which will point users to things they can help contribute with. + +# The `x:action/` labels describe what sort of work the contributor will be engaged in when working on the issue +- name: "x:action/create" + description: "Work on something from scratch" + color: "6f60d2" + +- name: "x:action/fix" + description: "Fix an issue" + color: "6f60d2" + +- name: "x:action/improve" + description: "Improve existing functionality/content" + color: "6f60d2" + +- name: "x:action/proofread" + description: "Proofread text" + color: "6f60d2" + +- name: "x:action/sync" + description: "Sync content with its latest version" + color: "6f60d2" + +# The `x:knowledge/` labels describe how much Exercism knowledge is required by the contributor +- name: "x:knowledge/none" + description: "No existing Exercism knowledge required" + color: "604fcd" + +- name: "x:knowledge/elementary" + description: "Little Exercism knowledge required" + color: "604fcd" + +- name: "x:knowledge/intermediate" + description: "Quite a bit of Exercism knowledge required" + color: "604fcd" + +- name: "x:knowledge/advanced" + description: "Comprehensive Exercism knowledge required" + color: "604fcd" + +# The `x:module/` labels indicate what part of Exercism the contributor will be working on +- name: "x:module/analyzer" + description: "Work on Analyzers" + color: "5240c9" + +- name: "x:module/concept" + description: "Work on Concepts" + color: "5240c9" + +- name: "x:module/concept-exercise" + description: "Work on Concept Exercises" + color: "5240c9" + +- name: "x:module/generator" + description: "Work on Exercise generators" + color: "5240c9" + +- name: "x:module/practice-exercise" + description: "Work on Practice Exercises" + color: "5240c9" + +- name: "x:module/representer" + description: "Work on Representers" + color: "5240c9" + +- name: "x:module/test-runner" + description: "Work on Test Runners" + color: "5240c9" + +# The `x:size/` labels describe the expected amount of work for a contributor +- name: "x:size/tiny" + description: "Tiny amount of work" + color: "4836bf" + +- name: "x:size/small" + description: "Small amount of work" + color: "4836bf" + +- name: "x:size/medium" + description: "Medium amount of work" + color: "4836bf" + +- name: "x:size/large" + description: "Large amount of work" + color: "4836bf" + +- name: "x:size/massive" + description: "Massive amount of work" + color: "4836bf" + +# The `x:status/` label indicates if there is already someone working on the issue +- name: "x:status/claimed" + description: "Someone is working on this issue" + color: "4231af" + +# The `x:type/` labels describe what type of work the contributor will be engaged in +- name: "x:type/ci" + description: "Work on Continuous Integration (e.g. GitHub Actions workflows)" + color: "3c2d9f" + +- name: "x:type/coding" + description: "Write code that is not student-facing content (e.g. test-runners, generators, but not exercises)" + color: "3c2d9f" + +- name: "x:type/content" + description: "Work on content (e.g. exercises, concepts)" + color: "3c2d9f" + +- name: "x:type/docker" + description: "Work on Dockerfiles" + color: "3c2d9f" + +- name: "x:type/docs" + description: "Work on Documentation" + color: "3c2d9f" + +# This Exercism-wide label is added to all automatically created pull requests that help migrate/prepare a track for Exercism v3 +- name: "v3-migration 🤖" + description: "Preparing for Exercism v3" + color: "e99695" + diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml new file mode 100644 index 000000000..d2d9bef93 --- /dev/null +++ b/.github/workflows/sync-labels.yml @@ -0,0 +1,21 @@ +name: Tools + +on: + push: + branches: [main] + paths: + - .github/labels.yml + - .github/workflows/sync-labels.yml + schedule: + - cron: 0 0 1 * * + workflow_dispatch: + +jobs: + sync-labels: + name: Sync labels + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + - uses: micnncim/action-label-syncer@3abd5ab72fda571e69fffd97bd4e0033dd5f495c + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 4065e3a74a1f74d31140d2848fb2457ca8724cda Mon Sep 17 00:00:00 2001 From: Exercism Bot Date: Mon, 30 Aug 2021 11:35:31 +0100 Subject: [PATCH 423/544] =?UTF-8?q?=F0=9F=A4=96=20Sync=20org-wide=20files?= =?UTF-8?q?=20to=20upstream=20repo=20(#1006)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit More info: https://github.com/exercism/org-wide-files/commit/c8c2611771545c6b7a36d413c5571e958e0ee233 --- .github/labels.yml | 4 +-- CODE_OF_CONDUCT.md | 82 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 CODE_OF_CONDUCT.md diff --git a/.github/labels.yml b/.github/labels.yml index 1e684c04d..ba61b61dc 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -7,7 +7,7 @@ # 1. The Exercism-wide labels: defined in https://github.com/exercism/org-wide-files/blob/main/global-files/.github/labels.yml # 2. The repository-specific labels: defined in the `.appends/.github/labels.yml` file within this repository. # -# If any of these two files change, a pull request is automatically containing a re-generated version of this file. +# If any of these two files change, a pull request is automatically created containing a re-generated version of this file. # Consequently, to change repository-specific labels you should update the `.appends/.github/labels.yml` file and _not_ this file. # # When the pull request has been merged, the GitHub labels will be automatically updated by the "Sync labels" workflow. @@ -17,7 +17,7 @@ # These are the Exercism-wide labels which are shared across all repos. # # --------------------------------------------------------------------- # -# The following Exercism-wide labels are used to show "tasks" on the website, which will point users to things they can help contribute with. +# The following Exercism-wide labels are used to show "tasks" on the website, which will point users to things they can contribute to. # The `x:action/` labels describe what sort of work the contributor will be engaged in when working on the issue - name: "x:action/create" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..368129a0b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,82 @@ +# Code of Conduct + +## Introduction + +Exercism is a platform centered around empathetic conversation. We have a low tolerance for communication that makes anyone feel unwelcome, unsupported, insulted or discriminated against. + +## Seen or experienced something uncomfortable? + +If you see or experience abuse, harassment, discrimination, or feel unsafe or upset, please email abuse@exercism.io. We will take your report seriously. + +## Enforcement + +We actively monitor for Code of Conduct (CoC) violations and take any reports of violations extremely seriously. We have banned contributors, mentors and users due to violations. + +After we receive a report of a CoC violation, we view that person's conversation history on Exercism and related communication channels and attempt to understand whether someone has deliberately broken the CoC, or accidentally crossed a line. We generally reach out to the person who has been reported to discuss any concerns we have and warn them that repeated violations will result in a ban. Sometimes we decide that no violation has occurred and that no action is required and sometimes we will also ban people on a first offense. We strive to be fair, but will err on the side of protecting the culture of our community. + +Exercism's leadership reserve the right to take whatever action they feel appropriate with regards to CoC violations. + +## The simple version + +- Be empathetic +- Be welcoming +- Be kind +- Be honest +- Be supportive +- Be polite + +## The details + +Exercism should be a safe place for everybody regardless of + +- Gender, gender identity or gender expression +- Sexual orientation +- Disability +- Physical appearance (including but not limited to body size) +- Race +- Age +- Religion +- Anything else you can think of. + +As someone who is part of this community, you agree that: + +- We are collectively and individually committed to safety and inclusivity. +- We have zero tolerance for abuse, harassment, or discrimination. +- We respect people’s boundaries and identities. +- We refrain from using language that can be considered offensive or oppressive (systemically or otherwise), eg. sexist, racist, homophobic, transphobic, ableist, classist, etc. - this includes (but is not limited to) various slurs. +- We avoid using offensive topics as a form of humor. + +We actively work towards: + +- Being a safe community +- Cultivating a network of support & encouragement for each other +- Encouraging responsible and varied forms of expression + +We condemn: + +- Stalking, doxxing, or publishing private information +- Violence, threats of violence or violent language +- Anything that compromises people’s safety +- Conduct or speech which might be considered sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory or offensive in nature. +- The use of unwelcome, suggestive, derogatory or inappropriate nicknames or terms. +- Disrespect towards others (jokes, innuendo, dismissive attitudes) and towards differences of opinion. +- Intimidation or harassment (online or in-person). Please read the [Citizen Code of Conduct](https://github.com/stumpsyn/policies/blob/master/citizen_code_of_conduct.md) for how we interpret harassment. +- Inappropriate attention or contact. +- Not understanding the differences between constructive criticism and disparagement. + +These things are NOT OK. + +Be aware of how your actions affect others. If it makes someone uncomfortable, stop. + +If you say something that is found offensive, and you are called out on it, try to: + +- Listen without interruption. +- Believe what the person is saying & do not attempt to disqualify what they have to say. +- Ask for tips / help with avoiding making the offense in the future. +- Apologize and ask forgiveness. + +## History + +This policy was initially adopted from the Front-end London Slack community and has been modified since. A version history can be seen on [GitHub](https://github.com/exercism/website-copy/edit/main/pages/code_of_conduct.md). + +_This policy is a "living" document, and subject to refinement and expansion in the future. This policy applies to the Exercism website, the Exercism GitHub organization, any other Exercism-related communication channels (e.g. Slack, Twitter, email) and any other Exercism entity or event._ From 0d998b978dc94c4564af2d0e9ce14e45615ec99c Mon Sep 17 00:00:00 2001 From: Johannes Werner Date: Mon, 18 Oct 2021 14:02:02 +0200 Subject: [PATCH 424/544] fixed broken link when metadata is missing (#1011) * fixed broken link when metadata is missing * fixed broken link when metadata is missing --- cmd/cmd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index 9fa48acdc..dcca08264 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -55,7 +55,7 @@ const msgRerunConfigure = ` const msgMissingMetadata = ` The exercise you are submitting doesn't have the necessary metadata. - Please see https://exercism.io/cli-v1-to-v2 for instructions on how to fix it. + Please see https://github.com/exercism/website-copy/blob/main/pages/cli_v1_to_v2.md for instructions on how to fix it. ` From d554b9b87ace9c276d452e5d9f71063fc3e13cee Mon Sep 17 00:00:00 2001 From: Sascha Mann Date: Fri, 18 Feb 2022 11:41:37 +0100 Subject: [PATCH 425/544] CI: Migrate to GHA (#1021) * CI: Migrate to GHA * Rename .travis.gofmt.sh to .gha.gofmt.sh * Delete .travis.yml * Delete appveyor.yml * Add caching * Test caching * Revert "Add caching" This reverts commit 0beee421e710916eab35c504690b07c1ab1e811c. --- .travis.gofmt.sh => .gha.gofmt.sh | 0 .github/workflows/ci.yml | 43 +++++++++++++++++++++++++++++++ .travis.yml | 15 ----------- appveyor.yml | 20 -------------- 4 files changed, 43 insertions(+), 35 deletions(-) rename .travis.gofmt.sh => .gha.gofmt.sh (100%) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml delete mode 100644 appveyor.yml diff --git a/.travis.gofmt.sh b/.gha.gofmt.sh similarity index 100% rename from .travis.gofmt.sh rename to .gha.gofmt.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..890af0c5d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + tests: + name: Go ${{ matrix.go-version }} - ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + go-version: ["1.15"] + os: [ubuntu-latest, windows-latest, macOS-latest] + + steps: + - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + + - uses: actions/setup-go@bfdd3570ce990073878bf10f6b2d79082de49492 + with: + go-version: ${{ matrix.go-version }} + + - name: Run Tests + run: | + go test -cover ./... + shell: bash + + formatting: + name: Go Format + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + + - name: Check formatting + run: ./.gha.gofmt.sh diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a195a1099..000000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: go - -sudo: false - -go: - - "1.15.x" - - tip - -matrix: - allow_failures: - - go: tip - -script: - - ./.travis.gofmt.sh - - go test -cover ./... diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 114035734..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,20 +0,0 @@ ---- -version: "{build}" - -clone_folder: c:\gopath\src\github.com\exercism\cli - -environment: - GOPATH: c:\gopath - GO111MODULE: on - -init: - - git config --global core.autocrlf input - -install: - - echo %PATH% - - echo %GOPATH% - - go version - - go env - -build_script: - - go test -cover ./... From 2e780d48077cc29c1eb45fe5c634b55b7ae27b24 Mon Sep 17 00:00:00 2001 From: Sascha Mann Date: Fri, 18 Feb 2022 12:50:23 +0100 Subject: [PATCH 426/544] Update CI badge in README (#1022) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa1c45db8..c92650a85 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Exercism Command-line Interface (CLI) -[![Build Status](https://travis-ci.org/exercism/cli.svg?branch=master)](https://travis-ci.org/exercism/cli) +[![CI](https://github.com/exercism/cli/actions/workflows/ci.yml/badge.svg)](https://github.com/exercism/cli/actions/workflows/ci.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/exercism/cli)](https://goreportcard.com/report/github.com/exercism/cli) The CLI is the link between the [Exercism][exercism] website and your local work environment. It lets you download exercises and submit your solution to the site. From 5563b647c46faf9dc7ea6f146b4d21072acd2d08 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Feb 2022 12:04:42 +0100 Subject: [PATCH 427/544] Bump the printed version of Exercism to v3 (#1013) --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index aa97c4670..734c8d4b7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -16,7 +16,7 @@ import ( var RootCmd = &cobra.Command{ Use: BinaryName, Short: "A friendly command-line interface to Exercism.", - Long: `A command-line interface for the v2 redesign of Exercism. + Long: `A command-line interface for the v3 redesign of Exercism. Download exercises and submit your solutions.`, SilenceUsage: true, From efe5e0bea5d20efd70157b8a1aa880d42d730b2b Mon Sep 17 00:00:00 2001 From: Faisal Afroz Date: Thu, 24 Feb 2022 16:37:26 +0530 Subject: [PATCH 428/544] Create dependabot.yml (#1015) Co-authored-by: Erik Schierboom --- .github/dependabot.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..eb2ead799 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 + +updates: + # Keep dependencies for GitHub Actions up-to-date + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'daily' + labels: + - 'x:size/small' From d281c7a626231f8cf43eca23d6f41897091618fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Feb 2022 12:15:24 +0100 Subject: [PATCH 429/544] Bump actions/checkout from 2.3.4 to 2.4.0 (#1024) Bumps [actions/checkout](https://github.com/actions/checkout) from 2.3.4 to 2.4.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f...ec3a7ce113134d7a93b817d10a8272cb61118579) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- .github/workflows/sync-labels.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 890af0c5d..da039e413 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: os: [ubuntu-latest, windows-latest, macOS-latest] steps: - - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 - uses: actions/setup-go@bfdd3570ce990073878bf10f6b2d79082de49492 with: @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 - name: Check formatting run: ./.gha.gofmt.sh diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml index d2d9bef93..095884ad5 100644 --- a/.github/workflows/sync-labels.yml +++ b/.github/workflows/sync-labels.yml @@ -15,7 +15,7 @@ jobs: name: Sync labels runs-on: ubuntu-latest steps: - - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 - uses: micnncim/action-label-syncer@3abd5ab72fda571e69fffd97bd4e0033dd5f495c env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 1b72d9ebf7885d6f089860976baba5c666ac0c80 Mon Sep 17 00:00:00 2001 From: Francisco Miamoto Date: Thu, 24 Feb 2022 11:30:03 -0300 Subject: [PATCH 430/544] Update build instructions for *nix (#1005) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf13f1181..7fa8422c1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,7 +40,7 @@ damaging your real Exercism submissions, or test different tokens, etc. On Unices: -- `cd /path/to/the/development/directory/cli && go build -o testercism main.go` +- `cd /path/to/the/development/directory/cli && go build -o testercism ./exercism/main.go` - `./testercism -h` On Windows: From eee729bb170aa6e4be6ac0c057c6392ead456bc1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 09:23:27 +0100 Subject: [PATCH 431/544] Bump actions/checkout from 2.4.0 to 3 (#1027) Bumps [actions/checkout](https://github.com/actions/checkout) from 2.4.0 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/ec3a7ce113134d7a93b817d10a8272cb61118579...a12a3943b4bdde767164f792f33f40b04645d846) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- .github/workflows/sync-labels.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da039e413..8f38a110d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: os: [ubuntu-latest, windows-latest, macOS-latest] steps: - - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 + - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 - uses: actions/setup-go@bfdd3570ce990073878bf10f6b2d79082de49492 with: @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 + - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 - name: Check formatting run: ./.gha.gofmt.sh diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml index 095884ad5..c3e277eb9 100644 --- a/.github/workflows/sync-labels.yml +++ b/.github/workflows/sync-labels.yml @@ -15,7 +15,7 @@ jobs: name: Sync labels runs-on: ubuntu-latest steps: - - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 + - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 - uses: micnncim/action-label-syncer@3abd5ab72fda571e69fffd97bd4e0033dd5f495c env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 8f0655b9d23af3f9995a2b5689d4fd307d9a2bd4 Mon Sep 17 00:00:00 2001 From: Exercism Bot Date: Tue, 8 Mar 2022 15:16:10 +0000 Subject: [PATCH 432/544] =?UTF-8?q?=F0=9F=A4=96=20Sync=20org-wide=20files?= =?UTF-8?q?=20to=20upstream=20repo=20(#1028)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit More info: https://github.com/exercism/org-wide-files/commit/cc4bf900aaee77af9629a751dadc8092d82e379a --- .github/labels.yml | 59 +++++++++++++++++-------------- .github/workflows/sync-labels.yml | 18 +++++----- 2 files changed, 40 insertions(+), 37 deletions(-) diff --git a/.github/labels.yml b/.github/labels.yml index ba61b61dc..7a7915e56 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -22,116 +22,121 @@ # The `x:action/` labels describe what sort of work the contributor will be engaged in when working on the issue - name: "x:action/create" description: "Work on something from scratch" - color: "6f60d2" + color: "ffffff" - name: "x:action/fix" description: "Fix an issue" - color: "6f60d2" + color: "ffffff" - name: "x:action/improve" description: "Improve existing functionality/content" - color: "6f60d2" + color: "ffffff" - name: "x:action/proofread" description: "Proofread text" - color: "6f60d2" + color: "ffffff" - name: "x:action/sync" description: "Sync content with its latest version" - color: "6f60d2" + color: "ffffff" # The `x:knowledge/` labels describe how much Exercism knowledge is required by the contributor - name: "x:knowledge/none" description: "No existing Exercism knowledge required" - color: "604fcd" + color: "ffffff" - name: "x:knowledge/elementary" description: "Little Exercism knowledge required" - color: "604fcd" + color: "ffffff" - name: "x:knowledge/intermediate" description: "Quite a bit of Exercism knowledge required" - color: "604fcd" + color: "ffffff" - name: "x:knowledge/advanced" description: "Comprehensive Exercism knowledge required" - color: "604fcd" + color: "ffffff" # The `x:module/` labels indicate what part of Exercism the contributor will be working on - name: "x:module/analyzer" description: "Work on Analyzers" - color: "5240c9" + color: "ffffff" - name: "x:module/concept" description: "Work on Concepts" - color: "5240c9" + color: "ffffff" - name: "x:module/concept-exercise" description: "Work on Concept Exercises" - color: "5240c9" + color: "ffffff" - name: "x:module/generator" description: "Work on Exercise generators" - color: "5240c9" + color: "ffffff" - name: "x:module/practice-exercise" description: "Work on Practice Exercises" - color: "5240c9" + color: "ffffff" - name: "x:module/representer" description: "Work on Representers" - color: "5240c9" + color: "ffffff" - name: "x:module/test-runner" description: "Work on Test Runners" - color: "5240c9" + color: "ffffff" # The `x:size/` labels describe the expected amount of work for a contributor - name: "x:size/tiny" description: "Tiny amount of work" - color: "4836bf" + color: "ffffff" - name: "x:size/small" description: "Small amount of work" - color: "4836bf" + color: "ffffff" - name: "x:size/medium" description: "Medium amount of work" - color: "4836bf" + color: "ffffff" - name: "x:size/large" description: "Large amount of work" - color: "4836bf" + color: "ffffff" - name: "x:size/massive" description: "Massive amount of work" - color: "4836bf" + color: "ffffff" # The `x:status/` label indicates if there is already someone working on the issue - name: "x:status/claimed" description: "Someone is working on this issue" - color: "4231af" + color: "ffffff" # The `x:type/` labels describe what type of work the contributor will be engaged in - name: "x:type/ci" description: "Work on Continuous Integration (e.g. GitHub Actions workflows)" - color: "3c2d9f" + color: "ffffff" - name: "x:type/coding" description: "Write code that is not student-facing content (e.g. test-runners, generators, but not exercises)" - color: "3c2d9f" + color: "ffffff" - name: "x:type/content" description: "Work on content (e.g. exercises, concepts)" - color: "3c2d9f" + color: "ffffff" - name: "x:type/docker" description: "Work on Dockerfiles" - color: "3c2d9f" + color: "ffffff" - name: "x:type/docs" description: "Work on Documentation" - color: "3c2d9f" + color: "ffffff" + +# This label can be added to accept PRs as part of Hacktoberfest +- name: "hacktoberfest-accepted" + description: "Make this PR count for hacktoberfest" + color: "ff7518" # This Exercism-wide label is added to all automatically created pull requests that help migrate/prepare a track for Exercism v3 - name: "v3-migration 🤖" diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml index c3e277eb9..e7b99e504 100644 --- a/.github/workflows/sync-labels.yml +++ b/.github/workflows/sync-labels.yml @@ -2,20 +2,18 @@ name: Tools on: push: - branches: [main] + branches: + - main paths: - .github/labels.yml - .github/workflows/sync-labels.yml - schedule: - - cron: 0 0 1 * * workflow_dispatch: + schedule: + - cron: 0 0 1 * * # First day of each month + +permissions: + issues: write jobs: sync-labels: - name: Sync labels - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 - - uses: micnncim/action-label-syncer@3abd5ab72fda571e69fffd97bd4e0033dd5f495c - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: exercism/github-actions/.github/workflows/labels.yml@main From d3ef85576c42c1d52816bc743c211417608a1a9a Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 17 Mar 2022 09:35:18 +0100 Subject: [PATCH 433/544] Add a .appends/.github/labels.yml file. (#1030) The `.appends/.github/labels.yml` file contains all the labels that are currently used in this repo. The `.github/labels.yml` file will contain the full list of labels that this repo can use, which will be a combination of the `.appends/.github/labels.yml` file and a centrally-managed `labels.yml` file. We'll automatically sync any changes, which allows us to guarantee that all the track repositories will have a pre-determined set of labels, augmented with any custom labels defined in the `.appends/.github/labels.yml` file. --- .appends/.github/labels.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .appends/.github/labels.yml diff --git a/.appends/.github/labels.yml b/.appends/.github/labels.yml new file mode 100644 index 000000000..2bef75771 --- /dev/null +++ b/.appends/.github/labels.yml @@ -0,0 +1,8 @@ +# ----------------------------------------------------------------------------------------- # +# These are the repository-specific labels that augment the Exercise-wide labels defined in # +# https://github.com/exercism/org-wide-files/blob/main/global-files/.github/labels.yml. # +# ----------------------------------------------------------------------------------------- # + +- name: "bug?" + description: "" + color: "eb6420" From 68a08b96163d8f1f09a7c715a11cd7ca011b4be2 Mon Sep 17 00:00:00 2001 From: Exercism Bot Date: Thu, 17 Mar 2022 09:37:47 +0000 Subject: [PATCH 434/544] =?UTF-8?q?=F0=9F=A4=96=20Sync=20org-wide=20files?= =?UTF-8?q?=20to=20upstream=20repo=20(#1031)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit More info: https://github.com/exercism/org-wide-files/commit/4d7ccede08978ef000a784a756bb34edd1f10ed4 --- .github/labels.yml | 8 ++++++++ CODE_OF_CONDUCT.md | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/labels.yml b/.github/labels.yml index 7a7915e56..9e95c4201 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -143,3 +143,11 @@ description: "Preparing for Exercism v3" color: "e99695" +# ----------------------------------------------------------------------------------------- # +# These are the repository-specific labels that augment the Exercise-wide labels defined in # +# https://github.com/exercism/org-wide-files/blob/main/global-files/.github/labels.yml. # +# ----------------------------------------------------------------------------------------- # + +- name: "bug?" + description: "" + color: "eb6420" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 368129a0b..16e94f493 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -6,7 +6,7 @@ Exercism is a platform centered around empathetic conversation. We have a low to ## Seen or experienced something uncomfortable? -If you see or experience abuse, harassment, discrimination, or feel unsafe or upset, please email abuse@exercism.io. We will take your report seriously. +If you see or experience abuse, harassment, discrimination, or feel unsafe or upset, please email abuse@exercism.org. We will take your report seriously. ## Enforcement From 4c1848878435b807252e4cfcff9f772fa41b816f Mon Sep 17 00:00:00 2001 From: Eric Kingery Date: Tue, 3 May 2022 09:11:56 -0400 Subject: [PATCH 435/544] ran go fmt ./... (#1038) --- cmd/configure_test.go | 1 + cmd/submit_symlink_test.go | 1 + config/config_notwin_test.go | 1 + config/config_windows_test.go | 1 + config/resolve_notwin_test.go | 1 + workspace/path_type_symlinks_test.go | 1 + 6 files changed, 6 insertions(+) diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 8d3643522..7a0f28eac 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package cmd diff --git a/cmd/submit_symlink_test.go b/cmd/submit_symlink_test.go index b2d30ac4c..e10fb4890 100644 --- a/cmd/submit_symlink_test.go +++ b/cmd/submit_symlink_test.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package cmd diff --git a/config/config_notwin_test.go b/config/config_notwin_test.go index f7e4564da..31ddf77f0 100644 --- a/config/config_notwin_test.go +++ b/config/config_notwin_test.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package config diff --git a/config/config_windows_test.go b/config/config_windows_test.go index e4374def3..da676739f 100644 --- a/config/config_windows_test.go +++ b/config/config_windows_test.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package config diff --git a/config/resolve_notwin_test.go b/config/resolve_notwin_test.go index d44b73714..eea7ae440 100644 --- a/config/resolve_notwin_test.go +++ b/config/resolve_notwin_test.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package config diff --git a/workspace/path_type_symlinks_test.go b/workspace/path_type_symlinks_test.go index 0b8421019..ace7b741d 100644 --- a/workspace/path_type_symlinks_test.go +++ b/workspace/path_type_symlinks_test.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package workspace From fbd552b334235d4537d55af45331107c3ad09c12 Mon Sep 17 00:00:00 2001 From: Exercism Bot Date: Tue, 24 May 2022 09:20:48 +0100 Subject: [PATCH 436/544] =?UTF-8?q?=F0=9F=A4=96=20Sync=20org-wide=20files?= =?UTF-8?q?=20to=20upstream=20repo=20(#1039)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit More info: https://github.com/exercism/org-wide-files/commit/f28daf625fa326fafdcf261db7699377c8d1ed37 --- .github/labels.yml | 24 ++++++++++++++++++++++++ CODE_OF_CONDUCT.md | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/labels.yml b/.github/labels.yml index 9e95c4201..fcb7c3552 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -86,6 +86,30 @@ description: "Work on Test Runners" color: "ffffff" +# The `x:rep/` labels describe the amount of reputation to award +# +# For more information on reputation and how these labels should be used, +# check out https://exercism.org/docs/using/product/reputation +- name: "x:rep/tiny" + description: "Tiny amount of reputation" + color: "ffffff" + +- name: "x:rep/small" + description: "Small amount of reputation" + color: "ffffff" + +- name: "x:rep/medium" + description: "Medium amount of reputation" + color: "ffffff" + +- name: "x:rep/large" + description: "Large amount of reputation" + color: "ffffff" + +- name: "x:rep/massive" + description: "Massive amount of reputation" + color: "ffffff" + # The `x:size/` labels describe the expected amount of work for a contributor - name: "x:size/tiny" description: "Tiny amount of work" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 16e94f493..9bb22baa7 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -6,7 +6,7 @@ Exercism is a platform centered around empathetic conversation. We have a low to ## Seen or experienced something uncomfortable? -If you see or experience abuse, harassment, discrimination, or feel unsafe or upset, please email abuse@exercism.org. We will take your report seriously. +If you see or experience abuse, harassment, discrimination, or feel unsafe or upset, please email [abuse@exercism.org](mailto:abuse@exercism.org?subject=%5BCoC%5D) and include \[CoC\] in the subject line. We will follow up with you as a priority. ## Enforcement From 8e73ec3131e8218c0ff31d5e0d1a050cabd6864d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Thu, 9 Jun 2022 07:41:31 +0100 Subject: [PATCH 437/544] Submit without specifying files (#1044) --- cmd/submit.go | 26 +++++++++++++++++++++++++- cmd/submit_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/cmd/submit.go b/cmd/submit.go index 4f2a8df72..aaf3a93c1 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 diff --git a/cmd/submit_test.go b/cmd/submit_test.go index c20c47748..7286a19e6 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) From 972305178d04a8532b68b94dfa46f8f45c793338 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Mon, 3 Oct 2022 15:56:03 +0200 Subject: [PATCH 438/544] Update TLD in GoReleaser summary (#1057) We changed the default TLD of Exercism from .io to .org. --- .goreleaser.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 19fb72b2a..ca7d2bbcb 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -83,7 +83,7 @@ snapcrafts: # Remember you need to `snapcraft login` first. # Defaults to false. # publish: true - summary: Command-line client for https://exercism.io + summary: Command-line client for https://exercism.org # https://snapcraft.io/docs/reference/confinement confinement: strict # A snap of type base to be used as the execution environment for this snap. From 3d37fefd190bf08c25f38d12171e363e20163a0e Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 4 Oct 2022 10:52:59 +0200 Subject: [PATCH 439/544] Bump version to v3.1.0 (#1060) I arbitrarily decided that today is a good day to move to 3.1x. This release adds a feature that has been requested since forever, i.e. the ability to submit without specifying files. Seems worth the minor version bump. --- CHANGELOG.md | 7 +++++++ cmd/version.go | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccec41b11..110c6fe80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ## Next Release * **Your contribution here** +## v3.1.0 (2022-10-04) +* [#979](https://github.com/exercism/cli/pull/979) Protect existing solutions from being overwritten by 'download' - [@harugo] +* [#981](https://github.com/exercism/cli/pull/981) Check if authorisation header is set before attempting to extract token - [@harugo] +* [#1044](https://github.com/exercism/cli/pull/1044) Submit without specifying files - [@andrerfcsantos] + ## v3.0.13 (2019-10-23) * [#866](https://github.com/exercism/cli/pull/866) The API token outputted during verbose will now be masked by default - [@Jrank2013] * [#873](https://github.com/exercism/cli/pull/873) Make all errors in cmd package checked - [@avegner] @@ -423,6 +428,7 @@ All changes by [@msgehard] * Build on Travis [@AlexWheeler]: https://github.com/AlexWheeler +[@andrerfcsantos]: https://github.com/andrerfcsantos [@avegner]: https://github.com/avegner [@Dparker1990]: https://github.com/Dparker1990 [@John-Goff]: https://github.com/John-Goff @@ -453,6 +459,7 @@ All changes by [@msgehard] [@farisj]: https://github.com/farisj [@glebedel]: https://github.com/glebedel [@harimp]: https://github.com/harimp +[@harugo]: https://github.com/harugo [@hjljo]: https://github.com/hjljo [@isbadawi]: https://github.com/isbadawi [@jbaiter]: https://github.com/jbaiter diff --git a/cmd/version.go b/cmd/version.go index 3b81f5897..069866b78 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.0.13" +const Version = "3.1.0" // checkLatest flag for version command. var checkLatest bool From 45ccec2e2163ded09c461ad8e277a0f809bffd27 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 4 Oct 2022 11:26:56 +0200 Subject: [PATCH 440/544] Add missing go.sum entry (#1061) The release failed, complaining about the wrong mousetrap library reference. This updates it, so we can try releasing again. --- go.sum | 1 + 1 file changed, 1 insertion(+) diff --git a/go.sum b/go.sum index 0a9864259..3f5f7852a 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,7 @@ github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e h1:KJWs1uTCkN3E/J5of github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/magiconair/properties v1.7.3 h1:6AOjgCKyZFMG/1yfReDPDz3CJZPxnYk7DGmj2HtyF24= github.com/magiconair/properties v1.7.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= From 423aff554f512c48d5e6b72342ae7afe9d58a598 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 4 Oct 2022 18:12:02 +0200 Subject: [PATCH 441/544] Rename master to main in release docs (#1059) We switched the default branch name a while back. This updates the release documentation to match the new state. --- RELEASE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index c8c1c92f0..b1200d882 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -15,7 +15,7 @@ release process. Make sure all the recent changes are reflected in the "next release" section of the CHANGELOG.md file. All the changes in the "next release" section should be moved to a new section that describes the version number, and gives it a date. You can view changes using the /compare/ view: -https://github.com/exercism/cli/compare/$PREVIOUS_RELEASE...master +https://github.com/exercism/cli/compare/$PREVIOUS_RELEASE...main GoReleaser supports the [auto generation of a changelog](https://goreleaser.com/customization/#customize-the-changelog) we will want to customize to meet our standards (not including refactors, test updates, etc). We should also consider using [the release notes feature](https://goreleaser.com/customization/#custom-release-notes). @@ -27,7 +27,7 @@ _Note: It's useful to add the version to the commit message when you bump it: e. In the future we will probably want to replace the hardcoded `Version` constant with [main.version](https://goreleaser.com/environment/#using-the-main-version). Here is a [stack overflow post on injecting to cmd/version.go](https://stackoverflow.com/a/47510909). -Commit this change on a branch along with the CHANGELOG updates in a single commit, and create a PR for merge to master. +Commit this change on a branch along with the CHANGELOG updates in a single commit, and create a PR for merge to main. ## Cut a release @@ -35,7 +35,7 @@ Commit this change on a branch along with the CHANGELOG updates in a single comm # Test run goreleaser --skip-publish --snapshot --rm-dist -# Create a new tag on the master branch and push it +# Create a new tag on the main branch and push it git tag -a v3.0.16 -m "Trying out GoReleaser" git push origin v3.0.16 From c811ef5ece3348f1478bf45c9365a12bab7bc4bb Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 4 Oct 2022 18:51:28 +0200 Subject: [PATCH 442/544] Fix broken links in release instructions (#1058) * Fix broken links in release instructions The GoReleaser docs have changed and they moved the information about the requirements for the GitHub API token as well as about signing releases. * Tracked down broken link to old main.version docs Co-authored-by: Eric Kingery --- RELEASE.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index b1200d882..bf3e1c8bf 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,14 +1,14 @@ # Cutting a CLI Release The Exercism CLI uses [GoReleaser](https://goreleaser.com) to automate the -release process. +release process. ## Requirements 1. [Install GoReleaser](https://goreleaser.com/install/) 1. [Install snapcraft](https://snapcraft.io/docs/snapcraft-overview) -1. [Setup GitHub token](https://goreleaser.com/environment/#github-token) -1. Have a gpg key installed on your machine - it is [used for signing the artifacts](https://goreleaser.com/sign/) +1. [Setup GitHub token](https://goreleaser.com/scm/github/) +1. Have a gpg key installed on your machine - it is [used for signing the artifacts](https://goreleaser.com/customization/sign/) ## Confirm / Update the Changelog @@ -25,7 +25,7 @@ Edit the `Version` constant in `cmd/version.go` _Note: It's useful to add the version to the commit message when you bump it: e.g. `Bump version to v2.3.4`._ -In the future we will probably want to replace the hardcoded `Version` constant with [main.version](https://goreleaser.com/environment/#using-the-main-version). Here is a [stack overflow post on injecting to cmd/version.go](https://stackoverflow.com/a/47510909). +In the future we will probably want to replace the hardcoded `Version` constant with [main.version](https://goreleaser.com/cookbooks/using-main.version). Here is a [stack overflow post on injecting to cmd/version.go](https://stackoverflow.com/a/47510909). Commit this change on a branch along with the CHANGELOG updates in a single commit, and create a PR for merge to main. From 0513c81da2aba80852e3f8c16359707a9ea8718e Mon Sep 17 00:00:00 2001 From: Conor Fleming <32988273+Conor-Fleming@users.noreply.github.com> Date: Fri, 7 Oct 2022 13:32:27 -0700 Subject: [PATCH 443/544] removing broken links from contribution docs (#1064) --- CONTRIBUTING.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7fa8422c1..1936a39eb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,8 +5,6 @@ Exercism would be impossible without people like you being willing to spend time ## Documentation * [Exercism Documentation Repository](https://github.com/exercism/docs) -* [Exercism Glossary](https://github.com/exercism/docs/blob/master/about/glossary.md) -* [Exercism Architecture](https://github.com/exercism/docs/blob/master/about/architecture.md) ## Dependencies From 60a9ff8a0e3675957eabc94af58f58b621b3214f Mon Sep 17 00:00:00 2001 From: Exercism Bot Date: Wed, 23 Nov 2022 13:50:35 +0000 Subject: [PATCH 444/544] =?UTF-8?q?=F0=9F=A4=96=20Sync=20org-wide=20files?= =?UTF-8?q?=20to=20upstream=20repo=20(#1071)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit More info: https://github.com/exercism/org-wide-files/commit/ceface8fc48f8f8be135cecafe5ea8355aac9ad6 --- .github/labels.yml | 10 ++++---- CODE_OF_CONDUCT.md | 57 +++++++++++++++++++++++++++------------------- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/.github/labels.yml b/.github/labels.yml index fcb7c3552..cd989c70f 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -157,16 +157,16 @@ description: "Work on Documentation" color: "ffffff" -# This label can be added to accept PRs as part of Hacktoberfest -- name: "hacktoberfest-accepted" - description: "Make this PR count for hacktoberfest" - color: "ff7518" - # This Exercism-wide label is added to all automatically created pull requests that help migrate/prepare a track for Exercism v3 - name: "v3-migration 🤖" description: "Preparing for Exercism v3" color: "e99695" +# This Exercism-wide label can be used to bulk-close issues in preparation for pausing community contributions +- name: "paused" + description: "Work paused until further notice" + color: "e4e669" + # ----------------------------------------------------------------------------------------- # # These are the repository-specific labels that augment the Exercise-wide labels defined in # # https://github.com/exercism/org-wide-files/blob/main/global-files/.github/labels.yml. # diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 9bb22baa7..df8e36761 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,17 +2,23 @@ ## Introduction -Exercism is a platform centered around empathetic conversation. We have a low tolerance for communication that makes anyone feel unwelcome, unsupported, insulted or discriminated against. +Exercism is a platform centered around empathetic conversation. +We have a low tolerance for communication that makes anyone feel unwelcome, unsupported, insulted or discriminated against. ## Seen or experienced something uncomfortable? -If you see or experience abuse, harassment, discrimination, or feel unsafe or upset, please email [abuse@exercism.org](mailto:abuse@exercism.org?subject=%5BCoC%5D) and include \[CoC\] in the subject line. We will follow up with you as a priority. +If you see or experience abuse, harassment, discrimination, or feel unsafe or upset, please email [abuse@exercism.org](mailto:abuse@exercism.org?subject=%5BCoC%5D) and include \[CoC\] in the subject line. +We will follow up with you as a priority. ## Enforcement -We actively monitor for Code of Conduct (CoC) violations and take any reports of violations extremely seriously. We have banned contributors, mentors and users due to violations. +We actively monitor for Code of Conduct (CoC) violations and take any reports of violations extremely seriously. +We have banned contributors, mentors and users due to violations. -After we receive a report of a CoC violation, we view that person's conversation history on Exercism and related communication channels and attempt to understand whether someone has deliberately broken the CoC, or accidentally crossed a line. We generally reach out to the person who has been reported to discuss any concerns we have and warn them that repeated violations will result in a ban. Sometimes we decide that no violation has occurred and that no action is required and sometimes we will also ban people on a first offense. We strive to be fair, but will err on the side of protecting the culture of our community. +After we receive a report of a CoC violation, we view that person's conversation history on Exercism and related communication channels and attempt to understand whether someone has deliberately broken the CoC, or accidentally crossed a line. +We generally reach out to the person who has been reported to discuss any concerns we have and warn them that repeated violations will result in a ban. +Sometimes we decide that no violation has occurred and that no action is required and sometimes we will also ban people on a first offense. +We strive to be fair, but will err on the side of protecting the culture of our community. Exercism's leadership reserve the right to take whatever action they feel appropriate with regards to CoC violations. @@ -36,15 +42,16 @@ Exercism should be a safe place for everybody regardless of - Race - Age - Religion -- Anything else you can think of. +- Anything else you can think of As someone who is part of this community, you agree that: -- We are collectively and individually committed to safety and inclusivity. -- We have zero tolerance for abuse, harassment, or discrimination. -- We respect people’s boundaries and identities. -- We refrain from using language that can be considered offensive or oppressive (systemically or otherwise), eg. sexist, racist, homophobic, transphobic, ableist, classist, etc. - this includes (but is not limited to) various slurs. -- We avoid using offensive topics as a form of humor. +- We are collectively and individually committed to safety and inclusivity +- We have zero tolerance for abuse, harassment, or discrimination +- We respect people’s boundaries and identities +- We refrain from using language that can be considered offensive or oppressive (systemically or otherwise), eg. sexist, racist, homophobic, transphobic, ableist, classist, etc. + - this includes (but is not limited to) various slurs. +- We avoid using offensive topics as a form of humor We actively work towards: @@ -57,26 +64,30 @@ We condemn: - Stalking, doxxing, or publishing private information - Violence, threats of violence or violent language - Anything that compromises people’s safety -- Conduct or speech which might be considered sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory or offensive in nature. -- The use of unwelcome, suggestive, derogatory or inappropriate nicknames or terms. -- Disrespect towards others (jokes, innuendo, dismissive attitudes) and towards differences of opinion. -- Intimidation or harassment (online or in-person). Please read the [Citizen Code of Conduct](https://github.com/stumpsyn/policies/blob/master/citizen_code_of_conduct.md) for how we interpret harassment. -- Inappropriate attention or contact. -- Not understanding the differences between constructive criticism and disparagement. +- Conduct or speech which might be considered sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory or offensive in nature +- The use of unwelcome, suggestive, derogatory or inappropriate nicknames or terms +- Disrespect towards others (jokes, innuendo, dismissive attitudes) and towards differences of opinion +- Intimidation or harassment (online or in-person). + Please read the [Citizen Code of Conduct](https://github.com/stumpsyn/policies/blob/master/citizen_code_of_conduct.md) for how we interpret harassment +- Inappropriate attention or contact +- Not understanding the differences between constructive criticism and disparagement These things are NOT OK. -Be aware of how your actions affect others. If it makes someone uncomfortable, stop. +Be aware of how your actions affect others. +If it makes someone uncomfortable, stop. If you say something that is found offensive, and you are called out on it, try to: -- Listen without interruption. -- Believe what the person is saying & do not attempt to disqualify what they have to say. -- Ask for tips / help with avoiding making the offense in the future. -- Apologize and ask forgiveness. +- Listen without interruption +- Believe what the person is saying & do not attempt to disqualify what they have to say +- Ask for tips / help with avoiding making the offense in the future +- Apologize and ask forgiveness ## History -This policy was initially adopted from the Front-end London Slack community and has been modified since. A version history can be seen on [GitHub](https://github.com/exercism/website-copy/edit/main/pages/code_of_conduct.md). +This policy was initially adopted from the Front-end London Slack community and has been modified since. +A version history can be seen on [GitHub](https://github.com/exercism/website-copy/edit/main/pages/code_of_conduct.md). -_This policy is a "living" document, and subject to refinement and expansion in the future. This policy applies to the Exercism website, the Exercism GitHub organization, any other Exercism-related communication channels (e.g. Slack, Twitter, email) and any other Exercism entity or event._ +_This policy is a "living" document, and subject to refinement and expansion in the future. +This policy applies to the Exercism website, the Exercism GitHub organization, any other Exercism-related communication channels (e.g. Slack, Twitter, email) and any other Exercism entity or event._ From e0dbcd1dbbf95836d6dc9bd634ac0d8c53b27525 Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Fri, 2 Dec 2022 09:37:16 +0100 Subject: [PATCH 445/544] Create autoresponder for pausing community contributions (#1072) * Create autoresponder for pausing community contributions We're going to take a step back and redesign the volunteering model for Exercism. Please see [this forum post](https://forum.exercism.org/t/freeing-our-maintainers-exercism-wide-changes-to-track-repositories/1109) for context. This PR adds an autoresponder that runs when an issue or PR is opened. If the person opening the issue is not a member of the Exercism organization, the autoresponder posts a comment and closes the issue. In the comment the author is directed to discuss the issue in the forum. If the discussion in the forum results in the issue/PR being approved, a maintainer or staff member will reopen it. Please feel free to merge this PR. It will be merged on December 1st, 2022. Please do not close it. If you wish to discuss this, please do so in [the forum post](https://forum.exercism.org/t/freeing-our-maintainers-exercism-wide-changes-to-track-repositories/1109) rather than here. * Update workflow for pausing community contributions This removes duplicated logic, relying on a shared workflow. --- .../pause-community-contributions.yml | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/pause-community-contributions.yml diff --git a/.github/workflows/pause-community-contributions.yml b/.github/workflows/pause-community-contributions.yml new file mode 100644 index 000000000..055ff8676 --- /dev/null +++ b/.github/workflows/pause-community-contributions.yml @@ -0,0 +1,21 @@ +name: Pause Community Contributions + +on: + issues: + types: + - opened + pull_request_target: + types: + - opened + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +jobs: + pause: + if: github.repository_owner == 'exercism' # Stops this job from running on forks + uses: exercism/github-actions/.github/workflows/community-contributions.yml@main + with: + forum_category: support From b29b91d4d08051a8008611025c646fe1f9a5f31c Mon Sep 17 00:00:00 2001 From: ee7 <45465154+ee7@users.noreply.github.com> Date: Fri, 9 Dec 2022 09:04:41 +0100 Subject: [PATCH 446/544] goreleaser: add arm64 build for each OS (#1073) * goreleaser: add arm64 build for each OS The latest release of the Exercism CLI (3.1.0, 2022-10-04) includes these assets, via GoReleaser: exercism-3.1.0-darwin-x86_64.tar.gz exercism-3.1.0-freebsd-i386.tar.gz exercism-3.1.0-freebsd-x86_64.tar.gz exercism-3.1.0-linux-armv5.tar.gz exercism-3.1.0-linux-armv6.tar.gz exercism-3.1.0-linux-i386.tar.gz exercism-3.1.0-linux-ppc64.tar.gz exercism-3.1.0-linux-x86_64.tar.gz exercism-3.1.0-openbsd-i386.tar.gz exercism-3.1.0-openbsd-x86_64.tar.gz exercism-3.1.0-windows-armv5.zip exercism-3.1.0-windows-armv6.zip exercism-3.1.0-windows-i386.zip exercism-3.1.0-windows-x86_64.zip exercism_checksums.txt exercism_checksums.txt.sig For future releases, this commit configures GoReleaser to include an arm64 (also known as aarch64) release asset for each of the existing OSes. Note that we will have separate x86_64 and arm64 release assets for macOS; previously macOS users on arm64 had to run the x86_64 binary via Rosetta (or build the CLI themselves). At least for now, let's avoid adding a fat binary. But note that GoReleaser does support it [1]. [1] https://goreleaser.com/customization/universalbinaries/ Closes: #966 * go.mod, go.sum: bump `golang.org/x/sys` from 20170803 to 20201202 This commits the result of running go get golang.org/x/sys@v0.0.0-20201202213521-69691e467435 to fix a build failure for freebsd_arm64. This seems to be the minimum bump to fix that failure without introducing a build failure for darwin_arm64 and openbsd_arm64. We cannot bump to the latest release (0.3.0, 2022-12-03) because that causes the Linux and macOS CI jobs to fail with: Error: ../../../go/pkg/mod/golang.org/x/sys@v0.3.0/unix/syscall.go:83:16: undefined: unsafe.Slice Error: ../../../go/pkg/mod/golang.org/x/sys@v0.3.0/unix/syscall_darwin.go:95:8: undefined: unsafe.Slice Error: ../../../go/pkg/mod/golang.org/x/sys@v0.3.0/unix/syscall_unix.go:118:7: undefined: unsafe.Slice Error: ../../../go/pkg/mod/golang.org/x/sys@v0.3.0/unix/sysvshm_unix.go:33:7: undefined: unsafe.Slice note: module requires Go 1.17 and we need other changes in order to bump the minimum Go version. --- .goreleaser.yml | 1 + go.mod | 2 +- go.sum | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index ca7d2bbcb..cb55b79f4 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -15,6 +15,7 @@ builds: - amd64 - 386 - arm + - arm64 - ppc64 goarm: - 5 diff --git a/go.mod b/go.mod index a3723bfad..91e79ff1a 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/spf13/viper v0.0.0-20180507071007-15738813a09d github.com/stretchr/testify v1.1.4 golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7 - golang.org/x/sys v0.0.0-20170803140359-d8f5ea21b929 + golang.org/x/sys v0.0.0-20201202213521-69691e467435 golang.org/x/text v0.0.0-20170730040918-3bd178b88a81 gopkg.in/yaml.v2 v2.0.0-20170721122051-25c4ec802a7d ) diff --git a/go.sum b/go.sum index 3f5f7852a..7d53fd399 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7 h1:1Pw+ZX4dmGORIwGkTwnUr7RFu golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sys v0.0.0-20170803140359-d8f5ea21b929 h1:M4VPQYSW/nB4Bcg1XMD4yW2sprnwerD3Kb6apRphtZw= golang.org/x/sys v0.0.0-20170803140359-d8f5ea21b929/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201202213521-69691e467435 h1:25AvDqqB9PrNqj1FLf2/70I4W0L19qqoaFq3gjNwbKk= +golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170730040918-3bd178b88a81 h1:7aXI3TQ9sZ4JdDoIDGjxL6G2mQxlsPy9dySnJaL6Bdk= golang.org/x/text v0.0.0-20170730040918-3bd178b88a81/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/yaml.v2 v2.0.0-20170721122051-25c4ec802a7d h1:2DX7x6HUDGZUyuEDAhUsQQNqkb1zvDyKTjVoTdzaEzo= From bfd0789e57da9b70155f23741eb56bd18b44f02f Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Tue, 13 Dec 2022 19:09:41 +0100 Subject: [PATCH 447/544] Add custom token to community contributions workflow (#1076) The pause-community-contributions workflow makes a call to the GitHub API to check if the person contributing is a member of the organization. However, this call currently fails if the contributor has set their membership to 'private'. This is because the default token provided for GitHub Actions only has permissions for the repository, not for the organization. With this token, we're not allowed to see private memberships. We've created a custom, org-wide secret containing a personal token that has permissions to read organization membership. Unfortunately the secret cannot be accessed directly by the shared workflow, it has to be passed in. We updated the shared workflow to use the token, if it is provided, and this PR updates the workflow in this repo to pass the secret. Until this is merged, contributions from people with private membership in the Exercism organization will be automatically closed. Note that this PR also removes the workflow_dispatch which fails if you try to use it. --- .github/workflows/pause-community-contributions.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pause-community-contributions.yml b/.github/workflows/pause-community-contributions.yml index 055ff8676..46f0c60b4 100644 --- a/.github/workflows/pause-community-contributions.yml +++ b/.github/workflows/pause-community-contributions.yml @@ -7,7 +7,6 @@ on: pull_request_target: types: - opened - workflow_dispatch: permissions: issues: write @@ -19,3 +18,5 @@ jobs: uses: exercism/github-actions/.github/workflows/community-contributions.yml@main with: forum_category: support + secrets: + github_membership_token: ${{ secrets.COMMUNITY_CONTRIBUTIONS_WORKFLOW_TOKEN }} From 94755c68f030fac1f8bff3f112c83fe69d7fd8c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Dec 2022 19:59:48 +0100 Subject: [PATCH 448/544] Bump actions/checkout from 3.0.0 to 3.2.0 (#1077) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.0.0 to 3.2.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/a12a3943b4bdde767164f792f33f40b04645d846...755da8c3cf115ac066823e79a1e1788f8940201b) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f38a110d..c396f2e2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: os: [ubuntu-latest, windows-latest, macOS-latest] steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 + - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b - uses: actions/setup-go@bfdd3570ce990073878bf10f6b2d79082de49492 with: @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 + - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b - name: Check formatting run: ./.gha.gofmt.sh From edee2079639ae4e5f2e628eb689bf489ed10b389 Mon Sep 17 00:00:00 2001 From: Nate Eagleson Date: Mon, 13 Feb 2023 11:37:26 -0500 Subject: [PATCH 449/544] Explain auto-close policy in ISSUE_TEMPLATE.md (#1084) * Explain auto-close policy in ISSUE_TEMPLATE.md Per discussion in this forum thread: http://forum.exercism.org/t/update-github-issue-template-to-warn-users-new-issues-are-being-auto-closed/3676/2 * Ask people to use the forum instead in .github/ISSUE_TEMPLATE.md Co-authored-by: Jeremy Walker --------- Co-authored-by: Jeremy Walker --- .github/ISSUE_TEMPLATE.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 04f539626..73ac4bb57 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,7 +1,4 @@ From a8ffc316a462ea07a5e4cfbfb3df24eb0f9342eb Mon Sep 17 00:00:00 2001 From: David Brownman Date: Fri, 28 Jul 2023 00:44:39 -0700 Subject: [PATCH 450/544] Add `test` command to run any unit test (#1092) This commit adds a `test` command that allows the student to run the tests for an exercise without knowing the track-specific test command. This makes it easier for the student to get started. For debugging and education purposes, we print the command that is used to run the tests. --- CHANGELOG.md | 2 + cmd/cmd_test.go | 14 +- cmd/test.go | 84 +++++++++ workspace/exercise_config.go | 57 ++++++ workspace/exercise_config_test.go | 98 ++++++++++ workspace/test_configurations.go | 252 ++++++++++++++++++++++++++ workspace/test_configurations_test.go | 103 +++++++++++ 7 files changed, 604 insertions(+), 6 deletions(-) create mode 100644 cmd/test.go create mode 100644 workspace/exercise_config.go create mode 100644 workspace/exercise_config_test.go create mode 100644 workspace/test_configurations.go create mode 100644 workspace/test_configurations_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 110c6fe80..1c84fa0a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ---------------- ## Next Release +* [#1092](https://github.com/exercism/cli/pull/1092) Add `exercism test` command to run the unit tests for nearly any track (inspired by [universal-test-runner](https://github.com/xavdid/universal-test-runner)) - [@xavdid] * **Your contribution here** ## v3.1.0 (2022-10-04) @@ -489,5 +490,6 @@ All changes by [@msgehard] [@sfairchild]: https://github.com/sfairchild [@simonjefford]: https://github.com/simonjefford [@srt32]: https://github.com/srt32 +[@xavdid]: https://github.com/xavdid [@williandrade]: https://github.com/williandrade [@zabawaba99]: https://github.com/zabawaba99 diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 76ed0634e..fee08cd98 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -26,12 +26,14 @@ const cfgHomeKey = "EXERCISM_CONFIG_HOME" // test, call the command by calling Execute on the App. // // Example: -// cmdTest := &CommandTest{ -// Cmd: myCmd, -// InitFn: initMyCmd, -// Args: []string{"fakeapp", "mycommand", "arg1", "--flag", "value"}, -// MockInteractiveResponse: "first-input\nsecond\n", -// } +// +// cmdTest := &CommandTest{ +// Cmd: myCmd, +// InitFn: initMyCmd, +// Args: []string{"fakeapp", "mycommand", "arg1", "--flag", "value"}, +// MockInteractiveResponse: "first-input\nsecond\n", +// } +// // cmdTest.Setup(t) // defer cmdTest.Teardown(t) // ... diff --git a/cmd/test.go b/cmd/test.go new file mode 100644 index 000000000..8f5b54e35 --- /dev/null +++ b/cmd/test.go @@ -0,0 +1,84 @@ +package cmd + +import ( + "fmt" + "log" + "os" + "os/exec" + "strings" + + "github.com/exercism/cli/workspace" + "github.com/spf13/cobra" +) + +var testCmd = &cobra.Command{ + Use: "test", + Aliases: []string{"t"}, + Short: "Run the exercise's tests.", + Long: `Run the exercise's tests. + + Run this command in an exercise's root directory.`, + RunE: func(cmd *cobra.Command, args []string) error { + return runTest(args) + }, +} + +func runTest(args []string) error { + track, err := getTrack() + if err != nil { + return err + } + + testConf, ok := workspace.TestConfigurations[track] + + if !ok { + return fmt.Errorf("the \"%s\" track does not yet support running tests using the Exercism CLI. Please see HELP.md for testing instructions", track) + } + + command, err := testConf.GetTestCommand() + if err != nil { + return err + } + cmdParts := strings.Split(command, " ") + + // pass args/flags to this command down to the test handler + if len(args) > 0 { + cmdParts = append(cmdParts, args...) + } + + fmt.Printf("Running tests via `%s`\n\n", strings.Join(cmdParts, " ")) + exerciseTestCmd := exec.Command(cmdParts[0], cmdParts[1:]...) + + // pipe output directly out, preserving any color + exerciseTestCmd.Stdout = os.Stdout + exerciseTestCmd.Stderr = os.Stderr + + err = exerciseTestCmd.Run() + if err != nil { + // unclear what other errors would pop up here, but it pays to be defensive + if exitErr, ok := err.(*exec.ExitError); ok { + exitCode := exitErr.ExitCode() + // if subcommand returned a non-zero exit code, exit with the same + os.Exit(exitCode) + } else { + log.Fatalf("Failed to get error from failed subcommand: %v", err) + } + } + return nil +} + +func getTrack() (string, error) { + metadata, err := workspace.NewExerciseMetadata(".") + if err != nil { + return "", err + } + if metadata.Track == "" { + return "", fmt.Errorf("no track found in exercise metadata") + } + + return metadata.Track, nil +} + +func init() { + RootCmd.AddCommand(testCmd) +} diff --git a/workspace/exercise_config.go b/workspace/exercise_config.go new file mode 100644 index 000000000..9b0164ebb --- /dev/null +++ b/workspace/exercise_config.go @@ -0,0 +1,57 @@ +package workspace + +import ( + "encoding/json" + "errors" + "io/ioutil" + "path/filepath" +) + +const configFilename = "config.json" + +var configFilepath = filepath.Join(ignoreSubdir, configFilename) + +// ExerciseConfig contains exercise metadata. +// Note: we only use a subset of its fields +type ExerciseConfig struct { + Files struct { + Solution []string `json:"solution"` + Test []string `json:"test"` + } `json:"files"` +} + +// NewExerciseConfig reads exercise metadata from a file in the given directory. +func NewExerciseConfig(dir string) (*ExerciseConfig, error) { + b, err := ioutil.ReadFile(filepath.Join(dir, configFilepath)) + if err != nil { + return nil, err + } + var config ExerciseConfig + if err := json.Unmarshal(b, &config); err != nil { + return nil, err + } + + return &config, nil +} + +// GetTestFiles finds returns the names of the file(s) that hold unit tests for this exercise, if any +func (c *ExerciseConfig) GetSolutionFiles() ([]string, error) { + result := c.Files.Solution + if result == nil { + // solution file(s) key was missing in config json, which is an error when calling this fuction + return []string{}, errors.New("no `files.solution` key in your `config.json`. Was it removed by mistake?") + } + + return result, nil +} + +// GetTestFiles finds returns the names of the file(s) that hold unit tests for this exercise, if any +func (c *ExerciseConfig) GetTestFiles() ([]string, error) { + result := c.Files.Test + if result == nil { + // test file(s) key was missing in config json, which is an error when calling this fuction + return []string{}, errors.New("no `files.test` key in your `config.json`. Was it removed by mistake?") + } + + return result, nil +} diff --git a/workspace/exercise_config_test.go b/workspace/exercise_config_test.go new file mode 100644 index 000000000..d07264c0d --- /dev/null +++ b/workspace/exercise_config_test.go @@ -0,0 +1,98 @@ +package workspace + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExerciseConfig(t *testing.T) { + dir, err := ioutil.TempDir("", "exercise_config") + assert.NoError(t, err) + defer os.RemoveAll(dir) + + err = os.Mkdir(filepath.Join(dir, ".exercism"), os.ModePerm) + assert.NoError(t, err) + + f, err := os.Create(filepath.Join(dir, ".exercism", "config.json")) + assert.NoError(t, err) + defer f.Close() + + _, err = f.WriteString(`{ "blurb": "Learn about the basics of Ruby by following a lasagna recipe.", "authors": ["iHiD", "pvcarrera"], "files": { "solution": ["lasagna.rb"], "test": ["lasagna_test.rb"], "exemplar": [".meta/exemplar.rb"] } } `) + assert.NoError(t, err) + + ec, err := NewExerciseConfig(dir) + assert.NoError(t, err) + + assert.Equal(t, ec.Files.Solution, []string{"lasagna.rb"}) + solutionFiles, err := ec.GetSolutionFiles() + assert.NoError(t, err) + assert.Equal(t, solutionFiles, []string{"lasagna.rb"}) + + assert.Equal(t, ec.Files.Test, []string{"lasagna_test.rb"}) + testFiles, err := ec.GetTestFiles() + assert.NoError(t, err) + assert.Equal(t, testFiles, []string{"lasagna_test.rb"}) +} + +func TestExerciseConfigNoTestKey(t *testing.T) { + dir, err := ioutil.TempDir("", "exercise_config") + assert.NoError(t, err) + defer os.RemoveAll(dir) + + err = os.Mkdir(filepath.Join(dir, ".exercism"), os.ModePerm) + assert.NoError(t, err) + + f, err := os.Create(filepath.Join(dir, ".exercism", "config.json")) + assert.NoError(t, err) + defer f.Close() + + _, err = f.WriteString(`{ "blurb": "Learn about the basics of Ruby by following a lasagna recipe.", "authors": ["iHiD", "pvcarrera"], "files": { "exemplar": [".meta/exemplar.rb"] } } `) + assert.NoError(t, err) + + ec, err := NewExerciseConfig(dir) + assert.NoError(t, err) + + _, err = ec.GetSolutionFiles() + assert.Error(t, err, "no `files.solution` key in your `config.json`") + _, err = ec.GetTestFiles() + assert.Error(t, err, "no `files.test` key in your `config.json`") +} + +func TestMissingExerciseConfig(t *testing.T) { + dir, err := ioutil.TempDir("", "exercise_config") + assert.NoError(t, err) + defer os.RemoveAll(dir) + + _, err = NewExerciseConfig(dir) + assert.Error(t, err) + // any assertions about this error message have to work across all platforms, so be vague + // unix: ".exercism/config.json: no such file or directory" + // windows: "open .exercism\config.json: The system cannot find the path specified." + assert.Contains(t, err.Error(), filepath.Join(".exercism", "config.json:")) +} + +func TestInvalidExerciseConfig(t *testing.T) { + dir, err := ioutil.TempDir("", "exercise_config") + assert.NoError(t, err) + defer os.RemoveAll(dir) + + err = os.Mkdir(filepath.Join(dir, ".exercism"), os.ModePerm) + assert.NoError(t, err) + + f, err := os.Create(filepath.Join(dir, ".exercism", "config.json")) + assert.NoError(t, err) + defer f.Close() + + // invalid JSON + _, err = f.WriteString(`{ "blurb": "Learn about the basics of Ruby by following a lasagna recipe.", "authors": ["iHiD", "pvcarr `) + assert.NoError(t, err) + + _, err = NewExerciseConfig(dir) + assert.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "unexpected end of JSON input")) +} diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go new file mode 100644 index 000000000..32c6fbc3a --- /dev/null +++ b/workspace/test_configurations.go @@ -0,0 +1,252 @@ +package workspace + +import ( + "fmt" + "runtime" + "strings" +) + +type TestConfiguration struct { + // The static portion of the test Command, which will be run for every test on this track. Examples include `cargo test` or `go test`. + // Might be empty if there are platform-specific versions + Command string + + // Windows-specific test command. Mostly relevant for tests wrapped by shell invocations. Falls back to `Command` if we're not running windows or this is empty. + WindowsCommand string +} + +func (c *TestConfiguration) GetTestCommand() (string, error) { + var cmd string + if runtime.GOOS == "windows" && c.WindowsCommand != "" { + cmd = c.WindowsCommand + } else { + cmd = c.Command + } + + // pre-declare these so we can conditionally initialize them + var exerciseConfig *ExerciseConfig + var err error + + if strings.Contains(cmd, "{{") { + // only read exercise's config.json if we need it + exerciseConfig, err = NewExerciseConfig(".") + if err != nil { + return "", err + } + } + + if strings.Contains(cmd, "{{solution_files}}") { + if exerciseConfig == nil { + return "", fmt.Errorf("exerciseConfig not initialize before use") + } + solutionFiles, err := exerciseConfig.GetSolutionFiles() + if err != nil { + return "", err + } + cmd = strings.ReplaceAll(cmd, "{{solution_files}}", strings.Join(solutionFiles, " ")) + } + if strings.Contains(cmd, "{{test_files}}") { + if exerciseConfig == nil { + return "", fmt.Errorf("exerciseConfig not initialize before use") + } + testFiles, err := exerciseConfig.GetTestFiles() + if err != nil { + return "", err + } + cmd = strings.ReplaceAll(cmd, "{{test_files}}", strings.Join(testFiles, " ")) + } + + return cmd, nil +} + +// some tracks aren't (or won't be) implemented; every track is listed either way +var TestConfigurations = map[string]TestConfiguration{ + "8th": { + Command: "bash tester.sh", + WindowsCommand: "tester.bat", + }, + // abap: tests are run via "ABAP Development Tools", not the CLI + "awk": { + Command: "bats {{test_files}}", + }, + "ballerina": { + Command: "bal test", + }, + "bash": { + Command: "bats {{test_files}}", + }, + "c": { + Command: "make", + }, + "cfml": { + Command: "box task run TestRunner", + }, + "clojure": { + // chosen because the docs recommend `clj` by default and `lein` as optional + Command: "clj -X:test", + }, + "cobol": { + Command: "bash test.sh", + WindowsCommand: "pwsh test.ps1", + }, + "coffeescript": { + Command: "jasmine-node --coffee {{test_files}}", + }, + // common-lisp: tests are loaded into a "running Lisp implementation", not the CLI directly + "cpp": { + Command: "make", + }, + "crystal": { + Command: "crystal spec", + }, + "csharp": { + Command: "dotnet test", + }, + "d": { + // this always works even if the user installed DUB + Command: "dmd source/*.d -de -w -main -unittest", + }, + "dart": { + Command: "dart test", + }, + // delphi: tests are run via IDE + "elixir": { + Command: "mix test", + }, + "elm": { + Command: "elm-test", + }, + "emacs-lisp": { + Command: "emacs -batch -l ert -l *-test.el -f ert-run-tests-batch-and-exit", + }, + "erlang": { + Command: "rebar3 eunit", + }, + "fortran": { + Command: "make", + }, + "fsharp": { + Command: "dotnet test", + }, + "gleam": { + Command: "gleam test", + }, + "go": { + Command: "go test", + }, + "groovy": { + Command: "gradle test", + }, + "haskell": { + Command: "stack test", + }, + "java": { + Command: "gradle test", + }, + "javascript": { + Command: "npm run test", + }, + "jq": { + Command: "bats {{test_files}}", + }, + "julia": { + Command: "julia runtests.jl", + }, + "kotlin": { + Command: "./gradlew test", + WindowsCommand: "gradlew.bat test", + }, + "lfe": { + Command: "make test", + }, + "lua": { + Command: "busted", + }, + "mips": { + Command: "java -jar /path/to/mars.jar nc runner.mips impl.mips", + }, + "nim": { + Command: "nim r {{test_files}}", + }, + // objective-c: tests are run via XCode. There's a CLI option (ruby gem `objc`), but the docs note that this is an inferior experience + "ocaml": { + Command: "make", + }, + "perl5": { + Command: "prove .", + }, + // pharo-smalltalk: tests are run via IDE + "php": { + Command: "phpunit {{test_files}}", + }, + // plsql: test are run via a "mounted oracle db" + "powershell": { + Command: "Invoke-Pester", + }, + "prolog": { + Command: "swipl -f {{solution_files}} -s {{test_files}} -g run_tests,halt -t 'halt(1)'", + }, + "purescript": { + Command: "spago test", + }, + "python": { + Command: "python3 -m pytest -o markers=task {{test_files}}", + }, + "r": { + Command: "Rscript {{test_files}}", + }, + "racket": { + Command: "raco test {{test_files}}", + }, + "raku": { + Command: "prove6 {{test_files}}", + }, + "reasonml": { + Command: "npm run test", + }, + "red": { + Command: "red {{test_files}}", + }, + "ruby": { + Command: "ruby {{test_files}}", + }, + "rust": { + Command: "cargo test --", + }, + "scala": { + Command: "sbt test", + }, + // scheme: docs present 2 equally valid test methods (`make chez` and `make guile`). So I wasn't sure which to pick + "sml": { + Command: "poly -q --use {{test_files}}", + }, + "swift": { + Command: "swift test", + }, + "tcl": { + Command: "tclsh {{test_files}}", + }, + "typescript": { + Command: "yarn test", + }, + // unison: tests are run from an active UCM session + "vbnet": { + Command: "dotnet test", + }, + // vimscript: tests are run from inside a vim session + "vlang": { + Command: "v -stats test run_test.v", + }, + "wasm": { + Command: "npm run test", + }, + "wren": { + Command: "wrenc {{test_files}}", + }, + "x86-64-assembly": { + Command: "make", + }, + "zig": { + Command: "zig test {{test_files}}", + }, +} diff --git a/workspace/test_configurations_test.go b/workspace/test_configurations_test.go new file mode 100644 index 000000000..97ff40902 --- /dev/null +++ b/workspace/test_configurations_test.go @@ -0,0 +1,103 @@ +package workspace + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetCommand(t *testing.T) { + testConfig, ok := TestConfigurations["elixir"] + assert.True(t, ok, "unexpectedly unable to find elixir test config") + + cmd, err := testConfig.GetTestCommand() + assert.NoError(t, err) + + assert.Equal(t, cmd, "mix test") +} + +func TestWindowsCommands(t *testing.T) { + testConfig, ok := TestConfigurations["cobol"] + assert.True(t, ok, "unexpectedly unable to find cobol test config") + + cmd, err := testConfig.GetTestCommand() + assert.NoError(t, err) + + if runtime.GOOS == "windows" { + assert.Contains(t, cmd, ".ps1") + assert.NotContains(t, cmd, ".sh") + } else { + assert.Contains(t, cmd, ".sh") + assert.NotContains(t, cmd, ".ps1") + } +} + +func TestGetCommandMissingConfig(t *testing.T) { + testConfig, ok := TestConfigurations["ruby"] + assert.True(t, ok, "unexpectedly unable to find ruby test config") + + _, err := testConfig.GetTestCommand() + assert.Error(t, err) + // any assertions about this error message have to work across all platforms, so be vague + // unix: ".exercism/config.json: no such file or directory" + // windows: "open .exercism\config.json: The system cannot find the path specified." + assert.Contains(t, err.Error(), filepath.Join(".exercism", "config.json:")) +} + +func TestIncludesSolutionAndTestFilesInCommand(t *testing.T) { + testConfig, ok := TestConfigurations["prolog"] + assert.True(t, ok, "unexpectedly unable to find prolog test config") + + // this creates a config file in the test directory and removes it + dir := filepath.Join(".", ".exercism") + defer os.RemoveAll(dir) + err := os.Mkdir(dir, os.ModePerm) + assert.NoError(t, err) + + f, err := os.Create(filepath.Join(dir, "config.json")) + assert.NoError(t, err) + defer f.Close() + + _, err = f.WriteString(`{ "blurb": "Learn about the basics of Prolog by following a lasagna recipe.", "authors": ["iHiD", "pvcarrera"], "files": { "solution": ["lasagna.pl"], "test": ["lasagna_tests.plt"] } } `) + assert.NoError(t, err) + + cmd, err := testConfig.GetTestCommand() + assert.NoError(t, err) + assert.Equal(t, cmd, "swipl -f lasagna.pl -s lasagna_tests.plt -g run_tests,halt -t 'halt(1)'") +} + +func TestIncludesTestFilesInCommand(t *testing.T) { + testConfig, ok := TestConfigurations["ruby"] + assert.True(t, ok, "unexpectedly unable to find ruby test config") + + // this creates a config file in the test directory and removes it + dir := filepath.Join(".", ".exercism") + defer os.RemoveAll(dir) + err := os.Mkdir(dir, os.ModePerm) + assert.NoError(t, err) + + f, err := os.Create(filepath.Join(dir, "config.json")) + assert.NoError(t, err) + defer f.Close() + + _, err = f.WriteString(`{ "blurb": "Learn about the basics of Ruby by following a lasagna recipe.", "authors": ["iHiD", "pvcarrera"], "files": { "solution": ["lasagna.rb"], "test": ["lasagna_test.rb", "some_other_file.rb"], "exemplar": [".meta/exemplar.rb"] } } `) + assert.NoError(t, err) + + cmd, err := testConfig.GetTestCommand() + assert.NoError(t, err) + assert.Equal(t, cmd, "ruby lasagna_test.rb some_other_file.rb") +} + +func TestRustHasTrailingDashes(t *testing.T) { + testConfig, ok := TestConfigurations["rust"] + assert.True(t, ok, "unexpectedly unable to find rust test config") + + cmd, err := testConfig.GetTestCommand() + assert.NoError(t, err) + + assert.True(t, strings.HasSuffix(cmd, "--"), "rust's test command should have trailing dashes") +} From 3551c62b2cc844255badd0b9926f5362741cf93c Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 28 Jul 2023 14:02:02 +0200 Subject: [PATCH 451/544] Bump version to 3.2.0 (#1093) --- CHANGELOG.md | 5 ++++- cmd/version.go | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c84fa0a3..a1dcca2ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,12 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ---------------- ## Next Release -* [#1092](https://github.com/exercism/cli/pull/1092) Add `exercism test` command to run the unit tests for nearly any track (inspired by [universal-test-runner](https://github.com/xavdid/universal-test-runner)) - [@xavdid] * **Your contribution here** +## v3.2.0 (2023-07-28) +* [#1092](https://github.com/exercism/cli/pull/1092) Add `exercism test` command to run the unit tests for nearly any track (inspired by [universal-test-runner](https://github.com/xavdid/universal-test-runner)) - [@xavdid] +* [#1073](https://github.com/exercism/cli/pull/1073) Add `arm64` build for each OS + ## v3.1.0 (2022-10-04) * [#979](https://github.com/exercism/cli/pull/979) Protect existing solutions from being overwritten by 'download' - [@harugo] * [#981](https://github.com/exercism/cli/pull/981) Check if authorisation header is set before attempting to extract token - [@harugo] diff --git a/cmd/version.go b/cmd/version.go index 069866b78..d8e287f4a 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.1.0" +const Version = "3.2.0" // checkLatest flag for version command. var checkLatest bool From d0faf4c2fef579ac85012c74cb285b969c274ffa Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 28 Jul 2023 14:53:22 +0200 Subject: [PATCH 452/544] Fix deprecated argument to goreleaser (#1094) --- RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index bf3e1c8bf..fe41e37ea 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -33,7 +33,7 @@ Commit this change on a branch along with the CHANGELOG updates in a single comm ```bash # Test run -goreleaser --skip-publish --snapshot --rm-dist +goreleaser --skip-publish --snapshot --clean # Create a new tag on the main branch and push it git tag -a v3.0.16 -m "Trying out GoReleaser" From ca046ee7579aacac64b1a4f1390e5dd6084b9415 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 10:31:17 +0200 Subject: [PATCH 453/544] Bump actions/setup-go from 2.2.0 to 4.0.0 (#1088) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2.2.0 to 4.0.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/bfdd3570ce990073878bf10f6b2d79082de49492...4d34df0c2316fe8122ab82dc22947d607c0c91f9) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Erik Schierboom --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c396f2e2a..493acf702 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b - - uses: actions/setup-go@bfdd3570ce990073878bf10f6b2d79082de49492 + - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 with: go-version: ${{ matrix.go-version }} From 2ab0439a2cecf71ef418b470471b6dbe6ed238ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 10:31:29 +0200 Subject: [PATCH 454/544] Bump actions/checkout from 3.2.0 to 3.5.0 (#1091) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.2.0 to 3.5.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/755da8c3cf115ac066823e79a1e1788f8940201b...8f4b7f84864484a7bf31766abe9204da3cbe65b3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Erik Schierboom --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 493acf702..f3913210b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: os: [ubuntu-latest, windows-latest, macOS-latest] steps: - - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b + - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 with: @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b + - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 - name: Check formatting run: ./.gha.gofmt.sh From eaf599b3a89125a9667800f77b8ba07473a07192 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Tue, 1 Aug 2023 11:27:35 +0200 Subject: [PATCH 455/544] Fix goreleaser name templates (#1095) --- .goreleaser.yml | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index cb55b79f4..c2f9886af 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -37,10 +37,14 @@ changelog: - '^test:' archives: - - name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" - replacements: - amd64: x86_64 - 386: i386 + - name_template: >- + {{- .ProjectName }}- + {{- .Version }}- + {{- .Os }}- + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{- .Arch }}{{ end }} + {{- if .Arm }}v{{- .Arm }}{{ end }} format_overrides: - goos: windows format: zip @@ -92,10 +96,14 @@ snapcrafts: # https://snapcraft.io/docs/reference/channels grade: stable description: Exercism is an online platform designed to help you improve your coding skills through practice and mentorship. Exercism provides you with thousands of exercises spread across numerous language tracks. Each one is a fun and interesting challenge designed to teach you a little more about the features of a language. - name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" - replacements: - amd64: x86_64 - 386: i386 + name_template: >- + {{- .ProjectName }}- + {{- .Version }}- + {{- .Os }}- + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{- .Arch }}{{ end }} + {{- if .Arm }}v{{- .Arm }}{{ end }} apps: exercism: plugs: ["home", "network", "removable-media","personal-files"] From 1464c3836b0a7204573f4726b47445309e466e32 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Tue, 1 Aug 2023 12:56:50 +0200 Subject: [PATCH 456/544] Fix link in release doc (#1096) --- RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index fe41e37ea..ce89b8503 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -97,6 +97,6 @@ For more information see [How To Open a Homebrew Pull Request](https://docs.brew ## Update the docs site If there are any significant changes, we should describe them on -[exercism.io/cli]([https://exercism.io/cli). +[exercism.io/cli](https://exercism.io/cli). The codebase lives at [exercism/website-copy](https://github.com/exercism/website-copy) in `pages/cli.md`. From cddc1228f3a515b4ee49442eebadc99329b156c2 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Tue, 1 Aug 2023 13:02:32 +0200 Subject: [PATCH 457/544] More release doc cleanup (#1097) --- RELEASE.md | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index ce89b8503..47645c282 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -12,7 +12,7 @@ release process. ## Confirm / Update the Changelog -Make sure all the recent changes are reflected in the "next release" section of the CHANGELOG.md file. All the changes in the "next release" section should be moved to a new section that describes the version number, and gives it a date. +Make sure all the recent changes are reflected in the "next release" section of the CHANGELOG.md file. All the changes in the "next release" section should be moved to a new section that describes the version number, and gives it a date. You can view changes using the /compare/ view: https://github.com/exercism/cli/compare/$PREVIOUS_RELEASE...main @@ -40,7 +40,7 @@ git tag -a v3.0.16 -m "Trying out GoReleaser" git push origin v3.0.16 # Build and release -goreleaser --rm-dist +goreleaser --clean # You must be logged into snapcraft to publish a new snap snapcraft login @@ -53,9 +53,8 @@ for f in `ls dist/*.snap`; do snapcraft push --release=stable $f; done ## Cut Release on GitHub -Run [exercism-cp-archive-hack.sh](https://gist.github.com/ekingery/961650fca4e2233098c8320f32736836) which takes the new archive files and renames them to match the old naming scheme for backward compatibility. Until mid to late 2020, we will need to manually upload the backward-compatible archive files generated in `/tmp/exercism_tmp_upload`. - -The generated archive files should be uploaded to the [draft release page created by GoReleaser](https://github.com/exercism/cli/releases). Describe the release, select a specific commit to target, paste the following release text, and describe the new changes. +At this point, Goreleaser will a created a draft PR at https://github.com/exercism/cli/releases/tag/vX.Y.Z. +On that page, update the release description to: ``` To install, follow the interactive installation instructions at https://exercism.io/cli-walkthrough @@ -64,32 +63,18 @@ To install, follow the interactive installation instructions at https://exercism [describe changes in this release] ``` - Lastly, test and publish the draft - +Lastly, test and publish the draft ## Update Homebrew -This is helpful for the (many) Mac OS X users. - -First, get a copy of the latest tarball of the source code: - -``` -cd ~/tmp && wget https://github.com/exercism/cli/archive/vX.Y.Z.tar.gz -``` - -Get the SHA256 of the tarball: - -``` -shasum -a 256 vX.Y.Z.tar.gz -``` - -Update the homebrew formula: +Next, we'll submit a PR to Homebrew to update the Exercism formula (which is how macOS users usually download the CLI): ``` +cd /tmp && curl -O https://github.com/exercism/cli/archive/vX.Y.Z.tar.gz cd $(brew --repository) git checkout master brew update -brew bump-formula-pr --strict exercism --url=https://github.com/exercism/cli/archive/vX.Y.Z.tar.gz --sha256=$SHA +brew bump-formula-pr --strict exercism --url=https://github.com/exercism/cli/archive/vX.Y.Z.tar.gz --sha256=$(shasum -a 256 /tmp/vX.Y.Z.tar.gz) ``` For more information see [How To Open a Homebrew Pull Request](https://docs.brew.sh/How-To-Open-a-Homebrew-Pull-Request). From ba0ea34a356fe43beed8765f3255a550f8e241d2 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Tue, 1 Aug 2023 14:12:12 +0200 Subject: [PATCH 458/544] Remove snapcraft (#1098) --- .goreleaser.yml | 32 -------------------------------- RELEASE.md | 7 ------- 2 files changed, 39 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index c2f9886af..ee029e45c 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -79,35 +79,3 @@ release: # brews: # We do not use the brew config, which is for taps, not core forumulas. - -snapcrafts: - - - name: exercism - license: MIT - # Whether to publish the snap to the snapcraft store. - # Remember you need to `snapcraft login` first. - # Defaults to false. - # publish: true - summary: Command-line client for https://exercism.org - # https://snapcraft.io/docs/reference/confinement - confinement: strict - # A snap of type base to be used as the execution environment for this snap. - base: core18 - # https://snapcraft.io/docs/reference/channels - grade: stable - description: Exercism is an online platform designed to help you improve your coding skills through practice and mentorship. Exercism provides you with thousands of exercises spread across numerous language tracks. Each one is a fun and interesting challenge designed to teach you a little more about the features of a language. - name_template: >- - {{- .ProjectName }}- - {{- .Version }}- - {{- .Os }}- - {{- if eq .Arch "amd64" }}x86_64 - {{- else if eq .Arch "386" }}i386 - {{- else }}{{- .Arch }}{{ end }} - {{- if .Arm }}v{{- .Arm }}{{ end }} - apps: - exercism: - plugs: ["home", "network", "removable-media","personal-files"] - plugs: - personal-files: - write: - - $HOME/ diff --git a/RELEASE.md b/RELEASE.md index 47645c282..1592c7519 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -6,7 +6,6 @@ release process. ## Requirements 1. [Install GoReleaser](https://goreleaser.com/install/) -1. [Install snapcraft](https://snapcraft.io/docs/snapcraft-overview) 1. [Setup GitHub token](https://goreleaser.com/scm/github/) 1. Have a gpg key installed on your machine - it is [used for signing the artifacts](https://goreleaser.com/customization/sign/) @@ -42,12 +41,6 @@ git push origin v3.0.16 # Build and release goreleaser --clean -# You must be logged into snapcraft to publish a new snap -snapcraft login - -# Push to snapcraft -for f in `ls dist/*.snap`; do snapcraft push --release=stable $f; done - # [TODO] Push to homebrew ``` From 8b850d25149a98bfe0cca84afc56160b0b5782e0 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Tue, 1 Aug 2023 14:52:18 +0200 Subject: [PATCH 459/544] Simplify release (#1099) * Remove snapcraft * Simply release process --- RELEASE.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 1592c7519..1819773af 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -20,23 +20,27 @@ GoReleaser supports the [auto generation of a changelog](https://goreleaser.com/ ## Bump the version -Edit the `Version` constant in `cmd/version.go` +1. Create a branch for the new version +1. Edit the `Version` constant in `cmd/version.go` +1. Update the `CHANGELOG.md` file +1. Commit the updated version +1. Create a PR _Note: It's useful to add the version to the commit message when you bump it: e.g. `Bump version to v2.3.4`._ -In the future we will probably want to replace the hardcoded `Version` constant with [main.version](https://goreleaser.com/cookbooks/using-main.version). Here is a [stack overflow post on injecting to cmd/version.go](https://stackoverflow.com/a/47510909). - -Commit this change on a branch along with the CHANGELOG updates in a single commit, and create a PR for merge to main. - ## Cut a release +Once the version bump PR has been merged, run the following commands: + ```bash +VERSION=$(sed -n -E 's/^const Version = "([0-9]+\.[0-9]+\.[0-9]+)"$/v\1/p' cmd/version.go) + # Test run goreleaser --skip-publish --snapshot --clean # Create a new tag on the main branch and push it -git tag -a v3.0.16 -m "Trying out GoReleaser" -git push origin v3.0.16 +git tag -a "${VERSION}" -m "Trying out GoReleaser" +git push origin "${VERSION}" # Build and release goreleaser --clean From 16b7d8f6d48309a8b2bb5bb6c32d51c3e1cbdcc8 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 11 Aug 2023 08:50:53 +0200 Subject: [PATCH 460/544] Update release instructions (#1104) --- RELEASE.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 1819773af..35c26bfba 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -33,24 +33,31 @@ _Note: It's useful to add the version to the commit message when you bump it: e. Once the version bump PR has been merged, run the following commands: ```bash -VERSION=$(sed -n -E 's/^const Version = "([0-9]+\.[0-9]+\.[0-9]+)"$/v\1/p' cmd/version.go) +VERSION=$(sed -n -E 's/^const Version = "([0-9]+\.[0-9]+\.[0-9]+)"$/\1/p' cmd/version.go) +TAG_NAME="v${VERSION}" # Test run goreleaser --skip-publish --snapshot --clean # Create a new tag on the main branch and push it -git tag -a "${VERSION}" -m "Trying out GoReleaser" -git push origin "${VERSION}" +git tag -a "${TAG_NAME}" -m "Trying out GoReleaser" +git push origin "${TAG_NAME}" # Build and release goreleaser --clean +# Upload copies of the Windows files for use by the Exercism Windows installer +cp "dist/exercism-${VERSION}-windows-i386.zip" dist/exercism-windows-32bit.zip +cp "dist/exercism-${VERSION}-windows-x86_64.zip" dist/exercism-windows-64bit.zip +gh release upload "${TAG_NAME}" dist/exercism-windows-32bit.zip +gh release upload "${TAG_NAME}" dist/exercism-windows-64bit.zip + # [TODO] Push to homebrew ``` ## Cut Release on GitHub -At this point, Goreleaser will a created a draft PR at https://github.com/exercism/cli/releases/tag/vX.Y.Z. +At this point, Goreleaser will have created a draft PR at https://github.com/exercism/cli/releases/tag/vX.Y.Z. On that page, update the release description to: ``` From bbd449ee91cb6ea4fa3d05605f38ea4281d8f8df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 13:49:04 +0200 Subject: [PATCH 461/544] Bump actions/setup-go from 4.0.0 to 4.1.0 (#1105) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4.0.0 to 4.1.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/4d34df0c2316fe8122ab82dc22947d607c0c91f9...93397bea11091df50f3d7e59dc26a7711a8bcfbe) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f3913210b..688076d0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 - - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 + - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe with: go-version: ${{ matrix.go-version }} From ecd826de4f64cc7a4f3167443701162d4451a09f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 13:49:13 +0200 Subject: [PATCH 462/544] Bump actions/checkout from 3.5.0 to 3.5.3 (#1101) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.0 to 3.5.3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/8f4b7f84864484a7bf31766abe9204da3cbe65b3...c85c95e3d7251135ab7dc9ce3241c5835cc595a9) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 688076d0c..63ee024b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: os: [ubuntu-latest, windows-latest, macOS-latest] steps: - - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe with: @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 - name: Check formatting run: ./.gha.gofmt.sh From 0fcf8c28f821728a0e4bbc533b04e1f4a8e0e495 Mon Sep 17 00:00:00 2001 From: Wilken Rivera Date: Wed, 6 Sep 2023 10:35:27 -0400 Subject: [PATCH 463/544] github/workflow/release.yml: Add automated release pipeline (#1106) --- .github/workflows/release.yml | 41 ++++++++++++++ .goreleaser.yml | 101 ++++++++++++++++++++++++---------- .release/header.md | 3 + RELEASE.md | 21 ++----- 4 files changed, 121 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .release/header.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..c2c9aecad --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +name: release + +on: + push: + tags: + - 'v*.*.*' # semver release tags + - 'v*.*.*-*' # pre-release tags for testing + +permissions: + contents: write # needed by goreleaser/goreleaser-action for publishing release artifacts + +jobs: + goreleaser: + runs-on: ubuntu-22.04 + steps: + + - name: Checkout code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version: '1.19' + + - name: Import GPG Key + id: import_gpg + uses: crazy-max/ghaction-import-gpg@72b6676b71ab476b77e676928516f6982eef7a41 # v5.3.0 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.PASSPHRASE }} + + - name: Cut Release + uses: goreleaser/goreleaser-action@3fa32b8bb5620a2c1afe798654bbad59f9da4906 # v4.4.0 + with: + version: latest + args: release --clean --release-header .release/header.md --timeout 120m # default time is 30m + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} diff --git a/.goreleaser.yml b/.goreleaser.yml index ee029e45c..8a5dccbd2 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,33 +1,48 @@ # You can find the GoReleaser documentation at http://goreleaser.com project_name: exercism +env: + - CGO_ENABLED=0 builds: -- env: - - CGO_ENABLED=0 - main: ./exercism/main.go - goos: - - darwin - - linux - - windows - - freebsd - - openbsd - goarch: - - amd64 - - 386 - - arm - - arm64 - - ppc64 - goarm: - - 5 - - 6 - ignore: - - goos: openbsd - goarch: arm - - goos: freebsd - goarch: arm - -checksum: - name_template: '{{ .ProjectName }}_checksums.txt' + - id: release-build + main: ./exercism/main.go + mod_timestamp: '{{ .CommitTimestamp }}' + flags: + - -trimpath # removes file system paths from compiled executable + ldflags: + - '-s -w' # strip debug symbols and DWARF debugging info + goos: + - darwin + - linux + - windows + - freebsd + - openbsd + goarch: + - amd64 + - 386 + - arm + - arm64 + - ppc64 + goarm: + - 5 + - 6 + ignore: + - goos: openbsd + goarch: arm + - goos: freebsd + goarch: arm + - id: installer-build + main: ./exercism/main.go + mod_timestamp: '{{ .CommitTimestamp }}' + flags: + - -trimpath # removes file system paths from compiled executable + ldflags: + - '-s -w' # strip debug symbols and DWARF debugging info + goos: + - windows + goarch: + - amd64 + - 386 changelog: sort: asc @@ -37,7 +52,10 @@ changelog: - '^test:' archives: - - name_template: >- + - id: release-archives + builds: + - release-build + name_template: >- {{- .ProjectName }}- {{- .Version }}- {{- .Os }}- @@ -49,12 +67,37 @@ archives: - goos: windows format: zip files: - - shell/**/* + - shell/** - LICENSE - README.md + - id: installer-archives + builds: + - installer-build + name_template: >- + {{- .ProjectName }}- + {{- .Version }}- + {{- .Os }}- + {{- if eq .Arch "amd64" }}64bit + {{- else if eq .Arch "386" }}32bit + {{- else }}{{- .Arch }}{{ end }} + {{- if .Arm }}v{{- .Arm }}{{ end }} + format_overrides: + - goos: windows + format: zip + files: + - shell/** + - LICENSE + - README.md + +checksum: + name_template: '{{ .ProjectName }}_checksums.txt' + ids: + - release-archives + - installer-archives signs: -- artifacts: checksum + - artifacts: checksum + args: ["--batch", "-u", "{{ .Env.GPG_FINGERPRINT }}", "--output", "${signature}", "--detach-sign", "${artifact}"] release: # Repo in which the release will be created. diff --git a/.release/header.md b/.release/header.md new file mode 100644 index 000000000..55027b78c --- /dev/null +++ b/.release/header.md @@ -0,0 +1,3 @@ +To install, follow the interactive installation instructions at https://exercism.io/cli-walkthrough + +--- diff --git a/RELEASE.md b/RELEASE.md index 35c26bfba..566db9f83 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,7 +1,6 @@ # Cutting a CLI Release -The Exercism CLI uses [GoReleaser](https://goreleaser.com) to automate the -release process. +The Exercism CLI uses [GoReleaser](https://goreleaser.com) to automate the release process. ## Requirements @@ -11,13 +10,12 @@ release process. ## Confirm / Update the Changelog -Make sure all the recent changes are reflected in the "next release" section of the CHANGELOG.md file. All the changes in the "next release" section should be moved to a new section that describes the version number, and gives it a date. +Make sure all the recent changes are reflected in the "next release" section of the CHANGELOG.md file. +All the changes in the "next release" section should be moved to a new section that describes the version number, and gives it a date. You can view changes using the /compare/ view: https://github.com/exercism/cli/compare/$PREVIOUS_RELEASE...main -GoReleaser supports the [auto generation of a changelog](https://goreleaser.com/customization/#customize-the-changelog) we will want to customize to meet our standards (not including refactors, test updates, etc). We should also consider using [the release notes feature](https://goreleaser.com/customization/#custom-release-notes). - ## Bump the version 1. Create a branch for the new version @@ -43,28 +41,19 @@ goreleaser --skip-publish --snapshot --clean git tag -a "${TAG_NAME}" -m "Trying out GoReleaser" git push origin "${TAG_NAME}" -# Build and release -goreleaser --clean - -# Upload copies of the Windows files for use by the Exercism Windows installer -cp "dist/exercism-${VERSION}-windows-i386.zip" dist/exercism-windows-32bit.zip -cp "dist/exercism-${VERSION}-windows-x86_64.zip" dist/exercism-windows-64bit.zip -gh release upload "${TAG_NAME}" dist/exercism-windows-32bit.zip -gh release upload "${TAG_NAME}" dist/exercism-windows-64bit.zip - # [TODO] Push to homebrew ``` ## Cut Release on GitHub -At this point, Goreleaser will have created a draft PR at https://github.com/exercism/cli/releases/tag/vX.Y.Z. +At this point, Goreleaser will have created a draft release at https://github.com/exercism/cli/releases/tag/vX.Y.Z. On that page, update the release description to: ``` To install, follow the interactive installation instructions at https://exercism.io/cli-walkthrough --- -[describe changes in this release] +[modify the generated release-notes to describe changes in this release] ``` Lastly, test and publish the draft From c763ea58ce341b19407e9e7ac403bf2efc22d25d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 16:58:05 +0200 Subject: [PATCH 464/544] Bump actions/checkout from 3.5.3 to 4.0.0 (#1108) --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63ee024b0..511b27234 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: os: [ubuntu-latest, windows-latest, macOS-latest] steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe with: @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - name: Check formatting run: ./.gha.gofmt.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c2c9aecad..0c827639a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 with: fetch-depth: 0 From 8f2c7caa030d42d6bfbd5c001b036995af10667e Mon Sep 17 00:00:00 2001 From: Wilken Rivera Date: Fri, 22 Sep 2023 09:38:11 -0400 Subject: [PATCH 465/544] cmd/submit_test.go: Update test server to use unmodified filename (#1115) Following RFC 7578, Go 1.17+ strips the directory information in fileHeader.Filename. This change updates the test server to use Header["Content-Disposition"] for obtaining the unmodified filename for validating the submitted files directory tree in the request. This work builds on the PR opened by @QuLogic https://github.com/exercism/cli/pull/1066 Co-authored-by: Erik Schierboom --- cmd/submit_test.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 7286a19e6..ea16b6543 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "mime" "net/http" "net/http/httptest" "os" @@ -561,7 +562,16 @@ func fakeSubmitServer(t *testing.T, submittedFiles map[string]string) *httptest. if err != nil { t.Fatal(err) } - submittedFiles[fileHeader.Filename] = string(body) + // Following RFC 7578, Go 1.17+ strips the directory information in fileHeader.Filename. + // Validating the submitted files directory tree is important so Content-Disposition is used for + // obtaining the unmodified filename. + v := fileHeader.Header.Get("Content-Disposition") + _, dispositionParams, err := mime.ParseMediaType(v) + if err != nil { + t.Fatalf("failed to obtain submitted filename from multipart header: %s", err.Error()) + } + filename := dispositionParams["filename"] + submittedFiles[filename] = string(body) } fmt.Fprint(w, "{}") From 0e017aa3b5f72c8796609557c05f1308ce714d30 Mon Sep 17 00:00:00 2001 From: Wilken Rivera Date: Thu, 5 Oct 2023 02:46:02 -0400 Subject: [PATCH 466/544] Bump target Go version to 1.20 (#1118) * Bump minimum Go version to 1.18 This change bumps the minimum Go version to 1.18 to take advantage of a number of fixes to the language, while matching the minimum version for a number of key dependencies which have been moving away from Go 1.15. This change drops support for Go 1.15 in the Exercism CLI. * Bump minimum go version to 1.19 * fix: update build tags to Go 1.18 syntax ``` ~> go1.18.10 fix ./... ``` * Replace calls to deprecated io/ioutil pkg * fix(deps): update module github.com/spf13/viper to v1.15.0 This change bumps spf13/viper to address reported vulnerabilities in yaml.v2 ``` ~> govulncheck -test ./... Scanning your code and 210 packages across 21 dependent modules for known vulnerabilities... Vulnerability #1: GO-2022-0956 Excessive resource consumption in gopkg.in/yaml.v2 More info: https://pkg.go.dev/vuln/GO-2022-0956 Module: gopkg.in/yaml.v2 Found in: gopkg.in/yaml.v2@v2.0.0-20170721122051-25c4ec802a7d Fixed in: gopkg.in/yaml.v2@v2.2.4 Example traces found: #1: cmd/submit.go:129:23: cmd.getExerciseSolutionFiles calls viper.Viper.ReadInConfig, which eventually calls yaml.Unmarshal Vulnerability #2: GO-2021-0061 Denial of service in gopkg.in/yaml.v2 More info: https://pkg.go.dev/vuln/GO-2021-0061 Module: gopkg.in/yaml.v2 Found in: gopkg.in/yaml.v2@v2.0.0-20170721122051-25c4ec802a7d Fixed in: gopkg.in/yaml.v2@v2.2.3 Example traces found: #1: cmd/submit.go:129:23: cmd.getExerciseSolutionFiles calls viper.Viper.ReadInConfig, which eventually calls yaml.Unmarshal Vulnerability #3: GO-2020-0036 Excessive resource consumption in YAML parsing in gopkg.in/yaml.v2 More info: https://pkg.go.dev/vuln/GO-2020-0036 Module: gopkg.in/yaml.v2 Found in: gopkg.in/yaml.v2@v2.0.0-20170721122051-25c4ec802a7d Fixed in: gopkg.in/yaml.v2@v2.2.8 Example traces found: #1: cmd/submit.go:129:23: cmd.getExerciseSolutionFiles calls viper.Viper.ReadInConfig, which eventually calls yaml.Unmarshal ``` * deps: update module github.com/spf13/cobra to v1.7.0 * deps: update module github.com/stretchr/testify to v1.8.4 * workflows/ci.yml: Add multiple Go versions to testing matrix This change officially removes Go 1.15 from the testing matrix and adds the Go versions used for supporting the Exercism CLI. Namely Go 1.19, 1.20, and 1.21.x. * Bump minimum Go version to 1.20 * Bump Go tooling to use 1.20.x for release and testing. ``` Scanning your code and 207 packages across 19 dependent modules for known vulnerabilities... Vulnerability #1: GO-2023-2043 Improper handling of special tags within script contexts in html/template More info: https://pkg.go.dev/vuln/GO-2023-2043 Standard library Found in: html/template@go1.19.8 Fixed in: html/template@go1.21.1 Example traces found: #1: cmd/troubleshoot.go:127:20: cmd.Status.compile calls template.Template.Execute Vulnerability #2: GO-2023-2041 Improper handling of HTML-like comments in script contexts in html/template More info: https://pkg.go.dev/vuln/GO-2023-2041 Standard library Found in: html/template@go1.19.8 Fixed in: html/template@go1.21.1 Example traces found: #1: cmd/troubleshoot.go:127:20: cmd.Status.compile calls template.Template.Execute Vulnerability #3: GO-2023-1987 Large RSA keys can cause high CPU usage in crypto/tls More info: https://pkg.go.dev/vuln/GO-2023-1987 Standard library Found in: crypto/tls@go1.19.8 Fixed in: crypto/tls@go1.21rc4 Example traces found: #1: api/client.go:68:25: api.Client.Do calls http.Client.Do, which eventually calls tls.Conn.HandshakeContext #2: cli/cli.go:199:23: cli.extractBinary calls io.Copy, which eventually calls tls.Conn.Read #3: debug/debug.go:32:14: debug.Printf calls fmt.Fprintf, which calls tls.Conn.Write #4: api/client.go:68:25: api.Client.Do calls http.Client.Do, which eventually calls tls.Dialer.DialContext Vulnerability #4: GO-2023-1878 Insufficient sanitization of Host header in net/http More info: https://pkg.go.dev/vuln/GO-2023-1878 Standard library Found in: net/http@go1.19.8 Fixed in: net/http@go1.20.6 Example traces found: #1: api/client.go:68:25: api.Client.Do calls http.Client.Do #2: cmd/troubleshoot.go:206:32: cmd.apiPing.Call calls http.Client.Get Vulnerability #5: GO-2023-1840 Unsafe behavior in setuid/setgid binaries in runtime More info: https://pkg.go.dev/vuln/GO-2023-1840 Standard library Found in: runtime@go1.19.8 Fixed in: runtime@go1.20.5 Example traces found: #1: debug/debug.go:80:12: debug.DumpResponse calls log.Fatal, which eventually calls runtime.Caller #2: workspace/exercise_metadata.go:39:26: workspace.NewExerciseMetadata calls json.Unmarshal, which eventually calls runtime.Callers #3: workspace/exercise_metadata.go:39:26: workspace.NewExerciseMetadata calls json.Unmarshal, which eventually calls runtime.CallersFrames #4: workspace/exercise_metadata.go:39:26: workspace.NewExerciseMetadata calls json.Unmarshal, which eventually calls runtime.Frames.Next #5: cmd/root.go:39:27: cmd.Execute calls cobra.Command.Execute, which eventually calls runtime.GC #6: workspace/exercise_metadata.go:66:24: workspace.ExerciseMetadata.Write calls json.Marshal, which eventually calls runtime.GOMAXPROCS #7: config/config.go:57:18: config.Dir calls os.Getenv, which eventually calls runtime.GOROOT #8: cli/cli.go:202:29: cli.extractBinary calls os.File.Seek, which eventually calls runtime.KeepAlive #9: cli/cli.go:135:2: cli.CLI.Upgrade calls os.File.Close, which eventually calls runtime.SetFinalizer #10: debug/debug.go:32:14: debug.Printf calls fmt.Fprintf, which eventually calls runtime.Stack #11: cmd/root.go:39:27: cmd.Execute calls cobra.Command.Execute, which eventually calls runtime.TypeAssertionError.Error #12: workspace/test_configurations.go:5:2: workspace.init calls runtime.init, which calls runtime.defaultMemProfileRate #13: workspace/test_configurations.go:5:2: workspace.init calls runtime.init, which calls runtime.efaceOf #14: workspace/test_configurations.go:5:2: workspace.init calls runtime.init, which eventually calls runtime.findfunc #15: workspace/test_configurations.go:5:2: workspace.init calls runtime.init, which calls runtime.float64frombits #16: workspace/test_configurations.go:5:2: workspace.init calls runtime.init, which eventually calls runtime.forcegchelper #17: workspace/test_configurations.go:5:2: workspace.init calls runtime.init, which eventually calls runtime.funcMaxSPDelta #18: cmd/root.go:39:27: cmd.Execute calls cobra.Command.Execute, which eventually calls runtime.plainError.Error #19: workspace/test_configurations.go:5:2: workspace.init calls runtime.init, which eventually calls runtime.throw Vulnerability #6: GO-2023-1753 Improper handling of empty HTML attributes in html/template More info: https://pkg.go.dev/vuln/GO-2023-1753 Standard library Found in: html/template@go1.19.8 Fixed in: html/template@go1.20.4 Example traces found: #1: cmd/troubleshoot.go:127:20: cmd.Status.compile calls template.Template.Execute Vulnerability #7: GO-2023-1752 Improper handling of JavaScript whitespace in html/template More info: https://pkg.go.dev/vuln/GO-2023-1752 Standard library Found in: html/template@go1.19.8 Fixed in: html/template@go1.20.4 Example traces found: #1: cmd/troubleshoot.go:127:20: cmd.Status.compile calls template.Template.Execute Vulnerability #8: GO-2023-1751 Improper sanitization of CSS values in html/template More info: https://pkg.go.dev/vuln/GO-2023-1751 Standard library Found in: html/template@go1.19.8 Fixed in: html/template@go1.20.4 Example traces found: #1: cmd/troubleshoot.go:127:20: cmd.Status.compile calls template.Template.Execute ``` --- .github/workflows/ci.yml | 4 +- .github/workflows/release.yml | 2 +- CONTRIBUTING.md | 2 +- api/api.go | 4 +- cli/asset.go | 4 +- cli/cli.go | 8 +- cmd/cmd_test.go | 7 +- cmd/configure_test.go | 8 +- cmd/download.go | 5 +- cmd/download_test.go | 13 +- cmd/submit_symlink_test.go | 8 +- cmd/submit_test.go | 90 ++--- config/config_notwin_test.go | 1 - config/config_windows_test.go | 1 - config/resolve_notwin_test.go | 1 - debug/debug.go | 9 +- go.mod | 47 +-- go.sum | 524 +++++++++++++++++++++++++-- workspace/document_test.go | 5 +- workspace/exercise_config.go | 4 +- workspace/exercise_config_test.go | 9 +- workspace/exercise_metadata.go | 5 +- workspace/exercise_metadata_test.go | 3 +- workspace/exercise_test.go | 23 +- workspace/path_type_symlinks_test.go | 1 - workspace/workspace.go | 7 +- workspace/workspace_darwin_test.go | 3 +- workspace/workspace_test.go | 7 +- 28 files changed, 620 insertions(+), 185 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 511b27234..704eabdf9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,9 @@ jobs: strategy: fail-fast: false matrix: - go-version: ["1.15"] + go-version: + - '1.20.x' + - '1.21.x' os: [ubuntu-latest, windows-latest, macOS-latest] steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0c827639a..06175aa06 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: '1.19' + go-version: '1.20.x' - name: Import GPG Key id: import_gpg diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1936a39eb..1515a4fcb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ Exercism would be impossible without people like you being willing to spend time ## Dependencies -You'll need Go version 1.11 or higher. Follow the directions on http://golang.org/doc/install +You'll need Go version 1.20 or higher. Follow the directions on http://golang.org/doc/install ## Development diff --git a/api/api.go b/api/api.go index 342ce4230..25b5e9d14 100644 --- a/api/api.go +++ b/api/api.go @@ -2,7 +2,7 @@ package api import ( "bytes" - "io/ioutil" + "os" "golang.org/x/net/html/charset" "golang.org/x/text/transform" @@ -17,7 +17,7 @@ var ( ) func readFileAsUTF8String(filename string) (*string, error) { - b, err := ioutil.ReadFile(filename) + b, err := os.ReadFile(filename) if err != nil { return nil, err } diff --git a/cli/asset.go b/cli/asset.go index 246895faa..10bbab051 100644 --- a/cli/asset.go +++ b/cli/asset.go @@ -3,7 +3,7 @@ package cli import ( "bytes" "fmt" - "io/ioutil" + "io" "net/http" ) @@ -28,7 +28,7 @@ func (a *Asset) download() (*bytes.Reader, error) { } defer res.Body.Close() - bs, err := ioutil.ReadAll(res.Body) + bs, err := io.ReadAll(res.Body) if err != nil { return nil, err } diff --git a/cli/cli.go b/cli/cli.go index 4312eb018..fe113ec5b 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -8,8 +8,8 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net/http" + "os" "runtime" "strings" "time" @@ -161,8 +161,8 @@ func (c *CLI) fetchLatestRelease() error { return nil } -func extractBinary(source *bytes.Reader, os string) (binary io.ReadCloser, err error) { - if os == "windows" { +func extractBinary(source *bytes.Reader, platform string) (binary io.ReadCloser, err error) { + if platform == "windows" { zr, err := zip.NewReader(source, int64(source.Len())) if err != nil { return nil, err @@ -191,7 +191,7 @@ func extractBinary(source *bytes.Reader, os string) (binary io.ReadCloser, err e if err != nil { return nil, err } - tmpfile, err := ioutil.TempFile("", "temp-exercism") + tmpfile, err := os.CreateTemp("", "temp-exercism") if err != nil { return nil, err } diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index fee08cd98..782be18d2 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -2,7 +2,6 @@ package cmd import ( "io" - "io/ioutil" "os" "testing" @@ -63,7 +62,7 @@ type CommandTest struct { // The method takes a *testing.T as an argument, that way the method can // fail the test if the creation of the temporary directory fails. func (test *CommandTest) Setup(t *testing.T) { - dir, err := ioutil.TempDir("", "command-test") + dir, err := os.MkdirTemp("", "command-test") defer os.RemoveAll(dir) assert.NoError(t, err) @@ -104,8 +103,8 @@ func newCapturedOutput() capturedOutput { return capturedOutput{ oldOut: Out, oldErr: Err, - newOut: ioutil.Discard, - newErr: ioutil.Discard, + newOut: io.Discard, + newErr: io.Discard, } } diff --git a/cmd/configure_test.go b/cmd/configure_test.go index 7a0f28eac..df3542328 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -1,11 +1,9 @@ //go:build !windows -// +build !windows package cmd import ( "bytes" - "io/ioutil" "net/http" "net/http/httptest" "os" @@ -344,7 +342,7 @@ func TestConfigureDefaultWorkspaceWithoutClobbering(t *testing.T) { ts := httptest.NewServer(endpoint) defer ts.Close() - tmpDir, err := ioutil.TempDir("", "no-clobber") + tmpDir, err := os.MkdirTemp("", "no-clobber") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -379,7 +377,7 @@ func TestConfigureExplicitWorkspaceWithoutClobberingNonDirectory(t *testing.T) { co.override() defer co.reset() - tmpDir, err := ioutil.TempDir("", "no-clobber") + tmpDir, err := os.MkdirTemp("", "no-clobber") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -396,7 +394,7 @@ func TestConfigureExplicitWorkspaceWithoutClobberingNonDirectory(t *testing.T) { } // Create a file at the workspace directory's location - err = ioutil.WriteFile(filepath.Join(tmpDir, "workspace"), []byte("This is not a directory"), os.FileMode(0755)) + err = os.WriteFile(filepath.Join(tmpDir, "workspace"), []byte("This is not a directory"), os.FileMode(0755)) assert.NoError(t, err) flags := pflag.NewFlagSet("fake", pflag.PanicOnError) diff --git a/cmd/download.go b/cmd/download.go index 7bf278bff..f376e959b 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" netURL "net/url" "os" @@ -202,8 +201,8 @@ func newDownload(flags *pflag.FlagSet, usrCfg *viper.Viper) (*download, error) { return nil, decodedAPIError(res) } - body, _ := ioutil.ReadAll(res.Body) - res.Body = ioutil.NopCloser(bytes.NewReader(body)) + body, _ := io.ReadAll(res.Body) + res.Body = io.NopCloser(bytes.NewReader(body)) if err := json.Unmarshal(body, &d.payload); err != nil { return nil, decodedAPIError(res) diff --git a/cmd/download_test.go b/cmd/download_test.go index 676d90518..d95729719 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -3,7 +3,6 @@ package cmd import ( "encoding/json" "fmt" - "io/ioutil" "net/http" "net/http/httptest" "os" @@ -169,7 +168,7 @@ func TestDownload(t *testing.T) { } for _, tc := range testCases { - tmpDir, err := ioutil.TempDir("", "download-cmd") + tmpDir, err := os.MkdirTemp("", "download-cmd") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -197,7 +196,7 @@ func TestDownload(t *testing.T) { assertDownloadedCorrectFiles(t, targetDir) dir := filepath.Join(targetDir, "bogus-track", "bogus-exercise") - b, err := ioutil.ReadFile(workspace.NewExerciseFromDir(dir).MetadataFilepath()) + b, err := os.ReadFile(workspace.NewExerciseFromDir(dir).MetadataFilepath()) assert.NoError(t, err) var metadata workspace.ExerciseMetadata err = json.Unmarshal(b, &metadata) @@ -229,7 +228,7 @@ func TestDownloadToExistingDirectory(t *testing.T) { } for _, tc := range testCases { - tmpDir, err := ioutil.TempDir("", "download-cmd") + tmpDir, err := os.MkdirTemp("", "download-cmd") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -281,7 +280,7 @@ func TestDownloadToExistingDirectoryWithForce(t *testing.T) { } for _, tc := range testCases { - tmpDir, err := ioutil.TempDir("", "download-cmd") + tmpDir, err := os.MkdirTemp("", "download-cmd") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -363,7 +362,7 @@ func assertDownloadedCorrectFiles(t *testing.T, targetDir string) { for _, file := range expectedFiles { t.Run(file.desc, func(t *testing.T) { - b, err := ioutil.ReadFile(file.path) + b, err := os.ReadFile(file.path) assert.NoError(t, err) assert.Equal(t, file.contents, string(b)) }) @@ -383,7 +382,7 @@ func TestDownloadError(t *testing.T) { ts := httptest.NewServer(handler) defer ts.Close() - tmpDir, err := ioutil.TempDir("", "submit-err-tmp-dir") + tmpDir, err := os.MkdirTemp("", "submit-err-tmp-dir") defer os.RemoveAll(tmpDir) assert.NoError(t, err) diff --git a/cmd/submit_symlink_test.go b/cmd/submit_symlink_test.go index e10fb4890..093895fb3 100644 --- a/cmd/submit_symlink_test.go +++ b/cmd/submit_symlink_test.go @@ -1,10 +1,8 @@ //go:build !windows -// +build !windows package cmd import ( - "io/ioutil" "os" "path/filepath" "testing" @@ -25,12 +23,12 @@ func TestSubmitFilesInSymlinkedPath(t *testing.T) { ts := fakeSubmitServer(t, submittedFiles) defer ts.Close() - tmpDir, err := ioutil.TempDir("", "symlink-destination") + tmpDir, err := os.MkdirTemp("", "symlink-destination") defer os.RemoveAll(tmpDir) assert.NoError(t, err) dstDir := filepath.Join(tmpDir, "workspace") - srcDir, err := ioutil.TempDir("", "symlink-source") + srcDir, err := os.MkdirTemp("", "symlink-source") defer os.RemoveAll(srcDir) assert.NoError(t, err) @@ -53,7 +51,7 @@ func TestSubmitFilesInSymlinkedPath(t *testing.T) { } file := filepath.Join(dir, "file.txt") - err = ioutil.WriteFile(filepath.Join(dir, "file.txt"), []byte("This is a file."), os.FileMode(0755)) + err = os.WriteFile(filepath.Join(dir, "file.txt"), []byte("This is a file."), os.FileMode(0755)) assert.NoError(t, err) err = runSubmit(cfg, pflag.NewFlagSet("symlinks", pflag.PanicOnError), []string{file}) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index ea16b6543..2a12d0bca 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/json" "fmt" - "io/ioutil" + "io" "mime" "net/http" "net/http/httptest" @@ -49,7 +49,7 @@ func TestSubmitWithoutWorkspace(t *testing.T) { } func TestSubmitNonExistentFile(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "submit-no-such-file") + tmpDir, err := os.MkdirTemp("", "submit-no-such-file") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -64,10 +64,10 @@ func TestSubmitNonExistentFile(t *testing.T) { DefaultBaseURL: "http://example.com", } - err = ioutil.WriteFile(filepath.Join(tmpDir, "file-1.txt"), []byte("This is file 1"), os.FileMode(0755)) + err = os.WriteFile(filepath.Join(tmpDir, "file-1.txt"), []byte("This is file 1"), os.FileMode(0755)) assert.NoError(t, err) - err = ioutil.WriteFile(filepath.Join(tmpDir, "file-2.txt"), []byte("This is file 2"), os.FileMode(0755)) + err = os.WriteFile(filepath.Join(tmpDir, "file-2.txt"), []byte("This is file 2"), os.FileMode(0755)) assert.NoError(t, err) files := []string{ filepath.Join(tmpDir, "file-1.txt"), @@ -81,7 +81,7 @@ func TestSubmitNonExistentFile(t *testing.T) { } func TestSubmitExerciseWithoutMetadataFile(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "no-metadata-file") + tmpDir, err := os.MkdirTemp("", "no-metadata-file") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -89,7 +89,7 @@ func TestSubmitExerciseWithoutMetadataFile(t *testing.T) { os.MkdirAll(dir, os.FileMode(0755)) file := filepath.Join(dir, "file.txt") - err = ioutil.WriteFile(file, []byte("This is a file."), os.FileMode(0755)) + err = os.WriteFile(file, []byte("This is a file."), os.FileMode(0755)) assert.NoError(t, err) v := viper.New() @@ -111,7 +111,7 @@ func TestSubmitExerciseWithoutMetadataFile(t *testing.T) { func TestGetExerciseSolutionFiles(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "dir-with-no-metadata") + tmpDir, err := os.MkdirTemp("", "dir-with-no-metadata") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -120,7 +120,7 @@ func TestGetExerciseSolutionFiles(t *testing.T) { assert.Regexp(t, "no files to submit", err.Error()) } - validTmpDir, err := ioutil.TempDir("", "dir-with-valid-metadata") + validTmpDir, err := os.MkdirTemp("", "dir-with-valid-metadata") defer os.RemoveAll(validTmpDir) assert.NoError(t, err) @@ -128,7 +128,7 @@ func TestGetExerciseSolutionFiles(t *testing.T) { err = os.MkdirAll(metadataDir, os.FileMode(0755)) assert.NoError(t, err) - err = ioutil.WriteFile( + err = os.WriteFile( filepath.Join(metadataDir, "config.json"), []byte(` { @@ -149,7 +149,7 @@ func TestGetExerciseSolutionFiles(t *testing.T) { } func TestSubmitFilesAndDir(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "submit-no-such-file") + tmpDir, err := os.MkdirTemp("", "submit-no-such-file") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -164,10 +164,10 @@ func TestSubmitFilesAndDir(t *testing.T) { DefaultBaseURL: "http://example.com", } - err = ioutil.WriteFile(filepath.Join(tmpDir, "file-1.txt"), []byte("This is file 1"), os.FileMode(0755)) + err = os.WriteFile(filepath.Join(tmpDir, "file-1.txt"), []byte("This is file 1"), os.FileMode(0755)) assert.NoError(t, err) - err = ioutil.WriteFile(filepath.Join(tmpDir, "file-2.txt"), []byte("This is file 2"), os.FileMode(0755)) + err = os.WriteFile(filepath.Join(tmpDir, "file-2.txt"), []byte("This is file 2"), os.FileMode(0755)) assert.NoError(t, err) files := []string{ filepath.Join(tmpDir, "file-1.txt"), @@ -192,7 +192,7 @@ func TestDuplicateFiles(t *testing.T) { ts := fakeSubmitServer(t, submittedFiles) defer ts.Close() - tmpDir, err := ioutil.TempDir("", "duplicate-files") + tmpDir, err := os.MkdirTemp("", "duplicate-files") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -212,7 +212,7 @@ func TestDuplicateFiles(t *testing.T) { } file1 := filepath.Join(dir, "file-1.txt") - err = ioutil.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) + err = os.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file1, file1}) assert.NoError(t, err) @@ -232,7 +232,7 @@ func TestSubmitFiles(t *testing.T) { ts := fakeSubmitServer(t, submittedFiles) defer ts.Close() - tmpDir, err := ioutil.TempDir("", "submit-files") + tmpDir, err := os.MkdirTemp("", "submit-files") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -241,16 +241,16 @@ func TestSubmitFiles(t *testing.T) { writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") file1 := filepath.Join(dir, "file-1.txt") - err = ioutil.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) + err = os.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) assert.NoError(t, err) file2 := filepath.Join(dir, "subdir", "file-2.txt") - err = ioutil.WriteFile(file2, []byte("This is file 2."), os.FileMode(0755)) + err = os.WriteFile(file2, []byte("This is file 2."), os.FileMode(0755)) assert.NoError(t, err) // We don't filter *.md files if you explicitly pass the file path. readme := filepath.Join(dir, "README.md") - err = ioutil.WriteFile(readme, []byte("This is the readme."), os.FileMode(0755)) + err = os.WriteFile(readme, []byte("This is the readme."), os.FileMode(0755)) assert.NoError(t, err) v := viper.New() @@ -289,7 +289,7 @@ func TestLegacyMetadataMigration(t *testing.T) { ts := fakeSubmitServer(t, submittedFiles) defer ts.Close() - tmpDir, err := ioutil.TempDir("", "legacy-metadata-file") + tmpDir, err := os.MkdirTemp("", "legacy-metadata-file") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -306,11 +306,11 @@ func TestLegacyMetadataMigration(t *testing.T) { b, err := json.Marshal(metadata) assert.NoError(t, err) exercise := workspace.NewExerciseFromDir(dir) - err = ioutil.WriteFile(exercise.LegacyMetadataFilepath(), b, os.FileMode(0600)) + err = os.WriteFile(exercise.LegacyMetadataFilepath(), b, os.FileMode(0600)) assert.NoError(t, err) file := filepath.Join(dir, "file.txt") - err = ioutil.WriteFile(file, []byte("This is a file."), os.FileMode(0755)) + err = os.WriteFile(file, []byte("This is a file."), os.FileMode(0755)) assert.NoError(t, err) v := viper.New() @@ -352,7 +352,7 @@ func TestSubmitWithEmptyFile(t *testing.T) { ts := fakeSubmitServer(t, submittedFiles) defer ts.Close() - tmpDir, err := ioutil.TempDir("", "empty-file") + tmpDir, err := os.MkdirTemp("", "empty-file") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -372,9 +372,9 @@ func TestSubmitWithEmptyFile(t *testing.T) { } file1 := filepath.Join(dir, "file-1.txt") - err = ioutil.WriteFile(file1, []byte(""), os.FileMode(0755)) + err = os.WriteFile(file1, []byte(""), os.FileMode(0755)) file2 := filepath.Join(dir, "file-2.txt") - err = ioutil.WriteFile(file2, []byte("This is file 2."), os.FileMode(0755)) + err = os.WriteFile(file2, []byte("This is file 2."), os.FileMode(0755)) err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file1, file2}) assert.NoError(t, err) @@ -393,7 +393,7 @@ func TestSubmitWithEnormousFile(t *testing.T) { ts := fakeSubmitServer(t, submittedFiles) defer ts.Close() - tmpDir, err := ioutil.TempDir("", "enormous-file") + tmpDir, err := os.MkdirTemp("", "enormous-file") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -413,7 +413,7 @@ func TestSubmitWithEnormousFile(t *testing.T) { } file := filepath.Join(dir, "file.txt") - err = ioutil.WriteFile(file, make([]byte, 65535), os.FileMode(0755)) + err = os.WriteFile(file, make([]byte, 65535), os.FileMode(0755)) if err != nil { t.Fatal(err) } @@ -435,7 +435,7 @@ func TestSubmitFilesForTeamExercise(t *testing.T) { ts := fakeSubmitServer(t, submittedFiles) defer ts.Close() - tmpDir, err := ioutil.TempDir("", "submit-files") + tmpDir, err := os.MkdirTemp("", "submit-files") assert.NoError(t, err) dir := filepath.Join(tmpDir, "teams", "bogus-team", "bogus-track", "bogus-exercise") @@ -443,11 +443,11 @@ func TestSubmitFilesForTeamExercise(t *testing.T) { writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") file1 := filepath.Join(dir, "file-1.txt") - err = ioutil.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) + err = os.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) assert.NoError(t, err) file2 := filepath.Join(dir, "subdir", "file-2.txt") - err = ioutil.WriteFile(file2, []byte("This is file 2."), os.FileMode(0755)) + err = os.WriteFile(file2, []byte("This is file 2."), os.FileMode(0755)) assert.NoError(t, err) v := viper.New() @@ -477,7 +477,7 @@ func TestSubmitOnlyEmptyFile(t *testing.T) { co.override() defer co.reset() - tmpDir, err := ioutil.TempDir("", "just-an-empty-file") + tmpDir, err := os.MkdirTemp("", "just-an-empty-file") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -497,7 +497,7 @@ func TestSubmitOnlyEmptyFile(t *testing.T) { } file := filepath.Join(dir, "file.txt") - err = ioutil.WriteFile(file, []byte(""), os.FileMode(0755)) + err = os.WriteFile(file, []byte(""), os.FileMode(0755)) err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{file}) if assert.Error(t, err) { @@ -506,7 +506,7 @@ func TestSubmitOnlyEmptyFile(t *testing.T) { } func TestSubmitFilesFromDifferentSolutions(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "dir-1-submit") + tmpDir, err := os.MkdirTemp("", "dir-1-submit") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -519,11 +519,11 @@ func TestSubmitFilesFromDifferentSolutions(t *testing.T) { writeFakeMetadata(t, dir2, "bogus-track", "bogus-exercise-2") file1 := filepath.Join(dir1, "file-1.txt") - err = ioutil.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) + err = os.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) assert.NoError(t, err) file2 := filepath.Join(dir2, "file-2.txt") - err = ioutil.WriteFile(file2, []byte("This is file 2."), os.FileMode(0755)) + err = os.WriteFile(file2, []byte("This is file 2."), os.FileMode(0755)) assert.NoError(t, err) v := viper.New() @@ -558,7 +558,7 @@ func fakeSubmitServer(t *testing.T, submittedFiles map[string]string) *httptest. t.Fatal(err) } defer file.Close() - body, err := ioutil.ReadAll(file) + body, err := io.ReadAll(file) if err != nil { t.Fatal(err) } @@ -589,7 +589,7 @@ func TestSubmitRelativePath(t *testing.T) { ts := fakeSubmitServer(t, submittedFiles) defer ts.Close() - tmpDir, err := ioutil.TempDir("", "relative-path") + tmpDir, err := os.MkdirTemp("", "relative-path") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -608,7 +608,7 @@ func TestSubmitRelativePath(t *testing.T) { UserViperConfig: v, } - err = ioutil.WriteFile(filepath.Join(dir, "file.txt"), []byte("This is a file."), os.FileMode(0755)) + err = os.WriteFile(filepath.Join(dir, "file.txt"), []byte("This is a file."), os.FileMode(0755)) err = os.Chdir(dir) assert.NoError(t, err) @@ -629,7 +629,7 @@ func TestSubmitServerErr(t *testing.T) { ts := httptest.NewServer(handler) defer ts.Close() - tmpDir, err := ioutil.TempDir("", "submit-err-tmp-dir") + tmpDir, err := os.MkdirTemp("", "submit-err-tmp-dir") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -648,7 +648,7 @@ func TestSubmitServerErr(t *testing.T) { os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") - err = ioutil.WriteFile(filepath.Join(dir, "file-1.txt"), []byte("This is file 1"), os.FileMode(0755)) + err = os.WriteFile(filepath.Join(dir, "file-1.txt"), []byte("This is file 1"), os.FileMode(0755)) assert.NoError(t, err) files := []string{ @@ -668,7 +668,7 @@ func TestHandleErrorResponse(t *testing.T) { ts := httptest.NewServer(handler) defer ts.Close() - tmpDir, err := ioutil.TempDir("", "submit-nonsuccess") + tmpDir, err := os.MkdirTemp("", "submit-nonsuccess") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -687,7 +687,7 @@ func TestHandleErrorResponse(t *testing.T) { os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") - err = ioutil.WriteFile(filepath.Join(dir, "file-1.txt"), []byte("This is file 1"), os.FileMode(0755)) + err = os.WriteFile(filepath.Join(dir, "file-1.txt"), []byte("This is file 1"), os.FileMode(0755)) assert.NoError(t, err) files := []string{ @@ -703,7 +703,7 @@ func TestSubmissionNotConnectedToRequesterAccount(t *testing.T) { ts := fakeSubmitServer(t, submittedFiles) defer ts.Close() - tmpDir, err := ioutil.TempDir("", "submit-files") + tmpDir, err := os.MkdirTemp("", "submit-files") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -721,7 +721,7 @@ func TestSubmissionNotConnectedToRequesterAccount(t *testing.T) { assert.NoError(t, err) file1 := filepath.Join(dir, "file-1.txt") - err = ioutil.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) + err = os.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) assert.NoError(t, err) v := viper.New() @@ -746,7 +746,7 @@ func TestExerciseDirnameMatchesMetadataSlug(t *testing.T) { ts := fakeSubmitServer(t, submittedFiles) defer ts.Close() - tmpDir, err := ioutil.TempDir("", "submit-files") + tmpDir, err := os.MkdirTemp("", "submit-files") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -755,7 +755,7 @@ func TestExerciseDirnameMatchesMetadataSlug(t *testing.T) { writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") file1 := filepath.Join(dir, "file-1.txt") - err = ioutil.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) + err = os.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) assert.NoError(t, err) v := viper.New() diff --git a/config/config_notwin_test.go b/config/config_notwin_test.go index 31ddf77f0..33912cc9b 100644 --- a/config/config_notwin_test.go +++ b/config/config_notwin_test.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows package config diff --git a/config/config_windows_test.go b/config/config_windows_test.go index da676739f..5757b783f 100644 --- a/config/config_windows_test.go +++ b/config/config_windows_test.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package config diff --git a/config/resolve_notwin_test.go b/config/resolve_notwin_test.go index eea7ae440..fb70d5f69 100644 --- a/config/resolve_notwin_test.go +++ b/config/resolve_notwin_test.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows package config diff --git a/debug/debug.go b/debug/debug.go index 133681439..a96d1f3bd 100644 --- a/debug/debug.go +++ b/debug/debug.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "log" "net/http" "net/http/httputil" @@ -42,7 +41,7 @@ func DumpRequest(req *http.Request) { var bodyCopy bytes.Buffer body := io.TeeReader(req.Body, &bodyCopy) - req.Body = ioutil.NopCloser(body) + req.Body = io.NopCloser(body) authHeader := req.Header.Get("Authorization") @@ -63,7 +62,7 @@ func DumpRequest(req *http.Request) { Println("") req.Header.Set("Authorization", authHeader) - req.Body = ioutil.NopCloser(&bodyCopy) + req.Body = io.NopCloser(&bodyCopy) } // DumpResponse dumps out the provided http.Response @@ -74,7 +73,7 @@ func DumpResponse(res *http.Response) { var bodyCopy bytes.Buffer body := io.TeeReader(res.Body, &bodyCopy) - res.Body = ioutil.NopCloser(body) + res.Body = io.NopCloser(body) dump, err := httputil.DumpResponse(res, res.ContentLength > 0) if err != nil { @@ -86,7 +85,7 @@ func DumpResponse(res *http.Response) { Println("========================= END DumpResponse =========================") Println("") - res.Body = ioutil.NopCloser(body) + res.Body = io.NopCloser(body) } // Redact masks the given token by replacing part of the string with * diff --git a/go.mod b/go.mod index 91e79ff1a..c6a4a02a2 100644 --- a/go.mod +++ b/go.mod @@ -1,28 +1,33 @@ module github.com/exercism/cli -go 1.15 +go 1.20 require ( github.com/blang/semver v3.5.1+incompatible - github.com/davecgh/go-spew v1.1.0 - github.com/fsnotify/fsnotify v1.4.2 - github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf - github.com/inconshreveable/mousetrap v1.0.0 - github.com/magiconair/properties v1.7.3 - github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992 - github.com/pelletier/go-buffruneio v0.2.0 - github.com/pelletier/go-toml v1.0.0 - github.com/pmezard/go-difflib v1.0.0 - github.com/spf13/afero v0.0.0-20170217164146-9be650865eab - github.com/spf13/cast v1.1.0 - github.com/spf13/cobra v0.0.0-20170731170427-b26b538f6930 - github.com/spf13/jwalterweatherman v0.0.0-20170523133247-0efa5202c046 - github.com/spf13/pflag v1.0.0 - github.com/spf13/viper v0.0.0-20180507071007-15738813a09d - github.com/stretchr/testify v1.1.4 - golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7 - golang.org/x/sys v0.0.0-20201202213521-69691e467435 - golang.org/x/text v0.0.0-20170730040918-3bd178b88a81 - gopkg.in/yaml.v2 v2.0.0-20170721122051-25c4ec802a7d + github.com/spf13/cobra v1.7.0 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.15.0 + github.com/stretchr/testify v1.8.4 + golang.org/x/net v0.4.0 + golang.org/x/text v0.5.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/afero v1.9.3 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + golang.org/x/sys v0.3.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7d53fd399..bc3b4c47c 100644 --- a/go.sum +++ b/go.sum @@ -1,46 +1,492 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fsnotify/fsnotify v1.4.2 h1:v5tKwtf2hNhBV24eNYfQ5UmvFOGlOCmRqk7/P1olxtk= -github.com/fsnotify/fsnotify v1.4.2/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e h1:KJWs1uTCkN3E/J5ofCH9Pf8KKsibTFc3fv0CA9+WsVo= -github.com/hashicorp/hcl v0.0.0-20170509225359-392dba7d905e/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/magiconair/properties v1.7.3 h1:6AOjgCKyZFMG/1yfReDPDz3CJZPxnYk7DGmj2HtyF24= -github.com/magiconair/properties v1.7.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992 h1:W7VHAEVflA5/eTyRvQ53Lz5j8bhRd1myHZlI/IZFvbU= -github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA= -github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= -github.com/pelletier/go-toml v1.0.0 h1:QFDlmAXZrfPXEF6c9+15fMqhQIS3O0pxszhnk936vg4= -github.com/pelletier/go-toml v1.0.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= +github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/spf13/afero v0.0.0-20170217164146-9be650865eab h1:IVAbBHQR8rXL2Fc8Zba/lMF7KOnTi70lqdx91UTuAwQ= -github.com/spf13/afero v0.0.0-20170217164146-9be650865eab/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.1.0 h1:0Rhw4d6C8J9VPu6cjZLIhZ8+aAOHcDvGeKn+cq5Aq3k= -github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= -github.com/spf13/cobra v0.0.0-20170731170427-b26b538f6930 h1:uJND9FKkf5s8kdTQX1jDygtp/zV4BJQpYvOmXPCYWgc= -github.com/spf13/cobra v0.0.0-20170731170427-b26b538f6930/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/jwalterweatherman v0.0.0-20170523133247-0efa5202c046 h1:RpxSq53NruItMGgp6q5MsDYoZynisJgEpisQdWJ7PyM= -github.com/spf13/jwalterweatherman v0.0.0-20170523133247-0efa5202c046/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.0 h1:oaPbdDe/x0UncahuwiPxW1GYJyilRAdsPnq3e1yaPcI= -github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v0.0.0-20180507071007-15738813a09d h1:pIz+bbPLk78K39d3u77IlNpJvpS/f0ao8n3sdy82eCs= -github.com/spf13/viper v0.0.0-20180507071007-15738813a09d/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= -github.com/stretchr/testify v1.1.4 h1:ToftOQTytwshuOSj6bDSolVUa3GINfJP/fg3OkkOzQQ= -github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7 h1:1Pw+ZX4dmGORIwGkTwnUr7RFuMhfpCYHXRZNF04XPYs= -golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/sys v0.0.0-20170803140359-d8f5ea21b929 h1:M4VPQYSW/nB4Bcg1XMD4yW2sprnwerD3Kb6apRphtZw= -golang.org/x/sys v0.0.0-20170803140359-d8f5ea21b929/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201202213521-69691e467435 h1:25AvDqqB9PrNqj1FLf2/70I4W0L19qqoaFq3gjNwbKk= -golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.0.0-20170730040918-3bd178b88a81 h1:7aXI3TQ9sZ4JdDoIDGjxL6G2mQxlsPy9dySnJaL6Bdk= -golang.org/x/text v0.0.0-20170730040918-3bd178b88a81/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -gopkg.in/yaml.v2 v2.0.0-20170721122051-25c4ec802a7d h1:2DX7x6HUDGZUyuEDAhUsQQNqkb1zvDyKTjVoTdzaEzo= -gopkg.in/yaml.v2 v2.0.0-20170721122051-25c4ec802a7d/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= +github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= +github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/workspace/document_test.go b/workspace/document_test.go index 4e055df2f..50013ee73 100644 --- a/workspace/document_test.go +++ b/workspace/document_test.go @@ -1,7 +1,6 @@ package workspace import ( - "io/ioutil" "os" "path/filepath" "testing" @@ -10,7 +9,7 @@ import ( ) func TestNormalizedDocumentPath(t *testing.T) { - root, err := ioutil.TempDir("", "docpath") + root, err := os.MkdirTemp("", "docpath") assert.NoError(t, err) defer os.RemoveAll(root) @@ -32,7 +31,7 @@ func TestNormalizedDocumentPath(t *testing.T) { } for _, tc := range testCases { - err = ioutil.WriteFile(tc.filepath, []byte("a file"), os.FileMode(0600)) + err = os.WriteFile(tc.filepath, []byte("a file"), os.FileMode(0600)) assert.NoError(t, err) doc, err := NewDocument(root, tc.filepath) diff --git a/workspace/exercise_config.go b/workspace/exercise_config.go index 9b0164ebb..35ae7545c 100644 --- a/workspace/exercise_config.go +++ b/workspace/exercise_config.go @@ -3,7 +3,7 @@ package workspace import ( "encoding/json" "errors" - "io/ioutil" + "os" "path/filepath" ) @@ -22,7 +22,7 @@ type ExerciseConfig struct { // NewExerciseConfig reads exercise metadata from a file in the given directory. func NewExerciseConfig(dir string) (*ExerciseConfig, error) { - b, err := ioutil.ReadFile(filepath.Join(dir, configFilepath)) + b, err := os.ReadFile(filepath.Join(dir, configFilepath)) if err != nil { return nil, err } diff --git a/workspace/exercise_config_test.go b/workspace/exercise_config_test.go index d07264c0d..5d9847453 100644 --- a/workspace/exercise_config_test.go +++ b/workspace/exercise_config_test.go @@ -1,7 +1,6 @@ package workspace import ( - "io/ioutil" "os" "path/filepath" "strings" @@ -11,7 +10,7 @@ import ( ) func TestExerciseConfig(t *testing.T) { - dir, err := ioutil.TempDir("", "exercise_config") + dir, err := os.MkdirTemp("", "exercise_config") assert.NoError(t, err) defer os.RemoveAll(dir) @@ -40,7 +39,7 @@ func TestExerciseConfig(t *testing.T) { } func TestExerciseConfigNoTestKey(t *testing.T) { - dir, err := ioutil.TempDir("", "exercise_config") + dir, err := os.MkdirTemp("", "exercise_config") assert.NoError(t, err) defer os.RemoveAll(dir) @@ -64,7 +63,7 @@ func TestExerciseConfigNoTestKey(t *testing.T) { } func TestMissingExerciseConfig(t *testing.T) { - dir, err := ioutil.TempDir("", "exercise_config") + dir, err := os.MkdirTemp("", "exercise_config") assert.NoError(t, err) defer os.RemoveAll(dir) @@ -77,7 +76,7 @@ func TestMissingExerciseConfig(t *testing.T) { } func TestInvalidExerciseConfig(t *testing.T) { - dir, err := ioutil.TempDir("", "exercise_config") + dir, err := os.MkdirTemp("", "exercise_config") assert.NoError(t, err) defer os.RemoveAll(dir) diff --git a/workspace/exercise_metadata.go b/workspace/exercise_metadata.go index 0a0305576..729174258 100644 --- a/workspace/exercise_metadata.go +++ b/workspace/exercise_metadata.go @@ -3,7 +3,6 @@ package workspace import ( "encoding/json" "fmt" - "io/ioutil" "os" "path/filepath" "strings" @@ -32,7 +31,7 @@ type ExerciseMetadata struct { // NewExerciseMetadata reads exercise metadata from a file in the given directory. func NewExerciseMetadata(dir string) (*ExerciseMetadata, error) { - b, err := ioutil.ReadFile(filepath.Join(dir, metadataFilepath)) + b, err := os.ReadFile(filepath.Join(dir, metadataFilepath)) if err != nil { return nil, err } @@ -72,7 +71,7 @@ func (em *ExerciseMetadata) Write(dir string) error { if err = os.MkdirAll(filepath.Dir(metadataAbsoluteFilepath), os.FileMode(0755)); err != nil { return err } - if err = ioutil.WriteFile(metadataAbsoluteFilepath, b, os.FileMode(0600)); err != nil { + if err = os.WriteFile(metadataAbsoluteFilepath, b, os.FileMode(0600)); err != nil { return err } em.Dir = dir diff --git a/workspace/exercise_metadata_test.go b/workspace/exercise_metadata_test.go index e05ffeac3..0c3c7afa7 100644 --- a/workspace/exercise_metadata_test.go +++ b/workspace/exercise_metadata_test.go @@ -1,7 +1,6 @@ package workspace import ( - "io/ioutil" "os" "testing" "time" @@ -10,7 +9,7 @@ import ( ) func TestExerciseMetadata(t *testing.T) { - dir, err := ioutil.TempDir("", "solution") + dir, err := os.MkdirTemp("", "solution") assert.NoError(t, err) defer os.RemoveAll(dir) diff --git a/workspace/exercise_test.go b/workspace/exercise_test.go index 0a6d1d8fa..7dd15c583 100644 --- a/workspace/exercise_test.go +++ b/workspace/exercise_test.go @@ -1,7 +1,6 @@ package workspace import ( - "io/ioutil" "os" "path/filepath" "testing" @@ -10,7 +9,7 @@ import ( ) func TestHasMetadata(t *testing.T) { - ws, err := ioutil.TempDir("", "fake-workspace") + ws, err := os.MkdirTemp("", "fake-workspace") defer os.RemoveAll(ws) assert.NoError(t, err) @@ -22,7 +21,7 @@ func TestHasMetadata(t *testing.T) { err = os.MkdirAll(filepath.Dir(exerciseB.MetadataFilepath()), os.FileMode(0755)) assert.NoError(t, err) - err = ioutil.WriteFile(exerciseA.MetadataFilepath(), []byte{}, os.FileMode(0600)) + err = os.WriteFile(exerciseA.MetadataFilepath(), []byte{}, os.FileMode(0600)) assert.NoError(t, err) ok, err := exerciseA.HasMetadata() @@ -35,7 +34,7 @@ func TestHasMetadata(t *testing.T) { } func TestHasLegacyMetadata(t *testing.T) { - ws, err := ioutil.TempDir("", "fake-workspace") + ws, err := os.MkdirTemp("", "fake-workspace") defer os.RemoveAll(ws) assert.NoError(t, err) @@ -47,7 +46,7 @@ func TestHasLegacyMetadata(t *testing.T) { err = os.MkdirAll(filepath.Dir(exerciseB.LegacyMetadataFilepath()), os.FileMode(0755)) assert.NoError(t, err) - err = ioutil.WriteFile(exerciseA.LegacyMetadataFilepath(), []byte{}, os.FileMode(0600)) + err = os.WriteFile(exerciseA.LegacyMetadataFilepath(), []byte{}, os.FileMode(0600)) assert.NoError(t, err) ok, err := exerciseA.HasLegacyMetadata() @@ -76,7 +75,7 @@ func TestMigrationStatusString(t *testing.T) { } func TestMigrateLegacyMetadataFileWithoutLegacy(t *testing.T) { - ws, err := ioutil.TempDir("", "fake-workspace") + ws, err := os.MkdirTemp("", "fake-workspace") defer os.RemoveAll(ws) assert.NoError(t, err) @@ -85,7 +84,7 @@ func TestMigrateLegacyMetadataFileWithoutLegacy(t *testing.T) { err = os.MkdirAll(filepath.Dir(metadataFilepath), os.FileMode(0755)) assert.NoError(t, err) - err = ioutil.WriteFile(metadataFilepath, []byte{}, os.FileMode(0600)) + err = os.WriteFile(metadataFilepath, []byte{}, os.FileMode(0600)) assert.NoError(t, err) ok, _ := exercise.HasLegacyMetadata() @@ -104,7 +103,7 @@ func TestMigrateLegacyMetadataFileWithoutLegacy(t *testing.T) { } func TestMigrateLegacyMetadataFileWithLegacy(t *testing.T) { - ws, err := ioutil.TempDir("", "fake-workspace") + ws, err := os.MkdirTemp("", "fake-workspace") defer os.RemoveAll(ws) assert.NoError(t, err) @@ -113,7 +112,7 @@ func TestMigrateLegacyMetadataFileWithLegacy(t *testing.T) { err = os.MkdirAll(filepath.Dir(legacyMetadataFilepath), os.FileMode(0755)) assert.NoError(t, err) - err = ioutil.WriteFile(legacyMetadataFilepath, []byte{}, os.FileMode(0600)) + err = os.WriteFile(legacyMetadataFilepath, []byte{}, os.FileMode(0600)) assert.NoError(t, err) ok, _ := exercise.HasLegacyMetadata() @@ -132,7 +131,7 @@ func TestMigrateLegacyMetadataFileWithLegacy(t *testing.T) { } func TestMigrateLegacyMetadataFileWithLegacyAndModern(t *testing.T) { - ws, err := ioutil.TempDir("", "fake-workspace") + ws, err := os.MkdirTemp("", "fake-workspace") defer os.RemoveAll(ws) assert.NoError(t, err) @@ -144,9 +143,9 @@ func TestMigrateLegacyMetadataFileWithLegacyAndModern(t *testing.T) { err = os.MkdirAll(filepath.Dir(metadataFilepath), os.FileMode(0755)) assert.NoError(t, err) - err = ioutil.WriteFile(legacyMetadataFilepath, []byte{}, os.FileMode(0600)) + err = os.WriteFile(legacyMetadataFilepath, []byte{}, os.FileMode(0600)) assert.NoError(t, err) - err = ioutil.WriteFile(metadataFilepath, []byte{}, os.FileMode(0600)) + err = os.WriteFile(metadataFilepath, []byte{}, os.FileMode(0600)) assert.NoError(t, err) ok, _ := exercise.HasLegacyMetadata() diff --git a/workspace/path_type_symlinks_test.go b/workspace/path_type_symlinks_test.go index ace7b741d..a80b6171a 100644 --- a/workspace/path_type_symlinks_test.go +++ b/workspace/path_type_symlinks_test.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows package workspace diff --git a/workspace/workspace.go b/workspace/workspace.go index eb9875e8a..0011d4be2 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -3,7 +3,6 @@ package workspace import ( "errors" "fmt" - "io/ioutil" "os" "path/filepath" "runtime" @@ -46,7 +45,7 @@ func New(dir string) (Workspace, error) { func (ws Workspace) PotentialExercises() ([]Exercise, error) { exercises := []Exercise{} - topInfos, err := ioutil.ReadDir(ws.Dir) + topInfos, err := os.ReadDir(ws.Dir) if err != nil { return nil, err } @@ -60,7 +59,7 @@ func (ws Workspace) PotentialExercises() ([]Exercise, error) { } if topInfo.Name() == "teams" { - subInfos, err := ioutil.ReadDir(filepath.Join(ws.Dir, "teams")) + subInfos, err := os.ReadDir(filepath.Join(ws.Dir, "teams")) if err != nil { return nil, err } @@ -81,7 +80,7 @@ func (ws Workspace) PotentialExercises() ([]Exercise, error) { continue } - subInfos, err := ioutil.ReadDir(filepath.Join(ws.Dir, topInfo.Name())) + subInfos, err := os.ReadDir(filepath.Join(ws.Dir, topInfo.Name())) if err != nil { return nil, err } diff --git a/workspace/workspace_darwin_test.go b/workspace/workspace_darwin_test.go index 03794a795..47bcab7ec 100644 --- a/workspace/workspace_darwin_test.go +++ b/workspace/workspace_darwin_test.go @@ -2,11 +2,12 @@ package workspace import ( "fmt" - "github.com/stretchr/testify/assert" "path/filepath" "runtime" "strings" "testing" + + "github.com/stretchr/testify/assert" ) func TestExerciseDir_case_insensitive(t *testing.T) { diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go index 160c7cd0e..63053a5cd 100644 --- a/workspace/workspace_test.go +++ b/workspace/workspace_test.go @@ -1,7 +1,6 @@ package workspace import ( - "io/ioutil" "os" "path/filepath" "runtime" @@ -12,7 +11,7 @@ import ( ) func TestWorkspacePotentialExercises(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "walk") + tmpDir, err := os.MkdirTemp("", "walk") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -54,7 +53,7 @@ func TestWorkspacePotentialExercises(t *testing.T) { } func TestWorkspaceExercises(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "walk-with-metadata") + tmpDir, err := os.MkdirTemp("", "walk-with-metadata") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -69,7 +68,7 @@ func TestWorkspaceExercises(t *testing.T) { assert.NoError(t, err) if path != a2 { - err = ioutil.WriteFile(metadataAbsoluteFilepath, []byte{}, os.FileMode(0600)) + err = os.WriteFile(metadataAbsoluteFilepath, []byte{}, os.FileMode(0600)) assert.NoError(t, err) } } From 6e178aa57dc37967e950fefae72c9d4120fb5213 Mon Sep 17 00:00:00 2001 From: baduker Date: Thu, 23 Nov 2023 14:12:40 +0100 Subject: [PATCH 467/544] Setup goreleaser to update homebrew (#1121) --- .goreleaser.yml | 16 ++++++++++++++-- RELEASE.md | 6 ++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 8a5dccbd2..1fa59b225 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -120,5 +120,17 @@ release: # Default is `{{.Tag}}` name_template: "{{.ProjectName}}-v{{.Version}} {{.Env.USER}}" -# brews: -# We do not use the brew config, which is for taps, not core forumulas. +brews: + - + name: exercism + repository: + owner: exercism + name: homebrew-exercism + commit_author: + name: Exercism Bot + email: 66069679+exercism-bot@users.noreply.github.com + folder: Formula + homepage: "https://exercism.org/" + description: "Command-line tool to interact with exercism.io" + test: | + system "exercism version" diff --git a/RELEASE.md b/RELEASE.md index 566db9f83..87155f4e8 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -40,9 +40,11 @@ goreleaser --skip-publish --snapshot --clean # Create a new tag on the main branch and push it git tag -a "${TAG_NAME}" -m "Trying out GoReleaser" git push origin "${TAG_NAME}" - -# [TODO] Push to homebrew ``` +Brew tap is now managed by `.goreleaser.yml` so no need to update it manually. +GoReleaser can generate and publish a homebrew-tap recipe into a repository +automatically. See [GoReleaser docs](https://goreleaser.com/customization/homebrew/) +for more details. ## Cut Release on GitHub From 90b4b86e8009a992bd1fa34d374418b07383e840 Mon Sep 17 00:00:00 2001 From: Tomas Norre Mikkelsen Date: Fri, 5 Jan 2024 10:44:59 +0100 Subject: [PATCH 468/544] Update FAQ link in Upgrade output (#1124) * Update FAQ link in Upgrade output * Update FAQ link in Upgrade output - update after feedback --- cmd/upgrade.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/upgrade.go b/cmd/upgrade.go index 4b78d4b6f..e4ec04508 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -32,7 +32,7 @@ We were not able to upgrade the cli because we encountered an error: Please check the FAQ for solutions to common upgrading issues. -https://exercism.io/faqs`, err) +https://exercism.org/faqs`, err) } return nil }, From 331bc718ba974f5f5aafedbc789c26b7533e0ae8 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Tue, 9 Jan 2024 14:45:14 +0100 Subject: [PATCH 469/544] Simplify the root command description (#1125) --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index 734c8d4b7..35d266356 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -16,7 +16,7 @@ import ( var RootCmd = &cobra.Command{ Use: BinaryName, Short: "A friendly command-line interface to Exercism.", - Long: `A command-line interface for the v3 redesign of Exercism. + Long: `A command-line interface for Exercism. Download exercises and submit your solutions.`, SilenceUsage: true, From 4950e3cc4a7a85f3cb8da3f005602e3f6c39824f Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Wed, 14 Feb 2024 09:10:34 -0500 Subject: [PATCH 470/544] test: update 8th and emacs-lisp test commands (#1128) --- workspace/test_configurations.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index 32c6fbc3a..176729e1b 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -62,8 +62,7 @@ func (c *TestConfiguration) GetTestCommand() (string, error) { // some tracks aren't (or won't be) implemented; every track is listed either way var TestConfigurations = map[string]TestConfiguration{ "8th": { - Command: "bash tester.sh", - WindowsCommand: "tester.bat", + Command: "8th -f test.8th", }, // abap: tests are run via "ABAP Development Tools", not the CLI "awk": { @@ -117,7 +116,7 @@ var TestConfigurations = map[string]TestConfiguration{ Command: "elm-test", }, "emacs-lisp": { - Command: "emacs -batch -l ert -l *-test.el -f ert-run-tests-batch-and-exit", + Command: "emacs -batch -l ert -l {{test_files}} -f ert-run-tests-batch-and-exit", }, "erlang": { Command: "rebar3 eunit", From 32aeac6c28d426a5149145813d14a3085f84ad1f Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 15 Feb 2024 09:14:35 +0100 Subject: [PATCH 471/544] Bump version to v3.3.0 (#1129) --- CHANGELOG.md | 5 +++++ cmd/version.go | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1dcca2ff..9063d3e46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ## Next Release * **Your contribution here** +## v3.3.0 (2024-02-15) +* [#1128](https://github.com/exercism/cli/pull/1128) Fix `exercism test` command not working for the `8th` and `emacs-lisp` tracks - [@glennj] +* [#1125](https://github.com/exercism/cli/pull/1125) Simplify root command description +* [#1124](https://github.com/exercism/cli/pull/1124) Use correct domain for FAQ link [@tomasnorre] + ## v3.2.0 (2023-07-28) * [#1092](https://github.com/exercism/cli/pull/1092) Add `exercism test` command to run the unit tests for nearly any track (inspired by [universal-test-runner](https://github.com/xavdid/universal-test-runner)) - [@xavdid] * [#1073](https://github.com/exercism/cli/pull/1073) Add `arm64` build for each OS diff --git a/cmd/version.go b/cmd/version.go index d8e287f4a..c143cc403 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.2.0" +const Version = "3.3.0" // checkLatest flag for version command. var checkLatest bool From 0a8f77b21f57905722cf182909a71ab6ab0b05f4 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 15 Feb 2024 09:52:50 +0100 Subject: [PATCH 472/544] Add fingerprint export --- RELEASE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index 87155f4e8..bded12a2c 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -10,7 +10,7 @@ The Exercism CLI uses [GoReleaser](https://goreleaser.com) to automate the relea ## Confirm / Update the Changelog -Make sure all the recent changes are reflected in the "next release" section of the CHANGELOG.md file. +Make sure all the recent changes are reflected in the "next release" section of the CHANGELOG.md file. All the changes in the "next release" section should be moved to a new section that describes the version number, and gives it a date. You can view changes using the /compare/ view: @@ -33,6 +33,7 @@ Once the version bump PR has been merged, run the following commands: ```bash VERSION=$(sed -n -E 's/^const Version = "([0-9]+\.[0-9]+\.[0-9]+)"$/\1/p' cmd/version.go) TAG_NAME="v${VERSION}" +GPG_FINGERPRINT="" # Test run goreleaser --skip-publish --snapshot --clean @@ -41,6 +42,7 @@ goreleaser --skip-publish --snapshot --clean git tag -a "${TAG_NAME}" -m "Trying out GoReleaser" git push origin "${TAG_NAME}" ``` + Brew tap is now managed by `.goreleaser.yml` so no need to update it manually. GoReleaser can generate and publish a homebrew-tap recipe into a repository automatically. See [GoReleaser docs](https://goreleaser.com/customization/homebrew/) From d461855c5bb9a7d7dcce00e8abed53e4513d8288 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 15 Feb 2024 10:26:13 +0100 Subject: [PATCH 473/544] Format goreleaser.yml --- .goreleaser.yml | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 1fa59b225..16e221e4a 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -6,11 +6,11 @@ env: builds: - id: release-build main: ./exercism/main.go - mod_timestamp: '{{ .CommitTimestamp }}' + mod_timestamp: "{{ .CommitTimestamp }}" flags: - -trimpath # removes file system paths from compiled executable ldflags: - - '-s -w' # strip debug symbols and DWARF debugging info + - "-s -w" # strip debug symbols and DWARF debugging info goos: - darwin - linux @@ -33,11 +33,11 @@ builds: goarch: arm - id: installer-build main: ./exercism/main.go - mod_timestamp: '{{ .CommitTimestamp }}' + mod_timestamp: "{{ .CommitTimestamp }}" flags: - -trimpath # removes file system paths from compiled executable ldflags: - - '-s -w' # strip debug symbols and DWARF debugging info + - "-s -w" # strip debug symbols and DWARF debugging info goos: - windows goarch: @@ -48,8 +48,8 @@ changelog: sort: asc filters: exclude: - - '^docs:' - - '^test:' + - "^docs:" + - "^test:" archives: - id: release-archives @@ -64,8 +64,8 @@ archives: {{- else }}{{- .Arch }}{{ end }} {{- if .Arm }}v{{- .Arm }}{{ end }} format_overrides: - - goos: windows - format: zip + - goos: windows + format: zip files: - shell/** - LICENSE @@ -82,22 +82,31 @@ archives: {{- else }}{{- .Arch }}{{ end }} {{- if .Arm }}v{{- .Arm }}{{ end }} format_overrides: - - goos: windows - format: zip + - goos: windows + format: zip files: - shell/** - LICENSE - README.md checksum: - name_template: '{{ .ProjectName }}_checksums.txt' + name_template: "{{ .ProjectName }}_checksums.txt" ids: - release-archives - installer-archives signs: - artifacts: checksum - args: ["--batch", "-u", "{{ .Env.GPG_FINGERPRINT }}", "--output", "${signature}", "--detach-sign", "${artifact}"] + args: + [ + "--batch", + "-u", + "{{ .Env.GPG_FINGERPRINT }}", + "--output", + "${signature}", + "--detach-sign", + "${artifact}", + ] release: # Repo in which the release will be created. @@ -121,8 +130,7 @@ release: name_template: "{{.ProjectName}}-v{{.Version}} {{.Env.USER}}" brews: - - - name: exercism + - name: exercism repository: owner: exercism name: homebrew-exercism From 8f1e8f296f7a08158a2f8138c810ddc95e39f279 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 15 Feb 2024 10:33:14 +0100 Subject: [PATCH 474/544] Fix homebrew repo name (#1130) --- .goreleaser.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 16e221e4a..f3f965002 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -133,7 +133,7 @@ brews: - name: exercism repository: owner: exercism - name: homebrew-exercism + name: homebrew-core commit_author: name: Exercism Bot email: 66069679+exercism-bot@users.noreply.github.com From 7677e4b17c910ce22d47b39d7eac66052e3dd89d Mon Sep 17 00:00:00 2001 From: Exercism Bot Date: Fri, 1 Mar 2024 14:05:57 +0000 Subject: [PATCH 475/544] =?UTF-8?q?=F0=9F=A4=96=20Sync=20org-wide=20files?= =?UTF-8?q?=20to=20upstream=20repo=20(#1134)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit More info: https://github.com/exercism/org-wide-files/commit/0c0972d1df4cd18d98c7df316348315b06ef49b4 --- CODE_OF_CONDUCT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index df8e36761..3f7813de1 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -90,4 +90,4 @@ This policy was initially adopted from the Front-end London Slack community and A version history can be seen on [GitHub](https://github.com/exercism/website-copy/edit/main/pages/code_of_conduct.md). _This policy is a "living" document, and subject to refinement and expansion in the future. -This policy applies to the Exercism website, the Exercism GitHub organization, any other Exercism-related communication channels (e.g. Slack, Twitter, email) and any other Exercism entity or event._ +This policy applies to the Exercism website, the Exercism GitHub organization, any other Exercism-related communication channels (e.g. Discord, Forum, Twitter, email) and any other Exercism entity or event._ From c0825fd32b4beba5956b004761d8386635d06942 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 09:12:37 +0100 Subject: [PATCH 476/544] Bump actions/checkout from 4.0.0 to 4.1.0 (#1116) Bumps [actions/checkout](https://github.com/actions/checkout) from 4.0.0 to 4.1.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/3df4ab11eba7bda6032a0b82a6bb43b11571feac...8ade135a41bc03ea155e62e844d188df1ea18608) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 704eabdf9..b4510d825 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: os: [ubuntu-latest, windows-latest, macOS-latest] steps: - - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe with: @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - name: Check formatting run: ./.gha.gofmt.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 06175aa06..b7b3c3a47 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: fetch-depth: 0 From 38961a652c31b2213fbef766d4c74e58d6cf19e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:01:57 +0200 Subject: [PATCH 477/544] Bump golang.org/x/net from 0.4.0 to 0.23.0 (#1135) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.4.0 to 0.23.0. - [Commits](https://github.com/golang/net/compare/v0.4.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index c6a4a02a2..e56c3f97a 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,8 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.15.0 github.com/stretchr/testify v1.8.4 - golang.org/x/net v0.4.0 - golang.org/x/text v0.5.0 + golang.org/x/net v0.23.0 + golang.org/x/text v0.14.0 ) require ( @@ -26,7 +26,7 @@ require ( github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect - golang.org/x/sys v0.3.0 // indirect + golang.org/x/sys v0.18.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index bc3b4c47c..c042354a7 100644 --- a/go.sum +++ b/go.sum @@ -260,8 +260,8 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -316,8 +316,8 @@ golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -325,8 +325,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From 0b4ebdca31931e2198fa6ae7a25fad76db7a1af7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:09:24 +0200 Subject: [PATCH 478/544] Bump goreleaser/goreleaser-action from 4.4.0 to 5.0.0 (#1112) Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 4.4.0 to 5.0.0. - [Release notes](https://github.com/goreleaser/goreleaser-action/releases) - [Commits](https://github.com/goreleaser/goreleaser-action/compare/3fa32b8bb5620a2c1afe798654bbad59f9da4906...7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8) --- updated-dependencies: - dependency-name: goreleaser/goreleaser-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b7b3c3a47..ef69cb37d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,7 +32,7 @@ jobs: passphrase: ${{ secrets.PASSPHRASE }} - name: Cut Release - uses: goreleaser/goreleaser-action@3fa32b8bb5620a2c1afe798654bbad59f9da4906 # v4.4.0 + uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5.0.0 with: version: latest args: release --clean --release-header .release/header.md --timeout 120m # default time is 30m From e6aeb01305d4717feb98ecb74097862a3054499a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:09:31 +0200 Subject: [PATCH 479/544] Bump crazy-max/ghaction-import-gpg from 5.3.0 to 6.0.0 (#1111) Bumps [crazy-max/ghaction-import-gpg](https://github.com/crazy-max/ghaction-import-gpg) from 5.3.0 to 6.0.0. - [Release notes](https://github.com/crazy-max/ghaction-import-gpg/releases) - [Commits](https://github.com/crazy-max/ghaction-import-gpg/compare/72b6676b71ab476b77e676928516f6982eef7a41...82a020f1f7f605c65dd2449b392a52c3fcfef7ef) --- updated-dependencies: - dependency-name: crazy-max/ghaction-import-gpg dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef69cb37d..b74d61e58 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: - name: Import GPG Key id: import_gpg - uses: crazy-max/ghaction-import-gpg@72b6676b71ab476b77e676928516f6982eef7a41 # v5.3.0 + uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0 with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.PASSPHRASE }} From d08e50e28f898c6b7a9a82e0c28206fcc5d4a074 Mon Sep 17 00:00:00 2001 From: Erivelton de Andrade Nascimento <85421753+eNascimento178@users.noreply.github.com> Date: Thu, 2 May 2024 03:25:37 -0300 Subject: [PATCH 480/544] Add support for j in test_configurations.go (#1136) * Add support for j in test_configurations.go * Exchange double quotes to backtick * Update test_configurations.go --- workspace/test_configurations.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index 176729e1b..c6ac6fef8 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -139,6 +139,9 @@ var TestConfigurations = map[string]TestConfiguration{ "haskell": { Command: "stack test", }, + "j": { + Command: `jconsole -js "exit echo unittest {{test_files}} [ load {{solution_files}}"`, + }, "java": { Command: "gradle test", }, From 31bf0dc8a2f91516e201b363be2def059eb2477c Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Thu, 2 May 2024 02:35:18 -0400 Subject: [PATCH 481/544] Update homebrew formula description to use "exercism.org" (#1131) Co-authored-by: Erik Schierboom --- .goreleaser.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index f3f965002..149fbd8e5 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -139,6 +139,6 @@ brews: email: 66069679+exercism-bot@users.noreply.github.com folder: Formula homepage: "https://exercism.org/" - description: "Command-line tool to interact with exercism.io" + description: "Command-line tool to interact with exercism.org" test: | system "exercism version" From 6bbd4146a822cf2931b2087cc9cbcb2e8858ecc1 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 2 May 2024 09:22:39 +0200 Subject: [PATCH 482/544] Add scripts (#1138) * Add test build script * Add test script * Ignore test binary --- .gitignore | 1 + bin/build.sh | 3 +++ bin/format.sh | 3 +++ bin/test.sh | 3 +++ 4 files changed, 10 insertions(+) create mode 100755 bin/build.sh create mode 100755 bin/format.sh create mode 100755 bin/test.sh diff --git a/.gitignore b/.gitignore index 1f3baf919..c7963cd6f 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ _testmain.go out/ release/ go-exercism +testercism # Intellij /.idea diff --git a/bin/build.sh b/bin/build.sh new file mode 100755 index 000000000..f2ed29c5d --- /dev/null +++ b/bin/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +go build -o testercism ./exercism/main.go diff --git a/bin/format.sh b/bin/format.sh new file mode 100755 index 000000000..25c19b2e3 --- /dev/null +++ b/bin/format.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +go fmt ./... diff --git a/bin/test.sh b/bin/test.sh new file mode 100755 index 000000000..062c46972 --- /dev/null +++ b/bin/test.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +go test ./... From 664afb7aa437c85ac9f927551d0718ef87d7bd6e Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 2 May 2024 09:25:09 +0200 Subject: [PATCH 483/544] Add pyret support to `exercism test` (#1139) --- workspace/test_configurations.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index c6ac6fef8..ebf9cb4b7 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -191,6 +191,9 @@ var TestConfigurations = map[string]TestConfiguration{ "purescript": { Command: "spago test", }, + "pyret": { + Command: "pyret {{test_files}}", + }, "python": { Command: "python3 -m pytest -o markers=task {{test_files}}", }, From f2ee85a948667cf7bf7275e2211ee48bab963cdd Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Thu, 2 May 2024 03:28:44 -0400 Subject: [PATCH 484/544] troubleshoot command should not recommend creating a github issue (#1122) Co-authored-by: Erik Schierboom --- cmd/troubleshoot.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go index 5fd134d3c..8e5572794 100644 --- a/cmd/troubleshoot.go +++ b/cmd/troubleshoot.go @@ -26,7 +26,7 @@ var troubleshootCmd = &cobra.Command{ Long: `Provides output to help with troubleshooting. If you're running into trouble, copy and paste the output from the troubleshoot -command into a GitHub issue so we can help figure out what's going on. +command into a topic on the Exercism forum so we can help figure out what's going on. `, RunE: func(cmd *cobra.Command, args []string) error { cli.TimeoutInSeconds = cli.TimeoutInSeconds * 2 @@ -255,8 +255,8 @@ API Reachability * {{ .Latency }} {{ end }} -If you are having trouble please file a GitHub issue at -https://github.com/exercism/exercism.io/issues and include +If you are having trouble, please create a new topic in the Exercism forum +at https://forum.exercism.org/c/support/cli/10 and include this information. {{ if not .Censor }} Don't share your API key. Keep that private. From 33ce248e28d29ed1bac7704448c57d505f0541f4 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 2 May 2024 09:48:21 +0200 Subject: [PATCH 485/544] Fix the release notes link (#1140) --- cmd/troubleshoot.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go index 8e5572794..4ea5b1444 100644 --- a/cmd/troubleshoot.go +++ b/cmd/troubleshoot.go @@ -227,7 +227,7 @@ Latest: {{ with .Version.Latest }}{{ . }}{{ else }}{{ end }} {{ end -}} {{ if not .Version.UpToDate }} Call 'exercism upgrade' to get the latest version. -See the release notes at https://github.com/exercism/cli/releases/tag/{{ .Version.Latest }} for details. +See the release notes at https://github.com/exercism/cli/releases/tag/v{{ .Version.Latest }} for details. {{ end }} Operating System From 13d89051e6e6cd516715880481501a3c0ad7b22a Mon Sep 17 00:00:00 2001 From: Fabian Becker Date: Thu, 2 May 2024 12:14:15 +0200 Subject: [PATCH 486/544] Change open cmd to default to current directory (#1070) fixes #1069 Co-authored-by: Erik Schierboom --- cmd/open.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/open.go b/cmd/open.go index dd41a3cf6..627eac639 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -15,9 +15,13 @@ var openCmd = &cobra.Command{ Pass the path to the directory that contains the solution you want to see on the website. `, - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - metadata, err := workspace.NewExerciseMetadata(args[0]) + path := "." + if len(args) == 1 { + path = args[0] + } + metadata, err := workspace.NewExerciseMetadata(path) if err != nil { return err } From 0d2cbbd77b6ecc931fa6920b85b9b7028203dcb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Tue, 7 May 2024 09:05:10 +0100 Subject: [PATCH 487/544] Fix submit command usage (#1065) * Fix submit command usage * Update cmd/submit.go Co-authored-by: Victor Goff * Update cmd/submit.go * Update cmd/submit.go * Another format --------- Co-authored-by: Erik Schierboom Co-authored-by: Victor Goff --- cmd/submit.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/submit.go b/cmd/submit.go index aaf3a93c1..eb00aa1f4 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -19,12 +19,14 @@ import ( // submitCmd lets people upload a solution to the website. var submitCmd = &cobra.Command{ - Use: "submit FILE1 [FILE2 ...]", + Use: "submit [ ...]", Aliases: []string{"s"}, Short: "Submit your solution to an exercise.", Long: `Submit your solution to an Exercism exercise. Call the command with the list of files you want to submit. + If you omit the list of files, the CLI will submit the + default solution files for the exercise. `, RunE: func(cmd *cobra.Command, args []string) error { cfg := config.NewConfig() From db9c0e8c231f27bbadd24e40bfa59081e72e789e Mon Sep 17 00:00:00 2001 From: Sander Ploegsma Date: Tue, 7 May 2024 16:14:38 +0200 Subject: [PATCH 488/544] test: use Gradle wrapper for Java track (#1126) Co-authored-by: Erik Schierboom --- workspace/test_configurations.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index ebf9cb4b7..d09863762 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -143,7 +143,8 @@ var TestConfigurations = map[string]TestConfiguration{ Command: `jconsole -js "exit echo unittest {{test_files}} [ load {{solution_files}}"`, }, "java": { - Command: "gradle test", + Command: "./gradlew test", + WindowsCommand: "gradlew.bat test", }, "javascript": { Command: "npm run test", From be487d803709203385390c896bdacf700ef4e22e Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 9 May 2024 09:12:46 +0200 Subject: [PATCH 489/544] Update the release instructions (#1141) --- RELEASE.md | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index bded12a2c..efcd648cc 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -8,19 +8,12 @@ The Exercism CLI uses [GoReleaser](https://goreleaser.com) to automate the relea 1. [Setup GitHub token](https://goreleaser.com/scm/github/) 1. Have a gpg key installed on your machine - it is [used for signing the artifacts](https://goreleaser.com/customization/sign/) -## Confirm / Update the Changelog - -Make sure all the recent changes are reflected in the "next release" section of the CHANGELOG.md file. -All the changes in the "next release" section should be moved to a new section that describes the version number, and gives it a date. - -You can view changes using the /compare/ view: -https://github.com/exercism/cli/compare/$PREVIOUS_RELEASE...main - ## Bump the version 1. Create a branch for the new version -1. Edit the `Version` constant in `cmd/version.go` -1. Update the `CHANGELOG.md` file +1. Bump the `Version` constant in `cmd/version.go` +1. Update the `CHANGELOG.md` file to include a section for the new version and its changes. + Hint: you can view changes using the compare view: https://github.com/exercism/cli/compare/$PREVIOUS_RELEASE...main. 1. Commit the updated version 1. Create a PR From 95f08f3010a600635afb448f6b96db15329814e1 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 9 May 2024 09:41:13 +0200 Subject: [PATCH 490/544] Bump version to v3.4.0 (#1142) --- CHANGELOG.md | 9 +++++++++ cmd/version.go | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9063d3e46..86d07e2bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ## Next Release * **Your contribution here** +## v3.4.0 (2024-05-09) +* [#1126](https://github.com/exercism/cli/pull/1126) Update `exercism test` to use Gradle wrapper to test Java exercise - [@sanderploegsma] +* [#1139](https://github.com/exercism/cli/pull/1139) Add support for Pyret to `exercism test` +* [#1136](https://github.com/exercism/cli/pull/1136) Add support for J to `exercism test` - [@enascimento178] +* [#1070](https://github.com/exercism/cli/pull/1070) `exercism open` does not require specifying the directory (defaults to current directory) - [@halfdan] +* [#1122](https://github.com/exercism/cli/pull/1122) Troubleshoot command suggest to open forum post instead of GitHub issue - [@glennj] +* [#1065](https://github.com/exercism/cli/pull/1065) Update help text for `exercism submit` to indicate specifying files is optional - [@andrerfcsantos] +* [#1140](https://github.com/exercism/cli/pull/1140) Fix release notes link + ## v3.3.0 (2024-02-15) * [#1128](https://github.com/exercism/cli/pull/1128) Fix `exercism test` command not working for the `8th` and `emacs-lisp` tracks - [@glennj] * [#1125](https://github.com/exercism/cli/pull/1125) Simplify root command description diff --git a/cmd/version.go b/cmd/version.go index c143cc403..260afbb79 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.3.0" +const Version = "3.4.0" // checkLatest flag for version command. var checkLatest bool From 66b78b6bfb8bbc2c044d68356eb609e7fbc65d6c Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 9 May 2024 10:07:18 +0200 Subject: [PATCH 491/544] Fix homebrew (#1143) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b74d61e58..6b72a309e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,5 +37,5 @@ jobs: version: latest args: release --clean --release-header .release/header.md --timeout 120m # default time is 30m env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.CLI_GITHUB_TOKEN }} GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} From 9cf9401592058bb960e1c0dd9d2b054a54e328e4 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 9 May 2024 14:19:22 +0200 Subject: [PATCH 492/544] Fix goreleaser deprecations (#1144) --- .goreleaser.yml | 2 +- RELEASE.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 149fbd8e5..972d4e704 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -137,7 +137,7 @@ brews: commit_author: name: Exercism Bot email: 66069679+exercism-bot@users.noreply.github.com - folder: Formula + directory: Formula homepage: "https://exercism.org/" description: "Command-line tool to interact with exercism.org" test: | diff --git a/RELEASE.md b/RELEASE.md index efcd648cc..5c65318d3 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -29,7 +29,7 @@ TAG_NAME="v${VERSION}" GPG_FINGERPRINT="" # Test run -goreleaser --skip-publish --snapshot --clean +goreleaser --skip=publish --snapshot --clean # Create a new tag on the main branch and push it git tag -a "${TAG_NAME}" -m "Trying out GoReleaser" From 0509b1bd8c866d3dafc2adb50c317c826116f15a Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 9 May 2024 15:14:25 +0200 Subject: [PATCH 493/544] Add release script (#1145) --- RELEASE.md | 36 ++++-------------------------------- bin/release.sh | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 32 deletions(-) create mode 100755 bin/release.sh diff --git a/RELEASE.md b/RELEASE.md index 5c65318d3..ec956744b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -14,33 +14,19 @@ The Exercism CLI uses [GoReleaser](https://goreleaser.com) to automate the relea 1. Bump the `Version` constant in `cmd/version.go` 1. Update the `CHANGELOG.md` file to include a section for the new version and its changes. Hint: you can view changes using the compare view: https://github.com/exercism/cli/compare/$PREVIOUS_RELEASE...main. -1. Commit the updated version +1. Commit the updated files 1. Create a PR _Note: It's useful to add the version to the commit message when you bump it: e.g. `Bump version to v2.3.4`._ ## Cut a release -Once the version bump PR has been merged, run the following commands: +Once the version bump PR has been merged, run the following command to cut a release: -```bash -VERSION=$(sed -n -E 's/^const Version = "([0-9]+\.[0-9]+\.[0-9]+)"$/\1/p' cmd/version.go) -TAG_NAME="v${VERSION}" -GPG_FINGERPRINT="" - -# Test run -goreleaser --skip=publish --snapshot --clean - -# Create a new tag on the main branch and push it -git tag -a "${TAG_NAME}" -m "Trying out GoReleaser" -git push origin "${TAG_NAME}" +```shell +GPG_FINGERPINT="" ./bin/release.sh ``` -Brew tap is now managed by `.goreleaser.yml` so no need to update it manually. -GoReleaser can generate and publish a homebrew-tap recipe into a repository -automatically. See [GoReleaser docs](https://goreleaser.com/customization/homebrew/) -for more details. - ## Cut Release on GitHub At this point, Goreleaser will have created a draft release at https://github.com/exercism/cli/releases/tag/vX.Y.Z. @@ -55,20 +41,6 @@ To install, follow the interactive installation instructions at https://exercism Lastly, test and publish the draft -## Update Homebrew - -Next, we'll submit a PR to Homebrew to update the Exercism formula (which is how macOS users usually download the CLI): - -``` -cd /tmp && curl -O https://github.com/exercism/cli/archive/vX.Y.Z.tar.gz -cd $(brew --repository) -git checkout master -brew update -brew bump-formula-pr --strict exercism --url=https://github.com/exercism/cli/archive/vX.Y.Z.tar.gz --sha256=$(shasum -a 256 /tmp/vX.Y.Z.tar.gz) -``` - -For more information see [How To Open a Homebrew Pull Request](https://docs.brew.sh/How-To-Open-a-Homebrew-Pull-Request). - ## Update the docs site If there are any significant changes, we should describe them on diff --git a/bin/release.sh b/bin/release.sh new file mode 100755 index 000000000..05f228455 --- /dev/null +++ b/bin/release.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "${GPG_FINGERPRINT}" ]]; then + echo "GPG_FINGERPRINT environment variable is not set" + exit 1 +fi + +echo "Syncing repo with latest main..." +git checkout main +git pull + +VERSION=$(sed -n -E 's/^const Version = "([0-9]+\.[0-9]+\.[0-9]+)"$/\1/p' cmd/version.go) +TAG_NAME="v${VERSION}" + +echo "Verify release can be built..." +goreleaser --skip=publish --snapshot --clean + +echo "Pushing tag..." +git tag -a "${TAG_NAME}" -m "Release ${TAG_NAME}" +git push origin "${TAG_NAME}" + +echo "Tag pushed" +echo "The release CI workflow will automatically create a draft release." +echo "Once created, edit the release notes and publish it." From 753a4eb927d8602c785d09a82068237762f66f76 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 9 May 2024 16:07:05 +0200 Subject: [PATCH 494/544] Remove homebrew references (#1146) --- .github/workflows/release.yml | 2 +- .goreleaser.yml | 14 -------------- RELEASE.md | 6 +++++- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b72a309e..b74d61e58 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,5 +37,5 @@ jobs: version: latest args: release --clean --release-header .release/header.md --timeout 120m # default time is 30m env: - GITHUB_TOKEN: ${{ secrets.CLI_GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} diff --git a/.goreleaser.yml b/.goreleaser.yml index 972d4e704..b7491e0b8 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -128,17 +128,3 @@ release: # You can change the name of the GitHub release. # Default is `{{.Tag}}` name_template: "{{.ProjectName}}-v{{.Version}} {{.Env.USER}}" - -brews: - - name: exercism - repository: - owner: exercism - name: homebrew-core - commit_author: - name: Exercism Bot - email: 66069679+exercism-bot@users.noreply.github.com - directory: Formula - homepage: "https://exercism.org/" - description: "Command-line tool to interact with exercism.org" - test: | - system "exercism version" diff --git a/RELEASE.md b/RELEASE.md index ec956744b..35a02dbf1 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -39,7 +39,11 @@ To install, follow the interactive installation instructions at https://exercism [modify the generated release-notes to describe changes in this release] ``` -Lastly, test and publish the draft +Lastly, test and then publish the draft. + +## Homebrew + +Homebrew will automatically bump the version, no manual action is required. ## Update the docs site From ee8414b12d980e8594d3ccc44e83cfdf3e8dc9f1 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 6 Jun 2024 21:04:38 +0200 Subject: [PATCH 495/544] Add arturo to tests (#1147) --- workspace/test_configurations.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index d09863762..14b838be1 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -65,6 +65,9 @@ var TestConfigurations = map[string]TestConfiguration{ Command: "8th -f test.8th", }, // abap: tests are run via "ABAP Development Tools", not the CLI + "arturo": { + Command: "arturo tester.art", + }, "awk": { Command: "bats {{test_files}}", }, From 72e984f02e8684d686478e71321a067ca39b2836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nenad=20Misi=C4=87?= Date: Tue, 23 Jul 2024 12:57:05 +0200 Subject: [PATCH 496/544] Add cairo to test config (#1151) --- workspace/test_configurations.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index 14b838be1..72b359edf 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -80,6 +80,9 @@ var TestConfigurations = map[string]TestConfiguration{ "c": { Command: "make", }, + "cairo": { + Command: "scarb cairo-test", + }, "cfml": { Command: "box task run TestRunner", }, From def312e0b1512846e8d8b53d2e0e774b22b978a2 Mon Sep 17 00:00:00 2001 From: isberg Date: Fri, 2 Aug 2024 14:32:10 +0200 Subject: [PATCH 497/544] Add idris test configuration (#1152) --- workspace/test_configurations.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index 72b359edf..f5bb29019 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -145,6 +145,9 @@ var TestConfigurations = map[string]TestConfiguration{ "haskell": { Command: "stack test", }, + "idris": { + Command: "pack test `basename *.ipkg .ipkg`", + }, "j": { Command: `jconsole -js "exit echo unittest {{test_files}} [ load {{solution_files}}"`, }, From 743afdfe32390cdec0e4855e1cb8c5b13341adc2 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 15 Aug 2024 15:35:02 +0200 Subject: [PATCH 498/544] Bump version to v3.4.1 (#1154) --- CHANGELOG.md | 526 ++++++++++++++++++++++++++----------------------- cmd/version.go | 2 +- 2 files changed, 278 insertions(+), 250 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86d07e2bd..6398a756a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,448 +2,476 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ----------------- +--- ## Next Release -* **Your contribution here** + +- **Your contribution here** + +## v3.4.1 (2024-08-15) + +- [#1152](https://github.com/exercism/cli/pull/1152) Add support for Idris to `exercism test` - + [@isberg] +- [#1151](https://github.com/exercism/cli/pull/1151) Add support for Cairo to `exercism test` - [@isberg] +- [#1147](https://github.com/exercism/cli/pull/1147) Add support for Arturo to `exercism test` - [@erikschierboom] ## v3.4.0 (2024-05-09) -* [#1126](https://github.com/exercism/cli/pull/1126) Update `exercism test` to use Gradle wrapper to test Java exercise - [@sanderploegsma] -* [#1139](https://github.com/exercism/cli/pull/1139) Add support for Pyret to `exercism test` -* [#1136](https://github.com/exercism/cli/pull/1136) Add support for J to `exercism test` - [@enascimento178] -* [#1070](https://github.com/exercism/cli/pull/1070) `exercism open` does not require specifying the directory (defaults to current directory) - [@halfdan] -* [#1122](https://github.com/exercism/cli/pull/1122) Troubleshoot command suggest to open forum post instead of GitHub issue - [@glennj] -* [#1065](https://github.com/exercism/cli/pull/1065) Update help text for `exercism submit` to indicate specifying files is optional - [@andrerfcsantos] -* [#1140](https://github.com/exercism/cli/pull/1140) Fix release notes link + +- [#1126](https://github.com/exercism/cli/pull/1126) Update `exercism test` to use Gradle wrapper to test Java exercise - [@sanderploegsma] +- [#1139](https://github.com/exercism/cli/pull/1139) Add support for Pyret to `exercism test` +- [#1136](https://github.com/exercism/cli/pull/1136) Add support for J to `exercism test` - [@enascimento178] +- [#1070](https://github.com/exercism/cli/pull/1070) `exercism open` does not require specifying the directory (defaults to current directory) - [@halfdan] +- [#1122](https://github.com/exercism/cli/pull/1122) Troubleshoot command suggest to open forum post instead of GitHub issue - [@glennj] +- [#1065](https://github.com/exercism/cli/pull/1065) Update help text for `exercism submit` to indicate specifying files is optional - [@andrerfcsantos] +- [#1140](https://github.com/exercism/cli/pull/1140) Fix release notes link ## v3.3.0 (2024-02-15) -* [#1128](https://github.com/exercism/cli/pull/1128) Fix `exercism test` command not working for the `8th` and `emacs-lisp` tracks - [@glennj] -* [#1125](https://github.com/exercism/cli/pull/1125) Simplify root command description -* [#1124](https://github.com/exercism/cli/pull/1124) Use correct domain for FAQ link [@tomasnorre] + +- [#1128](https://github.com/exercism/cli/pull/1128) Fix `exercism test` command not working for the `8th` and `emacs-lisp` tracks - [@glennj] +- [#1125](https://github.com/exercism/cli/pull/1125) Simplify root command description +- [#1124](https://github.com/exercism/cli/pull/1124) Use correct domain for FAQ link [@tomasnorre] ## v3.2.0 (2023-07-28) -* [#1092](https://github.com/exercism/cli/pull/1092) Add `exercism test` command to run the unit tests for nearly any track (inspired by [universal-test-runner](https://github.com/xavdid/universal-test-runner)) - [@xavdid] -* [#1073](https://github.com/exercism/cli/pull/1073) Add `arm64` build for each OS + +- [#1092](https://github.com/exercism/cli/pull/1092) Add `exercism test` command to run the unit tests for nearly any track (inspired by [universal-test-runner](https://github.com/xavdid/universal-test-runner)) - [@xavdid] +- [#1073](https://github.com/exercism/cli/pull/1073) Add `arm64` build for each OS ## v3.1.0 (2022-10-04) -* [#979](https://github.com/exercism/cli/pull/979) Protect existing solutions from being overwritten by 'download' - [@harugo] -* [#981](https://github.com/exercism/cli/pull/981) Check if authorisation header is set before attempting to extract token - [@harugo] -* [#1044](https://github.com/exercism/cli/pull/1044) Submit without specifying files - [@andrerfcsantos] + +- [#979](https://github.com/exercism/cli/pull/979) Protect existing solutions from being overwritten by 'download' - [@harugo] +- [#981](https://github.com/exercism/cli/pull/981) Check if authorisation header is set before attempting to extract token - [@harugo] +- [#1044](https://github.com/exercism/cli/pull/1044) Submit without specifying files - [@andrerfcsantos] ## v3.0.13 (2019-10-23) -* [#866](https://github.com/exercism/cli/pull/866) The API token outputted during verbose will now be masked by default - [@Jrank2013] -* [#873](https://github.com/exercism/cli/pull/873) Make all errors in cmd package checked - [@avegner] -* [#871](https://github.com/exercism/cli/pull/871) Error message returned if the track is locked - [@Jrank2013] -* [#886](https://github.com/exercism/cli/pull/886) Added GoReleaser config, updated docs, made archive naming adjustments - [@ekingery] + +- [#866](https://github.com/exercism/cli/pull/866) The API token outputted during verbose will now be masked by default - [@Jrank2013] +- [#873](https://github.com/exercism/cli/pull/873) Make all errors in cmd package checked - [@avegner] +- [#871](https://github.com/exercism/cli/pull/871) Error message returned if the track is locked - [@Jrank2013] +- [#886](https://github.com/exercism/cli/pull/886) Added GoReleaser config, updated docs, made archive naming adjustments - [@ekingery] ## v3.0.12 (2019-07-07) -* [#770](https://github.com/exercism/cli/pull/770) Print API error messages in submit command - [@Smarticles101] -* [#763](https://github.com/exercism/cli/pull/763) Add Fish shell tab completions - [@John-Goff] -* [#806](https://github.com/exercism/cli/pull/806) Make Zsh shell tab completions work on $fpath - [@QuLogic] -* [#797](https://github.com/exercism/cli/pull/797) Fix panic when submit command is not given args - [@jdsutherland] -* [#828](https://github.com/exercism/cli/pull/828) Remove duplicate files before submitting - [@larson004] -* [#793](https://github.com/exercism/cli/pull/793) Submit handles non 2xx responses - [@jdsutherland] + +- [#770](https://github.com/exercism/cli/pull/770) Print API error messages in submit command - [@Smarticles101] +- [#763](https://github.com/exercism/cli/pull/763) Add Fish shell tab completions - [@John-Goff] +- [#806](https://github.com/exercism/cli/pull/806) Make Zsh shell tab completions work on $fpath - [@QuLogic] +- [#797](https://github.com/exercism/cli/pull/797) Fix panic when submit command is not given args - [@jdsutherland] +- [#828](https://github.com/exercism/cli/pull/828) Remove duplicate files before submitting - [@larson004] +- [#793](https://github.com/exercism/cli/pull/793) Submit handles non 2xx responses - [@jdsutherland] ## v3.0.11 (2018-11-18) -* [#752](https://github.com/exercism/cli/pull/752) Improve error message on upgrade command - [@farisj] -* [#759](https://github.com/exercism/cli/pull/759) Update shell tab completion for bash and zsh - [@nywilken] -* [#762](https://github.com/exercism/cli/pull/762) Improve usage documentation - [@Smarticles101] -* [#766](https://github.com/exercism/cli/pull/766) Tweak messaging to work for teams edition - [@kytrinyx] + +- [#752](https://github.com/exercism/cli/pull/752) Improve error message on upgrade command - [@farisj] +- [#759](https://github.com/exercism/cli/pull/759) Update shell tab completion for bash and zsh - [@nywilken] +- [#762](https://github.com/exercism/cli/pull/762) Improve usage documentation - [@Smarticles101] +- [#766](https://github.com/exercism/cli/pull/766) Tweak messaging to work for teams edition - [@kytrinyx] ## v3.0.10 (2018-10-03) -* official release of v3.0.10-alpha.1 - [@nywilken] + +- official release of v3.0.10-alpha.1 - [@nywilken] ## v3.0.10-alpha.1 (2018-09-21) -* [#739](https://github.com/exercism/cli/pull/739) update maxFileSize error to include filename - [@nywilken] -* [#736](https://github.com/exercism/cli/pull/736) Metadata file .solution.json renamed to metadata.json - [@jdsutherland] -* [#738](https://github.com/exercism/cli/pull/738) Add missing contributor URLs to CHANGELOG - [@nywilken] -* [#737](https://github.com/exercism/cli/pull/737) Remove unused solutions type - [@jdsutherland] -* [#729](https://github.com/exercism/cli/pull/729) Update Oh My Zsh instructions - [@katrinleinweber] -* [#725](https://github.com/exercism/cli/pull/725) Do not allow submission of enormous files - [@sfairchild] -* [#724](https://github.com/exercism/cli/pull/724) Update submit error message when submitting a directory - [@sfairchild] -* [#723](https://github.com/exercism/cli/pull/720) Move .solution.json to hidden subdirectory - [@jdsutherland] + +- [#739](https://github.com/exercism/cli/pull/739) update maxFileSize error to include filename - [@nywilken] +- [#736](https://github.com/exercism/cli/pull/736) Metadata file .solution.json renamed to metadata.json - [@jdsutherland] +- [#738](https://github.com/exercism/cli/pull/738) Add missing contributor URLs to CHANGELOG - [@nywilken] +- [#737](https://github.com/exercism/cli/pull/737) Remove unused solutions type - [@jdsutherland] +- [#729](https://github.com/exercism/cli/pull/729) Update Oh My Zsh instructions - [@katrinleinweber] +- [#725](https://github.com/exercism/cli/pull/725) Do not allow submission of enormous files - [@sfairchild] +- [#724](https://github.com/exercism/cli/pull/724) Update submit error message when submitting a directory - [@sfairchild] +- [#723](https://github.com/exercism/cli/pull/720) Move .solution.json to hidden subdirectory - [@jdsutherland] ## v3.0.9 (2018-08-29) -* [#720](https://github.com/exercism/cli/pull/720) Make the timeout configurable globally - [@kytrinyx] -* [#721](https://github.com/exercism/cli/pull/721) Handle windows filepaths that accidentally got submitted to the server - [@kytrinyx] -* [#722](https://github.com/exercism/cli/pull/722) Handle exercise directories with numeric suffixes - [@kytrinyx] + +- [#720](https://github.com/exercism/cli/pull/720) Make the timeout configurable globally - [@kytrinyx] +- [#721](https://github.com/exercism/cli/pull/721) Handle windows filepaths that accidentally got submitted to the server - [@kytrinyx] +- [#722](https://github.com/exercism/cli/pull/722) Handle exercise directories with numeric suffixes - [@kytrinyx] ## v3.0.8 (2018-08-22) -* [#713](https://github.com/exercism/cli/pull/713) Fix broken support for uuid flag on download command - [@nywilken] + +- [#713](https://github.com/exercism/cli/pull/713) Fix broken support for uuid flag on download command - [@nywilken] ## v3.0.7 (2018-08-21) -* [#705](https://github.com/exercism/cli/pull/705) Fix confusion about path and filepath - [@kytrinyx] -* [#650](https://github.com/exercism/cli/pull/650) Fix encoding problem in filenames - [@williandrade] + +- [#705](https://github.com/exercism/cli/pull/705) Fix confusion about path and filepath - [@kytrinyx] +- [#650](https://github.com/exercism/cli/pull/650) Fix encoding problem in filenames - [@williandrade] ## v3.0.6 (2018-07-17) -* [#652](https://github.com/exercism/cli/pull/652) Add support for teams feature - [@kytrinyx] -* [#683](https://github.com/exercism/cli/pull/683) Fix typo in welcome message - [@glebedel] -* [#675](https://github.com/exercism/cli/pull/675) Improve output of troubleshoot command when CLI is unconfigured - [@kytrinyx] -* [#679](https://github.com/exercism/cli/pull/679) Improve error message for failed /ping on configure - [@kytrinyx] -* [#669](https://github.com/exercism/cli/pull/669) Add debug as alias for troubleshoot - [@kytrinyx] -* [#647](https://github.com/exercism/cli/pull/647) Ensure welcome message has full link to settings page - [@kytrinyx] -* [#667](https://github.com/exercism/cli/pull/667) Improve bash completion script - [@cookrn] + +- [#652](https://github.com/exercism/cli/pull/652) Add support for teams feature - [@kytrinyx] +- [#683](https://github.com/exercism/cli/pull/683) Fix typo in welcome message - [@glebedel] +- [#675](https://github.com/exercism/cli/pull/675) Improve output of troubleshoot command when CLI is unconfigured - [@kytrinyx] +- [#679](https://github.com/exercism/cli/pull/679) Improve error message for failed /ping on configure - [@kytrinyx] +- [#669](https://github.com/exercism/cli/pull/669) Add debug as alias for troubleshoot - [@kytrinyx] +- [#647](https://github.com/exercism/cli/pull/647) Ensure welcome message has full link to settings page - [@kytrinyx] +- [#667](https://github.com/exercism/cli/pull/667) Improve bash completion script - [@cookrn] ## v3.0.5 (2018-07-17) -* [#646](https://github.com/exercism/cli/pull/646) Fix issue with upgrading on Windows - [@nywilken] + +- [#646](https://github.com/exercism/cli/pull/646) Fix issue with upgrading on Windows - [@nywilken] ## v3.0.4 (2018-07-15) -* [#644](https://github.com/exercism/cli/pull/644) Add better error messages when solution metadata is missing - [@kytrinyx] + +- [#644](https://github.com/exercism/cli/pull/644) Add better error messages when solution metadata is missing - [@kytrinyx] ## v3.0.3 (2018-07-14) -* [#642](https://github.com/exercism/cli/pull/642) Add better error messages when configuration is needed before download - [@kytrinyx] -* [#641](https://github.com/exercism/cli/pull/641) Fix broken download for uuid flag - [@kytrinyx] -* [#618](https://github.com/exercism/cli/pull/618) Fix broken test in Windows build for relative paths - [@nywilken] -* [#631](https://github.com/exercism/cli/pull/631) Stop accepting token flag on download command - [@kytrinyx] -* [#616](https://github.com/exercism/cli/pull/616) Add shell completion scripts to build artifacts - [@jdsutherland] -* [#624](https://github.com/exercism/cli/pull/624) Tweak command documentation to reflect reality - [@kytrinyx] -* [#625](https://github.com/exercism/cli/pull/625) Fix wildly excessive whitespace in error messages - [@kytrinyx] + +- [#642](https://github.com/exercism/cli/pull/642) Add better error messages when configuration is needed before download - [@kytrinyx] +- [#641](https://github.com/exercism/cli/pull/641) Fix broken download for uuid flag - [@kytrinyx] +- [#618](https://github.com/exercism/cli/pull/618) Fix broken test in Windows build for relative paths - [@nywilken] +- [#631](https://github.com/exercism/cli/pull/631) Stop accepting token flag on download command - [@kytrinyx] +- [#616](https://github.com/exercism/cli/pull/616) Add shell completion scripts to build artifacts - [@jdsutherland] +- [#624](https://github.com/exercism/cli/pull/624) Tweak command documentation to reflect reality - [@kytrinyx] +- [#625](https://github.com/exercism/cli/pull/625) Fix wildly excessive whitespace in error messages - [@kytrinyx] ## v3.0.2 (2018-07-13) -* [#622](https://github.com/exercism/cli/pull/622) Fix bug with multi-file submission - [@kytrinyx] + +- [#622](https://github.com/exercism/cli/pull/622) Fix bug with multi-file submission - [@kytrinyx] ## v3.0.1 (2018-07-13) -* [#619](https://github.com/exercism/cli/pull/619) Improve error message for successful configuration - [@kytrinyx] + +- [#619](https://github.com/exercism/cli/pull/619) Improve error message for successful configuration - [@kytrinyx] ## v3.0.0 (2018-07-13) This is a complete rewrite from the ground up to work against the new https://exercism.io site. ## v2.4.1 (2017-07-01) -* [#385](https://github.com/exercism/cli/pull/385) Fix broken upgrades for Windows - [@Tonkpils] + +- [#385](https://github.com/exercism/cli/pull/385) Fix broken upgrades for Windows - [@Tonkpils] ## v2.4.0 (2017-03-24) -* [#344](https://github.com/exercism/cli/pull/344) Make the CLI config paths more XDG friendly - [@narqo] -* [#346](https://github.com/exercism/cli/pull/346) Fallback to UTF-8 if encoding is uncertain - [@petertseng] -* [#350](https://github.com/exercism/cli/pull/350) Add ARMv8 binaries to CLI releases - [@Tonkpils] -* [#352](https://github.com/exercism/cli/pull/352) Fix case sensitivity on slug and track ID - [@Tonkpils] -* [#353](https://github.com/exercism/cli/pull/353) Print confirmation when fetching --all - [@neslom] -* [#356](https://github.com/exercism/cli/pull/356) Resolve symlinks before attempting to read files - [@lcowell] -* [#358](https://github.com/exercism/cli/pull/358) Redact API key from debug output - [@Tonkpils] -* [#359](https://github.com/exercism/cli/pull/359) Add short flag `-m` for submit comment flag - [@jgsqware] -* [#366](https://github.com/exercism/cli/pull/366) Allow obfuscation on configure command - [@dmmulroy] -* [#367](https://github.com/exercism/cli/pull/367) Use supplied confirmation text from API on submit - [@nilbus] +- [#344](https://github.com/exercism/cli/pull/344) Make the CLI config paths more XDG friendly - [@narqo] +- [#346](https://github.com/exercism/cli/pull/346) Fallback to UTF-8 if encoding is uncertain - [@petertseng] +- [#350](https://github.com/exercism/cli/pull/350) Add ARMv8 binaries to CLI releases - [@Tonkpils] +- [#352](https://github.com/exercism/cli/pull/352) Fix case sensitivity on slug and track ID - [@Tonkpils] +- [#353](https://github.com/exercism/cli/pull/353) Print confirmation when fetching --all - [@neslom] +- [#356](https://github.com/exercism/cli/pull/356) Resolve symlinks before attempting to read files - [@lcowell] +- [#358](https://github.com/exercism/cli/pull/358) Redact API key from debug output - [@Tonkpils] +- [#359](https://github.com/exercism/cli/pull/359) Add short flag `-m` for submit comment flag - [@jgsqware] +- [#366](https://github.com/exercism/cli/pull/366) Allow obfuscation on configure command - [@dmmulroy] +- [#367](https://github.com/exercism/cli/pull/367) Use supplied confirmation text from API on submit - [@nilbus] ## v2.3.0 (2016-08-07) -* [#339](https://github.com/exercism/cli/pull/339) Don't run status command if API key is missing - [@ests] -* [#336](https://github.com/exercism/cli/pull/336) Add '--all' flag to fetch command - [@neslom] -* [#333](https://github.com/exercism/cli/pull/333) Update references of codegangsta/cli -> urfave/cli - [@manusajith], [@blackerby] -* [#331](https://github.com/exercism/cli/pull/331) Improve usage/help text of submit command - [@manusajith] +- [#339](https://github.com/exercism/cli/pull/339) Don't run status command if API key is missing - [@ests] +- [#336](https://github.com/exercism/cli/pull/336) Add '--all' flag to fetch command - [@neslom] +- [#333](https://github.com/exercism/cli/pull/333) Update references of codegangsta/cli -> urfave/cli - [@manusajith], [@blackerby] +- [#331](https://github.com/exercism/cli/pull/331) Improve usage/help text of submit command - [@manusajith] ## v2.2.6 (2016-05-30) -* [#306](https://github.com/exercism/cli/pull/306) Don't use Fatal to print usage - [@broady] -* [#307](https://github.com/exercism/cli/pull/307) Pass API key when fetching individual exercises - [@kytrinyx] -* [#312](https://github.com/exercism/cli/pull/312) Add missing newline on usage string - [@jppunnett] -* [#318](https://github.com/exercism/cli/pull/318) Show activity stream URL after submitting - [@lcowell] -* [4710640](https://github.com/exercism/cli/commit/4710640751c7a01deb1b5bf8a9a65b611b078c05) - [@lcowell] -* Update codegangsta/cli dependency - [@manusajith], [@lcowell] -* [#320](https://github.com/exercism/cli/pull/320) Add missing newlines to usage strings - [@hjljo] -* [#328](https://github.com/exercism/cli/pull/328) Append solution URL path consistently - [@Tonkpils] +- [#306](https://github.com/exercism/cli/pull/306) Don't use Fatal to print usage - [@broady] +- [#307](https://github.com/exercism/cli/pull/307) Pass API key when fetching individual exercises - [@kytrinyx] +- [#312](https://github.com/exercism/cli/pull/312) Add missing newline on usage string - [@jppunnett] +- [#318](https://github.com/exercism/cli/pull/318) Show activity stream URL after submitting - [@lcowell] +- [4710640](https://github.com/exercism/cli/commit/4710640751c7a01deb1b5bf8a9a65b611b078c05) - [@lcowell] +- Update codegangsta/cli dependency - [@manusajith], [@lcowell] +- [#320](https://github.com/exercism/cli/pull/320) Add missing newlines to usage strings - [@hjljo] +- [#328](https://github.com/exercism/cli/pull/328) Append solution URL path consistently - [@Tonkpils] ## v2.2.5 (2016-04-02) -* [#284](https://github.com/exercism/cli/pull/284) Update release instructions - [@kytrinyx] -* [#285](https://github.com/exercism/cli/pull/285) Create a copy/pastable release text - [@kytrinyx] -* [#289](https://github.com/exercism/cli/pull/289) Fix a typo in the usage statement - [@AlexWheeler] -* [#290](https://github.com/exercism/cli/pull/290) Fix upgrade command for Linux systems - [@jbaiter] -* [#292](https://github.com/exercism/cli/pull/292) Vendor dependencies - [@Tonkpils] -* [#293](https://github.com/exercism/cli/pull/293) Remove extraneous/distracting details from README - [@Tonkpils] -* [#294](https://github.com/exercism/cli/pull/294) Improve usage statement: alphabetize commands - [@beanieboi] -* [#297](https://github.com/exercism/cli/pull/297) Improve debug output when API key is unconfigured - [@mrageh] -* [#299](https://github.com/exercism/cli/pull/299) List output uses track ID and problem from list - [@Tonkpils] -* [#301](https://github.com/exercism/cli/pull/301) Return error message for unknown track status - [@neslom] -* [#302](https://github.com/exercism/cli/pull/302) Add helpful error message when user tries to submit a directory - [@alebaffa] +- [#284](https://github.com/exercism/cli/pull/284) Update release instructions - [@kytrinyx] +- [#285](https://github.com/exercism/cli/pull/285) Create a copy/pastable release text - [@kytrinyx] +- [#289](https://github.com/exercism/cli/pull/289) Fix a typo in the usage statement - [@AlexWheeler] +- [#290](https://github.com/exercism/cli/pull/290) Fix upgrade command for Linux systems - [@jbaiter] +- [#292](https://github.com/exercism/cli/pull/292) Vendor dependencies - [@Tonkpils] +- [#293](https://github.com/exercism/cli/pull/293) Remove extraneous/distracting details from README - [@Tonkpils] +- [#294](https://github.com/exercism/cli/pull/294) Improve usage statement: alphabetize commands - [@beanieboi] +- [#297](https://github.com/exercism/cli/pull/297) Improve debug output when API key is unconfigured - [@mrageh] +- [#299](https://github.com/exercism/cli/pull/299) List output uses track ID and problem from list - [@Tonkpils] +- [#301](https://github.com/exercism/cli/pull/301) Return error message for unknown track status - [@neslom] +- [#302](https://github.com/exercism/cli/pull/302) Add helpful error message when user tries to submit a directory - [@alebaffa] ## v2.2.4 (2016-01-28) -* [#270](https://github.com/exercism/cli/pull/270) Allow commenting on submission with --comment - [@Tonkpils] -* [#271](https://github.com/exercism/cli/pull/271) Increase timeout to 20 seconds - [@Tonkpils] -* [#273](https://github.com/exercism/cli/pull/273) Guard against submitting spec files and README - [@daveyarwood] -* [#278](https://github.com/exercism/cli/pull/278) Create files with 0644 mode, create missing directories for downloaded solutions - [@petertseng] -* [#281](https://github.com/exercism/cli/pull/281) Create missing directories for downloaded problems - [@petertseng] -* [#282](https://github.com/exercism/cli/pull/282) Remove random encouragement after submitting - [@kytrinyx] -* [#283](https://github.com/exercism/cli/pull/283) Print current configuration after calling configure command - [@kytrinyx] +- [#270](https://github.com/exercism/cli/pull/270) Allow commenting on submission with --comment - [@Tonkpils] +- [#271](https://github.com/exercism/cli/pull/271) Increase timeout to 20 seconds - [@Tonkpils] +- [#273](https://github.com/exercism/cli/pull/273) Guard against submitting spec files and README - [@daveyarwood] +- [#278](https://github.com/exercism/cli/pull/278) Create files with 0644 mode, create missing directories for downloaded solutions - [@petertseng] +- [#281](https://github.com/exercism/cli/pull/281) Create missing directories for downloaded problems - [@petertseng] +- [#282](https://github.com/exercism/cli/pull/282) Remove random encouragement after submitting - [@kytrinyx] +- [#283](https://github.com/exercism/cli/pull/283) Print current configuration after calling configure command - [@kytrinyx] ## v2.2.3 (2015-12-27) -* [#264](https://github.com/exercism/cli/pull/264) Fix version flag to use --version and --v - [@Tonkpils] + +- [#264](https://github.com/exercism/cli/pull/264) Fix version flag to use --version and --v - [@Tonkpils] ## v2.2.2 (2015-12-26) -* [#212](https://github.com/exercism/cli/pull/212) extract path related code from config - [@lcowell] -* [#215](https://github.com/exercism/cli/pull/215) use $XDG_CONFIG_HOME if available - [@lcowell] -* [#248](https://github.com/exercism/cli/pull/248) [#253](https://github.com/exercism/cli/pull/253) add debugging output - [@lcowell] -* [#256](https://github.com/exercism/cli/pull/256) clean up build scripts - [@lcowell] -* [#258](https://github.com/exercism/cli/pull/258) reduce filesystem noise on fetch [@devonestes] -* [#261](https://github.com/exercism/cli/pull/261) improve error message when track and exercise can't be identified on submit - [@anxiousmodernman] -* [#262](https://github.com/exercism/cli/pull/262) encourage iterating to improve after first submission on an exercise - [@eToThePiIPower] +- [#212](https://github.com/exercism/cli/pull/212) extract path related code from config - [@lcowell] +- [#215](https://github.com/exercism/cli/pull/215) use $XDG_CONFIG_HOME if available - [@lcowell] +- [#248](https://github.com/exercism/cli/pull/248) [#253](https://github.com/exercism/cli/pull/253) add debugging output - [@lcowell] +- [#256](https://github.com/exercism/cli/pull/256) clean up build scripts - [@lcowell] +- [#258](https://github.com/exercism/cli/pull/258) reduce filesystem noise on fetch [@devonestes] +- [#261](https://github.com/exercism/cli/pull/261) improve error message when track and exercise can't be identified on submit - [@anxiousmodernman] +- [#262](https://github.com/exercism/cli/pull/262) encourage iterating to improve after first submission on an exercise - [@eToThePiIPower] ## v2.2.1 (2015-08-11) -* [#200](https://github.com/exercism/cli/pull/200): Add guard to unsubmit command - [@kytrinyx] -* [#204](https://github.com/exercism/cli/pull/204): Improve upgrade failure messages and increase timeout - [@Tonkpils] -* [#206](https://github.com/exercism/cli/pull/207): Fix verbose flag and removed short `-v` - [@zabawaba99] -* [#208](https://github.com/exercism/cli/pull/208): avoid ambiguous or unresolvable exercism paths - [@lcowell] +- [#200](https://github.com/exercism/cli/pull/200): Add guard to unsubmit command - [@kytrinyx] +- [#204](https://github.com/exercism/cli/pull/204): Improve upgrade failure messages and increase timeout - [@Tonkpils] +- [#206](https://github.com/exercism/cli/pull/207): Fix verbose flag and removed short `-v` - [@zabawaba99] +- [#208](https://github.com/exercism/cli/pull/208): avoid ambiguous or unresolvable exercism paths - [@lcowell] ## v2.2.0 (2015-06-27) -* [b3c3d6f](https://github.com/exercism/cli/commit/b3c3d6fe54c622fc0ee07fdd221c8e8e5b73c8cd): Improve error message on Internal Server Error - [@Tonkpils] -* [#196](https://github.com/exercism/cli/pull/196): Add upgrade command - [@Tonkpils] -* [#194](https://github.com/exercism/cli/pull/194): Fix home expansion on configure update - [@Tonkpils] -* [523c5bd](https://github.com/exercism/cli/commit/523c5bdec5ef857f07b39de738a764589660cd5a): Document release process - [@kytrinyx] +- [b3c3d6f](https://github.com/exercism/cli/commit/b3c3d6fe54c622fc0ee07fdd221c8e8e5b73c8cd): Improve error message on Internal Server Error - [@Tonkpils] +- [#196](https://github.com/exercism/cli/pull/196): Add upgrade command - [@Tonkpils] +- [#194](https://github.com/exercism/cli/pull/194): Fix home expansion on configure update - [@Tonkpils] +- [523c5bd](https://github.com/exercism/cli/commit/523c5bdec5ef857f07b39de738a764589660cd5a): Document release process - [@kytrinyx] ## v2.1.1 (2015-05-13) -* [#192](https://github.com/exercism/cli/pull/192): Loosen up restrictions on --test flag for submissions - [@Tonkpils] -* [#190](https://github.com/exercism/cli/pull/190): Fix bug in home directory expansion for Windows - [@Tonkpils] +- [#192](https://github.com/exercism/cli/pull/192): Loosen up restrictions on --test flag for submissions - [@Tonkpils] +- [#190](https://github.com/exercism/cli/pull/190): Fix bug in home directory expansion for Windows - [@Tonkpils] ## v2.1.0 (2015-05-08) -* [1a2fd1b](https://github.com/exercism/cli/commit/1a2fd1bfb2dba358611a7c3266f935cccaf924b5): Handle config as either directory or file - [@lcowell] -* [#177](https://github.com/exercism/cli/pull/177): Improve JSON error handling and reporting - [@Tonkpils] -* [#178](https://github.com/exercism/cli/pull/178): Add support for $XDG_CONFIG_HOME - [@lcowell] -* [#184](https://github.com/exercism/cli/pull/184): Handle different file encodings in submissions - [@ambroff] -* [#179](https://github.com/exercism/cli/pull/179): Pretty print the JSON config - [@Tonkpils] -* [#181](https://github.com/exercism/cli/pull/181): Fix path issue when downloading problems - [@Tonkpils] -* [#186](https://github.com/exercism/cli/pull/186): Allow people to specify a target directory for the demo - [@Tonkpils] -* [#189](https://github.com/exercism/cli/pull/189): Implement `--test` flag to allow submitting a test file in the solution - [@pminten] +- [1a2fd1b](https://github.com/exercism/cli/commit/1a2fd1bfb2dba358611a7c3266f935cccaf924b5): Handle config as either directory or file - [@lcowell] +- [#177](https://github.com/exercism/cli/pull/177): Improve JSON error handling and reporting - [@Tonkpils] +- [#178](https://github.com/exercism/cli/pull/178): Add support for $XDG_CONFIG_HOME - [@lcowell] +- [#184](https://github.com/exercism/cli/pull/184): Handle different file encodings in submissions - [@ambroff] +- [#179](https://github.com/exercism/cli/pull/179): Pretty print the JSON config - [@Tonkpils] +- [#181](https://github.com/exercism/cli/pull/181): Fix path issue when downloading problems - [@Tonkpils] +- [#186](https://github.com/exercism/cli/pull/186): Allow people to specify a target directory for the demo - [@Tonkpils] +- [#189](https://github.com/exercism/cli/pull/189): Implement `--test` flag to allow submitting a test file in the solution - [@pminten] ## v2.0.2 (2015-04-01) -* [#174](https://github.com/exercism/cli/issues/174): Fix panic during fetch - [@kytrinyx] -* Refactor handling of ENV vars - [@lcowell] +- [#174](https://github.com/exercism/cli/issues/174): Fix panic during fetch - [@kytrinyx] +- Refactor handling of ENV vars - [@lcowell] ## v2.0.1 (2015-03-25) -* [#167](https://github.com/exercism/cli/pull/167): Fixes misspelling of exercism list command - [@queuebit] -* Tweak output from `fetch` so that languages are scannable. -* [#35](https://github.com/exercism/cli/issues/35): Add support for submitting multiple-file solutions -* [#171](https://github.com/exercism/cli/pull/171): Implement `skip` command to bypass individual exercises - [@Tonkpils] +- [#167](https://github.com/exercism/cli/pull/167): Fixes misspelling of exercism list command - [@queuebit] +- Tweak output from `fetch` so that languages are scannable. +- [#35](https://github.com/exercism/cli/issues/35): Add support for submitting multiple-file solutions +- [#171](https://github.com/exercism/cli/pull/171): Implement `skip` command to bypass individual exercises - [@Tonkpils] ## v2.0.0 (2015-03-05) Added: -* [#154](https://github.com/exercism/cli/pull/154): Add 'list' command to list available exercises for a language - [@lcowell] -* [3551884](https://github.com/exercism/cli/commit/3551884e9f38d6e563b99dae7b28a18d4525455d): Add host connectivity status to debug output. - [@lcowell] -* [#162](https://github.com/exercism/cli/pull/162): Allow users to open the browser from the terminal. - [@zabawaba99] +- [#154](https://github.com/exercism/cli/pull/154): Add 'list' command to list available exercises for a language - [@lcowell] +- [3551884](https://github.com/exercism/cli/commit/3551884e9f38d6e563b99dae7b28a18d4525455d): Add host connectivity status to debug output. - [@lcowell] +- [#162](https://github.com/exercism/cli/pull/162): Allow users to open the browser from the terminal. - [@zabawaba99] Removed: -* Stop supporting legacy config files (`~/.exercism.go`) -* Deleted deprecated login/logout commands -* Deleted deprecated key names in config +- Stop supporting legacy config files (`~/.exercism.go`) +- Deleted deprecated login/logout commands +- Deleted deprecated key names in config Fixed: -* [#151](https://github.com/exercism/cli/pull/151): Expand '~' in config path to home directory - [@lcowell] -* [#155](https://github.com/exercism/cli/pull/155): Display problems not yet submitted on fetch API - [@Tonkpils] -* [f999e69](https://github.com/exercism/cli/commit/f999e69e5290cec6c5c9933aecc6fddfad8cf019): Disambiguate debug and verbose flags. - [@lcowell] -* Report 'new' at the bottom after fetching, it's going to be more relevant than 'unchanged', which includes all the languages they don't care about. +- [#151](https://github.com/exercism/cli/pull/151): Expand '~' in config path to home directory - [@lcowell] +- [#155](https://github.com/exercism/cli/pull/155): Display problems not yet submitted on fetch API - [@Tonkpils] +- [f999e69](https://github.com/exercism/cli/commit/f999e69e5290cec6c5c9933aecc6fddfad8cf019): Disambiguate debug and verbose flags. - [@lcowell] +- Report 'new' at the bottom after fetching, it's going to be more relevant than 'unchanged', which includes all the languages they don't care about. Tweaked: -* Set environment variable in build script -* [#153](https://github.com/exercism/cli/pull/153): Refactored configuration package - [@kytrinyx] -* [#157](https://github.com/exercism/cli/pull/157): Refactored API package - [@Tonkpils] +- Set environment variable in build script +- [#153](https://github.com/exercism/cli/pull/153): Refactored configuration package - [@kytrinyx] +- [#157](https://github.com/exercism/cli/pull/157): Refactored API package - [@Tonkpils] ## v1.9.2 (2015-01-11) -* [exercism.io#2155](https://github.com/exercism/exercism.io/issues/2155): Fixed problem with passed in config file being ignored. -* Added first version of changelog +- [exercism.io#2155](https://github.com/exercism/exercism.io/issues/2155): Fixed problem with passed in config file being ignored. +- Added first version of changelog ## v1.9.1 (2015-01-10) -* [#147](https://github.com/exercism/cli/pull/147): added `--api` option to exercism configure - [@morphatic] +- [#147](https://github.com/exercism/cli/pull/147): added `--api` option to exercism configure - [@morphatic] ## v1.9.0 (2014-11-27) -* [#143](https://github.com/exercism/cli/pull/143): added command for downloading a specific solution - [@harimp] -* [#142](https://github.com/exercism/cli/pull/142): fixed command name to be `exercism` rather than `cli` on `go get` - [@Tonkpils] +- [#143](https://github.com/exercism/cli/pull/143): added command for downloading a specific solution - [@harimp] +- [#142](https://github.com/exercism/cli/pull/142): fixed command name to be `exercism` rather than `cli` on `go get` - [@Tonkpils] ## v1.8.2 (2014-10-24) -* [9cbd069](https://github.com/exercism/cli/commit/9cbd06916cc05bbb165e8c2cb00d5e03cb4dbb99): Made path comparison case insensitive +- [9cbd069](https://github.com/exercism/cli/commit/9cbd06916cc05bbb165e8c2cb00d5e03cb4dbb99): Made path comparison case insensitive ## v1.8.1 (2014-10-23) -* [0ccc7a4](https://github.com/exercism/cli/commit/0ccc7a479940d2d7bb5e12eab41c91105519f135): Implemented debug flag on submit command +- [0ccc7a4](https://github.com/exercism/cli/commit/0ccc7a479940d2d7bb5e12eab41c91105519f135): Implemented debug flag on submit command ## v1.8.0 (2014-10-15) -* [#138](https://github.com/exercism/cli/pull/138): Added conversion to line endings for submissions on Windows - [@rprouse] -* [#116](https://github.com/exercism/cli/issues/116): Added support for setting name of config file in an environment variable -* [47d6fd4](https://github.com/exercism/cli/commit/47d6fd407fd0410f5c81d60172e01e8624608f53): Added a `track` command to list the problems in a given language -* [#126](https://github.com/exercism/cli/issues/126): Added explanation in `submit` response about fetching the next problems -* [#133](https://github.com/exercism/cli/pull/133): Changed config command to create the exercism directory, rather than waiting until the first time problems are fetched - [@Tonkpils] +- [#138](https://github.com/exercism/cli/pull/138): Added conversion to line endings for submissions on Windows - [@rprouse] +- [#116](https://github.com/exercism/cli/issues/116): Added support for setting name of config file in an environment variable +- [47d6fd4](https://github.com/exercism/cli/commit/47d6fd407fd0410f5c81d60172e01e8624608f53): Added a `track` command to list the problems in a given language +- [#126](https://github.com/exercism/cli/issues/126): Added explanation in `submit` response about fetching the next problems +- [#133](https://github.com/exercism/cli/pull/133): Changed config command to create the exercism directory, rather than waiting until the first time problems are fetched - [@Tonkpils] ## v1.7.5 (2014-10-5) -* [88cf1a1fbc884545dfc10e98535f667e4a43e693](https://github.com/exercism/cli/commit/88cf1a1fbc884545dfc10e98535f667e4a43e693): Added ARMv6 to build -* [12672c4](https://github.com/exercism/cli/commit/12672c4f695cfe3891f96467619a3615e6d57c34): Added an error message when people submit a file that is not within the exercism directory tree -* [#128](https://github.com/exercism/cli/pull/128): Made paths os-agnostic in tests - [@ccnp123] +- [88cf1a1fbc884545dfc10e98535f667e4a43e693](https://github.com/exercism/cli/commit/88cf1a1fbc884545dfc10e98535f667e4a43e693): Added ARMv6 to build +- [12672c4](https://github.com/exercism/cli/commit/12672c4f695cfe3891f96467619a3615e6d57c34): Added an error message when people submit a file that is not within the exercism directory tree +- [#128](https://github.com/exercism/cli/pull/128): Made paths os-agnostic in tests - [@ccnp123] ## v1.7.4 (2014-09-27) -* [4ca3e97](https://github.com/exercism/cli/commit/4ca3e9743f6d421903c91dfa27f4747fb1081392): Fixed incorrect HOME directory on Windows -* [8bd1a25](https://github.com/exercism/cli/commit/4ca3e9743f6d421903c91dfa27f4747fb1081392): Added ARMv5 to build -* [#117](https://github.com/exercism/cli/pull/117): Archive windows binaries using zip rather than tar and gzip - [@LegalizeAdulthood] +- [4ca3e97](https://github.com/exercism/cli/commit/4ca3e9743f6d421903c91dfa27f4747fb1081392): Fixed incorrect HOME directory on Windows +- [8bd1a25](https://github.com/exercism/cli/commit/4ca3e9743f6d421903c91dfa27f4747fb1081392): Added ARMv5 to build +- [#117](https://github.com/exercism/cli/pull/117): Archive windows binaries using zip rather than tar and gzip - [@LegalizeAdulthood] ## v1.7.3 (2014-09-26) -* [8bec393](https://github.com/exercism/cli/commit/8bec39387094680990af7cf438ada1780cf87129): Fixed submit so it can handle symlinks +- [8bec393](https://github.com/exercism/cli/commit/8bec39387094680990af7cf438ada1780cf87129): Fixed submit so it can handle symlinks ## v1.7.2 (2014-09-24) -* [#111](https://github.com/exercism/cli/pull/111): Don't clobber existing config values when adding more - [@jish] +- [#111](https://github.com/exercism/cli/pull/111): Don't clobber existing config values when adding more - [@jish] ## v1.7.1 (2014-09-19) -* Completely reorganized the code, separating each command into a separate handler -* [17fc164](https://github.com/exercism/cli/commit/17fc1644e9fc9ee5aa4e136de11556e65a7b6036): Fixed paths to be platform-independent -* [8b174e2](https://github.com/exercism/cli/commit/17fc1644e9fc9ee5aa4e136de11556e65a7b6036): Made the output of demo command more helpful -* [8b174e2](https://github.com/exercism/cli/commit/8b174e2fd8c7a545ea5c47c998ac10c5a7ab371f): Deleted the 'current' command +- Completely reorganized the code, separating each command into a separate handler +- [17fc164](https://github.com/exercism/cli/commit/17fc1644e9fc9ee5aa4e136de11556e65a7b6036): Fixed paths to be platform-independent +- [8b174e2](https://github.com/exercism/cli/commit/17fc1644e9fc9ee5aa4e136de11556e65a7b6036): Made the output of demo command more helpful +- [8b174e2](https://github.com/exercism/cli/commit/8b174e2fd8c7a545ea5c47c998ac10c5a7ab371f): Deleted the 'current' command ## v1.7.0 (2014-08-28) -* [ac6dbfd](https://github.com/exercism/cli/commit/ac6dbfd81a86e7a9a5a9b68521b0226c40d8e813): Added os and architecture to the user agent -* [5d58fd1](https://github.com/exercism/cli/commit/5d58fd14b9db84fb752b3bf6112123cd6f04c532): Fixed bug in detecting user's home directory -* [#100](https://github.com/exercism/cli/pull/100): Added 'debug' command, which supersedes the 'info' command - [@Tonkpils] -* Extracted a couple of commands into separate handlers -* [6ec5876](https://github.com/exercism/cli/commit/6ec5876bde0b02206cacbe685bb8aedcbdba25d4): Added a hack to rename old config files to the new default name -* [bb7d0d6](https://github.com/exercism/cli/commit/bb7d0d6151a950c92590dc771ec3ff5fdd1c83b0): Rename 'home' command to 'info' -* [#95](https://github.com/exercism/cli/issues/95): Added 'home' command -* Deprecate login/logout commands -* [1a39134](https://github.com/exercism/cli/commit/1a391342da93aa32ae398f1500a3981aa65b9f41): Changed demo to write exercises to the default exercism problems directory -* [07cc334](https://github.com/exercism/cli/commit/07cc334739465b21d6eb5d973e16e1c88f67758e): Deleted the whoami command, we weren't using github usernames for anything -* [#97](https://github.com/exercism/cli/pull/97): Changed default exercism directory to ~/exercism - [@lcowell] -* [#94](https://github.com/exercism/cli/pull/94): Updated language detection to handle C++ - [@LegalizeAdulthood] -* [#92](https://github.com/exercism/cli/pull/92): Renamed config json file to .exercism.json instead of .exercism.go - [@lcowell] -* [f55653f](https://github.com/exercism/cli/commit/f55653f35863914086a54375afb0898e142c1638): Deleted go vet from travis build temporarily until the codebase can be cleaned up -* [#91](https://github.com/exercism/cli/pull/91): Replaced temp file usage with encode/decode - [@lcowell] -* [#90](https://github.com/exercism/cli/pull/90): Added sanitization to config values to trim whitespace before writing it - [@lcowell] -* Did a fair amount of cleanup to make code a bit more idiomatic -* [#86](https://github.com/exercism/cli/pull/86): Triggered interactive login command for commands that require auth - [@Tonkpils] +- [ac6dbfd](https://github.com/exercism/cli/commit/ac6dbfd81a86e7a9a5a9b68521b0226c40d8e813): Added os and architecture to the user agent +- [5d58fd1](https://github.com/exercism/cli/commit/5d58fd14b9db84fb752b3bf6112123cd6f04c532): Fixed bug in detecting user's home directory +- [#100](https://github.com/exercism/cli/pull/100): Added 'debug' command, which supersedes the 'info' command - [@Tonkpils] +- Extracted a couple of commands into separate handlers +- [6ec5876](https://github.com/exercism/cli/commit/6ec5876bde0b02206cacbe685bb8aedcbdba25d4): Added a hack to rename old config files to the new default name +- [bb7d0d6](https://github.com/exercism/cli/commit/bb7d0d6151a950c92590dc771ec3ff5fdd1c83b0): Rename 'home' command to 'info' +- [#95](https://github.com/exercism/cli/issues/95): Added 'home' command +- Deprecate login/logout commands +- [1a39134](https://github.com/exercism/cli/commit/1a391342da93aa32ae398f1500a3981aa65b9f41): Changed demo to write exercises to the default exercism problems directory +- [07cc334](https://github.com/exercism/cli/commit/07cc334739465b21d6eb5d973e16e1c88f67758e): Deleted the whoami command, we weren't using github usernames for anything +- [#97](https://github.com/exercism/cli/pull/97): Changed default exercism directory to ~/exercism - [@lcowell] +- [#94](https://github.com/exercism/cli/pull/94): Updated language detection to handle C++ - [@LegalizeAdulthood] +- [#92](https://github.com/exercism/cli/pull/92): Renamed config json file to .exercism.json instead of .exercism.go - [@lcowell] +- [f55653f](https://github.com/exercism/cli/commit/f55653f35863914086a54375afb0898e142c1638): Deleted go vet from travis build temporarily until the codebase can be cleaned up +- [#91](https://github.com/exercism/cli/pull/91): Replaced temp file usage with encode/decode - [@lcowell] +- [#90](https://github.com/exercism/cli/pull/90): Added sanitization to config values to trim whitespace before writing it - [@lcowell] +- Did a fair amount of cleanup to make code a bit more idiomatic +- [#86](https://github.com/exercism/cli/pull/86): Triggered interactive login command for commands that require auth - [@Tonkpils] ## v1.6.2 (2014-06-02) -* [a5b7a55](https://github.com/exercism/cli/commit/a5b7a55f52c23ac5ce2c6bd1826ea7767aea38c4): Update login prompt +- [a5b7a55](https://github.com/exercism/cli/commit/a5b7a55f52c23ac5ce2c6bd1826ea7767aea38c4): Update login prompt ## v1.6.1 (2014-05-16) -* [#84](https://github.com/exercism/cli/pull/84): Change hard-coded filepath so that it will work on any platform - [@simonjefford] +- [#84](https://github.com/exercism/cli/pull/84): Change hard-coded filepath so that it will work on any platform - [@simonjefford] ## v1.6.0 (2014-05-10) -* [#82](https://github.com/exercism/cli/pull/82): Fixed typo in tests - [@srt32] -* [aa7446d](https://github.com/exercism/cli/commit/aa7446d598fc894ef329756555c48ef358baf676): Clarified output to user after they fetch -* [#79](https://github.com/exercism/cli/pull/79): Updated development instructions to fix permissions problem - [@andrewsardone] -* [#78](https://github.com/exercism/cli/pull/78): Deleted deprecated action `peek` - [@djquan] -* [#74](https://github.com/exercism/cli/pull/74): Implemented new option on `fetch` to get a single language - [@Tonkpils] -* [#75](https://github.com/exercism/cli/pull/75): Improved feedback to user after logging in - [@Tonkpils] -* [#72](https://github.com/exercism/cli/pull/72): Optimized use of temp file - [@Dparker1990] -* [#70](https://github.com/exercism/cli/pull/70): Fixed a panic - [@Tonkpils] -* [#68](https://github.com/exercism/cli/pull/68): Fixed how user input is read so that it doesn't stop at the first space - [@Tonkpils] +- [#82](https://github.com/exercism/cli/pull/82): Fixed typo in tests - [@srt32] +- [aa7446d](https://github.com/exercism/cli/commit/aa7446d598fc894ef329756555c48ef358baf676): Clarified output to user after they fetch +- [#79](https://github.com/exercism/cli/pull/79): Updated development instructions to fix permissions problem - [@andrewsardone] +- [#78](https://github.com/exercism/cli/pull/78): Deleted deprecated action `peek` - [@djquan] +- [#74](https://github.com/exercism/cli/pull/74): Implemented new option on `fetch` to get a single language - [@Tonkpils] +- [#75](https://github.com/exercism/cli/pull/75): Improved feedback to user after logging in - [@Tonkpils] +- [#72](https://github.com/exercism/cli/pull/72): Optimized use of temp file - [@Dparker1990] +- [#70](https://github.com/exercism/cli/pull/70): Fixed a panic - [@Tonkpils] +- [#68](https://github.com/exercism/cli/pull/68): Fixed how user input is read so that it doesn't stop at the first space - [@Tonkpils] ## v1.5.1 (2014-03-14) -* [5b672ee](https://github.com/exercism/cli/commit/5b672ee7bf26859c41de9eed83396b7454286063 ): Provided a visual mark next to new problems that get fetched +- [5b672ee](https://github.com/exercism/cli/commit/5b672ee7bf26859c41de9eed83396b7454286063): Provided a visual mark next to new problems that get fetched ## v1.5.0 (2014-02-28) -* [#63](https://github.com/exercism/cli/pull/63): Implemeted `fetch` for a single language - [@Tonkpils] -* [#62](https://github.com/exercism/cli/pull/62): Expose error message from API to user on `fetch` - [@Tonkpils] -* [#59](https://github.com/exercism/cli/pull/59): Added global flag to pass the path to the config file instead of relying on default - [@isbadawi] -* [#57](https://github.com/exercism/cli/pull/57): Added description to the restore command - [@rcode5] -* [#56](https://github.com/exercism/cli/pull/56): Updated developer instructions in README based on real-life experience - [@rcode5] +- [#63](https://github.com/exercism/cli/pull/63): Implemeted `fetch` for a single language - [@Tonkpils] +- [#62](https://github.com/exercism/cli/pull/62): Expose error message from API to user on `fetch` - [@Tonkpils] +- [#59](https://github.com/exercism/cli/pull/59): Added global flag to pass the path to the config file instead of relying on default - [@isbadawi] +- [#57](https://github.com/exercism/cli/pull/57): Added description to the restore command - [@rcode5] +- [#56](https://github.com/exercism/cli/pull/56): Updated developer instructions in README based on real-life experience - [@rcode5] ## v1.4.0 (2014-01-13) -* [#47](https://github.com/exercism/cli/pull/47): Added 'restore' command to download all of a user's existing solutions with their corresponding problems - [@ebautistabar] -* Numerous small fixes and cleanup to code and documentation - [@dpritchett], [@TrevorBramble], [@elimisteve] +- [#47](https://github.com/exercism/cli/pull/47): Added 'restore' command to download all of a user's existing solutions with their corresponding problems - [@ebautistabar] +- Numerous small fixes and cleanup to code and documentation - [@dpritchett], [@TrevorBramble], [@elimisteve] ## v1.3.2 (2013-12-14) -* [f8dd974](https://github.com/exercism/cli/commit/f8dd9748078b1b191629eae385aaeda8af94305b): Fixed content-type header when posting to API -* Fixed user-agent string +- [f8dd974](https://github.com/exercism/cli/commit/f8dd9748078b1b191629eae385aaeda8af94305b): Fixed content-type header when posting to API +- Fixed user-agent string ## v1.3.1 (2013-12-01) -* [exercism.io#1039](https://github.com/exercism/exercism.io/issues/1039): Stopped clobbering existing files on fetch +- [exercism.io#1039](https://github.com/exercism/exercism.io/issues/1039): Stopped clobbering existing files on fetch ## v1.3.0 (2013-11-16) -* [7f39ee4](https://github.com/exercism/cli/commit/7f39ee4802752925466bc2715790dc965026b09d): Allow users to specify a particular problem when fetching. +- [7f39ee4](https://github.com/exercism/cli/commit/7f39ee4802752925466bc2715790dc965026b09d): Allow users to specify a particular problem when fetching. ## v1.2.3 (2013-11-13) -* [exercism.io#998](https://github.com/exercism/exercism.io/issues/998): Fix problem with writing an empty config file under certain circumstances. +- [exercism.io#998](https://github.com/exercism/exercism.io/issues/998): Fix problem with writing an empty config file under certain circumstances. ## v1.2.2 (2013-11-12) -* [#28](https://github.com/exercism/cli/issues/28): Create exercism directory immediately upon logging in. -* Upgrade to newer version of [codegangsta/cli](https://github.com/codegansta/cli) library, which returns an error from the main Run() function. +- [#28](https://github.com/exercism/cli/issues/28): Create exercism directory immediately upon logging in. +- Upgrade to newer version of [codegangsta/cli](https://github.com/codegansta/cli) library, which returns an error from the main Run() function. ## v1.2.1 (2013-11-09) -* [371521f](https://github.com/exercism/cli/commit/371521fd97460aa92269831f10dadd467cb06592): Add support for nested directories under the language track directory allowing us to create idiomatic scala, clojure, and other exercises. +- [371521f](https://github.com/exercism/cli/commit/371521fd97460aa92269831f10dadd467cb06592): Add support for nested directories under the language track directory allowing us to create idiomatic scala, clojure, and other exercises. ## v1.2.0 (2013-11-07) -* [371521f](https://github.com/exercism/cli/commit/371521fd97460aa92269831f10dadd467cb06592): Consume the new hash of filename => content that the problem API returns. +- [371521f](https://github.com/exercism/cli/commit/371521fd97460aa92269831f10dadd467cb06592): Consume the new hash of filename => content that the problem API returns. ## v1.1.1 (2013-10-20) -* [371521f](https://github.com/exercism/cli/commit/371521fd97460aa92269831f10dadd467cb06592): Add output when fetching to tell the user where the files where created. +- [371521f](https://github.com/exercism/cli/commit/371521fd97460aa92269831f10dadd467cb06592): Add output when fetching to tell the user where the files where created. ## v1.1.0 (2013-10-24) -* Refactor to extract config package -* Delete stray binary **TODO** we might rewrite history on this one, see [#102](https://github.com/exercism/xgo/issues/102). -* [#22](https://github.com/exercism/cli/pull/22): Display submission url after submitting solution - [@Tonkpils] -* [#21](https://github.com/exercism/cli/pull/21): Add unsubmit command - [@Tonkpils] -* [#20](https://github.com/exercism/cli/pull/20): Add current command - [@Tonkpils] -* Inline refactoring experiment, various cleanup +- Refactor to extract config package +- Delete stray binary **TODO** we might rewrite history on this one, see [#102](https://github.com/exercism/xgo/issues/102). +- [#22](https://github.com/exercism/cli/pull/22): Display submission url after submitting solution - [@Tonkpils] +- [#21](https://github.com/exercism/cli/pull/21): Add unsubmit command - [@Tonkpils] +- [#20](https://github.com/exercism/cli/pull/20): Add current command - [@Tonkpils] +- Inline refactoring experiment, various cleanup ## v1.0.1 (2013-09-27) -* [#11](https://github.com/exercism/cli/pull/11): Don't require authentication for demo - [@msgehard] -* [#14](https://github.com/exercism/cli/pull/14): Print out fetched assignments - [@Tonkpils] -* [#16](https://github.com/exercism/cli/pull/16): Fix broken submit for relative path names - [@nf] -* Create a separate demo directory if there's no configured exercism directory +- [#11](https://github.com/exercism/cli/pull/11): Don't require authentication for demo - [@msgehard] +- [#14](https://github.com/exercism/cli/pull/14): Print out fetched assignments - [@Tonkpils] +- [#16](https://github.com/exercism/cli/pull/16): Fix broken submit for relative path names - [@nf] +- Create a separate demo directory if there's no configured exercism directory ## v1.0.0 (2013-09-22) -* [#7](https://github.com/exercism/cli/pull/7): Recognize haskell test files -* [#5](https://github.com/exercism/cli/pull/5): Fix typo - [@simonjefford] -* [#1](https://github.com/exercism/cli/pull/1): Output the location of the config file - [@msgehard] -* Recognize more language test files - [@msgehard] +- [#7](https://github.com/exercism/cli/pull/7): Recognize haskell test files +- [#5](https://github.com/exercism/cli/pull/5): Fix typo - [@simonjefford] +- [#1](https://github.com/exercism/cli/pull/1): Output the location of the config file - [@msgehard] +- Recognize more language test files - [@msgehard] ## v0.0.27.beta (2013-08-25) All changes by [@msgehard] -* Clean up homedir -* Add dev instructions to README +- Clean up homedir +- Add dev instructions to README ## v0.0.26.beta (2013-08-24) All changes by [@msgehard] -* Ensure that ruby gem's config file doesn't get clobbered -* Add cross-compilation -* Set proper User-Agent so server doesn't blow up. -* Implement `submit` -* Implement `demo` -* Implement `peek` -* Expand ~ in config -* Implement `fetch` -* Implement `current` -* Implement `whoami` -* Implement login and logout -* Build on Travis +- Ensure that ruby gem's config file doesn't get clobbered +- Add cross-compilation +- Set proper User-Agent so server doesn't blow up. +- Implement `submit` +- Implement `demo` +- Implement `peek` +- Expand ~ in config +- Implement `fetch` +- Implement `current` +- Implement `whoami` +- Implement login and logout +- Build on Travis [@AlexWheeler]: https://github.com/AlexWheeler [@andrerfcsantos]: https://github.com/andrerfcsantos diff --git a/cmd/version.go b/cmd/version.go index 260afbb79..300ba8f72 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.4.0" +const Version = "3.4.1" // checkLatest flag for version command. var checkLatest bool From b43bd3724601780363c5069f14cec286c47a2c62 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 15 Aug 2024 15:42:10 +0200 Subject: [PATCH 499/544] Add version to goreleaser file (#1155) --- .goreleaser.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.goreleaser.yml b/.goreleaser.yml index b7491e0b8..1995cf6b4 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,4 +1,5 @@ # You can find the GoReleaser documentation at http://goreleaser.com +version: 2 project_name: exercism env: From 20d58b1faedf346f3a0135ccd44fd8c8121415ee Mon Sep 17 00:00:00 2001 From: Yukai Chou Date: Tue, 20 Aug 2024 19:03:44 +0800 Subject: [PATCH 500/544] Add `test` command to Shell completions (#1156) `test` command was supported since v3.2.0, see a8ffc31 (Add `test` command to run any unit test (\#1092), 2023-07-28) --- shell/exercism.fish | 6 +++++- shell/exercism_completion.bash | 2 +- shell/exercism_completion.zsh | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/shell/exercism.fish b/shell/exercism.fish index dc20fdce0..28702a48f 100644 --- a/shell/exercism.fish +++ b/shell/exercism.fish @@ -15,7 +15,7 @@ complete -f -c exercism -n "__fish_seen_subcommand_from download" -s u -l uuid - # Help complete -f -c exercism -n "__fish_use_subcommand" -a "help" -d "Shows a list of commands or help for one command" -complete -f -c exercism -n "__fish_seen_subcommand_from help" -a "configure download help open submit troubleshoot upgrade version workspace" +complete -f -c exercism -n "__fish_seen_subcommand_from help" -a "configure download help open submit test troubleshoot upgrade version workspace" # Open complete -f -c exercism -n "__fish_use_subcommand" -a "open" -d "Opens a browser to exercism.io for the specified submission." @@ -25,6 +25,10 @@ complete -f -c exercism -n "__fish_seen_subcommand_from open" -s h -l help -d "h complete -f -c exercism -n "__fish_use_subcommand" -a "submit" -d "Submits a new iteration to a problem on exercism.io." complete -f -c exercism -n "__fish_seen_subcommand_from submit" -s h -l help -d "help for submit" +# Test +complete -f -c exercism -n "__fish_use_subcommand" -a "test" -d "Run the exercise's tests." +complete -f -c exercism -n "__fish_seen_subcommand_from submit" -s h -l help -d "help for test" + # Troubleshoot complete -f -c exercism -n "__fish_use_subcommand" -a "troubleshoot" -d "Outputs useful debug information." complete -f -c exercism -n "__fish_seen_subcommand_from troubleshoot" -s f -l full-api-key -d "display full API key (censored by default)" diff --git a/shell/exercism_completion.bash b/shell/exercism_completion.bash index f5f4e9010..6864ec153 100644 --- a/shell/exercism_completion.bash +++ b/shell/exercism_completion.bash @@ -7,7 +7,7 @@ _exercism () { opts="--verbose --timeout" commands="configure download open - submit troubleshoot upgrade version workspace help" + submit test troubleshoot upgrade version workspace help" config_opts="--show" version_opts="--latest" diff --git a/shell/exercism_completion.zsh b/shell/exercism_completion.zsh index 424b24e67..5d476ac66 100644 --- a/shell/exercism_completion.zsh +++ b/shell/exercism_completion.zsh @@ -8,6 +8,7 @@ options=(configure:"Writes config values to a JSON file." download:"Downloads and saves a specified submission into the local system" open:"Opens a browser to exercism.io for the specified submission." submit:"Submits a new iteration to a problem on exercism.io." + test:"Run the exercise's tests." troubleshoot:"Outputs useful debug information." upgrade:"Upgrades to the latest available version." version:"Outputs version information." From 9c0b6c5f3d8d5dd6fd242d5ec07a1a86c39dc405 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Tue, 20 Aug 2024 13:08:20 +0200 Subject: [PATCH 501/544] Bump version to v3.4.2 (#1158) --- CHANGELOG.md | 5 +++++ cmd/version.go | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6398a756a..959baeab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ The exercism CLI follows [semantic versioning](http://semver.org/). - **Your contribution here** +## v3.4.2 (2024-08-20) + +- [#1156](https://github.com/exercism/cli/pull/1156) Add `test` command to Shell completions - + [@muzimuzhi] + ## v3.4.1 (2024-08-15) - [#1152](https://github.com/exercism/cli/pull/1152) Add support for Idris to `exercism test` - diff --git a/cmd/version.go b/cmd/version.go index 300ba8f72..147c8e0b9 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.4.1" +const Version = "3.4.2" // checkLatest flag for version command. var checkLatest bool From e3f9fecbdc9180504d40cc6ea5e5446d207eedc3 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Tue, 20 Aug 2024 13:32:23 +0200 Subject: [PATCH 502/544] Fix env variable typo in release doc (#1160) --- RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index 35a02dbf1..d5ba8ad4a 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -24,7 +24,7 @@ _Note: It's useful to add the version to the commit message when you bump it: e. Once the version bump PR has been merged, run the following command to cut a release: ```shell -GPG_FINGERPINT="" ./bin/release.sh +GPG_FINGERPRINT="" ./bin/release.sh ``` ## Cut Release on GitHub From f47c9b412d66e9432d15676bc540af098684c49a Mon Sep 17 00:00:00 2001 From: Murat Kirazkaya <77299279+GroophyLifefor@users.noreply.github.com> Date: Wed, 21 Aug 2024 09:16:07 +0300 Subject: [PATCH 503/544] Updated test_configurations.go for new batch track (#1157) * Update test_configurations.go * Update test_configurations.go --------- Co-authored-by: Erik Schierboom --- workspace/test_configurations.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index f5bb29019..f0cbd174b 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -74,6 +74,9 @@ var TestConfigurations = map[string]TestConfiguration{ "ballerina": { Command: "bal test", }, + "batch": { + WindowsCommand: "call {{test_files}}", + }, "bash": { Command: "bats {{test_files}}", }, From 8951e55dddf1e3482ec8ff1cc6e00862c6e8b27b Mon Sep 17 00:00:00 2001 From: Yukai Chou Date: Wed, 21 Aug 2024 18:24:14 +0800 Subject: [PATCH 504/544] Fix dup alias "t", keep it for "test" only (#1159) Command alias "t" was registered by both "troubleshoot" and "test" commands. This PR drops it from "troubleshoot" aliases. Co-authored-by: Erik Schierboom --- cmd/troubleshoot.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go index 4ea5b1444..2c599d6d5 100644 --- a/cmd/troubleshoot.go +++ b/cmd/troubleshoot.go @@ -21,7 +21,7 @@ var fullAPIKey bool // troubleshootCmd does a diagnostic self-check. var troubleshootCmd = &cobra.Command{ Use: "troubleshoot", - Aliases: []string{"t", "debug"}, + Aliases: []string{"debug"}, Short: "Troubleshoot does a diagnostic self-check.", Long: `Provides output to help with troubleshooting. From bdbcc49fac3496ef92d1ac24d715a6d3ec76384e Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 22 Aug 2024 15:56:35 +0200 Subject: [PATCH 505/544] Bump to version 3.5.0 (#1161) --- CHANGELOG.md | 15 +++++++++++++++ cmd/version.go | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 959baeab4..03cd7ed6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ The exercism CLI follows [semantic versioning](http://semver.org/). - **Your contribution here** +## v3.5.0 (2024-08-22) + +- [#1157](https://github.com/exercism/cli/pull/1157) Add support for Batch to `exercism test` - [@GroophyLifefor] +- [#1159](https://github.com/exercism/cli/pull/1159) Fix duplicated `t` alias - + [@muzimuzhi] + ## v3.4.2 (2024-08-20) - [#1156](https://github.com/exercism/cli/pull/1156) Add `test` command to Shell completions - @@ -543,3 +549,12 @@ All changes by [@msgehard] [@xavdid]: https://github.com/xavdid [@williandrade]: https://github.com/williandrade [@zabawaba99]: https://github.com/zabawaba99 +[@GroophyLifefor]: https://github.com/GroophyLifefor +[@muzimuzhi]: https://github.com/muzimuzhi +[@isberg]: https://github.com/isberg +[@erikschierboom]: https://github.com/erikschierboom +[@sanderploegsma]: https://github.com/sanderploegsma +[@enascimento178]: https://github.com/enascimento178 +[@halfdan]: https://github.com/halfdan +[@glennj]: https://github.com/glennj +[@tomasnorre]: https://github.com/tomasnorre diff --git a/cmd/version.go b/cmd/version.go index 147c8e0b9..cc6644b4c 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.4.2" +const Version = "3.5.0" // checkLatest flag for version command. var checkLatest bool From 28d1c6d1b6ce675581662d9faf6434a7e9f1d801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Geron?= Date: Wed, 28 Aug 2024 21:40:03 +1200 Subject: [PATCH 506/544] Add support for the Roc language (#1162) --- workspace/test_configurations.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index f0cbd174b..9fd549e26 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -225,6 +225,9 @@ var TestConfigurations = map[string]TestConfiguration{ "red": { Command: "red {{test_files}}", }, + "roc": { + Command: "roc test {{test_files}}", + }, "ruby": { Command: "ruby {{test_files}}", }, From fe08d1879e820d4133ea87e9e8b2a913fb53727d Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Wed, 28 Aug 2024 11:45:25 +0200 Subject: [PATCH 507/544] Bump version to v3.5.1 (#1163) --- CHANGELOG.md | 5 +++++ cmd/version.go | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03cd7ed6b..83f6c0443 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ The exercism CLI follows [semantic versioning](http://semver.org/). - **Your contribution here** +## v3.5.1 (2024-08-28) + +- [#1162](https://github.com/exercism/cli/pull/1162) Add support for Roc to `exercism test` - [@ageron] + ## v3.5.0 (2024-08-22) - [#1157](https://github.com/exercism/cli/pull/1157) Add support for Batch to `exercism test` - [@GroophyLifefor] @@ -558,3 +562,4 @@ All changes by [@msgehard] [@halfdan]: https://github.com/halfdan [@glennj]: https://github.com/glennj [@tomasnorre]: https://github.com/tomasnorre +[@ageron]: https://github.com/ageron diff --git a/cmd/version.go b/cmd/version.go index cc6644b4c..9d2e226bb 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.5.0" +const Version = "3.5.1" // checkLatest flag for version command. var checkLatest bool From 7428fd467247169deada18979eb5ee0a4c69318f Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Wed, 28 Aug 2024 12:27:15 +0200 Subject: [PATCH 508/544] Add release workflow to release document (#1164) --- RELEASE.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index d5ba8ad4a..8feda68f0 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -29,8 +29,9 @@ GPG_FINGERPRINT="" ./bin/release.sh ## Cut Release on GitHub -At this point, Goreleaser will have created a draft release at https://github.com/exercism/cli/releases/tag/vX.Y.Z. -On that page, update the release description to: +Once the `./bin/release.sh` command finishes, the [release workflow](https://github.com/exercism/cli/actions/workflows/release.yml) will automatically run. +This workflow will create a draft release at https://github.com/exercism/cli/releases/tag/vX.Y.Z. +Once created, go that page to update the release description to: ``` To install, follow the interactive installation instructions at https://exercism.io/cli-walkthrough From 102aeb8f07d691b352412c8a80f7626656ae2f7e Mon Sep 17 00:00:00 2001 From: 521337 <57076905+521337@users.noreply.github.com> Date: Sun, 15 Sep 2024 00:55:50 +0000 Subject: [PATCH 509/544] Fix URL in no token error message (#1166) Update Exercism API endpoint URL. --- cmd/download_test.go | 2 +- cmd/submit_test.go | 2 +- config/config.go | 6 +++--- config/config_test.go | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index d95729719..a3e565831 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -26,7 +26,7 @@ func TestDownloadWithoutToken(t *testing.T) { if assert.Error(t, err) { assert.Regexp(t, "Welcome to Exercism", err.Error()) // It uses the default base API url to infer the host - assert.Regexp(t, "exercism.io/my/settings", err.Error()) + assert.Regexp(t, "exercism.org/settings/api_cli", err.Error()) } } diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 2a12d0bca..c974794c8 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -28,7 +28,7 @@ func TestSubmitWithoutToken(t *testing.T) { err := runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) if assert.Error(t, err) { assert.Regexp(t, "Welcome to Exercism", err.Error()) - assert.Regexp(t, "exercism.io/my/settings", err.Error()) + assert.Regexp(t, "exercism.org/settings/api_cli", err.Error()) } } diff --git a/config/config.go b/config/config.go index b9b1883ba..e70595054 100644 --- a/config/config.go +++ b/config/config.go @@ -12,7 +12,7 @@ import ( ) var ( - defaultBaseURL = "https://api.exercism.io/v1" + defaultBaseURL = "https://exercism.org" // DefaultDirName is the default name used for config and workspace directories. DefaultDirName string @@ -122,7 +122,7 @@ func InferSiteURL(apiURL string) string { apiURL = defaultBaseURL } if apiURL == "https://api.exercism.io/v1" { - return "https://exercism.io" + return "https://exercism.org" } re := regexp.MustCompile("^(https?://[^/]*).*") return re.ReplaceAllString(apiURL, "$1") @@ -130,5 +130,5 @@ func InferSiteURL(apiURL string) string { // SettingsURL provides a link to where the user can find their API token. func SettingsURL(apiURL string) string { - return fmt.Sprintf("%s%s", InferSiteURL(apiURL), "/my/settings") + return fmt.Sprintf("%s%s", InferSiteURL(apiURL), "/settings/api_cli") } diff --git a/config/config_test.go b/config/config_test.go index e9d29f97f..2df6ce1fd 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -10,11 +10,11 @@ func TestInferSiteURL(t *testing.T) { testCases := []struct { api, url string }{ - {"https://api.exercism.io/v1", "https://exercism.io"}, + {"https://api.exercism.io/v1", "https://exercism.org"}, {"https://v2.exercism.io/api/v1", "https://v2.exercism.io"}, {"https://mentors-beta.exercism.io/api/v1", "https://mentors-beta.exercism.io"}, {"http://localhost:3000/api/v1", "http://localhost:3000"}, - {"", "https://exercism.io"}, // use the default + {"", "https://exercism.org"}, // use the default {"http://whatever", "http://whatever"}, // you're on your own, pal } From c3738a8dd583c7257f459af288837b3139599613 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Tue, 17 Sep 2024 08:41:04 +0200 Subject: [PATCH 510/544] Change the interval for dependabot updates to monthly (#1167) --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index eb2ead799..5c6cb5943 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,6 +5,6 @@ updates: - package-ecosystem: 'github-actions' directory: '/' schedule: - interval: 'daily' + interval: 'monthly' labels: - 'x:size/small' From 62d9851793d31968909af9ecf839973acde2007e Mon Sep 17 00:00:00 2001 From: Isaac Good Date: Wed, 18 Sep 2024 11:03:28 -0700 Subject: [PATCH 511/544] Revert "Fix URL in no token error message (#1166)" (#1173) This reverts commit 102aeb8f07d691b352412c8a80f7626656ae2f7e. --- cmd/download_test.go | 2 +- cmd/submit_test.go | 2 +- config/config.go | 6 +++--- config/config_test.go | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index a3e565831..d95729719 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -26,7 +26,7 @@ func TestDownloadWithoutToken(t *testing.T) { if assert.Error(t, err) { assert.Regexp(t, "Welcome to Exercism", err.Error()) // It uses the default base API url to infer the host - assert.Regexp(t, "exercism.org/settings/api_cli", err.Error()) + assert.Regexp(t, "exercism.io/my/settings", err.Error()) } } diff --git a/cmd/submit_test.go b/cmd/submit_test.go index c974794c8..2a12d0bca 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -28,7 +28,7 @@ func TestSubmitWithoutToken(t *testing.T) { err := runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) if assert.Error(t, err) { assert.Regexp(t, "Welcome to Exercism", err.Error()) - assert.Regexp(t, "exercism.org/settings/api_cli", err.Error()) + assert.Regexp(t, "exercism.io/my/settings", err.Error()) } } diff --git a/config/config.go b/config/config.go index e70595054..b9b1883ba 100644 --- a/config/config.go +++ b/config/config.go @@ -12,7 +12,7 @@ import ( ) var ( - defaultBaseURL = "https://exercism.org" + defaultBaseURL = "https://api.exercism.io/v1" // DefaultDirName is the default name used for config and workspace directories. DefaultDirName string @@ -122,7 +122,7 @@ func InferSiteURL(apiURL string) string { apiURL = defaultBaseURL } if apiURL == "https://api.exercism.io/v1" { - return "https://exercism.org" + return "https://exercism.io" } re := regexp.MustCompile("^(https?://[^/]*).*") return re.ReplaceAllString(apiURL, "$1") @@ -130,5 +130,5 @@ func InferSiteURL(apiURL string) string { // SettingsURL provides a link to where the user can find their API token. func SettingsURL(apiURL string) string { - return fmt.Sprintf("%s%s", InferSiteURL(apiURL), "/settings/api_cli") + return fmt.Sprintf("%s%s", InferSiteURL(apiURL), "/my/settings") } diff --git a/config/config_test.go b/config/config_test.go index 2df6ce1fd..e9d29f97f 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -10,11 +10,11 @@ func TestInferSiteURL(t *testing.T) { testCases := []struct { api, url string }{ - {"https://api.exercism.io/v1", "https://exercism.org"}, + {"https://api.exercism.io/v1", "https://exercism.io"}, {"https://v2.exercism.io/api/v1", "https://v2.exercism.io"}, {"https://mentors-beta.exercism.io/api/v1", "https://mentors-beta.exercism.io"}, {"http://localhost:3000/api/v1", "http://localhost:3000"}, - {"", "https://exercism.org"}, // use the default + {"", "https://exercism.io"}, // use the default {"http://whatever", "http://whatever"}, // you're on your own, pal } From a581e6a2c7285e57209205d341b9978369777be8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 19:36:11 -0700 Subject: [PATCH 512/544] Bump actions/setup-go from 4.1.0 to 5.0.2 (#1168) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4.1.0 to 5.0.2. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/93397bea11091df50f3d7e59dc26a7711a8bcfbe...0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4510d825..fbf2b030d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: steps: - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 with: go-version: ${{ matrix.go-version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b74d61e58..e946c5100 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version: '1.20.x' From 50e8b0eab2f0f9ea032d2c643bab884e50643330 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 19:40:02 -0700 Subject: [PATCH 513/544] Bump goreleaser/goreleaser-action from 5.0.0 to 6.0.0 (#1169) Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 5.0.0 to 6.0.0. - [Release notes](https://github.com/goreleaser/goreleaser-action/releases) - [Commits](https://github.com/goreleaser/goreleaser-action/compare/7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8...286f3b13b1b49da4ac219696163fb8c1c93e1200) --- updated-dependencies: - dependency-name: goreleaser/goreleaser-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e946c5100..b5bd2b21c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,7 +32,7 @@ jobs: passphrase: ${{ secrets.PASSPHRASE }} - name: Cut Release - uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5.0.0 + uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 with: version: latest args: release --clean --release-header .release/header.md --timeout 120m # default time is 30m From 6779bed32e700182f7efb68bac575366053f1a5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 19:42:53 -0700 Subject: [PATCH 514/544] Bump actions/checkout from 4.1.0 to 4.1.7 (#1171) Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.0 to 4.1.7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/8ade135a41bc03ea155e62e844d188df1ea18608...692973e3d937129bcbf40652eb9f2f61becf3332) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbf2b030d..05aa6a748 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: os: [ubuntu-latest, windows-latest, macOS-latest] steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 with: @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 - name: Check formatting run: ./.gha.gofmt.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b5bd2b21c..a8d026f7a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 From 3daad45b111a17ab7f20b4e6ea88795e5a2d96c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 19:50:12 -0700 Subject: [PATCH 515/544] Bump crazy-max/ghaction-import-gpg from 6.0.0 to 6.1.0 (#1170) Bumps [crazy-max/ghaction-import-gpg](https://github.com/crazy-max/ghaction-import-gpg) from 6.0.0 to 6.1.0. - [Release notes](https://github.com/crazy-max/ghaction-import-gpg/releases) - [Commits](https://github.com/crazy-max/ghaction-import-gpg/compare/82a020f1f7f605c65dd2449b392a52c3fcfef7ef...01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4) --- updated-dependencies: - dependency-name: crazy-max/ghaction-import-gpg dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8d026f7a..599f23bdb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: - name: Import GPG Key id: import_gpg - uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0 + uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.PASSPHRASE }} From 0706bdbd7f2a5a1df57e2e5419e3647a6366077b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20B=20Nagy?= <20251272+BNAndras@users.noreply.github.com> Date: Fri, 20 Sep 2024 00:33:55 -0700 Subject: [PATCH 516/544] Fix batch command (#1172) Co-authored-by: Isaac Good Co-authored-by: Erik Schierboom --- workspace/test_configurations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index 9fd549e26..6c5caff62 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -75,7 +75,7 @@ var TestConfigurations = map[string]TestConfiguration{ Command: "bal test", }, "batch": { - WindowsCommand: "call {{test_files}}", + WindowsCommand: "cmd /c {{test_files}}", }, "bash": { Command: "bats {{test_files}}", From d3b5d7f6ed706ebf0c2d11327c81aad9b25cbe80 Mon Sep 17 00:00:00 2001 From: PetreM Date: Wed, 9 Oct 2024 13:35:38 +0300 Subject: [PATCH 517/544] Fix BinaryName initialization from args (#1174) Fixes an issue with `completion bash` where the command name is not present in the completion output. --- cmd/root.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 35d266356..d94d5c847 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -14,7 +14,7 @@ import ( // RootCmd represents the base command when called without any subcommands. var RootCmd = &cobra.Command{ - Use: BinaryName, + Use: getCommandName(), Short: "A friendly command-line interface to Exercism.", Long: `A command-line interface for Exercism. @@ -41,8 +41,12 @@ func Execute() { } } +func getCommandName() string { + return os.Args[0] +} + func init() { - BinaryName = os.Args[0] + BinaryName = getCommandName() config.SetDefaultDirName(BinaryName) Out = os.Stdout Err = os.Stderr From 24276b7dba306ade75404b998017911ac7600569 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Wed, 9 Oct 2024 12:41:18 +0200 Subject: [PATCH 518/544] Bump version to v.3.5.2 (#1176) --- CHANGELOG.md | 7 +++++++ cmd/version.go | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83f6c0443..5d98f969a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ The exercism CLI follows [semantic versioning](http://semver.org/). - **Your contribution here** +## v3.5.2 (2024-10-09) + +- [#1174](https://github.com/exercism/cli/pull/1174) Fix an issue with `exercism completion bash` where the command name is not present in the completion output. - [@petrem] +- [#1172](https://github.com/exercism/cli/pull/1172) Fix `exercism test` command for Batch track - [@bnandras] + ## v3.5.1 (2024-08-28) - [#1162](https://github.com/exercism/cli/pull/1162) Add support for Roc to `exercism test` - [@ageron] @@ -563,3 +568,5 @@ All changes by [@msgehard] [@glennj]: https://github.com/glennj [@tomasnorre]: https://github.com/tomasnorre [@ageron]: https://github.com/ageron +[@petrem]: https://github.com/petrem +[@bnandras]: https://github.com/bnandras diff --git a/cmd/version.go b/cmd/version.go index 9d2e226bb..73a20bf04 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.5.1" +const Version = "3.5.2" // checkLatest flag for version command. var checkLatest bool From 0a329255e8ce989b3168525d04384926215b8b1b Mon Sep 17 00:00:00 2001 From: keiravillekode Date: Sat, 26 Oct 2024 11:07:24 +1100 Subject: [PATCH 519/544] Add arm64-assembly test configuration (#1178) --- workspace/test_configurations.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index 6c5caff62..a1fe1cccb 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -65,6 +65,9 @@ var TestConfigurations = map[string]TestConfiguration{ Command: "8th -f test.8th", }, // abap: tests are run via "ABAP Development Tools", not the CLI + "arm64-assembly": { + Command: "make", + }, "arturo": { Command: "arturo tester.art", }, From efd43c4e794a1bb7f658ecf0f73ed29bb861ac6d Mon Sep 17 00:00:00 2001 From: Patrick Kodal <6306551+ladokp@users.noreply.github.com> Date: Sun, 3 Nov 2024 16:17:07 +0100 Subject: [PATCH 520/544] refactored exercism.io to exercism.org (#1177) * refactored exercism.io to exercism.org * refactored exercism.io github references --- .release/header.md | 2 +- CHANGELOG.md | 8 ++++---- README.md | 2 +- RELEASE.md | 4 ++-- cmd/download_test.go | 2 +- cmd/submit_test.go | 2 +- config/config.go | 6 +++--- config/config_test.go | 8 ++++---- exercism/doc.go | 2 +- shell/exercism.fish | 4 ++-- shell/exercism_completion.zsh | 4 ++-- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.release/header.md b/.release/header.md index 55027b78c..c648e2d8b 100644 --- a/.release/header.md +++ b/.release/header.md @@ -1,3 +1,3 @@ -To install, follow the interactive installation instructions at https://exercism.io/cli-walkthrough +To install, follow the interactive installation instructions at https://exercism.org/cli-walkthrough --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d98f969a..b12f04f2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -153,7 +153,7 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ## v3.0.0 (2018-07-13) -This is a complete rewrite from the ground up to work against the new https://exercism.io site. +This is a complete rewrite from the ground up to work against the new https://exercism.org site. ## v2.4.1 (2017-07-01) @@ -299,7 +299,7 @@ Tweaked: ## v1.9.2 (2015-01-11) -- [exercism.io#2155](https://github.com/exercism/exercism.io/issues/2155): Fixed problem with passed in config file being ignored. +- [exercism#2155](https://github.com/exercism/exercism/issues/2155): Fixed problem with passed in config file being ignored. - Added first version of changelog ## v1.9.1 (2015-01-10) @@ -419,7 +419,7 @@ Tweaked: ## v1.3.1 (2013-12-01) -- [exercism.io#1039](https://github.com/exercism/exercism.io/issues/1039): Stopped clobbering existing files on fetch +- [exercism#1039](https://github.com/exercism/exercism/issues/1039): Stopped clobbering existing files on fetch ## v1.3.0 (2013-11-16) @@ -427,7 +427,7 @@ Tweaked: ## v1.2.3 (2013-11-13) -- [exercism.io#998](https://github.com/exercism/exercism.io/issues/998): Fix problem with writing an empty config file under certain circumstances. +- [exercism#998](https://github.com/exercism/exercism/issues/998): Fix problem with writing an empty config file under certain circumstances. ## v1.2.2 (2013-11-12) diff --git a/README.md b/README.md index c92650a85..8b0fa92c6 100644 --- a/README.md +++ b/README.md @@ -15,5 +15,5 @@ Instructions can be found at [exercism/cli/releases](https://github.com/exercism If you wish to help improve the CLI, please see the [Contributing guide][contributing]. -[exercism]: http://exercism.io +[exercism]: http://exercism.org [contributing]: /CONTRIBUTING.md diff --git a/RELEASE.md b/RELEASE.md index 8feda68f0..289c78822 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -34,7 +34,7 @@ This workflow will create a draft release at https://github.com/exercism/cli/rel Once created, go that page to update the release description to: ``` -To install, follow the interactive installation instructions at https://exercism.io/cli-walkthrough +To install, follow the interactive installation instructions at https://exercism.org/cli-walkthrough --- [modify the generated release-notes to describe changes in this release] @@ -49,6 +49,6 @@ Homebrew will automatically bump the version, no manual action is required. ## Update the docs site If there are any significant changes, we should describe them on -[exercism.io/cli](https://exercism.io/cli). +[exercism.org/cli](https://exercism.org/cli). The codebase lives at [exercism/website-copy](https://github.com/exercism/website-copy) in `pages/cli.md`. diff --git a/cmd/download_test.go b/cmd/download_test.go index d95729719..aadcf84aa 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -26,7 +26,7 @@ func TestDownloadWithoutToken(t *testing.T) { if assert.Error(t, err) { assert.Regexp(t, "Welcome to Exercism", err.Error()) // It uses the default base API url to infer the host - assert.Regexp(t, "exercism.io/my/settings", err.Error()) + assert.Regexp(t, "exercism.org/my/settings", err.Error()) } } diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 2a12d0bca..9b645965b 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -28,7 +28,7 @@ func TestSubmitWithoutToken(t *testing.T) { err := runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) if assert.Error(t, err) { assert.Regexp(t, "Welcome to Exercism", err.Error()) - assert.Regexp(t, "exercism.io/my/settings", err.Error()) + assert.Regexp(t, "exercism.org/my/settings", err.Error()) } } diff --git a/config/config.go b/config/config.go index b9b1883ba..e1cc37e08 100644 --- a/config/config.go +++ b/config/config.go @@ -12,7 +12,7 @@ import ( ) var ( - defaultBaseURL = "https://api.exercism.io/v1" + defaultBaseURL = "https://api.exercism.org/v1" // DefaultDirName is the default name used for config and workspace directories. DefaultDirName string @@ -121,8 +121,8 @@ func InferSiteURL(apiURL string) string { if apiURL == "" { apiURL = defaultBaseURL } - if apiURL == "https://api.exercism.io/v1" { - return "https://exercism.io" + if apiURL == "https://api.exercism.org/v1" { + return "https://exercism.org" } re := regexp.MustCompile("^(https?://[^/]*).*") return re.ReplaceAllString(apiURL, "$1") diff --git a/config/config_test.go b/config/config_test.go index e9d29f97f..40ecb0c36 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -10,11 +10,11 @@ func TestInferSiteURL(t *testing.T) { testCases := []struct { api, url string }{ - {"https://api.exercism.io/v1", "https://exercism.io"}, - {"https://v2.exercism.io/api/v1", "https://v2.exercism.io"}, - {"https://mentors-beta.exercism.io/api/v1", "https://mentors-beta.exercism.io"}, + {"https://api.exercism.org/v1", "https://exercism.org"}, + {"https://v2.exercism.org/api/v1", "https://v2.exercism.org"}, + {"https://mentors-beta.exercism.org/api/v1", "https://mentors-beta.exercism.org"}, {"http://localhost:3000/api/v1", "http://localhost:3000"}, - {"", "https://exercism.io"}, // use the default + {"", "https://exercism.org"}, // use the default {"http://whatever", "http://whatever"}, // you're on your own, pal } diff --git a/exercism/doc.go b/exercism/doc.go index a60d6e7b2..3e00d9015 100644 --- a/exercism/doc.go +++ b/exercism/doc.go @@ -1,5 +1,5 @@ /* -Command exercism allows users to interact with the exercism.io platform. +Command exercism allows users to interact with the exercism.org platform. The primary actions are to fetch problems to be solved, and submit iterations of these problems. diff --git a/shell/exercism.fish b/shell/exercism.fish index 28702a48f..c2c7c0811 100644 --- a/shell/exercism.fish +++ b/shell/exercism.fish @@ -18,11 +18,11 @@ complete -f -c exercism -n "__fish_use_subcommand" -a "help" -d "Shows a list of complete -f -c exercism -n "__fish_seen_subcommand_from help" -a "configure download help open submit test troubleshoot upgrade version workspace" # Open -complete -f -c exercism -n "__fish_use_subcommand" -a "open" -d "Opens a browser to exercism.io for the specified submission." +complete -f -c exercism -n "__fish_use_subcommand" -a "open" -d "Opens a browser to exercism.org for the specified submission." complete -f -c exercism -n "__fish_seen_subcommand_from open" -s h -l help -d "help for open" # Submit -complete -f -c exercism -n "__fish_use_subcommand" -a "submit" -d "Submits a new iteration to a problem on exercism.io." +complete -f -c exercism -n "__fish_use_subcommand" -a "submit" -d "Submits a new iteration to a problem on exercism.org." complete -f -c exercism -n "__fish_seen_subcommand_from submit" -s h -l help -d "help for submit" # Test diff --git a/shell/exercism_completion.zsh b/shell/exercism_completion.zsh index 5d476ac66..542c8a576 100644 --- a/shell/exercism_completion.zsh +++ b/shell/exercism_completion.zsh @@ -6,8 +6,8 @@ typeset -A opt_args local -a options options=(configure:"Writes config values to a JSON file." download:"Downloads and saves a specified submission into the local system" - open:"Opens a browser to exercism.io for the specified submission." - submit:"Submits a new iteration to a problem on exercism.io." + open:"Opens a browser to exercism.org for the specified submission." + submit:"Submits a new iteration to a problem on exercism.org." test:"Run the exercise's tests." troubleshoot:"Outputs useful debug information." upgrade:"Upgrades to the latest available version." From 26c5d34b1e301e7ef289d6f06d5dcde6c049313a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingy=20d=C3=B6t=20Net?= Date: Sun, 3 Nov 2024 08:35:53 -0800 Subject: [PATCH 521/544] Add support for the YAMLScript language (#1165) Co-authored-by: Glenn Jackman --- workspace/test_configurations.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index a1fe1cccb..90a7a1670 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -270,6 +270,9 @@ var TestConfigurations = map[string]TestConfiguration{ "x86-64-assembly": { Command: "make", }, + "yamlscript": { + Command: "make test", + }, "zig": { Command: "zig test {{test_files}}", }, From 3f1db64571a3348441204d2997589a8274a585b9 Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Mon, 4 Nov 2024 12:30:25 -0500 Subject: [PATCH 522/544] Bump version to v.3.5.3 (#1182) * Bump version to v.3.5.3 * add yamlscript pr --- CHANGELOG.md | 6 ++++++ cmd/version.go | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b12f04f2c..d88ba59e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ The exercism CLI follows [semantic versioning](http://semver.org/). - **Your contribution here** +## v3.5.3 (2024-11-03) + +- [#1178](https://github.com/exercism/cli/pull/1178) Add arm64-assembly test configuration [@keiravillekode] +- [#1177](https://github.com/exercism/cli/pull/1177) refactored exercism.io links to exercism.org [@ladokp] +- [#1165](https://github.com/exercism/cli/pull/1165) Add support for the YAMLScript language [@ingydotnet] + ## v3.5.2 (2024-10-09) - [#1174](https://github.com/exercism/cli/pull/1174) Fix an issue with `exercism completion bash` where the command name is not present in the completion output. - [@petrem] diff --git a/cmd/version.go b/cmd/version.go index 73a20bf04..bac407e01 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.5.2" +const Version = "3.5.3" // checkLatest flag for version command. var checkLatest bool From 24504cc373bb19d75ba7bdcd672dce43e34ceb27 Mon Sep 17 00:00:00 2001 From: Christian Willner <34183939+vaeng@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:23:28 +0100 Subject: [PATCH 523/544] feat: add uiua test command (#1183) --- workspace/test_configurations.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index 90a7a1670..497100818 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -253,6 +253,9 @@ var TestConfigurations = map[string]TestConfiguration{ "typescript": { Command: "yarn test", }, + "uiua": { + Command: "uiua test {{test_files}}", + }, // unison: tests are run from an active UCM session "vbnet": { Command: "dotnet test", From 7580d02cbb5b0904d0e72e9e9782e61c734265a3 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 15 Nov 2024 09:27:28 +0100 Subject: [PATCH 524/544] Bump version to 3.5.4 (#1184) --- CHANGELOG.md | 9 +++++++-- cmd/version.go | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d88ba59e2..3ec666ab6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,15 @@ The exercism CLI follows [semantic versioning](http://semver.org/). - **Your contribution here** +## v3.5.4 (2024-11-15) + +- [#1183](https://github.com/exercism/cli/pull/1183) Add support for Uiua track to `exercism test` - [@vaeng] + ## v3.5.3 (2024-11-03) - [#1178](https://github.com/exercism/cli/pull/1178) Add arm64-assembly test configuration [@keiravillekode] -- [#1177](https://github.com/exercism/cli/pull/1177) refactored exercism.io links to exercism.org [@ladokp] -- [#1165](https://github.com/exercism/cli/pull/1165) Add support for the YAMLScript language [@ingydotnet] +- [#1177](https://github.com/exercism/cli/pull/1177) refactored exercism.io links to exercism.org [@ladokp] +- [#1165](https://github.com/exercism/cli/pull/1165) Add support for the YAMLScript language [@ingydotnet] ## v3.5.2 (2024-10-09) @@ -576,3 +580,4 @@ All changes by [@msgehard] [@ageron]: https://github.com/ageron [@petrem]: https://github.com/petrem [@bnandras]: https://github.com/bnandras +[@vaeng]: https://github.com/vaeng diff --git a/cmd/version.go b/cmd/version.go index bac407e01..ed54b885c 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.5.3" +const Version = "3.5.4" // checkLatest flag for version command. var checkLatest bool From 4a57aa74143ba686d1f6d972a0f57fd02277e6d0 Mon Sep 17 00:00:00 2001 From: keiravillekode Date: Tue, 27 May 2025 16:44:08 +1000 Subject: [PATCH 525/544] idris uses slug (#1192) * idris uses slug * Obtain exercise slug from metadata * TestIdrisUsesExerciseSlug --- workspace/test_configurations.go | 9 +++++- workspace/test_configurations_test.go | 40 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index 497100818..d07a62f0e 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -55,6 +55,13 @@ func (c *TestConfiguration) GetTestCommand() (string, error) { } cmd = strings.ReplaceAll(cmd, "{{test_files}}", strings.Join(testFiles, " ")) } + if strings.Contains(cmd, "{{slug}}") { + metadata, err := NewExerciseMetadata(".") + if err != nil { + return "", err + } + cmd = strings.ReplaceAll(cmd, "{{slug}}", metadata.ExerciseSlug) + } return cmd, nil } @@ -152,7 +159,7 @@ var TestConfigurations = map[string]TestConfiguration{ Command: "stack test", }, "idris": { - Command: "pack test `basename *.ipkg .ipkg`", + Command: "pack test {{slug}}", }, "j": { Command: `jconsole -js "exit echo unittest {{test_files}} [ load {{solution_files}}"`, diff --git a/workspace/test_configurations_test.go b/workspace/test_configurations_test.go index 97ff40902..b8a8097ee 100644 --- a/workspace/test_configurations_test.go +++ b/workspace/test_configurations_test.go @@ -101,3 +101,43 @@ func TestRustHasTrailingDashes(t *testing.T) { assert.True(t, strings.HasSuffix(cmd, "--"), "rust's test command should have trailing dashes") } + +func TestIdrisUsesExerciseSlug(t *testing.T) { + currentDir, err := os.Getwd() + assert.NoError(t, err) + + tmpDir, err := os.MkdirTemp("", "solution") + assert.NoError(t, err) + defer os.RemoveAll(tmpDir) + + em := &ExerciseMetadata{ + Track: "idris", + ExerciseSlug: "bogus-exercise", + ID: "abc", + URL: "http://example.com", + Handle: "alice", + IsRequester: true, + Dir: tmpDir, + } + err = em.Write(tmpDir) + assert.NoError(t, err) + + defer os.Chdir(currentDir) + err = os.Chdir(tmpDir) + assert.NoError(t, err) + + exercismDir := filepath.Join(".", ".exercism") + f, err := os.Create(filepath.Join(exercismDir, "config.json")) + assert.NoError(t, err) + defer f.Close() + + _, err = f.WriteString(`{ "files": { "solution": [ "src/BogusExercise.idr" ], "test": [ "test/src/Main.idr" ] } }`) + assert.NoError(t, err) + + testConfig, ok := TestConfigurations["idris"] + assert.True(t, ok, "unexpectedly unable to find idris test config") + + cmd, err := testConfig.GetTestCommand() + assert.NoError(t, err) + assert.Equal(t, cmd, "pack test bogus-exercise") +} From 14f2349edc8cd1e2ad7393c14e17ceca06969ef9 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 30 May 2025 14:51:19 +0200 Subject: [PATCH 526/544] Bump version to 3.5.5 (#1194) --- CHANGELOG.md | 5 +++++ cmd/version.go | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ec666ab6..606588690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ The exercism CLI follows [semantic versioning](http://semver.org/). - **Your contribution here** +## v3.5.5 (2025-05-30) + +- [#1192](https://github.com/exercism/cli/pull/1192) Change Idris test command to use slug - [@keiravillekode] + ## v3.5.4 (2024-11-15) - [#1183](https://github.com/exercism/cli/pull/1183) Add support for Uiua track to `exercism test` - [@vaeng] @@ -581,3 +585,4 @@ All changes by [@msgehard] [@petrem]: https://github.com/petrem [@bnandras]: https://github.com/bnandras [@vaeng]: https://github.com/vaeng +[@keiravillekode]: https://github.com/keiravillekode diff --git a/cmd/version.go b/cmd/version.go index ed54b885c..a0bb6abbe 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.5.4" +const Version = "3.5.5" // checkLatest flag for version command. var checkLatest bool From 05260ea2e699f45f2728ecea703a4af84a8f2d9b Mon Sep 17 00:00:00 2001 From: Isaac Good Date: Fri, 13 Jun 2025 22:20:54 -0700 Subject: [PATCH 527/544] Check HTTP response content type before trying to decode it as JSON (#1196) --- cmd/cmd.go | 10 ++++++++ cmd/cmd_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++ cmd/download_test.go | 2 +- cmd/submit_test.go | 2 +- 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index dcca08264..91597b5b1 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "regexp" "strings" "io" @@ -23,6 +24,8 @@ var ( Out io.Writer // Err is used to write errors. Err io.Writer + // jsonContentTypeRe is used to match Content-Type which contains JSON. + jsonContentTypeRe = regexp.MustCompile(`^application/([[:alpha:]]+\+)?json$`) ) const msgWelcomePleaseConfigure = ` @@ -77,6 +80,13 @@ func validateUserConfig(cfg *viper.Viper) error { // decodedAPIError decodes and returns the error message from the API response. // If the message is blank, it returns a fallback message with the status code. func decodedAPIError(resp *http.Response) error { + if contentType := resp.Header.Get("Content-Type"); !jsonContentTypeRe.MatchString(contentType) { + return fmt.Errorf( + "expected response with Content-Type \"application/json\" but got status %q with Content-Type %q", + resp.Status, + contentType, + ) + } var apiError struct { Error struct { Type string `json:"type"` diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 782be18d2..101c52c87 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -2,7 +2,10 @@ package cmd import ( "io" + "io/ioutil" + "net/http" "os" + "strings" "testing" "github.com/spf13/cobra" @@ -119,3 +122,54 @@ func (co capturedOutput) reset() { Out = co.oldOut Err = co.oldErr } + +func errorResponse(contentType string, body string) *http.Response { + response := &http.Response{ + Status: "418 I'm a teapot", + StatusCode: 418, + Header: make(http.Header), + Body: ioutil.NopCloser(strings.NewReader(body)), + ContentLength: int64(len(body)), + } + response.Header.Set("Content-Type", contentType) + return response +} + +func TestDecodeErrorResponse(t *testing.T) { + testCases := []struct { + response *http.Response + wantMessage string + }{ + { + response: errorResponse("text/html", "Time for tea"), + wantMessage: `expected response with Content-Type "application/json" but got status "418 I'm a teapot" with Content-Type "text/html"`, + }, + { + response: errorResponse("application/json", `{"error": {"type": "json", "valid": no}}`), + wantMessage: "failed to parse API error response: invalid character 'o' in literal null (expecting 'u')", + }, + { + response: errorResponse("application/json", `{"error": {"type": "track_ambiguous", "message": "message", "possible_track_ids": ["a", "b"]}}`), + wantMessage: "message: a, b", + }, + { + response: errorResponse("application/json", `{"error": {"message": "message"}}`), + wantMessage: "message", + }, + { + response: errorResponse("application/problem+json", `{"error": {"message": "new json format"}}`), + wantMessage: "new json format", + }, + { + response: errorResponse("application/json", `{"error": {}}`), + wantMessage: "unexpected API response: 418", + }, + } + tc := testCases[0] + got := decodedAPIError(tc.response) + assert.Equal(t, tc.wantMessage, got.Error()) + for _, tc = range testCases { + got := decodedAPIError(tc.response) + assert.Equal(t, tc.wantMessage, got.Error()) + } +} diff --git a/cmd/download_test.go b/cmd/download_test.go index aadcf84aa..791be4230 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -403,7 +403,7 @@ func TestDownloadError(t *testing.T) { err = runDownload(cfg, flags, []string{}) - assert.Equal(t, "test error", err.Error()) + assert.Equal(t, `expected response with Content-Type "application/json" but got status "400 Bad Request" with Content-Type "text/plain; charset=utf-8"`, err.Error()) } diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 9b645965b..7db152a75 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -657,7 +657,7 @@ func TestSubmitServerErr(t *testing.T) { err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) - assert.Regexp(t, "test error", err.Error()) + assert.Regexp(t, `expected response with Content-Type "application/json" but got status "400 Bad Request" with Content-Type "text/plain; charset=utf-8"`, err.Error()) } func TestHandleErrorResponse(t *testing.T) { From 19a7972fecaecf3d9c24425ee27b3e611fc7f957 Mon Sep 17 00:00:00 2001 From: Isaac Good Date: Thu, 19 Jun 2025 10:18:14 -0700 Subject: [PATCH 528/544] cmd error parsing: show a "try again after" message when a response sets a Retry-After header (#1198) --- cmd/cmd.go | 17 +++++++++++++++++ cmd/cmd_test.go | 36 +++++++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index 91597b5b1..61fe9c047 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "regexp" + "strconv" "strings" "io" @@ -80,6 +81,22 @@ func validateUserConfig(cfg *viper.Viper) error { // decodedAPIError decodes and returns the error message from the API response. // If the message is blank, it returns a fallback message with the status code. func decodedAPIError(resp *http.Response) error { + // First and foremost, handle Retry-After headers; if set, show this to the user. + if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" { + // The Retry-After header can be an HTTP Date or delay seconds. + // The date can be used as-is. The delay seconds should have "seconds" appended. + if delay, err := strconv.Atoi(retryAfter); err == nil { + retryAfter = fmt.Sprintf("%d seconds", delay) + } + return fmt.Errorf( + "request failed with status %s; please try again after %s", + resp.Status, + retryAfter, + ) + } + + // Check for JSON data. On non-JSON data, show the status and content type then bail. + // Otherwise, extract the message details from the JSON. if contentType := resp.Header.Get("Content-Type"); !jsonContentTypeRe.MatchString(contentType) { return fmt.Errorf( "expected response with Content-Type \"application/json\" but got status %q with Content-Type %q", diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 101c52c87..a4d8b9fe2 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -123,7 +123,7 @@ func (co capturedOutput) reset() { Err = co.oldErr } -func errorResponse(contentType string, body string) *http.Response { +func errorResponse418(contentType string, body string) *http.Response { response := &http.Response{ Status: "418 I'm a teapot", StatusCode: 418, @@ -135,35 +135,57 @@ func errorResponse(contentType string, body string) *http.Response { return response } +func errorResponse429(retryAfter string) *http.Response { + body := "" + response := &http.Response{ + Status: "429 Too Many Requests", + StatusCode: 429, + Header: make(http.Header), + Body: ioutil.NopCloser(strings.NewReader(body)), + ContentLength: int64(len(body)), + } + response.Header.Set("Content-Type", "text/plain") + response.Header.Set("Retry-After", retryAfter) + return response +} + func TestDecodeErrorResponse(t *testing.T) { testCases := []struct { response *http.Response wantMessage string }{ { - response: errorResponse("text/html", "Time for tea"), + response: errorResponse418("text/html", "Time for tea"), wantMessage: `expected response with Content-Type "application/json" but got status "418 I'm a teapot" with Content-Type "text/html"`, }, { - response: errorResponse("application/json", `{"error": {"type": "json", "valid": no}}`), + response: errorResponse418("application/json", `{"error": {"type": "json", "valid": no}}`), wantMessage: "failed to parse API error response: invalid character 'o' in literal null (expecting 'u')", }, { - response: errorResponse("application/json", `{"error": {"type": "track_ambiguous", "message": "message", "possible_track_ids": ["a", "b"]}}`), + response: errorResponse418("application/json", `{"error": {"type": "track_ambiguous", "message": "message", "possible_track_ids": ["a", "b"]}}`), wantMessage: "message: a, b", }, { - response: errorResponse("application/json", `{"error": {"message": "message"}}`), + response: errorResponse418("application/json", `{"error": {"message": "message"}}`), wantMessage: "message", }, { - response: errorResponse("application/problem+json", `{"error": {"message": "new json format"}}`), + response: errorResponse418("application/problem+json", `{"error": {"message": "new json format"}}`), wantMessage: "new json format", }, { - response: errorResponse("application/json", `{"error": {}}`), + response: errorResponse418("application/json", `{"error": {}}`), wantMessage: "unexpected API response: 418", }, + { + response: errorResponse429("30"), + wantMessage: "request failed with status 429 Too Many Requests; please try again after 30 seconds", + }, + { + response: errorResponse429("Wed, 21 Oct 2015 07:28:00 GMT"), + wantMessage: "request failed with status 429 Too Many Requests; please try again after Wed, 21 Oct 2015 07:28:00 GMT", + }, } tc := testCases[0] got := decodedAPIError(tc.response) From f8a1087d1620888e6162ad8d8219cf7a544850f0 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Sun, 6 Jul 2025 20:39:51 +0200 Subject: [PATCH 529/544] Support for Futhark in `exercism test` (#1199) --- workspace/test_configurations.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index d07a62f0e..d3e237817 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -146,6 +146,9 @@ var TestConfigurations = map[string]TestConfiguration{ "fsharp": { Command: "dotnet test", }, + "futhark": { + Command: "futhark test test.fut", + }, "gleam": { Command: "gleam test", }, From c9e0237f5da1be2adce4b2afd8a7369399e03fbd Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Sun, 6 Jul 2025 20:46:32 +0200 Subject: [PATCH 530/544] Bump version to 3.5.6 (#1200) --- CHANGELOG.md | 30 +++++++++++++++++++----------- cmd/version.go | 2 +- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 606588690..e5693792d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ The exercism CLI follows [semantic versioning](http://semver.org/). - **Your contribution here** +## v3.5.6 (2025-07-06) + +- [#1199](https://github.com/exercism/cli/pull/1199) Support for Futhark in exercism test - [@erikschierboom] +- [#1198](https://github.com/exercism/cli/pull/1198) Show a "try again after" message when a response sets a Retry-After header - [@isaacg] +- [#1196](https://github.com/exercism/cli/pull/1196) Check HTTP response content type before trying to decode it as JSON - [@isaacg] + ## v3.5.5 (2025-05-30) - [#1192](https://github.com/exercism/cli/pull/1192) Change Idris test command to use slug - [@keiravillekode] @@ -507,16 +513,16 @@ All changes by [@msgehard] - Implement login and logout - Build on Travis -[@AlexWheeler]: https://github.com/AlexWheeler +[@alexwheeler]: https://github.com/AlexWheeler [@andrerfcsantos]: https://github.com/andrerfcsantos [@avegner]: https://github.com/avegner -[@Dparker1990]: https://github.com/Dparker1990 -[@John-Goff]: https://github.com/John-Goff -[@LegalizeAdulthood]: https://github.com/LegalizeAdulthood -[@QuLogic]: https://github.com/QuLogic -[@Smarticles101]: https://github.com/Smarticles101 -[@Tonkpils]: https://github.com/Tonkpils -[@TrevorBramble]: https://github.com/TrevorBramble +[@dparker1990]: https://github.com/Dparker1990 +[@john-goff]: https://github.com/John-Goff +[@legalizeadulthood]: https://github.com/LegalizeAdulthood +[@qulogic]: https://github.com/QuLogic +[@smarticles101]: https://github.com/Smarticles101 +[@tonkpils]: https://github.com/Tonkpils +[@trevorbramble]: https://github.com/TrevorBramble [@alebaffa]: https://github.com/alebaffa [@ambroff]: https://github.com/ambroff [@andrewsardone]: https://github.com/andrewsardone @@ -531,22 +537,24 @@ All changes by [@msgehard] [@djquan]: https://github.com/djquan [@dmmulroy]: https://github.com/dmmulroy [@dpritchett]: https://github.com/dpritchett -[@eToThePiIPower]: https://github.com/eToThePiIPower +[@etothepiipower]: https://github.com/eToThePiIPower [@ebautistabar]: https://github.com/ebautistabar [@ekingery]: https://github.com/ekingery [@elimisteve]: https://github.com/elimisteve +[@erikschierboom]: https://github.com/erikschierboom [@ests]: https://github.com/ests [@farisj]: https://github.com/farisj [@glebedel]: https://github.com/glebedel [@harimp]: https://github.com/harimp [@harugo]: https://github.com/harugo [@hjljo]: https://github.com/hjljo +[@isaacg]: https://github.com/isaacg [@isbadawi]: https://github.com/isbadawi [@jbaiter]: https://github.com/jbaiter [@jdsutherland]: https://github.com/jdsutherland [@jgsqware]: https://github.com/jgsqware [@jish]: https://github.com/jish -[@Jrank2013]: https://github.com/Jrank2013 +[@jrank2013]: https://github.com/Jrank2013 [@jppunnett]: https://github.com/jppunnett [@katrinleinweber]: https://github.com/katrinleinweber [@kytrinyx]: https://github.com/kytrinyx @@ -572,7 +580,7 @@ All changes by [@msgehard] [@xavdid]: https://github.com/xavdid [@williandrade]: https://github.com/williandrade [@zabawaba99]: https://github.com/zabawaba99 -[@GroophyLifefor]: https://github.com/GroophyLifefor +[@groophylifefor]: https://github.com/GroophyLifefor [@muzimuzhi]: https://github.com/muzimuzhi [@isberg]: https://github.com/isberg [@erikschierboom]: https://github.com/erikschierboom diff --git a/cmd/version.go b/cmd/version.go index a0bb6abbe..02f9c955b 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.5.5" +const Version = "3.5.6" // checkLatest flag for version command. var checkLatest bool From c84b33cfb9b0f8e71f7b2031714d98d092a00640 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Sun, 6 Jul 2025 21:00:19 +0200 Subject: [PATCH 531/544] Fix goreleaser deprecations (#1201) 1. https://goreleaser.com/deprecations/#archivesformat_overridesformat 2. https://goreleaser.com/deprecations/#archivesbuilds --- .goreleaser.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 1995cf6b4..6e9a9d182 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -54,7 +54,7 @@ changelog: archives: - id: release-archives - builds: + ids: - release-build name_template: >- {{- .ProjectName }}- @@ -66,7 +66,7 @@ archives: {{- if .Arm }}v{{- .Arm }}{{ end }} format_overrides: - goos: windows - format: zip + formats: ["zip"] files: - shell/** - LICENSE @@ -84,7 +84,7 @@ archives: {{- if .Arm }}v{{- .Arm }}{{ end }} format_overrides: - goos: windows - format: zip + formats: ["zip"] files: - shell/** - LICENSE From de0cf3eff4fc3f2e978e1004df99c9de865d1e17 Mon Sep 17 00:00:00 2001 From: Isaac Good Date: Mon, 14 Jul 2025 09:30:17 -0700 Subject: [PATCH 532/544] Add error decoding support for content type parameters such as charset (#1202) --- cmd/cmd.go | 5 +++-- cmd/cmd_test.go | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index 61fe9c047..b0c7a8310 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -2,6 +2,7 @@ package cmd import ( "encoding/json" + "errors" "fmt" "net/http" "regexp" @@ -26,7 +27,7 @@ var ( // Err is used to write errors. Err io.Writer // jsonContentTypeRe is used to match Content-Type which contains JSON. - jsonContentTypeRe = regexp.MustCompile(`^application/([[:alpha:]]+\+)?json$`) + jsonContentTypeRe = regexp.MustCompile(`^application/([[:alpha:]]+\+)?json($|;)`) ) const msgWelcomePleaseConfigure = ` @@ -122,7 +123,7 @@ func decodedAPIError(resp *http.Response) error { strings.Join(apiError.Error.PossibleTrackIDs, ", "), ) } - return fmt.Errorf(apiError.Error.Message) + return errors.New(apiError.Error.Message) } return fmt.Errorf("unexpected API response: %d", resp.StatusCode) } diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index a4d8b9fe2..d712e742b 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -162,6 +162,10 @@ func TestDecodeErrorResponse(t *testing.T) { response: errorResponse418("application/json", `{"error": {"type": "json", "valid": no}}`), wantMessage: "failed to parse API error response: invalid character 'o' in literal null (expecting 'u')", }, + { + response: errorResponse418("application/json; charset=utf-8", `{"error": {"type": "track_ambiguous", "message": "message", "possible_track_ids": ["a", "b"]}}`), + wantMessage: "message: a, b", + }, { response: errorResponse418("application/json", `{"error": {"type": "track_ambiguous", "message": "message", "possible_track_ids": ["a", "b"]}}`), wantMessage: "message: a, b", From 0038ab5ef3299c23e007228464ac815029b48ad6 Mon Sep 17 00:00:00 2001 From: Isaac Good Date: Tue, 15 Jul 2025 08:11:58 -0700 Subject: [PATCH 533/544] Add version bump from v3.5.6 to v3.5.7 (#1204) --- CHANGELOG.md | 5 +++++ cmd/version.go | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5693792d..3be77cb6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ The exercism CLI follows [semantic versioning](http://semver.org/). - **Your contribution here** +## v3.5.7 (2025-07-14) + +- [#1202](https://github.com/exercism/cli/pull/1202) Add error decoding support for content type parameters such as charset - [@isaacg] +- [#1201](https://github.com/exercism/cli/pull/1201) Fix goreleaser deprecations - [@erikschierboom] + ## v3.5.6 (2025-07-06) - [#1199](https://github.com/exercism/cli/pull/1199) Support for Futhark in exercism test - [@erikschierboom] diff --git a/cmd/version.go b/cmd/version.go index 02f9c955b..7ab4ff377 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.5.6" +const Version = "3.5.7" // checkLatest flag for version command. var checkLatest bool From 8d0bdbedeb07339bec7e5a9d5e822f19210ff521 Mon Sep 17 00:00:00 2001 From: Isaac Good Date: Tue, 12 Aug 2025 08:56:37 -0700 Subject: [PATCH 534/544] Drop team-specific logic from the CLI (#1206) --- cmd/download.go | 24 +++++------------ cmd/download_test.go | 30 +++++----------------- cmd/submit.go | 2 +- cmd/submit_test.go | 47 ---------------------------------- shell/exercism.fish | 1 - workspace/exercise_metadata.go | 4 --- workspace/workspace.go | 22 ---------------- workspace/workspace_test.go | 8 ++---- 8 files changed, 16 insertions(+), 122 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index f376e959b..d42771f58 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -135,7 +135,7 @@ type download struct { token, apibaseurl, workspace string // optional - track, team string + track string forceoverwrite bool payload *downloadPayload @@ -156,7 +156,6 @@ func newDownload(flags *pflag.FlagSet, usrCfg *viper.Viper) (*download, error) { if err != nil { return nil, err } - d.team, err = flags.GetString("team") if err != nil { return nil, err } @@ -176,7 +175,7 @@ func newDownload(flags *pflag.FlagSet, usrCfg *viper.Viper) (*download, error) { if err = d.needsUserConfigValues(); err != nil { return nil, err } - if err = d.needsSlugWhenGivenTrackOrTeam(); err != nil { + if err = d.needsSlugWhenGivenTrack(); err != nil { return nil, err } @@ -226,9 +225,6 @@ func (d download) buildQueryParams(url *netURL.URL) { if d.track != "" { query.Add("track_id", d.track) } - if d.team != "" { - query.Add("team_id", d.team) - } } url.RawQuery = query.Encode() } @@ -256,11 +252,11 @@ func (d download) needsUserConfigValues() error { return nil } -// needsSlugWhenGivenTrackOrTeam ensures that track/team arguments are also given with a slug. -// (track/team meaningless when given a uuid). -func (d download) needsSlugWhenGivenTrackOrTeam() error { - if (d.team != "" || d.track != "") && d.slug == "" { - return errors.New("--track or --team requires --exercise (not --uuid)") +// needsSlugWhenGivenTrack ensures that track arguments are also given with a slug. +// (track meaningless when given a uuid). +func (d download) needsSlugWhenGivenTrack() error { + if d.track != "" && d.slug == "" { + return errors.New("--track or requires --exercise (not --uuid)") } return nil } @@ -269,10 +265,6 @@ type downloadPayload struct { Solution struct { ID string `json:"id"` URL string `json:"url"` - Team struct { - Name string `json:"name"` - Slug string `json:"slug"` - } `json:"team"` User struct { Handle string `json:"handle"` IsRequester bool `json:"is_requester"` @@ -303,7 +295,6 @@ func (dp downloadPayload) metadata() workspace.ExerciseMetadata { return workspace.ExerciseMetadata{ AutoApprove: dp.Solution.Exercise.AutoApprove, Track: dp.Solution.Exercise.Track.ID, - Team: dp.Solution.Team.Slug, ExerciseSlug: dp.Solution.Exercise.ID, ID: dp.Solution.ID, URL: dp.Solution.URL, @@ -361,7 +352,6 @@ func setupDownloadFlags(flags *pflag.FlagSet) { flags.StringP("uuid", "u", "", "the solution UUID") flags.StringP("track", "t", "", "the track ID") flags.StringP("exercise", "e", "", "the exercise slug") - flags.StringP("team", "T", "", "the team slug") flags.BoolP("force", "F", false, "overwrite existing exercise directory") } diff --git a/cmd/download_test.go b/cmd/download_test.go index 791be4230..03d2b1eed 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -160,11 +160,6 @@ func TestDownload(t *testing.T) { expectedDir: filepath.Join("users", "alice"), flags: map[string]string{"uuid": "bogus-id"}, }, - { - requester: true, - expectedDir: filepath.Join("teams", "bogus-team"), - flags: map[string]string{"exercise": "bogus-exercise", "track": "bogus-track", "team": "bogus-team"}, - }, } for _, tc := range testCases { @@ -172,7 +167,7 @@ func TestDownload(t *testing.T) { defer os.RemoveAll(tmpDir) assert.NoError(t, err) - ts := fakeDownloadServer(strconv.FormatBool(tc.requester), tc.flags["team"]) + ts := fakeDownloadServer(strconv.FormatBool(tc.requester)) defer ts.Close() v := viper.New() @@ -221,10 +216,6 @@ func TestDownloadToExistingDirectory(t *testing.T) { exerciseDir: filepath.Join("bogus-track", "bogus-exercise"), flags: map[string]string{"exercise": "bogus-exercise", "track": "bogus-track"}, }, - { - exerciseDir: filepath.Join("teams", "bogus-team", "bogus-track", "bogus-exercise"), - flags: map[string]string{"exercise": "bogus-exercise", "track": "bogus-track", "team": "bogus-team"}, - }, } for _, tc := range testCases { @@ -235,7 +226,7 @@ func TestDownloadToExistingDirectory(t *testing.T) { err = os.MkdirAll(filepath.Join(tmpDir, tc.exerciseDir), os.FileMode(0755)) assert.NoError(t, err) - ts := fakeDownloadServer("true", "") + ts := fakeDownloadServer("true") defer ts.Close() v := viper.New() @@ -273,10 +264,6 @@ func TestDownloadToExistingDirectoryWithForce(t *testing.T) { exerciseDir: filepath.Join("bogus-track", "bogus-exercise"), flags: map[string]string{"exercise": "bogus-exercise", "track": "bogus-track"}, }, - { - exerciseDir: filepath.Join("teams", "bogus-team", "bogus-track", "bogus-exercise"), - flags: map[string]string{"exercise": "bogus-exercise", "track": "bogus-track", "team": "bogus-team"}, - }, } for _, tc := range testCases { @@ -287,7 +274,7 @@ func TestDownloadToExistingDirectoryWithForce(t *testing.T) { err = os.MkdirAll(filepath.Join(tmpDir, tc.exerciseDir), os.FileMode(0755)) assert.NoError(t, err) - ts := fakeDownloadServer("true", "") + ts := fakeDownloadServer("true") defer ts.Close() v := viper.New() @@ -310,7 +297,7 @@ func TestDownloadToExistingDirectoryWithForce(t *testing.T) { } } -func fakeDownloadServer(requestor, teamSlug string) *httptest.Server { +func fakeDownloadServer(requestor string) *httptest.Server { mux := http.NewServeMux() server := httptest.NewServer(mux) @@ -327,15 +314,11 @@ func fakeDownloadServer(requestor, teamSlug string) *httptest.Server { }) mux.HandleFunc("/solutions/latest", func(w http.ResponseWriter, r *http.Request) { - team := "null" - if teamSlug := r.FormValue("team_id"); teamSlug != "" { - team = fmt.Sprintf(`{"name": "Bogus Team", "slug": "%s"}`, teamSlug) - } - payloadBody := fmt.Sprintf(payloadTemplate, requestor, team, server.URL+"/") + payloadBody := fmt.Sprintf(payloadTemplate, requestor, server.URL+"/") fmt.Fprint(w, payloadBody) }) mux.HandleFunc("/solutions/bogus-id", func(w http.ResponseWriter, r *http.Request) { - payloadBody := fmt.Sprintf(payloadTemplate, requestor, "null", server.URL+"/") + payloadBody := fmt.Sprintf(payloadTemplate, requestor, server.URL+"/") fmt.Fprint(w, payloadBody) }) @@ -415,7 +398,6 @@ const payloadTemplate = ` "handle": "alice", "is_requester": %s }, - "team": %s, "exercise": { "id": "bogus-exercise", "instructions_url": "http://example.com/bogus-exercise", diff --git a/cmd/submit.go b/cmd/submit.go index eb00aa1f4..3135f65ae 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -312,7 +312,7 @@ func (s *submitCmdContext) printResult(metadata *workspace.ExerciseMetadata) { %s ` suffix := "View it at:\n\n " - if metadata.AutoApprove && metadata.Team == "" { + if metadata.AutoApprove { suffix = "You can complete the exercise and unlock the next core exercise at:\n" } fmt.Fprintf(Err, msg, suffix) diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 7db152a75..4e1349953 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -425,53 +425,6 @@ func TestSubmitWithEnormousFile(t *testing.T) { } } -func TestSubmitFilesForTeamExercise(t *testing.T) { - co := newCapturedOutput() - co.override() - defer co.reset() - - // The fake endpoint will populate this when it receives the call from the command. - submittedFiles := map[string]string{} - ts := fakeSubmitServer(t, submittedFiles) - defer ts.Close() - - tmpDir, err := os.MkdirTemp("", "submit-files") - assert.NoError(t, err) - - dir := filepath.Join(tmpDir, "teams", "bogus-team", "bogus-track", "bogus-exercise") - os.MkdirAll(filepath.Join(dir, "subdir"), os.FileMode(0755)) - writeFakeMetadata(t, dir, "bogus-track", "bogus-exercise") - - file1 := filepath.Join(dir, "file-1.txt") - err = os.WriteFile(file1, []byte("This is file 1."), os.FileMode(0755)) - assert.NoError(t, err) - - file2 := filepath.Join(dir, "subdir", "file-2.txt") - err = os.WriteFile(file2, []byte("This is file 2."), os.FileMode(0755)) - assert.NoError(t, err) - - v := viper.New() - v.Set("token", "abc123") - v.Set("workspace", tmpDir) - v.Set("apibaseurl", ts.URL) - - cfg := config.Config{ - Dir: tmpDir, - UserViperConfig: v, - } - - files := []string{ - file1, file2, - } - err = runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), files) - assert.NoError(t, err) - - 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["subdir/file-2.txt"]) -} - func TestSubmitOnlyEmptyFile(t *testing.T) { co := newCapturedOutput() co.override() diff --git a/shell/exercism.fish b/shell/exercism.fish index c2c7c0811..4abe9b21e 100644 --- a/shell/exercism.fish +++ b/shell/exercism.fish @@ -9,7 +9,6 @@ complete -f -c exercism -n "__fish_seen_subcommand_from configure" -s s -l show complete -f -c exercism -n "__fish_use_subcommand" -a "download" -d "Downloads and saves a specified submission into the local system" complete -f -c exercism -n "__fish_seen_subcommand_from download" -s e -l exercise -d "the exercise slug" complete -f -c exercism -n "__fish_seen_subcommand_from download" -s h -l help -d "help for download" -complete -f -c exercism -n "__fish_seen_subcommand_from download" -s T -l team -d "the team slug" complete -f -c exercism -n "__fish_seen_subcommand_from download" -s t -l track -d "the track ID" complete -f -c exercism -n "__fish_seen_subcommand_from download" -s u -l uuid -d "the solution UUID" diff --git a/workspace/exercise_metadata.go b/workspace/exercise_metadata.go index 729174258..3ec902c1b 100644 --- a/workspace/exercise_metadata.go +++ b/workspace/exercise_metadata.go @@ -20,7 +20,6 @@ type ExerciseMetadata struct { Track string `json:"track"` ExerciseSlug string `json:"exercise"` ID string `json:"id"` - Team string `json:"team,omitempty"` URL string `json:"url"` Handle string `json:"handle"` IsRequester bool `json:"is_requester"` @@ -98,9 +97,6 @@ func (em *ExerciseMetadata) Exercise(workspace string) Exercise { // root represents the root of the exercise. func (em *ExerciseMetadata) root(workspace string) string { - if em.Team != "" { - return filepath.Join(workspace, "teams", em.Team) - } if !em.IsRequester { return filepath.Join(workspace, "users", em.Handle) } diff --git a/workspace/workspace.go b/workspace/workspace.go index 0011d4be2..dbd0604f1 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -58,28 +58,6 @@ func (ws Workspace) PotentialExercises() ([]Exercise, error) { continue } - if topInfo.Name() == "teams" { - subInfos, err := os.ReadDir(filepath.Join(ws.Dir, "teams")) - if err != nil { - return nil, err - } - - for _, subInfo := range subInfos { - teamWs, err := New(filepath.Join(ws.Dir, "teams", subInfo.Name())) - if err != nil { - return nil, err - } - - teamExercises, err := teamWs.PotentialExercises() - if err != nil { - return nil, err - } - - exercises = append(exercises, teamExercises...) - } - continue - } - subInfos, err := os.ReadDir(filepath.Join(ws.Dir, topInfo.Name())) if err != nil { return nil, err diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go index 63053a5cd..8614ddc2c 100644 --- a/workspace/workspace_test.go +++ b/workspace/workspace_test.go @@ -19,16 +19,13 @@ func TestWorkspacePotentialExercises(t *testing.T) { b1 := filepath.Join(tmpDir, "track-b", "exercise-one") b2 := filepath.Join(tmpDir, "track-b", "exercise-two") - // It should find teams exercises - team := filepath.Join(tmpDir, "teams", "some-team", "track-c", "exercise-one") - // It should ignore other people's exercises. alice := filepath.Join(tmpDir, "users", "alice", "track-a", "exercise-one") // It should ignore nested dirs within exercises. nested := filepath.Join(a1, "subdir", "deeper-dir", "another-deep-dir") - for _, path := range []string{a1, b1, b2, team, alice, nested} { + for _, path := range []string{a1, b1, b2, alice, nested} { err := os.MkdirAll(path, os.FileMode(0755)) assert.NoError(t, err) } @@ -38,7 +35,7 @@ func TestWorkspacePotentialExercises(t *testing.T) { exercises, err := ws.PotentialExercises() assert.NoError(t, err) - if assert.Equal(t, 4, len(exercises)) { + if assert.Equal(t, 3, len(exercises)) { paths := make([]string, len(exercises)) for i, e := range exercises { paths[i] = e.Path() @@ -48,7 +45,6 @@ func TestWorkspacePotentialExercises(t *testing.T) { assert.Equal(t, paths[0], "track-a/exercise-one") assert.Equal(t, paths[1], "track-b/exercise-one") assert.Equal(t, paths[2], "track-b/exercise-two") - assert.Equal(t, paths[3], "track-c/exercise-one") } } From 2599e0b8c3cc974d1ed4f2634df2d08f16984f68 Mon Sep 17 00:00:00 2001 From: Isaac Good Date: Sat, 30 Aug 2025 20:39:18 -0700 Subject: [PATCH 535/544] Include empty files in downloads (#1213) --- cmd/download.go | 4 ---- cmd/download_test.go | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index d42771f58..0e74c49ed 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -101,10 +101,6 @@ func runDownload(cfg config.Config, flags *pflag.FlagSet, args []string) error { // TODO: deal with it continue } - // Don't bother with empty files. - if res.Header.Get("Content-Length") == "0" { - continue - } path := sf.relativePath() dir := filepath.Join(metadata.Dir, filepath.Dir(path)) diff --git a/cmd/download_test.go b/cmd/download_test.go index 03d2b1eed..b297e4be9 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -353,7 +353,7 @@ func assertDownloadedCorrectFiles(t *testing.T, targetDir string) { path := filepath.Join(targetDir, "bogus-track", "bogus-exercise", "file-3.txt") _, err := os.Lstat(path) - assert.True(t, os.IsNotExist(err), "It should not write the file if empty.") + assert.NoError(t, err) } func TestDownloadError(t *testing.T) { From bfa07bdfc1148a24ff621f0d2b727ff5a21aaea7 Mon Sep 17 00:00:00 2001 From: Isaac Good Date: Sun, 31 Aug 2025 07:58:48 -0700 Subject: [PATCH 536/544] goreleaser: update deprecated archives.builds to new archives.ids (#1205) See [goreleaser docs](https://goreleaser.com/deprecations/#archivesbuilds) for details. --- .goreleaser.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 6e9a9d182..86f2100cc 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -72,7 +72,7 @@ archives: - LICENSE - README.md - id: installer-archives - builds: + ids: - installer-build name_template: >- {{- .ProjectName }}- From c1fb1037ad9751d40879e62a6a628998e3be811d Mon Sep 17 00:00:00 2001 From: Isaac Good Date: Sun, 31 Aug 2025 08:08:54 -0700 Subject: [PATCH 537/544] Use mode 0700 for the config dir, not 0755; other users should not have access to the config (#1210) --- config/persister.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/persister.go b/config/persister.go index 1ad046f0d..1b29dc720 100644 --- a/config/persister.go +++ b/config/persister.go @@ -25,7 +25,7 @@ func (p FilePersister) Save(v *viper.Viper, basename string) error { v.SetConfigName(basename) if _, err := os.Stat(p.Dir); os.IsNotExist(err) { - if err := os.MkdirAll(p.Dir, os.FileMode(0755)); err != nil { + if err := os.MkdirAll(p.Dir, os.FileMode(0700)); err != nil { return err } } From efc3ce2c86fb961744e6f18335c0417b88b72c7c Mon Sep 17 00:00:00 2001 From: Isaac Good Date: Tue, 23 Sep 2025 17:50:02 -0700 Subject: [PATCH 538/544] Update the token URL to point to the API settings page (#1215) --- cmd/cmd.go | 2 +- cmd/configure.go | 4 ++-- cmd/troubleshoot.go | 2 +- config/config.go | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index b0c7a8310..ac28286a7 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -69,7 +69,7 @@ func validateUserConfig(cfg *viper.Viper) error { if cfg.GetString("token") == "" { return fmt.Errorf( msgWelcomePleaseConfigure, - config.SettingsURL(cfg.GetString("apibaseurl")), + config.TokenURL(cfg.GetString("apibaseurl")), BinaryName, ) } diff --git a/cmd/configure.go b/cmd/configure.go index dd1c47ed8..57aabc04c 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -60,7 +60,7 @@ func runConfigure(configuration config.Config, flags *pflag.FlagSet) error { // If the command is run 'bare' and we have no token, // explain how to set the token. if flags.NFlag() == 0 && cfg.GetString("token") == "" { - tokenURL := config.SettingsURL(cfg.GetString("apibaseurl")) + tokenURL := config.TokenURL(cfg.GetString("apibaseurl")) return fmt.Errorf("There is no token configured. Find your token on %s, and call this command again with --token=.", tokenURL) } @@ -107,7 +107,7 @@ func runConfigure(configuration config.Config, flags *pflag.FlagSet) error { token = cfg.GetString("token") } - tokenURL := config.SettingsURL(cfg.GetString("apibaseurl")) + tokenURL := config.TokenURL(cfg.GetString("apibaseurl")) // If we don't have a token then explain how to set it and bail. if token == "" { diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go index 2c599d6d5..f83d5cc96 100644 --- a/cmd/troubleshoot.go +++ b/cmd/troubleshoot.go @@ -191,7 +191,7 @@ func newConfigurationStatus(status *Status) configurationStatus { Workspace: workspace, Dir: status.cfg.Dir, Token: v.GetString("token"), - TokenURL: config.SettingsURL(v.GetString("apibaseurl")), + TokenURL: config.TokenURL(v.GetString("apibaseurl")), } if status.Censor && cs.Token != "" { cs.Token = debug.Redact(cs.Token) diff --git a/config/config.go b/config/config.go index e1cc37e08..d52e3fd97 100644 --- a/config/config.go +++ b/config/config.go @@ -128,7 +128,7 @@ func InferSiteURL(apiURL string) string { return re.ReplaceAllString(apiURL, "$1") } -// SettingsURL provides a link to where the user can find their API token. -func SettingsURL(apiURL string) string { - return fmt.Sprintf("%s%s", InferSiteURL(apiURL), "/my/settings") +// TokenURL provides a link to where the user can find their API token. +func TokenURL(apiURL string) string { + return fmt.Sprintf("%s%s", InferSiteURL(apiURL), "/my/settings/api_cli") } From 282894b3e484c81a6e4646bb95a85ce3adf7ec50 Mon Sep 17 00:00:00 2001 From: Isaac Good Date: Wed, 24 Sep 2025 00:03:58 -0700 Subject: [PATCH 539/544] Bump version to v3.5.8 (#1216) --- CHANGELOG.md | 8 ++++++++ RELEASE.md | 2 +- cmd/version.go | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3be77cb6c..9de216f5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ The exercism CLI follows [semantic versioning](http://semver.org/). - **Your contribution here** +## v3.5.8 (2025-09-24) + +- [#1215](https://github.com/exercism/cli/pull/1215) Update the token URL to point to the API settings page [@isaacg] +- [#1210](https://github.com/exercism/cli/pull/1210) Use mode 0700 for the config dir, not 0755; other users should not have access to the config [@isaacg] +- [#1205](https://github.com/exercism/cli/pull/1205) goreleaser: update deprecated archives.builds to new archives.ids [@isaacg] +- [#1213](https://github.com/exercism/cli/pull/1213) Include empty files in downloads [@isaacg] +- [#1206](https://github.com/exercism/cli/pull/1206) Drop team-specific logic from the CLI [@isaacg] + ## v3.5.7 (2025-07-14) - [#1202](https://github.com/exercism/cli/pull/1202) Add error decoding support for content type parameters such as charset - [@isaacg] diff --git a/RELEASE.md b/RELEASE.md index 289c78822..17182f601 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -17,7 +17,7 @@ The Exercism CLI uses [GoReleaser](https://goreleaser.com) to automate the relea 1. Commit the updated files 1. Create a PR -_Note: It's useful to add the version to the commit message when you bump it: e.g. `Bump version to v2.3.4`._ +_Note: It's useful to add the version to the commit message when you bump it: e.g. `Bump version to v3.5.9`._ ## Cut a release diff --git a/cmd/version.go b/cmd/version.go index 7ab4ff377..7eec21d45 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,7 +9,7 @@ import ( // Version is the version of the current build. // It follows semantic versioning. -const Version = "3.5.7" +const Version = "3.5.8" // checkLatest flag for version command. var checkLatest bool From 3c2804bcf2e9f9352cf194fb1f494da5fdb22d66 Mon Sep 17 00:00:00 2001 From: Isaac Good Date: Wed, 24 Sep 2025 08:21:29 -0700 Subject: [PATCH 540/544] Fix the API settings URL: remove the `/my/` prefix (#1217) --- cmd/download_test.go | 2 +- cmd/submit_test.go | 2 +- config/config.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/download_test.go b/cmd/download_test.go index b297e4be9..fb18cfc86 100644 --- a/cmd/download_test.go +++ b/cmd/download_test.go @@ -26,7 +26,7 @@ func TestDownloadWithoutToken(t *testing.T) { if assert.Error(t, err) { assert.Regexp(t, "Welcome to Exercism", err.Error()) // It uses the default base API url to infer the host - assert.Regexp(t, "exercism.org/my/settings", err.Error()) + assert.Regexp(t, "exercism.org/settings", err.Error()) } } diff --git a/cmd/submit_test.go b/cmd/submit_test.go index 4e1349953..38caf9d93 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -28,7 +28,7 @@ func TestSubmitWithoutToken(t *testing.T) { err := runSubmit(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{}) if assert.Error(t, err) { assert.Regexp(t, "Welcome to Exercism", err.Error()) - assert.Regexp(t, "exercism.org/my/settings", err.Error()) + assert.Regexp(t, "exercism.org/settings", err.Error()) } } diff --git a/config/config.go b/config/config.go index d52e3fd97..f16b2b6ce 100644 --- a/config/config.go +++ b/config/config.go @@ -130,5 +130,5 @@ func InferSiteURL(apiURL string) string { // TokenURL provides a link to where the user can find their API token. func TokenURL(apiURL string) string { - return fmt.Sprintf("%s%s", InferSiteURL(apiURL), "/my/settings/api_cli") + return fmt.Sprintf("%s%s", InferSiteURL(apiURL), "/settings/api_cli") } From 9e52305d14e83ed4e4bbb0ea0afc47cea641daf3 Mon Sep 17 00:00:00 2001 From: keiravillekode Date: Sun, 28 Dec 2025 01:12:58 +1100 Subject: [PATCH 541/544] Support for Free Pascal in `exercism test` (#1219) --- workspace/test_configurations.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index d3e237817..2e3e31d08 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -143,6 +143,9 @@ var TestConfigurations = map[string]TestConfiguration{ "fortran": { Command: "make", }, + "free-pascal": { + Command: "make test=all", + }, "fsharp": { Command: "dotnet test", }, From 1a6aad5e53facf1c7003b7ab7869e183f9a187d8 Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Mon, 12 Jan 2026 09:55:01 -0500 Subject: [PATCH 542/544] Add Odin support to `exercism test` (#1220) Co-authored-by: Erik Schierboom --- workspace/test_configurations.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index 2e3e31d08..7d3731e83 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -203,6 +203,9 @@ var TestConfigurations = map[string]TestConfiguration{ "ocaml": { Command: "make", }, + "odin": { + Command: "odin test .", + }, "perl5": { Command: "prove .", }, From 9c1e805bc08bc0a2f8205782ab6556c9695f2eaf Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Mon, 12 Jan 2026 09:57:42 -0500 Subject: [PATCH 543/544] Add moonscript for `exercism test` (#1222) --- workspace/test_configurations.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index 7d3731e83..3c989d2c7 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -196,6 +196,9 @@ var TestConfigurations = map[string]TestConfiguration{ "mips": { Command: "java -jar /path/to/mars.jar nc runner.mips impl.mips", }, + "moonscript": { + Command: "busted", + }, "nim": { Command: "nim r {{test_files}}", }, From 47588cb78733d60cf45aa33c52790adff9d562a4 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 13 Jan 2026 11:16:21 -0500 Subject: [PATCH 544/544] Add Lean test command to test configurations (#1224) --- workspace/test_configurations.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go index 3c989d2c7..85479f30c 100644 --- a/workspace/test_configurations.go +++ b/workspace/test_configurations.go @@ -187,6 +187,9 @@ var TestConfigurations = map[string]TestConfiguration{ Command: "./gradlew test", WindowsCommand: "gradlew.bat test", }, + "lean": { + Command: "lake test", + }, "lfe": { Command: "make test", },