Merge branch 'trunk' into move-predicate-type-filtering
This commit is contained in:
commit
cbe80fd169
40 changed files with 874 additions and 348 deletions
4
.github/CONTRIBUTING.md
vendored
4
.github/CONTRIBUTING.md
vendored
|
|
@ -62,9 +62,9 @@ To propose a design:
|
|||
- Include a link to the issue that the design is for.
|
||||
- Describe the design you are proposing to resolve the issue, leveraging the [CLI Design System][].
|
||||
- Mock up the design you are proposing using our [Google Docs Template][] or code blocks.
|
||||
- Mock ups should cleary illustrate the command(s) being run and the expected output(s).
|
||||
- Mock ups should clearly illustrate the command(s) being run and the expected output(s).
|
||||
|
||||
### (core team only) Revewing a design
|
||||
### (core team only) Reviewing a design
|
||||
|
||||
A member of the core team will [triage](../docs/triage.md) the design proposal. Once a member of the core team has reviewed the design, they may add the [`help wanted`][hw] label to the issue, so a PR can be opened to provide the implementation.
|
||||
|
||||
|
|
|
|||
2
.github/workflows/deployment.yml
vendored
2
.github/workflows/deployment.yml
vendored
|
|
@ -301,7 +301,7 @@ jobs:
|
|||
base64 -d <<<"$GPG_KEY" | gpg --import --no-tty --batch --yes
|
||||
echo "allow-preset-passphrase" > ~/.gnupg/gpg-agent.conf
|
||||
gpg-connect-agent RELOADAGENT /bye
|
||||
/usr/lib/gnupg2/gpg-preset-passphrase --preset "$GPG_KEYGRIP" <<<"$GPG_PASSPHRASE"
|
||||
base64 -d <<<"$GPG_PASSPHRASE" | /usr/lib/gnupg2/gpg-preset-passphrase --preset "$GPG_KEYGRIP"
|
||||
- name: Sign RPMs
|
||||
if: inputs.environment == 'production'
|
||||
run: |
|
||||
|
|
|
|||
1
.github/workflows/issueauto.yml
vendored
1
.github/workflows/issueauto.yml
vendored
|
|
@ -10,6 +10,7 @@ permissions:
|
|||
jobs:
|
||||
issue-auto:
|
||||
runs-on: ubuntu-latest
|
||||
environment: cli-automation
|
||||
steps:
|
||||
- name: label incoming issue
|
||||
env:
|
||||
|
|
|
|||
1
.github/workflows/prauto.yml
vendored
1
.github/workflows/prauto.yml
vendored
|
|
@ -11,6 +11,7 @@ permissions:
|
|||
jobs:
|
||||
pr-auto:
|
||||
runs-on: ubuntu-latest
|
||||
environment: cli-automation
|
||||
steps:
|
||||
- name: lint pr
|
||||
env:
|
||||
|
|
|
|||
2
.github/workflows/triage.yml
vendored
2
.github/workflows/triage.yml
vendored
|
|
@ -11,6 +11,7 @@ env:
|
|||
TARGET_REPO: github/cli
|
||||
jobs:
|
||||
issue:
|
||||
environment: cli-discuss-automation
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'discuss'
|
||||
steps:
|
||||
|
|
@ -42,6 +43,7 @@ jobs:
|
|||
|
||||
pull_request:
|
||||
runs-on: ubuntu-latest
|
||||
environment: cli-discuss-automation
|
||||
if: github.event_name == 'pull_request_target' && github.event.action == 'labeled' && github.event.label.name == 'discuss'
|
||||
steps:
|
||||
- name: Create issue based on source pull request
|
||||
|
|
|
|||
|
|
@ -39,8 +39,7 @@ builds:
|
|||
goarch: [386, amd64, arm64]
|
||||
hooks:
|
||||
post:
|
||||
- cmd: >-
|
||||
{{ if eq .Runtime.Goos "windows" }}pwsh .\script\sign.ps1{{ else }}./script/sign{{ end }} '{{ .Path }}'
|
||||
- cmd: pwsh .\script\sign.ps1 '{{ .Path }}'
|
||||
output: true
|
||||
binary: bin/gh
|
||||
main: ./cmd/gh
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ func TestIssues(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "pr"))
|
||||
testscript.Run(t, testScriptParamsFor(tsEnv, "issue"))
|
||||
}
|
||||
|
||||
func TestLabels(t *testing.T) {
|
||||
|
|
|
|||
31
acceptance/testdata/issue/issue-comment-edit-last-with-comments.txtar
vendored
Normal file
31
acceptance/testdata/issue/issue-comment-edit-last-with-comments.txtar
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Setup environment variables used for testscript
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create ${ORG}/${REPO} --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes ${ORG}/${REPO}
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone ${ORG}/${REPO}
|
||||
|
||||
# Create an issue in the repo
|
||||
cd ${REPO}
|
||||
exec gh issue create --title 'Feature Request' --body 'Feature Body'
|
||||
stdout2env ISSUE_URL
|
||||
|
||||
# Comment on the issue
|
||||
exec gh issue comment ${ISSUE_URL} --body 'Looks like a great feature!'
|
||||
|
||||
# View the issue
|
||||
exec gh issue view ${ISSUE_URL} --comments
|
||||
stdout 'Looks like a great feature!'
|
||||
|
||||
# Edit the last comment
|
||||
exec gh issue comment ${ISSUE_URL} --edit-last --body 'Looks like an amazing feature!'
|
||||
|
||||
# View the issue
|
||||
exec gh issue view ${ISSUE_URL} --comments
|
||||
! stdout 'Looks like a great feature!'
|
||||
stdout 'Looks like an amazing feature!'
|
||||
23
acceptance/testdata/issue/issue-comment-edit-last-without-comments-creates.txtar
vendored
Normal file
23
acceptance/testdata/issue/issue-comment-edit-last-without-comments-creates.txtar
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Setup environment variables used for testscript
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create ${ORG}/${REPO} --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes ${ORG}/${REPO}
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone ${ORG}/${REPO}
|
||||
|
||||
# Create an issue in the repo
|
||||
cd ${REPO}
|
||||
exec gh issue create --title 'Feature Request' --body 'Feature Body'
|
||||
stdout2env ISSUE_URL
|
||||
|
||||
# Comment on the issue
|
||||
exec gh issue comment ${ISSUE_URL} --edit-last --create-if-none --body 'Looks like a great feature!'
|
||||
|
||||
# View the issue
|
||||
exec gh issue view ${ISSUE_URL} --comments
|
||||
stdout 'Looks like a great feature!'
|
||||
20
acceptance/testdata/issue/issue-comment-edit-last-without-comments-errors.txtar
vendored
Normal file
20
acceptance/testdata/issue/issue-comment-edit-last-without-comments-errors.txtar
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Setup environment variables used for testscript
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create ${ORG}/${REPO} --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes ${ORG}/${REPO}
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone ${ORG}/${REPO}
|
||||
|
||||
# Create an issue in the repo
|
||||
cd ${REPO}
|
||||
exec gh issue create --title 'Feature Request' --body 'Feature Body'
|
||||
stdout2env ISSUE_URL
|
||||
|
||||
# Comment on the issue
|
||||
! exec gh issue comment ${ISSUE_URL} --edit-last --body 'Looks like a great feature!'
|
||||
stderr 'no comments found for current user'
|
||||
23
acceptance/testdata/issue/issue-comment-new.txtar
vendored
Normal file
23
acceptance/testdata/issue/issue-comment-new.txtar
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Setup environment variables used for testscript
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create ${ORG}/${REPO} --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes ${ORG}/${REPO}
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone ${ORG}/${REPO}
|
||||
|
||||
# Create an issue in the repo
|
||||
cd ${REPO}
|
||||
exec gh issue create --title 'Feature Request' --body 'Feature Body'
|
||||
stdout2env ISSUE_URL
|
||||
|
||||
# Comment on the issue
|
||||
exec gh issue comment ${ISSUE_URL} --body 'Looks like a great feature!'
|
||||
|
||||
# View the issue
|
||||
exec gh issue view ${ISSUE_URL} --comments
|
||||
stdout 'Looks like a great feature!'
|
||||
20
acceptance/testdata/issue/issue-comment.txtar
vendored
20
acceptance/testdata/issue/issue-comment.txtar
vendored
|
|
@ -1,20 +0,0 @@
|
|||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Create an issue in the repo
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec gh issue create --title 'Feature Request' --body 'Feature Body'
|
||||
stdout2env ISSUE_URL
|
||||
|
||||
# Comment on the issue
|
||||
exec gh issue comment $ISSUE_URL --body 'Looks like a great feature!'
|
||||
|
||||
# View the issue
|
||||
exec gh issue view $ISSUE_URL --comments
|
||||
stdout 'Looks like a great feature!'
|
||||
40
acceptance/testdata/pr/pr-comment-edit-last-with-comments.txtar
vendored
Normal file
40
acceptance/testdata/pr/pr-comment-edit-last-with-comments.txtar
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Setup environment variables used for testscript
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
|
||||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create ${ORG}/${REPO} --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes ${ORG}/${REPO}
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone ${ORG}/${REPO}
|
||||
|
||||
# Prepare a branch to PR
|
||||
cd ${REPO}
|
||||
exec git checkout -b feature-branch
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push -u origin feature-branch
|
||||
|
||||
|
||||
# Create the PR
|
||||
exec gh pr create --title 'Feature Title' --body 'Feature Body'
|
||||
stdout2env PR_URL
|
||||
|
||||
# Comment on the PR
|
||||
exec gh pr comment ${PR_URL} --body 'Looks like a great feature!'
|
||||
|
||||
# View the PR
|
||||
exec gh pr view ${PR_URL} --comments
|
||||
stdout 'Looks like a great feature!'
|
||||
|
||||
# Edit the last comment
|
||||
exec gh pr comment ${PR_URL} --edit-last --body 'Looks like an amazing feature!'
|
||||
|
||||
# View the PR
|
||||
exec gh pr view ${PR_URL} --comments
|
||||
! stdout 'Looks like a great feature!'
|
||||
stdout 'Looks like an amazing feature!'
|
||||
32
acceptance/testdata/pr/pr-comment-edit-last-without-comments-creates.txtar
vendored
Normal file
32
acceptance/testdata/pr/pr-comment-edit-last-without-comments-creates.txtar
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Setup environment variables used for testscript
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
|
||||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create ${ORG}/${REPO} --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes ${ORG}/${REPO}
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone ${ORG}/${REPO}
|
||||
|
||||
# Prepare a branch to PR
|
||||
cd ${REPO}
|
||||
exec git checkout -b feature-branch
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push -u origin feature-branch
|
||||
|
||||
|
||||
# Create the PR
|
||||
exec gh pr create --title 'Feature Title' --body 'Feature Body'
|
||||
stdout2env PR_URL
|
||||
|
||||
# Comment on the PR
|
||||
exec gh pr comment ${PR_URL} --edit-last --create-if-none --body 'Looks like a great feature!'
|
||||
|
||||
# View the PR
|
||||
exec gh pr view ${PR_URL} --comments
|
||||
stdout 'Looks like a great feature!'
|
||||
29
acceptance/testdata/pr/pr-comment-edit-last-without-comments-errors.txtar
vendored
Normal file
29
acceptance/testdata/pr/pr-comment-edit-last-without-comments-errors.txtar
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Setup environment variables used for testscript
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
|
||||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create ${ORG}/${REPO} --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes ${ORG}/${REPO}
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone ${ORG}/${REPO}
|
||||
|
||||
# Prepare a branch to PR
|
||||
cd ${REPO}
|
||||
exec git checkout -b feature-branch
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push -u origin feature-branch
|
||||
|
||||
|
||||
# Create the PR
|
||||
exec gh pr create --title 'Feature Title' --body 'Feature Body'
|
||||
stdout2env PR_URL
|
||||
|
||||
# Comment on the PR
|
||||
! exec gh pr comment ${PR_URL} --edit-last --body 'Looks like a great feature!'
|
||||
stderr 'no comments found for current user'
|
||||
|
|
@ -1,17 +1,20 @@
|
|||
# Setup environment variables used for testscript
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
|
||||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
exec gh repo create ${ORG}/${REPO} --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
defer gh repo delete --yes ${ORG}/${REPO}
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
exec gh repo clone ${ORG}/${REPO}
|
||||
|
||||
# Prepare a branch to PR
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
cd ${REPO}
|
||||
exec git checkout -b feature-branch
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push -u origin feature-branch
|
||||
|
|
@ -21,8 +24,8 @@ exec gh pr create --title 'Feature Title' --body 'Feature Body'
|
|||
stdout2env PR_URL
|
||||
|
||||
# Comment on the PR
|
||||
exec gh pr comment $PR_URL --body 'Looks like a great feature!'
|
||||
exec gh pr comment ${PR_URL} --body 'Looks like a great feature!'
|
||||
|
||||
# View the PR
|
||||
exec gh pr view $PR_URL --comments
|
||||
exec gh pr view ${PR_URL} --comments
|
||||
stdout 'Looks like a great feature!'
|
||||
|
|
@ -428,9 +428,6 @@ Breaking this command down:
|
|||
* `/dlib` points to the previously extracted DLL
|
||||
* `/dmdf` points to the previously created metadata file
|
||||
|
||||
> [!WARNING]
|
||||
> The [`GoReleaser` signing hook](https://github.com/cli/cli/blob/756f4ec04abdc9fdbab3fef35b182c546ef1dd17/.goreleaser.yml#L43) can currently call `./script/sign` on a non-windows machine, but this is an artifact from pre-HSM that should be removed.
|
||||
|
||||
## <a id="release">[release](https://github.com/cli/cli/blob/756f4ec04abdc9fdbab3fef35b182c546ef1dd17/.github/workflows/deployment.yml#L250-L395)</a>
|
||||
|
||||
<details>
|
||||
|
|
|
|||
12
go.mod
12
go.mod
|
|
@ -46,10 +46,10 @@ require (
|
|||
github.com/spf13/pflag v1.0.6
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/zalando/go-keyring v0.2.5
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/sync v0.10.0
|
||||
golang.org/x/term v0.28.0
|
||||
golang.org/x/text v0.21.0
|
||||
golang.org/x/crypto v0.35.0
|
||||
golang.org/x/sync v0.11.0
|
||||
golang.org/x/term v0.29.0
|
||||
golang.org/x/text v0.22.0
|
||||
google.golang.org/grpc v1.69.4
|
||||
google.golang.org/protobuf v1.36.5
|
||||
gopkg.in/h2non/gock.v1 v1.1.2
|
||||
|
|
@ -158,8 +158,8 @@ require (
|
|||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/net v0.36.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect
|
||||
|
|
|
|||
24
go.sum
24
go.sum
|
|
@ -502,8 +502,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.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
|
|
@ -512,14 +512,14 @@ golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
|||
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.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
|
||||
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
|
||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -529,19 +529,19 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.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.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
|
@ -265,8 +264,6 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
|
|||
return err
|
||||
}
|
||||
|
||||
opts.RequestPath = escapePackageNameInPath(opts.RequestPath)
|
||||
|
||||
if runF != nil {
|
||||
return runF(&opts)
|
||||
}
|
||||
|
|
@ -694,37 +691,3 @@ func previewNamesToMIMETypes(names []string) string {
|
|||
}
|
||||
return strings.Join(types, ", ")
|
||||
}
|
||||
|
||||
// The package name part in the `packages` endpoints may contain slashes and
|
||||
// other characters that need to be URL encoded.
|
||||
//
|
||||
// The `escapePackageNameInPath` function extracts and normalizes package names
|
||||
// in the path. The regex `pathWithPackageNameRE` is being used to extract the
|
||||
// package name with a capture group named `package`.
|
||||
//
|
||||
// See https://docs.github.com/en/rest/packages/packages APIs for more details.
|
||||
//
|
||||
// Here's an example:
|
||||
//
|
||||
// The package name `orders/cache` needs to be URL encoded because it contains
|
||||
// a slash `/`. The `escapePackageNameInPath` function will extract the
|
||||
// `orders/cache` part, perform the URL encoding, and return the normalized API
|
||||
// endpoint with `%2F` replacing the slash `/` in the package name part only.
|
||||
//
|
||||
// - Package name: `orders/cache`
|
||||
// - API endpoint: `/users/USER/packages/container/orders/cache`
|
||||
// - Normalized: `/users/USER/packages/container/orders%2Fcache`
|
||||
|
||||
var pathWithPackageNameRE = regexp.MustCompile(`^\/(?:orgs|user|users)(?:\/.*)?\/packages\/(?:npm|maven|rubygems|docker|nuget|container)\/(?<package>.*?)(?:\/(?:restore|versions)|$)`)
|
||||
|
||||
func escapePackageNameInPath(path string) string {
|
||||
matches := pathWithPackageNameRE.FindStringSubmatch(path)
|
||||
if len(matches) > 0 {
|
||||
i := pathWithPackageNameRE.SubexpIndex("package")
|
||||
packageName := matches[i]
|
||||
if packageName != "" {
|
||||
return strings.Replace(path, packageName, url.QueryEscape(packageName), 1)
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
|
|
|||
|
|
@ -367,72 +367,6 @@ func Test_NewCmdApi(t *testing.T) {
|
|||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "request path with container package name containing slashes",
|
||||
cli: "/user/packages/container/github.com/username/package_name --verbose",
|
||||
wants: ApiOptions{
|
||||
Hostname: "",
|
||||
RequestMethod: "GET",
|
||||
RequestMethodPassed: false,
|
||||
RequestPath: "/user/packages/container/github.com%2Fusername%2Fpackage_name",
|
||||
RequestInputFile: "",
|
||||
RawFields: []string(nil),
|
||||
MagicFields: []string(nil),
|
||||
RequestHeaders: []string(nil),
|
||||
ShowResponseHeaders: false,
|
||||
Paginate: false,
|
||||
Silent: false,
|
||||
CacheTTL: 0,
|
||||
Template: "",
|
||||
FilterOutput: "",
|
||||
Verbose: true,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "request path with container package name containing slashes and restore",
|
||||
cli: "/user/packages/container/github.com/username/package_name/restore --verbose",
|
||||
wants: ApiOptions{
|
||||
Hostname: "",
|
||||
RequestMethod: "GET",
|
||||
RequestMethodPassed: false,
|
||||
RequestPath: "/user/packages/container/github.com%2Fusername%2Fpackage_name/restore",
|
||||
RequestInputFile: "",
|
||||
RawFields: []string(nil),
|
||||
MagicFields: []string(nil),
|
||||
RequestHeaders: []string(nil),
|
||||
ShowResponseHeaders: false,
|
||||
Paginate: false,
|
||||
Silent: false,
|
||||
CacheTTL: 0,
|
||||
Template: "",
|
||||
FilterOutput: "",
|
||||
Verbose: true,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "request path with container package name containing slashes and versions",
|
||||
cli: "/user/packages/container/github.com/username/package_name/versions --verbose",
|
||||
wants: ApiOptions{
|
||||
Hostname: "",
|
||||
RequestMethod: "GET",
|
||||
RequestMethodPassed: false,
|
||||
RequestPath: "/user/packages/container/github.com%2Fusername%2Fpackage_name/versions",
|
||||
RequestInputFile: "",
|
||||
RawFields: []string(nil),
|
||||
MagicFields: []string(nil),
|
||||
RequestHeaders: []string(nil),
|
||||
ShowResponseHeaders: false,
|
||||
Paginate: false,
|
||||
Silent: false,
|
||||
CacheTTL: 0,
|
||||
Template: "",
|
||||
FilterOutput: "",
|
||||
Verbose: true,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -48,13 +48,13 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create [<filename>... | -]",
|
||||
Use: "create [<filename>... | <pattern>... | -]",
|
||||
Short: "Create a new gist",
|
||||
Long: heredoc.Docf(`
|
||||
Create a new GitHub gist with given contents.
|
||||
|
||||
Gists can be created from one or multiple files. Alternatively, pass %[1]s-%[1]s as
|
||||
file name to read from standard input.
|
||||
filename to read from standard input.
|
||||
|
||||
By default, gists are secret; use %[1]s--public%[1]s to make publicly listed ones.
|
||||
`, "`"),
|
||||
|
|
@ -68,6 +68,9 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
# Create a gist containing several files
|
||||
$ gh gist create hello.py world.py cool.txt
|
||||
|
||||
# Create a gist containing several files using patterns
|
||||
$ gh gist create *.md *.txt artifact.*
|
||||
|
||||
# Read from standard input to create a gist
|
||||
$ gh gist create -
|
||||
|
||||
|
|
@ -102,12 +105,23 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
}
|
||||
|
||||
func createRun(opts *CreateOptions) error {
|
||||
fileArgs := opts.Filenames
|
||||
if len(fileArgs) == 0 {
|
||||
fileArgs = []string{"-"}
|
||||
|
||||
readFromStdInArg, filenames := cmdutil.Partition(opts.Filenames, func(f string) bool {
|
||||
return f == "-"
|
||||
})
|
||||
|
||||
filenames, err := cmdutil.GlobPaths(filenames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
files, err := processFiles(opts.IO.In, opts.FilenameOverride, fileArgs)
|
||||
filenames = append(filenames, readFromStdInArg...)
|
||||
|
||||
if len(filenames) == 0 {
|
||||
filenames = []string{"-"}
|
||||
}
|
||||
|
||||
files, err := processFiles(opts.IO.In, opts.FilenameOverride, filenames)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to collect files for posting: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ func Test_createRun(t *testing.T) {
|
|||
responseStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "multiple files",
|
||||
name: "when both a file and the stdin '-' are provided, it matches on all the files passed in and stdin",
|
||||
opts: &CreateOptions{
|
||||
Filenames: []string{fixtureFile, "-"},
|
||||
},
|
||||
|
|
@ -248,6 +248,30 @@ func Test_createRun(t *testing.T) {
|
|||
},
|
||||
responseStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "when both a file and the stdin '-' are provided, but '-' is not the last argument, it matches on all the files provided and stdin",
|
||||
opts: &CreateOptions{
|
||||
Filenames: []string{"-", fixtureFile},
|
||||
},
|
||||
stdin: "cool stdin content",
|
||||
wantOut: "https://gist.github.com/aa5a315d61ae9438b18d\n",
|
||||
wantStderr: "- Creating gist with multiple files\n✓ Created secret gist fixture.txt\n",
|
||||
wantErr: false,
|
||||
wantParams: map[string]interface{}{
|
||||
"description": "",
|
||||
"updated_at": "0001-01-01T00:00:00Z",
|
||||
"public": false,
|
||||
"files": map[string]interface{}{
|
||||
"fixture.txt": map[string]interface{}{
|
||||
"content": "{}",
|
||||
},
|
||||
"gistfile1.txt": map[string]interface{}{
|
||||
"content": "cool stdin content",
|
||||
},
|
||||
},
|
||||
},
|
||||
responseStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "file with empty content",
|
||||
opts: &CreateOptions{
|
||||
|
|
|
|||
|
|
@ -223,9 +223,10 @@ func Test_commentRun(t *testing.T) {
|
|||
httpStubs func(*testing.T, *httpmock.Registry)
|
||||
stdout string
|
||||
stderr string
|
||||
wantsErr bool
|
||||
}{
|
||||
{
|
||||
name: "interactive editor",
|
||||
name: "creating new comment with interactive editor succeeds",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
|
|
@ -234,13 +235,29 @@ func Test_commentRun(t *testing.T) {
|
|||
InteractiveEditSurvey: func(string) (string, error) { return "comment body", nil },
|
||||
ConfirmSubmitSurvey: func() (bool, error) { return true, nil },
|
||||
},
|
||||
emptyComments: true,
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentCreate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "interactive editor with edit last",
|
||||
name: "updating last comment with interactive editor fails if there are no comments and decline prompt to create",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
|
||||
InteractiveEditSurvey: func(string) (string, error) { return "comment body", nil },
|
||||
ConfirmSubmitSurvey: func() (bool, error) { return true, nil },
|
||||
ConfirmCreateIfNoneSurvey: func() (bool, error) { return false, nil },
|
||||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "updating last comment with interactive editor succeeds if there are comments",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
|
|
@ -253,10 +270,11 @@ func Test_commentRun(t *testing.T) {
|
|||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentUpdate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-111\n",
|
||||
emptyComments: false,
|
||||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-111\n",
|
||||
},
|
||||
{
|
||||
name: "interactive editor with edit last and create if none",
|
||||
name: "updating last comment with interactive editor creates new comment if there are no comments but --create-if-none",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
|
|
@ -269,12 +287,14 @@ func Test_commentRun(t *testing.T) {
|
|||
ConfirmSubmitSurvey: func() (bool, error) { return true, nil },
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentUpdate(t, reg)
|
||||
mockCommentCreate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-111\n",
|
||||
emptyComments: true,
|
||||
stderr: "No comments found. Creating a new comment.\n",
|
||||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive web",
|
||||
name: "creating new comment with non-interactive web opens issue in browser focusing on new comment",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeWeb,
|
||||
|
|
@ -282,55 +302,38 @@ func Test_commentRun(t *testing.T) {
|
|||
|
||||
OpenInBrowser: func(string) error { return nil },
|
||||
},
|
||||
stderr: "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n",
|
||||
emptyComments: true,
|
||||
stderr: "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive web with edit last",
|
||||
name: "updating last comment with non-interactive web opens issue in browser focusing on the last comment",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
|
||||
OpenInBrowser: func(string) error { return nil },
|
||||
},
|
||||
stderr: "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive web with edit last and create if none for empty comments",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
|
||||
OpenInBrowser: func(u string) error {
|
||||
assert.Contains(t, u, "#issuecomment-new")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
emptyComments: true,
|
||||
stderr: "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive web with edit last and create if none",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
|
||||
OpenInBrowser: func(u string) error {
|
||||
assert.Contains(t, u, "#issuecomment-111")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
stderr: "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n",
|
||||
emptyComments: false,
|
||||
stderr: "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive editor",
|
||||
name: "updating last comment with non-interactive web errors because there are no comments",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "creating new comment with non-interactive editor succeeds",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeEditor,
|
||||
|
|
@ -344,7 +347,20 @@ func Test_commentRun(t *testing.T) {
|
|||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive editor with edit last",
|
||||
name: "updating last comment with non-interactive editor fails if there are no comments",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
|
||||
EditSurvey: func(string) (string, error) { return "comment body", nil },
|
||||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "updating last comment with non-interactive editor succeeds if there are comments",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeEditor,
|
||||
|
|
@ -356,10 +372,11 @@ func Test_commentRun(t *testing.T) {
|
|||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentUpdate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-111\n",
|
||||
emptyComments: false,
|
||||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-111\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive editor with edit last and create if none",
|
||||
name: "updating last comment with non-interactive editor creates new comment if there are no comments but --create-if-none",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeEditor,
|
||||
|
|
@ -376,7 +393,7 @@ func Test_commentRun(t *testing.T) {
|
|||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive inline",
|
||||
name: "creating new comment with non-interactive inline succeeds if comment body is provided",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeInline,
|
||||
|
|
@ -388,7 +405,7 @@ func Test_commentRun(t *testing.T) {
|
|||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive inline with edit last",
|
||||
name: "updating last comment with non-interactive inline succeeds if there are comments and comment body is provided",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeInline,
|
||||
|
|
@ -398,7 +415,23 @@ func Test_commentRun(t *testing.T) {
|
|||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentUpdate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-111\n",
|
||||
emptyComments: false,
|
||||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-111\n",
|
||||
},
|
||||
{
|
||||
name: "updating last comment with non-interactive inline creates new comment if there are no comments but --create-if-none",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeInline,
|
||||
Body: "comment body",
|
||||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
},
|
||||
emptyComments: true,
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentCreate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-456\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
|
@ -437,6 +470,10 @@ func Test_commentRun(t *testing.T) {
|
|||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := shared.CommentableRun(tt.input)
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.stdout, stdout.String())
|
||||
assert.Equal(t, tt.stderr, stderr.String())
|
||||
|
|
|
|||
|
|
@ -243,9 +243,10 @@ func Test_commentRun(t *testing.T) {
|
|||
httpStubs func(*testing.T, *httpmock.Registry)
|
||||
stdout string
|
||||
stderr string
|
||||
wantsErr bool
|
||||
}{
|
||||
{
|
||||
name: "interactive editor",
|
||||
name: "creating new comment with interactive editor succeeds",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
|
|
@ -260,7 +261,22 @@ func Test_commentRun(t *testing.T) {
|
|||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "interactive editor with edit last",
|
||||
name: "updating last comment with interactive editor fails if there are no comments and decline prompt to create",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
|
||||
InteractiveEditSurvey: func(string) (string, error) { return "comment body", nil },
|
||||
ConfirmSubmitSurvey: func() (bool, error) { return true, nil },
|
||||
ConfirmCreateIfNoneSurvey: func() (bool, error) { return false, nil },
|
||||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "updating last comment with interactive editor succeeds if there are comments",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
|
|
@ -273,10 +289,11 @@ func Test_commentRun(t *testing.T) {
|
|||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentUpdate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-111\n",
|
||||
emptyComments: false,
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-111\n",
|
||||
},
|
||||
{
|
||||
name: "interactive editor with edit last and create if none",
|
||||
name: "updating last comment with interactive editor creates new comment if there are no comments but --create-if-none",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
|
|
@ -289,12 +306,14 @@ func Test_commentRun(t *testing.T) {
|
|||
ConfirmSubmitSurvey: func() (bool, error) { return true, nil },
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentUpdate(t, reg)
|
||||
mockCommentCreate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-111\n",
|
||||
emptyComments: true,
|
||||
stderr: "No comments found. Creating a new comment.\n",
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive web",
|
||||
name: "creating new comment with non-interactive web opens pull request in browser focusing on new comment",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeWeb,
|
||||
|
|
@ -302,10 +321,11 @@ func Test_commentRun(t *testing.T) {
|
|||
|
||||
OpenInBrowser: func(string) error { return nil },
|
||||
},
|
||||
stderr: "Opening https://github.com/OWNER/REPO/pull/123 in your browser.\n",
|
||||
emptyComments: true,
|
||||
stderr: "Opening https://github.com/OWNER/REPO/pull/123 in your browser.\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive web with edit last",
|
||||
name: "updating last comment with non-interactive web opens pull request in browser focusing on the last comment",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeWeb,
|
||||
|
|
@ -317,43 +337,22 @@ func Test_commentRun(t *testing.T) {
|
|||
return nil
|
||||
},
|
||||
},
|
||||
stderr: "Opening https://github.com/OWNER/REPO/pull/123 in your browser.\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive web with edit last and create if none for empty comments",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
|
||||
OpenInBrowser: func(u string) error {
|
||||
assert.Contains(t, u, "#issuecomment-new")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
emptyComments: true,
|
||||
emptyComments: false,
|
||||
stderr: "Opening https://github.com/OWNER/REPO/pull/123 in your browser.\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive web with edit last and create if none",
|
||||
name: "updating last comment with non-interactive web errors because there are no comments",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
|
||||
OpenInBrowser: func(u string) error {
|
||||
assert.Contains(t, u, "#issuecomment-111")
|
||||
return nil
|
||||
},
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
},
|
||||
stderr: "Opening https://github.com/OWNER/REPO/pull/123 in your browser.\n",
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "non-interactive editor",
|
||||
name: "creating new comment with non-interactive editor succeeds",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeEditor,
|
||||
|
|
@ -367,7 +366,20 @@ func Test_commentRun(t *testing.T) {
|
|||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive editor with edit last",
|
||||
name: "updating last comment with non-interactive editor fails if there are no comments",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
|
||||
EditSurvey: func(string) (string, error) { return "comment body", nil },
|
||||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "updating last comment with non-interactive editor succeeds if there are comments",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeEditor,
|
||||
|
|
@ -382,7 +394,7 @@ func Test_commentRun(t *testing.T) {
|
|||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-111\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive editor with edit last and create if none",
|
||||
name: "updating last comment with non-interactive editor creates new comment if there are no comments but --create-if-none",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeEditor,
|
||||
|
|
@ -399,7 +411,7 @@ func Test_commentRun(t *testing.T) {
|
|||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive inline",
|
||||
name: "creating new comment with non-interactive inline succeeds if comment body is provided",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeInline,
|
||||
|
|
@ -411,7 +423,7 @@ func Test_commentRun(t *testing.T) {
|
|||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive inline with edit last",
|
||||
name: "updating last comment with non-interactive inline succeeds if there are comments and comment body is provided",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeInline,
|
||||
|
|
@ -421,7 +433,23 @@ func Test_commentRun(t *testing.T) {
|
|||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentUpdate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-111\n",
|
||||
emptyComments: false,
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-111\n",
|
||||
},
|
||||
{
|
||||
name: "updating last comment with non-interactive inline creates new comment if there are no comments but --create-if-none",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeInline,
|
||||
Body: "comment body",
|
||||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
},
|
||||
emptyComments: true,
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentCreate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-456\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
|
@ -459,6 +487,10 @@ func Test_commentRun(t *testing.T) {
|
|||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := shared.CommentableRun(tt.input)
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.stdout, stdout.String())
|
||||
assert.Equal(t, tt.stderr, stderr.String())
|
||||
|
|
|
|||
|
|
@ -92,26 +92,37 @@ func CommentableRun(opts *CommentableOptions) error {
|
|||
return err
|
||||
}
|
||||
opts.Host = repo.RepoHost()
|
||||
if opts.EditLast {
|
||||
err := updateComment(commentable, opts)
|
||||
if !errors.Is(err, errNoUserComments) {
|
||||
|
||||
// Create new comment, bail before complexities of updating the last comment
|
||||
if !opts.EditLast {
|
||||
return createComment(commentable, opts)
|
||||
}
|
||||
|
||||
// Update the last comment, handling success or unexpected errors accordingly
|
||||
err = updateComment(commentable, opts)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, errNoUserComments) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Determine whether to create new comment, prompt user if interactive and missing option
|
||||
if !opts.CreateIfNone && opts.Interactive {
|
||||
opts.CreateIfNone, err = opts.ConfirmCreateIfNoneSurvey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.Interactive {
|
||||
if opts.CreateIfNone {
|
||||
fmt.Fprintln(opts.IO.ErrOut, "No comments found. Creating a new comment.")
|
||||
} else {
|
||||
ok, err := opts.ConfirmCreateIfNoneSurvey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return errNoUserComments
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !opts.CreateIfNone {
|
||||
return errNoUserComments
|
||||
}
|
||||
|
||||
// Create new comment because updating the last comment failed due to no user comments
|
||||
if opts.Interactive {
|
||||
fmt.Fprintln(opts.IO.ErrOut, "No comments found. Creating a new comment.")
|
||||
}
|
||||
|
||||
return createComment(commentable, opts)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ func NewCmdUpdateBranch(f *cmdutil.Factory, runF func(*UpdateBranchOptions) erro
|
|||
Without an argument, the pull request that belongs to the current branch is selected.
|
||||
|
||||
The default behavior is to update with a merge commit (i.e., merging the base branch
|
||||
into the the PR's branch). To reconcile the changes with rebasing on top of the base
|
||||
into the PR's branch). To reconcile the changes with rebasing on top of the base
|
||||
branch, the %[1]s--rebase%[1]s option should be provided.
|
||||
`, "`"),
|
||||
Example: heredoc.Doc(`
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
cmd := &cobra.Command{
|
||||
DisableFlagsInUseLine: true,
|
||||
|
||||
Use: "create [<tag>] [<files>...]",
|
||||
Use: "create [<tag>] [<filename>... | <pattern>...]",
|
||||
Short: "Create a new release",
|
||||
Long: heredoc.Docf(`
|
||||
Create a new GitHub Release for a repository.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
|
|
@ -36,6 +37,17 @@ type AssetForUpload struct {
|
|||
}
|
||||
|
||||
func AssetsFromArgs(args []string) (assets []*AssetForUpload, err error) {
|
||||
labeledArgs, unlabeledArgs := cmdutil.Partition(args, func(arg string) bool {
|
||||
return strings.Contains(arg, "#")
|
||||
})
|
||||
|
||||
args, err = cmdutil.GlobPaths(unlabeledArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
args = append(args, labeledArgs...)
|
||||
|
||||
for _, arg := range args {
|
||||
var label string
|
||||
fn := arg
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ func latestCommit(client *api.Client, repo ghrepo.Interface, branch string) (com
|
|||
|
||||
type upstreamMergeErr struct{ error }
|
||||
|
||||
var missingWorkflowScopeRE = regexp.MustCompile("refusing to allow.*without `workflow` scope")
|
||||
var missingWorkflowScopeErr = errors.New("Upstream commits contain workflow changes, which require the `workflow` scope to merge. To request it, run: gh auth refresh -s workflow")
|
||||
var missingWorkflowScopeRE = regexp.MustCompile("refusing to allow.*without `workflow(s)?` (scope|permission)")
|
||||
var missingWorkflowScopeErr = errors.New("Upstream commits contain workflow changes, which require the `workflow` scope or permission to merge. To request it, run: gh auth refresh -s workflow")
|
||||
|
||||
func triggerUpstreamMerge(client *api.Client, repo ghrepo.Interface, branch string) (string, error) {
|
||||
var payload bytes.Buffer
|
||||
|
|
|
|||
|
|
@ -470,7 +470,7 @@ func Test_SyncRun(t *testing.T) {
|
|||
errMsg: "trunk branch does not exist on OWNER/REPO-FORK repository",
|
||||
},
|
||||
{
|
||||
name: "sync remote fork with missing workflow scope on token",
|
||||
name: "sync remote fork with missing workflow scope on OAuth App token",
|
||||
opts: &SyncOptions{
|
||||
DestArg: "FORKOWNER/REPO-FORK",
|
||||
},
|
||||
|
|
@ -485,7 +485,43 @@ func Test_SyncRun(t *testing.T) {
|
|||
}{Message: "refusing to allow an OAuth App to create or update workflow `.github/workflows/unimportant.yml` without `workflow` scope"}))
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "Upstream commits contain workflow changes, which require the `workflow` scope to merge. To request it, run: gh auth refresh -s workflow",
|
||||
errMsg: "Upstream commits contain workflow changes, which require the `workflow` scope or permission to merge. To request it, run: gh auth refresh -s workflow",
|
||||
},
|
||||
{
|
||||
name: "sync remote fork with missing workflow permission on GitHub App token",
|
||||
opts: &SyncOptions{
|
||||
DestArg: "FORKOWNER/REPO-FORK",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryInfo\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"defaultBranchRef":{"name": "trunk"}}}}`))
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "repos/FORKOWNER/REPO-FORK/merge-upstream"),
|
||||
httpmock.StatusJSONResponse(422, struct {
|
||||
Message string `json:"message"`
|
||||
}{Message: "refusing to allow a GitHub App to create or update workflow `.github/workflows/unimportant.yml` without `workflows` permission"}))
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "Upstream commits contain workflow changes, which require the `workflow` scope or permission to merge. To request it, run: gh auth refresh -s workflow",
|
||||
},
|
||||
{
|
||||
name: "sync remote fork with missing workflow scope on personal access token",
|
||||
opts: &SyncOptions{
|
||||
DestArg: "FORKOWNER/REPO-FORK",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryInfo\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"defaultBranchRef":{"name": "trunk"}}}}`))
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "repos/FORKOWNER/REPO-FORK/merge-upstream"),
|
||||
httpmock.StatusJSONResponse(422, struct {
|
||||
Message string `json:"message"`
|
||||
}{Message: "refusing to allow a Personal Access Token to create or update workflow `.github/workflows/unimportant.yml` without `workflow` scope"}))
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "Upstream commits contain workflow changes, which require the `workflow` scope or permission to merge. To request it, run: gh auth refresh -s workflow",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
|
|||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// If the user specified a repo directly, then we're using the OverrideBaseRepoFunc set by EnableRepoOverride
|
||||
// So there's no reason to use the specialised BaseRepoFunc that requires remote disambiguation.
|
||||
if cmd.Flags().Changed("repo") || os.Getenv("GH_REPO") != "" {
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
} else {
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
isRepoUserProvided := cmd.Flags().Changed("repo") || os.Getenv("GH_REPO") != ""
|
||||
if !isRepoUserProvided {
|
||||
// If they haven't specified a repo directly, then we will wrap the BaseRepoFunc in one that errors if
|
||||
// there might be multiple valid remotes.
|
||||
opts.BaseRepo = shared.RequireNoAmbiguityBaseRepoFunc(opts.BaseRepo, f.Remotes)
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ func TestNewCmdDelete(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewCmdDeleteBaseRepoFuncs(t *testing.T) {
|
||||
remotes := ghContext.Remotes{
|
||||
multipleRemotes := ghContext.Remotes{
|
||||
&ghContext.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "origin",
|
||||
|
|
@ -141,10 +141,20 @@ func TestNewCmdDeleteBaseRepoFuncs(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
singleRemote := ghContext.Remotes{
|
||||
&ghContext.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "origin",
|
||||
},
|
||||
Repo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
env map[string]string
|
||||
remotes ghContext.Remotes
|
||||
prompterStubs func(*prompter.MockPrompter)
|
||||
wantRepo ghrepo.Interface
|
||||
wantErr error
|
||||
|
|
@ -152,6 +162,7 @@ func TestNewCmdDeleteBaseRepoFuncs(t *testing.T) {
|
|||
{
|
||||
name: "when there is a repo flag provided, the factory base repo func is used",
|
||||
args: "SECRET_NAME --repo owner/repo",
|
||||
remotes: multipleRemotes,
|
||||
wantRepo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
{
|
||||
|
|
@ -160,18 +171,27 @@ func TestNewCmdDeleteBaseRepoFuncs(t *testing.T) {
|
|||
env: map[string]string{
|
||||
"GH_REPO": "owner/repo",
|
||||
},
|
||||
remotes: multipleRemotes,
|
||||
wantRepo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
{
|
||||
name: "when there is no repo flag or GH_REPO env var provided, and no prompting, the base func requiring no ambiguity is used",
|
||||
args: "SECRET_NAME",
|
||||
name: "when there is no repo flag or GH_REPO env var provided, and no prompting, the base func requiring no ambiguity is used",
|
||||
args: "SECRET_NAME",
|
||||
remotes: multipleRemotes,
|
||||
wantErr: shared.AmbiguousBaseRepoError{
|
||||
Remotes: remotes,
|
||||
Remotes: multipleRemotes,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when there is no repo flag or GH_REPO env var provided, and can prompt, the base func resolving ambiguity is used",
|
||||
args: "SECRET_NAME",
|
||||
name: "when there is no repo flag or GH_REPO env provided, and there is a single remote, the factory base repo func is used",
|
||||
args: "SECRET_NAME",
|
||||
remotes: singleRemote,
|
||||
wantRepo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
{
|
||||
name: "when there is no repo flag or GH_REPO env var provided, and can prompt, the base func resolving ambiguity is used",
|
||||
args: "SECRET_NAME",
|
||||
remotes: multipleRemotes,
|
||||
prompterStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterSelect(
|
||||
"Select a repo",
|
||||
|
|
@ -204,7 +224,7 @@ func TestNewCmdDeleteBaseRepoFuncs(t *testing.T) {
|
|||
},
|
||||
Prompter: pm,
|
||||
Remotes: func() (ghContext.Remotes, error) {
|
||||
return remotes, nil
|
||||
return tt.remotes, nil
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,9 +70,9 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// If the user specified a repo directly, then we're using the OverrideBaseRepoFunc set by EnableRepoOverride
|
||||
// So there's no reason to use the specialised BaseRepoFunc that requires remote disambiguation.
|
||||
if cmd.Flags().Changed("repo") || os.Getenv("GH_REPO") != "" {
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
} else {
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
isRepoUserProvided := cmd.Flags().Changed("repo") || os.Getenv("GH_REPO") != ""
|
||||
if !isRepoUserProvided {
|
||||
// If they haven't specified a repo directly, then we will wrap the BaseRepoFunc in one that errors if
|
||||
// there might be multiple valid remotes.
|
||||
opts.BaseRepo = shared.RequireNoAmbiguityBaseRepoFunc(opts.BaseRepo, f.Remotes)
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ func Test_NewCmdList(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewCmdListBaseRepoFuncs(t *testing.T) {
|
||||
remotes := ghContext.Remotes{
|
||||
multipleRemotes := ghContext.Remotes{
|
||||
&ghContext.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "origin",
|
||||
|
|
@ -121,10 +121,20 @@ func TestNewCmdListBaseRepoFuncs(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
singleRemote := ghContext.Remotes{
|
||||
&ghContext.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "origin",
|
||||
},
|
||||
Repo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
env map[string]string
|
||||
remotes ghContext.Remotes
|
||||
prompterStubs func(*prompter.MockPrompter)
|
||||
wantRepo ghrepo.Interface
|
||||
wantErr error
|
||||
|
|
@ -132,6 +142,7 @@ func TestNewCmdListBaseRepoFuncs(t *testing.T) {
|
|||
{
|
||||
name: "when there is a repo flag provided, the factory base repo func is used",
|
||||
args: "--repo owner/repo",
|
||||
remotes: multipleRemotes,
|
||||
wantRepo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
{
|
||||
|
|
@ -139,18 +150,27 @@ func TestNewCmdListBaseRepoFuncs(t *testing.T) {
|
|||
env: map[string]string{
|
||||
"GH_REPO": "owner/repo",
|
||||
},
|
||||
remotes: multipleRemotes,
|
||||
wantRepo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
{
|
||||
name: "when there is no repo flag or GH_REPO env var provided, and no prompting, the base func requiring no ambiguity is used",
|
||||
args: "",
|
||||
name: "when there is no repo flag or GH_REPO env var provided, and no prompting, the base func requiring no ambiguity is used",
|
||||
args: "",
|
||||
remotes: multipleRemotes,
|
||||
wantErr: shared.AmbiguousBaseRepoError{
|
||||
Remotes: remotes,
|
||||
Remotes: multipleRemotes,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when there is no repo flag or GH_REPO env var provided, and can prompt, the base func resolving ambiguity is used",
|
||||
args: "",
|
||||
name: "when there is no repo flag or GH_REPO env provided, and there is a single remote, the factory base repo func is used",
|
||||
args: "",
|
||||
remotes: singleRemote,
|
||||
wantRepo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
{
|
||||
name: "when there is no repo flag or GH_REPO env var provided, and can prompt, the base func resolving ambiguity is used",
|
||||
args: "",
|
||||
remotes: multipleRemotes,
|
||||
prompterStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterSelect(
|
||||
"Select a repo",
|
||||
|
|
@ -183,7 +203,7 @@ func TestNewCmdListBaseRepoFuncs(t *testing.T) {
|
|||
},
|
||||
Prompter: pm,
|
||||
Remotes: func() (ghContext.Remotes, error) {
|
||||
return remotes, nil
|
||||
return tt.remotes, nil
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -106,9 +106,9 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// If the user specified a repo directly, then we're using the OverrideBaseRepoFunc set by EnableRepoOverride
|
||||
// So there's no reason to use the specialised BaseRepoFunc that requires remote disambiguation.
|
||||
if cmd.Flags().Changed("repo") || os.Getenv("GH_REPO") != "" {
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
} else {
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
isRepoUserProvided := cmd.Flags().Changed("repo") || os.Getenv("GH_REPO") != ""
|
||||
if !isRepoUserProvided {
|
||||
// If they haven't specified a repo directly, then we will wrap the BaseRepoFunc in one that errors if
|
||||
// there might be multiple valid remotes.
|
||||
opts.BaseRepo = shared.RequireNoAmbiguityBaseRepoFunc(opts.BaseRepo, f.Remotes)
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ func TestNewCmdSet(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewCmdSetBaseRepoFuncs(t *testing.T) {
|
||||
remotes := ghContext.Remotes{
|
||||
multipleRemotes := ghContext.Remotes{
|
||||
&ghContext.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "origin",
|
||||
|
|
@ -240,10 +240,20 @@ func TestNewCmdSetBaseRepoFuncs(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
singleRemote := ghContext.Remotes{
|
||||
&ghContext.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "origin",
|
||||
},
|
||||
Repo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
env map[string]string
|
||||
remotes ghContext.Remotes
|
||||
prompterStubs func(*prompter.MockPrompter)
|
||||
wantRepo ghrepo.Interface
|
||||
wantErr error
|
||||
|
|
@ -251,6 +261,7 @@ func TestNewCmdSetBaseRepoFuncs(t *testing.T) {
|
|||
{
|
||||
name: "when there is a repo flag provided, the factory base repo func is used",
|
||||
args: "SECRET_NAME --repo owner/repo",
|
||||
remotes: multipleRemotes,
|
||||
wantRepo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
{
|
||||
|
|
@ -259,18 +270,27 @@ func TestNewCmdSetBaseRepoFuncs(t *testing.T) {
|
|||
env: map[string]string{
|
||||
"GH_REPO": "owner/repo",
|
||||
},
|
||||
remotes: multipleRemotes,
|
||||
wantRepo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
{
|
||||
name: "when there is no repo flag or GH_REPO env var provided, and no prompting, the base func requiring no ambiguity is used",
|
||||
args: "SECRET_NAME",
|
||||
name: "when there is no repo flag or GH_REPO env var provided, and no prompting, the base func requiring no ambiguity is used",
|
||||
args: "SECRET_NAME",
|
||||
remotes: multipleRemotes,
|
||||
wantErr: shared.AmbiguousBaseRepoError{
|
||||
Remotes: remotes,
|
||||
Remotes: multipleRemotes,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when there is no repo flag or GH_REPO env var provided, and can prompt, the base func resolving ambiguity is used",
|
||||
args: "SECRET_NAME",
|
||||
name: "when there is no repo flag or GH_REPO env provided, and there is a single remote, the factory base repo func is used",
|
||||
args: "SECRET_NAME",
|
||||
remotes: singleRemote,
|
||||
wantRepo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
{
|
||||
name: "when there is no repo flag or GH_REPO env var provided, and can prompt, the base func resolving ambiguity is used",
|
||||
args: "SECRET_NAME",
|
||||
remotes: multipleRemotes,
|
||||
prompterStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterSelect(
|
||||
"Select a repo",
|
||||
|
|
@ -303,7 +323,7 @@ func TestNewCmdSetBaseRepoFuncs(t *testing.T) {
|
|||
},
|
||||
Prompter: pm,
|
||||
Remotes: func() (ghContext.Remotes, error) {
|
||||
return remotes, nil
|
||||
return tt.remotes, nil
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package cmdutil
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
|
@ -57,3 +58,43 @@ func NoArgsQuoteReminder(cmd *cobra.Command, args []string) error {
|
|||
|
||||
return FlagErrorf("%s", errMsg)
|
||||
}
|
||||
|
||||
// Partition takes a slice of any type T and separates it into two slices
|
||||
// of the same type based on the provided predicate function. Any item
|
||||
// that returns true for the predicate will be included in the first slice
|
||||
// returned, and any item that returns false for the predicate will be
|
||||
// included in the second slice returned.
|
||||
func Partition[T any](slice []T, predicate func(T) bool) ([]T, []T) {
|
||||
var matching, nonMatching []T
|
||||
for _, item := range slice {
|
||||
if predicate(item) {
|
||||
matching = append(matching, item)
|
||||
} else {
|
||||
nonMatching = append(nonMatching, item)
|
||||
}
|
||||
}
|
||||
return matching, nonMatching
|
||||
}
|
||||
|
||||
// GlobPaths expands a list of file patterns into a list of file paths.
|
||||
// If no files match a pattern, that pattern will return an error.
|
||||
// If no pattern is passed, this returns an empty list and no error.
|
||||
//
|
||||
// For information on supported glob patterns, see
|
||||
// https://pkg.go.dev/path/filepath#Match
|
||||
func GlobPaths(patterns []string) ([]string, error) {
|
||||
expansions := []string{}
|
||||
|
||||
for _, pattern := range patterns {
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %v", pattern, err)
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return []string{}, fmt.Errorf("no matches found for `%s`", pattern)
|
||||
}
|
||||
expansions = append(expansions, matches...)
|
||||
}
|
||||
|
||||
return expansions, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
package cmdutil
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMinimumArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
|
|
@ -48,3 +56,207 @@ func TestMinimumNs_with_error(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartition(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
slice []any
|
||||
predicate func(any) bool
|
||||
wantMatching []any
|
||||
wantNonMatching []any
|
||||
}{
|
||||
{
|
||||
name: "When the slice is empty, it returns two empty slices",
|
||||
slice: []any{},
|
||||
predicate: func(any) bool {
|
||||
return true
|
||||
},
|
||||
wantMatching: []any{},
|
||||
wantNonMatching: []any{},
|
||||
},
|
||||
{
|
||||
name: "when the slice has one element that satisfies the predicate, it returns a slice with that element and an empty slice",
|
||||
slice: []any{
|
||||
"foo",
|
||||
},
|
||||
predicate: func(any) bool {
|
||||
return true
|
||||
},
|
||||
wantMatching: []any{"foo"},
|
||||
wantNonMatching: []any{},
|
||||
},
|
||||
{
|
||||
name: "when the slice has one element that does not satisfy the predicate, it returns an empty slice and a slice with that element",
|
||||
slice: []any{
|
||||
"foo",
|
||||
},
|
||||
predicate: func(any) bool {
|
||||
return false
|
||||
},
|
||||
wantMatching: []any{},
|
||||
wantNonMatching: []any{"foo"},
|
||||
},
|
||||
{
|
||||
name: "when the slice has multiple elements, it returns a slice with the elements that satisfy the predicate and a slice with the elements that do not satisfy the predicate",
|
||||
slice: []any{
|
||||
"foo",
|
||||
"bar",
|
||||
"baz",
|
||||
},
|
||||
predicate: func(s any) bool {
|
||||
return s.(string) != "foo"
|
||||
},
|
||||
wantMatching: []any{"bar", "baz"},
|
||||
wantNonMatching: []any{"foo"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotMatching, gotNonMatching := Partition(tt.slice, tt.predicate)
|
||||
assert.ElementsMatch(t, tt.wantMatching, gotMatching)
|
||||
assert.ElementsMatch(t, tt.wantNonMatching, gotNonMatching)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobPaths(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
os string
|
||||
patterns []string
|
||||
wantOut []string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "When no patterns are passed, return an empty slice",
|
||||
patterns: []string{},
|
||||
wantOut: []string{},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "When no files match, it returns an empty expansions array with error",
|
||||
patterns: []string{"foo"},
|
||||
wantOut: []string{},
|
||||
wantErr: errors.New("no matches found for `foo`"),
|
||||
},
|
||||
{
|
||||
name: "When a single pattern, '*.txt' is passed with one match, it returns that match",
|
||||
patterns: []string{
|
||||
"*.txt",
|
||||
},
|
||||
wantOut: []string{
|
||||
"rootFile.txt",
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "When a single pattern, '*/*.txt' is passed with multiple matches, it returns those matches",
|
||||
patterns: []string{
|
||||
"*/*.txt",
|
||||
},
|
||||
wantOut: []string{
|
||||
filepath.Join("subDir1", "subDir1_file.txt"),
|
||||
filepath.Join("subDir2", "subDir2_file.txt"),
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "When multiple patterns, '*/*.txt' and '*/*.go', are passed with multiple matches, it returns those matches",
|
||||
patterns: []string{
|
||||
"*/*.txt",
|
||||
"*/*.go",
|
||||
},
|
||||
wantOut: []string{
|
||||
filepath.Join("subDir1", "subDir1_file.txt"),
|
||||
filepath.Join("subDir2", "subDir2_file.txt"),
|
||||
filepath.Join("subDir2", "subDir2_file.go"),
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cleanupFn := createTestDir(t)
|
||||
defer cleanupFn()
|
||||
|
||||
got, err := GlobPaths(tt.patterns)
|
||||
if tt.wantErr != nil {
|
||||
assert.EqualError(t, err, tt.wantErr.Error())
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.wantOut, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a temporary directory with the structure below. Returns
|
||||
// a cleanup function that will remove the directory and all of its
|
||||
// contents. The cleanup function should be wrapped in a defer statement.
|
||||
//
|
||||
// | root
|
||||
// |-- rootFile.txt
|
||||
// |-- subDir1
|
||||
// | |-- subDir1_file.txt
|
||||
// |
|
||||
// |-- subDir2
|
||||
// |-- subDir2_file.go
|
||||
// |-- subDir2_file.txt
|
||||
func createTestDir(t *testing.T) (cleanupFn func()) {
|
||||
t.Helper()
|
||||
// Make Directories
|
||||
rootDir := t.TempDir()
|
||||
|
||||
// Move workspace to temporary directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = os.Chdir(rootDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Make subdirectories
|
||||
err = os.Mkdir(filepath.Join(rootDir, "subDir1"), 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = os.Mkdir(filepath.Join(rootDir, "subDir2"), 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Make Files
|
||||
err = os.WriteFile(filepath.Join(rootDir, "rootFile.txt"), []byte(""), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(filepath.Join(rootDir, "subDir1", "subDir1_file.txt"), []byte(""), 0o644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(filepath.Join(rootDir, "subDir2", "subDir2_file.go"), []byte(""), 0o644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(filepath.Join(rootDir, "subDir2", "subDir2_file.txt"), []byte(""), 0o644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cleanupFn = func() {
|
||||
os.RemoveAll(rootDir)
|
||||
err = os.Chdir(cwd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return cleanupFn
|
||||
}
|
||||
|
|
|
|||
49
script/sign
49
script/sign
|
|
@ -1,36 +1,12 @@
|
|||
#!/bin/bash
|
||||
# usage: script/sign <file>
|
||||
#
|
||||
# Signs macOS binaries using codesign, notarizes macOS zip archives using notarytool, and signs
|
||||
# Windows EXE and MSI files using osslsigncode.
|
||||
# Signs macOS binaries using codesign, notarizes macOS zip archives using notarytool
|
||||
#
|
||||
set -e
|
||||
|
||||
sign_windows() {
|
||||
if [ -z "$CERT_FILE" ]; then
|
||||
echo "skipping Windows code-signing; CERT_FILE not set" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$CERT_FILE" ]; then
|
||||
echo "error Windows code-signing; file '$CERT_FILE' not found" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$CERT_PASSWORD" ]; then
|
||||
echo "error Windows code-signing; no value for CERT_PASSWORD" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
osslsigncode sign -n "GitHub CLI" -t http://timestamp.digicert.com \
|
||||
-pkcs12 "$CERT_FILE" -readpass <(printf "%s" "$CERT_PASSWORD") -h sha256 \
|
||||
-in "$1" -out "$1"~
|
||||
|
||||
mv "$1"~ "$1"
|
||||
}
|
||||
|
||||
sign_macos() {
|
||||
if [ -z "$APPLE_DEVELOPER_ID" ]; then
|
||||
if [[ -z "$APPLE_DEVELOPER_ID" ]]; then
|
||||
echo "skipping macOS code-signing; APPLE_DEVELOPER_ID not set" >&2
|
||||
return 0
|
||||
fi
|
||||
|
|
@ -42,24 +18,17 @@ sign_macos() {
|
|||
fi
|
||||
}
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "usage: script/sign <file>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
platform="$(uname -s)"
|
||||
if [[ $platform != "Darwin" ]]; then
|
||||
echo "error: must run on macOS; skipping codesigning/notarization" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for input_file; do
|
||||
case "$input_file" in
|
||||
*.exe | *.msi )
|
||||
sign_windows "$input_file"
|
||||
;;
|
||||
* )
|
||||
if [ "$platform" = "Darwin" ]; then
|
||||
sign_macos "$input_file"
|
||||
else
|
||||
printf "warning: don't know how to sign %s on %s\n" "$1", "$platform" >&2
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
sign_macos "$input_file"
|
||||
done
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue