Merge branch 'trunk' into install_solus
This commit is contained in:
commit
8541d6e290
182 changed files with 9528 additions and 2154 deletions
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/go:1.23",
|
||||
"image": "mcr.microsoft.com/devcontainers/go:1.24",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/sshd:1": {}
|
||||
},
|
||||
|
|
|
|||
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
|
|
@ -5,6 +5,10 @@ internal/codespaces/ @cli/codespaces
|
|||
|
||||
# Limit Package Security team ownership to the attestation command package and related integration tests
|
||||
pkg/cmd/attestation/ @cli/package-security
|
||||
pkg/cmd/release/attestation @cli/package-security
|
||||
pkg/cmd/release/verify @cli/package-security
|
||||
pkg/cmd/release/verify-asset @cli/package-security
|
||||
|
||||
test/integration/attestation-cmd @cli/package-security
|
||||
|
||||
pkg/cmd/attestation/verification/embed/tuf-repo.github.com/ @cli/tuf-root-reviewers
|
||||
|
|
|
|||
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
|
|
@ -24,7 +24,7 @@ We accept pull requests for bug fixes and features where we've discussed the app
|
|||
## Building the project
|
||||
|
||||
Prerequisites:
|
||||
- Go 1.23+
|
||||
- Go 1.24+
|
||||
|
||||
Build with:
|
||||
* Unix-like systems: `make`
|
||||
|
|
|
|||
3
.github/secret_scanning.yml
vendored
Normal file
3
.github/secret_scanning.yml
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
paths-ignore:
|
||||
- 'third-party/**'
|
||||
- 'third-party-licenses.*.md'
|
||||
23
.github/workflows/codeql.yml
vendored
23
.github/workflows/codeql.yml
vendored
|
|
@ -18,21 +18,32 @@ permissions:
|
|||
jobs:
|
||||
CodeQL-Build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ['go', 'actions']
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
if: matrix.language == 'go'
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: go
|
||||
languages: ${{ matrix.language }}
|
||||
queries: security-and-quality
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
config: |
|
||||
paths-ignore:
|
||||
- 'third-party/**'
|
||||
- 'third-party-licenses.*.md'
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
|
|
|
|||
4
.github/workflows/deployment.yml
vendored
4
.github/workflows/deployment.yml
vendored
|
|
@ -309,7 +309,7 @@ jobs:
|
|||
rpmsign --addsign dist/*.rpm
|
||||
- name: Attest release artifacts
|
||||
if: inputs.environment == 'production'
|
||||
uses: actions/attest-build-provenance@bd77c077858b8d561b7a36cbe48ef4cc642ca39d # v2.2.2
|
||||
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
||||
with:
|
||||
subject-path: "dist/gh_*"
|
||||
- name: Run createrepo
|
||||
|
|
@ -384,7 +384,7 @@ jobs:
|
|||
git diff --name-status @{upstream}..
|
||||
fi
|
||||
- name: Bump homebrew-core formula
|
||||
uses: mislav/bump-homebrew-formula-action@942e550c6344cfdb9e1ab29b9bb9bf0c43efa19b
|
||||
uses: mislav/bump-homebrew-formula-action@8e2baa47daaa8db10fcdeb04105dfa6850eb0d68
|
||||
if: inputs.environment == 'production' && !contains(inputs.tag_name, '-')
|
||||
with:
|
||||
formula-name: gh
|
||||
|
|
|
|||
2
.github/workflows/homebrew-bump.yml
vendored
2
.github/workflows/homebrew-bump.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Bump homebrew-core formula
|
||||
uses: mislav/bump-homebrew-formula-action@942e550c6344cfdb9e1ab29b9bb9bf0c43efa19b
|
||||
uses: mislav/bump-homebrew-formula-action@8e2baa47daaa8db10fcdeb04105dfa6850eb0d68
|
||||
if: inputs.environment == 'production' && !contains(inputs.tag_name, '-')
|
||||
with:
|
||||
formula-name: gh
|
||||
|
|
|
|||
22
.github/workflows/lint.yml
vendored
22
.github/workflows/lint.yml
vendored
|
|
@ -27,17 +27,7 @@ jobs:
|
|||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Verify dependencies
|
||||
run: |
|
||||
go mod verify
|
||||
go mod download
|
||||
|
||||
LINT_VERSION=1.63.4
|
||||
curl -fsSL https://github.com/golangci/golangci-lint/releases/download/v${LINT_VERSION}/golangci-lint-${LINT_VERSION}-linux-amd64.tar.gz | \
|
||||
tar xz --strip-components 1 --wildcards \*/golangci-lint
|
||||
mkdir -p bin && mv golangci-lint bin/
|
||||
|
||||
- name: Run checks
|
||||
- name: Ensure go.mod and go.sum are up to date
|
||||
run: |
|
||||
STATUS=0
|
||||
assert-nothing-changed() {
|
||||
|
|
@ -49,10 +39,10 @@ jobs:
|
|||
STATUS=1
|
||||
fi
|
||||
}
|
||||
|
||||
assert-nothing-changed go fmt ./...
|
||||
assert-nothing-changed go mod tidy
|
||||
|
||||
bin/golangci-lint run --out-format=colored-line-number --timeout=3m || STATUS=$?
|
||||
|
||||
exit $STATUS
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
||||
with:
|
||||
version: v2.1.6
|
||||
|
|
|
|||
27
.github/workflows/pr-help-wanted.yml
vendored
Normal file
27
.github/workflows/pr-help-wanted.yml
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
name: PR Help Wanted Check
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: none
|
||||
issues: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check-help-wanted:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for issues without help-wanted label
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
PR_AUTHOR_TYPE: ${{ github.event.pull_request.user.type }}
|
||||
PR_AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }}
|
||||
if: "!github.event.pull_request.draft"
|
||||
run: |
|
||||
# Run the script to check for issues without help-wanted label
|
||||
bash .github/workflows/scripts/check-help-wanted.sh ${{ github.event.pull_request.html_url }}
|
||||
99
.github/workflows/scripts/check-help-wanted.sh
vendored
Executable file
99
.github/workflows/scripts/check-help-wanted.sh
vendored
Executable file
|
|
@ -0,0 +1,99 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
PR_URL="$1"
|
||||
|
||||
if [ -z "$PR_URL" ]; then
|
||||
echo "Usage: $0 <PR_URL>"
|
||||
echo ""
|
||||
echo "Check if the PR references any non-help-wanted issues and, if so, comment"
|
||||
echo "on it explaining why the team might close/dismiss it."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Skip if PR is from a bot or org member
|
||||
if [ "$PR_AUTHOR_TYPE" = "Bot" ] || [ "$PR_AUTHOR_ASSOCIATION" = "MEMBER" ] || [ "$PR_AUTHOR_ASSOCIATION" = "OWNER" ]; then
|
||||
echo "Skipping check for PR #$PR_URL as it is from a bot ($PR_AUTHOR_TYPE) or an org member ($PR_AUTHOR_ASSOCIATION: MEMBER/OWNER)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract PR number from URL for logging
|
||||
PR_NUM="$(basename "$PR_URL")"
|
||||
|
||||
# Extract cli/cli closing issues references from PR
|
||||
CLOSING_ISSUES="$(gh pr view "$PR_URL" --json closingIssuesReferences --jq '.closingIssuesReferences[] | select(.repository.name == "cli" and .repository.owner.login == "cli") | .number')"
|
||||
|
||||
if [ -z "$CLOSING_ISSUES" ]; then
|
||||
echo "No closing issues found for PR #$PR_NUM"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check each closing issue for 'help-wanted' label
|
||||
ISSUES_WITHOUT_HELP_WANTED=()
|
||||
|
||||
for issue_num in $CLOSING_ISSUES; do
|
||||
echo "Checking issue #$issue_num for 'help wanted' label..."
|
||||
|
||||
# Get issue labels
|
||||
LABELS=$(gh issue view "$issue_num" --json labels --jq '.labels[].name')
|
||||
|
||||
# Skip if the issue has the gh-attestion or gh-codespace label
|
||||
# This is because the codeowners for these commands may not be public
|
||||
# cli org members, and so unless we authenticate with a PAT, we can't
|
||||
# know who is an external contributor or not.
|
||||
# So we skip these issues to avoid falsely writing a comment
|
||||
# on each PR opened by these codeowners.
|
||||
if echo "$LABELS" | grep -q -e "gh-attestation" -e "gh-codespace"; then
|
||||
echo "Issue #$issue_num is skipped due to labels"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if 'help wanted' label exists
|
||||
if ! echo "$LABELS" | grep -q "help wanted"; then
|
||||
ISSUES_WITHOUT_HELP_WANTED+=("$issue_num")
|
||||
echo "Issue #$issue_num does not have 'help wanted' label"
|
||||
else
|
||||
echo "Issue #$issue_num has 'help wanted' label"
|
||||
fi
|
||||
done
|
||||
|
||||
# If we found issues without 'help wanted' label, post a comment
|
||||
if [ ${#ISSUES_WITHOUT_HELP_WANTED[@]} -gt 0 ]; then
|
||||
echo "Found ${#ISSUES_WITHOUT_HELP_WANTED[@]} issues without 'help wanted' label"
|
||||
|
||||
# Build issue list for comment
|
||||
ISSUE_LIST=""
|
||||
for issue_num in "${ISSUES_WITHOUT_HELP_WANTED[@]}"; do
|
||||
ISSUE_LIST="$ISSUE_LIST- #$issue_num"$'\n'
|
||||
done
|
||||
|
||||
# Create comment message
|
||||
gh pr comment "$PR_URL" --body-file - <<EOF
|
||||
Thank you for your pull request! 🎉
|
||||
|
||||
This PR appears to fix the following issues that are not labeled with \`help wanted\`:
|
||||
|
||||
$ISSUE_LIST
|
||||
As outlined in our [Contributing Guidelines](https://github.com/cli/cli/blob/trunk/.github/CONTRIBUTING.md), we expect that PRs are only created for issues that have been labeled \`help wanted\`.
|
||||
|
||||
While we appreciate your initiative, please note that:
|
||||
|
||||
- **PRs for non-\`help wanted\` issues may not be reviewed immediately** as they might not align with our current priorities
|
||||
- **The issue might already be assigned** to a team member or planned for a specific release
|
||||
- **We may need to close this PR**. For example, if it conflicts with ongoing work or architectural decisions
|
||||
|
||||
**What happens next:**
|
||||
- Our team will review this PR and the associated issues
|
||||
- We may add the \`help wanted\` label to the issues, if appropriate, and review this pull request
|
||||
- In some cases, we may need to close the PR. For example, if it doesn't fit our current roadmap
|
||||
|
||||
Thank you for your understanding and contribution to the project! 🙏
|
||||
|
||||
*This comment was automatically generated by cliAutomation.*
|
||||
EOF
|
||||
|
||||
echo "Posted comment on PR #$PR_NUM"
|
||||
else
|
||||
echo "All closing issues have 'help wanted' label - no action needed"
|
||||
fi
|
||||
|
|
@ -1,7 +1,17 @@
|
|||
version: "2"
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- gofmt
|
||||
- nolintlint
|
||||
- nolintlint
|
||||
disable:
|
||||
# The following linters are disabled purely because this config was migrated to v2 where they are in the default
|
||||
# set, and we should have separate work to enable them if we truly want them.
|
||||
- staticcheck
|
||||
- errcheck
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ For [installation options see below](#installation), for usage instructions [see
|
|||
|
||||
## Contributing
|
||||
|
||||
If anything feels off, or if you feel that some functionality is missing, please check out the [contributing page][contributing]. There you will find instructions for sharing your feedback, building the tool locally, and submitting pull requests to the project.
|
||||
If anything feels off or if you feel that some functionality is missing, please check out the [contributing page][contributing]. There you will find instructions for sharing your feedback, building the tool locally, and submitting pull requests to the project.
|
||||
|
||||
If you are a hubber and are interested in shipping new commands for the CLI, check out our [doc on internal contributions][intake-doc].
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ Additional Conda installation options available on the [gh-feedstock page](https
|
|||
| ----------------------------------- | ---------------- |
|
||||
| `curl -sS https://webi.sh/gh \| sh` | `webi gh@stable` |
|
||||
|
||||
For more information about the Webi installer see [its homepage](https://webinstall.dev/).
|
||||
For more information about the Webi installer, see [its homepage](https://webinstall.dev/).
|
||||
|
||||
#### Flox
|
||||
|
||||
|
|
@ -127,9 +127,9 @@ Download packaged binaries from the [releases page][].
|
|||
|
||||
#### Verification of binaries
|
||||
|
||||
Since version 2.50.0 `gh` has been producing [Build Provenance Attestation](https://github.blog/changelog/2024-06-25-artifact-attestations-is-generally-available/) enabling a cryptographically verifiable paper-trail back to the origin GitHub repository, git revision and build instructions used. The build provenance attestations are signed and relies on Public Good [Sigstore](https://www.sigstore.dev/) for PKI.
|
||||
Since version 2.50.0, `gh` has been producing [Build Provenance Attestation](https://github.blog/changelog/2024-06-25-artifact-attestations-is-generally-available/), enabling a cryptographically verifiable paper-trail back to the origin GitHub repository, git revision, and build instructions used. The build provenance attestations are signed and rely on Public Good [Sigstore](https://www.sigstore.dev/) for PKI.
|
||||
|
||||
There are two common ways to verify a downloaded release, depending if `gh` is already installed or not. If `gh` is installed, it's trivial to verify a new release:
|
||||
There are two common ways to verify a downloaded release, depending on whether `gh` is already installed or not. If `gh` is installed, it's trivial to verify a new release:
|
||||
|
||||
- **Option 1: Using `gh` if already installed:**
|
||||
|
||||
|
|
|
|||
50
acceptance/testdata/pr/pr-create-guesses-remote-from-sha-with-branch-name-slash.txtar
vendored
Normal file
50
acceptance/testdata/pr/pr-create-guesses-remote-from-sha-with-branch-name-slash.txtar
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
skip 'it creates a fork owned by the user running the test'
|
||||
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
env FORK=${REPO}-fork
|
||||
|
||||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Get the current username for the fork owner
|
||||
exec gh api user --jq .login
|
||||
stdout2env USER
|
||||
|
||||
# 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}
|
||||
|
||||
# Create a user fork of repository. This will be owned by USER.
|
||||
exec gh repo fork ${ORG}/${REPO} --fork-name ${FORK}
|
||||
sleep 5
|
||||
|
||||
# Defer repo cleanup of fork
|
||||
defer gh repo delete --yes ${USER}/${FORK}
|
||||
|
||||
# Retrieve fork repository information
|
||||
exec gh repo view ${USER}/${FORK} --json id --jq '.id'
|
||||
stdout2env FORK_ID
|
||||
|
||||
exec gh repo clone ${USER}/${FORK}
|
||||
cd ${FORK}
|
||||
|
||||
# Prepare a branch to commit
|
||||
exec git checkout -b feature/branch
|
||||
exec git commit --allow-empty -m 'Upstream Commit'
|
||||
# Push without setting an upstream (-u or config)
|
||||
exec git push upstream feature/branch
|
||||
|
||||
# Prepare an additional commit
|
||||
exec git commit --allow-empty -m 'Fork Commit'
|
||||
# Push without setting an upstream (-u or config)
|
||||
exec git push origin feature/branch
|
||||
|
||||
# Create the PR
|
||||
exec gh pr create --title 'Feature Title' --body 'Feature Body'
|
||||
stdout https://${GH_HOST}/${ORG}/${REPO}/pull/1
|
||||
|
||||
# Check the PR is indeed created
|
||||
exec gh pr view ${USER}:feature/branch --json headRefName,headRepository,baseRefName,isCrossRepository
|
||||
stdout {"baseRefName":"main","headRefName":"feature/branch","headRepository":{"id":"${FORK_ID}","name":"${FORK}"},"isCrossRepository":true}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
skip 'it creates a fork owned by the user running the test'
|
||||
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
env FORK=${REPO}-fork
|
||||
|
||||
|
|
|
|||
50
acceptance/testdata/pr/pr-create-push-default-upstream-no-merge-ref-fork.txtar
vendored
Normal file
50
acceptance/testdata/pr/pr-create-push-default-upstream-no-merge-ref-fork.txtar
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
skip 'it creates a fork owned by the user running the test'
|
||||
skip 'this never worked, but could be fixed if we fixed show-refs'
|
||||
|
||||
# Setup environment variables used for testscript
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
env FORK=${REPO}-fork
|
||||
|
||||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Get the current username for the fork owner
|
||||
exec gh api user --jq .login
|
||||
stdout2env USER
|
||||
|
||||
# Create a repository to act as upstream with a file so it has a default branch
|
||||
exec gh repo create ${ORG}/${REPO} --add-readme --private
|
||||
|
||||
# Defer repo cleanup of upstream
|
||||
defer gh repo delete --yes ${ORG}/${REPO}
|
||||
|
||||
# Create a user fork of repository. This will be owned by USER.
|
||||
exec gh repo fork ${ORG}/${REPO} --fork-name ${FORK}
|
||||
sleep 5
|
||||
|
||||
# Defer repo cleanup of fork
|
||||
defer gh repo delete --yes ${USER}/${FORK}
|
||||
|
||||
# Retrieve fork repository information
|
||||
exec gh repo view ${USER}/${FORK} --json id --jq '.id'
|
||||
stdout2env FORK_ID
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone ${USER}/${FORK}
|
||||
cd ${FORK}
|
||||
|
||||
# Configure push.default so that it should use the merge ref
|
||||
exec git config push.default upstream
|
||||
|
||||
# But prepare a branch that doesn't have a tracking merge ref
|
||||
exec git checkout -b feature-branch
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push origin feature-branch
|
||||
|
||||
# Create the PR
|
||||
exec gh pr create --title 'Feature Title' --body 'Feature Body'
|
||||
stdout https://${GH_HOST}/${ORG}/${REPO}/pull/1
|
||||
|
||||
# Assert that the PR was created with the correct head repository and refs
|
||||
exec gh pr view --json headRefName,headRepository,baseRefName,isCrossRepository
|
||||
stdout {"baseRefName":"main","headRefName":"feature-branch","headRepository":{"id":"${FORK_ID}","name":"${FORK}"},"isCrossRepository":true}
|
||||
33
acceptance/testdata/pr/pr-create-push-default-upstream-no-merge-ref.txtar
vendored
Normal file
33
acceptance/testdata/pr/pr-create-push-default-upstream-no-merge-ref.txtar
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
skip 'it creates a fork owned by the user running the test'
|
||||
|
||||
# Setup environment variables used for testscript
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
|
||||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Get the current username for the fork owner
|
||||
exec gh api user --jq .login
|
||||
stdout2env USER
|
||||
|
||||
# Create a repository to act as upstream with a file so it has a default branch
|
||||
exec gh repo create ${ORG}/${REPO} --add-readme --private
|
||||
|
||||
# Defer repo cleanup of upstream
|
||||
defer gh repo delete --yes ${ORG}/${REPO}
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone ${ORG}/${REPO}
|
||||
cd ${REPO}
|
||||
|
||||
# Configure push.default so that it should use the merge ref
|
||||
exec git config push.default upstream
|
||||
|
||||
# But prepare a branch that doesn't have a tracking merge ref
|
||||
exec git checkout -b feature-branch
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push origin feature-branch
|
||||
|
||||
# Create the PR
|
||||
exec gh pr create --title 'Feature Title' --body 'Feature Body'
|
||||
stdout https://${GH_HOST}/${ORG}/${REPO}/pull/1
|
||||
46
acceptance/testdata/pr/pr-create-remote-ref-with-branch-name-slash.txtar
vendored
Normal file
46
acceptance/testdata/pr/pr-create-remote-ref-with-branch-name-slash.txtar
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
skip 'it creates a fork owned by the user running the test'
|
||||
|
||||
# Setup environment variables used for testscript
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
env FORK=${REPO}-fork
|
||||
|
||||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Get the current username for the fork owner
|
||||
exec gh api user --jq .login
|
||||
stdout2env USER
|
||||
|
||||
# Create a repository to act as upstream with a file so it has a default branch
|
||||
exec gh repo create ${ORG}/${REPO} --add-readme --private
|
||||
|
||||
# Defer repo cleanup of upstream
|
||||
defer gh repo delete --yes ${ORG}/${REPO}
|
||||
|
||||
# Create a user fork of repository. This will be owned by USER.
|
||||
exec gh repo fork ${ORG}/${REPO} --fork-name ${FORK}
|
||||
sleep 5
|
||||
|
||||
# Defer repo cleanup of fork
|
||||
defer gh repo delete --yes ${USER}/${FORK}
|
||||
|
||||
# Retrieve fork repository information
|
||||
exec gh repo view ${USER}/${FORK} --json id --jq '.id'
|
||||
stdout2env FORK_ID
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone ${USER}/${FORK}
|
||||
cd ${FORK}
|
||||
|
||||
# Prepare a branch where changes are pulled from the upstream default branch but pushed to fork
|
||||
exec git checkout -b feature/branch
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push -u origin feature/branch
|
||||
|
||||
# Create the PR spanning upstream and fork repositories
|
||||
exec gh pr create --title 'Feature Title' --body 'Feature Body'
|
||||
stdout https://${GH_HOST}/${ORG}/${REPO}/pull/1
|
||||
|
||||
# Assert that the PR was created with the correct head repository and refs
|
||||
exec gh pr view --json headRefName,headRepository,baseRefName,isCrossRepository
|
||||
stdout {"baseRefName":"main","headRefName":"feature/branch","headRepository":{"id":"${FORK_ID}","name":"${FORK}"},"isCrossRepository":true}
|
||||
|
|
@ -3,7 +3,7 @@ exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
|||
|
||||
# Attempt to rename the repo with a slash in the name
|
||||
! exec gh repo rename $ORG/new-name --repo=$ORG/$SCRIPT_NAME-$RANDOM_STRING --yes
|
||||
stderr 'New repository name cannot contain '/' character - to transfer a repository to a new owner, you must follow additional steps on GitHub.com. For more information on transferring repository ownership, see <https://docs.github.com/en/repositories/creating-and-managing-repositories/transferring-a-repository>.'
|
||||
stderr 'New repository name cannot contain \''/\'' character - to transfer a repository to a new owner, you must follow additional steps on <github.com>. For more information on transferring repository ownership, see <https://docs.github.com/en/repositories/creating-and-managing-repositories/transferring-a-repository>.'
|
||||
|
||||
# Defer repo deletion
|
||||
defer gh repo delete $ORG/$SCRIPT_NAME-$RANDOM_STRING --yes
|
||||
|
|
@ -7,7 +7,7 @@ defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
|||
# Ensure that no default is set
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec gh repo set-default --view
|
||||
stderr 'no default repository has been set; use `gh repo set-default` to select one'
|
||||
stderr 'No default remote repository has been set. To learn more about the default repository, run: gh repo set-default --help'
|
||||
|
||||
# Set the default
|
||||
exec gh repo set-default $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
|
|
|||
|
|
@ -28,6 +28,24 @@ func (issue *Issue) ExportData(fields []string) map[string]interface{} {
|
|||
})
|
||||
}
|
||||
data[f] = items
|
||||
case "closedByPullRequestsReferences":
|
||||
items := make([]map[string]interface{}, 0, len(issue.ClosedByPullRequestsReferences.Nodes))
|
||||
for _, n := range issue.ClosedByPullRequestsReferences.Nodes {
|
||||
items = append(items, map[string]interface{}{
|
||||
"id": n.ID,
|
||||
"number": n.Number,
|
||||
"url": n.URL,
|
||||
"repository": map[string]interface{}{
|
||||
"id": n.Repository.ID,
|
||||
"name": n.Repository.Name,
|
||||
"owner": map[string]interface{}{
|
||||
"id": n.Repository.Owner.ID,
|
||||
"login": n.Repository.Owner.Login,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
data[f] = items
|
||||
default:
|
||||
sf := fieldByName(v, f)
|
||||
data[f] = sf.Interface()
|
||||
|
|
@ -139,6 +157,24 @@ func (pr *PullRequest) ExportData(fields []string) map[string]interface{} {
|
|||
}
|
||||
}
|
||||
data[f] = &requests
|
||||
case "closingIssuesReferences":
|
||||
items := make([]map[string]interface{}, 0, len(pr.ClosingIssuesReferences.Nodes))
|
||||
for _, n := range pr.ClosingIssuesReferences.Nodes {
|
||||
items = append(items, map[string]interface{}{
|
||||
"id": n.ID,
|
||||
"number": n.Number,
|
||||
"url": n.URL,
|
||||
"repository": map[string]interface{}{
|
||||
"id": n.Repository.ID,
|
||||
"name": n.Repository.Name,
|
||||
"owner": map[string]interface{}{
|
||||
"id": n.Repository.Owner.ID,
|
||||
"login": n.Repository.Owner.Login,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
data[f] = items
|
||||
default:
|
||||
sf := fieldByName(v, f)
|
||||
data[f] = sf.Interface()
|
||||
|
|
|
|||
|
|
@ -107,6 +107,70 @@ func TestIssue_ExportData(t *testing.T) {
|
|||
}
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "linked pull requests",
|
||||
fields: []string{"closedByPullRequestsReferences"},
|
||||
inputJSON: heredoc.Doc(`
|
||||
{ "closedByPullRequestsReferences": { "nodes": [
|
||||
{
|
||||
"id": "I_123",
|
||||
"number": 123,
|
||||
"url": "https://github.com/cli/cli/pull/123",
|
||||
"repository": {
|
||||
"id": "R_123",
|
||||
"name": "cli",
|
||||
"owner": {
|
||||
"id": "O_123",
|
||||
"login": "cli"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "I_456",
|
||||
"number": 456,
|
||||
"url": "https://github.com/cli/cli/pull/456",
|
||||
"repository": {
|
||||
"id": "R_456",
|
||||
"name": "cli",
|
||||
"owner": {
|
||||
"id": "O_456",
|
||||
"login": "cli"
|
||||
}
|
||||
}
|
||||
}
|
||||
] } }
|
||||
`),
|
||||
outputJSON: heredoc.Doc(`
|
||||
{ "closedByPullRequestsReferences": [
|
||||
{
|
||||
"id": "I_123",
|
||||
"number": 123,
|
||||
"repository": {
|
||||
"id": "R_123",
|
||||
"name": "cli",
|
||||
"owner": {
|
||||
"id": "O_123",
|
||||
"login": "cli"
|
||||
}
|
||||
},
|
||||
"url": "https://github.com/cli/cli/pull/123"
|
||||
},
|
||||
{
|
||||
"id": "I_456",
|
||||
"number": 456,
|
||||
"repository": {
|
||||
"id": "R_456",
|
||||
"name": "cli",
|
||||
"owner": {
|
||||
"id": "O_456",
|
||||
"login": "cli"
|
||||
}
|
||||
},
|
||||
"url": "https://github.com/cli/cli/pull/456"
|
||||
}
|
||||
] }
|
||||
`),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
@ -120,7 +184,14 @@ func TestIssue_ExportData(t *testing.T) {
|
|||
enc := json.NewEncoder(&buf)
|
||||
enc.SetIndent("", "\t")
|
||||
require.NoError(t, enc.Encode(exported))
|
||||
assert.Equal(t, tt.outputJSON, buf.String())
|
||||
|
||||
var gotData interface{}
|
||||
dec = json.NewDecoder(&buf)
|
||||
require.NoError(t, dec.Decode(&gotData))
|
||||
var expectData interface{}
|
||||
require.NoError(t, json.Unmarshal([]byte(tt.outputJSON), &expectData))
|
||||
|
||||
assert.Equal(t, expectData, gotData)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -245,6 +316,70 @@ func TestPullRequest_ExportData(t *testing.T) {
|
|||
}
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "linked issues",
|
||||
fields: []string{"closingIssuesReferences"},
|
||||
inputJSON: heredoc.Doc(`
|
||||
{ "closingIssuesReferences": { "nodes": [
|
||||
{
|
||||
"id": "I_123",
|
||||
"number": 123,
|
||||
"url": "https://github.com/cli/cli/issues/123",
|
||||
"repository": {
|
||||
"id": "R_123",
|
||||
"name": "cli",
|
||||
"owner": {
|
||||
"id": "O_123",
|
||||
"login": "cli"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "I_456",
|
||||
"number": 456,
|
||||
"url": "https://github.com/cli/cli/issues/456",
|
||||
"repository": {
|
||||
"id": "R_456",
|
||||
"name": "cli",
|
||||
"owner": {
|
||||
"id": "O_456",
|
||||
"login": "cli"
|
||||
}
|
||||
}
|
||||
}
|
||||
] } }
|
||||
`),
|
||||
outputJSON: heredoc.Doc(`
|
||||
{ "closingIssuesReferences": [
|
||||
{
|
||||
"id": "I_123",
|
||||
"number": 123,
|
||||
"repository": {
|
||||
"id": "R_123",
|
||||
"name": "cli",
|
||||
"owner": {
|
||||
"id": "O_123",
|
||||
"login": "cli"
|
||||
}
|
||||
},
|
||||
"url": "https://github.com/cli/cli/issues/123"
|
||||
},
|
||||
{
|
||||
"id": "I_456",
|
||||
"number": 456,
|
||||
"repository": {
|
||||
"id": "R_456",
|
||||
"name": "cli",
|
||||
"owner": {
|
||||
"id": "O_456",
|
||||
"login": "cli"
|
||||
}
|
||||
},
|
||||
"url": "https://github.com/cli/cli/issues/456"
|
||||
}
|
||||
] }
|
||||
`),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ type CommentCreateInput struct {
|
|||
SubjectId string
|
||||
}
|
||||
|
||||
type CommentDeleteInput struct {
|
||||
CommentId string
|
||||
}
|
||||
|
||||
type CommentUpdateInput struct {
|
||||
Body string
|
||||
CommentId string
|
||||
|
|
@ -99,6 +103,27 @@ func CommentUpdate(client *Client, repoHost string, params CommentUpdateInput) (
|
|||
return mutation.UpdateIssueComment.IssueComment.URL, nil
|
||||
}
|
||||
|
||||
func CommentDelete(client *Client, repoHost string, params CommentDeleteInput) error {
|
||||
var mutation struct {
|
||||
DeleteIssueComment struct {
|
||||
ClientMutationID string
|
||||
} `graphql:"deleteIssueComment(input: $input)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"input": githubv4.DeleteIssueCommentInput{
|
||||
ID: githubv4.ID(params.CommentId),
|
||||
},
|
||||
}
|
||||
|
||||
err := client.Mutate(repoHost, "CommentDelete", &mutation, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Comment) Identifier() string {
|
||||
return c.ID
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,12 +38,35 @@ type Issue struct {
|
|||
Comments Comments
|
||||
Author Author
|
||||
Assignees Assignees
|
||||
AssignedActors AssignedActors
|
||||
Labels Labels
|
||||
ProjectCards ProjectCards
|
||||
ProjectItems ProjectItems
|
||||
Milestone *Milestone
|
||||
ReactionGroups ReactionGroups
|
||||
IsPinned bool
|
||||
|
||||
ClosedByPullRequestsReferences ClosedByPullRequestsReferences
|
||||
}
|
||||
|
||||
type ClosedByPullRequestsReferences struct {
|
||||
Nodes []struct {
|
||||
ID string
|
||||
Number int
|
||||
URL string
|
||||
Repository struct {
|
||||
ID string
|
||||
Name string
|
||||
Owner struct {
|
||||
ID string
|
||||
Login string
|
||||
}
|
||||
}
|
||||
}
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
}
|
||||
|
||||
// return values for Issue.Typename
|
||||
|
|
@ -69,6 +92,61 @@ func (a Assignees) Logins() []string {
|
|||
return logins
|
||||
}
|
||||
|
||||
type AssignedActors struct {
|
||||
Nodes []Actor
|
||||
TotalCount int
|
||||
}
|
||||
|
||||
func (a AssignedActors) Logins() []string {
|
||||
logins := make([]string, len(a.Nodes))
|
||||
for i, a := range a.Nodes {
|
||||
logins[i] = a.Login
|
||||
}
|
||||
return logins
|
||||
}
|
||||
|
||||
// DisplayNames returns a list of display names for the assigned actors.
|
||||
func (a AssignedActors) DisplayNames() []string {
|
||||
// These display names are used for populating the "default" assigned actors
|
||||
// from the AssignedActors type. But, this is only one piece of the puzzle
|
||||
// as later, other queries will fetch the full list of possible assignable
|
||||
// actors from the repository, and the two lists will be reconciled.
|
||||
//
|
||||
// It's important that the display names are the same between the defaults
|
||||
// (the values returned here) and the full list (the values returned by
|
||||
// other repository queries). Any discrepancy would result in an
|
||||
// "invalid default", which means an assigned actor will not be matched
|
||||
// to an assignable actor and not presented as a "default" selection.
|
||||
// Not being presented as a default would cause the actor to be potentially
|
||||
// unassigned if the edits were submitted.
|
||||
//
|
||||
// To prevent this, we need shared logic to look up an actor's display name.
|
||||
// However, our API types between assignedActors and the full list of
|
||||
// assignableActors are different. So, as an attempt to maintain
|
||||
// consistency we convert the assignedActors to the same types as the
|
||||
// repository's assignableActors, treating the assignableActors DisplayName
|
||||
// methods as the sources of truth.
|
||||
// TODO KW: make this comment less of a wall of text if needed.
|
||||
var displayNames []string
|
||||
for _, a := range a.Nodes {
|
||||
if a.TypeName == "User" {
|
||||
u := NewAssignableUser(
|
||||
a.ID,
|
||||
a.Login,
|
||||
a.Name,
|
||||
)
|
||||
displayNames = append(displayNames, u.DisplayName())
|
||||
} else if a.TypeName == "Bot" {
|
||||
b := NewAssignableBot(
|
||||
a.ID,
|
||||
a.Login,
|
||||
)
|
||||
displayNames = append(displayNames, b.DisplayName())
|
||||
}
|
||||
}
|
||||
return displayNames
|
||||
}
|
||||
|
||||
type Labels struct {
|
||||
Nodes []IssueLabel
|
||||
TotalCount int
|
||||
|
|
|
|||
|
|
@ -84,15 +84,28 @@ type PullRequest struct {
|
|||
}
|
||||
|
||||
Assignees Assignees
|
||||
Labels Labels
|
||||
ProjectCards ProjectCards
|
||||
ProjectItems ProjectItems
|
||||
Milestone *Milestone
|
||||
Comments Comments
|
||||
ReactionGroups ReactionGroups
|
||||
Reviews PullRequestReviews
|
||||
LatestReviews PullRequestReviews
|
||||
ReviewRequests ReviewRequests
|
||||
AssignedActors AssignedActors
|
||||
// AssignedActorsUsed is a GIGANTIC hack to carry around whether we expected AssignedActors to be requested
|
||||
// on this PR. This is required because the Feature Detection of support for AssignedActors occurs inside the
|
||||
// PR Finder, but knowledge of support is required at the command level. However, we can't easily construct
|
||||
// the feature detector at the command level because it needs knowledge of the BaseRepo, which is only available
|
||||
// inside the PR Finder. This is bad and we should feel bad.
|
||||
//
|
||||
// The right solution is to extract argument parsing from the PR Finder into each command, so that we have access
|
||||
// to the BaseRepo and can construct the feature detector there. This is what happens in the issue commands with
|
||||
// `shared.ParseIssueFromArg`.
|
||||
AssignedActorsUsed bool
|
||||
Labels Labels
|
||||
ProjectCards ProjectCards
|
||||
ProjectItems ProjectItems
|
||||
Milestone *Milestone
|
||||
Comments Comments
|
||||
ReactionGroups ReactionGroups
|
||||
Reviews PullRequestReviews
|
||||
LatestReviews PullRequestReviews
|
||||
ReviewRequests ReviewRequests
|
||||
|
||||
ClosingIssuesReferences ClosingIssuesReferences
|
||||
}
|
||||
|
||||
type StatusCheckRollupNode struct {
|
||||
|
|
@ -107,6 +120,26 @@ type CommitStatusCheckRollup struct {
|
|||
Contexts CheckContexts
|
||||
}
|
||||
|
||||
type ClosingIssuesReferences struct {
|
||||
Nodes []struct {
|
||||
ID string
|
||||
Number int
|
||||
URL string
|
||||
Repository struct {
|
||||
ID string
|
||||
Name string
|
||||
Owner struct {
|
||||
ID string
|
||||
Login string
|
||||
}
|
||||
}
|
||||
}
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.github.com/en/graphql/reference/enums#checkrunstate
|
||||
type CheckRunState string
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
|
|
@ -145,6 +146,18 @@ type GitHubUser struct {
|
|||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Actor is a superset of User and Bot, among others.
|
||||
// At the time of writing, some of these fields
|
||||
// are not directly supported by the Actor type and
|
||||
// instead are only available on the User or Bot types
|
||||
// directly.
|
||||
type Actor struct {
|
||||
ID string `json:"id"`
|
||||
Login string `json:"login"`
|
||||
Name string `json:"name"`
|
||||
TypeName string `json:"__typename"`
|
||||
}
|
||||
|
||||
// BranchRef is the branch name in a GitHub repository
|
||||
type BranchRef struct {
|
||||
Name string `json:"name"`
|
||||
|
|
@ -673,13 +686,14 @@ func RepoFindForks(client *Client, repo ghrepo.Interface, limit int) ([]*Reposit
|
|||
}
|
||||
|
||||
type RepoMetadataResult struct {
|
||||
CurrentLogin string
|
||||
AssignableUsers []RepoAssignee
|
||||
Labels []RepoLabel
|
||||
Projects []RepoProject
|
||||
ProjectsV2 []ProjectV2
|
||||
Milestones []RepoMilestone
|
||||
Teams []OrgTeam
|
||||
CurrentLogin string
|
||||
AssignableUsers []AssignableUser
|
||||
AssignableActors []AssignableActor
|
||||
Labels []RepoLabel
|
||||
Projects []RepoProject
|
||||
ProjectsV2 []ProjectV2
|
||||
Milestones []RepoMilestone
|
||||
Teams []OrgTeam
|
||||
}
|
||||
|
||||
func (m *RepoMetadataResult) MembersToIDs(names []string) ([]string, error) {
|
||||
|
|
@ -687,12 +701,30 @@ func (m *RepoMetadataResult) MembersToIDs(names []string) ([]string, error) {
|
|||
for _, assigneeLogin := range names {
|
||||
found := false
|
||||
for _, u := range m.AssignableUsers {
|
||||
if strings.EqualFold(assigneeLogin, u.Login) {
|
||||
ids = append(ids, u.ID)
|
||||
if strings.EqualFold(assigneeLogin, u.Login()) {
|
||||
ids = append(ids, u.ID())
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Look for ID in assignable actors if not found in assignable users
|
||||
if !found {
|
||||
for _, a := range m.AssignableActors {
|
||||
if strings.EqualFold(assigneeLogin, a.Login()) {
|
||||
ids = append(ids, a.ID())
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if strings.EqualFold(assigneeLogin, a.DisplayName()) {
|
||||
ids = append(ids, a.ID())
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// And if we still didn't find an ID, return an error
|
||||
if !found {
|
||||
return nil, fmt.Errorf("'%s' not found", assigneeLogin)
|
||||
}
|
||||
|
|
@ -737,34 +769,37 @@ func (m *RepoMetadataResult) LabelsToIDs(names []string) ([]string, error) {
|
|||
return ids, nil
|
||||
}
|
||||
|
||||
// ProjectsToIDs returns two arrays:
|
||||
// ProjectsTitlesToIDs returns two arrays:
|
||||
// - the first contains IDs of projects V1
|
||||
// - the second contains IDs of projects V2
|
||||
// - if neither project V1 or project V2 can be found with a given name, then an error is returned
|
||||
func (m *RepoMetadataResult) ProjectsToIDs(names []string) ([]string, []string, error) {
|
||||
func (m *RepoMetadataResult) ProjectsTitlesToIDs(titles []string) ([]string, []string, error) {
|
||||
var ids []string
|
||||
var idsV2 []string
|
||||
for _, projectName := range names {
|
||||
id, found := m.projectNameToID(projectName)
|
||||
for _, title := range titles {
|
||||
id, found := m.v1ProjectNameToID(title)
|
||||
if found {
|
||||
ids = append(ids, id)
|
||||
continue
|
||||
}
|
||||
|
||||
idV2, found := m.projectV2TitleToID(projectName)
|
||||
idV2, found := m.v2ProjectTitleToID(title)
|
||||
if found {
|
||||
idsV2 = append(idsV2, idV2)
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, nil, fmt.Errorf("'%s' not found", projectName)
|
||||
return nil, nil, fmt.Errorf("'%s' not found", title)
|
||||
}
|
||||
return ids, idsV2, nil
|
||||
}
|
||||
|
||||
func (m *RepoMetadataResult) projectNameToID(projectName string) (string, bool) {
|
||||
// We use the word "titles" when referring to v1 and v2 projects.
|
||||
// In reality, v1 projects really have "names", so there is a bit of a
|
||||
// mismatch we just need to gloss over.
|
||||
func (m *RepoMetadataResult) v1ProjectNameToID(name string) (string, bool) {
|
||||
for _, p := range m.Projects {
|
||||
if strings.EqualFold(projectName, p.Name) {
|
||||
if strings.EqualFold(name, p.Name) {
|
||||
return p.ID, true
|
||||
}
|
||||
}
|
||||
|
|
@ -772,9 +807,9 @@ func (m *RepoMetadataResult) projectNameToID(projectName string) (string, bool)
|
|||
return "", false
|
||||
}
|
||||
|
||||
func (m *RepoMetadataResult) projectV2TitleToID(projectTitle string) (string, bool) {
|
||||
func (m *RepoMetadataResult) v2ProjectTitleToID(title string) (string, bool) {
|
||||
for _, p := range m.ProjectsV2 {
|
||||
if strings.EqualFold(projectTitle, p.Title) {
|
||||
if strings.EqualFold(title, p.Title) {
|
||||
return p.ID, true
|
||||
}
|
||||
}
|
||||
|
|
@ -782,35 +817,54 @@ func (m *RepoMetadataResult) projectV2TitleToID(projectTitle string) (string, bo
|
|||
return "", false
|
||||
}
|
||||
|
||||
func ProjectsToPaths(projects []RepoProject, projectsV2 []ProjectV2, names []string) ([]string, error) {
|
||||
var paths []string
|
||||
for _, projectName := range names {
|
||||
found := false
|
||||
for _, p := range projects {
|
||||
if strings.EqualFold(projectName, p.Name) {
|
||||
// format of ResourcePath: /OWNER/REPO/projects/PROJECT_NUMBER or /orgs/ORG/projects/PROJECT_NUMBER or /users/USER/projects/PROJECT_NUBER
|
||||
// required format of path: OWNER/REPO/PROJECT_NUMBER or ORG/PROJECT_NUMBER or USER/PROJECT_NUMBER
|
||||
var path string
|
||||
pathParts := strings.Split(p.ResourcePath, "/")
|
||||
if pathParts[1] == "orgs" || pathParts[1] == "users" {
|
||||
path = fmt.Sprintf("%s/%s", pathParts[2], pathParts[4])
|
||||
} else {
|
||||
path = fmt.Sprintf("%s/%s/%s", pathParts[1], pathParts[2], pathParts[4])
|
||||
func ProjectTitlesToPaths(client *Client, repo ghrepo.Interface, titles []string, projectsV1Support gh.ProjectsV1Support) ([]string, error) {
|
||||
paths := make([]string, 0, len(titles))
|
||||
matchedPaths := map[string]struct{}{}
|
||||
|
||||
// TODO: ProjectsV1Cleanup
|
||||
// At this point, we only know the names that the user has provided, so we can't push this conditional up the stack.
|
||||
// First we'll try to match against v1 projects, if supported
|
||||
if projectsV1Support == gh.ProjectsV1Supported {
|
||||
v1Projects, err := v1Projects(client, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, title := range titles {
|
||||
for _, p := range v1Projects {
|
||||
if strings.EqualFold(title, p.Name) {
|
||||
pathParts := strings.Split(p.ResourcePath, "/")
|
||||
var path string
|
||||
if pathParts[1] == "orgs" || pathParts[1] == "users" {
|
||||
path = fmt.Sprintf("%s/%s", pathParts[2], pathParts[4])
|
||||
} else {
|
||||
path = fmt.Sprintf("%s/%s/%s", pathParts[1], pathParts[2], pathParts[4])
|
||||
}
|
||||
paths = append(paths, path)
|
||||
matchedPaths[title] = struct{}{}
|
||||
break
|
||||
}
|
||||
paths = append(paths, path)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
}
|
||||
|
||||
// Then we'll try to match against v2 projects
|
||||
v2Projects, err := v2Projects(client, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, title := range titles {
|
||||
// If we already found a v1 project with this name, skip it
|
||||
if _, ok := matchedPaths[title]; ok {
|
||||
continue
|
||||
}
|
||||
for _, p := range projectsV2 {
|
||||
if strings.EqualFold(projectName, p.Title) {
|
||||
// format of ResourcePath: /OWNER/REPO/projects/PROJECT_NUMBER or /orgs/ORG/projects/PROJECT_NUMBER or /users/USER/projects/PROJECT_NUBER
|
||||
// required format of path: OWNER/REPO/PROJECT_NUMBER or ORG/PROJECT_NUMBER or USER/PROJECT_NUMBER
|
||||
var path string
|
||||
|
||||
found := false
|
||||
for _, p := range v2Projects {
|
||||
if strings.EqualFold(title, p.Title) {
|
||||
pathParts := strings.Split(p.ResourcePath, "/")
|
||||
var path string
|
||||
if pathParts[1] == "orgs" || pathParts[1] == "users" {
|
||||
path = fmt.Sprintf("%s/%s", pathParts[2], pathParts[4])
|
||||
} else {
|
||||
|
|
@ -821,10 +875,12 @@ func ProjectsToPaths(projects []RepoProject, projectsV2 []ProjectV2, names []str
|
|||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil, fmt.Errorf("'%s' not found", projectName)
|
||||
return nil, fmt.Errorf("'%s' not found", title)
|
||||
}
|
||||
}
|
||||
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
|
|
@ -860,11 +916,13 @@ func (m *RepoMetadataResult) Merge(m2 *RepoMetadataResult) {
|
|||
}
|
||||
|
||||
type RepoMetadataInput struct {
|
||||
Assignees bool
|
||||
Reviewers bool
|
||||
Labels bool
|
||||
Projects bool
|
||||
Milestones bool
|
||||
Assignees bool
|
||||
ActorAssignees bool
|
||||
Reviewers bool
|
||||
Labels bool
|
||||
ProjectsV1 bool
|
||||
ProjectsV2 bool
|
||||
Milestones bool
|
||||
}
|
||||
|
||||
// RepoMetadata pre-fetches the metadata for attaching to issues and pull requests
|
||||
|
|
@ -873,15 +931,39 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput
|
|||
var g errgroup.Group
|
||||
|
||||
if input.Assignees || input.Reviewers {
|
||||
g.Go(func() error {
|
||||
users, err := RepoAssignableUsers(client, repo)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error fetching assignees: %w", err)
|
||||
}
|
||||
result.AssignableUsers = users
|
||||
return err
|
||||
})
|
||||
if input.ActorAssignees {
|
||||
g.Go(func() error {
|
||||
actors, err := RepoAssignableActors(client, repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching assignable actors: %w", err)
|
||||
}
|
||||
result.AssignableActors = actors
|
||||
|
||||
// Filter actors for users to use for pull request reviewers,
|
||||
// skip retrieving the same info through RepoAssignableUsers().
|
||||
var users []AssignableUser
|
||||
for _, a := range actors {
|
||||
if _, ok := a.(AssignableUser); !ok {
|
||||
continue
|
||||
}
|
||||
users = append(users, a.(AssignableUser))
|
||||
}
|
||||
result.AssignableUsers = users
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
// Not using Actors, fetch legacy assignable users.
|
||||
g.Go(func() error {
|
||||
users, err := RepoAssignableUsers(client, repo)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error fetching assignable users: %w", err)
|
||||
}
|
||||
result.AssignableUsers = users
|
||||
return err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if input.Reviewers {
|
||||
g.Go(func() error {
|
||||
teams, err := OrganizationTeams(client, repo)
|
||||
|
|
@ -894,6 +976,7 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput
|
|||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if input.Reviewers {
|
||||
g.Go(func() error {
|
||||
login, err := CurrentLoginName(client, repo.RepoHost())
|
||||
|
|
@ -904,6 +987,7 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput
|
|||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if input.Labels {
|
||||
g.Go(func() error {
|
||||
labels, err := RepoLabels(client, repo)
|
||||
|
|
@ -914,13 +998,23 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput
|
|||
return err
|
||||
})
|
||||
}
|
||||
if input.Projects {
|
||||
|
||||
if input.ProjectsV1 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
result.Projects, result.ProjectsV2, err = relevantProjects(client, repo)
|
||||
result.Projects, err = v1Projects(client, repo)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if input.ProjectsV2 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
result.ProjectsV2, err = v2Projects(client, repo)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if input.Milestones {
|
||||
g.Go(func() error {
|
||||
milestones, err := RepoMilestones(client, repo, "open")
|
||||
|
|
@ -943,7 +1037,8 @@ type RepoResolveInput struct {
|
|||
Assignees []string
|
||||
Reviewers []string
|
||||
Labels []string
|
||||
Projects []string
|
||||
ProjectsV1 bool
|
||||
ProjectsV2 bool
|
||||
Milestones []string
|
||||
}
|
||||
|
||||
|
|
@ -970,7 +1065,8 @@ func RepoResolveMetadataIDs(client *Client, repo ghrepo.Interface, input RepoRes
|
|||
|
||||
// there is no way to look up projects nor milestones by name, so preload them all
|
||||
mi := RepoMetadataInput{
|
||||
Projects: len(input.Projects) > 0,
|
||||
ProjectsV1: input.ProjectsV1,
|
||||
ProjectsV2: input.ProjectsV2,
|
||||
Milestones: len(input.Milestones) > 0,
|
||||
}
|
||||
result, err := RepoMetadata(client, repo, mi)
|
||||
|
|
@ -1029,12 +1125,16 @@ func RepoResolveMetadataIDs(client *Client, repo ghrepo.Interface, input RepoRes
|
|||
result.Teams = append(result.Teams, t)
|
||||
}
|
||||
default:
|
||||
user := RepoAssignee{}
|
||||
user := struct {
|
||||
Id string
|
||||
Login string
|
||||
Name string
|
||||
}{}
|
||||
err := json.Unmarshal(v, &user)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.AssignableUsers = append(result.AssignableUsers, user)
|
||||
result.AssignableUsers = append(result.AssignableUsers, NewAssignableUser(user.Id, user.Login, user.Name))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1086,26 +1186,99 @@ func RepoProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, error)
|
|||
return projects, nil
|
||||
}
|
||||
|
||||
type RepoAssignee struct {
|
||||
ID string
|
||||
Login string
|
||||
Name string
|
||||
// Expected login for Copilot when retrieved as an Actor
|
||||
// This is returned from assignable actors and issue/pr assigned actors.
|
||||
// We use this to check if the actor is Copilot.
|
||||
const CopilotActorLogin = "copilot-swe-agent"
|
||||
|
||||
type AssignableActor interface {
|
||||
DisplayName() string
|
||||
ID() string
|
||||
Login() string
|
||||
|
||||
sealedAssignableActor()
|
||||
}
|
||||
|
||||
// Always a user
|
||||
type AssignableUser struct {
|
||||
id string
|
||||
login string
|
||||
name string
|
||||
}
|
||||
|
||||
func NewAssignableUser(id, login, name string) AssignableUser {
|
||||
return AssignableUser{
|
||||
id: id,
|
||||
login: login,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
// DisplayName returns a formatted string that uses Login and Name to be displayed e.g. 'Login (Name)' or 'Login'
|
||||
func (ra RepoAssignee) DisplayName() string {
|
||||
if ra.Name != "" {
|
||||
return fmt.Sprintf("%s (%s)", ra.Login, ra.Name)
|
||||
func (u AssignableUser) DisplayName() string {
|
||||
if u.name != "" {
|
||||
return fmt.Sprintf("%s (%s)", u.login, u.name)
|
||||
}
|
||||
return ra.Login
|
||||
return u.login
|
||||
}
|
||||
|
||||
func (u AssignableUser) ID() string {
|
||||
return u.id
|
||||
}
|
||||
|
||||
func (u AssignableUser) Login() string {
|
||||
return u.login
|
||||
}
|
||||
|
||||
func (u AssignableUser) Name() string {
|
||||
return u.name
|
||||
}
|
||||
|
||||
func (u AssignableUser) sealedAssignableActor() {}
|
||||
|
||||
type AssignableBot struct {
|
||||
id string
|
||||
login string
|
||||
}
|
||||
|
||||
func NewAssignableBot(id, login string) AssignableBot {
|
||||
return AssignableBot{
|
||||
id: id,
|
||||
login: login,
|
||||
}
|
||||
}
|
||||
|
||||
func (b AssignableBot) DisplayName() string {
|
||||
if b.login == CopilotActorLogin {
|
||||
return "Copilot (AI)"
|
||||
}
|
||||
return b.Login()
|
||||
}
|
||||
|
||||
func (b AssignableBot) ID() string {
|
||||
return b.id
|
||||
}
|
||||
|
||||
func (b AssignableBot) Login() string {
|
||||
return b.login
|
||||
}
|
||||
|
||||
func (b AssignableBot) Name() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b AssignableBot) sealedAssignableActor() {}
|
||||
|
||||
// RepoAssignableUsers fetches all the assignable users for a repository
|
||||
func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]RepoAssignee, error) {
|
||||
func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]AssignableUser, error) {
|
||||
type responseData struct {
|
||||
Repository struct {
|
||||
AssignableUsers struct {
|
||||
Nodes []RepoAssignee
|
||||
Nodes []struct {
|
||||
ID string
|
||||
Login string
|
||||
Name string
|
||||
}
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
|
|
@ -1120,7 +1293,7 @@ func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]RepoAssignee,
|
|||
"endCursor": (*githubv4.String)(nil),
|
||||
}
|
||||
|
||||
var users []RepoAssignee
|
||||
var users []AssignableUser
|
||||
for {
|
||||
var query responseData
|
||||
err := client.Query(repo.RepoHost(), "RepositoryAssignableUsers", &query, variables)
|
||||
|
|
@ -1128,7 +1301,15 @@ func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]RepoAssignee,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
users = append(users, query.Repository.AssignableUsers.Nodes...)
|
||||
for _, node := range query.Repository.AssignableUsers.Nodes {
|
||||
user := AssignableUser{
|
||||
id: node.ID,
|
||||
login: node.Login,
|
||||
name: node.Name,
|
||||
}
|
||||
|
||||
users = append(users, user)
|
||||
}
|
||||
if !query.Repository.AssignableUsers.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
|
|
@ -1138,6 +1319,72 @@ func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]RepoAssignee,
|
|||
return users, nil
|
||||
}
|
||||
|
||||
// RepoAssignableActors fetches all the assignable actors for a repository on
|
||||
// GitHub hosts that support Actor assignees.
|
||||
func RepoAssignableActors(client *Client, repo ghrepo.Interface) ([]AssignableActor, error) {
|
||||
type responseData struct {
|
||||
Repository struct {
|
||||
SuggestedActors struct {
|
||||
Nodes []struct {
|
||||
User struct {
|
||||
ID string
|
||||
Login string
|
||||
Name string
|
||||
TypeName string `graphql:"__typename"`
|
||||
} `graphql:"... on User"`
|
||||
Bot struct {
|
||||
ID string
|
||||
Login string
|
||||
TypeName string `graphql:"__typename"`
|
||||
} `graphql:"... on Bot"`
|
||||
}
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
} `graphql:"suggestedActors(first: 100, after: $endCursor, capabilities: CAN_BE_ASSIGNED)"`
|
||||
} `graphql:"repository(owner: $owner, name: $name)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": githubv4.String(repo.RepoOwner()),
|
||||
"name": githubv4.String(repo.RepoName()),
|
||||
"endCursor": (*githubv4.String)(nil),
|
||||
}
|
||||
|
||||
var actors []AssignableActor
|
||||
for {
|
||||
var query responseData
|
||||
err := client.Query(repo.RepoHost(), "RepositoryAssignableActors", &query, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, node := range query.Repository.SuggestedActors.Nodes {
|
||||
if node.User.TypeName == "User" {
|
||||
actor := AssignableUser{
|
||||
id: node.User.ID,
|
||||
login: node.User.Login,
|
||||
name: node.User.Name,
|
||||
}
|
||||
actors = append(actors, actor)
|
||||
} else if node.Bot.TypeName == "Bot" {
|
||||
actor := AssignableBot{
|
||||
id: node.Bot.ID,
|
||||
login: node.Bot.Login,
|
||||
}
|
||||
actors = append(actors, actor)
|
||||
}
|
||||
}
|
||||
|
||||
if !query.Repository.SuggestedActors.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
variables["endCursor"] = githubv4.String(query.Repository.SuggestedActors.PageInfo.EndCursor)
|
||||
}
|
||||
return actors, nil
|
||||
}
|
||||
|
||||
type RepoLabel struct {
|
||||
ID string
|
||||
Name string
|
||||
|
|
@ -1237,26 +1484,12 @@ func RepoMilestones(client *Client, repo ghrepo.Interface, state string) ([]Repo
|
|||
return milestones, nil
|
||||
}
|
||||
|
||||
func ProjectNamesToPaths(client *Client, repo ghrepo.Interface, projectNames []string) ([]string, error) {
|
||||
projects, projectsV2, err := relevantProjects(client, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ProjectsToPaths(projects, projectsV2, projectNames)
|
||||
}
|
||||
|
||||
// RelevantProjects retrieves set of Projects and ProjectsV2 relevant to given repository:
|
||||
// v1Projects retrieves set of RepoProjects relevant to given repository:
|
||||
// - Projects for repository
|
||||
// - Projects for repository organization, if it belongs to one
|
||||
// - ProjectsV2 owned by current user
|
||||
// - ProjectsV2 linked to repository
|
||||
// - ProjectsV2 owned by repository organization, if it belongs to one
|
||||
func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []ProjectV2, error) {
|
||||
func v1Projects(client *Client, repo ghrepo.Interface) ([]RepoProject, error) {
|
||||
var repoProjects []RepoProject
|
||||
var orgProjects []RepoProject
|
||||
var userProjectsV2 []ProjectV2
|
||||
var repoProjectsV2 []ProjectV2
|
||||
var orgProjectsV2 []ProjectV2
|
||||
|
||||
g, _ := errgroup.WithContext(context.Background())
|
||||
|
||||
|
|
@ -1268,6 +1501,7 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P
|
|||
}
|
||||
return err
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
orgProjects, err = OrganizationProjects(client, repo)
|
||||
|
|
@ -1277,6 +1511,29 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P
|
|||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projects := make([]RepoProject, 0, len(repoProjects)+len(orgProjects))
|
||||
projects = append(projects, repoProjects...)
|
||||
projects = append(projects, orgProjects...)
|
||||
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
// v2Projects retrieves set of ProjectV2 relevant to given repository:
|
||||
// - ProjectsV2 owned by current user
|
||||
// - ProjectsV2 linked to repository
|
||||
// - ProjectsV2 owned by repository organization, if it belongs to one
|
||||
func v2Projects(client *Client, repo ghrepo.Interface) ([]ProjectV2, error) {
|
||||
var userProjectsV2 []ProjectV2
|
||||
var repoProjectsV2 []ProjectV2
|
||||
var orgProjectsV2 []ProjectV2
|
||||
|
||||
g, _ := errgroup.WithContext(context.Background())
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
userProjectsV2, err = CurrentUserProjectsV2(client, repo.RepoHost())
|
||||
|
|
@ -1286,6 +1543,7 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P
|
|||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
repoProjectsV2, err = RepoProjectsV2(client, repo)
|
||||
|
|
@ -1295,6 +1553,7 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P
|
|||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
orgProjectsV2, err = OrganizationProjectsV2(client, repo)
|
||||
|
|
@ -1308,13 +1567,9 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P
|
|||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projects := make([]RepoProject, 0, len(repoProjects)+len(orgProjects))
|
||||
projects = append(projects, repoProjects...)
|
||||
projects = append(projects, orgProjects...)
|
||||
|
||||
// ProjectV2 might appear across multiple queries so use a map to keep them deduplicated.
|
||||
m := make(map[string]ProjectV2, len(userProjectsV2)+len(repoProjectsV2)+len(orgProjectsV2))
|
||||
for _, p := range userProjectsV2 {
|
||||
|
|
@ -1331,7 +1586,7 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P
|
|||
projectsV2 = append(projectsV2, p)
|
||||
}
|
||||
|
||||
return projects, projectsV2, nil
|
||||
return projectsV2, nil
|
||||
}
|
||||
|
||||
func CreateRepoTransformToV4(apiClient *Client, hostname string, method string, path string, body io.Reader) (*Repository, error) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -44,7 +45,8 @@ func Test_RepoMetadata(t *testing.T) {
|
|||
Assignees: true,
|
||||
Reviewers: true,
|
||||
Labels: true,
|
||||
Projects: true,
|
||||
ProjectsV1: true,
|
||||
ProjectsV2: true,
|
||||
Milestones: true,
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +187,7 @@ func Test_RepoMetadata(t *testing.T) {
|
|||
|
||||
expectedProjectIDs := []string{"TRIAGEID", "ROADMAPID"}
|
||||
expectedProjectV2IDs := []string{"TRIAGEV2ID", "ROADMAPV2ID", "MONALISAV2ID"}
|
||||
projectIDs, projectV2IDs, err := result.ProjectsToIDs([]string{"triage", "roadmap", "triagev2", "roadmapv2", "monalisav2"})
|
||||
projectIDs, projectV2IDs, err := result.ProjectsTitlesToIDs([]string{"triage", "roadmap", "triagev2", "roadmapv2", "monalisav2"})
|
||||
if err != nil {
|
||||
t.Errorf("error resolving projects: %v", err)
|
||||
}
|
||||
|
|
@ -211,37 +213,16 @@ func Test_RepoMetadata(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_ProjectsToPaths(t *testing.T) {
|
||||
expectedProjectPaths := []string{"OWNER/REPO/PROJECT_NUMBER", "ORG/PROJECT_NUMBER", "OWNER/REPO/PROJECT_NUMBER_2"}
|
||||
projects := []RepoProject{
|
||||
{ID: "id1", Name: "My Project", ResourcePath: "/OWNER/REPO/projects/PROJECT_NUMBER"},
|
||||
{ID: "id2", Name: "Org Project", ResourcePath: "/orgs/ORG/projects/PROJECT_NUMBER"},
|
||||
{ID: "id3", Name: "Project", ResourcePath: "/orgs/ORG/projects/PROJECT_NUMBER_2"},
|
||||
}
|
||||
projectsV2 := []ProjectV2{
|
||||
{ID: "id4", Title: "My Project V2", ResourcePath: "/OWNER/REPO/projects/PROJECT_NUMBER_2"},
|
||||
{ID: "id5", Title: "Org Project V2", ResourcePath: "/orgs/ORG/projects/PROJECT_NUMBER_3"},
|
||||
}
|
||||
projectNames := []string{"My Project", "Org Project", "My Project V2"}
|
||||
|
||||
projectPaths, err := ProjectsToPaths(projects, projectsV2, projectNames)
|
||||
if err != nil {
|
||||
t.Errorf("error resolving projects: %v", err)
|
||||
}
|
||||
if !sliceEqual(projectPaths, expectedProjectPaths) {
|
||||
t.Errorf("expected projects %v, got %v", expectedProjectPaths, projectPaths)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ProjectNamesToPaths(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
client := newTestClient(http)
|
||||
t.Run("when projectsV1 is supported, requests them", func(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
client := newTestClient(http)
|
||||
|
||||
repo, _ := ghrepo.FromFullName("OWNER/REPO")
|
||||
repo, _ := ghrepo.FromFullName("OWNER/REPO")
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryProjectList\b`),
|
||||
httpmock.StringResponse(`
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryProjectList\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "projects": {
|
||||
"nodes": [
|
||||
{ "name": "Cleanup", "id": "CLEANUPID", "resourcePath": "/OWNER/REPO/projects/1" },
|
||||
|
|
@ -250,9 +231,9 @@ func Test_ProjectNamesToPaths(t *testing.T) {
|
|||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query OrganizationProjectList\b`),
|
||||
httpmock.StringResponse(`
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query OrganizationProjectList\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "organization": { "projects": {
|
||||
"nodes": [
|
||||
{ "name": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/1" }
|
||||
|
|
@ -260,9 +241,9 @@ func Test_ProjectNamesToPaths(t *testing.T) {
|
|||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryProjectV2List\b`),
|
||||
httpmock.StringResponse(`
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryProjectV2List\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "projectsV2": {
|
||||
"nodes": [
|
||||
{ "title": "CleanupV2", "id": "CLEANUPV2ID", "resourcePath": "/OWNER/REPO/projects/3" },
|
||||
|
|
@ -271,9 +252,9 @@ func Test_ProjectNamesToPaths(t *testing.T) {
|
|||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query OrganizationProjectV2List\b`),
|
||||
httpmock.StringResponse(`
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query OrganizationProjectV2List\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "organization": { "projectsV2": {
|
||||
"nodes": [
|
||||
{ "title": "TriageV2", "id": "TRIAGEV2ID", "resourcePath": "/orgs/ORG/projects/2" }
|
||||
|
|
@ -281,9 +262,9 @@ func Test_ProjectNamesToPaths(t *testing.T) {
|
|||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query UserProjectV2List\b`),
|
||||
httpmock.StringResponse(`
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query UserProjectV2List\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "viewer": { "projectsV2": {
|
||||
"nodes": [
|
||||
{ "title": "MonalisaV2", "id": "MONALISAV2ID", "resourcePath": "/users/MONALISA/projects/5" }
|
||||
|
|
@ -292,15 +273,110 @@ func Test_ProjectNamesToPaths(t *testing.T) {
|
|||
} } } }
|
||||
`))
|
||||
|
||||
projectPaths, err := ProjectNamesToPaths(client, repo, []string{"Triage", "Roadmap", "TriageV2", "RoadmapV2", "MonalisaV2"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
projectPaths, err := ProjectTitlesToPaths(client, repo, []string{"Triage", "Roadmap", "TriageV2", "RoadmapV2", "MonalisaV2"}, gh.ProjectsV1Supported)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
expectedProjectPaths := []string{"ORG/1", "OWNER/REPO/2", "ORG/2", "OWNER/REPO/4", "MONALISA/5"}
|
||||
if !sliceEqual(projectPaths, expectedProjectPaths) {
|
||||
t.Errorf("expected projects paths %v, got %v", expectedProjectPaths, projectPaths)
|
||||
}
|
||||
expectedProjectPaths := []string{"ORG/1", "OWNER/REPO/2", "ORG/2", "OWNER/REPO/4", "MONALISA/5"}
|
||||
if !sliceEqual(projectPaths, expectedProjectPaths) {
|
||||
t.Errorf("expected projects paths %v, got %v", expectedProjectPaths, projectPaths)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("when projectsV1 is not supported, does not request them", func(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
client := newTestClient(http)
|
||||
|
||||
repo, _ := ghrepo.FromFullName("OWNER/REPO")
|
||||
|
||||
http.Exclude(
|
||||
t,
|
||||
httpmock.GraphQL(`query RepositoryProjectList\b`),
|
||||
)
|
||||
http.Exclude(
|
||||
t,
|
||||
httpmock.GraphQL(`query OrganizationProjectList\b`),
|
||||
)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryProjectV2List\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "projectsV2": {
|
||||
"nodes": [
|
||||
{ "title": "CleanupV2", "id": "CLEANUPV2ID", "resourcePath": "/OWNER/REPO/projects/3" },
|
||||
{ "title": "RoadmapV2", "id": "ROADMAPV2ID", "resourcePath": "/OWNER/REPO/projects/4" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query OrganizationProjectV2List\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "organization": { "projectsV2": {
|
||||
"nodes": [
|
||||
{ "title": "TriageV2", "id": "TRIAGEV2ID", "resourcePath": "/orgs/ORG/projects/2" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query UserProjectV2List\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "viewer": { "projectsV2": {
|
||||
"nodes": [
|
||||
{ "title": "MonalisaV2", "id": "MONALISAV2ID", "resourcePath": "/users/MONALISA/projects/5" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
|
||||
projectPaths, err := ProjectTitlesToPaths(client, repo, []string{"TriageV2", "RoadmapV2", "MonalisaV2"}, gh.ProjectsV1Unsupported)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
expectedProjectPaths := []string{"ORG/2", "OWNER/REPO/4", "MONALISA/5"}
|
||||
if !sliceEqual(projectPaths, expectedProjectPaths) {
|
||||
t.Errorf("expected projects paths %v, got %v", expectedProjectPaths, projectPaths)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("when a project is not found, returns an error", func(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
client := newTestClient(http)
|
||||
|
||||
repo, _ := ghrepo.FromFullName("OWNER/REPO")
|
||||
|
||||
// No projects found
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryProjectV2List\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "projectsV2": {
|
||||
"nodes": [],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query OrganizationProjectV2List\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "organization": { "projectsV2": {
|
||||
"nodes": [],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query UserProjectV2List\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "viewer": { "projectsV2": {
|
||||
"nodes": [],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
|
||||
_, err := ProjectTitlesToPaths(client, repo, []string{"TriageV2"}, gh.ProjectsV1Unsupported)
|
||||
require.Equal(t, err, fmt.Errorf("'TriageV2' not found"))
|
||||
})
|
||||
}
|
||||
|
||||
func Test_RepoResolveMetadataIDs(t *testing.T) {
|
||||
|
|
@ -385,6 +461,78 @@ t001: team(slug:"robots"){id,slug}
|
|||
}
|
||||
}
|
||||
|
||||
func TestMembersToIDs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("finds ids in assignable users", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
repoMetadataResult := RepoMetadataResult{
|
||||
AssignableUsers: []AssignableUser{
|
||||
NewAssignableUser("MONAID", "monalisa", ""),
|
||||
NewAssignableUser("MONAID2", "monalisa2", ""),
|
||||
},
|
||||
AssignableActors: []AssignableActor{
|
||||
NewAssignableBot("HUBOTID", "hubot"),
|
||||
},
|
||||
}
|
||||
ids, err := repoMetadataResult.MembersToIDs([]string{"monalisa"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"MONAID"}, ids)
|
||||
})
|
||||
|
||||
t.Run("finds ids by assignable actor logins", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
repoMetadataResult := RepoMetadataResult{
|
||||
AssignableActors: []AssignableActor{
|
||||
NewAssignableBot("HUBOTID", "hubot"),
|
||||
NewAssignableUser("MONAID", "monalisa", ""),
|
||||
},
|
||||
}
|
||||
ids, err := repoMetadataResult.MembersToIDs([]string{"monalisa"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"MONAID"}, ids)
|
||||
})
|
||||
|
||||
t.Run("finds ids by assignable actor display names", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
repoMetadataResult := RepoMetadataResult{
|
||||
AssignableActors: []AssignableActor{
|
||||
NewAssignableUser("MONAID", "monalisa", "mona"),
|
||||
},
|
||||
}
|
||||
ids, err := repoMetadataResult.MembersToIDs([]string{"monalisa (mona)"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"MONAID"}, ids)
|
||||
})
|
||||
|
||||
t.Run("when a name appears in both assignable users and actors, the id is only returned once", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
repoMetadataResult := RepoMetadataResult{
|
||||
AssignableUsers: []AssignableUser{
|
||||
NewAssignableUser("MONAID", "monalisa", ""),
|
||||
},
|
||||
AssignableActors: []AssignableActor{
|
||||
NewAssignableUser("MONAID", "monalisa", ""),
|
||||
},
|
||||
}
|
||||
ids, err := repoMetadataResult.MembersToIDs([]string{"monalisa"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"MONAID"}, ids)
|
||||
})
|
||||
|
||||
t.Run("when id is not found, returns an error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
repoMetadataResult := RepoMetadataResult{}
|
||||
_, err := repoMetadataResult.MembersToIDs([]string{"monalisa"})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func sliceEqual(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
|
|
@ -450,17 +598,17 @@ func Test_RepoMilestones(t *testing.T) {
|
|||
func TestDisplayName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
assignee RepoAssignee
|
||||
assignee AssignableUser
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "assignee with name",
|
||||
assignee: RepoAssignee{"123", "octocat123", "Octavious Cath"},
|
||||
assignee: AssignableUser{"123", "octocat123", "Octavious Cath"},
|
||||
want: "octocat123 (Octavious Cath)",
|
||||
},
|
||||
{
|
||||
name: "assignee without name",
|
||||
assignee: RepoAssignee{"123", "octocat123", ""},
|
||||
assignee: AssignableUser{"123", "octocat123", ""},
|
||||
want: "octocat123",
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,25 @@ func shortenQuery(q string) string {
|
|||
return strings.Map(squeeze, q)
|
||||
}
|
||||
|
||||
var assignedActors = shortenQuery(`
|
||||
assignedActors(first: 10) {
|
||||
nodes {
|
||||
...on User {
|
||||
id,
|
||||
login,
|
||||
name,
|
||||
__typename
|
||||
}
|
||||
...on Bot {
|
||||
id,
|
||||
login,
|
||||
__typename
|
||||
}
|
||||
},
|
||||
totalCount
|
||||
}
|
||||
`)
|
||||
|
||||
var issueComments = shortenQuery(`
|
||||
comments(first: 100) {
|
||||
nodes {
|
||||
|
|
@ -56,6 +75,25 @@ var issueCommentLast = shortenQuery(`
|
|||
}
|
||||
`)
|
||||
|
||||
var issueClosedByPullRequestsReferences = shortenQuery(`
|
||||
closedByPullRequestsReferences(first: 100) {
|
||||
nodes {
|
||||
id,
|
||||
number,
|
||||
url,
|
||||
repository {
|
||||
id,
|
||||
name,
|
||||
owner {
|
||||
id,
|
||||
login
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo{hasNextPage,endCursor}
|
||||
}
|
||||
`)
|
||||
|
||||
var prReviewRequests = shortenQuery(`
|
||||
reviewRequests(first: 100) {
|
||||
nodes {
|
||||
|
|
@ -132,6 +170,25 @@ var prCommits = shortenQuery(`
|
|||
}
|
||||
`)
|
||||
|
||||
var prClosingIssuesReferences = shortenQuery(`
|
||||
closingIssuesReferences(first: 100) {
|
||||
nodes {
|
||||
id,
|
||||
number,
|
||||
url,
|
||||
repository {
|
||||
id,
|
||||
name,
|
||||
owner {
|
||||
id,
|
||||
login
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo{hasNextPage,endCursor}
|
||||
}
|
||||
`)
|
||||
|
||||
var autoMergeRequest = shortenQuery(`
|
||||
autoMergeRequest {
|
||||
authorEmail,
|
||||
|
|
@ -277,6 +334,7 @@ var sharedIssuePRFields = []string{
|
|||
var issueOnlyFields = []string{
|
||||
"isPinned",
|
||||
"stateReason",
|
||||
"closedByPullRequestsReferences",
|
||||
}
|
||||
|
||||
var IssueFields = append(sharedIssuePRFields, issueOnlyFields...)
|
||||
|
|
@ -287,6 +345,7 @@ var PullRequestFields = append(sharedIssuePRFields,
|
|||
"baseRefName",
|
||||
"baseRefOid",
|
||||
"changedFiles",
|
||||
"closingIssuesReferences",
|
||||
"commits",
|
||||
"deletions",
|
||||
"files",
|
||||
|
|
@ -326,6 +385,8 @@ func IssueGraphQL(fields []string) string {
|
|||
q = append(q, `headRepository{id,name}`)
|
||||
case "assignees":
|
||||
q = append(q, `assignees(first:100){nodes{id,login,name},totalCount}`)
|
||||
case "assignedActors":
|
||||
q = append(q, assignedActors)
|
||||
case "labels":
|
||||
q = append(q, `labels(first:100){nodes{id,name,description,color},totalCount}`)
|
||||
case "projectCards":
|
||||
|
|
@ -366,6 +427,10 @@ func IssueGraphQL(fields []string) string {
|
|||
q = append(q, StatusCheckRollupGraphQLWithoutCountByState(""))
|
||||
case "statusCheckRollupWithCountByState": // pseudo-field
|
||||
q = append(q, StatusCheckRollupGraphQLWithCountByState())
|
||||
case "closingIssuesReferences":
|
||||
q = append(q, prClosingIssuesReferences)
|
||||
case "closedByPullRequestsReferences":
|
||||
q = append(q, issueClosedByPullRequestsReferences)
|
||||
default:
|
||||
q = append(q, field)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ Install:
|
|||
&& out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
||||
&& cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
|
||||
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||
&& sudo mkdir -p -m 755 /etc/apt/sources.list.d \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
||||
&& sudo apt update \
|
||||
&& sudo apt install gh -y
|
||||
|
|
@ -163,6 +164,20 @@ Or via [pkg(8)](https://www.freebsd.org/cgi/man.cgi?pkg(8)):
|
|||
pkg install gh
|
||||
```
|
||||
|
||||
### MidnightBSD
|
||||
|
||||
MidnightBSD users can install from [mports](https://www.midnightbsd.org/documentation/mports/index.html)
|
||||
|
||||
```bash
|
||||
cd /usr/mports/devel/gh/ && make install clean
|
||||
```
|
||||
|
||||
Or via [mport(1)](http://man.midnightbsd.org/cgi-bin/man.cgi/mport):
|
||||
|
||||
```bash
|
||||
mport install gh
|
||||
```
|
||||
|
||||
### NetBSD/pkgsrc
|
||||
|
||||
NetBSD users and those on [platforms supported by pkgsrc](https://pkgsrc.org/#index4h1) can install the [gh package](https://pkgsrc.se/net/gh):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Installation from source
|
||||
|
||||
1. Verify that you have Go 1.23+ installed
|
||||
1. Verify that you have Go 1.24+ installed
|
||||
|
||||
```sh
|
||||
$ go version
|
||||
|
|
|
|||
|
|
@ -518,15 +518,56 @@ func (r RemoteTrackingRef) String() string {
|
|||
|
||||
// ParseRemoteTrackingRef parses a string of the form "refs/remotes/<remote>/<branch>" into
|
||||
// a RemoteTrackingBranch struct. If the string does not match this format, an error is returned.
|
||||
//
|
||||
// For now, we assume that refnames are of the format "<remote>/<branch>", where
|
||||
// the remote is a single path component, and branch may have many path components e.g.
|
||||
// "origin/my/branch" is valid as: {Remote: "origin", Branch: "my/branch"}
|
||||
// but "my/origin/branch" would parse incorrectly as: {Remote: "my", Branch: "origin/branch"}
|
||||
// I don't believe there is a way to fix this without providing the list of remotes to this function.
|
||||
//
|
||||
// It becomes particularly confusing if you have something like:
|
||||
//
|
||||
// ```
|
||||
// [remote "foo"]
|
||||
// url = https://github.com/williammartin/test-repo.git
|
||||
// fetch = +refs/heads/*:refs/remotes/foo/*
|
||||
// [remote "foo/bar"]
|
||||
// url = https://github.com/williammartin/test-repo.git
|
||||
// fetch = +refs/heads/*:refs/remotes/foo/bar/*
|
||||
// [branch "bar/baz"]
|
||||
// remote = foo
|
||||
// merge = refs/heads/bar/baz
|
||||
// [branch "baz"]
|
||||
// remote = foo/bar
|
||||
// merge = refs/heads/baz
|
||||
// ```
|
||||
//
|
||||
// These @{push} refs would resolve identically:
|
||||
//
|
||||
// ```
|
||||
// ➜ git rev-parse --symbolic-full-name baz@{push}
|
||||
// refs/remotes/foo/bar/baz
|
||||
|
||||
// ➜ git rev-parse --symbolic-full-name bar/baz@{push}
|
||||
// refs/remotes/foo/bar/baz
|
||||
// ```
|
||||
//
|
||||
// When using this ref, git assumes it means `remote: foo` `branch: bar/baz`.
|
||||
func ParseRemoteTrackingRef(s string) (RemoteTrackingRef, error) {
|
||||
parts := strings.Split(s, "/")
|
||||
if len(parts) != 4 || parts[0] != "refs" || parts[1] != "remotes" {
|
||||
prefix := "refs/remotes/"
|
||||
if !strings.HasPrefix(s, prefix) {
|
||||
return RemoteTrackingRef{}, fmt.Errorf("remote tracking branch must have format refs/remotes/<remote>/<branch> but was: %s", s)
|
||||
}
|
||||
|
||||
refName := strings.TrimPrefix(s, prefix)
|
||||
refNameParts := strings.SplitN(refName, "/", 2)
|
||||
if len(refNameParts) != 2 {
|
||||
return RemoteTrackingRef{}, fmt.Errorf("remote tracking branch must have format refs/remotes/<remote>/<branch> but was: %s", s)
|
||||
}
|
||||
|
||||
return RemoteTrackingRef{
|
||||
Remote: parts[2],
|
||||
Branch: parts[3],
|
||||
Remote: refNameParts[0],
|
||||
Branch: refNameParts[1],
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1151,13 +1151,38 @@ func TestRemoteTrackingRef(t *testing.T) {
|
|||
wantError error
|
||||
}{
|
||||
{
|
||||
name: "valid remote tracking ref",
|
||||
name: "valid remote tracking ref without slash in branch name",
|
||||
remoteTrackingRef: "refs/remotes/origin/branchName",
|
||||
wantRemoteTrackingRef: RemoteTrackingRef{
|
||||
Remote: "origin",
|
||||
Branch: "branchName",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid remote tracking ref with slash in branch name",
|
||||
remoteTrackingRef: "refs/remotes/origin/branch/name",
|
||||
wantRemoteTrackingRef: RemoteTrackingRef{
|
||||
Remote: "origin",
|
||||
Branch: "branch/name",
|
||||
},
|
||||
},
|
||||
// TODO: Uncomment when we support slashes in remote names
|
||||
// {
|
||||
// name: "valid remote tracking ref with slash in remote name",
|
||||
// remoteTrackingRef: "refs/remotes/my/origin/branchName",
|
||||
// wantRemoteTrackingRef: RemoteTrackingRef{
|
||||
// Remote: "my/origin",
|
||||
// Branch: "branchName",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: "valid remote tracking ref with slash in remote name and branch name",
|
||||
// remoteTrackingRef: "refs/remotes/my/origin/branch/name",
|
||||
// wantRemoteTrackingRef: RemoteTrackingRef{
|
||||
// Remote: "my/origin",
|
||||
// Branch: "branch/name",
|
||||
// },
|
||||
// },
|
||||
{
|
||||
name: "incorrect parts",
|
||||
remoteTrackingRef: "refs/remotes/origin",
|
||||
|
|
|
|||
75
go.mod
75
go.mod
|
|
@ -1,8 +1,8 @@
|
|||
module github.com/cli/cli/v2
|
||||
|
||||
go 1.23.0
|
||||
go 1.24
|
||||
|
||||
toolchain go1.23.5
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
|
|
@ -10,29 +10,30 @@ require (
|
|||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2
|
||||
github.com/briandowns/spinner v1.18.1
|
||||
github.com/cenkalti/backoff/v4 v4.3.0
|
||||
github.com/cenkalti/backoff/v5 v5.0.2
|
||||
github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3
|
||||
github.com/charmbracelet/huh v0.6.1-0.20250409210615-c5906631cbb5
|
||||
github.com/charmbracelet/huh v0.7.0
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc
|
||||
github.com/cli/go-gh/v2 v2.12.0
|
||||
github.com/cli/go-gh/v2 v2.12.1
|
||||
github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24
|
||||
github.com/cli/oauth v1.1.1
|
||||
github.com/cli/safeexec v1.0.1
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.8
|
||||
github.com/gabriel-vasile/mimetype v1.4.9
|
||||
github.com/gdamore/tcell/v2 v2.5.4
|
||||
github.com/golang/snappy v0.0.4
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/go-containerregistry v0.20.3
|
||||
github.com/google/go-containerregistry v0.20.6
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/go-version v1.3.0
|
||||
github.com/henvic/httpretty v0.1.4
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec
|
||||
github.com/in-toto/attestation v1.1.1
|
||||
github.com/in-toto/attestation v1.1.2
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/mattn/go-colorable v0.1.14
|
||||
|
|
@ -43,17 +44,19 @@ 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.4.1
|
||||
github.com/sigstore/sigstore-go v0.7.2
|
||||
github.com/sigstore/protobuf-specs v0.4.3
|
||||
github.com/sigstore/sigstore-go v1.0.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/theupdateframework/go-tuf/v2 v2.1.1
|
||||
github.com/yuin/goldmark v1.7.12
|
||||
github.com/zalando/go-keyring v0.2.5
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/sync v0.13.0
|
||||
golang.org/x/term v0.31.0
|
||||
golang.org/x/text v0.24.0
|
||||
google.golang.org/grpc v1.71.1
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/sync v0.15.0
|
||||
golang.org/x/term v0.32.0
|
||||
golang.org/x/text v0.26.0
|
||||
google.golang.org/grpc v1.72.2
|
||||
google.golang.org/protobuf v1.36.6
|
||||
gopkg.in/h2non/gock.v1 v1.1.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
|
|
@ -83,13 +86,13 @@ require (
|
|||
github.com/cli/shurcooL-graphql v0.0.4 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||
github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 // indirect
|
||||
github.com/danieljoos/wincred v1.2.1 // indirect
|
||||
github.com/danieljoos/wincred v1.2.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||
github.com/docker/cli v27.5.0+incompatible // indirect
|
||||
github.com/docker/cli v28.2.2+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
|
|
@ -97,7 +100,7 @@ require (
|
|||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/analysis v0.23.0 // indirect
|
||||
github.com/go-openapi/errors v0.22.1 // indirect
|
||||
|
|
@ -123,7 +126,7 @@ require (
|
|||
github.com/itchyny/timefmt-go v0.1.5 // indirect
|
||||
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
|
|
@ -141,7 +144,7 @@ require (
|
|||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
|
|
@ -154,9 +157,9 @@ require (
|
|||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
|
||||
github.com/sigstore/rekor v1.3.9 // indirect
|
||||
github.com/sigstore/sigstore v1.9.1 // indirect
|
||||
github.com/sigstore/timestamp-authority v1.2.5 // indirect
|
||||
github.com/sigstore/rekor v1.3.10 // indirect
|
||||
github.com/sigstore/sigstore v1.9.4 // indirect
|
||||
github.com/sigstore/timestamp-authority v1.2.7 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
|
|
@ -165,27 +168,25 @@ require (
|
|||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/theupdateframework/go-tuf v0.7.0 // indirect
|
||||
github.com/theupdateframework/go-tuf/v2 v2.0.2 // indirect
|
||||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect
|
||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
|
||||
github.com/transparency-dev/merkle v0.0.2 // indirect
|
||||
github.com/vbatts/tar-split v0.11.6 // indirect
|
||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
)
|
||||
|
|
|
|||
269
go.sum
269
go.sum
|
|
@ -1,17 +1,17 @@
|
|||
cloud.google.com/go v0.118.3 h1:jsypSnrE/w4mJysioGdMBg4MiW/hHx/sArFpaBWHdME=
|
||||
cloud.google.com/go v0.118.3/go.mod h1:Lhs3YLnBlwJ4KA6nuObNMZ/fCbOQBPuWKPoE0Wa/9Vc=
|
||||
cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps=
|
||||
cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
|
||||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
cloud.google.com/go/iam v1.4.1 h1:cFC25Nv+u5BkTR/BT1tXdoF2daiVbZ1RLx2eqfQ9RMM=
|
||||
cloud.google.com/go/iam v1.4.1/go.mod h1:2vUEJpUG3Q9p2UdsyksaKpDzlwOrnMzS30isdReIcLM=
|
||||
cloud.google.com/go/kms v1.21.1 h1:r1Auo+jlfJSf8B7mUnVw5K0fI7jWyoUy65bV53VjKyk=
|
||||
cloud.google.com/go/kms v1.21.1/go.mod h1:s0wCyByc9LjTdCjG88toVs70U9W+cc6RKFc8zAqX7nE=
|
||||
cloud.google.com/go/longrunning v0.6.5 h1:sD+t8DO8j4HKW4QfouCklg7ZC1qC4uzVZt8iz3uTW+Q=
|
||||
cloud.google.com/go/longrunning v0.6.5/go.mod h1:Et04XK+0TTLKa5IPYryKf5DkpwImy6TluQ1QTLwlKmY=
|
||||
cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
|
||||
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
|
||||
cloud.google.com/go/auth v0.16.0 h1:Pd8P1s9WkcrBE2n/PhAwKsdrR35V3Sg2II9B+ndM3CU=
|
||||
cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
|
||||
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
|
||||
cloud.google.com/go/iam v1.5.0 h1:QlLcVMhbLGOjRcGe6VTGGTyQib8dRLK2B/kYNV0+2xs=
|
||||
cloud.google.com/go/iam v1.5.0/go.mod h1:U+DOtKQltF/LxPEtcDLoobcsZMilSRwR7mgNL7knOpo=
|
||||
cloud.google.com/go/kms v1.21.2 h1:c/PRUSMNQ8zXrc1sdAUnsenWWaNXN+PzTXfXOcSFdoE=
|
||||
cloud.google.com/go/kms v1.21.2/go.mod h1:8wkMtHV/9Z8mLXEXr1GK7xPSBdi6knuLXIhqjuWcI6w=
|
||||
cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw=
|
||||
cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
|
|
@ -20,18 +20,18 @@ github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjq
|
|||
github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
|
|
@ -58,10 +58,10 @@ github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk
|
|||
github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.10 h1:yNjgjiGBp4GgaJrGythyBXg2wAs+Im9fSWIUwvi1CAc=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.10/go.mod h1:A0mbLXSdtob/2t59n1X0iMkPQ5d+YzYZB4rwu7SZ7aA=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.63 h1:rv1V3kIJ14pdmTu01hwcMJ0WAERensSiD9rEWEBb1Tk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.63/go.mod h1:EJj+yDf0txT26Ulo0VWTavBl31hOsaeuMxIHu2m0suY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
|
||||
|
|
@ -74,14 +74,14 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b
|
|||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.38.1 h1:tecq7+mAav5byF+Mr+iONJnCBf4B4gon8RSp4BrweSc=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.38.1/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2 h1:wK8O+j2dOolmpNVY1EWIbLgxrGCHJKVPm08Hv/u80M8=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.38.3 h1:RivOtUH3eEu6SWnUMFHKAW4MqDOzWn1vGQ3S38Y5QMg=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.38.3/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
|
||||
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
|
||||
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
|
|
@ -100,6 +100,8 @@ github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
|||
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||
|
|
@ -110,25 +112,33 @@ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4p
|
|||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3 h1:hx6E25SvI2WiZdt/gxINcYBnHD7PE2Vr9auqwg5B05g=
|
||||
github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3/go.mod h1:ihVqv4/YOY5Fweu1cxajuQrwJFh3zU4Ukb4mHVNjq3s=
|
||||
github.com/charmbracelet/huh v0.6.1-0.20250409210615-c5906631cbb5 h1:uOnMxWghHfEYm2DPMeIHHAEirV/TduBVC9ZRXGcX9Q8=
|
||||
github.com/charmbracelet/huh v0.6.1-0.20250409210615-c5906631cbb5/go.mod h1:xl27E/xNaX3WwdkqpvBwjJcGWhupkU52CWLC5hReBTw=
|
||||
github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc=
|
||||
github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk=
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc h1:nFRtCfZu/zkltd2lsLUPlVNv3ej/Atod9hcdbRZtlys=
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
|
||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
|
||||
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
|
||||
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
|
||||
github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q=
|
||||
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
|
||||
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
|
||||
github.com/cli/go-gh/v2 v2.12.0 h1:PIurZ13fXbWDbr2//6ws4g4zDbryO+iDuTpiHgiV+6k=
|
||||
github.com/cli/go-gh/v2 v2.12.0/go.mod h1:+5aXmEOJsH9fc9mBHfincDwnS02j2AIA/DsTH0Bk5uw=
|
||||
github.com/cli/go-gh/v2 v2.12.1 h1:SVt1/afj5FRAythyMV3WJKaUfDNsxXTIe7arZbwTWKA=
|
||||
github.com/cli/go-gh/v2 v2.12.1/go.mod h1:+5aXmEOJsH9fc9mBHfincDwnS02j2AIA/DsTH0Bk5uw=
|
||||
github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24 h1:QDrhR4JA2n3ij9YQN0u5ZeuvRIIvsUGmf5yPlTS0w8E=
|
||||
github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24/go.mod h1:rr9GNING0onuVw8MnracQHn7PcchnFlP882Y0II2KZk=
|
||||
github.com/cli/oauth v1.1.1 h1:459gD3hSjlKX9B1uXBuiAMdpXBUQ9QGf/NDcCpoQxPs=
|
||||
|
|
@ -142,15 +152,16 @@ github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUo
|
|||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 h1:vU+EP9ZuFUCYE0NYLwTSob+3LNEJATzNfP/DC7SWGWI=
|
||||
github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
|
||||
github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs=
|
||||
github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps=
|
||||
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
|
||||
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
|
|
@ -164,12 +175,12 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
|||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM=
|
||||
github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A=
|
||||
github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
|
||||
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
|
|
@ -183,8 +194,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.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
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=
|
||||
|
|
@ -194,8 +205,8 @@ github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxm
|
|||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
|
||||
|
|
@ -218,8 +229,8 @@ github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZ
|
|||
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
|
||||
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
|
||||
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
|
|
@ -236,16 +247,14 @@ github.com/google/certificate-transparency-go v1.3.1 h1:akbcTfQg0iZlANZLn0L9xOeW
|
|||
github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
|
||||
github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
|
||||
github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=
|
||||
github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w=
|
||||
github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM=
|
||||
github.com/google/trillian v1.7.1 h1:+zX8jLM3524bAMPS+VxaDIDgsMv3/ty6DuLWerHXcek=
|
||||
github.com/google/trillian v1.7.1/go.mod h1:E1UMAHqpZCA8AQdrKdWmHmtUfSeiD0sDWD1cv00Xa+c=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
|
|
@ -295,8 +304,8 @@ github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb
|
|||
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/in-toto/attestation v1.1.1 h1:QD3d+oATQ0dFsWoNh5oT0udQ3tUrOsZZ0Fc3tSgWbzI=
|
||||
github.com/in-toto/attestation v1.1.1/go.mod h1:Dcq1zVwA2V7Qin8I7rgOi+i837wEf/mOZwRm047Sjys=
|
||||
github.com/in-toto/attestation v1.1.2 h1:MBFn6lsMq6dptQZJBhalXTcWMb/aJy3V+GX3VYj/V1E=
|
||||
github.com/in-toto/attestation v1.1.2/go.mod h1:gYFddHMZj3DiQ0b62ltNi1Vj5rC879bTmBbrv9CRHpM=
|
||||
github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU=
|
||||
github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
|
|
@ -329,8 +338,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
|||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
|
|
@ -393,8 +402,8 @@ github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
|||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
|
|
@ -406,8 +415,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
|
|
@ -446,24 +455,24 @@ 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.4.1 h1:5SsMqZbdkcO/DNHudaxuCUEjj6x29tS2Xby1BxGU7Zc=
|
||||
github.com/sigstore/protobuf-specs v0.4.1/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc=
|
||||
github.com/sigstore/rekor v1.3.9 h1:sUjRpKVh/hhgqGMs0t+TubgYsksArZ6poLEC3MsGAzU=
|
||||
github.com/sigstore/rekor v1.3.9/go.mod h1:xThNUhm6eNEmkJ/SiU/FVU7pLY2f380fSDZFsdDWlcM=
|
||||
github.com/sigstore/sigstore v1.9.1 h1:bNMsfFATsMPaagcf+uppLk4C9rQZ2dh5ysmCxQBYWaw=
|
||||
github.com/sigstore/sigstore v1.9.1/go.mod h1:zUoATYzR1J3rLNp3jmp4fzIJtWdhC3ZM6MnpcBtnsE4=
|
||||
github.com/sigstore/sigstore-go v0.7.2 h1:CN4xPasChSEb0QBMxMW5dLcXdA9KD4QiRyVnMkhXj6U=
|
||||
github.com/sigstore/sigstore-go v0.7.2/go.mod h1:AIRj4I3LC82qd07VFm3T2zXYiddxeBV1k/eoS8nTz0E=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.1 h1:/YcNq687WnXpIRXl04nLfJX741G4iW+w+7Nem2Zy0f4=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.1/go.mod h1:ApL9RpKsi7gkSYN0bMNdm/3jZ9EefxMmfYHfUmq2ZYM=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.9.1 h1:FnusXyTIInnwfIOzzl5PFilRm1I97dxMSOcCkZBu9Kc=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.9.1/go.mod h1:d5m5LOa/69a+t2YC9pDPwS1n2i/PhqB4cUKbpVDlKKE=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.1 h1:LFiYK1DEWQ6Hf/nroFzBMM+s5rVSjVL45Alpb5Ctl5A=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.1/go.mod h1:GFyFmDsE2wDuIHZD+4+JErGpA0S4zJsKNz5l2JVJd8s=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.1 h1:sIW6xe4yU5eIMH8fve2C78d+r29KmHnIb+7po+80bsY=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.1/go.mod h1:3pNf99GnK9eu3XUa5ebHzgEQSVYf9hqAoPFwbwD6O6M=
|
||||
github.com/sigstore/timestamp-authority v1.2.5 h1:W22JmwRv1Salr/NFFuP7iJuhytcZszQjldoB8GiEdnw=
|
||||
github.com/sigstore/timestamp-authority v1.2.5/go.mod h1:gWPKWq4HMWgPCETre0AakgBzcr9DRqHrsgbrRqsigOs=
|
||||
github.com/sigstore/protobuf-specs v0.4.3 h1:kRgJ+ciznipH9xhrkAbAEHuuxD3GhYnGC873gZpjJT4=
|
||||
github.com/sigstore/protobuf-specs v0.4.3/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc=
|
||||
github.com/sigstore/rekor v1.3.10 h1:/mSvRo4MZ/59ECIlARhyykAlQlkmeAQpvBPlmJtZOCU=
|
||||
github.com/sigstore/rekor v1.3.10/go.mod h1:JvryKJ40O0XA48MdzYUPu0y4fyvqt0C4iSY7ri9iu3A=
|
||||
github.com/sigstore/sigstore v1.9.4 h1:64+OGed80+A4mRlNzRd055vFcgBeDghjZw24rPLZgDU=
|
||||
github.com/sigstore/sigstore v1.9.4/go.mod h1:Q7tGTC3gbtK7c3jcxEmGc2MmK4rRpIRzi3bxRFWKvEY=
|
||||
github.com/sigstore/sigstore-go v1.0.0 h1:4N07S2zLxf09nTRwaPKyAxbKzpM8WJYUS8lWWaYxneU=
|
||||
github.com/sigstore/sigstore-go v1.0.0/go.mod h1:UYsZ/XHE4eltv1o1Lu+n6poW1Z5to3f0+emvfXNxIN8=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.4 h1:kQqUJ1VuWdJltMkinFXAHTlJrzMRPoNgL+dy6WyJ/dA=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.4/go.mod h1:9miLz7c69vj/7VH7UpCKHDia41HCTIDJWJWf4Ex5yUk=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.9.4 h1:MHRm7YQuF4zFyoXRLgUdLaNxqVO6JlLGnkDUI9fm9ow=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.9.4/go.mod h1:899VNYSSnQ0QtcuhkW0gznzxn0cqhowTL3nzc/xnym8=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.4 h1:C2nSyTmTxpuamUmLCWWZwz+0Y1IQIig9XwAJ4UAn/SI=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.4/go.mod h1:vjDahU0sEw/WMkKkygZNH72EMg86iaFNLAaJFXhItXU=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.4 h1:t9yfb6yteIDv8CNRT6OHdqgTV6TSj+CdOtZP9dVhpsQ=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.4/go.mod h1:m7sQxVJmDa+rsmS1m6biQxaLX83pzNS7ThUEyjOqkCU=
|
||||
github.com/sigstore/timestamp-authority v1.2.7 h1:HP/VT4wnL4uzP0fVo3eHXlt0reuNgW3PLt78+BV0I5I=
|
||||
github.com/sigstore/timestamp-authority v1.2.7/go.mod h1:te4ThQ3Q/CX1bzVsf5mMN0K7Z/cgc2OcoEGxAJiFqqI=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
|
|
@ -490,28 +499,30 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
|
|||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
|
||||
github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug=
|
||||
github.com/theupdateframework/go-tuf/v2 v2.0.2 h1:PyNnjV9BJNzN1ZE6BcWK+5JbF+if370jjzO84SS+Ebo=
|
||||
github.com/theupdateframework/go-tuf/v2 v2.0.2/go.mod h1:baB22nBHeHBCeuGZcIlctNq4P61PcOdyARlplg5xmLA=
|
||||
github.com/theupdateframework/go-tuf/v2 v2.1.1 h1:OWcoHItwsGO+7m0wLa7FDWPR4oB1cj0zOr1kosE4G+I=
|
||||
github.com/theupdateframework/go-tuf/v2 v2.1.1/go.mod h1:V675cQGhZONR0OGQ8r1feO0uwtsTBYPDWHzAAPn5rjE=
|
||||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8=
|
||||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
|
||||
github.com/tink-crypto/tink-go-awskms/v2 v2.1.0 h1:N9UxlsOzu5mttdjhxkDLbzwtEecuXmlxZVo/ds7JKJI=
|
||||
github.com/tink-crypto/tink-go-awskms/v2 v2.1.0/go.mod h1:PxSp9GlOkKL9rlybW804uspnHuO9nbD98V/fDX4uSis=
|
||||
github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 h1:3B9i6XBXNTRspfkTC0asN5W0K6GhOSgcujNiECNRNb0=
|
||||
github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0/go.mod h1:jY5YN2BqD/KSCHM9SqZPIpJNG/u3zwfLXHgws4x2IRw=
|
||||
github.com/tink-crypto/tink-go/v2 v2.3.0 h1:4/TA0lw0lA/iVKBL9f8R5eP7397bfc4antAMXF5JRhs=
|
||||
github.com/tink-crypto/tink-go/v2 v2.3.0/go.mod h1:kfPOtXIadHlekBTeBtJrHWqoGL+Fm3JQg0wtltPuxLU=
|
||||
github.com/tink-crypto/tink-go-hcvault/v2 v2.3.0 h1:6nAX1aRGnkg2SEUMwO5toB2tQkP0Jd6cbmZ/K5Le1V0=
|
||||
github.com/tink-crypto/tink-go-hcvault/v2 v2.3.0/go.mod h1:HOC5NWW1wBI2Vke1FGcRBvDATkEYE7AUDiYbXqi2sBw=
|
||||
github.com/tink-crypto/tink-go/v2 v2.4.0 h1:8VPZeZI4EeZ8P/vB6SIkhlStrJfivTJn+cQ4dtyHNh0=
|
||||
github.com/tink-crypto/tink-go/v2 v2.4.0/go.mod h1:l//evrF2Y3MjdbpNDNGnKgCpo5zSmvUvnQ4MU+yE2sw=
|
||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
|
||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
|
||||
github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4=
|
||||
github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A=
|
||||
github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
|
||||
github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
|
||||
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
|
||||
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY=
|
||||
github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
||||
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||
github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8=
|
||||
|
|
@ -520,22 +531,22 @@ go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd
|
|||
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
go.step.sm/crypto v0.60.0 h1:UgSw8DFG5xUOGB3GUID17UA32G4j1iNQ4qoMhBmsVFw=
|
||||
go.step.sm/crypto v0.60.0/go.mod h1:Ep83Lv818L4gV0vhFTdPWRKnL6/5fRMpi8SaoP5ArSw=
|
||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||
go.step.sm/crypto v0.63.0 h1:U1QGELQqJ85oDfeNFE2V52cow1rvy0m3MekG3wFmyXY=
|
||||
go.step.sm/crypto v0.63.0/go.mod h1:aj3LETmCZeSil1DMq3BlbhDBcN86+mmKrHZtXWyc0L4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
|
|
@ -544,24 +555,24 @@ 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.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
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/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg=
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
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.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
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=
|
||||
|
|
@ -572,37 +583,37 @@ 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.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
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.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.227.0 h1:QvIHF9IuyG6d6ReE+BNd11kIB8hZvjN8Z5xY5t21zYc=
|
||||
google.golang.org/api v0.227.0/go.mod h1:EIpaG6MbTgQarWF5xJvX0eOJPK9n/5D4Bynb9j2HXvQ=
|
||||
google.golang.org/api v0.230.0 h1:2u1hni3E+UXAXrONrrkfWpi/V6cyKVAbfGVeGtC3OxM=
|
||||
google.golang.org/api v0.230.0/go.mod h1:aqvtoMk7YkiXx+6U12arQFExiRV9D/ekvMCwCd/TksQ=
|
||||
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE=
|
||||
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
|
||||
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
|
||||
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e h1:UdXH7Kzbj+Vzastr5nVfccbmFsmYNygVLSPk1pEfDoY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
|
||||
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
|||
|
|
@ -14,18 +14,23 @@ import (
|
|||
ghConfig "github.com/cli/go-gh/v2/pkg/config"
|
||||
)
|
||||
|
||||
// Important: some of the following configuration settings are used outside of `cli/cli`,
|
||||
// they are defined here to avoid `cli/cli` being changed unexpectedly.
|
||||
const (
|
||||
accessibleColorsKey = "accessible_colors" // used by cli/go-gh to enable the use of customizable, accessible 4-bit colors.
|
||||
accessiblePrompterKey = "accessible_prompter"
|
||||
aliasesKey = "aliases"
|
||||
browserKey = "browser"
|
||||
browserKey = "browser" // used by cli/go-gh to open URLs in web browsers
|
||||
colorLabelsKey = "color_labels"
|
||||
editorKey = "editor"
|
||||
editorKey = "editor" // used by cli/go-gh to open interactive text editor
|
||||
gitProtocolKey = "git_protocol"
|
||||
hostsKey = "hosts"
|
||||
hostsKey = "hosts" // used by cli/go-gh to locate authenticated host tokens
|
||||
httpUnixSocketKey = "http_unix_socket"
|
||||
oauthTokenKey = "oauth_token"
|
||||
oauthTokenKey = "oauth_token" // used by cli/go-gh to locate authenticated host tokens
|
||||
pagerKey = "pager"
|
||||
promptKey = "prompt"
|
||||
preferEditorPromptKey = "prefer_editor_prompt"
|
||||
spinnerKey = "spinner"
|
||||
userKey = "user"
|
||||
usersKey = "users"
|
||||
versionKey = "version"
|
||||
|
|
@ -109,6 +114,16 @@ func (c *cfg) Authentication() gh.AuthConfig {
|
|||
return &AuthConfig{cfg: c.cfg}
|
||||
}
|
||||
|
||||
func (c *cfg) AccessibleColors(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, accessibleColorsKey).Unwrap()
|
||||
}
|
||||
|
||||
func (c *cfg) AccessiblePrompter(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, accessiblePrompterKey).Unwrap()
|
||||
}
|
||||
|
||||
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()
|
||||
|
|
@ -149,6 +164,11 @@ func (c *cfg) PreferEditorPrompt(hostname string) gh.ConfigEntry {
|
|||
return c.GetOrDefault(hostname, preferEditorPromptKey).Unwrap()
|
||||
}
|
||||
|
||||
func (c *cfg) Spinner(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, spinnerKey).Unwrap()
|
||||
}
|
||||
|
||||
func (c *cfg) Version() o.Option[string] {
|
||||
return c.get("", versionKey)
|
||||
}
|
||||
|
|
@ -540,6 +560,12 @@ http_unix_socket:
|
|||
browser:
|
||||
# Whether to display labels using their RGB hex color codes in terminals that support truecolor. Supported values: enabled, disabled
|
||||
color_labels: disabled
|
||||
# Whether customizable, 4-bit accessible colors should be used. Supported values: enabled, disabled
|
||||
accessible_colors: disabled
|
||||
# Whether an accessible prompter should be used. Supported values: enabled, disabled
|
||||
accessible_prompter: disabled
|
||||
# Whether to use a animated spinner as a progress indicator. If disabled, a textual progress indicator is used instead. Supported values: enabled, disabled
|
||||
spinner: enabled
|
||||
`
|
||||
|
||||
type ConfigOption struct {
|
||||
|
|
@ -619,6 +645,33 @@ var Options = []ConfigOption{
|
|||
return c.ColorLabels(hostname).Value
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: accessibleColorsKey,
|
||||
Description: "whether customizable, 4-bit accessible colors should be used",
|
||||
DefaultValue: "disabled",
|
||||
AllowedValues: []string{"enabled", "disabled"},
|
||||
CurrentValue: func(c gh.Config, hostname string) string {
|
||||
return c.AccessibleColors(hostname).Value
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: accessiblePrompterKey,
|
||||
Description: "whether an accessible prompter should be used",
|
||||
DefaultValue: "disabled",
|
||||
AllowedValues: []string{"enabled", "disabled"},
|
||||
CurrentValue: func(c gh.Config, hostname string) string {
|
||||
return c.AccessiblePrompter(hostname).Value
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: spinnerKey,
|
||||
Description: "whether to use a animated spinner as a progress indicator",
|
||||
DefaultValue: "enabled",
|
||||
AllowedValues: []string{"enabled", "disabled"},
|
||||
CurrentValue: func(c gh.Config, hostname string) string {
|
||||
return c.Spinner(hostname).Value
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func HomeDirPath(subdir string) (string, error) {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,12 @@ func NewFromString(cfgStr string) *ghmock.ConfigMock {
|
|||
},
|
||||
}
|
||||
}
|
||||
mock.AccessibleColorsFunc = func(hostname string) gh.ConfigEntry {
|
||||
return cfg.AccessibleColors(hostname)
|
||||
}
|
||||
mock.AccessiblePrompterFunc = func(hostname string) gh.ConfigEntry {
|
||||
return cfg.AccessiblePrompter(hostname)
|
||||
}
|
||||
mock.BrowserFunc = func(hostname string) gh.ConfigEntry {
|
||||
return cfg.Browser(hostname)
|
||||
}
|
||||
|
|
@ -76,6 +82,9 @@ func NewFromString(cfgStr string) *ghmock.ConfigMock {
|
|||
mock.PreferEditorPromptFunc = func(hostname string) gh.ConfigEntry {
|
||||
return cfg.PreferEditorPrompt(hostname)
|
||||
}
|
||||
mock.SpinnerFunc = func(hostname string) gh.ConfigEntry {
|
||||
return cfg.Spinner(hostname)
|
||||
}
|
||||
mock.VersionFunc = func() o.Option[string] {
|
||||
return cfg.Version()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,8 +142,7 @@ func genMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string)
|
|||
fmt.Fprintf(w, "```\n%s\n```\n\n", cmd.UseLine())
|
||||
}
|
||||
if hasLong {
|
||||
longWithEscapedPipe := strings.ReplaceAll(cmd.Long, "|", "|")
|
||||
fmt.Fprintf(w, "%s\n\n", longWithEscapedPipe)
|
||||
fmt.Fprintf(w, "%s\n\n", cmd.Long)
|
||||
}
|
||||
|
||||
for _, g := range root.GroupedCommands(cmd) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package featuredetection
|
||||
|
||||
import "github.com/cli/cli/v2/internal/gh"
|
||||
|
||||
type DisabledDetectorMock struct{}
|
||||
|
||||
func (md *DisabledDetectorMock) IssueFeatures() (IssueFeatures, error) {
|
||||
|
|
@ -14,6 +16,10 @@ func (md *DisabledDetectorMock) RepositoryFeatures() (RepositoryFeatures, error)
|
|||
return RepositoryFeatures{}, nil
|
||||
}
|
||||
|
||||
func (md *DisabledDetectorMock) ProjectsV1() gh.ProjectsV1Support {
|
||||
return gh.ProjectsV1Unsupported
|
||||
}
|
||||
|
||||
type EnabledDetectorMock struct{}
|
||||
|
||||
func (md *EnabledDetectorMock) IssueFeatures() (IssueFeatures, error) {
|
||||
|
|
@ -27,3 +33,7 @@ func (md *EnabledDetectorMock) PullRequestFeatures() (PullRequestFeatures, error
|
|||
func (md *EnabledDetectorMock) RepositoryFeatures() (RepositoryFeatures, error) {
|
||||
return allRepositoryFeatures, nil
|
||||
}
|
||||
|
||||
func (md *EnabledDetectorMock) ProjectsV1() gh.ProjectsV1Support {
|
||||
return gh.ProjectsV1Supported
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
ghauth "github.com/cli/go-gh/v2/pkg/auth"
|
||||
|
|
@ -13,14 +14,17 @@ type Detector interface {
|
|||
IssueFeatures() (IssueFeatures, error)
|
||||
PullRequestFeatures() (PullRequestFeatures, error)
|
||||
RepositoryFeatures() (RepositoryFeatures, error)
|
||||
ProjectsV1() gh.ProjectsV1Support
|
||||
}
|
||||
|
||||
type IssueFeatures struct {
|
||||
StateReason bool
|
||||
StateReason bool
|
||||
ActorIsAssignable bool
|
||||
}
|
||||
|
||||
var allIssueFeatures = IssueFeatures{
|
||||
StateReason: true,
|
||||
StateReason: true,
|
||||
ActorIsAssignable: true,
|
||||
}
|
||||
|
||||
type PullRequestFeatures struct {
|
||||
|
|
@ -68,7 +72,8 @@ func (d *detector) IssueFeatures() (IssueFeatures, error) {
|
|||
}
|
||||
|
||||
features := IssueFeatures{
|
||||
StateReason: false,
|
||||
StateReason: false,
|
||||
ActorIsAssignable: false, // replaceActorsForAssignable GraphQL mutation unavailable on GHES
|
||||
}
|
||||
|
||||
var featureDetection struct {
|
||||
|
|
@ -199,3 +204,13 @@ func (d *detector) RepositoryFeatures() (RepositoryFeatures, error) {
|
|||
|
||||
return features, nil
|
||||
}
|
||||
|
||||
func (d *detector) ProjectsV1() gh.ProjectsV1Support {
|
||||
// Currently, projects v1 support is entirely dependent on the host. As this is deprecated in GHES,
|
||||
// we will do feature detection on whether the GHES version has support.
|
||||
if ghauth.IsEnterprise(d.host) {
|
||||
return gh.ProjectsV1Supported
|
||||
}
|
||||
|
||||
return gh.ProjectsV1Unsupported
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIssueFeatures(t *testing.T) {
|
||||
|
|
@ -21,7 +23,8 @@ func TestIssueFeatures(t *testing.T) {
|
|||
name: "github.com",
|
||||
hostname: "github.com",
|
||||
wantFeatures: IssueFeatures{
|
||||
StateReason: true,
|
||||
StateReason: true,
|
||||
ActorIsAssignable: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
@ -29,7 +32,8 @@ func TestIssueFeatures(t *testing.T) {
|
|||
name: "ghec data residency (ghe.com)",
|
||||
hostname: "stampname.ghe.com",
|
||||
wantFeatures: IssueFeatures{
|
||||
StateReason: true,
|
||||
StateReason: true,
|
||||
ActorIsAssignable: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
@ -40,7 +44,8 @@ func TestIssueFeatures(t *testing.T) {
|
|||
`query Issue_fields\b`: `{"data": {}}`,
|
||||
},
|
||||
wantFeatures: IssueFeatures{
|
||||
StateReason: false,
|
||||
StateReason: false,
|
||||
ActorIsAssignable: false,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
@ -366,3 +371,19 @@ func TestRepositoryFeatures(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectV1Support(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("when the host is enterprise, project v1 is supported", func(t *testing.T) {
|
||||
detector := detector{host: "my.ghes.com"}
|
||||
isProjectV1Supported := detector.ProjectsV1()
|
||||
require.Equal(t, gh.ProjectsV1Supported, isProjectV1Supported)
|
||||
})
|
||||
|
||||
t.Run("when the host is not enterprise, project v1 is not supported", func(t *testing.T) {
|
||||
detector := detector{host: "github.com"}
|
||||
isProjectV1Supported := detector.ProjectsV1()
|
||||
require.Equal(t, gh.ProjectsV1Unsupported, isProjectV1Supported)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ type Config interface {
|
|||
// Set provides primitive access for setting configuration values, optionally scoped by host.
|
||||
Set(hostname string, key string, value string)
|
||||
|
||||
// AccessibleColors returns the configured accessible_colors setting, optionally scoped by host.
|
||||
AccessibleColors(hostname string) ConfigEntry
|
||||
// AccessiblePrompter returns the configured accessible_prompter setting, optionally scoped by host.
|
||||
AccessiblePrompter(hostname string) ConfigEntry
|
||||
// Browser returns the configured browser, optionally scoped by host.
|
||||
Browser(hostname string) ConfigEntry
|
||||
// ColorLabels returns the configured color_label setting, optionally scoped by host.
|
||||
|
|
@ -51,6 +55,8 @@ type Config interface {
|
|||
Prompt(hostname string) ConfigEntry
|
||||
// PreferEditorPrompt returns the configured editor-based prompt, optionally scoped by host.
|
||||
PreferEditorPrompt(hostname string) ConfigEntry
|
||||
// Spinner returns the configured spinner setting, optionally scoped by host.
|
||||
Spinner(hostname string) ConfigEntry
|
||||
|
||||
// Aliases provides persistent storage and modification of command aliases.
|
||||
Aliases() AliasConfig
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ var _ gh.Config = &ConfigMock{}
|
|||
//
|
||||
// // make and configure a mocked gh.Config
|
||||
// mockedConfig := &ConfigMock{
|
||||
// AccessibleColorsFunc: func(hostname string) gh.ConfigEntry {
|
||||
// panic("mock out the AccessibleColors method")
|
||||
// },
|
||||
// AccessiblePrompterFunc: func(hostname string) gh.ConfigEntry {
|
||||
// panic("mock out the AccessiblePrompter method")
|
||||
// },
|
||||
// AliasesFunc: func() gh.AliasConfig {
|
||||
// panic("mock out the Aliases method")
|
||||
// },
|
||||
|
|
@ -61,6 +67,9 @@ var _ gh.Config = &ConfigMock{}
|
|||
// SetFunc: func(hostname string, key string, value string) {
|
||||
// panic("mock out the Set method")
|
||||
// },
|
||||
// SpinnerFunc: func(hostname string) gh.ConfigEntry {
|
||||
// panic("mock out the Spinner method")
|
||||
// },
|
||||
// VersionFunc: func() o.Option[string] {
|
||||
// panic("mock out the Version method")
|
||||
// },
|
||||
|
|
@ -74,6 +83,12 @@ var _ gh.Config = &ConfigMock{}
|
|||
//
|
||||
// }
|
||||
type ConfigMock struct {
|
||||
// AccessibleColorsFunc mocks the AccessibleColors method.
|
||||
AccessibleColorsFunc func(hostname string) gh.ConfigEntry
|
||||
|
||||
// AccessiblePrompterFunc mocks the AccessiblePrompter method.
|
||||
AccessiblePrompterFunc func(hostname string) gh.ConfigEntry
|
||||
|
||||
// AliasesFunc mocks the Aliases method.
|
||||
AliasesFunc func() gh.AliasConfig
|
||||
|
||||
|
|
@ -116,6 +131,9 @@ type ConfigMock struct {
|
|||
// SetFunc mocks the Set method.
|
||||
SetFunc func(hostname string, key string, value string)
|
||||
|
||||
// SpinnerFunc mocks the Spinner method.
|
||||
SpinnerFunc func(hostname string) gh.ConfigEntry
|
||||
|
||||
// VersionFunc mocks the Version method.
|
||||
VersionFunc func() o.Option[string]
|
||||
|
||||
|
|
@ -124,6 +142,16 @@ type ConfigMock struct {
|
|||
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
// AccessibleColors holds details about calls to the AccessibleColors method.
|
||||
AccessibleColors []struct {
|
||||
// Hostname is the hostname argument value.
|
||||
Hostname string
|
||||
}
|
||||
// AccessiblePrompter holds details about calls to the AccessiblePrompter method.
|
||||
AccessiblePrompter []struct {
|
||||
// Hostname is the hostname argument value.
|
||||
Hostname string
|
||||
}
|
||||
// Aliases holds details about calls to the Aliases method.
|
||||
Aliases []struct {
|
||||
}
|
||||
|
|
@ -194,6 +222,11 @@ type ConfigMock struct {
|
|||
// Value is the value argument value.
|
||||
Value string
|
||||
}
|
||||
// Spinner holds details about calls to the Spinner method.
|
||||
Spinner []struct {
|
||||
// Hostname is the hostname argument value.
|
||||
Hostname string
|
||||
}
|
||||
// Version holds details about calls to the Version method.
|
||||
Version []struct {
|
||||
}
|
||||
|
|
@ -201,6 +234,8 @@ type ConfigMock struct {
|
|||
Write []struct {
|
||||
}
|
||||
}
|
||||
lockAccessibleColors sync.RWMutex
|
||||
lockAccessiblePrompter sync.RWMutex
|
||||
lockAliases sync.RWMutex
|
||||
lockAuthentication sync.RWMutex
|
||||
lockBrowser sync.RWMutex
|
||||
|
|
@ -215,10 +250,75 @@ type ConfigMock struct {
|
|||
lockPreferEditorPrompt sync.RWMutex
|
||||
lockPrompt sync.RWMutex
|
||||
lockSet sync.RWMutex
|
||||
lockSpinner sync.RWMutex
|
||||
lockVersion sync.RWMutex
|
||||
lockWrite sync.RWMutex
|
||||
}
|
||||
|
||||
// AccessibleColors calls AccessibleColorsFunc.
|
||||
func (mock *ConfigMock) AccessibleColors(hostname string) gh.ConfigEntry {
|
||||
if mock.AccessibleColorsFunc == nil {
|
||||
panic("ConfigMock.AccessibleColorsFunc: method is nil but Config.AccessibleColors was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Hostname string
|
||||
}{
|
||||
Hostname: hostname,
|
||||
}
|
||||
mock.lockAccessibleColors.Lock()
|
||||
mock.calls.AccessibleColors = append(mock.calls.AccessibleColors, callInfo)
|
||||
mock.lockAccessibleColors.Unlock()
|
||||
return mock.AccessibleColorsFunc(hostname)
|
||||
}
|
||||
|
||||
// AccessibleColorsCalls gets all the calls that were made to AccessibleColors.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedConfig.AccessibleColorsCalls())
|
||||
func (mock *ConfigMock) AccessibleColorsCalls() []struct {
|
||||
Hostname string
|
||||
} {
|
||||
var calls []struct {
|
||||
Hostname string
|
||||
}
|
||||
mock.lockAccessibleColors.RLock()
|
||||
calls = mock.calls.AccessibleColors
|
||||
mock.lockAccessibleColors.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// AccessiblePrompter calls AccessiblePrompterFunc.
|
||||
func (mock *ConfigMock) AccessiblePrompter(hostname string) gh.ConfigEntry {
|
||||
if mock.AccessiblePrompterFunc == nil {
|
||||
panic("ConfigMock.AccessiblePrompterFunc: method is nil but Config.AccessiblePrompter was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Hostname string
|
||||
}{
|
||||
Hostname: hostname,
|
||||
}
|
||||
mock.lockAccessiblePrompter.Lock()
|
||||
mock.calls.AccessiblePrompter = append(mock.calls.AccessiblePrompter, callInfo)
|
||||
mock.lockAccessiblePrompter.Unlock()
|
||||
return mock.AccessiblePrompterFunc(hostname)
|
||||
}
|
||||
|
||||
// AccessiblePrompterCalls gets all the calls that were made to AccessiblePrompter.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedConfig.AccessiblePrompterCalls())
|
||||
func (mock *ConfigMock) AccessiblePrompterCalls() []struct {
|
||||
Hostname string
|
||||
} {
|
||||
var calls []struct {
|
||||
Hostname string
|
||||
}
|
||||
mock.lockAccessiblePrompter.RLock()
|
||||
calls = mock.calls.AccessiblePrompter
|
||||
mock.lockAccessiblePrompter.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Aliases calls AliasesFunc.
|
||||
func (mock *ConfigMock) Aliases() gh.AliasConfig {
|
||||
if mock.AliasesFunc == nil {
|
||||
|
|
@ -664,6 +764,38 @@ func (mock *ConfigMock) SetCalls() []struct {
|
|||
return calls
|
||||
}
|
||||
|
||||
// Spinner calls SpinnerFunc.
|
||||
func (mock *ConfigMock) Spinner(hostname string) gh.ConfigEntry {
|
||||
if mock.SpinnerFunc == nil {
|
||||
panic("ConfigMock.SpinnerFunc: method is nil but Config.Spinner was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Hostname string
|
||||
}{
|
||||
Hostname: hostname,
|
||||
}
|
||||
mock.lockSpinner.Lock()
|
||||
mock.calls.Spinner = append(mock.calls.Spinner, callInfo)
|
||||
mock.lockSpinner.Unlock()
|
||||
return mock.SpinnerFunc(hostname)
|
||||
}
|
||||
|
||||
// SpinnerCalls gets all the calls that were made to Spinner.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedConfig.SpinnerCalls())
|
||||
func (mock *ConfigMock) SpinnerCalls() []struct {
|
||||
Hostname string
|
||||
} {
|
||||
var calls []struct {
|
||||
Hostname string
|
||||
}
|
||||
mock.lockSpinner.RLock()
|
||||
calls = mock.calls.Spinner
|
||||
mock.lockSpinner.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Version calls VersionFunc.
|
||||
func (mock *ConfigMock) Version() o.Option[string] {
|
||||
if mock.VersionFunc == nil {
|
||||
|
|
|
|||
23
internal/gh/projects.go
Normal file
23
internal/gh/projects.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package gh
|
||||
|
||||
// ProjectsV1Support provides type safety and readability around whether or not Projects v1 is supported
|
||||
// by the targeted host.
|
||||
//
|
||||
// It is a sealed type to ensure that consumers must use the exported ProjectsV1Supported and ProjectsV1Unsupported
|
||||
// variables to get an instance of the type.
|
||||
type ProjectsV1Support interface {
|
||||
sealed()
|
||||
}
|
||||
|
||||
type projectsV1Supported struct{}
|
||||
|
||||
func (projectsV1Supported) sealed() {}
|
||||
|
||||
type projectsV1Unsupported struct{}
|
||||
|
||||
func (projectsV1Unsupported) sealed() {}
|
||||
|
||||
var (
|
||||
ProjectsV1Supported ProjectsV1Support = projectsV1Supported{}
|
||||
ProjectsV1Unsupported ProjectsV1Support = projectsV1Unsupported{}
|
||||
)
|
||||
|
|
@ -29,14 +29,6 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// updaterEnabled is a statically linked build property set in gh formula within homebrew/homebrew-core
|
||||
// used to control whether users are notified of newer GitHub CLI releases.
|
||||
// This needs to be set to 'cli/cli' as it affects where update.CheckForUpdate() checks for releases.
|
||||
// It is unclear whether this means that only homebrew builds will check for updates or not.
|
||||
// Development builds leave this empty as impossible to determine if newer or not.
|
||||
// For more information, <https://github.com/Homebrew/homebrew-core/blob/master/Formula/g/gh.rb>.
|
||||
var updaterEnabled = ""
|
||||
|
||||
type exitCode int
|
||||
|
||||
const (
|
||||
|
|
|
|||
6
internal/ghcmd/update_disabled.go
Normal file
6
internal/ghcmd/update_disabled.go
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
//go:build !updateable
|
||||
|
||||
package ghcmd
|
||||
|
||||
// See update_enabled.go comment for more information.
|
||||
var updaterEnabled = ""
|
||||
18
internal/ghcmd/update_enabled.go
Normal file
18
internal/ghcmd/update_enabled.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
//go:build updateable
|
||||
|
||||
package ghcmd
|
||||
|
||||
// `updateable` is a build tag set in the gh formula within homebrew/homebrew-core
|
||||
// and is used to control whether users are notified of newer GitHub CLI releases.
|
||||
//
|
||||
// Currently, updaterEnabled needs to be set to 'cli/cli' as it affects where
|
||||
// update.CheckForUpdate() checks for releases. It is unclear to what extent
|
||||
// this updaterEnabled is being used by unofficial forks or builds, so we decided
|
||||
// to leave it available for injection as a string variable for now.
|
||||
//
|
||||
// Development builds do not generate update messages by default.
|
||||
//
|
||||
// For more information, see:
|
||||
// - the Homebrew formula for gh: <https://github.com/Homebrew/homebrew-core/blob/master/Formula/g/gh.rb>.
|
||||
// - a discussion about adding this build tag: <https://github.com/cli/cli/pull/11024#discussion_r2107597618>.
|
||||
var updaterEnabled = "cli/cli"
|
||||
|
|
@ -5,12 +5,14 @@ package prompter_test
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Netflix/go-expect"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/creack/pty"
|
||||
"github.com/hinshun/vt10x"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -31,13 +33,16 @@ import (
|
|||
// are sufficient to ensure that the accessible prompter behaves roughly as expected
|
||||
// but doesn't mandate that prompts always look exactly the same.
|
||||
func TestAccessiblePrompter(t *testing.T) {
|
||||
|
||||
beforePasswordSendTimeout := 100 * time.Microsecond
|
||||
|
||||
t.Run("Select", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Choose:")
|
||||
_, err := console.ExpectString("Input a number between 1 and 3:")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Select option 1
|
||||
|
|
@ -50,13 +55,80 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
assert.Equal(t, 0, selectValue)
|
||||
})
|
||||
|
||||
t.Run("MultiSelect", func(t *testing.T) {
|
||||
t.Run("Select - blank input returns default value", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
dummyDefaultValue := "12345abcdefg"
|
||||
options := []string{"1", "2", dummyDefaultValue}
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Select a number")
|
||||
_, err := console.ExpectString("Input a number between 1 and 3:")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Just press enter to accept the default
|
||||
_, err = console.SendLine("")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
selectValue, err := p.Select("Select a number", dummyDefaultValue, options)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedIndex := slices.Index(options, dummyDefaultValue)
|
||||
assert.Equal(t, expectedIndex, selectValue)
|
||||
})
|
||||
|
||||
t.Run("Select - default value is in prompt and in readable format", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
dummyDefaultValue := "12345abcdefg"
|
||||
options := []string{"1", "2", dummyDefaultValue}
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Select a number (default: 12345abcdefg)")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Just press enter to accept the default
|
||||
_, err = console.SendLine("")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
selectValue, err := p.Select("Select a number", dummyDefaultValue, options)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedIndex := slices.Index(options, dummyDefaultValue)
|
||||
assert.Equal(t, expectedIndex, selectValue)
|
||||
})
|
||||
|
||||
t.Run("Select - invalid defaults are excluded from prompt", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
dummyDefaultValue := "foo"
|
||||
options := []string{"1", "2"}
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear without the invalid default value
|
||||
_, err := console.ExpectString("Select a number \r\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Select option 2
|
||||
_, err = console.SendLine("2")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
selectValue, err := p.Select("Select a number", dummyDefaultValue, options)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, selectValue)
|
||||
})
|
||||
|
||||
t.Run("MultiSelect", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Input a number between 0 and 3:")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Select options 1 and 2
|
||||
|
|
@ -75,9 +147,86 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
assert.Equal(t, []int{0, 1}, multiSelectValue)
|
||||
})
|
||||
|
||||
t.Run("MultiSelect - default values are respected by being pre-selected", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Select a number")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Don't select anything because the default should be selected.
|
||||
|
||||
// This confirms selections
|
||||
_, err = console.SendLine("0")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
multiSelectValue, err := p.MultiSelect("Select a number", []string{"2"}, []string{"1", "2", "3"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{1}, multiSelectValue)
|
||||
})
|
||||
|
||||
t.Run("MultiSelect - default value is in prompt and in readable format", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
dummyDefaultValues := []string{"foo", "bar"}
|
||||
options := []string{"1", "2"}
|
||||
options = append(options, dummyDefaultValues...)
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Select a number (defaults: foo, bar)")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Don't select anything because the defaults should be selected.
|
||||
|
||||
// This confirms selections
|
||||
_, err = console.SendLine("0")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
multiSelectValues, err := p.MultiSelect("Select a number", dummyDefaultValues, options)
|
||||
require.NoError(t, err)
|
||||
var expectedIndices []int
|
||||
|
||||
// Get the indices of the default values within the options slice
|
||||
// as that's what we expect the prompter to return when no selections are made.
|
||||
for _, defaultValue := range dummyDefaultValues {
|
||||
expectedIndices = append(expectedIndices, slices.Index(options, defaultValue))
|
||||
}
|
||||
assert.Equal(t, expectedIndices, multiSelectValues)
|
||||
})
|
||||
|
||||
t.Run("MultiSelect - invalid defaults are excluded from prompt", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
dummyDefaultValues := []string{"foo", "bar"}
|
||||
options := []string{"1", "2"}
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear without the invalid default values
|
||||
_, err := console.ExpectString("Select a number \r\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Not selecting anything will fail because there are no defaults.
|
||||
_, err = console.SendLine("2")
|
||||
require.NoError(t, err)
|
||||
|
||||
// This confirms selections
|
||||
_, err = console.SendLine("0")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
multiSelectValues, err := p.MultiSelect("Select a number", dummyDefaultValues, options)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{1}, multiSelectValues)
|
||||
})
|
||||
|
||||
t.Run("Input", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
dummyText := "12345abcdefg"
|
||||
|
||||
go func() {
|
||||
|
|
@ -97,7 +246,7 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
|
||||
t.Run("Input - blank input returns default value", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
dummyDefaultValue := "12345abcdefg"
|
||||
|
||||
go func() {
|
||||
|
|
@ -115,9 +264,29 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
assert.Equal(t, dummyDefaultValue, inputValue)
|
||||
})
|
||||
|
||||
t.Run("Input - default value is in prompt and in readable format", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
dummyDefaultValue := "12345abcdefg"
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Enter some characters (default: 12345abcdefg)")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Enter nothing
|
||||
_, err = console.SendLine("")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
inputValue, err := p.Input("Enter some characters", dummyDefaultValue)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, dummyDefaultValue, inputValue)
|
||||
})
|
||||
|
||||
t.Run("Password", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
dummyPassword := "12345abcdefg"
|
||||
|
||||
go func() {
|
||||
|
|
@ -125,6 +294,9 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
_, err := console.ExpectString("Enter password")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait to ensure huh has time to set the echo mode
|
||||
time.Sleep(beforePasswordSendTimeout)
|
||||
|
||||
// Enter a number
|
||||
_, err = console.SendLine(dummyPassword)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -133,11 +305,21 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
passwordValue, err := p.Password("Enter password")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dummyPassword, passwordValue)
|
||||
|
||||
// Ensure the dummy password is not printed to the screen,
|
||||
// asserting that echo mode is disabled.
|
||||
//
|
||||
// Note that since console.ExpectString returns successful if the
|
||||
// expected string matches any part of the stream, we have to use an
|
||||
// anchored regexp (i.e., with ^ and $) to make sure the password/token
|
||||
// is not printed at all.
|
||||
_, err = console.Expect(expect.RegexpPattern("^ \r\n\r\n$"))
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Confirm", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
|
|
@ -156,7 +338,7 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
|
||||
t.Run("Confirm - blank input returns default", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
|
|
@ -173,9 +355,29 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
require.Equal(t, false, confirmValue)
|
||||
})
|
||||
|
||||
t.Run("Confirm - default value is in prompt and in readable format", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
defaultValue := true
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
_, err := console.ExpectString("Are you sure (default: yes)")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Enter nothing
|
||||
_, err = console.SendLine("")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
confirmValue, err := p.Confirm("Are you sure", defaultValue)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, defaultValue, confirmValue)
|
||||
})
|
||||
|
||||
t.Run("AuthToken", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
dummyAuthToken := "12345abcdefg"
|
||||
|
||||
go func() {
|
||||
|
|
@ -183,6 +385,9 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
_, err := console.ExpectString("Paste your authentication token:")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait to ensure huh has time to set the echo mode
|
||||
time.Sleep(beforePasswordSendTimeout)
|
||||
|
||||
// Enter some dummy auth token
|
||||
_, err = console.SendLine(dummyAuthToken)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -191,11 +396,21 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
authValue, err := p.AuthToken()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dummyAuthToken, authValue)
|
||||
|
||||
// Ensure the dummy password is not printed to the screen,
|
||||
// asserting that echo mode is disabled.
|
||||
//
|
||||
// Note that since console.ExpectString returns successful if the
|
||||
// expected string matches any part of the stream, we have to use an
|
||||
// anchored regexp (i.e., with ^ and $) to make sure the password/token
|
||||
// is not printed at all.
|
||||
_, err = console.Expect(expect.RegexpPattern("^ \r\n\r\n$"))
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("AuthToken - blank input returns error", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
dummyAuthTokenForAfterFailure := "12345abcdefg"
|
||||
|
||||
go func() {
|
||||
|
|
@ -211,6 +426,13 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
_, err = console.ExpectString("token is required")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the retry prompt
|
||||
_, err = console.ExpectString("Paste your authentication token:")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait to ensure huh has time to set the echo mode
|
||||
time.Sleep(beforePasswordSendTimeout)
|
||||
|
||||
// Now enter some dummy auth token to return control back to the test
|
||||
_, err = console.SendLine(dummyAuthTokenForAfterFailure)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -219,11 +441,21 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
authValue, err := p.AuthToken()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dummyAuthTokenForAfterFailure, authValue)
|
||||
|
||||
// Ensure the dummy password is not printed to the screen,
|
||||
// asserting that echo mode is disabled.
|
||||
//
|
||||
// Note that since console.ExpectString returns successful if the
|
||||
// expected string matches any part of the stream, we have to use an
|
||||
// anchored regexp (i.e., with ^ and $) to make sure the password/token
|
||||
// is not printed at all.
|
||||
_, err = console.Expect(expect.RegexpPattern("^ \r\n\r\n$"))
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("ConfirmDeletion", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
|
||||
requiredValue := "test"
|
||||
go func() {
|
||||
|
|
@ -243,7 +475,7 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
|
||||
t.Run("ConfirmDeletion - bad input", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
requiredValue := "test"
|
||||
badInputValue := "garbage"
|
||||
|
||||
|
|
@ -272,7 +504,7 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
|
||||
t.Run("InputHostname", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
hostname := "example.com"
|
||||
|
||||
go func() {
|
||||
|
|
@ -292,7 +524,7 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
|
||||
t.Run("MarkdownEditor - blank allowed with blank input returns blank", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
|
|
@ -311,7 +543,7 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
|
||||
t.Run("MarkdownEditor - blank disallowed with default value returns default value", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
defaultValue := "12345abcdefg"
|
||||
|
||||
go func() {
|
||||
|
|
@ -324,7 +556,7 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Expect a notice to enter something valid since blank is disallowed.
|
||||
_, err = console.ExpectString("invalid input. please try again")
|
||||
_, err = console.ExpectString("Invalid: must be between 1 and 1")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Send a 1 to select to open the editor. This will immediately exit
|
||||
|
|
@ -339,7 +571,7 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
|
||||
t.Run("MarkdownEditor - blank disallowed no default value returns error", func(t *testing.T) {
|
||||
console := newTestVirtualTerminal(t)
|
||||
p := newTestAcessiblePrompter(t, console)
|
||||
p := newTestAccessiblePrompter(t, console)
|
||||
|
||||
go func() {
|
||||
// Wait for prompt to appear
|
||||
|
|
@ -351,7 +583,7 @@ func TestAccessiblePrompter(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Expect a notice to enter something valid since blank is disallowed.
|
||||
_, err = console.ExpectString("invalid input. please try again")
|
||||
_, err = console.ExpectString("Invalid: must be between 1 and 1")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Send a 1 to select to open the editor since skip is invalid and
|
||||
|
|
@ -419,21 +651,40 @@ func newTestVirtualTerminal(t *testing.T) *expect.Console {
|
|||
return console
|
||||
}
|
||||
|
||||
func newTestAcessiblePrompter(t *testing.T, console *expect.Console) prompter.Prompter {
|
||||
func newTestVirtualTerminalIOStreams(t *testing.T, console *expect.Console) *iostreams.IOStreams {
|
||||
t.Helper()
|
||||
io := &iostreams.IOStreams{
|
||||
In: console.Tty(),
|
||||
Out: console.Tty(),
|
||||
ErrOut: console.Tty(),
|
||||
}
|
||||
io.SetStdinTTY(false)
|
||||
io.SetStdoutTTY(false)
|
||||
io.SetStderrTTY(false)
|
||||
return io
|
||||
}
|
||||
|
||||
// `echo` is chosen as the editor command because it immediately returns
|
||||
// a success exit code, returns an empty string, doesn't require any user input,
|
||||
// and since this file is only built on Linux, it is near guaranteed to be available.
|
||||
var editorCmd = "echo"
|
||||
|
||||
func newTestAccessiblePrompter(t *testing.T, console *expect.Console) prompter.Prompter {
|
||||
t.Helper()
|
||||
|
||||
t.Setenv("GH_ACCESSIBLE_PROMPTER", "true")
|
||||
// `echo`` is chose as the editor command because it immediately returns
|
||||
// a success exit code, returns an empty string, doesn't require any user input,
|
||||
// and since this file is only built on Linux, it is near guaranteed to be available.
|
||||
return prompter.New("echo", console.Tty(), console.Tty(), console.Tty())
|
||||
io := newTestVirtualTerminalIOStreams(t, console)
|
||||
io.SetAccessiblePrompterEnabled(true)
|
||||
|
||||
return prompter.New(editorCmd, io)
|
||||
}
|
||||
|
||||
func newTestSurveyPrompter(t *testing.T, console *expect.Console) prompter.Prompter {
|
||||
t.Helper()
|
||||
|
||||
t.Setenv("GH_ACCESSIBLE_PROMPTER", "false")
|
||||
return prompter.New("echo", console.Tty(), console.Tty(), console.Tty())
|
||||
io := newTestVirtualTerminalIOStreams(t, console)
|
||||
io.SetAccessiblePrompterEnabled(false)
|
||||
|
||||
return prompter.New(editorCmd, io)
|
||||
}
|
||||
|
||||
// failOnExpectError adds an observer that will fail the test in a standardised way
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ package prompter
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/surveyext"
|
||||
ghPrompter "github.com/cli/go-gh/v2/pkg/prompter"
|
||||
)
|
||||
|
|
@ -43,24 +43,21 @@ type Prompter interface {
|
|||
MarkdownEditor(prompt string, defaultValue string, blankAllowed bool) (string, error)
|
||||
}
|
||||
|
||||
func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWriter, stderr ghPrompter.FileWriter) Prompter {
|
||||
accessiblePrompterValue, accessiblePrompterIsSet := os.LookupEnv("GH_ACCESSIBLE_PROMPTER")
|
||||
falseyValues := []string{"false", "0", "no", ""}
|
||||
|
||||
if accessiblePrompterIsSet && !slices.Contains(falseyValues, accessiblePrompterValue) {
|
||||
func New(editorCmd string, io *iostreams.IOStreams) Prompter {
|
||||
if io.AccessiblePrompterEnabled() {
|
||||
return &accessiblePrompter{
|
||||
stdin: stdin,
|
||||
stdout: stdout,
|
||||
stderr: stderr,
|
||||
stdin: io.In,
|
||||
stdout: io.Out,
|
||||
stderr: io.ErrOut,
|
||||
editorCmd: editorCmd,
|
||||
}
|
||||
}
|
||||
|
||||
return &surveyPrompter{
|
||||
prompter: ghPrompter.New(stdin, stdout, stderr),
|
||||
stdin: stdin,
|
||||
stdout: stdout,
|
||||
stderr: stderr,
|
||||
prompter: ghPrompter.New(io.In, io.Out, io.ErrOut),
|
||||
stdin: io.In,
|
||||
stdout: io.Out,
|
||||
stderr: io.ErrOut,
|
||||
editorCmd: editorCmd,
|
||||
}
|
||||
}
|
||||
|
|
@ -80,10 +77,40 @@ func (p *accessiblePrompter) newForm(groups ...*huh.Group) *huh.Form {
|
|||
WithOutput(p.stdout)
|
||||
}
|
||||
|
||||
func (p *accessiblePrompter) Select(prompt, _ string, options []string) (int, error) {
|
||||
// addDefaultsToPrompt adds default values to the prompt string.
|
||||
func (p *accessiblePrompter) addDefaultsToPrompt(prompt string, defaultValues []string) string {
|
||||
// Removing empty defaults from the slice.
|
||||
defaultValues = slices.DeleteFunc(defaultValues, func(s string) bool {
|
||||
return s == ""
|
||||
})
|
||||
|
||||
// Pluralizing the prompt if there are multiple default values.
|
||||
if len(defaultValues) == 1 {
|
||||
prompt = fmt.Sprintf("%s (default: %s)", prompt, defaultValues[0])
|
||||
} else if len(defaultValues) > 1 {
|
||||
prompt = fmt.Sprintf("%s (defaults: %s)", prompt, strings.Join(defaultValues, ", "))
|
||||
}
|
||||
|
||||
// Zero-length defaultValues means return prompt unchanged.
|
||||
return prompt
|
||||
}
|
||||
|
||||
func (p *accessiblePrompter) Select(prompt, defaultValue string, options []string) (int, error) {
|
||||
var result int
|
||||
|
||||
// Remove invalid default values from the defaults slice.
|
||||
if !slices.Contains(options, defaultValue) {
|
||||
defaultValue = ""
|
||||
}
|
||||
|
||||
prompt = p.addDefaultsToPrompt(prompt, []string{defaultValue})
|
||||
formOptions := []huh.Option[int]{}
|
||||
for i, o := range options {
|
||||
// If this option is the default value, assign its index
|
||||
// to the result variable. huh will treat it as a default selection.
|
||||
if defaultValue == o {
|
||||
result = i
|
||||
}
|
||||
formOptions = append(formOptions, huh.NewOption(o, i))
|
||||
}
|
||||
|
||||
|
|
@ -102,8 +129,22 @@ func (p *accessiblePrompter) Select(prompt, _ string, options []string) (int, er
|
|||
|
||||
func (p *accessiblePrompter) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) {
|
||||
var result []int
|
||||
|
||||
// Remove invalid default values from the defaults slice.
|
||||
defaults = slices.DeleteFunc(defaults, func(s string) bool {
|
||||
return !slices.Contains(options, s)
|
||||
})
|
||||
|
||||
prompt = p.addDefaultsToPrompt(prompt, defaults)
|
||||
formOptions := make([]huh.Option[int], len(options))
|
||||
for i, o := range options {
|
||||
// If this option is in the defaults slice,
|
||||
// let's add its index to the result slice and huh
|
||||
// will treat it as a default selection.
|
||||
if slices.Contains(defaults, o) {
|
||||
result = append(result, i)
|
||||
}
|
||||
|
||||
formOptions[i] = huh.NewOption(o, i)
|
||||
}
|
||||
|
||||
|
|
@ -126,7 +167,7 @@ func (p *accessiblePrompter) MultiSelect(prompt string, defaults []string, optio
|
|||
|
||||
func (p *accessiblePrompter) Input(prompt, defaultValue string) (string, error) {
|
||||
result := defaultValue
|
||||
prompt = fmt.Sprintf("%s (%s)", prompt, defaultValue)
|
||||
prompt = p.addDefaultsToPrompt(prompt, []string{defaultValue})
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
|
|
@ -141,10 +182,12 @@ func (p *accessiblePrompter) Input(prompt, defaultValue string) (string, error)
|
|||
|
||||
func (p *accessiblePrompter) Password(prompt string) (string, error) {
|
||||
var result string
|
||||
// EchoMode(huh.EchoModePassword) doesn't have any effect in accessible mode.
|
||||
// EchoModePassword is not used as password masking is unsupported in huh.
|
||||
// EchoModeNone and EchoModePassword have the same effect of hiding user input.
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
EchoMode(huh.EchoModeNone).
|
||||
Title(prompt).
|
||||
Value(&result),
|
||||
),
|
||||
|
|
@ -160,6 +203,13 @@ func (p *accessiblePrompter) Password(prompt string) (string, error) {
|
|||
|
||||
func (p *accessiblePrompter) Confirm(prompt string, defaultValue bool) (bool, error) {
|
||||
result := defaultValue
|
||||
|
||||
if defaultValue {
|
||||
prompt = p.addDefaultsToPrompt(prompt, []string{"yes"})
|
||||
} else {
|
||||
prompt = p.addDefaultsToPrompt(prompt, []string{"no"})
|
||||
}
|
||||
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewConfirm().
|
||||
|
|
@ -167,6 +217,7 @@ func (p *accessiblePrompter) Confirm(prompt string, defaultValue bool) (bool, er
|
|||
Value(&result),
|
||||
),
|
||||
)
|
||||
|
||||
if err := form.Run(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
|
@ -175,9 +226,12 @@ func (p *accessiblePrompter) Confirm(prompt string, defaultValue bool) (bool, er
|
|||
|
||||
func (p *accessiblePrompter) AuthToken() (string, error) {
|
||||
var result string
|
||||
// EchoModeNone and EchoModePassword both result in disabling echo mode
|
||||
// as password masking is outside of VT100 spec.
|
||||
form := p.newForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
EchoMode(huh.EchoModeNone).
|
||||
Title("Paste your authentication token:").
|
||||
// Note: if this validation fails, the prompt loops.
|
||||
Validate(func(input string) error {
|
||||
|
|
@ -187,8 +241,6 @@ func (p *accessiblePrompter) AuthToken() (string, error) {
|
|||
return nil
|
||||
}).
|
||||
Value(&result),
|
||||
// This doesn't have any effect in accessible mode.
|
||||
// EchoMode(huh.EchoModePassword),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,28 +20,28 @@ var _ Prompter = &PrompterMock{}
|
|||
// AuthTokenFunc: func() (string, error) {
|
||||
// panic("mock out the AuthToken method")
|
||||
// },
|
||||
// ConfirmFunc: func(s string, b bool) (bool, error) {
|
||||
// ConfirmFunc: func(prompt string, defaultValue bool) (bool, error) {
|
||||
// panic("mock out the Confirm method")
|
||||
// },
|
||||
// ConfirmDeletionFunc: func(s string) error {
|
||||
// ConfirmDeletionFunc: func(requiredValue string) error {
|
||||
// panic("mock out the ConfirmDeletion method")
|
||||
// },
|
||||
// InputFunc: func(s1 string, s2 string) (string, error) {
|
||||
// InputFunc: func(prompt string, defaultValue string) (string, error) {
|
||||
// panic("mock out the Input method")
|
||||
// },
|
||||
// InputHostnameFunc: func() (string, error) {
|
||||
// panic("mock out the InputHostname method")
|
||||
// },
|
||||
// MarkdownEditorFunc: func(s1 string, s2 string, b bool) (string, error) {
|
||||
// MarkdownEditorFunc: func(prompt string, defaultValue string, blankAllowed bool) (string, error) {
|
||||
// panic("mock out the MarkdownEditor method")
|
||||
// },
|
||||
// MultiSelectFunc: func(prompt string, defaults []string, options []string) ([]int, error) {
|
||||
// panic("mock out the MultiSelect method")
|
||||
// },
|
||||
// PasswordFunc: func(s string) (string, error) {
|
||||
// PasswordFunc: func(prompt string) (string, error) {
|
||||
// panic("mock out the Password method")
|
||||
// },
|
||||
// SelectFunc: func(s1 string, s2 string, strings []string) (int, error) {
|
||||
// SelectFunc: func(prompt string, defaultValue string, options []string) (int, error) {
|
||||
// panic("mock out the Select method")
|
||||
// },
|
||||
// }
|
||||
|
|
@ -55,28 +55,28 @@ type PrompterMock struct {
|
|||
AuthTokenFunc func() (string, error)
|
||||
|
||||
// ConfirmFunc mocks the Confirm method.
|
||||
ConfirmFunc func(s string, b bool) (bool, error)
|
||||
ConfirmFunc func(prompt string, defaultValue bool) (bool, error)
|
||||
|
||||
// ConfirmDeletionFunc mocks the ConfirmDeletion method.
|
||||
ConfirmDeletionFunc func(s string) error
|
||||
ConfirmDeletionFunc func(requiredValue string) error
|
||||
|
||||
// InputFunc mocks the Input method.
|
||||
InputFunc func(s1 string, s2 string) (string, error)
|
||||
InputFunc func(prompt string, defaultValue string) (string, error)
|
||||
|
||||
// InputHostnameFunc mocks the InputHostname method.
|
||||
InputHostnameFunc func() (string, error)
|
||||
|
||||
// MarkdownEditorFunc mocks the MarkdownEditor method.
|
||||
MarkdownEditorFunc func(s1 string, s2 string, b bool) (string, error)
|
||||
MarkdownEditorFunc func(prompt string, defaultValue string, blankAllowed bool) (string, error)
|
||||
|
||||
// MultiSelectFunc mocks the MultiSelect method.
|
||||
MultiSelectFunc func(prompt string, defaults []string, options []string) ([]int, error)
|
||||
|
||||
// PasswordFunc mocks the Password method.
|
||||
PasswordFunc func(s string) (string, error)
|
||||
PasswordFunc func(prompt string) (string, error)
|
||||
|
||||
// SelectFunc mocks the Select method.
|
||||
SelectFunc func(s1 string, s2 string, strings []string) (int, error)
|
||||
SelectFunc func(prompt string, defaultValue string, options []string) (int, error)
|
||||
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
|
|
@ -85,34 +85,34 @@ type PrompterMock struct {
|
|||
}
|
||||
// Confirm holds details about calls to the Confirm method.
|
||||
Confirm []struct {
|
||||
// S is the s argument value.
|
||||
S string
|
||||
// B is the b argument value.
|
||||
B bool
|
||||
// Prompt is the prompt argument value.
|
||||
Prompt string
|
||||
// DefaultValue is the defaultValue argument value.
|
||||
DefaultValue bool
|
||||
}
|
||||
// ConfirmDeletion holds details about calls to the ConfirmDeletion method.
|
||||
ConfirmDeletion []struct {
|
||||
// S is the s argument value.
|
||||
S string
|
||||
// RequiredValue is the requiredValue argument value.
|
||||
RequiredValue string
|
||||
}
|
||||
// Input holds details about calls to the Input method.
|
||||
Input []struct {
|
||||
// S1 is the s1 argument value.
|
||||
S1 string
|
||||
// S2 is the s2 argument value.
|
||||
S2 string
|
||||
// Prompt is the prompt argument value.
|
||||
Prompt string
|
||||
// DefaultValue is the defaultValue argument value.
|
||||
DefaultValue string
|
||||
}
|
||||
// InputHostname holds details about calls to the InputHostname method.
|
||||
InputHostname []struct {
|
||||
}
|
||||
// MarkdownEditor holds details about calls to the MarkdownEditor method.
|
||||
MarkdownEditor []struct {
|
||||
// S1 is the s1 argument value.
|
||||
S1 string
|
||||
// S2 is the s2 argument value.
|
||||
S2 string
|
||||
// B is the b argument value.
|
||||
B bool
|
||||
// Prompt is the prompt argument value.
|
||||
Prompt string
|
||||
// DefaultValue is the defaultValue argument value.
|
||||
DefaultValue string
|
||||
// BlankAllowed is the blankAllowed argument value.
|
||||
BlankAllowed bool
|
||||
}
|
||||
// MultiSelect holds details about calls to the MultiSelect method.
|
||||
MultiSelect []struct {
|
||||
|
|
@ -125,17 +125,17 @@ type PrompterMock struct {
|
|||
}
|
||||
// Password holds details about calls to the Password method.
|
||||
Password []struct {
|
||||
// S is the s argument value.
|
||||
S string
|
||||
// Prompt is the prompt argument value.
|
||||
Prompt string
|
||||
}
|
||||
// Select holds details about calls to the Select method.
|
||||
Select []struct {
|
||||
// S1 is the s1 argument value.
|
||||
S1 string
|
||||
// S2 is the s2 argument value.
|
||||
S2 string
|
||||
// Strings is the strings argument value.
|
||||
Strings []string
|
||||
// Prompt is the prompt argument value.
|
||||
Prompt string
|
||||
// DefaultValue is the defaultValue argument value.
|
||||
DefaultValue string
|
||||
// Options is the options argument value.
|
||||
Options []string
|
||||
}
|
||||
}
|
||||
lockAuthToken sync.RWMutex
|
||||
|
|
@ -177,21 +177,21 @@ func (mock *PrompterMock) AuthTokenCalls() []struct {
|
|||
}
|
||||
|
||||
// Confirm calls ConfirmFunc.
|
||||
func (mock *PrompterMock) Confirm(s string, b bool) (bool, error) {
|
||||
func (mock *PrompterMock) Confirm(prompt string, defaultValue bool) (bool, error) {
|
||||
if mock.ConfirmFunc == nil {
|
||||
panic("PrompterMock.ConfirmFunc: method is nil but Prompter.Confirm was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
S string
|
||||
B bool
|
||||
Prompt string
|
||||
DefaultValue bool
|
||||
}{
|
||||
S: s,
|
||||
B: b,
|
||||
Prompt: prompt,
|
||||
DefaultValue: defaultValue,
|
||||
}
|
||||
mock.lockConfirm.Lock()
|
||||
mock.calls.Confirm = append(mock.calls.Confirm, callInfo)
|
||||
mock.lockConfirm.Unlock()
|
||||
return mock.ConfirmFunc(s, b)
|
||||
return mock.ConfirmFunc(prompt, defaultValue)
|
||||
}
|
||||
|
||||
// ConfirmCalls gets all the calls that were made to Confirm.
|
||||
|
|
@ -199,12 +199,12 @@ func (mock *PrompterMock) Confirm(s string, b bool) (bool, error) {
|
|||
//
|
||||
// len(mockedPrompter.ConfirmCalls())
|
||||
func (mock *PrompterMock) ConfirmCalls() []struct {
|
||||
S string
|
||||
B bool
|
||||
Prompt string
|
||||
DefaultValue bool
|
||||
} {
|
||||
var calls []struct {
|
||||
S string
|
||||
B bool
|
||||
Prompt string
|
||||
DefaultValue bool
|
||||
}
|
||||
mock.lockConfirm.RLock()
|
||||
calls = mock.calls.Confirm
|
||||
|
|
@ -213,19 +213,19 @@ func (mock *PrompterMock) ConfirmCalls() []struct {
|
|||
}
|
||||
|
||||
// ConfirmDeletion calls ConfirmDeletionFunc.
|
||||
func (mock *PrompterMock) ConfirmDeletion(s string) error {
|
||||
func (mock *PrompterMock) ConfirmDeletion(requiredValue string) error {
|
||||
if mock.ConfirmDeletionFunc == nil {
|
||||
panic("PrompterMock.ConfirmDeletionFunc: method is nil but Prompter.ConfirmDeletion was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
S string
|
||||
RequiredValue string
|
||||
}{
|
||||
S: s,
|
||||
RequiredValue: requiredValue,
|
||||
}
|
||||
mock.lockConfirmDeletion.Lock()
|
||||
mock.calls.ConfirmDeletion = append(mock.calls.ConfirmDeletion, callInfo)
|
||||
mock.lockConfirmDeletion.Unlock()
|
||||
return mock.ConfirmDeletionFunc(s)
|
||||
return mock.ConfirmDeletionFunc(requiredValue)
|
||||
}
|
||||
|
||||
// ConfirmDeletionCalls gets all the calls that were made to ConfirmDeletion.
|
||||
|
|
@ -233,10 +233,10 @@ func (mock *PrompterMock) ConfirmDeletion(s string) error {
|
|||
//
|
||||
// len(mockedPrompter.ConfirmDeletionCalls())
|
||||
func (mock *PrompterMock) ConfirmDeletionCalls() []struct {
|
||||
S string
|
||||
RequiredValue string
|
||||
} {
|
||||
var calls []struct {
|
||||
S string
|
||||
RequiredValue string
|
||||
}
|
||||
mock.lockConfirmDeletion.RLock()
|
||||
calls = mock.calls.ConfirmDeletion
|
||||
|
|
@ -245,21 +245,21 @@ func (mock *PrompterMock) ConfirmDeletionCalls() []struct {
|
|||
}
|
||||
|
||||
// Input calls InputFunc.
|
||||
func (mock *PrompterMock) Input(s1 string, s2 string) (string, error) {
|
||||
func (mock *PrompterMock) Input(prompt string, defaultValue string) (string, error) {
|
||||
if mock.InputFunc == nil {
|
||||
panic("PrompterMock.InputFunc: method is nil but Prompter.Input was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
S1 string
|
||||
S2 string
|
||||
Prompt string
|
||||
DefaultValue string
|
||||
}{
|
||||
S1: s1,
|
||||
S2: s2,
|
||||
Prompt: prompt,
|
||||
DefaultValue: defaultValue,
|
||||
}
|
||||
mock.lockInput.Lock()
|
||||
mock.calls.Input = append(mock.calls.Input, callInfo)
|
||||
mock.lockInput.Unlock()
|
||||
return mock.InputFunc(s1, s2)
|
||||
return mock.InputFunc(prompt, defaultValue)
|
||||
}
|
||||
|
||||
// InputCalls gets all the calls that were made to Input.
|
||||
|
|
@ -267,12 +267,12 @@ func (mock *PrompterMock) Input(s1 string, s2 string) (string, error) {
|
|||
//
|
||||
// len(mockedPrompter.InputCalls())
|
||||
func (mock *PrompterMock) InputCalls() []struct {
|
||||
S1 string
|
||||
S2 string
|
||||
Prompt string
|
||||
DefaultValue string
|
||||
} {
|
||||
var calls []struct {
|
||||
S1 string
|
||||
S2 string
|
||||
Prompt string
|
||||
DefaultValue string
|
||||
}
|
||||
mock.lockInput.RLock()
|
||||
calls = mock.calls.Input
|
||||
|
|
@ -308,23 +308,23 @@ func (mock *PrompterMock) InputHostnameCalls() []struct {
|
|||
}
|
||||
|
||||
// MarkdownEditor calls MarkdownEditorFunc.
|
||||
func (mock *PrompterMock) MarkdownEditor(s1 string, s2 string, b bool) (string, error) {
|
||||
func (mock *PrompterMock) MarkdownEditor(prompt string, defaultValue string, blankAllowed bool) (string, error) {
|
||||
if mock.MarkdownEditorFunc == nil {
|
||||
panic("PrompterMock.MarkdownEditorFunc: method is nil but Prompter.MarkdownEditor was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
S1 string
|
||||
S2 string
|
||||
B bool
|
||||
Prompt string
|
||||
DefaultValue string
|
||||
BlankAllowed bool
|
||||
}{
|
||||
S1: s1,
|
||||
S2: s2,
|
||||
B: b,
|
||||
Prompt: prompt,
|
||||
DefaultValue: defaultValue,
|
||||
BlankAllowed: blankAllowed,
|
||||
}
|
||||
mock.lockMarkdownEditor.Lock()
|
||||
mock.calls.MarkdownEditor = append(mock.calls.MarkdownEditor, callInfo)
|
||||
mock.lockMarkdownEditor.Unlock()
|
||||
return mock.MarkdownEditorFunc(s1, s2, b)
|
||||
return mock.MarkdownEditorFunc(prompt, defaultValue, blankAllowed)
|
||||
}
|
||||
|
||||
// MarkdownEditorCalls gets all the calls that were made to MarkdownEditor.
|
||||
|
|
@ -332,14 +332,14 @@ func (mock *PrompterMock) MarkdownEditor(s1 string, s2 string, b bool) (string,
|
|||
//
|
||||
// len(mockedPrompter.MarkdownEditorCalls())
|
||||
func (mock *PrompterMock) MarkdownEditorCalls() []struct {
|
||||
S1 string
|
||||
S2 string
|
||||
B bool
|
||||
Prompt string
|
||||
DefaultValue string
|
||||
BlankAllowed bool
|
||||
} {
|
||||
var calls []struct {
|
||||
S1 string
|
||||
S2 string
|
||||
B bool
|
||||
Prompt string
|
||||
DefaultValue string
|
||||
BlankAllowed bool
|
||||
}
|
||||
mock.lockMarkdownEditor.RLock()
|
||||
calls = mock.calls.MarkdownEditor
|
||||
|
|
@ -388,19 +388,19 @@ func (mock *PrompterMock) MultiSelectCalls() []struct {
|
|||
}
|
||||
|
||||
// Password calls PasswordFunc.
|
||||
func (mock *PrompterMock) Password(s string) (string, error) {
|
||||
func (mock *PrompterMock) Password(prompt string) (string, error) {
|
||||
if mock.PasswordFunc == nil {
|
||||
panic("PrompterMock.PasswordFunc: method is nil but Prompter.Password was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
S string
|
||||
Prompt string
|
||||
}{
|
||||
S: s,
|
||||
Prompt: prompt,
|
||||
}
|
||||
mock.lockPassword.Lock()
|
||||
mock.calls.Password = append(mock.calls.Password, callInfo)
|
||||
mock.lockPassword.Unlock()
|
||||
return mock.PasswordFunc(s)
|
||||
return mock.PasswordFunc(prompt)
|
||||
}
|
||||
|
||||
// PasswordCalls gets all the calls that were made to Password.
|
||||
|
|
@ -408,10 +408,10 @@ func (mock *PrompterMock) Password(s string) (string, error) {
|
|||
//
|
||||
// len(mockedPrompter.PasswordCalls())
|
||||
func (mock *PrompterMock) PasswordCalls() []struct {
|
||||
S string
|
||||
Prompt string
|
||||
} {
|
||||
var calls []struct {
|
||||
S string
|
||||
Prompt string
|
||||
}
|
||||
mock.lockPassword.RLock()
|
||||
calls = mock.calls.Password
|
||||
|
|
@ -420,23 +420,23 @@ func (mock *PrompterMock) PasswordCalls() []struct {
|
|||
}
|
||||
|
||||
// Select calls SelectFunc.
|
||||
func (mock *PrompterMock) Select(s1 string, s2 string, strings []string) (int, error) {
|
||||
func (mock *PrompterMock) Select(prompt string, defaultValue string, options []string) (int, error) {
|
||||
if mock.SelectFunc == nil {
|
||||
panic("PrompterMock.SelectFunc: method is nil but Prompter.Select was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
S1 string
|
||||
S2 string
|
||||
Strings []string
|
||||
Prompt string
|
||||
DefaultValue string
|
||||
Options []string
|
||||
}{
|
||||
S1: s1,
|
||||
S2: s2,
|
||||
Strings: strings,
|
||||
Prompt: prompt,
|
||||
DefaultValue: defaultValue,
|
||||
Options: options,
|
||||
}
|
||||
mock.lockSelect.Lock()
|
||||
mock.calls.Select = append(mock.calls.Select, callInfo)
|
||||
mock.lockSelect.Unlock()
|
||||
return mock.SelectFunc(s1, s2, strings)
|
||||
return mock.SelectFunc(prompt, defaultValue, options)
|
||||
}
|
||||
|
||||
// SelectCalls gets all the calls that were made to Select.
|
||||
|
|
@ -444,14 +444,14 @@ func (mock *PrompterMock) Select(s1 string, s2 string, strings []string) (int, e
|
|||
//
|
||||
// len(mockedPrompter.SelectCalls())
|
||||
func (mock *PrompterMock) SelectCalls() []struct {
|
||||
S1 string
|
||||
S2 string
|
||||
Strings []string
|
||||
Prompt string
|
||||
DefaultValue string
|
||||
Options []string
|
||||
} {
|
||||
var calls []struct {
|
||||
S1 string
|
||||
S2 string
|
||||
Strings []string
|
||||
Prompt string
|
||||
DefaultValue string
|
||||
Options []string
|
||||
}
|
||||
mock.lockSelect.RLock()
|
||||
calls = mock.calls.Select
|
||||
|
|
|
|||
|
|
@ -141,6 +141,18 @@ func IndexFor(options []string, answer string) (int, error) {
|
|||
return -1, NoSuchAnswerErr(answer, options)
|
||||
}
|
||||
|
||||
func IndexesFor(options []string, answers ...string) ([]int, error) {
|
||||
indexes := make([]int, len(answers))
|
||||
for i, answer := range answers {
|
||||
index, err := IndexFor(options, answer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
indexes[i] = index
|
||||
}
|
||||
return indexes, nil
|
||||
}
|
||||
|
||||
func NoSuchPromptErr(prompt string) error {
|
||||
return fmt.Errorf("no such prompt '%s'", prompt)
|
||||
}
|
||||
|
|
|
|||
142
pkg/cmd/accessibility/accessibility.go
Normal file
142
pkg/cmd/accessibility/accessibility.go
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
package accessibility
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
webURL = "https://accessibility.github.com/conformance/cli/"
|
||||
)
|
||||
|
||||
type AccessibilityOptions struct {
|
||||
IO *iostreams.IOStreams
|
||||
Browser browser.Browser
|
||||
Web bool
|
||||
}
|
||||
|
||||
func NewCmdAccessibility(f *cmdutil.Factory) *cobra.Command {
|
||||
opts := AccessibilityOptions{
|
||||
IO: f.IOStreams,
|
||||
Browser: f.Browser,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "accessibility",
|
||||
Aliases: []string{"a11y"},
|
||||
Short: "Learn about GitHub CLI's accessibility experiences",
|
||||
Long: longDescription(opts.IO),
|
||||
Hidden: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if opts.Web {
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(webURL))
|
||||
}
|
||||
return opts.Browser.Browse(webURL)
|
||||
}
|
||||
|
||||
return cmd.Help()
|
||||
},
|
||||
Example: heredoc.Doc(`
|
||||
# Open the GitHub Accessibility site in your browser
|
||||
$ gh accessibility --web
|
||||
|
||||
# Display color using customizable, 4-bit accessible colors
|
||||
$ gh config set accessible_colors enabled
|
||||
|
||||
# Use input prompts without redrawing the screen
|
||||
$ gh config set accessible_prompter enabled
|
||||
|
||||
# Disable motion-based spinners for progress indicators in favor of text
|
||||
$ gh config set spinner disabled
|
||||
`),
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open the GitHub Accessibility site in your browser")
|
||||
cmdutil.DisableAuthCheck(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func longDescription(io *iostreams.IOStreams) string {
|
||||
cs := io.ColorScheme()
|
||||
title := cs.Bold("Learn about GitHub CLI's accessibility experiences")
|
||||
color := cs.Bold("Customizable and contrasting colors")
|
||||
prompter := cs.Bold("Non-interactive user input prompting")
|
||||
spinner := cs.Bold("Text-based spinners")
|
||||
feedback := cs.Bold("Join the conversation")
|
||||
|
||||
return heredoc.Docf(`
|
||||
%[2]s
|
||||
|
||||
As the home for all developers, we want every developer to feel welcome in our
|
||||
community and be empowered to contribute to the future of global software
|
||||
development with everything GitHub has to offer including the GitHub CLI.
|
||||
|
||||
%[3]s
|
||||
|
||||
Text interfaces often use color for various purposes, but insufficient contrast
|
||||
or customizability can leave some users unable to benefit.
|
||||
|
||||
For a more accessible experience, the GitHub CLI can use color palettes
|
||||
based on terminal background appearance and limit colors to 4-bit ANSI color
|
||||
palettes, which users can customize within terminal preferences.
|
||||
|
||||
With this new experience, the GitHub CLI provides multiple options to address
|
||||
color usage:
|
||||
|
||||
1. The GitHub CLI will use 4-bit color palette for increased color contrast based
|
||||
on dark and light backgrounds including rendering Markdown based on the
|
||||
GitHub Primer design system.
|
||||
|
||||
To enable this experience, use one of the following methods:
|
||||
- Run %[1]sgh config set accessible_colors enabled%[1]s
|
||||
- Set %[1]sGH_ACCESSIBLE_COLORS=enabled%[1]s environment variable
|
||||
|
||||
2. The GitHub CLI will display issue and pull request labels' custom RGB colors
|
||||
in terminals with true color support.
|
||||
|
||||
To enable this experience, use one of the following methods:
|
||||
- Run %[1]sgh config set color_labels enabled%[1]s
|
||||
- Set %[1]sGH_COLOR_LABELS=enabled%[1]s environment variable
|
||||
|
||||
%[4]s
|
||||
|
||||
Interactive text user interfaces manipulate the terminal cursor to redraw parts
|
||||
of the screen, which can be difficult for speech synthesizers or braille displays
|
||||
to accurately detect and read.
|
||||
|
||||
For a more accessible experience, the GitHub CLI can provide a similar experience using
|
||||
non-interactive prompts for user input.
|
||||
|
||||
To enable this experience, use one of the following methods:
|
||||
- Run %[1]sgh config set accessible_prompter enabled%[1]s
|
||||
- Set %[1]sGH_ACCESSIBLE_PROMPTER=enabled%[1]s environment variable
|
||||
|
||||
%[5]s
|
||||
|
||||
Motion-based spinners communicate in-progress activity by manipulating the
|
||||
terminal cursor to create a spinning effect, which may cause discomfort to users
|
||||
with motion sensitivity or miscommunicate information to speech synthesizers.
|
||||
|
||||
For a more accessible experience, this interactivity can be disabled in favor
|
||||
of text-based progress indicators.
|
||||
|
||||
To enable this experience, use one of the following methods:
|
||||
- Run %[1]sgh config set spinner disabled%[1]s
|
||||
- Set %[1]sGH_SPINNER_DISABLED=yes%[1]s environment variable
|
||||
|
||||
%[6]s
|
||||
|
||||
We invite you to join us in improving GitHub CLI accessibility by sharing your
|
||||
feedback and ideas through GitHub Accessibility feedback channels:
|
||||
|
||||
%[7]s
|
||||
`, "`", title, color, prompter, spinner, feedback, webURL)
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/sigstore/sigstore-go/pkg/bundle"
|
||||
)
|
||||
|
||||
|
|
@ -20,3 +23,35 @@ type Attestation struct {
|
|||
type AttestationsResponse struct {
|
||||
Attestations []*Attestation `json:"attestations"`
|
||||
}
|
||||
|
||||
type IntotoStatement struct {
|
||||
PredicateType string `json:"predicateType"`
|
||||
}
|
||||
|
||||
func FilterAttestations(predicateType string, attestations []*Attestation) ([]*Attestation, error) {
|
||||
filteredAttestations := []*Attestation{}
|
||||
|
||||
for _, each := range attestations {
|
||||
dsseEnvelope := each.Bundle.GetDsseEnvelope()
|
||||
if dsseEnvelope != nil {
|
||||
if dsseEnvelope.PayloadType != "application/vnd.in-toto+json" {
|
||||
// Don't fail just because an entry isn't intoto
|
||||
continue
|
||||
}
|
||||
var intotoStatement IntotoStatement
|
||||
if err := json.Unmarshal([]byte(dsseEnvelope.Payload), &intotoStatement); err != nil {
|
||||
// Don't fail just because a single entry can't be unmarshalled
|
||||
continue
|
||||
}
|
||||
if intotoStatement.PredicateType == predicateType {
|
||||
filteredAttestations = append(filteredAttestations, each)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(filteredAttestations) == 0 {
|
||||
return nil, fmt.Errorf("no attestations found with predicate type: %s", predicateType)
|
||||
}
|
||||
|
||||
return filteredAttestations, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,28 @@ const (
|
|||
// Allow injecting backoff interval in tests.
|
||||
var getAttestationRetryInterval = time.Millisecond * 200
|
||||
|
||||
// FetchParams are the parameters for fetching attestations from the GitHub API
|
||||
type FetchParams struct {
|
||||
Digest string
|
||||
Limit int
|
||||
Owner string
|
||||
PredicateType string
|
||||
Repo string
|
||||
}
|
||||
|
||||
func (p *FetchParams) Validate() error {
|
||||
if p.Digest == "" {
|
||||
return fmt.Errorf("digest must be provided")
|
||||
}
|
||||
if p.Limit <= 0 || p.Limit > maxLimitForFlag {
|
||||
return fmt.Errorf("limit must be greater than 0 and less than or equal to %d", maxLimitForFlag)
|
||||
}
|
||||
if p.Repo == "" && p.Owner == "" {
|
||||
return fmt.Errorf("owner or repo must be provided")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// githubApiClient makes REST calls to the GitHub API
|
||||
type githubApiClient interface {
|
||||
REST(hostname, method, p string, body io.Reader, data interface{}) error
|
||||
|
|
@ -39,8 +61,7 @@ type httpClient interface {
|
|||
}
|
||||
|
||||
type Client interface {
|
||||
GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error)
|
||||
GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error)
|
||||
GetByDigest(params FetchParams) ([]*Attestation, error)
|
||||
GetTrustDomain() (string, error)
|
||||
}
|
||||
|
||||
|
|
@ -60,22 +81,11 @@ func NewLiveClient(hc *http.Client, host string, l *ioconfig.Handler) *LiveClien
|
|||
}
|
||||
}
|
||||
|
||||
// GetByRepoAndDigest fetches the attestation by repo and digest
|
||||
func (c *LiveClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) {
|
||||
c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", digest)
|
||||
url := fmt.Sprintf(GetAttestationByRepoAndSubjectDigestPath, repo, digest)
|
||||
return c.getByURL(url, limit)
|
||||
}
|
||||
|
||||
// GetByOwnerAndDigest fetches attestation by owner and digest
|
||||
func (c *LiveClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) {
|
||||
c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", digest)
|
||||
url := fmt.Sprintf(GetAttestationByOwnerAndSubjectDigestPath, owner, digest)
|
||||
return c.getByURL(url, limit)
|
||||
}
|
||||
|
||||
func (c *LiveClient) getByURL(url string, limit int) ([]*Attestation, error) {
|
||||
attestations, err := c.getAttestations(url, limit)
|
||||
// GetByDigest fetches the attestation by digest and either owner or repo
|
||||
// depending on which is provided
|
||||
func (c *LiveClient) GetByDigest(params FetchParams) ([]*Attestation, error) {
|
||||
c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", params.Digest)
|
||||
attestations, err := c.getAttestations(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -88,40 +98,52 @@ func (c *LiveClient) getByURL(url string, limit int) ([]*Attestation, error) {
|
|||
return bundles, nil
|
||||
}
|
||||
|
||||
// GetTrustDomain returns the current trust domain. If the default is used
|
||||
// the empty string is returned
|
||||
func (c *LiveClient) GetTrustDomain() (string, error) {
|
||||
return c.getTrustDomain(MetaPath)
|
||||
}
|
||||
|
||||
func (c *LiveClient) getAttestations(url string, limit int) ([]*Attestation, error) {
|
||||
perPage := limit
|
||||
if perPage <= 0 || perPage > maxLimitForFlag {
|
||||
return nil, fmt.Errorf("limit must be greater than 0 and less than or equal to %d", maxLimitForFlag)
|
||||
func (c *LiveClient) buildRequestURL(params FetchParams) (string, error) {
|
||||
if err := params.Validate(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var url string
|
||||
if params.Repo != "" {
|
||||
// check if Repo is set first because if Repo has been set, Owner will be set using the value of Repo.
|
||||
// If Repo is not set, the field will remain empty. It will not be populated using the value of Owner.
|
||||
url = fmt.Sprintf(GetAttestationByRepoAndSubjectDigestPath, params.Repo, params.Digest)
|
||||
} else {
|
||||
url = fmt.Sprintf(GetAttestationByOwnerAndSubjectDigestPath, params.Owner, params.Digest)
|
||||
}
|
||||
|
||||
perPage := params.Limit
|
||||
if perPage > maxLimitForFetch {
|
||||
perPage = maxLimitForFetch
|
||||
}
|
||||
|
||||
// ref: https://github.com/cli/go-gh/blob/d32c104a9a25c9de3d7c7b07a43ae0091441c858/example_gh_test.go#L96
|
||||
url = fmt.Sprintf("%s?per_page=%d", url, perPage)
|
||||
if params.PredicateType != "" {
|
||||
url = fmt.Sprintf("%s&predicate_type=%s", url, params.PredicateType)
|
||||
}
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func (c *LiveClient) getAttestations(params FetchParams) ([]*Attestation, error) {
|
||||
url, err := c.buildRequestURL(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var attestations []*Attestation
|
||||
var resp AttestationsResponse
|
||||
bo := backoff.NewConstantBackOff(getAttestationRetryInterval)
|
||||
|
||||
// if no attestation or less than limit, then keep fetching
|
||||
for url != "" && len(attestations) < limit {
|
||||
for url != "" && len(attestations) < params.Limit {
|
||||
err := backoff.Retry(func() error {
|
||||
newURL, restErr := c.githubAPI.RESTWithNext(c.host, http.MethodGet, url, nil, &resp)
|
||||
|
||||
if restErr != nil {
|
||||
if shouldRetry(restErr) {
|
||||
return restErr
|
||||
} else {
|
||||
return backoff.Permanent(restErr)
|
||||
}
|
||||
return backoff.Permanent(restErr)
|
||||
}
|
||||
|
||||
url = newURL
|
||||
|
|
@ -140,8 +162,8 @@ func (c *LiveClient) getAttestations(url string, limit int) ([]*Attestation, err
|
|||
return nil, ErrNoAttestationsFound
|
||||
}
|
||||
|
||||
if len(attestations) > limit {
|
||||
return attestations[:limit], nil
|
||||
if len(attestations) > params.Limit {
|
||||
return attestations[:params.Limit], nil
|
||||
}
|
||||
|
||||
return attestations, nil
|
||||
|
|
@ -241,6 +263,12 @@ func shouldRetry(err error) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// GetTrustDomain returns the current trust domain. If the default is used
|
||||
// the empty string is returned
|
||||
func (c *LiveClient) GetTrustDomain() (string, error) {
|
||||
return c.getTrustDomain(MetaPath)
|
||||
}
|
||||
|
||||
func (c *LiveClient) getTrustDomain(url string) (string, error) {
|
||||
var resp MetaResponse
|
||||
|
||||
|
|
|
|||
|
|
@ -42,78 +42,75 @@ func NewClientWithMockGHClient(hasNextPage bool) Client {
|
|||
}
|
||||
}
|
||||
|
||||
var testFetchParamsWithOwner = FetchParams{
|
||||
Digest: testDigest,
|
||||
Limit: DefaultLimit,
|
||||
Owner: testOwner,
|
||||
PredicateType: "https://slsa.dev/provenance/v1",
|
||||
}
|
||||
var testFetchParamsWithRepo = FetchParams{
|
||||
Digest: testDigest,
|
||||
Limit: DefaultLimit,
|
||||
Repo: testRepo,
|
||||
PredicateType: "https://slsa.dev/provenance/v1",
|
||||
}
|
||||
|
||||
type getByTestCase struct {
|
||||
name string
|
||||
params FetchParams
|
||||
limit int
|
||||
expectedAttestations int
|
||||
hasNextPage bool
|
||||
}
|
||||
|
||||
var getByTestCases = []getByTestCase{
|
||||
{
|
||||
name: "get by digest with owner",
|
||||
params: testFetchParamsWithOwner,
|
||||
expectedAttestations: 5,
|
||||
},
|
||||
{
|
||||
name: "get by digest with repo",
|
||||
params: testFetchParamsWithRepo,
|
||||
expectedAttestations: 5,
|
||||
},
|
||||
{
|
||||
name: "get by digest with attestations greater than limit",
|
||||
params: testFetchParamsWithRepo,
|
||||
limit: 3,
|
||||
expectedAttestations: 3,
|
||||
},
|
||||
{
|
||||
name: "get by digest with next page",
|
||||
params: testFetchParamsWithRepo,
|
||||
expectedAttestations: 10,
|
||||
hasNextPage: true,
|
||||
},
|
||||
{
|
||||
name: "greater than limit with next page",
|
||||
params: testFetchParamsWithRepo,
|
||||
limit: 7,
|
||||
expectedAttestations: 7,
|
||||
hasNextPage: true,
|
||||
},
|
||||
}
|
||||
|
||||
func TestGetByDigest(t *testing.T) {
|
||||
c := NewClientWithMockGHClient(false)
|
||||
attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit)
|
||||
require.NoError(t, err)
|
||||
for _, tc := range getByTestCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := NewClientWithMockGHClient(tc.hasNextPage)
|
||||
|
||||
require.Equal(t, 5, len(attestations))
|
||||
bundle := (attestations)[0].Bundle
|
||||
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle.v0.3+json")
|
||||
if tc.limit > 0 {
|
||||
tc.params.Limit = tc.limit
|
||||
}
|
||||
attestations, err := c.GetByDigest(tc.params)
|
||||
require.NoError(t, err)
|
||||
|
||||
attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 5, len(attestations))
|
||||
bundle = (attestations)[0].Bundle
|
||||
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle.v0.3+json")
|
||||
}
|
||||
|
||||
func TestGetByDigestGreaterThanLimit(t *testing.T) {
|
||||
c := NewClientWithMockGHClient(false)
|
||||
|
||||
limit := 3
|
||||
// The method should return five results when the limit is not set
|
||||
attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, limit)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 3, len(attestations))
|
||||
bundle := (attestations)[0].Bundle
|
||||
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle.v0.3+json")
|
||||
|
||||
attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, limit)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, len(attestations), limit)
|
||||
bundle = (attestations)[0].Bundle
|
||||
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle.v0.3+json")
|
||||
}
|
||||
|
||||
func TestGetByDigestWithNextPage(t *testing.T) {
|
||||
c := NewClientWithMockGHClient(true)
|
||||
attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, len(attestations), 10)
|
||||
bundle := (attestations)[0].Bundle
|
||||
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle.v0.3+json")
|
||||
|
||||
attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, len(attestations), 10)
|
||||
bundle = (attestations)[0].Bundle
|
||||
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle.v0.3+json")
|
||||
}
|
||||
|
||||
func TestGetByDigestGreaterThanLimitWithNextPage(t *testing.T) {
|
||||
c := NewClientWithMockGHClient(true)
|
||||
|
||||
limit := 7
|
||||
// The method should return five results when the limit is not set
|
||||
attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, limit)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, len(attestations), limit)
|
||||
bundle := (attestations)[0].Bundle
|
||||
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle.v0.3+json")
|
||||
|
||||
attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, limit)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, len(attestations), limit)
|
||||
bundle = (attestations)[0].Bundle
|
||||
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle.v0.3+json")
|
||||
require.Equal(t, tc.expectedAttestations, len(attestations))
|
||||
bundle := (attestations)[0].Bundle
|
||||
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle.v0.3+json")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetByDigest_NoAttestationsFound(t *testing.T) {
|
||||
|
|
@ -130,12 +127,7 @@ func TestGetByDigest_NoAttestationsFound(t *testing.T) {
|
|||
logger: io.NewTestHandler(),
|
||||
}
|
||||
|
||||
attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit)
|
||||
require.Error(t, err)
|
||||
require.IsType(t, ErrNoAttestationsFound, err)
|
||||
require.Nil(t, attestations)
|
||||
|
||||
attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit)
|
||||
attestations, err := c.GetByDigest(testFetchParamsWithRepo)
|
||||
require.Error(t, err)
|
||||
require.IsType(t, ErrNoAttestationsFound, err)
|
||||
require.Nil(t, attestations)
|
||||
|
|
@ -153,11 +145,7 @@ func TestGetByDigest_Error(t *testing.T) {
|
|||
logger: io.NewTestHandler(),
|
||||
}
|
||||
|
||||
attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, attestations)
|
||||
|
||||
attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit)
|
||||
attestations, err := c.GetByDigest(testFetchParamsWithRepo)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, attestations)
|
||||
}
|
||||
|
|
@ -362,7 +350,8 @@ func TestGetAttestationsRetries(t *testing.T) {
|
|||
logger: io.NewTestHandler(),
|
||||
}
|
||||
|
||||
attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit)
|
||||
testFetchParamsWithRepo.Limit = 30
|
||||
attestations, err := c.GetByDigest(testFetchParamsWithRepo)
|
||||
require.NoError(t, err)
|
||||
|
||||
// assert the error path was executed; because this is a paged
|
||||
|
|
@ -373,17 +362,6 @@ func TestGetAttestationsRetries(t *testing.T) {
|
|||
require.Equal(t, len(attestations), 10)
|
||||
bundle := (attestations)[0].Bundle
|
||||
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle.v0.3+json")
|
||||
|
||||
// same test as above, but for GetByOwnerAndDigest:
|
||||
attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit)
|
||||
require.NoError(t, err)
|
||||
|
||||
// because we haven't reset the mock, we have added 2 more failed requests
|
||||
fetcher.AssertNumberOfCalls(t, "FlakyOnRESTSuccessWithNextPage:error", 4)
|
||||
|
||||
require.Equal(t, len(attestations), 10)
|
||||
bundle = (attestations)[0].Bundle
|
||||
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle.v0.3+json")
|
||||
}
|
||||
|
||||
// test total retries
|
||||
|
|
@ -401,7 +379,7 @@ func TestGetAttestationsMaxRetries(t *testing.T) {
|
|||
logger: io.NewTestHandler(),
|
||||
}
|
||||
|
||||
_, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit)
|
||||
_, err := c.GetByDigest(testFetchParamsWithRepo)
|
||||
require.Error(t, err)
|
||||
|
||||
fetcher.AssertNumberOfCalls(t, "OnREST500Error", 4)
|
||||
|
|
|
|||
|
|
@ -6,58 +6,60 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/cmd/attestation/test/data"
|
||||
)
|
||||
|
||||
type MockClient struct {
|
||||
OnGetByRepoAndDigest func(repo, digest string, limit int) ([]*Attestation, error)
|
||||
OnGetByOwnerAndDigest func(owner, digest string, limit int) ([]*Attestation, error)
|
||||
OnGetTrustDomain func() (string, error)
|
||||
}
|
||||
|
||||
func (m MockClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) {
|
||||
return m.OnGetByRepoAndDigest(repo, digest, limit)
|
||||
}
|
||||
|
||||
func (m MockClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) {
|
||||
return m.OnGetByOwnerAndDigest(owner, digest, limit)
|
||||
}
|
||||
|
||||
func (m MockClient) GetTrustDomain() (string, error) {
|
||||
return m.OnGetTrustDomain()
|
||||
func makeTestReleaseAttestation() Attestation {
|
||||
return Attestation{
|
||||
Bundle: data.GitHubReleaseBundle(nil),
|
||||
BundleURL: "https://example.com",
|
||||
}
|
||||
}
|
||||
|
||||
func makeTestAttestation() Attestation {
|
||||
return Attestation{Bundle: data.SigstoreBundle(nil), BundleURL: "https://example.com"}
|
||||
}
|
||||
|
||||
func OnGetByRepoAndDigestSuccess(repo, digest string, limit int) ([]*Attestation, error) {
|
||||
type MockClient struct {
|
||||
OnGetByDigest func(params FetchParams) ([]*Attestation, error)
|
||||
OnGetTrustDomain func() (string, error)
|
||||
}
|
||||
|
||||
func (m MockClient) GetByDigest(params FetchParams) ([]*Attestation, error) {
|
||||
return m.OnGetByDigest(params)
|
||||
}
|
||||
|
||||
func (m MockClient) GetTrustDomain() (string, error) {
|
||||
return m.OnGetTrustDomain()
|
||||
}
|
||||
|
||||
func OnGetByDigestSuccess(params FetchParams) ([]*Attestation, error) {
|
||||
att1 := makeTestAttestation()
|
||||
att2 := makeTestAttestation()
|
||||
return []*Attestation{&att1, &att2}, nil
|
||||
att3 := makeTestReleaseAttestation()
|
||||
attestations := []*Attestation{&att1, &att2}
|
||||
if params.PredicateType != "" {
|
||||
if params.PredicateType == "https://in-toto.io/attestation/release/v0.1" {
|
||||
attestations = append(attestations, &att3)
|
||||
}
|
||||
return FilterAttestations(params.PredicateType, attestations)
|
||||
}
|
||||
|
||||
return attestations, nil
|
||||
}
|
||||
|
||||
func OnGetByRepoAndDigestFailure(repo, digest string, limit int) ([]*Attestation, error) {
|
||||
return nil, fmt.Errorf("failed to fetch by repo and digest")
|
||||
}
|
||||
|
||||
func OnGetByOwnerAndDigestSuccess(owner, digest string, limit int) ([]*Attestation, error) {
|
||||
att1 := makeTestAttestation()
|
||||
att2 := makeTestAttestation()
|
||||
return []*Attestation{&att1, &att2}, nil
|
||||
}
|
||||
|
||||
func OnGetByOwnerAndDigestFailure(owner, digest string, limit int) ([]*Attestation, error) {
|
||||
return nil, fmt.Errorf("failed to fetch by owner and digest")
|
||||
func OnGetByDigestFailure(params FetchParams) ([]*Attestation, error) {
|
||||
if params.Repo != "" {
|
||||
return nil, fmt.Errorf("failed to fetch attestations from %s", params.Repo)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to fetch attestations from %s", params.Owner)
|
||||
}
|
||||
|
||||
func NewTestClient() *MockClient {
|
||||
return &MockClient{
|
||||
OnGetByRepoAndDigest: OnGetByRepoAndDigestSuccess,
|
||||
OnGetByOwnerAndDigest: OnGetByOwnerAndDigestSuccess,
|
||||
OnGetByDigest: OnGetByDigestSuccess,
|
||||
}
|
||||
}
|
||||
|
||||
func NewFailTestClient() *MockClient {
|
||||
return &MockClient{
|
||||
OnGetByRepoAndDigest: OnGetByRepoAndDigestFailure,
|
||||
OnGetByOwnerAndDigest: OnGetByOwnerAndDigestFailure,
|
||||
OnGetByDigest: OnGetByDigestFailure,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,13 @@ func normalizeReference(reference string, pathSeparator rune) (normalized string
|
|||
return filepath.Clean(reference), fileArtifactType, nil
|
||||
}
|
||||
|
||||
func NewDigestedArtifactForRelease(digest string, digestAlg string) (artifact *DigestedArtifact) {
|
||||
return &DigestedArtifact{
|
||||
digest: digest,
|
||||
digestAlg: digestAlg,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDigestedArtifact(client oci.Client, reference, digestAlg string) (artifact *DigestedArtifact, err error) {
|
||||
normalized, artifactType, err := normalizeReference(reference, os.PathSeparator)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
func digestLocalFileArtifact(filename, digestAlg string) (*DigestedArtifact, error) {
|
||||
data, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get open local artifact: %v", err)
|
||||
return nil, fmt.Errorf("failed to open local artifact: %v", err)
|
||||
}
|
||||
defer data.Close()
|
||||
digest, err := digest.CalculateDigestWithAlgorithm(data, digestAlg)
|
||||
|
|
|
|||
23
pkg/cmd/attestation/artifact/file_test.go
Normal file
23
pkg/cmd/attestation/artifact/file_test.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package artifact
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_digestLocalFileArtifact_withRealZip(t *testing.T) {
|
||||
// Path to the test artifact
|
||||
artifactPath := test.NormalizeRelativePath("../../attestation/test/data/github_release_artifact.zip")
|
||||
|
||||
// Calculate expected digest using the same algorithm as the function under test
|
||||
expectedDigest := "e15b593c6ab8d7725a3cc82226ef816cac6bf9c70eed383bd459295cc65f5ec3"
|
||||
|
||||
// Call the function under test
|
||||
artifact, err := digestLocalFileArtifact(artifactPath, "sha256")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "file://"+artifactPath, artifact.URL)
|
||||
require.Equal(t, expectedDigest, artifact.digest)
|
||||
require.Equal(t, "sha256", artifact.digestAlg)
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/auth"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
ghauth "github.com/cli/go-gh/v2/pkg/auth"
|
||||
|
||||
|
|
@ -127,13 +126,16 @@ func runDownload(opts *Options) error {
|
|||
|
||||
opts.Logger.VerbosePrintf("Downloading trusted metadata for artifact %s\n\n", opts.ArtifactPath)
|
||||
|
||||
params := verification.FetchRemoteAttestationsParams{
|
||||
if opts.APIClient == nil {
|
||||
return fmt.Errorf("no APIClient provided")
|
||||
}
|
||||
params := api.FetchParams{
|
||||
Digest: artifact.DigestWithAlg(),
|
||||
Limit: opts.Limit,
|
||||
Owner: opts.Owner,
|
||||
Repo: opts.Repo,
|
||||
}
|
||||
attestations, err := verification.GetRemoteAttestations(opts.APIClient, params)
|
||||
attestations, err := opts.APIClient.GetByDigest(params)
|
||||
if err != nil {
|
||||
if errors.Is(err, api.ErrNoAttestationsFound) {
|
||||
fmt.Fprintf(opts.Logger.IO.Out, "No attestations found for %s\n", opts.ArtifactPath)
|
||||
|
|
@ -144,10 +146,9 @@ func runDownload(opts *Options) error {
|
|||
|
||||
// Apply predicate type filter to returned attestations
|
||||
if opts.PredicateType != "" {
|
||||
filteredAttestations := verification.FilterAttestations(opts.PredicateType, attestations)
|
||||
|
||||
if len(filteredAttestations) == 0 {
|
||||
return fmt.Errorf("no attestations found with predicate type: %s", opts.PredicateType)
|
||||
filteredAttestations, err := api.FilterAttestations(opts.PredicateType, attestations)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to filter attestations: %v", err)
|
||||
}
|
||||
|
||||
attestations = filteredAttestations
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ func TestRunDownload(t *testing.T) {
|
|||
t.Run("no attestations found", func(t *testing.T) {
|
||||
opts := baseOpts
|
||||
opts.APIClient = api.MockClient{
|
||||
OnGetByOwnerAndDigest: func(repo, digest string, limit int) ([]*api.Attestation, error) {
|
||||
OnGetByDigest: func(params api.FetchParams) ([]*api.Attestation, error) {
|
||||
return nil, api.ErrNoAttestationsFound
|
||||
},
|
||||
}
|
||||
|
|
@ -291,7 +291,7 @@ func TestRunDownload(t *testing.T) {
|
|||
t.Run("failed to fetch attestations", func(t *testing.T) {
|
||||
opts := baseOpts
|
||||
opts.APIClient = api.MockClient{
|
||||
OnGetByOwnerAndDigest: func(repo, digest string, limit int) ([]*api.Attestation, error) {
|
||||
OnGetByDigest: func(params api.FetchParams) ([]*api.Attestation, error) {
|
||||
return nil, fmt.Errorf("failed to fetch attestations")
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,13 +77,18 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
|
|||
opts.Hostname, _ = ghauth.DefaultHost()
|
||||
}
|
||||
|
||||
err := auth.IsHostSupported(opts.Hostname)
|
||||
if err := auth.IsHostSupported(opts.Hostname); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hc, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := verification.SigstoreConfig{
|
||||
Logger: opts.Logger,
|
||||
HttpClient: hc,
|
||||
Logger: opts.Logger,
|
||||
}
|
||||
|
||||
if ghauth.IsTenancy(opts.Hostname) {
|
||||
|
|
|
|||
45
pkg/cmd/attestation/inspect/inspect_integration_test.go
Normal file
45
pkg/cmd/attestation/inspect/inspect_integration_test.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
//go:build integration
|
||||
|
||||
package inspect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewInspectCmd_PrintOutputJSONFormat(t *testing.T) {
|
||||
testIO, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: testIO,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return http.DefaultClient, nil
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("Print output in JSON format", func(t *testing.T) {
|
||||
var opts *Options
|
||||
cmd := NewInspectCmd(f, func(o *Options) error {
|
||||
opts = o
|
||||
return nil
|
||||
})
|
||||
|
||||
argv := strings.Split(fmt.Sprintf("%s --format json", bundlePath), " ")
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
_, err := cmd.ExecuteC()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, bundlePath, opts.BundlePath)
|
||||
assert.NotNil(t, opts.Logger)
|
||||
assert.NotNil(t, opts.exporter)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,11 +1,7 @@
|
|||
package inspect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
|
||||
|
|
@ -14,7 +10,6 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -26,66 +21,7 @@ const (
|
|||
SigstoreSanRegex = "^https://github.com/sigstore/sigstore-js/"
|
||||
)
|
||||
|
||||
var (
|
||||
bundlePath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json")
|
||||
)
|
||||
|
||||
func TestNewInspectCmd(t *testing.T) {
|
||||
testIO, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: testIO,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
reg := &httpmock.Registry{}
|
||||
client := &http.Client{}
|
||||
httpmock.ReplaceTripper(client, reg)
|
||||
return client, nil
|
||||
},
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants Options
|
||||
wantsErr bool
|
||||
wantsExporter bool
|
||||
}{
|
||||
{
|
||||
name: "Prints output in JSON format",
|
||||
cli: fmt.Sprintf("%s --format json", bundlePath),
|
||||
wants: Options{
|
||||
BundlePath: bundlePath,
|
||||
SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
|
||||
},
|
||||
wantsExporter: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var opts *Options
|
||||
cmd := NewInspectCmd(f, func(o *Options) error {
|
||||
opts = o
|
||||
return nil
|
||||
})
|
||||
|
||||
argv := strings.Split(tc.cli, " ")
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
_, err := cmd.ExecuteC()
|
||||
if tc.wantsErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.wants.BundlePath, opts.BundlePath)
|
||||
assert.NotNil(t, opts.Logger)
|
||||
assert.Equal(t, tc.wantsExporter, opts.exporter != nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
var bundlePath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json")
|
||||
|
||||
func TestRunInspect(t *testing.T) {
|
||||
opts := Options{
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import (
|
|||
//go:embed sigstore-js-2.1.0-bundle.json
|
||||
var SigstoreBundleRaw []byte
|
||||
|
||||
//go:embed github_release_bundle.json
|
||||
var GitHubReleaseBundleRaw []byte
|
||||
|
||||
// SigstoreBundle returns a test sigstore-go bundle.Bundle
|
||||
func SigstoreBundle(t *testing.T) *bundle.Bundle {
|
||||
b := &bundle.Bundle{}
|
||||
|
|
@ -19,3 +22,12 @@ func SigstoreBundle(t *testing.T) *bundle.Bundle {
|
|||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func GitHubReleaseBundle(t *testing.T) *bundle.Bundle {
|
||||
b := &bundle.Bundle{}
|
||||
err := b.UnmarshalJSON(GitHubReleaseBundleRaw)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unmarshal GitHub release bundle: %v", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
|
|
|||
BIN
pkg/cmd/attestation/test/data/github_release_artifact.zip
Normal file
BIN
pkg/cmd/attestation/test/data/github_release_artifact.zip
Normal file
Binary file not shown.
Binary file not shown.
24
pkg/cmd/attestation/test/data/github_release_bundle.json
Normal file
24
pkg/cmd/attestation/test/data/github_release_bundle.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
|
||||
"verificationMaterial": {
|
||||
"timestampVerificationData": {
|
||||
"rfc3161Timestamps": [
|
||||
{
|
||||
"signedTimestamp": "MIIC0DADAgEAMIICxwYJKoZIhvcNAQcCoIICuDCCArQCAQMxDTALBglghkgBZQMEAgIwgbwGCyqGSIb3DQEJEAEEoIGsBIGpMIGmAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQg1c3kQQpo4Adf2E+nx78lNg8EjRSLpIRERpPF0HIavogCFQCOfZuxr0DOc1LsM+y+sjQCMFrtbxgPMjAyNTA1MzAyMDEzMzlaMAMCAQGgNqQ0MDIxFTATBgNVBAoTDEdpdEh1YiwgSW5jLjEZMBcGA1UEAxMQVFNBIFRpbWVzdGFtcGluZ6AAMYIB3TCCAdkCAQEwSjAyMRUwEwYDVQQKEwxHaXRIdWIsIEluYy4xGTAXBgNVBAMTEFRTQSBpbnRlcm1lZGlhdGUCFB+7MIjE5/rL4XA4fNDnmXHA04+wMAsGCWCGSAFlAwQCAqCCAQUwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNTA1MzAyMDEzMzlaMD8GCSqGSIb3DQEJBDEyBDDkw1fXMZ6l/uWne+PcdzhLl2ckTZftcUuHcnYCwjhyYMeGOcgbpNNMDem46JCxItwwgYcGCyqGSIb3DQEJEAIvMXgwdjB0MHIEIHuISsKSyiJtlhGjT+RyS+tYQ7iwCMsMCTGmz2NK3D7DME4wNqQ0MDIxFTATBgNVBAoTDEdpdEh1YiwgSW5jLjEZMBcGA1UEAxMQVFNBIGludGVybWVkaWF0ZQIUH7swiMTn+svhcDh80OeZccDTj7AwCgYIKoZIzj0EAwMEZjBkAjBhE76zis18/xOtQdx6rJUuaRoZCflXHCjH6BqEk1B29r9C8STztZhAKalXL+Wy4rsCMFFaGPKF1uOl5JADiKMg5/7chJbWrfwyO9oa0tbmvcGrtBCdFeJ1Ic0tIi1sOVvq5Q=="
|
||||
}
|
||||
]
|
||||
},
|
||||
"certificate": {
|
||||
"rawBytes": "MIICKjCCAbCgAwIBAgIUaa62dj98DUB+TpyvKtVaR4vGSM0wCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZGdWxjaW8gSW50ZXJtZWRpYXRlIGwxMB4XDTI1MDMxMDE1MDMwMloXDTI2MDMxMDE1MDMwMlowKjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMREwDwYDVQQDEwhBdHRlc3RlcjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIMB7plPnZvBRlC2lvAocKTAqAPMJqstEqYk26e9vDJDC1yqoiHxZfPV4W/1RqUMZD1dFKm9t4RiSmm73/QnQKajgaUwgaIwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFOqaGpr5SbdYk5CQXsmmDZCBHR+XMB8GA1UdIwQYMBaAFMDhuFKkS08+3no4EQbPSY6hRZszMC0GA1UdEQQmMCSGImh0dHBzOi8vZG90Y29tLnJlbGVhc2VzLmdpdGh1Yi5jb20wCgYIKoZIzj0EAwMDaAAwZQIwWFdF6xcXazHVPHEAtd1SeaizLdY1erRl5hK+XlwhfpnasQHHZ9bdu4Zj8ARhW/AhAjEArujhmJGo7Fi4/Ek1RN8bufs6UhIQneQd/pxE8QdorwZkj2C8nf2EzrUYzlxKfktC"
|
||||
}
|
||||
},
|
||||
"dsseEnvelope": {
|
||||
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCAic3ViamVjdCI6W3sidXJpIjoicGtnOmdpdGh1Yi9iZGVoYW1lci9kZWxtZUB2NiIsICJkaWdlc3QiOnsic2hhMSI6ImM1ZTE3YTYyZTA2YTFkMjAxNTcwMjQ5YzYxZmFlNTMxZTkyNDRlMWIifX0sIHsibmFtZSI6ImFydGlmYWN0LnppcCIsICJkaWdlc3QiOnsic2hhMjU2IjoiZTE1YjU5M2M2YWI4ZDc3MjVhM2NjODIyMjZlZjgxNmNhYzZiZjljNzBlZWQzODNiZDQ1OTI5NWNjNjVmNWVjMyJ9fV0sICJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9pbi10b3RvLmlvL2F0dGVzdGF0aW9uL3JlbGVhc2UvdjAuMSIsICJwcmVkaWNhdGUiOnsib3duZXJJZCI6IjM5ODAyNyIsICJwdXJsIjoicGtnOmdpdGh1Yi9iZGVoYW1lci9kZWxtZUB2NiIsICJyZWxlYXNlSWQiOiIyMjIxNTg2NzEiLCAicmVwb3NpdG9yeSI6ImJkZWhhbWVyL2RlbG1lIiwgInJlcG9zaXRvcnlJZCI6IjkwNTk4ODA0NCIsICJ0YWciOiJ2NiJ9fQ==",
|
||||
"payloadType": "application/vnd.in-toto+json",
|
||||
"signatures": [
|
||||
{
|
||||
"sig": "MEUCIGq+T2g2gV2+lcmgyCaVPrjO1tj86RxitwiEOjU5dH/GAiEAvKaT/7H0sIVdAY7EzLq1IFaF8LmlW6eV68eZQvtuA0c="
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
|
||||
|
|
@ -68,6 +69,10 @@ func NewTrustedRootCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Com
|
|||
return err
|
||||
}
|
||||
|
||||
hc, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ghauth.IsTenancy(opts.Hostname) {
|
||||
c, err := f.Config()
|
||||
if err != nil {
|
||||
|
|
@ -77,11 +82,6 @@ func NewTrustedRootCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Com
|
|||
if !c.Authentication().HasActiveToken(opts.Hostname) {
|
||||
return fmt.Errorf("not authenticated with %s", opts.Hostname)
|
||||
}
|
||||
|
||||
hc, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger := io.NewHandler(f.IOStreams)
|
||||
apiClient := api.NewLiveClient(hc, opts.Hostname, logger)
|
||||
td, err := apiClient.GetTrustDomain()
|
||||
|
|
@ -95,7 +95,7 @@ func NewTrustedRootCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Com
|
|||
return runF(opts)
|
||||
}
|
||||
|
||||
if err := getTrustedRoot(tuf.New, opts); err != nil {
|
||||
if err := getTrustedRoot(tuf.New, opts, hc); err != nil {
|
||||
return fmt.Errorf("Failed to verify the TUF repository: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -118,11 +118,11 @@ type tufConfig struct {
|
|||
targets []string
|
||||
}
|
||||
|
||||
func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options) error {
|
||||
func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options, hc *http.Client) error {
|
||||
var tufOptions []tufConfig
|
||||
var defaultTR = "trusted_root.json"
|
||||
|
||||
tufOpt := verification.DefaultOptionsWithCacheSetting(o.None[string]())
|
||||
tufOpt := verification.DefaultOptionsWithCacheSetting(o.None[string](), hc)
|
||||
// Disable local caching, so we get up-to-date response from TUF repository
|
||||
tufOpt.CacheValidity = 0
|
||||
|
||||
|
|
@ -151,7 +151,7 @@ func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options) error {
|
|||
targets: []string{defaultTR},
|
||||
})
|
||||
|
||||
tufOpt = verification.GitHubTUFOptions(o.None[string]())
|
||||
tufOpt = verification.GitHubTUFOptions(o.None[string](), hc)
|
||||
tufOpt.CacheValidity = 0
|
||||
tufOptions = append(tufOptions, tufConfig{
|
||||
tufOptions: tufOpt,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ func TestNewTrustedRootCmd(t *testing.T) {
|
|||
Config: func() (gh.Config, error) {
|
||||
return &ghmock.ConfigMock{}, nil
|
||||
},
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
reg := &httpmock.Registry{}
|
||||
client := &http.Client{}
|
||||
httpmock.ReplaceTripper(client, reg)
|
||||
return client, nil
|
||||
},
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
|
|
@ -113,6 +119,7 @@ func TestNewTrustedRootWithTenancy(t *testing.T) {
|
|||
},
|
||||
}, nil
|
||||
},
|
||||
HttpClient: httpClientFunc,
|
||||
}
|
||||
|
||||
cmd := NewTrustedRootCmd(f, func(_ *Options) error {
|
||||
|
|
@ -171,15 +178,19 @@ func TestGetTrustedRoot(t *testing.T) {
|
|||
TufRootPath: root,
|
||||
}
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
client := &http.Client{}
|
||||
httpmock.ReplaceTripper(client, reg)
|
||||
|
||||
t.Run("failed to create TUF root", func(t *testing.T) {
|
||||
err := getTrustedRoot(newTUFErrClient, opts)
|
||||
err := getTrustedRoot(newTUFErrClient, opts, client)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "failed to create TUF client")
|
||||
})
|
||||
|
||||
t.Run("fails because the root cannot be found", func(t *testing.T) {
|
||||
opts.TufRootPath = test.NormalizeRelativePath("./does/not/exist/root.json")
|
||||
err := getTrustedRoot(tuf.New, opts)
|
||||
err := getTrustedRoot(tuf.New, opts, client)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "failed to read root file")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -20,13 +20,6 @@ const SLSAPredicateV1 = "https://slsa.dev/provenance/v1"
|
|||
var ErrUnrecognisedBundleExtension = errors.New("bundle file extension not supported, must be json or jsonl")
|
||||
var ErrEmptyBundleFile = errors.New("provided bundle file is empty")
|
||||
|
||||
type FetchRemoteAttestationsParams struct {
|
||||
Digest string
|
||||
Limit int
|
||||
Owner string
|
||||
Repo string
|
||||
}
|
||||
|
||||
// GetLocalAttestations returns a slice of attestations read from a local bundle file.
|
||||
func GetLocalAttestations(path string) ([]*api.Attestation, error) {
|
||||
var attestations []*api.Attestation
|
||||
|
|
@ -89,28 +82,6 @@ func loadBundlesFromJSONLinesFile(path string) ([]*api.Attestation, error) {
|
|||
return attestations, nil
|
||||
}
|
||||
|
||||
func GetRemoteAttestations(client api.Client, params FetchRemoteAttestationsParams) ([]*api.Attestation, error) {
|
||||
if client == nil {
|
||||
return nil, fmt.Errorf("api client must be provided")
|
||||
}
|
||||
// check if Repo is set first because if Repo has been set, Owner will be set using the value of Repo.
|
||||
// If Repo is not set, the field will remain empty. It will not be populated using the value of Owner.
|
||||
if params.Repo != "" {
|
||||
attestations, err := client.GetByRepoAndDigest(params.Repo, params.Digest, params.Limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch attestations from %s: %w", params.Repo, err)
|
||||
}
|
||||
return attestations, nil
|
||||
} else if params.Owner != "" {
|
||||
attestations, err := client.GetByOwnerAndDigest(params.Owner, params.Digest, params.Limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch attestations from %s: %w", params.Owner, err)
|
||||
}
|
||||
return attestations, nil
|
||||
}
|
||||
return nil, fmt.Errorf("owner or repo must be provided")
|
||||
}
|
||||
|
||||
func GetOCIAttestations(client oci.Client, artifact artifact.DigestedArtifact) ([]*api.Attestation, error) {
|
||||
attestations, err := client.GetAttestations(artifact.NameRef(), artifact.DigestWithAlg())
|
||||
if err != nil {
|
||||
|
|
@ -121,31 +92,3 @@ func GetOCIAttestations(client oci.Client, artifact artifact.DigestedArtifact) (
|
|||
}
|
||||
return attestations, nil
|
||||
}
|
||||
|
||||
type IntotoStatement struct {
|
||||
PredicateType string `json:"predicateType"`
|
||||
}
|
||||
|
||||
func FilterAttestations(predicateType string, attestations []*api.Attestation) []*api.Attestation {
|
||||
filteredAttestations := []*api.Attestation{}
|
||||
|
||||
for _, each := range attestations {
|
||||
dsseEnvelope := each.Bundle.GetDsseEnvelope()
|
||||
if dsseEnvelope != nil {
|
||||
if dsseEnvelope.PayloadType != "application/vnd.in-toto+json" {
|
||||
// Don't fail just because an entry isn't intoto
|
||||
continue
|
||||
}
|
||||
var intotoStatement IntotoStatement
|
||||
if err := json.Unmarshal([]byte(dsseEnvelope.Payload), &intotoStatement); err != nil {
|
||||
// Don't fail just because a single entry can't be unmarshalled
|
||||
continue
|
||||
}
|
||||
if intotoStatement.PredicateType == predicateType {
|
||||
filteredAttestations = append(filteredAttestations, each)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredAttestations
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,10 +157,11 @@ func TestFilterAttestations(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
filtered := FilterAttestations("https://slsa.dev/provenance/v1", attestations)
|
||||
|
||||
filtered, err := api.FilterAttestations("https://slsa.dev/provenance/v1", attestations)
|
||||
require.Len(t, filtered, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
filtered = FilterAttestations("NonExistentPredicate", attestations)
|
||||
require.Len(t, filtered, 0)
|
||||
filtered, err = api.FilterAttestations("NonExistentPredicate", attestations)
|
||||
require.Nil(t, filtered)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
|
||||
|
|
@ -33,6 +34,7 @@ type SigstoreConfig struct {
|
|||
TrustedRoot string
|
||||
Logger *io.Handler
|
||||
NoPublicGood bool
|
||||
HttpClient *http.Client
|
||||
// If tenancy mode is not used, trust domain is empty
|
||||
TrustDomain string
|
||||
// TUFMetadataDir
|
||||
|
|
@ -46,9 +48,9 @@ type SigstoreVerifier interface {
|
|||
type LiveSigstoreVerifier struct {
|
||||
Logger *io.Handler
|
||||
NoPublicGood bool
|
||||
PublicGood *verify.SignedEntityVerifier
|
||||
GitHub *verify.SignedEntityVerifier
|
||||
Custom map[string]*verify.SignedEntityVerifier
|
||||
PublicGood *verify.Verifier
|
||||
GitHub *verify.Verifier
|
||||
Custom map[string]*verify.Verifier
|
||||
}
|
||||
|
||||
var ErrNoAttestationsVerified = errors.New("no attestations were verified")
|
||||
|
|
@ -71,13 +73,13 @@ func NewLiveSigstoreVerifier(config SigstoreConfig) (*LiveSigstoreVerifier, erro
|
|||
return liveVerifier, nil
|
||||
}
|
||||
if !config.NoPublicGood {
|
||||
publicGoodVerifier, err := newPublicGoodVerifier(config.TUFMetadataDir)
|
||||
publicGoodVerifier, err := newPublicGoodVerifier(config.TUFMetadataDir, config.HttpClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
liveVerifier.PublicGood = publicGoodVerifier
|
||||
}
|
||||
github, err := newGitHubVerifier(config.TrustDomain, config.TUFMetadataDir)
|
||||
github, err := newGitHubVerifier(config.TrustDomain, config.TUFMetadataDir, config.HttpClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -86,13 +88,13 @@ func NewLiveSigstoreVerifier(config SigstoreConfig) (*LiveSigstoreVerifier, erro
|
|||
return liveVerifier, nil
|
||||
}
|
||||
|
||||
func createCustomVerifiers(trustedRoot string, noPublicGood bool) (map[string]*verify.SignedEntityVerifier, error) {
|
||||
func createCustomVerifiers(trustedRoot string, noPublicGood bool) (map[string]*verify.Verifier, error) {
|
||||
customTrustRoots, err := os.ReadFile(trustedRoot)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read file %s: %v", trustedRoot, err)
|
||||
}
|
||||
|
||||
verifiers := make(map[string]*verify.SignedEntityVerifier)
|
||||
verifiers := make(map[string]*verify.Verifier)
|
||||
reader := bufio.NewReader(bytes.NewReader(customTrustRoots))
|
||||
var line []byte
|
||||
var readError error
|
||||
|
|
@ -189,7 +191,7 @@ func getBundleIssuer(b *bundle.Bundle) (string, error) {
|
|||
return leafCert.Issuer.Organization[0], nil
|
||||
}
|
||||
|
||||
func (v *LiveSigstoreVerifier) chooseVerifier(issuer string) (*verify.SignedEntityVerifier, error) {
|
||||
func (v *LiveSigstoreVerifier) chooseVerifier(issuer string) (*verify.Verifier, error) {
|
||||
// if no custom trusted root is set, return either the Public Good or GitHub verifier
|
||||
// If the chosen verifier has not yet been created, create it as a LiveSigstoreVerifier field for use in future calls
|
||||
if v.Custom != nil {
|
||||
|
|
@ -291,7 +293,7 @@ func (v *LiveSigstoreVerifier) Verify(attestations []*api.Attestation, policy ve
|
|||
return results, nil
|
||||
}
|
||||
|
||||
func newCustomVerifier(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerifier, error) {
|
||||
func newCustomVerifier(trustedRoot *root.TrustedRoot) (*verify.Verifier, error) {
|
||||
// All we know about this trust root is its configuration so make some
|
||||
// educated guesses as to what the policy should be.
|
||||
verifierConfig := []verify.VerifierOption{}
|
||||
|
|
@ -306,7 +308,7 @@ func newCustomVerifier(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerif
|
|||
verifierConfig = append(verifierConfig, verify.WithTransparencyLog(1))
|
||||
}
|
||||
|
||||
gv, err := verify.NewSignedEntityVerifier(trustedRoot, verifierConfig...)
|
||||
gv, err := verify.NewVerifier(trustedRoot, verifierConfig...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create custom verifier: %v", err)
|
||||
}
|
||||
|
|
@ -314,10 +316,10 @@ func newCustomVerifier(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerif
|
|||
return gv, nil
|
||||
}
|
||||
|
||||
func newGitHubVerifier(trustDomain string, tufMetadataDir o.Option[string]) (*verify.SignedEntityVerifier, error) {
|
||||
func newGitHubVerifier(trustDomain string, tufMetadataDir o.Option[string], hc *http.Client) (*verify.Verifier, error) {
|
||||
var tr string
|
||||
|
||||
opts := GitHubTUFOptions(tufMetadataDir)
|
||||
opts := GitHubTUFOptions(tufMetadataDir, hc)
|
||||
client, err := tuf.New(opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create TUF client: %v", err)
|
||||
|
|
@ -339,8 +341,8 @@ func newGitHubVerifier(trustDomain string, tufMetadataDir o.Option[string]) (*ve
|
|||
return newGitHubVerifierWithTrustedRoot(trustedRoot)
|
||||
}
|
||||
|
||||
func newGitHubVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerifier, error) {
|
||||
gv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedTimestamps(1))
|
||||
func newGitHubVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.Verifier, error) {
|
||||
gv, err := verify.NewVerifier(trustedRoot, verify.WithSignedTimestamps(1))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create GitHub verifier: %v", err)
|
||||
}
|
||||
|
|
@ -348,8 +350,8 @@ func newGitHubVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.Si
|
|||
return gv, nil
|
||||
}
|
||||
|
||||
func newPublicGoodVerifier(tufMetadataDir o.Option[string]) (*verify.SignedEntityVerifier, error) {
|
||||
opts := DefaultOptionsWithCacheSetting(tufMetadataDir)
|
||||
func newPublicGoodVerifier(tufMetadataDir o.Option[string], hc *http.Client) (*verify.Verifier, error) {
|
||||
opts := DefaultOptionsWithCacheSetting(tufMetadataDir, hc)
|
||||
client, err := tuf.New(opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create TUF client: %v", err)
|
||||
|
|
@ -362,8 +364,8 @@ func newPublicGoodVerifier(tufMetadataDir o.Option[string]) (*verify.SignedEntit
|
|||
return newPublicGoodVerifierWithTrustedRoot(trustedRoot)
|
||||
}
|
||||
|
||||
func newPublicGoodVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerifier, error) {
|
||||
sv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedCertificateTimestamps(1), verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1))
|
||||
func newPublicGoodVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.Verifier, error) {
|
||||
sv, err := verify.NewVerifier(trustedRoot, verify.WithSignedCertificateTimestamps(1), verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Public Good verifier: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
package verification
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
|
||||
|
|
@ -51,6 +52,7 @@ func TestLiveSigstoreVerifier(t *testing.T) {
|
|||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
verifier, err := NewLiveSigstoreVerifier(SigstoreConfig{
|
||||
HttpClient: http.DefaultClient,
|
||||
Logger: io.NewTestHandler(),
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
})
|
||||
|
|
@ -71,6 +73,7 @@ func TestLiveSigstoreVerifier(t *testing.T) {
|
|||
|
||||
t.Run("with 2/3 verified attestations", func(t *testing.T) {
|
||||
verifier, err := NewLiveSigstoreVerifier(SigstoreConfig{
|
||||
HttpClient: http.DefaultClient,
|
||||
Logger: io.NewTestHandler(),
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
})
|
||||
|
|
@ -89,6 +92,7 @@ func TestLiveSigstoreVerifier(t *testing.T) {
|
|||
|
||||
t.Run("fail with 0/2 verified attestations", func(t *testing.T) {
|
||||
verifier, err := NewLiveSigstoreVerifier(SigstoreConfig{
|
||||
HttpClient: http.DefaultClient,
|
||||
Logger: io.NewTestHandler(),
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
})
|
||||
|
|
@ -114,6 +118,7 @@ func TestLiveSigstoreVerifier(t *testing.T) {
|
|||
attestations := getAttestationsFor(t, "../test/data/github_provenance_demo-0.0.12-py3-none-any-bundle.jsonl")
|
||||
|
||||
verifier, err := NewLiveSigstoreVerifier(SigstoreConfig{
|
||||
HttpClient: http.DefaultClient,
|
||||
Logger: io.NewTestHandler(),
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
})
|
||||
|
|
@ -128,6 +133,7 @@ func TestLiveSigstoreVerifier(t *testing.T) {
|
|||
attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl")
|
||||
|
||||
verifier, err := NewLiveSigstoreVerifier(SigstoreConfig{
|
||||
HttpClient: http.DefaultClient,
|
||||
Logger: io.NewTestHandler(),
|
||||
TrustedRoot: test.NormalizeRelativePath("../test/data/trusted_root.json"),
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@ package verification
|
|||
|
||||
import (
|
||||
_ "embed"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cenkalti/backoff/v5"
|
||||
o "github.com/cli/cli/v2/pkg/option"
|
||||
"github.com/cli/go-gh/v2/pkg/config"
|
||||
"github.com/sigstore/sigstore-go/pkg/tuf"
|
||||
"github.com/theupdateframework/go-tuf/v2/metadata/fetcher"
|
||||
)
|
||||
|
||||
//go:embed embed/tuf-repo.github.com/root.json
|
||||
|
|
@ -15,7 +18,7 @@ var githubRoot []byte
|
|||
|
||||
const GitHubTUFMirror = "https://tuf-repo.github.com"
|
||||
|
||||
func DefaultOptionsWithCacheSetting(tufMetadataDir o.Option[string]) *tuf.Options {
|
||||
func DefaultOptionsWithCacheSetting(tufMetadataDir o.Option[string], hc *http.Client) *tuf.Options {
|
||||
opts := tuf.DefaultOptions()
|
||||
|
||||
// The CODESPACES environment variable will be set to true in a Codespaces workspace
|
||||
|
|
@ -32,11 +35,18 @@ func DefaultOptionsWithCacheSetting(tufMetadataDir o.Option[string]) *tuf.Option
|
|||
// Allow TUF cache for 1 day
|
||||
opts.CacheValidity = 1
|
||||
|
||||
// configure fetcher timeout and retry
|
||||
f := fetcher.NewDefaultFetcher()
|
||||
f.SetHTTPClient(hc)
|
||||
retryOptions := []backoff.RetryOption{backoff.WithMaxTries(3)}
|
||||
f.SetRetryOptions(retryOptions...)
|
||||
opts.WithFetcher(f)
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
func GitHubTUFOptions(tufMetadataDir o.Option[string]) *tuf.Options {
|
||||
opts := DefaultOptionsWithCacheSetting(tufMetadataDir)
|
||||
func GitHubTUFOptions(tufMetadataDir o.Option[string], hc *http.Client) *tuf.Options {
|
||||
opts := DefaultOptionsWithCacheSetting(tufMetadataDir, hc)
|
||||
|
||||
opts.Root = githubRoot
|
||||
opts.RepositoryBaseURL = GitHubTUFMirror
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
func TestGitHubTUFOptionsNoMetadataDir(t *testing.T) {
|
||||
os.Setenv("CODESPACES", "true")
|
||||
opts := GitHubTUFOptions(o.None[string]())
|
||||
opts := GitHubTUFOptions(o.None[string](), nil)
|
||||
|
||||
require.Equal(t, GitHubTUFMirror, opts.RepositoryBaseURL)
|
||||
require.NotNil(t, opts.Root)
|
||||
|
|
@ -21,6 +21,6 @@ func TestGitHubTUFOptionsNoMetadataDir(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGitHubTUFOptionsWithMetadataDir(t *testing.T) {
|
||||
opts := GitHubTUFOptions(o.Some("anything"))
|
||||
opts := GitHubTUFOptions(o.Some("anything"), nil)
|
||||
require.Equal(t, "anything", opts.CachePath)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package verify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
|
|
@ -10,43 +11,63 @@ import (
|
|||
)
|
||||
|
||||
func getAttestations(o *Options, a artifact.DigestedArtifact) ([]*api.Attestation, string, error) {
|
||||
// Fetch attestations from GitHub API within this if block since predicate type
|
||||
// filter is done when the API is called
|
||||
if o.FetchAttestationsFromGitHubAPI() {
|
||||
if o.APIClient == nil {
|
||||
errMsg := "✗ No APIClient provided"
|
||||
return nil, errMsg, errors.New(errMsg)
|
||||
}
|
||||
|
||||
params := api.FetchParams{
|
||||
Digest: a.DigestWithAlg(),
|
||||
Limit: o.Limit,
|
||||
Owner: o.Owner,
|
||||
PredicateType: o.PredicateType,
|
||||
Repo: o.Repo,
|
||||
}
|
||||
|
||||
attestations, err := o.APIClient.GetByDigest(params)
|
||||
if err != nil {
|
||||
msg := "✗ Loading attestations from GitHub API failed"
|
||||
return nil, msg, err
|
||||
}
|
||||
pluralAttestation := text.Pluralize(len(attestations), "attestation")
|
||||
msg := fmt.Sprintf("Loaded %s from GitHub API", pluralAttestation)
|
||||
return attestations, msg, nil
|
||||
}
|
||||
|
||||
// Fetch attestations from local bundle or OCI registry
|
||||
// Predicate type filtering is done after the attestations are fetched
|
||||
var attestations []*api.Attestation
|
||||
var err error
|
||||
var msg string
|
||||
if o.BundlePath != "" {
|
||||
attestations, err := verification.GetLocalAttestations(o.BundlePath)
|
||||
attestations, err = verification.GetLocalAttestations(o.BundlePath)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("✗ Loading attestations from %s failed", a.URL)
|
||||
return nil, msg, err
|
||||
pluralAttestation := text.Pluralize(len(attestations), "attestation")
|
||||
msg = fmt.Sprintf("Loaded %s from %s", pluralAttestation, o.BundlePath)
|
||||
} else {
|
||||
msg = fmt.Sprintf("Loaded %d attestations from %s", len(attestations), o.BundlePath)
|
||||
}
|
||||
pluralAttestation := text.Pluralize(len(attestations), "attestation")
|
||||
msg := fmt.Sprintf("Loaded %s from %s", pluralAttestation, o.BundlePath)
|
||||
return attestations, msg, nil
|
||||
}
|
||||
|
||||
if o.UseBundleFromRegistry {
|
||||
attestations, err := verification.GetOCIAttestations(o.OCIClient, a)
|
||||
} else if o.UseBundleFromRegistry {
|
||||
attestations, err = verification.GetOCIAttestations(o.OCIClient, a)
|
||||
if err != nil {
|
||||
msg := "✗ Loading attestations from OCI registry failed"
|
||||
return nil, msg, err
|
||||
msg = "✗ Loading attestations from OCI registry failed"
|
||||
} else {
|
||||
pluralAttestation := text.Pluralize(len(attestations), "attestation")
|
||||
msg = fmt.Sprintf("Loaded %s from OCI registry", pluralAttestation)
|
||||
}
|
||||
pluralAttestation := text.Pluralize(len(attestations), "attestation")
|
||||
msg := fmt.Sprintf("Loaded %s from %s", pluralAttestation, o.ArtifactPath)
|
||||
return attestations, msg, nil
|
||||
}
|
||||
|
||||
params := verification.FetchRemoteAttestationsParams{
|
||||
Digest: a.DigestWithAlg(),
|
||||
Limit: o.Limit,
|
||||
Owner: o.Owner,
|
||||
Repo: o.Repo,
|
||||
}
|
||||
|
||||
attestations, err := verification.GetRemoteAttestations(o.APIClient, params)
|
||||
if err != nil {
|
||||
msg := "✗ Loading attestations from GitHub API failed"
|
||||
return nil, msg, err
|
||||
}
|
||||
pluralAttestation := text.Pluralize(len(attestations), "attestation")
|
||||
msg := fmt.Sprintf("Loaded %s from GitHub API", pluralAttestation)
|
||||
return attestations, msg, nil
|
||||
|
||||
filtered, err := api.FilterAttestations(o.PredicateType, attestations)
|
||||
if err != nil {
|
||||
return nil, err.Error(), err
|
||||
}
|
||||
return filtered, msg, nil
|
||||
}
|
||||
|
||||
func verifyAttestations(art artifact.DigestedArtifact, att []*api.Attestation, sgVerifier verification.SigstoreVerifier, ec verification.EnforcementCriteria) ([]*verification.AttestationProcessingResult, string, error) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
package verify
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
|
||||
|
|
@ -26,6 +27,7 @@ func getAttestationsFor(t *testing.T, bundlePath string) []*api.Attestation {
|
|||
|
||||
func TestVerifyAttestations(t *testing.T) {
|
||||
sgVerifier, err := verification.NewLiveSigstoreVerifier(verification.SigstoreConfig{
|
||||
HttpClient: http.DefaultClient,
|
||||
Logger: io.NewTestHandler(),
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
})
|
||||
|
|
|
|||
71
pkg/cmd/attestation/verify/attestation_test.go
Normal file
71
pkg/cmd/attestation/verify/attestation_test.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package verify
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
|
||||
"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/attestation/verification"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetAttestations_OCIRegistry_PredicateTypeFiltering(t *testing.T) {
|
||||
artifact, err := artifact.NewDigestedArtifact(nil, "../test/data/gh_2.60.1_windows_arm64.zip", "sha256")
|
||||
require.NoError(t, err)
|
||||
|
||||
o := &Options{
|
||||
OCIClient: oci.MockClient{},
|
||||
PredicateType: verification.SLSAPredicateV1,
|
||||
Repo: "cli/cli",
|
||||
UseBundleFromRegistry: true,
|
||||
}
|
||||
attestations, msg, err := getAttestations(o, *artifact)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, msg, "Loaded 2 attestations from OCI registry")
|
||||
require.Len(t, attestations, 2)
|
||||
|
||||
o.PredicateType = "custom predicate type"
|
||||
attestations, msg, err = getAttestations(o, *artifact)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, msg, "no attestations found with predicate type")
|
||||
require.Nil(t, attestations)
|
||||
}
|
||||
|
||||
func TestGetAttestations_LocalBundle_PredicateTypeFiltering(t *testing.T) {
|
||||
artifact, err := artifact.NewDigestedArtifact(nil, "../test/data/gh_2.60.1_windows_arm64.zip", "sha256")
|
||||
require.NoError(t, err)
|
||||
|
||||
o := &Options{
|
||||
BundlePath: "../test/data/sigstore-js-2.1.0-bundle.json",
|
||||
PredicateType: verification.SLSAPredicateV1,
|
||||
Repo: "sigstore/sigstore-js",
|
||||
}
|
||||
attestations, _, err := getAttestations(o, *artifact)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, attestations, 1)
|
||||
|
||||
o.PredicateType = "custom predicate type"
|
||||
attestations, _, err = getAttestations(o, *artifact)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, attestations)
|
||||
}
|
||||
|
||||
func TestGetAttestations_GhAPI_NoAttestationsFound(t *testing.T) {
|
||||
artifact, err := artifact.NewDigestedArtifact(nil, "../test/data/gh_2.60.1_windows_arm64.zip", "sha256")
|
||||
require.NoError(t, err)
|
||||
|
||||
o := &Options{
|
||||
APIClient: api.NewTestClient(),
|
||||
PredicateType: verification.SLSAPredicateV1,
|
||||
Repo: "sigstore/sigstore-js",
|
||||
}
|
||||
attestations, _, err := getAttestations(o, *artifact)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, attestations, 2)
|
||||
|
||||
o.PredicateType = "custom predicate type"
|
||||
attestations, _, err = getAttestations(o, *artifact)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, attestations)
|
||||
}
|
||||
|
|
@ -53,6 +53,12 @@ func (opts *Options) Clean() {
|
|||
}
|
||||
}
|
||||
|
||||
// FetchAttestationsFromGitHubAPI returns true if the command should fetch attestations from the GitHub API
|
||||
// It checks that a bundle path is not provided and that the "use bundle from registry" flag is not set
|
||||
func (opts *Options) FetchAttestationsFromGitHubAPI() bool {
|
||||
return opts.BundlePath == "" && !opts.UseBundleFromRegistry
|
||||
}
|
||||
|
||||
// AreFlagsValid checks that the provided flag combination is valid
|
||||
// and returns an error otherwise
|
||||
func (opts *Options) AreFlagsValid() error {
|
||||
|
|
|
|||
|
|
@ -186,9 +186,10 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
|
|||
opts.APIClient = api.NewLiveClient(hc, opts.Hostname, opts.Logger)
|
||||
|
||||
config := verification.SigstoreConfig{
|
||||
TrustedRoot: opts.TrustedRoot,
|
||||
HttpClient: hc,
|
||||
Logger: opts.Logger,
|
||||
NoPublicGood: opts.NoPublicGood,
|
||||
TrustedRoot: opts.TrustedRoot,
|
||||
}
|
||||
|
||||
// Prepare for tenancy if detected
|
||||
|
|
@ -288,14 +289,6 @@ func runVerify(opts *Options) error {
|
|||
// Print the message signifying success fetching attestations
|
||||
opts.Logger.Println(logMsg)
|
||||
|
||||
// Apply predicate type filter to returned attestations
|
||||
filteredAttestations := verification.FilterAttestations(ec.PredicateType, attestations)
|
||||
if len(filteredAttestations) == 0 {
|
||||
opts.Logger.Printf(opts.Logger.ColorScheme.Red("✗ No attestations found with predicate type: %s\n"), opts.PredicateType)
|
||||
return fmt.Errorf("no matching predicate found")
|
||||
}
|
||||
attestations = filteredAttestations
|
||||
|
||||
// print information about the policy that will be enforced against attestations
|
||||
opts.Logger.Println("\nThe following policy criteria will be enforced:")
|
||||
opts.Logger.Println(ec.BuildPolicyInformation())
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
package verify
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
|
||||
|
|
@ -20,6 +21,7 @@ func TestVerifyIntegration(t *testing.T) {
|
|||
logger := io.NewTestHandler()
|
||||
|
||||
sigstoreConfig := verification.SigstoreConfig{
|
||||
HttpClient: http.DefaultClient,
|
||||
Logger: logger,
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
}
|
||||
|
|
@ -136,6 +138,7 @@ func TestVerifyIntegrationCustomIssuer(t *testing.T) {
|
|||
logger := io.NewTestHandler()
|
||||
|
||||
sigstoreConfig := verification.SigstoreConfig{
|
||||
HttpClient: http.DefaultClient,
|
||||
Logger: logger,
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
}
|
||||
|
|
@ -209,6 +212,7 @@ func TestVerifyIntegrationReusableWorkflow(t *testing.T) {
|
|||
logger := io.NewTestHandler()
|
||||
|
||||
sigstoreConfig := verification.SigstoreConfig{
|
||||
HttpClient: http.DefaultClient,
|
||||
Logger: logger,
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
}
|
||||
|
|
@ -301,6 +305,7 @@ func TestVerifyIntegrationReusableWorkflowSignerWorkflow(t *testing.T) {
|
|||
logger := io.NewTestHandler()
|
||||
|
||||
sigstoreConfig := verification.SigstoreConfig{
|
||||
HttpClient: http.DefaultClient,
|
||||
Logger: logger,
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -510,7 +510,7 @@ func TestRunVerify(t *testing.T) {
|
|||
|
||||
err := runVerify(&customOpts)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "no matching predicate found")
|
||||
require.ErrorContains(t, err, "no attestations found with predicate type")
|
||||
})
|
||||
|
||||
t.Run("with valid OCI artifact with UseBundleFromRegistry flag but no bundle return from registry", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -16,14 +16,14 @@ import (
|
|||
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\n")
|
||||
longDoc.WriteString("Current respected settings:\n")
|
||||
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, "|")))
|
||||
longDoc.WriteString(fmt.Sprintf(" `{%s}`", strings.Join(co.AllowedValues, " | ")))
|
||||
}
|
||||
if co.DefaultValue != "" {
|
||||
longDoc.WriteString(fmt.Sprintf(" (default %s)", co.DefaultValue))
|
||||
longDoc.WriteString(fmt.Sprintf(" (default `%s`)", co.DefaultValue))
|
||||
}
|
||||
longDoc.WriteRune('\n')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,6 +101,9 @@ func Test_listRun(t *testing.T) {
|
|||
http_unix_socket=
|
||||
browser=brave
|
||||
color_labels=disabled
|
||||
accessible_colors=disabled
|
||||
accessible_prompter=disabled
|
||||
spinner=enabled
|
||||
`),
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,15 +23,14 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewCmdExtension(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
oldWd, _ := os.Getwd()
|
||||
localExtensionTempDir := filepath.Join(tempDir, "gh-hello")
|
||||
assert.NoError(t, os.MkdirAll(localExtensionTempDir, 0755))
|
||||
assert.NoError(t, os.Chdir(localExtensionTempDir))
|
||||
t.Cleanup(func() { _ = os.Chdir(oldWd) })
|
||||
require.NoError(t, os.MkdirAll(localExtensionTempDir, 0755))
|
||||
t.Chdir(localExtensionTempDir)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -1251,9 +1251,10 @@ func TestManager_repo_not_found(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestManager_Create(t *testing.T) {
|
||||
chdirTemp(t)
|
||||
tempDir := t.TempDir()
|
||||
t.Chdir(tempDir)
|
||||
err := os.MkdirAll("gh-test", 0755)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
|
||||
|
|
@ -1279,9 +1280,10 @@ func TestManager_Create(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestManager_Create_go_binary(t *testing.T) {
|
||||
chdirTemp(t)
|
||||
tempDir := t.TempDir()
|
||||
t.Chdir(tempDir)
|
||||
err := os.MkdirAll("gh-test", 0755)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
reg := httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
|
|
@ -1329,9 +1331,10 @@ func TestManager_Create_go_binary(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestManager_Create_other_binary(t *testing.T) {
|
||||
chdirTemp(t)
|
||||
tempDir := t.TempDir()
|
||||
t.Chdir(tempDir)
|
||||
err := os.MkdirAll("gh-test", 0755)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
|
||||
|
|
@ -1392,18 +1395,6 @@ func Test_ensurePrefixed(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// chdirTemp changes the current working directory to a temporary directory for the duration of the test.
|
||||
func chdirTemp(t *testing.T) {
|
||||
oldWd, _ := os.Getwd()
|
||||
tempDir := t.TempDir()
|
||||
if err := os.Chdir(tempDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = os.Chdir(oldWd)
|
||||
})
|
||||
}
|
||||
|
||||
func fileNames(files []os.DirEntry) []string {
|
||||
names := make([]string, len(files))
|
||||
for i, f := range files {
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ func newBrowser(f *cmdutil.Factory) browser.Browser {
|
|||
func newPrompter(f *cmdutil.Factory) prompter.Prompter {
|
||||
editor, _ := cmdutil.DetermineEditor(f.Config)
|
||||
io := f.IOStreams
|
||||
return prompter.New(editor, io.In, io.Out, io.ErrOut)
|
||||
return prompter.New(editor, io)
|
||||
}
|
||||
|
||||
func configFunc() func() (gh.Config, error) {
|
||||
|
|
@ -284,9 +284,23 @@ func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams {
|
|||
io.SetNeverPrompt(true)
|
||||
}
|
||||
|
||||
ghSpinnerDisabledValue, ghSpinnerDisabledIsSet := os.LookupEnv("GH_SPINNER_DISABLED")
|
||||
falseyValues := []string{"false", "0", "no", ""}
|
||||
if ghSpinnerDisabledIsSet && !slices.Contains(falseyValues, ghSpinnerDisabledValue) {
|
||||
|
||||
accessiblePrompterValue, accessiblePrompterIsSet := os.LookupEnv("GH_ACCESSIBLE_PROMPTER")
|
||||
if accessiblePrompterIsSet {
|
||||
if !slices.Contains(falseyValues, accessiblePrompterValue) {
|
||||
io.SetAccessiblePrompterEnabled(true)
|
||||
}
|
||||
} else if prompt := cfg.AccessiblePrompter(""); prompt.Value == "enabled" {
|
||||
io.SetAccessiblePrompterEnabled(true)
|
||||
}
|
||||
|
||||
ghSpinnerDisabledValue, ghSpinnerDisabledIsSet := os.LookupEnv("GH_SPINNER_DISABLED")
|
||||
if ghSpinnerDisabledIsSet {
|
||||
if !slices.Contains(falseyValues, ghSpinnerDisabledValue) {
|
||||
io.SetSpinnerDisabled(true)
|
||||
}
|
||||
} else if spinnerDisabled := cfg.Spinner(""); spinnerDisabled.Value == "disabled" {
|
||||
io.SetSpinnerDisabled(true)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -435,6 +435,7 @@ func Test_ioStreams_prompt(t *testing.T) {
|
|||
func Test_ioStreams_spinnerDisabled(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config gh.Config
|
||||
spinnerDisabled bool
|
||||
env map[string]string
|
||||
}{
|
||||
|
|
@ -442,6 +443,16 @@ func Test_ioStreams_spinnerDisabled(t *testing.T) {
|
|||
name: "default config",
|
||||
spinnerDisabled: false,
|
||||
},
|
||||
{
|
||||
name: "config with spinner disabled",
|
||||
config: disableSpinnersConfig(),
|
||||
spinnerDisabled: true,
|
||||
},
|
||||
{
|
||||
name: "config with spinner enabled",
|
||||
config: enableSpinnersConfig(),
|
||||
spinnerDisabled: false,
|
||||
},
|
||||
{
|
||||
name: "spinner disabled via GH_SPINNER_DISABLED env var = 0",
|
||||
env: map[string]string{"GH_SPINNER_DISABLED": "0"},
|
||||
|
|
@ -467,6 +478,18 @@ func Test_ioStreams_spinnerDisabled(t *testing.T) {
|
|||
env: map[string]string{"GH_SPINNER_DISABLED": "true"},
|
||||
spinnerDisabled: true,
|
||||
},
|
||||
{
|
||||
name: "config enabled but env disabled, respects env",
|
||||
config: enableSpinnersConfig(),
|
||||
env: map[string]string{"GH_SPINNER_DISABLED": "true"},
|
||||
spinnerDisabled: true,
|
||||
},
|
||||
{
|
||||
name: "config disabled but env enabled, respects env",
|
||||
config: disableSpinnersConfig(),
|
||||
env: map[string]string{"GH_SPINNER_DISABLED": "false"},
|
||||
spinnerDisabled: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
@ -474,12 +497,87 @@ func Test_ioStreams_spinnerDisabled(t *testing.T) {
|
|||
t.Setenv(k, v)
|
||||
}
|
||||
f := New("1")
|
||||
f.Config = func() (gh.Config, error) {
|
||||
if tt.config == nil {
|
||||
return config.NewBlankConfig(), nil
|
||||
} else {
|
||||
return tt.config, nil
|
||||
}
|
||||
}
|
||||
io := ioStreams(f)
|
||||
assert.Equal(t, tt.spinnerDisabled, io.GetSpinnerDisabled())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ioStreams_accessiblePrompterEnabled(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config gh.Config
|
||||
accessiblePrompterEnabled bool
|
||||
env map[string]string
|
||||
}{
|
||||
{
|
||||
name: "default config",
|
||||
accessiblePrompterEnabled: false,
|
||||
},
|
||||
{
|
||||
name: "config with accessible prompter enabled",
|
||||
config: enableAccessiblePrompterConfig(),
|
||||
accessiblePrompterEnabled: true,
|
||||
},
|
||||
{
|
||||
name: "config with accessible prompter disabled",
|
||||
config: disableAccessiblePrompterConfig(),
|
||||
accessiblePrompterEnabled: false,
|
||||
},
|
||||
{
|
||||
name: "accessible prompter enabled via GH_ACCESSIBLE_PROMPTER env var = 1",
|
||||
env: map[string]string{"GH_ACCESSIBLE_PROMPTER": "1"},
|
||||
accessiblePrompterEnabled: true,
|
||||
},
|
||||
{
|
||||
name: "accessible prompter enabled via GH_ACCESSIBLE_PROMPTER env var = true",
|
||||
env: map[string]string{"GH_ACCESSIBLE_PROMPTER": "true"},
|
||||
accessiblePrompterEnabled: true,
|
||||
},
|
||||
{
|
||||
name: "accessible prompter disabled via GH_ACCESSIBLE_PROMPTER env var = 0",
|
||||
env: map[string]string{"GH_ACCESSIBLE_PROMPTER": "0"},
|
||||
accessiblePrompterEnabled: false,
|
||||
},
|
||||
{
|
||||
name: "config disabled but env enabled, respects env",
|
||||
config: disableAccessiblePrompterConfig(),
|
||||
env: map[string]string{"GH_ACCESSIBLE_PROMPTER": "true"},
|
||||
accessiblePrompterEnabled: true,
|
||||
},
|
||||
{
|
||||
name: "config enabled but env disabled, respects env",
|
||||
config: enableAccessiblePrompterConfig(),
|
||||
env: map[string]string{"GH_ACCESSIBLE_PROMPTER": "false"},
|
||||
accessiblePrompterEnabled: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for k, v := range tt.env {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
f := New("1")
|
||||
f.Config = func() (gh.Config, error) {
|
||||
if tt.config == nil {
|
||||
return config.NewBlankConfig(), nil
|
||||
} else {
|
||||
return tt.config, nil
|
||||
}
|
||||
}
|
||||
io := ioStreams(f)
|
||||
assert.Equal(t, tt.accessiblePrompterEnabled, io.AccessiblePrompterEnabled())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ioStreams_colorLabels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -664,6 +762,22 @@ func disablePromptConfig() gh.Config {
|
|||
return config.NewFromString("prompt: disabled")
|
||||
}
|
||||
|
||||
func enableAccessiblePrompterConfig() gh.Config {
|
||||
return config.NewFromString("accessible_prompter: enabled")
|
||||
}
|
||||
|
||||
func disableAccessiblePrompterConfig() gh.Config {
|
||||
return config.NewFromString("accessible_prompter: disabled")
|
||||
}
|
||||
|
||||
func disableSpinnersConfig() gh.Config {
|
||||
return config.NewFromString("spinner: disabled")
|
||||
}
|
||||
|
||||
func enableSpinnersConfig() gh.Config {
|
||||
return config.NewFromString("spinner: enabled")
|
||||
}
|
||||
|
||||
func disableColorLabelsConfig() gh.Config {
|
||||
return config.NewFromString("color_labels: disabled")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import (
|
|||
ghAPI "github.com/cli/go-gh/v2/pkg/api"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewCmdDelete(t *testing.T) {
|
||||
|
|
@ -327,11 +328,12 @@ func Test_deleteRun(t *testing.T) {
|
|||
|
||||
func Test_gistDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
httpStubs func(*httpmock.Registry)
|
||||
hostname string
|
||||
gistID string
|
||||
wantErr error
|
||||
name string
|
||||
httpStubs func(*httpmock.Registry)
|
||||
hostname string
|
||||
gistID string
|
||||
wantErr error
|
||||
wantErrString string
|
||||
}{
|
||||
{
|
||||
name: "successful delete",
|
||||
|
|
@ -343,36 +345,34 @@ func Test_gistDelete(t *testing.T) {
|
|||
},
|
||||
hostname: "github.com",
|
||||
gistID: "1234",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "when an gist is not found, it returns a NotFoundError",
|
||||
name: "when a gist is not found, it returns a NotFoundError",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "gists/1234"),
|
||||
httpmock.StatusStringResponse(404, "{}"),
|
||||
)
|
||||
},
|
||||
hostname: "github.com",
|
||||
gistID: "1234",
|
||||
wantErr: shared.NotFoundErr,
|
||||
hostname: "github.com",
|
||||
gistID: "1234",
|
||||
wantErr: shared.NotFoundErr, // To make sure we return the pre-defined error instance.
|
||||
wantErrString: "not found",
|
||||
},
|
||||
{
|
||||
name: "when there is a non-404 error deleting the gist, that error is returned",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "gists/1234"),
|
||||
httpmock.StatusJSONResponse(500, `{"message": "arbitrary error"}`),
|
||||
httpmock.JSONErrorResponse(500, ghAPI.HTTPError{
|
||||
StatusCode: 500,
|
||||
Message: "arbitrary error",
|
||||
}),
|
||||
)
|
||||
},
|
||||
hostname: "github.com",
|
||||
gistID: "1234",
|
||||
wantErr: api.HTTPError{
|
||||
HTTPError: &ghAPI.HTTPError{
|
||||
StatusCode: 500,
|
||||
Message: "arbitrary error",
|
||||
},
|
||||
},
|
||||
hostname: "github.com",
|
||||
gistID: "1234",
|
||||
wantErrString: "HTTP 500: arbitrary error (https://api.github.com/gists/1234)",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -383,12 +383,16 @@ func Test_gistDelete(t *testing.T) {
|
|||
client := api.NewClientFromHTTP(&http.Client{Transport: reg})
|
||||
|
||||
err := deleteGist(client, tt.hostname, tt.gistID)
|
||||
if tt.wantErr != nil {
|
||||
assert.ErrorAs(t, err, &tt.wantErr)
|
||||
if tt.wantErrString == "" && tt.wantErr == nil {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
if tt.wantErrString != "" {
|
||||
require.EqualError(t, err, tt.wantErrString)
|
||||
}
|
||||
if tt.wantErr != nil {
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,6 +236,8 @@ func editRun(opts *EditOptions) error {
|
|||
if filename == "" {
|
||||
if len(candidates) == 1 {
|
||||
filename = candidates[0]
|
||||
} else if len(candidates) == 0 {
|
||||
return errors.New("no file in the gist")
|
||||
} else {
|
||||
if !opts.IO.CanPrompt() {
|
||||
return errors.New("unsure what file to edit; either specify --filename or run interactively")
|
||||
|
|
|
|||
|
|
@ -557,6 +557,30 @@ func Test_editRun(t *testing.T) {
|
|||
},
|
||||
wantErr: "gist ID or URL required when not running interactively",
|
||||
},
|
||||
{
|
||||
name: "edit no-file gist (#10626)",
|
||||
opts: &EditOptions{
|
||||
Selector: "1234",
|
||||
},
|
||||
mockGist: &shared.Gist{
|
||||
ID: "1234",
|
||||
Files: map[string]*shared.GistFile{},
|
||||
Owner: &shared.GistOwner{Login: "octocat"},
|
||||
},
|
||||
wantErr: "no file in the gist",
|
||||
},
|
||||
{
|
||||
name: "edit no-file gist, nil map (#10626)",
|
||||
opts: &EditOptions{
|
||||
Selector: "1234",
|
||||
},
|
||||
mockGist: &shared.Gist{
|
||||
ID: "1234",
|
||||
Files: nil,
|
||||
Owner: &shared.GistOwner{Login: "octocat"},
|
||||
},
|
||||
wantErr: "no file in the gist",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ func (g Gist) Filename() string {
|
|||
for fn := range g.Files {
|
||||
filenames = append(filenames, fn)
|
||||
}
|
||||
if len(filenames) == 0 {
|
||||
return ""
|
||||
}
|
||||
sort.Strings(filenames)
|
||||
return filenames[0]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,6 +163,33 @@ func TestPromptGists(t *testing.T) {
|
|||
response: `{ "data": { "viewer": { "gists": { "nodes": [] } } } }`,
|
||||
wantOut: Gist{},
|
||||
},
|
||||
{
|
||||
name: "prompt list contains no-file gist (#10626)",
|
||||
prompterStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterSelect("Select a gist",
|
||||
[]string{" about 6 hours ago", "gistfile0.txt about 6 hours ago"},
|
||||
func(_, _ string, opts []string) (int, error) {
|
||||
return prompter.IndexFor(opts, " about 6 hours ago")
|
||||
})
|
||||
},
|
||||
response: `{ "data": { "viewer": { "gists": { "nodes": [
|
||||
{
|
||||
"name": "1234",
|
||||
"files": [],
|
||||
"description": "",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
},
|
||||
{
|
||||
"name": "5678",
|
||||
"files": [{ "name": "gistfile0.txt" }],
|
||||
"description": "",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
}
|
||||
] } } } }`,
|
||||
wantOut: Gist{ID: "1234", Files: map[string]*GistFile{}, UpdatedAt: sixHoursAgo, Public: true},
|
||||
},
|
||||
}
|
||||
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ func Test_deleteRun(t *testing.T) {
|
|||
opts: DeleteOptions{KeyID: "ABC123", Confirmed: true},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "user/gpg_keys"), httpmock.StatusStringResponse(200, keysResp))
|
||||
reg.Register(httpmock.REST("DELETE", "user/gpg_keys/123"), httpmock.StatusJSONResponse(404, api.HTTPError{
|
||||
reg.Register(httpmock.REST("DELETE", "user/gpg_keys/123"), httpmock.JSONErrorResponse(404, api.HTTPError{
|
||||
StatusCode: 404,
|
||||
Message: "GPG key 123 not found",
|
||||
}))
|
||||
|
|
|
|||
137
pkg/cmd/issue/argparsetest/argparsetest.go
Normal file
137
pkg/cmd/issue/argparsetest/argparsetest.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
package argparsetest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/google/shlex"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// newCmdFunc represents the typical function signature we use for creating commands e.g. `NewCmdView`.
|
||||
//
|
||||
// It is generic over `T` as each command construction has their own Options type e.g. `ViewOptions`
|
||||
type newCmdFunc[T any] func(f *cmdutil.Factory, runF func(*T) error) *cobra.Command
|
||||
|
||||
// TestArgParsing is a test helper that verifies that issue commands correctly parse the `{issue number | url}`
|
||||
// positional arg into an issue number and base repo.
|
||||
//
|
||||
// Looking through the existing tests, I noticed that the coverage was pretty smattered.
|
||||
// Since nearly all issue commands only accept a single positional argument, we are able to reuse this test helper.
|
||||
// Commands with no further flags or args can use this solely.
|
||||
// Commands with extra flags use this and further table tests.
|
||||
// Commands with extra required positional arguments (like `transfer`) cannot use this. They duplicate these cases inline.
|
||||
func TestArgParsing[T any](t *testing.T, fn newCmdFunc[T]) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedissueNumber int
|
||||
expectedBaseRepo ghrepo.Interface
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "no argument",
|
||||
input: "",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "issue number argument",
|
||||
input: "23 --repo owner/repo",
|
||||
expectedissueNumber: 23,
|
||||
expectedBaseRepo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
{
|
||||
name: "argument is hash prefixed number",
|
||||
// Escaping is required here to avoid what I think is shellex treating it as a comment.
|
||||
input: "\\#23 --repo owner/repo",
|
||||
expectedissueNumber: 23,
|
||||
expectedBaseRepo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
{
|
||||
name: "argument is a URL",
|
||||
input: "https://github.com/cli/cli/issues/23",
|
||||
expectedissueNumber: 23,
|
||||
expectedBaseRepo: ghrepo.New("cli", "cli"),
|
||||
},
|
||||
{
|
||||
name: "argument cannot be parsed to an issue",
|
||||
input: "unparseable",
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := &cmdutil.Factory{}
|
||||
|
||||
argv, err := shlex.Split(tt.input)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts T
|
||||
cmd := fn(f, func(opts *T) error {
|
||||
gotOpts = *opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmdutil.EnableRepoOverride(cmd, f)
|
||||
|
||||
// TODO: remember why we do this
|
||||
cmd.Flags().BoolP("help", "x", false, "")
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
|
||||
if tt.expectErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
actualIssueNumber := issueNumberFromOpts(t, gotOpts)
|
||||
assert.Equal(t, tt.expectedissueNumber, actualIssueNumber)
|
||||
|
||||
actualBaseRepo := baseRepoFromOpts(t, gotOpts)
|
||||
assert.True(
|
||||
t,
|
||||
ghrepo.IsSame(tt.expectedBaseRepo, actualBaseRepo),
|
||||
"expected base repo %+v, got %+v", tt.expectedBaseRepo, actualBaseRepo,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func issueNumberFromOpts(t *testing.T, v any) int {
|
||||
rv := reflect.ValueOf(v)
|
||||
field := rv.FieldByName("IssueNumber")
|
||||
if !field.IsValid() || field.Kind() != reflect.Int {
|
||||
t.Fatalf("Type %T does not have IssueNumber int field", v)
|
||||
}
|
||||
return int(field.Int())
|
||||
}
|
||||
|
||||
func baseRepoFromOpts(t *testing.T, v any) ghrepo.Interface {
|
||||
rv := reflect.ValueOf(v)
|
||||
field := rv.FieldByName("BaseRepo")
|
||||
// check whether the field is valid and of type func() (ghrepo.Interface, error)
|
||||
if !field.IsValid() || field.Kind() != reflect.Func {
|
||||
t.Fatalf("Type %T does not have BaseRepo func field", v)
|
||||
}
|
||||
// call the function and check the return value
|
||||
results := field.Call([]reflect.Value{})
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("%T.BaseRepo() does not return two values", v)
|
||||
}
|
||||
if !results[1].IsNil() {
|
||||
t.Fatalf("%T.BaseRepo() returned an error: %v", v, results[1].Interface())
|
||||
}
|
||||
return results[0].Interface().(ghrepo.Interface)
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ type CloseOptions struct {
|
|||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
|
||||
SelectorArg string
|
||||
IssueNumber int
|
||||
Comment string
|
||||
Reason string
|
||||
|
||||
|
|
@ -39,13 +39,23 @@ func NewCmdClose(f *cmdutil.Factory, runF func(*CloseOptions) error) *cobra.Comm
|
|||
Short: "Close issue",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
if len(args) > 0 {
|
||||
opts.SelectorArg = args[0]
|
||||
issueNumber, baseRepo, err := shared.ParseIssueFromArg(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the args provided the base repo then use that directly.
|
||||
if baseRepo, present := baseRepo.Value(); present {
|
||||
opts.BaseRepo = func() (ghrepo.Interface, error) {
|
||||
return baseRepo, nil
|
||||
}
|
||||
} else {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
}
|
||||
|
||||
opts.IssueNumber = issueNumber
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
|
|
@ -67,7 +77,12 @@ func closeRun(opts *CloseOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
issue, baseRepo, err := shared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.SelectorArg, []string{"id", "number", "title", "state"})
|
||||
baseRepo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
issue, err := shared.FindIssueOrPR(httpClient, baseRepo, opts.IssueNumber, []string{"id", "number", "title", "state"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,46 +7,32 @@ import (
|
|||
|
||||
fd "github.com/cli/cli/v2/internal/featuredetection"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/issue/argparsetest"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewCmdClose(t *testing.T) {
|
||||
// Test shared parsing of issue number / URL.
|
||||
argparsetest.TestArgParsing(t, NewCmdClose)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
output CloseOptions
|
||||
wantErr bool
|
||||
errMsg string
|
||||
name string
|
||||
input string
|
||||
output CloseOptions
|
||||
expectedBaseRepo ghrepo.Interface
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "no argument",
|
||||
input: "",
|
||||
wantErr: true,
|
||||
errMsg: "accepts 1 arg(s), received 0",
|
||||
},
|
||||
{
|
||||
name: "issue number",
|
||||
input: "123",
|
||||
output: CloseOptions{
|
||||
SelectorArg: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "issue url",
|
||||
input: "https://github.com/cli/cli/3",
|
||||
output: CloseOptions{
|
||||
SelectorArg: "https://github.com/cli/cli/3",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "comment",
|
||||
input: "123 --comment 'closing comment'",
|
||||
output: CloseOptions{
|
||||
SelectorArg: "123",
|
||||
IssueNumber: 123,
|
||||
Comment: "closing comment",
|
||||
},
|
||||
},
|
||||
|
|
@ -54,7 +40,7 @@ func TestNewCmdClose(t *testing.T) {
|
|||
name: "reason",
|
||||
input: "123 --reason 'not planned'",
|
||||
output: CloseOptions{
|
||||
SelectorArg: "123",
|
||||
IssueNumber: 123,
|
||||
Reason: "not planned",
|
||||
},
|
||||
},
|
||||
|
|
@ -79,15 +65,24 @@ func TestNewCmdClose(t *testing.T) {
|
|||
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, tt.errMsg, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.output.SelectorArg, gotOpts.SelectorArg)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.output.IssueNumber, gotOpts.IssueNumber)
|
||||
assert.Equal(t, tt.output.Comment, gotOpts.Comment)
|
||||
assert.Equal(t, tt.output.Reason, gotOpts.Reason)
|
||||
if tt.expectedBaseRepo != nil {
|
||||
baseRepo, err := gotOpts.BaseRepo()
|
||||
require.NoError(t, err)
|
||||
require.True(
|
||||
t,
|
||||
ghrepo.IsSame(tt.expectedBaseRepo, baseRepo),
|
||||
"expected base repo %+v, got %+v", tt.expectedBaseRepo, baseRepo,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -104,7 +99,7 @@ func TestCloseRun(t *testing.T) {
|
|||
{
|
||||
name: "close issue by number",
|
||||
opts: &CloseOptions{
|
||||
SelectorArg: "13",
|
||||
IssueNumber: 13,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
|
|
@ -128,7 +123,7 @@ func TestCloseRun(t *testing.T) {
|
|||
{
|
||||
name: "close issue with comment",
|
||||
opts: &CloseOptions{
|
||||
SelectorArg: "13",
|
||||
IssueNumber: 13,
|
||||
Comment: "closing comment",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
|
|
@ -164,7 +159,7 @@ func TestCloseRun(t *testing.T) {
|
|||
{
|
||||
name: "close issue with reason",
|
||||
opts: &CloseOptions{
|
||||
SelectorArg: "13",
|
||||
IssueNumber: 13,
|
||||
Reason: "not planned",
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
},
|
||||
|
|
@ -192,7 +187,7 @@ func TestCloseRun(t *testing.T) {
|
|||
{
|
||||
name: "close issue with reason when reason is not supported",
|
||||
opts: &CloseOptions{
|
||||
SelectorArg: "13",
|
||||
IssueNumber: 13,
|
||||
Reason: "not planned",
|
||||
Detector: &fd.DisabledDetectorMock{},
|
||||
},
|
||||
|
|
@ -219,7 +214,7 @@ func TestCloseRun(t *testing.T) {
|
|||
{
|
||||
name: "issue already closed",
|
||||
opts: &CloseOptions{
|
||||
SelectorArg: "13",
|
||||
IssueNumber: 13,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
|
|
@ -236,7 +231,7 @@ func TestCloseRun(t *testing.T) {
|
|||
{
|
||||
name: "issues disabled",
|
||||
opts: &CloseOptions{
|
||||
SelectorArg: "13",
|
||||
IssueNumber: 13,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package comment
|
|||
import (
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/issue/shared"
|
||||
issueShared "github.com/cli/cli/v2/pkg/cmd/issue/shared"
|
||||
prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
|
|
@ -17,6 +18,7 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) e
|
|||
InteractiveEditSurvey: prShared.CommentableInteractiveEditSurvey(f.Config, f.IOStreams),
|
||||
ConfirmSubmitSurvey: prShared.CommentableConfirmSubmitSurvey(f.Prompter),
|
||||
ConfirmCreateIfNoneSurvey: prShared.CommentableInteractiveCreateIfNoneSurvey(f.Prompter),
|
||||
ConfirmDeleteLastComment: prShared.CommentableConfirmDeleteLastComment(f.Prompter),
|
||||
OpenInBrowser: f.Browser.Browse,
|
||||
}
|
||||
|
||||
|
|
@ -37,15 +39,41 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) e
|
|||
Args: cobra.ExactArgs(1),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.RetrieveCommentable = func() (prShared.Commentable, ghrepo.Interface, error) {
|
||||
// TODO wm: more testing
|
||||
issueNumber, parsedBaseRepo, err := shared.ParseIssueFromArg(args[0])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// If the args provided the base repo then use that directly.
|
||||
var baseRepo ghrepo.Interface
|
||||
|
||||
if parsedBaseRepo, present := parsedBaseRepo.Value(); present {
|
||||
baseRepo = parsedBaseRepo
|
||||
} else {
|
||||
// support `-R, --repo` override
|
||||
baseRepo, err = f.BaseRepo()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
httpClient, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
fields := []string{"id", "url"}
|
||||
if opts.EditLast {
|
||||
if opts.EditLast || opts.DeleteLast {
|
||||
fields = append(fields, "comments")
|
||||
}
|
||||
return issueShared.IssueFromArgWithFields(httpClient, f.BaseRepo, args[0], fields)
|
||||
|
||||
issue, err := issueShared.FindIssueOrPR(httpClient, baseRepo, issueNumber, fields)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return issue, baseRepo, nil
|
||||
}
|
||||
return prShared.CommentablePreRun(cmd, opts)
|
||||
},
|
||||
|
|
@ -69,7 +97,9 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) e
|
|||
cmd.Flags().StringVarP(&bodyFile, "body-file", "F", "", "Read body text from `file` (use \"-\" to read from standard input)")
|
||||
cmd.Flags().BoolP("editor", "e", false, "Skip prompts and open the text editor to write the body in")
|
||||
cmd.Flags().BoolP("web", "w", false, "Open the web browser to write the comment")
|
||||
cmd.Flags().BoolVar(&opts.EditLast, "edit-last", false, "Edit the last comment of the same author")
|
||||
cmd.Flags().BoolVar(&opts.EditLast, "edit-last", false, "Edit the last comment of the current user")
|
||||
cmd.Flags().BoolVar(&opts.DeleteLast, "delete-last", false, "Delete the last comment of the current user")
|
||||
cmd.Flags().BoolVar(&opts.DeleteLastConfirmed, "yes", false, "Skip the delete confirmation prompt when --delete-last is provided")
|
||||
cmd.Flags().BoolVar(&opts.CreateIfNone, "create-if-none", false, "Create a new comment if no comments are found. Can be used only with --edit-last")
|
||||
|
||||
return cmd
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package comment
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
|
@ -31,11 +32,13 @@ func TestNewCmdComment(t *testing.T) {
|
|||
stdin string
|
||||
output shared.CommentableOptions
|
||||
wantsErr bool
|
||||
isTTY bool
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
input: "",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -46,6 +49,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: 0,
|
||||
Body: "",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -56,6 +60,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: 0,
|
||||
Body: "",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -66,6 +71,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: shared.InputTypeInline,
|
||||
Body: "test",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -77,6 +83,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: shared.InputTypeInline,
|
||||
Body: "this is on standard input",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -87,6 +94,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: shared.InputTypeInline,
|
||||
Body: "a body from file",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -97,6 +105,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -107,6 +116,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -118,6 +128,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
Body: "",
|
||||
EditLast: true,
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
|
|
@ -130,42 +141,110 @@ func TestNewCmdComment(t *testing.T) {
|
|||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "delete last flag non-interactive",
|
||||
input: "1 --delete-last",
|
||||
isTTY: false,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "delete last flag and pre-confirmation non-interactive",
|
||||
input: "1 --delete-last --yes",
|
||||
output: shared.CommentableOptions{
|
||||
DeleteLast: true,
|
||||
DeleteLastConfirmed: true,
|
||||
},
|
||||
isTTY: false,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "delete last flag interactive",
|
||||
input: "1 --delete-last",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "delete last flag and pre-confirmation interactive",
|
||||
input: "1 --delete-last --yes",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
DeleteLastConfirmed: true,
|
||||
},
|
||||
isTTY: true,
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "delete last flag and pre-confirmation with web flag",
|
||||
input: "1 --delete-last --yes --web",
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "delete last flag and pre-confirmation with editor flag",
|
||||
input: "1 --delete-last --yes --editor",
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "delete last flag and pre-confirmation with body flag",
|
||||
input: "1 --delete-last --yes --body",
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "delete pre-confirmation without delete last flag",
|
||||
input: "1 --yes",
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "body and body-file flags",
|
||||
input: "1 --body 'test' --body-file 'test-file.txt'",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "editor and web flags",
|
||||
input: "1 --editor --web",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "editor and body flags",
|
||||
input: "1 --editor --body test",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "web and body flags",
|
||||
input: "1 --web --body test",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "editor, web, and body flags",
|
||||
input: "1 --editor --web --body test",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "create-if-none flag without edit-last",
|
||||
input: "1 --create-if-none",
|
||||
output: shared.CommentableOptions{},
|
||||
isTTY: true,
|
||||
wantsErr: true,
|
||||
},
|
||||
}
|
||||
|
|
@ -173,9 +252,10 @@ func TestNewCmdComment(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, stdin, _, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
ios.SetStdinTTY(true)
|
||||
ios.SetStderrTTY(true)
|
||||
isTTY := tt.isTTY
|
||||
ios.SetStdoutTTY(isTTY)
|
||||
ios.SetStdinTTY(isTTY)
|
||||
ios.SetStderrTTY(isTTY)
|
||||
|
||||
if tt.stdin != "" {
|
||||
_, _ = stdin.WriteString(tt.stdin)
|
||||
|
|
@ -211,6 +291,8 @@ func TestNewCmdComment(t *testing.T) {
|
|||
assert.Equal(t, tt.output.Interactive, gotOpts.Interactive)
|
||||
assert.Equal(t, tt.output.InputType, gotOpts.InputType)
|
||||
assert.Equal(t, tt.output.Body, gotOpts.Body)
|
||||
assert.Equal(t, tt.output.DeleteLast, gotOpts.DeleteLast)
|
||||
assert.Equal(t, tt.output.DeleteLastConfirmed, gotOpts.DeleteLastConfirmed)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -220,6 +302,7 @@ func Test_commentRun(t *testing.T) {
|
|||
name string
|
||||
input *shared.CommentableOptions
|
||||
emptyComments bool
|
||||
comments api.Comments
|
||||
httpStubs func(*testing.T, *httpmock.Registry)
|
||||
stdout string
|
||||
stderr string
|
||||
|
|
@ -255,6 +338,7 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
stdout: "no comments found for current user",
|
||||
},
|
||||
{
|
||||
name: "updating last comment with interactive editor succeeds if there are comments",
|
||||
|
|
@ -331,6 +415,7 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
stdout: "no comments found for current user",
|
||||
},
|
||||
{
|
||||
name: "creating new comment with non-interactive editor succeeds",
|
||||
|
|
@ -358,6 +443,7 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
stdout: "no comments found for current user",
|
||||
},
|
||||
{
|
||||
name: "updating last comment with non-interactive editor succeeds if there are comments",
|
||||
|
|
@ -433,6 +519,117 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment non-interactively without any comment",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
DeleteLast: true,
|
||||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
stdout: "no comments found for current user",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment interactively without any comment",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
},
|
||||
emptyComments: true,
|
||||
wantsErr: true,
|
||||
stdout: "no comments found for current user",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment non-interactively and pre-confirmed",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
DeleteLast: true,
|
||||
DeleteLastConfirmed: true,
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentDelete(t, reg)
|
||||
},
|
||||
stderr: "Comment deleted\n",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment interactively and pre-confirmed",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
DeleteLastConfirmed: true,
|
||||
},
|
||||
comments: api.Comments{Nodes: []api.Comment{
|
||||
{ID: "id1", Author: api.CommentAuthor{Login: "octocat"}, URL: "https://github.com/OWNER/REPO/pull/123#issuecomment-111", ViewerDidAuthor: true, Body: "comment body"},
|
||||
}},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentDelete(t, reg)
|
||||
},
|
||||
stderr: "Comment deleted\n",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment interactively and confirmed",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
|
||||
ConfirmDeleteLastComment: func(body string) (bool, error) {
|
||||
if body != "comment body" {
|
||||
return false, errors.New("unexpected comment body")
|
||||
}
|
||||
return true, nil
|
||||
},
|
||||
},
|
||||
comments: api.Comments{Nodes: []api.Comment{
|
||||
{ID: "id1", Author: api.CommentAuthor{Login: "octocat"}, URL: "https://github.com/OWNER/REPO/pull/123#issuecomment-111", ViewerDidAuthor: true, Body: "comment body"},
|
||||
}},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentDelete(t, reg)
|
||||
},
|
||||
stdout: "! Deleted comments cannot be recovered.\n",
|
||||
stderr: "Comment deleted\n",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment interactively and confirmation declined",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
|
||||
ConfirmDeleteLastComment: func(body string) (bool, error) {
|
||||
if body != "comment body" {
|
||||
return false, errors.New("unexpected comment body")
|
||||
}
|
||||
return true, nil
|
||||
},
|
||||
},
|
||||
comments: api.Comments{Nodes: []api.Comment{
|
||||
{ID: "id1", Author: api.CommentAuthor{Login: "octocat"}, URL: "https://github.com/OWNER/REPO/pull/123#issuecomment-111", ViewerDidAuthor: true, Body: "comment body"},
|
||||
}},
|
||||
wantsErr: true,
|
||||
stdout: "deletion not confirmed",
|
||||
},
|
||||
{
|
||||
name: "deleting last comment interactively and confirmed with long comment body",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
DeleteLast: true,
|
||||
|
||||
ConfirmDeleteLastComment: func(body string) (bool, error) {
|
||||
if body != "Lorem ipsum dolor sit amet, consectet lo..." {
|
||||
return false, errors.New("unexpected comment body")
|
||||
}
|
||||
return true, nil
|
||||
},
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentDelete(t, reg)
|
||||
},
|
||||
comments: api.Comments{Nodes: []api.Comment{
|
||||
{ID: "id1", Author: api.CommentAuthor{Login: "octocat"}, URL: "https://github.com/OWNER/REPO/pull/123#issuecomment-111", ViewerDidAuthor: true, Body: "Lorem ipsum dolor sit amet, consectet lorem ipsum again"},
|
||||
}},
|
||||
wantsErr: false,
|
||||
stdout: "! Deleted comments cannot be recovered.\n",
|
||||
stderr: "Comment deleted\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
|
|
@ -458,6 +655,8 @@ func Test_commentRun(t *testing.T) {
|
|||
|
||||
if tt.emptyComments {
|
||||
comments.Nodes = []api.Comment{}
|
||||
} else if len(tt.comments.Nodes) > 0 {
|
||||
comments = tt.comments
|
||||
}
|
||||
|
||||
tt.input.RetrieveCommentable = func() (shared.Commentable, ghrepo.Interface, error) {
|
||||
|
|
@ -472,6 +671,7 @@ func Test_commentRun(t *testing.T) {
|
|||
err := shared.CommentableRun(tt.input)
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.stderr, stderr.String())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -508,3 +708,15 @@ func mockCommentUpdate(t *testing.T, reg *httpmock.Registry) {
|
|||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func mockCommentDelete(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation CommentDelete\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "deleteIssueComment": {} } }`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, "id1", inputs["id"])
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
fd "github.com/cli/cli/v2/internal/featuredetection"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
|
|
@ -24,6 +26,7 @@ type CreateOptions struct {
|
|||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Browser browser.Browser
|
||||
Prompter prShared.Prompt
|
||||
Detector fd.Detector
|
||||
TitledEditSurvey func(string, string) (string, string, error)
|
||||
|
||||
RootDirOverride string
|
||||
|
|
@ -46,11 +49,12 @@ type CreateOptions struct {
|
|||
|
||||
func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
|
||||
opts := &CreateOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Config: f.Config,
|
||||
Browser: f.Browser,
|
||||
Prompter: f.Prompter,
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Config: f.Config,
|
||||
Browser: f.Browser,
|
||||
Prompter: f.Prompter,
|
||||
|
||||
TitledEditSurvey: prShared.TitledEditSurvey(&prShared.UserEditor{Config: f.Config, IO: f.IOStreams}),
|
||||
}
|
||||
|
||||
|
|
@ -146,6 +150,15 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// TODO projectsV1Deprecation
|
||||
// Remove this section as we should no longer need to detect
|
||||
if opts.Detector == nil {
|
||||
cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
|
||||
opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost())
|
||||
}
|
||||
|
||||
projectsV1Support := opts.Detector.ProjectsV1()
|
||||
|
||||
isTerminal := opts.IO.IsStdoutTTY()
|
||||
|
||||
var milestones []string
|
||||
|
|
@ -160,13 +173,13 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
}
|
||||
|
||||
tb := prShared.IssueMetadataState{
|
||||
Type: prShared.IssueMetadata,
|
||||
Assignees: assignees,
|
||||
Labels: opts.Labels,
|
||||
Projects: opts.Projects,
|
||||
Milestones: milestones,
|
||||
Title: opts.Title,
|
||||
Body: opts.Body,
|
||||
Type: prShared.IssueMetadata,
|
||||
Assignees: assignees,
|
||||
Labels: opts.Labels,
|
||||
ProjectTitles: opts.Projects,
|
||||
Milestones: milestones,
|
||||
Title: opts.Title,
|
||||
Body: opts.Body,
|
||||
}
|
||||
|
||||
if opts.RecoverFile != "" {
|
||||
|
|
@ -182,7 +195,7 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
if opts.WebMode {
|
||||
var openURL string
|
||||
if opts.Title != "" || opts.Body != "" || tb.HasMetadata() {
|
||||
openURL, err = generatePreviewURL(apiClient, baseRepo, tb)
|
||||
openURL, err = generatePreviewURL(apiClient, baseRepo, tb, projectsV1Support)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -260,7 +273,7 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
openURL, err = generatePreviewURL(apiClient, baseRepo, tb)
|
||||
openURL, err = generatePreviewURL(apiClient, baseRepo, tb, projectsV1Support)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -279,7 +292,7 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
Repo: baseRepo,
|
||||
State: &tb,
|
||||
}
|
||||
err = prShared.MetadataSurvey(opts.Prompter, opts.IO, baseRepo, fetcher, &tb)
|
||||
err = prShared.MetadataSurvey(opts.Prompter, opts.IO, baseRepo, fetcher, &tb, projectsV1Support)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -335,7 +348,7 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
params["issueTemplate"] = templateNameForSubmit
|
||||
}
|
||||
|
||||
err = prShared.AddMetadataToIssueParams(apiClient, baseRepo, params, &tb)
|
||||
err = prShared.AddMetadataToIssueParams(apiClient, baseRepo, params, &tb, projectsV1Support)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -354,7 +367,7 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func generatePreviewURL(apiClient *api.Client, baseRepo ghrepo.Interface, tb prShared.IssueMetadataState) (string, error) {
|
||||
func generatePreviewURL(apiClient *api.Client, baseRepo ghrepo.Interface, tb prShared.IssueMetadataState, projectsV1Support gh.ProjectsV1Support) (string, error) {
|
||||
openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new")
|
||||
return prShared.WithPrAndIssueQueryParams(apiClient, baseRepo, openURL, tb)
|
||||
return prShared.WithPrAndIssueQueryParams(apiClient, baseRepo, openURL, tb, projectsV1Support)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
fd "github.com/cli/cli/v2/internal/featuredetection"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
|
|
@ -473,6 +474,7 @@ func Test_createRun(t *testing.T) {
|
|||
opts.BaseRepo = func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
}
|
||||
opts.Detector = &fd.EnabledDetectorMock{}
|
||||
browser := &browser.Stub{}
|
||||
opts.Browser = browser
|
||||
|
||||
|
|
@ -521,6 +523,7 @@ func runCommandWithRootDirOverridden(rt http.RoundTripper, isTTY bool, cli strin
|
|||
|
||||
cmd := NewCmdCreate(factory, func(opts *CreateOptions) error {
|
||||
opts.RootDirOverride = rootDir
|
||||
opts.Detector = &fd.EnabledDetectorMock{}
|
||||
return createRun(opts)
|
||||
})
|
||||
|
||||
|
|
@ -1026,3 +1029,146 @@ func TestIssueCreate_projectsV2(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
|
||||
}
|
||||
|
||||
// TODO projectsV1Deprecation
|
||||
// Remove this test.
|
||||
func TestProjectsV1Deprecation(t *testing.T) {
|
||||
|
||||
t.Run("non-interactive submission", func(t *testing.T) {
|
||||
t.Run("when projects v1 is supported, queries for it", func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
reg.StubRepoInfoResponse("OWNER", "REPO", "main")
|
||||
reg.Register(
|
||||
// ( is required to avoid matching projectsV2
|
||||
httpmock.GraphQL(`projects\(`),
|
||||
// Simulate a GraphQL error to early exit the test.
|
||||
httpmock.StatusStringResponse(500, ""),
|
||||
)
|
||||
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
// Ignore the error because we have no way to really stub it without
|
||||
// fully stubbing a GQL error structure in the request body.
|
||||
_ = createRun(&CreateOptions{
|
||||
IO: ios,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
Title: "Test Title",
|
||||
Body: "Test Body",
|
||||
// Required to force a lookup of projects
|
||||
Projects: []string{"Project"},
|
||||
})
|
||||
|
||||
// Verify that our request contained projects
|
||||
reg.Verify(t)
|
||||
})
|
||||
|
||||
t.Run("when projects v1 is not supported, does not query for it", func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
reg.StubRepoInfoResponse("OWNER", "REPO", "main")
|
||||
// ( is required to avoid matching projectsV2
|
||||
reg.Exclude(t, httpmock.GraphQL(`projects\(`))
|
||||
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
// Ignore the error because we're not really interested in it.
|
||||
_ = createRun(&CreateOptions{
|
||||
IO: ios,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
|
||||
Detector: &fd.DisabledDetectorMock{},
|
||||
Title: "Test Title",
|
||||
Body: "Test Body",
|
||||
// Required to force a lookup of projects
|
||||
Projects: []string{"Project"},
|
||||
})
|
||||
|
||||
// Verify that our request contained projectCards
|
||||
reg.Verify(t)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("web mode", func(t *testing.T) {
|
||||
t.Run("when projects v1 is supported, queries for it", func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Register(
|
||||
// ( is required to avoid matching projectsV2
|
||||
httpmock.GraphQL(`projects\(`),
|
||||
// Simulate a GraphQL error to early exit the test.
|
||||
httpmock.StatusStringResponse(500, ""),
|
||||
)
|
||||
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
// Ignore the error because we have no way to really stub it without
|
||||
// fully stubbing a GQL error structure in the request body.
|
||||
_ = createRun(&CreateOptions{
|
||||
IO: ios,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
WebMode: true,
|
||||
// Required to force a lookup of projects
|
||||
Projects: []string{"Project"},
|
||||
})
|
||||
|
||||
// Verify that our request contained projects
|
||||
reg.Verify(t)
|
||||
})
|
||||
|
||||
t.Run("when projects v1 is not supported, does not query for it", func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
// ( is required to avoid matching projectsV2
|
||||
reg.Exclude(t, httpmock.GraphQL(`projects\(`))
|
||||
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
// Ignore the error because we're not really interested in it.
|
||||
_ = createRun(&CreateOptions{
|
||||
IO: ios,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
|
||||
Detector: &fd.DisabledDetectorMock{},
|
||||
WebMode: true,
|
||||
// Required to force a lookup of projects
|
||||
Projects: []string{"Project"},
|
||||
})
|
||||
|
||||
// Verify that our request contained projectCards
|
||||
reg.Verify(t)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
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