Merge remote-tracking branch 'origin/trunk' into feat/gist-edit

This commit is contained in:
nate smith 2022-01-24 17:23:18 -06:00
commit de4e37ed75
231 changed files with 9636 additions and 3873 deletions

View file

@ -17,6 +17,15 @@ jobs:
- name: Check out code
uses: actions/checkout@v2
- name: Cache Go modules
uses: actions/cache@v2
with:
path: ~/go
key: ${{ runner.os }}-build-${{ hashFiles('go.mod') }}
restore-keys: |
${{ runner.os }}-build-
${{ runner.os }}-
- name: Download dependencies
run: go mod download

19
.github/workflows/issueauto.yml vendored Normal file
View file

@ -0,0 +1,19 @@
name: Issue Automation
on:
issues:
types: [opened]
jobs:
issue-auto:
runs-on: ubuntu-latest
steps:
- name: label incoming issue
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }}
ISSUENUM: ${{ github.event.issue.number }}
ISSUEAUTHOR: ${{ github.event.issue.user.login }}
run: |
if ! gh api orgs/cli/public_members/$ISSUEAUTHOR --silent 2>/dev/null
then
gh issue edit $ISSUENUM --add-label "needs-triage"
fi

View file

@ -45,6 +45,10 @@ jobs:
if [ "$PR_AUTHOR_TYPE" = "Bot" ] || gh api orgs/cli/public_members/$PRAUTHOR --silent 2>/dev/null
then
if [ "$PR_AUTHOR_TYPE" != "Bot" ]
then
gh pr edit $PRNUM --add-assignee $PRAUTHOR
fi
if ! errtext="$(addToBoard 2>&1)"
then
cat <<<"$errtext" >&2
@ -56,6 +60,8 @@ jobs:
exit 0
fi
gh pr edit $PRNUM --add-label "external"
if [ "$PRHEAD" = "cli:trunk" ]
then
closePR

View file

@ -25,6 +25,8 @@ jobs:
-q .body > CHANGELOG.md
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Install osslsigncode
run: sudo apt-get install -y osslsigncode
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
@ -33,6 +35,8 @@ jobs:
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
GORELEASER_CURRENT_TAG: ${{steps.changelog.outputs.tag-name}}
GITHUB_CERT_PASSWORD: ${{secrets.GITHUB_CERT_PASSWORD}}
DESKTOP_CERT_TOKEN: ${{secrets.DESKTOP_CERT_TOKEN}}
- name: Checkout documentation site
uses: actions/checkout@v2
with:
@ -61,7 +65,6 @@ jobs:
api-write --silent projects/columns/cards/$card/moves -f position=top -F column_id=$DONE_COLUMN
done
echo "moved ${#cards[@]} cards to the Done column"
- name: Install packaging dependencies
run: sudo apt-get install -y rpm reprepro
- name: Set up GPG
@ -129,23 +132,19 @@ jobs:
unzip -o *.zip && rm -v *.zip
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Install go-msi
run: choco install -y "go-msi"
- name: Prepare PATH
shell: bash
run: |
echo "$WIX\\bin" >> $GITHUB_PATH
echo "C:\\Program Files\\go-msi" >> $GITHUB_PATH
id: setupmsbuild
uses: microsoft/setup-msbuild@v1.0.3
- name: Build MSI
id: buildmsi
shell: bash
env:
ZIP_FILE: ${{ steps.download_exe.outputs.zip }}
MSBUILD_PATH: ${{ steps.setupmsbuild.outputs.msbuildPath }}
run: |
mkdir -p build
msi="$(basename "$ZIP_FILE" ".zip").msi"
printf "::set-output name=msi::%s\n" "$msi"
go-msi make --msi "$PWD/$msi" --out "$PWD/build" --version "${GITHUB_REF#refs/tags/}"
name="$(basename "$ZIP_FILE" ".zip")"
version="$(echo -e ${GITHUB_REF#refs/tags/v} | sed s/-.*$//)"
"${MSBUILD_PATH}\MSBuild.exe" ./build/windows/gh.wixproj -p:SourceDir="$PWD" -p:OutputPath="$PWD" -p:OutputName="$name" -p:ProductVersion="$version"
- name: Obtain signing cert
id: obtain_cert
env:

View file

@ -8,7 +8,8 @@ release:
before:
hooks:
- go mod tidy
- make manpages
- make manpages GH_VERSION={{.Version}}
- ./script/prepare-windows-cert.sh '{{ if index .Env "GITHUB_CERT_PASSWORD" }}{{ .Env.GITHUB_CERT_PASSWORD}}{{ end }}' '{{ if index .Env "DESKTOP_CERT_TOKEN" }}{{ .Env.DESKTOP_CERT_TOKEN}}{{ end }}'
builds:
- <<: &build_defaults
@ -32,6 +33,9 @@ builds:
id: windows
goos: [windows]
goarch: [386, amd64]
hooks:
post:
- ./script/sign-windows-executable.sh '{{ .Path }}'
archives:
- id: nix

View file

@ -1,5 +1,5 @@
* @cli/code-reviewers
pkg/cmd/codespace/* @cli/codespaces
pkg/liveshare/* @cli/codespaces
internal/codespaces/* @cli/codespaces
pkg/cmd/codespace/ @cli/codespaces
pkg/liveshare/ @cli/codespaces
internal/codespaces/ @cli/codespaces

View file

@ -12,8 +12,8 @@ import (
"strings"
"github.com/cli/cli/v2/internal/ghinstance"
graphql "github.com/cli/shurcooL-graphql"
"github.com/henvic/httpretty"
"github.com/shurcooL/graphql"
)
// ClientOption represents an argument to NewClient
@ -98,6 +98,22 @@ func ReplaceTripper(tr http.RoundTripper) ClientOption {
}
}
// ExtractHeader extracts a named header from any response received by this client and, if non-blank, saves
// it to dest.
func ExtractHeader(name string, dest *string) ClientOption {
return func(tr http.RoundTripper) http.RoundTripper {
return &funcTripper{roundTrip: func(req *http.Request) (*http.Response, error) {
res, err := tr.RoundTrip(req)
if err == nil {
if value := res.Header.Get(name); value != "" {
*dest = value
}
}
return res, err
}}
}
}
type funcTripper struct {
roundTrip func(*http.Request) (*http.Response, error)
}
@ -124,7 +140,18 @@ type graphQLResponse struct {
type GraphQLError struct {
Type string
Message string
// Path []interface // mixed strings and numbers
Path []interface{} // mixed strings and numbers
}
func (ge GraphQLError) PathString() string {
var res strings.Builder
for i, v := range ge.Path {
if i > 0 {
res.WriteRune('.')
}
fmt.Fprintf(&res, "%v", v)
}
return res.String()
}
// GraphQLErrorResponse contains errors returned in a GraphQL response
@ -135,9 +162,31 @@ type GraphQLErrorResponse struct {
func (gr GraphQLErrorResponse) Error() string {
errorMessages := make([]string, 0, len(gr.Errors))
for _, e := range gr.Errors {
errorMessages = append(errorMessages, e.Message)
msg := e.Message
if p := e.PathString(); p != "" {
msg = fmt.Sprintf("%s (%s)", msg, p)
}
errorMessages = append(errorMessages, msg)
}
return fmt.Sprintf("GraphQL error: %s", strings.Join(errorMessages, "\n"))
return fmt.Sprintf("GraphQL: %s", strings.Join(errorMessages, ", "))
}
// Match checks if this error is only about a specific type on a specific path. If the path argument ends
// with a ".", it will match all its subpaths as well.
func (gr GraphQLErrorResponse) Match(expectType, expectPath string) bool {
for _, e := range gr.Errors {
if e.Type != expectType || !matchPath(e.PathString(), expectPath) {
return false
}
}
return true
}
func matchPath(p, expect string) bool {
if strings.HasSuffix(expect, ".") {
return strings.HasPrefix(p, expect) || p == strings.TrimSuffix(expect, ".")
}
return p == expect
}
// HTTPError is an error returned by a failed API call
@ -173,7 +222,7 @@ func (err HTTPError) ScopesSuggestion() string {
// ScopesSuggestion is an error messaging utility that prints the suggestion to request additional OAuth
// scopes in case a server response indicates that there are missing scopes.
func ScopesSuggestion(resp *http.Response) string {
if resp.StatusCode < 400 || resp.StatusCode > 499 {
if resp.StatusCode < 400 || resp.StatusCode > 499 || resp.StatusCode == 422 {
return ""
}
@ -221,7 +270,8 @@ func EndpointNeedsScopes(resp *http.Response, s string) *http.Response {
return resp
}
// GraphQL performs a GraphQL request and parses the response
// GraphQL performs a GraphQL request and parses the response. If there are errors in the response,
// *GraphQLErrorResponse will be returned, but the data will also be parsed into the receiver.
func (c Client) GraphQL(hostname string, query string, variables map[string]interface{}, data interface{}) error {
reqBody, err := json.Marshal(map[string]interface{}{"query": query, "variables": variables})
if err != nil {

View file

@ -50,15 +50,23 @@ func TestGraphQLError(t *testing.T) {
httpmock.GraphQL(""),
httpmock.StringResponse(`
{ "errors": [
{"message":"OH NO"},
{"message":"this is fine"}
{
"type": "NOT_FOUND",
"message": "OH NO",
"path": ["repository", "issue"]
},
{
"type": "ACTUALLY_ITS_FINE",
"message": "this is fine",
"path": ["repository", "issues", 0, "comments"]
}
]
}
`),
)
err := client.GraphQL("github.com", "", nil, &response)
if err == nil || err.Error() != "GraphQL error: OH NO\nthis is fine" {
if err == nil || err.Error() != "GraphQL: OH NO (repository.issue), this is fine (repository.issues.0.comments)" {
t.Fatalf("got %q", err.Error())
}
}
@ -200,6 +208,11 @@ func TestHTTPError_ScopesSuggestion(t *testing.T) {
resp: makeResponse(404, "https://api.github.com/gists", "", "gist, delete_repo"),
want: ``,
},
{
name: "http code is 422",
resp: makeResponse(422, "https://api.github.com/gists", "", "gist"),
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View file

@ -75,6 +75,8 @@ func (pr *PullRequest) ExportData(fields []string) map[string]interface{} {
data[f] = pr.ProjectCards.Nodes
case "reviews":
data[f] = pr.Reviews.Nodes
case "latestReviews":
data[f] = pr.LatestReviews.Nodes
case "files":
data[f] = pr.Files.Nodes
case "reviewRequests":

View file

@ -4,8 +4,8 @@ import (
"context"
"time"
graphql "github.com/cli/shurcooL-graphql"
"github.com/shurcooL/githubv4"
"github.com/shurcooL/graphql"
)
type Comments struct {

View file

@ -1,12 +1,10 @@
package api
import (
"context"
"fmt"
"time"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/shurcooL/githubv4"
)
type IssuesPayload struct {
@ -22,6 +20,7 @@ type IssuesAndTotalCount struct {
}
type Issue struct {
Typename string `json:"__typename"`
ID string
Number int
Title string
@ -41,6 +40,10 @@ type Issue struct {
ReactionGroups ReactionGroups
}
func (i Issue) IsPullRequest() bool {
return i.Typename == "PullRequest"
}
type Assignees struct {
Nodes []GitHubUser
TotalCount int
@ -68,17 +71,19 @@ func (l Labels) Names() []string {
}
type ProjectCards struct {
Nodes []struct {
Project struct {
Name string `json:"name"`
} `json:"project"`
Column struct {
Name string `json:"name"`
} `json:"column"`
}
Nodes []*ProjectInfo
TotalCount int
}
type ProjectInfo struct {
Project struct {
Name string `json:"name"`
} `json:"project"`
Column struct {
Name string `json:"name"`
} `json:"column"`
}
func (p ProjectCards) ProjectNames() []string {
names := make([]string, len(p.Nodes))
for i, c := range p.Nodes {
@ -230,194 +235,6 @@ func IssueStatus(client *Client, repo ghrepo.Interface, options IssueStatusOptio
return &payload, nil
}
func IssueByNumber(client *Client, repo ghrepo.Interface, number int) (*Issue, error) {
type response struct {
Repository struct {
Issue Issue
HasIssuesEnabled bool
}
}
query := `
query IssueByNumber($owner: String!, $repo: String!, $issue_number: Int!) {
repository(owner: $owner, name: $repo) {
hasIssuesEnabled
issue(number: $issue_number) {
id
title
state
body
author {
login
}
comments(last: 1) {
nodes {
author {
login
}
authorAssociation
body
createdAt
includesCreatedEdit
isMinimized
minimizedReason
reactionGroups {
content
users {
totalCount
}
}
}
totalCount
}
number
url
createdAt
assignees(first: 100) {
nodes {
id
name
login
}
totalCount
}
labels(first: 100) {
nodes {
id
name
description
color
}
totalCount
}
projectCards(first: 100) {
nodes {
project {
name
}
column {
name
}
}
totalCount
}
milestone {
number
title
description
dueOn
}
reactionGroups {
content
users {
totalCount
}
}
}
}
}`
variables := map[string]interface{}{
"owner": repo.RepoOwner(),
"repo": repo.RepoName(),
"issue_number": number,
}
var resp response
err := client.GraphQL(repo.RepoHost(), query, variables, &resp)
if err != nil {
return nil, err
}
if !resp.Repository.HasIssuesEnabled {
return nil, &IssuesDisabledError{fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(repo))}
}
return &resp.Repository.Issue, nil
}
func IssueClose(client *Client, repo ghrepo.Interface, issue Issue) error {
var mutation struct {
CloseIssue struct {
Issue struct {
ID githubv4.ID
}
} `graphql:"closeIssue(input: $input)"`
}
variables := map[string]interface{}{
"input": githubv4.CloseIssueInput{
IssueID: issue.ID,
},
}
gql := graphQLClient(client.http, repo.RepoHost())
err := gql.MutateNamed(context.Background(), "IssueClose", &mutation, variables)
if err != nil {
return err
}
return nil
}
func IssueReopen(client *Client, repo ghrepo.Interface, issue Issue) error {
var mutation struct {
ReopenIssue struct {
Issue struct {
ID githubv4.ID
}
} `graphql:"reopenIssue(input: $input)"`
}
variables := map[string]interface{}{
"input": githubv4.ReopenIssueInput{
IssueID: issue.ID,
},
}
gql := graphQLClient(client.http, repo.RepoHost())
err := gql.MutateNamed(context.Background(), "IssueReopen", &mutation, variables)
return err
}
func IssueDelete(client *Client, repo ghrepo.Interface, issue Issue) error {
var mutation struct {
DeleteIssue struct {
Repository struct {
ID githubv4.ID
}
} `graphql:"deleteIssue(input: $input)"`
}
variables := map[string]interface{}{
"input": githubv4.DeleteIssueInput{
IssueID: issue.ID,
},
}
gql := graphQLClient(client.http, repo.RepoHost())
err := gql.MutateNamed(context.Background(), "IssueDelete", &mutation, variables)
return err
}
func IssueUpdate(client *Client, repo ghrepo.Interface, params githubv4.UpdateIssueInput) error {
var mutation struct {
UpdateIssue struct {
Issue struct {
ID string
}
} `graphql:"updateIssue(input: $input)"`
}
variables := map[string]interface{}{"input": params}
gql := graphQLClient(client.http, repo.RepoHost())
err := gql.MutateNamed(context.Background(), "IssueUpdate", &mutation, variables)
return err
}
func (i Issue) Link() string {
return i.URL
}

View file

@ -64,7 +64,8 @@ type PullRequest struct {
BaseRef struct {
BranchProtectionRule struct {
RequiresStrictStatusChecks bool
RequiresStrictStatusChecks bool
RequiredApprovingReviewCount int
}
}
@ -108,6 +109,7 @@ type PullRequest struct {
Comments Comments
ReactionGroups ReactionGroups
Reviews PullRequestReviews
LatestReviews PullRequestReviews
ReviewRequests ReviewRequests
}
@ -405,6 +407,11 @@ func PullRequestStatus(client *Client, repo ghrepo.Interface, options StatusOpti
}
pullRequest(number: $number) {
...prWithReviews
baseRef {
branchProtectionRule {
requiredApprovingReviewCount
}
}
}
}
`
@ -519,7 +526,7 @@ func pullRequestFragment(httpClient *http.Client, hostname string) (string, erro
var reviewFields []string
if prFeatures.HasReviewDecision {
reviewFields = append(reviewFields, "reviewDecision")
reviewFields = append(reviewFields, "reviewDecision", "latestReviews")
}
fragments := fmt.Sprintf(`
@ -621,20 +628,6 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter
return pr, nil
}
func UpdatePullRequest(client *Client, repo ghrepo.Interface, params githubv4.UpdatePullRequestInput) error {
var mutation struct {
UpdatePullRequest struct {
PullRequest struct {
ID string
}
} `graphql:"updatePullRequest(input: $input)"`
}
variables := map[string]interface{}{"input": params}
gql := graphQLClient(client.http, repo.RepoHost())
err := gql.MutateNamed(context.Background(), "PullRequestUpdate", &mutation, variables)
return err
}
func UpdatePullRequestReviews(client *Client, repo ghrepo.Interface, params githubv4.RequestReviewsInput) error {
var mutation struct {
RequestReviews struct {
@ -660,7 +653,7 @@ func isBlank(v interface{}) bool {
}
}
func PullRequestClose(client *Client, repo ghrepo.Interface, pr *PullRequest) error {
func PullRequestClose(httpClient *http.Client, repo ghrepo.Interface, prID string) error {
var mutation struct {
ClosePullRequest struct {
PullRequest struct {
@ -671,17 +664,15 @@ func PullRequestClose(client *Client, repo ghrepo.Interface, pr *PullRequest) er
variables := map[string]interface{}{
"input": githubv4.ClosePullRequestInput{
PullRequestID: pr.ID,
PullRequestID: prID,
},
}
gql := graphQLClient(client.http, repo.RepoHost())
err := gql.MutateNamed(context.Background(), "PullRequestClose", &mutation, variables)
return err
gql := graphQLClient(httpClient, repo.RepoHost())
return gql.MutateNamed(context.Background(), "PullRequestClose", &mutation, variables)
}
func PullRequestReopen(client *Client, repo ghrepo.Interface, pr *PullRequest) error {
func PullRequestReopen(httpClient *http.Client, repo ghrepo.Interface, prID string) error {
var mutation struct {
ReopenPullRequest struct {
PullRequest struct {
@ -692,14 +683,12 @@ func PullRequestReopen(client *Client, repo ghrepo.Interface, pr *PullRequest) e
variables := map[string]interface{}{
"input": githubv4.ReopenPullRequestInput{
PullRequestID: pr.ID,
PullRequestID: prID,
},
}
gql := graphQLClient(client.http, repo.RepoHost())
err := gql.MutateNamed(context.Background(), "PullRequestReopen", &mutation, variables)
return err
gql := graphQLClient(httpClient, repo.RepoHost())
return gql.MutateNamed(context.Background(), "PullRequestReopen", &mutation, variables)
}
func PullRequestReady(client *Client, repo ghrepo.Interface, pr *PullRequest) error {

View file

@ -524,6 +524,26 @@ func ForkRepo(client *Client, repo ghrepo.Interface, org string) (*Repository, e
}, nil
}
func LastCommit(client *Client, repo ghrepo.Interface) (*Commit, error) {
var responseData struct {
Repository struct {
DefaultBranchRef struct {
Target struct {
Commit `graphql:"... on Commit"`
}
}
} `graphql:"repository(owner: $owner, name: $repo)"`
}
variables := map[string]interface{}{
"owner": githubv4.String(repo.RepoOwner()), "repo": githubv4.String(repo.RepoName()),
}
gql := graphQLClient(client.http, repo.RepoHost())
if err := gql.QueryNamed(context.Background(), "LastCommit", &responseData, variables); err != nil {
return nil, err
}
return &responseData.Repository.DefaultBranchRef.Target.Commit, nil
}
// RepoFindForks finds forks of the repo that are affiliated with the viewer
func RepoFindForks(client *Client, repo ghrepo.Interface, limit int) ([]*Repository, error) {
result := struct {
@ -741,7 +761,7 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput
go func() {
teams, err := OrganizationTeams(client, repo)
// TODO: better detection of non-org repos
if err != nil && !strings.HasPrefix(err.Error(), "Could not resolve to an Organization") {
if err != nil && !strings.Contains(err.Error(), "Could not resolve to an Organization") {
errc <- fmt.Errorf("error fetching organization teams: %w", err)
return
}
@ -952,7 +972,7 @@ func RepoAndOrgProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, e
orgProjects, err := OrganizationProjects(client, repo)
// TODO: better detection of non-org repos
if err != nil && !strings.HasPrefix(err.Error(), "Could not resolve to an Organization") {
if err != nil && !strings.Contains(err.Error(), "Could not resolve to an Organization") {
return projects, fmt.Errorf("error fetching organization projects: %w", err)
}
projects = append(projects, orgProjects...)
@ -963,6 +983,15 @@ func RepoAndOrgProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, e
type RepoAssignee struct {
ID string
Login string
Name string
}
// DisplayName returns a formatted string that uses Login and Name to be displayed e.g. 'Login (Name)' or 'Login'
func (ra RepoAssignee) DisplayName() string {
if ra.Name != "" {
return fmt.Sprintf("%s (%s)", ra.Login, ra.Name)
}
return ra.Login
}
// RepoAssignableUsers fetches all the assignable users for a repository
@ -1108,46 +1137,6 @@ func RepoMilestones(client *Client, repo ghrepo.Interface, state string) ([]Repo
return milestones, nil
}
func MilestoneByTitle(client *Client, repo ghrepo.Interface, state, title string) (*RepoMilestone, error) {
milestones, err := RepoMilestones(client, repo, state)
if err != nil {
return nil, err
}
for i := range milestones {
if strings.EqualFold(milestones[i].Title, title) {
return &milestones[i], nil
}
}
return nil, fmt.Errorf("no milestone found with title %q", title)
}
func MilestoneByNumber(client *Client, repo ghrepo.Interface, number int32) (*RepoMilestone, error) {
var query struct {
Repository struct {
Milestone *RepoMilestone `graphql:"milestone(number: $number)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}
variables := map[string]interface{}{
"owner": githubv4.String(repo.RepoOwner()),
"name": githubv4.String(repo.RepoName()),
"number": githubv4.Int(number),
}
gql := graphQLClient(client.http, repo.RepoHost())
err := gql.QueryNamed(context.Background(), "RepositoryMilestoneByNumber", &query, variables)
if err != nil {
return nil, err
}
if query.Repository.Milestone == nil {
return nil, fmt.Errorf("no milestone found with number '%d'", number)
}
return query.Repository.Milestone, nil
}
func ProjectNamesToPaths(client *Client, repo ghrepo.Interface, projectNames []string) ([]string, error) {
var paths []string
projects, err := RepoAndOrgProjects(client, repo)

View file

@ -23,7 +23,7 @@ func TestGitHubRepo_notFound(t *testing.T) {
if err == nil {
t.Fatal("GitHubRepo did not return an error")
}
if wants := "GraphQL error: Could not resolve to a Repository with the name 'OWNER/REPO'."; err.Error() != wants {
if wants := "GraphQL: Could not resolve to a Repository with the name 'OWNER/REPO'."; err.Error() != wants {
t.Errorf("GitHubRepo error: want %q, got %q", wants, err.Error())
}
if repo != nil {
@ -362,3 +362,28 @@ func Test_RepoMilestones(t *testing.T) {
}
}
}
func TestDisplayName(t *testing.T) {
tests := []struct {
name string
assignee RepoAssignee
want string
}{
{
name: "assignee with name",
assignee: RepoAssignee{"123", "octocat123", "Octavious Cath"},
want: "octocat123 (Octavious Cath)",
},
{
name: "assignee without name",
assignee: RepoAssignee{"123", "octocat123", ""},
want: "octocat123",
},
}
for _, tt := range tests {
actual := tt.assignee.DisplayName()
if actual != tt.want {
t.Errorf("display name was %s wanted %s", actual, tt.want)
}
}
}

View file

@ -35,6 +35,22 @@ var issueComments = shortenQuery(`
}
`)
var issueCommentLast = shortenQuery(`
comments(last: 1) {
nodes {
author{login},
authorAssociation,
body,
createdAt,
includesCreatedEdit,
isMinimized,
minimizedReason,
reactionGroups{content,users{totalCount}}
},
totalCount
}
`)
var prReviewRequests = shortenQuery(`
reviewRequests(first: 100) {
nodes {
@ -62,6 +78,19 @@ var prReviews = shortenQuery(`
reactionGroups{content,users{totalCount}}
}
pageInfo{hasNextPage,endCursor}
totalCount
}
`)
var prLatestReviews = shortenQuery(`
latestReviews(first: 100) {
nodes {
author{login},
authorAssociation,
submittedAt,
body,
state
}
}
`)
@ -163,6 +192,7 @@ var PullRequestFields = append(IssueFields,
"headRepositoryOwner",
"isCrossRepository",
"isDraft",
"latestReviews",
"maintainerCanModify",
"mergeable",
"mergeCommit",
@ -176,6 +206,8 @@ var PullRequestFields = append(IssueFields,
"statusCheckRollup",
)
// PullRequestGraphQL constructs a GraphQL query fragment for a set of pull request fields. Since GitHub
// pull requests are also technically issues, this function can be used to query issues as well.
func PullRequestGraphQL(fields []string) string {
var q []string
for _, field := range fields {
@ -204,10 +236,14 @@ func PullRequestGraphQL(fields []string) string {
q = append(q, `potentialMergeCommit{oid}`)
case "comments":
q = append(q, issueComments)
case "lastComment": // pseudo-field
q = append(q, issueCommentLast)
case "reviewRequests":
q = append(q, prReviewRequests)
case "reviews":
q = append(q, prReviews)
case "latestReviews":
q = append(q, prLatestReviews)
case "files":
q = append(q, prFiles)
case "commits":

38
build/windows/gh.wixproj Normal file
View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform Condition="'$(Platform)' == ''">x64</Platform>
<ProductVersion Condition="'$(ProductVersion)' == ''">0.1.0</ProductVersion>
<OutputName Condition="'$(OutputName)' == ''">$(MSBuildProjectName)</OutputName>
<OutputType>package</OutputType>
<RepoPath>$([MSBuild]::NormalizeDirectory($(MSBuildProjectDirectory)\..\..))</RepoPath>
<OutputPath Condition="'$(OutputPath)' == ''">$(RepoPath)bin\$(Platform)\</OutputPath>
<IntermediateOutputPath>$(RepoPath)bin\obj\$(Platform)\</IntermediateOutputPath>
<DefineConstants>
$(DefineConstants);
ProductVersion=$(ProductVersion);
</DefineConstants>
<SuppressIces Condition="'$(Platform)' == 'arm' Or '$(Platform)' == 'arm64'">ICE39</SuppressIces>
<DefineSolutionProperties>false</DefineSolutionProperties>
<WixTargetsPath Condition="'$(WixTargetsPath)' == ''">$(MSBuildExtensionsPath)\Microsoft\WiX\v3.x\Wix.targets</WixTargetsPath>
</PropertyGroup>
<ItemGroup>
<Compile Include="gh.wxs"/>
<Compile Include="ui.wxs"/>
</ItemGroup>
<ItemGroup>
<!-- Include directories containing both user-specified output and unzipped release for ease -->
<BindInputPaths Include="$(SourceDir)"/>
<BindInputPaths Include="$(SourceDir)\bin"/>
</ItemGroup>
<ItemGroup>
<WixExtension Include="WixUIExtension"/>
<WixExtension Include="WixUtilExtension"/>
</ItemGroup>
<Target Name="SetStepOutput" AfterTargets="Build" Condition="'$(GITHUB_ACTIONS)' != ''">
<!-- Make sure the correct target path is always set as the step output -->
<Message Importance="high" Text="::set-output name=msi::$(TargetPath)"/>
</Target>
<Import Project="$(WixTargetsPath)"/>
</Project>

77
build/windows/gh.wxs Normal file
View file

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<?ifndef ProductVersion?>
<?error ProductVersion property not defined?>
<?endif?>
<!-- Define a unique UpgradeCode per platform -->
<?if $(var.Platform) = "x64"?>
<?define InstallerVersion = "200"?>
<?define UpgradeCode = "8CFB9531-B959-4E1B-AA2E-4AF0FFCC4AF4"?>
<?define ProgramFilesFolder = "ProgramFiles64Folder"?>
<?elseif $(var.Platform) = "x86"?>
<?define InstallerVersion = "200"?>
<?define UpgradeCode = "767EC5D2-C8F0-4912-9901-45E21F59A284"?>
<?define ProgramFilesFolder = "ProgramFilesFolder"?>
<?elseif $(var.Platform) = "arm64"?>
<?define InstallerVersion = "500"?>
<?define UpgradeCode = "5D15E95C-F979-41B0-826C-C33C8CB5A7EB"?>
<?define ProgramFilesFolder = "ProgramFiles64Folder"?>
<?elseif $(var.Platform) = "arm"?>
<?define InstallerVersion = "500"?>
<?define UpgradeCode = "DDDE52AA-42DA-404B-9238-77DC86117CFF"?>
<?define ProgramFilesFolder = "ProgramFilesFolder"?>
<?endif?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*" Name="GitHub CLI" Version="$(var.ProductVersion)" Language="1033" Manufacturer="GitHub, Inc." UpgradeCode="$(var.UpgradeCode)">
<Package Compressed="yes" InstallerVersion="$(var.InstallerVersion)" InstallScope="perMachine"/>
<MediaTemplate EmbedCab="yes"/>
<!-- Remove older product(s) early but within the transaction -->
<MajorUpgrade Schedule="afterInstallInitialize" DowngradeErrorMessage="A newer version of !(bind.property.ProductName) is already installed."/>
<!-- Upgrade older x86 products -->
<Upgrade Id="7C0A5736-5B8E-4176-B350-613FA2D8A1B3">
<UpgradeVersion Maximum="$(var.ProductVersion)" Property="OLDERX86VERSIONDETECTED"/>
</Upgrade>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="$(var.ProgramFilesFolder)" Name="Program Files">
<Directory Id="INSTALLDIR" Name="GitHub CLI"/>
</Directory>
</Directory>
<!-- Restore the INSTALLDIR if previously persisted to the registry -->
<Property Id="INSTALLDIR">
<RegistrySearch Id="InstallDir" Root="HKLM" Key="SOFTWARE\GitHub\CLI" Name="InstallDir" Type="directory"/>
</Property>
<Feature Id="DefaultFeature" ConfigurableDirectory="INSTALLDIR">
<!-- @Guid will be automatically and durably assigned based on key path -->
<Component Directory="INSTALLDIR">
<File Name="gh.exe"/>
<Environment Id="Path" Action="set" Name="PATH" Part="last" System="yes" Value="[INSTALLDIR]"/>
</Component>
<!-- Persist the INSTALLDIR and restore it in subsequent installs -->
<Component Directory="INSTALLDIR">
<RegistryValue Root="HKLM" Key="SOFTWARE\GitHub\CLI" Name="InstallDir" Type="string" Value="[INSTALLDIR]"/>
</Component>
<Component Id="OlderX86Env" Guid="50C15744-A674-404B-873C-6B58957E2A32" Directory="TARGETDIR" Win64="no">
<Condition><![CDATA[OLDERX86VERSIONDETECTED]]></Condition>
<!-- Clean up the old x86 package default directory from the user environment -->
<Environment Id="OlderX86Path" Action="remove" Name="PATH" Part="last" System="no" Value="[ProgramFilesFolder]GitHub CLI\"/>
</Component>
</Feature>
<!-- Broadcast environment variable changes -->
<CustomActionRef Id="WixBroadcastEnvironmentChange" />
<!-- Use customized WixUI_InstallDir that removes WixUI_LicenseAgreementDlg -->
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR"/>
<UIRef Id="GitHubCLI_InstallDir"/>
</Product>
</Wix>

54
build/windows/ui.wxs Normal file
View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Fragment>
<UI Id="GitHubCLI_InstallDir">
<TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
<TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
<TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />
<Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
<Property Id="WixUI_Mode" Value="InstallDir" />
<DialogRef Id="BrowseDlg" />
<DialogRef Id="DiskCostDlg" />
<DialogRef Id="ErrorDlg" />
<DialogRef Id="FatalError" />
<DialogRef Id="FilesInUse" />
<DialogRef Id="MsiRMFilesInUse" />
<DialogRef Id="PrepareDlg" />
<DialogRef Id="ProgressDlg" />
<DialogRef Id="ResumeDlg" />
<DialogRef Id="UserExit" />
<Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath" Order="3">1</Publish>
<Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="4"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
<Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999">1</Publish>
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg">NOT Installed</Publish>
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">Installed AND PATCH</Publish>
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath" Order="2">NOT WIXUI_DONTVALIDATEPATH</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4">WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"</Publish>
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2">1</Publish>
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="InstallDirDlg" Order="1">NOT Installed</Publish>
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2">Installed AND NOT PATCH</Publish>
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">Installed AND PATCH</Publish>
<Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg">1</Publish>
<Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
<Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
<Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg">1</Publish>
<Property Id="ARPNOMODIFY" Value="1" />
</UI>
<UIRef Id="WixUI_Common" />
</Fragment>
</Wix>

View file

@ -58,13 +58,7 @@ func run(args []string) error {
}
if *manPage {
header := &docs.GenManHeader{
Title: "gh",
Section: "1",
Source: "",
Manual: "",
}
if err := docs.GenManTree(rootCmd, header, *dir); err != nil {
if err := docs.GenManTree(rootCmd, *dir); err != nil {
return err
}
}

View file

@ -18,7 +18,7 @@ func Test_run(t *testing.T) {
if err != nil {
t.Fatalf("error reading `gh-issue-create.1`: %v", err)
}
if !strings.Contains(string(manPage), `\fBgh issue create`) {
if !strings.Contains(string(manPage), `\fB\fCgh issue create`) {
t.Fatal("man page corrupted")
}

View file

@ -224,8 +224,9 @@ func mainRun() exitCode {
var httpErr api.HTTPError
if errors.As(err, &httpErr) && httpErr.StatusCode == 401 {
fmt.Fprintln(stderr, "Try authenticating with: gh auth login")
} else if strings.Contains(err.Error(), "Resource protected by organization SAML enforcement") {
fmt.Fprintln(stderr, "Try re-authenticating with: gh auth refresh")
} else if u := factory.SSOURL(); u != "" {
// handles organization SAML enforcement error
fmt.Fprintf(stderr, "Authorize in your web browser: %s\n", u)
} else if msg := httpErr.ScopesSuggestion(); msg != "" {
fmt.Fprintln(stderr, msg)
}

View file

@ -14,14 +14,12 @@ our release schedule.
Install:
```bash
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh
```
**Note**: If you get the error _"gpg: failed to start the dirmngr '/usr/bin/dirmngr': No such file or directory"_, try installing the `dirmngr` package: `sudo apt install dirmngr`.
Upgrade:
```bash
@ -106,6 +104,20 @@ Or via [pkg(8)](https://www.freebsd.org/cgi/man.cgi?pkg(8)):
pkg install gh
```
### NetBSD/pkgsrc
NetBSD users and those on [platforms supported by pkgsrc](https://pkgsrc.org/#index4h1) can install the [gh package](https://pkgsrc.se/net/gh):
```bash
pkgin install gh
```
To install from source:
```bash
cd /usr/pkgsrc/net/gh && make package-install
```
### OpenBSD
In -current, or in releases starting from 7.0, OpenBSD users can install from packages:

View file

@ -84,6 +84,15 @@ func CurrentBranch() (string, error) {
return "", fmt.Errorf("%sgit: %s", stderr.String(), err)
}
func listRemotesForPath(path string) ([]string, error) {
remoteCmd, err := GitCommand("-C", path, "remote", "-v")
if err != nil {
return nil, err
}
output, err := run.PrepareCmd(remoteCmd).Output()
return outputLines(output), err
}
func listRemotes() ([]string, error) {
remoteCmd, err := GitCommand("remote", "-v")
if err != nil {
@ -298,6 +307,19 @@ func CheckoutBranch(branch string) error {
return run.PrepareCmd(configCmd).Run()
}
// pull changes from remote branch without version history
func Pull(remote, branch string) error {
pullCmd, err := GitCommand("pull", "--ff-only", remote, branch)
if err != nil {
return err
}
pullCmd.Stdout = os.Stdout
pullCmd.Stderr = os.Stderr
pullCmd.Stdin = os.Stdin
return run.PrepareCmd(pullCmd).Run()
}
func parseCloneArgs(extraArgs []string) (args []string, target string) {
args = extraArgs
@ -366,6 +388,16 @@ func ToplevelDir() (string, error) {
}
// ToplevelDirFromPath returns the top-level given path of the current repository
func GetDirFromPath(p string) (string, error) {
showCmd, err := GitCommand("-C", p, "rev-parse", "--git-dir")
if err != nil {
return "", err
}
output, err := run.PrepareCmd(showCmd).Output()
return firstLine(output), err
}
func PathFromRepoRoot() string {
showCmd, err := GitCommand("rev-parse", "--show-prefix")
if err != nil {

View file

@ -35,16 +35,11 @@ func (r *Remote) String() string {
return r.Name
}
// Remotes gets the git remotes set for the current repo
func Remotes() (RemoteSet, error) {
list, err := listRemotes()
if err != nil {
return nil, err
}
remotes := parseRemotes(list)
func remotes(path string, remoteList []string) (RemoteSet, error) {
remotes := parseRemotes(remoteList)
// this is affected by SetRemoteResolution
remoteCmd, err := GitCommand("config", "--get-regexp", `^remote\..*\.gh-resolved$`)
remoteCmd, err := GitCommand("-C", path, "config", "--get-regexp", `^remote\..*\.gh-resolved$`)
if err != nil {
return nil, err
}
@ -70,6 +65,23 @@ func Remotes() (RemoteSet, error) {
return remotes, nil
}
func RemotesForPath(path string) (RemoteSet, error) {
list, err := listRemotesForPath(path)
if err != nil {
return nil, err
}
return remotes(path, list)
}
// Remotes gets the git remotes set for the current repo
func Remotes() (RemoteSet, error) {
list, err := listRemotes()
if err != nil {
return nil, err
}
return remotes(".", list)
}
func parseRemotes(gitRemotes []string) (remotes RemoteSet) {
for _, r := range gitRemotes {
match := remoteRE.FindStringSubmatch(r)
@ -140,6 +152,14 @@ func AddRemote(name, u string) (*Remote, error) {
}, nil
}
func UpdateRemoteURL(name, u string) error {
addCmd, err := GitCommand("remote", "set-url", name, u)
if err != nil {
return err
}
return run.PrepareCmd(addCmd).Run()
}
func SetRemoteResolution(name, resolution string) error {
addCmd, err := GitCommand("config", "--add", fmt.Sprintf("remote.%s.gh-resolved", name), resolution)
if err != nil {

View file

@ -30,9 +30,8 @@ func (m SSHAliasMap) Translator() func(*url.URL) *url.URL {
if !ok {
return u
}
// FIXME: cleanup domain logic
if strings.EqualFold(u.Hostname(), "github.com") && strings.EqualFold(resolvedHost, "ssh.github.com") {
return u
if strings.EqualFold(resolvedHost, "ssh.github.com") {
resolvedHost = "github.com"
}
newURL, _ := url.Parse(u.String())
newURL.Host = resolvedHost

View file

@ -128,12 +128,14 @@ func Test_Translator(t *testing.T) {
m := SSHAliasMap{
"gh": "github.com",
"github.com": "ssh.github.com",
"my.gh.com": "ssh.github.com",
}
tr := m.Translator()
cases := [][]string{
{"ssh://gh/o/r", "ssh://github.com/o/r"},
{"ssh://github.com/o/r", "ssh://github.com/o/r"},
{"ssh://my.gh.com", "ssh://github.com"},
{"https://gh/o/r", "https://gh/o/r"},
}
for _, c := range cases {

28
go.mod
View file

@ -5,44 +5,42 @@ go 1.16
require (
github.com/AlecAivazis/survey/v2 v2.3.2
github.com/MakeNowJust/heredoc v1.0.0
github.com/briandowns/spinner v1.13.0
github.com/charmbracelet/glamour v0.3.0
github.com/briandowns/spinner v1.18.0
github.com/charmbracelet/glamour v0.4.0
github.com/cli/browser v1.1.0
github.com/cli/oauth v0.9.0
github.com/cli/safeexec v1.0.0
github.com/cli/shurcooL-graphql v0.0.1
github.com/cpuguy83/go-md2man/v2 v2.0.1
github.com/creack/pty v1.1.17
github.com/gabriel-vasile/mimetype v1.4.0
github.com/google/go-cmp v0.5.6
github.com/google/go-cmp v0.5.7
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/gorilla/websocket v1.4.2
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.3.0
github.com/henvic/httpretty v0.0.6
github.com/itchyny/gojq v0.12.5
github.com/itchyny/gojq v0.12.6
github.com/joho/godotenv v1.4.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mattn/go-colorable v0.1.11
github.com/mattn/go-colorable v0.1.12
github.com/mattn/go-isatty v0.0.14
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
github.com/microcosm-cc/bluemonday v1.0.16 // indirect
github.com/muesli/reflow v0.2.1-0.20210502190812-c80126ec2ad5
github.com/muesli/reflow v0.3.0
github.com/muesli/termenv v0.9.0
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38
github.com/opentracing/opentracing-go v1.1.0
github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a // indirect
github.com/sourcegraph/jsonrpc2 v0.1.0
github.com/spf13/cobra v1.2.1
github.com/spf13/cobra v1.3.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/objx v0.1.1 // indirect
github.com/stretchr/testify v1.7.0
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)
replace github.com/shurcooL/graphql => github.com/cli/shurcooL-graphql v0.0.0-20200707151639-0f7232a2bf7e
replace golang.org/x/crypto => github.com/cli/crypto v0.0.0-20210929142629-6be313f59b03

337
go.sum
View file

@ -18,6 +18,15 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@ -26,7 +35,7 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@ -41,35 +50,44 @@ github.com/AlecAivazis/survey/v2 v2.3.2 h1:TqTB+aDDCLYhf9/bD2TwSO8u8jDSmMUd2SUVO
github.com/AlecAivazis/survey/v2 v2.3.2/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
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-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.8.2 h1:x3zkuE2lUk/RIekyAJ3XRqSCP4zwWDfcw/YJCuCAACg=
github.com/alecthomas/chroma v0.8.2/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/briandowns/spinner v1.13.0 h1:q/Y9LtpwtvL0CRzXrAMj0keVXqNhBYUFg6tBOUiY8ek=
github.com/briandowns/spinner v1.13.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ=
github.com/briandowns/spinner v1.18.0 h1:SJs0maNOs4FqhBwiJ3Gr7Z1D39/rukIVGQvpNZVHVcM=
github.com/briandowns/spinner v1.18.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/charmbracelet/glamour v0.3.0 h1:3H+ZrKlSg8s+WU6V7eF2eRVYt8lCueffbi7r2+ffGkc=
github.com/charmbracelet/glamour v0.3.0/go.mod h1:TzF0koPZhqq0YVBNL100cPHznAAjVj7fksX2RInwjGw=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/glamour v0.4.0 h1:scR+smyB7WdmrlIaff6IVlm48P48JaNM7JypM/VGl4k=
github.com/charmbracelet/glamour v0.4.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q=
github.com/cli/browser v1.1.0 h1:xOZBfkfY9L9vMBgqb1YwRirGu6QFaQ5dP/vXt5ENSOY=
github.com/cli/browser v1.1.0/go.mod h1:HKMQAt9t12kov91Mn7RfZxyJQQgWgyS/3SZswlZ5iTI=
@ -79,48 +97,65 @@ github.com/cli/oauth v0.9.0 h1:nxBC0Df4tUzMkqffAB+uZvisOwT3/N9FpkfdTDtafxc=
github.com/cli/oauth v0.9.0/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4=
github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/cli/shurcooL-graphql v0.0.0-20200707151639-0f7232a2bf7e h1:aq/1jlmtZoS6nlSp3yLOTZQ50G+dzHdeRNENgE/iBew=
github.com/cli/shurcooL-graphql v0.0.0-20200707151639-0f7232a2bf7e/go.mod h1:it23pLwxmz6OyM6I5O0ATIXQS1S190Nas26L5Kahp4U=
github.com/cli/shurcooL-graphql v0.0.1 h1:/9J3t9O6p1B8zdBBtQighq5g7DQRItBwuwGh3SocsKM=
github.com/cli/shurcooL-graphql v0.0.1/go.mod h1:U7gCSuMZP/Qy7kbqkk5PrqXEeDgtfG5K+W+u8weorps=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro=
github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@ -129,6 +164,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -147,6 +183,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -160,12 +197,14 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@ -177,66 +216,90 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
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/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/graph-gophers/graphql-go v0.0.0-20200622220639-c1d9693c95a6/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
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.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw=
github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs=
github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/itchyny/go-flags v1.5.0/go.mod h1:lenkYuCobuxLBAd/HGFE4LRoW8D3B6iXRQfWYJ+MNbA=
github.com/itchyny/gojq v0.12.5 h1:6SJ1BQ1VAwJAlIvLSIZmqHP/RUEq3qfVWvsRxrqhsD0=
github.com/itchyny/gojq v0.12.5/go.mod h1:3e1hZXv+Kwvdp6V9HXpVrvddiHVApi5EDZwS+zLFeiE=
github.com/itchyny/gojq v0.12.6 h1:VjaFn59Em2wTxDNGcrRkDK9ZHMNa8IksOgL13sLL4d0=
github.com/itchyny/gojq v0.12.6/go.mod h1:ZHrkfu7A+RbZLy5J1/JKpS4poEqrzItSTGDItqsfP0A=
github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921iRkU=
github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@ -244,90 +307,108 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.6/go.mod h1:HOT/6NaBlR0f9XlxD3zolN6Z3N8Lp4pvhp+jLS5ihnI=
github.com/microcosm-cc/bluemonday v1.0.16 h1:kHmAq2t7WPWLjiGvzKa5o3HzSfahUKiOq7fAPUiMNIc=
github.com/microcosm-cc/bluemonday v1.0.16/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
github.com/microcosm-cc/bluemonday v1.0.17 h1:Z1a//hgsQ4yjC+8zEkV8IWySkXnsxmdSY642CTFQb5Y=
github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/muesli/reflow v0.2.0/go.mod h1:qT22vjVmM9MIUeLgsVYe/Ye7eZlbv9dZjL3dVhUqLX8=
github.com/muesli/reflow v0.2.1-0.20210502190812-c80126ec2ad5 h1:T+Fc6qGlSfM+z0JPlp+n5rijvlg6C6JYFSNaqnCifDU=
github.com/muesli/reflow v0.2.1-0.20210502190812-c80126ec2ad5/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/fQSFKo0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8=
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b h1:0/ecDXh/HTHRtSDSFnD2/Ta1yQ5J76ZspVY4u0/jGFk=
github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a h1:KikTa6HtAK8cS1qjvUvvq4QO21QnwC+EfvB+OAuZ/ZU=
github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sourcegraph/jsonrpc2 v0.1.0 h1:ohJHjZ+PcaLxDUjqk2NC3tIGsVa5bXThe1ZheSXOjuk=
github.com/sourcegraph/jsonrpc2 v0.1.0/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0=
github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
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.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -340,18 +421,19 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.3/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs=
github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@ -359,6 +441,7 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
@ -397,19 +480,22 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -431,11 +517,14 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -447,8 +536,13 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 h1:0Ja1LBD+yisY6RWM/BH7TJVXWsSjs2VwBSmvSX4HdBc=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -463,30 +557,35 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cO
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -499,6 +598,7 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -507,11 +607,20 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
@ -522,8 +631,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -533,7 +643,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@ -541,9 +650,9 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@ -577,7 +686,11 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -604,7 +717,17 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -653,7 +776,29 @@ google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -673,7 +818,15 @@ google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA5
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -685,15 +838,21 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -27,13 +27,11 @@ type iconfig interface {
Write() error
}
func AuthFlowWithConfig(cfg iconfig, IO *iostreams.IOStreams, hostname, notice string, additionalScopes []string) (string, error) {
func AuthFlowWithConfig(cfg iconfig, IO *iostreams.IOStreams, hostname, notice string, additionalScopes []string, isInteractive bool) (string, error) {
// TODO this probably shouldn't live in this package. It should probably be in a new package that
// depends on both iostreams and config.
stderr := IO.ErrOut
cs := IO.ColorScheme()
token, userLogin, err := authFlow(hostname, IO, notice, additionalScopes)
token, userLogin, err := authFlow(hostname, IO, notice, additionalScopes, isInteractive)
if err != nil {
return "", err
}
@ -47,19 +45,10 @@ func AuthFlowWithConfig(cfg iconfig, IO *iostreams.IOStreams, hostname, notice s
return "", err
}
err = cfg.Write()
if err != nil {
return "", err
}
fmt.Fprintf(stderr, "%s Authentication complete. %s to continue...\n",
cs.SuccessIcon(), cs.Bold("Press Enter"))
_ = waitForEnter(IO.In)
return token, nil
return token, cfg.Write()
}
func authFlow(oauthHost string, IO *iostreams.IOStreams, notice string, additionalScopes []string) (string, string, error) {
func authFlow(oauthHost string, IO *iostreams.IOStreams, notice string, additionalScopes []string, isInteractive bool) (string, string, error) {
w := IO.ErrOut
cs := IO.ColorScheme()
@ -90,7 +79,12 @@ func authFlow(oauthHost string, IO *iostreams.IOStreams, notice string, addition
return nil
},
BrowseURL: func(url string) error {
fmt.Fprintf(w, "- %s to open %s in your browser... ", cs.Bold("Press Enter"), oauthHost)
if !isInteractive {
fmt.Fprintf(w, "%s to continue in your web browser: %s\n", cs.Bold("Open this URL"), url)
return nil
}
fmt.Fprintf(w, "%s to open %s in your browser... ", cs.Bold("Press Enter"), oauthHost)
_ = waitForEnter(IO.In)
// FIXME: read the browser from cmd Factory rather than recreating it
@ -103,7 +97,7 @@ func authFlow(oauthHost string, IO *iostreams.IOStreams, notice string, addition
return nil
},
WriteSuccessHTML: func(w io.Writer) {
fmt.Fprintln(w, oauthSuccessPage)
fmt.Fprint(w, oauthSuccessPage)
},
HTTPClient: httpClient,
Stdin: IO.In,

View file

@ -45,25 +45,40 @@ import (
"github.com/opentracing/opentracing-go"
)
const githubAPI = "https://api.github.com"
const (
githubServer = "https://github.com"
githubAPI = "https://api.github.com"
vscsAPI = "https://online.visualstudio.com"
)
// API is the interface to the codespace service.
type API struct {
token string
client httpClient
githubAPI string
client httpClient
vscsAPI string
githubAPI string
githubServer string
}
type httpClient interface {
Do(req *http.Request) (*http.Response, error)
}
// New creates a new API client with the given token and HTTP client.
func New(token string, httpClient httpClient) *API {
// New creates a new API client connecting to the configured endpoints with the HTTP client.
func New(serverURL, apiURL, vscsURL string, httpClient httpClient) *API {
if serverURL == "" {
serverURL = githubServer
}
if apiURL == "" {
apiURL = githubAPI
}
if vscsURL == "" {
vscsURL = vscsAPI
}
return &API{
token: token,
client: httpClient,
githubAPI: githubAPI,
client: httpClient,
vscsAPI: strings.TrimSuffix(vscsURL, "/"),
githubAPI: strings.TrimSuffix(apiURL, "/"),
githubServer: strings.TrimSuffix(serverURL, "/"),
}
}
@ -386,7 +401,7 @@ type getCodespaceRegionLocationResponse struct {
// GetCodespaceRegionLocation returns the closest codespace location for the user.
func (a *API) GetCodespaceRegionLocation(ctx context.Context) (string, error) {
req, err := http.NewRequest(http.MethodGet, "https://online.visualstudio.com/api/v1/locations", nil)
req, err := http.NewRequest(http.MethodGet, a.vscsAPI+"/api/v1/locations", nil)
if err != nil {
return "", fmt.Errorf("error creating request: %w", err)
}
@ -415,8 +430,9 @@ func (a *API) GetCodespaceRegionLocation(ctx context.Context) (string, error) {
}
type Machine struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
PrebuildAvailability string `json:"prebuild_availability"`
}
// GetCodespacesMachines returns the codespaces machines for the given repo, branch and location.
@ -460,14 +476,17 @@ func (a *API) GetCodespacesMachines(ctx context.Context, repoID int, branch, loc
// CreateCodespaceParams are the required parameters for provisioning a Codespace.
type CreateCodespaceParams struct {
RepositoryID int
Branch, Machine, Location string
RepositoryID int
IdleTimeoutMinutes int
Branch string
Machine string
Location string
}
// CreateCodespace creates a codespace with the given parameters and returns a non-nil error if it
// fails to create.
func (a *API) CreateCodespace(ctx context.Context, params *CreateCodespaceParams) (*Codespace, error) {
codespace, err := a.startCreate(ctx, params.RepositoryID, params.Machine, params.Branch, params.Location)
codespace, err := a.startCreate(ctx, params)
if err != errProvisioningInProgress {
return codespace, err
}
@ -502,10 +521,11 @@ func (a *API) CreateCodespace(ctx context.Context, params *CreateCodespaceParams
}
type startCreateRequest struct {
RepositoryID int `json:"repository_id"`
Ref string `json:"ref"`
Location string `json:"location"`
Machine string `json:"machine"`
RepositoryID int `json:"repository_id"`
IdleTimeoutMinutes int `json:"idle_timeout_minutes,omitempty"`
Ref string `json:"ref"`
Location string `json:"location"`
Machine string `json:"machine"`
}
var errProvisioningInProgress = errors.New("provisioning in progress")
@ -514,8 +534,18 @@ var errProvisioningInProgress = errors.New("provisioning in progress")
// It may return success or an error, or errProvisioningInProgress indicating that the operation
// did not complete before the GitHub API's time limit for RPCs (10s), in which case the caller
// must poll the server to learn the outcome.
func (a *API) startCreate(ctx context.Context, repoID int, machine, branch, location string) (*Codespace, error) {
requestBody, err := json.Marshal(startCreateRequest{repoID, branch, location, machine})
func (a *API) startCreate(ctx context.Context, params *CreateCodespaceParams) (*Codespace, error) {
if params == nil {
return nil, errors.New("startCreate missing parameters")
}
requestBody, err := json.Marshal(startCreateRequest{
RepositoryID: params.RepositoryID,
IdleTimeoutMinutes: params.IdleTimeoutMinutes,
Ref: params.Branch,
Location: params.Location,
Machine: params.Machine,
})
if err != nil {
return nil, fmt.Errorf("error marshaling request: %w", err)
}
@ -620,7 +650,7 @@ func (a *API) GetCodespaceRepositoryContents(ctx context.Context, codespace *Cod
// AuthorizedKeys returns the public keys (in ~/.ssh/authorized_keys
// format) registered by the specified GitHub user.
func (a *API) AuthorizedKeys(ctx context.Context, user string) ([]byte, error) {
url := fmt.Sprintf("https://github.com/%s.keys", user)
url := fmt.Sprintf("%s/%s.keys", a.githubServer, user)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
@ -654,8 +684,5 @@ func (a *API) do(ctx context.Context, req *http.Request, spanName string) (*http
// setHeaders sets the required headers for the API.
func (a *API) setHeaders(req *http.Request) {
if a.token != "" {
req.Header.Set("Authorization", "Bearer "+a.token)
}
req.Header.Set("Accept", "application/vnd.github.v3+json")
}

View file

@ -72,7 +72,6 @@ func TestListCodespaces_limited(t *testing.T) {
api := API{
githubAPI: svr.URL,
client: &http.Client{},
token: "faketoken",
}
ctx := context.TODO()
codespaces, err := api.ListCodespaces(ctx, 200)
@ -98,7 +97,6 @@ func TestListCodespaces_unlimited(t *testing.T) {
api := API{
githubAPI: svr.URL,
client: &http.Client{},
token: "faketoken",
}
ctx := context.TODO()
codespaces, err := api.ListCodespaces(ctx, -1)

View file

@ -43,7 +43,7 @@ func PollPostCreateStates(ctx context.Context, progress progressIndicator, apiCl
session, err := ConnectToLiveshare(ctx, progress, noopLogger, apiClient, codespace)
if err != nil {
return fmt.Errorf("connect to Live Share: %w", err)
return fmt.Errorf("connect to codespace: %w", err)
}
defer func() {
if closeErr := session.Close(); err == nil {

View file

@ -49,7 +49,7 @@ func ConfigDir() string {
}
// State path precedence
// 1. XDG_CONFIG_HOME
// 1. XDG_STATE_HOME
// 2. LocalAppData (windows only)
// 3. HOME
func StateDir() string {

View file

@ -80,13 +80,13 @@ example.com:
`)()
config, err := parseConfig("config.yml")
assert.NoError(t, err)
val, err := config.Get("example.com", "git_protocol")
val, err := config.GetOrDefault("example.com", "git_protocol")
assert.NoError(t, err)
assert.Equal(t, "https", val)
val, err = config.Get("github.com", "git_protocol")
val, err = config.GetOrDefault("github.com", "git_protocol")
assert.NoError(t, err)
assert.Equal(t, "ssh", val)
val, err = config.Get("nonexistent.io", "git_protocol")
val, err = config.GetOrDefault("nonexistent.io", "git_protocol")
assert.NoError(t, err)
assert.Equal(t, "ssh", val)
}

View file

@ -6,7 +6,7 @@ import (
"gopkg.in/yaml.v3"
)
// This type implements a low-level get/set config that is backed by an in-memory tree of Yaml
// This type implements a low-level get/set config that is backed by an in-memory tree of yaml
// nodes. It allows us to interact with a yaml-based config programmatically, preserving any
// comments that were present when the yaml was parsed.
type ConfigMap struct {
@ -37,41 +37,41 @@ func (cm *ConfigMap) GetStringValue(key string) (string, error) {
func (cm *ConfigMap) SetStringValue(key, value string) error {
entry, err := cm.FindEntry(key)
if err == nil {
entry.ValueNode.Value = value
return nil
}
var notFound *NotFoundError
valueNode := entry.ValueNode
if err != nil && errors.As(err, &notFound) {
keyNode := &yaml.Node{
Kind: yaml.ScalarNode,
Value: key,
}
valueNode = &yaml.Node{
Kind: yaml.ScalarNode,
Tag: "!!str",
Value: "",
}
cm.Root.Content = append(cm.Root.Content, keyNode, valueNode)
} else if err != nil {
if err != nil && !errors.As(err, &notFound) {
return err
}
valueNode.Value = value
keyNode := &yaml.Node{
Kind: yaml.ScalarNode,
Value: key,
}
valueNode := &yaml.Node{
Kind: yaml.ScalarNode,
Tag: "!!str",
Value: value,
}
cm.Root.Content = append(cm.Root.Content, keyNode, valueNode)
return nil
}
func (cm *ConfigMap) FindEntry(key string) (ce *ConfigEntry, err error) {
err = nil
func (cm *ConfigMap) FindEntry(key string) (*ConfigEntry, error) {
ce := &ConfigEntry{}
ce = &ConfigEntry{}
if cm.Empty() {
return ce, &NotFoundError{errors.New("not found")}
}
// Content slice goes [key1, value1, key2, value2, ...]
// Content slice goes [key1, value1, key2, value2, ...].
topLevelPairs := cm.Root.Content
for i, v := range topLevelPairs {
// Skip every other slice item since we only want to check against keys
// Skip every other slice item since we only want to check against keys.
if i%2 != 0 {
continue
}
@ -81,7 +81,7 @@ func (cm *ConfigMap) FindEntry(key string) (ce *ConfigEntry, err error) {
if i+1 < len(topLevelPairs) {
ce.ValueNode = topLevelPairs[i+1]
}
return
return ce, nil
}
}
@ -89,14 +89,23 @@ func (cm *ConfigMap) FindEntry(key string) (ce *ConfigEntry, err error) {
}
func (cm *ConfigMap) RemoveEntry(key string) {
if cm.Empty() {
return
}
newContent := []*yaml.Node{}
content := cm.Root.Content
for i := 0; i < len(content); i++ {
if content[i].Value == key {
i++ // skip the next node which is this key's value
var skipNext bool
for i, v := range cm.Root.Content {
if skipNext {
skipNext = false
continue
}
if i%2 != 0 || v.Value != key {
newContent = append(newContent, v)
} else {
newContent = append(newContent, content[i])
// Don't append current node and skip the next which is this key's value.
skipNext = true
}
}

View file

@ -1,7 +1,6 @@
package config
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
@ -46,12 +45,135 @@ func TestFindEntry(t *testing.T) {
return
}
assert.NoError(t, err)
fmt.Println(out)
assert.Equal(t, tt.output, out.ValueNode.Value)
})
}
}
func TestEmpty(t *testing.T) {
cm := ConfigMap{}
assert.Equal(t, true, cm.Empty())
cm.Root = &yaml.Node{
Content: []*yaml.Node{
{
Value: "test",
},
},
}
assert.Equal(t, false, cm.Empty())
}
func TestGetStringValue(t *testing.T) {
tests := []struct {
name string
key string
wantValue string
wantErr bool
}{
{
name: "get key",
key: "valid",
wantValue: "present",
},
{
name: "get key that is not present",
key: "invalid",
wantErr: true,
},
{
name: "get key that has same content as a value",
key: "same",
wantValue: "logical",
},
}
for _, tt := range tests {
cm := ConfigMap{Root: testYaml()}
t.Run(tt.name, func(t *testing.T) {
val, err := cm.GetStringValue(tt.key)
if tt.wantErr {
assert.EqualError(t, err, "not found")
return
}
assert.Equal(t, tt.wantValue, val)
})
}
}
func TestSetStringValue(t *testing.T) {
tests := []struct {
name string
key string
value string
}{
{
name: "set key that is not present",
key: "notPresent",
value: "test1",
},
{
name: "set key that is present",
key: "erroneous",
value: "test2",
},
{
name: "set key that is blank",
key: "blank",
value: "test3",
},
{
name: "set key that has same content as a value",
key: "present",
value: "test4",
},
}
for _, tt := range tests {
cm := ConfigMap{Root: testYaml()}
t.Run(tt.name, func(t *testing.T) {
err := cm.SetStringValue(tt.key, tt.value)
assert.NoError(t, err)
val, err := cm.GetStringValue(tt.key)
assert.NoError(t, err)
assert.Equal(t, tt.value, val)
})
}
}
func TestRemoveEntry(t *testing.T) {
tests := []struct {
name string
key string
wantLength int
}{
{
name: "remove key",
key: "erroneous",
wantLength: 6,
},
{
name: "remove key that is not present",
key: "invalid",
wantLength: 8,
},
{
name: "remove key that has same content as a value",
key: "same",
wantLength: 6,
},
}
for _, tt := range tests {
cm := ConfigMap{Root: testYaml()}
t.Run(tt.name, func(t *testing.T) {
cm.RemoveEntry(tt.key)
assert.Equal(t, tt.wantLength, len(cm.Root.Content))
_, err := cm.FindEntry(tt.key)
assert.EqualError(t, err, "not found")
})
}
}
func testYaml() *yaml.Node {
var root yaml.Node
var data = `

View file

@ -9,7 +9,10 @@ import (
// This interface describes interacting with some persistent configuration for gh.
type Config interface {
Get(string, string) (string, error)
GetOrDefault(string, string) (string, error)
GetWithSource(string, string) (string, string, error)
GetOrDefaultWithSource(string, string) (string, string, error)
Default(string) string
Set(string, string, string) error
UnsetHost(string)
Hosts() ([]string, error)

View file

@ -58,7 +58,7 @@ func Test_defaultConfig(t *testing.T) {
assert.Equal(t, expected, mainBuf.String())
assert.Equal(t, "", hostsBuf.String())
proto, err := cfg.Get("", "git_protocol")
proto, err := cfg.GetOrDefault("", "git_protocol")
assert.NoError(t, err)
assert.Equal(t, "https", proto)

View file

@ -3,6 +3,7 @@ package config
import (
"fmt"
"os"
"strconv"
"github.com/cli/cli/v2/internal/ghinstance"
)
@ -13,6 +14,7 @@ const (
GITHUB_TOKEN = "GITHUB_TOKEN"
GH_ENTERPRISE_TOKEN = "GH_ENTERPRISE_TOKEN"
GITHUB_ENTERPRISE_TOKEN = "GITHUB_ENTERPRISE_TOKEN"
CODESPACES = "CODESPACES"
)
type ReadOnlyEnvError struct {
@ -74,6 +76,24 @@ func (c *envConfig) GetWithSource(hostname, key string) (string, string, error)
return c.Config.GetWithSource(hostname, key)
}
func (c *envConfig) GetOrDefault(hostname, key string) (val string, err error) {
val, _, err = c.GetOrDefaultWithSource(hostname, key)
return
}
func (c *envConfig) GetOrDefaultWithSource(hostname, key string) (val string, src string, err error) {
val, src, err = c.GetWithSource(hostname, key)
if err == nil && val == "" {
val = c.Default(key)
}
return
}
func (c *envConfig) Default(key string) string {
return c.Config.Default(key)
}
func (c *envConfig) CheckWriteable(hostname, key string) error {
if hostname != "" && key == "oauth_token" {
if token, env := AuthTokenFromEnv(hostname); token != "" {
@ -90,7 +110,15 @@ func AuthTokenFromEnv(hostname string) (string, string) {
return token, GH_ENTERPRISE_TOKEN
}
return os.Getenv(GITHUB_ENTERPRISE_TOKEN), GITHUB_ENTERPRISE_TOKEN
if token := os.Getenv(GITHUB_ENTERPRISE_TOKEN); token != "" {
return token, GITHUB_ENTERPRISE_TOKEN
}
if isCodespaces, _ := strconv.ParseBool(os.Getenv(CODESPACES)); isCodespaces {
return os.Getenv(GITHUB_TOKEN), GITHUB_TOKEN
}
return "", ""
}
if token := os.Getenv(GH_TOKEN); token != "" {

View file

@ -8,6 +8,18 @@ import (
"github.com/stretchr/testify/assert"
)
func setenv(t *testing.T, key, newValue string) {
oldValue, hasValue := os.LookupEnv(key)
os.Setenv(key, newValue)
t.Cleanup(func() {
if hasValue {
os.Setenv(key, oldValue)
} else {
os.Unsetenv(key)
}
})
}
func TestInheritEnv(t *testing.T) {
orig_GITHUB_TOKEN := os.Getenv("GITHUB_TOKEN")
orig_GITHUB_ENTERPRISE_TOKEN := os.Getenv("GITHUB_ENTERPRISE_TOKEN")
@ -36,6 +48,7 @@ func TestInheritEnv(t *testing.T) {
GITHUB_ENTERPRISE_TOKEN string
GH_TOKEN string
GH_ENTERPRISE_TOKEN string
CODESPACES string
hostname string
wants wants
}{
@ -98,6 +111,19 @@ func TestInheritEnv(t *testing.T) {
writeable: true,
},
},
{
name: "GITHUB_TOKEN allowed in Codespaces",
baseConfig: ``,
GITHUB_TOKEN: "OTOKEN",
hostname: "example.org",
CODESPACES: "true",
wants: wants{
hosts: []string{"github.com"},
token: "OTOKEN",
source: "GITHUB_TOKEN",
writeable: false,
},
},
{
name: "GITHUB_ENTERPRISE_TOKEN over blank config",
baseConfig: ``,
@ -262,11 +288,12 @@ func TestInheritEnv(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
os.Setenv("GITHUB_TOKEN", tt.GITHUB_TOKEN)
os.Setenv("GITHUB_ENTERPRISE_TOKEN", tt.GITHUB_ENTERPRISE_TOKEN)
os.Setenv("GH_TOKEN", tt.GH_TOKEN)
os.Setenv("GH_ENTERPRISE_TOKEN", tt.GH_ENTERPRISE_TOKEN)
os.Setenv("AppData", "")
setenv(t, "GITHUB_TOKEN", tt.GITHUB_TOKEN)
setenv(t, "GITHUB_ENTERPRISE_TOKEN", tt.GITHUB_ENTERPRISE_TOKEN)
setenv(t, "GH_TOKEN", tt.GH_TOKEN)
setenv(t, "GH_ENTERPRISE_TOKEN", tt.GH_ENTERPRISE_TOKEN)
setenv(t, "AppData", "")
setenv(t, "CODESPACES", tt.CODESPACES)
baseCfg := NewFromString(tt.baseConfig)
cfg := InheritEnv(baseCfg)

View file

@ -65,13 +65,26 @@ func (c *fileConfig) GetWithSource(hostname, key string) (string, string, error)
return "", defaultSource, err
}
if value == "" {
return defaultFor(key), defaultSource, nil
}
return value, defaultSource, nil
}
func (c *fileConfig) GetOrDefault(hostname, key string) (val string, err error) {
val, _, err = c.GetOrDefaultWithSource(hostname, key)
return
}
func (c *fileConfig) GetOrDefaultWithSource(hostname, key string) (val string, src string, err error) {
val, src, err = c.GetWithSource(hostname, key)
if err != nil && val == "" {
val = c.Default(key)
}
return
}
func (c *fileConfig) Default(key string) string {
return defaultFor(key)
}
func (c *fileConfig) Set(hostname, key, value string) error {
if hostname == "" {
return c.SetStringValue(key, value)

View file

@ -25,6 +25,23 @@ func (c ConfigStub) GetWithSource(host, key string) (string, string, error) {
return "", "", errors.New("not found")
}
func (c ConfigStub) GetOrDefault(hostname, key string) (val string, err error) {
val, _, err = c.GetOrDefaultWithSource(hostname, key)
return
}
func (c ConfigStub) GetOrDefaultWithSource(hostname, key string) (val string, src string, err error) {
val, src, err = c.GetWithSource(hostname, key)
if err == nil && val == "" {
val = c.Default(key)
}
return
}
func (c ConfigStub) Default(key string) string {
return defaultFor(key)
}
func (c ConfigStub) Set(host, key, value string) error {
c[genKey(host, key)] = value
return nil

View file

@ -79,12 +79,14 @@ var dummyCmd = &cobra.Command{
}
func checkStringContains(t *testing.T, got, expected string) {
t.Helper()
if !strings.Contains(got, expected) {
t.Errorf("Expected to contain: \n %v\nGot:\n %v\n", expected, got)
}
}
func checkStringOmits(t *testing.T, got, expected string) {
t.Helper()
if strings.Contains(got, expected) {
t.Errorf("Expected to not contain: \n %v\nGot: %v", expected, got)
}

View file

@ -6,7 +6,6 @@ import (
"io"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
@ -21,9 +20,8 @@ import (
// correctly if your command names have `-` in them. If you have `cmd` with two
// subcmds, `sub` and `sub-third`, and `sub` has a subcommand called `third`
// it is undefined which help output will be in the file `cmd-sub-third.1`.
func GenManTree(cmd *cobra.Command, header *GenManHeader, dir string) error {
func GenManTree(cmd *cobra.Command, dir string) error {
return GenManTreeFromOpts(cmd, GenManTreeOptions{
Header: header,
Path: dir,
CommandSeparator: "-",
})
@ -32,10 +30,6 @@ func GenManTree(cmd *cobra.Command, header *GenManHeader, dir string) error {
// GenManTreeFromOpts generates a man page for the command and all descendants.
// The pages are written to the opts.Path directory.
func GenManTreeFromOpts(cmd *cobra.Command, opts GenManTreeOptions) error {
header := opts.Header
if header == nil {
header = &GenManHeader{}
}
for _, c := range cmd.Commands() {
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
continue
@ -44,11 +38,8 @@ func GenManTreeFromOpts(cmd *cobra.Command, opts GenManTreeOptions) error {
return err
}
}
section := "1"
if header.Section != "" {
section = header.Section
}
section := "1"
separator := "_"
if opts.CommandSeparator != "" {
separator = opts.CommandSeparator
@ -61,14 +52,21 @@ func GenManTreeFromOpts(cmd *cobra.Command, opts GenManTreeOptions) error {
}
defer f.Close()
headerCopy := *header
return GenMan(cmd, &headerCopy, f)
var versionString string
if v := os.Getenv("GH_VERSION"); v != "" {
versionString = "GitHub CLI " + v
}
return GenMan(cmd, &GenManHeader{
Section: section,
Source: versionString,
Manual: "GitHub CLI manual",
}, f)
}
// GenManTreeOptions is the options for generating the man pages.
// Used only in GenManTreeFromOpts.
type GenManTreeOptions struct {
Header *GenManHeader
Path string
CommandSeparator string
}
@ -80,7 +78,6 @@ type GenManHeader struct {
Title string
Section string
Date *time.Time
date string
Source string
Manual string
}
@ -88,14 +85,12 @@ type GenManHeader struct {
// GenMan will generate a man page for the given command and write it to
// w. The header argument may be nil, however obviously w may not.
func GenMan(cmd *cobra.Command, header *GenManHeader, w io.Writer) error {
if header == nil {
header = &GenManHeader{}
}
if err := fillHeader(header, cmd.CommandPath()); err != nil {
return err
}
b := genMan(cmd, header)
_, err := w.Write(md2man.Render(b))
return err
}
@ -118,51 +113,40 @@ func fillHeader(header *GenManHeader, name string) error {
}
header.Date = &now
}
header.date = (*header.Date).Format("Jan 2006")
return nil
}
func manPreamble(buf *bytes.Buffer, header *GenManHeader, cmd *cobra.Command, dashedName string) {
description := cmd.Long
if len(description) == 0 {
description = cmd.Short
}
buf.WriteString(fmt.Sprintf(`%% "%s" "%s" "%s" "%s" "%s"
# NAME
`, header.Title, header.Section, header.date, header.Source, header.Manual))
`, header.Title, header.Section, header.Date.Format("Jan 2006"), header.Source, header.Manual))
buf.WriteString(fmt.Sprintf("%s \\- %s\n\n", dashedName, cmd.Short))
buf.WriteString("# SYNOPSIS\n")
buf.WriteString(fmt.Sprintf("**%s**\n\n", cmd.UseLine()))
buf.WriteString("# DESCRIPTION\n")
buf.WriteString(description + "\n\n")
buf.WriteString(fmt.Sprintf("`%s`\n\n", cmd.UseLine()))
if cmd.Long != "" && cmd.Long != cmd.Short {
buf.WriteString("# DESCRIPTION\n")
buf.WriteString(cmd.Long + "\n\n")
}
}
func manPrintFlags(buf *bytes.Buffer, flags *pflag.FlagSet) {
flags.VisitAll(func(flag *pflag.Flag) {
if len(flag.Deprecated) > 0 || flag.Hidden {
if len(flag.Deprecated) > 0 || flag.Hidden || flag.Name == "help" {
return
}
format := ""
varname, usage := pflag.UnquoteUsage(flag)
if len(flag.Shorthand) > 0 && len(flag.ShorthandDeprecated) == 0 {
format = fmt.Sprintf("**-%s**, **--%s**", flag.Shorthand, flag.Name)
buf.WriteString(fmt.Sprintf("`-%s`, `--%s`", flag.Shorthand, flag.Name))
} else {
format = fmt.Sprintf("**--%s**", flag.Name)
buf.WriteString(fmt.Sprintf("`--%s`", flag.Name))
}
if len(flag.NoOptDefVal) > 0 {
format += "["
}
if flag.Value.Type() == "string" {
// put quotes on the value
format += "=%q"
if varname == "" {
buf.WriteString("\n")
} else {
format += "=%s"
buf.WriteString(fmt.Sprintf(" `<%s>`\n", varname))
}
if len(flag.NoOptDefVal) > 0 {
format += "]"
}
format += "\n\t%s\n\n"
buf.WriteString(fmt.Sprintf(format, flag.DefValue, flag.Usage))
buf.WriteString(fmt.Sprintf(": %s\n\n", usage))
})
}
@ -174,7 +158,7 @@ func manPrintOptions(buf *bytes.Buffer, command *cobra.Command) {
buf.WriteString("\n")
}
flags = command.InheritedFlags()
if flags.HasAvailableFlags() {
if hasNonHelpFlags(flags) {
buf.WriteString("# OPTIONS INHERITED FROM PARENT COMMANDS\n")
manPrintFlags(buf, flags)
buf.WriteString("\n")
@ -191,52 +175,28 @@ func genMan(cmd *cobra.Command, header *GenManHeader) []byte {
buf := new(bytes.Buffer)
manPreamble(buf, header, cmd, dashCommandName)
for _, g := range subcommandGroups(cmd) {
if len(g.Commands) == 0 {
continue
}
fmt.Fprintf(buf, "# %s\n", strings.ToUpper(g.Name))
for _, subcmd := range g.Commands {
fmt.Fprintf(buf, "`%s`\n: %s\n\n", manLink(subcmd), subcmd.Short)
}
}
manPrintOptions(buf, cmd)
if len(cmd.Example) > 0 {
buf.WriteString("# EXAMPLE\n")
buf.WriteString(fmt.Sprintf("```\n%s\n```\n", cmd.Example))
}
if hasSeeAlso(cmd) {
if cmd.HasParent() {
buf.WriteString("# SEE ALSO\n")
seealsos := make([]string, 0)
if cmd.HasParent() {
parentPath := cmd.Parent().CommandPath()
dashParentPath := strings.Replace(parentPath, " ", "-", -1)
seealso := fmt.Sprintf("**%s(%s)**", dashParentPath, header.Section)
seealsos = append(seealsos, seealso)
}
children := cmd.Commands()
sort.Sort(byName(children))
for _, c := range children {
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
continue
}
seealso := fmt.Sprintf("**%s-%s(%s)**", dashCommandName, c.Name(), header.Section)
seealsos = append(seealsos, seealso)
}
buf.WriteString(strings.Join(seealsos, ", ") + "\n")
buf.WriteString(fmt.Sprintf("`%s`\n", manLink(cmd.Parent())))
}
return buf.Bytes()
}
// Test to see if we have a reason to print See Also information in docs
// Basically this is a test for a parent command or a subcommand which is
// both not deprecated and not the autogenerated help command.
func hasSeeAlso(cmd *cobra.Command) bool {
if cmd.HasParent() {
return true
}
for _, c := range cmd.Commands() {
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
continue
}
return true
}
return false
func manLink(cmd *cobra.Command) string {
p := cmd.CommandPath()
return fmt.Sprintf("%s(%d)", strings.Replace(p, " ", "-", -1), 1)
}
type byName []*cobra.Command
func (s byName) Len() int { return len(s) }
func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }

View file

@ -20,7 +20,7 @@ func translate(in string) string {
func TestGenManDoc(t *testing.T) {
header := &GenManHeader{
Title: "Project",
Section: "2",
Section: "1",
}
// We generate on a subcommand so we have both subcommands and parents
@ -49,7 +49,7 @@ func TestGenManDoc(t *testing.T) {
func TestGenManNoHiddenParents(t *testing.T) {
header := &GenManHeader{
Title: "Project",
Section: "2",
Section: "1",
}
// We generate on a subcommand so we have both subcommands and parents
@ -94,15 +94,8 @@ func TestGenManSeeAlso(t *testing.T) {
t.Fatal(err)
}
scanner := bufio.NewScanner(buf)
if err := assertLineFound(scanner, ".SH SEE ALSO"); err != nil {
t.Fatalf("Couldn't find SEE ALSO section header: %v", err)
}
if err := assertNextLineEquals(scanner, ".PP"); err != nil {
t.Fatalf("First line after SEE ALSO wasn't break-indent: %v", err)
}
if err := assertNextLineEquals(scanner, `\fBroot-bbb(1)\fP, \fBroot-ccc(1)\fP`); err != nil {
t.Fatalf("Second line after SEE ALSO wasn't correct: %v", err)
if err := assertLineFound(scanner, ".SH SEE ALSO"); err == nil {
t.Fatalf("Did not expect SEE ALSO section header")
}
}
@ -115,31 +108,26 @@ func TestManPrintFlagsHidesShortDeprecated(t *testing.T) {
manPrintFlags(buf, c.Flags())
got := buf.String()
expected := "**--foo**=\"default\"\n\tFoo flag\n\n"
expected := "`--foo` `<string>`\n: Foo flag\n\n"
if got != expected {
t.Errorf("Expected %v, got %v", expected, got)
t.Errorf("Expected %q, got %q", expected, got)
}
}
func TestGenManTree(t *testing.T) {
c := &cobra.Command{Use: "do [OPTIONS] arg1 arg2"}
header := &GenManHeader{Section: "2"}
tmpdir, err := ioutil.TempDir("", "test-gen-man-tree")
if err != nil {
t.Fatalf("Failed to create tmpdir: %s", err.Error())
}
defer os.RemoveAll(tmpdir)
if err := GenManTree(c, header, tmpdir); err != nil {
if err := GenManTree(c, tmpdir); err != nil {
t.Fatalf("GenManTree failed: %s", err.Error())
}
if _, err := os.Stat(filepath.Join(tmpdir, "do.2")); err != nil {
t.Fatalf("Expected file 'do.2' to exist")
}
if header.Title != "" {
t.Fatalf("Expected header.Title to be unmodified")
if _, err := os.Stat(filepath.Join(tmpdir, "do.1")); err != nil {
t.Fatalf("Expected file 'do.1' to exist")
}
}
@ -158,22 +146,6 @@ func assertLineFound(scanner *bufio.Scanner, expectedLine string) error {
return fmt.Errorf("hit EOF before finding %v", expectedLine)
}
func assertNextLineEquals(scanner *bufio.Scanner, expectedLine string) error {
if scanner.Scan() {
line := scanner.Text()
if line == expectedLine {
return nil
}
return fmt.Errorf("got %v, not %v", line, expectedLine)
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("scan failed: %v", err)
}
return fmt.Errorf("hit EOF before finding %v", expectedLine)
}
func BenchmarkGenManToFile(b *testing.B) {
file, err := ioutil.TempFile(b.TempDir(), "")
if err != nil {

View file

@ -1,35 +1,83 @@
package docs
import (
"bytes"
"fmt"
"html/template"
"io"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func printOptions(buf *bytes.Buffer, cmd *cobra.Command, name string) error {
func printOptions(w io.Writer, cmd *cobra.Command) error {
flags := cmd.NonInheritedFlags()
flags.SetOutput(buf)
flags.SetOutput(w)
if flags.HasAvailableFlags() {
buf.WriteString("### Options\n\n```\n")
flags.PrintDefaults()
buf.WriteString("```\n\n")
fmt.Fprint(w, "### Options\n\n")
if err := printFlagsHTML(w, flags); err != nil {
return err
}
fmt.Fprint(w, "\n\n")
}
parentFlags := cmd.InheritedFlags()
parentFlags.SetOutput(buf)
if parentFlags.HasAvailableFlags() {
buf.WriteString("### Options inherited from parent commands\n\n```\n")
parentFlags.PrintDefaults()
buf.WriteString("```\n\n")
parentFlags.SetOutput(w)
if hasNonHelpFlags(parentFlags) {
fmt.Fprint(w, "### Options inherited from parent commands\n\n")
if err := printFlagsHTML(w, parentFlags); err != nil {
return err
}
fmt.Fprint(w, "\n\n")
}
return nil
}
func hasNonHelpFlags(fs *pflag.FlagSet) (found bool) {
fs.VisitAll(func(f *pflag.Flag) {
if !f.Hidden && f.Name != "help" {
found = true
}
})
return
}
type flagView struct {
Name string
Varname string
Shorthand string
Usage string
}
var flagsTemplate = `
<dl class="flags">{{ range . }}
<dt>{{ if .Shorthand }}<code>-{{.Shorthand}}</code>, {{ end -}}
<code>--{{.Name}}{{ if .Varname }} &lt;{{.Varname}}&gt;{{ end }}</code></dt>
<dd>{{.Usage}}</dd>
{{ end }}</dl>
`
var tpl = template.Must(template.New("flags").Parse(flagsTemplate))
func printFlagsHTML(w io.Writer, fs *pflag.FlagSet) error {
var flags []flagView
fs.VisitAll(func(f *pflag.Flag) {
if f.Hidden || f.Name == "help" {
return
}
varname, usage := pflag.UnquoteUsage(f)
flags = append(flags, flagView{
Name: f.Name,
Varname: varname,
Shorthand: f.Shorthand,
Usage: usage,
})
})
return tpl.Execute(w, flags)
}
// GenMarkdown creates markdown output.
func GenMarkdown(cmd *cobra.Command, w io.Writer) error {
return GenMarkdownCustom(cmd, w, func(s string) string { return s })
@ -37,33 +85,97 @@ func GenMarkdown(cmd *cobra.Command, w io.Writer) error {
// GenMarkdownCustom creates custom markdown output.
func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error {
cmd.InitDefaultHelpCmd()
cmd.InitDefaultHelpFlag()
fmt.Fprintf(w, "## %s\n\n", cmd.CommandPath())
buf := new(bytes.Buffer)
name := cmd.CommandPath()
buf.WriteString("## " + name + "\n\n")
buf.WriteString(cmd.Short + "\n\n")
if len(cmd.Long) > 0 {
buf.WriteString("### Synopsis\n\n")
buf.WriteString(cmd.Long + "\n\n")
hasLong := cmd.Long != ""
if !hasLong {
fmt.Fprintf(w, "%s\n\n", cmd.Short)
}
if cmd.Runnable() {
fmt.Fprintf(w, "```\n%s\n```\n\n", cmd.UseLine())
}
if hasLong {
fmt.Fprintf(w, "%s\n\n", cmd.Long)
}
if cmd.Runnable() {
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.UseLine()))
for _, g := range subcommandGroups(cmd) {
if len(g.Commands) == 0 {
continue
}
fmt.Fprintf(w, "### %s\n\n", g.Name)
for _, subcmd := range g.Commands {
fmt.Fprintf(w, "* [%s](%s)\n", subcmd.CommandPath(), linkHandler(cmdManualPath(subcmd)))
}
fmt.Fprint(w, "\n\n")
}
if err := printOptions(w, cmd); err != nil {
return err
}
if len(cmd.Example) > 0 {
buf.WriteString("### Examples\n\n")
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.Example))
fmt.Fprint(w, "### Examples\n\n{% highlight bash %}{% raw %}\n")
fmt.Fprint(w, cmd.Example)
fmt.Fprint(w, "{% endraw %}{% endhighlight %}\n\n")
}
if err := printOptions(buf, cmd, name); err != nil {
return err
if cmd.HasParent() {
p := cmd.Parent()
fmt.Fprint(w, "### See also\n\n")
fmt.Fprintf(w, "* [%s](%s)\n", p.CommandPath(), linkHandler(cmdManualPath(p)))
}
return nil
}
type commandGroup struct {
Name string
Commands []*cobra.Command
}
// subcommandGroups lists child commands of a Cobra command split into groups.
// TODO: have rootHelpFunc use this instead of repeating the same logic.
func subcommandGroups(c *cobra.Command) []commandGroup {
var rest []*cobra.Command
var core []*cobra.Command
var actions []*cobra.Command
for _, subcmd := range c.Commands() {
if !subcmd.IsAvailableCommand() {
continue
}
if _, ok := subcmd.Annotations["IsCore"]; ok {
core = append(core, subcmd)
} else if _, ok := subcmd.Annotations["IsActions"]; ok {
actions = append(actions, subcmd)
} else {
rest = append(rest, subcmd)
}
}
if len(core) > 0 {
return []commandGroup{
{
Name: "Core commands",
Commands: core,
},
{
Name: "Actions commands",
Commands: actions,
},
{
Name: "Additional commands",
Commands: rest,
},
}
}
return []commandGroup{
{
Name: "Commands",
Commands: rest,
},
}
_, err := buf.WriteTo(w)
return err
}
// GenMarkdownTree will generate a markdown page for this command and all
@ -92,12 +204,7 @@ func GenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHa
}
}
basename := strings.Replace(cmd.CommandPath(), " ", "_", -1) + ".md"
if basenameOverride, found := cmd.Annotations["markdown:basename"]; found {
basename = basenameOverride + ".md"
}
filename := filepath.Join(dir, basename)
filename := filepath.Join(dir, cmdManualPath(cmd))
f, err := os.Create(filename)
if err != nil {
return err
@ -112,3 +219,10 @@ func GenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHa
}
return nil
}
func cmdManualPath(c *cobra.Command) string {
if basenameOverride, found := c.Annotations["markdown:basename"]; found {
return basenameOverride + ".md"
}
return strings.ReplaceAll(c.CommandPath(), " ", "_") + ".md"
}

View file

@ -72,13 +72,23 @@ func RESTPrefix(hostname string) string {
}
func GistPrefix(hostname string) string {
prefix := "https://"
if strings.EqualFold(hostname, localhost) {
prefix = "http://"
}
return prefix + GistHost(hostname)
}
func GistHost(hostname string) string {
if IsEnterprise(hostname) {
return fmt.Sprintf("https://%s/gist/", hostname)
return fmt.Sprintf("%s/gist/", hostname)
}
if strings.EqualFold(hostname, localhost) {
return fmt.Sprintf("http://%s/gist/", hostname)
return fmt.Sprintf("%s/gist/", hostname)
}
return fmt.Sprintf("https://gist.%s/", hostname)
return fmt.Sprintf("gist.%s/", hostname)
}
func HostPrefix(hostname string) string {

View file

@ -53,6 +53,12 @@ func SetDefaultHost(host string) {
// FromFullName extracts the GitHub repository information from the following
// formats: "OWNER/REPO", "HOST/OWNER/REPO", and a full URL.
func FromFullName(nwo string) (Interface, error) {
return FromFullNameWithHost(nwo, defaultHost())
}
// FromFullNameWithHost is like FromFullName that defaults to a specific host for values that don't
// explicitly include a hostname.
func FromFullNameWithHost(nwo, fallbackHost string) (Interface, error) {
if git.IsURL(nwo) {
u, err := git.ParseURL(nwo)
if err != nil {
@ -71,7 +77,7 @@ func FromFullName(nwo string) (Interface, error) {
case 3:
return NewWithHost(parts[1], parts[2], parts[0]), nil
case 2:
return NewWithHost(parts[0], parts[1], defaultHost()), nil
return NewWithHost(parts[0], parts[1], fallbackHost), nil
default:
return nil, fmt.Errorf(`expected the "[HOST/]OWNER/REPO" format, got %q`, nwo)
}
@ -105,7 +111,9 @@ func IsSame(a, b Interface) bool {
func GenerateRepoURL(repo Interface, p string, args ...interface{}) string {
baseURL := fmt.Sprintf("%s%s/%s", ghinstance.HostPrefix(repo.RepoHost()), repo.RepoOwner(), repo.RepoName())
if p != "" {
return baseURL + "/" + fmt.Sprintf(p, args...)
if path := fmt.Sprintf(p, args...); path != "" {
return baseURL + "/" + path
}
}
return baseURL
}

View file

@ -1,8 +1,6 @@
package actions
import (
"fmt"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -14,11 +12,8 @@ func NewCmdActions(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "actions",
Short: "Learn about working with GitHub actions",
Short: "Learn about working with GitHub Actions",
Long: actionsExplainer(cs),
Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintln(f.IOStreams.Out, actionsExplainer(cs))
},
Annotations: map[string]string{
"IsActions": "true",
},

View file

@ -21,6 +21,7 @@ import (
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/factory"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/export"
"github.com/cli/cli/v2/pkg/iostreams"
@ -71,21 +72,23 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
The endpoint argument should either be a path of a GitHub API v3 endpoint, or
"graphql" to access the GitHub API v4.
Placeholder values "{owner}", "{repo}", and "{branch}" in the endpoint argument will
get replaced with values from the repository of the current directory. Note that in
some shells, for example PowerShell, you may need to enclose any value that contains
"{...}" in quotes to prevent the shell from applying special meaning to curly braces.
Placeholder values "{owner}", "{repo}", and "{branch}" in the endpoint
argument will get replaced with values from the repository of the current
directory or the repository specified in the GH_REPO environment variable.
Note that in some shells, for example PowerShell, you may need to enclose
any value that contains "{...}" in quotes to prevent the shell from
applying special meaning to curly braces.
The default HTTP request method is "GET" normally and "POST" if any parameters
were added. Override the method with %[1]s--method%[1]s.
Pass one or more %[1]s--raw-field%[1]s values in "key=value" format to add string
parameters to the request payload. To add non-string parameters, see %[1]s--field%[1]s below.
Note that adding request parameters will automatically switch the request method to POST.
To send the parameters as a GET query string instead, use %[1]s--method%[1]s GET.
Pass one or more %[1]s-f/--raw-field%[1]s values in "key=value" format to add static string
parameters to the request payload. To add non-string or otherwise dynamic values, see
%[1]s--field%[1]s below. Note that adding request parameters will automatically switch the
request method to POST. To send the parameters as a GET query string instead, use
%[1]s--method GET%[1]s.
The %[1]s--field%[1]s flag behaves like %[1]s--raw-field%[1]s with magic type conversion based
on the format of the value:
The %[1]s-F/--field%[1]s flag has magic type conversion based on the format of the value:
- literal values "true", "false", "null", and integer numbers get converted to
appropriate JSON types;
@ -167,6 +170,9 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
`),
},
Args: cobra.ExactArgs(1),
PreRun: func(c *cobra.Command, args []string) {
opts.BaseRepo = cmdutil.OverrideBaseRepoFunc(f, "")
},
RunE: func(c *cobra.Command, args []string) error {
opts.RequestPath = args[0]
opts.RequestMethodPassed = c.Flags().Changed("method")
@ -393,6 +399,9 @@ func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream
if msg := api.ScopesSuggestion(resp); msg != "" {
fmt.Fprintf(opts.IO.ErrOut, "gh: %s\n", msg)
}
if u := factory.SSOURL(); u != "" {
fmt.Fprintf(opts.IO.ErrOut, "Authorize in your web browser: %s\n", u)
}
err = cmdutil.SilentError
return
}
@ -540,36 +549,58 @@ func parseErrorResponse(r io.Reader, statusCode int) (io.Reader, string, error)
var parsedBody struct {
Message string
Errors []json.RawMessage
Errors json.RawMessage
}
err = json.Unmarshal(b, &parsedBody)
if err != nil {
return r, "", err
return bodyCopy, "", err
}
if len(parsedBody.Errors) > 0 && parsedBody.Errors[0] == '"' {
var stringError string
if err := json.Unmarshal(parsedBody.Errors, &stringError); err != nil {
return bodyCopy, "", err
}
if stringError != "" {
if parsedBody.Message != "" {
return bodyCopy, fmt.Sprintf("%s (%s)", stringError, parsedBody.Message), nil
}
return bodyCopy, stringError, nil
}
}
if parsedBody.Message != "" {
return bodyCopy, fmt.Sprintf("%s (HTTP %d)", parsedBody.Message, statusCode), nil
}
type errorMessage struct {
if len(parsedBody.Errors) == 0 || parsedBody.Errors[0] != '[' {
return bodyCopy, "", nil
}
var errorObjects []json.RawMessage
if err := json.Unmarshal(parsedBody.Errors, &errorObjects); err != nil {
return bodyCopy, "", err
}
var objectError struct {
Message string
}
var errors []string
for _, rawErr := range parsedBody.Errors {
for _, rawErr := range errorObjects {
if len(rawErr) == 0 {
continue
}
if rawErr[0] == '{' {
var objectError errorMessage
err := json.Unmarshal(rawErr, &objectError)
if err != nil {
return r, "", err
return bodyCopy, "", err
}
errors = append(errors, objectError.Message)
} else if rawErr[0] == '"' {
var stringError string
err := json.Unmarshal(rawErr, &stringError)
if err != nil {
return r, "", err
return bodyCopy, "", err
}
errors = append(errors, stringError)
}

View file

@ -1284,3 +1284,85 @@ func Test_processResponse_template(t *testing.T) {
`), stdout.String())
assert.Equal(t, "", stderr.String())
}
func Test_parseErrorResponse(t *testing.T) {
type args struct {
input string
statusCode int
}
tests := []struct {
name string
args args
wantErrMsg string
wantErr bool
}{
{
name: "no error",
args: args{
input: `{}`,
statusCode: 500,
},
wantErrMsg: "",
wantErr: false,
},
{
name: "nil errors",
args: args{
input: `{"errors":null}`,
statusCode: 500,
},
wantErrMsg: "",
wantErr: false,
},
{
name: "simple error",
args: args{
input: `{"message": "OH NOES"}`,
statusCode: 500,
},
wantErrMsg: "OH NOES (HTTP 500)",
wantErr: false,
},
{
name: "errors string",
args: args{
input: `{"message": "Conflict", "errors": "Some description"}`,
statusCode: 409,
},
wantErrMsg: "Some description (Conflict)",
wantErr: false,
},
{
name: "errors array of strings",
args: args{
input: `{"errors": ["fail1", "asplode2"]}`,
statusCode: 500,
},
wantErrMsg: "fail1\nasplode2",
wantErr: false,
},
{
name: "errors array of objects",
args: args{
input: `{"errors": [{"message":"fail1"}, {"message":"asplode2"}]}`,
statusCode: 500,
},
wantErrMsg: "fail1\nasplode2",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := parseErrorResponse(strings.NewReader(tt.args.input), tt.args.statusCode)
if (err != nil) != tt.wantErr {
t.Errorf("parseErrorResponse() error = %v, wantErr %v", err, tt.wantErr)
}
if gotString, _ := ioutil.ReadAll(got); tt.args.input != string(gotString) {
t.Errorf("parseErrorResponse() got = %q, want %q", string(gotString), tt.args.input)
}
if got1 != tt.wantErrMsg {
t.Errorf("parseErrorResponse() got1 = %q, want %q", got1, tt.wantErrMsg)
}
})
}
}

View file

@ -5,6 +5,7 @@ import (
authLoginCmd "github.com/cli/cli/v2/pkg/cmd/auth/login"
authLogoutCmd "github.com/cli/cli/v2/pkg/cmd/auth/logout"
authRefreshCmd "github.com/cli/cli/v2/pkg/cmd/auth/refresh"
authSetupGitCmd "github.com/cli/cli/v2/pkg/cmd/auth/setupgit"
authStatusCmd "github.com/cli/cli/v2/pkg/cmd/auth/status"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/spf13/cobra"
@ -24,6 +25,7 @@ func NewCmdAuth(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(authStatusCmd.NewCmdStatus(f, nil))
cmd.AddCommand(authRefreshCmd.NewCmdRefresh(f, nil))
cmd.AddCommand(gitCredentialCmd.NewCmdCredential(f, nil))
cmd.AddCommand(authSetupGitCmd.NewCmdSetupGit(f, nil))
return cmd
}

View file

@ -100,12 +100,18 @@ func helperRun(opts *CredentialOptions) error {
return err
}
lookupHost := wants["host"]
var gotUser string
gotToken, source, _ := cfg.GetWithSource(wants["host"], "oauth_token")
gotToken, source, _ := cfg.GetWithSource(lookupHost, "oauth_token")
if gotToken == "" && strings.HasPrefix(lookupHost, "gist.") {
lookupHost = strings.TrimPrefix(lookupHost, "gist.")
gotToken, source, _ = cfg.GetWithSource(lookupHost, "oauth_token")
}
if strings.HasSuffix(source, "_TOKEN") {
gotUser = tokenUser
} else {
gotUser, _, _ = cfg.GetWithSource(wants["host"], "user")
gotUser, _, _ = cfg.GetWithSource(lookupHost, "user")
}
if gotUser == "" || gotToken == "" {

View file

@ -8,12 +8,18 @@ import (
"github.com/cli/cli/v2/pkg/iostreams"
)
// why not just use the config stub argh
type tinyConfig map[string]string
func (c tinyConfig) GetWithSource(host, key string) (string, string, error) {
return c[fmt.Sprintf("%s:%s", host, key)], c["_source"], nil
}
func (c tinyConfig) Get(host, key string) (val string, err error) {
val, _, err = c.GetWithSource(host, key)
return
}
func Test_helperRun(t *testing.T) {
tests := []struct {
name string
@ -74,6 +80,32 @@ func Test_helperRun(t *testing.T) {
`),
wantStderr: "",
},
{
name: "gist host",
opts: CredentialOptions{
Operation: "get",
Config: func() (config, error) {
return tinyConfig{
"_source": "/Users/monalisa/.config/gh/hosts.yml",
"github.com:user": "monalisa",
"github.com:oauth_token": "OTOKEN",
}, nil
},
},
input: heredoc.Doc(`
protocol=https
host=gist.github.com
username=monalisa
`),
wantErr: false,
wantStdout: heredoc.Doc(`
protocol=https
host=gist.github.com
username=monalisa
password=OTOKEN
`),
wantStderr: "",
},
{
name: "url input",
opts: CredentialOptions{

View file

@ -68,37 +68,34 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
$ gh auth login --hostname enterprise.internal
`),
RunE: func(cmd *cobra.Command, args []string) error {
if !opts.IO.CanPrompt() && !(tokenStdin || opts.Web) {
return cmdutil.FlagErrorf("--web or --with-token required when not running interactively")
}
if tokenStdin && opts.Web {
return cmdutil.FlagErrorf("specify only one of --web or --with-token")
return cmdutil.FlagErrorf("specify only one of `--web` or `--with-token`")
}
if tokenStdin && len(opts.Scopes) > 0 {
return cmdutil.FlagErrorf("specify only one of `--scopes` or `--with-token`")
}
if tokenStdin {
defer opts.IO.In.Close()
token, err := ioutil.ReadAll(opts.IO.In)
if err != nil {
return fmt.Errorf("failed to read token from STDIN: %w", err)
return fmt.Errorf("failed to read token from standard input: %w", err)
}
opts.Token = strings.TrimSpace(string(token))
}
if opts.IO.CanPrompt() && opts.Token == "" && !opts.Web {
if opts.IO.CanPrompt() && opts.Token == "" {
opts.Interactive = true
}
if cmd.Flags().Changed("hostname") {
if err := ghinstance.HostnameValidator(opts.Hostname); err != nil {
return cmdutil.FlagErrorf("error parsing --hostname: %w", err)
return cmdutil.FlagErrorf("error parsing hostname: %w", err)
}
}
if !opts.Interactive {
if opts.Hostname == "" {
opts.Hostname = ghinstance.Default()
}
if opts.Hostname == "" && (!opts.Interactive || opts.Web) {
opts.Hostname = ghinstance.Default()
}
opts.MainExecutable = f.Executable()
@ -125,15 +122,11 @@ func loginRun(opts *LoginOptions) error {
}
hostname := opts.Hostname
if hostname == "" {
if opts.Interactive {
var err error
hostname, err = promptForHostname()
if err != nil {
return err
}
} else {
return errors.New("must specify --hostname")
if opts.Interactive && hostname == "" {
var err error
hostname, err = promptForHostname()
if err != nil {
return err
}
}

View file

@ -5,6 +5,7 @@ import (
"net/http"
"os"
"regexp"
"runtime"
"testing"
"github.com/MakeNowJust/heredoc"
@ -18,6 +19,21 @@ import (
"github.com/stretchr/testify/assert"
)
func stubHomeDir(t *testing.T, dir string) {
homeEnv := "HOME"
switch runtime.GOOS {
case "windows":
homeEnv = "USERPROFILE"
case "plan9":
homeEnv = "home"
}
oldHomeDir := os.Getenv(homeEnv)
os.Setenv(homeEnv, dir)
t.Cleanup(func() {
os.Setenv(homeEnv, oldHomeDir)
})
}
func Test_NewCmdLogin(t *testing.T) {
tests := []struct {
name string
@ -50,13 +66,19 @@ func Test_NewCmdLogin(t *testing.T) {
name: "nontty, hostname",
stdinTTY: false,
cli: "--hostname claire.redfield",
wantsErr: true,
wants: LoginOptions{
Hostname: "claire.redfield",
Token: "",
},
},
{
name: "nontty",
stdinTTY: false,
cli: "",
wantsErr: true,
wants: LoginOptions{
Hostname: "github.com",
Token: "",
},
},
{
name: "nontty, with-token, hostname",
@ -102,8 +124,9 @@ func Test_NewCmdLogin(t *testing.T) {
stdinTTY: true,
cli: "--web",
wants: LoginOptions{
Hostname: "github.com",
Web: true,
Hostname: "github.com",
Web: true,
Interactive: true,
},
},
{
@ -147,8 +170,7 @@ func Test_NewCmdLogin(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
io, stdin, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: io,
Executable: func() string { return "/path/to/gh" },
IOStreams: io,
}
io.SetStdoutTTY(true)
@ -346,6 +368,8 @@ func Test_loginRun_nontty(t *testing.T) {
}
func Test_loginRun_Survey(t *testing.T) {
stubHomeDir(t, t.TempDir())
tests := []struct {
name string
opts *LoginOptions
@ -371,8 +395,8 @@ func Test_loginRun_Survey(t *testing.T) {
// httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`))
},
askStubs: func(as *prompt.AskStubber) {
as.StubOne(0) // host type github.com
as.StubOne(false) // do not continue
as.StubPrompt("What account do you want to log into?").AnswerWith("GitHub.com")
as.StubPrompt("You're already logged into github.com. Do you want to re-authenticate?").AnswerWith(false)
},
wantHosts: "", // nothing should have been written to hosts
wantErrOut: nil,
@ -390,10 +414,10 @@ func Test_loginRun_Survey(t *testing.T) {
git_protocol: https
`),
askStubs: func(as *prompt.AskStubber) {
as.StubOne("HTTPS") // git_protocol
as.StubOne(false) // cache credentials
as.StubOne(1) // auth mode: token
as.StubOne("def456") // auth token
as.StubPrompt("What is your preferred protocol for Git operations?").AnswerWith("HTTPS")
as.StubPrompt("Authenticate Git with your GitHub credentials?").AnswerWith(false)
as.StubPrompt("How would you like to authenticate GitHub CLI?").AnswerWith("Paste an authentication token")
as.StubPrompt("Paste your authentication token:").AnswerWith("def456")
},
runStubs: func(rs *run.CommandStubber) {
rs.Register(`git config credential\.https:/`, 1, "")
@ -419,12 +443,12 @@ func Test_loginRun_Survey(t *testing.T) {
Interactive: true,
},
askStubs: func(as *prompt.AskStubber) {
as.StubOne(1) // host type enterprise
as.StubOne("brad.vickers") // hostname
as.StubOne("HTTPS") // git_protocol
as.StubOne(false) // cache credentials
as.StubOne(1) // auth mode: token
as.StubOne("def456") // auth token
as.StubPrompt("What account do you want to log into?").AnswerWith("GitHub Enterprise Server")
as.StubPrompt("GHE hostname:").AnswerWith("brad.vickers")
as.StubPrompt("What is your preferred protocol for Git operations?").AnswerWith("HTTPS")
as.StubPrompt("Authenticate Git with your GitHub credentials?").AnswerWith(false)
as.StubPrompt("How would you like to authenticate GitHub CLI?").AnswerWith("Paste an authentication token")
as.StubPrompt("Paste your authentication token:").AnswerWith("def456")
},
runStubs: func(rs *run.CommandStubber) {
rs.Register(`git config credential\.https:/`, 1, "")
@ -450,11 +474,11 @@ func Test_loginRun_Survey(t *testing.T) {
Interactive: true,
},
askStubs: func(as *prompt.AskStubber) {
as.StubOne(0) // host type github.com
as.StubOne("HTTPS") // git_protocol
as.StubOne(false) // cache credentials
as.StubOne(1) // auth mode: token
as.StubOne("def456") // auth token
as.StubPrompt("What account do you want to log into?").AnswerWith("GitHub.com")
as.StubPrompt("What is your preferred protocol for Git operations?").AnswerWith("HTTPS")
as.StubPrompt("Authenticate Git with your GitHub credentials?").AnswerWith(false)
as.StubPrompt("How would you like to authenticate GitHub CLI?").AnswerWith("Paste an authentication token")
as.StubPrompt("Paste your authentication token:").AnswerWith("def456")
},
runStubs: func(rs *run.CommandStubber) {
rs.Register(`git config credential\.https:/`, 1, "")
@ -474,11 +498,11 @@ func Test_loginRun_Survey(t *testing.T) {
Interactive: true,
},
askStubs: func(as *prompt.AskStubber) {
as.StubOne(0) // host type github.com
as.StubOne("SSH") // git_protocol
as.StubOne(10) // TODO: SSH key selection
as.StubOne(1) // auth mode: token
as.StubOne("def456") // auth token
as.StubPrompt("What account do you want to log into?").AnswerWith("GitHub.com")
as.StubPrompt("What is your preferred protocol for Git operations?").AnswerWith("SSH")
as.StubPrompt("Generate a new SSH key to add to your GitHub account?").AnswerWith(false)
as.StubPrompt("How would you like to authenticate GitHub CLI?").AnswerWith("Paste an authentication token")
as.StubPrompt("Paste your authentication token:").AnswerWith("def456")
},
wantErrOut: regexp.MustCompile("Tip: you can generate a Personal Access Token here https://github.com/settings/tokens"),
},
@ -524,8 +548,7 @@ func Test_loginRun_Survey(t *testing.T) {
hostsBuf := bytes.Buffer{}
defer config.StubWriteConfig(&mainBuf, &hostsBuf)()
as, teardown := prompt.InitAskStubber()
defer teardown()
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)
}

View file

@ -106,8 +106,8 @@ func Test_logoutRun_tty(t *testing.T) {
cfgHosts: []string{"cheryl.mason", "github.com"},
wantHosts: "cheryl.mason:\n oauth_token: abc123\n",
askStubs: func(as *prompt.AskStubber) {
as.StubOne("github.com")
as.StubOne(true)
as.StubPrompt("What account do you want to log out of?").AnswerWith("github.com")
as.StubPrompt("Are you sure you want to log out of github.com account 'cybilb'?").AnswerWith(true)
},
wantErrOut: regexp.MustCompile(`Logged out of github.com account 'cybilb'`),
},
@ -116,7 +116,7 @@ func Test_logoutRun_tty(t *testing.T) {
opts: &LogoutOptions{},
cfgHosts: []string{"github.com"},
askStubs: func(as *prompt.AskStubber) {
as.StubOne(true)
as.StubPrompt("Are you sure you want to log out of github.com account 'cybilb'?").AnswerWith(true)
},
wantErrOut: regexp.MustCompile(`Logged out of github.com account 'cybilb'`),
},
@ -133,7 +133,7 @@ func Test_logoutRun_tty(t *testing.T) {
cfgHosts: []string{"cheryl.mason", "github.com"},
wantHosts: "github.com:\n oauth_token: abc123\n",
askStubs: func(as *prompt.AskStubber) {
as.StubOne(true)
as.StubPrompt("Are you sure you want to log out of cheryl.mason account 'cybilb'?").AnswerWith(true)
},
wantErrOut: regexp.MustCompile(`Logged out of cheryl.mason account 'cybilb'`),
},
@ -169,8 +169,7 @@ func Test_logoutRun_tty(t *testing.T) {
hostsBuf := bytes.Buffer{}
defer config.StubWriteConfig(&mainBuf, &hostsBuf)()
as, teardown := prompt.InitAskStubber()
defer teardown()
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)
}

View file

@ -26,7 +26,7 @@ type RefreshOptions struct {
Hostname string
Scopes []string
AuthFlow func(config.Config, *iostreams.IOStreams, string, []string) error
AuthFlow func(config.Config, *iostreams.IOStreams, string, []string, bool) error
Interactive bool
}
@ -35,8 +35,8 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.
opts := &RefreshOptions{
IO: f.IOStreams,
Config: f.Config,
AuthFlow: func(cfg config.Config, io *iostreams.IOStreams, hostname string, scopes []string) error {
_, err := authflow.AuthFlowWithConfig(cfg, io, hostname, "", scopes)
AuthFlow: func(cfg config.Config, io *iostreams.IOStreams, hostname string, scopes []string, interactive bool) error {
_, err := authflow.AuthFlowWithConfig(cfg, io, hostname, "", scopes, interactive)
return err
},
httpClient: http.DefaultClient,
@ -146,7 +146,7 @@ func refreshRun(opts *RefreshOptions) error {
credentialFlow := &shared.GitCredentialFlow{
Executable: opts.MainExecutable,
}
gitProtocol, _ := cfg.Get(hostname, "git_protocol")
gitProtocol, _ := cfg.GetOrDefault(hostname, "git_protocol")
if opts.Interactive && gitProtocol == "https" {
if err := credentialFlow.Prompt(hostname); err != nil {
return err
@ -154,10 +154,13 @@ func refreshRun(opts *RefreshOptions) error {
additionalScopes = append(additionalScopes, credentialFlow.Scopes()...)
}
if err := opts.AuthFlow(cfg, opts.IO, hostname, append(opts.Scopes, additionalScopes...)); err != nil {
if err := opts.AuthFlow(cfg, opts.IO, hostname, append(opts.Scopes, additionalScopes...), opts.Interactive); err != nil {
return err
}
cs := opts.IO.ColorScheme()
fmt.Fprintf(opts.IO.ErrOut, "%s Authentication complete.\n", cs.SuccessIcon())
if credentialFlow.ShouldSetup() {
username, _ := cfg.Get(hostname, "user")
password, _ := cfg.Get(hostname, "oauth_token")

View file

@ -91,8 +91,7 @@ func Test_NewCmdRefresh(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
io, _, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: io,
Executable: func() string { return "/path/to/gh" },
IOStreams: io,
}
io.SetStdinTTY(tt.tty)
io.SetStdoutTTY(tt.tty)
@ -195,7 +194,7 @@ func Test_refreshRun(t *testing.T) {
Hostname: "",
},
askStubs: func(as *prompt.AskStubber) {
as.StubOne("github.com")
as.StubPrompt("What account do you want to refresh auth for?").AnswerWith("github.com")
},
wantAuthArgs: authArgs{
hostname: "github.com",
@ -233,7 +232,7 @@ func Test_refreshRun(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
aa := authArgs{}
tt.opts.AuthFlow = func(_ config.Config, _ *iostreams.IOStreams, hostname string, scopes []string) error {
tt.opts.AuthFlow = func(_ config.Config, _ *iostreams.IOStreams, hostname string, scopes []string, interactive bool) error {
aa.hostname = hostname
aa.scopes = scopes
return nil
@ -277,8 +276,7 @@ func Test_refreshRun(t *testing.T) {
hostsBuf := bytes.Buffer{}
defer config.StubWriteConfig(&mainBuf, &hostsBuf)()
as, teardown := prompt.InitAskStubber()
defer teardown()
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)
}

View file

@ -0,0 +1,100 @@
package setupgit
import (
"fmt"
"strings"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
)
type gitConfigurator interface {
Setup(hostname, username, authToken string) error
}
type SetupGitOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
Hostname string
gitConfigure gitConfigurator
}
func NewCmdSetupGit(f *cmdutil.Factory, runF func(*SetupGitOptions) error) *cobra.Command {
opts := &SetupGitOptions{
IO: f.IOStreams,
Config: f.Config,
}
cmd := &cobra.Command{
Short: "Configure git to use GitHub CLI as a credential helper",
Use: "setup-git",
RunE: func(cmd *cobra.Command, args []string) error {
opts.gitConfigure = &shared.GitCredentialFlow{
Executable: f.Executable(),
}
if runF != nil {
return runF(opts)
}
return setupGitRun(opts)
},
}
cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The hostname to configure git for")
return cmd
}
func setupGitRun(opts *SetupGitOptions) error {
cfg, err := opts.Config()
if err != nil {
return err
}
hostnames, err := cfg.Hosts()
if err != nil {
return err
}
stderr := opts.IO.ErrOut
cs := opts.IO.ColorScheme()
if len(hostnames) == 0 {
fmt.Fprintf(
stderr,
"You are not logged into any GitHub hosts. Run %s to authenticate.\n",
cs.Bold("gh auth login"),
)
return cmdutil.SilentError
}
hostnamesToSetup := hostnames
if opts.Hostname != "" {
if !has(opts.Hostname, hostnames) {
return fmt.Errorf("You are not logged into the GitHub host %q\n", opts.Hostname)
}
hostnamesToSetup = []string{opts.Hostname}
}
for _, hostname := range hostnamesToSetup {
if err := opts.gitConfigure.Setup(hostname, "", ""); err != nil {
return fmt.Errorf("failed to set up git credential helper: %w", err)
}
}
return nil
}
func has(needle string, haystack []string) bool {
for _, s := range haystack {
if strings.EqualFold(s, needle) {
return true
}
}
return false
}

View file

@ -0,0 +1,122 @@
package setupgit
import (
"fmt"
"testing"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mockGitConfigurer struct {
setupErr error
}
func (gf *mockGitConfigurer) Setup(hostname, username, authToken string) error {
return gf.setupErr
}
func Test_setupGitRun(t *testing.T) {
tests := []struct {
name string
opts *SetupGitOptions
expectedErr string
expectedErrOut string
}{
{
name: "opts.Config returns an error",
opts: &SetupGitOptions{
Config: func() (config.Config, error) {
return nil, fmt.Errorf("oops")
},
},
expectedErr: "oops",
},
{
name: "no authenticated hostnames",
opts: &SetupGitOptions{},
expectedErr: "SilentError",
expectedErrOut: "You are not logged into any GitHub hosts. Run gh auth login to authenticate.\n",
},
{
name: "not authenticated with the hostname given as flag",
opts: &SetupGitOptions{
Hostname: "foo",
Config: func() (config.Config, error) {
cfg := config.NewBlankConfig()
require.NoError(t, cfg.Set("bar", "", ""))
return cfg, nil
},
},
expectedErr: "You are not logged into the GitHub host \"foo\"\n",
expectedErrOut: "",
},
{
name: "error setting up git for hostname",
opts: &SetupGitOptions{
gitConfigure: &mockGitConfigurer{
setupErr: fmt.Errorf("broken"),
},
Config: func() (config.Config, error) {
cfg := config.NewBlankConfig()
require.NoError(t, cfg.Set("bar", "", ""))
return cfg, nil
},
},
expectedErr: "failed to set up git credential helper: broken",
expectedErrOut: "",
},
{
name: "no hostname option given. Setup git for each hostname in config",
opts: &SetupGitOptions{
gitConfigure: &mockGitConfigurer{},
Config: func() (config.Config, error) {
cfg := config.NewBlankConfig()
require.NoError(t, cfg.Set("bar", "", ""))
return cfg, nil
},
},
},
{
name: "setup git for the hostname given via options",
opts: &SetupGitOptions{
Hostname: "yes",
gitConfigure: &mockGitConfigurer{},
Config: func() (config.Config, error) {
cfg := config.NewBlankConfig()
require.NoError(t, cfg.Set("bar", "", ""))
require.NoError(t, cfg.Set("yes", "", ""))
return cfg, nil
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.opts.Config == nil {
tt.opts.Config = func() (config.Config, error) {
return config.NewBlankConfig(), nil
}
}
io, _, _, stderr := iostreams.Test()
io.SetStdinTTY(true)
io.SetStderrTTY(true)
io.SetStdoutTTY(true)
tt.opts.IO = io
err := setupGitRun(tt.opts)
if tt.expectedErr != "" {
assert.EqualError(t, err, tt.expectedErr)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.expectedErrOut, stderr.String())
})
}
}

View file

@ -63,25 +63,46 @@ func (flow *GitCredentialFlow) Setup(hostname, username, authToken string) error
func (flow *GitCredentialFlow) gitCredentialSetup(hostname, username, password string) error {
if flow.helper == "" {
// first use a blank value to indicate to git we want to sever the chain of credential helpers
preConfigureCmd, err := git.GitCommand("config", "--global", gitCredentialHelperKey(hostname), "")
if err != nil {
return err
}
if err = run.PrepareCmd(preConfigureCmd).Run(); err != nil {
return err
credHelperKeys := []string{
gitCredentialHelperKey(hostname),
}
// use GitHub CLI as a credential helper (for this host only)
configureCmd, err := git.GitCommand(
"config", "--global", "--add",
gitCredentialHelperKey(hostname),
fmt.Sprintf("!%s auth git-credential", shellQuote(flow.Executable)),
)
if err != nil {
return err
gistHost := strings.TrimSuffix(ghinstance.GistHost(hostname), "/")
if strings.HasPrefix(gistHost, "gist.") {
credHelperKeys = append(credHelperKeys, gitCredentialHelperKey(gistHost))
}
return run.PrepareCmd(configureCmd).Run()
var configErr error
for _, credHelperKey := range credHelperKeys {
if configErr != nil {
break
}
// first use a blank value to indicate to git we want to sever the chain of credential helpers
preConfigureCmd, err := git.GitCommand("config", "--global", "--replace-all", credHelperKey, "")
if err != nil {
configErr = err
break
}
if err = run.PrepareCmd(preConfigureCmd).Run(); err != nil {
configErr = err
break
}
// second configure the actual helper for this host
configureCmd, err := git.GitCommand(
"config", "--global", "--add",
credHelperKey,
fmt.Sprintf("!%s auth git-credential", shellQuote(flow.Executable)),
)
if err != nil {
configErr = err
} else {
configErr = run.PrepareCmd(configureCmd).Run()
}
}
return configErr
}
// clear previous cached credentials
@ -121,7 +142,8 @@ func (flow *GitCredentialFlow) gitCredentialSetup(hostname, username, password s
}
func gitCredentialHelperKey(hostname string) string {
return fmt.Sprintf("credential.%s.helper", strings.TrimSuffix(ghinstance.HostPrefix(hostname), "/"))
host := strings.TrimSuffix(ghinstance.HostPrefix(hostname), "/")
return fmt.Sprintf("credential.%s.helper", host)
}
func gitCredentialHelper(hostname string) (helper string, err error) {

View file

@ -22,10 +22,57 @@ func TestGitCredentialSetup_configureExisting(t *testing.T) {
}
}
func TestGitCredentialSetup_setOurs(t *testing.T) {
func TestGitCredentialsSetup_setOurs_GH(t *testing.T) {
cs, restoreRun := run.Stub()
defer restoreRun(t)
cs.Register(`git config --global credential\.`, 0, "", func(args []string) {
cs.Register(`git config --global --replace-all credential\.`, 0, "", func(args []string) {
if key := args[len(args)-2]; key != "credential.https://github.com.helper" {
t.Errorf("git config key was %q", key)
}
if val := args[len(args)-1]; val != "" {
t.Errorf("global credential helper configured to %q", val)
}
})
cs.Register(`git config --global --add credential\.`, 0, "", func(args []string) {
if key := args[len(args)-2]; key != "credential.https://github.com.helper" {
t.Errorf("git config key was %q", key)
}
if val := args[len(args)-1]; val != "!/path/to/gh auth git-credential" {
t.Errorf("global credential helper configured to %q", val)
}
})
cs.Register(`git config --global --replace-all credential\.`, 0, "", func(args []string) {
if key := args[len(args)-2]; key != "credential.https://gist.github.com.helper" {
t.Errorf("git config key was %q", key)
}
if val := args[len(args)-1]; val != "" {
t.Errorf("global credential helper configured to %q", val)
}
})
cs.Register(`git config --global --add credential\.`, 0, "", func(args []string) {
if key := args[len(args)-2]; key != "credential.https://gist.github.com.helper" {
t.Errorf("git config key was %q", key)
}
if val := args[len(args)-1]; val != "!/path/to/gh auth git-credential" {
t.Errorf("global credential helper configured to %q", val)
}
})
f := GitCredentialFlow{
Executable: "/path/to/gh",
helper: "",
}
if err := f.gitCredentialSetup("github.com", "monalisa", "PASSWD"); err != nil {
t.Errorf("GitCredentialSetup() error = %v", err)
}
}
func TestGitCredentialSetup_setOurs_nonGH(t *testing.T) {
cs, restoreRun := run.Stub()
defer restoreRun(t)
cs.Register(`git config --global --replace-all credential\.`, 0, "", func(args []string) {
if key := args[len(args)-2]; key != "credential.https://example.com.helper" {
t.Errorf("git config key was %q", key)
}

View file

@ -99,7 +99,7 @@ func Login(opts *LoginOptions) error {
var authMode int
if opts.Web {
authMode = 0
} else {
} else if opts.Interactive {
err := prompt.SurveyAskOne(&survey.Select{
Message: "How would you like to authenticate GitHub CLI?",
Options: []string{
@ -117,10 +117,11 @@ func Login(opts *LoginOptions) error {
if authMode == 0 {
var err error
authToken, err = authflow.AuthFlowWithConfig(cfg, opts.IO, hostname, "", append(opts.Scopes, additionalScopes...))
authToken, err = authflow.AuthFlowWithConfig(cfg, opts.IO, hostname, "", append(opts.Scopes, additionalScopes...), opts.Interactive)
if err != nil {
return fmt.Errorf("failed to authenticate via web browser: %w", err)
}
fmt.Fprintf(opts.IO.ErrOut, "%s Authentication complete.\n", cs.SuccessIcon())
userValidated = true
} else {
minimumScopes := append([]string{"repo", "read:org"}, additionalScopes...)

View file

@ -47,14 +47,13 @@ func TestLogin_ssh(t *testing.T) {
httpmock.REST("POST", "api/v3/user/keys"),
httpmock.StringResponse(`{}`))
ask, askRestore := prompt.InitAskStubber()
defer askRestore()
ask := prompt.NewAskStubber(t)
ask.StubOne("SSH") // preferred protocol
ask.StubOne(true) // generate a new key
ask.StubOne("monkey") // enter a passphrase
ask.StubOne(1) // paste a token
ask.StubOne("ATOKEN") // token
ask.StubPrompt("What is your preferred protocol for Git operations?").AnswerWith("SSH")
ask.StubPrompt("Generate a new SSH key to add to your GitHub account?").AnswerWith(true)
ask.StubPrompt("Enter a passphrase for your new SSH key (Optional)").AnswerWith("monkey")
ask.StubPrompt("How would you like to authenticate GitHub CLI?").AnswerWith("Paste an authentication token")
ask.StubPrompt("Paste your authentication token:").AnswerWith("ATOKEN")
rs, runRestore := run.Stub()
defer runRestore(t)

View file

@ -35,7 +35,7 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co
Args: cobra.ExactArgs(0),
Short: "View authentication status",
Long: heredoc.Doc(`Verifies and displays information about your authentication state.
This command will test your authentication state for each GitHub host that gh knows about and
report on any issues.
`),
@ -127,7 +127,7 @@ func statusRun(opts *StatusOptions) error {
addMsg("%s %s: api call failed: %s", cs.Red("X"), hostname, err)
}
addMsg("%s Logged in to %s as %s (%s)", cs.SuccessIcon(), hostname, cs.Bold(username), tokenSource)
proto, _ := cfg.Get(hostname, "git_protocol")
proto, _ := cfg.GetOrDefault(hostname, "git_protocol")
if proto != "" {
addMsg("%s Git operations for %s configured to use %s protocol.",
cs.SuccessIcon(), hostname, cs.Bold(proto))

View file

@ -3,6 +3,8 @@ package browse
import (
"fmt"
"net/http"
"net/url"
"path"
"path/filepath"
"strconv"
"strings"
@ -27,6 +29,7 @@ type BrowseOptions struct {
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
PathFromRepoRoot func() string
GitClient gitClient
SelectorArg string
@ -44,6 +47,7 @@ func NewCmdBrowse(f *cmdutil.Factory, runF func(*BrowseOptions) error) *cobra.Co
HttpClient: f.HttpClient,
IO: f.IOStreams,
PathFromRepoRoot: git.PathFromRepoRoot,
GitClient: &localGitClient{},
}
cmd := &cobra.Command{
@ -95,6 +99,9 @@ func NewCmdBrowse(f *cmdutil.Factory, runF func(*BrowseOptions) error) *cobra.Co
); err != nil {
return err
}
if cmd.Flags().Changed("repo") {
opts.GitClient = &remoteGitClient{opts.BaseRepo, opts.HttpClient}
}
if runF != nil {
return runF(opts)
@ -121,17 +128,18 @@ func runBrowse(opts *BrowseOptions) error {
}
if opts.CommitFlag {
commit, err := git.LastCommit()
if err == nil {
opts.Branch = commit.Sha
commit, err := opts.GitClient.LastCommit()
if err != nil {
return err
}
opts.Branch = commit.Sha
}
section, err := parseSection(baseRepo, opts)
if err != nil {
return err
}
url := ghrepo.GenerateRepoURL(baseRepo, section)
url := ghrepo.GenerateRepoURL(baseRepo, "%s", section)
if opts.NoBrowserFlag {
_, err := fmt.Fprintln(opts.IO.Out, url)
@ -186,23 +194,30 @@ func parseSection(baseRepo ghrepo.Interface, opts *BrowseOptions) (string, error
} else {
rangeFragment = fmt.Sprintf("L%d", rangeStart)
}
return fmt.Sprintf("blob/%s/%s?plain=1#%s", branchName, filePath, rangeFragment), nil
return fmt.Sprintf("blob/%s/%s?plain=1#%s", escapePath(branchName), escapePath(filePath), rangeFragment), nil
}
return fmt.Sprintf("tree/%s/%s", branchName, filePath), nil
return strings.TrimSuffix(fmt.Sprintf("tree/%s/%s", escapePath(branchName), escapePath(filePath)), "/"), nil
}
// escapePath URL-encodes special characters but leaves slashes unchanged
func escapePath(p string) string {
return strings.ReplaceAll(url.PathEscape(p), "%2F", "/")
}
func parseFile(opts BrowseOptions, f string) (p string, start int, end int, err error) {
if f == "" {
return
}
parts := strings.SplitN(f, ":", 3)
if len(parts) > 2 {
err = fmt.Errorf("invalid file argument: %q", f)
return
}
p = parts[0]
if !filepath.IsAbs(p) {
p = filepath.Clean(filepath.Join(opts.PathFromRepoRoot(), p))
// Ensure that a path using \ can be used in a URL
p = strings.ReplaceAll(p, "\\", "/")
p = filepath.ToSlash(parts[0])
if !path.IsAbs(p) {
p = path.Join(opts.PathFromRepoRoot(), p)
if p == "." || strings.HasPrefix(p, "..") {
p = ""
}
@ -236,3 +251,33 @@ func isNumber(arg string) bool {
_, err := strconv.Atoi(arg)
return err == nil
}
// gitClient is used to implement functions that can be performed on both local and remote git repositories
type gitClient interface {
LastCommit() (*git.Commit, error)
}
type localGitClient struct{}
type remoteGitClient struct {
repo func() (ghrepo.Interface, error)
httpClient func() (*http.Client, error)
}
func (gc *localGitClient) LastCommit() (*git.Commit, error) { return git.LastCommit() }
func (gc *remoteGitClient) LastCommit() (*git.Commit, error) {
httpClient, err := gc.httpClient()
if err != nil {
return nil, err
}
repo, err := gc.repo()
if err != nil {
return nil, err
}
commit, err := api.LastCommit(api.NewClientFromHTTP(httpClient), repo)
if err != nil {
return nil, err
}
return &git.Commit{Sha: commit.OID}, nil
}

View file

@ -2,6 +2,7 @@ package browse
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
@ -125,6 +126,8 @@ func TestNewCmdBrowse(t *testing.T) {
argv, err := shlex.Split(tt.cli)
assert.NoError(t, err)
cmd.SetArgs(argv)
cmd.SetOut(ioutil.Discard)
cmd.SetErr(ioutil.Discard)
_, err = cmd.ExecuteC()
if tt.wantsErr {
@ -140,6 +143,7 @@ func TestNewCmdBrowse(t *testing.T) {
assert.Equal(t, tt.wants.WikiFlag, opts.WikiFlag)
assert.Equal(t, tt.wants.NoBrowserFlag, opts.NoBrowserFlag)
assert.Equal(t, tt.wants.SettingsFlag, opts.SettingsFlag)
assert.Equal(t, tt.wants.CommitFlag, opts.CommitFlag)
})
}
}
@ -153,6 +157,12 @@ func setGitDir(t *testing.T, dir string) {
})
}
type testGitClient struct{}
func (gc *testGitClient) LastCommit() (*git.Commit, error) {
return &git.Commit{Sha: "6f1a2405cace1633d89a79c74c65f22fe78f9659"}, nil
}
func Test_runBrowse(t *testing.T) {
s := string(os.PathSeparator)
setGitDir(t, "../../../git/fixtures/simple.git")
@ -217,7 +227,7 @@ func Test_runBrowse(t *testing.T) {
Branch: "trunk",
},
baseRepo: ghrepo.New("jlsestak", "CouldNotThinkOfARepoName"),
expectedURL: "https://github.com/jlsestak/CouldNotThinkOfARepoName/tree/trunk/",
expectedURL: "https://github.com/jlsestak/CouldNotThinkOfARepoName/tree/trunk",
},
{
name: "branch flag with file",
@ -228,6 +238,35 @@ func Test_runBrowse(t *testing.T) {
baseRepo: ghrepo.New("bchadwic", "LedZeppelinIV"),
expectedURL: "https://github.com/bchadwic/LedZeppelinIV/tree/trunk/main.go",
},
{
name: "branch flag within dir",
opts: BrowseOptions{
Branch: "feature-123",
PathFromRepoRoot: func() string { return "pkg/dir" },
},
baseRepo: ghrepo.New("bstnc", "yeepers"),
expectedURL: "https://github.com/bstnc/yeepers/tree/feature-123",
},
{
name: "branch flag within dir with .",
opts: BrowseOptions{
Branch: "feature-123",
SelectorArg: ".",
PathFromRepoRoot: func() string { return "pkg/dir" },
},
baseRepo: ghrepo.New("bstnc", "yeepers"),
expectedURL: "https://github.com/bstnc/yeepers/tree/feature-123/pkg/dir",
},
{
name: "branch flag within dir with dir",
opts: BrowseOptions{
Branch: "feature-123",
SelectorArg: "inner/more",
PathFromRepoRoot: func() string { return "pkg/dir" },
},
baseRepo: ghrepo.New("bstnc", "yeepers"),
expectedURL: "https://github.com/bstnc/yeepers/tree/feature-123/pkg/dir/inner/more",
},
{
name: "file with line number",
opts: BrowseOptions{
@ -322,16 +361,18 @@ func Test_runBrowse(t *testing.T) {
name: "open last commit",
opts: BrowseOptions{
CommitFlag: true,
GitClient: &testGitClient{},
},
baseRepo: ghrepo.New("vilmibm", "gh-user-status"),
wantsErr: false,
expectedURL: "https://github.com/vilmibm/gh-user-status/tree/6f1a2405cace1633d89a79c74c65f22fe78f9659/",
expectedURL: "https://github.com/vilmibm/gh-user-status/tree/6f1a2405cace1633d89a79c74c65f22fe78f9659",
},
{
name: "open last commit with a file",
opts: BrowseOptions{
CommitFlag: true,
SelectorArg: "main.go",
GitClient: &testGitClient{},
},
baseRepo: ghrepo.New("vilmibm", "gh-user-status"),
wantsErr: false,
@ -363,6 +404,16 @@ func Test_runBrowse(t *testing.T) {
expectedURL: "https://github.com/bchadwic/gh-graph/tree/trunk/pkg/cmd/pr",
wantsErr: false,
},
{
name: "use special characters in selector arg",
opts: BrowseOptions{
SelectorArg: "?=hello world/ *:23-44",
Branch: "branch/with spaces?",
},
baseRepo: ghrepo.New("bchadwic", "test"),
expectedURL: "https://github.com/bchadwic/test/blob/branch/with%20spaces%3F/%3F=hello%20world/%20%2A?plain=1#L23-L44",
wantsErr: false,
},
}
for _, tt := range tests {
@ -410,67 +461,100 @@ func Test_runBrowse(t *testing.T) {
}
func Test_parsePathFromFileArg(t *testing.T) {
s := string(os.PathSeparator)
tests := []struct {
name string
currentDir string
fileArg string
expectedPath string
}{
{
name: "empty paths",
currentDir: "",
fileArg: "",
expectedPath: "",
},
{
name: "root directory",
currentDir: "",
fileArg: ".",
expectedPath: "",
},
{
name: "relative path",
currentDir: "",
fileArg: filepath.FromSlash("foo/bar.py"),
expectedPath: "foo/bar.py",
},
{
name: "go to parent folder",
fileArg: ".." + s,
currentDir: "pkg/cmd/browse/",
fileArg: filepath.FromSlash("../"),
expectedPath: "pkg/cmd",
},
{
name: "current folder",
currentDir: "pkg/cmd/browse/",
fileArg: ".",
expectedPath: "pkg/cmd/browse",
},
{
name: "current folder (alternative)",
fileArg: "." + s,
currentDir: "pkg/cmd/browse/",
fileArg: filepath.FromSlash("./"),
expectedPath: "pkg/cmd/browse",
},
{
name: "file that starts with '.'",
currentDir: "pkg/cmd/browse/",
fileArg: ".gitignore",
expectedPath: "pkg/cmd/browse/.gitignore",
},
{
name: "file in current folder",
currentDir: "pkg/cmd/browse/",
fileArg: filepath.Join(".", "browse.go"),
expectedPath: "pkg/cmd/browse/browse.go",
},
{
name: "file within parent folder",
currentDir: "pkg/cmd/browse/",
fileArg: filepath.Join("..", "browse.go"),
expectedPath: "pkg/cmd/browse.go",
},
{
name: "file within parent folder uncleaned",
fileArg: filepath.Join("..", ".") + s + s + s + "browse.go",
currentDir: "pkg/cmd/browse/",
fileArg: filepath.FromSlash(".././//browse.go"),
expectedPath: "pkg/cmd/browse.go",
},
{
name: "different path from root directory",
currentDir: "pkg/cmd/browse/",
fileArg: filepath.Join("..", "..", "..", "internal/build/build.go"),
expectedPath: "internal/build/build.go",
},
{
name: "go out of repository",
fileArg: filepath.Join("..", "..", "..", "..", "..", "..") + s + "",
currentDir: "pkg/cmd/browse/",
fileArg: filepath.FromSlash("../../../../../../"),
expectedPath: "",
},
{
name: "go to root of repository",
fileArg: filepath.Join("..", "..", "..") + s + "",
currentDir: "pkg/cmd/browse/",
fileArg: filepath.Join("../../../"),
expectedPath: "",
},
{
name: "empty fileArg",
fileArg: "",
expectedPath: "",
},
}
for _, tt := range tests {
path, _, _, _ := parseFile(BrowseOptions{
PathFromRepoRoot: func() string {
return "pkg/cmd/browse/"
return tt.currentDir
}}, tt.fileArg)
assert.Equal(t, tt.expectedPath, path, tt.name)
}

View file

@ -3,12 +3,17 @@ package codespace
import (
"context"
"fmt"
"io/ioutil"
"net/url"
"github.com/skratchdot/open-golang/open"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/spf13/cobra"
)
type browser interface {
Browse(string) error
}
func newCodeCmd(app *App) *cobra.Command {
var (
codespace string
@ -20,7 +25,8 @@ func newCodeCmd(app *App) *cobra.Command {
Short: "Open a codespace in Visual Studio Code",
Args: noArgsConstraint,
RunE: func(cmd *cobra.Command, args []string) error {
return app.VSCode(cmd.Context(), codespace, useInsiders)
b := cmdutil.NewBrowser("", ioutil.Discard, app.io.ErrOut)
return app.VSCode(cmd.Context(), b, codespace, useInsiders)
},
}
@ -31,7 +37,7 @@ func newCodeCmd(app *App) *cobra.Command {
}
// VSCode opens a codespace in the local VS VSCode application.
func (a *App) VSCode(ctx context.Context, codespaceName string, useInsiders bool) error {
func (a *App) VSCode(ctx context.Context, browser browser, codespaceName string, useInsiders bool) error {
if codespaceName == "" {
codespace, err := chooseCodespace(ctx, a.apiClient)
if err != nil {
@ -44,8 +50,8 @@ func (a *App) VSCode(ctx context.Context, codespaceName string, useInsiders bool
}
url := vscodeProtocolURL(codespaceName, useInsiders)
if err := open.Run(url); err != nil {
return fmt.Errorf("error opening vscode URL %s: %s. (Is Visual Studio Code installed?)", url, err)
if err := browser.Browse(url); err != nil {
return fmt.Errorf("error opening Visual Studio Code: %w", err)
}
return nil

View file

@ -0,0 +1,50 @@
package codespace
import (
"context"
"testing"
"github.com/cli/cli/v2/pkg/cmdutil"
)
func TestApp_VSCode(t *testing.T) {
type args struct {
codespaceName string
useInsiders bool
}
tests := []struct {
name string
args args
wantErr bool
wantURL string
}{
{
name: "open VS Code",
args: args{
codespaceName: "monalisa-cli-cli-abcdef",
useInsiders: false,
},
wantErr: false,
wantURL: "vscode://github.codespaces/connect?name=monalisa-cli-cli-abcdef",
},
{
name: "open VS Code Insiders",
args: args{
codespaceName: "monalisa-cli-cli-abcdef",
useInsiders: true,
},
wantErr: false,
wantURL: "vscode-insiders://github.codespaces/connect?name=monalisa-cli-cli-abcdef",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := &cmdutil.TestBrowser{}
a := &App{}
if err := a.VSCode(context.Background(), b, tt.args.codespaceName, tt.args.useInsiders); (err != nil) != tt.wantErr {
t.Errorf("App.VSCode() error = %v, wantErr %v", err, tt.wantErr)
}
b.Verify(t, tt.wantURL)
})
}
}

View file

@ -21,19 +21,25 @@ import (
"golang.org/x/term"
)
type App struct {
io *iostreams.IOStreams
apiClient apiClient
errLogger *log.Logger
type executable interface {
Executable() string
}
func NewApp(io *iostreams.IOStreams, apiClient apiClient) *App {
type App struct {
io *iostreams.IOStreams
apiClient apiClient
errLogger *log.Logger
executable executable
}
func NewApp(io *iostreams.IOStreams, exe executable, apiClient apiClient) *App {
errLogger := log.New(io.ErrOut, "", 0)
return &App{
io: io,
apiClient: apiClient,
errLogger: errLogger,
io: io,
apiClient: apiClient,
errLogger: errLogger,
executable: exe,
}
}
@ -209,8 +215,13 @@ func ask(qs []*survey.Question, response interface{}) error {
// checkAuthorizedKeys reports an error if the user has not registered any SSH keys;
// see https://github.com/cli/cli/v2/issues/166#issuecomment-921769703.
// The check is not required for security but it improves the error message.
func checkAuthorizedKeys(ctx context.Context, client apiClient, user string) error {
keys, err := client.AuthorizedKeys(ctx, user)
func checkAuthorizedKeys(ctx context.Context, client apiClient) error {
user, err := client.GetUser(ctx)
if err != nil {
return fmt.Errorf("error getting user: %w", err)
}
keys, err := client.AuthorizedKeys(ctx, user.Login)
if err != nil {
return fmt.Errorf("failed to read GitHub-authorized SSH keys for %s: %w", user, err)
}

View file

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/cli/cli/v2/internal/codespaces"
@ -12,10 +13,11 @@ import (
)
type createOptions struct {
repo string
branch string
machine string
showStatus bool
repo string
branch string
machine string
showStatus bool
idleTimeout time.Duration
}
func newCreateCmd(app *App) *cobra.Command {
@ -34,6 +36,7 @@ func newCreateCmd(app *App) *cobra.Command {
createCmd.Flags().StringVarP(&opts.branch, "branch", "b", "", "repository branch")
createCmd.Flags().StringVarP(&opts.machine, "machine", "m", "", "hardware specifications for the VM")
createCmd.Flags().BoolVarP(&opts.showStatus, "status", "s", false, "show status of post-create command and dotfiles")
createCmd.Flags().DurationVar(&opts.idleTimeout, "idle-timeout", 0, "allowed inactivity before codespace is stopped, e.g. \"10m\", \"1h\"")
return createCmd
}
@ -101,10 +104,11 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
a.StartProgressIndicatorWithLabel("Creating codespace")
codespace, err := a.apiClient.CreateCodespace(ctx, &api.CreateCodespaceParams{
RepositoryID: repository.ID,
Branch: branch,
Machine: machine,
Location: locationResult.Location,
RepositoryID: repository.ID,
Branch: branch,
Machine: machine,
Location: locationResult.Location,
IdleTimeoutMinutes: int(opts.idleTimeout.Minutes()),
})
a.StopProgressIndicator()
if err != nil {
@ -234,7 +238,7 @@ func getMachineName(ctx context.Context, apiClient apiClient, repoID int, machin
machineNames := make([]string, 0, len(machines))
machineByName := make(map[string]*api.Machine)
for _, m := range machines {
machineName := m.DisplayName
machineName := buildDisplayName(m.DisplayName, m.PrebuildAvailability)
machineNames = append(machineNames, machineName)
machineByName[machineName] = m
}
@ -260,3 +264,14 @@ func getMachineName(ctx context.Context, apiClient apiClient, repoID int, machin
return selectedMachine.Name, nil
}
// buildDisplayName returns display name to be used in the machine survey prompt.
func buildDisplayName(displayName string, prebuildAvailability string) string {
prebuildText := ""
if prebuildAvailability == "blob" || prebuildAvailability == "pool" {
prebuildText = " (Prebuild ready)"
}
return fmt.Sprintf("%s%s", displayName, prebuildText)
}

View file

@ -0,0 +1,126 @@
package codespace
import (
"context"
"fmt"
"testing"
"time"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/pkg/iostreams"
)
func TestApp_Create(t *testing.T) {
type fields struct {
apiClient apiClient
}
tests := []struct {
name string
fields fields
opts createOptions
wantErr bool
wantStdout string
wantStderr string
}{
{
name: "create codespace with default branch and 30m idle timeout",
fields: fields{
apiClient: &apiClientMock{
GetCodespaceRegionLocationFunc: func(ctx context.Context) (string, error) {
return "EUROPE", nil
},
GetRepositoryFunc: func(ctx context.Context, nwo string) (*api.Repository, error) {
return &api.Repository{
ID: 1234,
FullName: nwo,
DefaultBranch: "main",
}, nil
},
GetCodespacesMachinesFunc: func(ctx context.Context, repoID int, branch, location string) ([]*api.Machine, error) {
return []*api.Machine{
{
Name: "GIGA",
DisplayName: "Gigabits of a machine",
},
}, nil
},
CreateCodespaceFunc: func(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error) {
if params.Branch != "main" {
return nil, fmt.Errorf("got branch %q, want %q", params.Branch, "main")
}
if params.IdleTimeoutMinutes != 30 {
return nil, fmt.Errorf("idle timeout minutes was %v", params.IdleTimeoutMinutes)
}
return &api.Codespace{
Name: "monalisa-dotfiles-abcd1234",
}, nil
},
},
},
opts: createOptions{
repo: "monalisa/dotfiles",
branch: "",
machine: "GIGA",
showStatus: false,
idleTimeout: 30 * time.Minute,
},
wantStdout: "monalisa-dotfiles-abcd1234\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
io, _, stdout, stderr := iostreams.Test()
a := &App{
io: io,
apiClient: tt.fields.apiClient,
}
if err := a.Create(context.Background(), tt.opts); (err != nil) != tt.wantErr {
t.Errorf("App.Create() error = %v, wantErr %v", err, tt.wantErr)
}
if got := stdout.String(); got != tt.wantStdout {
t.Errorf("stdout = %v, want %v", got, tt.wantStdout)
}
if got := stderr.String(); got != tt.wantStderr {
t.Errorf("stderr = %v, want %v", got, tt.wantStderr)
}
})
}
}
func TestBuildDisplayName(t *testing.T) {
tests := []struct {
name string
prebuildAvailability string
expectedDisplayName string
}{
{
name: "prebuild availability is pool",
prebuildAvailability: "pool",
expectedDisplayName: "4 cores, 8 GB RAM, 32 GB storage (Prebuild ready)",
},
{
name: "prebuild availability is blob",
prebuildAvailability: "blob",
expectedDisplayName: "4 cores, 8 GB RAM, 32 GB storage (Prebuild ready)",
},
{
name: "prebuild availability is none",
prebuildAvailability: "none",
expectedDisplayName: "4 cores, 8 GB RAM, 32 GB storage",
},
{
name: "prebuild availability is empty",
prebuildAvailability: "",
expectedDisplayName: "4 cores, 8 GB RAM, 32 GB storage",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
displayName := buildDisplayName("4 cores, 8 GB RAM, 32 GB storage", tt.prebuildAvailability)
if displayName != tt.expectedDisplayName {
t.Errorf("displayName = %q, expectedDisplayName %q", displayName, tt.expectedDisplayName)
}
})
}
}

View file

@ -190,7 +190,7 @@ func TestDelete(t *testing.T) {
io, _, stdout, stderr := iostreams.Test()
io.SetStdinTTY(true)
io.SetStdoutTTY(true)
app := NewApp(io, apiMock)
app := NewApp(io, nil, apiMock)
err := app.Delete(context.Background(), opts)
if (err != nil) != tt.wantErr {
t.Errorf("delete() error = %v, wantErr %v", err, tt.wantErr)

View file

@ -41,19 +41,14 @@ func (a *App) Logs(ctx context.Context, codespaceName string, follow bool) (err
return fmt.Errorf("get or choose codespace: %w", err)
}
user, err := a.apiClient.GetUser(ctx)
if err != nil {
return fmt.Errorf("getting user: %w", err)
}
authkeys := make(chan error, 1)
go func() {
authkeys <- checkAuthorizedKeys(ctx, a.apiClient, user.Login)
authkeys <- checkAuthorizedKeys(ctx, a.apiClient)
}()
session, err := codespaces.ConnectToLiveshare(ctx, a, noopLogger(), a.apiClient, codespace)
if err != nil {
return fmt.Errorf("connecting to Live Share: %w", err)
return fmt.Errorf("connecting to codespace: %w", err)
}
defer safeClose(session, &err)

View file

@ -59,7 +59,7 @@ func (a *App) ListPorts(ctx context.Context, codespaceName string, exporter cmdu
session, err := codespaces.ConnectToLiveshare(ctx, a, noopLogger(), a.apiClient, codespace)
if err != nil {
return fmt.Errorf("error connecting to Live Share: %w", err)
return fmt.Errorf("error connecting to codespace: %w", err)
}
defer safeClose(session, &err)
@ -245,7 +245,7 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar
session, err := codespaces.ConnectToLiveshare(ctx, a, noopLogger(), a.apiClient, codespace)
if err != nil {
return fmt.Errorf("error connecting to Live Share: %w", err)
return fmt.Errorf("error connecting to codespace: %w", err)
}
defer safeClose(session, &err)
@ -321,7 +321,7 @@ func (a *App) ForwardPorts(ctx context.Context, codespaceName string, ports []st
session, err := codespaces.ConnectToLiveshare(ctx, a, noopLogger(), a.apiClient, codespace)
if err != nil {
return fmt.Errorf("error connecting to Live Share: %w", err)
return fmt.Errorf("error connecting to codespace: %w", err)
}
defer safeClose(session, &err)

View file

@ -4,15 +4,21 @@ package codespace
import (
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"path/filepath"
"strings"
"sync"
"text/template"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/codespaces"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/liveshare"
"github.com/spf13/cobra"
@ -24,6 +30,8 @@ type sshOptions struct {
serverPort int
debug bool
debugFile string
stdio bool
config bool
scpArgs []string // scp arguments, for 'cs cp' (nil for 'cs ssh')
}
@ -33,8 +41,57 @@ func newSSHCmd(app *App) *cobra.Command {
sshCmd := &cobra.Command{
Use: "ssh [<flags>...] [-- <ssh-flags>...] [<command>]",
Short: "SSH into a codespace",
Long: heredoc.Doc(`
The 'ssh' command is used to SSH into a codespace. In its simplest form, you can
run 'gh cs ssh', select a codespace interactively, and connect.
The 'ssh' command also supports deeper integration with OpenSSH using a
'--config' option that generates per-codespace ssh configuration in OpenSSH
format. Including this configuration in your ~/.ssh/config improves the user
experience of tools that integrate with OpenSSH, such as bash/zsh completion of
ssh hostnames, remote path completion for scp/rsync/sshfs, git ssh remotes, and
so on.
Once that is set up (see the second example below), you can ssh to codespaces as
if they were ordinary remote hosts (using 'ssh', not 'gh cs ssh').
`),
Example: heredoc.Doc(`
$ gh codespace ssh
$ gh codespace ssh --config > ~/.ssh/codespaces
$ echo 'include ~/.ssh/codespaces' >> ~/.ssh/config'
`),
PreRunE: func(c *cobra.Command, args []string) error {
if opts.stdio {
if opts.codespace == "" {
return errors.New("`--stdio` requires explicit `--codespace`")
}
if opts.config {
return errors.New("cannot use `--stdio` with `--config`")
}
if opts.serverPort != 0 {
return errors.New("cannot use `--stdio` with `--server-port`")
}
if opts.profile != "" {
return errors.New("cannot use `--stdio` with `--profile`")
}
}
if opts.config {
if opts.profile != "" {
return errors.New("cannot use `--config` with `--profile`")
}
if opts.serverPort != 0 {
return errors.New("cannot use `--config` with `--server-port`")
}
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return app.SSH(cmd.Context(), args, opts)
if opts.config {
return app.printOpenSSHConfig(cmd.Context(), opts)
} else {
return app.SSH(cmd.Context(), args, opts)
}
},
DisableFlagsInUseLine: true,
}
@ -44,6 +101,11 @@ func newSSHCmd(app *App) *cobra.Command {
sshCmd.Flags().StringVarP(&opts.codespace, "codespace", "c", "", "Name of the codespace")
sshCmd.Flags().BoolVarP(&opts.debug, "debug", "d", false, "Log debug data to a file")
sshCmd.Flags().StringVarP(&opts.debugFile, "debug-file", "", "", "Path of the file log to")
sshCmd.Flags().BoolVarP(&opts.config, "config", "", false, "Write OpenSSH configuration to stdout")
sshCmd.Flags().BoolVar(&opts.stdio, "stdio", false, "Proxy sshd connection to stdio")
if err := sshCmd.Flags().MarkHidden("stdio"); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
}
return sshCmd
}
@ -54,23 +116,18 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// While connecting, ensure in the background that the user has keys installed.
// That lets us report a more useful error message if they don't.
authkeys := make(chan error, 1)
go func() {
authkeys <- checkAuthorizedKeys(ctx, a.apiClient)
}()
codespace, err := getOrChooseCodespace(ctx, a.apiClient, opts.codespace)
if err != nil {
return fmt.Errorf("get or choose codespace: %w", err)
}
// TODO(josebalius): We can fetch the user in parallel to everything else
// we should convert this call and others to happen async
user, err := a.apiClient.GetUser(ctx)
if err != nil {
return fmt.Errorf("error getting user: %w", err)
}
authkeys := make(chan error, 1)
go func() {
authkeys <- checkAuthorizedKeys(ctx, a.apiClient, user.Login)
}()
liveshareLogger := noopLogger()
if opts.debug {
debugLogger, err := newFileLogger(opts.debugFile)
@ -85,14 +142,13 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
session, err := codespaces.ConnectToLiveshare(ctx, a, liveshareLogger, a.apiClient, codespace)
if err != nil {
return fmt.Errorf("error connecting to Live Share: %w", err)
if authErr := <-authkeys; authErr != nil {
return authErr
}
return fmt.Errorf("error connecting to codespace: %w", err)
}
defer safeClose(session, &err)
if err := <-authkeys; err != nil {
return err
}
a.StartProgressIndicatorWithLabel("Fetching SSH Details")
remoteSSHServerPort, sshUser, err := session.StartSSHServer(ctx)
a.StopProgressIndicator()
@ -100,6 +156,13 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
return fmt.Errorf("error getting ssh server details: %w", err)
}
if opts.stdio {
fwd := liveshare.NewPortForwarder(session, "sshd", remoteSSHServerPort, true)
stdio := newReadWriteCloser(os.Stdin, os.Stdout)
err := fwd.Forward(ctx, stdio) // always non-nil
return fmt.Errorf("tunnel closed: %w", err)
}
localSSHServerPort := opts.serverPort
usingCustomPort := localSSHServerPort != 0 // suppress log of command line in Shell
@ -146,6 +209,130 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
}
}
func (a *App) printOpenSSHConfig(ctx context.Context, opts sshOptions) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var err error
var csList []*api.Codespace
if opts.codespace == "" {
a.StartProgressIndicatorWithLabel("Fetching codespaces")
csList, err = a.apiClient.ListCodespaces(ctx, -1)
a.StopProgressIndicator()
} else {
var codespace *api.Codespace
codespace, err = getOrChooseCodespace(ctx, a.apiClient, opts.codespace)
csList = []*api.Codespace{codespace}
}
if err != nil {
return fmt.Errorf("error getting codespace info: %w", err)
}
type sshResult struct {
codespace *api.Codespace
user string // on success, the remote ssh username; else nil
err error
}
sshUsers := make(chan sshResult, len(csList))
var wg sync.WaitGroup
var status error
for _, cs := range csList {
if cs.State != "Available" && opts.codespace == "" {
fmt.Fprintf(os.Stderr, "skipping unavailable codespace %s: %s\n", cs.Name, cs.State)
status = cmdutil.SilentError
continue
}
cs := cs
wg.Add(1)
go func() {
result := sshResult{}
defer wg.Done()
session, err := codespaces.ConnectToLiveshare(ctx, a, noopLogger(), a.apiClient, cs)
if err != nil {
result.err = fmt.Errorf("error connecting to codespace: %w", err)
} else {
defer session.Close()
_, result.user, err = session.StartSSHServer(ctx)
if err != nil {
result.err = fmt.Errorf("error getting ssh server details: %w", err)
} else {
result.codespace = cs
}
}
sshUsers <- result
}()
}
go func() {
wg.Wait()
close(sshUsers)
}()
// While the above fetches are running, ensure that the user has keys installed.
// That lets us report a more useful error message if they don't.
if err = checkAuthorizedKeys(ctx, a.apiClient); err != nil {
return err
}
t, err := template.New("ssh_config").Parse(heredoc.Doc(`
Host cs.{{.Name}}.{{.EscapedRef}}
User {{.SSHUser}}
ProxyCommand {{.GHExec}} cs ssh -c {{.Name}} --stdio
UserKnownHostsFile=/dev/null
StrictHostKeyChecking no
LogLevel quiet
ControlMaster auto
`))
if err != nil {
return fmt.Errorf("error formatting template: %w", err)
}
ghExec := a.executable.Executable()
for result := range sshUsers {
if result.err != nil {
fmt.Fprintf(os.Stderr, "%v\n", result.err)
status = cmdutil.SilentError
continue
}
// codespaceSSHConfig contains values needed to write an OpenSSH host
// configuration for a single codespace. For example:
//
// Host {{Name}}.{{EscapedRef}
// User {{SSHUser}
// ProxyCommand {{GHExec}} cs ssh -c {{Name}} --stdio
//
// EscapedRef is included in the name to help distinguish between codespaces
// when tab-completing ssh hostnames. '/' characters in EscapedRef are
// flattened to '-' to prevent problems with tab completion or when the
// hostname appears in ControlMaster socket paths.
type codespaceSSHConfig struct {
Name string // the codespace name, passed to `ssh -c`
EscapedRef string // the currently checked-out branch
SSHUser string // the remote ssh username
GHExec string // path used for invoking the current `gh` binary
}
conf := codespaceSSHConfig{
Name: result.codespace.Name,
EscapedRef: strings.ReplaceAll(result.codespace.GitStatus.Ref, "/", "-"),
SSHUser: result.user,
GHExec: ghExec,
}
if err := t.Execute(a.io.Out, conf); err != nil {
return err
}
}
return status
}
type cpOptions struct {
sshOptions
recursive bool // -r
@ -156,36 +343,36 @@ func newCpCmd(app *App) *cobra.Command {
var opts cpOptions
cpCmd := &cobra.Command{
Use: "cp [-e] [-r] srcs... dest",
Use: "cp [-e] [-r] <sources>... <dest>",
Short: "Copy files between local and remote file systems",
Long: `
The cp command copies files between the local and remote file systems.
Long: heredoc.Docf(`
The cp command copies files between the local and remote file systems.
As with the UNIX cp command, the first argument specifies the source and the last
specifies the destination; additional sources may be specified after the first,
if the destination is a directory.
As with the UNIX %[1]scp%[1]s command, the first argument specifies the source and the last
specifies the destination; additional sources may be specified after the first,
if the destination is a directory.
The -r (recursive) flag is required if any source is a directory.
The %[1]s--recursive%[1]s flag is required if any source is a directory.
A 'remote:' prefix on any file name argument indicates that it refers to
the file system of the remote (Codespace) machine. It is resolved relative
to the home directory of the remote user.
A "remote:" prefix on any file name argument indicates that it refers to
the file system of the remote (Codespace) machine. It is resolved relative
to the home directory of the remote user.
By default, remote file names are interpreted literally. With the -e flag,
each such argument is treated in the manner of scp, as a Bash expression to
be evaluated on the remote machine, subject to expansion of tildes, braces,
globs, environment variables, and backticks, as in these examples:
$ gh codespace cp -e README.md 'remote:/workspace/$RepositoryName/'
$ gh codespace cp -e 'remote:~/*.go' ./gofiles/
$ gh codespace cp -e 'remote:/workspace/myproj/go.{mod,sum}' ./gofiles/
For security, do not use the -e flag with arguments provided by untrusted
users; see https://lwn.net/Articles/835962/ for discussion.
`,
By default, remote file names are interpreted literally. With the %[1]s--expand%[1]s flag,
each such argument is treated in the manner of %[1]sscp%[1]s, as a Bash expression to
be evaluated on the remote machine, subject to expansion of tildes, braces, globs,
environment variables, and backticks. For security, do not use this flag with arguments
provided by untrusted users; see <https://lwn.net/Articles/835962/> for discussion.
`, "`"),
Example: heredoc.Doc(`
$ gh codespace cp -e README.md 'remote:/workspaces/$RepositoryName/'
$ gh codespace cp -e 'remote:~/*.go' ./gofiles/
$ gh codespace cp -e 'remote:/workspaces/myproj/go.{mod,sum}' ./gofiles/
`),
RunE: func(cmd *cobra.Command, args []string) error {
return app.Copy(cmd.Context(), args, opts)
},
DisableFlagsInUseLine: true,
}
// We don't expose all sshOptions.
@ -276,3 +463,21 @@ func (fl *fileLogger) Name() string {
func (fl *fileLogger) Close() error {
return fl.f.Close()
}
type combinedReadWriteCloser struct {
io.ReadCloser
io.WriteCloser
}
func newReadWriteCloser(reader io.ReadCloser, writer io.WriteCloser) io.ReadWriteCloser {
return &combinedReadWriteCloser{reader, writer}
}
func (crwc *combinedReadWriteCloser) Close() error {
werr := crwc.WriteCloser.Close()
rerr := crwc.ReadCloser.Close()
if werr != nil {
return werr
}
return rerr
}

View file

@ -6,6 +6,7 @@ import (
"github.com/cli/cli/v2/internal/config"
cmdGet "github.com/cli/cli/v2/pkg/cmd/config/get"
cmdList "github.com/cli/cli/v2/pkg/cmd/config/list"
cmdSet "github.com/cli/cli/v2/pkg/cmd/config/set"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/spf13/cobra"
@ -33,6 +34,7 @@ func NewCmdConfig(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(cmdGet.NewCmdConfigGet(f, nil))
cmd.AddCommand(cmdSet.NewCmdConfigSet(f, nil))
cmd.AddCommand(cmdList.NewCmdConfigList(f, nil))
return cmd
}

View file

@ -53,7 +53,7 @@ func NewCmdConfigGet(f *cmdutil.Factory, runF func(*GetOptions) error) *cobra.Co
}
func getRun(opts *GetOptions) error {
val, err := opts.Config.Get(opts.Hostname, opts.Key)
val, err := opts.Config.GetOrDefault(opts.Hostname, opts.Key)
if err != nil {
return err
}

View file

@ -115,6 +115,8 @@ func Test_getRun(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, tt.stdout, stdout.String())
assert.Equal(t, tt.stderr, stderr.String())
_, err = tt.input.Config.GetOrDefault("", "_written")
assert.Error(t, err)
_, err = tt.input.Config.Get("", "_written")
assert.Error(t, err)
})

View file

@ -0,0 +1,70 @@
package list
import (
"fmt"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
)
type ListOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
Hostname string
}
func NewCmdConfigList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
opts := &ListOptions{
IO: f.IOStreams,
Config: f.Config,
}
cmd := &cobra.Command{
Use: "list",
Short: "Print a list of configuration keys and values",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
if runF != nil {
return runF(opts)
}
return listRun(opts)
},
}
cmd.Flags().StringVarP(&opts.Hostname, "host", "h", "", "Get per-host configuration")
return cmd
}
func listRun(opts *ListOptions) error {
cfg, err := opts.Config()
if err != nil {
return err
}
var host string
if opts.Hostname != "" {
host = opts.Hostname
} else {
host, err = cfg.DefaultHost()
if err != nil {
return err
}
}
configOptions := config.ConfigOptions()
for _, key := range configOptions {
val, err := cfg.GetOrDefault(host, key.Key)
if err != nil {
return err
}
fmt.Fprintf(opts.IO.Out, "%s=%s\n", key.Key, val)
}
return nil
}

View file

@ -0,0 +1,113 @@
package list
import (
"bytes"
"testing"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
)
func TestNewCmdConfigList(t *testing.T) {
tests := []struct {
name string
input string
output ListOptions
wantsErr bool
}{
{
name: "no arguments",
input: "",
output: ListOptions{},
wantsErr: false,
},
{
name: "list with host",
input: "--host HOST.com",
output: ListOptions{Hostname: "HOST.com"},
wantsErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := &cmdutil.Factory{
Config: func() (config.Config, error) {
return config.ConfigStub{}, nil
},
}
argv, err := shlex.Split(tt.input)
assert.NoError(t, err)
var gotOpts *ListOptions
cmd := NewCmdConfigList(f, func(opts *ListOptions) error {
gotOpts = opts
return nil
})
cmd.Flags().BoolP("help", "x", false, "")
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err = cmd.ExecuteC()
if tt.wantsErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.output.Hostname, gotOpts.Hostname)
})
}
}
func Test_listRun(t *testing.T) {
tests := []struct {
name string
input *ListOptions
config config.ConfigStub
stdout string
wantErr bool
}{
{
name: "list",
config: config.ConfigStub{
"HOST:git_protocol": "ssh",
"HOST:editor": "/usr/bin/vim",
"HOST:prompt": "disabled",
"HOST:pager": "less",
"HOST:http_unix_socket": "",
"HOST:browser": "brave",
},
input: &ListOptions{Hostname: "HOST"}, // ConfigStub gives empty DefaultHost
stdout: `git_protocol=ssh
editor=/usr/bin/vim
prompt=disabled
pager=less
http_unix_socket=
browser=brave
`,
},
}
for _, tt := range tests {
io, _, stdout, _ := iostreams.Test()
tt.input.IO = io
tt.input.Config = func() (config.Config, error) {
return tt.config, nil
}
t.Run(tt.name, func(t *testing.T) {
err := listRun(tt.input)
assert.NoError(t, err)
assert.Equal(t, tt.stdout, stdout.String())
//assert.Equal(t, tt.stderr, stderr.String())
})
}
}

View file

@ -145,11 +145,11 @@ func Test_setRun(t *testing.T) {
assert.Equal(t, tt.stdout, stdout.String())
assert.Equal(t, tt.stderr, stderr.String())
val, err := tt.input.Config.Get(tt.input.Hostname, tt.input.Key)
val, err := tt.input.Config.GetOrDefault(tt.input.Hostname, tt.input.Key)
assert.NoError(t, err)
assert.Equal(t, tt.expectedValue, val)
val, err = tt.input.Config.Get("", "_written")
val, err = tt.input.Config.GetOrDefault("", "_written")
assert.NoError(t, err)
assert.Equal(t, "true", val)
})

View file

@ -6,11 +6,13 @@ import (
"os"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/extensions"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
@ -106,7 +108,15 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
return err
}
return m.Install(repo)
if err := m.Install(repo); err != nil {
return err
}
if io.IsStdoutTTY() {
cs := io.ColorScheme()
fmt.Fprintf(io.Out, "%s Installed extension %s\n", cs.SuccessIcon(), args[0])
}
return nil
},
},
func() *cobra.Command {
@ -117,7 +127,7 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
Short: "Upgrade installed extensions",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 && !flagAll {
return cmdutil.FlagErrorf("must specify an extension to upgrade")
return cmdutil.FlagErrorf("specify an extension to upgrade or `--all`")
}
if len(args) > 0 && flagAll {
return cmdutil.FlagErrorf("cannot use `--all` with extension name")
@ -134,16 +144,18 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
}
cs := io.ColorScheme()
err := m.Upgrade(name, flagForce)
if err != nil {
if err != nil && !errors.Is(err, upToDateError) {
if name != "" {
fmt.Fprintf(io.ErrOut, "%s Failed upgrading extension %s: %s", cs.FailureIcon(), name, err)
fmt.Fprintf(io.ErrOut, "%s Failed upgrading extension %s: %s\n", cs.FailureIcon(), name, err)
} else {
fmt.Fprintf(io.ErrOut, "%s Failed upgrading extensions", cs.FailureIcon())
fmt.Fprintf(io.ErrOut, "%s Failed upgrading extensions\n", cs.FailureIcon())
}
return cmdutil.SilentError
}
if io.IsStdoutTTY() {
if name != "" {
if errors.Is(err, upToDateError) {
fmt.Fprintf(io.Out, "%s Extension already up to date\n", cs.SuccessIcon())
} else if name != "" {
fmt.Fprintf(io.Out, "%s Successfully upgraded extension %s\n", cs.SuccessIcon(), name)
} else {
fmt.Fprintf(io.Out, "%s Successfully upgraded extensions\n", cs.SuccessIcon())
@ -172,41 +184,127 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
return nil
},
},
&cobra.Command{
Use: "create <name>",
Short: "Create a new extension",
Args: cmdutil.ExactArgs(1, "must specify a name for the extension"),
RunE: func(cmd *cobra.Command, args []string) error {
extName := args[0]
if !strings.HasPrefix(extName, "gh-") {
extName = "gh-" + extName
func() *cobra.Command {
promptCreate := func() (string, extensions.ExtTemplateType, error) {
var extName string
var extTmplType int
err := prompt.SurveyAskOne(&survey.Input{
Message: "Extension name:",
}, &extName)
if err != nil {
return extName, -1, err
}
if err := m.Create(extName); err != nil {
return err
}
if !io.IsStdoutTTY() {
return nil
}
link := "https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions"
cs := io.ColorScheme()
out := heredoc.Docf(`
err = prompt.SurveyAskOne(&survey.Select{
Message: "What kind of extension?",
Options: []string{
"Script (Bash, Ruby, Python, etc)",
"Go",
"Other Precompiled (C++, Rust, etc)",
},
}, &extTmplType)
return extName, extensions.ExtTemplateType(extTmplType), err
}
var flagType string
cmd := &cobra.Command{
Use: "create [<name>]",
Short: "Create a new extension",
Example: heredoc.Doc(`
# Use interactively
gh extension create
# Create a script-based extension
gh extension create foobar
# Create a Go extension
gh extension create --precompiled=go foobar
# Create a non-Go precompiled extension
gh extension create --precompiled=other foobar
`),
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("precompiled") {
if flagType != "go" && flagType != "other" {
return cmdutil.FlagErrorf("value for --precompiled must be 'go' or 'other'. Got '%s'", flagType)
}
}
var extName string
var err error
tmplType := extensions.GitTemplateType
if len(args) == 0 {
if io.IsStdoutTTY() {
extName, tmplType, err = promptCreate()
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
}
}
} else {
extName = args[0]
if flagType == "go" {
tmplType = extensions.GoBinTemplateType
} else if flagType == "other" {
tmplType = extensions.OtherBinTemplateType
}
}
var fullName string
if strings.HasPrefix(extName, "gh-") {
fullName = extName
extName = extName[3:]
} else {
fullName = "gh-" + extName
}
if err := m.Create(fullName, tmplType); err != nil {
return err
}
if !io.IsStdoutTTY() {
return nil
}
var goBinChecks string
steps := fmt.Sprintf(
"- run 'cd %[1]s; gh extension install .; gh %[2]s' to see your new extension in action",
fullName, extName)
cs := io.ColorScheme()
if tmplType == extensions.GoBinTemplateType {
goBinChecks = heredoc.Docf(`
%[1]s Downloaded Go dependencies
%[1]s Built %[2]s binary
`, cs.SuccessIcon(), fullName)
steps = heredoc.Docf(`
- run 'cd %[1]s; gh extension install .; gh %[2]s' to see your new extension in action
- use 'go build && gh %[2]s' to see changes in your code as you develop`, fullName, extName)
} else if tmplType == extensions.OtherBinTemplateType {
steps = heredoc.Docf(`
- run 'cd %[1]s; gh extension install .' to install your extension locally
- fill in script/build.sh with your compilation script for automated builds
- compile a %[1]s binary locally and run 'gh %[2]s' to see changes`, fullName, extName)
}
link := "https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions"
out := heredoc.Docf(`
%[1]s Created directory %[2]s
%[1]s Initialized git repository
%[1]s Set up extension scaffolding
%[6]s
%[2]s is ready for development!
%[2]s is ready for development
Install locally with: cd %[2]s && gh extension install .
Publish to GitHub with: gh repo create %[2]s
%[4]s
%[5]s
- commit and use 'gh repo create' to share your extension with others
For more information on writing extensions:
%[3]s
`, cs.SuccessIcon(), extName, link)
fmt.Fprint(io.Out, out)
return nil
},
},
`, cs.SuccessIcon(), fullName, link, cs.Bold("Next Steps"), steps, goBinChecks)
fmt.Fprint(io.Out, out)
return nil
},
}
cmd.Flags().StringVar(&flagType, "precompiled", "", "Create a precompiled extension. Possible values: go, other")
return cmd
}(),
)
return &extCmd

View file

@ -1,6 +1,7 @@
package extension
import (
"errors"
"io/ioutil"
"net/http"
"os"
@ -14,6 +15,7 @@ import (
"github.com/cli/cli/v2/pkg/extensions"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)
@ -28,6 +30,7 @@ func TestNewCmdExtension(t *testing.T) {
name string
args []string
managerStubs func(em *extensions.ExtensionManagerMock) func(*testing.T)
askStubs func(as *prompt.AskStubber)
isTTY bool
wantErr bool
errMsg string
@ -84,10 +87,10 @@ func TestNewCmdExtension(t *testing.T) {
},
},
{
name: "upgrade error",
name: "upgrade argument error",
args: []string{"upgrade"},
wantErr: true,
errMsg: "must specify an extension to upgrade",
errMsg: "specify an extension to upgrade or `--all`",
},
{
name: "upgrade an extension",
@ -120,6 +123,42 @@ func TestNewCmdExtension(t *testing.T) {
},
isTTY: false,
},
{
name: "upgrade an up-to-date extension",
args: []string{"upgrade", "hello"},
managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
em.UpgradeFunc = func(name string, force bool) error {
return upToDateError
}
return func(t *testing.T) {
calls := em.UpgradeCalls()
assert.Equal(t, 1, len(calls))
assert.Equal(t, "hello", calls[0].Name)
}
},
isTTY: true,
wantStdout: "✓ Extension already up to date\n",
wantStderr: "",
},
{
name: "upgrade extension error",
args: []string{"upgrade", "hello"},
managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
em.UpgradeFunc = func(name string, force bool) error {
return errors.New("oh no")
}
return func(t *testing.T) {
calls := em.UpgradeCalls()
assert.Equal(t, 1, len(calls))
assert.Equal(t, "hello", calls[0].Name)
}
},
isTTY: false,
wantErr: true,
errMsg: "SilentError",
wantStdout: "",
wantStderr: "X Failed upgrading extension hello: oh no\n",
},
{
name: "upgrade an extension gh-prefix",
args: []string{"upgrade", "gh-hello"},
@ -263,10 +302,77 @@ func TestNewCmdExtension(t *testing.T) {
wantStdout: "gh test\tcli/gh-test\t\ngh test2\tcli/gh-test2\tUpgrade available\n",
},
{
name: "create extension tty",
args: []string{"create", "test"},
name: "create extension interactive",
args: []string{"create"},
managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
em.CreateFunc = func(name string) error {
em.CreateFunc = func(name string, tmplType extensions.ExtTemplateType) error {
return nil
}
return func(t *testing.T) {
calls := em.CreateCalls()
assert.Equal(t, 1, len(calls))
assert.Equal(t, "gh-test", calls[0].Name)
}
},
isTTY: true,
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("Extension name:").AnswerWith("test")
as.StubPrompt("What kind of extension?").
AssertOptions([]string{"Script (Bash, Ruby, Python, etc)", "Go", "Other Precompiled (C++, Rust, etc)"}).
AnswerDefault()
},
wantStdout: heredoc.Doc(`
Created directory gh-test
Initialized git repository
Set up extension scaffolding
gh-test is ready for development!
Next Steps
- run 'cd gh-test; gh extension install .; gh test' to see your new extension in action
- commit and use 'gh repo create' to share your extension with others
For more information on writing extensions:
https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions
`),
},
{
name: "create extension with arg, --precompiled=go",
args: []string{"create", "test", "--precompiled", "go"},
managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
em.CreateFunc = func(name string, tmplType extensions.ExtTemplateType) error {
return nil
}
return func(t *testing.T) {
calls := em.CreateCalls()
assert.Equal(t, 1, len(calls))
assert.Equal(t, "gh-test", calls[0].Name)
}
},
isTTY: true,
wantStdout: heredoc.Doc(`
Created directory gh-test
Initialized git repository
Set up extension scaffolding
Downloaded Go dependencies
Built gh-test binary
gh-test is ready for development!
Next Steps
- run 'cd gh-test; gh extension install .; gh test' to see your new extension in action
- use 'go build && gh test' to see changes in your code as you develop
- commit and use 'gh repo create' to share your extension with others
For more information on writing extensions:
https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions
`),
},
{
name: "create extension with arg, --precompiled=other",
args: []string{"create", "test", "--precompiled", "other"},
managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
em.CreateFunc = func(name string, tmplType extensions.ExtTemplateType) error {
return nil
}
return func(t *testing.T) {
@ -281,11 +387,42 @@ func TestNewCmdExtension(t *testing.T) {
Initialized git repository
Set up extension scaffolding
gh-test is ready for development
gh-test is ready for development!
Install locally with: cd gh-test && gh extension install .
Next Steps
- run 'cd gh-test; gh extension install .' to install your extension locally
- fill in script/build.sh with your compilation script for automated builds
- compile a gh-test binary locally and run 'gh test' to see changes
- commit and use 'gh repo create' to share your extension with others
Publish to GitHub with: gh repo create gh-test
For more information on writing extensions:
https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions
`),
},
{
name: "create extension tty with argument",
args: []string{"create", "test"},
managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
em.CreateFunc = func(name string, tmplType extensions.ExtTemplateType) error {
return nil
}
return func(t *testing.T) {
calls := em.CreateCalls()
assert.Equal(t, 1, len(calls))
assert.Equal(t, "gh-test", calls[0].Name)
}
},
isTTY: true,
wantStdout: heredoc.Doc(`
Created directory gh-test
Initialized git repository
Set up extension scaffolding
gh-test is ready for development!
Next Steps
- run 'cd gh-test; gh extension install .; gh test' to see your new extension in action
- commit and use 'gh repo create' to share your extension with others
For more information on writing extensions:
https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions
@ -295,7 +432,7 @@ func TestNewCmdExtension(t *testing.T) {
name: "create extension notty",
args: []string{"create", "gh-test"},
managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
em.CreateFunc = func(name string) error {
em.CreateFunc = func(name string, tmplType extensions.ExtTemplateType) error {
return nil
}
return func(t *testing.T) {
@ -321,6 +458,11 @@ func TestNewCmdExtension(t *testing.T) {
assertFunc = tt.managerStubs(em)
}
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)
}
reg := httpmock.Registry{}
defer reg.Verify(t)
client := http.Client{Transport: &reg}

View file

@ -0,0 +1,4 @@
#!/usr/bin/env bash
echo "TODO implement this script."
echo "It should build binaries in dist/<platform>-<arch>[.exe] as needed."
exit 1

View file

@ -0,0 +1,26 @@
package main
import (
"fmt"
"github.com/cli/go-gh"
)
func main() {
fmt.Println("hi world, this is the %s extension!")
client, err := gh.RESTClient(nil)
if err != nil {
fmt.Println(err)
return
}
response := struct {Login string}{}
err = client.Get("user", &response)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("running as %%s\n", response.Login)
}
// For more examples of using go-gh, see:
// https://github.com/cli/go-gh/blob/trunk/example_gh_test.go

View file

@ -0,0 +1,14 @@
name: release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: cli/gh-extension-precompile@v1

View file

@ -0,0 +1,16 @@
name: release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: cli/gh-extension-precompile@v1
with:
build_script_override: "script/build.sh"

View file

@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -e
echo "Hello %[1]s!"
# Snippets to help get started:
# Determine if an executable is in the PATH
# if ! type -p ruby >/dev/null; then
# echo "Ruby not found on the system" >&2
# exit 1
# fi
# Pass arguments through to another command
# gh issue list "$@" -R cli/cli
# Using the gh api command to retrieve and format information
# QUERY='
# query($endCursor: String) {
# viewer {
# repositories(first: 100, after: $endCursor) {
# nodes {
# nameWithOwner
# stargazerCount
# }
# }
# }
# }
# '
# TEMPLATE='
# {{- range $repo := .data.viewer.repositories.nodes -}}
# {{- printf "name: %%s - stargazers: %%v\n" $repo.nameWithOwner $repo.stargazerCount -}}
# {{- end -}}
# '
# exec gh api graphql -f query="${QUERY}" --paginate --template="${TEMPLATE}"

View file

@ -2,6 +2,7 @@ package extension
import (
"bytes"
_ "embed"
"errors"
"fmt"
"io"
@ -16,8 +17,8 @@ import (
"strings"
"sync"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/extensions"
@ -32,7 +33,7 @@ type Manager struct {
lookPath func(string) (string, error)
findSh func() (string, error)
newCommand func(string, ...string) *exec.Cmd
platform func() string
platform func() (string, string)
client *http.Client
config config.Config
io *iostreams.IOStreams
@ -44,8 +45,12 @@ func NewManager(io *iostreams.IOStreams) *Manager {
lookPath: safeexec.LookPath,
findSh: findsh.Find,
newCommand: exec.Command,
platform: func() string {
return fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH)
platform: func() (string, string) {
ext := ""
if runtime.GOOS == "windows" {
ext = ".exe"
}
return fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH), ext
},
io: io,
}
@ -268,7 +273,7 @@ func (m *Manager) populateLatestVersions(exts []Extension) {
func (m *Manager) getLatestVersion(ext Extension) (string, error) {
if ext.isLocal {
return "", fmt.Errorf("unable to get latest version for local extensions")
return "", localExtensionUpgradeError
}
if ext.IsBinary() {
repo, err := ghrepo.FromFullName(ext.url)
@ -329,11 +334,10 @@ func (m *Manager) Install(repo ghrepo.Interface) error {
return err
}
if !hs {
// TODO open an issue hint, here?
return errors.New("extension is uninstallable: missing executable")
return errors.New("extension is not installable: missing executable")
}
protocol, _ := m.config.Get(repo.RepoHost(), "git_protocol")
protocol, _ := m.config.GetOrDefault(repo.RepoHost(), "git_protocol")
return m.installGit(ghrepo.FormatRemoteURL(repo, protocol), m.io.Out, m.io.ErrOut)
}
@ -344,19 +348,19 @@ func (m *Manager) installBin(repo ghrepo.Interface) error {
return err
}
suffix := m.platform()
platform, ext := m.platform()
var asset *releaseAsset
for _, a := range r.Assets {
if strings.HasSuffix(a.Name, suffix) {
if strings.HasSuffix(a.Name, platform+ext) {
asset = &a
break
}
}
if asset == nil {
return fmt.Errorf("%s unsupported for %s. Open an issue: `gh issue create -R %s/%s -t'Support %s'`",
repo.RepoName(),
suffix, repo.RepoOwner(), repo.RepoName(), suffix)
return fmt.Errorf(
"%[1]s unsupported for %[2]s. Open an issue: `gh issue create -R %[3]s/%[1]s -t'Support %[2]s'`",
repo.RepoName(), platform, repo.RepoOwner())
}
name := repo.RepoName()
@ -368,6 +372,7 @@ func (m *Manager) installBin(repo ghrepo.Interface) error {
}
binPath := filepath.Join(targetDir, name)
binPath += ext
err = downloadAsset(m.client, *asset, binPath)
if err != nil {
@ -482,6 +487,19 @@ func (m *Manager) upgradeExtension(ext Extension, force bool) error {
if ext.IsBinary() {
err = m.upgradeBinExtension(ext)
} else {
// Check if git extension has changed to a binary extension
var isBin bool
repo, repoErr := repoFromPath(filepath.Join(ext.Path(), ".."))
if repoErr == nil {
isBin, _ = isBinExtension(m.client, repo)
}
if isBin {
err = m.Remove(ext.Name())
if err != nil {
return fmt.Errorf("failed to migrate to new precompiled extension format: %w", err)
}
return m.installBin(repo)
}
err = m.upgradeGitExtension(ext, force)
}
return err
@ -492,17 +510,14 @@ func (m *Manager) upgradeGitExtension(ext Extension, force bool) error {
if err != nil {
return err
}
var cmds []*exec.Cmd
dir := filepath.Dir(ext.path)
if force {
fetchCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "fetch", "origin", "HEAD")
resetCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "reset", "--hard", "origin/HEAD")
cmds = []*exec.Cmd{fetchCmd, resetCmd}
} else {
pullCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "pull", "--ff-only")
cmds = []*exec.Cmd{pullCmd}
if err := m.newCommand(exe, "-C", dir, "fetch", "origin", "HEAD").Run(); err != nil {
return err
}
return m.newCommand(exe, "-C", dir, "reset", "--hard", "origin/HEAD").Run()
}
return runCmds(cmds)
return m.newCommand(exe, "-C", dir, "pull", "--ff-only").Run()
}
func (m *Manager) upgradeBinExtension(ext Extension) error {
@ -525,89 +540,119 @@ func (m *Manager) installDir() string {
return filepath.Join(m.dataDir(), "extensions")
}
func (m *Manager) Create(name string) error {
//go:embed ext_tmpls/goBinMain.go.txt
var mainGoTmpl string
//go:embed ext_tmpls/goBinWorkflow.yml
var goBinWorkflow []byte
//go:embed ext_tmpls/otherBinWorkflow.yml
var otherBinWorkflow []byte
//go:embed ext_tmpls/script.sh
var scriptTmpl string
//go:embed ext_tmpls/buildScript.sh
var buildScript []byte
func (m *Manager) Create(name string, tmplType extensions.ExtTemplateType) error {
exe, err := m.lookPath("git")
if err != nil {
return err
}
err = os.Mkdir(name, 0755)
if err != nil {
if err := m.newCommand(exe, "init", "--quiet", name).Run(); err != nil {
return err
}
initCmd := m.newCommand(exe, "init", "--quiet", name)
err = initCmd.Run()
if err != nil {
if tmplType == extensions.GoBinTemplateType {
return m.goBinScaffolding(exe, name)
} else if tmplType == extensions.OtherBinTemplateType {
return m.otherBinScaffolding(exe, name)
}
script := fmt.Sprintf(scriptTmpl, name)
if err := writeFile(filepath.Join(name, name), []byte(script), 0755); err != nil {
return err
}
fileTmpl := heredoc.Docf(`
#!/usr/bin/env bash
set -e
echo "Hello %[1]s!"
# Snippets to help get started:
# Determine if an executable is in the PATH
# if ! type -p ruby >/dev/null; then
# echo "Ruby not found on the system" >&2
# exit 1
# fi
# Pass arguments through to another command
# gh issue list "$@" -R cli/cli
# Using the gh api command to retrieve and format information
# QUERY='
# query($endCursor: String) {
# viewer {
# repositories(first: 100, after: $endCursor) {
# nodes {
# nameWithOwner
# stargazerCount
# }
# }
# }
# }
# '
# TEMPLATE='
# {{- range $repo := .data.viewer.repositories.nodes -}}
# {{- printf "name: %[2]s - stargazers: %[3]s\n" $repo.nameWithOwner $repo.stargazerCount -}}
# {{- end -}}
# '
# exec gh api graphql -f query="${QUERY}" --paginate --template="${TEMPLATE}"
`, name, "%s", "%v")
filePath := filepath.Join(name, name)
err = ioutil.WriteFile(filePath, []byte(fileTmpl), 0755)
if err != nil {
return err
}
wd, err := os.Getwd()
if err != nil {
return err
}
dir := filepath.Join(wd, name)
addCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "add", name, "--chmod=+x")
err = addCmd.Run()
return err
return m.newCommand(exe, "-C", name, "add", name, "--chmod=+x").Run()
}
func runCmds(cmds []*exec.Cmd) error {
for _, cmd := range cmds {
if err := cmd.Run(); err != nil {
return err
func (m *Manager) otherBinScaffolding(gitExe, name string) error {
if err := writeFile(filepath.Join(name, ".github", "workflows", "release.yml"), otherBinWorkflow, 0644); err != nil {
return err
}
buildScriptPath := filepath.Join("script", "build.sh")
if err := writeFile(filepath.Join(name, buildScriptPath), buildScript, 0755); err != nil {
return err
}
if err := m.newCommand(gitExe, "-C", name, "add", buildScriptPath, "--chmod=+x").Run(); err != nil {
return err
}
return m.newCommand(gitExe, "-C", name, "add", ".").Run()
}
func (m *Manager) goBinScaffolding(gitExe, name string) error {
goExe, err := m.lookPath("go")
if err != nil {
return fmt.Errorf("go is required for creating Go extensions: %w", err)
}
if err := writeFile(filepath.Join(name, ".github", "workflows", "release.yml"), goBinWorkflow, 0644); err != nil {
return err
}
mainGo := fmt.Sprintf(mainGoTmpl, name)
if err := writeFile(filepath.Join(name, "main.go"), []byte(mainGo), 0644); err != nil {
return err
}
host, err := m.config.DefaultHost()
if err != nil {
return err
}
currentUser, err := api.CurrentLoginName(api.NewClientFromHTTP(m.client), host)
if err != nil {
return err
}
goCmds := [][]string{
{"mod", "init", fmt.Sprintf("%s/%s/%s", host, currentUser, name)},
{"mod", "tidy"},
{"build"},
}
ignore := fmt.Sprintf("/%[1]s\n/%[1]s.exe\n", name)
if err := writeFile(filepath.Join(name, ".gitignore"), []byte(ignore), 0644); err != nil {
return err
}
for _, args := range goCmds {
goCmd := m.newCommand(goExe, args...)
goCmd.Dir = name
if err := goCmd.Run(); err != nil {
return fmt.Errorf("failed to set up go module: %w", err)
}
}
return nil
return m.newCommand(gitExe, "-C", name, "add", ".").Run()
}
func isSymlink(m os.FileMode) bool {
return m&os.ModeSymlink != 0
}
func writeFile(p string, contents []byte, mode os.FileMode) error {
if dir := filepath.Dir(p); dir != "." {
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
}
return os.WriteFile(p, contents, mode)
}
// reads the product of makeSymlink on Windows
func readPathFromFile(path string) (string, error) {
f, err := os.Open(path)
@ -635,7 +680,11 @@ func isBinExtension(client *http.Client, repo ghrepo.Interface) (isBin bool, err
for _, a := range r.Assets {
dists := possibleDists()
for _, d := range dists {
if strings.HasSuffix(a.Name, d) {
suffix := d
if strings.HasPrefix(d, "windows") {
suffix += ".exe"
}
if strings.HasSuffix(a.Name, suffix) {
isBin = true
break
}
@ -645,6 +694,32 @@ func isBinExtension(client *http.Client, repo ghrepo.Interface) (isBin bool, err
return
}
func repoFromPath(path string) (ghrepo.Interface, error) {
remotes, err := git.RemotesForPath(path)
if err != nil {
return nil, err
}
if len(remotes) == 0 {
return nil, fmt.Errorf("no remotes configured for %s", path)
}
var remote *git.Remote
for _, r := range remotes {
if r.Name == "origin" {
remote = r
break
}
}
if remote == nil {
remote = remotes[0]
}
return ghrepo.FromURL(remote.FetchURL)
}
func possibleDists() []string {
return []string{
"aix-ppc64",

View file

@ -9,14 +9,19 @@ import (
"os/exec"
"path/filepath"
"runtime"
"sort"
"strings"
"testing"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/run"
"github.com/cli/cli/v2/pkg/extensions"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
@ -25,6 +30,15 @@ func TestHelperProcess(t *testing.T) {
return
}
if err := func(args []string) error {
// git init should create the directory named by argument
if len(args) > 2 && strings.HasPrefix(strings.Join(args, " "), "git init") {
dir := args[len(args)-1]
if !strings.HasPrefix(dir, "-") {
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
}
}
fmt.Fprintf(os.Stdout, "%v\n", args)
return nil
}(os.Args[3:]); err != nil {
@ -52,8 +66,8 @@ func newTestManager(dir string, client *http.Client, io *iostreams.IOStreams) *M
config: config.NewBlankConfig(),
io: io,
client: client,
platform: func() string {
return "windows-amd64"
platform: func() (string, string) {
return "windows-amd64", ".exe"
},
}
}
@ -217,16 +231,14 @@ func TestManager_UpgradeExtensions(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, heredoc.Docf(
`
[hello]: [git -C %s --git-dir=%s pull --ff-only]
[hello]: [git -C %s pull --ff-only]
upgrade complete
[local]: local extensions can not be upgraded
[two]: [git -C %s --git-dir=%s pull --ff-only]
[two]: [git -C %s pull --ff-only]
upgrade complete
`,
filepath.Join(tempDir, "extensions", "gh-hello"),
filepath.Join(tempDir, "extensions", "gh-hello", ".git"),
filepath.Join(tempDir, "extensions", "gh-two"),
filepath.Join(tempDir, "extensions", "gh-two", ".git"),
), stdout.String())
assert.Equal(t, "", stderr.String())
}
@ -261,10 +273,9 @@ func TestManager_UpgradeExtension_GitExtension(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, heredoc.Docf(
`
[git -C %s --git-dir=%s pull --ff-only]
[git -C %s pull --ff-only]
`,
filepath.Join(tempDir, "extensions", "gh-remote"),
filepath.Join(tempDir, "extensions", "gh-remote", ".git"),
), stdout.String())
assert.Equal(t, "", stderr.String())
}
@ -272,7 +283,6 @@ func TestManager_UpgradeExtension_GitExtension(t *testing.T) {
func TestManager_UpgradeExtension_GitExtension_Force(t *testing.T) {
tempDir := t.TempDir()
extensionDir := filepath.Join(tempDir, "extensions", "gh-remote")
gitDir := filepath.Join(tempDir, "extensions", "gh-remote", ".git")
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote")))
io, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, nil, io)
@ -286,23 +296,97 @@ func TestManager_UpgradeExtension_GitExtension_Force(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, heredoc.Docf(
`
[git -C %s --git-dir=%s fetch origin HEAD]
[git -C %s --git-dir=%s reset --hard origin/HEAD]
[git -C %[1]s fetch origin HEAD]
[git -C %[1]s reset --hard origin/HEAD]
`,
extensionDir,
gitDir,
extensionDir,
gitDir,
), stdout.String())
assert.Equal(t, "", stderr.String())
}
func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) {
func TestManager_MigrateToBinaryExtension(t *testing.T) {
tempDir := t.TempDir()
io, _, _, _ := iostreams.Test()
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote")))
io, _, stdout, stderr := iostreams.Test()
reg := httpmock.Registry{}
defer reg.Verify(t)
client := http.Client{Transport: &reg}
m := newTestManager(tempDir, &client, io)
exts, err := m.list(false)
assert.NoError(t, err)
assert.Equal(t, 1, len(exts))
ext := exts[0]
ext.currentVersion = "old version"
ext.latestVersion = "new version"
rs, restoreRun := run.Stub()
defer restoreRun(t)
rs.Register(`git -C.*?gh-remote remote -v`, 0, "origin git@github.com:owner/gh-remote.git (fetch)\norigin git@github.com:owner/gh-remote.git (push)")
rs.Register(`git -C.*?gh-remote config --get-regexp \^.*`, 0, "remote.origin.gh-resolve base")
reg.Register(
httpmock.REST("GET", "repos/owner/gh-remote/releases/latest"),
httpmock.JSONResponse(
release{
Tag: "v1.0.2",
Assets: []releaseAsset{
{
Name: "gh-remote-windows-amd64.exe",
APIURL: "/release/cool",
},
},
}))
reg.Register(
httpmock.REST("GET", "repos/owner/gh-remote/releases/latest"),
httpmock.JSONResponse(
release{
Tag: "v1.0.2",
Assets: []releaseAsset{
{
Name: "gh-remote-windows-amd64.exe",
APIURL: "/release/cool",
},
},
}))
reg.Register(
httpmock.REST("GET", "release/cool"),
httpmock.StringResponse("FAKE UPGRADED BINARY"))
err = m.upgradeExtension(ext, false)
assert.NoError(t, err)
assert.Equal(t, "", stdout.String())
assert.Equal(t, "", stderr.String())
manifest, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-remote", manifestName))
assert.NoError(t, err)
var bm binManifest
err = yaml.Unmarshal(manifest, &bm)
assert.NoError(t, err)
assert.Equal(t, binManifest{
Name: "gh-remote",
Owner: "owner",
Host: "github.com",
Tag: "v1.0.2",
Path: filepath.Join(tempDir, "extensions/gh-remote/gh-remote.exe"),
}, bm)
fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-remote/gh-remote.exe"))
assert.NoError(t, err)
assert.Equal(t, "FAKE UPGRADED BINARY", string(fakeBin))
}
func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) {
tempDir := t.TempDir()
reg := httpmock.Registry{}
defer reg.Verify(t)
assert.NoError(t, stubBinaryExtension(
filepath.Join(tempDir, "extensions", "gh-bin-ext"),
binManifest{
@ -311,7 +395,9 @@ func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) {
Host: "example.com",
Tag: "v1.0.1",
}))
m := newTestManager(tempDir, &client, io)
io, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, &http.Client{Transport: &reg}, io)
reg.Register(
httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"),
httpmock.JSONResponse(
@ -319,7 +405,7 @@ func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) {
Tag: "v1.0.2",
Assets: []releaseAsset{
{
Name: "gh-bin-ext-windows-amd64",
Name: "gh-bin-ext-windows-amd64.exe",
APIURL: "https://example.com/release/cool2",
},
},
@ -348,13 +434,15 @@ func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) {
Owner: "owner",
Host: "example.com",
Tag: "v1.0.2",
Path: filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext"),
Path: filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext.exe"),
}, bm)
fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext"))
fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext.exe"))
assert.NoError(t, err)
assert.Equal(t, "FAKE UPGRADED BINARY", string(fakeBin))
assert.Equal(t, "", stdout.String())
assert.Equal(t, "", stderr.String())
}
func TestManager_Install_git(t *testing.T) {
@ -365,7 +453,6 @@ func TestManager_Install_git(t *testing.T) {
client := http.Client{Transport: &reg}
io, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, &client, io)
reg.Register(
@ -422,17 +509,16 @@ func TestManager_Install_binary_unsupported(t *testing.T) {
},
}))
io, _, _, _ := iostreams.Test()
io, _, stdout, stderr := iostreams.Test()
tempDir := t.TempDir()
m := newTestManager(tempDir, &client, io)
err := m.Install(repo)
assert.Error(t, err)
assert.EqualError(t, err, "gh-bin-ext unsupported for windows-amd64. Open an issue: `gh issue create -R owner/gh-bin-ext -t'Support windows-amd64'`")
errText := "gh-bin-ext unsupported for windows-amd64. Open an issue: `gh issue create -R owner/gh-bin-ext -t'Support windows-amd64'`"
assert.Equal(t, errText, err.Error())
assert.Equal(t, "", stdout.String())
assert.Equal(t, "", stderr.String())
}
func TestManager_Install_binary(t *testing.T) {
@ -440,7 +526,6 @@ func TestManager_Install_binary(t *testing.T) {
reg := httpmock.Registry{}
defer reg.Verify(t)
client := http.Client{Transport: &reg}
reg.Register(
httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"),
@ -448,7 +533,7 @@ func TestManager_Install_binary(t *testing.T) {
release{
Assets: []releaseAsset{
{
Name: "gh-bin-ext-windows-amd64",
Name: "gh-bin-ext-windows-amd64.exe",
APIURL: "https://example.com/release/cool",
},
},
@ -460,7 +545,7 @@ func TestManager_Install_binary(t *testing.T) {
Tag: "v1.0.1",
Assets: []releaseAsset{
{
Name: "gh-bin-ext-windows-amd64",
Name: "gh-bin-ext-windows-amd64.exe",
APIURL: "https://example.com/release/cool",
},
},
@ -469,10 +554,10 @@ func TestManager_Install_binary(t *testing.T) {
httpmock.REST("GET", "release/cool"),
httpmock.StringResponse("FAKE BINARY"))
io, _, _, _ := iostreams.Test()
io, _, stdout, stderr := iostreams.Test()
tempDir := t.TempDir()
m := newTestManager(tempDir, &client, io)
m := newTestManager(tempDir, &http.Client{Transport: &reg}, io)
err := m.Install(repo)
assert.NoError(t, err)
@ -489,33 +574,121 @@ func TestManager_Install_binary(t *testing.T) {
Owner: "owner",
Host: "example.com",
Tag: "v1.0.1",
Path: filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext"),
Path: filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext.exe"),
}, bm)
fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext"))
fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext.exe"))
assert.NoError(t, err)
assert.Equal(t, "FAKE BINARY", string(fakeBin))
assert.Equal(t, "", stdout.String())
assert.Equal(t, "", stderr.String())
}
func TestManager_Create(t *testing.T) {
tempDir := t.TempDir()
chdirTemp(t)
io, _, stdout, stderr := iostreams.Test()
m := newTestManager(".", nil, io)
err := m.Create("gh-test", extensions.GitTemplateType)
assert.NoError(t, err)
files, err := ioutil.ReadDir("gh-test")
assert.NoError(t, err)
assert.Equal(t, []string{"gh-test"}, fileNames(files))
assert.Equal(t, heredoc.Doc(`
[git init --quiet gh-test]
[git -C gh-test add gh-test --chmod=+x]
`), stdout.String())
assert.Equal(t, "", stderr.String())
}
func TestManager_Create_go_binary(t *testing.T) {
chdirTemp(t)
reg := httpmock.Registry{}
defer reg.Verify(t)
reg.Register(
httpmock.GraphQL(`query UserCurrent\b`),
httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`))
io, _, stdout, stderr := iostreams.Test()
m := newTestManager(".", &http.Client{Transport: &reg}, io)
err := m.Create("gh-test", extensions.GoBinTemplateType)
require.NoError(t, err)
files, err := ioutil.ReadDir("gh-test")
require.NoError(t, err)
assert.Equal(t, []string{".github", ".gitignore", "main.go"}, fileNames(files))
gitignore, err := os.ReadFile(filepath.Join("gh-test", ".gitignore"))
require.NoError(t, err)
assert.Equal(t, heredoc.Doc(`
/gh-test
/gh-test.exe
`), string(gitignore))
files, err = ioutil.ReadDir(filepath.Join("gh-test", ".github", "workflows"))
require.NoError(t, err)
assert.Equal(t, []string{"release.yml"}, fileNames(files))
assert.Equal(t, heredoc.Doc(`
[git init --quiet gh-test]
[go mod init github.com/jillv/gh-test]
[go mod tidy]
[go build]
[git -C gh-test add .]
`), stdout.String())
assert.Equal(t, "", stderr.String())
}
func TestManager_Create_other_binary(t *testing.T) {
chdirTemp(t)
io, _, stdout, stderr := iostreams.Test()
m := newTestManager(".", nil, io)
err := m.Create("gh-test", extensions.OtherBinTemplateType)
assert.NoError(t, err)
files, err := ioutil.ReadDir("gh-test")
assert.NoError(t, err)
assert.Equal(t, 2, len(files))
files, err = ioutil.ReadDir(filepath.Join("gh-test", ".github", "workflows"))
assert.NoError(t, err)
assert.Equal(t, []string{"release.yml"}, fileNames(files))
files, err = ioutil.ReadDir(filepath.Join("gh-test", "script"))
assert.NoError(t, err)
assert.Equal(t, []string{"build.sh"}, fileNames(files))
assert.Equal(t, heredoc.Docf(`
[git init --quiet gh-test]
[git -C gh-test add %s --chmod=+x]
[git -C gh-test add .]
`, filepath.FromSlash("script/build.sh")), stdout.String())
assert.Equal(t, "", stderr.String())
}
// chdirTemp changes the current working directory to a temporary directory for the duration of the test.
func chdirTemp(t *testing.T) {
oldWd, _ := os.Getwd()
assert.NoError(t, os.Chdir(tempDir))
t.Cleanup(func() { _ = os.Chdir(oldWd) })
m := newTestManager(tempDir, nil, nil)
err := m.Create("gh-test")
assert.NoError(t, err)
files, err := ioutil.ReadDir(filepath.Join(tempDir, "gh-test"))
assert.NoError(t, err)
assert.Equal(t, 1, len(files))
extFile := files[0]
assert.Equal(t, "gh-test", extFile.Name())
if runtime.GOOS == "windows" {
assert.Equal(t, os.FileMode(0666), extFile.Mode())
} else {
assert.Equal(t, os.FileMode(0755), extFile.Mode())
tempDir := t.TempDir()
if err := os.Chdir(tempDir); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_ = os.Chdir(oldWd)
})
}
func fileNames(files []os.FileInfo) []string {
names := make([]string, len(files))
for i, f := range files {
names[i] = f.Name()
}
sort.Strings(names)
return names
}
func stubExtension(path string) error {

View file

@ -1,3 +1,4 @@
//go:build !windows
// +build !windows
package extension

View file

@ -5,7 +5,6 @@ import (
"fmt"
"net/http"
"os"
"path/filepath"
"time"
"github.com/cli/cli/v2/api"
@ -19,17 +18,10 @@ import (
)
func New(appVersion string) *cmdutil.Factory {
var exe string
f := &cmdutil.Factory{
Config: configFunc(), // No factory dependencies
Branch: branchFunc(), // No factory dependencies
Executable: func() string {
if exe != "" {
return exe
}
exe = executable("gh")
return exe
},
Config: configFunc(), // No factory dependencies
Branch: branchFunc(), // No factory dependencies
ExecutableName: "gh",
}
f.IOStreams = ioStreams(f) // Depends on Config
@ -121,52 +113,6 @@ func browserLauncher(f *cmdutil.Factory) string {
return os.Getenv("BROWSER")
}
// Finds the location of the executable for the current process as it's found in PATH, respecting symlinks.
// If the process couldn't determine its location, return fallbackName. If the executable wasn't found in
// PATH, return the absolute location to the program.
//
// The idea is that the result of this function is callable in the future and refers to the same
// installation of gh, even across upgrades. This is needed primarily for Homebrew, which installs software
// under a location such as `/usr/local/Cellar/gh/1.13.1/bin/gh` and symlinks it from `/usr/local/bin/gh`.
// When the version is upgraded, Homebrew will often delete older versions, but keep the symlink. Because of
// this, we want to refer to the `gh` binary as `/usr/local/bin/gh` and not as its internal Homebrew
// location.
//
// None of this would be needed if we could just refer to GitHub CLI as `gh`, i.e. without using an absolute
// path. However, for some reason Homebrew does not include `/usr/local/bin` in PATH when it invokes git
// commands to update its taps. If `gh` (no path) is being used as git credential helper, as set up by `gh
// auth login`, running `brew update` will print out authentication errors as git is unable to locate
// Homebrew-installed `gh`.
func executable(fallbackName string) string {
exe, err := os.Executable()
if err != nil {
return fallbackName
}
base := filepath.Base(exe)
path := os.Getenv("PATH")
for _, dir := range filepath.SplitList(path) {
p, err := filepath.Abs(filepath.Join(dir, base))
if err != nil {
continue
}
f, err := os.Stat(p)
if err != nil {
continue
}
if p == exe {
return p
} else if f.Mode()&os.ModeSymlink != 0 {
if t, err := os.Readlink(p); err == nil && t == exe {
return p
}
}
}
return exe
}
func configFunc() func() (config.Config, error) {
var cachedConfig config.Config
var configError error
@ -220,7 +166,7 @@ func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams {
return io
}
if prompt, _ := cfg.Get("", "prompt"); prompt == "disabled" {
if prompt, _ := cfg.GetOrDefault("", "prompt"); prompt == "disabled" {
io.SetNeverPrompt(true)
}

View file

@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"os"
"regexp"
"strings"
"time"
@ -107,6 +108,7 @@ func NewHTTPClient(io *iostreams.IOStreams, cfg configGetter, appVersion string,
}
return "", nil
}),
api.ExtractHeader("X-GitHub-SSO", &ssoHeader),
)
if setAccept {
@ -126,6 +128,22 @@ func NewHTTPClient(io *iostreams.IOStreams, cfg configGetter, appVersion string,
return api.NewHTTPClient(opts...), nil
}
var ssoHeader string
var ssoURLRE = regexp.MustCompile(`\burl=([^;]+)`)
// SSOURL returns the URL of a SAML SSO challenge received by the server for clients that use ExtractHeader
// to extract the value of the "X-GitHub-SSO" response header.
func SSOURL() string {
if ssoHeader == "" {
return ""
}
m := ssoURLRE.FindStringSubmatch(ssoHeader)
if m == nil {
return ""
}
return m[1]
}
func getHost(r *http.Request) string {
if r.Host != "" {
return r.Host

View file

@ -25,8 +25,10 @@ func TestNewHTTPClient(t *testing.T) {
args args
envDebug string
host string
sso string
wantHeader map[string]string
wantStderr string
wantSSO string
}{
{
name: "github.com with Accept header",
@ -95,10 +97,10 @@ func TestNewHTTPClient(t *testing.T) {
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
> Authorization: token
> User-Agent: GitHub CLI v1.2.3
< HTTP/1.1 204 No Content
< Date: <time>
* Request took <duration>
`),
},
@ -117,11 +119,25 @@ func TestNewHTTPClient(t *testing.T) {
},
wantStderr: "",
},
{
name: "SSO challenge in response header",
args: args{
config: tinyConfig{},
appVersion: "v1.2.3",
},
host: "github.com",
sso: "required; url=https://github.com/login/sso?return_to=xyz&param=123abc; another",
wantStderr: "",
wantSSO: "https://github.com/login/sso?return_to=xyz&param=123abc",
},
}
var gotReq *http.Request
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotReq = r
if sso := r.URL.Query().Get("sso"); sso != "" {
w.Header().Set("X-GitHub-SSO", sso)
}
w.WriteHeader(http.StatusNoContent)
}))
defer ts.Close()
@ -139,6 +155,11 @@ func TestNewHTTPClient(t *testing.T) {
require.NoError(t, err)
req, err := http.NewRequest("GET", ts.URL, nil)
if tt.sso != "" {
q := req.URL.Query()
q.Set("sso", tt.sso)
req.URL.RawQuery = q.Encode()
}
req.Host = tt.host
require.NoError(t, err)
@ -151,6 +172,7 @@ func TestNewHTTPClient(t *testing.T) {
assert.Equal(t, 204, res.StatusCode)
assert.Equal(t, tt.wantStderr, normalizeVerboseLog(stderr.String()))
assert.Equal(t, tt.wantSSO, SSOURL())
})
}
}

View file

@ -79,7 +79,7 @@ func cloneRun(opts *CloneOptions) error {
if err != nil {
return err
}
protocol, err := cfg.Get(hostname, "git_protocol")
protocol, err := cfg.GetOrDefault(hostname, "git_protocol")
if err != nil {
return err
}

View file

@ -10,7 +10,6 @@ import (
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
)
@ -61,7 +60,6 @@ func Test_deleteRun(t *testing.T) {
opts *DeleteOptions
gist *shared.Gist
httpStubs func(*httpmock.Registry)
askStubs func(*prompt.AskStubber)
nontty bool
wantErr bool
wantStderr string
@ -122,12 +120,6 @@ func Test_deleteRun(t *testing.T) {
tt.httpStubs(reg)
}
as, teardown := prompt.InitAskStubber()
defer teardown()
if tt.askStubs != nil {
tt.askStubs(as)
}
if tt.opts == nil {
tt.opts = &DeleteOptions{}
}

View file

@ -34,6 +34,7 @@ type EditOptions struct {
EditFilename string
AddFilename string
SourceFile string
Description string
}
func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Command {
@ -46,7 +47,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
editorCmd,
"*."+filename,
defaultContent,
io.In, io.Out, io.ErrOut, nil)
io.In, io.Out, io.ErrOut)
},
}
@ -77,6 +78,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
}
cmd.Flags().StringVarP(&opts.AddFilename, "add", "a", "", "Add a new file to the gist")
cmd.Flags().StringVarP(&opts.Description, "desc", "d", "", "New description for the gist")
cmd.Flags().StringVarP(&opts.EditFilename, "filename", "f", "", "Select a file to edit")
return cmd
@ -127,6 +129,12 @@ func editRun(opts *EditOptions) error {
return fmt.Errorf("You do not own this gist.")
}
shouldUpdate := false
if opts.Description != "" {
shouldUpdate = true
gist.Description = opts.Description
}
if opts.AddFilename != "" {
var input io.Reader
switch src := opts.SourceFile; {
@ -275,16 +283,15 @@ func editRun(opts *EditOptions) error {
}
}
if len(filesToUpdate) == 0 {
if len(filesToUpdate) > 0 {
shouldUpdate = true
}
if !shouldUpdate {
return nil
}
err = updateGist(apiClient, host, gist)
if err != nil {
return err
}
return nil
return updateGist(apiClient, host, gist)
}
func updateGist(apiClient *api.Client, hostname string, gist *shared.Gist) error {

View file

@ -71,6 +71,14 @@ func TestNewCmdEdit(t *testing.T) {
SourceFile: "-",
},
},
{
name: "description",
cli: `123 --desc "my new description"`,
wants: EditOptions{
Selector: "123",
Description: "my new description",
},
},
}
for _, tt := range tests {
@ -153,8 +161,8 @@ func Test_editRun(t *testing.T) {
{
name: "multiple files, submit",
askStubs: func(as *prompt.AskStubber) {
as.StubOne("unix.md")
as.StubOne("Submit")
as.StubPrompt("Edit which file?").AnswerWith("unix.md")
as.StubPrompt("What next?").AnswerWith("Submit")
},
gist: &shared.Gist{
ID: "1234",
@ -198,8 +206,8 @@ func Test_editRun(t *testing.T) {
{
name: "multiple files, cancel",
askStubs: func(as *prompt.AskStubber) {
as.StubOne("unix.md")
as.StubOne("Cancel")
as.StubPrompt("Edit which file?").AnswerWith("unix.md")
as.StubPrompt("What next?").AnswerWith("Cancel")
},
wantErr: "CancelError",
gist: &shared.Gist{
@ -269,6 +277,39 @@ func Test_editRun(t *testing.T) {
AddFilename: fileToAdd,
},
},
{
name: "change description",
opts: &EditOptions{
Description: "my new description",
},
gist: &shared.Gist{
ID: "1234",
Description: "my old description",
Files: map[string]*shared.GistFile{
"sample.txt": {
Filename: "sample.txt",
Type: "text/plain",
},
},
Owner: &shared.GistOwner{Login: "octocat"},
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("POST", "gists/1234"),
httpmock.StatusStringResponse(201, "{}"))
},
wantParams: map[string]interface{}{
"description": "my new description",
"updated_at": "0001-01-01T00:00:00Z",
"public": false,
"files": map[string]interface{}{
"sample.txt": map[string]interface{}{
"content": "new file content",
"filename": "sample.txt",
"type": "text/plain",
},
},
},
},
{
name: "add file to existing gist from source parameter",
gist: &shared.Gist{
@ -421,12 +462,6 @@ func Test_editRun(t *testing.T) {
tt.httpStubs(reg)
}
as, teardown := prompt.InitAskStubber()
defer teardown()
if tt.askStubs != nil {
tt.askStubs(as)
}
if tt.opts == nil {
tt.opts = &EditOptions{}
}
@ -450,6 +485,11 @@ func Test_editRun(t *testing.T) {
}
t.Run(tt.name, func(t *testing.T) {
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)
}
err := editRun(tt.opts)
reg.Verify(t)
if tt.wantErr != "" {

Some files were not shown because too many files have changed in this diff Show more