diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 851de9cb3..31cd4b8e7 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -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') diff --git a/acceptance/testdata/pr/pr-view-status-respects-branch-pushremote.txtar b/acceptance/testdata/pr/pr-view-status-respects-branch-pushremote.txtar new file mode 100644 index 000000000..ef80cd8ba --- /dev/null +++ b/acceptance/testdata/pr/pr-view-status-respects-branch-pushremote.txtar @@ -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 diff --git a/acceptance/testdata/pr/pr-view-status-respects-push-destination.txtar b/acceptance/testdata/pr/pr-view-status-respects-push-destination.txtar new file mode 100644 index 000000000..ff9db4037 --- /dev/null +++ b/acceptance/testdata/pr/pr-view-status-respects-push-destination.txtar @@ -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 diff --git a/acceptance/testdata/pr/pr-view-status-respects-remote-pushdefault.txtar b/acceptance/testdata/pr/pr-view-status-respects-remote-pushdefault.txtar new file mode 100644 index 000000000..8bfac2837 --- /dev/null +++ b/acceptance/testdata/pr/pr-view-status-respects-remote-pushdefault.txtar @@ -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 diff --git a/acceptance/testdata/pr/pr-view-status-respects-simple-pushdefault.txtar b/acceptance/testdata/pr/pr-view-status-respects-simple-pushdefault.txtar new file mode 100644 index 000000000..114f401ec --- /dev/null +++ b/acceptance/testdata/pr/pr-view-status-respects-simple-pushdefault.txtar @@ -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 diff --git a/git/client.go b/git/client.go index 19688ca51..11a2e2e20 100644 --- a/git/client.go +++ b/git/client.go @@ -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) diff --git a/git/client_test.go b/git/client_test.go index fe3047db9..9fa076199 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -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 != "" { diff --git a/git/objects.go b/git/objects.go index c09683042..9db528b8c 100644 --- a/git/objects.go +++ b/git/objects.go @@ -60,10 +60,14 @@ type Commit struct { Body string } +// These are the keys we read from the git branch. 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 } diff --git a/go.mod b/go.mod index 2484bbd15..38837b883 100644 --- a/go.mod +++ b/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,7 +41,7 @@ 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/stretchr/testify v1.10.0 @@ -50,8 +50,8 @@ require ( 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.4 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 ) diff --git a/go.sum b/go.sum index e00b98d55..883ac72ce 100644 --- a/go.sum +++ b/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,36 @@ 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/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 +448,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 +475,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 +487,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 +503,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 +541,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.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= +google.golang.org/protobuf v1.36.4/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 +573,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= diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index 359bdad54..76ef6688a 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -173,6 +173,7 @@ func (c *LiveClient) fetchBundleFromAttestations(attestations []*Attestation) ([ fetched[i] = &Attestation{ Bundle: b, } + return nil }) } diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index f023dd03a..cb39fb78a 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -162,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, @@ -170,12 +170,15 @@ func TestFetchBundleFromAttestations(t *testing.T) { } att1 := makeTestAttestation() + att1.Bundle = nil att2 := makeTestAttestation() + att2.Bundle = nil + // zero out the bundle field so it tries fetching by URL attestations := []*Attestation{&att1, &att2} 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) } @@ -195,6 +198,7 @@ func TestFetchBundleFromAttestations_InvalidAttestation(t *testing.T) { func TestFetchBundleFromAttestations_FetchByURLFail(t *testing.T) { mockHTTPClient := &failHttpClient{} + // failAfterOneCallHttpClient c := &LiveClient{ httpClient: mockHTTPClient, @@ -217,6 +221,7 @@ func TestFetchBundleFromAttestations_FailWithGetErr(t *testing.T) { } a := makeTestAttestation() + a.Bundle = nil attestations := []*Attestation{&a} bundle, err := c.fetchBundleFromAttestations(attestations) require.Error(t, err) diff --git a/pkg/cmd/attestation/inspect/inspect.go b/pkg/cmd/attestation/inspect/inspect.go index 81c37588d..b2d0adfb7 100644 --- a/pkg/cmd/attestation/inspect/inspect.go +++ b/pkg/cmd/attestation/inspect/inspect.go @@ -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 { diff --git a/pkg/cmd/attestation/verification/sigstore.go b/pkg/cmd/attestation/verification/sigstore.go index 4f94666e5..c71c600c7 100644 --- a/pkg/cmd/attestation/verification/sigstore.go +++ b/pkg/cmd/attestation/verification/sigstore.go @@ -70,7 +70,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") } @@ -116,7 +116,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 +153,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 diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 124bd4b07..b2abe0938 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -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..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 { diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index 992439df1..6df3f9880 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -1515,7 +1515,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 +1549,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 { diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index 3f036c0cd..e4f89502c 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -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,6 +97,28 @@ 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 :. +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 len(opts.Fields) == 0 { return nil, nil, errors.New("Find error: no fields specified") @@ -92,26 +126,18 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err 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 } - if opts.Selector == "" { - if branch, prNumber, err := f.parseCurrentBranch(); err != nil { - return nil, nil, err - } else if prNumber > 0 { - f.prNumber = prNumber - } else { - f.branchName = branch - } - } else if f.prNumber == 0 { + if f.prNumber == 0 && 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 @@ -123,8 +149,28 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err } else { f.branchName = opts.Selector } + } else { + currentBranchName, err := f.branchFn() + if err != nil { + return nil, nil, err + } + 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 + } + + // Set up HTTP client httpClient, err := f.httpClient() if err != nil { return nil, nil, err @@ -143,7 +189,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 +210,62 @@ 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 + rems, err := f.remotesFn() + if err != nil { + return nil, nil, err + } + + pushDefault, err := f.pushDefault() + 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) + + 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 + } + + 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 +273,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 +302,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 /. + if parsedPushRevision != "" { + for _, r := range rems { + // Find the remote who's name matches the push 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..pushRemote + // 2. remote.pushDefault + // 3. branch..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 +398,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 +425,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 +450,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 { diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go index dd96e684a..694e0c20d 100644 --- a/pkg/cmd/pr/shared/finder_test.go +++ b/pkg/cmd/pr/shared/finder_test.go @@ -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,18 @@ func TestFind(t *testing.T) { wantRepo: "https://example.org/OWNER/REPO", }, { - name: "branch argument", + 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: func() (ghrepo.Interface, error) { - return ghrepo.FromFullName("OWNER/REPO") + 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 +236,7 @@ func TestFind(t *testing.T) { "baseRefName": "main", "headRefName": "blueberries", "isCrossRepository": false, - "headRepositoryOwner": {"login":"OWNER"} + "headRepositoryOwner": {"login":"ORIGINOWNER"} }, { "number": 13, @@ -177,13 +244,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 +261,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 +307,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 +342,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 +383,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 +416,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 +456,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 +483,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 +530,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 +592,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 +635,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 +945,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 + } +} diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go index a97a59e44..3877fb1cf 100644 --- a/pkg/cmd/pr/status/status.go +++ b/pkg/cmd/pr/status/status.go @@ -7,7 +7,6 @@ import ( "net/http" "regexp" "strconv" - "strings" "time" "github.com/cli/cli/v2/api" @@ -78,27 +77,56 @@ 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() + 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 +135,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 +144,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 +152,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 +179,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 +217,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 { diff --git a/pkg/cmd/pr/status/status_test.go b/pkg/cmd/pr/status/status_test.go index 43cce5f4c..c55604c28 100644 --- a/pkg/cmd/pr/status/status_test.go +++ b/pkg/cmd/pr/status/status_test.go @@ -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) - }) - } -} diff --git a/pkg/cmd/repo/autolink/autolink.go b/pkg/cmd/repo/autolink/autolink.go index d9430f562..09b766b85 100644 --- a/pkg/cmd/repo/autolink/autolink.go +++ b/pkg/cmd/repo/autolink/autolink.go @@ -2,7 +2,9 @@ package autolink import ( "github.com/MakeNowJust/heredoc" + cmdCreate "github.com/cli/cli/v2/pkg/cmd/repo/autolink/create" 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 +14,18 @@ func NewCmdAutolink(f *cmdutil.Factory) *cobra.Command { Use: "autolink ", 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 + `, "`"), } cmdutil.EnableRepoOverride(cmd, f) cmd.AddCommand(cmdList.NewCmdList(f, nil)) + cmd.AddCommand(cmdCreate.NewCmdCreate(f, nil)) + cmd.AddCommand(cmdView.NewCmdView(f, nil)) return cmd } diff --git a/pkg/cmd/repo/autolink/create/create.go b/pkg/cmd/repo/autolink/create/create.go new file mode 100644 index 000000000..aad71a941 --- /dev/null +++ b/pkg/cmd/repo/autolink/create/create.go @@ -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 ", + 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%[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%[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%[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=" + + # 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=" --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), + ghrepo.FullName(repo)) + + return nil +} diff --git a/pkg/cmd/repo/autolink/create/create_test.go b/pkg/cmd/repo/autolink/create/create_test.go new file mode 100644 index 000000000..477d28da9 --- /dev/null +++ b/pkg/cmd/repo/autolink/create/create_test.go @@ -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=", + output: createOptions{ + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + }, + }, + { + name: "numeric flag", + input: "TICKET- https://example.com/TICKET?query= --numeric", + output: createOptions{ + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + 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=", + }, + stubCreator: stubAutoLinkCreator{}, + wantStdout: "✓ Created repository autolink 1 on OWNER/REPO\n", + }, + { + name: "success, numeric", + opts: &createOptions{ + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + 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=", + }, + 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()) + } + }) + } +} diff --git a/pkg/cmd/repo/autolink/create/http.go b/pkg/cmd/repo/autolink/create/http.go new file mode 100644 index 000000000..d7ee940b9 --- /dev/null +++ b/pkg/cmd/repo/autolink/create/http.go @@ -0,0 +1,83 @@ +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() + + // if resp.StatusCode != http.StatusCreated { + // return nil, api.HandleHTTPError(resp) + // } + + 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) + } +} diff --git a/pkg/cmd/repo/autolink/create/http_test.go b/pkg/cmd/repo/autolink/create/http_test.go new file mode 100644 index 000000000..9ef5e0da5 --- /dev/null +++ b/pkg/cmd/repo/autolink/create/http_test.go @@ -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=", + }, + stubStatus: http.StatusCreated, + stubRespJSON: `{ + "id": 1, + "is_alphanumeric": true, + "key_prefix": "TICKET-", + "url_template": "https://example.com/TICKET?query=" + }`, + expectedAutolink: &shared.Autolink{ + ID: 1, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + }, + { + name: "422 URL template not valid URL", + req: AutolinkCreateRequest{ + IsAlphanumeric: true, + KeyPrefix: "TICKET-", + URLTemplate: "foo/", + }, + 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=", + }, + 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 ", + 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 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 token`), + }, + { + name: "422 already exists", + req: AutolinkCreateRequest{ + IsAlphanumeric: true, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + }, + 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) + } + }) + } +} diff --git a/pkg/cmd/repo/autolink/list/http.go b/pkg/cmd/repo/autolink/list/http.go index 70d913d70..cdb8e621c 100644 --- a/pkg/cmd/repo/autolink/list/http.go +++ b/pkg/cmd/repo/autolink/list/http.go @@ -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 diff --git a/pkg/cmd/repo/autolink/list/http_test.go b/pkg/cmd/repo/autolink/list/http_test.go index fc1e44b23..65289c419 100644 --- a/pkg/cmd/repo/autolink/list/http_test.go +++ b/pkg/cmd/repo/autolink/list/http_test.go @@ -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{}, + resp: []shared.Autolink{}, status: 200, }, { name: "two autolinks", repo: ghrepo.New("OWNER", "REPO"), - resp: []autolink{ + resp: []shared.Autolink{ { ID: 1, IsAlphanumeric: true, @@ -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) diff --git a/pkg/cmd/repo/autolink/list/list.go b/pkg/cmd/repo/autolink/list/list.go index d8a9c9f12..402479c44 100644 --- a/pkg/cmd/repo/autolink/list/list.go +++ b/pkg/cmd/repo/autolink/list/list.go @@ -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 } @@ -121,8 +104,10 @@ func listRun(opts *listOptions) error { tp := tableprinter.New(opts.IO, tableprinter.WithHeader("ID", "KEY PREFIX", "URL TEMPLATE", "ALPHANUMERIC")) + cs := opts.IO.ColorScheme() + 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)) diff --git a/pkg/cmd/repo/autolink/list/list_test.go b/pkg/cmd/repo/autolink/list/list_test.go index 3fc8e0261..1e4d73ab8 100644 --- a/pkg/cmd/repo/autolink/list/list_test.go +++ b/pkg/cmd/repo/autolink/list/list_test.go @@ -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" @@ -96,11 +97,11 @@ func TestNewCmdList(t *testing.T) { } type stubAutoLinkLister struct { - autolinks []autolink + 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 } @@ -125,7 +126,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []autolink{ + autolinks: []shared.Autolink{ { ID: 1, KeyPrefix: "TICKET-", @@ -161,7 +162,7 @@ func TestListRun(t *testing.T) { }, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []autolink{ + autolinks: []shared.Autolink{ { ID: 1, KeyPrefix: "TICKET-", @@ -184,7 +185,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: false, stubLister: stubAutoLinkLister{ - autolinks: []autolink{ + autolinks: []shared.Autolink{ { ID: 1, KeyPrefix: "TICKET-", @@ -210,7 +211,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []autolink{}, + autolinks: []shared.Autolink{}, }, expectedErr: cmdutil.NewNoResultsError("no autolinks found in OWNER/REPO"), wantStderr: "", @@ -220,7 +221,7 @@ func TestListRun(t *testing.T) { opts: &listOptions{}, isTTY: true, stubLister: stubAutoLinkLister{ - autolinks: []autolink{}, + autolinks: []shared.Autolink{}, err: testAutolinkClientListError{}, }, expectedErr: testAutolinkClientListError{}, diff --git a/pkg/cmd/repo/autolink/shared/autolink.go b/pkg/cmd/repo/autolink/shared/autolink.go new file mode 100644 index 000000000..28e975e7c --- /dev/null +++ b/pkg/cmd/repo/autolink/shared/autolink.go @@ -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) +} diff --git a/pkg/cmd/repo/autolink/view/http.go b/pkg/cmd/repo/autolink/view/http.go new file mode 100644 index 000000000..8dd6dc12d --- /dev/null +++ b/pkg/cmd/repo/autolink/view/http.go @@ -0,0 +1,46 @@ +package view + +import ( + "encoding/json" + "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 AutolinkViewer struct { + HTTPClient *http.Client +} + +func (a *AutolinkViewer) View(repo ghrepo.Interface, id string) (*shared.Autolink, 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.MethodGet, url, nil) + if err != nil { + return nil, err + } + + resp, err := a.HTTPClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("HTTP 404: Either no autolink with this ID exists for this repository or you are missing admin rights to the repository. (https://api.github.com/%s)", path) + } else if resp.StatusCode > 299 { + return nil, api.HandleHTTPError(resp) + } + + var autolink shared.Autolink + err = json.NewDecoder(resp.Body).Decode(&autolink) + + if err != nil { + return nil, err + } + + return &autolink, nil +} diff --git a/pkg/cmd/repo/autolink/view/http_test.go b/pkg/cmd/repo/autolink/view/http_test.go new file mode 100644 index 000000000..5bfe9369f --- /dev/null +++ b/pkg/cmd/repo/autolink/view/http_test.go @@ -0,0 +1,102 @@ +package view + +import ( + "fmt" + "net/http" + "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 TestAutolinkViewer_View(t *testing.T) { + repo := ghrepo.New("OWNER", "REPO") + + tests := []struct { + name string + id string + stubStatus int + stubRespJSON string + + expectedAutolink *shared.Autolink + expectErr bool + expectedErrMsg string + }{ + { + name: "200 successful alphanumeric view", + id: "123", + stubStatus: 200, + stubRespJSON: `{ + "id": 123, + "key_prefix": "TICKET-", + "url_template": "https://example.com/TICKET?query=", + "is_alphanumeric": true + }`, + expectedAutolink: &shared.Autolink{ + ID: 123, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + }, + { + name: "200 successful numeric view", + id: "123", + stubStatus: 200, + stubRespJSON: `{ + "id": 123, + "key_prefix": "TICKET-", + "url_template": "https://example.com/TICKET?query=", + "is_alphanumeric": false + }`, + expectedAutolink: &shared.Autolink{ + ID: 123, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: false, + }, + }, + { + name: "404 repo or autolink not found", + id: "123", + stubStatus: 404, + stubRespJSON: `{ + "message": "Not Found", + "documentation_url": "https://docs.github.com/rest/repos/autolinks#get-an-autolink-reference-of-a-repository", + "status": "404" + }`, + expectErr: true, + expectedErrMsg: "HTTP 404: Either no autolink with this ID exists for this repository or you are missing admin rights to the repository. (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.MethodGet, + fmt.Sprintf("repos/%s/%s/autolinks/%s", repo.RepoOwner(), repo.RepoName(), tt.id), + ), + httpmock.StatusStringResponse(tt.stubStatus, tt.stubRespJSON), + ) + defer reg.Verify(t) + + autolinkCreator := &AutolinkViewer{ + HTTPClient: &http.Client{Transport: reg}, + } + + autolink, err := autolinkCreator.View(repo, tt.id) + + if tt.expectErr { + require.EqualError(t, err, tt.expectedErrMsg) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedAutolink, autolink) + } + }) + } +} diff --git a/pkg/cmd/repo/autolink/view/view.go b/pkg/cmd/repo/autolink/view/view.go new file mode 100644 index 000000000..0b212aece --- /dev/null +++ b/pkg/cmd/repo/autolink/view/view.go @@ -0,0 +1,96 @@ +package view + +import ( + "fmt" + + "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 viewOptions struct { + BaseRepo func() (ghrepo.Interface, error) + Browser browser.Browser + AutolinkClient AutolinkViewClient + IO *iostreams.IOStreams + Exporter cmdutil.Exporter + + ID string +} + +type AutolinkViewClient interface { + View(repo ghrepo.Interface, id string) (*shared.Autolink, error) +} + +func NewCmdView(f *cmdutil.Factory, runF func(*viewOptions) error) *cobra.Command { + opts := &viewOptions{ + Browser: f.Browser, + IO: f.IOStreams, + } + + cmd := &cobra.Command{ + Use: "view ", + Short: "View an autolink reference", + Long: "View an autolink reference for a repository.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + httpClient, err := f.HttpClient() + if err != nil { + return err + } + + opts.BaseRepo = f.BaseRepo + opts.ID = args[0] + opts.AutolinkClient = &AutolinkViewer{HTTPClient: httpClient} + + if runF != nil { + return runF(opts) + } + + return viewRun(opts) + }, + } + + cmdutil.AddJSONFlags(cmd, &opts.Exporter, shared.AutolinkFields) + + return cmd +} + +func viewRun(opts *viewOptions) error { + repo, err := opts.BaseRepo() + if err != nil { + return err + } + + out := opts.IO.Out + cs := opts.IO.ColorScheme() + + autolink, err := opts.AutolinkClient.View(repo, opts.ID) + + if err != nil { + return fmt.Errorf("%s %w", cs.Red("error viewing autolink:"), err) + } + + if opts.Exporter != nil { + return opts.Exporter.Write(opts.IO, autolink) + } + + fmt.Fprintf(out, "Autolink in %s\n\n", ghrepo.FullName(repo)) + + fmt.Fprint(out, cs.Bold("ID: ")) + fmt.Fprintln(out, cs.Cyanf("%d", autolink.ID)) + + fmt.Fprint(out, cs.Bold("Key Prefix: ")) + fmt.Fprintln(out, autolink.KeyPrefix) + + fmt.Fprint(out, cs.Bold("URL Template: ")) + fmt.Fprintln(out, autolink.URLTemplate) + + fmt.Fprint(out, cs.Bold("Alphanumeric: ")) + fmt.Fprintln(out, autolink.IsAlphanumeric) + + return nil +} diff --git a/pkg/cmd/repo/autolink/view/view_test.go b/pkg/cmd/repo/autolink/view/view_test.go new file mode 100644 index 000000000..bc0a1bf7f --- /dev/null +++ b/pkg/cmd/repo/autolink/view/view_test.go @@ -0,0 +1,197 @@ +package view + +import ( + "bytes" + "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/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" + "github.com/google/shlex" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestJSONFields(t *testing.T) { + jsonfieldstest.ExpectCommandToSupportJSONFields(t, NewCmdView, []string{ + "id", + "isAlphanumeric", + "keyPrefix", + "urlTemplate", + }) +} + +func TestNewCmdView(t *testing.T) { + tests := []struct { + name string + input string + output viewOptions + wantErr bool + wantExporter bool + errMsg string + }{ + { + name: "no argument", + input: "", + wantErr: true, + errMsg: "accepts 1 arg(s), received 0", + }, + { + name: "id provided", + input: "123", + output: viewOptions{ID: "123"}, + }, + { + name: "json flag", + input: "123 --json id", + output: viewOptions{}, + wantExporter: true, + }, + { + name: "invalid json flag", + input: "123 --json invalid", + output: viewOptions{}, + wantErr: true, + errMsg: "Unknown JSON field: \"invalid\"\nAvailable fields:\n id\n isAlphanumeric\n keyPrefix\n urlTemplate", + }, + } + + 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 *viewOptions + cmd := NewCmdView(f, func(opts *viewOptions) 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.wantExporter, gotOpts.Exporter != nil) + } + }) + } +} + +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 +} + +type testAutolinkClientViewError struct{} + +func (e testAutolinkClientViewError) Error() string { + return "autolink client view error" +} + +func TestViewRun(t *testing.T) { + tests := []struct { + name string + opts *viewOptions + stubViewer stubAutoLinkViewer + expectedErr error + wantStdout string + }{ + { + name: "view", + opts: &viewOptions{ + ID: "1", + }, + stubViewer: stubAutoLinkViewer{ + autolink: &shared.Autolink{ + ID: 1, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + }, + wantStdout: heredoc.Doc(` + Autolink in OWNER/REPO + + ID: 1 + Key Prefix: TICKET- + URL Template: https://example.com/TICKET?query= + Alphanumeric: true + `), + }, + { + name: "view json", + opts: &viewOptions{ + Exporter: func() cmdutil.Exporter { + exporter := cmdutil.NewJSONExporter() + exporter.SetFields([]string{"id"}) + return exporter + }(), + }, + stubViewer: stubAutoLinkViewer{ + autolink: &shared.Autolink{ + ID: 1, + KeyPrefix: "TICKET-", + URLTemplate: "https://example.com/TICKET?query=", + IsAlphanumeric: true, + }, + }, + wantStdout: "{\"id\":1}\n", + }, + { + name: "client error", + opts: &viewOptions{}, + stubViewer: stubAutoLinkViewer{ + autolink: nil, + err: testAutolinkClientViewError{}, + }, + expectedErr: testAutolinkClientViewError{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, stdout, _ := 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.stubViewer + err := viewRun(opts) + + if tt.expectedErr != nil { + require.Error(t, err) + assert.ErrorIs(t, err, tt.expectedErr) + } else { + require.NoError(t, err) + assert.Equal(t, tt.wantStdout, stdout.String()) + } + }) + } +} diff --git a/pkg/cmd/run/list/list.go b/pkg/cmd/run/list/list.go index 793bfbb85..3113fdabd 100644 --- a/pkg/cmd/run/list/list.go +++ b/pkg/cmd/run/list/list.go @@ -119,6 +119,8 @@ func listRun(opts *ListOptions) error { } opts.IO.StartProgressIndicator() + defer opts.IO.StopProgressIndicator() + if opts.WorkflowSelector != "" { // initially the workflow state is limited to 'active' states := []workflowShared.WorkflowState{workflowShared.Active} diff --git a/pkg/httpmock/stub.go b/pkg/httpmock/stub.go index 196a047d8..4e61d12f4 100644 --- a/pkg/httpmock/stub.go +++ b/pkg/httpmock/stub.go @@ -188,7 +188,11 @@ func RESTPayload(responseStatus int, responseBody string, cb func(payload map[st return nil, err } cb(bodyData) - return httpResponse(responseStatus, req, bytes.NewBufferString(responseBody)), nil + + header := http.Header{ + "Content-Type": []string{"application/json"}, + } + return httpResponseWithHeader(responseStatus, req, bytes.NewBufferString(responseBody), header), nil } }