Repository: exercism/cli
Pull request #628 (2018-07-14 04:04:32)

Improve setup instructions for contributors
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. There may be a better way of doing this, if so please comment!

See:

* https://help.github.com/articles/configuring-a-remote-for-a-fork/
* https://www.atlassian.com/git/tutorials/syncing

Also `git update-index --assume-unchanged Gopkg.lock` prevents
accidental commits by contributors.


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7f483d1..df933fa 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/<your-github-username>/cli`
+1. `git remote rename origin upstream`
+1. `git remote add origin git@github.com:<your-github-username>/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
 
diff --git a/bin/build-all b/bin/build-all
index 4c87436..36ebe54 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"
+	release_name="release/$BINNAME-$osname-$osarch"
+	if [ "$osname" = windows ]; then
+		(cd out && zip "../$release_name.zip" ../shell/* "./$binname")
 	else
-		tar cvzf "$relname.tgz" "$binname"
+		tar cvzf "$release_name.tgz" shell -C out "./$binname"
 	fi
-	cd ..
 }
 
 # Mac Releases
diff --git a/cmd/cmd.go b/cmd/cmd.go
new file mode 100644
index 0000000..c7f97fe
--- /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 cd76805..e95564a 100644
--- a/cmd/download.go
+++ b/cmd/download.go
@@ -30,16 +30,15 @@ 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")
+		usrCfg, err := config.NewUserConfig()
 		if err != nil {
 			return err
 		}
-		if token != "" {
-			RootCmd.SetArgs([]string{"configure", "--token", token})
-			if err := RootCmd.Execute(); err != nil {
-				return err
-			}
+		if usrCfg.Token == "" {
+			tokenURL := config.InferSiteURL(usrCfg.APIBaseURL) + "/my/settings"
+			return fmt.Errorf(msgWelcomePleaseConfigure, tokenURL, BinaryName)
 		}
+
 		uuid, err := cmd.Flags().GetString("uuid")
 		if err != nil {
 			return err
@@ -51,10 +50,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 == "" {
@@ -229,7 +224,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/submit.go b/cmd/submit.go
index 0feed88..9e8b213 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()
@@ -63,22 +54,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") == "" {
diff --git a/shell/README.md b/shell/README.md
new file mode 100644
index 0000000..b5ee140
--- /dev/null
+++ b/shell/README.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 ../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:
+
+    if [ -f ~/.config/exercism/exercism_completion.bash ]; then
+      source ~/.config/exercism/exercism_completion.bash
+    fi
+
+### Zsh
+
+    mkdir -p ~/.config/exercism
+    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
+
+    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/shell/exercism_completion.bash b/shell/exercism_completion.bash
new file mode 100644
index 0000000..aa383d4
--- /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 0000000..44a9cc6
--- /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


Test patch:
diff --git a/cmd/download_test.go b/cmd/download_test.go
index c3865b9..ebb8f81 100644
--- a/cmd/download_test.go
+++ b/cmd/download_test.go
@@ -13,35 +13,38 @@ 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 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
@@ -61,10 +64,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 {
@@ -104,14 +107,52 @@ 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)
+		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 makeMockServer() *httptest.Server {
+func fakeDownloadServer() *httptest.Server {
 	mux := http.NewServeMux()
 	server := httptest.NewServer(mux)
 
@@ -136,35 +177,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)
 	}
 }
+`