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" 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/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 @@ diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..5c6cb5943 --- /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: 'monthly' + labels: + - 'x:size/small' diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 000000000..cd989c70f --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,177 @@ +# --------------------------------------------------------------- # +# 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 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. +# 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 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" + description: "Work on something from scratch" + color: "ffffff" + +- name: "x:action/fix" + description: "Fix an issue" + color: "ffffff" + +- name: "x:action/improve" + description: "Improve existing functionality/content" + color: "ffffff" + +- name: "x:action/proofread" + description: "Proofread text" + color: "ffffff" + +- name: "x:action/sync" + description: "Sync content with its latest version" + 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: "ffffff" + +- name: "x:knowledge/elementary" + description: "Little Exercism knowledge required" + color: "ffffff" + +- name: "x:knowledge/intermediate" + description: "Quite a bit of Exercism knowledge required" + color: "ffffff" + +- name: "x:knowledge/advanced" + description: "Comprehensive Exercism knowledge required" + 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: "ffffff" + +- name: "x:module/concept" + description: "Work on Concepts" + color: "ffffff" + +- name: "x:module/concept-exercise" + description: "Work on Concept Exercises" + color: "ffffff" + +- name: "x:module/generator" + description: "Work on Exercise generators" + color: "ffffff" + +- name: "x:module/practice-exercise" + description: "Work on Practice Exercises" + color: "ffffff" + +- name: "x:module/representer" + description: "Work on Representers" + color: "ffffff" + +- name: "x:module/test-runner" + 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" + color: "ffffff" + +- name: "x:size/small" + description: "Small amount of work" + color: "ffffff" + +- name: "x:size/medium" + description: "Medium amount of work" + color: "ffffff" + +- name: "x:size/large" + description: "Large amount of work" + color: "ffffff" + +- name: "x:size/massive" + description: "Massive amount of work" + 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: "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: "ffffff" + +- name: "x:type/coding" + description: "Write code that is not student-facing content (e.g. test-runners, generators, but not exercises)" + color: "ffffff" + +- name: "x:type/content" + description: "Work on content (e.g. exercises, concepts)" + color: "ffffff" + +- name: "x:type/docker" + description: "Work on Dockerfiles" + color: "ffffff" + +- name: "x:type/docs" + description: "Work on Documentation" + color: "ffffff" + +# 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. # +# ----------------------------------------------------------------------------------------- # + +- name: "bug?" + description: "" + color: "eb6420" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..c6f903425 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +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.26.x' + os: [ubuntu-latest, windows-latest, macOS-latest] + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e + 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@3d3c42e5aac5ba805825da76410c181273ba90b1 + + - name: Check formatting + run: ./.gha.gofmt.sh diff --git a/.github/workflows/pause-community-contributions.yml b/.github/workflows/pause-community-contributions.yml new file mode 100644 index 000000000..46f0c60b4 --- /dev/null +++ b/.github/workflows/pause-community-contributions.yml @@ -0,0 +1,22 @@ +name: Pause Community Contributions + +on: + issues: + types: + - opened + pull_request_target: + types: + - opened + +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 + secrets: + github_membership_token: ${{ secrets.COMMUNITY_CONTRIBUTIONS_WORKFLOW_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..a28848ea2 --- /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-26.04 + steps: + + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: '1.26.x' + + - name: Import GPG Key + id: import_gpg + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.PASSPHRASE }} + + - name: Cut Release + uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2 + 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/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml new file mode 100644 index 000000000..e7b99e504 --- /dev/null +++ b/.github/workflows/sync-labels.yml @@ -0,0 +1,19 @@ +name: Tools + +on: + push: + branches: + - main + paths: + - .github/labels.yml + - .github/workflows/sync-labels.yml + workflow_dispatch: + schedule: + - cron: 0 0 1 * * # First day of each month + +permissions: + issues: write + +jobs: + sync-labels: + uses: exercism/github-actions/.github/workflows/labels.yml@main 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/.goreleaser.yml b/.goreleaser.yml index 564944e1a..86f2100cc 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,55 +1,113 @@ # You can find the GoReleaser documentation at http://goreleaser.com +version: 2 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 - - 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 filters: exclude: - - '^docs:' - - '^test:' + - "^docs:" + - "^test:" archives: - - name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" - replacements: - amd64: x86_64 - 386: i386 + - id: release-archives + ids: + - release-build + 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 + formats: ["zip"] + files: + - shell/** + - LICENSE + - README.md + - id: installer-archives + ids: + - 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 + - goos: windows + formats: ["zip"] files: - - shell/**/* + - 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. @@ -71,30 +129,3 @@ release: # You can change the name of the GitHub 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 - 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"] diff --git a/.release/header.md b/.release/header.md new file mode 100644 index 000000000..c648e2d8b --- /dev/null +++ b/.release/header.md @@ -0,0 +1,3 @@ +To install, follow the interactive installation instructions at https://exercism.org/cli-walkthrough + +--- diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a30fc6eca..000000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: go - -sudo: false - -go: - - "1.11.x" - - "1.12.x" - - tip - -matrix: - allow_failures: - - go: tip - -script: - - ./.travis.gofmt.sh - - go test -cover ./... diff --git a/CHANGELOG.md b/CHANGELOG.md index ccec41b11..9de216f5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,435 +2,540 @@ The exercism CLI follows [semantic versioning](http://semver.org/). ----------------- +--- ## Next Release -* **Your contribution here** + +- **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] +- [#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] +- [#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] + +## 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] + +## 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] + +## 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 - + [@muzimuzhi] + +## 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 + +## 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 + +## 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] -* [#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. +This is a complete rewrite from the ground up to work against the new https://exercism.org 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#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) -* [#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#1039](https://github.com/exercism/exercism/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#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) -* [#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 - -[@AlexWheeler]: https://github.com/AlexWheeler +- 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 [@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 @@ -445,21 +550,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 @@ -482,5 +590,20 @@ 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 +[@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 +[@ageron]: https://github.com/ageron +[@petrem]: https://github.com/petrem +[@bnandras]: https://github.com/bnandras +[@vaeng]: https://github.com/vaeng +[@keiravillekode]: https://github.com/keiravillekode diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..3f7813de1 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,93 @@ +# 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.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. + +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. Discord, Forum, Twitter, email) and any other Exercism entity or event._ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index df85ab2cf..471c48688 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,12 +5,10 @@ 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 -You'll need Go version 1.11 or higher. Follow the directions on http://golang.org/doc/install +You'll need Go version 1.26 or higher. Follow the directions on http://golang.org/doc/install ## Development @@ -40,7 +38,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: @@ -50,4 +48,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). diff --git a/README.md b/README.md index fa1c45db8..8b0fa92c6 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. @@ -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 c8c1c92f0..17182f601 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,102 +1,54 @@ # 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 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/) - -## 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). +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/) ## Bump the version -Edit the `Version` constant in `cmd/version.go` +1. Create a branch for the new version +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 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`._ - -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. +_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 -```bash -# Test run -goreleaser --skip-publish --snapshot --rm-dist - -# 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 +Once the version bump PR has been merged, run the following command to cut a release: -# Build and release -goreleaser --rm-dist - -# 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 +```shell +GPG_FINGERPRINT="" ./bin/release.sh ``` ## 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. +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 +To install, follow the interactive installation instructions at https://exercism.org/cli-walkthrough --- -[describe changes in this release] +[modify the generated release-notes to describe changes in this release] ``` - Lastly, test and publish the draft - - -## Update Homebrew +Lastly, test and then publish the draft. -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: - -``` -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 -``` +## Homebrew -For more information see [How To Open a Homebrew Pull Request](https://docs.brew.sh/How-To-Open-a-Homebrew-Pull-Request). +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/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/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 ./... 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/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." 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 ./... 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..d8b9bf4d3 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -6,15 +6,16 @@ import ( "bytes" "compress/gzip" "encoding/json" + "errors" "fmt" "io" - "io/ioutil" "net/http" + "os" "runtime" "strings" "time" - "github.com/blang/semver" + semver "github.com/blang/semver/v4" "github.com/exercism/cli/debug" update "github.com/inconshreveable/go-update" ) @@ -150,7 +151,7 @@ func (c *CLI) fetchLatestRelease() error { for k, v := range resp.Header { msg += fmt.Sprintf("\n %s:\n %s", k, v) } - return fmt.Errorf(msg) + return errors.New(msg) } var rel Release @@ -161,8 +162,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 +192,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.go b/cmd/cmd.go index 9fa48acdc..ac28286a7 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -2,8 +2,11 @@ package cmd import ( "encoding/json" + "errors" "fmt" "net/http" + "regexp" + "strconv" "strings" "io" @@ -23,6 +26,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 = ` @@ -55,7 +60,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. ` @@ -64,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, ) } @@ -77,6 +82,29 @@ 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", + resp.Status, + contentType, + ) + } var apiError struct { Error struct { Type string `json:"type"` @@ -95,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 76ed0634e..d712e742b 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -3,7 +3,9 @@ package cmd import ( "io" "io/ioutil" + "net/http" "os" + "strings" "testing" "github.com/spf13/cobra" @@ -26,12 +28,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) // ... @@ -61,7 +65,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) @@ -102,8 +106,8 @@ func newCapturedOutput() capturedOutput { return capturedOutput{ oldOut: Out, oldErr: Err, - newOut: ioutil.Discard, - newErr: ioutil.Discard, + newOut: io.Discard, + newErr: io.Discard, } } @@ -118,3 +122,80 @@ func (co capturedOutput) reset() { Out = co.oldOut Err = co.oldErr } + +func errorResponse418(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 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: 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: 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", + }, + { + response: errorResponse418("application/json", `{"error": {"message": "message"}}`), + wantMessage: "message", + }, + { + response: errorResponse418("application/problem+json", `{"error": {"message": "new json format"}}`), + wantMessage: "new json format", + }, + { + 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) + 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/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/configure_test.go b/cmd/configure_test.go index 8d3643522..df3542328 100644 --- a/cmd/configure_test.go +++ b/cmd/configure_test.go @@ -1,10 +1,9 @@ -// +build !windows +//go:build !windows package cmd import ( "bytes" - "io/ioutil" "net/http" "net/http/httptest" "os" @@ -343,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) @@ -378,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) @@ -395,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 e4c19377b..0e74c49ed 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" @@ -64,6 +63,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 } @@ -98,12 +101,7 @@ 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 - } - // 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 +131,8 @@ type download struct { token, apibaseurl, workspace string // optional - track, team string + track string + forceoverwrite bool payload *downloadPayload } @@ -153,7 +152,11 @@ 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 + } + + d.forceoverwrite, err = flags.GetBool("force") if err != nil { return nil, err } @@ -168,7 +171,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 } @@ -193,8 +196,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) @@ -218,9 +221,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() } @@ -248,11 +248,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 } @@ -261,10 +261,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"` @@ -295,7 +291,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, @@ -353,7 +348,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") + 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..fb18cfc86 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" @@ -27,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", err.Error()) } } @@ -161,19 +160,14 @@ 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 { - tmpDir, err := ioutil.TempDir("", "download-cmd") + tmpDir, err := os.MkdirTemp("", "download-cmd") 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() @@ -197,7 +191,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) @@ -209,7 +203,101 @@ func TestDownload(t *testing.T) { } } -func fakeDownloadServer(requestor, teamSlug string) *httptest.Server { +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"}, + }, + } + + for _, tc := range testCases { + tmpDir, err := os.MkdirTemp("", "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"}, + }, + } + + for _, tc := range testCases { + tmpDir, err := os.MkdirTemp("", "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 string) *httptest.Server { mux := http.NewServeMux() server := httptest.NewServer(mux) @@ -226,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) }) @@ -261,7 +345,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)) }) @@ -269,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) { @@ -281,7 +365,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) @@ -302,7 +386,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()) } @@ -314,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/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 } diff --git a/cmd/root.go b/cmd/root.go index aa97c4670..d94d5c847 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -14,9 +14,9 @@ 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 the v2 redesign of Exercism. + Long: `A command-line interface for Exercism. Download exercises and submit your solutions.`, SilenceUsage: true, @@ -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 diff --git a/cmd/submit.go b/cmd/submit.go index 4f2a8df72..005cb8cc4 100644 --- a/cmd/submit.go +++ b/cmd/submit.go @@ -19,14 +19,15 @@ 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. `, - Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cfg := config.NewConfig() @@ -45,6 +46,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 +123,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 @@ -182,7 +208,7 @@ func (s *submitCmdContext) migrateLegacyMetadata(exercise workspace.Exercise) er return err } if verbose, _ := s.flags.GetBool("verbose"); verbose { - fmt.Fprintf(Err, migrationStatus.String()) + Err.Write([]byte(migrationStatus.String())) } return nil } @@ -286,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_symlink_test.go b/cmd/submit_symlink_test.go index b2d30ac4c..093895fb3 100644 --- a/cmd/submit_symlink_test.go +++ b/cmd/submit_symlink_test.go @@ -1,9 +1,8 @@ -// +build !windows +//go:build !windows package cmd import ( - "io/ioutil" "os" "path/filepath" "testing" @@ -24,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) @@ -52,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 c20c47748..38caf9d93 100644 --- a/cmd/submit_test.go +++ b/cmd/submit_test.go @@ -4,7 +4,8 @@ import ( "bytes" "encoding/json" "fmt" - "io/ioutil" + "io" + "mime" "net/http" "net/http/httptest" "os" @@ -27,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", err.Error()) } } @@ -48,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) @@ -63,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"), @@ -80,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) @@ -88,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() @@ -108,8 +109,47 @@ func TestSubmitExerciseWithoutMetadataFile(t *testing.T) { } } +func TestGetExerciseSolutionFiles(t *testing.T) { + + tmpDir, err := os.MkdirTemp("", "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 := os.MkdirTemp("", "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 = os.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") + tmpDir, err := os.MkdirTemp("", "submit-no-such-file") defer os.RemoveAll(tmpDir) assert.NoError(t, err) @@ -124,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"), @@ -152,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) @@ -172,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) @@ -192,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) @@ -201,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() @@ -249,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) @@ -266,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() @@ -312,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) @@ -332,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) @@ -353,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) @@ -373,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) } @@ -385,59 +425,12 @@ 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 := 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)) - 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) - - 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.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() 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) @@ -457,7 +450,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) { @@ -466,7 +459,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) @@ -479,11 +472,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() @@ -518,11 +511,20 @@ 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) } - 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, "{}") @@ -540,7 +542,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) @@ -559,7 +561,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) @@ -580,7 +582,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) @@ -599,7 +601,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{ @@ -608,7 +610,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) { @@ -619,7 +621,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) @@ -638,7 +640,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{ @@ -654,7 +656,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) @@ -672,7 +674,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() @@ -697,7 +699,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) @@ -706,7 +708,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/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/cmd/troubleshoot.go b/cmd/troubleshoot.go index 5fd134d3c..f83d5cc96 100644 --- a/cmd/troubleshoot.go +++ b/cmd/troubleshoot.go @@ -21,12 +21,12 @@ 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. 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 @@ -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) @@ -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 @@ -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. 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 }, diff --git a/cmd/version.go b/cmd/version.go index 3b81f5897..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.0.13" +const Version = "3.5.8" // checkLatest flag for version command. var checkLatest bool diff --git a/config/config.go b/config/config.go index b9b1883ba..f16b2b6ce 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,14 +121,14 @@ 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") } -// 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), "/settings/api_cli") } diff --git a/config/config_notwin_test.go b/config/config_notwin_test.go index f7e4564da..33912cc9b 100644 --- a/config/config_notwin_test.go +++ b/config/config_notwin_test.go @@ -1,4 +1,4 @@ -// +build !windows +//go:build !windows package config 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/config/config_windows_test.go b/config/config_windows_test.go index e4374def3..5757b783f 100644 --- a/config/config_windows_test.go +++ b/config/config_windows_test.go @@ -1,4 +1,4 @@ -// +build windows +//go:build windows package config 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 } } diff --git a/config/resolve_notwin_test.go b/config/resolve_notwin_test.go index d44b73714..fb70d5f69 100644 --- a/config/resolve_notwin_test.go +++ b/config/resolve_notwin_test.go @@ -1,4 +1,4 @@ -// +build !windows +//go:build !windows package config diff --git a/debug/debug.go b/debug/debug.go index dad0b629c..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,12 +41,12 @@ 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) - 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,8 +61,8 @@ func DumpRequest(req *http.Request) { Println("========================= END DumpRequest =========================") Println("") - req.Header.Set("Authorization", temp) - req.Body = ioutil.NopCloser(&bodyCopy) + req.Header.Set("Authorization", authHeader) + 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/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" 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/go.mod b/go.mod index c0d986d0f..41f9d07a5 100644 --- a/go.mod +++ b/go.mod @@ -1,26 +1,31 @@ module github.com/exercism/cli +go 1.26.0 + 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/blang/semver/v4 v4.0.0 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 + github.com/spf13/cobra v1.10.0 + github.com/spf13/pflag v1.0.10 + github.com/spf13/viper v1.21.0 + github.com/stretchr/testify v1.11.1 + golang.org/x/net v0.55.0 + golang.org/x/text v0.37.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sys v0.45.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0a9864259..0bb204f45 100644 --- a/go.sum +++ b/go.sum @@ -1,43 +1,60 @@ -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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 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= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.0 h1:a5/WeUlSDCvV5a45ljW2ZFtV0bTDpkfSAj3uqB6Sc+0= +github.com/spf13/cobra v1.10.0/go.mod h1:9dhySC7dnTtEiqzmqfkLj47BslqLCUPMXjG2lj/NgoE= +github.com/spf13/pflag v1.0.8/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 diff --git a/shell/exercism.fish b/shell/exercism.fish index dc20fdce0..4abe9b21e 100644 --- a/shell/exercism.fish +++ b/shell/exercism.fish @@ -9,22 +9,25 @@ 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" # 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." +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 +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..542c8a576 100644 --- a/shell/exercism_completion.zsh +++ b/shell/exercism_completion.zsh @@ -6,8 +6,9 @@ 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." version:"Outputs version information." 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 new file mode 100644 index 000000000..35ae7545c --- /dev/null +++ b/workspace/exercise_config.go @@ -0,0 +1,57 @@ +package workspace + +import ( + "encoding/json" + "errors" + "os" + "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 := os.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..5d9847453 --- /dev/null +++ b/workspace/exercise_config_test.go @@ -0,0 +1,97 @@ +package workspace + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExerciseConfig(t *testing.T) { + dir, err := os.MkdirTemp("", "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 := os.MkdirTemp("", "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 := os.MkdirTemp("", "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 := os.MkdirTemp("", "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/exercise_metadata.go b/workspace/exercise_metadata.go index 0a0305576..3ec902c1b 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" @@ -21,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"` @@ -32,7 +30,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 +70,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 @@ -99,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/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 0b8421019..a80b6171a 100644 --- a/workspace/path_type_symlinks_test.go +++ b/workspace/path_type_symlinks_test.go @@ -1,4 +1,4 @@ -// +build !windows +//go:build !windows package workspace diff --git a/workspace/test_configurations.go b/workspace/test_configurations.go new file mode 100644 index 000000000..7d5672f69 --- /dev/null +++ b/workspace/test_configurations.go @@ -0,0 +1,307 @@ +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, " ")) + } + if strings.Contains(cmd, "{{slug}}") { + metadata, err := NewExerciseMetadata(".") + if err != nil { + return "", err + } + cmd = strings.ReplaceAll(cmd, "{{slug}}", metadata.ExerciseSlug) + } + + 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: "8th -f test.8th", + }, + // abap: tests are run via "ABAP Development Tools", not the CLI + "arm64-assembly": { + Command: "make", + }, + "arturo": { + Command: "arturo tester.art", + }, + "awk": { + Command: "bats {{test_files}}", + }, + "ballerina": { + Command: "bal test", + }, + "batch": { + WindowsCommand: "cmd /c {{test_files}}", + }, + "bash": { + Command: "bats {{test_files}}", + }, + "c": { + Command: "make", + }, + "cairo": { + Command: "scarb cairo-test", + }, + "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_files}} -f ert-run-tests-batch-and-exit", + }, + "erlang": { + Command: "rebar3 eunit", + }, + "factor": { + Command: "factor -roots=. -run=exercism-tools {{slug}}", + }, + "fortran": { + Command: "make", + }, + "free-pascal": { + Command: "make test=all", + }, + "fsharp": { + Command: "dotnet test", + }, + "futhark": { + Command: "futhark test test.fut", + }, + "gleam": { + Command: "gleam test", + }, + "go": { + Command: "go test", + }, + "groovy": { + Command: "gradle test", + }, + "haskell": { + Command: "stack test", + }, + "idris": { + Command: "pack test {{slug}}", + }, + "j": { + Command: `jconsole -js "exit echo unittest {{test_files}} [ load {{solution_files}}"`, + }, + "java": { + Command: "./gradlew test", + WindowsCommand: "gradlew.bat test", + }, + "javascript": { + Command: "npm run test", + }, + "jq": { + Command: "bats {{test_files}}", + }, + "julia": { + Command: "julia runtests.jl", + }, + "kotlin": { + Command: "./gradlew test", + WindowsCommand: "gradlew.bat test", + }, + "lean": { + Command: "lake test", + }, + "lfe": { + Command: "make test", + }, + "lua": { + Command: "busted", + }, + "mips": { + Command: "java -jar /path/to/mars.jar nc runner.mips impl.mips", + }, + "moonscript": { + Command: "busted", + }, + "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", + }, + "odin": { + Command: "odin test .", + }, + "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", + }, + "pyret": { + Command: "pyret {{test_files}}", + }, + "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}}", + }, + "roc": { + Command: "roc test {{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", + }, + "uiua": { + Command: "uiua test {{test_files}}", + }, + // 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", + }, + "yamlscript": { + Command: "make test", + }, + "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..b8a8097ee --- /dev/null +++ b/workspace/test_configurations_test.go @@ -0,0 +1,143 @@ +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") +} + +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") +} diff --git a/workspace/workspace.go b/workspace/workspace.go index 71036f511..dbd0604f1 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -2,9 +2,10 @@ package workspace import ( "errors" - "io/ioutil" + "fmt" "os" "path/filepath" + "runtime" "strings" ) @@ -44,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 } @@ -57,29 +58,7 @@ 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())) + subInfos, err := os.ReadDir(filepath.Join(ws.Dir, topInfo.Name())) if err != nil { return nil, err } @@ -121,7 +100,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..47bcab7ec --- /dev/null +++ b/workspace/workspace_darwin_test.go @@ -0,0 +1,26 @@ +package workspace + +import ( + "fmt" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +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()) +} diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go index 160c7cd0e..8614ddc2c 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) @@ -20,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) } @@ -39,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() @@ -49,12 +45,11 @@ 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") } } 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 +64,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) } }