Merge branch 'trunk' into attestation-verify-ref-commit-policy-opts
This commit is contained in:
commit
ba5cf8e1ce
123 changed files with 5086 additions and 1353 deletions
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/go:1.22",
|
||||
"image": "mcr.microsoft.com/devcontainers/go:1.23",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/sshd:1": {}
|
||||
},
|
||||
|
|
|
|||
27
.github/CONTRIBUTING.md
vendored
27
.github/CONTRIBUTING.md
vendored
|
|
@ -4,23 +4,22 @@ Hi! Thanks for your interest in contributing to the GitHub CLI!
|
|||
|
||||
We accept pull requests for bug fixes and features where we've discussed the approach in an issue and given the go-ahead for a community member to work on it. We'd also love to hear about ideas for new features as issues.
|
||||
|
||||
Please do:
|
||||
### Please do:
|
||||
|
||||
* Check issues to verify that a [bug][bug issues] or [feature request][feature request issues] issue does not already exist for the same problem or feature.
|
||||
* Open an issue if things aren't working as expected.
|
||||
* Open an issue to propose a significant change.
|
||||
* Open an issue to propose a design for an issue labelled [`needs-design` and `help wanted`][needs design and help wanted], following the [proposing a design guidelines](#proposing-a-design) instructions below.
|
||||
* Open a pull request to fix a bug.
|
||||
* Open a pull request to fix documentation about a command.
|
||||
* Open a pull request for any issue labelled [`help wanted`][hw] or [`good first issue`][gfi].
|
||||
* Check issues to verify that a [bug][bug issues] or [feature request][feature request issues] issue does not already exist for the same problem or feature
|
||||
* Open an issue if things aren't working as expected
|
||||
* Open an issue to propose a significant change
|
||||
* Open an issue to propose a design for an issue labelled [`needs-design` and `help wanted`][needs design and help wanted], following the [proposing a design guidelines](#proposing-a-design) instructions below
|
||||
* Mention `@cli/code-reviewers` when an issue you want to work on does not have clear Acceptance Criteria
|
||||
* Open a pull request for any issue labelled [`help wanted`][hw] and [`good first issue`][gfi]
|
||||
|
||||
Please avoid:
|
||||
### Please _do not_:
|
||||
|
||||
* Opening pull requests for issues marked `needs-design`, `needs-investigation`, or `blocked`.
|
||||
* Opening pull requests that haven't been approved for work in an issue
|
||||
* Adding installation instructions specifically for your OS/package manager.
|
||||
* Opening pull requests for any issue marked `core`. These issues require additional context from
|
||||
the core CLI team at GitHub and any external pull requests will not be accepted.
|
||||
* Open a pull request for issues without the `help wanted` label or explicit Acceptance Criteria
|
||||
* Expand pull request scope to include changes that are not described in the issue's Acceptance Criteria
|
||||
* Add installation instructions specifically for your OS/package manager
|
||||
* Open pull requests for any issue marked `core`. These issues require additional context from
|
||||
the core CLI team at GitHub and any external pull requests will not be accepted
|
||||
|
||||
## Building the project
|
||||
|
||||
|
|
|
|||
12
.github/workflows/deployment.yml
vendored
12
.github/workflows/deployment.yml
vendored
|
|
@ -28,7 +28,17 @@ on:
|
|||
default: true
|
||||
|
||||
jobs:
|
||||
validate-tag-name:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Validate tag name format
|
||||
run: |
|
||||
if [[ ! "${{ inputs.tag_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Invalid tag name format. Must be in the form v1.2.3"
|
||||
exit 1
|
||||
fi
|
||||
linux:
|
||||
needs: validate-tag-name
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.environment }}
|
||||
if: contains(inputs.platforms, 'linux')
|
||||
|
|
@ -63,6 +73,7 @@ jobs:
|
|||
dist/*.deb
|
||||
|
||||
macos:
|
||||
needs: validate-tag-name
|
||||
runs-on: macos-latest
|
||||
environment: ${{ inputs.environment }}
|
||||
if: contains(inputs.platforms, 'macos')
|
||||
|
|
@ -134,6 +145,7 @@ jobs:
|
|||
dist/*.pkg
|
||||
|
||||
windows:
|
||||
needs: validate-tag-name
|
||||
runs-on: windows-latest
|
||||
environment: ${{ inputs.environment }}
|
||||
if: contains(inputs.platforms, 'windows')
|
||||
|
|
|
|||
16
.github/workflows/prauto.yml
vendored
16
.github/workflows/prauto.yml
vendored
|
|
@ -16,7 +16,6 @@ jobs:
|
|||
env:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }}
|
||||
PRID: ${{ github.event.pull_request.node_id }}
|
||||
PRBODY: ${{ github.event.pull_request.body }}
|
||||
PRNUM: ${{ github.event.pull_request.number }}
|
||||
PRHEAD: ${{ github.event.pull_request.head.label }}
|
||||
|
|
@ -43,26 +42,12 @@ jobs:
|
|||
-q ".data.repository.project.columns.nodes[] | select(.name | startswith(\"$1\")) | .id"
|
||||
}
|
||||
|
||||
addToBoard () {
|
||||
gh api graphql --silent -f query='
|
||||
mutation($colID:ID!, $prID:ID!) { addProjectCard(input: { projectColumnId: $colID, contentId: $prID }) { clientMutationId } }
|
||||
' -f colID="$(colID "Needs review")" -f prID="$PRID"
|
||||
}
|
||||
|
||||
if [ "$PR_AUTHOR_TYPE" = "Bot" ] || gh api orgs/cli/public_members/$PRAUTHOR --silent 2>/dev/null
|
||||
then
|
||||
if [ "$PR_AUTHOR_TYPE" != "Bot" ]
|
||||
then
|
||||
gh pr edit $PRNUM --add-assignee $PRAUTHOR
|
||||
fi
|
||||
if ! errtext="$(addToBoard 2>&1)"
|
||||
then
|
||||
cat <<<"$errtext" >&2
|
||||
if ! grep -iq 'project already has the associated issue' <<<"$errtext"
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
|
@ -86,5 +71,4 @@ jobs:
|
|||
commentPR "Hi! Thanks for the pull request. Please ensure that this change is linked to an issue by mentioning an issue number in the description of the pull request. If this pull request would close the issue, please put the word 'Fixes' before the issue number somewhere in the pull request body. If this is a tiny change like fixing a typo, feel free to ignore this message."
|
||||
fi
|
||||
|
||||
addToBoard
|
||||
exit 0
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ There are two common ways to verify a downloaded release, depending if `gh` is a
|
|||
- **Option 1: Using `gh` if already installed:**
|
||||
|
||||
```shell
|
||||
$ % gh at verify -R cli/cli gh_2.62.0_macOS_arm64.zip
|
||||
$ gh at verify -R cli/cli gh_2.62.0_macOS_arm64.zip
|
||||
Loaded digest sha256:fdb77f31b8a6dd23c3fd858758d692a45f7fc76383e37d475bdcae038df92afc for file://gh_2.62.0_macOS_arm64.zip
|
||||
Loaded 1 attestation from GitHub API
|
||||
✓ Verification succeeded!
|
||||
|
|
|
|||
28
acceptance/testdata/pr/pr-view-outside-repo.txtar
vendored
Normal file
28
acceptance/testdata/pr/pr-view-outside-repo.txtar
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Prepare a branch to PR
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec git checkout -b feature-branch
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push -u origin feature-branch
|
||||
|
||||
# Create the PR
|
||||
exec gh pr create --title 'Feature Title' --body 'Feature Body'
|
||||
stdout2env PR_URL
|
||||
|
||||
# Change directories
|
||||
cd ..
|
||||
|
||||
# View the PR
|
||||
exec gh pr view $PR_URL
|
||||
stdout 'Feature Title'
|
||||
45
acceptance/testdata/pr/pr-view-status-respects-branch-pushremote.txtar
vendored
Normal file
45
acceptance/testdata/pr/pr-view-status-respects-branch-pushremote.txtar
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# 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
|
||||
|
||||
# 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}
|
||||
exec gh repo view ${ORG}/${REPO} --json id --jq '.id'
|
||||
stdout2env REPO_ID
|
||||
|
||||
# Create a user fork of repository as opposed to private organization fork
|
||||
exec gh repo fork ${ORG}/${REPO} --org ${ORG} --fork-name ${FORK}
|
||||
|
||||
# Defer repo cleanup of fork
|
||||
defer gh repo delete --yes ${ORG}/${FORK}
|
||||
sleep 5
|
||||
exec gh repo view ${ORG}/${FORK} --json id --jq '.id'
|
||||
stdout2env FORK_ID
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone ${ORG}/${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 upstream/main
|
||||
exec git config branch.feature-branch.pushRemote origin
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push
|
||||
|
||||
# Create the PR spanning upstream and fork repositories, gh pr create does not support headRepositoryId needed for private forks
|
||||
exec gh api graphql -F repositoryId="${REPO_ID}" -F headRepositoryId="${FORK_ID}" -F query='mutation CreatePullRequest($headRepositoryId: ID!, $repositoryId: ID!) { createPullRequest(input:{ baseRefName: "main", body: "Feature Body", draft: false, headRefName: "feature-branch", headRepositoryId: $headRepositoryId, repositoryId: $repositoryId, title:"Feature Title" }){ pullRequest{ id url } } }'
|
||||
|
||||
# View the PR
|
||||
exec gh pr view
|
||||
stdout 'Feature Title'
|
||||
|
||||
# Check the PR status
|
||||
env PR_STATUS_BRANCH=#1 Feature Title [${ORG}:feature-branch]
|
||||
exec gh pr status
|
||||
stdout $PR_STATUS_BRANCH
|
||||
38
acceptance/testdata/pr/pr-view-status-respects-push-destination.txtar
vendored
Normal file
38
acceptance/testdata/pr/pr-view-status-respects-push-destination.txtar
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Setup environment variables used for testscript
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
|
||||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create ${ORG}/${REPO} --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes ${ORG}/${REPO}
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone ${ORG}/${REPO}
|
||||
cd ${REPO}
|
||||
|
||||
# Configure default push behavior so local and remote branches will be the same
|
||||
exec git config push.default current
|
||||
|
||||
# Prepare a branch where changes are pulled from the default branch instead of remote branch of same name
|
||||
exec git checkout -b feature-branch
|
||||
exec git branch --set-upstream-to origin/main
|
||||
exec git rev-parse --abbrev-ref feature-branch@{upstream}
|
||||
stdout origin/main
|
||||
|
||||
# Create the PR
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push
|
||||
exec gh pr create -B main -H feature-branch --title 'Feature Title' --body 'Feature Body'
|
||||
|
||||
# View the PR
|
||||
exec gh pr view
|
||||
stdout 'Feature Title'
|
||||
|
||||
# Check the PR status
|
||||
env PR_STATUS_BRANCH=#1 Feature Title [feature-branch]
|
||||
exec gh pr status
|
||||
stdout $PR_STATUS_BRANCH
|
||||
45
acceptance/testdata/pr/pr-view-status-respects-remote-pushdefault.txtar
vendored
Normal file
45
acceptance/testdata/pr/pr-view-status-respects-remote-pushdefault.txtar
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# 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
|
||||
|
||||
# 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}
|
||||
exec gh repo view ${ORG}/${REPO} --json id --jq '.id'
|
||||
stdout2env REPO_ID
|
||||
|
||||
# Create a user fork of repository as opposed to private organization fork
|
||||
exec gh repo fork ${ORG}/${REPO} --org ${ORG} --fork-name ${FORK}
|
||||
|
||||
# Defer repo cleanup of fork
|
||||
defer gh repo delete --yes ${ORG}/${FORK}
|
||||
sleep 5
|
||||
exec gh repo view ${ORG}/${FORK} --json id --jq '.id'
|
||||
stdout2env FORK_ID
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone ${ORG}/${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 upstream/main
|
||||
exec git config remote.pushDefault origin
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push
|
||||
|
||||
# Create the PR spanning upstream and fork repositories, gh pr create does not support headRepositoryId needed for private forks
|
||||
exec gh api graphql -F repositoryId="${REPO_ID}" -F headRepositoryId="${FORK_ID}" -F query='mutation CreatePullRequest($headRepositoryId: ID!, $repositoryId: ID!) { createPullRequest(input:{ baseRefName: "main", body: "Feature Body", draft: false, headRefName: "feature-branch", headRepositoryId: $headRepositoryId, repositoryId: $repositoryId, title:"Feature Title" }){ pullRequest{ id url } } }'
|
||||
|
||||
# View the PR
|
||||
exec gh pr view
|
||||
stdout 'Feature Title'
|
||||
|
||||
# Check the PR status
|
||||
env PR_STATUS_BRANCH=#1 Feature Title [${ORG}:feature-branch]
|
||||
exec gh pr status
|
||||
stdout $PR_STATUS_BRANCH
|
||||
35
acceptance/testdata/pr/pr-view-status-respects-simple-pushdefault.txtar
vendored
Normal file
35
acceptance/testdata/pr/pr-view-status-respects-simple-pushdefault.txtar
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Setup environment variables used for testscript
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
|
||||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create ${ORG}/${REPO} --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes ${ORG}/${REPO}
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone ${ORG}/${REPO}
|
||||
cd ${REPO}
|
||||
|
||||
# Configure default push behavior so local and remote branches have to be the same
|
||||
exec git config push.default simple
|
||||
|
||||
# Prepare a branch where changes are pulled from the default branch instead of remote branch of same name
|
||||
exec git checkout -b feature-branch origin/main
|
||||
|
||||
# Create the PR
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push origin feature-branch
|
||||
exec gh pr create -H feature-branch --title 'Feature Title' --body 'Feature Body'
|
||||
|
||||
# View the PR
|
||||
exec gh pr view
|
||||
stdout 'Feature Title'
|
||||
|
||||
# Check the PR status
|
||||
env PR_STATUS_BRANCH=#1 Feature Title [feature-branch]
|
||||
exec gh pr status
|
||||
stdout $PR_STATUS_BRANCH
|
||||
9
acceptance/testdata/repo/repo-rename-transfer-ownership.txtar
vendored
Normal file
9
acceptance/testdata/repo/repo-rename-transfer-ownership.txtar
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Create a repository with a file so it has a default branch
|
||||
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>.'
|
||||
|
||||
# Defer repo deletion
|
||||
defer gh repo delete $ORG/$SCRIPT_NAME-$RANDOM_STRING --yes
|
||||
|
|
@ -99,7 +99,7 @@ func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams) (ghrepo.Interface, e
|
|||
cs := io.ColorScheme()
|
||||
|
||||
fmt.Fprintf(io.ErrOut,
|
||||
"%s No default remote repository has been set for this directory.\n",
|
||||
"%s No default remote repository has been set. To learn more about the default repository, run: gh repo set-default --help\n",
|
||||
cs.FailureIcon())
|
||||
|
||||
fmt.Fprintln(io.Out)
|
||||
|
|
|
|||
|
|
@ -1,39 +1,52 @@
|
|||
# Releasing
|
||||
|
||||
To initiate a new production deployment:
|
||||
|
||||
```sh
|
||||
script/release vX.Y.Z
|
||||
```
|
||||
|
||||
See `script/release --help` for more information.
|
||||
|
||||
> **Note:**
|
||||
> Every production release will request an approval by the select few people before it can proceed.
|
||||
> [!NOTE]
|
||||
> Deployment workflow requires maintainer approval to run.
|
||||
|
||||
What this does is:
|
||||
|
||||
- Builds Linux binaries on Ubuntu;
|
||||
- Builds and signs Windows binaries on Windows;
|
||||
- Builds, signs, and notarizes macOS binaries on macOS;
|
||||
- Uploads all release artifacts to a new GitHub Release;
|
||||
- A new git tag `vX.Y.Z` is created in the remote repository;
|
||||
- The changelog is [generated from the list of merged pull requests](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes);
|
||||
- Updates cli.github.com with the contents of the new release;
|
||||
- Updates [GitHub CLI marketing site](https://cli.github.com) with the contents of the new release;
|
||||
- Updates the [`gh` Homebrew formula](https://github.com/williammartin/homebrew-core/blob/master/Formula/g/gh.rb) in the [`homebrew/homebrew-core` repo](https://github.com/search?q=repo%3AHomebrew%2Fhomebrew-core+%22gh%22+in%3Atitle&type=pullrequests).
|
||||
|
||||
> [!NOTE]
|
||||
> `Homebrew/formulae.brew.sh` makes new formula versions available every 15 minutes through scheduled [CI workflow](https://github.com/Homebrew/formulae.brew.sh/actions/workflows/tests.yml).
|
||||
>
|
||||
> For more information, see https://docs.brew.sh/Formula-Cookbook#an-introduction
|
||||
|
||||
To test out the build system while avoiding creating an actual release:
|
||||
|
||||
```sh
|
||||
script/release --staging vX.Y.Z --branch patch-1 -p macos
|
||||
```
|
||||
|
||||
The build artifacts will be available via `gh run download <RUN> -n macos`.
|
||||
|
||||
## General guidelines
|
||||
|
||||
* Features to be released should be reviewed and approved at least one day prior to the release.
|
||||
* Feature releases should bump up the minor version number.
|
||||
* Breaking releases should bump up the major version number. These should generally be rare.
|
||||
- Features to be released should be reviewed and approved at least one day prior
|
||||
to the release.
|
||||
- Feature releases should bump up the minor version number.
|
||||
- Breaking releases should bump up the major version number. These should
|
||||
generally be rare.
|
||||
|
||||
## Test the build system locally
|
||||
|
||||
A local release can be created for testing without creating anything official on the release page.
|
||||
A local release can be created for testing without creating anything official on
|
||||
the release page.
|
||||
|
||||
1. Make sure GoReleaser is installed: `brew install goreleaser`
|
||||
2. `script/release --local`
|
||||
|
|
@ -45,5 +58,6 @@ Occasionally, it might be necessary to clean up a bad release and re-release.
|
|||
|
||||
1. Delete the release and associated tag
|
||||
2. Re-release and monitor the workflow run logs
|
||||
3. Open pull request updating [`gh` Homebrew formula](https://github.com/williammartin/homebrew-core/blob/master/Formula/g/gh.rb) with new SHA versions, linking the previous PR
|
||||
3. Open pull request updating [`gh` Homebrew formula](https://github.com/williammartin/homebrew-core/blob/master/Formula/g/gh.rb)
|
||||
with new SHA versions, linking the previous PR
|
||||
4. Verify resulting Debian and RPM packages, Homebrew formula
|
||||
|
|
|
|||
|
|
@ -376,20 +376,20 @@ func (c *Client) lookupCommit(ctx context.Context, sha, format string) ([]byte,
|
|||
return out, nil
|
||||
}
|
||||
|
||||
// ReadBranchConfig parses the `branch.BRANCH.(remote|merge|gh-merge-base)` part of git config.
|
||||
// ReadBranchConfig parses the `branch.BRANCH.(remote|merge|pushremote|gh-merge-base)` part of git config.
|
||||
// If no branch config is found or there is an error in the command, it returns an empty BranchConfig.
|
||||
// Downstream consumers of ReadBranchConfig should consider the behavior they desire if this errors,
|
||||
// as an empty config is not necessarily breaking.
|
||||
func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (BranchConfig, error) {
|
||||
|
||||
prefix := regexp.QuoteMeta(fmt.Sprintf("branch.%s.", branch))
|
||||
args := []string{"config", "--get-regexp", fmt.Sprintf("^%s(remote|merge|%s)$", prefix, MergeBaseConfig)}
|
||||
args := []string{"config", "--get-regexp", fmt.Sprintf("^%s(remote|merge|pushremote|%s)$", prefix, MergeBaseConfig)}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return BranchConfig{}, err
|
||||
}
|
||||
|
||||
out, err := cmd.Output()
|
||||
branchCfgOut, err := cmd.Output()
|
||||
if err != nil {
|
||||
// This is the error we expect if the git command does not run successfully.
|
||||
// If the ExitCode is 1, then we just didn't find any config for the branch.
|
||||
|
|
@ -400,13 +400,14 @@ func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (BranchCon
|
|||
return BranchConfig{}, nil
|
||||
}
|
||||
|
||||
return parseBranchConfig(outputLines(out)), nil
|
||||
return parseBranchConfig(outputLines(branchCfgOut)), nil
|
||||
}
|
||||
|
||||
func parseBranchConfig(configLines []string) BranchConfig {
|
||||
func parseBranchConfig(branchConfigLines []string) BranchConfig {
|
||||
var cfg BranchConfig
|
||||
|
||||
for _, line := range configLines {
|
||||
// Read the config lines for the specific branch
|
||||
for _, line := range branchConfigLines {
|
||||
parts := strings.SplitN(line, " ", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
|
|
@ -414,21 +415,16 @@ func parseBranchConfig(configLines []string) BranchConfig {
|
|||
keys := strings.Split(parts[0], ".")
|
||||
switch keys[len(keys)-1] {
|
||||
case "remote":
|
||||
if strings.Contains(parts[1], ":") {
|
||||
u, err := ParseURL(parts[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
cfg.RemoteURL = u
|
||||
} else if !isFilesystemPath(parts[1]) {
|
||||
cfg.RemoteName = parts[1]
|
||||
}
|
||||
cfg.RemoteURL, cfg.RemoteName = parseRemoteURLOrName(parts[1])
|
||||
case "pushremote":
|
||||
cfg.PushRemoteURL, cfg.PushRemoteName = parseRemoteURLOrName(parts[1])
|
||||
case "merge":
|
||||
cfg.MergeRef = parts[1]
|
||||
case MergeBaseConfig:
|
||||
cfg.MergeBase = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
|
|
@ -445,6 +441,47 @@ func (c *Client) SetBranchConfig(ctx context.Context, branch, name, value string
|
|||
return err
|
||||
}
|
||||
|
||||
// PushDefault returns the value of push.default in the config. If the value
|
||||
// is not set, it returns "simple" (the default git value). See
|
||||
// https://git-scm.com/docs/git-config#Documentation/git-config.txt-pushdefault
|
||||
func (c *Client) PushDefault(ctx context.Context) (string, error) {
|
||||
pushDefault, err := c.Config(ctx, "push.default")
|
||||
if err == nil {
|
||||
return pushDefault, nil
|
||||
}
|
||||
|
||||
var gitError *GitError
|
||||
if ok := errors.As(err, &gitError); ok && gitError.ExitCode == 1 {
|
||||
return "simple", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
// RemotePushDefault returns the value of remote.pushDefault in the config. If
|
||||
// the value is not set, it returns an empty string.
|
||||
func (c *Client) RemotePushDefault(ctx context.Context) (string, error) {
|
||||
remotePushDefault, err := c.Config(ctx, "remote.pushDefault")
|
||||
if err == nil {
|
||||
return remotePushDefault, nil
|
||||
}
|
||||
|
||||
var gitError *GitError
|
||||
if ok := errors.As(err, &gitError); ok && gitError.ExitCode == 1 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
// ParsePushRevision gets the value of the @{push} revision syntax
|
||||
// An error here doesn't necessarily mean something is broken, but may mean that the @{push}
|
||||
// revision syntax couldn't be resolved, such as in non-centralized workflows with
|
||||
// push.default = simple. Downstream consumers should consider how to handle this error.
|
||||
func (c *Client) ParsePushRevision(ctx context.Context, branch string) (string, error) {
|
||||
revParseOut, err := c.revParse(ctx, "--abbrev-ref", branch+"@{push}")
|
||||
return firstLine(revParseOut), err
|
||||
}
|
||||
|
||||
func (c *Client) DeleteLocalTag(ctx context.Context, tag string) error {
|
||||
args := []string{"tag", "-d", tag}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
|
|
@ -790,6 +827,17 @@ func parseRemotes(remotesStr []string) RemoteSet {
|
|||
return remotes
|
||||
}
|
||||
|
||||
func parseRemoteURLOrName(value string) (*url.URL, string) {
|
||||
if strings.Contains(value, ":") {
|
||||
if u, err := ParseURL(value); err == nil {
|
||||
return u, ""
|
||||
}
|
||||
} else if !isFilesystemPath(value) {
|
||||
return nil, value
|
||||
}
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
func populateResolvedRemotes(remotes RemoteSet, resolved []string) {
|
||||
for _, l := range resolved {
|
||||
parts := strings.SplitN(l, " ", 2)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -728,56 +729,76 @@ func TestClientCommitBody(t *testing.T) {
|
|||
func TestClientReadBranchConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cmdExitStatus int
|
||||
cmdStdout string
|
||||
cmdStderr string
|
||||
cmds mockedCommands
|
||||
branch string
|
||||
wantBranchConfig BranchConfig
|
||||
wantError *GitError
|
||||
}{
|
||||
{
|
||||
name: "read branch config",
|
||||
cmdExitStatus: 0,
|
||||
cmdStdout: "branch.trunk.remote origin\nbranch.trunk.merge refs/heads/trunk\nbranch.trunk.gh-merge-base trunk",
|
||||
branch: "trunk",
|
||||
wantBranchConfig: BranchConfig{RemoteName: "origin", MergeRef: "refs/heads/trunk", MergeBase: "trunk"},
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "git config runs successfully but returns no output (Exit Code 1)",
|
||||
cmdExitStatus: 1,
|
||||
cmdStdout: "",
|
||||
cmdStderr: "",
|
||||
name: "when the git config has no (remote|merge|pushremote|gh-merge-base) keys, it should return an empty BranchConfig and no error",
|
||||
cmds: mockedCommands{
|
||||
`path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: {
|
||||
ExitStatus: 1,
|
||||
},
|
||||
},
|
||||
branch: "trunk",
|
||||
wantBranchConfig: BranchConfig{},
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "output error (Exit Code > 1)",
|
||||
cmdExitStatus: 2,
|
||||
cmdStdout: "",
|
||||
cmdStderr: "git error message",
|
||||
name: "when the git fails to read the config, it should return an empty BranchConfig and the error",
|
||||
cmds: mockedCommands{
|
||||
`path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: {
|
||||
ExitStatus: 2,
|
||||
Stderr: "git error",
|
||||
},
|
||||
},
|
||||
branch: "trunk",
|
||||
wantBranchConfig: BranchConfig{},
|
||||
wantError: &GitError{},
|
||||
wantError: &GitError{
|
||||
ExitCode: 2,
|
||||
Stderr: "git error",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when the config is read, it should return the correct BranchConfig",
|
||||
cmds: mockedCommands{
|
||||
`path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`: {
|
||||
Stdout: heredoc.Doc(`
|
||||
branch.trunk.remote upstream
|
||||
branch.trunk.merge refs/heads/trunk
|
||||
branch.trunk.pushremote origin
|
||||
branch.trunk.gh-merge-base gh-merge-base
|
||||
`),
|
||||
},
|
||||
},
|
||||
branch: "trunk",
|
||||
wantBranchConfig: BranchConfig{
|
||||
RemoteName: "upstream",
|
||||
PushRemoteName: "origin",
|
||||
MergeRef: "refs/heads/trunk",
|
||||
MergeBase: "gh-merge-base",
|
||||
},
|
||||
wantError: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
|
||||
cmdCtx := createMockedCommandContext(t, tt.cmds)
|
||||
client := Client{
|
||||
GitPath: "path/to/git",
|
||||
commandContext: cmdCtx,
|
||||
}
|
||||
branchConfig, err := client.ReadBranchConfig(context.Background(), tt.branch)
|
||||
wantCmdArgs := fmt.Sprintf("path/to/git config --get-regexp ^branch\\.%s\\.(remote|merge|gh-merge-base)$", tt.branch)
|
||||
assert.Equal(t, wantCmdArgs, strings.Join(cmd.Args[3:], " "))
|
||||
assert.Equal(t, tt.wantBranchConfig, branchConfig)
|
||||
if tt.wantError != nil {
|
||||
assert.ErrorAs(t, err, &tt.wantError)
|
||||
var gitError *GitError
|
||||
require.ErrorAs(t, err, &gitError)
|
||||
assert.Equal(t, tt.wantError.ExitCode, gitError.ExitCode)
|
||||
assert.Equal(t, tt.wantError.Stderr, gitError.Stderr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.wantBranchConfig, branchConfig)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -810,44 +831,297 @@ func Test_parseBranchConfig(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
name: "remote, merge ref, and merge base all specified",
|
||||
configLines: []string{
|
||||
"branch.trunk.remote origin",
|
||||
"branch.trunk.merge refs/heads/trunk",
|
||||
"branch.trunk.gh-merge-base gh-merge-base",
|
||||
},
|
||||
name: "pushremote",
|
||||
configLines: []string{"branch.trunk.pushremote pushremote"},
|
||||
wantBranchConfig: BranchConfig{
|
||||
RemoteName: "origin",
|
||||
MergeRef: "refs/heads/trunk",
|
||||
MergeBase: "gh-merge-base",
|
||||
PushRemoteName: "pushremote",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remote URL",
|
||||
name: "remote and pushremote are specified by name",
|
||||
configLines: []string{
|
||||
"branch.Frederick888/main.remote git@github.com:Frederick888/playground.git",
|
||||
"branch.Frederick888/main.merge refs/heads/main",
|
||||
"branch.trunk.remote upstream",
|
||||
"branch.trunk.pushremote origin",
|
||||
},
|
||||
wantBranchConfig: BranchConfig{
|
||||
RemoteName: "upstream",
|
||||
PushRemoteName: "origin",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remote and pushremote are specified by url",
|
||||
configLines: []string{
|
||||
"branch.trunk.remote git@github.com:UPSTREAMOWNER/REPO.git",
|
||||
"branch.trunk.pushremote git@github.com:ORIGINOWNER/REPO.git",
|
||||
},
|
||||
wantBranchConfig: BranchConfig{
|
||||
MergeRef: "refs/heads/main",
|
||||
RemoteURL: &url.URL{
|
||||
Scheme: "ssh",
|
||||
User: url.User("git"),
|
||||
Host: "github.com",
|
||||
Path: "/Frederick888/playground.git",
|
||||
Path: "/UPSTREAMOWNER/REPO.git",
|
||||
},
|
||||
PushRemoteURL: &url.URL{
|
||||
Scheme: "ssh",
|
||||
User: url.User("git"),
|
||||
Host: "github.com",
|
||||
Path: "/ORIGINOWNER/REPO.git",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remote, pushremote, gh-merge-base, and merge ref all specified",
|
||||
configLines: []string{
|
||||
"branch.trunk.remote remote",
|
||||
"branch.trunk.pushremote pushremote",
|
||||
"branch.trunk.gh-merge-base gh-merge-base",
|
||||
"branch.trunk.merge refs/heads/trunk",
|
||||
},
|
||||
wantBranchConfig: BranchConfig{
|
||||
RemoteName: "remote",
|
||||
PushRemoteName: "pushremote",
|
||||
MergeBase: "gh-merge-base",
|
||||
MergeRef: "refs/heads/trunk",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
branchConfig := parseBranchConfig(tt.configLines)
|
||||
assert.Equal(t, tt.wantBranchConfig.RemoteName, branchConfig.RemoteName)
|
||||
assert.Equal(t, tt.wantBranchConfig.MergeRef, branchConfig.MergeRef)
|
||||
assert.Equal(t, tt.wantBranchConfig.MergeBase, branchConfig.MergeBase)
|
||||
assert.Equalf(t, tt.wantBranchConfig.RemoteName, branchConfig.RemoteName, "unexpected RemoteName")
|
||||
assert.Equalf(t, tt.wantBranchConfig.MergeRef, branchConfig.MergeRef, "unexpected MergeRef")
|
||||
assert.Equalf(t, tt.wantBranchConfig.MergeBase, branchConfig.MergeBase, "unexpected MergeBase")
|
||||
assert.Equalf(t, tt.wantBranchConfig.PushRemoteName, branchConfig.PushRemoteName, "unexpected PushRemoteName")
|
||||
if tt.wantBranchConfig.RemoteURL != nil {
|
||||
assert.Equal(t, tt.wantBranchConfig.RemoteURL.String(), branchConfig.RemoteURL.String())
|
||||
assert.Equalf(t, tt.wantBranchConfig.RemoteURL.String(), branchConfig.RemoteURL.String(), "unexpected RemoteURL")
|
||||
}
|
||||
if tt.wantBranchConfig.PushRemoteURL != nil {
|
||||
assert.Equalf(t, tt.wantBranchConfig.PushRemoteURL.String(), branchConfig.PushRemoteURL.String(), "unexpected PushRemoteURL")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseRemoteURLOrName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
wantRemoteURL *url.URL
|
||||
wantRemoteName string
|
||||
}{
|
||||
{
|
||||
name: "empty value",
|
||||
value: "",
|
||||
wantRemoteURL: nil,
|
||||
wantRemoteName: "",
|
||||
},
|
||||
{
|
||||
name: "remote URL",
|
||||
value: "git@github.com:foo/bar.git",
|
||||
wantRemoteURL: &url.URL{
|
||||
Scheme: "ssh",
|
||||
User: url.User("git"),
|
||||
Host: "github.com",
|
||||
Path: "/foo/bar.git",
|
||||
},
|
||||
wantRemoteName: "",
|
||||
},
|
||||
{
|
||||
name: "remote name",
|
||||
value: "origin",
|
||||
wantRemoteURL: nil,
|
||||
wantRemoteName: "origin",
|
||||
},
|
||||
{
|
||||
name: "remote name is from filesystem",
|
||||
value: "./path/to/repo",
|
||||
wantRemoteURL: nil,
|
||||
wantRemoteName: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
remoteURL, remoteName := parseRemoteURLOrName(tt.value)
|
||||
assert.Equal(t, tt.wantRemoteURL, remoteURL)
|
||||
assert.Equal(t, tt.wantRemoteName, remoteName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientPushDefault(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
commandResult commandResult
|
||||
wantPushDefault string
|
||||
wantError *GitError
|
||||
}{
|
||||
{
|
||||
name: "push default is not set",
|
||||
commandResult: commandResult{
|
||||
ExitStatus: 1,
|
||||
Stderr: "error: key does not contain a section: remote.pushDefault",
|
||||
},
|
||||
wantPushDefault: "simple",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "push default is set to current",
|
||||
commandResult: commandResult{
|
||||
ExitStatus: 0,
|
||||
Stdout: "current",
|
||||
},
|
||||
wantPushDefault: "current",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "push default errors",
|
||||
commandResult: commandResult{
|
||||
ExitStatus: 128,
|
||||
Stderr: "fatal: git error",
|
||||
},
|
||||
wantPushDefault: "",
|
||||
wantError: &GitError{
|
||||
ExitCode: 128,
|
||||
Stderr: "fatal: git error",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmdCtx := createMockedCommandContext(t, mockedCommands{
|
||||
`path/to/git config push.default`: tt.commandResult,
|
||||
},
|
||||
)
|
||||
client := Client{
|
||||
GitPath: "path/to/git",
|
||||
commandContext: cmdCtx,
|
||||
}
|
||||
pushDefault, err := client.PushDefault(context.Background())
|
||||
if tt.wantError != nil {
|
||||
var gitError *GitError
|
||||
require.ErrorAs(t, err, &gitError)
|
||||
assert.Equal(t, tt.wantError.ExitCode, gitError.ExitCode)
|
||||
assert.Equal(t, tt.wantError.Stderr, gitError.Stderr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.wantPushDefault, pushDefault)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientRemotePushDefault(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
commandResult commandResult
|
||||
wantRemotePushDefault string
|
||||
wantError *GitError
|
||||
}{
|
||||
{
|
||||
name: "remote.pushDefault is not set",
|
||||
commandResult: commandResult{
|
||||
ExitStatus: 1,
|
||||
Stderr: "error: key does not contain a section: remote.pushDefault",
|
||||
},
|
||||
wantRemotePushDefault: "",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "remote.pushDefault is set to origin",
|
||||
commandResult: commandResult{
|
||||
ExitStatus: 0,
|
||||
Stdout: "origin",
|
||||
},
|
||||
wantRemotePushDefault: "origin",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "remote.pushDefault errors",
|
||||
commandResult: commandResult{
|
||||
ExitStatus: 128,
|
||||
Stderr: "fatal: git error",
|
||||
},
|
||||
wantRemotePushDefault: "",
|
||||
wantError: &GitError{
|
||||
ExitCode: 128,
|
||||
Stderr: "fatal: git error",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmdCtx := createMockedCommandContext(t, mockedCommands{
|
||||
`path/to/git config remote.pushDefault`: tt.commandResult,
|
||||
},
|
||||
)
|
||||
client := Client{
|
||||
GitPath: "path/to/git",
|
||||
commandContext: cmdCtx,
|
||||
}
|
||||
pushDefault, err := client.RemotePushDefault(context.Background())
|
||||
if tt.wantError != nil {
|
||||
var gitError *GitError
|
||||
require.ErrorAs(t, err, &gitError)
|
||||
assert.Equal(t, tt.wantError.ExitCode, gitError.ExitCode)
|
||||
assert.Equal(t, tt.wantError.Stderr, gitError.Stderr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.wantRemotePushDefault, pushDefault)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientParsePushRevision(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
branch string
|
||||
commandResult commandResult
|
||||
wantParsedPushRevision string
|
||||
wantError *GitError
|
||||
}{
|
||||
{
|
||||
name: "@{push} resolves to origin/branchName",
|
||||
branch: "branchName",
|
||||
commandResult: commandResult{
|
||||
ExitStatus: 0,
|
||||
Stdout: "origin/branchName",
|
||||
},
|
||||
wantParsedPushRevision: "origin/branchName",
|
||||
},
|
||||
{
|
||||
name: "@{push} doesn't resolve",
|
||||
commandResult: commandResult{
|
||||
ExitStatus: 128,
|
||||
Stderr: "fatal: git error",
|
||||
},
|
||||
wantParsedPushRevision: "",
|
||||
wantError: &GitError{
|
||||
ExitCode: 128,
|
||||
Stderr: "fatal: git error",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd := fmt.Sprintf("path/to/git rev-parse --abbrev-ref %s@{push}", tt.branch)
|
||||
cmdCtx := createMockedCommandContext(t, mockedCommands{
|
||||
args(cmd): tt.commandResult,
|
||||
})
|
||||
client := Client{
|
||||
GitPath: "path/to/git",
|
||||
commandContext: cmdCtx,
|
||||
}
|
||||
pushDefault, err := client.ParsePushRevision(context.Background(), tt.branch)
|
||||
if tt.wantError != nil {
|
||||
var gitError *GitError
|
||||
require.ErrorAs(t, err, &gitError)
|
||||
assert.Equal(t, tt.wantError.ExitCode, gitError.ExitCode)
|
||||
assert.Equal(t, tt.wantError.Stderr, gitError.Stderr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.wantParsedPushRevision, pushDefault)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1601,13 +1875,17 @@ func TestCommandMocking(t *testing.T) {
|
|||
jsonVar, ok := os.LookupEnv("GH_HELPER_PROCESS_RICH_COMMANDS")
|
||||
if !ok {
|
||||
fmt.Fprint(os.Stderr, "missing GH_HELPER_PROCESS_RICH_COMMANDS")
|
||||
os.Exit(1)
|
||||
// Exit 1 is used for empty key values in the git config. This is non-breaking in those use cases,
|
||||
// so this is returning a non-zero exit code to avoid suppressing this error for those use cases.
|
||||
os.Exit(16)
|
||||
}
|
||||
|
||||
var commands mockedCommands
|
||||
if err := json.Unmarshal([]byte(jsonVar), &commands); err != nil {
|
||||
fmt.Fprint(os.Stderr, "failed to unmarshal GH_HELPER_PROCESS_RICH_COMMANDS")
|
||||
os.Exit(1)
|
||||
// Exit 1 is used for empty key values in the git config. This is non-breaking in those use cases,
|
||||
// so this is returning a non-zero exit code to avoid suppressing this error for those use cases.
|
||||
os.Exit(16)
|
||||
}
|
||||
|
||||
// The discarded args are those for the go test binary itself, e.g. `-test.run=TestHelperProcessRich`
|
||||
|
|
@ -1616,7 +1894,9 @@ func TestCommandMocking(t *testing.T) {
|
|||
commandResult, ok := commands[args(strings.Join(realArgs, " "))]
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "unexpected command: %s\n", strings.Join(realArgs, " "))
|
||||
os.Exit(1)
|
||||
// Exit 1 is used for empty key values in the git config. This is non-breaking in those use cases,
|
||||
// so this is returning a non-zero exit code to avoid suppressing this error for those use cases.
|
||||
os.Exit(16)
|
||||
}
|
||||
|
||||
if commandResult.Stdout != "" {
|
||||
|
|
|
|||
104
git/command_test.go
Normal file
104
git/command_test.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
exitCode int
|
||||
stdout string
|
||||
stderr string
|
||||
wantErr *GitError
|
||||
}{
|
||||
{
|
||||
name: "successful command",
|
||||
stdout: "hello world",
|
||||
stderr: "",
|
||||
exitCode: 0,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "not a repo failure",
|
||||
stdout: "",
|
||||
stderr: "fatal: not a git repository (or any of the parent directories): .git",
|
||||
exitCode: 128,
|
||||
wantErr: &GitError{
|
||||
ExitCode: 128,
|
||||
Stderr: "fatal: not a git repository (or any of the parent directories): .git",
|
||||
err: &exec.ExitError{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
cmd := Command{
|
||||
&exec.Cmd{
|
||||
Path: createMockExecutable(t, tt.stdout, tt.stderr, tt.exitCode),
|
||||
},
|
||||
}
|
||||
|
||||
out, err := cmd.Output()
|
||||
if tt.wantErr != nil {
|
||||
require.Error(t, err)
|
||||
var gitError *GitError
|
||||
require.ErrorAs(t, err, &gitError)
|
||||
assert.Equal(t, tt.wantErr.ExitCode, gitError.ExitCode)
|
||||
assert.Equal(t, tt.wantErr.Stderr, gitError.Stderr)
|
||||
assert.Equal(t, tt.wantErr.Error(), gitError.Error())
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.stdout, string(out))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createMockExecutable(t *testing.T, stdout string, stderr string, exitCode int) string {
|
||||
tmpDir := t.TempDir()
|
||||
sourcePath := filepath.Join(tmpDir, "main.go")
|
||||
binaryPath := filepath.Join(tmpDir, "mockexec")
|
||||
if runtime.GOOS == "windows" {
|
||||
binaryPath += ".exe"
|
||||
}
|
||||
|
||||
// Create Go source
|
||||
source := buildCommandSourceCode(exitCode, stdout, stderr)
|
||||
|
||||
// Write source file
|
||||
if err := os.WriteFile(sourcePath, []byte(source), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Compile
|
||||
cmd := exec.Command("go", "build", "-o", binaryPath, sourcePath)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("failed to compile: %v\n%s", err, out)
|
||||
}
|
||||
return binaryPath
|
||||
|
||||
}
|
||||
|
||||
func buildCommandSourceCode(exitCode int, stdout, stderr string) string {
|
||||
return fmt.Sprintf(`package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
func main() {
|
||||
fmt.Printf(%q)
|
||||
fmt.Fprintf(os.Stderr, %q)
|
||||
os.Exit(%d)
|
||||
}`, stdout, stderr, exitCode)
|
||||
}
|
||||
|
|
@ -60,10 +60,14 @@ type Commit struct {
|
|||
Body string
|
||||
}
|
||||
|
||||
// These are the keys we read from the git branch.<name> config.
|
||||
type BranchConfig struct {
|
||||
RemoteName string
|
||||
RemoteURL *url.URL
|
||||
RemoteName string // .remote if string
|
||||
RemoteURL *url.URL // .remote if url
|
||||
MergeRef string // .merge
|
||||
PushRemoteName string // .pushremote if string
|
||||
PushRemoteURL *url.URL // .pushremote if url
|
||||
|
||||
// MergeBase is the optional base branch to target in a new PR if `--base` is not specified.
|
||||
MergeBase string
|
||||
MergeRef string
|
||||
}
|
||||
|
|
|
|||
38
go.mod
38
go.mod
|
|
@ -29,7 +29,7 @@ require (
|
|||
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/in-toto/attestation v1.1.0
|
||||
github.com/in-toto/attestation v1.1.1
|
||||
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
|
||||
|
|
@ -41,17 +41,17 @@ require (
|
|||
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d
|
||||
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc
|
||||
github.com/sigstore/protobuf-specs v0.3.3
|
||||
github.com/sigstore/sigstore-go v0.6.2
|
||||
github.com/sigstore/sigstore-go v0.7.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/zalando/go-keyring v0.2.5
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/sync v0.10.0
|
||||
golang.org/x/term v0.28.0
|
||||
golang.org/x/text v0.21.0
|
||||
google.golang.org/grpc v1.64.1
|
||||
google.golang.org/protobuf v1.36.3
|
||||
google.golang.org/grpc v1.69.4
|
||||
google.golang.org/protobuf v1.36.5
|
||||
gopkg.in/h2non/gock.v1 v1.1.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
|
@ -93,13 +93,11 @@ require (
|
|||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-openapi/validate v0.24.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/certificate-transparency-go v1.2.1 // indirect
|
||||
github.com/google/certificate-transparency-go v1.3.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/in-toto/in-toto-golang v0.9.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
|
|
@ -121,7 +119,7 @@ require (
|
|||
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/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
|
|
@ -130,21 +128,21 @@ require (
|
|||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sassoftware/relic v7.2.1+incompatible // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
|
||||
github.com/sigstore/rekor v1.3.6 // indirect
|
||||
github.com/sigstore/sigstore v1.8.9 // indirect
|
||||
github.com/sigstore/timestamp-authority v1.2.2 // indirect
|
||||
github.com/sigstore/rekor v1.3.8 // indirect
|
||||
github.com/sigstore/sigstore v1.8.12 // indirect
|
||||
github.com/sigstore/timestamp-authority v1.2.4 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/viper v1.18.2 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/spf13/viper v1.19.0 // indirect
|
||||
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.1 // 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
|
||||
|
|
@ -158,13 +156,13 @@ require (
|
|||
go.opentelemetry.io/otel/trace v1.33.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-20240112132812-db7319d0e0e3 // indirect
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
k8s.io/klog/v2 v2.120.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
)
|
||||
|
|
|
|||
281
go.sum
281
go.sum
|
|
@ -1,29 +1,36 @@
|
|||
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
|
||||
cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
|
||||
cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
|
||||
cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
|
||||
cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs=
|
||||
cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
|
||||
cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg=
|
||||
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.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc=
|
||||
cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI=
|
||||
cloud.google.com/go/kms v1.15.8 h1:szIeDCowID8th2i8XE4uRev5PMxQFqW+JjwYxL9h6xs=
|
||||
cloud.google.com/go/kms v1.15.8/go.mod h1:WoUHcDjD9pluCg7pNds131awnH429QGvRM3N/4MyoVs=
|
||||
cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA=
|
||||
cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY=
|
||||
cloud.google.com/go/kms v1.20.4 h1:CJ0hMpOg1ANN9tx/a/GPJ+Uxudy8k6f3fvGFuTHiE5A=
|
||||
cloud.google.com/go/kms v1.20.4/go.mod h1:gPLsp1r4FblUgBYPOcvI/bUPpdMg2Jm1ZVKU4tQUfcc=
|
||||
cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc=
|
||||
cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg=
|
||||
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.10.0 h1:n1DH8TPV4qqPTje2RcUBYwtrTWlabVp4n46+74X2pn4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.10.0/go.mod h1:HDcZnuGbiyppErN6lB+idp4CKhjbc8gwjto6OPpyggM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 h1:LqbJ/WzJUwBf8UiaSzgX7aMclParm9/5Vgp+TY51uBQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0 h1:DRiANoJTiW6obBQe3SqZizkuV1PEgfiiGivmVocDy64=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0/go.mod h1:qLIye2hwb/ZouqhpSD9Zn3SJipvpEnz1Ywl3VUk9Y0s=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI=
|
||||
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/security/keyvault/azkeys v1.3.0 h1:7rKG7UmnrxX4N53TFhkYqjc+kVUZuw0fL8I3Fh+Ld9E=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0/go.mod h1:Wjo+24QJVhhl/L7jy6w9yzFF2yDOf3cKECAa8ecf9vE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 h1:eXnN9kaS8TiDwXjoie3hMRLuwdUBUMW9KRgOqB3mCaw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0/go.mod h1:XIpam8wumeZ5rVMuhdDQLMfIPDf1WO3IzrCRO3e3e3o=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1 h1:gUDtaZk8heteyfdmv+pcfHvhR9llnh7c7GMwZ8RVG04=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1/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/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
||||
|
|
@ -38,36 +45,36 @@ github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4u
|
|||
github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/aws/aws-sdk-go v1.51.6 h1:Ld36dn9r7P9IjU8WZSaswQ8Y/XUCRpewim5980DwYiU=
|
||||
github.com/aws/aws-sdk-go v1.51.6/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.27.2 h1:pLsTXqX93rimAOZG2FIYraDQstZaaGVVN4tNw65v0h8=
|
||||
github.com/aws/aws-sdk-go-v2 v1.27.2/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.18 h1:wFvAnwOKKe7QAyIxziwSKjmer9JBMH1vzIL6W+fYuKk=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.18/go.mod h1:0xz6cgdX55+kmppvPm2IaKzIXOheGJhAufacPJaXZ7c=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.18 h1:D/ALDWqK4JdY3OFgA2thcPO1c9aYTT5STS/CvnkqY1c=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.18/go.mod h1:JuitCWq+F5QGUrmMPsk945rop6bB57jdscu+Glozdnc=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5 h1:dDgptDO9dxeFkXy+tEgVkzSClHZje/6JkPW5aZyEvrQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5/go.mod h1:gjvE2KBUgUQhcv89jqxrIxH9GaKs1JbZzWejj/DaHGA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 h1:cy8ahBJuhtM8GTTSyOkfy6WVPV1IE+SS5/wfXUYuulw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9/go.mod h1:CZBXGLaJnEZI6EVNcPd7a6B5IC5cA/GkRWtu9fp3S6Y=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 h1:A4SYk07ef04+vxZToz9LWvAXl9LW0NClpPpMsi31cz0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9/go.mod h1:5jJcHuwDagxN+ErjQ3PU3ocf6Ylc/p9x+BLO/+X4iXw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11 h1:o4T+fKxA3gTMcluBNZZXE9DNaMkJuUL1O3mffCUjoJo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11/go.mod h1:84oZdJ+VjuJKs9v1UTC9NaodRZRseOXCTgku+vQJWR8=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.30.0 h1:yS0JkEdV6h9JOo8sy2JSpjX+i7vsKifU8SIeHrqiDhU=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.30.0/go.mod h1:+I8VUUSVD4p5ISQtzpgSva4I8cJ4SQ4b1dcBcof7O+g=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.11 h1:gEYM2GSpr4YNWc6hCd5nod4+d4kd9vWIAWrmGuLdlMw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.11/go.mod h1:gVvwPdPNYehHSP9Rs7q27U1EU+3Or2ZpXvzAYJNh63w=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5 h1:iXjh3uaH3vsVcnyZX7MqCoCfcyxIrVE9iOQruRaWPrQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5/go.mod h1:5ZXesEuy/QcO0WUnt+4sDkxhdXRHTu2yG0uCSH8B6os=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 h1:M/1u4HBpwLuMtjlxuI2y6HoVLzF5e2mfxHCg7ZVMYmk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.12/go.mod h1:kcfd+eTdEi/40FIbLq4Hif3XMXnl5b/+t/KTfLt9xIk=
|
||||
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
|
||||
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
|
||||
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.8 h1:cZV+NUS/eGxKXMtmyhtYPJ7Z4YLoI/V8bkTdRZfYhGo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.8/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.10 h1:fKODZHfqQu06pCzR69KJ3GuttraRJkhlC8g80RZ0Dfg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.10/go.mod h1:PvdxRYZ5Um9QMq9PQ0zHHNdtKK+he2NHtFCUFMXWXeg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.51 h1:F/9Sm6Y6k4LqDesZDPJCLxQGXNNHd/ZtJiWd0lCZKRk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.51/go.mod h1:TKbzCHm43AoPyA+iLGGcruXd4AFhF8tOmLex2R9jWNQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23 h1:IBAoD/1d8A8/1aA8g4MBVtTRHhXRiNAgwdbo/xRM2DI=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23/go.mod h1:vfENuCM7dofkgKpYzuzf1VT1UKkA/YL3qanfBn7HCaA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27 h1:jSJjSBzw8VDIbWv+mmvBSP8ezsztMYJGH+eKqi9AmNs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27/go.mod h1:/DAhLbFRgwhmvJdOfSm+WwikZrCuUJiA4WgJG0fTNSw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27 h1:l+X4K77Dui85pIj5foXDhPlnqcNRG2QUyvca300lXh8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27/go.mod h1:KvZXSFEXm6x84yE8qffKvT3x8J5clWnVFXphpohhzJ8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8 h1:cWno7lefSH6Pp+mSznagKCgfDGeZRin66UvYUqAkyeA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8/go.mod h1:tPD+VjU3ABTBoEJ3nctu5Nyg4P4yjqSH5bJGGkY4+XE=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.37.8 h1:KbLZjYqhQ9hyB4HwXiheiflTlYQa0+Fz0Ms/rh5f3mk=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.37.8/go.mod h1:ANs9kBhK4Ghj9z1W+bsr3WsNaPF71qkgd6eE6Ekol/Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.9 h1:YqtxripbjWb2QLyzRK9pByfEDvgg95gpC2AyDq4hFE8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.9/go.mod h1:lV8iQpg6OLOfBnqbGMBKYjilBlf633qwHnBEiMSPoHY=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8 h1:6dBT1Lz8fK11m22R+AqfRsFn8320K0T5DTGxxOQBSMw=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8/go.mod h1:/kiBvRQXBc6xeJTYzhSdGvJ5vm1tjaDEjH+MSeRJnlY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.6 h1:VwhTrsTuVn52an4mXx29PqRzs2Dvu921NpGk7y43tAM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.6/go.mod h1:+8h7PZb3yY5ftmVLD7ocEoE98hdc8PoKS0H3wfx1dlc=
|
||||
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
|
||||
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||
|
|
@ -80,12 +87,10 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn
|
|||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY=
|
||||
github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU=
|
||||
github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
|
||||
github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
|
||||
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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs=
|
||||
github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw=
|
||||
github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs=
|
||||
|
|
@ -195,34 +200,32 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
|||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/certificate-transparency-go v1.2.1 h1:4iW/NwzqOqYEEoCBEFP+jPbBXbLqMpq3CifMyOnDUME=
|
||||
github.com/google/certificate-transparency-go v1.2.1/go.mod h1:bvn/ytAccv+I6+DGkqpvSsEdiVGramgaSC6RD3tEmeE=
|
||||
github.com/google/certificate-transparency-go v1.3.1 h1:akbcTfQg0iZlANZLn0L9xOeWtyCIdeoYhKrqi5iH3Go=
|
||||
github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/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.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
|
||||
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
|
||||
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.6.0 h1:jMBeDBIkINFvS2n6oV5maDqfRlxREAc6CW9QYWQ0qT4=
|
||||
github.com/google/trillian v1.6.0/go.mod h1:Yu3nIMITzNhhMJEHjAtp6xKiu+H/iHu2Oq5FjV2mCWI=
|
||||
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=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=
|
||||
github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
|
|
@ -234,8 +237,6 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
|
|||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||
|
|
@ -255,8 +256,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
|
|||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/vault/api v1.12.2 h1:7YkCTE5Ni90TcmYHDBExdt4WGJxhpzaHqR6uGbQb/rE=
|
||||
github.com/hashicorp/vault/api v1.12.2/go.mod h1:LSGf1NGT1BnvFFnKVtnvcaLBM2Lz+gJdpL6HUYed8KE=
|
||||
github.com/hashicorp/vault/api v1.15.0 h1:O24FYQCWwhwKnF7CuSqP30S51rTV7vz1iACXE/pj5DA=
|
||||
github.com/hashicorp/vault/api v1.15.0/go.mod h1:+5YTO09JGn0u+b6ySD/LLVf8WkJCPLAL2Vkmrn2+CM8=
|
||||
github.com/henvic/httpretty v0.1.4 h1:Jo7uwIRWVFxkqOnErcoYfH90o3ddQyVrSANeS4cxYmU=
|
||||
github.com/henvic/httpretty v0.1.4/go.mod h1:Dn60sQTZfbt2dYsdUSNsCljyF4AfdqnuJFDLJA1I4AM=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
|
|
@ -265,8 +266,8 @@ github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u
|
|||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM=
|
||||
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
|
||||
github.com/in-toto/attestation v1.1.0 h1:oRWzfmZPDSctChD0VaQV7MJrywKOzyNrtpENQFq//2Q=
|
||||
github.com/in-toto/attestation v1.1.0/go.mod h1:DB59ytd3z7cIHgXxwpSX2SABrU6WJUKg/grpdgHVgVs=
|
||||
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/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=
|
||||
|
|
@ -275,12 +276,22 @@ github.com/itchyny/gojq v0.12.15 h1:WC1Nxbx4Ifw5U2oQWACYz32JK8G9qxNtHzrvW4KEcqI=
|
|||
github.com/itchyny/gojq v0.12.15/go.mod h1:uWAHCbCIla1jiNxmeT5/B5mOjSdfkCq6p8vxWg+BM10=
|
||||
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
|
||||
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
||||
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0=
|
||||
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
|
||||
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b h1:ZGiXF8sz7PDk6RgkP+A/SFfUD0ZR/AgG6SpRNEDKZy8=
|
||||
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b/go.mod h1:hQmNrgofl+IY/8L+n20H6E6PWBBTokdsv+q49j0QhsU=
|
||||
github.com/jellydator/ttlcache/v3 v3.2.0 h1:6lqVJ8X3ZaUwvzENqPAobDsXNExfUJd61u++uW8a3LE=
|
||||
github.com/jellydator/ttlcache/v3 v3.2.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc=
|
||||
github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw=
|
||||
github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY=
|
||||
github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs=
|
||||
github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
|
|
@ -333,6 +344,8 @@ github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54
|
|||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
|
||||
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38 h1:0FrBxrkJ0hVembTb/e4EU5Ml6vLcOusAqymmYISg5Uo=
|
||||
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
|
|
@ -343,8 +356,8 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ
|
|||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
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.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
|
|
@ -352,14 +365,14 @@ 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.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
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.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d h1:jKIUJdMcIVGOSHi6LSqJqw9RqblyblE2ZrHvFbWR3S0=
|
||||
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d/go.mod h1:YX2wUZOcJGOIycErz2s9KvDaP0jnWwRCirQMPLPpQ+Y=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
|
|
@ -382,8 +395,8 @@ github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGq
|
|||
github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk=
|
||||
github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4=
|
||||
github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3mcNEL9NBPB0iuVjyxvq3LZc=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
|
||||
|
|
@ -394,36 +407,37 @@ github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZV
|
|||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
|
||||
github.com/sigstore/protobuf-specs v0.3.3 h1:RMZQgXTD/pF7KW6b5NaRLYxFYZ/wzx44PQFXN2PEo5g=
|
||||
github.com/sigstore/protobuf-specs v0.3.3/go.mod h1:vIhZ6Uor1a38+wvRrKcqL2PtYNlgoIW9lhzYzkyy4EU=
|
||||
github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8=
|
||||
github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc=
|
||||
github.com/sigstore/sigstore v1.8.9 h1:NiUZIVWywgYuVTxXmRoTT4O4QAGiTEKup4N1wdxFadk=
|
||||
github.com/sigstore/sigstore v1.8.9/go.mod h1:d9ZAbNDs8JJfxJrYmulaTazU3Pwr8uLL9+mii4BNR3w=
|
||||
github.com/sigstore/sigstore-go v0.6.2 h1:8uiywjt73vzfrGfWYVwVsiB1E1Qmwmpgr1kVpl4fs6A=
|
||||
github.com/sigstore/sigstore-go v0.6.2/go.mod h1:pOIUH7Jx+ctwMICo+2zNrViOJJN5sGaQgwX4yAVJkA0=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3 h1:LTfPadUAo+PDRUbbdqbeSl2OuoFQwUFTnJ4stu+nwWw=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3/go.mod h1:QV/Lxlxm0POyhfyBtIbTWxNeF18clMlkkyL9mu45y18=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3 h1:xgbPRCr2npmmsuVVteJqi/ERw9+I13Wou7kq0Yk4D8g=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3/go.mod h1:G4+I83FILPX6MtnoaUdmv/bRGEVtR3JdLeJa/kXdk/0=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.3 h1:vDl2fqPT0h3D/k6NZPlqnKFd1tz3335wm39qjvpZNJc=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.3/go.mod h1:9uOJXbXEXj+M6QjMKH5PaL5WDMu43rHfbIMgXzA8eKI=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.3 h1:h9G8j+Ds21zqqulDbA/R/ft64oQQIyp8S7wJYABYSlg=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.3/go.mod h1:zgCeHOuqF6k7A7TTEvftcA9V3FRzB7mrPtHOhXAQBnc=
|
||||
github.com/sigstore/timestamp-authority v1.2.2 h1:X4qyutnCQqJ0apMewFyx+3t7Tws00JQ/JonBiu3QvLE=
|
||||
github.com/sigstore/timestamp-authority v1.2.2/go.mod h1:nEah4Eq4wpliDjlY342rXclGSO7Kb9hoRrl9tqLW13A=
|
||||
github.com/sigstore/rekor v1.3.8 h1:B8kJI8mpSIXova4Jxa6vXdJyysRxFGsEsLKBDl0rRjA=
|
||||
github.com/sigstore/rekor v1.3.8/go.mod h1:/dHFYKSuxEygfDRnEwyJ+ZD6qoVYNXQdi1mJrKvKWsI=
|
||||
github.com/sigstore/sigstore v1.8.12 h1:S8xMVZbE2z9ZBuQUEG737pxdLjnbOIcFi5v9UFfkJFc=
|
||||
github.com/sigstore/sigstore v1.8.12/go.mod h1:+PYQAa8rfw0QdPpBcT+Gl3egKD9c+TUgAlF12H3Nmjo=
|
||||
github.com/sigstore/sigstore-go v0.7.0 h1:bIGPc2IbnbxnzlqQcKlh1o96bxVJ4yRElpP1gHrOH48=
|
||||
github.com/sigstore/sigstore-go v0.7.0/go.mod h1:4RrCK+i+jhx7lyOG2Vgef0/kFLbKlDI1hrioUYvkxxA=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.12 h1:EC3UmIaa7nV9sCgSpVevmvgvTYTkMqyrRbj5ojPp7tE=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.12/go.mod h1:aw60vs3crnQdM/DYH+yF2P0MVKtItwAX34nuaMrY7Lk=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.12 h1:FPpliDTywSy0woLHMAdmTSZ5IS/lVBZ0dY0I+2HmnSY=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.12/go.mod h1:NkPiz4XA0JcBSXzJUrjMj7Xi7oSTew1Ip3Zmt56mHlw=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.12 h1:kweBChR6M9FEvmxN3BMEcl7SNnwxTwKF7THYFKLOE5U=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.12/go.mod h1:6+d+A6oYt1W5OgtzgEVb21V7tAZ/C2Ihtzc5MNJbayY=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.12 h1:jvY1B9bjP+tKzdKDyuq5K7O19CG2IKzGJNTy5tuL2Gs=
|
||||
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.12/go.mod h1:2uEeOb8xE2RC6OvzxKux1wkS39Zv8gA27z92m49xUTc=
|
||||
github.com/sigstore/timestamp-authority v1.2.4 h1:RjXZxOWorEiem/uSr0pFHVtQpyzpcFxgugo5jVqm3mw=
|
||||
github.com/sigstore/timestamp-authority v1.2.4/go.mod h1:ExrbobKdEuwuBptZIiKp1IaVBRiUeKbiuSyZTO8Okik=
|
||||
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=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
|
@ -435,14 +449,15 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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.1 h1:11p9tXpq10KQEujxjcIjDSivMKCMLguls7erXHZnxJQ=
|
||||
github.com/theupdateframework/go-tuf/v2 v2.0.1/go.mod h1:baB22nBHeHBCeuGZcIlctNq4P61PcOdyARlplg5xmLA=
|
||||
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/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/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
|
||||
|
|
@ -461,12 +476,10 @@ github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8L
|
|||
github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
|
||||
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
|
||||
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
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.52.0 h1:vS1Ao/R55RNV4O7TA2Qopok8yN+X0LIP6RVWLFkprck=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0/go.mod h1:BMsdeOxN04K0L5FNUBfjFdvwWGNe/rkmSwH4Aelu/X0=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
|
||||
|
|
@ -475,10 +488,12 @@ go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5W
|
|||
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
|
||||
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
|
||||
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
||||
go.step.sm/crypto v0.44.2 h1:t3p3uQ7raP2jp2ha9P6xkQF85TJZh+87xmjSLaib+jk=
|
||||
go.step.sm/crypto v0.44.2/go.mod h1:x1439EnFhadzhkuaGX7sz03LEMQ+jV4gRamf5LCZJQQ=
|
||||
go.step.sm/crypto v0.57.0 h1:YjoRQDaJYAxHLVwjst0Bl0xcnoKzVwuHCJtEo2VSHYU=
|
||||
go.step.sm/crypto v0.57.0/go.mod h1:+Lwp5gOVPaTa3H/Ul/TzGbxQPXZZcKIUGMS0lG6n9Go=
|
||||
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=
|
||||
|
|
@ -489,8 +504,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
|
|
@ -527,26 +542,26 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.172.0 h1:/1OcMZGPmW1rX2LCu2CmGUD1KXK1+pfzxotxyRUCCdk=
|
||||
google.golang.org/api v0.172.0/go.mod h1:+fJZq6QXWfa9pXhnIzsjx4yI22d4aI9ZpLb58gvXjis=
|
||||
google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7 h1:ImUcDPHjTrAqNhlOkSocDLfG9rrNHH7w7uoKWPaWZ8s=
|
||||
google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7/go.mod h1:/3XmxOjePkvmKrHuBy4zNFw7IzxJXtAgdpXi8Ll990U=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 h1:P8OJ/WCl/Xo4E4zoe4/bifHpSmmKwARqyqE4nW6J2GQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:RGnPtTG7r4i8sPlNyDeikXF99hMM+hN6QMm4ooG9g2g=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 h1:Q2RxlXqh1cgzzUgV261vBO2jI5R/3DD1J2pM0nI4NhU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
||||
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
|
||||
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/api v0.216.0 h1:xnEHy+xWFrtYInWPy8OdGFsyIfWJjtVnO39g7pz2BFY=
|
||||
google.golang.org/api v0.216.0/go.mod h1:K9wzQMvWi47Z9IU7OgdOofvZuw75Ge3PPITImZR/UyI=
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
|
||||
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
|
||||
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
|
@ -559,8 +574,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
|
||||
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
||||
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
|
||||
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
|
||||
|
|
|
|||
|
|
@ -142,7 +142,8 @@ func genMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string)
|
|||
fmt.Fprintf(w, "```\n%s\n```\n\n", cmd.UseLine())
|
||||
}
|
||||
if hasLong {
|
||||
fmt.Fprintf(w, "%s\n\n", cmd.Long)
|
||||
longWithEscapedPipe := strings.ReplaceAll(cmd.Long, "|", "|")
|
||||
fmt.Fprintf(w, "%s\n\n", longWithEscapedPipe)
|
||||
}
|
||||
|
||||
for _, g := range root.GroupedCommands(cmd) {
|
||||
|
|
|
|||
|
|
@ -34,19 +34,19 @@ func actionsExplainer(cs *iostreams.ColorScheme) string {
|
|||
GitHub CLI integrates with GitHub Actions to help you manage runs and workflows.
|
||||
|
||||
%[3]s
|
||||
gh run list: List recent workflow runs
|
||||
gh run view: View details for a workflow run or one of its jobs
|
||||
gh run watch: Watch a workflow run while it executes
|
||||
gh run rerun: Rerun a failed workflow run
|
||||
gh run list: List recent workflow runs
|
||||
gh run view: View details for a workflow run or one of its jobs
|
||||
gh run watch: Watch a workflow run while it executes
|
||||
gh run rerun: Rerun a failed workflow run
|
||||
gh run download: Download artifacts generated by runs
|
||||
|
||||
To see more help, run %[1]sgh help run <subcommand>%[1]s
|
||||
|
||||
%[4]s
|
||||
gh workflow list: List workflow files in your repository
|
||||
gh workflow view: View details for a workflow file
|
||||
gh workflow enable: Enable a workflow file
|
||||
gh workflow disable: Disable a workflow file
|
||||
gh workflow view: View details for a workflow file
|
||||
gh workflow enable: Enable a workflow file
|
||||
gh workflow disable: Disable a workflow file
|
||||
gh workflow run: Trigger a workflow_dispatch run for a workflow file
|
||||
|
||||
To see more help, run %[1]sgh help workflow <subcommand>%[1]s
|
||||
|
|
|
|||
|
|
@ -201,12 +201,12 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
|
|||
Annotations: map[string]string{
|
||||
"help:environment": heredoc.Doc(`
|
||||
GH_TOKEN, GITHUB_TOKEN (in order of precedence): an authentication token for
|
||||
github.com API requests.
|
||||
<github.com> API requests.
|
||||
|
||||
GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN (in order of precedence): an
|
||||
authentication token for API requests to GitHub Enterprise.
|
||||
|
||||
GH_HOST: make the request to a GitHub host other than github.com.
|
||||
GH_HOST: make the request to a GitHub host other than <github.com>.
|
||||
`),
|
||||
},
|
||||
Args: cobra.ExactArgs(1),
|
||||
|
|
@ -239,17 +239,19 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
|
|||
return err
|
||||
}
|
||||
|
||||
if opts.Slurp && !opts.Paginate {
|
||||
return cmdutil.FlagErrorf("`--paginate` required when passing `--slurp`")
|
||||
}
|
||||
if opts.Slurp {
|
||||
if err := cmdutil.MutuallyExclusive(
|
||||
"the `--slurp` option is not supported with `--jq` or `--template`",
|
||||
opts.Slurp,
|
||||
opts.FilterOutput != "",
|
||||
opts.Template != "",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cmdutil.MutuallyExclusive(
|
||||
"the `--slurp` option is not supported with `--jq` or `--template`",
|
||||
opts.Slurp,
|
||||
opts.FilterOutput != "",
|
||||
opts.Template != "",
|
||||
); err != nil {
|
||||
return err
|
||||
if !opts.Paginate {
|
||||
return cmdutil.FlagErrorf("`--paginate` required when passing `--slurp`")
|
||||
}
|
||||
}
|
||||
|
||||
if err := cmdutil.MutuallyExclusive(
|
||||
|
|
@ -461,9 +463,11 @@ func processResponse(resp *http.Response, opts *ApiOptions, bodyWriter, headersW
|
|||
|
||||
var serverError string
|
||||
if isJSON && (opts.RequestPath == "graphql" || resp.StatusCode >= 400) {
|
||||
responseBody, serverError, err = parseErrorResponse(responseBody, resp.StatusCode)
|
||||
if err != nil {
|
||||
return
|
||||
if !strings.EqualFold(opts.RequestMethod, "HEAD") {
|
||||
responseBody, serverError, err = parseErrorResponse(responseBody, resp.StatusCode)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1234,6 +1234,35 @@ func Test_apiRun_DELETE(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_apiRun_HEAD(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
err := apiRun(&ApiOptions{
|
||||
IO: ios,
|
||||
Config: func() (gh.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
var tr roundTripper = func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: 422,
|
||||
Request: req,
|
||||
Header: map[string][]string{
|
||||
"Content-Type": {"application/json"},
|
||||
}}, nil
|
||||
}
|
||||
return &http.Client{Transport: tr}, nil
|
||||
},
|
||||
MagicFields: []string(nil),
|
||||
RawFields: []string(nil),
|
||||
RequestMethod: "HEAD",
|
||||
RequestMethodPassed: true,
|
||||
})
|
||||
if err != cmdutil.SilentError {
|
||||
t.Fatalf("got error %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_apiRun_inputFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"errors"
|
||||
"github.com/sigstore/sigstore-go/pkg/bundle"
|
||||
)
|
||||
|
||||
|
|
@ -11,18 +10,7 @@ const (
|
|||
GetAttestationByOwnerAndSubjectDigestPath = "orgs/%s/attestations/%s"
|
||||
)
|
||||
|
||||
type ErrNoAttestations struct {
|
||||
name string
|
||||
digest string
|
||||
}
|
||||
|
||||
func (e ErrNoAttestations) Error() string {
|
||||
return fmt.Sprintf("no attestations found for digest %s in %s", e.name, e.digest)
|
||||
}
|
||||
|
||||
func newErrNoAttestations(name, digest string) ErrNoAttestations {
|
||||
return ErrNoAttestations{name, digest}
|
||||
}
|
||||
var ErrNoAttestationsFound = errors.New("no attestations found")
|
||||
|
||||
type Attestation struct {
|
||||
Bundle *bundle.Bundle `json:"bundle"`
|
||||
|
|
|
|||
|
|
@ -60,44 +60,26 @@ func NewLiveClient(hc *http.Client, host string, l *ioconfig.Handler) *LiveClien
|
|||
}
|
||||
}
|
||||
|
||||
func (c *LiveClient) BuildRepoAndDigestURL(repo, digest string) string {
|
||||
repo = strings.Trim(repo, "/")
|
||||
return fmt.Sprintf(GetAttestationByRepoAndSubjectDigestPath, repo, digest)
|
||||
}
|
||||
|
||||
// GetByRepoAndDigest fetches the attestation by repo and digest
|
||||
func (c *LiveClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) {
|
||||
url := c.BuildRepoAndDigestURL(repo, digest)
|
||||
attestations, err := c.getAttestations(url, repo, digest, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bundles, err := c.fetchBundleFromAttestations(attestations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch bundle with URL: %w", err)
|
||||
}
|
||||
|
||||
return bundles, nil
|
||||
}
|
||||
|
||||
func (c *LiveClient) BuildOwnerAndDigestURL(owner, digest string) string {
|
||||
owner = strings.Trim(owner, "/")
|
||||
return fmt.Sprintf(GetAttestationByOwnerAndSubjectDigestPath, owner, digest)
|
||||
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) {
|
||||
url := c.BuildOwnerAndDigestURL(owner, digest)
|
||||
attestations, err := c.getAttestations(url, owner, digest, limit)
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(attestations) == 0 {
|
||||
return nil, newErrNoAttestations(owner, digest)
|
||||
}
|
||||
|
||||
bundles, err := c.fetchBundleFromAttestations(attestations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch bundle with URL: %w", err)
|
||||
|
|
@ -112,9 +94,7 @@ func (c *LiveClient) GetTrustDomain() (string, error) {
|
|||
return c.getTrustDomain(MetaPath)
|
||||
}
|
||||
|
||||
func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*Attestation, error) {
|
||||
c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", digest)
|
||||
|
||||
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)
|
||||
|
|
@ -157,7 +137,7 @@ func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*At
|
|||
}
|
||||
|
||||
if len(attestations) == 0 {
|
||||
return nil, newErrNoAttestations(name, digest)
|
||||
return nil, ErrNoAttestationsFound
|
||||
}
|
||||
|
||||
if len(attestations) > limit {
|
||||
|
|
@ -176,7 +156,7 @@ func (c *LiveClient) fetchBundleFromAttestations(attestations []*Attestation) ([
|
|||
return fmt.Errorf("attestation has no bundle or bundle URL")
|
||||
}
|
||||
|
||||
// for now, we fallback to the bundle field if the bundle URL is empty
|
||||
// for now, we fall back to the bundle field if the bundle URL is empty
|
||||
if a.BundleURL == "" {
|
||||
c.logger.VerbosePrintf("Bundle URL is empty. Falling back to bundle field\n\n")
|
||||
fetched[i] = &Attestation{
|
||||
|
|
@ -186,13 +166,14 @@ func (c *LiveClient) fetchBundleFromAttestations(attestations []*Attestation) ([
|
|||
}
|
||||
|
||||
// otherwise fetch the bundle with the provided URL
|
||||
b, err := c.GetBundle(a.BundleURL)
|
||||
b, err := c.getBundle(a.BundleURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch bundle with URL: %w", err)
|
||||
}
|
||||
fetched[i] = &Attestation{
|
||||
Bundle: b,
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
@ -204,38 +185,49 @@ func (c *LiveClient) fetchBundleFromAttestations(attestations []*Attestation) ([
|
|||
return fetched, nil
|
||||
}
|
||||
|
||||
func (c *LiveClient) GetBundle(url string) (*bundle.Bundle, error) {
|
||||
func (c *LiveClient) getBundle(url string) (*bundle.Bundle, error) {
|
||||
c.logger.VerbosePrintf("Fetching attestation bundle with bundle URL\n\n")
|
||||
|
||||
resp, err := c.httpClient.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sgBundle *bundle.Bundle
|
||||
bo := backoff.NewConstantBackOff(getAttestationRetryInterval)
|
||||
err := backoff.Retry(func() error {
|
||||
resp, err := c.httpClient.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request to fetch bundle from URL failed: %w", err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
if resp.StatusCode >= 500 && resp.StatusCode <= 599 {
|
||||
return fmt.Errorf("attestation bundle with URL %s returned status code %d", url, resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read blob storage response body: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read blob storage response body: %w", err)
|
||||
}
|
||||
|
||||
var out []byte
|
||||
decompressed, err := snappy.Decode(out, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decompress with snappy: %w", err)
|
||||
}
|
||||
var out []byte
|
||||
decompressed, err := snappy.Decode(out, body)
|
||||
if err != nil {
|
||||
return backoff.Permanent(fmt.Errorf("failed to decompress with snappy: %w", err))
|
||||
}
|
||||
|
||||
var pbBundle v1.Bundle
|
||||
if err = protojson.Unmarshal(decompressed, &pbBundle); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal to bundle: %w", err)
|
||||
}
|
||||
var pbBundle v1.Bundle
|
||||
if err = protojson.Unmarshal(decompressed, &pbBundle); err != nil {
|
||||
return backoff.Permanent(fmt.Errorf("failed to unmarshal to bundle: %w", err))
|
||||
}
|
||||
|
||||
c.logger.VerbosePrintf("Successfully fetched bundle\n\n")
|
||||
c.logger.VerbosePrintf("Successfully fetched bundle\n\n")
|
||||
|
||||
return bundle.NewBundle(&pbBundle)
|
||||
sgBundle, err = bundle.NewBundle(&pbBundle)
|
||||
if err != nil {
|
||||
return backoff.Permanent(fmt.Errorf("failed to create new bundle: %w", err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}, backoff.WithMaxRetries(bo, 3))
|
||||
|
||||
return sgBundle, err
|
||||
}
|
||||
|
||||
func shouldRetry(err error) bool {
|
||||
|
|
|
|||
|
|
@ -42,24 +42,6 @@ func NewClientWithMockGHClient(hasNextPage bool) Client {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetURL(t *testing.T) {
|
||||
c := LiveClient{}
|
||||
|
||||
testData := []struct {
|
||||
repo string
|
||||
digest string
|
||||
expected string
|
||||
}{
|
||||
{repo: "/github/example/", digest: "sha256:12313213", expected: "repos/github/example/attestations/sha256:12313213"},
|
||||
{repo: "/github/example", digest: "sha256:12313213", expected: "repos/github/example/attestations/sha256:12313213"},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
s := c.BuildRepoAndDigestURL(data.repo, data.digest)
|
||||
require.Equal(t, data.expected, s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetByDigest(t *testing.T) {
|
||||
c := NewClientWithMockGHClient(false)
|
||||
attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit)
|
||||
|
|
@ -150,12 +132,12 @@ func TestGetByDigest_NoAttestationsFound(t *testing.T) {
|
|||
|
||||
attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit)
|
||||
require.Error(t, err)
|
||||
require.IsType(t, ErrNoAttestations{}, err)
|
||||
require.IsType(t, ErrNoAttestationsFound, err)
|
||||
require.Nil(t, attestations)
|
||||
|
||||
attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit)
|
||||
require.Error(t, err)
|
||||
require.IsType(t, ErrNoAttestations{}, err)
|
||||
require.IsType(t, ErrNoAttestationsFound, err)
|
||||
require.Nil(t, attestations)
|
||||
}
|
||||
|
||||
|
|
@ -180,7 +162,7 @@ func TestGetByDigest_Error(t *testing.T) {
|
|||
require.Nil(t, attestations)
|
||||
}
|
||||
|
||||
func TestFetchBundleFromAttestations(t *testing.T) {
|
||||
func TestFetchBundleFromAttestations_BundleURL(t *testing.T) {
|
||||
httpClient := &mockHttpClient{}
|
||||
client := LiveClient{
|
||||
httpClient: httpClient,
|
||||
|
|
@ -193,43 +175,50 @@ func TestFetchBundleFromAttestations(t *testing.T) {
|
|||
fetched, err := client.fetchBundleFromAttestations(attestations)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, fetched, 2)
|
||||
require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", fetched[0].Bundle.GetMediaType())
|
||||
require.NotNil(t, "application/vnd.dev.sigstore.bundle.v0.3+json", fetched[0].Bundle.GetMediaType())
|
||||
httpClient.AssertNumberOfCalls(t, "OnGetSuccess", 2)
|
||||
}
|
||||
|
||||
func TestFetchBundleFromAttestations_InvalidAttestation(t *testing.T) {
|
||||
func TestFetchBundleFromAttestations_MissingBundleAndBundleURLFields(t *testing.T) {
|
||||
httpClient := &mockHttpClient{}
|
||||
client := LiveClient{
|
||||
httpClient: httpClient,
|
||||
logger: io.NewTestHandler(),
|
||||
}
|
||||
|
||||
// If both the BundleURL and Bundle fields are empty, the function should
|
||||
// return an error indicating that
|
||||
att1 := Attestation{}
|
||||
attestations := []*Attestation{&att1}
|
||||
fetched, err := client.fetchBundleFromAttestations(attestations)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, fetched, 2)
|
||||
bundles, err := client.fetchBundleFromAttestations(attestations)
|
||||
require.ErrorContains(t, err, "attestation has no bundle or bundle URL")
|
||||
require.Nil(t, bundles, 2)
|
||||
}
|
||||
|
||||
func TestFetchBundleFromAttestations_Fail(t *testing.T) {
|
||||
httpClient := &failAfterOneCallHttpClient{}
|
||||
func TestFetchBundleFromAttestations_FailOnTheSecondAttestation(t *testing.T) {
|
||||
mockHTTPClient := &failAfterNCallsHttpClient{
|
||||
// the initial HTTP request will succeed, which returns a bundle for the first attestation
|
||||
// all following HTTP requests will fail, which means the function fails to fetch a bundle
|
||||
// for the second attestation and the function returns an error
|
||||
FailOnCallN: 2,
|
||||
FailOnAllSubsequentCalls: true,
|
||||
}
|
||||
|
||||
c := &LiveClient{
|
||||
httpClient: httpClient,
|
||||
httpClient: mockHTTPClient,
|
||||
logger: io.NewTestHandler(),
|
||||
}
|
||||
|
||||
att1 := makeTestAttestation()
|
||||
att2 := makeTestAttestation()
|
||||
attestations := []*Attestation{&att1, &att2}
|
||||
fetched, err := c.fetchBundleFromAttestations(attestations)
|
||||
bundles, err := c.fetchBundleFromAttestations(attestations)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, fetched)
|
||||
httpClient.AssertNumberOfCalls(t, "OnGetFailAfterOneCall", 2)
|
||||
require.Nil(t, bundles)
|
||||
}
|
||||
|
||||
func TestFetchBundleFromAttestations_FetchByURLFail(t *testing.T) {
|
||||
mockHTTPClient := &failHttpClient{}
|
||||
func TestFetchBundleFromAttestations_FailAfterRetrying(t *testing.T) {
|
||||
mockHTTPClient := &reqFailHttpClient{}
|
||||
|
||||
c := &LiveClient{
|
||||
httpClient: mockHTTPClient,
|
||||
|
|
@ -241,10 +230,10 @@ func TestFetchBundleFromAttestations_FetchByURLFail(t *testing.T) {
|
|||
bundle, err := c.fetchBundleFromAttestations(attestations)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, bundle)
|
||||
mockHTTPClient.AssertNumberOfCalls(t, "OnGetFail", 1)
|
||||
mockHTTPClient.AssertNumberOfCalls(t, "OnGetReqFail", 4)
|
||||
}
|
||||
|
||||
func TestFetchBundleByURL_FallbackToBundleField(t *testing.T) {
|
||||
func TestFetchBundleFromAttestations_FallbackToBundleField(t *testing.T) {
|
||||
mockHTTPClient := &mockHttpClient{}
|
||||
|
||||
c := &LiveClient{
|
||||
|
|
@ -252,6 +241,7 @@ func TestFetchBundleByURL_FallbackToBundleField(t *testing.T) {
|
|||
logger: io.NewTestHandler(),
|
||||
}
|
||||
|
||||
// If the bundle URL is empty, the code will fallback to the bundle field
|
||||
a := Attestation{Bundle: data.SigstoreBundle(t)}
|
||||
attestations := []*Attestation{&a}
|
||||
fetched, err := c.fetchBundleFromAttestations(attestations)
|
||||
|
|
@ -260,6 +250,71 @@ func TestFetchBundleByURL_FallbackToBundleField(t *testing.T) {
|
|||
mockHTTPClient.AssertNotCalled(t, "OnGetSuccess")
|
||||
}
|
||||
|
||||
// getBundle successfully fetches a bundle on the first HTTP request attempt
|
||||
func TestGetBundle(t *testing.T) {
|
||||
mockHTTPClient := &mockHttpClient{}
|
||||
|
||||
c := &LiveClient{
|
||||
httpClient: mockHTTPClient,
|
||||
logger: io.NewTestHandler(),
|
||||
}
|
||||
|
||||
b, err := c.getBundle("https://mybundleurl.com")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", b.GetMediaType())
|
||||
mockHTTPClient.AssertNumberOfCalls(t, "OnGetSuccess", 1)
|
||||
}
|
||||
|
||||
// getBundle retries successfully when the initial HTTP request returns
|
||||
// a 5XX status code
|
||||
func TestGetBundle_SuccessfulRetry(t *testing.T) {
|
||||
mockHTTPClient := &failAfterNCallsHttpClient{
|
||||
FailOnCallN: 1,
|
||||
FailOnAllSubsequentCalls: false,
|
||||
}
|
||||
|
||||
c := &LiveClient{
|
||||
httpClient: mockHTTPClient,
|
||||
logger: io.NewTestHandler(),
|
||||
}
|
||||
|
||||
b, err := c.getBundle("mybundleurl")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", b.GetMediaType())
|
||||
mockHTTPClient.AssertNumberOfCalls(t, "OnGetFailAfterNCalls", 2)
|
||||
}
|
||||
|
||||
// getBundle does not retry when the function fails with a permanent backoff error condition
|
||||
func TestGetBundle_PermanentBackoffFail(t *testing.T) {
|
||||
mockHTTPClient := &invalidBundleClient{}
|
||||
c := &LiveClient{
|
||||
httpClient: mockHTTPClient,
|
||||
logger: io.NewTestHandler(),
|
||||
}
|
||||
|
||||
b, err := c.getBundle("mybundleurl")
|
||||
// var permanent *backoff.PermanentError
|
||||
//require.IsType(t, &backoff.PermanentError{}, err)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, b)
|
||||
mockHTTPClient.AssertNumberOfCalls(t, "OnGetInvalidBundle", 1)
|
||||
}
|
||||
|
||||
// getBundle retries when the HTTP request fails
|
||||
func TestGetBundle_RequestFail(t *testing.T) {
|
||||
mockHTTPClient := &reqFailHttpClient{}
|
||||
|
||||
c := &LiveClient{
|
||||
httpClient: mockHTTPClient,
|
||||
logger: io.NewTestHandler(),
|
||||
}
|
||||
|
||||
b, err := c.getBundle("mybundleurl")
|
||||
require.Error(t, err)
|
||||
require.Nil(t, b)
|
||||
mockHTTPClient.AssertNumberOfCalls(t, "OnGetReqFail", 4)
|
||||
}
|
||||
|
||||
func TestGetTrustDomain(t *testing.T) {
|
||||
fetcher := mockMetaGenerator{
|
||||
TrustDomain: "foo",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/test/data"
|
||||
"github.com/golang/snappy"
|
||||
|
|
@ -27,34 +28,59 @@ func (m *mockHttpClient) Get(url string) (*http.Response, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
type failHttpClient struct {
|
||||
type invalidBundleClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *failHttpClient) Get(url string) (*http.Response, error) {
|
||||
m.On("OnGetFail").Return()
|
||||
m.MethodCalled("OnGetFail")
|
||||
func (m *invalidBundleClient) Get(url string) (*http.Response, error) {
|
||||
m.On("OnGetInvalidBundle").Return()
|
||||
m.MethodCalled("OnGetInvalidBundle")
|
||||
|
||||
var compressed []byte
|
||||
compressed = snappy.Encode(compressed, []byte("invalid bundle bytes"))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(bytes.NewReader(compressed)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type reqFailHttpClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *reqFailHttpClient) Get(url string) (*http.Response, error) {
|
||||
m.On("OnGetReqFail").Return()
|
||||
m.MethodCalled("OnGetReqFail")
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: 500,
|
||||
}, fmt.Errorf("failed to fetch with %s", url)
|
||||
}
|
||||
|
||||
type failAfterOneCallHttpClient struct {
|
||||
type failAfterNCallsHttpClient struct {
|
||||
mock.Mock
|
||||
mu sync.Mutex
|
||||
FailOnCallN int
|
||||
FailOnAllSubsequentCalls bool
|
||||
NumCalls int
|
||||
}
|
||||
|
||||
func (m *failAfterOneCallHttpClient) Get(url string) (*http.Response, error) {
|
||||
m.On("OnGetFailAfterOneCall").Return()
|
||||
func (m *failAfterNCallsHttpClient) Get(url string) (*http.Response, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if len(m.Calls) >= 1 {
|
||||
m.MethodCalled("OnGetFailAfterOneCall")
|
||||
m.On("OnGetFailAfterNCalls").Return()
|
||||
|
||||
m.NumCalls++
|
||||
|
||||
if m.NumCalls == m.FailOnCallN || (m.NumCalls > m.FailOnCallN && m.FailOnAllSubsequentCalls) {
|
||||
m.MethodCalled("OnGetFailAfterNCalls")
|
||||
return &http.Response{
|
||||
StatusCode: 500,
|
||||
}, fmt.Errorf("failed to fetch with %s", url)
|
||||
}, nil
|
||||
}
|
||||
|
||||
m.MethodCalled("OnGetFailAfterOneCall")
|
||||
m.MethodCalled("OnGetFailAfterNCalls")
|
||||
var compressed []byte
|
||||
compressed = snappy.Encode(compressed, data.SigstoreBundleRaw)
|
||||
return &http.Response{
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ func runDownload(opts *Options) error {
|
|||
}
|
||||
attestations, err := verification.GetRemoteAttestations(opts.APIClient, params)
|
||||
if err != nil {
|
||||
if errors.Is(err, api.ErrNoAttestations{}) {
|
||||
if errors.Is(err, api.ErrNoAttestationsFound) {
|
||||
fmt.Fprintf(opts.Logger.IO.Out, "No attestations found for %s\n", opts.ArtifactPath)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ func TestRunDownload(t *testing.T) {
|
|||
opts := baseOpts
|
||||
opts.APIClient = api.MockClient{
|
||||
OnGetByOwnerAndDigest: func(repo, digest string, limit int) ([]*api.Attestation, error) {
|
||||
return nil, api.ErrNoAttestations{}
|
||||
return nil, api.ErrNoAttestationsFound
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ func runInspect(opts *Options) error {
|
|||
}
|
||||
|
||||
// summarize cert if present
|
||||
if leafCert := verificationContent.GetCertificate(); leafCert != nil {
|
||||
if leafCert := verificationContent.Certificate(); leafCert != nil {
|
||||
|
||||
certSummary, err := certificate.SummarizeCertificate(leafCert)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"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"
|
||||
o "github.com/cli/cli/v2/pkg/option"
|
||||
ghauth "github.com/cli/go-gh/v2/pkg/auth"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
|
|
@ -121,7 +122,7 @@ func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options) error {
|
|||
var tufOptions []tufConfig
|
||||
var defaultTR = "trusted_root.json"
|
||||
|
||||
tufOpt := verification.DefaultOptionsWithCacheSetting()
|
||||
tufOpt := verification.DefaultOptionsWithCacheSetting(o.None[string]())
|
||||
// Disable local caching, so we get up-to-date response from TUF repository
|
||||
tufOpt.CacheValidity = 0
|
||||
|
||||
|
|
@ -150,7 +151,7 @@ func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options) error {
|
|||
targets: []string{defaultTR},
|
||||
})
|
||||
|
||||
tufOpt = verification.GitHubTUFOptions()
|
||||
tufOpt = verification.GitHubTUFOptions(o.None[string]())
|
||||
tufOpt.CacheValidity = 0
|
||||
tufOptions = append(tufOptions, tufConfig{
|
||||
tufOptions: tufOpt,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
|
||||
o "github.com/cli/cli/v2/pkg/option"
|
||||
|
||||
"github.com/sigstore/sigstore-go/pkg/bundle"
|
||||
"github.com/sigstore/sigstore-go/pkg/root"
|
||||
|
|
@ -34,6 +35,8 @@ type SigstoreConfig struct {
|
|||
NoPublicGood bool
|
||||
// If tenancy mode is not used, trust domain is empty
|
||||
TrustDomain string
|
||||
// TUFMetadataDir
|
||||
TUFMetadataDir o.Option[string]
|
||||
}
|
||||
|
||||
type SigstoreVerifier interface {
|
||||
|
|
@ -45,7 +48,8 @@ type LiveSigstoreVerifier struct {
|
|||
Logger *io.Handler
|
||||
NoPublicGood bool
|
||||
// If tenancy mode is not used, trust domain is empty
|
||||
TrustDomain string
|
||||
TrustDomain string
|
||||
TUFMetadataDir o.Option[string]
|
||||
}
|
||||
|
||||
var ErrNoAttestationsVerified = errors.New("no attestations were verified")
|
||||
|
|
@ -55,10 +59,11 @@ var ErrNoAttestationsVerified = errors.New("no attestations were verified")
|
|||
// Public Good, GitHub, or a custom trusted root.
|
||||
func NewLiveSigstoreVerifier(config SigstoreConfig) *LiveSigstoreVerifier {
|
||||
return &LiveSigstoreVerifier{
|
||||
TrustedRoot: config.TrustedRoot,
|
||||
Logger: config.Logger,
|
||||
NoPublicGood: config.NoPublicGood,
|
||||
TrustDomain: config.TrustDomain,
|
||||
TrustedRoot: config.TrustedRoot,
|
||||
Logger: config.Logger,
|
||||
NoPublicGood: config.NoPublicGood,
|
||||
TrustDomain: config.TrustDomain,
|
||||
TUFMetadataDir: config.TUFMetadataDir,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +75,7 @@ func getBundleIssuer(b *bundle.Bundle) (string, error) {
|
|||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get bundle verification content: %v", err)
|
||||
}
|
||||
leafCert := verifyContent.GetCertificate()
|
||||
leafCert := verifyContent.Certificate()
|
||||
if leafCert == nil {
|
||||
return "", fmt.Errorf("leaf cert not found")
|
||||
}
|
||||
|
|
@ -89,9 +94,9 @@ func (v *LiveSigstoreVerifier) chooseVerifier(issuer string) (*verify.SignedEnti
|
|||
if v.NoPublicGood {
|
||||
return nil, fmt.Errorf("detected public good instance but requested verification without public good instance")
|
||||
}
|
||||
return newPublicGoodVerifier()
|
||||
return newPublicGoodVerifier(v.TUFMetadataDir)
|
||||
case GitHubIssuerOrg:
|
||||
return newGitHubVerifier(v.TrustDomain)
|
||||
return newGitHubVerifier(v.TrustDomain, v.TUFMetadataDir)
|
||||
default:
|
||||
return nil, fmt.Errorf("leaf certificate issuer is not recognized")
|
||||
}
|
||||
|
|
@ -116,7 +121,11 @@ func (v *LiveSigstoreVerifier) chooseVerifier(issuer string) (*verify.SignedEnti
|
|||
// Compare bundle leafCert issuer with trusted root cert authority
|
||||
certAuthorities := trustedRoot.FulcioCertificateAuthorities()
|
||||
for _, certAuthority := range certAuthorities {
|
||||
lowestCert, err := getLowestCertInChain(&certAuthority)
|
||||
fulcioCertAuthority, ok := certAuthority.(*root.FulcioCertificateAuthority)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("trusted root cert authority is not a FulcioCertificateAuthority")
|
||||
}
|
||||
lowestCert, err := getLowestCertInChain(fulcioCertAuthority)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -149,10 +158,8 @@ func (v *LiveSigstoreVerifier) chooseVerifier(issuer string) (*verify.SignedEnti
|
|||
return nil, fmt.Errorf("unable to use provided trusted roots")
|
||||
}
|
||||
|
||||
func getLowestCertInChain(ca *root.CertificateAuthority) (*x509.Certificate, error) {
|
||||
if ca.Leaf != nil {
|
||||
return ca.Leaf, nil
|
||||
} else if len(ca.Intermediates) > 0 {
|
||||
func getLowestCertInChain(ca *root.FulcioCertificateAuthority) (*x509.Certificate, error) {
|
||||
if len(ca.Intermediates) > 0 {
|
||||
return ca.Intermediates[0], nil
|
||||
} else if ca.Root != nil {
|
||||
return ca.Root, nil
|
||||
|
|
@ -253,10 +260,10 @@ func newCustomVerifier(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerif
|
|||
return gv, nil
|
||||
}
|
||||
|
||||
func newGitHubVerifier(trustDomain string) (*verify.SignedEntityVerifier, error) {
|
||||
func newGitHubVerifier(trustDomain string, tufMetadataDir o.Option[string]) (*verify.SignedEntityVerifier, error) {
|
||||
var tr string
|
||||
|
||||
opts := GitHubTUFOptions()
|
||||
opts := GitHubTUFOptions(tufMetadataDir)
|
||||
client, err := tuf.New(opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create TUF client: %v", err)
|
||||
|
|
@ -287,8 +294,8 @@ func newGitHubVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.Si
|
|||
return gv, nil
|
||||
}
|
||||
|
||||
func newPublicGoodVerifier() (*verify.SignedEntityVerifier, error) {
|
||||
opts := DefaultOptionsWithCacheSetting()
|
||||
func newPublicGoodVerifier(tufMetadataDir o.Option[string]) (*verify.SignedEntityVerifier, error) {
|
||||
opts := DefaultOptionsWithCacheSetting(tufMetadataDir)
|
||||
client, err := tuf.New(opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create TUF client: %v", err)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/test"
|
||||
o "github.com/cli/cli/v2/pkg/option"
|
||||
|
||||
"github.com/sigstore/sigstore-go/pkg/verify"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -48,25 +49,29 @@ func TestLiveSigstoreVerifier(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
verifier := NewLiveSigstoreVerifier(SigstoreConfig{
|
||||
Logger: io.NewTestHandler(),
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
verifier := NewLiveSigstoreVerifier(SigstoreConfig{
|
||||
Logger: io.NewTestHandler(),
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
})
|
||||
|
||||
results, err := verifier.Verify(tc.attestations, publicGoodPolicy(t))
|
||||
|
||||
if tc.expectErr {
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, tc.errContains)
|
||||
require.Nil(t, results)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(tc.attestations), len(results))
|
||||
}
|
||||
})
|
||||
|
||||
results, err := verifier.Verify(tc.attestations, publicGoodPolicy(t))
|
||||
|
||||
if tc.expectErr {
|
||||
require.Error(t, err, "test case: %s", tc.name)
|
||||
require.ErrorContains(t, err, tc.errContains, "test case: %s", tc.name)
|
||||
require.Nil(t, results, "test case: %s", tc.name)
|
||||
} else {
|
||||
require.Equal(t, len(tc.attestations), len(results), "test case: %s", tc.name)
|
||||
require.NoError(t, err, "test case: %s", tc.name)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("with 2/3 verified attestations", func(t *testing.T) {
|
||||
verifier := NewLiveSigstoreVerifier(SigstoreConfig{
|
||||
Logger: io.NewTestHandler(),
|
||||
Logger: io.NewTestHandler(),
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
})
|
||||
|
||||
invalidBundle := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0-bundle-v0.1.json")
|
||||
|
|
@ -82,7 +87,8 @@ func TestLiveSigstoreVerifier(t *testing.T) {
|
|||
|
||||
t.Run("fail with 0/2 verified attestations", func(t *testing.T) {
|
||||
verifier := NewLiveSigstoreVerifier(SigstoreConfig{
|
||||
Logger: io.NewTestHandler(),
|
||||
Logger: io.NewTestHandler(),
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
})
|
||||
|
||||
invalidBundle := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0-bundle-v0.1.json")
|
||||
|
|
@ -105,7 +111,8 @@ func TestLiveSigstoreVerifier(t *testing.T) {
|
|||
attestations := getAttestationsFor(t, "../test/data/github_provenance_demo-0.0.12-py3-none-any-bundle.jsonl")
|
||||
|
||||
verifier := NewLiveSigstoreVerifier(SigstoreConfig{
|
||||
Logger: io.NewTestHandler(),
|
||||
Logger: io.NewTestHandler(),
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
})
|
||||
|
||||
results, err := verifier.Verify(attestations, githubPolicy)
|
||||
|
|
@ -117,8 +124,9 @@ func TestLiveSigstoreVerifier(t *testing.T) {
|
|||
attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl")
|
||||
|
||||
verifier := NewLiveSigstoreVerifier(SigstoreConfig{
|
||||
Logger: io.NewTestHandler(),
|
||||
TrustedRoot: test.NormalizeRelativePath("../test/data/trusted_root.json"),
|
||||
Logger: io.NewTestHandler(),
|
||||
TrustedRoot: test.NormalizeRelativePath("../test/data/trusted_root.json"),
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
})
|
||||
|
||||
results, err := verifier.Verify(attestations, publicGoodPolicy(t))
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
o "github.com/cli/cli/v2/pkg/option"
|
||||
"github.com/cli/go-gh/v2/pkg/config"
|
||||
"github.com/sigstore/sigstore-go/pkg/tuf"
|
||||
)
|
||||
|
|
@ -14,7 +15,7 @@ var githubRoot []byte
|
|||
|
||||
const GitHubTUFMirror = "https://tuf-repo.github.com"
|
||||
|
||||
func DefaultOptionsWithCacheSetting() *tuf.Options {
|
||||
func DefaultOptionsWithCacheSetting(tufMetadataDir o.Option[string]) *tuf.Options {
|
||||
opts := tuf.DefaultOptions()
|
||||
|
||||
// The CODESPACES environment variable will be set to true in a Codespaces workspace
|
||||
|
|
@ -25,8 +26,8 @@ func DefaultOptionsWithCacheSetting() *tuf.Options {
|
|||
opts.DisableLocalCache = true
|
||||
}
|
||||
|
||||
// Set the cache path to a directory owned by the CLI
|
||||
opts.CachePath = filepath.Join(config.CacheDir(), ".sigstore", "root")
|
||||
// Set the cache path to the provided dir, or a directory owned by the CLI
|
||||
opts.CachePath = tufMetadataDir.UnwrapOr(filepath.Join(config.CacheDir(), ".sigstore", "root"))
|
||||
|
||||
// Allow TUF cache for 1 day
|
||||
opts.CacheValidity = 1
|
||||
|
|
@ -34,8 +35,8 @@ func DefaultOptionsWithCacheSetting() *tuf.Options {
|
|||
return opts
|
||||
}
|
||||
|
||||
func GitHubTUFOptions() *tuf.Options {
|
||||
opts := DefaultOptionsWithCacheSetting()
|
||||
func GitHubTUFOptions(tufMetadataDir o.Option[string]) *tuf.Options {
|
||||
opts := DefaultOptionsWithCacheSetting(tufMetadataDir)
|
||||
|
||||
opts.Root = githubRoot
|
||||
opts.RepositoryBaseURL = GitHubTUFMirror
|
||||
|
|
|
|||
|
|
@ -5,16 +5,22 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
o "github.com/cli/cli/v2/pkg/option"
|
||||
"github.com/cli/go-gh/v2/pkg/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGitHubTUFOptions(t *testing.T) {
|
||||
func TestGitHubTUFOptionsNoMetadataDir(t *testing.T) {
|
||||
os.Setenv("CODESPACES", "true")
|
||||
opts := GitHubTUFOptions()
|
||||
opts := GitHubTUFOptions(o.None[string]())
|
||||
|
||||
require.Equal(t, GitHubTUFMirror, opts.RepositoryBaseURL)
|
||||
require.NotNil(t, opts.Root)
|
||||
require.True(t, opts.DisableLocalCache)
|
||||
require.Equal(t, filepath.Join(config.CacheDir(), ".sigstore", "root"), opts.CachePath)
|
||||
}
|
||||
|
||||
func TestGitHubTUFOptionsWithMetadataDir(t *testing.T) {
|
||||
opts := GitHubTUFOptions(o.Some("anything"))
|
||||
require.Equal(t, "anything", opts.CachePath)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/test"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
|
||||
o "github.com/cli/cli/v2/pkg/option"
|
||||
"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -25,7 +26,8 @@ func getAttestationsFor(t *testing.T, bundlePath string) []*api.Attestation {
|
|||
|
||||
func TestVerifyAttestations(t *testing.T) {
|
||||
sgVerifier := verification.NewLiveSigstoreVerifier(verification.SigstoreConfig{
|
||||
Logger: io.NewTestHandler(),
|
||||
Logger: io.NewTestHandler(),
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
})
|
||||
|
||||
certSummary := certificate.Summary{}
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ func runVerify(opts *Options) error {
|
|||
|
||||
attestations, logMsg, err := getAttestations(opts, *artifact)
|
||||
if err != nil {
|
||||
if ok := errors.Is(err, api.ErrNoAttestations{}); ok {
|
||||
if ok := errors.Is(err, api.ErrNoAttestationsFound); ok {
|
||||
opts.Logger.Printf(opts.Logger.ColorScheme.Red("✗ No attestations found for subject %s\n"), artifact.DigestWithAlg())
|
||||
return err
|
||||
}
|
||||
|
|
@ -239,7 +239,7 @@ func runVerify(opts *Options) error {
|
|||
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 err
|
||||
return fmt.Errorf("no matching predicate found")
|
||||
}
|
||||
attestations = filteredAttestations
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/cmd/attestation/test"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
|
||||
"github.com/cli/cli/v2/pkg/cmd/factory"
|
||||
o "github.com/cli/cli/v2/pkg/option"
|
||||
"github.com/cli/go-gh/v2/pkg/auth"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -19,7 +20,8 @@ func TestVerifyIntegration(t *testing.T) {
|
|||
logger := io.NewTestHandler()
|
||||
|
||||
sigstoreConfig := verification.SigstoreConfig{
|
||||
Logger: logger,
|
||||
Logger: logger,
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
}
|
||||
|
||||
cmdFactory := factory.New("test")
|
||||
|
|
@ -130,7 +132,8 @@ func TestVerifyIntegrationCustomIssuer(t *testing.T) {
|
|||
logger := io.NewTestHandler()
|
||||
|
||||
sigstoreConfig := verification.SigstoreConfig{
|
||||
Logger: logger,
|
||||
Logger: logger,
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
}
|
||||
|
||||
cmdFactory := factory.New("test")
|
||||
|
|
@ -200,7 +203,8 @@ func TestVerifyIntegrationReusableWorkflow(t *testing.T) {
|
|||
logger := io.NewTestHandler()
|
||||
|
||||
sigstoreConfig := verification.SigstoreConfig{
|
||||
Logger: logger,
|
||||
Logger: logger,
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
}
|
||||
|
||||
cmdFactory := factory.New("test")
|
||||
|
|
@ -289,7 +293,8 @@ func TestVerifyIntegrationReusableWorkflowSignerWorkflow(t *testing.T) {
|
|||
logger := io.NewTestHandler()
|
||||
|
||||
sigstoreConfig := verification.SigstoreConfig{
|
||||
Logger: logger,
|
||||
Logger: logger,
|
||||
TUFMetadataDir: o.Some(t.TempDir()),
|
||||
}
|
||||
|
||||
cmdFactory := factory.New("test")
|
||||
|
|
|
|||
|
|
@ -501,6 +501,18 @@ func TestRunVerify(t *testing.T) {
|
|||
require.Nil(t, runVerify(&customOpts))
|
||||
})
|
||||
|
||||
t.Run("with valid OCI artifact with UseBundleFromRegistry flag and unknown predicate type", func(t *testing.T) {
|
||||
customOpts := publicGoodOpts
|
||||
customOpts.ArtifactPath = "oci://ghcr.io/github/test"
|
||||
customOpts.BundlePath = ""
|
||||
customOpts.UseBundleFromRegistry = true
|
||||
customOpts.PredicateType = "https://predicate.type"
|
||||
|
||||
err := runVerify(&customOpts)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "no matching predicate found")
|
||||
})
|
||||
|
||||
t.Run("with valid OCI artifact with UseBundleFromRegistry flag but no bundle return from registry", func(t *testing.T) {
|
||||
customOpts := publicGoodOpts
|
||||
customOpts.ArtifactPath = "oci://ghcr.io/github/test"
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
|
|||
The minimum required scopes for the token are: %[1]srepo%[1]s, %[1]sread:org%[1]s, and %[1]sgist%[1]s.
|
||||
Take care when passing a fine-grained personal access token to %[1]s--with-token%[1]s
|
||||
as the inherent scoping to certain resources may cause confusing behaviour when interacting with other
|
||||
resources. Favour setting %[1]sGH_TOKEN$%[1]s for fine-grained personal access token usage.
|
||||
resources. Favour setting %[1]sGH_TOKEN$%[1]s for fine-grained personal access token usage.
|
||||
|
||||
Alternatively, gh will use the authentication token found in environment variables.
|
||||
This method is most suitable for "headless" use of gh such as in automation. See
|
||||
|
|
@ -94,7 +94,7 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
|
|||
# Start interactive setup
|
||||
$ gh auth login
|
||||
|
||||
# Authenticate against github.com by reading the token from a file
|
||||
# Authenticate against <github.com> by reading the token from a file
|
||||
$ gh auth login --with-token < mytoken.txt
|
||||
|
||||
# Authenticate with specific host
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ func NewCmdBrowse(f *cmdutil.Factory, runF func(*BrowseOptions) error) *cobra.Co
|
|||
- Repository home page
|
||||
- Repository settings
|
||||
`),
|
||||
Use: "browse [<number> | <path> | <commit-SHA>]",
|
||||
Use: "browse [<number> | <path> | <commit-sha>]",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Example: heredoc.Doc(`
|
||||
$ gh browse
|
||||
|
|
|
|||
28
pkg/cmd/cache/delete/delete.go
vendored
28
pkg/cmd/cache/delete/delete.go
vendored
|
|
@ -22,8 +22,9 @@ type DeleteOptions struct {
|
|||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
|
||||
DeleteAll bool
|
||||
Identifier string
|
||||
DeleteAll bool
|
||||
SucceedOnNoCaches bool
|
||||
Identifier string
|
||||
}
|
||||
|
||||
func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Command {
|
||||
|
|
@ -33,7 +34,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
|
|||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete [<cache-id>| <cache-key> | --all]",
|
||||
Use: "delete [<cache-id> | <cache-key> | --all]",
|
||||
Short: "Delete GitHub Actions caches",
|
||||
Long: heredoc.Docf(`
|
||||
Delete GitHub Actions caches.
|
||||
|
|
@ -50,8 +51,11 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
|
|||
# Delete a cache by id in a specific repo
|
||||
$ gh cache delete 1234 --repo cli/cli
|
||||
|
||||
# Delete all caches
|
||||
# Delete all caches (exit code 1 on no caches)
|
||||
$ gh cache delete --all
|
||||
|
||||
# Delete all caches (exit code 0 on no caches)
|
||||
$ gh cache delete --all --succeed-on-no-caches
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
|
@ -65,6 +69,10 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
|
|||
return err
|
||||
}
|
||||
|
||||
if !opts.DeleteAll && opts.SucceedOnNoCaches {
|
||||
return cmdutil.FlagErrorf("--succeed-on-no-caches must be used in conjunction with --all")
|
||||
}
|
||||
|
||||
if !opts.DeleteAll && len(args) == 0 {
|
||||
return cmdutil.FlagErrorf("must provide either cache id, cache key, or use --all")
|
||||
}
|
||||
|
|
@ -82,6 +90,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
|
|||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.DeleteAll, "all", "a", false, "Delete all caches")
|
||||
cmd.Flags().BoolVar(&opts.SucceedOnNoCaches, "succeed-on-no-caches", false, "Return exit code 0 if no caches found. Must be used in conjunction with `--all`")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -100,12 +109,21 @@ func deleteRun(opts *DeleteOptions) error {
|
|||
|
||||
var toDelete []string
|
||||
if opts.DeleteAll {
|
||||
opts.IO.StartProgressIndicator()
|
||||
caches, err := shared.GetCaches(client, repo, shared.GetCachesOptions{Limit: -1})
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(caches.ActionsCaches) == 0 {
|
||||
return fmt.Errorf("%s No caches to delete", opts.IO.ColorScheme().FailureIcon())
|
||||
if opts.SucceedOnNoCaches {
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(opts.IO.Out, "%s No caches to delete\n", opts.IO.ColorScheme().SuccessIcon())
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("%s No caches to delete", opts.IO.ColorScheme().FailureIcon())
|
||||
}
|
||||
}
|
||||
for _, cache := range caches.ActionsCaches {
|
||||
toDelete = append(toDelete, strconv.Itoa(cache.Id))
|
||||
|
|
|
|||
77
pkg/cmd/cache/delete/delete_test.go
vendored
77
pkg/cmd/cache/delete/delete_test.go
vendored
|
|
@ -43,6 +43,21 @@ func TestNewCmdDelete(t *testing.T) {
|
|||
cli: "--all",
|
||||
wants: DeleteOptions{DeleteAll: true},
|
||||
},
|
||||
{
|
||||
name: "delete all and succeed-on-no-caches flags",
|
||||
cli: "--all --succeed-on-no-caches",
|
||||
wants: DeleteOptions{DeleteAll: true, SucceedOnNoCaches: true},
|
||||
},
|
||||
{
|
||||
name: "succeed-on-no-caches flag",
|
||||
cli: "--succeed-on-no-caches",
|
||||
wantsErr: "--succeed-on-no-caches must be used in conjunction with --all",
|
||||
},
|
||||
{
|
||||
name: "succeed-on-no-caches flag and id argument",
|
||||
cli: "--succeed-on-no-caches 123",
|
||||
wantsErr: "--succeed-on-no-caches must be used in conjunction with --all",
|
||||
},
|
||||
{
|
||||
name: "id argument and delete all flag",
|
||||
cli: "1 --all",
|
||||
|
|
@ -72,6 +87,7 @@ func TestNewCmdDelete(t *testing.T) {
|
|||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wants.DeleteAll, gotOpts.DeleteAll)
|
||||
assert.Equal(t, tt.wants.SucceedOnNoCaches, gotOpts.SucceedOnNoCaches)
|
||||
assert.Equal(t, tt.wants.Identifier, gotOpts.Identifier)
|
||||
})
|
||||
}
|
||||
|
|
@ -160,6 +176,19 @@ func TestDeleteRun(t *testing.T) {
|
|||
tty: true,
|
||||
wantStdout: "✓ Deleted 2 caches from OWNER/REPO\n",
|
||||
},
|
||||
{
|
||||
name: "attempts to delete all caches but api errors",
|
||||
opts: DeleteOptions{DeleteAll: true},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.StatusStringResponse(500, ""),
|
||||
)
|
||||
},
|
||||
tty: true,
|
||||
wantErr: true,
|
||||
wantErrMsg: "HTTP 500 (https://api.github.com/repos/OWNER/REPO/actions/caches?per_page=100)",
|
||||
},
|
||||
{
|
||||
name: "displays delete error",
|
||||
opts: DeleteOptions{Identifier: "123"},
|
||||
|
|
@ -186,6 +215,54 @@ func TestDeleteRun(t *testing.T) {
|
|||
tty: true,
|
||||
wantStdout: "✓ Deleted 1 cache from OWNER/REPO\n",
|
||||
},
|
||||
{
|
||||
name: "no caches to delete when deleting all",
|
||||
opts: DeleteOptions{DeleteAll: true},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.JSONResponse(shared.CachePayload{
|
||||
ActionsCaches: []shared.Cache{},
|
||||
TotalCount: 0,
|
||||
}),
|
||||
)
|
||||
},
|
||||
tty: false,
|
||||
wantErr: true,
|
||||
wantErrMsg: "X No caches to delete",
|
||||
},
|
||||
{
|
||||
name: "no caches to delete when deleting all but succeed on no cache tty",
|
||||
opts: DeleteOptions{DeleteAll: true, SucceedOnNoCaches: true},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.JSONResponse(shared.CachePayload{
|
||||
ActionsCaches: []shared.Cache{},
|
||||
TotalCount: 0,
|
||||
}),
|
||||
)
|
||||
},
|
||||
tty: true,
|
||||
wantErr: false,
|
||||
wantStdout: "✓ No caches to delete\n",
|
||||
},
|
||||
{
|
||||
name: "no caches to delete when deleting all but succeed on no cache non-tty",
|
||||
opts: DeleteOptions{DeleteAll: true, SucceedOnNoCaches: true},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.JSONResponse(shared.CachePayload{
|
||||
ActionsCaches: []shared.Cache{},
|
||||
TotalCount: 0,
|
||||
}),
|
||||
)
|
||||
},
|
||||
tty: false,
|
||||
wantErr: false,
|
||||
wantStdout: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ func newRebuildCmd(app *App) *cobra.Command {
|
|||
Short: "Rebuild a codespace",
|
||||
Long: heredoc.Doc(`
|
||||
Rebuilding recreates your codespace.
|
||||
|
||||
|
||||
Your code and any current changes will be preserved. Your codespace will be rebuilt using
|
||||
your working directory's dev container. A full rebuild also removes cached Docker images.
|
||||
`),
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ func newSSHCmd(app *App) *cobra.Command {
|
|||
Long: heredoc.Docf(`
|
||||
The %[1]sssh%[1]s command is used to SSH into a codespace. In its simplest form, you can
|
||||
run %[1]sgh cs ssh%[1]s, select a codespace interactively, and connect.
|
||||
|
||||
|
||||
The %[1]sssh%[1]s command will automatically create a public/private ssh key pair in the
|
||||
%[1]s~/.ssh%[1]s directory if you do not have an existing valid key pair. When selecting the
|
||||
key pair to use, the preferred order is:
|
||||
|
|
@ -732,8 +732,8 @@ func newCpCmd(app *App) *cobra.Command {
|
|||
be evaluated on the remote machine, subject to expansion of tildes, braces, globs,
|
||||
environment variables, and backticks. For security, do not use this flag with arguments
|
||||
provided by untrusted users; see <https://lwn.net/Articles/835962/> for discussion.
|
||||
|
||||
By default, the %[1]scp%[1]s command will create a public/private ssh key pair to authenticate with
|
||||
|
||||
By default, the %[1]scp%[1]s command will create a public/private ssh key pair to authenticate with
|
||||
the codespace inside the %[1]s~/.ssh directory%[1]s.
|
||||
`, "`"),
|
||||
Example: heredoc.Doc(`
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ func newViewCmd(app *App) *cobra.Command {
|
|||
Short: "View details about a codespace",
|
||||
Example: heredoc.Doc(`
|
||||
# select a codespace from a list of all codespaces you own
|
||||
$ gh cs view
|
||||
$ gh cs view
|
||||
|
||||
# view the details of a specific codespace
|
||||
$ gh cs view -c codespace-name-12345
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func NewCmdCompletion(io *iostreams.IOStreams) *cobra.Command {
|
|||
After, add this to your %[1]s~/.bash_profile%[1]s:
|
||||
|
||||
eval "$(gh completion -s bash)"
|
||||
|
||||
|
||||
### zsh
|
||||
|
||||
Generate a %[1]s_gh%[1]s completion script and put it somewhere in your %[1]s$fpath%[1]s:
|
||||
|
|
@ -44,7 +44,7 @@ func NewCmdCompletion(io *iostreams.IOStreams) *cobra.Command {
|
|||
|
||||
autoload -U compinit
|
||||
compinit -i
|
||||
|
||||
|
||||
Zsh version 5.7 or later is recommended.
|
||||
|
||||
### fish
|
||||
|
|
@ -59,7 +59,7 @@ func NewCmdCompletion(io *iostreams.IOStreams) *cobra.Command {
|
|||
|
||||
mkdir -Path (Split-Path -Parent $profile) -ErrorAction SilentlyContinue
|
||||
notepad $profile
|
||||
|
||||
|
||||
Add the line and save the file:
|
||||
|
||||
Invoke-Expression -Command $(gh completion -s powershell | Out-String)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ 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")
|
||||
longDoc.WriteString("Current respected settings:\n\n")
|
||||
for _, co := range config.Options {
|
||||
longDoc.WriteString(fmt.Sprintf("- `%s`: %s", co.Key, co.Description))
|
||||
if len(co.AllowedValues) > 0 {
|
||||
|
|
|
|||
|
|
@ -298,8 +298,17 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
|
|||
Long: heredoc.Docf(`
|
||||
Install a GitHub CLI extension from a GitHub or local repository.
|
||||
|
||||
For GitHub repositories, the repository argument can be specified in %[1]sOWNER/REPO%[1]s format or as a full repository URL.
|
||||
The URL format is useful when the repository is not hosted on github.com.
|
||||
For GitHub repositories, the repository argument can be specified in
|
||||
%[1]sOWNER/REPO%[1]s format or as a full repository URL.
|
||||
The URL format is useful when the repository is not hosted on <github.com>.
|
||||
|
||||
For remote repositories, the GitHub CLI first looks for the release artifacts assuming
|
||||
that it's a binary extension i.e. prebuilt binaries provided as part of the release.
|
||||
In the absence of a release, the repository itself is cloned assuming that it's a
|
||||
script extension i.e. prebuilt executable or script exists on its root.
|
||||
|
||||
The %[1]s--pin%[1]s flag may be used to specify a tag or commit for binary and script
|
||||
extensions respectively, the latest version is used otherwise.
|
||||
|
||||
For local repositories, often used while developing extensions, use %[1]s.%[1]s as the
|
||||
value of the repository argument. Note the following:
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ func (m *Manager) Install(repo ghrepo.Interface, target string) error {
|
|||
return err
|
||||
}
|
||||
if !hs {
|
||||
return errors.New("extension is not installable: missing executable")
|
||||
return fmt.Errorf("extension is not installable: no usable release artifact or script found in %s", repo)
|
||||
}
|
||||
|
||||
return m.installGit(repo, target)
|
||||
|
|
|
|||
|
|
@ -21,25 +21,24 @@ type remoteResolver struct {
|
|||
readRemotes func() (git.RemoteSet, error)
|
||||
getConfig func() (gh.Config, error)
|
||||
urlTranslator context.Translator
|
||||
cachedRemotes context.Remotes
|
||||
remotesError error
|
||||
}
|
||||
|
||||
func (rr *remoteResolver) Resolver() func() (context.Remotes, error) {
|
||||
var cachedRemotes context.Remotes
|
||||
var remotesError error
|
||||
|
||||
return func() (context.Remotes, error) {
|
||||
if cachedRemotes != nil || remotesError != nil {
|
||||
return cachedRemotes, remotesError
|
||||
if rr.cachedRemotes != nil || rr.remotesError != nil {
|
||||
return rr.cachedRemotes, rr.remotesError
|
||||
}
|
||||
|
||||
gitRemotes, err := rr.readRemotes()
|
||||
if err != nil {
|
||||
remotesError = err
|
||||
rr.remotesError = err
|
||||
return nil, err
|
||||
}
|
||||
if len(gitRemotes) == 0 {
|
||||
remotesError = errors.New("no git remotes found")
|
||||
return nil, remotesError
|
||||
rr.remotesError = errors.New("no git remotes found")
|
||||
return nil, rr.remotesError
|
||||
}
|
||||
|
||||
sshTranslate := rr.urlTranslator
|
||||
|
|
@ -68,30 +67,31 @@ func (rr *remoteResolver) Resolver() func() (context.Remotes, error) {
|
|||
// Sort remotes
|
||||
sort.Sort(resolvedRemotes)
|
||||
|
||||
// Filter remotes by hosts
|
||||
// Note that this is not caching correctly: https://github.com/cli/cli/issues/10103
|
||||
cachedRemotes := resolvedRemotes.FilterByHosts(hosts)
|
||||
rr.cachedRemotes = resolvedRemotes.FilterByHosts(hosts)
|
||||
|
||||
// Filter again by default host if one is set
|
||||
// For config file default host fallback to cachedRemotes if none match
|
||||
// For environment default host (GH_HOST) do not fallback to cachedRemotes if none match
|
||||
if src != "default" {
|
||||
filteredRemotes := cachedRemotes.FilterByHosts([]string{defaultHost})
|
||||
filteredRemotes := rr.cachedRemotes.FilterByHosts([]string{defaultHost})
|
||||
if isHostEnv(src) || len(filteredRemotes) > 0 {
|
||||
cachedRemotes = filteredRemotes
|
||||
rr.cachedRemotes = filteredRemotes
|
||||
}
|
||||
}
|
||||
|
||||
if len(cachedRemotes) == 0 {
|
||||
if len(rr.cachedRemotes) == 0 {
|
||||
if isHostEnv(src) {
|
||||
return nil, fmt.Errorf("none of the git remotes configured for this repository correspond to the %s environment variable. Try adding a matching remote or unsetting the variable.", src)
|
||||
rr.remotesError = fmt.Errorf("none of the git remotes configured for this repository correspond to the %s environment variable. Try adding a matching remote or unsetting the variable", src)
|
||||
return nil, rr.remotesError
|
||||
} else if cfg.Authentication().HasEnvToken() {
|
||||
return nil, errors.New("set the GH_HOST environment variable to specify which GitHub host to use")
|
||||
rr.remotesError = errors.New("set the GH_HOST environment variable to specify which GitHub host to use")
|
||||
return nil, rr.remotesError
|
||||
}
|
||||
return nil, errors.New("none of the git remotes configured for this repository point to a known GitHub host. To tell gh about a new GitHub host, please use `gh auth login`")
|
||||
rr.remotesError = errors.New("none of the git remotes configured for this repository point to a known GitHub host. To tell gh about a new GitHub host, please use `gh auth login`")
|
||||
return nil, rr.remotesError
|
||||
}
|
||||
|
||||
return cachedRemotes, nil
|
||||
return rr.cachedRemotes, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
package factory
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/context"
|
||||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
ghmock "github.com/cli/cli/v2/internal/gh/mock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type identityTranslator struct{}
|
||||
|
|
@ -288,3 +291,95 @@ func Test_remoteResolver(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_remoteResolver_Caching(t *testing.T) {
|
||||
t.Run("cache remotes", func(t *testing.T) {
|
||||
var readRemotesCalled bool
|
||||
|
||||
rr := &remoteResolver{
|
||||
readRemotes: func() (git.RemoteSet, error) {
|
||||
if readRemotesCalled {
|
||||
return git.RemoteSet{}, errors.New("readRemotes should only be called once")
|
||||
}
|
||||
|
||||
readRemotesCalled = true
|
||||
return git.RemoteSet{
|
||||
git.NewRemote("origin", "https://github.com/owner/repo.git"),
|
||||
}, nil
|
||||
},
|
||||
getConfig: func() (gh.Config, error) {
|
||||
cfg := &ghmock.ConfigMock{}
|
||||
cfg.AuthenticationFunc = func() gh.AuthConfig {
|
||||
authCfg := &config.AuthConfig{}
|
||||
authCfg.SetHosts([]string{"github.com"})
|
||||
authCfg.SetDefaultHost("github.com", "default")
|
||||
return authCfg
|
||||
}
|
||||
return cfg, nil
|
||||
},
|
||||
urlTranslator: identityTranslator{},
|
||||
}
|
||||
|
||||
resolver := rr.Resolver()
|
||||
|
||||
expectedRemoteNames := []string{"origin"}
|
||||
remotes, err := resolver()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedRemoteNames, mapRemotesToNames(remotes))
|
||||
|
||||
require.Equal(t, readRemotesCalled, true)
|
||||
|
||||
cachedRemotes, err := resolver()
|
||||
require.NoError(t, err, "expected no error to be cached")
|
||||
require.Equal(t, expectedRemoteNames, mapRemotesToNames(cachedRemotes), "expected the remotes to be cached")
|
||||
})
|
||||
|
||||
t.Run("cache error", func(t *testing.T) {
|
||||
var readRemotesCalled bool
|
||||
|
||||
rr := &remoteResolver{
|
||||
readRemotes: func() (git.RemoteSet, error) {
|
||||
if readRemotesCalled {
|
||||
return git.RemoteSet{
|
||||
git.NewRemote("origin", "https://github.com/owner/repo.git"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
readRemotesCalled = true
|
||||
return git.RemoteSet{}, errors.New("error to be cached")
|
||||
},
|
||||
getConfig: func() (gh.Config, error) {
|
||||
cfg := &ghmock.ConfigMock{}
|
||||
cfg.AuthenticationFunc = func() gh.AuthConfig {
|
||||
authCfg := &config.AuthConfig{}
|
||||
authCfg.SetHosts([]string{"github.com"})
|
||||
authCfg.SetDefaultHost("github.com", "default")
|
||||
return authCfg
|
||||
}
|
||||
return cfg, nil
|
||||
},
|
||||
urlTranslator: identityTranslator{},
|
||||
}
|
||||
|
||||
resolver := rr.Resolver()
|
||||
|
||||
expectedErr := errors.New("error to be cached")
|
||||
remotes, err := resolver()
|
||||
require.Equal(t, expectedErr, err)
|
||||
require.Empty(t, remotes, "should return no remotes")
|
||||
|
||||
require.Equal(t, readRemotesCalled, true)
|
||||
|
||||
cachedRemotes, err := resolver()
|
||||
require.Equal(t, expectedErr, err, "expected the error to be cached")
|
||||
require.Empty(t, cachedRemotes, "should return no remotes")
|
||||
})
|
||||
}
|
||||
|
||||
func mapRemotesToNames(remotes context.Remotes) []string {
|
||||
names := make([]string, len(remotes))
|
||||
for i, r := range remotes {
|
||||
names[i] = r.Name
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
|
|
|||
|
|
@ -540,14 +540,14 @@ func Test_listRun(t *testing.T) {
|
|||
wantOut: heredoc.Doc(`
|
||||
1234 main.txt
|
||||
octo match in the description
|
||||
|
||||
|
||||
2345 octo.txt
|
||||
match in the file name
|
||||
|
||||
3456 main.txt
|
||||
match in the file text
|
||||
octo in the text
|
||||
|
||||
|
||||
`),
|
||||
},
|
||||
{
|
||||
|
|
@ -599,14 +599,14 @@ func Test_listRun(t *testing.T) {
|
|||
wantOut: heredoc.Docf(`
|
||||
%[1]s[0;34m1234%[1]s[0m %[1]s[0;32mmain.txt%[1]s[0m
|
||||
%[1]s[0;30;43mocto%[1]s[0m%[1]s[0;1;39m match in the description%[1]s[0m
|
||||
|
||||
|
||||
%[1]s[0;34m2345%[1]s[0m %[1]s[0;30;43mocto%[1]s[0m%[1]s[0;32m.txt%[1]s[0m
|
||||
%[1]s[0;1;39mmatch in the file name%[1]s[0m
|
||||
|
||||
|
||||
%[1]s[0;34m3456%[1]s[0m %[1]s[0;32mmain.txt%[1]s[0m
|
||||
%[1]s[0;1;39mmatch in the file text%[1]s[0m
|
||||
%[1]s[0;30;43mocto%[1]s[0m in the text
|
||||
|
||||
|
||||
`, "\x1b"),
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ func NewCmdRename(f *cmdutil.Factory, runf func(*RenameOptions) error) *cobra.Co
|
|||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "rename {<id> | <url>} <oldFilename> <newFilename>",
|
||||
Use: "rename {<id> | <url>} <old-filename> <new-filename>",
|
||||
Short: "Rename a file in a gist",
|
||||
Long: heredoc.Doc(`Rename a file in the given gist ID / URL.`),
|
||||
Args: cobra.ExactArgs(3),
|
||||
|
|
|
|||
|
|
@ -11,12 +11,13 @@ import (
|
|||
|
||||
func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) error) *cobra.Command {
|
||||
opts := &prShared.CommentableOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
EditSurvey: prShared.CommentableEditSurvey(f.Config, f.IOStreams),
|
||||
InteractiveEditSurvey: prShared.CommentableInteractiveEditSurvey(f.Config, f.IOStreams),
|
||||
ConfirmSubmitSurvey: prShared.CommentableConfirmSubmitSurvey(f.Prompter),
|
||||
OpenInBrowser: f.Browser.Browse,
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
EditSurvey: prShared.CommentableEditSurvey(f.Config, f.IOStreams),
|
||||
InteractiveEditSurvey: prShared.CommentableInteractiveEditSurvey(f.Config, f.IOStreams),
|
||||
ConfirmSubmitSurvey: prShared.CommentableConfirmSubmitSurvey(f.Prompter),
|
||||
ConfirmCreateIfNoneSurvey: prShared.CommentableInteractiveCreateIfNoneSurvey(f.Prompter),
|
||||
OpenInBrowser: f.Browser.Browse,
|
||||
}
|
||||
|
||||
var bodyFile string
|
||||
|
|
@ -69,6 +70,7 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) e
|
|||
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.CreateIfNone, "create-if-none", false, "Create a new comment if no comments are found. Can be used only with --edit-last")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,6 +109,29 @@ func TestNewCmdComment(t *testing.T) {
|
|||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "edit last flag",
|
||||
input: "1 --edit-last",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "edit last flag with create if none",
|
||||
input: "1 --edit-last --create-if-none",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "body and body-file flags",
|
||||
input: "1 --body 'test' --body-file 'test-file.txt'",
|
||||
|
|
@ -139,6 +162,12 @@ func TestNewCmdComment(t *testing.T) {
|
|||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "create-if-none flag without edit-last",
|
||||
input: "1 --create-if-none",
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -188,11 +217,12 @@ func TestNewCmdComment(t *testing.T) {
|
|||
|
||||
func Test_commentRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input *shared.CommentableOptions
|
||||
httpStubs func(*testing.T, *httpmock.Registry)
|
||||
stdout string
|
||||
stderr string
|
||||
name string
|
||||
input *shared.CommentableOptions
|
||||
emptyComments bool
|
||||
httpStubs func(*testing.T, *httpmock.Registry)
|
||||
stdout string
|
||||
stderr string
|
||||
}{
|
||||
{
|
||||
name: "interactive editor",
|
||||
|
|
@ -225,6 +255,24 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-111\n",
|
||||
},
|
||||
{
|
||||
name: "interactive editor with edit last and create if none",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
|
||||
InteractiveEditSurvey: func(string) (string, error) { return "comment body", nil },
|
||||
ConfirmCreateIfNoneSurvey: func() (bool, error) { return true, nil },
|
||||
ConfirmSubmitSurvey: func() (bool, error) { return true, nil },
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentUpdate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-111\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive web",
|
||||
input: &shared.CommentableOptions{
|
||||
|
|
@ -248,6 +296,39 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
stderr: "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive web with edit last and create if none for empty comments",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
|
||||
OpenInBrowser: func(u string) error {
|
||||
assert.Contains(t, u, "#issuecomment-new")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
emptyComments: true,
|
||||
stderr: "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive web with edit last and create if none",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
|
||||
OpenInBrowser: func(u string) error {
|
||||
assert.Contains(t, u, "#issuecomment-111")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
stderr: "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive editor",
|
||||
input: &shared.CommentableOptions{
|
||||
|
|
@ -277,6 +358,23 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-111\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive editor with edit last and create if none",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
|
||||
EditSurvey: func(string) (string, error) { return "comment body", nil },
|
||||
},
|
||||
emptyComments: true,
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentCreate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/issues/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive inline",
|
||||
input: &shared.CommentableOptions{
|
||||
|
|
@ -319,14 +417,21 @@ func Test_commentRun(t *testing.T) {
|
|||
tt.input.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
|
||||
comments := api.Comments{Nodes: []api.Comment{
|
||||
{ID: "id1", Author: api.CommentAuthor{Login: "octocat"}, URL: "https://github.com/OWNER/REPO/issues/123#issuecomment-111", ViewerDidAuthor: true},
|
||||
{ID: "id2", Author: api.CommentAuthor{Login: "monalisa"}, URL: "https://github.com/OWNER/REPO/issues/123#issuecomment-222"},
|
||||
}}
|
||||
|
||||
if tt.emptyComments {
|
||||
comments.Nodes = []api.Comment{}
|
||||
}
|
||||
|
||||
tt.input.RetrieveCommentable = func() (shared.Commentable, ghrepo.Interface, error) {
|
||||
return &api.Issue{
|
||||
ID: "ISSUE-ID",
|
||||
URL: "https://github.com/OWNER/REPO/issues/123",
|
||||
Comments: api.Comments{Nodes: []api.Comment{
|
||||
{ID: "id1", Author: api.CommentAuthor{Login: "octocat"}, URL: "https://github.com/OWNER/REPO/issues/123#issuecomment-111", ViewerDidAuthor: true},
|
||||
{ID: "id2", Author: api.CommentAuthor{Login: "monalisa"}, URL: "https://github.com/OWNER/REPO/issues/123#issuecomment-222"},
|
||||
}},
|
||||
ID: "ISSUE-ID",
|
||||
URL: "https://github.com/OWNER/REPO/issues/123",
|
||||
Comments: comments,
|
||||
}, ghrepo.New("OWNER", "REPO"), nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
Use: "list",
|
||||
Short: "List issues in a repository",
|
||||
Long: heredoc.Doc(`
|
||||
List issues in a GitHub repository.
|
||||
List issues in a GitHub repository. By default, this only lists open issues.
|
||||
|
||||
The search query syntax is documented here:
|
||||
<https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests>
|
||||
|
|
@ -70,6 +70,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
$ gh issue list --assignee "@me"
|
||||
$ gh issue list --milestone "The big 1.0"
|
||||
$ gh issue list --search "error no:assignee sort:created-asc"
|
||||
$ gh issue list --state all
|
||||
`),
|
||||
Aliases: []string{"ls"},
|
||||
Args: cmdutil.NoArgsQuoteReminder,
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ func TestListRun(t *testing.T) {
|
|||
wantStdout: heredoc.Doc(`
|
||||
|
||||
Showing 2 of 2 labels in OWNER/REPO
|
||||
|
||||
|
||||
NAME DESCRIPTION COLOR
|
||||
bug This is a bug label #d73a4a
|
||||
docs This is a docs label #ffa8da
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
cliContext "github.com/cli/cli/v2/context"
|
||||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
|
|
@ -25,8 +27,12 @@ type CheckoutOptions struct {
|
|||
Remotes func() (cliContext.Remotes, error)
|
||||
Branch func() (string, error)
|
||||
|
||||
Finder shared.PRFinder
|
||||
Finder shared.PRFinder
|
||||
Prompter shared.Prompt
|
||||
Lister shared.PRLister
|
||||
|
||||
Interactive bool
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
SelectorArg string
|
||||
RecurseSubmodules bool
|
||||
Force bool
|
||||
|
|
@ -42,17 +48,33 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr
|
|||
Config: f.Config,
|
||||
Remotes: f.Remotes,
|
||||
Branch: f.Branch,
|
||||
Prompter: f.Prompter,
|
||||
BaseRepo: f.BaseRepo,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "checkout {<number> | <url> | <branch>}",
|
||||
Use: "checkout [<number> | <url> | <branch>]",
|
||||
Short: "Check out a pull request in git",
|
||||
Args: cmdutil.ExactArgs(1, "argument required"),
|
||||
Example: heredoc.Doc(`
|
||||
# Interactively select a PR from the 10 most recent to check out
|
||||
$ gh pr checkout
|
||||
|
||||
# Checkout a specific PR
|
||||
$ gh pr checkout 32
|
||||
$ gh pr checkout https://github.com/OWNER/REPO/pull/32
|
||||
$ gh pr checkout feature
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.Finder = shared.NewFinder(f)
|
||||
opts.Lister = shared.NewLister(f)
|
||||
|
||||
if len(args) > 0 {
|
||||
opts.SelectorArg = args[0]
|
||||
} else if !opts.IO.CanPrompt() {
|
||||
return cmdutil.FlagErrorf("pull request number, URL, or branch required when not running interactively")
|
||||
} else {
|
||||
opts.Interactive = true
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
|
|
@ -71,11 +93,12 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr
|
|||
}
|
||||
|
||||
func checkoutRun(opts *CheckoutOptions) error {
|
||||
findOptions := shared.FindOptions{
|
||||
Selector: opts.SelectorArg,
|
||||
Fields: []string{"number", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository", "maintainerCanModify"},
|
||||
baseRepo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pr, baseRepo, err := opts.Finder.Find(findOptions)
|
||||
|
||||
pr, err := resolvePR(baseRepo, opts.Prompter, opts.SelectorArg, opts.Interactive, opts.Finder, opts.Lister, opts.IO)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -149,7 +172,7 @@ func cmdsForExistingRemote(remote *cliContext.Remote, pr *api.PullRequest, opts
|
|||
refSpec += fmt.Sprintf(":refs/remotes/%s", remoteBranch)
|
||||
}
|
||||
|
||||
cmds = append(cmds, []string{"fetch", remote.Name, refSpec})
|
||||
cmds = append(cmds, []string{"fetch", remote.Name, refSpec, "--no-tags"})
|
||||
|
||||
localBranch := pr.HeadRefName
|
||||
if opts.BranchName != "" {
|
||||
|
|
@ -179,7 +202,7 @@ func cmdsForMissingRemote(pr *api.PullRequest, baseURLOrName, repoHost, defaultB
|
|||
ref := fmt.Sprintf("refs/pull/%d/head", pr.Number)
|
||||
|
||||
if opts.Detach {
|
||||
cmds = append(cmds, []string{"fetch", baseURLOrName, ref})
|
||||
cmds = append(cmds, []string{"fetch", baseURLOrName, ref, "--no-tags"})
|
||||
cmds = append(cmds, []string{"checkout", "--detach", "FETCH_HEAD"})
|
||||
return cmds
|
||||
}
|
||||
|
|
@ -195,7 +218,7 @@ func cmdsForMissingRemote(pr *api.PullRequest, baseURLOrName, repoHost, defaultB
|
|||
currentBranch, _ := opts.Branch()
|
||||
if localBranch == currentBranch {
|
||||
// PR head matches currently checked out branch
|
||||
cmds = append(cmds, []string{"fetch", baseURLOrName, ref})
|
||||
cmds = append(cmds, []string{"fetch", baseURLOrName, ref, "--no-tags"})
|
||||
if opts.Force {
|
||||
cmds = append(cmds, []string{"reset", "--hard", "FETCH_HEAD"})
|
||||
} else {
|
||||
|
|
@ -203,13 +226,12 @@ func cmdsForMissingRemote(pr *api.PullRequest, baseURLOrName, repoHost, defaultB
|
|||
cmds = append(cmds, []string{"merge", "--ff-only", "FETCH_HEAD"})
|
||||
}
|
||||
} else {
|
||||
// TODO: check if non-fast-forward and suggest to use `--force`
|
||||
fetchCmd := []string{"fetch", baseURLOrName, fmt.Sprintf("%s:%s", ref, localBranch), "--no-tags"}
|
||||
if opts.Force {
|
||||
cmds = append(cmds, []string{"fetch", baseURLOrName, fmt.Sprintf("%s:%s", ref, localBranch), "--force"})
|
||||
} else {
|
||||
// TODO: check if non-fast-forward and suggest to use `--force`
|
||||
cmds = append(cmds, []string{"fetch", baseURLOrName, fmt.Sprintf("%s:%s", ref, localBranch)})
|
||||
fetchCmd = append(fetchCmd, "--force")
|
||||
}
|
||||
|
||||
cmds = append(cmds, fetchCmd)
|
||||
cmds = append(cmds, []string{"checkout", localBranch})
|
||||
}
|
||||
|
||||
|
|
@ -263,3 +285,77 @@ func executeCmds(client *git.Client, credentialPattern git.CredentialPattern, cm
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolvePR(baseRepo ghrepo.Interface, prompter shared.Prompt, pullRequestSelector string, isInteractive bool, pullRequestFinder shared.PRFinder, prLister shared.PRLister, io *iostreams.IOStreams) (*api.PullRequest, error) {
|
||||
// When non-interactive
|
||||
if pullRequestSelector != "" {
|
||||
pr, _, err := pullRequestFinder.Find(shared.FindOptions{
|
||||
Selector: pullRequestSelector,
|
||||
Fields: []string{
|
||||
"number",
|
||||
"headRefName",
|
||||
"headRepository",
|
||||
"headRepositoryOwner",
|
||||
"isCrossRepository",
|
||||
"maintainerCanModify",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pr, nil
|
||||
}
|
||||
if !isInteractive {
|
||||
return nil, cmdutil.FlagErrorf("pull request number, URL, or branch required when not running interactively")
|
||||
}
|
||||
// When interactive
|
||||
io.StartProgressIndicator()
|
||||
listResult, err := prLister.List(shared.ListOptions{
|
||||
State: "open",
|
||||
Fields: []string{
|
||||
"number",
|
||||
"title",
|
||||
"state",
|
||||
"isDraft",
|
||||
|
||||
"headRefName",
|
||||
"headRepository",
|
||||
"headRepositoryOwner",
|
||||
"isCrossRepository",
|
||||
"maintainerCanModify",
|
||||
},
|
||||
LimitResults: 10})
|
||||
io.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(listResult.PullRequests) == 0 {
|
||||
return nil, shared.ListNoResults(ghrepo.FullName(baseRepo), "pull request", false)
|
||||
}
|
||||
|
||||
pr, err := promptForPR(prompter, *listResult)
|
||||
return pr, err
|
||||
}
|
||||
|
||||
func promptForPR(prompter shared.Prompt, jobs api.PullRequestAndTotalCount) (*api.PullRequest, error) {
|
||||
candidates := []string{}
|
||||
for _, pr := range jobs.PullRequests {
|
||||
candidates = append(candidates, fmt.Sprintf("%d\t%s %s [%s]",
|
||||
pr.Number,
|
||||
shared.PrStateWithDraft(&pr),
|
||||
text.RemoveExcessiveWhitespace(pr.Title),
|
||||
pr.HeadLabel(),
|
||||
))
|
||||
}
|
||||
|
||||
selected, err := prompter.Select("Select a pull request", "", candidates)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if selected >= 0 {
|
||||
return &jobs.PullRequests[selected], nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
|
|
@ -27,6 +28,10 @@ import (
|
|||
// repo: either "baseOwner/baseRepo" or "baseOwner/baseRepo:defaultBranch"
|
||||
// prHead: "headOwner/headRepo:headBranch"
|
||||
func stubPR(repo, prHead string) (ghrepo.Interface, *api.PullRequest) {
|
||||
return _stubPR(repo, prHead, 123, "PR title", "OPEN", false)
|
||||
}
|
||||
|
||||
func _stubPR(repo, prHead string, number int, title string, state string, isDraft bool) (ghrepo.Interface, *api.PullRequest) {
|
||||
defaultBranch := ""
|
||||
if idx := strings.IndexRune(repo, ':'); idx >= 0 {
|
||||
defaultBranch = repo[idx+1:]
|
||||
|
|
@ -52,25 +57,33 @@ func stubPR(repo, prHead string) (ghrepo.Interface, *api.PullRequest) {
|
|||
}
|
||||
|
||||
return baseRepo, &api.PullRequest{
|
||||
Number: 123,
|
||||
Number: number,
|
||||
HeadRefName: headRefName,
|
||||
HeadRepositoryOwner: api.Owner{Login: headRepo.RepoOwner()},
|
||||
HeadRepository: &api.PRRepository{Name: headRepo.RepoName()},
|
||||
IsCrossRepository: !ghrepo.IsSame(baseRepo, headRepo),
|
||||
MaintainerCanModify: false,
|
||||
|
||||
Title: title,
|
||||
State: state,
|
||||
IsDraft: isDraft,
|
||||
}
|
||||
}
|
||||
|
||||
func Test_checkoutRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *CheckoutOptions
|
||||
httpStubs func(*httpmock.Registry)
|
||||
runStubs func(*run.CommandStubber)
|
||||
name string
|
||||
opts *CheckoutOptions
|
||||
|
||||
httpStubs func(*httpmock.Registry)
|
||||
runStubs func(*run.CommandStubber)
|
||||
promptStubs func(*prompter.MockPrompter)
|
||||
|
||||
remotes map[string]string
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "checkout with ssh remote URL",
|
||||
|
|
@ -81,6 +94,10 @@ func Test_checkoutRun(t *testing.T) {
|
|||
finder := shared.NewMockFinder("123", pr, baseRepo)
|
||||
return finder
|
||||
}(),
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
baseRepo, _ := stubPR("OWNER/REPO:master", "OWNER/REPO:feature")
|
||||
return baseRepo, nil
|
||||
},
|
||||
Config: func() (gh.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
|
|
@ -93,7 +110,7 @@ func Test_checkoutRun(t *testing.T) {
|
|||
},
|
||||
runStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git show-ref --verify -- refs/heads/feature`, 1, "")
|
||||
cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "")
|
||||
cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature --no-tags`, 0, "")
|
||||
cs.Register(`git checkout -b feature --track origin/feature`, 0, "")
|
||||
},
|
||||
},
|
||||
|
|
@ -108,6 +125,10 @@ func Test_checkoutRun(t *testing.T) {
|
|||
finder := shared.NewMockFinder("123", pr, baseRepo)
|
||||
return finder
|
||||
}(),
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
baseRepo, _ := stubPR("OWNER/REPO:master", "OWNER/REPO:feature")
|
||||
return baseRepo, nil
|
||||
},
|
||||
Config: func() (gh.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
|
|
@ -119,7 +140,7 @@ func Test_checkoutRun(t *testing.T) {
|
|||
"origin": "OWNER/REPO",
|
||||
},
|
||||
runStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git fetch origin refs/pull/123/head:feature`, 0, "")
|
||||
cs.Register(`git fetch origin refs/pull/123/head:feature --no-tags`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.merge`, 1, "")
|
||||
cs.Register(`git checkout feature`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.remote origin`, 0, "")
|
||||
|
|
@ -137,6 +158,10 @@ func Test_checkoutRun(t *testing.T) {
|
|||
finder := shared.NewMockFinder("123", pr, baseRepo)
|
||||
return finder
|
||||
}(),
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
baseRepo, _ := stubPR("OWNER/REPO:master", "OWNER/REPO:feature")
|
||||
return baseRepo, nil
|
||||
},
|
||||
Config: func() (gh.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
|
|
@ -149,7 +174,7 @@ func Test_checkoutRun(t *testing.T) {
|
|||
},
|
||||
runStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git show-ref --verify -- refs/heads/foobar`, 1, "")
|
||||
cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "")
|
||||
cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature --no-tags`, 0, "")
|
||||
cs.Register(`git checkout -b foobar --track origin/feature`, 0, "")
|
||||
},
|
||||
},
|
||||
|
|
@ -164,6 +189,10 @@ func Test_checkoutRun(t *testing.T) {
|
|||
finder := shared.NewMockFinder("123", pr, baseRepo)
|
||||
return finder
|
||||
}(),
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
baseRepo, _ := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
|
||||
return baseRepo, nil
|
||||
},
|
||||
Config: func() (gh.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
|
|
@ -176,19 +205,94 @@ func Test_checkoutRun(t *testing.T) {
|
|||
},
|
||||
runStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git config branch\.foobar\.merge`, 1, "")
|
||||
cs.Register(`git fetch origin refs/pull/123/head:foobar`, 0, "")
|
||||
cs.Register(`git fetch origin refs/pull/123/head:foobar --no-tags`, 0, "")
|
||||
cs.Register(`git checkout foobar`, 0, "")
|
||||
cs.Register(`git config branch\.foobar\.remote https://github.com/hubot/REPO.git`, 0, "")
|
||||
cs.Register(`git config branch\.foobar\.pushRemote https://github.com/hubot/REPO.git`, 0, "")
|
||||
cs.Register(`git config branch\.foobar\.merge refs/heads/feature`, 0, "")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with no selected PR args and non tty, return error",
|
||||
opts: &CheckoutOptions{
|
||||
SelectorArg: "",
|
||||
Interactive: false,
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
},
|
||||
remotes: map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "pull request number, URL, or branch required when not running interactively",
|
||||
},
|
||||
{
|
||||
name: "with no selected PR args and stdin tty, prompts for choice",
|
||||
opts: &CheckoutOptions{
|
||||
SelectorArg: "",
|
||||
Interactive: true,
|
||||
Lister: func() shared.PRLister {
|
||||
_, pr1 := _stubPR("OWNER/REPO:master", "OWNER/REPO:feature", 32, "New feature", "OPEN", false)
|
||||
_, pr2 := _stubPR("OWNER/REPO:master", "OWNER/REPO:bug-fix", 29, "Fixed bad bug", "OPEN", false)
|
||||
_, pr3 := _stubPR("OWNER/REPO:master", "OWNER/REPO:docs", 28, "Improve documentation", "OPEN", true)
|
||||
lister := shared.NewMockLister(&api.PullRequestAndTotalCount{
|
||||
TotalCount: 3,
|
||||
PullRequests: []api.PullRequest{
|
||||
*pr1, *pr2, *pr3,
|
||||
}, SearchCapped: false}, nil)
|
||||
lister.ExpectFields([]string{"number", "title", "state", "isDraft", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository", "maintainerCanModify"})
|
||||
return lister
|
||||
}(),
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
Config: func() (gh.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
},
|
||||
promptStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterSelect("Select a pull request",
|
||||
[]string{"32\tOPEN New feature [feature]", "29\tOPEN Fixed bad bug [bug-fix]", "28\tDRAFT Improve documentation [docs]"},
|
||||
func(_, _ string, opts []string) (int, error) {
|
||||
return prompter.IndexFor(opts, "32\tOPEN New feature [feature]")
|
||||
})
|
||||
},
|
||||
runStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git show-ref --verify -- refs/heads/feature`, 1, "")
|
||||
cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature --no-tags`, 0, "")
|
||||
cs.Register(`git checkout -b feature --track origin/feature`, 0, "")
|
||||
},
|
||||
remotes: map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with no select PR args and no open PR, return error",
|
||||
opts: &CheckoutOptions{
|
||||
SelectorArg: "",
|
||||
Interactive: true,
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
Lister: shared.NewMockLister(&api.PullRequestAndTotalCount{
|
||||
TotalCount: 0,
|
||||
PullRequests: []api.PullRequest{},
|
||||
}, nil),
|
||||
},
|
||||
remotes: map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "no open pull requests in OWNER/REPO",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
opts := tt.opts
|
||||
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
|
||||
opts.IO = ios
|
||||
httpReg := &httpmock.Registry{}
|
||||
defer httpReg.Verify(t)
|
||||
|
|
@ -205,6 +309,12 @@ func Test_checkoutRun(t *testing.T) {
|
|||
tt.runStubs(cmdStubs)
|
||||
}
|
||||
|
||||
pm := prompter.NewMockPrompter(t)
|
||||
tt.opts.Prompter = pm
|
||||
if tt.promptStubs != nil {
|
||||
tt.promptStubs(pm)
|
||||
}
|
||||
|
||||
opts.Remotes = func() (context.Remotes, error) {
|
||||
if len(tt.remotes) == 0 {
|
||||
return nil, errors.New("no remotes")
|
||||
|
|
@ -232,6 +342,9 @@ func Test_checkoutRun(t *testing.T) {
|
|||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("want error: %v, got: %v", tt.wantErr, err)
|
||||
}
|
||||
if err != nil {
|
||||
assert.Equal(t, tt.errMsg, err.Error())
|
||||
}
|
||||
assert.Equal(t, tt.wantStdout, stdout.String())
|
||||
assert.Equal(t, tt.wantStderr, stderr.String())
|
||||
})
|
||||
|
|
@ -240,7 +353,7 @@ func Test_checkoutRun(t *testing.T) {
|
|||
|
||||
/** LEGACY TESTS **/
|
||||
|
||||
func runCommand(rt http.RoundTripper, remotes context.Remotes, branch string, cli string) (*test.CmdOut, error) {
|
||||
func runCommand(rt http.RoundTripper, remotes context.Remotes, branch string, cli string, baseRepo ghrepo.Interface) (*test.CmdOut, error) {
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
|
||||
factory := &cmdutil.Factory{
|
||||
|
|
@ -269,6 +382,9 @@ func runCommand(rt http.RoundTripper, remotes context.Remotes, branch string, cl
|
|||
GhPath: "some/path/gh",
|
||||
GitPath: "some/path/git",
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return baseRepo, nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd := NewCmdCheckout(factory, nil)
|
||||
|
|
@ -301,11 +417,11 @@ func TestPRCheckout_sameRepo(t *testing.T) {
|
|||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "")
|
||||
cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature --no-tags`, 0, "")
|
||||
cs.Register(`git show-ref --verify -- refs/heads/feature`, 1, "")
|
||||
cs.Register(`git checkout -b feature --track origin/feature`, 0, "")
|
||||
|
||||
output, err := runCommand(http, nil, "master", `123`)
|
||||
output, err := runCommand(http, nil, "master", `123`, baseRepo)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
|
|
@ -320,12 +436,12 @@ func TestPRCheckout_existingBranch(t *testing.T) {
|
|||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "")
|
||||
cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature --no-tags`, 0, "")
|
||||
cs.Register(`git show-ref --verify -- refs/heads/feature`, 0, "")
|
||||
cs.Register(`git checkout feature`, 0, "")
|
||||
cs.Register(`git merge --ff-only refs/remotes/origin/feature`, 0, "")
|
||||
|
||||
output, err := runCommand(http, nil, "master", `123`)
|
||||
output, err := runCommand(http, nil, "master", `123`, baseRepo)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
|
|
@ -352,11 +468,11 @@ func TestPRCheckout_differentRepo_remoteExists(t *testing.T) {
|
|||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
cs.Register(`git fetch robot-fork \+refs/heads/feature:refs/remotes/robot-fork/feature`, 0, "")
|
||||
cs.Register(`git fetch robot-fork \+refs/heads/feature:refs/remotes/robot-fork/feature --no-tags`, 0, "")
|
||||
cs.Register(`git show-ref --verify -- refs/heads/feature`, 1, "")
|
||||
cs.Register(`git checkout -b feature --track robot-fork/feature`, 0, "")
|
||||
|
||||
output, err := runCommand(http, remotes, "master", `123`)
|
||||
output, err := runCommand(http, remotes, "master", `123`, baseRepo)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
|
|
@ -372,14 +488,37 @@ func TestPRCheckout_differentRepo(t *testing.T) {
|
|||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
cs.Register(`git fetch origin refs/pull/123/head:feature`, 0, "")
|
||||
cs.Register(`git fetch origin refs/pull/123/head:feature --no-tags`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.merge`, 1, "")
|
||||
cs.Register(`git checkout feature`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.remote origin`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.pushRemote origin`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.merge refs/pull/123/head`, 0, "")
|
||||
|
||||
output, err := runCommand(http, nil, "master", `123`)
|
||||
output, err := runCommand(http, nil, "master", `123`, baseRepo)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepoForce(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
|
||||
finder := shared.RunCommandFinder("123", pr, baseRepo)
|
||||
finder.ExpectFields([]string{"number", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository", "maintainerCanModify"})
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
cs.Register(`git fetch origin refs/pull/123/head:feature --no-tags --force`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.merge`, 1, "")
|
||||
cs.Register(`git checkout feature`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.remote origin`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.pushRemote origin`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.merge refs/pull/123/head`, 0, "")
|
||||
|
||||
output, err := runCommand(http, nil, "master", `123 --force`, baseRepo)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
|
|
@ -394,11 +533,11 @@ func TestPRCheckout_differentRepo_existingBranch(t *testing.T) {
|
|||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
cs.Register(`git fetch origin refs/pull/123/head:feature`, 0, "")
|
||||
cs.Register(`git fetch origin refs/pull/123/head:feature --no-tags`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.merge`, 0, "refs/heads/feature\n")
|
||||
cs.Register(`git checkout feature`, 0, "")
|
||||
|
||||
output, err := runCommand(http, nil, "master", `123`)
|
||||
output, err := runCommand(http, nil, "master", `123`, baseRepo)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
|
|
@ -413,11 +552,11 @@ func TestPRCheckout_detachedHead(t *testing.T) {
|
|||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
cs.Register(`git fetch origin refs/pull/123/head:feature`, 0, "")
|
||||
cs.Register(`git fetch origin refs/pull/123/head:feature --no-tags`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.merge`, 0, "refs/heads/feature\n")
|
||||
cs.Register(`git checkout feature`, 0, "")
|
||||
|
||||
output, err := runCommand(http, nil, "", `123`)
|
||||
output, err := runCommand(http, nil, "", `123`, baseRepo)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
|
|
@ -432,11 +571,11 @@ func TestPRCheckout_differentRepo_currentBranch(t *testing.T) {
|
|||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
cs.Register(`git fetch origin refs/pull/123/head`, 0, "")
|
||||
cs.Register(`git fetch origin refs/pull/123/head --no-tags`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.merge`, 0, "refs/heads/feature\n")
|
||||
cs.Register(`git merge --ff-only FETCH_HEAD`, 0, "")
|
||||
|
||||
output, err := runCommand(http, nil, "feature", `123`)
|
||||
output, err := runCommand(http, nil, "feature", `123`, baseRepo)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
|
|
@ -451,7 +590,9 @@ func TestPRCheckout_differentRepo_invalidBranchName(t *testing.T) {
|
|||
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
output, err := runCommand(http, nil, "master", `123`)
|
||||
|
||||
output, err := runCommand(http, nil, "master", `123`, baseRepo)
|
||||
|
||||
assert.EqualError(t, err, `invalid branch name: "-foo"`)
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
|
|
@ -467,14 +608,14 @@ func TestPRCheckout_maintainerCanModify(t *testing.T) {
|
|||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
cs.Register(`git fetch origin refs/pull/123/head:feature`, 0, "")
|
||||
cs.Register(`git fetch origin refs/pull/123/head:feature --no-tags`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.merge`, 1, "")
|
||||
cs.Register(`git checkout feature`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.remote https://github\.com/hubot/REPO\.git`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.pushRemote https://github\.com/hubot/REPO\.git`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.merge refs/heads/feature`, 0, "")
|
||||
|
||||
output, err := runCommand(http, nil, "master", `123`)
|
||||
output, err := runCommand(http, nil, "master", `123`, baseRepo)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
|
|
@ -488,14 +629,14 @@ func TestPRCheckout_recurseSubmodules(t *testing.T) {
|
|||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "")
|
||||
cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature --no-tags`, 0, "")
|
||||
cs.Register(`git show-ref --verify -- refs/heads/feature`, 0, "")
|
||||
cs.Register(`git checkout feature`, 0, "")
|
||||
cs.Register(`git merge --ff-only refs/remotes/origin/feature`, 0, "")
|
||||
cs.Register(`git submodule sync --recursive`, 0, "")
|
||||
cs.Register(`git submodule update --init --recursive`, 0, "")
|
||||
|
||||
output, err := runCommand(http, nil, "master", `123 --recurse-submodules`)
|
||||
output, err := runCommand(http, nil, "master", `123 --recurse-submodules`, baseRepo)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
|
|
@ -509,12 +650,12 @@ func TestPRCheckout_force(t *testing.T) {
|
|||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "")
|
||||
cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature --no-tags`, 0, "")
|
||||
cs.Register(`git show-ref --verify -- refs/heads/feature`, 0, "")
|
||||
cs.Register(`git checkout feature`, 0, "")
|
||||
cs.Register(`git reset --hard refs/remotes/origin/feature`, 0, "")
|
||||
|
||||
output, err := runCommand(http, nil, "master", `123 --force`)
|
||||
output, err := runCommand(http, nil, "master", `123 --force`, baseRepo)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
|
|
@ -531,9 +672,9 @@ func TestPRCheckout_detach(t *testing.T) {
|
|||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
cs.Register(`git checkout --detach FETCH_HEAD`, 0, "")
|
||||
cs.Register(`git fetch origin refs/pull/123/head`, 0, "")
|
||||
cs.Register(`git fetch origin refs/pull/123/head --no-tags`, 0, "")
|
||||
|
||||
output, err := runCommand(http, nil, "", `123 --detach`)
|
||||
output, err := runCommand(http, nil, "", `123 --detach`, baseRepo)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ func closeRun(opts *CloseOptions) error {
|
|||
branchSwitchString = fmt.Sprintf(" and switched to branch %s", cs.Cyan(branchToSwitchTo))
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Skipped deleting the local branch since current directory is not a git repository \n", cs.WarningIcon())
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Skipped deleting the local branch since current directory is not a git repository\n", cs.WarningIcon())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -271,7 +271,7 @@ func TestPrClose_deleteBranch_notInGitRepo(t *testing.T) {
|
|||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
✓ Closed pull request OWNER/REPO#96 (The title of the PR)
|
||||
! Skipped deleting the local branch since current directory is not a git repository
|
||||
! Skipped deleting the local branch since current directory is not a git repository
|
||||
✓ Deleted branch trunk
|
||||
`), output.Stderr())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,13 @@ import (
|
|||
|
||||
func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) error) *cobra.Command {
|
||||
opts := &shared.CommentableOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
EditSurvey: shared.CommentableEditSurvey(f.Config, f.IOStreams),
|
||||
InteractiveEditSurvey: shared.CommentableInteractiveEditSurvey(f.Config, f.IOStreams),
|
||||
ConfirmSubmitSurvey: shared.CommentableConfirmSubmitSurvey(f.Prompter),
|
||||
OpenInBrowser: f.Browser.Browse,
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
EditSurvey: shared.CommentableEditSurvey(f.Config, f.IOStreams),
|
||||
InteractiveEditSurvey: shared.CommentableInteractiveEditSurvey(f.Config, f.IOStreams),
|
||||
ConfirmSubmitSurvey: shared.CommentableConfirmSubmitSurvey(f.Prompter),
|
||||
ConfirmCreateIfNoneSurvey: shared.CommentableInteractiveCreateIfNoneSurvey(f.Prompter),
|
||||
OpenInBrowser: f.Browser.Browse,
|
||||
}
|
||||
|
||||
var bodyFile string
|
||||
|
|
@ -75,6 +76,7 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) err
|
|||
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.CreateIfNone, "create-if-none", false, "Create a new comment if no comments are found. Can be used only with --edit-last")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,6 +129,29 @@ func TestNewCmdComment(t *testing.T) {
|
|||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "edit last flag",
|
||||
input: "1 --edit-last",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "edit last flag with create if none",
|
||||
input: "1 --edit-last --create-if-none",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "body and body-file flags",
|
||||
input: "1 --body 'test' --body-file 'test-file.txt'",
|
||||
|
|
@ -159,6 +182,12 @@ func TestNewCmdComment(t *testing.T) {
|
|||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "create-if-none flag without edit-last",
|
||||
input: "1 --create-if-none",
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -208,11 +237,12 @@ func TestNewCmdComment(t *testing.T) {
|
|||
|
||||
func Test_commentRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input *shared.CommentableOptions
|
||||
httpStubs func(*testing.T, *httpmock.Registry)
|
||||
stdout string
|
||||
stderr string
|
||||
name string
|
||||
input *shared.CommentableOptions
|
||||
emptyComments bool
|
||||
httpStubs func(*testing.T, *httpmock.Registry)
|
||||
stdout string
|
||||
stderr string
|
||||
}{
|
||||
{
|
||||
name: "interactive editor",
|
||||
|
|
@ -245,6 +275,24 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-111\n",
|
||||
},
|
||||
{
|
||||
name: "interactive editor with edit last and create if none",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
|
||||
InteractiveEditSurvey: func(string) (string, error) { return "comment body", nil },
|
||||
ConfirmCreateIfNoneSurvey: func() (bool, error) { return true, nil },
|
||||
ConfirmSubmitSurvey: func() (bool, error) { return true, nil },
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentUpdate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-111\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive web",
|
||||
input: &shared.CommentableOptions{
|
||||
|
|
@ -264,7 +312,43 @@ func Test_commentRun(t *testing.T) {
|
|||
Body: "",
|
||||
EditLast: true,
|
||||
|
||||
OpenInBrowser: func(string) error { return nil },
|
||||
OpenInBrowser: func(u string) error {
|
||||
assert.Contains(t, u, "#issuecomment-111")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
stderr: "Opening https://github.com/OWNER/REPO/pull/123 in your browser.\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive web with edit last and create if none for empty comments",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
|
||||
OpenInBrowser: func(u string) error {
|
||||
assert.Contains(t, u, "#issuecomment-new")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
emptyComments: true,
|
||||
stderr: "Opening https://github.com/OWNER/REPO/pull/123 in your browser.\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive web with edit last and create if none",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
|
||||
OpenInBrowser: func(u string) error {
|
||||
assert.Contains(t, u, "#issuecomment-111")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
stderr: "Opening https://github.com/OWNER/REPO/pull/123 in your browser.\n",
|
||||
},
|
||||
|
|
@ -297,6 +381,23 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-111\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive editor with edit last and create if none",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
EditLast: true,
|
||||
CreateIfNone: true,
|
||||
|
||||
EditSurvey: func(string) (string, error) { return "comment body", nil },
|
||||
},
|
||||
emptyComments: true,
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockCommentCreate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive inline",
|
||||
input: &shared.CommentableOptions{
|
||||
|
|
@ -339,14 +440,20 @@ func Test_commentRun(t *testing.T) {
|
|||
|
||||
tt.input.IO = ios
|
||||
tt.input.HttpClient = httpClient
|
||||
|
||||
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},
|
||||
{ID: "id2", Author: api.CommentAuthor{Login: "monalisa"}, URL: "https://github.com/OWNER/REPO/pull/123#issuecomment-222"},
|
||||
}}
|
||||
if tt.emptyComments {
|
||||
comments.Nodes = []api.Comment{}
|
||||
}
|
||||
|
||||
tt.input.RetrieveCommentable = func() (shared.Commentable, ghrepo.Interface, error) {
|
||||
return &api.PullRequest{
|
||||
Number: 123,
|
||||
URL: "https://github.com/OWNER/REPO/pull/123",
|
||||
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},
|
||||
{ID: "id2", Author: api.CommentAuthor{Login: "monalisa"}, URL: "https://github.com/OWNER/REPO/pull/123#issuecomment-222"},
|
||||
}},
|
||||
Number: 123,
|
||||
URL: "https://github.com/OWNER/REPO/pull/123",
|
||||
Comments: comments,
|
||||
}, ghrepo.New("OWNER", "REPO"), nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -518,6 +518,7 @@ func initDefaultTitleBody(ctx CreateContext, state *shared.IssueMetadataState, u
|
|||
return nil
|
||||
}
|
||||
|
||||
// TODO: Replace with the finder's PullRequestRefs struct
|
||||
// trackingRef represents a ref for a remote tracking branch.
|
||||
type trackingRef struct {
|
||||
remoteName string
|
||||
|
|
@ -685,7 +686,9 @@ func NewCreateContext(opts *CreateOptions) (*CreateContext, error) {
|
|||
return nil, err
|
||||
}
|
||||
if isPushEnabled {
|
||||
// determine whether the head branch is already pushed to a remote
|
||||
// TODO: This doesn't respect the @{push} revision resolution or triagular workflows assembled with
|
||||
// remote.pushDefault, or branch.<branchName>.pushremote config settings. The finder's ParsePRRefs
|
||||
// may be able to replace this function entirely.
|
||||
if trackingRef, found := tryDetermineTrackingRef(gitClient, remotes, headBranch, headBranchConfig); found {
|
||||
isPushEnabled = false
|
||||
if r, err := remotes.FindByName(trackingRef.remoteName); err == nil {
|
||||
|
|
@ -932,6 +935,7 @@ func handlePush(opts CreateOptions, ctx CreateContext) error {
|
|||
headRepo := ctx.HeadRepo
|
||||
headRemote := ctx.HeadRemote
|
||||
client := ctx.Client
|
||||
gitClient := ctx.GitClient
|
||||
|
||||
var err error
|
||||
// if a head repository could not be determined so far, automatically create
|
||||
|
|
@ -973,7 +977,8 @@ func handlePush(opts CreateOptions, ctx CreateContext) error {
|
|||
headRepoURL := ghrepo.FormatRemoteURL(headRepo, cloneProtocol)
|
||||
gitClient := ctx.GitClient
|
||||
origin, _ := remotes.FindByName("origin")
|
||||
upstream, _ := remotes.FindByName("upstream")
|
||||
upstreamName := "upstream"
|
||||
upstream, _ := remotes.FindByName(upstreamName)
|
||||
remoteName := "origin"
|
||||
|
||||
if origin != nil {
|
||||
|
|
@ -981,7 +986,7 @@ func handlePush(opts CreateOptions, ctx CreateContext) error {
|
|||
}
|
||||
|
||||
if origin != nil && upstream == nil && ghrepo.IsSame(origin, ctx.BaseRepo) {
|
||||
renameCmd, err := gitClient.Command(context.Background(), "remote", "rename", "origin", "upstream")
|
||||
renameCmd, err := gitClient.Command(context.Background(), "remote", "rename", "origin", upstreamName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -989,7 +994,7 @@ func handlePush(opts CreateOptions, ctx CreateContext) error {
|
|||
return fmt.Errorf("error renaming origin remote: %w", err)
|
||||
}
|
||||
remoteName = "origin"
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Changed %s remote to %q\n", ghrepo.FullName(ctx.BaseRepo), "upstream")
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Changed %s remote to %q\n", ghrepo.FullName(ctx.BaseRepo), upstreamName)
|
||||
}
|
||||
|
||||
gitRemote, err := gitClient.AddRemote(context.Background(), remoteName, headRepoURL, []string{})
|
||||
|
|
@ -999,6 +1004,19 @@ func handlePush(opts CreateOptions, ctx CreateContext) error {
|
|||
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Added %s as remote %q\n", ghrepo.FullName(headRepo), remoteName)
|
||||
|
||||
// Only mark `upstream` remote as default if `gh pr create` created the remote.
|
||||
if didForkRepo {
|
||||
err := gitClient.SetRemoteResolution(context.Background(), upstreamName, "base")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting upstream as default: %w", err)
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
cs := opts.IO.ColorScheme()
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Repository %s set as the default repository. To learn more about the default repository, run: gh repo set-default --help\n", cs.WarningIcon(), cs.Bold(ghrepo.FullName(headRepo)))
|
||||
}
|
||||
}
|
||||
|
||||
headRemote = &ghContext.Remote{
|
||||
Remote: gitRemote,
|
||||
Repo: headRepo,
|
||||
|
|
@ -1010,7 +1028,6 @@ func handlePush(opts CreateOptions, ctx CreateContext) error {
|
|||
pushBranch := func() error {
|
||||
w := NewRegexpWriter(opts.IO.ErrOut, gitPushRegexp, "")
|
||||
defer w.Flush()
|
||||
gitClient := ctx.GitClient
|
||||
ref := fmt.Sprintf("HEAD:refs/heads/%s", ctx.HeadBranch)
|
||||
bo := backoff.NewConstantBackOff(2 * time.Second)
|
||||
ctx := context.Background()
|
||||
|
|
|
|||
|
|
@ -798,6 +798,7 @@ func Test_createRun(t *testing.T) {
|
|||
cs.Register("git remote rename origin upstream", 0, "")
|
||||
cs.Register(`git remote add origin https://github.com/monalisa/REPO.git`, 0, "")
|
||||
cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "")
|
||||
cs.Register(`git config --add remote.upstream.gh-resolved base`, 0, "")
|
||||
},
|
||||
promptStubs: func(pm *prompter.PrompterMock) {
|
||||
pm.SelectFunc = func(p, _ string, opts []string) (int, error) {
|
||||
|
|
@ -809,7 +810,7 @@ func Test_createRun(t *testing.T) {
|
|||
}
|
||||
},
|
||||
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
||||
expectedErrOut: "\nCreating pull request for monalisa:feature into master in OWNER/REPO\n\nChanged OWNER/REPO remote to \"upstream\"\nAdded monalisa/REPO as remote \"origin\"\n",
|
||||
expectedErrOut: "\nCreating pull request for monalisa:feature into master in OWNER/REPO\n\nChanged OWNER/REPO remote to \"upstream\"\nAdded monalisa/REPO as remote \"origin\"\n! Repository monalisa/REPO set as the default repository. To learn more about the default repository, run: gh repo set-default --help\n",
|
||||
},
|
||||
{
|
||||
name: "pushed to non base repo",
|
||||
|
|
@ -1515,7 +1516,7 @@ func Test_createRun(t *testing.T) {
|
|||
},
|
||||
customBranchConfig: true,
|
||||
cmdStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git config --get-regexp \^branch\\\.task1\\\.\(remote\|merge\|gh-merge-base\)\$`, 0, heredoc.Doc(`
|
||||
cs.Register(`git config --get-regexp \^branch\\\.task1\\\.\(remote\|merge\|pushremote\|gh-merge-base\)\$`, 0, heredoc.Doc(`
|
||||
branch.task1.remote origin
|
||||
branch.task1.merge refs/heads/task1
|
||||
branch.task1.gh-merge-base feature/feat2`)) // ReadBranchConfig
|
||||
|
|
@ -1549,7 +1550,7 @@ func Test_createRun(t *testing.T) {
|
|||
defer cmdTeardown(t)
|
||||
cs.Register(`git status --porcelain`, 0, "")
|
||||
if !tt.customBranchConfig {
|
||||
cs.Register(`git config --get-regexp \^branch\\\..+\\\.\(remote\|merge\|gh-merge-base\)\$`, 0, "")
|
||||
cs.Register(`git config --get-regexp \^branch\\\..+\\\.\(remote\|merge\|pushremote\|gh-merge-base\)\$`, 0, "")
|
||||
}
|
||||
|
||||
if tt.cmdStubs != nil {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
)
|
||||
|
||||
func shouldUseSearch(filters prShared.FilterOptions) bool {
|
||||
|
|
@ -18,113 +19,16 @@ func listPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters pr
|
|||
return searchPullRequests(httpClient, repo, filters, limit)
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Repository struct {
|
||||
PullRequests struct {
|
||||
Nodes []api.PullRequest
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
TotalCount int
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment := fmt.Sprintf("fragment pr on PullRequest{%s}", api.PullRequestGraphQL(filters.Fields))
|
||||
query := fragment + `
|
||||
query PullRequestList(
|
||||
$owner: String!,
|
||||
$repo: String!,
|
||||
$limit: Int!,
|
||||
$endCursor: String,
|
||||
$baseBranch: String,
|
||||
$headBranch: String,
|
||||
$state: [PullRequestState!] = OPEN
|
||||
) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequests(
|
||||
states: $state,
|
||||
baseRefName: $baseBranch,
|
||||
headRefName: $headBranch,
|
||||
first: $limit,
|
||||
after: $endCursor,
|
||||
orderBy: {field: CREATED_AT, direction: DESC}
|
||||
) {
|
||||
totalCount
|
||||
nodes {
|
||||
...pr
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
pageLimit := min(limit, 100)
|
||||
variables := map[string]interface{}{
|
||||
"owner": repo.RepoOwner(),
|
||||
"repo": repo.RepoName(),
|
||||
}
|
||||
|
||||
switch filters.State {
|
||||
case "open":
|
||||
variables["state"] = []string{"OPEN"}
|
||||
case "closed":
|
||||
variables["state"] = []string{"CLOSED", "MERGED"}
|
||||
case "merged":
|
||||
variables["state"] = []string{"MERGED"}
|
||||
case "all":
|
||||
variables["state"] = []string{"OPEN", "CLOSED", "MERGED"}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid state: %s", filters.State)
|
||||
}
|
||||
|
||||
if filters.BaseBranch != "" {
|
||||
variables["baseBranch"] = filters.BaseBranch
|
||||
}
|
||||
if filters.HeadBranch != "" {
|
||||
variables["headBranch"] = filters.HeadBranch
|
||||
}
|
||||
|
||||
res := api.PullRequestAndTotalCount{}
|
||||
var check = make(map[int]struct{})
|
||||
client := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
loop:
|
||||
for {
|
||||
variables["limit"] = pageLimit
|
||||
var data response
|
||||
err := client.GraphQL(repo.RepoHost(), query, variables, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prData := data.Repository.PullRequests
|
||||
res.TotalCount = prData.TotalCount
|
||||
|
||||
for _, pr := range prData.Nodes {
|
||||
if _, exists := check[pr.Number]; exists && pr.Number > 0 {
|
||||
continue
|
||||
}
|
||||
check[pr.Number] = struct{}{}
|
||||
|
||||
res.PullRequests = append(res.PullRequests, pr)
|
||||
if len(res.PullRequests) == limit {
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
if prData.PageInfo.HasNextPage {
|
||||
variables["endCursor"] = prData.PageInfo.EndCursor
|
||||
pageLimit = min(pageLimit, limit-len(res.PullRequests))
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
return prShared.NewLister(&cmdutil.Factory{
|
||||
HttpClient: func() (*http.Client, error) { return httpClient, nil },
|
||||
BaseRepo: func() (ghrepo.Interface, error) { return repo, nil },
|
||||
}).List(prShared.ListOptions{
|
||||
LimitResults: limit,
|
||||
State: filters.State,
|
||||
BaseBranch: filters.BaseBranch,
|
||||
HeadBranch: filters.HeadBranch,
|
||||
Fields: filters.Fields,
|
||||
})
|
||||
}
|
||||
|
||||
func searchPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.PullRequestAndTotalCount, error) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
)
|
||||
|
||||
func Test_listPullRequests(t *testing.T) {
|
||||
func Test_ListPullRequests(t *testing.T) {
|
||||
type args struct {
|
||||
repo ghrepo.Interface
|
||||
filters prShared.FilterOptions
|
||||
|
|
@ -155,7 +155,7 @@ func Test_listPullRequests(t *testing.T) {
|
|||
|
||||
_, err := listPullRequests(httpClient, tt.args.repo, tt.args.filters, tt.args.limit)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("listPullRequests() error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Errorf("ListPullRequests() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
Use: "list",
|
||||
Short: "List pull requests in a repository",
|
||||
Long: heredoc.Doc(`
|
||||
List pull requests in a GitHub repository.
|
||||
List pull requests in a GitHub repository. By default, this only lists open PRs.
|
||||
|
||||
The search query syntax is documented here:
|
||||
<https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests>
|
||||
|
|
@ -222,7 +222,7 @@ func listRun(opts *ListOptions) error {
|
|||
table.AddField(text.RemoveExcessiveWhitespace(pr.Title))
|
||||
table.AddField(pr.HeadLabel(), tableprinter.WithColor(cs.Cyan))
|
||||
if !isTTY {
|
||||
table.AddField(prStateWithDraft(&pr))
|
||||
table.AddField(shared.PrStateWithDraft(&pr))
|
||||
}
|
||||
table.AddTimeField(opts.Now(), pr.CreatedAt, cs.Gray)
|
||||
table.EndRow()
|
||||
|
|
@ -234,11 +234,3 @@ func listRun(opts *ListOptions) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func prStateWithDraft(pr *api.PullRequest) string {
|
||||
if pr.IsDraft && pr.State == "OPEN" {
|
||||
return "DRAFT"
|
||||
}
|
||||
|
||||
return pr.State
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var errNoUserComments = errors.New("no comments found for current user")
|
||||
|
||||
type InputType int
|
||||
|
||||
const (
|
||||
|
|
@ -32,19 +34,21 @@ type Commentable interface {
|
|||
}
|
||||
|
||||
type CommentableOptions struct {
|
||||
IO *iostreams.IOStreams
|
||||
HttpClient func() (*http.Client, error)
|
||||
RetrieveCommentable func() (Commentable, ghrepo.Interface, error)
|
||||
EditSurvey func(string) (string, error)
|
||||
InteractiveEditSurvey func(string) (string, error)
|
||||
ConfirmSubmitSurvey func() (bool, error)
|
||||
OpenInBrowser func(string) error
|
||||
Interactive bool
|
||||
InputType InputType
|
||||
Body string
|
||||
EditLast bool
|
||||
Quiet bool
|
||||
Host string
|
||||
IO *iostreams.IOStreams
|
||||
HttpClient func() (*http.Client, error)
|
||||
RetrieveCommentable func() (Commentable, ghrepo.Interface, error)
|
||||
EditSurvey func(string) (string, error)
|
||||
InteractiveEditSurvey func(string) (string, error)
|
||||
ConfirmSubmitSurvey func() (bool, error)
|
||||
ConfirmCreateIfNoneSurvey func() (bool, error)
|
||||
OpenInBrowser func(string) error
|
||||
Interactive bool
|
||||
InputType InputType
|
||||
Body string
|
||||
EditLast bool
|
||||
CreateIfNone bool
|
||||
Quiet bool
|
||||
Host string
|
||||
}
|
||||
|
||||
func CommentablePreRun(cmd *cobra.Command, opts *CommentableOptions) error {
|
||||
|
|
@ -66,6 +70,10 @@ func CommentablePreRun(cmd *cobra.Command, opts *CommentableOptions) error {
|
|||
inputFlags++
|
||||
}
|
||||
|
||||
if opts.CreateIfNone && !opts.EditLast {
|
||||
return cmdutil.FlagErrorf("`--create-if-none` can only be used with `--edit-last`")
|
||||
}
|
||||
|
||||
if inputFlags == 0 {
|
||||
if !opts.IO.CanPrompt() {
|
||||
return cmdutil.FlagErrorf("flags required when not running interactively")
|
||||
|
|
@ -85,7 +93,24 @@ func CommentableRun(opts *CommentableOptions) error {
|
|||
}
|
||||
opts.Host = repo.RepoHost()
|
||||
if opts.EditLast {
|
||||
return updateComment(commentable, opts)
|
||||
err := updateComment(commentable, opts)
|
||||
if !errors.Is(err, errNoUserComments) {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.Interactive {
|
||||
if opts.CreateIfNone {
|
||||
fmt.Fprintln(opts.IO.ErrOut, "No comments found. Creating a new comment.")
|
||||
} else {
|
||||
ok, err := opts.ConfirmCreateIfNoneSurvey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return errNoUserComments
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return createComment(commentable, opts)
|
||||
}
|
||||
|
|
@ -144,7 +169,7 @@ func createComment(commentable Commentable, opts *CommentableOptions) error {
|
|||
func updateComment(commentable Commentable, opts *CommentableOptions) error {
|
||||
comments := commentable.CurrentUserComments()
|
||||
if len(comments) == 0 {
|
||||
return fmt.Errorf("no comments found for current user")
|
||||
return errNoUserComments
|
||||
}
|
||||
|
||||
lastComment := &comments[len(comments)-1]
|
||||
|
|
@ -219,6 +244,12 @@ func CommentableInteractiveEditSurvey(cf func() (gh.Config, error), io *iostream
|
|||
}
|
||||
}
|
||||
|
||||
func CommentableInteractiveCreateIfNoneSurvey(p Prompt) func() (bool, error) {
|
||||
return func() (bool, error) {
|
||||
return p.Confirm("No comments found. Create one?", true)
|
||||
}
|
||||
}
|
||||
|
||||
func CommentableEditSurvey(cf func() (gh.Config, error), io *iostreams.IOStreams) func(string) (string, error) {
|
||||
return func(initialValue string) (string, error) {
|
||||
editorCommand, err := cmdutil.DetermineEditor(cf)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,14 @@ func StateTitleWithColor(cs *iostreams.ColorScheme, pr api.PullRequest) string {
|
|||
return prStateColorFunc(text.Title(pr.State))
|
||||
}
|
||||
|
||||
func PrStateWithDraft(pr *api.PullRequest) string {
|
||||
if pr.IsDraft && pr.State == "OPEN" {
|
||||
return "DRAFT"
|
||||
}
|
||||
|
||||
return pr.State
|
||||
}
|
||||
|
||||
func ColorForPRState(pr api.PullRequest) string {
|
||||
switch pr.State {
|
||||
case "OPEN":
|
||||
|
|
|
|||
|
|
@ -33,16 +33,19 @@ type progressIndicator interface {
|
|||
}
|
||||
|
||||
type finder struct {
|
||||
baseRepoFn func() (ghrepo.Interface, error)
|
||||
branchFn func() (string, error)
|
||||
remotesFn func() (remotes.Remotes, error)
|
||||
httpClient func() (*http.Client, error)
|
||||
branchConfig func(string) (git.BranchConfig, error)
|
||||
progress progressIndicator
|
||||
baseRepoFn func() (ghrepo.Interface, error)
|
||||
branchFn func() (string, error)
|
||||
remotesFn func() (remotes.Remotes, error)
|
||||
httpClient func() (*http.Client, error)
|
||||
pushDefault func() (string, error)
|
||||
remotePushDefault func() (string, error)
|
||||
parsePushRevision func(string) (string, error)
|
||||
branchConfig func(string) (git.BranchConfig, error)
|
||||
progress progressIndicator
|
||||
|
||||
repo ghrepo.Interface
|
||||
prNumber int
|
||||
branchName string
|
||||
baseRefRepo ghrepo.Interface
|
||||
prNumber int
|
||||
branchName string
|
||||
}
|
||||
|
||||
func NewFinder(factory *cmdutil.Factory) PRFinder {
|
||||
|
|
@ -57,7 +60,16 @@ func NewFinder(factory *cmdutil.Factory) PRFinder {
|
|||
branchFn: factory.Branch,
|
||||
remotesFn: factory.Remotes,
|
||||
httpClient: factory.HttpClient,
|
||||
progress: factory.IOStreams,
|
||||
pushDefault: func() (string, error) {
|
||||
return factory.GitClient.PushDefault(context.Background())
|
||||
},
|
||||
remotePushDefault: func() (string, error) {
|
||||
return factory.GitClient.RemotePushDefault(context.Background())
|
||||
},
|
||||
parsePushRevision: func(branch string) (string, error) {
|
||||
return factory.GitClient.ParsePushRevision(context.Background(), branch)
|
||||
},
|
||||
progress: factory.IOStreams,
|
||||
branchConfig: func(s string) (git.BranchConfig, error) {
|
||||
return factory.GitClient.ReadBranchConfig(context.Background(), s)
|
||||
},
|
||||
|
|
@ -85,46 +97,118 @@ type FindOptions struct {
|
|||
States []string
|
||||
}
|
||||
|
||||
// TODO: Does this also need the BaseBranchName?
|
||||
// PR's are represented by the following:
|
||||
// baseRef -----PR-----> headRef
|
||||
//
|
||||
// A ref is described as "remoteName/branchName", so
|
||||
// baseRepoName/baseBranchName -----PR-----> headRepoName/headBranchName
|
||||
type PullRequestRefs struct {
|
||||
BranchName string
|
||||
HeadRepo ghrepo.Interface
|
||||
BaseRepo ghrepo.Interface
|
||||
}
|
||||
|
||||
// GetPRHeadLabel returns the string that the GitHub API uses to identify the PR. This is
|
||||
// either just the branch name or, if the PR is originating from a fork, the fork owner
|
||||
// and the branch name, like <owner>:<branch>.
|
||||
func (s *PullRequestRefs) GetPRHeadLabel() string {
|
||||
if ghrepo.IsSame(s.HeadRepo, s.BaseRepo) {
|
||||
return s.BranchName
|
||||
}
|
||||
return fmt.Sprintf("%s:%s", s.HeadRepo.RepoOwner(), s.BranchName)
|
||||
}
|
||||
|
||||
func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, error) {
|
||||
// If we have a URL, we don't need git stuff
|
||||
if len(opts.Fields) == 0 {
|
||||
return nil, nil, errors.New("Find error: no fields specified")
|
||||
}
|
||||
|
||||
if repo, prNumber, err := f.parseURL(opts.Selector); err == nil {
|
||||
f.prNumber = prNumber
|
||||
f.repo = repo
|
||||
f.baseRefRepo = repo
|
||||
}
|
||||
|
||||
if f.repo == nil {
|
||||
if f.baseRefRepo == nil {
|
||||
repo, err := f.baseRepoFn()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
f.repo = repo
|
||||
f.baseRefRepo = repo
|
||||
}
|
||||
|
||||
var prRefs PullRequestRefs
|
||||
if opts.Selector == "" {
|
||||
if branch, prNumber, err := f.parseCurrentBranch(); err != nil {
|
||||
// You must be in a git repo for this case to work
|
||||
currentBranchName, err := f.branchFn()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if prNumber > 0 {
|
||||
f.prNumber = prNumber
|
||||
} else {
|
||||
f.branchName = branch
|
||||
}
|
||||
f.branchName = currentBranchName
|
||||
|
||||
// Get the branch config for the current branchName
|
||||
branchConfig, err := f.branchConfig(f.branchName)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Determine if the branch is configured to merge to a special PR ref
|
||||
prHeadRE := regexp.MustCompile(`^refs/pull/(\d+)/head$`)
|
||||
if m := prHeadRE.FindStringSubmatch(branchConfig.MergeRef); m != nil {
|
||||
prNumber, _ := strconv.Atoi(m[1])
|
||||
f.prNumber = prNumber
|
||||
}
|
||||
|
||||
// Determine the PullRequestRefs from config
|
||||
if f.prNumber == 0 {
|
||||
rems, err := f.remotesFn()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Suppressing these errors as we have other means of computing the PullRequestRefs when these fail.
|
||||
parsedPushRevision, _ := f.parsePushRevision(f.branchName)
|
||||
|
||||
pushDefault, err := f.pushDefault()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
remotePushDefault, err := f.remotePushDefault()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
prRefs, err = ParsePRRefs(f.branchName, branchConfig, parsedPushRevision, pushDefault, remotePushDefault, f.baseRefRepo, rems)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
} else if f.prNumber == 0 {
|
||||
// You gave me a selector but I couldn't find a PR number (it wasn't a URL)
|
||||
|
||||
// Try to get a PR number from the selector
|
||||
prNumber, err := strconv.Atoi(strings.TrimPrefix(opts.Selector, "#"))
|
||||
// If opts.Selector is a valid number then assume it is the
|
||||
// PR number unless opts.BaseBranch is specified. This is a
|
||||
// special case for PR create command which will always want
|
||||
// to assume that a numerical selector is a branch name rather
|
||||
// than PR number.
|
||||
prNumber, err := strconv.Atoi(strings.TrimPrefix(opts.Selector, "#"))
|
||||
if opts.BaseBranch == "" && err == nil {
|
||||
f.prNumber = prNumber
|
||||
} else {
|
||||
f.branchName = opts.Selector
|
||||
// We don't expect an error here because parsedPushRevision is empty
|
||||
prRefs, err = ParsePRRefs(f.branchName, git.BranchConfig{}, "", "", "", f.baseRefRepo, remotes.Remotes{})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set up HTTP client
|
||||
httpClient, err := f.httpClient()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
@ -143,7 +227,7 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err
|
|||
|
||||
if fields.Contains("isInMergeQueue") || fields.Contains("isMergeQueueEnabled") {
|
||||
cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
|
||||
detector := fd.NewDetector(cachedClient, f.repo.RepoHost())
|
||||
detector := fd.NewDetector(cachedClient, f.baseRefRepo.RepoHost())
|
||||
prFeatures, err := detector.PullRequestFeatures()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
@ -164,36 +248,39 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err
|
|||
if f.prNumber > 0 {
|
||||
if numberFieldOnly {
|
||||
// avoid hitting the API if we already have all the information
|
||||
return &api.PullRequest{Number: f.prNumber}, f.repo, nil
|
||||
return &api.PullRequest{Number: f.prNumber}, f.baseRefRepo, nil
|
||||
}
|
||||
pr, err = findByNumber(httpClient, f.baseRefRepo, f.prNumber, fields.ToSlice())
|
||||
if err != nil {
|
||||
return pr, f.baseRefRepo, err
|
||||
}
|
||||
pr, err = findByNumber(httpClient, f.repo, f.prNumber, fields.ToSlice())
|
||||
} else {
|
||||
pr, err = findForBranch(httpClient, f.repo, opts.BaseBranch, f.branchName, opts.States, fields.ToSlice())
|
||||
}
|
||||
if err != nil {
|
||||
return pr, f.repo, err
|
||||
pr, err = findForBranch(httpClient, f.baseRefRepo, opts.BaseBranch, prRefs.GetPRHeadLabel(), opts.States, fields.ToSlice())
|
||||
if err != nil {
|
||||
return pr, f.baseRefRepo, err
|
||||
}
|
||||
}
|
||||
|
||||
g, _ := errgroup.WithContext(context.Background())
|
||||
if fields.Contains("reviews") {
|
||||
g.Go(func() error {
|
||||
return preloadPrReviews(httpClient, f.repo, pr)
|
||||
return preloadPrReviews(httpClient, f.baseRefRepo, pr)
|
||||
})
|
||||
}
|
||||
if fields.Contains("comments") {
|
||||
g.Go(func() error {
|
||||
return preloadPrComments(httpClient, f.repo, pr)
|
||||
return preloadPrComments(httpClient, f.baseRefRepo, pr)
|
||||
})
|
||||
}
|
||||
if fields.Contains("statusCheckRollup") {
|
||||
g.Go(func() error {
|
||||
return preloadPrChecks(httpClient, f.repo, pr)
|
||||
return preloadPrChecks(httpClient, f.baseRefRepo, pr)
|
||||
})
|
||||
}
|
||||
if getProjectItems {
|
||||
g.Go(func() error {
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
err := api.ProjectsV2ItemsForPullRequest(apiClient, f.repo, pr)
|
||||
err := api.ProjectsV2ItemsForPullRequest(apiClient, f.baseRefRepo, pr)
|
||||
if err != nil && !api.ProjectsV2IgnorableError(err) {
|
||||
return err
|
||||
}
|
||||
|
|
@ -201,7 +288,7 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err
|
|||
})
|
||||
}
|
||||
|
||||
return pr, f.repo, g.Wait()
|
||||
return pr, f.baseRefRepo, g.Wait()
|
||||
}
|
||||
|
||||
var pullURLRE = regexp.MustCompile(`^/([^/]+)/([^/]+)/pull/(\d+)`)
|
||||
|
|
@ -230,59 +317,70 @@ func (f *finder) parseURL(prURL string) (ghrepo.Interface, int, error) {
|
|||
return repo, prNumber, nil
|
||||
}
|
||||
|
||||
var prHeadRE = regexp.MustCompile(`^refs/pull/(\d+)/head$`)
|
||||
|
||||
func (f *finder) parseCurrentBranch() (string, int, error) {
|
||||
prHeadRef, err := f.branchFn()
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
func ParsePRRefs(currentBranchName string, branchConfig git.BranchConfig, parsedPushRevision string, pushDefault string, remotePushDefault string, baseRefRepo ghrepo.Interface, rems remotes.Remotes) (PullRequestRefs, error) {
|
||||
prRefs := PullRequestRefs{
|
||||
BaseRepo: baseRefRepo,
|
||||
}
|
||||
|
||||
branchConfig, err := f.branchConfig(prHeadRef)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
// If @{push} resolves, then we have all the information we need to determine the head repo
|
||||
// and branch name. It is of the form <remote>/<branch>.
|
||||
if parsedPushRevision != "" {
|
||||
for _, r := range rems {
|
||||
// Find the remote who's name matches the push <remote> prefix
|
||||
if strings.HasPrefix(parsedPushRevision, r.Name+"/") {
|
||||
prRefs.BranchName = strings.TrimPrefix(parsedPushRevision, r.Name+"/")
|
||||
prRefs.HeadRepo = r.Repo
|
||||
return prRefs, nil
|
||||
}
|
||||
}
|
||||
|
||||
remoteNames := make([]string, len(rems))
|
||||
for i, r := range rems {
|
||||
remoteNames[i] = r.Name
|
||||
}
|
||||
return PullRequestRefs{}, fmt.Errorf("no remote for %q found in %q", parsedPushRevision, strings.Join(remoteNames, ", "))
|
||||
}
|
||||
|
||||
// the branch is configured to merge a special PR head ref
|
||||
if m := prHeadRE.FindStringSubmatch(branchConfig.MergeRef); m != nil {
|
||||
prNumber, _ := strconv.Atoi(m[1])
|
||||
return "", prNumber, nil
|
||||
// We assume the PR's branch name is the same as whatever f.BranchFn() returned earlier
|
||||
// unless the user has specified push.default = upstream or tracking, then we use the
|
||||
// branch name from the merge ref.
|
||||
prRefs.BranchName = currentBranchName
|
||||
if pushDefault == "upstream" || pushDefault == "tracking" {
|
||||
prRefs.BranchName = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/")
|
||||
}
|
||||
|
||||
var gitRemoteRepo ghrepo.Interface
|
||||
if branchConfig.RemoteURL != nil {
|
||||
// the branch merges from a remote specified by URL
|
||||
if r, err := ghrepo.FromURL(branchConfig.RemoteURL); err == nil {
|
||||
gitRemoteRepo = r
|
||||
// To get the HeadRepo, we look to the git config. The HeadRepo comes from one of the following, in order of precedence:
|
||||
// 1. branch.<name>.pushRemote
|
||||
// 2. remote.pushDefault
|
||||
// 3. branch.<name>.remote
|
||||
if branchConfig.PushRemoteName != "" {
|
||||
if r, err := rems.FindByName(branchConfig.PushRemoteName); err == nil {
|
||||
prRefs.HeadRepo = r.Repo
|
||||
}
|
||||
} else if branchConfig.PushRemoteURL != nil {
|
||||
if r, err := ghrepo.FromURL(branchConfig.PushRemoteURL); err == nil {
|
||||
prRefs.HeadRepo = r
|
||||
}
|
||||
} else if remotePushDefault != "" {
|
||||
if r, err := rems.FindByName(remotePushDefault); err == nil {
|
||||
prRefs.HeadRepo = r.Repo
|
||||
}
|
||||
} else if branchConfig.RemoteName != "" {
|
||||
// the branch merges from a remote specified by name
|
||||
rem, _ := f.remotesFn()
|
||||
if r, err := rem.FindByName(branchConfig.RemoteName); err == nil {
|
||||
gitRemoteRepo = r
|
||||
if r, err := rems.FindByName(branchConfig.RemoteName); err == nil {
|
||||
prRefs.HeadRepo = r.Repo
|
||||
}
|
||||
} else if branchConfig.RemoteURL != nil {
|
||||
if r, err := ghrepo.FromURL(branchConfig.RemoteURL); err == nil {
|
||||
prRefs.HeadRepo = r
|
||||
}
|
||||
}
|
||||
|
||||
if gitRemoteRepo != nil {
|
||||
if strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") {
|
||||
prHeadRef = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/")
|
||||
}
|
||||
// prepend `OWNER:` if this branch is pushed to a fork
|
||||
// This is determined by:
|
||||
// - The repo having a different owner
|
||||
// - The repo having the same owner but a different name (private org fork)
|
||||
// I suspect that the implementation of the second case may be broken in the face
|
||||
// of a repo rename, where the remote hasn't been updated locally. This is a
|
||||
// frequent issue in commands that use SmartBaseRepoFunc. It's not any worse than not
|
||||
// supporting this case at all though.
|
||||
sameOwner := strings.EqualFold(gitRemoteRepo.RepoOwner(), f.repo.RepoOwner())
|
||||
sameOwnerDifferentRepoName := sameOwner && !strings.EqualFold(gitRemoteRepo.RepoName(), f.repo.RepoName())
|
||||
if !sameOwner || sameOwnerDifferentRepoName {
|
||||
prHeadRef = fmt.Sprintf("%s:%s", gitRemoteRepo.RepoOwner(), prHeadRef)
|
||||
}
|
||||
// The PR merges from a branch in the same repo as the base branch (usually the default branch)
|
||||
if prRefs.HeadRepo == nil {
|
||||
prRefs.HeadRepo = baseRefRepo
|
||||
}
|
||||
|
||||
return prHeadRef, 0, nil
|
||||
return prRefs, nil
|
||||
}
|
||||
|
||||
func findByNumber(httpClient *http.Client, repo ghrepo.Interface, number int, fields []string) (*api.PullRequest, error) {
|
||||
|
|
@ -315,7 +413,7 @@ func findByNumber(httpClient *http.Client, repo ghrepo.Interface, number int, fi
|
|||
return &resp.Repository.PullRequest, nil
|
||||
}
|
||||
|
||||
func findForBranch(httpClient *http.Client, repo ghrepo.Interface, baseBranch, headBranch string, stateFilters, fields []string) (*api.PullRequest, error) {
|
||||
func findForBranch(httpClient *http.Client, repo ghrepo.Interface, baseBranch, headBranchWithOwnerIfFork string, stateFilters, fields []string) (*api.PullRequest, error) {
|
||||
type response struct {
|
||||
Repository struct {
|
||||
PullRequests struct {
|
||||
|
|
@ -342,9 +440,9 @@ func findForBranch(httpClient *http.Client, repo ghrepo.Interface, baseBranch, h
|
|||
}
|
||||
}`, api.PullRequestGraphQL(fieldSet.ToSlice()))
|
||||
|
||||
branchWithoutOwner := headBranch
|
||||
if idx := strings.Index(headBranch, ":"); idx >= 0 {
|
||||
branchWithoutOwner = headBranch[idx+1:]
|
||||
branchWithoutOwner := headBranchWithOwnerIfFork
|
||||
if idx := strings.Index(headBranchWithOwnerIfFork, ":"); idx >= 0 {
|
||||
branchWithoutOwner = headBranchWithOwnerIfFork[idx+1:]
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
|
|
@ -367,18 +465,17 @@ func findForBranch(httpClient *http.Client, repo ghrepo.Interface, baseBranch, h
|
|||
})
|
||||
|
||||
for _, pr := range prs {
|
||||
headBranchMatches := pr.HeadLabel() == headBranch
|
||||
headBranchMatches := pr.HeadLabel() == headBranchWithOwnerIfFork
|
||||
baseBranchEmptyOrMatches := baseBranch == "" || pr.BaseRefName == baseBranch
|
||||
// When the head is the default branch, it doesn't really make sense to show merged or closed PRs.
|
||||
// https://github.com/cli/cli/issues/4263
|
||||
isNotClosedOrMergedWhenHeadIsDefault := pr.State == "OPEN" || resp.Repository.DefaultBranchRef.Name != headBranch
|
||||
|
||||
isNotClosedOrMergedWhenHeadIsDefault := pr.State == "OPEN" || resp.Repository.DefaultBranchRef.Name != headBranchWithOwnerIfFork
|
||||
if headBranchMatches && baseBranchEmptyOrMatches && isNotClosedOrMergedWhenHeadIsDefault {
|
||||
return &pr, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, &NotFoundError{fmt.Errorf("no pull requests found for branch %q", headBranch)}
|
||||
return nil, &NotFoundError{fmt.Errorf("no pull requests found for branch %q", headBranchWithOwnerIfFork)}
|
||||
}
|
||||
|
||||
func preloadPrReviews(httpClient *http.Client, repo ghrepo.Interface, pr *api.PullRequest) error {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package shared
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
|
@ -15,16 +16,50 @@ import (
|
|||
)
|
||||
|
||||
type args struct {
|
||||
baseRepoFn func() (ghrepo.Interface, error)
|
||||
branchFn func() (string, error)
|
||||
branchConfig func(string) (git.BranchConfig, error)
|
||||
remotesFn func() (context.Remotes, error)
|
||||
selector string
|
||||
fields []string
|
||||
baseBranch string
|
||||
baseRepoFn func() (ghrepo.Interface, error)
|
||||
branchFn func() (string, error)
|
||||
branchConfig func(string) (git.BranchConfig, error)
|
||||
pushDefault func() (string, error)
|
||||
remotePushDefault func() (string, error)
|
||||
parsePushRevision func(string) (string, error)
|
||||
selector string
|
||||
fields []string
|
||||
baseBranch string
|
||||
}
|
||||
|
||||
func TestFind(t *testing.T) {
|
||||
// TODO: Abstract these out meaningfully for reuse in parsePRRefs tests
|
||||
originOwnerUrl, err := url.Parse("https://github.com/ORIGINOWNER/REPO.git")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
remoteOrigin := context.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "origin",
|
||||
FetchURL: originOwnerUrl,
|
||||
},
|
||||
Repo: ghrepo.New("ORIGINOWNER", "REPO"),
|
||||
}
|
||||
remoteOther := context.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "other",
|
||||
FetchURL: originOwnerUrl,
|
||||
},
|
||||
Repo: ghrepo.New("ORIGINOWNER", "OTHER-REPO"),
|
||||
}
|
||||
|
||||
upstreamOwnerUrl, err := url.Parse("https://github.com/UPSTREAMOWNER/REPO.git")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
remoteUpstream := context.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "upstream",
|
||||
FetchURL: upstreamOwnerUrl,
|
||||
},
|
||||
Repo: ghrepo.New("UPSTREAMOWNER", "REPO"),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
|
|
@ -36,11 +71,13 @@ func TestFind(t *testing.T) {
|
|||
{
|
||||
name: "number argument",
|
||||
args: args{
|
||||
selector: "13",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
selector: "13",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil),
|
||||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{}, nil),
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
|
|
@ -50,7 +87,7 @@ func TestFind(t *testing.T) {
|
|||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
wantRepo: "https://github.com/ORIGINOWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "number argument with base branch",
|
||||
|
|
@ -58,9 +95,16 @@ func TestFind(t *testing.T) {
|
|||
selector: "13",
|
||||
baseBranch: "main",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil),
|
||||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{
|
||||
PushRemoteName: remoteOrigin.Remote.Name,
|
||||
}, nil),
|
||||
pushDefault: stubPushDefault("simple", nil),
|
||||
remotePushDefault: stubRemotePushDefault("", nil),
|
||||
parsePushRevision: stubParsedPushRevision("", nil),
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
|
|
@ -73,22 +117,26 @@ func TestFind(t *testing.T) {
|
|||
"baseRefName": "main",
|
||||
"headRefName": "13",
|
||||
"isCrossRepository": false,
|
||||
"headRepositoryOwner": {"login":"OWNER"}
|
||||
"headRepositoryOwner": {"login":"ORIGINOWNER"}
|
||||
}
|
||||
]}
|
||||
}}}`))
|
||||
},
|
||||
wantPR: 123,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
wantRepo: "https://github.com/ORIGINOWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "baseRepo is error",
|
||||
args: args{
|
||||
selector: "13",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return nil, errors.New("baseRepoErr")
|
||||
selector: "13",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: stubBaseRepoFn(nil, errors.New("baseRepoErr")),
|
||||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{}, nil),
|
||||
pushDefault: stubPushDefault("simple", nil),
|
||||
remotePushDefault: stubRemotePushDefault("", nil),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
|
|
@ -103,24 +151,32 @@ func TestFind(t *testing.T) {
|
|||
{
|
||||
name: "number only",
|
||||
args: args{
|
||||
selector: "13",
|
||||
fields: []string{"number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
selector: "13",
|
||||
fields: []string{"number"},
|
||||
baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil),
|
||||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{}, nil),
|
||||
pushDefault: stubPushDefault("simple", nil),
|
||||
remotePushDefault: stubRemotePushDefault("", nil),
|
||||
},
|
||||
httpStub: nil,
|
||||
wantPR: 13,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
wantRepo: "https://github.com/ORIGINOWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "number with hash argument",
|
||||
args: args{
|
||||
selector: "#13",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
selector: "#13",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil),
|
||||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{}, nil),
|
||||
pushDefault: stubPushDefault("simple", nil),
|
||||
remotePushDefault: stubRemotePushDefault("", nil),
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
|
|
@ -130,14 +186,20 @@ func TestFind(t *testing.T) {
|
|||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
wantRepo: "https://github.com/ORIGINOWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "URL argument",
|
||||
name: "PR URL argument",
|
||||
args: args{
|
||||
selector: "https://example.org/OWNER/REPO/pull/13/files",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: nil,
|
||||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{}, nil),
|
||||
pushDefault: stubPushDefault("simple", nil),
|
||||
remotePushDefault: stubRemotePushDefault("", nil),
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
|
|
@ -150,13 +212,50 @@ func TestFind(t *testing.T) {
|
|||
wantRepo: "https://example.org/OWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "branch argument",
|
||||
name: "PR URL argument and not in a local git repo",
|
||||
args: args{
|
||||
selector: "blueberries",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
selector: "https://example.org/OWNER/REPO/pull/13/files",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: nil,
|
||||
branchFn: func() (string, error) {
|
||||
return "", &git.GitError{
|
||||
Stderr: "fatal: branchFn error",
|
||||
ExitCode: 128,
|
||||
}
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{}, &git.GitError{
|
||||
Stderr: "fatal: branchConfig error",
|
||||
ExitCode: 128,
|
||||
}),
|
||||
pushDefault: stubPushDefault("simple", nil),
|
||||
remotePushDefault: stubRemotePushDefault("", &git.GitError{
|
||||
Stderr: "fatal: remotePushDefault error",
|
||||
ExitCode: 128,
|
||||
}),
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{
|
||||
"pullRequest":{"number":13}
|
||||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://example.org/OWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "when provided branch argument with an open and closed PR for that branch name, it returns the open PR",
|
||||
args: args{
|
||||
selector: "blueberries",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil),
|
||||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{}, nil),
|
||||
pushDefault: stubPushDefault("simple", nil),
|
||||
parsePushRevision: stubParsedPushRevision("", nil),
|
||||
remotePushDefault: stubRemotePushDefault("", nil),
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
|
|
@ -169,7 +268,7 @@ func TestFind(t *testing.T) {
|
|||
"baseRefName": "main",
|
||||
"headRefName": "blueberries",
|
||||
"isCrossRepository": false,
|
||||
"headRepositoryOwner": {"login":"OWNER"}
|
||||
"headRepositoryOwner": {"login":"ORIGINOWNER"}
|
||||
},
|
||||
{
|
||||
"number": 13,
|
||||
|
|
@ -177,13 +276,13 @@ func TestFind(t *testing.T) {
|
|||
"baseRefName": "main",
|
||||
"headRefName": "blueberries",
|
||||
"isCrossRepository": false,
|
||||
"headRepositoryOwner": {"login":"OWNER"}
|
||||
"headRepositoryOwner": {"login":"ORIGINOWNER"}
|
||||
}
|
||||
]}
|
||||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
wantRepo: "https://github.com/ORIGINOWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "branch argument with base branch",
|
||||
|
|
@ -194,6 +293,13 @@ func TestFind(t *testing.T) {
|
|||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
},
|
||||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{}, nil),
|
||||
pushDefault: stubPushDefault("simple", nil),
|
||||
remotePushDefault: stubRemotePushDefault("", nil),
|
||||
parsePushRevision: stubParsedPushRevision("", nil),
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
|
|
@ -233,7 +339,10 @@ func TestFind(t *testing.T) {
|
|||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{}, nil),
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{}, nil),
|
||||
pushDefault: stubPushDefault("simple", nil),
|
||||
remotePushDefault: stubRemotePushDefault("", nil),
|
||||
parsePushRevision: stubParsedPushRevision("", nil),
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
|
|
@ -265,7 +374,10 @@ func TestFind(t *testing.T) {
|
|||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{}, nil),
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{}, nil),
|
||||
pushDefault: stubPushDefault("simple", nil),
|
||||
remotePushDefault: stubRemotePushDefault("", nil),
|
||||
parsePushRevision: stubParsedPushRevision("", nil),
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
|
|
@ -303,26 +415,21 @@ func TestFind(t *testing.T) {
|
|||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "current branch with upstream configuration",
|
||||
name: "when the current branch is configured to push to and pull from 'upstream' and push.default = upstream but the repo push/pulls from 'origin', it finds the PR associated with the upstream repo and returns origin as the base repo",
|
||||
args: args{
|
||||
selector: "",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
},
|
||||
selector: "",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil),
|
||||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{
|
||||
MergeRef: "refs/heads/blue-upstream-berries",
|
||||
RemoteName: "origin",
|
||||
MergeRef: "refs/heads/blue-upstream-berries",
|
||||
PushRemoteName: "upstream",
|
||||
}, nil),
|
||||
remotesFn: func() (context.Remotes, error) {
|
||||
return context.Remotes{{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.New("UPSTREAMOWNER", "REPO"),
|
||||
}}, nil
|
||||
},
|
||||
pushDefault: stubPushDefault("upstream", nil),
|
||||
remotePushDefault: stubRemotePushDefault("", nil),
|
||||
parsePushRevision: stubParsedPushRevision("", nil),
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
|
|
@ -341,27 +448,28 @@ func TestFind(t *testing.T) {
|
|||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
wantRepo: "https://github.com/ORIGINOWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "current branch with upstream RemoteURL configuration",
|
||||
// The current BRANCH is configured to push to and pull from a URL (upstream, in this example)
|
||||
// which is different from what the REPO is configured to push to and pull from (origin, in this example)
|
||||
// and push.default = upstream. It should find the PR associated with the upstream repo and return
|
||||
// origin as the base repo
|
||||
name: "when push.default = upstream and the current branch is configured to push/pull from a different remote than the repo",
|
||||
args: args{
|
||||
selector: "",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
},
|
||||
selector: "",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil),
|
||||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: func(branch string) (git.BranchConfig, error) {
|
||||
u, _ := url.Parse("https://github.com/UPSTREAMOWNER/REPO")
|
||||
return stubBranchConfig(git.BranchConfig{
|
||||
MergeRef: "refs/heads/blue-upstream-berries",
|
||||
RemoteURL: u,
|
||||
}, nil)(branch)
|
||||
},
|
||||
remotesFn: nil,
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{
|
||||
MergeRef: "refs/heads/blue-upstream-berries",
|
||||
PushRemoteURL: remoteUpstream.Remote.FetchURL,
|
||||
}, nil),
|
||||
pushDefault: stubPushDefault("upstream", nil),
|
||||
remotePushDefault: stubRemotePushDefault("", nil),
|
||||
parsePushRevision: stubParsedPushRevision("", nil),
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
|
|
@ -380,28 +488,21 @@ func TestFind(t *testing.T) {
|
|||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
wantRepo: "https://github.com/ORIGINOWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "current branch with upstream and fork in same org",
|
||||
args: args{
|
||||
selector: "",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
},
|
||||
selector: "",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: stubBaseRepoFn(ghrepo.New("ORIGINOWNER", "REPO"), nil),
|
||||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{
|
||||
RemoteName: "origin",
|
||||
}, nil),
|
||||
remotesFn: func() (context.Remotes, error) {
|
||||
return context.Remotes{{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.New("OWNER", "REPO-FORK"),
|
||||
}}, nil
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{}, nil),
|
||||
pushDefault: stubPushDefault("simple", nil),
|
||||
remotePushDefault: stubRemotePushDefault("", nil),
|
||||
parsePushRevision: stubParsedPushRevision("other/blueberries", nil),
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
|
|
@ -414,13 +515,13 @@ func TestFind(t *testing.T) {
|
|||
"baseRefName": "main",
|
||||
"headRefName": "blueberries",
|
||||
"isCrossRepository": true,
|
||||
"headRepositoryOwner": {"login":"OWNER"}
|
||||
"headRepositoryOwner": {"login":"ORIGINOWNER"}
|
||||
}
|
||||
]}
|
||||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
wantRepo: "https://github.com/ORIGINOWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "current branch made by pr checkout",
|
||||
|
|
@ -461,6 +562,8 @@ func TestFind(t *testing.T) {
|
|||
branchConfig: stubBranchConfig(git.BranchConfig{
|
||||
MergeRef: "refs/pull/13/head",
|
||||
}, nil),
|
||||
pushDefault: stubPushDefault("simple", nil),
|
||||
remotePushDefault: stubRemotePushDefault("", nil),
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
|
|
@ -521,10 +624,17 @@ func TestFind(t *testing.T) {
|
|||
httpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
baseRepoFn: tt.args.baseRepoFn,
|
||||
branchFn: tt.args.branchFn,
|
||||
branchConfig: tt.args.branchConfig,
|
||||
remotesFn: tt.args.remotesFn,
|
||||
baseRepoFn: tt.args.baseRepoFn,
|
||||
branchFn: tt.args.branchFn,
|
||||
branchConfig: tt.args.branchConfig,
|
||||
pushDefault: tt.args.pushDefault,
|
||||
remotePushDefault: tt.args.remotePushDefault,
|
||||
parsePushRevision: tt.args.parsePushRevision,
|
||||
remotesFn: stubRemotes(context.Remotes{
|
||||
&remoteOrigin,
|
||||
&remoteOther,
|
||||
&remoteUpstream,
|
||||
}, nil),
|
||||
}
|
||||
|
||||
pr, repo, err := f.Find(FindOptions{
|
||||
|
|
@ -557,42 +667,307 @@ func TestFind(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_parseCurrentBranch(t *testing.T) {
|
||||
func TestParsePRRefs(t *testing.T) {
|
||||
originOwnerUrl, err := url.Parse("https://github.com/ORIGINOWNER/REPO.git")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
remoteOrigin := context.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "origin",
|
||||
FetchURL: originOwnerUrl,
|
||||
},
|
||||
Repo: ghrepo.New("ORIGINOWNER", "REPO"),
|
||||
}
|
||||
remoteOther := context.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "other",
|
||||
FetchURL: originOwnerUrl,
|
||||
},
|
||||
Repo: ghrepo.New("ORIGINOWNER", "REPO"),
|
||||
}
|
||||
|
||||
upstreamOwnerUrl, err := url.Parse("https://github.com/UPSTREAMOWNER/REPO.git")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
remoteUpstream := context.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "upstream",
|
||||
FetchURL: upstreamOwnerUrl,
|
||||
},
|
||||
Repo: ghrepo.New("UPSTREAMOWNER", "REPO"),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantSelector string
|
||||
wantPR int
|
||||
wantError error
|
||||
name string
|
||||
branchConfig git.BranchConfig
|
||||
pushDefault string
|
||||
parsedPushRevision string
|
||||
remotePushDefault string
|
||||
currentBranchName string
|
||||
baseRefRepo ghrepo.Interface
|
||||
rems context.Remotes
|
||||
wantPRRefs PullRequestRefs
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "failed branch config",
|
||||
args: args{
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{}, errors.New("branchConfigErr")),
|
||||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
name: "When the branch is called 'blueberries' with an empty branch config, it returns the correct PullRequestRefs",
|
||||
branchConfig: git.BranchConfig{},
|
||||
currentBranchName: "blueberries",
|
||||
baseRefRepo: remoteOrigin.Repo,
|
||||
wantPRRefs: PullRequestRefs{
|
||||
BranchName: "blueberries",
|
||||
HeadRepo: remoteOrigin.Repo,
|
||||
BaseRepo: remoteOrigin.Repo,
|
||||
},
|
||||
wantSelector: "",
|
||||
wantPR: 0,
|
||||
wantError: errors.New("branchConfigErr"),
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "When the branch is called 'otherBranch' with an empty branch config, it returns the correct PullRequestRefs",
|
||||
branchConfig: git.BranchConfig{},
|
||||
currentBranchName: "otherBranch",
|
||||
baseRefRepo: remoteOrigin.Repo,
|
||||
wantPRRefs: PullRequestRefs{
|
||||
BranchName: "otherBranch",
|
||||
HeadRepo: remoteOrigin.Repo,
|
||||
BaseRepo: remoteOrigin.Repo,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "When the branch name doesn't match the branch name in BranchConfig.Push, it returns the BranchConfig.Push branch name",
|
||||
parsedPushRevision: "origin/pushBranch",
|
||||
currentBranchName: "blueberries",
|
||||
baseRefRepo: remoteOrigin.Repo,
|
||||
rems: context.Remotes{
|
||||
&remoteOrigin,
|
||||
},
|
||||
wantPRRefs: PullRequestRefs{
|
||||
BranchName: "pushBranch",
|
||||
HeadRepo: remoteOrigin.Repo,
|
||||
BaseRepo: remoteOrigin.Repo,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "When the push revision doesn't match a remote, it returns an error",
|
||||
parsedPushRevision: "origin/differentPushBranch",
|
||||
currentBranchName: "blueberries",
|
||||
baseRefRepo: remoteOrigin.Repo,
|
||||
rems: context.Remotes{
|
||||
&remoteUpstream,
|
||||
&remoteOther,
|
||||
},
|
||||
wantPRRefs: PullRequestRefs{},
|
||||
wantErr: fmt.Errorf("no remote for %q found in %q", "origin/differentPushBranch", "upstream, other"),
|
||||
},
|
||||
{
|
||||
name: "When the branch name doesn't match a different branch name in BranchConfig.Push and the remote isn't 'origin', it returns the BranchConfig.Push branch name",
|
||||
parsedPushRevision: "other/pushBranch",
|
||||
currentBranchName: "blueberries",
|
||||
baseRefRepo: remoteOrigin.Repo,
|
||||
rems: context.Remotes{
|
||||
&remoteOther,
|
||||
},
|
||||
wantPRRefs: PullRequestRefs{
|
||||
BranchName: "pushBranch",
|
||||
HeadRepo: remoteOther.Repo,
|
||||
BaseRepo: remoteOrigin.Repo,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "When the push remote is the same as the baseRepo, it returns the baseRepo as the PullRequestRefs HeadRepo",
|
||||
branchConfig: git.BranchConfig{
|
||||
PushRemoteName: remoteOrigin.Remote.Name,
|
||||
},
|
||||
currentBranchName: "blueberries",
|
||||
baseRefRepo: remoteOrigin.Repo,
|
||||
rems: context.Remotes{
|
||||
&remoteOrigin,
|
||||
&remoteUpstream,
|
||||
},
|
||||
wantPRRefs: PullRequestRefs{
|
||||
BranchName: "blueberries",
|
||||
HeadRepo: remoteOrigin.Repo,
|
||||
BaseRepo: remoteOrigin.Repo,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "When the push remote is different from the baseRepo, it returns the push remote repo as the PullRequestRefs HeadRepo",
|
||||
branchConfig: git.BranchConfig{
|
||||
PushRemoteName: remoteOrigin.Remote.Name,
|
||||
},
|
||||
currentBranchName: "blueberries",
|
||||
baseRefRepo: remoteUpstream.Repo,
|
||||
rems: context.Remotes{
|
||||
&remoteOrigin,
|
||||
&remoteUpstream,
|
||||
},
|
||||
wantPRRefs: PullRequestRefs{
|
||||
BranchName: "blueberries",
|
||||
HeadRepo: remoteOrigin.Repo,
|
||||
BaseRepo: remoteUpstream.Repo,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "When the push remote defined by a URL and the baseRepo is different from the push remote, it returns the push remote repo as the PullRequestRefs HeadRepo",
|
||||
branchConfig: git.BranchConfig{
|
||||
PushRemoteURL: remoteOrigin.Remote.FetchURL,
|
||||
},
|
||||
currentBranchName: "blueberries",
|
||||
baseRefRepo: remoteUpstream.Repo,
|
||||
rems: context.Remotes{
|
||||
&remoteOrigin,
|
||||
&remoteUpstream,
|
||||
},
|
||||
wantPRRefs: PullRequestRefs{
|
||||
BranchName: "blueberries",
|
||||
HeadRepo: remoteOrigin.Repo,
|
||||
BaseRepo: remoteUpstream.Repo,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "When the push remote and merge ref are configured to a different repo and push.default = upstream, it should return the branch name from the other repo",
|
||||
branchConfig: git.BranchConfig{
|
||||
PushRemoteName: remoteUpstream.Remote.Name,
|
||||
MergeRef: "refs/heads/blue-upstream-berries",
|
||||
},
|
||||
pushDefault: "upstream",
|
||||
currentBranchName: "blueberries",
|
||||
baseRefRepo: remoteOrigin.Repo,
|
||||
rems: context.Remotes{
|
||||
&remoteOrigin,
|
||||
&remoteUpstream,
|
||||
},
|
||||
wantPRRefs: PullRequestRefs{
|
||||
BranchName: "blue-upstream-berries",
|
||||
HeadRepo: remoteUpstream.Repo,
|
||||
BaseRepo: remoteOrigin.Repo,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "When the push remote and merge ref are configured to a different repo and push.default = tracking, it should return the branch name from the other repo",
|
||||
branchConfig: git.BranchConfig{
|
||||
PushRemoteName: remoteUpstream.Remote.Name,
|
||||
MergeRef: "refs/heads/blue-upstream-berries",
|
||||
},
|
||||
pushDefault: "tracking",
|
||||
currentBranchName: "blueberries",
|
||||
baseRefRepo: remoteOrigin.Repo,
|
||||
rems: context.Remotes{
|
||||
&remoteOrigin,
|
||||
&remoteUpstream,
|
||||
},
|
||||
wantPRRefs: PullRequestRefs{
|
||||
BranchName: "blue-upstream-berries",
|
||||
HeadRepo: remoteUpstream.Repo,
|
||||
BaseRepo: remoteOrigin.Repo,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "When remote.pushDefault is set, it returns the correct PullRequestRefs",
|
||||
branchConfig: git.BranchConfig{},
|
||||
remotePushDefault: remoteUpstream.Remote.Name,
|
||||
currentBranchName: "blueberries",
|
||||
baseRefRepo: remoteOrigin.Repo,
|
||||
rems: context.Remotes{
|
||||
&remoteOrigin,
|
||||
&remoteUpstream,
|
||||
},
|
||||
wantPRRefs: PullRequestRefs{
|
||||
BranchName: "blueberries",
|
||||
HeadRepo: remoteUpstream.Repo,
|
||||
BaseRepo: remoteOrigin.Repo,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "When the remote name is set on the branch, it returns the correct PullRequestRefs",
|
||||
branchConfig: git.BranchConfig{
|
||||
RemoteName: remoteUpstream.Remote.Name,
|
||||
},
|
||||
currentBranchName: "blueberries",
|
||||
baseRefRepo: remoteOrigin.Repo,
|
||||
rems: context.Remotes{
|
||||
&remoteOrigin,
|
||||
&remoteUpstream,
|
||||
},
|
||||
wantPRRefs: PullRequestRefs{
|
||||
BranchName: "blueberries",
|
||||
HeadRepo: remoteUpstream.Repo,
|
||||
BaseRepo: remoteOrigin.Repo,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "When the remote URL is set on the branch, it returns the correct PullRequestRefs",
|
||||
branchConfig: git.BranchConfig{
|
||||
RemoteURL: remoteUpstream.Remote.FetchURL,
|
||||
},
|
||||
currentBranchName: "blueberries",
|
||||
baseRefRepo: remoteOrigin.Repo,
|
||||
rems: context.Remotes{
|
||||
&remoteOrigin,
|
||||
&remoteUpstream,
|
||||
},
|
||||
wantPRRefs: PullRequestRefs{
|
||||
BranchName: "blueberries",
|
||||
HeadRepo: remoteUpstream.Repo,
|
||||
BaseRepo: remoteOrigin.Repo,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := finder{
|
||||
httpClient: func() (*http.Client, error) {
|
||||
return &http.Client{}, nil
|
||||
},
|
||||
baseRepoFn: tt.args.baseRepoFn,
|
||||
branchFn: tt.args.branchFn,
|
||||
branchConfig: tt.args.branchConfig,
|
||||
remotesFn: tt.args.remotesFn,
|
||||
prRefs, err := ParsePRRefs(tt.currentBranchName, tt.branchConfig, tt.parsedPushRevision, tt.pushDefault, tt.remotePushDefault, tt.baseRefRepo, tt.rems)
|
||||
if tt.wantErr != nil {
|
||||
require.Equal(t, tt.wantErr, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
selector, pr, err := f.parseCurrentBranch()
|
||||
assert.Equal(t, tt.wantSelector, selector)
|
||||
assert.Equal(t, tt.wantPR, pr)
|
||||
assert.Equal(t, tt.wantError, err)
|
||||
require.Equal(t, tt.wantPRRefs, prRefs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRRefs_GetPRHeadLabel(t *testing.T) {
|
||||
originRepo := ghrepo.New("ORIGINOWNER", "REPO")
|
||||
upstreamRepo := ghrepo.New("UPSTREAMOWNER", "REPO")
|
||||
tests := []struct {
|
||||
name string
|
||||
prRefs PullRequestRefs
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "When the HeadRepo and BaseRepo match, it returns the branch name",
|
||||
prRefs: PullRequestRefs{
|
||||
BranchName: "blueberries",
|
||||
HeadRepo: originRepo,
|
||||
BaseRepo: originRepo,
|
||||
},
|
||||
want: "blueberries",
|
||||
},
|
||||
{
|
||||
name: "When the HeadRepo and BaseRepo do not match, it returns the prepended HeadRepo owner to the branch name",
|
||||
prRefs: PullRequestRefs{
|
||||
BranchName: "blueberries",
|
||||
HeadRepo: originRepo,
|
||||
BaseRepo: upstreamRepo,
|
||||
},
|
||||
want: "ORIGINOWNER:blueberries",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, tt.prRefs.GetPRHeadLabel())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -602,3 +977,33 @@ func stubBranchConfig(branchConfig git.BranchConfig, err error) func(string) (gi
|
|||
return branchConfig, err
|
||||
}
|
||||
}
|
||||
|
||||
func stubRemotes(remotes context.Remotes, err error) func() (context.Remotes, error) {
|
||||
return func() (context.Remotes, error) {
|
||||
return remotes, err
|
||||
}
|
||||
}
|
||||
|
||||
func stubBaseRepoFn(baseRepo ghrepo.Interface, err error) func() (ghrepo.Interface, error) {
|
||||
return func() (ghrepo.Interface, error) {
|
||||
return baseRepo, err
|
||||
}
|
||||
}
|
||||
|
||||
func stubPushDefault(pushDefault string, err error) func() (string, error) {
|
||||
return func() (string, error) {
|
||||
return pushDefault, err
|
||||
}
|
||||
}
|
||||
|
||||
func stubRemotePushDefault(remotePushDefault string, err error) func() (string, error) {
|
||||
return func() (string, error) {
|
||||
return remotePushDefault, err
|
||||
}
|
||||
}
|
||||
|
||||
func stubParsedPushRevision(parsedPushRevision string, err error) func(string) (string, error) {
|
||||
return func(_ string) (string, error) {
|
||||
return parsedPushRevision, err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
196
pkg/cmd/pr/shared/lister.go
Normal file
196
pkg/cmd/pr/shared/lister.go
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
|
||||
api "github.com/cli/cli/v2/api"
|
||||
)
|
||||
|
||||
type PRLister interface {
|
||||
List(opt ListOptions) (*api.PullRequestAndTotalCount, error)
|
||||
}
|
||||
|
||||
type ListOptions struct {
|
||||
LimitResults int
|
||||
|
||||
State string
|
||||
BaseBranch string
|
||||
HeadBranch string
|
||||
|
||||
Fields []string
|
||||
}
|
||||
|
||||
type lister struct {
|
||||
baseRepoFn func() (ghrepo.Interface, error)
|
||||
httpClient func() (*http.Client, error)
|
||||
}
|
||||
|
||||
func NewLister(factory *cmdutil.Factory) PRLister {
|
||||
return &lister{
|
||||
baseRepoFn: factory.BaseRepo,
|
||||
httpClient: factory.HttpClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lister) List(opts ListOptions) (*api.PullRequestAndTotalCount, error) {
|
||||
repo, err := l.baseRepoFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := l.httpClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return listPullRequests(client, repo, opts)
|
||||
}
|
||||
|
||||
func listPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters ListOptions) (*api.PullRequestAndTotalCount, error) {
|
||||
type response struct {
|
||||
Repository struct {
|
||||
PullRequests struct {
|
||||
Nodes []api.PullRequest
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
TotalCount int
|
||||
}
|
||||
}
|
||||
}
|
||||
limit := filters.LimitResults
|
||||
fragment := fmt.Sprintf("fragment pr on PullRequest{%s}", api.PullRequestGraphQL(filters.Fields))
|
||||
query := fragment + `
|
||||
query PullRequestList(
|
||||
$owner: String!,
|
||||
$repo: String!,
|
||||
$limit: Int!,
|
||||
$endCursor: String,
|
||||
$baseBranch: String,
|
||||
$headBranch: String,
|
||||
$state: [PullRequestState!] = OPEN
|
||||
) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequests(
|
||||
states: $state,
|
||||
baseRefName: $baseBranch,
|
||||
headRefName: $headBranch,
|
||||
first: $limit,
|
||||
after: $endCursor,
|
||||
orderBy: {field: CREATED_AT, direction: DESC}
|
||||
) {
|
||||
totalCount
|
||||
nodes {
|
||||
...pr
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
pageLimit := min(limit, 100)
|
||||
variables := map[string]interface{}{
|
||||
"owner": repo.RepoOwner(),
|
||||
"repo": repo.RepoName(),
|
||||
}
|
||||
|
||||
switch filters.State {
|
||||
case "open":
|
||||
variables["state"] = []string{"OPEN"}
|
||||
case "closed":
|
||||
variables["state"] = []string{"CLOSED", "MERGED"}
|
||||
case "merged":
|
||||
variables["state"] = []string{"MERGED"}
|
||||
case "all":
|
||||
variables["state"] = []string{"OPEN", "CLOSED", "MERGED"}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid state: %s", filters.State)
|
||||
}
|
||||
|
||||
if filters.BaseBranch != "" {
|
||||
variables["baseBranch"] = filters.BaseBranch
|
||||
}
|
||||
if filters.HeadBranch != "" {
|
||||
variables["headBranch"] = filters.HeadBranch
|
||||
}
|
||||
|
||||
res := api.PullRequestAndTotalCount{}
|
||||
var check = make(map[int]struct{})
|
||||
client := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
loop:
|
||||
for {
|
||||
variables["limit"] = pageLimit
|
||||
var data response
|
||||
err := client.GraphQL(repo.RepoHost(), query, variables, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prData := data.Repository.PullRequests
|
||||
res.TotalCount = prData.TotalCount
|
||||
|
||||
for _, pr := range prData.Nodes {
|
||||
if _, exists := check[pr.Number]; exists && pr.Number > 0 {
|
||||
continue
|
||||
}
|
||||
check[pr.Number] = struct{}{}
|
||||
|
||||
res.PullRequests = append(res.PullRequests, pr)
|
||||
if len(res.PullRequests) == limit {
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
if prData.PageInfo.HasNextPage {
|
||||
variables["endCursor"] = prData.PageInfo.EndCursor
|
||||
pageLimit = min(pageLimit, limit-len(res.PullRequests))
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
type mockLister struct {
|
||||
called bool
|
||||
expectFields []string
|
||||
|
||||
result *api.PullRequestAndTotalCount
|
||||
err error
|
||||
}
|
||||
|
||||
func NewMockLister(result *api.PullRequestAndTotalCount, err error) *mockLister {
|
||||
return &mockLister{
|
||||
result: result,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockLister) List(opt ListOptions) (*api.PullRequestAndTotalCount, error) {
|
||||
m.called = true
|
||||
if m.err != nil {
|
||||
return nil, m.err
|
||||
}
|
||||
|
||||
if m.expectFields != nil {
|
||||
|
||||
if !isEqualSet(m.expectFields, opt.Fields) {
|
||||
return nil, fmt.Errorf("unexpected fields: %v", opt.Fields)
|
||||
}
|
||||
}
|
||||
|
||||
return m.result, m.err
|
||||
}
|
||||
|
||||
func (m *mockLister) ExpectFields(fields []string) {
|
||||
m.expectFields = fields
|
||||
}
|
||||
|
|
@ -7,9 +7,9 @@ import (
|
|||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
ghContext "github.com/cli/cli/v2/context"
|
||||
"github.com/cli/cli/v2/git"
|
||||
|
|
@ -52,7 +52,15 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co
|
|||
cmd := &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show status of relevant pull requests",
|
||||
Args: cmdutil.NoArgsQuoteReminder,
|
||||
Long: heredoc.Docf(`
|
||||
Show status of relevant pull requests.
|
||||
|
||||
The status shows a summary of pull requests that includes information such as
|
||||
pull request number, title, CI checks, reviews, etc.
|
||||
|
||||
To see more details of CI checks, run %[1]sgh pr checks%[1]s.
|
||||
`, "`"),
|
||||
Args: cmdutil.NoArgsQuoteReminder,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
|
@ -78,27 +86,57 @@ func statusRun(opts *StatusOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
baseRepo, err := opts.BaseRepo()
|
||||
baseRefRepo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var currentBranch string
|
||||
var currentBranchName string
|
||||
var currentPRNumber int
|
||||
var currentPRHeadRef string
|
||||
var currentHeadRefBranchName string
|
||||
|
||||
if !opts.HasRepoOverride {
|
||||
currentBranch, err = opts.Branch()
|
||||
// We must be in a repo without the override
|
||||
currentBranchName, err = opts.Branch()
|
||||
if err != nil && !errors.Is(err, git.ErrNotOnAnyBranch) {
|
||||
return fmt.Errorf("could not query for pull request for current branch: %w", err)
|
||||
}
|
||||
|
||||
remotes, _ := opts.Remotes()
|
||||
branchConfig, err := opts.GitClient.ReadBranchConfig(ctx, currentBranch)
|
||||
branchConfig, err := opts.GitClient.ReadBranchConfig(ctx, currentBranchName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentPRNumber, currentPRHeadRef, err = prSelectorForCurrentBranch(branchConfig, baseRepo, currentBranch, remotes)
|
||||
// Determine if the branch is configured to merge to a special PR ref
|
||||
prHeadRE := regexp.MustCompile(`^refs/pull/(\d+)/head$`)
|
||||
if m := prHeadRE.FindStringSubmatch(branchConfig.MergeRef); m != nil {
|
||||
currentPRNumber, _ = strconv.Atoi(m[1])
|
||||
}
|
||||
|
||||
if currentPRNumber == 0 {
|
||||
remotes, err := opts.Remotes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Suppressing these errors as we have other means of computing the PullRequestRefs when these fail.
|
||||
parsedPushRevision, _ := opts.GitClient.ParsePushRevision(ctx, currentBranchName)
|
||||
|
||||
remotePushDefault, err := opts.GitClient.RemotePushDefault(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pushDefault, err := opts.GitClient.PushDefault(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prRefs, err := shared.ParsePRRefs(currentBranchName, branchConfig, parsedPushRevision, pushDefault, remotePushDefault, baseRefRepo, remotes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentHeadRefBranchName = prRefs.BranchName
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not query for pull request for current branch: %w", err)
|
||||
}
|
||||
|
|
@ -107,7 +145,7 @@ func statusRun(opts *StatusOptions) error {
|
|||
options := requestOptions{
|
||||
Username: "@me",
|
||||
CurrentPR: currentPRNumber,
|
||||
HeadRef: currentPRHeadRef,
|
||||
HeadRef: currentHeadRefBranchName,
|
||||
ConflictStatus: opts.ConflictStatus,
|
||||
}
|
||||
if opts.Exporter != nil {
|
||||
|
|
@ -116,7 +154,7 @@ func statusRun(opts *StatusOptions) error {
|
|||
|
||||
if opts.Detector == nil {
|
||||
cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
|
||||
opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost())
|
||||
opts.Detector = fd.NewDetector(cachedClient, baseRefRepo.RepoHost())
|
||||
}
|
||||
prFeatures, err := opts.Detector.PullRequestFeatures()
|
||||
if err != nil {
|
||||
|
|
@ -124,7 +162,7 @@ func statusRun(opts *StatusOptions) error {
|
|||
}
|
||||
options.CheckRunAndStatusContextCountsSupported = prFeatures.CheckRunAndStatusContextCounts
|
||||
|
||||
prPayload, err := pullRequestStatus(httpClient, baseRepo, options)
|
||||
prPayload, err := pullRequestStatus(httpClient, baseRefRepo, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -151,21 +189,21 @@ func statusRun(opts *StatusOptions) error {
|
|||
cs := opts.IO.ColorScheme()
|
||||
|
||||
fmt.Fprintln(out, "")
|
||||
fmt.Fprintf(out, "Relevant pull requests in %s\n", ghrepo.FullName(baseRepo))
|
||||
fmt.Fprintf(out, "Relevant pull requests in %s\n", ghrepo.FullName(baseRefRepo))
|
||||
fmt.Fprintln(out, "")
|
||||
|
||||
if !opts.HasRepoOverride {
|
||||
shared.PrintHeader(opts.IO, "Current branch")
|
||||
currentPR := prPayload.CurrentPR
|
||||
if currentPR != nil && currentPR.State != "OPEN" && prPayload.DefaultBranch == currentBranch {
|
||||
if currentPR != nil && currentPR.State != "OPEN" && prPayload.DefaultBranch == currentBranchName {
|
||||
currentPR = nil
|
||||
}
|
||||
if currentPR != nil {
|
||||
printPrs(opts.IO, 1, *currentPR)
|
||||
} else if currentPRHeadRef == "" {
|
||||
} else if currentHeadRefBranchName == "" {
|
||||
shared.PrintMessage(opts.IO, " There is no current branch")
|
||||
} else {
|
||||
shared.PrintMessage(opts.IO, fmt.Sprintf(" There is no pull request associated with %s", cs.Cyan("["+currentPRHeadRef+"]")))
|
||||
shared.PrintMessage(opts.IO, fmt.Sprintf(" There is no pull request associated with %s", cs.Cyan("["+currentHeadRefBranchName+"]")))
|
||||
}
|
||||
fmt.Fprintln(out)
|
||||
}
|
||||
|
|
@ -189,55 +227,6 @@ func statusRun(opts *StatusOptions) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func prSelectorForCurrentBranch(branchConfig git.BranchConfig, baseRepo ghrepo.Interface, prHeadRef string, rem ghContext.Remotes) (int, string, error) {
|
||||
// the branch is configured to merge a special PR head ref
|
||||
prHeadRE := regexp.MustCompile(`^refs/pull/(\d+)/head$`)
|
||||
if m := prHeadRE.FindStringSubmatch(branchConfig.MergeRef); m != nil {
|
||||
prNumber, err := strconv.Atoi(m[1])
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
return prNumber, prHeadRef, nil
|
||||
}
|
||||
|
||||
var branchOwner string
|
||||
if branchConfig.RemoteURL != nil {
|
||||
// the branch merges from a remote specified by URL
|
||||
r, err := ghrepo.FromURL(branchConfig.RemoteURL)
|
||||
if err != nil {
|
||||
// TODO: We aren't returning the error because we discovered that it was shadowed
|
||||
// before refactoring to its current return pattern. Thus, we aren't confident
|
||||
// that returning the error won't break existing behavior.
|
||||
return 0, prHeadRef, nil
|
||||
}
|
||||
branchOwner = r.RepoOwner()
|
||||
} else if branchConfig.RemoteName != "" {
|
||||
// the branch merges from a remote specified by name
|
||||
r, err := rem.FindByName(branchConfig.RemoteName)
|
||||
if err != nil {
|
||||
// TODO: We aren't returning the error because we discovered that it was shadowed
|
||||
// before refactoring to its current return pattern. Thus, we aren't confident
|
||||
// that returning the error won't break existing behavior.
|
||||
return 0, prHeadRef, nil
|
||||
}
|
||||
branchOwner = r.RepoOwner()
|
||||
}
|
||||
|
||||
if branchOwner != "" {
|
||||
selector := prHeadRef
|
||||
if strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") {
|
||||
selector = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/")
|
||||
}
|
||||
// prepend `OWNER:` if this branch is pushed to a fork
|
||||
if !strings.EqualFold(branchOwner, baseRepo.RepoOwner()) {
|
||||
selector = fmt.Sprintf("%s:%s", branchOwner, selector)
|
||||
}
|
||||
return 0, selector, nil
|
||||
}
|
||||
|
||||
return 0, prHeadRef, nil
|
||||
}
|
||||
|
||||
func totalApprovals(pr *api.PullRequest) int {
|
||||
approvals := 0
|
||||
for _, review := range pr.LatestReviews.Nodes {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -96,10 +95,13 @@ func TestPRStatus(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatus.json"))
|
||||
|
||||
// stub successful git command
|
||||
// stub successful git commands
|
||||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
rs.Register(`git config remote.pushDefault`, 0, "")
|
||||
rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "")
|
||||
rs.Register(`git config push.default`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
|
|
@ -130,6 +132,9 @@ func TestPRStatus_reviewsAndChecks(t *testing.T) {
|
|||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
rs.Register(`git config remote.pushDefault`, 0, "")
|
||||
rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "")
|
||||
rs.Register(`git config push.default`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
|
|
@ -160,6 +165,9 @@ func TestPRStatus_reviewsAndChecksWithStatesByCount(t *testing.T) {
|
|||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
rs.Register(`git config remote.pushDefault`, 0, "")
|
||||
rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "")
|
||||
rs.Register(`git config push.default`, 0, "")
|
||||
|
||||
output, err := runCommandWithDetector(http, "blueberries", true, "", &fd.EnabledDetectorMock{})
|
||||
if err != nil {
|
||||
|
|
@ -189,6 +197,9 @@ func TestPRStatus_currentBranch_showTheMostRecentPR(t *testing.T) {
|
|||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
rs.Register(`git config remote.pushDefault`, 0, "")
|
||||
rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "")
|
||||
rs.Register(`git config push.default`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
|
|
@ -222,6 +233,9 @@ func TestPRStatus_currentBranch_defaultBranch(t *testing.T) {
|
|||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
rs.Register(`git config remote.pushDefault`, 0, "")
|
||||
rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "")
|
||||
rs.Register(`git config push.default`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
|
|
@ -261,6 +275,9 @@ func TestPRStatus_currentBranch_Closed(t *testing.T) {
|
|||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
rs.Register(`git config remote.pushDefault`, 0, "")
|
||||
rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "")
|
||||
rs.Register(`git config push.default`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
|
|
@ -283,6 +300,9 @@ func TestPRStatus_currentBranch_Closed_defaultBranch(t *testing.T) {
|
|||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
rs.Register(`git config remote.pushDefault`, 0, "")
|
||||
rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "")
|
||||
rs.Register(`git config push.default`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
|
|
@ -305,6 +325,9 @@ func TestPRStatus_currentBranch_Merged(t *testing.T) {
|
|||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
rs.Register(`git config remote.pushDefault`, 0, "")
|
||||
rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "")
|
||||
rs.Register(`git config push.default`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
|
|
@ -327,6 +350,9 @@ func TestPRStatus_currentBranch_Merged_defaultBranch(t *testing.T) {
|
|||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
rs.Register(`git config remote.pushDefault`, 0, "")
|
||||
rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "")
|
||||
rs.Register(`git config push.default`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
|
|
@ -349,6 +375,9 @@ func TestPRStatus_blankSlate(t *testing.T) {
|
|||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
rs.Register(`git config remote.pushDefault`, 0, "")
|
||||
rs.Register(`git rev-parse --abbrev-ref blueberries@{push}`, 0, "")
|
||||
rs.Register(`git config push.default`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
|
|
@ -407,6 +436,9 @@ func TestPRStatus_detachedHead(t *testing.T) {
|
|||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
rs.Register(`git config remote.pushDefault`, 0, "")
|
||||
rs.Register(`git rev-parse --abbrev-ref @{push}`, 0, "")
|
||||
rs.Register(`git config push.default`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "", true, "")
|
||||
if err != nil {
|
||||
|
|
@ -434,216 +466,8 @@ Requesting a code review from you
|
|||
func TestPRStatus_error_ReadBranchConfig(t *testing.T) {
|
||||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 1, "")
|
||||
|
||||
// We only need the one stub because this fails early
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 2, "")
|
||||
_, err := runCommand(initFakeHTTP(), "blueberries", true, "")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func Test_prSelectorForCurrentBranch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
branchConfig git.BranchConfig
|
||||
baseRepo ghrepo.Interface
|
||||
prHeadRef string
|
||||
remotes context.Remotes
|
||||
wantPrNumber int
|
||||
wantSelector string
|
||||
wantError error
|
||||
}{
|
||||
{
|
||||
name: "Empty branch config",
|
||||
branchConfig: git.BranchConfig{},
|
||||
prHeadRef: "monalisa/main",
|
||||
wantPrNumber: 0,
|
||||
wantSelector: "monalisa/main",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "The branch is configured to merge a special PR head ref",
|
||||
branchConfig: git.BranchConfig{
|
||||
MergeRef: "refs/pull/42/head",
|
||||
},
|
||||
prHeadRef: "monalisa/main",
|
||||
wantPrNumber: 42,
|
||||
wantSelector: "monalisa/main",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "Branch merges from a remote specified by URL",
|
||||
branchConfig: git.BranchConfig{
|
||||
RemoteURL: &url.URL{
|
||||
Scheme: "ssh",
|
||||
User: url.User("git"),
|
||||
Host: "github.com",
|
||||
Path: "monalisa/playground.git",
|
||||
},
|
||||
},
|
||||
baseRepo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
prHeadRef: "monalisa/main",
|
||||
remotes: context.Remotes{
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
},
|
||||
},
|
||||
wantPrNumber: 0,
|
||||
wantSelector: "monalisa/main",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "Branch merges from a remote specified by name",
|
||||
branchConfig: git.BranchConfig{
|
||||
RemoteName: "upstream",
|
||||
},
|
||||
baseRepo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
prHeadRef: "monalisa/main",
|
||||
remotes: context.Remotes{
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.NewWithHost("forkName", "playground", "github.com"),
|
||||
},
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "upstream"},
|
||||
Repo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
},
|
||||
},
|
||||
wantPrNumber: 0,
|
||||
wantSelector: "monalisa/main",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "Branch is a fork and merges from a remote specified by URL",
|
||||
branchConfig: git.BranchConfig{
|
||||
RemoteURL: &url.URL{
|
||||
Scheme: "ssh",
|
||||
User: url.User("git"),
|
||||
Host: "github.com",
|
||||
Path: "forkName/playground.git",
|
||||
},
|
||||
MergeRef: "refs/heads/main",
|
||||
},
|
||||
baseRepo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
prHeadRef: "monalisa/main",
|
||||
remotes: context.Remotes{
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.NewWithHost("forkName", "playground", "github.com"),
|
||||
},
|
||||
},
|
||||
wantPrNumber: 0,
|
||||
wantSelector: "forkName:main",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "Branch is a fork and merges from a remote specified by name",
|
||||
branchConfig: git.BranchConfig{
|
||||
RemoteName: "origin",
|
||||
},
|
||||
baseRepo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
prHeadRef: "monalisa/main",
|
||||
remotes: context.Remotes{
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.NewWithHost("forkName", "playground", "github.com"),
|
||||
},
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "upstream"},
|
||||
Repo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
},
|
||||
},
|
||||
wantPrNumber: 0,
|
||||
wantSelector: "forkName:monalisa/main",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "Branch specifies a mergeRef and merges from a remote specified by name",
|
||||
branchConfig: git.BranchConfig{
|
||||
RemoteName: "upstream",
|
||||
MergeRef: "refs/heads/main",
|
||||
},
|
||||
baseRepo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
prHeadRef: "monalisa/main",
|
||||
remotes: context.Remotes{
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.NewWithHost("forkName", "playground", "github.com"),
|
||||
},
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "upstream"},
|
||||
Repo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
},
|
||||
},
|
||||
wantPrNumber: 0,
|
||||
wantSelector: "main",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "Branch is a fork, specifies a mergeRef, and merges from a remote specified by name",
|
||||
branchConfig: git.BranchConfig{
|
||||
RemoteName: "origin",
|
||||
MergeRef: "refs/heads/main",
|
||||
},
|
||||
baseRepo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
prHeadRef: "monalisa/main",
|
||||
remotes: context.Remotes{
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.NewWithHost("forkName", "playground", "github.com"),
|
||||
},
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "upstream"},
|
||||
Repo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
},
|
||||
},
|
||||
wantPrNumber: 0,
|
||||
wantSelector: "forkName:main",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "Remote URL errors",
|
||||
branchConfig: git.BranchConfig{
|
||||
RemoteURL: &url.URL{
|
||||
Scheme: "ssh",
|
||||
User: url.User("git"),
|
||||
Host: "github.com",
|
||||
Path: "/\\invalid?Path/",
|
||||
},
|
||||
},
|
||||
prHeadRef: "monalisa/main",
|
||||
wantPrNumber: 0,
|
||||
wantSelector: "monalisa/main",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "Remote Name errors",
|
||||
branchConfig: git.BranchConfig{
|
||||
RemoteName: "nonexistentRemote",
|
||||
},
|
||||
prHeadRef: "monalisa/main",
|
||||
remotes: context.Remotes{
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.NewWithHost("forkName", "playground", "github.com"),
|
||||
},
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "upstream"},
|
||||
Repo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
},
|
||||
},
|
||||
wantPrNumber: 0,
|
||||
wantSelector: "monalisa/main",
|
||||
wantError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
prNum, headRef, err := prSelectorForCurrentBranch(tt.branchConfig, tt.baseRepo, tt.prHeadRef, tt.remotes)
|
||||
assert.Equal(t, tt.wantPrNumber, prNum)
|
||||
assert.Equal(t, tt.wantSelector, headRef)
|
||||
assert.Equal(t, tt.wantError, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ type editItemOpts struct {
|
|||
fieldID string
|
||||
projectID string
|
||||
text string
|
||||
number float32
|
||||
number float64
|
||||
numberChanged bool
|
||||
date string
|
||||
singleSelectOptionID string
|
||||
iterationID string
|
||||
|
|
@ -63,7 +64,7 @@ func NewCmdEditItem(f *cmdutil.Factory, runF func(config editItemConfig) error)
|
|||
Short: "Edit an item in a project",
|
||||
Long: heredoc.Docf(`
|
||||
Edit either a draft issue or a project item. Both usages require the ID of the item to edit.
|
||||
|
||||
|
||||
For non-draft issues, the ID of the project is also required, and only a single field value can be updated per invocation.
|
||||
|
||||
Remove project item field value using %[1]s--clear%[1]s flag.
|
||||
|
|
@ -76,10 +77,11 @@ func NewCmdEditItem(f *cmdutil.Factory, runF func(config editItemConfig) error)
|
|||
gh project item-edit --id <item-ID> --field-id <field-ID> --project-id <project-ID> --clear
|
||||
`),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.numberChanged = cmd.Flags().Changed("number")
|
||||
if err := cmdutil.MutuallyExclusive(
|
||||
"only one of `--text`, `--number`, `--date`, `--single-select-option-id` or `--iteration-id` may be used",
|
||||
opts.text != "",
|
||||
opts.number != 0,
|
||||
opts.numberChanged,
|
||||
opts.date != "",
|
||||
opts.singleSelectOptionID != "",
|
||||
opts.iterationID != "",
|
||||
|
|
@ -89,7 +91,7 @@ func NewCmdEditItem(f *cmdutil.Factory, runF func(config editItemConfig) error)
|
|||
|
||||
if err := cmdutil.MutuallyExclusive(
|
||||
"cannot use `--text`, `--number`, `--date`, `--single-select-option-id` or `--iteration-id` in conjunction with `--clear`",
|
||||
opts.text != "" || opts.number != 0 || opts.date != "" || opts.singleSelectOptionID != "" || opts.iterationID != "",
|
||||
opts.text != "" || opts.numberChanged || opts.date != "" || opts.singleSelectOptionID != "" || opts.iterationID != "",
|
||||
opts.clear,
|
||||
); err != nil {
|
||||
return err
|
||||
|
|
@ -123,7 +125,7 @@ func NewCmdEditItem(f *cmdutil.Factory, runF func(config editItemConfig) error)
|
|||
editItemCmd.Flags().StringVar(&opts.fieldID, "field-id", "", "ID of the field to update")
|
||||
editItemCmd.Flags().StringVar(&opts.projectID, "project-id", "", "ID of the project to which the field belongs to")
|
||||
editItemCmd.Flags().StringVar(&opts.text, "text", "", "Text value for the field")
|
||||
editItemCmd.Flags().Float32Var(&opts.number, "number", 0, "Number value for the field")
|
||||
editItemCmd.Flags().Float64Var(&opts.number, "number", 0, "Number value for the field")
|
||||
editItemCmd.Flags().StringVar(&opts.date, "date", "", "Date value for the field (YYYY-MM-DD)")
|
||||
editItemCmd.Flags().StringVar(&opts.singleSelectOptionID, "single-select-option-id", "", "ID of the single select option value to set on the field")
|
||||
editItemCmd.Flags().StringVar(&opts.iterationID, "iteration-id", "", "ID of the iteration value to set on the field")
|
||||
|
|
@ -146,7 +148,7 @@ func runEditItem(config editItemConfig) error {
|
|||
}
|
||||
|
||||
// update item values
|
||||
if config.opts.text != "" || config.opts.number != 0 || config.opts.date != "" || config.opts.singleSelectOptionID != "" || config.opts.iterationID != "" {
|
||||
if config.opts.text != "" || config.opts.numberChanged || config.opts.date != "" || config.opts.singleSelectOptionID != "" || config.opts.iterationID != "" {
|
||||
return updateItemValues(config)
|
||||
}
|
||||
|
||||
|
|
@ -172,7 +174,7 @@ func buildUpdateItem(config editItemConfig, date time.Time) (*UpdateProjectV2Fie
|
|||
value = githubv4.ProjectV2FieldValue{
|
||||
Text: githubv4.NewString(githubv4.String(config.opts.text)),
|
||||
}
|
||||
} else if config.opts.number != 0 {
|
||||
} else if config.opts.numberChanged {
|
||||
value = githubv4.ProjectV2FieldValue{
|
||||
Number: githubv4.NewFloat(githubv4.Float(config.opts.number)),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,22 @@ func TestNewCmdeditItem(t *testing.T) {
|
|||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "number with floating point value",
|
||||
cli: "--number 123.45 --id 123",
|
||||
wants: editItemOpts{
|
||||
number: 123.45,
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "number zero",
|
||||
cli: "--number 0 --id 123",
|
||||
wants: editItemOpts{
|
||||
number: 0,
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "field-id",
|
||||
cli: "--field-id FIELD_ID --id 123",
|
||||
|
|
@ -255,7 +271,7 @@ func TestRunItemEdit_Number(t *testing.T) {
|
|||
// edit item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation UpdateItemValues.*","variables":{"input":{"projectId":"project_id","itemId":"item_id","fieldId":"field_id","value":{"number":2}}}}`).
|
||||
BodyString(`{"query":"mutation UpdateItemValues.*","variables":{"input":{"projectId":"project_id","itemId":"item_id","fieldId":"field_id","value":{"number":123.45}}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
|
|
@ -284,10 +300,64 @@ func TestRunItemEdit_Number(t *testing.T) {
|
|||
config := editItemConfig{
|
||||
io: ios,
|
||||
opts: editItemOpts{
|
||||
number: 2,
|
||||
itemID: "item_id",
|
||||
projectID: "project_id",
|
||||
fieldID: "field_id",
|
||||
number: 123.45,
|
||||
numberChanged: true,
|
||||
itemID: "item_id",
|
||||
projectID: "project_id",
|
||||
fieldID: "field_id",
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runEditItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Edited item \"title\"\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunItemEdit_NumberZero(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// edit item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation UpdateItemValues.*","variables":{"input":{"projectId":"project_id","itemId":"item_id","fieldId":"field_id","value":{"number":0}}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"updateProjectV2ItemFieldValue": map[string]interface{}{
|
||||
"projectV2Item": map[string]interface{}{
|
||||
"ID": "item_id",
|
||||
"content": map[string]interface{}{
|
||||
"__typename": "Issue",
|
||||
"body": "body",
|
||||
"title": "title",
|
||||
"number": 1,
|
||||
"repository": map[string]interface{}{
|
||||
"nameWithOwner": "my-repo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
|
||||
config := editItemConfig{
|
||||
io: ios,
|
||||
opts: editItemOpts{
|
||||
number: 0,
|
||||
numberChanged: true,
|
||||
itemID: "item_id",
|
||||
projectID: "project_id",
|
||||
fieldID: "field_id",
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -295,6 +295,7 @@ func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldIterationValue(t *testing.
|
|||
iterationFieldValue := FieldValueNodes{Type: "ProjectV2ItemFieldIterationValue"}
|
||||
iterationFieldValue.ProjectV2ItemFieldIterationValue.Title = "Iteration Title"
|
||||
iterationFieldValue.ProjectV2ItemFieldIterationValue.Field = iterationField
|
||||
iterationFieldValue.ProjectV2ItemFieldIterationValue.IterationId = "iterationId"
|
||||
|
||||
draftIssue := ProjectItem{
|
||||
Id: "draftIssueId",
|
||||
|
|
@ -321,7 +322,7 @@ func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldIterationValue(t *testing.
|
|||
assert.NoError(t, err)
|
||||
assert.JSONEq(
|
||||
t,
|
||||
`{"items":[{"sprint":{"title":"Iteration Title","startDate":"","duration":0},"content":{"type":"DraftIssue","body":"a body","title":"Pull Request title","id":"draftIssueId"},"id":"draftIssueId"}],"totalCount":5}`,
|
||||
`{"items":[{"sprint":{"title":"Iteration Title","startDate":"","duration":0,"iterationId":"iterationId"},"content":{"type":"DraftIssue","body":"a body","title":"Pull Request title","id":"draftIssueId"},"id":"draftIssueId"}],"totalCount":5}`,
|
||||
string(out))
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -235,10 +235,11 @@ type FieldValueNodes struct {
|
|||
Field ProjectField
|
||||
} `graphql:"... on ProjectV2ItemFieldDateValue"`
|
||||
ProjectV2ItemFieldIterationValue struct {
|
||||
Title string
|
||||
StartDate string
|
||||
Duration int
|
||||
Field ProjectField
|
||||
Title string
|
||||
StartDate string
|
||||
Duration int
|
||||
Field ProjectField
|
||||
IterationId string
|
||||
} `graphql:"... on ProjectV2ItemFieldIterationValue"`
|
||||
ProjectV2ItemFieldLabelValue struct {
|
||||
Labels struct {
|
||||
|
|
@ -249,7 +250,7 @@ type FieldValueNodes struct {
|
|||
Field ProjectField
|
||||
} `graphql:"... on ProjectV2ItemFieldLabelValue"`
|
||||
ProjectV2ItemFieldNumberValue struct {
|
||||
Number float32
|
||||
Number float64
|
||||
Field ProjectField
|
||||
} `graphql:"... on ProjectV2ItemFieldNumberValue"`
|
||||
ProjectV2ItemFieldSingleSelectValue struct {
|
||||
|
|
@ -1488,9 +1489,10 @@ func projectFieldValueData(v FieldValueNodes) interface{} {
|
|||
return v.ProjectV2ItemFieldDateValue.Date
|
||||
case "ProjectV2ItemFieldIterationValue":
|
||||
return map[string]interface{}{
|
||||
"title": v.ProjectV2ItemFieldIterationValue.Title,
|
||||
"startDate": v.ProjectV2ItemFieldIterationValue.StartDate,
|
||||
"duration": v.ProjectV2ItemFieldIterationValue.Duration,
|
||||
"title": v.ProjectV2ItemFieldIterationValue.Title,
|
||||
"startDate": v.ProjectV2ItemFieldIterationValue.StartDate,
|
||||
"duration": v.ProjectV2ItemFieldIterationValue.Duration,
|
||||
"iterationId": v.ProjectV2ItemFieldIterationValue.IterationId,
|
||||
}
|
||||
case "ProjectV2ItemFieldNumberValue":
|
||||
return v.ProjectV2ItemFieldNumberValue.Number
|
||||
|
|
|
|||
|
|
@ -395,8 +395,9 @@ func TestProjectItems_FieldTitle(t *testing.T) {
|
|||
"fieldValues": map[string]interface{}{
|
||||
"nodes": []map[string]interface{}{
|
||||
{
|
||||
"__typename": "ProjectV2ItemFieldIterationValue",
|
||||
"title": "Iteration Title 1",
|
||||
"__typename": "ProjectV2ItemFieldIterationValue",
|
||||
"title": "Iteration Title 1",
|
||||
"iterationId": "iterationId1",
|
||||
},
|
||||
{
|
||||
"__typename": "ProjectV2ItemFieldMilestoneValue",
|
||||
|
|
@ -426,6 +427,7 @@ func TestProjectItems_FieldTitle(t *testing.T) {
|
|||
assert.Len(t, project.Items.Nodes, 1)
|
||||
assert.Len(t, project.Items.Nodes[0].FieldValues.Nodes, 2)
|
||||
assert.Equal(t, project.Items.Nodes[0].FieldValues.Nodes[0].ProjectV2ItemFieldIterationValue.Title, "Iteration Title 1")
|
||||
assert.Equal(t, project.Items.Nodes[0].FieldValues.Nodes[0].ProjectV2ItemFieldIterationValue.IterationId, "iterationId1")
|
||||
assert.Equal(t, project.Items.Nodes[0].FieldValues.Nodes[1].ProjectV2ItemFieldMilestoneValue.Milestone.Title, "Milestone Title 1")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ type CreateOptions struct {
|
|||
NotesStartTag string
|
||||
VerifyTag bool
|
||||
NotesFromTag bool
|
||||
FailOnNoCommits bool
|
||||
}
|
||||
|
||||
func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
|
||||
|
|
@ -99,6 +100,11 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
When using automatically generated release notes, a release title will also be automatically
|
||||
generated unless a title was explicitly passed. Additional release notes can be prepended to
|
||||
automatically generated notes by using the %[1]s--notes%[1]s flag.
|
||||
|
||||
By default, the release is created even if there are no new commits since the last release.
|
||||
This may result in the same or duplicate release which may not be desirable in some cases.
|
||||
Use %[1]s--fail-on-no-commits%[1]s to fail if no new commits are available. This flag has no
|
||||
effect if there are no existing releases or this is the very first release.
|
||||
`, "`"),
|
||||
Example: heredoc.Doc(`
|
||||
Interactively create a release
|
||||
|
|
@ -120,7 +126,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
$ gh release create v1.2.3 --notes-from-tag
|
||||
|
||||
Don't mark the release as latest
|
||||
$ gh release create v1.2.3 --latest=false
|
||||
$ gh release create v1.2.3 --latest=false
|
||||
|
||||
Upload all tarballs in a directory as release assets
|
||||
$ gh release create v1.2.3 ./dist/*.tgz
|
||||
|
|
@ -130,6 +136,9 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
|
||||
Create a release and start a discussion
|
||||
$ gh release create v1.2.3 --discussion-category "General"
|
||||
|
||||
# Create a release only if there are new commits available since the last release
|
||||
$ gh release create v1.2.3 --fail-on-no-commits
|
||||
`),
|
||||
Aliases: []string{"new"},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
|
@ -194,6 +203,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
cmdutil.NilBoolFlag(cmd, &opts.IsLatest, "latest", "", "Mark this release as \"Latest\" (default [automatic based on date and version]). --latest=false to explicitly NOT set as latest")
|
||||
cmd.Flags().BoolVarP(&opts.VerifyTag, "verify-tag", "", false, "Abort in case the git tag doesn't already exist in the remote repository")
|
||||
cmd.Flags().BoolVarP(&opts.NotesFromTag, "notes-from-tag", "", false, "Automatically generate notes from annotated tag")
|
||||
cmd.Flags().BoolVar(&opts.FailOnNoCommits, "fail-on-no-commits", false, "Fail if there are no commits since the last release (no impact on the first release)")
|
||||
|
||||
_ = cmdutil.RegisterBranchCompletionFlags(f.GitClient, cmd, "target")
|
||||
|
||||
|
|
@ -211,6 +221,16 @@ func createRun(opts *CreateOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if opts.FailOnNoCommits {
|
||||
isNew, err := isNewRelease(httpClient, baseRepo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check whether there were new commits since last release: %v", err)
|
||||
}
|
||||
if !isNew {
|
||||
return fmt.Errorf("no new commits since the last release")
|
||||
}
|
||||
}
|
||||
|
||||
var existingTag bool
|
||||
if opts.TagName == "" {
|
||||
tags, err := getTags(httpClient, baseRepo, 5)
|
||||
|
|
|
|||
|
|
@ -52,17 +52,18 @@ func Test_NewCmdCreate(t *testing.T) {
|
|||
args: "",
|
||||
isTTY: true,
|
||||
want: CreateOptions{
|
||||
TagName: "",
|
||||
Target: "",
|
||||
Name: "",
|
||||
Body: "",
|
||||
BodyProvided: false,
|
||||
Draft: false,
|
||||
Prerelease: false,
|
||||
RepoOverride: "",
|
||||
Concurrency: 5,
|
||||
Assets: []*shared.AssetForUpload(nil),
|
||||
VerifyTag: false,
|
||||
TagName: "",
|
||||
Target: "",
|
||||
Name: "",
|
||||
Body: "",
|
||||
BodyProvided: false,
|
||||
Draft: false,
|
||||
Prerelease: false,
|
||||
RepoOverride: "",
|
||||
Concurrency: 5,
|
||||
Assets: []*shared.AssetForUpload(nil),
|
||||
VerifyTag: false,
|
||||
FailOnNoCommits: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -76,17 +77,18 @@ func Test_NewCmdCreate(t *testing.T) {
|
|||
args: "v1.2.3",
|
||||
isTTY: true,
|
||||
want: CreateOptions{
|
||||
TagName: "v1.2.3",
|
||||
Target: "",
|
||||
Name: "",
|
||||
Body: "",
|
||||
BodyProvided: false,
|
||||
Draft: false,
|
||||
Prerelease: false,
|
||||
RepoOverride: "",
|
||||
Concurrency: 5,
|
||||
Assets: []*shared.AssetForUpload(nil),
|
||||
VerifyTag: false,
|
||||
TagName: "v1.2.3",
|
||||
Target: "",
|
||||
Name: "",
|
||||
Body: "",
|
||||
BodyProvided: false,
|
||||
Draft: false,
|
||||
Prerelease: false,
|
||||
RepoOverride: "",
|
||||
Concurrency: 5,
|
||||
Assets: []*shared.AssetForUpload(nil),
|
||||
VerifyTag: false,
|
||||
FailOnNoCommits: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -347,6 +349,19 @@ func Test_NewCmdCreate(t *testing.T) {
|
|||
isTTY: false,
|
||||
wantErr: "using `--notes-from-tag` with `--generate-notes` or `--notes-start-tag` is not supported",
|
||||
},
|
||||
{
|
||||
name: "with --fail-on-no-commits",
|
||||
args: "v1.2.3 --fail-on-no-commits",
|
||||
isTTY: false,
|
||||
want: CreateOptions{
|
||||
TagName: "v1.2.3",
|
||||
BodyProvided: false,
|
||||
Concurrency: 5,
|
||||
Assets: []*shared.AssetForUpload(nil),
|
||||
NotesFromTag: false,
|
||||
FailOnNoCommits: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
@ -402,6 +417,7 @@ func Test_NewCmdCreate(t *testing.T) {
|
|||
assert.Equal(t, tt.want.IsLatest, opts.IsLatest)
|
||||
assert.Equal(t, tt.want.VerifyTag, opts.VerifyTag)
|
||||
assert.Equal(t, tt.want.NotesFromTag, opts.NotesFromTag)
|
||||
assert.Equal(t, tt.want.FailOnNoCommits, opts.FailOnNoCommits)
|
||||
|
||||
require.Equal(t, len(tt.want.Assets), len(opts.Assets))
|
||||
for i := range tt.want.Assets {
|
||||
|
|
@ -460,6 +476,100 @@ func Test_createRun(t *testing.T) {
|
|||
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
|
||||
wantStderr: ``,
|
||||
},
|
||||
{
|
||||
name: "create a release if there are new commits and the last release does not exist",
|
||||
isTTY: true,
|
||||
opts: CreateOptions{
|
||||
TagName: "v1.2.3",
|
||||
Name: "The Big 1.2",
|
||||
Body: "* Fixed bugs",
|
||||
BodyProvided: true,
|
||||
Target: "",
|
||||
FailOnNoCommits: true,
|
||||
},
|
||||
runStubs: defaultRunStubs,
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/releases/latest"), httpmock.StatusStringResponse(404, `{
|
||||
"message": "Not Found",
|
||||
"documentation_url": "https://docs.github.com/rest/releases/releases#get-the-latest-release",
|
||||
"status": "404"
|
||||
}`))
|
||||
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
|
||||
"url": "https://api.github.com/releases/123",
|
||||
"upload_url": "https://api.github.com/assets/upload",
|
||||
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
|
||||
}`, func(params map[string]interface{}) {
|
||||
assert.Equal(t, map[string]interface{}{
|
||||
"tag_name": "v1.2.3",
|
||||
"name": "The Big 1.2",
|
||||
"body": "* Fixed bugs",
|
||||
"draft": false,
|
||||
"prerelease": false,
|
||||
}, params)
|
||||
}))
|
||||
},
|
||||
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
|
||||
wantStderr: ``,
|
||||
},
|
||||
{
|
||||
name: "create a release if there are new commits and the last release exists",
|
||||
isTTY: true,
|
||||
opts: CreateOptions{
|
||||
TagName: "v1.2.3",
|
||||
Name: "The Big 1.2",
|
||||
Body: "* Fixed bugs",
|
||||
BodyProvided: true,
|
||||
Target: "",
|
||||
FailOnNoCommits: true,
|
||||
},
|
||||
runStubs: defaultRunStubs,
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/releases/latest"), httpmock.StatusStringResponse(200, `{
|
||||
"tag_name": "v1.2.2"
|
||||
}`))
|
||||
reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/compare/v1.2.2...HEAD"), httpmock.StatusStringResponse(200, `{
|
||||
"status": "ahead"
|
||||
}`))
|
||||
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
|
||||
"url": "https://api.github.com/releases/123",
|
||||
"upload_url": "https://api.github.com/assets/upload",
|
||||
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
|
||||
}`, func(params map[string]interface{}) {
|
||||
assert.Equal(t, map[string]interface{}{
|
||||
"tag_name": "v1.2.3",
|
||||
"name": "The Big 1.2",
|
||||
"body": "* Fixed bugs",
|
||||
"draft": false,
|
||||
"prerelease": false,
|
||||
}, params)
|
||||
}))
|
||||
},
|
||||
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
|
||||
wantStderr: ``,
|
||||
},
|
||||
{
|
||||
name: "create a release if there are no new commits but the last release exists",
|
||||
isTTY: true,
|
||||
opts: CreateOptions{
|
||||
TagName: "v1.2.3",
|
||||
Name: "The Big 1.2",
|
||||
Body: "* Fixed bugs",
|
||||
BodyProvided: true,
|
||||
Target: "",
|
||||
FailOnNoCommits: true,
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/releases/latest"), httpmock.StatusStringResponse(200, `{
|
||||
"tag_name": "v1.2.2"
|
||||
}`))
|
||||
reg.Register(httpmock.REST("GET", "repos/OWNER/REPO/compare/v1.2.2...HEAD"), httpmock.StatusStringResponse(200, `{
|
||||
"status": "identical"
|
||||
}`))
|
||||
},
|
||||
wantErr: "no new commits since the last release",
|
||||
wantStdout: "",
|
||||
wantStderr: ``,
|
||||
},
|
||||
{
|
||||
name: "with discussion category",
|
||||
isTTY: true,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package create
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -299,3 +300,31 @@ func tokenHasWorkflowScope(resp *http.Response) bool {
|
|||
|
||||
return slices.Contains(strings.Split(scopes, ","), "workflow")
|
||||
}
|
||||
|
||||
// isNewRelease checks if there are new commits since the latest release.
|
||||
func isNewRelease(httpClient *http.Client, repo ghrepo.Interface) (bool, error) {
|
||||
ctx := context.Background()
|
||||
release, err := shared.FetchLatestRelease(ctx, httpClient, repo)
|
||||
if err != nil {
|
||||
if errors.Is(err, shared.ErrReleaseNotFound) {
|
||||
return true, nil
|
||||
} else {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
tagName := release.TagName
|
||||
path := fmt.Sprintf("repos/%s/%s/compare/%s...HEAD?per_page=1", repo.RepoOwner(), repo.RepoName(), tagName)
|
||||
|
||||
var comparisonStatus struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
if err := apiClient.REST(repo.RepoHost(), "GET", path, nil, &comparisonStatus); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
isNew := comparisonStatus.Status == "ahead"
|
||||
return isNew, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ func (rel *Release) ExportData(fields []string) map[string]interface{} {
|
|||
return data
|
||||
}
|
||||
|
||||
var errNotFound = errors.New("release not found")
|
||||
var ErrReleaseNotFound = errors.New("release not found")
|
||||
|
||||
type fetchResult struct {
|
||||
release *Release
|
||||
|
|
@ -150,7 +150,7 @@ func FetchRelease(ctx context.Context, httpClient *http.Client, repo ghrepo.Inte
|
|||
}()
|
||||
|
||||
res := <-results
|
||||
if errors.Is(res.error, errNotFound) {
|
||||
if errors.Is(res.error, ErrReleaseNotFound) {
|
||||
res = <-results
|
||||
cancel() // satisfy the linter even though no goroutines are running anymore
|
||||
} else {
|
||||
|
|
@ -190,7 +190,7 @@ func fetchDraftRelease(ctx context.Context, httpClient *http.Client, repo ghrepo
|
|||
}
|
||||
|
||||
if query.Repository.Release == nil || !query.Repository.Release.IsDraft {
|
||||
return nil, errNotFound
|
||||
return nil, ErrReleaseNotFound
|
||||
}
|
||||
|
||||
// Then, use REST to get information about the draft release. In theory, we could have fetched
|
||||
|
|
@ -213,7 +213,7 @@ func fetchReleasePath(ctx context.Context, httpClient *http.Client, host string,
|
|||
|
||||
if resp.StatusCode == 404 {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
return nil, errNotFound
|
||||
return nil, ErrReleaseNotFound
|
||||
} else if resp.StatusCode > 299 {
|
||||
return nil, api.HandleHTTPError(resp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,15 +144,15 @@ func Test_viewRun(t *testing.T) {
|
|||
wantStdout: heredoc.Doc(`
|
||||
v1.2.3
|
||||
MonaLisa released this about 1 day ago
|
||||
|
||||
|
||||
|
||||
• Fixed bugs
|
||||
|
||||
|
||||
|
||||
|
||||
Assets
|
||||
windows.zip 12 B
|
||||
linux.tgz 34 B
|
||||
|
||||
|
||||
View on GitHub: https://github.com/OWNER/REPO/releases/tags/v1.2.3
|
||||
`),
|
||||
wantStderr: ``,
|
||||
|
|
@ -168,15 +168,15 @@ func Test_viewRun(t *testing.T) {
|
|||
wantStdout: heredoc.Doc(`
|
||||
v1.2.3
|
||||
MonaLisa released this about 1 day ago
|
||||
|
||||
|
||||
|
||||
• Fixed bugs
|
||||
|
||||
|
||||
|
||||
|
||||
Assets
|
||||
windows.zip 12 B
|
||||
linux.tgz 34 B
|
||||
|
||||
|
||||
View on GitHub: https://github.com/OWNER/REPO/releases/tags/v1.2.3
|
||||
`),
|
||||
wantStderr: ``,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ package autolink
|
|||
|
||||
import (
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
cmdCreate "github.com/cli/cli/v2/pkg/cmd/repo/autolink/create"
|
||||
cmdDelete "github.com/cli/cli/v2/pkg/cmd/repo/autolink/delete"
|
||||
cmdList "github.com/cli/cli/v2/pkg/cmd/repo/autolink/list"
|
||||
cmdView "github.com/cli/cli/v2/pkg/cmd/repo/autolink/view"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -12,18 +15,19 @@ func NewCmdAutolink(f *cmdutil.Factory) *cobra.Command {
|
|||
Use: "autolink <command>",
|
||||
Short: "Manage autolink references",
|
||||
Long: heredoc.Docf(`
|
||||
Work with GitHub autolink references.
|
||||
|
||||
GitHub autolinks require admin access to configure and can be found at
|
||||
https://github.com/{owner}/{repo}/settings/key_links.
|
||||
Use %[1]sgh repo autolink list --web%[1]s to open this page for the current repository.
|
||||
|
||||
For more information about GitHub autolinks, see https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/configuring-autolinks-to-reference-external-resources
|
||||
`, "`"),
|
||||
Autolinks link issues, pull requests, commit messages, and release descriptions to external third-party services.
|
||||
|
||||
Autolinks require %[1]sadmin%[1]s role to view or manage.
|
||||
|
||||
For more information, see <https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/configuring-autolinks-to-reference-external-resources>
|
||||
`, "`"),
|
||||
}
|
||||
cmdutil.EnableRepoOverride(cmd, f)
|
||||
|
||||
cmd.AddCommand(cmdList.NewCmdList(f, nil))
|
||||
cmd.AddCommand(cmdCreate.NewCmdCreate(f, nil))
|
||||
cmd.AddCommand(cmdView.NewCmdView(f, nil))
|
||||
cmd.AddCommand(cmdDelete.NewCmdDelete(f, nil))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
118
pkg/cmd/repo/autolink/create/create.go
Normal file
118
pkg/cmd/repo/autolink/create/create.go
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
package create
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type createOptions struct {
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Browser browser.Browser
|
||||
AutolinkClient AutolinkCreateClient
|
||||
IO *iostreams.IOStreams
|
||||
Exporter cmdutil.Exporter
|
||||
|
||||
KeyPrefix string
|
||||
URLTemplate string
|
||||
Numeric bool
|
||||
}
|
||||
|
||||
type AutolinkCreateClient interface {
|
||||
Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*shared.Autolink, error)
|
||||
}
|
||||
|
||||
func NewCmdCreate(f *cmdutil.Factory, runF func(*createOptions) error) *cobra.Command {
|
||||
opts := &createOptions{
|
||||
Browser: f.Browser,
|
||||
IO: f.IOStreams,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create <keyPrefix> <urlTemplate>",
|
||||
Short: "Create a new autolink reference",
|
||||
Long: heredoc.Docf(`
|
||||
Create a new autolink reference for a repository.
|
||||
|
||||
The %[1]skeyPrefix%[1]s argument specifies the prefix that will generate a link when it is appended by certain characters.
|
||||
|
||||
The %[1]surlTemplate%[1]s argument specifies the target URL that will be generated when the keyPrefix is found, which
|
||||
must contain %[1]s<num>%[1]s variable for the reference number.
|
||||
|
||||
By default, autolinks are alphanumeric with %[1]s--numeric%[1]s flag used to create a numeric autolink.
|
||||
|
||||
The %[1]s<num>%[1]s variable behavior differs depending on whether the autolink is alphanumeric or numeric:
|
||||
|
||||
- alphanumeric: matches %[1]sA-Z%[1]s (case insensitive), %[1]s0-9%[1]s, and %[1]s-%[1]s
|
||||
- numeric: matches %[1]s0-9%[1]s
|
||||
|
||||
If the template contains multiple instances of %[1]s<num>%[1]s, only the first will be replaced.
|
||||
`, "`"),
|
||||
Example: heredoc.Doc(`
|
||||
# Create an alphanumeric autolink to example.com for the key prefix "TICKET-".
|
||||
# Generates https://example.com/TICKET?query=123abc from "TICKET-123abc".
|
||||
$ gh repo autolink create TICKET- "https://example.com/TICKET?query=<num>"
|
||||
|
||||
# Create a numeric autolink to example.com for the key prefix "STORY-".
|
||||
# Generates https://example.com/STORY?id=123 from "STORY-123".
|
||||
$ gh repo autolink create STORY- "https://example.com/STORY?id=<num>" --numeric
|
||||
`),
|
||||
Args: cmdutil.ExactArgs(2, "Cannot create autolink: keyPrefix and urlTemplate arguments are both required"),
|
||||
Aliases: []string{"new"},
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
httpClient, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.AutolinkClient = &AutolinkCreator{HTTPClient: httpClient}
|
||||
opts.KeyPrefix = args[0]
|
||||
opts.URLTemplate = args[1]
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
|
||||
return createRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.Numeric, "numeric", "n", false, "Mark autolink as numeric")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func createRun(opts *createOptions) error {
|
||||
repo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request := AutolinkCreateRequest{
|
||||
KeyPrefix: opts.KeyPrefix,
|
||||
URLTemplate: opts.URLTemplate,
|
||||
IsAlphanumeric: !opts.Numeric,
|
||||
}
|
||||
|
||||
autolink, err := opts.AutolinkClient.Create(repo, request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating autolink: %w", err)
|
||||
}
|
||||
|
||||
cs := opts.IO.ColorScheme()
|
||||
fmt.Fprintf(opts.IO.Out,
|
||||
"%s Created repository autolink %s on %s\n",
|
||||
cs.SuccessIconWithColor(cs.Green),
|
||||
cs.Cyanf("%d", autolink.ID),
|
||||
cs.Bold(ghrepo.FullName(repo)))
|
||||
|
||||
return nil
|
||||
}
|
||||
190
pkg/cmd/repo/autolink/create/create_test.go
Normal file
190
pkg/cmd/repo/autolink/create/create_test.go
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
package create
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewCmdCreate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
output createOptions
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "no argument",
|
||||
input: "",
|
||||
wantErr: true,
|
||||
errMsg: "Cannot create autolink: keyPrefix and urlTemplate arguments are both required",
|
||||
},
|
||||
{
|
||||
name: "one argument",
|
||||
input: "TEST-",
|
||||
wantErr: true,
|
||||
errMsg: "Cannot create autolink: keyPrefix and urlTemplate arguments are both required",
|
||||
},
|
||||
{
|
||||
name: "two argument",
|
||||
input: "TICKET- https://example.com/TICKET?query=<num>",
|
||||
output: createOptions{
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "numeric flag",
|
||||
input: "TICKET- https://example.com/TICKET?query=<num> --numeric",
|
||||
output: createOptions{
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
Numeric: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
f.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{}, nil
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.input)
|
||||
require.NoError(t, err)
|
||||
|
||||
var gotOpts *createOptions
|
||||
cmd := NewCmdCreate(f, func(opts *createOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantErr {
|
||||
require.EqualError(t, err, tt.errMsg)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.output.KeyPrefix, gotOpts.KeyPrefix)
|
||||
assert.Equal(t, tt.output.URLTemplate, gotOpts.URLTemplate)
|
||||
assert.Equal(t, tt.output.Numeric, gotOpts.Numeric)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type stubAutolinkCreator struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (g stubAutolinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*shared.Autolink, error) {
|
||||
if g.err != nil {
|
||||
return nil, g.err
|
||||
}
|
||||
|
||||
return &shared.Autolink{
|
||||
ID: 1,
|
||||
KeyPrefix: request.KeyPrefix,
|
||||
URLTemplate: request.URLTemplate,
|
||||
IsAlphanumeric: request.IsAlphanumeric,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type testAutolinkClientCreateError struct{}
|
||||
|
||||
func (e testAutolinkClientCreateError) Error() string {
|
||||
return "autolink client create error"
|
||||
}
|
||||
|
||||
func TestCreateRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *createOptions
|
||||
stubCreator stubAutolinkCreator
|
||||
expectedErr error
|
||||
errMsg string
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
}{
|
||||
{
|
||||
name: "success, alphanumeric",
|
||||
opts: &createOptions{
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
},
|
||||
stubCreator: stubAutolinkCreator{},
|
||||
wantStdout: "✓ Created repository autolink 1 on OWNER/REPO\n",
|
||||
},
|
||||
{
|
||||
name: "success, numeric",
|
||||
opts: &createOptions{
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
Numeric: true,
|
||||
},
|
||||
stubCreator: stubAutolinkCreator{},
|
||||
wantStdout: "✓ Created repository autolink 1 on OWNER/REPO\n",
|
||||
},
|
||||
{
|
||||
name: "client error",
|
||||
opts: &createOptions{
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
},
|
||||
stubCreator: stubAutolinkCreator{err: testAutolinkClientCreateError{}},
|
||||
expectedErr: testAutolinkClientCreateError{},
|
||||
errMsg: fmt.Sprint("error creating autolink: ", testAutolinkClientCreateError{}.Error()),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
|
||||
opts := tt.opts
|
||||
opts.IO = ios
|
||||
opts.Browser = &browser.Stub{}
|
||||
|
||||
opts.IO = ios
|
||||
opts.BaseRepo = func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }
|
||||
|
||||
opts.AutolinkClient = &tt.stubCreator
|
||||
err := createRun(opts)
|
||||
|
||||
if tt.expectedErr != nil {
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, tt.expectedErr)
|
||||
assert.Equal(t, tt.errMsg, err.Error())
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
if tt.wantStdout != "" {
|
||||
assert.Equal(t, tt.wantStdout, stdout.String())
|
||||
}
|
||||
|
||||
if tt.wantStderr != "" {
|
||||
assert.Equal(t, tt.wantStderr, stderr.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
79
pkg/cmd/repo/autolink/create/http.go
Normal file
79
pkg/cmd/repo/autolink/create/http.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package create
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared"
|
||||
)
|
||||
|
||||
type AutolinkCreator struct {
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
type AutolinkCreateRequest struct {
|
||||
IsAlphanumeric bool `json:"is_alphanumeric"`
|
||||
KeyPrefix string `json:"key_prefix"`
|
||||
URLTemplate string `json:"url_template"`
|
||||
}
|
||||
|
||||
func (a *AutolinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*shared.Autolink, error) {
|
||||
path := fmt.Sprintf("repos/%s/%s/autolinks", repo.RepoOwner(), repo.RepoName())
|
||||
url := ghinstance.RESTPrefix(repo.RepoHost()) + path
|
||||
|
||||
requestByte, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
requestBody := bytes.NewReader(requestByte)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url, requestBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := a.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
err = handleAutolinkCreateError(resp)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var autolink shared.Autolink
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(&autolink)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &autolink, nil
|
||||
}
|
||||
|
||||
func handleAutolinkCreateError(resp *http.Response) error {
|
||||
switch resp.StatusCode {
|
||||
case http.StatusCreated:
|
||||
return nil
|
||||
case http.StatusNotFound:
|
||||
err := api.HandleHTTPError(resp)
|
||||
var httpErr api.HTTPError
|
||||
if errors.As(err, &httpErr) {
|
||||
httpErr.Message = "Must have admin rights to Repository."
|
||||
return httpErr
|
||||
}
|
||||
return err
|
||||
default:
|
||||
return api.HandleHTTPError(resp)
|
||||
}
|
||||
}
|
||||
155
pkg/cmd/repo/autolink/create/http_test.go
Normal file
155
pkg/cmd/repo/autolink/create/http_test.go
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
package create
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAutolinkCreator_Create(t *testing.T) {
|
||||
repo := ghrepo.New("OWNER", "REPO")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req AutolinkCreateRequest
|
||||
stubStatus int
|
||||
stubRespJSON string
|
||||
|
||||
expectedAutolink *shared.Autolink
|
||||
expectErr bool
|
||||
expectedErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "201 successful creation",
|
||||
req: AutolinkCreateRequest{
|
||||
IsAlphanumeric: true,
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
},
|
||||
stubStatus: http.StatusCreated,
|
||||
stubRespJSON: `{
|
||||
"id": 1,
|
||||
"is_alphanumeric": true,
|
||||
"key_prefix": "TICKET-",
|
||||
"url_template": "https://example.com/TICKET?query=<num>"
|
||||
}`,
|
||||
expectedAutolink: &shared.Autolink{
|
||||
ID: 1,
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
IsAlphanumeric: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "422 URL template not valid URL",
|
||||
req: AutolinkCreateRequest{
|
||||
IsAlphanumeric: true,
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "foo/<num>",
|
||||
},
|
||||
stubStatus: http.StatusUnprocessableEntity,
|
||||
stubRespJSON: `{
|
||||
"message": "Validation Failed",
|
||||
"errors": [
|
||||
{
|
||||
"resource": "KeyLink",
|
||||
"code": "custom",
|
||||
"field": "url_template",
|
||||
"message": "url_template must be an absolute URL"
|
||||
}
|
||||
],
|
||||
"documentation_url": "https://docs.github.com/rest/repos/autolinks#create-an-autolink-reference-for-a-repository",
|
||||
"status": "422"
|
||||
}`,
|
||||
expectErr: true,
|
||||
expectedErrMsg: heredoc.Doc(`
|
||||
HTTP 422: Validation Failed (https://api.github.com/repos/OWNER/REPO/autolinks)
|
||||
url_template must be an absolute URL`),
|
||||
},
|
||||
{
|
||||
name: "404 repo not found",
|
||||
req: AutolinkCreateRequest{
|
||||
IsAlphanumeric: true,
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
},
|
||||
stubStatus: http.StatusNotFound,
|
||||
stubRespJSON: `{
|
||||
"message": "Not Found",
|
||||
"documentation_url": "https://docs.github.com/rest/repos/autolinks#create-an-autolink-reference-for-a-repository",
|
||||
"status": "404"
|
||||
}`,
|
||||
expectErr: true,
|
||||
expectedErrMsg: "HTTP 404: Must have admin rights to Repository. (https://api.github.com/repos/OWNER/REPO/autolinks)",
|
||||
},
|
||||
{
|
||||
name: "422 URL template missing <num>",
|
||||
req: AutolinkCreateRequest{
|
||||
IsAlphanumeric: true,
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET",
|
||||
},
|
||||
stubStatus: http.StatusUnprocessableEntity,
|
||||
stubRespJSON: `{"message":"Validation Failed","errors":[{"resource":"KeyLink","code":"custom","field":"url_template","message":"url_template is missing a <num> token"}],"documentation_url":"https://docs.github.com/rest/repos/autolinks#create-an-autolink-reference-for-a-repository","status":"422"}`,
|
||||
expectErr: true,
|
||||
expectedErrMsg: heredoc.Doc(`
|
||||
HTTP 422: Validation Failed (https://api.github.com/repos/OWNER/REPO/autolinks)
|
||||
url_template is missing a <num> token`),
|
||||
},
|
||||
{
|
||||
name: "422 already exists",
|
||||
req: AutolinkCreateRequest{
|
||||
IsAlphanumeric: true,
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
},
|
||||
stubStatus: http.StatusUnprocessableEntity,
|
||||
stubRespJSON: `{"message":"Validation Failed","errors":[{"resource":"KeyLink","code":"already_exists","field":"key_prefix"}],"documentation_url":"https://docs.github.com/rest/repos/autolinks#create-an-autolink-reference-for-a-repository","status":"422"}`,
|
||||
expectErr: true,
|
||||
expectedErrMsg: heredoc.Doc(`
|
||||
HTTP 422: Validation Failed (https://api.github.com/repos/OWNER/REPO/autolinks)
|
||||
KeyLink.key_prefix already exists`),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Register(
|
||||
httpmock.REST(
|
||||
http.MethodPost,
|
||||
fmt.Sprintf("repos/%s/%s/autolinks", repo.RepoOwner(), repo.RepoName())),
|
||||
httpmock.RESTPayload(tt.stubStatus, tt.stubRespJSON,
|
||||
func(payload map[string]interface{}) {
|
||||
require.Equal(t, map[string]interface{}{
|
||||
"is_alphanumeric": tt.req.IsAlphanumeric,
|
||||
"key_prefix": tt.req.KeyPrefix,
|
||||
"url_template": tt.req.URLTemplate,
|
||||
}, payload)
|
||||
},
|
||||
),
|
||||
)
|
||||
defer reg.Verify(t)
|
||||
|
||||
autolinkCreator := &AutolinkCreator{
|
||||
HTTPClient: &http.Client{Transport: reg},
|
||||
}
|
||||
|
||||
autolink, err := autolinkCreator.Create(repo, tt.req)
|
||||
|
||||
if tt.expectErr {
|
||||
require.EqualError(t, err, tt.expectedErrMsg)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedAutolink, autolink)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
107
pkg/cmd/repo/autolink/delete/delete.go
Normal file
107
pkg/cmd/repo/autolink/delete/delete.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/repo/autolink/view"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type deleteOptions struct {
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Browser browser.Browser
|
||||
AutolinkDeleteClient AutolinkDeleteClient
|
||||
AutolinkViewClient view.AutolinkViewClient
|
||||
IO *iostreams.IOStreams
|
||||
|
||||
ID string
|
||||
Confirmed bool
|
||||
Prompter prompter.Prompter
|
||||
}
|
||||
|
||||
type AutolinkDeleteClient interface {
|
||||
Delete(repo ghrepo.Interface, id string) error
|
||||
}
|
||||
|
||||
func NewCmdDelete(f *cmdutil.Factory, runF func(*deleteOptions) error) *cobra.Command {
|
||||
opts := &deleteOptions{
|
||||
Browser: f.Browser,
|
||||
IO: f.IOStreams,
|
||||
Prompter: f.Prompter,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete <id>",
|
||||
Short: "Delete an autolink reference",
|
||||
Long: "Delete an autolink reference for a repository.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
httpClient, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.AutolinkDeleteClient = &AutolinkDeleter{HTTPClient: httpClient}
|
||||
opts.AutolinkViewClient = &view.AutolinkViewer{HTTPClient: httpClient}
|
||||
opts.ID = args[0]
|
||||
|
||||
if !opts.IO.CanPrompt() && !opts.Confirmed {
|
||||
return cmdutil.FlagErrorf("--yes required when not running interactively")
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
|
||||
return deleteRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&opts.Confirmed, "yes", false, "Confirm deletion without prompting")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func deleteRun(opts *deleteOptions) error {
|
||||
repo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out := opts.IO.Out
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
autolink, err := opts.AutolinkViewClient.View(repo, opts.ID)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %w", cs.Red("error deleting autolink:"), err)
|
||||
}
|
||||
|
||||
if opts.IO.CanPrompt() && !opts.Confirmed {
|
||||
fmt.Fprintf(out, "Autolink %s has key prefix %s.\n", cs.Cyan(opts.ID), autolink.KeyPrefix)
|
||||
|
||||
err := opts.Prompter.ConfirmDeletion(autolink.KeyPrefix)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = opts.AutolinkDeleteClient.Delete(repo, opts.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(out, "%s Autolink %s deleted from %s\n", cs.SuccessIcon(), cs.Cyan(opts.ID), cs.Bold(ghrepo.FullName(repo)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
335
pkg/cmd/repo/autolink/delete/delete_test.go
Normal file
335
pkg/cmd/repo/autolink/delete/delete_test.go
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewCmdDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
output deleteOptions
|
||||
isTTY bool
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "no argument",
|
||||
input: "",
|
||||
isTTY: true,
|
||||
wantErr: true,
|
||||
errMsg: "accepts 1 arg(s), received 0",
|
||||
},
|
||||
{
|
||||
name: "id provided",
|
||||
input: "123",
|
||||
isTTY: true,
|
||||
output: deleteOptions{ID: "123"},
|
||||
},
|
||||
{
|
||||
name: "yes flag",
|
||||
input: "123 --yes",
|
||||
isTTY: true,
|
||||
output: deleteOptions{ID: "123", Confirmed: true},
|
||||
},
|
||||
{
|
||||
name: "non-TTY",
|
||||
input: "123 --yes",
|
||||
isTTY: false,
|
||||
output: deleteOptions{ID: "123", Confirmed: true},
|
||||
},
|
||||
{
|
||||
name: "non-TTY missing yes flag",
|
||||
input: "123",
|
||||
isTTY: false,
|
||||
wantErr: true,
|
||||
errMsg: "--yes required when not running interactively",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
ios.SetStdinTTY(tt.isTTY)
|
||||
ios.SetStdoutTTY(tt.isTTY)
|
||||
ios.SetStderrTTY(tt.isTTY)
|
||||
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
f.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{}, nil
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.input)
|
||||
require.NoError(t, err)
|
||||
|
||||
var gotOpts *deleteOptions
|
||||
cmd := NewCmdDelete(f, func(opts *deleteOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantErr {
|
||||
require.EqualError(t, err, tt.errMsg)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.output.ID, gotOpts.ID)
|
||||
assert.Equal(t, tt.output.Confirmed, gotOpts.Confirmed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type stubAutolinkDeleter struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (d *stubAutolinkDeleter) Delete(repo ghrepo.Interface, id string) error {
|
||||
return d.err
|
||||
}
|
||||
|
||||
type stubAutolinkViewer struct {
|
||||
autolink *shared.Autolink
|
||||
err error
|
||||
}
|
||||
|
||||
func (g stubAutolinkViewer) View(repo ghrepo.Interface, id string) (*shared.Autolink, error) {
|
||||
return g.autolink, g.err
|
||||
}
|
||||
|
||||
var errTestPrompt = errors.New("prompt error")
|
||||
var errTestAutolinkClientView = errors.New("autolink client view error")
|
||||
var errTestAutolinkClientDelete = errors.New("autolink client delete error")
|
||||
|
||||
func TestDeleteRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *deleteOptions
|
||||
isTTY bool
|
||||
stubDeleter stubAutolinkDeleter
|
||||
stubViewer stubAutolinkViewer
|
||||
prompterStubs func(*prompter.PrompterMock)
|
||||
|
||||
wantStdout string
|
||||
expectedErr error
|
||||
expectedErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "delete",
|
||||
opts: &deleteOptions{
|
||||
ID: "123",
|
||||
},
|
||||
isTTY: true,
|
||||
stubViewer: stubAutolinkViewer{
|
||||
autolink: &shared.Autolink{
|
||||
ID: 123,
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
IsAlphanumeric: true,
|
||||
},
|
||||
},
|
||||
stubDeleter: stubAutolinkDeleter{
|
||||
err: nil,
|
||||
},
|
||||
prompterStubs: func(pm *prompter.PrompterMock) {
|
||||
pm.ConfirmDeletionFunc = func(_ string) error {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
wantStdout: heredoc.Doc(`
|
||||
Autolink 123 has key prefix TICKET-.
|
||||
✓ Autolink 123 deleted from OWNER/REPO
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "delete with confirm flag",
|
||||
opts: &deleteOptions{
|
||||
ID: "123",
|
||||
Confirmed: true,
|
||||
},
|
||||
isTTY: true,
|
||||
stubViewer: stubAutolinkViewer{
|
||||
autolink: &shared.Autolink{
|
||||
ID: 123,
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
IsAlphanumeric: true,
|
||||
},
|
||||
},
|
||||
stubDeleter: stubAutolinkDeleter{},
|
||||
wantStdout: "✓ Autolink 123 deleted from OWNER/REPO\n",
|
||||
},
|
||||
{
|
||||
name: "confirmation fails",
|
||||
opts: &deleteOptions{
|
||||
ID: "123",
|
||||
},
|
||||
isTTY: true,
|
||||
stubViewer: stubAutolinkViewer{
|
||||
autolink: &shared.Autolink{
|
||||
ID: 123,
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
IsAlphanumeric: true,
|
||||
},
|
||||
},
|
||||
stubDeleter: stubAutolinkDeleter{},
|
||||
prompterStubs: func(pm *prompter.PrompterMock) {
|
||||
pm.ConfirmDeletionFunc = func(_ string) error {
|
||||
return errTestPrompt
|
||||
}
|
||||
},
|
||||
expectedErr: errTestPrompt,
|
||||
expectedErrMsg: errTestPrompt.Error(),
|
||||
wantStdout: "Autolink 123 has key prefix TICKET-.\n",
|
||||
},
|
||||
{
|
||||
name: "view error",
|
||||
opts: &deleteOptions{
|
||||
ID: "123",
|
||||
},
|
||||
isTTY: true,
|
||||
stubViewer: stubAutolinkViewer{
|
||||
err: errTestAutolinkClientView,
|
||||
},
|
||||
stubDeleter: stubAutolinkDeleter{},
|
||||
expectedErr: errTestAutolinkClientView,
|
||||
expectedErrMsg: "error deleting autolink: autolink client view error",
|
||||
},
|
||||
{
|
||||
name: "delete error",
|
||||
opts: &deleteOptions{
|
||||
ID: "123",
|
||||
},
|
||||
isTTY: true,
|
||||
stubViewer: stubAutolinkViewer{
|
||||
autolink: &shared.Autolink{
|
||||
ID: 123,
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
IsAlphanumeric: true,
|
||||
},
|
||||
},
|
||||
stubDeleter: stubAutolinkDeleter{
|
||||
err: errTestAutolinkClientDelete,
|
||||
},
|
||||
prompterStubs: func(pm *prompter.PrompterMock) {
|
||||
pm.ConfirmDeletionFunc = func(_ string) error {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
expectedErr: errTestAutolinkClientDelete,
|
||||
expectedErrMsg: errTestAutolinkClientDelete.Error(),
|
||||
wantStdout: "Autolink 123 has key prefix TICKET-.\n",
|
||||
},
|
||||
{
|
||||
name: "no TTY",
|
||||
opts: &deleteOptions{
|
||||
ID: "123",
|
||||
Confirmed: true,
|
||||
},
|
||||
isTTY: false,
|
||||
stubViewer: stubAutolinkViewer{
|
||||
autolink: &shared.Autolink{
|
||||
ID: 123,
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
IsAlphanumeric: true,
|
||||
}},
|
||||
stubDeleter: stubAutolinkDeleter{},
|
||||
wantStdout: "",
|
||||
},
|
||||
{
|
||||
name: "no TTY view error",
|
||||
opts: &deleteOptions{
|
||||
ID: "123",
|
||||
},
|
||||
isTTY: false,
|
||||
stubViewer: stubAutolinkViewer{
|
||||
err: errTestAutolinkClientView,
|
||||
},
|
||||
stubDeleter: stubAutolinkDeleter{},
|
||||
expectedErr: errTestAutolinkClientView,
|
||||
expectedErrMsg: "error deleting autolink: autolink client view error",
|
||||
},
|
||||
{
|
||||
name: "no TTY delete error",
|
||||
opts: &deleteOptions{
|
||||
ID: "123",
|
||||
Confirmed: true,
|
||||
},
|
||||
isTTY: false,
|
||||
stubViewer: stubAutolinkViewer{
|
||||
autolink: &shared.Autolink{
|
||||
ID: 123,
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
IsAlphanumeric: true,
|
||||
},
|
||||
},
|
||||
stubDeleter: stubAutolinkDeleter{
|
||||
err: errTestAutolinkClientDelete,
|
||||
},
|
||||
expectedErr: errTestAutolinkClientDelete,
|
||||
expectedErrMsg: errTestAutolinkClientDelete.Error(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
|
||||
ios.SetStdinTTY(tt.isTTY)
|
||||
ios.SetStdoutTTY(tt.isTTY)
|
||||
|
||||
opts := tt.opts
|
||||
opts.IO = ios
|
||||
opts.Browser = &browser.Stub{}
|
||||
|
||||
opts.IO = ios
|
||||
opts.BaseRepo = func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }
|
||||
|
||||
opts.AutolinkDeleteClient = &tt.stubDeleter
|
||||
opts.AutolinkViewClient = &tt.stubViewer
|
||||
|
||||
pm := &prompter.PrompterMock{}
|
||||
if tt.prompterStubs != nil {
|
||||
tt.prompterStubs(pm)
|
||||
}
|
||||
tt.opts.Prompter = pm
|
||||
|
||||
err := deleteRun(opts)
|
||||
|
||||
if tt.expectedErr != nil {
|
||||
require.Error(t, err, "expected error but got none")
|
||||
assert.ErrorIs(t, err, tt.expectedErr, "unexpected error")
|
||||
assert.Equal(t, tt.expectedErrMsg, err.Error(), "unexpected error message")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout")
|
||||
})
|
||||
}
|
||||
}
|
||||
37
pkg/cmd/repo/autolink/delete/http.go
Normal file
37
pkg/cmd/repo/autolink/delete/http.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
)
|
||||
|
||||
type AutolinkDeleter struct {
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
func (a *AutolinkDeleter) Delete(repo ghrepo.Interface, id string) error {
|
||||
path := fmt.Sprintf("repos/%s/%s/autolinks/%s", repo.RepoOwner(), repo.RepoName(), id)
|
||||
url := ghinstance.RESTPrefix(repo.RepoHost()) + path
|
||||
req, err := http.NewRequest(http.MethodDelete, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := a.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return fmt.Errorf("error deleting autolink: HTTP 404: Perhaps you are missing admin rights to the repository? (https://api.github.com/%s)", path)
|
||||
} else if resp.StatusCode > 299 {
|
||||
return api.HandleHTTPError(resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
73
pkg/cmd/repo/autolink/delete/http_test.go
Normal file
73
pkg/cmd/repo/autolink/delete/http_test.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAutolinkDeleter_Delete(t *testing.T) {
|
||||
repo := ghrepo.New("OWNER", "REPO")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
stubStatus int
|
||||
stubRespJSON string
|
||||
|
||||
expectErr bool
|
||||
expectedErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "204 successful delete",
|
||||
id: "123",
|
||||
stubStatus: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
name: "404 repo or autolink not found",
|
||||
id: "123",
|
||||
stubStatus: http.StatusNotFound,
|
||||
stubRespJSON: `{}`, // API response not used in output
|
||||
expectErr: true,
|
||||
expectedErrMsg: "error deleting autolink: HTTP 404: Perhaps you are missing admin rights to the repository? (https://api.github.com/repos/OWNER/REPO/autolinks/123)",
|
||||
},
|
||||
{
|
||||
name: "500 unexpected error",
|
||||
id: "123",
|
||||
stubRespJSON: `{"messsage": "arbitrary error"}`,
|
||||
stubStatus: http.StatusInternalServerError,
|
||||
expectErr: true,
|
||||
expectedErrMsg: "HTTP 500 (https://api.github.com/repos/OWNER/REPO/autolinks/123)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Register(
|
||||
httpmock.REST(
|
||||
http.MethodDelete,
|
||||
fmt.Sprintf("repos/%s/%s/autolinks/%s", repo.RepoOwner(), repo.RepoName(), tt.id),
|
||||
),
|
||||
httpmock.StatusJSONResponse(tt.stubStatus, tt.stubRespJSON),
|
||||
)
|
||||
defer reg.Verify(t)
|
||||
|
||||
autolinkDeleter := &AutolinkDeleter{
|
||||
HTTPClient: &http.Client{Transport: reg},
|
||||
}
|
||||
|
||||
err := autolinkDeleter.Delete(repo, tt.id)
|
||||
|
||||
if tt.expectErr {
|
||||
require.EqualError(t, err, tt.expectedErrMsg)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -8,16 +8,17 @@ import (
|
|||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared"
|
||||
)
|
||||
|
||||
type AutolinkLister struct {
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
func (a *AutolinkLister) List(repo ghrepo.Interface) ([]autolink, error) {
|
||||
func (a *AutolinkLister) List(repo ghrepo.Interface) ([]shared.Autolink, error) {
|
||||
path := fmt.Sprintf("repos/%s/%s/autolinks", repo.RepoOwner(), repo.RepoName())
|
||||
url := ghinstance.RESTPrefix(repo.RepoHost()) + path
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -33,7 +34,7 @@ func (a *AutolinkLister) List(repo ghrepo.Interface) ([]autolink, error) {
|
|||
} else if resp.StatusCode > 299 {
|
||||
return nil, api.HandleHTTPError(resp)
|
||||
}
|
||||
var autolinks []autolink
|
||||
var autolinks []shared.Autolink
|
||||
err = json.NewDecoder(resp.Body).Decode(&autolinks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -6,28 +6,29 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAutoLinkLister_List(t *testing.T) {
|
||||
func TestAutolinkLister_List(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
repo ghrepo.Interface
|
||||
resp []autolink
|
||||
resp []shared.Autolink
|
||||
status int
|
||||
}{
|
||||
{
|
||||
name: "no autolinks",
|
||||
repo: ghrepo.New("OWNER", "REPO"),
|
||||
resp: []autolink{},
|
||||
status: 200,
|
||||
resp: []shared.Autolink{},
|
||||
status: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "two autolinks",
|
||||
repo: ghrepo.New("OWNER", "REPO"),
|
||||
resp: []autolink{
|
||||
resp: []shared.Autolink{
|
||||
{
|
||||
ID: 1,
|
||||
IsAlphanumeric: true,
|
||||
|
|
@ -41,12 +42,12 @@ func TestAutoLinkLister_List(t *testing.T) {
|
|||
URLTemplate: "https://example2.com",
|
||||
},
|
||||
},
|
||||
status: 200,
|
||||
status: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "http error",
|
||||
repo: ghrepo.New("OWNER", "REPO"),
|
||||
status: 404,
|
||||
status: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +55,7 @@ func TestAutoLinkLister_List(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Register(
|
||||
httpmock.REST("GET", fmt.Sprintf("repos/%s/%s/autolinks", tt.repo.RepoOwner(), tt.repo.RepoName())),
|
||||
httpmock.REST(http.MethodGet, fmt.Sprintf("repos/%s/%s/autolinks", tt.repo.RepoOwner(), tt.repo.RepoName())),
|
||||
httpmock.StatusJSONResponse(tt.status, tt.resp),
|
||||
)
|
||||
defer reg.Verify(t)
|
||||
|
|
@ -63,7 +64,7 @@ func TestAutoLinkLister_List(t *testing.T) {
|
|||
HTTPClient: &http.Client{Transport: reg},
|
||||
}
|
||||
autolinks, err := autolinkLister.List(tt.repo)
|
||||
if tt.status == 404 {
|
||||
if tt.status == http.StatusNotFound {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "error getting autolinks: HTTP 404: Perhaps you are missing admin rights to the repository? (https://api.github.com/repos/OWNER/REPO/autolinks)", err.Error())
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -9,41 +9,24 @@ import (
|
|||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var autolinkFields = []string{
|
||||
"id",
|
||||
"isAlphanumeric",
|
||||
"keyPrefix",
|
||||
"urlTemplate",
|
||||
}
|
||||
|
||||
type autolink struct {
|
||||
ID int `json:"id"`
|
||||
IsAlphanumeric bool `json:"is_alphanumeric"`
|
||||
KeyPrefix string `json:"key_prefix"`
|
||||
URLTemplate string `json:"url_template"`
|
||||
}
|
||||
|
||||
func (s *autolink) ExportData(fields []string) map[string]interface{} {
|
||||
return cmdutil.StructExportData(s, fields)
|
||||
}
|
||||
|
||||
type listOptions struct {
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Browser browser.Browser
|
||||
AutolinkClient AutolinkClient
|
||||
AutolinkClient AutolinkListClient
|
||||
IO *iostreams.IOStreams
|
||||
|
||||
Exporter cmdutil.Exporter
|
||||
WebMode bool
|
||||
}
|
||||
|
||||
type AutolinkClient interface {
|
||||
List(repo ghrepo.Interface) ([]autolink, error)
|
||||
type AutolinkListClient interface {
|
||||
List(repo ghrepo.Interface) ([]shared.Autolink, error)
|
||||
}
|
||||
|
||||
func NewCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Command {
|
||||
|
|
@ -80,7 +63,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Comman
|
|||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "List autolink references in the web browser")
|
||||
cmdutil.AddJSONFlags(cmd, &opts.Exporter, autolinkFields)
|
||||
cmdutil.AddJSONFlags(cmd, &opts.Exporter, shared.AutolinkFields)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -106,8 +89,14 @@ func listRun(opts *listOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
if len(autolinks) == 0 {
|
||||
return cmdutil.NewNoResultsError(fmt.Sprintf("no autolinks found in %s", ghrepo.FullName(repo)))
|
||||
return cmdutil.NewNoResultsError(
|
||||
fmt.Sprintf(
|
||||
"no autolinks found in %s",
|
||||
cs.Bold(ghrepo.FullName(repo))),
|
||||
)
|
||||
}
|
||||
|
||||
if opts.Exporter != nil {
|
||||
|
|
@ -115,14 +104,18 @@ func listRun(opts *listOptions) error {
|
|||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
title := listHeader(ghrepo.FullName(repo), len(autolinks))
|
||||
title := fmt.Sprintf(
|
||||
"Showing %s in %s",
|
||||
text.Pluralize(len(autolinks), "autolink reference"),
|
||||
cs.Bold(ghrepo.FullName(repo)),
|
||||
)
|
||||
fmt.Fprintf(opts.IO.Out, "\n%s\n\n", title)
|
||||
}
|
||||
|
||||
tp := tableprinter.New(opts.IO, tableprinter.WithHeader("ID", "KEY PREFIX", "URL TEMPLATE", "ALPHANUMERIC"))
|
||||
|
||||
for _, autolink := range autolinks {
|
||||
tp.AddField(fmt.Sprintf("%d", autolink.ID))
|
||||
tp.AddField(cs.Cyanf("%d", autolink.ID))
|
||||
tp.AddField(autolink.KeyPrefix)
|
||||
tp.AddField(autolink.URLTemplate)
|
||||
tp.AddField(strconv.FormatBool(autolink.IsAlphanumeric))
|
||||
|
|
@ -131,7 +124,3 @@ func listRun(opts *listOptions) error {
|
|||
|
||||
return tp.Render()
|
||||
}
|
||||
|
||||
func listHeader(repoName string, count int) string {
|
||||
return fmt.Sprintf("Showing %s in %s", text.Pluralize(count, "autolink reference"), repoName)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/jsonfieldstest"
|
||||
|
|
@ -95,12 +96,12 @@ func TestNewCmdList(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
type stubAutoLinkLister struct {
|
||||
autolinks []autolink
|
||||
type stubAutolinkLister struct {
|
||||
autolinks []shared.Autolink
|
||||
err error
|
||||
}
|
||||
|
||||
func (g stubAutoLinkLister) List(repo ghrepo.Interface) ([]autolink, error) {
|
||||
func (g stubAutolinkLister) List(repo ghrepo.Interface) ([]shared.Autolink, error) {
|
||||
return g.autolinks, g.err
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +116,7 @@ func TestListRun(t *testing.T) {
|
|||
name string
|
||||
opts *listOptions
|
||||
isTTY bool
|
||||
stubLister stubAutoLinkLister
|
||||
stubLister stubAutolinkLister
|
||||
expectedErr error
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
|
|
@ -124,8 +125,8 @@ func TestListRun(t *testing.T) {
|
|||
name: "list tty",
|
||||
opts: &listOptions{},
|
||||
isTTY: true,
|
||||
stubLister: stubAutoLinkLister{
|
||||
autolinks: []autolink{
|
||||
stubLister: stubAutolinkLister{
|
||||
autolinks: []shared.Autolink{
|
||||
{
|
||||
ID: 1,
|
||||
KeyPrefix: "TICKET-",
|
||||
|
|
@ -160,8 +161,8 @@ func TestListRun(t *testing.T) {
|
|||
}(),
|
||||
},
|
||||
isTTY: true,
|
||||
stubLister: stubAutoLinkLister{
|
||||
autolinks: []autolink{
|
||||
stubLister: stubAutolinkLister{
|
||||
autolinks: []shared.Autolink{
|
||||
{
|
||||
ID: 1,
|
||||
KeyPrefix: "TICKET-",
|
||||
|
|
@ -183,8 +184,8 @@ func TestListRun(t *testing.T) {
|
|||
name: "list non-tty",
|
||||
opts: &listOptions{},
|
||||
isTTY: false,
|
||||
stubLister: stubAutoLinkLister{
|
||||
autolinks: []autolink{
|
||||
stubLister: stubAutolinkLister{
|
||||
autolinks: []shared.Autolink{
|
||||
{
|
||||
ID: 1,
|
||||
KeyPrefix: "TICKET-",
|
||||
|
|
@ -209,8 +210,8 @@ func TestListRun(t *testing.T) {
|
|||
name: "no results",
|
||||
opts: &listOptions{},
|
||||
isTTY: true,
|
||||
stubLister: stubAutoLinkLister{
|
||||
autolinks: []autolink{},
|
||||
stubLister: stubAutolinkLister{
|
||||
autolinks: []shared.Autolink{},
|
||||
},
|
||||
expectedErr: cmdutil.NewNoResultsError("no autolinks found in OWNER/REPO"),
|
||||
wantStderr: "",
|
||||
|
|
@ -219,8 +220,8 @@ func TestListRun(t *testing.T) {
|
|||
name: "client error",
|
||||
opts: &listOptions{},
|
||||
isTTY: true,
|
||||
stubLister: stubAutoLinkLister{
|
||||
autolinks: []autolink{},
|
||||
stubLister: stubAutolinkLister{
|
||||
autolinks: []shared.Autolink{},
|
||||
err: testAutolinkClientListError{},
|
||||
},
|
||||
expectedErr: testAutolinkClientListError{},
|
||||
|
|
|
|||
21
pkg/cmd/repo/autolink/shared/autolink.go
Normal file
21
pkg/cmd/repo/autolink/shared/autolink.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package shared
|
||||
|
||||
import "github.com/cli/cli/v2/pkg/cmdutil"
|
||||
|
||||
type Autolink struct {
|
||||
ID int `json:"id"`
|
||||
IsAlphanumeric bool `json:"is_alphanumeric"`
|
||||
KeyPrefix string `json:"key_prefix"`
|
||||
URLTemplate string `json:"url_template"`
|
||||
}
|
||||
|
||||
var AutolinkFields = []string{
|
||||
"id",
|
||||
"isAlphanumeric",
|
||||
"keyPrefix",
|
||||
"urlTemplate",
|
||||
}
|
||||
|
||||
func (a *Autolink) ExportData(fields []string) map[string]interface{} {
|
||||
return cmdutil.StructExportData(a, fields)
|
||||
}
|
||||
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