Merge branch 'trunk' into 8426-add-pr-update-cmd-no-local-update

This commit is contained in:
Babak K. Shandiz 2024-06-07 15:40:28 +01:00
commit 8ac5ad7244
No known key found for this signature in database
GPG key ID: 44950AED81AD710F
204 changed files with 3240 additions and 1131 deletions

View file

@ -6,7 +6,9 @@ concurrency:
cancel-in-progress: true
permissions:
attestations: write
contents: write
id-token: write
on:
workflow_dispatch:
@ -108,6 +110,19 @@ jobs:
run: |
shopt -s failglob
script/sign dist/gh_*_macOS_*.zip
- name: Build universal macOS pkg installer
if: inputs.environment != 'production'
env:
TAG_NAME: ${{ inputs.tag_name }}
run: script/pkgmacos "$TAG_NAME"
- name: Build & notarize universal macOS pkg installer
if: inputs.environment == 'production'
env:
TAG_NAME: ${{ inputs.tag_name }}
APPLE_DEVELOPER_INSTALLER_ID: ${{ vars.APPLE_DEVELOPER_INSTALLER_ID }}
run: |
shopt -s failglob
script/pkgmacos "$TAG_NAME"
- uses: actions/upload-artifact@v4
with:
name: macos
@ -116,7 +131,8 @@ jobs:
path: |
dist/*.tar.gz
dist/*.zip
dist/*.pkg
windows:
runs-on: windows-latest
environment: ${{ inputs.environment }}
@ -281,6 +297,11 @@ jobs:
run: |
cp script/rpmmacros ~/.rpmmacros
rpmsign --addsign dist/*.rpm
- name: Attest release artifacts
if: inputs.environment == 'production'
uses: actions/attest-build-provenance@173725a1209d09b31f9d30a3890cf2757ebbff0d # v1.1.2
with:
subject-path: "dist/gh_*"
- name: Run createrepo
env:
GPG_SIGN: ${{ inputs.environment == 'production' }}

2
.gitignore vendored
View file

@ -12,6 +12,8 @@
/.goreleaser.generated.yml
/script/build
/script/build.exe
/pkg_payload
/build/macOS/resources
# VS Code
.vscode

View file

@ -7,11 +7,10 @@ release:
before:
hooks:
- >-
- >- # The linux and windows archives package the manpages below
{{ if eq .Runtime.Goos "windows" }}echo{{ end }} make manpages GH_VERSION={{.Version}}
- >-
{{ if ne .Runtime.Goos "linux" }}echo{{ end }} make completions
- >- # On linux the completions are used in nfpms below, but on macos they are used outside in the deployment build.
{{ if eq .Runtime.Goos "windows" }}echo{{ end }} make completions
builds:
- id: macos #build:macos
goos: [darwin]

View file

@ -93,3 +93,11 @@ uninstall:
rm -f ${DESTDIR}${datadir}/bash-completion/completions/gh
rm -f ${DESTDIR}${datadir}/fish/vendor_completions.d/gh.fish
rm -f ${DESTDIR}${datadir}/zsh/site-functions/_gh
.PHONY: macospkg
macospkg: manpages completions
ifndef VERSION
$(error VERSION is not set. Use `make macospkg VERSION=vX.Y.Z`)
endif
./script/release --local "$(VERSION)" --platform macos
./script/pkgmacos $(VERSION)

View file

@ -21,7 +21,10 @@ If you are a hubber and are interested in shipping new commands for the CLI, che
### macOS
`gh` is available via [Homebrew][], [MacPorts][], [Conda][], [Spack][], [Webi][], and as a downloadable binary from the [releases page][].
`gh` is available via [Homebrew][], [MacPorts][], [Conda][], [Spack][], [Webi][], and as a downloadable binary including Mac OS installer `.pkg` from the [releases page][].
> [!NOTE]
> As of May 29th, Mac OS installer `.pkg` are unsigned with efforts prioritized in [`cli/cli#9139`](https://github.com/cli/cli/issues/9139) to support signing them.
#### Homebrew

View file

@ -268,6 +268,7 @@ var IssueFields = []string{
"title",
"updatedAt",
"url",
"stateReason",
}
var PullRequestFields = append(IssueFields,
@ -298,6 +299,12 @@ var PullRequestFields = append(IssueFields,
"statusCheckRollup",
)
// Some fields are only valid in the context of issues.
var issueOnlyFields = []string{
"isPinned",
"stateReason",
}
// IssueGraphQL constructs a GraphQL query fragment for a set of issue fields.
func IssueGraphQL(fields []string) string {
var q []string
@ -363,10 +370,9 @@ func IssueGraphQL(fields []string) string {
// PullRequestGraphQL constructs a GraphQL query fragment for a set of pull request fields.
// It will try to sanitize the fields to just those available on pull request.
func PullRequestGraphQL(fields []string) string {
invalidFields := []string{"isPinned", "stateReason"}
s := set.NewStringSet()
s.AddValues(fields)
s.RemoveValues(invalidFields)
s.RemoveValues(issueOnlyFields)
return IssueGraphQL(s.ToSlice())
}

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<installer-gui-script minSpecVersion="2">
<title>GitHub CLI</title>
<license file="LICENSE" mime-type="text/plain"/>
<options hostArchitectures="arm64,x86_64" customize="never" require-scripts="true" allow-external-scripts="false"/>
<domains enable_localSystem="true"/>
<installation-check script="installCheck();"/>
<script>
function installCheck() {
// this check is redundant, but it produces a user friendly error message
// compared to a disabled install button caused by allowed-os-versions
if (!(system.compareVersions(system.version.ProductVersion, '12') &gt;= 0)) {
my.result.title = 'Unable to install';
my.result.message = 'GitHub CLI requires macOS 12 or later.';
my.result.type = 'Fatal';
return false;
}
return true;
}
</script>
<allowed-os-versions>
<os-version min="12.0" />
</allowed-os-versions>
<choices-outline>
<line choice="gh-cli"/>
</choices-outline>
<choice id="gh-cli" title="GitHub CLI (universal)">
<pkg-ref id="com.github.cli.pkg"/>
</choice>
<pkg-ref id="com.github.cli.pkg" auth="root">#com.github.cli.pkg</pkg-ref>
</installer-gui-script>

View file

@ -9,6 +9,7 @@ import (
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/docs"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/root"
"github.com/cli/cli/v2/pkg/cmdutil"
@ -48,7 +49,7 @@ func run(args []string) error {
rootCmd, _ := root.NewCmdRoot(&cmdutil.Factory{
IOStreams: ios,
Browser: &browser{},
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return config.NewFromString(""), nil
},
ExtensionManager: &em{},

36
docs/codespaces.md Normal file
View file

@ -0,0 +1,36 @@
# Guide to working with Codespaces using the CLI
For more information on Codespaces, see [Codespaces section in GitHub Docs](https://docs.github.com/en/codespaces).
## Access to other repositories
The codespace creation process will prompt you to review and authorize additional permissions defined in
`devcontainer.json` at creation time:
```json
{
"customizations": {
"codespaces": {
"repositories": {
"my_org/my_repo": {
"permissions": {
"issues": "write"
}
}
}
}
}
}
```
However, any changes to `codespaces` customizations will not be re-evaluated for an existing
codespace. This requires you to create a new codespace in order to authorize the new
permissions using `gh codespace create`.
For more information, see ["Repository access"](https://docs.github.com/en/codespaces/managing-your-codespaces/managing-repository-access-for-your-codespaces).
If additional access is needed for an existing codespace or access to a repository outside of
your user or organization account, the use of a fine-grained personal access token as an
environment variable or Codespaces secret might be considered.
For more information, see ["Authenticating to repositories"](https://docs.github.com/en/codespaces/troubleshooting/troubleshooting-authentication-to-a-repository).

View file

@ -40,7 +40,7 @@ Install from our package repository for immediate access to latest releases:
```bash
sudo dnf install 'dnf-command(config-manager)'
sudo dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo
sudo dnf install gh
sudo dnf install gh --repo gh-cli
```
Alternatively, install from the [community repository](https://packages.fedoraproject.org/pkgs/gh/gh/):

18
go.mod
View file

@ -17,7 +17,7 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.4
github.com/creack/pty v1.1.21
github.com/distribution/reference v0.5.0
github.com/gabriel-vasile/mimetype v1.4.3
github.com/gabriel-vasile/mimetype v1.4.4
github.com/gdamore/tcell/v2 v2.5.4
github.com/google/go-cmp v0.6.0
github.com/google/go-containerregistry v0.19.1
@ -37,18 +37,18 @@ require (
github.com/opentracing/opentracing-go v1.2.0
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc
github.com/sigstore/protobuf-specs v0.3.1
github.com/sigstore/protobuf-specs v0.3.2
github.com/sigstore/sigstore-go v0.3.0
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0
github.com/zalando/go-keyring v0.2.4
golang.org/x/crypto v0.22.0
github.com/zalando/go-keyring v0.2.5
golang.org/x/crypto v0.23.0
golang.org/x/sync v0.6.0
golang.org/x/term v0.19.0
golang.org/x/text v0.14.0
golang.org/x/term v0.20.0
golang.org/x/text v0.15.0
google.golang.org/grpc v1.62.2
google.golang.org/protobuf v1.33.0
google.golang.org/protobuf v1.34.1
gopkg.in/h2non/gock.v1 v1.1.2
gopkg.in/yaml.v3 v3.0.1
)
@ -158,8 +158,8 @@ require (
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240311173647-c811ad7063a7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect

36
go.sum
View file

@ -149,8 +149,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
@ -384,8 +384,8 @@ github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc h1:vH0NQbIDk+mJL
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
github.com/sigstore/protobuf-specs v0.3.1 h1:9aJQrPq7iRDSLBNg//zsP7tAzxdHnD1sA+1FyCCrkrQ=
github.com/sigstore/protobuf-specs v0.3.1/go.mod h1:HfkcPi5QXteuew4+c5ONz8vYQ8aOH//ZTQ3gg0X8ZUA=
github.com/sigstore/protobuf-specs v0.3.2 h1:nCVARCN+fHjlNCk3ThNXwrZRqIommIeNKWwQvORuRQo=
github.com/sigstore/protobuf-specs v0.3.2/go.mod h1:RZ0uOdJR4OB3tLQeAyWoJFbNCBFrPQdcokntde4zRBA=
github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8=
github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc=
github.com/sigstore/sigstore v1.8.3 h1:G7LVXqL+ekgYtYdksBks9B38dPoIsbscjQJX/MGWkA4=
@ -453,8 +453,8 @@ github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68=
github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8=
github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
@ -481,8 +481,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@ -491,8 +491,8 @@ golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -510,19 +510,19 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -543,8 +543,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.62.2 h1:iEIj1U5qjyBjzkM5nk3Fq+S1IbjbXSyqeULZ1Nfo4AA=
google.golang.org/grpc v1.62.2/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View file

@ -12,6 +12,8 @@ import (
"testing"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
ghmock "github.com/cli/cli/v2/internal/gh/mock"
"github.com/cli/cli/v2/pkg/cmdutil"
)
@ -137,13 +139,13 @@ func createHttpClient() (*http.Client, error) {
func TestNew_APIURL_dotcomConfig(t *testing.T) {
t.Setenv("GITHUB_API_URL", "")
t.Setenv("GITHUB_SERVER_URL", "https://github.com")
cfg := &config.ConfigMock{
AuthenticationFunc: func() *config.AuthConfig {
cfg := &ghmock.ConfigMock{
AuthenticationFunc: func() gh.AuthConfig {
return &config.AuthConfig{}
},
}
f := &cmdutil.Factory{
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return cfg, nil
},
}
@ -160,15 +162,15 @@ func TestNew_APIURL_dotcomConfig(t *testing.T) {
func TestNew_APIURL_customConfig(t *testing.T) {
t.Setenv("GITHUB_API_URL", "")
t.Setenv("GITHUB_SERVER_URL", "https://github.mycompany.com")
cfg := &config.ConfigMock{
AuthenticationFunc: func() *config.AuthConfig {
cfg := &ghmock.ConfigMock{
AuthenticationFunc: func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
authCfg.SetDefaultHost("github.mycompany.com", "GH_HOST")
return authCfg
},
}
f := &cmdutil.Factory{
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return cfg, nil
},
}
@ -185,13 +187,13 @@ func TestNew_APIURL_customConfig(t *testing.T) {
func TestNew_APIURL_env(t *testing.T) {
t.Setenv("GITHUB_API_URL", "https://api.mycompany.com")
t.Setenv("GITHUB_SERVER_URL", "https://mycompany.com")
cfg := &config.ConfigMock{
AuthenticationFunc: func() *config.AuthConfig {
cfg := &ghmock.ConfigMock{
AuthenticationFunc: func() gh.AuthConfig {
return &config.AuthConfig{}
},
}
f := &cmdutil.Factory{
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return cfg, nil
},
}
@ -208,7 +210,7 @@ func TestNew_APIURL_env(t *testing.T) {
func TestNew_APIURL_dotcomFallback(t *testing.T) {
t.Setenv("GITHUB_API_URL", "")
f := &cmdutil.Factory{
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return nil, errors.New("Failed to load")
},
}
@ -222,13 +224,13 @@ func TestNew_APIURL_dotcomFallback(t *testing.T) {
func TestNew_ServerURL_dotcomConfig(t *testing.T) {
t.Setenv("GITHUB_SERVER_URL", "")
t.Setenv("GITHUB_API_URL", "https://api.github.com")
cfg := &config.ConfigMock{
AuthenticationFunc: func() *config.AuthConfig {
cfg := &ghmock.ConfigMock{
AuthenticationFunc: func() gh.AuthConfig {
return &config.AuthConfig{}
},
}
f := &cmdutil.Factory{
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return cfg, nil
},
}
@ -245,15 +247,15 @@ func TestNew_ServerURL_dotcomConfig(t *testing.T) {
func TestNew_ServerURL_customConfig(t *testing.T) {
t.Setenv("GITHUB_SERVER_URL", "")
t.Setenv("GITHUB_API_URL", "https://github.mycompany.com/api/v3")
cfg := &config.ConfigMock{
AuthenticationFunc: func() *config.AuthConfig {
cfg := &ghmock.ConfigMock{
AuthenticationFunc: func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
authCfg.SetDefaultHost("github.mycompany.com", "GH_HOST")
return authCfg
},
}
f := &cmdutil.Factory{
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return cfg, nil
},
}
@ -270,13 +272,13 @@ func TestNew_ServerURL_customConfig(t *testing.T) {
func TestNew_ServerURL_env(t *testing.T) {
t.Setenv("GITHUB_SERVER_URL", "https://mycompany.com")
t.Setenv("GITHUB_API_URL", "https://api.mycompany.com")
cfg := &config.ConfigMock{
AuthenticationFunc: func() *config.AuthConfig {
cfg := &ghmock.ConfigMock{
AuthenticationFunc: func() gh.AuthConfig {
return &config.AuthConfig{}
},
}
f := &cmdutil.Factory{
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return cfg, nil
},
}
@ -293,7 +295,7 @@ func TestNew_ServerURL_env(t *testing.T) {
func TestNew_ServerURL_dotcomFallback(t *testing.T) {
t.Setenv("GITHUB_SERVER_URL", "")
f := &cmdutil.Factory{
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return nil, errors.New("Failed to load")
},
}

View file

@ -13,7 +13,7 @@ import (
// Note that NewIsolatedTestConfig sets up a Mock keyring as well
func newTestAuthConfig(t *testing.T) *AuthConfig {
cfg, _ := NewIsolatedTestConfig(t)
return cfg.Authentication()
return &AuthConfig{cfg: cfg.cfg}
}
func TestTokenFromKeyring(t *testing.T) {

View file

@ -7,7 +7,9 @@ import (
"path/filepath"
"slices"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/keyring"
o "github.com/cli/cli/v2/pkg/option"
ghAuth "github.com/cli/go-gh/v2/pkg/auth"
ghConfig "github.com/cli/go-gh/v2/pkg/config"
)
@ -27,49 +29,7 @@ const (
versionKey = "version"
)
// This interface describes interacting with some persistent configuration for gh.
//
//go:generate moq -rm -out config_mock.go . Config
type Config interface {
GetOrDefault(string, string) (string, error)
Set(string, string, string)
Write() error
Migrate(Migration) error
CacheDir() string
Aliases() *AliasConfig
Authentication() *AuthConfig
Browser(string) string
Editor(string) string
GitProtocol(string) string
HTTPUnixSocket(string) string
Pager(string) string
Prompt(string) string
Version() string
}
// Migration is the interface that config migrations must implement.
//
// Migrations will receive a copy of the config, and should modify that copy
// as necessary. After migration has completed, the modified config contents
// will be used.
//
// The calling code is expected to verify that the current version of the config
// matches the PreVersion of the migration before calling Do, and will set the
// config version to the PostVersion after the migration has completed successfully.
//
//go:generate moq -rm -out migration_mock.go . Migration
type Migration interface {
// PreVersion is the required config version for this to be applied
PreVersion() string
// PostVersion is the config version that must be applied after migration
PostVersion() string
// Do is expected to apply any necessary changes to the config in place
Do(*ghConfig.Config) error
}
func NewConfig() (Config, error) {
func NewConfig() (gh.Config, error) {
c, err := ghConfig.Read(fallbackConfig())
if err != nil {
return nil, err
@ -82,28 +42,44 @@ type cfg struct {
cfg *ghConfig.Config
}
func (c *cfg) Get(hostname, key string) (string, error) {
func (c *cfg) get(hostname, key string) o.Option[string] {
if hostname != "" {
val, err := c.cfg.Get([]string{hostsKey, hostname, key})
if err == nil {
return val, err
return o.Some(val)
}
}
return c.cfg.Get([]string{key})
val, err := c.cfg.Get([]string{key})
if err == nil {
return o.Some(val)
}
return o.None[string]()
}
func (c *cfg) GetOrDefault(hostname, key string) (string, error) {
val, err := c.Get(hostname, key)
if err == nil {
return val, err
func (c *cfg) GetOrDefault(hostname, key string) o.Option[gh.ConfigEntry] {
if val := c.get(hostname, key); val.IsSome() {
// Map the Option[string] to Option[gh.ConfigEntry] with a source of ConfigUserProvided
return o.Map(val, toConfigEntry(gh.ConfigUserProvided))
}
if val, ok := defaultFor(key); ok {
return val, nil
if defaultVal := defaultFor(key); defaultVal.IsSome() {
// Map the Option[string] to Option[gh.ConfigEntry] with a source of ConfigDefaultProvided
return o.Map(defaultVal, toConfigEntry(gh.ConfigDefaultProvided))
}
return val, err
return o.None[gh.ConfigEntry]()
}
// toConfigEntry is a helper function to convert a string value to a ConfigEntry with a given source.
//
// It's a bit of FP style but it allows us to map an Option[string] to Option[gh.ConfigEntry] without
// unwrapping the it and rewrapping it.
func toConfigEntry(source gh.ConfigSource) func(val string) gh.ConfigEntry {
return func(val string) gh.ConfigEntry {
return gh.ConfigEntry{Value: val, Source: source}
}
}
func (c *cfg) Set(hostname, key, value string) {
@ -123,51 +99,52 @@ func (c *cfg) Write() error {
return ghConfig.Write(c.cfg)
}
func (c *cfg) Aliases() *AliasConfig {
func (c *cfg) Aliases() gh.AliasConfig {
return &AliasConfig{cfg: c.cfg}
}
func (c *cfg) Authentication() *AuthConfig {
func (c *cfg) Authentication() gh.AuthConfig {
return &AuthConfig{cfg: c.cfg}
}
func (c *cfg) Browser(hostname string) string {
val, _ := c.GetOrDefault(hostname, browserKey)
return val
func (c *cfg) Browser(hostname string) gh.ConfigEntry {
// Intentionally panic if there is no user provided value or default value (which would be a programmer error)
return c.GetOrDefault(hostname, browserKey).Unwrap()
}
func (c *cfg) Editor(hostname string) string {
val, _ := c.GetOrDefault(hostname, editorKey)
return val
func (c *cfg) Editor(hostname string) gh.ConfigEntry {
// Intentionally panic if there is no user provided value or default value (which would be a programmer error)
return c.GetOrDefault(hostname, editorKey).Unwrap()
}
func (c *cfg) GitProtocol(hostname string) string {
val, _ := c.GetOrDefault(hostname, gitProtocolKey)
return val
func (c *cfg) GitProtocol(hostname string) gh.ConfigEntry {
// Intentionally panic if there is no user provided value or default value (which would be a programmer error)
return c.GetOrDefault(hostname, gitProtocolKey).Unwrap()
}
func (c *cfg) HTTPUnixSocket(hostname string) string {
val, _ := c.GetOrDefault(hostname, httpUnixSocketKey)
return val
func (c *cfg) HTTPUnixSocket(hostname string) gh.ConfigEntry {
// Intentionally panic if there is no user provided value or default value (which would be a programmer error)
return c.GetOrDefault(hostname, httpUnixSocketKey).Unwrap()
}
func (c *cfg) Pager(hostname string) string {
val, _ := c.GetOrDefault(hostname, pagerKey)
return val
func (c *cfg) Pager(hostname string) gh.ConfigEntry {
// Intentionally panic if there is no user provided value or default value (which would be a programmer error)
return c.GetOrDefault(hostname, pagerKey).Unwrap()
}
func (c *cfg) Prompt(hostname string) string {
val, _ := c.GetOrDefault(hostname, promptKey)
return val
func (c *cfg) Prompt(hostname string) gh.ConfigEntry {
// Intentionally panic if there is no user provided value or default value (which would be a programmer error)
return c.GetOrDefault(hostname, promptKey).Unwrap()
}
func (c *cfg) Version() string {
val, _ := c.GetOrDefault("", versionKey)
return val
func (c *cfg) Version() o.Option[string] {
return c.get("", versionKey)
}
func (c *cfg) Migrate(m Migration) error {
version := c.Version()
func (c *cfg) Migrate(m gh.Migration) error {
// If there is no version entry we must never have applied a migration, and the following conditional logic
// handles the version as an empty string correctly.
version := c.Version().UnwrapOrZero()
// If migration has already occurred then do not attempt to migrate again.
if m.PostVersion() == version {
@ -197,13 +174,13 @@ func (c *cfg) CacheDir() string {
return ghConfig.CacheDir()
}
func defaultFor(key string) (string, bool) {
for _, co := range ConfigOptions() {
func defaultFor(key string) o.Option[string] {
for _, co := range Options {
if co.Key == key {
return co.DefaultValue, true
return o.Some(co.DefaultValue)
}
}
return "", false
return o.None[string]()
}
// AuthConfig is used for interacting with some persistent configuration for gh,
@ -548,43 +525,60 @@ type ConfigOption struct {
Description string
DefaultValue string
AllowedValues []string
CurrentValue func(c gh.Config, hostname string) string
}
func ConfigOptions() []ConfigOption {
return []ConfigOption{
{
Key: gitProtocolKey,
Description: "the protocol to use for git clone and push operations",
DefaultValue: "https",
AllowedValues: []string{"https", "ssh"},
var Options = []ConfigOption{
{
Key: gitProtocolKey,
Description: "the protocol to use for git clone and push operations",
DefaultValue: "https",
AllowedValues: []string{"https", "ssh"},
CurrentValue: func(c gh.Config, hostname string) string {
return c.GitProtocol(hostname).Value
},
{
Key: editorKey,
Description: "the text editor program to use for authoring text",
DefaultValue: "",
},
{
Key: editorKey,
Description: "the text editor program to use for authoring text",
DefaultValue: "",
CurrentValue: func(c gh.Config, hostname string) string {
return c.Editor(hostname).Value
},
{
Key: promptKey,
Description: "toggle interactive prompting in the terminal",
DefaultValue: "enabled",
AllowedValues: []string{"enabled", "disabled"},
},
{
Key: promptKey,
Description: "toggle interactive prompting in the terminal",
DefaultValue: "enabled",
AllowedValues: []string{"enabled", "disabled"},
CurrentValue: func(c gh.Config, hostname string) string {
return c.Prompt(hostname).Value
},
{
Key: pagerKey,
Description: "the terminal pager program to send standard output to",
DefaultValue: "",
},
{
Key: pagerKey,
Description: "the terminal pager program to send standard output to",
DefaultValue: "",
CurrentValue: func(c gh.Config, hostname string) string {
return c.Pager(hostname).Value
},
{
Key: httpUnixSocketKey,
Description: "the path to a Unix socket through which to make an HTTP connection",
DefaultValue: "",
},
{
Key: httpUnixSocketKey,
Description: "the path to a Unix socket through which to make an HTTP connection",
DefaultValue: "",
CurrentValue: func(c gh.Config, hostname string) string {
return c.HTTPUnixSocket(hostname).Value
},
{
Key: browserKey,
Description: "the web browser to use for opening URLs",
DefaultValue: "",
},
{
Key: browserKey,
Description: "the web browser to use for opening URLs",
DefaultValue: "",
CurrentValue: func(c gh.Config, hostname string) string {
return c.Browser(hostname).Value
},
}
},
}
func HomeDirPath(subdir string) (string, error) {

View file

@ -1,10 +1,12 @@
package config
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/cli/cli/v2/internal/gh"
ghConfig "github.com/cli/go-gh/v2/pkg/config"
)
@ -32,71 +34,6 @@ func TestNewConfigProvidesFallback(t *testing.T) {
requireKeyWithValue(t, spiedCfg, []string{browserKey}, "")
}
func TestGetNonExistentKey(t *testing.T) {
// Given we have no top level configuration
cfg := newTestConfig()
// When we get a key that has no value
val, err := cfg.Get("", "non-existent-key")
// Then it returns an error and the value is empty
var keyNotFoundError *ghConfig.KeyNotFoundError
require.ErrorAs(t, err, &keyNotFoundError)
require.Empty(t, val)
}
func TestGetNonExistentHostSpecificKey(t *testing.T) {
// Given have no top level configuration
cfg := newTestConfig()
// When we get a key for a host that has no value
val, err := cfg.Get("non-existent-host", "non-existent-key")
// Then it returns an error and the value is empty
var keyNotFoundError *ghConfig.KeyNotFoundError
require.ErrorAs(t, err, &keyNotFoundError)
require.Empty(t, val)
}
func TestGetExistingTopLevelKey(t *testing.T) {
// Given have a top level config entry
cfg := newTestConfig()
cfg.Set("", "top-level-key", "top-level-value")
// When we get that key
val, err := cfg.Get("non-existent-host", "top-level-key")
// Then it returns successfully with the correct value
require.NoError(t, err)
require.Equal(t, "top-level-value", val)
}
func TestGetExistingHostSpecificKey(t *testing.T) {
// Given have a host specific config entry
cfg := newTestConfig()
cfg.Set("github.com", "host-specific-key", "host-specific-value")
// When we get that key
val, err := cfg.Get("github.com", "host-specific-key")
// Then it returns successfully with the correct value
require.NoError(t, err)
require.Equal(t, "host-specific-value", val)
}
func TestGetHostnameSpecificKeyFallsBackToTopLevel(t *testing.T) {
// Given have a top level config entry
cfg := newTestConfig()
cfg.Set("", "key", "value")
// When we get that key on a specific host
val, err := cfg.Get("github.com", "key")
// Then it returns successfully, falling back to the top level config
require.NoError(t, err)
require.Equal(t, "value", val)
}
func TestGetOrDefaultApplicationDefaults(t *testing.T) {
tests := []struct {
key string
@ -116,40 +53,79 @@ func TestGetOrDefaultApplicationDefaults(t *testing.T) {
cfg := newTestConfig()
// When we get a key that has no value, but has a default
val, err := cfg.GetOrDefault("", tt.key)
optionalEntry := cfg.GetOrDefault("", tt.key)
// Then it returns the default value
require.NoError(t, err)
require.Equal(t, tt.expectedDefault, val)
// Then there is an entry with the default value, and source set as default
entry := optionalEntry.Expect(fmt.Sprintf("expected there to be a value for %s", tt.key))
require.Equal(t, tt.expectedDefault, entry.Value)
require.Equal(t, gh.ConfigDefaultProvided, entry.Source)
})
}
}
func TestGetOrDefaultExistingKey(t *testing.T) {
// Given have a top level config entry
func TestGetOrDefaultNonExistentKey(t *testing.T) {
// Given we have no top level configuration
cfg := newTestConfig()
cfg.Set("", gitProtocolKey, "ssh")
// When we get that key
val, err := cfg.GetOrDefault("", gitProtocolKey)
// When we get a key that has no value
optionalEntry := cfg.GetOrDefault("", "non-existent-key")
// Then it returns successfully with the correct value, and doesn't fall back
// to the default
require.NoError(t, err)
require.Equal(t, "ssh", val)
// Then it returns a None variant
require.True(t, optionalEntry.IsNone(), "expected there to be no value")
}
func TestGetOrDefaultNotFoundAndNoDefault(t *testing.T) {
// Given have no configuration
func TestGetOrDefaultNonExistentHostSpecificKey(t *testing.T) {
// Given have no top level configuration
cfg := newTestConfig()
// When we get a non-existent-key that has no default
val, err := cfg.GetOrDefault("", "non-existent-key")
// When we get a key for a host that has no value
optionalEntry := cfg.GetOrDefault("non-existent-host", "non-existent-key")
// Then it returns an error and the value is empty
var keyNotFoundError *ghConfig.KeyNotFoundError
require.ErrorAs(t, err, &keyNotFoundError)
require.Empty(t, val)
// Then it returns a None variant
require.True(t, optionalEntry.IsNone(), "expected there to be no value")
}
func TestGetOrDefaultExistingTopLevelKey(t *testing.T) {
// Given have a top level config entry
cfg := newTestConfig()
cfg.Set("", "top-level-key", "top-level-value")
// When we get that key
optionalEntry := cfg.GetOrDefault("non-existent-host", "top-level-key")
// Then it returns a Some variant containing the correct value and a source of user
entry := optionalEntry.Expect("expected there to be a value")
require.Equal(t, "top-level-value", entry.Value)
require.Equal(t, gh.ConfigUserProvided, entry.Source)
}
func TestGetOrDefaultExistingHostSpecificKey(t *testing.T) {
// Given have a host specific config entry
cfg := newTestConfig()
cfg.Set("github.com", "host-specific-key", "host-specific-value")
// When we get that key
optionalEntry := cfg.GetOrDefault("github.com", "host-specific-key")
// Then it returns a Some variant containing the correct value and a source of user
entry := optionalEntry.Expect("expected there to be a value")
require.Equal(t, "host-specific-value", entry.Value)
require.Equal(t, gh.ConfigUserProvided, entry.Source)
}
func TestGetOrDefaultHostnameSpecificKeyFallsBackToTopLevel(t *testing.T) {
// Given have a top level config entry
cfg := newTestConfig()
cfg.Set("", "key", "value")
// When we get that key on a specific host
optionalEntry := cfg.GetOrDefault("github.com", "key")
// Then it returns a Some variant containing the correct value by falling back
// to the top level config, with a source of user
entry := optionalEntry.Expect("expected there to be a value")
require.Equal(t, "value", entry.Value)
require.Equal(t, gh.ConfigUserProvided, entry.Source)
}
func TestFallbackConfig(t *testing.T) {

View file

@ -8,6 +8,7 @@ import (
"path/filepath"
"testing"
ghmock "github.com/cli/cli/v2/internal/gh/mock"
ghConfig "github.com/cli/go-gh/v2/pkg/config"
"github.com/stretchr/testify/require"
)
@ -60,7 +61,7 @@ func TestMigrationAppliedBumpsVersion(t *testing.T) {
c.Set([]string{versionKey}, "expected-pre-version")
topLevelKey := []string{"toplevelkey"}
migration := &MigrationMock{
migration := &ghmock.MigrationMock{
DoFunc: func(config *ghConfig.Config) error {
config.Set(topLevelKey, "toplevelvalue")
return nil
@ -96,7 +97,7 @@ func TestMigrationIsNoopWhenAlreadyApplied(t *testing.T) {
c := ghConfig.ReadFromString(testFullConfig())
c.Set([]string{versionKey}, "expected-post-version")
migration := &MigrationMock{
migration := &ghmock.MigrationMock{
DoFunc: func(config *ghConfig.Config) error {
return errors.New("is not called")
},
@ -126,7 +127,7 @@ func TestMigrationErrorsWhenPreVersionMismatch(t *testing.T) {
c.Set([]string{versionKey}, "not-expected-pre-version")
topLevelKey := []string{"toplevelkey"}
migration := &MigrationMock{
migration := &ghmock.MigrationMock{
DoFunc: func(config *ghConfig.Config) error {
config.Set(topLevelKey, "toplevelvalue")
return nil
@ -228,8 +229,8 @@ func makeFileUnwriteable(t *testing.T, file string) {
require.NoError(t, os.Chmod(file, 0000))
}
func mockMigration(doFunc func(config *ghConfig.Config) error) *MigrationMock {
return &MigrationMock{
func mockMigration(doFunc func(config *ghConfig.Config) error) *ghmock.MigrationMock {
return &ghmock.MigrationMock{
DoFunc: doFunc,
PreVersionFunc: func() string {
return ""

View file

@ -6,19 +6,22 @@ import (
"path/filepath"
"testing"
"github.com/cli/cli/v2/internal/gh"
ghmock "github.com/cli/cli/v2/internal/gh/mock"
"github.com/cli/cli/v2/internal/keyring"
o "github.com/cli/cli/v2/pkg/option"
ghConfig "github.com/cli/go-gh/v2/pkg/config"
)
func NewBlankConfig() *ConfigMock {
func NewBlankConfig() *ghmock.ConfigMock {
return NewFromString(defaultConfigStr)
}
func NewFromString(cfgStr string) *ConfigMock {
func NewFromString(cfgStr string) *ghmock.ConfigMock {
c := ghConfig.ReadFromString(cfgStr)
cfg := cfg{c}
mock := &ConfigMock{}
mock.GetOrDefaultFunc = func(host, key string) (string, error) {
mock := &ghmock.ConfigMock{}
mock.GetOrDefaultFunc = func(host, key string) o.Option[gh.ConfigEntry] {
return cfg.GetOrDefault(host, key)
}
mock.SetFunc = func(host, key, value string) {
@ -27,13 +30,13 @@ func NewFromString(cfgStr string) *ConfigMock {
mock.WriteFunc = func() error {
return cfg.Write()
}
mock.MigrateFunc = func(m Migration) error {
mock.MigrateFunc = func(m gh.Migration) error {
return cfg.Migrate(m)
}
mock.AliasesFunc = func() *AliasConfig {
mock.AliasesFunc = func() gh.AliasConfig {
return &AliasConfig{cfg: c}
}
mock.AuthenticationFunc = func() *AuthConfig {
mock.AuthenticationFunc = func() gh.AuthConfig {
return &AuthConfig{
cfg: c,
defaultHostOverride: func() (string, string) {
@ -49,33 +52,26 @@ func NewFromString(cfgStr string) *ConfigMock {
},
}
}
mock.BrowserFunc = func(hostname string) string {
val, _ := cfg.GetOrDefault(hostname, browserKey)
return val
mock.BrowserFunc = func(hostname string) gh.ConfigEntry {
return cfg.Browser(hostname)
}
mock.EditorFunc = func(hostname string) string {
val, _ := cfg.GetOrDefault(hostname, editorKey)
return val
mock.EditorFunc = func(hostname string) gh.ConfigEntry {
return cfg.Editor(hostname)
}
mock.GitProtocolFunc = func(hostname string) string {
val, _ := cfg.GetOrDefault(hostname, gitProtocolKey)
return val
mock.GitProtocolFunc = func(hostname string) gh.ConfigEntry {
return cfg.GitProtocol(hostname)
}
mock.HTTPUnixSocketFunc = func(hostname string) string {
val, _ := cfg.GetOrDefault(hostname, httpUnixSocketKey)
return val
mock.HTTPUnixSocketFunc = func(hostname string) gh.ConfigEntry {
return cfg.HTTPUnixSocket(hostname)
}
mock.PagerFunc = func(hostname string) string {
val, _ := cfg.GetOrDefault(hostname, pagerKey)
return val
mock.PagerFunc = func(hostname string) gh.ConfigEntry {
return cfg.Pager(hostname)
}
mock.PromptFunc = func(hostname string) string {
val, _ := cfg.GetOrDefault(hostname, promptKey)
return val
mock.PromptFunc = func(hostname string) gh.ConfigEntry {
return cfg.Prompt(hostname)
}
mock.VersionFunc = func() string {
val, _ := cfg.GetOrDefault("", versionKey)
return val
mock.VersionFunc = func() o.Option[string] {
return cfg.Version()
}
mock.CacheDirFunc = func() string {
return cfg.CacheDir()
@ -88,7 +84,7 @@ func NewFromString(cfgStr string) *ConfigMock {
// in the real implementation, sets the GH_CONFIG_DIR env var so that
// any call to Write goes to a different location on disk, and then returns
// the blank config and a function that reads any data written to disk.
func NewIsolatedTestConfig(t *testing.T) (Config, func(io.Writer, io.Writer)) {
func NewIsolatedTestConfig(t *testing.T) (*cfg, func(io.Writer, io.Writer)) {
keyring.MockInit()
c := ghConfig.ReadFromString("")

View file

@ -28,6 +28,8 @@ func init() {
jsonCmd.Flags().StringSlice("json", nil, "help message for flag json")
aliasCmd.Flags().StringSlice("yang", nil, "help message for flag yang")
echoCmd.AddCommand(timesCmd, echoSubCmd, deprecatedCmd)
rootCmd.AddCommand(printCmd, echoCmd, dummyCmd)
}
@ -75,6 +77,13 @@ var printCmd = &cobra.Command{
Long: `an absolutely utterly useless command for testing.`,
}
var aliasCmd = &cobra.Command{
Use: "ying [yang]",
Short: "The ying and yang of it all",
Long: "an absolutely utterly useless command for testing aliases!.",
Aliases: []string{"yoo", "foo"},
}
var jsonCmd = &cobra.Command{
Use: "blah --json <fields>",
Short: "View details in JSON",

View file

@ -179,6 +179,14 @@ func manPrintOptions(buf *bytes.Buffer, command *cobra.Command) {
}
}
func manPrintAliases(buf *bytes.Buffer, command *cobra.Command) {
if len(command.Aliases) > 0 {
buf.WriteString("# ALIASES\n")
buf.WriteString(strings.Join(root.BuildAliasList(command, command.Aliases), ", "))
buf.WriteString("\n")
}
}
func manPrintJSONFields(buf *bytes.Buffer, command *cobra.Command) {
raw, ok := command.Annotations["help:json-fields"]
if !ok {
@ -207,6 +215,7 @@ func genMan(cmd *cobra.Command, header *GenManHeader) []byte {
}
}
manPrintOptions(buf, cmd)
manPrintAliases(buf, cmd)
manPrintJSONFields(buf, cmd)
if len(cmd.Example) > 0 {
buf.WriteString("# EXAMPLE\n")

View file

@ -98,6 +98,21 @@ func TestGenManSeeAlso(t *testing.T) {
}
}
func TestGenManAliases(t *testing.T) {
buf := new(bytes.Buffer)
header := &GenManHeader{}
if err := renderMan(aliasCmd, header, buf); err != nil {
t.Fatal(err)
}
output := buf.String()
checkStringContains(t, output, translate(aliasCmd.Name()))
checkStringContains(t, output, "ALIASES")
checkStringContains(t, output, "foo")
checkStringContains(t, output, "yoo")
}
func TestGenManJSONFields(t *testing.T) {
buf := new(bytes.Buffer)
header := &GenManHeader{}

View file

@ -26,6 +26,15 @@ func printJSONFields(w io.Writer, cmd *cobra.Command) {
fmt.Fprint(w, "\n\n")
}
func printAliases(w io.Writer, cmd *cobra.Command) {
if len(cmd.Aliases) > 0 {
fmt.Fprintf(w, "### ALIASES\n\n")
fmt.Fprint(w, text.FormatSlice(strings.Split(strings.Join(root.BuildAliasList(cmd, cmd.Aliases), ", "), ","), 0, 0, "", "", true))
fmt.Fprint(w, "\n\n")
}
}
func printOptions(w io.Writer, cmd *cobra.Command) error {
flags := cmd.NonInheritedFlags()
flags.SetOutput(w)
@ -147,6 +156,7 @@ func genMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string)
if err := printOptions(w, cmd); err != nil {
return err
}
printAliases(w, cmd)
printJSONFields(w, cmd)
fmt.Fprint(w, "{% endraw %}\n")

View file

@ -70,6 +70,20 @@ func TestGenMdNoHiddenParents(t *testing.T) {
checkStringOmits(t, output, "Options inherited from parent commands")
}
func TestGenMdAliases(t *testing.T) {
buf := new(bytes.Buffer)
if err := genMarkdownCustom(aliasCmd, buf, nil); err != nil {
t.Fatal(err)
}
output := buf.String()
checkStringContains(t, output, aliasCmd.Long)
checkStringContains(t, output, jsonCmd.Example)
checkStringContains(t, output, "ALIASES")
checkStringContains(t, output, "yoo")
checkStringContains(t, output, "foo")
}
func TestGenMdJSONFields(t *testing.T) {
buf := new(bytes.Buffer)
if err := genMarkdownCustom(jsonCmd, buf, nil); err != nil {

View file

@ -25,7 +25,7 @@ var allIssueFeatures = IssueFeatures{
type PullRequestFeatures struct {
MergeQueue bool
// CheckRunAndStatusContextCounts indicates whether the API supports
// the checkRunCount, checkRunCountsByState, statusContextCount and stausContextCountsByState
// the checkRunCount, checkRunCountsByState, statusContextCount and statusContextCountsByState
// fields on the StatusCheckRollupContextConnection
CheckRunAndStatusContextCounts bool
CheckRunEvent bool

171
internal/gh/gh.go Normal file
View file

@ -0,0 +1,171 @@
// Package gh provides types that represent the domain of the CLI application.
//
// For example, the CLI expects to be able to get and set user configuration in order to perform its functionality,
// so the Config interface is defined here, though the concrete implementation lives elsewhere. Though the current
// implementation of config writes to certain files on disk, that is an implementation detail compared to the contract
// laid out in the interface here.
//
// Currently this package is in an early state but we could imagine other domain concepts living here for interacting
// with git or GitHub.
package gh
import (
o "github.com/cli/cli/v2/pkg/option"
ghConfig "github.com/cli/go-gh/v2/pkg/config"
)
type ConfigSource string
const (
ConfigDefaultProvided ConfigSource = "default"
ConfigUserProvided ConfigSource = "user"
)
type ConfigEntry struct {
Value string
Source ConfigSource
}
// A Config implements persistent storage and modification of application configuration.
//
//go:generate moq -rm -pkg ghmock -out mock/config.go . Config
type Config interface {
// GetOrDefault provides primitive access for fetching configuration values, optionally scoped by host.
GetOrDefault(hostname string, key string) o.Option[ConfigEntry]
// Set provides primitive access for setting configuration values, optionally scoped by host.
Set(hostname string, key string, value string)
// Browser returns the configured browser, optionally scoped by host.
Browser(hostname string) ConfigEntry
// Editor returns the configured editor, optionally scoped by host.
Editor(hostname string) ConfigEntry
// GitProtocol returns the configured git protocol, optionally scoped by host.
GitProtocol(hostname string) ConfigEntry
// HTTPUnixSocket returns the configured HTTP unix socket, optionally scoped by host.
HTTPUnixSocket(hostname string) ConfigEntry
// Pager returns the configured Pager, optionally scoped by host.
Pager(hostname string) ConfigEntry
// Prompt returns the configured prompt, optionally scoped by host.
Prompt(hostname string) ConfigEntry
// Aliases provides persistent storage and modification of command aliases.
Aliases() AliasConfig
// Authentication provides persistent storage and modification of authentication configuration.
Authentication() AuthConfig
// CacheDir returns the directory where the cacheable artifacts can be persisted.
CacheDir() string
// Migrate applies a migration to the configuration.
Migrate(Migration) error
// Version returns the current schema version of the configuration.
Version() o.Option[string]
// Write persists modifications to the configuration.
Write() error
}
// Migration is the interface that config migrations must implement.
//
// Migrations will receive a copy of the config, and should modify that copy
// as necessary. After migration has completed, the modified config contents
// will be used.
//
// The calling code is expected to verify that the current version of the config
// matches the PreVersion of the migration before calling Do, and will set the
// config version to the PostVersion after the migration has completed successfully.
//
//go:generate moq -rm -pkg ghmock -out mock/migration.go . Migration
type Migration interface {
// PreVersion is the required config version for this to be applied
PreVersion() string
// PostVersion is the config version that must be applied after migration
PostVersion() string
// Do is expected to apply any necessary changes to the config in place
Do(*ghConfig.Config) error
}
// AuthConfig is used for interacting with some persistent configuration for gh,
// with knowledge on how to access encrypted storage when neccesarry.
// Behavior is scoped to authentication specific tasks.
type AuthConfig interface {
// ActiveToken will retrieve the active auth token for the given hostname, searching environment variables,
// general configuration, and finally encrypted storage.
ActiveToken(hostname string) (token string, source string)
// HasEnvToken returns true when a token has been specified in an environment variable, else returns false.
HasEnvToken() bool
// TokenFromKeyring will retrieve the auth token for the given hostname, only searching in encrypted storage.
TokenFromKeyring(hostname string) (token string, err error)
// TokenFromKeyringForUser will retrieve the auth token for the given hostname and username, only searching
// in encrypted storage.
//
// An empty username will return an error because the potential to return the currently active token under
// surprising cases is just too high to risk compared to the utility of having the function being smart.
TokenFromKeyringForUser(hostname, username string) (token string, err error)
// ActiveUser will retrieve the username for the active user at the given hostname.
//
// This will not be accurate if the oauth token is set from an environment variable.
ActiveUser(hostname string) (username string, err error)
// Hosts retrieves a list of known hosts.
Hosts() []string
// DefaultHost retrieves the default host.
DefaultHost() (host string, source string)
// Login will set user, git protocol, and auth token for the given hostname.
//
// If the encrypt option is specified it will first try to store the auth token
// in encrypted storage and will fall back to the general insecure configuration.
Login(hostname, username, token, gitProtocol string, secureStorage bool) (insecureStorageUsed bool, err error)
// SwitchUser switches the active user for a given hostname.
SwitchUser(hostname, user string) error
// Logout will remove user, git protocol, and auth token for the given hostname.
// It will remove the auth token from the encrypted storage if it exists there.
Logout(hostname, username string) error
// UsersForHost retrieves a list of users configured for a specific host.
UsersForHost(hostname string) []string
// TokenForUser retrieves the authentication token and its source for a specified user and hostname.
TokenForUser(hostname, user string) (token string, source string, err error)
// The following methods are only for testing and that is a design smell we should consider fixing.
// SetActiveToken will override any token resolution and return the given token and source for all calls to
// ActiveToken.
// Use for testing purposes only.
SetActiveToken(token, source string)
// SetHosts will override any hosts resolution and return the given hosts for all calls to Hosts.
// Use for testing purposes only.
SetHosts(hosts []string)
// SetDefaultHost will override any host resolution and return the given host and source for all calls to
// DefaultHost.
// Use for testing purposes only.
SetDefaultHost(host, source string)
}
// AliasConfig defines an interface for managing command aliases.
type AliasConfig interface {
// Get retrieves the expansion for a specified alias.
Get(alias string) (expansion string, err error)
// Add adds a new alias with the specified expansion.
Add(alias, expansion string)
// Delete removes an alias.
Delete(alias string) error
// All returns a map of all aliases to their corresponding expansions.
All() map[string]string
}

View file

@ -1,59 +1,61 @@
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package config
package ghmock
import (
"github.com/cli/cli/v2/internal/gh"
o "github.com/cli/cli/v2/pkg/option"
"sync"
)
// Ensure, that ConfigMock does implement Config.
// Ensure, that ConfigMock does implement gh.Config.
// If this is not the case, regenerate this file with moq.
var _ Config = &ConfigMock{}
var _ gh.Config = &ConfigMock{}
// ConfigMock is a mock implementation of Config.
// ConfigMock is a mock implementation of gh.Config.
//
// func TestSomethingThatUsesConfig(t *testing.T) {
//
// // make and configure a mocked Config
// // make and configure a mocked gh.Config
// mockedConfig := &ConfigMock{
// AliasesFunc: func() *AliasConfig {
// AliasesFunc: func() gh.AliasConfig {
// panic("mock out the Aliases method")
// },
// AuthenticationFunc: func() *AuthConfig {
// AuthenticationFunc: func() gh.AuthConfig {
// panic("mock out the Authentication method")
// },
// BrowserFunc: func(s string) string {
// BrowserFunc: func(hostname string) gh.ConfigEntry {
// panic("mock out the Browser method")
// },
// CacheDirFunc: func() string {
// panic("mock out the CacheDir method")
// },
// EditorFunc: func(s string) string {
// EditorFunc: func(hostname string) gh.ConfigEntry {
// panic("mock out the Editor method")
// },
// GetOrDefaultFunc: func(s1 string, s2 string) (string, error) {
// GetOrDefaultFunc: func(hostname string, key string) o.Option[gh.ConfigEntry] {
// panic("mock out the GetOrDefault method")
// },
// GitProtocolFunc: func(s string) string {
// GitProtocolFunc: func(hostname string) gh.ConfigEntry {
// panic("mock out the GitProtocol method")
// },
// HTTPUnixSocketFunc: func(s string) string {
// HTTPUnixSocketFunc: func(hostname string) gh.ConfigEntry {
// panic("mock out the HTTPUnixSocket method")
// },
// MigrateFunc: func(migration Migration) error {
// MigrateFunc: func(migration gh.Migration) error {
// panic("mock out the Migrate method")
// },
// PagerFunc: func(s string) string {
// PagerFunc: func(hostname string) gh.ConfigEntry {
// panic("mock out the Pager method")
// },
// PromptFunc: func(s string) string {
// PromptFunc: func(hostname string) gh.ConfigEntry {
// panic("mock out the Prompt method")
// },
// SetFunc: func(s1 string, s2 string, s3 string) {
// SetFunc: func(hostname string, key string, value string) {
// panic("mock out the Set method")
// },
// VersionFunc: func() string {
// VersionFunc: func() o.Option[string] {
// panic("mock out the Version method")
// },
// WriteFunc: func() error {
@ -61,49 +63,49 @@ var _ Config = &ConfigMock{}
// },
// }
//
// // use mockedConfig in code that requires Config
// // use mockedConfig in code that requires gh.Config
// // and then make assertions.
//
// }
type ConfigMock struct {
// AliasesFunc mocks the Aliases method.
AliasesFunc func() *AliasConfig
AliasesFunc func() gh.AliasConfig
// AuthenticationFunc mocks the Authentication method.
AuthenticationFunc func() *AuthConfig
AuthenticationFunc func() gh.AuthConfig
// BrowserFunc mocks the Browser method.
BrowserFunc func(s string) string
BrowserFunc func(hostname string) gh.ConfigEntry
// CacheDirFunc mocks the CacheDir method.
CacheDirFunc func() string
// EditorFunc mocks the Editor method.
EditorFunc func(s string) string
EditorFunc func(hostname string) gh.ConfigEntry
// GetOrDefaultFunc mocks the GetOrDefault method.
GetOrDefaultFunc func(s1 string, s2 string) (string, error)
GetOrDefaultFunc func(hostname string, key string) o.Option[gh.ConfigEntry]
// GitProtocolFunc mocks the GitProtocol method.
GitProtocolFunc func(s string) string
GitProtocolFunc func(hostname string) gh.ConfigEntry
// HTTPUnixSocketFunc mocks the HTTPUnixSocket method.
HTTPUnixSocketFunc func(s string) string
HTTPUnixSocketFunc func(hostname string) gh.ConfigEntry
// MigrateFunc mocks the Migrate method.
MigrateFunc func(migration Migration) error
MigrateFunc func(migration gh.Migration) error
// PagerFunc mocks the Pager method.
PagerFunc func(s string) string
PagerFunc func(hostname string) gh.ConfigEntry
// PromptFunc mocks the Prompt method.
PromptFunc func(s string) string
PromptFunc func(hostname string) gh.ConfigEntry
// SetFunc mocks the Set method.
SetFunc func(s1 string, s2 string, s3 string)
SetFunc func(hostname string, key string, value string)
// VersionFunc mocks the Version method.
VersionFunc func() string
VersionFunc func() o.Option[string]
// WriteFunc mocks the Write method.
WriteFunc func() error
@ -118,57 +120,57 @@ type ConfigMock struct {
}
// Browser holds details about calls to the Browser method.
Browser []struct {
// S is the s argument value.
S string
// Hostname is the hostname argument value.
Hostname string
}
// CacheDir holds details about calls to the CacheDir method.
CacheDir []struct {
}
// Editor holds details about calls to the Editor method.
Editor []struct {
// S is the s argument value.
S string
// Hostname is the hostname argument value.
Hostname string
}
// GetOrDefault holds details about calls to the GetOrDefault method.
GetOrDefault []struct {
// S1 is the s1 argument value.
S1 string
// S2 is the s2 argument value.
S2 string
// Hostname is the hostname argument value.
Hostname string
// Key is the key argument value.
Key string
}
// GitProtocol holds details about calls to the GitProtocol method.
GitProtocol []struct {
// S is the s argument value.
S string
// Hostname is the hostname argument value.
Hostname string
}
// HTTPUnixSocket holds details about calls to the HTTPUnixSocket method.
HTTPUnixSocket []struct {
// S is the s argument value.
S string
// Hostname is the hostname argument value.
Hostname string
}
// Migrate holds details about calls to the Migrate method.
Migrate []struct {
// Migration is the migration argument value.
Migration Migration
Migration gh.Migration
}
// Pager holds details about calls to the Pager method.
Pager []struct {
// S is the s argument value.
S string
// Hostname is the hostname argument value.
Hostname string
}
// Prompt holds details about calls to the Prompt method.
Prompt []struct {
// S is the s argument value.
S string
// Hostname is the hostname argument value.
Hostname string
}
// Set holds details about calls to the Set method.
Set []struct {
// S1 is the s1 argument value.
S1 string
// S2 is the s2 argument value.
S2 string
// S3 is the s3 argument value.
S3 string
// Hostname is the hostname argument value.
Hostname string
// Key is the key argument value.
Key string
// Value is the value argument value.
Value string
}
// Version holds details about calls to the Version method.
Version []struct {
@ -194,7 +196,7 @@ type ConfigMock struct {
}
// Aliases calls AliasesFunc.
func (mock *ConfigMock) Aliases() *AliasConfig {
func (mock *ConfigMock) Aliases() gh.AliasConfig {
if mock.AliasesFunc == nil {
panic("ConfigMock.AliasesFunc: method is nil but Config.Aliases was just called")
}
@ -221,7 +223,7 @@ func (mock *ConfigMock) AliasesCalls() []struct {
}
// Authentication calls AuthenticationFunc.
func (mock *ConfigMock) Authentication() *AuthConfig {
func (mock *ConfigMock) Authentication() gh.AuthConfig {
if mock.AuthenticationFunc == nil {
panic("ConfigMock.AuthenticationFunc: method is nil but Config.Authentication was just called")
}
@ -248,19 +250,19 @@ func (mock *ConfigMock) AuthenticationCalls() []struct {
}
// Browser calls BrowserFunc.
func (mock *ConfigMock) Browser(s string) string {
func (mock *ConfigMock) Browser(hostname string) gh.ConfigEntry {
if mock.BrowserFunc == nil {
panic("ConfigMock.BrowserFunc: method is nil but Config.Browser was just called")
}
callInfo := struct {
S string
Hostname string
}{
S: s,
Hostname: hostname,
}
mock.lockBrowser.Lock()
mock.calls.Browser = append(mock.calls.Browser, callInfo)
mock.lockBrowser.Unlock()
return mock.BrowserFunc(s)
return mock.BrowserFunc(hostname)
}
// BrowserCalls gets all the calls that were made to Browser.
@ -268,10 +270,10 @@ func (mock *ConfigMock) Browser(s string) string {
//
// len(mockedConfig.BrowserCalls())
func (mock *ConfigMock) BrowserCalls() []struct {
S string
Hostname string
} {
var calls []struct {
S string
Hostname string
}
mock.lockBrowser.RLock()
calls = mock.calls.Browser
@ -307,19 +309,19 @@ func (mock *ConfigMock) CacheDirCalls() []struct {
}
// Editor calls EditorFunc.
func (mock *ConfigMock) Editor(s string) string {
func (mock *ConfigMock) Editor(hostname string) gh.ConfigEntry {
if mock.EditorFunc == nil {
panic("ConfigMock.EditorFunc: method is nil but Config.Editor was just called")
}
callInfo := struct {
S string
Hostname string
}{
S: s,
Hostname: hostname,
}
mock.lockEditor.Lock()
mock.calls.Editor = append(mock.calls.Editor, callInfo)
mock.lockEditor.Unlock()
return mock.EditorFunc(s)
return mock.EditorFunc(hostname)
}
// EditorCalls gets all the calls that were made to Editor.
@ -327,10 +329,10 @@ func (mock *ConfigMock) Editor(s string) string {
//
// len(mockedConfig.EditorCalls())
func (mock *ConfigMock) EditorCalls() []struct {
S string
Hostname string
} {
var calls []struct {
S string
Hostname string
}
mock.lockEditor.RLock()
calls = mock.calls.Editor
@ -339,21 +341,21 @@ func (mock *ConfigMock) EditorCalls() []struct {
}
// GetOrDefault calls GetOrDefaultFunc.
func (mock *ConfigMock) GetOrDefault(s1 string, s2 string) (string, error) {
func (mock *ConfigMock) GetOrDefault(hostname string, key string) o.Option[gh.ConfigEntry] {
if mock.GetOrDefaultFunc == nil {
panic("ConfigMock.GetOrDefaultFunc: method is nil but Config.GetOrDefault was just called")
}
callInfo := struct {
S1 string
S2 string
Hostname string
Key string
}{
S1: s1,
S2: s2,
Hostname: hostname,
Key: key,
}
mock.lockGetOrDefault.Lock()
mock.calls.GetOrDefault = append(mock.calls.GetOrDefault, callInfo)
mock.lockGetOrDefault.Unlock()
return mock.GetOrDefaultFunc(s1, s2)
return mock.GetOrDefaultFunc(hostname, key)
}
// GetOrDefaultCalls gets all the calls that were made to GetOrDefault.
@ -361,12 +363,12 @@ func (mock *ConfigMock) GetOrDefault(s1 string, s2 string) (string, error) {
//
// len(mockedConfig.GetOrDefaultCalls())
func (mock *ConfigMock) GetOrDefaultCalls() []struct {
S1 string
S2 string
Hostname string
Key string
} {
var calls []struct {
S1 string
S2 string
Hostname string
Key string
}
mock.lockGetOrDefault.RLock()
calls = mock.calls.GetOrDefault
@ -375,19 +377,19 @@ func (mock *ConfigMock) GetOrDefaultCalls() []struct {
}
// GitProtocol calls GitProtocolFunc.
func (mock *ConfigMock) GitProtocol(s string) string {
func (mock *ConfigMock) GitProtocol(hostname string) gh.ConfigEntry {
if mock.GitProtocolFunc == nil {
panic("ConfigMock.GitProtocolFunc: method is nil but Config.GitProtocol was just called")
}
callInfo := struct {
S string
Hostname string
}{
S: s,
Hostname: hostname,
}
mock.lockGitProtocol.Lock()
mock.calls.GitProtocol = append(mock.calls.GitProtocol, callInfo)
mock.lockGitProtocol.Unlock()
return mock.GitProtocolFunc(s)
return mock.GitProtocolFunc(hostname)
}
// GitProtocolCalls gets all the calls that were made to GitProtocol.
@ -395,10 +397,10 @@ func (mock *ConfigMock) GitProtocol(s string) string {
//
// len(mockedConfig.GitProtocolCalls())
func (mock *ConfigMock) GitProtocolCalls() []struct {
S string
Hostname string
} {
var calls []struct {
S string
Hostname string
}
mock.lockGitProtocol.RLock()
calls = mock.calls.GitProtocol
@ -407,19 +409,19 @@ func (mock *ConfigMock) GitProtocolCalls() []struct {
}
// HTTPUnixSocket calls HTTPUnixSocketFunc.
func (mock *ConfigMock) HTTPUnixSocket(s string) string {
func (mock *ConfigMock) HTTPUnixSocket(hostname string) gh.ConfigEntry {
if mock.HTTPUnixSocketFunc == nil {
panic("ConfigMock.HTTPUnixSocketFunc: method is nil but Config.HTTPUnixSocket was just called")
}
callInfo := struct {
S string
Hostname string
}{
S: s,
Hostname: hostname,
}
mock.lockHTTPUnixSocket.Lock()
mock.calls.HTTPUnixSocket = append(mock.calls.HTTPUnixSocket, callInfo)
mock.lockHTTPUnixSocket.Unlock()
return mock.HTTPUnixSocketFunc(s)
return mock.HTTPUnixSocketFunc(hostname)
}
// HTTPUnixSocketCalls gets all the calls that were made to HTTPUnixSocket.
@ -427,10 +429,10 @@ func (mock *ConfigMock) HTTPUnixSocket(s string) string {
//
// len(mockedConfig.HTTPUnixSocketCalls())
func (mock *ConfigMock) HTTPUnixSocketCalls() []struct {
S string
Hostname string
} {
var calls []struct {
S string
Hostname string
}
mock.lockHTTPUnixSocket.RLock()
calls = mock.calls.HTTPUnixSocket
@ -439,12 +441,12 @@ func (mock *ConfigMock) HTTPUnixSocketCalls() []struct {
}
// Migrate calls MigrateFunc.
func (mock *ConfigMock) Migrate(migration Migration) error {
func (mock *ConfigMock) Migrate(migration gh.Migration) error {
if mock.MigrateFunc == nil {
panic("ConfigMock.MigrateFunc: method is nil but Config.Migrate was just called")
}
callInfo := struct {
Migration Migration
Migration gh.Migration
}{
Migration: migration,
}
@ -459,10 +461,10 @@ func (mock *ConfigMock) Migrate(migration Migration) error {
//
// len(mockedConfig.MigrateCalls())
func (mock *ConfigMock) MigrateCalls() []struct {
Migration Migration
Migration gh.Migration
} {
var calls []struct {
Migration Migration
Migration gh.Migration
}
mock.lockMigrate.RLock()
calls = mock.calls.Migrate
@ -471,19 +473,19 @@ func (mock *ConfigMock) MigrateCalls() []struct {
}
// Pager calls PagerFunc.
func (mock *ConfigMock) Pager(s string) string {
func (mock *ConfigMock) Pager(hostname string) gh.ConfigEntry {
if mock.PagerFunc == nil {
panic("ConfigMock.PagerFunc: method is nil but Config.Pager was just called")
}
callInfo := struct {
S string
Hostname string
}{
S: s,
Hostname: hostname,
}
mock.lockPager.Lock()
mock.calls.Pager = append(mock.calls.Pager, callInfo)
mock.lockPager.Unlock()
return mock.PagerFunc(s)
return mock.PagerFunc(hostname)
}
// PagerCalls gets all the calls that were made to Pager.
@ -491,10 +493,10 @@ func (mock *ConfigMock) Pager(s string) string {
//
// len(mockedConfig.PagerCalls())
func (mock *ConfigMock) PagerCalls() []struct {
S string
Hostname string
} {
var calls []struct {
S string
Hostname string
}
mock.lockPager.RLock()
calls = mock.calls.Pager
@ -503,19 +505,19 @@ func (mock *ConfigMock) PagerCalls() []struct {
}
// Prompt calls PromptFunc.
func (mock *ConfigMock) Prompt(s string) string {
func (mock *ConfigMock) Prompt(hostname string) gh.ConfigEntry {
if mock.PromptFunc == nil {
panic("ConfigMock.PromptFunc: method is nil but Config.Prompt was just called")
}
callInfo := struct {
S string
Hostname string
}{
S: s,
Hostname: hostname,
}
mock.lockPrompt.Lock()
mock.calls.Prompt = append(mock.calls.Prompt, callInfo)
mock.lockPrompt.Unlock()
return mock.PromptFunc(s)
return mock.PromptFunc(hostname)
}
// PromptCalls gets all the calls that were made to Prompt.
@ -523,10 +525,10 @@ func (mock *ConfigMock) Prompt(s string) string {
//
// len(mockedConfig.PromptCalls())
func (mock *ConfigMock) PromptCalls() []struct {
S string
Hostname string
} {
var calls []struct {
S string
Hostname string
}
mock.lockPrompt.RLock()
calls = mock.calls.Prompt
@ -535,23 +537,23 @@ func (mock *ConfigMock) PromptCalls() []struct {
}
// Set calls SetFunc.
func (mock *ConfigMock) Set(s1 string, s2 string, s3 string) {
func (mock *ConfigMock) Set(hostname string, key string, value string) {
if mock.SetFunc == nil {
panic("ConfigMock.SetFunc: method is nil but Config.Set was just called")
}
callInfo := struct {
S1 string
S2 string
S3 string
Hostname string
Key string
Value string
}{
S1: s1,
S2: s2,
S3: s3,
Hostname: hostname,
Key: key,
Value: value,
}
mock.lockSet.Lock()
mock.calls.Set = append(mock.calls.Set, callInfo)
mock.lockSet.Unlock()
mock.SetFunc(s1, s2, s3)
mock.SetFunc(hostname, key, value)
}
// SetCalls gets all the calls that were made to Set.
@ -559,14 +561,14 @@ func (mock *ConfigMock) Set(s1 string, s2 string, s3 string) {
//
// len(mockedConfig.SetCalls())
func (mock *ConfigMock) SetCalls() []struct {
S1 string
S2 string
S3 string
Hostname string
Key string
Value string
} {
var calls []struct {
S1 string
S2 string
S3 string
Hostname string
Key string
Value string
}
mock.lockSet.RLock()
calls = mock.calls.Set
@ -575,7 +577,7 @@ func (mock *ConfigMock) SetCalls() []struct {
}
// Version calls VersionFunc.
func (mock *ConfigMock) Version() string {
func (mock *ConfigMock) Version() o.Option[string] {
if mock.VersionFunc == nil {
panic("ConfigMock.VersionFunc: method is nil but Config.Version was just called")
}

View file

@ -1,22 +1,23 @@
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package config
package ghmock
import (
"github.com/cli/cli/v2/internal/gh"
ghConfig "github.com/cli/go-gh/v2/pkg/config"
"sync"
)
// Ensure, that MigrationMock does implement Migration.
// Ensure, that MigrationMock does implement gh.Migration.
// If this is not the case, regenerate this file with moq.
var _ Migration = &MigrationMock{}
var _ gh.Migration = &MigrationMock{}
// MigrationMock is a mock implementation of Migration.
// MigrationMock is a mock implementation of gh.Migration.
//
// func TestSomethingThatUsesMigration(t *testing.T) {
//
// // make and configure a mocked Migration
// // make and configure a mocked gh.Migration
// mockedMigration := &MigrationMock{
// DoFunc: func(config *ghConfig.Config) error {
// panic("mock out the Do method")
@ -29,7 +30,7 @@ var _ Migration = &MigrationMock{}
// },
// }
//
// // use mockedMigration in code that requires Migration
// // use mockedMigration in code that requires gh.Migration
// // and then make assertions.
//
// }

View file

@ -4,14 +4,14 @@ import (
"fmt"
"sort"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
)
type DeleteOptions struct {
Config func() (config.Config, error)
Config func() (gh.Config, error)
IO *iostreams.IOStreams
Name string

View file

@ -7,6 +7,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/google/shlex"
@ -161,7 +162,7 @@ func TestDeleteRun(t *testing.T) {
tt.opts.IO = ios
cfg := config.NewFromString(tt.config)
tt.opts.Config = func() (config.Config, error) {
tt.opts.Config = func() (gh.Config, error) {
return cfg, nil
}

View file

@ -6,7 +6,7 @@ import (
"strings"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmd/alias/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -15,7 +15,7 @@ import (
)
type ImportOptions struct {
Config func() (config.Config, error)
Config func() (gh.Config, error)
IO *iostreams.IOStreams
Filename string

View file

@ -11,6 +11,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmd/alias/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -309,7 +310,7 @@ func TestImportRun(t *testing.T) {
readConfigs := config.StubWriteConfig(t)
cfg := config.NewFromString(tt.initConfig)
tt.opts.Config = func() (config.Config, error) {
tt.opts.Config = func() (gh.Config, error) {
return cfg, nil
}

View file

@ -2,7 +2,7 @@ package list
import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
@ -10,7 +10,7 @@ import (
)
type ListOptions struct {
Config func() (config.Config, error)
Config func() (gh.Config, error)
IO *iostreams.IOStreams
}

View file

@ -7,6 +7,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/stretchr/testify/assert"
@ -66,7 +67,7 @@ func TestAliasList(t *testing.T) {
factory := &cmdutil.Factory{
IOStreams: ios,
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return cfg, nil
},
}

View file

@ -6,7 +6,7 @@ import (
"strings"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmd/alias/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -14,7 +14,7 @@ import (
)
type SetOptions struct {
Config func() (config.Config, error)
Config func() (gh.Config, error)
IO *iostreams.IOStreams
Name string

View file

@ -6,6 +6,7 @@ import (
"testing"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmd/alias/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -284,7 +285,7 @@ func TestSetRun(t *testing.T) {
cfg.WriteFunc = func() error {
return nil
}
tt.opts.Config = func() (config.Config, error) {
tt.opts.Config = func() (gh.Config, error) {
return cfg, nil
}

View file

@ -17,7 +17,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/factory"
@ -37,7 +37,7 @@ type ApiOptions struct {
AppVersion string
BaseRepo func() (ghrepo.Interface, error)
Branch func() (string, error)
Config func() (config.Config, error)
Config func() (gh.Config, error)
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
@ -149,7 +149,7 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
$ gh api repos/{owner}/{repo}/issues --template \
'{{range .}}{{.title}} ({{.labels | pluck "name" | join ", " | color "yellow"}}){{"\n"}}{{end}}'
# update allowed values of the "environment" custom property in a deeply nested array
# update allowed values of the "environment" custom property in a deeply nested array
gh api --PATCH /orgs/{org}/properties/schema \
-F 'properties[][property_name]=environment' \
-F 'properties[][default_value]=production' \

View file

@ -16,6 +16,8 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
ghmock "github.com/cli/cli/v2/internal/gh/mock"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -662,7 +664,7 @@ func Test_apiRun(t *testing.T) {
ios.SetStdoutTTY(tt.isatty)
tt.options.IO = ios
tt.options.Config = func() (config.Config, error) { return config.NewBlankConfig(), nil }
tt.options.Config = func() (gh.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
@ -737,7 +739,7 @@ func Test_apiRun_paginationREST(t *testing.T) {
}
return &http.Client{Transport: tr}, nil
},
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return config.NewBlankConfig(), nil
},
@ -809,7 +811,7 @@ func Test_apiRun_arrayPaginationREST(t *testing.T) {
}
return &http.Client{Transport: tr}, nil
},
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return config.NewBlankConfig(), nil
},
@ -881,7 +883,7 @@ func Test_apiRun_arrayPaginationREST_with_headers(t *testing.T) {
}
return &http.Client{Transport: tr}, nil
},
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return config.NewBlankConfig(), nil
},
@ -950,7 +952,7 @@ func Test_apiRun_paginationGraphQL(t *testing.T) {
}
return &http.Client{Transport: tr}, nil
},
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return config.NewBlankConfig(), nil
},
@ -1049,7 +1051,7 @@ func Test_apiRun_paginationGraphQL_slurp(t *testing.T) {
}
return &http.Client{Transport: tr}, nil
},
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return config.NewBlankConfig(), nil
},
@ -1161,7 +1163,7 @@ func Test_apiRun_paginated_template(t *testing.T) {
}
return &http.Client{Transport: tr}, nil
},
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return config.NewBlankConfig(), nil
},
@ -1208,7 +1210,7 @@ func Test_apiRun_DELETE(t *testing.T) {
var gotRequest *http.Request
err := apiRun(&ApiOptions{
IO: ios,
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return config.NewBlankConfig(), nil
},
HttpClient: func() (*http.Client, error) {
@ -1293,7 +1295,7 @@ func Test_apiRun_inputFile(t *testing.T) {
}
return &http.Client{Transport: tr}, nil
},
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return config.NewBlankConfig(), nil
},
}
@ -1324,9 +1326,9 @@ func Test_apiRun_cache(t *testing.T) {
ios, _, stdout, stderr := iostreams.Test()
options := ApiOptions{
IO: ios,
Config: func() (config.Config, error) {
return &config.ConfigMock{
AuthenticationFunc: func() *config.AuthConfig {
Config: func() (gh.Config, error) {
return &ghmock.ConfigMock{
AuthenticationFunc: func() gh.AuthConfig {
return &config.AuthConfig{}
},
// Cached responses are stored in a tempdir that gets automatically cleaned up
@ -1738,7 +1740,7 @@ func Test_apiRun_acceptHeader(t *testing.T) {
ios, _, _, _ := iostreams.Test()
tt.options.IO = ios
tt.options.Config = func() (config.Config, error) {
tt.options.Config = func() (gh.Config, error) {
return config.NewBlankConfig(), nil
}

File diff suppressed because one or more lines are too long

View file

@ -97,6 +97,6 @@ func TestFilterAttestations(t *testing.T) {
require.Len(t, filtered, 1)
filtered = FilterAttestations("NonExistantPredicate", attestations)
filtered = FilterAttestations("NonExistentPredicate", attestations)
require.Len(t, filtered, 0)
}

View file

@ -5,6 +5,7 @@ import (
"path/filepath"
"strings"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
@ -16,6 +17,7 @@ import (
type Options struct {
ArtifactPath string
BundlePath string
Config func() (gh.Config, error)
CustomTrustedRoot string
DenySelfHostedRunner bool
DigestAlgorithm string
@ -27,6 +29,8 @@ type Options struct {
Repo string
SAN string
SANRegex string
SignerRepo string
SignerWorkflow string
APIClient api.Client
Logger *io.Handler
OCIClient oci.Client
@ -51,12 +55,12 @@ func (opts *Options) SetPolicyFlags() {
// to Owner
opts.Owner = splitRepo[0]
if opts.SAN == "" && opts.SANRegex == "" {
if !isSignerIdentityProvided(opts) {
opts.SANRegex = expandToGitHubURL(opts.Repo)
}
return
}
if opts.SAN == "" && opts.SANRegex == "" {
if !isSignerIdentityProvided(opts) {
opts.SANRegex = expandToGitHubURL(opts.Owner)
}
}
@ -64,13 +68,14 @@ func (opts *Options) SetPolicyFlags() {
// AreFlagsValid checks that the provided flag combination is valid
// and returns an error otherwise
func (opts *Options) AreFlagsValid() error {
// check that Repo is in the expected format if provided
if opts.Repo != "" {
// we expect the repo argument to be in the format <OWNER>/<REPO>
splitRepo := strings.Split(opts.Repo, "/")
if len(splitRepo) != 2 {
return fmt.Errorf("invalid value provided for repo: %s", opts.Repo)
}
// If provided, check that the Repo option is in the expected format <OWNER>/<REPO>
if opts.Repo != "" && !isProvidedRepoValid(opts.Repo) {
return fmt.Errorf("invalid value provided for repo: %s", opts.Repo)
}
// If provided, check that the SignerRepo option is in the expected format <OWNER>/<REPO>
if opts.SignerRepo != "" && !isProvidedRepoValid(opts.SignerRepo) {
return fmt.Errorf("invalid value provided for signer-repo: %s", opts.SignerRepo)
}
// Check that limit is between 1 and 1000
@ -81,6 +86,13 @@ func (opts *Options) AreFlagsValid() error {
return nil
}
func expandToGitHubURL(ownerOrRepo string) string {
return fmt.Sprintf("^https://github.com/%s/", ownerOrRepo)
// check if any of the signer identity flags have been provided
func isSignerIdentityProvided(opts *Options) bool {
return opts.SAN != "" || opts.SANRegex != "" || opts.SignerRepo != "" || opts.SignerWorkflow != ""
}
func isProvidedRepoValid(repo string) bool {
// we expect a provided repository argument be in the format <OWNER>/<REPO>
splitRepo := strings.Split(repo, "/")
return len(splitRepo) == 2
}

View file

@ -2,6 +2,8 @@ package verify
import (
"fmt"
"os"
"regexp"
"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
"github.com/sigstore/sigstore-go/pkg/verify"
@ -14,18 +16,30 @@ const (
GitHubOIDCIssuer = "https://token.actions.githubusercontent.com"
// represents the GitHub hosted runner in the certificate RunnerEnvironment extension
GitHubRunner = "github-hosted"
githubHost = "github.com"
hostRegex = `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+.*$`
)
func buildSANMatcher(san, sanRegex string) (verify.SubjectAlternativeNameMatcher, error) {
if san == "" && sanRegex == "" {
return verify.SubjectAlternativeNameMatcher{}, nil
func expandToGitHubURL(ownerOrRepo string) string {
return fmt.Sprintf("^https://github.com/%s/", ownerOrRepo)
}
func buildSANMatcher(opts *Options) (verify.SubjectAlternativeNameMatcher, error) {
if opts.SignerRepo != "" {
signedRepoRegex := expandToGitHubURL(opts.SignerRepo)
return verify.NewSANMatcher("", "", signedRepoRegex)
} else if opts.SignerWorkflow != "" {
validatedWorkflowRegex, err := validateSignerWorkflow(opts)
if err != nil {
return verify.SubjectAlternativeNameMatcher{}, err
}
return verify.NewSANMatcher("", "", validatedWorkflowRegex)
} else if opts.SAN != "" || opts.SANRegex != "" {
return verify.NewSANMatcher(opts.SAN, "", opts.SANRegex)
}
sanMatcher, err := verify.NewSANMatcher(san, "", sanRegex)
if err != nil {
return verify.SubjectAlternativeNameMatcher{}, err
}
return sanMatcher, nil
return verify.SubjectAlternativeNameMatcher{}, nil
}
func buildCertExtensions(opts *Options, runnerEnv string) certificate.Extensions {
@ -43,7 +57,7 @@ func buildCertExtensions(opts *Options, runnerEnv string) certificate.Extensions
}
func buildCertificateIdentityOption(opts *Options, runnerEnv string) (verify.PolicyOption, error) {
sanMatcher, err := buildSANMatcher(opts.SAN, opts.SANRegex)
sanMatcher, err := buildSANMatcher(opts)
if err != nil {
return nil, err
}
@ -93,3 +107,54 @@ func buildVerifyPolicy(opts *Options, a artifact.DigestedArtifact) (verify.Polic
policy := verify.NewPolicy(artifactDigestPolicyOption, certIdOption)
return policy, nil
}
func addSchemeToRegex(s string) string {
return fmt.Sprintf("^https://%s", s)
}
func validateSignerWorkflow(opts *Options) (string, error) {
// we expect a provided workflow argument be in the format [HOST/]/<OWNER>/<REPO>/path/to/workflow.yml
// if the provided workflow does not contain a host, set the host
match, err := regexp.MatchString(hostRegex, opts.SignerWorkflow)
if err != nil {
return "", err
}
if match {
return addSchemeToRegex(opts.SignerWorkflow), nil
}
// if the provided workflow does not contain a host, check for a host
// and prepend it to the workflow
host, err := chooseHost(opts)
if err != nil {
return "", err
}
return addSchemeToRegex(fmt.Sprintf("%s/%s", host, opts.SignerWorkflow)), nil
}
// if a host was not provided as part of a flag argument choose a host based
// on gh cli configuration
func chooseHost(opts *Options) (string, error) {
// check if GH_HOST is set and use that host if it is
host := os.Getenv("GH_HOST")
if host != "" {
return host, nil
}
// check if the CLI is authenticated with any hosts
cfg, err := opts.Config()
if err != nil {
return "", err
}
// if authenticated, return the authenticated host
authCfg := cfg.Authentication()
if host, _ := authCfg.DefaultHost(); host != "" {
return host, nil
}
// if not authenticated, return the default host github.com
return githubHost, nil
}

View file

@ -1,10 +1,12 @@
package verify
import (
"os"
"testing"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
"github.com/cli/cli/v2/pkg/cmd/factory"
"github.com/stretchr/testify/require"
)
@ -29,3 +31,65 @@ func TestBuildPolicy(t *testing.T) {
_, err = buildVerifyPolicy(opts, *artifact)
require.NoError(t, err)
}
func ValidateSignerWorkflow(t *testing.T) {
type testcase struct {
name string
providedSignerWorkflow string
expectedWorkflowRegex string
ghHost string
authHost string
}
testcases := []testcase{
{
name: "workflow with no host specified",
providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
expectedWorkflowRegex: "^https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
},
{
name: "workflow with host specified",
providedSignerWorkflow: "github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
expectedWorkflowRegex: "^https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
},
{
name: "workflow with GH_HOST set",
providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
expectedWorkflowRegex: "^https://myhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
ghHost: "myhost.github.com",
},
{
name: "workflow with authenticated host",
providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
expectedWorkflowRegex: "^https://authedhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
authHost: "authedhost.github.com",
},
}
for _, tc := range testcases {
cmdFactory := factory.New("test")
opts := &Options{
Config: cmdFactory.Config,
SignerWorkflow: tc.providedSignerWorkflow,
}
if tc.ghHost != "" {
err := os.Setenv("GH_HOST", tc.ghHost)
require.NoError(t, err)
}
if tc.authHost != "" {
cfg, err := opts.Config()
require.NoError(t, err)
// if authenticated, return the authenticated host
authCfg := cfg.Authentication()
authCfg.SetDefaultHost(tc.authHost, "")
}
workflowRegex, err := validateSignerWorkflow(opts)
require.NoError(t, err)
require.Equal(t, tc.expectedWorkflowRegex, workflowRegex)
}
}

View file

@ -52,7 +52,7 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
provide a path to the %[1]s--bundle%[1]s flag.
To see the full results that are generated upon successful verification, i.e.
for use with a policy engine, provide the %[1]s--json-result%[1]s flag.
for use with a policy engine, provide the %[1]s--format=json%[1]s flag.
The attestation's certificate's Subject Alternative Name (SAN) identifies the entity
responsible for creating the attestation, which most of the time will be a GitHub
@ -123,6 +123,7 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
}
opts.SigstoreVerifier = verification.NewLiveSigstoreVerifier(config)
opts.Config = f.Config
if err := runVerify(opts); err != nil {
return fmt.Errorf("\nError: %v", err)
@ -148,7 +149,9 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
verifyCmd.Flags().BoolVarP(&opts.DenySelfHostedRunner, "deny-self-hosted-runners", "", false, "Fail verification for attestations generated on self-hosted runners")
verifyCmd.Flags().StringVarP(&opts.SAN, "cert-identity", "", "", "Enforce that the certificate's subject alternative name matches the provided value exactly")
verifyCmd.Flags().StringVarP(&opts.SANRegex, "cert-identity-regex", "i", "", "Enforce that the certificate's subject alternative name matches the provided regex")
verifyCmd.MarkFlagsMutuallyExclusive("cert-identity", "cert-identity-regex")
verifyCmd.Flags().StringVarP(&opts.SignerRepo, "signer-repo", "", "", "Repository of reusable workflow that signed attestation in the format <owner>/<repo>")
verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Workflow that signed attestation in the format [host/]<owner>/<repo>/<path>/<to>/<workflow>")
verifyCmd.MarkFlagsMutuallyExclusive("cert-identity", "cert-identity-regex", "signer-repo", "signer-workflow")
verifyCmd.Flags().StringVarP(&opts.OIDCIssuer, "cert-oidc-issuer", "", GitHubOIDCIssuer, "Issuer of the OIDC token")
return verifyCmd

View file

@ -8,6 +8,7 @@ import (
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/cli/v2/pkg/cmd/attestation/test"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
"github.com/cli/cli/v2/pkg/cmd/factory"
"github.com/stretchr/testify/require"
@ -80,3 +81,157 @@ func TestVerifyIntegration(t *testing.T) {
require.ErrorContains(t, err, "verifying with issuer \"sigstore.dev\": failed to verify certificate identity: no matching certificate identity found")
})
}
func TestVerifyIntegrationReusableWorkflow(t *testing.T) {
artifactPath := test.NormalizeRelativePath("../test/data/reusable-workflow-artifact")
bundlePath := test.NormalizeRelativePath("../test/data/reusable-workflow-attestation.sigstore.json")
logger := io.NewTestHandler()
sigstoreConfig := verification.SigstoreConfig{
Logger: logger,
}
cmdFactory := factory.New("test")
hc, err := cmdFactory.HttpClient()
if err != nil {
t.Fatal(err)
}
baseOpts := Options{
APIClient: api.NewLiveClient(hc, logger),
ArtifactPath: artifactPath,
BundlePath: bundlePath,
DigestAlgorithm: "sha256",
Logger: logger,
OCIClient: oci.NewLiveClient(),
OIDCIssuer: GitHubOIDCIssuer,
SigstoreVerifier: verification.NewLiveSigstoreVerifier(sigstoreConfig),
}
t.Run("with owner and valid reusable workflow SAN", func(t *testing.T) {
opts := baseOpts
opts.Owner = "malancas"
opts.SAN = "https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml@09b495c3f12c7881b3cc17209a327792065c1a1d"
err := runVerify(&opts)
require.NoError(t, err)
})
t.Run("with owner and valid reusable workflow SAN regex", func(t *testing.T) {
opts := baseOpts
opts.Owner = "malancas"
opts.SANRegex = "^https://github.com/github/artifact-attestations-workflows/"
err := runVerify(&opts)
require.NoError(t, err)
})
t.Run("with owner and valid reusable signer repo", func(t *testing.T) {
opts := baseOpts
opts.Owner = "malancas"
opts.SignerRepo = "github/artifact-attestations-workflows"
err := runVerify(&opts)
require.NoError(t, err)
})
t.Run("with repo and valid reusable workflow SAN", func(t *testing.T) {
opts := baseOpts
opts.Owner = "malancas"
opts.Repo = "malancas/attest-demo"
opts.SAN = "https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml@09b495c3f12c7881b3cc17209a327792065c1a1d"
err := runVerify(&opts)
require.NoError(t, err)
})
t.Run("with repo and valid reusable workflow SAN regex", func(t *testing.T) {
opts := baseOpts
opts.Owner = "malancas"
opts.Repo = "malancas/attest-demo"
opts.SANRegex = "^https://github.com/github/artifact-attestations-workflows/"
err := runVerify(&opts)
require.NoError(t, err)
})
t.Run("with repo and valid reusable signer repo", func(t *testing.T) {
opts := baseOpts
opts.Owner = "malancas"
opts.Repo = "malancas/attest-demo"
opts.SignerRepo = "github/artifact-attestations-workflows"
err := runVerify(&opts)
require.NoError(t, err)
})
}
func TestVerifyIntegrationReusableWorkflowSignerWorkflow(t *testing.T) {
artifactPath := test.NormalizeRelativePath("../test/data/reusable-workflow-artifact")
bundlePath := test.NormalizeRelativePath("../test/data/reusable-workflow-attestation.sigstore.json")
logger := io.NewTestHandler()
sigstoreConfig := verification.SigstoreConfig{
Logger: logger,
}
cmdFactory := factory.New("test")
hc, err := cmdFactory.HttpClient()
if err != nil {
t.Fatal(err)
}
baseOpts := Options{
APIClient: api.NewLiveClient(hc, logger),
ArtifactPath: artifactPath,
BundlePath: bundlePath,
Config: cmdFactory.Config,
DigestAlgorithm: "sha256",
Logger: logger,
OCIClient: oci.NewLiveClient(),
OIDCIssuer: GitHubOIDCIssuer,
Owner: "malancas",
Repo: "malancas/attest-demo",
SigstoreVerifier: verification.NewLiveSigstoreVerifier(sigstoreConfig),
}
type testcase struct {
name string
signerWorkflow string
expectErr bool
}
testcases := []testcase{
{
name: "with invalid signer workflow",
signerWorkflow: "foo/bar/.github/workflows/attest.yml",
expectErr: true,
},
{
name: "valid signer workflow with host",
signerWorkflow: "github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
expectErr: false,
},
{
name: "valid signer workflow without host (defaults to github.com)",
signerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
expectErr: false,
},
}
for _, tc := range testcases {
opts := baseOpts
opts.SignerWorkflow = tc.signerWorkflow
err := runVerify(&opts)
if tc.expectErr {
require.Error(t, err, "expected error for '%s'", tc.name)
} else {
require.NoError(t, err, "unexpected error for '%s'", tc.name)
}
}
}

View file

@ -9,9 +9,10 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
"github.com/cli/cli/v2/pkg/cmd/auth/shared/gitcredentials"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
ghAuth "github.com/cli/go-gh/v2/pkg/auth"
@ -20,7 +21,7 @@ import (
type LoginOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
Config func() (gh.Config, error)
HttpClient func() (*http.Client, error)
GitClient *git.Client
Prompter shared.Prompt
@ -195,18 +196,26 @@ func loginRun(opts *LoginOptions) error {
}
return shared.Login(&shared.LoginOptions{
IO: opts.IO,
Config: authCfg,
HTTPClient: httpClient,
Hostname: hostname,
Interactive: opts.Interactive,
Web: opts.Web,
Scopes: opts.Scopes,
Executable: opts.MainExecutable,
GitProtocol: opts.GitProtocol,
Prompter: opts.Prompter,
GitClient: opts.GitClient,
Browser: opts.Browser,
IO: opts.IO,
Config: authCfg,
HTTPClient: httpClient,
Hostname: hostname,
Interactive: opts.Interactive,
Web: opts.Web,
Scopes: opts.Scopes,
GitProtocol: opts.GitProtocol,
Prompter: opts.Prompter,
Browser: opts.Browser,
CredentialFlow: &shared.GitCredentialFlow{
Prompter: opts.Prompter,
HelperConfig: &gitcredentials.HelperConfig{
SelfExecutablePath: opts.MainExecutable,
GitClient: opts.GitClient,
},
Updater: &gitcredentials.Updater{
GitClient: opts.GitClient,
},
},
SecureStorage: !opts.InsecureStorage,
SkipSSHKeyPrompt: opts.SkipSSHKeyPrompt,
})

View file

@ -10,6 +10,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/internal/run"
"github.com/cli/cli/v2/pkg/cmdutil"
@ -281,7 +282,7 @@ func Test_loginRun_nontty(t *testing.T) {
opts *LoginOptions
env map[string]string
httpStubs func(*httpmock.Registry)
cfgStubs func(*testing.T, config.Config)
cfgStubs func(*testing.T, gh.Config)
wantHosts string
wantErr string
wantStderr string
@ -417,7 +418,7 @@ func Test_loginRun_nontty(t *testing.T) {
Hostname: "github.com",
Token: "newUserToken",
},
cfgStubs: func(t *testing.T, c config.Config) {
cfgStubs: func(t *testing.T, c gh.Config) {
_, err := c.Authentication().Login("github.com", "monalisa", "abc123", "https", false)
require.NoError(t, err)
},
@ -451,7 +452,7 @@ func Test_loginRun_nontty(t *testing.T) {
if tt.cfgStubs != nil {
tt.cfgStubs(t, cfg)
}
tt.opts.Config = func() (config.Config, error) {
tt.opts.Config = func() (gh.Config, error) {
return cfg, nil
}
@ -500,7 +501,7 @@ func Test_loginRun_Survey(t *testing.T) {
httpStubs func(*httpmock.Registry)
prompterStubs func(*prompter.PrompterMock)
runStubs func(*run.CommandStubber)
cfgStubs func(*testing.T, config.Config)
cfgStubs func(*testing.T, gh.Config)
wantHosts string
wantErrOut *regexp.Regexp
wantSecureToken string
@ -700,7 +701,7 @@ func Test_loginRun_Survey(t *testing.T) {
return -1, prompter.NoSuchPromptErr(prompt)
}
},
cfgStubs: func(t *testing.T, c config.Config) {
cfgStubs: func(t *testing.T, c gh.Config) {
_, err := c.Authentication().Login("github.com", "monalisa", "abc123", "https", false)
require.NoError(t, err)
},
@ -744,7 +745,7 @@ func Test_loginRun_Survey(t *testing.T) {
if tt.cfgStubs != nil {
tt.cfgStubs(t, cfg)
}
tt.opts.Config = func() (config.Config, error) {
tt.opts.Config = func() (gh.Config, error) {
return cfg, nil
}

View file

@ -6,7 +6,7 @@ import (
"slices"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -15,7 +15,7 @@ import (
type LogoutOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
Config func() (gh.Config, error)
Prompter shared.Prompt
Hostname string
Username string

View file

@ -7,6 +7,7 @@ import (
"testing"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -124,7 +125,7 @@ type hostUsers struct {
users []user
}
type tokenAssertion func(t *testing.T, cfg config.Config)
type tokenAssertion func(t *testing.T, cfg gh.Config)
func Test_logoutRun_tty(t *testing.T) {
tests := []struct {
@ -322,7 +323,7 @@ func Test_logoutRun_tty(t *testing.T) {
}
}
tt.opts.Config = func() (config.Config, error) {
tt.opts.Config = func() (gh.Config, error) {
return cfg, nil
}
@ -516,7 +517,7 @@ func Test_logoutRun_nontty(t *testing.T) {
)
}
}
tt.opts.Config = func() (config.Config, error) {
tt.opts.Config = func() (gh.Config, error) {
return cfg, nil
}
@ -552,7 +553,7 @@ func Test_logoutRun_nontty(t *testing.T) {
}
func hasNoToken(hostname string) tokenAssertion {
return func(t *testing.T, cfg config.Config) {
return func(t *testing.T, cfg gh.Config) {
t.Helper()
token, _ := cfg.Authentication().ActiveToken(hostname)
@ -561,7 +562,7 @@ func hasNoToken(hostname string) tokenAssertion {
}
func hasActiveToken(hostname string, expectedToken string) tokenAssertion {
return func(t *testing.T, cfg config.Config) {
return func(t *testing.T, cfg gh.Config) {
t.Helper()
token, _ := cfg.Authentication().ActiveToken(hostname)

View file

@ -8,8 +8,9 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/authflow"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
"github.com/cli/cli/v2/pkg/cmd/auth/shared/gitcredentials"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/set"
@ -21,7 +22,7 @@ type username string
type RefreshOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
Config func() (gh.Config, error)
HttpClient *http.Client
GitClient *git.Client
Prompter shared.Prompt
@ -173,11 +174,16 @@ func refreshRun(opts *RefreshOptions) error {
}
credentialFlow := &shared.GitCredentialFlow{
Executable: opts.MainExecutable,
Prompter: opts.Prompter,
GitClient: opts.GitClient,
Prompter: opts.Prompter,
HelperConfig: &gitcredentials.HelperConfig{
SelfExecutablePath: opts.MainExecutable,
GitClient: opts.GitClient,
},
Updater: &gitcredentials.Updater{
GitClient: opts.GitClient,
},
}
gitProtocol := cfg.GitProtocol(hostname)
gitProtocol := cfg.GitProtocol(hostname).Value
if opts.Interactive && gitProtocol == "https" {
if err := credentialFlow.Prompt(hostname); err != nil {
return err

View file

@ -8,6 +8,7 @@ import (
"testing"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
@ -441,7 +442,7 @@ func Test_refreshRun(t *testing.T) {
_, err := cfg.Authentication().Login(hostname, "test-user", "abc123", "https", false)
require.NoError(t, err)
}
tt.opts.Config = func() (config.Config, error) {
tt.opts.Config = func() (gh.Config, error) {
return cfg, nil
}

View file

@ -5,23 +5,23 @@ import (
"strings"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmd/auth/shared/gitcredentials"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
)
type gitConfigurator interface {
Setup(hostname, username, authToken string) error
type gitCredentialsConfigurer interface {
ConfigureOurs(hostname string) error
}
type SetupGitOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
Hostname string
Force bool
gitConfigure gitConfigurator
IO *iostreams.IOStreams
Config func() (gh.Config, error)
Hostname string
Force bool
CredentialsHelperConfig gitCredentialsConfigurer
}
func NewCmdSetupGit(f *cmdutil.Factory, runF func(*SetupGitOptions) error) *cobra.Command {
@ -52,9 +52,9 @@ func NewCmdSetupGit(f *cmdutil.Factory, runF func(*SetupGitOptions) error) *cobr
$ gh auth setup-git --hostname enterprise.internal
`),
RunE: func(cmd *cobra.Command, args []string) error {
opts.gitConfigure = &shared.GitCredentialFlow{
Executable: f.Executable(),
GitClient: f.GitClient,
opts.CredentialsHelperConfig = &gitcredentials.HelperConfig{
SelfExecutablePath: f.Executable(),
GitClient: f.GitClient,
}
if opts.Hostname == "" && opts.Force {
return cmdutil.FlagErrorf("`--force` must be used in conjunction with `--hostname`")
@ -92,7 +92,7 @@ func setupGitRun(opts *SetupGitOptions) error {
)
}
if err := opts.gitConfigure.Setup(opts.Hostname, "", ""); err != nil {
if err := opts.CredentialsHelperConfig.ConfigureOurs(opts.Hostname); err != nil {
return fmt.Errorf("failed to set up git credential helper: %s", err)
}
@ -111,7 +111,7 @@ func setupGitRun(opts *SetupGitOptions) error {
}
for _, hostname := range hostnames {
if err := opts.gitConfigure.Setup(hostname, "", ""); err != nil {
if err := opts.CredentialsHelperConfig.ConfigureOurs(hostname); err != nil {
return fmt.Errorf("failed to set up git credential helper: %s", err)
}
}

View file

@ -8,25 +8,23 @@ import (
"testing"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/google/shlex"
"github.com/stretchr/testify/require"
)
type mockGitConfigurer struct {
type gitCredentialsConfigurerSpy struct {
hosts []string
setupErr error
}
func (gf *mockGitConfigurer) SetupFor(hostname string) []string {
return gf.hosts
}
func (gf *mockGitConfigurer) Setup(hostname, username, authToken string) error {
func (gf *gitCredentialsConfigurerSpy) ConfigureOurs(hostname string) error {
gf.hosts = append(gf.hosts, hostname)
return gf.setupErr
}
func TestNewCmdSetupGit(t *testing.T) {
tests := []struct {
name string
@ -81,7 +79,7 @@ func Test_setupGitRun(t *testing.T) {
name string
opts *SetupGitOptions
setupErr error
cfgStubs func(*testing.T, config.Config)
cfgStubs func(*testing.T, gh.Config)
expectedHostsSetup []string
expectedErr error
expectedErrOut string
@ -89,7 +87,7 @@ func Test_setupGitRun(t *testing.T) {
{
name: "opts.Config returns an error",
opts: &SetupGitOptions{
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return nil, fmt.Errorf("oops")
},
},
@ -100,7 +98,7 @@ func Test_setupGitRun(t *testing.T) {
opts: &SetupGitOptions{
Hostname: "ghe.io",
},
cfgStubs: func(t *testing.T, cfg config.Config) {
cfgStubs: func(t *testing.T, cfg gh.Config) {
login(t, cfg, "github.com", "test-user", "gho_ABCDEFG", "https", false)
},
expectedErr: errors.New("You are not logged into the GitHub host \"ghe.io\". Run gh auth login -h ghe.io to authenticate or provide `--force`"),
@ -118,7 +116,7 @@ func Test_setupGitRun(t *testing.T) {
opts: &SetupGitOptions{
Hostname: "ghe.io",
},
cfgStubs: func(t *testing.T, cfg config.Config) {
cfgStubs: func(t *testing.T, cfg gh.Config) {
login(t, cfg, "ghe.io", "test-user", "gho_ABCDEFG", "https", false)
},
expectedHostsSetup: []string{"ghe.io"},
@ -129,7 +127,7 @@ func Test_setupGitRun(t *testing.T) {
Hostname: "ghe.io",
},
setupErr: fmt.Errorf("broken"),
cfgStubs: func(t *testing.T, cfg config.Config) {
cfgStubs: func(t *testing.T, cfg gh.Config) {
login(t, cfg, "ghe.io", "test-user", "gho_ABCDEFG", "https", false)
},
expectedErr: errors.New("failed to set up git credential helper: broken"),
@ -144,7 +142,7 @@ func Test_setupGitRun(t *testing.T) {
{
name: "when there are known hosts, and no hostname is provided, set them all up",
opts: &SetupGitOptions{},
cfgStubs: func(t *testing.T, cfg config.Config) {
cfgStubs: func(t *testing.T, cfg gh.Config) {
login(t, cfg, "ghe.io", "test-user", "gho_ABCDEFG", "https", false)
login(t, cfg, "github.com", "test-user", "gho_ABCDEFG", "https", false)
},
@ -154,7 +152,7 @@ func Test_setupGitRun(t *testing.T) {
name: "when no hostname is provided but setting one up errors, that error is bubbled",
opts: &SetupGitOptions{},
setupErr: fmt.Errorf("broken"),
cfgStubs: func(t *testing.T, cfg config.Config) {
cfgStubs: func(t *testing.T, cfg gh.Config) {
login(t, cfg, "ghe.io", "test-user", "gho_ABCDEFG", "https", false)
},
expectedErr: errors.New("failed to set up git credential helper: broken"),
@ -177,13 +175,13 @@ func Test_setupGitRun(t *testing.T) {
}
if tt.opts.Config == nil {
tt.opts.Config = func() (config.Config, error) {
tt.opts.Config = func() (gh.Config, error) {
return cfg, nil
}
}
gcSpy := &mockGitConfigurer{setupErr: tt.setupErr}
tt.opts.gitConfigure = gcSpy
credentialsConfigurerSpy := &gitCredentialsConfigurerSpy{setupErr: tt.setupErr}
tt.opts.CredentialsHelperConfig = credentialsConfigurerSpy
err := setupGitRun(tt.opts)
if tt.expectedErr != nil {
@ -193,7 +191,7 @@ func Test_setupGitRun(t *testing.T) {
}
if tt.expectedHostsSetup != nil {
require.Equal(t, tt.expectedHostsSetup, gcSpy.hosts)
require.Equal(t, tt.expectedHostsSetup, credentialsConfigurerSpy.hosts)
}
require.Equal(t, tt.expectedErrOut, stderr.String())
@ -201,7 +199,7 @@ func Test_setupGitRun(t *testing.T) {
}
}
func login(t *testing.T, c config.Config, hostname, username, token, gitProtocol string, secureStorage bool) {
func login(t *testing.T, c gh.Config, hostname, username, token, gitProtocol string, secureStorage bool) {
t.Helper()
_, err := c.Authentication().Login(hostname, username, token, gitProtocol, secureStorage)
require.NoError(t, err)

View file

@ -0,0 +1,64 @@
package contract
import (
"testing"
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
"github.com/stretchr/testify/require"
)
// This HelperConfig contract exist to ensure that any HelperConfig implementation conforms to this behaviour.
// This is useful because we can swap in fake implementations for testing, rather than requiring our tests to be
// isolated from git.
//
// See for example, TestAuthenticatingGitCredentials for LoginFlow.
type HelperConfig struct {
NewHelperConfig func(t *testing.T) shared.HelperConfig
ConfigureHelper func(t *testing.T, hostname string)
}
func (contract HelperConfig) Test(t *testing.T) {
t.Run("when there are no credential helpers, configures gh for repo and gist host", func(t *testing.T) {
hc := contract.NewHelperConfig(t)
require.NoError(t, hc.ConfigureOurs("github.com"))
repoHelper, err := hc.ConfiguredHelper("github.com")
require.NoError(t, err)
require.True(t, repoHelper.IsConfigured(), "expected our helper to be configured")
require.True(t, repoHelper.IsOurs(), "expected the helper to be ours but was %q", repoHelper.Cmd)
gistHelper, err := hc.ConfiguredHelper("gist.github.com")
require.NoError(t, err)
require.True(t, gistHelper.IsConfigured(), "expected our helper to be configured")
require.True(t, gistHelper.IsOurs(), "expected the helper to be ours but was %q", gistHelper.Cmd)
})
t.Run("when there is a global credential helper, it should be configured but not ours", func(t *testing.T) {
hc := contract.NewHelperConfig(t)
contract.ConfigureHelper(t, "credential.helper")
helper, err := hc.ConfiguredHelper("github.com")
require.NoError(t, err)
require.True(t, helper.IsConfigured(), "expected helper to be configured")
require.False(t, helper.IsOurs(), "expected the helper not to be ours but was %q", helper.Cmd)
})
t.Run("when there is a host credential helper, it should be configured but not ours", func(t *testing.T) {
hc := contract.NewHelperConfig(t)
contract.ConfigureHelper(t, "credential.https://github.com.helper")
helper, err := hc.ConfiguredHelper("github.com")
require.NoError(t, err)
require.True(t, helper.IsConfigured(), "expected helper to be configured")
require.False(t, helper.IsOurs(), "expected the helper not to be ours but was %q", helper.Cmd)
})
t.Run("returns non configured helper when no helpers are configured", func(t *testing.T) {
hc := contract.NewHelperConfig(t)
helper, err := hc.ConfiguredHelper("github.com")
require.NoError(t, err)
require.False(t, helper.IsConfigured(), "expected no helper to be configured")
require.False(t, helper.IsOurs(), "expected the helper not to be ours but was %q", helper.Cmd)
})
}

View file

@ -1,37 +1,43 @@
package shared
import (
"bytes"
"context"
"errors"
"fmt"
"path/filepath"
"strings"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/google/shlex"
"github.com/cli/cli/v2/pkg/cmd/auth/shared/gitcredentials"
)
type HelperConfig interface {
ConfigureOurs(hostname string) error
ConfiguredHelper(hostname string) (gitcredentials.Helper, error)
}
type GitCredentialFlow struct {
Executable string
Prompter Prompt
GitClient *git.Client
Prompter Prompt
HelperConfig HelperConfig
Updater *gitcredentials.Updater
shouldSetup bool
helper string
helper gitcredentials.Helper
scopes []string
}
func (flow *GitCredentialFlow) Prompt(hostname string) error {
var gitErr error
flow.helper, gitErr = gitCredentialHelper(flow.GitClient, hostname)
if isOurCredentialHelper(flow.helper) {
// First we'll fetch the credential helper that would be used for this host
var configuredHelperErr error
flow.helper, configuredHelperErr = flow.HelperConfig.ConfiguredHelper(hostname)
// If the helper is gh itself, then we don't need to ask the user if they want to update their git credentials
// because it will happen automatically by virtue of the fact that gh will return the active token.
//
// Since gh is the helper, this token may be used for git operations, so we'll additionally request the workflow
// scope to ensure that git push operations that include workflow changes succeed.
if flow.helper.IsOurs() {
flow.scopes = append(flow.scopes, "workflow")
return nil
}
// Prompt the user for whether they want to configure git with the newly obtained token
result, err := flow.Prompter.Confirm("Authenticate Git with your GitHub credentials?", true)
if err != nil {
return err
@ -39,9 +45,24 @@ func (flow *GitCredentialFlow) Prompt(hostname string) error {
flow.shouldSetup = result
if flow.shouldSetup {
if isGitMissing(gitErr) {
return gitErr
// If the user does want to configure git, we'll check the error returned from fetching the configured helper
// above. If the error indicates that git isn't installed, we'll return an error now to ensure that the auth
// flow is aborted before the user goes any further.
//
// Note that this is _slightly_ naive because there may be other reasons that fetching the configured helper
// fails that might cause later failures but this code has existed for a long time and I don't want to change
// it as part of a refactoring.
//
// Refs:
// * https://git-scm.com/docs/git-config#_description
// * https://github.com/cli/cli/pull/4109
var errNotInstalled *git.NotInstalled
if errors.As(configuredHelperErr, &errNotInstalled) {
return configuredHelperErr
}
// On the other hand, if the user has requested setup we'll additionally request the workflow
// scope to ensure that git push operations that include workflow changes succeed.
flow.scopes = append(flow.scopes, "workflow")
}
@ -57,131 +78,12 @@ func (flow *GitCredentialFlow) ShouldSetup() bool {
}
func (flow *GitCredentialFlow) Setup(hostname, username, authToken string) error {
return flow.gitCredentialSetup(hostname, username, authToken)
}
func (flow *GitCredentialFlow) gitCredentialSetup(hostname, username, password string) error {
gitClient := flow.GitClient
ctx := context.Background()
if flow.helper == "" {
credHelperKeys := []string{
gitCredentialHelperKey(hostname),
}
gistHost := strings.TrimSuffix(ghinstance.GistHost(hostname), "/")
if strings.HasPrefix(gistHost, "gist.") {
credHelperKeys = append(credHelperKeys, gitCredentialHelperKey(gistHost))
}
var configErr error
for _, credHelperKey := range credHelperKeys {
if configErr != nil {
break
}
// first use a blank value to indicate to git we want to sever the chain of credential helpers
preConfigureCmd, err := gitClient.Command(ctx, "config", "--global", "--replace-all", credHelperKey, "")
if err != nil {
configErr = err
break
}
if _, err = preConfigureCmd.Output(); err != nil {
configErr = err
break
}
// second configure the actual helper for this host
configureCmd, err := gitClient.Command(ctx,
"config", "--global", "--add",
credHelperKey,
fmt.Sprintf("!%s auth git-credential", shellQuote(flow.Executable)),
)
if err != nil {
configErr = err
} else {
_, configErr = configureCmd.Output()
}
}
return configErr
}
// clear previous cached credentials
rejectCmd, err := gitClient.Command(ctx, "credential", "reject")
if err != nil {
return err
}
rejectCmd.Stdin = bytes.NewBufferString(heredoc.Docf(`
protocol=https
host=%s
`, hostname))
_, err = rejectCmd.Output()
if err != nil {
return err
}
approveCmd, err := gitClient.Command(ctx, "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 = approveCmd.Output()
if err != nil {
return err
}
return nil
}
func gitCredentialHelperKey(hostname string) string {
host := strings.TrimSuffix(ghinstance.HostPrefix(hostname), "/")
return fmt.Sprintf("credential.%s.helper", host)
}
func gitCredentialHelper(gitClient *git.Client, hostname string) (helper string, err error) {
ctx := context.Background()
helper, err = gitClient.Config(ctx, gitCredentialHelperKey(hostname))
if helper != "" {
return
}
helper, err = gitClient.Config(ctx, "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
// If there is no credential helper configured then we will set ourselves up as
// the credential helper for this host.
if !flow.helper.IsConfigured() {
return flow.HelperConfig.ConfigureOurs(hostname)
}
// Otherwise, we'll tell git to inform the existing credential helper of the new credentials.
return flow.Updater.Update(hostname, username, authToken)
}

View file

@ -5,22 +5,24 @@ import (
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/run"
"github.com/cli/cli/v2/pkg/cmd/auth/shared/gitcredentials"
)
func TestGitCredentialSetup_configureExisting(t *testing.T) {
func TestSetup_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",
GitClient: &git.Client{GitPath: "some/path/git"},
helper: gitcredentials.Helper{Cmd: "osxkeychain"},
Updater: &gitcredentials.Updater{
GitClient: &git.Client{GitPath: "some/path/git"},
},
}
if err := f.gitCredentialSetup("example.com", "monalisa", "PASSWD"); err != nil {
t.Errorf("GitCredentialSetup() error = %v", err)
if err := f.Setup("example.com", "monalisa", "PASSWD"); err != nil {
t.Errorf("Setup() error = %v", err)
}
}
@ -61,18 +63,20 @@ func TestGitCredentialsSetup_setOurs_GH(t *testing.T) {
})
f := GitCredentialFlow{
Executable: "/path/to/gh",
helper: "",
GitClient: &git.Client{GitPath: "some/path/git"},
helper: gitcredentials.Helper{},
HelperConfig: &gitcredentials.HelperConfig{
SelfExecutablePath: "/path/to/gh",
GitClient: &git.Client{GitPath: "some/path/git"},
},
}
if err := f.gitCredentialSetup("github.com", "monalisa", "PASSWD"); err != nil {
t.Errorf("GitCredentialSetup() error = %v", err)
if err := f.Setup("github.com", "monalisa", "PASSWD"); err != nil {
t.Errorf("Setup() error = %v", err)
}
}
func TestGitCredentialSetup_setOurs_nonGH(t *testing.T) {
func TestSetup_setOurs_nonGH(t *testing.T) {
cs, restoreRun := run.Stub()
defer restoreRun(t)
cs.Register(`git config --global --replace-all credential\.`, 0, "", func(args []string) {
@ -93,53 +97,14 @@ func TestGitCredentialSetup_setOurs_nonGH(t *testing.T) {
})
f := GitCredentialFlow{
Executable: "/path/to/gh",
helper: "",
GitClient: &git.Client{GitPath: "some/path/git"},
helper: gitcredentials.Helper{},
HelperConfig: &gitcredentials.HelperConfig{
SelfExecutablePath: "/path/to/gh",
GitClient: &git.Client{GitPath: "some/path/git"},
},
}
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)
}
})
if err := f.Setup("example.com", "monalisa", "PASSWD"); err != nil {
t.Errorf("Setup() error = %v", err)
}
}

View file

@ -0,0 +1,49 @@
package gitcredentials
import (
"fmt"
"strings"
"github.com/cli/cli/v2/internal/ghinstance"
)
type FakeHelperConfig struct {
SelfExecutablePath string
Helpers map[string]Helper
}
// ConfigureOurs sets up the git credential helper chain to use the GitHub CLI credential helper for git repositories
// including gists.
func (hc *FakeHelperConfig) ConfigureOurs(hostname string) error {
credHelperKeys := []string{
keyFor(hostname),
}
gistHost := strings.TrimSuffix(ghinstance.GistHost(hostname), "/")
if strings.HasPrefix(gistHost, "gist.") {
credHelperKeys = append(credHelperKeys, keyFor(gistHost))
}
for _, credHelperKey := range credHelperKeys {
hc.Helpers[credHelperKey] = Helper{
Cmd: fmt.Sprintf("!%s auth git-credential", shellQuote(hc.SelfExecutablePath)),
}
}
return nil
}
// ConfiguredHelper returns the configured git credential helper for a given hostname.
func (hc *FakeHelperConfig) ConfiguredHelper(hostname string) (Helper, error) {
helper, ok := hc.Helpers[keyFor(hostname)]
if ok {
return helper, nil
}
helper, ok = hc.Helpers["credential.helper"]
if ok {
return helper, nil
}
return Helper{}, nil
}

View file

@ -0,0 +1,32 @@
package gitcredentials_test
import (
"testing"
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
"github.com/cli/cli/v2/pkg/cmd/auth/shared/contract"
"github.com/cli/cli/v2/pkg/cmd/auth/shared/gitcredentials"
)
func TestFakeHelperConfigContract(t *testing.T) {
// Note that this being mutated by `NewHelperConfig` makes these tests not parallelizable
var fhc *gitcredentials.FakeHelperConfig
contract.HelperConfig{
NewHelperConfig: func(t *testing.T) shared.HelperConfig {
// Mutate the closed over fhc so that ConfigureHelper is able to configure helpers
// for tests. An alternative would be to provide the Helper as an argument back to ConfigureHelper
// but then we'd have to type assert it back to *FakeHelperConfig, which is probably more trouble than
// it's worth to parallelize these tests, sinced it's not even possible to parallelize the real Helperconfig
// ones due to them using t.Setenv
fhc = &gitcredentials.FakeHelperConfig{
SelfExecutablePath: "/path/to/gh",
Helpers: map[string]gitcredentials.Helper{},
}
return fhc
},
ConfigureHelper: func(t *testing.T, hostname string) {
fhc.Helpers[hostname] = gitcredentials.Helper{Cmd: "test-helper"}
},
}.Test(t)
}

View file

@ -0,0 +1,124 @@
package gitcredentials
import (
"context"
"fmt"
"path/filepath"
"strings"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/google/shlex"
)
// A HelperConfig is used to configure and inspect the state of git credential helpers.
type HelperConfig struct {
SelfExecutablePath string
GitClient *git.Client
}
// ConfigureOurs sets up the git credential helper chain to use the GitHub CLI credential helper for git repositories
// including gists.
func (hc *HelperConfig) ConfigureOurs(hostname string) error {
ctx := context.TODO()
credHelperKeys := []string{
keyFor(hostname),
}
gistHost := strings.TrimSuffix(ghinstance.GistHost(hostname), "/")
if strings.HasPrefix(gistHost, "gist.") {
credHelperKeys = append(credHelperKeys, keyFor(gistHost))
}
var configErr error
for _, credHelperKey := range credHelperKeys {
if configErr != nil {
break
}
// first use a blank value to indicate to git we want to sever the chain of credential helpers
preConfigureCmd, err := hc.GitClient.Command(ctx, "config", "--global", "--replace-all", credHelperKey, "")
if err != nil {
configErr = err
break
}
if _, err = preConfigureCmd.Output(); err != nil {
configErr = err
break
}
// second configure the actual helper for this host
configureCmd, err := hc.GitClient.Command(ctx,
"config", "--global", "--add",
credHelperKey,
fmt.Sprintf("!%s auth git-credential", shellQuote(hc.SelfExecutablePath)),
)
if err != nil {
configErr = err
} else {
_, configErr = configureCmd.Output()
}
}
return configErr
}
// A Helper represents a git credential helper configuration.
type Helper struct {
Cmd string
}
// IsConfigured returns true if the helper has a non-empty command, i.e. the git config had an entry
func (h Helper) IsConfigured() bool {
return h.Cmd != ""
}
// IsOurs returns true if the helper command is the GitHub CLI credential helper
func (h Helper) IsOurs() bool {
if !strings.HasPrefix(h.Cmd, "!") {
return false
}
args, err := shlex.Split(h.Cmd[1:])
if err != nil || len(args) == 0 {
return false
}
return strings.TrimSuffix(filepath.Base(args[0]), ".exe") == "gh"
}
// ConfiguredHelper returns the configured git credential helper for a given hostname.
func (hc *HelperConfig) ConfiguredHelper(hostname string) (Helper, error) {
ctx := context.TODO()
hostHelperCmd, err := hc.GitClient.Config(ctx, keyFor(hostname))
if hostHelperCmd != "" {
// TODO: This is a direct refactoring removing named and naked returns
// but we should probably look closer at the error handling here
return Helper{
Cmd: hostHelperCmd,
}, err
}
globalHelperCmd, err := hc.GitClient.Config(ctx, "credential.helper")
if globalHelperCmd != "" {
return Helper{
Cmd: globalHelperCmd,
}, err
}
return Helper{}, nil
}
func keyFor(hostname string) string {
host := strings.TrimSuffix(ghinstance.HostPrefix(hostname), "/")
return fmt.Sprintf("credential.%s.helper", host)
}
func shellQuote(s string) string {
if strings.ContainsAny(s, " $\\") {
return "'" + s + "'"
}
return s
}

View file

@ -0,0 +1,125 @@
package gitcredentials_test
import (
"context"
"path/filepath"
"runtime"
"testing"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
"github.com/cli/cli/v2/pkg/cmd/auth/shared/contract"
"github.com/cli/cli/v2/pkg/cmd/auth/shared/gitcredentials"
"github.com/stretchr/testify/require"
)
func withIsolatedGitConfig(t *testing.T) {
t.Helper()
// https://git-scm.com/docs/git-config#ENVIRONMENT
// Set the global git config to a temporary file
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, ".gitconfig")
t.Setenv("GIT_CONFIG_GLOBAL", configFile)
// And disable git reading the system config
t.Setenv("GIT_CONFIG_NOSYSTEM", "true")
}
func configureTestCredentialHelper(t *testing.T, key string) {
t.Helper()
gc := &git.Client{}
cmd, err := gc.Command(context.Background(), "config", "--global", "--add", key, "test-helper")
require.NoError(t, err)
require.NoError(t, cmd.Run())
}
func TestHelperConfigContract(t *testing.T) {
contract.HelperConfig{
NewHelperConfig: func(t *testing.T) shared.HelperConfig {
withIsolatedGitConfig(t)
return &gitcredentials.HelperConfig{
SelfExecutablePath: "/path/to/gh",
GitClient: &git.Client{},
}
},
ConfigureHelper: func(t *testing.T, hostname string) {
configureTestCredentialHelper(t, hostname)
},
}.Test(t)
}
// This is a whitebox test unlike the contract because although we don't use the exact configured command, it's
// important that it is exactly right since git uses it.
func TestSetsCorrectCommandInGitConfig(t *testing.T) {
withIsolatedGitConfig(t)
gc := &git.Client{}
hc := &gitcredentials.HelperConfig{
SelfExecutablePath: "/path/to/gh",
GitClient: gc,
}
require.NoError(t, hc.ConfigureOurs("github.com"))
// Check that the correct command was set in the git config
cmd, err := gc.Command(context.Background(), "config", "--get", "credential.https://github.com.helper")
require.NoError(t, err)
output, err := cmd.Output()
require.NoError(t, err)
require.Equal(t, "!/path/to/gh auth git-credential\n", string(output))
}
func TestHelperIsOurs(t *testing.T) {
tests := []struct {
name string
cmd string
want bool
windowsOnly bool
}{
{
name: "blank",
cmd: "",
want: false,
},
{
name: "invalid",
cmd: "!",
want: false,
},
{
name: "osxkeychain",
cmd: "osxkeychain",
want: false,
},
{
name: "looks like gh but isn't",
cmd: "gh auth",
want: false,
},
{
name: "ours",
cmd: "!/path/to/gh auth",
want: true,
},
{
name: "ours - Windows edition",
cmd: `!'C:\Program Files\GitHub CLI\gh.exe' auth git-credential`,
want: true,
windowsOnly: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.windowsOnly && runtime.GOOS != "windows" {
t.Skip("skipping test on non-Windows platform")
}
h := gitcredentials.Helper{Cmd: tt.cmd}
if got := h.IsOurs(); got != tt.want {
t.Errorf("IsOurs() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -0,0 +1,55 @@
package gitcredentials
import (
"bytes"
"context"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/git"
)
// An Updater is used to update the git credentials for a given hostname.
type Updater struct {
GitClient *git.Client
}
// Update updates the git credentials for a given hostname, first by rejecting any existing credentials and then
// approving the new credentials.
func (u *Updater) Update(hostname, username, password string) error {
ctx := context.TODO()
// clear previous cached credentials
rejectCmd, err := u.GitClient.Command(ctx, "credential", "reject")
if err != nil {
return err
}
rejectCmd.Stdin = bytes.NewBufferString(heredoc.Docf(`
protocol=https
host=%s
`, hostname))
_, err = rejectCmd.Output()
if err != nil {
return err
}
approveCmd, err := u.GitClient.Command(ctx, "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 = approveCmd.Output()
if err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,85 @@
package gitcredentials_test
import (
"bytes"
"context"
"fmt"
"path/filepath"
"testing"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/pkg/cmd/auth/shared/gitcredentials"
"github.com/stretchr/testify/require"
)
func configureStoreCredentialHelper(t *testing.T) {
t.Helper()
tmpCredentialsFile := filepath.Join(t.TempDir(), "credentials")
gc := &git.Client{}
// Use `--file` to store credentials in a temporary file that gets cleaned up when the test has finished running
cmd, err := gc.Command(context.Background(), "config", "--global", "--add", "credential.helper", fmt.Sprintf("store --file %s", tmpCredentialsFile))
require.NoError(t, err)
require.NoError(t, cmd.Run())
}
func fillCredentials(t *testing.T) string {
gc := &git.Client{}
fillCmd, err := gc.Command(context.Background(), "credential", "fill")
require.NoError(t, err)
fillCmd.Stdin = bytes.NewBufferString(heredoc.Docf(`
protocol=https
host=%s
`, "github.com"))
b, err := fillCmd.Output()
require.NoError(t, err)
return string(b)
}
func TestUpdateAddsNewCredentials(t *testing.T) {
// Given we have an isolated git config and we're using the built in store credential helper
// https://git-scm.com/docs/git-credential-store
withIsolatedGitConfig(t)
configureStoreCredentialHelper(t)
// When we add new credentials
u := &gitcredentials.Updater{
GitClient: &git.Client{},
}
require.NoError(t, u.Update("github.com", "monalisa", "password"))
// Then our credential description is successfully filled
require.Equal(t, heredoc.Doc(`
protocol=https
host=github.com
username=monalisa
password=password
`), fillCredentials(t))
}
func TestUpdateReplacesOldCredentials(t *testing.T) {
// Given we have an isolated git config and we're using the built in store credential helper
// https://git-scm.com/docs/git-credential-store
// and we have existing credentials
withIsolatedGitConfig(t)
configureStoreCredentialHelper(t)
// When we replace old credentials
u := &gitcredentials.Updater{
GitClient: &git.Client{},
}
require.NoError(t, u.Update("github.com", "monalisa", "old-password"))
require.NoError(t, u.Update("github.com", "monalisa", "new-password"))
// Then our credential description is successfully filled
require.Equal(t, heredoc.Doc(`
protocol=https
host=github.com
username=monalisa
password=new-password
`), fillCredentials(t))
}

View file

@ -11,7 +11,6 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/authflow"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/ghinstance"
@ -31,15 +30,14 @@ type LoginOptions struct {
IO *iostreams.IOStreams
Config iconfig
HTTPClient *http.Client
GitClient *git.Client
Hostname string
Interactive bool
Web bool
Scopes []string
Executable string
GitProtocol string
Prompter Prompt
Browser browser.Browser
CredentialFlow *GitCredentialFlow
SecureStorage bool
SkipSSHKeyPrompt bool
@ -71,16 +69,11 @@ func Login(opts *LoginOptions) error {
var additionalScopes []string
credentialFlow := &GitCredentialFlow{
Executable: opts.Executable,
Prompter: opts.Prompter,
GitClient: opts.GitClient,
}
if opts.Interactive && gitProtocol == "https" {
if err := credentialFlow.Prompt(hostname); err != nil {
if err := opts.CredentialFlow.Prompt(hostname); err != nil {
return err
}
additionalScopes = append(additionalScopes, credentialFlow.Scopes()...)
additionalScopes = append(additionalScopes, opts.CredentialFlow.Scopes()...)
}
var keyToUpload string
@ -208,8 +201,8 @@ func Login(opts *LoginOptions) error {
fmt.Fprintf(opts.IO.ErrOut, "%s Authentication credentials saved in plain text\n", cs.Yellow("!"))
}
if credentialFlow.ShouldSetup() {
err := credentialFlow.Setup(hostname, username, authToken)
if opts.CredentialFlow.ShouldSetup() {
err := opts.CredentialFlow.Setup(hostname, username, authToken)
if err != nil {
return err
}

View file

@ -10,10 +10,12 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/internal/run"
"github.com/cli/cli/v2/pkg/cmd/auth/shared/gitcredentials"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/ssh"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type tinyConfig map[string]string
@ -255,6 +257,11 @@ func TestLogin(t *testing.T) {
tt.opts.IO = ios
tt.opts.Config = &cfg
tt.opts.HTTPClient = &http.Client{Transport: reg}
tt.opts.CredentialFlow = &GitCredentialFlow{
// Intentionally not instantiating anything in here because the tests do not hit this code path.
// Right now it's better to panic if we write a test that hits the code than say, start calling
// out to git unintentionally.
}
if tt.runStubs != nil {
rs, runRestore := run.Stub()
@ -282,6 +289,61 @@ func TestLogin(t *testing.T) {
}
}
func TestAuthenticatingGitCredentials(t *testing.T) {
// Given we have no host or global credential helpers configured
// And given they have chosen https as their git protocol
// When they choose to authenticate git with their GitHub credentials
// Then gh is configured as their credential helper for that host
ios, _, _, _ := iostreams.Test()
reg := &httpmock.Registry{}
defer reg.Verify(t)
reg.Register(
httpmock.REST("GET", "api/v3/"),
httpmock.ScopesResponder("repo,read:org"))
reg.Register(
httpmock.GraphQL(`query UserCurrent\b`),
httpmock.StringResponse(`{"data":{"viewer":{ "login": "monalisa" }}}`))
opts := &LoginOptions{
IO: ios,
Config: tinyConfig{},
HTTPClient: &http.Client{Transport: reg},
Hostname: "example.com",
Interactive: true,
GitProtocol: "https",
Prompter: &prompter.PrompterMock{
SelectFunc: func(prompt, _ string, opts []string) (int, error) {
if prompt == "How would you like to authenticate GitHub CLI?" {
return prompter.IndexFor(opts, "Paste an authentication token")
}
return -1, prompter.NoSuchPromptErr(prompt)
},
AuthTokenFunc: func() (string, error) {
return "ATOKEN", nil
},
},
CredentialFlow: &GitCredentialFlow{
Prompter: &prompter.PrompterMock{
ConfirmFunc: func(prompt string, _ bool) (bool, error) {
return true, nil
},
},
HelperConfig: &gitcredentials.FakeHelperConfig{
SelfExecutablePath: "/path/to/gh",
Helpers: map[string]gitcredentials.Helper{},
},
// Updater not required for this test as we will be setting gh as the helper
},
}
require.NoError(t, Login(opts))
helper, err := opts.CredentialFlow.HelperConfig.ConfiguredHelper("example.com")
require.NoError(t, err)
require.True(t, helper.IsOurs(), "expected gh to be the configured helper")
}
func Test_scopesSentence(t *testing.T) {
type args struct {
scopes []string

View file

@ -3,10 +3,10 @@ package shared
import (
"strings"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
)
func AuthTokenWriteable(authCfg *config.AuthConfig, hostname string) (string, bool) {
func AuthTokenWriteable(authCfg gh.AuthConfig, hostname string) (string, bool) {
token, src := authCfg.ActiveToken(hostname)
return src, (token == "" || !strings.HasSuffix(src, "_TOKEN"))
}

View file

@ -12,6 +12,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -124,7 +125,7 @@ func (e Entries) Strings(cs *iostreams.ColorScheme) []string {
type StatusOptions struct {
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
Config func() (config.Config, error)
Config func() (gh.Config, error)
Hostname string
ShowToken bool
@ -200,7 +201,7 @@ func statusRun(opts *StatusOptions) error {
}
var activeUser string
gitProtocol := cfg.GitProtocol(hostname)
gitProtocol := cfg.GitProtocol(hostname).Value
activeUserToken, activeUserTokenSource := authCfg.ActiveToken(hostname)
if authTokenWriteable(activeUserTokenSource) {
activeUser, _ = authCfg.ActiveUser(hostname)

View file

@ -10,6 +10,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
@ -80,7 +81,7 @@ func Test_statusRun(t *testing.T) {
opts StatusOptions
env map[string]string
httpStubs func(*httpmock.Registry)
cfgStubs func(*testing.T, config.Config)
cfgStubs func(*testing.T, gh.Config)
wantErr error
wantOut string
wantErrOut string
@ -90,7 +91,7 @@ func Test_statusRun(t *testing.T) {
opts: StatusOptions{
Hostname: "github.com",
},
cfgStubs: func(t *testing.T, c config.Config) {
cfgStubs: func(t *testing.T, c gh.Config) {
login(t, c, "github.com", "monalisa", "abc123", "https")
},
httpStubs: func(reg *httpmock.Registry) {
@ -110,7 +111,7 @@ func Test_statusRun(t *testing.T) {
opts: StatusOptions{
Hostname: "ghe.io",
},
cfgStubs: func(t *testing.T, c config.Config) {
cfgStubs: func(t *testing.T, c gh.Config) {
login(t, c, "github.com", "monalisa", "gho_abc123", "https")
login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https")
},
@ -130,7 +131,7 @@ func Test_statusRun(t *testing.T) {
{
name: "missing scope",
opts: StatusOptions{},
cfgStubs: func(t *testing.T, c config.Config) {
cfgStubs: func(t *testing.T, c gh.Config) {
login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https")
},
httpStubs: func(reg *httpmock.Registry) {
@ -151,7 +152,7 @@ func Test_statusRun(t *testing.T) {
{
name: "bad token",
opts: StatusOptions{},
cfgStubs: func(t *testing.T, c config.Config) {
cfgStubs: func(t *testing.T, c gh.Config) {
login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https")
},
httpStubs: func(reg *httpmock.Registry) {
@ -170,7 +171,7 @@ func Test_statusRun(t *testing.T) {
{
name: "all good",
opts: StatusOptions{},
cfgStubs: func(t *testing.T, c config.Config) {
cfgStubs: func(t *testing.T, c gh.Config) {
login(t, c, "github.com", "monalisa", "gho_abc123", "https")
login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "ssh")
},
@ -204,7 +205,7 @@ func Test_statusRun(t *testing.T) {
name: "token from env",
opts: StatusOptions{},
env: map[string]string{"GH_TOKEN": "gho_abc123"},
cfgStubs: func(t *testing.T, c config.Config) {},
cfgStubs: func(t *testing.T, c gh.Config) {},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("GET", ""),
@ -225,7 +226,7 @@ func Test_statusRun(t *testing.T) {
{
name: "server-to-server token",
opts: StatusOptions{},
cfgStubs: func(t *testing.T, c config.Config) {
cfgStubs: func(t *testing.T, c gh.Config) {
login(t, c, "github.com", "monalisa", "ghs_abc123", "https")
},
httpStubs: func(reg *httpmock.Registry) {
@ -245,7 +246,7 @@ func Test_statusRun(t *testing.T) {
{
name: "PAT V2 token",
opts: StatusOptions{},
cfgStubs: func(t *testing.T, c config.Config) {
cfgStubs: func(t *testing.T, c gh.Config) {
login(t, c, "github.com", "monalisa", "github_pat_abc123", "https")
},
httpStubs: func(reg *httpmock.Registry) {
@ -267,7 +268,7 @@ func Test_statusRun(t *testing.T) {
opts: StatusOptions{
ShowToken: true,
},
cfgStubs: func(t *testing.T, c config.Config) {
cfgStubs: func(t *testing.T, c gh.Config) {
login(t, c, "github.com", "monalisa", "gho_abc123", "https")
login(t, c, "ghe.io", "monalisa-ghe", "gho_xyz456", "https")
},
@ -298,7 +299,7 @@ func Test_statusRun(t *testing.T) {
opts: StatusOptions{
Hostname: "github.example.com",
},
cfgStubs: func(t *testing.T, c config.Config) {
cfgStubs: func(t *testing.T, c gh.Config) {
login(t, c, "github.com", "monalisa", "abc123", "https")
},
httpStubs: func(reg *httpmock.Registry) {},
@ -308,7 +309,7 @@ func Test_statusRun(t *testing.T) {
{
name: "multiple accounts on a host",
opts: StatusOptions{},
cfgStubs: func(t *testing.T, c config.Config) {
cfgStubs: func(t *testing.T, c gh.Config) {
login(t, c, "github.com", "monalisa", "gho_abc123", "https")
login(t, c, "github.com", "monalisa-2", "gho_abc123", "https")
},
@ -335,7 +336,7 @@ func Test_statusRun(t *testing.T) {
name: "multiple hosts with multiple accounts with environment tokens and with errors",
opts: StatusOptions{},
env: map[string]string{"GH_ENTERPRISE_TOKEN": "gho_abc123"},
cfgStubs: func(t *testing.T, c config.Config) {
cfgStubs: func(t *testing.T, c gh.Config) {
login(t, c, "github.com", "monalisa", "gho_def456", "https")
login(t, c, "github.com", "monalisa-2", "gho_ghi789", "https")
login(t, c, "ghe.io", "monalisa-ghe", "gho_xyz123", "ssh")
@ -398,7 +399,7 @@ func Test_statusRun(t *testing.T) {
if tt.cfgStubs != nil {
tt.cfgStubs(t, cfg)
}
tt.opts.Config = func() (config.Config, error) {
tt.opts.Config = func() (gh.Config, error) {
return cfg, nil
}
@ -430,7 +431,7 @@ func Test_statusRun(t *testing.T) {
}
}
func login(t *testing.T, c config.Config, hostname, username, protocol, token string) {
func login(t *testing.T, c gh.Config, hostname, username, protocol, token string) {
t.Helper()
_, err := c.Authentication().Login(hostname, username, protocol, token, false)
require.NoError(t, err)

View file

@ -6,7 +6,7 @@ import (
"slices"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -15,7 +15,7 @@ import (
type SwitchOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
Config func() (gh.Config, error)
Prompter shared.Prompt
Hostname string
Username string

View file

@ -8,6 +8,7 @@ import (
"testing"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/keyring"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmdutil"
@ -403,7 +404,7 @@ func TestSwitchRun(t *testing.T) {
}
}
tt.opts.Config = func() (config.Config, error) {
tt.opts.Config = func() (gh.Config, error) {
return cfg, nil
}

View file

@ -4,7 +4,7 @@ import (
"fmt"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
@ -12,7 +12,7 @@ import (
type TokenOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
Config func() (gh.Config, error)
Hostname string
Username string

View file

@ -5,6 +5,7 @@ import (
"testing"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/google/shlex"
@ -56,7 +57,7 @@ func TestNewCmdToken(t *testing.T) {
ios, _, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: ios,
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
cfg := config.NewBlankConfig()
return cfg, nil
},
@ -96,7 +97,7 @@ func TestTokenRun(t *testing.T) {
name string
opts TokenOptions
env map[string]string
cfgStubs func(*testing.T, config.Config)
cfgStubs func(*testing.T, gh.Config)
wantStdout string
wantErr bool
wantErrMsg string
@ -104,7 +105,7 @@ func TestTokenRun(t *testing.T) {
{
name: "token",
opts: TokenOptions{},
cfgStubs: func(t *testing.T, cfg config.Config) {
cfgStubs: func(t *testing.T, cfg gh.Config) {
login(t, cfg, "github.com", "test-user", "gho_ABCDEFG", "https", false)
},
wantStdout: "gho_ABCDEFG\n",
@ -114,7 +115,7 @@ func TestTokenRun(t *testing.T) {
opts: TokenOptions{
Hostname: "github.mycompany.com",
},
cfgStubs: func(t *testing.T, cfg config.Config) {
cfgStubs: func(t *testing.T, cfg gh.Config) {
login(t, cfg, "github.com", "test-user", "gho_ABCDEFG", "https", false)
login(t, cfg, "github.mycompany.com", "test-user", "gho_1234567", "https", false)
},
@ -138,7 +139,7 @@ func TestTokenRun(t *testing.T) {
{
name: "uses default host when one is not provided",
opts: TokenOptions{},
cfgStubs: func(t *testing.T, cfg config.Config) {
cfgStubs: func(t *testing.T, cfg gh.Config) {
login(t, cfg, "github.com", "test-user", "gho_ABCDEFG", "https", false)
login(t, cfg, "github.mycompany.com", "test-user", "gho_1234567", "https", false)
},
@ -151,7 +152,7 @@ func TestTokenRun(t *testing.T) {
Hostname: "github.com",
Username: "test-user",
},
cfgStubs: func(t *testing.T, cfg config.Config) {
cfgStubs: func(t *testing.T, cfg gh.Config) {
login(t, cfg, "github.com", "test-user", "gho_ABCDEFG", "https", false)
login(t, cfg, "github.com", "test-user-2", "gho_1234567", "https", false)
},
@ -173,7 +174,7 @@ func TestTokenRun(t *testing.T) {
tt.cfgStubs(t, cfg)
}
tt.opts.Config = func() (config.Config, error) {
tt.opts.Config = func() (gh.Config, error) {
return cfg, nil
}
@ -193,7 +194,7 @@ func TestTokenRunSecureStorage(t *testing.T) {
tests := []struct {
name string
opts TokenOptions
cfgStubs func(*testing.T, config.Config)
cfgStubs func(*testing.T, gh.Config)
wantStdout string
wantErr bool
wantErrMsg string
@ -201,7 +202,7 @@ func TestTokenRunSecureStorage(t *testing.T) {
{
name: "token",
opts: TokenOptions{},
cfgStubs: func(t *testing.T, cfg config.Config) {
cfgStubs: func(t *testing.T, cfg gh.Config) {
login(t, cfg, "github.com", "test-user", "gho_ABCDEFG", "https", true)
},
wantStdout: "gho_ABCDEFG\n",
@ -211,7 +212,7 @@ func TestTokenRunSecureStorage(t *testing.T) {
opts: TokenOptions{
Hostname: "mycompany.com",
},
cfgStubs: func(t *testing.T, cfg config.Config) {
cfgStubs: func(t *testing.T, cfg gh.Config) {
login(t, cfg, "mycompany.com", "test-user", "gho_1234567", "https", true)
},
wantStdout: "gho_1234567\n",
@ -237,7 +238,7 @@ func TestTokenRunSecureStorage(t *testing.T) {
Hostname: "github.com",
Username: "test-user",
},
cfgStubs: func(t *testing.T, cfg config.Config) {
cfgStubs: func(t *testing.T, cfg gh.Config) {
login(t, cfg, "github.com", "test-user", "gho_ABCDEFG", "https", true)
login(t, cfg, "github.com", "test-user-2", "gho_1234567", "https", true)
},
@ -256,7 +257,7 @@ func TestTokenRunSecureStorage(t *testing.T) {
tt.cfgStubs(t, cfg)
}
tt.opts.Config = func() (config.Config, error) {
tt.opts.Config = func() (gh.Config, error) {
return cfg, nil
}
@ -272,7 +273,7 @@ func TestTokenRunSecureStorage(t *testing.T) {
}
}
func login(t *testing.T, c config.Config, hostname, username, token, gitProtocol string, secureStorage bool) {
func login(t *testing.T, c gh.Config, hostname, username, token, gitProtocol string, secureStorage bool) {
t.Helper()
_, err := c.Authentication().Login(hostname, username, token, gitProtocol, secureStorage)
require.NoError(t, err)

View file

@ -11,8 +11,8 @@ import (
func NewCmdCache(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "cache <command>",
Short: "Manage Github Actions caches",
Long: "Work with Github Actions caches.",
Short: "Manage GitHub Actions caches",
Long: "Work with GitHub Actions caches.",
Example: heredoc.Doc(`
$ gh cache list
$ gh cache delete --all

View file

@ -34,9 +34,9 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
cmd := &cobra.Command{
Use: "delete [<cache-id>| <cache-key> | --all]",
Short: "Delete Github Actions caches",
Short: "Delete GitHub Actions caches",
Long: `
Delete Github Actions caches.
Delete GitHub Actions caches.
Deletion requires authorization with the "repo" scope.
`,

View file

@ -39,7 +39,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
cmd := &cobra.Command{
Use: "list",
Short: "List Github Actions caches",
Short: "List GitHub Actions caches",
Example: heredoc.Doc(`
# List caches for current repository
$ gh cache list

View file

@ -17,7 +17,7 @@ func NewCmdConfig(f *cmdutil.Factory) *cobra.Command {
longDoc := strings.Builder{}
longDoc.WriteString("Display or change configuration settings for gh.\n\n")
longDoc.WriteString("Current respected settings:\n")
for _, co := range config.ConfigOptions() {
for _, co := range config.Options {
longDoc.WriteString(fmt.Sprintf("- `%s`: %s", co.Key, co.Description))
if len(co.AllowedValues) > 0 {
longDoc.WriteString(fmt.Sprintf(" {%s}", strings.Join(co.AllowedValues, "|")))

View file

@ -5,7 +5,7 @@ import (
"fmt"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
@ -13,7 +13,7 @@ import (
type GetOptions struct {
IO *iostreams.IOStreams
Config config.Config
Config gh.Config
Hostname string
Key string
@ -64,13 +64,22 @@ func getRun(opts *GetOptions) error {
return nil
}
val, err := opts.Config.GetOrDefault(opts.Hostname, opts.Key)
if err != nil {
return err
optionalEntry := opts.Config.GetOrDefault(opts.Hostname, opts.Key)
if optionalEntry.IsNone() {
return nonExistentKeyError{key: opts.Key}
}
val := optionalEntry.Unwrap().Value
if val != "" {
fmt.Fprintf(opts.IO.Out, "%s\n", val)
}
return nil
}
type nonExistentKeyError struct {
key string
}
func (e nonExistentKeyError) Error() string {
return fmt.Sprintf("could not find key \"%s\"", e.key)
}

View file

@ -5,10 +5,12 @@ import (
"testing"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"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"
)
func TestNewCmdConfigGet(t *testing.T) {
@ -41,7 +43,7 @@ func TestNewCmdConfigGet(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := &cmdutil.Factory{
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return config.NewBlankConfig(), nil
},
}
@ -76,17 +78,16 @@ func TestNewCmdConfigGet(t *testing.T) {
func Test_getRun(t *testing.T) {
tests := []struct {
name string
input *GetOptions
stdout string
stderr string
wantErr bool
name string
input *GetOptions
stdout string
err error
}{
{
name: "get key",
input: &GetOptions{
Key: "editor",
Config: func() config.Config {
Config: func() gh.Config {
cfg := config.NewBlankConfig()
cfg.Set("", "editor", "ed")
return cfg
@ -99,7 +100,7 @@ func Test_getRun(t *testing.T) {
input: &GetOptions{
Hostname: "github.com",
Key: "editor",
Config: func() config.Config {
Config: func() gh.Config {
cfg := config.NewBlankConfig()
cfg.Set("", "editor", "ed")
cfg.Set("github.com", "editor", "vim")
@ -108,17 +109,24 @@ func Test_getRun(t *testing.T) {
},
stdout: "vim\n",
},
{
name: "non-existent key",
input: &GetOptions{
Key: "non-existent",
Config: config.NewBlankConfig(),
},
err: nonExistentKeyError{key: "non-existent"},
},
}
for _, tt := range tests {
ios, _, stdout, stderr := iostreams.Test()
ios, _, stdout, _ := iostreams.Test()
tt.input.IO = ios
t.Run(tt.name, func(t *testing.T) {
err := getRun(tt.input)
assert.NoError(t, err)
assert.Equal(t, tt.stdout, stdout.String())
assert.Equal(t, tt.stderr, stderr.String())
require.Equal(t, err, tt.err)
require.Equal(t, tt.stdout, stdout.String())
})
}
}

View file

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
@ -11,7 +12,7 @@ import (
type ListOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
Config func() (gh.Config, error)
Hostname string
}
@ -54,14 +55,8 @@ func listRun(opts *ListOptions) error {
host, _ = cfg.Authentication().DefaultHost()
}
configOptions := config.ConfigOptions()
for _, key := range configOptions {
val, err := cfg.GetOrDefault(host, key.Key)
if err != nil {
return err
}
fmt.Fprintf(opts.IO.Out, "%s=%s\n", key.Key, val)
for _, option := range config.Options {
fmt.Fprintf(opts.IO.Out, "%s=%s\n", option.Key, option.CurrentValue(cfg, host))
}
return nil

View file

@ -5,10 +5,12 @@ import (
"testing"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"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"
)
func TestNewCmdConfigList(t *testing.T) {
@ -35,7 +37,7 @@ func TestNewCmdConfigList(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := &cmdutil.Factory{
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return config.NewBlankConfig(), nil
},
}
@ -71,13 +73,13 @@ func Test_listRun(t *testing.T) {
tests := []struct {
name string
input *ListOptions
config config.Config
config gh.Config
stdout string
wantErr bool
}{
{
name: "list",
config: func() config.Config {
config: func() gh.Config {
cfg := config.NewBlankConfig()
cfg.Set("HOST", "git_protocol", "ssh")
cfg.Set("HOST", "editor", "/usr/bin/vim")
@ -101,15 +103,14 @@ browser=brave
for _, tt := range tests {
ios, _, stdout, _ := iostreams.Test()
tt.input.IO = ios
tt.input.Config = func() (config.Config, error) {
tt.input.Config = func() (gh.Config, error) {
return tt.config, nil
}
t.Run(tt.name, func(t *testing.T) {
err := listRun(tt.input)
assert.NoError(t, err)
assert.Equal(t, tt.stdout, stdout.String())
//assert.Equal(t, tt.stderr, stderr.String())
require.NoError(t, err)
require.Equal(t, tt.stdout, stdout.String())
})
}
}

View file

@ -7,6 +7,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
@ -14,7 +15,7 @@ import (
type SetOptions struct {
IO *iostreams.IOStreams
Config config.Config
Config gh.Config
Key string
Value string
@ -87,7 +88,7 @@ func setRun(opts *SetOptions) error {
}
func ValidateKey(key string) error {
for _, configKey := range config.ConfigOptions() {
for _, configKey := range config.Options {
if key == configKey.Key {
return nil
}
@ -107,7 +108,7 @@ func (e InvalidValueError) Error() string {
func ValidateValue(key, value string) error {
var validValues []string
for _, v := range config.ConfigOptions() {
for _, v := range config.Options {
if v.Key == key {
validValues = v.AllowedValues
break

View file

@ -5,6 +5,7 @@ import (
"testing"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/google/shlex"
@ -49,7 +50,7 @@ func TestNewCmdConfigSet(t *testing.T) {
_ = config.StubWriteConfig(t)
f := &cmdutil.Factory{
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return config.NewBlankConfig(), nil
},
}
@ -149,9 +150,10 @@ func Test_setRun(t *testing.T) {
assert.Equal(t, tt.stdout, stdout.String())
assert.Equal(t, tt.stderr, stderr.String())
val, err := tt.input.Config.GetOrDefault(tt.input.Hostname, tt.input.Key)
assert.NoError(t, err)
assert.Equal(t, tt.expectedValue, val)
optionalEntry := tt.input.Config.GetOrDefault(tt.input.Hostname, tt.input.Key)
entry := optionalEntry.Expect("expected a value to be set")
assert.Equal(t, tt.expectedValue, entry.Value)
assert.Equal(t, gh.ConfigUserProvided, entry.Source)
})
}
}

View file

@ -13,7 +13,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/charmbracelet/glamour"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/extensions"
"github.com/cli/cli/v2/pkg/iostreams"
@ -33,7 +33,7 @@ type ExtBrowseOpts struct {
Em extensions.ExtensionManager
Client *http.Client
Logger *log.Logger
Cfg config.Config
Cfg gh.Config
Rg *readmeGetter
Debug bool
SingleColumn bool

View file

@ -11,6 +11,7 @@ import (
"time"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/repo/view"
"github.com/cli/cli/v2/pkg/extensions"
@ -76,7 +77,7 @@ func Test_getExtensionRepos(t *testing.T) {
}
cfg := config.NewBlankConfig()
cfg.AuthenticationFunc = func() *config.AuthConfig {
cfg.AuthenticationFunc = func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
authCfg.SetDefaultHost("github.com", "")
return authCfg

View file

@ -50,7 +50,7 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
Aliases: []string{"extensions", "ext"},
}
upgradeFunc := func(name string, flagForce, flagDryRun bool) error {
upgradeFunc := func(name string, flagForce bool) error {
cs := io.ColorScheme()
err := m.Upgrade(name, flagForce)
if err != nil {
@ -63,17 +63,11 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
}
return cmdutil.SilentError
}
if io.IsStdoutTTY() {
successStr := "Successfully"
if flagDryRun {
successStr = "Would have"
}
extensionStr := "extension"
if name == "" {
extensionStr = "extensions"
}
fmt.Fprintf(io.Out, "%s %s upgraded %s\n", cs.SuccessIcon(), successStr, extensionStr)
fmt.Fprintf(io.Out, "%s Successfully checked extension upgrades\n", cs.SuccessIcon())
}
return nil
}
@ -336,7 +330,7 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
if ext, err := checkValidExtension(cmd.Root(), m, repo.RepoName(), repo.RepoOwner()); err != nil {
// If an existing extension was found and --force was specified, attempt to upgrade.
if forceFlag && ext != nil {
return upgradeFunc(ext.Name(), forceFlag, false)
return upgradeFunc(ext.Name(), forceFlag)
}
if errors.Is(err, alreadyInstalledError) {
@ -405,7 +399,7 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
if flagDryRun {
m.EnableDryRunMode()
}
return upgradeFunc(name, flagForce, flagDryRun)
return upgradeFunc(name, flagForce)
},
}
cmd.Flags().BoolVar(&flagAll, "all", false, "Upgrade all extensions")

View file

@ -13,6 +13,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmdutil"
@ -331,7 +332,7 @@ func TestNewCmdExtension(t *testing.T) {
}
},
isTTY: true,
wantStdout: "✓ Successfully upgraded extension\n",
wantStdout: "✓ Successfully checked extension upgrades\n",
},
{
name: "upgrade an extension dry run",
@ -351,7 +352,7 @@ func TestNewCmdExtension(t *testing.T) {
}
},
isTTY: true,
wantStdout: "✓ Would have upgraded extension\n",
wantStdout: "✓ Successfully checked extension upgrades\n",
},
{
name: "upgrade an extension notty",
@ -384,7 +385,7 @@ func TestNewCmdExtension(t *testing.T) {
}
},
isTTY: true,
wantStdout: "✓ Successfully upgraded extension\n",
wantStdout: "✓ Successfully checked extension upgrades\n",
},
{
name: "upgrade extension error",
@ -419,7 +420,7 @@ func TestNewCmdExtension(t *testing.T) {
}
},
isTTY: true,
wantStdout: "✓ Successfully upgraded extension\n",
wantStdout: "✓ Successfully checked extension upgrades\n",
},
{
name: "upgrade an extension full name",
@ -435,7 +436,7 @@ func TestNewCmdExtension(t *testing.T) {
}
},
isTTY: true,
wantStdout: "✓ Successfully upgraded extension\n",
wantStdout: "✓ Successfully checked extension upgrades\n",
},
{
name: "upgrade all",
@ -451,7 +452,7 @@ func TestNewCmdExtension(t *testing.T) {
}
},
isTTY: true,
wantStdout: "✓ Successfully upgraded extensions\n",
wantStdout: "✓ Successfully checked extension upgrades\n",
},
{
name: "upgrade all dry run",
@ -471,7 +472,7 @@ func TestNewCmdExtension(t *testing.T) {
}
},
isTTY: true,
wantStdout: "✓ Would have upgraded extensions\n",
wantStdout: "✓ Successfully checked extension upgrades\n",
},
{
name: "upgrade all none installed",
@ -852,7 +853,7 @@ func TestNewCmdExtension(t *testing.T) {
}
},
isTTY: true,
wantStdout: "✓ Successfully upgraded extension\n",
wantStdout: "✓ Successfully checked extension upgrades\n",
},
}
@ -888,7 +889,7 @@ func TestNewCmdExtension(t *testing.T) {
}
f := cmdutil.Factory{
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return config.NewBlankConfig(), nil
},
IOStreams: ios,

View file

@ -17,6 +17,7 @@ import (
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/extensions"
"github.com/cli/cli/v2/pkg/findsh"
@ -36,7 +37,7 @@ type Manager struct {
platform func() (string, string)
client *http.Client
gitClient gitClient
config config.Config
config gh.Config
io *iostreams.IOStreams
dryRunMode bool
}
@ -59,7 +60,7 @@ func NewManager(ios *iostreams.IOStreams, gc *git.Client) *Manager {
}
}
func (m *Manager) SetConfig(cfg config.Config) {
func (m *Manager) SetConfig(cfg gh.Config) {
m.config = cfg
}
@ -346,7 +347,7 @@ func writeManifest(dir, name string, data []byte) (writeErr error) {
}
func (m *Manager) installGit(repo ghrepo.Interface, target string) error {
protocol := m.config.GitProtocol(repo.RepoHost())
protocol := m.config.GitProtocol(repo.RepoHost()).Value
cloneURL := ghrepo.FormatRemoteURL(repo, protocol)
var commitSHA string

View file

@ -13,6 +13,7 @@ import (
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmd/extension"
@ -134,10 +135,10 @@ func newPrompter(f *cmdutil.Factory) prompter.Prompter {
return prompter.New(editor, io.In, io.Out, io.ErrOut)
}
func configFunc() func() (config.Config, error) {
var cachedConfig config.Config
func configFunc() func() (gh.Config, error) {
var cachedConfig gh.Config
var configError error
return func() (config.Config, error) {
return func() (gh.Config, error) {
if cachedConfig != nil || configError != nil {
return cachedConfig, configError
}
@ -184,7 +185,7 @@ func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams {
if _, ghPromptDisabled := os.LookupEnv("GH_PROMPT_DISABLED"); ghPromptDisabled {
io.SetNeverPrompt(true)
} else if prompt := cfg.Prompt(""); prompt == "disabled" {
} else if prompt := cfg.Prompt(""); prompt.Value == "disabled" {
io.SetNeverPrompt(true)
}
@ -194,8 +195,8 @@ func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams {
// 3. PAGER
if ghPager, ghPagerExists := os.LookupEnv("GH_PAGER"); ghPagerExists {
io.SetPager(ghPager)
} else if pager := cfg.Pager(""); pager != "" {
io.SetPager(pager)
} else if pager := cfg.Pager(""); pager.Value != "" {
io.SetPager(pager.Value)
}
return io

View file

@ -9,6 +9,8 @@ import (
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
ghmock "github.com/cli/cli/v2/internal/gh/mock"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
@ -69,9 +71,9 @@ func Test_BaseRepo(t *testing.T) {
readRemotes: func() (git.RemoteSet, error) {
return tt.remotes, nil
},
getConfig: func() (config.Config, error) {
cfg := &config.ConfigMock{}
cfg.AuthenticationFunc = func() *config.AuthConfig {
getConfig: func() (gh.Config, error) {
cfg := &ghmock.ConfigMock{}
cfg.AuthenticationFunc = func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
hosts := []string{"nonsense.com"}
if tt.override != "" {
@ -207,9 +209,9 @@ func Test_SmartBaseRepo(t *testing.T) {
readRemotes: func() (git.RemoteSet, error) {
return tt.remotes, nil
},
getConfig: func() (config.Config, error) {
cfg := &config.ConfigMock{}
cfg.AuthenticationFunc = func() *config.AuthConfig {
getConfig: func() (gh.Config, error) {
cfg := &ghmock.ConfigMock{}
cfg.AuthenticationFunc = func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
hosts := []string{"nonsense.com"}
if tt.override != "" {
@ -256,7 +258,7 @@ func Test_OverrideBaseRepo(t *testing.T) {
tests := []struct {
name string
remotes git.RemoteSet
config config.Config
config gh.Config
envOverride string
argOverride string
wantsErr bool
@ -300,7 +302,7 @@ func Test_OverrideBaseRepo(t *testing.T) {
readRemotes: func() (git.RemoteSet, error) {
return tt.remotes, nil
},
getConfig: func() (config.Config, error) {
getConfig: func() (gh.Config, error) {
return tt.config, nil
},
}
@ -323,7 +325,7 @@ func Test_ioStreams_pager(t *testing.T) {
tests := []struct {
name string
env map[string]string
config config.Config
config gh.Config
wantPager string
}{
{
@ -374,7 +376,7 @@ func Test_ioStreams_pager(t *testing.T) {
}
}
f := New("1")
f.Config = func() (config.Config, error) {
f.Config = func() (gh.Config, error) {
if tt.config == nil {
return config.NewBlankConfig(), nil
} else {
@ -390,7 +392,7 @@ func Test_ioStreams_pager(t *testing.T) {
func Test_ioStreams_prompt(t *testing.T) {
tests := []struct {
name string
config config.Config
config gh.Config
promptDisabled bool
env map[string]string
}{
@ -417,7 +419,7 @@ func Test_ioStreams_prompt(t *testing.T) {
}
}
f := New("1")
f.Config = func() (config.Config, error) {
f.Config = func() (gh.Config, error) {
if tt.config == nil {
return config.NewBlankConfig(), nil
} else {
@ -458,7 +460,7 @@ func TestSSOURL(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := New("1")
f.Config = func() (config.Config, error) {
f.Config = func() (gh.Config, error) {
return config.NewBlankConfig(), nil
}
ios, _, _, stderr := iostreams.Test()
@ -487,7 +489,7 @@ func TestSSOURL(t *testing.T) {
func TestNewGitClient(t *testing.T) {
tests := []struct {
name string
config config.Config
config gh.Config
executable string
wantAuthHosts []string
wantGhPath string
@ -503,7 +505,7 @@ func TestNewGitClient(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := New("1")
f.Config = func() (config.Config, error) {
f.Config = func() (gh.Config, error) {
if tt.config == nil {
return config.NewBlankConfig(), nil
} else {
@ -522,16 +524,16 @@ func TestNewGitClient(t *testing.T) {
}
}
func defaultConfig() *config.ConfigMock {
func defaultConfig() *ghmock.ConfigMock {
cfg := config.NewFromString("")
cfg.Set("nonsense.com", "oauth_token", "BLAH")
return cfg
}
func pagerConfig() config.Config {
func pagerConfig() gh.Config {
return config.NewFromString("pager: CONFIG_PAGER")
}
func disablePromptConfig() config.Config {
func disablePromptConfig() gh.Config {
return config.NewFromString("prompt: disabled")
}

View file

@ -7,7 +7,7 @@ import (
"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/gh"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/pkg/set"
"github.com/cli/go-gh/v2/pkg/ssh"
@ -19,7 +19,7 @@ const (
type remoteResolver struct {
readRemotes func() (git.RemoteSet, error)
getConfig func() (config.Config, error)
getConfig func() (gh.Config, error)
urlTranslator context.Translator
}

View file

@ -6,6 +6,8 @@ import (
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
ghmock "github.com/cli/cli/v2/internal/gh/mock"
"github.com/stretchr/testify/assert"
)
@ -19,7 +21,7 @@ func Test_remoteResolver(t *testing.T) {
tests := []struct {
name string
remotes func() (git.RemoteSet, error)
config config.Config
config gh.Config
output []string
wantsErr bool
}{
@ -30,9 +32,9 @@ func Test_remoteResolver(t *testing.T) {
git.NewRemote("origin", "https://github.com/owner/repo.git"),
}, nil
},
config: func() config.Config {
cfg := &config.ConfigMock{}
cfg.AuthenticationFunc = func() *config.AuthConfig {
config: func() gh.Config {
cfg := &ghmock.ConfigMock{}
cfg.AuthenticationFunc = func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
authCfg.SetHosts([]string{})
authCfg.SetDefaultHost("github.com", "default")
@ -47,9 +49,9 @@ func Test_remoteResolver(t *testing.T) {
remotes: func() (git.RemoteSet, error) {
return git.RemoteSet{}, nil
},
config: func() config.Config {
cfg := &config.ConfigMock{}
cfg.AuthenticationFunc = func() *config.AuthConfig {
config: func() gh.Config {
cfg := &ghmock.ConfigMock{}
cfg.AuthenticationFunc = func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
authCfg.SetHosts([]string{"example.com"})
authCfg.SetDefaultHost("example.com", "hosts")
@ -66,9 +68,9 @@ func Test_remoteResolver(t *testing.T) {
git.NewRemote("origin", "https://test.com/owner/repo.git"),
}, nil
},
config: func() config.Config {
cfg := &config.ConfigMock{}
cfg.AuthenticationFunc = func() *config.AuthConfig {
config: func() gh.Config {
cfg := &ghmock.ConfigMock{}
cfg.AuthenticationFunc = func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
authCfg.SetHosts([]string{"example.com"})
authCfg.SetActiveToken("", "")
@ -86,9 +88,9 @@ func Test_remoteResolver(t *testing.T) {
git.NewRemote("origin", "https://github.com/owner/repo.git"),
}, nil
},
config: func() config.Config {
cfg := &config.ConfigMock{}
cfg.AuthenticationFunc = func() *config.AuthConfig {
config: func() gh.Config {
cfg := &ghmock.ConfigMock{}
cfg.AuthenticationFunc = func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
authCfg.SetHosts([]string{"example.com"})
authCfg.SetDefaultHost("example.com", "hosts")
@ -105,9 +107,9 @@ func Test_remoteResolver(t *testing.T) {
git.NewRemote("origin", "https://example.com/owner/repo.git"),
}, nil
},
config: func() config.Config {
cfg := &config.ConfigMock{}
cfg.AuthenticationFunc = func() *config.AuthConfig {
config: func() gh.Config {
cfg := &ghmock.ConfigMock{}
cfg.AuthenticationFunc = func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
authCfg.SetHosts([]string{"example.com"})
authCfg.SetDefaultHost("example.com", "default")
@ -127,9 +129,9 @@ func Test_remoteResolver(t *testing.T) {
git.NewRemote("fork", "https://example.com/owner/repo.git"),
}, nil
},
config: func() config.Config {
cfg := &config.ConfigMock{}
cfg.AuthenticationFunc = func() *config.AuthConfig {
config: func() gh.Config {
cfg := &ghmock.ConfigMock{}
cfg.AuthenticationFunc = func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
authCfg.SetHosts([]string{"example.com"})
authCfg.SetDefaultHost("example.com", "default")
@ -146,9 +148,9 @@ func Test_remoteResolver(t *testing.T) {
git.NewRemote("origin", "https://test.com/owner/repo.git"),
}, nil
},
config: func() config.Config {
cfg := &config.ConfigMock{}
cfg.AuthenticationFunc = func() *config.AuthConfig {
config: func() gh.Config {
cfg := &ghmock.ConfigMock{}
cfg.AuthenticationFunc = func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
authCfg.SetHosts([]string{"example.com", "github.com"})
authCfg.SetActiveToken("", "")
@ -167,9 +169,9 @@ func Test_remoteResolver(t *testing.T) {
git.NewRemote("origin", "https://example.com/owner/repo.git"),
}, nil
},
config: func() config.Config {
cfg := &config.ConfigMock{}
cfg.AuthenticationFunc = func() *config.AuthConfig {
config: func() gh.Config {
cfg := &ghmock.ConfigMock{}
cfg.AuthenticationFunc = func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
authCfg.SetHosts([]string{"example.com", "github.com"})
authCfg.SetDefaultHost("github.com", "default")
@ -190,9 +192,9 @@ func Test_remoteResolver(t *testing.T) {
git.NewRemote("test", "https://test.com/owner/repo.git"),
}, nil
},
config: func() config.Config {
cfg := &config.ConfigMock{}
cfg.AuthenticationFunc = func() *config.AuthConfig {
config: func() gh.Config {
cfg := &ghmock.ConfigMock{}
cfg.AuthenticationFunc = func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
authCfg.SetHosts([]string{"example.com", "github.com"})
authCfg.SetDefaultHost("github.com", "default")
@ -209,9 +211,9 @@ func Test_remoteResolver(t *testing.T) {
git.NewRemote("origin", "https://example.com/owner/repo.git"),
}, nil
},
config: func() config.Config {
cfg := &config.ConfigMock{}
cfg.AuthenticationFunc = func() *config.AuthConfig {
config: func() gh.Config {
cfg := &ghmock.ConfigMock{}
cfg.AuthenticationFunc = func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
authCfg.SetHosts([]string{"example.com"})
authCfg.SetDefaultHost("test.com", "GH_HOST")
@ -229,9 +231,9 @@ func Test_remoteResolver(t *testing.T) {
git.NewRemote("origin", "https://test.com/owner/repo.git"),
}, nil
},
config: func() config.Config {
cfg := &config.ConfigMock{}
cfg.AuthenticationFunc = func() *config.AuthConfig {
config: func() gh.Config {
cfg := &ghmock.ConfigMock{}
cfg.AuthenticationFunc = func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
authCfg.SetHosts([]string{"example.com"})
authCfg.SetDefaultHost("test.com", "GH_HOST")
@ -250,9 +252,9 @@ func Test_remoteResolver(t *testing.T) {
git.NewRemote("origin", "https://test.com/owner/repo.git"),
}, nil
},
config: func() config.Config {
cfg := &config.ConfigMock{}
cfg.AuthenticationFunc = func() *config.AuthConfig {
config: func() gh.Config {
cfg := &ghmock.ConfigMock{}
cfg.AuthenticationFunc = func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
authCfg.SetHosts([]string{"example.com", "test.com"})
authCfg.SetDefaultHost("test.com", "GH_HOST")
@ -268,7 +270,7 @@ func Test_remoteResolver(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
rr := &remoteResolver{
readRemotes: tt.remotes,
getConfig: func() (config.Config, error) { return tt.config, nil },
getConfig: func() (gh.Config, error) { return tt.config, nil },
urlTranslator: identityTranslator{},
}
resolver := rr.Resolver()

View file

@ -7,7 +7,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -18,7 +18,7 @@ import (
type CloneOptions struct {
HttpClient func() (*http.Client, error)
GitClient *git.Client
Config func() (config.Config, error)
Config func() (gh.Config, error)
IO *iostreams.IOStreams
GitArgs []string
@ -80,7 +80,7 @@ func cloneRun(opts *CloneOptions) error {
return err
}
hostname, _ := cfg.Authentication().DefaultHost()
protocol := cfg.GitProtocol(hostname)
protocol := cfg.GitProtocol(hostname).Value
gistURL = formatRemoteURL(hostname, gistURL, protocol)
}

View file

@ -6,6 +6,7 @@ import (
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/run"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
@ -22,7 +23,7 @@ func runCloneCommand(httpClient *http.Client, cli string) (*test.CmdOut, error)
HttpClient: func() (*http.Client, error) {
return httpClient, nil
},
Config: func() (config.Config, error) {
Config: func() (gh.Config, error) {
return config.NewBlankConfig(), nil
},
GitClient: &git.Client{

View file

@ -16,7 +16,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
@ -34,7 +34,7 @@ type CreateOptions struct {
FilenameOverride string
WebMode bool
Config func() (config.Config, error)
Config func() (gh.Config, error)
HttpClient func() (*http.Client, error)
Browser browser.Browser
}

View file

@ -13,6 +13,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/run"
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
@ -331,7 +332,7 @@ func Test_createRun(t *testing.T) {
}
tt.opts.HttpClient = mockClient
tt.opts.Config = func() (config.Config, error) {
tt.opts.Config = func() (gh.Config, error) {
return config.NewBlankConfig(), nil
}

View file

@ -7,7 +7,7 @@ import (
"strings"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -16,7 +16,7 @@ import (
type DeleteOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
Config func() (gh.Config, error)
HttpClient func() (*http.Client, error)
Selector string

View file

@ -6,6 +6,7 @@ import (
"testing"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
@ -99,7 +100,7 @@ func Test_deleteRun(t *testing.T) {
tt.opts.HttpClient = func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
}
tt.opts.Config = func() (config.Config, error) {
tt.opts.Config = func() (gh.Config, error) {
return config.NewBlankConfig(), nil
}
ios, _, _, _ := iostreams.Test()

View file

@ -13,7 +13,7 @@ import (
"strings"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
@ -27,7 +27,7 @@ var editNextOptions = []string{"Edit another file", "Submit", "Cancel"}
type EditOptions struct {
IO *iostreams.IOStreams
HttpClient func() (*http.Client, error)
Config func() (config.Config, error)
Config func() (gh.Config, error)
Prompter prompter.Prompter
Edit func(string, string, string, *iostreams.IOStreams) (string, error)

View file

@ -10,6 +10,7 @@ import (
"testing"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
@ -554,7 +555,7 @@ func Test_editRun(t *testing.T) {
tt.opts.IO = ios
tt.opts.Selector = "1234"
tt.opts.Config = func() (config.Config, error) {
tt.opts.Config = func() (gh.Config, error) {
return config.NewBlankConfig(), nil
}

View file

@ -6,7 +6,7 @@ import (
"strings"
"time"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/tableprinter"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
@ -17,7 +17,7 @@ import (
type ListOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
Config func() (gh.Config, error)
HttpClient func() (*http.Client, error)
Limit int

View file

@ -9,6 +9,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
@ -367,7 +368,7 @@ func Test_listRun(t *testing.T) {
return &http.Client{Transport: reg}, nil
}
tt.opts.Config = func() (config.Config, error) {
tt.opts.Config = func() (gh.Config, error) {
return config.NewBlankConfig(), nil
}

Some files were not shown because too many files have changed in this diff Show more