Merge branch 'trunk' into 8426-add-pr-update-cmd-no-local-update
This commit is contained in:
commit
8ac5ad7244
204 changed files with 3240 additions and 1131 deletions
23
.github/workflows/deployment.yml
vendored
23
.github/workflows/deployment.yml
vendored
|
|
@ -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
2
.gitignore
vendored
|
|
@ -12,6 +12,8 @@
|
|||
/.goreleaser.generated.yml
|
||||
/script/build
|
||||
/script/build.exe
|
||||
/pkg_payload
|
||||
/build/macOS/resources
|
||||
|
||||
# VS Code
|
||||
.vscode
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
8
Makefile
8
Makefile
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
|||
33
build/macOS/distribution.xml
Normal file
33
build/macOS/distribution.xml
Normal 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') >= 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>
|
||||
|
|
@ -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
36
docs/codespaces.md
Normal 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).
|
||||
|
|
@ -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
18
go.mod
|
|
@ -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
36
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
|
|||
|
|
@ -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("")
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
171
internal/gh/gh.go
Normal 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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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.
|
||||
//
|
||||
// }
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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' \
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
BIN
pkg/cmd/attestation/test/data/reusable-workflow-artifact
Normal file
BIN
pkg/cmd/attestation/test/data/reusable-workflow-artifact
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
64
pkg/cmd/auth/shared/contract/helper_config.go
Normal file
64
pkg/cmd/auth/shared/contract/helper_config.go
Normal 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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
49
pkg/cmd/auth/shared/gitcredentials/fake_helper_config.go
Normal file
49
pkg/cmd/auth/shared/gitcredentials/fake_helper_config.go
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
124
pkg/cmd/auth/shared/gitcredentials/helper_config.go
Normal file
124
pkg/cmd/auth/shared/gitcredentials/helper_config.go
Normal 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
|
||||
}
|
||||
125
pkg/cmd/auth/shared/gitcredentials/helper_config_test.go
Normal file
125
pkg/cmd/auth/shared/gitcredentials/helper_config_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
55
pkg/cmd/auth/shared/gitcredentials/updater.go
Normal file
55
pkg/cmd/auth/shared/gitcredentials/updater.go
Normal 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
|
||||
}
|
||||
85
pkg/cmd/auth/shared/gitcredentials/updater_test.go
Normal file
85
pkg/cmd/auth/shared/gitcredentials/updater_test.go
Normal 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))
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
4
pkg/cmd/cache/cache.go
vendored
4
pkg/cmd/cache/cache.go
vendored
|
|
@ -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
|
||||
|
|
|
|||
4
pkg/cmd/cache/delete/delete.go
vendored
4
pkg/cmd/cache/delete/delete.go
vendored
|
|
@ -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.
|
||||
`,
|
||||
|
|
|
|||
2
pkg/cmd/cache/list/list.go
vendored
2
pkg/cmd/cache/list/list.go
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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, "|")))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue