From 94411e1984bfdab55af7cf6fb570c2838edf416e Mon Sep 17 00:00:00 2001 From: Bob Woolsey Date: Sun, 21 Nov 2021 01:41:37 -0500 Subject: [PATCH] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 06c06c87dc02e5a7b50d9771ae4178bd10317e8d Merge: dcf3f60e a4015b7f Author: Nate Smith Date: Fri Oct 15 15:14:49 2021 -0500 Merge pull request #3833 from cristiand391/gh-run-cancel Add `run cancel` command commit a4015b7f09438cfe1729a0112b55aa0939393787 Author: nate smith Date: Fri Oct 15 15:08:53 2021 -0500 prompt tests commit f381a804fce659ec26ee02e27bd47afd0893a7d7 Author: nate smith Date: Fri Oct 15 14:57:20 2021 -0500 fix tests commit 18975e61d135fc0d20049efea721dcad63d09652 Author: nate smith Date: Fri Oct 15 14:31:29 2021 -0500 fix imports commit b81eda0c46baa6fd70ba4453ea0f66a9e5676c62 Author: nate smith Date: Fri Oct 15 14:28:28 2021 -0500 newline commit f329ebd7cafd6a8492b7f924422107a457270a0d Author: nate smith Date: Fri Oct 15 14:19:16 2021 -0500 add interactive prompt for in progress runs commit dcf3f60e00aa0888a1f1638afdc048413a9b3c84 Merge: 6f34e4a6 70c78f2a Author: Nate Smith Date: Fri Oct 15 14:13:58 2021 -0500 Merge pull request #4007 from bchadwic/relative-path gh browse relative path enhancement commit 4a01e6b7021fba5f65f63b6fd17cd11230cea2c5 Merge: 49652cde 6f34e4a6 Author: nate smith Date: Fri Oct 15 14:12:53 2021 -0500 Merge remote-tracking branch 'origin/trunk' into gh-run-cancel commit 6f34e4a6057260402c98ab9276912bd02930635c Author: Marwan Sulaiman Date: Fri Oct 15 09:51:46 2021 -0400 Rename privacy sub-command to visibility (#4533) * Rename privacy sub-command to visibility * PR fixes commit dc614f66a91d98953c715ce2a728fa4bc6077b71 Merge: 9a86a2ee 1e52b4c7 Author: Mislav Marohnić Date: Fri Oct 15 13:50:21 2021 +0200 Merge pull request #4529 from nickfyson/change_expected_status change expected delete status to 202 commit 1e52b4c7f95852c19158ff483cfa5beb1913e805 Author: Nick Fyson Date: Fri Oct 15 12:30:11 2021 +0100 change expected delete status to 202 commit 9a86a2ee6a45a675478deb8f6c2cb37572180f7e Merge: 1aec8923 4504e49e Author: Jose Garcia Date: Thu Oct 14 20:13:35 2021 -0400 Merge pull request #4524 from cli/jg/fix-trunk codespace: update running method commit 4504e49e960a994147271e560e1c023cb9c3e809 Author: Jose Garcia Date: Thu Oct 14 20:02:02 2021 -0400 Update running method commit 1aec8923266f5b0ff7d48e916d3090640356516a Merge: f6b33572 5e56b4a7 Author: Jose Garcia Date: Thu Oct 14 19:59:59 2021 -0400 Merge pull request #4520 from cli/jg/new-api-shape codespace: implement new API payload commit 70c78f2aa896f2c6baf9a045c087f1042fd4b62d Author: nate smith Date: Thu Oct 14 17:07:51 2021 -0500 some fixes, streamlining commit f6b33572fddc0df381ba6323d0c05b605f462914 Merge: cb89b8ca 8f581fa4 Author: Mislav Marohnić Date: Thu Oct 14 20:54:56 2021 +0200 Merge pull request #4279 from SiarheiFedartsou/sf-pr-list-head-filter Add `--head` filter to `gh pr list` commit cb89b8ca7a706cf51927477ad3f979ea4769e16e Merge: b0360612 defbf0f3 Author: Nate Smith Date: Thu Oct 14 13:39:14 2021 -0500 Merge pull request #4442 from cli/cleanup-upgrade Cleanup extension upgrade command commit b0360612d21808304dc6d4c7cc18a4030e6cbb8b Merge: c2abe170 0748e658 Author: Mislav Marohnić Date: Thu Oct 14 20:29:21 2021 +0200 Merge pull request #4521 from cli/jg/bind-locally codespace: switches port binding to 127.0.0.1 where possible commit c2abe170d85fee3314d7a72fe3b025d22da0e0f1 Merge: 78443964 1464a8a0 Author: Mislav Marohnić Date: Thu Oct 14 20:29:12 2021 +0200 Merge pull request #4512 from cli/changelog-api Generate release notes using the new API commit 1464a8a0f35397ccdbf2c07aedddcbf35bd70c1d Author: Mislav Marohnić Date: Wed Oct 13 20:51:37 2021 +0200 Generate release notes using the new API https://docs.github.com/en/rest/reference/repos#generate-release-notes-content-for-a-release commit 78443964d44394dc4fa0e313d94af2459f229359 Merge: 78ac7718 89ad8701 Author: Mislav Marohnić Date: Thu Oct 14 20:21:13 2021 +0200 Merge pull request #4513 from cli/missing-oauth-scopes Warn about missing OAuth scopes when reporting HTTP 4xx errors commit 89ad87019043642fc8ef144fa51eb9b8e0601a5c Author: Mislav Marohnić Date: Thu Oct 14 19:52:59 2021 +0200 auth refresh: preserve existing scopes when requesting new ones When there was a previously valid token that was granted some scopes, ensure all those scopes will be re-requested when doing the authentication flow for the new token. commit 4996ba2ba4e0d6c15b90a7cae35f06215290b542 Merge: 59930186 78ac7718 Author: nate smith Date: Thu Oct 14 12:24:27 2021 -0500 Merge remote-tracking branch 'origin/trunk' into relative-path commit 64a19ee71feb0b5d1024227ff87e4104e36e896f Author: Mislav Marohnić Date: Thu Oct 14 18:36:55 2021 +0200 Remove OAuth scopes checking logic from `ssh-key` commands Scopes checking is now handled on the HTTP client level for all commands. commit 78ac77180e833acaf233fb252d4c8e232800d603 Merge: b0905a41 5bdaab88 Author: Nate Smith Date: Thu Oct 14 11:31:15 2021 -0500 Merge pull request #3950 from bchadwic/browse-commit Add feature open latest commit in gh browse commit 5bdaab882b77e2c870a50d208284dc5c374108c1 Author: nate smith Date: Thu Oct 14 11:25:33 2021 -0500 fix commit 693193fe847879d09e60525fc1e180eb94587025 Author: Mislav Marohnić Date: Thu Oct 14 18:16:04 2021 +0200 Consistent error handling in codespaces API operations Using HandleHTTPError ensures that hints regarding insufficient OAuth scopes will be properly reported on stderr. commit 2c3f02ee62fe18e6472d6ff159ef24a4d42c1309 Author: Mislav Marohnić Date: Thu Oct 14 17:30:05 2021 +0200 Ensure NOT_FOUND error when querying private repos using insufficient scope commit b0905a415f1bd17ad083130c739778e94e0af29a Merge: fccc9101 07fa60b6 Author: Jose Garcia Date: Thu Oct 14 11:22:25 2021 -0400 Merge pull request #4508 from cli/jg/codespace-stop codespace stop: new command to stop a running codespace commit 0748e658ccdf5ff0f1f37a6ab758674630a2259f Author: Jose Garcia Date: Thu Oct 14 11:07:25 2021 -0400 Switches port binding to 127.0.0.1 where possible commit fccc910166930abe63c6fab92655fc7b2af07a5a Merge: 53479c71 d68126af Author: Mislav Marohnić Date: Thu Oct 14 16:15:42 2021 +0200 Merge pull request #4517 from cli/macos-firewall-prompt-skip Avoid macOS prompt to allow incoming connections in liveshare tests commit 5e56b4a7ceb04c78b3e9d4de859031b3e2cc53a8 Author: Jose Garcia Date: Thu Oct 14 09:21:03 2021 -0400 Fix tests commit 3544275c2ff5588bc746fcf0091e4e2cc1b0b320 Author: Jose Garcia Date: Thu Oct 14 09:10:15 2021 -0400 Implement new API payload for a Codespace commit d68126af9970a9e4f0e4c429f199fbfe4c42e9ac Author: Mislav Marohnić Date: Thu Oct 14 14:23:36 2021 +0200 Avoid macOS prompt to allow incoming connections in liveshare tests Listening on the localhost interface disallows connections from the outside anyway, so the OS firewall doesn't have to prompt whether the user wants to allow incoming connections to the Go process. commit d72d0f47f617ac716a15348451b27f104e06031b Merge: 285f8659 53479c71 Author: nate smith Date: Wed Oct 13 16:53:50 2021 -0500 Merge remote-tracking branch 'origin/trunk' into browse-commit commit 2ca18e0600223d8e2e8588a75cf693d17598fe43 Author: Mislav Marohnić Date: Wed Oct 13 23:24:14 2021 +0200 Warn about missing OAuth scopes when reporting HTTP 4xx errors If a 4xx server response lists scopes in the X-Accepted-Oauth-Scopes header that are not present in the X-Oauth-Scopes header, the final error messaging on stderr will now include a hint for the user that they might need to request the additional scope: $ gh codespace list error getting codespaces: HTTP 403: Must have admin rights to Repository. (https://api.github.com/user/codespaces?per_page=30) This API operation needs the "codespace" scope. To request it, run: gh auth refresh -h github.com -s codespace commit 53479c712c6069ca7b5ce3cccf835c2e87689d15 Merge: d5c9630f 127e2dae Author: Mislav Marohnić Date: Wed Oct 13 21:01:39 2021 +0200 Merge pull request #4510 from cli/dependabot-patch Configure Dependabot to only consider patch version bumps commit d5c9630faed73530cad87ac898c404fb463cb835 Merge: 9f1a1d88 3dbec865 Author: Jose Garcia Date: Wed Oct 13 14:44:10 2021 -0400 Merge pull request #4511 from cli/jg/keepalive-fix codespace ssh: fix for nil logger on non-debugging scenarios commit 3dbec8655688b54df93918996c942963872fd5d4 Author: Jose Garcia Date: Wed Oct 13 14:30:33 2021 -0400 PR Feedback commit d6b5157effdc923ab6d21655f8220a9d94e26f2e Author: Jose Garcia Date: Wed Oct 13 14:15:26 2021 -0400 Fix for nil logger on non-debugging scenarios commit 9f1a1d88052b412126772eb813c7ae43c451f6d0 Merge: b9bdef2b cf80fbe5 Author: Mislav Marohnić Date: Wed Oct 13 20:12:17 2021 +0200 Merge pull request #4509 from cli/downgrade-spinner Downgrade spinner package due to cleanup bug commit 127e2dae990aa3d7ecdcf056421f4388a7535ef3 Author: Mislav Marohnić Date: Wed Oct 13 20:06:19 2021 +0200 Configure Dependabot to only consider patch version bumps https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates#ignore commit b9bdef2b002e07b22e6f5e5e341612ae58c686b3 Author: Marwan Sulaiman Date: Wed Oct 13 13:56:03 2021 -0400 Add org scoped port forwarding + fix test formatting (#4497) * Add org scoped port forwarding + fix test formatting * Redesign port visibility * Update pkg/cmd/codespace/ports.go Co-authored-by: Jose Garcia * Change sub command to privacy * Example pr comment * Fix test mock * Fix test mock Co-authored-by: Jose Garcia commit cf80fbe509efc6079d05827a556f152101cf31d7 Author: Mislav Marohnić Date: Wed Oct 13 19:45:20 2021 +0200 Downgrade spinner package due to cleanup bug The spinner is not successfully visually cleaned up after calling its Stop method. https://github.com/briandowns/spinner/issues/123 commit 07fa60b69a6a47a60f270f13abdc37d5e5779aae Author: Jose Garcia Date: Wed Oct 13 11:46:57 2021 -0400 PR Feedback commit a033b85fa294969ed2df0ed14dac7f7c4514cdd6 Merge: 12e5b942 97b52b30 Author: Jose Garcia Date: Wed Oct 13 11:21:43 2021 -0400 Merge pull request #4461 from cli/jg/liveshare-keepalive codespace/liveshare keepalive: keep LS sessions alive with PF traffic commit be10950058cf45ccf8d5e8c7692e780aeb7fff90 Author: Jose Garcia Date: Wed Oct 13 11:04:15 2021 -0400 Update mocks commit 12e5b9420557061d907948d1659185189faddc58 Merge: 675ee316 961cd57b Author: Mislav Marohnić Date: Wed Oct 13 16:58:36 2021 +0200 Merge pull request #4507 from alefranz/patch-1 Update winget installation instructions commit 4fb4a21efd0f3e2383d6dd3657067ba450c8430c Author: Jose Garcia Date: Wed Oct 13 10:57:50 2021 -0400 Rename + docs commit 97b52b30fca88bfe6bd46b3bbc116977aaa5b806 Merge: 5170a293 675ee316 Author: Jose Garcia Date: Wed Oct 13 10:45:40 2021 -0400 Merge branch 'trunk' of github.com:cli/cli into jg/liveshare-keepalive commit 961cd57b8783837a054836af53d21e19b9d38137 Author: Alessio Franceschelli Date: Wed Oct 13 15:01:50 2021 +0100 Update winget installation instructions commit 675ee316e9727a1d9822ba68a0565c45bffab461 Merge: aa227079 9302e68c Author: Alan Donovan Date: Wed Oct 13 09:00:37 2021 -0400 Merge pull request #4504 from cli/show-http-status cs create: include HTTP status code in error from /user/codespaces/*/start commit aa2270799ca0357618d30222f9f3368dcc714ced Merge: 968b093e 77a86e86 Author: Jose Garcia Date: Wed Oct 13 08:29:11 2021 -0400 Merge pull request #4505 from cli/jg/fix-connection codespace: fix for API response body change commit 77a86e8611eeb9c7681dda49a7d36ed9ce520d67 Author: Jose Garcia Date: Wed Oct 13 08:07:59 2021 -0400 Fix for API response body change - Connection is now embedded within the top level of the Codespace payload instead of inside the environment. commit ea0d0a543faa50c4e57eea019f09c6b64a9921bd Author: Jose Garcia Date: Wed Oct 13 07:45:54 2021 -0400 Initial StopCodespace implementation - API - Command commit 9302e68c92eeb2687c169c9a84d574df2e5e7053 Author: Alan Donovan Date: Wed Oct 13 07:00:55 2021 -0400 Include HTTP status code in error commit defbf0f306b665bf86847ff5d704df2a8addd308 Author: Sam Coe Date: Tue Oct 5 11:17:35 2021 -0700 Make extension upgrade output more friendly commit 968b093eda2c9c18a41ed9150546f7e0820451ed Merge: 7abf682e ed342797 Author: Sam Date: Tue Oct 12 15:53:51 2021 -0700 Merge pull request #4396 from cli/speedy-extension-list Use concurrency to check for extension updates commit 5170a2931f9cd8378fafeba89cab1db014a2de43 Author: Jose Garcia Date: Tue Oct 12 15:45:05 2021 -0400 Switch to standard lib log.Logger & support dfile - --debug-file flag can now be used in conjuction with --debug to specify the debug file path - Push out logger concerns to callers of liveshare commit 7abf682e26d4619c9639e6b1ece8d5b810e0bf04 Merge: efd0b677 2819deb1 Author: Mislav Marohnić Date: Tue Oct 12 18:56:24 2021 +0200 Merge pull request #4480 from cli/codeql-dependabot CodeQL-Dependabot compatibility commit ed3427974c1746c15f7873cd89688beea897855b Author: Sam Coe Date: Thu Sep 30 08:10:42 2021 -0700 Use concurrency to check for extension updates commit efd0b677ef515dbec2b667cfdf456be380a962df Merge: ec554822 5fd9f68c Author: Mislav Marohnić Date: Tue Oct 12 17:27:29 2021 +0200 Merge pull request #4496 from cli/dependabot/go_modules/github.com/gabriel-vasile/mimetype-1.4.0 Bump github.com/gabriel-vasile/mimetype from 1.1.2 to 1.4.0 commit 5fd9f68c85072f5c6356ea605a07dbacd75422ff Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue Oct 12 14:35:16 2021 +0000 Bump github.com/gabriel-vasile/mimetype from 1.1.2 to 1.4.0 Bumps [github.com/gabriel-vasile/mimetype](https://github.com/gabriel-vasile/mimetype) from 1.1.2 to 1.4.0. - [Release notes](https://github.com/gabriel-vasile/mimetype/releases) - [Commits](https://github.com/gabriel-vasile/mimetype/compare/v1.1.2...v1.4.0) --- updated-dependencies: - dependency-name: github.com/gabriel-vasile/mimetype dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] commit ec554822b81d3f26095ef6b4eff10ce7cc2f8174 Author: Parth <76231594+pxrth9@users.noreply.github.com> Date: Tue Oct 12 06:48:40 2021 -0400 Add repo archive command (#4410) Co-authored-by: meiji163 commit cedbbe3c2ae569f0f7a011b83930a1930145ee5a Author: Mateusz Urbanek Date: Mon Oct 11 19:32:40 2021 +0100 Add limit flag to codespace list (#4453) Co-authored-by: Mislav Marohnić commit 4d274bd24e3288c9f97bb1934b5c2da440689ac0 Merge: 3956510c adc30723 Author: Mislav Marohnić Date: Mon Oct 11 18:30:57 2021 +0200 Merge pull request #4485 from cli/dependabot/go_modules/github.com/creack/pty-1.1.16 Bump github.com/creack/pty from 1.1.13 to 1.1.16 commit 3956510cbca7c0bb012fa0b959cf219c6a1a4c59 Merge: af522e4e c20a5c28 Author: Mislav Marohnić Date: Mon Oct 11 18:25:59 2021 +0200 Merge pull request #4484 from cli/dependabot/go_modules/github.com/hashicorp/go-version-1.3.0 Bump github.com/hashicorp/go-version from 1.2.1 to 1.3.0 commit af522e4e69ed4fc8f7534eb801c2967752afd97c Merge: a364cd34 32c17b2d Author: Mislav Marohnić Date: Mon Oct 11 18:25:23 2021 +0200 Merge pull request #4482 from cli/dependabot/go_modules/github.com/itchyny/gojq-0.12.5 Bump github.com/itchyny/gojq from 0.12.4 to 0.12.5 commit adc3072303450db0643c7eec9ab00f941aca1cb8 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Oct 11 16:24:32 2021 +0000 Bump github.com/creack/pty from 1.1.13 to 1.1.16 Bumps [github.com/creack/pty](https://github.com/creack/pty) from 1.1.13 to 1.1.16. - [Release notes](https://github.com/creack/pty/releases) - [Commits](https://github.com/creack/pty/compare/v1.1.13...v1.1.16) --- updated-dependencies: - dependency-name: github.com/creack/pty dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] commit a364cd34875c2d5c0bc5412ace9a50b4b6923df8 Merge: 152c2888 8cc77bd6 Author: Mislav Marohnić Date: Mon Oct 11 18:12:32 2021 +0200 Merge pull request #4483 from cli/dependabot/go_modules/github.com/cpuguy83/go-md2man/v2-2.0.1 Bump github.com/cpuguy83/go-md2man/v2 from 2.0.0 to 2.0.1 commit 8cc77bd61ee6918217377b286f70afcc9ad5bbe9 Author: Mislav Marohnić Date: Mon Oct 11 18:05:52 2021 +0200 Fix test expectation for man contents https://github.com/cpuguy83/go-md2man/pull/74 commit 32c17b2d9538c7005acfe8c99c5a40775e3f888d Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Oct 11 15:29:21 2021 +0000 Bump github.com/itchyny/gojq from 0.12.4 to 0.12.5 Bumps [github.com/itchyny/gojq](https://github.com/itchyny/gojq) from 0.12.4 to 0.12.5. - [Release notes](https://github.com/itchyny/gojq/releases) - [Changelog](https://github.com/itchyny/gojq/blob/main/CHANGELOG.md) - [Commits](https://github.com/itchyny/gojq/compare/v0.12.4...v0.12.5) --- updated-dependencies: - dependency-name: github.com/itchyny/gojq dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] commit 152c2888d7e41ce6050eb8e4b7a9708694500e80 Merge: 9406ba3a a131644d Author: Mislav Marohnić Date: Mon Oct 11 17:23:35 2021 +0200 Merge pull request #4481 from cli/dependabot/go_modules/github.com/mattn/go-colorable-0.1.11 Bump github.com/mattn/go-colorable from 0.1.8 to 0.1.11 commit c20a5c28327965600e78356a7c86241a016f0c22 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Oct 11 14:25:33 2021 +0000 Bump github.com/hashicorp/go-version from 1.2.1 to 1.3.0 Bumps [github.com/hashicorp/go-version](https://github.com/hashicorp/go-version) from 1.2.1 to 1.3.0. - [Release notes](https://github.com/hashicorp/go-version/releases) - [Changelog](https://github.com/hashicorp/go-version/blob/master/CHANGELOG.md) - [Commits](https://github.com/hashicorp/go-version/compare/v1.2.1...v1.3.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/go-version dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] commit 668d7dc8ffe086e5f7bf3ed6c043ebc328fbba66 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Oct 11 14:25:26 2021 +0000 Bump github.com/cpuguy83/go-md2man/v2 from 2.0.0 to 2.0.1 Bumps [github.com/cpuguy83/go-md2man/v2](https://github.com/cpuguy83/go-md2man) from 2.0.0 to 2.0.1. - [Release notes](https://github.com/cpuguy83/go-md2man/releases) - [Commits](https://github.com/cpuguy83/go-md2man/compare/v2.0.0...v2.0.1) --- updated-dependencies: - dependency-name: github.com/cpuguy83/go-md2man/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] commit a131644d858c2204ac740e6f617d65d82f43fe1c Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Oct 11 14:25:14 2021 +0000 Bump github.com/mattn/go-colorable from 0.1.8 to 0.1.11 Bumps [github.com/mattn/go-colorable](https://github.com/mattn/go-colorable) from 0.1.8 to 0.1.11. - [Release notes](https://github.com/mattn/go-colorable/releases) - [Commits](https://github.com/mattn/go-colorable/compare/v0.1.8...v0.1.11) --- updated-dependencies: - dependency-name: github.com/mattn/go-colorable dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] commit 9406ba3abcb5bb983222b1c36c14ade0bc92bd19 Merge: 5dcd1dd5 30187708 Author: Mislav Marohnić Date: Mon Oct 11 11:49:51 2021 +0200 Merge pull request #4478 from cli/dependabot/go_modules/github.com/mattn/go-isatty-0.0.14 Bump github.com/mattn/go-isatty from 0.0.13 to 0.0.14 commit 5dcd1dd5b1b74bc0461ad5a5515c991a1687997b Merge: 74ddc24d b3947a7a Author: Mislav Marohnić Date: Mon Oct 11 11:49:18 2021 +0200 Merge pull request #4477 from cli/dependabot/go_modules/github.com/briandowns/spinner-1.16.0 Bump github.com/briandowns/spinner from 1.11.1 to 1.16.0 commit 3018770846358d6dc552109fb56975d9a8c1e253 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Oct 11 09:43:40 2021 +0000 Bump github.com/mattn/go-isatty from 0.0.13 to 0.0.14 Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.13 to 0.0.14. - [Release notes](https://github.com/mattn/go-isatty/releases) - [Commits](https://github.com/mattn/go-isatty/compare/v0.0.13...v0.0.14) --- updated-dependencies: - dependency-name: github.com/mattn/go-isatty dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] commit 74ddc24dac67ac85309a5c73cd874a332f0f5199 Merge: fd16453e e472f460 Author: Mislav Marohnić Date: Mon Oct 11 11:38:02 2021 +0200 Merge pull request #4479 from cli/dependabot/go_modules/github.com/muesli/termenv-0.9.0 Bump github.com/muesli/termenv from 0.8.1 to 0.9.0 commit 2819deb15b6770fc2dd6f24fc6fa8ef89956ce86 Author: Mislav Marohnić Date: Mon Oct 11 11:33:44 2021 +0200 Avoid applying human-oriented PR automation to PRs from bots commit dabaa5ad7ddde85b455ea21c94bc11388dfaed83 Author: Mislav Marohnić Date: Mon Oct 11 11:17:48 2021 +0200 CodeQL-Dependabot compatibility Configure the CodeQL workflow to avoid running for pushes on all pull requests because that causes problems with Dependabot PRs. https://github.com/cli/cli/pull/4475/checks?check_run_id=3857074760 commit fd16453eb6fbae2305376308a5b66da314e380d8 Merge: 31cca114 98fa94cc Author: Mislav Marohnić Date: Mon Oct 11 11:21:36 2021 +0200 Merge pull request #4475 from cli/dependabot/go_modules/github.com/google/go-cmp-0.5.6 Bump github.com/google/go-cmp from 0.5.5 to 0.5.6 commit e472f46083fecdca5a40f1c7578cd0ab31e4b0b2 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Oct 11 08:54:27 2021 +0000 Bump github.com/muesli/termenv from 0.8.1 to 0.9.0 Bumps [github.com/muesli/termenv](https://github.com/muesli/termenv) from 0.8.1 to 0.9.0. - [Release notes](https://github.com/muesli/termenv/releases) - [Commits](https://github.com/muesli/termenv/compare/v0.8.1...v0.9.0) --- updated-dependencies: - dependency-name: github.com/muesli/termenv dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] commit b3947a7a94fe4c25c466ef419a64b332b49e4f97 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Oct 11 08:54:15 2021 +0000 Bump github.com/briandowns/spinner from 1.11.1 to 1.16.0 Bumps [github.com/briandowns/spinner](https://github.com/briandowns/spinner) from 1.11.1 to 1.16.0. - [Release notes](https://github.com/briandowns/spinner/releases) - [Commits](https://github.com/briandowns/spinner/compare/v1.11.1...v1.16.0) --- updated-dependencies: - dependency-name: github.com/briandowns/spinner dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] commit 98fa94cc6b15c1793dfb9aa6ee6601ee699d3f1d Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Oct 11 08:53:52 2021 +0000 Bump github.com/google/go-cmp from 0.5.5 to 0.5.6 Bumps [github.com/google/go-cmp](https://github.com/google/go-cmp) from 0.5.5 to 0.5.6. - [Release notes](https://github.com/google/go-cmp/releases) - [Commits](https://github.com/google/go-cmp/compare/v0.5.5...v0.5.6) --- updated-dependencies: - dependency-name: github.com/google/go-cmp dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] commit 31cca1145f3e8e0e14bf39b2b49850faecd2cbd4 Merge: 0c5c2378 706dede7 Author: Mislav Marohnić Date: Mon Oct 11 10:48:22 2021 +0200 Merge pull request #4473 from neil465/neil/dependabot Enable dependabot to get security updates and if needed version updat… commit 706dede7acfee08c2b4f09d9ab64f81bdb197d72 Author: flying-cow <42328488+neil465@users.noreply.github.com> Date: Sun Oct 10 19:41:30 2021 -0500 Enable dependabot to get security updates and if needed version updates on dependencies https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically Having knowledge about vulnerabilities of the dependencies helps the project owners decide on their dependencies security posture to make decisions. If the project decides to get updates only on security updates and not on any version updates then setting these options would not open any PR 's open-pull-requests-limit: 0 commit 0c5c2378ac540f07700dd87b55bbb3313a728ca2 Merge: 79832c1b e4c8aa3b Author: Mislav Marohnić Date: Fri Oct 8 13:58:56 2021 +0200 Merge pull request #4460 from m4ver1k/feat/2720 #2720 | Add patch flag to pull-request diff command commit e4c8aa3b2bb50ee2f41f307eacf5b2026ef6911e Author: Mislav Marohnić Date: Fri Oct 8 13:53:19 2021 +0200 Add tests for `pr diff --patch` commit 79832c1b04ad0d0a01897c1f5992bbefde44d584 Author: Luciano Zago Date: Fri Oct 8 06:14:49 2021 -0300 Add instructions to gh installation via spack (#4412) commit 1aefc7437834522f1bd1fbc3c1f7ff5cbf7fa801 Author: Jose Garcia Date: Thu Oct 7 16:48:09 2021 -0400 Add more time between events commit 1ff58a3de734f93351111775e64a7fafd99cb683 Author: Jose Garcia Date: Thu Oct 7 16:39:43 2021 -0400 Update docs, remove needless condition check commit 97cbdca84a2e11f1d249cf34d2760ebf5c7a3faa Author: Jose Garcia Date: Thu Oct 7 15:45:55 2021 -0400 Fix additional race in tests commit 8a559ee12a0cd4b564b68564a0aef803c69f3d87 Author: Jose Garcia Date: Thu Oct 7 15:38:16 2021 -0400 Fix unrelated tests commit 2406f3f09a0d618a5d442e5b34b09dbd25adbaad Author: Jose Garcia Date: Thu Oct 7 15:32:07 2021 -0400 Fix races and remove unreachable code commit 7ba2fb4c0ed1a376dbb2cf76d2bfd6f6c810cc7a Author: Jose Garcia Date: Thu Oct 7 15:19:14 2021 -0400 Make fileLogger more versatile commit 8f5d6bb672e889ed8723a7f5bbc22da1c0a9ef12 Author: Jose Garcia Date: Thu Oct 7 15:14:42 2021 -0400 Tests for most of the new behavior - Made the heartbeat interval configurable for easier testing - Moved span to the top of connect to capture the full execution commit e0897fd8e8ba9ec370b1710fe21719aa457516a5 Author: Adarsh K Kumar Date: Thu Oct 7 23:09:21 2021 +0530 #2720 | Add patch flag to pull-request diff command commit 9c8351ecd82caa1e83af78255a71e24d60b2ddf9 Merge: 55f4fcf0 cff6cf9a Author: Jose Garcia Date: Thu Oct 7 11:10:45 2021 -0400 Merge branch 'trunk' of github.com:cli/cli into jg/liveshare-keepalive commit cff6cf9acf68ba4560da657cb40f34df7a07748e Merge: becc45a1 400749d5 Author: Jose Garcia Date: Thu Oct 7 11:09:26 2021 -0400 Merge pull request #4457 from cli/jg/fix-create-regression codespace create: fix regression returning nil codespace commit 400749d560caf6ed7f8cc9e9a11923ea32bbd8fa Author: Jose Garcia Date: Thu Oct 7 11:01:24 2021 -0400 Fix regression returning nil codespace commit 55f4fcf05c51896a98e6223615a4faaf4210e0b6 Author: Jose Garcia Date: Thu Oct 7 10:42:06 2021 -0400 Live Share session activity detection - Session now accepts two new options: ClientName and Logger - Port forwarder now supports a keepAlive parameter which when true, instructs the PF to call the session's keepAlive method. - Port forwarder uses a new traffic monitor to detect I/O traffic and notify the session when applicable. - The SSH command introduces a new debug flag which enables the command to log to a new temporary file. The file path is printed to the user. commit becc45a1df4bfd16adf5f46e45e3d46551a4bd48 Merge: cd1eec5e 8f5806d6 Author: Sam Date: Wed Oct 6 12:54:25 2021 -0700 Merge pull request #4437 from cli/fix-extension-io-bug Set io when initializing extension manager commit cd1eec5e38e4a47ee507233d18a44e3ac81ddf1c Merge: 6f978dd8 073200a2 Author: Jose Garcia Date: Wed Oct 6 13:55:56 2021 -0400 Merge pull request #4448 from cli/jg/codeowners Add codeowners commit 6f978dd8a4e6f11e33fa5530297ad29e22b11445 Merge: 3d0d95ce b44474c3 Author: Jose Garcia Date: Wed Oct 6 13:55:45 2021 -0400 Merge pull request #4440 from cli/jg/choose-codespace-prompt codespace: choose codespace prompt improvements commit b44474c32b1e2e155aacce3e812d56ebd0ea8159 Author: Jose Garcia Date: Wed Oct 6 13:47:20 2021 -0400 Revert rename for ports cmd commit bdc9ad30e765c05913245247a44fcd3f8c573a13 Author: Jose Garcia Date: Wed Oct 6 13:46:04 2021 -0400 Revert other rename changes commit 811d841fae35c7555854560014419d2ab9c6fa46 Merge: 771ac714 3d0d95ce Author: Jose Garcia Date: Wed Oct 6 13:40:01 2021 -0400 Merge branch 'trunk' of github.com:cli/cli into jg/choose-codespace-prompt commit 3d0d95ce2b95917089035326db885ffd995ab128 Merge: 52e16dfe 20ae9d30 Author: Gabriel Ramírez Date: Wed Oct 6 12:38:50 2021 -0500 Merge pull request #4439 from geramirez/geramirez/codespaces-pagination Adding pagination to list codespaces commit 771ac714ac955451c3c917ea6bbf43f8d126ec3e Author: Jose Garcia Date: Wed Oct 6 11:47:18 2021 -0400 Update docs commit a509c2d88487c05d5ac84f781536e8dbd08e8543 Author: Jose Garcia Date: Wed Oct 6 11:45:43 2021 -0400 Remove unused guid commit 3a2864363035720e9c1da0371abab2f4c4847176 Author: Jose Garcia Date: Wed Oct 6 11:44:26 2021 -0400 Keep codespace struct in API for now - Use a private codespace structure in the cmd pkg to encapsulate common behavior commit 20ae9d305d13746c5691611686a21168266dafba Author: Gabriel Ramírez Date: Wed Oct 6 14:53:55 2021 +0000 Fetch 100 codespaces by default commit 073200a275b97ed1cd0f9da6927c74dfe341efce Author: Jose Garcia Date: Wed Oct 6 10:19:17 2021 -0400 Add codeowners commit e29a0ac7c464b5493766ee5843fa73f3cb87e9d3 Merge: 34f9c0a6 237fdd40 Author: Gabriel Ramírez Date: Wed Oct 6 14:18:57 2021 +0000 Merge branch 'geramirez/codespaces-pagination' of https://github.com/geramirez/cli into geramirez/codespaces-pagination commit 34f9c0a67c201f98ddde69f26794d1b2bf54539c Author: Gabriel Ramírez Date: Wed Oct 6 14:18:44 2021 +0000 Updating docs and interation exit condition to not check the final page commit 237fdd40c8773f87039b80b651fea840eb0e54c0 Author: Gabriel Ramírez Date: Wed Oct 6 09:03:49 2021 -0500 Update internal/codespaces/api/api.go Co-authored-by: Jose Garcia commit 017632d63d398dfcb5d150c3e51760f93be8f07c Merge: 7fe8357d 52e16dfe Author: Jose Garcia Date: Wed Oct 6 09:57:39 2021 -0400 Merge branch 'trunk' of github.com:cli/cli into jg/choose-codespace-prompt commit 52e16dfe094fff85b684930fe377b3b363e63278 Merge: 616d6c2d b2ff4c32 Author: Jose Garcia Date: Wed Oct 6 09:41:01 2021 -0400 Merge pull request #4446 from cli/jg/get-codespace-api codespace: switch `API.GetCodespace` to new API endpoint commit b2ff4c321a942539ad5dd4931c1e64738c05db34 Author: Jose Garcia Date: Wed Oct 6 09:25:39 2021 -0400 Remove unused structs commit 7c497f283b06d9e2e1bff145d6a0b7edf4b3616a Author: Jose Garcia Date: Wed Oct 6 09:15:07 2021 -0400 Update test signature commit 8bb55359b1f9b7a9a97f0d8d162d3d9b3717d7a1 Author: Jose Garcia Date: Wed Oct 6 09:10:00 2021 -0400 Update mock API commit 8135fdbd993270690702e06a233440038c087d22 Author: Jose Garcia Date: Wed Oct 6 08:50:42 2021 -0400 Switch GetCodespace to new API endpoint - Drop GetCodespaceToken as it is no longer necessary - Introduces new behavior with the API to optionally include connection details in the GET request. Ommitting to do so results in a faster response commit aa49a3a5571e78eddb49a90a833ffb74bc76da5b Author: Gabriel Ramírez Date: Tue Oct 5 21:43:28 2021 +0000 Adding a second condition just in case commit 61b0fe36b2f354967ee6a009e9b744737ef113ed Author: Gabriel Ramírez Date: Tue Oct 5 21:31:47 2021 +0000 Adding additional tests for mid-flight deletions and additions commit 02145cc4fd27cd619da298208846982e85203f7a Author: Gabriel Ramírez Date: Tue Oct 5 21:15:24 2021 +0000 Updated PR with suggestions - created a new method to avoid defer pileup issue - removed extra api call at the end of the sequence commit c6dc50983dc9d9c1d441797d74443397506c0c66 Author: Gabriel Ramírez Date: Tue Oct 5 15:33:22 2021 -0500 Update internal/codespaces/api/api.go Co-authored-by: Jose Garcia commit 4314f734e30ff4ceb0bd67834527c1cf8d53044a Author: Gabriel Ramírez Date: Tue Oct 5 15:33:16 2021 -0500 Update internal/codespaces/api/api.go Co-authored-by: Jose Garcia commit e793a5961d4b400ec122391e21f77e663ac47225 Author: Gabriel Ramírez Date: Tue Oct 5 19:00:21 2021 +0000 Add strconv commit fd5894d28defa0d92a98bf8f66c837bfaeb7709c Merge: 409cd9c3 616d6c2d Author: Gabriel Ramírez Date: Tue Oct 5 18:58:23 2021 +0000 Merge remote-tracking branch 'upstream/trunk' into geramirez/codespaces-pagination commit 409cd9c388976b4025bb929684d8fb034eb9ef52 Author: Gabriel Ramírez Date: Tue Oct 5 18:49:41 2021 +0000 Fixing Has() issue due to go version commit 7fe8357d40883f2d6294197bb53b428320fcb401 Author: Jose Garcia Date: Tue Oct 5 13:30:17 2021 -0400 Better short name commit 1971292175011112e461240f64ff93131e288c24 Author: Jose Garcia Date: Tue Oct 5 13:24:47 2021 -0400 Fixes bug there are +2 codespaces with a conflict - Tracks conflicting name going forward for other records - Moves the git status dirty star into a constant so we can reference it commit a8d1718f21603511ff3b248f11df067b77355591 Merge: 975bd7c0 616d6c2d Author: Jose Garcia Date: Tue Oct 5 12:57:11 2021 -0400 Merge branch 'trunk' of github.com:cli/cli into jg/choose-codespace-prompt commit 777978644c348eaaaeb3b208412baee90cf70b2c Author: Gabriel Ramírez Date: Tue Oct 5 16:49:40 2021 +0000 Adding pagination to list codespaces commit 616d6c2db122eda1241dc9f10cc68e3873a79237 Merge: 3652307c e0db10e4 Author: Jose Garcia Date: Tue Oct 5 12:44:38 2021 -0400 Merge pull request #4431 from cli/jg/get-skus-public codespace machine: switch `API.GetCodespacesMachines` to use new API commit 8f5806d61ffb6508a65a7d4630e125ce6da31892 Author: Sam Coe Date: Tue Oct 5 09:10:49 2021 -0700 Set io when initializing extension manager commit 975bd7c08a459c2f26ffd6dc2998c05793710f2a Author: Jose Garcia Date: Tue Oct 5 11:23:12 2021 -0400 Change choose codespace prompt - Repo + branch is favored - Codespace name is included to disambiguate between two codespaces - Move Codespace model out of out API into its own package - Update call sites and group behavior under codespace.Codespace commit 3652307cf3c6a8a48711e7c28a7b8cf6c9dcb1a3 Merge: a2e6df18 a3efb53c Author: Jose Garcia Date: Tue Oct 5 09:20:33 2021 -0400 Merge pull request #4429 from cli/jg/start-codespace-public codespace create: update `API.StartCodespace` to use new API endpoint commit a2e6df18e72c2bee6e3af3ef729c671c83a81587 Merge: 50d8f1e0 d02876e6 Author: Jose Garcia Date: Tue Oct 5 08:23:36 2021 -0400 Merge pull request #4432 from cli/jg/rename-pkg codespace: rename the cmd pkg to codespace commit 50d8f1e09ab61e64733a13cda446aaf27148fbfd Merge: 93cea6d3 61af29bb Author: Jose Garcia Date: Tue Oct 5 08:20:53 2021 -0400 Merge pull request #4409 from cli/jg/delete-codespace-public codespace delete: update DeleteCodespaces to new API endpoint commit d02876e6ea3cc0ad12437cd5ecd684e9d0d226ca Author: Jose Garcia Date: Mon Oct 4 14:16:04 2021 -0400 Rename the cmd pkg to codespace commit e0db10e4dd72c02e1c3b820661c50a766d7defd8 Author: Jose Garcia Date: Mon Oct 4 13:40:18 2021 -0400 Switch API.GetCodespacesMachines to use new API - The SKU terminology is also dropped in favor of "machine" which matches the nomenclature of the rest of the product. commit a3efb53c443d07dfab92a2dae7d66073d6e47124 Author: Jose Garcia Date: Mon Oct 4 08:32:02 2021 -0400 Update API.StartCodespace to use new API endpoint - Switch to using name instead of GUID - Remove GUID from the code since it is not used anywhere else - Add docs to the api client methods - Re-gen mocked client commit 61af29bb968ad77a654f3db68ae67c21e949a238 Author: Jose Garcia Date: Fri Oct 1 13:02:29 2021 -0400 Update telemetry path commit 93cea6d370992d4dc0bfc9206389d199ce196ac4 Merge: 68e54a59 86a4706e Author: Jose Garcia Date: Fri Oct 1 12:55:44 2021 -0400 Merge pull request #4408 from cli/jg/create-codespace-public codespace create: update startCreate to use new API endpoint commit 6b1876161d696629bdf8e2e0d8c34951313fb8c9 Author: Jose Garcia Date: Fri Oct 1 12:53:35 2021 -0400 Update DeleteCodespaces to new API endpoint - Drop the need for the user argument - Update mocks - Remove no longer applicable TODO comment - Show message for successful deletion (this regressed) commit 86a4706ed224c0368de114ced339c9fef7fc41f6 Author: Jose Garcia Date: Fri Oct 1 11:29:16 2021 -0400 Update startCreate to use new API endpoint - Updates the signature of startCreate - Can't update API.CreateCodespace just yet until we support expanded access on the GET codespace endpoint which is used for polling commit 68e54a5904297cfb433306bc06871fdb4aab2bde Merge: cb6db95e 05297b8c Author: Jose Garcia Date: Fri Oct 1 11:28:37 2021 -0400 Merge pull request #4407 from cli/jg/list-codespaces-public codespace list: update ListCodespaces to use new API endpoint commit cb6db95ec6fd7fa72bd76df2f9e56bd0bf6a2ff8 Merge: 77e8b16b a7238130 Author: Mislav Marohnić Date: Fri Oct 1 17:26:04 2021 +0200 Merge pull request #4403 from cli/cs-concurrent-requests Don't allow the lazyLoadedHTTPClient mutex to wrap `Do()` commit 05297b8c8d899ce9491be9e14d33f8472d22f011 Author: Jose Garcia Date: Fri Oct 1 10:37:15 2021 -0400 Update ListCodespaces to use new API endpoint - Removes the need for a User to list codespaces which should result in a slight speed improvement. commit 77e8b16b27330d51bf3a836f0ac581bf6beecae3 Merge: be22cabe 6249bee1 Author: Mislav Marohnić Date: Fri Oct 1 14:20:37 2021 +0200 Merge pull request #4405 from cli/bump-survey Bump Survey to fix "Unexpected escape sequence" commit 6249bee148f1c1a900dc9c00d1849a8dd622de3a Author: Mislav Marohnić Date: Fri Oct 1 14:11:15 2021 +0200 Bump Survey to fix "Unexpected escape sequence" https://github.com/AlecAivazis/survey/pull/367 commit a7238130643528bfb29499db8853cae73640788c Author: Mislav Marohnić Date: Fri Oct 1 11:18:15 2021 +0200 Don't allow the lazyLoadedHTTPClient mutex to wrap `Do()` We only need a mutex around accessing `l.httpClient`, but never around the actual HTTP request. commit be22cabe0b789ee11d4964817a518a5a53982b9c Merge: ab27d1b5 0859de01 Author: Jose Garcia Date: Thu Sep 30 12:49:50 2021 -0400 Merge pull request #4395 from cli/jg/move-ghcs-cmd pkg/cmd/codespace: move `cmd/ghcs` to `pkg/cmd/codespace` commit 0859de0124c7914d854565505058bb50a069f76c Merge: 9e6c11e7 ab27d1b5 Author: Jose Garcia Date: Thu Sep 30 12:38:41 2021 -0400 Merge branch 'trunk' of github.com:cli/cli into jg/move-ghcs-cmd commit ab27d1b5e12ddf6e9a6a137b780616a9290eff41 Merge: 4e9c443a 94953ed3 Author: Jose Garcia Date: Thu Sep 30 12:02:14 2021 -0400 Merge pull request #4394 from cli/jg/move-codespace-api codespace api: move `internal/api` to `internal/codespaces/api` commit 94953ed3fcd9cc83dadef201216420a1806dcdea Merge: 23d41ddb 4e9c443a Author: Jose Garcia Date: Thu Sep 30 11:52:22 2021 -0400 Merge branch 'trunk' of github.com:cli/cli into jg/move-codespace-api commit 4e9c443adf64452a94073a903e17b8cc906622a9 Merge: 877ad22d 53e4149c Author: Jose Garcia Date: Thu Sep 30 11:50:07 2021 -0400 Merge pull request #4393 from cli/jg/move-liveshare liveshare: move location from `internal/liveshare` to `pkg/liveshare` commit 9e6c11e767cb0a0d2ca8fcfe4508e0e819891ba5 Author: Jose Garcia Date: Thu Sep 30 11:20:13 2021 -0400 Move `cmd/ghcs` to `pkg/cmd/codespace` - Delete pkg/cmd/codespace/main as it is no longer needed in this codebase commit 23d41ddb0d0e204a6816e51d4a296ae68fd360bd Author: Jose Garcia Date: Thu Sep 30 11:09:59 2021 -0400 Format files commit b5bbb442fd61dae0581ae1758958a0d943181bcf Author: Jose Garcia Date: Thu Sep 30 11:06:43 2021 -0400 Move `internal/api` to `internal/codespaces/api` commit 53e4149c91711fdfac59ddb40566816936a48a9a Author: Jose Garcia Date: Thu Sep 30 10:42:03 2021 -0400 Update tests commit f4a5f82312377b267762f923af625933c7d00a51 Author: Jose Garcia Date: Thu Sep 30 10:34:27 2021 -0400 Move internal/liveshare to pkg/liveshare commit 877ad22da6b4dfddff584125c0349218ff3e16e3 Merge: c4ec0a65 a1e72af1 Author: Jose Garcia Date: Thu Sep 30 09:44:29 2021 -0400 Merge pull request #4384 from cli/import-codespaces Import codespaces commit a1e72af1dad5df202fba73cb3e0890620310ba68 Merge: c3ce95ea 2ce14f60 Author: Jose Garcia Date: Thu Sep 30 09:26:28 2021 -0400 Merge pull request #4392 from cli/jg/validate-host-key codespace: validate host public keys commit 2ce14f603f0a944fbbc857193da7e916a22dd209 Author: Jose Garcia Date: Thu Sep 30 08:40:18 2021 -0400 Update tests with mock public key and test case commit 5b5c3da39382927a1568f6a0a563f1454623b42e Author: Jose Garcia Date: Thu Sep 30 08:16:28 2021 -0400 Validate host public keys commit c4ec0a65bace0d47067ef671fd2980b3c565aeef Merge: af812e2b 9e3893e1 Author: Mislav Marohnić Date: Thu Sep 30 12:01:47 2021 +0200 Merge pull request #4363 from RasmusWL/patch-1 Also set `pushRemote` on `gh pr checkout` from fork commit af812e2bdcb59b781c2384013252463145ebbe74 Merge: 425bc648 7efd06b8 Author: Nate Smith Date: Wed Sep 29 16:03:05 2021 -0500 Merge pull request #4373 from cli/ext-bin-upgrade binary extensions list & upgrade commit c3ce95ea1c140297122812b3aa87a831cc291ff0 Author: Jose Garcia Date: Wed Sep 29 16:45:51 2021 -0400 Add mutex to guard httpClient commit 0d0152b0fab46ca3c54032673c95f3d670d1f063 Author: Mislav Marohnić Date: Wed Sep 29 21:21:16 2021 +0200 Have `gh codespace` inherit the correct API client commit 7efd06b87dd5210cbb079c377fff08c0ae75c8aa Author: vilmibm Date: Wed Sep 29 13:43:37 2021 -0500 rename function commit a2e7eaf80878e12f23edde21bfb054a44af23e52 Author: vilmibm Date: Wed Sep 29 13:38:57 2021 -0500 test update available for binary ext in list commit 9bc50f5ab8cc8e7d08ba601e78d5dc1e8baeb7df Merge: 76736228 48bac0ab Author: Jose Garcia Date: Wed Sep 29 10:49:30 2021 -0400 Merge pull request #4379 from cli/import-codespaces-add-nacl Switch to using cli/crypto & test fixes commit 48bac0abd2f7fcc5cbed5d48038f7e4307401eb7 Author: Mislav Marohnić Date: Wed Sep 29 13:54:04 2021 +0200 Fix race in codespaces delete test commit a0f11b66643cefc50a6cacee3bb085a92316f706 Author: Jose Garcia Date: Tue Sep 28 16:29:45 2021 -0400 Handle concurrency in tests and logger - Live Share tests - Logger implementation for ghcs commit 0e98b3065106cc5fdecde02609a957066c8f5647 Author: Jose Garcia Date: Tue Sep 28 12:54:06 2021 -0400 Remove internal crypto pkg in favor of fork commit ef087123544b57251dcff77b5b7a760556af9433 Author: vilmibm Date: Tue Sep 28 19:27:21 2021 -0500 test list commit 3971df4f93b6b7a9eefe6e27cf22c1e51bed8870 Author: vilmibm Date: Tue Sep 28 19:23:28 2021 -0500 switch to stubBinaryExtension commit 392460b81e4445962c57b988d5e9ffce8ffcf598 Author: vilmibm Date: Tue Sep 28 15:45:49 2021 -0500 WIP switching to stubBinaryExtension commit 541ed3ba6fb06ba6826c8c60d8f0ff81b99cd43d Author: vilmibm Date: Tue Sep 28 15:33:11 2021 -0500 test Upgrade with binary exts commit 22c1778b9f1e443b107c8f342fc7d518d9c24a0e Author: vilmibm Date: Tue Sep 28 15:04:45 2021 -0500 TODOs commit 94778a9cb05013c7cb9813b536a5e57c283879a5 Author: vilmibm Date: Tue Sep 28 15:04:39 2021 -0500 un-stupid the file mode commit 1fe49fa77682090237f5f422d9ec4634f0109c19 Author: vilmibm Date: Mon Sep 27 16:23:31 2021 -0500 fix listing, cleanup commit 54ec5329c5ad00e57151481182e210ac869176bd Author: vilmibm Date: Wed Sep 22 16:37:08 2021 -0500 add ability to upgrade binary extensions commit db5bbf799fde38097889f016bb5447fad2ab2724 Author: vilmibm Date: Wed Sep 22 16:11:12 2021 -0500 use manager io in Upgrade commit 7673622830445ca8497ba4036cc9263095b02ab1 Author: Mislav Marohnić Date: Tue Sep 28 16:58:29 2021 +0200 Mount `gh codespace` command commit f749590e878cb985051d968b39aaf117ea469d18 Author: Mislav Marohnić Date: Tue Sep 28 16:57:56 2021 +0200 Replace old "github/ghcs" import statements commit e64607d07b2c9c4937ab5de156846a814a654957 Merge: 425bc648 f947ef34 Author: Mislav Marohnić Date: Tue Sep 28 16:46:27 2021 +0200 Merge remote-tracking branch 'ghcs/main' into import-codespaces Co-authored-by: Jose Garcia commit f947ef3448e47cd0b1fa38e8f3e8a43d42bbfb52 Author: Mislav Marohnić Date: Tue Sep 28 16:42:35 2021 +0200 Remove lightstep configuration The `github.com/shirou/gopsutil` dependency of lightstep-tracer is giving us trouble during building. Ref. https://github.com/shirou/gopsutil/issues/976 Another build problem raises its head even after we upgrade gopsutil to a version where the above bug is fixed. commit 0483765da5d44f761f345be46e43fb0625460cf5 Merge: 4015af04 c82d4c54 Author: Mislav Marohnić Date: Tue Sep 28 15:46:57 2021 +0200 Merge pull request #190 from github/app-struct Introduce an App struct that executes core business logic commit 4015af0427421065486f31f982b69d5e55b69995 Merge: 8807b3a7 57d9b1a9 Author: Alan Donovan Date: Mon Sep 27 18:04:55 2021 -0400 Merge pull request #192 from github/json-error create: decode JSON error heuristically commit 57d9b1a9e1acae43a14e4e577be5c1f41a8472a0 Author: Alan Donovan Date: Mon Sep 27 14:51:52 2021 -0400 create: decode JSON error heuristically commit 425bc64859d5af805cad15f89892b9af0a1bf2ec Merge: 4d5ec86b 1f2ab7fb Author: Nate Smith Date: Mon Sep 27 09:25:21 2021 -0700 Merge pull request #4090 from bchadwic/pr-checks Revised pending and skipped symbols for pr checks / run commands commit 4d5ec86b17924a619ed6f902736d6ccfa4daaf46 Merge: 750c3e38 f6e9734f Author: Mislav Marohnić Date: Mon Sep 27 14:24:30 2021 +0200 Merge pull request #4291 from rethab/patch-1 docs: add hint how to use use stdin in api command commit f6e9734f171a49e24259459a84a1152014c998ac Author: Reto Hablützel Date: Sat Sep 25 08:38:26 2021 +0200 add more hints to docs commit f5b0d01e86e8ca8d6700ee59d3f69eda6b9c3fdb Author: rethab Date: Wed Sep 8 14:10:57 2021 +0200 docs: add hint how to use use stdin in api command commit 9e3893e10463757edf1dbf0c34fb8a4d3b6ed1e8 Author: Rasmus Wriedt Larsen Date: Fri Sep 24 16:02:25 2021 +0200 Also set `pushRemote` on `gh pr checkout` from fork As explained in https://git-scm.com/docs/git-config#Documentation/git-config.txt-branchltnamegtremote if you have `remote.pushDefault` set in your global gitconfig (like I do), then _that_ setting will take precedence over `branch..remote` :disappointed: However, `branch..pushRemote` will take precedence over your `remote.pushDefault` setting, such that `gh pr checkout 123 && make changes && git push` will just work, even if you have `remote.pushDefault` set :muscle: commit c82d4c54724d9d879350052d7f0c993d92ec13c7 Author: Mislav Marohnić Date: Fri Sep 24 17:36:18 2021 +0200 Avoid passing params struct as pointer commit dc8f6ef183f6c4d7a0f4135376d54724302abb01 Author: Mislav Marohnić Date: Fri Sep 24 17:30:31 2021 +0200 No longer accept a logger in CreateCodespace The API layer shouldn't concern itself with logging progress to stderr. Instead, we will subsequently add progress indicators in the caller around CreateCodespace and other potentially slow commands as needed. commit ca0f89d3bc1bbf2292ec4e0e2b3fbf97e1047fd6 Author: Mislav Marohnić Date: Fri Sep 24 16:03:44 2021 +0200 Introduce an App struct that executes core business logic The Cobra commands are now a light wrapper around the App struct. Co-authored-by: Jose Garcia commit 8807b3a73a701faf8b8905e7f76f1c086b6b5ba3 Merge: 92d0abd6 7a91ba59 Author: Mislav Marohnić Date: Fri Sep 24 16:02:36 2021 +0200 Merge pull request #184 from github/args-constraint Consistently institute constraints for position arguments and improve error message commit 92d0abd6ab079a584c2d00de99a19db6af617a19 Merge: 4eb15134 3d017b28 Author: Mislav Marohnić Date: Fri Sep 24 16:02:05 2021 +0200 Merge pull request #79 from github/raffo/delete-codespaces Add code and command to delete unused codespaces commit 3d017b282484617a73ca185d27dfcefedefe2e46 Author: Mislav Marohnić Date: Fri Sep 24 15:09:41 2021 +0200 Fix stderr output on delete errors commit 750c3e38f32d0bb845922fa4d96d0c9341928d1c Merge: 96aed388 b38ce244 Author: Mislav Marohnić Date: Fri Sep 24 14:50:59 2021 +0200 Merge pull request #4243 from wilso199/3704-credential-helper Fixes #4274 commit b38ce2449752a41c4a653671bb10f6938cabcca3 Author: Mislav Marohnić Date: Fri Sep 24 14:43:48 2021 +0200 Ensure correct path to `gh` after `gh auth refresh` git credential setup commit d853ce5bc980963d2136459d6707e889443997da Author: Mislav Marohnić Date: Fri Sep 24 14:42:41 2021 +0200 Avoid resolving `executable()` until requested at runtime This is to avoid hitting the filesystem and resolving symlinks unnecessarily. The value of executable is just used conditionally by a handful of commands. commit 78b35b7b6eb4b24a3a776b145e8b443e4d80f4d2 Merge: d731cb9c 96aed388 Author: Mislav Marohnić Date: Fri Sep 24 14:35:01 2021 +0200 Merge remote-tracking branch 'origin' into 3704-credential-helper commit 96aed3881945d9cbcbbe185603280ef96b601db4 Merge: 2c43436c 7bf85355 Author: Nate Smith Date: Thu Sep 23 10:51:37 2021 -0700 Merge pull request #4308 from cli/ext-bin install binary extensions commit 4eb15134a465a356ed72ee18c20e56000751fc08 Merge: 3e26a153 5d6ea502 Author: Jose Garcia Date: Thu Sep 23 13:45:11 2021 -0400 Merge pull request #189 from github/jg/inline-go-liveshare Inline go-liveshare v0.20.0 commit 5d6ea5029ed7cf2ab39abf1cb2fc37da5883b842 Author: Jose Garcia Date: Thu Sep 23 13:36:04 2021 -0400 Linter fixes commit 65dcb0f428ff703135c43db9ce1246860905f469 Author: Jose Garcia Date: Thu Sep 23 13:22:20 2021 -0400 Linter fixes commit 08bc181d79f30f344f361494ac490762453edb16 Author: Jose Garcia Date: Thu Sep 23 13:16:20 2021 -0400 Linter fixes commit b8f35f950ca104c88489a9dd0f4586cd2a47fa36 Author: Jose Garcia Date: Thu Sep 23 13:14:35 2021 -0400 Linter fixes commit 75c1dfdf49e4b43c31704639d5d33b8361fb58e5 Author: Mislav Marohnić Date: Thu Sep 23 18:57:22 2021 +0200 Fetch codespace by name directly if name argument given commit c4114cc972ccbccabbd3b7e1152703ebab12a892 Author: Jose Garcia Date: Thu Sep 23 11:58:55 2021 -0400 Linter fixes commit fb53ccb06a1b21e06ce2849d98c2c79f14e4ba76 Author: Jose Garcia Date: Thu Sep 23 11:56:41 2021 -0400 Linter fixes commit 958990cef83defd3c70278d3b4597c9165641128 Author: Jose Garcia Date: Thu Sep 23 11:47:52 2021 -0400 More linter fixes commit d0c65e549067426f80caf6dc5a99f99ffa4006cd Author: Jose Garcia Date: Thu Sep 23 11:36:27 2021 -0400 Linter fixes commit f4396e8f1a0b79630e81b233b802d49cd0172dad Author: Jose Garcia Date: Thu Sep 23 11:28:04 2021 -0400 Inline go-liveshare with history commit 9ae7eb586959369fa60819c83a2a22da67bd32af Merge: 3e26a153 6ca35d0e Author: Jose Garcia Date: Thu Sep 23 11:19:46 2021 -0400 Merge branch 'go-liveshare-download' into jg/inline-go-liveshare commit 6ca35d0e730d1adaecc1c7c79c9c4892e2138449 Author: Jose Garcia Date: Thu Sep 23 11:18:49 2021 -0400 Moved files to liveshare dir commit e8212a80a9dcdbecb698f47bd45176ad1703bff1 Author: Mislav Marohnić Date: Thu Sep 23 17:14:25 2021 +0200 Print `delete` failures as they occur commit 1232dba684f38721ffe34fe8d331f51362e44ce4 Merge: 32d3a384 3e26a153 Author: Mislav Marohnić Date: Thu Sep 23 16:43:22 2021 +0200 Merge remote-tracking branch 'origin' into raffo/delete-codespaces commit 3e26a153429cfa34bb96f197fae89b3ae0e86d86 Merge: fb12f410 f1c35ba9 Author: Jose Garcia Date: Thu Sep 23 10:22:09 2021 -0400 Merge pull request #188 from github/jg/update-liveshare Update to go-liveshare v0.20.0 commit f1c35ba9daa06996205082f05a275ce97aa68297 Author: Jose Garcia Date: Thu Sep 23 10:21:01 2021 -0400 Update docs commit fb12f4108f362a1f1f1dc70872df621cd39ad14c Merge: a3c900c3 186b90b1 Author: Jose Garcia Date: Thu Sep 23 10:10:57 2021 -0400 Merge pull request #181 from github/jg/poll-on-async-creation ghcs create: poll for codespaces that are being retried by the server commit a3c900c3b92b0c9d89a48179d2818332b8f403f0 Merge: 090e0c81 4e0ac15f Author: Jose Garcia Date: Thu Sep 23 10:07:56 2021 -0400 Merge pull request #185 from github/jg/buffer-channels Add buffered to channels to avoid goroutine leak commit 9654dc4bd3711ed6ec00a112355313827cfe95bf Author: Jose Garcia Date: Thu Sep 23 10:07:14 2021 -0400 Update to go-liveshare v0.20.0 commit 186b90b12e4d253d091a265714965bc96284c78f Author: Jose Garcia Date: Thu Sep 23 08:29:24 2021 -0400 Rename request type commit 13d7804a359f8062817ec1e1da183da1e08a927a Author: Jose Garcia Date: Thu Sep 23 08:26:23 2021 -0400 Remove API test, inline poller commit 7bf85355a92b80437df7470b53e12a554238d0fc Author: vilmibm Date: Wed Sep 22 15:59:57 2021 -0500 restore cached client commit 5f02ed2656b5c2846505cb4c6a570f508a9bb7ad Author: vilmibm Date: Wed Sep 22 15:59:50 2021 -0500 linter appeasement commit 4e0ac15fe045012a2398690fefefff66c86d43a7 Author: Jose Garcia Date: Wed Sep 22 15:10:47 2021 -0400 Add buffer to channels to avoid goroutine leak commit 9a558bc58c0d6d2c9a50f6123242ba0e9bec1257 Author: Jose Garcia Date: Wed Sep 22 15:03:12 2021 -0400 Early return if polling is not required - Add context to errors in poller commit 7a91ba5942f6535ce840312594ec5fcc630be5d8 Author: Mislav Marohnić Date: Wed Sep 22 19:51:12 2021 +0200 Print usage help when args given to "NoArgs" commands commit a55f7af92c5e35491ed002e26b7105caf6d1fa5f Author: Mislav Marohnić Date: Wed Sep 22 19:36:25 2021 +0200 Correct wrong args constraints commit 208f1721b5a29834c9f6420b765b95dd41ce7020 Author: Jose Garcia Date: Wed Sep 22 13:21:02 2021 -0400 Rename ProvisionCodespaceParams commit 70a2ea2e6aaf36cd8e9206adab640892c7892d0d Author: Jose Garcia Date: Wed Sep 22 13:19:26 2021 -0400 PR Feedback - Rename ProvisionCodespace -> CreateCodespace - Rename createCodespace -> startCreate - Additional docs/comments - Simplify ProvisionCodespaceParams commit d2d21996bc1a12a24f3b757e2fbc2ae933aa8a5e Author: Jose Garcia Date: Wed Sep 22 11:49:41 2021 -0400 Move ProvisionCodespace to API client - Make CreateCodespace private along with its errors commit 32d3a38465ef15e8e7b305dccfef31dbc05c07f1 Author: Mislav Marohnić Date: Wed Sep 22 16:39:50 2021 +0200 Name of the codespace commit cb7b535b917ffddc38f12069b32fbce5a4034eb7 Author: Mislav Marohnić Date: Wed Sep 22 16:11:34 2021 +0200 Add tests for delete commit 8c5330d9e9691289c29bd6130efbca265985023c Author: Jose Garcia Date: Wed Sep 22 10:04:18 2021 -0400 Rename error commit 2a0ea1617b3fccd06d29c4e4b81fd6d4b815fa15 Author: Jose Garcia Date: Wed Sep 22 09:40:45 2021 -0400 Handle specific error for GetCodespaceToken commit 86717f14a1a6c0ba1f9f45f55367863541c69d53 Author: Jose Garcia Date: Wed Sep 22 09:09:09 2021 -0400 Implement codespaces.Provision - Move polling logic into the Provision function - Document the behavior expected of callers when an ErrCreateAsyncRetry is returned commit 770151313fe4ac9d2b7a810d0d33b0e119069617 Merge: 77656280 f8a87135 Author: Alan Donovan Date: Wed Sep 22 08:55:26 2021 -0400 Merge pull request #22 from github/connect Merge NewClient and JoinWorkspace into Connect commit 48e3473a953b9502a840bf9ea40fb818dca05f5b Author: Jose Garcia Date: Tue Sep 21 18:18:30 2021 -0400 PR Feedback - Bring context.Timeout into the poller - Accept duration and interval - Other tidy up commit 514d4d992ce4900f18f286b241d788a01c721d0e Author: vilmibm Date: Tue Sep 21 15:55:31 2021 -0500 refactor dependencies of ext manager commit f8a8713520f031758a2b75dc70c5faaea2927ea5 Author: Alan Donovan Date: Tue Sep 21 15:23:02 2021 -0400 refactor Options API commit d2113e3b59e74c6a6b98f63779cde19750e09d9d Merge: c222c3d6 678da44c Author: Mislav Marohnić Date: Tue Sep 21 21:10:29 2021 +0200 Merge branch 'mislav/delete-codespaces' into raffo/delete-codespaces commit 678da44c28506629da1feb53b34efbe59d38b7f0 Author: Mislav Marohnić Date: Tue Sep 21 21:09:26 2021 +0200 Simplify delete further commit ab86739b6b168c35311b5dd1a4c4b0d18fbbb05a Merge: b894d3e1 090e0c81 Author: Mislav Marohnić Date: Tue Sep 21 20:35:28 2021 +0200 Merge remote-tracking branch 'origin' into mislav/delete-codespaces commit 861811baf03d461e0d89113b07c80ff414c4e146 Author: Jose Garcia Date: Tue Sep 21 14:02:05 2021 -0400 Upgrade pkg name after merge commit d5b03df4069a0691e05ebcdbafe8c5362c8b140f Merge: 323462ca 090e0c81 Author: Jose Garcia Date: Tue Sep 21 14:01:37 2021 -0400 Merge branch 'main' of github.com:github/ghcs into jg/poll-on-async-creation commit b3b675d108d02f32b24ad69b33f1dacdd5e85c1d Author: Alan Donovan Date: Tue Sep 21 12:44:30 2021 -0400 Merge NewClient and JoinWorkspace into Connect commit 323462ca5c3ed803da22b47f68e24d7d697c43bf Author: Jose Garcia Date: Tue Sep 21 12:37:11 2021 -0400 Poll codespace on ErrCreateAsyncRetry error - Introduce tests for the poller - Attempt to fetch codespace for 2 mins commit 090e0c81a1423aac4d46b6310b58aaeaad35423b Merge: 8a53c436 683d847d Author: Mislav Marohnić Date: Tue Sep 21 18:06:06 2021 +0200 Merge pull request #171 from github/cli-migration Split out "main" package from "ghcs" commit 683d847dd2fee3163eae059268918fce153dfb25 Merge: 83607521 8a53c436 Author: Mislav Marohnić Date: Tue Sep 21 17:38:41 2021 +0200 Merge remote-tracking branch 'origin' into cli-migration commit 8a53c4369e529a1217714646fa710ded8d18c008 Merge: 0f88081b e8e914c2 Author: Jose Garcia Date: Tue Sep 21 10:14:22 2021 -0400 Merge pull request #179 from github/jg/close-session liveshare: close sessions commit e8e914c220b9ec828b4965a07a843d67bb4c3c18 Author: Jose Garcia Date: Tue Sep 21 10:05:48 2021 -0400 PR Feedback - Upgrade to go-liveshare v0.19.0 - Remove export helper method - Use local implementation commit 0f88081bfa77ec3c06b6bde1d03686bc2b28516c Merge: f33d4305 d3d1ce72 Author: Alan Donovan Date: Tue Sep 21 10:03:45 2021 -0400 Merge pull request #178 from github/check-authorized-keys ghcs ssh: check user has authorised SSH keys commit d3d1ce726d5853c907775e2bafd8b0dbd163e416 Author: Alan Donovan Date: Tue Sep 21 09:59:19 2021 -0400 do logs too commit 0b68aaab7edf7083679e0d257b4fc2e18aa5e26e Author: Jose Garcia Date: Tue Sep 21 09:59:16 2021 -0400 Return error on 202 responses - Start implementing the retry/poll flow commit 7765628033925a64a1c272c6af6920f45f89821e Merge: 5e9382e8 5f6b3a5e Author: Jose Garcia Date: Tue Sep 21 09:56:27 2021 -0400 Merge pull request #21 from github/jg/err-context Add error context to Session.Close commit 5f6b3a5eeed2c8d0ea3c9073df06dad33686af88 Author: Jose Garcia Date: Tue Sep 21 13:46:30 2021 +0000 Add error context to Session.Close commit f33d430500b0ff58b6cc51665e1ce7251ba9a2a6 Merge: 85f79ed8 9e08b747 Author: Alan Donovan Date: Tue Sep 21 09:26:22 2021 -0400 Merge pull request #177 from github/delete-surplus-args delete: reject positional arguments commit a83b3c08167cddb3f8edb07c25ad1de428194c79 Author: Jose Garcia Date: Tue Sep 21 08:46:32 2021 -0400 Update to go-livesare v0.18.0 - Only set err if closeErr is non-nil commit 2c43436c7ab7dd0bfca11fee1b2d00d4c6c58a11 Merge: 02ed5a96 9f439670 Author: Mislav Marohnić Date: Tue Sep 21 14:40:19 2021 +0200 Merge pull request #4347 from danburzo/trunk Add 'git+https' to list of supported URL protocols commit 5e9382e8b4f99c23b33f0819da94d6ad5725b8f7 Merge: 82d7733f 23f6d449 Author: Jose Garcia Date: Tue Sep 21 07:54:04 2021 -0400 Merge pull request #20 from github/jg/close-session-v2 Close RPC conn only commit 1f3b872859d33a5e5d1a4445c66f10aa6899b88f Author: vilmibm Date: Mon Sep 20 17:17:18 2021 -0500 test for unsupported platform commit e85b0480e90820a52dbbf8748602d67eced01524 Author: vilmibm Date: Mon Sep 20 17:10:18 2021 -0500 track installed tag name commit 0e2861a507cc079bed0f5883bd98f38f9cacee92 Author: vilmibm Date: Mon Sep 20 17:05:19 2021 -0500 WIP refactoring commit f5d269ebad0be6fcf42a0e76a87cc887f3ab37bd Author: vilmibm Date: Mon Sep 20 17:02:34 2021 -0500 WIP refactoring commit af7805af53384839e3b421d356cf7067d88a09ae Author: vilmibm Date: Mon Sep 20 16:46:54 2021 -0500 WIP refactoring commit f4d97dcedd4b4b4f31ffdf117e357a6fbac5ccae Author: vilmibm Date: Mon Sep 20 16:25:26 2021 -0500 WIP refactoring commit 23f6d449e0f6bcf9846dbc57b652f129db7e133d Author: Jose Garcia Date: Mon Sep 20 21:16:54 2021 +0000 Close RPC conn only - Only close SSH if RPC fails. Closing RPC automatically closes the underlying stream which in this case is the SSH connection. - I thought about closing the SSH conn instead of RPC, but there is a bit more cleanup that the RPC library needs to do. commit ae38daf08a451bbccb9964fcd8f5a49190cce823 Author: vilmibm Date: Mon Sep 20 16:05:35 2021 -0500 nit commit b00e8a5681d8934a9408e99c3e8ced693faefe84 Author: vilmibm Date: Mon Sep 20 16:02:20 2021 -0500 more accurately check for binary extension commit 7f682f9c398099f30ab0824db56afc95a9edce9e Author: Jose Garcia Date: Mon Sep 20 16:56:57 2021 -0400 Close Live Share sessions - New helper method codespaces.CloseSession to be used using defer - Upgrade to go-liveshare v0.17.0 commit 82d7733f433a2f8bd24dae557c42ad87a8dab5b2 Merge: 22433a57 40886479 Author: Jose Garcia Date: Mon Sep 20 16:36:16 2021 -0400 Merge pull request #19 from github/jg/session-closer-tidy-up Allow clients to Close a Session, general tidy up commit 40886479ae42cff937e37febadeea4708451d4cb Author: Jose Garcia Date: Mon Sep 20 20:35:12 2021 +0000 Close SSH even if RPC Close fails commit dbb80d8b1ef8bf2289c9d6614dbf95d4ad6fba0d Author: Alan Donovan Date: Mon Sep 20 16:01:43 2021 -0400 check for authorised SSH keys commit 02ed5a9666c29dc25ab03fc422cdf90deaf133fb Merge: bb86145c d14715f1 Author: Sam Date: Mon Sep 20 12:07:52 2021 -0700 Merge pull request #4316 from SiarheiFedartsou/sf-pulls-draft Add `--draft` and `--non-draft` filters to `gh pr list` commit d14715f1e37e7db19408dea8cc3162f8b2c3fb9f Author: Sam Coe Date: Mon Sep 20 11:29:37 2021 -0700 Convert bool to string early for pr list draft flag commit 9e08b7477da09d6ccb717716fa2c5874579a72a1 Author: Alan Donovan Date: Mon Sep 20 13:40:45 2021 -0400 delete: reject position args commit b894d3e1340da8aeaf8b83df77a88cc85b1f169a Author: Mislav Marohnić Date: Mon Sep 20 18:37:00 2021 +0200 Simplify delete implementation commit 9f43967042bbf5e57a63db1e34124ba4d647c995 Author: Dan Burzo Date: Mon Sep 20 19:35:06 2021 +0300 Fixes #4346: allow git+https URL protocol commit c222c3d696ef599229a47b20a023aa2ca2ecfdea Author: Raffaele Di Fazio Date: Mon Sep 20 18:23:00 2021 +0200 drop check on shut down Signed-off-by: Raffaele Di Fazio commit 57d04dc5f020ebefbe080e1fa6873dcded731d7a Author: Jose Garcia Date: Mon Sep 20 13:16:38 2021 +0000 Allow clients to Close a Session, general tidy up - Allow clients to call Close on a Session to clean up resources - Switch to the %w verb for error wrapping - Fix typo on Port struct after verifying the server does not have a typo commit 836075215d3b659968d7acc162cb7dbd281eea37 Merge: 8c0c7a8e 85f79ed8 Author: Mislav Marohnić Date: Mon Sep 20 13:59:32 2021 +0200 Merge remote-tracking branch 'origin' into cli-migration commit 85f79ed8e807242401072eec1ea97f67dda59580 Merge: 67a6f0a8 82c19729 Author: Jose Garcia Date: Mon Sep 20 07:43:16 2021 -0400 Merge pull request #159 from github/jg/ssh-cmd-flags ghcs ssh: ssh flags and command support commit c4f0eda96d18199431b66c83c75ab954e13af685 Author: Raffaele Di Fazio Date: Mon Sep 20 11:54:30 2021 +0200 force was actually needed by a next commit Signed-off-by: Raffaele Di Fazio commit eca3ecb43b1f702a5eea4a55363e26e86c5c0fa6 Merge: 4721e700 67a6f0a8 Author: Raffaele Di Fazio Date: Mon Sep 20 11:53:35 2021 +0200 Merge branch 'main' into raffo/delete-codespaces commit 4721e7004be64656c693901ae87a236ee646cd51 Author: Raffaele Di Fazio Date: Mon Sep 20 11:10:44 2021 +0200 add threshold to delete by repo Signed-off-by: Raffaele Di Fazio commit 11024f71fabc349404b1115884eb26e0ecf5ea2b Author: Raffaele Di Fazio Date: Mon Sep 20 10:27:29 2021 +0200 force is not used in delete by repo Signed-off-by: Raffaele Di Fazio commit 82c19729d3ce9fcc0c604c811ddd609ed6190f5e Author: Jose Garcia Date: Fri Sep 17 15:17:38 2021 -0400 Wrap -- with optional argument brackets commit 47c6a5fce818b6681edec6a39d292f9387c0d008 Author: Jose Garcia Date: Fri Sep 17 15:13:09 2021 -0400 Update usage commit 6a34f53c6c50046cfbe47eb681208fc89f83e3e5 Author: Siarhei Fedartsou Date: Fri Sep 17 22:05:47 2021 +0300 Change pr list --draft UX commit 5890d6ad66ee56899ad497fe503d987adebfe744 Author: Jose Garcia Date: Fri Sep 17 15:04:55 2021 -0400 Switch if block logic, assert err string commit da58313358f62a478794fa1337841dcc00aab065 Author: Jose Garcia Date: Fri Sep 17 14:03:31 2021 -0400 Remove redudant type def commit 9f84015bd010818416442935717ae174c1072098 Author: Jose Garcia Date: Fri Sep 17 14:00:16 2021 -0400 Avoid append commit 65e1c6f789fb52415b74b6a3b5d33787732b2ab4 Author: Jose Garcia Date: Fri Sep 17 13:56:38 2021 -0400 More test cases commit 76037ee75367125bdea702aa92885947cff3973c Author: Jose Garcia Date: Fri Sep 17 13:54:00 2021 -0400 Update docs, simplify loop to append to command commit 54265afda00db981d030cffa6c9564161e096ca9 Author: Jose Garcia Date: Fri Sep 17 13:43:23 2021 -0400 PR Feedback - use named returns - handle command flags + test case - simplify tests commit 60d066f0a69ad86e6a09053284bc4beef924dfa0 Author: Jose Garcia Date: Fri Sep 17 11:51:37 2021 -0400 PR Feedback - return nil for slices - handle `-L -l` case - document `parseSSHArgs` commit 67a6f0a85dc64d5557cfff9a741e5010850fdc05 Merge: 1271e20b 747d7e71 Author: Alan Donovan Date: Fri Sep 17 11:48:02 2021 -0400 Merge pull request #168 from github/restore-delete-r-confirm Restore confirmation to delete -r, lost in botched merge commit 1271e20b9b71e8cf07c9dabb262bcbe65e61f55b Merge: 930f1945 d23eca8c Author: Alan Donovan Date: Fri Sep 17 11:28:53 2021 -0400 Merge pull request #169 from github/delete-no-list delete -r: don't perform "list" operation commit 930f194507cbd17414948974ccd7463b01fb2f9d Merge: 6ac8a0ad ce4bbe5b Author: Alan Donovan Date: Fri Sep 17 11:28:26 2021 -0400 Merge pull request #170 from github/list-show-branch-not-name list: show branch (not name) in branch column commit 8c0c7a8e19c5f971f434cb2b69c9e8a4dcbbccdf Author: Mislav Marohnić Date: Fri Sep 17 16:29:35 2021 +0200 Make GITHUB_TOKEN configurable through Go member Co-authored-by: Jose Garcia commit c2f3537a322e25b8ffdfcd6ab31b9081f8695995 Author: Mislav Marohnić Date: Fri Sep 17 16:26:20 2021 +0200 Separate "main" package from "ghcs" package To make "ghcs" importable, this separates out the `main()` function into its own package that lives under "cmd/ghcs/main". Typically the main package would be called "cmd/ghcs", but we wanted to leave the current ghcs implementation where it is to avoid causing conflicts with current work in progress. Co-authored-by: Jose Garcia commit ce4bbe5bd862917f5cc51478c69b2e1d0bd02639 Author: Alan Donovan Date: Fri Sep 17 10:13:35 2021 -0400 list: show branch (not name) in branch column commit 23c9026f58ae04400d1f71e3f8ddafd6fc6c4759 Merge: c6b5fb5b 6ac8a0ad Author: Raffaele Di Fazio Date: Fri Sep 17 16:13:12 2021 +0200 Merge branch 'main' into raffo/delete-codespaces commit d23eca8c5fa8c9b40c679dc05884c2b63eefba72 Author: Alan Donovan Date: Fri Sep 17 09:51:11 2021 -0400 remove "list" operation from "delete -r" command commit 747d7e717354bcb0671e2e36ae2d31c35b42f437 Author: Alan Donovan Date: Fri Sep 17 09:45:49 2021 -0400 Restore confirmation to delete -r, lost in botched merge commit 6ac8a0ad882d3f610ff2519d160d51acd8c1821a Merge: a4f1fa07 610ab89c Author: Alan Donovan Date: Fri Sep 17 09:34:10 2021 -0400 Merge pull request #165 from github/delete-parallel Delete in parallel commit 610ab89c3d204846c12371093280d4b5ba676598 Merge: 4de45728 a4f1fa07 Author: Alan Donovan Date: Fri Sep 17 09:32:56 2021 -0400 Merge branch 'main' into delete-parallel commit 4de457281375b5306b0b5644c7446033a27d7e0c Author: Alan Donovan Date: Fri Sep 17 09:31:05 2021 -0400 add comment commit fb197c8e757a4186216fd3453c8884d4d83a0c2d Merge: bc74c4aa cc1b8646 Author: Alan Donovan Date: Fri Sep 17 09:30:48 2021 -0400 Merge branch 'main' into delete-parallel commit c6b5fb5ba336cc3160c637bab413342c15ffcfc5 Author: Raffaele Di Fazio Date: Fri Sep 17 14:55:50 2021 +0200 add the tests Signed-off-by: Raffaele Di Fazio commit 054fec0ba117508cb761e527bcf7a30d449b9a89 Author: Raffaele Di Fazio Date: Fri Sep 17 14:45:08 2021 +0200 address code comments Signed-off-by: Raffaele Di Fazio commit bb86145cb6614537b04642b689bcd0ecbb6ff449 Merge: 0f7c4d94 f9e49f4a Author: Mislav Marohnić Date: Fri Sep 17 14:37:24 2021 +0200 Merge pull request #4331 from cli/repo-create-prompt-error Do not swallow prompt error during `repo create` commit a4f1fa076b7c1d98d47af20bd698c3036ff50c1d Author: Max Beizer Date: Fri Sep 17 06:10:37 2021 -0500 Fix up all the static-check warnings (#162) commit 29c2a17866cc03f3c701b7503aa26b90d0950c97 Author: Raffaele Di Fazio Date: Fri Sep 17 08:55:54 2021 +0200 Update cmd/ghcs/delete.go Co-authored-by: Jose Garcia commit bc74c4aafab3c0f2bd25cccb7b2a796b4357a607 Author: Alan Donovan Date: Thu Sep 16 18:24:43 2021 -0400 make delete --repo parallel commit 42e47a98d7b51d0ea70ee1bcc5a09392a49080fc Author: Jose Garcia Date: Thu Sep 16 15:22:47 2021 -0400 add docs, simplify map, error on invalid args commit 0f7c4d94af2522257d3a7cd743bff91205a75151 Merge: e13398f6 03057885 Author: Sam Date: Thu Sep 16 10:15:26 2021 -0700 Merge pull request #4328 from cli/fix-sync-fetch Allow user input for git fetch in repo sync commit 455dabb484f818cb88e1e204b98ff86e2cf5cb8f Author: Raffaele Di Fazio Date: Thu Sep 16 18:49:44 2021 +0200 use named params Signed-off-by: Raffaele Di Fazio commit 22e9da790c92e6e65b5ec531a466856bda0fc2a3 Author: Raffaele Di Fazio Date: Thu Sep 16 18:43:16 2021 +0200 Update internal/api/api_test.go Co-authored-by: CamiloGarciaLaRotta commit 35e0f95243e1048033748de94afb932d6fa401e8 Author: Raffaele Di Fazio Date: Thu Sep 16 18:42:41 2021 +0200 Update cmd/ghcs/delete.go Co-authored-by: CamiloGarciaLaRotta commit 0305788536307b65250f3bc98581eab7d06b6a8e Author: Sam Coe Date: Thu Sep 16 09:02:10 2021 -0700 Typo commit 5cd90fea889c9a3a496fa4b16030933a5b58bcfe Author: Raffaele Di Fazio Date: Thu Sep 16 16:45:07 2021 +0200 fix linter Signed-off-by: Raffaele Di Fazio commit 68f4cad1af18a1a10f8bea2ef8a3e78a11df6567 Author: Raffaele Di Fazio Date: Thu Sep 16 16:42:53 2021 +0200 implement delete all with thresold Signed-off-by: Raffaele Di Fazio commit 8a0f8b6d1c1834186ca4fcb6da650d34df89b1eb Author: Jose Garcia Date: Thu Sep 16 10:32:27 2021 -0400 parse ssh args and command commit cc1b86461e457a5ab0eac363f360791389f1fe83 Author: Christian Gregg Date: Thu Sep 16 13:47:15 2021 +0100 Confirm deletion of codespaces with unpushed/uncommited changes (#129) Adds a confirmation dialog on `ghcs delete` if the codespace in question has unpushed or uncommited changes. This confirmation can be skipped using the `--force` or `-f` flag. Closes: #84 Closes: #10 commit f9e49f4aecf383d235b0c81e78a311dd3cb3d88a Author: Mislav Marohnić Date: Thu Sep 16 12:25:44 2021 +0200 Do not swallow prompt error during `repo create` commit 5617db6614a4715a05ec06b4e8a7d028c3137781 Merge: 10ad8548 dbb4e0c1 Author: Mislav Marohnić Date: Thu Sep 16 12:21:36 2021 +0200 Merge pull request #151 from github/ruleguard Wrap errors using "%w" instead of "%v" commit dbb4e0c177d0e259828aca7ede196d748ebb6b44 Merge: fb5a3556 10ad8548 Author: Mislav Marohnić Date: Thu Sep 16 11:46:50 2021 +0200 Merge remote-tracking branch 'origin' into ruleguard commit eeca99864087b41fbff6eceed7b6878545722b37 Author: vilmibm Date: Wed Sep 8 17:27:06 2021 -0500 binary extension support in gh extension install commit 514448dde8f2852c7b5c2bd4131cd93409b0fc93 Merge: c5bd8c41 10ad8548 Author: Jose Garcia Date: Wed Sep 15 15:45:53 2021 -0400 Merge branch 'main' of github.com:github/ghcs into jg/ssh-cmd-flags commit 10ad85486f96f5fa1175653675c68a73b05b5e6c Merge: 45a9715e 0f72e3d8 Author: Jose Garcia Date: Wed Sep 15 15:45:35 2021 -0400 Merge pull request #154 from github/jg/fix-ctx-error ghcs create/ssh: fix ctx cancellation errors & fix todo for X11 forwarding commit 45a9715e9f9beb3dc9651e0a0935604274afe55a Merge: eafadd37 b2234969 Author: Jose Garcia Date: Wed Sep 15 15:40:58 2021 -0400 Merge pull request #158 from github/jg/go-liveshare-0.16.0 upgrade to go-liveshare 0.16.0 commit b2234969e4f728e049a9dc0f5eeff856baf91af7 Author: Jose Garcia Date: Wed Sep 15 15:40:07 2021 -0400 update logs commit eafadd3757dcc3cffca4ada2f44f4170b5344c2f Merge: 3abde5f0 06719866 Author: Alan Donovan Date: Wed Sep 15 15:38:11 2021 -0400 Merge pull request #156 from github/api-internal move api to internal/api commit c5bd8c41279a91711be7ce5a33cfcba4c98208bd Author: Jose Garcia Date: Wed Sep 15 15:37:37 2021 -0400 initial spike to accept args commit 26d3199082dae4dcf8bf75d234e94ff781b872c4 Author: Jose Garcia Date: Wed Sep 15 15:18:54 2021 -0400 add back codespaces.Shell commit ecd0c7056798bcd1b0d1fb2d65d3da7b1477a7be Author: Jose Garcia Date: Wed Sep 15 15:15:28 2021 -0400 upgrade to go-liveshare 0.16.0 commit 0f72e3d88642e36a1f3cbb2ef95866b2269541d2 Author: Jose Garcia Date: Wed Sep 15 14:29:16 2021 -0400 defer stopPolling and docs commit 06719866c95e834d87fd2ecd87b5889b14d6df2b Author: Alan Donovan Date: Wed Sep 15 13:09:31 2021 -0400 move api to internal/ commit 547c62922050ad98fa8a20f42c2532b05c932909 Author: Jose Garcia Date: Wed Sep 15 10:38:19 2021 -0400 fix ctx cancellation errors & fix todo for X11 forwarding commit 22433a57dba2626ba1f96195c3bf63ba7784e8a5 Merge: 2de51a8e 20e618fd Author: Jose Garcia Date: Wed Sep 15 09:52:31 2021 -0400 Merge pull request #18 from github/jg/ssh-server-docs ssh server: docs commit 20e618fd025e115726853db4b4fc37ec76295ebd Author: Jose Garcia Date: Wed Sep 15 13:49:03 2021 +0000 pr feedback commit 8abff2af97688a47744066aac4cd632f22259b53 Author: Jose Garcia Date: Wed Sep 15 13:14:58 2021 +0000 move StartSSHServer to Session commit 2de51a8ec80518aa16573cfc4e699cada88bad10 Merge: 467c6951 5b23d87d Author: Alan Donovan Date: Wed Sep 15 09:06:49 2021 -0400 Merge pull request #17 from github/rm-terminal Remove Terminal, no longer needed by ghcs commit fb5a35568ca44340ba6e332452bca6873baae62d Author: Mislav Marohnić Date: Wed Sep 15 13:58:10 2021 +0200 Ensure original errors are wrapped with "%w" instead of "%v" commit 497b45e4e2e41c2fca55f61995a289a6e62022e6 Author: Jose Garcia Date: Tue Sep 14 23:57:40 2021 +0000 ssh server docs commit e13398f6b4d58a40d819893d7b9258124a1de7c5 Author: Andrew Hsu Date: Tue Sep 14 09:08:19 2021 -0500 fix browse of markdown files with line ranges (#4310) Co-authored-by: Mislav Marohnić commit 3abde5f0c52346cdbb65cff8d8c92b503371c030 Merge: a5b98552 f5adc9e3 Author: Jose Garcia Date: Mon Sep 13 14:44:40 2021 -0400 Merge pull request #148 from github/jg/remove-deprecation-msg remove all deprecation messages and deprecated functionality commit d7f7a98d818041e627811f65b586579f3db83e9e Merge: 6f3b1eb0 2f451733 Author: Mislav Marohnić Date: Mon Sep 13 19:43:39 2021 +0200 Merge pull request #4320 from cli/site-ssh-key Publish docs site using a deploy key instead of PAT commit 6f3b1eb080d586d47e753e4d869121641e07a0c1 Merge: 3b20dfc0 619333ad Author: Mislav Marohnić Date: Mon Sep 13 19:30:16 2021 +0200 Merge pull request #3967 from despreston/des/3839-warn-limit show warning when limit exceeds search api max commit a8492bb0eab73171b46fa2c3871a2cfb71a9bf34 Author: Sam Coe Date: Mon Sep 13 10:14:20 2021 -0700 Allow user input for git fetch in repo sync commit f5adc9e3a75eecd6a20b2541e9406fad2820a4fc Author: Jose Garcia Date: Mon Sep 13 10:58:00 2021 -0400 remove all deprecation messages and deprecated functionality commit 2f45173370be73d6a1475c18fdadaf89c54cf7a0 Author: Mislav Marohnić Date: Mon Sep 13 13:28:47 2021 +0200 Publish docs site using a deploy key instead of PAT I'd like to decommission SITE_GITHUB_TOKEN as it's a PAT that has write access to all my `github/*` repositories. Instead, I've created a deploy key that only has access to `github/cli.github.com`. ssh-keygen -t ed25519 -C "gh docs push" -N "" -f ~/.ssh/gh-docs-publish gh repo -R github/cli.github.com deploy-key add ~/.ssh/gh-docs-publish.pub # testing: GIT_SSH_COMMAND='ssh -i $HOME/.ssh/gh-docs-publish' git push ... commit a5b985523c07aa3a0986b8acaa3d1d60e09e401a Merge: 74a4afcd c4be0a0e Author: Alan Donovan Date: Mon Sep 13 09:30:39 2021 -0400 Merge pull request #147 from github/tty-windows Don't assume stdin/stdout fds are 0/1 on windows commit c4be0a0e284ed1d22fc26dc452b81b56ab4549b5 Author: Alan Donovan Date: Mon Sep 13 09:29:46 2021 -0400 this time without compile errors commit 74a4afcd77e50a3ed5df2588cdb84ccc53243d9d Merge: 3ca3dd05 810c1276 Author: Issy Long Date: Mon Sep 13 10:32:19 2021 +0100 Merge pull request #139 from github/fix-version-string goreleaser: Fix version string replacement commit 1926971a762325d3f8a46b08efcb54770ab2aa8c Author: Siarhei Fedartsou Date: Sun Sep 12 21:04:03 2021 +0300 Remove non-relevant test commit f3053c36287078aa144ff7ac259ac5d89de0a3e0 Author: Siarhei Fedartsou Date: Sun Sep 12 20:58:13 2021 +0300 fix tests commit 373d1efb583bc65a71ba3838ed4d60e6aaa6718f Author: Siarhei Fedartsou Date: Sun Sep 12 20:50:21 2021 +0300 format code commit 11466fda12b7ca98780b3b9981d232e2fd9e6e21 Author: Siarhei Fedartsou Date: Sun Sep 12 20:48:23 2021 +0300 Add --draft and --non-draft filters to gh pr list commit 1526ab5bff3065558bbc3b40db3eb501f6e56061 Author: Alan Donovan Date: Fri Sep 10 18:08:48 2021 -0400 fix URL commit af301bfff1ab4669e95f79067bb4aae1460f17c0 Author: Alan Donovan Date: Fri Sep 10 17:36:20 2021 -0400 stdin/stdout fds are not 0/1 on windows commit 3ca3dd05a1f5264b491628d9c0fa6609a7cad44b Merge: 8b06ffe2 ad600d22 Author: Jose Garcia Date: Fri Sep 10 16:04:09 2021 -0400 Merge pull request #143 from github/jg/ssh-flags ghcs ssh: make flag description consistent commit ad600d22edffa916eb80ab2e82ecf522e6110c71 Merge: 81f08e7b 8b06ffe2 Author: Jose Garcia Date: Fri Sep 10 16:03:09 2021 -0400 merge upstream commit 8b06ffe2bdcdd170243abb5f628ec58e5cf6da9f Merge: 6c907869 31e1d974 Author: Alan Donovan Date: Fri Sep 10 15:48:40 2021 -0400 Merge pull request #145 from github/remove-script Remove go-ghcs-crypto vendor script commit 5b23d87d47f4ffa6dfce6c10794e9e32ec5c6371 Author: Alan Donovan Date: Fri Sep 10 15:09:45 2021 -0400 Remove Terminal, no longer needed by ghcs commit 6c907869ff34dac7905db6ec8290ae0d84463071 Merge: 31e1d974 79807504 Author: Jose Garcia Date: Fri Sep 10 15:06:18 2021 -0400 Merge pull request #144 from github/jg/remove-terminal ghcs ssh: remove terminal, bash_profile setup commit 798075045e39e4dc1dd2319dde9665bd69edeab1 Author: Jose Garcia Date: Fri Sep 10 14:58:47 2021 -0400 remove terminal, bash_profile setup commit 31e1d974aa29d6b138f57ce25490c932433505b4 Merge: 2fe71e35 34e52ba2 Author: Jose Garcia Date: Fri Sep 10 14:17:44 2021 -0400 Merge pull request #142 from github/jg/delete-flags ghcs delete: deprecate argument/subcommands & introduce flags commit 2fe71e357671e875f43f215a698922fbff82ef4e Merge: 9b1ed00a 4f6cab19 Author: Alan Donovan Date: Fri Sep 10 14:15:36 2021 -0400 Merge pull request #138 from github/sigint-delay Wait forever for SIGINT delivery commit 34e52ba24a30f3b289ad1f485f09eef3c57a13b9 Author: Jose Garcia Date: Fri Sep 10 14:11:50 2021 -0400 deprecate subcommands commit 9b1ed00ae3705e61f8f9189e41287391241168f0 Merge: 28ab023b efb2569d Author: Alan Donovan Date: Fri Sep 10 14:11:27 2021 -0400 Merge pull request #140 from github/crypto fork go-ghcs-crypto repo into internal module commit 28ab023b070c2447424b059f8e008fd6f9e09fb1 Merge: 219a22ef 9b9e533c Author: Jose Garcia Date: Fri Sep 10 13:33:26 2021 -0400 Merge pull request #133 from github/jg/ports-flags ghcs ports: deprecate codespace arg, introduce global flag commit 9b9e533cb9697c63bac18062f1fc38ab07505d06 Author: Jose Garcia Date: Fri Sep 10 13:32:30 2021 -0400 add comment for special handling commit efb2569d2bc22c09835b9fd761a052fa692cf35d Author: Alan Donovan Date: Fri Sep 10 12:29:25 2021 -0400 move vendored go-ghcs-crypto to internal module commit 810c127608a200318cfbd4d8247d0a6948f9443c Author: Issy Long Date: Fri Sep 10 15:24:46 2021 +0100 goreleaser: Fix version string replacement - The `Version` variable casing changed in https://github.com/github/ghcs/commit/6a4950cf7ae02afc36cee06c26c232bc3fb71347#diff-d897a31624bae4fe935e8dc2243f41626c68639be6643535297c06935277ffb4, so we need to update our version setting code. - Otherwise, for `ghcs 0.11.0`, `ghcs --version` would print "DEV". commit 4f6cab195a89535ae8a96f9ad5bef8afba145b23 Author: Alan Donovan Date: Fri Sep 10 10:08:54 2021 -0400 wait for sigint delivery commit 219a22ef12f1f5953635d9935311527bb501277e Merge: c06d56fa 0afdcdec Author: Jose Garcia Date: Fri Sep 10 08:35:15 2021 -0400 Merge pull request #137 from github/jg/liveshare-0.15.0 go-liveshare 0.15.0 commit c06d56fa200e28e002beb4bd798b6f55a2bdab14 Merge: 0afdcdec 22f9824e Author: Alan Donovan Date: Thu Sep 9 18:10:09 2021 -0400 Merge pull request #136 from github/sigint deliver SIGINT to self after Ctrl-C in survey commit fe2d5ebf37aa2a1e4b3abdab2db1ad14f434845d Merge: 230bf640 0afdcdec Author: Jose Garcia Date: Thu Sep 9 17:22:07 2021 -0400 merge upstream + pr feedback commit 467c6951221766afaa9c0f9dceeaee1127f865b2 Merge: 2b4e8810 272ea57b Author: Jose Garcia Date: Thu Sep 9 17:00:47 2021 -0400 Merge pull request #16 from github/jg/ignore-pf-errs port forwarder: ignore conn errors commit 272ea57b541c33846add2b9b493c0ca011985c93 Author: Jose Garcia Date: Thu Sep 9 21:00:09 2021 +0000 revert comment update commit 0afdcdec84a7ff5aacdd830d97a1d1f8ff0c908c Merge: af541ad5 2cbe1207 Author: Alan Donovan Date: Thu Sep 9 16:41:38 2021 -0400 Merge pull request #126 from github/lightstep ghcs: add --lightstep flag for tracing commit 2cbe1207742a7e5add6d0da54edda4257a98083c Author: Alan Donovan Date: Thu Sep 9 16:37:26 2021 -0400 return err, don"t fatal commit 22f9824ec8da912e5a4cecb3b77b7f23130f1829 Author: Alan Donovan Date: Thu Sep 9 16:31:15 2021 -0400 deliver SIGINT to self after Ctrl-C in survey commit efe519cb7af8015a0747ac20d91877627b21b8cc Author: Jose Garcia Date: Thu Sep 9 20:11:45 2021 +0000 comments + fix Forward method commit 920f793c6ddf001901308df947091c9d98563fdd Author: Jose Garcia Date: Thu Sep 9 19:33:16 2021 +0000 pr feedback commit 1ff5c514fb82458558757fc8f1fcdf6cc838afc0 Author: Jose Garcia Date: Thu Sep 9 18:35:05 2021 +0000 fix erroneous ctx waiting and introduce back io.EOF handling commit ee44ecc944cbd6288dadec9c9b723367f81b9f8c Author: Alan Donovan Date: Thu Sep 9 13:42:44 2021 -0400 include span context in HTTP request commit 2b4e88101849f0b90411e68d1990a83ff1c12aa7 Merge: 5c26d148 72659a36 Author: Alan Donovan Date: Thu Sep 9 13:31:20 2021 -0400 Merge pull request #15 from github/lightstep Add OpenTelemetry instrumentation commit 8b0e8c990e68dc9b74ed3d40f4c73c9a24bacb7b Author: Jose Garcia Date: Thu Sep 9 17:31:18 2021 +0000 ignore pf conn errors commit af541ad5f2ec2be54bad96bf114b0219b541c22f Merge: 803666a5 b96341a5 Author: Alan Donovan Date: Thu Sep 9 12:43:00 2021 -0400 Merge pull request #132 from github/ask-tty consolidate survey functions commit 3b20dfc03218d1e6d6036689f9f79a8a6c254344 Merge: 156872c5 7519421f Author: Mislav Marohnić Date: Thu Sep 9 18:16:47 2021 +0200 Merge pull request #4299 from andrewhsu/fix-project-layout fix link to code in project-layout doc commit 81f08e7baf9ec7f3610d852b08834b338a77b18d Author: Jose Garcia Date: Thu Sep 9 12:08:07 2021 -0400 start converting to flags commit b96341a5f99fd5ddf0f68c974e0f2f08b633df2d Merge: cbb82535 803666a5 Author: Alan Donovan Date: Thu Sep 9 12:01:55 2021 -0400 Merge branch 'main' into ask-tty commit cbb82535448b11b53dfd51ea8a67c37e6a9ac1f4 Author: Alan Donovan Date: Thu Sep 9 10:28:55 2021 -0400 consolidate survey functions commit 7519421fc0a712e1a7a28628f035ac77ce2de33c Author: Andrew Hsu Date: Thu Sep 9 10:24:42 2021 -0500 fix link to code in project-layout doc Update link to code so it matches given example in doc: `gh help issue list`. commit 230bf640c5f020ec866596c5fbf53011c355b54d Author: Jose Garcia Date: Thu Sep 9 11:06:18 2021 -0400 global flag, choose codespace when empty commit 3a4088a31c667f61d5358d27713aa52fd4c55738 Merge: 9dbf267e 803666a5 Author: Jose Garcia Date: Thu Sep 9 10:19:23 2021 -0400 Merge branch 'main' of github.com:github/ghcs into jg/ports-flags commit 803666a552887d48dc8404648b654420ae968a6b Merge: e87ca729 3b198c17 Author: Jose Garcia Date: Thu Sep 9 10:10:35 2021 -0400 Merge pull request #116 from github/jg/code-flag ghcs code: codespace flag, deprecate argument commit 3b198c1707737ed000febd0eb9b469452231f097 Author: Jose Garcia Date: Thu Sep 9 10:09:14 2021 -0400 switch to Errorln commit 8422e29242d818a696c2a33f5b7eeccf828a66f4 Merge: 3216cbc0 e87ca729 Author: Jose Garcia Date: Thu Sep 9 10:08:00 2021 -0400 Merge branch 'main' of github.com:github/ghcs into jg/code-flag commit e87ca729f1b8e9fab337122ebb8e54beec689039 Merge: 09a66090 c89c14af Author: Jose Garcia Date: Thu Sep 9 10:07:41 2021 -0400 Merge pull request #115 from github/jg/logs-flags ghcs logs: codespace flag, deprecate argument commit ecb6c8a09a84d93adaad52994252c02ac363cec4 Merge: c6a99158 cbbfafee Author: Alan Donovan Date: Wed Sep 8 18:06:08 2021 -0400 Merge branch 'main' into lightstep commit 09a660905081d74504a376d262a9886a150526d9 Author: Christian Gregg Date: Thu Sep 9 12:37:05 2021 +0100 Show * after branch name if codespace working directory is dirty Append a `*` to the end of a branch name in `ghcs list` if the working directory of the codespace is dirty (has uncommited or unpushed changes). Closes: #104 commit c89c14aff0c314da2aaf6453c9a2ee0064d41fe7 Merge: c86cd34f 6b0510f8 Author: Mislav Marohnić Date: Thu Sep 9 12:55:32 2021 +0200 Merge remote-tracking branch 'origin' into jg/logs-flags commit cbbfafee9825e08059fe577c46852c22f9d4c8a1 Merge: 3a46f2ac 6b0510f8 Author: Alan Donovan Date: Wed Sep 8 18:04:56 2021 -0400 Merge branch 'main' of https://github.com/github/ghcs commit c6a991586104cc933e09fd1ba008ec98f6e32073 Author: Alan Donovan Date: Wed Sep 8 17:15:42 2021 -0400 add --lightstep flag for tracing commit 72659a360334186804e5cfcf781d504104ca50a8 Author: Alan Donovan Date: Wed Sep 8 17:21:54 2021 -0400 add lightstep instrumentation commit 3a46f2ac56e9aa23037f76e56a0d2858ea41f152 Author: Alan Donovan Date: Wed Sep 8 17:15:42 2021 -0400 add --lightstep flag for tracing commit 6b0510f81aab926d96d1e8d89c5d28e57cc5af50 Merge: 466ad857 d8138c08 Author: Jose Garcia Date: Wed Sep 8 15:17:11 2021 -0400 Merge pull request #125 from github/jg/revert-errgroup ghcs ssh/logs: revert errgroup usage commit d8138c08b8b59a39918c572e06d2d38317beadf0 Author: Jose Garcia Date: Wed Sep 8 14:56:16 2021 -0400 revert errgroup usage for ssh and logs commit 466ad8572c0c514b8160b2368b541bab8b71f54e Merge: 5569cc56 07300aeb Author: Jose Garcia Date: Wed Sep 8 14:32:35 2021 -0400 Merge pull request #122 from github/jg/go-liveshare-0.13.0 upgrade to go-liveshare 0.13.0 commit c86cd34f5ef9680de672d388b8d9a1dc2c4e35b3 Author: Jose Garcia Date: Wed Sep 8 13:38:27 2021 -0400 switch to Errorln commit efc6fd369c9e5410ea4e068972337e4f2aa2f70a Merge: b79ea871 5569cc56 Author: Jose Garcia Date: Wed Sep 8 13:37:35 2021 -0400 merge upstream commit 5569cc56c6af31bfd57380a1360dc09698f881c9 Merge: 07300aeb fda40a96 Author: Jose Garcia Date: Wed Sep 8 13:34:14 2021 -0400 Merge pull request #120 from github/jg/output-logger output pkg: new Errorln method and add comments commit fda40a96826f9915a73545894d855f03d47670c7 Author: Jose Garcia Date: Wed Sep 8 10:29:30 2021 -0400 new Errorln method and add comments commit 156872c5db90f6b492d7ee40c7e860a5db1d4dae Merge: 8f3b6749 2a2088dc Author: Mislav Marohnić Date: Tue Sep 7 14:33:41 2021 +0200 Merge pull request #4271 from wrslatz/4201-extension-doc Document installing extensions using full repo URL commit 07300aeb3817435b9b8a9e596f95c4fc3fcec05a Merge: dd5fe494 d395dae3 Author: Alan Donovan Date: Tue Sep 7 07:12:57 2021 -0400 Merge pull request #119 from github/silence-errors ghcs: don't double-print errors commit d395dae3a875fb248b6fcc1abe705064d422d057 Author: Alan Donovan Date: Mon Sep 6 15:17:24 2021 -0400 don't double-print errors commit dd5fe49420311be285757311337a57cd04456ee6 Merge: 88146a16 9e81dc7f Author: Alan Donovan Date: Mon Sep 6 14:56:20 2021 -0400 Merge pull request #114 from github/listen-race Update to go-liveshare@v0.12.0 commit 2a2088dc891daaeea7f0a81d2d2b5bcde166d5db Author: Mislav Marohnić Date: Mon Sep 6 20:04:40 2021 +0200 :nail_care: tweak extension docs commit d731cb9c73d7c9cf331cc3d7aea2df2eb0dd1bbf Author: Mislav Marohnić Date: Mon Sep 6 16:57:59 2021 +0200 Fix determining current process location commit 55c3064f4546af948181232512b018ce9e062ed9 Merge: 2dba58bf 8f3b6749 Author: Mislav Marohnić Date: Mon Sep 6 16:16:52 2021 +0200 Merge remote-tracking branch 'origin' into 3704-credential-helper commit 619333adb60203aff2e19455790a353c76fa3628 Author: Mislav Marohnić Date: Mon Sep 6 16:00:31 2021 +0200 Avoid using error values to pass information about the search cap commit e8b015b80dfb21a3408ab97961be7aba52a4c58b Author: Des Preston Date: Fri Jul 9 15:15:35 2021 -0400 show warning when limit exceeds search api max Fixes #3839 commit 8f581fa4504299006442f3ae0855b1a29f4a8e22 Author: Siarhei Fedartsou Date: Sun Sep 5 21:25:48 2021 +0300 Extend test for params.go commit a133e9d9a7cba88d4af16c10ce68c3fa16adb029 Author: Siarhei Fedartsou Date: Sun Sep 5 21:13:16 2021 +0300 Add --head filter to gh pr list commit 28cbfc4aab369f7f6b665979bccf846a5023ccd1 Author: wrslatz <20246633+wrslatz@users.noreply.github.com> Date: Fri Sep 3 12:18:24 2021 -0400 Document installing extensions using full repo URL commit b79ea871fd38c1ba4d6b3f8a995a1754a72eb651 Author: Jose Garcia Date: Fri Sep 3 16:04:00 2021 -0400 rename arg commit 5c26d1488f537b4f9b0f2d63fa8afe251ab79e68 Merge: 10a129b7 50523c4f Author: Alan Donovan Date: Fri Sep 3 15:13:20 2021 -0400 Merge pull request #14 from github/ssh-close-workaround add workaround for ssh.channel.Close EOF commit 50523c4f1087ea361b1216d894a26c7ca6fb7d46 Author: Alan Donovan Date: Fri Sep 3 14:39:47 2021 -0400 remove ListenTCP and add workaround for ssh.channel.Close EOF commit 9e81dc7fdef457f09a18bd81a231b34a3e8f7d03 Author: Alan Donovan Date: Fri Sep 3 12:56:47 2021 -0400 fix missing error return commit 2c660fa2e5a47c499f74aeb7dc522349a5753d3a Author: Alan Donovan Date: Fri Sep 3 12:55:40 2021 -0400 avoid ListenTCP helper commit 43198b24aa6c5342dba92cbe514ab30f9dea05ac Author: Alan Donovan Date: Fri Sep 3 12:50:11 2021 -0400 use errgroup commit 9193b03b696eb0f91eb0f7d1273b3aed38f514f5 Author: Jose Garcia Date: Fri Sep 3 12:40:01 2021 -0400 introduce follow, deprecate tail commit 9dbf267e54fd679563474e442d0956c444f5f328 Author: Jose Garcia Date: Fri Sep 3 12:33:47 2021 -0400 codespace flag, deprecate argument commit 3216cbc07f9eb698ae88d3214c0647650a542bb7 Author: Jose Garcia Date: Fri Sep 3 11:43:10 2021 -0400 codespace flag, deprecate argument commit b1d83fe294e3082e4def167e7b891b9c5c81a80c Author: Jose Garcia Date: Fri Sep 3 11:33:33 2021 -0400 codespace flag, deprecate argument commit a56a84947a0384e86dbdbe77cdbfa9b5084b2402 Merge: 981b2545 88146a16 Author: Alan Donovan Date: Thu Sep 2 17:27:25 2021 -0400 Update ghcs for go-liveshare@v0.12.0 commit 8f3b6749d7dd7ceafa1a15d211a5cb4d32422b22 Merge: 029d49f3 b0b67014 Author: Mislav Marohnić Date: Fri Sep 3 16:10:16 2021 +0200 Merge pull request #4241 from cli/saml-error Suggest to re-authenticate to fix "SAML enforcement" error commit 10a129b7645fba5cfe16430a64ea65a440894a79 Merge: b4686935 e2552fbd Author: Alan Donovan Date: Fri Sep 3 10:05:02 2021 -0400 Merge pull request #13 from github/listen-race move Listen call into clients to avoid race commit e2552fbd2a049a8314d83e1b1357b6b5494267dd Author: Alan Donovan Date: Fri Sep 3 09:43:31 2021 -0400 rename to ForwardToListener commit 5bd0519ef32827e59d94003b995a62a8915f48d4 Author: Alan Donovan Date: Thu Sep 2 16:45:23 2021 -0400 move Listen call into clients to avoid race commit 88146a16a05ee9010152495f3d3304d44692a71b Merge: ab47e975 786a6319 Author: Alan Donovan Date: Thu Sep 2 17:26:01 2021 -0400 Merge pull request #110 from github/join-session Update to go-liveshare v0.11.0 commit 786a6319959b1fc38adf07b9e867d66cdd1ce352 Author: Alan Donovan Date: Thu Sep 2 17:21:24 2021 -0400 fix local/remote confusion in getPorts (!) commit 981b2545bc91e6f190c8ac8b8152a7c7cb0695a5 Author: Alan Donovan Date: Thu Sep 2 17:04:07 2021 -0400 sketch of changes for https://github.com/github/go-liveshare/pull/13 commit 1162c8adff7d236009ec99b8704e90f12b344e4c Author: Alan Donovan Date: Thu Sep 2 16:02:09 2021 -0400 fix go vet loopclosure finding commit 4e2c20606af750b6d980053510ef7e72106f51dc Merge: cee76123 ab47e975 Author: Alan Donovan Date: Thu Sep 2 15:56:50 2021 -0400 Merge branch 'main' into join-session commit cee761238ba166fad40414058c6ae6837f65324f Author: Alan Donovan Date: Thu Sep 2 15:51:56 2021 -0400 update go-liveshare@v0.11.0 commit 3485bacc97751521be724326189a566f02e30fb7 Author: Alan Donovan Date: Thu Sep 2 14:10:29 2021 -0400 fix StartSharing data race commit b4686935b99e8f73fee1ae61556fcf90131b630a Merge: e9cb521b 4438b85e Author: Alan Donovan Date: Thu Sep 2 15:44:49 2021 -0400 Merge pull request #12 from github/fix-share-data-race Fix data race in StartSharing commit 4438b85e294e510edf97510ede486db175e8f084 Author: Alan Donovan Date: Thu Sep 2 15:41:36 2021 -0400 comment tweaks commit 94319d4cfeaa6b6a0389e75c0401e265e2078e09 Author: Alan Donovan Date: Thu Sep 2 15:34:57 2021 -0400 move localPort parameter to ForwardToLocalPort commit 94b91661cc68b200e30e809d8c26b41a7f37c1af Author: Alan Donovan Date: Thu Sep 2 14:30:19 2021 -0400 don't forget to close conn in case of sharing error commit ab47e9754013df47a245e99af45f5e140e4df4c3 Merge: 881e6d59 090af229 Author: Jose Garcia Date: Thu Sep 2 14:14:41 2021 -0400 Merge pull request #112 from github/jg/start-errs api start: ignore any 7 err code in start commit 090af2290b186306f27e75c112afeed7df25b8d5 Author: Jose Garcia Date: Thu Sep 2 14:13:40 2021 -0400 pr feedback commit 029d49f3b38ebd54cd84b23732bb7efabfce4896 Merge: 1af6a669 e0fa56dc Author: Mislav Marohnić Date: Thu Sep 2 20:10:09 2021 +0200 Merge pull request #4199 from cli/go-module-v2 Rename the module to "github.com/cli/cli/v2" commit 87b15aa264e583688aa9b448ea57663b87a2b4cf Author: Alan Donovan Date: Thu Sep 2 14:03:48 2021 -0400 Fix data race in StartSharing commit e0fa56dc299c22183faaf634a2c0eacf4c82e69a Merge: 11fbb60a 1af6a669 Author: Mislav Marohnić Date: Thu Sep 2 20:02:16 2021 +0200 Merge remote-tracking branch 'origin' into go-module-v2 commit 49652cdefa175cb62616d8cf8958f1bb7fc2e88e Author: Nate Smith Date: Thu Sep 2 12:57:50 2021 -0500 Update pkg/cmd/run/cancel/cancel.go Co-authored-by: Josh Soref <2119212+jsoref@users.noreply.github.com> commit 5c65cfd2498785d82617357d0ce49ffc8a78c7c2 Author: Jose Garcia Date: Thu Sep 2 13:42:52 2021 -0400 ignore any 7 err code in start commit 1af6a669e342e7b5853c561710a9817bdd5d88bd Merge: abfe2961 6cbc886c Author: Nate Smith Date: Thu Sep 2 12:39:23 2021 -0500 Merge pull request #4214 from cli/garden-portability Use `x/term` package for repo garden instead of shelling out to stty commit 881e6d5940b4187f422630c54a88b1a75b1f213b Merge: 65c19a33 c15d810d Author: Jose Garcia Date: Thu Sep 2 13:33:53 2021 -0400 Merge pull request #111 from github/jg/remove-dir ghcs ssh: remove dir command commit c15d810d68ab0e9ffb0e2e094125845557d2ad7c Author: Jose Garcia Date: Thu Sep 2 13:27:12 2021 -0400 remove extra verb arg commit 8570f4111d954d9f15b78ed763ddb34ab7932740 Author: Alan Donovan Date: Thu Sep 2 11:14:36 2021 -0400 sketch after API changes in go-liveshare#11 commit e9cb521bfdef383ec750151c1860461b6613b912 Merge: 34c20cf1 6f45c7fa Author: Alan Donovan Date: Thu Sep 2 12:37:00 2021 -0400 Merge pull request #11 from github/session rename Server to Session and simplify API commit 6f45c7fa7dfd4553483d28242df77ca059a174d1 Author: Alan Donovan Date: Thu Sep 2 12:14:04 2021 -0400 point out data races to be fixed commit 23ea329f2eca3275ed356951d61c04c48be83680 Merge: 05a3d90a 34c20cf1 Author: Alan Donovan Date: Thu Sep 2 11:40:39 2021 -0400 Merge branch 'main' into session commit 05a3d90a99b4f884a492c797f352458519d1252a Author: Alan Donovan Date: Thu Sep 2 11:39:29 2021 -0400 more tweaks commit 34c20cf10573e4dd8ccd5bf6c7902c554ec59704 Merge: 9964a444 af38292f Author: Alan Donovan Date: Thu Sep 2 11:38:38 2021 -0400 Merge pull request #10 from github/rpc-fixes fix two data races in rpcHandler commit 65c19a3317adce3f81f24a3837c66e44e9820d38 Merge: 45f9ae76 c31fc057 Author: Alan Donovan Date: Thu Sep 2 11:38:25 2021 -0400 Merge pull request #107 from github/spelling-again use correct correct spelling of codespace commit 4cceda1af02e3a097418baadd14048d331780c50 Author: Alan Donovan Date: Thu Sep 2 11:06:49 2021 -0400 rename Server to Session and simplify API commit 45f9ae76f48bddbb8375b38014d23952c74bdc25 Merge: ef8dde4d 72a2099a Author: Alan Donovan Date: Thu Sep 2 09:48:28 2021 -0400 Merge pull request #108 from github/update-liveshare Update go-liveshare to v0.10.0 commit c31fc05746b02ace70283197b33fd3f7b6d0866a Author: Alan Donovan Date: Thu Sep 2 09:09:05 2021 -0400 more typo fixes commit af38292f1e0a80e0ef6d996f0d67aee0452c7232 Author: Alan Donovan Date: Wed Sep 1 18:12:23 2021 -0400 fix data races commit ca6d074333cf699b703387661dc096f99eae863d Merge: 55fa17d8 9964a444 Author: Alan Donovan Date: Wed Sep 1 17:55:31 2021 -0400 Merge branch 'main' into wip commit 72a2099a50bea5862ad3597833fc247ff94e0679 Author: Alan Donovan Date: Wed Sep 1 17:50:24 2021 -0400 fix breakage from API changes commit 49ccdd3d21a277094306a4462a6b19103e8575d1 Author: Alan Donovan Date: Wed Sep 1 17:26:26 2021 -0400 use correct correct spelling of codespace commit ef8dde4d2e4a873b49b5648462f285e3363e0018 Merge: c0fbb7e9 2163aba3 Author: Jose Garcia Date: Wed Sep 1 16:43:02 2021 -0400 Merge pull request #106 from github/jg/sku-params ghcs create: pass branch for sku selection, pre-select if only one is returned commit bfeb4e77c9063519547e1b9cf142fbc3f002e8a3 Author: Jose Garcia Date: Wed Sep 1 14:38:37 2021 -0400 remove dir command commit 2163aba3d5ae5f9643e295038a0710bc57b78cc7 Author: Jose Garcia Date: Wed Sep 1 13:54:45 2021 -0400 pass branch for sku selection, pre-select if only one is returned commit abfe29617a939baf2875346d01d2a7f653a4d243 Merge: e6ff77ce dafd0bfb Author: Mislav Marohnić Date: Wed Sep 1 18:08:51 2021 +0200 Merge pull request #4253 from despreston/4248-line-range feat(4248): add support for line range w/ browse commit 55fa17d8bc3055ddd143ac0b4e70f8513c01ef70 Author: Alan Donovan Date: Tue Aug 31 17:30:40 2021 -0400 wip commit 9964a444b012a3d2cf99d0f25ccf9320adfb32fc Merge: fc4c678d b63972b6 Author: Alan Donovan Date: Tue Aug 31 17:30:19 2021 -0400 Merge pull request #9 from github/liveshare-spelling spell Live Share product name correctly in UI commit c0fbb7e9fbf6ec2bc1b53868d202c0008f1433d3 Merge: 4a45feb7 3aad0bbe Author: Alan Donovan Date: Tue Aug 31 17:30:00 2021 -0400 Merge pull request #99 from github/runcommand preparatory cleanups to ssh tunnel/port forwarding code commit 3aad0bbeb4025b588d5192f153859d90e9f4289a Author: Alan Donovan Date: Tue Aug 31 17:27:53 2021 -0400 check context error in PollPostCreateStates commit 37b2f5e979fc56549f93c951d34b0e4f48644724 Merge: 68792d5a 4a45feb7 Author: Alan Donovan Date: Tue Aug 31 17:10:36 2021 -0400 Merge branch 'main' into runcommand commit 4a45feb7f03c0cebbbdfe01c0fbe01d6585d294e Merge: 5238f249 6a527941 Author: Alan Donovan Date: Tue Aug 31 16:35:49 2021 -0400 Merge pull request #101 from github/fix-80 Suppress display of usage message after errors commit 6a527941bf8c6098cf7763ae695b690024fccca2 Author: Alan Donovan Date: Mon Aug 30 18:09:52 2021 -0400 suppress display of usage message after errors commit dafd0bfbd1a60da186398fda138535f721904777 Author: Des Preston Date: Tue Aug 31 16:32:37 2021 -0400 feat(4248): add support for line range w/ browse Allow using a range of lines when browsing files. For example: `gh browse test.go:10-20` Closes #4248 commit 5238f249f3a003abcb8f68a36b80cd6822fef413 Merge: d6f52ae5 f1c3a22a Author: Alan Donovan Date: Tue Aug 31 16:24:47 2021 -0400 Merge pull request #105 from github/product-spelling spell product names (Codespace, Live Share) correctly in UI commit f1c3a22a0b74649db94921724858f2ca7dd713fa Merge: bbcf2dd3 d6f52ae5 Author: Alan Donovan Date: Tue Aug 31 16:24:41 2021 -0400 Merge branch 'main' into product-spelling commit fc4c678d0357181bea67b0b97fcfc1e76f0cd360 Merge: 273782bc 4af240d8 Author: Alan Donovan Date: Tue Aug 31 16:23:33 2021 -0400 Merge pull request #8 from github/portfwd-errors handle errors in port forwarding commit 68792d5aa51447fd9d3405de0d17d486281e0ce0 Merge: c0aae522 fee4b484 Author: Alan Donovan Date: Tue Aug 31 16:19:37 2021 -0400 Merge branch 'main' into runcommand commit d6f52ae55733333cb7b187cff212aedb6805b55c Merge: fee4b484 ebb04d17 Author: Edmundo Gonzalez <51725820+edgonmsft@users.noreply.github.com> Date: Tue Aug 31 13:05:56 2021 -0700 Merge pull request #91 from github/edgonmsft/codespaces-ssh-rpc Implement RPC interface so that the codespaces agent starts and configures ssh in the codespace commit ebb04d1753f27fa8747916907e9012c577708e42 Author: Jose Garcia Date: Tue Aug 31 19:52:32 2021 +0000 format code commit 535d832f8abfaaf997931b162411dd7aeedfa4ba Author: Jose Garcia Date: Tue Aug 31 15:50:04 2021 -0400 small tweak commit c0aae52289a8c3274c85d64639050dec66063138 Author: Alan Donovan Date: Tue Aug 31 13:52:37 2021 -0400 move port choice, and PortForwarder.Start call, into clients commit 509e037a5e916a2ebc7744859b4cc2a83998edc0 Author: Alan Donovan Date: Tue Aug 31 12:01:59 2021 -0400 address review comments commit bbcf2dd321527e08df5c483b1646b8b7fab53d78 Author: Alan Donovan Date: Tue Aug 31 11:15:26 2021 -0400 spell product names (Codespaces, Live Share) correctly commit b63972b62f2564dad26a922d346108c4f7683953 Author: Alan Donovan Date: Tue Aug 31 11:07:26 2021 -0400 spell Live Share product name correctly in UI commit fee4b484ea8c17f1c13dcd86fc17c1db6b552abf Merge: dc40d990 ea97e2e7 Author: Alan Donovan Date: Tue Aug 31 09:20:35 2021 -0400 Merge pull request #102 from github/fix-12 remove sleep 1s in ssh subcommand commit dc40d990960d1544cb255654c3403c9ddce73ea4 Merge: 3967d0c6 15dab395 Author: Alan Donovan Date: Tue Aug 31 09:18:45 2021 -0400 Merge pull request #103 from github/fix-EnvironmentNotShutdown in Start, ignore HTTP 503 with reason 7 EnvironmentNotShutdown commit 15dab395a519bee18c9b2a2f7bd7cebafc578b1b Author: Alan Donovan Date: Mon Aug 30 18:23:55 2021 -0400 in Start, ignore HTTP 503 with reason 7 EnvironmentNotShutdown commit ea97e2e73dfe8b89ffdaafb1f9c83a8cd4f2919c Author: Alan Donovan Date: Mon Aug 30 18:15:37 2021 -0400 remove sleep 1s commit 40317e91f8ae0a5460234ae398155af1650c9086 Author: Alan Donovan Date: Sat Aug 28 20:02:08 2021 -0400 cleanup to ssh api commit 4af240d87da018b38c4765f31ece31b7aa2c8478 Author: Alan Donovan Date: Mon Aug 30 17:36:28 2021 -0400 handle errors in port forwarding commit 903b7be7dea2d4a0f5a2c9cc4ef3053d90029ca2 Author: Edmundo Gonzalez <51725820+edgonmsft@users.noreply.github.com> Date: Mon Aug 30 21:01:13 2021 +0000 Comments from pr. commit 2dba58bfa80b8ed1c8fe81c5f6413ae42694f24e Author: wilso199 Date: Mon Aug 30 16:57:43 2021 -0400 Using gh executable from PATH in favor of the resolved path commit 954d46dce5846c94c2f3cfd07206acacc5208d19 Author: Edmundo Gonzalez <51725820+edgonmsft@users.noreply.github.com> Date: Mon Aug 30 17:30:28 2021 +0000 Changes from comments on pr. commit b0b67014f124a8eeeb74c8a921fd79f69f66a576 Author: Mislav Marohnić Date: Mon Aug 30 17:18:59 2021 +0200 Suggest to re-authenticate to fix "SAML enforcement" error As far as I can put together, this error appears when someone has authenticated GitHub CLI with a PAT that isn't authorized to access a certain org. It can also happen if someone has authorized GitHub CLI using the legacy "GitHub CLI (dev)" OAuth app instead of our production OAuth app. Doing `gh auth refresh` will re-authenticate the user using our production "GitHub CLI" OAuth app which will not have problems accessing resources in different GitHub organizations. commit e6ff77ce73c201b0ee36d2b802ea45e9e1ad1822 Merge: 99cbfd3d 1102de89 Author: Mislav Marohnić Date: Mon Aug 30 16:27:33 2021 +0200 Merge pull request #4239 from Jernik/trunk add quotes around `@me` in documentation to ensure examples work on ps commit 1102de89be1f40512201f06b2ff9f8009ee12950 Author: Luke Date: Mon Aug 30 09:13:43 2021 -0500 add quotes around @me in documentation to ensure examples work on powershell commit 0c066cbd099622d16516b346445050d16f9d68df Author: Edmundo Gonzalez <51725820+edgonmsft@users.noreply.github.com> Date: Mon Aug 30 05:05:43 2021 +0000 Fix compilation error. commit ee36a005b152f08594e00b9fb5720fb7614eae91 Merge: 13917a28 3967d0c6 Author: Edmundo Gonzalez <51725820+edgonmsft@users.noreply.github.com> Date: Mon Aug 30 05:01:22 2021 +0000 Merge remote-tracking branch 'origin/main' into edgonmsft/codespaces-ssh-rpc commit 13917a289df253bd819511ee75d7851551b5f86e Author: Edmundo Gonzalez <51725820+edgonmsft@users.noreply.github.com> Date: Mon Aug 30 04:52:27 2021 +0000 Moved function to ssh.go file. commit 5db9e2d83e04754f85f18c6a6f9e9834826e86cc Author: Edmundo Gonzalez <51725820+edgonmsft@users.noreply.github.com> Date: Mon Aug 30 04:48:56 2021 +0000 PR changes. commit 3967d0c6233a6c0eaf745f5a82321bc3da5ee6e8 Merge: c053bd0b 634796e8 Author: Jose Garcia Date: Fri Aug 27 18:24:21 2021 -0400 Merge pull request #64 from github/jg/dotfiles-status Post create status support on create commit 634796e8a8934aaaa640556a71066a396fea8cc9 Merge: 0cf2640c c053bd0b Author: Jose Garcia Date: Fri Aug 27 18:18:30 2021 -0400 merge main commit 0cf2640c863898fb3a78f19be2d87f29f2ba677f Author: Jose Garcia Date: Fri Aug 27 18:14:10 2021 -0400 better docs and stop ticker commit c053bd0bde0ec7d1729e3e56c4af80392426d170 Merge: a7a57d8c 54a9e745 Author: Alan Donovan Date: Fri Aug 27 18:13:53 2021 -0400 Merge pull request #95 from github/main-style cmd/ghcs: style tweaks commit 54a9e74565151d3f5a8b22be601c35a907a4e53f Merge: 9f082ca8 a7a57d8c Author: Alan Donovan Date: Fri Aug 27 18:12:38 2021 -0400 Merge branch 'main' into main-style commit 272af2fadf4694bf2ed59845aa469c8bd2a8c0ea Author: Jose Garcia Date: Fri Aug 27 18:12:06 2021 -0400 add docs commit a7a57d8c659fc8760f03a349677922f26217ba91 Merge: c66cf0da dcf4f041 Author: Alan Donovan Date: Fri Aug 27 18:11:35 2021 -0400 Merge pull request #93 from github/defer-close api: close HTTP response body stream on all control-flow paths commit dcf4f041e9817e7aed0f2e584f7c00884c85f258 Author: Alan Donovan Date: Fri Aug 27 18:01:52 2021 -0400 deal with Start errors, non-JSON commit adc1ee5e2d96f8d76a26dbad75f84fb4e850dc4d Merge: a5a18026 c66cf0da Author: Jose Garcia Date: Fri Aug 27 17:43:43 2021 -0400 merge main commit a5a18026cc34382da6b7c7bf3a6d950f7a8f0678 Author: Jose Garcia Date: Fri Aug 27 17:39:10 2021 -0400 fix linter commit 368e8c61105f7beafcdedd3d6c3376293db345ca Author: Jose Garcia Date: Fri Aug 27 17:34:06 2021 -0400 simplify contract for state polling commit 4545e11ffc159e21bd01deb7e6a71216e8668248 Merge: 90f3ac6f c66cf0da Author: Alan Donovan Date: Fri Aug 27 17:33:46 2021 -0400 Merge branch 'main' into defer-close commit 9f082ca887b8da38076d1c5515fe50f5fe4f6cc5 Merge: a5ae72cb c66cf0da Author: Alan Donovan Date: Fri Aug 27 17:30:34 2021 -0400 Merge branch 'main' into main-style commit c66cf0da39a6df940f28985a6a541993a030a500 Merge: e423cb0e 8e954938 Author: Jose Garcia Date: Fri Aug 27 17:06:48 2021 -0400 Merge pull request #92 from github/jg/remove-dst-port Remove destination port column and add docs commit e5f45d4bfab826871cebabda66da6f46ad0be504 Author: Jose Garcia Date: Fri Aug 27 16:41:22 2021 -0400 docs and improvement to the showStatus implementation commit 1e8a8370fee4988be9ab089473d8d7ad07438761 Author: Jose Garcia Date: Fri Aug 27 16:29:02 2021 -0400 initial round of PR feedback commit e423cb0ef953f4d6e02707547f1c02615f9a7cff Author: Alan Donovan Date: Fri Aug 27 16:09:02 2021 -0400 display colon and cursor in survey prompts commit 44a22d6965a21beba91eea358101054463d16c9f Merge: a48f1070 8b395b5a Author: Alan Donovan Date: Fri Aug 27 15:56:03 2021 -0400 Merge pull request #97 from github/vscode-error ghcs code: improve vscode error commit 8b395b5ab5b86366ca6e2c6b5a46133cac703028 Author: Alan Donovan Date: Fri Aug 27 15:53:55 2021 -0400 ghcs code: improve vscode error commit a5ae72cb26fe05bb5d1ac031af14631183fe23d9 Author: Alan Donovan Date: Fri Aug 27 15:38:41 2021 -0400 revert removal of _ = f() to pacify linter commit d8f1baa519e1daa0441664956882f433a6f91df1 Author: Alan Donovan Date: Fri Aug 27 15:36:45 2021 -0400 more SKU renames. commit da34d12abb099b9ea3e1093c6c8af6c0fecae4ad Author: Alan Donovan Date: Fri Aug 27 15:26:34 2021 -0400 respond to review commit 90f3ac6f56a5b44fc65da1dd3003e97b2f4b378b Author: Alan Donovan Date: Fri Aug 27 14:23:33 2021 -0400 check status codes commit 2cc91c224edcbc8a266c8b82c9b898364f399cc3 Author: Jose Garcia Date: Fri Aug 27 12:53:27 2021 -0400 add comment for improvements commit da8655209b59fe4a5791e00d26fbbe63f9dc1db4 Author: Jose Garcia Date: Fri Aug 27 12:52:30 2021 -0400 make things private commit cb6552f4cae0d5255471319455f24254a5bcfae0 Author: Jose Garcia Date: Fri Aug 27 12:50:32 2021 -0400 more efficient impl for processing states commit 38ff786a7d2b209a74b3ab9bfd1ee3f2106323da Author: Alan Donovan Date: Fri Aug 27 11:25:24 2021 -0400 cmd/ghcs: style tweaks commit 8e95493872f31e953a33a86381e12ab93f7999f5 Author: Jose Garcia Date: Fri Aug 27 15:46:40 2021 +0000 period commit 5dc923777be8c5ca232128c2b7924420b49b6bfd Author: Jose Garcia Date: Fri Aug 27 15:32:18 2021 +0000 update docs, make ports private to be more consistent commit 0392c5017408cac6e63b71dd13b7e318350fd225 Author: Alan Donovan Date: Fri Aug 27 11:25:24 2021 -0400 api: close HTTP response body on all paths commit 3dcee5cca72f0a70aed7cdc270cd4a0d6f0d584b Author: Jose Garcia Date: Fri Aug 27 12:41:36 2021 +0000 remove dst port column and add docs commit 273782bcbcb06bb143d28f322fcc1e935e378737 Author: Jose Garcia Date: Fri Aug 27 11:49:21 2021 +0000 rename file commit 49cebb11ca769adf58347f30ba4fb0de662df2b3 Merge: 269196c9 0eb769d6 Author: Edmundo Gonzalez <51725820+edgonmsft@users.noreply.github.com> Date: Thu Aug 26 16:27:16 2021 -0700 Merge pull request #5 from github/edgonmsft/codespaces-ssh-rpc Adding sshRPC interface commit d5a26e1536048ab6294b064960f22c8e5ced71cc Author: Edmundo Gonzalez <51725820+edgonmsft@users.noreply.github.com> Date: Thu Aug 26 23:14:13 2021 +0000 Apply renames on the go-liveshare side. commit 0eb769d608552e54f6db6bdb53e70996893a6acb Author: Edmundo Gonzalez <51725820+edgonmsft@users.noreply.github.com> Date: Thu Aug 26 23:04:35 2021 +0000 Rename File commit 18ab421b0846e7a152d431646b12702f14b26ba9 Author: Edmundo Gonzalez <51725820+edgonmsft@users.noreply.github.com> Date: Thu Aug 26 23:04:05 2021 +0000 Rename to SSHServer commit a89c17a564b9a9a9bbee261d3b4a158c4efff36d Author: Edmundo Gonzalez <51725820+edgonmsft@users.noreply.github.com> Date: Thu Aug 26 22:34:56 2021 +0000 Adding sshRPC interface commit b6094e0006b8fd73c390429b2b46166048706b84 Author: Edmundo Gonzalez <51725820+edgonmsft@users.noreply.github.com> Date: Thu Aug 26 21:50:20 2021 +0000 Changes to point to RPC service. commit 99cbfd3dd8ea32a27b9545e6ada56a1f4732e90b Merge: 6ec289c4 5dfeb1f7 Author: Mislav Marohnić Date: Thu Aug 26 18:16:40 2021 +0200 Merge pull request #4213 from cli/defunct-doc-link Remove defunct link from `gh actions` commit 6cbc886c535bfead66476c061bd140dbea47265a Author: Mislav Marohnić Date: Thu Aug 26 16:45:55 2021 +0200 Use `x/term` package for repo garden instead of shelling out to stty Shelling out to stty seems hard to get right between Linux and macOS. commit 5dfeb1f7ce9be3a92fb33ac845d113ce51aa8f15 Author: Mislav Marohnić Date: Thu Aug 26 15:48:08 2021 +0200 Remove defunct link from `gh actions` The online guide that was GitHub CLI-specific no longer exists. Instead, "GitHub CLI" sections were added to existing individual articles about managing workflow runs. commit e4e77a42943c80c00d65a42088b466c3bf01f5d1 Merge: 151eb2b6 a48f1070 Author: Jose Garcia Date: Thu Aug 26 08:55:38 2021 -0400 merge upstream commit 151eb2b656e8ee2b4864653f3f785133f506c47e Author: Jose Garcia Date: Thu Aug 26 08:35:30 2021 -0400 fix linter commit 6ec289c408065830e4db8c9a860b3a5bd4efe0c1 Merge: 355d2819 b56f77b2 Author: Mislav Marohnić Date: Thu Aug 26 11:36:23 2021 +0200 Merge pull request #4203 from kidonng/patch-1 Use `/usr/bin/env bash` instead of `/bin/bash` in `gh extension create` commit b56f77b2e5a215aeabfa49a1490949849127e082 Author: Kid <44045911+kidonng@users.noreply.github.com> Date: Thu Aug 26 00:49:33 2021 +0800 Use `/usr/bin/env bash` instead of `/bin/bash` in `gh extension create` commit a48f1070c3a9c15cf43c1a60512e6787fd1b0806 Merge: a19997a3 30be4c98 Author: Gabriel Ramírez Date: Wed Aug 25 09:11:43 2021 -0500 Merge pull request #86 from github/auto-ssh-composition Piping support for ssh and code commit 11fbb60ae7464b516b7b022599f042427758c53b Author: Mislav Marohnić Date: Wed Aug 25 12:01:12 2021 +0200 Rename the module to "github.com/cli/cli/v2" commit 2ef6e95982342bcb0069c810889a853d8857f4a7 Author: Jose Garcia Date: Tue Aug 24 20:15:21 2021 -0400 show status under a flag commit 46ee45bcdd99c54849f76c4484950c7825a85616 Author: Jose Garcia Date: Tue Aug 24 17:46:24 2021 -0400 simplify the state iteration commit 355d28195ef1630fb97d2816a4e932fcce4b8f9f Merge: a94cadc7 6cf6bb4a Author: Mislav Marohnić Date: Tue Aug 24 20:48:46 2021 +0200 Merge pull request #4189 from cuonglm/issue-4188 Disable auth for cobra shell request completion hidden commands commit 6cf6bb4a44453f0f03bd987b73ea1c8e734a6a3f Author: Cuong Manh Le Date: Wed Aug 25 01:35:49 2021 +0700 Disable auth for cobra shell request completion hidden commands Those hidden command are used by the shell completion scripts, but they are not disabled auth check. Thus, the shell completion does not work even the completion setup was done properly. Fixes #4188 commit 30be4c98f95c0827b2060054271b8b8e736fce86 Author: Gabriel Ramirez Date: Tue Aug 24 13:12:18 2021 -0500 Send codespace name to Stdout to enable scripting commit a94cadc7021df3ce22403510c5b578c3bdfaf9d8 Merge: e2973453 4e219a9c Author: Mislav Marohnić Date: Tue Aug 24 18:59:30 2021 +0200 Merge pull request #4184 from cli/changelog-squashed-merges Improve changelog script so it includes squashed merges commit 4e219a9c8f4fb2b38c2b9ff001a39729b58d61f8 Author: Mislav Marohnić Date: Tue Aug 24 14:29:29 2021 +0200 Improve changelog script so it includes squashed merges commit e2973453b5cd77df1b246a6147bbed6b47e4ce1c Author: Heath Stewart Date: Mon Aug 23 12:00:25 2021 -0700 Add helper template functions for rendering tables (#3519) Co-authored-by: Mislav Marohnić commit a53ea0c655bbea14ab254f47e6cc27caf06d46a5 Merge: 88af63d3 0388d457 Author: Nate Smith Date: Mon Aug 23 13:51:03 2021 -0500 Merge pull request #4177 from cli/survey231 bump survey to 2.3.1 commit 88af63d36fbfb1338adbd08fe6ba1da122708a72 Author: Heath Stewart Date: Mon Aug 23 10:55:12 2021 -0700 Re-enable label colors for issue list (#4106) * Re-enable label colors for issue list * Drop parentheses wrapping issue labels * Support ANSI escape codes in TablePrinter cells * Switch to a Truncate implementation that correctly measures ANSI escape codes * Only output RGB color if terminal has truecolor capabilities * Enable `ENABLE_VIRTUAL_TERMINAL_PROCESSING` on Windows - fixes wrapping issues with full lines and allows truecolor rendering Co-authored-by: Mislav Marohnić commit 0388d457b09589af7d4215a013ed569a41abff83 Author: vilmibm Date: Mon Aug 23 12:53:11 2021 -0500 bump survey to 2.3.1 commit 25b150ad6ed20fdc30b48794b8cd84b80eaa0576 Merge: 5a328c38 5756e239 Author: Sam Date: Mon Aug 23 10:44:54 2021 -0700 Merge pull request #4159 from cli/ext-create Add extension create command commit 5756e2397ad99ac20abfe190e63c4652350627ad Author: Sam Coe Date: Mon Aug 23 10:36:29 2021 -0700 Extension template is executable according to git commit a19997a399a917f714ebe079ab20473bd0a41031 Merge: 9cb82007 ae88091f Author: Josh Gross Date: Mon Aug 23 13:14:00 2021 -0400 Merge pull request #83 from github/joshmgross/code-insiders Add support to `code` for VS Code Insiders commit 5a328c38db6763ca71e6ad09d11d2b9251b95f89 Merge: 351cd622 18ea94a2 Author: Nate Smith Date: Mon Aug 23 11:55:49 2021 -0500 Merge pull request #4172 from cli/accessible-survey Bump Survey for accessibility features commit 351cd622e7499ab94b57ec5dbbcd2143c3761192 Merge: af2aecd4 eb78ac0d Author: Mislav Marohnić Date: Mon Aug 23 18:41:40 2021 +0200 Merge pull request #4175 from cli/bump-go Require Go 1.16+ commit eb78ac0dcb73b2f5a1368d9c7e1372ff0e67e9ef Author: Mislav Marohnić Date: Mon Aug 23 18:08:09 2021 +0200 Require Go 1.16+ commit ae88091fd8276d57437aa7f44053f1462f3412e2 Author: Josh Gross Date: Mon Aug 23 12:01:13 2021 -0400 Replace options struct with variable commit c8963a6345c6269579581722ece081a3af6fd042 Author: Sam Coe Date: Mon Aug 23 09:01:05 2021 -0700 Address PR comments commit bb0107ad51c4a639500b391ae6ebe09d25e688cf Author: Sam Coe Date: Mon Aug 23 08:49:09 2021 -0700 Unhide extension commands commit af2aecd40bf680b45a0732252d7447ac08de54f7 Merge: 8129fb3d 0701f8aa Author: Mislav Marohnić Date: Mon Aug 23 16:46:14 2021 +0200 Merge pull request #4146 from cli/force-tty Add ability to force terminal-style output even when redirected commit 0701f8aa82778d336fd795fc3f5ee86c5cea9dfc Author: Mislav Marohnić Date: Mon Aug 23 16:09:08 2021 +0200 Add tests for ForceTerminal commit 8129fb3d3e6db34270be6014f9b540c0964f9f38 Merge: a508fee4 db74830e Author: Mislav Marohnić Date: Mon Aug 23 13:12:35 2021 +0200 Merge pull request #4171 from mahdyar/chore/make-gh-status-clickable chore: make gh status link clickable commit a508fee4e1ebea6a4ce1311903c0a1af80e2d3ce Merge: c9683422 51d60907 Author: Mislav Marohnić Date: Mon Aug 23 12:45:22 2021 +0200 Merge pull request #4158 from cli/extensions-ux Enable `help` for extensions, accept full extension names as argument commit c96834228eee1e231760ef38b1b2a1678afeccad Merge: 4a45ca6f 75dc5108 Author: Mislav Marohnić Date: Mon Aug 23 12:23:44 2021 +0200 Merge pull request #4169 from saintmalik/patch-1 fix typos in docs commit 18ea94a28e7db40ee8d64424391e5bf0e3327554 Author: Mislav Marohnić Date: Mon Aug 23 12:20:31 2021 +0200 Bump Survey for accessibility features https://github.com/AlecAivazis/survey/commit/a4e159a1a700e113e78760bc04f204cb477a1a82 commit 4a45ca6fa6c80b334d9df94734babd5f51536f37 Merge: c84bfa9e 3e23dcab Author: Mislav Marohnić Date: Mon Aug 23 12:03:34 2021 +0200 Merge pull request #4154 from cli/graphql-502-error Fix HTTP 502 error reporting from GraphQL request commit db74830e64efc3a09a57b80f20fe75eb032f0ee8 Author: Mahdyar Hasanpour Date: Sun Aug 22 18:01:55 2021 +0430 chore: make gh status clickable Adding https:// to githubstatus.com to make it clickable on terminal emulators commit 75dc51084b0fd569cc61a270ea6337a894b497a3 Author: SaintMalik <37118134+saintmalik@users.noreply.github.com> Date: Sun Aug 22 08:25:15 2021 +0100 fix typos in docs commit 7f0e09c13f95d663eca59fd488531481cbb90270 Author: Sam Coe Date: Thu Aug 19 16:06:35 2021 -0700 Move link to next line since it is long commit d22286a8e8a5f3d0ddb80fd8473815c652b034a0 Author: Sam Coe Date: Thu Aug 19 15:58:01 2021 -0700 Add examples to template commit 50a15cae8631005592bd414c71e282a715f7dcf8 Author: Sam Coe Date: Thu Aug 19 14:46:26 2021 -0700 Better error message commit 530c0244f9461874b206a4a1324759d8032ec745 Author: Josh Gross Date: Thu Aug 19 17:37:57 2021 -0400 Add support to `code` for VS Code Insiders commit 232ad2a67c81b31c101b2f558b313302182123d4 Author: Sam Coe Date: Thu Aug 19 13:41:26 2021 -0700 Fix up link commit e9f7459ce2b9086fdb90decbfc5e38f3ff1e0bac Author: Sam Coe Date: Thu Aug 19 12:08:14 2021 -0700 Add extension create command commit 51d609078b51b25ae041283c001818ffa7f4dc29 Author: Mislav Marohnić Date: Thu Aug 19 20:38:19 2021 +0200 Enable `gh help ` for extensions This sends the `--help` flag to the extension. The extension is reponsible for printing something useful as a result. commit 55f0dad3db30d4db42a0af565bb1b9e4836cda27 Merge: 76aca39f 9cb82007 Author: Jose Garcia Date: Thu Aug 19 18:30:10 2021 +0000 merge upstream commit 1881de6d41911f5b1ce8edbde8bf7bf8fc005930 Author: Mislav Marohnić Date: Thu Aug 19 20:01:18 2021 +0200 Allow fully qualified extension name as argument to `upgrade`, `remove` commit c84bfa9e66304fb37f6a7edaa6ddb9fa74ea72a8 Merge: d24e963f 6ebafb55 Author: Mislav Marohnić Date: Thu Aug 19 19:01:01 2021 +0200 Merge pull request #4156 from rneatherway/patch-1 Extend query suite with quality queries commit 6ebafb55ae2cb896ad78052bf499ee418616163b Author: Robin Neatherway Date: Thu Aug 19 15:05:28 2021 +0100 Extend query suite with quality queries commit d24e963f3423c2fe27d51c370b50c54ddc58192a Merge: 8a563599 0e51ec16 Author: Mislav Marohnić Date: Thu Aug 19 15:52:02 2021 +0200 Merge pull request #4155 from rneatherway/rneatherway/off-by-one Correct benign mistake in off-by-one guard commit 0e51ec1699f54fb18c3d156cb293d3fa720d305d Author: Robin Neatherway Date: Thu Aug 19 14:41:04 2021 +0100 Correct benign mistake in off-by-one guard m[2] is the third element of m, rather than the second, so we have to check instead that the len of m is at least 3. Because the regular expression has two capture groups, the length of m will always be 3, so currently the guard will always be true. commit 9cb8200732b2a9afe75135378a47149f5595fc3f Merge: 9697aa3a a53eb53a Author: Issy Long Date: Thu Aug 19 10:12:57 2021 +0100 Merge pull request #78 from github/fix-ports-usage-docs cmd/ghcs/ports: Fix usage docs for the new `source:forward` syntax commit a53eb53ad4c6850ce13234b123b122c16796e1c7 Author: Issy Long Date: Thu Aug 19 10:10:30 2021 +0100 cmd/ghcs/ports: Fix usage docs for the new `source:forward` syntax Co-authored-by: George Brocklehurst commit 3e23dcab1598176049a54c57ce85f5a770a1c685 Author: Mislav Marohnić Date: Wed Aug 18 22:17:32 2021 +0200 Fix HTTP 502 error reporting from GraphQL request Now it makes sure that the message portion will be printed to stderr when the user encounters the error. commit 9697aa3a788627514348eb4a311657836dc9d708 Merge: 00502e92 28a3644a Author: Issy Long Date: Wed Aug 18 18:33:57 2021 +0100 Merge pull request #77 from github/lowercase-deletions cmd/ghcs/delete: When matching repos to delete, standardize casing commit 28a3644a079169b78aa0a8149e9eed15ef98445d Author: Issy Long Date: Wed Aug 18 18:15:15 2021 +0100 cmd/ghcs/delete: I learnt about `strings.EqualFold` - thanks, linter! commit 5af1cccb73310136e0de4d1055ee55e9db29cb7a Author: Issy Long Date: Wed Aug 18 18:05:59 2021 +0100 cmd/ghcs/delete: When matching repos to delete, standardize casing - It was possible to delete Codespaces for repo `SomePerson/foo` but not `someperson/foo`, despite the fact that the GitHub APIs don't actually care about casing - `SomePerson` and `someperson` is the same account. - This fixes that by lowercasing both the user-provided repo name, and the repository that is attached to the Codespace for a match. - Fixes #76. commit 8a563599022e4e8fd911d38a9da5d35b56c466eb Merge: 45b358bc 810c4212 Author: Mislav Marohnić Date: Wed Aug 18 17:21:24 2021 +0200 Merge pull request #4144 from cli/install-docs Refresh Linux & BSD installation docs commit 269196c94f3ca7b2d8fb9efe001295bc161d6948 Author: Jose Garcia Date: Wed Aug 18 15:12:47 2021 +0000 support existing connections for port forwarding commit 45b358bcfc49a68a84f667122f06be06621fa450 Merge: 8fb6bb66 998a29d3 Author: Mislav Marohnić Date: Wed Aug 18 16:55:48 2021 +0200 Merge pull request #4136 from lepasq/filter-by-topic Add topic filter to repository listing commit 8fb6bb66c8aff4ef7ba78794a17aa5b085e807cf Merge: 2c02c281 b9438015 Author: Sam Date: Tue Aug 17 14:17:54 2021 -0700 Merge pull request #3992 from despreston/858-config-browser add browser option to config commit b9438015a214aff7f4ddc9f50c954f06bdfb3492 Author: Sam Coe Date: Tue Aug 17 14:10:15 2021 -0700 Add GH_BROWSER to help topic commit a07748f1f1ef6ba5cdab497d54e45c171d98dec6 Author: Sam Coe Date: Tue Aug 17 14:07:49 2021 -0700 Add support for GH_BROWSER env var commit 2c02c2819bc0e4351e0fac7d0bd8534487f41dc5 Merge: 05328fbe 315c6e4e Author: Sam Date: Tue Aug 17 13:56:40 2021 -0700 Merge pull request #4145 from cli/remove-homedir Remove backwards compatibility with homedir library for config files commit 321fd98f82aef661933324e65071b7c59cef422f Author: Mislav Marohnić Date: Tue Aug 17 20:06:41 2021 +0200 Add ability to force terminal-style output even when redirected commit 315c6e4eb7ec8c3db45b10e24ac37f3b1ea2743c Author: Sam Coe Date: Tue Aug 17 10:28:16 2021 -0700 Remove backwards compatibility with homedir library for config files commit 34b3d5bb8693964134fd8492181f8ae0a4d5c129 Author: Sam Coe Date: Tue Aug 17 10:05:54 2021 -0700 Add tests and a little polish commit 00502e926353a3742b60428bb50baea0c8bbb605 Merge: c1b54925 8533d084 Author: Jose Garcia Date: Tue Aug 17 10:02:48 2021 -0400 Merge pull request #66 from github/jg/multiple-ports Multiple ports support in port forwarding commit 8533d084614a373412d20013e3c7e0a7d77dd833 Author: Jose Garcia Date: Tue Aug 17 13:07:40 2021 +0000 rename var commit b5670252decdcc142d3efd928403c67c771d1c22 Author: Jose Garcia Date: Tue Aug 17 12:58:46 2021 +0000 small update to description commit 5fe84c61218234e79e664f2a73ba4506bb73f50b Merge: 619862a4 c1b54925 Author: Jose Garcia Date: Tue Aug 17 12:57:04 2021 +0000 merge upstream commit c1b549258f8ab2bacd6f4f1f77eb9ab0342945c4 Merge: 9b109688 517aae28 Author: Mislav Marohnić Date: Tue Aug 17 14:43:20 2021 +0200 Merge pull request #69 from github/docs Improve command docs commit 517aae2805f041d413bb5fd6417ec3d2d20e3f2b Merge: 5e472bc0 9b109688 Author: Mislav Marohnić Date: Tue Aug 17 14:42:09 2021 +0200 Merge remote-tracking branch 'origin' into docs commit 9b1096887848bc01a774a57052bef56a171f6108 Merge: 58096e68 5ad9736c Author: Mislav Marohnić Date: Tue Aug 17 14:33:08 2021 +0200 Merge pull request #70 from github/ci-lint Run tests on all branches, perform lint check in CI commit 58096e685271111159181b9d68e3ce5214f63849 Merge: 35e50cb6 2f1543a2 Author: Mislav Marohnić Date: Tue Aug 17 14:09:14 2021 +0200 Merge pull request #68 from github/dev Improve experience in development commit 5ad9736c4ee1cd86c45f09f22d7d7799dbdaadc2 Merge: 2f1543a2 35e50cb6 Author: Mislav Marohnić Date: Tue Aug 17 13:31:50 2021 +0200 Merge remote-tracking branch 'origin' into ci-lint commit 35e50cb67905354cd6059b480d074dd7c7834aec Merge: 2f1543a2 6dd0bf8e Author: Mislav Marohnić Date: Tue Aug 17 13:31:11 2021 +0200 Merge pull request #59 from github/output-formats Add machine-readable output formats commit 6dd0bf8e5ee66a573cd10b77f8d04be4c5bd2258 Merge: 22be2643 b4768616 Author: Mislav Marohnić Date: Tue Aug 17 13:07:14 2021 +0200 Merge pull request #67 from github/jg/output-formats add back . indicators & update ConnectToTunnel commit b47686163a2d06ce4e1b46d480f34133df611f2f Author: Mislav Marohnić Date: Tue Aug 17 13:04:55 2021 +0200 Fixes for log/output streams commit 810c42120aa4ffb5ebba439ee77e160f2bcb7cdb Author: Mislav Marohnić Date: Tue Aug 17 12:16:59 2021 +0200 Add installation note about Raspberry Pi OS commit 0366e047e21ba23efea67e7cf101231cb7cb58d8 Author: Mislav Marohnić Date: Tue Aug 17 11:47:37 2021 +0200 Cleanup Linux installation docs commit 1ec632b1b86c047638cfba474c0c35e6bcc9d81c Author: Mislav Marohnić Date: Tue Aug 17 11:47:21 2021 +0200 Add OpenBSD instructions commit ca60e30171e2ea142ac91aa8747bac083c1942a5 Author: Mislav Marohnić Date: Tue Aug 17 11:46:25 2021 +0200 Add warning about Snap commit 5e472bc0e5996f478d69388d2fc6cb24afff9c11 Author: Mislav Marohnić Date: Mon Aug 16 23:24:11 2021 +0200 Improve command descriptions and argument assertions commit 97d8285b5870d478a22bbbc949ff663e77043141 Author: Mislav Marohnić Date: Mon Aug 16 23:19:20 2021 +0200 Do not require GITHUB_TOKEN for merely viewing command help commit 22be26431e918fa10142ddb471ffaf151d9877a5 Author: Mislav Marohnić Date: Mon Aug 16 22:28:39 2021 +0200 Have `--codespace ` flag be consistent across commands commit c9c1ff8dacdee9fae5d386d8f084890b6c6147a8 Author: Jose Garcia Date: Mon Aug 16 20:16:50 2021 +0000 add back . indicators & update ConnectToTunnel commit 05328fbe13491b08d31f47e080073bfc6abf1400 Merge: ca40eeba e35d41ec Author: Mislav Marohnić Date: Mon Aug 16 19:28:22 2021 +0200 Merge pull request #4114 from cli/powershell-docs Add PowerShell instructions to completions help commit ca40eeba5faa6140e2fbbf391f82fe4f1eefecdb Merge: fbe1487d a7fc43bc Author: Mislav Marohnić Date: Mon Aug 16 19:00:30 2021 +0200 Merge pull request #4116 from cli/extensions-ui-tweaks Extensions UI tweaks commit 998a29d391d342dcff470442baf34a23f834af6c Author: lepasq Date: Sun Aug 15 14:31:24 2021 +0200 Update list_test.go to include topics as well commit 87e5e6f2e3be02d78610af8ead3a191fba8ee139 Author: lepasq Date: Sat Aug 14 19:12:59 2021 +0200 Add topic filter to repository listing commit 79111d85ac2a6483d805993fd4f30c6e16a69939 Merge: cd993992 eb2a1764 Author: Jose Garcia Date: Fri Aug 13 08:41:43 2021 -0400 Merge pull request #4 from github/jg/port-forwarding-errors-test-server Port forwarding improvements & slight refactor commit a7fc43bc5f711943c110d638e6523bb85cd9ab51 Author: Mislav Marohnić Date: Thu Aug 12 15:41:17 2021 +0200 Add hint about argument to `extensions remove` usage synopsis commit ac6c859ca0dff9411bbb7c0f17d2f4ca34753c4c Author: Mislav Marohnić Date: Thu Aug 12 15:40:46 2021 +0200 Print "Upgrade available" instead of "Update available" This is because we have an `upgrade` command, not `update` command. commit fbe1487dd06506cbde02558ba27ff09461ff1cc4 Merge: 0c99f7d8 bf9c49ec Author: Mislav Marohnić Date: Thu Aug 12 15:35:01 2021 +0200 Merge pull request #4112 from cli/extension-cmd-rename Rename `gh extensions` → `gh extension` commit 0c99f7d8d5b70e11b1bbe76f86cda3d1c1c0a5c1 Merge: e567ce00 3946606e Author: Mislav Marohnić Date: Thu Aug 12 15:33:56 2021 +0200 Merge pull request #4051 from cli/extensions-overhaul Rework local extensions for Windows commit e567ce00cd77c839c367b7ced310c7171d6b5d18 Merge: c39dd1e3 f4bded30 Author: Mislav Marohnić Date: Thu Aug 12 15:33:32 2021 +0200 Merge pull request #4102 from cli/sync-no-checkout Repo sync optimizations commit 20d75f0ff9198f952311a8aa21bf648df6147767 Author: Mislav Marohnić Date: Thu Aug 12 14:37:23 2021 +0200 Normalize logging, output, and error reporting - Return errors as errors, not print to stdout and return nil - Ensure errors and warnings are always written to stderr, not stout - Do not print progress to stdout unless stdout is a terminal commit 41e223869e0d8e0c96f79eb6801875b3b39f1109 Author: Mislav Marohnić Date: Thu Aug 12 14:37:06 2021 +0200 Fix mapping port numbers to labels commit db95f2f71f5c390dac86811f120a193074dffbb6 Author: Mislav Marohnić Date: Thu Aug 12 14:35:49 2021 +0200 Add machine-readable output functionality to `ports` command commit e35d41ec1f0ed7a5e5e89bb461ea5b8a6826efa3 Author: Mislav Marohnić Date: Thu Aug 12 12:56:46 2021 +0200 Add PowerShell instructions to completions help commit bf9c49eccd7e174264db1c243bf9e902f0fecd9b Author: Mislav Marohnić Date: Wed Aug 11 22:22:39 2021 +0200 Rename `gh extensions` → `gh extension` This is for compatibility with other core commands which are all singular. commit 3946606e5e20285ab93f83e711d26afdcb7e4bd6 Author: Mislav Marohnić Date: Wed Aug 11 22:11:24 2021 +0200 Use symlinks on most platforms and keep using plain files on Windows commit c39dd1e3eb588c1d6fd10933a62aed3b0f25b561 Merge: 4fa984a3 21521b06 Author: Mislav Marohnić Date: Wed Aug 11 19:27:37 2021 +0200 Merge pull request #4109 from ShaharyarAhmed-bot/dev Check path for git executable before auth commit 21521b06b9bbdc8e58fa5c7f5e6cfe4325d30e79 Author: Mislav Marohnić Date: Wed Aug 11 19:21:13 2021 +0200 Check git presence during `auth login` only if it's going to be needed commit e28236a447ab0aa89e520abcf65faaa19afb8002 Author: Shaharyar Ahmed Date: Wed Aug 11 15:40:15 2021 +0500 Check path for git executable before auth There was a bug where if git was not installed then gh would do its authentication and try to configure git but would then find out that the git executable was not in PATH. Now gh checks to see if the git executable is in PATH before authenticating the user. If the git executable is in PATH the authentication continues as normal, if it is not in PATH then it prints out an error to the console: $ git executable not found in $PATH Resolves: #3818 commit 7a9d1fc3314be4af108ef123e9bb15f57193990d Merge: 140a54a0 2f1543a2 Author: Mislav Marohnić Date: Wed Aug 11 12:13:09 2021 +0200 Merge remote-tracking branch 'origin/main' into output-formats commit 4fa984a33304f006aab08cfee218fc187acc6ba9 Merge: c5371d53 bdc5b55f Author: Mislav Marohnić Date: Tue Aug 10 15:54:03 2021 +0200 Merge pull request #4003 from despreston/3381-release-discussions add --discussion-category flag to release cmd commit bdc5b55f556c768068f146182a30b799b4f95a0a Author: Des Preston Date: Tue Aug 10 09:47:49 2021 -0400 pr comments Only add discussion category to request if there is one. This eliminates the need to update old tests. Renaming the variable to something shorter. commit f4bded30f85965689b4b8d98b21db6170e0d32dc Author: Mislav Marohnić Date: Tue Aug 10 14:30:55 2021 +0200 Mark test helper commit 6136a39ed63ae903ea17eb347a1bae5e0474054e Author: Mislav Marohnić Date: Tue Aug 10 14:30:36 2021 +0200 Use `remotes.FindByRepo()` commit 0f1ab13b9ef0c2c7ee4cd9b204f3af06278d9adf Author: Mislav Marohnić Date: Tue Aug 10 14:29:23 2021 +0200 Only check if working copy is dirty when syncing current branch In other cases, we don't have to abort the operation since it can proceed without being affected by the working copy at all. commit 66ad6ad7d06d517a1dd69daccdd3ad0ac56bfe87 Author: Mislav Marohnić Date: Mon Aug 9 22:10:52 2021 +0200 Avoid `git checkout` during `gh repo sync` - If the local branch already exists, use `git update-ref` - If it needs to be created, use `git branch `, but don't switch to the new branch Bonus fixes - Enables operation while on detached HEAD - Enables operation even when the current remote doesn't track all branches in the remote repo (uses FETCH_HEAD instead of the `/` syntax) commit eb2a17645056c6b34638cf32c9b0d39e068e1e69 Author: Jose Garcia Date: Sat Aug 7 17:54:43 2021 +0000 remove err print commit c5371d530346ff80a848f4acb119361a12c101dd Merge: ac13fc80 174e26ec Author: Sam Date: Thu Aug 5 19:39:40 2021 -0700 Merge pull request #3813 from cli/repo-sync Add repo sync command commit fbf0d286729dd355889a8caad0b80be71e4ae601 Author: Jose Garcia Date: Fri Aug 6 01:03:03 2021 +0000 port forwarding err handling and test refactors commit ac13fc807cdfdecef9cddf279a74eca60098c1e9 Merge: 95a515ec 9a485ddf Author: Mislav Marohnić Date: Thu Aug 5 21:02:27 2021 +0200 Merge pull request #4005 from despreston/835-rename-checkout add --branch flag to pr checkout commit 9a485ddfa278ee85e61ebb59f5c40a6c3d8fb888 Author: Mislav Marohnić Date: Thu Aug 5 20:42:52 2021 +0200 :nail_care: Cleanup local branch handling during `pr checkout` commit 294a029e70bcb9cc2a27eff2ce089c160b6ac42e Author: Des Preston Date: Thu Jul 15 13:13:01 2021 -0400 add --branch flag to pr checkout Allows renaming the checked out branch. commit 619862a46bb99cebf4d7698dca741ffb2c596d4b Author: Jose Garcia Date: Thu Aug 5 15:21:26 2021 +0000 initial spike for multiple port support commit 2f1543a2d770895dc08c549f38167875ef4d2b69 Merge: 7b079d98 4362b0b2 Author: Issy Long Date: Thu Aug 5 15:12:09 2021 +0100 Merge pull request #62 from github/interactive-menu-for-deleting-a-codespace cmd/ghcs/delete: Display the interactive menu when there are no args commit 4362b0b241201ed90678b44ac8da6ff075784642 Author: Issy Long Date: Wed Aug 4 17:39:51 2021 +0100 cmd/ghcs/delete: Display the interactive menu when there are no args - Currently the flow to delete a single Codespace is `gh cs list`, copy and paste the Codespace name onto the end of `gh cs delete`. - This improves consistency with other commands by letting the user choose which Codespace they want to delete, interactively. A Codespace name on the command-line still works too. commit 7b079d9855f1237e8a4625627adb6731091128a4 Merge: ebc8ce5a 70f4a7b4 Author: Jose Garcia Date: Thu Aug 5 08:09:48 2021 -0400 Merge pull request #56 from github/jg/logs-cmd ghcs logs commit 1f2ab7fbe48c65b195d087abcf696c88e2f45243 Author: bchadwic Date: Thu Aug 5 02:13:55 2021 -0700 pr and run check symbols revision commit 174e26ecac2aea6f83fddfea29acd331d6d50cd4 Author: Sam Coe Date: Wed Aug 4 18:26:30 2021 -0700 Fix tests commit 2c4a6626607dbf67948d86ff6ad9c6b11aa50099 Author: Sam Coe Date: Wed Aug 4 18:02:07 2021 -0700 Rework git client interface commit 00d67e3e5ab4628ce3c0cc5efb3eb84b14a475c9 Author: Sam Coe Date: Wed Aug 4 17:10:50 2021 -0700 Remove unnecessary + commit 86f16dbaf54057c910bfa042aba07222f50aa147 Author: Sam Coe Date: Wed Aug 4 17:00:20 2021 -0700 Use more idiomatic pattern commit c0756c2d1c918b47a9ae780a6bf0d18d6488733c Author: Sam Coe Date: Wed Aug 4 16:35:27 2021 -0700 Clean up UX commit 59930186790108c5d0c6029c691a32f4a5023e10 Author: bchadwic Date: Wed Aug 4 15:20:45 2021 -0700 NEW functionality: current folder '.', from current folder '.(pathsep)', parent folder '..(path sep)', absolute 'folder | filename' commit 7ef919d713fe26f43cf879c1fe6e5c983a32bc9a Author: bchadwic Date: Wed Aug 4 15:03:30 2021 -0700 NEW functionality: current folder '.', from current folder '.(path sep)', parent folder '..(path sep)', absolute 'folder | filename' commit 76aca39f5bc56e01fc07628903e940d75ae5064d Author: Jose Garcia Date: Wed Aug 4 17:35:11 2021 +0000 Create status support commit 140a54a009b27c41fe7d602b5fbada129fcacd4d Author: Mislav Marohnić Date: Wed Aug 4 15:56:10 2021 +0200 Add machine-readable output formats - Default table output (when stdout is attached to a terminal) stays the same; - When stdout is redirected, output tab-separated values and no header line; - With `--json` flag, output structured JSON data. Example: $ ghcs list --json [ { "Branch": "main", "Created At": "2021-06-10T15:04:46+02:00", "Name": "mislav-playground-jvqj", "Repository": "mislav/playground", "State": "Shutdown" }, { "Branch": "master", "Created At": "2021-07-15T15:51:08+02:00", "Name": "mislav-github-github-pwgg365xv", "Repository": "github/github", "State": "Shutdown" } ] commit 95a515ecf0d9cd006b525074633dce7af3bdfb76 Merge: 1007c1a3 90b78861 Author: Mislav Marohnić Date: Wed Aug 4 15:43:00 2021 +0200 Merge pull request #4087 from cli/graphql-error-fix Fix unmarshalling GraphQL error type commit 90b7886142886acfb1a8bc7f7c5f934e5d7ec973 Author: Mislav Marohnić Date: Wed Aug 4 15:34:53 2021 +0200 Fix unmarshalling GraphQL error type The "path" field of a GraphQL error object contains a mix of strings and numbers and cannot be deserialized into `[]string`. Fortunately, we don't need to rely on the "path" field and instead have the final error message be constructed by aggregating human-readable "message" fields. commit 1007c1a3ae38908979963a64512a1bd51b2fda33 Merge: fddca218 930ee60a Author: Mislav Marohnić Date: Wed Aug 4 15:23:36 2021 +0200 Merge pull request #4079 from cli/no-label-colors Disable colorizing labels in `issue list` output commit 70f4a7b4b5dabb2133a172123c687bd2708a4ed5 Author: Jose Garcia Date: Wed Aug 4 13:19:00 2021 +0000 Re-introduce secrets export commit fddca218159a2ba4a791853562149565b96884ba Merge: fbdebe8e 549caf29 Author: Mislav Marohnić Date: Wed Aug 4 15:10:15 2021 +0200 Merge pull request #4085 from marckhouzam/feat/compPowershell Fixes #4084: Enable completion descriptions for powershell commit 549caf29b5e87e1019747b5415eb377d1749bcd3 Author: Marc Khouzam Date: Wed Aug 4 07:45:30 2021 -0400 Enable completion descriptions for powershell Signed-off-by: Marc Khouzam commit 65d11247996d60ade6865831365933dbeecd7f33 Author: Mislav Marohnić Date: Tue Aug 3 16:46:04 2021 +0200 Close file after resolving faux symlinks on Windows https://github.com/cli/cli/pull/4051/checks?check_run_id=3186063173 commit 930ee60ac5895e4eb4e19a22bf34148b76a2d431 Author: Mislav Marohnić Date: Tue Aug 3 16:02:16 2021 +0200 Disable colorizing labels in `issue list` output - Labels with dark color are not visible on a dark background - "Raw" `issue view` output should never output color, not even with CLICOLOR_FORCE=1 commit fbdebe8e4e6b59f8f1cbf96499a328f163bc35b3 Merge: 5a46c1ca 5d1d967c Author: Mislav Marohnić Date: Tue Aug 3 15:56:25 2021 +0200 Merge pull request #4071 from rsteube/gh-merge-admin pr merge: added `--admin` flag commit 5d1d967c439cc1a8ffa6bbb6e0b1e3349ea646a9 Author: Mislav Marohnić Date: Tue Aug 3 15:49:55 2021 +0200 :nail_care: Clean up pr merge admin logic commit d5003334e36b94d5b81f27ac890ce6ebe4f06bf7 Author: Jose Garcia Date: Tue Aug 3 13:43:09 2021 +0000 Remove secrets export commit e57b390d4a75e4b97e304b65a122f5874b1e14c7 Author: Jose Garcia Date: Tue Aug 3 13:42:34 2021 +0000 dotfiles status spike commit baa18c164de838c9f6ffe9f04363fd5608e6b229 Author: rsteube Date: Mon Aug 2 12:10:13 2021 +0200 pr merge: added `--admin` flag commit be794f1579e8839a75c8cd3149bd28bede0ce63e Author: Jose Garcia Date: Thu Jul 29 17:09:50 2021 +0000 creation log support for cat and tail commit 58a055609dea29874e5a4e1ba00a56897a1599a2 Author: Jose Garcia Date: Thu Jul 29 10:57:51 2021 -0400 logs cmd spike and refactor of ssh tunnel methods commit 1efc07b183b59621ef3ac676986e37444671ff61 Author: Ben Chadwick Date: Wed Jul 28 22:09:37 2021 -0700 made tests non os dependant commit bbd74f004f5519985018481eea28ed966bac4185 Author: Mislav Marohnić Date: Wed Jul 28 23:00:34 2021 +0200 Go 1.14 compat commit 0d999ddaa184e2d0f6026148fc4977bf8a52f452 Author: Mislav Marohnić Date: Wed Jul 28 22:47:54 2021 +0200 Rework local extensions for Windows Replace the implementation that relied on symlinks with the one that create regular files that act like symlinks: they contain a reference to the local directory where to find the extension. commit 5a46c1cab601a3394caa8de85adb14f909b811e9 Author: Mislav Marohnić Date: Wed Jul 28 21:07:29 2021 +0200 Merge pull request #4043 from cli/upgrade-goreleaser This reverts commit 85d0447. commit 4b499be96bc3564fa270bfef3b7b3b341868c5df Merge: ef9b7812 d6b70bee Author: Mislav Marohnić Date: Wed Jul 28 17:27:56 2021 +0200 Merge pull request #3942 from dscho/complete--repo-flag Allow auto-completing the `--repo` values commit d6b70beeaa92110054e5a27d0a4a2554e83916eb Author: Mislav Marohnić Date: Wed Jul 28 17:18:56 2021 +0200 List repos from non-default hostnames in completions for `-R` commit ebc8ce5adb6202094624f9369ed388270001a60f Merge: 77d0bc19 9544f8ac Author: Jose Garcia Date: Wed Jul 28 10:13:06 2021 -0400 Merge pull request #53 from github/jg/perf-improvements Liveshare client upgrade to v0.6.0 commit cd99399290bf1f12e3cca17d3e778f302da60a08 Merge: 4fc27b3e ae29c3c1 Author: Jose Garcia Date: Wed Jul 28 09:56:07 2021 -0400 Merge pull request #3 from github/jg/ignore-eof-terminal Ignore EOF on terminal close commit ae29c3c1ea7358504650e0c6fd07dc4a57cbb2a0 Author: Jose Garcia Date: Wed Jul 28 13:55:33 2021 +0000 Ignore EOF on terminal close commit 4fc27b3ed9e37fcfcc2fb1688c59ce475af9ee04 Merge: 39fe550a 3a2ade23 Author: Jose Garcia Date: Wed Jul 28 09:52:58 2021 -0400 Merge pull request #2 from github/jg/connection-test Connection test commit 3a2ade23a4a154eb327c097214784aa95bd138c9 Author: Jose Garcia Date: Wed Jul 28 13:52:30 2021 +0000 Connection test commit b43f78bc196f1fd6a9e4a1ba0e63a9d3ad861740 Author: Johannes Schindelin Date: Fri Jul 2 23:06:50 2021 +0200 completions: auto-complete `--repo` values Looking at the locally-registered remotes, we have a pretty good idea what `--repo` values are available. Let's complete them. Helped by Nate Smith and Mislav Marohnić. Signed-off-by: Johannes Schindelin commit 9544f8acc9a3204756e56b4382eedc34ff0a132d Author: Jose Garcia Date: Wed Jul 28 09:05:58 2021 -0400 Commit vendors commit cba40ad72aa8ba432a2aeb9115de9b1c70170324 Author: Jose Garcia Date: Wed Jul 28 08:33:06 2021 -0400 liveshare client upgrade commit 39fe550aeba71f1cb5fe08f82363fcdbe1eb3636 Merge: 71a55b21 0ab67bad Author: Jose Garcia Date: Tue Jul 27 19:23:37 2021 -0400 Merge pull request #1 from github/jg/refactor Refactors most of the library to solidify some of the implementation with tests commit 0ab67badfad20a67a73bb170647fa115538b2995 Author: Jose Garcia Date: Tue Jul 27 23:19:55 2021 +0000 Final changes to finish this refactor commit ef9b78128395bd93f89db7bda939efd250ebd756 Merge: 92ed42c5 515902ad Author: Sam Date: Tue Jul 27 08:18:37 2021 -0700 Merge pull request #4047 from tniessen/opensuse-typo Fix typo in openSUSE installation instructions commit 92ed42c54a43783f058fd6fb3410cb100108e8d5 Merge: f3a7d007 340a1fdc Author: Sam Date: Tue Jul 27 08:07:14 2021 -0700 Merge pull request #4029 from cli/extensions-upgrade-force Add --force flag for extensions upgrade commit f3a7d0076e2ecada71b68ad6927e37fdc24d0187 Merge: fdad37e2 28012066 Author: Mislav Marohnić Date: Tue Jul 27 16:34:20 2021 +0200 Merge pull request #4028 from cli/bump-cobra Upgrade Cobra for improved shell completion support commit fdad37e24889802cdb9fd65e0903b5f572efbe52 Merge: fa354a92 82c6fb7d Author: Mislav Marohnić Date: Tue Jul 27 15:29:14 2021 +0200 Merge pull request #4019 from cli/enterprise-env Fix error message when using GH_ENTERPRISE_TOKEN but host is ambiguous commit 77d0bc1901b863f5c71a087f79433e7c535ae54c Merge: 529e3bf5 bba5ebec Author: Jose Garcia Date: Tue Jul 27 08:03:27 2021 -0400 Merge pull request #43 from github/gh-token Add note about gh extension commit 529e3bf5e36c69b907f8548737b9b1bc51c2b1fb Merge: 967ef483 bba5ebec Author: Jose Garcia Date: Tue Jul 27 08:03:17 2021 -0400 Merge pull request #42 from github/rm-dotcom-note Remove note about creating a custom SSH setup for dotcom commit c8ee9829a76ae7a076ecc4d98aa5a3ebd81529d2 Author: Ben Chadwick Date: Mon Jul 26 21:55:47 2021 -0700 Revert "fixing mistake" This reverts commit 5e3ca02198010c9cd4bd5db52abb9e4d56cd3ca0. commit 515902ade30f95448dbcda3cc0b9eb2dea13d5ee Author: Tobias Nießen Date: Tue Jul 27 02:13:35 2021 +0200 Fix typo in openSUSE installation instructions commit 967ef4830652b886388fd5478c7f977889214166 Merge: bba5ebec 3931c16b Author: Mislav Marohnić Date: Mon Jul 26 19:25:06 2021 +0200 Merge pull request #41 from github/auto-version Provide version number at build time commit 82c6fb7d1a3387ebd8f8dbc730b01a9199499731 Author: Mislav Marohnić Date: Mon Jul 26 18:59:53 2021 +0200 Add a note about the dummy GHE hostname commit 3931c16bd765a301e71f7918bd14427a519a0e2f Author: Mislav Marohnić Date: Mon Jul 26 17:07:42 2021 +0200 Provide version number at build time commit bba5ebec8b21a76b0934b696e80321e7ab0c97a6 Merge: d688dc76 c092a293 Author: Issy Long Date: Mon Jul 26 15:44:11 2021 +0100 Merge pull request #39 from github/ssh-codespace-param cmd/ghcs/ssh: Add `-c` parameter for specifying a Codespace to SSH to commit 892f73221c69d21f77d53e77eddd16f40c20c4ba Author: Jose Garcia Date: Mon Jul 26 14:39:52 2021 +0000 Update shared visibility finalized tests commit 98282ba4b51085e03965672b1b124cc708bc6e82 Author: Jose Garcia Date: Mon Jul 26 14:31:00 2021 +0000 Update shared visibility tests commit c092a293501cd757b99e2a89c8b8548310fa440a Author: Issy Long Date: Mon Jul 26 13:34:11 2021 +0100 cmd/ghcs/ssh: Add `-c` parameter for specifying a Codespace to SSH to - This adds a `-c`, `--codespace` parameter to `ghcs ssh` to allow for non-interactively specifying a Codespace to SSH into, for instance if a user has recently done `ghcs list` and already knows which Codespace they want to access. Without a value for the `-c` parameter, the interactive prompt appears as usual. commit 5e3ca02198010c9cd4bd5db52abb9e4d56cd3ca0 Author: bchadwic Date: Mon Jul 26 00:37:14 2021 -0700 fixing mistake commit aac4c59c319597d188fb06cbca42e43411568f8f Author: bchadwic Date: Mon Jul 26 00:22:25 2021 -0700 fixing operating system dependant regex, and tests commit 8469441464ff31263af9dcc90f317c2bba813189 Author: bchadwic Date: Sun Jul 25 23:53:27 2021 -0700 new functionality: current folder './', parent folder '../', absolute 'filename' commit 91114d35c3d04245a58f78ebf2feb6bb5edde4e2 Author: Jose Garcia Date: Sat Jul 24 03:44:20 2021 +0000 More tests commit fcfb10cb56e6aaaae9c745bc1ee00f48897220f9 Author: Jose Garcia Date: Fri Jul 23 20:24:50 2021 +0000 Working test for Client.Join commit 9132a28e9cf2a09359a80e104a558c77a0c0abea Author: Jose Garcia Date: Fri Jul 23 19:15:54 2021 +0000 Checking point after continuing to flesh out mock server commit b9cd9af7fa83ad2fd7cca4727d5adc1be51fa384 Author: Jose Garcia Date: Fri Jul 23 01:17:32 2021 +0000 Start of tests and comments commit d688dc76ecf026348cfe49e38c90aede5eaa927e Merge: f3518648 7e49db3b Author: CamiloGarciaLaRotta Date: Thu Jul 22 10:35:23 2021 -0400 Merge pull request #36 from github/config/v0.7.1 config: bump to 0.7.1 commit 7e49db3be3129761aaddeb9acdb038797335b303 Author: CamiloGarciaLaRotta Date: Thu Jul 22 10:33:00 2021 -0400 config: bump to 0.7.1 Hoping to prove that Goreleaser & Homebrew run automatically commit f35186485cdaf313a39b3504d95425fe4d8122a5 Merge: ba373119 14468bab Author: CamiloGarciaLaRotta Date: Thu Jul 22 10:18:03 2021 -0400 Merge pull request #31 from github/feat/promptless-create feat: introduce repo, branch and machine flags for ghcs create commit 14468baba6d84fca7546e44e8d7633f0968c8aa5 Author: Camilo Garcia La Rotta Date: Thu Jul 22 10:13:20 2021 -0400 config: bump to v0.7.0 commit ba37311927a2276885a6f3a1433b79a0b96db357 Merge: 8f73b189 2f467e3d Author: CamiloGarciaLaRotta Date: Thu Jul 22 10:10:58 2021 -0400 Merge pull request #30 from github/feat/richer-delete feat: delete all & delete per repo commit 89cf916e23bc67482e29fcd00c6176e3141bb07b Merge: 3ef0226e 8f73b189 Author: CamiloGarciaLaRotta Date: Thu Jul 22 10:09:21 2021 -0400 Merge branch 'main' into feat/promptless-create commit 3ef0226e20e64088c4755b7b91a0d02a5fccf697 Author: Camilo Garcia La Rotta Date: Thu Jul 22 10:07:09 2021 -0400 fix: output available machine names on --machine error commit 2f467e3ddc173c926d97653199ec044ead76edac Merge: 5ca2fa55 8f73b189 Author: CamiloGarciaLaRotta Date: Thu Jul 22 09:55:20 2021 -0400 Merge branch 'main' into feat/richer-delete commit 8f73b1891116ffe1f5099ac143f1dc6148831c4b Merge: 35f458b8 69865fa7 Author: Issy Long Date: Thu Jul 22 14:19:07 2021 +0100 Merge pull request #33 from github/improve-descriptions cmd/ghcs/*.go: Better short descriptions of what commands do commit 69865fa7623bc0a857dc8a1f4140d3c4567785a7 Author: Issy Long Date: Thu Jul 22 14:08:20 2021 +0100 cmd/ghcs/main: Better description of `ghcs` as a whole Co-authored-by: Camilo Garcia La Rotta commit b66d65379faba6635690cd9313fd43b60a69a59b Author: Issy Long Date: Thu Jul 22 11:07:23 2021 +0100 cmd/ghcs/*.go: Better short descriptions of what commands do - I ran `--help` on `ghcs code` and saw `ghcs code` and that was it, which was surprising. I expected a description. - Here's a fix for all of the commands thus far to give them longer descriptions. - I've only done "short" descriptions in Cobra terms, and removed the "long" descriptions as they seemed like they needed to be unnecessarily verbose. Before: ``` ❯ ghcs --help Codespaces Usage: ghcs [command] Available Commands: code code create Create delete delete help Help about any command list list ports ports ssh ssh Flags: -h, --help help for ghcs -v, --version version for ghcs Use "ghcs [command] --help" for more information about a command. ❯ ghcs ssh --help ssh Usage: ghcs ssh [flags] Flags: -h, --help help for ssh --profile string SSH Profile --server-port int SSH Server Port ``` After: ``` ❯ ./ghcs --help Codespaces Usage: ghcs [command] Available Commands: code Open a GitHub Codespace in VSCode. create Create a GitHub Codespace. delete Delete a GitHub Codespace. help Help about any command list List GitHub Codespaces you have on your account. ports Forward ports from a GitHub Codespace. ssh SSH into a GitHub Codespace, for use with running tests/editing in vim, etc. Flags: -h, --help help for ghcs -v, --version version for ghcs Use "ghcs [command] --help" for more information about a command. ❯ ./ghcs ssh --help SSH into a GitHub Codespace, for use with running tests/editing in vim, etc. Usage: ghcs ssh [flags] Flags: -h, --help help for ssh --profile string SSH Profile --server-port int SSH Server Port ``` commit a99d0f5495a575c0694307dbbe606210b16830d7 Author: Jose Garcia Date: Thu Jul 22 01:07:06 2021 +0000 Better naming for rpc client and ssh session commit fddcd876b0b6e50959b0530662c50dda1b0079c1 Author: Jose Garcia Date: Thu Jul 22 01:02:03 2021 +0000 Some more cleanup to the port forwarder and connection commit a68cda14698887808309dda7eaa0f11ed7c51829 Author: Camilo Garcia La Rotta Date: Wed Jul 21 20:54:18 2021 -0400 refactor: break down Create() into smaller funcs commit 7332aa428c4db7b87c4280063b89a4d10763cb3c Author: Jose Garcia Date: Thu Jul 22 00:45:45 2021 +0000 Large refactor and solidifying of APIs before tests commit 3db217fef063ce2bc877d6d11f184ca5b4bc696e Author: Camilo Garcia La Rotta Date: Wed Jul 21 20:27:22 2021 -0400 feat: make sku survey optional commit aab98ccc18fcaa74c7c59bdb52b6b4a11fa3b7e2 Author: Camilo Garcia La Rotta Date: Wed Jul 21 20:06:05 2021 -0400 feat: break out repo and branch surveys commit c751e88120baa4f25ec978a092e308d915f95cc3 Author: Camilo Garcia La Rotta Date: Wed Jul 21 19:56:08 2021 -0400 feat: introduce repo, branch and machine flags for ghcs create commit 71a55b2126bbd4339ab3de8c9f9d549d96f4ec10 Merge: 53fd96d2 6d5726d7 Author: Jose Garcia Date: Wed Jul 21 18:54:13 2021 -0400 Merge branch 'main' of github.com:github/go-liveshare commit 5ca2fa556270763ee08e2c054b0bd80a8204ea1e Author: Camilo Garcia La Rotta Date: Wed Jul 21 18:13:36 2021 -0400 feat: ghcs delete repo REPO_NAME commit 7a0a8fa39c517647de4a4a499a757b70d16a5321 Author: Camilo Garcia La Rotta Date: Wed Jul 21 18:02:50 2021 -0400 feat: ghcs delete all commit 0d6926e14bd4248186656b981bc280a20b9ce7bd Author: Camilo Garcia La Rotta Date: Wed Jul 21 17:41:50 2021 -0400 doc: root cmd description commit 340a1fdc93b38208e12db3f1be3ada347a665bee Author: Sam Coe Date: Wed Jul 21 10:47:39 2021 -0700 Add --force flag for extensions upgrade commit 35f458b853e401a80ddf604af6e050e03f807cf8 Merge: 345e3e1b 532ee681 Author: Jose Garcia Date: Wed Jul 21 14:06:18 2021 -0400 Merge pull request #28 from github/jg/fix-ssh Fix ssh command order commit 532ee681657c0c662c2743c69bc5659774434c5f Author: Jose Garcia Date: Wed Jul 21 14:04:42 2021 -0400 Fix ssh command order commit 345e3e1b8af2c51c74fc8129a971495b797f025f Author: Jose Garcia Date: Wed Jul 21 13:50:19 2021 -0400 Update main.go commit f7c80c99e7286e99b393bb0c8439cbc3f2aa44ba Merge: b44355d8 c2b136a8 Author: Jose Garcia Date: Wed Jul 21 13:48:10 2021 -0400 Merge pull request #27 from github/jg/code-command ghcs code command support commit b44355d80dbecbe6fbc8d16b9fb0e41a7cc4ca00 Merge: aa0ef37f 3e50fff2 Author: Jose Garcia Date: Wed Jul 21 13:30:16 2021 -0400 Merge pull request #26 from github/jg/secrets-and-server-port Don't overwrite .zshenv, support server port for SSH, forward X11 commit c2b136a84f4054c79923c9c06ae9a832577d9432 Author: Jose Garcia Date: Wed Jul 21 13:28:47 2021 -0400 ghcs code command support commit 28012066638de94c5390172577caa01b6bb390dc Author: Mislav Marohnić Date: Wed Jul 21 16:32:39 2021 +0200 Switch to Cobra's bash completion V2 commit 3e50fff2c9d60d898104c5cd791a06c42199c110 Author: Jose Garcia Date: Wed Jul 21 10:22:33 2021 -0400 X11 support commit 6ed2e8f7f891713d848a3b16c7e4df38758f3d42 Author: Mislav Marohnić Date: Wed Jul 21 16:11:24 2021 +0200 Add completion support to `repo create --gitignore` and `--license` commit 4d7625c8a0516652f27f3465a696df30c931d3ef Author: Mislav Marohnić Date: Wed Jul 21 15:34:13 2021 +0200 Allow shell-completing multiple `--json` fields separated by commas commit db8204dc56499829629aa286251ed7c40c688652 Author: Mislav Marohnić Date: Wed Jul 21 15:27:03 2021 +0200 Allow space to be added after completing `issue/pr list --state` values The "nospace" directive instructs the shell completion logic to avoid adding a space after completing the word. However, this feature was broken in an older Cobra, and users still saw a space character added. In most case we want the space because we anticipate that the user might want to add extra arguments to the command. commit 3e8f075a8e4e9c29e40ae1a3ea8da41a74a2a2ec Author: Mislav Marohnić Date: Wed Jul 21 15:01:31 2021 +0200 Bump Cobra for improved completion support commit aa0ef37ffcc7ce941ffff45cbfc00e662cd0fea1 Merge: 8faee1e5 7a3e47ff Author: Kristján Oddsson Date: Wed Jul 21 13:01:07 2021 +0100 Merge pull request #25 from github/fix-error-message-wording-and-link Update error message link and wording. commit 7a3e47ff3ef548a887997d21f7867af23de10575 Author: Kristján Oddsson Date: Wed Jul 21 12:46:44 2021 +0100 Update error message link and wording. commit 285f8659b3e5ba990f5d5133afed255b670ad656 Author: bchadwic Date: Wed Jul 21 01:11:38 2021 -0700 clean up commit 8962aeebf963a09beca9fedb32de36ca09091305 Author: bchadwic Date: Wed Jul 21 01:08:15 2021 -0700 changed functionality to open up last commit with -c / --commit for gh browse commit e81bee6886ba0998d2faa5b2f47fda6c1eee9f28 Author: Jose Garcia Date: Tue Jul 20 18:43:43 2021 -0400 Doesn't overwrite .zshenv and supports server-port commit fa354a922b638863cb55ab310596414139c69a83 Merge: 3ff94ae7 e70bdbf7 Author: Sam Date: Tue Jul 20 13:21:30 2021 -0700 Merge pull request #3905 from cli/extensions-list-notice Add update checking to extensions list commit e70bdbf7a9b104388ce645b422c1351ffe3e4940 Author: Sam Coe Date: Thu Jun 24 12:04:54 2021 -0700 Add update checking to extensions list commit 3ff94ae76b7695235f0365eccb27ce58f44f84b3 Merge: 5bbce4a5 6c8abe9d Author: Mislav Marohnić Date: Tue Jul 20 22:02:49 2021 +0200 Merge pull request #4025 from cli/revert-3926-update-goreleaser-20210630 Revert "Update GoReleaser" commit 6c8abe9df68c21e8cc5541dbbb703e1a4d580100 Merge: 85d0447a 5bbce4a5 Author: Mislav Marohnić Date: Tue Jul 20 21:43:51 2021 +0200 Merge branch 'trunk' into revert-3926-update-goreleaser-20210630 commit 85d0447a6ea2263b0f2babbd44c1be1ccf3104d4 Author: Mislav Marohnić Date: Tue Jul 20 21:39:50 2021 +0200 Revert "Update GoReleaser to `v0.172.1`" commit 5bbce4a5b97830ade16d902b2e9300d6697b5480 Author: Mislav Marohnić Date: Tue Jul 20 20:15:48 2021 +0200 Fix goreleaser config for linux packages commit ccd4f0fa9a77a30e8a2e547b83728a902d9f39a1 Merge: 1ca49a8b a83c2924 Author: Mislav Marohnić Date: Tue Jul 20 19:55:43 2021 +0200 Merge pull request #3706 from cristiand391/improve-automerge PR auto-merge improvements commit a83c2924c53d75083115407a574a349c0bd8cb04 Author: Mislav Marohnić Date: Tue Jul 20 19:49:19 2021 +0200 Never prompt to delete branch if `--auto` was given commit 6e026412dfb0e719584d106bfad0cb1df547cb20 Author: Mislav Marohnić Date: Tue Jul 20 19:42:53 2021 +0200 Add "UNSTABLE" to immediately mergeable statuses This status describes a state where the head branch is mergeable and technically not blocked per base branch requirements, but it does have non-passing checks. commit 6f2dfd7eea1e4cfc004417a778ce2058c814d22a Author: Mislav Marohnić Date: Tue Jul 20 19:34:32 2021 +0200 Adjust conditions for switching between regular and auto merge Conditions prohibiting a regular merge: BLOCKED, BEHIND, DIRTY. Conditions triggering a regular merge even if `--auto` was set: CLEAN, HAS_HOOKS. Note that UNKNOWN status does not trigger either of the conditions. commit 0ab9c70c3f6b77c96da8cf7ea1ace34a35f2e928 Merge: bec0a102 1ca49a8b Author: Mislav Marohnić Date: Tue Jul 20 18:45:47 2021 +0200 Merge remote-tracking branch 'origin' into improve-automerge commit 1ca49a8bcd40f7f69d1d3eedd6a78f81a37c8289 Merge: 4a7b1305 2624ed9d Author: Mislav Marohnić Date: Tue Jul 20 18:34:14 2021 +0200 Merge pull request #3980 from cli/bump-survey Bump Survey library for cursor improvements commit 2624ed9d8c795fda2fcee38c54dac9a2e8b75bef Merge: 079542d3 4a7b1305 Author: Mislav Marohnić Date: Tue Jul 20 18:28:58 2021 +0200 Merge remote-tracking branch 'origin' into bump-survey commit 4a7b13051118c82fd456aa128886584cfcae67f0 Merge: 25ef1119 4dc23d86 Author: Mislav Marohnić Date: Tue Jul 20 18:27:07 2021 +0200 Merge pull request #3981 from cli/bump-gojq Bump gojq to latest version commit 25ef11198f30f2570208c27a21c65f77a12f4527 Merge: 1121ec66 9033258f Author: Mislav Marohnić Date: Tue Jul 20 18:02:23 2021 +0200 Merge pull request #4020 from cli/xdg-docs Clean up GH_CONFIG_DIR docs commit 1121ec6669f80740aea318ecd5e1c9e44b50c4db Merge: 75c7fc15 efa4d43c Author: Mislav Marohnić Date: Tue Jul 20 18:01:36 2021 +0200 Merge pull request #4013 from chemotaxis/docs/alias-quotes-windows Add documentation about double quoting on Windows commit 75c7fc1536864a5b2a38393aefd7f0aa7e309cea Merge: 25b6eecc 1de756f6 Author: Mislav Marohnić Date: Tue Jul 20 16:39:36 2021 +0200 Merge pull request #3972 from g14a/fix/private-repo-create fix private repo creation in case of ignore templates & repo description bugs in case of template repos commit 1de756f6f3857da1506e3497106a21a3ff411cce Author: Mislav Marohnić Date: Tue Jul 20 16:34:11 2021 +0200 :nail_care: address review comments commit c598a1edc2f1f8efff270acdca03d2ea9ff96e6c Author: Mislav Marohnić Date: Tue Jul 20 15:47:15 2021 +0200 Fix detecting cases when `cfg.Hosts()` is empty commit efa4d43cf4c800fb4646046f8410837505c5119e Author: Mislav Marohnić Date: Tue Jul 20 15:32:51 2021 +0200 Simplify `alias set` documentation commit 9033258f5f6b33f58a8a902d877b11298fe53cdc Author: Mislav Marohnić Date: Tue Jul 20 14:24:11 2021 +0200 Clean up GH_CONFIG_DIR docs This removes the false equivalence between GH_CONFIG_DIR and XDG_CONFIG_HOME. These settings do not have the same effect and should not be used for the same purposes. Also remove the documentation about what `XDG_*` settings do. We simply conform to the XDG Base Directory Specification, but will not document it. It's likely that users of these environment variables already know what they do. commit aec0f10041469f18af6c478b41a6b798552e49fd Author: Mislav Marohnić Date: Tue Jul 20 14:11:07 2021 +0200 Fix error message when using GH_ENTERPRISE_TOKEN but host is ambiguous Before: $ GH_ENTERPRISE_TOKEN="..." gh pr create could not find hosts config: not found Now: $ GH_ENTERPRISE_TOKEN="..." gh pr create set the GH_HOST environment variable to specify which GitHub host to use Also amends `gh help environment` documentation to suggest the use of GH_HOST when scripting operations with GitHub Enterprise repositories. commit 8faee1e5a951c41a790253b5f60ba09f4b7ab8ab Author: Jose Garcia Date: Tue Jul 20 08:09:48 2021 -0400 Update main.go commit 5e803aca7922788b76f0ed400505c101bb524ecc Merge: 2f8d00e0 6642fb52 Author: Jose Garcia Date: Tue Jul 20 08:08:33 2021 -0400 Merge pull request #20 from github/jg/better-conn-handling Bump go-liveshare w/ better connection handling and simpler ssh setup commit 6642fb520a9fd43a928cf9fcfae0de8e306a00ec Author: Jose Garcia Date: Tue Jul 20 08:04:34 2021 -0400 Better connection handling and simpler ssh setup commit 2f8d00e0f843006207bf2f648a6184a982ffed7e Merge: f7e32485 4582fed1 Author: Issy Long Date: Tue Jul 20 13:02:41 2021 +0100 Merge pull request #19 from github/version-cmd cmd/ghcs/main: Add `--version` flag commit f7e32485fcc207368b2c358108faadb221e263a9 Merge: 570a407b cb29b11a Author: Issy Long Date: Tue Jul 20 13:02:35 2021 +0100 Merge pull request #18 from github/gracefully-fail-if-token-envvar-unset cmd/ghcs/main: Fail gracefully if `GITHUB_TOKEN` entirely unset commit 6d5726d78a665643f89514fc678d9ab1ccb1a138 Author: Jose Garcia Date: Tue Jul 20 11:59:14 2021 +0000 Better way to discard requests & close channel/conn on disconnects commit 25b6eecc8dd7845ca42afa3362b80b13c355356a Merge: 97a52f74 1c9b4bf9 Author: Mislav Marohnić Date: Tue Jul 20 13:33:16 2021 +0200 Merge pull request #4017 from despreston/des/avoid-migrate Skip auto migrate of config when GH_CONFIG_DIR commit 1c9b4bf99dba2153217b6c192a699e292de4d135 Author: Des Preston Date: Mon Jul 19 16:33:51 2021 -0400 Skip auto migrate of config when GH_CONFIG_DIR If GH_CONFIG_DIR is set, don't auto migrate the config file. This fixes the situation where the path given via GH_CONFIG_DIR does not exist and the cli attempts to migrate an existing config to that location. Fixes #3837 commit 1e4e536bcbdeab4c733db2462c6840ddc06e88c9 Author: chemotaxis Date: Mon Jul 19 15:44:37 2021 -0400 Revise Windows note commit 2c52819c1a158f494f2e258e159a56d82ceab85c Author: chemotaxis Date: Mon Jul 19 15:28:25 2021 -0400 Remove note about using Git for Windows As discussed in pull request #4013. commit 97a52f74cdd18740baa55f5419579950213042fb Merge: 496b70ac b71735d0 Author: Sam Date: Mon Jul 19 10:55:16 2021 -0700 Merge pull request #3933 from cli/extensions-upgrade Skip trying to upgrade local extensions commit b71735d0d773585a6451dd2c3a9705234c22edb4 Author: Sam Coe Date: Mon Jul 19 10:48:09 2021 -0700 Address PR comments commit 4582fed1ccef6bdaa40f17c3a2985f92bedb90b3 Author: Issy Long Date: Mon Jul 19 18:44:02 2021 +0100 cmd/ghcs/main: Add `--version` flag - This is built into Cobra the argument parser. Now `ghcs --version` exists. - When we prepare to bump the version, we need to remember to update this value else the Homebrew formula, GitHub releases and the `ghcs --version` output will be mismatched. - Fixes https://github.com/github/ghcs/issues/16. commit cb29b11ab207d1bb2ef6479d77c9991501e5a675 Author: Issy Long Date: Mon Jul 19 18:10:15 2021 +0100 cmd/ghcs/main: Fail gracefully if `GITHUB_TOKEN` entirely unset - I have my GitHub API token in my environment as `HOMEBREW_GITHUB_API_TOKEN`, so with things that need `GITHUB_TOKEN` I have to remember to `export GITHUB_TOKEN=$HOMEBREW_GITHUB_API_TOKEN`. - I didn't for this tool, and got this unfriendly error message: ``` ❯ ghcs list Error: error getting user: Bad credentials Usage: ghcs list [flags] Flags: -h, --help help for list error getting user: Bad credentials ``` - This moves the "do you have a `GITHUB_TOKEN`" question to the very beginning (no guarantees about org SSO access, just a string that exists), erroring out with a nice message if users don't have that envvar set: ``` issyl0 in cetus in ~/repos/github/ghcs/cmd/ghcs on gracefully-fail-if-token-envvar-unset ❯ ./ghcs list The GITHUB_TOKEN environment variable is required. Create a Personal Access Token with org SSO access at https://github.com/settings/tokens/new. issyl0 in cetus in ~/repos/github/ghcs/cmd/ghcs on gracefully-fail-if-token-envvar-unset ❯ export GITHUB_TOKEN=$HOMEBREW_GITHUB_API_TOKEN ❯ ./ghcs list +--------------------------------+--------------------+------------------------------------+----------+---------------------------+ | NAME | REPOSITORY | BRANCH | STATE | CREATED AT | +--------------------------------+--------------------+------------------------------------+----------+---------------------------+ | issyl0-github-cat-ggrpj5fvwvr | github/cat | dependabot/bundler/graphql-1.12.13 | Shutdown | 2021-07-13T12:36:53+01:00 | +--------------------------------+--------------------+------------------------------------+----------+---------------------------+ ``` commit 570a407bace2ff3ace0d8b5fb57c9e56c2a7fb03 Author: Jose Garcia Date: Mon Jul 19 08:00:51 2021 -0400 Fix directive commit 23ffca45f7dd7042239e7b89f5907a5f1883324d Author: chemotaxis Date: Mon Jul 19 00:46:06 2021 -0400 Unify use of single quotes to mark shell arguments and variables The first paragraph uses single quotes when referring to shell arguments and variables, but the rest of the docs use double quotes. This commit switches to using single quotes throughout the docs. I prefer to use single quotes inside string literals because Go uses double quotes to define a string literal. commit 5314e7c39804cbc0462cff96ee293d55f6a8e7c6 Author: chemotaxis Date: Mon Jul 19 00:36:07 2021 -0400 Add note about double quotes on non-Unix-like shells On non-Unix-like shells like Windows Command Prompt, single quotes are handled differently. You need to define aliases using double quotes instead of single quotes. I added an inline example to illustrate the quotes. The example is formatted as inline code blocks in Markdown. Unfortunately, because Go uses backticks for raw string literals, I needed to do some rather ugly string concatenation in order to get the backticks included in the doc string. This also rearranges the notes so that the platform specific notes are at the end of the documentation. commit 798413848b0c8b211caf1fd96fa3bc5b74baef29 Author: Jose Garcia Date: Sat Jul 17 20:32:47 2021 -0400 Portfowarding private/public/forward now supported commit e373c91f8b2121a25eacf2f70f040dab14bff730 Author: Jose Garcia Date: Sun Jul 18 00:05:13 2021 +0000 UpdateSharedServerVisibility API for Server commit 3c42ab8f7a3eb5068bd0bb5edbdb76cbc10664b3 Author: Jose Garcia Date: Fri Jul 16 18:45:38 2021 -0400 ghcs ports v1 commit 98bcdd16cfccafd7ef601067287012d8010a150f Author: Jose Garcia Date: Fri Jul 16 22:34:51 2021 +0000 Support for GetSharedServers commit 496b70ac0eac78ac69c8c6eb582e0f8bc13e9c1e Merge: 30beb67c 882f6d33 Author: Mislav Marohnić Date: Fri Jul 16 15:18:38 2021 +0200 Merge pull request #3934 from cli/extensions-remove-notice Add confirmation to extensions remove commit 882f6d33cbb3caa4149aa64766231d25be7dad4e Merge: d68df4a9 30beb67c Author: Mislav Marohnić Date: Fri Jul 16 15:12:56 2021 +0200 Merge remote-tracking branch 'origin' into extensions-remove-notice commit d68df4a9d8b7aa986aafddb4a59775d3f56394b7 Author: Mislav Marohnić Date: Fri Jul 16 15:10:36 2021 +0200 Do not output error messages for nontty commit 30beb67cb3c24707466a49403ba27299077471ec Merge: 4d986724 d6b0749e Author: Mislav Marohnić Date: Fri Jul 16 14:58:52 2021 +0200 Merge pull request #3941 from cli/extension-install-check Extension install check commit d6b0749ea2f55ab1e00883ae98275d6316194894 Author: Mislav Marohnić Date: Fri Jul 16 14:52:05 2021 +0200 Tweak error messages and add more tests for extension install name check commit b3a24d273b5d095e49f17062371a527bd990fd33 Author: bchadwic Date: Fri Jul 16 00:07:04 2021 -0700 cleaned up git.go, browse_test.go, and browse.go commit 25a35a6e88c17f8a2a13c5c08ad7b478448ba255 Author: bchadwic Date: Thu Jul 15 23:38:54 2021 -0700 added relative path access in gh browse commit 4d9867243cb7800654ac3ad49b1a4fb32f681bac Merge: 6d0fb9b4 b514d22f Author: Nate Smith Date: Thu Jul 15 11:27:10 2021 -0500 Merge pull request #3911 from cli/fix-extensions-panic Fix issue in FindEntry that causes extensions and alias crash commit 349d3f382ef72494f4d08ce1b129f98b3b75a324 Merge: ecea5b82 44698ea1 Author: Jose Garcia Date: Thu Jul 15 12:00:45 2021 -0400 Merge pull request #2 from github/mislav/timeout Increase ssh command timeout and improve error message commit 44698ea1de38b8bc1bdfa6fc0afaca9c74c87219 Merge: d506a974 ecea5b82 Author: Jose Garcia Date: Thu Jul 15 11:54:52 2021 -0400 Merge branch 'main' into mislav/timeout commit 6d0fb9b47318a0dac690c83ac1f05c0869051416 Merge: aed8966f ab675a33 Author: Sam Date: Thu Jul 15 08:12:16 2021 -0700 Merge pull request #3926 from chemotaxis/update-goreleaser-20210630 Update GoReleaser to `v0.172.1` commit ecea5b821aceec24133ca88c8b2bf1e68a124841 Author: Jose Garcia Date: Thu Jul 15 14:35:26 2021 +0000 Give more time to start commit b28b4a13b7bac636be30759a7e75a63eb9ca216d Merge: e108b5d1 d46420e8 Author: Jose Garcia Date: Thu Jul 15 10:32:32 2021 -0400 Merge pull request #1 from github/mislav/ssh-tweaks Improve ssh command commit d506a97419e4f1e2e3f35746bb0426d42fd598de Author: Mislav Marohnić Date: Thu Jul 15 16:10:03 2021 +0200 Increase ssh command timeout and improve error message - My `github/github` codespace failed to start within 10s - Output more precise error message commit d46420e812aa34e21d667508a32378c9f3e18c90 Author: Mislav Marohnić Date: Thu Jul 15 16:07:23 2021 +0200 Improve ssh command - Ensure parent process exits when `ssh` sub-process is done - Enable connections to `github/github` when `--profile` flag wasn't given commit 45a4257612fe600456d1ad1a5efec64a490a8d4e Author: Des Preston Date: Thu Jul 15 10:07:21 2021 -0400 add --discussion-category flag to release cmd Flag for signaling that a discussion should be created with the given category for the release. Discussions are not supported for draft releases. If a discussion category is given for a draft, an err will be shown. Closes #3381 commit e108b5d18ffd13d9fd70de2b218c2ed65f58b4ba Merge: a5f558bf 4a0eaa3d Author: Jose Garcia Date: Thu Jul 15 08:49:28 2021 -0400 Merge branch 'main' of github.com:github/ghcs commit a5f558bf2a577bc276d2eff0b139098ee909511b Author: Jose Garcia Date: Thu Jul 15 08:49:18 2021 -0400 Makes secrets work commit 5c21c949a0d9d602cd4891cc8312ac1c9d2015db Merge: 0ad153f6 aed8966f Author: Mislav Marohnić Date: Thu Jul 15 12:58:00 2021 +0200 Merge remote-tracking branch 'origin' into fix/private-repo-create commit 0ad153f6969c66bd4572c67aa1722da006de644d Author: Mislav Marohnić Date: Mon Jul 12 16:49:55 2021 +0200 Separate payload structs for REST vs GraphQL repo create This enforces strict separation between serialization structs used for repository creation payload with respect to whether GraphQL or REST was used. Before, a field added to a GraphQL payload would leak to REST payload (and vice versa). commit 53fd96d22ec4443ecd98b8b645f7ea0eee6e6d31 Author: Jose Garcia Date: Wed Jul 14 20:47:06 2021 -0400 Some polish and module replacement commit 4a0eaa3da503e5117045ab4ea39ebaafae522c0b Author: Jose Garcia Date: Wed Jul 14 16:12:30 2021 -0400 Latest and greatest commit aed8966f75f77b903426a1bc757af5d35c3661e7 Merge: 161de77f 17b58bf0 Author: Mislav Marohnić Date: Wed Jul 14 17:26:15 2021 +0200 Merge pull request #3995 from despreston/3989-fix-pr-create-confirm fix repo create --confirm commit 17b58bf0b2006d671cdcc5555bd91c731f54322c Author: Des Preston Date: Wed Jul 14 09:57:59 2021 -0400 fix repo create --confirm Respect the --confirm flag when deciding whether to prompt for gitignore and license creation during `repo create` Fixes #3989 commit 158a15160df77eb1419ea0dc29ecbc7cbbe14fb2 Author: bchadwic Date: Wed Jul 14 01:19:55 2021 -0700 Changed name from SHA to Commit commit c95f30af800ecae9be99e1348044e277c33e0e37 Author: Des Preston Date: Tue Jul 13 15:06:40 2021 -0400 add browser option to config Allows setting the path to the browser using the config. Closes #858 commit e600ce054ab8ac61094b7b83a91fa75299f1914e Author: Gowtham Munukutla Date: Tue Jul 13 09:38:34 2021 +0530 fix description related bugs in creating a template repo commit 0a9fcb9332e0650725f5347630999cd780a66868 Merge: a1f26057 161de77f Author: Gowtham Munukutla Date: Tue Jul 13 09:30:48 2021 +0530 Merge branch 'trunk' of https://github.com/cli/cli into fix/private-repo-create commit 0e18db2b114122abdc942d51d6a499fa69031beb Merge: 0a496317 161de77f Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Mon Jul 12 14:26:10 2021 -0700 Merge branch 'cli:trunk' into browse-commit commit 161de77fd707a0b0314e96ac6687319bd001b7af Merge: 6c7bff25 98d3b7cc Author: Nate Smith Date: Mon Jul 12 13:39:30 2021 -0500 Merge pull request #3943 from silby/browse-output Add a no-browser mode to gh browse commit 98d3b7cc795f622832b7305e571d8a4fe6bac65b Author: nate smith Date: Mon Jul 12 13:05:49 2021 -0500 don't check Fprintf error we don't ever check the return of Fprintf anywhere else in the codebase so doing it here suggests that it's a special case. if it's something we should be doing we can circle back and do it more consistently. commit 6c7bff252403d45989e2ad3721d3d4a78a537925 Merge: cd3df4cd 47314a6b Author: Nate Smith Date: Mon Jul 12 12:46:01 2021 -0500 Merge pull request #3912 from bchadwic/trunk Added colors to issue / pr labels in the terminal commit cd3df4cdf96662381f9a46e82f1b412ff17b2b56 Merge: a6710ec5 13037226 Author: Mislav Marohnić Date: Mon Jul 12 17:05:41 2021 +0200 Merge pull request #3982 from despreston/des/err-return Return SilentError if completed run failed commit 13037226c232659db87aa44c0bf99ad3a5b769a4 Author: Mislav Marohnić Date: Mon Jul 12 16:58:45 2021 +0200 Add test for `gh run watch --exit-status` with completed runs commit 4dc23d869e0499ff569a3d76cbb268636e606975 Author: Mislav Marohnić Date: Mon Jul 12 15:34:08 2021 +0200 Bump gojq to latest version Upgrades gojq, the library that powers the `--jq` filter flag for JSON. This upgrade is purely housekeeping and not to address any particular issue reported with gh. https://github.com/itchyny/gojq/releases/tag/v0.12.4 commit 079542d35c60d5f17eeeab40767a6c73ced4cdf0 Author: Mislav Marohnić Date: Mon Jul 12 14:40:44 2021 +0200 Bump Survey library for cursor improvements https://github.com/AlecAivazis/survey/releases/tag/v2.2.14 commit a1f26057de5446cc4197d10d85e18824c581e7ba Author: Gowtham Munukutla Date: Sat Jul 10 23:04:46 2021 +0530 gofmt commit 52550f0bee1b3cb8765550d3e7323f311ce61ffa Author: Gowtham Munukutla Date: Sat Jul 10 23:03:16 2021 +0530 fix private repo creation in case of ignore templates commit 1980cc83b9dc93dae11d653fadff73944a0f79e5 Author: Des Preston Date: Fri Jul 9 11:54:58 2021 -0400 return SilentError if completed run failed If `gh run watch ${ID} --exit-status` is run and "ID" is the ID of a completed job that failed, return a SilentError. This ensures that the program returns a non-zero code. Fixes #3962 commit 04a6383ccb778d0683bf63a113ac79b74d6d2b2b Author: Jose Garcia Date: Wed Jul 7 08:00:01 2021 -0400 Tidy up go.mod commit 6cd0aa7a90c9737a8c9c0f113a301ebaf1264e5e Author: Jose Garcia Date: Tue Jul 6 09:12:43 2021 -0400 Working albeit not imperfect implementation commit 0a496317a51480146e414cf98a345a52f08e986e Author: bchadwic Date: Mon Jul 5 14:57:44 2021 -0700 added in the ability to view repository by commit hashes commit bec0a102c53595ddd04e865fa1ff1b875cdd604f Author: Cristian Dominguez Date: Mon Jul 5 12:47:48 2021 -0300 Fix tests commit bcab8c07660ff090aa427b55d6242440990790ab Author: Cristian Dominguez Date: Sun Jul 4 21:38:17 2021 -0300 Only merge PRs if branch checks have passed commit 47314a6bbcaf99439301564f07aeab196ddf22c8 Author: bchadwic Date: Sat Jul 3 17:09:25 2021 -0700 modified HexToRGB to check whether terminal and gh have color enabled, as well as created tests for HexToRGB commit 49ff0c65308e8a1911bb4e6df80c16b78e09c226 Author: Evan Silberman Date: Fri Jul 2 17:12:16 2021 -0700 Add a no-browser mode to gh browse For when you just want the destination URL on stdout. commit 16d218508d8afb7035fcd0978f8c6b34d3437a68 Author: Sam Coe Date: Fri Jul 2 13:53:26 2021 -0700 Fix tests commit 0482e5cd9b6c30bdf3f6e7cfe365dcb8233383ad Author: Sam Coe Date: Thu Jul 1 13:51:13 2021 -0700 Disallow installing extensions with the same name commit ab675a33f319df327e6bbfc0b9603e003ce83cff Author: chemotaxis Date: Thu Jul 1 18:41:13 2021 -0400 Upgrade GoReleaser Now that the config file is updated, upgrade from v0.169.0 to v0.172.1. commit 103e18cab548c7a9116a0bfe139f1285f0edfa49 Author: Sam Coe Date: Thu Jul 1 13:45:29 2021 -0700 Disallow installing extensions with same name as gh command commit bc7eaf8004c542a8740f5949334e2d06b107054c Author: Sam Coe Date: Thu Jul 1 11:19:25 2021 -0700 Add IsLocal helper func commit 86f2df1f495bad229144a947878194f381fb316d Author: Sam Coe Date: Thu Jul 1 10:43:41 2021 -0700 Add confirmation to extensions remove commit d19c348d7af23528f5d2f7cc13746e8d5c61e233 Author: Sam Coe Date: Thu Jul 1 10:14:29 2021 -0700 Rework code for go 1.15 commit 63f7372b31a97cb4855de32b8c381f77866bc5bc Author: Sam Coe Date: Thu Jul 1 09:55:13 2021 -0700 Fix lint commit e9a8b7f78125b3aebb927a404924f20cbdf5da7b Author: Sam Coe Date: Wed Jun 30 12:36:52 2021 -0700 Skip trying to upgrade local extensions commit f82750dfe62ef83cf7b868f14467bc9c3c7ec613 Author: chemotaxis Date: Thu Jul 1 03:04:02 2021 -0400 Update `goreleaser` config `nfpms.files` is deprecated: ```shell goreleaser version 0.172.1 commit: 32a44ab928879bb32c1e266b80de32e07d5d6721 ``` Before this commit, `goreleaser check` prints this: ```shell $goreleaser check • loading config file file=.goreleaser.yml ⨯ command failed error=yaml: unmarshal errors: line 67: field files not found in type config.NFPM ``` commit a6710ec5064995aadd18da5808c8c4ff41a8199c Merge: 717c91c9 589b695b Author: Nate Smith Date: Wed Jun 30 16:49:34 2021 -0700 Merge pull request #3924 from cli/rest-org-repo-bug fix repo create in org with license/ignore commit 717c91c912db396dae855560ca564cef3e16718a Merge: 3cc4c40d e5b099b1 Author: Nate Smith Date: Wed Jun 30 15:46:33 2021 -0700 Merge pull request #3922 from cli/fix-branch-protection Fix bug where branchProtectionRule doesn't exist in enterprise 2.22 commit 589b695bcf1a5232ffeafec7d18e760db82e66b0 Author: vilmibm Date: Wed Jun 30 17:41:39 2021 -0500 test for org + license/ignore commit 202168ee8dbbba01b0a904de3cf58fb64e282a72 Author: vilmibm Date: Wed Jun 30 17:22:07 2021 -0500 add nebula preview commit 2723a01760e0aa98ba5daaa86b05ca38943f73cb Author: vilmibm Date: Wed Jun 30 17:21:58 2021 -0500 fix repo generation in org with license/ignore commit e5b099b1dd78bcaeafecc15bf792ea0ec81b65e8 Author: Sam Coe Date: Wed Jun 30 09:47:30 2021 -0700 Fix bug where branchProtectionRule doesn't exist in enterprise 2.22 commit af2499cb69d721c6a4418642216aed9669b5a6ed Author: bchadwic Date: Tue Jun 29 22:32:07 2021 -0700 renamed func RGB to HexToRGB commit 4c412bc88ccd2067e1f6ae50fcc28fc6e4e63791 Author: bchadwic Date: Tue Jun 29 22:26:41 2021 -0700 Added in label rgb functionality for both prs and issues commit a1e1842e6df0c9f82a8feaeab122af994b8bcdf6 Author: Sam Coe Date: Tue Jun 29 20:48:30 2021 -0700 Catch a couple more edge cases commit b514d22f1ec142cf0d2964733e3abb57eb29d551 Author: Sam Coe Date: Tue Jun 29 13:59:51 2021 -0700 Fix issue in FindEntry that causes extensions and alias crash commit 3cc4c40dcb3c3fcc5f0f8e89fed9a7db307b3561 Author: vilmibm Date: Tue Jun 29 13:52:10 2021 -0500 pin goreleaser version commit 554250bc4edeac5fd6e7014165afc0187aa96dce Merge: 0474ba68 666ed2f3 Author: Nate Smith Date: Tue Jun 29 09:46:33 2021 -0700 Merge pull request #3779 from jgold-stripe/unix Add ability to dial API via unix socket commit 0474ba686d85fe789a634db77383815d3b653499 Merge: 33c3fb5c 93118c65 Author: Nate Smith Date: Tue Jun 29 09:17:30 2021 -0700 Merge pull request #3773 from bchadwic/first-browse-pull Feature/Create browse command commit 33c3fb5cdd110f08791bf6afbdad522c414e7a54 Merge: 7fc0acd8 6c984f45 Author: Sam Date: Mon Jun 28 17:12:29 2021 -0700 Merge pull request #3870 from cli/extensions-revisited Improvements to gh extensions commit 0b80c30789840c136f8fd56423522cfea658b92a Author: Sam Coe Date: Mon Jun 28 17:00:06 2021 -0700 Fix remote resolving for source repo commit 7fc0acd8a505b4160c823d49f9a958f233eb4cc8 Merge: c33b7d0c 5c7da584 Author: Nate Smith Date: Mon Jun 28 14:57:31 2021 -0700 Merge pull request #3746 from g14a/feature/repo-with-gitignore-license Feature/create repo with gitignore license commit 6c984f4512baad1182c0343d6f2225eb0828fa84 Author: nate smith Date: Mon Jun 28 14:36:51 2021 -0500 remove dead code commit c33b7d0c22572d519ac5663ffecafdb915d51758 Merge: c3e6fcca 568f4e4e Author: Nate Smith Date: Mon Jun 28 11:16:30 2021 -0700 Merge pull request #3807 from camillesf/nonempty_fork_org repo fork: check that --org is not the empty string commit 0477084a30cfd6056d7c746f6e6a5782292c72e8 Author: Sam Coe Date: Mon Jun 28 10:57:01 2021 -0700 remove now unused color code commit 666ed2f3d916a384a164e2f015002280f89648a7 Author: jonathan gold Date: Thu Jun 3 11:16:32 2021 -0700 Apply value of `http_unix_socket` if present in config commit 5f162561acee875df15be376c2db9e85a3a0ccbf Author: jonathan gold Date: Thu Jun 3 11:32:59 2021 -0700 Add config handling for `http_unix_socket` commit fb54cae00e54384f915178ef1e0e5c571659b6b2 Author: jonathan gold Date: Thu Jun 3 11:05:13 2021 -0700 Add package `httpunix` commit fc3dec4a5828456115a94df836be8b2dbc99a7e1 Author: jonathan gold Date: Thu Jun 3 10:56:31 2021 -0700 Change signature of `NewHTTPClient` to accomodate errors commit c3e6fccabe699aaac46bafcc2317d36a0a727969 Merge: 640a089e e8040537 Author: Sam Date: Fri Jun 25 09:32:11 2021 -0700 Merge pull request #3890 from Yuuki77/fix-artifact-download Fix `gh run download fails on large artifacts due to uint32 limitation` commit e80405377789c2570b2b2c9e78f78e250c96fdd6 Author: Yuki Osaki Date: Fri Jun 25 14:32:45 2021 +0900 change unit 32 to unit 64 commit 897ab1598b3d2cd9dd08c3310f0938f5c2ce4264 Author: Jose Garcia Date: Thu Jun 24 20:45:03 2021 -0400 RPC functionality started take two commit a8b1b87f7b33ab3d37fc3aed276d356d61631367 Author: Jose Garcia Date: Thu Jun 24 20:44:16 2021 -0400 Start of RPC implementation, need to figure out format commit 5481b2b5e616e2ed7534afab46a7ce118bbec297 Author: Sam Coe Date: Thu Jun 24 09:08:36 2021 -0700 Move dirty repo check before any network calls commit 8eba57a9ed6ccf69c4944939cd587ff3e1403e70 Author: Jose Garcia Date: Wed Jun 23 20:00:24 2021 -0400 initial commit commit 821971055169b78ca59b80b22e79766d014fe28b Author: Sam Coe Date: Wed Jun 23 13:45:15 2021 -0700 Address PR comments commit 0e838052a446b344fd1b0018771f8185e9003621 Author: Sam Coe Date: Tue Jun 22 14:52:26 2021 -0700 Rewrite tests with new mocks commit 5c7da584e553b00855bbc9018c04fe7747b451e2 Author: Gowtham Munukutla Date: Wed Jun 23 10:21:28 2021 +0530 clone remote repo after creating with gitignore and license commit 9ecbdb26c562166747665c475d692d90c73a58ca Merge: a44a3c8f 640a089e Author: Gowtham Munukutla Date: Wed Jun 23 09:28:20 2021 +0530 Merge branch 'trunk' of https://github.com/cli/cli into feature/repo-with-gitignore-license commit 640a089e55f28af5386bfb020ce6fcd77fb300ef Merge: 654336fe b0f58d0c Author: Nate Smith Date: Tue Jun 22 14:24:06 2021 -0700 Merge pull request #3850 from chemotaxis/fix-actions-help Print help even if logged out commit 654336fe641a23dfb6794f7a491f52655cbedb99 Merge: 68f2e77c 22235c2f Author: Nate Smith Date: Tue Jun 22 14:20:02 2021 -0700 Merge pull request #3871 from jonlorusso/trunk Update documentation for gist create command with default of secret commit 4ed2bfc4a0cf053da385d1dcdb3279ea965bf526 Author: Sam Coe Date: Tue Jun 22 13:55:30 2021 -0700 Revert "Add counterfeiter" This reverts commit 096f30a3199f1a7cb5227b6abc66c926b06b510c. commit 68f2e77c9f00c4b977b959fadd4392ff2b927a2f Merge: 936d6e1a 1302b71f Author: Sam Date: Tue Jun 22 11:49:01 2021 -0700 Merge pull request #3877 from autopp/complete-state-flag Add shell completion for the `--state` flag commit 1302b71fa71800cfba2d11da944bbf3d5af87031 Author: Sam Coe Date: Tue Jun 22 11:41:22 2021 -0700 linter commit e0468dbb294f6a2d71f42acaf13d565b324f3e25 Author: Sam Coe Date: Tue Jun 22 11:39:47 2021 -0700 whitespace commit 665f552def1a8d3d622aaf82d69dfc70f1a45290 Author: Sam Coe Date: Tue Jun 22 11:37:56 2021 -0700 Small reordering commit 22235c2f974b5a0843aba28f65aa4614e8a43741 Author: Jon Lorusso Date: Mon Jun 21 11:25:49 2021 -0400 Update documentation for gist create command to reflect default of secret. commit 42efc3f25a9fee1fc11089f9c1f18701a0ebc2be Author: Mislav Marohnić Date: Mon Jun 21 17:22:17 2021 +0200 Fix test cleanup on Windows commit f99191ea6f87e56559af9e02bf548509058058f6 Author: Mislav Marohnić Date: Mon Jun 21 16:22:10 2021 +0200 Enable setting an alias for an extension command commit 1ec47d8191ca37eede24fe34e588823c8a0e965f Author: Mislav Marohnić Date: Mon Jun 21 16:06:03 2021 +0200 Improvements to gh extensions - Extensions on Windows now enabled through the `sh.exe` interpreter - `sh.exe` now found on Windows when git was installed via scoop - `gh extensions list` command shows origin repo for the extension - `gh extensions upgrade --all` is required to upgrade all extensions - Added `gh extensions remove` - Shell completions now include aliases and extension names - `gh` help output now lists available extension names - Extensions are stored to XDG_DATA_HOME commit 0179651dc3ee8f3c1bf80251141633a570364cae Author: autopp Date: Sun Jun 20 20:03:21 2021 +0900 Add shell completion for the `--state` flag commit 6115868343c74e6ccc4431e96f542f4804a812d6 Author: Cristian Dominguez Date: Sat Jun 19 11:46:05 2021 -0300 Suggest to enable auto-merge when PR merge state is BLOCKED commit 498f15653edcc1a059145bcf61299f8fdc6a4de0 Author: Cristian Dominguez Date: Sat Jun 19 01:40:28 2021 -0300 Simplify auto-merge detection commit b0f58d0cf05ecc9255568eb7550d6fd88c047f92 Author: chemotaxis Date: Fri Jun 18 23:17:41 2021 -0400 Disable authentication check, but keep runnable In this branch, we originally avoided the authentication check by getting rid of the run method attached to the command. Instead of that, this commit makes the `gh actions` command runnable again, but the authentication is disabled with `cmdutil.DisableAuthCheck`; this mirrors what's done for `gh version`. `gh actions` and `gh actions [-h | --help]` all work while being logged out. In addition, this commit restores some original behavior. Before this commit, the help footer (usage, inherited flags, etc.) is appended whether you use `gh actions` or `gh actions --help`. This commit restores the original behavior where `gh actions` prints just the text for the actions explanation, but `gh actions --help` appends the help footer. commit 1c103e20ac0ef0f69a503dd523e3a5cdc47944e8 Author: chemotaxis Date: Fri Jun 18 23:07:31 2021 -0400 Always try to render bold font It looks like a similar check is done in ColorScheme.Bold() where it checks whether the scheme is enabled or not. commit 936d6e1a8f0816b5076acc8172e73253b4b6a645 Merge: 4d20aa78 4b2cded1 Author: Nate Smith Date: Fri Jun 18 14:03:02 2021 -0700 Merge pull request #3856 from cli/isolate-config-in-tests Ensure that tests for command factory never read from user's config commit 4d20aa78733aa1e6f8c8d35dd27e9b4979ae4e6f Author: Vishesh Gupta Date: Fri Jun 18 09:56:58 2021 -0400 Merge pull request #3801 from Vishesh-Gupta/automate-winget-release Automate packaging for Winget commit 052d6588eaa505b500fc40614aa56cdb9d99b366 Author: Mislav Marohnić Date: Fri Jun 18 15:16:48 2021 +0200 Revert "Whitespace" Trailing whitespace is significant in Markdown. This reverts commit 682c15d52c387e4af9d065ce8c536a45f813a6a7. commit 89ce78e48be593058ec2035d5f6dbe78c2fc706d Author: chemotaxis Date: Thu Jun 17 13:30:26 2021 -0400 Restore help footer At the moment, the "help footer" doesn't add any new information, but if additional flags are added later, they should appear in the footer. This change restores this help footer: ```shell USAGE gh actions [flags] INHERITED FLAGS --help Show help for command LEARN MORE Use 'gh --help' for more information about a command. Read the manual at https://cli.github.com/manual ``` commit 682c15d52c387e4af9d065ce8c536a45f813a6a7 Author: chemotaxis Date: Thu Jun 17 12:48:53 2021 -0400 Whitespace commit 4b2cded1f86e9cfac309b96814f999a847c24dc3 Author: Mislav Marohnić Date: Thu Jun 17 17:59:34 2021 +0200 Ensure that tests for command factory never read from user's config If these tests are going to exerise `factory.New()`, the config getter should always be overriden since the default config getter reads from `~/.config/gh` and thus makes tests dependent on the user's environment. commit 8ff42bf28c2d0fca0f5be893318db4418f225de2 Author: Mislav Marohnić Date: Thu Jun 17 17:58:46 2021 +0200 Fix repo override commit 8dd1e12f64de66ab4921b97cb32ebdb470990881 Merge: 94c16462 dd3aac7f Author: Mislav Marohnić Date: Thu Jun 17 16:12:57 2021 +0200 Merge remote-tracking branch 'origin' into fix-actions-help commit 94c1646209d5cb9a95918ceb8647f54918b4a312 Author: Mislav Marohnić Date: Thu Jun 17 16:02:00 2021 +0200 Simplify `gh actions` implemenation The command is now non-runnable, meaning it's exempt from auth check. commit 883943946a377547dba322cebabf9ed76b576ad3 Author: Mislav Marohnić Date: Thu Jun 17 15:33:07 2021 +0200 Add a global pre-run hook to handle auth check and repo override With auth check being done via Cobra hooks, it is automatically skipped for non-runnable commands and `-h/--help` flag usage. commit a44a3c8fd0e536613405e5d6585d442a46c7aea3 Author: Gowtham Munukutla Date: Thu Jun 17 10:41:24 2021 +0530 remove redundant logs commit 9b05254285dbf64c57c853969a610ddc604eeeae Author: Gowtham Munukutla Date: Thu Jun 17 10:28:24 2021 +0530 gofmt commit acaaeb5567a5cefd6afead59ead9f5e8b77ee0a2 Author: Gowtham Munukutla Date: Thu Jun 17 10:27:40 2021 +0530 remove unnecessary assignment of id commit 26105dec29f5c5f1289498fd8da0d85c04e91e81 Author: Gowtham Munukutla Date: Thu Jun 17 10:25:54 2021 +0530 fix lint commit 137053399e53c87dd747db18047a197701be07e8 Author: Gowtham Munukutla Date: Thu Jun 17 10:17:26 2021 +0530 tweak tests and add extra validations commit 93118c65df622ce2fc1937da57ba0706aa323104 Merge: 203d41c3 876b61af Author: jlsestak <73488851+jlsestak@users.noreply.github.com> Date: Wed Jun 16 20:53:06 2021 -0700 Merge pull request #52 from bchadwic/trunk Update browse.go removed trailing newlines on errors commit 876b61af7b66023d0db423ba99975d5aa6e0cc02 Author: jlsestak <73488851+jlsestak@users.noreply.github.com> Date: Wed Jun 16 20:50:08 2021 -0700 Update browse.go commit 203d41c37b3fb4f039fa377c072792a59ba8b31f Merge: 0a801c1e dd3aac7f Author: jlsestak <73488851+jlsestak@users.noreply.github.com> Date: Wed Jun 16 20:37:34 2021 -0700 Merge branch 'cli:trunk' into first-browse-pull commit 1e3bba5ff64c1bfba52295d0e38acd38d4ed78b4 Author: chemotaxis Date: Wed Jun 16 22:28:41 2021 -0400 Add comment about cmd.Help() The linter picked up that the error value from cmd.Help() isn't checked. Even though cmd.Help() returns an error value, it's always nil. The inner HelpFunc() function directly prints the error message instead of returning an error value. commit 558ff2dff088a12a3e178d3449de38eb64de61e9 Author: chemotaxis Date: Wed Jun 16 14:25:08 2021 -0400 Skip authentication message if asking for help Currently, this still checks authentication, but we skip the authentication message and exit normally. commit 3c8e163e8b1413287688ab71b4e1f86f534ad91d Author: Gowtham Munukutla Date: Wed Jun 16 12:50:05 2021 +0530 resolve PR comments. Tests WIP commit c903f1ecd09d585b5d5c89d671b034b07993c213 Author: chemotaxis Date: Wed Jun 16 01:18:52 2021 -0400 Ask for and print help even if logged out You have to explicitly ask for help using the help flags. Otherwise, `gh` will just print the authentication message. commit 7c8b6867f48b0dec171f2ba40ce76b9136ec7bce Merge: 5c96d5d4 dd3aac7f Author: Gowtham Munukutla Date: Wed Jun 16 09:27:53 2021 +0530 Merge branch 'trunk' of https://github.com/cli/cli into feature/repo-with-gitignore-license commit 0a801c1ed55af25685438d10631974b834e7ee97 Merge: 3d97aaf7 c8152ed9 Author: jlsestak <73488851+jlsestak@users.noreply.github.com> Date: Tue Jun 15 20:14:44 2021 -0700 Merge pull request #51 from bchadwic/trunk Fixed parseFileArgs, reformatted tests commit c8152ed9b133d010094314644b213b55f21495e1 Merge: 679d396c b64488fe Author: jlsestak <73488851+jlsestak@users.noreply.github.com> Date: Tue Jun 15 20:12:23 2021 -0700 Merge pull request #50 from jlsestak/trunk Changed parsefileArg to return a string, reformatted testing, polishe… commit b64488fe5cd0fcf7fe547368c3e88729e8703416 Author: Jessica Sestak Date: Tue Jun 15 20:10:48 2021 -0700 Changed parsefileArg to return a string, reformatted testing, polished up browse.go commit dd3aac7f522ccaa83bfc16878b6e23af9857c933 Merge: 741f768a bd015662 Author: Mislav Marohnić Date: Tue Jun 15 18:52:03 2021 +0200 Merge pull request #3846 from cli/build-windows-fix Improvements to build script on Windows commit 741f768a286722c64edd40726a4984fc8cd2e88e Merge: d299b74a a72f6346 Author: Sam Date: Tue Jun 15 12:19:24 2021 -0400 Merge pull request #3792 from chemotaxis/docs-install-via-conda Document installing via Conda package manager commit d299b74a3731d274ab8a3d4ecebb5621f2823ff3 Merge: 543a17df edfac423 Author: Sam Date: Tue Jun 15 12:10:36 2021 -0400 Merge pull request #3841 from cli/factory-cleanup Factory cleanup commit bd0156625137f3490d38dd6dbcc149e2726e7af1 Author: Mislav Marohnić Date: Tue Jun 15 17:33:33 2021 +0200 Allow `script\build` as shorthand for `go run script\build.go` on Windows commit 32f9a462a8a5be23b133272a14b18e6c4552da46 Author: Mislav Marohnić Date: Tue Jun 15 17:32:43 2021 +0200 Speed up build script by avoiding recursing into 3rd-party directories commit cda406f495e564aac3596f0aa8c79e00a303ab1a Author: Mislav Marohnić Date: Tue Jun 15 17:31:01 2021 +0200 Better error handling in build script on Windows `script/build.go` could encounter an "Access is denied" error when the project contains a symlink that could not be followed. This ignores such errors with a warning and allows the build to resume. commit 543a17df7f1e1a76ddaaa75697022161c64d6ba4 Merge: e380d68e 1f4bd80c Author: Mislav Marohnić Date: Tue Jun 15 16:16:59 2021 +0200 Merge pull request #3787 from cli/editor-tests Allow explicitly empty body in issue/pr create commit edfac4238408673be932208970fc153458408419 Author: Sam Coe Date: Mon Jun 14 16:17:33 2021 -0400 Set up iostreams in factory default commit 53fac59ef92d691fa265375df70d96a42c400fb6 Author: Sam Coe Date: Mon Jun 14 15:56:11 2021 -0400 Cleanup factory/default and add tests commit e380d68ed2d60dc480ee62e9831d75630c4125d6 Merge: 5984cf2a b3c2318e Author: Sam Date: Tue Jun 15 09:18:57 2021 -0400 Merge pull request #3789 from cristiand391/increase-gh-pager-precedence Increase `GH_PAGER` precedence commit 3d97aaf7f4eec3aa6a446c72f3432ee0b4a8e75b Merge: 707897ff 679d396c Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Mon Jun 14 22:37:21 2021 -0700 Merge pull request #49 from bchadwic/trunk Fixed pr issues commit 679d396c8d78d398db7699c98c2c322de6f23b9a Author: bchadwic Date: Mon Jun 14 22:31:29 2021 -0700 reformatted spacing and final touches commit cac372d017a3b8d7e42d8e2d89fb7f34866db0ee Merge: b08a6d2b 21691c8d Author: Husrav Homidov <38958131+ravocean@users.noreply.github.com> Date: Mon Jun 14 22:19:01 2021 -0700 Merge pull request #48 from ravocean/trunk Refactored browse.go commit 21691c8d2e099ba172960287c2c1afeae7688338 Author: ravocean Date: Mon Jun 14 22:16:58 2021 -0700 Reorganized tests commit 79da79fb6845f7b603c48a905738b2078791b4b9 Author: ravocean Date: Mon Jun 14 21:40:52 2021 -0700 We added new tests to bring the coverage to 90%+ commit d68a2038b6197f497d4052290e40c3b16fda066c Author: Cristian Dominguez Date: Mon Jun 14 22:01:35 2021 -0300 add test case commit 707897ffb66d59f73d9c763b79fa7633ae609b69 Author: jlsestak <73488851+jlsestak@users.noreply.github.com> Date: Mon Jun 14 08:29:22 2021 -0700 Deleted extra space commit 8423de7f508f849880955426ac82f0367ee285d2 Author: Sam Coe Date: Mon Jun 14 10:47:38 2021 -0400 Add tests commit 096f30a3199f1a7cb5227b6abc66c926b06b510c Author: Sam Coe Date: Mon Jun 14 10:46:41 2021 -0400 Add counterfeiter commit 2729af50df056bf921f7bb8e2efd3b72b09a59ae Author: Sam Coe Date: Fri Jun 11 13:51:25 2021 -0400 Cleanup SyncOptions commit 8d61b96bde7520ea355ad96beda1fd4af9395bdf Author: Sam Coe Date: Fri Jun 11 13:34:16 2021 -0400 Fix up error message commit 8b5abc77eaf3852abd5ff4891a721972bb7119f8 Author: Sam Coe Date: Fri Jun 11 13:25:21 2021 -0400 Extract git interactions into interface commit c6f89d3c172d31212505d59949776b39beb3ee1d Author: Sam Coe Date: Thu Jun 10 13:52:23 2021 -0400 Start tests commit 86e16cc7c4f25e1074ddbc072c9a9f4d2dfe17c9 Author: Sam Coe Date: Tue Jun 8 11:00:40 2021 -0400 Add repo sync command commit 5984cf2a8235fba9e4c182d56f06f970b05875f0 Merge: f7a78640 3a7ce3a4 Author: Mislav Marohnić Date: Mon Jun 14 16:17:24 2021 +0200 Merge pull request #3832 from cli/env-set-fix Fix setting environment secrets commit f7a786407d71e713cd8924ba00273730efc79483 Merge: aecfc01e d8ce6152 Author: Mislav Marohnić Date: Mon Jun 14 15:40:20 2021 +0200 Merge pull request #3834 from cristiand391/remove-unused-method Remove unused method from `httpmock` package commit d8ce6152528f8f711cc334334d10a52193e752b7 Author: Cristian Dominguez Date: Mon Jun 14 09:39:14 2021 -0300 Remove unused method from `httpmock` package commit 6ba70d4a1e75e6c715a8c3d1348bef2081e09505 Author: Cristian Dominguez Date: Mon Jun 14 08:57:06 2021 -0300 Add `run cancel` command commit 3a7ce3a440247c32c782153cd1837708e9471568 Author: Mislav Marohnić Date: Mon Jun 14 11:51:20 2021 +0200 Fix setting environment secrets This uses the correct public key when setting environment secrets. https://docs.github.com/en/rest/reference/actions#get-an-environment-public-key commit 1f4bd80c5698c8ce6fb44d8e3b46cd2728af5261 Author: Mislav Marohnić Date: Mon Jun 14 10:58:53 2021 +0200 Fix test flaky due to race in showing/hiding cursor https://github.com/cli/cli/pull/3787/checks?check_run_id=2793254411 commit 6eed0ded39c52b21212ac904b911ddcc682d014f Merge: 7999a457 b08a6d2b Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Sun Jun 13 21:36:03 2021 -0700 Merge pull request #47 from bchadwic/trunk Updated browse tests for Test_runBrowse and TestNewCmdBrowse commit b08a6d2bd0753de7c6403115b6fd40826bc5c4c6 Merge: 69ac4fc1 d8cf76e1 Author: bchadwic Date: Sun Jun 13 21:29:57 2021 -0700 Merge branch 'trunk' of https://github.com/bchadwic/cli into trunk commit 69ac4fc1db6389beef7ba54c58a4b57440558ac7 Author: bchadwic Date: Sun Jun 13 21:29:45 2021 -0700 reverted files commit d8cf76e19359c2480c409f0d810ce86df8ffc6c6 Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Sun Jun 13 21:23:23 2021 -0700 Delete build.exe commit 9c7a788193edae7d7e01fedbbfc391c423a7d88c Author: bchadwic Date: Sun Jun 13 21:05:50 2021 -0700 created more tests, and removed repoFlag commit d3efc5dcbdbeb8750fe5fbbbc69cacc3088e6f90 Merge: 05511c1c 7ea9d916 Author: jlsestak <73488851+jlsestak@users.noreply.github.com> Date: Sun Jun 13 20:17:03 2021 -0700 Merge pull request #46 from jlsestak/trunk Fixed TestNewCmdBrowse and added more tests commit 7ea9d9164aa228a3bad79ccf3338e67470f5a8e2 Author: Jessica Sestak Date: Sun Jun 13 20:16:19 2021 -0700 Fixed TestNewCmdBrowse and added more tests commit 05511c1ca6fabb75ab715c73a378593f12d36f82 Merge: 2b42ab92 edef757c Author: jlsestak <73488851+jlsestak@users.noreply.github.com> Date: Sat Jun 12 22:38:07 2021 -0700 Merge pull request #45 from jlsestak/trunk Made more runBrowse tests commit edef757c6ca26374ea542e2a2e865e1732ab59ea Author: Jessica Sestak Date: Sat Jun 12 22:36:03 2021 -0700 Made more runBrowse tests commit 2b42ab92c35835ed305da79362ddec8b410a4e19 Author: bchadwic Date: Sat Jun 12 21:58:29 2021 -0700 refactored browse.go commit 5f64243eb63cc6400f226cf70b6735f382ce00c2 Merge: 88ec5ad3 7999a457 Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Sat Jun 12 21:02:10 2021 -0700 Merge pull request #44 from bchadwic/first-browse-pull Restructured test file and discussed the structure of browse.go commit 568f4e4ee00a2ad6cce26bc87fea19bf9d938733 Author: camille folch Date: Tue Jun 8 20:26:22 2021 -0300 Minor refactoring for readability in NewCmdFork's runE commit 54b86c70937f4cf9b75877fa31127a2cf9c796a3 Author: camille folch Date: Tue Jun 8 20:22:57 2021 -0300 repo fork: check that --org is not the empty string As it is already being done for --remote-name, except in this case the default is the empty string. commit 7999a457ade82ec4b33411c6a46ba729d2e1b627 Author: Mislav Marohnić Date: Fri Jun 11 18:53:34 2021 +0200 Separate out `NewCmdBrowse` tests from `runBrowse` tests Co-authored-by: Benjamin Chadwick Co-authored-by: Jessica Sestak commit aecfc01e6911e0ed58e91f5072305f5441cc76fe Merge: af90f724 b0998772 Author: Nate Smith Date: Fri Jun 11 11:31:33 2021 -0500 Merge pull request #3809 from cli/fork-test-cleanup fork tests cleanup commit 14a1f63f787ea2c45f6caf3561893a7d0f1c9e1d Merge: e1a63110 88ec5ad3 Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Fri Jun 11 08:04:09 2021 -0700 Merge pull request #43 from bchadwic/trunk Fixing issues in the pr commit af90f72493ae1fab2cd3ca7d7331eb7c6a38df49 Merge: 7dbaaf2e 4debbb17 Author: Mislav Marohnić Date: Fri Jun 11 14:38:52 2021 +0200 Merge pull request #3803 from cli/http-accept-header Update "Accept" header for github.com requests commit 4debbb17cd40aaa6eb89e29c5ef7fd1e09ec2b6a Author: Mislav Marohnić Date: Fri Jun 11 14:32:08 2021 +0200 Further separate out test cases commit 7dbaaf2eb73bb632526cd115c9efcd7978f06804 Merge: 75abeb13 a4d1ce77 Author: Mislav Marohnić Date: Fri Jun 11 14:18:50 2021 +0200 Merge pull request #3804 from cli/pr-status-checks Fix showing Checks information in `pr status` commit 88ec5ad3b2d7f817e94ff071078a77454e379226 Author: bchadwic Date: Thu Jun 10 21:14:10 2021 -0700 Fixing issues in the pr commit b0998772ae6843659609e1410dae0a7b9ebd91b5 Author: Nate Smith Date: Thu Jun 10 21:40:56 2021 +0000 more cleanup commit f31a31e2edc0b30880dfddddb7bbf7c83e30c89a Author: Nate Smith Date: Thu Jun 10 21:30:04 2021 +0000 stop stubbing out a Since function commit 4a7ec7f4f6f79a457375f8d55556da50a4743a52 Author: vilmibm Date: Fri Jun 4 12:47:58 2021 -0500 cleaning up fork tests commit 14de70a0112ada64cbd90467ead60ea0bf6b609f Author: vilmibm Date: Wed Jun 9 14:10:19 2021 -0500 add defaultRemoteName commit a4d1ce77097ed3068592385712bd73ad9edc08c8 Author: Mislav Marohnić Date: Thu Jun 10 15:51:27 2021 +0200 Fix fetching information about the PR potentially being behind base branch commit 885e94786aa02f50f3eb5eeb00c136f618e7a9fa Author: Mislav Marohnić Date: Thu Jun 10 15:37:58 2021 +0200 Shorten GraphQL query for `pr status --json` commit e1b5f78df35f9c4855a0b1ea85fcdf9f608683df Author: Mislav Marohnić Date: Thu Jun 10 14:09:43 2021 +0200 :nail_care: grammar in comment commit e1a6311083c0ee954a3817bfdf77fbeee4c5d431 Merge: ab468a99 170f7fa3 Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Tue Jun 8 22:55:15 2021 -0700 Merge pull request #42 from bchadwic/trunk Refactored the output, and created more test functions / test cases commit 170f7fa305fcfcabc9520e75dc1adffa0bf4f94e Merge: 98a5541d e5b81fb6 Author: jlsestak <73488851+jlsestak@users.noreply.github.com> Date: Tue Jun 8 22:10:57 2021 -0700 Merge pull request #41 from jlsestak/trunk Made test for parse files args, fixed parse file args, fixed repo flag commit e5b81fb6a5a7a2dd59188969ee5530aec89eaf7b Author: Jessica Sestak Date: Tue Jun 8 22:09:11 2021 -0700 Started working on testing for parseFileArgs commit 1657cf46c1924a785f47996d525df94dda294bfb Author: Jessica Sestak Date: Tue Jun 8 21:23:50 2021 -0700 Fixed parsefileargs to throw errors with incorrect line numbers commit a96612c5e942538c3c66ddf3bc26a59fcf6cdf75 Author: Jessica Sestak Date: Tue Jun 8 20:54:56 2021 -0700 Fixed --repo to open with Args commit 98a5541d15feb8229ab87f8cdf3294ced4951bea Author: bchadwic Date: Tue Jun 8 20:35:38 2021 -0700 Started to fix the flags and the restrictions using them commit 95afca882b6b00806aeddbecaac45382e205795e Author: bchadwic Date: Tue Jun 8 16:35:53 2021 -0700 Finished refactoring now commit ab468a99e2b4e901cf1edc1a7ee8a308f8d568fe Merge: 698ca014 506bd4b2 Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Tue Jun 8 15:39:39 2021 -0700 Merge pull request #40 from bchadwic/trunk Refactored output, the tests, and the use of opts in runBrowse commit 506bd4b2debe96596b2abfbb2d67248044c4a2ad Author: bchadwic Date: Tue Jun 8 15:36:11 2021 -0700 working on refactoring the output, the tests and how opts is structured commit 395355d075b9c632fa6b2e7e0ea04cb2075cad99 Author: vilmibm Date: Tue Jun 8 15:49:06 2021 -0500 make prompt.Confirm stubbable commit 5b8a849a9cb07a45597c6f44e0d6b48bdd1c537c Merge: b0590541 698ca014 Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Tue Jun 8 13:21:27 2021 -0700 Merge pull request #39 from bchadwic/first-browse-pull keeping working branch up to speed commit 698ca014c73ecd8981682a17897de9276e819fb7 Merge: 9fe986a4 b0590541 Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Tue Jun 8 13:12:48 2021 -0700 Merge pull request #38 from bchadwic/trunk Redefined browse output, and added in more test cases commit b0590541021a22bf3a0a90cc1dab1ffcd0690fca Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Tue Jun 8 13:12:11 2021 -0700 Update settings.json commit 5efe9f8df3bb8c4a3892a4237a032442d7a67967 Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Tue Jun 8 13:10:33 2021 -0700 Update settings.json commit 9fe986a48041288cdddaf155cbbedd6dc862bc48 Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Tue Jun 8 12:48:17 2021 -0700 Update pkg/cmd/browse/browse.go Co-authored-by: Mislav Marohnić commit 891f4af69c171d3d7a06d35375a4658f8b119c97 Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Tue Jun 8 12:47:56 2021 -0700 Update pkg/cmd/browse/browse.go Co-authored-by: Mislav Marohnić commit 94002bf0ccb7f37c14cfe7adb050f7373b33be8a Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Tue Jun 8 12:47:44 2021 -0700 Update pkg/cmd/browse/browse.go Co-authored-by: Mislav Marohnić commit f5cd33b4bfe042a79cd6a712a001f2737305e60a Author: Mislav Marohnić Date: Tue Jun 8 20:22:33 2021 +0200 Fix showing Checks in `pr status` This was a regression in how `statusCheckRollup` is queried and stored. As a result, `gh pr status` did not include rendered information about checks related to each pull request. This switches the query builder to `PullRequestGraphQL()` to eliminate the outdated query. commit 3a55c2600022d2c8d4f431acbf24e3f0425a822e Author: Mislav Marohnić Date: Tue Jun 8 19:25:40 2021 +0200 Update "Accept" header for github.com requests The `antiope-preview` has graduated in github.com and no longer needs activating. However, we still need it for GHES requests. commit 8a221bb766e5841dd733167c1395e3ff6accfed8 Author: Mislav Marohnić Date: Tue Jun 8 19:21:48 2021 +0200 Add tests for our default HTTP client commit 0ebcbdd448cc5b2fbcd44edd15cec1a944251432 Merge: 569d2fd5 898d585e Author: Husrav Homidov <38958131+ravocean@users.noreply.github.com> Date: Mon Jun 7 22:01:38 2021 -0700 Merge pull request #37 from ravocean/trunk A small change in code commit 898d585e6a8d9d7be7ffc2da1e7a469d652880be Author: ravocean Date: Mon Jun 7 22:00:05 2021 -0700 A small change in code commit 569d2fd58afb9f80108e4d1b3c8d3c3975f14e99 Author: bchadwic Date: Mon Jun 7 21:47:08 2021 -0700 Got tests objects to fully work! Currently working on making more test cases commit 698c7d61b566fcf89bae51ca0e5b30f79c6b02ec Merge: f9a5818d 3dc66b95 Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Mon Jun 7 13:02:15 2021 -0700 Merge pull request #36 from bchadwic/trunk Updating pr, Fixed output comparison commit 3dc66b953992822a681ba3fd933df0d27dfe85e1 Merge: a3a010b6 44985815 Author: Husrav Homidov <38958131+ravocean@users.noreply.github.com> Date: Mon Jun 7 12:58:21 2021 -0700 Merge pull request #35 from ravocean/trunk Continued to work on the output of test file commit 449858158d1a17006300d74902c633e09f71861b Author: ravocean Date: Mon Jun 7 12:57:36 2021 -0700 Continued to work on the output of test file commit f9a5818ddcd896596bc7c47e5d42aca7e2897dbb Merge: bc3a3414 a3a010b6 Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Mon Jun 7 11:59:53 2021 -0700 Merge pull request #34 from bchadwic/trunk Added in a rough test file commit a3a010b603d590cc5b5fa4af2e562daf111dbdab Merge: 6ed99000 f7927040 Author: jlsestak <73488851+jlsestak@users.noreply.github.com> Date: Mon Jun 7 11:06:24 2021 -0700 Merge pull request #33 from jlsestak/trunk Worked on browse_test.go, still not getting appropriate errors commit f79270400313a43d33889bb145e3a5184aa4968e Author: Jessica Sestak Date: Mon Jun 7 11:04:29 2021 -0700 Worked on browse_test.go, still not getting errors commit 6ed990008497c464da2d0135ba1d13fde368c167 Author: bchadwic Date: Sun Jun 6 01:09:43 2021 -0700 working on the test file for the browse.go commit dd4ebc9b8fc38dd929ceabf22525d6e8d0591b98 Merge: 5fb5d41b e425e897 Author: Thanh D Tran <72171009+ttran112@users.noreply.github.com> Date: Sat Jun 5 23:34:44 2021 -0700 Merge pull request #32 from ttran112/trunk working on test file commit e425e897a68fd14642f550ee2f12b357d4b186ca Author: ttran112 Date: Sat Jun 5 23:31:19 2021 -0700 create new test file for first browse branch commit 1a9704dbfb58db378e62025c2607360b997eb89a Author: ttran112 Date: Sat Jun 5 22:46:46 2021 -0700 fix the route commit 0174dbcdc4c705dd951e2838218e50423472b633 Merge: 5fb5d41b bc3a3414 Author: Thanh D Tran <72171009+ttran112@users.noreply.github.com> Date: Sat Jun 5 22:18:44 2021 -0700 Merge pull request #2 from bchadwic/first-browse-pull firstbrowse commit bc3a341479d55c29818651f5261bd55dedaec98d Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Sat Jun 5 21:56:06 2021 -0700 Update browse.go commit f27b47f4f9c7bb7dc8df7eeac3c6450779cc4aac Merge: ed87cf71 6f09e4f5 Author: jlsestak <73488851+jlsestak@users.noreply.github.com> Date: Sat Jun 5 21:48:54 2021 -0700 Merge pull request #31 from jlsestak/trunk Updated errors commit 6f09e4f51a8b2c58f042325f72f7d397a9484184 Author: Jessica Sestak Date: Sat Jun 5 21:47:03 2021 -0700 Fixed print error messages and returning error from openInBrowser commit 95f7fb4bd837d00879d7aabd684424522595e78c Merge: 5fb5d41b ed87cf71 Author: jlsestak <73488851+jlsestak@users.noreply.github.com> Date: Sat Jun 5 20:53:40 2021 -0700 Merge pull request #1 from bchadwic/first-browse-pull Working on the errors commit ed87cf71b786b80b802f62cc66562c125c412fb0 Merge: 314b8e85 75abeb13 Author: Thanh D Tran <72171009+ttran112@users.noreply.github.com> Date: Sat Jun 5 20:43:39 2021 -0700 Merge branch 'cli:trunk' into first-browse-pull commit 314b8e85718d3ebf3438cdb03cb16e04fdc35ba0 Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Sat Jun 5 20:21:43 2021 -0700 Update main.go commit e5bd9666ff4cd21c8302dc61314e62eed3f93cc3 Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Sat Jun 5 20:20:57 2021 -0700 Fix white space commit 6fc36cf01f97057739027c80412a160c7b6ee873 Author: bchadwic Date: Sat Jun 5 17:48:52 2021 -0700 Fixed long message commit e28000b5d2c17f61ca3e32aa2ddabf37075bc06f Author: bchadwic Date: Sat Jun 5 17:46:07 2021 -0700 Worked on help section commit a72f6346dd9dba218156759047ecc4a98b95fff6 Author: chemotaxis Date: Sat Jun 5 12:37:55 2021 -0400 Rearrange Conda installation instructions Originally, I was thinking of putting Conda in a separate section after the Windows section, since Conda probably isn't as well known or used. But reading through the readme again, it seems like arranging it like the other instructions makes more sense. I found myself trying to look for the instructions when I first read it in the MacOS section, but couldn't find the instructions. commit 71c6b8f43ab8fde443ce1ef2146246fae8ff07a1 Author: chemotaxis Date: Sat Jun 5 00:48:36 2021 -0400 Add links to Conda section within each OS section commit 71b738b553ff22f17fc7c00e83bb66d54d315a64 Author: chemotaxis Date: Sat Jun 5 00:42:39 2021 -0400 Make whitespace consistent commit 3caba02f3c222be27449b70b8c83f26017b3acc8 Author: chemotaxis Date: Sat Jun 5 00:35:30 2021 -0400 Add documentation for installing via Conda Conda is a cross-platform package and environment manager, primarily associated with the scientific computing and data science communities. commit b3c2318e09023820b0235984ac449499fc5325f4 Author: Cristian Dominguez Date: Fri Jun 4 23:22:37 2021 -0300 Increase `GH_PAGER` precedence If `GH_PAGER` is exists, set it as the pager even if one is already set in config. This allows a user to change/disable the pager per single invocation. commit 606deaf134dc7c7eabeb2914b1e3302e2968b589 Author: Mislav Marohnić Date: Fri Jun 4 21:32:54 2021 +0200 Allow setting empty body via editor in `issue/pr create` commit f570deb11859a0da7e956124e1800b8272616ff0 Author: Mislav Marohnić Date: Fri Jun 4 21:24:17 2021 +0200 Add tests for opening the editor program commit 75abeb13a836cbf3faf43a2af12a8f1206787b4d Merge: 7d8940b7 051520af Author: Mislav Marohnić Date: Fri Jun 4 20:18:26 2021 +0200 Merge pull request #3786 from browniebroke/remove-secret-long-description Add a long command description for secrets remove commit bcfe176594fcd7dbab1d42e7a9b8ccf723f9bea1 Author: Mislav Marohnić Date: Fri Jun 4 20:06:21 2021 +0200 Fix flaky editor test There was a race condition wherein the test didn't wait enough time for the prompt to get rendered before testing the terminal output. commit 051520afe10c7e0219288db63cf8b691e37f950e Author: Bruno Alla Date: Fri Jun 4 16:32:21 2021 +0100 Add a long command description for secrets remove commit 7d8940b7515451f9f69ca50a9adff3606f638dfa Merge: ffebd23b 4d46447e Author: Mislav Marohnić Date: Fri Jun 4 16:41:53 2021 +0200 Merge pull request #3784 from browniebroke/fix/set-env-secret-description Fix description for gh secret set --env option commit 4d46447eb3ebbb34f123b6e31e1485e0c7b3b0d5 Author: Bruno Alla Date: Fri Jun 4 15:29:01 2021 +0100 Fix description for gh secret set --env option commit ffebd23ba77e0679ec9feac8aff9024c7dc1db63 Merge: 83fcecef 4bdddd72 Author: Mislav Marohnić Date: Thu Jun 3 19:13:38 2021 +0200 Merge pull request #3761 from cli/command-extensions Experimental command extensions support commit 4bdddd72d34b61c391397ab3332989a6f85f6b60 Author: Mislav Marohnić Date: Thu Jun 3 19:06:28 2021 +0200 Allow installing local extensions via symlinks This also quits searching for local extensions in PATH. commit 83fcecef748fef7751a26014f7a696e11dc962f8 Merge: b1663762 c2c691f4 Author: Mislav Marohnić Date: Thu Jun 3 18:36:45 2021 +0200 Merge pull request #3658 from chemotaxis/fix-pr-issue-body Handle default body text when creating issues and pull requests commit c2c691f44419cc1b1ece7f1771450d43269080a9 Author: Mislav Marohnić Date: Thu Jun 3 16:19:53 2021 +0200 Add test for our survey editor extension commit d974dbd338893027432a078248d35a78dbe765a2 Author: chemotaxis Date: Thu May 20 03:32:34 2021 -0400 Return default text if skipping the text editor when prompted If we are allowed to skip the editor _and_ we want to append the default text to the editor if we'd opened it, we just return the default text. Co-Authored-By: Mislav Marohnić commit b166376211f34a762354e0897b8548b46ac297d1 Merge: a1cedfcd 4b79edf6 Author: Mislav Marohnić Date: Thu Jun 3 13:55:34 2021 +0200 Merge pull request #3774 from browniebroke/feat/remove-env-secret Add support for removing environment secrets commit 4b79edf603044928c8bb5a2bef9070209de0ee52 Author: Bruno Alla Date: Thu Jun 3 08:51:39 2021 +0100 Add support for removing environment secrets commit 95ceb85ee5e9d299c6c3df1c577e330c02544a79 Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Wed Jun 2 12:42:09 2021 -0700 Delete helpful-resources.txt commit 5aeca61fd0920ea1a4b9a865e0c2ca25a6fadf0d Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Wed Jun 2 12:42:03 2021 -0700 Delete browse_test.go commit a1cedfcd5c0ad053537c54d38506de157ea1b654 Merge: 4d900058 6a74eb12 Author: Nate Smith Date: Wed Jun 2 13:53:48 2021 -0500 Merge pull request #3741 from jack1142/patch-1 Include issue number alongside the title in `gh issue/pr view` commit 4d900058175a99265c0b2d3cdb7edd3bf3cc09c0 Merge: 71547f45 20f915d5 Author: Nate Smith Date: Wed Jun 2 13:50:58 2021 -0500 Merge pull request #3772 from astroparam/escape-metacharacters escape metacharacters in job name commit 71547f456050395433cff28db86ce7595bf28e37 Merge: 7388d1e2 0931531e Author: Nate Smith Date: Wed Jun 2 13:35:08 2021 -0500 Merge pull request #3769 from browniebroke/feat/set-env-secrets Add support for setting environments secrets commit 0931531e2fac2322f37ed28a106183188b87a547 Author: vilmibm Date: Wed Jun 2 13:27:19 2021 -0500 collapse conditional commit 7388d1e2ff4c4f07cc95bacb45e0d97c0ee51cfa Merge: 548a91f0 389fdb7f Author: Sam Date: Wed Jun 2 13:40:16 2021 -0400 Merge pull request #3768 from cli/xdg-data Add support for XDG_DATA_HOME commit 20f915d5ba89141f2f26f05e87a27e9c247dac4f Author: Param Patidar Date: Wed Jun 2 17:20:31 2021 +0000 escape metacharacters in job name commit 32856c987dc8b4ab688f7cbb23d16bb9313c1eba Author: Bruno Alla Date: Wed Jun 2 15:53:40 2021 +0100 Add ability to set environments secrets commit d396674e12e1fdaba930043eae2f208b751c635c Author: Bruno Alla Date: Wed Jun 2 15:53:03 2021 +0100 Fix a typo in the docs commit 389fdb7f99ffe445b8b5b881cd8dae1c428e249b Author: Sam Coe Date: Wed Jun 2 09:56:22 2021 -0400 Add XDG env variables to environment help topic commit e6ad8a10d0bd576d105f99fab318dbd382d7a03a Author: Sam Coe Date: Wed Jun 2 09:46:14 2021 -0400 Add support for XDG_DATA_HOME commit 6a74eb12620e4278a1170977728b560e917f828a Merge: 184149b8 548a91f0 Author: jack1142 <6032823+jack1142@users.noreply.github.com> Date: Wed Jun 2 11:10:25 2021 +0200 Merge branch 'trunk' into patch-1 commit 548a91f0cda256ec35fa5aefa61e222d8e2c01cf Merge: 49609350 c34d017a Author: Mislav Marohnić Date: Wed Jun 2 09:29:11 2021 +0200 Merge pull request #3767 from astroparam/fix-project-layout-link fix project layout link commit c34d017a04947a6eca1facfbd90f1f75ea588109 Author: Param Patidar Date: Wed Jun 2 12:33:40 2021 +0530 fix project layout link commit 49609350afdb6ffdae982505de0ab1d2560739dd Merge: e65d9560 1a980e76 Author: Mislav Marohnić Date: Wed Jun 2 08:48:11 2021 +0200 Merge pull request #3737 from cli/requested-reviewers-slug Fix how teams are displayed in requested reviewers commit 184149b8440ec013a10ea90ce71879017c0f453a Author: jack1142 <6032823+jack1142@users.noreply.github.com> Date: Tue Jun 1 23:53:58 2021 +0200 Add missing new line commit e65d9560400aca520fc25702c1d38735a13f64bf Merge: 89eff6c7 1d7ffc20 Author: Sam Date: Tue Jun 1 15:57:16 2021 -0400 Merge pull request #3717 from cli/xdg-state-2 Add support for XDG_STATE_HOME commit 1d7ffc20133c49bcd8cba6ba68c77a637d7364fd Author: Sam Coe Date: Tue Jun 1 15:40:08 2021 -0400 Add support for LocalAppData and .local/state/ fallback commit fce93d60809ba97487b1268de1cd3977b59034fa Author: Mislav Marohnić Date: Fri May 28 19:54:22 2021 +0200 Experimental command extensions support Extensions are looked up as `~/.config/gh/extensions/gh-*`. Additionally, any executables found in PATH named `gh-*` are available as `gh `. commit 89eff6c79086a9d8f2888ef6000c94339cb64dd9 Merge: 6bb34cf6 b5140339 Author: Mislav Marohnić Date: Tue Jun 1 13:44:48 2021 +0200 Merge pull request #3750 from itsme-alan/patch-1 Update upgrade docs for winget commit 5fb5d41bb8021b3887a3ff8a404221b35eb2c2b7 Merge: 798e03cf 6391bf58 Author: Husrav Homidov <38958131+ravocean@users.noreply.github.com> Date: Mon May 31 16:40:57 2021 -0700 Merge pull request #28 from ravocean/trunk Starter code for testing commit 6391bf580186eccd7721333bc0dc0792db3b5293 Author: ravocean Date: Mon May 31 16:39:10 2021 -0700 Starter code for testing commit 798e03cf159342dd53774e1c0d7d5797ff197c71 Author: bchadwic Date: Mon May 31 15:21:15 2021 -0700 working on testing browse.go commit 8fa24571637bad942ff23d6db9618b97c7d70d1e Merge: 0f2e2990 52e7b7cc Author: Thanh D Tran <72171009+ttran112@users.noreply.github.com> Date: Mon May 31 14:09:23 2021 -0700 Merge pull request #27 from ttran112/trunk added the message to help the user navigate github commit 52e7b7cc86c26947abc4395467557458d195446c Author: ttran112 Date: Mon May 31 14:08:15 2021 -0700 added the message to help the user navigate github commit 0f2e2990cca83eeda2f32f0ab8705bafe6d1507f Merge: bd28e0e9 0bfeb892 Author: jlsestak <73488851+jlsestak@users.noreply.github.com> Date: Mon May 31 13:02:51 2021 -0700 Merge pull request #26 from jlsestak/trunk Fixed error messages, added repo override commit 0bfeb8926af0c82294aec3d6ff7c91dea3e8b733 Author: Jessica Sestak Date: Mon May 31 13:02:05 2021 -0700 Fixed error messages, added repo override commit 6bb34cf6a249444632acb59fbbfab709e143b42a Merge: 157bab18 e5bdaaab Author: Mislav Marohnić Date: Mon May 31 21:22:07 2021 +0200 Merge pull request #3270 from tylerxwright/feature/issue-3265-implement-gh-secret-list-env Adds the ability to list environment secrets - gh secret list -e dev commit e5bdaaab2c9a7e5efbe39c24e356aace40c60a8b Author: Tyler Wright Date: Mon May 31 21:13:13 2021 +0200 Add ability to list environment secrets Co-authored-by: Mislav Marohnić commit 157bab18e4c9f9f6f6306c39aa1964d04ec94a71 Merge: e160dd3e 260f720c Author: Mislav Marohnić Date: Mon May 31 21:11:21 2021 +0200 Merge pull request #3679 from g14a/feature/list-secrets-limit List all secrets instead of being subject to the default limit of 30 results commit bd28e0e9578465970117054bd32f4dac9ba87274 Merge: c162a280 a8a01bad Author: Husrav Homidov <38958131+ravocean@users.noreply.github.com> Date: Mon May 31 11:45:50 2021 -0700 Merge pull request #23 from ravocean/trunk Finished refactoring (addCombined) commit a8a01badf1e9eacb8f6dbe274460c92072923c34 Author: ravocean Date: Mon May 31 11:42:48 2021 -0700 Finished refactoring (addCombined) commit 260f720c0738c403132f10477a1565cd57477c5c Author: Mislav Marohnić Date: Mon May 31 19:00:44 2021 +0200 :nail_care: refactor and add tests for Secrets pagination commit cb605387091ab70dd50cdc250f6ed460f439c7fc Author: Gowtham Munukutla Date: Sat May 22 13:57:40 2021 +0530 paginate to get all secrets at once commit 2444e4aadc9045d6892b1682157512c373de273d Merge: ab037b0e c162a280 Author: Husrav Homidov <38958131+ravocean@users.noreply.github.com> Date: Mon May 31 10:59:35 2021 -0700 Merge branch 'bchadwic:trunk' into trunk commit c162a28099d14507e1b5de131b617c36b49e8537 Author: bchadwic Date: Mon May 31 10:57:16 2021 -0700 started to refactor the code base commit ab037b0e74b842089fc37fbf72cceed20f1df467 Merge: 74d9a9b8 e160dd3e Author: Husrav Homidov <38958131+ravocean@users.noreply.github.com> Date: Mon May 31 10:11:40 2021 -0700 Merge branch 'cli:trunk' into trunk commit b51403391eeac9f95f519dc80bce90c98500bfff Author: itsme-alan <75578443+itsme-alan@users.noreply.github.com> Date: Mon May 31 13:22:35 2021 +0530 Update upgrade docs for winget commit 74d9a9b86676719af48dd0124849e4438ef8d6ea Merge: 53dff30a 5077d479 Author: Thanh D Tran <72171009+ttran112@users.noreply.github.com> Date: Sun May 30 20:28:37 2021 -0700 Merge pull request #22 from ttran112/trunk fixed the 404 edge case error commit 5077d4794c0f05f8eb996052d0aced10132d4edc Author: ttran112 Date: Sun May 30 20:25:11 2021 -0700 fixed the 404 edge case error commit 5c96d5d46087828a5e2f02007b73cfa0146614e6 Author: Gowtham Munukutla Date: Sun May 30 13:53:56 2021 +0530 fix lint errors commit c4beed8276ae42c40e0bf6325487f52dda66a925 Author: Gowtham Munukutla Date: Sun May 30 13:42:39 2021 +0530 complete tests commit 9b87b13b80b9a13e67ee45ad7900b893d4522c9d Author: Gowtham Munukutla Date: Sat May 29 19:27:30 2021 +0530 add test cases WIP commit 42333bb2d1cb11a3d224d11aa1c15bf298382ad2 Author: jack1142 <6032823+jack1142@users.noreply.github.com> Date: Sat May 29 04:43:58 2021 +0200 Update issue non-tty view formatting and its tests commit 979ec9298d7d88f84ac84b1bf753a4b296262923 Author: jack1142 <6032823+jack1142@users.noreply.github.com> Date: Sat May 29 04:43:34 2021 +0200 Update issue tty view formatting and its tests commit 3943a8bb1fca0c2356f145e381e2dc4bf179120c Author: jack1142 <6032823+jack1142@users.noreply.github.com> Date: Fri May 28 18:53:31 2021 +0200 Update PR tty view formatting and its tests commit 1a980e768cdad166b30e8c3ae706c99c6c709bd6 Author: Mislav Marohnić Date: Fri May 28 14:32:31 2021 +0200 Fix how teams are displayed in requested reviewers 1. The `--json` export now only renders the `login` field for User types and `name` and `slug` fields for Team types. 2. The `pr view` command now renders team reviewers in the format of `ORG/SLUG` instead of the team name. This is so that the same value can be used in the `pr create -r` flag. commit e160dd3eae3925546402ed2b1def91e756c0074e Author: Gowtham Munukutla Date: Fri May 28 15:41:12 2021 +0530 fix listing of PRs when merged ones are searched (#3730) Co-authored-by: Mislav Marohnić commit 35e5c758b5ee3c8d086eeff927aaa95544394510 Merge: ece536a5 761fa948 Author: Sam Date: Thu May 27 09:09:58 2021 -0400 Merge pull request #3701 from mercimat/fix-pr-team-reviewrequests fix adding/removing team reviewers with `gh pr edit` commit 761fa94831c7d2bbc99bcb81a3069c23c6da0155 Author: Sam Coe Date: Thu May 27 08:47:41 2021 -0400 Small nitpicky polish commit 53dff30ab0a79a2c86c24f319a8a6234fba9eba0 Merge: 43a9709e ece536a5 Author: Husrav Homidov <38958131+ravocean@users.noreply.github.com> Date: Wed May 26 19:32:29 2021 -0700 Merge branch 'cli:trunk' into trunk commit 43a9709e4ecbc1cdbabc569a694dabd2bddcb9b8 Merge: c127ca40 cb379205 Author: Thanh D Tran <72171009+ttran112@users.noreply.github.com> Date: Wed May 26 19:29:14 2021 -0700 Merge pull request #18 from ttran112/trunk fix minor error and add branchName for addArgs, addCombine commit cb3792051a0b1efe2d6c7c1656411db3999b43cc Author: ttran112 Date: Wed May 26 19:27:07 2021 -0700 fix minor error and add branchName for addArgs, addCombine commit c127ca405148ca8a39d20a2584378dc7332c51ef Merge: 7a0329e7 3a48c35b Author: Thanh D Tran <72171009+ttran112@users.noreply.github.com> Date: Wed May 26 17:47:30 2021 -0700 Merge pull request #17 from ttran112/trunk fix addArgs and combine by addding branchName parameter commit 3a48c35bc6261c223280a5bfc65a21a0a036ff43 Author: ttran112 Date: Wed May 26 17:45:42 2021 -0700 fix addArgs and combine by addding branchName parameter commit 602167c0c7345f28148d74fb6d674d3a65938bf5 Author: Sam Coe Date: Wed May 26 11:28:58 2021 -0400 Address PR comments commit ece536a5fdd6920ea94673364a11fe835f207728 Merge: 6b49b487 ebf2bb9f Author: Mislav Marohnić Date: Wed May 26 12:59:02 2021 +0200 Merge pull request #3700 from cli/pr-checkout-deleted Fix `pr checkout` for PRs coming from deleted forks commit ebf2bb9f4b0f1cafd610eaaff5302e28cff9126b Merge: b9a4a425 053d43f7 Author: Mislav Marohnić Date: Wed May 26 12:49:24 2021 +0200 Merge pull request #3714 from cli/pr-checkout-push Fix `pr checkout` setting up git push configuration commit 7a0329e708016b207f1db5ffddd980e5162763e2 Merge: f43113e3 88ce5320 Author: jlsestak <73488851+jlsestak@users.noreply.github.com> Date: Tue May 25 23:05:25 2021 -0700 Merge pull request #15 from jlsestak/trunk made branch and line flags, fixed path commit 88ce5320f608ab39d11011d4a4cb1e836fb1739d Author: Jessica Sestak Date: Tue May 25 23:04:42 2021 -0700 made branch and line flags, fixed path commit f43113e3c96bb084c0ece3097c4b687022eb8cc7 Merge: 282bb9a0 a65266bf Author: jlsestak <73488851+jlsestak@users.noreply.github.com> Date: Tue May 25 22:49:32 2021 -0700 Merge pull request #14 from jlsestak/trunk added combination of args, partial fix to args commit a65266bf3617e2f1003af88381c371a1f1b30aac Author: Jessica Sestak Date: Tue May 25 22:47:08 2021 -0700 added combination of args, partial fix to args commit 282bb9a014310ec8ede3573f380329dfa60fb5db Merge: 50af6c16 1e61f41c Author: Husrav Homidov <38958131+ravocean@users.noreply.github.com> Date: Tue May 25 21:44:45 2021 -0700 Merge pull request #11 from ravocean/trunk Tidied up the logic of openInBrowser commit 1e61f41cd93431e643312f1e49a5a772835076ef Author: ravocean Date: Tue May 25 21:42:42 2021 -0700 Tidied up the logic of openInBrowser commit 50af6c1658fb34ba56fa274bbbf59389aa681618 Author: Ben Date: Tue May 25 17:54:23 2021 -0700 broke the flag checking into a separate function, and created a http response checker function commit 583e74d70c22356f7dc80e71d10a4bb4ec397efc Author: Sam Coe Date: Tue May 25 16:01:37 2021 -0400 Add support for XDG_STATE_HOME commit 053d43f7053e30cc0f21773a2c8ca9b065dc20b1 Author: Mislav Marohnić Date: Tue May 25 15:43:18 2021 +0200 Fix `pr checkout` setting up git push configuration commit 24abf29acb22697dff3ba6fcacccb1835e53e675 Author: bchadwic Date: Mon May 24 21:16:37 2021 -0700 created a test file commit 6b49b487e2bde73c257b4895aea517554025eaa4 Merge: 045089c4 972d5ff5 Author: Sam Date: Mon May 24 18:41:41 2021 -0400 Merge pull request #3707 from cli/fix-test-env Fix test that was deleting local config folder commit 972d5ff5f0a731dc2c94b14cc1ab94263eadaca1 Author: Sam Coe Date: Mon May 24 18:35:37 2021 -0400 Fix test that was deleting local config folder commit 325886c0e4d4c374be4d99eb8e1eff735a041a85 Author: Cristian Dominguez Date: Mon May 24 18:14:10 2021 -0300 Only schedule an auto-merge when PR state is blocked When passing `--auto` flag, only schedule an auto-merge if the `mergeStateStatus` field is "BLOCKED". This ensures that a PR will always be merged when passing `--auto` even if it doesn't have required checks or if checks have already passed. commit 045089c483b37c7a5afd3905638576a79d7b6c9d Merge: 55b183f3 0d49bfba Author: Sam Date: Mon May 24 16:22:36 2021 -0400 Merge pull request #3671 from cli/xdg-config Add support for XDG_CONFIG_HOME and AppData on Windows commit 0d49bfba42f2e77865a08565b18cffe2a9417261 Author: Sam Coe Date: Wed May 19 10:26:38 2021 -0400 Add support for XDG_CONFIG_HOME and AppData on Windows commit 70e72afdcc705287bf0ca46152324f39337b33ed Author: bchadwic Date: Mon May 24 13:06:33 2021 -0700 cleaned up, added suggestion to 404 handling commit fdca340b28b0059101cf546a5e00b4b3b4e96890 Author: bchadwic Date: Mon May 24 12:53:17 2021 -0700 simplified the if block for flags, now allows for checking destination before opening browser commit 88e2a9f748c5b2801954281913a1dfbc25b64dd2 Author: bchadwic Date: Mon May 24 12:48:32 2021 -0700 cleaned logic and added in a print function that takes in an exitCode commit a612f06deec8bd79085c05d55e7083da5beebce8 Author: mercimat <11376662+mercimat@users.noreply.github.com> Date: Mon May 24 17:00:25 2021 +0200 fix pr review requests for teams commit b9a4a425bfebcf44b16b4dc97190fa44347426fa Author: Mislav Marohnić Date: Mon May 24 16:52:53 2021 +0200 Fix `pr checkout` for PRs coming from deleted forks commit 55b183f3c92707b019bae208662d6e2bf0893407 Merge: c49c7f4d 0da3a195 Author: Mislav Marohnić Date: Mon May 24 12:27:21 2021 +0200 Merge pull request #3697 from sadikkuzu/patch-1 Capitalized h commit 6b8f1f68c8e755ee1f45fade9e48da7be245bbe7 Merge: aa7acb72 edebfa10 Author: jlsestak <73488851+jlsestak@users.noreply.github.com> Date: Sun May 23 19:10:31 2021 -0700 Merge pull request #9 from jlsestak/trunk Added errors and started defining the the path for flags and args to … commit edebfa104bebfc7e8a5808876e99441bdccb83e9 Author: Jessica Sestak Date: Sun May 23 19:04:38 2021 -0700 Added errors and started defining the the path for flags and args to open in the browser. commit 0da3a19526de3b7388674f14209a2b8eb9dcdce6 Author: SADIK KUZU Date: Mon May 24 04:04:05 2021 +0300 Capitalized h commit aa7acb727239b9dcfa574397c27395f79bf8c14d Merge: c359b3e4 b2251930 Author: Husrav Homidov <38958131+ravocean@users.noreply.github.com> Date: Sun May 23 16:02:51 2021 -0700 Merge pull request #7 from ravocean/trunk Added flags: --settings, --projects, and --wiki commit b22519309f2e798b4ce06bda5e7ff9ad6c046282 Author: ravocean Date: Sun May 23 16:01:18 2021 -0700 Fixed the name of projects flag commit e4b2ec202d28696cecf276f9a409d0de6b2bdda0 Author: ravocean Date: Sun May 23 15:57:29 2021 -0700 We added a few flags based on priority commit c359b3e4541a74b9f988654aceab8923a53391d5 Merge: 8d1dee4c 6e7cd6fd Author: Husrav Homidov <38958131+ravocean@users.noreply.github.com> Date: Sun May 23 15:34:08 2021 -0700 Merge pull request #5 from ravocean/trunk Added parseArgs() function, beginning to work on parseFlags() functions commit 6e7cd6fd05ff20d301bb7148166590556c0aeb98 Author: ravocean Date: Sun May 23 15:31:01 2021 -0700 Added parseArgs() function, beginning to work on parseFlags() functions commit 8d1dee4cd42808b33b96c51a1f2df88b22b75ac2 Merge: bab7b498 f66a086a Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Sun May 23 12:36:01 2021 -0700 Merge pull request #2 from jlsestak/trunk Update browse.go commit f66a086a5d063e7d384a343c3259b4cc946bd7ce Merge: e4e8b77c bab7b498 Author: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Sun May 23 12:35:53 2021 -0700 Merge branch 'trunk' into trunk commit e4e8b77c7d7b9f14bb3fb00d79f7ced2212d9e75 Author: jlsestak <73488851+jlsestak@users.noreply.github.com> Date: Sun May 23 12:30:27 2021 -0700 Update browse.go commit bab7b498ec93237db66761a6dd1e8a254b153929 Author: jlsestak <73488851+jlsestak@users.noreply.github.com> Date: Sun May 23 12:23:33 2021 -0700 Update browse.go commit c49c7f4de289a7528f1882b2d6e77d3df606e757 Merge: 09a92c6e 9c614247 Author: Nate Smith Date: Fri May 21 15:41:16 2021 -0500 Merge pull request #3575 from g14a/feature/action-headers add column headers and age column in listing runs commit 09a92c6e1dda467208a51455e23ce16f401be247 Merge: ca04e219 0208620a Author: Mislav Marohnić Date: Fri May 21 19:30:32 2021 +0200 Merge pull request #3688 from cli/pr-merge-fix Fix `pr merge` with GHE < 3.0 commit 0208620a6ffbda59c0440a15d36627d45b6c0a98 Author: Mislav Marohnić Date: Fri May 21 17:37:21 2021 +0200 Remove unnecessary `commits` stubs from fixtures commit 6bec7a956a1a046eaa590c63d7497d3b7cfea592 Author: Mislav Marohnić Date: Fri May 21 17:35:41 2021 +0200 Fix `pr merge` on GHE < 3.0 This avoids loading authorship information for git commits, since it relies on a GraphQL API that wasn't available before GHE v3.0. The authorship information wasn't necessary for the merge operation anyway; just loading the last commit OID was. commit 9c614247a645279a0b5a9d1e64769e640befd813 Author: Gowtham Munukutla Date: Fri May 21 09:36:23 2021 +0530 lint fix commit 605f785c70b61e45fe8760fab5b138f04c777269 Author: Gowtham Munukutla Date: Fri May 21 09:33:55 2021 +0530 use fuzzy abbr to display age commit ca04e2192185e372474624c84b25eab30053f0e5 Author: Dan Greene <14020024+dgreene1@users.noreply.github.com> Date: Thu May 20 14:03:44 2021 -0400 Merge pull request #3681 from dgreene1/trunk Describes usage in Github Actions commit 4842b69b6b01d4466321783d2730778f60637a01 Merge: 48f114da 3e21da91 Author: Nate Smith Date: Thu May 20 09:50:36 2021 -0700 Merge pull request #3672 from cli/deb-doc simplify deb installation docs commit 3e21da9167f45aefc12f5d5a97156c7b8db53670 Author: vilmibm Date: Thu May 20 11:44:50 2021 -0500 just use stable commit 48f114dae4713b41a3a7654af531234f33f63561 Merge: 6aedba38 7ed4204d Author: Mislav Marohnić Date: Thu May 20 13:40:57 2021 +0200 Merge pull request #3668 from tklauser/uniseg-bump Bump github.com/rivo/uniseg to v0.2.0 commit bc4be19319ea693c1b6527220793d2b47a88d81d Author: Gowtham Munukutla Date: Thu May 20 12:28:26 2021 +0530 lint repair commit 31854f0b25e4608e0b28c1ebfdb0a7f53de6323e Author: Gowtham Munukutla Date: Thu May 20 12:24:47 2021 +0530 add extra age column and repair tests commit b8de5c87e1dff2fad77a86777556762161e99626 Author: Ben Date: Wed May 19 22:45:03 2021 -0700 removed help message for non error operation commit 6b0a07f22e37625df73007032017bd5169801749 Merge: 97f80740 6aedba38 Author: Gowtham Munukutla Date: Thu May 20 10:59:14 2021 +0530 Merge branch 'trunk' of https://github.com/cli/cli into feature/action-headers commit d8c20faa65031639264cb837167a09bb14c337c4 Author: Ben Date: Wed May 19 22:14:58 2021 -0700 added in a help dialogue and error message commit daf27af34aa7247f47bd86b7eb682ed7cec4b0a0 Author: Ben Date: Wed May 19 19:17:14 2021 -0700 only accepts 1 arg at most commit 2d704ba5910fe48111dd7e623c95d0b4ada79e12 Author: vilmibm Date: Wed May 19 16:51:58 2021 -0500 bonus: support sid commit 320ab8ad31769468e237dea08086d6de17ea5f35 Author: vilmibm Date: Wed May 19 16:20:01 2021 -0500 restore header, use an emoji commit 14a40fb0db3df193ae1389f7577a9f1d6d3ac411 Author: Ben Date: Wed May 19 13:13:23 2021 -0700 added a return statement to avoid a segmentation fault on a null baseRepo, error handling will be improved more later commit 697ff425a8e717df8b92c94ce9a0934d62360205 Author: Ben Date: Wed May 19 13:09:30 2021 -0700 disregard commit 097dd25884fc792b8d30e967f346493c2bbad78c Author: Ben Date: Wed May 19 12:57:56 2021 -0700 redefined how gh browse opens the repo. We now use the BaseRepo to extract the correct paths. This should make it easier to navigate to other directories within the repo using the repoUrl variable created commit 7ed4204dfce7dfb5d354181671a2bae9ac384e43 Author: Tobias Klauser Date: Wed May 19 19:12:43 2021 +0200 Bump github.com/rivo/uniseg to v0.2.0 Changes: https://github.com/rivo/uniseg/compare/v0.1.0...v0.2.0 This speed up and reduces the memory footprint of `uniseg.NewGraphemes`, used in `pkg/text`. commit 6c5b690bbec934034826b2c34ee0ee86d3b80e6f Author: vilmibm Date: Wed May 19 12:13:29 2021 -0500 simplify deb installation docs commit 6aedba38c4d98df99f3896845a9682aa08f72982 Merge: dee89a1b e0e25c82 Author: Mislav Marohnić Date: Wed May 19 17:18:18 2021 +0200 Merge pull request #3666 from cli/fix-windows-config Fix creating Windows directory for gh config commit e0e25c82ff7b50d5587ebdb42efdf3822e759e0d Author: Mislav Marohnić Date: Wed May 19 16:29:44 2021 +0200 Fix creating Windows directory for gh config commit dee89a1b6a7a346eb5109f5c6b798ecfa7702f25 Merge: 001e92e3 79896ed5 Author: Mislav Marohnić Date: Wed May 19 13:35:57 2021 +0200 Merge pull request #3663 from cli/cross-repo-pr-checkout Fix `pr checkout` for cross-repository pull requests commit 79896ed513736b987a47f30e7a193e8132d5cb07 Author: Mislav Marohnić Date: Wed May 19 13:13:11 2021 +0200 Fix `pr checkout` for cross-repository pull requests commit 001e92e3e640a61b12e9ff9b235122880115cc96 Merge: f30afce5 c667a0bc Author: Mislav Marohnić Date: Tue May 18 19:54:44 2021 +0200 Merge pull request #3656 from cli/release-json-export Add `release view --json` export support commit c667a0bc49598ea6de09f5127ae18a36f29b559e Author: Mislav Marohnić Date: Tue May 18 19:44:29 2021 +0200 Fix fetching draft releases from GitHub Actions When using GITHUB_TOKEN in Actions, the permissions on a repository are null and therefore we can't check whether the viewer has push access or not. The solution is to unconditionally check for draft releases instead of trying to be smart about it. Draft releases are going to be on top, so we don't have to paginate through all releases in a repository. commit 4425365004faf125a33de404fbe7593cd206625e Author: Mislav Marohnić Date: Tue May 18 19:40:28 2021 +0200 Add `release view --json` support commit f30afce5da8103b78f18ff43bf0347f35884a56d Merge: 068ad31c 1440fd81 Author: Mislav Marohnić Date: Tue May 18 18:55:00 2021 +0200 Merge pull request #3547 from cli/pr-lookup-refactor Eliminate API overfetching in `pr` commands commit 1440fd81a1e76fa6a32b1c5cc6e97affbed420e4 Author: Mislav Marohnić Date: Tue May 18 18:35:34 2021 +0200 Fix broken GraphQL queries due to editing Author struct commit 42155c7d2de352a667c99a64c9091c4b177eee38 Author: Mislav Marohnić Date: Tue May 18 18:19:28 2021 +0200 Export more IDs in issue/pr JSON payload commit e758f30073002dcb06fa4b3bb10e69706b5982be Author: Mislav Marohnić Date: Tue May 18 16:59:03 2021 +0200 Fix preloading of pr reviews, checks, and issue/pr comments commit 51f7cbdfde0a533b99db23f2e3d2f607ea693d16 Author: Mislav Marohnić Date: Tue May 18 09:58:21 2021 +0200 :nail_care: cleanup and tests for PR finder commit 068ad31c14cbbb789c2dd0a251a09b66e7484850 Author: Mislav Marohnić Date: Tue May 18 08:11:47 2021 +0200 Add support for new Ubuntu, Kali linux (#3645) Co-authored-by: vilmibm commit bc3bb97c431df5279fca9de5693307dfd49f0240 Merge: c50d390c 29908d70 Author: Mislav Marohnić Date: Mon May 17 17:41:38 2021 +0200 Merge remote-tracking branch 'origin' into pr-lookup-refactor commit 29908d70eb934775b2e1a591540f39298d1f6340 Merge: 4b0b422e 42d2da81 Author: Mislav Marohnić Date: Mon May 17 17:20:39 2021 +0200 Merge pull request #3648 from cli/docs-links Fix some docs formatting for the web commit 42d2da812c83bc0a6a108f55b2476fd7fba5b070 Author: Mislav Marohnić Date: Mon May 17 17:01:33 2021 +0200 Preserve list fomatting in web docs for `gh actions` commit eb35a3457c4cc7fc726b633c06eb4dea03937d93 Author: Mislav Marohnić Date: Mon May 17 17:00:25 2021 +0200 Make sure docs URLs are linked in web docs commit 4b0b422eb56a5b1d4a836a0dbff39ab25ff60cab Merge: adbfb6e8 3f3d4e38 Author: Mislav Marohnić Date: Mon May 17 16:53:20 2021 +0200 Add `--json` export functionality to repo commands (#3627) commit 3f3d4e38d44e21727e55fc628113357f9918c268 Author: Mislav Marohnić Date: Mon May 17 16:43:39 2021 +0200 Avoid crash when `--json` doesn't request `nameWithOwner` commit a2307e357dfa74423c8641794e728b1017bc03b4 Author: Mislav Marohnić Date: Mon May 17 16:32:01 2021 +0200 Add `repo list --json` support commit adbfb6e8deb49667376f53ec60b9bd21dde0658a Author: Mislav Marohnić Date: Mon May 17 15:37:39 2021 +0200 Merge pull request #3638 from cli/release-discussion Create a Release Discussion on every new release commit 301a35eedc330bfadb8c6ec3f8f285b001603595 Merge: f2456f48 5f0301c9 Author: Mislav Marohnić Date: Mon May 17 13:43:00 2021 +0200 Merge pull request #3621 from cli/export-data Push data serialization concern into Exporter commit f2456f4820ca20f171ef6e2ed437e3460d13d03d Merge: 26b987aa b09c1f7a Author: Mislav Marohnić Date: Mon May 17 12:47:54 2021 +0200 Merge pull request #3628 from cli/json-flag-completion Add shell completion for valid `--json` flag values commit 26b987aaf5c6b1f020a94c2e41e7d08feb6b6ee5 Merge: 02b7a717 fddc888a Author: Mislav Marohnić Date: Mon May 17 12:43:55 2021 +0200 Merge pull request #3626 from cli/json-color-gray Fix "null" display in colored JSON output commit d656a9077b35fea1e6aeaa0347c2214a9a29001d Author: Ben Date: Sat May 15 21:40:43 2021 -0700 cleaned up the code base, opening the web browser is now matching the style of th e repo. We also made a dialogue in our helpful-resources.txt about what we will be doing next. Big things to comegit add . commit 68ce66801b5fb076e449d30c3dcb2867d7cd47b9 Author: Ben Date: Sat May 15 21:09:13 2021 -0700 We got the browser to open on gh browse, now we're working towards making it specific to the argument passed in. Mini wingit add .! commit e905133068af4447f20839ba54784acb9df4c9db Author: Ben Date: Sat May 15 14:47:50 2021 -0700 working towards removing the help message when browse is inputted commit 88ef7ce58b541b5f93135d9d3ffc6cb649324a62 Author: Ben Date: Sat May 15 12:08:51 2021 -0700 removed view, from here on out exclusively working in browse to do most of the logic commit 602334f58aecee86c1e65420992877171bb5a3a4 Author: bchadwic Date: Wed May 12 16:02:14 2021 -0700 working on the browse view commit c42f6acbdefd541894707b79d6f03345e0ac1712 Author: bchadwic Date: Wed May 12 14:54:18 2021 -0700 begun a view subcommand of browse commit 9333d93c2f567dcf49481800c763813dff400e8b Author: bchadwic Date: Wed May 12 14:52:01 2021 -0700 started creating the new command. Added useful resources commit b09c1f7a6f54651353e95a5fc59af9f1aab671d9 Author: Mislav Marohnić Date: Wed May 12 17:35:17 2021 +0200 Add shell completion for the `--json` flag commit df2ae17b548a6c1e38190bafb3edbbe9d02475dd Author: Mislav Marohnić Date: Wed May 12 17:35:02 2021 +0200 Bump Cobra to v1.1.3 commit 02a2ed2f73789e56963e46a52b07a3ec8a840c72 Author: Mislav Marohnić Date: Tue May 11 20:00:36 2021 +0200 Add `repo view --json` export functionality commit 5f0301c990bcac47cfdb36273032050da40fcaca Author: Mislav Marohnić Date: Tue May 11 18:25:20 2021 +0200 Have Exporter.Write automatically call ExportData on given data structure commit fddc888a69a8ab6135dc0ec0516df670bed363b2 Author: Mislav Marohnić Date: Wed May 12 16:56:52 2021 +0200 Fix "null" display in colored JSON output "null" was previously rendered in "bright black", an ANSI color that is not guaranteed to be visible at all depending on the terminal. Switch the color to cyan to ensure that "null" is visible. commit 02b7a7178336628311b75fb59e60f33a6594ab65 Author: Mislav Marohnić Date: Tue May 11 21:21:57 2021 +0200 Add project layout documentation (#3587) commit 26d2e5c5cef62b3667eb2f05051a7b17a9fbe9e4 Author: Mislav Marohnić Date: Tue May 11 17:08:28 2021 +0200 Rework our pull request template (#3584) commit 3cbd5b49346378136f5e85601c42882cf0ef4d06 Author: Mislav Marohnić Date: Mon May 10 17:09:03 2021 +0200 Add `repo fork --org` functionality (#3611) Co-authored-by: Gowtham Munukutla commit c50d390cf52c7726368f9928cadf9f0266a32814 Author: Mislav Marohnić Date: Fri May 7 22:09:58 2021 +0200 Fix tests commit 026b07d1cfbf23a3ccb4d4703a2496d65e177fcc Merge: 2f94adab 70a96219 Author: Mislav Marohnić Date: Fri May 7 14:21:20 2021 +0200 Merge pull request #3578 from g14a/fix/empty-gist-contents Add validation to gists if contents are empty commit 70a9621928bfc9c66ffc057938ab27a928cc0820 Author: Mislav Marohnić Date: Fri May 7 13:50:33 2021 +0200 :nail_care: cleanup in gist create commit cc94dc762d14eeb54e13cb184a9c55c2234a3b20 Author: Gowtham Munukutla Date: Tue May 4 18:50:27 2021 +0530 shift gist validation to server rather than client commit 2f94adabb2ddbda4cfbb717019714dca6f0a3fa1 Author: Cristian Dominguez <6853656+cristiand391@users.noreply.github.com> Date: Fri May 7 10:21:26 2021 +0000 Use `T.TempDir` for temporary dirs in tests (#3580) commit 97f80740aa98f65c29dedc9924a53aeaeede11a4 Author: Gowtham Munukutla Date: Fri May 7 11:17:23 2021 +0530 remove extra quotes commit 0437a699676116c30011a768586a25401197f8b1 Author: Gowtham Munukutla Date: Tue May 4 10:51:07 2021 +0530 add column headers and age column in listing runs commit 25d79c4e16d54d6f3621517269f07048748a6b97 Merge: 011e455b 796d2e24 Author: Mislav Marohnić Date: Mon May 3 21:04:35 2021 +0200 Merge pull request #3525 from cristiand391/improve-issue-status-detection Improve issue status detection commit 796d2e24ef06266a343244dc6f78ef5a39f94f93 Author: Cristian Dominguez Date: Fri Apr 30 17:32:16 2021 -0300 Bring back `Closed` field commit 9bdc63c4ca6de084d5a9db3b068aa6314e1e81e7 Author: Mislav Marohnić Date: Wed Apr 28 19:25:27 2021 +0200 Eliminate API overfetching in `pr` commands This completely rewrites the PR lookup mechanism so that the caller must specify the GraphQL fields to query for each PR. Additionally, this fixes some export problems with `pr view --json`. Features: - Each pr command now gets assigned a concept of a Finder. This makes it easier to stub the PR in tests without having to stub the underlying HTTP calls or git invocations. - `pr view --web` is much faster since it only fetches the "url" field. - `pr diff 123` now skips a whole API call where a whole PR was unnecessarily preloaded just to access its diff in a subsequent call. - PullRequestGraphQL query builder is now used to construct queries. - A bunch of individual commands are now freed of having to know about concepts such as BaseRepo, Branch, Config, or Remotes. commit 011e455b7332ecb34a5a1aa6a17450b70968264f Merge: d478a652 6a57dcfd Author: Mislav Marohnić Date: Fri Apr 30 14:29:10 2021 +0200 Merge pull request #3536 from rneatherway/rneatherway/placeholder-syntax Support standard path variable replacement syntax commit 6a57dcfd7dde07908676646dcea3021ad14c5a78 Author: Mislav Marohnić Date: Fri Apr 30 14:19:26 2021 +0200 :nail_care: cleanup placeholder implementation commit 59b4d5cb7c0a3696843cf0b06db07efd8bb86e5f Author: Robin Neatherway Date: Thu Apr 29 16:31:31 2021 +0100 Support standard path variable replacement syntax Add support for the following synonyms: {owner} for :owner {repo} for :repo {branch} for :branch commit d478a652548ce335db9effcf5dac9d524c178a41 Merge: 6b9a8608 b586d517 Author: Mislav Marohnić Date: Fri Apr 30 11:57:01 2021 +0200 Merge pull request #3530 from tklauser/x-term Use golang.org/x/term commit 6b9a8608f6211f9f46f745b610021492c79c3482 Merge: d1d49c18 9f451d9e Author: Nate Smith Date: Thu Apr 29 16:11:58 2021 -0500 Merge pull request #3537 from cli/incorrect-function catch mintty error and add help for it commit 9f451d9eef69aa6488d4514a10c664811ef08c08 Author: nate smith Date: Thu Apr 29 16:06:26 2021 -0500 review feedback commit f4592e3f94807382d6c1a48807c3902562f6d3b8 Author: nate smith Date: Thu Apr 29 16:01:15 2021 -0500 Revert "ignore gh.exe" This reverts commit 15c16bd12aa37bd5207adc6b904e89ab459b0eb9. commit 15c16bd12aa37bd5207adc6b904e89ab459b0eb9 Author: nate smith Date: Thu Apr 29 10:59:56 2021 -0500 ignore gh.exe commit 9110db7f7e8f46c516c65ca464fba1a70acf27cb Author: nate smith Date: Thu Apr 29 10:58:40 2021 -0500 fill in help topic commit d1d49c18108e2cf5da4bf0c00a3b41bc6ef85d3c Merge: d5954e2e a8e02529 Author: Mislav Marohnić Date: Thu Apr 29 11:52:15 2021 +0200 Merge pull request #3529 from cli/milestone-export-fix Fix exporting `milestone` for issues and PRs commit 02e9fa086d730237387a5abccffc2360444d1cf6 Author: nate smith Date: Wed Apr 28 13:59:54 2021 -0500 start on incorrect function error handling commit d5954e2e9436c19575bbbf5f78445466dadd423c Merge: cc7c2f2c 3e365962 Author: Nate Smith Date: Wed Apr 28 13:28:33 2021 -0500 Merge pull request #3499 from cli/secret-prompt tweak secret set to allow prompting commit cc7c2f2c9d28ac532e67249b816d38c6a35d88d8 Merge: 9436990e 00da7f9f Author: Nate Smith Date: Wed Apr 28 13:27:04 2021 -0500 Merge pull request #3517 from cli/watch-404 handle 404 for annotations commit b586d517786282065737f3eb8d9dc2ca126c814c Author: Tobias Klauser Date: Wed Apr 28 18:44:36 2021 +0200 Use golang.org/x/term The golang.org/x/crypto/ssh/terminal package is deprecated and merely a wrapper around golang.org/x/term. Use the latter directly. commit a8e025291f629f0f8dc31f9d6068e89f5dfeaad5 Author: Mislav Marohnić Date: Wed Apr 28 18:28:18 2021 +0200 Fix exporting `milestone` for issues and PRs There was a weird pointer bug which would cause a null milestone to erase "milestone" fields for previous entries in the list. commit 6b49e21295c992f5159384cda4c3dd930bb56d19 Author: Cristian Dominguez Date: Tue Apr 27 16:23:11 2021 -0300 Improve issue status detection commit 9436990e1812ee632a4b1784c53fb7b7aaaf9502 Merge: ac0fe6bf f70bdcf9 Author: Mislav Marohnić Date: Tue Apr 27 18:38:23 2021 +0200 Merge pull request #3524 from sgerrand/cmd-docs-updates Fixes typo in `pr create` docs commit f70bdcf98263e979e9fcb46407ab61af78cc25ea Author: Sasha Gerrand Date: Tue Apr 27 11:57:42 2021 +0100 Corrects a typo in `pr create` docs commit 00da7f9fc1244dcdac71e613d703fbe412e3e861 Author: vilmibm Date: Mon Apr 26 16:55:08 2021 -0500 handle 404 for annotations commit ac0fe6bf715537a5fb9b99f80344ea098134a335 Merge: 5a2ec546 aaa5a9e9 Author: Nate Smith Date: Mon Apr 26 16:40:07 2021 -0500 Merge pull request #3490 from heaths/issue3487 Optionally read stdin for `gh alias set` commit aaa5a9e9495f71d75f7fc86c94f49ef7c71cb6c9 Author: Heath Stewart Date: Fri Apr 23 17:23:27 2021 -0700 Use `-` to read from stdin instead Resolves PR feedback. commit 3e36596269adde7a2e62910ae33c2387c90b30f2 Author: vilmibm Date: Fri Apr 23 13:07:20 2021 -0500 ability to paste secrets in a prompt commit 5a2ec54685806a6576bdc185751afc09aba44408 Merge: bf7ed68a c69f2108 Author: Nate Smith Date: Fri Apr 23 11:51:07 2021 -0500 Merge pull request #3494 from cli/pr-list-example add some more examples for pr list commit bf7ed68aa8f1103515e9a70d51076bb46738f385 Merge: 9ee580de 47ed41bf Author: Nate Smith Date: Fri Apr 23 11:50:46 2021 -0500 Merge pull request #3495 from cli/release-create-eg add some more examples for release create commit 47ed41bfcddfafc1c7c57882d3ea655f4f501675 Author: vilmibm Date: Thu Apr 22 15:26:26 2021 -0500 add some more examples for release create commit c69f21080733bd2904cbf7e727680f62d46cf899 Author: vilmibm Date: Thu Apr 22 14:45:19 2021 -0500 add some more examples for pr list commit 9ee580de867c3a2bbeeac7fafc0a145274ad1f75 Merge: 5821065a c57e30ff Author: Mislav Marohnić Date: Thu Apr 22 13:33:40 2021 +0200 Merge pull request #3491 from junjieyuan/trunk using filepath.Join() instead of path.Join() to fix wrong filepath on Windows commit c57e30fff0d5d0aee204610466a8a295ad865ba7 Author: Mislav Marohnić Date: Thu Apr 22 11:42:51 2021 +0200 Fix stubbing config in tests on Windows commit c6c3e72f4399e678086516bc10bf339986fc9d23 Author: Mislav Marohnić Date: Thu Apr 22 11:42:28 2021 +0200 Unexport StubConfig commit 927e4c7e4df255b4091a3301875bb190a4e08f04 Author: Junjie Yuan Date: Thu Apr 22 15:10:35 2021 +0800 using filepath.Join() instead of path.Join() to fix wrong filepath on Windows: PS C:\Users\Junjie Yuan> gh auth status github.com ✓ Logged in to github.com as junjieyuan (C:\Users\Junjie Yuan\.config\gh/hosts.yml) ✓ Git operations for github.com configured to use https protocol. ✓ Token: ******************* Signed-off-by: Junjie Yuan commit bd2738379b63b1d4924562bc9fb156531336fa36 Author: Heath Stewart Date: Wed Apr 21 22:16:41 2021 -0700 Optionally read stdin for `gh alias set` Resolves #3487 commit 5821065ac0e7e52fc688e249a99210ec85773fe6 Merge: f65af147 f4eb60d7 Author: Nate Smith Date: Tue Apr 20 12:13:59 2021 -0500 Merge pull request #3419 from zxaos/patch-1 add Debian install variant without add-apt-repository commit f65af1473e6fdf7ee226117a86828214f0a1c979 Merge: 67d179ee 388075d5 Author: Nate Smith Date: Tue Apr 20 11:54:00 2021 -0500 Merge pull request #3438 from invakid404/trunk Add Funtoo Linux install instructions commit 67d179ee8400f17af999329280e630ae04504460 Merge: a3a7dead 929b5840 Author: Nate Smith Date: Tue Apr 20 11:52:38 2021 -0500 Merge pull request #3465 from darkRaspberry/patch-1 linuxbrew details added in linux installation docs. commit a3a7deadb83cfed8f37aca07c72744f7849f1b9d Merge: 47b79877 42b51819 Author: Mislav Marohnić Date: Tue Apr 20 18:49:41 2021 +0200 Merge pull request #3400 from embano1/patch-1 Add note on supported value types in raw-fields commit 47b7987772a86ea81617403675e596a0b6214927 Merge: d0989646 b656b560 Author: Mislav Marohnić Date: Tue Apr 20 18:48:19 2021 +0200 Merge pull request #3402 from cristiand391/remove-unused-embedded-struct Remove unused embedded struct commit 929b58400315751e8f6bd2d32cb673c47b35e6aa Author: Nate Smith Date: Tue Apr 20 11:47:22 2021 -0500 Update docs/install_linux.md commit 2872e79a4fd058d5b3ff6ad8ad41ebb31ab19a9a Author: Nate Smith Date: Tue Apr 20 11:47:17 2021 -0500 Update docs/install_linux.md commit d09896468d61baed5de5fb312496aada5434f81f Merge: 882c0810 a8921162 Author: Nate Smith Date: Tue Apr 20 11:41:16 2021 -0500 Merge pull request #3351 from cristiand391/fix-pr-reopen Fix detecting PR status when passing branch as arg commit 882c08104da7104c24af9be972abfdea6797b7f7 Merge: 754dc109 a2ff97d7 Author: Nate Smith Date: Tue Apr 20 11:39:48 2021 -0500 Merge pull request #3472 from cli/issue-create-web-fix Fix `issue create --web` commit 754dc109d0ecac40b08a423ec5d6d670935a072e Merge: aea6163a 9ebcca70 Author: Nate Smith Date: Tue Apr 20 10:30:11 2021 -0500 Merge pull request #3468 from cli/actions-remote include magic repo resolution magic for workflow and run commit aea6163a833e50c93eee518da7b91a69821c361c Merge: a1630968 75227f44 Author: Sam Date: Tue Apr 20 08:13:26 2021 -0700 Merge pull request #3445 from adamslc/log_fix Fix `run view --log` when steps have slashes commit 75227f44d17a2e30805e7a4ac4ee43e862810e8e Author: Sam Coe Date: Tue Apr 20 08:05:19 2021 -0700 linter commit a163096809969a47504623c187f6ea9452d07904 Merge: ae99ad4f f18929cf Author: Mislav Marohnić Date: Tue Apr 20 14:02:53 2021 +0200 Merge pull request #3462 from cli/lint-timeout-increase Increase linter timeout from 1min to 3min commit a2ff97d73f6758a3b9d992cb629759c0c7d04100 Author: Mislav Marohnić Date: Tue Apr 20 13:50:01 2021 +0200 Fix `issue create --web` commit ae99ad4fbe844dc100797c13d7732a38b5ced421 Merge: e99239c6 ac348b0d Author: Mislav Marohnić Date: Tue Apr 20 11:31:23 2021 +0200 Merge pull request #3461 from cli/jobs-url-enterprise Fix requesting REST sub-resources on GHE commit e99239c6b712edfa0427f485a6f00c9765ee99eb Merge: 057f5f26 17805c8c Author: Sam Date: Mon Apr 19 15:27:54 2021 -0700 Merge pull request #3439 from kou029w/bump-survey Bump AlecAivazis/survey commit 9ebcca70828613088534377c825a4f77d93cd6e9 Author: vilmibm Date: Mon Apr 19 16:23:49 2021 -0500 include magic repo resolution magic for workflow and run commit c77c7ec7c9f897bf8650f4ee4d11d58702b3d9c0 Author: Sam Coe Date: Mon Apr 19 11:35:20 2021 -0700 Match logs based on job name and step number commit 057f5f26312c4b0d3357cde7beeef2893d9ea06c Merge: 09b09810 8458c5f9 Author: Nate Smith Date: Mon Apr 19 13:22:49 2021 -0500 Merge pull request #3432 from cli/actions-int64 use int64 explicitly in Actions support commit 58d3aa878da296233c2ee15dbd409824a8f2327e Author: Luke Adams Date: Mon Apr 19 10:59:36 2021 -0600 Extract filename creation logic to seperate function commit d479449ec5af2c3f55b8124f953104686ebafbe3 Author: Abhay Kumar Verma <56693429+darkRaspberry@users.noreply.github.com> Date: Mon Apr 19 19:58:37 2021 +0530 linuxbrew details added commit f18929cf3d9c85ba6ab82b050e7281152f4e6ed7 Author: Mislav Marohnić Date: Mon Apr 19 12:53:52 2021 +0200 Increase linter timeout from 1min to 3min Hopefully avoids CI failures like https://github.com/cli/cli/runs/2379956774 commit ac348b0dec50b5dff69f9743ea43e45c6a5e08d9 Author: Mislav Marohnić Date: Mon Apr 19 12:08:57 2021 +0200 Fix requesting REST sub-resources on GHE GitHub REST resources typically return full URLs to fetch related resources at. We used to parse those URLs to find just the path portion and pass that in to the `REST()` function, which only accepted paths. By doing so, we are essential de-constructing a URL just to re-assemble it again. While re-assembling it for Enterprise, though, we would accidentally inject an extra `api/v3/` prefix where one was not needed. The solution is just to use raw URLs as reported by the REST API with no modifications. This extends the `REST()` function to accept full URLs in addition to just paths to resources. commit 0822e6b4edae0c5077a6e4ca5c29103781c72692 Author: Luke Adams Date: Fri Apr 16 23:37:24 2021 -0600 Fix run view --log when steps have slashes commit 17805c8cd5eed3fc020e06baa323a7353acef97d Author: Kohei Watanabe Date: Fri Apr 16 21:01:23 2021 +0900 Bump AlecAivazis/survey commit 388075d55a38adc324e553e5a37ed961573e1bbc Author: invakid404 Date: Fri Apr 16 11:36:33 2021 +0300 docs: add funtoo linux install instructions commit 8458c5f95d3d54ee0aaa59cc0c5d99d3a6808488 Author: vilmibm Date: Thu Apr 15 13:32:09 2021 -0500 use int64 explicitly in Actions support commit 09b09810dd812e3ede54b59ad9d6912b946ac6c5 Merge: bd663b50 01505daa Author: Sam Date: Thu Apr 15 10:33:49 2021 -0700 Merge pull request #3403 from cli/update-glamour Update glamour to version which includes emoji support commit bd663b50491c2eef4a40e7d8eea3f29d245d0e2f Merge: 31cccb46 a2f4f725 Author: Nate Smith Date: Thu Apr 15 11:21:17 2021 -0500 Merge pull request #3429 from cli/log-fail-cache hotfix: create cache dir in run view commit a2f4f725af9c9b5626508192a7631bad7d3ace10 Author: vilmibm Date: Thu Apr 15 11:15:03 2021 -0500 create cache dir commit 31cccb4604053dc966d2b67ca3112fd944c26349 Merge: f4d96ee7 516ea869 Author: Mislav Marohnić Date: Wed Apr 14 20:15:29 2021 +0200 Merge pull request #3414 from cli/json-format Add `--json` export flag for issues and pull requests commit 516ea8691e74a991cb47c0adf0347e6820896864 Author: Mislav Marohnić Date: Wed Apr 14 20:04:47 2021 +0200 Fix whitespace formatting of `issue/pr view` help text commit 654bd29ca0810785b1fdccf9933a8c56497552f2 Author: Mislav Marohnić Date: Wed Apr 14 19:58:58 2021 +0200 Disallow unsupported values for `--json` flag commit 56cdbb643f180a672ad3fa004a7a891d1be4a2d1 Author: Mislav Marohnić Date: Wed Apr 14 19:41:55 2021 +0200 Fix `pr view` tests broken by `createdAt` → `submittedAt` switch commit 625505dcfbd53474302111deb76c823ca1cd3ea4 Author: Mislav Marohnić Date: Wed Apr 14 19:11:08 2021 +0200 Fix assigning null Exporter commit 7ec5b0f8cfce3f4cd1eb05ae0e2b454808bbd222 Merge: 78e48e2d f4d96ee7 Author: Mislav Marohnić Date: Wed Apr 14 18:52:34 2021 +0200 Merge remote-tracking branch 'origin' into json-format commit 78e48e2dfdecb907ee8c956514e7434a39f1830e Author: Mislav Marohnić Date: Wed Apr 14 18:52:02 2021 +0200 Tests for GraphQL query builder and JSON exporter commit e327b42f79a74876a8a67614b7c4a9c7006d42de Author: Mislav Marohnić Date: Wed Apr 14 18:27:15 2021 +0200 Add `gh help formatting` topic & link to it from commands with JSON output commit f4d96ee78976ae638e9bbe99f565ab515718f6fe Merge: a12cbe7c bca828be Author: Nate Smith Date: Wed Apr 14 11:22:58 2021 -0500 Merge pull request #3418 from cli/startup-failure actions wrap up commit bca828be2cc017a8a1aee50192486afc2fe54eda Author: vilmibm Date: Wed Apr 14 11:17:12 2021 -0500 placeholder consistency commit 3ad41e3e651647236ed4ece290afb12dbdc924bf Author: Mislav Marohnić Date: Wed Apr 14 18:14:51 2021 +0200 Change JSON Exporter to an interface commit e63904bacd8e0b8e77cefb373dc4901f6dbcfffc Author: Mislav Marohnić Date: Wed Apr 14 16:14:41 2021 +0200 Expose more fields for PR JSON export commit f4eb60d7a8bf6667d19e09e6f8e95d7de9742d75 Author: Matt Bond Date: Wed Apr 14 00:13:50 2021 -0400 add Debian install variant without add-apt-repository commit a12cbe7c016ae47bc42982dbca0550848d508c6b Merge: cadabb4e 8a0d5b0e Author: Nate Smith Date: Tue Apr 13 23:00:24 2021 -0500 Merge pull request #3412 from cristiand391/current-branch-help-text Add note about current branch detection commit e10a3f164f9569d7ab68d176458c3594ccc09dac Author: vilmibm Date: Tue Apr 13 22:43:02 2021 -0500 minor usage improvements commit a85ef9273fe496f0c3b1d03632444dd6357fbb45 Author: vilmibm Date: Tue Apr 13 22:42:38 2021 -0500 bump run list limit commit 04844256ddc1da0217425b46889c64998b3d19cc Author: vilmibm Date: Tue Apr 13 22:10:32 2021 -0500 annotation fixes commit cd8ec471637cf39d346754b73900c0ebb5449472 Author: vilmibm Date: Tue Apr 13 22:08:06 2021 -0500 unhide actions commands commit 4e281153f66f32649577620e356f5390022391d8 Author: vilmibm Date: Tue Apr 13 22:06:06 2021 -0500 incorporate wording feedback commit efe7aa1f784ad10f1821091d201759b20ac1611f Author: vilmibm Date: Tue Apr 13 22:03:59 2021 -0500 fix small bug with startup_failure conclusion commit a516ee68335c49f0bd54478fc34524f588040533 Author: Mislav Marohnić Date: Tue Apr 13 21:26:13 2021 +0200 Add `issue status --json` support commit 3f22e3b353dd6a334c615748f0b28061ea15119d Author: Mislav Marohnić Date: Tue Apr 13 21:12:30 2021 +0200 Add `pr status --json` support commit e158fac1a9649f538b890e4f4d4f25c8053433e3 Author: Mislav Marohnić Date: Tue Apr 13 20:54:09 2021 +0200 Restructure PullRequestStatus function commit 298ef8add5ebf8c93543ee3572a03eec41bb3826 Merge: abe452bb cadabb4e Author: Mislav Marohnić Date: Tue Apr 13 20:31:11 2021 +0200 Merge remote-tracking branch 'origin' into json-format commit abe452bb192acb99f20fc3bc836e4f0ebed5138f Author: Mislav Marohnić Date: Tue Apr 13 20:29:31 2021 +0200 Add `--json` export flag for issues and pull requests The `--json` flag accepts a list of GraphQL fields to query for and output in JSON format. To get the list of available flags, run the command with a blank value for `--json`. Additional `--jq` and `--template` flags are available just like in `gh api`. commit 19ea49b5a9bbf2cf4fb693eeb0299ff6bd9dc66a Author: Mislav Marohnić Date: Tue Apr 13 19:11:13 2021 +0200 Move issue list queries to under the `issue/list` package commit cadabb4e6d0bb48c7d4c99618a93acd58b12297e Merge: 93b5bf20 f8c7fd1d Author: Nate Smith Date: Tue Apr 13 12:23:30 2021 -0500 Merge pull request #3413 from cli/run-download-fix Fix extracting workflow artifact to a relative path commit f8c7fd1d2834011bc72ace326477f78d881f349a Author: Mislav Marohnić Date: Tue Apr 13 19:15:14 2021 +0200 Fix extracting workflow artifact to a relative path To prevent zipslip, we verify that each extracted file would fall strictly under the prefix of the path to extract to. However, this yielded a false positive when extracting to `.`, which is the default for downloading a single archive. commit 8a0d5b0e43cef52d64e6dd2c5c6cbb4238cebe77 Author: Cristian Dominguez Date: Tue Apr 13 12:30:07 2021 -0300 Add note about current branch detection commit 61a8049592c3764ad3be90a3075f95e8f72dd95b Author: Mislav Marohnić Date: Tue Apr 13 16:48:21 2021 +0200 Extract JSON filtering functionality from `gh api` commit 93b5bf20ebf5b8d4e7cc5f917190d5b92527750e Author: Mislav Marohnić Date: Tue Apr 13 11:10:13 2021 +0200 Fix secrets in PR automation being available to PR from forks commit a579b00daca5e258927a3cacb9487d7f0b7936a3 Merge: 861fd5b5 4dd8a44f Author: Nate Smith Date: Mon Apr 12 18:44:07 2021 -0500 Merge pull request #3408 from cli/fix-run-selection Make run selection unique commit 4dd8a44ff152c8bb891d6a0cf5ed582c117015af Author: Sam Coe Date: Mon Apr 12 16:27:51 2021 -0700 make run log cache key unique commit 1ddebf6396deb724d88f9f3971b4012fd8d70898 Author: Sam Coe Date: Mon Apr 12 16:03:50 2021 -0700 Fix trying to read from non-existent log file commit 3f3c8f2b26cd51952d7d8e8ab809d59e4cf18d79 Author: Sam Coe Date: Mon Apr 12 15:22:30 2021 -0700 Add time since run to run selection survey options commit 861fd5b5e388df849783b49fda2d3e5a50c63217 Merge: 77de8e93 0b794710 Author: Nate Smith Date: Mon Apr 12 17:48:51 2021 -0500 Merge pull request #3407 from skedwards88/patch-1 Use `--exit-status` instead of `-e` in example commit 0b79471024a804bc0ee3510e370db7d97f205886 Author: Sarah Edwards Date: Mon Apr 12 15:16:12 2021 -0700 Use `--exit-status` instead of `-e` in example commit 77de8e935627470f0e38edf9847d131b0e33f110 Merge: 0e7a6579 c724070c Author: Nate Smith Date: Mon Apr 12 15:57:17 2021 -0500 Merge pull request #3405 from cli/gh-actions make gh actions output Real commit c724070cbe92b806e5cfdc6125d880d1f399d09a Author: vilmibm Date: Mon Apr 12 15:51:00 2021 -0500 just check nil commit 2b17de80f67b4a6cdbfdd2825ed810c4aa0475c4 Author: vilmibm Date: Mon Apr 12 14:38:45 2021 -0500 workflow view commit 9a0193b77c21f72e7ce4f0d1e34b46558eafe0a3 Author: vilmibm Date: Mon Apr 12 14:28:58 2021 -0500 add gh run download commit cb03a3a7761e092abf96a16f05c70347c76d9abc Author: vilmibm Date: Mon Apr 12 14:17:05 2021 -0500 linter appeasement commit 185f6242b957f1167adc183f9c7ee7e414fdd479 Author: vilmibm Date: Mon Apr 12 13:58:40 2021 -0500 make gh actions output Real commit 0e7a6579846e842ac407b75fc908a94bb9be4749 Merge: a9bebff0 eb66ee9d Author: Nate Smith Date: Mon Apr 12 12:12:11 2021 -0500 Merge pull request #3404 from cli/on-array handle array type for on: in workflow file commit eb66ee9dfe7b8efb33eba462ec769cb5d499f905 Author: vilmibm Date: Mon Apr 12 12:05:05 2021 -0500 handle array type for on: in workflow file commit 01505daaf569029bc174403e93e02801073c9153 Author: Sam Coe Date: Mon Apr 12 09:56:00 2021 -0700 remove previous emoji workaround commit dafa2f61c9cd9028d73b45aca89a75be1d68a449 Author: Sam Coe Date: Mon Apr 12 09:50:42 2021 -0700 Enable emoji in markdown commit 21c4f1498e556cedb41c1da6408676d986efc459 Author: Sam Coe Date: Mon Apr 12 09:45:14 2021 -0700 go mod tidy commit e7776cc9061f4c885b84d132a6f51137161bc59d Author: Sam Coe Date: Mon Apr 12 09:29:38 2021 -0700 update glamour to version which includes emoji support commit b656b56061cb2d49f23eddbb7e2bcb0f59239b2c Author: Cristian Dominguez Date: Mon Apr 12 12:25:33 2021 -0300 Remove unused embedded struct commit 42b518191c1e28d782a069d743407df4932f7293 Author: Michael Gasch Date: Mon Apr 12 09:53:50 2021 +0200 Add note on supported value types in raw-fields Proposing a slight amendment to the `gh api` field docs to clarify the current limitation around "complex" field values, e.g. arrays/objects. Related: #1484 Signed-off-by: Michael Gasch commit a9bebff03fc51d8b0f7410ecace25e0cc76e7b78 Merge: bc681b94 d4372062 Author: Mislav Marohnić Date: Mon Apr 12 09:33:59 2021 +0200 Merge pull request #3383 from cli/build-env Tweak build scripts to enable cross-compiling commit bc681b94804c55066926b0522ba5057a12137ab1 Merge: 76b0ff7a de0e53b1 Author: Nate Smith Date: Fri Apr 9 16:14:08 2021 -0500 Merge pull request #3303 from cli/workflow-run gh workflow run commit 76b0ff7a6d3cf93f441d913a6e231fa663a939ef Merge: d78e215c 33d60174 Author: Sam Date: Fri Apr 9 13:07:40 2021 -0700 Merge pull request #3379 from cli/failed-logs Add flag log-failed to display only logs of failed steps commit de0e53b18f702a80714f378ba145bf726b0e0975 Author: vilmibm Date: Fri Apr 9 14:33:43 2021 -0500 correct Examples commit d89993720dee6ac4bc972a1839540803f871f2b1 Author: vilmibm Date: Fri Apr 9 14:32:32 2021 -0500 add specific unit tests for findInputs commit 33d601746744c3d2714bbbbed9f57007e4fe719a Author: Sam Coe Date: Fri Apr 9 12:16:31 2021 -0700 linter commit 8a4a8dd451b90ca0d6ac6000043f404c91de50df Author: Sam Coe Date: Fri Apr 9 12:13:01 2021 -0700 Moar memory commit 6c92f2fa603a44eef7a6b496f12481173f499636 Author: vilmibm Date: Fri Apr 9 13:24:19 2021 -0500 review feedback commit 5b7f8fd9eb2a0a7a02e2f124fbd9092d8c50be09 Author: vilmibm Date: Fri Apr 9 13:20:00 2021 -0500 review feedback commit 4a610f13cf777ef7e5788a411dec322e0b1fe62f Author: Sam Coe Date: Fri Apr 9 09:19:15 2021 -0700 tweak wording commit d43720620e13a682fe6a060b518073783f39db98 Author: Mislav Marohnić Date: Thu Apr 8 21:11:15 2021 +0200 Tweak build scripts to enable cross-compiling The main build script for this project is `script/build.go` which implements Makefile-like building of the `gh` binary and associated man pages. Our Makefile defers to the Go script. However, when setting GOOS, GOARCH, and other environment variables to modify the target for the resulting binary, these environment variables would affect the execution of `build.go` as well, which was unintended. This tweaks our Makefile to reset variables like GOOS and GOARCH when building the `build.go` script itself, ensuring that the built script runs on the same platform, and adds the ability to pass environment variables as arguments to `go run script/build.go`. This allows the following usage on platforms without `make`: go run script/build.go GOOS=linux With this style of invocation, the GOOS setting does not actually affect `go run` itself; just the `go build` that is executed in a child process. commit 6b2b7922411301f786397c715a4223d19f47d9c6 Author: Sam Coe Date: Thu Apr 8 15:57:57 2021 -0700 linter commit dc63480cf6b5e53303b69a62a4e895ca55db4277 Author: Sam Coe Date: Thu Apr 8 11:29:55 2021 -0700 Add flag log-failed to display only logs of failed steps commit 48e162fa051c19ec12b99e1930a924bae3a57f9f Author: vilmibm Date: Thu Apr 8 16:11:21 2021 -0500 share workflow content getting code commit 1b5eb30575fa405a4d15e26ab0404f3e8f13ab27 Author: vilmibm Date: Thu Apr 8 12:04:33 2021 -0500 review feedback - Switch to -f/-F instead of -- (this lead to some `gh api` copypasta) - Remove --json - Don't parse workflow YAML if not collecting interactively - Share GetWorkflowContent - Fix a parsing issue commit f80268d96658f5b5d27b24048062767ce68070ef Author: vilmibm Date: Thu Apr 8 16:21:06 2021 -0500 gh workflow run commit a8921162b15ff3465b8c49d0b08f7711ca9f283a Author: Cristian Dominguez Date: Thu Apr 8 00:27:52 2021 -0300 Update tests commit 9c471fe7a5f7cdf1995995a7b75abfd0b45cadc5 Author: Cristian Dominguez Date: Thu Apr 8 00:23:22 2021 -0300 Improve PR status detection commit d78e215c19df2d71ac9f3361026651ecb1c133c4 Merge: 35c55fd7 ed0ef6ad Author: Nate Smith Date: Wed Apr 7 20:20:52 2021 -0500 Merge pull request #3368 from cli/display-all-the-logs Display all run logs commit ed0ef6ad14083d22fb9749cc3b1381158a5a03d8 Merge: 7f021413 35c55fd7 Author: Nate Smith Date: Wed Apr 7 20:12:47 2021 -0500 Merge branch 'trunk' into display-all-the-logs commit 7f021413644f7630e604f512c868f85e39ace1fb Author: Nate Smith Date: Wed Apr 7 19:55:57 2021 -0500 obsolete TODO commit c2375205f4ca23c79c59b72a3814cf0ed706d8ae Author: Nate Smith Date: Wed Apr 7 19:55:44 2021 -0500 golf commit 35c55fd72e799ffaedd560b6f1c9d9b3c4b21f5d Merge: b6277e7c 878bdb8d Author: Nate Smith Date: Wed Apr 7 14:55:08 2021 -0500 Merge pull request #3349 from cli/artifact-download Add `run download` command for fetching workflow artifacts commit 878bdb8d500eecbfd0279fcbd157139b81bbbadb Author: vilmibm Date: Wed Apr 7 14:48:28 2021 -0500 add an example commit 9b5e92e64ad47c6d1b1c8ece2b428ffeea7a1639 Author: vilmibm Date: Wed Apr 7 14:47:12 2021 -0500 make test windows friendly commit 026dc4657a2ca768b039f726ebd7ea32d1893903 Merge: a449a1a6 b6277e7c Author: Mislav Marohnić Date: Wed Apr 7 20:26:31 2021 +0200 Merge remote-tracking branch 'origin' into artifact-download commit a449a1a6f727368ae597c9789a201800899d756c Author: Mislav Marohnić Date: Wed Apr 7 20:22:30 2021 +0200 Add tests for artifact rendering in `run view` commit 6ce12c07f6ecc304ba0efb6bae10fcb5c5056c46 Author: Mislav Marohnić Date: Wed Apr 7 20:08:13 2021 +0200 Move `Artifact` to the "shared" package commit 0e94de1ce61ff4bcb46c74584bb2164b20e408d9 Author: Mislav Marohnić Date: Wed Apr 7 19:56:28 2021 +0200 Address `run download` feedback - With no arguments in TTY mode, prompt which artifacts to download - Change `--pattern` argument to be just `--name` and only do exact matching - For multi-archive downloads, prefix the destination path with the name of the artifact - Add tests exercising HTTP functionality - Avoid "zipslip" path injection when extracting ZIP files - Add tests for ZIP extraction commit a4a41c23e6e9ba524d9daf57091aea89efc642c7 Author: Mislav Marohnić Date: Wed Apr 7 19:55:09 2021 +0200 Have StringSet preserve original order of values commit b6277e7ce2151f9e6a46204abd4384c425b5113b Merge: b2e32a50 65524f1e Author: Nate Smith Date: Wed Apr 7 12:30:22 2021 -0500 Merge pull request #3324 from cli/run-watch gh run watch commit 65524f1ea807991b1ad0891c418b42c287d2dece Author: vilmibm Date: Wed Apr 7 11:45:49 2021 -0500 review feedback commit 5cb4ece75420b22c566bca6117fb86e188dce06d Author: Sam Coe Date: Wed Apr 7 10:10:11 2021 -0700 Renaming and cleanup commit 67e45f1bceddb3dc532da047994a08be0e2bd1ab Author: Sam Coe Date: Tue Apr 6 11:36:29 2021 -0700 Display all run logs commit c8e481e165271cc8238e87cc4a75e18ea1578e38 Author: vilmibm Date: Fri Apr 2 12:37:01 2021 -0500 gh run watch commit be5dab8166948b3a11898861834acefa8e9077a6 Author: Cristian Dominguez Date: Mon Apr 5 18:57:14 2021 -0300 Add test case commit c8d1d6e8b4635a6d07b78e8daad83716762e5413 Author: vilmibm Date: Mon Apr 5 15:39:14 2021 -0500 sigh commit 97b15fea2d269cc579ea299a9af33e7c61a685ba Author: vilmibm Date: Mon Apr 5 15:33:31 2021 -0500 xplatform oops commit b5fc794b7843f148d7ea75f49dbf7d942af4d0bb Author: vilmibm Date: Mon Apr 5 14:37:21 2021 -0500 support --log for runs commit b2e32a508d1b07132cd8a43b66c5b9fa18e5b35c Merge: 20c58f1a 71c2bc58 Author: Nate Smith Date: Mon Apr 5 14:21:44 2021 -0500 Merge pull request #3330 from cli/man-gen-test Add tests for manual pages generation commit b705b3d6ba0589ed8079e29b6fe5d90d8da76f68 Author: vilmibm Date: Mon Apr 5 14:11:06 2021 -0500 make ExitStatus reflect focused job commit 20c58f1a0b3efe5c99b2d818d667bdbabc3a3567 Merge: 211324d9 3f95e459 Author: Nate Smith Date: Mon Apr 5 13:41:00 2021 -0500 Merge pull request #3357 from cli/run-flags support --web in gh run view commit 3f95e4595fbf1cc813519f18b217d73b2cf48870 Author: vilmibm Date: Mon Apr 5 13:35:47 2021 -0500 tweak error text commit 211324d90e11b235dbe7381bc55943b9741ea8b8 Merge: 216cfb63 e15189f4 Author: Sam Date: Mon Apr 5 09:37:29 2021 -0700 Merge pull request #3337 from cli/workflow-view-2 Workflow view commit 1424e4a973d8770fa695a5e5b0e1c10aaf9d9684 Author: vilmibm Date: Sun Apr 4 21:33:27 2021 -0500 support --web when focusing a job commit 7fc9295786f100df848706f48ae66e37d1698659 Author: vilmibm Date: Sun Apr 4 21:27:20 2021 -0500 support --web for run view commit 60d22b7d6d6f6dfaa2e23d73fe9a5e02db3d28ae Author: Cristian Dominguez Date: Fri Apr 2 21:26:21 2021 -0300 Fix detecting PR status when passing branch as arg commit 8552cacf79d01fd5424713bce28db32f73a46295 Author: Mislav Marohnić Date: Fri Apr 2 21:04:31 2021 +0200 Fix test after merge commit 9c4afca00306453fed3be056567c4fe89b7becdb Merge: b41681cb 216cfb63 Author: Mislav Marohnić Date: Fri Apr 2 21:00:25 2021 +0200 Merge remote-tracking branch 'origin' into artifact-download commit b41681cbb702ac00abca910ddef7c756dc2ecfbc Author: Mislav Marohnić Date: Fri Apr 2 20:54:56 2021 +0200 Restore Go < 1.16 compatibility commit 51a0a27a6fbb8c6fe379d50b4e6edc0e5276538f Author: Mislav Marohnić Date: Fri Apr 2 20:52:41 2021 +0200 Add ARTIFACTS information to `run view` commit c54e3c9ca87782e4b60e2c1970e77ca6be93a76d Author: Mislav Marohnić Date: Fri Apr 2 20:41:44 2021 +0200 Add `run download` command for downloading workflow artifacts commit e15189f43ed1ddbb75ab164a8730700e78094a36 Author: Sam Coe Date: Fri Apr 2 10:16:09 2021 -0700 Address PR comments commit d1b153c6dc8ec0bb672eaa7c60a1714d990eb475 Author: Sam Coe Date: Fri Apr 2 09:43:33 2021 -0700 Yaml to YAML commit 5566289d1c986541188fc714623e430ce70d3d9e Author: Sam Coe Date: Mon Mar 29 13:41:01 2021 -0700 workflow view commit 216cfb631f6d1b34e7fc0529344fa367faee59c6 Merge: 9fe7326d 238a3714 Author: Nate Smith Date: Fri Apr 2 11:06:15 2021 -0500 Merge pull request #3333 from cli/run-rerun gh run rerun commit 9fe7326d46f8f9dc9a0a273ac6bbf0fdf20aaa49 Merge: 658004a6 cc8d55c5 Author: Nate Smith Date: Fri Apr 2 11:00:48 2021 -0500 Merge pull request #3338 from cli/merge-job-run-view absorb job view into run view commit 238a371477070ede7d7359c5643b6e6cff9c5f85 Author: vilmibm Date: Fri Apr 2 10:58:33 2021 -0500 no need to hide this commit 658004a6ef21c2e6beb1d96246deb0894548561e Merge: 01b3f0f1 ee687d7e Author: Sam Date: Fri Apr 2 08:56:28 2021 -0700 Merge pull request #3347 from cli/hostname-override Quick fix: respect default hostname when parsing `owner/repo` pairs commit ee687d7e7f810d74a28f0d10db285c8e812e7810 Author: Sam Coe Date: Fri Apr 2 08:42:19 2021 -0700 Add tests for quick fix commit 01b3f0f1cbc596995c3d04fa0104138b09aa17a6 Merge: a35d451b 8b1ed281 Author: Sam Date: Fri Apr 2 08:34:35 2021 -0700 Merge pull request #3341 from cli/actions-help-section Add section in help for actions commands commit a35d451b67e4cbd2e33a4f63fd7a2fc9a59804c7 Author: Mislav Marohnić Date: Fri Apr 2 15:31:22 2021 +0200 Fix PR automation workflow - Use AUTOMATION_TOKEN to get around "resource not available by integration". It looks like jobs triggered from community pull requests do not have permissions to write to our project. - Tolerate the "project already has the associated issue" error for staff as non-fatal. commit 69ca2dda4a493e634c0513bbb2edcbccee44c712 Author: Mislav Marohnić Date: Fri Apr 2 15:16:27 2021 +0200 Quick fix: respect default hostname when parsing `owner/repo` pairs This re-enables using GH_HOST to set a default hostname when supplying repo argument like `gh repo clone owner/repo`. commit 815ae7a22d0e733f6fd3c471c6c23aa3e1c6ea06 Merge: df588318 19c95cf3 Author: Mislav Marohnić Date: Fri Apr 2 11:44:16 2021 +0200 Merge pull request #3334 from g14a/fix/nontty-fork fix premature return while forking repo non interactively commit 8b1ed281215c9993c8a5594e1a2f3be667e2c262 Author: Sam Coe Date: Wed Mar 31 11:24:38 2021 -0700 Add section in help for actions commands commit 8a60ea3c628e41889f859fb526c99b0dfa8c5a5d Author: vilmibm Date: Tue Mar 30 11:46:23 2021 -0500 gh run rerun commit 1ddb33bf3c4d9ace2c9fb36648cfbd493ee6f270 Author: vilmibm Date: Tue Mar 30 16:31:27 2021 -0500 add GetRunsWithFilter commit 19c95cf35821e88105b6efd9f4a7bbc442c1f9c6 Merge: 5caf01c5 df588318 Author: Gowtham Munukutla Date: Thu Apr 1 10:49:22 2021 +0530 Merge branch 'trunk' of https://github.com/cli/cli into fix/nontty-fork commit 5caf01c5649c549aed0443bf90e67e9b9de6a9a4 Author: Gowtham Munukutla Date: Thu Apr 1 10:49:12 2021 +0530 add test for non tty fork commit cc8d55c51866a00e17b60163de16757bb312b560 Author: vilmibm Date: Wed Mar 31 13:16:16 2021 -0500 remove job command, update gh actions text commit 0d0ec847759f7f5b1a25fb582e37e2b8dba4b1ea Author: vilmibm Date: Tue Mar 30 22:35:39 2021 -0500 absorb gh job view into gh run view commit df5883186cb1a490ad8401cc6d55d2772f590eda Merge: 531b15c8 05c7c2bd Author: Mislav Marohnić Date: Wed Mar 31 20:21:52 2021 +0200 Merge pull request #3336 from cli/pr-checks-crash Treat unrecognized PR Checks statuses as "pending" commit 531b15c831314010f6c44c6ac5460c0a28f1dda7 Author: Mislav Marohnić Date: Wed Mar 31 18:39:00 2021 +0200 Fix pr automation workflow commit 8d3e910dece7f1a56e8aa628666349a40a446969 Merge: 6bbebcde a238d295 Author: Mislav Marohnić Date: Wed Mar 31 18:29:30 2021 +0200 Merge pull request #3193 from cli/prautomation Add workflow for automated PR linting commit 05c7c2bd1aeee25efbd158219691b9015bbdb8d8 Author: Mislav Marohnić Date: Wed Mar 31 18:21:45 2021 +0200 Treat unrecognized PR Checks statuses as "pending" Previously, unrecognized Checks statuses would crash the program. For the sake of supporting the "WAITING" status and for forward-compatibility, this treats any unrecognized status as "pending". commit 745b3fec17e19b2a7fcdb72995036f1b847ef0e0 Author: Gowtham Munukutla Date: Wed Mar 31 20:20:07 2021 +0530 fix premature return while forking repo non interactively commit 6bbebcdee5f0f26d1fc8925b7af34871f9b2de41 Merge: 23b25946 0ecb04c6 Author: Nate Smith Date: Tue Mar 30 14:51:59 2021 -0700 Merge pull request #3332 from cli/run-prompt-refactor small refactor around prompting for runs commit 0ecb04c687bd5c4c943f87e905c0de2d1a260102 Author: vilmibm Date: Tue Mar 30 15:48:42 2021 -0500 small refactor around prompting for runs commit 71c2bc5807bd6c311a2a67107b5d62f3509c9802 Author: Mislav Marohnić Date: Tue Mar 30 20:47:25 2021 +0200 Add tests for manual pages generation commit 23b2594662a51d9656542987ce5eb34dca5f63ad Author: Mislav Marohnić Date: Tue Mar 30 19:59:39 2021 +0200 Fix crash when generating man pages Fixes #3329 commit 79219c77414e2779423534062b745c18ca507c40 Merge: 6018c02d 57536e7b Author: Mislav Marohnić Date: Tue Mar 30 19:34:28 2021 +0200 Merge pull request #3271 from cristiand391/disable-preview-long-url Disable preview option in prompts if URL size is too long commit 57536e7b0de71986a63731df5c630aca3ec785e9 Author: Mislav Marohnić Date: Tue Mar 30 19:11:31 2021 +0200 :nail_care: cleanup URL length checking commit fb39c38c85c4d6de1112e32e6bd115227ce56a9a Author: Cristian Dominguez Date: Sat Mar 20 18:46:10 2021 -0300 Disable preview option in prompts if URL size is too long commit 6018c02dabd11c6e0551e3c275847b8151e84975 Merge: d22e72a3 43dac8fc Author: Mislav Marohnić Date: Tue Mar 30 18:53:23 2021 +0200 Merge pull request #3258 from g14a/feature/transfer-issue Feature/transfer issues commit 43dac8fc09928d11014adec62a781550891fdce7 Author: Mislav Marohnić Date: Tue Mar 30 18:21:23 2021 +0200 :nail_care: cleanup issue transfer commit 5db62046fd490e078b3e054704e56e686b268d01 Author: Gowtham Munukutla Date: Tue Mar 16 16:49:19 2021 +0530 Add `issue transfer` command commit d22e72a373f484244ab080c57eba912f69c3cbe5 Author: Mislav Marohnić Date: Tue Mar 30 18:39:37 2021 +0200 Fix tests broken by incompatible merges commit 44ae7ae3cff6d4e3ff009e5c54a6ad4c0b0b0b8d Merge: b70b0402 e53d02b6 Author: Mislav Marohnić Date: Tue Mar 30 16:54:59 2021 +0200 Merge pull request #3279 from cli/browser-refactor Pass web browser to each individual command commit b70b0402ebd9659b27e05d6dd3945b29f0ba5bba Merge: 63a47d03 fc2cdd99 Author: Mislav Marohnić Date: Tue Mar 30 16:54:13 2021 +0200 Merge pull request #3200 from cli/table-widths Ensure that table printer always uses all available width in the terminal commit 63a47d03340fad2c5b5c9e0e2bad9613b0904fcf Merge: 9ec1e21d 949df38d Author: Mislav Marohnić Date: Tue Mar 30 16:52:14 2021 +0200 Merge pull request #3295 from cli/labels-and-combinator BREAKING: lookup all issue/PR labels with "AND" instead of "OR" commit 9ec1e21d4cf21e0b1d0ab5526adab187444836c2 Author: Sam Date: Tue Mar 30 07:51:00 2021 -0700 Default to GHES host if only GHES is authenticated (#3286) commit 5a3641540fc1ddf1c8528e58c4865fd35cd6bef0 Merge: c48e5f55 a2251735 Author: Nate Smith Date: Mon Mar 29 16:59:37 2021 -0700 Merge pull request #3318 from cli/list-by-workflow support --workflow in run list commit c48e5f5591cfff1b0220d004a1b4338b4d563a6e Merge: 6dba073a 6f898dcb Author: Nate Smith Date: Mon Mar 29 16:59:27 2021 -0700 Merge pull request #3323 from cli/share-run-view Preemptively share code from gh run view commit 6f898dcbb32a3c16d0f9fa1c5d62bce2ee5e8acb Author: vilmibm Date: Mon Mar 29 16:37:42 2021 -0500 share prForRun commit e7fa99b70f77ad9916b0d929bb19ea40d40a01f1 Author: vilmibm Date: Mon Mar 29 16:34:17 2021 -0500 share annotation printing commit 43ab74a023728883ab33550a9902d1471ae8fe6d Author: vilmibm Date: Mon Mar 29 16:31:18 2021 -0500 share job rendering code commit 5d9a7825f8303934d19b9349a13d1da875b5369b Author: vilmibm Date: Mon Mar 29 16:17:12 2021 -0500 share run header printing commit a238d2952d1ec296a68bd201d7b9f030f2d68fa6 Author: Mislav Marohnić Date: Mon Mar 29 15:27:16 2021 +0200 Dynamically resolve the column ID for "Needs review" commit c1c936b74b250654bdec2ddb41a2f75b28c86a91 Author: Mislav Marohnić Date: Mon Mar 29 15:05:34 2021 +0200 Improve PR lint script - Do not add draft PRs to the review board - Do not enforce that the base branch must be "trunk" - Refuse PRs made with our "trunk" as the head - Improve staff check to avoid hardcoding - Improve pattern matching when suggesting to link to an issue - Use the stock GITHUB_TOKEN commit a2251735512089e44fd02c4fc6f29b6d9b87fde1 Author: vilmibm Date: Sun Mar 28 14:02:26 2021 -0500 support --workflow in run list commit 6dba073a23f1a055b618a1311242686c59d299e7 Merge: 5aac1912 dd9a9ef9 Author: Nate Smith Date: Thu Mar 25 10:15:02 2021 -0700 Merge pull request #3267 from cli/workflow-toggle gh workflow {enable,disable} commit dd9a9ef966764f53545f06a94fbe6b04b26a5727 Author: vilmibm Date: Thu Mar 25 12:04:32 2021 -0500 red check for disabling commit f801a944826020f6caf9453ae8eda236c5fe7284 Author: vilmibm Date: Thu Mar 25 10:46:54 2021 -0500 bonus: bump list limit commit 248ee424f2a2e14f7167ad0586a2583efa103caf Author: vilmibm Date: Fri Mar 12 13:02:59 2021 -0600 gh workflow {enable, disable} commit 5aac1912d3c0901b83482d3de46afc9a6a9647eb Merge: 9b0f7066 210d9dff Author: Mislav Marohnić Date: Thu Mar 25 14:13:44 2021 +0100 Merge pull request #3294 from cli/pr-search Add `pr list --search` and `--author` flags commit 210d9dff2077816506b6efcce0db8bf5514dd6e6 Author: Mislav Marohnić Date: Thu Mar 25 13:44:07 2021 +0100 Remove implicit `sort:created-desc` sort clause for `pr list` This is the default sort mode for issues, so it's not needed to explicitly set it. Furthermore, the user can specify their own sort mode through the `--search` option. commit 2fa8a85813a89e15c6e52d4a2537e6434cffaf1e Author: Mislav Marohnić Date: Wed Mar 24 18:11:21 2021 +0100 Unify checking whether search filters were passed by the user commit 949df38d49ce7dad4bb99ad446ec7868e00cb578 Author: Mislav Marohnić Date: Wed Mar 24 17:52:29 2021 +0100 BREAKING: lookup all issue/PR labels with "AND" instead of "OR" combinator This switches to the Search API whenever labels are specified in `issue list` or `pr list`. This ensures that the results match those that would be returned in the web UI. commit f008c61d13d8895eec3fa723a5dc5ae85e948604 Author: Mislav Marohnić Date: Wed Mar 24 17:54:20 2021 +0100 WIP fix filter commit 0131541bb21fd4411b6cacc0f88b8ae1cb50d5f6 Author: Mislav Marohnić Date: Wed Mar 24 16:45:09 2021 +0100 Add tests for `listPullRequests` commit 4388c1db1493a1c91f08fbc19fbdd9a15c2f7090 Author: Mislav Marohnić Date: Wed Mar 24 16:25:26 2021 +0100 Add `pr list --author` filter commit 01bbb15b57118befe4416d4785ffdd87a45a8f33 Author: Mislav Marohnić Date: Tue Mar 23 21:48:51 2021 +0100 Add `pr list --search` commit 75cfed4bef615be987aba4162e18d6f1255870c1 Author: Mislav Marohnić Date: Tue Mar 23 18:50:19 2021 +0100 Import PR list API implementation to `pr/list` package Also splits List vs. Search queries into separate methods for better maintanability. commit 9b0f7066046b3bc4695ea1aff2a6ba6c31583471 Merge: 2ab073d5 179d3f02 Author: Mislav Marohnić Date: Tue Mar 23 19:14:57 2021 +0100 Merge pull request #3196 from g14a/feature/search-issues add search feature in listing issues commit 179d3f0249e693d79e30ce60d5d6eed5ac8e9673 Author: Mislav Marohnić Date: Tue Mar 23 17:58:28 2021 +0100 Add a unified GitHub Search query builder commit 70d4873914236f9b8eb9e78a0745a9cf7caa8feb Author: Gowtham Munukutla Date: Tue Mar 23 20:01:19 2021 +0530 add generic search naming for issues and PRs @samcoe commit 80035aa686253d73d56c1ca13c10641aa5b4e088 Author: Mislav Marohnić Date: Fri Mar 19 13:41:19 2021 +0100 :nail_care: cleanup switching to search mode in `issue list` commit f791bbdbcb0aefec987431bf69338603a1cef092 Author: Gowtham Munukutla Date: Wed Mar 10 16:14:32 2021 +0530 add search feature in listing issues commit e53d02b680b1e8dd3ecc84462cdb53bf883a5c13 Author: Mislav Marohnić Date: Mon Mar 22 12:55:04 2021 +0100 Add back isolated tests for issue/PR lookup by argument commit 111e8dbcf29d8121c941868b0798e979a3698666 Author: Mislav Marohnić Date: Fri Mar 19 21:19:04 2021 +0100 Pass web browser to each individual command This removes sensitivity to the BROWSER environment variable in tests and makes it easier to verify the URL that the browser was invoked with without having to stub sub-processes. commit 2ab073d599b1a64bd58af0ddc6eed63d3a0deb65 Author: Nate Smith Date: Fri Mar 19 09:29:32 2021 -0700 Refactor use of glamour to allow style overrides (#3243) * Refactor use of glamour to allow style overrides * leave the things the way they were and just expose the ability to set overrides commit 84f0f597799a274e75e75b9addd3d9e0174820df Merge: 8480381d 40da8f9c Author: Mislav Marohnić Date: Fri Mar 19 16:10:02 2021 +0100 Merge pull request #3253 from cli/http2-upload-retry Allow retrying HTTP/2 uploads for release assets commit 40da8f9c6919de57624e5f7f08beb29c4ea48365 Author: Mislav Marohnić Date: Wed Mar 17 15:55:22 2021 +0100 Allow retrying HTTP/2 uploads for release assets The `Request.GetBody` func allows the retry mechanism to reopen the file that's being uploaded as the request body in case the body of the previous request has already started to be read. Hopefully fixes the error: http2: Transport: cannot retry err [stream error: stream ID 1; REFUSED_STREAM] after Request.Body was written; define Request.GetBody to avoid this error Ref. https://github.com/golang/net/blob/d523dce5a7f4b994f7ed0531dbe44cd8fd803e26/http2/transport.go#L554 commit 8480381d13b72a059c4e5a4e3c36c06a2a119e91 Author: Nate Smith Date: Tue Mar 16 14:12:27 2021 -0700 workflow list (#3245) * add gh workflow list * review feedback commit 126b498e9fd2d3a97c4b0fdd60eb2739b604f766 Author: Nate Smith Date: Tue Mar 16 13:59:34 2021 -0700 Actions Support Phase 1 (#2923) * Implement first round of support for GitHub Actions This commit adds: gh actions gh run list gh run view gh job view as part of our first round of actions support. These commands are unlisted and considered in beta. * review feedback * tests for exit status on job view * spinner tracks io itself * review feedback * fix PR matching * enable pager for job log viewing * add more colorf functions * add AnnotationSymbol * hide job, run * do not add method to api.Client * remove useless cargo coded copypasta commit e2de02d6b0cbe300b8ae20fe001324361a6b424d Merge: eddd8f00 d5fb817d Author: Mislav Marohnić Date: Mon Mar 15 15:19:10 2021 +0100 Merge pull request #3192 from cristiand391/add-body-file-flag Accept `--body-file` flag if `--body` is supported commit d5fb817dfc33420e68295ec4c1d8643a666724f8 Author: Mislav Marohnić Date: Mon Mar 15 15:11:53 2021 +0100 Remove unnecessary `BodyFile` field commit 8bfe64d59328ae8dc6549822aeab3142b3501c57 Author: Cristian Dominguez Date: Wed Mar 10 14:32:20 2021 -0300 Accept `--body-file` flag if `--body` is supported commit fc2cdd99b188fff0d0514d1f295dc3c943b9b056 Author: Mislav Marohnić Date: Thu Mar 11 18:33:57 2021 +0100 Ensure that `ssh-key list` uses the full width of the terminal Due to a bug in its truncate function, the width of the table would sometimes be off-by-one. commit ed15bebb841b0fb9fc19962839a701325a977774 Author: Mislav Marohnić Date: Thu Mar 11 18:27:35 2021 +0100 Ensure that table printer fills the full width of the terminal Sometimes, due to rounding errors, after calculating the width of each column in a table, the sum of all columns would be shorter that the total available width in the terminal. This reimplements the elastic column resizing algorithm to ensure that all available space has been filled. As a bonus fix, columns that contain URLs are never truncated. commit bfdad5bd48707e5c0332d9e4c3626b0044012713 Author: vilmibm Date: Wed Mar 10 18:15:01 2021 -0600 fix missing PRAUTHOR and add TODO commit 767558828080f33ad6480e2956aabadf58c1f70a Author: vilmibm Date: Wed Mar 10 18:07:09 2021 -0600 grep tweaks commit 31312d22d0fec6731dc21471dcd89839710d9a61 Author: vilmibm Date: Wed Mar 10 14:49:42 2021 -0600 meh commit 8ddc82e55784bd9dcb2b2507a8bdcafe0cf73892 Author: vilmibm Date: Wed Mar 10 14:13:47 2021 -0600 rename commit 75ac2595b30d3f1b4eabced101f7d8d42d24ea5e Author: vilmibm Date: Wed Mar 10 14:01:15 2021 -0600 add workflow commit eddd8f00d1c8965fae7ece7a3877514fc1390a03 Merge: b8937e46 1d8dd2f1 Author: Nate Smith Date: Mon Mar 8 16:03:39 2021 -0600 Merge pull request #3086 from g14a/feature/diffstat-pr Feature/diffstat pr commit b8937e46dfa667430937697c82de39aa7e831ce3 Merge: ad778357 3a8313b4 Author: Mislav Marohnić Date: Mon Mar 8 15:28:47 2021 +0100 Merge pull request #3183 from kidonng/patch-1 Fix a typo in gh api's help text commit ad7783573d4b92276035f65875b5e5c0de6c363d Merge: 2fbc0376 bcef9f83 Author: Mislav Marohnić Date: Mon Mar 8 15:27:45 2021 +0100 Merge pull request #3182 from educhastenier/patch-1 fix typo in docs of `alias` command commit 3a8313b4f7714b4ace188a672c094102a5642fc4 Author: Kid <44045911+kidonng@users.noreply.github.com> Date: Mon Mar 8 22:14:29 2021 +0800 Fix a typo in gh api commit bcef9f83a8f82b5df82fe204ab32546245a6d999 Author: Emmanuel Duchastenier Date: Mon Mar 8 15:11:31 2021 +0100 fix typo in docs of `alias` command correct syntax is `--assignee` instead of `--assigned` commit 1d8dd2f1e97439bc44ea5eee1a8212e575169929 Author: Gowtham Munukutla Date: Sat Mar 6 15:02:16 2021 +0530 add tests for additions and deletions commit f2489ed22fff94a7d53d30343e9e98cff2bc56d9 Author: Gowtham Munukutla Date: Fri Mar 5 14:05:41 2021 +0530 reduce arg length to fprintf commit bdd663e6581acc4961ba0c29431af873a3b2ca6a Author: Gowtham Munukutla Date: Fri Mar 5 13:41:25 2021 +0530 Add additions and deletions in pr view raw as well commit 9944698665ca459823ef0aa61757d015a72ae169 Author: Gowtham Munukutla Date: Fri Mar 5 11:20:49 2021 +0530 Add additions and deletions in pr view commit 2fbc0376586dd0c36d5c2a3860a62396eac72b95 Merge: 6b483aa4 e16c3124 Author: Nate Smith Date: Thu Mar 4 13:01:54 2021 -0600 Merge pull request #3042 from g14a/bug/gist-binary-files Remove functionality to add, view and edit binary files in gists commit 6b483aa46813f6047da58ce8bd14b3478cc9931c Merge: dbf1145c c63247f0 Author: Mislav Marohnić Date: Thu Mar 4 17:52:09 2021 +0100 Merge pull request #3083 from cli/update-notice-redirect Avoid checking for new releases when authenticating git commit dbf1145cc09ad17c2e70217510e2b87775b6f4fd Merge: 0aebfacd 4e24f364 Author: Mislav Marohnić Date: Thu Mar 4 17:47:03 2021 +0100 Merge pull request #3012 from cli/api-jq Add `--filter` to api command to filter data using jq syntax commit 0aebfacd951c19e5865a40b2a437792a0bcebde8 Merge: aa5cf6c4 eb087743 Author: Mislav Marohnić Date: Thu Mar 4 17:46:49 2021 +0100 Merge pull request #3011 from cli/api-template Add a template `--format` flag to api command commit c63247f0eac867fb406388d848db2f4b5986622b Author: Mislav Marohnić Date: Thu Mar 4 17:39:25 2021 +0100 Avoid checking for new releases when authenticating git Avoid displaying upgrade notice if any output is redirected. This also alleviates the need to specifically check for `gh completion -s `, `gh __complete`, and other scripting scenarios where we absolutely don't want to trigger any upgrade checks or notices. commit 4e24f364951352b0540496440d11cdcd7e038112 Author: Mislav Marohnić Date: Thu Mar 4 17:29:59 2021 +0100 Declare `--jq`, `--template`, `--silent` options mutually exclusive commit d89756c94c4a7c1a368186dbb37888e6113035cf Author: Mislav Marohnić Date: Thu Mar 4 17:10:48 2021 +0100 Add test for `api --jq` commit 06eeea073744281198769c54630753fbb4a29101 Author: Mislav Marohnić Date: Thu Mar 4 17:05:31 2021 +0100 Change the `api --filter` flag to `api --jq` commit 4c26d617d3408116fd8bb026001daeacd262eb69 Merge: 03baeb26 eb087743 Author: Mislav Marohnić Date: Thu Mar 4 17:00:41 2021 +0100 Merge remote-tracking branch 'origin/api-template' into api-jq commit eb08774370a0240f07a6235edceb53dd29aa2aa7 Author: Mislav Marohnić Date: Thu Mar 4 16:48:06 2021 +0100 Assert that `executeTemplate` is invoked commit f53ad7161ac956f94f1c4a6770f081472a0f8971 Author: Mislav Marohnić Date: Thu Mar 4 16:35:08 2021 +0100 Add more `api --template` tests commit 0f27084f570995936fb63671ec247e70d4768808 Author: Mislav Marohnić Date: Thu Mar 4 15:01:59 2021 +0100 Add flag parsing test for `api --template` commit 07cb5e9e17604bf77cad8f854471ef8c615df096 Merge: bf97c6e2 aa5cf6c4 Author: Mislav Marohnić Date: Thu Mar 4 14:53:08 2021 +0100 Merge remote-tracking branch 'origin' into api-template commit aa5cf6c48a95929e4fe2e293c371538f52c44004 Merge: e96d9743 cfbfb578 Author: Mislav Marohnić Date: Thu Mar 4 13:51:24 2021 +0100 Merge pull request #3075 from cli/credential-helper-absolute Use absolute path when configuring gh as git credential commit e96d974331d01990ffb4660808e227d40f315874 Merge: 1eefb6bb 92341636 Author: Mislav Marohnić Date: Thu Mar 4 13:45:11 2021 +0100 Merge pull request #3023 from cli/cancel-error-status Issue/pr create: exit with nonzero status code when "Cancel" was chosen commit cfbfb578f07e329623d1bd5c0010a89bbf613e72 Author: Mislav Marohnić Date: Thu Mar 4 13:41:50 2021 +0100 Read `Executable` from factory instead of from stdlib commit 9234163679f265dacdd3c87877eeee21560bc401 Author: Mislav Marohnić Date: Thu Mar 4 13:22:11 2021 +0100 Formalize `gh` process exit codes Here are the statuses: - 0: success - 1: misc. error - 2: user interrupt/cancellation - 4: authentication needed These old exit codes are now changed to "1": - we used to return "2" for config file errors; - we used to return "2" for alias expansion errors; - we used to return "3" for alias runtime errors. I do not believe that there is a need to distinguish these specific cases via exit status, and converting them to "1" frees codes "2" and "3" for more practical use. commit 1eefb6bbc044a6972fd94ed422d985211f4e538c Merge: 3444d00b 440b59f8 Author: Mislav Marohnić Date: Thu Mar 4 12:39:46 2021 +0100 Merge pull request #3077 from cli/api-preview Add the `api --preview` flag to opt into GitHub API previews commit 440b59f8c393d9aceeae1bc6e43742ebca494f62 Author: Mislav Marohnić Date: Wed Mar 3 20:12:51 2021 +0100 Add the `api --preview` flag to opt into GitHub API previews This was previously available manually via the `-H` flag, but it was verbose, especially when opting into multiple previews. commit 03baeb2645fb7d3afccff53f97e0cfc4f2a4f638 Author: Mislav Marohnić Date: Wed Mar 3 19:24:38 2021 +0100 Add documentation and tests for `api --filter` commit 9f4eb55b6664b61c46d06edda2317927bf3acc95 Merge: 0e917dc1 bf97c6e2 Author: Mislav Marohnić Date: Wed Mar 3 17:35:18 2021 +0100 Merge remote-tracking branch 'origin/api-template' into api-jq commit 98f1f5ec0d570a1b89cfe249cc6fff37dc6d7ab1 Author: Mislav Marohnić Date: Wed Mar 3 16:20:21 2021 +0100 Use absolute path when configuring gh as git credential This keeps git operations working even when PATH is modified, e.g. `brew update` will work even though Homebrew runs the command explicitly without `/usr/local/bin` in PATH. Additionally, this inserts a blank value for `credential.*.helper` to instruct git to ignore previously configured credential helpers, i.e. those that might have been set up in system configuration files. We do this because otherwise, git will store the credential obtained from gh in every other credential helper in the chain, which we want to avoid. Before: git config --global credential.https://jackfan.us.kg.helper '!gh auth git-credential' After: git config --global credential.https://jackfan.us.kg.helper '' git config --global --add credential.https://jackfan.us.kg.helper '!/path/to/gh auth git-credential' commit bf97c6e273ff419f22b4da0e9ff9a22bfccc0d12 Author: Mislav Marohnić Date: Tue Mar 2 20:07:04 2021 +0100 Add template functions, documentation, tests commit ed219ab5f30ed0ac13730bceadc5ba3a413d8be7 Merge: fd82d621 3444d00b Author: Mislav Marohnić Date: Tue Mar 2 18:31:28 2021 +0100 Merge remote-tracking branch 'origin' into api-template commit 3444d00beeefe86497a61e7c85b638d57cb90ecb Merge: 07e6d60c fee7adf9 Author: Mislav Marohnić Date: Tue Mar 2 15:21:17 2021 +0100 Merge pull request #3018 from castaneai/pr-create-body-file Add `pr create --body-file` flag commit 07e6d60c80ac0d53fbbf12c3965bee09429c77e2 Merge: f93674bc 9e63199a Author: Mislav Marohnić Date: Tue Mar 2 15:14:16 2021 +0100 Merge pull request #2991 from cli/repo-create-prompt-change Repo create tweaks commit f93674bc76f8e68e0bf7792633cdb7168168675b Merge: 50c49df4 331bf500 Author: Mislav Marohnić Date: Tue Mar 2 15:06:53 2021 +0100 Merge pull request #3059 from fossdd/patch-1 Add information about AUR commit 331bf500763d35322833b03b7a2eedd90bad9925 Author: Mislav Marohnić Date: Tue Mar 2 15:05:16 2021 +0100 Tweak language commit e16c3124ddeaeda4f14a739e47664575cad4aaf5 Author: Mislav Marohnić Date: Tue Mar 2 14:58:16 2021 +0100 Disallow binary files with `gist edit -a` commit 066ba54549ed20e31f5c2d12898ba766bc92a3ae Author: Mislav Marohnić Date: Tue Mar 2 14:44:49 2021 +0100 Sort gist files case-insensitively commit 77d9051d0e318a707ff22f63247a7120dd2ea46c Author: Mislav Marohnić Date: Tue Mar 2 14:40:43 2021 +0100 Simplify looking up binary types in gist commit 973fbb09254e30709a45ae9c7b08a710cb822fd8 Author: Gowtham Munukutla Date: Wed Feb 24 18:39:25 2021 +0530 Disallow operating on binary files in gist commit dd34cae1120913f5e4b6f9d56dbffd316519e04f Merge: 2ebdde1d 50c49df4 Author: Mislav Marohnić Date: Tue Mar 2 13:49:59 2021 +0100 Merge remote-tracking branch 'origin' into cancel-error-status commit 2ebdde1ddd0b0887b92ba3f3fc4b59082bb7498c Author: Mislav Marohnić Date: Tue Mar 2 13:48:44 2021 +0100 Exit with status code "2" on user cancellation errors This also stops printing "interrupt" after Ctrl-C is pressed. commit 50c49df41abb98d00f526daee18f4ea9fb1ec002 Merge: 953855c1 69b9aa3a Author: Mislav Marohnić Date: Tue Mar 2 12:47:03 2021 +0100 Merge pull request #3010 from cli/api-cache Add `api --cache` flag commit 953855c1c3da69a2968889b8929879ec03ba859f Merge: 00cb921c 39718cd5 Author: Nate Smith Date: Mon Mar 1 16:10:05 2021 -0600 Merge pull request #3008 from ganboonhong/interactive-gist-view Add interactive select in gist view commit 39718cd5ca018364c5d98aa69bb00913fae9484e Author: vilmibm Date: Mon Mar 1 16:07:04 2021 -0600 just hide empty descriptions commit 00e8c07021687e7ceadfa4eac37ea5fa7b96a0c7 Merge: e100b15a 00cb921c Author: vilmibm Date: Mon Mar 1 16:05:26 2021 -0600 Merge remote-tracking branch 'origin/trunk' into interactive-gist-view commit e100b15acb730b5843f45ec6a3be91db34e6ca02 Author: vilmibm Date: Mon Mar 1 16:03:13 2021 -0600 some text tweaks commit 14c4743f8c4828bc06f254f0e226c937db150906 Author: fossdd Date: Mon Mar 1 19:01:18 2021 +0000 Fix markdown link Co-authored-by: Nate Smith commit 69b9aa3a57d1db122938b37f71fa538b2bb8a4ab Merge: 162a1b29 00cb921c Author: Mislav Marohnić Date: Mon Mar 1 16:06:17 2021 +0100 Merge remote-tracking branch 'origin' into api-cache commit 162a1b290ae3f5d84eadf0673e994f98d703a60a Author: Mislav Marohnić Date: Mon Mar 1 16:04:34 2021 +0100 Allow caching HTTP 204 responses commit e32e6406a77d5cf18d4196b670564d6998a53ecd Author: Mislav Marohnić Date: Mon Mar 1 16:04:19 2021 +0100 Add test for `api --cache` behavior commit 9e63199a652a8e8791d03558b4f24be8c4ed42ed Author: Mislav Marohnić Date: Mon Mar 1 14:12:56 2021 +0100 Add tests for checking out repository after creating from template commit a6e9940b81edbf41a11e33303190fb6ef84e4df2 Author: fossdd Date: Sun Feb 28 12:54:19 2021 +0000 Add information about AUR The AUR is a community-based location for PKGBUILDs, Arch's Install scripts. There is a unofficial PKGBUILD for building and installing `gh` from the git repo. https://aur.archlinux.org/packages/github-cli-git commit 00cb921cd5882a963932b655fb6226da26ef6550 Merge: da2a732c e27a77fc Author: Mislav Marohnić Date: Sat Feb 27 17:34:32 2021 +0100 Merge pull request #2953 from cristiand391/add-repo-list Add `repo list` command commit e27a77fc99fd7e76c93fc45bdf9ad0901a372dec Author: Mislav Marohnić Date: Sat Feb 27 17:20:06 2021 +0100 Add ability to filter by archived in `repo list` Like `--language`, archived filters also use the Search API. commit 5da8301d5d6490c2082e228398e884db41e52bad Author: Mislav Marohnić Date: Sat Feb 27 16:51:45 2021 +0100 Enable filtering `repo list` by coding language commit f75144dd1f1d45dfa7b6ec2adf2942f5b06313e3 Author: Mislav Marohnić Date: Sat Feb 27 15:05:11 2021 +0100 Enable pager for `repo list` output commit da2a732c6affd72245aba2dffdff9b2ce0bde6b6 Merge: a4965497 8f96e406 Author: Mislav Marohnić Date: Sat Feb 27 14:49:46 2021 +0100 Merge pull request #2997 from g14a/feature/add-files-to-gist Feature/add files to gist commit 2bdffc85e21b7e7161164d8570bc2b102a2f2d9e Author: Mislav Marohnić Date: Sat Feb 27 14:39:06 2021 +0100 Isolate flag processing tests in `repo list` commit 1fa763f51472075d26a67f0fc89e2696bd867280 Author: Mislav Marohnić Date: Sat Feb 27 14:21:26 2021 +0100 Avoid having to first query for username in `repo list` Dynamically construct the GraphQL query by using the `viewer` connection if the owner isn't set and the `repositoryOwner(login:"...")` connection if the owner was set. commit 4da02614ed6bc7ff22307723bda6d15c87ff324e Author: Mislav Marohnić Date: Sat Feb 27 13:17:59 2021 +0100 Switch `repo list` to query via `graphql` package Also order results by PUSHED_AT instead of UPDATED_AT to match the web interface. commit 8f96e406ac450bdaeda7129bf75ad2341701774b Author: Mislav Marohnić Date: Sat Feb 27 12:23:18 2021 +0100 Improve error handling and avoid writing confirmation to stdout Right now the `gist edit` command doesn't write anything to stdout, so let's keep it that way until we want to intentionally provide some feedback in the terminal. commit 406d7eee456f8c91da5a7ba66c2d9e8ab30c2213 Author: Mislav Marohnić Date: Sat Feb 27 12:03:29 2021 +0100 :nail_care: cleanup `gist edit -a` feature commit a49654970c82db57ea1310cb4443378fae3f4447 Merge: 82351402 34da5977 Author: Sam Date: Fri Feb 26 10:31:06 2021 -0800 Merge pull request #3024 from cli/normalize-pr-commands Normalize pr command arguments commit 823514022de923ed1bbec5c0617b4e7a7dd5ea33 Merge: e4ce0d76 492f4542 Author: Mislav Marohnić Date: Fri Feb 26 13:08:27 2021 +0100 Merge pull request #3036 from cli/pr-merge-no-commits Avoid crash in `pr merge` when the pull request has no commits commit 492f45422ecd796416acaaaaac2e5a37ae43dbc0 Author: Mislav Marohnić Date: Fri Feb 26 13:07:38 2021 +0100 Add a note about the style of git tests commit 7fd0634a24e32e5bcfbc025abaed11db2022ed1b Merge: 3e5d5a23 e4ce0d76 Author: Gowtham Munukutla Date: Thu Feb 25 14:47:26 2021 +0530 rebase with trunk commit 0833bdc6b4754414d49971892578b805fe1f0280 Merge: 5403a376 e4ce0d76 Author: boonhong Date: Wed Feb 24 23:32:23 2021 +0800 Merge branch 'trunk' of github.com:cli/cli into interactive-gist-view commit e4ce0d76aac90a0c56365777d87d5e458104170e Merge: 896f2273 732e919a Author: Mislav Marohnić Date: Wed Feb 24 15:57:50 2021 +0100 Merge pull request #3022 from ganboonhong/pr-edit-branch Add `pr edit --base` to change the base branch of a PR commit 896f2273e85da9aa640429f35d404ae718db6480 Merge: 98df059e 66d4307b Author: Mislav Marohnić Date: Wed Feb 24 15:37:06 2021 +0100 Merge pull request #3021 from g14a/bug/gist-deletion Accept only one argument when deleting a gist commit d97e8fe172d272d1718109234c7a4f24def7f871 Author: Mislav Marohnić Date: Wed Feb 24 15:05:56 2021 +0100 Add live tests for some methods in the `git` package We relied too much on stubs for these methods. These new tests actually invoke `git` commands in the context of a test repository. commit 0f85304e3e144b06ee142da461e43d0f0443ed4f Author: Mislav Marohnić Date: Wed Feb 24 14:37:29 2021 +0100 Avoid crash in `pr merge` when verifying whether a PR had diverged A PR is not guaranteed to have commits, it seems, so add a guard against assuming that there is always a head commit. commit 66d4307bce0d0189c1cb4650b432852cde39f328 Author: Gowtham Munukutla Date: Wed Feb 24 18:05:11 2021 +0530 return msg instead of too many arguments commit 5403a37601e589b05198cf3c009d81487d38ef27 Author: boonhong Date: Mon Feb 22 20:25:18 2021 +0800 Add interactive select in gist view commit 79b77b4273f2ce3728820b7fd27d16da4513c2af Merge: 61eb7eee 98df059e Author: Gowtham Munukutla Date: Wed Feb 24 15:54:29 2021 +0530 Merge branch 'trunk' of https://github.com/cli/cli into bug/gist-deletion commit 61eb7eeeab3f346ff7a4c61a79d5b0341cc71cff Author: Gowtham Munukutla Date: Wed Feb 24 15:53:07 2021 +0530 Add msg in gist delete commit 56ead91702e61575622233713ccf247af4644bcb Author: Gowtham Munukutla Date: Wed Feb 24 15:49:40 2021 +0530 Add helper function to validate exact args in cmdutil commit 3e5d5a23c083848b510a7ad6c0b6056140031680 Author: Gowtham Munukutla Date: Wed Feb 24 10:22:56 2021 +0530 add fixturefile const in tests commit d4e14beb57880f493438849b97d6ca92090333fb Author: Gowtham Munukutla Date: Wed Feb 24 10:14:31 2021 +0530 remove unwanted tests and unwanted functionality commit 8f1c467001f0c6821922f7ec155ec087985e0b52 Merge: a6fa1486 98df059e Author: Gowtham Munukutla Date: Wed Feb 24 09:44:19 2021 +0530 Merge branch 'trunk' of https://github.com/cli/cli into feature/add-files-to-gist commit a6fa14866be9e521b82ea0ea09cdb6653319b855 Author: Gowtham Munukutla Date: Wed Feb 24 09:44:11 2021 +0530 updating tests WIP commit 34da59777bd5245e2958290f4d1ab21867933362 Author: Sam Coe Date: Tue Feb 23 13:24:48 2021 -0800 Revert close and reopen changes commit 98df059e846f3e98b5dd0190b5c3ebecb316c29d Merge: c5af4ddf 27aea42d Author: Mislav Marohnić Date: Tue Feb 23 21:04:57 2021 +0100 Merge pull request #3020 from cli/brew-upgrade-notice Avoid upgrade notice for recent release if gh is under Homebrew prefix commit 3efa76430576eb11ac33aaadc4719cfc14db4ee4 Author: Mislav Marohnić Date: Tue Feb 23 20:09:03 2021 +0100 Avoid the issue/pr recovery mechanism handling Ctrl-C keypress in prompts Either InterruptErr or SilentErr will be present when the user has chosen "Cancel" or pressed Ctrl-C in prompts. We don't want the recovery mechanism to kick in these cases because the cancellation was likely willingly initiated by the user. commit fff051468eb793e89e946f4eac6ed1f5ab1efd20 Author: Mislav Marohnić Date: Tue Feb 23 19:42:41 2021 +0100 Avoid triggering recovery mechanism when cancelling `issue/pr create` commit 9d062ed8fcdd5845e55ebfe364a41c3524a09fc3 Author: Sam Coe Date: Tue Feb 23 09:17:35 2021 -0800 Normalize pr command arguments commit 732e919a835882700262b0a448c1150faa24d1bf Author: boonhong Date: Tue Feb 23 22:51:10 2021 +0800 Add `pr edit --base` to change the base branch of a PR commit b0b90afa872ccaceedeac8610f788af896e53dd1 Author: Mislav Marohnić Date: Tue Feb 23 17:06:29 2021 +0100 issue/pr create: exit with nonzero status code when "Cancel" was chosen This is to indicate that the command had not finished successfully. commit c5af4ddfdc0ffbcd4f117d8813e4d40a2ce54c4b Merge: d6798b18 9bf1668b Author: Mislav Marohnić Date: Tue Feb 23 16:32:23 2021 +0100 Merge pull request #3009 from cli/git-credential-env Fix `auth git-credential` when the token comes from environment commit cbf8a0d9641869de104ad1c94493e07addcd45c9 Author: Gowtham Munukutla Date: Tue Feb 23 20:12:26 2021 +0530 Accept only one argument when deleting a gist commit fee7adf9baf426bc2a4ea19ad253dc3125159dc3 Author: Mislav Marohnić Date: Tue Feb 23 14:25:32 2021 +0100 Add `issue create -F ` flag and tests commit 13c3c6543bf29bf09dabe86de26eb3188dfa9bd3 Author: castaneai Date: Tue Feb 23 15:51:47 2021 +0900 Add `pr create --body-file` flag commit a60a6d854b1c2f35eaf1ad032195f4efc7a70427 Merge: faffc4de d6798b18 Author: Gowtham Munukutla Date: Tue Feb 23 17:13:07 2021 +0530 Merge branch 'trunk' of https://github.com/cli/cli into feature/add-files-to-gist commit 27aea42d8a43951b880145fcbbd42b262ed6af08 Author: Mislav Marohnić Date: Tue Feb 23 12:10:56 2021 +0100 Avoid upgrade notice for recent release if gh is under Homebrew prefix Before, when gh detected there was a new release in the `cli/cli` repo, it would show this notice: A new release of gh is available: {V1} → {V2} Additionally, when the release was more than 24h old, we would show this to Homebrew users: To upgrade, run: brew update && brew upgrade gh Ref. feb4acc2c00ce42d5ed67252ae1ec66addeb786d This change makes it so that the original notice "A new release of gh is available" is NOT shown to Homebrew users unless the release is older than 24h. We effectively hide the fact that any release happened until we're sure that the version bump has made it to `homebrew-core`. commit d6798b185257a9098b611ac50a9884f8d04d5b7c Merge: 04dcb327 cfddda88 Author: Mislav Marohnić Date: Tue Feb 23 11:34:02 2021 +0100 Merge pull request #3019 from cli/ghe-paste-token Fix pasting Personal Access Token to `auth login` for GHE commit cfddda8829cd205b88a1e854b6fca76d6acc34b2 Author: Mislav Marohnić Date: Tue Feb 23 10:52:29 2021 +0100 Indicate `workflow` scope is GHE 3.0+ only during `auth login` commit f807795491e90954e26873e39d2f90ec1f07bf33 Author: Mislav Marohnić Date: Tue Feb 23 10:19:11 2021 +0100 Fix pasting Personal Access Token to `auth login` for GHE commit 0e917dc1ad328d14b32c1a23245278f9d90cc96d Author: Mislav Marohnić Date: Mon Feb 22 19:02:35 2021 +0100 go mod tidy commit 329ba1d57bce466704899ec22f211aeffbcdd67c Author: Mislav Marohnić Date: Mon Feb 22 17:48:52 2021 +0100 Add `--filter` to api command to filter data using jq syntax commit fd82d621d5fa24b8de18d78d05ab93a5b978d71b Author: Mislav Marohnić Date: Mon Feb 22 16:54:27 2021 +0100 Add `color` function to api templates commit 517cfc236573a7ff872dae9e010a4929d7374e9c Author: Mislav Marohnić Date: Mon Feb 22 16:15:02 2021 +0100 Add `api --format` flag for specifying an output template With the `--format` flag, the value of the flag is parsed as a Go template which is then evaluated against parsed response data. https://golang.org/pkg/text/template commit 9dff05bf205a7554d83310731cd8e65beb423777 Author: Mislav Marohnić Date: Mon Feb 22 16:13:24 2021 +0100 Add `api --cache` flag Cache API responses on disk for a specified duration. commit 2284ef43d079c1c5ae280848e098f04e6ee1ddf3 Author: Cristian Dominguez Date: Fri Feb 19 17:34:17 2021 -0300 repo list: add tests commit 9bf1668b3f83997f6fdecf1410bf0f586c3c11f4 Author: Mislav Marohnić Date: Fri Feb 19 15:37:11 2021 +0100 Fix `auth git-credential` when the token comes from environment When a token such as GH_TOKEN is set through environment variables and `~/.config/gh/hosts.yml` is non-existent, the `auth git-credential get` command used to fail due to missing username. Since GitHub username isn't at all required for token authentication, use the `x-access-token` faux username instead of trying to obtain one from a config file. commit faffc4de95e31e8e59951956ad967f82688ab419 Author: Gowtham Munukutla Date: Fri Feb 19 12:05:20 2021 +0530 Add go fmt to pass ci/cd commit f56b38908ebf731db39d7219f8cc0312c2cd4095 Merge: 4ed10140 04dcb327 Author: Gowtham Munukutla Date: Fri Feb 19 12:02:24 2021 +0530 Merge branch 'trunk' of https://github.com/cli/cli into feature/add-files-to-gist commit 4ed10140ab191fbf0465560a51ce6be04a46e402 Author: Gowtham Munukutla Date: Fri Feb 19 12:02:17 2021 +0530 Resolved PR review comments and test cases commit cad875a05fa006bf1ff8cee0f62e6d99f1d666b6 Author: Cristian Dominguez Date: Thu Feb 18 19:02:59 2021 -0300 repo list: render repo tags into the 3rd column instead of the 2nd commit b7c2865d0f021f581ce5d14b18d3e81dd711af30 Author: Cristian Dominguez Date: Thu Feb 18 17:34:00 2021 -0300 Remove archived filter from repo list commit 04dcb327caeacce723095e892f5f347449ad51e2 Merge: 2f563bab dcff6c4f Author: Mislav Marohnić Date: Thu Feb 18 19:41:04 2021 +0100 Merge pull request #2996 from cli/ghe-branchprotectionrule Fix `pr status` for GHE 2.22 and older commit 882bd1adb1fadc1c99e67d56f71d9204b19e25cc Author: Gowtham Munukutla Date: Thu Feb 18 22:39:56 2021 +0530 add go lint to pass checks commit dcff6c4f2d849733b35a8d331bb2ddd03f710429 Author: Mislav Marohnić Date: Thu Feb 18 17:46:13 2021 +0100 Fix `pr status` for GHE 2.22 and older This queries for the availability of the `branchProtectionRule` field on "Ref" before trying to request it from GraphQL. commit a4a194011ff072f1c5127ae3714363b1c6cdbd9c Author: Gowtham Munukutla Date: Thu Feb 18 19:18:49 2021 +0530 gofmt commit 9a4fd0d706219af9fee861cc831d0d43400b43ec Author: Gowtham Munukutla Date: Thu Feb 18 19:17:42 2021 +0530 Remove unwanted prompt for user. Unwanted test as well commit d469f4b2cc9f66d9af26ee49e7d8d683ae8f0a6b Merge: bff8b300 2f563bab Author: Gowtham Munukutla Date: Thu Feb 18 18:46:40 2021 +0530 Merge branch 'trunk' of https://github.com/cli/cli into feature/add-files-to-gist commit bff8b3007a85aed6206beb079257b5d44fb367fc Author: Gowtham Munukutla Date: Thu Feb 18 18:45:43 2021 +0530 Add test cases and improve errors with color schemes commit 2f563babbf07dda4f9dc80114753f86753487873 Merge: e91b97b4 1a9e42ed Author: Nate Smith Date: Wed Feb 17 14:30:37 2021 -0600 Merge pull request #2990 from cli/ssh-key-commands Add `ssh-key add` command and publish `ssh-key` commit e596f8732b0c3402026d07b4524ec4bdebd1f596 Author: Mislav Marohnić Date: Wed Feb 17 20:26:06 2021 +0100 Fix creating a repository from template Fixes a problem where setting up a new local directory for the repository created from a template would not contain any files: gh repo create -p OWNER/some-template my-repo --private --confirm ls my-repo //=> [empty directory] Fixes #2290 commit a8fdd9a303526f2570da7969530e0dbd0d2ccb3f Author: Mislav Marohnić Date: Wed Feb 17 20:22:23 2021 +0100 Further clarify what will happen on `repo create` In local git directory: 1. `This will add an "origin" git remote to your local repository. Continue?` 2. "origin" git remote is added in current directory. Outside of a local git directory: 1. This will create the "REPO" repository on GitHub. Continue? 2. `Create a local project directory for "REPO"?` 3. new directory called "REPO" now set up for the GitHub repository. commit 1a9e42ed550440aaea973bbc56b3040d5061a133 Author: Mislav Marohnić Date: Wed Feb 17 19:46:59 2021 +0100 Add `ssh-key add` command and publish `ssh-key` commit e91b97b4c50341f0342917de058290f17afafc99 Author: Nate Smith Date: Wed Feb 17 12:33:22 2021 -0600 fully restore fork remote renaming behavior (#2982) * fully restore fork remote renaming behavior * catch blank remote name and error + arg tests * hard wrap fork usage * do not rename if remote-name supplied * tweak error text commit 4a897f70c3db49e77f4862f08100cd143cbc8220 Merge: c148a9ba 95a8f926 Author: Nate Smith Date: Wed Feb 17 12:32:27 2021 -0600 Merge pull request #2962 from ulwlu/fix_prompt_string_when_creating_remote_repository Fix prompt string when creating remote repository commit c148a9ba243bb45a7cedc3403bef8a0970e9a1d2 Merge: 70d4786e a90997ec Author: Mislav Marohnić Date: Wed Feb 17 19:26:17 2021 +0100 Merge pull request #2989 from cli/pr-merge-immediate-edit pr merge: avoid prompting to enter editor after editing phase is chosen commit a90997ec95911a42c151dd029f7bcff752277d5a Author: Mislav Marohnić Date: Wed Feb 17 18:47:17 2021 +0100 pr merge: avoid prompting to enter editor after editing phase is chosen When user chooses "Edit commit message", open the editor immediately instead of showing an additional prompt to open the editor. commit 70d4786e372cb874a07e7e79e24cd78dac699125 Merge: 3b117e6c 05421db4 Author: Mislav Marohnić Date: Wed Feb 17 18:13:15 2021 +0100 Merge pull request #2988 from cli/strict-status-checks-base pr status: fix checking branch protection rules on the base branch commit 05421db4040cb58bcb62116127c777393f3585a9 Author: Mislav Marohnić Date: Wed Feb 17 17:56:26 2021 +0100 pr status: fix checking branch protection rules on the base branch Instead of checking branch protection rules on the main branch of the repository, branch protection rules for a specific PR should be checked on its base branch, since not all PRs are based on the main branch. Additionally, do not display "Up to date" if the actual merge status reported from the server was "UNKNOWN" or "DIRTY", since in those cases "Up to date" could be false information. commit 3b117e6c3c5ada3284584414c12d2c8473189af5 Merge: faa6981f 3a0a8c4e Author: Mislav Marohnić Date: Wed Feb 17 17:59:42 2021 +0100 Merge pull request #2539 from divyaramanathan/issue-create-template Implementing issue template GraphQL API call commit 3a0a8c4e2538abba6edf40db58089a221ecd6e63 Author: Mislav Marohnić Date: Wed Feb 17 17:35:04 2021 +0100 Add tests for templateManager commit faa6981f467d7a8e6e2b8ca1295071c8861645c4 Merge: 3a224b7c 4a49e352 Author: Mislav Marohnić Date: Wed Feb 17 17:26:16 2021 +0100 Merge pull request #2965 from cli/writeorg-oauth-scope Recognize the `write:org` OAuth scope as satisfying `read:org` commit 4a49e3526c5e2e55587ffefbc66786fcaed5aa43 Merge: 0be2033d 3a224b7c Author: Mislav Marohnić Date: Wed Feb 17 17:11:24 2021 +0100 Merge remote-tracking branch 'origin' into writeorg-oauth-scope commit 0cd57443983042a43689763aba143708021c6843 Author: Mislav Marohnić Date: Wed Feb 17 17:08:50 2021 +0100 Un-export `HasAPI` leaky abstraction commit 3a224b7c2a1474e67677fd95ec454850ca56f26c Merge: e874236a b4bf8cda Author: Mislav Marohnić Date: Wed Feb 17 17:07:05 2021 +0100 Merge pull request #2892 from cli/auth-with-ssh Add SSH key generation & uploading to `gh auth login` flow commit b4bf8cda8dbcc991d69a6e56e4e513212b9ed412 Author: Mislav Marohnić Date: Wed Feb 17 17:01:14 2021 +0100 Close pubkey file after reading commit e874236ad6cc1c86cfad53b5a35d1049a0b35aaa Merge: c2c211db ebc5d019 Author: Mislav Marohnić Date: Wed Feb 17 16:58:49 2021 +0100 Merge pull request #2980 from cli/auto-merge PR merge improvements: auto-merge, edit commit body commit 87fcda5fbc87c289452d5b362782618e25ba24a0 Author: Mislav Marohnić Date: Wed Feb 17 16:52:05 2021 +0100 Add tests for SSH login flow commit 298388745879a2978573a8822440bcc646358683 Author: Gowtham Munukutla Date: Wed Feb 17 20:39:43 2021 +0530 Add files by absolute path to gist commit 4cd43cc8ef2b88fbf7eaadb0572607f48fde2f84 Merge: 9550ad01 c2c211db Author: Mislav Marohnić Date: Wed Feb 17 15:29:36 2021 +0100 Merge remote-tracking branch 'origin' into auth-with-ssh commit ebc5d019424e06ec8b3146a63cc6535cdb9e101d Merge: 203397ba c2c211db Author: Mislav Marohnić Date: Wed Feb 17 15:25:25 2021 +0100 Merge remote-tracking branch 'origin' into auto-merge commit 203397baf91b3a08523fd0643d27eb03b668780d Author: Mislav Marohnić Date: Wed Feb 17 15:24:52 2021 +0100 Add tests for `pr merge --auto/--disable-auto` commit 037343c5c2bb7994f01f6480cb881a602ab426db Author: Gowtham Munukutla Date: Wed Feb 17 19:20:43 2021 +0530 Add existing files in the current wd to gist commit ddddd95d7373f7e05c918907a56793b2938e1932 Author: Mislav Marohnić Date: Wed Feb 17 14:38:33 2021 +0100 Allow `pr merge --body ''` to prevent having the default body applied commit 12cf8ef65b68a296a8b00ecb2f7560d3dbdbf56b Author: Mislav Marohnić Date: Wed Feb 17 14:06:27 2021 +0100 Separately query `viewerMergeBodyText` for GHE compatibility GHE versions 2.22 and older will not have this GraphQL field. Avoid the resulting error and have the command proceeed with empty text as the default. commit 2b36b09abf54661280f8e15873482e927418eabf Author: Mislav Marohnić Date: Wed Feb 17 12:30:04 2021 +0100 Update wording for auto-merge confirmation Co-authored-by: Amanda Pinsker commit 05e45e38637e2e385885dab28814e449d4d2d668 Author: Gowtham Munukutla Date: Wed Feb 17 11:27:57 2021 +0530 Feature of adding new files to an existing Github gist commit c2c211dbeddab9b326ba6a2bae75869ae5692886 Merge: 4e5aa91f 57140ad3 Author: Nate Smith Date: Tue Feb 16 12:50:28 2021 -0600 Merge pull request #2952 from redreceipt/up-to-date Adds Branch Up to Date Status commit 57140ad35e8eb600fcc5b36ac86835a4c126364e Author: vilmibm Date: Tue Feb 16 12:25:09 2021 -0600 add header in correct place commit 3b650a8c566269097f76ebd199dfcb43d1ce1158 Author: Mislav Marohnić Date: Tue Feb 16 16:28:23 2021 +0100 Fix typo commit 57abe45b96c253f6bc921b86ef756dcbbce1298c Author: Mislav Marohnić Date: Tue Feb 16 16:17:37 2021 +0100 Let the server choose the commit subject for squashed merge For single-commit PRs, the commit subject will be the subject of the head commit and the PR number. For multi-commit PRs, the commit subject will be the PR title and PR number. Instead of trying to replicate this logic client-side, omit the `commitHeadline` param and let the server apply defaults appropriately. Reverts https://github.com/cli/cli/pull/1627 commit 67bfedd56b3d68b11c838ebbb8123ab502992816 Author: Mislav Marohnić Date: Tue Feb 16 15:55:24 2021 +0100 Add `pr merge --auto` commit f75bd7280fddce4e53cd6bd82f8e9b3e73a46e50 Author: Cristian Dominguez Date: Mon Feb 8 19:45:51 2021 -0300 Pre-populate default merge commit message if no body was provided commit 4ea8d25b8566b4eaf1f69d4a7b7f5625654c781d Author: Sam Coe Date: Thu Feb 4 13:22:26 2021 -0800 Fix tests and polish commit d57cb56945ddd17c41cc420e8b3aafd8994dcb71 Author: Cristian Dominguez Date: Tue Feb 2 08:43:42 2021 -0300 Allow editing commit msg when squash merging a PR commit 0be2033d51185edacfe524e217c29ed44f92fc76 Author: Mislav Marohnić Date: Mon Feb 15 17:52:41 2021 +0100 Recognize the `write:org` OAuth scope as satisfying `read:org` If someone pastes a PAT with `write:org` scope, this avoids the error complaining that the token doesn't have `read:org` permissions. On GitHub, `write:org` implies `read:org`. commit 95a8f926abc2e5afc7886400ccd4d92fde83d574 Author: ulwlu Date: Sat Feb 13 18:10:38 2021 +0900 Remove unnecessary Sprint commit 16be90c53828aeb36916a068ac10ced53a4f5ef8 Author: ulwlu Date: Sat Feb 13 17:20:39 2021 +0900 Fix unnecessary Sprintf with Sprint commit e461baa217f7ea3f723460e8ff4b8ab17443841b Author: ulwlu Date: Sat Feb 13 17:11:08 2021 +0900 Fix prompt string when creating remote repository If you are in git project not pushed to remote yet, prompt says 'This will create {reponame} in current directory. Continue?', however, it doesn't create while it only adds remote origin. I was going to create PR to avoid creating new directory before I knew this behavior. This behavior is already ideal, so I changed prompt not to scare users like I got scared. commit 4e5aa91fac4f9157610f7ebe65c3a3828af65c3f Merge: a84145eb 4fdf28d8 Author: Sam Date: Fri Feb 12 15:02:26 2021 -0800 Merge pull request #2949 from cli/edit-improvements Change behavior of slice flags for issue edit and pr edit commands commit 4fdf28d8a43c39013ce974d65f079240dce3796d Author: Sam Coe Date: Wed Feb 10 15:13:59 2021 -0800 Change behavior of slice flags for issue edit and pr edit commands commit a84145eb684660212d85e0cd7f4b5f5d75c9d54f Merge: 4109af9b a47ee660 Author: Sam Date: Fri Feb 12 10:21:11 2021 -0800 Merge pull request #2940 from cli/pr-edit Edit pull request command commit a47ee660a79cc578e3c860015e2329fcecfe84be Author: Sam Coe Date: Fri Feb 5 11:46:58 2021 -0800 Pr edit command commit 4109af9b499e9e5adeed1f9deea008dc2bce910c Merge: 6b1e6db8 cd9f2118 Author: Sam Date: Fri Feb 12 09:50:11 2021 -0800 Merge pull request #2915 from cli/issue-edit Edit issue command commit 9be9229a48c2fca10942eb707b94fcc16286cd05 Author: Michael Neeley Date: Fri Feb 12 08:51:47 2021 -0500 adds strict status checks commit 6b1e6db81b9e62b12e3ce4075e62675653c910dd Merge: 5af2dca3 335f0117 Author: Mislav Marohnić Date: Fri Feb 12 12:58:48 2021 +0100 Merge pull request #2951 from cli/api-docs Add more examples to `api` docs commit 9a149d7694b7ce767dcd84f9dfa91e0c89b4d0ca Author: Cristian Dominguez Date: Thu Feb 11 19:43:08 2021 -0300 Add `repo list` command commit 8511365afb56a3ea24b3c7f17768ef2080b519ad Author: Michael Neeley Date: Thu Feb 11 16:46:16 2021 -0500 linter commit 0d55f8648c0540c4a0aaaf568a39967fbbff93ed Author: Michael Neeley Date: Thu Feb 11 16:27:23 2021 -0500 adds merge state status commit 335f0117c0d0be4af63692ccab866b9cc9a227fc Author: Mislav Marohnić Date: Thu Feb 11 19:16:11 2021 +0100 Add more examples to `api` docs - Clarify that fields need to be in "key=value" format - Headers need to be in "key:value" format - Contrast POST vs GET requests with params in examples - Add an example of how to add HTTP headers - Use backticks where applicable commit 83bb1bfd9d359b235b27e3b4606683eae09bac57 Author: Mislav Marohnić Date: Wed Feb 10 18:18:41 2021 +0100 Port `pr create` to new templates implementation commit 3ddd93793c3060ee61907822344b469cbb186921 Author: Mislav Marohnić Date: Wed Feb 10 17:32:00 2021 +0100 Port `issue create` to using templates API commit cd9f211826c7f6a35d218401f81b8f5db750b7b7 Author: Sam Coe Date: Tue Feb 9 09:48:58 2021 -0800 Remove unused code commit 5af2dca351fad13fbce8830039d34db1277afa64 Merge: f43fb26a feb4acc2 Author: Mislav Marohnić Date: Tue Feb 9 18:39:42 2021 +0100 Merge pull request #2929 from cli/brew-upgrade-suggestion Suggest `brew upgrade gh` when new version detected commit c7eb7382f01c13e3549ed36fd771e9e07d2a185b Author: Divya Ramanathan Date: Wed Dec 2 21:53:24 2020 -0500 implementing issue template GraphQL API call Co-authored-by: Zach Boyle commit 4ed94c2a069843b554e61ddc98e695c5fcf91d6f Author: Sam Coe Date: Mon Feb 8 09:59:30 2021 -0800 Fix up flag descriptions commit 68f71d82a0f69607bb6f9e2bd3513d7f19da166c Author: Sam Coe Date: Mon Feb 8 09:17:04 2021 -0800 Remove webmode commit feb4acc2c00ce42d5ed67252ae1ec66addeb786d Author: Mislav Marohnić Date: Mon Feb 8 13:57:08 2021 +0100 Suggest `brew upgrade gh` when new version detected When the update notifier is enabled and a new version was detected, show a Homebrew upgrade notice if: - the release was at least 24 hours ago; and - the current `gh` binary is under the Homebrew prefix. commit f43fb26acf196ef7ec3d9384c0360c38361a1fc2 Merge: 092cc4c4 9920ea97 Author: Mislav Marohnić Date: Sat Feb 6 13:31:07 2021 +0100 Merge pull request #2926 from xvqxy/verbose_build Display output of build commands. commit 9920ea97f6b51f84d3376dbb30193c64370c1f9c Author: xvqxy <76832739+xvqxy@users.noreply.github.com> Date: Sat Feb 6 09:49:43 2021 +0100 Display output of build commands. This fixes #2920. Print out output of executed command to stdout/stderr. commit 9550ad0159a5788c4e954d20a6011c3463d0fcd7 Author: Mislav Marohnić Date: Fri Feb 5 10:16:42 2021 +0100 Tweak prompt for SSH passphrase Co-authored-by: Amanda Pinsker commit 092cc4c4bc5404fadbd6b9d942aa2924a3b339a8 Merge: d0553bbc 47baf8fb Author: Mislav Marohnić Date: Fri Feb 5 10:02:43 2021 +0100 Merge pull request #2909 from cli/docs-pr-create Explain how to link an issue in `pr create` commit b366802aa1b9a770f3b30d3d1f8605090a9769d1 Author: Sam Coe Date: Wed Jan 27 14:12:47 2021 -0800 Edit issue command commit 1bd6d7b36c4bf10b23c575e3f660eda1c6d2a2f3 Author: Mislav Marohnić Date: Thu Feb 4 18:36:42 2021 +0100 Extract parts of HTTPS-related setup shared between `login` and `refresh` commit 075b6f8aa6841bc66a85fad25f5fac5cc7129f76 Author: Mislav Marohnić Date: Thu Feb 4 17:40:09 2021 +0100 Remove `workflow` from default OAuth scopes We now request it conditionally only for "HTTPS" login flow commit d0553bbc9dcc439f4e52328284ced7806401baf2 Merge: 72a0ad49 ab21dbea Author: Mislav Marohnić Date: Thu Feb 4 12:56:39 2021 +0100 Merge pull request #2908 from cli/docs-repo-create Improve `repo create` docs commit 72a0ad49cf0a689d5f9182dec444aa2bfa143657 Merge: 481ec92e 9dcf47d5 Author: Mislav Marohnić Date: Thu Feb 4 12:49:20 2021 +0100 Merge pull request #2910 from cli/docs-release-create Clarify handling of git tags during `release create` commit 481ec92ebf5192a5583bf43acac929255b19b035 Merge: 962791bf 622317ee Author: Mislav Marohnić Date: Wed Feb 3 22:52:09 2021 +0100 Merge pull request #2898 from Ma3oBblu/add-gist-view-files add --files to list filenames in gist (#2885) commit 622317ee89a45eda6ba91c2b6b9e5d08ebfbc02b Author: Mislav Marohnić Date: Wed Feb 3 22:39:16 2021 +0100 Tweak gist docs commit 907524f459135df0abb6180d37973ac754a10e25 Author: Ruslan Gilyazetdinov Date: Tue Feb 2 19:17:23 2021 +0300 add --files to list filenames in gist (#2885) commit 9dcf47d5d17213bf43fdb85a5b3c1ef889bb1145 Author: Mislav Marohnić Date: Wed Feb 3 22:33:37 2021 +0100 `release create`: clarify handling of git tags commit 47baf8fb10a416af8affad89f62af533d7f920e6 Author: Mislav Marohnić Date: Wed Feb 3 22:17:31 2021 +0100 `pr create`: explain how to link an issue commit ab21dbeaf04898d1bb0505c5f2be9278ebff5933 Author: Mislav Marohnić Date: Wed Feb 3 22:05:06 2021 +0100 Improve `repo create` docs - Clarify what will happen when in the git directory vs. out; - List requirements for non-interactive use; - Demonstrate how to turn issues/wiki off. - Misc. formatting tweaks commit 962791bf27e289949de19c4bfb14158e887b0bb6 Merge: de5c04f7 3f7b1387 Author: Mislav Marohnić Date: Tue Feb 2 15:12:12 2021 +0100 Merge pull request #2888 from Ma3oBblu/gist-view-raw-fix remove gist description from single file raw view (#2886) commit 3f7b1387e55377d1644e5f606bc18a9d057b9d49 Author: Mislav Marohnić Date: Tue Feb 2 13:18:23 2021 +0100 Improve `gist view` rendering - Separate out logic to render a single file - Render directly to stdout instead of to string slice - Normalize whitespace between files; ensure no excessive trailing whitespace - Add terminal pager support - Sentence-case for flags commit bf4370bc3ac8498f238c2d788773c650c0125ae3 Author: Ruslan Gilyazetdinov Date: Tue Feb 2 10:56:29 2021 +0300 linter fixes commit 232dc7b7fa6559de74bd6b4b5902fd7985a772fe Author: Ruslan Gilyazetdinov Date: Tue Feb 2 10:54:07 2021 +0300 change condition for single file, remove empty lines in single file mode commit 5a110c8e42f8f0eea5b57a9b81ea0c2d33273a5a Author: Mislav Marohnić Date: Mon Feb 1 23:34:00 2021 +0100 Add SSH key generation & uploading to `gh auth login` flow commit de5c04f721d684dd629ff2a0ba307edc0801a9e0 Merge: 7e8348a6 7479b383 Author: Nate Smith Date: Mon Feb 1 13:20:51 2021 -0800 Merge pull request #2856 from cli/fix-rpms run createrepo via docker commit 8e86129e2eb37830fd88bb2e484a4c4d225e986c Author: Ruslan Gilyazetdinov Date: Mon Feb 1 18:49:26 2021 +0300 remove gist description from single file raw view (#2886) commit 7e8348a68f96c5983251f89871167797545dbbe0 Author: Mislav Marohnić Date: Mon Feb 1 13:18:15 2021 +0100 Remove duplicate link to report a security vulnerability We already have a `.github/security.md` file which auto-generates a link in the issue template chooser commit c8704260b150bbf0e3aefaaadc055963de3a74d3 Author: Mislav Marohnić Date: Mon Feb 1 13:16:33 2021 +0100 Add additional resources to issue template chooser commit d771bef1068ca73d6573f4385d758d76ec5342fc Merge: d91b3121 051fbbc1 Author: Mislav Marohnić Date: Thu Jan 28 22:38:50 2021 +0100 Merge pull request #2858 from dpromanko/dpromanko/remove-set-cmd-prepare Remove use of run.SetPrepareCmd in tests commit 051fbbc1e11c56ab4d613532d66f0ae5903b9605 Merge: d86cfe46 d91b3121 Author: Mislav Marohnić Date: Thu Jan 28 22:00:08 2021 +0100 Merge remote-tracking branch 'origin' into dpromanko/remove-set-cmd-prepare commit d86cfe4627742123e9a55101a693b3591f8573f8 Author: Mislav Marohnić Date: Thu Jan 28 21:59:30 2021 +0100 Unpublish SetPrepareCmd commit 88c27934a1724ff5eda0c4c42b2b8fde9bf2e2e9 Author: Mislav Marohnić Date: Thu Jan 28 21:58:45 2021 +0100 Update some stubs to be closer to how git behaves commit d91b3121c8df75d493d2471835f445413af9689a Merge: 11d3972b 4b036f66 Author: Nate Smith Date: Wed Jan 27 16:41:30 2021 -0800 Merge pull request #2839 from kevinmbeaulieu/kb/delete-issue-cmd Add `issue delete` command commit 4b036f66755cf35f44e786f28c7533f553754e60 Author: Kevin Beaulieu Date: Wed Jan 27 16:30:48 2021 -0800 Skip confirmation for non-interactive contexts commit 2964895a7746f9bb36e37e72b3f9c9869ae0a3d8 Author: Devon Romanko <28825608+dpromanko@users.noreply.github.com> Date: Wed Jan 27 17:55:28 2021 -0500 fix test behavior changes from migration to run.Stub commit 11d3972bace0cedd9415a3792218da34bf39fb78 Merge: c71dc877 52bdcad8 Author: Sam Date: Wed Jan 27 11:20:01 2021 -0800 Merge pull request #2859 from cli/hidden-comments Do not display minimized comments commit 7479b3834de11f10bbe1bd6f2216ce5746f433b0 Author: vilmibm Date: Wed Jan 27 10:58:54 2021 -0800 use volume to avoid having to rebuild commit 773c8b39230d3fab518ea97a8af43e71deb6c234 Author: vilmibm Date: Wed Jan 27 10:58:08 2021 -0800 no longer try and install createrepo commit 33c119aa980680b49f375c6aa1e8aa310da942c8 Author: vilmibm Date: Wed Jan 27 10:41:03 2021 -0800 remove stray debug line commit 52bdcad8eade31414d0e87d0455d8d4ff2649753 Author: Sam Coe Date: Tue Jan 26 10:19:52 2021 -0800 Do not display minimized comments commit cb897fd7e2b953bba02ddaff0d5a241f0050fc49 Author: Devon Romanko <28825608+dpromanko@users.noreply.github.com> Date: Wed Jan 27 08:04:57 2021 -0500 remove unused errorStub from 'pr checkout' test commit 696cbfc8d1443f8263e415184fb533cd993335dd Author: Devon Romanko <28825608+dpromanko@users.noreply.github.com> Date: Wed Jan 27 07:59:32 2021 -0500 use Stub instead of SetPrepareCmd in 'repo create' tests commit a04e0ece717fead00d5b7a8e84539c5dde88230d Author: Devon Romanko <28825608+dpromanko@users.noreply.github.com> Date: Wed Jan 27 07:53:21 2021 -0500 use Stub instead of SetPrepareCmd in 'pr checkout' tests commit 45bc1d787c09319287106d55c1c565137c6a8244 Author: Devon Romanko <28825608+dpromanko@users.noreply.github.com> Date: Wed Jan 27 07:23:49 2021 -0500 use Stub instead of SetPrepareCmd in 'pr view' tests commit 6ef5248d2156f03da884fd34c7ea1213cb9b17d8 Author: vilmibm Date: Tue Jan 26 17:08:08 2021 -0800 cp -r instead of mv commit 39628a43548989a2c46245486aa1d893e22c1dd6 Author: vilmibm Date: Tue Jan 26 16:53:00 2021 -0800 use new docker-based script commit a603526e013e38b250c326e5d7a76adb28699f68 Author: vilmibm Date: Tue Jan 26 16:44:37 2021 -0800 clean up temporary directory commit 71da09e560531c9c74b211ca4b6f1bc8396ada18 Author: vilmibm Date: Tue Jan 26 16:43:53 2021 -0800 add docker-based script for running createrepo commit 9dcd3fbacf10ce218486844e8d5025aef8d10924 Author: Devon Romanko <28825608+dpromanko@users.noreply.github.com> Date: Tue Jan 26 08:24:22 2021 -0500 use Stub instead of SetPrepareCmd in 'issue create' tests commit 3068d80a279f8b2f986bd62c1be86e7918a8f88b Author: Devon Romanko <28825608+dpromanko@users.noreply.github.com> Date: Tue Jan 26 08:08:56 2021 -0500 use Stub instead of SetPrepareCmd in 'issue view' tests commit 2eed1593ce28a1bd098f81d3486230ee0b8697cd Author: Devon Romanko <28825608+dpromanko@users.noreply.github.com> Date: Tue Jan 26 08:02:32 2021 -0500 use Stub instead of SetPrepareCmd in 'pr list' tests commit 877cbbb5dc22d9b54f34eb91ae0d7c240785da18 Author: Devon Romanko <28825608+dpromanko@users.noreply.github.com> Date: Tue Jan 26 07:59:03 2021 -0500 use Stub instead of SetPrepareCmd in 'issue list' tests commit 4468a5c950662d9938b7527d81861f9f9e644d34 Author: Kevin Beaulieu Date: Mon Jan 25 18:24:52 2021 -0800 Update tests to handle confirmation commit e7df220b01476fbb217410c7dc8866f9a5b8dedb Author: Kevin Beaulieu Date: Mon Jan 25 18:03:45 2021 -0800 Add confirmation prompt commit c71dc877b1fbd796f0def8285744162010ed4d4c Merge: e6e92082 3f172ad9 Author: Sam Date: Mon Jan 25 13:56:47 2021 -0800 Merge pull request #2255 from quiye/alert-merging-with-unpushed Alert unpushed commits when merging a pull request commit 3f172ad9912b67eb3ddab4959c9c20169fd387f6 Author: Sam Coe Date: Mon Jan 25 12:03:14 2021 -0800 Add contional and tests commit e4b9f7cb8c3935d22aea08e55aa8a3fcf2e9e472 Author: zamasu Date: Thu Oct 22 21:15:10 2020 +0900 Alert unpushed commits when merging a pull request commit e6e920827f75b6a1ee9a08df181340cebac66258 Merge: e9afac93 85a62678 Author: Sam Date: Mon Jan 25 11:50:02 2021 -0800 Merge pull request #2444 from bigkevmcd/config-dir-from-env Configuration path can come from environment. commit 85a6267810f930b8b306cec4000acb5b2f637650 Author: Sam Coe Date: Fri Jan 22 10:44:27 2021 -0800 Remove last hardcoded config paths and fix tests commit e9afac9373fb9b7f795d1bf3b677f6917c76d109 Merge: 6152d8a4 f8135c15 Author: Nate Smith Date: Mon Jan 25 11:34:59 2021 -0800 Merge pull request #2844 from cli/secret-set-fix Fix `secret set --repos` for repositories that have dashes commit 6152d8a419cfabffeaa2ea792f1008a8784a8013 Merge: 8125cd59 26f67614 Author: Nate Smith Date: Mon Jan 25 11:29:52 2021 -0800 Merge pull request #2825 from cli/rename-fork restore fork rename behavior for nontty case commit 8125cd5911c62560c5c3b3917f706c52a6097c04 Merge: 9cbfdac2 cec3aa29 Author: Sam Date: Mon Jan 25 11:29:36 2021 -0800 Merge pull request #2117 from fsmiamoto/checkout-detached-head Add flag for using detached HEAD to `pr checkout` commit 26f6761481f035325a0776571bf1cb08b9389270 Author: vilmibm Date: Mon Jan 25 11:29:26 2021 -0800 add note about future behavior commit cec3aa294efe22a0a689e1f6c21106eb01bbd8b9 Author: Sam Coe Date: Mon Jan 25 11:15:30 2021 -0800 Support detach head for pr checkout commit 9cbfdac2c01894ec0651b9f14515a0c444c94143 Merge: d0a46399 70c4cbf2 Author: Nate Smith Date: Mon Jan 25 11:21:11 2021 -0800 Merge pull request #2799 from cli/success-icon-consistency Consistently use success icon commit d0fe1ce61b1dfc9d5f1985421a3163df8f7d484b Merge: 57d5470d d0a46399 Author: vilmibm Date: Mon Jan 25 10:16:18 2021 -0800 Merge remote-tracking branch 'origin/trunk' into kb/delete-issue-cmd commit d54a7618d44102fc3edd6e01d2cc3e5b099fcc3e Author: Kevin McDermott Date: Fri Nov 20 09:17:17 2020 +0000 Configuration path can come from environment. This adds support for using the GH_CONFIG_DIR environment variable to determine where the config files are written, this is useful for cases where the homedir is not writable. commit 70c4cbf240266e786d3a31d75d257cbd0d65fb22 Merge: f46bab25 d0a46399 Author: Mislav Marohnić Date: Mon Jan 25 14:57:04 2021 +0100 Merge remote-tracking branch 'origin' into success-icon-consistency commit f46bab256ce4a4f26ae491fa8f88c25ab7756f4b Author: Mislav Marohnić Date: Mon Jan 25 14:56:39 2021 +0100 Rename to `SuccessIconWithColor` commit f8135c15b126cc56b3cf5bab3e873636cfb345ca Author: Mislav Marohnić Date: Mon Jan 25 14:29:11 2021 +0100 Fix `secret set --repos` for repositories that have dashes A GraphQL alias cannot contain dashes. Instead, generate safe identifiers for GraphQL aliases. commit d0a46399b701c08270229f635c9fb44372a0b667 Merge: a881c130 9c4c1efc Author: Mislav Marohnić Date: Mon Jan 25 14:06:10 2021 +0100 Merge pull request #2828 from dpromanko/dpromanko/remove-init-cmd-stubber Remove use of deprecated test.InitCmdStubber commit 9c4c1efc7811db97f865bfb46e3f8f24414263f9 Merge: 8cba14b5 a881c130 Author: Mislav Marohnić Date: Mon Jan 25 13:56:28 2021 +0100 Merge remote-tracking branch 'origin' into dpromanko/remove-init-cmd-stubber commit a881c130c391cb49fc750bd174a3e29d21adc468 Merge: 6e2c1b33 d465b7f5 Author: Mislav Marohnić Date: Mon Jan 25 13:56:00 2021 +0100 Merge pull request #2843 from cli/freeze-time-in-test Freeze time in `issue view` test commit d465b7f5d5852f35f9539a7c6d6451e9b899f4ad Author: Mislav Marohnić Date: Mon Jan 25 13:46:30 2021 +0100 Freeze time in `issue view` test commit 8cba14b564454952c4b3d0c49fc310c2c514764d Author: Mislav Marohnić Date: Mon Jan 25 13:13:36 2021 +0100 :nail_care: cleanup command stub assertions commit 57d5470df912ccf743fa6b88d1cf19fe66ebcd5d Author: Kevin Beaulieu Date: Sun Jan 24 15:08:19 2021 -0800 Add `issue delete` command Similar to `issue close`, but for deleting an issue rather than just closing it. Resolves cli/cli#2820. commit e39c9d8f9fd8799f3333cb2928cb1c7182f7eebc Author: Devon Romanko <28825608+dpromanko@users.noreply.github.com> Date: Sat Jan 23 08:31:49 2021 -0500 remove new uses of InitCmdStubber after rebase commit 5d23f116c993b8cc51766026be8a3f587b5696b5 Author: Devon Romanko <28825608+dpromanko@users.noreply.github.com> Date: Wed Jan 20 07:57:48 2021 -0500 removed no longer used CmdStubber commit ac5bfc09b8b32bed4caa5c78e1378c5d33c978c1 Author: Devon Romanko <28825608+dpromanko@users.noreply.github.com> Date: Wed Jan 20 07:19:53 2021 -0500 remove use of deprecated InitCmdStubber commit d051f0634fdf085b2387351c8b0471f71ae1e009 Author: vilmibm Date: Fri Jan 22 16:13:39 2021 -0800 update tests commit 1fa3047b59d07027e233dd6fe8f038718fcf3102 Author: vilmibm Date: Fri Jan 22 15:52:23 2021 -0800 restore renaming behavior for nontty case commit 96fa6e7830da6198aa99d632f69ed27f58facba3 Merge: 23d68705 6e2c1b33 Author: Mislav Marohnić Date: Fri Jan 22 23:56:54 2021 +0100 Merge remote-tracking branch 'origin' into success-icon-consistency commit 23d68705bc3f21f0da55526cd2de90225d457554 Author: Mislav Marohnić Date: Fri Jan 22 23:55:33 2021 +0100 Match color of the success icon with the end state of the record commit 6e2c1b33b0a046a7290b645e87774752b3a0d45e Merge: 20e167a7 f09b8a8e Author: Nate Smith Date: Fri Jan 22 14:23:23 2021 -0800 Merge pull request #2224 from divbhasin/1190-limit-merge-methods #1190 - limit merge methods commit 20e167a7ae081aa28d1a1228278db9a57eb79f84 Merge: 9f7fd6e9 1c54db49 Author: Mislav Marohnić Date: Fri Jan 22 23:20:12 2021 +0100 Merge pull request #2108 from AliabbasMerchant/fix1309 fix: Project Argument working alongwith --web flag commit 1c54db49191531d46e9f57ddca8704b8ccc7f10b Author: AliabbasMerchant Date: Tue Oct 6 17:55:23 2020 +0530 fix: Project Argument working along with --web flag commit 9f7fd6e9171c86b467663fdf75127bd3d46848ee Merge: 77cc378a 874375f0 Author: Mislav Marohnić Date: Fri Jan 22 22:47:05 2021 +0100 Merge pull request #1953 from neelr/trunk Add ability to force checkout PR commit 874375f01e1fdf48da484f2f5bb923e4afd5281f Author: Mislav Marohnić Date: Fri Jan 22 22:29:14 2021 +0100 :nail_care: `pr checkout --force` commit a89fa1ebed812c0941868da7bd370e91e82dd932 Author: Neel Redkar Date: Thu Sep 24 16:26:35 2020 -0700 add ability to force checkout commit 77cc378ae02e0f04c6da4f4dd9bcf9a0a5feb354 Merge: 7fecaaa8 e334a1f1 Author: Mislav Marohnić Date: Fri Jan 22 22:17:24 2021 +0100 Merge pull request #2004 from marclop/f/add-self-assign-flag-to-issue-create Allow assigning to or filtering by special `@me` keyword commit e334a1f10c2291559faab1d6a9c7fdf3a40b47a8 Author: Mislav Marohnić Date: Fri Jan 22 22:14:47 2021 +0100 Add docs for using `@me` to reference self commit 73da25a96d26a260533dff868e3492c17c81628d Author: Mislav Marohnić Date: Fri Jan 22 21:57:46 2021 +0100 Use concrete type instead of an interface to avoid forced casting commit 28c2d042e7862ee79a15b01d8d49becb460fd11e Author: Mislav Marohnić Date: Fri Jan 22 21:53:50 2021 +0100 Extend `@me` replacing behavior to `issue list` commit 6ad0c57a310d2316ed3492d15660a33292c7095a Author: Marc Lopez Date: Mon Sep 28 09:46:55 2020 +0300 issue/pr create: add "@me" syntax to self-assign Signed-off-by: Marc Lopez commit 7fecaaa8e28aac3132c27699426df816fed23371 Merge: 188ff13d 64fda211 Author: Mislav Marohnić Date: Fri Jan 22 20:29:39 2021 +0100 Merge pull request #2740 from Matt-Gleich/homedir Refactor to use os.UserHomeDir() commit 64fda21116b23af846836eb97732545d62c39528 Author: Mislav Marohnić Date: Fri Jan 22 20:19:59 2021 +0100 Avoid ever invoking go-homedir when config was found in a new location commit 7d9461b819921280c34d621b85dae4286c3e0938 Author: Matthew Gleich Date: Wed Jan 6 15:13:58 2021 -0500 ♻️ Refactor to use os.UserHomeDir() commit 188ff13d28ffa0a2b92b7d512ef37ca3d649a39d Merge: bdce08a7 71fd2fa2 Author: Sam Date: Fri Jan 22 08:56:17 2021 -0800 Merge pull request #2785 from n1lesh/auth-status-fail Add fail message for non-existent hostname commit 71fd2fa24c85fafdc70876974d3ca1852f6b143c Author: Sam Coe Date: Fri Jan 22 08:50:09 2021 -0800 Fix up test commit 5e10638b0e44060d9f38d0ba3368534c49a1f691 Merge: f3fcaf6c bdce08a7 Author: Sam Coe Date: Fri Jan 22 08:46:24 2021 -0800 Merge branch 'trunk' into auth-status-fail commit bdce08a793abe4744c02d537e37c9ba63601a849 Merge: 4d28c791 06cf2c9f Author: Mislav Marohnić Date: Fri Jan 22 17:34:35 2021 +0100 Merge pull request #2801 from cli/cmd-stub-new Deprecate `test` package commit 06cf2c9f81c387a4c8a42b9453386c14a011db17 Merge: 14b80470 4d28c791 Author: Mislav Marohnić Date: Fri Jan 22 16:05:56 2021 +0100 Merge remote-tracking branch 'origin' into cmd-stub-new commit 4d28c791921621550f19a4c6bcc13778a7525025 Merge: d25938e2 6decf438 Author: Nate Smith Date: Thu Jan 21 15:25:42 2021 -0800 Merge pull request #2810 from ptxmac/ptx/pr-merge-body Add body argument to `pr merge` command. commit 6decf4384fa95b8c65e3b884a2cf6e0b39c60f7d Author: vilmibm Date: Thu Jan 21 15:18:08 2021 -0800 simplify slightly, add tests commit f09b8a8e788ea9b55d4268e4ea8bcf39bfc93d06 Author: Sam Coe Date: Thu Jan 21 14:56:05 2021 -0800 Add mergeMethodSurvey test commit 8624a9397e22481c3e939372874f4b22ac842b41 Author: vilmibm Date: Thu Jan 21 14:57:30 2021 -0800 remove stray binary commit d25938e223719443f69e48571c256769c3c14f40 Merge: 3a9e47bf 05d39ebd Author: Nate Smith Date: Thu Jan 21 14:54:15 2021 -0800 Merge pull request #2808 from KarelCoudijzer/fork-hang Show progress while creating a new pull request commit 05d39ebd0de3717b7dfe029e2440ca46c3986048 Author: vilmibm Date: Thu Jan 21 14:53:19 2021 -0800 remove garbling spinner one of the three new spinners produced less than ideal output when a push happened; the other two do enhance the create experience and i think we can get away without one for push. commit 11e873c66975d021c2e5a809617210238ded499b Author: Sam Coe Date: Thu Jan 21 14:45:06 2021 -0800 Cleanup impossible code path commit 7cc2975a981b4a45e494941d6b225b04e3bae005 Author: Sam Coe Date: Thu Jan 21 14:38:42 2021 -0800 Fix tests commit 3a9e47bf1bab25fd1a7926967542a89735c757a6 Merge: 29805a40 55c717d3 Author: Nate Smith Date: Thu Jan 21 14:41:03 2021 -0800 Merge pull request #2664 from cristiand391/gist-friendly-error Print friendly error when 'gh gist ' is missing required argument commit a305ff1488c876e38ebb43b9f7b27fbaef946fe2 Author: Sam Coe Date: Thu Jan 21 14:00:33 2021 -0800 Split apart interactive merge survey function commit 2d782fcb46cb2c27dac3c5a940a4b8ccd8f3870b Author: Sam Coe Date: Thu Jan 21 13:13:58 2021 -0800 Retrieve repo outside of survey function commit 29805a4003c3b2fe99eb670b90d12e405db6e4fd Merge: 4860cccd b906826a Author: Nate Smith Date: Thu Jan 21 12:57:10 2021 -0800 Merge pull request #2588 from cdce8p/gh-clone-fetch Only fetch default branch when adding upstream remote commit b906826a6835598f7d5026cb60b462b903e83e77 Author: vilmibm Date: Thu Jan 21 12:56:53 2021 -0800 i like trunk commit 71a66cc8d643093ade175edd7cd436d2861dce0a Author: Sam Coe Date: Thu Jan 21 12:53:04 2021 -0800 Fix merge commit a9123966e38f4043a955e107bf5a76d92a361f69 Merge: 8b0618f4 4860cccd Author: Sam Coe Date: Thu Jan 21 12:45:10 2021 -0800 Merge branch 'trunk' into 1190-limit-merge-methods commit dcedd32249111fc62eb7a16579c39df36e4b3c8f Author: vilmibm Date: Thu Jan 21 12:32:40 2021 -0800 use newer command stubbing in tests commit 4860cccd72b93ba15dcfbbefa5f1e1b46532f6c6 Merge: 938f6f4b 48c89076 Author: Nate Smith Date: Thu Jan 21 12:11:42 2021 -0800 Merge pull request #2282 from cristiand391/repo-fork-gitflags Add support for git flags in gh repo fork commit 48c89076f667b943c4e233cbb0746b17eaf97d6a Author: vilmibm Date: Thu Jan 21 12:04:19 2021 -0800 add positive case test commit a27a94f8b53bb78b08eb0de5650f218cf2d870f9 Merge: 07bd6472 938f6f4b Author: vilmibm Date: Thu Jan 21 11:58:20 2021 -0800 Merge remote-tracking branch 'origin/trunk' into repo-fork-gitflags commit 14b804703280fbb4875ec80b168d24891b9d9166 Merge: 411bd4a7 fc77cbc9 Author: Nate Smith Date: Thu Jan 21 11:45:20 2021 -0800 Merge pull request #2803 from cli/expectlines-deprecate Deprecate `test.ExpectLines` commit 938f6f4bdde9c6fc7c1cea00593f8bbfc8751e0f Merge: 948088a1 3797aa72 Author: Sam Date: Thu Jan 21 09:56:15 2021 -0800 Merge pull request #2809 from cli/deadcode delete unused parameter commit 948088a143be9ecf5e4d0bfa066ebb4fa7045007 Merge: 10b1314d a70b69e3 Author: Sam Date: Thu Jan 21 09:55:52 2021 -0800 Merge pull request #2776 from cli/pr-comments Comment on pull requests commit a70b69e359533197524416c92c555075eb7dc68e Author: Mislav Marohnić Date: Thu Jan 21 17:41:55 2021 +0100 Bring the "Press Enter" UI closer to the authentication experience - "Press Enter" is both bold - "Enter" is capitalized - The prompt ends with "..." commit a26fba7800b7e012116f4223b40fe4a7c49e26e6 Author: Sam Coe Date: Tue Jan 12 11:29:55 2021 -0800 Comment on pull requests commit 10b1314dc1ccf5c264a5e0771332c3cf52c233c4 Author: Mislav Marohnić Date: Thu Jan 21 17:55:22 2021 +0100 Hide `ssh-key` command until it's ready for prime-time commit f89346f335a1e81953211667c763df5cb9483eb8 Merge: ad62d6a4 3673a9be Author: Mislav Marohnić Date: Thu Jan 21 17:36:36 2021 +0100 Merge pull request #2748 from cli/makefile-rewrite Port build tasks to Go script commit ad62d6a4713b4907dad69f009fe2840c8dd333d3 Merge: 4158209d 6e5a9082 Author: Mislav Marohnić Date: Thu Jan 21 17:36:00 2021 +0100 Merge pull request #2798 from cli/pr-merge-crossrepo Handle case when a cross-repo PR was already merged commit 4158209d50aa95120d19f77adda8962de5b67437 Merge: f1a9da40 a2bee1fa Author: Mislav Marohnić Date: Thu Jan 21 17:34:39 2021 +0100 Merge pull request #2811 from cli/utils-spinner-buh-bye Retire utils.Spinner in favor of IOStreams.StartProgressIndicator commit 3673a9beb2579cd414580f2f05194258d7ae566b Author: Mislav Marohnić Date: Thu Jan 21 17:16:11 2021 +0100 Add more documentation for `script/build.go` commit f1a9da40a414896972b84167e5de7edceab3555d Merge: 65572896 82d19b73 Author: Mislav Marohnić Date: Thu Jan 21 15:11:23 2021 +0100 Merge pull request #2812 from cli/updater-dev-version-match Update notifier: avoid false positives when gh is built from source commit 655728965d83968873d3e39db59a1ea23bd4f62d Merge: 5430728a 509e5dd0 Author: Nate Smith Date: Wed Jan 20 16:19:10 2021 -0800 Merge pull request #1882 from dfireBird/remote-renaming-847 feat: implement prompt for remote renaming commit 509e5dd0c90d6c32e56ed5b545dede4eacf8a1ed Author: vilmibm Date: Wed Jan 20 16:10:56 2021 -0800 fix tests commit 99c312e8ceeada3a4fbcd6dce158f418df2197dc Author: vilmibm Date: Wed Jan 20 15:32:19 2021 -0800 accept a remote name instead of doing magic remote naming in repo fork commit 03f99a01402d9d642b1d4240993a757e75f2b008 Merge: e513333f 5430728a Author: vilmibm Date: Wed Jan 20 15:10:20 2021 -0800 Merge remote-tracking branch 'origin/trunk' into remote-renaming-847 commit 5430728a0ab3c0351c39732cf2523a64891aba5a Merge: c9f79271 c9407b26 Author: Nate Smith Date: Wed Jan 20 15:06:52 2021 -0800 Merge pull request #2813 from cli/token-from-env-err More descriptive error when aborting auth due to environment variables commit c9f79271b17d3cb727b3586d4e50fc9a5fad8fca Author: Björn Heinrichs Date: Wed Jan 20 23:51:27 2021 +0100 Add --maintainer-edit flag (#2250) * Add --maintainer-edit flag Closes #2213 while retaining backwards compatibility. * Fix linting * Adjust documentation and validation * Negate logic and fix build errors * rename to no-maintainer-edit * test * use a positive option instead of negative Co-authored-by: vilmibm commit 3bec668aaa9d7ee3febbd6758de0573df95b0285 Merge: dcf5a27f b9b10794 Author: Sam Date: Wed Jan 20 12:22:03 2021 -0800 Merge pull request #2757 from cli/review-comments Display reviews when viewing pull requests commit b9b1079493221bb419bcd0c705ecf4b6e1bb18cc Author: Sam Coe Date: Thu Jan 7 15:19:27 2021 -0800 Display reviews when viewing pull requests commit dcf5a27f5343ea0e9b3ef71ca37a4c3948102667 Merge: 2086d135 4a2cc8d2 Author: Mislav Marohnić Date: Wed Jan 20 20:47:21 2021 +0100 Merge pull request #1862 from edualb/trunk [#1755] SSH key management (gh ssh-key list) commit 4a2cc8d2a467a5ce57a36de9475bd6325627291e Author: Mislav Marohnić Date: Mon Jan 18 17:23:54 2021 +0100 Simplify `ssh-key list` Do not require nor request `read:public_key` scope by default. commit e26a1b98a1ad832bdd97cbba90a0e4e98ad09892 Author: edualb Date: Fri Sep 18 18:27:27 2020 -0300 add ssh-key command commit 2086d135f3cf5fa7e4b99828040d84d2880b3eeb Author: Cristian Dominguez <6853656+cristiand391@users.noreply.github.com> Date: Wed Jan 20 18:44:46 2021 +0000 Respect system/user timezone in API requests (#2630) * Respect system/user timezone in API requests * Fall back to a known timezone if TZ is not set Co-authored-by: Cristian Dominguez commit c9407b2629219021925df4290efa1c8ed207145a Author: Mislav Marohnić Date: Fri Jan 8 18:13:19 2021 +0100 More descriptive error when aborting auth due to environment variables Old message: read-only token in GH_TOKEN cannot be modified This message was vague and some users did not understand that this refers to the value that is read from environment variables. New message: $ GH_TOKEN=123 ghd auth login -h github.com The value of the GH_TOKEN environment variable is being used for authentication. To have GitHub CLI store credentials instead, first clear the value from the environment. commit 82d19b73d63df2eb27885c181f1ccd8ad5d298ef Author: Mislav Marohnić Date: Wed Jan 20 16:10:20 2021 +0100 Update notifier: avoid false positives when gh is built from source When gh was built from source, the version number will look something like this since it's taken from `git describe`: v1.4.0-34-g{SHA} When compared as semver against `v1.4.0`, the latter version is falsely reported as newer. This is because the output of `git describe` wasn't meant to be interpreted as semver. The solution is to translate the `git describe` string to faux-semver so it can be safely compared with the version reported from the server. Fixes this case: A new release of gh is available: v1.4.0-41-g2f9e4cb1 → v1.4.0 https://github.com/cli/cli/releases/tag/v1.4.0 commit a2bee1fad349093ca6300ae34986904270fa40e3 Author: Mislav Marohnić Date: Wed Jan 20 14:48:14 2021 +0100 :fire: utils.Spinner commit aa0de5f636ce38a211749b6e1ea04e716c19eaa1 Author: Mislav Marohnić Date: Wed Jan 20 14:46:45 2021 +0100 Stop using utils.Spinner in `repo fork` commit bc7f73326728496b06fd30c9db3c771a6b5b2948 Author: Peter Kristensen Date: Wed Jan 20 12:09:29 2021 +0100 Add body argument to `pr merge` command. commit 3797aa72ff4b3a1354e9b3f6de1cc144c8af294f Author: vilmibm Date: Tue Jan 19 19:08:49 2021 -0800 delete unused parameter commit a7b9e3916c228c7daf89aa006fc2ac7ba2f9abd8 Author: Karel Coudijzer Date: Tue Jan 19 21:29:24 2021 +0100 Show progress while creating pr commit b5366c6ebf22589f62089ea79c349538335a3e99 Merge: e566c616 75ebb863 Author: Mislav Marohnić Date: Tue Jan 19 14:08:25 2021 +0100 Merge pull request #2794 from cristiand391/use-testify-assertion Use Testify assertions in tests commit 75ebb863e3b2cfbaf1c6611a1af014d7ece7be7d Author: Mislav Marohnić Date: Tue Jan 19 13:59:37 2021 +0100 Use testify assertions for error matching commit 45f4a1f087984b4d048cf6f57183e21d17096506 Author: Cristian Dominguez Date: Mon Jan 18 21:00:59 2021 -0300 Equal: flip arguments position commit fc77cbc96403a9e6711d8749cb4d5ed9e9d732de Author: Mislav Marohnić Date: Mon Jan 18 23:25:45 2021 +0100 Deprecate `test.ExpectLines` For asserting command output, exact string matches are preferred in most cases. In cases when a pattern match is needed, the test can use regexp ad hoc. commit 411bd4a70e1a73b9f29e45c171ae2415e8416c2d Author: Mislav Marohnić Date: Mon Jan 18 22:53:03 2021 +0100 :fire: unused `test/fixtures/` commit c308f1cd910d52acf73bbb6f6eb6bfe469916490 Author: Mislav Marohnić Date: Mon Jan 18 22:44:53 2021 +0100 Prevent further use of SetPrepareCmd and InitCmdStubber commit 5531498f27a158edc4e58c95221ea4ece0aea04f Author: Mislav Marohnić Date: Mon Jan 18 22:42:27 2021 +0100 Migrate to new cmd stubber in `repo fork` tests commit 584b33e79ce85535abf4d6a54d2d2c7a6759bbaf Author: Mislav Marohnić Date: Mon Jan 18 22:42:13 2021 +0100 Migrate to new cmd stubber in `repo clone` tests commit c63acf672821f6a362dcf76960253afbb3ffae20 Author: Mislav Marohnić Date: Mon Jan 18 22:42:01 2021 +0100 Migrate to new cmd stubber in misc. tests commit 1717c8d0838ae3a46be34eebc39e305375349f0b Author: Mislav Marohnić Date: Mon Jan 18 22:39:59 2021 +0100 Migrate to new cmd stubber in git tests commit bf4bc1511f593d7ed44be7cfa066b058fe043e66 Author: Mislav Marohnić Date: Mon Jan 18 20:15:40 2021 +0100 Migrate to new cmd stubber in `merge` tests commit 683ebee6ef02d8639a9befad4c80b372680aea38 Author: Mislav Marohnić Date: Mon Jan 18 19:57:38 2021 +0100 Consistently use green success icon For operations such as closing an issue or merging a PR, we would display the success icon (a checkmark) in red and magenta colors, respectively, to reflect the latest state of the record operated on (red: closed; magenta: merged). This was always confusing to me, seeing it both in code and in the UI, because I'm instinctively thinking that it's a bug and have to remind myself that it's by design. commit 6e5a90821c0309da9ad6d01b20229fbc589c40ab Author: Mislav Marohnić Date: Mon Jan 18 19:49:20 2021 +0100 pr merge: handle case when a cross-repo PR was already merged In this case, do not ever offer to delete the branch. commit e566c616714b1c884a8e6b0578852ce3c78630cd Merge: 4750e4ae 66546e22 Author: Mislav Marohnić Date: Mon Jan 18 18:41:01 2021 +0100 Merge pull request #2789 from dpromanko/dpromanko/2625 commit 66546e2245108f117f262b49b172eba1d63328e5 Author: Mislav Marohnić Date: Mon Jan 18 18:10:20 2021 +0100 When `pr merge --delete-branch` flag is supplied, avoid prompting for it commit f3fcaf6c9c2422b5a40636bd2d6ef1f286e7166b Author: Nilesh Singh Date: Sun Jan 17 14:42:49 2021 +0530 Fix error message text & add test case commit 3afb1d0b1aeea95c594b376796aa5025f3b83335 Author: Cristian Dominguez Date: Sat Jan 16 19:19:30 2021 -0300 Use Testify assertions in test commit df31fae9c664e21e9d7910e4c6a277e6c862ff91 Author: Devon Romanko <28825608+dpromanko@users.noreply.github.com> Date: Fri Jan 15 17:18:04 2021 -0500 remove prompt for deleting branches on pr merge in interactive mode when -d flag is passed commit 2c35eb04ff7a454c19e83014510db9c1087aa6f5 Author: Devon Romanko <28825608+dpromanko@users.noreply.github.com> Date: Fri Jan 15 16:54:46 2021 -0500 address pr comments commit 85e0e44920181cd411ac5251bb944e6c6837a6a5 Author: Devon Romanko <28825608+dpromanko@users.noreply.github.com> Date: Fri Jan 15 07:25:24 2021 -0500 Add prompt to delete local branch when attempting to merge a PR that is already merged commit 2f5ffbd60ad91820ac0c76d96aab174f9d9285b3 Author: Nilesh Singh Date: Fri Jan 15 13:28:57 2021 +0530 Add fail message for non-existent hostname commit 4750e4ae18735375ef91e84fa5c148783743c379 Merge: b0ae09e6 3ab01661 Author: Mislav Marohnić Date: Wed Jan 13 14:40:38 2021 +0100 Merge pull request #2774 from rneatherway/codeql-add-pull-request-trigger Add on: pull_request trigger to CodeQL workflow commit 3ab01661e4c4baa03658d0328c10537a327d03a5 Author: Robin Neatherway Date: Wed Jan 13 11:09:00 2021 +0000 Add on: pull_request trigger to CodeQL workflow From February 2021, in order to provide feedback on pull requests, Code Scanning workflows must be configured with both `push` and `pull_request` triggers. This is because Code Scanning compares the results from a pull request against the results for the base branch to tell you only what has changed between the two. Early in the beta period we supported displaying results on pull requests for workflows with only `push` triggers, but have discontinued support as this proved to be less robust. See https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#scanning-pull-requests for more information on how best to configure your Code Scanning workflows. commit b0ae09e62772eaf0a126b750b2ad0b48b748cb45 Merge: 3985577d 723e9e31 Author: Sam Date: Tue Jan 12 11:24:09 2021 -0800 Merge pull request #2535 from cli/create-comments Create issue comments commit 3985577de07fd8594bff3aab6bc6d1ee46c7cad2 Merge: c73845ee ce151420 Author: Mislav Marohnić Date: Tue Jan 12 18:20:45 2021 +0100 Merge pull request #2759 from cristiand391/migrate-legacy-tests Migrate legacy HTTP tests commit ce151420f3ef3e12fa6c0a656aa17515ea62b0dd Author: Cristian Dominguez Date: Mon Jan 11 21:07:19 2021 -0300 Migrate legacy tests commit 723e9e31baaee1e3cbb35841cb07b16527542b43 Author: Sam Coe Date: Mon Jan 11 13:56:17 2021 -0800 Address PR comments commit c73845ee22a6676ec1ddc331f2359692501fda29 Merge: 5e977759 19ee0eff Author: Mislav Marohnić Date: Mon Jan 11 15:32:18 2021 +0100 Merge pull request #2742 from cli/linter-update Simplify linter output setup commit 39431a101d61760de562643565eb20f3c8a4d5ef Author: Mislav Marohnić Date: Fri Jan 8 22:30:21 2021 +0100 Port select portions of Makefile to `script/build.go` This is to enable build tasks on Windows. commit 19ee0eff0831ce17d5d1979cb46869e3436f8e05 Author: Mislav Marohnić Date: Thu Jan 7 16:16:44 2021 +0100 Simplify linter output setup `golangci-lint` now supports an output formatter for GitHub Actions, so we don't need to manually reformat the failure output anymore. commit 5e9777597825831777511c3f32351bcb751019e2 Author: Mislav Marohnić Date: Wed Jan 6 18:08:02 2021 +0100 Clarify building from source on Windows Address https://github.com/cli/cli/issues/2545#issuecomment-751842548 commit 155507d31d64322d31ae5ff43d8ec9cc3f327b6c Author: Sam Coe Date: Tue Jan 5 14:11:26 2021 -0800 Make comment command easier to test commit 8b5c5896f2632e97270bc2d735fc541b640a08c3 Author: xhqr <76452811+xhqr@users.noreply.github.com> Date: Tue Jan 5 23:40:52 2021 +0100 [repo/create] Create local repo dir with non tty. (#2671) This addresses issue #2587. commit de73b161784e791ac3b6093dd0d19c1235d5d30f Author: Sam Coe Date: Tue Jan 5 10:39:07 2021 -0800 Move CreateComment mutation commit 1fc8b66b261bdd0b9080abb171b7f0c0c8ed668b Author: Sam Coe Date: Mon Jan 4 15:58:57 2021 -0800 Address PR comments commit f862123071c7f485aec4396282f74c1b762b828a Author: Sam Coe Date: Mon Nov 30 16:00:30 2020 -0500 Modify issue commenting to adhere to designs and add tests commit 338bf1d112bd4ee7d87991b4c0fb49ba671c5e1c Author: Yuki Osaki Date: Tue Sep 29 00:32:03 2020 +0900 fix linter issue commit 8ef2bb4d14508d80080b6c9c98cfa930a8b1db0b Author: Yuki Osaki Date: Tue Sep 29 00:10:11 2020 +0900 Comment on issues from editor commit 86eb264277ada444f9488413ddc2aefc1c9f89c2 Merge: ca83106a 536a1733 Author: Mislav Marohnić Date: Tue Jan 5 16:41:22 2021 +0100 Merge pull request #2650 from cli/auth-extract Extract the oauth package into a separate repo commit 536a173364758194ab3a30daddc5e74b11eb2dac Author: Mislav Marohnić Date: Tue Jan 5 16:24:47 2021 +0100 Enable debugging HTTP traffic during `auth login/refresh` commit ca83106a38be828b6f6f5bfc56043119d88abe3b Merge: 4c53e5ee 21a57d92 Author: Mislav Marohnić Date: Tue Jan 5 15:33:56 2021 +0100 Merge pull request #2727 from cristiand391/remove-unused-methods Remove unused methods commit 4c53e5eee6f77f4b542ba497b564c8e0393564a6 Merge: 230441e1 8ff4cc40 Author: Sam Date: Mon Jan 4 11:20:40 2021 -0800 Merge pull request #2698 from camelid/view-newline view: Add missing newline commit 21a57d9253b3bc1e9e2ec91a33d3853c768e9d59 Author: Cristian Dominguez Date: Mon Jan 4 07:35:41 2021 -0300 Remove unused methods commit 8ff4cc40e642cc90715fabc5bdfc04bc698e74d0 Author: Camelid Date: Sun Dec 27 11:37:59 2020 -0800 view: Add missing newline Add a newline at the end of the 'View this {issue, pull request} on GitHub' message. `gh repo view` already had a newline at the end, so this only changes `issue view` and `pr view`. commit 55c717d3d37dcfa225ef1d7fc3ce52536b52092f Author: Cristian Dominguez Date: Mon Dec 21 10:01:30 2020 -0300 Print friendly error when 'gh gist ' is missing required argument commit 230441e1a594eea80d0f3384488925ddae074155 Merge: f26ad4cc 326fe371 Author: Nate Smith Date: Fri Dec 18 13:07:44 2020 -0800 Merge pull request #2222 from Crunch09/issue-2115 add `gist clone` command commit 9140e887085c962c0520d961cc4f23f83bf3b805 Author: Mislav Marohnić Date: Mon Dec 7 16:12:57 2020 +0100 Extract oauth package commit f26ad4cce4977110dc445b9baf42b68a11e4d5a2 Merge: 782932bf 7415d236 Author: Mislav Marohnić Date: Fri Dec 18 15:10:48 2020 +0100 Merge pull request #2639 from marmorag/bump-survey Bump AlecAivazis/survey commit 7415d236bcc0088c338ac15f20f49d0c05b32fee Author: marmorag Date: Wed Dec 16 18:52:36 2020 +0100 chore(deps): bump AlecAivazis/survey commit 782932bfae10833cbfa571a3bcfbcd658c6eaf60 Merge: 3d2d9e72 9d502216 Author: Mislav Marohnić Date: Thu Dec 17 18:21:19 2020 +0100 Merge pull request #2636 from cli/docs-completion Improve `completion` docs for bash, zsh, fish commit 3d2d9e7229aba68457749acfa2098776502fe799 Merge: 04e6446d 18b19e07 Author: Mislav Marohnić Date: Thu Dec 17 13:05:47 2020 +0100 Merge pull request #2638 from cli/version-output-fix Fix link in `version` output commit 18b19e074da2c2f04af78c59c75d0a168662aed1 Author: Mislav Marohnić Date: Wed Dec 16 17:07:41 2020 +0100 Fix link in `version` output It now correctly links to the tagged release instead of to the latest release. commit 9d50221669483e086575d924a86d833a1b930bf2 Author: Mislav Marohnić Date: Tue Dec 1 15:28:23 2020 +0100 Improve `completion` docs for bash, zsh, fish commit 04e6446d33fcedfd08cb0e39f917749d88389e66 Merge: 3b1e526a 8b197ac0 Author: Nate Smith Date: Tue Dec 15 11:18:32 2020 -0600 Merge pull request #2621 from cli/arm32 add arm build for raspberry pi commit 3b1e526a276dbfe4da9c7c34a4d0a0d8e81a678e Merge: 65e5ba9d 352cde05 Author: Nate Smith Date: Tue Dec 15 11:16:53 2020 -0600 Merge pull request #2529 from cli/441-secrets gh secret {set,list,remove} commit 65e5ba9da1b723495dc660ff47400c00164d3575 Merge: a77f3ddb 39a0a8c5 Author: Mislav Marohnić Date: Tue Dec 15 18:01:55 2020 +0100 Merge pull request #2493 from gunadhya/repo-clone-wiki Initial fix for gh-repo-clone wiki commit 39a0a8c57c851a2763248550348043cf2af7e163 Author: Mislav Marohnić Date: Tue Dec 15 17:38:21 2020 +0100 Improve clone wiki test commit fd57835bb9d620bc06b5e682a7484442535912cc Author: gunadhya <6939749+gunadhya@users.noreply.github.com> Date: Fri Nov 27 16:20:19 2020 +0530 Fix repo clone wiki commit a77f3ddb49ab2e66557b32922be50eabef56ed1f Merge: 1e4fa604 dee7077f Author: Mislav Marohnić Date: Tue Dec 15 16:31:03 2020 +0100 Merge pull request #2575 from cli/pr-view-comments Add ability to view comments with `pr view` commit 1e4fa60478439bdb35cde6fb1a5b90ef5ad1cb0d Merge: d81b2927 efc05dee Author: Mislav Marohnić Date: Tue Dec 15 16:22:32 2020 +0100 Merge pull request #2462 from cli/view-comments Add issue comment viewing commit d81b2927a597edc68c94a7022976884bf07121f0 Merge: b1f93426 2843ffff Author: Mislav Marohnić Date: Tue Dec 15 16:15:01 2020 +0100 Merge pull request #2488 from cristiand391/new-release-notification Notify new releases only once per day commit b1f93426ebc0c31de2947b99d7d9cd38bb5504fd Merge: ae68da65 ada59236 Author: Mislav Marohnić Date: Tue Dec 15 16:14:07 2020 +0100 Merge pull request #2449 from cli/git-credentials Set up git authentication when logging in to gh commit ae68da6520af6fafcdb8d6a85264dbfd5e84b7ec Merge: f3ea05f2 935f6444 Author: Mislav Marohnić Date: Tue Dec 15 16:11:27 2020 +0100 Merge pull request #2230 from alissonbrunosa/fix-2179 Add support for ssh_config Include directives commit 2843ffff23eb85859aa46227089b185af6a7e235 Author: Mislav Marohnić Date: Tue Dec 15 16:09:08 2020 +0100 Classify the `update` package as internal commit 42c97509ca18fd5057441ff13af9200751f6f47c Author: Sam Coe Date: Tue Dec 1 12:18:32 2020 -0500 Simplify CheckForUpdate handling of state file commit 0ec8c2e9edf969ab7968e6a6b8e2d86ee4504098 Author: Cristian Dominguez Date: Thu Nov 26 11:00:27 2020 -0300 Notify new releases only once per day commit 935f6444ae330b8ba89fd048c4ef5ef145e5910f Author: Mislav Marohnić Date: Tue Dec 15 15:02:49 2020 +0100 Refactor ssh parser for format compatibility & testability - Per ssh_config(5), keywords and arguments may be separated by an `=` sign as well as whitespace. - When following the `Include` directive, skip directories that were returned as the result of globbing. - Respect the `Host` context when recursing into `Include`s - Avoid having tests read from the actual filesystem. - Avoid repeatedly looking up the home directory. commit dc8698ee46b6fa2b0311a1352e429cbbf9e2599d Author: Alisson Santos Date: Mon Oct 19 15:09:48 2020 +0200 Make ssh parser to parse included config files commit f3ea05f2e1651982d30ab7faa305b6f1c7297732 Merge: c765c71e cba15d01 Author: Mislav Marohnić Date: Tue Dec 15 15:24:59 2020 +0100 Merge pull request #2624 from nopeinomicon/patch-1 Add openSUSE distro package install instructions commit cba15d0109bd1e3a650879935bacc7d05e36b62d Author: Mislav Marohnić Date: Tue Dec 15 15:16:33 2020 +0100 Clarify openSUSE Tumbleweed instructions commit b8522f683c571c70c9672f03f5dff0480fef5c42 Author: Emily Roberts Date: Tue Dec 15 01:17:44 2020 -0700 Add openSUSE distro package install instructions Added the instructions to install the GitHub CLI from the openSUSE distribution repositories commit 8b197ac0bc8f169862d296350554ec875192eb05 Author: vilmibm Date: Mon Dec 14 14:33:54 2020 -0800 package for armhf commit 8f0c388ad661376372f68299240d77a131e9dd6b Merge: 4a30800e c765c71e Author: vilmibm Date: Mon Dec 14 14:08:26 2020 -0800 Merge remote-tracking branch 'origin/trunk' into arm32 commit 4a30800eb934a74da62f3e4f006b79c375f136e6 Author: vilmibm Date: Mon Dec 14 14:05:50 2020 -0800 add arm build for raspberry pi commit c765c71e47463e1676c7356898430df87922e2ce Merge: 3ad9f39e 03949a4d Author: Mislav Marohnić Date: Mon Dec 14 21:04:15 2020 +0100 Merge pull request #2556 from pete-woods/build-static-binaries Build static binaries on Linux commit 3ad9f39ec4cc638c79d2e7ab28f09d86e4cec960 Merge: cee4f853 f853a4b0 Author: Mislav Marohnić Date: Mon Dec 14 21:01:23 2020 +0100 Merge pull request #2582 from dyl10s/Remove-Unknown-Check Allow API request to be made if a PR is in an "UNKNOWN" state commit cee4f853dc9e883116736949d5443b6c6acb4187 Merge: 46f2eae8 d7f68e9e Author: Mislav Marohnić Date: Mon Dec 14 20:30:56 2020 +0100 Merge pull request #2580 from ismaell/ldflags-fix Filter flags taken from LDFLAGS into CGO_LDFLAGS commit 46f2eae88f3e675bbecc9cd55cc1475246490c8c Merge: f4152454 1fb54225 Author: Nate Smith Date: Mon Dec 14 13:15:59 2020 -0600 Merge pull request #2612 from yzgyyang/patch-1 Document installation instructions for FreeBSD commit 352cde0563eed40ecdd98cd1c8c868250a62f6ce Author: vilmibm Date: Mon Dec 14 10:44:26 2020 -0800 do not process filename arguments commit 1fb542250ad7cb5c25660165ad6d93735db7de5e Author: Guangyuan Yang Date: Mon Dec 14 13:51:36 2020 +0800 Document installation instructions for FreeBSD commit a5a043c5a59e5dee35e3d09bc991dab00387a5d5 Author: vilmibm Date: Thu Dec 10 15:24:09 2020 -0800 automatically set vis when just -r passed commit 33063511624892aaab68a39a7a45b8a7a391168c Author: vilmibm Date: Thu Dec 10 15:11:08 2020 -0800 use type for Visibility commit e1e838c281fdeedd42c0e234dfaab48783feb182 Author: vilmibm Date: Thu Dec 10 15:04:56 2020 -0800 more explicit success message commit 2e6639fe78043983989e8e77a2822174243abf6f Author: vilmibm Date: Thu Dec 10 14:59:37 2020 -0800 show number of selected repositories in secret list commit 408d5c6d9683d533455d24030b0ee14b85ae0dc5 Author: vilmibm Date: Thu Dec 10 13:23:35 2020 -0800 no space in usage placeholder commit 4e8a6805756cf8bf50c848002169a79649c8afd3 Author: vilmibm Date: Thu Dec 10 13:22:11 2020 -0800 remove implied org functionality from secret set commit 1675bd9249c7ac559480dc131fe9650a5b98da90 Author: vilmibm Date: Thu Dec 10 12:45:38 2020 -0800 remove implied org functionality from secret remove also fill in missing test cases >_> commit c036e6699c3c8450cb925542973d0b4762d3fa73 Author: vilmibm Date: Thu Dec 10 12:36:57 2020 -0800 remove implied org functionality from secret list commit 2248565839cd469bea56ecb41b88108d13455669 Author: vilmibm Date: Wed Dec 9 17:31:34 2020 -0800 print success messages commit dbff17e6ed9b7418db5e4b86da557b836c5a387a Author: vilmibm Date: Wed Dec 9 15:44:31 2020 -0800 add removing secrets commit 486aa81dfe55c4276f5c67205b0272c9777c7b3f Author: vilmibm Date: Wed Dec 9 15:32:02 2020 -0800 validate secret name commit dee7077fcf80ab5675e2791f4a6bdca7d7cd629d Author: Sam Coe Date: Wed Dec 9 14:35:29 2020 -0500 Extract shared comment and reaction group code commit f536901bc1ac39a4771af570c193732c73eecaaf Author: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Wed Dec 9 20:10:59 2020 +0100 Only fetch DefaultBranchRef when adding upstream remote during gh repo clone commit efc05dee907cb764c659cf8f8a92d2c2652ce74d Author: Sam Coe Date: Wed Dec 9 13:50:08 2020 -0500 Use spinner helper commit f853a4b0e2d389206a9f641325ed3ca9e94fb91d Author: Dylan Strohschein Date: Wed Dec 9 00:25:21 2020 +0000 Allow API request to be made if the PR is in an unknown state commit d7f68e9ee2182417e9124bace0890203f6993868 Author: Ismael Luceno Date: Tue Dec 8 22:39:38 2020 +0100 Filter flags taken from LDFLAGS into CGO_LDFLAGS Make sure we take only flags compatible with cgo. Solves: https://github.com/cli/cli/issues/2577 commit 9f101ff0a2ddf31d67a3066beb3a4634977900f9 Author: Sam Coe Date: Tue Dec 8 10:24:59 2020 -0500 Add comments to pr view commit b2edf782cf205d158f8a300eeb730e2604ec5ebf Author: Sam Coe Date: Tue Dec 8 14:16:40 2020 -0500 Reverse order of issue lookup checks commit bec5e0cd77782587bc9ac4ab653107a939972b9c Author: Sam Coe Date: Mon Dec 7 15:02:43 2020 -0500 Address PR comments commit 40c4007d98f5d851ff46a3f3d18abd68c232391b Author: vilmibm Date: Mon Dec 7 15:01:55 2020 -0800 rename create->set commit 326fe371c67ba02a16182fd05428411b5e9e02cb Author: Florian Thomas Date: Sun Oct 18 16:34:32 2020 +0100 add `gist clone` command This adds the ability to clone a gist. Usage: ```sh $ gh gist clone 5b0e0062eb8e9654adad7bb1d81cc75f $ gh gist clone https://gist.github.com/OWNER/5b0e0062eb8e9654adad7bb1d81cc75f ``` This closes #2115. commit bad5a5942761eaeebb51356079e99c9c4d757ace Author: Sam Coe Date: Tue Nov 24 11:18:20 2020 +0300 Update non-tty comment output commit 8c5e5a382094551df9ea768e0fc8b7bfb0a52472 Author: Sam Coe Date: Mon Nov 23 18:37:27 2020 +0300 Appease the linter commit c843a4fa13a206db8331b52f93305c6a19b955ab Author: Sam Coe Date: Fri Nov 13 11:16:54 2020 +0300 Add issue comment viewing commit ada59236c6b18feba6f3bffb0d18edfa6beeeb01 Author: Mislav Marohnić Date: Mon Dec 7 20:12:58 2020 +0100 Add `workflow` to the list of default OAuth scopes we request Since GitHub CLI now offers to authenticate your Git as well, the token we request here will be used for git pushes. Since we do anticipate our users making edits to their GitHub Actions workflow files, we want them to be able to push their changes, and this scope allows that. commit 38ea595ce270c768a65fc2ab31326ac703f31e1a Author: Mislav Marohnić Date: Mon Dec 7 20:07:20 2020 +0100 Fix `refresh` test commit bc81282a6ce4adc6436f4eb316e6f05c2fed2a89 Merge: 3c76eb15 f4152454 Author: Mislav Marohnić Date: Mon Dec 7 20:02:08 2020 +0100 Merge remote-tracking branch 'origin' into git-credentials commit 3c76eb15a45fc63206664d364d115791834fb9b7 Author: Mislav Marohnić Date: Mon Dec 7 20:01:53 2020 +0100 Add tests for `auth git-credential` command commit 381e83e6e5d3c4995256f25dc053d121f98854f0 Author: Mislav Marohnić Date: Mon Dec 7 20:01:16 2020 +0100 Extend git credential prompt to `auth refresh` commit c39dc28fa1cdbbd4b429d2a65a8aa179efaffd20 Author: Mislav Marohnić Date: Mon Dec 7 17:07:45 2020 +0100 Rename `auth/client` to `auth/shared` commit 03949a4d72bf1f39887126c040dcf5ef2bf295bd Author: Pete Woods Date: Mon Dec 7 12:53:32 2020 +0000 Build static binaries Fixes #2555 commit 5309a2089a2600fa4ac475f4570b9622718ee8a7 Author: vilmibm Date: Tue Nov 24 12:07:54 2020 -0800 implement gh secret create and gh secret list commit f4152454f2e2038e87b2516bcd419660945c501f Merge: 8e1f7367 8db2027c Author: Mislav Marohnić Date: Thu Dec 3 18:51:29 2020 +0100 Merge pull request #2472 from cristiand391/preserve-metadata-state Prompt: avoid resetting PR/issue metadata commit 8db2027c99c669e9ed5980e6bf33700815240286 Author: Mislav Marohnić Date: Thu Dec 3 18:02:24 2020 +0100 Allow interactive `pr create` even if we failed to look up commits commit 2b4372bc3a6f4dd6d69293369d54d1c0c0294822 Author: Mislav Marohnić Date: Thu Dec 3 17:51:58 2020 +0100 AskStubber now throws a more descriptive error when stubs do not match commit be39f4363bfc14078800ebda3d8ddae7e0424ad1 Author: Mislav Marohnić Date: Thu Dec 3 17:47:40 2020 +0100 Make MetadataSurvey testable by accepting an interface commit d6add864b8b08497c5cb127e8d97d3e56348618b Author: Mislav Marohnić Date: Wed Dec 2 17:20:07 2020 +0100 Ensure efficient resolving of `issue/pr create` metadata to GraphQL IDs For metadata types chosen in interactive flow, we fetch all records from the API in order to be able to display a multi-select interface. For metadata defined via command-line flags, we resolve records that can be looked up directly, avoiding fetching the entirety of expensive datasets (e.g. all members of an organization) if we can. The new approach ensures efficient fetching when interactive flow is combined with values from flags. commit 8e1f73677580c94e72ab431116f5036a3d1f5e96 Merge: 0ade3935 be759785 Author: Mislav Marohnić Date: Wed Dec 2 13:02:16 2020 +0100 Merge pull request #2519 from cli/issue-create-browser-fix Fix respecting chosen action in interactive `issue create` commit 0ade39351a8165388bd883988e78b80e7ff48388 Merge: a84b9f09 e21c5100 Author: Sam Date: Tue Dec 1 21:08:59 2020 -0500 Merge pull request #2521 from cli/fix-env-auth-token Properly check env auth tokens in CheckAuth commit be759785f0b47bcc4288df03f69e186a127c5202 Author: Mislav Marohnić Date: Tue Dec 1 21:23:39 2020 +0100 Fix "Continue in browser" for `pr create` coming from forks Ensures that the `owner:` prefix is present when referencing the head branch commit a84b9f09d4f43e1f403795e9eb9f4d9ffd0f8f83 Merge: d74086da a4daf96a Author: Mislav Marohnić Date: Tue Dec 1 20:52:32 2020 +0100 Merge pull request #2456 from ismaell/build-flags Simplify build flags setup commit d74086da92a671bf4f2907eefbd3b7a7c8adde32 Merge: faa1e44f 413ccb71 Author: Mislav Marohnić Date: Tue Dec 1 20:42:31 2020 +0100 Merge pull request #2505 from nilsleiffischer/patch-1 Fix typo in an error message commit faa1e44f5299bc2bdb1c6e4269c9ea45af4f4048 Merge: 34d549e7 6f689ff0 Author: Mislav Marohnić Date: Tue Dec 1 20:33:37 2020 +0100 Merge pull request #2455 from ismaell/install-target Add make (un)install targets for POSIX systems Fixes #293 commit 6f689ff051d0be160fb3014d57f3162722ac658d Author: Mislav Marohnić Date: Tue Dec 1 20:31:20 2020 +0100 Document `make install` commit e21c5100fa02696073100a9a94684bd889ad4c6f Author: Sam Coe Date: Tue Dec 1 11:44:14 2020 -0500 Properly check env auth tokens in CheckAuth commit df2ca9c9f9e98a17e4926854bd2ab7c9d916475e Author: Mislav Marohnić Date: Tue Dec 1 15:55:40 2020 +0100 Fix browser URL test on Windows commit c92f416cc0a590cdef6e6b8bef88b91306e66a5f Author: Mislav Marohnić Date: Tue Dec 1 15:46:18 2020 +0100 Simplify `make install/uninstall` commit dc1fad9cb093959bf3b9a4a388964b242bef7d24 Author: Mislav Marohnić Date: Tue Dec 1 15:28:39 2020 +0100 Fix respecting chosen action in interactive `issue create` The `action` variable started being shadowed in the `if` block in 6671106448ed7b342797b426e15af032dfe3b1be commit 413ccb71cce962dfeddbda86a2a4dab563efa630 Author: Nils Leif Fischer Date: Sun Nov 29 16:07:43 2020 +0100 Delete an error message that is not useful (and had a typo) commit 8d2881d5ea4412439179ca748e7ba47a70e6e4af Author: Ismael Luceno Date: Sun Nov 29 20:36:32 2020 +0100 Install manual pages commit da3287c26cc7e39cddee46363df8821a305c7a8c Author: Ismael Luceno Date: Sat Nov 21 21:46:32 2020 +0100 Add make (un)install targets for POSIX systems The implementation imitates the behavior of build-systems generated by GNU Automake. Implemented targets: - install - install-strip - uninstall Implemented variables: - DESTDIR - prefix - bindir - INSTALL_STRIP_FLAG Internal implementation details: - install-bins variable collects user binaries to be installed - install-dirs variable collects directories to be created commit 34d549e7b61660c7c993181c0be046d6277cad03 Author: Max Horstmann Date: Thu Nov 26 11:31:15 2020 -0500 Document that reviewers can be teams (#2465) Co-authored-by: Mislav Marohnić commit 7c6574d8e99b1d870c330dd6cb5ae8a5b8117365 Merge: 08408805 e9e8f207 Author: Mislav Marohnić Date: Thu Nov 26 12:06:25 2020 +0100 Merge pull request #2480 from cli/bump-survey Bump AlecAivazis/survey commit 08408805a2ecc1393dfa518f94e8b6d55e6f92c2 Merge: 1135e5e3 21e2544d Author: Mislav Marohnić Date: Thu Nov 26 11:55:11 2020 +0100 Merge pull request #2479 from cli/prs-by-branch-order Prioritize latest (open) PR when looking up PRs for a branch commit 1135e5e3ededb4261293b4e2b2fdeab6f79c0738 Author: Zach Boyle <33520963+zaboyle@users.noreply.github.com> Date: Thu Nov 26 05:54:28 2020 -0500 set delete-branch merge flag default to false (#2466) Co-authored-by: Divya Ramanathan commit 504cfbc654b39b994895d69f792859e69fa2589c Merge: 37891a54 436846a7 Author: Amanda Pinsker Date: Wed Nov 25 16:06:29 2020 -0800 Merge pull request #2482 from cli/ds-docs Add design system docs to contributing commit 436846a7154f5261349138604e5cb27ce0f9d6ac Author: Amanda Pinsker Date: Wed Nov 25 11:58:26 2020 -0800 Add design system docs to contributing commit ab05736b9806fce952df96e8ac953afa35ae3462 Author: Cristian Dominguez Date: Wed Nov 25 13:30:54 2020 -0300 don't reset previously added metadata commit e9e8f207cc5c051b6a505dc0c3bb373a703556c5 Author: Mislav Marohnić Date: Wed Nov 25 14:52:13 2020 +0100 Bump AlecAivazis/survey commit 21e2544d73eb38435c1774ec231a118559062848 Author: Mislav Marohnić Date: Wed Nov 25 12:06:35 2020 +0100 Sort latest PRs first when looking up PRs for a branch Fixes #2452 commit 37891a54d93a7c54e0a1d589399f3dda9589f535 Author: Vixb Date: Wed Nov 25 18:40:30 2020 +0800 Update scoop install option (#2478) Co-authored-by: Mislav Marohnić Co-authored-by: Jan Pokorný commit ea50666c304f33774ff39a58cb8ffdf37b7b7a54 Author: Cristian Dominguez Date: Tue Nov 24 13:49:04 2020 -0300 Prompt: avoid resetting PR/issue metadata if no option is selected commit 9f84f0ffa1d5b3141399b7ed499eb81b69ef74d2 Author: Shubhankar Kanchan Gupta Date: Tue Nov 24 17:26:26 2020 +0530 Warn termux users with older Android versions (#2467) Co-authored-by: Mislav Marohnić commit 959b1aae67a0b2cc050ecbd6614745c4f6f18a22 Merge: d6c9004d cf37ce74 Author: Nate Smith Date: Mon Nov 23 13:39:43 2020 -0600 Merge pull request #2408 from cli/preserve-input Preserve/restore pr and issue input commit cf37ce74634be6e6f5fc6b7f506141712d95b592 Author: vilmibm Date: Mon Nov 23 11:20:27 2020 -0800 no shorthand for --recover commit d6e84a75fb3009d50d3a154251a3209097315c04 Author: vilmibm Date: Fri Nov 20 11:43:41 2020 -0800 switch to recover instead of resubmit commit 1d408eb30de84538cea441dcb45ffd9c9cf416c3 Author: vilmibm Date: Fri Nov 20 11:00:25 2020 -0800 linter appeasement commit f68909b7a840d08a542d86939c799d4e45fcf35a Author: vilmibm Date: Fri Nov 20 10:57:35 2020 -0800 use TempFile though the testing is gross commit fffd315a7e3c977825ef921af8f7ceb997f9f1d3 Author: vilmibm Date: Mon Nov 16 14:08:14 2020 -0800 fix dumb test commit d300526318bd7589ed1527a7a9b376336e8c4e32 Author: vilmibm Date: Fri Nov 13 10:11:39 2020 -0800 preserve and restore issue/pr input on failure commit e92cd432598775998211267eed0c7ef79f825e88 Author: vilmibm Date: Tue Nov 3 13:08:37 2020 -0800 add IOStreams.ReadUserFile commit d56d92c908cb2454d9b592916e980754fa310041 Author: Mislav Marohnić Date: Mon Nov 23 20:19:18 2020 +0100 If git credential helper is non-defined, set gh as credential helper commit d6c9004d64dbfb30580e6b08caeafd90813708be Merge: 05a1a252 a66a65d4 Author: Mislav Marohnić Date: Mon Nov 23 13:22:01 2020 +0100 Merge pull request #2460 from cli/spell-check-fixes Spell check fixes commit a66a65d4220b970af61b27fbe7ce9b4b62c0c7ba Author: Josh Soref Date: Sat Nov 21 21:18:52 2020 -0500 spelling: unmatched commit ded92972cd684f95f2e35ad3ee20f3f28a71bd86 Author: Josh Soref Date: Sat Nov 21 21:18:51 2020 -0500 spelling: template commit ec82d3c47e5b2340df8ccbda074ec2120b0c25fe Author: Josh Soref Date: Sat Nov 21 21:18:51 2020 -0500 spelling: settings commit e5f59a15fe6de03de0c17b8ec1ed004bf8c5afc6 Author: Josh Soref Date: Sat Nov 21 21:18:51 2020 -0500 spelling: response commit c8b9486fd3ac6a1692965013d1685e66cb197bfe Author: Josh Soref Date: Sat Nov 21 21:18:51 2020 -0500 spelling: nonexistent commit 76bd3772530f09de9b2d40f480c63a6d6427f410 Author: Josh Soref Date: Sat Nov 21 21:18:51 2020 -0500 spelling: error commit 861d350440ca0ba866c4651351b29afb939124a2 Author: Josh Soref Date: Sat Nov 21 21:18:50 2020 -0500 spelling: dunno commit ddd438d5e14ce696298f684e24e6933b3fe49548 Author: Josh Soref Date: Sat Nov 21 21:18:50 2020 -0500 spelling: dismissed commit 8ba68fc68ada852d8468753dd0a2ffb3c357a110 Author: Josh Soref Date: Sat Nov 21 21:18:50 2020 -0500 spelling: deprecated commit e58b2dbe92815222858af49ec0e168a2aae4a253 Author: Josh Soref Date: Sat Nov 21 21:18:50 2020 -0500 spelling: chestnuts commit 0e681ca6c4a43c6f33532917ef995d591375c348 Author: Josh Soref Date: Sat Nov 21 21:18:50 2020 -0500 spelling: beginning commit a4daf96a694b56e7945748e1d392dd746ec0de71 Author: Ismael Luceno Date: Sat Nov 21 23:00:45 2020 +0100 Make bin/gh rule verbose commit e16bf094bdca3c8e5d0486f6e1a9f6e2c3f701c5 Author: Ismael Luceno Date: Sat Nov 21 23:00:14 2020 +0100 Simplify CGO flags setup commit e36c9029d37e50c51a1f9ea7da6c824af91aa2a4 Author: Mislav Marohnić Date: Fri Nov 20 20:33:08 2020 +0100 Fix broken tests commit 91d2adc13442177cec7556a57dc0d4def2c0d550 Author: Mislav Marohnić Date: Fri Nov 20 19:36:26 2020 +0100 Avoid re-requesting username if we already have it commit 67672fa88c944b6118daa2b2c5dbcb82b7b84791 Author: Mislav Marohnić Date: Fri Nov 20 19:36:04 2020 +0100 Prime user's git HTTPS credentials on `auth login` commit 05a1a252712e529a385c850bab501590a0156526 Author: Nate Smith Date: Fri Nov 20 12:00:49 2020 -0600 match parent repo protocol when forking (#2434) * match parent repo protocol when forking * guard against nil and prefer PushURL commit c7eb57d443a392629f7cda31b34b918acad00925 Author: Nate Smith Date: Thu Nov 19 12:59:18 2020 -0600 respect GH_HOST when resolving remotes (#2301) * vim to gitignore * respect GH_HOST in Resolver * slight restructure, add a test * grammar fix commit e87b5bcaff227aad0118532d74decc90e8528723 Author: Nikola Ristić Date: Wed Nov 18 19:31:36 2020 +0100 Add "reference" help topic (#2223) * Add "reference" help topic * Only print reference as a help topic * fix for color fns, slightly generalize * WIP for switching to markdown * escape gt/lt * minor * higher wrap point * detect terminal theme * futz with angle brackets once more * minor cleanup * prepend parent commands * rename help topic fns and add test * Simplify reference help generation - the `<...>` characters from command usage line are now preserved by enclosing the entire usage synopsis in a code span - hard breaks in flag usage lines are preserved by enclosing flag usage in a code block - TTY detection and Markdown rendering are now delayed until the user explicitly requests `gh help reference` - `gh help reference` output is now pager-enabled Co-authored-by: vilmibm Co-authored-by: vilmibm Co-authored-by: Mislav Marohnić commit ee4827483d75a9d3c819ae8cf152c729f3e03366 Merge: b205faa9 7f57c1c3 Author: Sam Date: Wed Nov 18 09:26:37 2020 +0300 Merge pull request #2421 from cli/downgrade-survey Downgrade survey to v2.1.1 commit b205faa9412e2a18a79cd4a93cbe860b904e134b Author: Jakub Warczarek Date: Tue Nov 17 19:27:07 2020 +0100 Implement --web for gh pr checks (#2146) commit 7f57c1c3f238c3dff75710f23e4968fbdcaa803c Author: Sam Coe Date: Tue Nov 17 11:25:07 2020 +0300 Downgrade survey to v2.1.1 commit 00617216b8fc12dceb938371159903e6b1a993ac Author: vilmibm Date: Mon Nov 16 14:03:52 2020 -0800 fix missing import commit 0bb44c9cedcdf03cff43e820cbfba523c81dad58 Author: Christopher Oswald <8537054+cesoun@users.noreply.github.com> Date: Mon Nov 16 15:01:30 2020 -0700 Support for --web when using gist create (#2263) * Support for --web when using gist create Proposal to close #2071 I have not worked with Go prior to this so please smite me down with the wisdom of a million Golang gods if I'm doing something terribly wrong. I also added a test to gist/create for the added web arg. Pretty much referenced the implementation from pr/create. * Fix for Tests / build (windows-latest) I believe this fixes it as it stopped failing on a local vm. Otherwise I will try and tackle it tomorrow. * minor cleanup Co-authored-by: vilmibm commit 99574f85a3a5250ebfe6accfb6cc80c149f04df3 Author: Alex Johnson Date: Mon Nov 16 16:47:55 2020 -0500 Add a command to delete a gist (#2265) * Add a command to delete a gist * minor cleanup Co-authored-by: vilmibm commit e513333fb133072416ebda8b7da9cd607200639c Author: dfireBird Date: Mon Sep 21 16:26:34 2020 +0530 feat: implement prompt for remote renaming commit 116d5815b59591947dd088722616e8278bcfe794 Merge: ef52376f 2eb40f8a Author: Sam Date: Mon Nov 16 08:27:50 2020 +0300 Merge pull request #2388 from cli/gh-token Add support for GH_TOKEN and GH_ENTERPRISE_TOKEN commit ef52376fe0af81462e6a678d519fdd5485bd8517 Author: vilmibm Date: Fri Nov 13 10:35:32 2020 -0800 fix survey invocation commit 9a20719ec4ad6673b34edd615d5f6ba3a94828f3 Merge: fed4df2a a686455f Author: Nate Smith Date: Fri Nov 13 11:37:15 2020 -0600 Merge pull request #2386 from cli/create-refactor Refactor pr/issue creation code commit fed4df2afabe5ce79ff8e66f8f1d479f99684763 Merge: 6a0d38df 867f3897 Author: Mislav Marohnić Date: Fri Nov 13 16:13:51 2020 +0100 Merge pull request #2405 from cristiand391/fix-usage-help Minor fix in USAGE help info for some commands commit 867f38970f0229fedb7404ee1f1bc28fdfc3d184 Author: Cristian Dominguez Date: Fri Nov 13 10:10:53 2020 -0300 Fix USAGE help for some commands commit 6a0d38df44456486fc0d276264f4b2838605f195 Merge: c87deab1 62e560d6 Author: Mislav Marohnić Date: Fri Nov 13 13:05:56 2020 +0100 Merge pull request #2404 from alissonbrunosa/fix-repo-view-command-with-branch Generate correct URL when branch option is passed in commit 62e560d6ee2220d0ea4d8e4a1a6c6e2be03a97f6 Author: Alisson Santos Date: Fri Nov 13 10:24:17 2020 +0100 add empty line between functions commit 9ecc9029599841c7b2503366679f6fc5e0752836 Author: Alisson Santos Date: Fri Nov 13 10:20:30 2020 +0100 Generate correct URL when branch option is passed in commit a686455fb6001018bf4b55a5356c36fc1e5eeb64 Author: vilmibm Date: Thu Nov 12 12:17:37 2020 -0800 add Draft to issue state commit b231e6c2cf81c4ee5830da8f5824b38dea0050b6 Author: vilmibm Date: Thu Nov 12 12:15:48 2020 -0800 use NewIssueState commit f5277e452e3e29a51c866e8bcf96e2b33b7b5b0a Author: vilmibm Date: Tue Nov 10 16:27:06 2020 -0800 get everything working commit 0ed78793299fcdac782bc80c5f7c13493c75c1c8 Author: vilmibm Date: Tue Nov 10 16:25:18 2020 -0800 stop using Defaults struct commit 1c280d434109229ab5d804032f35880e9cae636c Author: vilmibm Date: Tue Nov 10 16:18:36 2020 -0800 stop using string pointer commit 6671106448ed7b342797b426e15af032dfe3b1be Author: vilmibm Date: Mon Nov 9 17:15:21 2020 -0800 WIP works, probably some title/body input edge cases commit 6fde02df1c6881c4f2cd98b95742af0cb413649b Author: vilmibm Date: Mon Nov 9 14:24:19 2020 -0800 use named output param commit c87deab14a0b9782e0853d90b63d7143d49e9250 Merge: 6e1e62f4 57ec879a Author: Mislav Marohnić Date: Thu Nov 12 15:47:26 2020 +0100 Merge pull request #2397 from ganboonhong/fix-typos Fix typos commit 57ec879aea9adcfc8edfe327d889f314c5975ff6 Author: boonhong Date: Thu Nov 12 22:32:36 2020 +0800 Fix typos commit 2eb40f8a14e561499134d7631b237fc730664fc6 Author: Sam Coe Date: Thu Nov 12 10:03:39 2020 +0300 Empty auth token env variables are equal to being unset commit 414de332fb17e96e095c4415318e91eb696beef5 Author: Sam Coe Date: Tue Nov 10 11:15:25 2020 +0300 cleanup commit a79a0bbfd746566de191cf28cf542ada25c9ef26 Author: Sam Coe Date: Mon Nov 9 15:02:31 2020 +0300 Add support for GH_TOKEN and GH_ENTERPRISE_TOKEN commit 6e1e62f496f34cea792de5ba73c745d947668801 Author: Mislav Marohnić Date: Wed Nov 11 18:17:01 2020 +0100 Consistently print commands in DEBUG mode commit 97b176da933e531006a88b1f3e97e9d71a52cf35 Author: Mislav Marohnić Date: Wed Nov 11 16:46:49 2020 +0100 Fix git executable name for Windows in tests commit fc3f5174190a2224a787f683bf02104f161e84b5 Merge: 3a1e0021 38f68d84 Author: Mislav Marohnić Date: Wed Nov 11 16:36:50 2020 +0100 Merge pull request from GHSA-fqfh-778m-2v32 Ensure that only PATH is searched when shelling out to external commands commit 38f68d849fbbf4c540398015719047672d0e3d9e Author: Mislav Marohnić Date: Wed Nov 11 13:25:35 2020 +0100 Improve error message when git isn't found commit c87dc00f385438c09162d8e0f511d0ff10c87fce Author: Mislav Marohnić Date: Wed Nov 11 13:23:06 2020 +0100 Omit the full path of a command in DEBUG mode commit 5b4a08dcb9bc63f8da5f966b4e47d73edf87f3b7 Author: Mislav Marohnić Date: Tue Nov 10 20:03:24 2020 +0100 Ensure that only PATH is searched when shelling out to external commands Works around https://github.com/golang/go/issues/38736 for Windows. commit 3a1e0021de060464876fc74c680082833d5b37ce Merge: 85a7dfbf f505d1fd Author: Mislav Marohnić Date: Wed Nov 11 16:11:47 2020 +0100 Merge pull request #2393 from cli/bump-deps Bump dependencies commit 85a7dfbf4cb9c53fa9ef78e81d274a3f0cad9b43 Merge: fa867438 feec114b Author: Mislav Marohnić Date: Wed Nov 11 16:10:40 2020 +0100 Merge pull request #2312 from jan25/jan25/issue-2042 Fetch and print all Issue labels commit f505d1fdf7918ff6701e5751de25b0d0440e0669 Author: Mislav Marohnić Date: Wed Nov 11 15:36:35 2020 +0100 Bump golang.org/x/text commit 6ac38d9774c43142fbfa107d93ecd5f9dd2eb038 Author: Mislav Marohnić Date: Wed Nov 11 15:36:23 2020 +0100 Bump golang.org/x/crypto commit 67cafa305b21cb5f09fea78bd836aab459f6009f Author: Mislav Marohnić Date: Wed Nov 11 15:36:01 2020 +0100 Bump shurcooL/githubv4 commit a860708c7b47bfeef0e6cd0197f84f013c993a0c Author: Mislav Marohnić Date: Wed Nov 11 15:34:15 2020 +0100 Bump muesli/termenv commit f2864aa5b56a9704276231753e8d29b15109e3c1 Author: Mislav Marohnić Date: Wed Nov 11 15:33:27 2020 +0100 Bump mattn/go-colorable commit be783cf2567d27f6396b4d32ea4082857192a0da Author: Mislav Marohnić Date: Wed Nov 11 15:32:54 2020 +0100 Bump AlecAivazis/survey commit feec114bb6f1659e89060d5239e1b082df946f5d Author: Mislav Marohnić Date: Wed Nov 11 15:29:40 2020 +0100 Fix `truncateLabels` for empty values commit 8d054486a3d5409d7e07ca44a56259eb365723c8 Merge: 62e4c536 fa867438 Author: Mislav Marohnić Date: Wed Nov 11 15:23:46 2020 +0100 Merge remote-tracking branch 'origin' into jan25/issue-2042 commit 62e4c536ce0fa3c23995ead2410b8ded35203025 Author: Mislav Marohnić Date: Wed Nov 11 15:21:51 2020 +0100 Ensure parentheses are preserved after truncating labels in table view commit fa8674386b21a90e33b4f6fd00fb025da4712275 Author: Cristian Dominguez <6853656+cristiand391@users.noreply.github.com> Date: Wed Nov 11 12:13:19 2020 +0000 Render links as absolute URLs in `repo view` (#2363) Co-authored-by: Cristian Dominguez commit 91df27f30ac0ffc7158c438cab3924b38bdf21d1 Merge: 3c3ce909 79878a67 Author: Mislav Marohnić Date: Wed Nov 11 12:52:21 2020 +0100 Merge pull request #2383 from cristiand391/fix-mousetrap-windows Show MousetrapHelpText when double-clicking gh.exe commit 3c3ce909a1d824a845d77dc8cf7db8e6622ce9ea Author: Zoltan Puskas Date: Wed Nov 11 03:39:22 2020 -0800 Add Gentoo instructions to install the package (#2389) Co-authored-by: Mislav Marohnić commit 79878a67365336b1b823c50c1ca7f15eddcedbe2 Author: Cristian Dominguez Date: Mon Nov 9 08:28:55 2020 -0300 Show MousetrapHelpText when double-clicking gh.exe commit 07bd6472112f965352a9242c4260bb8618187614 Author: Cristian Dominguez Date: Wed Nov 4 12:14:02 2020 -0300 Update repo fork help msg commit 97aa0b2f7daf7abaf445970aa16e89b1532a42c7 Author: Cristian Dominguez Date: Wed Nov 4 12:10:35 2020 -0300 Warn when passing git flags without repository arg commit 9e03d01bdfde9c0ced4aae512f6db551b22f49b3 Author: Abhilash Gnan Date: Wed Oct 28 19:45:21 2020 +0100 Fetch all issue labels Signed-off-by: Abhilash Gnan commit c6938941d53847a2baa2fb95da8cca24f3720dc5 Author: Cristian Dominguez Date: Fri Oct 23 22:32:26 2020 -0300 Fix typo commit 69b64507ea6fddf09ddd4d3ec76a90b3a221d31a Author: Cristian Dominguez Date: Fri Oct 23 18:45:20 2020 -0300 Add support for git flags in gh repo fork commit 8b0618f45e0ac061e3aed1ddfcc737a8169f6d6a Author: div_bhasin Date: Sat Oct 17 16:13:42 2020 -0400 refactoring commit 5f67ddc975fd8b008eae775930ad155b297d144f Author: div_bhasin Date: Sat Oct 17 13:12:26 2020 -0400 added to repo struct and working on fetching merge opts for repo --- .github/CONTRIBUTING.md | 21 +- .github/ISSUE_TEMPLATE/config.yml | 8 + .github/PULL_REQUEST_TEMPLATE.md | 4 + .github/PULL_REQUEST_TEMPLATE/bug_fix.md | 19 - .github/dependabot.yml | 15 + .github/workflows/codeql.yml | 6 + .github/workflows/go.yml | 19 +- .github/workflows/lint.yml | 12 +- .github/workflows/prauto.yml | 78 ++ .github/workflows/releases.yml | 64 +- .gitignore | 4 + .golangci.yml | 7 +- .goreleaser.yml | 13 +- CODEOWNERS | 5 + Makefile | 76 +- README.md | 52 +- api/cache.go | 18 +- api/cache_test.go | 4 +- api/client.go | 158 ++- api/client_test.go | 174 +-- api/export_pr.go | 112 ++ api/export_pr_test.go | 184 +++ api/export_repo.go | 53 + api/pull_request_test.go | 14 +- api/queries_comments.go | 101 ++ api/queries_issue.go | 358 +++--- api/queries_issue_test.go | 145 --- api/queries_org.go | 2 +- api/queries_pr.go | 964 +++++----------- api/queries_pr_review.go | 113 ++ api/queries_pr_test.go | 189 +++- api/queries_repo.go | 384 ++++++- api/queries_repo_test.go | 82 +- api/query_builder.go | 358 ++++++ api/query_builder_test.go | 39 + api/reaction_groups.go | 59 + api/reaction_groups_test.go | 100 ++ auth/oauth.go | 275 ----- auth/oauth_test.go | 258 ----- cmd/gen-docs/main.go | 69 +- cmd/gen-docs/main_test.go | 32 + cmd/gh/main.go | 229 +++- cmd/gh/main_test.go | 4 +- context/context.go | 12 +- context/remote.go | 18 +- context/remote_test.go | 39 +- docs/install_linux.md | 99 +- docs/project-layout.md | 84 ++ docs/releasing.md | 2 +- docs/source.md | 45 +- git/fixtures/.gitignore | 1 + git/fixtures/simple.git/HEAD | 1 + git/fixtures/simple.git/config | 9 + git/fixtures/simple.git/index | Bin 0 -> 65 bytes git/fixtures/simple.git/logs/HEAD | 2 + git/fixtures/simple.git/logs/refs/heads/main | 2 + .../4b/825dc642cb6eb9a060e54bf8d69288fbee4904 | Bin 0 -> 15 bytes .../6f/1a2405cace1633d89a79c74c65f22fe78f9659 | Bin 0 -> 191 bytes .../d1/e0abfb7d158ed544a202a6958c62d4fc22e12f | 3 + git/fixtures/simple.git/refs/heads/main | 1 + git/git.go | 173 ++- git/git_test.go | 146 ++- git/remote.go | 20 +- git/remote_test.go | 26 +- git/ssh_config.go | 145 ++- git/ssh_config_test.go | 132 ++- git/url.go | 5 + git/url_test.go | 25 + go.mod | 57 +- go.sum | 605 +++++++--- internal/authflow/flow.go | 67 +- internal/codespaces/api/api.go | 617 ++++++++++ internal/codespaces/api/api_test.go | 118 ++ internal/codespaces/codespaces.go | 85 ++ internal/codespaces/ssh.go | 90 ++ internal/codespaces/ssh_test.go | 105 ++ internal/codespaces/states.go | 117 ++ internal/config/config_file.go | 177 ++- internal/config/config_file_test.go | 483 +++++++- internal/config/config_map.go | 104 ++ internal/config/config_map_test.go | 65 ++ internal/config/config_type.go | 444 +------- internal/config/config_type_test.go | 29 +- internal/config/from_env.go | 75 +- internal/config/from_env_test.go | 251 +++- internal/config/from_file.go | 318 ++++++ internal/config/from_file_test.go | 15 + internal/config/stub.go | 8 + internal/config/testing.go | 8 +- internal/docs/man_test.go | 7 +- internal/docs/markdown_test.go | 3 +- internal/ghinstance/host.go | 16 - internal/ghinstance/host_test.go | 25 +- internal/ghrepo/repo.go | 25 +- internal/ghrepo/repo_test.go | 34 +- internal/httpunix/transport.go | 21 + internal/run/run.go | 24 +- internal/run/stub.go | 33 +- {update => internal/update}/update.go | 61 +- {update => internal/update}/update_test.go | 38 +- pkg/browser/browser.go | 74 -- pkg/browser/browser_test.go | 70 -- pkg/cmd/actions/actions.go | 60 + pkg/cmd/alias/alias.go | 10 +- pkg/cmd/alias/delete/delete.go | 10 +- pkg/cmd/alias/delete/delete_test.go | 10 +- pkg/cmd/alias/expand/expand.go | 35 +- pkg/cmd/alias/expand/expand_test.go | 2 +- pkg/cmd/alias/list/list.go | 8 +- pkg/cmd/alias/list/list_test.go | 6 +- pkg/cmd/alias/set/set.go | 101 +- pkg/cmd/alias/set/set_test.go | 141 ++- pkg/cmd/api/api.go | 285 +++-- pkg/cmd/api/api_test.go | 507 ++++++++- pkg/cmd/api/http.go | 2 +- pkg/cmd/api/pagination.go | 2 +- pkg/cmd/auth/auth.go | 12 +- pkg/cmd/auth/client/client.go | 48 - pkg/cmd/auth/gitcredential/helper.go | 125 ++ pkg/cmd/auth/gitcredential/helper_test.go | 184 +++ pkg/cmd/auth/login/login.go | 249 ++-- pkg/cmd/auth/login/login_test.go | 201 +++- pkg/cmd/auth/logout/logout.go | 19 +- pkg/cmd/auth/logout/logout_test.go | 45 +- pkg/cmd/auth/refresh/refresh.go | 80 +- pkg/cmd/auth/refresh/refresh_test.go | 86 +- pkg/cmd/auth/shared/git_credential.go | 161 +++ pkg/cmd/auth/shared/git_credential_test.go | 94 ++ pkg/cmd/auth/shared/login_flow.go | 208 ++++ pkg/cmd/auth/shared/login_flow_test.go | 155 +++ pkg/cmd/auth/shared/oauth_scopes.go | 98 ++ pkg/cmd/auth/shared/oauth_scopes_test.go | 79 ++ pkg/cmd/auth/shared/ssh_keys.go | 119 ++ pkg/cmd/auth/status/status.go | 29 +- pkg/cmd/auth/status/status_test.go | 68 +- pkg/cmd/browse/browse.go | 238 ++++ pkg/cmd/browse/browse_test.go | 477 ++++++++ pkg/cmd/codespace/code.go | 60 + pkg/cmd/codespace/common.go | 265 +++++ pkg/cmd/codespace/create.go | 296 +++++ pkg/cmd/codespace/delete.go | 175 +++ pkg/cmd/codespace/delete_test.go | 240 ++++ pkg/cmd/codespace/list.go | 57 + pkg/cmd/codespace/logs.go | 112 ++ pkg/cmd/codespace/mock_api.go | 629 ++++++++++ pkg/cmd/codespace/mock_prompter.go | 69 ++ pkg/cmd/codespace/output/format_json.go | 55 + pkg/cmd/codespace/output/format_table.go | 31 + pkg/cmd/codespace/output/format_tsv.go | 25 + pkg/cmd/codespace/output/logger.go | 78 ++ pkg/cmd/codespace/ports.go | 312 +++++ pkg/cmd/codespace/root.go | 31 + pkg/cmd/codespace/ssh.go | 172 +++ pkg/cmd/codespace/stop.go | 68 ++ pkg/cmd/completion/completion.go | 62 +- pkg/cmd/completion/completion_test.go | 2 +- pkg/cmd/config/config.go | 10 +- pkg/cmd/config/get/get.go | 6 +- pkg/cmd/config/get/get_test.go | 6 +- pkg/cmd/config/set/set.go | 6 +- pkg/cmd/config/set/set_test.go | 6 +- pkg/cmd/extension/command.go | 241 ++++ pkg/cmd/extension/command_test.go | 439 +++++++ pkg/cmd/extension/extension.go | 50 + pkg/cmd/extension/http.go | 114 ++ pkg/cmd/extension/manager.go | 693 ++++++++++++ pkg/cmd/extension/manager_test.go | 568 ++++++++++ pkg/cmd/extension/symlink_other.go | 9 + pkg/cmd/extension/symlink_windows.go | 15 + pkg/cmd/factory/default.go | 247 +++- pkg/cmd/factory/default_test.go | 469 ++++++++ pkg/cmd/factory/http.go | 109 +- pkg/cmd/factory/http_test.go | 175 +++ pkg/cmd/factory/remote_resolver.go | 62 +- pkg/cmd/factory/remote_resolver_test.go | 275 ++++- pkg/cmd/gist/clone/clone.go | 103 ++ pkg/cmd/gist/clone/clone_test.go | 118 ++ pkg/cmd/gist/create/create.go | 115 +- pkg/cmd/gist/create/create_test.go | 160 ++- pkg/cmd/gist/delete/delete.go | 101 ++ pkg/cmd/gist/delete/delete_test.go | 161 +++ pkg/cmd/gist/edit/edit.go | 101 +- pkg/cmd/gist/edit/edit_test.go | 94 +- pkg/cmd/gist/fixture.txt | 1 - pkg/cmd/gist/gist.go | 16 +- pkg/cmd/gist/list/http.go | 88 -- pkg/cmd/gist/list/list.go | 25 +- pkg/cmd/gist/list/list_test.go | 28 +- pkg/cmd/gist/shared/shared.go | 118 +- pkg/cmd/gist/shared/shared_test.go | 35 + pkg/cmd/gist/view/view.go | 217 +++- pkg/cmd/gist/view/view_test.go | 311 ++++- pkg/cmd/issue/close/close.go | 16 +- pkg/cmd/issue/close/close_test.go | 62 +- pkg/cmd/issue/comment/comment.go | 79 ++ pkg/cmd/issue/comment/comment_test.go | 302 +++++ pkg/cmd/issue/create/create.go | 242 ++-- pkg/cmd/issue/create/create_test.go | 646 ++++++++--- pkg/cmd/issue/delete/delete.go | 98 ++ pkg/cmd/issue/delete/delete_test.go | 157 +++ pkg/cmd/issue/edit/edit.go | 252 +++++ pkg/cmd/issue/edit/edit_test.go | 437 +++++++ pkg/cmd/issue/issue.go | 22 +- pkg/cmd/issue/list/fixtures/issueList.json | 74 +- pkg/cmd/issue/list/fixtures/issueSearch.json | 54 + pkg/cmd/issue/list/http.go | 229 ++++ pkg/cmd/issue/list/http_test.go | 162 +++ pkg/cmd/issue/list/list.go | 129 ++- pkg/cmd/issue/list/list_test.go | 478 ++++++-- pkg/cmd/issue/reopen/reopen.go | 16 +- pkg/cmd/issue/reopen/reopen_test.go | 62 +- pkg/cmd/issue/shared/display.go | 30 +- pkg/cmd/issue/shared/lookup.go | 45 +- pkg/cmd/issue/shared/lookup_test.go | 102 ++ pkg/cmd/issue/status/status.go | 47 +- pkg/cmd/issue/status/status_test.go | 12 +- pkg/cmd/issue/transfer/transfer.go | 114 ++ pkg/cmd/issue/transfer/transfer_test.go | 147 +++ .../view/fixtures/issueView_preview.json | 8 +- .../issueView_previewClosedState.json | 2 +- .../issueView_previewFullComments.json | 318 ++++++ .../issueView_previewSingleComment.json | 147 +++ .../issueView_previewWithEmptyBody.json | 2 +- .../issueView_previewWithMetadata.json | 60 +- pkg/cmd/issue/view/http.go | 50 + pkg/cmd/issue/view/view.go | 167 ++- pkg/cmd/issue/view/view_test.go | 394 ++++--- pkg/cmd/pr/checkout/checkout.go | 230 ++-- pkg/cmd/pr/checkout/checkout_test.go | 842 ++++++-------- pkg/cmd/pr/checks/checks.go | 103 +- pkg/cmd/pr/checks/checks_test.go | 159 ++- pkg/cmd/pr/checks/fixtures/allPassing.json | 9 +- pkg/cmd/pr/checks/fixtures/someFailing.json | 9 +- pkg/cmd/pr/checks/fixtures/somePending.json | 9 +- pkg/cmd/pr/checks/fixtures/someSkipping.json | 42 + pkg/cmd/pr/checks/fixtures/withStatuses.json | 9 +- pkg/cmd/pr/close/close.go | 69 +- pkg/cmd/pr/close/close_test.go | 217 +++- pkg/cmd/pr/comment/comment.go | 79 ++ pkg/cmd/pr/comment/comment_test.go | 312 +++++ pkg/cmd/pr/create/create.go | 692 ++++++----- pkg/cmd/pr/create/create_test.go | 727 +++++++----- pkg/cmd/pr/create/regexp_writer_test.go | 6 +- pkg/cmd/pr/diff/diff.go | 74 +- pkg/cmd/pr/diff/diff_test.go | 145 ++- pkg/cmd/pr/edit/edit.go | 338 ++++++ pkg/cmd/pr/edit/edit_test.go | 595 ++++++++++ pkg/cmd/pr/list/fixtures/prList.json | 50 +- .../list/fixtures/prListWithDuplicates.json | 38 +- pkg/cmd/pr/list/http.go | 241 ++++ pkg/cmd/pr/list/http_test.go | 163 +++ pkg/cmd/pr/list/list.go | 149 ++- pkg/cmd/pr/list/list_test.go | 155 ++- pkg/cmd/pr/merge/http.go | 138 +++ pkg/cmd/pr/merge/merge.go | 496 +++++--- pkg/cmd/pr/merge/merge_test.go | 966 +++++++++++----- pkg/cmd/pr/pr.go | 30 +- pkg/cmd/pr/ready/ready.go | 56 +- pkg/cmd/pr/ready/ready_test.go | 123 +- pkg/cmd/pr/reopen/reopen.go | 42 +- pkg/cmd/pr/reopen/reopen_test.go | 112 +- pkg/cmd/pr/review/review.go | 71 +- pkg/cmd/pr/review/review_test.go | 443 +++----- pkg/cmd/pr/shared/commentable.go | 172 +++ pkg/cmd/pr/shared/comments.go | 203 ++++ pkg/cmd/pr/shared/display.go | 6 +- pkg/cmd/pr/shared/editable.go | 371 ++++++ pkg/cmd/pr/shared/finder.go | 532 +++++++++ pkg/cmd/pr/shared/finder_test.go | 394 +++++++ pkg/cmd/pr/shared/lookup.go | 121 -- pkg/cmd/pr/shared/params.go | 244 +++- pkg/cmd/pr/shared/params_test.go | 208 +++- pkg/cmd/pr/shared/preserve.go | 68 ++ pkg/cmd/pr/shared/preserve_test.go | 114 ++ pkg/cmd/pr/shared/reaction_groups.go | 32 + pkg/cmd/pr/shared/state.go | 68 ++ pkg/cmd/pr/shared/survey.go | 354 ++++++ pkg/cmd/pr/shared/survey_test.go | 144 +++ pkg/cmd/pr/shared/templates.go | 261 +++++ pkg/cmd/pr/shared/templates_test.go | 74 ++ pkg/cmd/pr/shared/title_body_survey.go | 396 ------- .../pr/status/fixtures/prStatusChecks.json | 6 +- .../fixtures/prStatusCurrentBranchClosed.json | 19 +- .../fixtures/prStatusCurrentBranchMerged.json | 19 +- pkg/cmd/pr/status/status.go | 61 +- pkg/cmd/pr/status/status_test.go | 16 +- pkg/cmd/pr/view/fixtures/prView.json | 50 - pkg/cmd/pr/view/fixtures/prViewPreview.json | 2 + .../fixtures/prViewPreviewClosedState.json | 2 + .../fixtures/prViewPreviewDraftState.json | 2 + .../prViewPreviewDraftStatebyBranch.json | 50 - .../fixtures/prViewPreviewFullComments.json | 320 ++++++ .../fixtures/prViewPreviewManyReviews.json | 67 ++ .../fixtures/prViewPreviewMergedState.json | 2 + .../view/fixtures/prViewPreviewReviews.json | 318 ++++++ .../fixtures/prViewPreviewSingleComment.json | 157 +++ .../prViewPreviewWithMetadataByBranch.json | 128 --- .../prViewPreviewWithMetadataByNumber.json | 24 +- .../prViewPreviewWithReviewersByNumber.json | 70 +- .../pr/view/fixtures/prView_EmptyBody.json | 48 - .../view/fixtures/prView_NoActiveBranch.json | 15 - pkg/cmd/pr/view/view.go | 163 ++- pkg/cmd/pr/view/view_test.go | 773 +++++++------ pkg/cmd/release/create/create.go | 100 +- pkg/cmd/release/create/create_test.go | 80 +- pkg/cmd/release/create/http.go | 8 +- pkg/cmd/release/delete/delete.go | 16 +- pkg/cmd/release/delete/delete_test.go | 8 +- pkg/cmd/release/download/download.go | 10 +- pkg/cmd/release/download/download_test.go | 8 +- pkg/cmd/release/list/http.go | 4 +- pkg/cmd/release/list/list.go | 10 +- pkg/cmd/release/list/list_test.go | 8 +- pkg/cmd/release/release.go | 14 +- pkg/cmd/release/shared/fetch.go | 123 +- pkg/cmd/release/shared/upload.go | 3 +- pkg/cmd/release/upload/upload.go | 10 +- pkg/cmd/release/view/view.go | 42 +- pkg/cmd/release/view/view_test.go | 8 +- pkg/cmd/repo/archive/archive.go | 99 ++ pkg/cmd/repo/archive/archive_test.go | 165 +++ pkg/cmd/repo/archive/http.go | 32 + pkg/cmd/repo/clone/clone.go | 44 +- pkg/cmd/repo/clone/clone_test.go | 75 +- pkg/cmd/repo/create/create.go | 475 ++++++-- pkg/cmd/repo/create/create_test.go | 474 ++++++-- pkg/cmd/repo/create/http.go | 208 +++- pkg/cmd/repo/create/http_test.go | 441 +++++++- pkg/cmd/repo/credits/credits.go | 10 +- pkg/cmd/repo/fork/fork.go | 146 ++- pkg/cmd/repo/fork/fork_test.go | 1003 ++++++++-------- pkg/cmd/repo/garden/garden.go | 112 +- pkg/cmd/repo/garden/http.go | 10 +- pkg/cmd/repo/list/fixtures/repoList.json | 40 + pkg/cmd/repo/list/fixtures/repoSearch.json | 37 + pkg/cmd/repo/list/http.go | 219 ++++ pkg/cmd/repo/list/http_test.go | 162 +++ pkg/cmd/repo/list/list.go | 219 ++++ pkg/cmd/repo/list/list_test.go | 395 +++++++ pkg/cmd/repo/repo.go | 20 +- pkg/cmd/repo/sync/git.go | 126 +++ pkg/cmd/repo/sync/http.go | 42 + pkg/cmd/repo/sync/mocks.go | 59 + pkg/cmd/repo/sync/sync.go | 313 +++++ pkg/cmd/repo/sync/sync_test.go | 479 ++++++++ pkg/cmd/repo/view/http.go | 7 +- pkg/cmd/repo/view/view.go | 78 +- pkg/cmd/repo/view/view_test.go | 77 +- pkg/cmd/root/help.go | 42 +- pkg/cmd/root/help_reference.go | 96 +- pkg/cmd/root/help_topic.go | 94 +- pkg/cmd/root/help_topic_test.go | 6 +- pkg/cmd/run/cancel/cancel.go | 136 +++ pkg/cmd/run/cancel/cancel_test.go | 218 ++++ pkg/cmd/run/download/download.go | 190 ++++ pkg/cmd/run/download/download_test.go | 313 +++++ pkg/cmd/run/download/fixtures/myproject.zip | Bin 0 -> 1710 bytes pkg/cmd/run/download/http.go | 70 ++ pkg/cmd/run/download/http_test.go | 106 ++ pkg/cmd/run/download/zip.go | 69 ++ pkg/cmd/run/download/zip_test.go | 32 + pkg/cmd/run/list/list.go | 160 +++ pkg/cmd/run/list/list_test.go | 229 ++++ pkg/cmd/run/rerun/rerun.go | 120 ++ pkg/cmd/run/rerun/rerun_test.go | 214 ++++ pkg/cmd/run/run.go | 33 + pkg/cmd/run/shared/artifacts.go | 53 + pkg/cmd/run/shared/presentation.go | 54 + pkg/cmd/run/shared/shared.go | 393 +++++++ pkg/cmd/run/shared/shared_test.go | 54 + pkg/cmd/run/shared/test.go | 122 ++ pkg/cmd/run/view/fixtures/run_log.zip | Bin 0 -> 1120 bytes pkg/cmd/run/view/view.go | 528 +++++++++ pkg/cmd/run/view/view_test.go | 1007 +++++++++++++++++ pkg/cmd/run/watch/watch.go | 246 ++++ pkg/cmd/run/watch/watch_test.go | 333 ++++++ pkg/cmd/secret/list/list.go | 258 +++++ pkg/cmd/secret/list/list_test.go | 268 +++++ pkg/cmd/secret/remove/remove.go | 117 ++ pkg/cmd/secret/remove/remove_test.go | 203 ++++ pkg/cmd/secret/secret.go | 29 + pkg/cmd/secret/set/http.go | 149 +++ pkg/cmd/secret/set/set.go | 253 +++++ pkg/cmd/secret/set/set_test.go | 393 +++++++ pkg/cmd/secret/shared/shared.go | 9 + pkg/cmd/ssh-key/add/add.go | 96 ++ pkg/cmd/ssh-key/add/add_test.go | 43 + pkg/cmd/ssh-key/add/http.go | 64 ++ pkg/cmd/ssh-key/list/http.go | 53 + pkg/cmd/ssh-key/list/list.go | 101 ++ pkg/cmd/ssh-key/list/list_test.go | 133 +++ pkg/cmd/ssh-key/ssh-key.go | 21 + pkg/cmd/version/version.go | 7 +- pkg/cmd/version/version_test.go | 7 + pkg/cmd/workflow/disable/disable.go | 93 ++ pkg/cmd/workflow/disable/disable_test.go | 298 +++++ pkg/cmd/workflow/enable/enable.go | 93 ++ pkg/cmd/workflow/enable/enable_test.go | 300 +++++ pkg/cmd/workflow/list/list.go | 105 ++ pkg/cmd/workflow/list/list_test.go | 254 +++++ pkg/cmd/workflow/run/run.go | 412 +++++++ pkg/cmd/workflow/run/run_test.go | 671 +++++++++++ pkg/cmd/workflow/shared/shared.go | 245 ++++ pkg/cmd/workflow/shared/test.go | 45 + pkg/cmd/workflow/view/http.go | 31 + pkg/cmd/workflow/view/view.go | 269 +++++ pkg/cmd/workflow/view/view_test.go | 438 +++++++ pkg/cmd/workflow/workflow.go | 31 + pkg/cmdutil/args.go | 15 + pkg/cmdutil/auth_check.go | 13 +- pkg/cmdutil/auth_check_test.go | 23 +- pkg/cmdutil/errors.go | 26 +- pkg/cmdutil/factory.go | 22 +- pkg/cmdutil/file_input.go | 16 + pkg/cmdutil/json_flags.go | 187 +++ pkg/cmdutil/json_flags_test.go | 207 ++++ pkg/cmdutil/legacy.go | 2 +- pkg/cmdutil/repo_override.go | 66 +- pkg/cmdutil/web_browser.go | 83 ++ pkg/export/filter.go | 61 + pkg/export/filter_test.go | 85 ++ pkg/export/template.go | 204 ++++ pkg/export/template_test.go | 280 +++++ pkg/extensions/extension.go | 27 + pkg/extensions/extension_mock.go | 210 ++++ pkg/extensions/manager_mock.go | 357 ++++++ pkg/findsh/find.go | 10 + pkg/findsh/find_windows.go | 35 + pkg/githubsearch/query.go | 221 ++++ pkg/githubtemplate/github_template.go | 7 +- pkg/githubtemplate/github_template_test.go | 16 +- pkg/httpmock/legacy.go | 75 -- pkg/httpmock/stub.go | 12 + pkg/iostreams/color.go | 96 +- pkg/iostreams/color_test.go | 39 + pkg/iostreams/console.go | 16 + pkg/iostreams/console_windows.go | 30 + pkg/iostreams/iostreams.go | 126 ++- pkg/iostreams/iostreams_test.go | 68 ++ pkg/iostreams/tty_size.go | 19 + pkg/iostreams/tty_size_windows.go | 16 + pkg/jsoncolor/jsoncolor.go | 2 +- pkg/liveshare/client.go | 170 +++ pkg/liveshare/client_test.go | 74 ++ pkg/liveshare/options_test.go | 57 + pkg/liveshare/port_forwarder.go | 199 ++++ pkg/liveshare/port_forwarder_test.go | 118 ++ pkg/liveshare/rpc.go | 41 + pkg/liveshare/session.go | 144 +++ pkg/liveshare/session_test.go | 403 +++++++ pkg/liveshare/socket.go | 100 ++ pkg/liveshare/ssh.go | 79 ++ pkg/liveshare/test/server.go | 334 ++++++ pkg/liveshare/test/socket.go | 77 ++ pkg/markdown/markdown.go | 67 +- pkg/prompt/prompt.go | 2 +- pkg/prompt/stubber.go | 6 +- pkg/set/string_set.go | 69 ++ pkg/set/string_set_test.go | 27 + pkg/surveyext/editor.go | 14 +- pkg/surveyext/editor_manual.go | 20 + pkg/surveyext/editor_test.go | 284 +++++ pkg/text/truncate.go | 62 +- pkg/text/truncate_test.go | 105 ++ script/build.bat | 1 + script/build.go | 237 ++++ script/changelog | 20 - script/createrepo.sh | 12 + script/distributions | 68 +- test/fixtures/test.git/HEAD | 1 - test/fixtures/test.git/config | 6 - test/fixtures/test.git/info/exclude | 6 - .../08/f4b7b6513dffc6245857e497cfd6101dc47818 | 3 - .../8a/1cdac440b4a3c44b988e300758a903a9866905 | Bin 54 -> 0 bytes .../9b/5a719a3d76ac9dc2fa635d9b1f34fd73994c06 | 3 - .../9d/aeafb9864cf43055ae93beb0afd6c7d144bfa4 | Bin 20 -> 0 bytes .../ca/93b49848670d03b3968c8a481eca55f5fb2150 | Bin 56 -> 0 bytes .../e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 | Bin 15 -> 0 bytes test/fixtures/test.git/refs/heads/master | 1 - test/helpers.go | 52 +- utils/table_printer.go | 146 ++- utils/terminal.go | 4 +- utils/utils.go | 56 +- utils/utils_test.go | 27 +- 484 files changed, 52977 insertions(+), 10516 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE/bug_fix.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/prauto.yml create mode 100644 CODEOWNERS create mode 100644 api/export_pr.go create mode 100644 api/export_pr_test.go create mode 100644 api/export_repo.go create mode 100644 api/queries_comments.go delete mode 100644 api/queries_issue_test.go create mode 100644 api/queries_pr_review.go create mode 100644 api/query_builder.go create mode 100644 api/query_builder_test.go create mode 100644 api/reaction_groups.go create mode 100644 api/reaction_groups_test.go delete mode 100644 auth/oauth.go delete mode 100644 auth/oauth_test.go create mode 100644 cmd/gen-docs/main_test.go create mode 100644 docs/project-layout.md create mode 100644 git/fixtures/.gitignore create mode 100644 git/fixtures/simple.git/HEAD create mode 100644 git/fixtures/simple.git/config create mode 100644 git/fixtures/simple.git/index create mode 100644 git/fixtures/simple.git/logs/HEAD create mode 100644 git/fixtures/simple.git/logs/refs/heads/main create mode 100644 git/fixtures/simple.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 create mode 100644 git/fixtures/simple.git/objects/6f/1a2405cace1633d89a79c74c65f22fe78f9659 create mode 100644 git/fixtures/simple.git/objects/d1/e0abfb7d158ed544a202a6958c62d4fc22e12f create mode 100644 git/fixtures/simple.git/refs/heads/main create mode 100644 internal/codespaces/api/api.go create mode 100644 internal/codespaces/api/api_test.go create mode 100644 internal/codespaces/codespaces.go create mode 100644 internal/codespaces/ssh.go create mode 100644 internal/codespaces/ssh_test.go create mode 100644 internal/codespaces/states.go create mode 100644 internal/config/config_map.go create mode 100644 internal/config/config_map_test.go create mode 100644 internal/config/from_file.go create mode 100644 internal/config/from_file_test.go create mode 100644 internal/httpunix/transport.go rename {update => internal/update}/update.go (53%) rename {update => internal/update}/update_test.go (67%) delete mode 100644 pkg/browser/browser.go delete mode 100644 pkg/browser/browser_test.go create mode 100644 pkg/cmd/actions/actions.go delete mode 100644 pkg/cmd/auth/client/client.go create mode 100644 pkg/cmd/auth/gitcredential/helper.go create mode 100644 pkg/cmd/auth/gitcredential/helper_test.go create mode 100644 pkg/cmd/auth/shared/git_credential.go create mode 100644 pkg/cmd/auth/shared/git_credential_test.go create mode 100644 pkg/cmd/auth/shared/login_flow.go create mode 100644 pkg/cmd/auth/shared/login_flow_test.go create mode 100644 pkg/cmd/auth/shared/oauth_scopes.go create mode 100644 pkg/cmd/auth/shared/oauth_scopes_test.go create mode 100644 pkg/cmd/auth/shared/ssh_keys.go create mode 100644 pkg/cmd/browse/browse.go create mode 100644 pkg/cmd/browse/browse_test.go create mode 100644 pkg/cmd/codespace/code.go create mode 100644 pkg/cmd/codespace/common.go create mode 100644 pkg/cmd/codespace/create.go create mode 100644 pkg/cmd/codespace/delete.go create mode 100644 pkg/cmd/codespace/delete_test.go create mode 100644 pkg/cmd/codespace/list.go create mode 100644 pkg/cmd/codespace/logs.go create mode 100644 pkg/cmd/codespace/mock_api.go create mode 100644 pkg/cmd/codespace/mock_prompter.go create mode 100644 pkg/cmd/codespace/output/format_json.go create mode 100644 pkg/cmd/codespace/output/format_table.go create mode 100644 pkg/cmd/codespace/output/format_tsv.go create mode 100644 pkg/cmd/codespace/output/logger.go create mode 100644 pkg/cmd/codespace/ports.go create mode 100644 pkg/cmd/codespace/root.go create mode 100644 pkg/cmd/codespace/ssh.go create mode 100644 pkg/cmd/codespace/stop.go create mode 100644 pkg/cmd/extension/command.go create mode 100644 pkg/cmd/extension/command_test.go create mode 100644 pkg/cmd/extension/extension.go create mode 100644 pkg/cmd/extension/http.go create mode 100644 pkg/cmd/extension/manager.go create mode 100644 pkg/cmd/extension/manager_test.go create mode 100644 pkg/cmd/extension/symlink_other.go create mode 100644 pkg/cmd/extension/symlink_windows.go create mode 100644 pkg/cmd/factory/default_test.go create mode 100644 pkg/cmd/factory/http_test.go create mode 100644 pkg/cmd/gist/clone/clone.go create mode 100644 pkg/cmd/gist/clone/clone_test.go create mode 100644 pkg/cmd/gist/delete/delete.go create mode 100644 pkg/cmd/gist/delete/delete_test.go delete mode 100644 pkg/cmd/gist/fixture.txt delete mode 100644 pkg/cmd/gist/list/http.go create mode 100644 pkg/cmd/issue/comment/comment.go create mode 100644 pkg/cmd/issue/comment/comment_test.go create mode 100644 pkg/cmd/issue/delete/delete.go create mode 100644 pkg/cmd/issue/delete/delete_test.go create mode 100644 pkg/cmd/issue/edit/edit.go create mode 100644 pkg/cmd/issue/edit/edit_test.go create mode 100644 pkg/cmd/issue/list/fixtures/issueSearch.json create mode 100644 pkg/cmd/issue/list/http.go create mode 100644 pkg/cmd/issue/list/http_test.go create mode 100644 pkg/cmd/issue/shared/lookup_test.go create mode 100644 pkg/cmd/issue/transfer/transfer.go create mode 100644 pkg/cmd/issue/transfer/transfer_test.go create mode 100644 pkg/cmd/issue/view/fixtures/issueView_previewFullComments.json create mode 100644 pkg/cmd/issue/view/fixtures/issueView_previewSingleComment.json create mode 100644 pkg/cmd/issue/view/http.go create mode 100644 pkg/cmd/pr/checks/fixtures/someSkipping.json create mode 100644 pkg/cmd/pr/comment/comment.go create mode 100644 pkg/cmd/pr/comment/comment_test.go create mode 100644 pkg/cmd/pr/edit/edit.go create mode 100644 pkg/cmd/pr/edit/edit_test.go create mode 100644 pkg/cmd/pr/list/http.go create mode 100644 pkg/cmd/pr/list/http_test.go create mode 100644 pkg/cmd/pr/merge/http.go create mode 100644 pkg/cmd/pr/shared/commentable.go create mode 100644 pkg/cmd/pr/shared/comments.go create mode 100644 pkg/cmd/pr/shared/editable.go create mode 100644 pkg/cmd/pr/shared/finder.go create mode 100644 pkg/cmd/pr/shared/finder_test.go delete mode 100644 pkg/cmd/pr/shared/lookup.go create mode 100644 pkg/cmd/pr/shared/preserve.go create mode 100644 pkg/cmd/pr/shared/preserve_test.go create mode 100644 pkg/cmd/pr/shared/reaction_groups.go create mode 100644 pkg/cmd/pr/shared/state.go create mode 100644 pkg/cmd/pr/shared/survey.go create mode 100644 pkg/cmd/pr/shared/survey_test.go create mode 100644 pkg/cmd/pr/shared/templates.go create mode 100644 pkg/cmd/pr/shared/templates_test.go delete mode 100644 pkg/cmd/pr/shared/title_body_survey.go delete mode 100644 pkg/cmd/pr/view/fixtures/prView.json delete mode 100644 pkg/cmd/pr/view/fixtures/prViewPreviewDraftStatebyBranch.json create mode 100644 pkg/cmd/pr/view/fixtures/prViewPreviewFullComments.json create mode 100644 pkg/cmd/pr/view/fixtures/prViewPreviewManyReviews.json create mode 100644 pkg/cmd/pr/view/fixtures/prViewPreviewReviews.json create mode 100644 pkg/cmd/pr/view/fixtures/prViewPreviewSingleComment.json delete mode 100644 pkg/cmd/pr/view/fixtures/prViewPreviewWithMetadataByBranch.json delete mode 100644 pkg/cmd/pr/view/fixtures/prView_EmptyBody.json delete mode 100644 pkg/cmd/pr/view/fixtures/prView_NoActiveBranch.json create mode 100644 pkg/cmd/repo/archive/archive.go create mode 100644 pkg/cmd/repo/archive/archive_test.go create mode 100644 pkg/cmd/repo/archive/http.go create mode 100644 pkg/cmd/repo/list/fixtures/repoList.json create mode 100644 pkg/cmd/repo/list/fixtures/repoSearch.json create mode 100644 pkg/cmd/repo/list/http.go create mode 100644 pkg/cmd/repo/list/http_test.go create mode 100644 pkg/cmd/repo/list/list.go create mode 100644 pkg/cmd/repo/list/list_test.go create mode 100644 pkg/cmd/repo/sync/git.go create mode 100644 pkg/cmd/repo/sync/http.go create mode 100644 pkg/cmd/repo/sync/mocks.go create mode 100644 pkg/cmd/repo/sync/sync.go create mode 100644 pkg/cmd/repo/sync/sync_test.go create mode 100644 pkg/cmd/run/cancel/cancel.go create mode 100644 pkg/cmd/run/cancel/cancel_test.go create mode 100644 pkg/cmd/run/download/download.go create mode 100644 pkg/cmd/run/download/download_test.go create mode 100644 pkg/cmd/run/download/fixtures/myproject.zip create mode 100644 pkg/cmd/run/download/http.go create mode 100644 pkg/cmd/run/download/http_test.go create mode 100644 pkg/cmd/run/download/zip.go create mode 100644 pkg/cmd/run/download/zip_test.go create mode 100644 pkg/cmd/run/list/list.go create mode 100644 pkg/cmd/run/list/list_test.go create mode 100644 pkg/cmd/run/rerun/rerun.go create mode 100644 pkg/cmd/run/rerun/rerun_test.go create mode 100644 pkg/cmd/run/run.go create mode 100644 pkg/cmd/run/shared/artifacts.go create mode 100644 pkg/cmd/run/shared/presentation.go create mode 100644 pkg/cmd/run/shared/shared.go create mode 100644 pkg/cmd/run/shared/shared_test.go create mode 100644 pkg/cmd/run/shared/test.go create mode 100644 pkg/cmd/run/view/fixtures/run_log.zip create mode 100644 pkg/cmd/run/view/view.go create mode 100644 pkg/cmd/run/view/view_test.go create mode 100644 pkg/cmd/run/watch/watch.go create mode 100644 pkg/cmd/run/watch/watch_test.go create mode 100644 pkg/cmd/secret/list/list.go create mode 100644 pkg/cmd/secret/list/list_test.go create mode 100644 pkg/cmd/secret/remove/remove.go create mode 100644 pkg/cmd/secret/remove/remove_test.go create mode 100644 pkg/cmd/secret/secret.go create mode 100644 pkg/cmd/secret/set/http.go create mode 100644 pkg/cmd/secret/set/set.go create mode 100644 pkg/cmd/secret/set/set_test.go create mode 100644 pkg/cmd/secret/shared/shared.go create mode 100644 pkg/cmd/ssh-key/add/add.go create mode 100644 pkg/cmd/ssh-key/add/add_test.go create mode 100644 pkg/cmd/ssh-key/add/http.go create mode 100644 pkg/cmd/ssh-key/list/http.go create mode 100644 pkg/cmd/ssh-key/list/list.go create mode 100644 pkg/cmd/ssh-key/list/list_test.go create mode 100644 pkg/cmd/ssh-key/ssh-key.go create mode 100644 pkg/cmd/workflow/disable/disable.go create mode 100644 pkg/cmd/workflow/disable/disable_test.go create mode 100644 pkg/cmd/workflow/enable/enable.go create mode 100644 pkg/cmd/workflow/enable/enable_test.go create mode 100644 pkg/cmd/workflow/list/list.go create mode 100644 pkg/cmd/workflow/list/list_test.go create mode 100644 pkg/cmd/workflow/run/run.go create mode 100644 pkg/cmd/workflow/run/run_test.go create mode 100644 pkg/cmd/workflow/shared/shared.go create mode 100644 pkg/cmd/workflow/shared/test.go create mode 100644 pkg/cmd/workflow/view/http.go create mode 100644 pkg/cmd/workflow/view/view.go create mode 100644 pkg/cmd/workflow/view/view_test.go create mode 100644 pkg/cmd/workflow/workflow.go create mode 100644 pkg/cmdutil/file_input.go create mode 100644 pkg/cmdutil/json_flags.go create mode 100644 pkg/cmdutil/json_flags_test.go create mode 100644 pkg/cmdutil/web_browser.go create mode 100644 pkg/export/filter.go create mode 100644 pkg/export/filter_test.go create mode 100644 pkg/export/template.go create mode 100644 pkg/export/template_test.go create mode 100644 pkg/extensions/extension.go create mode 100644 pkg/extensions/extension_mock.go create mode 100644 pkg/extensions/manager_mock.go create mode 100644 pkg/findsh/find.go create mode 100644 pkg/findsh/find_windows.go create mode 100644 pkg/githubsearch/query.go create mode 100644 pkg/iostreams/console.go create mode 100644 pkg/iostreams/console_windows.go create mode 100644 pkg/iostreams/iostreams_test.go create mode 100644 pkg/iostreams/tty_size.go create mode 100644 pkg/iostreams/tty_size_windows.go create mode 100644 pkg/liveshare/client.go create mode 100644 pkg/liveshare/client_test.go create mode 100644 pkg/liveshare/options_test.go create mode 100644 pkg/liveshare/port_forwarder.go create mode 100644 pkg/liveshare/port_forwarder_test.go create mode 100644 pkg/liveshare/rpc.go create mode 100644 pkg/liveshare/session.go create mode 100644 pkg/liveshare/session_test.go create mode 100644 pkg/liveshare/socket.go create mode 100644 pkg/liveshare/ssh.go create mode 100644 pkg/liveshare/test/server.go create mode 100644 pkg/liveshare/test/socket.go create mode 100644 pkg/set/string_set.go create mode 100644 pkg/set/string_set_test.go create mode 100644 pkg/surveyext/editor_test.go create mode 100644 script/build.bat create mode 100644 script/build.go delete mode 100755 script/changelog create mode 100644 script/createrepo.sh delete mode 100644 test/fixtures/test.git/HEAD delete mode 100644 test/fixtures/test.git/config delete mode 100644 test/fixtures/test.git/info/exclude delete mode 100644 test/fixtures/test.git/objects/08/f4b7b6513dffc6245857e497cfd6101dc47818 delete mode 100644 test/fixtures/test.git/objects/8a/1cdac440b4a3c44b988e300758a903a9866905 delete mode 100644 test/fixtures/test.git/objects/9b/5a719a3d76ac9dc2fa635d9b1f34fd73994c06 delete mode 100644 test/fixtures/test.git/objects/9d/aeafb9864cf43055ae93beb0afd6c7d144bfa4 delete mode 100644 test/fixtures/test.git/objects/ca/93b49848670d03b3968c8a481eca55f5fb2150 delete mode 100644 test/fixtures/test.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 delete mode 100644 test/fixtures/test.git/refs/heads/master diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index c15d1e8b07a..3b258406387 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -23,14 +23,19 @@ Please avoid: ## Building the project Prerequisites: -- Go 1.13+ for building the binary -- Go 1.15+ for running the test suite +- Go 1.16+ -Build with: `make` or `go build -o bin/gh ./cmd/gh` +Build with: +* Unix-like systems: `make` +* Windows: `go run script/build.go` -Run the new binary as: `./bin/gh` +Run the new binary as: +* Unix-like systems: `bin/gh` +* Windows: `bin\gh` -Run tests with: `make test` or `go test ./...` +Run tests with: `go test ./...` + +See [project layout documentation](../docs/project-layout.md) for information on where to find specific source files. ## Submitting a pull request @@ -44,6 +49,10 @@ Please note that this project adheres to a [Contributor Code of Conduct][code-of We generate manual pages from source on every release. You do not need to submit pull requests for documentation specifically; manual pages for commands will automatically get updated after your pull requests gets accepted. +## Design guidelines + +You may reference the [CLI Design System][] when suggesting features, and are welcome to use our [Google Docs Template][] to suggest designs. + ## Resources - [How to Contribute to Open Source][] @@ -61,3 +70,5 @@ We generate manual pages from source on every release. You do not need to submit [How to Contribute to Open Source]: https://opensource.guide/how-to-contribute/ [Using Pull Requests]: https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/about-pull-requests [GitHub Help]: https://docs.github.com/ +[CLI Design System]: https://primer.style/cli/ +[Google Docs Template]: https://docs.google.com/document/d/1JIRErIUuJ6fTgabiFYfCH3x91pyHuytbfa0QLnTfXKM/edit#heading=h.or54sa47ylpg diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..0cad7e02ccc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Ask a question on how to use GitHub CLI + about: For general-purpose questions and answers, see the Discussions section. + url: https://github.com/cli/cli/discussions + - name: Ask a question about the GitHub API + about: Please check out the GitHub community forum for discussions about the GitHub API. + url: https://github.community/c/github-ecosystem/37 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..aa6662d49b2 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,4 @@ + diff --git a/.github/PULL_REQUEST_TEMPLATE/bug_fix.md b/.github/PULL_REQUEST_TEMPLATE/bug_fix.md deleted file mode 100644 index ca33dd34cb1..00000000000 --- a/.github/PULL_REQUEST_TEMPLATE/bug_fix.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: "\U0001F41B Bug fix" -about: Fix a bug in GitHub CLI - ---- - - - -## Summary - -closes #[issue number] - -## Details - -- diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..1a850c9b3bc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: +- package-ecosystem: gomod + directory: "/" + schedule: + interval: "daily" + ignore: + - dependency-name: "*" + update-types: + - version-update:semver-minor + - version-update:semver-major +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 28d17464b14..d46c0bcf330 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,6 +2,11 @@ name: Code Scanning on: push: + branches: [trunk] + pull_request: + branches: [trunk] + paths-ignore: + - '**/*.md' schedule: - cron: "0 0 * * 0" @@ -17,6 +22,7 @@ jobs: uses: github/codeql-action/init@v1 with: languages: go + queries: security-and-quality - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 83fa87b8e59..226050ea90a 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -9,10 +9,10 @@ jobs: runs-on: ${{ matrix.os }} steps: - - name: Set up Go 1.15 + - name: Set up Go 1.16 uses: actions/setup-go@v2 with: - go-version: 1.15 + go-version: 1.16 - name: Check out code uses: actions/checkout@v2 @@ -25,18 +25,3 @@ jobs: - name: Build run: go build -v ./cmd/gh - - build-minimum: - runs-on: ubuntu-latest - - steps: - - name: Set up Go 1.13 - uses: actions/setup-go@v2 - with: - go-version: 1.13 - - - name: Check out code - uses: actions/checkout@v2 - - - name: Build - run: go build -v ./cmd/gh diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4dc95a4f1a8..78d94c78e5d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,10 +16,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go 1.15 + - name: Set up Go 1.16 uses: actions/setup-go@v2 with: - go-version: 1.15 + go-version: 1.16 - name: Check out code uses: actions/checkout@v2 @@ -29,7 +29,7 @@ jobs: go mod verify go mod download - LINT_VERSION=1.29.0 + LINT_VERSION=1.39.0 curl -fsSL https://github.com/golangci/golangci-lint/releases/download/v${LINT_VERSION}/golangci-lint-${LINT_VERSION}-linux-amd64.tar.gz | \ tar xz --strip-components 1 --wildcards \*/golangci-lint mkdir -p bin && mv golangci-lint bin/ @@ -50,10 +50,6 @@ jobs: assert-nothing-changed go fmt ./... assert-nothing-changed go mod tidy - while read -r file linter msg; do - IFS=: read -ra f <<<"$file" - printf '::error file=%s,line=%s,col=%s::%s\n' "${f[0]}" "${f[1]}" "${f[2]}" "[$linter] $msg" - STATUS=1 - done < <(bin/golangci-lint run --out-format tab) + bin/golangci-lint run --out-format=github-actions --timeout=3m || STATUS=$? exit $STATUS diff --git a/.github/workflows/prauto.yml b/.github/workflows/prauto.yml new file mode 100644 index 00000000000..58930656acb --- /dev/null +++ b/.github/workflows/prauto.yml @@ -0,0 +1,78 @@ +name: PR Automation +on: + pull_request_target: + types: [ready_for_review, opened, reopened] +jobs: + pr-auto: + runs-on: ubuntu-latest + steps: + - name: lint pr + env: + GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} + PRID: ${{ github.event.pull_request.node_id }} + PRBODY: ${{ github.event.pull_request.body }} + PRNUM: ${{ github.event.pull_request.number }} + PRHEAD: ${{ github.event.pull_request.head.label }} + PRAUTHOR: ${{ github.event.pull_request.user.login }} + PR_AUTHOR_TYPE: ${{ github.event.pull_request.user.type }} + if: "!github.event.pull_request.draft" + run: | + commentPR () { + gh pr comment $PRNUM -b "${1}" + } + + closePR () { + gh pr close $PRNUM + } + + colID () { + gh api graphql -f query='query($owner:String!, $repo:String!) { + repository(owner:$owner, name:$repo) { + project(number:1) { + columns(first:10) { nodes {id,name} } + } + } + }' -f owner="${GH_REPO%/*}" -f repo="${GH_REPO#*/}" \ + -q ".data.repository.project.columns.nodes[] | select(.name | startswith(\"$1\")) | .id" + } + + addToBoard () { + gh api graphql --silent -f query=' + mutation($colID:ID!, $prID:ID!) { addProjectCard(input: { projectColumnId: $colID, contentId: $prID }) { clientMutationId } } + ' -f colID="$(colID "Needs review")" -f prID="$PRID" + } + + if [ "$PR_AUTHOR_TYPE" = "Bot" ] || gh api orgs/cli/public_members/$PRAUTHOR --silent 2>/dev/null + then + if ! errtext="$(addToBoard 2>&1)" + then + cat <<<"$errtext" >&2 + if ! grep -iq 'project already has the associated issue' <<<"$errtext" + then + exit 1 + fi + fi + exit 0 + fi + + if [ "$PRHEAD" = "cli:trunk" ] + then + closePR + exit 0 + fi + + if [ $(wc -c <<<"$PRBODY") -lt 10 ] + then + commentPR "Thanks for the pull request! We're a small team and it's helpful to have context around community submissions in order to review them appropriately. Our automation has closed this pull request since it does not have an adequate description. Please edit the body of this pull request to describe what this does, then reopen it." + closePR + exit 0 + fi + + if ! grep -Eq '(#|issues/)[0-9]+' <<<"$PRBODY" + then + commentPR "Hi! Thanks for the pull request. Please ensure that this change is linked to an issue by mentioning an issue number in the description of the pull request. If this pull request would close the issue, please put the word 'Fixes' before the issue number somewhere in the pull request body. If this is a tiny change like fixing a typo, feel free to ignore this message." + fi + + addToBoard + exit 0 diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 3af2d090eb2..f44689804e3 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -11,29 +11,35 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - - name: Set up Go 1.15 + - name: Set up Go 1.16 uses: actions/setup-go@v2 with: - go-version: 1.15 + go-version: 1.16 - name: Generate changelog + id: changelog run: | - echo "GORELEASER_CURRENT_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - git fetch --unshallow - script/changelog | tee CHANGELOG.md + echo "::set-output name=tag-name::${GITHUB_REF#refs/tags/}" + gh api repos/$GITHUB_REPOSITORY/releases/generate-notes \ + -f tag_name="${GITHUB_REF#refs/tags/}" \ + -f target_commitish=trunk \ + -q .body > CHANGELOG.md + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 with: - version: latest + version: v0.174.1 args: release --release-notes=CHANGELOG.md env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + GORELEASER_CURRENT_TAG: ${{steps.changelog.outputs.tag-name}} - name: Checkout documentation site uses: actions/checkout@v2 with: repository: github/cli.github.com path: site fetch-depth: 0 - token: ${{secrets.SITE_GITHUB_TOKEN}} + ssh-key: ${{secrets.SITE_SSH_KEY}} - name: Update site man pages env: GIT_COMMITTER_NAME: cli automation @@ -57,7 +63,7 @@ jobs: echo "moved ${#cards[@]} cards to the Done column" - name: Install packaging dependencies - run: sudo apt-get install -y createrepo rpm reprepro + run: sudo apt-get install -y rpm reprepro - name: Set up GPG run: | gpg --import --no-tty --batch --yes < script/pubkey.asc @@ -73,13 +79,14 @@ jobs: run: | mkdir -p site/packages/rpm cp dist/*.rpm site/packages/rpm/ - createrepo site/packages/rpm + ./script/createrepo.sh + cp -r dist/repodata site/packages/rpm/ pushd site/packages/rpm gpg --yes --detach-sign --armor repodata/repomd.xml popd - name: Run reprepro env: - RELEASES: "cosmic eoan disco groovy focal stable oldstable testing unstable buster bullseye stretch jessie bionic trusty precise xenial" + RELEASES: "cosmic eoan disco groovy focal stable oldstable testing sid unstable buster bullseye stretch jessie bionic trusty precise xenial hirsute impish kali-rolling" run: | mkdir -p upload for release in $RELEASES; do @@ -132,9 +139,11 @@ jobs: - name: Build MSI id: buildmsi shell: bash + env: + ZIP_FILE: ${{ steps.download_exe.outputs.zip }} run: | mkdir -p build - msi="$(basename "${{ steps.download_exe.outputs.zip }}" ".zip").msi" + msi="$(basename "$ZIP_FILE" ".zip").msi" printf "::set-output name=msi::%s\n" "$msi" go-msi make --msi "$PWD/$msi" --out "$PWD/build" --version "${GITHUB_REF#refs/tags/}" - name: Obtain signing cert @@ -144,14 +153,24 @@ jobs: run: .\script\setup-windows-certificate.ps1 - name: Sign MSI env: + CERT_FILE: ${{ steps.obtain_cert.outputs.cert-file }} + EXE_FILE: ${{ steps.buildmsi.outputs.msi }} GITHUB_CERT_PASSWORD: ${{ secrets.GITHUB_CERT_PASSWORD }} - run: | - .\script\sign.ps1 -Certificate "${{ steps.obtain_cert.outputs.cert-file }}" ` - -Executable "${{ steps.buildmsi.outputs.msi }}" + run: .\script\sign.ps1 -Certificate $env:CERT_FILE -Executable $env:EXE_FILE - name: Upload MSI shell: bash - run: hub release edit "${GITHUB_REF#refs/tags/}" -m "" --draft=false -a "${{ steps.buildmsi.outputs.msi }}" + run: | + tag_name="${GITHUB_REF#refs/tags/}" + hub release edit "$tag_name" -m "" -a "$MSI_FILE" + release_url="$(gh api repos/:owner/:repo/releases -q ".[]|select(.tag_name==\"${tag_name}\")|.url")" + publish_args=( -F draft=false ) + if [[ $GITHUB_REF != *-* ]]; then + publish_args+=( -f discussion_category_name="$DISCUSSION_CATEGORY" ) + fi + gh api -X PATCH "$release_url" "${publish_args[@]}" env: + MSI_FILE: ${{ steps.buildmsi.outputs.msi }} + DISCUSSION_CATEGORY: General GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - name: Bump homebrew-core formula uses: mislav/bump-homebrew-formula-action@v1 @@ -184,3 +203,18 @@ jobs: GIT_AUTHOR_NAME: cli automation GIT_COMMITTER_EMAIL: noreply@github.com GIT_AUTHOR_EMAIL: noreply@github.com + - name: Bump Winget manifest + shell: pwsh + env: + WINGETCREATE_VERSION: v0.2.0.29-preview + GITHUB_TOKEN: ${{ secrets.UPLOAD_GITHUB_TOKEN }} + run: | + $tagname = $env:GITHUB_REF.Replace("refs/tags/", "") + $version = $tagname.Replace("v", "") + $url = "https://github.com/cli/cli/releases/download/${tagname}/gh_${version}_windows_amd64.msi" + iwr https://github.com/microsoft/winget-create/releases/download/${env:WINGETCREATE_VERSION}/wingetcreate.exe -OutFile wingetcreate.exe + + .\wingetcreate.exe update GitHub.cli --url $url --version $version + if ($version -notmatch "-") { + .\wingetcreate.exe submit .\manifests\g\GitHub\cli\${version}\ --token $env:GITHUB_TOKEN + } diff --git a/.gitignore b/.gitignore index 00a5bb5a69e..9057015344c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /site .github/**/node_modules /CHANGELOG.md +/script/build # VS Code .vscode @@ -16,4 +17,7 @@ # macOS .DS_Store +# vim +*.swp + vendor/ diff --git a/.golangci.yml b/.golangci.yml index 57e53d6fbf6..ff7f3701405 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,3 +1,8 @@ linters: enable: - gofmt + - gofmt + - nolintlint + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/.goreleaser.yml b/.goreleaser.yml index f04e7f7f2e9..4c5f62a0609 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -15,7 +15,7 @@ builds: binary: bin/gh main: ./cmd/gh ldflags: - - -s -w -X github.com/cli/cli/internal/build.Version={{.Version}} -X github.com/cli/cli/internal/build.Date={{time "2006-01-02"}} + - -s -w -X github.com/cli/cli/v2/internal/build.Version={{.Version}} -X github.com/cli/cli/v2/internal/build.Date={{time "2006-01-02"}} - -X main.updaterEnabled=cli/cli id: macos goos: [darwin] @@ -24,7 +24,9 @@ builds: - <<: *build_defaults id: linux goos: [linux] - goarch: [386, amd64, arm64] + goarch: [386, arm, amd64, arm64] + env: + - CGO_ENABLED=0 - <<: *build_defaults id: windows @@ -55,12 +57,13 @@ nfpms: - license: MIT maintainer: GitHub homepage: https://github.com/cli/cli - bindir: /usr + bindir: /usr/bin dependencies: - git description: GitHub’s official command line tool. formats: - deb - rpm - files: - "./share/man/man1/gh*.1": "/usr/share/man/man1" + contents: + - src: "./share/man/man1/gh*.1" + dst: "/usr/share/man/man1" diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000000..0f505885f4c --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,5 @@ +* @cli/code-reviewers + +pkg/cmd/codespace/* @cli/codespaces +pkg/liveshare/* @cli/codespaces +internal/codespaces/* @cli/codespaces diff --git a/Makefile b/Makefile index 85946198463..46d40a7a928 100644 --- a/Makefile +++ b/Makefile @@ -1,45 +1,43 @@ -BUILD_FILES = $(shell go list -f '{{range .GoFiles}}{{$$.Dir}}/{{.}}\ -{{end}}' ./...) +CGO_CPPFLAGS ?= ${CPPFLAGS} +export CGO_CPPFLAGS +CGO_CFLAGS ?= ${CFLAGS} +export CGO_CFLAGS +CGO_LDFLAGS ?= $(filter -g -L% -l% -O%,${LDFLAGS}) +export CGO_LDFLAGS -GH_VERSION ?= $(shell git describe --tags 2>/dev/null || git rev-parse --short HEAD) -DATE_FMT = +%Y-%m-%d -ifdef SOURCE_DATE_EPOCH - BUILD_DATE ?= $(shell date -u -d "@$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u -r "$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u "$(DATE_FMT)") -else - BUILD_DATE ?= $(shell date "$(DATE_FMT)") +EXE = +ifeq ($(GOOS),windows) +EXE = .exe endif -ifndef CGO_CPPFLAGS - export CGO_CPPFLAGS := $(CPPFLAGS) -endif -ifndef CGO_CFLAGS - export CGO_CFLAGS := $(CFLAGS) -endif -ifndef CGO_LDFLAGS - export CGO_LDFLAGS := $(LDFLAGS) -endif +## The following tasks delegate to `script/build.go` so they can be run cross-platform. -GO_LDFLAGS := -X github.com/cli/cli/internal/build.Version=$(GH_VERSION) $(GO_LDFLAGS) -GO_LDFLAGS := -X github.com/cli/cli/internal/build.Date=$(BUILD_DATE) $(GO_LDFLAGS) -ifdef GH_OAUTH_CLIENT_SECRET - GO_LDFLAGS := -X github.com/cli/cli/internal/authflow.oauthClientID=$(GH_OAUTH_CLIENT_ID) $(GO_LDFLAGS) - GO_LDFLAGS := -X github.com/cli/cli/internal/authflow.oauthClientSecret=$(GH_OAUTH_CLIENT_SECRET) $(GO_LDFLAGS) -endif +.PHONY: bin/gh$(EXE) +bin/gh$(EXE): script/build + @script/build $@ -bin/gh: $(BUILD_FILES) - @go build -trimpath -ldflags "$(GO_LDFLAGS)" -o "$@" ./cmd/gh +script/build: script/build.go + GOOS= GOARCH= GOARM= GOFLAGS= CGO_ENABLED= go build -o $@ $< -clean: - rm -rf ./bin ./share .PHONY: clean +clean: script/build + @script/build $@ +.PHONY: manpages +manpages: script/build + @script/build $@ + +# just a convenience task around `go test` +.PHONY: test test: go test ./... -.PHONY: test + +## Site-related tasks are exclusively intended for use by the GitHub CLI team and for our release automation. site: git clone https://github.com/github/cli.github.com.git "$@" +.PHONY: site-docs site-docs: site git -C site pull git -C site rm 'manual/gh*.md' 2>/dev/null || true @@ -47,8 +45,8 @@ site-docs: site rm -f site/manual/*.bak git -C site add 'manual/gh*.md' git -C site commit -m 'update help docs' || true -.PHONY: site-docs +.PHONY: site-bump site-bump: site-docs ifndef GITHUB_REF $(error GITHUB_REF is not set) @@ -56,9 +54,21 @@ endif sed -i.bak -E 's/(assign version = )".+"/\1"$(GITHUB_REF:refs/tags/v%=%)"/' site/index.html rm -f site/index.html.bak git -C site commit -m '$(GITHUB_REF:refs/tags/v%=%)' index.html -.PHONY: site-bump +## Install/uninstall tasks are here for use on *nix platform. On Windows, there is no equivalent. -.PHONY: manpages -manpages: - go run ./cmd/gen-docs --man-page --doc-path ./share/man/man1/ +DESTDIR := +prefix := /usr/local +bindir := ${prefix}/bin +mandir := ${prefix}/share/man + +.PHONY: install +install: bin/gh manpages + install -d ${DESTDIR}${bindir} + install -m755 bin/gh ${DESTDIR}${bindir}/ + install -d ${DESTDIR}${mandir}/man1 + install -m644 ./share/man/man1/* ${DESTDIR}${mandir}/man1/ + +.PHONY: uninstall +uninstall: + rm -f ${DESTDIR}${bindir}/gh ${DESTDIR}${mandir}/man1/gh.1 ${DESTDIR}${mandir}/man1/gh-*.1 diff --git a/README.md b/README.md index 146b44d1ceb..e503c88c9fc 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,12 @@ GitHub CLI is available for repositories hosted on GitHub.com and GitHub Enterpr If anything feels off, or if you feel that some functionality is missing, please check out the [contributing page][contributing]. There you will find instructions for sharing your feedback, building the tool locally, and submitting pull requests to the project. - ## Installation ### macOS -`gh` is available via [Homebrew][], [MacPorts][], and as a downloadable binary from the [releases page][]. +`gh` is available via [Homebrew][], [MacPorts][], [Conda][], [Spack][], and as a downloadable binary from the [releases page][]. #### Homebrew @@ -34,39 +33,41 @@ If anything feels off, or if you feel that some functionality is missing, please | ---------------------- | ---------------------------------------------- | | `sudo port install gh` | `sudo port selfupdate && sudo port upgrade gh` | -### Linux +#### Conda -`gh` is available via [Homebrew](#homebrew), and as downloadable binaries from the [releases page][]. +| Install: | Upgrade: | +|------------------------------------------|-----------------------------------------| +| `conda install gh --channel conda-forge` | `conda update gh --channel conda-forge` | -For more information and distro-specific instructions, see the [Linux installation docs](./docs/install_linux.md). +Additional Conda installation options available on the [gh-feedstock page](https://github.com/conda-forge/gh-feedstock#installing-gh). -### Windows +#### Spack -`gh` is available via [WinGet][], [scoop][], [Chocolatey][], and as downloadable MSI. +| Install: | Upgrade: | +| ------------------ | ---------------------------------------- | +| `spack install gh` | `spack uninstall gh && spack install gh` | +### Linux & BSD -#### WinGet +`gh` is available via [Homebrew](#homebrew), [Conda](#conda), [Spack](#spack), and as downloadable binaries from the [releases page][]. -| Install: | Upgrade: | -| ------------------- | --------------------| -| `winget install gh` | `winget install gh` | +For instructions on specific distributions and package managers, see [Linux & BSD installation](./docs/install_linux.md). -WinGet does not have a specialized `upgrade` command yet, but the `install` command should work for upgrading to a newer version of GitHub CLI. +### Windows -#### scoop +`gh` is available via [WinGet][], [scoop][], [Chocolatey][], [Conda](#conda), and as downloadable MSI. -Install: +#### WinGet -```powershell -scoop bucket add github-gh https://github.com/cli/scoop-gh.git -scoop install gh -``` +| Install: | Upgrade: | +| ------------------- | --------------------| +| `winget install --id GitHub.cli` | `winget upgrade --id GitHub.cli` | -Upgrade: +#### scoop -```powershell -scoop update gh -``` +| Install: | Upgrade: | +| ------------------ | ------------------ | +| `scoop install gh` | `scoop update gh` | #### Chocolatey @@ -78,6 +79,10 @@ scoop update gh MSI installers are available for download on the [releases page][]. +### GitHub Actions + +GitHub CLI comes pre-installed in all [GitHub-Hosted Runners](https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners). + ### Other platforms Download packaged binaries from the [releases page][]. @@ -93,13 +98,14 @@ what an official GitHub CLI tool can look like with a fundamentally different de tools bring GitHub to the terminal, `hub` behaves as a proxy to `git`, and `gh` is a standalone tool. Check out our [more detailed explanation][gh-vs-hub] to learn more. - [manual]: https://cli.github.com/manual/ [Homebrew]: https://brew.sh [MacPorts]: https://www.macports.org [winget]: https://github.com/microsoft/winget-cli [scoop]: https://scoop.sh [Chocolatey]: https://chocolatey.org +[Conda]: https://docs.conda.io/en/latest/ +[Spack]: https://spack.io [releases page]: https://github.com/cli/cli/releases/latest [hub]: https://github.com/github/hub [contributing]: ./.github/CONTRIBUTING.md diff --git a/api/cache.go b/api/cache.go index 620660c1506..fc9d1c5ba2a 100644 --- a/api/cache.go +++ b/api/cache.go @@ -16,10 +16,10 @@ import ( "time" ) -func makeCachedClient(httpClient *http.Client, cacheTTL time.Duration) *http.Client { +func NewCachedClient(httpClient *http.Client, cacheTTL time.Duration) *http.Client { cacheDir := filepath.Join(os.TempDir(), "gh-cli-cache") return &http.Client{ - Transport: CacheReponse(cacheTTL, cacheDir)(httpClient.Transport), + Transport: CacheResponse(cacheTTL, cacheDir)(httpClient.Transport), } } @@ -39,8 +39,8 @@ func isCacheableResponse(res *http.Response) bool { return res.StatusCode < 500 && res.StatusCode != 403 } -// CacheReponse produces a RoundTripper that caches HTTP responses to disk for a specified amount of time -func CacheReponse(ttl time.Duration, dir string) ClientOption { +// CacheResponse produces a RoundTripper that caches HTTP responses to disk for a specified amount of time +func CacheResponse(ttl time.Duration, dir string) ClientOption { fs := fileStorage{ dir: dir, ttl: ttl, @@ -167,9 +167,13 @@ func (fs *fileStorage) store(key string, res *http.Response) error { defer f.Close() var origBody io.ReadCloser - origBody, res.Body = copyStream(res.Body) - defer res.Body.Close() + if res.Body != nil { + origBody, res.Body = copyStream(res.Body) + defer res.Body.Close() + } err = res.Write(f) - res.Body = origBody + if origBody != nil { + res.Body = origBody + } return err } diff --git a/api/cache_test.go b/api/cache_test.go index d1039d71b71..f4a6a756ee4 100644 --- a/api/cache_test.go +++ b/api/cache_test.go @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/require" ) -func Test_CacheReponse(t *testing.T) { +func Test_CacheResponse(t *testing.T) { counter := 0 fakeHTTP := funcTripper{ roundTrip: func(req *http.Request) (*http.Response, error) { @@ -32,7 +32,7 @@ func Test_CacheReponse(t *testing.T) { } cacheDir := filepath.Join(t.TempDir(), "gh-cli-cache") - httpClient := NewHTTPClient(ReplaceTripper(fakeHTTP), CacheReponse(time.Minute, cacheDir)) + httpClient := NewHTTPClient(ReplaceTripper(fakeHTTP), CacheResponse(time.Minute, cacheDir)) do := func(method, url string, body io.Reader) (string, error) { req, err := http.NewRequest(method, url, body) diff --git a/api/client.go b/api/client.go index 09195181bf2..e3e48f57d73 100644 --- a/api/client.go +++ b/api/client.go @@ -11,7 +11,7 @@ import ( "regexp" "strings" - "github.com/cli/cli/internal/ghinstance" + "github.com/cli/cli/v2/internal/ghinstance" "github.com/henvic/httpretty" "github.com/shurcooL/graphql" ) @@ -111,6 +111,10 @@ type Client struct { http *http.Client } +func (c *Client) HTTP() *http.Client { + return c.http +} + type graphQLResponse struct { Data interface{} Errors []GraphQLError @@ -119,8 +123,8 @@ type graphQLResponse struct { // GraphQLError is a single error returned in a GraphQL response type GraphQLError struct { Type string - Path []string Message string + // Path []interface // mixed strings and numbers } // GraphQLErrorResponse contains errors returned in a GraphQL response @@ -138,10 +142,19 @@ func (gr GraphQLErrorResponse) Error() string { // HTTPError is an error returned by a failed API call type HTTPError struct { - StatusCode int - RequestURL *url.URL - Message string - OAuthScopes string + StatusCode int + RequestURL *url.URL + Message string + Errors []HTTPErrorItem + + scopesSuggestion string +} + +type HTTPErrorItem struct { + Message string + Resource string + Field string + Code string } func (err HTTPError) Error() string { @@ -153,77 +166,59 @@ func (err HTTPError) Error() string { return fmt.Sprintf("HTTP %d (%s)", err.StatusCode, err.RequestURL) } -type MissingScopesError struct { - MissingScopes []string +func (err HTTPError) ScopesSuggestion() string { + return err.scopesSuggestion } -func (e MissingScopesError) Error() string { - var missing []string - for _, s := range e.MissingScopes { - missing = append(missing, fmt.Sprintf("'%s'", s)) +// ScopesSuggestion is an error messaging utility that prints the suggestion to request additional OAuth +// scopes in case a server response indicates that there are missing scopes. +func ScopesSuggestion(resp *http.Response) string { + if resp.StatusCode < 400 || resp.StatusCode > 499 { + return "" } - scopes := strings.Join(missing, ", ") - if len(e.MissingScopes) == 1 { - return "missing required scope " + scopes + endpointNeedsScopes := resp.Header.Get("X-Accepted-Oauth-Scopes") + tokenHasScopes := resp.Header.Get("X-Oauth-Scopes") + if tokenHasScopes == "" { + return "" } - return "missing required scopes " + scopes -} - -func (c Client) HasMinimumScopes(hostname string) error { - apiEndpoint := ghinstance.RESTPrefix(hostname) - - req, err := http.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return err - } - - req.Header.Set("Content-Type", "application/json; charset=utf-8") - res, err := c.http.Do(req) - if err != nil { - return err - } - - defer func() { - // Ensure the response body is fully read and closed - // before we reconnect, so that we reuse the same TCPconnection. - _, _ = io.Copy(ioutil.Discard, res.Body) - res.Body.Close() - }() - if res.StatusCode != 200 { - return HandleHTTPError(res) - } - - scopesHeader := res.Header.Get("X-Oauth-Scopes") - if scopesHeader == "" { - // if the token reports no scopes, assume that it's an integration token and give up on - // detecting its capabilities - return nil - } - - search := map[string]bool{ - "repo": false, - "read:org": false, - "admin:org": false, - } - for _, s := range strings.Split(scopesHeader, ",") { - search[strings.TrimSpace(s)] = true + gotScopes := map[string]struct{}{} + for _, s := range strings.Split(tokenHasScopes, ",") { + s = strings.TrimSpace(s) + gotScopes[s] = struct{}{} + if strings.HasPrefix(s, "admin:") { + gotScopes["read:"+strings.TrimPrefix(s, "admin:")] = struct{}{} + gotScopes["write:"+strings.TrimPrefix(s, "admin:")] = struct{}{} + } else if strings.HasPrefix(s, "write:") { + gotScopes["read:"+strings.TrimPrefix(s, "write:")] = struct{}{} + } } - var missingScopes []string - if !search["repo"] { - missingScopes = append(missingScopes, "repo") + for _, s := range strings.Split(endpointNeedsScopes, ",") { + s = strings.TrimSpace(s) + if _, gotScope := gotScopes[s]; s == "" || gotScope { + continue + } + return fmt.Sprintf( + "This API operation needs the %[1]q scope. To request it, run: gh auth refresh -h %[2]s -s %[1]s", + s, + ghinstance.NormalizeHostname(resp.Request.URL.Hostname()), + ) } - if !search["read:org"] && !search["admin:org"] { - missingScopes = append(missingScopes, "read:org") - } + return "" +} - if len(missingScopes) > 0 { - return &MissingScopesError{MissingScopes: missingScopes} +// EndpointNeedsScopes adds additional OAuth scopes to an HTTP response as if they were returned from the +// server endpoint. This improves HTTP 4xx error messaging for endpoints that don't explicitly list the +// OAuth scopes they need. +func EndpointNeedsScopes(resp *http.Response, s string) *http.Response { + if resp.StatusCode >= 400 && resp.StatusCode < 500 { + oldScopes := resp.Header.Get("X-Accepted-Oauth-Scopes") + resp.Header.Set("X-Accepted-Oauth-Scopes", fmt.Sprintf("%s, %s", oldScopes, s)) } - return nil + return resp } // GraphQL performs a GraphQL request and parses the response @@ -255,8 +250,7 @@ func graphQLClient(h *http.Client, hostname string) *graphql.Client { // REST performs a REST request and parses the response. func (c Client) REST(hostname string, method string, p string, body io.Reader, data interface{}) error { - url := ghinstance.RESTPrefix(hostname) + p - req, err := http.NewRequest(method, url, body) + req, err := http.NewRequest(method, restURL(hostname, p), body) if err != nil { return err } @@ -282,7 +276,6 @@ func (c Client) REST(hostname string, method string, p string, body io.Reader, d if err != nil { return err } - err = json.Unmarshal(b, &data) if err != nil { return err @@ -291,6 +284,13 @@ func (c Client) REST(hostname string, method string, p string, body io.Reader, d return nil } +func restURL(hostname string, pathOrURL string) string { + if strings.HasPrefix(pathOrURL, "https://") || strings.HasPrefix(pathOrURL, "http://") { + return pathOrURL + } + return ghinstance.RESTPrefix(hostname) + pathOrURL +} + func handleResponse(resp *http.Response, data interface{}) error { success := resp.StatusCode >= 200 && resp.StatusCode < 300 @@ -317,9 +317,9 @@ func handleResponse(resp *http.Response, data interface{}) error { func HandleHTTPError(resp *http.Response) error { httpError := HTTPError{ - StatusCode: resp.StatusCode, - RequestURL: resp.Request.URL, - OAuthScopes: resp.Header.Get("X-Oauth-Scopes"), + StatusCode: resp.StatusCode, + RequestURL: resp.Request.URL, + scopesSuggestion: ScopesSuggestion(resp), } if !jsonTypeRE.MatchString(resp.Header.Get("Content-Type")) { @@ -341,30 +341,28 @@ func HandleHTTPError(resp *http.Response) error { return httpError } - type errorObject struct { - Message string - Resource string - Field string - Code string + var messages []string + if parsedBody.Message != "" { + messages = append(messages, parsedBody.Message) } - - messages := []string{parsedBody.Message} for _, raw := range parsedBody.Errors { switch raw[0] { case '"': var errString string _ = json.Unmarshal(raw, &errString) messages = append(messages, errString) + httpError.Errors = append(httpError.Errors, HTTPErrorItem{Message: errString}) case '{': - var errInfo errorObject + var errInfo HTTPErrorItem _ = json.Unmarshal(raw, &errInfo) msg := errInfo.Message - if errInfo.Code != "custom" { + if errInfo.Code != "" && errInfo.Code != "custom" { msg = fmt.Sprintf("%s.%s %s", errInfo.Resource, errInfo.Field, errorCodeToMessage(errInfo.Code)) } if msg != "" { messages = append(messages, msg) } + httpError.Errors = append(httpError.Errors, errInfo) } } httpError.Message = strings.Join(messages, "\n") diff --git a/api/client_test.go b/api/client_test.go index 35a45af8c38..c7848d24250 100644 --- a/api/client_test.go +++ b/api/client_test.go @@ -5,19 +5,12 @@ import ( "errors" "io/ioutil" "net/http" - "reflect" "testing" - "github.com/cli/cli/pkg/httpmock" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/stretchr/testify/assert" ) -func eq(t *testing.T, got interface{}, expected interface{}) { - t.Helper() - if !reflect.DeepEqual(got, expected) { - t.Errorf("expected: %v, got: %v", expected, got) - } -} - func TestGraphQL(t *testing.T) { http := &httpmock.Registry{} client := NewClient( @@ -32,15 +25,19 @@ func TestGraphQL(t *testing.T) { } }{} - http.StubResponse(200, bytes.NewBufferString(`{"data":{"viewer":{"login":"hubot"}}}`)) + http.Register( + httpmock.GraphQL("QUERY"), + httpmock.StringResponse(`{"data":{"viewer":{"login":"hubot"}}}`), + ) + err := client.GraphQL("github.com", "QUERY", vars, &response) - eq(t, err, nil) - eq(t, response.Viewer.Login, "hubot") + assert.NoError(t, err) + assert.Equal(t, "hubot", response.Viewer.Login) req := http.Requests[0] reqBody, _ := ioutil.ReadAll(req.Body) - eq(t, string(reqBody), `{"query":"QUERY","variables":{"name":"Mona"}}`) - eq(t, req.Header.Get("Authorization"), "token OTOKEN") + assert.Equal(t, `{"query":"QUERY","variables":{"name":"Mona"}}`, string(reqBody)) + assert.Equal(t, "token OTOKEN", req.Header.Get("Authorization")) } func TestGraphQLError(t *testing.T) { @@ -48,12 +45,17 @@ func TestGraphQLError(t *testing.T) { client := NewClient(ReplaceTripper(http)) response := struct{}{} - http.StubResponse(200, bytes.NewBufferString(` - { "errors": [ - {"message":"OH NO"}, - {"message":"this is fine"} - ] - }`)) + + http.Register( + httpmock.GraphQL(""), + httpmock.StringResponse(` + { "errors": [ + {"message":"OH NO"}, + {"message":"this is fine"} + ] + } + `), + ) err := client.GraphQL("github.com", "", nil, &response) if err == nil || err.Error() != "GraphQL error: OH NO\nthis is fine" { @@ -68,11 +70,34 @@ func TestRESTGetDelete(t *testing.T) { ReplaceTripper(http), ) - http.StubResponse(204, bytes.NewBuffer([]byte{})) + http.Register( + httpmock.REST("DELETE", "applications/CLIENTID/grant"), + httpmock.StatusStringResponse(204, "{}"), + ) r := bytes.NewReader([]byte(`{}`)) err := client.REST("github.com", "DELETE", "applications/CLIENTID/grant", r, nil) - eq(t, err, nil) + assert.NoError(t, err) +} + +func TestRESTWithFullURL(t *testing.T) { + http := &httpmock.Registry{} + client := NewClient(ReplaceTripper(http)) + + http.Register( + httpmock.REST("GET", "api/v3/user/repos"), + httpmock.StatusStringResponse(200, "{}")) + http.Register( + httpmock.REST("GET", "user/repos"), + httpmock.StatusStringResponse(200, "{}")) + + err := client.REST("example.com", "GET", "user/repos", nil, nil) + assert.NoError(t, err) + err = client.REST("example.com", "GET", "https://another.net/user/repos", nil, nil) + assert.NoError(t, err) + + assert.Equal(t, "example.com", http.Requests[0].URL.Hostname()) + assert.Equal(t, "another.net", http.Requests[1].URL.Hostname()) } func TestRESTError(t *testing.T) { @@ -105,66 +130,83 @@ func TestRESTError(t *testing.T) { } } -func Test_HasMinimumScopes(t *testing.T) { +func TestHandleHTTPError_GraphQL502(t *testing.T) { + req, err := http.NewRequest("GET", "https://api.github.com/user", nil) + if err != nil { + t.Fatal(err) + } + resp := &http.Response{ + Request: req, + StatusCode: 502, + Body: ioutil.NopCloser(bytes.NewBufferString(`{ "data": null, "errors": [{ "message": "Something went wrong" }] }`)), + Header: map[string][]string{"Content-Type": {"application/json"}}, + } + err = HandleHTTPError(resp) + if err == nil || err.Error() != "HTTP 502: Something went wrong (https://api.github.com/user)" { + t.Errorf("got error: %v", err) + } +} + +func TestHTTPError_ScopesSuggestion(t *testing.T) { + makeResponse := func(s int, u, haveScopes, needScopes string) *http.Response { + req, err := http.NewRequest("GET", u, nil) + if err != nil { + t.Fatal(err) + } + return &http.Response{ + Request: req, + StatusCode: s, + Body: ioutil.NopCloser(bytes.NewBufferString(`{}`)), + Header: map[string][]string{ + "Content-Type": {"application/json"}, + "X-Oauth-Scopes": {haveScopes}, + "X-Accepted-Oauth-Scopes": {needScopes}, + }, + } + } + tests := []struct { - name string - header string - wantErr string + name string + resp *http.Response + want string }{ { - name: "no scopes", - header: "", - wantErr: "", + name: "has necessary scopes", + resp: makeResponse(404, "https://api.github.com/gists", "repo, gist, read:org", "gist"), + want: ``, + }, + { + name: "normalizes scopes", + resp: makeResponse(404, "https://api.github.com/orgs/ORG/discussions", "admin:org, write:discussion", "read:org, read:discussion"), + want: ``, }, { - name: "default scopes", - header: "repo, read:org", - wantErr: "", + name: "no scopes on endpoint", + resp: makeResponse(404, "https://api.github.com/user", "repo", ""), + want: ``, }, { - name: "admin:org satisfies read:org", - header: "repo, admin:org", - wantErr: "", + name: "missing a scope", + resp: makeResponse(404, "https://api.github.com/gists", "repo, read:org", "gist, delete_repo"), + want: `This API operation needs the "gist" scope. To request it, run: gh auth refresh -h github.com -s gist`, }, { - name: "insufficient scope", - header: "repo", - wantErr: "missing required scope 'read:org'", + name: "server error", + resp: makeResponse(500, "https://api.github.com/gists", "repo", "gist"), + want: ``, }, { - name: "insufficient scopes", - header: "gist", - wantErr: "missing required scopes 'repo', 'read:org'", + name: "no scopes on token", + resp: makeResponse(404, "https://api.github.com/gists", "", "gist, delete_repo"), + want: ``, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - fakehttp := &httpmock.Registry{} - client := NewClient(ReplaceTripper(fakehttp)) - - fakehttp.Register(httpmock.REST("GET", ""), func(req *http.Request) (*http.Response, error) { - return &http.Response{ - Request: req, - StatusCode: 200, - Body: ioutil.NopCloser(&bytes.Buffer{}), - Header: map[string][]string{ - "X-Oauth-Scopes": {tt.header}, - }, - }, nil - }) - - err := client.HasMinimumScopes("github.com") - if tt.wantErr == "" { - if err != nil { - t.Errorf("error: %v", err) - } - return - } - if err.Error() != tt.wantErr { - t.Errorf("want %q, got %q", tt.wantErr, err.Error()) - + httpError := HandleHTTPError(tt.resp) + if got := httpError.(HTTPError).ScopesSuggestion(); got != tt.want { + t.Errorf("HTTPError.ScopesSuggestion() = %v, want %v", got, tt.want) } }) } - } diff --git a/api/export_pr.go b/api/export_pr.go new file mode 100644 index 00000000000..29a5c4a639a --- /dev/null +++ b/api/export_pr.go @@ -0,0 +1,112 @@ +package api + +import ( + "reflect" + "strings" +) + +func (issue *Issue) ExportData(fields []string) *map[string]interface{} { + v := reflect.ValueOf(issue).Elem() + data := map[string]interface{}{} + + for _, f := range fields { + switch f { + case "comments": + data[f] = issue.Comments.Nodes + case "assignees": + data[f] = issue.Assignees.Nodes + case "labels": + data[f] = issue.Labels.Nodes + case "projectCards": + data[f] = issue.ProjectCards.Nodes + default: + sf := fieldByName(v, f) + data[f] = sf.Interface() + } + } + + return &data +} + +func (pr *PullRequest) ExportData(fields []string) *map[string]interface{} { + v := reflect.ValueOf(pr).Elem() + data := map[string]interface{}{} + + for _, f := range fields { + switch f { + case "headRepository": + data[f] = pr.HeadRepository + case "statusCheckRollup": + if n := pr.StatusCheckRollup.Nodes; len(n) > 0 { + data[f] = n[0].Commit.StatusCheckRollup.Contexts.Nodes + } else { + data[f] = nil + } + case "commits": + commits := make([]interface{}, 0, len(pr.Commits.Nodes)) + for _, c := range pr.Commits.Nodes { + commit := c.Commit + authors := make([]interface{}, 0, len(commit.Authors.Nodes)) + for _, author := range commit.Authors.Nodes { + authors = append(authors, map[string]interface{}{ + "name": author.Name, + "email": author.Email, + "id": author.User.ID, + "login": author.User.Login, + }) + } + commits = append(commits, map[string]interface{}{ + "oid": commit.OID, + "messageHeadline": commit.MessageHeadline, + "messageBody": commit.MessageBody, + "committedDate": commit.CommittedDate, + "authoredDate": commit.AuthoredDate, + "authors": authors, + }) + } + data[f] = commits + case "comments": + data[f] = pr.Comments.Nodes + case "assignees": + data[f] = pr.Assignees.Nodes + case "labels": + data[f] = pr.Labels.Nodes + case "projectCards": + data[f] = pr.ProjectCards.Nodes + case "reviews": + data[f] = pr.Reviews.Nodes + case "files": + data[f] = pr.Files.Nodes + case "reviewRequests": + requests := make([]interface{}, 0, len(pr.ReviewRequests.Nodes)) + for _, req := range pr.ReviewRequests.Nodes { + r := req.RequestedReviewer + switch r.TypeName { + case "User": + requests = append(requests, map[string]string{ + "__typename": r.TypeName, + "login": r.Login, + }) + case "Team": + requests = append(requests, map[string]string{ + "__typename": r.TypeName, + "name": r.Name, + "slug": r.LoginOrSlug(), + }) + } + } + data[f] = &requests + default: + sf := fieldByName(v, f) + data[f] = sf.Interface() + } + } + + return &data +} + +func fieldByName(v reflect.Value, field string) reflect.Value { + return v.FieldByNameFunc(func(s string) bool { + return strings.EqualFold(field, s) + }) +} diff --git a/api/export_pr_test.go b/api/export_pr_test.go new file mode 100644 index 00000000000..dde730884bd --- /dev/null +++ b/api/export_pr_test.go @@ -0,0 +1,184 @@ +package api + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIssue_ExportData(t *testing.T) { + tests := []struct { + name string + fields []string + inputJSON string + outputJSON string + }{ + { + name: "simple", + fields: []string{"number", "title"}, + inputJSON: heredoc.Doc(` + { "title": "Bugs hugs", "number": 2345 } + `), + outputJSON: heredoc.Doc(` + { + "number": 2345, + "title": "Bugs hugs" + } + `), + }, + { + name: "milestone", + fields: []string{"number", "milestone"}, + inputJSON: heredoc.Doc(` + { "number": 2345, "milestone": {"title": "The next big thing"} } + `), + outputJSON: heredoc.Doc(` + { + "milestone": { + "number": 0, + "title": "The next big thing", + "description": "", + "dueOn": null + }, + "number": 2345 + } + `), + }, + { + name: "project cards", + fields: []string{"projectCards"}, + inputJSON: heredoc.Doc(` + { "projectCards": { "nodes": [ + { + "project": { "name": "Rewrite" }, + "column": { "name": "TO DO" } + } + ] } } + `), + outputJSON: heredoc.Doc(` + { + "projectCards": [ + { + "project": { + "name": "Rewrite" + }, + "column": { + "name": "TO DO" + } + } + ] + } + `), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var issue Issue + dec := json.NewDecoder(strings.NewReader(tt.inputJSON)) + require.NoError(t, dec.Decode(&issue)) + + exported := issue.ExportData(tt.fields) + + buf := bytes.Buffer{} + enc := json.NewEncoder(&buf) + enc.SetIndent("", "\t") + require.NoError(t, enc.Encode(exported)) + assert.Equal(t, tt.outputJSON, buf.String()) + }) + } +} + +func TestPullRequest_ExportData(t *testing.T) { + tests := []struct { + name string + fields []string + inputJSON string + outputJSON string + }{ + { + name: "simple", + fields: []string{"number", "title"}, + inputJSON: heredoc.Doc(` + { "title": "Bugs hugs", "number": 2345 } + `), + outputJSON: heredoc.Doc(` + { + "number": 2345, + "title": "Bugs hugs" + } + `), + }, + { + name: "milestone", + fields: []string{"number", "milestone"}, + inputJSON: heredoc.Doc(` + { "number": 2345, "milestone": {"title": "The next big thing"} } + `), + outputJSON: heredoc.Doc(` + { + "milestone": { + "number": 0, + "title": "The next big thing", + "description": "", + "dueOn": null + }, + "number": 2345 + } + `), + }, + { + name: "status checks", + fields: []string{"statusCheckRollup"}, + inputJSON: heredoc.Doc(` + { "statusCheckRollup": { "nodes": [ + { "commit": { "statusCheckRollup": { "contexts": { "nodes": [ + { + "__typename": "CheckRun", + "name": "mycheck", + "status": "COMPLETED", + "conclusion": "SUCCESS", + "startedAt": "2020-08-31T15:44:24+02:00", + "completedAt": "2020-08-31T15:45:24+02:00", + "detailsUrl": "http://example.com/details" + } + ] } } } } + ] } } + `), + outputJSON: heredoc.Doc(` + { + "statusCheckRollup": [ + { + "__typename": "CheckRun", + "name": "mycheck", + "status": "COMPLETED", + "conclusion": "SUCCESS", + "startedAt": "2020-08-31T15:44:24+02:00", + "completedAt": "2020-08-31T15:45:24+02:00", + "detailsUrl": "http://example.com/details" + } + ] + } + `), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var pr PullRequest + dec := json.NewDecoder(strings.NewReader(tt.inputJSON)) + require.NoError(t, dec.Decode(&pr)) + + exported := pr.ExportData(tt.fields) + + buf := bytes.Buffer{} + enc := json.NewEncoder(&buf) + enc.SetIndent("", "\t") + require.NoError(t, enc.Encode(exported)) + assert.Equal(t, tt.outputJSON, buf.String()) + }) + } +} diff --git a/api/export_repo.go b/api/export_repo.go new file mode 100644 index 00000000000..8d4e669ad96 --- /dev/null +++ b/api/export_repo.go @@ -0,0 +1,53 @@ +package api + +import ( + "reflect" +) + +func (repo *Repository) ExportData(fields []string) *map[string]interface{} { + v := reflect.ValueOf(repo).Elem() + data := map[string]interface{}{} + + for _, f := range fields { + switch f { + case "parent": + data[f] = miniRepoExport(repo.Parent) + case "templateRepository": + data[f] = miniRepoExport(repo.TemplateRepository) + case "languages": + data[f] = repo.Languages.Edges + case "labels": + data[f] = repo.Labels.Nodes + case "assignableUsers": + data[f] = repo.AssignableUsers.Nodes + case "mentionableUsers": + data[f] = repo.MentionableUsers.Nodes + case "milestones": + data[f] = repo.Milestones.Nodes + case "projects": + data[f] = repo.Projects.Nodes + case "repositoryTopics": + var topics []RepositoryTopic + for _, n := range repo.RepositoryTopics.Nodes { + topics = append(topics, n.Topic) + } + data[f] = topics + default: + sf := fieldByName(v, f) + data[f] = sf.Interface() + } + } + + return &data +} + +func miniRepoExport(r *Repository) map[string]interface{} { + if r == nil { + return nil + } + return map[string]interface{}{ + "id": r.ID, + "name": r.Name, + "owner": r.Owner, + } +} diff --git a/api/pull_request_test.go b/api/pull_request_test.go index 609a27e7c9c..2e4fa73b17e 100644 --- a/api/pull_request_test.go +++ b/api/pull_request_test.go @@ -3,12 +3,14 @@ package api import ( "encoding/json" "testing" + + "github.com/stretchr/testify/assert" ) func TestPullRequest_ChecksStatus(t *testing.T) { pr := PullRequest{} payload := ` - { "commits": { "nodes": [{ "commit": { + { "statusCheckRollup": { "nodes": [{ "commit": { "statusCheckRollup": { "contexts": { "nodes": [ @@ -31,11 +33,11 @@ func TestPullRequest_ChecksStatus(t *testing.T) { } }] } } ` err := json.Unmarshal([]byte(payload), &pr) - eq(t, err, nil) + assert.NoError(t, err) checks := pr.ChecksStatus() - eq(t, checks.Total, 8) - eq(t, checks.Pending, 3) - eq(t, checks.Failing, 3) - eq(t, checks.Passing, 2) + assert.Equal(t, 8, checks.Total) + assert.Equal(t, 3, checks.Pending) + assert.Equal(t, 3, checks.Failing) + assert.Equal(t, 2, checks.Passing) } diff --git a/api/queries_comments.go b/api/queries_comments.go new file mode 100644 index 00000000000..999c3903326 --- /dev/null +++ b/api/queries_comments.go @@ -0,0 +1,101 @@ +package api + +import ( + "context" + "time" + + "github.com/shurcooL/githubv4" + "github.com/shurcooL/graphql" +) + +type Comments struct { + Nodes []Comment + TotalCount int + PageInfo struct { + HasNextPage bool + EndCursor string + } +} + +type Comment struct { + Author Author `json:"author"` + AuthorAssociation string `json:"authorAssociation"` + Body string `json:"body"` + CreatedAt time.Time `json:"createdAt"` + IncludesCreatedEdit bool `json:"includesCreatedEdit"` + IsMinimized bool `json:"isMinimized"` + MinimizedReason string `json:"minimizedReason"` + ReactionGroups ReactionGroups `json:"reactionGroups"` +} + +type CommentCreateInput struct { + Body string + SubjectId string +} + +func CommentCreate(client *Client, repoHost string, params CommentCreateInput) (string, error) { + var mutation struct { + AddComment struct { + CommentEdge struct { + Node struct { + URL string + } + } + } `graphql:"addComment(input: $input)"` + } + + variables := map[string]interface{}{ + "input": githubv4.AddCommentInput{ + Body: githubv4.String(params.Body), + SubjectID: graphql.ID(params.SubjectId), + }, + } + + gql := graphQLClient(client.http, repoHost) + err := gql.MutateNamed(context.Background(), "CommentCreate", &mutation, variables) + if err != nil { + return "", err + } + + return mutation.AddComment.CommentEdge.Node.URL, nil +} + +func (c Comment) AuthorLogin() string { + return c.Author.Login +} + +func (c Comment) Association() string { + return c.AuthorAssociation +} + +func (c Comment) Content() string { + return c.Body +} + +func (c Comment) Created() time.Time { + return c.CreatedAt +} + +func (c Comment) HiddenReason() string { + return c.MinimizedReason +} + +func (c Comment) IsEdited() bool { + return c.IncludesCreatedEdit +} + +func (c Comment) IsHidden() bool { + return c.IsMinimized +} + +func (c Comment) Link() string { + return "" +} + +func (c Comment) Reactions() ReactionGroups { + return c.ReactionGroups +} + +func (c Comment) Status() string { + return "" +} diff --git a/api/queries_issue.go b/api/queries_issue.go index 690180feb9a..d09497569e6 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -2,13 +2,10 @@ package api import ( "context" - "encoding/base64" "fmt" - "strconv" - "strings" "time" - "github.com/cli/cli/internal/ghrepo" + "github.com/cli/cli/v2/internal/ghrepo" "github.com/shurcooL/githubv4" ) @@ -19,73 +16,100 @@ type IssuesPayload struct { } type IssuesAndTotalCount struct { - Issues []Issue - TotalCount int + Issues []Issue + TotalCount int + SearchCapped bool } type Issue struct { - ID string - Number int - Title string - URL string - State string - Closed bool - Body string - CreatedAt time.Time - UpdatedAt time.Time - Comments struct { - TotalCount int - } - Author struct { - Login string - } - Assignees struct { - Nodes []struct { - Login string - } - TotalCount int + ID string + Number int + Title string + URL string + State string + Closed bool + Body string + CreatedAt time.Time + UpdatedAt time.Time + ClosedAt *time.Time + Comments Comments + Author Author + Assignees Assignees + Labels Labels + ProjectCards ProjectCards + Milestone *Milestone + ReactionGroups ReactionGroups +} + +type Assignees struct { + Nodes []GitHubUser + TotalCount int +} + +func (a Assignees) Logins() []string { + logins := make([]string, len(a.Nodes)) + for i, a := range a.Nodes { + logins[i] = a.Login } - Labels struct { - Nodes []struct { - Name string - } - TotalCount int + return logins +} + +type Labels struct { + Nodes []IssueLabel + TotalCount int +} + +func (l Labels) Names() []string { + names := make([]string, len(l.Nodes)) + for i, l := range l.Nodes { + names[i] = l.Name } - ProjectCards struct { - Nodes []struct { - Project struct { - Name string - } - Column struct { - Name string - } - } - TotalCount int + return names +} + +type ProjectCards struct { + Nodes []struct { + Project struct { + Name string `json:"name"` + } `json:"project"` + Column struct { + Name string `json:"name"` + } `json:"column"` } - Milestone struct { - Title string + TotalCount int +} + +func (p ProjectCards) ProjectNames() []string { + names := make([]string, len(p.Nodes)) + for i, c := range p.Nodes { + names[i] = c.Project.Name } + return names +} + +type Milestone struct { + Number int `json:"number"` + Title string `json:"title"` + Description string `json:"description"` + DueOn *time.Time `json:"dueOn"` } type IssuesDisabledError struct { error } -const fragments = ` - fragment issue on Issue { - number - title - url - state - updatedAt - labels(first: 3) { - nodes { - name - } - totalCount - } - } -` +type Owner struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Login string `json:"login"` +} + +type Author struct { + // adding these breaks generated GraphQL requests + //ID string `json:"id,omitempty"` + //Name string `json:"name,omitempty"` + Login string `json:"login"` +} // IssueCreate creates an issue in a GitHub repository func IssueCreate(client *Client, repo *Repository, params map[string]interface{}) (*Issue, error) { @@ -122,7 +146,12 @@ func IssueCreate(client *Client, repo *Repository, params map[string]interface{} return &result.CreateIssue.Issue, nil } -func IssueStatus(client *Client, repo ghrepo.Interface, currentUsername string) (*IssuesPayload, error) { +type IssueStatusOptions struct { + Username string + Fields []string +} + +func IssueStatus(client *Client, repo ghrepo.Interface, options IssueStatusOptions) (*IssuesPayload, error) { type response struct { Repository struct { Assigned struct { @@ -141,6 +170,7 @@ func IssueStatus(client *Client, repo ghrepo.Interface, currentUsername string) } } + fragments := fmt.Sprintf("fragment issue on Issue{%s}", PullRequestGraphQL(options.Fields)) query := fragments + ` query IssueStatus($owner: String!, $repo: String!, $viewer: String!, $per_page: Int = 10) { repository(owner: $owner, name: $repo) { @@ -169,7 +199,7 @@ func IssueStatus(client *Client, repo ghrepo.Interface, currentUsername string) variables := map[string]interface{}{ "owner": repo.RepoOwner(), "repo": repo.RepoName(), - "viewer": currentUsername, + "viewer": options.Username, } var resp response @@ -200,126 +230,6 @@ func IssueStatus(client *Client, repo ghrepo.Interface, currentUsername string) return &payload, nil } -func IssueList(client *Client, repo ghrepo.Interface, state string, labels []string, assigneeString string, limit int, authorString string, mentionString string, milestoneString string) (*IssuesAndTotalCount, error) { - var states []string - switch state { - case "open", "": - states = []string{"OPEN"} - case "closed": - states = []string{"CLOSED"} - case "all": - states = []string{"OPEN", "CLOSED"} - default: - return nil, fmt.Errorf("invalid state: %s", state) - } - - query := fragments + ` - query IssueList($owner: String!, $repo: String!, $limit: Int, $endCursor: String, $states: [IssueState!] = OPEN, $labels: [String!], $assignee: String, $author: String, $mention: String, $milestone: String) { - repository(owner: $owner, name: $repo) { - hasIssuesEnabled - issues(first: $limit, after: $endCursor, orderBy: {field: CREATED_AT, direction: DESC}, states: $states, labels: $labels, filterBy: {assignee: $assignee, createdBy: $author, mentioned: $mention, milestone: $milestone}) { - totalCount - nodes { - ...issue - } - pageInfo { - hasNextPage - endCursor - } - } - } - } - ` - - variables := map[string]interface{}{ - "owner": repo.RepoOwner(), - "repo": repo.RepoName(), - "states": states, - } - if len(labels) > 0 { - variables["labels"] = labels - } - if assigneeString != "" { - variables["assignee"] = assigneeString - } - if authorString != "" { - variables["author"] = authorString - } - if mentionString != "" { - variables["mention"] = mentionString - } - - if milestoneString != "" { - var milestone *RepoMilestone - if milestoneNumber, err := strconv.ParseInt(milestoneString, 10, 32); err == nil { - milestone, err = MilestoneByNumber(client, repo, int32(milestoneNumber)) - if err != nil { - return nil, err - } - } else { - milestone, err = MilestoneByTitle(client, repo, "all", milestoneString) - if err != nil { - return nil, err - } - } - - milestoneRESTID, err := milestoneNodeIdToDatabaseId(milestone.ID) - if err != nil { - return nil, err - } - variables["milestone"] = milestoneRESTID - } - - type responseData struct { - Repository struct { - Issues struct { - TotalCount int - Nodes []Issue - PageInfo struct { - HasNextPage bool - EndCursor string - } - } - HasIssuesEnabled bool - } - } - - var issues []Issue - var totalCount int - pageLimit := min(limit, 100) - -loop: - for { - var response responseData - variables["limit"] = pageLimit - err := client.GraphQL(repo.RepoHost(), query, variables, &response) - if err != nil { - return nil, err - } - if !response.Repository.HasIssuesEnabled { - return nil, fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(repo)) - } - totalCount = response.Repository.Issues.TotalCount - - for _, issue := range response.Repository.Issues.Nodes { - issues = append(issues, issue) - if len(issues) == limit { - break loop - } - } - - if response.Repository.Issues.PageInfo.HasNextPage { - variables["endCursor"] = response.Repository.Issues.PageInfo.EndCursor - pageLimit = min(pageLimit, limit-len(issues)) - } else { - break - } - } - - res := IssuesAndTotalCount{Issues: issues, TotalCount: totalCount} - return &res, nil -} - func IssueByNumber(client *Client, repo ghrepo.Interface, number int) (*Issue, error) { type response struct { Repository struct { @@ -336,12 +246,28 @@ func IssueByNumber(client *Client, repo ghrepo.Interface, number int) (*Issue, e id title state - closed body author { login } - comments { + comments(last: 1) { + nodes { + author { + login + } + authorAssociation + body + createdAt + includesCreatedEdit + isMinimized + minimizedReason + reactionGroups { + content + users { + totalCount + } + } + } totalCount } number @@ -349,13 +275,18 @@ func IssueByNumber(client *Client, repo ghrepo.Interface, number int) (*Issue, e createdAt assignees(first: 100) { nodes { + id + name login } totalCount } labels(first: 100) { nodes { + id name + description + color } totalCount } @@ -370,8 +301,17 @@ func IssueByNumber(client *Client, repo ghrepo.Interface, number int) (*Issue, e } totalCount } - milestone{ + milestone { + number title + description + dueOn + } + reactionGroups { + content + users { + totalCount + } } } } @@ -443,19 +383,45 @@ func IssueReopen(client *Client, repo ghrepo.Interface, issue Issue) error { return err } -// milestoneNodeIdToDatabaseId extracts the REST Database ID from the GraphQL Node ID -// This conversion is necessary since the GraphQL API requires the use of the milestone's database ID -// for querying the related issues. -func milestoneNodeIdToDatabaseId(nodeId string) (string, error) { - // The Node ID is Base64 obfuscated, with an underlying pattern: - // "09:Milestone12345", where "12345" is the database ID - decoded, err := base64.StdEncoding.DecodeString(nodeId) - if err != nil { - return "", err +func IssueDelete(client *Client, repo ghrepo.Interface, issue Issue) error { + var mutation struct { + DeleteIssue struct { + Repository struct { + ID githubv4.ID + } + } `graphql:"deleteIssue(input: $input)"` } - splitted := strings.Split(string(decoded), "Milestone") - if len(splitted) != 2 { - return "", fmt.Errorf("couldn't get database id from node id") + + variables := map[string]interface{}{ + "input": githubv4.DeleteIssueInput{ + IssueID: issue.ID, + }, + } + + gql := graphQLClient(client.http, repo.RepoHost()) + err := gql.MutateNamed(context.Background(), "IssueDelete", &mutation, variables) + + return err +} + +func IssueUpdate(client *Client, repo ghrepo.Interface, params githubv4.UpdateIssueInput) error { + var mutation struct { + UpdateIssue struct { + Issue struct { + ID string + } + } `graphql:"updateIssue(input: $input)"` } - return splitted[1], nil + variables := map[string]interface{}{"input": params} + gql := graphQLClient(client.http, repo.RepoHost()) + err := gql.MutateNamed(context.Background(), "IssueUpdate", &mutation, variables) + return err +} + +func (i Issue) Link() string { + return i.URL +} + +func (i Issue) Identifier() string { + return i.ID } diff --git a/api/queries_issue_test.go b/api/queries_issue_test.go deleted file mode 100644 index 83dee55b795..00000000000 --- a/api/queries_issue_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package api - -import ( - "bytes" - "encoding/json" - "io/ioutil" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/cli/cli/internal/ghrepo" - "github.com/cli/cli/pkg/httpmock" -) - -func TestIssueList(t *testing.T) { - http := &httpmock.Registry{} - client := NewClient(ReplaceTripper(http)) - - http.StubResponse(200, bytes.NewBufferString(` - { "data": { "repository": { - "hasIssuesEnabled": true, - "issues": { - "nodes": [], - "pageInfo": { - "hasNextPage": true, - "endCursor": "ENDCURSOR" - } - } - } } } - `)) - http.StubResponse(200, bytes.NewBufferString(` - { "data": { "repository": { - "hasIssuesEnabled": true, - "issues": { - "nodes": [], - "pageInfo": { - "hasNextPage": false, - "endCursor": "ENDCURSOR" - } - } - } } } - `)) - - repo, _ := ghrepo.FromFullName("OWNER/REPO") - _, err := IssueList(client, repo, "open", []string{}, "", 251, "", "", "") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - if len(http.Requests) != 2 { - t.Fatalf("expected 2 HTTP requests, seen %d", len(http.Requests)) - } - var reqBody struct { - Query string - Variables map[string]interface{} - } - - bodyBytes, _ := ioutil.ReadAll(http.Requests[0].Body) - _ = json.Unmarshal(bodyBytes, &reqBody) - if reqLimit := reqBody.Variables["limit"].(float64); reqLimit != 100 { - t.Errorf("expected 100, got %v", reqLimit) - } - if _, cursorPresent := reqBody.Variables["endCursor"]; cursorPresent { - t.Error("did not expect first request to pass 'endCursor'") - } - - bodyBytes, _ = ioutil.ReadAll(http.Requests[1].Body) - _ = json.Unmarshal(bodyBytes, &reqBody) - if endCursor := reqBody.Variables["endCursor"].(string); endCursor != "ENDCURSOR" { - t.Errorf("expected %q, got %q", "ENDCURSOR", endCursor) - } -} - -func TestIssueList_pagination(t *testing.T) { - http := &httpmock.Registry{} - client := NewClient(ReplaceTripper(http)) - - http.StubResponse(200, bytes.NewBufferString(` - { "data": { "repository": { - "hasIssuesEnabled": true, - "issues": { - "nodes": [ - { - "title": "issue1", - "labels": { "nodes": [ { "name": "bug" } ], "totalCount": 1 }, - "assignees": { "nodes": [ { "login": "user1" } ], "totalCount": 1 } - } - ], - "pageInfo": { - "hasNextPage": true, - "endCursor": "ENDCURSOR" - }, - "totalCount": 2 - } - } } } - `)) - http.StubResponse(200, bytes.NewBufferString(` - { "data": { "repository": { - "hasIssuesEnabled": true, - "issues": { - "nodes": [ - { - "title": "issue2", - "labels": { "nodes": [ { "name": "enhancement" } ], "totalCount": 1 }, - "assignees": { "nodes": [ { "login": "user2" } ], "totalCount": 1 } - } - ], - "pageInfo": { - "hasNextPage": false, - "endCursor": "ENDCURSOR" - }, - "totalCount": 2 - } - } } } - `)) - - repo := ghrepo.New("OWNER", "REPO") - res, err := IssueList(client, repo, "", nil, "", 0, "", "", "") - if err != nil { - t.Fatalf("IssueList() error = %v", err) - } - - assert.Equal(t, 2, res.TotalCount) - assert.Equal(t, 2, len(res.Issues)) - - getLabels := func(i Issue) []string { - var labels []string - for _, l := range i.Labels.Nodes { - labels = append(labels, l.Name) - } - return labels - } - getAssignees := func(i Issue) []string { - var logins []string - for _, u := range i.Assignees.Nodes { - logins = append(logins, u.Login) - } - return logins - } - - assert.Equal(t, []string{"bug"}, getLabels(res.Issues[0])) - assert.Equal(t, []string{"user1"}, getAssignees(res.Issues[0])) - assert.Equal(t, []string{"enhancement"}, getLabels(res.Issues[1])) - assert.Equal(t, []string{"user2"}, getAssignees(res.Issues[1])) -} diff --git a/api/queries_org.go b/api/queries_org.go index a47963aead7..6f6dda100f1 100644 --- a/api/queries_org.go +++ b/api/queries_org.go @@ -3,7 +3,7 @@ package api import ( "context" - "github.com/cli/cli/internal/ghrepo" + "github.com/cli/cli/v2/internal/ghrepo" "github.com/shurcooL/githubv4" ) diff --git a/api/queries_pr.go b/api/queries_pr.go index a95feab28c5..744b4c9e291 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -2,32 +2,18 @@ package api import ( "context" - "errors" "fmt" - "io" "net/http" - "sort" "strings" "time" - "github.com/cli/cli/internal/ghinstance" - "github.com/cli/cli/internal/ghrepo" + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/set" "github.com/shurcooL/githubv4" + "golang.org/x/sync/errgroup" ) -type PullRequestReviewState int - -const ( - ReviewApprove PullRequestReviewState = iota - ReviewRequestChanges - ReviewComment -) - -type PullRequestReviewInput struct { - Body string - State PullRequestReviewState -} - type PullRequestsPayload struct { ViewerCreated PullRequestAndTotalCount ReviewRequested PullRequestAndTotalCount @@ -38,113 +24,160 @@ type PullRequestsPayload struct { type PullRequestAndTotalCount struct { TotalCount int PullRequests []PullRequest + SearchCapped bool } type PullRequest struct { - ID string - Number int - Title string - State string - Closed bool - URL string - BaseRefName string - HeadRefName string - Body string - Mergeable string - - Author struct { - Login string - } - HeadRepositoryOwner struct { - Login string - } - HeadRepository struct { - Name string - DefaultBranchRef struct { - Name string - } - } + ID string + Number int + Title string + State string + Closed bool + URL string + BaseRefName string + HeadRefName string + Body string + Mergeable string + Additions int + Deletions int + ChangedFiles int + MergeStateStatus string + CreatedAt time.Time + UpdatedAt time.Time + ClosedAt *time.Time + MergedAt *time.Time + + MergeCommit *Commit + PotentialMergeCommit *Commit + + Files struct { + Nodes []PullRequestFile + } + + Author Author + MergedBy *Author + HeadRepositoryOwner Owner + HeadRepository *PRRepository IsCrossRepository bool IsDraft bool MaintainerCanModify bool + BaseRef struct { + BranchProtectionRule struct { + RequiresStrictStatusChecks bool + } + } + ReviewDecision string Commits struct { TotalCount int - Nodes []struct { + Nodes []PullRequestCommit + } + StatusCheckRollup struct { + Nodes []struct { Commit struct { - Oid string StatusCheckRollup struct { Contexts struct { Nodes []struct { - Name string - Context string - State string - Status string - Conclusion string - StartedAt time.Time - CompletedAt time.Time - DetailsURL string - TargetURL string + TypeName string `json:"__typename"` + Name string `json:"name"` + Context string `json:"context,omitempty"` + State string `json:"state,omitempty"` + Status string `json:"status"` + Conclusion string `json:"conclusion"` + StartedAt time.Time `json:"startedAt"` + CompletedAt time.Time `json:"completedAt"` + DetailsURL string `json:"detailsUrl"` + TargetURL string `json:"targetUrl,omitempty"` + } + PageInfo struct { + HasNextPage bool + EndCursor string } } } } } } - ReviewRequests struct { - Nodes []struct { - RequestedReviewer struct { - TypeName string `json:"__typename"` - Login string - Name string - } - } - TotalCount int - } - Reviews struct { - Nodes []struct { - Author struct { - Login string - } - State string - } - } - Assignees struct { - Nodes []struct { - Login string - } - TotalCount int - } - Labels struct { - Nodes []struct { - Name string - } - TotalCount int - } - ProjectCards struct { + + Assignees Assignees + Labels Labels + ProjectCards ProjectCards + Milestone *Milestone + Comments Comments + ReactionGroups ReactionGroups + Reviews PullRequestReviews + ReviewRequests ReviewRequests +} + +type PRRepository struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// Commit loads just the commit SHA and nothing else +type Commit struct { + OID string `json:"oid"` +} + +type PullRequestCommit struct { + Commit PullRequestCommitCommit +} + +// PullRequestCommitCommit contains full information about a commit +type PullRequestCommitCommit struct { + OID string `json:"oid"` + Authors struct { Nodes []struct { - Project struct { - Name string - } - Column struct { - Name string - } + Name string + Email string + User GitHubUser } - TotalCount int } - Milestone struct { - Title string + MessageHeadline string + MessageBody string + CommittedDate time.Time + AuthoredDate time.Time +} + +type PullRequestFile struct { + Path string `json:"path"` + Additions int `json:"additions"` + Deletions int `json:"deletions"` +} + +type ReviewRequests struct { + Nodes []struct { + RequestedReviewer RequestedReviewer } } -type NotFoundError struct { - error +type RequestedReviewer struct { + TypeName string `json:"__typename"` + Login string `json:"login"` + Name string `json:"name"` + Slug string `json:"slug"` + Organization struct { + Login string `json:"login"` + } `json:"organization"` } -func (err *NotFoundError) Unwrap() error { - return err.error +func (r RequestedReviewer) LoginOrSlug() string { + if r.TypeName == teamTypeName { + return fmt.Sprintf("%s/%s", r.Organization.Login, r.Slug) + } + return r.Login +} + +const teamTypeName = "Team" + +func (r ReviewRequests) Logins() []string { + logins := make([]string, len(r.Nodes)) + for i, r := range r.Nodes { + logins[i] = r.RequestedReviewer.LoginOrSlug() + } + return logins } func (pr PullRequest) HeadLabel() string { @@ -154,20 +187,24 @@ func (pr PullRequest) HeadLabel() string { return pr.HeadRefName } +func (pr PullRequest) Link() string { + return pr.URL +} + +func (pr PullRequest) Identifier() string { + return pr.ID +} + +func (pr PullRequest) IsOpen() bool { + return pr.State == "OPEN" +} + type PullRequestReviewStatus struct { ChangesRequested bool Approved bool ReviewRequired bool } -type PullRequestMergeMethod int - -const ( - PullRequestMergeMethodMerge PullRequestMergeMethod = iota - PullRequestMergeMethodRebase - PullRequestMergeMethodSquash -) - func (pr *PullRequest) ReviewStatus() PullRequestReviewStatus { var status PullRequestReviewStatus switch pr.ReviewDecision { @@ -189,10 +226,10 @@ type PullRequestChecksStatus struct { } func (pr *PullRequest) ChecksStatus() (summary PullRequestChecksStatus) { - if len(pr.Commits.Nodes) == 0 { + if len(pr.StatusCheckRollup.Nodes) == 0 { return } - commit := pr.Commits.Nodes[0].Commit + commit := pr.StatusCheckRollup.Nodes[0].Commit for _, c := range commit.StatusCheckRollup.Contexts.Nodes { state := c.State // StatusContext if state == "" { @@ -208,49 +245,37 @@ func (pr *PullRequest) ChecksStatus() (summary PullRequestChecksStatus) { summary.Passing++ case "ERROR", "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED": summary.Failing++ - case "EXPECTED", "REQUESTED", "QUEUED", "PENDING", "IN_PROGRESS", "STALE": + default: // "EXPECTED", "REQUESTED", "WAITING", "QUEUED", "PENDING", "IN_PROGRESS", "STALE" summary.Pending++ - default: - panic(fmt.Errorf("unsupported status: %q", state)) } summary.Total++ } return } -func (c Client) PullRequestDiff(baseRepo ghrepo.Interface, prNumber int) (io.ReadCloser, error) { - url := fmt.Sprintf("%srepos/%s/pulls/%d", - ghinstance.RESTPrefix(baseRepo.RepoHost()), ghrepo.FullName(baseRepo), prNumber) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } - - req.Header.Set("Accept", "application/vnd.github.v3.diff; charset=utf-8") - - resp, err := c.http.Do(req) - if err != nil { - return nil, err - } - - if resp.StatusCode == 404 { - return nil, &NotFoundError{errors.New("pull request not found")} - } else if resp.StatusCode != 200 { - return nil, HandleHTTPError(resp) +func (pr *PullRequest) DisplayableReviews() PullRequestReviews { + published := []PullRequestReview{} + for _, prr := range pr.Reviews.Nodes { + //Dont display pending reviews + //Dont display commenting reviews without top level comment body + if prr.State != "PENDING" && !(prr.State == "COMMENTED" && prr.Body == "") { + published = append(published, prr) + } } - - return resp.Body, nil + return PullRequestReviews{Nodes: published, TotalCount: len(published)} } type pullRequestFeature struct { - HasReviewDecision bool - HasStatusCheckRollup bool + HasReviewDecision bool + HasStatusCheckRollup bool + HasBranchProtectionRule bool } func determinePullRequestFeatures(httpClient *http.Client, hostname string) (prFeatures pullRequestFeature, err error) { if !ghinstance.IsEnterprise(hostname) { prFeatures.HasReviewDecision = true prFeatures.HasStatusCheckRollup = true + prFeatures.HasBranchProtectionRule = true return } @@ -267,8 +292,26 @@ func determinePullRequestFeatures(httpClient *http.Client, hostname string) (prF } `graphql:"Commit: __type(name: \"Commit\")"` } + // needs to be a separate query because the backend only supports 2 `__type` expressions in one query + var featureDetection2 struct { + Ref struct { + Fields []struct { + Name string + } `graphql:"fields(includeDeprecated: true)"` + } `graphql:"Ref: __type(name: \"Ref\")"` + } + v4 := graphQLClient(httpClient, hostname) - err = v4.QueryNamed(context.Background(), "PullRequest_fields", &featureDetection, nil) + + g := new(errgroup.Group) + g.Go(func() error { + return v4.QueryNamed(context.Background(), "PullRequest_fields", &featureDetection, nil) + }) + g.Go(func() error { + return v4.QueryNamed(context.Background(), "PullRequest_fields2", &featureDetection2, nil) + }) + + err = g.Wait() if err != nil { return } @@ -285,10 +328,23 @@ func determinePullRequestFeatures(httpClient *http.Client, hostname string) (prF prFeatures.HasStatusCheckRollup = true } } + for _, field := range featureDetection2.Ref.Fields { + switch field.Name { + case "branchProtectionRule": + prFeatures.HasBranchProtectionRule = true + } + } return } -func PullRequests(client *Client, repo ghrepo.Interface, currentPRNumber int, currentPRHeadRef, currentUsername string) (*PullRequestsPayload, error) { +type StatusOptions struct { + CurrentPR int + HeadRef string + Username string + Fields []string +} + +func PullRequestStatus(client *Client, repo ghrepo.Interface, options StatusOptions) (*PullRequestsPayload, error) { type edges struct { TotalCount int Edges []struct { @@ -308,66 +364,28 @@ func PullRequests(client *Client, repo ghrepo.Interface, currentPRNumber int, cu ReviewRequested edges } - cachedClient := makeCachedClient(client.http, time.Hour*24) - prFeatures, err := determinePullRequestFeatures(cachedClient, repo.RepoHost()) - if err != nil { - return nil, err - } - - var reviewsFragment string - if prFeatures.HasReviewDecision { - reviewsFragment = "reviewDecision" - } - - var statusesFragment string - if prFeatures.HasStatusCheckRollup { - statusesFragment = ` - commits(last: 1) { - nodes { - commit { - statusCheckRollup { - contexts(last: 100) { - nodes { - ...on StatusContext { - state - } - ...on CheckRun { - conclusion - status - } - } - } - } - } - } - } - ` - } - - fragments := fmt.Sprintf(` - fragment pr on PullRequest { - number - title - state - url - headRefName - headRepositoryOwner { - login + var fragments string + if len(options.Fields) > 0 { + fields := set.NewStringSet() + fields.AddValues(options.Fields) + // these are always necessary to find the PR for the current branch + fields.AddValues([]string{"isCrossRepository", "headRepositoryOwner", "headRefName"}) + gr := PullRequestGraphQL(fields.ToSlice()) + fragments = fmt.Sprintf("fragment pr on PullRequest{%s}fragment prWithReviews on PullRequest{...pr}", gr) + } else { + var err error + fragments, err = pullRequestFragment(client.http, repo.RepoHost()) + if err != nil { + return nil, err } - isCrossRepository - isDraft - %s - } - fragment prWithReviews on PullRequest { - ...pr - %s } - `, statusesFragment, reviewsFragment) queryPrefix := ` query PullRequestStatus($owner: String!, $repo: String!, $headRefName: String!, $viewerQuery: String!, $reviewerQuery: String!, $per_page: Int = 10) { repository(owner: $owner, name: $repo) { - defaultBranchRef { name } + defaultBranchRef { + name + } pullRequests(headRefName: $headRefName, first: $per_page, orderBy: { field: CREATED_AT, direction: DESC }) { totalCount edges { @@ -378,11 +396,13 @@ func PullRequests(client *Client, repo ghrepo.Interface, currentPRNumber int, cu } } ` - if currentPRNumber > 0 { + if options.CurrentPR > 0 { queryPrefix = ` query PullRequestStatus($owner: String!, $repo: String!, $number: Int!, $viewerQuery: String!, $reviewerQuery: String!, $per_page: Int = 10) { repository(owner: $owner, name: $repo) { - defaultBranchRef { name } + defaultBranchRef { + name + } pullRequest(number: $number) { ...prWithReviews } @@ -410,7 +430,9 @@ func PullRequests(client *Client, repo ghrepo.Interface, currentPRNumber int, cu } ` + currentUsername := options.Username if currentUsername == "@me" && ghinstance.IsEnterprise(repo.RepoHost()) { + var err error currentUsername, err = CurrentLoginName(client, repo.RepoHost()) if err != nil { return nil, err @@ -420,6 +442,7 @@ func PullRequests(client *Client, repo ghrepo.Interface, currentPRNumber int, cu viewerQuery := fmt.Sprintf("repo:%s state:open is:pr author:%s", ghrepo.FullName(repo), currentUsername) reviewerQuery := fmt.Sprintf("repo:%s state:open review-requested:%s", ghrepo.FullName(repo), currentUsername) + currentPRHeadRef := options.HeadRef branchWithoutOwner := currentPRHeadRef if idx := strings.Index(currentPRHeadRef, ":"); idx >= 0 { branchWithoutOwner = currentPRHeadRef[idx+1:] @@ -431,11 +454,11 @@ func PullRequests(client *Client, repo ghrepo.Interface, currentPRNumber int, cu "owner": repo.RepoOwner(), "repo": repo.RepoName(), "headRefName": branchWithoutOwner, - "number": currentPRNumber, + "number": options.CurrentPR, } var resp response - err = client.GraphQL(repo.RepoHost(), query, variables, &resp) + err := client.GraphQL(repo.RepoHost(), query, variables, &resp) if err != nil { return nil, err } @@ -476,282 +499,34 @@ func PullRequests(client *Client, repo ghrepo.Interface, currentPRNumber int, cu return &payload, nil } -func prCommitsFragment(httpClient *http.Client, hostname string) (string, error) { - cachedClient := makeCachedClient(httpClient, time.Hour*24) - if prFeatures, err := determinePullRequestFeatures(cachedClient, hostname); err != nil { - return "", err - } else if !prFeatures.HasStatusCheckRollup { - return "", nil - } - - return ` - commits(last: 1) { - totalCount - nodes { - commit { - oid - statusCheckRollup { - contexts(last: 100) { - nodes { - ...on StatusContext { - context - state - targetUrl - } - ...on CheckRun { - name - status - conclusion - startedAt - completedAt - detailsUrl - } - } - } - } - } - } - } - `, nil -} - -func PullRequestByNumber(client *Client, repo ghrepo.Interface, number int) (*PullRequest, error) { - type response struct { - Repository struct { - PullRequest PullRequest - } - } - - statusesFragment, err := prCommitsFragment(client.http, repo.RepoHost()) - if err != nil { - return nil, err - } - - query := ` - query PullRequestByNumber($owner: String!, $repo: String!, $pr_number: Int!) { - repository(owner: $owner, name: $repo) { - pullRequest(number: $pr_number) { - id - url - number - title - state - closed - body - mergeable - author { - login - } - ` + statusesFragment + ` - baseRefName - headRefName - headRepositoryOwner { - login - } - headRepository { - name - } - isCrossRepository - isDraft - maintainerCanModify - reviewRequests(first: 100) { - nodes { - requestedReviewer { - __typename - ...on User { - login - } - ...on Team { - name - } - } - } - totalCount - } - reviews(last: 100) { - nodes { - author { - login - } - state - } - totalCount - } - assignees(first: 100) { - nodes { - login - } - totalCount - } - labels(first: 100) { - nodes { - name - } - totalCount - } - projectCards(first: 100) { - nodes { - project { - name - } - column { - name - } - } - totalCount - } - milestone{ - title - } - } - } - }` - - variables := map[string]interface{}{ - "owner": repo.RepoOwner(), - "repo": repo.RepoName(), - "pr_number": number, - } - - var resp response - err = client.GraphQL(repo.RepoHost(), query, variables, &resp) - if err != nil { - return nil, err - } - - return &resp.Repository.PullRequest, nil -} - -func PullRequestForBranch(client *Client, repo ghrepo.Interface, baseBranch, headBranch string, stateFilters []string) (*PullRequest, error) { - type response struct { - Repository struct { - PullRequests struct { - Nodes []PullRequest - } - } - } - - statusesFragment, err := prCommitsFragment(client.http, repo.RepoHost()) +func pullRequestFragment(httpClient *http.Client, hostname string) (string, error) { + cachedClient := NewCachedClient(httpClient, time.Hour*24) + prFeatures, err := determinePullRequestFeatures(cachedClient, hostname) if err != nil { - return nil, err + return "", err } - query := ` - query PullRequestForBranch($owner: String!, $repo: String!, $headRefName: String!, $states: [PullRequestState!]) { - repository(owner: $owner, name: $repo) { - pullRequests(headRefName: $headRefName, states: $states, first: 30) { - nodes { - id - number - title - state - body - mergeable - author { - login - } - ` + statusesFragment + ` - url - baseRefName - headRefName - headRepositoryOwner { - login - } - headRepository { - name - } - isCrossRepository - isDraft - maintainerCanModify - reviewRequests(first: 100) { - nodes { - requestedReviewer { - __typename - ...on User { - login - } - ...on Team { - name - } - } - } - totalCount - } - reviews(last: 100) { - nodes { - author { - login - } - state - } - totalCount - } - assignees(first: 100) { - nodes { - login - } - totalCount - } - labels(first: 100) { - nodes { - name - } - totalCount - } - projectCards(first: 100) { - nodes { - project { - name - } - column { - name - } - } - totalCount - } - milestone{ - title - } - } - } - } - }` - - branchWithoutOwner := headBranch - if idx := strings.Index(headBranch, ":"); idx >= 0 { - branchWithoutOwner = headBranch[idx+1:] + fields := []string{ + "number", "title", "state", "url", "isDraft", "isCrossRepository", + "headRefName", "headRepositoryOwner", "mergeStateStatus", } - - variables := map[string]interface{}{ - "owner": repo.RepoOwner(), - "repo": repo.RepoName(), - "headRefName": branchWithoutOwner, - "states": stateFilters, + if prFeatures.HasStatusCheckRollup { + fields = append(fields, "statusCheckRollup") } - - var resp response - err = client.GraphQL(repo.RepoHost(), query, variables, &resp) - if err != nil { - return nil, err + if prFeatures.HasBranchProtectionRule { + fields = append(fields, "requiresStrictStatusChecks") } - prs := resp.Repository.PullRequests.Nodes - sortPullRequestsByState(prs) - - for _, pr := range prs { - if pr.HeadLabel() == headBranch && (baseBranch == "" || pr.BaseRefName == baseBranch) { - return &pr, nil - } + var reviewFields []string + if prFeatures.HasReviewDecision { + reviewFields = append(reviewFields, "reviewDecision") } - return nil, &NotFoundError{fmt.Errorf("no pull requests found for branch %q", headBranch)} -} - -// sortPullRequestsByState sorts a PullRequest slice by open-first -func sortPullRequestsByState(prs []PullRequest) { - sort.SliceStable(prs, func(a, b int) bool { - return prs[a].State == "OPEN" - }) + fragments := fmt.Sprintf(` + fragment pr on PullRequest {%s} + fragment prWithReviews on PullRequest {...pr,%s} + `, PullRequestGraphQL(fields), PullRequestGraphQL(reviewFields)) + return fragments, nil } // CreatePullRequest creates a pull request in a GitHub repository @@ -771,7 +546,7 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter } for key, val := range params { switch key { - case "title", "body", "draft", "baseRefName", "headRefName": + case "title", "body", "draft", "baseRefName", "headRefName", "maintainerCanModify": inputParams[key] = val } } @@ -826,6 +601,7 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter reviewParams["teamIds"] = ids } + //TODO: How much work to extract this into own method and use for create and edit? if len(reviewParams) > 0 { reviewQuery := ` mutation PullRequestCreateRequestReviews($input: RequestReviewsInput!) { @@ -845,211 +621,43 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter return pr, nil } -func isBlank(v interface{}) bool { - switch vv := v.(type) { - case string: - return vv == "" - case []string: - return len(vv) == 0 - default: - return true - } -} - -func AddReview(client *Client, repo ghrepo.Interface, pr *PullRequest, input *PullRequestReviewInput) error { +func UpdatePullRequest(client *Client, repo ghrepo.Interface, params githubv4.UpdatePullRequestInput) error { var mutation struct { - AddPullRequestReview struct { - ClientMutationID string - } `graphql:"addPullRequestReview(input:$input)"` - } - - state := githubv4.PullRequestReviewEventComment - switch input.State { - case ReviewApprove: - state = githubv4.PullRequestReviewEventApprove - case ReviewRequestChanges: - state = githubv4.PullRequestReviewEventRequestChanges - } - - body := githubv4.String(input.Body) - variables := map[string]interface{}{ - "input": githubv4.AddPullRequestReviewInput{ - PullRequestID: pr.ID, - Event: &state, - Body: &body, - }, + UpdatePullRequest struct { + PullRequest struct { + ID string + } + } `graphql:"updatePullRequest(input: $input)"` } - + variables := map[string]interface{}{"input": params} gql := graphQLClient(client.http, repo.RepoHost()) - return gql.MutateNamed(context.Background(), "PullRequestReviewAdd", &mutation, variables) + err := gql.MutateNamed(context.Background(), "PullRequestUpdate", &mutation, variables) + return err } -func PullRequestList(client *Client, repo ghrepo.Interface, vars map[string]interface{}, limit int) (*PullRequestAndTotalCount, error) { - type prBlock struct { - Edges []struct { - Node PullRequest - } - PageInfo struct { - HasNextPage bool - EndCursor string - } - TotalCount int - IssueCount int - } - type response struct { - Repository struct { - PullRequests prBlock - } - Search prBlock - } - - fragment := ` - fragment pr on PullRequest { - number - title - state - url - headRefName - headRepositoryOwner { - login - } - isCrossRepository - isDraft - } - ` - - // If assignee wasn't specified, use `Repository.pullRequest` for ability to - // query by multiple labels - query := fragment + ` - query PullRequestList( - $owner: String!, - $repo: String!, - $limit: Int!, - $endCursor: String, - $baseBranch: String, - $labels: [String!], - $state: [PullRequestState!] = OPEN - ) { - repository(owner: $owner, name: $repo) { - pullRequests( - states: $state, - baseRefName: $baseBranch, - labels: $labels, - first: $limit, - after: $endCursor, - orderBy: {field: CREATED_AT, direction: DESC} - ) { - totalCount - edges { - node { - ...pr - } - } - pageInfo { - hasNextPage - endCursor - } - } - } - }` - - var check = make(map[int]struct{}) - var prs []PullRequest - pageLimit := min(limit, 100) - variables := map[string]interface{}{} - res := PullRequestAndTotalCount{} - - // If assignee was specified, use the `search` API rather than - // `Repository.pullRequests`, but this mode doesn't support multiple labels - if assignee, ok := vars["assignee"].(string); ok { - query = fragment + ` - query PullRequestList( - $q: String!, - $limit: Int!, - $endCursor: String, - ) { - search(query: $q, type: ISSUE, first: $limit, after: $endCursor) { - issueCount - edges { - node { - ...pr - } - } - pageInfo { - hasNextPage - endCursor - } - } - }` - search := []string{ - fmt.Sprintf("repo:%s/%s", repo.RepoOwner(), repo.RepoName()), - fmt.Sprintf("assignee:%s", assignee), - "is:pr", - "sort:created-desc", - } - if states, ok := vars["state"].([]string); ok && len(states) == 1 { - switch states[0] { - case "OPEN": - search = append(search, "state:open") - case "CLOSED": - search = append(search, "state:closed") - case "MERGED": - search = append(search, "is:merged") - } - } - if labels, ok := vars["labels"].([]string); ok && len(labels) > 0 { - if len(labels) > 1 { - return nil, fmt.Errorf("multiple labels with --assignee are not supported") +func UpdatePullRequestReviews(client *Client, repo ghrepo.Interface, params githubv4.RequestReviewsInput) error { + var mutation struct { + RequestReviews struct { + PullRequest struct { + ID string } - search = append(search, fmt.Sprintf(`label:"%s"`, labels[0])) - } - if baseBranch, ok := vars["baseBranch"].(string); ok { - search = append(search, fmt.Sprintf(`base:"%s"`, baseBranch)) - } - variables["q"] = strings.Join(search, " ") - } else { - variables["owner"] = repo.RepoOwner() - variables["repo"] = repo.RepoName() - for name, val := range vars { - variables[name] = val - } + } `graphql:"requestReviews(input: $input)"` } -loop: - for { - variables["limit"] = pageLimit - var data response - err := client.GraphQL(repo.RepoHost(), query, variables, &data) - if err != nil { - return nil, err - } - prData := data.Repository.PullRequests - res.TotalCount = prData.TotalCount - if _, ok := variables["q"]; ok { - prData = data.Search - res.TotalCount = prData.IssueCount - } - - for _, edge := range prData.Edges { - if _, exists := check[edge.Node.Number]; exists { - continue - } - - prs = append(prs, edge.Node) - check[edge.Node.Number] = struct{}{} - if len(prs) == limit { - break loop - } - } + variables := map[string]interface{}{"input": params} + gql := graphQLClient(client.http, repo.RepoHost()) + err := gql.MutateNamed(context.Background(), "PullRequestUpdateRequestReviews", &mutation, variables) + return err +} - if prData.PageInfo.HasNextPage { - variables["endCursor"] = prData.PageInfo.EndCursor - pageLimit = min(pageLimit, limit-len(prs)) - } else { - break - } +func isBlank(v interface{}) bool { + switch vv := v.(type) { + case string: + return vv == "" + case []string: + return len(vv) == 0 + default: + return true } - res.PullRequests = prs - return &res, nil } func PullRequestClose(client *Client, repo ghrepo.Interface, pr *PullRequest) error { @@ -1094,43 +702,6 @@ func PullRequestReopen(client *Client, repo ghrepo.Interface, pr *PullRequest) e return err } -func PullRequestMerge(client *Client, repo ghrepo.Interface, pr *PullRequest, m PullRequestMergeMethod) error { - mergeMethod := githubv4.PullRequestMergeMethodMerge - switch m { - case PullRequestMergeMethodRebase: - mergeMethod = githubv4.PullRequestMergeMethodRebase - case PullRequestMergeMethodSquash: - mergeMethod = githubv4.PullRequestMergeMethodSquash - } - - var mutation struct { - MergePullRequest struct { - PullRequest struct { - ID githubv4.ID - } - } `graphql:"mergePullRequest(input: $input)"` - } - - input := githubv4.MergePullRequestInput{ - PullRequestID: pr.ID, - MergeMethod: &mergeMethod, - } - - if m == PullRequestMergeMethodSquash { - commitHeadline := githubv4.String(fmt.Sprintf("%s (#%d)", pr.Title, pr.Number)) - input.CommitHeadline = &commitHeadline - } - - variables := map[string]interface{}{ - "input": input, - } - - gql := graphQLClient(client.http, repo.RepoHost()) - err := gql.MutateNamed(context.Background(), "PullRequestMerge", &mutation, variables) - - return err -} - func PullRequestReady(client *Client, repo ghrepo.Interface, pr *PullRequest) error { var mutation struct { MarkPullRequestReadyForReview struct { @@ -1154,10 +725,3 @@ func BranchDeleteRemote(client *Client, repo ghrepo.Interface, branch string) er path := fmt.Sprintf("repos/%s/%s/git/refs/heads/%s", repo.RepoOwner(), repo.RepoName(), branch) return client.REST(repo.RepoHost(), "DELETE", path, nil, nil) } - -func min(a, b int) int { - if a < b { - return a - } - return b -} diff --git a/api/queries_pr_review.go b/api/queries_pr_review.go new file mode 100644 index 00000000000..bd345e9779d --- /dev/null +++ b/api/queries_pr_review.go @@ -0,0 +1,113 @@ +package api + +import ( + "context" + "time" + + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/shurcooL/githubv4" +) + +type PullRequestReviewState int + +const ( + ReviewApprove PullRequestReviewState = iota + ReviewRequestChanges + ReviewComment +) + +type PullRequestReviewInput struct { + Body string + State PullRequestReviewState +} + +type PullRequestReviews struct { + Nodes []PullRequestReview + PageInfo struct { + HasNextPage bool + EndCursor string + } + TotalCount int +} + +type PullRequestReview struct { + Author Author `json:"author"` + AuthorAssociation string `json:"authorAssociation"` + Body string `json:"body"` + SubmittedAt *time.Time `json:"submittedAt"` + IncludesCreatedEdit bool `json:"includesCreatedEdit"` + ReactionGroups ReactionGroups `json:"reactionGroups"` + State string `json:"state"` + URL string `json:"url,omitempty"` +} + +func AddReview(client *Client, repo ghrepo.Interface, pr *PullRequest, input *PullRequestReviewInput) error { + var mutation struct { + AddPullRequestReview struct { + ClientMutationID string + } `graphql:"addPullRequestReview(input:$input)"` + } + + state := githubv4.PullRequestReviewEventComment + switch input.State { + case ReviewApprove: + state = githubv4.PullRequestReviewEventApprove + case ReviewRequestChanges: + state = githubv4.PullRequestReviewEventRequestChanges + } + + body := githubv4.String(input.Body) + variables := map[string]interface{}{ + "input": githubv4.AddPullRequestReviewInput{ + PullRequestID: pr.ID, + Event: &state, + Body: &body, + }, + } + + gql := graphQLClient(client.http, repo.RepoHost()) + return gql.MutateNamed(context.Background(), "PullRequestReviewAdd", &mutation, variables) +} + +func (prr PullRequestReview) AuthorLogin() string { + return prr.Author.Login +} + +func (prr PullRequestReview) Association() string { + return prr.AuthorAssociation +} + +func (prr PullRequestReview) Content() string { + return prr.Body +} + +func (prr PullRequestReview) Created() time.Time { + if prr.SubmittedAt == nil { + return time.Time{} + } + return *prr.SubmittedAt +} + +func (prr PullRequestReview) HiddenReason() string { + return "" +} + +func (prr PullRequestReview) IsEdited() bool { + return prr.IncludesCreatedEdit +} + +func (prr PullRequestReview) IsHidden() bool { + return false +} + +func (prr PullRequestReview) Link() string { + return prr.URL +} + +func (prr PullRequestReview) Reactions() ReactionGroups { + return prr.ReactionGroups +} + +func (prr PullRequestReview) Status() string { + return prr.State +} diff --git a/api/queries_pr_test.go b/api/queries_pr_test.go index 4e0c1581e86..0d692b45146 100644 --- a/api/queries_pr_test.go +++ b/api/queries_pr_test.go @@ -1,12 +1,13 @@ package api import ( - "reflect" + "encoding/json" "testing" "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/internal/ghrepo" - "github.com/cli/cli/pkg/httpmock" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/stretchr/testify/assert" ) func TestBranchDeleteRemote(t *testing.T) { @@ -52,7 +53,7 @@ func Test_determinePullRequestFeatures(t *testing.T) { tests := []struct { name string hostname string - queryResponse string + queryResponse map[string]string wantPrFeatures pullRequestFeature wantErr bool }{ @@ -60,58 +61,80 @@ func Test_determinePullRequestFeatures(t *testing.T) { name: "github.com", hostname: "github.com", wantPrFeatures: pullRequestFeature{ - HasReviewDecision: true, - HasStatusCheckRollup: true, + HasReviewDecision: true, + HasStatusCheckRollup: true, + HasBranchProtectionRule: true, }, wantErr: false, }, { name: "GHE empty response", hostname: "git.my.org", - queryResponse: heredoc.Doc(` - {"data": {}} - `), + queryResponse: map[string]string{ + `query PullRequest_fields\b`: `{"data": {}}`, + `query PullRequest_fields2\b`: `{"data": {}}`, + }, wantPrFeatures: pullRequestFeature{ - HasReviewDecision: false, - HasStatusCheckRollup: false, + HasReviewDecision: false, + HasStatusCheckRollup: false, + HasBranchProtectionRule: false, }, wantErr: false, }, { name: "GHE has reviewDecision", hostname: "git.my.org", - queryResponse: heredoc.Doc(` - {"data": { - "PullRequest": { - "fields": [ + queryResponse: map[string]string{ + `query PullRequest_fields\b`: heredoc.Doc(` + { "data": { "PullRequest": { "fields": [ {"name": "foo"}, {"name": "reviewDecision"} - ] - } - } } - `), + ] } } } + `), + `query PullRequest_fields2\b`: `{"data": {}}`, + }, wantPrFeatures: pullRequestFeature{ - HasReviewDecision: true, - HasStatusCheckRollup: false, + HasReviewDecision: true, + HasStatusCheckRollup: false, + HasBranchProtectionRule: false, }, wantErr: false, }, { name: "GHE has statusCheckRollup", hostname: "git.my.org", - queryResponse: heredoc.Doc(` - {"data": { - "Commit": { - "fields": [ + queryResponse: map[string]string{ + `query PullRequest_fields\b`: heredoc.Doc(` + { "data": { "Commit": { "fields": [ {"name": "foo"}, {"name": "statusCheckRollup"} - ] - } - } } - `), + ] } } } + `), + `query PullRequest_fields2\b`: `{"data": {}}`, + }, + wantPrFeatures: pullRequestFeature{ + HasReviewDecision: false, + HasStatusCheckRollup: true, + HasBranchProtectionRule: false, + }, + wantErr: false, + }, + { + name: "GHE has branchProtectionRule", + hostname: "git.my.org", + queryResponse: map[string]string{ + `query PullRequest_fields\b`: `{"data": {}}`, + `query PullRequest_fields2\b`: heredoc.Doc(` + { "data": { "Ref": { "fields": [ + {"name": "foo"}, + {"name": "branchProtectionRule"} + ] } } } + `), + }, wantPrFeatures: pullRequestFeature{ - HasReviewDecision: false, - HasStatusCheckRollup: true, + HasReviewDecision: false, + HasStatusCheckRollup: false, + HasBranchProtectionRule: true, }, wantErr: false, }, @@ -121,49 +144,99 @@ func Test_determinePullRequestFeatures(t *testing.T) { fakeHTTP := &httpmock.Registry{} httpClient := NewHTTPClient(ReplaceTripper(fakeHTTP)) - if tt.queryResponse != "" { - fakeHTTP.Register( - httpmock.GraphQL(`query PullRequest_fields\b`), - httpmock.StringResponse(tt.queryResponse)) + for query, resp := range tt.queryResponse { + fakeHTTP.Register(httpmock.GraphQL(query), httpmock.StringResponse(resp)) } gotPrFeatures, err := determinePullRequestFeatures(httpClient, tt.hostname) - if (err != nil) != tt.wantErr { - t.Errorf("determinePullRequestFeatures() error = %v, wantErr %v", err, tt.wantErr) + if tt.wantErr { + assert.Error(t, err) return + } else { + assert.NoError(t, err) } - if !reflect.DeepEqual(gotPrFeatures, tt.wantPrFeatures) { - t.Errorf("determinePullRequestFeatures() = %v, want %v", gotPrFeatures, tt.wantPrFeatures) - } + assert.Equal(t, tt.wantPrFeatures, gotPrFeatures) }) } } -func Test_sortPullRequestsByState(t *testing.T) { - prs := []PullRequest{ +func Test_Logins(t *testing.T) { + rr := ReviewRequests{} + var tests = []struct { + name string + requestedReviews string + want []string + }{ { - BaseRefName: "test1", - State: "MERGED", + name: "no requested reviewers", + requestedReviews: `{"nodes": []}`, + want: []string{}, }, { - BaseRefName: "test2", - State: "CLOSED", + name: "user", + requestedReviews: `{"nodes": [ + { + "requestedreviewer": { + "__typename": "User", "login": "testuser" + } + } + ]}`, + want: []string{"testuser"}, }, { - BaseRefName: "test3", - State: "OPEN", + name: "team", + requestedReviews: `{"nodes": [ + { + "requestedreviewer": { + "__typename": "Team", + "name": "Test Team", + "slug": "test-team", + "organization": {"login": "myorg"} + } + } + ]}`, + want: []string{"myorg/test-team"}, + }, + { + name: "multiple users and teams", + requestedReviews: `{"nodes": [ + { + "requestedreviewer": { + "__typename": "User", "login": "user1" + } + }, + { + "requestedreviewer": { + "__typename": "User", "login": "user2" + } + }, + { + "requestedreviewer": { + "__typename": "Team", + "name": "Test Team", + "slug": "test-team", + "organization": {"login": "myorg"} + } + }, + { + "requestedreviewer": { + "__typename": "Team", + "name": "Dev Team", + "slug": "dev-team", + "organization": {"login": "myorg"} + } + } + ]}`, + want: []string{"user1", "user2", "myorg/test-team", "myorg/dev-team"}, }, } - sortPullRequestsByState(prs) - - if prs[0].BaseRefName != "test3" { - t.Errorf("prs[0]: got %s, want %q", prs[0].BaseRefName, "test3") - } - if prs[1].BaseRefName != "test1" { - t.Errorf("prs[1]: got %s, want %q", prs[1].BaseRefName, "test1") - } - if prs[2].BaseRefName != "test2" { - t.Errorf("prs[2]: got %s, want %q", prs[2].BaseRefName, "test2") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := json.Unmarshal([]byte(tt.requestedReviews), &rr) + assert.NoError(t, err, "Failed to unmarshal json string as ReviewRequests") + logins := rr.Logins() + assert.Equal(t, tt.want, logins) + }) } } diff --git a/api/queries_repo.go b/api/queries_repo.go index 54f88a7e193..8832aae7d0f 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -4,33 +4,113 @@ import ( "bytes" "context" "encoding/json" - "errors" "fmt" + "io" "net/http" "sort" "strings" "time" - "github.com/cli/cli/internal/ghrepo" + "github.com/cli/cli/v2/internal/ghrepo" "github.com/shurcooL/githubv4" ) // Repository contains information about a GitHub repo type Repository struct { - ID string - Name string - Description string - URL string - CloneURL string - CreatedAt time.Time - Owner RepositoryOwner - - IsPrivate bool - HasIssuesEnabled bool - ViewerPermission string - DefaultBranchRef BranchRef + ID string + Name string + NameWithOwner string + Owner RepositoryOwner + Parent *Repository + TemplateRepository *Repository + Description string + HomepageURL string + OpenGraphImageURL string + UsesCustomOpenGraphImage bool + URL string + SSHURL string + MirrorURL string + SecurityPolicyURL string + + CreatedAt time.Time + PushedAt *time.Time + UpdatedAt time.Time + + IsBlankIssuesEnabled bool + IsSecurityPolicyEnabled bool + HasIssuesEnabled bool + HasProjectsEnabled bool + HasWikiEnabled bool + MergeCommitAllowed bool + SquashMergeAllowed bool + RebaseMergeAllowed bool + + ForkCount int + StargazerCount int + Watchers struct { + TotalCount int `json:"totalCount"` + } + Issues struct { + TotalCount int `json:"totalCount"` + } + PullRequests struct { + TotalCount int `json:"totalCount"` + } + + CodeOfConduct *CodeOfConduct + ContactLinks []ContactLink + DefaultBranchRef BranchRef + DeleteBranchOnMerge bool + DiskUsage int + FundingLinks []FundingLink + IsArchived bool + IsEmpty bool + IsFork bool + IsInOrganization bool + IsMirror bool + IsPrivate bool + IsTemplate bool + IsUserConfigurationRepository bool + LicenseInfo *RepositoryLicense + ViewerCanAdminister bool + ViewerDefaultCommitEmail string + ViewerDefaultMergeMethod string + ViewerHasStarred bool + ViewerPermission string + ViewerPossibleCommitEmails []string + ViewerSubscription string + + RepositoryTopics struct { + Nodes []struct { + Topic RepositoryTopic + } + } + PrimaryLanguage *CodingLanguage + Languages struct { + Edges []struct { + Size int `json:"size"` + Node CodingLanguage `json:"node"` + } + } + IssueTemplates []IssueTemplate + PullRequestTemplates []PullRequestTemplate + Labels struct { + Nodes []IssueLabel + } + Milestones struct { + Nodes []Milestone + } + LatestRelease *RepositoryRelease - Parent *Repository + AssignableUsers struct { + Nodes []GitHubUser + } + MentionableUsers struct { + Nodes []GitHubUser + } + Projects struct { + Nodes []RepoProject + } // pseudo-field that keeps track of host name of this repo hostname string @@ -38,12 +118,81 @@ type Repository struct { // RepositoryOwner is the owner of a GitHub repository type RepositoryOwner struct { - Login string + ID string `json:"id"` + Login string `json:"login"` +} + +type GitHubUser struct { + ID string `json:"id"` + Login string `json:"login"` + Name string `json:"name"` } // BranchRef is the branch name in a GitHub repository type BranchRef struct { - Name string + Name string `json:"name"` +} + +type CodeOfConduct struct { + Key string `json:"key"` + Name string `json:"name"` + URL string `json:"url"` +} + +type RepositoryLicense struct { + Key string `json:"key"` + Name string `json:"name"` + Nickname string `json:"nickname"` +} + +type ContactLink struct { + About string `json:"about"` + Name string `json:"name"` + URL string `json:"url"` +} + +type FundingLink struct { + Platform string `json:"platform"` + URL string `json:"url"` +} + +type CodingLanguage struct { + Name string `json:"name"` +} + +type IssueTemplate struct { + Name string `json:"name"` + Title string `json:"title"` + Body string `json:"body"` + About string `json:"about"` +} + +type PullRequestTemplate struct { + Filename string `json:"filename"` + Body string `json:"body"` +} + +type RepositoryTopic struct { + Name string `json:"name"` +} + +type RepositoryRelease struct { + Name string `json:"name"` + TagName string `json:"tagName"` + URL string `json:"url"` + PublishedAt time.Time `json:"publishedAt"` +} + +type IssueLabel struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Color string `json:"color"` +} + +type License struct { + Key string `json:"key"` + Name string `json:"name"` } // RepoOwner is the login name of the owner @@ -61,11 +210,6 @@ func (r Repository) RepoHost() string { return r.hostname } -// IsFork is true when this repository has a parent repository -func (r Repository) IsFork() bool { - return r.Parent != nil -} - // ViewerCanPush is true when the requesting user has push access func (r Repository) ViewerCanPush() bool { switch r.ViewerPermission { @@ -86,6 +230,36 @@ func (r Repository) ViewerCanTriage() bool { } } +func FetchRepository(client *Client, repo ghrepo.Interface, fields []string) (*Repository, error) { + query := fmt.Sprintf(`query RepositoryInfo($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) {%s} + }`, RepositoryGraphQL(fields)) + + variables := map[string]interface{}{ + "owner": repo.RepoOwner(), + "name": repo.RepoName(), + } + + var result struct { + Repository *Repository + } + if err := client.GraphQL(repo.RepoHost(), query, variables, &result); err != nil { + return nil, err + } + // The GraphQL API should have returned an error in case of a missing repository, but this isn't + // guaranteed to happen when an authentication token with insufficient permissions is being used. + if result.Repository == nil { + return nil, GraphQLErrorResponse{ + Errors: []GraphQLError{{ + Type: "NOT_FOUND", + Message: fmt.Sprintf("Could not resolve to a Repository with the name '%s/%s'.", repo.RepoOwner(), repo.RepoName()), + }}, + } + } + + return InitRepoHostname(result.Repository, repo.RepoHost()), nil +} + func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) { query := ` fragment repo on Repository { @@ -94,6 +268,7 @@ func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) { owner { login } hasIssuesEnabled description + hasWikiEnabled viewerPermission defaultBranchRef { name @@ -106,6 +281,9 @@ func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) { parent { ...repo } + mergeCommitAllowed + rebaseMergeAllowed + squashMergeAllowed } }` variables := map[string]interface{}{ @@ -113,16 +291,24 @@ func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) { "name": repo.RepoName(), } - result := struct { - Repository Repository - }{} - err := client.GraphQL(repo.RepoHost(), query, variables, &result) - - if err != nil { + var result struct { + Repository *Repository + } + if err := client.GraphQL(repo.RepoHost(), query, variables, &result); err != nil { return nil, err } + // The GraphQL API should have returned an error in case of a missing repository, but this isn't + // guaranteed to happen when an authentication token with insufficient permissions is being used. + if result.Repository == nil { + return nil, GraphQLErrorResponse{ + Errors: []GraphQLError{{ + Type: "NOT_FOUND", + Message: fmt.Sprintf("Could not resolve to a Repository with the name '%s/%s'.", repo.RepoOwner(), repo.RepoName()), + }}, + } + } - return InitRepoHostname(&result.Repository, repo.RepoHost()), nil + return InitRepoHostname(result.Repository, repo.RepoHost()), nil } func RepoDefaultBranch(client *Client, repo ghrepo.Interface) (string, error) { @@ -292,21 +478,34 @@ func InitRepoHostname(repo *Repository, hostname string) *Repository { return repo } -// repositoryV3 is the repository result from GitHub API v3 +// RepositoryV3 is the repository result from GitHub API v3 type repositoryV3 struct { - NodeID string + NodeID string `json:"node_id"` Name string CreatedAt time.Time `json:"created_at"` - CloneURL string `json:"clone_url"` Owner struct { Login string } + Private bool + HTMLUrl string `json:"html_url"` + Parent *repositoryV3 } // ForkRepo forks the repository on GitHub and returns the new repository -func ForkRepo(client *Client, repo ghrepo.Interface) (*Repository, error) { +func ForkRepo(client *Client, repo ghrepo.Interface, org string) (*Repository, error) { path := fmt.Sprintf("repos/%s/forks", ghrepo.FullName(repo)) - body := bytes.NewBufferString(`{}`) + + params := map[string]interface{}{} + if org != "" { + params["organization"] = org + } + + body := &bytes.Buffer{} + enc := json.NewEncoder(body) + if err := enc.Encode(params); err != nil { + return nil, err + } + result := repositoryV3{} err := client.REST(repo.RepoHost(), "POST", path, body, &result) if err != nil { @@ -316,7 +515,6 @@ func ForkRepo(client *Client, repo ghrepo.Interface) (*Repository, error) { return &Repository{ ID: result.NodeID, Name: result.Name, - CloneURL: result.CloneURL, CreatedAt: result.CreatedAt, Owner: RepositoryOwner{ Login: result.Owner.Login, @@ -455,13 +653,62 @@ func (m *RepoMetadataResult) ProjectsToIDs(names []string) ([]string, error) { return ids, nil } +func ProjectsToPaths(projects []RepoProject, names []string) ([]string, error) { + var paths []string + for _, projectName := range names { + found := false + for _, p := range projects { + if strings.EqualFold(projectName, p.Name) { + // format of ResourcePath: /OWNER/REPO/projects/PROJECT_NUMBER or /orgs/ORG/projects/PROJECT_NUMBER + // required format of path: OWNER/REPO/PROJECT_NUMBER or ORG/PROJECT_NUMBER + var path string + pathParts := strings.Split(p.ResourcePath, "/") + if pathParts[1] == "orgs" { + path = fmt.Sprintf("%s/%s", pathParts[2], pathParts[4]) + } else { + path = fmt.Sprintf("%s/%s/%s", pathParts[1], pathParts[2], pathParts[4]) + } + paths = append(paths, path) + found = true + break + } + } + if !found { + return nil, fmt.Errorf("'%s' not found", projectName) + } + } + return paths, nil +} + func (m *RepoMetadataResult) MilestoneToID(title string) (string, error) { for _, m := range m.Milestones { if strings.EqualFold(title, m.Title) { return m.ID, nil } } - return "", errors.New("not found") + return "", fmt.Errorf("'%s' not found", title) +} + +func (m *RepoMetadataResult) Merge(m2 *RepoMetadataResult) { + if len(m2.AssignableUsers) > 0 || len(m.AssignableUsers) == 0 { + m.AssignableUsers = m2.AssignableUsers + } + + if len(m2.Teams) > 0 || len(m.Teams) == 0 { + m.Teams = m2.Teams + } + + if len(m2.Labels) > 0 || len(m.Labels) == 0 { + m.Labels = m2.Labels + } + + if len(m2.Projects) > 0 || len(m.Projects) == 0 { + m.Projects = m2.Projects + } + + if len(m2.Milestones) > 0 || len(m.Milestones) == 0 { + m.Milestones = m2.Milestones + } } type RepoMetadataInput struct { @@ -516,20 +763,12 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput if input.Projects { count++ go func() { - projects, err := RepoProjects(client, repo) + projects, err := RepoAndOrgProjects(client, repo) if err != nil { - errc <- fmt.Errorf("error fetching projects: %w", err) + errc <- err return } result.Projects = projects - - orgProjects, err := OrganizationProjects(client, repo) - // TODO: better detection of non-org repos - if err != nil && !strings.HasPrefix(err.Error(), "Could not resolve to an Organization") { - errc <- fmt.Errorf("error fetching organization projects: %w", err) - return - } - result.Projects = append(result.Projects, orgProjects...) errc <- nil }() } @@ -658,8 +897,10 @@ func RepoResolveMetadataIDs(client *Client, repo ghrepo.Interface, input RepoRes } type RepoProject struct { - ID string - Name string + ID string `json:"id"` + Name string `json:"name"` + Number int `json:"number"` + ResourcePath string `json:"resourcePath"` } // RepoProjects fetches all open projects for a repository @@ -702,6 +943,23 @@ func RepoProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, error) return projects, nil } +// RepoAndOrgProjects fetches all open projects for a repository and its org +func RepoAndOrgProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, error) { + projects, err := RepoProjects(client, repo) + if err != nil { + return projects, fmt.Errorf("error fetching projects: %w", err) + } + + orgProjects, err := OrganizationProjects(client, repo) + // TODO: better detection of non-org repos + if err != nil && !strings.HasPrefix(err.Error(), "Could not resolve to an Organization") { + return projects, fmt.Errorf("error fetching organization projects: %w", err) + } + projects = append(projects, orgProjects...) + + return projects, nil +} + type RepoAssignee struct { ID string Login string @@ -889,3 +1147,33 @@ func MilestoneByNumber(client *Client, repo ghrepo.Interface, number int32) (*Re return query.Repository.Milestone, nil } + +func ProjectNamesToPaths(client *Client, repo ghrepo.Interface, projectNames []string) ([]string, error) { + var paths []string + projects, err := RepoAndOrgProjects(client, repo) + if err != nil { + return paths, err + } + return ProjectsToPaths(projects, projectNames) +} + +func CreateRepoTransformToV4(apiClient *Client, hostname string, method string, path string, body io.Reader) (*Repository, error) { + var responsev3 repositoryV3 + err := apiClient.REST(hostname, method, path, body, &responsev3) + + if err != nil { + return nil, err + } + + return &Repository{ + Name: responsev3.Name, + CreatedAt: responsev3.CreatedAt, + Owner: RepositoryOwner{ + Login: responsev3.Owner.Login, + }, + ID: responsev3.NodeID, + hostname: hostname, + URL: responsev3.HTMLUrl, + IsPrivate: responsev3.Private, + }, nil +} diff --git a/api/queries_repo_test.go b/api/queries_repo_test.go index a90f6bea511..8846e16cc91 100644 --- a/api/queries_repo_test.go +++ b/api/queries_repo_test.go @@ -6,10 +6,31 @@ import ( "strings" "testing" - "github.com/cli/cli/internal/ghrepo" - "github.com/cli/cli/pkg/httpmock" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/httpmock" ) +func TestGitHubRepo_notFound(t *testing.T) { + httpReg := &httpmock.Registry{} + defer httpReg.Verify(t) + + httpReg.Register( + httpmock.GraphQL(`query RepositoryInfo\b`), + httpmock.StringResponse(`{ "data": { "repository": null } }`)) + + client := NewClient(ReplaceTripper(httpReg)) + repo, err := GitHubRepo(client, ghrepo.New("OWNER", "REPO")) + if err == nil { + t.Fatal("GitHubRepo did not return an error") + } + if wants := "GraphQL error: Could not resolve to a Repository with the name 'OWNER/REPO'."; err.Error() != wants { + t.Errorf("GitHubRepo error: want %q, got %q", wants, err.Error()) + } + if repo != nil { + t.Errorf("GitHubRepo: expected nil repo, got %v", repo) + } +} + func Test_RepoMetadata(t *testing.T) { http := &httpmock.Registry{} client := NewClient(ReplaceTripper(http)) @@ -141,6 +162,63 @@ func Test_RepoMetadata(t *testing.T) { } } +func Test_ProjectsToPaths(t *testing.T) { + expectedProjectPaths := []string{"OWNER/REPO/PROJECT_NUMBER", "ORG/PROJECT_NUMBER"} + projects := []RepoProject{ + {ID: "id1", Name: "My Project", ResourcePath: "/OWNER/REPO/projects/PROJECT_NUMBER"}, + {ID: "id2", Name: "Org Project", ResourcePath: "/orgs/ORG/projects/PROJECT_NUMBER"}, + {ID: "id3", Name: "Project", ResourcePath: "/orgs/ORG/projects/PROJECT_NUMBER_2"}, + } + projectNames := []string{"My Project", "Org Project"} + + projectPaths, err := ProjectsToPaths(projects, projectNames) + if err != nil { + t.Errorf("error resolving projects: %v", err) + } + if !sliceEqual(projectPaths, expectedProjectPaths) { + t.Errorf("expected projects %v, got %v", expectedProjectPaths, projectPaths) + } +} + +func Test_ProjectNamesToPaths(t *testing.T) { + http := &httpmock.Registry{} + client := NewClient(ReplaceTripper(http)) + + repo, _ := ghrepo.FromFullName("OWNER/REPO") + + http.Register( + httpmock.GraphQL(`query RepositoryProjectList\b`), + httpmock.StringResponse(` + { "data": { "repository": { "projects": { + "nodes": [ + { "name": "Cleanup", "id": "CLEANUPID", "resourcePath": "/OWNER/REPO/projects/1" }, + { "name": "Roadmap", "id": "ROADMAPID", "resourcePath": "/OWNER/REPO/projects/2" } + ], + "pageInfo": { "hasNextPage": false } + } } } } + `)) + http.Register( + httpmock.GraphQL(`query OrganizationProjectList\b`), + httpmock.StringResponse(` + { "data": { "organization": { "projects": { + "nodes": [ + { "name": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/1" } + ], + "pageInfo": { "hasNextPage": false } + } } } } + `)) + + projectPaths, err := ProjectNamesToPaths(client, repo, []string{"Triage", "Roadmap"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expectedProjectPaths := []string{"ORG/1", "OWNER/REPO/2"} + if !sliceEqual(projectPaths, expectedProjectPaths) { + t.Errorf("expected projects paths %v, got %v", expectedProjectPaths, projectPaths) + } +} + func Test_RepoResolveMetadataIDs(t *testing.T) { http := &httpmock.Registry{} client := NewClient(ReplaceTripper(http)) diff --git a/api/query_builder.go b/api/query_builder.go new file mode 100644 index 00000000000..c9ab62d13c9 --- /dev/null +++ b/api/query_builder.go @@ -0,0 +1,358 @@ +package api + +import ( + "fmt" + "strings" +) + +func squeeze(r rune) rune { + switch r { + case '\n', '\t': + return -1 + default: + return r + } +} + +func shortenQuery(q string) string { + return strings.Map(squeeze, q) +} + +var issueComments = shortenQuery(` + comments(first: 100) { + nodes { + author{login}, + authorAssociation, + body, + createdAt, + includesCreatedEdit, + isMinimized, + minimizedReason, + reactionGroups{content,users{totalCount}} + }, + pageInfo{hasNextPage,endCursor}, + totalCount + } +`) + +var prReviewRequests = shortenQuery(` + reviewRequests(first: 100) { + nodes { + requestedReviewer { + __typename, + ...on User{login}, + ...on Team{ + organization{login} + name, + slug + } + } + } + } +`) + +var prReviews = shortenQuery(` + reviews(first: 100) { + nodes { + author{login}, + authorAssociation, + submittedAt, + body, + state, + reactionGroups{content,users{totalCount}} + } + pageInfo{hasNextPage,endCursor} + } +`) + +var prFiles = shortenQuery(` + files(first: 100) { + nodes { + additions, + deletions, + path + } + } +`) + +var prCommits = shortenQuery(` + commits(first: 100) { + nodes { + commit { + authors(first:100) { + nodes { + name, + email, + user{id,login} + } + }, + messageHeadline, + messageBody, + oid, + committedDate, + authoredDate + } + } + } +`) + +func StatusCheckRollupGraphQL(after string) string { + var afterClause string + if after != "" { + afterClause = ",after:" + after + } + return fmt.Sprintf(shortenQuery(` + statusCheckRollup: commits(last: 1) { + nodes { + commit { + statusCheckRollup { + contexts(first:100%s) { + nodes { + __typename + ...on StatusContext { + context, + state, + targetUrl + }, + ...on CheckRun { + name, + status, + conclusion, + startedAt, + completedAt, + detailsUrl + } + }, + pageInfo{hasNextPage,endCursor} + } + } + } + } + }`), afterClause) +} + +var IssueFields = []string{ + "assignees", + "author", + "body", + "closed", + "comments", + "createdAt", + "closedAt", + "id", + "labels", + "milestone", + "number", + "projectCards", + "reactionGroups", + "state", + "title", + "updatedAt", + "url", +} + +var PullRequestFields = append(IssueFields, + "additions", + "baseRefName", + "changedFiles", + "commits", + "deletions", + "files", + "headRefName", + "headRepository", + "headRepositoryOwner", + "isCrossRepository", + "isDraft", + "maintainerCanModify", + "mergeable", + "mergeCommit", + "mergedAt", + "mergedBy", + "mergeStateStatus", + "potentialMergeCommit", + "reviewDecision", + "reviewRequests", + "reviews", + "statusCheckRollup", +) + +func PullRequestGraphQL(fields []string) string { + var q []string + for _, field := range fields { + switch field { + case "author": + q = append(q, `author{login}`) + case "mergedBy": + q = append(q, `mergedBy{login}`) + case "headRepositoryOwner": + q = append(q, `headRepositoryOwner{id,login,...on User{name}}`) + case "headRepository": + q = append(q, `headRepository{id,name}`) + case "assignees": + q = append(q, `assignees(first:100){nodes{id,login,name},totalCount}`) + case "labels": + q = append(q, `labels(first:100){nodes{id,name,description,color},totalCount}`) + case "projectCards": + q = append(q, `projectCards(first:100){nodes{project{name}column{name}},totalCount}`) + case "milestone": + q = append(q, `milestone{number,title,description,dueOn}`) + case "reactionGroups": + q = append(q, `reactionGroups{content,users{totalCount}}`) + case "mergeCommit": + q = append(q, `mergeCommit{oid}`) + case "potentialMergeCommit": + q = append(q, `potentialMergeCommit{oid}`) + case "comments": + q = append(q, issueComments) + case "reviewRequests": + q = append(q, prReviewRequests) + case "reviews": + q = append(q, prReviews) + case "files": + q = append(q, prFiles) + case "commits": + q = append(q, prCommits) + case "lastCommit": // pseudo-field + q = append(q, `commits(last:1){nodes{commit{oid}}}`) + case "commitsCount": // pseudo-field + q = append(q, `commits{totalCount}`) + case "requiresStrictStatusChecks": // pseudo-field + q = append(q, `baseRef{branchProtectionRule{requiresStrictStatusChecks}}`) + case "statusCheckRollup": + q = append(q, StatusCheckRollupGraphQL("")) + default: + q = append(q, field) + } + } + return strings.Join(q, ",") +} + +var RepositoryFields = []string{ + "id", + "name", + "nameWithOwner", + "owner", + "parent", + "templateRepository", + "description", + "homepageUrl", + "openGraphImageUrl", + "usesCustomOpenGraphImage", + "url", + "sshUrl", + "mirrorUrl", + "securityPolicyUrl", + + "createdAt", + "pushedAt", + "updatedAt", + + "isBlankIssuesEnabled", + "isSecurityPolicyEnabled", + "hasIssuesEnabled", + "hasProjectsEnabled", + "hasWikiEnabled", + "mergeCommitAllowed", + "squashMergeAllowed", + "rebaseMergeAllowed", + + "forkCount", + "stargazerCount", + "watchers", + "issues", + "pullRequests", + + "codeOfConduct", + "contactLinks", + "defaultBranchRef", + "deleteBranchOnMerge", + "diskUsage", + "fundingLinks", + "isArchived", + "isEmpty", + "isFork", + "isInOrganization", + "isMirror", + "isPrivate", + "isTemplate", + "isUserConfigurationRepository", + "licenseInfo", + "viewerCanAdminister", + "viewerDefaultCommitEmail", + "viewerDefaultMergeMethod", + "viewerHasStarred", + "viewerPermission", + "viewerPossibleCommitEmails", + "viewerSubscription", + + "repositoryTopics", + "primaryLanguage", + "languages", + "issueTemplates", + "pullRequestTemplates", + "labels", + "milestones", + "latestRelease", + + "assignableUsers", + "mentionableUsers", + "projects", + + // "branchProtectionRules", // too complex to expose + // "collaborators", // does it make sense to expose without affiliation filter? +} + +func RepositoryGraphQL(fields []string) string { + var q []string + for _, field := range fields { + switch field { + case "codeOfConduct": + q = append(q, "codeOfConduct{key,name,url}") + case "contactLinks": + q = append(q, "contactLinks{about,name,url}") + case "fundingLinks": + q = append(q, "fundingLinks{platform,url}") + case "licenseInfo": + q = append(q, "licenseInfo{key,name,nickname}") + case "owner": + q = append(q, "owner{id,login}") + case "parent": + q = append(q, "parent{id,name,owner{id,login}}") + case "templateRepository": + q = append(q, "templateRepository{id,name,owner{id,login}}") + case "repositoryTopics": + q = append(q, "repositoryTopics(first:100){nodes{topic{name}}}") + case "issueTemplates": + q = append(q, "issueTemplates{name,title,body,about}") + case "pullRequestTemplates": + q = append(q, "pullRequestTemplates{body,filename}") + case "labels": + q = append(q, "labels(first:100){nodes{id,color,name,description}}") + case "languages": + q = append(q, "languages(first:100){edges{size,node{name}}}") + case "primaryLanguage": + q = append(q, "primaryLanguage{name}") + case "latestRelease": + q = append(q, "latestRelease{publishedAt,tagName,name,url}") + case "milestones": + q = append(q, "milestones(first:100,states:OPEN){nodes{number,title,description,dueOn}}") + case "assignableUsers": + q = append(q, "assignableUsers(first:100){nodes{id,login,name}}") + case "mentionableUsers": + q = append(q, "mentionableUsers(first:100){nodes{id,login,name}}") + case "projects": + q = append(q, "projects(first:100,states:OPEN){nodes{id,name,number,body,resourcePath}}") + case "watchers": + q = append(q, "watchers{totalCount}") + case "issues": + q = append(q, "issues(states:OPEN){totalCount}") + case "pullRequests": + q = append(q, "pullRequests(states:OPEN){totalCount}") + case "defaultBranchRef": + q = append(q, "defaultBranchRef{name}") + default: + q = append(q, field) + } + } + return strings.Join(q, ",") +} diff --git a/api/query_builder_test.go b/api/query_builder_test.go new file mode 100644 index 00000000000..7806f2d0557 --- /dev/null +++ b/api/query_builder_test.go @@ -0,0 +1,39 @@ +package api + +import "testing" + +func TestPullRequestGraphQL(t *testing.T) { + tests := []struct { + name string + fields []string + want string + }{ + { + name: "empty", + fields: []string(nil), + want: "", + }, + { + name: "simple fields", + fields: []string{"number", "title"}, + want: "number,title", + }, + { + name: "fields with nested structures", + fields: []string{"author", "assignees"}, + want: "author{login},assignees(first:100){nodes{id,login,name},totalCount}", + }, + { + name: "compressed query", + fields: []string{"files"}, + want: "files(first: 100) {nodes {additions,deletions,path}}", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := PullRequestGraphQL(tt.fields); got != tt.want { + t.Errorf("PullRequestGraphQL() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/api/reaction_groups.go b/api/reaction_groups.go new file mode 100644 index 00000000000..08ae5304065 --- /dev/null +++ b/api/reaction_groups.go @@ -0,0 +1,59 @@ +package api + +import ( + "bytes" + "encoding/json" +) + +type ReactionGroups []ReactionGroup + +func (rg ReactionGroups) MarshalJSON() ([]byte, error) { + buf := bytes.Buffer{} + buf.WriteRune('[') + encoder := json.NewEncoder(&buf) + encoder.SetEscapeHTML(false) + + hasPrev := false + for _, g := range rg { + if g.Users.TotalCount == 0 { + continue + } + if hasPrev { + buf.WriteRune(',') + } + if err := encoder.Encode(&g); err != nil { + return nil, err + } + hasPrev = true + } + buf.WriteRune(']') + return buf.Bytes(), nil +} + +type ReactionGroup struct { + Content string `json:"content"` + Users ReactionGroupUsers `json:"users"` +} + +type ReactionGroupUsers struct { + TotalCount int `json:"totalCount"` +} + +func (rg ReactionGroup) Count() int { + return rg.Users.TotalCount +} + +func (rg ReactionGroup) Emoji() string { + return reactionEmoji[rg.Content] +} + +var reactionEmoji = map[string]string{ + "THUMBS_UP": "\U0001f44d", + "THUMBS_DOWN": "\U0001f44e", + "LAUGH": "\U0001f604", + "HOORAY": "\U0001f389", + "CONFUSED": "\U0001f615", + "HEART": "\u2764\ufe0f", + "ROCKET": "\U0001f680", + "EYES": "\U0001f440", +} diff --git a/api/reaction_groups_test.go b/api/reaction_groups_test.go new file mode 100644 index 00000000000..e30a9e1f8e0 --- /dev/null +++ b/api/reaction_groups_test.go @@ -0,0 +1,100 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_String(t *testing.T) { + tests := map[string]struct { + rg ReactionGroup + emoji string + count int + }{ + "empty reaction group": { + rg: ReactionGroup{}, + emoji: "", + count: 0, + }, + "unknown reaction group": { + rg: ReactionGroup{ + Content: "UNKNOWN", + Users: ReactionGroupUsers{TotalCount: 1}, + }, + emoji: "", + count: 1, + }, + "thumbs up reaction group": { + rg: ReactionGroup{ + Content: "THUMBS_UP", + Users: ReactionGroupUsers{TotalCount: 2}, + }, + emoji: "\U0001f44d", + count: 2, + }, + "thumbs down reaction group": { + rg: ReactionGroup{ + Content: "THUMBS_DOWN", + Users: ReactionGroupUsers{TotalCount: 3}, + }, + emoji: "\U0001f44e", + count: 3, + }, + "laugh reaction group": { + rg: ReactionGroup{ + Content: "LAUGH", + Users: ReactionGroupUsers{TotalCount: 4}, + }, + emoji: "\U0001f604", + count: 4, + }, + "hooray reaction group": { + rg: ReactionGroup{ + Content: "HOORAY", + Users: ReactionGroupUsers{TotalCount: 5}, + }, + emoji: "\U0001f389", + count: 5, + }, + "confused reaction group": { + rg: ReactionGroup{ + Content: "CONFUSED", + Users: ReactionGroupUsers{TotalCount: 6}, + }, + emoji: "\U0001f615", + count: 6, + }, + "heart reaction group": { + rg: ReactionGroup{ + Content: "HEART", + Users: ReactionGroupUsers{TotalCount: 7}, + }, + emoji: "\u2764\ufe0f", + count: 7, + }, + "rocket reaction group": { + rg: ReactionGroup{ + Content: "ROCKET", + Users: ReactionGroupUsers{TotalCount: 8}, + }, + emoji: "\U0001f680", + count: 8, + }, + "eyes reaction group": { + rg: ReactionGroup{ + Content: "EYES", + Users: ReactionGroupUsers{TotalCount: 9}, + }, + emoji: "\U0001f440", + count: 9, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tt.emoji, tt.rg.Emoji()) + assert.Equal(t, tt.count, tt.rg.Count()) + }) + } +} diff --git a/auth/oauth.go b/auth/oauth.go deleted file mode 100644 index 2c8a78cd4e4..00000000000 --- a/auth/oauth.go +++ /dev/null @@ -1,275 +0,0 @@ -package auth - -import ( - "crypto/rand" - "encoding/hex" - "errors" - "fmt" - "io" - "io/ioutil" - "net" - "net/http" - "net/url" - "strconv" - "strings" - "time" - - "github.com/cli/cli/internal/ghinstance" -) - -func randomString(length int) (string, error) { - b := make([]byte, length/2) - _, err := rand.Read(b) - if err != nil { - return "", err - } - return hex.EncodeToString(b), nil -} - -// OAuthFlow represents the setup for authenticating with GitHub -type OAuthFlow struct { - Hostname string - ClientID string - ClientSecret string - Scopes []string - OpenInBrowser func(string, string) error - WriteSuccessHTML func(io.Writer) - VerboseStream io.Writer - HTTPClient *http.Client - TimeNow func() time.Time - TimeSleep func(time.Duration) -} - -func detectDeviceFlow(statusCode int, values url.Values) (bool, error) { - if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden || - statusCode == http.StatusNotFound || statusCode == http.StatusUnprocessableEntity || - (statusCode == http.StatusOK && values == nil) || - (statusCode == http.StatusBadRequest && values != nil && values.Get("error") == "unauthorized_client") { - return true, nil - } else if statusCode != http.StatusOK { - if values != nil && values.Get("error_description") != "" { - return false, fmt.Errorf("HTTP %d: %s", statusCode, values.Get("error_description")) - } - return false, fmt.Errorf("error: HTTP %d", statusCode) - } - return false, nil -} - -// ObtainAccessToken guides the user through the browser OAuth flow on GitHub -// and returns the OAuth access token upon completion. -func (oa *OAuthFlow) ObtainAccessToken() (accessToken string, err error) { - // first, check if OAuth Device Flow is supported - initURL := fmt.Sprintf("https://%s/login/device/code", oa.Hostname) - tokenURL := fmt.Sprintf("https://%s/login/oauth/access_token", oa.Hostname) - - oa.logf("POST %s\n", initURL) - resp, err := oa.HTTPClient.PostForm(initURL, url.Values{ - "client_id": {oa.ClientID}, - "scope": {strings.Join(oa.Scopes, " ")}, - }) - if err != nil { - return - } - defer resp.Body.Close() - - var values url.Values - if strings.Contains(resp.Header.Get("Content-Type"), "application/x-www-form-urlencoded") { - var bb []byte - bb, err = ioutil.ReadAll(resp.Body) - if err != nil { - return - } - values, err = url.ParseQuery(string(bb)) - if err != nil { - return - } - } - - if doFallback, err := detectDeviceFlow(resp.StatusCode, values); doFallback { - // OAuth Device Flow is not available; continue with OAuth browser flow with a - // local server endpoint as callback target - return oa.localServerFlow() - } else if err != nil { - return "", fmt.Errorf("%v (%s)", err, initURL) - } - - timeNow := oa.TimeNow - if timeNow == nil { - timeNow = time.Now - } - timeSleep := oa.TimeSleep - if timeSleep == nil { - timeSleep = time.Sleep - } - - intervalSeconds, err := strconv.Atoi(values.Get("interval")) - if err != nil { - return "", fmt.Errorf("could not parse interval=%q as integer: %w", values.Get("interval"), err) - } - checkInterval := time.Duration(intervalSeconds) * time.Second - - expiresIn, err := strconv.Atoi(values.Get("expires_in")) - if err != nil { - return "", fmt.Errorf("could not parse expires_in=%q as integer: %w", values.Get("expires_in"), err) - } - expiresAt := timeNow().Add(time.Duration(expiresIn) * time.Second) - - err = oa.OpenInBrowser(values.Get("verification_uri"), values.Get("user_code")) - if err != nil { - return - } - - for { - timeSleep(checkInterval) - accessToken, err = oa.deviceFlowPing(tokenURL, values.Get("device_code")) - if accessToken == "" && err == nil { - if timeNow().After(expiresAt) { - err = errors.New("authentication timed out") - } else { - continue - } - } - break - } - - return -} - -func (oa *OAuthFlow) deviceFlowPing(tokenURL, deviceCode string) (accessToken string, err error) { - oa.logf("POST %s\n", tokenURL) - resp, err := oa.HTTPClient.PostForm(tokenURL, url.Values{ - "client_id": {oa.ClientID}, - "device_code": {deviceCode}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }) - if err != nil { - return "", err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("error: HTTP %d (%s)", resp.StatusCode, tokenURL) - } - - bb, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", err - } - values, err := url.ParseQuery(string(bb)) - if err != nil { - return "", err - } - - if accessToken := values.Get("access_token"); accessToken != "" { - return accessToken, nil - } - - errorType := values.Get("error") - if errorType == "authorization_pending" { - return "", nil - } - - if errorDescription := values.Get("error_description"); errorDescription != "" { - return "", errors.New(errorDescription) - } - return "", errors.New("OAuth device flow error") -} - -func (oa *OAuthFlow) localServerFlow() (accessToken string, err error) { - state, _ := randomString(20) - - code := "" - listener, err := net.Listen("tcp4", "127.0.0.1:0") - if err != nil { - return - } - port := listener.Addr().(*net.TCPAddr).Port - - scopes := "repo" - if oa.Scopes != nil { - scopes = strings.Join(oa.Scopes, " ") - } - - localhost := "127.0.0.1" - callbackPath := "/callback" - if ghinstance.IsEnterprise(oa.Hostname) { - // the OAuth app on Enterprise hosts is still registered with a legacy callback URL - // see https://github.com/cli/cli/pull/222, https://github.com/cli/cli/pull/650 - localhost = "localhost" - callbackPath = "/" - } - - q := url.Values{} - q.Set("client_id", oa.ClientID) - q.Set("redirect_uri", fmt.Sprintf("http://%s:%d%s", localhost, port, callbackPath)) - q.Set("scope", scopes) - q.Set("state", state) - - startURL := fmt.Sprintf("https://%s/login/oauth/authorize?%s", oa.Hostname, q.Encode()) - oa.logf("open %s\n", startURL) - err = oa.OpenInBrowser(startURL, "") - if err != nil { - return - } - - _ = http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - oa.logf("server handler: %s\n", r.URL.Path) - if r.URL.Path != callbackPath { - w.WriteHeader(404) - return - } - defer listener.Close() - rq := r.URL.Query() - if state != rq.Get("state") { - fmt.Fprintf(w, "Error: state mismatch") - return - } - code = rq.Get("code") - oa.logf("server received code %q\n", code) - w.Header().Add("content-type", "text/html") - if oa.WriteSuccessHTML != nil { - oa.WriteSuccessHTML(w) - } else { - fmt.Fprintf(w, "

You have successfully authenticated. You may now close this page.

") - } - })) - - tokenURL := fmt.Sprintf("https://%s/login/oauth/access_token", oa.Hostname) - oa.logf("POST %s\n", tokenURL) - resp, err := oa.HTTPClient.PostForm(tokenURL, - url.Values{ - "client_id": {oa.ClientID}, - "client_secret": {oa.ClientSecret}, - "code": {code}, - "state": {state}, - }) - if err != nil { - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - err = fmt.Errorf("HTTP %d error while obtaining OAuth access token", resp.StatusCode) - return - } - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return - } - tokenValues, err := url.ParseQuery(string(body)) - if err != nil { - return - } - accessToken = tokenValues.Get("access_token") - if accessToken == "" { - err = errors.New("the access token could not be read from HTTP response") - } - return -} - -func (oa *OAuthFlow) logf(format string, args ...interface{}) { - if oa.VerboseStream == nil { - return - } - fmt.Fprintf(oa.VerboseStream, format, args...) -} diff --git a/auth/oauth_test.go b/auth/oauth_test.go deleted file mode 100644 index a9070a1b113..00000000000 --- a/auth/oauth_test.go +++ /dev/null @@ -1,258 +0,0 @@ -package auth - -import ( - "bytes" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "testing" - "time" -) - -type roundTripper func(*http.Request) (*http.Response, error) - -func (rt roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - return rt(req) -} - -func TestObtainAccessToken_deviceFlow(t *testing.T) { - requestCount := 0 - rt := func(req *http.Request) (*http.Response, error) { - route := fmt.Sprintf("%s %s", req.Method, req.URL) - switch route { - case "POST https://github.com/login/device/code": - if err := req.ParseForm(); err != nil { - return nil, err - } - if req.PostForm.Get("client_id") != "CLIENT-ID" { - t.Errorf("expected POST /login/device/code to supply client_id=%q, got %q", "CLIENT-ID", req.PostForm.Get("client_id")) - } - if req.PostForm.Get("scope") != "repo gist" { - t.Errorf("expected POST /login/device/code to supply scope=%q, got %q", "repo gist", req.PostForm.Get("scope")) - } - - responseData := url.Values{} - responseData.Set("device_code", "DEVICE-CODE") - responseData.Set("user_code", "1234-ABCD") - responseData.Set("verification_uri", "https://github.com/login/device") - responseData.Set("interval", "5") - responseData.Set("expires_in", "899") - - return &http.Response{ - StatusCode: 200, - Body: ioutil.NopCloser(bytes.NewBufferString(responseData.Encode())), - Header: http.Header{ - "Content-Type": []string{"application/x-www-form-urlencoded; charset=utf-8"}, - }, - }, nil - case "POST https://github.com/login/oauth/access_token": - if err := req.ParseForm(); err != nil { - return nil, err - } - if req.PostForm.Get("client_id") != "CLIENT-ID" { - t.Errorf("expected POST /login/oauth/access_token to supply client_id=%q, got %q", "CLIENT-ID", req.PostForm.Get("client_id")) - } - if req.PostForm.Get("device_code") != "DEVICE-CODE" { - t.Errorf("expected POST /login/oauth/access_token to supply device_code=%q, got %q", "DEVICE-CODE", req.PostForm.Get("scope")) - } - if req.PostForm.Get("grant_type") != "urn:ietf:params:oauth:grant-type:device_code" { - t.Errorf("expected POST /login/oauth/access_token to supply grant_type=%q, got %q", "urn:ietf:params:oauth:grant-type:device_code", req.PostForm.Get("grant_type")) - } - - responseData := url.Values{} - requestCount++ - if requestCount == 1 { - responseData.Set("error", "authorization_pending") - } else { - responseData.Set("access_token", "OTOKEN") - } - - return &http.Response{ - StatusCode: 200, - Body: ioutil.NopCloser(bytes.NewBufferString(responseData.Encode())), - }, nil - default: - return nil, fmt.Errorf("unstubbed HTTP request: %v", route) - } - } - httpClient := &http.Client{ - Transport: roundTripper(rt), - } - - slept := time.Duration(0) - var browseURL string - var browseCode string - - oa := &OAuthFlow{ - Hostname: "github.com", - ClientID: "CLIENT-ID", - ClientSecret: "CLIENT-SEKRIT", - Scopes: []string{"repo", "gist"}, - OpenInBrowser: func(url, code string) error { - browseURL = url - browseCode = code - return nil - }, - HTTPClient: httpClient, - TimeNow: time.Now, - TimeSleep: func(d time.Duration) { - slept += d - }, - } - - token, err := oa.ObtainAccessToken() - if err != nil { - t.Fatalf("ObtainAccessToken error: %v", err) - } - - if token != "OTOKEN" { - t.Errorf("expected token %q, got %q", "OTOKEN", token) - } - if requestCount != 2 { - t.Errorf("expected 2 HTTP pings for token, got %d", requestCount) - } - if slept.String() != "10s" { - t.Errorf("expected total sleep duration of %s, got %s", "10s", slept.String()) - } - if browseURL != "https://github.com/login/device" { - t.Errorf("expected to open browser at %s, got %s", "https://github.com/login/device", browseURL) - } - if browseCode != "1234-ABCD" { - t.Errorf("expected to provide user with one-time code %q, got %q", "1234-ABCD", browseCode) - } -} - -func Test_detectDeviceFlow(t *testing.T) { - type args struct { - statusCode int - values url.Values - } - tests := []struct { - name string - args args - doFallback bool - wantErr string - }{ - { - name: "success", - args: args{ - statusCode: 200, - values: url.Values{}, - }, - doFallback: false, - wantErr: "", - }, - { - name: "wrong response type", - args: args{ - statusCode: 200, - values: nil, - }, - doFallback: true, - wantErr: "", - }, - { - name: "401 unauthorized", - args: args{ - statusCode: 401, - values: nil, - }, - doFallback: true, - wantErr: "", - }, - { - name: "403 forbidden", - args: args{ - statusCode: 403, - values: nil, - }, - doFallback: true, - wantErr: "", - }, - { - name: "404 not found", - args: args{ - statusCode: 404, - values: nil, - }, - doFallback: true, - wantErr: "", - }, - { - name: "422 unprocessable", - args: args{ - statusCode: 422, - values: nil, - }, - doFallback: true, - wantErr: "", - }, - { - name: "402 payment required", - args: args{ - statusCode: 402, - values: nil, - }, - doFallback: false, - wantErr: "error: HTTP 402", - }, - { - name: "400 bad request", - args: args{ - statusCode: 400, - values: nil, - }, - doFallback: false, - wantErr: "error: HTTP 400", - }, - { - name: "400 with values", - args: args{ - statusCode: 400, - values: url.Values{ - "error": []string{"blah"}, - }, - }, - doFallback: false, - wantErr: "error: HTTP 400", - }, - { - name: "400 with unauthorized_client", - args: args{ - statusCode: 400, - values: url.Values{ - "error": []string{"unauthorized_client"}, - }, - }, - doFallback: true, - wantErr: "", - }, - { - name: "400 with error_description", - args: args{ - statusCode: 400, - values: url.Values{ - "error_description": []string{"HI"}, - }, - }, - doFallback: false, - wantErr: "HTTP 400: HI", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := detectDeviceFlow(tt.args.statusCode, tt.args.values) - if (err != nil) != (tt.wantErr != "") { - t.Errorf("detectDeviceFlow() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.wantErr != "" && err.Error() != tt.wantErr { - t.Errorf("error = %q, wantErr = %q", err, tt.wantErr) - } - if got != tt.doFallback { - t.Errorf("detectDeviceFlow() = %v, want %v", got, tt.doFallback) - } - }) - } -} diff --git a/cmd/gen-docs/main.go b/cmd/gen-docs/main.go index 5698794e0fb..22c4c95f99a 100644 --- a/cmd/gen-docs/main.go +++ b/cmd/gen-docs/main.go @@ -3,52 +3,57 @@ package main import ( "fmt" "os" + "path/filepath" "strings" - "github.com/cli/cli/internal/docs" - "github.com/cli/cli/pkg/cmd/root" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/v2/internal/docs" + "github.com/cli/cli/v2/pkg/cmd/root" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/pflag" ) func main() { - var flagError pflag.ErrorHandling - docCmd := pflag.NewFlagSet("", flagError) - manPage := docCmd.BoolP("man-page", "", false, "Generate manual pages") - website := docCmd.BoolP("website", "", false, "Generate website pages") - dir := docCmd.StringP("doc-path", "", "", "Path directory where you want generate doc files") - help := docCmd.BoolP("help", "h", false, "Help about any command") - - if err := docCmd.Parse(os.Args); err != nil { + if err := run(os.Args); err != nil { + fmt.Fprintln(os.Stderr, err) os.Exit(1) } +} + +func run(args []string) error { + flags := pflag.NewFlagSet("", pflag.ContinueOnError) + manPage := flags.BoolP("man-page", "", false, "Generate manual pages") + website := flags.BoolP("website", "", false, "Generate website pages") + dir := flags.StringP("doc-path", "", "", "Path directory where you want generate doc files") + help := flags.BoolP("help", "h", false, "Help about any command") + + if err := flags.Parse(args); err != nil { + return err + } if *help { - _, err := fmt.Fprintf(os.Stderr, "Usage of %s:\n\n%s", os.Args[0], docCmd.FlagUsages()) - if err != nil { - fatal(err) - } - os.Exit(1) + fmt.Fprintf(os.Stderr, "Usage of %s:\n\n%s", filepath.Base(args[0]), flags.FlagUsages()) + return nil } if *dir == "" { - fatal("no dir set") + return fmt.Errorf("error: --doc-path not set") } io, _, _, _ := iostreams.Test() - rootCmd := root.NewCmdRoot(&cmdutil.Factory{IOStreams: io}, "", "") + rootCmd := root.NewCmdRoot(&cmdutil.Factory{ + IOStreams: io, + Browser: &browser{}, + }, "", "") rootCmd.InitDefaultHelpCmd() - err := os.MkdirAll(*dir, 0755) - if err != nil { - fatal(err) + if err := os.MkdirAll(*dir, 0755); err != nil { + return err } if *website { - err = docs.GenMarkdownTreeCustom(rootCmd, *dir, filePrepender, linkHandler) - if err != nil { - fatal(err) + if err := docs.GenMarkdownTreeCustom(rootCmd, *dir, filePrepender, linkHandler); err != nil { + return err } } @@ -59,11 +64,12 @@ func main() { Source: "", Manual: "", } - err = docs.GenManTree(rootCmd, header, *dir) - if err != nil { - fatal(err) + if err := docs.GenManTree(rootCmd, header, *dir); err != nil { + return err } } + + return nil } func filePrepender(filename string) string { @@ -79,7 +85,8 @@ func linkHandler(name string) string { return fmt.Sprintf("./%s", strings.TrimSuffix(name, ".md")) } -func fatal(msg interface{}) { - fmt.Fprintln(os.Stderr, msg) - os.Exit(1) +type browser struct{} + +func (b *browser) Browse(url string) error { + return nil } diff --git a/cmd/gen-docs/main_test.go b/cmd/gen-docs/main_test.go new file mode 100644 index 00000000000..5c69ff6b23a --- /dev/null +++ b/cmd/gen-docs/main_test.go @@ -0,0 +1,32 @@ +package main + +import ( + "io/ioutil" + "strings" + "testing" +) + +func Test_run(t *testing.T) { + dir := t.TempDir() + args := []string{"--man-page", "--website", "--doc-path", dir} + err := run(args) + if err != nil { + t.Fatalf("got error: %v", err) + } + + manPage, err := ioutil.ReadFile(dir + "/gh-issue-create.1") + if err != nil { + t.Fatalf("error reading `gh-issue-create.1`: %v", err) + } + if !strings.Contains(string(manPage), `\fBgh issue create`) { + t.Fatal("man page corrupted") + } + + markdownPage, err := ioutil.ReadFile(dir + "/gh_issue_create.md") + if err != nil { + t.Fatalf("error reading `gh_issue_create.md`: %v", err) + } + if !strings.Contains(string(markdownPage), `## gh issue create`) { + t.Fatal("markdown page corrupted") + } +} diff --git a/cmd/gh/main.go b/cmd/gh/main.go index 354f5392b53..50f8335a314 100644 --- a/cmd/gh/main.go +++ b/cmd/gh/main.go @@ -7,21 +7,25 @@ import ( "net" "os" "os/exec" - "path" + "path/filepath" "strings" + "time" surveyCore "github.com/AlecAivazis/survey/v2/core" - "github.com/cli/cli/api" - "github.com/cli/cli/internal/build" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/internal/ghinstance" - "github.com/cli/cli/internal/run" - "github.com/cli/cli/pkg/cmd/alias/expand" - "github.com/cli/cli/pkg/cmd/factory" - "github.com/cli/cli/pkg/cmd/root" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/update" - "github.com/cli/cli/utils" + "github.com/AlecAivazis/survey/v2/terminal" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/build" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/run" + "github.com/cli/cli/v2/internal/update" + "github.com/cli/cli/v2/pkg/cmd/alias/expand" + "github.com/cli/cli/v2/pkg/cmd/factory" + "github.com/cli/cli/v2/pkg/cmd/root" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/utils" + "github.com/cli/safeexec" "github.com/mattn/go-colorable" "github.com/mgutz/ansi" "github.com/spf13/cobra" @@ -29,7 +33,21 @@ import ( var updaterEnabled = "" +type exitCode int + +const ( + exitOK exitCode = 0 + exitError exitCode = 1 + exitCancel exitCode = 2 + exitAuth exitCode = 4 +) + func main() { + code := mainRun() + os.Exit(int(code)) +} + +func mainRun() exitCode { buildDate := build.Date buildVersion := build.Version @@ -41,12 +59,12 @@ func main() { hasDebug := os.Getenv("DEBUG") != "" - if hostFromEnv := os.Getenv("GH_HOST"); hostFromEnv != "" { - ghinstance.OverrideDefault(hostFromEnv) - } - cmdFactory := factory.New(buildVersion) stderr := cmdFactory.IOStreams.ErrOut + + if spec := os.Getenv("GH_FORCE_TTY"); spec != "" { + cmdFactory.IOStreams.ForceTerminal(spec) + } if !cmdFactory.IOStreams.ColorEnabled() { surveyCore.DisableColor = true } else { @@ -64,24 +82,23 @@ func main() { } } - // Enable running gh from explorer.exe. Without this, the user is told to stop and run from a + // Enable running gh from Windows File Explorer's address bar. Without this, the user is told to stop and run from a // terminal. With this, a user can clone a repo (or take other actions) directly from explorer. - cobra.MousetrapHelpText = "" + if len(os.Args) > 1 && os.Args[1] != "" { + cobra.MousetrapHelpText = "" + } rootCmd := root.NewCmdRoot(cmdFactory, buildVersion, buildDate) cfg, err := cmdFactory.Config() if err != nil { fmt.Fprintf(stderr, "failed to read configuration: %s\n", err) - os.Exit(2) - } - - if prompt, _ := cfg.Get("", "prompt"); prompt == "disabled" { - cmdFactory.IOStreams.SetNeverPrompt(true) + return exitError } - if pager, _ := cfg.Get("", "pager"); pager != "" { - cmdFactory.IOStreams.SetPager(pager) + // TODO: remove after FromFullName has been revisited + if host, err := cfg.DefaultHost(); err == nil { + ghrepo.SetDefaultHost(host) } expandedArgs := []string{} @@ -89,15 +106,20 @@ func main() { expandedArgs = os.Args[1:] } - cmd, _, err := rootCmd.Traverse(expandedArgs) - if err != nil || cmd == rootCmd { + // translate `gh help ` to `gh --help` for extensions + if len(expandedArgs) == 2 && expandedArgs[0] == "help" && !hasCommand(rootCmd, expandedArgs[1:]) { + expandedArgs = []string{expandedArgs[1], "--help"} + } + + if !hasCommand(rootCmd, expandedArgs) { originalArgs := expandedArgs isShell := false - expandedArgs, isShell, err = expand.ExpandAlias(cfg, os.Args, nil) + argsForExpansion := append([]string{"gh"}, expandedArgs...) + expandedArgs, isShell, err = expand.ExpandAlias(cfg, argsForExpansion, nil) if err != nil { fmt.Fprintf(stderr, "failed to process aliases: %s\n", err) - os.Exit(2) + return exitError } if hasDebug { @@ -105,7 +127,13 @@ func main() { } if isShell { - externalCmd := exec.Command(expandedArgs[0], expandedArgs[1:]...) + exe, err := safeexec.LookPath(expandedArgs[0]) + if err != nil { + fmt.Fprintf(stderr, "failed to run external command: %s", err) + return exitError + } + + externalCmd := exec.Command(exe, expandedArgs[1:]...) externalCmd.Stderr = os.Stderr externalCmd.Stdout = os.Stdout externalCmd.Stdin = os.Stdin @@ -113,73 +141,136 @@ func main() { err = preparedCmd.Run() if err != nil { - if ee, ok := err.(*exec.ExitError); ok { - os.Exit(ee.ExitCode()) + var execError *exec.ExitError + if errors.As(err, &execError) { + return exitCode(execError.ExitCode()) } - fmt.Fprintf(stderr, "failed to run external command: %s", err) - os.Exit(3) + return exitError + } + + return exitOK + } else if len(expandedArgs) > 0 && !hasCommand(rootCmd, expandedArgs) { + extensionManager := cmdFactory.ExtensionManager + if found, err := extensionManager.Dispatch(expandedArgs, os.Stdin, os.Stdout, os.Stderr); err != nil { + var execError *exec.ExitError + if errors.As(err, &execError) { + return exitCode(execError.ExitCode()) + } + fmt.Fprintf(stderr, "failed to run extension: %s", err) + return exitError + } else if found { + return exitOK } + } + } - os.Exit(0) + // provide completions for aliases and extensions + rootCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var results []string + if aliases, err := cfg.Aliases(); err == nil { + for aliasName := range aliases.All() { + if strings.HasPrefix(aliasName, toComplete) { + results = append(results, aliasName) + } + } } + for _, ext := range cmdFactory.ExtensionManager.List(false) { + if strings.HasPrefix(ext.Name(), toComplete) { + results = append(results, ext.Name()) + } + } + return results, cobra.ShellCompDirectiveNoFileComp } cs := cmdFactory.IOStreams.ColorScheme() - authCheckEnabled := os.Getenv("GITHUB_TOKEN") == "" && - os.Getenv("GITHUB_ENTERPRISE_TOKEN") == "" && - cmd != nil && cmdutil.IsAuthCheckEnabled(cmd) - if authCheckEnabled { - if !cmdutil.CheckAuth(cfg) { + authError := errors.New("authError") + rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + // require that the user is authenticated before running most commands + if cmdutil.IsAuthCheckEnabled(cmd) && !cmdutil.CheckAuth(cfg) { fmt.Fprintln(stderr, cs.Bold("Welcome to GitHub CLI!")) fmt.Fprintln(stderr) fmt.Fprintln(stderr, "To authenticate, please run `gh auth login`.") - fmt.Fprintln(stderr, "You can also set the GITHUB_TOKEN environment variable, if preferred.") - os.Exit(4) + return authError } + + return nil } rootCmd.SetArgs(expandedArgs) if cmd, err := rootCmd.ExecuteC(); err != nil { + if err == cmdutil.SilentError { + return exitError + } else if cmdutil.IsUserCancellation(err) { + if errors.Is(err, terminal.InterruptErr) { + // ensure the next shell prompt will start on its own line + fmt.Fprint(stderr, "\n") + } + return exitCancel + } else if errors.Is(err, authError) { + return exitAuth + } + printError(stderr, err, cmd, hasDebug) + if strings.Contains(err.Error(), "Incorrect function") { + fmt.Fprintln(stderr, "You appear to be running in MinTTY without pseudo terminal support.") + fmt.Fprintln(stderr, "To learn about workarounds for this error, run: gh help mintty") + return exitError + } + var httpErr api.HTTPError if errors.As(err, &httpErr) && httpErr.StatusCode == 401 { - fmt.Println("hint: try authenticating with `gh auth login`") + fmt.Fprintln(stderr, "Try authenticating with: gh auth login") + } else if strings.Contains(err.Error(), "Resource protected by organization SAML enforcement") { + fmt.Fprintln(stderr, "Try re-authenticating with: gh auth refresh") + } else if msg := httpErr.ScopesSuggestion(); msg != "" { + fmt.Fprintln(stderr, msg) } - os.Exit(1) + return exitError } if root.HasFailed() { - os.Exit(1) + return exitError } newRelease := <-updateMessageChan if newRelease != nil { - msg := fmt.Sprintf("%s %s → %s\n%s", + isHomebrew := isUnderHomebrew(cmdFactory.Executable()) + if isHomebrew && isRecentRelease(newRelease.PublishedAt) { + // do not notify Homebrew users before the version bump had a chance to get merged into homebrew-core + return exitOK + } + fmt.Fprintf(stderr, "\n\n%s %s → %s\n", ansi.Color("A new release of gh is available:", "yellow"), ansi.Color(buildVersion, "cyan"), - ansi.Color(newRelease.Version, "cyan"), + ansi.Color(newRelease.Version, "cyan")) + if isHomebrew { + fmt.Fprintf(stderr, "To upgrade, run: %s\n", "brew update && brew upgrade gh") + } + fmt.Fprintf(stderr, "%s\n\n", ansi.Color(newRelease.URL, "yellow")) - - fmt.Fprintf(stderr, "\n\n%s\n\n", msg) } + + return exitOK } -func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) { - if err == cmdutil.SilentError { - return - } +// hasCommand returns true if args resolve to a built-in command +func hasCommand(rootCmd *cobra.Command, args []string) bool { + c, _, err := rootCmd.Traverse(args) + return err == nil && c != rootCmd +} +func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) { var dnsError *net.DNSError if errors.As(err, &dnsError) { fmt.Fprintf(out, "error connecting to %s\n", dnsError.Name) if debug { fmt.Fprintln(out, dnsError) } - fmt.Fprintln(out, "check your internet connection or githubstatus.com") + fmt.Fprintln(out, "check your internet connection or https://githubstatus.com") return } @@ -201,7 +292,7 @@ func shouldCheckForUpdate() bool { if os.Getenv("CODESPACES") != "" { return false } - return updaterEnabled != "" && !isCI() && !isCompletionCommand() && utils.IsTerminal(os.Stderr) + return updaterEnabled != "" && !isCI() && utils.IsTerminal(os.Stdout) && utils.IsTerminal(os.Stderr) } // based on https://github.com/watson/ci-info/blob/HEAD/index.js @@ -211,10 +302,6 @@ func isCI() bool { os.Getenv("RUN_ID") != "" // TaskCluster, dsari } -func isCompletionCommand() bool { - return len(os.Args) > 1 && os.Args[1] == "completion" -} - func checkForUpdate(currentVersion string) (*update.ReleaseInfo, error) { if !shouldCheckForUpdate() { return nil, nil @@ -226,7 +313,7 @@ func checkForUpdate(currentVersion string) (*update.ReleaseInfo, error) { } repo := updaterEnabled - stateFilePath := path.Join(config.ConfigDir(), "state.yml") + stateFilePath := filepath.Join(config.StateDir(), "state.yml") return update.CheckForUpdate(client, stateFilePath, repo, currentVersion) } @@ -239,7 +326,7 @@ func basicClient(currentVersion string) (*api.Client, error) { } opts = append(opts, api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", currentVersion))) - token := os.Getenv("GITHUB_TOKEN") + token, _ := config.AuthTokenFromEnv(ghinstance.Default()) if token == "" { if c, err := config.ParseDefaultConfig(); err == nil { token, _ = c.Get(ghinstance.Default(), "oauth_token") @@ -256,3 +343,23 @@ func apiVerboseLog() api.ClientOption { colorize := utils.IsTerminal(os.Stderr) return api.VerboseLog(colorable.NewColorable(os.Stderr), logTraffic, colorize) } + +func isRecentRelease(publishedAt time.Time) bool { + return !publishedAt.IsZero() && time.Since(publishedAt) < time.Hour*24 +} + +// Check whether the gh binary was found under the Homebrew prefix +func isUnderHomebrew(ghBinary string) bool { + brewExe, err := safeexec.LookPath("brew") + if err != nil { + return false + } + + brewPrefixBytes, err := exec.Command(brewExe, "--prefix").Output() + if err != nil { + return false + } + + brewBinPrefix := filepath.Join(strings.TrimSpace(string(brewPrefixBytes)), "bin") + string(filepath.Separator) + return strings.HasPrefix(ghBinary, brewBinPrefix) +} diff --git a/cmd/gh/main_test.go b/cmd/gh/main_test.go index 9036391fd03..b428ff4b3b7 100644 --- a/cmd/gh/main_test.go +++ b/cmd/gh/main_test.go @@ -7,7 +7,7 @@ import ( "net" "testing" - "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/cmdutil" "github.com/spf13/cobra" ) @@ -43,7 +43,7 @@ func Test_printError(t *testing.T) { debug: false, }, wantOut: `error connecting to api.github.com -check your internet connection or githubstatus.com +check your internet connection or https://githubstatus.com `, }, { diff --git a/context/context.go b/context/context.go index babd51f55e7..d549dc04c2f 100644 --- a/context/context.go +++ b/context/context.go @@ -6,11 +6,11 @@ import ( "sort" "github.com/AlecAivazis/survey/v2" - "github.com/cli/cli/api" - "github.com/cli/cli/git" - "github.com/cli/cli/internal/ghrepo" - "github.com/cli/cli/pkg/iostreams" - "github.com/cli/cli/pkg/prompt" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/prompt" ) // cap the number of git remotes looked up, since the user might have an @@ -104,7 +104,7 @@ func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams) (ghrepo.Interface, e if repo == nil { continue } - if repo.IsFork() { + if repo.Parent != nil { add(repo.Parent) } add(repo) diff --git a/context/remote.go b/context/remote.go index b8887848377..b88f1cfa386 100644 --- a/context/remote.go +++ b/context/remote.go @@ -5,8 +5,8 @@ import ( "net/url" "strings" - "github.com/cli/cli/git" - "github.com/cli/cli/internal/ghrepo" + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/ghrepo" ) // Remotes represents a set of git remotes @@ -54,6 +54,20 @@ func (r Remotes) Less(i, j int) bool { return remoteNameSortScore(r[i].Name) > remoteNameSortScore(r[j].Name) } +// Filter remotes by given hostnames, maintains original order +func (r Remotes) FilterByHosts(hosts []string) Remotes { + filtered := make(Remotes, 0) + for _, rr := range r { + for _, host := range hosts { + if strings.EqualFold(rr.RepoHost(), host) { + filtered = append(filtered, rr) + break + } + } + } + return filtered +} + // Remote represents a git remote mapped to a GitHub repository type Remote struct { *git.Remote diff --git a/context/remote_test.go b/context/remote_test.go index ab3f7e2e284..2f0fc50bba9 100644 --- a/context/remote_test.go +++ b/context/remote_test.go @@ -1,22 +1,14 @@ package context import ( - "errors" "net/url" - "reflect" "testing" - "github.com/cli/cli/git" - "github.com/cli/cli/internal/ghrepo" + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/stretchr/testify/assert" ) -func eq(t *testing.T, got interface{}, expected interface{}) { - t.Helper() - if !reflect.DeepEqual(got, expected) { - t.Errorf("expected: %v, got: %v", expected, got) - } -} - func Test_Remotes_FindByName(t *testing.T) { list := Remotes{ &Remote{Remote: &git.Remote{Name: "mona"}, Repo: ghrepo.New("monalisa", "myfork")}, @@ -25,15 +17,15 @@ func Test_Remotes_FindByName(t *testing.T) { } r, err := list.FindByName("upstream", "origin") - eq(t, err, nil) - eq(t, r.Name, "upstream") + assert.NoError(t, err) + assert.Equal(t, "upstream", r.Name) - r, err = list.FindByName("nonexist", "*") - eq(t, err, nil) - eq(t, r.Name, "mona") + r, err = list.FindByName("nonexistent", "*") + assert.NoError(t, err) + assert.Equal(t, "mona", r.Name) - _, err = list.FindByName("nonexist") - eq(t, err, errors.New(`no GitHub remotes found`)) + _, err = list.FindByName("nonexistent") + assert.Error(t, err, "no GitHub remotes found") } func Test_translateRemotes(t *testing.T) { @@ -66,3 +58,14 @@ func Test_translateRemotes(t *testing.T) { t.Errorf("got %q", result[0].RepoName()) } } + +func Test_FilterByHosts(t *testing.T) { + r1 := &Remote{Remote: &git.Remote{Name: "mona"}, Repo: ghrepo.NewWithHost("monalisa", "myfork", "test.com")} + r2 := &Remote{Remote: &git.Remote{Name: "origin"}, Repo: ghrepo.NewWithHost("monalisa", "octo-cat", "example.com")} + r3 := &Remote{Remote: &git.Remote{Name: "upstream"}, Repo: ghrepo.New("hubot", "tools")} + list := Remotes{r1, r2, r3} + f := list.FilterByHosts([]string{"example.com", "test.com"}) + assert.Equal(t, 2, len(f)) + assert.Equal(t, r1, f[0]) + assert.Equal(t, r2, f[1]) +} diff --git a/docs/install_linux.md b/docs/install_linux.md index 65a9471537c..c14273d5dec 100644 --- a/docs/install_linux.md +++ b/docs/install_linux.md @@ -1,34 +1,26 @@ -# Installing gh on Linux +# Installing gh on Linux and BSD Packages downloaded from https://cli.github.com or from https://github.com/cli/cli/releases are considered official binaries. We focus on popular Linux distros and -the following CPU architectures: `i386`, `amd64`, `arm64`. +the following CPU architectures: `i386`, `amd64`, `arm64`, `armhf`. Other sources for installation are community-maintained and thus might lag behind our release schedule. -If none of our official binaries, packages, repositories, nor community sources work for you, we recommend using our `Makefile` to build `gh` from source. It's quick and easy. - ## Official sources -### Debian, Ubuntu Linux (apt) +### Debian, Ubuntu Linux, Raspberry Pi OS (apt) Install: ```bash -sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-key C99B11DEB97541F0 -sudo apt-add-repository https://cli.github.com/packages +curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null sudo apt update sudo apt install gh ``` -**Note**: If you are behind a firewall, the connection to `keyserver.ubuntu.com` might fail. In that case, try running `sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-key C99B11DEB97541F0`. - -**Note**: If you get _"gpg: failed to start the dirmngr '/usr/bin/dirmngr': No such file or directory"_ error, try installing the `dirmngr` package. Run `sudo apt-get install dirmngr` and repeat the steps above. - -**Note**: most systems will have `apt-add-repository` already. If you get a _command not found_ -error, try running `sudo apt install software-properties-common` and trying these steps again. - +**Note**: If you get the error _"gpg: failed to start the dirmngr '/usr/bin/dirmngr': No such file or directory"_, try installing the `dirmngr` package: `sudo apt install dirmngr`. Upgrade: @@ -74,16 +66,13 @@ sudo zypper update gh * [Download release binaries][releases page] that match your platform; or * [Build from source](./source.md). -### openSUSE/SUSE Linux (zypper) - -Install and upgrade: +## Unofficial, community-supported methods -1. Download the `.rpm` file from the [releases page][]; -2. Install the downloaded file: `sudo zypper in gh_*_linux_amd64.rpm` +The GitHub CLI team does not maintain the following packages or repositories and thus we are unable to provide support for those installation methods. -## Unofficial, Community-supported methods +### Snap (do not use) -The core GitHub CLI team does not maintain the following packages or repositories. They are unofficial and we are unable to provide support or guarantees for them. They are linked here as a convenience and their presence does not imply continued oversight from the CLI core team. Users who choose to use them do so at their own risk. +There are [so many issues with Snap](https://github.com/casperdcl/cli/issues/7) as a runtime mechanism for apps like GitHub CLI that our team suggests _never installing gh as a snap_. ### Arch Linux @@ -93,14 +82,68 @@ Arch Linux users can install from the [community repo][arch linux repo]: sudo pacman -S github-cli ``` +Alternatively, use the [unofficial AUR package][arch linux aur] to build GitHub CLI from source. + ### Android -Android users can install via Termux: +Android 7+ users can install via [Termux](https://wiki.termux.com/wiki/Main_Page): + +```bash +pkg install gh +``` + +### FreeBSD + +FreeBSD users can install from the [ports collection](https://www.freshports.org/devel/gh/): + +```bash +cd /usr/ports/devel/gh/ && make install clean +``` + +Or via [pkg(8)](https://www.freebsd.org/cgi/man.cgi?pkg(8)): ```bash pkg install gh ``` +### OpenBSD + +In -current, or in releases starting from 7.0, OpenBSD users can install from packages: + +``` +pkg_add github-cli +``` + +### Funtoo + +Funtoo Linux has an autogenerated github-cli package, located in [dev-kit](https://github.com/funtoo/dev-kit/tree/1.4-release/dev-util/github-cli), which can be installed in the following way: + +``` bash +emerge -av github-cli +``` + +Upgrading can be done by syncing the repos and then requesting an upgrade: + +``` bash +ego sync +emerge -u github-cli +``` + +### Gentoo + +Gentoo Linux users can install from the [main portage tree](https://packages.gentoo.org/packages/dev-util/github-cli): + +``` bash +emerge -av github-cli +``` + +Upgrading can be done by updating the portage tree and then requesting an upgrade: + +``` bash +emerge --sync +emerge -u github-cli +``` + ### Kiss Linux Kiss Linux users can install from the [community repos](https://github.com/kisslinux/community): @@ -117,17 +160,13 @@ Nix/NixOS users can install from [nixpkgs](https://search.nixos.org/packages?sho nix-env -iA nixos.gitAndTools.gh ``` -### Snaps - -Many Linux distro users can install using Snapd from the [Snap Store](https://snapcraft.io/gh) or the associated [repo](https://github.com/casperdcl/cli/tree/snap) +### openSUSE Tumbleweed +openSUSE Tumbleweed users can install from the [official distribution repo](https://software.opensuse.org/package/gh): ```bash -sudo snap install --edge gh && snap connect gh:ssh-keys +sudo zypper in gh ``` -> Snaps are auto-updated every 6 hours. `Snapd` is required and is available on a wide range of Linux distros. -> Find out which distros have Snapd pre-installed and how to install it in the [Snapcraft Installation Docs](https://snapcraft.io/docs/installing-snapd) -> -> **Note:** `snap connect gh:ssh-keys` is needed for all authentication and SSH needs. [releases page]: https://github.com/cli/cli/releases/latest [arch linux repo]: https://www.archlinux.org/packages/community/x86_64/github-cli +[arch linux aur]: https://aur.archlinux.org/packages/github-cli-git diff --git a/docs/project-layout.md b/docs/project-layout.md new file mode 100644 index 00000000000..60d0c2aac0c --- /dev/null +++ b/docs/project-layout.md @@ -0,0 +1,84 @@ +# GitHub CLI project layout + +At a high level, these areas make up the `github.com/cli/cli` project: +- [`cmd/`](../cmd) - `main` packages for building binaries such as the `gh` executable +- [`pkg/`](../pkg) - most other packages, including the implementation for individual gh commands +- [`docs/`](../docs) - documentation for maintainers and contributors +- [`script/`](../script) - build and release scripts +- [`internal/`](../internal) - Go packages highly specific to our needs and thus internal +- [`go.mod`](../go.mod) - external Go dependencies for this project, automatically fetched by Go at build time + +Some auxiliary Go packages are at the top level of the project for historical reasons: +- [`api/`](../api) - main utilities for making requests to the GitHub API +- [`context/`](../context) - DEPRECATED: use only for referencing git remotes +- [`git/`](../git) - utilities to gather information from a local git repository +- [`test/`](../test) - DEPRECATED: do not use +- [`utils/`](../utils) - DEPRECATED: use only for printing table output + +## Command-line help text + +Running `gh help issue list` displays help text for a topic. In this case, the topic is a specific command, +and help text for every command is embedded in that command's source code. The naming convention for gh +commands is: +``` +pkg/cmd///.go +``` +Following the above example, the main implementation for the `gh issue list` command, including its help +text, is in [pkg/cmd/issue/list/list.go](../pkg/cmd/issue/list/list.go) + +Other help topics not specific to any command, for example `gh help environment`, are found in +[pkg/cmd/root/help_topic.go](../pkg/cmd/root/help_topic.go). + +During our release process, these help topics are [automatically converted](../cmd/gen-docs/main.go) to +manual pages and published under https://cli.github.com/manual/. + +## How GitHub CLI works + +To illustrate how GitHub CLI works in its typical mode of operation, let's build the project, run a command, +and talk through which code gets run in order. + +1. `go run script/build.go` - Makes sure all external Go dependencies are fetched, then compiles the + `cmd/gh/main.go` file into a `bin/gh` binary. +2. `bin/gh issue list --limit 5` - Runs the newly built `bin/gh` binary (note: on Windows you must use + backslashes like `bin\gh`) and passes the following arguments to the process: `["issue", "list", "--limit", "5"]`. +3. `func main()` inside `cmd/gh/main.go` is the first Go function that runs. The arguments passed to the + process are available through `os.Args`. +4. The `main` package initializes the "root" command with `root.NewCmdRoot()` and dispatches execution to it + with `rootCmd.ExecuteC()`. +5. The [root command](../pkg/cmd/root/root.go) represents the top-level `gh` command and knows how to + dispatch execution to any other gh command nested under it. +6. Based on `["issue", "list"]` arguments, the execution reaches the `RunE` block of the `cobra.Command` + within [pkg/cmd/issue/list/list.go](../pkg/cmd/issue/list/list.go). +7. The `--limit 5` flag originally passed as arguments be automatically parsed and its value stored as + `opts.LimitResults`. +8. `func listRun()` is called, which is responsible for implementing the logic of the `gh issue list` command. +9. The command collects information from sources like the GitHub API then writes the final output to + standard output and standard error [streams](../pkg/iostreams/iostreams.go) available at `opts.IO`. +10. The program execution is now back at `func main()` of `cmd/gh/main.go`. If there were any Go errors as a + result of processing the command, the function will abort the process with a non-zero exit status. + Otherwise, the process ends with status 0 indicating success. + +## How to add a new command + +0. First, check on our issue tracker to verify that our team had approved the plans for a new command. +1. Create a package for the new command, e.g. for a new command `gh boom` create the following directory + structure: `pkg/cmd/boom/` +2. The new package should expose a method, e.g. `NewCmdBoom()`, that accepts a `*cmdutil.Factory` type and + returns a `*cobra.Command`. + * Any logic specific to this command should be kept within the command's package and not added to any + "global" packages like `api` or `utils`. +3. Use the method from the previous step to generate the command and add it to the command tree, typically + somewhere in the `NewCmdRoot()` method. + +## How to write tests + +This task might be tricky. Typically, gh commands do things like look up information from the git repository +in the current directory, query the GitHub API, scan the user's `~/.ssh/config` file, clone or fetch git +repositories, etc. Naturally, none of these things should ever happen for real when running tests, unless +you are sure that any filesystem operations are strictly scoped to a location made for and maintained by the +test itself. To avoid actually running things like making real API requests or shelling out to `git` +commands, we stub them. You should look at how that's done within some existing tests. + +To make your code testable, write small, isolated pieces of functionality that are designed to be composed +together. Prefer table-driven tests for maintaining variations of different test inputs and expectations +when exercising a single piece of functionality. diff --git a/docs/releasing.md b/docs/releasing.md index 3c583a80b3f..f17902c7b00 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,6 +1,6 @@ # Releasing -Our build system automatically compiles and attaches cross-platform binaries to any git tag named `vX.Y.Z`. The automated changelog is generated from commit messages starting with “Merge pull request …” that landed between this tag and the previous one (as determined topologically by git). +Our build system automatically compiles and attaches cross-platform binaries to any git tag named `vX.Y.Z`. The changelog is [generated from git commit log](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes). Users who run official builds of `gh` on their machines will get notified about the new version within a 24 hour period. diff --git a/docs/source.md b/docs/source.md index fc90ef94f46..a8225c3713f 100644 --- a/docs/source.md +++ b/docs/source.md @@ -1,6 +1,6 @@ # Installation from source -0. Verify that you have Go 1.13+ installed +0. Verify that you have Go 1.16+ installed ```sh $ go version @@ -15,16 +15,45 @@ $ cd gh-cli ``` -2. Build the project +2. Build and install + #### Unix-like systems + ```sh + # installs to '/usr/local' by default; sudo may be required + $ make install + + # or, install to a different location + $ make install prefix=/path/to/gh ``` - $ make + + #### Windows + ```pwsh + # build the `bin\gh.exe` binary + > go run script\build.go ``` + There is no install step available on Windows. -3. Move the resulting `bin/gh` executable to somewhere in your PATH +3. Run `gh version` to check if it worked. - ```sh - $ sudo mv ./bin/gh /usr/local/bin/ - ``` + #### Windows + Run `bin\gh version` to check if it worked. + +## Cross-compiling binaries for different platforms + +You can use any platform with Go installed to build a binary that is intended for another platform +or CPU architecture. This is achieved by setting environment variables such as GOOS and GOARCH. + +For example, to compile the `gh` binary for the 32-bit Raspberry Pi OS: +```sh +# on a Unix-like system: +$ GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=0 make clean bin/gh +``` +```pwsh +# on Windows, pass environment variables as arguments to the build script: +> go run script\build.go clean bin\gh GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=0 +``` + +Run `go tool dist list` to list all supported values of GOOS/GOARCH. -4. Run `gh version` to check if it worked. +Tip: to reduce the size of the resulting binary, you can use `GO_LDFLAGS="-s -w"`. This omits +symbol tables used for debugging. See the list of [supported linker flags](https://golang.org/cmd/link/). diff --git a/git/fixtures/.gitignore b/git/fixtures/.gitignore new file mode 100644 index 00000000000..abae30d02a4 --- /dev/null +++ b/git/fixtures/.gitignore @@ -0,0 +1 @@ +*.git/COMMIT_EDITMSG diff --git a/git/fixtures/simple.git/HEAD b/git/fixtures/simple.git/HEAD new file mode 100644 index 00000000000..b870d82622c --- /dev/null +++ b/git/fixtures/simple.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/git/fixtures/simple.git/config b/git/fixtures/simple.git/config new file mode 100644 index 00000000000..f0858dd7370 --- /dev/null +++ b/git/fixtures/simple.git/config @@ -0,0 +1,9 @@ +[core] + repositoryformatversion = 0 + filemode = true + ;bare = true + ignorecase = true + precomposeunicode = true +[user] + name = Mona the Cat + email = monalisa@github.com diff --git a/git/fixtures/simple.git/index b/git/fixtures/simple.git/index new file mode 100644 index 0000000000000000000000000000000000000000..65d675154f23ffb2d0196e017d44a5e7017550f5 GIT binary patch literal 65 zcmZ?q402{*U|<4bhL9jvS0E+HV4z^Y<=qr}%;|LA&IJiiy? 1614174263 +0100 commit (initial): Initial commit +d1e0abfb7d158ed544a202a6958c62d4fc22e12f 6f1a2405cace1633d89a79c74c65f22fe78f9659 Mona the Cat 1614174275 +0100 commit: Second commit diff --git a/git/fixtures/simple.git/logs/refs/heads/main b/git/fixtures/simple.git/logs/refs/heads/main new file mode 100644 index 00000000000..216887f9ea9 --- /dev/null +++ b/git/fixtures/simple.git/logs/refs/heads/main @@ -0,0 +1,2 @@ +0000000000000000000000000000000000000000 d1e0abfb7d158ed544a202a6958c62d4fc22e12f Mona the Cat 1614174263 +0100 commit (initial): Initial commit +d1e0abfb7d158ed544a202a6958c62d4fc22e12f 6f1a2405cace1633d89a79c74c65f22fe78f9659 Mona the Cat 1614174275 +0100 commit: Second commit diff --git a/git/fixtures/simple.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 b/git/fixtures/simple.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 new file mode 100644 index 0000000000000000000000000000000000000000..adf64119a33d7621aeeaa505d30adb58afaa5559 GIT binary patch literal 15 Wcmb zVTQ@QwM_vh`=W;kP>SeF4um-cNi*AE#Z#)Wgc)P3NrYxg=7$g26^awfsivtoAEkIA zMvEL~A9KJ$H6x0{YWSgRKj7MT23-ZdSmC`5x^E|cE}O28^p<=302ds&iE#38vCdjE t-0@N6e{FM<-1h>1E5>}kHaL|J-S!2v!y@`TwDRCyhaSOcegWSFR-)K;Tv-4B literal 0 HcmV?d00001 diff --git a/git/fixtures/simple.git/objects/d1/e0abfb7d158ed544a202a6958c62d4fc22e12f b/git/fixtures/simple.git/objects/d1/e0abfb7d158ed544a202a6958c62d4fc22e12f new file mode 100644 index 00000000000..ec3ada617c1 --- /dev/null +++ b/git/fixtures/simple.git/objects/d1/e0abfb7d158ed544a202a6958c62d4fc22e12f @@ -0,0 +1,3 @@ +x�NK +�0t�S�� ILc +"�+�@���M�뢷7"^@�� �b�Z�xF��h���b轴;�l��K����r�3<��3��3�Kc#-��"�k8�Z.��2�d�=�^*)ES�&�iq��ɏ��i��ϋP� j��A�y�� 3*H/ \ No newline at end of file diff --git a/git/fixtures/simple.git/refs/heads/main b/git/fixtures/simple.git/refs/heads/main new file mode 100644 index 00000000000..8316cdaf59c --- /dev/null +++ b/git/fixtures/simple.git/refs/heads/main @@ -0,0 +1 @@ +6f1a2405cace1633d89a79c74c65f22fe78f9659 diff --git a/git/git.go b/git/git.go index 2c2d32e0e8c..97dafa1c7ea 100644 --- a/git/git.go +++ b/git/git.go @@ -10,9 +10,11 @@ import ( "os/exec" "path" "regexp" + "runtime" "strings" - "github.com/cli/cli/internal/run" + "github.com/cli/cli/v2/internal/run" + "github.com/cli/safeexec" ) // ErrNotOnAnyBranch indicates that the user is in detached HEAD state @@ -37,7 +39,10 @@ func (r TrackingRef) String() string { // ShowRefs resolves fully-qualified refs to commit hashes func ShowRefs(ref ...string) ([]Ref, error) { args := append([]string{"show-ref", "--verify", "--"}, ref...) - showRef := exec.Command("git", args...) + showRef, err := GitCommand(args...) + if err != nil { + return nil, err + } output, err := run.PrepareCmd(showRef).Output() var refs []Ref @@ -57,7 +62,13 @@ func ShowRefs(ref ...string) ([]Ref, error) { // CurrentBranch reads the checked-out branch for the git repository func CurrentBranch() (string, error) { - refCmd := GitCommand("symbolic-ref", "--quiet", "HEAD") + refCmd, err := GitCommand("symbolic-ref", "--quiet", "HEAD") + if err != nil { + return "", err + } + + stderr := bytes.Buffer{} + refCmd.Stderr = &stderr output, err := run.PrepareCmd(refCmd).Output() if err == nil { @@ -65,26 +76,28 @@ func CurrentBranch() (string, error) { return getBranchShortName(output), nil } - var cmdErr *run.CmdError - if errors.As(err, &cmdErr) { - if cmdErr.Stderr.Len() == 0 { - // Detached head - return "", ErrNotOnAnyBranch - } + if stderr.Len() == 0 { + // Detached head + return "", ErrNotOnAnyBranch } - // Unknown error - return "", err + return "", fmt.Errorf("%sgit: %s", stderr.String(), err) } func listRemotes() ([]string, error) { - remoteCmd := exec.Command("git", "remote", "-v") + remoteCmd, err := GitCommand("remote", "-v") + if err != nil { + return nil, err + } output, err := run.PrepareCmd(remoteCmd).Output() return outputLines(output), err } func Config(name string) (string, error) { - configCmd := exec.Command("git", "config", name) + configCmd, err := GitCommand("config", name) + if err != nil { + return "", err + } output, err := run.PrepareCmd(configCmd).Output() if err != nil { return "", fmt.Errorf("unknown config key: %s", name) @@ -94,12 +107,38 @@ func Config(name string) (string, error) { } -var GitCommand = func(args ...string) *exec.Cmd { - return exec.Command("git", args...) +type NotInstalled struct { + message string + error +} + +func (e *NotInstalled) Error() string { + return e.message +} + +func GitCommand(args ...string) (*exec.Cmd, error) { + gitExe, err := safeexec.LookPath("git") + if err != nil { + if errors.Is(err, exec.ErrNotFound) { + programName := "git" + if runtime.GOOS == "windows" { + programName = "Git for Windows" + } + return nil, &NotInstalled{ + message: fmt.Sprintf("unable to find git executable in PATH; please install %s before retrying", programName), + error: err, + } + } + return nil, err + } + return exec.Command(gitExe, args...), nil } func UncommittedChangeCount() (int, error) { - statusCmd := GitCommand("status", "--porcelain") + statusCmd, err := GitCommand("status", "--porcelain") + if err != nil { + return 0, err + } output, err := run.PrepareCmd(statusCmd).Output() if err != nil { return 0, err @@ -123,10 +162,13 @@ type Commit struct { } func Commits(baseRef, headRef string) ([]*Commit, error) { - logCmd := GitCommand( + logCmd, err := GitCommand( "-c", "log.ShowSignature=false", "log", "--pretty=format:%H,%s", "--cherry", fmt.Sprintf("%s...%s", baseRef, headRef)) + if err != nil { + return nil, err + } output, err := run.PrepareCmd(logCmd).Output() if err != nil { return []*Commit{}, err @@ -153,18 +195,38 @@ func Commits(baseRef, headRef string) ([]*Commit, error) { return commits, nil } -func CommitBody(sha string) (string, error) { - showCmd := GitCommand("-c", "log.ShowSignature=false", "show", "-s", "--pretty=format:%b", sha) - output, err := run.PrepareCmd(showCmd).Output() +func lookupCommit(sha, format string) ([]byte, error) { + logCmd, err := GitCommand("-c", "log.ShowSignature=false", "show", "-s", "--pretty=format:"+format, sha) if err != nil { - return "", err + return nil, err } - return string(output), nil + return run.PrepareCmd(logCmd).Output() +} + +func LastCommit() (*Commit, error) { + output, err := lookupCommit("HEAD", "%H,%s") + if err != nil { + return nil, err + } + + idx := bytes.IndexByte(output, ',') + return &Commit{ + Sha: string(output[0:idx]), + Title: strings.TrimSpace(string(output[idx+1:])), + }, nil +} + +func CommitBody(sha string) (string, error) { + output, err := lookupCommit(sha, "%b") + return string(output), err } // Push publishes a git ref to a remote and sets up upstream configuration func Push(remote string, ref string, cmdOut, cmdErr io.Writer) error { - pushCmd := GitCommand("push", "--set-upstream", remote, ref) + pushCmd, err := GitCommand("push", "--set-upstream", remote, ref) + if err != nil { + return err + } pushCmd.Stdout = cmdOut pushCmd.Stderr = cmdErr return run.PrepareCmd(pushCmd).Run() @@ -179,7 +241,10 @@ type BranchConfig struct { // ReadBranchConfig parses the `branch.BRANCH.(remote|merge)` part of git config func ReadBranchConfig(branch string) (cfg BranchConfig) { prefix := regexp.QuoteMeta(fmt.Sprintf("branch.%s.", branch)) - configCmd := GitCommand("config", "--get-regexp", fmt.Sprintf("^%s(remote|merge)$", prefix)) + configCmd, err := GitCommand("config", "--get-regexp", fmt.Sprintf("^%s(remote|merge)$", prefix)) + if err != nil { + return + } output, err := run.PrepareCmd(configCmd).Output() if err != nil { return @@ -209,21 +274,28 @@ func ReadBranchConfig(branch string) (cfg BranchConfig) { } func DeleteLocalBranch(branch string) error { - branchCmd := GitCommand("branch", "-D", branch) - err := run.PrepareCmd(branchCmd).Run() - return err + branchCmd, err := GitCommand("branch", "-D", branch) + if err != nil { + return err + } + return run.PrepareCmd(branchCmd).Run() } func HasLocalBranch(branch string) bool { - configCmd := GitCommand("rev-parse", "--verify", "refs/heads/"+branch) - _, err := run.PrepareCmd(configCmd).Output() + configCmd, err := GitCommand("rev-parse", "--verify", "refs/heads/"+branch) + if err != nil { + return false + } + _, err = run.PrepareCmd(configCmd).Output() return err == nil } func CheckoutBranch(branch string) error { - configCmd := GitCommand("checkout", branch) - err := run.PrepareCmd(configCmd).Run() - return err + configCmd, err := GitCommand("checkout", branch) + if err != nil { + return err + } + return run.PrepareCmd(configCmd).Run() } func parseCloneArgs(extraArgs []string) (args []string, target string) { @@ -252,7 +324,10 @@ func RunClone(cloneURL string, args []string) (target string, err error) { cloneArgs = append([]string{"clone"}, cloneArgs...) - cloneCmd := GitCommand(cloneArgs...) + cloneCmd, err := GitCommand(cloneArgs...) + if err != nil { + return "", err + } cloneCmd.Stdin = os.Stdin cloneCmd.Stdout = os.Stdout cloneCmd.Stderr = os.Stderr @@ -261,8 +336,16 @@ func RunClone(cloneURL string, args []string) (target string, err error) { return } -func AddUpstreamRemote(upstreamURL, cloneDir string) error { - cloneCmd := GitCommand("-C", cloneDir, "remote", "add", "-f", "upstream", upstreamURL) +func AddUpstreamRemote(upstreamURL, cloneDir string, branches []string) error { + args := []string{"-C", cloneDir, "remote", "add"} + for _, branch := range branches { + args = append(args, "-t", branch) + } + args = append(args, "-f", "upstream", upstreamURL) + cloneCmd, err := GitCommand(args...) + if err != nil { + return err + } cloneCmd.Stdout = os.Stdout cloneCmd.Stderr = os.Stderr return run.PrepareCmd(cloneCmd).Run() @@ -274,12 +357,30 @@ func isFilesystemPath(p string) bool { // ToplevelDir returns the top-level directory path of the current repository func ToplevelDir() (string, error) { - showCmd := exec.Command("git", "rev-parse", "--show-toplevel") + showCmd, err := GitCommand("rev-parse", "--show-toplevel") + if err != nil { + return "", err + } output, err := run.PrepareCmd(showCmd).Output() return firstLine(output), err } +func PathFromRepoRoot() string { + showCmd, err := GitCommand("rev-parse", "--show-prefix") + if err != nil { + return "" + } + output, err := run.PrepareCmd(showCmd).Output() + if err != nil { + return "" + } + if path := firstLine(output); path != "" { + return path[:len(path)-1] + } + return "" +} + func outputLines(output []byte) []string { lines := strings.TrimSuffix(string(output), "\n") return strings.Split(lines, "\n") diff --git a/git/git_test.go b/git/git_test.go index 34cc9c7cb3c..979a5e24322 100644 --- a/git/git_test.go +++ b/git/git_test.go @@ -1,14 +1,53 @@ package git import ( - "os/exec" + "os" "reflect" "testing" - "github.com/cli/cli/internal/run" - "github.com/cli/cli/test" + "github.com/cli/cli/v2/internal/run" ) +func setGitDir(t *testing.T, dir string) { + // TODO: also set XDG_CONFIG_HOME, GIT_CONFIG_NOSYSTEM + old_GIT_DIR := os.Getenv("GIT_DIR") + os.Setenv("GIT_DIR", dir) + t.Cleanup(func() { + os.Setenv("GIT_DIR", old_GIT_DIR) + }) +} + +func TestLastCommit(t *testing.T) { + setGitDir(t, "./fixtures/simple.git") + c, err := LastCommit() + if err != nil { + t.Fatalf("LastCommit error: %v", err) + } + if c.Sha != "6f1a2405cace1633d89a79c74c65f22fe78f9659" { + t.Errorf("expected sha %q, got %q", "6f1a2405cace1633d89a79c74c65f22fe78f9659", c.Sha) + } + if c.Title != "Second commit" { + t.Errorf("expected title %q, got %q", "Second commit", c.Title) + } +} + +func TestCommitBody(t *testing.T) { + setGitDir(t, "./fixtures/simple.git") + body, err := CommitBody("6f1a2405cace1633d89a79c74c65f22fe78f9659") + if err != nil { + t.Fatalf("CommitBody error: %v", err) + } + if body != "I'm starting to get the hang of things\n" { + t.Errorf("expected %q, got %q", "I'm starting to get the hang of things\n", body) + } +} + +/* + NOTE: below this are stubbed git tests, i.e. those that do not actually invoke `git`. If possible, utilize + `setGitDir()` to allow new tests to interact with `git`. For write operations, you can use `t.TempDir()` to + host a temporary git repository that is safe to be changed. +*/ + func Test_UncommittedChangeCount(t *testing.T) { type c struct { Label string @@ -21,20 +60,17 @@ func Test_UncommittedChangeCount(t *testing.T) { {Label: "untracked file", Expected: 2, Output: " M poem.txt\n?? new.txt"}, } - teardown := run.SetPrepareCmd(func(*exec.Cmd) run.Runnable { - return &test.OutputStub{} - }) - defer teardown() - for _, v := range cases { - _ = run.SetPrepareCmd(func(*exec.Cmd) run.Runnable { - return &test.OutputStub{Out: []byte(v.Output)} + t.Run(v.Label, func(t *testing.T) { + cs, restore := run.Stub() + defer restore(t) + cs.Register(`git status --porcelain`, 0, v.Output) + + ucc, _ := UncommittedChangeCount() + if ucc != v.Expected { + t.Errorf("UncommittedChangeCount() = %d, expected %d", ucc, v.Expected) + } }) - ucc, _ := UncommittedChangeCount() - - if ucc != v.Expected { - t.Errorf("got unexpected ucc value: %d for case %s", ucc, v.Label) - } } } @@ -59,59 +95,32 @@ func Test_CurrentBranch(t *testing.T) { } for _, v := range cases { - cs, teardown := test.InitCmdStubber() - cs.Stub(v.Stub) + cs, teardown := run.Stub() + cs.Register(`git symbolic-ref --quiet HEAD`, 0, v.Stub) result, err := CurrentBranch() if err != nil { - t.Errorf("got unexpected error: %w", err) - } - if len(cs.Calls) != 1 { - t.Errorf("expected 1 git call, saw %d", len(cs.Calls)) + t.Errorf("got unexpected error: %v", err) } if result != v.Expected { t.Errorf("unexpected branch name: %s instead of %s", result, v.Expected) } - teardown() + teardown(t) } } func Test_CurrentBranch_detached_head(t *testing.T) { - cs, teardown := test.InitCmdStubber() - defer teardown() - - cs.StubError("") + cs, teardown := run.Stub() + defer teardown(t) + cs.Register(`git symbolic-ref --quiet HEAD`, 1, "") _, err := CurrentBranch() if err == nil { - t.Errorf("expected an error") + t.Fatal("expected an error, got nil") } if err != ErrNotOnAnyBranch { t.Errorf("got unexpected error: %s instead of %s", err, ErrNotOnAnyBranch) } - if len(cs.Calls) != 1 { - t.Errorf("expected 1 git call, saw %d", len(cs.Calls)) - } -} - -func Test_CurrentBranch_unexpected_error(t *testing.T) { - cs, teardown := test.InitCmdStubber() - defer teardown() - - cs.StubError("lol") - - expectedError := "lol\nstub: lol" - - _, err := CurrentBranch() - if err == nil { - t.Errorf("expected an error") - } - if err.Error() != expectedError { - t.Errorf("got unexpected error: %s instead of %s", err.Error(), expectedError) - } - if len(cs.Calls) != 1 { - t.Errorf("expected 1 git call, saw %d", len(cs.Calls)) - } } func TestParseExtraCloneArgs(t *testing.T) { @@ -170,5 +179,42 @@ func TestParseExtraCloneArgs(t *testing.T) { } }) } +} +func TestAddUpstreamRemote(t *testing.T) { + tests := []struct { + name string + upstreamURL string + cloneDir string + branches []string + want string + }{ + { + name: "fetch all", + upstreamURL: "URL", + cloneDir: "DIRECTORY", + branches: []string{}, + want: "git -C DIRECTORY remote add -f upstream URL", + }, + { + name: "fetch specific branches only", + upstreamURL: "URL", + cloneDir: "DIRECTORY", + branches: []string{"master", "dev"}, + want: "git -C DIRECTORY remote add -t master -t dev -f upstream URL", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cs, cmdTeardown := run.Stub() + defer cmdTeardown(t) + + cs.Register(tt.want, 0, "") + + err := AddUpstreamRemote(tt.upstreamURL, tt.cloneDir, tt.branches) + if err != nil { + t.Fatalf("error running command `git remote add -f`: %v", err) + } + }) + } } diff --git a/git/remote.go b/git/remote.go index 77e550c37fd..f9dfbc4bd74 100644 --- a/git/remote.go +++ b/git/remote.go @@ -3,11 +3,10 @@ package git import ( "fmt" "net/url" - "os/exec" "regexp" "strings" - "github.com/cli/cli/internal/run" + "github.com/cli/cli/v2/internal/run" ) var remoteRE = regexp.MustCompile(`(.+)\s+(.+)\s+\((push|fetch)\)`) @@ -45,7 +44,10 @@ func Remotes() (RemoteSet, error) { remotes := parseRemotes(list) // this is affected by SetRemoteResolution - remoteCmd := exec.Command("git", "config", "--get-regexp", `^remote\..*\.gh-resolved$`) + remoteCmd, err := GitCommand("config", "--get-regexp", `^remote\..*\.gh-resolved$`) + if err != nil { + return nil, err + } output, _ := run.PrepareCmd(remoteCmd).Output() for _, l := range outputLines(output) { parts := strings.SplitN(l, " ", 2) @@ -107,8 +109,11 @@ func parseRemotes(gitRemotes []string) (remotes RemoteSet) { // AddRemote adds a new git remote and auto-fetches objects from it func AddRemote(name, u string) (*Remote, error) { - addCmd := exec.Command("git", "remote", "add", "-f", name, u) - err := run.PrepareCmd(addCmd).Run() + addCmd, err := GitCommand("remote", "add", "-f", name, u) + if err != nil { + return nil, err + } + err = run.PrepareCmd(addCmd).Run() if err != nil { return nil, err } @@ -136,6 +141,9 @@ func AddRemote(name, u string) (*Remote, error) { } func SetRemoteResolution(name, resolution string) error { - addCmd := exec.Command("git", "config", "--add", fmt.Sprintf("remote.%s.gh-resolved", name), resolution) + addCmd, err := GitCommand("config", "--add", fmt.Sprintf("remote.%s.gh-resolved", name), resolution) + if err != nil { + return err + } return run.PrepareCmd(addCmd).Run() } diff --git a/git/remote_test.go b/git/remote_test.go index 2e7d30cb622..38289659081 100644 --- a/git/remote_test.go +++ b/git/remote_test.go @@ -1,6 +1,10 @@ package git -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func Test_parseRemotes(t *testing.T) { remoteList := []string{ @@ -12,20 +16,20 @@ func Test_parseRemotes(t *testing.T) { "zardoz\thttps://example.com/zed.git (push)", } r := parseRemotes(remoteList) - eq(t, len(r), 4) + assert.Equal(t, 4, len(r)) - eq(t, r[0].Name, "mona") - eq(t, r[0].FetchURL.String(), "ssh://git@github.com/monalisa/myfork.git") + assert.Equal(t, "mona", r[0].Name) + assert.Equal(t, "ssh://git@github.com/monalisa/myfork.git", r[0].FetchURL.String()) if r[0].PushURL != nil { t.Errorf("expected no PushURL, got %q", r[0].PushURL) } - eq(t, r[1].Name, "origin") - eq(t, r[1].FetchURL.Path, "/monalisa/octo-cat.git") - eq(t, r[1].PushURL.Path, "/monalisa/octo-cat-push.git") + assert.Equal(t, "origin", r[1].Name) + assert.Equal(t, "/monalisa/octo-cat.git", r[1].FetchURL.Path) + assert.Equal(t, "/monalisa/octo-cat-push.git", r[1].PushURL.Path) - eq(t, r[2].Name, "upstream") - eq(t, r[2].FetchURL.Host, "example.com") - eq(t, r[2].PushURL.Host, "github.com") + assert.Equal(t, "upstream", r[2].Name) + assert.Equal(t, "example.com", r[2].FetchURL.Host) + assert.Equal(t, "github.com", r[2].PushURL.Host) - eq(t, r[3].Name, "zardoz") + assert.Equal(t, "zardoz", r[3].Name) } diff --git a/git/ssh_config.go b/git/ssh_config.go index 287298cd944..a4e234a02ad 100644 --- a/git/ssh_config.go +++ b/git/ssh_config.go @@ -9,19 +9,14 @@ import ( "regexp" "strings" - "github.com/mitchellh/go-homedir" + "github.com/cli/cli/v2/internal/config" ) var ( - sshHostRE, - sshTokenRE *regexp.Regexp + sshConfigLineRE = regexp.MustCompile(`\A\s*(?P[A-Za-z][A-Za-z0-9]*)(?:\s+|\s*=\s*)(?P.+)`) + sshTokenRE = regexp.MustCompile(`%[%h]`) ) -func init() { - sshHostRE = regexp.MustCompile("(?i)^[ \t]*(host|hostname)[ \t]+(.+)$") - sshTokenRE = regexp.MustCompile(`%[%h]`) -} - // SSHAliasMap encapsulates the translation of SSH hostname aliases type SSHAliasMap map[string]string @@ -45,55 +40,79 @@ func (m SSHAliasMap) Translator() func(*url.URL) *url.URL { } } -// ParseSSHConfig constructs a map of SSH hostname aliases based on user and -// system configuration files -func ParseSSHConfig() SSHAliasMap { - configFiles := []string{ - "/etc/ssh_config", - "/etc/ssh/ssh_config", - } - if homedir, err := homedir.Dir(); err == nil { - userConfig := filepath.Join(homedir, ".ssh", "config") - configFiles = append([]string{userConfig}, configFiles...) - } +type sshParser struct { + homeDir string - openFiles := make([]io.Reader, 0, len(configFiles)) - for _, file := range configFiles { - f, err := os.Open(file) + aliasMap SSHAliasMap + hosts []string + + open func(string) (io.Reader, error) + glob func(string) ([]string, error) +} + +func (p *sshParser) read(fileName string) error { + var file io.Reader + if p.open == nil { + f, err := os.Open(fileName) if err != nil { - continue + return err } defer f.Close() - openFiles = append(openFiles, f) + file = f + } else { + var err error + file, err = p.open(fileName) + if err != nil { + return err + } } - return sshParse(openFiles...) -} -func sshParse(r ...io.Reader) SSHAliasMap { - config := make(SSHAliasMap) - for _, file := range r { - _ = sshParseConfig(config, file) + if len(p.hosts) == 0 { + p.hosts = []string{"*"} } - return config -} -func sshParseConfig(c SSHAliasMap, file io.Reader) error { - hosts := []string{"*"} scanner := bufio.NewScanner(file) for scanner.Scan() { - line := scanner.Text() - match := sshHostRE.FindStringSubmatch(line) - if match == nil { + m := sshConfigLineRE.FindStringSubmatch(scanner.Text()) + if len(m) < 3 { continue } - names := strings.Fields(match[2]) - if strings.EqualFold(match[1], "host") { - hosts = names - } else { - for _, host := range hosts { - for _, name := range names { - c[host] = sshExpandTokens(name, host) + keyword, arguments := strings.ToLower(m[1]), m[2] + switch keyword { + case "host": + p.hosts = strings.Fields(arguments) + case "hostname": + for _, host := range p.hosts { + for _, name := range strings.Fields(arguments) { + if p.aliasMap == nil { + p.aliasMap = make(SSHAliasMap) + } + p.aliasMap[host] = sshExpandTokens(name, host) + } + } + case "include": + for _, arg := range strings.Fields(arguments) { + path := p.absolutePath(fileName, arg) + + var fileNames []string + if p.glob == nil { + paths, _ := filepath.Glob(path) + for _, p := range paths { + if s, err := os.Stat(p); err == nil && !s.IsDir() { + fileNames = append(fileNames, p) + } + } + } else { + var err error + fileNames, err = p.glob(path) + if err != nil { + continue + } + } + + for _, fileName := range fileNames { + _ = p.read(fileName) } } } @@ -102,6 +121,44 @@ func sshParseConfig(c SSHAliasMap, file io.Reader) error { return scanner.Err() } +func (p *sshParser) absolutePath(parentFile, path string) string { + if filepath.IsAbs(path) || strings.HasPrefix(filepath.ToSlash(path), "/") { + return path + } + + if strings.HasPrefix(path, "~") { + return filepath.Join(p.homeDir, strings.TrimPrefix(path, "~")) + } + + if strings.HasPrefix(filepath.ToSlash(parentFile), "/etc/ssh") { + return filepath.Join("/etc/ssh", path) + } + + return filepath.Join(p.homeDir, ".ssh", path) +} + +// ParseSSHConfig constructs a map of SSH hostname aliases based on user and +// system configuration files +func ParseSSHConfig() SSHAliasMap { + configFiles := []string{ + "/etc/ssh_config", + "/etc/ssh/ssh_config", + } + + p := sshParser{} + + if sshDir, err := config.HomeDirPath(".ssh"); err == nil { + userConfig := filepath.Join(sshDir, "config") + configFiles = append([]string{userConfig}, configFiles...) + p.homeDir = filepath.Dir(sshDir) + } + + for _, file := range configFiles { + _ = p.read(file) + } + return p.aliasMap +} + func sshExpandTokens(text, host string) string { return sshTokenRE.ReplaceAllStringFunc(text, func(match string) string { switch match { diff --git a/git/ssh_config_test.go b/git/ssh_config_test.go index 28f339aa65b..f05ca303b9e 100644 --- a/git/ssh_config_test.go +++ b/git/ssh_config_test.go @@ -1,31 +1,127 @@ package git import ( + "bytes" + "fmt" + "io" "net/url" - "reflect" - "strings" + "path/filepath" "testing" + + "github.com/MakeNowJust/heredoc" ) -// TODO: extract assertion helpers into a shared package -func eq(t *testing.T, got interface{}, expected interface{}) { - t.Helper() - if !reflect.DeepEqual(got, expected) { - t.Errorf("expected: %v, got: %v", expected, got) +func Test_sshParser_read(t *testing.T) { + testFiles := map[string]string{ + "/etc/ssh/config": heredoc.Doc(` + Include sites/* + `), + "/etc/ssh/sites/cfg1": heredoc.Doc(` + Host s1 + Hostname=site1.net + `), + "/etc/ssh/sites/cfg2": heredoc.Doc(` + Host s2 + Hostname = site2.net + `), + "HOME/.ssh/config": heredoc.Doc(` + Host * + Host gh gittyhubby + Hostname github.com + #Hostname example.com + Host ex + Include ex_config/* + `), + "HOME/.ssh/ex_config/ex_cfg": heredoc.Doc(` + Hostname example.com + `), + } + globResults := map[string][]string{ + "/etc/ssh/sites/*": {"/etc/ssh/sites/cfg1", "/etc/ssh/sites/cfg2"}, + "HOME/.ssh/ex_config/*": {"HOME/.ssh/ex_config/ex_cfg"}, + } + + p := &sshParser{ + homeDir: "HOME", + open: func(s string) (io.Reader, error) { + if contents, ok := testFiles[filepath.ToSlash(s)]; ok { + return bytes.NewBufferString(contents), nil + } else { + return nil, fmt.Errorf("no test file stub found: %q", s) + } + }, + glob: func(p string) ([]string, error) { + if results, ok := globResults[filepath.ToSlash(p)]; ok { + return results, nil + } else { + return nil, fmt.Errorf("no glob stubs found: %q", p) + } + }, + } + + if err := p.read("/etc/ssh/config"); err != nil { + t.Fatalf("read(global config) = %v", err) + } + if err := p.read("HOME/.ssh/config"); err != nil { + t.Fatalf("read(user config) = %v", err) + } + + if got := p.aliasMap["gh"]; got != "github.com" { + t.Errorf("expected alias %q to expand to %q, got %q", "gh", "github.com", got) + } + if got := p.aliasMap["gittyhubby"]; got != "github.com" { + t.Errorf("expected alias %q to expand to %q, got %q", "gittyhubby", "github.com", got) + } + if got := p.aliasMap["example.com"]; got != "" { + t.Errorf("expected alias %q to expand to %q, got %q", "example.com", "", got) + } + if got := p.aliasMap["ex"]; got != "example.com" { + t.Errorf("expected alias %q to expand to %q, got %q", "ex", "example.com", got) + } + if got := p.aliasMap["s1"]; got != "site1.net" { + t.Errorf("expected alias %q to expand to %q, got %q", "s1", "site1.net", got) } } -func Test_sshParse(t *testing.T) { - m := sshParse(strings.NewReader(` - Host foo bar - HostName example.com - `), strings.NewReader(` - Host bar baz - hostname %%%h.net%% - `)) - eq(t, m["foo"], "example.com") - eq(t, m["bar"], "%bar.net%") - eq(t, m["nonexist"], "") +func Test_sshParser_absolutePath(t *testing.T) { + dir := "HOME" + p := &sshParser{homeDir: dir} + + tests := map[string]struct { + parentFile string + arg string + want string + wantErr bool + }{ + "absolute path": { + parentFile: "/etc/ssh/ssh_config", + arg: "/etc/ssh/config", + want: "/etc/ssh/config", + }, + "system relative path": { + parentFile: "/etc/ssh/config", + arg: "configs/*.conf", + want: filepath.Join("/etc", "ssh", "configs", "*.conf"), + }, + "user relative path": { + parentFile: filepath.Join(dir, ".ssh", "ssh_config"), + arg: "configs/*.conf", + want: filepath.Join(dir, ".ssh", "configs/*.conf"), + }, + "shell-like ~ rerefence": { + parentFile: filepath.Join(dir, ".ssh", "ssh_config"), + arg: "~/.ssh/*.conf", + want: filepath.Join(dir, ".ssh", "*.conf"), + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + if got := p.absolutePath(tt.parentFile, tt.arg); got != tt.want { + t.Errorf("absolutePath(): %q, wants %q", got, tt.want) + } + }) + } } func Test_Translator(t *testing.T) { diff --git a/git/url.go b/git/url.go index 7d4aac4d77e..1a3e97fd62c 100644 --- a/git/url.go +++ b/git/url.go @@ -14,6 +14,7 @@ func isSupportedProtocol(u string) bool { strings.HasPrefix(u, "git+ssh:") || strings.HasPrefix(u, "git:") || strings.HasPrefix(u, "http:") || + strings.HasPrefix(u, "git+https:") || strings.HasPrefix(u, "https:") } @@ -43,6 +44,10 @@ func ParseURL(rawURL string) (u *url.URL, err error) { u.Scheme = "ssh" } + if u.Scheme == "git+https" { + u.Scheme = "https" + } + if u.Scheme != "ssh" { return } diff --git a/git/url_test.go b/git/url_test.go index 679739b4111..f5b3b50d07b 100644 --- a/git/url_test.go +++ b/git/url_test.go @@ -28,11 +28,26 @@ func TestIsURL(t *testing.T) { url: "git://example.com/owner/repo", want: true, }, + { + name: "git with extension", + url: "git://example.com/owner/repo.git", + want: true, + }, + { + name: "git+ssh", + url: "git+ssh://git@example.com/owner/repo.git", + want: true, + }, { name: "https", url: "https://example.com/owner/repo.git", want: true, }, + { + name: "git+https", + url: "git+https://example.com/owner/repo.git", + want: true, + }, { name: "no protocol", url: "example.com/owner/repo", @@ -121,6 +136,16 @@ func TestParseURL(t *testing.T) { Path: "/owner/repo.git", }, }, + { + name: "git+https", + url: "git+https://example.com/owner/repo.git", + want: url{ + Scheme: "https", + User: "", + Host: "example.com", + Path: "/owner/repo.git", + }, + }, { name: "scp-like", url: "git@example.com:owner/repo.git", diff --git a/go.mod b/go.mod index 3e56a01702b..5e4ef374e5a 100644 --- a/go.mod +++ b/go.mod @@ -1,34 +1,49 @@ -module github.com/cli/cli +module github.com/cli/cli/v2 -go 1.13 +go 1.16 require ( - github.com/AlecAivazis/survey/v2 v2.1.1 + github.com/AlecAivazis/survey/v2 v2.3.2 github.com/MakeNowJust/heredoc v1.0.0 - github.com/briandowns/spinner v1.11.1 - github.com/charmbracelet/glamour v0.2.1-0.20200724174618-1246d13c1684 - github.com/cpuguy83/go-md2man/v2 v2.0.0 - github.com/enescakir/emoji v1.0.0 - github.com/google/go-cmp v0.5.2 + github.com/briandowns/spinner v1.13.0 + github.com/charmbracelet/glamour v0.3.0 + github.com/cli/browser v1.1.0 + github.com/cli/oauth v0.8.0 + github.com/cli/safeexec v1.0.0 + github.com/cpuguy83/go-md2man/v2 v2.0.1 + github.com/creack/pty v1.1.16 + github.com/fatih/camelcase v1.0.0 + github.com/gabriel-vasile/mimetype v1.4.0 + github.com/google/go-cmp v0.5.6 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/hashicorp/go-version v1.2.1 + github.com/gorilla/websocket v1.4.2 + github.com/hashicorp/go-version v1.3.0 github.com/henvic/httpretty v0.0.6 + github.com/itchyny/gojq v0.12.5 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 - github.com/mattn/go-colorable v0.1.7 - github.com/mattn/go-isatty v0.0.12 - github.com/mattn/go-runewidth v0.0.9 + github.com/mattn/go-colorable v0.1.11 + github.com/mattn/go-isatty v0.0.14 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d - github.com/mitchellh/go-homedir v1.1.0 - github.com/muesli/termenv v0.7.2 - github.com/rivo/uniseg v0.1.0 - github.com/shurcooL/githubv4 v0.0.0-20200802174311-f27d2ca7f6d5 + github.com/muesli/reflow v0.2.1-0.20210502190812-c80126ec2ad5 + github.com/muesli/termenv v0.9.0 + github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38 + github.com/olekukonko/tablewriter v0.0.5 + github.com/opentracing/opentracing-go v1.1.0 + github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f - github.com/spf13/cobra v1.1.1 + github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 + github.com/sourcegraph/jsonrpc2 v0.1.0 + github.com/spf13/cobra v1.2.1 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.6.1 - golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a - golang.org/x/text v0.3.3 // indirect - gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 + github.com/stretchr/objx v0.1.1 // indirect + github.com/stretchr/testify v1.7.0 + golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 + golang.org/x/term v0.0.0-20210503060354-a79de5458b56 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) replace github.com/shurcooL/graphql => github.com/cli/shurcooL-graphql v0.0.0-20200707151639-0f7232a2bf7e + +replace golang.org/x/crypto => github.com/cli/crypto v0.0.0-20210929142629-6be313f59b03 diff --git a/go.sum b/go.sum index b3659cb766b..d0843e74c0e 100644 --- a/go.sum +++ b/go.sum @@ -5,110 +5,194 @@ cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6A cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/AlecAivazis/survey/v2 v2.1.1 h1:LEMbHE0pLj75faaVEKClEX1TM4AJmmnOh9eimREzLWI= -github.com/AlecAivazis/survey/v2 v2.1.1/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/AlecAivazis/survey/v2 v2.3.2 h1:TqTB+aDDCLYhf9/bD2TwSO8u8jDSmMUd2SUVO4gCnU8= +github.com/AlecAivazis/survey/v2 v2.3.2/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= -github.com/alecthomas/chroma v0.7.3 h1:NfdAERMy+esYQs8OXk0I868/qDxxCEo7FMz1WIqMAeI= -github.com/alecthomas/chroma v0.7.3/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM= +github.com/alecthomas/chroma v0.8.2 h1:x3zkuE2lUk/RIekyAJ3XRqSCP4zwWDfcw/YJCuCAACg= +github.com/alecthomas/chroma v0.8.2/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/briandowns/spinner v1.11.1 h1:OixPqDEcX3juo5AjQZAnFPbeUA0jvkp2qzB5gOZJ/L0= -github.com/briandowns/spinner v1.11.1/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/charmbracelet/glamour v0.2.1-0.20200724174618-1246d13c1684 h1:YMyvXRstOQc7n6eneHfudVMbARSCmZ7EZGjtTkkeB3A= -github.com/charmbracelet/glamour v0.2.1-0.20200724174618-1246d13c1684/go.mod h1:UA27Kwj3QHialP74iU6C+Gpc8Y7IOAKupeKMLLBURWM= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/briandowns/spinner v1.13.0 h1:q/Y9LtpwtvL0CRzXrAMj0keVXqNhBYUFg6tBOUiY8ek= +github.com/briandowns/spinner v1.13.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/charmbracelet/glamour v0.3.0 h1:3H+ZrKlSg8s+WU6V7eF2eRVYt8lCueffbi7r2+ffGkc= +github.com/charmbracelet/glamour v0.3.0/go.mod h1:TzF0koPZhqq0YVBNL100cPHznAAjVj7fksX2RInwjGw= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q= +github.com/cli/browser v1.1.0 h1:xOZBfkfY9L9vMBgqb1YwRirGu6QFaQ5dP/vXt5ENSOY= +github.com/cli/browser v1.1.0/go.mod h1:HKMQAt9t12kov91Mn7RfZxyJQQgWgyS/3SZswlZ5iTI= +github.com/cli/crypto v0.0.0-20210929142629-6be313f59b03 h1:3f4uHLfWx4/WlnMPXGai03eoWAI+oGHJwr+5OXfxCr8= +github.com/cli/crypto v0.0.0-20210929142629-6be313f59b03/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +github.com/cli/oauth v0.8.0 h1:YTFgPXSTvvDUFti3tR4o6q7Oll2SnQ9ztLwCAn4/IOA= +github.com/cli/oauth v0.8.0/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4= +github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= +github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= github.com/cli/shurcooL-graphql v0.0.0-20200707151639-0f7232a2bf7e h1:aq/1jlmtZoS6nlSp3yLOTZQ50G+dzHdeRNENgE/iBew= github.com/cli/shurcooL-graphql v0.0.0-20200707151639-0f7232a2bf7e/go.mod h1:it23pLwxmz6OyM6I5O0ATIXQS1S190Nas26L5Kahp4U= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.16 h1:vfetlOf3A+9YKggibynnX9mnFjuSVvkRj+IWpcTSLEQ= +github.com/creack/pty v1.1.16/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/enescakir/emoji v1.0.0 h1:W+HsNql8swfCQFtioDGDHCHri8nudlK1n5p2rHCJoog= -github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkKp+WKFD0= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro= +github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4= -github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/graph-gophers/graphql-go v0.0.0-20200622220639-c1d9693c95a6/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -121,8 +205,8 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= -github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= +github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -135,19 +219,24 @@ github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTx github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/itchyny/go-flags v1.5.0/go.mod h1:lenkYuCobuxLBAd/HGFE4LRoW8D3B6iXRQfWYJ+MNbA= +github.com/itchyny/gojq v0.12.5 h1:6SJ1BQ1VAwJAlIvLSIZmqHP/RUEq3qfVWvsRxrqhsD0= +github.com/itchyny/gojq v0.12.5/go.mod h1:3e1hZXv+Kwvdp6V9HXpVrvddiHVApi5EDZwS+zLFeiE= +github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921iRkU= +github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -155,137 +244,135 @@ github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ= github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= -github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= -github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= -github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= -github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/microcosm-cc/bluemonday v1.0.6 h1:ZOvqHKtnx0fUpnbQm3m3zKFWE+DRC+XB1onh8JoEObE= +github.com/microcosm-cc/bluemonday v1.0.6/go.mod h1:HOT/6NaBlR0f9XlxD3zolN6Z3N8Lp4pvhp+jLS5ihnI= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/muesli/reflow v0.1.0 h1:oQdpLfO56lr5pgLvqD0TcjW85rDjSYSBVdiG1Ch1ddM= -github.com/muesli/reflow v0.1.0/go.mod h1:I9bWAt7QTg/que/qmUCJBGlj7wEq8OAFBjPNjc6xK4I= -github.com/muesli/termenv v0.6.0 h1:zxvzTBmo4ZcxhNGGWeMz+Tttm51eF5bmPjfy4MCRYlk= -github.com/muesli/termenv v0.6.0/go.mod h1:SohX91w6swWA4AYU+QmPx+aSgXhWO0juiyID9UZmbpA= -github.com/muesli/termenv v0.7.2 h1:r1raklL3uKE7rOvWgSenmEm2px+dnc33OTisZ8YR1fw= -github.com/muesli/termenv v0.7.2/go.mod h1:ct2L5N2lmix82RaY3bMWwVu/jUFc9Ule0KGDCiKYPh8= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= -github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/muesli/reflow v0.2.0/go.mod h1:qT22vjVmM9MIUeLgsVYe/Ye7eZlbv9dZjL3dVhUqLX8= +github.com/muesli/reflow v0.2.1-0.20210502190812-c80126ec2ad5 h1:T+Fc6qGlSfM+z0JPlp+n5rijvlg6C6JYFSNaqnCifDU= +github.com/muesli/reflow v0.2.1-0.20210502190812-c80126ec2ad5/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= +github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/fQSFKo0= +github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8= +github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= +github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38 h1:0FrBxrkJ0hVembTb/e4EU5Ml6vLcOusAqymmYISg5Uo= +github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/shurcooL/githubv4 v0.0.0-20200802174311-f27d2ca7f6d5 h1:CA6Mjshr+g5YHENwllpQNR0UaYO7VGKo6TzJLM64WJQ= -github.com/shurcooL/githubv4 v0.0.0-20200802174311-f27d2ca7f6d5/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b h1:0/ecDXh/HTHRtSDSFnD2/Ta1yQ5J76ZspVY4u0/jGFk= +github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= -github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/sourcegraph/jsonrpc2 v0.1.0 h1:ohJHjZ+PcaLxDUjqk2NC3tIGsVa5bXThe1ZheSXOjuk= +github.com/sourcegraph/jsonrpc2 v0.1.0/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/yuin/goldmark v1.2.0 h1:WOOcyaJPlzb8fZ8TloxFe8QZkhOOJx87leDa9MIT9dc= -github.com/yuin/goldmark v1.2.0/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.3/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= +github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc= -golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -295,68 +382,151 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 h1:Ugb8sMTWuWRC3+sz5WeN/4kejDx9BvIwnPUiJBjJE+8= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 h1:0Ja1LBD+yisY6RWM/BH7TJVXWsSjs2VwBSmvSX4HdBc= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= +golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -366,6 +536,7 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -373,20 +544,75 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -396,27 +622,90 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/authflow/flow.go b/internal/authflow/flow.go index 896895d214c..a2875c5062f 100644 --- a/internal/authflow/flow.go +++ b/internal/authflow/flow.go @@ -8,11 +8,11 @@ import ( "os" "strings" - "github.com/cli/cli/api" - "github.com/cli/cli/auth" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/pkg/browser" - "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/oauth" ) var ( @@ -22,7 +22,12 @@ var ( oauthClientSecret = "34ddeff2b558a23d38fba8a6de74f086ede1cc0b" ) -func AuthFlowWithConfig(cfg config.Config, IO *iostreams.IOStreams, hostname, notice string, additionalScopes []string) (string, error) { +type iconfig interface { + Set(string, string, string) error + Write() error +} + +func AuthFlowWithConfig(cfg iconfig, IO *iostreams.IOStreams, hostname, notice string, additionalScopes []string) (string, error) { // TODO this probably shouldn't live in this package. It should probably be in a new package that // depends on both iostreams and config. stderr := IO.ErrOut @@ -58,58 +63,66 @@ func authFlow(oauthHost string, IO *iostreams.IOStreams, notice string, addition w := IO.ErrOut cs := IO.ColorScheme() - var verboseStream io.Writer - if strings.Contains(os.Getenv("DEBUG"), "oauth") { - verboseStream = w + httpClient := http.DefaultClient + if envDebug := os.Getenv("DEBUG"); envDebug != "" { + logTraffic := strings.Contains(envDebug, "api") || strings.Contains(envDebug, "oauth") + httpClient.Transport = api.VerboseLog(IO.ErrOut, logTraffic, IO.ColorEnabled())(httpClient.Transport) } minimumScopes := []string{"repo", "read:org", "gist"} scopes := append(minimumScopes, additionalScopes...) - flow := &auth.OAuthFlow{ + callbackURI := "http://127.0.0.1/callback" + if ghinstance.IsEnterprise(oauthHost) { + // the OAuth app on Enterprise hosts is still registered with a legacy callback URL + // see https://github.com/cli/cli/pull/222, https://github.com/cli/cli/pull/650 + callbackURI = "http://localhost/" + } + + flow := &oauth.Flow{ Hostname: oauthHost, ClientID: oauthClientID, ClientSecret: oauthClientSecret, + CallbackURI: callbackURI, Scopes: scopes, - WriteSuccessHTML: func(w io.Writer) { - fmt.Fprintln(w, oauthSuccessPage) + DisplayCode: func(code, verificationURL string) error { + fmt.Fprintf(w, "%s First copy your one-time code: %s\n", cs.Yellow("!"), cs.Bold(code)) + return nil }, - VerboseStream: verboseStream, - HTTPClient: http.DefaultClient, - OpenInBrowser: func(url, code string) error { - if code != "" { - fmt.Fprintf(w, "%s First copy your one-time code: %s\n", cs.Yellow("!"), cs.Bold(code)) - } + BrowseURL: func(url string) error { fmt.Fprintf(w, "- %s to open %s in your browser... ", cs.Bold("Press Enter"), oauthHost) _ = waitForEnter(IO.In) - browseCmd, err := browser.Command(url) - if err != nil { - return err - } - err = browseCmd.Run() - if err != nil { + // FIXME: read the browser from cmd Factory rather than recreating it + browser := cmdutil.NewBrowser(os.Getenv("BROWSER"), IO.Out, IO.ErrOut) + if err := browser.Browse(url); err != nil { fmt.Fprintf(w, "%s Failed opening a web browser at %s\n", cs.Red("!"), url) fmt.Fprintf(w, " %s\n", err) fmt.Fprint(w, " Please try entering the URL in your browser manually\n") } return nil }, + WriteSuccessHTML: func(w io.Writer) { + fmt.Fprintln(w, oauthSuccessPage) + }, + HTTPClient: httpClient, + Stdin: IO.In, + Stdout: w, } fmt.Fprintln(w, notice) - token, err := flow.ObtainAccessToken() + token, err := flow.DetectFlow() if err != nil { return "", "", err } - userLogin, err := getViewer(oauthHost, token) + userLogin, err := getViewer(oauthHost, token.Token) if err != nil { return "", "", err } - return token, userLogin, nil + return token.Token, userLogin, nil } func getViewer(hostname, token string) (string, error) { diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go new file mode 100644 index 00000000000..884e353f5aa --- /dev/null +++ b/internal/codespaces/api/api.go @@ -0,0 +1,617 @@ +package api + +// For descriptions of service interfaces, see: +// - https://online.visualstudio.com/api/swagger (for visualstudio.com) +// - https://docs.github.com/en/rest/reference/repos (for api.github.com) +// - https://github.com/github/github/blob/master/app/api/codespaces.rb (for vscs_internal) +// TODO(adonovan): replace the last link with a public doc URL when available. + +// TODO(adonovan): a possible reorganization would be to split this +// file into three internal packages, one per backend service, and to +// rename api.API to github.Client: +// +// - github.GetUser(github.Client) +// - github.GetRepository(Client) +// - github.ReadFile(Client, nwo, branch, path) // was GetCodespaceRepositoryContents +// - github.AuthorizedKeys(Client, user) +// - codespaces.Create(Client, user, repo, sku, branch, location) +// - codespaces.Delete(Client, user, token, name) +// - codespaces.Get(Client, token, owner, name) +// - codespaces.GetMachineTypes(Client, user, repo, branch, location) +// - codespaces.GetToken(Client, login, name) +// - codespaces.List(Client, user) +// - codespaces.Start(Client, token, codespace) +// - visualstudio.GetRegionLocation(http.Client) // no dependency on github +// +// This would make the meaning of each operation clearer. + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + "time" + + "github.com/cli/cli/v2/api" + "github.com/opentracing/opentracing-go" +) + +const githubAPI = "https://api.github.com" + +// API is the interface to the codespace service. +type API struct { + token string + client httpClient + githubAPI string +} + +type httpClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// New creates a new API client with the given token and HTTP client. +func New(token string, httpClient httpClient) *API { + return &API{ + token: token, + client: httpClient, + githubAPI: githubAPI, + } +} + +// User represents a GitHub user. +type User struct { + Login string `json:"login"` +} + +// GetUser returns the user associated with the given token. +func (a *API) GetUser(ctx context.Context) (*User, error) { + req, err := http.NewRequest(http.MethodGet, a.githubAPI+"/user", nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + a.setHeaders(req) + resp, err := a.do(ctx, req, "/user") + if err != nil { + return nil, fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, api.HandleHTTPError(resp) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + + var response User + if err := json.Unmarshal(b, &response); err != nil { + return nil, fmt.Errorf("error unmarshaling response: %w", err) + } + + return &response, nil +} + +// Repository represents a GitHub repository. +type Repository struct { + ID int `json:"id"` + FullName string `json:"full_name"` +} + +// GetRepository returns the repository associated with the given owner and name. +func (a *API) GetRepository(ctx context.Context, nwo string) (*Repository, error) { + req, err := http.NewRequest(http.MethodGet, a.githubAPI+"/repos/"+strings.ToLower(nwo), nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + a.setHeaders(req) + resp, err := a.do(ctx, req, "/repos/*") + if err != nil { + return nil, fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, api.HandleHTTPError(resp) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + + var response Repository + if err := json.Unmarshal(b, &response); err != nil { + return nil, fmt.Errorf("error unmarshaling response: %w", err) + } + + return &response, nil +} + +// Codespace represents a codespace. +type Codespace struct { + Name string `json:"name"` + CreatedAt string `json:"created_at"` + LastUsedAt string `json:"last_used_at"` + Owner User `json:"owner"` + Repository Repository `json:"repository"` + State string `json:"state"` + GitStatus CodespaceGitStatus `json:"git_status"` + Connection CodespaceConnection `json:"connection"` +} + +type CodespaceGitStatus struct { + Ahead int `json:"ahead"` + Behind int `json:"behind"` + Ref string `json:"ref"` + HasUnpushedChanges bool `json:"hasUnpushedChanges"` + HasUncommitedChanges bool `json:"hasUncommitedChanges"` +} + +const ( + // CodespaceStateAvailable is the state for a running codespace environment. + CodespaceStateAvailable = "Available" +) + +type CodespaceConnection struct { + SessionID string `json:"sessionId"` + SessionToken string `json:"sessionToken"` + RelayEndpoint string `json:"relayEndpoint"` + RelaySAS string `json:"relaySas"` + HostPublicKeys []string `json:"hostPublicKeys"` +} + +// ListCodespaces returns a list of codespaces for the user. Pass a negative limit to request all pages from +// the API until all codespaces have been fetched. +func (a *API) ListCodespaces(ctx context.Context, limit int) (codespaces []*Codespace, err error) { + perPage := 100 + if limit > 0 && limit < 100 { + perPage = limit + } + + listURL := fmt.Sprintf("%s/user/codespaces?per_page=%d", a.githubAPI, perPage) + for { + req, err := http.NewRequest(http.MethodGet, listURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + a.setHeaders(req) + + resp, err := a.do(ctx, req, "/user/codespaces") + if err != nil { + return nil, fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, api.HandleHTTPError(resp) + } + + var response struct { + Codespaces []*Codespace `json:"codespaces"` + } + dec := json.NewDecoder(resp.Body) + if err := dec.Decode(&response); err != nil { + return nil, fmt.Errorf("error unmarshaling response: %w", err) + } + + nextURL := findNextPage(resp.Header.Get("Link")) + codespaces = append(codespaces, response.Codespaces...) + + if nextURL == "" || (limit > 0 && len(codespaces) >= limit) { + break + } + + if newPerPage := limit - len(codespaces); limit > 0 && newPerPage < 100 { + u, _ := url.Parse(nextURL) + q := u.Query() + q.Set("per_page", strconv.Itoa(newPerPage)) + u.RawQuery = q.Encode() + listURL = u.String() + } else { + listURL = nextURL + } + } + + return codespaces, nil +} + +var linkRE = regexp.MustCompile(`<([^>]+)>;\s*rel="([^"]+)"`) + +func findNextPage(linkValue string) string { + for _, m := range linkRE.FindAllStringSubmatch(linkValue, -1) { + if len(m) > 2 && m[2] == "next" { + return m[1] + } + } + return "" +} + +// GetCodespace returns the user codespace based on the provided name. +// If the codespace is not found, an error is returned. +// If includeConnection is true, it will return the connection information for the codespace. +func (a *API) GetCodespace(ctx context.Context, codespaceName string, includeConnection bool) (*Codespace, error) { + req, err := http.NewRequest( + http.MethodGet, + a.githubAPI+"/user/codespaces/"+codespaceName, + nil, + ) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + if includeConnection { + q := req.URL.Query() + q.Add("internal", "true") + q.Add("refresh", "true") + req.URL.RawQuery = q.Encode() + } + + a.setHeaders(req) + resp, err := a.do(ctx, req, "/user/codespaces/*") + if err != nil { + return nil, fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, api.HandleHTTPError(resp) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + + var response Codespace + if err := json.Unmarshal(b, &response); err != nil { + return nil, fmt.Errorf("error unmarshaling response: %w", err) + } + + return &response, nil +} + +// StartCodespace starts a codespace for the user. +// If the codespace is already running, the returned error from the API is ignored. +func (a *API) StartCodespace(ctx context.Context, codespaceName string) error { + req, err := http.NewRequest( + http.MethodPost, + a.githubAPI+"/user/codespaces/"+codespaceName+"/start", + nil, + ) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + a.setHeaders(req) + resp, err := a.do(ctx, req, "/user/codespaces/*/start") + if err != nil { + return fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusConflict { + // 409 means the codespace is already running which we can safely ignore + return nil + } + return api.HandleHTTPError(resp) + } + + return nil +} + +func (a *API) StopCodespace(ctx context.Context, codespaceName string) error { + req, err := http.NewRequest( + http.MethodPost, + a.githubAPI+"/user/codespaces/"+codespaceName+"/stop", + nil, + ) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + a.setHeaders(req) + resp, err := a.do(ctx, req, "/user/codespaces/*/stop") + if err != nil { + return fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return api.HandleHTTPError(resp) + } + + return nil +} + +type getCodespaceRegionLocationResponse struct { + Current string `json:"current"` +} + +// GetCodespaceRegionLocation returns the closest codespace location for the user. +func (a *API) GetCodespaceRegionLocation(ctx context.Context) (string, error) { + req, err := http.NewRequest(http.MethodGet, "https://online.visualstudio.com/api/v1/locations", nil) + if err != nil { + return "", fmt.Errorf("error creating request: %w", err) + } + + resp, err := a.do(ctx, req, req.URL.String()) + if err != nil { + return "", fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", api.HandleHTTPError(resp) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("error reading response body: %w", err) + } + + var response getCodespaceRegionLocationResponse + if err := json.Unmarshal(b, &response); err != nil { + return "", fmt.Errorf("error unmarshaling response: %w", err) + } + + return response.Current, nil +} + +type Machine struct { + Name string `json:"name"` + DisplayName string `json:"display_name"` +} + +// GetCodespacesMachines returns the codespaces machines for the given repo, branch and location. +func (a *API) GetCodespacesMachines(ctx context.Context, repoID int, branch, location string) ([]*Machine, error) { + reqURL := fmt.Sprintf("%s/repositories/%d/codespaces/machines", a.githubAPI, repoID) + req, err := http.NewRequest(http.MethodGet, reqURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + q := req.URL.Query() + q.Add("location", location) + q.Add("ref", branch) + req.URL.RawQuery = q.Encode() + + a.setHeaders(req) + resp, err := a.do(ctx, req, "/repositories/*/codespaces/machines") + if err != nil { + return nil, fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, api.HandleHTTPError(resp) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + + var response struct { + Machines []*Machine `json:"machines"` + } + if err := json.Unmarshal(b, &response); err != nil { + return nil, fmt.Errorf("error unmarshaling response: %w", err) + } + + return response.Machines, nil +} + +// CreateCodespaceParams are the required parameters for provisioning a Codespace. +type CreateCodespaceParams struct { + RepositoryID int + Branch, Machine, Location string +} + +// CreateCodespace creates a codespace with the given parameters and returns a non-nil error if it +// fails to create. +func (a *API) CreateCodespace(ctx context.Context, params *CreateCodespaceParams) (*Codespace, error) { + codespace, err := a.startCreate(ctx, params.RepositoryID, params.Machine, params.Branch, params.Location) + if err != errProvisioningInProgress { + return codespace, err + } + + // errProvisioningInProgress indicates that codespace creation did not complete + // within the GitHub API RPC time limit (10s), so it continues asynchronously. + // We must poll the server to discover the outcome. + ctx, cancel := context.WithTimeout(ctx, 2*time.Minute) + defer cancel() + + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-ticker.C: + codespace, err = a.GetCodespace(ctx, codespace.Name, false) + if err != nil { + return nil, fmt.Errorf("failed to get codespace: %w", err) + } + + // we continue to poll until the codespace shows as provisioned + if codespace.State != CodespaceStateAvailable { + continue + } + + return codespace, nil + } + } +} + +type startCreateRequest struct { + RepositoryID int `json:"repository_id"` + Ref string `json:"ref"` + Location string `json:"location"` + Machine string `json:"machine"` +} + +var errProvisioningInProgress = errors.New("provisioning in progress") + +// startCreate starts the creation of a codespace. +// It may return success or an error, or errProvisioningInProgress indicating that the operation +// did not complete before the GitHub API's time limit for RPCs (10s), in which case the caller +// must poll the server to learn the outcome. +func (a *API) startCreate(ctx context.Context, repoID int, machine, branch, location string) (*Codespace, error) { + requestBody, err := json.Marshal(startCreateRequest{repoID, branch, location, machine}) + if err != nil { + return nil, fmt.Errorf("error marshaling request: %w", err) + } + + req, err := http.NewRequest(http.MethodPost, a.githubAPI+"/user/codespaces", bytes.NewBuffer(requestBody)) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + a.setHeaders(req) + resp, err := a.do(ctx, req, "/user/codespaces") + if err != nil { + return nil, fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusAccepted { + return nil, errProvisioningInProgress // RPC finished before result of creation known + } else if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + return nil, api.HandleHTTPError(resp) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + + var response Codespace + if err := json.Unmarshal(b, &response); err != nil { + return nil, fmt.Errorf("error unmarshaling response: %w", err) + } + + return &response, nil +} + +// DeleteCodespace deletes the given codespace. +func (a *API) DeleteCodespace(ctx context.Context, codespaceName string) error { + req, err := http.NewRequest(http.MethodDelete, a.githubAPI+"/user/codespaces/"+codespaceName, nil) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + a.setHeaders(req) + resp, err := a.do(ctx, req, "/user/codespaces/*") + if err != nil { + return fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted { + return api.HandleHTTPError(resp) + } + + return nil +} + +type getCodespaceRepositoryContentsResponse struct { + Content string `json:"content"` +} + +func (a *API) GetCodespaceRepositoryContents(ctx context.Context, codespace *Codespace, path string) ([]byte, error) { + req, err := http.NewRequest(http.MethodGet, a.githubAPI+"/repos/"+codespace.Repository.FullName+"/contents/"+path, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + q := req.URL.Query() + q.Add("ref", codespace.GitStatus.Ref) + req.URL.RawQuery = q.Encode() + + a.setHeaders(req) + resp, err := a.do(ctx, req, "/repos/*/contents/*") + if err != nil { + return nil, fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return nil, nil + } else if resp.StatusCode != http.StatusOK { + return nil, api.HandleHTTPError(resp) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + + var response getCodespaceRepositoryContentsResponse + if err := json.Unmarshal(b, &response); err != nil { + return nil, fmt.Errorf("error unmarshaling response: %w", err) + } + + decoded, err := base64.StdEncoding.DecodeString(response.Content) + if err != nil { + return nil, fmt.Errorf("error decoding content: %w", err) + } + + return decoded, nil +} + +// AuthorizedKeys returns the public keys (in ~/.ssh/authorized_keys +// format) registered by the specified GitHub user. +func (a *API) AuthorizedKeys(ctx context.Context, user string) ([]byte, error) { + url := fmt.Sprintf("https://github.com/%s.keys", user) + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + resp, err := a.do(ctx, req, "/user.keys") + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("server returned %s", resp.Status) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + return b, nil +} + +// do executes the given request and returns the response. It creates an +// opentracing span to track the length of the request. +func (a *API) do(ctx context.Context, req *http.Request, spanName string) (*http.Response, error) { + // TODO(adonovan): use NewRequestWithContext(ctx) and drop ctx parameter. + span, ctx := opentracing.StartSpanFromContext(ctx, spanName) + defer span.Finish() + req = req.WithContext(ctx) + return a.client.Do(req) +} + +// setHeaders sets the required headers for the API. +func (a *API) setHeaders(req *http.Request) { + if a.token != "" { + req.Header.Set("Authorization", "Bearer "+a.token) + } + req.Header.Set("Accept", "application/vnd.github.v3+json") +} diff --git a/internal/codespaces/api/api_test.go b/internal/codespaces/api/api_test.go new file mode 100644 index 00000000000..e8748fa9a11 --- /dev/null +++ b/internal/codespaces/api/api_test.go @@ -0,0 +1,118 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strconv" + "testing" +) + +func generateCodespaceList(start int, end int) []*Codespace { + codespacesList := []*Codespace{} + for i := start; i < end; i++ { + codespacesList = append(codespacesList, &Codespace{ + Name: fmt.Sprintf("codespace-%d", i), + }) + } + return codespacesList +} + +func createFakeListEndpointServer(t *testing.T, initalTotal int, finalTotal int) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/user/codespaces" { + t.Fatal("Incorrect path") + } + + page := 1 + if r.URL.Query().Get("page") != "" { + page, _ = strconv.Atoi(r.URL.Query().Get("page")) + } + + per_page := 0 + if r.URL.Query().Get("per_page") != "" { + per_page, _ = strconv.Atoi(r.URL.Query().Get("per_page")) + } + + response := struct { + Codespaces []*Codespace `json:"codespaces"` + TotalCount int `json:"total_count"` + }{ + Codespaces: []*Codespace{}, + TotalCount: finalTotal, + } + + switch page { + case 1: + response.Codespaces = generateCodespaceList(0, per_page) + response.TotalCount = initalTotal + w.Header().Set("Link", fmt.Sprintf(`; rel="last", ; rel="next"`, r.Host, per_page)) + case 2: + response.Codespaces = generateCodespaceList(per_page, per_page*2) + response.TotalCount = finalTotal + w.Header().Set("Link", fmt.Sprintf(`; rel="next"`, r.Host, per_page)) + case 3: + response.Codespaces = generateCodespaceList(per_page*2, per_page*3-per_page/2) + response.TotalCount = finalTotal + default: + t.Fatal("Should not check extra page") + } + + data, _ := json.Marshal(response) + fmt.Fprint(w, string(data)) + })) +} + +func TestListCodespaces_limited(t *testing.T) { + svr := createFakeListEndpointServer(t, 200, 200) + defer svr.Close() + + api := API{ + githubAPI: svr.URL, + client: &http.Client{}, + token: "faketoken", + } + ctx := context.TODO() + codespaces, err := api.ListCodespaces(ctx, 200) + if err != nil { + t.Fatal(err) + } + + if len(codespaces) != 200 { + t.Fatalf("expected 200 codespace, got %d", len(codespaces)) + } + if codespaces[0].Name != "codespace-0" { + t.Fatalf("expected codespace-0, got %s", codespaces[0].Name) + } + if codespaces[199].Name != "codespace-199" { + t.Fatalf("expected codespace-199, got %s", codespaces[0].Name) + } +} + +func TestListCodespaces_unlimited(t *testing.T) { + svr := createFakeListEndpointServer(t, 200, 200) + defer svr.Close() + + api := API{ + githubAPI: svr.URL, + client: &http.Client{}, + token: "faketoken", + } + ctx := context.TODO() + codespaces, err := api.ListCodespaces(ctx, -1) + if err != nil { + t.Fatal(err) + } + + if len(codespaces) != 250 { + t.Fatalf("expected 250 codespace, got %d", len(codespaces)) + } + if codespaces[0].Name != "codespace-0" { + t.Fatalf("expected codespace-0, got %s", codespaces[0].Name) + } + if codespaces[249].Name != "codespace-249" { + t.Fatalf("expected codespace-249, got %s", codespaces[0].Name) + } +} diff --git a/internal/codespaces/codespaces.go b/internal/codespaces/codespaces.go new file mode 100644 index 00000000000..7755e527292 --- /dev/null +++ b/internal/codespaces/codespaces.go @@ -0,0 +1,85 @@ +package codespaces + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/cli/cli/v2/internal/codespaces/api" + "github.com/cli/cli/v2/pkg/liveshare" +) + +type logger interface { + Print(v ...interface{}) (int, error) + Println(v ...interface{}) (int, error) +} + +// TODO(josebalius): clean this up once we standardrize +// logging for codespaces +type liveshareLogger interface { + Println(v ...interface{}) + Printf(f string, v ...interface{}) +} + +func connectionReady(codespace *api.Codespace) bool { + return codespace.Connection.SessionID != "" && + codespace.Connection.SessionToken != "" && + codespace.Connection.RelayEndpoint != "" && + codespace.Connection.RelaySAS != "" && + codespace.State == api.CodespaceStateAvailable +} + +type apiClient interface { + GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error) + StartCodespace(ctx context.Context, name string) error +} + +// ConnectToLiveshare waits for a Codespace to become running, +// and connects to it using a Live Share session. +func ConnectToLiveshare(ctx context.Context, log logger, sessionLogger liveshareLogger, apiClient apiClient, codespace *api.Codespace) (*liveshare.Session, error) { + var startedCodespace bool + if codespace.State != api.CodespaceStateAvailable { + startedCodespace = true + log.Print("Starting your codespace...") + if err := apiClient.StartCodespace(ctx, codespace.Name); err != nil { + return nil, fmt.Errorf("error starting codespace: %w", err) + } + } + + for retries := 0; !connectionReady(codespace); retries++ { + if retries > 1 { + if retries%2 == 0 { + log.Print(".") + } + + time.Sleep(1 * time.Second) + } + + if retries == 30 { + return nil, errors.New("timed out while waiting for the codespace to start") + } + + var err error + codespace, err = apiClient.GetCodespace(ctx, codespace.Name, true) + if err != nil { + return nil, fmt.Errorf("error getting codespace: %w", err) + } + } + + if startedCodespace { + fmt.Print("\n") + } + + log.Println("Connecting to your codespace...") + + return liveshare.Connect(ctx, liveshare.Options{ + ClientName: "gh", + SessionID: codespace.Connection.SessionID, + SessionToken: codespace.Connection.SessionToken, + RelaySAS: codespace.Connection.RelaySAS, + RelayEndpoint: codespace.Connection.RelayEndpoint, + HostPublicKeys: codespace.Connection.HostPublicKeys, + Logger: sessionLogger, + }) +} diff --git a/internal/codespaces/ssh.go b/internal/codespaces/ssh.go new file mode 100644 index 00000000000..36c8bf5b230 --- /dev/null +++ b/internal/codespaces/ssh.go @@ -0,0 +1,90 @@ +package codespaces + +import ( + "context" + "fmt" + "os" + "os/exec" + "strconv" + "strings" +) + +// Shell runs an interactive secure shell over an existing +// port-forwarding session. It runs until the shell is terminated +// (including by cancellation of the context). +func Shell(ctx context.Context, log logger, sshArgs []string, port int, destination string, usingCustomPort bool) error { + cmd, connArgs, err := newSSHCommand(ctx, port, destination, sshArgs) + if err != nil { + return fmt.Errorf("failed to create ssh command: %w", err) + } + + if usingCustomPort { + log.Println("Connection Details: ssh " + destination + " " + strings.Join(connArgs, " ")) + } + + return cmd.Run() +} + +// NewRemoteCommand returns an exec.Cmd that will securely run a shell +// command on the remote machine. +func NewRemoteCommand(ctx context.Context, tunnelPort int, destination string, sshArgs ...string) (*exec.Cmd, error) { + cmd, _, err := newSSHCommand(ctx, tunnelPort, destination, sshArgs) + return cmd, err +} + +// newSSHCommand populates an exec.Cmd to run a command (or if blank, +// an interactive shell) over ssh. +func newSSHCommand(ctx context.Context, port int, dst string, cmdArgs []string) (*exec.Cmd, []string, error) { + connArgs := []string{"-p", strconv.Itoa(port), "-o", "NoHostAuthenticationForLocalhost=yes"} + + // The ssh command syntax is: ssh [flags] user@host command [args...] + // There is no way to specify the user@host destination as a flag. + // Unfortunately, that means we need to know which user-provided words are + // SSH flags and which are command arguments so that we can place + // them before or after the destination, and that means we need to know all + // the flags and their arities. + cmdArgs, command, err := parseSSHArgs(cmdArgs) + if err != nil { + return nil, nil, err + } + + cmdArgs = append(cmdArgs, connArgs...) + cmdArgs = append(cmdArgs, "-C") // Compression + cmdArgs = append(cmdArgs, dst) // user@host + + if command != nil { + cmdArgs = append(cmdArgs, command...) + } + + cmd := exec.CommandContext(ctx, "ssh", cmdArgs...) + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + + return cmd, connArgs, nil +} + +// parseSSHArgs parses SSH arguments into two distinct slices of flags and command. +// It returns an error if a unary flag is provided without an argument. +func parseSSHArgs(args []string) (cmdArgs, command []string, err error) { + for i := 0; i < len(args); i++ { + arg := args[i] + + // if we've started parsing the command, set it to the rest of the args + if !strings.HasPrefix(arg, "-") { + command = args[i:] + break + } + + cmdArgs = append(cmdArgs, arg) + if len(arg) == 2 && strings.Contains("bcDeFIiLlmOopRSWw", arg[1:2]) { + if i++; i == len(args) { + return nil, nil, fmt.Errorf("ssh flag: %s requires an argument", arg) + } + + cmdArgs = append(cmdArgs, args[i]) + } + } + + return cmdArgs, command, nil +} diff --git a/internal/codespaces/ssh_test.go b/internal/codespaces/ssh_test.go new file mode 100644 index 00000000000..c804f600072 --- /dev/null +++ b/internal/codespaces/ssh_test.go @@ -0,0 +1,105 @@ +package codespaces + +import ( + "fmt" + "testing" +) + +func TestParseSSHArgs(t *testing.T) { + type testCase struct { + Args []string + ParsedArgs []string + Command []string + Error string + } + + testCases := []testCase{ + {}, // empty test case + { + Args: []string{"-X", "-Y"}, + ParsedArgs: []string{"-X", "-Y"}, + Command: nil, + }, + { + Args: []string{"-X", "-Y", "-o", "someoption=test"}, + ParsedArgs: []string{"-X", "-Y", "-o", "someoption=test"}, + Command: nil, + }, + { + Args: []string{"-X", "-Y", "-o", "someoption=test", "somecommand"}, + ParsedArgs: []string{"-X", "-Y", "-o", "someoption=test"}, + Command: []string{"somecommand"}, + }, + { + Args: []string{"-X", "-Y", "-o", "someoption=test", "echo", "test"}, + ParsedArgs: []string{"-X", "-Y", "-o", "someoption=test"}, + Command: []string{"echo", "test"}, + }, + { + Args: []string{"somecommand"}, + ParsedArgs: []string{}, + Command: []string{"somecommand"}, + }, + { + Args: []string{"echo", "test"}, + ParsedArgs: []string{}, + Command: []string{"echo", "test"}, + }, + { + Args: []string{"-v", "echo", "hello", "world"}, + ParsedArgs: []string{"-v"}, + Command: []string{"echo", "hello", "world"}, + }, + { + Args: []string{"-L", "-l"}, + ParsedArgs: []string{"-L", "-l"}, + Command: nil, + }, + { + Args: []string{"-v", "echo", "-n", "test"}, + ParsedArgs: []string{"-v"}, + Command: []string{"echo", "-n", "test"}, + }, + { + Args: []string{"-v", "echo", "-b", "test"}, + ParsedArgs: []string{"-v"}, + Command: []string{"echo", "-b", "test"}, + }, + { + Args: []string{"-b"}, + ParsedArgs: nil, + Command: nil, + Error: "ssh flag: -b requires an argument", + }, + } + + for _, tcase := range testCases { + args, command, err := parseSSHArgs(tcase.Args) + if tcase.Error != "" { + if err == nil { + t.Errorf("expected error and got nil: %#v", tcase) + } + + if err.Error() != tcase.Error { + t.Errorf("error does not match expected error, got: '%s', expected: '%s'", err.Error(), tcase.Error) + } + + continue + } + + if err != nil { + t.Errorf("unexpected error: %v on test case: %#v", err, tcase) + continue + } + + argsStr, parsedArgsStr := fmt.Sprintf("%s", args), fmt.Sprintf("%s", tcase.ParsedArgs) + if argsStr != parsedArgsStr { + t.Errorf("args do not match parsed args. got: '%s', expected: '%s'", argsStr, parsedArgsStr) + } + + commandStr, parsedCommandStr := fmt.Sprintf("%s", command), fmt.Sprintf("%s", tcase.Command) + if commandStr != parsedCommandStr { + t.Errorf("command does not match parsed command. got: '%s', expected: '%s'", commandStr, parsedCommandStr) + } + } +} diff --git a/internal/codespaces/states.go b/internal/codespaces/states.go new file mode 100644 index 00000000000..b686c188816 --- /dev/null +++ b/internal/codespaces/states.go @@ -0,0 +1,117 @@ +package codespaces + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net" + "strings" + "time" + + "github.com/cli/cli/v2/internal/codespaces/api" + "github.com/cli/cli/v2/pkg/liveshare" +) + +// PostCreateStateStatus is a string value representing the different statuses a state can have. +type PostCreateStateStatus string + +func (p PostCreateStateStatus) String() string { + return strings.Title(string(p)) +} + +const ( + PostCreateStateRunning PostCreateStateStatus = "running" + PostCreateStateSuccess PostCreateStateStatus = "succeeded" + PostCreateStateFailed PostCreateStateStatus = "failed" +) + +// PostCreateState is a combination of a state and status value that is captured +// during codespace creation. +type PostCreateState struct { + Name string `json:"name"` + Status PostCreateStateStatus `json:"status"` +} + +// PollPostCreateStates watches for state changes in a codespace, +// and calls the supplied poller for each batch of state changes. +// It runs until it encounters an error, including cancellation of the context. +func PollPostCreateStates(ctx context.Context, logger logger, apiClient apiClient, codespace *api.Codespace, poller func([]PostCreateState)) (err error) { + noopLogger := log.New(ioutil.Discard, "", 0) + + session, err := ConnectToLiveshare(ctx, logger, noopLogger, apiClient, codespace) + if err != nil { + return fmt.Errorf("connect to Live Share: %w", err) + } + defer func() { + if closeErr := session.Close(); err == nil { + err = closeErr + } + }() + + // Ensure local port is listening before client (getPostCreateOutput) connects. + listen, err := net.Listen("tcp", "127.0.0.1:0") // arbitrary port + if err != nil { + return err + } + localPort := listen.Addr().(*net.TCPAddr).Port + + logger.Println("Fetching SSH Details...") + remoteSSHServerPort, sshUser, err := session.StartSSHServer(ctx) + if err != nil { + return fmt.Errorf("error getting ssh server details: %w", err) + } + + tunnelClosed := make(chan error, 1) // buffered to avoid sender stuckness + go func() { + fwd := liveshare.NewPortForwarder(session, "sshd", remoteSSHServerPort, false) + tunnelClosed <- fwd.ForwardToListener(ctx, listen) // error is non-nil + }() + + t := time.NewTicker(1 * time.Second) + defer t.Stop() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + + case err := <-tunnelClosed: + return fmt.Errorf("connection failed: %w", err) + + case <-t.C: + states, err := getPostCreateOutput(ctx, localPort, sshUser) + if err != nil { + return fmt.Errorf("get post create output: %w", err) + } + + poller(states) + } + } +} + +func getPostCreateOutput(ctx context.Context, tunnelPort int, user string) ([]PostCreateState, error) { + cmd, err := NewRemoteCommand( + ctx, tunnelPort, fmt.Sprintf("%s@localhost", user), + "cat /workspaces/.codespaces/shared/postCreateOutput.json", + ) + if err != nil { + return nil, fmt.Errorf("remote command: %w", err) + } + + stdout := new(bytes.Buffer) + cmd.Stdout = stdout + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("run command: %w", err) + } + var output struct { + Steps []PostCreateState `json:"steps"` + } + if err := json.Unmarshal(stdout.Bytes(), &output); err != nil { + return nil, fmt.Errorf("unmarshal output: %w", err) + } + + return output.Steps, nil +} diff --git a/internal/config/config_file.go b/internal/config/config_file.go index d4c132360ee..f1f9a4d9cd4 100644 --- a/internal/config/config_file.go +++ b/internal/config/config_file.go @@ -3,31 +3,176 @@ package config import ( "errors" "fmt" - "io" "io/ioutil" "os" - "path" + "path/filepath" + "runtime" "syscall" - "github.com/mitchellh/go-homedir" "gopkg.in/yaml.v3" ) +const ( + GH_CONFIG_DIR = "GH_CONFIG_DIR" + XDG_CONFIG_HOME = "XDG_CONFIG_HOME" + XDG_STATE_HOME = "XDG_STATE_HOME" + XDG_DATA_HOME = "XDG_DATA_HOME" + APP_DATA = "AppData" + LOCAL_APP_DATA = "LocalAppData" +) + +// Config path precedence +// 1. GH_CONFIG_DIR +// 2. XDG_CONFIG_HOME +// 3. AppData (windows only) +// 4. HOME func ConfigDir() string { - dir, _ := homedir.Expand("~/.config/gh") - return dir + var path string + if a := os.Getenv(GH_CONFIG_DIR); a != "" { + path = a + } else if b := os.Getenv(XDG_CONFIG_HOME); b != "" { + path = filepath.Join(b, "gh") + } else if c := os.Getenv(APP_DATA); runtime.GOOS == "windows" && c != "" { + path = filepath.Join(c, "GitHub CLI") + } else { + d, _ := os.UserHomeDir() + path = filepath.Join(d, ".config", "gh") + } + + // If the path does not exist and the GH_CONFIG_DIR flag is not set try + // migrating config from default paths. + if !dirExists(path) && os.Getenv(GH_CONFIG_DIR) == "" { + _ = autoMigrateConfigDir(path) + } + + return path +} + +// State path precedence +// 1. XDG_CONFIG_HOME +// 2. LocalAppData (windows only) +// 3. HOME +func StateDir() string { + var path string + if a := os.Getenv(XDG_STATE_HOME); a != "" { + path = filepath.Join(a, "gh") + } else if b := os.Getenv(LOCAL_APP_DATA); runtime.GOOS == "windows" && b != "" { + path = filepath.Join(b, "GitHub CLI") + } else { + c, _ := os.UserHomeDir() + path = filepath.Join(c, ".local", "state", "gh") + } + + // If the path does not exist try migrating state from default paths + if !dirExists(path) { + _ = autoMigrateStateDir(path) + } + + return path +} + +// Data path precedence +// 1. XDG_DATA_HOME +// 2. LocalAppData (windows only) +// 3. HOME +func DataDir() string { + var path string + if a := os.Getenv(XDG_DATA_HOME); a != "" { + path = filepath.Join(a, "gh") + } else if b := os.Getenv(LOCAL_APP_DATA); runtime.GOOS == "windows" && b != "" { + path = filepath.Join(b, "GitHub CLI") + } else { + c, _ := os.UserHomeDir() + path = filepath.Join(c, ".local", "share", "gh") + } + + return path +} + +var errSamePath = errors.New("same path") +var errNotExist = errors.New("not exist") + +// Check default path, os.UserHomeDir, for existing configs +// If configs exist then move them to newPath +func autoMigrateConfigDir(newPath string) error { + path, err := os.UserHomeDir() + if oldPath := filepath.Join(path, ".config", "gh"); err == nil && dirExists(oldPath) { + return migrateDir(oldPath, newPath) + } + + return errNotExist +} + +// Check default path, os.UserHomeDir, for existing state file (state.yml) +// If state file exist then move it to newPath +func autoMigrateStateDir(newPath string) error { + path, err := os.UserHomeDir() + if oldPath := filepath.Join(path, ".config", "gh"); err == nil && dirExists(oldPath) { + return migrateFile(oldPath, newPath, "state.yml") + } + + return errNotExist +} + +func migrateFile(oldPath, newPath, file string) error { + if oldPath == newPath { + return errSamePath + } + + oldFile := filepath.Join(oldPath, file) + newFile := filepath.Join(newPath, file) + + if !fileExists(oldFile) { + return errNotExist + } + + _ = os.MkdirAll(filepath.Dir(newFile), 0755) + return os.Rename(oldFile, newFile) +} + +func migrateDir(oldPath, newPath string) error { + if oldPath == newPath { + return errSamePath + } + + if !dirExists(oldPath) { + return errNotExist + } + + _ = os.MkdirAll(filepath.Dir(newPath), 0755) + return os.Rename(oldPath, newPath) +} + +func dirExists(path string) bool { + f, err := os.Stat(path) + return err == nil && f.IsDir() +} + +func fileExists(path string) bool { + f, err := os.Stat(path) + return err == nil && !f.IsDir() } func ConfigFile() string { - return path.Join(ConfigDir(), "config.yml") + return filepath.Join(ConfigDir(), "config.yml") } -func hostsConfigFile(filename string) string { - return path.Join(path.Dir(filename), "hosts.yml") +func HostsConfigFile() string { + return filepath.Join(ConfigDir(), "hosts.yml") } func ParseDefaultConfig() (Config, error) { - return ParseConfig(ConfigFile()) + return parseConfig(ConfigFile()) +} + +func HomeDirPath(subdir string) (string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", err + } + + newPath := filepath.Join(homeDir, subdir) + return newPath, nil } var ReadConfigFile = func(filename string) ([]byte, error) { @@ -46,7 +191,7 @@ var ReadConfigFile = func(filename string) ([]byte, error) { } var WriteConfigFile = func(filename string, data []byte) error { - err := os.MkdirAll(path.Dir(filename), 0771) + err := os.MkdirAll(filepath.Dir(filename), 0771) if err != nil { return pathError(err) } @@ -57,11 +202,7 @@ var WriteConfigFile = func(filename string, data []byte) error { } defer cfgFile.Close() - n, err := cfgFile.Write(data) - if err == nil && n < len(data) { - err = io.ErrShortWrite - } - + _, err = cfgFile.Write(data) return err } @@ -144,7 +285,7 @@ func migrateConfig(filename string) error { return cfg.Write() } -func ParseConfig(filename string) (Config, error) { +func parseConfig(filename string) (Config, error) { _, root, err := parseConfigFile(filename) if err != nil { if os.IsNotExist(err) { @@ -165,7 +306,7 @@ func ParseConfig(filename string) (Config, error) { return nil, fmt.Errorf("failed to reparse migrated config: %w", err) } } else { - if _, hostsRoot, err := parseConfigFile(hostsConfigFile(filename)); err == nil { + if _, hostsRoot, err := parseConfigFile(HostsConfigFile()); err == nil { if len(hostsRoot.Content[0].Content) > 0 { newContent := []*yaml.Node{ {Value: "hosts"}, @@ -198,7 +339,7 @@ func findRegularFile(p string) string { if s, err := os.Stat(p); err == nil && s.Mode().IsRegular() { return p } - newPath := path.Dir(p) + newPath := filepath.Dir(p) if newPath == p || newPath == "/" || newPath == "." { break } diff --git a/internal/config/config_file_test.go b/internal/config/config_file_test.go index f40cb9097a5..4c35f24f926 100644 --- a/internal/config/config_file_test.go +++ b/internal/config/config_file_test.go @@ -3,39 +3,35 @@ package config import ( "bytes" "fmt" - "reflect" + "io/ioutil" + "os" + "path/filepath" + "runtime" "testing" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" ) -func eq(t *testing.T, got interface{}, expected interface{}) { - t.Helper() - if !reflect.DeepEqual(got, expected) { - t.Errorf("expected: %v, got: %v", expected, got) - } -} - func Test_parseConfig(t *testing.T) { - defer StubConfig(`--- + defer stubConfig(`--- hosts: github.com: user: monalisa oauth_token: OTOKEN `, "")() - config, err := ParseConfig("config.yml") - eq(t, err, nil) + config, err := parseConfig("config.yml") + assert.NoError(t, err) user, err := config.Get("github.com", "user") - eq(t, err, nil) - eq(t, user, "monalisa") + assert.NoError(t, err) + assert.Equal(t, "monalisa", user) token, err := config.Get("github.com", "oauth_token") - eq(t, err, nil) - eq(t, token, "OTOKEN") + assert.NoError(t, err) + assert.Equal(t, "OTOKEN", token) } func Test_parseConfig_multipleHosts(t *testing.T) { - defer StubConfig(`--- + defer stubConfig(`--- hosts: example.com: user: wronguser @@ -44,34 +40,34 @@ hosts: user: monalisa oauth_token: OTOKEN `, "")() - config, err := ParseConfig("config.yml") - eq(t, err, nil) + config, err := parseConfig("config.yml") + assert.NoError(t, err) user, err := config.Get("github.com", "user") - eq(t, err, nil) - eq(t, user, "monalisa") + assert.NoError(t, err) + assert.Equal(t, "monalisa", user) token, err := config.Get("github.com", "oauth_token") - eq(t, err, nil) - eq(t, token, "OTOKEN") + assert.NoError(t, err) + assert.Equal(t, "OTOKEN", token) } func Test_parseConfig_hostsFile(t *testing.T) { - defer StubConfig("", `--- + defer stubConfig("", `--- github.com: user: monalisa oauth_token: OTOKEN `)() - config, err := ParseConfig("config.yml") - eq(t, err, nil) + config, err := parseConfig("config.yml") + assert.NoError(t, err) user, err := config.Get("github.com", "user") - eq(t, err, nil) - eq(t, user, "monalisa") + assert.NoError(t, err) + assert.Equal(t, "monalisa", user) token, err := config.Get("github.com", "oauth_token") - eq(t, err, nil) - eq(t, token, "OTOKEN") + assert.NoError(t, err) + assert.Equal(t, "OTOKEN", token) } func Test_parseConfig_hostFallback(t *testing.T) { - defer StubConfig(`--- + defer stubConfig(`--- git_protocol: ssh `, `--- github.com: @@ -82,21 +78,21 @@ example.com: oauth_token: NOTTHIS git_protocol: https `)() - config, err := ParseConfig("config.yml") - eq(t, err, nil) + config, err := parseConfig("config.yml") + assert.NoError(t, err) val, err := config.Get("example.com", "git_protocol") - eq(t, err, nil) - eq(t, val, "https") + assert.NoError(t, err) + assert.Equal(t, "https", val) val, err = config.Get("github.com", "git_protocol") - eq(t, err, nil) - eq(t, val, "ssh") - val, err = config.Get("nonexist.io", "git_protocol") - eq(t, err, nil) - eq(t, val, "ssh") + assert.NoError(t, err) + assert.Equal(t, "ssh", val) + val, err = config.Get("nonexistent.io", "git_protocol") + assert.NoError(t, err) + assert.Equal(t, "ssh", val) } -func Test_ParseConfig_migrateConfig(t *testing.T) { - defer StubConfig(`--- +func Test_parseConfig_migrateConfig(t *testing.T) { + defer stubConfig(`--- github.com: - user: keiyuri oauth_token: 123456 @@ -107,8 +103,8 @@ github.com: defer StubWriteConfig(&mainBuf, &hostsBuf)() defer StubBackupConfig()() - _, err := ParseConfig("config.yml") - assert.Nil(t, err) + _, err := parseConfig("config.yml") + assert.NoError(t, err) expectedHosts := `github.com: user: keiyuri @@ -141,7 +137,7 @@ func Test_parseConfigFile(t *testing.T) { for _, tt := range tests { t.Run(fmt.Sprintf("contents: %q", tt.contents), func(t *testing.T) { - defer StubConfig(tt.contents, "")() + defer stubConfig(tt.contents, "")() _, yamlRoot, err := parseConfigFile("config.yml") if tt.wantsErr != (err != nil) { t.Fatalf("got error: %v", err) @@ -154,3 +150,402 @@ func Test_parseConfigFile(t *testing.T) { }) } } + +func Test_ConfigDir(t *testing.T) { + tempDir := t.TempDir() + + tests := []struct { + name string + onlyWindows bool + env map[string]string + output string + }{ + { + name: "HOME/USERPROFILE specified", + env: map[string]string{ + "GH_CONFIG_DIR": "", + "XDG_CONFIG_HOME": "", + "AppData": "", + "USERPROFILE": tempDir, + "HOME": tempDir, + }, + output: filepath.Join(tempDir, ".config", "gh"), + }, + { + name: "GH_CONFIG_DIR specified", + env: map[string]string{ + "GH_CONFIG_DIR": filepath.Join(tempDir, "gh_config_dir"), + }, + output: filepath.Join(tempDir, "gh_config_dir"), + }, + { + name: "XDG_CONFIG_HOME specified", + env: map[string]string{ + "XDG_CONFIG_HOME": tempDir, + }, + output: filepath.Join(tempDir, "gh"), + }, + { + name: "GH_CONFIG_DIR and XDG_CONFIG_HOME specified", + env: map[string]string{ + "GH_CONFIG_DIR": filepath.Join(tempDir, "gh_config_dir"), + "XDG_CONFIG_HOME": tempDir, + }, + output: filepath.Join(tempDir, "gh_config_dir"), + }, + { + name: "AppData specified", + onlyWindows: true, + env: map[string]string{ + "AppData": tempDir, + }, + output: filepath.Join(tempDir, "GitHub CLI"), + }, + { + name: "GH_CONFIG_DIR and AppData specified", + onlyWindows: true, + env: map[string]string{ + "GH_CONFIG_DIR": filepath.Join(tempDir, "gh_config_dir"), + "AppData": tempDir, + }, + output: filepath.Join(tempDir, "gh_config_dir"), + }, + { + name: "XDG_CONFIG_HOME and AppData specified", + onlyWindows: true, + env: map[string]string{ + "XDG_CONFIG_HOME": tempDir, + "AppData": tempDir, + }, + output: filepath.Join(tempDir, "gh"), + }, + } + + for _, tt := range tests { + if tt.onlyWindows && runtime.GOOS != "windows" { + continue + } + t.Run(tt.name, func(t *testing.T) { + if tt.env != nil { + for k, v := range tt.env { + old := os.Getenv(k) + os.Setenv(k, v) + defer os.Setenv(k, old) + } + } + + // Create directory to skip auto migration code + // which gets run when target directory does not exist + _ = os.MkdirAll(tt.output, 0755) + + assert.Equal(t, tt.output, ConfigDir()) + }) + } +} + +func Test_configFile_Write_toDisk(t *testing.T) { + configDir := filepath.Join(t.TempDir(), ".config", "gh") + _ = os.MkdirAll(configDir, 0755) + os.Setenv(GH_CONFIG_DIR, configDir) + defer os.Unsetenv(GH_CONFIG_DIR) + + cfg := NewFromString(`pager: less`) + err := cfg.Write() + if err != nil { + t.Fatal(err) + } + + expectedConfig := "pager: less\n" + if configBytes, err := ioutil.ReadFile(filepath.Join(configDir, "config.yml")); err != nil { + t.Error(err) + } else if string(configBytes) != expectedConfig { + t.Errorf("expected config.yml %q, got %q", expectedConfig, string(configBytes)) + } + + if configBytes, err := ioutil.ReadFile(filepath.Join(configDir, "hosts.yml")); err != nil { + t.Error(err) + } else if string(configBytes) != "" { + t.Errorf("unexpected hosts.yml: %q", string(configBytes)) + } +} + +func Test_autoMigrateConfigDir_noMigration_notExist(t *testing.T) { + homeDir := t.TempDir() + migrateDir := t.TempDir() + + homeEnvVar := "HOME" + if runtime.GOOS == "windows" { + homeEnvVar = "USERPROFILE" + } + old := os.Getenv(homeEnvVar) + os.Setenv(homeEnvVar, homeDir) + defer os.Setenv(homeEnvVar, old) + + err := autoMigrateConfigDir(migrateDir) + assert.Equal(t, errNotExist, err) + + files, err := ioutil.ReadDir(migrateDir) + assert.NoError(t, err) + assert.Equal(t, 0, len(files)) +} + +func Test_autoMigrateConfigDir_noMigration_samePath(t *testing.T) { + homeDir := t.TempDir() + migrateDir := filepath.Join(homeDir, ".config", "gh") + err := os.MkdirAll(migrateDir, 0755) + assert.NoError(t, err) + + homeEnvVar := "HOME" + if runtime.GOOS == "windows" { + homeEnvVar = "USERPROFILE" + } + old := os.Getenv(homeEnvVar) + os.Setenv(homeEnvVar, homeDir) + defer os.Setenv(homeEnvVar, old) + + err = autoMigrateConfigDir(migrateDir) + assert.Equal(t, errSamePath, err) + + files, err := ioutil.ReadDir(migrateDir) + assert.NoError(t, err) + assert.Equal(t, 0, len(files)) +} + +func Test_autoMigrateConfigDir_migration(t *testing.T) { + homeDir := t.TempDir() + migrateDir := t.TempDir() + homeConfigDir := filepath.Join(homeDir, ".config", "gh") + migrateConfigDir := filepath.Join(migrateDir, ".config", "gh") + + homeEnvVar := "HOME" + if runtime.GOOS == "windows" { + homeEnvVar = "USERPROFILE" + } + old := os.Getenv(homeEnvVar) + os.Setenv(homeEnvVar, homeDir) + defer os.Setenv(homeEnvVar, old) + + err := os.MkdirAll(homeConfigDir, 0755) + assert.NoError(t, err) + f, err := ioutil.TempFile(homeConfigDir, "") + assert.NoError(t, err) + f.Close() + + err = autoMigrateConfigDir(migrateConfigDir) + assert.NoError(t, err) + + _, err = ioutil.ReadDir(homeConfigDir) + assert.True(t, os.IsNotExist(err)) + + files, err := ioutil.ReadDir(migrateConfigDir) + assert.NoError(t, err) + assert.Equal(t, 1, len(files)) +} + +func Test_StateDir(t *testing.T) { + tempDir := t.TempDir() + + tests := []struct { + name string + onlyWindows bool + env map[string]string + output string + }{ + { + name: "HOME/USERPROFILE specified", + env: map[string]string{ + "XDG_STATE_HOME": "", + "GH_CONFIG_DIR": "", + "XDG_CONFIG_HOME": "", + "LocalAppData": "", + "USERPROFILE": tempDir, + "HOME": tempDir, + }, + output: filepath.Join(tempDir, ".local", "state", "gh"), + }, + { + name: "XDG_STATE_HOME specified", + env: map[string]string{ + "XDG_STATE_HOME": tempDir, + }, + output: filepath.Join(tempDir, "gh"), + }, + { + name: "LocalAppData specified", + onlyWindows: true, + env: map[string]string{ + "LocalAppData": tempDir, + }, + output: filepath.Join(tempDir, "GitHub CLI"), + }, + { + name: "XDG_STATE_HOME and LocalAppData specified", + onlyWindows: true, + env: map[string]string{ + "XDG_STATE_HOME": tempDir, + "LocalAppData": tempDir, + }, + output: filepath.Join(tempDir, "gh"), + }, + } + + for _, tt := range tests { + if tt.onlyWindows && runtime.GOOS != "windows" { + continue + } + t.Run(tt.name, func(t *testing.T) { + if tt.env != nil { + for k, v := range tt.env { + old := os.Getenv(k) + os.Setenv(k, v) + defer os.Setenv(k, old) + } + } + + // Create directory to skip auto migration code + // which gets run when target directory does not exist + _ = os.MkdirAll(tt.output, 0755) + + assert.Equal(t, tt.output, StateDir()) + }) + } +} + +func Test_autoMigrateStateDir_noMigration_notExist(t *testing.T) { + homeDir := t.TempDir() + migrateDir := t.TempDir() + + homeEnvVar := "HOME" + if runtime.GOOS == "windows" { + homeEnvVar = "USERPROFILE" + } + old := os.Getenv(homeEnvVar) + os.Setenv(homeEnvVar, homeDir) + defer os.Setenv(homeEnvVar, old) + + err := autoMigrateStateDir(migrateDir) + assert.Equal(t, errNotExist, err) + + files, err := ioutil.ReadDir(migrateDir) + assert.NoError(t, err) + assert.Equal(t, 0, len(files)) +} + +func Test_autoMigrateStateDir_noMigration_samePath(t *testing.T) { + homeDir := t.TempDir() + migrateDir := filepath.Join(homeDir, ".config", "gh") + err := os.MkdirAll(migrateDir, 0755) + assert.NoError(t, err) + + homeEnvVar := "HOME" + if runtime.GOOS == "windows" { + homeEnvVar = "USERPROFILE" + } + old := os.Getenv(homeEnvVar) + os.Setenv(homeEnvVar, homeDir) + defer os.Setenv(homeEnvVar, old) + + err = autoMigrateStateDir(migrateDir) + assert.Equal(t, errSamePath, err) + + files, err := ioutil.ReadDir(migrateDir) + assert.NoError(t, err) + assert.Equal(t, 0, len(files)) +} + +func Test_autoMigrateStateDir_migration(t *testing.T) { + homeDir := t.TempDir() + migrateDir := t.TempDir() + homeConfigDir := filepath.Join(homeDir, ".config", "gh") + migrateStateDir := filepath.Join(migrateDir, ".local", "state", "gh") + + homeEnvVar := "HOME" + if runtime.GOOS == "windows" { + homeEnvVar = "USERPROFILE" + } + old := os.Getenv(homeEnvVar) + os.Setenv(homeEnvVar, homeDir) + defer os.Setenv(homeEnvVar, old) + + err := os.MkdirAll(homeConfigDir, 0755) + assert.NoError(t, err) + err = ioutil.WriteFile(filepath.Join(homeConfigDir, "state.yml"), nil, 0755) + assert.NoError(t, err) + + err = autoMigrateStateDir(migrateStateDir) + assert.NoError(t, err) + + files, err := ioutil.ReadDir(homeConfigDir) + assert.NoError(t, err) + assert.Equal(t, 0, len(files)) + + files, err = ioutil.ReadDir(migrateStateDir) + assert.NoError(t, err) + assert.Equal(t, 1, len(files)) + assert.Equal(t, "state.yml", files[0].Name()) +} + +func Test_DataDir(t *testing.T) { + tempDir := t.TempDir() + + tests := []struct { + name string + onlyWindows bool + env map[string]string + output string + }{ + { + name: "HOME/USERPROFILE specified", + env: map[string]string{ + "XDG_DATA_HOME": "", + "GH_CONFIG_DIR": "", + "XDG_CONFIG_HOME": "", + "LocalAppData": "", + "USERPROFILE": tempDir, + "HOME": tempDir, + }, + output: filepath.Join(tempDir, ".local", "share", "gh"), + }, + { + name: "XDG_DATA_HOME specified", + env: map[string]string{ + "XDG_DATA_HOME": tempDir, + }, + output: filepath.Join(tempDir, "gh"), + }, + { + name: "LocalAppData specified", + onlyWindows: true, + env: map[string]string{ + "LocalAppData": tempDir, + }, + output: filepath.Join(tempDir, "GitHub CLI"), + }, + { + name: "XDG_DATA_HOME and LocalAppData specified", + onlyWindows: true, + env: map[string]string{ + "XDG_DATA_HOME": tempDir, + "LocalAppData": tempDir, + }, + output: filepath.Join(tempDir, "gh"), + }, + } + + for _, tt := range tests { + if tt.onlyWindows && runtime.GOOS != "windows" { + continue + } + t.Run(tt.name, func(t *testing.T) { + if tt.env != nil { + for k, v := range tt.env { + old := os.Getenv(k) + os.Setenv(k, v) + defer os.Setenv(k, old) + } + } + + assert.Equal(t, tt.output, DataDir()) + }) + } +} diff --git a/internal/config/config_map.go b/internal/config/config_map.go new file mode 100644 index 00000000000..8afaf3a4ccd --- /dev/null +++ b/internal/config/config_map.go @@ -0,0 +1,104 @@ +package config + +import ( + "errors" + + "gopkg.in/yaml.v3" +) + +// This type implements a low-level get/set config that is backed by an in-memory tree of Yaml +// nodes. It allows us to interact with a yaml-based config programmatically, preserving any +// comments that were present when the yaml was parsed. +type ConfigMap struct { + Root *yaml.Node +} + +type ConfigEntry struct { + KeyNode *yaml.Node + ValueNode *yaml.Node + Index int +} + +type NotFoundError struct { + error +} + +func (cm *ConfigMap) Empty() bool { + return cm.Root == nil || len(cm.Root.Content) == 0 +} + +func (cm *ConfigMap) GetStringValue(key string) (string, error) { + entry, err := cm.FindEntry(key) + if err != nil { + return "", err + } + return entry.ValueNode.Value, nil +} + +func (cm *ConfigMap) SetStringValue(key, value string) error { + entry, err := cm.FindEntry(key) + + var notFound *NotFoundError + + valueNode := entry.ValueNode + + if err != nil && errors.As(err, ¬Found) { + keyNode := &yaml.Node{ + Kind: yaml.ScalarNode, + Value: key, + } + valueNode = &yaml.Node{ + Kind: yaml.ScalarNode, + Tag: "!!str", + Value: "", + } + + cm.Root.Content = append(cm.Root.Content, keyNode, valueNode) + } else if err != nil { + return err + } + + valueNode.Value = value + + return nil +} + +func (cm *ConfigMap) FindEntry(key string) (ce *ConfigEntry, err error) { + err = nil + + ce = &ConfigEntry{} + + // Content slice goes [key1, value1, key2, value2, ...] + topLevelPairs := cm.Root.Content + for i, v := range topLevelPairs { + // Skip every other slice item since we only want to check against keys + if i%2 != 0 { + continue + } + if v.Value == key { + ce.KeyNode = v + ce.Index = i + if i+1 < len(topLevelPairs) { + ce.ValueNode = topLevelPairs[i+1] + } + return + } + } + + return ce, &NotFoundError{errors.New("not found")} +} + +func (cm *ConfigMap) RemoveEntry(key string) { + newContent := []*yaml.Node{} + + content := cm.Root.Content + for i := 0; i < len(content); i++ { + if content[i].Value == key { + i++ // skip the next node which is this key's value + } else { + newContent = append(newContent, content[i]) + } + } + + cm.Root.Content = newContent +} diff --git a/internal/config/config_map_test.go b/internal/config/config_map_test.go new file mode 100644 index 00000000000..c504e4cbc60 --- /dev/null +++ b/internal/config/config_map_test.go @@ -0,0 +1,65 @@ +package config + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" +) + +func TestFindEntry(t *testing.T) { + tests := []struct { + name string + key string + output string + wantErr bool + }{ + { + name: "find key", + key: "valid", + output: "present", + }, + { + name: "find key that is not present", + key: "invalid", + wantErr: true, + }, + { + name: "find key with blank value", + key: "blank", + output: "", + }, + { + name: "find key that has same content as a value", + key: "same", + output: "logical", + }, + } + + for _, tt := range tests { + cm := ConfigMap{Root: testYaml()} + t.Run(tt.name, func(t *testing.T) { + out, err := cm.FindEntry(tt.key) + if tt.wantErr { + assert.EqualError(t, err, "not found") + return + } + assert.NoError(t, err) + fmt.Println(out) + assert.Equal(t, tt.output, out.ValueNode.Value) + }) + } +} + +func testYaml() *yaml.Node { + var root yaml.Node + var data = ` +valid: present +erroneous: same +blank: +same: logical +` + _ = yaml.Unmarshal([]byte(data), &root) + return root.Content[0] +} diff --git a/internal/config/config_type.go b/internal/config/config_type.go index 40533f211b8..92792e93f27 100644 --- a/internal/config/config_type.go +++ b/internal/config/config_type.go @@ -1,15 +1,25 @@ package config import ( - "bytes" - "errors" "fmt" - "sort" - "github.com/cli/cli/internal/ghinstance" "gopkg.in/yaml.v3" ) +// This interface describes interacting with some persistent configuration for gh. +type Config interface { + Get(string, string) (string, error) + GetWithSource(string, string) (string, string, error) + Set(string, string, string) error + UnsetHost(string) + Hosts() ([]string, error) + DefaultHost() (string, error) + DefaultHostWithSource() (string, string, error) + Aliases() (*AliasConfig, error) + CheckWriteable(string, string) error + Write() error +} + type ConfigOption struct { Key string Description string @@ -40,6 +50,16 @@ var configOptions = []ConfigOption{ Description: "the terminal pager program to send standard output to", DefaultValue: "", }, + { + Key: "http_unix_socket", + Description: "the path to a unix socket through which to make HTTP connection", + DefaultValue: "", + }, + { + Key: "browser", + Description: "the web browser to use for opening URLs", + DefaultValue: "", + }, } func ConfigOptions() []ConfigOption { @@ -87,115 +107,6 @@ func ValidateValue(key, value string) error { return &InvalidValueError{ValidValues: validValues} } -// This interface describes interacting with some persistent configuration for gh. -type Config interface { - Get(string, string) (string, error) - GetWithSource(string, string) (string, string, error) - Set(string, string, string) error - UnsetHost(string) - Hosts() ([]string, error) - Aliases() (*AliasConfig, error) - CheckWriteable(string, string) error - Write() error -} - -type NotFoundError struct { - error -} - -type HostConfig struct { - ConfigMap - Host string -} - -// This type implements a low-level get/set config that is backed by an in-memory tree of Yaml -// nodes. It allows us to interact with a yaml-based config programmatically, preserving any -// comments that were present when the yaml was parsed. -type ConfigMap struct { - Root *yaml.Node -} - -func (cm *ConfigMap) Empty() bool { - return cm.Root == nil || len(cm.Root.Content) == 0 -} - -func (cm *ConfigMap) GetStringValue(key string) (string, error) { - entry, err := cm.FindEntry(key) - if err != nil { - return "", err - } - return entry.ValueNode.Value, nil -} - -func (cm *ConfigMap) SetStringValue(key, value string) error { - entry, err := cm.FindEntry(key) - - var notFound *NotFoundError - - valueNode := entry.ValueNode - - if err != nil && errors.As(err, ¬Found) { - keyNode := &yaml.Node{ - Kind: yaml.ScalarNode, - Value: key, - } - valueNode = &yaml.Node{ - Kind: yaml.ScalarNode, - Tag: "!!str", - Value: "", - } - - cm.Root.Content = append(cm.Root.Content, keyNode, valueNode) - } else if err != nil { - return err - } - - valueNode.Value = value - - return nil -} - -type ConfigEntry struct { - KeyNode *yaml.Node - ValueNode *yaml.Node - Index int -} - -func (cm *ConfigMap) FindEntry(key string) (ce *ConfigEntry, err error) { - err = nil - - ce = &ConfigEntry{} - - topLevelKeys := cm.Root.Content - for i, v := range topLevelKeys { - if v.Value == key { - ce.KeyNode = v - ce.Index = i - if i+1 < len(topLevelKeys) { - ce.ValueNode = topLevelKeys[i+1] - } - return - } - } - - return ce, &NotFoundError{errors.New("not found")} -} - -func (cm *ConfigMap) RemoveEntry(key string) { - newContent := []*yaml.Node{} - - content := cm.Root.Content - for i := 0; i < len(content); i++ { - if content[i].Value == key { - i++ // skip the next node which is this key's value - } else { - newContent = append(newContent, content[i]) - } - } - - cm.Root.Content = newContent -} - func NewConfig(root *yaml.Node) Config { return &fileConfig{ ConfigMap: ConfigMap{Root: root.Content[0]}, @@ -278,297 +189,26 @@ func NewBlankRoot() *yaml.Node { }, }, }, + { + HeadComment: "The path to a unix socket through which send HTTP connections. If blank, HTTP traffic will be handled by net/http.DefaultTransport.", + Kind: yaml.ScalarNode, + Value: "http_unix_socket", + }, + { + Kind: yaml.ScalarNode, + Value: "", + }, + { + HeadComment: "What web browser gh should use when opening URLs. If blank, will refer to environment.", + Kind: yaml.ScalarNode, + Value: "browser", + }, + { + Kind: yaml.ScalarNode, + Value: "", + }, }, }, }, } } - -// This type implements a Config interface and represents a config file on disk. -type fileConfig struct { - ConfigMap - documentRoot *yaml.Node -} - -func (c *fileConfig) Root() *yaml.Node { - return c.ConfigMap.Root -} - -func (c *fileConfig) Get(hostname, key string) (string, error) { - val, _, err := c.GetWithSource(hostname, key) - return val, err -} - -func (c *fileConfig) GetWithSource(hostname, key string) (string, string, error) { - if hostname != "" { - var notFound *NotFoundError - - hostCfg, err := c.configForHost(hostname) - if err != nil && !errors.As(err, ¬Found) { - return "", "", err - } - - var hostValue string - if hostCfg != nil { - hostValue, err = hostCfg.GetStringValue(key) - if err != nil && !errors.As(err, ¬Found) { - return "", "", err - } - } - - if hostValue != "" { - // TODO: avoid hardcoding this - return hostValue, "~/.config/gh/hosts.yml", nil - } - } - - // TODO: avoid hardcoding this - defaultSource := "~/.config/gh/config.yml" - - value, err := c.GetStringValue(key) - - var notFound *NotFoundError - - if err != nil && errors.As(err, ¬Found) { - return defaultFor(key), defaultSource, nil - } else if err != nil { - return "", defaultSource, err - } - - if value == "" { - return defaultFor(key), defaultSource, nil - } - - return value, defaultSource, nil -} - -func (c *fileConfig) Set(hostname, key, value string) error { - if hostname == "" { - return c.SetStringValue(key, value) - } else { - hostCfg, err := c.configForHost(hostname) - var notFound *NotFoundError - if errors.As(err, ¬Found) { - hostCfg = c.makeConfigForHost(hostname) - } else if err != nil { - return err - } - return hostCfg.SetStringValue(key, value) - } -} - -func (c *fileConfig) UnsetHost(hostname string) { - if hostname == "" { - return - } - - hostsEntry, err := c.FindEntry("hosts") - if err != nil { - return - } - - cm := ConfigMap{hostsEntry.ValueNode} - cm.RemoveEntry(hostname) -} - -func (c *fileConfig) configForHost(hostname string) (*HostConfig, error) { - hosts, err := c.hostEntries() - if err != nil { - return nil, fmt.Errorf("failed to parse hosts config: %w", err) - } - - for _, hc := range hosts { - if hc.Host == hostname { - return hc, nil - } - } - return nil, &NotFoundError{fmt.Errorf("could not find config entry for %q", hostname)} -} - -func (c *fileConfig) CheckWriteable(hostname, key string) error { - // TODO: check filesystem permissions - return nil -} - -func (c *fileConfig) Write() error { - mainData := yaml.Node{Kind: yaml.MappingNode} - hostsData := yaml.Node{Kind: yaml.MappingNode} - - nodes := c.documentRoot.Content[0].Content - for i := 0; i < len(nodes)-1; i += 2 { - if nodes[i].Value == "hosts" { - hostsData.Content = append(hostsData.Content, nodes[i+1].Content...) - } else { - mainData.Content = append(mainData.Content, nodes[i], nodes[i+1]) - } - } - - mainBytes, err := yaml.Marshal(&mainData) - if err != nil { - return err - } - - filename := ConfigFile() - err = WriteConfigFile(filename, yamlNormalize(mainBytes)) - if err != nil { - return err - } - - hostsBytes, err := yaml.Marshal(&hostsData) - if err != nil { - return err - } - - return WriteConfigFile(hostsConfigFile(filename), yamlNormalize(hostsBytes)) -} - -func yamlNormalize(b []byte) []byte { - if bytes.Equal(b, []byte("{}\n")) { - return []byte{} - } - return b -} - -func (c *fileConfig) Aliases() (*AliasConfig, error) { - // The complexity here is for dealing with either a missing or empty aliases key. It's something - // we'll likely want for other config sections at some point. - entry, err := c.FindEntry("aliases") - var nfe *NotFoundError - notFound := errors.As(err, &nfe) - if err != nil && !notFound { - return nil, err - } - - toInsert := []*yaml.Node{} - - keyNode := entry.KeyNode - valueNode := entry.ValueNode - - if keyNode == nil { - keyNode = &yaml.Node{ - Kind: yaml.ScalarNode, - Value: "aliases", - } - toInsert = append(toInsert, keyNode) - } - - if valueNode == nil || valueNode.Kind != yaml.MappingNode { - valueNode = &yaml.Node{ - Kind: yaml.MappingNode, - Value: "", - } - toInsert = append(toInsert, valueNode) - } - - if len(toInsert) > 0 { - newContent := []*yaml.Node{} - if notFound { - newContent = append(c.Root().Content, keyNode, valueNode) - } else { - for i := 0; i < len(c.Root().Content); i++ { - if i == entry.Index { - newContent = append(newContent, keyNode, valueNode) - i++ - } else { - newContent = append(newContent, c.Root().Content[i]) - } - } - } - c.Root().Content = newContent - } - - return &AliasConfig{ - Parent: c, - ConfigMap: ConfigMap{Root: valueNode}, - }, nil -} - -func (c *fileConfig) hostEntries() ([]*HostConfig, error) { - entry, err := c.FindEntry("hosts") - if err != nil { - return nil, fmt.Errorf("could not find hosts config: %w", err) - } - - hostConfigs, err := c.parseHosts(entry.ValueNode) - if err != nil { - return nil, fmt.Errorf("could not parse hosts config: %w", err) - } - - return hostConfigs, nil -} - -// Hosts returns a list of all known hostnames configured in hosts.yml -func (c *fileConfig) Hosts() ([]string, error) { - entries, err := c.hostEntries() - if err != nil { - return nil, err - } - - hostnames := []string{} - for _, entry := range entries { - hostnames = append(hostnames, entry.Host) - } - - sort.SliceStable(hostnames, func(i, j int) bool { return hostnames[i] == ghinstance.Default() }) - - return hostnames, nil -} - -func (c *fileConfig) makeConfigForHost(hostname string) *HostConfig { - hostRoot := &yaml.Node{Kind: yaml.MappingNode} - hostCfg := &HostConfig{ - Host: hostname, - ConfigMap: ConfigMap{Root: hostRoot}, - } - - var notFound *NotFoundError - hostsEntry, err := c.FindEntry("hosts") - if errors.As(err, ¬Found) { - hostsEntry.KeyNode = &yaml.Node{ - Kind: yaml.ScalarNode, - Value: "hosts", - } - hostsEntry.ValueNode = &yaml.Node{Kind: yaml.MappingNode} - root := c.Root() - root.Content = append(root.Content, hostsEntry.KeyNode, hostsEntry.ValueNode) - } else if err != nil { - panic(err) - } - - hostsEntry.ValueNode.Content = append(hostsEntry.ValueNode.Content, - &yaml.Node{ - Kind: yaml.ScalarNode, - Value: hostname, - }, hostRoot) - - return hostCfg -} - -func (c *fileConfig) parseHosts(hostsEntry *yaml.Node) ([]*HostConfig, error) { - hostConfigs := []*HostConfig{} - - for i := 0; i < len(hostsEntry.Content)-1; i = i + 2 { - hostname := hostsEntry.Content[i].Value - hostRoot := hostsEntry.Content[i+1] - hostConfig := HostConfig{ - ConfigMap: ConfigMap{Root: hostRoot}, - Host: hostname, - } - hostConfigs = append(hostConfigs, &hostConfig) - } - - if len(hostConfigs) == 0 { - return nil, errors.New("could not find any host configurations") - } - - return hostConfigs, nil -} - -func defaultFor(key string) string { - for _, co := range configOptions { - if co.Key == key { - return co.DefaultValue - } - } - return "" -} diff --git a/internal/config/config_type_test.go b/internal/config/config_type_test.go index 47295230f79..bf53aabe48d 100644 --- a/internal/config/config_type_test.go +++ b/internal/config/config_type_test.go @@ -50,23 +50,31 @@ func Test_defaultConfig(t *testing.T) { # Aliases allow you to create nicknames for gh commands aliases: co: pr checkout + # The path to a unix socket through which send HTTP connections. If blank, HTTP traffic will be handled by net/http.DefaultTransport. + http_unix_socket: + # What web browser gh should use when opening URLs. If blank, will refer to environment. + browser: `) assert.Equal(t, expected, mainBuf.String()) assert.Equal(t, "", hostsBuf.String()) proto, err := cfg.Get("", "git_protocol") - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, "https", proto) editor, err := cfg.Get("", "editor") - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, "", editor) aliases, err := cfg.Aliases() - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, len(aliases.All()), 1) expansion, _ := aliases.Get("co") assert.Equal(t, expansion, "pr checkout") + + browser, err := cfg.Get("", "browser") + assert.NoError(t, err) + assert.Equal(t, "", browser) } func Test_ValidateValue(t *testing.T) { @@ -74,13 +82,16 @@ func Test_ValidateValue(t *testing.T) { assert.EqualError(t, err, "invalid value") err = ValidateValue("git_protocol", "ssh") - assert.Nil(t, err) + assert.NoError(t, err) err = ValidateValue("editor", "vim") - assert.Nil(t, err) + assert.NoError(t, err) err = ValidateValue("got", "123") - assert.Nil(t, err) + assert.NoError(t, err) + + err = ValidateValue("http_unix_socket", "really_anything/is/allowed/and/net.Dial\\(...\\)/will/ultimately/validate") + assert.NoError(t, err) } func Test_ValidateKey(t *testing.T) { @@ -98,4 +109,10 @@ func Test_ValidateKey(t *testing.T) { err = ValidateKey("pager") assert.NoError(t, err) + + err = ValidateKey("http_unix_socket") + assert.NoError(t, err) + + err = ValidateKey("browser") + assert.NoError(t, err) } diff --git a/internal/config/from_env.go b/internal/config/from_env.go index fa930da0668..6373f1691db 100644 --- a/internal/config/from_env.go +++ b/internal/config/from_env.go @@ -4,14 +4,25 @@ import ( "fmt" "os" - "github.com/cli/cli/internal/ghinstance" + "github.com/cli/cli/v2/internal/ghinstance" ) const ( + GH_HOST = "GH_HOST" + GH_TOKEN = "GH_TOKEN" GITHUB_TOKEN = "GITHUB_TOKEN" + GH_ENTERPRISE_TOKEN = "GH_ENTERPRISE_TOKEN" GITHUB_ENTERPRISE_TOKEN = "GITHUB_ENTERPRISE_TOKEN" ) +type ReadOnlyEnvError struct { + Variable string +} + +func (e *ReadOnlyEnvError) Error() string { + return fmt.Sprintf("read-only value in %s", e.Variable) +} + func InheritEnv(c Config) Config { return &envConfig{Config: c} } @@ -28,13 +39,26 @@ func (c *envConfig) Hosts() ([]string, error) { hasDefault = true } } - if (err != nil || !hasDefault) && os.Getenv(GITHUB_TOKEN) != "" { + token, _ := AuthTokenFromEnv(ghinstance.Default()) + if (err != nil || !hasDefault) && token != "" { hosts = append([]string{ghinstance.Default()}, hosts...) return hosts, nil } return hosts, err } +func (c *envConfig) DefaultHost() (string, error) { + val, _, err := c.DefaultHostWithSource() + return val, err +} + +func (c *envConfig) DefaultHostWithSource() (string, string, error) { + if host := os.Getenv(GH_HOST); host != "" { + return host, GH_HOST, nil + } + return c.Config.DefaultHostWithSource() +} + func (c *envConfig) Get(hostname, key string) (string, error) { val, _, err := c.GetWithSource(hostname, key) return val, err @@ -42,13 +66,8 @@ func (c *envConfig) Get(hostname, key string) (string, error) { func (c *envConfig) GetWithSource(hostname, key string) (string, string, error) { if hostname != "" && key == "oauth_token" { - envName := GITHUB_TOKEN - if ghinstance.IsEnterprise(hostname) { - envName = GITHUB_ENTERPRISE_TOKEN - } - - if value := os.Getenv(envName); value != "" { - return value, envName, nil + if token, env := AuthTokenFromEnv(hostname); token != "" { + return token, env, nil } } @@ -57,15 +76,41 @@ func (c *envConfig) GetWithSource(hostname, key string) (string, string, error) func (c *envConfig) CheckWriteable(hostname, key string) error { if hostname != "" && key == "oauth_token" { - envName := GITHUB_TOKEN - if ghinstance.IsEnterprise(hostname) { - envName = GITHUB_ENTERPRISE_TOKEN + if token, env := AuthTokenFromEnv(hostname); token != "" { + return &ReadOnlyEnvError{Variable: env} } + } - if os.Getenv(envName) != "" { - return fmt.Errorf("read-only token in %s cannot be modified", envName) + return c.Config.CheckWriteable(hostname, key) +} + +func AuthTokenFromEnv(hostname string) (string, string) { + if ghinstance.IsEnterprise(hostname) { + if token := os.Getenv(GH_ENTERPRISE_TOKEN); token != "" { + return token, GH_ENTERPRISE_TOKEN } + + return os.Getenv(GITHUB_ENTERPRISE_TOKEN), GITHUB_ENTERPRISE_TOKEN } - return c.Config.CheckWriteable(hostname, key) + if token := os.Getenv(GH_TOKEN); token != "" { + return token, GH_TOKEN + } + + return os.Getenv(GITHUB_TOKEN), GITHUB_TOKEN +} + +func AuthTokenProvidedFromEnv() bool { + return os.Getenv(GH_ENTERPRISE_TOKEN) != "" || + os.Getenv(GITHUB_ENTERPRISE_TOKEN) != "" || + os.Getenv(GH_TOKEN) != "" || + os.Getenv(GITHUB_TOKEN) != "" +} + +func IsHostEnv(src string) bool { + return src == GH_HOST +} + +func IsEnterpriseEnv(src string) bool { + return src == GH_ENTERPRISE_TOKEN || src == GITHUB_ENTERPRISE_TOKEN } diff --git a/internal/config/from_env_test.go b/internal/config/from_env_test.go index 412d37248f1..765cd016022 100644 --- a/internal/config/from_env_test.go +++ b/internal/config/from_env_test.go @@ -11,9 +11,15 @@ import ( func TestInheritEnv(t *testing.T) { orig_GITHUB_TOKEN := os.Getenv("GITHUB_TOKEN") orig_GITHUB_ENTERPRISE_TOKEN := os.Getenv("GITHUB_ENTERPRISE_TOKEN") + orig_GH_TOKEN := os.Getenv("GH_TOKEN") + orig_GH_ENTERPRISE_TOKEN := os.Getenv("GH_ENTERPRISE_TOKEN") + orig_AppData := os.Getenv("AppData") t.Cleanup(func() { os.Setenv("GITHUB_TOKEN", orig_GITHUB_TOKEN) os.Setenv("GITHUB_ENTERPRISE_TOKEN", orig_GITHUB_ENTERPRISE_TOKEN) + os.Setenv("GH_TOKEN", orig_GH_TOKEN) + os.Setenv("GH_ENTERPRISE_TOKEN", orig_GH_ENTERPRISE_TOKEN) + os.Setenv("AppData", orig_AppData) }) type wants struct { @@ -28,28 +34,27 @@ func TestInheritEnv(t *testing.T) { baseConfig string GITHUB_TOKEN string GITHUB_ENTERPRISE_TOKEN string + GH_TOKEN string + GH_ENTERPRISE_TOKEN string hostname string wants wants }{ { - name: "blank", - baseConfig: ``, - GITHUB_TOKEN: "", - GITHUB_ENTERPRISE_TOKEN: "", - hostname: "github.com", + name: "blank", + baseConfig: ``, + hostname: "github.com", wants: wants{ - hosts: []string(nil), + hosts: []string{}, token: "", - source: "~/.config/gh/config.yml", + source: ".config.gh.config.yml", writeable: true, }, }, { - name: "GITHUB_TOKEN over blank config", - baseConfig: ``, - GITHUB_TOKEN: "OTOKEN", - GITHUB_ENTERPRISE_TOKEN: "", - hostname: "github.com", + name: "GITHUB_TOKEN over blank config", + baseConfig: ``, + GITHUB_TOKEN: "OTOKEN", + hostname: "github.com", wants: wants{ hosts: []string{"github.com"}, token: "OTOKEN", @@ -58,31 +63,65 @@ func TestInheritEnv(t *testing.T) { }, }, { - name: "GITHUB_TOKEN not applicable to GHE", - baseConfig: ``, - GITHUB_TOKEN: "OTOKEN", - GITHUB_ENTERPRISE_TOKEN: "", - hostname: "example.org", + name: "GH_TOKEN over blank config", + baseConfig: ``, + GH_TOKEN: "OTOKEN", + hostname: "github.com", + wants: wants{ + hosts: []string{"github.com"}, + token: "OTOKEN", + source: "GH_TOKEN", + writeable: false, + }, + }, + { + name: "GITHUB_TOKEN not applicable to GHE", + baseConfig: ``, + GITHUB_TOKEN: "OTOKEN", + hostname: "example.org", + wants: wants{ + hosts: []string{"github.com"}, + token: "", + source: ".config.gh.config.yml", + writeable: true, + }, + }, + { + name: "GH_TOKEN not applicable to GHE", + baseConfig: ``, + GH_TOKEN: "OTOKEN", + hostname: "example.org", wants: wants{ hosts: []string{"github.com"}, token: "", - source: "~/.config/gh/config.yml", + source: ".config.gh.config.yml", writeable: true, }, }, { name: "GITHUB_ENTERPRISE_TOKEN over blank config", baseConfig: ``, - GITHUB_TOKEN: "", GITHUB_ENTERPRISE_TOKEN: "ENTOKEN", hostname: "example.org", wants: wants{ - hosts: []string(nil), + hosts: []string{}, token: "ENTOKEN", source: "GITHUB_ENTERPRISE_TOKEN", writeable: false, }, }, + { + name: "GH_ENTERPRISE_TOKEN over blank config", + baseConfig: ``, + GH_ENTERPRISE_TOKEN: "ENTOKEN", + hostname: "example.org", + wants: wants{ + hosts: []string{}, + token: "ENTOKEN", + source: "GH_ENTERPRISE_TOKEN", + writeable: false, + }, + }, { name: "token from file", baseConfig: heredoc.Doc(` @@ -90,13 +129,11 @@ func TestInheritEnv(t *testing.T) { github.com: oauth_token: OTOKEN `), - GITHUB_TOKEN: "", - GITHUB_ENTERPRISE_TOKEN: "", - hostname: "github.com", + hostname: "github.com", wants: wants{ hosts: []string{"github.com"}, token: "OTOKEN", - source: "~/.config/gh/hosts.yml", + source: ".config.gh.hosts.yml", writeable: true, }, }, @@ -107,9 +144,8 @@ func TestInheritEnv(t *testing.T) { github.com: oauth_token: OTOKEN `), - GITHUB_TOKEN: "ENVTOKEN", - GITHUB_ENTERPRISE_TOKEN: "", - hostname: "github.com", + GITHUB_TOKEN: "ENVTOKEN", + hostname: "github.com", wants: wants{ hosts: []string{"github.com"}, token: "ENVTOKEN", @@ -117,6 +153,80 @@ func TestInheritEnv(t *testing.T) { writeable: false, }, }, + { + name: "GH_TOKEN shadows token from file", + baseConfig: heredoc.Doc(` + hosts: + github.com: + oauth_token: OTOKEN + `), + GH_TOKEN: "ENVTOKEN", + hostname: "github.com", + wants: wants{ + hosts: []string{"github.com"}, + token: "ENVTOKEN", + source: "GH_TOKEN", + writeable: false, + }, + }, + { + name: "GITHUB_ENTERPRISE_TOKEN shadows token from file", + baseConfig: heredoc.Doc(` + hosts: + example.org: + oauth_token: OTOKEN + `), + GITHUB_ENTERPRISE_TOKEN: "ENVTOKEN", + hostname: "example.org", + wants: wants{ + hosts: []string{"example.org"}, + token: "ENVTOKEN", + source: "GITHUB_ENTERPRISE_TOKEN", + writeable: false, + }, + }, + { + name: "GH_ENTERPRISE_TOKEN shadows token from file", + baseConfig: heredoc.Doc(` + hosts: + example.org: + oauth_token: OTOKEN + `), + GH_ENTERPRISE_TOKEN: "ENVTOKEN", + hostname: "example.org", + wants: wants{ + hosts: []string{"example.org"}, + token: "ENVTOKEN", + source: "GH_ENTERPRISE_TOKEN", + writeable: false, + }, + }, + { + name: "GH_TOKEN shadows token from GITHUB_TOKEN", + baseConfig: ``, + GH_TOKEN: "GHTOKEN", + GITHUB_TOKEN: "GITHUBTOKEN", + hostname: "github.com", + wants: wants{ + hosts: []string{"github.com"}, + token: "GHTOKEN", + source: "GH_TOKEN", + writeable: false, + }, + }, + { + name: "GH_ENTERPRISE_TOKEN shadows token from GITHUB_ENTERPRISE_TOKEN", + baseConfig: ``, + GH_ENTERPRISE_TOKEN: "GHTOKEN", + GITHUB_ENTERPRISE_TOKEN: "GITHUBTOKEN", + hostname: "example.org", + wants: wants{ + hosts: []string{}, + token: "GHTOKEN", + source: "GH_ENTERPRISE_TOKEN", + writeable: false, + }, + }, { name: "GITHUB_TOKEN adds host entry", baseConfig: heredoc.Doc(` @@ -124,9 +234,8 @@ func TestInheritEnv(t *testing.T) { example.org: oauth_token: OTOKEN `), - GITHUB_TOKEN: "ENVTOKEN", - GITHUB_ENTERPRISE_TOKEN: "", - hostname: "github.com", + GITHUB_TOKEN: "ENVTOKEN", + hostname: "github.com", wants: wants{ hosts: []string{"github.com", "example.org"}, token: "ENVTOKEN", @@ -134,11 +243,30 @@ func TestInheritEnv(t *testing.T) { writeable: false, }, }, + { + name: "GH_TOKEN adds host entry", + baseConfig: heredoc.Doc(` + hosts: + example.org: + oauth_token: OTOKEN + `), + GH_TOKEN: "ENVTOKEN", + hostname: "github.com", + wants: wants{ + hosts: []string{"github.com", "example.org"}, + token: "ENVTOKEN", + source: "GH_TOKEN", + writeable: false, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { os.Setenv("GITHUB_TOKEN", tt.GITHUB_TOKEN) os.Setenv("GITHUB_ENTERPRISE_TOKEN", tt.GITHUB_ENTERPRISE_TOKEN) + os.Setenv("GH_TOKEN", tt.GH_TOKEN) + os.Setenv("GH_ENTERPRISE_TOKEN", tt.GH_ENTERPRISE_TOKEN) + os.Setenv("AppData", "") baseCfg := NewFromString(tt.baseConfig) cfg := InheritEnv(baseCfg) @@ -148,13 +276,72 @@ func TestInheritEnv(t *testing.T) { val, source, _ := cfg.GetWithSource(tt.hostname, "oauth_token") assert.Equal(t, tt.wants.token, val) - assert.Equal(t, tt.wants.source, source) + assert.Regexp(t, tt.wants.source, source) val, _ = cfg.Get(tt.hostname, "oauth_token") assert.Equal(t, tt.wants.token, val) err := cfg.CheckWriteable(tt.hostname, "oauth_token") - assert.Equal(t, tt.wants.writeable, err == nil) + if tt.wants.writeable != (err == nil) { + t.Errorf("CheckWriteable() = %v, wants %v", err, tt.wants.writeable) + } + }) + } +} + +func TestAuthTokenProvidedFromEnv(t *testing.T) { + orig_GITHUB_TOKEN := os.Getenv("GITHUB_TOKEN") + orig_GITHUB_ENTERPRISE_TOKEN := os.Getenv("GITHUB_ENTERPRISE_TOKEN") + orig_GH_TOKEN := os.Getenv("GH_TOKEN") + orig_GH_ENTERPRISE_TOKEN := os.Getenv("GH_ENTERPRISE_TOKEN") + t.Cleanup(func() { + os.Setenv("GITHUB_TOKEN", orig_GITHUB_TOKEN) + os.Setenv("GITHUB_ENTERPRISE_TOKEN", orig_GITHUB_ENTERPRISE_TOKEN) + os.Setenv("GH_TOKEN", orig_GH_TOKEN) + os.Setenv("GH_ENTERPRISE_TOKEN", orig_GH_ENTERPRISE_TOKEN) + }) + + tests := []struct { + name string + GITHUB_TOKEN string + GITHUB_ENTERPRISE_TOKEN string + GH_TOKEN string + GH_ENTERPRISE_TOKEN string + provided bool + }{ + { + name: "no env tokens", + provided: false, + }, + { + name: "GH_TOKEN", + GH_TOKEN: "TOKEN", + provided: true, + }, + { + name: "GITHUB_TOKEN", + GITHUB_TOKEN: "TOKEN", + provided: true, + }, + { + name: "GH_ENTERPRISE_TOKEN", + GH_ENTERPRISE_TOKEN: "TOKEN", + provided: true, + }, + { + name: "GITHUB_ENTERPRISE_TOKEN", + GITHUB_ENTERPRISE_TOKEN: "TOKEN", + provided: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Setenv("GITHUB_TOKEN", tt.GITHUB_TOKEN) + os.Setenv("GITHUB_ENTERPRISE_TOKEN", tt.GITHUB_ENTERPRISE_TOKEN) + os.Setenv("GH_TOKEN", tt.GH_TOKEN) + os.Setenv("GH_ENTERPRISE_TOKEN", tt.GH_ENTERPRISE_TOKEN) + assert.Equal(t, tt.provided, AuthTokenProvidedFromEnv()) }) } } diff --git a/internal/config/from_file.go b/internal/config/from_file.go new file mode 100644 index 00000000000..080143df47b --- /dev/null +++ b/internal/config/from_file.go @@ -0,0 +1,318 @@ +package config + +import ( + "bytes" + "errors" + "fmt" + "sort" + "strings" + + "github.com/cli/cli/v2/internal/ghinstance" + "gopkg.in/yaml.v3" +) + +// This type implements a Config interface and represents a config file on disk. +type fileConfig struct { + ConfigMap + documentRoot *yaml.Node +} + +type HostConfig struct { + ConfigMap + Host string +} + +func (c *fileConfig) Root() *yaml.Node { + return c.ConfigMap.Root +} + +func (c *fileConfig) Get(hostname, key string) (string, error) { + val, _, err := c.GetWithSource(hostname, key) + return val, err +} + +func (c *fileConfig) GetWithSource(hostname, key string) (string, string, error) { + if hostname != "" { + var notFound *NotFoundError + + hostCfg, err := c.configForHost(hostname) + if err != nil && !errors.As(err, ¬Found) { + return "", "", err + } + + var hostValue string + if hostCfg != nil { + hostValue, err = hostCfg.GetStringValue(key) + if err != nil && !errors.As(err, ¬Found) { + return "", "", err + } + } + + if hostValue != "" { + return hostValue, HostsConfigFile(), nil + } + } + + defaultSource := ConfigFile() + + value, err := c.GetStringValue(key) + + var notFound *NotFoundError + + if err != nil && errors.As(err, ¬Found) { + return defaultFor(key), defaultSource, nil + } else if err != nil { + return "", defaultSource, err + } + + if value == "" { + return defaultFor(key), defaultSource, nil + } + + return value, defaultSource, nil +} + +func (c *fileConfig) Set(hostname, key, value string) error { + if hostname == "" { + return c.SetStringValue(key, value) + } else { + hostCfg, err := c.configForHost(hostname) + var notFound *NotFoundError + if errors.As(err, ¬Found) { + hostCfg = c.makeConfigForHost(hostname) + } else if err != nil { + return err + } + return hostCfg.SetStringValue(key, value) + } +} + +func (c *fileConfig) UnsetHost(hostname string) { + if hostname == "" { + return + } + + hostsEntry, err := c.FindEntry("hosts") + if err != nil { + return + } + + cm := ConfigMap{hostsEntry.ValueNode} + cm.RemoveEntry(hostname) +} + +func (c *fileConfig) configForHost(hostname string) (*HostConfig, error) { + hosts, err := c.hostEntries() + if err != nil { + return nil, err + } + + for _, hc := range hosts { + if strings.EqualFold(hc.Host, hostname) { + return hc, nil + } + } + return nil, &NotFoundError{fmt.Errorf("could not find config entry for %q", hostname)} +} + +func (c *fileConfig) CheckWriteable(hostname, key string) error { + // TODO: check filesystem permissions + return nil +} + +func (c *fileConfig) Write() error { + mainData := yaml.Node{Kind: yaml.MappingNode} + hostsData := yaml.Node{Kind: yaml.MappingNode} + + nodes := c.documentRoot.Content[0].Content + for i := 0; i < len(nodes)-1; i += 2 { + if nodes[i].Value == "hosts" { + hostsData.Content = append(hostsData.Content, nodes[i+1].Content...) + } else { + mainData.Content = append(mainData.Content, nodes[i], nodes[i+1]) + } + } + + mainBytes, err := yaml.Marshal(&mainData) + if err != nil { + return err + } + + filename := ConfigFile() + err = WriteConfigFile(filename, yamlNormalize(mainBytes)) + if err != nil { + return err + } + + hostsBytes, err := yaml.Marshal(&hostsData) + if err != nil { + return err + } + + return WriteConfigFile(HostsConfigFile(), yamlNormalize(hostsBytes)) +} + +func (c *fileConfig) Aliases() (*AliasConfig, error) { + // The complexity here is for dealing with either a missing or empty aliases key. It's something + // we'll likely want for other config sections at some point. + entry, err := c.FindEntry("aliases") + var nfe *NotFoundError + notFound := errors.As(err, &nfe) + if err != nil && !notFound { + return nil, err + } + + toInsert := []*yaml.Node{} + + keyNode := entry.KeyNode + valueNode := entry.ValueNode + + if keyNode == nil { + keyNode = &yaml.Node{ + Kind: yaml.ScalarNode, + Value: "aliases", + } + toInsert = append(toInsert, keyNode) + } + + if valueNode == nil || valueNode.Kind != yaml.MappingNode { + valueNode = &yaml.Node{ + Kind: yaml.MappingNode, + Value: "", + } + toInsert = append(toInsert, valueNode) + } + + if len(toInsert) > 0 { + newContent := []*yaml.Node{} + if notFound { + newContent = append(c.Root().Content, keyNode, valueNode) + } else { + for i := 0; i < len(c.Root().Content); i++ { + if i == entry.Index { + newContent = append(newContent, keyNode, valueNode) + i++ + } else { + newContent = append(newContent, c.Root().Content[i]) + } + } + } + c.Root().Content = newContent + } + + return &AliasConfig{ + Parent: c, + ConfigMap: ConfigMap{Root: valueNode}, + }, nil +} + +func (c *fileConfig) hostEntries() ([]*HostConfig, error) { + entry, err := c.FindEntry("hosts") + if err != nil { + return []*HostConfig{}, nil + } + + hostConfigs, err := c.parseHosts(entry.ValueNode) + if err != nil { + return nil, fmt.Errorf("could not parse hosts config: %w", err) + } + + return hostConfigs, nil +} + +// Hosts returns a list of all known hostnames configured in hosts.yml +func (c *fileConfig) Hosts() ([]string, error) { + entries, err := c.hostEntries() + if err != nil { + return nil, err + } + + hostnames := []string{} + for _, entry := range entries { + hostnames = append(hostnames, entry.Host) + } + + sort.SliceStable(hostnames, func(i, j int) bool { return hostnames[i] == ghinstance.Default() }) + + return hostnames, nil +} + +func (c *fileConfig) DefaultHost() (string, error) { + val, _, err := c.DefaultHostWithSource() + return val, err +} + +func (c *fileConfig) DefaultHostWithSource() (string, string, error) { + hosts, err := c.Hosts() + if err == nil && len(hosts) == 1 { + return hosts[0], HostsConfigFile(), nil + } + + return ghinstance.Default(), "", nil +} + +func (c *fileConfig) makeConfigForHost(hostname string) *HostConfig { + hostRoot := &yaml.Node{Kind: yaml.MappingNode} + hostCfg := &HostConfig{ + Host: hostname, + ConfigMap: ConfigMap{Root: hostRoot}, + } + + var notFound *NotFoundError + hostsEntry, err := c.FindEntry("hosts") + if errors.As(err, ¬Found) { + hostsEntry.KeyNode = &yaml.Node{ + Kind: yaml.ScalarNode, + Value: "hosts", + } + hostsEntry.ValueNode = &yaml.Node{Kind: yaml.MappingNode} + root := c.Root() + root.Content = append(root.Content, hostsEntry.KeyNode, hostsEntry.ValueNode) + } else if err != nil { + panic(err) + } + + hostsEntry.ValueNode.Content = append(hostsEntry.ValueNode.Content, + &yaml.Node{ + Kind: yaml.ScalarNode, + Value: hostname, + }, hostRoot) + + return hostCfg +} + +func (c *fileConfig) parseHosts(hostsEntry *yaml.Node) ([]*HostConfig, error) { + hostConfigs := []*HostConfig{} + + for i := 0; i < len(hostsEntry.Content)-1; i = i + 2 { + hostname := hostsEntry.Content[i].Value + hostRoot := hostsEntry.Content[i+1] + hostConfig := HostConfig{ + ConfigMap: ConfigMap{Root: hostRoot}, + Host: hostname, + } + hostConfigs = append(hostConfigs, &hostConfig) + } + + if len(hostConfigs) == 0 { + return nil, errors.New("could not find any host configurations") + } + + return hostConfigs, nil +} + +func yamlNormalize(b []byte) []byte { + if bytes.Equal(b, []byte("{}\n")) { + return []byte{} + } + return b +} + +func defaultFor(key string) string { + for _, co := range configOptions { + if co.Key == key { + return co.DefaultValue + } + } + return "" +} diff --git a/internal/config/from_file_test.go b/internal/config/from_file_test.go new file mode 100644 index 00000000000..0c43c43a7b4 --- /dev/null +++ b/internal/config/from_file_test.go @@ -0,0 +1,15 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_fileConfig_Hosts(t *testing.T) { + c := NewBlankConfig() + hosts, err := c.Hosts() + require.NoError(t, err) + assert.Equal(t, []string{}, hosts) +} diff --git a/internal/config/stub.go b/internal/config/stub.go index 57761dac599..e68183d32a9 100644 --- a/internal/config/stub.go +++ b/internal/config/stub.go @@ -49,3 +49,11 @@ func (c ConfigStub) Write() error { c["_written"] = "true" return nil } + +func (c ConfigStub) DefaultHost() (string, error) { + return "", nil +} + +func (c ConfigStub) DefaultHostWithSource() (string, string, error) { + return "", "", nil +} diff --git a/internal/config/testing.go b/internal/config/testing.go index a491787058e..31a5fb2a8b6 100644 --- a/internal/config/testing.go +++ b/internal/config/testing.go @@ -4,7 +4,7 @@ import ( "fmt" "io" "os" - "path" + "path/filepath" ) func StubBackupConfig() func() { @@ -21,7 +21,7 @@ func StubBackupConfig() func() { func StubWriteConfig(wc io.Writer, wh io.Writer) func() { orig := WriteConfigFile WriteConfigFile = func(fn string, data []byte) error { - switch path.Base(fn) { + switch filepath.Base(fn) { case "config.yml": _, err := wc.Write(data) return err @@ -37,10 +37,10 @@ func StubWriteConfig(wc io.Writer, wh io.Writer) func() { } } -func StubConfig(main, hosts string) func() { +func stubConfig(main, hosts string) func() { orig := ReadConfigFile ReadConfigFile = func(fn string) ([]byte, error) { - switch path.Base(fn) { + switch filepath.Base(fn) { case "config.yml": if main == "" { return []byte(nil), os.ErrNotExist diff --git a/internal/docs/man_test.go b/internal/docs/man_test.go index 4e844ad613f..dd1df2e9b7a 100644 --- a/internal/docs/man_test.go +++ b/internal/docs/man_test.go @@ -101,12 +101,12 @@ func TestGenManSeeAlso(t *testing.T) { if err := assertNextLineEquals(scanner, ".PP"); err != nil { t.Fatalf("First line after SEE ALSO wasn't break-indent: %v", err) } - if err := assertNextLineEquals(scanner, `\fBroot\-bbb(1)\fP, \fBroot\-ccc(1)\fP`); err != nil { + if err := assertNextLineEquals(scanner, `\fBroot-bbb(1)\fP, \fBroot-ccc(1)\fP`); err != nil { t.Fatalf("Second line after SEE ALSO wasn't correct: %v", err) } } -func TestManPrintFlagsHidesShortDeperecated(t *testing.T) { +func TestManPrintFlagsHidesShortDeprecated(t *testing.T) { c := &cobra.Command{} c.Flags().StringP("foo", "f", "default", "Foo flag") _ = c.Flags().MarkShorthandDeprecated("foo", "don't use it no more") @@ -175,11 +175,10 @@ func assertNextLineEquals(scanner *bufio.Scanner, expectedLine string) error { } func BenchmarkGenManToFile(b *testing.B) { - file, err := ioutil.TempFile("", "") + file, err := ioutil.TempFile(b.TempDir(), "") if err != nil { b.Fatal(err) } - defer os.Remove(file.Name()) defer file.Close() b.ResetTimer() diff --git a/internal/docs/markdown_test.go b/internal/docs/markdown_test.go index 27be95efa00..497a0384acc 100644 --- a/internal/docs/markdown_test.go +++ b/internal/docs/markdown_test.go @@ -83,11 +83,10 @@ func TestGenMdTree(t *testing.T) { } func BenchmarkGenMarkdownToFile(b *testing.B) { - file, err := ioutil.TempFile("", "") + file, err := ioutil.TempFile(b.TempDir(), "") if err != nil { b.Fatal(err) } - defer os.Remove(file.Name()) defer file.Close() b.ResetTimer() diff --git a/internal/ghinstance/host.go b/internal/ghinstance/host.go index 76639ed26a3..709d7125509 100644 --- a/internal/ghinstance/host.go +++ b/internal/ghinstance/host.go @@ -8,27 +8,11 @@ import ( const defaultHostname = "github.com" -var hostnameOverride string - // Default returns the host name of the default GitHub instance func Default() string { return defaultHostname } -// OverridableDefault is like Default, except it is overridable by the GH_HOST environment variable -func OverridableDefault() string { - if hostnameOverride != "" { - return hostnameOverride - } - return defaultHostname -} - -// OverrideDefault overrides the value returned from OverridableDefault. This should only ever be -// called from the main runtime path, not tests. -func OverrideDefault(newhost string) { - hostnameOverride = newhost -} - // IsEnterprise reports whether a non-normalized host name looks like a GHE instance func IsEnterprise(h string) bool { return NormalizeHostname(h) != defaultHostname diff --git a/internal/ghinstance/host_test.go b/internal/ghinstance/host_test.go index 787569c68d4..45bac3800c3 100644 --- a/internal/ghinstance/host_test.go +++ b/internal/ghinstance/host_test.go @@ -6,29 +6,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestOverridableDefault(t *testing.T) { - oldOverride := hostnameOverride - t.Cleanup(func() { - hostnameOverride = oldOverride - }) - - host := OverridableDefault() - if host != "github.com" { - t.Errorf("expected github.com, got %q", host) - } - - OverrideDefault("example.org") - - host = OverridableDefault() - if host != "example.org" { - t.Errorf("expected example.org, got %q", host) - } - host = Default() - if host != "github.com" { - t.Errorf("expected github.com, got %q", host) - } -} - func TestIsEnterprise(t *testing.T) { tests := []struct { host string @@ -139,7 +116,7 @@ func TestHostnameValidator(t *testing.T) { assert.Error(t, err) return } - assert.Equal(t, nil, err) + assert.NoError(t, err) }) } } diff --git a/internal/ghrepo/repo.go b/internal/ghrepo/repo.go index 86fb74d67bb..77ed0b14066 100644 --- a/internal/ghrepo/repo.go +++ b/internal/ghrepo/repo.go @@ -5,8 +5,8 @@ import ( "net/url" "strings" - "github.com/cli/cli/git" - "github.com/cli/cli/internal/ghinstance" + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/ghinstance" ) // Interface describes an object that represents a GitHub repository @@ -18,7 +18,7 @@ type Interface interface { // New instantiates a GitHub repository from owner and name arguments func New(owner, repo string) Interface { - return NewWithHost(owner, repo, ghinstance.OverridableDefault()) + return NewWithHost(owner, repo, ghinstance.Default()) } // NewWithHost is like New with an explicit host name @@ -35,6 +35,21 @@ func FullName(r Interface) string { return fmt.Sprintf("%s/%s", r.RepoOwner(), r.RepoName()) } +var defaultHostOverride string + +func defaultHost() string { + if defaultHostOverride != "" { + return defaultHostOverride + } + return ghinstance.Default() +} + +// SetDefaultHost overrides the default GitHub hostname for FromFullName. +// TODO: remove after FromFullName approach is revisited +func SetDefaultHost(host string) { + defaultHostOverride = host +} + // FromFullName extracts the GitHub repository information from the following // formats: "OWNER/REPO", "HOST/OWNER/REPO", and a full URL. func FromFullName(nwo string) (Interface, error) { @@ -54,9 +69,9 @@ func FromFullName(nwo string) (Interface, error) { } switch len(parts) { case 3: - return NewWithHost(parts[1], parts[2], normalizeHostname(parts[0])), nil + return NewWithHost(parts[1], parts[2], parts[0]), nil case 2: - return New(parts[0], parts[1]), nil + return NewWithHost(parts[0], parts[1], defaultHost()), nil default: return nil, fmt.Errorf(`expected the "[HOST/]OWNER/REPO" format, got %q`, nwo) } diff --git a/internal/ghrepo/repo_test.go b/internal/ghrepo/repo_test.go index 7d796f1cbd9..46fa37827b1 100644 --- a/internal/ghrepo/repo_test.go +++ b/internal/ghrepo/repo_test.go @@ -117,12 +117,13 @@ func Test_repoFromURL(t *testing.T) { func TestFromFullName(t *testing.T) { tests := []struct { - name string - input string - wantOwner string - wantName string - wantHost string - wantErr error + name string + input string + hostOverride string + wantOwner string + wantName string + wantHost string + wantErr error }{ { name: "OWNER/REPO combo", @@ -171,9 +172,30 @@ func TestFromFullName(t *testing.T) { wantName: "REPO", wantErr: nil, }, + { + name: "OWNER/REPO with default host override", + input: "OWNER/REPO", + hostOverride: "override.com", + wantHost: "override.com", + wantOwner: "OWNER", + wantName: "REPO", + wantErr: nil, + }, + { + name: "HOST/OWNER/REPO with default host override", + input: "example.com/OWNER/REPO", + hostOverride: "override.com", + wantHost: "example.com", + wantOwner: "OWNER", + wantName: "REPO", + wantErr: nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + if tt.hostOverride != "" { + SetDefaultHost(tt.hostOverride) + } r, err := FromFullName(tt.input) if tt.wantErr != nil { if err == nil { diff --git a/internal/httpunix/transport.go b/internal/httpunix/transport.go new file mode 100644 index 00000000000..2326a5f9127 --- /dev/null +++ b/internal/httpunix/transport.go @@ -0,0 +1,21 @@ +// package httpunix provides an http.RoundTripper which dials a server via a unix socket. +package httpunix + +import ( + "net" + "net/http" +) + +// NewRoundTripper returns an http.RoundTripper which sends requests via a unix +// socket at socketPath. +func NewRoundTripper(socketPath string) http.RoundTripper { + dial := func(network, addr string) (net.Conn, error) { + return net.Dial("unix", socketPath) + } + + return &http.Transport{ + Dial: dial, + DialTLS: dial, + DisableKeepAlives: true, + } +} diff --git a/internal/run/run.go b/internal/run/run.go index 67de76fa23f..58fb189e389 100644 --- a/internal/run/run.go +++ b/internal/run/run.go @@ -3,8 +3,10 @@ package run import ( "bytes" "fmt" + "io" "os" "os/exec" + "path/filepath" "strings" ) @@ -20,15 +22,6 @@ var PrepareCmd = func(cmd *exec.Cmd) Runnable { return &cmdWithStderr{cmd} } -// SetPrepareCmd overrides PrepareCmd and returns a func to revert it back -func SetPrepareCmd(fn func(*exec.Cmd) Runnable) func() { - origPrepare := PrepareCmd - PrepareCmd = fn - return func() { - PrepareCmd = origPrepare - } -} - // cmdWithStderr augments exec.Cmd by adding stderr to the error message type cmdWithStderr struct { *exec.Cmd @@ -36,7 +29,7 @@ type cmdWithStderr struct { func (c cmdWithStderr) Output() ([]byte, error) { if os.Getenv("DEBUG") != "" { - fmt.Fprintf(os.Stderr, "%v\n", c.Cmd.Args) + _ = printArgs(os.Stderr, c.Cmd.Args) } if c.Cmd.Stderr != nil { return c.Cmd.Output() @@ -52,7 +45,7 @@ func (c cmdWithStderr) Output() ([]byte, error) { func (c cmdWithStderr) Run() error { if os.Getenv("DEBUG") != "" { - fmt.Fprintf(os.Stderr, "%v\n", c.Cmd.Args) + _ = printArgs(os.Stderr, c.Cmd.Args) } if c.Cmd.Stderr != nil { return c.Cmd.Run() @@ -80,3 +73,12 @@ func (e CmdError) Error() string { } return fmt.Sprintf("%s%s: %s", msg, e.Args[0], e.Err) } + +func printArgs(w io.Writer, args []string) error { + if len(args) > 0 { + // print commands, but omit the full path to an executable + args = append([]string{filepath.Base(args[0])}, args[1:]...) + } + _, err := fmt.Fprintf(w, "%v\n", args) + return err +} diff --git a/internal/run/stub.go b/internal/run/stub.go index 9bd6e279ba6..bcb359cee84 100644 --- a/internal/run/stub.go +++ b/internal/run/stub.go @@ -3,6 +3,7 @@ package run import ( "fmt" "os/exec" + "path/filepath" "regexp" "strings" ) @@ -12,9 +13,11 @@ type T interface { Errorf(string, ...interface{}) } +// Stub installs a catch-all for all external commands invoked from gh. It returns a restore func that, when +// invoked from tests, fails the current test if some stubs that were registered were never matched. func Stub() (*CommandStubber, func(T)) { cs := &CommandStubber{} - teardown := SetPrepareCmd(func(cmd *exec.Cmd) Runnable { + teardown := setPrepareCmd(func(cmd *exec.Cmd) Runnable { s := cs.find(cmd.Args) if s == nil { panic(fmt.Sprintf("no exec stub for `%s`", strings.Join(cmd.Args, " "))) @@ -39,17 +42,37 @@ func Stub() (*CommandStubber, func(T)) { return } t.Helper() - t.Errorf("umatched stubs (%d): %s", len(unmatched), strings.Join(unmatched, ", ")) + t.Errorf("unmatched stubs (%d): %s", len(unmatched), strings.Join(unmatched, ", ")) } } +func setPrepareCmd(fn func(*exec.Cmd) Runnable) func() { + origPrepare := PrepareCmd + PrepareCmd = func(cmd *exec.Cmd) Runnable { + // normalize git executable name for consistency in tests + if baseName := filepath.Base(cmd.Args[0]); baseName == "git" || baseName == "git.exe" { + cmd.Args[0] = "git" + } + return fn(cmd) + } + return func() { + PrepareCmd = origPrepare + } +} + +// CommandStubber stubs out invocations to external commands. type CommandStubber struct { stubs []*commandStub } -func (cs *CommandStubber) Register(p string, exitStatus int, output string, callbacks ...CommandCallback) { +// Register a stub for an external command. Pattern is a regular expression, output is the standard output +// from a command. Pass callbacks to inspect raw arguments that the command was invoked with. +func (cs *CommandStubber) Register(pattern string, exitStatus int, output string, callbacks ...CommandCallback) { + if len(pattern) < 1 { + panic("cannot use empty regexp pattern") + } cs.stubs = append(cs.stubs, &commandStub{ - pattern: regexp.MustCompile(p), + pattern: regexp.MustCompile(pattern), exitStatus: exitStatus, stdout: output, callbacks: callbacks, @@ -76,6 +99,7 @@ type commandStub struct { callbacks []CommandCallback } +// Run satisfies Runnable func (s *commandStub) Run() error { if s.exitStatus != 0 { return fmt.Errorf("%s exited with status %d", s.pattern, s.exitStatus) @@ -83,6 +107,7 @@ func (s *commandStub) Run() error { return nil } +// Output satisfies Runnable func (s *commandStub) Output() ([]byte, error) { if s.exitStatus != 0 { return []byte(nil), fmt.Errorf("%s exited with status %d", s.pattern, s.exitStatus) diff --git a/update/update.go b/internal/update/update.go similarity index 53% rename from update/update.go rename to internal/update/update.go index bf89a12e894..6228ec359b1 100644 --- a/update/update.go +++ b/internal/update/update.go @@ -3,18 +3,26 @@ package update import ( "fmt" "io/ioutil" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" "time" - "github.com/cli/cli/api" - "github.com/cli/cli/internal/ghinstance" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/ghinstance" "github.com/hashicorp/go-version" "gopkg.in/yaml.v3" ) +var gitDescribeSuffixRE = regexp.MustCompile(`\d+-\d+-g[a-f0-9]{8}$`) + // ReleaseInfo stores information about a release type ReleaseInfo struct { - Version string `json:"tag_name"` - URL string `json:"html_url"` + Version string `json:"tag_name"` + URL string `json:"html_url"` + PublishedAt time.Time `json:"published_at"` } type StateEntry struct { @@ -24,31 +32,31 @@ type StateEntry struct { // CheckForUpdate checks whether this software has had a newer release on GitHub func CheckForUpdate(client *api.Client, stateFilePath, repo, currentVersion string) (*ReleaseInfo, error) { - latestRelease, err := getLatestReleaseInfo(client, stateFilePath, repo, currentVersion) + stateEntry, _ := getStateEntry(stateFilePath) + if stateEntry != nil && time.Since(stateEntry.CheckedForUpdateAt).Hours() < 24 { + return nil, nil + } + + releaseInfo, err := getLatestReleaseInfo(client, repo) if err != nil { return nil, err } - if versionGreaterThan(latestRelease.Version, currentVersion) { - return latestRelease, nil + err = setStateEntry(stateFilePath, time.Now(), *releaseInfo) + if err != nil { + return nil, err + } + + if versionGreaterThan(releaseInfo.Version, currentVersion) { + return releaseInfo, nil } return nil, nil } -func getLatestReleaseInfo(client *api.Client, stateFilePath, repo, currentVersion string) (*ReleaseInfo, error) { - stateEntry, err := getStateEntry(stateFilePath) - if err == nil && time.Since(stateEntry.CheckedForUpdateAt).Hours() < 24 { - return &stateEntry.LatestRelease, nil - } - +func getLatestReleaseInfo(client *api.Client, repo string) (*ReleaseInfo, error) { var latestRelease ReleaseInfo - err = client.REST(ghinstance.Default(), "GET", fmt.Sprintf("repos/%s/releases/latest", repo), nil, &latestRelease) - if err != nil { - return nil, err - } - - err = setStateEntry(stateFilePath, time.Now(), latestRelease) + err := client.REST(ghinstance.Default(), "GET", fmt.Sprintf("repos/%s/releases/latest", repo), nil, &latestRelease) if err != nil { return nil, err } @@ -77,12 +85,23 @@ func setStateEntry(stateFilePath string, t time.Time, r ReleaseInfo) error { if err != nil { return err } - _ = ioutil.WriteFile(stateFilePath, content, 0600) - return nil + err = os.MkdirAll(filepath.Dir(stateFilePath), 0755) + if err != nil { + return err + } + + err = ioutil.WriteFile(stateFilePath, content, 0600) + return err } func versionGreaterThan(v, w string) bool { + w = gitDescribeSuffixRE.ReplaceAllStringFunc(w, func(m string) string { + idx := strings.IndexRune(m, '-') + n, _ := strconv.Atoi(m[0:idx]) + return fmt.Sprintf("%d-pre.0", n+1) + }) + vv, ve := version.NewVersion(v) vw, we := version.NewVersion(w) diff --git a/update/update_test.go b/internal/update/update_test.go similarity index 67% rename from update/update_test.go rename to internal/update/update_test.go index 2fcb2d6ab6e..282bd185f81 100644 --- a/update/update_test.go +++ b/internal/update/update_test.go @@ -1,15 +1,14 @@ package update import ( - "bytes" "fmt" "io/ioutil" "log" "os" "testing" - "github.com/cli/cli/api" - "github.com/cli/cli/pkg/httpmock" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/pkg/httpmock" ) func TestCheckForUpdate(t *testing.T) { @@ -34,6 +33,27 @@ func TestCheckForUpdate(t *testing.T) { LatestURL: "https://www.spacejam.com/archive/spacejam/movie/jam.htm", ExpectsResult: true, }, + { + Name: "current is built from source", + CurrentVersion: "v1.2.3-123-gdeadbeef", + LatestVersion: "v1.2.3", + LatestURL: "https://www.spacejam.com/archive/spacejam/movie/jam.htm", + ExpectsResult: false, + }, + { + Name: "current is built from source after a prerelease", + CurrentVersion: "v1.2.3-rc.1-123-gdeadbeef", + LatestVersion: "v1.2.3", + LatestURL: "https://www.spacejam.com/archive/spacejam/movie/jam.htm", + ExpectsResult: true, + }, + { + Name: "latest is newer than version build from source", + CurrentVersion: "v1.2.3-123-gdeadbeef", + LatestVersion: "v1.2.4", + LatestURL: "https://www.spacejam.com/archive/spacejam/movie/jam.htm", + ExpectsResult: true, + }, { Name: "latest is current", CurrentVersion: "v1.0.0", @@ -54,10 +74,14 @@ func TestCheckForUpdate(t *testing.T) { t.Run(s.Name, func(t *testing.T) { http := &httpmock.Registry{} client := api.NewClient(api.ReplaceTripper(http)) - http.StubResponse(200, bytes.NewBufferString(fmt.Sprintf(`{ - "tag_name": "%s", - "html_url": "%s" - }`, s.LatestVersion, s.LatestURL))) + + http.Register( + httpmock.REST("GET", "repos/OWNER/REPO/releases/latest"), + httpmock.StringResponse(fmt.Sprintf(`{ + "tag_name": "%s", + "html_url": "%s" + }`, s.LatestVersion, s.LatestURL)), + ) rel, err := CheckForUpdate(client, tempFilePath(), "OWNER/REPO", s.CurrentVersion) if err != nil { diff --git a/pkg/browser/browser.go b/pkg/browser/browser.go deleted file mode 100644 index 133b3dbb1f5..00000000000 --- a/pkg/browser/browser.go +++ /dev/null @@ -1,74 +0,0 @@ -package browser - -import ( - "os" - "os/exec" - "runtime" - "strings" - - "github.com/google/shlex" -) - -// BrowserEnv simply returns the $BROWSER environment variable -func FromEnv() string { - return os.Getenv("BROWSER") -} - -// Command produces an exec.Cmd respecting runtime.GOOS and $BROWSER environment variable -func Command(url string) (*exec.Cmd, error) { - launcher := FromEnv() - if launcher != "" { - return FromLauncher(launcher, url) - } - return ForOS(runtime.GOOS, url), nil -} - -// ForOS produces an exec.Cmd to open the web browser for different OS -func ForOS(goos, url string) *exec.Cmd { - exe := "open" - var args []string - switch goos { - case "darwin": - args = append(args, url) - case "windows": - exe = "cmd" - r := strings.NewReplacer("&", "^&") - args = append(args, "/c", "start", r.Replace(url)) - default: - exe = linuxExe() - args = append(args, url) - } - - cmd := exec.Command(exe, args...) - cmd.Stderr = os.Stderr - return cmd -} - -// FromLauncher parses the launcher string based on shell splitting rules -func FromLauncher(launcher, url string) (*exec.Cmd, error) { - args, err := shlex.Split(launcher) - if err != nil { - return nil, err - } - - args = append(args, url) - cmd := exec.Command(args[0], args[1:]...) - cmd.Stderr = os.Stderr - return cmd, nil -} - -func linuxExe() string { - exe := "xdg-open" - - _, err := lookPath(exe) - if err != nil { - _, err := lookPath("wslview") - if err == nil { - exe = "wslview" - } - } - - return exe -} - -var lookPath = exec.LookPath diff --git a/pkg/browser/browser_test.go b/pkg/browser/browser_test.go deleted file mode 100644 index 48b91f7c138..00000000000 --- a/pkg/browser/browser_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package browser - -import ( - "errors" - "reflect" - "testing" -) - -func TestForOS(t *testing.T) { - type args struct { - goos string - url string - } - tests := []struct { - name string - args args - exe string - want []string - }{ - { - name: "macOS", - args: args{ - goos: "darwin", - url: "https://example.com/path?a=1&b=2", - }, - want: []string{"open", "https://example.com/path?a=1&b=2"}, - }, - { - name: "Linux", - args: args{ - goos: "linux", - url: "https://example.com/path?a=1&b=2", - }, - exe: "xdg-open", - want: []string{"xdg-open", "https://example.com/path?a=1&b=2"}, - }, - { - name: "WSL", - args: args{ - goos: "linux", - url: "https://example.com/path?a=1&b=2", - }, - exe: "wslview", - want: []string{"wslview", "https://example.com/path?a=1&b=2"}, - }, - { - name: "Windows", - args: args{ - goos: "windows", - url: "https://example.com/path?a=1&b=2&c=3", - }, - want: []string{"cmd", "/c", "start", "https://example.com/path?a=1^&b=2^&c=3"}, - }, - } - for _, tt := range tests { - lookPath = func(file string) (string, error) { - if file == tt.exe { - return file, nil - } else { - return "", errors.New("not found") - } - } - - t.Run(tt.name, func(t *testing.T) { - if cmd := ForOS(tt.args.goos, tt.args.url); !reflect.DeepEqual(cmd.Args, tt.want) { - t.Errorf("ForOS() = %v, want %v", cmd.Args, tt.want) - } - }) - } -} diff --git a/pkg/cmd/actions/actions.go b/pkg/cmd/actions/actions.go new file mode 100644 index 00000000000..1ce0dc23366 --- /dev/null +++ b/pkg/cmd/actions/actions.go @@ -0,0 +1,60 @@ +package actions + +import ( + "fmt" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/spf13/cobra" +) + +func NewCmdActions(f *cmdutil.Factory) *cobra.Command { + cs := f.IOStreams.ColorScheme() + + cmd := &cobra.Command{ + Use: "actions", + Short: "Learn about working with GitHub actions", + Long: actionsExplainer(cs), + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintln(f.IOStreams.Out, actionsExplainer(cs)) + }, + Annotations: map[string]string{ + "IsActions": "true", + }, + } + + cmdutil.DisableAuthCheck(cmd) + + return cmd +} + +func actionsExplainer(cs *iostreams.ColorScheme) string { + header := cs.Bold("Welcome to GitHub Actions on the command line.") + runHeader := cs.Bold("Interacting with workflow runs") + workflowHeader := cs.Bold("Interacting with workflow files") + + return heredoc.Docf(` + %s + + GitHub CLI integrates with Actions to help you manage runs and workflows. + + %s + gh run list: List recent workflow runs + gh run view: View details for a workflow run or one of its jobs + gh run watch: Watch a workflow run while it executes + gh run rerun: Rerun a failed workflow run + gh run download: Download artifacts generated by runs + + To see more help, run 'gh help run ' + + %s + gh workflow list: List all the workflow files in your repository + gh workflow view: View details for a workflow file + gh workflow enable: Enable a workflow file + gh workflow disable: Disable a workflow file + gh workflow run: Trigger a workflow_dispatch run for a workflow file + + To see more help, run 'gh help workflow ' + `, header, runHeader, workflowHeader) +} diff --git a/pkg/cmd/alias/alias.go b/pkg/cmd/alias/alias.go index 0a2971f0439..46d7e2bc819 100644 --- a/pkg/cmd/alias/alias.go +++ b/pkg/cmd/alias/alias.go @@ -2,16 +2,16 @@ package alias import ( "github.com/MakeNowJust/heredoc" - deleteCmd "github.com/cli/cli/pkg/cmd/alias/delete" - listCmd "github.com/cli/cli/pkg/cmd/alias/list" - setCmd "github.com/cli/cli/pkg/cmd/alias/set" - "github.com/cli/cli/pkg/cmdutil" + deleteCmd "github.com/cli/cli/v2/pkg/cmd/alias/delete" + listCmd "github.com/cli/cli/v2/pkg/cmd/alias/list" + setCmd "github.com/cli/cli/v2/pkg/cmd/alias/set" + "github.com/cli/cli/v2/pkg/cmdutil" "github.com/spf13/cobra" ) func NewCmdAlias(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ - Use: "alias", + Use: "alias ", Short: "Create command shortcuts", Long: heredoc.Doc(` Aliases can be used to make shortcuts for gh commands or to compose multiple commands. diff --git a/pkg/cmd/alias/delete/delete.go b/pkg/cmd/alias/delete/delete.go index a0ff1973b06..85372d18152 100644 --- a/pkg/cmd/alias/delete/delete.go +++ b/pkg/cmd/alias/delete/delete.go @@ -3,9 +3,9 @@ package delete import ( "fmt" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" ) @@ -62,8 +62,8 @@ func deleteRun(opts *DeleteOptions) error { } if opts.IO.IsStdoutTTY() { - redCheck := opts.IO.ColorScheme().Red("✓") - fmt.Fprintf(opts.IO.ErrOut, "%s Deleted alias %s; was %s\n", redCheck, opts.Name, expansion) + cs := opts.IO.ColorScheme() + fmt.Fprintf(opts.IO.ErrOut, "%s Deleted alias %s; was %s\n", cs.SuccessIconWithColor(cs.Red), opts.Name, expansion) } return nil diff --git a/pkg/cmd/alias/delete/delete_test.go b/pkg/cmd/alias/delete/delete_test.go index 3c6acea2840..ae9e6930761 100644 --- a/pkg/cmd/alias/delete/delete_test.go +++ b/pkg/cmd/alias/delete/delete_test.go @@ -6,9 +6,9 @@ import ( "testing" "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -76,9 +76,7 @@ func TestAliasDelete(t *testing.T) { _, err = cmd.ExecuteC() if tt.wantErr != "" { - if assert.Error(t, err) { - assert.Equal(t, tt.wantErr, err.Error()) - } + assert.EqualError(t, err, tt.wantErr) return } require.NoError(t, err) diff --git a/pkg/cmd/alias/expand/expand.go b/pkg/cmd/alias/expand/expand.go index b2117f19851..f67a939425a 100644 --- a/pkg/cmd/alias/expand/expand.go +++ b/pkg/cmd/alias/expand/expand.go @@ -3,14 +3,13 @@ package expand import ( "errors" "fmt" - "os" "os/exec" - "path/filepath" "regexp" "runtime" "strings" - "github.com/cli/cli/internal/config" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/findsh" "github.com/google/shlex" ) @@ -80,27 +79,15 @@ func ExpandAlias(cfg config.Config, args []string, findShFunc func() (string, er } func findSh() (string, error) { - shPath, err := exec.LookPath("sh") - if err == nil { - return shPath, nil - } - - if runtime.GOOS == "windows" { - winNotFoundErr := errors.New("unable to locate sh to execute the shell alias with. The sh.exe interpreter is typically distributed with Git for Windows.") - // We can try and find a sh executable in a Git for Windows install - gitPath, err := exec.LookPath("git") - if err != nil { - return "", winNotFoundErr - } - - shPath = filepath.Join(filepath.Dir(gitPath), "..", "bin", "sh.exe") - _, err = os.Stat(shPath) - if err != nil { - return "", winNotFoundErr + shPath, err := findsh.Find() + if err != nil { + if errors.Is(err, exec.ErrNotFound) { + if runtime.GOOS == "windows" { + return "", errors.New("unable to locate sh to execute the shell alias with. The sh.exe interpreter is typically distributed with Git for Windows.") + } + return "", errors.New("unable to locate sh to execute shell alias with") } - - return shPath, nil + return "", err } - - return "", errors.New("unable to locate sh to execute shell alias with") + return shPath, nil } diff --git a/pkg/cmd/alias/expand/expand_test.go b/pkg/cmd/alias/expand/expand_test.go index b9535f7c7cf..33af4b07315 100644 --- a/pkg/cmd/alias/expand/expand_test.go +++ b/pkg/cmd/alias/expand/expand_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/internal/config" + "github.com/cli/cli/v2/internal/config" ) func TestExpandAlias(t *testing.T) { diff --git a/pkg/cmd/alias/list/list.go b/pkg/cmd/alias/list/list.go index a8576db5c62..85609e7a468 100644 --- a/pkg/cmd/alias/list/list.go +++ b/pkg/cmd/alias/list/list.go @@ -5,10 +5,10 @@ import ( "sort" "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" - "github.com/cli/cli/utils" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/utils" "github.com/spf13/cobra" ) diff --git a/pkg/cmd/alias/list/list_test.go b/pkg/cmd/alias/list/list_test.go index b058a04ce08..88943841cdd 100644 --- a/pkg/cmd/alias/list/list_test.go +++ b/pkg/cmd/alias/list/list_test.go @@ -6,9 +6,9 @@ import ( "testing" "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/cmd/alias/set/set.go b/pkg/cmd/alias/set/set.go index 294a4bd0ed4..22f8d4c7330 100644 --- a/pkg/cmd/alias/set/set.go +++ b/pkg/cmd/alias/set/set.go @@ -2,12 +2,13 @@ package set import ( "fmt" + "io/ioutil" "strings" "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" "github.com/google/shlex" "github.com/spf13/cobra" ) @@ -19,7 +20,8 @@ type SetOptions struct { Name string Expansion string IsShell bool - RootCmd *cobra.Command + + validCommand func(string) bool } func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command { @@ -32,44 +34,62 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command Use: "set ", Short: "Create a shortcut for a gh command", Long: heredoc.Doc(` - Declare a word as a command alias that will expand to the specified command(s). - - The expansion may specify additional arguments and flags. If the expansion - includes positional placeholders such as '$1', '$2', etc., any extra arguments - that follow the invocation of an alias will be inserted appropriately. + Define a word that will expand to a full gh command when invoked. - If '--shell' is specified, the alias will be run through a shell interpreter (sh). This allows you - to compose commands with "|" or redirect with ">". Note that extra arguments following the alias - will not be automatically passed to the expanded expression. To have a shell alias receive - arguments, you must explicitly accept them using "$1", "$2", etc., or "$@" to accept all of them. + The expansion may specify additional arguments and flags. If the expansion includes + positional placeholders such as "$1", extra arguments that follow the alias will be + inserted appropriately. Otherwise, extra arguments will be appended to the expanded + command. - Platform note: on Windows, shell aliases are executed via "sh" as installed by Git For Windows. If - you have installed git on Windows in some other way, shell aliases may not work for you. + Use "-" as expansion argument to read the expansion string from standard input. This + is useful to avoid quoting issues when defining expansions. - Quotes must always be used when defining a command as in the examples. + If the expansion starts with "!" or if "--shell" was given, the expansion is a shell + expression that will be evaluated through the "sh" interpreter when the alias is + invoked. This allows for chaining multiple commands via piping and redirection. `), Example: heredoc.Doc(` + # note: Command Prompt on Windows requires using double quotes for arguments $ gh alias set pv 'pr view' - $ gh pv -w 123 - #=> gh pr view -w 123 - - $ gh alias set bugs 'issue list --label="bugs"' + $ gh pv -w 123 #=> gh pr view -w 123 + + $ gh alias set bugs 'issue list --label=bugs' + $ gh bugs + + $ gh alias set homework 'issue list --assignee @me' + $ gh homework $ gh alias set epicsBy 'issue list --author="$1" --label="epic"' - $ gh epicsBy vilmibm - #=> gh issue list --author="vilmibm" --label="epic" + $ gh epicsBy vilmibm #=> gh issue list --author="vilmibm" --label="epic" - $ gh alias set --shell igrep 'gh issue list --label="$1" | grep $2' - $ gh igrep epic foo - #=> gh issue list --label="epic" | grep "foo" + $ gh alias set --shell igrep 'gh issue list --label="$1" | grep "$2"' + $ gh igrep epic foo #=> gh issue list --label="epic" | grep "foo" `), Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - opts.RootCmd = cmd.Root() - opts.Name = args[0] opts.Expansion = args[1] + opts.validCommand = func(args string) bool { + split, err := shlex.Split(args) + if err != nil { + return false + } + + rootCmd := cmd.Root() + cmd, _, err := rootCmd.Traverse(split) + if err == nil && cmd != rootCmd { + return true + } + + for _, ext := range f.ExtensionManager.List(false) { + if ext.Name() == split[0] { + return true + } + } + return false + } + if runF != nil { return runF(opts) } @@ -94,23 +114,27 @@ func setRun(opts *SetOptions) error { return err } + expansion, err := getExpansion(opts) + if err != nil { + return fmt.Errorf("did not understand expansion: %w", err) + } + isTerminal := opts.IO.IsStdoutTTY() if isTerminal { - fmt.Fprintf(opts.IO.ErrOut, "- Adding alias for %s: %s\n", cs.Bold(opts.Name), cs.Bold(opts.Expansion)) + fmt.Fprintf(opts.IO.ErrOut, "- Adding alias for %s: %s\n", cs.Bold(opts.Name), cs.Bold(expansion)) } - expansion := opts.Expansion isShell := opts.IsShell if isShell && !strings.HasPrefix(expansion, "!") { expansion = "!" + expansion } isShell = strings.HasPrefix(expansion, "!") - if validCommand(opts.RootCmd, opts.Name) { + if opts.validCommand(opts.Name) { return fmt.Errorf("could not create alias: %q is already a gh command", opts.Name) } - if !isShell && !validCommand(opts.RootCmd, expansion) { + if !isShell && !opts.validCommand(expansion) { return fmt.Errorf("could not create alias: %s does not correspond to a gh command", expansion) } @@ -136,12 +160,15 @@ func setRun(opts *SetOptions) error { return nil } -func validCommand(rootCmd *cobra.Command, expansion string) bool { - split, err := shlex.Split(expansion) - if err != nil { - return false +func getExpansion(opts *SetOptions) (string, error) { + if opts.Expansion == "-" { + stdin, err := ioutil.ReadAll(opts.IO.In) + if err != nil { + return "", fmt.Errorf("failed to read from STDIN: %w", err) + } + + return string(stdin), nil } - cmd, _, err := rootCmd.Traverse(split) - return err == nil && cmd != rootCmd + return opts.Expansion, nil } diff --git a/pkg/cmd/alias/set/set_test.go b/pkg/cmd/alias/set/set_test.go index 95f3a50317d..e68ae88aa0e 100644 --- a/pkg/cmd/alias/set/set_test.go +++ b/pkg/cmd/alias/set/set_test.go @@ -6,27 +6,34 @@ import ( "testing" "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" - "github.com/cli/cli/test" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/extensions" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/test" "github.com/google/shlex" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func runCommand(cfg config.Config, isTTY bool, cli string) (*test.CmdOut, error) { - io, _, stdout, stderr := iostreams.Test() +func runCommand(cfg config.Config, isTTY bool, cli string, in string) (*test.CmdOut, error) { + io, stdin, stdout, stderr := iostreams.Test() io.SetStdoutTTY(isTTY) io.SetStdinTTY(isTTY) io.SetStderrTTY(isTTY) + stdin.WriteString(in) factory := &cmdutil.Factory{ IOStreams: io, Config: func() (config.Config, error) { return cfg, nil }, + ExtensionManager: &extensions.ExtensionManagerMock{ + ListFunc: func(bool) []extensions.Extension { + return []extensions.Extension{} + }, + }, } cmd := NewCmdSet(factory, nil) @@ -41,6 +48,9 @@ func runCommand(cfg config.Config, isTTY bool, cli string) (*test.CmdOut, error) issueCmd := &cobra.Command{Use: "issue"} issueCmd.AddCommand(&cobra.Command{Use: "list"}) rootCmd.AddCommand(issueCmd) + apiCmd := &cobra.Command{Use: "api"} + apiCmd.AddCommand(&cobra.Command{Use: "graphql"}) + rootCmd.AddCommand(apiCmd) argv, err := shlex.Split("set " + cli) if err != nil { @@ -48,7 +58,7 @@ func runCommand(cfg config.Config, isTTY bool, cli string) (*test.CmdOut, error) } rootCmd.SetArgs(argv) - rootCmd.SetIn(&bytes.Buffer{}) + rootCmd.SetIn(stdin) rootCmd.SetOut(ioutil.Discard) rootCmd.SetErr(ioutil.Discard) @@ -64,11 +74,8 @@ func TestAliasSet_gh_command(t *testing.T) { cfg := config.NewFromString(``) - _, err := runCommand(cfg, true, "pr 'pr status'") - - if assert.Error(t, err) { - assert.Equal(t, `could not create alias: "pr" is already a gh command`, err.Error()) - } + _, err := runCommand(cfg, true, "pr 'pr status'", "") + assert.EqualError(t, err, `could not create alias: "pr" is already a gh command`) } func TestAliasSet_empty_aliases(t *testing.T) { @@ -80,13 +87,15 @@ func TestAliasSet_empty_aliases(t *testing.T) { editor: vim `)) - output, err := runCommand(cfg, true, "co 'pr checkout'") + output, err := runCommand(cfg, true, "co 'pr checkout'", "") if err != nil { t.Fatalf("unexpected error: %s", err) } + //nolint:staticcheck // prefer exact matchers over ExpectLines test.ExpectLines(t, output.Stderr(), "Added alias") + //nolint:staticcheck // prefer exact matchers over ExpectLines test.ExpectLines(t, output.String(), "") expected := `aliases: @@ -105,9 +114,10 @@ func TestAliasSet_existing_alias(t *testing.T) { co: pr checkout `)) - output, err := runCommand(cfg, true, "co 'pr checkout -Rcool/repo'") + output, err := runCommand(cfg, true, "co 'pr checkout -Rcool/repo'", "") require.NoError(t, err) + //nolint:staticcheck // prefer exact matchers over ExpectLines test.ExpectLines(t, output.Stderr(), "Changed alias.*co.*from.*pr checkout.*to.*pr checkout -Rcool/repo") } @@ -117,11 +127,13 @@ func TestAliasSet_space_args(t *testing.T) { cfg := config.NewFromString(``) - output, err := runCommand(cfg, true, `il 'issue list -l "cool story"'`) + output, err := runCommand(cfg, true, `il 'issue list -l "cool story"'`, "") require.NoError(t, err) + //nolint:staticcheck // prefer exact matchers over ExpectLines test.ExpectLines(t, output.Stderr(), `Adding alias for.*il.*issue list -l "cool story"`) + //nolint:staticcheck // prefer exact matchers over ExpectLines test.ExpectLines(t, mainBuf.String(), `il: issue list -l "cool story"`) } @@ -151,12 +163,14 @@ func TestAliasSet_arg_processing(t *testing.T) { cfg := config.NewFromString(``) - output, err := runCommand(cfg, true, c.Cmd) + output, err := runCommand(cfg, true, c.Cmd, "") if err != nil { t.Fatalf("got unexpected error running %s: %s", c.Cmd, err) } + //nolint:staticcheck // prefer exact matchers over ExpectLines test.ExpectLines(t, output.Stderr(), c.ExpectedOutputLine) + //nolint:staticcheck // prefer exact matchers over ExpectLines test.ExpectLines(t, mainBuf.String(), c.ExpectedConfigLine) }) } @@ -170,7 +184,7 @@ func TestAliasSet_init_alias_cfg(t *testing.T) { editor: vim `)) - output, err := runCommand(cfg, true, "diff 'pr diff'") + output, err := runCommand(cfg, true, "diff 'pr diff'", "") require.NoError(t, err) expected := `editor: vim @@ -178,6 +192,7 @@ aliases: diff: pr diff ` + //nolint:staticcheck // prefer exact matchers over ExpectLines test.ExpectLines(t, output.Stderr(), "Adding alias for.*diff.*pr diff", "Added alias.") assert.Equal(t, expected, mainBuf.String()) } @@ -191,7 +206,7 @@ func TestAliasSet_existing_aliases(t *testing.T) { foo: bar `)) - output, err := runCommand(cfg, true, "view 'pr view'") + output, err := runCommand(cfg, true, "view 'pr view'", "") require.NoError(t, err) expected := `aliases: @@ -199,6 +214,7 @@ func TestAliasSet_existing_aliases(t *testing.T) { view: pr view ` + //nolint:staticcheck // prefer exact matchers over ExpectLines test.ExpectLines(t, output.Stderr(), "Adding alias for.*view.*pr view", "Added alias.") assert.Equal(t, expected, mainBuf.String()) @@ -209,10 +225,8 @@ func TestAliasSet_invalid_command(t *testing.T) { cfg := config.NewFromString(``) - _, err := runCommand(cfg, true, "co 'pe checkout'") - if assert.Error(t, err) { - assert.Equal(t, "could not create alias: pe checkout does not correspond to a gh command", err.Error()) - } + _, err := runCommand(cfg, true, "co 'pe checkout'", "") + assert.EqualError(t, err, "could not create alias: pe checkout does not correspond to a gh command") } func TestShellAlias_flag(t *testing.T) { @@ -221,11 +235,12 @@ func TestShellAlias_flag(t *testing.T) { cfg := config.NewFromString(``) - output, err := runCommand(cfg, true, "--shell igrep 'gh issue list | grep'") + output, err := runCommand(cfg, true, "--shell igrep 'gh issue list | grep'", "") if err != nil { t.Fatalf("unexpected error: %s", err) } + //nolint:staticcheck // prefer exact matchers over ExpectLines test.ExpectLines(t, output.Stderr(), "Adding alias for.*igrep") expected := `aliases: @@ -240,9 +255,10 @@ func TestShellAlias_bang(t *testing.T) { cfg := config.NewFromString(``) - output, err := runCommand(cfg, true, "igrep '!gh issue list | grep'") + output, err := runCommand(cfg, true, "igrep '!gh issue list | grep'", "") require.NoError(t, err) + //nolint:staticcheck // prefer exact matchers over ExpectLines test.ExpectLines(t, output.Stderr(), "Adding alias for.*igrep") expected := `aliases: @@ -250,3 +266,80 @@ func TestShellAlias_bang(t *testing.T) { ` assert.Equal(t, expected, mainBuf.String()) } + +func TestShellAlias_from_stdin(t *testing.T) { + mainBuf := bytes.Buffer{} + defer config.StubWriteConfig(&mainBuf, ioutil.Discard)() + + cfg := config.NewFromString(``) + + output, err := runCommand(cfg, true, "users -", `api graphql -F name="$1" -f query=' + query ($name: String!) { + user(login: $name) { + name + } + }'`) + + require.NoError(t, err) + + //nolint:staticcheck // prefer exact matchers over ExpectLines + test.ExpectLines(t, output.Stderr(), "Adding alias for.*users") + + expected := `aliases: + users: |- + api graphql -F name="$1" -f query=' + query ($name: String!) { + user(login: $name) { + name + } + }' +` + + assert.Equal(t, expected, mainBuf.String()) +} + +func TestShellAlias_getExpansion(t *testing.T) { + tests := []struct { + name string + want string + expansionArg string + stdin string + }{ + { + name: "co", + want: "pr checkout", + expansionArg: "pr checkout", + }, + { + name: "co", + want: "pr checkout", + expansionArg: "pr checkout", + stdin: "api graphql -F name=\"$1\"", + }, + { + name: "stdin", + expansionArg: "-", + want: "api graphql -F name=\"$1\"", + stdin: "api graphql -F name=\"$1\"", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + io, stdin, _, _ := iostreams.Test() + + io.SetStdinTTY(false) + + _, err := stdin.WriteString(tt.stdin) + assert.NoError(t, err) + + expansion, err := getExpansion(&SetOptions{ + Expansion: tt.expansionArg, + IO: io, + }) + assert.NoError(t, err) + + assert.Equal(t, expansion, tt.want) + }) + } +} diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index 4d64a260bd2..7d9faf9b689 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -14,13 +14,17 @@ import ( "strconv" "strings" "syscall" + "time" "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/internal/ghinstance" - "github.com/cli/cli/internal/ghrepo" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" - "github.com/cli/cli/pkg/jsoncolor" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/export" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/jsoncolor" "github.com/spf13/cobra" ) @@ -35,10 +39,15 @@ type ApiOptions struct { MagicFields []string RawFields []string RequestHeaders []string + Previews []string ShowResponseHeaders bool Paginate bool Silent bool + Template string + CacheTTL time.Duration + FilterOutput string + Config func() (config.Config, error) HttpClient func() (*http.Client, error) BaseRepo func() (ghrepo.Interface, error) Branch func() (string, error) @@ -47,6 +56,7 @@ type ApiOptions struct { func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command { opts := ApiOptions{ IO: f.IOStreams, + Config: f.Config, HttpClient: f.HttpClient, BaseRepo: f.BaseRepo, Branch: f.Branch, @@ -55,45 +65,72 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command cmd := &cobra.Command{ Use: "api ", Short: "Make an authenticated GitHub API request", - Long: `Makes an authenticated HTTP request to the GitHub API and prints the response. - -The endpoint argument should either be a path of a GitHub API v3 endpoint, or -"graphql" to access the GitHub API v4. - -Placeholder values ":owner", ":repo", and ":branch" in the endpoint argument will -get replaced with values from the repository of the current directory. + Long: heredoc.Docf(` + Makes an authenticated HTTP request to the GitHub API and prints the response. + + The endpoint argument should either be a path of a GitHub API v3 endpoint, or + "graphql" to access the GitHub API v4. + + Placeholder values "{owner}", "{repo}", and "{branch}" in the endpoint argument will + get replaced with values from the repository of the current directory. Note that in + some shells, for example PowerShell, you may need to enclose any value that contains + "{...}" in quotes to prevent the shell from applying special meaning to curly braces. + + The default HTTP request method is "GET" normally and "POST" if any parameters + were added. Override the method with %[1]s--method%[1]s. + + Pass one or more %[1]s--raw-field%[1]s values in "key=value" format to add string + parameters to the request payload. To add non-string parameters, see %[1]s--field%[1]s below. + Note that adding request parameters will automatically switch the request method to POST. + To send the parameters as a GET query string instead, use %[1]s--method%[1]s GET. + + The %[1]s--field%[1]s flag behaves like %[1]s--raw-field%[1]s with magic type conversion based + on the format of the value: + + - literal values "true", "false", "null", and integer numbers get converted to + appropriate JSON types; + - placeholder values "{owner}", "{repo}", and "{branch}" get populated with values + from the repository of the current directory; + - if the value starts with "@", the rest of the value is interpreted as a + filename to read the value from. Pass "-" to read from standard input. + + For GraphQL requests, all fields other than "query" and "operationName" are + interpreted as GraphQL variables. + + Raw request body may be passed from the outside via a file specified by %[1]s--input%[1]s. + Pass "-" to read from standard input. In this mode, parameters specified via + %[1]s--field%[1]s flags are serialized into URL query parameters. + + In %[1]s--paginate%[1]s mode, all pages of results will sequentially be requested until + there are no more pages of results. For GraphQL requests, this requires that the + original query accepts an %[1]s$endCursor: String%[1]s variable and that it fetches the + %[1]spageInfo{ hasNextPage, endCursor }%[1]s set of fields from a collection. + `, "`"), + Example: heredoc.Doc(` + # list releases in the current repository + $ gh api repos/{owner}/{repo}/releases -The default HTTP request method is "GET" normally and "POST" if any parameters -were added. Override the method with '--method'. + # post an issue comment + $ gh api repos/{owner}/{repo}/issues/123/comments -f body='Hi from CLI' -Pass one or more '--raw-field' values in "key=value" format to add -JSON-encoded string parameters to the POST body. + # add parameters to a GET request + $ gh api -X GET search/issues -f q='repo:cli/cli is:open remote' -The '--field' flag behaves like '--raw-field' with magic type conversion based -on the format of the value: + # set a custom HTTP header + $ gh api -H 'Accept: application/vnd.github.v3.raw+json' ... -- literal values "true", "false", "null", and integer numbers get converted to - appropriate JSON types; -- placeholder values ":owner", ":repo", and ":branch" get populated with values - from the repository of the current directory; -- if the value starts with "@", the rest of the value is interpreted as a - filename to read the value from. Pass "-" to read from standard input. + # opt into GitHub API previews + $ gh api --preview baptiste,nebula ... -For GraphQL requests, all fields other than "query" and "operationName" are -interpreted as GraphQL variables. + # print only specific fields from the response + $ gh api repos/{owner}/{repo}/issues --jq '.[].title' -Raw request body may be passed from the outside via a file specified by '--input'. -Pass "-" to read from standard input. In this mode, parameters specified via -'--field' flags are serialized into URL query parameters. + # use a template for the output + $ gh api repos/{owner}/{repo}/issues --template \ + '{{range .}}{{.title}} ({{.labels | pluck "name" | join ", " | color "yellow"}}){{"\n"}}{{end}}' -In '--paginate' mode, all pages of results will sequentially be requested until -there are no more pages of results. For GraphQL requests, this requires that the -original query accepts an '$endCursor: String' variable and that it fetches the -'pageInfo{ hasNextPage, endCursor }' set of fields from a collection.`, - Example: heredoc.Doc(` - $ gh api repos/:owner/:repo/releases - - $ gh api graphql -F owner=':owner' -F name=':repo' -f query=' + # list releases with GraphQL + $ gh api graphql -F owner='{owner}' -F name='{repo}' -f query=' query($name: String!, $owner: String!) { repository(owner: $owner, name: $name) { releases(last: 3) { @@ -103,6 +140,7 @@ original query accepts an '$endCursor: String' variable and that it fetches the } ' + # list all repositories for a user $ gh api graphql --paginate -f query=' query($endCursor: String) { viewer { @@ -119,9 +157,11 @@ original query accepts an '$endCursor: String' variable and that it fetches the `), Annotations: map[string]string{ "help:environment": heredoc.Doc(` - GITHUB_TOKEN: an authentication token for github.com API requests. + GH_TOKEN, GITHUB_TOKEN (in order of precedence): an authentication token for + github.com API requests. - GITHUB_ENTERPRISE_TOKEN: an authentication token for API requests to GitHub Enterprise. + GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN (in order of precedence): an + authentication token for API requests to GitHub Enterprise. GH_HOST: make the request to a GitHub host other than github.com. `), @@ -133,15 +173,29 @@ original query accepts an '$endCursor: String' variable and that it fetches the if c.Flags().Changed("hostname") { if err := ghinstance.HostnameValidator(opts.Hostname); err != nil { - return &cmdutil.FlagError{Err: fmt.Errorf("error parsing --hostname: %w", err)} + return &cmdutil.FlagError{Err: fmt.Errorf("error parsing `--hostname`: %w", err)} } } if opts.Paginate && !strings.EqualFold(opts.RequestMethod, "GET") && opts.RequestPath != "graphql" { - return &cmdutil.FlagError{Err: errors.New(`the '--paginate' option is not supported for non-GET requests`)} + return &cmdutil.FlagError{Err: errors.New("the `--paginate` option is not supported for non-GET requests")} } - if opts.Paginate && opts.RequestInputFile != "" { - return &cmdutil.FlagError{Err: errors.New(`the '--paginate' option is not supported with '--input'`)} + + if err := cmdutil.MutuallyExclusive( + "the `--paginate` option is not supported with `--input`", + opts.Paginate, + opts.RequestInputFile != "", + ); err != nil { + return err + } + + if err := cmdutil.MutuallyExclusive( + "only one of `--template`, `--jq`, or `--silent` may be used", + opts.Silent, + opts.FilterOutput != "", + opts.Template != "", + ); err != nil { + return err } if runF != nil { @@ -153,13 +207,17 @@ original query accepts an '$endCursor: String' variable and that it fetches the cmd.Flags().StringVar(&opts.Hostname, "hostname", "", "The GitHub hostname for the request (default \"github.com\")") cmd.Flags().StringVarP(&opts.RequestMethod, "method", "X", "GET", "The HTTP method for the request") - cmd.Flags().StringArrayVarP(&opts.MagicFields, "field", "F", nil, "Add a parameter of inferred type") - cmd.Flags().StringArrayVarP(&opts.RawFields, "raw-field", "f", nil, "Add a string parameter") - cmd.Flags().StringArrayVarP(&opts.RequestHeaders, "header", "H", nil, "Add an additional HTTP request header") + cmd.Flags().StringArrayVarP(&opts.MagicFields, "field", "F", nil, "Add a typed parameter in `key=value` format") + cmd.Flags().StringArrayVarP(&opts.RawFields, "raw-field", "f", nil, "Add a string parameter in `key=value` format") + cmd.Flags().StringArrayVarP(&opts.RequestHeaders, "header", "H", nil, "Add a HTTP request header in `key:value` format") + cmd.Flags().StringSliceVarP(&opts.Previews, "preview", "p", nil, "Opt into GitHub API previews") cmd.Flags().BoolVarP(&opts.ShowResponseHeaders, "include", "i", false, "Include HTTP response headers in the output") cmd.Flags().BoolVar(&opts.Paginate, "paginate", false, "Make additional HTTP requests to fetch all pages of results") - cmd.Flags().StringVar(&opts.RequestInputFile, "input", "", "The file to use as body for the HTTP request") + cmd.Flags().StringVar(&opts.RequestInputFile, "input", "", "The `file` to use as body for the HTTP request (use \"-\" to read from standard input)") cmd.Flags().BoolVar(&opts.Silent, "silent", false, "Do not print the response body") + cmd.Flags().StringVarP(&opts.Template, "template", "t", "", "Format the response using a Go template") + cmd.Flags().StringVarP(&opts.FilterOutput, "jq", "q", "", "Query to select values from the response using jq syntax") + cmd.Flags().DurationVar(&opts.CacheTTL, "cache", 0, "Cache the response, e.g. \"3600s\", \"60m\", \"1h\"") return cmd } @@ -199,10 +257,17 @@ func apiRun(opts *ApiOptions) error { } } + if len(opts.Previews) > 0 { + requestHeaders = append(requestHeaders, "Accept: "+previewNamesToMIMETypes(opts.Previews)) + } + httpClient, err := opts.HttpClient() if err != nil { return err } + if opts.CacheTTL > 0 { + httpClient = api.NewCachedClient(httpClient, opts.CacheTTL) + } headersOutputStream := opts.IO.Out if opts.Silent { @@ -215,11 +280,22 @@ func apiRun(opts *ApiOptions) error { defer opts.IO.StopPager() } - host := ghinstance.OverridableDefault() + cfg, err := opts.Config() + if err != nil { + return err + } + + host, err := cfg.DefaultHost() + if err != nil { + return err + } + if opts.Hostname != "" { host = opts.Hostname } + template := export.NewTemplate(opts.IO, opts.Template) + hasNextPage := true for hasNextPage { resp, err := httpRequest(httpClient, host, method, requestPath, requestBody, requestHeaders) @@ -227,7 +303,7 @@ func apiRun(opts *ApiOptions) error { return err } - endCursor, err := processResponse(resp, opts, headersOutputStream) + endCursor, err := processResponse(resp, opts, headersOutputStream, &template) if err != nil { return err } @@ -250,10 +326,10 @@ func apiRun(opts *ApiOptions) error { } } - return nil + return template.End() } -func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream io.Writer) (endCursor string, err error) { +func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream io.Writer, template *export.Template) (endCursor string, err error) { if opts.ShowResponseHeaders { fmt.Fprintln(headersOutputStream, resp.Proto, resp.Status) printHeaders(headersOutputStream, resp.Header, opts.IO.ColorEnabled()) @@ -283,7 +359,19 @@ func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream responseBody = io.TeeReader(responseBody, bodyCopy) } - if isJSON && opts.IO.ColorEnabled() { + if opts.FilterOutput != "" { + // TODO: reuse parsed query across pagination invocations + err = export.FilterJSON(opts.IO.Out, responseBody, opts.FilterOutput) + if err != nil { + return + } + } else if opts.Template != "" { + // TODO: reuse parsed template across pagination invocations + err = template.Execute(responseBody) + if err != nil { + return + } + } else if isJSON && opts.IO.ColorEnabled() { err = jsoncolor.Write(opts.IO.Out, responseBody, " ") } else { _, err = io.Copy(opts.IO.Out, responseBody) @@ -296,12 +384,14 @@ func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream } } + if serverError == "" && resp.StatusCode > 299 { + serverError = fmt.Sprintf("HTTP %d", resp.StatusCode) + } if serverError != "" { fmt.Fprintf(opts.IO.ErrOut, "gh: %s\n", serverError) - err = cmdutil.SilentError - return - } else if resp.StatusCode > 299 { - fmt.Fprintf(opts.IO.ErrOut, "gh: HTTP %d\n", resp.StatusCode) + if msg := api.ScopesSuggestion(resp); msg != "" { + fmt.Fprintf(opts.IO.ErrOut, "gh: %s\n", msg) + } err = cmdutil.SilentError return } @@ -313,41 +403,41 @@ func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream return } -var placeholderRE = regexp.MustCompile(`\:(owner|repo|branch)\b`) +var placeholderRE = regexp.MustCompile(`(\:(owner|repo|branch)\b|\{[a-z]+\})`) -// fillPlaceholders populates `:owner` and `:repo` placeholders with values from the current repository +// fillPlaceholders replaces placeholders with values from the current repository func fillPlaceholders(value string, opts *ApiOptions) (string, error) { - if !placeholderRE.MatchString(value) { - return value, nil - } + var err error + return placeholderRE.ReplaceAllStringFunc(value, func(m string) string { + var name string + if m[0] == ':' { + name = m[1:] + } else { + name = m[1 : len(m)-1] + } - baseRepo, err := opts.BaseRepo() - if err != nil { - return value, err - } - - filled := placeholderRE.ReplaceAllStringFunc(value, func(m string) string { - switch m { - case ":owner": - return baseRepo.RepoOwner() - case ":repo": - return baseRepo.RepoName() - case ":branch": - branch, e := opts.Branch() - if e != nil { + switch name { + case "owner": + if baseRepo, e := opts.BaseRepo(); e == nil { + return baseRepo.RepoOwner() + } else { + err = e + } + case "repo": + if baseRepo, e := opts.BaseRepo(); e == nil { + return baseRepo.RepoName() + } else { + err = e + } + case "branch": + if branch, e := opts.Branch(); e == nil { + return branch + } else { err = e } - return branch - default: - panic(fmt.Sprintf("invalid placeholder: %q", m)) } - }) - - if err != nil { - return value, err - } - - return filled, nil + return m + }), err } func printHeaders(w io.Writer, headers http.Header, colorize bool) { @@ -403,7 +493,7 @@ func parseField(f string) (string, string, error) { func magicFieldValue(v string, opts *ApiOptions) (interface{}, error) { if strings.HasPrefix(v, "@") { - return readUserFile(v[1:], opts.IO.In) + return opts.IO.ReadUserFile(v[1:]) } if n, err := strconv.Atoi(v); err == nil { @@ -422,21 +512,6 @@ func magicFieldValue(v string, opts *ApiOptions) (interface{}, error) { } } -func readUserFile(fn string, stdin io.ReadCloser) ([]byte, error) { - var r io.ReadCloser - if fn == "-" { - r = stdin - } else { - var err error - r, err = os.Open(fn) - if err != nil { - return nil, err - } - } - defer r.Close() - return ioutil.ReadAll(r) -} - func openUserFile(fn string, stdin io.ReadCloser) (io.ReadCloser, int64, error) { if fn == "-" { return stdin, -1, nil @@ -505,3 +580,11 @@ func parseErrorResponse(r io.Reader, statusCode int) (io.Reader, string, error) return bodyCopy, "", nil } + +func previewNamesToMIMETypes(names []string) string { + types := []string{fmt.Sprintf("application/vnd.github.%s-preview+json", names[0])} + for _, p := range names[1:] { + types = append(types, fmt.Sprintf("application/vnd.github.%s-preview", p)) + } + return strings.Join(types, ", ") +} diff --git a/pkg/cmd/api/api_test.go b/pkg/cmd/api/api_test.go index 6cd693ce1a5..daed26926bc 100644 --- a/pkg/cmd/api/api_test.go +++ b/pkg/cmd/api/api_test.go @@ -7,12 +7,18 @@ import ( "io/ioutil" "net/http" "os" + "path/filepath" + "strings" "testing" + "time" - "github.com/cli/cli/git" - "github.com/cli/cli/internal/ghrepo" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/export" + "github.com/cli/cli/v2/pkg/iostreams" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -42,6 +48,9 @@ func Test_NewCmdApi(t *testing.T) { ShowResponseHeaders: false, Paginate: false, Silent: false, + CacheTTL: 0, + Template: "", + FilterOutput: "", }, wantsErr: false, }, @@ -60,6 +69,9 @@ func Test_NewCmdApi(t *testing.T) { ShowResponseHeaders: false, Paginate: false, Silent: false, + CacheTTL: 0, + Template: "", + FilterOutput: "", }, wantsErr: false, }, @@ -78,6 +90,9 @@ func Test_NewCmdApi(t *testing.T) { ShowResponseHeaders: false, Paginate: false, Silent: false, + CacheTTL: 0, + Template: "", + FilterOutput: "", }, wantsErr: false, }, @@ -96,6 +111,9 @@ func Test_NewCmdApi(t *testing.T) { ShowResponseHeaders: true, Paginate: false, Silent: false, + CacheTTL: 0, + Template: "", + FilterOutput: "", }, wantsErr: false, }, @@ -114,6 +132,9 @@ func Test_NewCmdApi(t *testing.T) { ShowResponseHeaders: false, Paginate: true, Silent: false, + CacheTTL: 0, + Template: "", + FilterOutput: "", }, wantsErr: false, }, @@ -132,6 +153,9 @@ func Test_NewCmdApi(t *testing.T) { ShowResponseHeaders: false, Paginate: false, Silent: true, + CacheTTL: 0, + Template: "", + FilterOutput: "", }, wantsErr: false, }, @@ -155,6 +179,9 @@ func Test_NewCmdApi(t *testing.T) { ShowResponseHeaders: false, Paginate: true, Silent: false, + CacheTTL: 0, + Template: "", + FilterOutput: "", }, wantsErr: false, }, @@ -178,6 +205,9 @@ func Test_NewCmdApi(t *testing.T) { ShowResponseHeaders: false, Paginate: false, Silent: false, + CacheTTL: 0, + Template: "", + FilterOutput: "", }, wantsErr: false, }, @@ -201,22 +231,96 @@ func Test_NewCmdApi(t *testing.T) { ShowResponseHeaders: false, Paginate: false, Silent: false, + CacheTTL: 0, + Template: "", + FilterOutput: "", }, wantsErr: false, }, + { + name: "with cache", + cli: "user --cache 5m", + wants: ApiOptions{ + Hostname: "", + RequestMethod: "GET", + RequestMethodPassed: false, + RequestPath: "user", + RequestInputFile: "", + RawFields: []string(nil), + MagicFields: []string(nil), + RequestHeaders: []string(nil), + ShowResponseHeaders: false, + Paginate: false, + Silent: false, + CacheTTL: time.Minute * 5, + Template: "", + FilterOutput: "", + }, + wantsErr: false, + }, + { + name: "with template", + cli: "user -t 'hello {{.name}}'", + wants: ApiOptions{ + Hostname: "", + RequestMethod: "GET", + RequestMethodPassed: false, + RequestPath: "user", + RequestInputFile: "", + RawFields: []string(nil), + MagicFields: []string(nil), + RequestHeaders: []string(nil), + ShowResponseHeaders: false, + Paginate: false, + Silent: false, + CacheTTL: 0, + Template: "hello {{.name}}", + FilterOutput: "", + }, + wantsErr: false, + }, + { + name: "with jq filter", + cli: "user -q .name", + wants: ApiOptions{ + Hostname: "", + RequestMethod: "GET", + RequestMethodPassed: false, + RequestPath: "user", + RequestInputFile: "", + RawFields: []string(nil), + MagicFields: []string(nil), + RequestHeaders: []string(nil), + ShowResponseHeaders: false, + Paginate: false, + Silent: false, + CacheTTL: 0, + Template: "", + FilterOutput: ".name", + }, + wantsErr: false, + }, + { + name: "--silent with --jq", + cli: "user --silent -q .foo", + wantsErr: true, + }, + { + name: "--silent with --template", + cli: "user --silent -t '{{.foo}}'", + wantsErr: true, + }, + { + name: "--jq with --template", + cli: "user --jq .foo -t '{{.foo}}'", + wantsErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + var opts *ApiOptions cmd := NewCmdApi(f, func(o *ApiOptions) error { - assert.Equal(t, tt.wants.Hostname, o.Hostname) - assert.Equal(t, tt.wants.RequestMethod, o.RequestMethod) - assert.Equal(t, tt.wants.RequestMethodPassed, o.RequestMethodPassed) - assert.Equal(t, tt.wants.RequestPath, o.RequestPath) - assert.Equal(t, tt.wants.RequestInputFile, o.RequestInputFile) - assert.Equal(t, tt.wants.RawFields, o.RawFields) - assert.Equal(t, tt.wants.MagicFields, o.MagicFields) - assert.Equal(t, tt.wants.RequestHeaders, o.RequestHeaders) - assert.Equal(t, tt.wants.ShowResponseHeaders, o.ShowResponseHeaders) + opts = o return nil }) @@ -232,6 +336,21 @@ func Test_NewCmdApi(t *testing.T) { return } assert.NoError(t, err) + + assert.Equal(t, tt.wants.Hostname, opts.Hostname) + assert.Equal(t, tt.wants.RequestMethod, opts.RequestMethod) + assert.Equal(t, tt.wants.RequestMethodPassed, opts.RequestMethodPassed) + assert.Equal(t, tt.wants.RequestPath, opts.RequestPath) + assert.Equal(t, tt.wants.RequestInputFile, opts.RequestInputFile) + assert.Equal(t, tt.wants.RawFields, opts.RawFields) + assert.Equal(t, tt.wants.MagicFields, opts.MagicFields) + assert.Equal(t, tt.wants.RequestHeaders, opts.RequestHeaders) + assert.Equal(t, tt.wants.ShowResponseHeaders, opts.ShowResponseHeaders) + assert.Equal(t, tt.wants.Paginate, opts.Paginate) + assert.Equal(t, tt.wants.Silent, opts.Silent) + assert.Equal(t, tt.wants.CacheTTL, opts.CacheTTL) + assert.Equal(t, tt.wants.Template, opts.Template) + assert.Equal(t, tt.wants.FilterOutput, opts.FilterOutput) }) } } @@ -357,6 +476,34 @@ func Test_apiRun(t *testing.T) { stdout: "HTTP/1.1 200 Okey-dokey\nContent-Type: text/plain\r\n\r\n", stderr: ``, }, + { + name: "output template", + options: ApiOptions{ + Template: `{{.status}}`, + }, + httpResponse: &http.Response{ + StatusCode: 200, + Body: ioutil.NopCloser(bytes.NewBufferString(`{"status":"not a cat"}`)), + Header: http.Header{"Content-Type": []string{"application/json"}}, + }, + err: nil, + stdout: "not a cat", + stderr: ``, + }, + { + name: "jq filter", + options: ApiOptions{ + FilterOutput: `.[].name`, + }, + httpResponse: &http.Response{ + StatusCode: 200, + Body: ioutil.NopCloser(bytes.NewBufferString(`[{"name":"Mona"},{"name":"Hubot"}]`)), + Header: http.Header{"Content-Type": []string{"application/json"}}, + }, + err: nil, + stdout: "Mona\nHubot\n", + stderr: ``, + }, } for _, tt := range tests { @@ -364,6 +511,7 @@ func Test_apiRun(t *testing.T) { io, _, stdout, stderr := iostreams.Test() tt.options.IO = io + tt.options.Config = func() (config.Config, error) { return config.NewBlankConfig(), nil } tt.options.HttpClient = func() (*http.Client, error) { var tr roundTripper = func(req *http.Request) (*http.Response, error) { resp := tt.httpResponse @@ -425,6 +573,9 @@ func Test_apiRun_paginationREST(t *testing.T) { } return &http.Client{Transport: tr}, nil }, + Config: func() (config.Config, error) { + return config.NewBlankConfig(), nil + }, RequestPath: "issues", Paginate: true, @@ -485,6 +636,9 @@ func Test_apiRun_paginationGraphQL(t *testing.T) { } return &http.Client{Transport: tr}, nil }, + Config: func() (config.Config, error) { + return config.NewBlankConfig(), nil + }, RequestMethod: "POST", RequestPath: "graphql", @@ -518,6 +672,101 @@ func Test_apiRun_paginationGraphQL(t *testing.T) { assert.Equal(t, "PAGE1_END", endCursor) } +func Test_apiRun_paginated_template(t *testing.T) { + io, _, stdout, stderr := iostreams.Test() + io.SetStdoutTTY(true) + + requestCount := 0 + responses := []*http.Response{ + { + StatusCode: 200, + Header: http.Header{"Content-Type": []string{`application/json`}}, + Body: ioutil.NopCloser(bytes.NewBufferString(`{ + "data": { + "nodes": [ + { + "page": 1, + "caption": "page one" + } + ], + "pageInfo": { + "endCursor": "PAGE1_END", + "hasNextPage": true + } + } + }`)), + }, + { + StatusCode: 200, + Header: http.Header{"Content-Type": []string{`application/json`}}, + Body: ioutil.NopCloser(bytes.NewBufferString(`{ + "data": { + "nodes": [ + { + "page": 20, + "caption": "page twenty" + } + ], + "pageInfo": { + "endCursor": "PAGE20_END", + "hasNextPage": false + } + } + }`)), + }, + } + + options := ApiOptions{ + IO: io, + HttpClient: func() (*http.Client, error) { + var tr roundTripper = func(req *http.Request) (*http.Response, error) { + resp := responses[requestCount] + resp.Request = req + requestCount++ + return resp, nil + } + return &http.Client{Transport: tr}, nil + }, + Config: func() (config.Config, error) { + return config.NewBlankConfig(), nil + }, + + RequestMethod: "POST", + RequestPath: "graphql", + Paginate: true, + // test that templates executed per page properly render a table. + Template: `{{range .data.nodes}}{{tablerow .page .caption}}{{end}}`, + } + + err := apiRun(&options) + require.NoError(t, err) + + assert.Equal(t, heredoc.Doc(` + 1 page one + 20 page twenty + `), stdout.String(), "stdout") + assert.Equal(t, "", stderr.String(), "stderr") + + var requestData struct { + Variables map[string]interface{} + } + + bb, err := ioutil.ReadAll(responses[0].Request.Body) + require.NoError(t, err) + err = json.Unmarshal(bb, &requestData) + require.NoError(t, err) + _, hasCursor := requestData.Variables["endCursor"].(string) + assert.Equal(t, false, hasCursor) + + bb, err = ioutil.ReadAll(responses[1].Request.Body) + require.NoError(t, err) + err = json.Unmarshal(bb, &requestData) + require.NoError(t, err) + endCursor, hasCursor := requestData.Variables["endCursor"].(string) + assert.Equal(t, true, hasCursor) + assert.Equal(t, "PAGE1_END", endCursor) +} + func Test_apiRun_inputFile(t *testing.T) { tests := []struct { name string @@ -540,6 +789,9 @@ func Test_apiRun_inputFile(t *testing.T) { contentLength: 10, }, } + + tempDir := t.TempDir() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { io, stdin, _, _ := iostreams.Test() @@ -549,13 +801,12 @@ func Test_apiRun_inputFile(t *testing.T) { if tt.inputFile == "-" { _, _ = stdin.Write(tt.inputContents) } else { - f, err := ioutil.TempFile("", tt.inputFile) + f, err := ioutil.TempFile(tempDir, tt.inputFile) if err != nil { t.Fatal(err) } _, _ = f.Write(tt.inputContents) - f.Close() - t.Cleanup(func() { os.Remove(f.Name()) }) + defer f.Close() inputFile = f.Name() } @@ -577,6 +828,9 @@ func Test_apiRun_inputFile(t *testing.T) { } return &http.Client{Transport: tr}, nil }, + Config: func() (config.Config, error) { + return config.NewBlankConfig(), nil + }, } err := apiRun(&options) @@ -593,6 +847,45 @@ func Test_apiRun_inputFile(t *testing.T) { } } +func Test_apiRun_cache(t *testing.T) { + io, _, stdout, stderr := iostreams.Test() + + requestCount := 0 + options := ApiOptions{ + IO: io, + HttpClient: func() (*http.Client, error) { + var tr roundTripper = func(req *http.Request) (*http.Response, error) { + requestCount++ + return &http.Response{ + Request: req, + StatusCode: 204, + }, nil + } + return &http.Client{Transport: tr}, nil + }, + Config: func() (config.Config, error) { + return config.NewBlankConfig(), nil + }, + + RequestPath: "issues", + CacheTTL: time.Minute, + } + + t.Cleanup(func() { + cacheDir := filepath.Join(os.TempDir(), "gh-cli-cache") + os.RemoveAll(cacheDir) + }) + + err := apiRun(&options) + assert.NoError(t, err) + err = apiRun(&options) + assert.NoError(t, err) + + assert.Equal(t, 1, requestCount) + assert.Equal(t, "", stdout.String(), "stdout") + assert.Equal(t, "", stderr.String(), "stderr") +} + func Test_parseFields(t *testing.T) { io, stdin, _, _ := iostreams.Test() fmt.Fprint(stdin, "pasted contents") @@ -630,13 +923,13 @@ func Test_parseFields(t *testing.T) { } func Test_magicFieldValue(t *testing.T) { - f, err := ioutil.TempFile("", "gh-test") + f, err := ioutil.TempFile(t.TempDir(), "gh-test") if err != nil { t.Fatal(err) } + defer f.Close() + fmt.Fprint(f, "file contents") - f.Close() - t.Cleanup(func() { os.Remove(f.Name()) }) io, _, _, _ := iostreams.Test() @@ -675,7 +968,7 @@ func Test_magicFieldValue(t *testing.T) { wantErr: false, }, { - name: "placeholder", + name: "placeholder colon", args: args{ v: ":owner", opts: &ApiOptions{ @@ -688,6 +981,20 @@ func Test_magicFieldValue(t *testing.T) { want: "hubot", wantErr: false, }, + { + name: "placeholder braces", + args: args{ + v: "{owner}", + opts: &ApiOptions{ + IO: io, + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.New("hubot", "robot-uprising"), nil + }, + }, + }, + want: "hubot", + wantErr: false, + }, { name: "file", args: args{ @@ -723,13 +1030,13 @@ func Test_magicFieldValue(t *testing.T) { } func Test_openUserFile(t *testing.T) { - f, err := ioutil.TempFile("", "gh-test") + f, err := ioutil.TempFile(t.TempDir(), "gh-test") if err != nil { t.Fatal(err) } + defer f.Close() + fmt.Fprint(f, "file contents") - f.Close() - t.Cleanup(func() { os.Remove(f.Name()) }) file, length, err := openUserFile(f.Name(), nil) if err != nil { @@ -769,7 +1076,7 @@ func Test_fillPlaceholders(t *testing.T) { wantErr: false, }, { - name: "has substitutes", + name: "has substitutes (colon)", args: args{ value: "repos/:owner/:repo/releases", opts: &ApiOptions{ @@ -782,39 +1089,96 @@ func Test_fillPlaceholders(t *testing.T) { wantErr: false, }, { - name: "has branch placeholder", + name: "has branch placeholder (colon)", + args: args{ + value: "repos/owner/repo/branches/:branch/protection/required_status_checks", + opts: &ApiOptions{ + BaseRepo: nil, + Branch: func() (string, error) { + return "trunk", nil + }, + }, + }, + want: "repos/owner/repo/branches/trunk/protection/required_status_checks", + wantErr: false, + }, + { + name: "has branch placeholder and git is in detached head (colon)", args: args{ - value: "repos/cli/cli/branches/:branch/protection/required_status_checks", + value: "repos/:owner/:repo/branches/:branch", opts: &ApiOptions{ BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("cli", "cli"), nil + return ghrepo.New("hubot", "robot-uprising"), nil }, + Branch: func() (string, error) { + return "", git.ErrNotOnAnyBranch + }, + }, + }, + want: "repos/hubot/robot-uprising/branches/:branch", + wantErr: true, + }, + { + name: "has substitutes", + args: args{ + value: "repos/{owner}/{repo}/releases", + opts: &ApiOptions{ + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.New("hubot", "robot-uprising"), nil + }, + }, + }, + want: "repos/hubot/robot-uprising/releases", + wantErr: false, + }, + { + name: "has branch placeholder", + args: args{ + value: "repos/owner/repo/branches/{branch}/protection/required_status_checks", + opts: &ApiOptions{ + BaseRepo: nil, Branch: func() (string, error) { return "trunk", nil }, }, }, - want: "repos/cli/cli/branches/trunk/protection/required_status_checks", + want: "repos/owner/repo/branches/trunk/protection/required_status_checks", wantErr: false, }, { name: "has branch placeholder and git is in detached head", args: args{ - value: "repos/:owner/:repo/branches/:branch", + value: "repos/{owner}/{repo}/branches/{branch}", opts: &ApiOptions{ BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("cli", "cli"), nil + return ghrepo.New("hubot", "robot-uprising"), nil + }, + Branch: func() (string, error) { + return "", git.ErrNotOnAnyBranch + }, + }, + }, + want: "repos/hubot/robot-uprising/branches/{branch}", + wantErr: true, + }, + { + name: "surfaces errors in earlier placeholders", + args: args{ + value: "{branch}-{owner}", + opts: &ApiOptions{ + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.New("hubot", "robot-uprising"), nil }, Branch: func() (string, error) { return "", git.ErrNotOnAnyBranch }, }, }, - want: "repos/:owner/:repo/branches/:branch", + want: "{branch}-hubot", wantErr: true, }, { - name: "no greedy substitutes", + name: "no greedy substitutes (colon)", args: args{ value: ":ownership/:repository", opts: &ApiOptions{ @@ -824,6 +1188,17 @@ func Test_fillPlaceholders(t *testing.T) { want: ":ownership/:repository", wantErr: false, }, + { + name: "non-placeholders are left intact", + args: args{ + value: "{}{ownership}/{repository}", + opts: &ApiOptions{ + BaseRepo: nil, + }, + }, + want: "{}{ownership}/{repository}", + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -838,3 +1213,71 @@ func Test_fillPlaceholders(t *testing.T) { }) } } + +func Test_previewNamesToMIMETypes(t *testing.T) { + tests := []struct { + name string + previews []string + want string + }{ + { + name: "single", + previews: []string{"nebula"}, + want: "application/vnd.github.nebula-preview+json", + }, + { + name: "multiple", + previews: []string{"nebula", "baptiste", "squirrel-girl"}, + want: "application/vnd.github.nebula-preview+json, application/vnd.github.baptiste-preview, application/vnd.github.squirrel-girl-preview", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := previewNamesToMIMETypes(tt.previews); got != tt.want { + t.Errorf("previewNamesToMIMETypes() = %q, want %q", got, tt.want) + } + }) + } +} + +func Test_processResponse_template(t *testing.T) { + io, _, stdout, stderr := iostreams.Test() + + resp := http.Response{ + StatusCode: 200, + Header: map[string][]string{ + "Content-Type": {"application/json"}, + }, + Body: ioutil.NopCloser(strings.NewReader(`[ + { + "title": "First title", + "labels": [{"name":"bug"}, {"name":"help wanted"}] + }, + { + "title": "Second but not last" + }, + { + "title": "Alas, tis' the end", + "labels": [{}, {"name":"feature"}] + } + ]`)), + } + + opts := ApiOptions{ + IO: io, + Template: `{{range .}}{{.title}} ({{.labels | pluck "name" | join ", " }}){{"\n"}}{{end}}`, + } + template := export.NewTemplate(io, opts.Template) + _, err := processResponse(&resp, &opts, ioutil.Discard, &template) + require.NoError(t, err) + + err = template.End() + require.NoError(t, err) + + assert.Equal(t, heredoc.Doc(` + First title (bug, help wanted) + Second but not last () + Alas, tis' the end (, feature) + `), stdout.String()) + assert.Equal(t, "", stderr.String()) +} diff --git a/pkg/cmd/api/http.go b/pkg/cmd/api/http.go index 974373e69fd..c056f312a3d 100644 --- a/pkg/cmd/api/http.go +++ b/pkg/cmd/api/http.go @@ -10,7 +10,7 @@ import ( "strconv" "strings" - "github.com/cli/cli/internal/ghinstance" + "github.com/cli/cli/v2/internal/ghinstance" ) func httpRequest(client *http.Client, hostname string, method string, p string, params interface{}, headers []string) (*http.Response, error) { diff --git a/pkg/cmd/api/pagination.go b/pkg/cmd/api/pagination.go index fce88fb921b..65d816480e4 100644 --- a/pkg/cmd/api/pagination.go +++ b/pkg/cmd/api/pagination.go @@ -14,7 +14,7 @@ var linkRE = regexp.MustCompile(`<([^>]+)>;\s*rel="([^"]+)"`) func findNextPage(resp *http.Response) (string, bool) { for _, m := range linkRE.FindAllStringSubmatch(resp.Header.Get("Link"), -1) { - if len(m) >= 2 && m[2] == "next" { + if len(m) > 2 && m[2] == "next" { return m[1], true } } diff --git a/pkg/cmd/auth/auth.go b/pkg/cmd/auth/auth.go index ddeb07aa6d8..99d335a8dc6 100644 --- a/pkg/cmd/auth/auth.go +++ b/pkg/cmd/auth/auth.go @@ -1,11 +1,12 @@ package auth import ( - authLoginCmd "github.com/cli/cli/pkg/cmd/auth/login" - authLogoutCmd "github.com/cli/cli/pkg/cmd/auth/logout" - authRefreshCmd "github.com/cli/cli/pkg/cmd/auth/refresh" - authStatusCmd "github.com/cli/cli/pkg/cmd/auth/status" - "github.com/cli/cli/pkg/cmdutil" + gitCredentialCmd "github.com/cli/cli/v2/pkg/cmd/auth/gitcredential" + authLoginCmd "github.com/cli/cli/v2/pkg/cmd/auth/login" + authLogoutCmd "github.com/cli/cli/v2/pkg/cmd/auth/logout" + authRefreshCmd "github.com/cli/cli/v2/pkg/cmd/auth/refresh" + authStatusCmd "github.com/cli/cli/v2/pkg/cmd/auth/status" + "github.com/cli/cli/v2/pkg/cmdutil" "github.com/spf13/cobra" ) @@ -22,6 +23,7 @@ func NewCmdAuth(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(authLogoutCmd.NewCmdLogout(f, nil)) cmd.AddCommand(authStatusCmd.NewCmdStatus(f, nil)) cmd.AddCommand(authRefreshCmd.NewCmdRefresh(f, nil)) + cmd.AddCommand(gitCredentialCmd.NewCmdCredential(f, nil)) return cmd } diff --git a/pkg/cmd/auth/client/client.go b/pkg/cmd/auth/client/client.go deleted file mode 100644 index bacde0b824f..00000000000 --- a/pkg/cmd/auth/client/client.go +++ /dev/null @@ -1,48 +0,0 @@ -package client - -import ( - "fmt" - "net/http" - - "github.com/cli/cli/api" - "github.com/cli/cli/internal/config" -) - -func ValidateHostCfg(hostname string, cfg config.Config) error { - apiClient, err := ClientFromCfg(hostname, cfg) - if err != nil { - return err - } - - err = apiClient.HasMinimumScopes(hostname) - if err != nil { - return fmt.Errorf("could not validate token: %w", err) - } - - return nil -} - -var ClientFromCfg = func(hostname string, cfg config.Config) (*api.Client, error) { - var opts []api.ClientOption - - token, err := cfg.Get(hostname, "oauth_token") - if err != nil { - return nil, err - } - - if token == "" { - return nil, fmt.Errorf("no token found in config for %s", hostname) - } - - opts = append(opts, - // no access to Version so the user agent is more generic here. - api.AddHeader("User-Agent", "GitHub CLI"), - api.AddHeaderFunc("Authorization", func(req *http.Request) (string, error) { - return fmt.Sprintf("token %s", token), nil - }), - ) - - httpClient := api.NewHTTPClient(opts...) - - return api.NewClientFromHTTP(httpClient), nil -} diff --git a/pkg/cmd/auth/gitcredential/helper.go b/pkg/cmd/auth/gitcredential/helper.go new file mode 100644 index 00000000000..8d1ab7ff392 --- /dev/null +++ b/pkg/cmd/auth/gitcredential/helper.go @@ -0,0 +1,125 @@ +package login + +import ( + "bufio" + "fmt" + "net/url" + "strings" + + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/spf13/cobra" +) + +const tokenUser = "x-access-token" + +type config interface { + GetWithSource(string, string) (string, string, error) +} + +type CredentialOptions struct { + IO *iostreams.IOStreams + Config func() (config, error) + + Operation string +} + +func NewCmdCredential(f *cmdutil.Factory, runF func(*CredentialOptions) error) *cobra.Command { + opts := &CredentialOptions{ + IO: f.IOStreams, + Config: func() (config, error) { + return f.Config() + }, + } + + cmd := &cobra.Command{ + Use: "git-credential", + Args: cobra.ExactArgs(1), + Short: "Implements git credential helper protocol", + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + opts.Operation = args[0] + + if runF != nil { + return runF(opts) + } + return helperRun(opts) + }, + } + + return cmd +} + +func helperRun(opts *CredentialOptions) error { + if opts.Operation == "store" { + // We pretend to implement the "store" operation, but do nothing since we already have a cached token. + return cmdutil.SilentError + } + + if opts.Operation != "get" { + return fmt.Errorf("gh auth git-credential: %q operation not supported", opts.Operation) + } + + wants := map[string]string{} + + s := bufio.NewScanner(opts.IO.In) + for s.Scan() { + line := s.Text() + if line == "" { + break + } + parts := strings.SplitN(line, "=", 2) + if len(parts) < 2 { + continue + } + key, value := parts[0], parts[1] + if key == "url" { + u, err := url.Parse(value) + if err != nil { + return err + } + wants["protocol"] = u.Scheme + wants["host"] = u.Host + wants["path"] = u.Path + wants["username"] = u.User.Username() + wants["password"], _ = u.User.Password() + } else { + wants[key] = value + } + } + if err := s.Err(); err != nil { + return err + } + + if wants["protocol"] != "https" { + return cmdutil.SilentError + } + + cfg, err := opts.Config() + if err != nil { + return err + } + + var gotUser string + gotToken, source, _ := cfg.GetWithSource(wants["host"], "oauth_token") + if strings.HasSuffix(source, "_TOKEN") { + gotUser = tokenUser + } else { + gotUser, _, _ = cfg.GetWithSource(wants["host"], "user") + } + + if gotUser == "" || gotToken == "" { + return cmdutil.SilentError + } + + if wants["username"] != "" && gotUser != tokenUser && !strings.EqualFold(wants["username"], gotUser) { + return cmdutil.SilentError + } + + fmt.Fprint(opts.IO.Out, "protocol=https\n") + fmt.Fprintf(opts.IO.Out, "host=%s\n", wants["host"]) + fmt.Fprintf(opts.IO.Out, "username=%s\n", gotUser) + fmt.Fprintf(opts.IO.Out, "password=%s\n", gotToken) + + return nil +} diff --git a/pkg/cmd/auth/gitcredential/helper_test.go b/pkg/cmd/auth/gitcredential/helper_test.go new file mode 100644 index 00000000000..7e30ec49580 --- /dev/null +++ b/pkg/cmd/auth/gitcredential/helper_test.go @@ -0,0 +1,184 @@ +package login + +import ( + "fmt" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/pkg/iostreams" +) + +type tinyConfig map[string]string + +func (c tinyConfig) GetWithSource(host, key string) (string, string, error) { + return c[fmt.Sprintf("%s:%s", host, key)], c["_source"], nil +} + +func Test_helperRun(t *testing.T) { + tests := []struct { + name string + opts CredentialOptions + input string + wantStdout string + wantStderr string + wantErr bool + }{ + { + name: "host only, credentials found", + opts: CredentialOptions{ + Operation: "get", + Config: func() (config, error) { + return tinyConfig{ + "_source": "/Users/monalisa/.config/gh/hosts.yml", + "example.com:user": "monalisa", + "example.com:oauth_token": "OTOKEN", + }, nil + }, + }, + input: heredoc.Doc(` + protocol=https + host=example.com + `), + wantErr: false, + wantStdout: heredoc.Doc(` + protocol=https + host=example.com + username=monalisa + password=OTOKEN + `), + wantStderr: "", + }, + { + name: "host plus user", + opts: CredentialOptions{ + Operation: "get", + Config: func() (config, error) { + return tinyConfig{ + "_source": "/Users/monalisa/.config/gh/hosts.yml", + "example.com:user": "monalisa", + "example.com:oauth_token": "OTOKEN", + }, nil + }, + }, + input: heredoc.Doc(` + protocol=https + host=example.com + username=monalisa + `), + wantErr: false, + wantStdout: heredoc.Doc(` + protocol=https + host=example.com + username=monalisa + password=OTOKEN + `), + wantStderr: "", + }, + { + name: "url input", + opts: CredentialOptions{ + Operation: "get", + Config: func() (config, error) { + return tinyConfig{ + "_source": "/Users/monalisa/.config/gh/hosts.yml", + "example.com:user": "monalisa", + "example.com:oauth_token": "OTOKEN", + }, nil + }, + }, + input: heredoc.Doc(` + url=https://monalisa@example.com + `), + wantErr: false, + wantStdout: heredoc.Doc(` + protocol=https + host=example.com + username=monalisa + password=OTOKEN + `), + wantStderr: "", + }, + { + name: "host only, no credentials found", + opts: CredentialOptions{ + Operation: "get", + Config: func() (config, error) { + return tinyConfig{ + "_source": "/Users/monalisa/.config/gh/hosts.yml", + "example.com:user": "monalisa", + }, nil + }, + }, + input: heredoc.Doc(` + protocol=https + host=example.com + `), + wantErr: true, + wantStdout: "", + wantStderr: "", + }, + { + name: "user mismatch", + opts: CredentialOptions{ + Operation: "get", + Config: func() (config, error) { + return tinyConfig{ + "_source": "/Users/monalisa/.config/gh/hosts.yml", + "example.com:user": "monalisa", + "example.com:oauth_token": "OTOKEN", + }, nil + }, + }, + input: heredoc.Doc(` + protocol=https + host=example.com + username=hubot + `), + wantErr: true, + wantStdout: "", + wantStderr: "", + }, + { + name: "token from env", + opts: CredentialOptions{ + Operation: "get", + Config: func() (config, error) { + return tinyConfig{ + "_source": "GITHUB_ENTERPRISE_TOKEN", + "example.com:oauth_token": "OTOKEN", + }, nil + }, + }, + input: heredoc.Doc(` + protocol=https + host=example.com + username=hubot + `), + wantErr: false, + wantStdout: heredoc.Doc(` + protocol=https + host=example.com + username=x-access-token + password=OTOKEN + `), + wantStderr: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + io, stdin, stdout, stderr := iostreams.Test() + fmt.Fprint(stdin, tt.input) + opts := &tt.opts + opts.IO = io + if err := helperRun(opts); (err != nil) != tt.wantErr { + t.Fatalf("helperRun() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.wantStdout != stdout.String() { + t.Errorf("stdout: got %q, wants %q", stdout.String(), tt.wantStdout) + } + if tt.wantStderr != stderr.String() { + t.Errorf("stderr: got %q, wants %q", stderr.String(), tt.wantStderr) + } + }) + } +} diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index d702d0a1a7c..08bcf1dfdad 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -4,24 +4,26 @@ import ( "errors" "fmt" "io/ioutil" + "net/http" "strings" "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/api" - "github.com/cli/cli/internal/authflow" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/internal/ghinstance" - "github.com/cli/cli/pkg/cmd/auth/client" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" - "github.com/cli/cli/pkg/prompt" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/pkg/cmd/auth/shared" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/prompt" "github.com/spf13/cobra" ) type LoginOptions struct { - IO *iostreams.IOStreams - Config func() (config.Config, error) + IO *iostreams.IOStreams + Config func() (config.Config, error) + HttpClient func() (*http.Client, error) + + MainExecutable string Interactive bool @@ -33,8 +35,9 @@ type LoginOptions struct { func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Command { opts := &LoginOptions{ - IO: f.IOStreams, - Config: f.Config, + IO: f.IOStreams, + Config: f.Config, + HttpClient: f.HttpClient, } var tokenStdin bool @@ -98,6 +101,7 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm } } + opts.MainExecutable = f.Executable() if runF != nil { return runF(opts) } @@ -120,200 +124,103 @@ func loginRun(opts *LoginOptions) error { return err } - if opts.Token != "" { - // I chose to not error on existing host here; my thinking is that for --with-token the user - // probably doesn't care if a token is overwritten since they have a token in hand they - // explicitly want to use. - if opts.Hostname == "" { - return errors.New("empty hostname would leak oauth_token") - } - - err := cfg.Set(opts.Hostname, "oauth_token", opts.Token) - if err != nil { - return err + hostname := opts.Hostname + if hostname == "" { + if opts.Interactive { + var err error + hostname, err = promptForHostname() + if err != nil { + return err + } + } else { + return errors.New("must specify --hostname") } + } - err = client.ValidateHostCfg(opts.Hostname, cfg) - if err != nil { - return err + if err := cfg.CheckWriteable(hostname, "oauth_token"); err != nil { + var roErr *config.ReadOnlyEnvError + if errors.As(err, &roErr) { + fmt.Fprintf(opts.IO.ErrOut, "The value of the %s environment variable is being used for authentication.\n", roErr.Variable) + fmt.Fprint(opts.IO.ErrOut, "To have GitHub CLI store credentials instead, first clear the value from the environment.\n") + return cmdutil.SilentError } - - return cfg.Write() + return err } - // TODO consider explicitly telling survey what io to use since it's implicit right now - - hostname := opts.Hostname - - if hostname == "" { - var hostType int - err := prompt.SurveyAskOne(&survey.Select{ - Message: "What account do you want to log into?", - Options: []string{ - "GitHub.com", - "GitHub Enterprise Server", - }, - }, &hostType) + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + if opts.Token != "" { + err := cfg.Set(hostname, "oauth_token", opts.Token) if err != nil { - return fmt.Errorf("could not prompt: %w", err) + return err } - isEnterprise := hostType == 1 - - hostname = ghinstance.Default() - if isEnterprise { - err := prompt.SurveyAskOne(&survey.Input{ - Message: "GHE hostname:", - }, &hostname, survey.WithValidator(ghinstance.HostnameValidator)) - if err != nil { - return fmt.Errorf("could not prompt: %w", err) - } + if err := shared.HasMinimumScopes(httpClient, hostname, opts.Token); err != nil { + return fmt.Errorf("error validating token: %w", err) } - } - fmt.Fprintf(opts.IO.ErrOut, "- Logging into %s\n", hostname) + return cfg.Write() + } existingToken, _ := cfg.Get(hostname, "oauth_token") - if existingToken != "" && opts.Interactive { - err := client.ValidateHostCfg(hostname, cfg) - if err == nil { - apiClient, err := client.ClientFromCfg(hostname, cfg) - if err != nil { - return err - } - - username, err := api.CurrentLoginName(apiClient, hostname) - if err != nil { - return fmt.Errorf("error using api: %w", err) - } + if err := shared.HasMinimumScopes(httpClient, hostname, existingToken); err == nil { var keepGoing bool err = prompt.SurveyAskOne(&survey.Confirm{ Message: fmt.Sprintf( - "You're already logged into %s as %s. Do you want to re-authenticate?", - hostname, - username), + "You're already logged into %s. Do you want to re-authenticate?", + hostname), Default: false, }, &keepGoing) if err != nil { return fmt.Errorf("could not prompt: %w", err) } - if !keepGoing { return nil } } } - if err := cfg.CheckWriteable(hostname, "oauth_token"); err != nil { - return err - } - - var authMode int - if opts.Web { - authMode = 0 - } else { - err = prompt.SurveyAskOne(&survey.Select{ - Message: "How would you like to authenticate?", - Options: []string{ - "Login with a web browser", - "Paste an authentication token", - }, - }, &authMode) - if err != nil { - return fmt.Errorf("could not prompt: %w", err) - } - } - - if authMode == 0 { - _, err := authflow.AuthFlowWithConfig(cfg, opts.IO, hostname, "", opts.Scopes) - if err != nil { - return fmt.Errorf("failed to authenticate via web browser: %w", err) - } - } else { - fmt.Fprintln(opts.IO.ErrOut) - fmt.Fprintln(opts.IO.ErrOut, heredoc.Doc(getAccessTokenTip(hostname))) - var token string - err := prompt.SurveyAskOne(&survey.Password{ - Message: "Paste your authentication token:", - }, &token, survey.WithValidator(survey.Required)) - if err != nil { - return fmt.Errorf("could not prompt: %w", err) - } - - if hostname == "" { - return errors.New("empty hostname would leak oauth_token") - } + return shared.Login(&shared.LoginOptions{ + IO: opts.IO, + Config: cfg, + HTTPClient: httpClient, + Hostname: hostname, + Interactive: opts.Interactive, + Web: opts.Web, + Scopes: opts.Scopes, + Executable: opts.MainExecutable, + }) +} - err = cfg.Set(hostname, "oauth_token", token) - if err != nil { - return err - } +func promptForHostname() (string, error) { + var hostType int + err := prompt.SurveyAskOne(&survey.Select{ + Message: "What account do you want to log into?", + Options: []string{ + "GitHub.com", + "GitHub Enterprise Server", + }, + }, &hostType) - err = client.ValidateHostCfg(hostname, cfg) - if err != nil { - return err - } + if err != nil { + return "", fmt.Errorf("could not prompt: %w", err) } - cs := opts.IO.ColorScheme() - - gitProtocol := "https" - if opts.Interactive { - err = prompt.SurveyAskOne(&survey.Select{ - Message: "Choose default git protocol", - Options: []string{ - "HTTPS", - "SSH", - }, - }, &gitProtocol) - if err != nil { - return fmt.Errorf("could not prompt: %w", err) - } - - gitProtocol = strings.ToLower(gitProtocol) + isEnterprise := hostType == 1 - fmt.Fprintf(opts.IO.ErrOut, "- gh config set -h %s git_protocol %s\n", hostname, gitProtocol) - err = cfg.Set(hostname, "git_protocol", gitProtocol) + hostname := ghinstance.Default() + if isEnterprise { + err := prompt.SurveyAskOne(&survey.Input{ + Message: "GHE hostname:", + }, &hostname, survey.WithValidator(ghinstance.HostnameValidator)) if err != nil { - return err + return "", fmt.Errorf("could not prompt: %w", err) } - - fmt.Fprintf(opts.IO.ErrOut, "%s Configured git protocol\n", cs.SuccessIcon()) - } - - apiClient, err := client.ClientFromCfg(hostname, cfg) - if err != nil { - return err - } - - username, err := api.CurrentLoginName(apiClient, hostname) - if err != nil { - return fmt.Errorf("error using api: %w", err) - } - - err = cfg.Set(hostname, "user", username) - if err != nil { - return err } - err = cfg.Write() - if err != nil { - return err - } - - fmt.Fprintf(opts.IO.ErrOut, "%s Logged in as %s\n", cs.SuccessIcon(), cs.Bold(username)) - - return nil -} - -func getAccessTokenTip(hostname string) string { - ghHostname := hostname - if ghHostname == "" { - ghHostname = ghinstance.OverridableDefault() - } - return fmt.Sprintf(` - Tip: you can generate a Personal Access Token here https://%s/settings/tokens - The minimum required scopes are 'repo' and 'read:org'.`, ghHostname) + return hostname, nil } diff --git a/pkg/cmd/auth/login/login_test.go b/pkg/cmd/auth/login/login_test.go index 9a6b5aa8b70..f6fbcabd543 100644 --- a/pkg/cmd/auth/login/login_test.go +++ b/pkg/cmd/auth/login/login_test.go @@ -3,16 +3,17 @@ package login import ( "bytes" "net/http" + "os" "regexp" "testing" - "github.com/cli/cli/api" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/pkg/cmd/auth/client" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/httpmock" - "github.com/cli/cli/pkg/iostreams" - "github.com/cli/cli/pkg/prompt" + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/run" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/prompt" "github.com/google/shlex" "github.com/stretchr/testify/assert" ) @@ -146,7 +147,8 @@ func Test_NewCmdLogin(t *testing.T) { t.Run(tt.name, func(t *testing.T) { io, stdin, _, _ := iostreams.Test() f := &cmdutil.Factory{ - IOStreams: io, + IOStreams: io, + Executable: func() string { return "/path/to/gh" }, } io.SetStdoutTTY(true) @@ -189,11 +191,13 @@ func Test_NewCmdLogin(t *testing.T) { func Test_loginRun_nontty(t *testing.T) { tests := []struct { - name string - opts *LoginOptions - httpStubs func(*httpmock.Registry) - wantHosts string - wantErr *regexp.Regexp + name string + opts *LoginOptions + httpStubs func(*httpmock.Registry) + env map[string]string + wantHosts string + wantErr string + wantStderr string }{ { name: "with token", @@ -201,6 +205,9 @@ func Test_loginRun_nontty(t *testing.T) { Hostname: "github.com", Token: "abc123", }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org")) + }, wantHosts: "github.com:\n oauth_token: abc123\n", }, { @@ -223,7 +230,7 @@ func Test_loginRun_nontty(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("read:org")) }, - wantErr: regexp.MustCompile(`missing required scope 'repo'`), + wantErr: `error validating token: missing required scope 'repo'`, }, { name: "missing read scope", @@ -234,7 +241,7 @@ func Test_loginRun_nontty(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo")) }, - wantErr: regexp.MustCompile(`missing required scope 'read:org'`), + wantErr: `error validating token: missing required scope 'read:org'`, }, { name: "has admin scope", @@ -247,6 +254,36 @@ func Test_loginRun_nontty(t *testing.T) { }, wantHosts: "github.com:\n oauth_token: abc456\n", }, + { + name: "github.com token from environment", + opts: &LoginOptions{ + Hostname: "github.com", + Token: "abc456", + }, + env: map[string]string{ + "GH_TOKEN": "value_from_env", + }, + wantErr: "SilentError", + wantStderr: heredoc.Doc(` + The value of the GH_TOKEN environment variable is being used for authentication. + To have GitHub CLI store credentials instead, first clear the value from the environment. + `), + }, + { + name: "GHE token from environment", + opts: &LoginOptions{ + Hostname: "ghe.io", + Token: "abc456", + }, + env: map[string]string{ + "GH_ENTERPRISE_TOKEN": "value_from_env", + }, + wantErr: "SilentError", + wantStderr: heredoc.Doc(` + The value of the GH_ENTERPRISE_TOKEN environment variable is being used for authentication. + To have GitHub CLI store credentials instead, first clear the value from the environment. + `), + }, } for _, tt := range tests { @@ -256,44 +293,52 @@ func Test_loginRun_nontty(t *testing.T) { io.SetStdoutTTY(false) tt.opts.Config = func() (config.Config, error) { - return config.NewBlankConfig(), nil + cfg := config.NewBlankConfig() + return config.InheritEnv(cfg), nil } tt.opts.IO = io t.Run(tt.name, func(t *testing.T) { reg := &httpmock.Registry{} - origClientFromCfg := client.ClientFromCfg + tt.opts.HttpClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + } + + old_GH_TOKEN := os.Getenv("GH_TOKEN") + os.Setenv("GH_TOKEN", tt.env["GH_TOKEN"]) + old_GITHUB_TOKEN := os.Getenv("GITHUB_TOKEN") + os.Setenv("GITHUB_TOKEN", tt.env["GITHUB_TOKEN"]) + old_GH_ENTERPRISE_TOKEN := os.Getenv("GH_ENTERPRISE_TOKEN") + os.Setenv("GH_ENTERPRISE_TOKEN", tt.env["GH_ENTERPRISE_TOKEN"]) + old_GITHUB_ENTERPRISE_TOKEN := os.Getenv("GITHUB_ENTERPRISE_TOKEN") + os.Setenv("GITHUB_ENTERPRISE_TOKEN", tt.env["GITHUB_ENTERPRISE_TOKEN"]) defer func() { - client.ClientFromCfg = origClientFromCfg + os.Setenv("GH_TOKEN", old_GH_TOKEN) + os.Setenv("GITHUB_TOKEN", old_GITHUB_TOKEN) + os.Setenv("GH_ENTERPRISE_TOKEN", old_GH_ENTERPRISE_TOKEN) + os.Setenv("GITHUB_ENTERPRISE_TOKEN", old_GITHUB_ENTERPRISE_TOKEN) }() - client.ClientFromCfg = func(_ string, _ config.Config) (*api.Client, error) { - httpClient := &http.Client{Transport: reg} - return api.NewClientFromHTTP(httpClient), nil - } if tt.httpStubs != nil { tt.httpStubs(reg) - } else { - reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org")) } + _, restoreRun := run.Stub() + defer restoreRun(t) + mainBuf := bytes.Buffer{} hostsBuf := bytes.Buffer{} defer config.StubWriteConfig(&mainBuf, &hostsBuf)() err := loginRun(tt.opts) - assert.Equal(t, tt.wantErr == nil, err == nil) - if err != nil { - if tt.wantErr != nil { - assert.True(t, tt.wantErr.MatchString(err.Error())) - return - } else { - t.Fatalf("unexpected error: %s", err) - } + if tt.wantErr != "" { + assert.EqualError(t, err, tt.wantErr) + } else { + assert.NoError(t, err) } assert.Equal(t, "", stdout.String()) - assert.Equal(t, "", stderr.String()) + assert.Equal(t, tt.wantStderr, stderr.String()) assert.Equal(t, tt.wantHosts, hostsBuf.String()) reg.Verify(t) }) @@ -306,6 +351,7 @@ func Test_loginRun_Survey(t *testing.T) { opts *LoginOptions httpStubs func(*httpmock.Registry) askStubs func(*prompt.AskStubber) + runStubs func(*run.CommandStubber) wantHosts string wantErrOut *regexp.Regexp cfg func(config.Config) @@ -319,17 +365,17 @@ func Test_loginRun_Survey(t *testing.T) { _ = cfg.Set("github.com", "oauth_token", "ghi789") }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) - reg.Register( - httpmock.GraphQL(`query UserCurrent\b`), - httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`)) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org")) + // reg.Register( + // httpmock.GraphQL(`query UserCurrent\b`), + // httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`)) }, askStubs: func(as *prompt.AskStubber) { as.StubOne(0) // host type github.com as.StubOne(false) // do not continue }, wantHosts: "", // nothing should have been written to hosts - wantErrOut: regexp.MustCompile("Logging into github.com"), + wantErrOut: nil, }, { name: "hostname set", @@ -337,14 +383,24 @@ func Test_loginRun_Survey(t *testing.T) { Hostname: "rebecca.chambers", Interactive: true, }, - wantHosts: "rebecca.chambers:\n oauth_token: def456\n git_protocol: https\n user: jillv\n", + wantHosts: heredoc.Doc(` + rebecca.chambers: + oauth_token: def456 + user: jillv + git_protocol: https + `), askStubs: func(as *prompt.AskStubber) { + as.StubOne("HTTPS") // git_protocol + as.StubOne(false) // cache credentials as.StubOne(1) // auth mode: token as.StubOne("def456") // auth token - as.StubOne("HTTPS") // git_protocol + }, + runStubs: func(rs *run.CommandStubber) { + rs.Register(`git config credential\.https:/`, 1, "") + rs.Register(`git config credential\.helper`, 1, "") }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`)) @@ -352,20 +408,30 @@ func Test_loginRun_Survey(t *testing.T) { wantErrOut: regexp.MustCompile("Tip: you can generate a Personal Access Token here https://rebecca.chambers/settings/tokens"), }, { - name: "choose enterprise", - wantHosts: "brad.vickers:\n oauth_token: def456\n git_protocol: https\n user: jillv\n", + name: "choose enterprise", + wantHosts: heredoc.Doc(` + brad.vickers: + oauth_token: def456 + user: jillv + git_protocol: https + `), opts: &LoginOptions{ Interactive: true, }, askStubs: func(as *prompt.AskStubber) { as.StubOne(1) // host type enterprise as.StubOne("brad.vickers") // hostname + as.StubOne("HTTPS") // git_protocol + as.StubOne(false) // cache credentials as.StubOne(1) // auth mode: token as.StubOne("def456") // auth token - as.StubOne("HTTPS") // git_protocol + }, + runStubs: func(rs *run.CommandStubber) { + rs.Register(`git config credential\.https:/`, 1, "") + rs.Register(`git config credential\.helper`, 1, "") }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,read:public_key")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`)) @@ -373,30 +439,46 @@ func Test_loginRun_Survey(t *testing.T) { wantErrOut: regexp.MustCompile("Tip: you can generate a Personal Access Token here https://brad.vickers/settings/tokens"), }, { - name: "choose github.com", - wantHosts: "github.com:\n oauth_token: def456\n git_protocol: https\n user: jillv\n", + name: "choose github.com", + wantHosts: heredoc.Doc(` + github.com: + oauth_token: def456 + user: jillv + git_protocol: https + `), opts: &LoginOptions{ Interactive: true, }, askStubs: func(as *prompt.AskStubber) { as.StubOne(0) // host type github.com + as.StubOne("HTTPS") // git_protocol + as.StubOne(false) // cache credentials as.StubOne(1) // auth mode: token as.StubOne("def456") // auth token - as.StubOne("HTTPS") // git_protocol + }, + runStubs: func(rs *run.CommandStubber) { + rs.Register(`git config credential\.https:/`, 1, "") + rs.Register(`git config credential\.helper`, 1, "") }, wantErrOut: regexp.MustCompile("Tip: you can generate a Personal Access Token here https://github.com/settings/tokens"), }, { - name: "sets git_protocol", - wantHosts: "github.com:\n oauth_token: def456\n git_protocol: ssh\n user: jillv\n", + name: "sets git_protocol", + wantHosts: heredoc.Doc(` + github.com: + oauth_token: def456 + user: jillv + git_protocol: ssh + `), opts: &LoginOptions{ Interactive: true, }, askStubs: func(as *prompt.AskStubber) { as.StubOne(0) // host type github.com + as.StubOne("SSH") // git_protocol + as.StubOne(10) // TODO: SSH key selection as.StubOne(1) // auth mode: token as.StubOne("def456") // auth token - as.StubOne("SSH") // git_protocol }, wantErrOut: regexp.MustCompile("Tip: you can generate a Personal Access Token here https://github.com/settings/tokens"), }, @@ -426,18 +508,13 @@ func Test_loginRun_Survey(t *testing.T) { t.Run(tt.name, func(t *testing.T) { reg := &httpmock.Registry{} - origClientFromCfg := client.ClientFromCfg - defer func() { - client.ClientFromCfg = origClientFromCfg - }() - client.ClientFromCfg = func(_ string, _ config.Config) (*api.Client, error) { - httpClient := &http.Client{Transport: reg} - return api.NewClientFromHTTP(httpClient), nil + tt.opts.HttpClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil } if tt.httpStubs != nil { tt.httpStubs(reg) } else { - reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`)) @@ -453,6 +530,12 @@ func Test_loginRun_Survey(t *testing.T) { tt.askStubs(as) } + rs, restoreRun := run.Stub() + defer restoreRun(t) + if tt.runStubs != nil { + tt.runStubs(rs) + } + err := loginRun(tt.opts) if err != nil { t.Fatalf("unexpected error: %s", err) diff --git a/pkg/cmd/auth/logout/logout.go b/pkg/cmd/auth/logout/logout.go index 699de39684f..670c4cc459e 100644 --- a/pkg/cmd/auth/logout/logout.go +++ b/pkg/cmd/auth/logout/logout.go @@ -7,11 +7,11 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/api" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" - "github.com/cli/cli/pkg/prompt" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/prompt" "github.com/spf13/cobra" ) @@ -74,6 +74,9 @@ func logoutRun(opts *LogoutOptions) error { candidates, err := cfg.Hosts() if err != nil { + return err + } + if len(candidates) == 0 { return fmt.Errorf("not logged in to any hosts") } @@ -105,6 +108,12 @@ func logoutRun(opts *LogoutOptions) error { } if err := cfg.CheckWriteable(hostname, "oauth_token"); err != nil { + var roErr *config.ReadOnlyEnvError + if errors.As(err, &roErr) { + fmt.Fprintf(opts.IO.ErrOut, "The value of the %s environment variable is being used for authentication.\n", roErr.Variable) + fmt.Fprint(opts.IO.ErrOut, "To erase credentials stored in GitHub CLI, first clear the value from the environment.\n") + return cmdutil.SilentError + } return err } diff --git a/pkg/cmd/auth/logout/logout_test.go b/pkg/cmd/auth/logout/logout_test.go index a9d14cc91c6..a5d46ef094d 100644 --- a/pkg/cmd/auth/logout/logout_test.go +++ b/pkg/cmd/auth/logout/logout_test.go @@ -6,11 +6,11 @@ import ( "regexp" "testing" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/httpmock" - "github.com/cli/cli/pkg/iostreams" - "github.com/cli/cli/pkg/prompt" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/prompt" "github.com/google/shlex" "github.com/stretchr/testify/assert" ) @@ -98,7 +98,7 @@ func Test_logoutRun_tty(t *testing.T) { cfgHosts []string wantHosts string wantErrOut *regexp.Regexp - wantErr *regexp.Regexp + wantErr string }{ { name: "no arguments, multiple hosts", @@ -123,7 +123,7 @@ func Test_logoutRun_tty(t *testing.T) { { name: "no arguments, no hosts", opts: &LogoutOptions{}, - wantErr: regexp.MustCompile(`not logged in to any hosts`), + wantErr: `not logged in to any hosts`, }, { name: "hostname", @@ -176,14 +176,11 @@ func Test_logoutRun_tty(t *testing.T) { } err := logoutRun(tt.opts) - assert.Equal(t, tt.wantErr == nil, err == nil) - if err != nil { - if tt.wantErr != nil { - assert.True(t, tt.wantErr.MatchString(err.Error())) - return - } else { - t.Fatalf("unexpected error: %s", err) - } + if tt.wantErr != "" { + assert.EqualError(t, err, tt.wantErr) + return + } else { + assert.NoError(t, err) } if tt.wantErrOut == nil { @@ -204,7 +201,7 @@ func Test_logoutRun_nontty(t *testing.T) { opts *LogoutOptions cfgHosts []string wantHosts string - wantErr *regexp.Regexp + wantErr string ghtoken string }{ { @@ -227,7 +224,7 @@ func Test_logoutRun_nontty(t *testing.T) { opts: &LogoutOptions{ Hostname: "harry.mason", }, - wantErr: regexp.MustCompile(`not logged in to any hosts`), + wantErr: `not logged in to any hosts`, }, } @@ -258,16 +255,10 @@ func Test_logoutRun_nontty(t *testing.T) { defer config.StubWriteConfig(&mainBuf, &hostsBuf)() err := logoutRun(tt.opts) - assert.Equal(t, tt.wantErr == nil, err == nil) - if err != nil { - if tt.wantErr != nil { - if !tt.wantErr.MatchString(err.Error()) { - t.Errorf("got error: %v", err) - } - return - } else { - t.Fatalf("unexpected error: %s", err) - } + if tt.wantErr != "" { + assert.EqualError(t, err, tt.wantErr) + } else { + assert.NoError(t, err) } assert.Equal(t, "", stderr.String()) diff --git a/pkg/cmd/auth/refresh/refresh.go b/pkg/cmd/auth/refresh/refresh.go index dd7862f3f64..4137341a952 100644 --- a/pkg/cmd/auth/refresh/refresh.go +++ b/pkg/cmd/auth/refresh/refresh.go @@ -3,24 +3,32 @@ package refresh import ( "errors" "fmt" + "net/http" + "strings" "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/internal/authflow" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" - "github.com/cli/cli/pkg/prompt" + "github.com/cli/cli/v2/internal/authflow" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmd/auth/shared" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/prompt" "github.com/spf13/cobra" ) type RefreshOptions struct { - IO *iostreams.IOStreams - Config func() (config.Config, error) + IO *iostreams.IOStreams + Config func() (config.Config, error) + httpClient *http.Client + + MainExecutable string Hostname string Scopes []string AuthFlow func(config.Config, *iostreams.IOStreams, string, []string) error + + Interactive bool } func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.Command { @@ -31,6 +39,7 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra. _, err := authflow.AuthFlowWithConfig(cfg, io, hostname, "", scopes) return err }, + httpClient: http.DefaultClient, } cmd := &cobra.Command{ @@ -50,21 +59,16 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra. # => open a browser to ensure your authentication credentials have the correct minimum scopes `), RunE: func(cmd *cobra.Command, args []string) error { - isTTY := opts.IO.IsStdinTTY() && opts.IO.IsStdoutTTY() - - if !isTTY { - return fmt.Errorf("not attached to a terminal; in headless environments, GITHUB_TOKEN is recommended") - } + opts.Interactive = opts.IO.CanPrompt() - if opts.Hostname == "" && !opts.IO.CanPrompt() { - // here, we know we are attached to a TTY but prompts are disabled + if !opts.Interactive && opts.Hostname == "" { return &cmdutil.FlagError{Err: errors.New("--hostname required when not running interactively")} } + opts.MainExecutable = f.Executable() if runF != nil { return runF(opts) } - return refreshRun(opts) }, } @@ -83,6 +87,9 @@ func refreshRun(opts *RefreshOptions) error { candidates, err := cfg.Hosts() if err != nil { + return err + } + if len(candidates) == 0 { return fmt.Errorf("not logged in to any hosts. Use 'gh auth login' to authenticate with a host") } @@ -115,8 +122,49 @@ func refreshRun(opts *RefreshOptions) error { } if err := cfg.CheckWriteable(hostname, "oauth_token"); err != nil { + var roErr *config.ReadOnlyEnvError + if errors.As(err, &roErr) { + fmt.Fprintf(opts.IO.ErrOut, "The value of the %s environment variable is being used for authentication.\n", roErr.Variable) + fmt.Fprint(opts.IO.ErrOut, "To refresh credentials stored in GitHub CLI, first clear the value from the environment.\n") + return cmdutil.SilentError + } + return err + } + + var additionalScopes []string + if oldToken, _ := cfg.Get(hostname, "oauth_token"); oldToken != "" { + if oldScopes, err := shared.GetScopes(opts.httpClient, hostname, oldToken); err == nil { + for _, s := range strings.Split(oldScopes, ",") { + s = strings.TrimSpace(s) + if s != "" { + additionalScopes = append(additionalScopes, s) + } + } + } + } + + credentialFlow := &shared.GitCredentialFlow{ + Executable: opts.MainExecutable, + } + gitProtocol, _ := cfg.Get(hostname, "git_protocol") + if opts.Interactive && gitProtocol == "https" { + if err := credentialFlow.Prompt(hostname); err != nil { + return err + } + additionalScopes = append(additionalScopes, credentialFlow.Scopes()...) + } + + if err := opts.AuthFlow(cfg, opts.IO, hostname, append(opts.Scopes, additionalScopes...)); err != nil { return err } - return opts.AuthFlow(cfg, opts.IO, hostname, opts.Scopes) + if credentialFlow.ShouldSetup() { + username, _ := cfg.Get(hostname, "user") + password, _ := cfg.Get(hostname, "oauth_token") + if err := credentialFlow.Setup(hostname, username, password); err != nil { + return err + } + } + + return nil } diff --git a/pkg/cmd/auth/refresh/refresh_test.go b/pkg/cmd/auth/refresh/refresh_test.go index 065dd3fa2c2..dbdae26afb0 100644 --- a/pkg/cmd/auth/refresh/refresh_test.go +++ b/pkg/cmd/auth/refresh/refresh_test.go @@ -2,14 +2,16 @@ package refresh import ( "bytes" - "regexp" + "io/ioutil" + "net/http" + "strings" "testing" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/httpmock" - "github.com/cli/cli/pkg/iostreams" - "github.com/cli/cli/pkg/prompt" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/prompt" "github.com/google/shlex" "github.com/stretchr/testify/assert" ) @@ -37,9 +39,11 @@ func Test_NewCmdRefresh(t *testing.T) { wantsErr: true, }, { - name: "nontty hostname", - cli: "-h aline.cedrac", - wantsErr: true, + name: "nontty hostname", + cli: "-h aline.cedrac", + wants: RefreshOptions{ + Hostname: "aline.cedrac", + }, }, { name: "tty hostname", @@ -87,7 +91,8 @@ func Test_NewCmdRefresh(t *testing.T) { t.Run(tt.name, func(t *testing.T) { io, _, _, _ := iostreams.Test() f := &cmdutil.Factory{ - IOStreams: io, + IOStreams: io, + Executable: func() string { return "/path/to/gh" }, } io.SetStdinTTY(tt.tty) io.SetStdoutTTY(tt.tty) @@ -132,14 +137,15 @@ func Test_refreshRun(t *testing.T) { opts *RefreshOptions askStubs func(*prompt.AskStubber) cfgHosts []string - wantErr *regexp.Regexp + oldScopes string + wantErr string nontty bool wantAuthArgs authArgs }{ { name: "no hosts configured", opts: &RefreshOptions{}, - wantErr: regexp.MustCompile(`not logged in to any hosts`), + wantErr: `not logged in to any hosts`, }, { name: "hostname given but dne", @@ -150,7 +156,7 @@ func Test_refreshRun(t *testing.T) { opts: &RefreshOptions{ Hostname: "obed.morton", }, - wantErr: regexp.MustCompile(`not logged in to obed.morton`), + wantErr: `not logged in to obed.morton`, }, { name: "hostname provided and is configured", @@ -209,6 +215,20 @@ func Test_refreshRun(t *testing.T) { scopes: []string{"repo:invite", "public_key:read"}, }, }, + { + name: "scopes provided", + cfgHosts: []string{ + "github.com", + }, + oldScopes: "delete_repo, codespace", + opts: &RefreshOptions{ + Scopes: []string{"repo:invite", "public_key:read"}, + }, + wantAuthArgs: authArgs{ + hostname: "github.com", + scopes: []string{"repo:invite", "public_key:read", "delete_repo", "codespace"}, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -232,10 +252,26 @@ func Test_refreshRun(t *testing.T) { for _, hostname := range tt.cfgHosts { _ = cfg.Set(hostname, "oauth_token", "abc123") } - reg := &httpmock.Registry{} - reg.Register( - httpmock.GraphQL(`query UserCurrent\b`), - httpmock.StringResponse(`{"data":{"viewer":{"login":"cybilb"}}}`)) + + httpReg := &httpmock.Registry{} + httpReg.Register( + httpmock.REST("GET", ""), + func(req *http.Request) (*http.Response, error) { + statusCode := 200 + if req.Header.Get("Authorization") != "token abc123" { + statusCode = 400 + } + return &http.Response{ + Request: req, + StatusCode: statusCode, + Body: ioutil.NopCloser(strings.NewReader(``)), + Header: http.Header{ + "X-Oauth-Scopes": {tt.oldScopes}, + }, + }, nil + }, + ) + tt.opts.httpClient = &http.Client{Transport: httpReg} mainBuf := bytes.Buffer{} hostsBuf := bytes.Buffer{} @@ -248,18 +284,16 @@ func Test_refreshRun(t *testing.T) { } err := refreshRun(tt.opts) - assert.Equal(t, tt.wantErr == nil, err == nil) - if err != nil { - if tt.wantErr != nil { - assert.True(t, tt.wantErr.MatchString(err.Error())) - return - } else { - t.Fatalf("unexpected error: %s", err) + if tt.wantErr != "" { + if assert.Error(t, err) { + assert.Contains(t, err.Error(), tt.wantErr) } + } else { + assert.NoError(t, err) } - assert.Equal(t, aa.hostname, tt.wantAuthArgs.hostname) - assert.Equal(t, aa.scopes, tt.wantAuthArgs.scopes) + assert.Equal(t, tt.wantAuthArgs.hostname, aa.hostname) + assert.Equal(t, tt.wantAuthArgs.scopes, aa.scopes) }) } } diff --git a/pkg/cmd/auth/shared/git_credential.go b/pkg/cmd/auth/shared/git_credential.go new file mode 100644 index 00000000000..57a9b842943 --- /dev/null +++ b/pkg/cmd/auth/shared/git_credential.go @@ -0,0 +1,161 @@ +package shared + +import ( + "bytes" + "errors" + "fmt" + "path/filepath" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/run" + "github.com/cli/cli/v2/pkg/prompt" + "github.com/google/shlex" +) + +type GitCredentialFlow struct { + Executable string + + shouldSetup bool + helper string + scopes []string +} + +func (flow *GitCredentialFlow) Prompt(hostname string) error { + var gitErr error + flow.helper, gitErr = gitCredentialHelper(hostname) + if isOurCredentialHelper(flow.helper) { + flow.scopes = append(flow.scopes, "workflow") + return nil + } + + err := prompt.SurveyAskOne(&survey.Confirm{ + Message: "Authenticate Git with your GitHub credentials?", + Default: true, + }, &flow.shouldSetup) + if err != nil { + return fmt.Errorf("could not prompt: %w", err) + } + if flow.shouldSetup { + if isGitMissing(gitErr) { + return gitErr + } + flow.scopes = append(flow.scopes, "workflow") + } + + return nil +} + +func (flow *GitCredentialFlow) Scopes() []string { + return flow.scopes +} + +func (flow *GitCredentialFlow) ShouldSetup() bool { + return flow.shouldSetup +} + +func (flow *GitCredentialFlow) Setup(hostname, username, authToken string) error { + return flow.gitCredentialSetup(hostname, username, authToken) +} + +func (flow *GitCredentialFlow) gitCredentialSetup(hostname, username, password string) error { + if flow.helper == "" { + // first use a blank value to indicate to git we want to sever the chain of credential helpers + preConfigureCmd, err := git.GitCommand("config", "--global", gitCredentialHelperKey(hostname), "") + if err != nil { + return err + } + if err = run.PrepareCmd(preConfigureCmd).Run(); err != nil { + return err + } + + // use GitHub CLI as a credential helper (for this host only) + configureCmd, err := git.GitCommand( + "config", "--global", "--add", + gitCredentialHelperKey(hostname), + fmt.Sprintf("!%s auth git-credential", shellQuote(flow.Executable)), + ) + if err != nil { + return err + } + return run.PrepareCmd(configureCmd).Run() + } + + // clear previous cached credentials + rejectCmd, err := git.GitCommand("credential", "reject") + if err != nil { + return err + } + + rejectCmd.Stdin = bytes.NewBufferString(heredoc.Docf(` + protocol=https + host=%s + `, hostname)) + + err = run.PrepareCmd(rejectCmd).Run() + if err != nil { + return err + } + + approveCmd, err := git.GitCommand("credential", "approve") + if err != nil { + return err + } + + approveCmd.Stdin = bytes.NewBufferString(heredoc.Docf(` + protocol=https + host=%s + username=%s + password=%s + `, hostname, username, password)) + + err = run.PrepareCmd(approveCmd).Run() + if err != nil { + return err + } + + return nil +} + +func gitCredentialHelperKey(hostname string) string { + return fmt.Sprintf("credential.https://%s.helper", hostname) +} + +func gitCredentialHelper(hostname string) (helper string, err error) { + helper, err = git.Config(gitCredentialHelperKey(hostname)) + if helper != "" { + return + } + helper, err = git.Config("credential.helper") + return +} + +func isOurCredentialHelper(cmd string) bool { + if !strings.HasPrefix(cmd, "!") { + return false + } + + args, err := shlex.Split(cmd[1:]) + if err != nil || len(args) == 0 { + return false + } + + return strings.TrimSuffix(filepath.Base(args[0]), ".exe") == "gh" +} + +func isGitMissing(err error) bool { + if err == nil { + return false + } + var errNotInstalled *git.NotInstalled + return errors.As(err, &errNotInstalled) +} + +func shellQuote(s string) string { + if strings.ContainsAny(s, " $") { + return "'" + s + "'" + } + return s +} diff --git a/pkg/cmd/auth/shared/git_credential_test.go b/pkg/cmd/auth/shared/git_credential_test.go new file mode 100644 index 00000000000..58fe3988a5f --- /dev/null +++ b/pkg/cmd/auth/shared/git_credential_test.go @@ -0,0 +1,94 @@ +package shared + +import ( + "testing" + + "github.com/cli/cli/v2/internal/run" +) + +func TestGitCredentialSetup_configureExisting(t *testing.T) { + cs, restoreRun := run.Stub() + defer restoreRun(t) + cs.Register(`git credential reject`, 0, "") + cs.Register(`git credential approve`, 0, "") + + f := GitCredentialFlow{ + Executable: "gh", + helper: "osxkeychain", + } + + if err := f.gitCredentialSetup("example.com", "monalisa", "PASSWD"); err != nil { + t.Errorf("GitCredentialSetup() error = %v", err) + } +} + +func TestGitCredentialSetup_setOurs(t *testing.T) { + cs, restoreRun := run.Stub() + defer restoreRun(t) + cs.Register(`git config --global credential\.`, 0, "", func(args []string) { + if key := args[len(args)-2]; key != "credential.https://example.com.helper" { + t.Errorf("git config key was %q", key) + } + if val := args[len(args)-1]; val != "" { + t.Errorf("global credential helper configured to %q", val) + } + }) + cs.Register(`git config --global --add credential\.`, 0, "", func(args []string) { + if key := args[len(args)-2]; key != "credential.https://example.com.helper" { + t.Errorf("git config key was %q", key) + } + if val := args[len(args)-1]; val != "!/path/to/gh auth git-credential" { + t.Errorf("global credential helper configured to %q", val) + } + }) + + f := GitCredentialFlow{ + Executable: "/path/to/gh", + helper: "", + } + + if err := f.gitCredentialSetup("example.com", "monalisa", "PASSWD"); err != nil { + t.Errorf("GitCredentialSetup() error = %v", err) + } +} + +func Test_isOurCredentialHelper(t *testing.T) { + tests := []struct { + name string + arg string + want bool + }{ + { + name: "blank", + arg: "", + want: false, + }, + { + name: "invalid", + arg: "!", + want: false, + }, + { + name: "osxkeychain", + arg: "osxkeychain", + want: false, + }, + { + name: "looks like gh but isn't", + arg: "gh auth", + want: false, + }, + { + name: "ours", + arg: "!/path/to/gh auth", + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isOurCredentialHelper(tt.arg); got != tt.want { + t.Errorf("isOurCredentialHelper() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/cmd/auth/shared/login_flow.go b/pkg/cmd/auth/shared/login_flow.go new file mode 100644 index 00000000000..0bac49b35cf --- /dev/null +++ b/pkg/cmd/auth/shared/login_flow.go @@ -0,0 +1,208 @@ +package shared + +import ( + "fmt" + "net/http" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/authflow" + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/prompt" +) + +type iconfig interface { + Get(string, string) (string, error) + Set(string, string, string) error + Write() error +} + +type LoginOptions struct { + IO *iostreams.IOStreams + Config iconfig + HTTPClient *http.Client + Hostname string + Interactive bool + Web bool + Scopes []string + Executable string + + sshContext sshContext +} + +func Login(opts *LoginOptions) error { + cfg := opts.Config + hostname := opts.Hostname + httpClient := opts.HTTPClient + cs := opts.IO.ColorScheme() + + var gitProtocol string + if opts.Interactive { + var proto string + err := prompt.SurveyAskOne(&survey.Select{ + Message: "What is your preferred protocol for Git operations?", + Options: []string{ + "HTTPS", + "SSH", + }, + }, &proto) + if err != nil { + return fmt.Errorf("could not prompt: %w", err) + } + gitProtocol = strings.ToLower(proto) + } + + var additionalScopes []string + + credentialFlow := &GitCredentialFlow{Executable: opts.Executable} + if opts.Interactive && gitProtocol == "https" { + if err := credentialFlow.Prompt(hostname); err != nil { + return err + } + additionalScopes = append(additionalScopes, credentialFlow.Scopes()...) + } + + var keyToUpload string + if opts.Interactive && gitProtocol == "ssh" { + pubKeys, err := opts.sshContext.localPublicKeys() + if err != nil { + return err + } + + if len(pubKeys) > 0 { + var keyChoice int + err := prompt.SurveyAskOne(&survey.Select{ + Message: "Upload your SSH public key to your GitHub account?", + Options: append(pubKeys, "Skip"), + }, &keyChoice) + if err != nil { + return fmt.Errorf("could not prompt: %w", err) + } + if keyChoice < len(pubKeys) { + keyToUpload = pubKeys[keyChoice] + } + } else { + var err error + keyToUpload, err = opts.sshContext.generateSSHKey() + if err != nil { + return err + } + } + } + if keyToUpload != "" { + additionalScopes = append(additionalScopes, "admin:public_key") + } + + var authMode int + if opts.Web { + authMode = 0 + } else { + err := prompt.SurveyAskOne(&survey.Select{ + Message: "How would you like to authenticate GitHub CLI?", + Options: []string{ + "Login with a web browser", + "Paste an authentication token", + }, + }, &authMode) + if err != nil { + return fmt.Errorf("could not prompt: %w", err) + } + } + + var authToken string + userValidated := false + + if authMode == 0 { + var err error + authToken, err = authflow.AuthFlowWithConfig(cfg, opts.IO, hostname, "", append(opts.Scopes, additionalScopes...)) + if err != nil { + return fmt.Errorf("failed to authenticate via web browser: %w", err) + } + userValidated = true + } else { + minimumScopes := append([]string{"repo", "read:org"}, additionalScopes...) + fmt.Fprint(opts.IO.ErrOut, heredoc.Docf(` + Tip: you can generate a Personal Access Token here https://%s/settings/tokens + The minimum required scopes are %s. + `, hostname, scopesSentence(minimumScopes, ghinstance.IsEnterprise(hostname)))) + + err := prompt.SurveyAskOne(&survey.Password{ + Message: "Paste your authentication token:", + }, &authToken, survey.WithValidator(survey.Required)) + if err != nil { + return fmt.Errorf("could not prompt: %w", err) + } + + if err := HasMinimumScopes(httpClient, hostname, authToken); err != nil { + return fmt.Errorf("error validating token: %w", err) + } + + if err := cfg.Set(hostname, "oauth_token", authToken); err != nil { + return err + } + } + + var username string + if userValidated { + username, _ = cfg.Get(hostname, "user") + } else { + apiClient := api.NewClientFromHTTP(httpClient) + var err error + username, err = api.CurrentLoginName(apiClient, hostname) + if err != nil { + return fmt.Errorf("error using api: %w", err) + } + + err = cfg.Set(hostname, "user", username) + if err != nil { + return err + } + } + + if gitProtocol != "" { + fmt.Fprintf(opts.IO.ErrOut, "- gh config set -h %s git_protocol %s\n", hostname, gitProtocol) + err := cfg.Set(hostname, "git_protocol", gitProtocol) + if err != nil { + return err + } + fmt.Fprintf(opts.IO.ErrOut, "%s Configured git protocol\n", cs.SuccessIcon()) + } + + err := cfg.Write() + if err != nil { + return err + } + + if credentialFlow.ShouldSetup() { + err := credentialFlow.Setup(hostname, username, authToken) + if err != nil { + return err + } + } + + if keyToUpload != "" { + err := sshKeyUpload(httpClient, hostname, keyToUpload) + if err != nil { + return err + } + fmt.Fprintf(opts.IO.ErrOut, "%s Uploaded the SSH key to your GitHub account: %s\n", cs.SuccessIcon(), cs.Bold(keyToUpload)) + } + + fmt.Fprintf(opts.IO.ErrOut, "%s Logged in as %s\n", cs.SuccessIcon(), cs.Bold(username)) + return nil +} + +func scopesSentence(scopes []string, isEnterprise bool) string { + quoted := make([]string, len(scopes)) + for i, s := range scopes { + quoted[i] = fmt.Sprintf("'%s'", s) + if s == "workflow" && isEnterprise { + // remove when GHE 2.x reaches EOL + quoted[i] += " (GHE 3.0+)" + } + } + return strings.Join(quoted, ", ") +} diff --git a/pkg/cmd/auth/shared/login_flow_test.go b/pkg/cmd/auth/shared/login_flow_test.go new file mode 100644 index 00000000000..530e34045c2 --- /dev/null +++ b/pkg/cmd/auth/shared/login_flow_test.go @@ -0,0 +1,155 @@ +package shared + +import ( + "fmt" + "io/ioutil" + "net/http" + "path/filepath" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/run" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/prompt" + "github.com/stretchr/testify/assert" +) + +type tinyConfig map[string]string + +func (c tinyConfig) Get(host, key string) (string, error) { + return c[fmt.Sprintf("%s:%s", host, key)], nil +} + +func (c tinyConfig) Set(host string, key string, value string) error { + c[fmt.Sprintf("%s:%s", host, key)] = value + return nil +} + +func (c tinyConfig) Write() error { + return nil +} + +func TestLogin_ssh(t *testing.T) { + dir := t.TempDir() + io, _, stdout, stderr := iostreams.Test() + + tr := httpmock.Registry{} + defer tr.Verify(t) + + tr.Register( + httpmock.REST("GET", "api/v3/"), + httpmock.ScopesResponder("repo,read:org")) + tr.Register( + httpmock.GraphQL(`query UserCurrent\b`), + httpmock.StringResponse(`{"data":{"viewer":{ "login": "monalisa" }}}`)) + tr.Register( + httpmock.REST("POST", "api/v3/user/keys"), + httpmock.StringResponse(`{}`)) + + ask, askRestore := prompt.InitAskStubber() + defer askRestore() + + ask.StubOne("SSH") // preferred protocol + ask.StubOne(true) // generate a new key + ask.StubOne("monkey") // enter a passphrase + ask.StubOne(1) // paste a token + ask.StubOne("ATOKEN") // token + + rs, runRestore := run.Stub() + defer runRestore(t) + + keyFile := filepath.Join(dir, "id_ed25519") + rs.Register(`ssh-keygen`, 0, "", func(args []string) { + expected := []string{ + "ssh-keygen", "-t", "ed25519", + "-C", "", + "-N", "monkey", + "-f", keyFile, + } + assert.Equal(t, expected, args) + // simulate that the public key file has been generated + _ = ioutil.WriteFile(keyFile+".pub", []byte("PUBKEY"), 0600) + }) + + cfg := tinyConfig{} + + err := Login(&LoginOptions{ + IO: io, + Config: &cfg, + HTTPClient: &http.Client{Transport: &tr}, + Hostname: "example.com", + Interactive: true, + sshContext: sshContext{ + configDir: dir, + keygenExe: "ssh-keygen", + }, + }) + assert.NoError(t, err) + + assert.Equal(t, "", stdout.String()) + assert.Equal(t, heredoc.Docf(` + Tip: you can generate a Personal Access Token here https://example.com/settings/tokens + The minimum required scopes are 'repo', 'read:org', 'admin:public_key'. + - gh config set -h example.com git_protocol ssh + ✓ Configured git protocol + ✓ Uploaded the SSH key to your GitHub account: %s.pub + ✓ Logged in as monalisa + `, keyFile), stderr.String()) + + assert.Equal(t, "monalisa", cfg["example.com:user"]) + assert.Equal(t, "ATOKEN", cfg["example.com:oauth_token"]) + assert.Equal(t, "ssh", cfg["example.com:git_protocol"]) +} + +func Test_scopesSentence(t *testing.T) { + type args struct { + scopes []string + isEnterprise bool + } + tests := []struct { + name string + args args + want string + }{ + { + name: "basic scopes", + args: args{ + scopes: []string{"repo", "read:org"}, + isEnterprise: false, + }, + want: "'repo', 'read:org'", + }, + { + name: "empty", + args: args{ + scopes: []string(nil), + isEnterprise: false, + }, + want: "", + }, + { + name: "workflow scope for dotcom", + args: args{ + scopes: []string{"repo", "workflow"}, + isEnterprise: false, + }, + want: "'repo', 'workflow'", + }, + { + name: "workflow scope for GHE", + args: args{ + scopes: []string{"repo", "workflow"}, + isEnterprise: true, + }, + want: "'repo', 'workflow' (GHE 3.0+)", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := scopesSentence(tt.args.scopes, tt.args.isEnterprise); got != tt.want { + t.Errorf("scopesSentence() = %q, want %q", got, tt.want) + } + }) + } +} diff --git a/pkg/cmd/auth/shared/oauth_scopes.go b/pkg/cmd/auth/shared/oauth_scopes.go new file mode 100644 index 00000000000..c076722b28f --- /dev/null +++ b/pkg/cmd/auth/shared/oauth_scopes.go @@ -0,0 +1,98 @@ +package shared + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "strings" + + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/ghinstance" +) + +type MissingScopesError struct { + MissingScopes []string +} + +func (e MissingScopesError) Error() string { + var missing []string + for _, s := range e.MissingScopes { + missing = append(missing, fmt.Sprintf("'%s'", s)) + } + scopes := strings.Join(missing, ", ") + + if len(e.MissingScopes) == 1 { + return "missing required scope " + scopes + } + return "missing required scopes " + scopes +} + +type httpClient interface { + Do(*http.Request) (*http.Response, error) +} + +func GetScopes(httpClient httpClient, hostname, authToken string) (string, error) { + apiEndpoint := ghinstance.RESTPrefix(hostname) + + req, err := http.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return "", err + } + + req.Header.Set("Authorization", "token "+authToken) + + res, err := httpClient.Do(req) + if err != nil { + return "", err + } + + defer func() { + // Ensure the response body is fully read and closed + // before we reconnect, so that we reuse the same TCPconnection. + _, _ = io.Copy(ioutil.Discard, res.Body) + res.Body.Close() + }() + + if res.StatusCode != 200 { + return "", api.HandleHTTPError(res) + } + + return res.Header.Get("X-Oauth-Scopes"), nil +} + +func HasMinimumScopes(httpClient httpClient, hostname, authToken string) error { + scopesHeader, err := GetScopes(httpClient, hostname, authToken) + if err != nil { + return err + } + + if scopesHeader == "" { + // if the token reports no scopes, assume that it's an integration token and give up on + // detecting its capabilities + return nil + } + + search := map[string]bool{ + "repo": false, + "read:org": false, + "admin:org": false, + } + for _, s := range strings.Split(scopesHeader, ",") { + search[strings.TrimSpace(s)] = true + } + + var missingScopes []string + if !search["repo"] { + missingScopes = append(missingScopes, "repo") + } + + if !search["read:org"] && !search["write:org"] && !search["admin:org"] { + missingScopes = append(missingScopes, "read:org") + } + + if len(missingScopes) > 0 { + return &MissingScopesError{MissingScopes: missingScopes} + } + return nil +} diff --git a/pkg/cmd/auth/shared/oauth_scopes_test.go b/pkg/cmd/auth/shared/oauth_scopes_test.go new file mode 100644 index 00000000000..450416c1817 --- /dev/null +++ b/pkg/cmd/auth/shared/oauth_scopes_test.go @@ -0,0 +1,79 @@ +package shared + +import ( + "bytes" + "io/ioutil" + "net/http" + "testing" + + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/stretchr/testify/assert" +) + +func Test_HasMinimumScopes(t *testing.T) { + tests := []struct { + name string + header string + wantErr string + }{ + { + name: "no scopes", + header: "", + wantErr: "", + }, + { + name: "default scopes", + header: "repo, read:org", + wantErr: "", + }, + { + name: "admin:org satisfies read:org", + header: "repo, admin:org", + wantErr: "", + }, + { + name: "write:org satisfies read:org", + header: "repo, write:org", + wantErr: "", + }, + { + name: "insufficient scope", + header: "repo", + wantErr: "missing required scope 'read:org'", + }, + { + name: "insufficient scopes", + header: "gist", + wantErr: "missing required scopes 'repo', 'read:org'", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakehttp := &httpmock.Registry{} + defer fakehttp.Verify(t) + + var gotAuthorization string + fakehttp.Register(httpmock.REST("GET", ""), func(req *http.Request) (*http.Response, error) { + gotAuthorization = req.Header.Get("authorization") + return &http.Response{ + Request: req, + StatusCode: 200, + Body: ioutil.NopCloser(&bytes.Buffer{}), + Header: map[string][]string{ + "X-Oauth-Scopes": {tt.header}, + }, + }, nil + }) + + client := http.Client{Transport: fakehttp} + err := HasMinimumScopes(&client, "github.com", "ATOKEN") + if tt.wantErr != "" { + assert.EqualError(t, err, tt.wantErr) + } else { + assert.NoError(t, err) + } + assert.Equal(t, gotAuthorization, "token ATOKEN") + }) + } + +} diff --git a/pkg/cmd/auth/shared/ssh_keys.go b/pkg/cmd/auth/shared/ssh_keys.go new file mode 100644 index 00000000000..97f5174e441 --- /dev/null +++ b/pkg/cmd/auth/shared/ssh_keys.go @@ -0,0 +1,119 @@ +package shared + +import ( + "fmt" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + + "github.com/AlecAivazis/survey/v2" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/run" + "github.com/cli/cli/v2/pkg/cmd/ssh-key/add" + "github.com/cli/cli/v2/pkg/prompt" + "github.com/cli/safeexec" +) + +type sshContext struct { + configDir string + keygenExe string +} + +func (c *sshContext) sshDir() (string, error) { + if c.configDir != "" { + return c.configDir, nil + } + dir, err := config.HomeDirPath(".ssh") + if err == nil { + c.configDir = dir + } + return dir, err +} + +func (c *sshContext) localPublicKeys() ([]string, error) { + sshDir, err := c.sshDir() + if err != nil { + return nil, err + } + + return filepath.Glob(filepath.Join(sshDir, "*.pub")) +} + +func (c *sshContext) findKeygen() (string, error) { + if c.keygenExe != "" { + return c.keygenExe, nil + } + + keygenExe, err := safeexec.LookPath("ssh-keygen") + if err != nil && runtime.GOOS == "windows" { + // We can try and find ssh-keygen in a Git for Windows install + if gitPath, err := safeexec.LookPath("git"); err == nil { + gitKeygen := filepath.Join(filepath.Dir(gitPath), "..", "usr", "bin", "ssh-keygen.exe") + if _, err = os.Stat(gitKeygen); err == nil { + return gitKeygen, nil + } + } + } + + if err == nil { + c.keygenExe = keygenExe + } + return keygenExe, err +} + +func (c *sshContext) generateSSHKey() (string, error) { + keygenExe, err := c.findKeygen() + if err != nil { + // give up silently if `ssh-keygen` is not available + return "", nil + } + + var sshChoice bool + err = prompt.SurveyAskOne(&survey.Confirm{ + Message: "Generate a new SSH key to add to your GitHub account?", + Default: true, + }, &sshChoice) + if err != nil { + return "", fmt.Errorf("could not prompt: %w", err) + } + if !sshChoice { + return "", nil + } + + sshDir, err := c.sshDir() + if err != nil { + return "", err + } + keyFile := filepath.Join(sshDir, "id_ed25519") + if _, err := os.Stat(keyFile); err == nil { + return "", fmt.Errorf("refusing to overwrite file %s", keyFile) + } + + if err := os.MkdirAll(filepath.Dir(keyFile), 0711); err != nil { + return "", err + } + + var sshLabel string + var sshPassphrase string + err = prompt.SurveyAskOne(&survey.Password{ + Message: "Enter a passphrase for your new SSH key (Optional)", + }, &sshPassphrase) + if err != nil { + return "", fmt.Errorf("could not prompt: %w", err) + } + + keygenCmd := exec.Command(keygenExe, "-t", "ed25519", "-C", sshLabel, "-N", sshPassphrase, "-f", keyFile) + return keyFile + ".pub", run.PrepareCmd(keygenCmd).Run() +} + +func sshKeyUpload(httpClient *http.Client, hostname, keyFile string) error { + f, err := os.Open(keyFile) + if err != nil { + return err + } + defer f.Close() + + return add.SSHKeyUpload(httpClient, hostname, f, "GitHub CLI") +} diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 59603981d16..f84004c87ac 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -6,10 +6,11 @@ import ( "net/http" "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/api" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmd/auth/shared" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" ) @@ -68,7 +69,10 @@ func statusRun(opts *StatusOptions) error { statusInfo := map[string][]string{} hostnames, err := cfg.Hosts() - if len(hostnames) == 0 || err != nil { + if err != nil { + return err + } + if len(hostnames) == 0 { fmt.Fprintf(stderr, "You are not logged into any GitHub hosts. Run %s to authenticate.\n", cs.Bold("gh auth login")) return cmdutil.SilentError @@ -78,14 +82,15 @@ func statusRun(opts *StatusOptions) error { if err != nil { return err } - apiClient := api.NewClientFromHTTP(httpClient) var failed bool + var isHostnameFound bool for _, hostname := range hostnames { if opts.Hostname != "" && opts.Hostname != hostname { continue } + isHostnameFound = true token, tokenSource, _ := cfg.GetWithSource(hostname, "oauth_token") tokenIsWriteable := cfg.CheckWriteable(hostname, "oauth_token") == nil @@ -95,9 +100,8 @@ func statusRun(opts *StatusOptions) error { statusInfo[hostname] = append(statusInfo[hostname], fmt.Sprintf(x, ys...)) } - err = apiClient.HasMinimumScopes(hostname) - if err != nil { - var missingScopes *api.MissingScopesError + if err := shared.HasMinimumScopes(httpClient, hostname, token); err != nil { + var missingScopes *shared.MissingScopesError if errors.As(err, &missingScopes) { addMsg("%s %s: the token in %s is %s", cs.Red("X"), hostname, tokenSource, err) if tokenIsWriteable { @@ -117,6 +121,7 @@ func statusRun(opts *StatusOptions) error { } failed = true } else { + apiClient := api.NewClientFromHTTP(httpClient) username, err := api.CurrentLoginName(apiClient, hostname) if err != nil { addMsg("%s %s: api call failed: %s", cs.Red("X"), hostname, err) @@ -139,6 +144,12 @@ func statusRun(opts *StatusOptions) error { // not to since I wanted this command to be read-only. } + if !isHostnameFound { + fmt.Fprintf(stderr, + "Hostname %q not found among authenticated GitHub hosts\n", opts.Hostname) + return cmdutil.SilentError + } + for _, hostname := range hostnames { lines, ok := statusInfo[hostname] if !ok { diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index 0de14d388cc..07d32c422d3 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -6,12 +6,10 @@ import ( "regexp" "testing" - "github.com/cli/cli/api" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/pkg/cmd/auth/client" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/httpmock" - "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" "github.com/google/shlex" "github.com/stretchr/testify/assert" ) @@ -78,7 +76,7 @@ func Test_statusRun(t *testing.T) { opts *StatusOptions httpStubs func(*httpmock.Registry) cfg func(config.Config) - wantErr *regexp.Regexp + wantErr string wantErrOut *regexp.Regexp }{ { @@ -91,7 +89,7 @@ func Test_statusRun(t *testing.T) { _ = c.Set("github.com", "oauth_token", "abc123") }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) @@ -106,14 +104,14 @@ func Test_statusRun(t *testing.T) { _ = c.Set("github.com", "oauth_token", "abc123") }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,")) - reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) }, wantErrOut: regexp.MustCompile(`joel.miller: missing required.*Logged in to github.com as.*tess`), - wantErr: regexp.MustCompile(``), + wantErr: "SilentError", }, { name: "bad token", @@ -124,13 +122,13 @@ func Test_statusRun(t *testing.T) { }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.StatusStringResponse(400, "no bueno")) - reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) }, wantErrOut: regexp.MustCompile(`joel.miller: authentication failed.*Logged in to github.com as.*tess`), - wantErr: regexp.MustCompile(``), + wantErr: "SilentError", }, { name: "all good", @@ -140,8 +138,8 @@ func Test_statusRun(t *testing.T) { _ = c.Set("github.com", "oauth_token", "abc123") }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,")) - reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) @@ -159,8 +157,8 @@ func Test_statusRun(t *testing.T) { _ = c.Set("github.com", "oauth_token", "xyz456") }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,")) - reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) @@ -180,8 +178,8 @@ func Test_statusRun(t *testing.T) { _ = c.Set("github.com", "oauth_token", "xyz456") }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,")) - reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) @@ -190,6 +188,17 @@ func Test_statusRun(t *testing.T) { httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) }, wantErrOut: regexp.MustCompile(`(?s)Token: xyz456.*Token: abc123`), + }, { + name: "missing hostname", + opts: &StatusOptions{ + Hostname: "github.example.com", + }, + cfg: func(c config.Config) { + _ = c.Set("github.com", "oauth_token", "abc123") + }, + httpStubs: func(reg *httpmock.Registry) {}, + wantErrOut: regexp.MustCompile(`(?s)Hostname "github.example.com" not found among authenticated GitHub hosts`), + wantErr: "SilentError", }, } @@ -217,14 +226,6 @@ func Test_statusRun(t *testing.T) { } reg := &httpmock.Registry{} - origClientFromCfg := client.ClientFromCfg - defer func() { - client.ClientFromCfg = origClientFromCfg - }() - client.ClientFromCfg = func(_ string, _ config.Config) (*api.Client, error) { - httpClient := &http.Client{Transport: reg} - return api.NewClientFromHTTP(httpClient), nil - } tt.opts.HttpClient = func() (*http.Client, error) { return &http.Client{Transport: reg}, nil } @@ -236,14 +237,11 @@ func Test_statusRun(t *testing.T) { defer config.StubWriteConfig(&mainBuf, &hostsBuf)() err := statusRun(tt.opts) - assert.Equal(t, tt.wantErr == nil, err == nil) - if err != nil { - if tt.wantErr != nil { - assert.True(t, tt.wantErr.MatchString(err.Error())) - return - } else { - t.Fatalf("unexpected error: %s", err) - } + if tt.wantErr != "" { + assert.EqualError(t, err, tt.wantErr) + return + } else { + assert.NoError(t, err) } if tt.wantErrOut == nil { diff --git a/pkg/cmd/browse/browse.go b/pkg/cmd/browse/browse.go new file mode 100644 index 00000000000..8bd64940d6d --- /dev/null +++ b/pkg/cmd/browse/browse.go @@ -0,0 +1,238 @@ +package browse + +import ( + "fmt" + "net/http" + "path/filepath" + "strconv" + "strings" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/utils" + "github.com/spf13/cobra" +) + +type browser interface { + Browse(string) error +} + +type BrowseOptions struct { + BaseRepo func() (ghrepo.Interface, error) + Browser browser + HttpClient func() (*http.Client, error) + IO *iostreams.IOStreams + PathFromRepoRoot func() string + + SelectorArg string + + Branch string + CommitFlag bool + ProjectsFlag bool + SettingsFlag bool + WikiFlag bool + NoBrowserFlag bool +} + +func NewCmdBrowse(f *cmdutil.Factory, runF func(*BrowseOptions) error) *cobra.Command { + opts := &BrowseOptions{ + Browser: f.Browser, + HttpClient: f.HttpClient, + IO: f.IOStreams, + PathFromRepoRoot: git.PathFromRepoRoot, + } + + cmd := &cobra.Command{ + Long: "Open the GitHub repository in the web browser.", + Short: "Open the repository in the browser", + Use: "browse [ | ]", + Args: cobra.MaximumNArgs(1), + Example: heredoc.Doc(` + $ gh browse + #=> Open the home page of the current repository + + $ gh browse 217 + #=> Open issue or pull request 217 + + $ gh browse --settings + #=> Open repository settings + + $ gh browse main.go:312 + #=> Open main.go at line 312 + + $ gh browse main.go --branch main + #=> Open main.go in the main branch + `), + Annotations: map[string]string{ + "IsCore": "true", + "help:arguments": heredoc.Doc(` + A browser location can be specified using arguments in the following format: + - by number for issue or pull request, e.g. "123"; or + - by path for opening folders and files, e.g. "cmd/gh/main.go" + `), + "help:environment": heredoc.Doc(` + To configure a web browser other than the default, use the BROWSER environment variable. + `), + }, + RunE: func(cmd *cobra.Command, args []string) error { + opts.BaseRepo = f.BaseRepo + + if len(args) > 0 { + opts.SelectorArg = args[0] + } + + if err := cmdutil.MutuallyExclusive( + "specify only one of `--branch`, `--commit`, `--projects`, `--wiki`, or `--settings`", + opts.Branch != "", + opts.CommitFlag, + opts.WikiFlag, + opts.SettingsFlag, + opts.ProjectsFlag, + ); err != nil { + return err + } + + if runF != nil { + return runF(opts) + } + return runBrowse(opts) + }, + } + + cmdutil.EnableRepoOverride(cmd, f) + cmd.Flags().BoolVarP(&opts.ProjectsFlag, "projects", "p", false, "Open repository projects") + cmd.Flags().BoolVarP(&opts.WikiFlag, "wiki", "w", false, "Open repository wiki") + cmd.Flags().BoolVarP(&opts.SettingsFlag, "settings", "s", false, "Open repository settings") + cmd.Flags().BoolVarP(&opts.NoBrowserFlag, "no-browser", "n", false, "Print destination URL instead of opening the browser") + cmd.Flags().BoolVarP(&opts.CommitFlag, "commit", "c", false, "Open the last commit") + cmd.Flags().StringVarP(&opts.Branch, "branch", "b", "", "Select another branch by passing in the branch name") + + return cmd +} + +func runBrowse(opts *BrowseOptions) error { + baseRepo, err := opts.BaseRepo() + if err != nil { + return fmt.Errorf("unable to determine base repository: %w", err) + } + + if opts.CommitFlag { + commit, err := git.LastCommit() + if err == nil { + opts.Branch = commit.Sha + } + } + + section, err := parseSection(baseRepo, opts) + if err != nil { + return err + } + url := ghrepo.GenerateRepoURL(baseRepo, section) + + if opts.NoBrowserFlag { + _, err := fmt.Fprintln(opts.IO.Out, url) + return err + } + + if opts.IO.IsStdoutTTY() { + fmt.Fprintf(opts.IO.Out, "Opening %s in your browser.\n", utils.DisplayURL(url)) + } + return opts.Browser.Browse(url) +} + +func parseSection(baseRepo ghrepo.Interface, opts *BrowseOptions) (string, error) { + if opts.SelectorArg == "" { + if opts.ProjectsFlag { + return "projects", nil + } else if opts.SettingsFlag { + return "settings", nil + } else if opts.WikiFlag { + return "wiki", nil + } else if opts.Branch == "" { + return "", nil + } + } + + if isNumber(opts.SelectorArg) { + return fmt.Sprintf("issues/%s", opts.SelectorArg), nil + } + + filePath, rangeStart, rangeEnd, err := parseFile(*opts, opts.SelectorArg) + if err != nil { + return "", err + } + + branchName := opts.Branch + if branchName == "" { + httpClient, err := opts.HttpClient() + if err != nil { + return "", err + } + apiClient := api.NewClientFromHTTP(httpClient) + branchName, err = api.RepoDefaultBranch(apiClient, baseRepo) + if err != nil { + return "", fmt.Errorf("error determining the default branch: %w", err) + } + } + + if rangeStart > 0 { + var rangeFragment string + if rangeEnd > 0 && rangeStart != rangeEnd { + rangeFragment = fmt.Sprintf("L%d-L%d", rangeStart, rangeEnd) + } else { + rangeFragment = fmt.Sprintf("L%d", rangeStart) + } + return fmt.Sprintf("blob/%s/%s?plain=1#%s", branchName, filePath, rangeFragment), nil + } + return fmt.Sprintf("tree/%s/%s", branchName, filePath), nil +} + +func parseFile(opts BrowseOptions, f string) (p string, start int, end int, err error) { + parts := strings.SplitN(f, ":", 3) + if len(parts) > 2 { + err = fmt.Errorf("invalid file argument: %q", f) + return + } + + p = parts[0] + if !filepath.IsAbs(p) { + p = filepath.Clean(filepath.Join(opts.PathFromRepoRoot(), p)) + // Ensure that a path using \ can be used in a URL + p = strings.ReplaceAll(p, "\\", "/") + if p == "." || strings.HasPrefix(p, "..") { + p = "" + } + } + if len(parts) < 2 { + return + } + + if idx := strings.IndexRune(parts[1], '-'); idx >= 0 { + start, err = strconv.Atoi(parts[1][:idx]) + if err != nil { + err = fmt.Errorf("invalid file argument: %q", f) + return + } + end, err = strconv.Atoi(parts[1][idx+1:]) + if err != nil { + err = fmt.Errorf("invalid file argument: %q", f) + } + return + } + + start, err = strconv.Atoi(parts[1]) + if err != nil { + err = fmt.Errorf("invalid file argument: %q", f) + } + end = start + return +} + +func isNumber(arg string) bool { + _, err := strconv.Atoi(arg) + return err == nil +} diff --git a/pkg/cmd/browse/browse_test.go b/pkg/cmd/browse/browse_test.go new file mode 100644 index 00000000000..489ad4e0991 --- /dev/null +++ b/pkg/cmd/browse/browse_test.go @@ -0,0 +1,477 @@ +package browse + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/google/shlex" + "github.com/stretchr/testify/assert" +) + +func TestNewCmdBrowse(t *testing.T) { + tests := []struct { + name string + cli string + factory func(*cmdutil.Factory) *cmdutil.Factory + wants BrowseOptions + wantsErr bool + }{ + { + name: "no arguments", + cli: "", + wantsErr: false, + }, + { + name: "settings flag", + cli: "--settings", + wants: BrowseOptions{ + SettingsFlag: true, + }, + wantsErr: false, + }, + { + name: "projects flag", + cli: "--projects", + wants: BrowseOptions{ + ProjectsFlag: true, + }, + wantsErr: false, + }, + { + name: "wiki flag", + cli: "--wiki", + wants: BrowseOptions{ + WikiFlag: true, + }, + wantsErr: false, + }, + { + name: "no browser flag", + cli: "--no-browser", + wants: BrowseOptions{ + NoBrowserFlag: true, + }, + wantsErr: false, + }, + { + name: "branch flag", + cli: "--branch main", + wants: BrowseOptions{ + Branch: "main", + }, + wantsErr: false, + }, + { + name: "branch flag without a branch name", + cli: "--branch", + wantsErr: true, + }, + { + name: "combination: settings projects", + cli: "--settings --projects", + wants: BrowseOptions{ + SettingsFlag: true, + ProjectsFlag: true, + }, + wantsErr: true, + }, + { + name: "combination: projects wiki", + cli: "--projects --wiki", + wants: BrowseOptions{ + ProjectsFlag: true, + WikiFlag: true, + }, + wantsErr: true, + }, + { + name: "passed argument", + cli: "main.go", + wants: BrowseOptions{ + SelectorArg: "main.go", + }, + wantsErr: false, + }, + { + name: "passed two arguments", + cli: "main.go main.go", + wantsErr: true, + }, + { + name: "last commit flag", + cli: "-c", + wants: BrowseOptions{ + CommitFlag: true, + }, + wantsErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := cmdutil.Factory{} + var opts *BrowseOptions + cmd := NewCmdBrowse(&f, func(o *BrowseOptions) error { + opts = o + return nil + }) + argv, err := shlex.Split(tt.cli) + assert.NoError(t, err) + cmd.SetArgs(argv) + _, err = cmd.ExecuteC() + + if tt.wantsErr { + assert.Error(t, err) + return + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.wants.Branch, opts.Branch) + assert.Equal(t, tt.wants.SelectorArg, opts.SelectorArg) + assert.Equal(t, tt.wants.ProjectsFlag, opts.ProjectsFlag) + assert.Equal(t, tt.wants.WikiFlag, opts.WikiFlag) + assert.Equal(t, tt.wants.NoBrowserFlag, opts.NoBrowserFlag) + assert.Equal(t, tt.wants.SettingsFlag, opts.SettingsFlag) + }) + } +} + +func setGitDir(t *testing.T, dir string) { + // taken from git_test.go + old_GIT_DIR := os.Getenv("GIT_DIR") + os.Setenv("GIT_DIR", dir) + t.Cleanup(func() { + os.Setenv("GIT_DIR", old_GIT_DIR) + }) +} + +func Test_runBrowse(t *testing.T) { + s := string(os.PathSeparator) + setGitDir(t, "../../../git/fixtures/simple.git") + tests := []struct { + name string + opts BrowseOptions + baseRepo ghrepo.Interface + defaultBranch string + expectedURL string + wantsErr bool + }{ + { + name: "no arguments", + opts: BrowseOptions{ + SelectorArg: "", + }, + baseRepo: ghrepo.New("jlsestak", "cli"), + expectedURL: "https://github.com/jlsestak/cli", + }, + { + name: "settings flag", + opts: BrowseOptions{ + SettingsFlag: true, + }, + baseRepo: ghrepo.New("bchadwic", "ObscuredByClouds"), + expectedURL: "https://github.com/bchadwic/ObscuredByClouds/settings", + }, + { + name: "projects flag", + opts: BrowseOptions{ + ProjectsFlag: true, + }, + baseRepo: ghrepo.New("ttran112", "7ate9"), + expectedURL: "https://github.com/ttran112/7ate9/projects", + }, + { + name: "wiki flag", + opts: BrowseOptions{ + WikiFlag: true, + }, + baseRepo: ghrepo.New("ravocean", "ThreatLevelMidnight"), + expectedURL: "https://github.com/ravocean/ThreatLevelMidnight/wiki", + }, + { + name: "file argument", + opts: BrowseOptions{SelectorArg: "path/to/file.txt"}, + baseRepo: ghrepo.New("ken", "mrprofessor"), + defaultBranch: "main", + expectedURL: "https://github.com/ken/mrprofessor/tree/main/path/to/file.txt", + }, + { + name: "issue argument", + opts: BrowseOptions{ + SelectorArg: "217", + }, + baseRepo: ghrepo.New("kevin", "MinTy"), + expectedURL: "https://github.com/kevin/MinTy/issues/217", + }, + { + name: "branch flag", + opts: BrowseOptions{ + Branch: "trunk", + }, + baseRepo: ghrepo.New("jlsestak", "CouldNotThinkOfARepoName"), + expectedURL: "https://github.com/jlsestak/CouldNotThinkOfARepoName/tree/trunk/", + }, + { + name: "branch flag with file", + opts: BrowseOptions{ + Branch: "trunk", + SelectorArg: "main.go", + }, + baseRepo: ghrepo.New("bchadwic", "LedZeppelinIV"), + expectedURL: "https://github.com/bchadwic/LedZeppelinIV/tree/trunk/main.go", + }, + { + name: "file with line number", + opts: BrowseOptions{ + SelectorArg: "path/to/file.txt:32", + }, + baseRepo: ghrepo.New("ravocean", "angur"), + defaultBranch: "trunk", + expectedURL: "https://github.com/ravocean/angur/blob/trunk/path/to/file.txt?plain=1#L32", + }, + { + name: "file with line range", + opts: BrowseOptions{ + SelectorArg: "path/to/file.txt:32-40", + }, + baseRepo: ghrepo.New("ravocean", "angur"), + defaultBranch: "trunk", + expectedURL: "https://github.com/ravocean/angur/blob/trunk/path/to/file.txt?plain=1#L32-L40", + }, + { + name: "invalid default branch", + opts: BrowseOptions{ + SelectorArg: "chocolate-pecan-pie.txt", + }, + baseRepo: ghrepo.New("andrewhsu", "recipies"), + defaultBranch: "", + wantsErr: true, + }, + { + name: "file with invalid line number after colon", + opts: BrowseOptions{ + SelectorArg: "laptime-notes.txt:w-9", + }, + baseRepo: ghrepo.New("andrewhsu", "sonoma-raceway"), + wantsErr: true, + }, + { + name: "file with invalid file format", + opts: BrowseOptions{ + SelectorArg: "path/to/file.txt:32:32", + }, + baseRepo: ghrepo.New("ttran112", "ttrain211"), + wantsErr: true, + }, + { + name: "file with invalid line number", + opts: BrowseOptions{ + SelectorArg: "path/to/file.txt:32a", + }, + baseRepo: ghrepo.New("ttran112", "ttrain211"), + wantsErr: true, + }, + { + name: "file with invalid line range", + opts: BrowseOptions{ + SelectorArg: "path/to/file.txt:32-abc", + }, + baseRepo: ghrepo.New("ttran112", "ttrain211"), + wantsErr: true, + }, + { + name: "branch with issue number", + opts: BrowseOptions{ + SelectorArg: "217", + Branch: "trunk", + }, + baseRepo: ghrepo.New("ken", "grc"), + wantsErr: false, + expectedURL: "https://github.com/ken/grc/issues/217", + }, + { + name: "opening branch file with line number", + opts: BrowseOptions{ + Branch: "first-browse-pull", + SelectorArg: "browse.go:32", + }, + baseRepo: ghrepo.New("github", "ThankYouGitHub"), + wantsErr: false, + expectedURL: "https://github.com/github/ThankYouGitHub/blob/first-browse-pull/browse.go?plain=1#L32", + }, + { + name: "no browser with branch file and line number", + opts: BrowseOptions{ + Branch: "3-0-stable", + SelectorArg: "init.rb:6", + NoBrowserFlag: true, + }, + baseRepo: ghrepo.New("mislav", "will_paginate"), + wantsErr: false, + expectedURL: "https://github.com/mislav/will_paginate/blob/3-0-stable/init.rb?plain=1#L6", + }, + { + name: "open last commit", + opts: BrowseOptions{ + CommitFlag: true, + }, + baseRepo: ghrepo.New("vilmibm", "gh-user-status"), + wantsErr: false, + expectedURL: "https://github.com/vilmibm/gh-user-status/tree/6f1a2405cace1633d89a79c74c65f22fe78f9659/", + }, + { + name: "open last commit with a file", + opts: BrowseOptions{ + CommitFlag: true, + SelectorArg: "main.go", + }, + baseRepo: ghrepo.New("vilmibm", "gh-user-status"), + wantsErr: false, + expectedURL: "https://github.com/vilmibm/gh-user-status/tree/6f1a2405cace1633d89a79c74c65f22fe78f9659/main.go", + }, + { + name: "relative path from browse_test.go", + opts: BrowseOptions{ + SelectorArg: filepath.Join(".", "browse_test.go"), + PathFromRepoRoot: func() string { + return "pkg/cmd/browse/" + }, + }, + baseRepo: ghrepo.New("bchadwic", "gh-graph"), + defaultBranch: "trunk", + expectedURL: "https://github.com/bchadwic/gh-graph/tree/trunk/pkg/cmd/browse/browse_test.go", + wantsErr: false, + }, + { + name: "relative path to file in parent folder from browse_test.go", + opts: BrowseOptions{ + SelectorArg: ".." + s + "pr", + PathFromRepoRoot: func() string { + return "pkg/cmd/browse/" + }, + }, + baseRepo: ghrepo.New("bchadwic", "gh-graph"), + defaultBranch: "trunk", + expectedURL: "https://github.com/bchadwic/gh-graph/tree/trunk/pkg/cmd/pr", + wantsErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + io, _, stdout, stderr := iostreams.Test() + browser := cmdutil.TestBrowser{} + + reg := httpmock.Registry{} + defer reg.Verify(t) + if tt.defaultBranch != "" { + reg.StubRepoInfoResponse(tt.baseRepo.RepoOwner(), tt.baseRepo.RepoName(), tt.defaultBranch) + } + + opts := tt.opts + opts.IO = io + opts.BaseRepo = func() (ghrepo.Interface, error) { + return tt.baseRepo, nil + } + opts.HttpClient = func() (*http.Client, error) { + return &http.Client{Transport: ®}, nil + } + opts.Browser = &browser + if opts.PathFromRepoRoot == nil { + opts.PathFromRepoRoot = git.PathFromRepoRoot + } + + err := runBrowse(&opts) + if tt.wantsErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if opts.NoBrowserFlag { + assert.Equal(t, fmt.Sprintf("%s\n", tt.expectedURL), stdout.String()) + assert.Equal(t, "", stderr.String()) + browser.Verify(t, "") + } else { + assert.Equal(t, "", stdout.String()) + assert.Equal(t, "", stderr.String()) + browser.Verify(t, tt.expectedURL) + } + }) + } +} + +func Test_parsePathFromFileArg(t *testing.T) { + s := string(os.PathSeparator) + tests := []struct { + name string + fileArg string + expectedPath string + }{ + { + name: "go to parent folder", + fileArg: ".." + s, + expectedPath: "pkg/cmd", + }, + { + name: "current folder", + fileArg: ".", + expectedPath: "pkg/cmd/browse", + }, + { + name: "current folder (alternative)", + fileArg: "." + s, + expectedPath: "pkg/cmd/browse", + }, + { + name: "file that starts with '.'", + fileArg: ".gitignore", + expectedPath: "pkg/cmd/browse/.gitignore", + }, + { + name: "file in current folder", + fileArg: filepath.Join(".", "browse.go"), + expectedPath: "pkg/cmd/browse/browse.go", + }, + { + name: "file within parent folder", + fileArg: filepath.Join("..", "browse.go"), + expectedPath: "pkg/cmd/browse.go", + }, + { + name: "file within parent folder uncleaned", + fileArg: filepath.Join("..", ".") + s + s + s + "browse.go", + expectedPath: "pkg/cmd/browse.go", + }, + { + name: "different path from root directory", + fileArg: filepath.Join("..", "..", "..", "internal/build/build.go"), + expectedPath: "internal/build/build.go", + }, + { + name: "go out of repository", + fileArg: filepath.Join("..", "..", "..", "..", "..", "..") + s + "", + expectedPath: "", + }, + { + name: "go to root of repository", + fileArg: filepath.Join("..", "..", "..") + s + "", + expectedPath: "", + }, + } + for _, tt := range tests { + path, _, _, _ := parseFile(BrowseOptions{ + PathFromRepoRoot: func() string { + return "pkg/cmd/browse/" + }}, tt.fileArg) + assert.Equal(t, tt.expectedPath, path, tt.name) + } +} diff --git a/pkg/cmd/codespace/code.go b/pkg/cmd/codespace/code.go new file mode 100644 index 00000000000..4261bcf7d2b --- /dev/null +++ b/pkg/cmd/codespace/code.go @@ -0,0 +1,60 @@ +package codespace + +import ( + "context" + "fmt" + "net/url" + + "github.com/skratchdot/open-golang/open" + "github.com/spf13/cobra" +) + +func newCodeCmd(app *App) *cobra.Command { + var ( + codespace string + useInsiders bool + ) + + codeCmd := &cobra.Command{ + Use: "code", + Short: "Open a codespace in VS Code", + Args: noArgsConstraint, + RunE: func(cmd *cobra.Command, args []string) error { + return app.VSCode(cmd.Context(), codespace, useInsiders) + }, + } + + codeCmd.Flags().StringVarP(&codespace, "codespace", "c", "", "Name of the codespace") + codeCmd.Flags().BoolVar(&useInsiders, "insiders", false, "Use the insiders version of VS Code") + + return codeCmd +} + +// VSCode opens a codespace in the local VS VSCode application. +func (a *App) VSCode(ctx context.Context, codespaceName string, useInsiders bool) error { + if codespaceName == "" { + codespace, err := chooseCodespace(ctx, a.apiClient) + if err != nil { + if err == errNoCodespaces { + return err + } + return fmt.Errorf("error choosing codespace: %w", err) + } + codespaceName = codespace.Name + } + + url := vscodeProtocolURL(codespaceName, useInsiders) + if err := open.Run(url); err != nil { + return fmt.Errorf("error opening vscode URL %s: %s. (Is VS Code installed?)", url, err) + } + + return nil +} + +func vscodeProtocolURL(codespaceName string, useInsiders bool) string { + application := "vscode" + if useInsiders { + application = "vscode-insiders" + } + return fmt.Sprintf("%s://github.codespaces/connect?name=%s", application, url.QueryEscape(codespaceName)) +} diff --git a/pkg/cmd/codespace/common.go b/pkg/cmd/codespace/common.go new file mode 100644 index 00000000000..6b1c445d8db --- /dev/null +++ b/pkg/cmd/codespace/common.go @@ -0,0 +1,265 @@ +package codespace + +// This file defines functions common to the entire codespace command set. + +import ( + "context" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "sort" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/AlecAivazis/survey/v2/terminal" + "github.com/cli/cli/v2/internal/codespaces/api" + "github.com/cli/cli/v2/pkg/cmd/codespace/output" + "github.com/spf13/cobra" + "golang.org/x/term" +) + +type App struct { + apiClient apiClient + logger *output.Logger +} + +func NewApp(logger *output.Logger, apiClient apiClient) *App { + return &App{ + apiClient: apiClient, + logger: logger, + } +} + +//go:generate moq -fmt goimports -rm -skip-ensure -out mock_api.go . apiClient +type apiClient interface { + GetUser(ctx context.Context) (*api.User, error) + GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error) + ListCodespaces(ctx context.Context, limit int) ([]*api.Codespace, error) + DeleteCodespace(ctx context.Context, name string) error + StartCodespace(ctx context.Context, name string) error + StopCodespace(ctx context.Context, name string) error + CreateCodespace(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error) + GetRepository(ctx context.Context, nwo string) (*api.Repository, error) + AuthorizedKeys(ctx context.Context, user string) ([]byte, error) + GetCodespaceRegionLocation(ctx context.Context) (string, error) + GetCodespacesMachines(ctx context.Context, repoID int, branch, location string) ([]*api.Machine, error) + GetCodespaceRepositoryContents(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error) +} + +var errNoCodespaces = errors.New("you have no codespaces") + +func chooseCodespace(ctx context.Context, apiClient apiClient) (*api.Codespace, error) { + codespaces, err := apiClient.ListCodespaces(ctx, -1) + if err != nil { + return nil, fmt.Errorf("error getting codespaces: %w", err) + } + return chooseCodespaceFromList(ctx, codespaces) +} + +// chooseCodespaceFromList returns the selected codespace from the list, +// or an error if there are no codespaces. +func chooseCodespaceFromList(ctx context.Context, codespaces []*api.Codespace) (*api.Codespace, error) { + if len(codespaces) == 0 { + return nil, errNoCodespaces + } + + sort.Slice(codespaces, func(i, j int) bool { + return codespaces[i].CreatedAt > codespaces[j].CreatedAt + }) + + type codespaceWithIndex struct { + cs codespace + idx int + } + + namesWithConflict := make(map[string]bool) + codespacesByName := make(map[string]codespaceWithIndex) + codespacesNames := make([]string, 0, len(codespaces)) + for _, apiCodespace := range codespaces { + cs := codespace{apiCodespace} + csName := cs.displayName(false, false) + displayNameWithGitStatus := cs.displayName(false, true) + + _, hasExistingConflict := namesWithConflict[csName] + if seenCodespace, ok := codespacesByName[csName]; ok || hasExistingConflict { + // There is an existing codespace on the repo and branch. + // We need to disambiguate by adding the codespace name + // to the existing entry and the one we are processing now. + if !hasExistingConflict { + fullDisplayName := seenCodespace.cs.displayName(true, false) + fullDisplayNameWithGitStatus := seenCodespace.cs.displayName(true, true) + + codespacesByName[fullDisplayName] = codespaceWithIndex{seenCodespace.cs, seenCodespace.idx} + codespacesNames[seenCodespace.idx] = fullDisplayNameWithGitStatus + delete(codespacesByName, csName) // delete the existing map entry with old name + + // All other codespaces with the same name should update + // to their specific name, this tracks conflicting names going forward + namesWithConflict[csName] = true + } + + // update this codespace names to include the name to disambiguate + csName = cs.displayName(true, false) + displayNameWithGitStatus = cs.displayName(true, true) + } + + codespacesByName[csName] = codespaceWithIndex{cs, len(codespacesNames)} + codespacesNames = append(codespacesNames, displayNameWithGitStatus) + } + + csSurvey := []*survey.Question{ + { + Name: "codespace", + Prompt: &survey.Select{ + Message: "Choose codespace:", + Options: codespacesNames, + Default: codespacesNames[0], + }, + Validate: survey.Required, + }, + } + + var answers struct { + Codespace string + } + if err := ask(csSurvey, &answers); err != nil { + return nil, fmt.Errorf("error getting answers: %w", err) + } + + // Codespaces are indexed without the git status included as compared + // to how it is displayed in the prompt, so the git status symbol needs + // cleaning up in case it is included. + selectedCodespace := strings.Replace(answers.Codespace, gitStatusDirty, "", -1) + return codespacesByName[selectedCodespace].cs.Codespace, nil +} + +// getOrChooseCodespace prompts the user to choose a codespace if the codespaceName is empty. +// It then fetches the codespace record with full connection details. +func getOrChooseCodespace(ctx context.Context, apiClient apiClient, codespaceName string) (codespace *api.Codespace, err error) { + if codespaceName == "" { + codespace, err = chooseCodespace(ctx, apiClient) + if err != nil { + if err == errNoCodespaces { + return nil, err + } + return nil, fmt.Errorf("choosing codespace: %w", err) + } + } else { + codespace, err = apiClient.GetCodespace(ctx, codespaceName, true) + if err != nil { + return nil, fmt.Errorf("getting full codespace details: %w", err) + } + } + + return codespace, nil +} + +func safeClose(closer io.Closer, err *error) { + if closeErr := closer.Close(); *err == nil { + *err = closeErr + } +} + +// hasTTY indicates whether the process connected to a terminal. +// It is not portable to assume stdin/stdout are fds 0 and 1. +var hasTTY = term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd())) + +// ask asks survey questions on the terminal, using standard options. +// It fails unless hasTTY, but ideally callers should avoid calling it in that case. +func ask(qs []*survey.Question, response interface{}) error { + if !hasTTY { + return fmt.Errorf("no terminal") + } + err := survey.Ask(qs, response, survey.WithShowCursor(true)) + // The survey package temporarily clears the terminal's ISIG mode bit + // (see tcsetattr(3)) so the QUIT button (Ctrl-C) is reported as + // ASCII \x03 (ETX) instead of delivering SIGINT to the application. + // So we have to serve ourselves the SIGINT. + // + // https://github.com/AlecAivazis/survey/#why-isnt-ctrl-c-working + if err == terminal.InterruptErr { + self, _ := os.FindProcess(os.Getpid()) + _ = self.Signal(os.Interrupt) // assumes POSIX + + // Suspend the goroutine, to avoid a race between + // return from main and async delivery of INT signal. + select {} + } + return err +} + +// checkAuthorizedKeys reports an error if the user has not registered any SSH keys; +// see https://github.com/cli/cli/v2/issues/166#issuecomment-921769703. +// The check is not required for security but it improves the error message. +func checkAuthorizedKeys(ctx context.Context, client apiClient, user string) error { + keys, err := client.AuthorizedKeys(ctx, user) + if err != nil { + return fmt.Errorf("failed to read GitHub-authorized SSH keys for %s: %w", user, err) + } + if len(keys) == 0 { + return fmt.Errorf("user %s has no GitHub-authorized SSH keys", user) + } + return nil // success +} + +var ErrTooManyArgs = errors.New("the command accepts no arguments") + +func noArgsConstraint(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + return ErrTooManyArgs + } + return nil +} + +func noopLogger() *log.Logger { + return log.New(ioutil.Discard, "", 0) +} + +type codespace struct { + *api.Codespace +} + +// displayName returns the repository nwo and branch. +// If includeName is true, the name of the codespace is included. +// If includeGitStatus is true, the branch will include a star if +// the codespace has unsaved changes. +func (c codespace) displayName(includeName, includeGitStatus bool) string { + branch := c.GitStatus.Ref + if includeGitStatus { + branch = c.branchWithGitStatus() + } + + if includeName { + return fmt.Sprintf( + "%s: %s [%s]", c.Repository.FullName, branch, c.Name, + ) + } + return c.Repository.FullName + ": " + branch +} + +// gitStatusDirty represents an unsaved changes status. +const gitStatusDirty = "*" + +// branchWithGitStatus returns the branch with a star +// if the branch is currently being worked on. +func (c codespace) branchWithGitStatus() string { + if c.hasUnsavedChanges() { + return c.GitStatus.Ref + gitStatusDirty + } + + return c.GitStatus.Ref +} + +// hasUnsavedChanges returns whether the environment has +// unsaved changes. +func (c codespace) hasUnsavedChanges() bool { + return c.GitStatus.HasUncommitedChanges || c.GitStatus.HasUnpushedChanges +} + +// running returns whether the codespace environment is running. +func (c codespace) running() bool { + return c.State == api.CodespaceStateAvailable +} diff --git a/pkg/cmd/codespace/create.go b/pkg/cmd/codespace/create.go new file mode 100644 index 00000000000..a558f4c6732 --- /dev/null +++ b/pkg/cmd/codespace/create.go @@ -0,0 +1,296 @@ +package codespace + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/cli/cli/v2/internal/codespaces" + "github.com/cli/cli/v2/internal/codespaces/api" + "github.com/cli/cli/v2/pkg/cmd/codespace/output" + "github.com/fatih/camelcase" + "github.com/spf13/cobra" +) + +type createOptions struct { + repo string + branch string + machine string + showStatus bool +} + +func newCreateCmd(app *App) *cobra.Command { + opts := createOptions{} + + createCmd := &cobra.Command{ + Use: "create", + Short: "Create a codespace", + Args: noArgsConstraint, + RunE: func(cmd *cobra.Command, args []string) error { + return app.Create(cmd.Context(), opts) + }, + } + + createCmd.Flags().StringVarP(&opts.repo, "repo", "r", "", "repository name with owner: user/repo") + createCmd.Flags().StringVarP(&opts.branch, "branch", "b", "", "repository branch") + createCmd.Flags().StringVarP(&opts.machine, "machine", "m", "", "hardware specifications for the VM") + createCmd.Flags().BoolVarP(&opts.showStatus, "status", "s", false, "show status of post-create command and dotfiles") + + return createCmd +} + +// Create creates a new Codespace +func (a *App) Create(ctx context.Context, opts createOptions) error { + locationCh := getLocation(ctx, a.apiClient) + userCh := getUser(ctx, a.apiClient) + + repo, err := getRepoName(opts.repo) + if err != nil { + return fmt.Errorf("error getting repository name: %w", err) + } + branch, err := getBranchName(opts.branch) + if err != nil { + return fmt.Errorf("error getting branch name: %w", err) + } + + repository, err := a.apiClient.GetRepository(ctx, repo) + if err != nil { + return fmt.Errorf("error getting repository: %w", err) + } + + locationResult := <-locationCh + if locationResult.Err != nil { + return fmt.Errorf("error getting codespace region location: %w", locationResult.Err) + } + + userResult := <-userCh + if userResult.Err != nil { + return fmt.Errorf("error getting codespace user: %w", userResult.Err) + } + + machine, err := getMachineName(ctx, a.apiClient, repository.ID, opts.machine, branch, locationResult.Location) + if err != nil { + return fmt.Errorf("error getting machine type: %w", err) + } + if machine == "" { + return errors.New("there are no available machine types for this repository") + } + + a.logger.Print("Creating your codespace...") + codespace, err := a.apiClient.CreateCodespace(ctx, &api.CreateCodespaceParams{ + RepositoryID: repository.ID, + Branch: branch, + Machine: machine, + Location: locationResult.Location, + }) + a.logger.Print("\n") + if err != nil { + return fmt.Errorf("error creating codespace: %w", err) + } + + if opts.showStatus { + if err := showStatus(ctx, a.logger, a.apiClient, userResult.User, codespace); err != nil { + return fmt.Errorf("show status: %w", err) + } + } + + a.logger.Printf("Codespace created: ") + + fmt.Fprintln(os.Stdout, codespace.Name) + + return nil +} + +// showStatus polls the codespace for a list of post create states and their status. It will keep polling +// until all states have finished. Once all states have finished, we poll once more to check if any new +// states have been introduced and stop polling otherwise. +func showStatus(ctx context.Context, log *output.Logger, apiClient apiClient, user *api.User, codespace *api.Codespace) error { + var lastState codespaces.PostCreateState + var breakNextState bool + + finishedStates := make(map[string]bool) + ctx, stopPolling := context.WithCancel(ctx) + defer stopPolling() + + poller := func(states []codespaces.PostCreateState) { + var inProgress bool + for _, state := range states { + if _, found := finishedStates[state.Name]; found { + continue // skip this state as we've processed it already + } + + if state.Name != lastState.Name { + log.Print(state.Name) + + if state.Status == codespaces.PostCreateStateRunning { + inProgress = true + lastState = state + log.Print("...") + break + } + + finishedStates[state.Name] = true + log.Println("..." + state.Status) + } else { + if state.Status == codespaces.PostCreateStateRunning { + inProgress = true + log.Print(".") + break + } + + finishedStates[state.Name] = true + log.Println(state.Status) + lastState = codespaces.PostCreateState{} // reset the value + } + } + + if !inProgress { + if breakNextState { + stopPolling() + return + } + breakNextState = true + } + } + + err := codespaces.PollPostCreateStates(ctx, log, apiClient, codespace, poller) + if err != nil { + if errors.Is(err, context.Canceled) && breakNextState { + return nil // we cancelled the context to stop polling, we can ignore the error + } + + return fmt.Errorf("failed to poll state changes from codespace: %w", err) + } + + return nil +} + +type getUserResult struct { + User *api.User + Err error +} + +// getUser fetches the user record associated with the GITHUB_TOKEN +func getUser(ctx context.Context, apiClient apiClient) <-chan getUserResult { + ch := make(chan getUserResult, 1) + go func() { + user, err := apiClient.GetUser(ctx) + ch <- getUserResult{user, err} + }() + return ch +} + +type locationResult struct { + Location string + Err error +} + +// getLocation fetches the closest Codespace datacenter region/location to the user. +func getLocation(ctx context.Context, apiClient apiClient) <-chan locationResult { + ch := make(chan locationResult, 1) + go func() { + location, err := apiClient.GetCodespaceRegionLocation(ctx) + ch <- locationResult{location, err} + }() + return ch +} + +// getRepoName prompts the user for the name of the repository, or returns the repository if non-empty. +func getRepoName(repo string) (string, error) { + if repo != "" { + return repo, nil + } + + repoSurvey := []*survey.Question{ + { + Name: "repository", + Prompt: &survey.Input{Message: "Repository:"}, + Validate: survey.Required, + }, + } + err := ask(repoSurvey, &repo) + return repo, err +} + +// getBranchName prompts the user for the name of the branch, or returns the branch if non-empty. +func getBranchName(branch string) (string, error) { + if branch != "" { + return branch, nil + } + + branchSurvey := []*survey.Question{ + { + Name: "branch", + Prompt: &survey.Input{Message: "Branch:"}, + Validate: survey.Required, + }, + } + err := ask(branchSurvey, &branch) + return branch, err +} + +// getMachineName prompts the user to select the machine type, or validates the machine if non-empty. +func getMachineName(ctx context.Context, apiClient apiClient, repoID int, machine, branch, location string) (string, error) { + machines, err := apiClient.GetCodespacesMachines(ctx, repoID, branch, location) + if err != nil { + return "", fmt.Errorf("error requesting machine instance types: %w", err) + } + + // if user supplied a machine type, it must be valid + // if no machine type was supplied, we don't error if there are no machine types for the current repo + if machine != "" { + for _, m := range machines { + if machine == m.Name { + return machine, nil + } + } + + availableMachines := make([]string, len(machines)) + for i := 0; i < len(machines); i++ { + availableMachines[i] = machines[i].Name + } + + return "", fmt.Errorf("there is no such machine for the repository: %s\nAvailable machines: %v", machine, availableMachines) + } else if len(machines) == 0 { + return "", nil + } + + if len(machines) == 1 { + // VS Code does not prompt for machine if there is only one, this makes us consistent with that behavior + return machines[0].Name, nil + } + + machineNames := make([]string, 0, len(machines)) + machineByName := make(map[string]*api.Machine) + for _, m := range machines { + nameParts := camelcase.Split(m.Name) + machineName := strings.Title(strings.ToLower(nameParts[0])) + machineName = fmt.Sprintf("%s - %s", machineName, m.DisplayName) + machineNames = append(machineNames, machineName) + machineByName[machineName] = m + } + + machineSurvey := []*survey.Question{ + { + Name: "machine", + Prompt: &survey.Select{ + Message: "Choose Machine Type:", + Options: machineNames, + Default: machineNames[0], + }, + Validate: survey.Required, + }, + } + + var machineAnswers struct{ Machine string } + if err := ask(machineSurvey, &machineAnswers); err != nil { + return "", fmt.Errorf("error getting machine: %w", err) + } + + selectedMachine := machineByName[machineAnswers.Machine] + + return selectedMachine.Name, nil +} diff --git a/pkg/cmd/codespace/delete.go b/pkg/cmd/codespace/delete.go new file mode 100644 index 00000000000..23d2abc083c --- /dev/null +++ b/pkg/cmd/codespace/delete.go @@ -0,0 +1,175 @@ +package codespace + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/AlecAivazis/survey/v2" + "github.com/cli/cli/v2/internal/codespaces/api" + "github.com/spf13/cobra" + "golang.org/x/sync/errgroup" +) + +type deleteOptions struct { + deleteAll bool + skipConfirm bool + codespaceName string + repoFilter string + keepDays uint16 + + isInteractive bool + now func() time.Time + prompter prompter +} + +//go:generate moq -fmt goimports -rm -skip-ensure -out mock_prompter.go . prompter +type prompter interface { + Confirm(message string) (bool, error) +} + +func newDeleteCmd(app *App) *cobra.Command { + opts := deleteOptions{ + isInteractive: hasTTY, + now: time.Now, + prompter: &surveyPrompter{}, + } + + deleteCmd := &cobra.Command{ + Use: "delete", + Short: "Delete a codespace", + Args: noArgsConstraint, + RunE: func(cmd *cobra.Command, args []string) error { + if opts.deleteAll && opts.repoFilter != "" { + return errors.New("both --all and --repo is not supported") + } + return app.Delete(cmd.Context(), opts) + }, + } + + deleteCmd.Flags().StringVarP(&opts.codespaceName, "codespace", "c", "", "Name of the codespace") + deleteCmd.Flags().BoolVar(&opts.deleteAll, "all", false, "Delete all codespaces") + deleteCmd.Flags().StringVarP(&opts.repoFilter, "repo", "r", "", "Delete codespaces for a `repository`") + deleteCmd.Flags().BoolVarP(&opts.skipConfirm, "force", "f", false, "Skip confirmation for codespaces that contain unsaved changes") + deleteCmd.Flags().Uint16Var(&opts.keepDays, "days", 0, "Delete codespaces older than `N` days") + + return deleteCmd +} + +func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) { + var codespaces []*api.Codespace + nameFilter := opts.codespaceName + if nameFilter == "" { + codespaces, err = a.apiClient.ListCodespaces(ctx, -1) + if err != nil { + return fmt.Errorf("error getting codespaces: %w", err) + } + + if !opts.deleteAll && opts.repoFilter == "" { + c, err := chooseCodespaceFromList(ctx, codespaces) + if err != nil { + return fmt.Errorf("error choosing codespace: %w", err) + } + nameFilter = c.Name + } + } else { + codespace, err := a.apiClient.GetCodespace(ctx, nameFilter, false) + if err != nil { + return fmt.Errorf("error fetching codespace information: %w", err) + } + + codespaces = []*api.Codespace{codespace} + } + + codespacesToDelete := make([]*api.Codespace, 0, len(codespaces)) + lastUpdatedCutoffTime := opts.now().AddDate(0, 0, -int(opts.keepDays)) + for _, c := range codespaces { + if nameFilter != "" && c.Name != nameFilter { + continue + } + if opts.repoFilter != "" && !strings.EqualFold(c.Repository.FullName, opts.repoFilter) { + continue + } + if opts.keepDays > 0 { + t, err := time.Parse(time.RFC3339, c.LastUsedAt) + if err != nil { + return fmt.Errorf("error parsing last_used_at timestamp %q: %w", c.LastUsedAt, err) + } + if t.After(lastUpdatedCutoffTime) { + continue + } + } + if !opts.skipConfirm { + confirmed, err := confirmDeletion(opts.prompter, c, opts.isInteractive) + if err != nil { + return fmt.Errorf("unable to confirm: %w", err) + } + if !confirmed { + continue + } + } + codespacesToDelete = append(codespacesToDelete, c) + } + + if len(codespacesToDelete) == 0 { + return errors.New("no codespaces to delete") + } + + g := errgroup.Group{} + for _, c := range codespacesToDelete { + codespaceName := c.Name + g.Go(func() error { + if err := a.apiClient.DeleteCodespace(ctx, codespaceName); err != nil { + _, _ = a.logger.Errorf("error deleting codespace %q: %v\n", codespaceName, err) + return err + } + return nil + }) + } + + if err := g.Wait(); err != nil { + return errors.New("some codespaces failed to delete") + } + + noun := "Codespace" + if len(codespacesToDelete) > 1 { + noun = noun + "s" + } + a.logger.Println(noun + " deleted.") + + return nil +} + +func confirmDeletion(p prompter, apiCodespace *api.Codespace, isInteractive bool) (bool, error) { + cs := codespace{apiCodespace} + if !cs.hasUnsavedChanges() { + return true, nil + } + if !isInteractive { + return false, fmt.Errorf("codespace %s has unsaved changes (use --force to override)", cs.Name) + } + return p.Confirm(fmt.Sprintf("Codespace %s has unsaved changes. OK to delete?", cs.Name)) +} + +type surveyPrompter struct{} + +func (p *surveyPrompter) Confirm(message string) (bool, error) { + var confirmed struct { + Confirmed bool + } + q := []*survey.Question{ + { + Name: "confirmed", + Prompt: &survey.Confirm{ + Message: message, + }, + }, + } + if err := ask(q, &confirmed); err != nil { + return false, fmt.Errorf("failed to prompt: %w", err) + } + + return confirmed.Confirmed, nil +} diff --git a/pkg/cmd/codespace/delete_test.go b/pkg/cmd/codespace/delete_test.go new file mode 100644 index 00000000000..8ffdc3feef8 --- /dev/null +++ b/pkg/cmd/codespace/delete_test.go @@ -0,0 +1,240 @@ +package codespace + +import ( + "bytes" + "context" + "errors" + "fmt" + "sort" + "strings" + "testing" + "time" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/codespaces/api" + "github.com/cli/cli/v2/pkg/cmd/codespace/output" +) + +func TestDelete(t *testing.T) { + user := &api.User{Login: "hubot"} + now, _ := time.Parse(time.RFC3339, "2021-09-22T00:00:00Z") + daysAgo := func(n int) string { + return now.Add(time.Hour * -time.Duration(24*n)).Format(time.RFC3339) + } + + tests := []struct { + name string + opts deleteOptions + codespaces []*api.Codespace + confirms map[string]bool + deleteErr error + wantErr bool + wantDeleted []string + wantStdout string + wantStderr string + }{ + { + name: "by name", + opts: deleteOptions{ + codespaceName: "hubot-robawt-abc", + }, + codespaces: []*api.Codespace{ + { + Name: "hubot-robawt-abc", + }, + }, + wantDeleted: []string{"hubot-robawt-abc"}, + wantStdout: "Codespace deleted.\n", + }, + { + name: "by repo", + opts: deleteOptions{ + repoFilter: "monalisa/spoon-knife", + }, + codespaces: []*api.Codespace{ + { + Name: "monalisa-spoonknife-123", + Repository: api.Repository{ + FullName: "monalisa/Spoon-Knife", + }, + }, + { + Name: "hubot-robawt-abc", + Repository: api.Repository{ + FullName: "hubot/ROBAWT", + }, + }, + { + Name: "monalisa-spoonknife-c4f3", + Repository: api.Repository{ + FullName: "monalisa/Spoon-Knife", + }, + }, + }, + wantDeleted: []string{"monalisa-spoonknife-123", "monalisa-spoonknife-c4f3"}, + wantStdout: "Codespaces deleted.\n", + }, + { + name: "unused", + opts: deleteOptions{ + deleteAll: true, + keepDays: 3, + }, + codespaces: []*api.Codespace{ + { + Name: "monalisa-spoonknife-123", + LastUsedAt: daysAgo(1), + }, + { + Name: "hubot-robawt-abc", + LastUsedAt: daysAgo(4), + }, + { + Name: "monalisa-spoonknife-c4f3", + LastUsedAt: daysAgo(10), + }, + }, + wantDeleted: []string{"hubot-robawt-abc", "monalisa-spoonknife-c4f3"}, + wantStdout: "Codespaces deleted.\n", + }, + { + name: "deletion failed", + opts: deleteOptions{ + deleteAll: true, + }, + codespaces: []*api.Codespace{ + { + Name: "monalisa-spoonknife-123", + }, + { + Name: "hubot-robawt-abc", + }, + }, + deleteErr: errors.New("aborted by test"), + wantErr: true, + wantDeleted: []string{"hubot-robawt-abc", "monalisa-spoonknife-123"}, + wantStderr: heredoc.Doc(` + error deleting codespace "hubot-robawt-abc": aborted by test + error deleting codespace "monalisa-spoonknife-123": aborted by test + `), + }, + { + name: "with confirm", + opts: deleteOptions{ + isInteractive: true, + deleteAll: true, + skipConfirm: false, + }, + codespaces: []*api.Codespace{ + { + Name: "monalisa-spoonknife-123", + GitStatus: api.CodespaceGitStatus{ + HasUnpushedChanges: true, + }, + }, + { + Name: "hubot-robawt-abc", + GitStatus: api.CodespaceGitStatus{ + HasUncommitedChanges: true, + }, + }, + { + Name: "monalisa-spoonknife-c4f3", + GitStatus: api.CodespaceGitStatus{ + HasUnpushedChanges: false, + HasUncommitedChanges: false, + }, + }, + }, + confirms: map[string]bool{ + "Codespace monalisa-spoonknife-123 has unsaved changes. OK to delete?": false, + "Codespace hubot-robawt-abc has unsaved changes. OK to delete?": true, + }, + wantDeleted: []string{"hubot-robawt-abc", "monalisa-spoonknife-c4f3"}, + wantStdout: "Codespaces deleted.\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + apiMock := &apiClientMock{ + GetUserFunc: func(_ context.Context) (*api.User, error) { + return user, nil + }, + DeleteCodespaceFunc: func(_ context.Context, name string) error { + if tt.deleteErr != nil { + return tt.deleteErr + } + return nil + }, + } + if tt.opts.codespaceName == "" { + apiMock.ListCodespacesFunc = func(_ context.Context, num int) ([]*api.Codespace, error) { + return tt.codespaces, nil + } + } else { + apiMock.GetCodespaceFunc = func(_ context.Context, name string, includeConnection bool) (*api.Codespace, error) { + return tt.codespaces[0], nil + } + } + opts := tt.opts + opts.now = func() time.Time { return now } + opts.prompter = &prompterMock{ + ConfirmFunc: func(msg string) (bool, error) { + res, found := tt.confirms[msg] + if !found { + return false, fmt.Errorf("unexpected prompt %q", msg) + } + return res, nil + }, + } + + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + app := &App{ + apiClient: apiMock, + logger: output.NewLogger(stdout, stderr, false), + } + err := app.Delete(context.Background(), opts) + if (err != nil) != tt.wantErr { + t.Errorf("delete() error = %v, wantErr %v", err, tt.wantErr) + } + var gotDeleted []string + for _, delArgs := range apiMock.DeleteCodespaceCalls() { + gotDeleted = append(gotDeleted, delArgs.Name) + } + sort.Strings(gotDeleted) + if !sliceEquals(gotDeleted, tt.wantDeleted) { + t.Errorf("deleted %q, want %q", gotDeleted, tt.wantDeleted) + } + if out := stdout.String(); out != tt.wantStdout { + t.Errorf("stdout = %q, want %q", out, tt.wantStdout) + } + if out := sortLines(stderr.String()); out != tt.wantStderr { + t.Errorf("stderr = %q, want %q", out, tt.wantStderr) + } + }) + } +} + +func sliceEquals(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func sortLines(s string) string { + trailing := "" + if strings.HasSuffix(s, "\n") { + s = strings.TrimSuffix(s, "\n") + trailing = "\n" + } + lines := strings.Split(s, "\n") + sort.Strings(lines) + return strings.Join(lines, "\n") + trailing +} diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go new file mode 100644 index 00000000000..e130c9ed7ba --- /dev/null +++ b/pkg/cmd/codespace/list.go @@ -0,0 +1,57 @@ +package codespace + +import ( + "context" + "fmt" + "os" + + "github.com/cli/cli/v2/pkg/cmd/codespace/output" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/spf13/cobra" +) + +func newListCmd(app *App) *cobra.Command { + var asJSON bool + var limit int + + listCmd := &cobra.Command{ + Use: "list", + Short: "List your codespaces", + Args: noArgsConstraint, + RunE: func(cmd *cobra.Command, args []string) error { + if limit < 1 { + return &cmdutil.FlagError{Err: fmt.Errorf("invalid limit: %v", limit)} + } + + return app.List(cmd.Context(), asJSON, limit) + }, + } + + listCmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON") + listCmd.Flags().IntVarP(&limit, "limit", "L", 30, "Maximum number of codespaces to list") + + return listCmd +} + +func (a *App) List(ctx context.Context, asJSON bool, limit int) error { + codespaces, err := a.apiClient.ListCodespaces(ctx, limit) + if err != nil { + return fmt.Errorf("error getting codespaces: %w", err) + } + + table := output.NewTable(os.Stdout, asJSON) + table.SetHeader([]string{"Name", "Repository", "Branch", "State", "Created At"}) + for _, apiCodespace := range codespaces { + cs := codespace{apiCodespace} + table.Append([]string{ + cs.Name, + cs.Repository.FullName, + cs.branchWithGitStatus(), + cs.State, + cs.CreatedAt, + }) + } + + table.Render() + return nil +} diff --git a/pkg/cmd/codespace/logs.go b/pkg/cmd/codespace/logs.go new file mode 100644 index 00000000000..1b6e57a84f7 --- /dev/null +++ b/pkg/cmd/codespace/logs.go @@ -0,0 +1,112 @@ +package codespace + +import ( + "context" + "fmt" + "net" + + "github.com/cli/cli/v2/internal/codespaces" + "github.com/cli/cli/v2/pkg/liveshare" + "github.com/spf13/cobra" +) + +func newLogsCmd(app *App) *cobra.Command { + var ( + codespace string + follow bool + ) + + logsCmd := &cobra.Command{ + Use: "logs", + Short: "Access codespace logs", + Args: noArgsConstraint, + RunE: func(cmd *cobra.Command, args []string) error { + return app.Logs(cmd.Context(), codespace, follow) + }, + } + + logsCmd.Flags().StringVarP(&codespace, "codespace", "c", "", "Name of the codespace") + logsCmd.Flags().BoolVarP(&follow, "follow", "f", false, "Tail and follow the logs") + + return logsCmd +} + +func (a *App) Logs(ctx context.Context, codespaceName string, follow bool) (err error) { + // Ensure all child tasks (port forwarding, remote exec) terminate before return. + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + user, err := a.apiClient.GetUser(ctx) + if err != nil { + return fmt.Errorf("getting user: %w", err) + } + + authkeys := make(chan error, 1) + go func() { + authkeys <- checkAuthorizedKeys(ctx, a.apiClient, user.Login) + }() + + codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) + if err != nil { + return fmt.Errorf("get or choose codespace: %w", err) + } + + session, err := codespaces.ConnectToLiveshare(ctx, a.logger, noopLogger(), a.apiClient, codespace) + if err != nil { + return fmt.Errorf("connecting to Live Share: %w", err) + } + defer safeClose(session, &err) + + if err := <-authkeys; err != nil { + return err + } + + // Ensure local port is listening before client (getPostCreateOutput) connects. + listen, err := net.Listen("tcp", "127.0.0.1:0") // arbitrary port + if err != nil { + return err + } + defer listen.Close() + localPort := listen.Addr().(*net.TCPAddr).Port + + a.logger.Println("Fetching SSH Details...") + remoteSSHServerPort, sshUser, err := session.StartSSHServer(ctx) + if err != nil { + return fmt.Errorf("error getting ssh server details: %w", err) + } + + cmdType := "cat" + if follow { + cmdType = "tail -f" + } + + dst := fmt.Sprintf("%s@localhost", sshUser) + cmd, err := codespaces.NewRemoteCommand( + ctx, localPort, dst, fmt.Sprintf("%s /workspaces/.codespaces/.persistedshare/creation.log", cmdType), + ) + if err != nil { + return fmt.Errorf("remote command: %w", err) + } + + tunnelClosed := make(chan error, 1) + go func() { + fwd := liveshare.NewPortForwarder(session, "sshd", remoteSSHServerPort, false) + tunnelClosed <- fwd.ForwardToListener(ctx, listen) // error is non-nil + }() + + cmdDone := make(chan error, 1) + go func() { + cmdDone <- cmd.Run() + }() + + select { + case err := <-tunnelClosed: + return fmt.Errorf("connection closed: %w", err) + case err := <-cmdDone: + if err != nil { + return fmt.Errorf("error retrieving logs: %w", err) + } + + return nil // success + } +} diff --git a/pkg/cmd/codespace/mock_api.go b/pkg/cmd/codespace/mock_api.go new file mode 100644 index 00000000000..8d40934dab2 --- /dev/null +++ b/pkg/cmd/codespace/mock_api.go @@ -0,0 +1,629 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package codespace + +import ( + "context" + "sync" + + "github.com/cli/cli/v2/internal/codespaces/api" +) + +// apiClientMock is a mock implementation of apiClient. +// +// func TestSomethingThatUsesapiClient(t *testing.T) { +// +// // make and configure a mocked apiClient +// mockedapiClient := &apiClientMock{ +// AuthorizedKeysFunc: func(ctx context.Context, user string) ([]byte, error) { +// panic("mock out the AuthorizedKeys method") +// }, +// CreateCodespaceFunc: func(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error) { +// panic("mock out the CreateCodespace method") +// }, +// DeleteCodespaceFunc: func(ctx context.Context, name string) error { +// panic("mock out the DeleteCodespace method") +// }, +// GetCodespaceFunc: func(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error) { +// panic("mock out the GetCodespace method") +// }, +// GetCodespaceRegionLocationFunc: func(ctx context.Context) (string, error) { +// panic("mock out the GetCodespaceRegionLocation method") +// }, +// GetCodespaceRepositoryContentsFunc: func(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error) { +// panic("mock out the GetCodespaceRepositoryContents method") +// }, +// GetCodespacesMachinesFunc: func(ctx context.Context, repoID int, branch string, location string) ([]*api.Machine, error) { +// panic("mock out the GetCodespacesMachines method") +// }, +// GetRepositoryFunc: func(ctx context.Context, nwo string) (*api.Repository, error) { +// panic("mock out the GetRepository method") +// }, +// GetUserFunc: func(ctx context.Context) (*api.User, error) { +// panic("mock out the GetUser method") +// }, +// ListCodespacesFunc: func(ctx context.Context, limit int) ([]*api.Codespace, error) { +// panic("mock out the ListCodespaces method") +// }, +// StartCodespaceFunc: func(ctx context.Context, name string) error { +// panic("mock out the StartCodespace method") +// }, +// StopCodespaceFunc: func(ctx context.Context, name string) error { +// panic("mock out the StopCodespace method") +// }, +// } +// +// // use mockedapiClient in code that requires apiClient +// // and then make assertions. +// +// } +type apiClientMock struct { + // AuthorizedKeysFunc mocks the AuthorizedKeys method. + AuthorizedKeysFunc func(ctx context.Context, user string) ([]byte, error) + + // CreateCodespaceFunc mocks the CreateCodespace method. + CreateCodespaceFunc func(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error) + + // DeleteCodespaceFunc mocks the DeleteCodespace method. + DeleteCodespaceFunc func(ctx context.Context, name string) error + + // GetCodespaceFunc mocks the GetCodespace method. + GetCodespaceFunc func(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error) + + // GetCodespaceRegionLocationFunc mocks the GetCodespaceRegionLocation method. + GetCodespaceRegionLocationFunc func(ctx context.Context) (string, error) + + // GetCodespaceRepositoryContentsFunc mocks the GetCodespaceRepositoryContents method. + GetCodespaceRepositoryContentsFunc func(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error) + + // GetCodespacesMachinesFunc mocks the GetCodespacesMachines method. + GetCodespacesMachinesFunc func(ctx context.Context, repoID int, branch string, location string) ([]*api.Machine, error) + + // GetRepositoryFunc mocks the GetRepository method. + GetRepositoryFunc func(ctx context.Context, nwo string) (*api.Repository, error) + + // GetUserFunc mocks the GetUser method. + GetUserFunc func(ctx context.Context) (*api.User, error) + + // ListCodespacesFunc mocks the ListCodespaces method. + ListCodespacesFunc func(ctx context.Context, limit int) ([]*api.Codespace, error) + + // StartCodespaceFunc mocks the StartCodespace method. + StartCodespaceFunc func(ctx context.Context, name string) error + + // StopCodespaceFunc mocks the StopCodespace method. + StopCodespaceFunc func(ctx context.Context, name string) error + + // calls tracks calls to the methods. + calls struct { + // AuthorizedKeys holds details about calls to the AuthorizedKeys method. + AuthorizedKeys []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // User is the user argument value. + User string + } + // CreateCodespace holds details about calls to the CreateCodespace method. + CreateCodespace []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Params is the params argument value. + Params *api.CreateCodespaceParams + } + // DeleteCodespace holds details about calls to the DeleteCodespace method. + DeleteCodespace []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Name is the name argument value. + Name string + } + // GetCodespace holds details about calls to the GetCodespace method. + GetCodespace []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Name is the name argument value. + Name string + // IncludeConnection is the includeConnection argument value. + IncludeConnection bool + } + // GetCodespaceRegionLocation holds details about calls to the GetCodespaceRegionLocation method. + GetCodespaceRegionLocation []struct { + // Ctx is the ctx argument value. + Ctx context.Context + } + // GetCodespaceRepositoryContents holds details about calls to the GetCodespaceRepositoryContents method. + GetCodespaceRepositoryContents []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Codespace is the codespace argument value. + Codespace *api.Codespace + // Path is the path argument value. + Path string + } + // GetCodespacesMachines holds details about calls to the GetCodespacesMachines method. + GetCodespacesMachines []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // RepoID is the repoID argument value. + RepoID int + // Branch is the branch argument value. + Branch string + // Location is the location argument value. + Location string + } + // GetRepository holds details about calls to the GetRepository method. + GetRepository []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Nwo is the nwo argument value. + Nwo string + } + // GetUser holds details about calls to the GetUser method. + GetUser []struct { + // Ctx is the ctx argument value. + Ctx context.Context + } + // ListCodespaces holds details about calls to the ListCodespaces method. + ListCodespaces []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Limit is the limit argument value. + Limit int + } + // StartCodespace holds details about calls to the StartCodespace method. + StartCodespace []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Name is the name argument value. + Name string + } + // StopCodespace holds details about calls to the StopCodespace method. + StopCodespace []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Name is the name argument value. + Name string + } + } + lockAuthorizedKeys sync.RWMutex + lockCreateCodespace sync.RWMutex + lockDeleteCodespace sync.RWMutex + lockGetCodespace sync.RWMutex + lockGetCodespaceRegionLocation sync.RWMutex + lockGetCodespaceRepositoryContents sync.RWMutex + lockGetCodespacesMachines sync.RWMutex + lockGetRepository sync.RWMutex + lockGetUser sync.RWMutex + lockListCodespaces sync.RWMutex + lockStartCodespace sync.RWMutex + lockStopCodespace sync.RWMutex +} + +// AuthorizedKeys calls AuthorizedKeysFunc. +func (mock *apiClientMock) AuthorizedKeys(ctx context.Context, user string) ([]byte, error) { + if mock.AuthorizedKeysFunc == nil { + panic("apiClientMock.AuthorizedKeysFunc: method is nil but apiClient.AuthorizedKeys was just called") + } + callInfo := struct { + Ctx context.Context + User string + }{ + Ctx: ctx, + User: user, + } + mock.lockAuthorizedKeys.Lock() + mock.calls.AuthorizedKeys = append(mock.calls.AuthorizedKeys, callInfo) + mock.lockAuthorizedKeys.Unlock() + return mock.AuthorizedKeysFunc(ctx, user) +} + +// AuthorizedKeysCalls gets all the calls that were made to AuthorizedKeys. +// Check the length with: +// len(mockedapiClient.AuthorizedKeysCalls()) +func (mock *apiClientMock) AuthorizedKeysCalls() []struct { + Ctx context.Context + User string +} { + var calls []struct { + Ctx context.Context + User string + } + mock.lockAuthorizedKeys.RLock() + calls = mock.calls.AuthorizedKeys + mock.lockAuthorizedKeys.RUnlock() + return calls +} + +// CreateCodespace calls CreateCodespaceFunc. +func (mock *apiClientMock) CreateCodespace(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error) { + if mock.CreateCodespaceFunc == nil { + panic("apiClientMock.CreateCodespaceFunc: method is nil but apiClient.CreateCodespace was just called") + } + callInfo := struct { + Ctx context.Context + Params *api.CreateCodespaceParams + }{ + Ctx: ctx, + Params: params, + } + mock.lockCreateCodespace.Lock() + mock.calls.CreateCodespace = append(mock.calls.CreateCodespace, callInfo) + mock.lockCreateCodespace.Unlock() + return mock.CreateCodespaceFunc(ctx, params) +} + +// CreateCodespaceCalls gets all the calls that were made to CreateCodespace. +// Check the length with: +// len(mockedapiClient.CreateCodespaceCalls()) +func (mock *apiClientMock) CreateCodespaceCalls() []struct { + Ctx context.Context + Params *api.CreateCodespaceParams +} { + var calls []struct { + Ctx context.Context + Params *api.CreateCodespaceParams + } + mock.lockCreateCodespace.RLock() + calls = mock.calls.CreateCodespace + mock.lockCreateCodespace.RUnlock() + return calls +} + +// DeleteCodespace calls DeleteCodespaceFunc. +func (mock *apiClientMock) DeleteCodespace(ctx context.Context, name string) error { + if mock.DeleteCodespaceFunc == nil { + panic("apiClientMock.DeleteCodespaceFunc: method is nil but apiClient.DeleteCodespace was just called") + } + callInfo := struct { + Ctx context.Context + Name string + }{ + Ctx: ctx, + Name: name, + } + mock.lockDeleteCodespace.Lock() + mock.calls.DeleteCodespace = append(mock.calls.DeleteCodespace, callInfo) + mock.lockDeleteCodespace.Unlock() + return mock.DeleteCodespaceFunc(ctx, name) +} + +// DeleteCodespaceCalls gets all the calls that were made to DeleteCodespace. +// Check the length with: +// len(mockedapiClient.DeleteCodespaceCalls()) +func (mock *apiClientMock) DeleteCodespaceCalls() []struct { + Ctx context.Context + Name string +} { + var calls []struct { + Ctx context.Context + Name string + } + mock.lockDeleteCodespace.RLock() + calls = mock.calls.DeleteCodespace + mock.lockDeleteCodespace.RUnlock() + return calls +} + +// GetCodespace calls GetCodespaceFunc. +func (mock *apiClientMock) GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error) { + if mock.GetCodespaceFunc == nil { + panic("apiClientMock.GetCodespaceFunc: method is nil but apiClient.GetCodespace was just called") + } + callInfo := struct { + Ctx context.Context + Name string + IncludeConnection bool + }{ + Ctx: ctx, + Name: name, + IncludeConnection: includeConnection, + } + mock.lockGetCodespace.Lock() + mock.calls.GetCodespace = append(mock.calls.GetCodespace, callInfo) + mock.lockGetCodespace.Unlock() + return mock.GetCodespaceFunc(ctx, name, includeConnection) +} + +// GetCodespaceCalls gets all the calls that were made to GetCodespace. +// Check the length with: +// len(mockedapiClient.GetCodespaceCalls()) +func (mock *apiClientMock) GetCodespaceCalls() []struct { + Ctx context.Context + Name string + IncludeConnection bool +} { + var calls []struct { + Ctx context.Context + Name string + IncludeConnection bool + } + mock.lockGetCodespace.RLock() + calls = mock.calls.GetCodespace + mock.lockGetCodespace.RUnlock() + return calls +} + +// GetCodespaceRegionLocation calls GetCodespaceRegionLocationFunc. +func (mock *apiClientMock) GetCodespaceRegionLocation(ctx context.Context) (string, error) { + if mock.GetCodespaceRegionLocationFunc == nil { + panic("apiClientMock.GetCodespaceRegionLocationFunc: method is nil but apiClient.GetCodespaceRegionLocation was just called") + } + callInfo := struct { + Ctx context.Context + }{ + Ctx: ctx, + } + mock.lockGetCodespaceRegionLocation.Lock() + mock.calls.GetCodespaceRegionLocation = append(mock.calls.GetCodespaceRegionLocation, callInfo) + mock.lockGetCodespaceRegionLocation.Unlock() + return mock.GetCodespaceRegionLocationFunc(ctx) +} + +// GetCodespaceRegionLocationCalls gets all the calls that were made to GetCodespaceRegionLocation. +// Check the length with: +// len(mockedapiClient.GetCodespaceRegionLocationCalls()) +func (mock *apiClientMock) GetCodespaceRegionLocationCalls() []struct { + Ctx context.Context +} { + var calls []struct { + Ctx context.Context + } + mock.lockGetCodespaceRegionLocation.RLock() + calls = mock.calls.GetCodespaceRegionLocation + mock.lockGetCodespaceRegionLocation.RUnlock() + return calls +} + +// GetCodespaceRepositoryContents calls GetCodespaceRepositoryContentsFunc. +func (mock *apiClientMock) GetCodespaceRepositoryContents(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error) { + if mock.GetCodespaceRepositoryContentsFunc == nil { + panic("apiClientMock.GetCodespaceRepositoryContentsFunc: method is nil but apiClient.GetCodespaceRepositoryContents was just called") + } + callInfo := struct { + Ctx context.Context + Codespace *api.Codespace + Path string + }{ + Ctx: ctx, + Codespace: codespace, + Path: path, + } + mock.lockGetCodespaceRepositoryContents.Lock() + mock.calls.GetCodespaceRepositoryContents = append(mock.calls.GetCodespaceRepositoryContents, callInfo) + mock.lockGetCodespaceRepositoryContents.Unlock() + return mock.GetCodespaceRepositoryContentsFunc(ctx, codespace, path) +} + +// GetCodespaceRepositoryContentsCalls gets all the calls that were made to GetCodespaceRepositoryContents. +// Check the length with: +// len(mockedapiClient.GetCodespaceRepositoryContentsCalls()) +func (mock *apiClientMock) GetCodespaceRepositoryContentsCalls() []struct { + Ctx context.Context + Codespace *api.Codespace + Path string +} { + var calls []struct { + Ctx context.Context + Codespace *api.Codespace + Path string + } + mock.lockGetCodespaceRepositoryContents.RLock() + calls = mock.calls.GetCodespaceRepositoryContents + mock.lockGetCodespaceRepositoryContents.RUnlock() + return calls +} + +// GetCodespacesMachines calls GetCodespacesMachinesFunc. +func (mock *apiClientMock) GetCodespacesMachines(ctx context.Context, repoID int, branch string, location string) ([]*api.Machine, error) { + if mock.GetCodespacesMachinesFunc == nil { + panic("apiClientMock.GetCodespacesMachinesFunc: method is nil but apiClient.GetCodespacesMachines was just called") + } + callInfo := struct { + Ctx context.Context + RepoID int + Branch string + Location string + }{ + Ctx: ctx, + RepoID: repoID, + Branch: branch, + Location: location, + } + mock.lockGetCodespacesMachines.Lock() + mock.calls.GetCodespacesMachines = append(mock.calls.GetCodespacesMachines, callInfo) + mock.lockGetCodespacesMachines.Unlock() + return mock.GetCodespacesMachinesFunc(ctx, repoID, branch, location) +} + +// GetCodespacesMachinesCalls gets all the calls that were made to GetCodespacesMachines. +// Check the length with: +// len(mockedapiClient.GetCodespacesMachinesCalls()) +func (mock *apiClientMock) GetCodespacesMachinesCalls() []struct { + Ctx context.Context + RepoID int + Branch string + Location string +} { + var calls []struct { + Ctx context.Context + RepoID int + Branch string + Location string + } + mock.lockGetCodespacesMachines.RLock() + calls = mock.calls.GetCodespacesMachines + mock.lockGetCodespacesMachines.RUnlock() + return calls +} + +// GetRepository calls GetRepositoryFunc. +func (mock *apiClientMock) GetRepository(ctx context.Context, nwo string) (*api.Repository, error) { + if mock.GetRepositoryFunc == nil { + panic("apiClientMock.GetRepositoryFunc: method is nil but apiClient.GetRepository was just called") + } + callInfo := struct { + Ctx context.Context + Nwo string + }{ + Ctx: ctx, + Nwo: nwo, + } + mock.lockGetRepository.Lock() + mock.calls.GetRepository = append(mock.calls.GetRepository, callInfo) + mock.lockGetRepository.Unlock() + return mock.GetRepositoryFunc(ctx, nwo) +} + +// GetRepositoryCalls gets all the calls that were made to GetRepository. +// Check the length with: +// len(mockedapiClient.GetRepositoryCalls()) +func (mock *apiClientMock) GetRepositoryCalls() []struct { + Ctx context.Context + Nwo string +} { + var calls []struct { + Ctx context.Context + Nwo string + } + mock.lockGetRepository.RLock() + calls = mock.calls.GetRepository + mock.lockGetRepository.RUnlock() + return calls +} + +// GetUser calls GetUserFunc. +func (mock *apiClientMock) GetUser(ctx context.Context) (*api.User, error) { + if mock.GetUserFunc == nil { + panic("apiClientMock.GetUserFunc: method is nil but apiClient.GetUser was just called") + } + callInfo := struct { + Ctx context.Context + }{ + Ctx: ctx, + } + mock.lockGetUser.Lock() + mock.calls.GetUser = append(mock.calls.GetUser, callInfo) + mock.lockGetUser.Unlock() + return mock.GetUserFunc(ctx) +} + +// GetUserCalls gets all the calls that were made to GetUser. +// Check the length with: +// len(mockedapiClient.GetUserCalls()) +func (mock *apiClientMock) GetUserCalls() []struct { + Ctx context.Context +} { + var calls []struct { + Ctx context.Context + } + mock.lockGetUser.RLock() + calls = mock.calls.GetUser + mock.lockGetUser.RUnlock() + return calls +} + +// ListCodespaces calls ListCodespacesFunc. +func (mock *apiClientMock) ListCodespaces(ctx context.Context, limit int) ([]*api.Codespace, error) { + if mock.ListCodespacesFunc == nil { + panic("apiClientMock.ListCodespacesFunc: method is nil but apiClient.ListCodespaces was just called") + } + callInfo := struct { + Ctx context.Context + Limit int + }{ + Ctx: ctx, + Limit: limit, + } + mock.lockListCodespaces.Lock() + mock.calls.ListCodespaces = append(mock.calls.ListCodespaces, callInfo) + mock.lockListCodespaces.Unlock() + return mock.ListCodespacesFunc(ctx, limit) +} + +// ListCodespacesCalls gets all the calls that were made to ListCodespaces. +// Check the length with: +// len(mockedapiClient.ListCodespacesCalls()) +func (mock *apiClientMock) ListCodespacesCalls() []struct { + Ctx context.Context + Limit int +} { + var calls []struct { + Ctx context.Context + Limit int + } + mock.lockListCodespaces.RLock() + calls = mock.calls.ListCodespaces + mock.lockListCodespaces.RUnlock() + return calls +} + +// StartCodespace calls StartCodespaceFunc. +func (mock *apiClientMock) StartCodespace(ctx context.Context, name string) error { + if mock.StartCodespaceFunc == nil { + panic("apiClientMock.StartCodespaceFunc: method is nil but apiClient.StartCodespace was just called") + } + callInfo := struct { + Ctx context.Context + Name string + }{ + Ctx: ctx, + Name: name, + } + mock.lockStartCodespace.Lock() + mock.calls.StartCodespace = append(mock.calls.StartCodespace, callInfo) + mock.lockStartCodespace.Unlock() + return mock.StartCodespaceFunc(ctx, name) +} + +// StartCodespaceCalls gets all the calls that were made to StartCodespace. +// Check the length with: +// len(mockedapiClient.StartCodespaceCalls()) +func (mock *apiClientMock) StartCodespaceCalls() []struct { + Ctx context.Context + Name string +} { + var calls []struct { + Ctx context.Context + Name string + } + mock.lockStartCodespace.RLock() + calls = mock.calls.StartCodespace + mock.lockStartCodespace.RUnlock() + return calls +} + +// StopCodespace calls StopCodespaceFunc. +func (mock *apiClientMock) StopCodespace(ctx context.Context, name string) error { + if mock.StopCodespaceFunc == nil { + panic("apiClientMock.StopCodespaceFunc: method is nil but apiClient.StopCodespace was just called") + } + callInfo := struct { + Ctx context.Context + Name string + }{ + Ctx: ctx, + Name: name, + } + mock.lockStopCodespace.Lock() + mock.calls.StopCodespace = append(mock.calls.StopCodespace, callInfo) + mock.lockStopCodespace.Unlock() + return mock.StopCodespaceFunc(ctx, name) +} + +// StopCodespaceCalls gets all the calls that were made to StopCodespace. +// Check the length with: +// len(mockedapiClient.StopCodespaceCalls()) +func (mock *apiClientMock) StopCodespaceCalls() []struct { + Ctx context.Context + Name string +} { + var calls []struct { + Ctx context.Context + Name string + } + mock.lockStopCodespace.RLock() + calls = mock.calls.StopCodespace + mock.lockStopCodespace.RUnlock() + return calls +} diff --git a/pkg/cmd/codespace/mock_prompter.go b/pkg/cmd/codespace/mock_prompter.go new file mode 100644 index 00000000000..3ce257a393f --- /dev/null +++ b/pkg/cmd/codespace/mock_prompter.go @@ -0,0 +1,69 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package codespace + +import ( + "sync" +) + +// prompterMock is a mock implementation of prompter. +// +// func TestSomethingThatUsesprompter(t *testing.T) { +// +// // make and configure a mocked prompter +// mockedprompter := &prompterMock{ +// ConfirmFunc: func(message string) (bool, error) { +// panic("mock out the Confirm method") +// }, +// } +// +// // use mockedprompter in code that requires prompter +// // and then make assertions. +// +// } +type prompterMock struct { + // ConfirmFunc mocks the Confirm method. + ConfirmFunc func(message string) (bool, error) + + // calls tracks calls to the methods. + calls struct { + // Confirm holds details about calls to the Confirm method. + Confirm []struct { + // Message is the message argument value. + Message string + } + } + lockConfirm sync.RWMutex +} + +// Confirm calls ConfirmFunc. +func (mock *prompterMock) Confirm(message string) (bool, error) { + if mock.ConfirmFunc == nil { + panic("prompterMock.ConfirmFunc: method is nil but prompter.Confirm was just called") + } + callInfo := struct { + Message string + }{ + Message: message, + } + mock.lockConfirm.Lock() + mock.calls.Confirm = append(mock.calls.Confirm, callInfo) + mock.lockConfirm.Unlock() + return mock.ConfirmFunc(message) +} + +// ConfirmCalls gets all the calls that were made to Confirm. +// Check the length with: +// len(mockedprompter.ConfirmCalls()) +func (mock *prompterMock) ConfirmCalls() []struct { + Message string +} { + var calls []struct { + Message string + } + mock.lockConfirm.RLock() + calls = mock.calls.Confirm + mock.lockConfirm.RUnlock() + return calls +} diff --git a/pkg/cmd/codespace/output/format_json.go b/pkg/cmd/codespace/output/format_json.go new file mode 100644 index 00000000000..8488e8dfa07 --- /dev/null +++ b/pkg/cmd/codespace/output/format_json.go @@ -0,0 +1,55 @@ +package output + +import ( + "encoding/json" + "io" + "strings" + "unicode" +) + +type jsonwriter struct { + w io.Writer + pretty bool + cols []string + data []interface{} +} + +func (j *jsonwriter) SetHeader(cols []string) { + j.cols = cols +} + +func (j *jsonwriter) Append(values []string) { + row := make(map[string]string) + for i, v := range values { + row[camelize(j.cols[i])] = v + } + j.data = append(j.data, row) +} + +func (j *jsonwriter) Render() { + enc := json.NewEncoder(j.w) + if j.pretty { + enc.SetIndent("", " ") + } + _ = enc.Encode(j.data) +} + +func camelize(s string) string { + var b strings.Builder + capitalizeNext := false + for i, r := range s { + if r == ' ' { + capitalizeNext = true + continue + } + if capitalizeNext { + b.WriteRune(unicode.ToUpper(r)) + capitalizeNext = false + } else if i == 0 { + b.WriteRune(unicode.ToLower(r)) + } else { + b.WriteRune(r) + } + } + return b.String() +} diff --git a/pkg/cmd/codespace/output/format_table.go b/pkg/cmd/codespace/output/format_table.go new file mode 100644 index 00000000000..e0345672d67 --- /dev/null +++ b/pkg/cmd/codespace/output/format_table.go @@ -0,0 +1,31 @@ +package output + +import ( + "io" + "os" + + "github.com/olekukonko/tablewriter" + "golang.org/x/term" +) + +type Table interface { + SetHeader([]string) + Append([]string) + Render() +} + +func NewTable(w io.Writer, asJSON bool) Table { + isTTY := isTTY(w) + if asJSON { + return &jsonwriter{w: w, pretty: isTTY} + } + if isTTY { + return tablewriter.NewWriter(w) + } + return &tabwriter{w: w} +} + +func isTTY(w io.Writer) bool { + f, ok := w.(*os.File) + return ok && term.IsTerminal(int(f.Fd())) +} diff --git a/pkg/cmd/codespace/output/format_tsv.go b/pkg/cmd/codespace/output/format_tsv.go new file mode 100644 index 00000000000..3f1d226ca16 --- /dev/null +++ b/pkg/cmd/codespace/output/format_tsv.go @@ -0,0 +1,25 @@ +package output + +import ( + "fmt" + "io" +) + +type tabwriter struct { + w io.Writer +} + +func (j *tabwriter) SetHeader([]string) {} + +func (j *tabwriter) Append(values []string) { + var sep string + for i, v := range values { + if i == 1 { + sep = "\t" + } + fmt.Fprintf(j.w, "%s%s", sep, v) + } + fmt.Fprint(j.w, "\n") +} + +func (j *tabwriter) Render() {} diff --git a/pkg/cmd/codespace/output/logger.go b/pkg/cmd/codespace/output/logger.go new file mode 100644 index 00000000000..fdefcad0f67 --- /dev/null +++ b/pkg/cmd/codespace/output/logger.go @@ -0,0 +1,78 @@ +package output + +import ( + "fmt" + "io" + "sync" +) + +// NewLogger returns a Logger that will write to the given stdout/stderr writers. +// Disable the Logger to prevent it from writing to stdout in a TTY environment. +func NewLogger(stdout, stderr io.Writer, disabled bool) *Logger { + enabled := !disabled + if isTTY(stdout) && !enabled { + enabled = false + } + return &Logger{ + out: stdout, + errout: stderr, + enabled: enabled, + } +} + +// Logger writes to the given stdout/stderr writers. +// If not enabled, Print functions will noop but Error functions will continue +// to write to the stderr writer. +type Logger struct { + mu sync.Mutex // guards the writers + out io.Writer + errout io.Writer + enabled bool +} + +// Print writes the arguments to the stdout writer. +func (l *Logger) Print(v ...interface{}) (int, error) { + if !l.enabled { + return 0, nil + } + + l.mu.Lock() + defer l.mu.Unlock() + return fmt.Fprint(l.out, v...) +} + +// Println writes the arguments to the stdout writer with a newline at the end. +func (l *Logger) Println(v ...interface{}) (int, error) { + if !l.enabled { + return 0, nil + } + + l.mu.Lock() + defer l.mu.Unlock() + return fmt.Fprintln(l.out, v...) +} + +// Printf writes the formatted arguments to the stdout writer. +func (l *Logger) Printf(f string, v ...interface{}) (int, error) { + if !l.enabled { + return 0, nil + } + + l.mu.Lock() + defer l.mu.Unlock() + return fmt.Fprintf(l.out, f, v...) +} + +// Errorf writes the formatted arguments to the stderr writer. +func (l *Logger) Errorf(f string, v ...interface{}) (int, error) { + l.mu.Lock() + defer l.mu.Unlock() + return fmt.Fprintf(l.errout, f, v...) +} + +// Errorln writes the arguments to the stderr writer with a newline at the end. +func (l *Logger) Errorln(v ...interface{}) (int, error) { + l.mu.Lock() + defer l.mu.Unlock() + return fmt.Fprintln(l.errout, v...) +} diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go new file mode 100644 index 00000000000..a563ec87e2e --- /dev/null +++ b/pkg/cmd/codespace/ports.go @@ -0,0 +1,312 @@ +package codespace + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net" + "os" + "strconv" + "strings" + + "github.com/cli/cli/v2/internal/codespaces" + "github.com/cli/cli/v2/internal/codespaces/api" + "github.com/cli/cli/v2/pkg/cmd/codespace/output" + "github.com/cli/cli/v2/pkg/liveshare" + "github.com/muhammadmuzzammil1998/jsonc" + "github.com/spf13/cobra" + "golang.org/x/sync/errgroup" +) + +// newPortsCmd returns a Cobra "ports" command that displays a table of available ports, +// according to the specified flags. +func newPortsCmd(app *App) *cobra.Command { + var ( + codespace string + asJSON bool + ) + + portsCmd := &cobra.Command{ + Use: "ports", + Short: "List ports in a codespace", + Args: noArgsConstraint, + RunE: func(cmd *cobra.Command, args []string) error { + return app.ListPorts(cmd.Context(), codespace, asJSON) + }, + } + + portsCmd.PersistentFlags().StringVarP(&codespace, "codespace", "c", "", "Name of the codespace") + portsCmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON") + + portsCmd.AddCommand(newPortsForwardCmd(app)) + portsCmd.AddCommand(newPortsVisibilityCmd(app)) + + return portsCmd +} + +// ListPorts lists known ports in a codespace. +func (a *App) ListPorts(ctx context.Context, codespaceName string, asJSON bool) (err error) { + codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) + if err != nil { + // TODO(josebalius): remove special handling of this error here and it other places + if err == errNoCodespaces { + return err + } + return fmt.Errorf("error choosing codespace: %w", err) + } + + devContainerCh := getDevContainer(ctx, a.apiClient, codespace) + + session, err := codespaces.ConnectToLiveshare(ctx, a.logger, noopLogger(), a.apiClient, codespace) + if err != nil { + return fmt.Errorf("error connecting to Live Share: %w", err) + } + defer safeClose(session, &err) + + a.logger.Println("Loading ports...") + ports, err := session.GetSharedServers(ctx) + if err != nil { + return fmt.Errorf("error getting ports of shared servers: %w", err) + } + + devContainerResult := <-devContainerCh + if devContainerResult.err != nil { + // Warn about failure to read the devcontainer file. Not a codespace command error. + _, _ = a.logger.Errorf("Failed to get port names: %v\n", devContainerResult.err.Error()) + } + + table := output.NewTable(os.Stdout, asJSON) + table.SetHeader([]string{"Label", "Port", "Visibility", "Browse URL"}) + for _, port := range ports { + sourcePort := strconv.Itoa(port.SourcePort) + var portName string + if devContainerResult.devContainer != nil { + if attributes, ok := devContainerResult.devContainer.PortAttributes[sourcePort]; ok { + portName = attributes.Label + } + } + + table.Append([]string{ + portName, + sourcePort, + port.Privacy, + fmt.Sprintf("https://%s-%s.githubpreview.dev/", codespace.Name, sourcePort), + }) + } + table.Render() + + return nil +} + +type devContainerResult struct { + devContainer *devContainer + err error +} + +type devContainer struct { + PortAttributes map[string]portAttribute `json:"portsAttributes"` +} + +type portAttribute struct { + Label string `json:"label"` +} + +func getDevContainer(ctx context.Context, apiClient apiClient, codespace *api.Codespace) <-chan devContainerResult { + ch := make(chan devContainerResult, 1) + go func() { + contents, err := apiClient.GetCodespaceRepositoryContents(ctx, codespace, ".devcontainer/devcontainer.json") + if err != nil { + ch <- devContainerResult{nil, fmt.Errorf("error getting content: %w", err)} + return + } + + if contents == nil { + ch <- devContainerResult{nil, nil} + return + } + + convertedJSON := normalizeJSON(jsonc.ToJSON(contents)) + if !jsonc.Valid(convertedJSON) { + ch <- devContainerResult{nil, errors.New("failed to convert json to standard json")} + return + } + + var container devContainer + if err := json.Unmarshal(convertedJSON, &container); err != nil { + ch <- devContainerResult{nil, fmt.Errorf("error unmarshaling: %w", err)} + return + } + + ch <- devContainerResult{&container, nil} + }() + return ch +} + +func newPortsVisibilityCmd(app *App) *cobra.Command { + return &cobra.Command{ + Use: "visibility :{public|private|org}...", + Short: "Change the visibility of the forwarded port", + Example: "gh codespace ports visibility 80:org 3000:private 8000:public", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + codespace, err := cmd.Flags().GetString("codespace") + if err != nil { + // should only happen if flag is not defined + // or if the flag is not of string type + // since it's a persistent flag that we control it should never happen + return fmt.Errorf("get codespace flag: %w", err) + } + return app.UpdatePortVisibility(cmd.Context(), codespace, args) + }, + } +} + +func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, args []string) (err error) { + ports, err := a.parsePortVisibilities(args) + if err != nil { + return fmt.Errorf("error parsing port arguments: %w", err) + } + codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) + if err != nil { + if err == errNoCodespaces { + return err + } + return fmt.Errorf("error getting codespace: %w", err) + } + + session, err := codespaces.ConnectToLiveshare(ctx, a.logger, noopLogger(), a.apiClient, codespace) + if err != nil { + return fmt.Errorf("error connecting to Live Share: %w", err) + } + defer safeClose(session, &err) + + for _, port := range ports { + if err := session.UpdateSharedServerPrivacy(ctx, port.number, port.visibility); err != nil { + return fmt.Errorf("error update port to public: %w", err) + } + + a.logger.Printf("Port %d is now %s scoped.\n", port.number, port.visibility) + } + + return nil +} + +type portVisibility struct { + number int + visibility string +} + +func (a *App) parsePortVisibilities(args []string) ([]portVisibility, error) { + ports := make([]portVisibility, 0, len(args)) + for _, a := range args { + fields := strings.Split(a, ":") + if len(fields) != 2 { + return nil, fmt.Errorf("invalid port visibility format for %q", a) + } + portStr, visibility := fields[0], fields[1] + portNumber, err := strconv.Atoi(portStr) + if err != nil { + return nil, fmt.Errorf("invalid port number: %w", err) + } + ports = append(ports, portVisibility{portNumber, visibility}) + } + return ports, nil +} + +// NewPortsForwardCmd returns a Cobra "ports forward" subcommand, which forwards a set of +// port pairs from the codespace to localhost. +func newPortsForwardCmd(app *App) *cobra.Command { + return &cobra.Command{ + Use: "forward :...", + Short: "Forward ports", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + codespace, err := cmd.Flags().GetString("codespace") + if err != nil { + // should only happen if flag is not defined + // or if the flag is not of string type + // since it's a persistent flag that we control it should never happen + return fmt.Errorf("get codespace flag: %w", err) + } + + return app.ForwardPorts(cmd.Context(), codespace, args) + }, + } +} + +func (a *App) ForwardPorts(ctx context.Context, codespaceName string, ports []string) (err error) { + portPairs, err := getPortPairs(ports) + if err != nil { + return fmt.Errorf("get port pairs: %w", err) + } + + codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) + if err != nil { + if err == errNoCodespaces { + return err + } + return fmt.Errorf("error getting codespace: %w", err) + } + + session, err := codespaces.ConnectToLiveshare(ctx, a.logger, noopLogger(), a.apiClient, codespace) + if err != nil { + return fmt.Errorf("error connecting to Live Share: %w", err) + } + defer safeClose(session, &err) + + // Run forwarding of all ports concurrently, aborting all of + // them at the first failure, including cancellation of the context. + group, ctx := errgroup.WithContext(ctx) + for _, pair := range portPairs { + pair := pair + group.Go(func() error { + listen, err := net.Listen("tcp", fmt.Sprintf(":%d", pair.local)) + if err != nil { + return err + } + defer listen.Close() + a.logger.Printf("Forwarding ports: remote %d <=> local %d\n", pair.remote, pair.local) + name := fmt.Sprintf("share-%d", pair.remote) + fwd := liveshare.NewPortForwarder(session, name, pair.remote, false) + return fwd.ForwardToListener(ctx, listen) // error always non-nil + }) + } + return group.Wait() // first error +} + +type portPair struct { + remote, local int +} + +// getPortPairs parses a list of strings of form "%d:%d" into pairs of (remote, local) numbers. +func getPortPairs(ports []string) ([]portPair, error) { + pp := make([]portPair, 0, len(ports)) + + for _, portString := range ports { + parts := strings.Split(portString, ":") + if len(parts) < 2 { + return nil, fmt.Errorf("port pair: %q is not valid", portString) + } + + remote, err := strconv.Atoi(parts[0]) + if err != nil { + return pp, fmt.Errorf("convert remote port to int: %w", err) + } + + local, err := strconv.Atoi(parts[1]) + if err != nil { + return pp, fmt.Errorf("convert local port to int: %w", err) + } + + pp = append(pp, portPair{remote, local}) + } + + return pp, nil +} + +func normalizeJSON(j []byte) []byte { + // remove trailing commas + return bytes.ReplaceAll(j, []byte("},}"), []byte("}}")) +} diff --git a/pkg/cmd/codespace/root.go b/pkg/cmd/codespace/root.go new file mode 100644 index 00000000000..efc1c763ed7 --- /dev/null +++ b/pkg/cmd/codespace/root.go @@ -0,0 +1,31 @@ +package codespace + +import ( + "github.com/spf13/cobra" +) + +var version = "DEV" // Replaced in the release build process (by GoReleaser or Homebrew) by the git tag version number. + +func NewRootCmd(app *App) *cobra.Command { + root := &cobra.Command{ + Use: "codespace", + SilenceUsage: true, // don't print usage message after each error (see #80) + SilenceErrors: false, // print errors automatically so that main need not + Long: `Unofficial CLI tool to manage GitHub Codespaces. + +Running commands requires the GITHUB_TOKEN environment variable to be set to a +token to access the GitHub API with.`, + Version: version, + } + + root.AddCommand(newCodeCmd(app)) + root.AddCommand(newCreateCmd(app)) + root.AddCommand(newDeleteCmd(app)) + root.AddCommand(newListCmd(app)) + root.AddCommand(newLogsCmd(app)) + root.AddCommand(newPortsCmd(app)) + root.AddCommand(newSSHCmd(app)) + root.AddCommand(newStopCmd(app)) + + return root +} diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go new file mode 100644 index 00000000000..218494a1366 --- /dev/null +++ b/pkg/cmd/codespace/ssh.go @@ -0,0 +1,172 @@ +package codespace + +import ( + "context" + "fmt" + "io/ioutil" + "log" + "net" + "os" + + "github.com/cli/cli/v2/internal/codespaces" + "github.com/cli/cli/v2/pkg/liveshare" + "github.com/spf13/cobra" +) + +type sshOptions struct { + codespace string + profile string + serverPort int + debug bool + debugFile string +} + +func newSSHCmd(app *App) *cobra.Command { + var opts sshOptions + + sshCmd := &cobra.Command{ + Use: "ssh [flags] [--] [ssh-flags] [command]", + Short: "SSH into a codespace", + RunE: func(cmd *cobra.Command, args []string) error { + return app.SSH(cmd.Context(), args, opts) + }, + } + + sshCmd.Flags().StringVarP(&opts.profile, "profile", "", "", "Name of the SSH profile to use") + sshCmd.Flags().IntVarP(&opts.serverPort, "server-port", "", 0, "SSH server port number (0 => pick unused)") + sshCmd.Flags().StringVarP(&opts.codespace, "codespace", "c", "", "Name of the codespace") + sshCmd.Flags().BoolVarP(&opts.debug, "debug", "d", false, "Log debug data to a file") + sshCmd.Flags().StringVarP(&opts.debugFile, "debug-file", "", "", "Path of the file log to") + + return sshCmd +} + +// SSH opens an ssh session or runs an ssh command in a codespace. +func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err error) { + // Ensure all child tasks (e.g. port forwarding) terminate before return. + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + user, err := a.apiClient.GetUser(ctx) + if err != nil { + return fmt.Errorf("error getting user: %w", err) + } + + authkeys := make(chan error, 1) + go func() { + authkeys <- checkAuthorizedKeys(ctx, a.apiClient, user.Login) + }() + + codespace, err := getOrChooseCodespace(ctx, a.apiClient, opts.codespace) + if err != nil { + return fmt.Errorf("get or choose codespace: %w", err) + } + + liveshareLogger := noopLogger() + if opts.debug { + debugLogger, err := newFileLogger(opts.debugFile) + if err != nil { + return fmt.Errorf("error creating debug logger: %w", err) + } + defer safeClose(debugLogger, &err) + + liveshareLogger = debugLogger.Logger + a.logger.Println("Debug file located at: " + debugLogger.Name()) + } + + session, err := codespaces.ConnectToLiveshare(ctx, a.logger, liveshareLogger, a.apiClient, codespace) + if err != nil { + return fmt.Errorf("error connecting to Live Share: %w", err) + } + defer safeClose(session, &err) + + if err := <-authkeys; err != nil { + return err + } + + a.logger.Println("Fetching SSH Details...") + remoteSSHServerPort, sshUser, err := session.StartSSHServer(ctx) + if err != nil { + return fmt.Errorf("error getting ssh server details: %w", err) + } + + localSSHServerPort := opts.serverPort + usingCustomPort := localSSHServerPort != 0 // suppress log of command line in Shell + + // Ensure local port is listening before client (Shell) connects. + // Unless the user specifies a server port, localSSHServerPort is 0 + // and thus the client will pick a random port. + listen, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", localSSHServerPort)) + if err != nil { + return err + } + defer listen.Close() + localSSHServerPort = listen.Addr().(*net.TCPAddr).Port + + connectDestination := opts.profile + if connectDestination == "" { + connectDestination = fmt.Sprintf("%s@localhost", sshUser) + } + + a.logger.Println("Ready...") + tunnelClosed := make(chan error, 1) + go func() { + fwd := liveshare.NewPortForwarder(session, "sshd", remoteSSHServerPort, true) + tunnelClosed <- fwd.ForwardToListener(ctx, listen) // always non-nil + }() + + shellClosed := make(chan error, 1) + go func() { + shellClosed <- codespaces.Shell(ctx, a.logger, sshArgs, localSSHServerPort, connectDestination, usingCustomPort) + }() + + select { + case err := <-tunnelClosed: + return fmt.Errorf("tunnel closed: %w", err) + case err := <-shellClosed: + if err != nil { + return fmt.Errorf("shell closed: %w", err) + } + return nil // success + } +} + +// fileLogger is a wrapper around an log.Logger configured to write +// to a file. It exports two additional methods to get the log file name +// and close the file handle when the operation is finished. +type fileLogger struct { + *log.Logger + + f *os.File +} + +// newFileLogger creates a new fileLogger. It returns an error if the file +// cannot be created. The file is created on the specified path, if the path +// is empty it is created in the temporary directory. +func newFileLogger(file string) (fl *fileLogger, err error) { + var f *os.File + if file == "" { + f, err = ioutil.TempFile("", "") + if err != nil { + return nil, fmt.Errorf("failed to create tmp file: %w", err) + } + } else { + f, err = os.Create(file) + if err != nil { + return nil, err + } + } + + return &fileLogger{ + Logger: log.New(f, "", log.LstdFlags), + f: f, + }, nil +} + +func (fl *fileLogger) Name() string { + return fl.f.Name() +} + +func (fl *fileLogger) Close() error { + return fl.f.Close() +} diff --git a/pkg/cmd/codespace/stop.go b/pkg/cmd/codespace/stop.go new file mode 100644 index 00000000000..be439b1e86a --- /dev/null +++ b/pkg/cmd/codespace/stop.go @@ -0,0 +1,68 @@ +package codespace + +import ( + "context" + "errors" + "fmt" + + "github.com/cli/cli/v2/internal/codespaces/api" + "github.com/spf13/cobra" +) + +func newStopCmd(app *App) *cobra.Command { + var codespace string + + stopCmd := &cobra.Command{ + Use: "stop", + Short: "Stop a running codespace", + Args: noArgsConstraint, + RunE: func(cmd *cobra.Command, args []string) error { + return app.StopCodespace(cmd.Context(), codespace) + }, + } + stopCmd.Flags().StringVarP(&codespace, "codespace", "c", "", "Name of the codespace") + + return stopCmd +} + +func (a *App) StopCodespace(ctx context.Context, codespaceName string) error { + if codespaceName == "" { + codespaces, err := a.apiClient.ListCodespaces(ctx, -1) + if err != nil { + return fmt.Errorf("failed to list codespaces: %w", err) + } + + var runningCodespaces []*api.Codespace + for _, c := range codespaces { + cs := codespace{c} + if cs.running() { + runningCodespaces = append(runningCodespaces, c) + } + } + if len(runningCodespaces) == 0 { + return errors.New("no running codespaces") + } + + codespace, err := chooseCodespaceFromList(ctx, runningCodespaces) + if err != nil { + return fmt.Errorf("failed to choose codespace: %w", err) + } + codespaceName = codespace.Name + } else { + c, err := a.apiClient.GetCodespace(ctx, codespaceName, false) + if err != nil { + return fmt.Errorf("failed to get codespace: %q: %w", codespaceName, err) + } + cs := codespace{c} + if !cs.running() { + return fmt.Errorf("codespace %q is not running", codespaceName) + } + } + + if err := a.apiClient.StopCodespace(ctx, codespaceName); err != nil { + return fmt.Errorf("failed to stop codespace: %w", err) + } + a.logger.Println("Codespace stopped") + + return nil +} diff --git a/pkg/cmd/completion/completion.go b/pkg/cmd/completion/completion.go index 3d3699b5d9b..e711d6db67a 100644 --- a/pkg/cmd/completion/completion.go +++ b/pkg/cmd/completion/completion.go @@ -5,8 +5,8 @@ import ( "fmt" "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" ) @@ -14,22 +14,57 @@ func NewCmdCompletion(io *iostreams.IOStreams) *cobra.Command { var shellType string cmd := &cobra.Command{ - Use: "completion", + Use: "completion -s ", Short: "Generate shell completion scripts", - Long: heredoc.Doc(` + Long: heredoc.Docf(` Generate shell completion scripts for GitHub CLI commands. - The output of this command will be computer code and is meant to be saved to a - file or immediately evaluated by an interactive shell. + When installing GitHub CLI through a package manager, it's possible that + no additional shell configuration is necessary to gain completion support. For + Homebrew, see + + If you need to set up completions manually, follow the instructions below. The exact + config file locations might vary based on your system. Make sure to restart your + shell before testing whether completions are working. + + ### bash + + First, ensure that you install %[1]sbash-completion%[1]s using your package manager. - For example, for bash you could add this to your '~/.bash_profile': + After, add this to your %[1]s~/.bash_profile%[1]s: eval "$(gh completion -s bash)" + + ### zsh - When installing GitHub CLI through a package manager, however, it's possible that - no additional shell configuration is necessary to gain completion support. For - Homebrew, see https://docs.brew.sh/Shell-Completion - `), + Generate a %[1]s_gh%[1]s completion script and put it somewhere in your %[1]s$fpath%[1]s: + + gh completion -s zsh > /usr/local/share/zsh/site-functions/_gh + + Ensure that the following is present in your %[1]s~/.zshrc%[1]s: + + autoload -U compinit + compinit -i + + Zsh version 5.7 or later is recommended. + + ### fish + + Generate a %[1]sgh.fish%[1]s completion script: + + gh completion -s fish > ~/.config/fish/completions/gh.fish + + ### PowerShell + + Open your profile script with: + + mkdir -Path (Split-Path -Parent $profile) -ErrorAction SilentlyContinue + notepad $profile + + Add the line and save the file: + + Invoke-Expression -Command $(gh completion -s powershell | Out-String) + `, "`"), RunE: func(cmd *cobra.Command, args []string) error { if shellType == "" { if io.IsStdoutTTY() { @@ -43,17 +78,18 @@ func NewCmdCompletion(io *iostreams.IOStreams) *cobra.Command { switch shellType { case "bash": - return rootCmd.GenBashCompletion(w) + return rootCmd.GenBashCompletionV2(w, true) case "zsh": return rootCmd.GenZshCompletion(w) case "powershell": - return rootCmd.GenPowerShellCompletion(w) + return rootCmd.GenPowerShellCompletionWithDesc(w) case "fish": return rootCmd.GenFishCompletion(w, true) default: return fmt.Errorf("unsupported shell type %q", shellType) } }, + DisableFlagsInUseLine: true, } cmdutil.DisableAuthCheck(cmd) diff --git a/pkg/cmd/completion/completion_test.go b/pkg/cmd/completion/completion_test.go index 3ce03f9b60d..06b3d2f27d9 100644 --- a/pkg/cmd/completion/completion_test.go +++ b/pkg/cmd/completion/completion_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/v2/pkg/iostreams" "github.com/google/shlex" "github.com/spf13/cobra" ) diff --git a/pkg/cmd/config/config.go b/pkg/cmd/config/config.go index 7cecb66cb0f..c011ddcf042 100644 --- a/pkg/cmd/config/config.go +++ b/pkg/cmd/config/config.go @@ -4,10 +4,10 @@ import ( "fmt" "strings" - "github.com/cli/cli/internal/config" - cmdGet "github.com/cli/cli/pkg/cmd/config/get" - cmdSet "github.com/cli/cli/pkg/cmd/config/set" - "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/v2/internal/config" + cmdGet "github.com/cli/cli/v2/pkg/cmd/config/get" + cmdSet "github.com/cli/cli/v2/pkg/cmd/config/set" + "github.com/cli/cli/v2/pkg/cmdutil" "github.com/spf13/cobra" ) @@ -24,7 +24,7 @@ func NewCmdConfig(f *cmdutil.Factory) *cobra.Command { } cmd := &cobra.Command{ - Use: "config", + Use: "config ", Short: "Manage configuration for gh", Long: longDoc.String(), } diff --git a/pkg/cmd/config/get/get.go b/pkg/cmd/config/get/get.go index a3ce5517621..3a563445835 100644 --- a/pkg/cmd/config/get/get.go +++ b/pkg/cmd/config/get/get.go @@ -4,9 +4,9 @@ import ( "fmt" "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" ) diff --git a/pkg/cmd/config/get/get_test.go b/pkg/cmd/config/get/get_test.go index 1e2104dbda4..7c5efa9be93 100644 --- a/pkg/cmd/config/get/get_test.go +++ b/pkg/cmd/config/get/get_test.go @@ -4,9 +4,9 @@ import ( "bytes" "testing" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" "github.com/google/shlex" "github.com/stretchr/testify/assert" ) diff --git a/pkg/cmd/config/set/set.go b/pkg/cmd/config/set/set.go index d1a2d5c59a1..38a23c899bd 100644 --- a/pkg/cmd/config/set/set.go +++ b/pkg/cmd/config/set/set.go @@ -6,9 +6,9 @@ import ( "strings" "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" ) diff --git a/pkg/cmd/config/set/set_test.go b/pkg/cmd/config/set/set_test.go index 56efc2ebd0a..cdd2e7c94d8 100644 --- a/pkg/cmd/config/set/set_test.go +++ b/pkg/cmd/config/set/set_test.go @@ -4,9 +4,9 @@ import ( "bytes" "testing" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" "github.com/google/shlex" "github.com/stretchr/testify/assert" ) diff --git a/pkg/cmd/extension/command.go b/pkg/cmd/extension/command.go new file mode 100644 index 00000000000..7b51e1498d7 --- /dev/null +++ b/pkg/cmd/extension/command.go @@ -0,0 +1,241 @@ +package extension + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/extensions" + "github.com/cli/cli/v2/utils" + "github.com/spf13/cobra" +) + +func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { + m := f.ExtensionManager + io := f.IOStreams + + extCmd := cobra.Command{ + Use: "extension", + Short: "Manage gh extensions", + Long: heredoc.Docf(` + GitHub CLI extensions are repositories that provide additional gh commands. + + The name of the extension repository must start with "gh-" and it must contain an + executable of the same name. All arguments passed to the %[1]sgh %[1]s invocation + will be forwarded to the %[1]sgh-%[1]s executable of the extension. + + An extension cannot override any of the core gh commands. + + See the list of available extensions at + `, "`"), + Aliases: []string{"extensions"}, + } + + extCmd.AddCommand( + &cobra.Command{ + Use: "list", + Short: "List installed extension commands", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + cmds := m.List(true) + if len(cmds) == 0 { + return errors.New("no extensions installed") + } + cs := io.ColorScheme() + t := utils.NewTablePrinter(io) + for _, c := range cmds { + var repo string + if u, err := git.ParseURL(c.URL()); err == nil { + if r, err := ghrepo.FromURL(u); err == nil { + repo = ghrepo.FullName(r) + } + } + + t.AddField(fmt.Sprintf("gh %s", c.Name()), nil, nil) + t.AddField(repo, nil, nil) + var updateAvailable string + if c.UpdateAvailable() { + updateAvailable = "Upgrade available" + } + t.AddField(updateAvailable, nil, cs.Green) + t.EndRow() + } + return t.Render() + }, + }, + &cobra.Command{ + Use: "install ", + Short: "Install a gh extension from a repository", + Long: heredoc.Doc(` + Install a GitHub repository locally as a GitHub CLI extension. + + The repository argument can be specified in "owner/repo" format as well as a full URL. + The URL format is useful when the repository is not hosted on github.com. + + To install an extension in development from the current directory, use "." as the + value of the repository argument. + + See the list of available extensions at + `), + Example: heredoc.Doc(` + $ gh extension install owner/gh-extension + $ gh extension install https://git.example.com/owner/gh-extension + $ gh extension install . + `), + Args: cmdutil.MinimumArgs(1, "must specify a repository to install from"), + RunE: func(cmd *cobra.Command, args []string) error { + if args[0] == "." { + wd, err := os.Getwd() + if err != nil { + return err + } + return m.InstallLocal(wd) + } + + repo, err := ghrepo.FromFullName(args[0]) + if err != nil { + return err + } + + if err := checkValidExtension(cmd.Root(), m, repo.RepoName()); err != nil { + return err + } + + return m.Install(repo) + }, + }, + func() *cobra.Command { + var flagAll bool + var flagForce bool + cmd := &cobra.Command{ + Use: "upgrade { | --all}", + Short: "Upgrade installed extensions", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 && !flagAll { + return &cmdutil.FlagError{Err: errors.New("must specify an extension to upgrade")} + } + if len(args) > 0 && flagAll { + return &cmdutil.FlagError{Err: errors.New("cannot use `--all` with extension name")} + } + if len(args) > 1 { + return &cmdutil.FlagError{Err: errors.New("too many arguments")} + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + var name string + if len(args) > 0 { + name = normalizeExtensionSelector(args[0]) + } + cs := io.ColorScheme() + err := m.Upgrade(name, flagForce) + if err != nil { + if name != "" { + fmt.Fprintf(io.ErrOut, "%s Failed upgrading extension %s: %s", cs.FailureIcon(), name, err) + } else { + fmt.Fprintf(io.ErrOut, "%s Failed upgrading extensions", cs.FailureIcon()) + } + return cmdutil.SilentError + } + if io.IsStdoutTTY() { + if name != "" { + fmt.Fprintf(io.Out, "%s Successfully upgraded extension %s\n", cs.SuccessIcon(), name) + } else { + fmt.Fprintf(io.Out, "%s Successfully upgraded extensions\n", cs.SuccessIcon()) + } + } + return nil + }, + } + cmd.Flags().BoolVar(&flagAll, "all", false, "Upgrade all extensions") + cmd.Flags().BoolVar(&flagForce, "force", false, "Force upgrade extension") + return cmd + }(), + &cobra.Command{ + Use: "remove ", + Short: "Remove an installed extension", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + extName := normalizeExtensionSelector(args[0]) + if err := m.Remove(extName); err != nil { + return err + } + if io.IsStdoutTTY() { + cs := io.ColorScheme() + fmt.Fprintf(io.Out, "%s Removed extension %s\n", cs.SuccessIcon(), extName) + } + return nil + }, + }, + &cobra.Command{ + Use: "create ", + Short: "Create a new extension", + Args: cmdutil.ExactArgs(1, "must specify a name for the extension"), + RunE: func(cmd *cobra.Command, args []string) error { + extName := args[0] + if !strings.HasPrefix(extName, "gh-") { + extName = "gh-" + extName + } + if err := m.Create(extName); err != nil { + return err + } + if !io.IsStdoutTTY() { + return nil + } + link := "https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions" + cs := io.ColorScheme() + out := heredoc.Docf(` + %[1]s Created directory %[2]s + %[1]s Initialized git repository + %[1]s Set up extension scaffolding + + %[2]s is ready for development + + Install locally with: cd %[2]s && gh extension install . + + Publish to GitHub with: gh repo create %[2]s + + For more information on writing extensions: + %[3]s + `, cs.SuccessIcon(), extName, link) + fmt.Fprint(io.Out, out) + return nil + }, + }, + ) + + return &extCmd +} + +func checkValidExtension(rootCmd *cobra.Command, m extensions.ExtensionManager, extName string) error { + if !strings.HasPrefix(extName, "gh-") { + return errors.New("extension repository name must start with `gh-`") + } + + commandName := strings.TrimPrefix(extName, "gh-") + if c, _, err := rootCmd.Traverse([]string{commandName}); err != nil { + return err + } else if c != rootCmd { + return fmt.Errorf("%q matches the name of a built-in command", commandName) + } + + for _, ext := range m.List(false) { + if ext.Name() == commandName { + return fmt.Errorf("there is already an installed extension that provides the %q command", commandName) + } + } + + return nil +} + +func normalizeExtensionSelector(n string) string { + if idx := strings.IndexRune(n, '/'); idx >= 0 { + n = n[idx+1:] + } + return strings.TrimPrefix(n, "gh-") +} diff --git a/pkg/cmd/extension/command_test.go b/pkg/cmd/extension/command_test.go new file mode 100644 index 00000000000..839484c3e66 --- /dev/null +++ b/pkg/cmd/extension/command_test.go @@ -0,0 +1,439 @@ +package extension + +import ( + "io/ioutil" + "net/http" + "os" + "strings" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/extensions" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestNewCmdExtension(t *testing.T) { + tempDir := t.TempDir() + oldWd, _ := os.Getwd() + assert.NoError(t, os.Chdir(tempDir)) + t.Cleanup(func() { _ = os.Chdir(oldWd) }) + + tests := []struct { + name string + args []string + managerStubs func(em *extensions.ExtensionManagerMock) func(*testing.T) + isTTY bool + wantErr bool + errMsg string + wantStdout string + wantStderr string + }{ + { + name: "install an extension", + args: []string{"install", "owner/gh-some-ext"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.ListFunc = func(bool) []extensions.Extension { + return []extensions.Extension{} + } + em.InstallFunc = func(_ ghrepo.Interface) error { + return nil + } + return func(t *testing.T) { + installCalls := em.InstallCalls() + assert.Equal(t, 1, len(installCalls)) + assert.Equal(t, "gh-some-ext", installCalls[0].InterfaceMoqParam.RepoName()) + listCalls := em.ListCalls() + assert.Equal(t, 1, len(listCalls)) + } + }, + }, + { + name: "install an extension with same name as existing extension", + args: []string{"install", "owner/gh-existing-ext"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.ListFunc = func(bool) []extensions.Extension { + e := &Extension{path: "owner2/gh-existing-ext"} + return []extensions.Extension{e} + } + return func(t *testing.T) { + calls := em.ListCalls() + assert.Equal(t, 1, len(calls)) + } + }, + wantErr: true, + errMsg: "there is already an installed extension that provides the \"existing-ext\" command", + }, + { + name: "install local extension", + args: []string{"install", "."}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.InstallLocalFunc = func(dir string) error { + return nil + } + return func(t *testing.T) { + calls := em.InstallLocalCalls() + assert.Equal(t, 1, len(calls)) + assert.Equal(t, tempDir, normalizeDir(calls[0].Dir)) + } + }, + }, + { + name: "upgrade error", + args: []string{"upgrade"}, + wantErr: true, + errMsg: "must specify an extension to upgrade", + }, + { + name: "upgrade an extension", + args: []string{"upgrade", "hello"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.UpgradeFunc = func(name string, force bool) error { + return nil + } + return func(t *testing.T) { + calls := em.UpgradeCalls() + assert.Equal(t, 1, len(calls)) + assert.Equal(t, "hello", calls[0].Name) + } + }, + isTTY: true, + wantStdout: "✓ Successfully upgraded extension hello\n", + }, + { + name: "upgrade an extension notty", + args: []string{"upgrade", "hello"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.UpgradeFunc = func(name string, force bool) error { + return nil + } + return func(t *testing.T) { + calls := em.UpgradeCalls() + assert.Equal(t, 1, len(calls)) + assert.Equal(t, "hello", calls[0].Name) + } + }, + isTTY: false, + }, + { + name: "upgrade an extension gh-prefix", + args: []string{"upgrade", "gh-hello"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.UpgradeFunc = func(name string, force bool) error { + return nil + } + return func(t *testing.T) { + calls := em.UpgradeCalls() + assert.Equal(t, 1, len(calls)) + assert.Equal(t, "hello", calls[0].Name) + } + }, + isTTY: true, + wantStdout: "✓ Successfully upgraded extension hello\n", + }, + { + name: "upgrade an extension full name", + args: []string{"upgrade", "monalisa/gh-hello"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.UpgradeFunc = func(name string, force bool) error { + return nil + } + return func(t *testing.T) { + calls := em.UpgradeCalls() + assert.Equal(t, 1, len(calls)) + assert.Equal(t, "hello", calls[0].Name) + } + }, + isTTY: true, + wantStdout: "✓ Successfully upgraded extension hello\n", + }, + { + name: "upgrade all", + args: []string{"upgrade", "--all"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.UpgradeFunc = func(name string, force bool) error { + return nil + } + return func(t *testing.T) { + calls := em.UpgradeCalls() + assert.Equal(t, 1, len(calls)) + assert.Equal(t, "", calls[0].Name) + } + }, + isTTY: true, + wantStdout: "✓ Successfully upgraded extensions\n", + }, + { + name: "upgrade all notty", + args: []string{"upgrade", "--all"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.UpgradeFunc = func(name string, force bool) error { + return nil + } + return func(t *testing.T) { + calls := em.UpgradeCalls() + assert.Equal(t, 1, len(calls)) + assert.Equal(t, "", calls[0].Name) + } + }, + isTTY: false, + }, + { + name: "remove extension tty", + args: []string{"remove", "hello"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.RemoveFunc = func(name string) error { + return nil + } + return func(t *testing.T) { + calls := em.RemoveCalls() + assert.Equal(t, 1, len(calls)) + assert.Equal(t, "hello", calls[0].Name) + } + }, + isTTY: true, + wantStdout: "✓ Removed extension hello\n", + }, + { + name: "remove extension nontty", + args: []string{"remove", "hello"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.RemoveFunc = func(name string) error { + return nil + } + return func(t *testing.T) { + calls := em.RemoveCalls() + assert.Equal(t, 1, len(calls)) + assert.Equal(t, "hello", calls[0].Name) + } + }, + isTTY: false, + wantStdout: "", + }, + { + name: "remove extension gh-prefix", + args: []string{"remove", "gh-hello"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.RemoveFunc = func(name string) error { + return nil + } + return func(t *testing.T) { + calls := em.RemoveCalls() + assert.Equal(t, 1, len(calls)) + assert.Equal(t, "hello", calls[0].Name) + } + }, + isTTY: false, + wantStdout: "", + }, + { + name: "remove extension full name", + args: []string{"remove", "monalisa/gh-hello"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.RemoveFunc = func(name string) error { + return nil + } + return func(t *testing.T) { + calls := em.RemoveCalls() + assert.Equal(t, 1, len(calls)) + assert.Equal(t, "hello", calls[0].Name) + } + }, + isTTY: false, + wantStdout: "", + }, + { + name: "list extensions", + args: []string{"list"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.ListFunc = func(bool) []extensions.Extension { + ex1 := &Extension{path: "cli/gh-test", url: "https://github.com/cli/gh-test", currentVersion: "1", latestVersion: "1"} + ex2 := &Extension{path: "cli/gh-test2", url: "https://github.com/cli/gh-test2", currentVersion: "1", latestVersion: "2"} + return []extensions.Extension{ex1, ex2} + } + return func(t *testing.T) { + assert.Equal(t, 1, len(em.ListCalls())) + } + }, + wantStdout: "gh test\tcli/gh-test\t\ngh test2\tcli/gh-test2\tUpgrade available\n", + }, + { + name: "create extension tty", + args: []string{"create", "test"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.CreateFunc = func(name string) error { + return nil + } + return func(t *testing.T) { + calls := em.CreateCalls() + assert.Equal(t, 1, len(calls)) + assert.Equal(t, "gh-test", calls[0].Name) + } + }, + isTTY: true, + wantStdout: heredoc.Doc(` + ✓ Created directory gh-test + ✓ Initialized git repository + ✓ Set up extension scaffolding + + gh-test is ready for development + + Install locally with: cd gh-test && gh extension install . + + Publish to GitHub with: gh repo create gh-test + + For more information on writing extensions: + https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions + `), + }, + { + name: "create extension notty", + args: []string{"create", "gh-test"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.CreateFunc = func(name string) error { + return nil + } + return func(t *testing.T) { + calls := em.CreateCalls() + assert.Equal(t, 1, len(calls)) + assert.Equal(t, "gh-test", calls[0].Name) + } + }, + isTTY: false, + wantStdout: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, stdout, stderr := iostreams.Test() + ios.SetStdoutTTY(tt.isTTY) + ios.SetStderrTTY(tt.isTTY) + + var assertFunc func(*testing.T) + em := &extensions.ExtensionManagerMock{} + if tt.managerStubs != nil { + assertFunc = tt.managerStubs(em) + } + + reg := httpmock.Registry{} + defer reg.Verify(t) + client := http.Client{Transport: ®} + + f := cmdutil.Factory{ + Config: func() (config.Config, error) { + return config.NewBlankConfig(), nil + }, + IOStreams: ios, + ExtensionManager: em, + HttpClient: func() (*http.Client, error) { + return &client, nil + }, + } + + cmd := NewCmdExtension(&f) + cmd.SetArgs(tt.args) + cmd.SetOut(ioutil.Discard) + cmd.SetErr(ioutil.Discard) + + _, err := cmd.ExecuteC() + if tt.wantErr { + assert.EqualError(t, err, tt.errMsg) + } else { + assert.NoError(t, err) + } + + if assertFunc != nil { + assertFunc(t) + } + + assert.Equal(t, tt.wantStdout, stdout.String()) + assert.Equal(t, tt.wantStderr, stderr.String()) + }) + } +} + +func normalizeDir(d string) string { + return strings.TrimPrefix(d, "/private") +} + +func Test_checkValidExtension(t *testing.T) { + rootCmd := &cobra.Command{} + rootCmd.AddCommand(&cobra.Command{Use: "help"}) + rootCmd.AddCommand(&cobra.Command{Use: "auth"}) + + m := &extensions.ExtensionManagerMock{ + ListFunc: func(bool) []extensions.Extension { + return []extensions.Extension{ + &extensions.ExtensionMock{ + NameFunc: func() string { return "screensaver" }, + }, + &extensions.ExtensionMock{ + NameFunc: func() string { return "triage" }, + }, + } + }, + } + + type args struct { + rootCmd *cobra.Command + manager extensions.ExtensionManager + extName string + } + tests := []struct { + name string + args args + wantError string + }{ + { + name: "valid extension", + args: args{ + rootCmd: rootCmd, + manager: m, + extName: "gh-hello", + }, + }, + { + name: "invalid extension name", + args: args{ + rootCmd: rootCmd, + manager: m, + extName: "gherkins", + }, + wantError: "extension repository name must start with `gh-`", + }, + { + name: "clashes with built-in command", + args: args{ + rootCmd: rootCmd, + manager: m, + extName: "gh-auth", + }, + wantError: "\"auth\" matches the name of a built-in command", + }, + { + name: "clashes with an installed extension", + args: args{ + rootCmd: rootCmd, + manager: m, + extName: "gh-triage", + }, + wantError: "there is already an installed extension that provides the \"triage\" command", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := checkValidExtension(tt.args.rootCmd, tt.args.manager, tt.args.extName) + if tt.wantError == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantError) + } + }) + } +} diff --git a/pkg/cmd/extension/extension.go b/pkg/cmd/extension/extension.go new file mode 100644 index 00000000000..ead9204cd39 --- /dev/null +++ b/pkg/cmd/extension/extension.go @@ -0,0 +1,50 @@ +package extension + +import ( + "path/filepath" + "strings" +) + +const manifestName = "manifest.yml" + +type ExtensionKind int + +const ( + GitKind ExtensionKind = iota + BinaryKind +) + +type Extension struct { + path string + url string + isLocal bool + currentVersion string + latestVersion string + kind ExtensionKind +} + +func (e *Extension) Name() string { + return strings.TrimPrefix(filepath.Base(e.path), "gh-") +} + +func (e *Extension) Path() string { + return e.path +} + +func (e *Extension) URL() string { + return e.url +} + +func (e *Extension) IsLocal() bool { + return e.isLocal +} + +func (e *Extension) UpdateAvailable() bool { + if e.isLocal || + e.currentVersion == "" || + e.latestVersion == "" || + e.currentVersion == e.latestVersion { + return false + } + return true +} diff --git a/pkg/cmd/extension/http.go b/pkg/cmd/extension/http.go new file mode 100644 index 00000000000..cfae2b738f3 --- /dev/null +++ b/pkg/cmd/extension/http.go @@ -0,0 +1,114 @@ +package extension + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/internal/ghrepo" +) + +func hasScript(httpClient *http.Client, repo ghrepo.Interface) (hs bool, err error) { + path := fmt.Sprintf("repos/%s/%s/contents/%s", + repo.RepoOwner(), repo.RepoName(), repo.RepoName()) + url := ghinstance.RESTPrefix(repo.RepoHost()) + path + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return + } + + resp, err := httpClient.Do(req) + if err != nil { + return + } + defer resp.Body.Close() + + if resp.StatusCode == 404 { + return + } + + if resp.StatusCode > 299 { + err = api.HandleHTTPError(resp) + return + } + + hs = true + return +} + +type releaseAsset struct { + Name string + APIURL string `json:"url"` +} + +type release struct { + Tag string `json:"tag_name"` + Assets []releaseAsset +} + +// downloadAsset downloads a single asset to the given file path. +func downloadAsset(httpClient *http.Client, asset releaseAsset, destPath string) error { + req, err := http.NewRequest("GET", asset.APIURL, nil) + if err != nil { + return err + } + + req.Header.Set("Accept", "application/octet-stream") + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode > 299 { + return api.HandleHTTPError(resp) + } + + f, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) + if err != nil { + return err + } + defer f.Close() + + _, err = io.Copy(f, resp.Body) + return err +} + +// fetchLatestRelease finds the latest published release for a repository. +func fetchLatestRelease(httpClient *http.Client, baseRepo ghrepo.Interface) (*release, error) { + path := fmt.Sprintf("repos/%s/%s/releases/latest", baseRepo.RepoOwner(), baseRepo.RepoName()) + url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + resp, err := httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode > 299 { + return nil, api.HandleHTTPError(resp) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var r release + err = json.Unmarshal(b, &r) + if err != nil { + return nil, err + } + + return &r, nil +} diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go new file mode 100644 index 00000000000..aec54f5b2ef --- /dev/null +++ b/pkg/cmd/extension/manager.go @@ -0,0 +1,693 @@ +package extension + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/fs" + "io/ioutil" + "net/http" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strings" + "sync" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/extensions" + "github.com/cli/cli/v2/pkg/findsh" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/safeexec" + "gopkg.in/yaml.v3" +) + +type Manager struct { + dataDir func() string + lookPath func(string) (string, error) + findSh func() (string, error) + newCommand func(string, ...string) *exec.Cmd + platform func() string + client *http.Client + config config.Config + io *iostreams.IOStreams +} + +func NewManager(io *iostreams.IOStreams) *Manager { + return &Manager{ + dataDir: config.DataDir, + lookPath: safeexec.LookPath, + findSh: findsh.Find, + newCommand: exec.Command, + platform: func() string { + return fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH) + }, + io: io, + } +} + +func (m *Manager) SetConfig(cfg config.Config) { + m.config = cfg +} + +func (m *Manager) SetClient(client *http.Client) { + m.client = client +} + +func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Writer) (bool, error) { + if len(args) == 0 { + return false, errors.New("too few arguments in list") + } + + var exe string + extName := args[0] + forwardArgs := args[1:] + + exts, _ := m.list(false) + for _, e := range exts { + if e.Name() == extName { + exe = e.Path() + break + } + } + if exe == "" { + return false, nil + } + + var externalCmd *exec.Cmd + + if runtime.GOOS == "windows" { + // Dispatch all extension calls through the `sh` interpreter to support executable files with a + // shebang line on Windows. + shExe, err := m.findSh() + if err != nil { + if errors.Is(err, exec.ErrNotFound) { + return true, errors.New("the `sh.exe` interpreter is required. Please install Git for Windows and try again") + } + return true, err + } + forwardArgs = append([]string{"-c", `command "$@"`, "--", exe}, forwardArgs...) + externalCmd = m.newCommand(shExe, forwardArgs...) + } else { + externalCmd = m.newCommand(exe, forwardArgs...) + } + externalCmd.Stdin = stdin + externalCmd.Stdout = stdout + externalCmd.Stderr = stderr + return true, externalCmd.Run() +} + +func (m *Manager) List(includeMetadata bool) []extensions.Extension { + exts, _ := m.list(includeMetadata) + r := make([]extensions.Extension, len(exts)) + for i, v := range exts { + val := v + r[i] = &val + } + return r +} + +func (m *Manager) list(includeMetadata bool) ([]Extension, error) { + dir := m.installDir() + entries, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + + var results []Extension + for _, f := range entries { + if !strings.HasPrefix(f.Name(), "gh-") { + continue + } + var ext Extension + var err error + if f.IsDir() { + ext, err = m.parseExtensionDir(f) + if err != nil { + return nil, err + } + results = append(results, ext) + } else { + ext, err = m.parseExtensionFile(f) + if err != nil { + return nil, err + } + results = append(results, ext) + } + } + + if includeMetadata { + m.populateLatestVersions(results) + } + + return results, nil +} + +func (m *Manager) parseExtensionFile(fi fs.FileInfo) (Extension, error) { + ext := Extension{isLocal: true} + id := m.installDir() + exePath := filepath.Join(id, fi.Name(), fi.Name()) + if !isSymlink(fi.Mode()) { + // if this is a regular file, its contents is the local directory of the extension + p, err := readPathFromFile(filepath.Join(id, fi.Name())) + if err != nil { + return ext, err + } + exePath = filepath.Join(p, fi.Name()) + } + ext.path = exePath + return ext, nil +} + +func (m *Manager) parseExtensionDir(fi fs.FileInfo) (Extension, error) { + id := m.installDir() + if _, err := os.Stat(filepath.Join(id, fi.Name(), manifestName)); err == nil { + return m.parseBinaryExtensionDir(fi) + } + + return m.parseGitExtensionDir(fi) +} + +func (m *Manager) parseBinaryExtensionDir(fi fs.FileInfo) (Extension, error) { + id := m.installDir() + exePath := filepath.Join(id, fi.Name(), fi.Name()) + ext := Extension{path: exePath, kind: BinaryKind} + manifestPath := filepath.Join(id, fi.Name(), manifestName) + manifest, err := os.ReadFile(manifestPath) + if err != nil { + return ext, fmt.Errorf("could not open %s for reading: %w", manifestPath, err) + } + var bm binManifest + err = yaml.Unmarshal(manifest, &bm) + if err != nil { + return ext, fmt.Errorf("could not parse %s: %w", manifestPath, err) + } + repo := ghrepo.NewWithHost(bm.Owner, bm.Name, bm.Host) + remoteURL := ghrepo.GenerateRepoURL(repo, "") + ext.url = remoteURL + ext.currentVersion = bm.Tag + return ext, nil +} + +func (m *Manager) parseGitExtensionDir(fi fs.FileInfo) (Extension, error) { + id := m.installDir() + exePath := filepath.Join(id, fi.Name(), fi.Name()) + remoteUrl := m.getRemoteUrl(fi.Name()) + currentVersion := m.getCurrentVersion(fi.Name()) + return Extension{ + path: exePath, + url: remoteUrl, + isLocal: false, + currentVersion: currentVersion, + kind: GitKind, + }, nil +} + +// getCurrentVersion determines the current version for non-local git extensions. +func (m *Manager) getCurrentVersion(extension string) string { + gitExe, err := m.lookPath("git") + if err != nil { + return "" + } + dir := m.installDir() + gitDir := "--git-dir=" + filepath.Join(dir, extension, ".git") + cmd := m.newCommand(gitExe, gitDir, "rev-parse", "HEAD") + localSha, err := cmd.Output() + if err != nil { + return "" + } + return string(bytes.TrimSpace(localSha)) +} + +// getRemoteUrl determines the remote URL for non-local git extensions. +func (m *Manager) getRemoteUrl(extension string) string { + gitExe, err := m.lookPath("git") + if err != nil { + return "" + } + dir := m.installDir() + gitDir := "--git-dir=" + filepath.Join(dir, extension, ".git") + cmd := m.newCommand(gitExe, gitDir, "config", "remote.origin.url") + url, err := cmd.Output() + if err != nil { + return "" + } + return strings.TrimSpace(string(url)) +} + +func (m *Manager) populateLatestVersions(exts []Extension) { + size := len(exts) + type result struct { + index int + version string + } + ch := make(chan result, size) + var wg sync.WaitGroup + wg.Add(size) + for idx, ext := range exts { + go func(i int, e Extension) { + defer wg.Done() + version, _ := m.getLatestVersion(e) + ch <- result{index: i, version: version} + }(idx, ext) + } + wg.Wait() + close(ch) + for r := range ch { + ext := &exts[r.index] + ext.latestVersion = r.version + } +} + +func (m *Manager) getLatestVersion(ext Extension) (string, error) { + if ext.isLocal { + return "", fmt.Errorf("unable to get latest version for local extensions") + } + if ext.kind == GitKind { + gitExe, err := m.lookPath("git") + if err != nil { + return "", err + } + extDir := filepath.Dir(ext.path) + gitDir := "--git-dir=" + filepath.Join(extDir, ".git") + cmd := m.newCommand(gitExe, gitDir, "ls-remote", "origin", "HEAD") + lsRemote, err := cmd.Output() + if err != nil { + return "", err + } + remoteSha := bytes.SplitN(lsRemote, []byte("\t"), 2)[0] + return string(remoteSha), nil + } else { + repo, err := ghrepo.FromFullName(ext.url) + if err != nil { + return "", err + } + r, err := fetchLatestRelease(m.client, repo) + if err != nil { + return "", err + } + return r.Tag, nil + } +} + +func (m *Manager) InstallLocal(dir string) error { + name := filepath.Base(dir) + targetLink := filepath.Join(m.installDir(), name) + if err := os.MkdirAll(filepath.Dir(targetLink), 0755); err != nil { + return err + } + return makeSymlink(dir, targetLink) +} + +type binManifest struct { + Owner string + Name string + Host string + Tag string + // TODO I may end up not using this; just thinking ahead to local installs + Path string +} + +func (m *Manager) Install(repo ghrepo.Interface) error { + isBin, err := isBinExtension(m.client, repo) + if err != nil { + return fmt.Errorf("could not check for binary extension: %w", err) + } + if isBin { + return m.installBin(repo) + } + + hs, err := hasScript(m.client, repo) + if err != nil { + return err + } + if !hs { + // TODO open an issue hint, here? + return errors.New("extension is uninstallable: missing executable") + } + + protocol, _ := m.config.Get(repo.RepoHost(), "git_protocol") + return m.installGit(ghrepo.FormatRemoteURL(repo, protocol), m.io.Out, m.io.ErrOut) +} + +func (m *Manager) installBin(repo ghrepo.Interface) error { + var r *release + r, err := fetchLatestRelease(m.client, repo) + if err != nil { + return err + } + + suffix := m.platform() + var asset *releaseAsset + for _, a := range r.Assets { + if strings.HasSuffix(a.Name, suffix) { + asset = &a + break + } + } + + if asset == nil { + return fmt.Errorf("%s unsupported for %s. Open an issue: `gh issue create -R %s/%s -t'Support %s'`", + repo.RepoName(), + suffix, repo.RepoOwner(), repo.RepoName(), suffix) + } + + name := repo.RepoName() + targetDir := filepath.Join(m.installDir(), name) + // TODO clean this up if function errs? + err = os.MkdirAll(targetDir, 0755) + if err != nil { + return fmt.Errorf("failed to create installation directory: %w", err) + } + + binPath := filepath.Join(targetDir, name) + + err = downloadAsset(m.client, *asset, binPath) + if err != nil { + return fmt.Errorf("failed to download asset %s: %w", asset.Name, err) + } + + manifest := binManifest{ + Name: name, + Owner: repo.RepoOwner(), + Host: repo.RepoHost(), + Path: binPath, + Tag: r.Tag, + } + + bs, err := yaml.Marshal(manifest) + if err != nil { + return fmt.Errorf("failed to serialize manifest: %w", err) + } + + manifestPath := filepath.Join(targetDir, manifestName) + + f, err := os.OpenFile(manifestPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return fmt.Errorf("failed to open manifest for writing: %w", err) + } + defer f.Close() + + _, err = f.Write(bs) + if err != nil { + return fmt.Errorf("failed write manifest file: %w", err) + } + + return nil +} + +func (m *Manager) installGit(cloneURL string, stdout, stderr io.Writer) error { + exe, err := m.lookPath("git") + if err != nil { + return err + } + + name := strings.TrimSuffix(path.Base(cloneURL), ".git") + targetDir := filepath.Join(m.installDir(), name) + + externalCmd := m.newCommand(exe, "clone", cloneURL, targetDir) + externalCmd.Stdout = stdout + externalCmd.Stderr = stderr + return externalCmd.Run() +} + +var localExtensionUpgradeError = errors.New("local extensions can not be upgraded") +var upToDateError = errors.New("already up to date") +var noExtensionsInstalledError = errors.New("no extensions installed") + +func (m *Manager) Upgrade(name string, force bool) error { + // Fetch metadata during list only when upgrading all extensions. + // This is a performance improvement so that we don't make a + // bunch of unecessary network requests when trying to upgrade a single extension. + fetchMetadata := name == "" + exts, _ := m.list(fetchMetadata) + if len(exts) == 0 { + return noExtensionsInstalledError + } + if name == "" { + return m.upgradeExtensions(exts, force) + } + for _, f := range exts { + if f.Name() != name { + continue + } + var err error + // For single extensions manually retrieve latest version since we forgo + // doing it during list. + f.latestVersion, err = m.getLatestVersion(f) + if err != nil { + return err + } + return m.upgradeExtension(f, force) + } + return fmt.Errorf("no extension matched %q", name) +} + +func (m *Manager) upgradeExtensions(exts []Extension, force bool) error { + var failed bool + for _, f := range exts { + fmt.Fprintf(m.io.Out, "[%s]: ", f.Name()) + err := m.upgradeExtension(f, force) + if err != nil { + if !errors.Is(err, localExtensionUpgradeError) && + !errors.Is(err, upToDateError) { + failed = true + } + fmt.Fprintf(m.io.Out, "%s\n", err) + continue + } + fmt.Fprintf(m.io.Out, "upgrade complete\n") + } + if failed { + return errors.New("some extensions failed to upgrade") + } + return nil +} + +func (m *Manager) upgradeExtension(ext Extension, force bool) error { + if ext.isLocal { + return localExtensionUpgradeError + } + if !ext.UpdateAvailable() { + return upToDateError + } + var err error + if ext.kind == BinaryKind { + err = m.upgradeBinExtension(ext) + } else { + err = m.upgradeGitExtension(ext, force) + } + return err +} + +func (m *Manager) upgradeGitExtension(ext Extension, force bool) error { + exe, err := m.lookPath("git") + if err != nil { + return err + } + var cmds []*exec.Cmd + dir := filepath.Dir(ext.path) + if force { + fetchCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "fetch", "origin", "HEAD") + resetCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "reset", "--hard", "origin/HEAD") + cmds = []*exec.Cmd{fetchCmd, resetCmd} + } else { + pullCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "pull", "--ff-only") + cmds = []*exec.Cmd{pullCmd} + } + return runCmds(cmds) +} + +func (m *Manager) upgradeBinExtension(ext Extension) error { + repo, err := ghrepo.FromFullName(ext.url) + if err != nil { + return fmt.Errorf("failed to parse URL %s: %w", ext.url, err) + } + return m.installBin(repo) +} + +func (m *Manager) Remove(name string) error { + targetDir := filepath.Join(m.installDir(), "gh-"+name) + if _, err := os.Lstat(targetDir); os.IsNotExist(err) { + return fmt.Errorf("no extension found: %q", targetDir) + } + return os.RemoveAll(targetDir) +} + +func (m *Manager) installDir() string { + return filepath.Join(m.dataDir(), "extensions") +} + +func (m *Manager) Create(name string) error { + exe, err := m.lookPath("git") + if err != nil { + return err + } + + err = os.Mkdir(name, 0755) + if err != nil { + return err + } + + initCmd := m.newCommand(exe, "init", "--quiet", name) + err = initCmd.Run() + if err != nil { + return err + } + + fileTmpl := heredoc.Docf(` + #!/usr/bin/env bash + set -e + + echo "Hello %[1]s!" + + # Snippets to help get started: + + # Determine if an executable is in the PATH + # if ! type -p ruby >/dev/null; then + # echo "Ruby not found on the system" >&2 + # exit 1 + # fi + + # Pass arguments through to another command + # gh issue list "$@" -R cli/cli + + # Using the gh api command to retrieve and format information + # QUERY=' + # query($endCursor: String) { + # viewer { + # repositories(first: 100, after: $endCursor) { + # nodes { + # nameWithOwner + # stargazerCount + # } + # } + # } + # } + # ' + # TEMPLATE=' + # {{- range $repo := .data.viewer.repositories.nodes -}} + # {{- printf "name: %[2]s - stargazers: %[3]s\n" $repo.nameWithOwner $repo.stargazerCount -}} + # {{- end -}} + # ' + # exec gh api graphql -f query="${QUERY}" --paginate --template="${TEMPLATE}" + `, name, "%s", "%v") + filePath := filepath.Join(name, name) + err = ioutil.WriteFile(filePath, []byte(fileTmpl), 0755) + if err != nil { + return err + } + + wd, err := os.Getwd() + if err != nil { + return err + } + dir := filepath.Join(wd, name) + addCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "add", name, "--chmod=+x") + err = addCmd.Run() + return err +} + +func runCmds(cmds []*exec.Cmd) error { + for _, cmd := range cmds { + if err := cmd.Run(); err != nil { + return err + } + } + return nil +} + +func isSymlink(m os.FileMode) bool { + return m&os.ModeSymlink != 0 +} + +// reads the product of makeSymlink on Windows +func readPathFromFile(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + b := make([]byte, 1024) + n, err := f.Read(b) + return strings.TrimSpace(string(b[:n])), err +} + +func isBinExtension(client *http.Client, repo ghrepo.Interface) (isBin bool, err error) { + var r *release + r, err = fetchLatestRelease(client, repo) + if err != nil { + httpErr, ok := err.(api.HTTPError) + if ok && httpErr.StatusCode == 404 { + err = nil + return + } + return + } + + for _, a := range r.Assets { + dists := possibleDists() + for _, d := range dists { + if strings.HasSuffix(a.Name, d) { + isBin = true + break + } + } + } + + return +} + +func possibleDists() []string { + return []string{ + "aix-ppc64", + "android-386", + "android-amd64", + "android-arm", + "android-arm64", + "darwin-amd64", + "darwin-arm64", + "dragonfly-amd64", + "freebsd-386", + "freebsd-amd64", + "freebsd-arm", + "freebsd-arm64", + "illumos-amd64", + "ios-amd64", + "ios-arm64", + "js-wasm", + "linux-386", + "linux-amd64", + "linux-arm", + "linux-arm64", + "linux-mips", + "linux-mips64", + "linux-mips64le", + "linux-mipsle", + "linux-ppc64", + "linux-ppc64le", + "linux-riscv64", + "linux-s390x", + "netbsd-386", + "netbsd-amd64", + "netbsd-arm", + "netbsd-arm64", + "openbsd-386", + "openbsd-amd64", + "openbsd-arm", + "openbsd-arm64", + "openbsd-mips64", + "plan9-386", + "plan9-amd64", + "plan9-arm", + "solaris-amd64", + "windows-386", + "windows-amd64", + "windows-arm", + } +} diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go new file mode 100644 index 00000000000..121f477651b --- /dev/null +++ b/pkg/cmd/extension/manager_test.go @@ -0,0 +1,568 @@ +package extension + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" +) + +func TestHelperProcess(t *testing.T) { + if os.Getenv("GH_WANT_HELPER_PROCESS") != "1" { + return + } + if err := func(args []string) error { + fmt.Fprintf(os.Stdout, "%v\n", args) + return nil + }(os.Args[3:]); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + os.Exit(0) +} + +func newTestManager(dir string, client *http.Client, io *iostreams.IOStreams) *Manager { + return &Manager{ + dataDir: func() string { return dir }, + lookPath: func(exe string) (string, error) { return exe, nil }, + findSh: func() (string, error) { return "sh", nil }, + newCommand: func(exe string, args ...string) *exec.Cmd { + args = append([]string{os.Args[0], "-test.run=TestHelperProcess", "--", exe}, args...) + cmd := exec.Command(args[0], args[1:]...) + if io != nil { + cmd.Stdout = io.Out + cmd.Stderr = io.ErrOut + } + cmd.Env = []string{"GH_WANT_HELPER_PROCESS=1"} + return cmd + }, + config: config.NewBlankConfig(), + io: io, + client: client, + platform: func() string { + return "windows-amd64" + }, + } +} + +func TestManager_List(t *testing.T) { + tempDir := t.TempDir() + assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello"))) + assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-two", "gh-two"))) + + assert.NoError(t, stubBinaryExtension( + filepath.Join(tempDir, "extensions", "gh-bin-ext"), + binManifest{ + Owner: "owner", + Name: "gh-bin-ext", + Host: "example.com", + Tag: "v1.0.1", + })) + + m := newTestManager(tempDir, nil, nil) + exts := m.List(false) + assert.Equal(t, 3, len(exts)) + assert.Equal(t, "bin-ext", exts[0].Name()) + assert.Equal(t, "hello", exts[1].Name()) + assert.Equal(t, "two", exts[2].Name()) +} + +func TestManager_List_binary_update(t *testing.T) { + tempDir := t.TempDir() + + assert.NoError(t, stubBinaryExtension( + filepath.Join(tempDir, "extensions", "gh-bin-ext"), + binManifest{ + Owner: "owner", + Name: "gh-bin-ext", + Host: "example.com", + Tag: "v1.0.1", + })) + + reg := httpmock.Registry{} + defer reg.Verify(t) + client := http.Client{Transport: ®} + + reg.Register( + httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"), + httpmock.JSONResponse( + release{ + Tag: "v1.0.2", + Assets: []releaseAsset{ + { + Name: "gh-bin-ext-windows-amd64", + APIURL: "https://example.com/release/cool2", + }, + }, + })) + + m := newTestManager(tempDir, &client, nil) + + exts := m.List(true) + assert.Equal(t, 1, len(exts)) + assert.Equal(t, "bin-ext", exts[0].Name()) + assert.True(t, exts[0].UpdateAvailable()) + assert.Equal(t, "https://example.com/owner/gh-bin-ext", exts[0].URL()) +} + +func TestManager_Dispatch(t *testing.T) { + tempDir := t.TempDir() + extPath := filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello") + assert.NoError(t, stubExtension(extPath)) + + m := newTestManager(tempDir, nil, nil) + + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + found, err := m.Dispatch([]string{"hello", "one", "two"}, nil, stdout, stderr) + assert.NoError(t, err) + assert.True(t, found) + + if runtime.GOOS == "windows" { + assert.Equal(t, fmt.Sprintf("[sh -c command \"$@\" -- %s one two]\n", extPath), stdout.String()) + } else { + assert.Equal(t, fmt.Sprintf("[%s one two]\n", extPath), stdout.String()) + } + assert.Equal(t, "", stderr.String()) +} + +func TestManager_Remove(t *testing.T) { + tempDir := t.TempDir() + assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello"))) + assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-two", "gh-two"))) + + m := newTestManager(tempDir, nil, nil) + err := m.Remove("hello") + assert.NoError(t, err) + + items, err := ioutil.ReadDir(filepath.Join(tempDir, "extensions")) + assert.NoError(t, err) + assert.Equal(t, 1, len(items)) + assert.Equal(t, "gh-two", items[0].Name()) +} + +func TestManager_Upgrade_NoExtensions(t *testing.T) { + tempDir := t.TempDir() + io, _, stdout, stderr := iostreams.Test() + m := newTestManager(tempDir, nil, io) + err := m.Upgrade("", false) + assert.EqualError(t, err, "no extensions installed") + assert.Equal(t, "", stdout.String()) + assert.Equal(t, "", stderr.String()) +} + +func TestManager_Upgrade_NoMatchingExtension(t *testing.T) { + tempDir := t.TempDir() + assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello"))) + io, _, stdout, stderr := iostreams.Test() + m := newTestManager(tempDir, nil, io) + err := m.Upgrade("invalid", false) + assert.EqualError(t, err, `no extension matched "invalid"`) + assert.Equal(t, "", stdout.String()) + assert.Equal(t, "", stderr.String()) +} + +func TestManager_UpgradeExtensions(t *testing.T) { + tempDir := t.TempDir() + assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello"))) + assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-two", "gh-two"))) + assert.NoError(t, stubLocalExtension(tempDir, filepath.Join(tempDir, "extensions", "gh-local", "gh-local"))) + io, _, stdout, stderr := iostreams.Test() + m := newTestManager(tempDir, nil, io) + exts, err := m.list(false) + assert.NoError(t, err) + assert.Equal(t, 3, len(exts)) + for i := 0; i < 3; i++ { + exts[i].currentVersion = "old version" + exts[i].latestVersion = "new version" + } + err = m.upgradeExtensions(exts, false) + assert.NoError(t, err) + assert.Equal(t, heredoc.Docf( + ` + [hello]: [git -C %s --git-dir=%s pull --ff-only] + upgrade complete + [local]: local extensions can not be upgraded + [two]: [git -C %s --git-dir=%s pull --ff-only] + upgrade complete + `, + filepath.Join(tempDir, "extensions", "gh-hello"), + filepath.Join(tempDir, "extensions", "gh-hello", ".git"), + filepath.Join(tempDir, "extensions", "gh-two"), + filepath.Join(tempDir, "extensions", "gh-two", ".git"), + ), stdout.String()) + assert.Equal(t, "", stderr.String()) +} + +func TestManager_UpgradeExtension_LocalExtension(t *testing.T) { + tempDir := t.TempDir() + assert.NoError(t, stubLocalExtension(tempDir, filepath.Join(tempDir, "extensions", "gh-local", "gh-local"))) + + io, _, stdout, stderr := iostreams.Test() + m := newTestManager(tempDir, nil, io) + exts, err := m.list(false) + assert.NoError(t, err) + assert.Equal(t, 1, len(exts)) + err = m.upgradeExtension(exts[0], false) + assert.EqualError(t, err, "local extensions can not be upgraded") + assert.Equal(t, "", stdout.String()) + assert.Equal(t, "", stderr.String()) +} + +func TestManager_UpgradeExtension_GitExtension(t *testing.T) { + tempDir := t.TempDir() + assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote"))) + io, _, stdout, stderr := iostreams.Test() + m := newTestManager(tempDir, nil, io) + exts, err := m.list(false) + assert.NoError(t, err) + assert.Equal(t, 1, len(exts)) + ext := exts[0] + ext.currentVersion = "old version" + ext.latestVersion = "new version" + err = m.upgradeExtension(ext, false) + assert.NoError(t, err) + assert.Equal(t, heredoc.Docf( + ` + [git -C %s --git-dir=%s pull --ff-only] + `, + filepath.Join(tempDir, "extensions", "gh-remote"), + filepath.Join(tempDir, "extensions", "gh-remote", ".git"), + ), stdout.String()) + assert.Equal(t, "", stderr.String()) +} + +func TestManager_UpgradeExtension_GitExtension_Force(t *testing.T) { + tempDir := t.TempDir() + extensionDir := filepath.Join(tempDir, "extensions", "gh-remote") + gitDir := filepath.Join(tempDir, "extensions", "gh-remote", ".git") + assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote"))) + io, _, stdout, stderr := iostreams.Test() + m := newTestManager(tempDir, nil, io) + exts, err := m.list(false) + assert.NoError(t, err) + assert.Equal(t, 1, len(exts)) + ext := exts[0] + ext.currentVersion = "old version" + ext.latestVersion = "new version" + err = m.upgradeExtension(ext, true) + assert.NoError(t, err) + assert.Equal(t, heredoc.Docf( + ` + [git -C %s --git-dir=%s fetch origin HEAD] + [git -C %s --git-dir=%s reset --hard origin/HEAD] + `, + extensionDir, + gitDir, + extensionDir, + gitDir, + ), stdout.String()) + assert.Equal(t, "", stderr.String()) +} + +func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) { + tempDir := t.TempDir() + io, _, _, _ := iostreams.Test() + reg := httpmock.Registry{} + defer reg.Verify(t) + client := http.Client{Transport: ®} + assert.NoError(t, stubBinaryExtension( + filepath.Join(tempDir, "extensions", "gh-bin-ext"), + binManifest{ + Owner: "owner", + Name: "gh-bin-ext", + Host: "example.com", + Tag: "v1.0.1", + })) + m := newTestManager(tempDir, &client, io) + reg.Register( + httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"), + httpmock.JSONResponse( + release{ + Tag: "v1.0.2", + Assets: []releaseAsset{ + { + Name: "gh-bin-ext-windows-amd64", + APIURL: "https://example.com/release/cool2", + }, + }, + })) + reg.Register( + httpmock.REST("GET", "release/cool2"), + httpmock.StringResponse("FAKE UPGRADED BINARY")) + + exts, err := m.list(false) + assert.NoError(t, err) + assert.Equal(t, 1, len(exts)) + ext := exts[0] + ext.latestVersion = "v1.0.2" + err = m.upgradeExtension(ext, false) + assert.NoError(t, err) + + manifest, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext", manifestName)) + assert.NoError(t, err) + + var bm binManifest + err = yaml.Unmarshal(manifest, &bm) + assert.NoError(t, err) + + assert.Equal(t, binManifest{ + Name: "gh-bin-ext", + Owner: "owner", + Host: "example.com", + Tag: "v1.0.2", + Path: filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext"), + }, bm) + + fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext")) + assert.NoError(t, err) + + assert.Equal(t, "FAKE UPGRADED BINARY", string(fakeBin)) +} + +func TestManager_Install_git(t *testing.T) { + tempDir := t.TempDir() + + reg := httpmock.Registry{} + defer reg.Verify(t) + client := http.Client{Transport: ®} + + io, _, stdout, stderr := iostreams.Test() + + m := newTestManager(tempDir, &client, io) + + reg.Register( + httpmock.REST("GET", "repos/owner/gh-some-ext/releases/latest"), + httpmock.JSONResponse( + release{ + Assets: []releaseAsset{ + { + Name: "not-a-binary", + APIURL: "https://example.com/release/cool", + }, + }, + })) + reg.Register( + httpmock.REST("GET", "repos/owner/gh-some-ext/contents/gh-some-ext"), + httpmock.StringResponse("script")) + + repo := ghrepo.New("owner", "gh-some-ext") + + err := m.Install(repo) + assert.NoError(t, err) + assert.Equal(t, fmt.Sprintf("[git clone https://github.com/owner/gh-some-ext.git %s]\n", filepath.Join(tempDir, "extensions", "gh-some-ext")), stdout.String()) + assert.Equal(t, "", stderr.String()) +} + +func TestManager_Install_binary_unsupported(t *testing.T) { + repo := ghrepo.NewWithHost("owner", "gh-bin-ext", "example.com") + + reg := httpmock.Registry{} + defer reg.Verify(t) + client := http.Client{Transport: ®} + + reg.Register( + httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"), + httpmock.JSONResponse( + release{ + Assets: []releaseAsset{ + { + Name: "gh-bin-ext-linux-amd64", + APIURL: "https://example.com/release/cool", + }, + }, + })) + reg.Register( + httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"), + httpmock.JSONResponse( + release{ + Tag: "v1.0.1", + Assets: []releaseAsset{ + { + Name: "gh-bin-ext-linux-amd64", + APIURL: "https://example.com/release/cool", + }, + }, + })) + + io, _, _, _ := iostreams.Test() + tempDir := t.TempDir() + + m := newTestManager(tempDir, &client, io) + + err := m.Install(repo) + assert.Error(t, err) + + errText := "gh-bin-ext unsupported for windows-amd64. Open an issue: `gh issue create -R owner/gh-bin-ext -t'Support windows-amd64'`" + + assert.Equal(t, errText, err.Error()) +} + +func TestManager_Install_binary(t *testing.T) { + repo := ghrepo.NewWithHost("owner", "gh-bin-ext", "example.com") + + reg := httpmock.Registry{} + defer reg.Verify(t) + client := http.Client{Transport: ®} + + reg.Register( + httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"), + httpmock.JSONResponse( + release{ + Assets: []releaseAsset{ + { + Name: "gh-bin-ext-windows-amd64", + APIURL: "https://example.com/release/cool", + }, + }, + })) + reg.Register( + httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"), + httpmock.JSONResponse( + release{ + Tag: "v1.0.1", + Assets: []releaseAsset{ + { + Name: "gh-bin-ext-windows-amd64", + APIURL: "https://example.com/release/cool", + }, + }, + })) + reg.Register( + httpmock.REST("GET", "release/cool"), + httpmock.StringResponse("FAKE BINARY")) + + io, _, _, _ := iostreams.Test() + tempDir := t.TempDir() + + m := newTestManager(tempDir, &client, io) + + err := m.Install(repo) + assert.NoError(t, err) + + manifest, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext", manifestName)) + assert.NoError(t, err) + + var bm binManifest + err = yaml.Unmarshal(manifest, &bm) + assert.NoError(t, err) + + assert.Equal(t, binManifest{ + Name: "gh-bin-ext", + Owner: "owner", + Host: "example.com", + Tag: "v1.0.1", + Path: filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext"), + }, bm) + + fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext")) + assert.NoError(t, err) + + assert.Equal(t, "FAKE BINARY", string(fakeBin)) +} + +func TestManager_Create(t *testing.T) { + tempDir := t.TempDir() + oldWd, _ := os.Getwd() + assert.NoError(t, os.Chdir(tempDir)) + t.Cleanup(func() { _ = os.Chdir(oldWd) }) + m := newTestManager(tempDir, nil, nil) + err := m.Create("gh-test") + assert.NoError(t, err) + files, err := ioutil.ReadDir(filepath.Join(tempDir, "gh-test")) + assert.NoError(t, err) + assert.Equal(t, 1, len(files)) + extFile := files[0] + assert.Equal(t, "gh-test", extFile.Name()) + if runtime.GOOS == "windows" { + assert.Equal(t, os.FileMode(0666), extFile.Mode()) + } else { + assert.Equal(t, os.FileMode(0755), extFile.Mode()) + } +} + +func stubExtension(path string) error { + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return err + } + f, err := os.OpenFile(path, os.O_CREATE, 0755) + if err != nil { + return err + } + return f.Close() +} + +func stubLocalExtension(tempDir, path string) error { + extDir, err := ioutil.TempDir(tempDir, "local-ext") + if err != nil { + return err + } + extFile, err := os.OpenFile(filepath.Join(extDir, filepath.Base(path)), os.O_CREATE, 0755) + if err != nil { + return err + } + if err := extFile.Close(); err != nil { + return err + } + + linkPath := filepath.Dir(path) + if err := os.MkdirAll(filepath.Dir(linkPath), 0755); err != nil { + return err + } + f, err := os.OpenFile(linkPath, os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return err + } + _, err = f.WriteString(extDir) + if err != nil { + return err + } + return f.Close() +} + +// Given the path where an extension should be installed and a manifest struct, creates a fake binary extension on disk +func stubBinaryExtension(installPath string, bm binManifest) error { + if err := os.MkdirAll(installPath, 0755); err != nil { + return err + } + fakeBinaryPath := filepath.Join(installPath, filepath.Base(installPath)) + fb, err := os.OpenFile(fakeBinaryPath, os.O_CREATE, 0755) + if err != nil { + return err + } + err = fb.Close() + if err != nil { + return err + } + + bs, err := yaml.Marshal(bm) + if err != nil { + return fmt.Errorf("failed to serialize manifest: %w", err) + } + + manifestPath := filepath.Join(installPath, manifestName) + + fm, err := os.OpenFile(manifestPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return fmt.Errorf("failed to open manifest for writing: %w", err) + } + _, err = fm.Write(bs) + if err != nil { + return fmt.Errorf("failed write manifest file: %w", err) + } + + return fm.Close() +} diff --git a/pkg/cmd/extension/symlink_other.go b/pkg/cmd/extension/symlink_other.go new file mode 100644 index 00000000000..f426d031caa --- /dev/null +++ b/pkg/cmd/extension/symlink_other.go @@ -0,0 +1,9 @@ +// +build !windows + +package extension + +import "os" + +func makeSymlink(oldname, newname string) error { + return os.Symlink(oldname, newname) +} diff --git a/pkg/cmd/extension/symlink_windows.go b/pkg/cmd/extension/symlink_windows.go new file mode 100644 index 00000000000..5f29e3fb194 --- /dev/null +++ b/pkg/cmd/extension/symlink_windows.go @@ -0,0 +1,15 @@ +package extension + +import "os" + +func makeSymlink(oldname, newname string) error { + // Create a regular file that contains the location of the directory where to find this extension. We + // avoid relying on symlinks because creating them on Windows requires administrator privileges. + f, err := os.OpenFile(newname, os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return err + } + defer f.Close() + _, err = f.WriteString(oldname) + return err +} diff --git a/pkg/cmd/factory/default.go b/pkg/cmd/factory/default.go index ae8622b2aae..08d93c2be5d 100644 --- a/pkg/cmd/factory/default.go +++ b/pkg/cmd/factory/default.go @@ -5,20 +5,172 @@ import ( "fmt" "net/http" "os" + "path/filepath" + "time" - "github.com/cli/cli/git" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/internal/ghrepo" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/context" + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/extension" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" ) func New(appVersion string) *cmdutil.Factory { - io := iostreams.System() + var exe string + f := &cmdutil.Factory{ + Config: configFunc(), // No factory dependencies + Branch: branchFunc(), // No factory dependencies + Executable: func() string { + if exe != "" { + return exe + } + exe = executable("gh") + return exe + }, + } + + f.IOStreams = ioStreams(f) // Depends on Config + f.HttpClient = httpClientFunc(f, appVersion) // Depends on Config, IOStreams, and appVersion + f.Remotes = remotesFunc(f) // Depends on Config + f.BaseRepo = BaseRepoFunc(f) // Depends on Remotes + f.Browser = browser(f) // Depends on Config, and IOStreams + f.ExtensionManager = extensionManager(f) // Depends on Config, HttpClient, and IOStreams + + return f +} + +func BaseRepoFunc(f *cmdutil.Factory) func() (ghrepo.Interface, error) { + return func() (ghrepo.Interface, error) { + remotes, err := f.Remotes() + if err != nil { + return nil, err + } + return remotes[0], nil + } +} + +func SmartBaseRepoFunc(f *cmdutil.Factory) func() (ghrepo.Interface, error) { + return func() (ghrepo.Interface, error) { + httpClient, err := f.HttpClient() + if err != nil { + return nil, err + } + + apiClient := api.NewClientFromHTTP(httpClient) + + remotes, err := f.Remotes() + if err != nil { + return nil, err + } + repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "") + if err != nil { + return nil, err + } + baseRepo, err := repoContext.BaseRepo(f.IOStreams) + if err != nil { + return nil, err + } + + return baseRepo, nil + } +} + +func remotesFunc(f *cmdutil.Factory) func() (context.Remotes, error) { + rr := &remoteResolver{ + readRemotes: git.Remotes, + getConfig: f.Config, + } + return rr.Resolver() +} + +func httpClientFunc(f *cmdutil.Factory, appVersion string) func() (*http.Client, error) { + return func() (*http.Client, error) { + io := f.IOStreams + cfg, err := f.Config() + if err != nil { + return nil, err + } + return NewHTTPClient(io, cfg, appVersion, true) + } +} + +func browser(f *cmdutil.Factory) cmdutil.Browser { + io := f.IOStreams + return cmdutil.NewBrowser(browserLauncher(f), io.Out, io.ErrOut) +} + +// Browser precedence +// 1. GH_BROWSER +// 2. browser from config +// 3. BROWSER +func browserLauncher(f *cmdutil.Factory) string { + if ghBrowser := os.Getenv("GH_BROWSER"); ghBrowser != "" { + return ghBrowser + } + + cfg, err := f.Config() + if err == nil { + if cfgBrowser, _ := cfg.Get("", "browser"); cfgBrowser != "" { + return cfgBrowser + } + } + + return os.Getenv("BROWSER") +} + +// Finds the location of the executable for the current process as it's found in PATH, respecting symlinks. +// If the process couldn't determine its location, return fallbackName. If the executable wasn't found in +// PATH, return the absolute location to the program. +// +// The idea is that the result of this function is callable in the future and refers to the same +// installation of gh, even across upgrades. This is needed primarily for Homebrew, which installs software +// under a location such as `/usr/local/Cellar/gh/1.13.1/bin/gh` and symlinks it from `/usr/local/bin/gh`. +// When the version is upgraded, Homebrew will often delete older versions, but keep the symlink. Because of +// this, we want to refer to the `gh` binary as `/usr/local/bin/gh` and not as its internal Homebrew +// location. +// +// None of this would be needed if we could just refer to GitHub CLI as `gh`, i.e. without using an absolute +// path. However, for some reason Homebrew does not include `/usr/local/bin` in PATH when it invokes git +// commands to update its taps. If `gh` (no path) is being used as git credential helper, as set up by `gh +// auth login`, running `brew update` will print out authentication errors as git is unable to locate +// Homebrew-installed `gh`. +func executable(fallbackName string) string { + exe, err := os.Executable() + if err != nil { + return fallbackName + } + + base := filepath.Base(exe) + path := os.Getenv("PATH") + for _, dir := range filepath.SplitList(path) { + p, err := filepath.Abs(filepath.Join(dir, base)) + if err != nil { + continue + } + f, err := os.Stat(p) + if err != nil { + continue + } + + if p == exe { + return p + } else if f.Mode()&os.ModeSymlink != 0 { + if t, err := os.Readlink(p); err == nil && t == exe { + return p + } + } + } + return exe +} + +func configFunc() func() (config.Config, error) { var cachedConfig config.Config var configError error - configFunc := func() (config.Config, error) { + return func() (config.Config, error) { if cachedConfig != nil || configError != nil { return cachedConfig, configError } @@ -30,38 +182,57 @@ func New(appVersion string) *cmdutil.Factory { cachedConfig = config.InheritEnv(cachedConfig) return cachedConfig, configError } +} - rr := &remoteResolver{ - readRemotes: git.Remotes, - getConfig: configFunc, - } - remotesFunc := rr.Resolver() - - return &cmdutil.Factory{ - IOStreams: io, - Config: configFunc, - Remotes: remotesFunc, - HttpClient: func() (*http.Client, error) { - cfg, err := configFunc() - if err != nil { - return nil, err - } +func branchFunc() func() (string, error) { + return func() (string, error) { + currentBranch, err := git.CurrentBranch() + if err != nil { + return "", fmt.Errorf("could not determine current branch: %w", err) + } + return currentBranch, nil + } +} - return NewHTTPClient(io, cfg, appVersion, true), nil - }, - BaseRepo: func() (ghrepo.Interface, error) { - remotes, err := remotesFunc() - if err != nil { - return nil, err - } - return remotes[0], nil - }, - Branch: func() (string, error) { - currentBranch, err := git.CurrentBranch() - if err != nil { - return "", fmt.Errorf("could not determine current branch: %w", err) - } - return currentBranch, nil - }, +func extensionManager(f *cmdutil.Factory) *extension.Manager { + em := extension.NewManager(f.IOStreams) + + cfg, err := f.Config() + if err != nil { + return em + } + em.SetConfig(cfg) + + client, err := f.HttpClient() + if err != nil { + return em } + + em.SetClient(api.NewCachedClient(client, time.Second*30)) + + return em +} + +func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams { + io := iostreams.System() + cfg, err := f.Config() + if err != nil { + return io + } + + if prompt, _ := cfg.Get("", "prompt"); prompt == "disabled" { + io.SetNeverPrompt(true) + } + + // Pager precedence + // 1. GH_PAGER + // 2. pager from config + // 3. PAGER + if ghPager, ghPagerExists := os.LookupEnv("GH_PAGER"); ghPagerExists { + io.SetPager(ghPager) + } else if pager, _ := cfg.Get("", "pager"); pager != "" { + io.SetPager(pager) + } + + return io } diff --git a/pkg/cmd/factory/default_test.go b/pkg/cmd/factory/default_test.go new file mode 100644 index 00000000000..d181628e777 --- /dev/null +++ b/pkg/cmd/factory/default_test.go @@ -0,0 +1,469 @@ +package factory + +import ( + "net/url" + "os" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/stretchr/testify/assert" +) + +func Test_BaseRepo(t *testing.T) { + orig_GH_HOST := os.Getenv("GH_HOST") + t.Cleanup(func() { + os.Setenv("GH_HOST", orig_GH_HOST) + }) + + tests := []struct { + name string + remotes git.RemoteSet + config config.Config + override string + wantsErr bool + wantsName string + wantsOwner string + wantsHost string + }{ + { + name: "matching remote", + remotes: git.RemoteSet{ + git.NewRemote("origin", "https://nonsense.com/owner/repo.git"), + }, + config: defaultConfig(), + wantsName: "repo", + wantsOwner: "owner", + wantsHost: "nonsense.com", + }, + { + name: "no matching remote", + remotes: git.RemoteSet{ + git.NewRemote("origin", "https://test.com/owner/repo.git"), + }, + config: defaultConfig(), + wantsErr: true, + }, + { + name: "override with matching remote", + remotes: git.RemoteSet{ + git.NewRemote("origin", "https://test.com/owner/repo.git"), + }, + config: defaultConfig(), + override: "test.com", + wantsName: "repo", + wantsOwner: "owner", + wantsHost: "test.com", + }, + { + name: "override with no matching remote", + remotes: git.RemoteSet{ + git.NewRemote("origin", "https://nonsense.com/owner/repo.git"), + }, + config: defaultConfig(), + override: "test.com", + wantsErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.override != "" { + os.Setenv("GH_HOST", tt.override) + } else { + os.Unsetenv("GH_HOST") + } + f := New("1") + rr := &remoteResolver{ + readRemotes: func() (git.RemoteSet, error) { + return tt.remotes, nil + }, + getConfig: func() (config.Config, error) { + return tt.config, nil + }, + } + f.Remotes = rr.Resolver() + f.BaseRepo = BaseRepoFunc(f) + repo, err := f.BaseRepo() + if tt.wantsErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.wantsName, repo.RepoName()) + assert.Equal(t, tt.wantsOwner, repo.RepoOwner()) + assert.Equal(t, tt.wantsHost, repo.RepoHost()) + }) + } +} + +func Test_SmartBaseRepo(t *testing.T) { + pu, _ := url.Parse("https://test.com/newowner/newrepo.git") + orig_GH_HOST := os.Getenv("GH_HOST") + t.Cleanup(func() { + os.Setenv("GH_HOST", orig_GH_HOST) + }) + + tests := []struct { + name string + remotes git.RemoteSet + config config.Config + override string + wantsErr bool + wantsName string + wantsOwner string + wantsHost string + }{ + { + name: "override with matching remote", + remotes: git.RemoteSet{ + git.NewRemote("origin", "https://test.com/owner/repo.git"), + }, + config: defaultConfig(), + override: "test.com", + wantsName: "repo", + wantsOwner: "owner", + wantsHost: "test.com", + }, + { + name: "override with matching remote and base resolution", + remotes: git.RemoteSet{ + &git.Remote{Name: "origin", + Resolved: "base", + FetchURL: pu, + PushURL: pu}, + }, + config: defaultConfig(), + override: "test.com", + wantsName: "newrepo", + wantsOwner: "newowner", + wantsHost: "test.com", + }, + { + name: "override with matching remote and nonbase resolution", + remotes: git.RemoteSet{ + &git.Remote{Name: "origin", + Resolved: "johnny/test", + FetchURL: pu, + PushURL: pu}, + }, + config: defaultConfig(), + override: "test.com", + wantsName: "test", + wantsOwner: "johnny", + wantsHost: "test.com", + }, + { + name: "override with no matching remote", + remotes: git.RemoteSet{ + git.NewRemote("origin", "https://example.com/owner/repo.git"), + }, + config: defaultConfig(), + override: "test.com", + wantsErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.override != "" { + os.Setenv("GH_HOST", tt.override) + } else { + os.Unsetenv("GH_HOST") + } + f := New("1") + rr := &remoteResolver{ + readRemotes: func() (git.RemoteSet, error) { + return tt.remotes, nil + }, + getConfig: func() (config.Config, error) { + return tt.config, nil + }, + } + f.Remotes = rr.Resolver() + f.BaseRepo = SmartBaseRepoFunc(f) + repo, err := f.BaseRepo() + if tt.wantsErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.wantsName, repo.RepoName()) + assert.Equal(t, tt.wantsOwner, repo.RepoOwner()) + assert.Equal(t, tt.wantsHost, repo.RepoHost()) + }) + } +} + +// Defined in pkg/cmdutil/repo_override.go but test it along with other BaseRepo functions +func Test_OverrideBaseRepo(t *testing.T) { + orig_GH_HOST := os.Getenv("GH_REPO") + t.Cleanup(func() { + os.Setenv("GH_REPO", orig_GH_HOST) + }) + + tests := []struct { + name string + remotes git.RemoteSet + config config.Config + envOverride string + argOverride string + wantsErr bool + wantsName string + wantsOwner string + wantsHost string + }{ + { + name: "override from argument", + argOverride: "override/test", + wantsHost: "github.com", + wantsOwner: "override", + wantsName: "test", + }, + { + name: "override from environment", + envOverride: "somehost.com/override/test", + wantsHost: "somehost.com", + wantsOwner: "override", + wantsName: "test", + }, + { + name: "no override", + remotes: git.RemoteSet{ + git.NewRemote("origin", "https://nonsense.com/owner/repo.git"), + }, + config: defaultConfig(), + wantsHost: "nonsense.com", + wantsOwner: "owner", + wantsName: "repo", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.envOverride != "" { + os.Setenv("GH_REPO", tt.envOverride) + } else { + os.Unsetenv("GH_REPO") + } + f := New("1") + rr := &remoteResolver{ + readRemotes: func() (git.RemoteSet, error) { + return tt.remotes, nil + }, + getConfig: func() (config.Config, error) { + return tt.config, nil + }, + } + f.Remotes = rr.Resolver() + f.BaseRepo = cmdutil.OverrideBaseRepoFunc(f, tt.argOverride) + repo, err := f.BaseRepo() + if tt.wantsErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.wantsName, repo.RepoName()) + assert.Equal(t, tt.wantsOwner, repo.RepoOwner()) + assert.Equal(t, tt.wantsHost, repo.RepoHost()) + }) + } +} + +func Test_ioStreams_pager(t *testing.T) { + tests := []struct { + name string + env map[string]string + config config.Config + wantPager string + }{ + { + name: "GH_PAGER and PAGER set", + env: map[string]string{ + "GH_PAGER": "GH_PAGER", + "PAGER": "PAGER", + }, + wantPager: "GH_PAGER", + }, + { + name: "GH_PAGER and config pager set", + env: map[string]string{ + "GH_PAGER": "GH_PAGER", + }, + config: pagerConfig(), + wantPager: "GH_PAGER", + }, + { + name: "config pager and PAGER set", + env: map[string]string{ + "PAGER": "PAGER", + }, + config: pagerConfig(), + wantPager: "CONFIG_PAGER", + }, + { + name: "only PAGER set", + env: map[string]string{ + "PAGER": "PAGER", + }, + wantPager: "PAGER", + }, + { + name: "GH_PAGER set to blank string", + env: map[string]string{ + "GH_PAGER": "", + "PAGER": "PAGER", + }, + wantPager: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.env != nil { + for k, v := range tt.env { + old := os.Getenv(k) + os.Setenv(k, v) + if k == "GH_PAGER" { + defer os.Unsetenv(k) + } else { + defer os.Setenv(k, old) + } + } + } + f := New("1") + f.Config = func() (config.Config, error) { + if tt.config == nil { + return config.NewBlankConfig(), nil + } else { + return tt.config, nil + } + } + io := ioStreams(f) + assert.Equal(t, tt.wantPager, io.GetPager()) + }) + } +} + +func Test_ioStreams_prompt(t *testing.T) { + tests := []struct { + name string + config config.Config + promptDisabled bool + }{ + { + name: "default config", + promptDisabled: false, + }, + { + name: "config with prompt disabled", + config: disablePromptConfig(), + promptDisabled: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := New("1") + f.Config = func() (config.Config, error) { + if tt.config == nil { + return config.NewBlankConfig(), nil + } else { + return tt.config, nil + } + } + io := ioStreams(f) + assert.Equal(t, tt.promptDisabled, io.GetNeverPrompt()) + }) + } +} + +func Test_browserLauncher(t *testing.T) { + tests := []struct { + name string + env map[string]string + config config.Config + wantBrowser string + }{ + { + name: "GH_BROWSER set", + env: map[string]string{ + "GH_BROWSER": "GH_BROWSER", + }, + wantBrowser: "GH_BROWSER", + }, + { + name: "config browser set", + config: config.NewFromString("browser: CONFIG_BROWSER"), + wantBrowser: "CONFIG_BROWSER", + }, + { + name: "BROWSER set", + env: map[string]string{ + "BROWSER": "BROWSER", + }, + wantBrowser: "BROWSER", + }, + { + name: "GH_BROWSER and config browser set", + env: map[string]string{ + "GH_BROWSER": "GH_BROWSER", + }, + config: config.NewFromString("browser: CONFIG_BROWSER"), + wantBrowser: "GH_BROWSER", + }, + { + name: "config browser and BROWSER set", + env: map[string]string{ + "BROWSER": "BROWSER", + }, + config: config.NewFromString("browser: CONFIG_BROWSER"), + wantBrowser: "CONFIG_BROWSER", + }, + { + name: "GH_BROWSER and BROWSER set", + env: map[string]string{ + "BROWSER": "BROWSER", + "GH_BROWSER": "GH_BROWSER", + }, + wantBrowser: "GH_BROWSER", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.env != nil { + for k, v := range tt.env { + old := os.Getenv(k) + os.Setenv(k, v) + defer os.Setenv(k, old) + } + } + f := New("1") + f.Config = func() (config.Config, error) { + if tt.config == nil { + return config.NewBlankConfig(), nil + } else { + return tt.config, nil + } + } + browser := browserLauncher(f) + assert.Equal(t, tt.wantBrowser, browser) + }) + } +} + +func defaultConfig() config.Config { + return config.InheritEnv(config.NewFromString(heredoc.Doc(` + hosts: + nonsense.com: + oauth_token: BLAH + `))) +} + +func pagerConfig() config.Config { + return config.NewFromString("pager: CONFIG_PAGER") +} + +func disablePromptConfig() config.Config { + return config.NewFromString("prompt: disabled") +} diff --git a/pkg/cmd/factory/http.go b/pkg/cmd/factory/http.go index 47fbbefe88a..fd61f2dc712 100644 --- a/pkg/cmd/factory/http.go +++ b/pkg/cmd/factory/http.go @@ -5,16 +5,84 @@ import ( "net/http" "os" "strings" + "time" - "github.com/cli/cli/api" - "github.com/cli/cli/internal/config" - "github.com/cli/cli/internal/ghinstance" - "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/internal/httpunix" + "github.com/cli/cli/v2/pkg/iostreams" ) +var timezoneNames = map[int]string{ + -39600: "Pacific/Niue", + -36000: "Pacific/Honolulu", + -34200: "Pacific/Marquesas", + -32400: "America/Anchorage", + -28800: "America/Los_Angeles", + -25200: "America/Chihuahua", + -21600: "America/Chicago", + -18000: "America/Bogota", + -14400: "America/Caracas", + -12600: "America/St_Johns", + -10800: "America/Argentina/Buenos_Aires", + -7200: "Atlantic/South_Georgia", + -3600: "Atlantic/Cape_Verde", + 0: "Europe/London", + 3600: "Europe/Amsterdam", + 7200: "Europe/Athens", + 10800: "Europe/Istanbul", + 12600: "Asia/Tehran", + 14400: "Asia/Dubai", + 16200: "Asia/Kabul", + 18000: "Asia/Tashkent", + 19800: "Asia/Kolkata", + 20700: "Asia/Kathmandu", + 21600: "Asia/Dhaka", + 23400: "Asia/Rangoon", + 25200: "Asia/Bangkok", + 28800: "Asia/Manila", + 31500: "Australia/Eucla", + 32400: "Asia/Tokyo", + 34200: "Australia/Darwin", + 36000: "Australia/Brisbane", + 37800: "Australia/Adelaide", + 39600: "Pacific/Guadalcanal", + 43200: "Pacific/Nauru", + 46800: "Pacific/Auckland", + 49500: "Pacific/Chatham", + 50400: "Pacific/Kiritimati", +} + +type configGetter interface { + Get(string, string) (string, error) +} + // generic authenticated HTTP client for commands -func NewHTTPClient(io *iostreams.IOStreams, cfg config.Config, appVersion string, setAccept bool) *http.Client { +func NewHTTPClient(io *iostreams.IOStreams, cfg configGetter, appVersion string, setAccept bool) (*http.Client, error) { var opts []api.ClientOption + + // We need to check and potentially add the unix socket roundtripper option + // before adding any other options, since if we are going to use the unix + // socket transport, it needs to form the base of the transport chain + // represented by invocations of opts... + // + // Another approach might be to change the signature of api.NewHTTPClient to + // take an explicit base http.RoundTripper as its first parameter (it + // currently defaults internally to http.DefaultTransport), or add another + // variant like api.NewHTTPClientWithBaseRoundTripper. But, the only caller + // which would use that non-default behavior is right here, and it doesn't + // seem worth the cognitive overhead everywhere else just to serve this one + // use case. + unixSocket, err := cfg.Get("", "http_unix_socket") + if err != nil { + return nil, err + } + if unixSocket != "" { + opts = append(opts, api.ClientOption(func(http.RoundTripper) http.RoundTripper { + return httpunix.NewRoundTripper(unixSocket) + })) + } + if verbose := os.Getenv("DEBUG"); verbose != "" { logTraffic := strings.Contains(verbose, "api") opts = append(opts, api.VerboseLog(io.ErrOut, logTraffic, io.IsStderrTTY())) @@ -23,27 +91,44 @@ func NewHTTPClient(io *iostreams.IOStreams, cfg config.Config, appVersion string opts = append(opts, api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", appVersion)), api.AddHeaderFunc("Authorization", func(req *http.Request) (string, error) { - hostname := ghinstance.NormalizeHostname(req.URL.Hostname()) + hostname := ghinstance.NormalizeHostname(getHost(req)) if token, err := cfg.Get(hostname, "oauth_token"); err == nil && token != "" { return fmt.Sprintf("token %s", token), nil } return "", nil }), + api.AddHeaderFunc("Time-Zone", func(req *http.Request) (string, error) { + if req.Method != "GET" && req.Method != "HEAD" { + if time.Local.String() != "Local" { + return time.Local.String(), nil + } + _, offset := time.Now().Zone() + return timezoneNames[offset], nil + } + return "", nil + }), ) if setAccept { opts = append(opts, api.AddHeaderFunc("Accept", func(req *http.Request) (string, error) { - // antiope-preview: Checks - accept := "application/vnd.github.antiope-preview+json" - if ghinstance.IsEnterprise(req.URL.Hostname()) { - // shadow-cat-preview: Draft pull requests - accept += ", application/vnd.github.shadow-cat-preview" + accept := "application/vnd.github.merge-info-preview+json" // PullRequest.mergeStateStatus + accept += ", application/vnd.github.nebula-preview" // visibility when RESTing repos into an org + if ghinstance.IsEnterprise(getHost(req)) { + accept += ", application/vnd.github.antiope-preview" // Commit.statusCheckRollup + accept += ", application/vnd.github.shadow-cat-preview" // PullRequest.isDraft } return accept, nil }), ) } - return api.NewHTTPClient(opts...) + return api.NewHTTPClient(opts...), nil +} + +func getHost(r *http.Request) string { + if r.Host != "" { + return r.Host + } + return r.URL.Hostname() } diff --git a/pkg/cmd/factory/http_test.go b/pkg/cmd/factory/http_test.go new file mode 100644 index 00000000000..1505d1b65c3 --- /dev/null +++ b/pkg/cmd/factory/http_test.go @@ -0,0 +1,175 @@ +package factory + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "regexp" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewHTTPClient(t *testing.T) { + type args struct { + config configGetter + appVersion string + setAccept bool + } + tests := []struct { + name string + args args + envDebug string + host string + wantHeader map[string]string + wantStderr string + }{ + { + name: "github.com with Accept header", + args: args{ + config: tinyConfig{"github.com:oauth_token": "MYTOKEN"}, + appVersion: "v1.2.3", + setAccept: true, + }, + host: "github.com", + wantHeader: map[string]string{ + "authorization": "token MYTOKEN", + "user-agent": "GitHub CLI v1.2.3", + "accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview", + }, + wantStderr: "", + }, + { + name: "github.com no Accept header", + args: args{ + config: tinyConfig{"github.com:oauth_token": "MYTOKEN"}, + appVersion: "v1.2.3", + setAccept: false, + }, + host: "github.com", + wantHeader: map[string]string{ + "authorization": "token MYTOKEN", + "user-agent": "GitHub CLI v1.2.3", + "accept": "", + }, + wantStderr: "", + }, + { + name: "github.com no authentication token", + args: args{ + config: tinyConfig{"example.com:oauth_token": "MYTOKEN"}, + appVersion: "v1.2.3", + setAccept: true, + }, + host: "github.com", + wantHeader: map[string]string{ + "authorization": "", + "user-agent": "GitHub CLI v1.2.3", + "accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview", + }, + wantStderr: "", + }, + { + name: "github.com in verbose mode", + args: args{ + config: tinyConfig{"github.com:oauth_token": "MYTOKEN"}, + appVersion: "v1.2.3", + setAccept: true, + }, + host: "github.com", + envDebug: "api", + wantHeader: map[string]string{ + "authorization": "token MYTOKEN", + "user-agent": "GitHub CLI v1.2.3", + "accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview", + }, + wantStderr: heredoc.Doc(` + * Request at