diff -Nru hub-2.7.0~ds1/.agignore hub-2.14.2~ds1/.agignore --- hub-2.7.0~ds1/.agignore 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/.agignore 1970-01-01 00:00:00.000000000 +0000 @@ -1,5 +0,0 @@ -./tags -site/* -tmp/* -vendor/* -bundle/* diff -Nru hub-2.7.0~ds1/cmd/cmd.go hub-2.14.2~ds1/cmd/cmd.go --- hub-2.7.0~ds1/cmd/cmd.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/cmd/cmd.go 2020-03-05 17:48:23.000000000 +0000 @@ -9,8 +9,6 @@ "syscall" "github.com/github/hub/ui" - "github.com/github/hub/utils" - "github.com/kballard/go-shellquote" ) type Cmd struct { @@ -39,6 +37,15 @@ return cmd } +func (cmd *Cmd) Output() (string, error) { + verboseLog(cmd) + c := exec.Command(cmd.Name, cmd.Args...) + c.Stderr = cmd.Stderr + output, err := c.Output() + + return string(output), err +} + func (cmd *Cmd) CombinedOutput() (string, error) { verboseLog(cmd) output, err := exec.Command(cmd.Name, cmd.Args...).CombinedOutput() @@ -55,13 +62,35 @@ // Run runs command with `Exec` on platforms except Windows // which only supports `Spawn` func (cmd *Cmd) Run() error { - if runtime.GOOS == "windows" { + if isWindows() { return cmd.Spawn() } else { return cmd.Exec() } } +func isWindows() bool { + return runtime.GOOS == "windows" || detectWSL() +} + +var detectedWSL bool +var detectedWSLContents string + +// https://github.com/Microsoft/WSL/issues/423#issuecomment-221627364 +func detectWSL() bool { + if !detectedWSL { + b := make([]byte, 1024) + f, err := os.Open("/proc/version") + if err == nil { + f.Read(b) + f.Close() + detectedWSLContents = string(b) + } + detectedWSL = true + } + return strings.Contains(detectedWSLContents, "Microsoft") +} + // Spawn runs command with spawn(3) func (cmd *Cmd) Spawn() error { verboseLog(cmd) @@ -92,14 +121,14 @@ return syscall.Exec(binary, args, os.Environ()) } -func New(cmd string) *Cmd { - cmds, err := shellquote.Split(cmd) - utils.Check(err) - - name := cmds[0] - args := cmds[1:] - - return &Cmd{Name: name, Args: args, Stdin: os.Stdin, Stdout: os.Stdout, Stderr: os.Stderr} +func New(name string) *Cmd { + return &Cmd{ + Name: name, + Args: []string{}, + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stderr, + } } func NewWithArray(cmd []string) *Cmd { diff -Nru hub-2.7.0~ds1/cmd/cmd_test.go hub-2.14.2~ds1/cmd/cmd_test.go --- hub-2.7.0~ds1/cmd/cmd_test.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/cmd/cmd_test.go 2020-03-05 17:48:23.000000000 +0000 @@ -8,9 +8,8 @@ func TestNew(t *testing.T) { execCmd := New("vim --noplugin") - assert.Equal(t, "vim", execCmd.Name) - assert.Equal(t, 1, len(execCmd.Args)) - assert.Equal(t, "--noplugin", execCmd.Args[0]) + assert.Equal(t, "vim --noplugin", execCmd.Name) + assert.Equal(t, 0, len(execCmd.Args)) } func TestWithArg(t *testing.T) { diff -Nru hub-2.7.0~ds1/commands/alias.go hub-2.14.2~ds1/commands/alias.go --- hub-2.7.0~ds1/commands/alias.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/alias.go 2020-03-05 17:48:23.000000000 +0000 @@ -18,7 +18,7 @@ ## Options -s - Output shell script suitable for 'eval'. + Output shell script suitable for ''eval''. Specify the type of shell (default: "$SHELL" environment variable). @@ -29,10 +29,7 @@ `, } -var flagAliasScript bool - func init() { - cmdAlias.Flag.BoolVarP(&flagAliasScript, "script", "s", false, "SCRIPT") CmdRunner.Use(cmdAlias) } @@ -44,6 +41,7 @@ shell = os.Getenv("SHELL") } + flagAliasScript := args.Flag.Bool("-s") if shell == "" { cmd := "hub alias " if flagAliasScript { diff -Nru hub-2.7.0~ds1/commands/api.go hub-2.14.2~ds1/commands/api.go --- hub-2.7.0~ds1/commands/api.go 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/commands/api.go 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,370 @@ +package commands + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "regexp" + "strconv" + "strings" + "time" + + "github.com/github/hub/github" + "github.com/github/hub/ui" + "github.com/github/hub/utils" +) + +var cmdApi = &Command{ + Run: apiCommand, + Usage: "api [-it] [-X ] [-H
] [--cache ] [-F |--input ]", + Long: `Low-level GitHub API request interface. + +## Options: + -X, --method + The HTTP method to use for the request (default: "GET"). The method is + automatically set to "POST" if ''--field'', ''--raw-field'', or ''--input'' + are used. + + Use ''-XGET'' to force serializing fields into the query string for the GET + request instead of JSON body of the POST request. + + -F, --field = + Data to serialize with the request. has some magic handling; use + ''--raw-field'' for sending arbitrary string values. + + If starts with "@", the rest of the value is interpreted as a + filename to read the value from. Use "@-" to read from standard input. + + If is "true", "false", "null", or looks like a number, an + appropriate JSON type is used instead of a string. + + It is not possible to serialize as a nested JSON array or hash. + Instead, construct the request payload externally and pass it via + ''--input''. + + Unless ''-XGET'' was used, all fields are sent serialized as JSON within + the request body. When is "graphql", all fields other than + "query" are grouped under "variables". See + + + -f, --raw-field = + Same as ''--field'', except that it allows values starting with "@", literal + strings "true", "false", and "null", as well as strings that look like + numbers. + + --input + The filename to read the raw request body from. Use "-" to read from standard + input. Use this when you want to manually construct the request payload. + + -H, --header : + Set an HTTP request header. + + -i, --include + Include HTTP response headers in the output. + + -t, --flat + Parse response JSON and output the data in a line-based key-value format + suitable for use in shell scripts. + + --paginate + Automatically request and output the next page of results until all + resources have been listed. For GET requests, this follows the '''' + resource as indicated in the "Link" response header. For GraphQL queries, + this utilizes ''pageInfo'' that must be present in the query; see EXAMPLES. + + Note that multiple JSON documents will be output as a result. If the API + rate limit has been reached, the final document that is output will be the + HTTP 403 notice, and the process will exit with a non-zero status. One way + this can be avoided is by enabling ''--obey-ratelimit''. + + --color[=] + Enable colored output even if stdout is not a terminal. can be one + of "always" (default for ''--color''), "never", or "auto" (default). + + --cache + Cache valid responses to GET requests for seconds. + + When using "graphql" as , caching will apply to responses to POST + requests as well. Just make sure to not use ''--cache'' for any GraphQL + mutations. + + --obey-ratelimit + After exceeding the API rate limit, pause the process until the reset time + of the current rate limit window and retry the request. Note that this may + cause the process to hang for a long time (maximum of 1 hour). + + + The GitHub API endpoint to send the HTTP request to (default: "/"). + + To learn about available endpoints, see . + To make GraphQL queries, use "graphql" as and pass ''-F query=QUERY''. + + If the literal strings "{owner}" or "{repo}" appear in or in the + GraphQL "query" field, fill in those placeholders with values read from the + git remote configuration of the current git repository. + +## Examples: + + # fetch information about the currently authenticated user as JSON + $ hub api user + + # list user repositories as line-based output + $ hub api --flat users/octocat/repos + + # post a comment to issue #23 of the current repository + $ hub api repos/{owner}/{repo}/issues/23/comments --raw-field 'body=Nice job!' + + # perform a GraphQL query read from a file + $ hub api graphql -F query=@path/to/myquery.graphql + + # perform pagination with GraphQL + $ hub api --paginate graphql -f query=' + query($endCursor: String) { + repositoryOwner(login: "USER") { + repositories(first: 100, after: $endCursor) { + nodes { + nameWithOwner + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + ' + +## See also: + +hub(1) +`, +} + +func init() { + CmdRunner.Use(cmdApi) +} + +func apiCommand(_ *Command, args *Args) { + path := "" + if !args.IsParamsEmpty() { + path = args.GetParam(0) + } + + method := "GET" + if args.Flag.HasReceived("--method") { + method = args.Flag.Value("--method") + } else if args.Flag.HasReceived("--field") || args.Flag.HasReceived("--raw-field") || args.Flag.HasReceived("--input") { + method = "POST" + } + cacheTTL := args.Flag.Int("--cache") + + params := make(map[string]interface{}) + for _, val := range args.Flag.AllValues("--field") { + parts := strings.SplitN(val, "=", 2) + if len(parts) >= 2 { + params[parts[0]] = magicValue(parts[1]) + } + } + for _, val := range args.Flag.AllValues("--raw-field") { + parts := strings.SplitN(val, "=", 2) + if len(parts) >= 2 { + params[parts[0]] = parts[1] + } + } + + headers := make(map[string]string) + for _, val := range args.Flag.AllValues("--header") { + parts := strings.SplitN(val, ":", 2) + if len(parts) >= 2 { + headers[parts[0]] = strings.TrimLeft(parts[1], " ") + } + } + + host := "" + owner := "" + repo := "" + localRepo, localRepoErr := github.LocalRepo() + if localRepoErr == nil { + var project *github.Project + if project, localRepoErr = localRepo.MainProject(); localRepoErr == nil { + host = project.Host + owner = project.Owner + repo = project.Name + } + } + if host == "" { + defHost, err := github.CurrentConfig().DefaultHostNoPrompt() + utils.Check(err) + host = defHost.Host + } + + isGraphQL := path == "graphql" + if isGraphQL && params["query"] != nil { + query := params["query"].(string) + query = strings.Replace(query, "{owner}", owner, -1) + query = strings.Replace(query, "{repo}", repo, -1) + + variables := make(map[string]interface{}) + for key, value := range params { + if key != "query" { + variables[key] = value + } + } + if len(variables) > 0 { + params = make(map[string]interface{}) + params["variables"] = variables + } + + params["query"] = query + } else { + path = strings.Replace(path, "{owner}", owner, -1) + path = strings.Replace(path, "{repo}", repo, -1) + } + + var body interface{} + if args.Flag.HasReceived("--input") { + fn := args.Flag.Value("--input") + if fn == "-" { + body = os.Stdin + } else { + fi, err := os.Open(fn) + utils.Check(err) + body = fi + defer fi.Close() + } + } else { + body = params + } + + gh := github.NewClient(host) + + out := ui.Stdout + colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color")) + parseJSON := args.Flag.Bool("--flat") + includeHeaders := args.Flag.Bool("--include") + paginate := args.Flag.Bool("--paginate") + rateLimitWait := args.Flag.Bool("--obey-ratelimit") + + args.NoForward() + + for { + response, err := gh.GenericAPIRequest(method, path, body, headers, cacheTTL) + utils.Check(err) + + if rateLimitWait && response.StatusCode == 403 && response.RateLimitRemaining() == 0 { + pauseUntil(response.RateLimitReset()) + continue + } + + success := response.StatusCode < 300 + jsonType := true + if !success { + jsonType, _ = regexp.MatchString(`[/+]json(?:;|$)`, response.Header.Get("Content-Type")) + } + + if includeHeaders { + fmt.Fprintf(out, "%s %s\r\n", response.Proto, response.Status) + response.Header.Write(out) + fmt.Fprintf(out, "\r\n") + } + + endCursor := "" + hasNextPage := false + + if parseJSON && jsonType { + hasNextPage, endCursor = utils.JSONPath(out, response.Body, colorize) + } else if paginate && isGraphQL { + bodyCopy := &bytes.Buffer{} + io.Copy(out, io.TeeReader(response.Body, bodyCopy)) + hasNextPage, endCursor = utils.JSONPath(ioutil.Discard, bodyCopy, false) + } else { + io.Copy(out, response.Body) + } + response.Body.Close() + + if !success { + if ssoErr := github.ValidateGitHubSSO(response.Response); ssoErr != nil { + ui.Errorln() + ui.Errorln(ssoErr) + } + if scopeErr := github.ValidateSufficientOAuthScopes(response.Response); scopeErr != nil { + ui.Errorln() + ui.Errorln(scopeErr) + } + os.Exit(22) + } + + if paginate { + if isGraphQL && hasNextPage && endCursor != "" { + if v, ok := params["variables"]; ok { + variables := v.(map[string]interface{}) + variables["endCursor"] = endCursor + } else { + variables := map[string]interface{}{"endCursor": endCursor} + params["variables"] = variables + } + goto next + } else if nextLink := response.Link("next"); nextLink != "" { + path = nextLink + goto next + } + } + + break + next: + if !parseJSON { + fmt.Fprintf(out, "\n") + } + + if rateLimitWait && response.RateLimitRemaining() == 0 { + pauseUntil(response.RateLimitReset()) + } + } +} + +func pauseUntil(timestamp int) { + rollover := time.Unix(int64(timestamp)+1, 0) + duration := time.Until(rollover) + if duration > 0 { + ui.Errorf("API rate limit exceeded; pausing until %v ...\n", rollover) + time.Sleep(duration) + } +} + +const ( + trueVal = "true" + falseVal = "false" + nilVal = "null" +) + +func magicValue(value string) interface{} { + switch value { + case trueVal: + return true + case falseVal: + return false + case nilVal: + return nil + default: + if strings.HasPrefix(value, "@") { + return string(readFile(value[1:])) + } else if i, err := strconv.Atoi(value); err == nil { + return i + } else { + return value + } + } +} + +func readFile(file string) (content []byte) { + var err error + if file == "-" { + content, err = ioutil.ReadAll(os.Stdin) + } else { + content, err = ioutil.ReadFile(file) + } + utils.Check(err) + return +} diff -Nru hub-2.7.0~ds1/commands/apply.go hub-2.14.2~ds1/commands/apply.go --- hub-2.7.0~ds1/commands/apply.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/apply.go 2020-03-05 17:48:23.000000000 +0000 @@ -3,6 +3,7 @@ import ( "io" "io/ioutil" + "os" "regexp" "github.com/github/hub/github" @@ -69,7 +70,7 @@ gistRegexp := regexp.MustCompile("^https?://gist\\.github\\.com/([\\w.-]+/)?([a-f0-9]+)") commitRegexp := regexp.MustCompile("^(commit|pull/[0-9]+/commits)/([0-9a-f]+)") pullRegexp := regexp.MustCompile("^pull/([0-9]+)") - for _, arg := range args.Params { + for idx, arg := range args.Params { var ( patch io.ReadCloser apiError error @@ -96,8 +97,10 @@ continue } - idx := args.IndexOfParam(arg) - patchFile, err := ioutil.TempFile("", "hub") + tempDir := os.TempDir() + err = os.MkdirAll(tempDir, 0775) + utils.Check(err) + patchFile, err := ioutil.TempFile(tempDir, "hub") utils.Check(err) _, err = io.Copy(patchFile, patch) @@ -106,6 +109,6 @@ patchFile.Close() patch.Close() - args.Params[idx] = patchFile.Name() + args.ReplaceParam(idx, patchFile.Name()) } } diff -Nru hub-2.7.0~ds1/commands/args.go hub-2.14.2~ds1/commands/args.go --- hub-2.7.0~ds1/commands/args.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/args.go 2020-03-05 17:48:23.000000000 +0000 @@ -5,6 +5,7 @@ "strings" "github.com/github/hub/cmd" + "github.com/github/hub/utils" ) type Args struct { @@ -19,6 +20,7 @@ Terminator bool noForward bool Callbacks []func() error + Flag *utils.ArgsParser } func (a *Args) Words() []string { @@ -57,19 +59,33 @@ } func (a *Args) Commands() []*cmd.Cmd { - result := a.beforeChain + result := []*cmd.Cmd{} + appendFromChain := func(c *cmd.Cmd) { + if c.Name == "git" { + ga := []string{c.Name} + ga = append(ga, a.GlobalFlags...) + ga = append(ga, c.Args...) + result = append(result, cmd.NewWithArray(ga)) + } else { + result = append(result, c) + } + } + for _, c := range a.beforeChain { + appendFromChain(c) + } if !a.noForward { result = append(result, a.ToCmd()) } + for _, c := range a.afterChain { + appendFromChain(c) + } - result = append(result, a.afterChain...) return result } func (a *Args) ToCmd() *cmd.Cmd { - c := cmd.New(a.Executable) - c.WithArgs(a.GlobalFlags...) + c := cmd.NewWithArray(append([]string{a.Executable}, a.GlobalFlags...)) if a.Command != "" { c.WithArg(a.Command) @@ -126,9 +142,8 @@ } func (a *Args) RemoveParam(i int) string { - newParams, item := removeItem(a.Params, i) - a.Params = newParams - + item := a.Params[i] + a.Params = append(a.Params[:i], a.Params[i+1:]...) return item } @@ -166,30 +181,26 @@ a.Params = append(a.Params, params...) } -func (a *Args) HasFlags(flags ...string) bool { - for _, f := range flags { - if i := a.IndexOfParam(f); i != -1 { - return true - } - } - - return false -} - func NewArgs(args []string) *Args { var ( - command string - params []string - noop bool - globalFlags []string + command string + params []string + noop bool ) - slurpGlobalFlags(&args, &globalFlags) - noop = removeValue(&globalFlags, noopFlag) + cmdIdx := findCommandIndex(args) + globalFlags := args[:cmdIdx] + if cmdIdx > 0 { + args = args[cmdIdx:] + for i := len(globalFlags) - 1; i >= 0; i-- { + if globalFlags[i] == noopFlag { + noop = true + globalFlags = append(globalFlags[:i], globalFlags[i+1:]...) + } + } + } - if len(args) == 0 { - params = []string{} - } else { + if len(args) != 0 { command = args[0] params = args[1:] } @@ -219,11 +230,11 @@ return strings.HasPrefix(value, flagPrefix) } -func slurpGlobalFlags(args *[]string, globalFlags *[]string) { +func findCommandIndex(args []string) int { slurpNextValue := false commandIndex := 0 - for i, arg := range *args { + for i, arg := range args { if slurpNextValue { commandIndex = i + 1 slurpNextValue = false @@ -236,33 +247,5 @@ } } } - - if commandIndex > 0 { - aa := *args - *globalFlags = aa[0:commandIndex] - *args = aa[commandIndex:] - } -} - -func removeItem(slice []string, index int) (newSlice []string, item string) { - if index < 0 || index > len(slice)-1 { - panic(fmt.Sprintf("Index %d is out of bound", index)) - } - - item = slice[index] - newSlice = append(slice[:index], slice[index+1:]...) - - return newSlice, item -} - -func removeValue(slice *[]string, value string) (found bool) { - aa := *slice - for i := len(aa) - 1; i >= 0; i-- { - arg := aa[i] - if arg == value { - found = true - *slice, _ = removeItem(*slice, i) - } - } - return found + return commandIndex } diff -Nru hub-2.7.0~ds1/commands/args_test.go hub-2.14.2~ds1/commands/args_test.go --- hub-2.7.0~ds1/commands/args_test.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/args_test.go 2020-03-05 17:48:23.000000000 +0000 @@ -119,3 +119,16 @@ assert.Equal(t, "open", cmd.Name) assert.Equal(t, []string{"-a", "http://example.com"}, cmd.Args) } + +func TestArgs_GlobalFlags_BeforeAfterChain(t *testing.T) { + args := NewArgs([]string{"-c", "key=value", "-C", "dir", "status"}) + args.Before("git", "remote", "add") + args.After("git", "clean") + args.After("echo", "done!") + cmds := args.Commands() + assert.Equal(t, 4, len(cmds)) + assert.Equal(t, "git -c key=value -C dir remote add", cmds[0].String()) + assert.Equal(t, "git -c key=value -C dir status", cmds[1].String()) + assert.Equal(t, "git -c key=value -C dir clean", cmds[2].String()) + assert.Equal(t, "echo done!", cmds[3].String()) +} diff -Nru hub-2.7.0~ds1/commands/browse.go hub-2.14.2~ds1/commands/browse.go --- hub-2.7.0~ds1/commands/browse.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/browse.go 2020-03-05 17:48:23.000000000 +0000 @@ -46,15 +46,7 @@ `, } -var ( - flagBrowseURLPrint, - flagBrowseURLCopy bool -) - func init() { - cmdBrowse.Flag.BoolVarP(&flagBrowseURLPrint, "url", "u", false, "URL") - cmdBrowse.Flag.BoolVarP(&flagBrowseURLCopy, "copy", "c", false, "COPY") - CmdRunner.Use(cmdBrowse) } @@ -113,8 +105,7 @@ } if project == nil { - err := fmt.Errorf(command.Synopsis()) - utils.Check(err) + utils.Check(command.UsageError("")) } if subpage == "commits" { @@ -130,6 +121,8 @@ pageUrl := project.WebURL("", "", path) args.NoForward() + flagBrowseURLPrint := args.Flag.Bool("--url") + flagBrowseURLCopy := args.Flag.Bool("--copy") printBrowseOrCopy(args, pageUrl, !flagBrowseURLPrint && !flagBrowseURLCopy, flagBrowseURLCopy) } diff -Nru hub-2.7.0~ds1/commands/checkout.go hub-2.14.2~ds1/commands/checkout.go --- hub-2.7.0~ds1/commands/checkout.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/checkout.go 2020-03-05 17:48:23.000000000 +0000 @@ -101,7 +101,9 @@ newArgs = append(newArgs, newBranchName) args.After("git", "merge", "--ff-only", fmt.Sprintf("refs/remotes/%s", remoteBranch)) } else { - newArgs = append(newArgs, "-b", newBranchName, "--track", remoteBranch) + newArgs = append(newArgs, "-b", newBranchName, "--no-track", remoteBranch) + args.After("git", "config", fmt.Sprintf("branch.%s.remote", newBranchName), headRemote.Name) + args.After("git", "config", fmt.Sprintf("branch.%s.merge", newBranchName), "refs/heads/"+pullRequest.Head.Ref) } args.Before("git", "fetch", headRemote.Name, refSpec) } else { @@ -113,8 +115,16 @@ } newArgs = append(newArgs, newBranchName) + b, errB := repo.CurrentBranch() + isCurrentBranch := errB == nil && b.ShortName() == newBranchName + ref := fmt.Sprintf("refs/pull/%d/head", pullRequest.Number) - args.Before("git", "fetch", baseRemote.Name, fmt.Sprintf("%s:%s", ref, newBranchName)) + if isCurrentBranch { + args.Before("git", "fetch", baseRemote.Name, ref) + args.After("git", "merge", "--ff-only", "FETCH_HEAD") + } else { + args.Before("git", "fetch", baseRemote.Name, fmt.Sprintf("%s:%s", ref, newBranchName)) + } remote := baseRemote.Name mergeRef := ref @@ -128,8 +138,11 @@ remote = project.GitURL("", "", true) mergeRef = fmt.Sprintf("refs/heads/%s", pullRequest.Head.Ref) } - args.Before("git", "config", fmt.Sprintf("branch.%s.remote", newBranchName), remote) - args.Before("git", "config", fmt.Sprintf("branch.%s.merge", newBranchName), mergeRef) + + if mc, err := git.Config(fmt.Sprintf("branch.%s.merge", newBranchName)); err != nil || mc == "" { + args.After("git", "config", fmt.Sprintf("branch.%s.remote", newBranchName), remote) + args.After("git", "config", fmt.Sprintf("branch.%s.merge", newBranchName), mergeRef) + } } return } diff -Nru hub-2.7.0~ds1/commands/ci_status.go hub-2.14.2~ds1/commands/ci_status.go --- hub-2.7.0~ds1/commands/ci_status.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/ci_status.go 2020-03-05 17:48:23.000000000 +0000 @@ -3,6 +3,7 @@ import ( "fmt" "os" + "sort" "github.com/github/hub/git" "github.com/github/hub/github" @@ -19,6 +20,23 @@ -v, --verbose Print detailed report of all status checks and their URLs. + -f, --format + Pretty print all status checks using (implies ''--verbose''). See the + "PRETTY FORMATS" section of git-log(1) for some additional details on how + placeholders are used in format. The available placeholders for checks are: + + %U: the URL of this status check + + %S: check state (e.g. "success", "failure") + + %sC: set color to red, green, or yellow, depending on state + + %t: name of the status check + + --color[=] + Enable colored output even if stdout is not a terminal. can be one + of "always" (default for ''--color''), "never", or "auto" (default). + A commit SHA or branch name (default: "HEAD"). @@ -34,12 +52,9 @@ `, } -var flagCiStatusVerbose bool var severityList []string func init() { - cmdCiStatus.Flag.BoolVarP(&flagCiStatusVerbose, "verbose", "v", false, "VERBOSE") - CmdRunner.Use(cmdCiStatus) severityList = []string{ @@ -109,8 +124,10 @@ exitCode = 3 } - if flagCiStatusVerbose && len(response.Statuses) > 0 { - verboseFormat(response.Statuses) + verbose := args.Flag.Bool("--verbose") || args.Flag.HasReceived("--format") + if verbose && len(response.Statuses) > 0 { + colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color")) + ciVerboseFormat(response.Statuses, args.Flag.Value("--format"), colorize) } else { if state != "" { ui.Println(state) @@ -123,9 +140,7 @@ } } -func verboseFormat(statuses []github.CIStatus) { - colorize := ui.IsTerminal(os.Stdout) - +func ciVerboseFormat(statuses []github.CIStatus, formatString string, colorize bool) { contextWidth := 0 for _, status := range statuses { if len(status.Context) > contextWidth { @@ -133,6 +148,10 @@ } } + sort.SliceStable(statuses, func(a, b int) bool { + return stateRank(statuses[a].State) < stateRank(statuses[b].State) + }) + for _, status := range statuses { var color int var stateMarker string @@ -151,14 +170,36 @@ color = 33 } + placeholders := map[string]string{ + "S": status.State, + "sC": "", + "t": status.Context, + "U": status.TargetUrl, + } + if colorize { - stateMarker = fmt.Sprintf("\033[%dm%s\033[0m", color, stateMarker) + placeholders["sC"] = fmt.Sprintf("\033[%dm", color) } - if status.TargetUrl == "" { - ui.Printf("%s\t%s\n", stateMarker, status.Context) - } else { - ui.Printf("%s\t%-*s\t%s\n", stateMarker, contextWidth, status.Context, status.TargetUrl) + format := formatString + if format == "" { + if status.TargetUrl == "" { + format = fmt.Sprintf("%%sC%s%%Creset\t%%t\n", stateMarker) + } else { + format = fmt.Sprintf("%%sC%s%%Creset\t%%<(%d)%%t\t%%U\n", stateMarker, contextWidth) + } } + ui.Print(ui.Expand(format, placeholders, colorize)) + } +} + +func stateRank(state string) uint32 { + switch state { + case "failure", "error", "action_required", "cancelled", "timed_out": + return 1 + case "success", "neutral": + return 3 + default: + return 2 } } diff -Nru hub-2.7.0~ds1/commands/clone.go hub-2.14.2~ds1/commands/clone.go --- hub-2.7.0~ds1/commands/clone.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/clone.go 2020-03-05 17:48:23.000000000 +0000 @@ -27,7 +27,7 @@ ## Protocol used for cloning -The 'git:' protocol will be used for cloning public repositories, while the SSH +The ''git:'' protocol will be used for cloning public repositories, while the SSH protocol will be used for private repositories and those that you have push access to. Alternatively, hub can be configured to use HTTPS protocol for everything. See "HTTPS instead of git protocol" and "HUB_PROTOCOL" of hub(1). @@ -54,72 +54,35 @@ func transformCloneArgs(args *Args) { isSSH := parseClonePrivateFlag(args) - hasValueRegexp := regexp.MustCompile("^(--(upload-pack|template|depth|origin|branch|reference|name)|-[ubo])$") - nameWithOwnerRegexp := regexp.MustCompile(NameWithOwnerRe) - for i := 0; i < args.ParamsSize(); i++ { - a := args.Params[i] - if strings.HasPrefix(a, "-") { - if hasValueRegexp.MatchString(a) { - i++ - } - } else { - if nameWithOwnerRegexp.MatchString(a) && !isCloneable(a) { - name, owner := parseCloneNameAndOwner(a) - var host *github.Host - if owner == "" { - config := github.CurrentConfig() - h, err := config.DefaultHost() - if err != nil { - utils.Check(github.FormatError("cloning repository", err)) - } - - host = h - owner = host.User - } - - var hostStr string - if host != nil { - hostStr = host.Host - } - - expectWiki := strings.HasSuffix(name, ".wiki") - if expectWiki { - name = strings.TrimSuffix(name, ".wiki") - } - - project := github.NewProject(owner, name, hostStr) - gh := github.NewClient(project.Host) - repo, err := gh.Repository(project) - if err != nil { - if strings.Contains(err.Error(), "HTTP 404") { - err = fmt.Errorf("Error: repository %s/%s doesn't exist", project.Owner, project.Name) - } - utils.Check(err) - } - - owner = repo.Owner.Login - name = repo.Name - if expectWiki { - if !repo.HasWiki { - utils.Check(fmt.Errorf("Error: %s/%s doesn't have a wiki", owner, name)) - } else { - name = name + ".wiki" - } - } - - if !isSSH && - args.Command != "submodule" && - !github.IsHttpsProtocol() { - isSSH = repo.Private || repo.Permissions.Push - } - - url := project.GitURL(name, owner, isSSH) - args.ReplaceParam(i, url) - } + // git help clone | grep -e '^ \+-.\+<' + p := utils.NewArgsParser() + p.RegisterValue("--branch", "-b") + p.RegisterValue("--depth") + p.RegisterValue("--reference") + if args.Command == "submodule" { + p.RegisterValue("--name") + } else { + p.RegisterValue("--config", "-c") + p.RegisterValue("--jobs", "-j") + p.RegisterValue("--origin", "-o") + p.RegisterValue("--reference-if-able") + p.RegisterValue("--separate-git-dir") + p.RegisterValue("--shallow-exclude") + p.RegisterValue("--shallow-since") + p.RegisterValue("--template") + p.RegisterValue("--upload-pack", "-u") + } + p.Parse(args.Params) - break + nameWithOwnerRegexp := regexp.MustCompile(NameWithOwnerRe) + for _, i := range p.PositionalIndices { + a := args.Params[i] + if nameWithOwnerRegexp.MatchString(a) && !isCloneable(a) { + url := getCloneUrl(a, isSSH, args.Command != "submodule") + args.ReplaceParam(i, url) } + break } } @@ -132,13 +95,62 @@ return false } -func parseCloneNameAndOwner(arg string) (name, owner string) { - name, owner = arg, "" - if strings.Contains(arg, "/") { - split := strings.SplitN(arg, "/", 2) - name = split[1] +func getCloneUrl(nameWithOwner string, isSSH, allowSSH bool) string { + name := nameWithOwner + owner := "" + if strings.Contains(name, "/") { + split := strings.SplitN(name, "/", 2) owner = split[0] + name = split[1] + } + + var host *github.Host + if owner == "" { + config := github.CurrentConfig() + h, err := config.DefaultHost() + if err != nil { + utils.Check(github.FormatError("cloning repository", err)) + } + + host = h + owner = host.User + } + + var hostStr string + if host != nil { + hostStr = host.Host + } + + expectWiki := strings.HasSuffix(name, ".wiki") + if expectWiki { + name = strings.TrimSuffix(name, ".wiki") + } + + project := github.NewProject(owner, name, hostStr) + gh := github.NewClient(project.Host) + repo, err := gh.Repository(project) + if err != nil { + if strings.Contains(err.Error(), "HTTP 404") { + err = fmt.Errorf("Error: repository %s/%s doesn't exist", project.Owner, project.Name) + } + utils.Check(err) + } + + owner = repo.Owner.Login + name = repo.Name + if expectWiki { + if !repo.HasWiki { + utils.Check(fmt.Errorf("Error: %s/%s doesn't have a wiki", owner, name)) + } else { + name = name + ".wiki" + } + } + + if !isSSH && + allowSSH && + !github.IsHttpsProtocol() { + isSSH = repo.Private || repo.Permissions.Push } - return + return project.GitURL(name, owner, isSSH) } diff -Nru hub-2.7.0~ds1/commands/commands.go hub-2.14.2~ds1/commands/commands.go --- hub-2.7.0~ds1/commands/commands.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/commands.go 2020-03-05 17:48:23.000000000 +0000 @@ -2,36 +2,36 @@ import ( "fmt" + "regexp" "strings" - "github.com/github/hub/ui" - flag "github.com/ogier/pflag" + "github.com/github/hub/utils" ) var ( - NameRe = "[\\w.][\\w.-]*" + NameRe = `[\w.-]+` OwnerRe = "[a-zA-Z0-9][a-zA-Z0-9-]*" - NameWithOwnerRe = fmt.Sprintf("^(?:%s|%s\\/%s)$", NameRe, OwnerRe, NameRe) + NameWithOwnerRe = fmt.Sprintf(`^(%s/)?%s$`, OwnerRe, NameRe) CmdRunner = NewRunner() ) type Command struct { - Run func(cmd *Command, args *Args) - Flag flag.FlagSet + Run func(cmd *Command, args *Args) Key string Usage string Long string + KnownFlags string GitExtension bool - subCommands map[string]*Command + subCommands map[string]*Command + parentCommand *Command } func (c *Command) Call(args *Args) (err error) { runCommand, err := c.lookupSubCommand(args) if err != nil { - ui.Errorln(err) return } @@ -47,41 +47,30 @@ return } -func (c *Command) parseArguments(args *Args) (err error) { - c.Flag.SetInterspersed(true) - c.Flag.Init(c.Name(), flag.ContinueOnError) - c.Flag.Usage = func() { - ui.Errorln("") - ui.Errorln(c.Synopsis()) - } - if err = c.Flag.Parse(args.Params); err == nil { - for _, arg := range args.Params { - if arg == "--" { - args.Terminator = true - } - } - args.Params = c.Flag.Args() - } - - return +type ErrHelp struct { + err string } -func (c *Command) FlagPassed(name string) bool { - found := false - c.Flag.Visit(func(f *flag.Flag) { - if f.Name == name { - found = true - } - }) - return found +func (e ErrHelp) Error() string { + return e.err } -func (c *Command) Arg(idx int) string { - args := c.Flag.Args() - if idx < len(args) { - return args[idx] +func (c *Command) parseArguments(args *Args) error { + knownFlags := c.KnownFlags + if knownFlags == "" { + knownFlags = c.Long + } + args.Flag = utils.NewArgsParserWithUsage("-h, --help\n" + knownFlags) + + if rest, err := args.Flag.Parse(args.Params); err == nil { + if args.Flag.Bool("--help") { + return &ErrHelp{err: c.Synopsis()} + } + args.Params = rest + args.Terminator = args.Flag.HasTerminated + return nil } else { - return "" + return fmt.Errorf("%s\n%s", err, c.Synopsis()) } } @@ -90,13 +79,26 @@ c.subCommands = make(map[string]*Command) } c.subCommands[subCommand.Name()] = subCommand + subCommand.parentCommand = c +} + +func (c *Command) UsageError(msg string) error { + nl := "" + if msg != "" { + nl = "\n" + } + return fmt.Errorf("%s%s%s", msg, nl, c.Synopsis()) } func (c *Command) Synopsis() string { lines := []string{} usagePrefix := "Usage:" + usageStr := c.Usage + if usageStr == "" && c.parentCommand != nil { + usageStr = c.parentCommand.Usage + } - for _, line := range strings.Split(c.Usage, "\n") { + for _, line := range strings.Split(usageStr, "\n") { if line != "" { usage := fmt.Sprintf("%s hub %s", usagePrefix, line) usagePrefix = " " @@ -107,7 +109,28 @@ } func (c *Command) HelpText() string { - return fmt.Sprintf("%s\n\n%s", c.Synopsis(), strings.Replace(c.Long, "'", "`", -1)) + usage := strings.Replace(c.Usage, "-^", "`-^`", 1) + usageRe := regexp.MustCompile(`(?m)^([a-z-]+)(.*)$`) + usage = usageRe.ReplaceAllString(usage, "`hub $1`$2 ") + usage = strings.TrimSpace(usage) + + var desc string + long := strings.TrimSpace(c.Long) + if lines := strings.Split(long, "\n"); len(lines) > 1 { + desc = lines[0] + long = strings.Join(lines[1:], "\n") + } + + long = strings.Replace(long, "''", "`", -1) + headingRe := regexp.MustCompile(`(?m)^(## .+):$`) + long = headingRe.ReplaceAllString(long, "$1") + + indentRe := regexp.MustCompile(`(?m)^\t`) + long = indentRe.ReplaceAllLiteralString(long, "") + definitionListRe := regexp.MustCompile(`(?m)^(\* )?([^#\s][^\n]*?):?\n\t`) + long = definitionListRe.ReplaceAllString(long, "$2\n:\t") + + return fmt.Sprintf("hub-%s(1) -- %s\n===\n\n## Synopsis\n\n%s\n%s", c.Name(), desc, usage, long) } func (c *Command) Name() string { diff -Nru hub-2.7.0~ds1/commands/commands_test.go hub-2.14.2~ds1/commands/commands_test.go --- hub-2.7.0~ds1/commands/commands_test.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/commands_test.go 2020-03-05 17:48:23.000000000 +0000 @@ -3,6 +3,7 @@ import ( "io/ioutil" "os" + "regexp" "testing" "github.com/bmizerany/assert" @@ -73,16 +74,12 @@ } func TestFlagsAfterArguments(t *testing.T) { - c := &Command{Usage: "foo -m MESSAGE ARG1"} - - var flag string - c.Flag.StringVarP(&flag, "message", "m", "", "MESSAGE") + c := &Command{Long: "-m, --message MSG"} args := NewArgs([]string{"foo", "bar", "-m", "baz"}) - - c.parseArguments(args) - assert.Equal(t, "baz", flag) - + err := c.parseArguments(args) + assert.Equal(t, nil, err) + assert.Equal(t, "baz", args.Flag.Value("--message")) assert.Equal(t, 1, len(args.Params)) assert.Equal(t, "bar", args.LastParam()) } @@ -127,3 +124,23 @@ c.Call(args) assert.Equal(t, "baz", result) } + +func Test_NameWithOwnerRe(t *testing.T) { + re := regexp.MustCompile(NameWithOwnerRe) + + assert.Equal(t, true, re.MatchString("o/n")) + assert.Equal(t, true, re.MatchString("own-er/my-project.git")) + assert.Equal(t, true, re.MatchString("my-project.git")) + assert.Equal(t, true, re.MatchString("my_project")) + assert.Equal(t, true, re.MatchString("-dash")) + assert.Equal(t, true, re.MatchString(".dotfiles")) + + assert.Equal(t, false, re.MatchString("")) + assert.Equal(t, false, re.MatchString("/")) + assert.Equal(t, false, re.MatchString(" ")) + assert.Equal(t, false, re.MatchString("owner/na me")) + assert.Equal(t, false, re.MatchString("owner/na/me")) + assert.Equal(t, false, re.MatchString("own.er/name")) + assert.Equal(t, false, re.MatchString("own_er/name")) + assert.Equal(t, false, re.MatchString("-owner/name")) +} diff -Nru hub-2.7.0~ds1/commands/compare.go hub-2.14.2~ds1/commands/compare.go --- hub-2.7.0~ds1/commands/compare.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/compare.go 2020-03-05 17:48:23.000000000 +0000 @@ -11,8 +11,11 @@ ) var cmdCompare = &Command{ - Run: compare, - Usage: "compare [-u] [-b ] [] [[...]]", + Run: compare, + Usage: ` +compare [-uc] [-b ] +compare [-uc] [] [...] +`, Long: `Open a GitHub compare page in a web browser. ## Options: @@ -22,25 +25,34 @@ -c, --copy Put the URL to clipboard instead of opening it. - -b, --base= - Base branch to compare. + -b, --base + Base branch to compare against in case no explicit arguments were given. - [...] + [...] Branch names, tag names, or commit SHAs specifying the range to compare. - defaults to the current branch name. - - If a range with two dots ('A..B') is given, it will be transformed into a + If a range with two dots (''A..B'') is given, it will be transformed into a range with three dots. + The portion defaults to the default branch of the repository. + + The argument defaults to the current branch. If the current branch + is not pushed to a remote, the command will error. + + + Optionally specify the owner of the repository for the compare page URL. + ## Examples: + $ hub compare + > open https://github.com/OWNER/REPO/compare/BRANCH + $ hub compare refactor - > open https://github.com/USER/REPO/compare/refactor + > open https://github.com/OWNER/REPO/compare/refactor $ hub compare v1.0..v1.1 - > open https://github.com/USER/REPO/compare/v1.0...v1.1 + > open https://github.com/OWNER/REPO/compare/v1.0...v1.1 $ hub compare -u jingweno feature - > echo https://github.com/jingweno/REPO/compare/feature + https://github.com/jingweno/REPO/compare/feature ## See also: @@ -48,17 +60,7 @@ `, } -var ( - flagCompareCopy bool - flagCompareURLOnly bool - flagCompareBase string -) - func init() { - cmdCompare.Flag.BoolVarP(&flagCompareCopy, "copy", "c", false, "COPY") - cmdCompare.Flag.BoolVarP(&flagCompareURLOnly, "url", "u", false, "URL only") - cmdCompare.Flag.StringVarP(&flagCompareBase, "base", "b", "", "BASE") - CmdRunner.Use(cmdCompare) } @@ -66,68 +68,69 @@ localRepo, err := github.LocalRepo() utils.Check(err) - var ( - branch *github.Branch - project *github.Project - r string - ) + mainProject, err := localRepo.MainProject() + utils.Check(err) + + host, err := github.CurrentConfig().PromptForHost(mainProject.Host) + utils.Check(err) - usageHelp := func() { - utils.Check(fmt.Errorf("Usage: hub compare [-u] [-b ] [] [[...]]")) - } + var r string + flagCompareBase := args.Flag.Value("--base") if args.IsParamsEmpty() { - branch, project, err = localRepo.RemoteBranchAndProject("", false) - utils.Check(err) + currentBranch, err := localRepo.CurrentBranch() + if err != nil { + utils.Check(command.UsageError(err.Error())) + } - if branch == nil || - (branch.IsMaster() && flagCompareBase == "") || - (flagCompareBase == branch.ShortName()) { + var remoteBranch *github.Branch + var remoteProject *github.Project - usageHelp() - } else { - r = branch.ShortName() - if flagCompareBase != "" { - r = parseCompareRange(flagCompareBase + "..." + r) + remoteBranch, remoteProject, err = findPushTarget(currentBranch) + if err != nil { + if remoteProject, err = deducePushTarget(currentBranch, host.User); err == nil { + remoteBranch = currentBranch + } else { + utils.Check(fmt.Errorf("the current branch '%s' doesn't seem pushed to a remote", currentBranch.ShortName())) + } + } + + r = remoteBranch.ShortName() + if remoteProject.SameAs(mainProject) { + if flagCompareBase == "" && remoteBranch.IsMaster() { + utils.Check(fmt.Errorf("the branch to compare '%s' is the default branch", remoteBranch.ShortName())) } + } else { + r = fmt.Sprintf("%s:%s", remoteProject.Owner, r) + } + + if flagCompareBase == r { + utils.Check(fmt.Errorf("the branch to compare '%s' is the same as --base", r)) + } else if flagCompareBase != "" { + r = fmt.Sprintf("%s...%s", flagCompareBase, r) } } else { if flagCompareBase != "" { - usageHelp() + utils.Check(command.UsageError("")) } else { r = parseCompareRange(args.RemoveParam(args.ParamsSize() - 1)) - project, err = localRepo.CurrentProject() - if args.IsParamsEmpty() { - utils.Check(err) - } else { - projectName := "" - projectHost := "" - if err == nil { - projectName = project.Name - projectHost = project.Host - } - project = github.NewProject(args.RemoveParam(args.ParamsSize()-1), projectName, projectHost) - if project.Name == "" { - utils.Check(fmt.Errorf("error: missing project name (owner: %q)\n", project.Owner)) - } + if !args.IsParamsEmpty() { + owner := args.RemoveParam(args.ParamsSize() - 1) + mainProject = github.NewProject(owner, mainProject.Name, mainProject.Host) } } } - if project == nil { - project, err = localRepo.CurrentProject() - utils.Check(err) - } - - subpage := utils.ConcatPaths("compare", rangeQueryEscape(r)) - url := project.WebURL("", "", subpage) + url := mainProject.WebURL("", "", "compare/"+rangeQueryEscape(r)) args.NoForward() + flagCompareURLOnly := args.Flag.Bool("--url") + flagCompareCopy := args.Flag.Bool("--copy") printBrowseOrCopy(args, url, !flagCompareURLOnly && !flagCompareCopy, flagCompareCopy) } func parseCompareRange(r string) string { - shaOrTag := fmt.Sprintf("((?:%s:)?\\w(?:[\\w.-]*\\w)?)", OwnerRe) + shaOrTag := fmt.Sprintf("((?:%s:)?\\w(?:[\\w/.-]*\\w)?)", OwnerRe) shaOrTagRange := fmt.Sprintf("^%s\\.\\.%s$", shaOrTag, shaOrTag) shaOrTagRangeRegexp := regexp.MustCompile(shaOrTagRange) return shaOrTagRangeRegexp.ReplaceAllString(r, "$1...$2") diff -Nru hub-2.7.0~ds1/commands/create.go hub-2.14.2~ds1/commands/create.go --- hub-2.7.0~ds1/commands/create.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/create.go 2020-03-05 17:48:23.000000000 +0000 @@ -2,7 +2,6 @@ import ( "fmt" - "regexp" "strings" "github.com/github/hub/git" @@ -20,11 +19,15 @@ -p, --private Create a private repository. - -d, --description= - Use this text as the description of the GitHub repository. + -d, --description + A short description of the GitHub repository. - -h, --homepage= - Use this text as the URL of the GitHub repository. + -h, --homepage + A URL with more information about the repository. Use this, for example, if + your project has an external website. + + --remote-name + Set the name for the new git remote (default: "origin"). -o, --browse Open the new repository in a web browser. @@ -53,22 +56,7 @@ `, } -var ( - flagCreatePrivate, - flagCreateBrowse, - flagCreateCopy bool - - flagCreateDescription, - flagCreateHomepage string -) - func init() { - cmdCreate.Flag.BoolVarP(&flagCreatePrivate, "private", "p", false, "PRIVATE") - cmdCreate.Flag.BoolVarP(&flagCreateBrowse, "browse", "o", false, "BROWSE") - cmdCreate.Flag.BoolVarP(&flagCreateCopy, "copy", "c", false, "COPY") - cmdCreate.Flag.StringVarP(&flagCreateDescription, "description", "d", "", "DESCRIPTION") - cmdCreate.Flag.StringVarP(&flagCreateHomepage, "homepage", "h", "", "HOMEPAGE") - CmdRunner.Use(cmdCreate) } @@ -85,12 +73,10 @@ utils.Check(err) newRepoName = github.SanitizeProjectName(dirName) } else { - reg := regexp.MustCompile("^[^-]") - if !reg.MatchString(args.FirstParam()) { - err = fmt.Errorf("invalid argument: %s", args.FirstParam()) - utils.Check(err) - } newRepoName = args.FirstParam() + if newRepoName == "" { + utils.Check(command.UsageError("")) + } } config := github.CurrentConfig() @@ -109,6 +95,8 @@ project := github.NewProject(owner, newRepoName, host.Host) gh := github.NewClient(project.Host) + flagCreatePrivate := args.Flag.Bool("--private") + repo, err := gh.Repository(project) if err == nil { foundProject := github.NewProject(repo.FullName, "", project.Host) @@ -129,6 +117,8 @@ if repo == nil { if !args.Noop { + flagCreateDescription := args.Flag.Value("--description") + flagCreateHomepage := args.Flag.Value("--homepage") repo, err := gh.CreateRepository(project, flagCreateDescription, flagCreateHomepage, flagCreatePrivate) utils.Check(err) project = github.NewProject(repo.FullName, "", project.Host) @@ -138,11 +128,15 @@ localRepo, err := github.LocalRepo() utils.Check(err) - originName := "origin" + originName := args.Flag.Value("--remote-name") + if originName == "" { + originName = "origin" + } + if originRemote, err := localRepo.RemoteByName(originName); err == nil { originProject, err := originRemote.Project() if err != nil || !originProject.SameAs(project) { - ui.Errorf(`A git remote named "%s" already exists and is set to push to '%s'.\n`, originRemote.Name, originRemote.PushURL) + ui.Errorf("A git remote named '%s' already exists and is set to push to '%s'.\n", originRemote.Name, originRemote.PushURL) } } else { url := project.GitURL("", "", true) @@ -151,5 +145,7 @@ webUrl := project.WebURL("", "", "") args.NoForward() + flagCreateBrowse := args.Flag.Bool("--browse") + flagCreateCopy := args.Flag.Bool("--copy") printBrowseOrCopy(args, webUrl, flagCreateBrowse, flagCreateCopy) } diff -Nru hub-2.7.0~ds1/commands/delete.go hub-2.14.2~ds1/commands/delete.go --- hub-2.7.0~ds1/commands/delete.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/delete.go 2020-03-05 17:48:23.000000000 +0000 @@ -38,13 +38,7 @@ `, } -var ( - flagDeleteAssumeYes bool -) - func init() { - cmdDelete.Flag.BoolVarP(&flagDeleteAssumeYes, "--yes", "y", false, "YES") - CmdRunner.Use(cmdDelete) } @@ -56,7 +50,7 @@ re := regexp.MustCompile(NameWithOwnerRe) if !re.MatchString(repoName) { - utils.Check(fmt.Errorf(command.Synopsis())) + utils.Check(command.UsageError("")) } config := github.CurrentConfig() @@ -74,7 +68,7 @@ project := github.NewProject(owner, repoName, host.Host) gh := github.NewClient(project.Host) - if !flagDeleteAssumeYes { + if !args.Flag.Bool("--yes") { ui.Printf("Really delete repository '%s' (yes/N)? ", project) answer := "" scanner := bufio.NewScanner(os.Stdin) diff -Nru hub-2.7.0~ds1/commands/fork.go hub-2.14.2~ds1/commands/fork.go --- hub-2.7.0~ds1/commands/fork.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/fork.go 2020-03-05 17:48:23.000000000 +0000 @@ -10,17 +10,17 @@ var cmdFork = &Command{ Run: fork, - Usage: "fork [--no-remote] [--remote-name=] [--org=]", - Long: `Fork the current project on GitHub and add a git remote for it. + Usage: "fork [--no-remote] [--remote-name ] [--org ]", + Long: `Fork the current repository on GitHub and add a git remote for it. ## Options: --no-remote Skip adding a git remote for the fork. - --remote-name= + --remote-name Set the name for the new git remote. - --org= + --org Fork the repository within this organization. ## Examples: @@ -38,18 +38,7 @@ `, } -var ( - flagForkNoRemote bool - - flagForkOrganization string - flagForkRemoteName string -) - func init() { - cmdFork.Flag.BoolVar(&flagForkNoRemote, "no-remote", false, "") - cmdFork.Flag.StringVarP(&flagForkRemoteName, "remote-name", "", "", "REMOTE") - cmdFork.Flag.StringVarP(&flagForkOrganization, "org", "", "", "ORGANIZATION") - CmdRunner.Use(cmdFork) } @@ -69,14 +58,14 @@ params := map[string]interface{}{} forkOwner := host.User - if flagForkOrganization != "" { + if flagForkOrganization := args.Flag.Value("--org"); flagForkOrganization != "" { forkOwner = flagForkOrganization params["organization"] = forkOwner } forkProject := github.NewProject(forkOwner, project.Name, project.Host) var newRemoteName string - if flagForkRemoteName != "" { + if flagForkRemoteName := args.Flag.Value("--remote-name"); flagForkRemoteName != "" { newRemoteName = flagForkRemoteName } else { newRemoteName = forkProject.Owner @@ -110,7 +99,7 @@ } args.NoForward() - if !flagForkNoRemote { + if !args.Flag.Bool("--no-remote") { originURL := originRemote.URL.String() url := forkProject.GitURL("", "", true) diff -Nru hub-2.7.0~ds1/commands/gist.go hub-2.14.2~ds1/commands/gist.go --- hub-2.7.0~ds1/commands/gist.go 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/commands/gist.go 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,163 @@ +package commands + +import ( + "fmt" + "sort" + "strings" + + "github.com/github/hub/github" + "github.com/github/hub/ui" + "github.com/github/hub/utils" +) + +var ( + cmdGist = &Command{ + Run: printGistHelp, + Usage: ` +gist create [-oc] [--public] [...] +gist show [] +`, + Long: `Create and print GitHub Gists + +## Commands: + + * _create_: + Create a new gist. If no are specified, the content is read from + standard input. + + * _show_: + Print the contents of a gist. If the gist contains multiple files, the + operation will error out unless is specified. + +## Options: + + --public + Make the new gist public (default: false). + + -o, --browse + Open the new gist in a web browser. + + -c, --copy + Put the URL of the new gist to clipboard instead of printing it. + +## Examples: + + $ echo hello | hub gist create --public + + $ hub gist create file1.txt file2.txt + + # print a specific file within a gist: + $ hub gist show ID testfile1.txt + +## See also: + +hub(1), hub-api(1) +`, + } + + cmdShowGist = &Command{ + Key: "show", + Run: showGist, + } + + cmdCreateGist = &Command{ + Key: "create", + Run: createGist, + KnownFlags: ` + --public + -o, --browse + -c, --copy +`, + } +) + +func init() { + cmdGist.Use(cmdShowGist) + cmdGist.Use(cmdCreateGist) + CmdRunner.Use(cmdGist) +} + +func getGist(gh *github.Client, id string, filename string) error { + gist, err := gh.FetchGist(id) + if err != nil { + return err + } + + if len(gist.Files) > 1 && filename == "" { + filenames := []string{} + for name := range gist.Files { + filenames = append(filenames, name) + } + sort.Strings(filenames) + return fmt.Errorf("This gist contains multiple files, you must specify one:\n %s", strings.Join(filenames, "\n ")) + } + + if filename != "" { + if val, ok := gist.Files[filename]; ok { + ui.Println(val.Content) + } else { + return fmt.Errorf("no such file in gist") + } + } else { + for name := range gist.Files { + file := gist.Files[name] + ui.Println(file.Content) + } + } + return nil +} + +func printGistHelp(command *Command, args *Args) { + utils.Check(command.UsageError("")) +} + +func createGist(cmd *Command, args *Args) { + args.NoForward() + + host, err := github.CurrentConfig().DefaultHostNoPrompt() + utils.Check(err) + gh := github.NewClient(host.Host) + + filenames := []string{} + if args.IsParamsEmpty() { + filenames = append(filenames, "-") + } else { + filenames = args.Params + } + + var gist *github.Gist + if args.Noop { + ui.Println("Would create gist") + gist = &github.Gist{ + HtmlUrl: fmt.Sprintf("https://gist.%s/%s", gh.Host.Host, "ID"), + } + } else { + gist, err = gh.CreateGist(filenames, args.Flag.Bool("--public")) + utils.Check(err) + } + + flagIssueBrowse := args.Flag.Bool("--browse") + flagIssueCopy := args.Flag.Bool("--copy") + printBrowseOrCopy(args, gist.HtmlUrl, flagIssueBrowse, flagIssueCopy) +} + +func showGist(cmd *Command, args *Args) { + args.NoForward() + + if args.ParamsSize() < 1 { + utils.Check(cmd.UsageError("you must specify a gist ID")) + } + + host, err := github.CurrentConfig().DefaultHostNoPrompt() + utils.Check(err) + gh := github.NewClient(host.Host) + + id := args.GetParam(0) + filename := "" + if args.ParamsSize() > 1 { + filename = args.GetParam(1) + } + + err = getGist(gh, id, filename) + utils.Check(err) +} diff -Nru hub-2.7.0~ds1/commands/help.go hub-2.14.2~ds1/commands/help.go --- hub-2.7.0~ds1/commands/help.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/help.go 2020-03-05 17:48:23.000000000 +0000 @@ -3,13 +3,15 @@ import ( "fmt" "os" + "os/exec" "path/filepath" "sort" "strings" - "github.com/github/hub/cmd" + "github.com/github/hub/git" "github.com/github/hub/ui" "github.com/github/hub/utils" + "github.com/kballard/go-shellquote" ) var cmdHelp = &Command{ @@ -27,17 +29,7 @@ Use this format to view help for hub extensions to an existing git command. --plain-text - Skip man page lookup mechanism and display plain help text. - -## Man lookup mechanism: - -On systems that have 'man', help pages are looked up in these directories -relative to 'hub' install prefix: - -* 'man/.1' -* 'share/man/man1/.1' - -On systems without 'man', same help pages are looked up with a '.txt' suffix. + Skip man page lookup mechanism and display raw help text. ## See also: @@ -65,7 +57,14 @@ return } - if args.HasFlags("-a", "--all") { + p := utils.NewArgsParser() + p.RegisterBool("--all", "-a") + p.RegisterBool("--plain-text") + p.RegisterBool("--man", "-m") + p.RegisterBool("--web", "-w") + p.Parse(args.Params) + + if p.Bool("--all") { args.AfterFn(func() error { ui.Printf("\nhub custom commands\n\n %s\n", strings.Join(customCommands(), " ")) return nil @@ -73,27 +72,43 @@ return } - command := args.FirstParam() - - if command == "hub" { - err := displayManPage("hub.1", args) - if err != nil { - utils.Check(err) + isWeb := func() bool { + if p.Bool("--web") { + return true + } + if p.Bool("--man") { + return false } + if f, err := git.Config("help.format"); err == nil { + return f == "web" || f == "html" + } + return false } - if c := lookupCmd(command); c != nil { - if !args.HasFlags("--plain-text") { - manPage := fmt.Sprintf("hub-%s.1", c.Name()) - err := displayManPage(manPage, args) - if err == nil { - return - } - } + cmdName := "" + if words := args.Words(); len(words) > 0 { + cmdName = words[0] + } + + if cmdName == "hub" { + err := displayManPage("hub", args, isWeb()) + utils.Check(err) + return + } - ui.Println(c.HelpText()) - args.NoForward() + foundCmd := lookupCmd(cmdName) + if foundCmd == nil { + return + } + + if p.Bool("--plain-text") { + ui.Println(foundCmd.HelpText()) + os.Exit(0) } + + manPage := fmt.Sprintf("hub-%s", foundCmd.Name()) + err := displayManPage(manPage, args, isWeb()) + utils.Check(err) } func runListCmds(cmd *Command, args *Args) { @@ -114,51 +129,61 @@ } } -func displayManPage(manPage string, args *Args) error { - manProgram, _ := utils.CommandPath("man") - if manProgram == "" { - manPage += ".txt" - manProgram = os.Getenv("PAGER") - if manProgram == "" { - manProgram = "less -R" - } - } - +// On systems where `man` was found, invoke: +// MANPATH={PREFIX}/share/man:$MANPATH man +// +// otherwise: +// less -R {PREFIX}/share/man/man1/.1.txt +func displayManPage(manPage string, args *Args, isWeb bool) error { programPath, err := utils.CommandPath(args.ProgramPath) if err != nil { return err } - installPrefix := filepath.Join(filepath.Dir(programPath), "..") - manFile, err := localManPage(manPage, installPrefix) - if err != nil { - return err + if isWeb { + manPage += ".1.html" + manFile := filepath.Join(programPath, "..", "..", "share", "doc", "hub-doc", manPage) + args.Replace(args.Executable, "web--browse", manFile) + return nil } - man := cmd.New(manProgram) - man.WithArg(manFile) - if err = man.Run(); err == nil { - os.Exit(0) + var manArgs []string + manProgram, _ := utils.CommandPath("man") + if manProgram != "" { + manArgs = []string{manProgram} } else { - os.Exit(1) + manPage += ".1.txt" + if manProgram = os.Getenv("PAGER"); manProgram != "" { + var err error + manArgs, err = shellquote.Split(manProgram) + if err != nil { + return err + } + } else { + manArgs = []string{"less", "-R"} + } } - return nil -} -func localManPage(name, installPrefix string) (string, error) { - manPath := filepath.Join(installPrefix, "man", name) - _, err := os.Stat(manPath) - if err == nil { - return manPath, nil + env := os.Environ() + if strings.HasSuffix(manPage, ".txt") { + manFile := filepath.Join(programPath, "..", "..", "share", "man", "man1", manPage) + manArgs = append(manArgs, manFile) + } else { + manArgs = append(manArgs, manPage) + manPath := filepath.Join(programPath, "..", "..", "share", "man") + env = append(env, fmt.Sprintf("MANPATH=%s:%s", manPath, os.Getenv("MANPATH"))) } - manPath = filepath.Join(installPrefix, "share", "man", "man1", name) - _, err = os.Stat(manPath) - if err == nil { - return manPath, nil - } else { - return "", err + c := exec.Command(manArgs[0], manArgs[1:]...) + c.Stdin = os.Stdin + c.Stdout = os.Stdout + c.Stderr = os.Stderr + c.Env = env + if err := c.Run(); err != nil { + return err } + os.Exit(0) + return nil } func lookupCmd(name string) *Command { @@ -182,7 +207,7 @@ } } - sort.Sort(sort.StringSlice(cmds)) + sort.Strings(cmds) return cmds } @@ -190,12 +215,14 @@ var helpText = ` These GitHub commands are provided by hub: + api Low-level GitHub API request interface browse Open a GitHub page in the default browser ci-status Show the status of GitHub checks for a commit compare Open a compare page on GitHub create Create this repository on GitHub and add GitHub as origin delete Delete a repository on GitHub fork Make a fork of a remote repository on GitHub and add as remote + gist Make a gist issue List or create GitHub issues pr List or checkout GitHub pull requests pull-request Open a pull request on GitHub diff -Nru hub-2.7.0~ds1/commands/init.go hub-2.14.2~ds1/commands/init.go --- hub-2.7.0~ds1/commands/init.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/init.go 2020-03-05 17:48:23.000000000 +0000 @@ -78,7 +78,7 @@ // Assume that the name of the working directory is going to be the name of // the project on GitHub. projectName := strings.Replace(filepath.Base(dirToInit), " ", "-", -1) - project := github.NewProject(host.User, projectName, "") + project := github.NewProject(host.User, projectName, host.Host) url := project.GitURL("", "", true) addRemote := []string{ diff -Nru hub-2.7.0~ds1/commands/issue.go hub-2.14.2~ds1/commands/issue.go --- hub-2.7.0~ds1/commands/issue.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/issue.go 2020-03-05 17:48:23.000000000 +0000 @@ -20,9 +20,11 @@ issue [-a ] [-c ] [-@ ] [-s ] [-f ] [-M ] [-l ] [-d ] [-o [-^]] [-L ] issue show [-f ] issue create [-oc] [-m |-F ] [--edit] [-a ] [-M ] [-l ] +issue update [-m |-F ] [--edit] [-a ] [-M ] [-l ] [-s ] issue labels [--color] +issue transfer `, - Long: `Manage GitHub issues for the current project. + Long: `Manage GitHub Issues for the current repository. ## Commands: @@ -32,28 +34,35 @@ Show an existing issue specified by . * _create_: - Open an issue in the current project. + Open an issue in the current repository. + + * _update_: + Update fields of an existing issue specified by . Use ''--edit'' + to edit the title and message interactively in the text editor. * _labels_: List the labels available in this repository. + * _transfer_: + Transfer an issue to another repository. + ## Options: - -a, --assignee= - Display only issues assigned to . + -a, --assignee + In list mode, display only issues assigned to . - When opening an issue, this can be a comma-separated list of people to - assign to the new issue. + -a, --assign + A comma-separated list of GitHub handles to assign to the created issue. - -c, --creator= + -c, --creator Display only issues created by . - -@, --mentioned= + -@, --mentioned Display only issues mentioning . - -s, --state= + -s, --state Display issues with state (default: "open"). - -f, --format= + -f, --format Pretty print the contents of the issues using format (default: "%sC%>(8)%i%Creset %t% l%n"). See the "PRETTY FORMATS" section of git-log(1) for some additional details on how placeholders are used in @@ -109,18 +118,27 @@ %%: a literal % - -m, --message= + --color[=] + Enable colored output even if stdout is not a terminal. can be one + of "always" (default for ''--color''), "never", or "auto" (default). + + -m, --message The text up to the first blank line in is treated as the issue title, and the rest is used as issue description in Markdown format. - If multiple options are given, their values are concatenated as - separate paragraphs. + When multiple ''--message'' are passed, their values are concatenated with a + blank line in-between. - -F, --file= - Read the issue title and description from . + When neither ''--message'' nor ''--file'' were supplied to ''issue create'', a + text editor will open to author the title and description in. + + -F, --file + Read the issue title and description from . Pass "-" to read from + standard input instead. See ''--message'' for the formatting rules. -e, --edit - Further edit the contents of in a text editor before submitting. + Open the issue title and description in a text editor before submitting. + This can be used in combination with ''--message'' or ''--file''. -o, --browse Open the new issue in a web browser. @@ -128,26 +146,27 @@ -c, --copy Put the URL of the new issue to clipboard instead of printing it. - -M, --milestone= - Display only issues for a GitHub milestone with id . + -M, --milestone + Display only issues for a GitHub milestone with the name . - When opening an issue, add this issue to a GitHub milestone with id . + When opening an issue, add this issue to a GitHub milestone with the name . + Passing the milestone number is deprecated. - -l, --labels= + -l, --labels Display only issues with certain labels. When opening an issue, add a comma-separated list of labels to this issue. - -d, --since= + -d, --since Display only issues updated on or after in ISO 8601 format. - -o, --sort= + -o, --sort Sort displayed issues by "created" (default), "updated" or "comments". -^ --sort-ascending Sort by ascending dates instead of descending. - -L, --limit= + -L, --limit Display only the first issues. --include-pulls @@ -155,90 +174,86 @@ --color Enable colored output for labels list. + +## See also: + +hub-pr(1), hub(1) +`, + KnownFlags: ` + -a, --assignee USER + -s, --state STATE + -f, --format FMT + -M, --milestone NAME + -c, --creator USER + -@, --mentioned USER + -l, --labels LIST + -d, --since DATE + -o, --sort KEY + -^, --sort-ascending + --include-pulls + -L, --limit N + --color `, } cmdCreateIssue = &Command{ - Key: "create", - Run: createIssue, - Usage: "issue create [-o] [-m |-F ] [-a ] [-M ] [-l ]", - Long: "Open an issue in the current project.", + Key: "create", + Run: createIssue, + KnownFlags: ` + -m, --message MSG + -F, --file FILE + -M, --milestone NAME + -l, --labels LIST + -a, --assign USER + -o, --browse + -c, --copy + -e, --edit +`, } cmdShowIssue = &Command{ - Key: "show", - Run: showIssue, - Usage: "issue show ", - Long: "Show an issue in the current project.", + Key: "show", + Run: showIssue, + KnownFlags: ` + -f, --format FMT + --color +`, } cmdLabel = &Command{ - Key: "labels", - Run: listLabels, - Usage: "issue labels [--color]", - Long: "List the labels available in this repository.", - } - - flagIssueAssignee, - flagIssueState, - flagIssueFormat, - flagShowIssueFormat, - flagIssueMilestoneFilter, - flagIssueCreator, - flagIssueMentioned, - flagIssueLabelsFilter, - flagIssueSince, - flagIssueSort, - flagIssueFile string - - flagIssueMessage messageBlocks - - flagIssueEdit, - flagIssueCopy, - flagIssueBrowse, - flagIssueSortAscending bool - flagIssueIncludePulls bool - - flagIssueMilestone uint64 - - flagIssueAssignees, - flagIssueLabels listFlag - - flagIssueLimit int + Key: "labels", + Run: listLabels, + KnownFlags: ` + --color +`, + } - flagLabelsColorize bool + cmdTransfer = &Command{ + Key: "transfer", + Run: transferIssue, + } + + cmdUpdate = &Command{ + Key: "update", + Run: updateIssue, + KnownFlags: ` + -m, --message MSG + -F, --file FILE + -M, --milestone NAME + -l, --labels LIST + -a, --assign USER + -e, --edit + -s, --state STATE +`, + } ) func init() { - cmdShowIssue.Flag.StringVarP(&flagShowIssueFormat, "format", "f", "", "FORMAT") - - cmdCreateIssue.Flag.VarP(&flagIssueMessage, "message", "m", "MESSAGE") - cmdCreateIssue.Flag.StringVarP(&flagIssueFile, "file", "F", "", "FILE") - cmdCreateIssue.Flag.Uint64VarP(&flagIssueMilestone, "milestone", "M", 0, "MILESTONE") - cmdCreateIssue.Flag.VarP(&flagIssueLabels, "label", "l", "LABEL") - cmdCreateIssue.Flag.VarP(&flagIssueAssignees, "assign", "a", "ASSIGNEE") - cmdCreateIssue.Flag.BoolVarP(&flagIssueBrowse, "browse", "o", false, "BROWSE") - cmdCreateIssue.Flag.BoolVarP(&flagIssueCopy, "copy", "c", false, "COPY") - cmdCreateIssue.Flag.BoolVarP(&flagIssueEdit, "edit", "e", false, "EDIT") - - cmdIssue.Flag.StringVarP(&flagIssueAssignee, "assignee", "a", "", "ASSIGNEE") - cmdIssue.Flag.StringVarP(&flagIssueState, "state", "s", "", "STATE") - cmdIssue.Flag.StringVarP(&flagIssueFormat, "format", "f", "%sC%>(8)%i%Creset %t% l%n", "FORMAT") - cmdIssue.Flag.StringVarP(&flagIssueMilestoneFilter, "milestone", "M", "", "MILESTONE") - cmdIssue.Flag.StringVarP(&flagIssueCreator, "creator", "c", "", "CREATOR") - cmdIssue.Flag.StringVarP(&flagIssueMentioned, "mentioned", "@", "", "USER") - cmdIssue.Flag.StringVarP(&flagIssueLabelsFilter, "labels", "l", "", "LABELS") - cmdIssue.Flag.StringVarP(&flagIssueSince, "since", "d", "", "DATE") - cmdIssue.Flag.StringVarP(&flagIssueSort, "sort", "o", "created", "SORT_KEY") - cmdIssue.Flag.BoolVarP(&flagIssueSortAscending, "sort-ascending", "^", false, "SORT_KEY") - cmdIssue.Flag.BoolVarP(&flagIssueIncludePulls, "include-pulls", "", false, "INCLUDE_PULLS") - cmdIssue.Flag.IntVarP(&flagIssueLimit, "limit", "L", -1, "LIMIT") - - cmdLabel.Flag.BoolVarP(&flagLabelsColorize, "color", "", false, "COLORIZE") - cmdIssue.Use(cmdShowIssue) cmdIssue.Use(cmdCreateIssue) cmdIssue.Use(cmdLabel) + cmdIssue.Use(cmdTransfer) + cmdIssue.Use(cmdUpdate) CmdRunner.Use(cmdIssue) } @@ -254,29 +269,47 @@ if args.Noop { ui.Printf("Would request list of issues for %s\n", project) } else { - flagFilters := map[string]string{ - "state": flagIssueState, - "assignee": flagIssueAssignee, - "milestone": flagIssueMilestoneFilter, - "creator": flagIssueCreator, - "mentioned": flagIssueMentioned, - "labels": flagIssueLabelsFilter, - "sort": flagIssueSort, - } filters := map[string]interface{}{} - for flag, filter := range flagFilters { - if cmd.FlagPassed(flag) { - filters[flag] = filter + if args.Flag.HasReceived("--state") { + filters["state"] = args.Flag.Value("--state") + } + if args.Flag.HasReceived("--assignee") { + filters["assignee"] = args.Flag.Value("--assignee") + } + if args.Flag.HasReceived("--milestone") { + milestoneValue := args.Flag.Value("--milestone") + if milestoneValue == "none" { + filters["milestone"] = milestoneValue + } else { + milestoneNumber, err := milestoneValueToNumber(milestoneValue, gh, project) + utils.Check(err) + if milestoneNumber > 0 { + filters["milestone"] = milestoneNumber + } } } + if args.Flag.HasReceived("--creator") { + filters["creator"] = args.Flag.Value("--creator") + } + if args.Flag.HasReceived("--mentioned") { + filters["mentioned"] = args.Flag.Value("--mentioned") + } + if args.Flag.HasReceived("--labels") { + labels := commaSeparated(args.Flag.AllValues("--labels")) + filters["labels"] = strings.Join(labels, ",") + } + if args.Flag.HasReceived("--sort") { + filters["sort"] = args.Flag.Value("--sort") + } - if flagIssueSortAscending { + if args.Flag.Bool("--sort-ascending") { filters["direction"] = "asc" } else { filters["direction"] = "desc" } - if cmd.FlagPassed("since") { + if args.Flag.HasReceived("--since") { + flagIssueSince := args.Flag.Value("--since") if sinceTime, err := time.ParseInLocation("2006-01-02", flagIssueSince, time.Local); err == nil { filters["since"] = sinceTime.Format(time.RFC3339) } else { @@ -284,6 +317,13 @@ } } + flagIssueLimit := args.Flag.Int("--limit") + flagIssueIncludePulls := args.Flag.Bool("--include-pulls") + flagIssueFormat := "%sC%>(8)%i%Creset %t% l%n" + if args.Flag.HasReceived("--format") { + flagIssueFormat = args.Flag.Value("--format") + } + issues, err := gh.FetchIssues(project, filters, flagIssueLimit, func(issue *github.Issue) bool { return issue.PullRequest == nil || flagIssueIncludePulls }) @@ -296,7 +336,7 @@ } } - colorize := ui.IsTerminal(os.Stdout) + colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color")) for _, issue := range issues { ui.Print(formatIssue(issue, flagIssueFormat, colorize)) } @@ -318,16 +358,13 @@ var labelStrings []string var rawLabels []string for _, label := range issue.Labels { - if !colorize { - labelStrings = append(labelStrings, fmt.Sprintf(" %s ", label.Name)) - continue - } - color, err := utils.NewColor(label.Color) - if err != nil { + if colorize { + color, err := utils.NewColor(label.Color) utils.Check(err) + labelStrings = append(labelStrings, colorizeLabel(label, color)) + } else { + labelStrings = append(labelStrings, fmt.Sprintf(" %s ", label.Name)) } - - labelStrings = append(labelStrings, colorizeLabel(label, color)) rawLabels = append(rawLabels, label.Name) } @@ -390,7 +427,30 @@ } } -func formatPullRequestPlaceholders(pr github.PullRequest) map[string]string { +func formatPullRequestPlaceholders(pr github.PullRequest, colorize bool) map[string]string { + prState := pr.State + if prState == "open" && pr.Draft { + prState = "draft" + } else if !pr.MergedAt.IsZero() { + prState = "merged" + } + + var stateColorSwitch string + var prColor int + if colorize { + switch prState { + case "draft": + prColor = 37 + case "merged": + prColor = 35 + case "closed": + prColor = 31 + default: + prColor = 32 + } + stateColorSwitch = fmt.Sprintf("\033[%dm", prColor) + } + base := pr.Base.Ref head := pr.Head.Label if pr.IsSameRepo() { @@ -415,6 +475,8 @@ } return map[string]string{ + "pS": prState, + "pC": stateColorSwitch, "B": base, "H": head, "sB": pr.Base.Sha, @@ -434,9 +496,12 @@ } func showIssue(cmd *Command, args *Args) { - issueNumber := cmd.Arg(0) + issueNumber := "" + if args.ParamsSize() > 0 { + issueNumber = args.GetParam(0) + } if issueNumber == "" { - utils.Check(fmt.Errorf(cmd.Synopsis())) + utils.Check(cmd.UsageError("")) } localRepo, err := github.LocalRepo() @@ -453,8 +518,9 @@ args.NoForward() - colorize := ui.IsTerminal(os.Stdout) - if flagShowIssueFormat != "" { + colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color")) + if args.Flag.HasReceived("--format") { + flagShowIssueFormat := args.Flag.Value("--format") ui.Print(formatIssue(*issue, flagShowIssueFormat, colorize)) return } @@ -485,8 +551,6 @@ ui.Printf("\n### comment by @%s on %s\n\n%s\n", comment.User.Login, comment.CreatedAt.String(), comment.Body) } } - - return } func createIssue(cmd *Command, args *Args) { @@ -508,11 +572,13 @@ Write a message for this issue. The first block of text is the title and the rest is the description.`, project)) + flagIssueEdit := args.Flag.Bool("--edit") + flagIssueMessage := args.Flag.AllValues("--message") if len(flagIssueMessage) > 0 { - messageBuilder.Message = flagIssueMessage.String() + messageBuilder.Message = strings.Join(flagIssueMessage, "\n\n") messageBuilder.Edit = flagIssueEdit - } else if cmd.FlagPassed("file") { - messageBuilder.Message, err = msgFromFile(flagIssueFile) + } else if args.Flag.HasReceived("--file") { + messageBuilder.Message, err = msgFromFile(args.Flag.Value("--file")) utils.Check(err) messageBuilder.Edit = flagIssueEdit } else { @@ -541,17 +607,11 @@ "body": body, } - if len(flagIssueLabels) > 0 { - params["labels"] = flagIssueLabels - } + setLabelsFromArgs(params, args) - if len(flagIssueAssignees) > 0 { - params["assignees"] = flagIssueAssignees - } + setAssigneesFromArgs(params, args) - if flagIssueMilestone > 0 { - params["milestone"] = flagIssueMilestone - } + setMilestoneFromArgs(params, args, gh, project) args.NoForward() if args.Noop { @@ -560,12 +620,87 @@ issue, err := gh.CreateIssue(project, params) utils.Check(err) + flagIssueBrowse := args.Flag.Bool("--browse") + flagIssueCopy := args.Flag.Bool("--copy") printBrowseOrCopy(args, issue.HtmlUrl, flagIssueBrowse, flagIssueCopy) } messageBuilder.Cleanup() } +func updateIssue(cmd *Command, args *Args) { + issueNumber := 0 + if args.ParamsSize() > 0 { + issueNumber, _ = strconv.Atoi(args.GetParam(0)) + } + if issueNumber == 0 { + utils.Check(cmd.UsageError("")) + } + if !hasField(args, "--message", "--file", "--labels", "--milestone", "--assign", "--state", "--edit") { + utils.Check(cmd.UsageError("please specify fields to update")) + } + + localRepo, err := github.LocalRepo() + utils.Check(err) + + project, err := localRepo.MainProject() + utils.Check(err) + + gh := github.NewClient(project.Host) + + params := map[string]interface{}{} + setLabelsFromArgs(params, args) + setAssigneesFromArgs(params, args) + setMilestoneFromArgs(params, args, gh, project) + + if args.Flag.HasReceived("--state") { + params["state"] = args.Flag.Value("--state") + } + + if hasField(args, "--message", "--file", "--edit") { + messageBuilder := &github.MessageBuilder{ + Filename: "ISSUE_EDITMSG", + Title: "issue", + } + + messageBuilder.AddCommentedSection(fmt.Sprintf(`Editing issue #%d for %s + +Update the message for this issue. The first block of +text is the title and the rest is the description.`, issueNumber, project)) + + messageBuilder.Edit = args.Flag.Bool("--edit") + flagIssueMessage := args.Flag.AllValues("--message") + if len(flagIssueMessage) > 0 { + messageBuilder.Message = strings.Join(flagIssueMessage, "\n\n") + } else if args.Flag.HasReceived("--file") { + messageBuilder.Message, err = msgFromFile(args.Flag.Value("--file")) + utils.Check(err) + } else { + issue, err := gh.FetchIssue(project, strconv.Itoa(issueNumber)) + utils.Check(err) + existingMessage := fmt.Sprintf("%s\n\n%s", issue.Title, issue.Body) + messageBuilder.Message = strings.Replace(existingMessage, "\r\n", "\n", -1) + } + + title, body, err := messageBuilder.Extract() + utils.Check(err) + if title == "" { + utils.Check(fmt.Errorf("Aborting creation due to empty issue title")) + } + params["title"] = title + params["body"] = body + defer messageBuilder.Cleanup() + } + + args.NoForward() + if args.Noop { + ui.Printf("Would update issue #%d for %s\n", issueNumber, project) + } else { + err := gh.UpdateIssue(project, issueNumber, params) + utils.Check(err) + } +} + func listLabels(cmd *Command, args *Args) { localRepo, err := github.LocalRepo() utils.Check(err) @@ -584,11 +719,66 @@ labels, err := gh.FetchLabels(project) utils.Check(err) + flagLabelsColorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color")) for _, label := range labels { ui.Print(formatLabel(label, flagLabelsColorize)) } } +func hasField(args *Args, names ...string) bool { + found := false + for _, name := range names { + if args.Flag.HasReceived(name) { + found = true + } + } + return found +} + +func setLabelsFromArgs(params map[string]interface{}, args *Args) { + if !args.Flag.HasReceived("--labels") { + return + } + params["labels"] = commaSeparated(args.Flag.AllValues("--labels")) +} + +func setAssigneesFromArgs(params map[string]interface{}, args *Args) { + if !args.Flag.HasReceived("--assign") { + return + } + params["assignees"] = commaSeparated(args.Flag.AllValues("--assign")) +} + +func setMilestoneFromArgs(params map[string]interface{}, args *Args, gh *github.Client, project *github.Project) { + if !args.Flag.HasReceived("--milestone") { + return + } + milestoneNumber, err := milestoneValueToNumber(args.Flag.Value("--milestone"), gh, project) + utils.Check(err) + if milestoneNumber == 0 { + params["milestone"] = nil + } else { + params["milestone"] = milestoneNumber + } +} + +func colorizeOutput(colorSet bool, when string) bool { + if !colorSet || when == "auto" { + colorConfig, _ := git.Config("color.ui") + switch colorConfig { + case "false", "never": + return false + case "always": + return true + } + return ui.IsTerminal(os.Stdout) + } else if when == "never" { + return false + } else { + return true // "always" + } +} + func formatLabel(label github.IssueLabel, colorize bool) string { if colorize { if color, err := utils.NewColor(label.Color); err == nil { @@ -600,13 +790,136 @@ func colorizeLabel(label github.IssueLabel, color *utils.Color) string { bgColorCode := utils.RgbToTermColorCode(color) - return fmt.Sprintf("\033[38;5;%d;48;%sm %s \033[m", - getSuitableLabelTextColor(color), bgColorCode, label.Name) + fgColor := pickHighContrastTextColor(color) + fgColorCode := utils.RgbToTermColorCode(fgColor) + return fmt.Sprintf("\033[38;%s;48;%sm %s \033[m", + fgColorCode, bgColorCode, label.Name) +} + +type contrastCandidate struct { + color *utils.Color + contrast float64 +} + +func pickHighContrastTextColor(color *utils.Color) *utils.Color { + candidates := []contrastCandidate{} + appendCandidate := func(c *utils.Color) { + candidates = append(candidates, contrastCandidate{ + color: c, + contrast: color.ContrastRatio(c), + }) + } + + appendCandidate(utils.White) + appendCandidate(utils.Black) + + for _, candidate := range candidates { + if candidate.contrast >= 7.0 { + return candidate.color + } + } + for _, candidate := range candidates { + if candidate.contrast >= 4.5 { + return candidate.color + } + } + return utils.Black +} + +func milestoneValueToNumber(value string, client *github.Client, project *github.Project) (int, error) { + if value == "" { + return 0, nil + } + + if milestoneNumber, err := strconv.Atoi(value); err == nil { + return milestoneNumber, nil + } + + milestones, err := client.FetchMilestones(project) + if err != nil { + return 0, err + } + for _, milestone := range milestones { + if strings.EqualFold(milestone.Title, value) { + return milestone.Number, nil + } + } + + return 0, fmt.Errorf("error: no milestone found with name '%s'", value) } -func getSuitableLabelTextColor(color *utils.Color) int { - if color.Brightness() < 0.65 { - return 15 // white text +func transferIssue(cmd *Command, args *Args) { + if args.ParamsSize() < 2 { + utils.Check(cmd.UsageError("")) + } + + localRepo, err := github.LocalRepo() + utils.Check(err) + + project, err := localRepo.MainProject() + utils.Check(err) + + issueNumber, err := strconv.Atoi(args.GetParam(0)) + utils.Check(err) + targetOwner := project.Owner + targetRepo := args.GetParam(1) + if strings.Contains(targetRepo, "/") { + parts := strings.SplitN(targetRepo, "/", 2) + targetOwner = parts[0] + targetRepo = parts[1] } - return 16 // black text + + gh := github.NewClient(project.Host) + + nodeIDsResponse := struct { + Source struct { + Issue struct { + ID string + } + } + Target struct { + ID string + } + }{} + err = gh.GraphQL(` + query($issue: Int!, $sourceOwner: String!, $sourceRepo: String!, $targetOwner: String!, $targetRepo: String!) { + source: repository(owner: $sourceOwner, name: $sourceRepo) { + issue(number: $issue) { + id + } + } + target: repository(owner: $targetOwner, name: $targetRepo) { + id + } + }`, map[string]interface{}{ + "issue": issueNumber, + "sourceOwner": project.Owner, + "sourceRepo": project.Name, + "targetOwner": targetOwner, + "targetRepo": targetRepo, + }, &nodeIDsResponse) + utils.Check(err) + + issueResponse := struct { + TransferIssue struct { + Issue struct { + URL string + } + } + }{} + err = gh.GraphQL(` + mutation($issue: ID!, $repo: ID!) { + transferIssue(input: {issueId: $issue, repositoryId: $repo}) { + issue { + url + } + } + }`, map[string]interface{}{ + "issue": nodeIDsResponse.Source.Issue.ID, + "repo": nodeIDsResponse.Target.ID, + }, &issueResponse) + utils.Check(err) + + ui.Println(issueResponse.TransferIssue.Issue.URL) + args.NoForward() } diff -Nru hub-2.7.0~ds1/commands/issue_test.go hub-2.14.2~ds1/commands/issue_test.go --- hub-2.7.0~ds1/commands/issue_test.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/issue_test.go 2020-03-05 17:48:23.000000000 +0000 @@ -66,7 +66,7 @@ }, format: format, colorize: true, - expect: "\033[32m #42\033[m An issue with labels \033[38;5;15;48;2;128;0;0m bug \033[m \033[38;5;16;48;2;85;255;85m reproduced \033[m\n", + expect: "\033[32m #42\033[m An issue with labels \033[38;2;255;255;255;48;2;128;0;0m bug \033[m \033[38;2;0;0;0;48;2;85;255;85m reproduced \033[m\n", }, { name: "not colorized", @@ -181,7 +181,7 @@ issue: issue, format: "%l", colorize: true, - expect: "\033[38;5;15;48;2;136;0;0m bug \033[m \033[38;5;15;48;2;0;136;0m feature \033[m", + expect: "\033[38;2;255;255;255;48;2;136;0;0m bug \033[m \033[38;2;255;255;255;48;2;0;136;0m feature \033[m", }, { name: "label not colorized", diff -Nru hub-2.7.0~ds1/commands/pr.go hub-2.14.2~ds1/commands/pr.go --- hub-2.7.0~ds1/commands/pr.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/pr.go 2020-03-05 17:48:23.000000000 +0000 @@ -2,10 +2,10 @@ import ( "fmt" - "os" "strconv" "strings" + "github.com/github/hub/git" "github.com/github/hub/github" "github.com/github/hub/ui" "github.com/github/hub/utils" @@ -17,33 +17,43 @@ Usage: ` pr list [-s ] [-h ] [-b ] [-o [-^]] [-f ] [-L ] pr checkout [] +pr show [-uc] [-f ] [-h ] +pr show [-uc] [-f ] `, - Long: `Manage GitHub pull requests for the current project. + Long: `Manage GitHub Pull Requests for the current repository. ## Commands: * _list_: - List pull requests in the current project. + List pull requests in the current repository. * _checkout_: Check out the head of a pull request in a new branch. + To update the pull request with new commits, use ''git push''. + + * _show_: + Open a pull request page in a web browser. When no is + specified, is used to look up open pull requests and defaults to + the current branch name. With ''--format'', print information about the + pull request instead of opening it. + ## Options: - -s, --state= + -s, --state Filter pull requests by . Supported values are: "open" (default), "closed", "merged", or "all". - -h, --head= + -h, --head Show pull requests started from the specified head . The "OWNER:BRANCH" format must be used for pull requests from forks. - -b, --base= + -b, --base Show pull requests based off the specified . - -f, --format= + -f, --format Pretty print the list of pull requests using format (default: - "%sC%>(8)%i%Creset %t% l%n"). See the "PRETTY FORMATS" section of + "%pC%>(8)%i%Creset %t% l%n"). See the "PRETTY FORMATS" section of git-log(1) for some additional details on how placeholders are used in format. The available placeholders are: @@ -55,7 +65,11 @@ %S: state ("open" or "closed") - %sC: set color to red or green, depending on pull request state. + %pS: pull request state ("open", "draft", "merged", or "closed") + + %sC: set color to red or green, depending on state + + %pC: set color according to pull request state %t: title @@ -85,10 +99,6 @@ %Mt: milestone title - %NC: number of comments - - %Nc: number of comments wrapped in parentheses, or blank string if zero. - %cD: created date-only (no time of day) %cr: created date, relative @@ -117,14 +127,24 @@ %%: a literal % - -o, --sort= - Sort displayed issues by "created" (default), "updated", "popularity", or "long-running". + --color[=] + Enable colored output even if stdout is not a terminal. can be one + of "always" (default for ''--color''), "never", or "auto" (default). + + -o, --sort + Sort displayed pull requests by "created" (default), "updated", "popularity", or "long-running". - -^ --sort-ascending + -^, --sort-ascending Sort by ascending dates instead of descending. - -L, --limit= - Display only the first issues. + -L, --limit + Display only the first pull requests. + + -u, --url + Print the pull request URL instead of opening it. + + -c, --copy + Put the pull request URL to clipboard instead of opening it. ## See also: @@ -133,41 +153,39 @@ } cmdCheckoutPr = &Command{ - Key: "checkout", - Run: checkoutPr, + Key: "checkout", + Run: checkoutPr, + KnownFlags: "\n", } cmdListPulls = &Command{ - Key: "list", - Run: listPulls, + Key: "list", + Run: listPulls, + Long: cmdPr.Long, } - flagPullRequestState, - flagPullRequestFormat, - flagPullRequestSort string - - flagPullRequestSortAscending bool - - flagPullRequestLimit int + cmdShowPr = &Command{ + Key: "show", + Run: showPr, + KnownFlags: ` + -h, --head HEAD + -u, --url + -c, --copy + -f, --format FORMAT + --color +`, + } ) func init() { - cmdListPulls.Flag.StringVarP(&flagPullRequestState, "state", "s", "", "STATE") - cmdListPulls.Flag.StringVarP(&flagPullRequestBase, "base", "b", "", "BASE") - cmdListPulls.Flag.StringVarP(&flagPullRequestHead, "head", "h", "", "HEAD") - cmdListPulls.Flag.StringVarP(&flagPullRequestFormat, "format", "f", "%sC%>(8)%i%Creset %t% l%n", "FORMAT") - cmdListPulls.Flag.StringVarP(&flagPullRequestSort, "sort", "o", "created", "SORT_KEY") - cmdListPulls.Flag.BoolVarP(&flagPullRequestSortAscending, "sort-ascending", "^", false, "SORT_KEY") - cmdListPulls.Flag.IntVarP(&flagPullRequestLimit, "limit", "L", -1, "LIMIT") - cmdPr.Use(cmdListPulls) cmdPr.Use(cmdCheckoutPr) + cmdPr.Use(cmdShowPr) CmdRunner.Use(cmdPr) } func printHelp(command *Command, args *Args) { - fmt.Print(command.HelpText()) - os.Exit(0) + utils.Check(command.UsageError("")) } func listPulls(cmd *Command, args *Args) { @@ -185,23 +203,25 @@ return } - if flagPullRequestHead != "" && !strings.Contains(flagPullRequestHead, ":") { - flagPullRequestHead = fmt.Sprintf("%s:%s", project.Owner, flagPullRequestHead) + filters := map[string]interface{}{} + if args.Flag.HasReceived("--state") { + filters["state"] = args.Flag.Value("--state") } - - flagFilters := map[string]string{ - "state": flagPullRequestState, - "head": flagPullRequestHead, - "base": flagPullRequestBase, - "sort": flagPullRequestSort, + if args.Flag.HasReceived("--sort") { + filters["sort"] = args.Flag.Value("--sort") } - filters := map[string]interface{}{} - for flag, filter := range flagFilters { - if cmd.FlagPassed(flag) { - filters[flag] = filter + if args.Flag.HasReceived("--base") { + filters["base"] = args.Flag.Value("--base") + } + if args.Flag.HasReceived("--head") { + head := args.Flag.Value("--head") + if !strings.Contains(head, ":") { + head = fmt.Sprintf("%s:%s", project.Owner, head) } + filters["head"] = head } - if flagPullRequestSortAscending { + + if args.Flag.Bool("--sort-ascending") { filters["direction"] = "asc" } else { filters["direction"] = "desc" @@ -213,12 +233,18 @@ onlyMerged = true } + flagPullRequestLimit := args.Flag.Int("--limit") + flagPullRequestFormat := args.Flag.Value("--format") + if !args.Flag.HasReceived("--format") { + flagPullRequestFormat = "%pC%>(8)%i%Creset %t% l%n" + } + pulls, err := gh.FetchPullRequests(project, filters, flagPullRequestLimit, func(pr *github.PullRequest) bool { return !(onlyMerged && pr.MergedAt.IsZero()) }) utils.Check(err) - colorize := ui.IsTerminal(os.Stdout) + colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color")) for _, pr := range pulls { ui.Print(formatPullRequest(pr, flagPullRequestFormat, colorize)) } @@ -255,9 +281,144 @@ args.Replace(args.Executable, "checkout", newArgs...) } +func showPr(command *Command, args *Args) { + localRepo, err := github.LocalRepo() + utils.Check(err) + + baseProject, err := localRepo.MainProject() + utils.Check(err) + + host, err := github.CurrentConfig().PromptForHost(baseProject.Host) + utils.Check(err) + gh := github.NewClientWithHost(host) + + words := args.Words() + openUrl := "" + prNumber := 0 + var pr *github.PullRequest + + if len(words) > 0 { + if prNumber, err = strconv.Atoi(words[0]); err == nil { + openUrl = baseProject.WebURL("", "", fmt.Sprintf("pull/%d", prNumber)) + } else { + utils.Check(fmt.Errorf("invalid pull request number: '%s'", words[0])) + } + } else { + pr, err = findCurrentPullRequest(localRepo, gh, baseProject, args.Flag.Value("--head")) + utils.Check(err) + openUrl = pr.HtmlUrl + } + + args.NoForward() + if format := args.Flag.Value("--format"); format != "" { + if pr == nil { + pr, err = gh.PullRequest(baseProject, strconv.Itoa(prNumber)) + utils.Check(err) + } + colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color")) + ui.Println(formatPullRequest(*pr, format, colorize)) + return + } + + printUrl := args.Flag.Bool("--url") + copyUrl := args.Flag.Bool("--copy") + + printBrowseOrCopy(args, openUrl, !printUrl && !copyUrl, copyUrl) +} + +func findCurrentPullRequest(localRepo *github.GitHubRepo, gh *github.Client, baseProject *github.Project, headArg string) (*github.PullRequest, error) { + filterParams := map[string]interface{}{ + "state": "open", + } + headWithOwner := "" + + if headArg != "" { + headWithOwner = headArg + if !strings.Contains(headWithOwner, ":") { + headWithOwner = fmt.Sprintf("%s:%s", baseProject.Owner, headWithOwner) + } + } else { + currentBranch, err := localRepo.CurrentBranch() + utils.Check(err) + if headBranch, headProject, err := findPushTarget(currentBranch); err == nil { + headWithOwner = fmt.Sprintf("%s:%s", headProject.Owner, headBranch.ShortName()) + } else if headProject, err := deducePushTarget(currentBranch, gh.Host.User); err == nil { + headWithOwner = fmt.Sprintf("%s:%s", headProject.Owner, currentBranch.ShortName()) + } else { + headWithOwner = fmt.Sprintf("%s:%s", baseProject.Owner, currentBranch.ShortName()) + } + } + + filterParams["head"] = headWithOwner + + pulls, err := gh.FetchPullRequests(baseProject, filterParams, 1, nil) + if err != nil { + return nil, err + } else if len(pulls) == 1 { + return &pulls[0], nil + } else { + return nil, fmt.Errorf("no open pull requests found for branch '%s'", headWithOwner) + } +} + +func branchTrackingInformation(branch *github.Branch) (string, *github.Branch, error) { + branchRemote, err := git.Config(fmt.Sprintf("branch.%s.remote", branch.ShortName())) + if branchRemote == "." { + err = fmt.Errorf("branch is tracking another local branch") + } + if err != nil { + return "", nil, err + } + branchMerge, err := git.Config(fmt.Sprintf("branch.%s.merge", branch.ShortName())) + if err != nil { + return "", nil, err + } + trackingBranch := &github.Branch{ + Repo: branch.Repo, + Name: branchMerge, + } + return branchRemote, trackingBranch, nil +} + +func findPushTarget(branch *github.Branch) (*github.Branch, *github.Project, error) { + branchRemote, headBranch, err := branchTrackingInformation(branch) + if err != nil { + return nil, nil, err + } + + if headRemote, err := branch.Repo.RemoteByName(branchRemote); err == nil { + headProject, err := headRemote.Project() + if err != nil { + return nil, nil, err + } + return headBranch, headProject, nil + } + + remoteUrl, err := git.ParseURL(branchRemote) + if err != nil { + return nil, nil, err + } + headProject, err := github.NewProjectFromURL(remoteUrl) + if err != nil { + return nil, nil, err + } + return headBranch, headProject, nil +} + +func deducePushTarget(branch *github.Branch, owner string) (*github.Project, error) { + remote := branch.Repo.RemoteForBranch(branch, owner) + if remote == nil { + return nil, fmt.Errorf("no remote found for branch %s", branch.ShortName()) + } + return remote.Project() +} + func formatPullRequest(pr github.PullRequest, format string, colorize bool) string { placeholders := formatIssuePlaceholders(github.Issue(pr), colorize) - for key, value := range formatPullRequestPlaceholders(pr) { + delete(placeholders, "NC") + delete(placeholders, "Nc") + + for key, value := range formatPullRequestPlaceholders(pr, colorize) { placeholders[key] = value } return ui.Expand(format, placeholders, colorize) diff -Nru hub-2.7.0~ds1/commands/pull_request.go hub-2.14.2~ds1/commands/pull_request.go --- hub-2.7.0~ds1/commands/pull_request.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/pull_request.go 2020-03-05 17:48:23.000000000 +0000 @@ -16,42 +16,47 @@ var cmdPullRequest = &Command{ Run: pullRequest, Usage: ` -pull-request [-focp] [-b ] [-h ] [-r ] [-a ] [-M ] [-l ] +pull-request [-focpd] [-b ] [-h ] [-r ] [-a ] [-M ] [-l ] pull-request -m [--edit] pull-request -F [--edit] pull-request -i `, - Long: `Create a GitHub pull request. + Long: `Create a GitHub Pull Request. ## Options: -f, --force Skip the check for unpushed commits. - -m, --message= + -m, --message The text up to the first blank line in is treated as the pull request title, and the rest is used as pull request description in Markdown format. - If multiple options are given, their values are concatenated as - separate paragraphs. + When multiple ''--message'' are passed, their values are concatenated with a + blank line in-between. + + When neither ''--message'' nor ''--file'' were supplied, a text editor will open + to author the title and description in. --no-edit Use the message from the first commit on the branch as pull request title and description without opening a text editor. - -F, --file= - Read the pull request title and description from . + -F, --file + Read the pull request title and description from . Pass "-" to read + from standard input instead. See ''--message'' for the formatting rules. -e, --edit - Further edit the contents of in a text editor before submitting. + Open the pull request title and description in a text editor before + submitting. This can be used in combination with ''--message'' or ''--file''. - -i, --issue= - Convert an issue to a pull request. may be an issue number or a URL. + -i, --issue + Convert (referenced by its number) to a pull request. - You can only convert issues that you've opened or that which you have admin - rights over. In most workflows it isn't necessary to convert issues to pull - requests; you can simply reference the original issue in the body of the new - pull request. + You can only convert issues authored by you or that which you have admin + rights over. In most workflows it is not necessary to convert issues to + pull requests; you can simply reference the original issue in the body of + the new pull request. -o, --browse Open the new pull request in a web browser. @@ -62,30 +67,40 @@ -p, --push Push the current branch to before creating the pull request. - -b, --base= + -b, --base The base branch in the "[:]" format. Defaults to the default branch of the upstream repository (usually "master"). See the "CONVENTIONS" section of hub(1) for more information on how hub selects the defaults in case of multiple git remotes. - -h, --head= + -h, --head The head branch in "[:]" format. Defaults to the currently checked out branch. - -r, --reviewer= - A comma-separated list of GitHub handles to request a review from. - - -a, --assign= - A comma-separated list of GitHub handles to assign to this pull request. + -r, --reviewer + A comma-separated list (no spaces around the comma) of GitHub handles to + request a review from. + + -a, --assign + A comma-separated list (no spaces around the comma) of GitHub handles to + assign to this pull request. - -M, --milestone= + -M, --milestone The milestone name to add to this pull request. Passing the milestone number is deprecated. - -l, --labels= - Add a comma-separated list of labels to this pull request. Labels will be - created if they don't already exist. + -l, --labels + A comma-separated list (no spaces around the comma) of labels to add to + this pull request. Labels will be created if they do not already exist. + + -d, --draft + Create the pull request as a draft. + + --no-maintainer-edits + When creating a pull request from a fork, this disallows projects + maintainers from being able to push to the head branch of this fork. + Maintainer edits are allowed by default. ## Examples: $ hub pull-request @@ -103,8 +118,8 @@ ## Configuration: - HUB_RETRY_TIMEOUT= - The maximum time to keep retrying after HTTP 422 on '--push' (default: 9). + * ''HUB_RETRY_TIMEOUT'': + The maximum time to keep retrying after HTTP 422 on ''--push'' (default: 9). ## See also: @@ -112,44 +127,7 @@ `, } -var ( - flagPullRequestBase, - flagPullRequestHead, - flagPullRequestIssue, - flagPullRequestMilestone, - flagPullRequestFile string - - flagPullRequestMessage messageBlocks - - flagPullRequestBrowse, - flagPullRequestCopy, - flagPullRequestEdit, - flagPullRequestPush, - flagPullRequestForce, - flagPullRequestNoEdit bool - - flagPullRequestAssignees, - flagPullRequestReviewers, - flagPullRequestLabels listFlag -) - func init() { - cmdPullRequest.Flag.StringVarP(&flagPullRequestBase, "base", "b", "", "BASE") - cmdPullRequest.Flag.StringVarP(&flagPullRequestHead, "head", "h", "", "HEAD") - cmdPullRequest.Flag.StringVarP(&flagPullRequestIssue, "issue", "i", "", "ISSUE") - cmdPullRequest.Flag.BoolVarP(&flagPullRequestBrowse, "browse", "o", false, "BROWSE") - cmdPullRequest.Flag.BoolVarP(&flagPullRequestCopy, "copy", "c", false, "COPY") - cmdPullRequest.Flag.VarP(&flagPullRequestMessage, "message", "m", "MESSAGE") - cmdPullRequest.Flag.BoolVarP(&flagPullRequestEdit, "edit", "e", false, "EDIT") - cmdPullRequest.Flag.BoolVarP(&flagPullRequestPush, "push", "p", false, "PUSH") - cmdPullRequest.Flag.BoolVarP(&flagPullRequestForce, "force", "f", false, "FORCE") - cmdPullRequest.Flag.BoolVarP(&flagPullRequestNoEdit, "no-edit", "", false, "NO-EDIT") - cmdPullRequest.Flag.StringVarP(&flagPullRequestFile, "file", "F", "", "FILE") - cmdPullRequest.Flag.VarP(&flagPullRequestAssignees, "assign", "a", "USERS") - cmdPullRequest.Flag.VarP(&flagPullRequestReviewers, "reviewer", "r", "USERS") - cmdPullRequest.Flag.StringVarP(&flagPullRequestMilestone, "milestone", "M", "", "MILESTONE") - cmdPullRequest.Flag.VarP(&flagPullRequestLabels, "labels", "l", "LABELS") - CmdRunner.Use(cmdPullRequest) } @@ -157,8 +135,7 @@ localRepo, err := github.LocalRepo() utils.Check(err) - currentBranch, err := localRepo.CurrentBranch() - utils.Check(err) + currentBranch, currentBranchErr := localRepo.CurrentBranch() baseProject, err := localRepo.MainProject() utils.Check(err) @@ -169,29 +146,23 @@ } client := github.NewClientWithHost(host) - trackedBranch, headProject, err := localRepo.RemoteBranchAndProject(host.User, false) - utils.Check(err) + trackedBranch, headProject, _ := localRepo.RemoteBranchAndProject(host.User, false) + if headProject == nil { + utils.Check(fmt.Errorf("could not determine project for head branch")) + } var ( base, head string - force bool ) - force = flagPullRequestForce - - if flagPullRequestBase != "" { + if flagPullRequestBase := args.Flag.Value("--base"); flagPullRequestBase != "" { baseProject, base = parsePullRequestProject(baseProject, flagPullRequestBase) } - if flagPullRequestHead != "" { + if flagPullRequestHead := args.Flag.Value("--head"); flagPullRequestHead != "" { headProject, head = parsePullRequestProject(headProject, flagPullRequestHead) } - if args.ParamsSize() == 1 { - arg := args.RemoveParam(0) - flagPullRequestIssue = parsePullRequestIssueNumber(arg) - } - baseRemote, _ := localRepo.RemoteForProject(baseProject) if base == "" && baseRemote != nil { base = localRepo.DefaultBranch(baseRemote).ShortName() @@ -211,8 +182,22 @@ } } + force := args.Flag.Bool("--force") + flagPullRequestPush := args.Flag.Bool("--push") + if head == "" { if trackedBranch == nil { + utils.Check(currentBranchErr) + if !force && !flagPullRequestPush { + branchRemote, branchMerge, err := branchTrackingInformation(currentBranch) + if err != nil || (baseRemote != nil && branchRemote == baseRemote.Name && branchMerge.ShortName() == base) { + if localRepo.RemoteForBranch(currentBranch, host.User) == nil { + err = fmt.Errorf("Aborted: the current branch seems not yet pushed to a remote") + err = fmt.Errorf("%s\n(use `-p` to push the branch or `-f` to skip this check)", err) + utils.Check(err) + } + } + } head = currentBranch.ShortName() } else { head = trackedBranch.ShortName() @@ -228,8 +213,8 @@ fullHead := fmt.Sprintf("%s:%s", headProject.Owner, head) if !force && trackedBranch != nil { - remoteCommits, _ := git.RefList(trackedBranch.LongName(), "") - if len(remoteCommits) > 0 { + remoteCommits, err := git.RefList(trackedBranch.LongName(), "") + if err == nil && len(remoteCommits) > 0 { err = fmt.Errorf("Aborted: %d commits are not yet pushed to %s", len(remoteCommits), trackedBranch.LongName()) err = fmt.Errorf("%s\n(use `-f` to force submit a pull request anyway)", err) utils.Check(err) @@ -264,14 +249,21 @@ Write a message for this pull request. The first block of text is the title and the rest is the description.`, fullBase, fullHead)) + flagPullRequestMessage := args.Flag.AllValues("--message") + flagPullRequestEdit := args.Flag.Bool("--edit") + flagPullRequestIssue := args.Flag.Value("--issue") + if !args.Flag.HasReceived("--issue") && args.ParamsSize() > 0 { + flagPullRequestIssue = parsePullRequestIssueNumber(args.GetParam(0)) + } + if len(flagPullRequestMessage) > 0 { - messageBuilder.Message = flagPullRequestMessage.String() + messageBuilder.Message = strings.Join(flagPullRequestMessage, "\n\n") messageBuilder.Edit = flagPullRequestEdit - } else if cmd.FlagPassed("file") { - messageBuilder.Message, err = msgFromFile(flagPullRequestFile) + } else if args.Flag.HasReceived("--file") { + messageBuilder.Message, err = msgFromFile(args.Flag.Value("--file")) utils.Check(err) messageBuilder.Edit = flagPullRequestEdit - } else if flagPullRequestNoEdit { + } else if args.Flag.Bool("--no-edit") { commits, _ := git.RefList(baseTracking, head) if len(commits) == 0 { utils.Check(fmt.Errorf("Aborted: no commits detected between %s and %s", baseTracking, head)) @@ -288,22 +280,21 @@ } message := "" - commitLogs := "" commits, _ := git.RefList(baseTracking, headForMessage) if len(commits) == 1 { message, err = git.Show(commits[0]) utils.Check(err) - re := regexp.MustCompile(`\nSigned-off-by:\s.*$`) + re := regexp.MustCompile(`\n(Co-authored-by|Signed-off-by):[^\n]+`) message = re.ReplaceAllString(message, "") } else if len(commits) > 1 { - commitLogs, err = git.Log(baseTracking, headForMessage) + commitLogs, err := git.Log(baseTracking, headForMessage) utils.Check(err) - } - if commitLogs != "" { - messageBuilder.AddCommentedSection("\nChanges:\n\n" + strings.TrimSpace(commitLogs)) + if commitLogs != "" { + messageBuilder.AddCommentedSection("\nChanges:\n\n" + strings.TrimSpace(commitLogs)) + } } workdir, _ := git.WorkdirName() @@ -333,17 +324,8 @@ } } - milestoneNumber := 0 - if flagPullRequestMilestone != "" { - // BC: Don't try to resolve milestone name if it's an integer - milestoneNumber, err = strconv.Atoi(flagPullRequestMilestone) - if err != nil { - milestones, err := client.FetchMilestones(baseProject) - utils.Check(err) - milestoneNumber, err = findMilestoneNumber(milestones, flagPullRequestMilestone) - utils.Check(err) - } - } + milestoneNumber, err := milestoneValueToNumber(args.Flag.Value("--milestone"), client, baseProject) + utils.Check(err) var pullRequestURL string if args.Noop { @@ -351,8 +333,13 @@ pullRequestURL = "PULL_REQUEST_URL" } else { params := map[string]interface{}{ - "base": base, - "head": fullHead, + "base": base, + "head": fullHead, + "maintainer_can_modify": !args.Flag.Bool("--no-maintainer-edits"), + } + + if args.Flag.Bool("--draft") { + params["draft"] = true } if title != "" { @@ -389,7 +376,7 @@ numRetries += 1 } else { if numRetries > 0 { - duration := time.Now().Sub(startedAt) + duration := time.Since(startedAt) err = fmt.Errorf("%s\nGiven up after retrying for %.1f seconds.", err, duration.Seconds()) } break @@ -408,9 +395,11 @@ pullRequestURL = pr.HtmlUrl params = map[string]interface{}{} + flagPullRequestLabels := commaSeparated(args.Flag.AllValues("--labels")) if len(flagPullRequestLabels) > 0 { params["labels"] = flagPullRequestLabels } + flagPullRequestAssignees := commaSeparated(args.Flag.AllValues("--assign")) if len(flagPullRequestAssignees) > 0 { params["assignees"] = flagPullRequestAssignees } @@ -423,6 +412,7 @@ utils.Check(err) } + flagPullRequestReviewers := commaSeparated(args.Flag.AllValues("--reviewer")) if len(flagPullRequestReviewers) > 0 { userReviewers := []string{} teamReviewers := []string{} @@ -447,7 +437,7 @@ } args.NoForward() - printBrowseOrCopy(args, pullRequestURL, flagPullRequestBrowse, flagPullRequestCopy) + printBrowseOrCopy(args, pullRequestURL, args.Flag.Bool("--browse"), args.Flag.Bool("--copy")) } func parsePullRequestProject(context *github.Project, s string) (p *github.Project, ref string) { @@ -482,12 +472,13 @@ return "" } -func findMilestoneNumber(milestones []github.Milestone, name string) (int, error) { - for _, milestone := range milestones { - if strings.EqualFold(milestone.Title, name) { - return milestone.Number, nil +func commaSeparated(l []string) []string { + res := []string{} + for _, i := range l { + if i == "" { + continue } + res = append(res, strings.Split(i, ",")...) } - - return 0, fmt.Errorf("error: no milestone found with name '%s'", name) + return res } diff -Nru hub-2.7.0~ds1/commands/release.go hub-2.14.2~ds1/commands/release.go --- hub-2.7.0~ds1/commands/release.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/release.go 2020-03-05 17:48:23.000000000 +0000 @@ -21,42 +21,46 @@ release show [-f ] release create [-dpoc] [-a ] [-m |-F ] [-t ] release edit [] -release download +release download [-i ] release delete `, - Long: `Manage GitHub releases. + Long: `Manage GitHub Releases for the current repository. ## Commands: With no arguments, shows a list of existing releases. -With '--include-drafts', include draft releases in the listing. -With '--exclude-prereleases', exclude non-stable releases from the listing. - * _show_: Show GitHub release notes for . - With '--show-downloads', include the "Downloads" section. + With ''--show-downloads'', include the "Downloads" section. * _create_: Create a GitHub release for the specified name. If git tag - doesn't exist, it will be created at (default: current branch). + does not exist, it will be created at (default: current branch). * _edit_: Edit the GitHub release for the specified name. Accepts the same - options as _create_ command. Publish a draft with '--draft=false'. + options as _create_ command. Publish a draft with ''--draft=false''. - When or are not specified, a text editor will open - pre-populated with current release title and body. To re-use existing title - and body unchanged, pass '-m ""'. + Without ''--message'' or ''--file'', a text editor will open pre-populated with + the current release title and body. To re-use existing title and body + unchanged, pass ''-m ""''. * _download_: - Download the assets attached to release for the specified . + Download the assets attached to release for the specified . * _delete_: - Delete the release and associated assets for the specified . + Delete the release and associated assets for the specified . Note that + this does **not** remove the git tag . ## Options: + -d, --include-drafts + List drafts together with published releases. + + -p, --exclude-prereleases + Exclude prereleases from the list. + -L, --limit Display only the first releases. @@ -66,24 +70,29 @@ -p, --prerelease Create a pre-release. - -a, --attach= + -a, --attach Attach a file as an asset for this release. - If is in the "#" format, the text after the '#' + If is in the "#" format, the text after the "#" character is taken as asset label. - -m, --message= + -m, --message The text up to the first blank line in is treated as the release title, and the rest is used as release description in Markdown format. - If multiple options are given, their values are concatenated as - separate paragraphs. + When multiple ''--message'' are passed, their values are concatenated with a + blank line in-between. - -F, --file= - Read the release title and description from . + When neither ''--message'' nor ''--file'' were supplied to ''release create'', a + text editor will open to author the title and description in. + + -F, --file + Read the release title and description from . Pass "-" to read from + standard input instead. See ''--message'' for the formatting rules. -e, --edit - Further edit the contents of in a text editor before submitting. + Open the release title and description in a text editor before submitting. + This can be used in combination with ''--message'' or ''--file''. -o, --browse Open the new release in a web browser. @@ -91,11 +100,14 @@ -c, --copy Put the URL of the new release to clipboard instead of printing it. - -t, --commitish= + -t, --commitish A commit SHA or branch name to attach the release to, only used if - doesn't already exist (default: main branch). + does not already exist (default: main branch). + + -i, --include + Filter the files in the release to those that match the glob . - -f, --format= + -f, --format Pretty print releases using (default: "%T%n"). See the "PRETTY FORMATS" section of git-log(1) for some additional details on how placeholders are used in format. The available placeholders for issues are: @@ -140,88 +152,81 @@ %%: a literal % + --color[=] + Enable colored output even if stdout is not a terminal. can be one + of "always" (default for ''--color''), "never", or "auto" (default). + The git tag name for this release. ## See also: hub(1), git-tag(1) - `, +`, + KnownFlags: ` + -d, --include-drafts + -p, --exclude-prereleases + -L, --limit N + -f, --format FMT + --color +`, } cmdShowRelease = &Command{ Key: "show", Run: showRelease, + KnownFlags: ` + -d, --show-downloads + -f, --format FMT + --color +`, } cmdCreateRelease = &Command{ Key: "create", Run: createRelease, + KnownFlags: ` + -e, --edit + -d, --draft + -p, --prerelease + -o, --browse + -c, --copy + -a, --attach FILE + -m, --message MSG + -F, --file FILE + -t, --commitish C +`, } cmdEditRelease = &Command{ Key: "edit", Run: editRelease, + KnownFlags: ` + -e, --edit + -d, --draft + -p, --prerelease + -a, --attach FILE + -m, --message MSG + -F, --file FILE + -t, --commitish C +`, } cmdDownloadRelease = &Command{ Key: "download", Run: downloadRelease, + KnownFlags: ` + -i, --include PATTERN + `, } cmdDeleteRelease = &Command{ Key: "delete", Run: deleteRelease, } - - flagReleaseIncludeDrafts, - flagReleaseExcludePrereleases, - flagReleaseShowDownloads, - flagReleaseDraft, - flagReleaseEdit, - flagReleaseBrowse, - flagReleaseCopy, - flagReleasePrerelease bool - - flagReleaseFile, - flagReleaseFormat, - flagShowReleaseFormat, - flagReleaseCommitish string - - flagReleaseMessage messageBlocks - - flagReleaseAssets stringSliceValue - - flagReleaseLimit int ) func init() { - cmdRelease.Flag.BoolVarP(&flagReleaseIncludeDrafts, "include-drafts", "d", false, "DRAFTS") - cmdRelease.Flag.BoolVarP(&flagReleaseExcludePrereleases, "exclude-prereleases", "p", false, "PRERELEASE") - cmdRelease.Flag.IntVarP(&flagReleaseLimit, "limit", "L", -1, "LIMIT") - cmdRelease.Flag.StringVarP(&flagReleaseFormat, "format", "f", "%T%n", "FORMAT") - - cmdShowRelease.Flag.BoolVarP(&flagReleaseShowDownloads, "show-downloads", "d", false, "DRAFTS") - cmdShowRelease.Flag.StringVarP(&flagShowReleaseFormat, "format", "f", "", "FORMAT") - - cmdCreateRelease.Flag.BoolVarP(&flagReleaseEdit, "edit", "e", false, "EDIT") - cmdCreateRelease.Flag.BoolVarP(&flagReleaseDraft, "draft", "d", false, "DRAFT") - cmdCreateRelease.Flag.BoolVarP(&flagReleasePrerelease, "prerelease", "p", false, "PRERELEASE") - cmdCreateRelease.Flag.BoolVarP(&flagReleaseBrowse, "browse", "o", false, "BROWSE") - cmdCreateRelease.Flag.BoolVarP(&flagReleaseCopy, "copy", "c", false, "COPY") - cmdCreateRelease.Flag.VarP(&flagReleaseAssets, "attach", "a", "ATTACH_ASSETS") - cmdCreateRelease.Flag.VarP(&flagReleaseMessage, "message", "m", "MESSAGE") - cmdCreateRelease.Flag.StringVarP(&flagReleaseFile, "file", "F", "", "FILE") - cmdCreateRelease.Flag.StringVarP(&flagReleaseCommitish, "commitish", "t", "", "COMMITISH") - - cmdEditRelease.Flag.BoolVarP(&flagReleaseEdit, "edit", "e", false, "EDIT") - cmdEditRelease.Flag.BoolVarP(&flagReleaseDraft, "draft", "d", false, "DRAFT") - cmdEditRelease.Flag.BoolVarP(&flagReleasePrerelease, "prerelease", "p", false, "PRERELEASE") - cmdEditRelease.Flag.VarP(&flagReleaseAssets, "attach", "a", "ATTACH_ASSETS") - cmdEditRelease.Flag.VarP(&flagReleaseMessage, "message", "m", "MESSAGE") - cmdEditRelease.Flag.StringVarP(&flagReleaseFile, "file", "F", "", "FILE") - cmdEditRelease.Flag.StringVarP(&flagReleaseCommitish, "commitish", "t", "", "COMMITISH") - cmdRelease.Use(cmdShowRelease) cmdRelease.Use(cmdCreateRelease) cmdRelease.Use(cmdEditRelease) @@ -239,6 +244,10 @@ gh := github.NewClient(project.Host) + flagReleaseLimit := args.Flag.Int("--limit") + flagReleaseIncludeDrafts := args.Flag.Bool("--include-drafts") + flagReleaseExcludePrereleases := args.Flag.Bool("--exclude-prereleases") + if args.Noop { ui.Printf("Would request list of releases for %s\n", project) } else { @@ -248,8 +257,12 @@ }) utils.Check(err) - colorize := ui.IsTerminal(os.Stdout) + colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color")) for _, release := range releases { + flagReleaseFormat := "%T%n" + if args.Flag.HasReceived("--format") { + flagReleaseFormat = args.Flag.Value("--format") + } ui.Print(formatRelease(release, flagReleaseFormat, colorize)) } } @@ -313,9 +326,12 @@ } func showRelease(cmd *Command, args *Args) { - tagName := cmd.Arg(0) + tagName := "" + if args.ParamsSize() > 0 { + tagName = args.GetParam(0) + } if tagName == "" { - utils.Check(fmt.Errorf(cmdRelease.Synopsis())) + utils.Check(cmd.UsageError("")) } localRepo, err := github.LocalRepo() @@ -336,8 +352,8 @@ body := strings.TrimSpace(release.Body) - colorize := ui.IsTerminal(os.Stdout) - if flagShowReleaseFormat != "" { + colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color")) + if flagShowReleaseFormat := args.Flag.Value("--format"); flagShowReleaseFormat != "" { ui.Print(formatRelease(*release, flagShowReleaseFormat, colorize)) return } @@ -346,7 +362,7 @@ if body != "" { ui.Printf("\n%s\n", body) } - if flagReleaseShowDownloads { + if args.Flag.Bool("--show-downloads") { ui.Printf("\n## Downloads\n\n") for _, asset := range release.Assets { ui.Println(asset.DownloadUrl) @@ -360,9 +376,12 @@ } func downloadRelease(cmd *Command, args *Args) { - tagName := cmd.Arg(0) + tagName := "" + if args.ParamsSize() > 0 { + tagName = args.GetParam(0) + } if tagName == "" { - utils.Check(fmt.Errorf(cmdRelease.Synopsis())) + utils.Check(cmd.UsageError("")) } localRepo, err := github.LocalRepo() @@ -376,12 +395,31 @@ release, err := gh.FetchRelease(project, tagName) utils.Check(err) + hasPattern := args.Flag.HasReceived("--include") + found := false for _, asset := range release.Assets { + if hasPattern { + isMatch, err := filepath.Match(args.Flag.Value("--include"), asset.Name) + utils.Check(err) + if !isMatch { + continue + } + } + + found = true ui.Printf("Downloading %s ...\n", asset.Name) err := downloadReleaseAsset(asset, gh) utils.Check(err) } + if !found && hasPattern { + names := []string{} + for _, asset := range release.Assets { + names = append(names, asset.Name) + } + utils.Check(fmt.Errorf("the `--include` pattern did not match any available assets:\n%s", strings.Join(names, "\n"))) + } + args.NoForward() } @@ -406,12 +444,19 @@ } func createRelease(cmd *Command, args *Args) { - tagName := cmd.Arg(0) + tagName := "" + if args.ParamsSize() > 0 { + tagName = args.GetParam(0) + } if tagName == "" { - utils.Check(fmt.Errorf(cmdRelease.Synopsis())) + utils.Check(cmd.UsageError("")) return } + assetsToUpload, close, err := openAssetFiles(args.Flag.AllValues("--attach")) + utils.Check(err) + defer close() + localRepo, err := github.LocalRepo() utils.Check(err) @@ -430,13 +475,14 @@ Write a message for this release. The first block of text is the title and the rest is the description.`, tagName, project)) + flagReleaseMessage := args.Flag.AllValues("--message") if len(flagReleaseMessage) > 0 { - messageBuilder.Message = flagReleaseMessage.String() - messageBuilder.Edit = flagReleaseEdit - } else if cmd.FlagPassed("file") { - messageBuilder.Message, err = msgFromFile(flagReleaseFile) + messageBuilder.Message = strings.Join(flagReleaseMessage, "\n\n") + messageBuilder.Edit = args.Flag.Bool("--edit") + } else if args.Flag.HasReceived("--file") { + messageBuilder.Message, err = msgFromFile(args.Flag.Value("--file")) utils.Check(err) - messageBuilder.Edit = flagReleaseEdit + messageBuilder.Edit = args.Flag.Bool("--edit") } else { messageBuilder.Edit = true } @@ -450,11 +496,11 @@ params := &github.Release{ TagName: tagName, - TargetCommitish: flagReleaseCommitish, + TargetCommitish: args.Flag.Value("--commitish"), Name: title, Body: body, - Draft: flagReleaseDraft, - Prerelease: flagReleasePrerelease, + Draft: args.Flag.Bool("--draft"), + Prerelease: args.Flag.Bool("--prerelease"), } var release *github.Release @@ -466,21 +512,48 @@ release, err = gh.CreateRelease(project, params) utils.Check(err) + flagReleaseBrowse := args.Flag.Bool("--browse") + flagReleaseCopy := args.Flag.Bool("--copy") printBrowseOrCopy(args, release.HtmlUrl, flagReleaseBrowse, flagReleaseCopy) } messageBuilder.Cleanup() - uploadAssets(gh, release, flagReleaseAssets, args) + numAssets := len(assetsToUpload) + if numAssets == 0 { + return + } + if args.Noop { + ui.Printf("Would attach %d %s\n", numAssets, pluralize(numAssets, "asset")) + } else { + ui.Errorf("Attaching %d %s...\n", numAssets, pluralize(numAssets, "asset")) + uploaded, err := gh.UploadReleaseAssets(release, assetsToUpload) + if err != nil { + failed := []string{} + for _, a := range assetsToUpload[len(uploaded):] { + failed = append(failed, fmt.Sprintf("-a %s", a.Name)) + } + ui.Errorf("The release was created, but attaching %d %s failed. ", len(failed), pluralize(len(failed), "asset")) + ui.Errorf("You can retry with:\n%s release edit %s -m '' %s\n\n", "hub", release.TagName, strings.Join(failed, " ")) + utils.Check(err) + } + } } func editRelease(cmd *Command, args *Args) { - tagName := cmd.Arg(0) + tagName := "" + if args.ParamsSize() > 0 { + tagName = args.GetParam(0) + } if tagName == "" { - utils.Check(fmt.Errorf(cmdRelease.Synopsis())) + utils.Check(cmd.UsageError("")) return } + assetsToUpload, close, err := openAssetFiles(args.Flag.AllValues("--attach")) + utils.Check(err) + defer close() + localRepo, err := github.LocalRepo() utils.Check(err) @@ -493,17 +566,14 @@ utils.Check(err) params := map[string]interface{}{} - - if cmd.FlagPassed("commitish") { - params["target_commitish"] = flagReleaseCommitish + if args.Flag.HasReceived("--commitish") { + params["target_commitish"] = args.Flag.Value("--commitish") } - - if cmd.FlagPassed("draft") { - params["draft"] = flagReleaseDraft + if args.Flag.HasReceived("--draft") { + params["draft"] = args.Flag.Bool("--draft") } - - if cmd.FlagPassed("prerelease") { - params["prerelease"] = flagReleasePrerelease + if args.Flag.HasReceived("--prerelease") { + params["prerelease"] = args.Flag.Bool("--prerelease") } messageBuilder := &github.MessageBuilder{ @@ -516,22 +586,23 @@ Write a message for this release. The first block of text is the title and the rest is the description.`, tagName, project)) + flagReleaseMessage := args.Flag.AllValues("--message") if len(flagReleaseMessage) > 0 { - messageBuilder.Message = flagReleaseMessage.String() - messageBuilder.Edit = flagReleaseEdit - } else if cmd.FlagPassed("file") { - messageBuilder.Message, err = msgFromFile(flagReleaseFile) + messageBuilder.Message = strings.Join(flagReleaseMessage, "\n\n") + messageBuilder.Edit = args.Flag.Bool("--edit") + } else if args.Flag.HasReceived("--file") { + messageBuilder.Message, err = msgFromFile(args.Flag.Value("--file")) utils.Check(err) - messageBuilder.Edit = flagReleaseEdit + messageBuilder.Edit = args.Flag.Bool("--edit") } else { messageBuilder.Edit = true - messageBuilder.Message = fmt.Sprintf("%s\n\n%s", release.Name, release.Body) + messageBuilder.Message = strings.Replace(fmt.Sprintf("%s\n\n%s", release.Name, release.Body), "\r\n", "\n", -1) } title, body, err := messageBuilder.Extract() utils.Check(err) - if title == "" && !cmd.FlagPassed("message") { + if title == "" && len(flagReleaseMessage) == 0 { utils.Check(fmt.Errorf("Aborting editing due to empty release title")) } @@ -542,6 +613,7 @@ params["body"] = body } + args.NoForward() if len(params) > 0 { if args.Noop { ui.Printf("Would edit release `%s'\n", tagName) @@ -553,14 +625,33 @@ messageBuilder.Cleanup() } - uploadAssets(gh, release, flagReleaseAssets, args) - args.NoForward() + numAssets := len(assetsToUpload) + if numAssets == 0 { + return + } + if args.Noop { + ui.Printf("Would attach %d %s\n", numAssets, pluralize(numAssets, "asset")) + } else { + ui.Errorf("Attaching %d %s...\n", numAssets, pluralize(numAssets, "asset")) + uploaded, err := gh.UploadReleaseAssets(release, assetsToUpload) + if err != nil { + failed := []string{} + for _, a := range assetsToUpload[len(uploaded):] { + failed = append(failed, a.Name) + } + ui.Errorf("Attaching these assets failed:\n%s\n\n", strings.Join(failed, "\n")) + utils.Check(err) + } + } } func deleteRelease(cmd *Command, args *Args) { - tagName := cmd.Arg(0) + tagName := "" + if args.ParamsSize() > 0 { + tagName = args.GetParam(0) + } if tagName == "" { - utils.Check(fmt.Errorf(cmdRelease.Synopsis())) + utils.Check(cmd.UsageError("")) return } @@ -586,32 +677,48 @@ args.NoForward() } -func uploadAssets(gh *github.Client, release *github.Release, assets []string, args *Args) { - for _, asset := range assets { +func openAssetFiles(args []string) ([]github.LocalAsset, func(), error) { + assets := []github.LocalAsset{} + files := []*os.File{} + + for _, arg := range args { var label string - parts := strings.SplitN(asset, "#", 2) - asset = parts[0] + parts := strings.SplitN(arg, "#", 2) + path := parts[0] if len(parts) > 1 { label = parts[1] } - if args.Noop { - if label == "" { - ui.Errorf("Would attach release asset `%s'\n", asset) - } else { - ui.Errorf("Would attach release asset `%s' with label `%s'\n", asset, label) - } - } else { - for _, existingAsset := range release.Assets { - if existingAsset.Name == filepath.Base(asset) { - err := gh.DeleteReleaseAsset(&existingAsset) - utils.Check(err) - break - } - } - ui.Errorf("Attaching release asset `%s'...\n", asset) - _, err := gh.UploadReleaseAsset(release, asset, label) - utils.Check(err) + file, err := os.Open(path) + if err != nil { + return nil, nil, err } + stat, err := file.Stat() + if err != nil { + return nil, nil, err + } + files = append(files, file) + + assets = append(assets, github.LocalAsset{ + Name: path, + Label: label, + Size: stat.Size(), + Contents: file, + }) + } + + close := func() { + for _, f := range files { + f.Close() + } + } + + return assets, close, nil +} + +func pluralize(count int, label string) string { + if count == 1 { + return label } + return fmt.Sprintf("%ss", label) } diff -Nru hub-2.7.0~ds1/commands/remote.go hub-2.14.2~ds1/commands/remote.go --- hub-2.7.0~ds1/commands/remote.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/remote.go 2020-03-05 17:48:23.000000000 +0000 @@ -21,8 +21,8 @@ ## Options: -p - (Deprecated) Use the 'ssh:' protocol instead of 'git:' for the remote URL. - The writeable 'ssh:' protocol is automatically used for own repos, GitHub + (Deprecated) Use the ''ssh:'' protocol instead of ''git:'' for the remote URL. + The writeable ''ssh:'' protocol is automatically used for own repos, GitHub Enterprise remotes, and private or pushable repositories. [/] @@ -56,10 +56,17 @@ func transformRemoteArgs(args *Args) { ownerWithName := args.LastParam() - owner, name := parseRepoNameOwner(ownerWithName) - if owner == "" { + + re := regexp.MustCompile(fmt.Sprintf(`^%s(/%s)?$`, OwnerRe, NameRe)) + if !re.MatchString(ownerWithName) { return } + owner := ownerWithName + name := "" + if strings.Contains(ownerWithName, "/") { + parts := strings.SplitN(ownerWithName, "/", 2) + owner, name = parts[0], parts[1] + } localRepo, err := github.LocalRepo() utils.Check(err) @@ -91,18 +98,22 @@ } host = hostConfig.Host - numWord := 0 - for i, p := range args.Params { - if !looksLikeFlag(p) && (i < 1 || args.Params[i-1] != "-t") { - numWord += 1 - if numWord == 2 && strings.Contains(p, "/") { - args.ReplaceParam(i, owner) - } else if numWord == 3 { - args.RemoveParam(i) - } + p := utils.NewArgsParser() + p.RegisterValue("-t") + p.RegisterValue("-m") + params, _ := p.Parse(args.Params) + if len(params) > 3 { + return + } + + for i, pi := range p.PositionalIndices { + if i == 1 && strings.Contains(params[i], "/") { + args.ReplaceParam(pi, owner) + } else if i == 2 { + args.RemoveParam(pi) } } - if numWord == 2 && owner == "origin" { + if len(params) == 2 && owner == "origin" { owner = hostConfig.User } @@ -137,13 +148,3 @@ return false } - -func parseRepoNameOwner(nameWithOwner string) (owner, name string) { - nameWithOwnerRe := fmt.Sprintf("^(%s)(?:\\/(%s))?$", OwnerRe, NameRe) - nameWithOwnerRegexp := regexp.MustCompile(nameWithOwnerRe) - if nameWithOwnerRegexp.MatchString(nameWithOwner) { - result := nameWithOwnerRegexp.FindStringSubmatch(nameWithOwner) - owner, name = result[1], result[2] - } - return -} diff -Nru hub-2.7.0~ds1/commands/remote_test.go hub-2.14.2~ds1/commands/remote_test.go --- hub-2.7.0~ds1/commands/remote_test.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/remote_test.go 2020-03-05 17:48:23.000000000 +0000 @@ -10,16 +10,6 @@ "github.com/github/hub/github" ) -func TestParseRepoNameOwner(t *testing.T) { - owner, repo := parseRepoNameOwner("jingweno") - assert.Equal(t, "jingweno", owner) - assert.Equal(t, "", repo) - - owner, repo = parseRepoNameOwner("jingweno/gh") - assert.Equal(t, "jingweno", owner) - assert.Equal(t, "gh", repo) -} - func TestTransformRemoteArgs(t *testing.T) { repo := fixtures.SetupTestRepo() defer repo.TearDown() diff -Nru hub-2.7.0~ds1/commands/runner.go hub-2.14.2~ds1/commands/runner.go --- hub-2.7.0~ds1/commands/runner.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/runner.go 2020-03-05 17:48:23.000000000 +0000 @@ -2,60 +2,21 @@ import ( "fmt" - "os" - "os/exec" "strings" - "syscall" "github.com/github/hub/cmd" "github.com/github/hub/git" "github.com/github/hub/ui" "github.com/kballard/go-shellquote" - flag "github.com/ogier/pflag" ) -type ExecError struct { - Err error - Ran bool - ExitCode int -} - -func (execError *ExecError) Error() string { - return execError.Err.Error() -} - -func newExecError(err error) ExecError { - exitCode := 0 - ran := true - - if err != nil { - exitCode = 1 - switch e := err.(type) { - case *exec.ExitError: - if status, ok := e.Sys().(syscall.WaitStatus); ok { - exitCode = status.ExitStatus() - } - case *exec.Error: - ran = false - } - } - - return ExecError{ - Err: err, - Ran: ran, - ExitCode: exitCode, - } -} - type Runner struct { commands map[string]*Command - execute func([]*cmd.Cmd, bool) error } func NewRunner() *Runner { return &Runner{ commands: make(map[string]*Command), - execute: executeCommands, } } @@ -74,9 +35,9 @@ return r.commands[name] } -func (r *Runner) Execute() ExecError { - args := NewArgs(os.Args[1:]) - args.ProgramPath = os.Args[0] +func (r *Runner) Execute(cliArgs []string) error { + args := NewArgs(cliArgs[1:]) + args.ProgramPath = cliArgs[0] forceFail := false if args.Command == "" && len(args.GlobalFlags) == 0 { @@ -95,48 +56,53 @@ cmdName = args.Command } + // make ` --help` equivalent to `help ` + if args.ParamsSize() == 1 && args.GetParam(0) == helpFlag { + if c := r.Lookup(cmdName); c != nil && !c.GitExtension { + args.ReplaceParam(0, cmdName) + args.Command = "help" + cmdName = args.Command + } + } + cmd := r.Lookup(cmdName) if cmd != nil && cmd.Runnable() { - execErr := r.Call(cmd, args) - if execErr.ExitCode == 0 && forceFail { - execErr = newExecError(fmt.Errorf("")) + err := callRunnableCommand(cmd, args) + if err == nil && forceFail { + err = fmt.Errorf("") } - return execErr + return err } - gitArgs := []string{args.Command} + gitArgs := []string{} + if args.Command != "" { + gitArgs = append(gitArgs, args.Command) + } gitArgs = append(gitArgs, args.Params...) - err := git.Run(gitArgs...) - return newExecError(err) + return git.Run(gitArgs...) } -func (r *Runner) Call(cmd *Command, args *Args) ExecError { +func callRunnableCommand(cmd *Command, args *Args) error { err := cmd.Call(args) if err != nil { - if err == flag.ErrHelp { - err = nil - } - return newExecError(err) + return err } cmds := args.Commands() if args.Noop { printCommands(cmds) - } else { - err = r.execute(cmds, len(args.Callbacks) == 0) + } else if err = executeCommands(cmds, len(args.Callbacks) == 0); err != nil { + return err } - if err == nil { - for _, fn := range args.Callbacks { - err = fn() - if err != nil { - break - } + for _, fn := range args.Callbacks { + if err = fn(); err != nil { + return err } } - return newExecError(err) + return nil } func printCommands(cmds []*cmd.Cmd) { @@ -165,6 +131,9 @@ func expandAlias(args *Args) { cmd := args.Command + if cmd == "" { + return + } expandedCmd, err := git.Alias(cmd) if err == nil && expandedCmd != "" && !git.IsBuiltInGitCommand(cmd) { @@ -177,7 +146,7 @@ } func isBuiltInHubCommand(command string) bool { - for hubCommand, _ := range CmdRunner.All() { + for hubCommand := range CmdRunner.All() { if hubCommand == command { return true } diff -Nru hub-2.7.0~ds1/commands/runner_test.go hub-2.14.2~ds1/commands/runner_test.go --- hub-2.7.0~ds1/commands/runner_test.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/runner_test.go 2020-03-05 17:48:23.000000000 +0000 @@ -4,49 +4,16 @@ "testing" "github.com/bmizerany/assert" - "github.com/github/hub/cmd" ) func TestRunner_splitAliasCmd(t *testing.T) { - words, err := splitAliasCmd("!source ~/.zshrc") + _, err := splitAliasCmd("!source ~/.zshrc") assert.NotEqual(t, nil, err) - words, err = splitAliasCmd("log --pretty=oneline --abbrev-commit --graph --decorate") + words, err := splitAliasCmd("log --pretty=oneline --abbrev-commit --graph --decorate") assert.Equal(t, nil, err) assert.Equal(t, 5, len(words)) - words, err = splitAliasCmd("") + _, err = splitAliasCmd("") assert.NotEqual(t, nil, err) } - -func TestRunnerUseCommands(t *testing.T) { - r := &Runner{ - commands: make(map[string]*Command), - execute: func([]*cmd.Cmd, bool) error { return nil }, - } - c := &Command{Usage: "foo"} - r.Use(c) - - assert.Equal(t, c, r.Lookup("foo")) -} - -func TestRunnerCallCommands(t *testing.T) { - var result string - f := func(c *Command, args *Args) { - result = args.FirstParam() - args.Replace("git", "version", "") - } - - r := &Runner{ - commands: make(map[string]*Command), - execute: func([]*cmd.Cmd, bool) error { return nil }, - } - c := &Command{Usage: "foo", Run: f} - r.Use(c) - - args := NewArgs([]string{"foo", "bar"}) - err := r.Call(c, args) - - assert.Equal(t, 0, err.ExitCode) - assert.Equal(t, "bar", result) -} diff -Nru hub-2.7.0~ds1/commands/sync.go hub-2.14.2~ds1/commands/sync.go --- hub-2.7.0~ds1/commands/sync.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/sync.go 2020-03-05 17:48:23.000000000 +0000 @@ -2,7 +2,6 @@ import ( "fmt" - "os" "regexp" "strings" @@ -14,16 +13,21 @@ var cmdSync = &Command{ Run: sync, - Usage: "sync", - Long: `Fetch git objects from upstream and update branches. + Usage: "sync [--color]", + Long: `Fetch git objects from upstream and update local branches. - If the local branch is outdated, fast-forward it; - If the local branch contains unpushed work, warn about it; - If the branch seems merged and its upstream branch was deleted, delete it. -If a local branch doesn't have any upstream configuration, but has a +If a local branch does not have any upstream configuration, but has a same-named branch on the remote, treat that as its upstream branch. +## Options: + --color[=] + Enable colored output even if stdout is not a terminal. can be one + of "always" (default for ''--color''), "never", or "auto" (default). + ## See also: hub(1), git-fetch(1) @@ -71,7 +75,8 @@ lightRed, resetColor string - if ui.IsTerminal(os.Stdout) { + colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color")) + if colorize { green = "\033[32m" lightGreen = "\033[32;1m" red = "\033[31m" @@ -109,7 +114,7 @@ } ui.Printf("%sUpdated branch %s%s%s (was %s).\n", green, lightGreen, branch, resetColor, diff.A[0:7]) } else { - ui.Errorf("warning: `%s' seems to contain unpushed commits\n", branch) + ui.Errorf("warning: '%s' seems to contain unpushed commits\n", branch) } } else if gone { diff, err := git.NewRange(fullBranch, fullDefaultBranch) @@ -123,7 +128,7 @@ git.Quiet("branch", "-D", branch) ui.Printf("%sDeleted branch %s%s%s (was %s).\n", red, lightRed, branch, resetColor, diff.A[0:7]) } else { - ui.Errorf("warning: `%s' was deleted on %s, but appears not merged into %s\n", branch, remote.Name, defaultBranch) + ui.Errorf("warning: '%s' was deleted on %s, but appears not merged into '%s'\n", branch, remote.Name, defaultBranch) } } } diff -Nru hub-2.7.0~ds1/commands/updater_autoupdate.go hub-2.14.2~ds1/commands/updater_autoupdate.go --- hub-2.7.0~ds1/commands/updater_autoupdate.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/updater_autoupdate.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,7 +0,0 @@ -// +build autoupdate - -package commands - -func init() { - EnableAutoUpdate = true -} diff -Nru hub-2.7.0~ds1/commands/version.go hub-2.14.2~ds1/commands/version.go --- hub-2.7.0~ds1/commands/version.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/commands/version.go 2020-03-05 17:48:23.000000000 +0000 @@ -2,7 +2,6 @@ import ( "github.com/github/hub/ui" - "github.com/github/hub/utils" "github.com/github/hub/version" ) @@ -18,10 +17,8 @@ } func runVersion(cmd *Command, args *Args) { - output, err := version.FullVersion() - if output != "" { - ui.Println(output) - } - utils.Check(err) + versionCmd := args.ToCmd() + versionCmd.Spawn() + ui.Printf("hub version %s\n", version.Version) args.NoForward() } diff -Nru hub-2.7.0~ds1/CONTRIBUTING.md hub-2.14.2~ds1/CONTRIBUTING.md --- hub-2.7.0~ds1/CONTRIBUTING.md 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/CONTRIBUTING.md 2020-03-05 17:48:23.000000000 +0000 @@ -9,11 +9,14 @@ You will need: -1. Go 1.8+ +1. Go 1.11+ 1. Ruby 1.9+ with Bundler 2. git 1.8+ 3. tmux & zsh (optional) - for running shell completion tests +If setting up either Go or Ruby for development proves to be a pain, you can +run the test suite in a prepared Docker container via `script/docker`. + ## What makes a good hub feature hub is a tool that wraps git to provide useful integration with GitHub. A new @@ -28,7 +31,7 @@ ## How to install dependencies and run tests -1. [Clone the project](./README.md#source) into your GOPATH +1. [Clone the project](./README.md#source) 2. Verify that existing tests pass: `make test-all` 3. Create a topic branch: @@ -43,29 +46,14 @@ 8. Open a pull request describing your changes: `bin/hub pull-request` -Vendored Go dependencies are managed with [`dep`](https://golang.github.io/dep/docs/daily-dep.html). -Check `dep help ensure` for information on how to add or update a vendored +Vendored Go dependencies are managed with [`go mod`](https://github.com/golang/go/wiki/Modules). +Check `go help mod` for information on how to add or update a vendored dependency. ## How to write tests -The new test suite is written in Cucumber under `features/` directory. Each -scenario is actually making real invocations to `hub` on the command-line in the -context of a real (dynamically created) git repository. - -Whenever a scenario requires talking to the GitHub API, a fake HTTP server is -spun locally to replace the real GitHub API. This is done so that the test suite -runs faster and is available offline as well. The fake API server is defined -as a Sinatra app inline in each scenario: - -``` -Given the GitHub API server: - """ - post('/repos/github/hub/pulls') { - status 200 - } - """ -``` +Go unit tests are in `*_test.go` files and are runnable with `make test`. These +run really fast (under 10s). -The best way to learn to write new tests is to study the existing scenarios for -commands that are similar to those that you want to add or change. +However, most hub functionality is exercised through integration-style tests +written in Cucumber. See [Features](./features) for more info. diff -Nru hub-2.7.0~ds1/cucumber.yml hub-2.14.2~ds1/cucumber.yml --- hub-2.7.0~ds1/cucumber.yml 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/cucumber.yml 2020-03-05 17:48:23.000000000 +0000 @@ -1,3 +1,3 @@ -default: --format progress -t ~@completion +default: --format progress -t 'not @completion' completion: --format pretty -t @completion all: --format progress diff -Nru hub-2.7.0~ds1/debian/changelog hub-2.14.2~ds1/debian/changelog --- hub-2.7.0~ds1/debian/changelog 2020-11-01 18:47:01.000000000 +0000 +++ hub-2.14.2~ds1/debian/changelog 2021-02-18 09:23:57.000000000 +0000 @@ -1,3 +1,18 @@ +hub (2.14.2~ds1-1) unstable; urgency=medium + + * New upstream version 2.14.2 (Closes: #949637) + * debian/control: Bump Standards-Version to 4.5.1 (no change) + * debian/gbp.conf: Set debian-branch to debian/sid for DEP-14 conformance + * Remove Ruby ronn from build dependency + and remove related debian/patches/man-pages.patch + * Update build dependencies as per go.mod + but use golang-github-russross-blackfriday-v2-dev instead of v1 + * Patch md2roff to use updated blackfriday v2 import path + * debian/rules: Adapt to upstream new way of building man pages + * Install new man pages, HTML documentation and Vim files too + + -- Anthony Fok Thu, 18 Feb 2021 02:23:57 -0700 + hub (2.7.0~ds1-2) unstable; urgency=medium * Team upload. diff -Nru hub-2.7.0~ds1/debian/control hub-2.14.2~ds1/debian/control --- hub-2.7.0~ds1/debian/control 2020-11-01 18:45:30.000000000 +0000 +++ hub-2.14.2~ds1/debian/control 2021-02-18 09:21:23.000000000 +0000 @@ -13,15 +13,16 @@ golang-github-atotto-clipboard-dev, golang-github-bmizerany-assert-dev, golang-github-burntsushi-toml-dev, + golang-github-google-go-cmp-dev, golang-github-kballard-go-shellquote-dev, golang-github-mattn-go-colorable-dev, golang-github-mattn-go-isatty-dev, golang-github-mitchellh-go-homedir-dev, - golang-github-ogier-pflag-dev, + golang-github-russross-blackfriday-v2-dev, golang-golang-x-crypto-dev, - golang-gopkg-yaml.v2-dev, - ronn -Standards-Version: 4.5.0 + golang-golang-x-net-dev, + golang-gopkg-yaml.v2-dev +Standards-Version: 4.5.1 Vcs-Browser: https://salsa.debian.org/go-team/packages/hub Vcs-Git: https://salsa.debian.org/go-team/packages/hub.git Homepage: https://github.com/github/hub diff -Nru hub-2.7.0~ds1/debian/gbp.conf hub-2.14.2~ds1/debian/gbp.conf --- hub-2.7.0~ds1/debian/gbp.conf 2020-11-01 18:45:30.000000000 +0000 +++ hub-2.14.2~ds1/debian/gbp.conf 2021-02-18 07:27:26.000000000 +0000 @@ -1,2 +1,4 @@ [DEFAULT] +debian-branch = debian/sid +dist = DEP14 pristine-tar = True diff -Nru hub-2.7.0~ds1/debian/hub.docs hub-2.14.2~ds1/debian/hub.docs --- hub-2.7.0~ds1/debian/hub.docs 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/debian/hub.docs 2021-02-18 09:21:25.000000000 +0000 @@ -0,0 +1 @@ +share/doc/hub-doc/*.html diff -Nru hub-2.7.0~ds1/debian/hub.install hub-2.14.2~ds1/debian/hub.install --- hub-2.7.0~ds1/debian/hub.install 2020-11-01 18:45:30.000000000 +0000 +++ hub-2.14.2~ds1/debian/hub.install 2021-02-18 09:21:25.000000000 +0000 @@ -2,3 +2,4 @@ etc/hub.bash_completion.sh => /usr/share/bash-completion/completions/hub etc/hub.fish_completion => /usr/share/fish/vendor_completions.d/hub.fish etc/hub.zsh_completion => /usr/share/zsh/vendor-completions/_hub +share/vim/vimfiles/* /usr/share/vim/vimfiles/ diff -Nru hub-2.7.0~ds1/debian/hub.manpages hub-2.14.2~ds1/debian/hub.manpages --- hub-2.7.0~ds1/debian/hub.manpages 2020-11-01 18:45:30.000000000 +0000 +++ hub-2.14.2~ds1/debian/hub.manpages 2021-02-18 09:21:25.000000000 +0000 @@ -1,6 +1,7 @@ share/man/man1/hub.1 share/man/man1/hub-alias.1 share/man/man1/hub-am.1 +share/man/man1/hub-api.1 share/man/man1/hub-apply.1 share/man/man1/hub-browse.1 share/man/man1/hub-checkout.1 @@ -12,6 +13,7 @@ share/man/man1/hub-delete.1 share/man/man1/hub-fetch.1 share/man/man1/hub-fork.1 +share/man/man1/hub-gist.1 share/man/man1/hub-help.1 share/man/man1/hub-init.1 share/man/man1/hub-issue.1 diff -Nru hub-2.7.0~ds1/debian/patches/0001-blackfriday-v2.patch hub-2.14.2~ds1/debian/patches/0001-blackfriday-v2.patch --- hub-2.7.0~ds1/debian/patches/0001-blackfriday-v2.patch 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/debian/patches/0001-blackfriday-v2.patch 2021-02-18 09:21:24.000000000 +0000 @@ -0,0 +1,29 @@ +Description: Update blackfriday v2 import path +Author: Anthony Fok +Origin: vendor +Forwarded: no +Last-Update: 2021-02-18 +--- +This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ +--- a/md2roff-bin/cmd.go ++++ b/md2roff-bin/cmd.go +@@ -13,7 +13,7 @@ + + "github.com/github/hub/md2roff" + "github.com/github/hub/utils" +- "github.com/russross/blackfriday" ++ "github.com/russross/blackfriday/v2" + ) + + var ( +--- a/md2roff/renderer.go ++++ b/md2roff/renderer.go +@@ -8,7 +8,7 @@ + "strconv" + "strings" + +- "github.com/russross/blackfriday" ++ "github.com/russross/blackfriday/v2" + ) + + // https://github.com/russross/blackfriday/blob/v2/markdown.go diff -Nru hub-2.7.0~ds1/debian/patches/man-pages.patch hub-2.14.2~ds1/debian/patches/man-pages.patch --- hub-2.7.0~ds1/debian/patches/man-pages.patch 2020-11-01 18:45:30.000000000 +0000 +++ hub-2.14.2~ds1/debian/patches/man-pages.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,27 +0,0 @@ -Description: Tweak Makefile so that "make man-pages" work with system ronn -Author: Anthony Fok -Origin: vendor -Forwarded: not-needed -Last-Update: 2018-12-22 ---- -This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ - ---- a/Makefile -+++ b/Makefile -@@ -59,12 +59,12 @@ - groff -Wall -mtty-char -mandoc -Tutf8 -rLL=$(TEXT_WIDTH)n $< | col -b >$@ - - $(HELP_ALL): share/man/.man-pages.stamp --share/man/.man-pages.stamp: bin/ronn $(HELP_ALL:=.ronn) -- bin/ronn --organization=GITHUB --manual="Hub Manual" share/man/man1/*.ronn -+share/man/.man-pages.stamp: $(HELP_ALL:=.ronn) -+ ronn --organization=GITHUB --manual="Hub Manual" share/man/man1/*.ronn - touch $@ - --%.1.ronn: bin/hub -- bin/hub help $(*F) --plain-text | script/format-ronn $(*F) $@ -+%.1.ronn: -+ hub help $(*F) --plain-text | script/format-ronn $(*F) $@ - - share/man/man1/hub.1.ronn: - true diff -Nru hub-2.7.0~ds1/debian/patches/series hub-2.14.2~ds1/debian/patches/series --- hub-2.7.0~ds1/debian/patches/series 2020-11-01 18:45:30.000000000 +0000 +++ hub-2.14.2~ds1/debian/patches/series 2021-02-18 09:21:24.000000000 +0000 @@ -1 +1 @@ -man-pages.patch +0001-blackfriday-v2.patch diff -Nru hub-2.7.0~ds1/debian/rules hub-2.14.2~ds1/debian/rules --- hub-2.7.0~ds1/debian/rules 2020-11-01 18:45:30.000000000 +0000 +++ hub-2.14.2~ds1/debian/rules 2021-02-18 09:21:25.000000000 +0000 @@ -1,22 +1,15 @@ #!/usr/bin/make -f export DH_GOLANG_INSTALL_EXTRA := fixtures/ -export PATH := obj-$(DEB_HOST_GNU_TYPE)/bin/:$(PATH) %: - dh $@ --buildsystem=golang --with=golang + dh $@ --builddirectory=_build --buildsystem=golang --with=golang -override_dh_auto_build: - dh_auto_build -O--buildsystem=golang - LC_ALL=C.UTF-8 make man-pages - -override_dh_auto_clean: - dh_auto_clean -O--buildsystem=golang +execute_after_dh_auto_build: + mkdir -p bin + cp -av _build/bin/hub bin/hub + mv -v _build/bin/md2roff-bin bin/md2roff + $(MAKE) man-pages override_dh_auto_install: - dh_auto_install -O--buildsystem=golang -- --no-source - -override_dh_fixperms: - dh_fixperms -O--buildsystem=golang - # See https://github.com/github/hub/pull/1782 - chmod -x debian/hub/usr/share/*sh*/*completion*/*hub + dh_auto_install -- --no-source diff -Nru hub-2.7.0~ds1/Dockerfile hub-2.14.2~ds1/Dockerfile --- hub-2.7.0~ds1/Dockerfile 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/Dockerfile 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,24 @@ +FROM ruby:2.6 + +RUN apt-get update \ + && apt-get install -y sudo golang --no-install-recommends +RUN apt-get purge --auto-remove -y curl \ + && rm -rf /var/lib/apt/lists/* + +RUN groupadd -r app && useradd -r -g app -G sudo app \ + && mkdir -p /home/app && chown -R app:app /home/app +RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers + +USER app + +# throw errors if Gemfile has been modified since Gemfile.lock +RUN bundle config --global frozen 1 + +WORKDIR /home/app/workdir + +COPY Gemfile Gemfile.lock ./ +RUN bundle install + +ENV LANG C.UTF-8 +ENV GOFLAGS -mod=vendor +ENV USER app diff -Nru hub-2.7.0~ds1/.dockerignore hub-2.14.2~ds1/.dockerignore --- hub-2.7.0~ds1/.dockerignore 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/.dockerignore 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,3 @@ +* +!Gemfile +!Gemfile.lock \ No newline at end of file diff -Nru hub-2.7.0~ds1/etc/hub.bash_completion.sh hub-2.14.2~ds1/etc/hub.bash_completion.sh --- hub-2.7.0~ds1/etc/hub.bash_completion.sh 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/etc/hub.bash_completion.sh 2020-03-05 17:48:23.000000000 +0000 @@ -119,7 +119,7 @@ # revision. For example: # $ hub compare -u upstream # > https://github.com/USER/REPO/compare/upstream - if __hub_github_repos '\p' | grep -Eqx "^$i(/[^/]+)?"; then + if __hub_github_repos '\p' | command grep -Eqx "^$i(/[^/]+)?"; then arg_repo=$i else rev=$i @@ -218,21 +218,36 @@ esac } - # hub fork [--no-remote] + # hub fork [--no-remote] [--remote-name REMOTE] [--org ORGANIZATION] _git_fork() { - local i c=2 remote=yes + local i c=2 flags="--no-remote --remote-name --org" while [ $c -lt $cword ]; do i="${words[c]}" case "$i" in + --org) + ((c++)) + flags=${flags/$i/} + ;; + --remote-name) + ((c++)) + flags=${flags/$i/} + flags=${flags/--no-remote/} + ;; --no-remote) - unset remote + flags=${flags/$i/} + flags=${flags/--remote-name/} ;; esac ((c++)) done - if [ -n "$remote" ]; then - __gitcomp "--no-remote" - fi + case "$prev" in + --remote-name|--org) + COMPREPLY=() + ;; + *) + __gitcomp "$flags" + ;; + esac } # hub pull-request [-f] [-m |-F |-i |] [-b ] [-h ] [-a ] [-M ] [-l ] @@ -329,7 +344,7 @@ format=${format//\o/\3} fi command git config --get-regexp 'remote\.[^.]*\.url' | - grep -E ' ((https?|git)://|git@)github\.com[:/][^:/]+/[^/]+$' | + command grep -E ' ((https?|git)://|git@)github\.com[:/][^:/]+/[^/]+$' | sed -E 's#^remote\.([^.]+)\.url +.+[:/](([^/]+)/[^.]+)(\.git)?$#'"$format"'#' } diff -Nru hub-2.7.0~ds1/etc/hub.fish_completion hub-2.14.2~ds1/etc/hub.fish_completion --- hub-2.7.0~ds1/etc/hub.fish_completion 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/etc/hub.fish_completion 2020-03-05 17:48:23.000000000 +0000 @@ -1,3 +1,5 @@ +complete -c hub --wraps git + function __fish_hub_needs_command set cmd (commandline -opc) if [ (count $cmd) -eq 1 ] @@ -9,12 +11,21 @@ function __fish_hub_using_command set cmd (commandline -opc) - if [ (count $cmd) -gt 1 ] - if [ $argv[1] = $cmd[2] ] - return 0 + set subcmd_count (count $argv) + if [ (count $cmd) -gt "$subcmd_count" ] + for i in (seq 1 "$subcmd_count") + if [ "$argv[$i]" != $cmd[(math "$i" + 1)] ] + return 1 + end end + return 0 + else + return 1 end - return 1 +end + +function __fish_hub_prs + command hub pr list -f %I\t%t%n 2>/dev/null end complete -f -c hub -n '__fish_hub_needs_command' -a alias -d "show shell instructions for wrapping git" @@ -24,7 +35,7 @@ complete -f -c hub -n '__fish_hub_needs_command' -a delete -d "delete a GitHub repo" complete -f -c hub -n '__fish_hub_needs_command' -a fork -d "fork origin repo on GitHub" complete -f -c hub -n '__fish_hub_needs_command' -a pull-request -d "open a pull request on GitHub" -complete -f -c hub -n '__fish_hub_needs_command' -a pr -d "list or checkout a GitHub release" +complete -f -c hub -n '__fish_hub_needs_command' -a pr -d "list or checkout GitHub pull requests" complete -f -c hub -n '__fish_hub_needs_command' -a issue -d "list or create a GitHub issue" complete -f -c hub -n '__fish_hub_needs_command' -a release -d "list or create a GitHub release" complete -f -c hub -n '__fish_hub_needs_command' -a ci-status -d "display GitHub Status information for a commit" @@ -44,6 +55,26 @@ complete -f -c hub -n ' __fish_hub_using_command pull-request' -s a -d 'A comma-separated list of GitHub handles to assign to this pull request' complete -f -c hub -n ' __fish_hub_using_command pull-request' -s M -d "The milestone name to add to this pull request. Passing the milestone number is deprecated." complete -f -c hub -n ' __fish_hub_using_command pull-request' -s l -d "Add a comma-separated list of labels to this pull request" +# pr +set -l pr_commands list checkout show +complete -f -c hub -n ' __fish_hub_using_command pr' -l color -xa 'always never auto' -d 'enable colored output even if stdout is not a terminal. WHEN can be one of "always" (default for --color), "never", or "auto" (default).' +## pr list +complete -f -c hub -n " __fish_hub_using_command pr; and not __fish_seen_subcommand_from $pr_commands" -a list -d "list pull requests in the current repository" +complete -f -c hub -n ' __fish_hub_using_command pr list' -s s -l state -xa 'open closed merged all' -d 'filter pull requests by STATE. default: open' +complete -f -c hub -n ' __fish_hub_using_command pr list' -s h -l head -d 'show pull requests started from the specified head BRANCH in "[OWNER:]BRANCH" format' +complete -f -c hub -n ' __fish_hub_using_command pr list' -s b -l base -d 'show pull requests based off the specified BRANCH' +complete -f -c hub -n ' __fish_hub_using_command pr list' -s o -l sort -xa 'created updated popularity long-running' -d 'default: created' +complete -f -c hub -n ' __fish_hub_using_command pr list' -s '^' -l sort-ascending -d 'sort by ascending dates instead of descending' +complete -f -c hub -n ' __fish_hub_using_command pr list' -s f -l format -d 'pretty print the list of pull requests using format FORMAT (default: "%pC%>(8)%i%Creset %t% l%n")' +complete -f -c hub -n ' __fish_hub_using_command pr list' -s L -l limit -d 'display only the first LIMIT issues' +## pr checkout +complete -f -c hub -n " __fish_hub_using_command pr; and not __fish_seen_subcommand_from $pr_commands" -a checkout -d "check out the head of a pull request in a new branch" +complete -f -r -c hub -n ' __fish_hub_using_command pr checkout' -a '(__fish_hub_prs)' +## pr show +complete -f -c hub -n " __fish_hub_using_command pr; and not __fish_seen_subcommand_from $pr_commands" -a show -d "open a pull request page in a web browser" +complete -f -c hub -n ' __fish_hub_using_command pr show' -a '(__fish_hub_prs)' +complete -f -c hub -n ' __fish_hub_using_command pr show' -s u -d "print the pull request URL instead of opening it" +complete -f -c hub -n ' __fish_hub_using_command pr show' -s c -d "put the pull request URL to clipboard instead of opening it" # fork complete -f -c hub -n ' __fish_hub_using_command fork' -l no-remote -d "Skip adding a git remote for the fork" # browse diff -Nru hub-2.7.0~ds1/etc/hub.zsh_completion hub-2.14.2~ds1/etc/hub.zsh_completion --- hub-2.7.0~ds1/etc/hub.zsh_completion 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/etc/hub.zsh_completion 2020-03-05 17:48:23.000000000 +0000 @@ -58,6 +58,7 @@ - set1 \ '-m[message]' \ '-F[file]' \ + '--no-edit[use first commit message for pull request title/description]' \ '-a[user]' \ '-M[milestone]' \ '-l[labels]' \ diff -Nru hub-2.7.0~ds1/etc/README.md hub-2.14.2~ds1/etc/README.md --- hub-2.7.0~ds1/etc/README.md 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/etc/README.md 2020-03-05 17:48:23.000000000 +0000 @@ -1,8 +1,13 @@ # Installation instructions -## bash + Homebrew +## Homebrew -If you're using Homebrew, just run `brew install hub` and you should be all set with auto-completion. +If you're using Homebrew, just run `brew install hub` and you should be all set +with auto-completion. The extra steps to install hub completion scripts outlined +below are *not needed*. + +For bash/zsh, a one-time setup might be needed to [enable completion for all +Homebrew programs](https://docs.brew.sh/Shell-Completion). ## bash diff -Nru hub-2.7.0~ds1/features/am.feature hub-2.14.2~ds1/features/am.feature --- hub-2.7.0~ds1/features/am.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/am.feature 2020-03-05 17:48:23.000000000 +0000 @@ -16,7 +16,7 @@ } """ When I successfully run `hub am -q -3 https://github.com/mislav/dotfiles/pull/387` - Then there should be no output + Then the output should not contain anything Then the latest commit message should be "Create a README" Scenario: Apply commits when TMPDIR is empty diff -Nru hub-2.7.0~ds1/features/api.feature hub-2.14.2~ds1/features/api.feature --- hub-2.7.0~ds1/features/api.feature 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/features/api.feature 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,530 @@ +@cache_clear +Feature: hub api + Background: + Given I am "octokitten" on github.com with OAuth token "OTOKEN" + + Scenario: GET resource + Given the GitHub API server: + """ + get('/hello/world') { + halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token OTOKEN' + halt 401 unless request.env['HTTP_ACCEPT'] == 'application/vnd.github.v3+json;charset=utf-8' + json :name => "Ed" + } + """ + When I successfully run `hub api hello/world` + Then the output should contain exactly: + """ + {"name":"Ed"} + """ + + Scenario: GET Enterprise resource + Given I am "octokitten" on git.my.org with OAuth token "FITOKEN" + Given the GitHub API server: + """ + get('/api/v3/hello/world', :host_name => 'git.my.org') { + halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token FITOKEN' + json :name => "Ed" + } + """ + And $GITHUB_HOST is "git.my.org" + When I successfully run `hub api hello/world` + Then the output should contain exactly: + """ + {"name":"Ed"} + """ + + Scenario: Non-success response + Given the GitHub API server: + """ + get('/hello/world') { + status 400 + json :name => "Ed" + } + """ + When I run `hub api hello/world` + Then the exit status should be 22 + And the stdout should contain exactly: + """ + {"name":"Ed"} + """ + And the stderr should contain exactly "" + + Scenario: Non-success response flat output + Given the GitHub API server: + """ + get('/hello/world') { + status 400 + json :name => "Ed" + } + """ + When I run `hub api -t hello/world` + Then the exit status should be 22 + And the stdout should contain exactly: + """ + .name Ed\n + """ + And the stderr should contain exactly "" + + Scenario: Non-success response doesn't choke on non-JSON + Given the GitHub API server: + """ + get('/hello/world') { + status 400 + content_type :text + 'Something went wrong' + } + """ + When I run `hub api -t hello/world` + Then the exit status should be 22 + And the stdout should contain exactly: + """ + Something went wrong + """ + And the stderr should contain exactly "" + + Scenario: GET query string + Given the GitHub API server: + """ + get('/hello/world') { + json Hash[*params.sort.flatten] + } + """ + When I successfully run `hub api -XGET -Fname=Ed -Fnum=12 -Fbool=false -Fvoid=null hello/world` + Then the output should contain exactly: + """ + {"bool":"false","name":"Ed","num":"12","void":""} + """ + + Scenario: GET full URL + Given the GitHub API server: + """ + get('/hello/world', :host_name => 'api.github.com') { + halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token OTOKEN' + json :name => "Faye" + } + """ + When I successfully run `hub api https://api.github.com/hello/world` + Then the output should contain exactly: + """ + {"name":"Faye"} + """ + + Scenario: Paginate REST + Given the GitHub API server: + """ + get('/comments') { + assert :per_page => "6" + page = (params[:page] || 1).to_i + response.headers["Link"] = %(<#{request.url}&page=#{page+1}>; rel="next") if page < 3 + json [{:page => page}] + } + """ + When I successfully run `hub api --paginate comments?per_page=6` + Then the output should contain exactly: + """ + [{"page":1}] + [{"page":2}] + [{"page":3}] + """ + + Scenario: Paginate GraphQL + Given the GitHub API server: + """ + post('/graphql') { + variables = params[:variables] || {} + page = (variables["endCursor"] || 1).to_i + json :data => { + :pageInfo => { + :hasNextPage => page < 3, + :endCursor => (page+1).to_s + } + } + } + """ + When I successfully run `hub api --paginate graphql -f query=QUERY` + Then the output should contain exactly: + """ + {"data":{"pageInfo":{"hasNextPage":true,"endCursor":"2"}}} + {"data":{"pageInfo":{"hasNextPage":true,"endCursor":"3"}}} + {"data":{"pageInfo":{"hasNextPage":false,"endCursor":"4"}}} + """ + + Scenario: Avoid leaking token to a 3rd party + Given the GitHub API server: + """ + get('/hello/world', :host_name => 'example.com') { + halt 401 unless request.env['HTTP_AUTHORIZATION'].nil? + json :name => "Jet" + } + """ + When I successfully run `hub api http://example.com/hello/world` + Then the output should contain exactly: + """ + {"name":"Jet"} + """ + + Scenario: Request headers + Given the GitHub API server: + """ + get('/hello/world') { + json :accept => request.env['HTTP_ACCEPT'], + :foo => request.env['HTTP_X_FOO'] + } + """ + When I successfully run `hub api hello/world -H 'x-foo:bar' -H 'Accept: text/json'` + Then the output should contain exactly: + """ + {"accept":"text/json","foo":"bar"} + """ + + Scenario: Response headers + Given the GitHub API server: + """ + get('/hello/world') { + json({}) + } + """ + When I successfully run `hub api hello/world -i` + Then the output should contain "HTTP/1.1 200 OK" + And the output should contain "Content-Length: 2" + + Scenario: POST fields + Given the GitHub API server: + """ + post('/hello/world') { + json Hash[*params.sort.flatten] + } + """ + When I successfully run `hub api -f name=@hubot -Fnum=12 -Fbool=false -Fvoid=null hello/world` + Then the output should contain exactly: + """ + {"bool":false,"name":"@hubot","num":12,"void":null} + """ + + Scenario: POST raw fields + Given the GitHub API server: + """ + post('/hello/world') { + json Hash[*params.sort.flatten] + } + """ + When I successfully run `hub api -fnum=12 -fbool=false hello/world` + Then the output should contain exactly: + """ + {"bool":"false","num":"12"} + """ + + Scenario: POST from stdin + Given the GitHub API server: + """ + post('/graphql') { + json :query => params[:query] + } + """ + When I run `hub api -t -F query=@- graphql` interactively + And I pass in: + """ + query { + repository + } + """ + Then the output should contain exactly: + """ + .query query {\n repository\n}\n + """ + + Scenario: POST body from file + Given the GitHub API server: + """ + post('/create') { + params[:obj].inspect + } + """ + Given a file named "payload.json" with: + """ + {"obj": ["one", 2, null]} + """ + When I successfully run `hub api create --input payload.json` + Then the output should contain exactly: + """ + ["one", 2, nil] + """ + + Scenario: POST body from stdin + Given the GitHub API server: + """ + post('/create') { + params[:obj].inspect + } + """ + When I run `hub api create --input -` interactively + And I pass in: + """ + {"obj": {"name": "Ein", "datadog": true}} + """ + Then the output should contain exactly: + """ + {"name"=>"Ein", "datadog"=>true} + """ + + Scenario: Pass extra GraphQL variables + Given the GitHub API server: + """ + post('/graphql') { + json(params[:variables]) + } + """ + When I successfully run `hub api -F query='query {}' -Fname=Jet -Fsize=2 graphql` + Then the output should contain exactly: + """ + {"name":"Jet","size":2} + """ + + Scenario: Enterprise GraphQL + Given I am "octokitten" on git.my.org with OAuth token "FITOKEN" + Given the GitHub API server: + """ + post('/api/graphql', :host_name => 'git.my.org') { + halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token FITOKEN' + json :name => "Ed" + } + """ + And $GITHUB_HOST is "git.my.org" + When I successfully run `hub api graphql -f query=QUERY` + Then the output should contain exactly: + """ + {"name":"Ed"} + """ + + Scenario: Repo context + Given I am in "git://github.com/octocat/Hello-World.git" git repo + Given the GitHub API server: + """ + get('/repos/octocat/Hello-World/commits') { + json :commits => 12 + } + """ + When I successfully run `hub api repos/{owner}/{repo}/commits` + Then the output should contain exactly: + """ + {"commits":12} + """ + + Scenario: Multiple string interpolation + Given I am in "git://github.com/octocat/Hello-World.git" git repo + Given the GitHub API server: + """ + get('/repos/octocat/Hello-World/pulls') { + json(params) + } + """ + When I successfully run `hub api repos/{owner}/{repo}/pulls?head={owner}:{repo}` + Then the output should contain exactly: + """ + {"head":"octocat:Hello-World"} + """ + + Scenario: Repo context in graphql + Given I am in "git://github.com/octocat/Hello-World.git" git repo + Given the GitHub API server: + """ + post('/graphql') { + json :query => params[:query] + } + """ + When I run `hub api -t -F query=@- graphql` interactively + And I pass in: + """ + repository(owner: "{owner}", name: "{repo}", nameWithOwner: "{owner}/{repo}") + """ + Then the output should contain exactly: + """ + .query repository(owner: "octocat", name: "Hello-World", nameWithOwner: "octocat/Hello-World")\n + """ + + Scenario: Cache response + Given the GitHub API server: + """ + count = 0 + get('/count') { + count += 1 + json :count => count + } + """ + When I run `hub api -t 'count?a=1&b=2' --cache 5` + Then it should pass with ".count 1" + When I run `hub api -t 'count?b=2&a=1' --cache 5` + Then it should pass with ".count 1" + + Scenario: Cache graphql response + Given the GitHub API server: + """ + count = 0 + post('/graphql') { + halt 400 unless params[:query] =~ /^Q\d$/ + count += 1 + json :count => count + } + """ + When I run `hub api -t graphql -F query=Q1 --cache 5` + Then it should pass with ".count 1" + When I run `hub api -t graphql -F query=Q1 --cache 5` + Then it should pass with ".count 1" + When I run `hub api -t graphql -F query=Q2 --cache 5` + Then it should pass with ".count 2" + + Scenario: Cache client error response + Given the GitHub API server: + """ + count = 0 + get('/count') { + count += 1 + status 404 if count == 1 + json :count => count + } + """ + When I run `hub api -t count --cache 5` + Then it should fail with ".count 1" + When I run `hub api -t count --cache 5` + Then it should fail with ".count 1" + And the exit status should be 22 + + Scenario: Avoid caching server error response + Given the GitHub API server: + """ + count = 0 + get('/count') { + count += 1 + status 500 if count == 1 + json :count => count + } + """ + When I run `hub api -t count --cache 5` + Then it should fail with ".count 1" + When I run `hub api -t count --cache 5` + Then it should pass with ".count 2" + When I run `hub api -t count --cache 5` + Then it should pass with ".count 2" + + Scenario: Avoid caching response if the OAuth token changes + Given the GitHub API server: + """ + count = 0 + get('/count') { + count += 1 + json :count => count + } + """ + When I run `hub api -t count --cache 5` + Then it should pass with ".count 1" + Given I am "octocat" on github.com with OAuth token "TOKEN2" + When I run `hub api -t count --cache 5` + Then it should pass with ".count 2" + + Scenario: Honor rate limit with pagination + Given the GitHub API server: + """ + get('/hello') { + page = (params[:page] || 1).to_i + if page < 2 + response.headers['X-Ratelimit-Remaining'] = '0' + response.headers['X-Ratelimit-Reset'] = Time.now.utc.to_i.to_s + response.headers['Link'] = %(; rel="next") + end + json [{}] + } + """ + When I successfully run `hub api --obey-ratelimit --paginate hello` + Then the stderr should contain "API rate limit exceeded; pausing until " + + Scenario: Succumb to rate limit with pagination + Given the GitHub API server: + """ + get('/hello') { + page = (params[:page] || 1).to_i + response.headers['X-Ratelimit-Remaining'] = '0' + response.headers['X-Ratelimit-Reset'] = Time.now.utc.to_i.to_s + if page == 2 + status 403 + json :message => "API rate limit exceeded" + else + response.headers['Link'] = %(; rel="next") + json [{page:page}] + end + } + """ + When I run `hub api --paginate -t hello` + Then the exit status should be 22 + And the stderr should not contain "API rate limit exceeded" + And the stdout should contain exactly: + """ + .[0].page 1 + .message API rate limit exceeded\n + """ + + Scenario: Honor rate limit for 403s + Given the GitHub API server: + """ + count = 0 + get('/hello') { + count += 1 + if count == 1 + response.headers['X-Ratelimit-Remaining'] = '0' + response.headers['X-Ratelimit-Reset'] = Time.now.utc.to_i.to_s + halt 403 + end + json [{}] + } + """ + When I successfully run `hub api --obey-ratelimit hello` + Then the stderr should contain "API rate limit exceeded; pausing until " + + Scenario: 403 unrelated to rate limit + Given the GitHub API server: + """ + get('/hello') { + response.headers['X-Ratelimit-Remaining'] = '1' + status 403 + } + """ + When I run `hub api --obey-ratelimit hello` + Then the exit status should be 22 + Then the stderr should not contain "API rate limit exceeded" + + Scenario: Warn about insufficient OAuth scopes + Given the GitHub API server: + """ + get('/hello') { + response.headers['X-Accepted-Oauth-Scopes'] = 'repo, admin' + response.headers['X-Oauth-Scopes'] = 'public_repo' + status 403 + json({}) + } + """ + When I run `hub api hello` + Then the exit status should be 22 + And the output should contain exactly: + """ + {} + Your access token may have insufficient scopes. Visit http://github.com/settings/tokens + to edit the 'hub' token and enable one of the following scopes: admin, repo + """ + + Scenario: Print the SSO challenge to stderr + Given the GitHub API server: + """ + get('/orgs/acme') { + response.headers['X-GitHub-SSO'] = 'required; url=http://example.com?auth=HASH' + status 403 + json({}) + } + """ + When I run `hub api orgs/acme` + Then the exit status should be 22 + And the stderr should contain exactly: + """ + + You must authorize your token to access this organization: + http://example.com?auth=HASH + """ diff -Nru hub-2.7.0~ds1/features/apply.feature hub-2.14.2~ds1/features/apply.feature --- hub-2.7.0~ds1/features/apply.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/apply.feature 2020-03-05 17:48:23.000000000 +0000 @@ -18,7 +18,7 @@ } """ When I successfully run `hub apply -3 https://github.com/mislav/dotfiles/pull/387` - Then there should be no output + Then the output should not contain anything Then a file named "README.md" should exist Scenario: Apply commits when TMPDIR is empty diff -Nru hub-2.7.0~ds1/features/authentication.feature hub-2.14.2~ds1/features/authentication.feature --- hub-2.7.0~ds1/features/authentication.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/authentication.feature 2020-03-05 17:48:23.000000000 +0000 @@ -11,7 +11,7 @@ post('/authorizations') { assert_basic_auth 'mislav', 'kitty' - assert :scopes => ['repo'], + assert :scopes => ['repo', 'gist'], :note => "hub for #{machine_id}", :note_url => 'https://hub.github.com/' status 201 @@ -33,9 +33,9 @@ Then the output should contain "github.com username:" And the output should contain "github.com password for mislav (never stored):" And the exit status should be 0 - And the file "../home/.config/hub" should contain "user: MiSlAv" - And the file "../home/.config/hub" should contain "oauth_token: OTOKEN" - And the file "../home/.config/hub" should have mode "0600" + And the file "~/.config/hub" should contain "user: MiSlAv" + And the file "~/.config/hub" should contain "oauth_token: OTOKEN" + And the file "~/.config/hub" should have mode "0600" Scenario: Prompt for username & password, receive personal access token Given the GitHub API server: @@ -194,10 +194,10 @@ And $XDG_CONFIG_HOME is "$HOME/.xdg" When I successfully run `hub create` Then the file "../home/.xdg/hub" should contain "oauth_token: OTOKEN" - And the stderr should contain exactly: + And the stderr with expanded variables should contain exactly: """ - Notice: config file found but not respected at: $HOME/.config/hub - You might want to move it to `$HOME/.xdg/hub' to avoid re-authenticating.\n + Notice: config file found but not respected at: <$HOME>/.config/hub + You might want to move it to `<$HOME>/.xdg/hub' to avoid re-authenticating.\n """ Scenario: XDG: config from secondary directories @@ -241,6 +241,79 @@ And the output should not contain "github.com username" And the file "../home/.config/hub" should not exist + Scenario: Credentials from GITHUB_TOKEN when obtaining username fails + Given I am in "git://github.com/monalisa/playground.git" git repo + Given the GitHub API server: + """ + get('/user') { + status 403 + json :message => "Resource not accessible by integration", + :documentation_url => "https://developer.github.com/v3/users/#get-the-authenticated-user" + } + """ + Given $GITHUB_TOKEN is "OTOKEN" + Given $GITHUB_USER is "" + When I run `hub release show v1.2.0` + Then the output should not contain "github.com password" + And the output should not contain "github.com username" + And the file "../home/.config/hub" should not exist + And the exit status should be 1 + And the stderr should contain exactly: + """ + Error getting current user: Forbidden (HTTP 403) + Resource not accessible by integration + You must specify GITHUB_USER via environment variable.\n + """ + + Scenario: Credentials from GITHUB_TOKEN and GITHUB_USER + Given I am in "git://github.com/monalisa/playground.git" git repo + Given the GitHub API server: + """ + get('/user') { + status 403 + json :message => "Resource not accessible by integration", + :documentation_url => "https://developer.github.com/v3/users/#get-the-authenticated-user" + } + get('/repos/monalisa/playground/releases') { + halt 401 unless request.env["HTTP_AUTHORIZATION"] == "token OTOKEN" + json [ + { tag_name: 'v1.2.0', + } + ] + } + """ + Given $GITHUB_TOKEN is "OTOKEN" + Given $GITHUB_USER is "hubot" + When I successfully run `hub release show v1.2.0` + Then the output should not contain "github.com password" + And the output should not contain "github.com username" + And the file "../home/.config/hub" should not exist + + Scenario: Credentials from GITHUB_TOKEN and GITHUB_REPOSITORY + Given I am in "git://github.com/monalisa/playground.git" git repo + Given the GitHub API server: + """ + get('/user') { + status 403 + json :message => "Resource not accessible by integration", + :documentation_url => "https://developer.github.com/v3/users/#get-the-authenticated-user" + } + get('/repos/monalisa/playground/releases') { + halt 401 unless request.env["HTTP_AUTHORIZATION"] == "token OTOKEN" + json [ + { tag_name: 'v1.2.0', + } + ] + } + """ + Given $GITHUB_TOKEN is "OTOKEN" + Given $GITHUB_REPOSITORY is "mona-lisa/play-ground" + Given $GITHUB_USER is "" + When I successfully run `hub release show v1.2.0` + Then the output should not contain "github.com password" + And the output should not contain "github.com username" + And the file "../home/.config/hub" should not exist + Scenario: Credentials from GITHUB_TOKEN override those from config file Given I am "mislav" on github.com with OAuth token "OTOKEN" Given the GitHub API server: @@ -441,9 +514,27 @@ Scenario: Config file is not writeable on default location, should exit before asking for credentials Given a directory named "../home/.config" with mode "600" When I run `hub create` interactively - Then the output should contain: + Then the output with expanded variables should contain: """ - $HOME/.config/hub: permission denied\n + <$HOME>/.config/hub: permission denied\n """ And the exit status should be 1 And the file "../home/.config/hub" should not exist + + Scenario: GitHub SSO challenge + Given I am "monalisa" on github.com with OAuth token "OTOKEN" + And I am in "git://github.com/acme/playground.git" git repo + Given the GitHub API server: + """ + get('/repos/acme/playground/releases') { + response.headers['X-GitHub-SSO'] = 'required; url=http://example.com?auth=HASH' + status 403 + } + """ + When I run `hub release show v1.2.0` + Then the stderr should contain exactly: + """ + Error fetching releases: Forbidden (HTTP 403) + You must authorize your token to access this organization: + http://example.com?auth=HASH + """ diff -Nru hub-2.7.0~ds1/features/bash_completion.feature hub-2.14.2~ds1/features/bash_completion.feature --- hub-2.7.0~ds1/features/bash_completion.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/bash_completion.feature 2020-03-05 17:48:23.000000000 +0000 @@ -35,7 +35,8 @@ Scenario: Completion of fork argument When I type "git fork -" and press - Then the command should expand to "git fork --no-remote" + When I press again + Then the completion menu should offer "--no-remote --remote-name --org" unsorted Scenario: Completion of user/repo in "browse" Scenario: Completion of branch names in "compare" diff -Nru hub-2.7.0~ds1/features/browse.feature hub-2.14.2~ds1/features/browse.feature --- hub-2.7.0~ds1/features/browse.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/browse.feature 2020-03-05 17:48:23.000000000 +0000 @@ -9,7 +9,7 @@ Scenario: Project with owner When I successfully run `hub browse mislav/dotfiles` - Then there should be no output + Then the output should not contain anything And "open https://github.com/mislav/dotfiles" should be run Scenario: Project without owner @@ -47,7 +47,7 @@ Scenario: Current project Given I am in "git://github.com/mislav/dotfiles.git" git repo When I successfully run `hub browse` - Then there should be no output + Then the output should not contain anything And "open https://github.com/mislav/dotfiles" should be run Scenario: Commit in current project @@ -125,7 +125,7 @@ And the "mislav" remote has url "git@github.com:mislav/coffee-script.git" And I am on the "feature" branch pushed to "mislav/feature" When I successfully run `hub browse -- issues` - Then there should be no output + Then the output should not contain anything Then "open https://github.com/jashkenas/coffee-script/issues" should be run Scenario: Forward Slash Delimited branch diff -Nru hub-2.7.0~ds1/features/checkout.feature hub-2.14.2~ds1/features/checkout.feature --- hub-2.7.0~ds1/features/checkout.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/checkout.feature 2020-03-05 17:48:23.000000000 +0000 @@ -28,11 +28,38 @@ }, :maintainer_can_modify => false } """ - When I run `hub checkout -f https://github.com/mojombo/jekyll/pull/77 -q` + When I successfully run `hub checkout -f https://github.com/mojombo/jekyll/pull/77 -q` Then "git fetch origin refs/pull/77/head:fixes" should be run And "git checkout -f fixes -q" should be run And "fixes" should merge "refs/pull/77/head" from remote "origin" + Scenario: Avoid overriding existing merge configuration + Given the GitHub API server: + """ + get('/repos/mojombo/jekyll/pulls/77') { + json :number => 77, :head => { + :ref => "fixes", + :repo => { + :owner => { :login => "mislav" }, + :name => "jekyll", + :private => false + } + }, :base => { + :repo => { + :name => 'jekyll', + :html_url => 'https://github.com/mojombo/jekyll', + :owner => { :login => "mojombo" }, + } + }, :maintainer_can_modify => false + } + """ + Given I successfully run `git config branch.fixes.remote ORIG_REMOTE` + Given I successfully run `git config branch.fixes.merge custom/ref/spec` + When I successfully run `hub checkout https://github.com/mojombo/jekyll/pull/77` + Then "git fetch origin refs/pull/77/head:fixes" should be run + And "git checkout fixes" should be run + And "fixes" should merge "custom/ref/spec" from remote "ORIG_REMOTE" + Scenario: Head ref matches default branch Given the GitHub API server: """ @@ -54,7 +81,7 @@ }, :maintainer_can_modify => false } """ - When I run `hub checkout https://github.com/mojombo/jekyll/pull/77` + When I successfully run `hub checkout https://github.com/mojombo/jekyll/pull/77` Then "git fetch origin refs/pull/77/head:mislav-master" should be run And "git checkout mislav-master" should be run And "mislav-master" should merge "refs/pull/77/head" from remote "origin" @@ -98,7 +125,7 @@ }, :maintainer_can_modify => false } """ - When I run `hub checkout https://github.com/mojombo/jekyll/pull/77 fixes-from-mislav` + When I successfully run `hub checkout https://github.com/mojombo/jekyll/pull/77 fixes-from-mislav` Then "git fetch origin refs/pull/77/head:fixes-from-mislav" should be run And "git checkout fixes-from-mislav" should be run And "fixes-from-mislav" should merge "refs/pull/77/head" from remote "origin" @@ -122,9 +149,10 @@ } } """ - When I run `hub checkout -f https://github.com/mojombo/jekyll/pull/77 -q` + When I successfully run `hub checkout -f https://github.com/mojombo/jekyll/pull/77 -q` Then "git fetch origin +refs/heads/fixes:refs/remotes/origin/fixes" should be run - And "git checkout -f -b fixes --track origin/fixes -q" should be run + And "git checkout -f -b fixes --no-track origin/fixes -q" should be run + And "fixes" should merge "refs/heads/fixes" from remote "origin" Scenario: Same-repo with custom branch name Given the GitHub API server: @@ -145,9 +173,10 @@ } } """ - When I run `hub checkout https://github.com/mojombo/jekyll/pull/77 mycustombranch` + When I successfully run `hub checkout https://github.com/mojombo/jekyll/pull/77 mycustombranch` Then "git fetch origin +refs/heads/fixes:refs/remotes/origin/fixes" should be run - And "git checkout -b mycustombranch --track origin/fixes" should be run + And "git checkout -b mycustombranch --no-track origin/fixes" should be run + And "mycustombranch" should merge "refs/heads/fixes" from remote "origin" Scenario: Unavailable fork Given the GitHub API server: @@ -165,7 +194,7 @@ } } """ - When I run `hub checkout https://github.com/mojombo/jekyll/pull/77` + When I successfully run `hub checkout https://github.com/mojombo/jekyll/pull/77` Then "git fetch origin refs/pull/77/head:fixes" should be run And "git checkout fixes" should be run And "fixes" should merge "refs/pull/77/head" from remote "origin" @@ -191,9 +220,10 @@ } """ And the "mislav" remote has url "git://github.com/mislav/jekyll.git" - When I run `hub checkout -f https://github.com/mojombo/jekyll/pull/77 -q` + When I successfully run `hub checkout -f https://github.com/mojombo/jekyll/pull/77 -q` Then "git fetch mislav +refs/heads/fixes:refs/remotes/mislav/fixes" should be run - And "git checkout -f -b fixes --track mislav/fixes -q" should be run + And "git checkout -f -b fixes --no-track mislav/fixes -q" should be run + And "fixes" should merge "refs/heads/fixes" from remote "mislav" Scenario: Reuse existing remote and branch Given the GitHub API server: @@ -217,7 +247,7 @@ """ And the "mislav" remote has url "git://github.com/mislav/jekyll.git" And I am on the "fixes" branch - When I run `hub checkout -f https://github.com/mojombo/jekyll/pull/77 -q` + When I successfully run `hub checkout -f https://github.com/mojombo/jekyll/pull/77 -q` Then "git fetch mislav +refs/heads/fixes:refs/remotes/mislav/fixes" should be run And "git checkout -f fixes -q" should be run And "git merge --ff-only refs/remotes/mislav/fixes" should be run @@ -243,11 +273,40 @@ }, :maintainer_can_modify => true } """ - When I run `hub checkout -f https://github.com/mojombo/jekyll/pull/77 -q` + When I successfully run `hub checkout -f https://github.com/mojombo/jekyll/pull/77 -q` Then "git fetch origin refs/pull/77/head:fixes" should be run And "git checkout -f fixes -q" should be run And "fixes" should merge "refs/heads/fixes" from remote "git@github.com:mislav/jekyll.git" + Scenario: Modifiable fork into current branch + Given the GitHub API server: + """ + get('/repos/mojombo/jekyll/pulls/77') { + json :number => 77, :head => { + :ref => "fixes", + :repo => { + :owner => { :login => "mislav" }, + :name => "jekyll", + :html_url => "https://github.com/mislav/jekyll.git", + :private => false + }, + }, :base => { + :repo => { + :name => 'jekyll', + :html_url => 'https://github.com/mojombo/jekyll', + :owner => { :login => "mojombo" }, + } + }, :maintainer_can_modify => true + } + """ + And I am on the "fixes" branch + And there is a git FETCH_HEAD + When I successfully run `hub checkout https://github.com/mojombo/jekyll/pull/77` + Then "git fetch origin refs/pull/77/head" should be run + And "git checkout fixes" should be run + And "git merge --ff-only FETCH_HEAD" should be run + And "fixes" should merge "refs/heads/fixes" from remote "git@github.com:mislav/jekyll.git" + Scenario: Modifiable fork with HTTPS Given the GitHub API server: """ @@ -270,7 +329,7 @@ } """ And HTTPS is preferred - When I run `hub checkout -f https://github.com/mojombo/jekyll/pull/77 -q` + When I successfully run `hub checkout -f https://github.com/mojombo/jekyll/pull/77 -q` Then "git fetch origin refs/pull/77/head:fixes" should be run And "git checkout -f fixes -q" should be run And "fixes" should merge "refs/heads/fixes" from remote "https://github.com/mislav/jekyll.git" diff -Nru hub-2.7.0~ds1/features/ci_status.feature hub-2.14.2~ds1/features/ci_status.feature --- hub-2.7.0~ds1/features/ci_status.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/ci_status.feature 2020-03-05 17:48:23.000000000 +0000 @@ -30,24 +30,62 @@ { :state => "success", :context => "continuous-integration/travis-ci/push", :target_url => "https://travis-ci.org/michiels/pencilbox/builds/1234567" }, + { :state => "success", + :context => "continuous-integration/travis-ci/ants", + :target_url => "https://travis-ci.org/michiels/pencilbox/builds/1234568" }, { :state => "pending", :context => "continuous-integration/travis-ci/merge", :target_url => nil }, + { :state => "error", + :context => "whatevs!" }, { :state => "failure", :context => "GitHub CLA", :target_url => "https://cla.github.com/michiels/pencilbox/accept/mislav" }, - { :state => "error", - :context => "whatevs!" } ] } """ When I run `hub ci-status -v the_sha` Then the output should contain exactly: """ - ✔︎ continuous-integration/travis-ci/push https://travis-ci.org/michiels/pencilbox/builds/1234567 - ● continuous-integration/travis-ci/merge ✖︎ GitHub CLA https://cla.github.com/michiels/pencilbox/accept/mislav - ✖︎ whatevs!\n + ✖︎ whatevs! + ● continuous-integration/travis-ci/merge + ✔︎ continuous-integration/travis-ci/ants https://travis-ci.org/michiels/pencilbox/builds/1234568 + ✔︎ continuous-integration/travis-ci/push https://travis-ci.org/michiels/pencilbox/builds/1234567\n + """ + And the exit status should be 1 + + Scenario: Multiple statuses with format string + Given there is a commit named "the_sha" + Given the remote commit states of "michiels/pencilbox" "the_sha" are: + """ + { :state => "error", + :statuses => [ + { :state => "success", + :context => "continuous-integration/travis-ci/push", + :target_url => "https://travis-ci.org/michiels/pencilbox/builds/1234567" }, + { :state => "success", + :context => "continuous-integration/travis-ci/ants", + :target_url => "https://travis-ci.org/michiels/pencilbox/builds/1234568" }, + { :state => "pending", + :context => "continuous-integration/travis-ci/merge", + :target_url => nil }, + { :state => "error", + :context => "whatevs!" }, + { :state => "failure", + :context => "GitHub CLA", + :target_url => "https://cla.github.com/michiels/pencilbox/accept/mislav" }, + ] + } + """ + When I run `hub ci-status the_sha --format '%S: %t (%U)%n'` + Then the output should contain exactly: + """ + failure: GitHub CLA (https://cla.github.com/michiels/pencilbox/accept/mislav) + error: whatevs! () + pending: continuous-integration/travis-ci/merge () + success: continuous-integration/travis-ci/ants (https://travis-ci.org/michiels/pencilbox/builds/1234568) + success: continuous-integration/travis-ci/push (https://travis-ci.org/michiels/pencilbox/builds/1234567)\n """ And the exit status should be 1 diff -Nru hub-2.7.0~ds1/features/clone.feature hub-2.14.2~ds1/features/clone.feature --- hub-2.7.0~ds1/features/clone.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/clone.feature 2020-03-05 17:48:23.000000000 +0000 @@ -13,7 +13,7 @@ """ When I successfully run `hub clone rtomayko/ronn` Then it should clone "git://github.com/rtomayko/ronn.git" - And there should be no output + And the output should not contain anything Scenario: Clone a public repo with period in name Given the GitHub API server: @@ -26,7 +26,7 @@ """ When I successfully run `hub clone hookio/hook.js` Then it should clone "git://github.com/hookio/hook.js.git" - And there should be no output + And the output should not contain anything Scenario: Clone a public repo that starts with a period Given the GitHub API server: @@ -39,7 +39,7 @@ """ When I successfully run `hub clone zhuangya/.vim` Then it should clone "git://github.com/zhuangya/.vim.git" - And there should be no output + And the output should not contain anything Scenario: Clone a repo even if same-named directory exists Given the GitHub API server: @@ -53,7 +53,7 @@ And a directory named "rtomayko/ronn" When I successfully run `hub clone rtomayko/ronn` Then it should clone "git://github.com/rtomayko/ronn.git" - And there should be no output + And the output should not contain anything Scenario: Clone a public repo with HTTPS Given HTTPS is preferred @@ -67,7 +67,7 @@ """ When I successfully run `hub clone rtomayko/ronn` Then it should clone "https://github.com/rtomayko/ronn.git" - And there should be no output + And the output should not contain anything Scenario: Clone command aliased Given the GitHub API server: @@ -81,7 +81,7 @@ When I successfully run `git config --global alias.c "clone --bare"` And I successfully run `hub c rtomayko/ronn` Then "git clone --bare git://github.com/rtomayko/ronn.git" should be run - And there should be no output + And the output should not contain anything Scenario: Unchanged public clone When I successfully run `hub clone git://github.com/rtomayko/ronn.git` @@ -90,39 +90,39 @@ Scenario: Unchanged public clone with path When I successfully run `hub clone git://github.com/rtomayko/ronn.git ronnie` Then the git command should be unchanged - And there should be no output + And the output should not contain anything Scenario: Unchanged private clone When I successfully run `hub clone git@github.com:rtomayko/ronn.git` Then the git command should be unchanged - And there should be no output + And the output should not contain anything Scenario: Unchanged clone with complex arguments When I successfully run `hub clone --template=one/two git://github.com/defunkt/resque.git --origin master resquetastic` Then the git command should be unchanged - And there should be no output + And the output should not contain anything Scenario: Unchanged local clone When I successfully run `hub clone ./dotfiles` Then the git command should be unchanged - And there should be no output + And the output should not contain anything Scenario: Unchanged local clone with destination Given a directory named ".git" When I successfully run `hub clone -l . ../copy` Then the git command should be unchanged - And there should be no output + And the output should not contain anything Scenario: Unchanged local clone from bare repo Given a bare git repo in "rtomayko/ronn" When I successfully run `hub clone rtomayko/ronn` Then the git command should be unchanged - And there should be no output + And the output should not contain anything Scenario: Unchanged clone with host alias When I successfully run `hub clone shortcut:git/repo.git` Then the git command should be unchanged - And there should be no output + And the output should not contain anything Scenario: Preview cloning a private repo Given the GitHub API server: @@ -148,7 +148,7 @@ """ When I successfully run `hub clone -p rtomayko/ronn` Then it should clone "git@github.com:rtomayko/ronn.git" - And there should be no output + And the output should not contain anything Scenario: Clone my repo Given the GitHub API server: @@ -161,7 +161,7 @@ """ When I successfully run `hub clone dotfiles` Then it should clone "git@github.com:mislav/dotfiles.git" - And there should be no output + And the output should not contain anything Scenario: Clone my repo that doesn't exist Given the GitHub API server: @@ -185,7 +185,7 @@ """ When I successfully run `hub clone --bare -o master dotfiles` Then "git clone --bare -o master git@github.com:mislav/dotfiles.git" should be run - And there should be no output + And the output should not contain anything Scenario: Clone repo to which I have push access to Given the GitHub API server: @@ -198,7 +198,7 @@ """ When I successfully run `hub clone sstephenson/rbenv` Then "git clone git@github.com:sstephenson/rbenv.git" should be run - And there should be no output + And the output should not contain anything Scenario: Preview cloning a repo I have push access to Given the GitHub API server: @@ -226,19 +226,19 @@ """ When I successfully run `hub clone myorg/myrepo` Then it should clone "git@git.my.org:myorg/myrepo.git" - And there should be no output + And the output should not contain anything Scenario: Clone from existing directory is a local clone Given a directory named "dotfiles/.git" When I successfully run `hub clone dotfiles` Then the git command should be unchanged - And there should be no output + And the output should not contain anything Scenario: Clone from git bundle is a local clone Given a git bundle named "my-bundle" When I successfully run `hub clone my-bundle` Then the git command should be unchanged - And there should be no output + And the output should not contain anything Scenario: Clone a wiki Given the GitHub API server: @@ -252,7 +252,7 @@ """ When I successfully run `hub clone rtomayko/ronn.wiki` Then it should clone "git://github.com/RTomayko/ronin.wiki.git" - And there should be no output + And the output should not contain anything Scenario: Clone a nonexisting wiki Given the GitHub API server: @@ -285,4 +285,4 @@ """ When I successfully run `hub clone rtomayko/ronn` Then it should clone "git://github.com/RTomayko/ronin.git" - And there should be no output + And the output should not contain anything diff -Nru hub-2.7.0~ds1/features/compare.feature hub-2.14.2~ds1/features/compare.feature --- hub-2.7.0~ds1/features/compare.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/compare.feature 2020-03-05 17:48:23.000000000 +0000 @@ -5,54 +5,62 @@ Scenario: Compare branch When I successfully run `hub compare refactor` - Then there should be no output + Then the output should not contain anything And "open https://github.com/mislav/dotfiles/compare/refactor" should be run Scenario: Compare complex branch When I successfully run `hub compare feature/foo` - Then there should be no output + Then the output should not contain anything And "open https://github.com/mislav/dotfiles/compare/feature/foo" should be run Scenario: Compare branch with funky characters When I successfully run `hub compare 'my#branch!with.special+chars'` - Then there should be no output + Then the output should not contain anything And "open https://github.com/mislav/dotfiles/compare/my%23branch!with.special%2Bchars" should be run Scenario: No args, no upstream When I run `hub compare` Then the exit status should be 1 - And the stderr should contain: - """ - Usage: hub compare [-u] [-b ] [] [[...]] - """ + And the stderr should contain exactly "the current branch 'master' doesn't seem pushed to a remote" Scenario: Can't compare default branch to self Given the default branch for "origin" is "develop" And I am on the "develop" branch with upstream "origin/develop" When I run `hub compare` Then the exit status should be 1 - And the stderr should contain: - """ - Usage: hub compare [-u] [-b ] [] [[...]] - """ + And the stderr should contain exactly "the branch to compare 'develop' is the default branch" Scenario: No args, has upstream branch Given I am on the "feature" branch with upstream "origin/experimental" And git "push.default" is set to "upstream" When I successfully run `hub compare` - Then there should be no output + Then the output should not contain anything And "open https://github.com/mislav/dotfiles/compare/experimental" should be run Scenario: Current branch has funky characters Given I am on the "feature" branch with upstream "origin/my#branch!with.special+chars" And git "push.default" is set to "upstream" When I successfully run `hub compare` - Then there should be no output + Then the output should not contain anything And "open https://github.com/mislav/dotfiles/compare/my%23branch!with.special%2Bchars" should be run + Scenario: Current branch pushed to fork + Given I am "monalisa" on github.com with OAuth token "MONATOKEN" + And the "monalisa" remote has url "git@github.com:monalisa/dotfiles.git" + And I am on the "topic" branch pushed to "monalisa/topic" + When I successfully run `hub compare` + Then "open https://github.com/mislav/dotfiles/compare/monalisa:topic" should be run + + Scenario: Current branch with full URL in upstream configuration + Given I am on the "local-topic" branch + When I successfully run `git config branch.local-topic.remote https://github.com/monalisa/dotfiles.git` + When I successfully run `git config branch.local-topic.merge refs/remotes/remote-topic` + When I successfully run `hub compare` + Then "open https://github.com/mislav/dotfiles/compare/monalisa:remote-topic" should be run + Scenario: Compare range When I successfully run `hub compare 1.0...fix` - Then there should be no output + Then the output should not contain anything And "open https://github.com/mislav/dotfiles/compare/1.0...fix" should be run Scenario: Output URL without opening the browser @@ -67,14 +75,14 @@ Given I am on the "feature" branch with upstream "origin/experimental" And git "push.default" is set to "upstream" When I successfully run `hub compare -b master` - Then there should be no output + Then the output should not contain anything And "open https://github.com/mislav/dotfiles/compare/master...experimental" should be run Scenario: Compare base in master branch Given I am on the "master" branch with upstream "origin/master" And git "push.default" is set to "upstream" When I successfully run `hub compare -b experimental` - Then there should be no output + Then the output should not contain anything And "open https://github.com/mislav/dotfiles/compare/experimental...master" should be run Scenario: Compare base with same branch as the current branch @@ -83,50 +91,49 @@ When I run `hub compare -b experimental` Then "open https://github.com/mislav/dotfiles/compare/experimental...experimental" should not be run And the exit status should be 1 - And the stderr should contain: - """ - Usage: hub compare [-u] [-b ] [] [[...]] - """ + And the stderr should contain exactly "the branch to compare 'experimental' is the same as --base\n" Scenario: Compare base with parameters Given I am on the "master" branch with upstream "origin/master" When I run `hub compare -b master experimental..master` Then "open https://github.com/mislav/dotfiles/compare/experimental...master" should not be run And the exit status should be 1 - And the stderr should contain: - """ - Usage: hub compare [-u] [-b ] [] [[...]] - """ + And the stderr should contain "Usage: hub compare" Scenario: Compare 2-dots range for tags When I successfully run `hub compare 1.0..fix` - Then there should be no output + Then the output should not contain anything And "open https://github.com/mislav/dotfiles/compare/1.0...fix" should be run Scenario: Compare 2-dots range for SHAs When I successfully run `hub compare 1234abc..3456cde` - Then there should be no output + Then the output should not contain anything And "open https://github.com/mislav/dotfiles/compare/1234abc...3456cde" should be run Scenario: Compare 2-dots range with "user:repo" notation When I successfully run `hub compare henrahmagix:master..2b10927` - Then there should be no output + Then the output should not contain anything And "open https://github.com/mislav/dotfiles/compare/henrahmagix:master...2b10927" should be run + Scenario: Compare 2-dots range with slashes in branch names + When I successfully run `hub compare one/foo..two/bar/baz` + Then the output should not contain anything + And "open https://github.com/mislav/dotfiles/compare/one/foo...two/bar/baz" should be run + Scenario: Complex range is unchanged When I successfully run `hub compare @{a..b}..@{c..d}` - Then there should be no output + Then the output should not contain anything And "open https://github.com/mislav/dotfiles/compare/@{a..b}..@{c..d}" should be run Scenario: Compare wiki Given the "origin" remote has url "git://github.com/mislav/dotfiles.wiki.git" When I successfully run `hub compare 1.0..fix` - Then there should be no output + Then the output should not contain anything And "open https://github.com/mislav/dotfiles/wiki/_compare/1.0...fix" should be run Scenario: Compare fork When I successfully run `hub compare anotheruser feature` - Then there should be no output + Then the output should not contain anything And "open https://github.com/anotheruser/dotfiles/compare/feature" should be run Scenario: Enterprise repo over HTTP @@ -134,7 +141,7 @@ And I am "mislav" on http://git.my.org with OAuth token "FITOKEN" And "git.my.org" is a whitelisted Enterprise host When I successfully run `hub compare refactor` - Then there should be no output + Then the output should not contain anything And "open http://git.my.org/mislav/dotfiles/compare/refactor" should be run Scenario: Enterprise repo with explicit upstream project @@ -142,7 +149,7 @@ And I am "mislav" on git.my.org with OAuth token "FITOKEN" And "git.my.org" is a whitelisted Enterprise host When I successfully run `hub compare fehmicansaglam a..b` - Then there should be no output + Then the output should not contain anything And "open https://git.my.org/fehmicansaglam/dotfiles/compare/a...b" should be run Scenario: Compare in non-GitHub repo @@ -160,5 +167,5 @@ Given I am in detached HEAD And I run `hub compare refactor...master` Then the exit status should be 0 - And there should be no output + And the output should not contain anything And "open https://github.com/mislav/dotfiles/compare/refactor...master" should be run diff -Nru hub-2.7.0~ds1/features/create.feature hub-2.14.2~ds1/features/create.feature --- hub-2.7.0~ds1/features/create.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/create.feature 2020-03-05 17:48:23.000000000 +0000 @@ -28,6 +28,18 @@ When I successfully run `hub create -p` Then the url for "origin" should be "git@github.com:mislav/dotfiles.git" + Scenario: Alternate origin remote name + Given the GitHub API server: + """ + post('/user/repos') { + status 201 + json :full_name => 'mislav/dotfiles' + } + """ + When I successfully run `hub create --remote-name=work` + Then the url for "work" should be "git@github.com:mislav/dotfiles.git" + And there should be no "origin" remote + Scenario: HTTPS is preferred Given the GitHub API server: """ @@ -127,7 +139,7 @@ And the stdout should contain exactly "https://github.com/mislav/dotfiles\n" And the stderr should contain exactly: """ - A git remote named "origin" already exists and is set to push to 'git://example.com/unrelated.git'.\n + A git remote named 'origin' already exists and is set to push to 'git://example.com/unrelated.git'.\n """ Scenario: Another remote already exists @@ -274,3 +286,28 @@ < Location: http://disney.com {"full_name":"mislav/dotfiles"}\n """ + + Scenario: Create Enterprise repo + Given I am "nsartor" on git.my.org with OAuth token "FITOKEN" + Given the GitHub API server: + """ + post('/api/v3/user/repos', :host_name => 'git.my.org') { + assert :private => false + status 201 + json :full_name => 'nsartor/dotfiles' + } + """ + And $GITHUB_HOST is "git.my.org" + When I successfully run `hub create` + Then the url for "origin" should be "git@git.my.org:nsartor/dotfiles.git" + And the output should contain exactly "https://git.my.org/nsartor/dotfiles\n" + + Scenario: Invalid GITHUB_HOST + Given I am "nsartor" on {} with OAuth token "FITOKEN" + And $GITHUB_HOST is "{}" + When I run `hub create` + Then the exit status should be 1 + And the stderr should contain exactly: + """ + invalid hostname: "{}"\n + """ diff -Nru hub-2.7.0~ds1/features/fetch.feature hub-2.14.2~ds1/features/fetch.feature --- hub-2.7.0~ds1/features/fetch.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/fetch.feature 2020-03-05 17:48:23.000000000 +0000 @@ -7,19 +7,19 @@ Scenario: Fetch existing remote When I successfully run `hub fetch origin` Then the git command should be unchanged - And there should be no output + And the output should not contain anything Scenario: Fetch existing remote from non-GitHub source Given the "origin" remote has url "ssh://dev@codeserver.dev.xxx.drush.in/~/repository.git" When I successfully run `hub fetch origin` Then the git command should be unchanged - And there should be no output + And the output should not contain anything Scenario: Fetch from non-GitHub source via refspec Given the "origin" remote has url "ssh://dev@codeserver.dev.xxx.drush.in/~/repository.git" When I successfully run `hub fetch ssh://myusername@a.specific.server:1234/existing-project/gerrit-project-name refs/changes/16/6116/1` Then the git command should be unchanged - And there should be no output + And the output should not contain anything Scenario: Fetch from local bundle Given the GitHub API server: @@ -32,7 +32,7 @@ And a git bundle named "mislav" When I successfully run `hub fetch mislav` Then the git command should be unchanged - And there should be no output + And the output should not contain anything And there should be no "mislav" remote Scenario: Creates new remote @@ -46,7 +46,7 @@ When I successfully run `hub fetch mislav` Then "git fetch mislav" should be run And the url for "mislav" should be "git://github.com/mislav/dotfiles.git" - And there should be no output + And the output should not contain anything Scenario: Owner name with dash Given the GitHub API server: @@ -59,7 +59,7 @@ When I successfully run `hub fetch ankit-maverick` Then "git fetch ankit-maverick" should be run And the url for "ankit-maverick" should be "git://github.com/ankit-maverick/dotfiles.git" - And there should be no output + And the output should not contain anything Scenario: HTTPS is preferred Given the GitHub API server: @@ -85,7 +85,7 @@ When I successfully run `hub fetch mislav` Then "git fetch mislav" should be run And the url for "mislav" should be "git@github.com:mislav/dotfiles.git" - And there should be no output + And the output should not contain anything Scenario: Writeable repo Given the GitHub API server: @@ -98,7 +98,7 @@ When I successfully run `hub fetch mislav` Then "git fetch mislav" should be run And the url for "mislav" should be "git@github.com:mislav/dotfiles.git" - And there should be no output + And the output should not contain anything Scenario: Fetch with options Given the GitHub API server: diff -Nru hub-2.7.0~ds1/features/fork.feature hub-2.14.2~ds1/features/fork.feature --- hub-2.7.0~ds1/features/fork.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/fork.feature 2020-03-05 17:48:23.000000000 +0000 @@ -93,7 +93,7 @@ } """ When I successfully run `hub fork --no-remote` - Then there should be no output + Then the output should not contain anything And there should be no "mislav" remote Scenario: Fork failed diff -Nru hub-2.7.0~ds1/features/gist.feature hub-2.14.2~ds1/features/gist.feature --- hub-2.7.0~ds1/features/gist.feature 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/features/gist.feature 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,220 @@ +Feature: hub gist + Background: + Given I am "octokitten" on github.com with OAuth token "OTOKEN" + + Scenario: Fetch a gist with a single file + Given the GitHub API server: + """ + get('/gists/myhash') { + json({ + :files => { + 'hub_gist1.txt' => { + 'content' => "my content is here", + } + }, + :description => "my gist", + }) + } + """ + When I successfully run `hub gist show myhash` + Then the output should contain exactly: + """ + my content is here + """ + + Scenario: Fetch a gist with many files + Given the GitHub API server: + """ + get('/gists/myhash') { + json({ + :files => { + 'hub_gist1.txt' => { + 'content' => "my content is here" + }, + 'hub_gist2.txt' => { + 'content' => "more content is here" + } + }, + :description => "my gist", + :id => "myhash", + }) + } + """ + When I run `hub gist show myhash` + Then the exit status should be 1 + Then the stderr should contain: + """ + This gist contains multiple files, you must specify one: + hub_gist1.txt + hub_gist2.txt + """ + + Scenario: Fetch a single file from gist + Given the GitHub API server: + """ + get('/gists/myhash') { + json({ + :files => { + 'hub_gist1.txt' => { + 'content' => "my content is here" + }, + 'hub_gist2.txt' => { + 'content' => "more content is here" + } + }, + :description => "my gist", + :id => "myhash", + }) + } + """ + When I successfully run `hub gist show myhash hub_gist1.txt` + Then the output should contain exactly: + """ + my content is here + """ + + Scenario: Create a gist from file + Given the GitHub API server: + """ + post('/gists') { + status 201 + json :html_url => 'http://gists.github.com/somehash' + } + """ + Given a file named "testfile.txt" with: + """ + this is a test file + """ + When I successfully run `hub gist create testfile.txt` + Then the output should contain exactly: + """ + http://gists.github.com/somehash + """ + + Scenario: Open the new gist in a browser + Given the GitHub API server: + """ + post('/gists') { + status 201 + json :html_url => 'http://gists.github.com/somehash' + } + """ + Given a file named "testfile.txt" with: + """ + this is a test file + """ + When I successfully run `hub gist create -o testfile.txt` + Then the output should contain exactly "" + And "open http://gists.github.com/somehash" should be run + + Scenario: Create a gist with multiple files + Given the GitHub API server: + """ + post('/gists') { + halt 400 unless params[:files]["testfile.txt"]["content"] + halt 400 unless params[:files]["testfile2.txt"]["content"] + status 201 + json({ + :html_url => 'http://gists.github.com/somehash', + }) + } + """ + Given a file named "testfile.txt" with: + """ + this is a test file + """ + Given a file named "testfile2.txt" with: + """ + this is another test file + """ + When I successfully run `hub gist create testfile.txt testfile2.txt` + Then the output should contain exactly: + """ + http://gists.github.com/somehash + """ + + Scenario: Create a gist from stdin + Given the GitHub API server: + """ + post('/gists') { + halt 400 unless params[:files]["gistfile1.txt"]["content"] == "hello\n" + status 201 + json :html_url => 'http://gists.github.com/somehash' + } + """ + When I run `hub gist create` interactively + And I pass in: + """ + hello + """ + Then the output should contain exactly: + """ + http://gists.github.com/somehash + """ + + Scenario: Insufficient OAuth scopes + Given the GitHub API server: + """ + post('/gists') { + status 404 + response.headers['x-accepted-oauth-scopes'] = 'gist' + response.headers['x-oauth-scopes'] = 'repos' + json({}) + } + """ + Given a file named "testfile.txt" with: + """ + this is a test file + """ + When I run `hub gist create testfile.txt` + Then the exit status should be 1 + And the stderr should contain exactly: + """ + Error creating gist: Not Found (HTTP 404) + Your access token may have insufficient scopes. Visit http://github.com/settings/tokens + to edit the 'hub' token and enable one of the following scopes: gist + """ + + Scenario: Infer correct OAuth scopes for gist + Given the GitHub API server: + """ + post('/gists') { + status 404 + response.headers['x-oauth-scopes'] = 'repos' + json({}) + } + """ + Given a file named "testfile.txt" with: + """ + this is a test file + """ + When I run `hub gist create testfile.txt` + Then the exit status should be 1 + And the stderr should contain exactly: + """ + Error creating gist: Not Found (HTTP 404) + Your access token may have insufficient scopes. Visit http://github.com/settings/tokens + to edit the 'hub' token and enable one of the following scopes: gist + """ + + Scenario: Create error + Given the GitHub API server: + """ + post('/gists') { + status 404 + response.headers['x-accepted-oauth-scopes'] = 'gist' + response.headers['x-oauth-scopes'] = 'repos, gist' + json({}) + } + """ + Given a file named "testfile.txt" with: + """ + this is a test file + """ + When I run `hub gist create testfile.txt` + Then the exit status should be 1 + And the stderr should contain exactly: + """ + Error creating gist: Not Found (HTTP 404)\n + """ + diff -Nru hub-2.7.0~ds1/features/git_compatibility.feature hub-2.14.2~ds1/features/git_compatibility.feature --- hub-2.7.0~ds1/features/git_compatibility.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/git_compatibility.feature 2020-03-05 17:48:23.000000000 +0000 @@ -14,12 +14,14 @@ branch commit alias + api browse ci-status compare create delete fork + gist issue pr pull-request @@ -30,3 +32,8 @@ Scenario: Doesn't sabotage --exec-path When I successfully run `hub --exec-path` Then the output should not contain "These GitHub commands" + + Scenario: Shows help with --git-dir + When I run `hub --git-dir=.git` + Then the exit status should be 1 + And the output should contain "usage: git " diff -Nru hub-2.7.0~ds1/features/help.feature hub-2.14.2~ds1/features/help.feature --- hub-2.7.0~ds1/features/help.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/help.feature 2020-03-05 17:48:23.000000000 +0000 @@ -5,14 +5,48 @@ """ These GitHub commands are provided by hub: - browse Open a GitHub page in the default browser + api Low-level GitHub API request interface """ And the output should contain "usage: git " + Scenario: Shows help text with no arguments + When I run `hub` + Then the stdout should contain "usage: git " + And the stderr should contain exactly "" + And the exit status should be 1 + Scenario: Appends hub commands to `--all` output When I successfully run `hub help -a` Then the output should contain "pull-request" - Scenario: Shows help for a subcommand + Scenario: Shows help for a hub extension When I successfully run `hub help hub-help` - Then the output should contain "Usage: hub help" + Then "man hub-help" should be run + + Scenario: Shows help for a hub command + When I successfully run `hub help fork` + Then "man hub-fork" should be run + + Scenario: Show help in HTML format + When I successfully run `hub help -w fork` + Then "man hub-fork" should not be run + And "git web--browse PATH/hub-fork.1.html" should be run + + Scenario: Show help in HTML format by default + Given I successfully run `git config --global help.format html` + When I successfully run `hub help fork` + Then "git web--browse PATH/hub-fork.1.html" should be run + + Scenario: Override HTML format back to man + Given I successfully run `git config --global help.format html` + When I successfully run `hub help -m fork` + Then "man hub-fork" should be run + + Scenario: The --help flag opens man page + When I successfully run `hub fork --help` + Then "man hub-fork" should be run + + Scenario: The --help flag expands alias first + Given I successfully run `git config --global alias.ci ci-status` + When I successfully run `hub ci --help` + Then "man hub-ci-status" should be run diff -Nru hub-2.7.0~ds1/features/issue.feature hub-2.14.2~ds1/features/issue.feature --- hub-2.7.0~ds1/features/issue.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/issue.feature 2020-03-05 17:48:23.000000000 +0000 @@ -116,6 +116,34 @@ """ When I successfully run `hub issue -M none` + Scenario: Fetch issues assigned to milestone by number + Given the GitHub API server: + """ + get('/repos/github/hub/issues') { + assert :milestone => "12" + json [] + } + """ + When I successfully run `hub issue -M 12` + + Scenario: Fetch issues assigned to milestone by name + Given the GitHub API server: + """ + get('/repos/github/hub/milestones') { + status 200 + json [ + { :number => 237, :title => "prerelease" }, + { :number => 1337, :title => "v1" }, + { :number => 41319, :title => "Hello World!" } + ] + } + get('/repos/github/hub/issues') { + assert :milestone => "1337" + json [] + } + """ + When I successfully run `hub issue -M v1` + Scenario: Fetch issues created by a given user Given the GitHub API server: """ @@ -247,6 +275,38 @@ 13,mislav\n """ + Scenario: Custom format with no-color labels + Given the GitHub API server: + """ + get('/repos/github/hub/issues') { + json [ + { :number => 102, + :title => "First issue", + :state => "open", + :user => { :login => "morganwahl" }, + :labels => [ + { :name => 'Has Migration', + :color => 'cfcfcf' }, + { :name => 'Maintenance Window', + :color => '888888' }, + ] + }, + { :number => 201, + :title => "No labels", + :state => "open", + :user => { :login => "octocat" }, + :labels => [] + }, + ] + } + """ + When I successfully run `hub issue -f "%I: %L%n" --color=never` + Then the output should contain exactly: + """ + 102: Has Migration, Maintenance Window + 201: \n + """ + Scenario: List all assignees Given the GitHub API server: """ @@ -343,7 +403,30 @@ json :html_url => "https://github.com/github/hub/issues/1337" } """ - When I successfully run `hub issue create -m "hello" -M 12 -a mislav,josh -apcorpet` + When I successfully run `hub issue create -m "hello" -M 12 --assign mislav,josh -apcorpet` + Then the output should contain exactly: + """ + https://github.com/github/hub/issues/1337\n + """ + + Scenario: Create an issue with milestone by name + Given the GitHub API server: + """ + get('/repos/github/hub/milestones') { + status 200 + json [ + { :number => 237, :title => "prerelease" }, + { :number => 1337, :title => "v1" }, + { :number => 41319, :title => "Hello World!" } + ] + } + post('/repos/github/hub/issues') { + assert :milestone => 41319 + status 201 + json :html_url => "https://github.com/github/hub/issues/1337" + } + """ + When I successfully run `hub issue create -m "hello" -M "hello world!"` Then the output should contain exactly: """ https://github.com/github/hub/issues/1337\n @@ -510,11 +593,175 @@ https://github.com/github/hub/issues/1337\n """ + Scenario: Update an issue's title + Given the GitHub API server: + """ + patch('/repos/github/hub/issues/1337') { + assert :title => "Not workie, pls fix", + :body => "", + :milestone => :no, + :assignees => :no, + :labels => :no, + :state => :no + } + """ + Then I successfully run `hub issue update 1337 -m "Not workie, pls fix"` + + Scenario: Update an issue's state + Given the GitHub API server: + """ + patch('/repos/github/hub/issues/1337') { + assert :title => :no, + :labels => :no, + :state => "closed" + } + """ + Then I successfully run `hub issue update 1337 -s closed` + + Scenario: Update an issue's labels + Given the GitHub API server: + """ + patch('/repos/github/hub/issues/1337') { + assert :title => :no, + :body => :no, + :milestone => :no, + :assignees => :no, + :labels => ["bug", "important"] + } + """ + Then I successfully run `hub issue update 1337 -l bug,important` + + Scenario: Update an issue's milestone + Given the GitHub API server: + """ + patch('/repos/github/hub/issues/1337') { + assert :title => :no, + :body => :no, + :milestone => 42, + :assignees => :no, + :labels => :no + } + """ + Then I successfully run `hub issue update 1337 -M 42` + + Scenario: Upate an issue's milestone by name + Given the GitHub API server: + """ + get('/repos/github/hub/milestones') { + status 200 + json [ + { :number => 237, :title => "prerelease" }, + { :number => 42, :title => "Hello World!" } + ] + } + patch('/repos/github/hub/issues/1337') { + assert :title => :no, + :body => :no, + :milestone => 42, + :assignees => :no, + :labels => :no + } + """ + Then I successfully run `hub issue update 1337 -M "hello world!"` + + Scenario: Update an issue's assignees + Given the GitHub API server: + """ + patch('/repos/github/hub/issues/1337') { + assert :title => :no, + :body => :no, + :milestone => :no, + :assignees => ["Cornwe19"], + :labels => :no + } + """ + Then I successfully run `hub issue update 1337 -a Cornwe19` + + Scenario: Update an issue's title, labels, milestone, and assignees + Given the GitHub API server: + """ + patch('/repos/github/hub/issues/1337') { + assert :title => "Not workie, pls fix", + :body => "", + :milestone => 42, + :assignees => ["Cornwe19"], + :labels => ["bug", "important"] + } + """ + Then I successfully run `hub issue update 1337 -m "Not workie, pls fix" -M 42 -l bug,important -a Cornwe19` + + Scenario: Clear existing issue labels, assignees, milestone + Given the GitHub API server: + """ + patch('/repos/github/hub/issues/1337') { + assert :title => :no, + :body => :no, + :milestone => nil, + :assignees => [], + :labels => [] + } + """ + Then I successfully run `hub issue update 1337 --milestone= --assign= --labels=` + + Scenario: Update an issue's title and body manually + Given the git commit editor is "vim" + And the text editor adds: + """ + My new title + """ + Given the GitHub API server: + """ + get('/repos/github/hub/issues/1337') { + json \ + :number => 1337, + :title => "My old title", + :body => "My old body" + } + patch('/repos/github/hub/issues/1337') { + assert :title => "My new title", + :body => "My old title\n\nMy old body", + :milestone => :no, + :assignees => :no, + :labels => :no + } + """ + Then I successfully run `hub issue update 1337 --edit` + + Scenario: Update an issue's title and body via a file + Given a file named "my-issue.md" with: + """ + My new title + + My new body + """ + Given the GitHub API server: + """ + patch('/repos/github/hub/issues/1337') { + assert :title => "My new title", + :body => "My new body", + :milestone => :no, + :assignees => :no, + :labels => :no + } + """ + Then I successfully run `hub issue update 1337 -F my-issue.md` + + Scenario: Update an issue without specifying fields to update + When I run `hub issue update 1337` + Then the exit status should be 1 + Then the stderr should contain "please specify fields to update" + Then the stderr should contain "Usage: hub issue" + Scenario: Fetch issue labels Given the GitHub API server: """ get('/repos/github/hub/labels') { + response.headers["Link"] = %(; rel="next") + assert :per_page => "100", :page => nil json [ + { :name => "Discuss", + :color => "0000ff", + }, { :name => "bug", :color => "ff0000", }, @@ -523,11 +770,21 @@ }, ] } + get('/repositories/12345/labels') { + assert :per_page => "100", :page => "2" + json [ + { :name => "affects", + :color => "ffffff", + }, + ] + } """ When I successfully run `hub issue labels` Then the output should contain exactly: """ + affects bug + Discuss feature\n """ @@ -637,7 +894,7 @@ Scenario: Did not supply an issue number When I run `hub issue show` Then the exit status should be 1 - Then the output should contain exactly "Usage: hub issue show \n" + Then the stderr should contain "Usage: hub issue" Scenario: Show error message if http code is not 200 for issues endpoint Given the GitHub API server: diff -Nru hub-2.7.0~ds1/features/issue-transfer.feature hub-2.14.2~ds1/features/issue-transfer.feature --- hub-2.7.0~ds1/features/issue-transfer.feature 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/features/issue-transfer.feature 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,73 @@ +Feature: hub issue transfer + Background: + Given I am in "git://github.com/octocat/hello-world.git" git repo + And I am "srafi1" on github.com with OAuth token "OTOKEN" + + Scenario: Transfer issue + Given the GitHub API server: + """ + count = 0 + post('/graphql') { + halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token OTOKEN' + count += 1 + case count + when 1 + assert :query => /\A\s*query\(/, + :variables => { + :issue => 123, + :sourceOwner => "octocat", + :sourceRepo => "hello-world", + :targetOwner => "octocat", + :targetRepo => "spoon-knife", + } + json :data => { + :source => { :issue => { :id => "ISSUE-ID" } }, + :target => { :id => "REPO-ID" }, + } + when 2 + assert :query => /\A\s*mutation\(/, + :variables => { + :issue => "ISSUE-ID", + :repo => "REPO-ID", + } + json :data => { + :transferIssue => { :issue => { :url => "the://url" } } + } + else + status 400 + json :message => "request not stubbed" + end + } + """ + When I successfully run `hub issue transfer 123 spoon-knife` + Then the output should contain exactly "the://url\n" + + Scenario: Transfer to another owner + Given the GitHub API server: + """ + count = 0 + post('/graphql') { + count += 1 + case count + when 1 + assert :variables => { + :targetOwner => "monalisa", + :targetRepo => "playground", + } + json :data => {} + when 2 + json :errors => [ + { :message => "New repository must have the same owner as the current repository" }, + ] + else + status 400 + json :message => "request not stubbed" + end + } + """ + When I run `hub issue transfer 123 monalisa/playground` + Then the exit status should be 1 + Then the stderr should contain exactly: + """ + API error: New repository must have the same owner as the current repository\n + """ diff -Nru hub-2.7.0~ds1/features/pr-checkout.feature hub-2.14.2~ds1/features/pr-checkout.feature --- hub-2.7.0~ds1/features/pr-checkout.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/pr-checkout.feature 2020-03-05 17:48:23.000000000 +0000 @@ -25,7 +25,7 @@ :html_url => 'https://github.com/mojombo/jekyll/pull/77' } """ - When I run `hub pr checkout 77` + When I successfully run `hub pr checkout 77` Then "git fetch origin refs/pull/77/head:fixes" should be run And "git checkout fixes" should be run And "fixes" should merge "refs/pull/77/head" from remote "origin" @@ -51,7 +51,7 @@ :html_url => 'https://github.com/mojombo/jekyll/pull/77' } """ - When I run `hub pr checkout 77 fixes-from-mislav` + When I successfully run `hub pr checkout 77 fixes-from-mislav` Then "git fetch origin refs/pull/77/head:fixes-from-mislav" should be run And "git checkout fixes-from-mislav" should be run And "fixes-from-mislav" should merge "refs/pull/77/head" from remote "origin" @@ -76,6 +76,7 @@ :html_url => 'https://github.com/mojombo/jekyll/pull/77' } """ - When I run `hub pr checkout 77` + When I successfully run `hub pr checkout 77` Then "git fetch origin +refs/heads/fixes:refs/remotes/origin/fixes" should be run - And "git checkout -b fixes --track origin/fixes" should be run + And "git checkout -b fixes --no-track origin/fixes" should be run + And "fixes" should merge "refs/heads/fixes" from remote "origin" diff -Nru hub-2.7.0~ds1/features/pr-list.feature hub-2.14.2~ds1/features/pr-list.feature --- hub-2.7.0~ds1/features/pr-list.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/pr-list.feature 2020-03-05 17:48:23.000000000 +0000 @@ -112,6 +112,73 @@ #102 luke, jyn\n """ + Scenario: List draft status + Given the GitHub API server: + """ + get('/repos/github/hub/pulls') { + halt 400 unless env['HTTP_ACCEPT'] == 'application/vnd.github.shadow-cat-preview+json;charset=utf-8' + + json [ + { :number => 999, + :state => "open", + :draft => true, + :merged_at => nil, + :base => { :ref => "master", :label => "github:master" }, + :head => { :ref => "patch-2", :label => "octocat:patch-2" }, + :user => { :login => "octocat" }, + }, + { :number => 102, + :state => "open", + :draft => false, + :merged_at => nil, + :base => { :ref => "master", :label => "github:master" }, + :head => { :ref => "patch-1", :label => "octocat:patch-1" }, + :user => { :login => "octocat" }, + }, + { :number => 42, + :state => "closed", + :draft => false, + :merged_at => "2018-12-11T10:50:33Z", + :base => { :ref => "master", :label => "github:master" }, + :head => { :ref => "patch-3", :label => "octocat:patch-3" }, + :user => { :login => "octocat" }, + }, + { :number => 8, + :state => "closed", + :draft => false, + :merged_at => nil, + :base => { :ref => "master", :label => "github:master" }, + :head => { :ref => "patch-4", :label => "octocat:patch-4" }, + :user => { :login => "octocat" }, + }, + ] + } + """ + When I successfully run `hub pr list --format "%I %pC %pS %Creset%n" --color` + Then its output should contain exactly: + """ + 999 \e[37m draft \e[m + 102 \e[32m open \e[m + 42 \e[35m merged \e[m + 8 \e[31m closed \e[m\n + """ + When I successfully run `hub -c color.ui=always pr list --format "%I %pC %pS %Creset%n"` + Then its output should contain exactly: + """ + 999 \e[37m draft \e[m + 102 \e[32m open \e[m + 42 \e[35m merged \e[m + 8 \e[31m closed \e[m\n + """ + When I successfully run `hub -c color.ui=false pr list --format "%I %pC%pS%Creset%n" --color=auto` + Then its output should contain exactly: + """ + 999 draft + 102 open + 42 merged + 8 closed\n + """ + Scenario: Sort by number of comments ascending Given the GitHub API server: """ diff -Nru hub-2.7.0~ds1/features/pr-show.feature hub-2.14.2~ds1/features/pr-show.feature --- hub-2.7.0~ds1/features/pr-show.feature 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/features/pr-show.feature 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,220 @@ +Feature: hub pr show + Background: + Given I am in "git://github.com/ashemesh/hub.git" git repo + And I am "ashemesh" on github.com with OAuth token "OTOKEN" + + Scenario: Current branch + Given I am on the "topic" branch + Given the GitHub API server: + """ + get('/repos/ashemesh/hub/pulls'){ + assert :state => "open", + :head => "ashemesh:topic" + json [ + { :html_url => "https://github.com/ashemesh/hub/pull/102" }, + ] + } + """ + When I successfully run `hub pr show` + Then "open https://github.com/ashemesh/hub/pull/102" should be run + + Scenario: Current branch output URL + Given I am on the "topic" branch + Given the GitHub API server: + """ + get('/repos/ashemesh/hub/pulls'){ + assert :state => "open", + :head => "ashemesh:topic" + json [ + { :html_url => "https://github.com/ashemesh/hub/pull/102" }, + ] + } + """ + When I successfully run `hub pr show -u` + Then "open https://github.com/ashemesh/hub/pull/102" should not be run + And the output should contain exactly: + """ + https://github.com/ashemesh/hub/pull/102\n + """ + + Scenario: Format Current branch output URL + Given I am on the "topic" branch + Given the GitHub API server: + """ + get('/repos/ashemesh/hub/pulls'){ + assert :state => "open", + :head => "ashemesh:topic" + json [{ + :number => 102, + :state => "open", + :base => { + :ref => "master", + :label => "github:master", + :repo => { :owner => { :login => "github" } } + }, + :head => { :ref => "patch-1", :label => "octocat:patch-1" }, + :user => { :login => "octocat" }, + :requested_reviewers => [ + { :login => "rey" }, + ], + :requested_teams => [ + { :slug => "troopers" }, + { :slug => "cantina-band" }, + ], + :html_url => "https://github.com/ashemesh/hub/pull/102", + }] + } + """ + When I successfully run `hub pr show -f "%sC%>(8)%i %rs%n"` + Then "open https://github.com/ashemesh/hub/pull/102" should not be run + And the output should contain exactly: + """ + #102 rey, github/troopers, github/cantina-band\n + """ + + Scenario: Current branch in fork + Given the "upstream" remote has url "git@github.com:github/hub.git" + And I am on the "topic" branch pushed to "origin/topic" + Given the GitHub API server: + """ + get('/repos/github/hub/pulls'){ + assert :state => "open", + :head => "ashemesh:topic" + json [ + { :html_url => "https://github.com/github/hub/pull/102" }, + ] + } + """ + When I successfully run `hub pr show` + Then "open https://github.com/github/hub/pull/102" should be run + + Scenario: Differently named branch in fork + Given the "upstream" remote has url "git@github.com:github/hub.git" + And I am on the "local-topic" branch with upstream "origin/remote-topic" + Given the GitHub API server: + """ + get('/repos/github/hub/pulls'){ + assert :head => "ashemesh:remote-topic" + json [ + { :html_url => "https://github.com/github/hub/pull/102" }, + ] + } + """ + When I successfully run `hub pr show` + Then "open https://github.com/github/hub/pull/102" should be run + + Scenario: Upstream configuration with HTTPS URL + Given I am on the "local-topic" branch + When I successfully run `git config branch.local-topic.remote https://github.com/octocat/hub.git` + When I successfully run `git config branch.local-topic.merge refs/remotes/remote-topic` + Given the GitHub API server: + """ + get('/repos/ashemesh/hub/pulls'){ + assert :head => "octocat:remote-topic" + json [ + { :html_url => "https://github.com/github/hub/pull/102" }, + ] + } + """ + When I successfully run `hub pr show` + Then "open https://github.com/github/hub/pull/102" should be run + + Scenario: Upstream configuration with SSH URL + Given I am on the "local-topic" branch + When I successfully run `git config branch.local-topic.remote git@github.com:octocat/hub.git` + When I successfully run `git config branch.local-topic.merge refs/remotes/remote-topic` + Given the GitHub API server: + """ + get('/repos/ashemesh/hub/pulls'){ + assert :head => "octocat:remote-topic" + json [ + { :html_url => "https://github.com/github/hub/pull/102" }, + ] + } + """ + When I successfully run `hub pr show` + Then "open https://github.com/github/hub/pull/102" should be run + + Scenario: Explicit head branch + Given the GitHub API server: + """ + get('/repos/ashemesh/hub/pulls'){ + assert :state => "open", + :head => "ashemesh:topic" + json [ + { :html_url => "https://github.com/ashemesh/hub/pull/102" }, + ] + } + """ + When I successfully run `hub pr show --head topic` + Then "open https://github.com/ashemesh/hub/pull/102" should be run + + Scenario: Explicit head branch with owner + Given the GitHub API server: + """ + get('/repos/ashemesh/hub/pulls'){ + assert :state => "open", + :head => "github:topic" + json [ + { :html_url => "https://github.com/ashemesh/hub/pull/102" }, + ] + } + """ + When I successfully run `hub pr show --head github:topic` + Then "open https://github.com/ashemesh/hub/pull/102" should be run + + Scenario: No pull request found + Given the GitHub API server: + """ + get('/repos/ashemesh/hub/pulls'){ + json [] + } + """ + When I run `hub pr show --head topic` + Then the exit status should be 1 + And the stderr should contain exactly: + """ + no open pull requests found for branch 'ashemesh:topic'\n + """ + + Scenario: Show pull request by number + When I successfully run `hub pr show 102` + Then "open https://github.com/ashemesh/hub/pull/102" should be run + + Scenario: Format pull request by number + Given the GitHub API server: + """ + get('/repos/ashemesh/hub/pulls/102') { + json :number => 102, + :title => "First", + :state => "open", + :base => { + :ref => "master", + :label => "github:master", + :repo => { :owner => { :login => "github" } } + }, + :head => { :ref => "patch-1", :label => "octocat:patch-1" }, + :user => { :login => "octocat" }, + :requested_reviewers => [ + { :login => "rey" }, + ], + :requested_teams => [ + { :slug => "troopers" }, + { :slug => "cantina-band" }, + ] + } + """ + When I successfully run `hub pr show 102 -f "%sC%>(8)%i %rs%n"` + Then "open https://github.com/ashemesh/hub/pull/102" should not be run + And the output should contain exactly: + """ + #102 rey, github/troopers, github/cantina-band\n + """ + + Scenario: Show pull request by invalid number + When I run `hub pr show XYZ` + Then the exit status should be 1 + And the stderr should contain exactly: + """ + invalid pull request number: 'XYZ'\n + """ diff -Nru hub-2.7.0~ds1/features/pull_request.feature hub-2.14.2~ds1/features/pull_request.feature --- hub-2.7.0~ds1/features/pull_request.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/pull_request.feature 2020-03-05 17:48:23.000000000 +0000 @@ -4,12 +4,48 @@ And I am "mislav" on github.com with OAuth token "OTOKEN" And the git commit editor is "vim" + Scenario: Basic pull request + Given I am on the "topic" branch pushed to "origin/topic" + Given the GitHub API server: + """ + KNOWN_PARAMS = %w[title body base head draft issue maintainer_can_modify] + post('/repos/mislav/coral/pulls') { + halt 400 unless request.env['HTTP_ACCEPT'] == 'application/vnd.github.shadow-cat-preview+json;charset=utf-8' + halt 400 unless request.user_agent.include?('Hub') + halt 400 if (params.keys - KNOWN_PARAMS).any? + assert :title => 'hello', + :body => nil, + :base => 'master', + :head => 'mislav:topic', + :maintainer_can_modify => true, + :draft => nil, + :issue => nil + status 201 + json :html_url => "the://url" + } + """ + When I successfully run `hub pull-request -m hello` + Then the output should contain exactly "the://url\n" + Scenario: Detached HEAD Given I am in detached HEAD When I run `hub pull-request` Then the stderr should contain "Aborted: not currently on any branch.\n" And the exit status should be 1 + Scenario: Detached HEAD with explicit head + Given I am in detached HEAD + Given the GitHub API server: + """ + post('/repos/mislav/coral/pulls') { + assert :head => 'mislav:feature' + status 201 + json :html_url => "the://url" + } + """ + When I successfully run `hub pull-request -h feature -m message` + Then the output should contain exactly "the://url\n" + Scenario: Non-GitHub repo Given the "origin" remote has url "mygh:Manganeez/repo.git" When I run `hub pull-request` @@ -22,11 +58,12 @@ Scenario: Create pull request respecting "insteadOf" configuration Given the "origin" remote has url "mygh:Manganeez/repo.git" When I successfully run `git config url."git@github.com:".insteadOf mygh:` + Given I am on the "topic" branch pushed to "origin/topic" Given the GitHub API server: """ post('/repos/Manganeez/repo/pulls') { assert :base => 'master', - :head => 'Manganeez:master', + :head => 'Manganeez:topic', :title => 'here we go' status 201 json :html_url => "https://github.com/Manganeez/repo/pull/12" @@ -36,6 +73,7 @@ Then the output should contain exactly "https://github.com/Manganeez/repo/pull/12\n" Scenario: With Unicode characters + Given I am on the "topic" branch pushed to "origin/topic" Given the GitHub API server: """ post('/repos/mislav/coral/pulls') { @@ -97,6 +135,7 @@ Hello Signed-off-by: NAME + Co-authored-by: NAME """ And the "topic" branch is pushed to "origin/topic" When I successfully run `hub pull-request` @@ -237,7 +276,8 @@ Scenario: No commits with "--no-edit" Given I am on the "master" branch pushed to "origin/master" When I successfully run `git checkout --quiet -b topic` - And I run `hub pull-request --no-edit` + Given the "topic" branch is pushed to "origin/topic" + When I run `hub pull-request --no-edit` Then the exit status should be 1 And the stderr should contain exactly: """ @@ -296,7 +336,7 @@ """ post('/repos/origin/coral/pulls') { 404 } """ - When I run `hub pull-request -b origin:master -m here` + When I run `hub pull-request -b origin:master -h topic -m here` Then the exit status should be 1 Then the stderr should contain: """ @@ -304,19 +344,8 @@ Are you sure that github.com/origin/coral exists? """ - Scenario: Supplies User-Agent string to API calls - Given the GitHub API server: - """ - post('/repos/mislav/coral/pulls') { - halt 400 unless request.user_agent.include?('Hub') - status 201 - json :html_url => "the://url" - } - """ - When I successfully run `hub pull-request -m useragent` - Then the output should contain exactly "the://url\n" - Scenario: Text editor adds title and body + Given I am on the "topic" branch pushed to "origin/topic" Given the text editor adds: """ This title comes from vim! @@ -337,6 +366,7 @@ And the file ".git/PULLREQ_EDITMSG" should not exist Scenario: Text editor adds title and body with multiple lines + Given I am on the "topic" branch pushed to "origin/topic" Given the text editor adds: """ @@ -365,6 +395,7 @@ And the file ".git/PULLREQ_EDITMSG" should not exist Scenario: Text editor with custom commentchar + Given I am on the "topic" branch pushed to "origin/topic" Given git "core.commentchar" is set to "/" And the text editor adds: """ @@ -387,6 +418,7 @@ Then the output should contain exactly "the://url\n" Scenario: Failed pull request preserves previous message + Given I am on the "topic" branch pushed to "origin/topic" Given the text editor adds: """ This title will fail @@ -415,6 +447,7 @@ Then the file ".git/PULLREQ_EDITMSG" should not exist Scenario: Text editor fails + Given I am on the "topic" branch pushed to "origin/topic" Given the text editor exits with error status And an empty file named ".git/PULLREQ_EDITMSG" When I run `hub pull-request` @@ -423,6 +456,7 @@ And the file ".git/PULLREQ_EDITMSG" should not exist Scenario: Title and body from file + Given I am on the "topic" branch pushed to "origin/topic" Given the GitHub API server: """ post('/repos/mislav/coral/pulls') { @@ -445,6 +479,7 @@ And the file ".git/PULLREQ_EDITMSG" should not exist Scenario: Edit title and body from file + Given I am on the "topic" branch pushed to "origin/topic" Given the GitHub API server: """ post('/repos/mislav/coral/pulls') { @@ -468,6 +503,7 @@ Then the file ".git/PULLREQ_EDITMSG" should not exist Scenario: Title and body from stdin + Given I am on the "topic" branch pushed to "origin/topic" Given the GitHub API server: """ post('/repos/mislav/coral/pulls') { @@ -489,6 +525,7 @@ And the file ".git/PULLREQ_EDITMSG" should not exist Scenario: Title and body from command-line argument + Given I am on the "topic" branch pushed to "origin/topic" Given the GitHub API server: """ post('/repos/mislav/coral/pulls') { @@ -503,6 +540,7 @@ And the file ".git/PULLREQ_EDITMSG" should not exist Scenario: Title and body from multiple command-line arguments + Given I am on the "topic" branch pushed to "origin/topic" Given the GitHub API server: """ post('/repos/mislav/coral/pulls') { @@ -551,11 +589,12 @@ Then the output should contain exactly "the://url\n" Scenario: Explicit base - Given I am on the "feature" branch + Given I am on the "feature" branch pushed to "origin/feature" Given the GitHub API server: """ post('/repos/mislav/coral/pulls') { - assert :base => 'develop' + assert :base => 'develop', + :head => 'mislav:feature' status 201 json :html_url => "the://url" } @@ -565,7 +604,7 @@ Scenario: Implicit base by detecting main branch Given the default branch for "origin" is "develop" - And I make a commit + And the "master" branch is pushed to "origin/master" Given the GitHub API server: """ post('/repos/mislav/coral/pulls') { @@ -579,7 +618,7 @@ Then the output should contain exactly "the://url\n" Scenario: Explicit base with owner - Given I am on the "master" branch + Given I am on the "master" branch pushed to "origin/master" Given the GitHub API server: """ post('/repos/mojombo/coral/pulls') { @@ -592,7 +631,7 @@ Then the output should contain exactly "the://url\n" Scenario: Explicit base with owner and repo name - Given I am on the "master" branch + Given I am on the "master" branch pushed to "origin/master" Given the GitHub API server: """ post('/repos/mojombo/coralify/pulls') { @@ -628,6 +667,26 @@ And I successfully run `hub pull-request -f -m message` Then the output should contain exactly "the://url\n" + Scenario: Error from an unpushed branch + Given I am on the "feature" branch + When I run `hub pull-request -m hello` + Then the exit status should be 1 + And the stderr should contain exactly: + """ + Aborted: the current branch seems not yet pushed to a remote + (use `-p` to push the branch or `-f` to skip this check)\n + """ + + Scenario: Error from an unpushed branch with upstream same as base branch + Given I am on the "feature" branch with upstream "origin/master" + When I run `hub pull-request -m hello` + Then the exit status should be 1 + And the stderr should contain exactly: + """ + Aborted: the current branch seems not yet pushed to a remote + (use `-p` to push the branch or `-f` to skip this check)\n + """ + Scenario: Pull request fails on the server Given I am on the "feature" branch with upstream "origin/feature" Given the GitHub API server: @@ -692,11 +751,12 @@ Given the "origin" remote has url "git@git.my.org:mislav/coral.git" And I am "mislav" on git.my.org with OAuth token "FITOKEN" And "git.my.org" is a whitelisted Enterprise host + And I am on the "topic" branch pushed to "origin/topic" Given the GitHub API server: """ post('/api/v3/repos/mislav/coral/pulls', :host_name => 'git.my.org') { assert :base => 'master', - :head => 'mislav:master' + :head => 'mislav:topic' status 201 json :html_url => "the://url" } @@ -840,7 +900,26 @@ When I successfully run `hub pull-request -m hereyougo` Then the output should contain exactly "the://url\n" + Scenario: Create pull request to "github" remote when "origin" is non-GitHub + Given the "github" remote has url "git@github.com:sam-hart-swanson/debug.git" + Given the "origin" remote has url "ssh://git@private.server.com/path/to/repo.git" + And I am on the "feat/123-some-branch" branch pushed to "github/feat/123-some-branch" + And an empty file named ".git/refs/remotes/origin/feat/123-some-branch" + Given the GitHub API server: + """ + post('/repos/sam-hart-swanson/debug/pulls') { + assert :base => 'master', + :head => 'sam-hart-swanson:feat/123-some-branch', + :title => 'hereyougo' + status 201 + json :html_url => "the://url" + } + """ + When I successfully run `hub pull-request -m hereyougo` + Then the output should contain exactly "the://url\n" + Scenario: Open pull request in web browser + Given I am on the "topic" branch pushed to "origin/topic" Given the GitHub API server: """ post('/repos/mislav/coral/pulls') { @@ -853,8 +932,9 @@ Scenario: Current branch is tracking local branch Given git "push.default" is set to "upstream" - And I make a commit - And I am on the "feature" branch with upstream "refs/heads/master" + And I am on the "feature" branch pushed to "origin/feature" + When I successfully run `git config branch.feature.remote .` + When I successfully run `git config branch.feature.merge refs/heads/master` Given the GitHub API server: """ post('/repos/mislav/coral/pulls') { @@ -867,6 +947,22 @@ When I successfully run `hub pull-request -m hereyougo` Then the output should contain exactly "the://url\n" + Scenario: Current branch is pushed to remote without upstream configuration + Given the "upstream" remote has url "git://github.com/lestephane/coral.git" + And I am on the "feature" branch pushed to "origin/feature" + And git "push.default" is set to "upstream" + Given the GitHub API server: + """ + post('/repos/lestephane/coral/pulls') { + assert :base => 'master', + :head => 'mislav:feature' + status 201 + json :html_url => "the://url" + } + """ + When I successfully run `hub pull-request -m hereyougo` + Then the output should contain exactly "the://url\n" + Scenario: Branch with quotation mark in name Given I am on the "feat'ure" branch with upstream "origin/feat'ure" Given the GitHub API server: @@ -1093,7 +1189,7 @@ When I successfully run `hub pull-request -m hereyougo` Then the output should contain exactly "the://url\n" - Scenario: Pull request with redirect + Scenario: Pull request with 307 redirect Given the "origin" remote has url "https://github.com/mislav/coral.git" And I am on the "feature" branch pushed to "origin/feature" Given the GitHub API server: @@ -1118,6 +1214,36 @@ When I successfully run `hub pull-request -m hereyougo` Then the output should contain exactly "the://url\n" + Scenario: Pull request with 301 redirect + Given the "origin" remote has url "https://github.com/mislav/coral.git" + And I am on the "feature" branch pushed to "origin/feature" + Given the GitHub API server: + """ + get('/repos/mislav/coral') { + redirect 'https://api.github.com/repositories/12345', 301 + } + get('/repositories/12345') { + json :name => 'coralify', :owner => { :login => 'coral-org' } + } + post('/repos/mislav/coral/pulls') { + redirect 'https://api.github.com/repositories/12345/pulls', 301 + } + post('/repositories/12345/pulls', :host_name => 'api.github.com') { + assert :base => 'master', + :head => 'coral-org:feature', + :title => 'hereyougo' + status 201 + json :html_url => "the://url" + } + """ + When I run `hub pull-request -m hereyougo` + Then the exit status should be 1 + And stderr should contain exactly: + """ + Error creating pull request: Post https://api.github.com/repositories/12345/pulls: refusing to follow HTTP 301 redirect for a POST request + Have your site admin use HTTP 308 for this kind of redirect + """ + Scenario: Default message with --push Given the git commit editor is "true" Given the GitHub API server: @@ -1148,8 +1274,29 @@ And the file ".git/PULLREQ_EDITMSG" should not exist And "git push --set-upstream origin HEAD:topic" should not be run + Scenario: Triangular workflow with --push + Given the "upstream" remote has url "git://github.com/github/coral.git" + And I am on the "master" branch pushed to "upstream/master" + # TODO: head should be "mislav:topic" + Given the GitHub API server: + """ + post('/repos/github/coral/pulls') { + assert :base => 'master', + :head => 'github:topic', + :title => 'hereyougo' + status 201 + json :html_url => "the://url" + } + """ + When I successfully run `git checkout --quiet -b topic` + Given I make a commit with message "Fork commit" + When I successfully run `hub pull-request -p -m hereyougo` + Then the output should contain exactly "the://url\n" + # TODO: the push should be to the "origin" remote instead + And "git push --set-upstream upstream HEAD:topic" should be run + Scenario: Automatically retry when --push resulted in 422 - Given The default aruba timeout is 7 seconds + Given the default aruba exit timeout is 7 seconds And the text editor adds: """ hello! @@ -1184,7 +1331,7 @@ And the file ".git/PULLREQ_EDITMSG" should not exist Scenario: Eventually give up on retries for --push - Given The default aruba timeout is 7 seconds + Given the default aruba exit timeout is 7 seconds And the text editor adds: """ hello! @@ -1211,3 +1358,30 @@ """ And the output should match /Given up after retrying for 5\.\d seconds\./ And a file named ".git/PULLREQ_EDITMSG" should exist + + Scenario: Draft pull request + Given I am on the "topic" branch pushed to "origin/topic" + Given the GitHub API server: + """ + post('/repos/mislav/coral/pulls') { + halt 400 unless request.env['HTTP_ACCEPT'] == 'application/vnd.github.shadow-cat-preview+json;charset=utf-8' + assert :draft => true + status 201 + json :html_url => "the://url" + } + """ + When I successfully run `hub pull-request -d -m wip` + Then the output should contain exactly "the://url\n" + + Scenario: Disallow edits from maintainers + Given I am on the "topic" branch pushed to "origin/topic" + Given the GitHub API server: + """ + post('/repos/mislav/coral/pulls') { + assert :maintainer_can_modify => false + status 201 + json :html_url => "the://url" + } + """ + When I successfully run `hub pull-request -m hello --no-maintainer-edits` + Then the output should contain exactly "the://url\n" diff -Nru hub-2.7.0~ds1/features/README.md hub-2.14.2~ds1/features/README.md --- hub-2.7.0~ds1/features/README.md 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/features/README.md 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,49 @@ +# Cucumber features for hub + +How to run all features: + +```sh +make bin/cucumber +bin/cucumber +``` + +Because this can take a couple of minutes, you may want to only run select files +related to the functionality that you're developing: + +```sh +bin/cucumber feature/api.feature +``` + +The Cucumber test suite requires a Ruby development environment. If you want to +avoid setting that up, you can run tests inside a Docker container: + +```sh +script/docker feature/api.feature +``` + +## How it works + +Each scenario is actually making real invocations to `hub` on the command-line +in the context of a real (dynamically created) git repository. + +Whenever a scenario requires talking to the GitHub API, a fake HTTP server is +spun locally to replace the real GitHub API. This is done so that the test suite +runs faster and is available offline as well. The fake API server is defined +as a Sinatra app inline in each scenario: + +``` +Given the GitHub API server: + """ + post('/repos/github/hub/pulls') { + status 200 + } + """ +``` + +## How to write new tests + +The best way to learn to write new tests is to study the existing scenarios for +commands that are similar to those that you want to add or change. + +Since Cucumber tests are written in a natural language, you mostly don't need to +know Ruby to write new tests. diff -Nru hub-2.7.0~ds1/features/release.feature hub-2.14.2~ds1/features/release.feature --- hub-2.7.0~ds1/features/release.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/release.feature 2020-03-05 17:48:23.000000000 +0000 @@ -359,7 +359,7 @@ ### Hello to my release Here is what's broken: - - everything\n\n + - everything\n """ Scenario: Show release no tag @@ -387,6 +387,32 @@ https://github.com/mislav/will_paginate/releases/v1.2.0\n """ + Scenario: Create a release from file + Given the GitHub API server: + """ + post('/repos/mislav/will_paginate/releases') { + assert :name => "Epic New Version", + :body => "body\ngoes\n\nhere" + + status 201 + json :html_url => "https://github.com/mislav/will_paginate/releases/v1.2.0" + } + """ + And a file named "message.txt" with: + """ + Epic New Version + + body + goes + + here + """ + When I successfully run `hub release create -F message.txt v1.2.0` + Then the output should contain exactly: + """ + https://github.com/mislav/will_paginate/releases/v1.2.0\n + """ + Scenario: Create a release with target commitish Given the GitHub API server: """ @@ -410,9 +436,10 @@ post('/repos/mislav/will_paginate/releases') { status 201 json :html_url => "https://github.com/mislav/will_paginate/releases/v1.2.0", - :upload_url => "https://api.github.com/uploads/assets{?name,label}" + :upload_url => "https://uploads.github.com/uploads/assets{?name,label}" } - post('/uploads/assets') { + post('/uploads/assets', :host_name => 'uploads.github.com') { + halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token OTOKEN' assert :name => 'hello-1.2.0.tar.gz', :label => 'Hello World' status 201 @@ -426,7 +453,79 @@ Then the output should contain exactly: """ https://github.com/mislav/will_paginate/releases/v1.2.0 - Attaching release asset `./hello-1.2.0.tar.gz'...\n + Attaching 1 asset...\n + """ + + Scenario: Retry attaching assets on 5xx errors + Given the GitHub API server: + """ + attempt = 0 + post('/repos/mislav/will_paginate/releases') { + status 201 + json :html_url => "https://github.com/mislav/will_paginate/releases/v1.2.0", + :upload_url => "https://uploads.github.com/uploads/assets{?name,label}" + } + post('/uploads/assets', :host_name => 'uploads.github.com') { + attempt += 1 + halt 400 unless request.body.read.to_s == "TARBALL" + halt 502 if attempt == 1 + status 201 + } + """ + And a file named "hello-1.2.0.tar.gz" with: + """ + TARBALL + """ + When I successfully run `hub release create -m "hello" v1.2.0 -a hello-1.2.0.tar.gz` + Then the output should contain exactly: + """ + https://github.com/mislav/will_paginate/releases/v1.2.0 + Attaching 1 asset...\n + """ + + Scenario: Create a release with some assets failing + Given the GitHub API server: + """ + post('/repos/mislav/will_paginate/releases') { + status 201 + json :tag_name => "v1.2.0", + :html_url => "https://github.com/mislav/will_paginate/releases/v1.2.0", + :upload_url => "https://uploads.github.com/uploads/assets{?name,label}" + } + post('/uploads/assets', :host_name => 'uploads.github.com') { + halt 422 if params[:name] == "two" + status 201 + } + """ + And a file named "one" with: + """ + ONE + """ + And a file named "two" with: + """ + TWO + """ + And a file named "three" with: + """ + THREE + """ + When I run `hub release create -m "m" v1.2.0 -a one -a two -a three` + Then the exit status should be 1 + Then the stderr should contain exactly: + """ + Attaching 3 assets... + The release was created, but attaching 2 assets failed. You can retry with: + hub release edit v1.2.0 -m '' -a two -a three + + Error uploading release asset: Unprocessable Entity (HTTP 422)\n + """ + + Scenario: Create a release with nonexistent asset + When I run `hub release create -m "hello" v1.2.0 -a "idontexis.tgz"` + Then the exit status should be 1 + Then the stderr should contain exactly: + """ + open idontexis.tgz: no such file or directory\n """ Scenario: Open new release in web browser @@ -478,7 +577,7 @@ KITTENS EVERYWHERE """ When I successfully run `hub release edit --draft=false v1.2.0` - Then there should be no output + Then the output should not contain anything Scenario: Edit existing release when there is a fork Given the "doge" remote has url "git://github.com/doge/will_paginate.git" @@ -497,7 +596,7 @@ } """ When I successfully run `hub release edit -m "" v1.2.0` - Then there should be no output + Then the output should not contain anything Scenario: Edit existing release no title Given the GitHub API server: @@ -527,7 +626,7 @@ get('/repos/mislav/will_paginate/releases') { json [ { url: 'https://api.github.com/repos/mislav/will_paginate/releases/123', - upload_url: 'https://api.github.com/uploads/assets{?name,label}', + upload_url: 'https://uploads.github.com/uploads/assets{?name,label}', tag_name: 'v1.2.0', name: 'will_paginate 1.2.0', draft: true, @@ -544,8 +643,9 @@ deleted = true status 204 } - post('/uploads/assets') { + post('/uploads/assets', :host_name => 'uploads.github.com') { halt 422 unless deleted + halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token OTOKEN' assert :name => 'hello-1.2.0.tar.gz', :label => nil status 201 @@ -558,7 +658,7 @@ When I successfully run `hub release edit -m "" v1.2.0 -a hello-1.2.0.tar.gz` Then the output should contain exactly: """ - Attaching release asset `hello-1.2.0.tar.gz'...\n + Attaching 1 asset...\n """ Scenario: Edit release no tag @@ -566,48 +666,168 @@ Then the exit status should be 1 Then the stderr should contain "hub release edit" - Scenario: Download a release asset. - Given the GitHub API server: + Scenario: Download a release asset + Given the GitHub API server: + """ + get('/repos/mislav/will_paginate/releases') { + json [ + { tag_name: 'v1.2.0', + assets: [ + { url: 'https://api.github.com/repos/mislav/will_paginate/assets/9876', + name: 'hello-1.2.0.tar.gz', + }, + ], + }, + ] + } + get('/repos/mislav/will_paginate/assets/9876') { + halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token OTOKEN' + halt 415 unless request.accept?('application/octet-stream') + status 302 + headers['Location'] = 'https://github-cloud.s3.amazonaws.com/releases/12204602/22ea221a-cf2f-11e2-222a-b3a3c3b3aa3a.gz' + "" + } + get('/releases/12204602/22ea221a-cf2f-11e2-222a-b3a3c3b3aa3a.gz', :host_name => 'github-cloud.s3.amazonaws.com') { + halt 400 unless request.env['HTTP_AUTHORIZATION'].nil? + halt 415 unless request.accept?('application/octet-stream') + headers['Content-Type'] = 'application/octet-stream' + "ASSET_TARBALL" + } + """ + When I successfully run `hub release download v1.2.0` + Then the output should contain exactly: + """ + Downloading hello-1.2.0.tar.gz ...\n + """ + And the file "hello-1.2.0.tar.gz" should contain exactly: + """ + ASSET_TARBALL + """ + + Scenario: Download release assets that match pattern + Given the GitHub API server: + """ + get('/repos/mislav/will_paginate/releases') { + json [ + { tag_name: 'v1.2.0', + assets: [ + { url: 'https://api.github.com/repos/mislav/will_paginate/assets/9876', + name: 'hello-amd32-1.2.0.tar.gz', + }, + { url: 'https://api.github.com/repos/mislav/will_paginate/assets/9877', + name: 'hello-amd64-1.2.0.tar.gz', + }, + { url: 'https://api.github.com/repos/mislav/will_paginate/assets/9878', + name: 'hello-x86-1.2.0.tar.gz', + }, + ], + }, + ] + } + get('/repos/mislav/will_paginate/assets/9876') { "TARBALL" } + get('/repos/mislav/will_paginate/assets/9877') { "TARBALL" } + """ + When I successfully run `hub release download v1.2.0 --include '*amd*'` + Then the output should contain exactly: + """ + Downloading hello-amd32-1.2.0.tar.gz ... + Downloading hello-amd64-1.2.0.tar.gz ...\n + """ + And the file "hello-x86-1.2.0.tar.gz" should not exist + + Scenario: Glob pattern allows exact match + Given the GitHub API server: + """ + get('/repos/mislav/will_paginate/releases') { + json [ + { tag_name: 'v1.2.0', + assets: [ + { url: 'https://api.github.com/repos/mislav/will_paginate/assets/9876', + name: 'hello-amd32-1.2.0.tar.gz', + }, + { url: 'https://api.github.com/repos/mislav/will_paginate/assets/9877', + name: 'hello-amd64-1.2.0.tar.gz', + }, + { url: 'https://api.github.com/repos/mislav/will_paginate/assets/9878', + name: 'hello-x86-1.2.0.tar.gz', + }, + ], + }, + ] + } + get('/repos/mislav/will_paginate/assets/9876') { "ASSET_TARBALL" } + """ + When I successfully run `hub release download v1.2.0 --include hello-amd32-1.2.0.tar.gz` + Then the output should contain exactly: + """ + Downloading hello-amd32-1.2.0.tar.gz ...\n + """ + And the file "hello-amd32-1.2.0.tar.gz" should contain exactly: + """ + ASSET_TARBALL + """ + And the file "hello-amd64-1.2.0.tar.gz" should not exist + And the file "hello-x86-1.2.0.tar.gz" should not exist + + Scenario: Advanced glob pattern + Given the GitHub API server: + """ + get('/repos/mislav/will_paginate/releases') { + json [ + { tag_name: 'v1.2.0', + assets: [ + { url: 'https://api.github.com/repos/mislav/will_paginate/assets/9876', + name: 'hello-amd32-1.2.0.tar.gz', + }, + { url: 'https://api.github.com/repos/mislav/will_paginate/assets/9876', + name: 'hello-amd32-1.2.1.tar.gz', + }, + { url: 'https://api.github.com/repos/mislav/will_paginate/assets/9876', + name: 'hello-amd32-1.2.2.tar.gz', + }, + ], + }, + ] + } + get('/repos/mislav/will_paginate/assets/9876') { "ASSET_TARBALL" } + """ + When I successfully run `hub release download v1.2.0 --include '*-amd32-?.?.[01].tar.gz'` + Then the output should contain exactly: + """ + Downloading hello-amd32-1.2.0.tar.gz ... + Downloading hello-amd32-1.2.1.tar.gz ...\n + """ + + Scenario: No matches for download pattern + Given the GitHub API server: + """ + get('/repos/mislav/will_paginate/releases') { + json [ + { tag_name: 'v1.2.0', + assets: [ + { url: 'https://api.github.com/repos/mislav/will_paginate/assets/9876', + name: 'hello-amd32-1.2.0.tar.gz', + }, + { url: 'https://api.github.com/repos/mislav/will_paginate/assets/9876', + name: 'hello-amd32-1.2.1.tar.gz', + }, + { url: 'https://api.github.com/repos/mislav/will_paginate/assets/9876', + name: 'hello-amd32-1.2.2.tar.gz', + }, + ], + }, + ] + } + """ + When I run `hub release download v1.2.0 --include amd32` + Then the exit status should be 1 + Then the stderr should contain exactly: + """ + the `--include` pattern did not match any available assets: + hello-amd32-1.2.0.tar.gz + hello-amd32-1.2.1.tar.gz + hello-amd32-1.2.2.tar.gz\n """ - get('/repos/mislav/will_paginate/releases') { - json [ - { url: 'https://api.github.com/repos/mislav/will_paginate/releases/123', - upload_url: 'https://api.github.com/uploads/assets{?name,label}', - tag_name: 'v1.2.0', - name: 'will_paginate 1.2.0', - draft: true, - prerelease: false, - assets: [ - { url: 'https://api.github.com/repos/mislav/will_paginate/assets/9876', - name: 'hello-1.2.0.tar.gz', - }, - ], - }, - ] - } - get('/repos/mislav/will_paginate/assets/9876') { - halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token OTOKEN' - halt 415 unless request.accept?('application/octet-stream') - status 302 - headers['Location'] = 'https://github-cloud.s3.amazonaws.com/releases/12204602/22ea221a-cf2f-11e2-222a-b3a3c3b3aa3a.gz' - "" - } - get('/releases/12204602/22ea221a-cf2f-11e2-222a-b3a3c3b3aa3a.gz', :host_name => 'github-cloud.s3.amazonaws.com') { - halt 400 unless request.env['HTTP_AUTHORIZATION'].nil? - halt 415 unless request.accept?('application/octet-stream') - headers['Content-Type'] = 'application/octet-stream' - "ASSET_TARBALL" - } - """ - When I successfully run `hub release download v1.2.0` - Then the output should contain exactly: - """ - Downloading hello-1.2.0.tar.gz ...\n - """ - And the file "hello-1.2.0.tar.gz" should contain exactly: - """ - ASSET_TARBALL - """ Scenario: Download release no tag When I run `hub release download` @@ -630,7 +850,7 @@ } """ When I successfully run `hub release delete v1.2.0` - Then there should be no output + Then the output should not contain anything Scenario: Release not found Given the GitHub API server: diff -Nru hub-2.7.0~ds1/features/remote_add.feature hub-2.14.2~ds1/features/remote_add.feature --- hub-2.7.0~ds1/features/remote_add.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/remote_add.feature 2020-03-05 17:48:23.000000000 +0000 @@ -7,7 +7,7 @@ Given there are no remotes When I successfully run `hub remote add origin` Then the url for "origin" should be "git@github.com:EvilChelu/dotfiles.git" - And there should be no output + And the output should not contain anything Scenario: Add origin remote for my own repo using -C Given there are no remotes @@ -15,32 +15,32 @@ When I successfully run `hub -C dotfiles remote add origin` And I cd to "dotfiles" Then the url for "origin" should be "git@github.com:EvilChelu/dotfiles.git" - And there should be no output + And the output should not contain anything Scenario: Unchanged public remote add When I successfully run `hub remote add origin http://github.com/defunkt/resque.git` Then the url for "origin" should be "http://github.com/defunkt/resque.git" - And there should be no output + And the output should not contain anything Scenario: Unchanged private remote add When I successfully run `hub remote add origin git@github.com:defunkt/resque.git` Then the url for "origin" should be "git@github.com:defunkt/resque.git" - And there should be no output + And the output should not contain anything Scenario: Unchanged local path remote add When I successfully run `hub remote add myremote ./path` Then the git command should be unchanged - And there should be no output + And the output should not contain anything Scenario: Unchanged local absolute path remote add When I successfully run `hub remote add myremote /path` Then the git command should be unchanged - And there should be no output + And the output should not contain anything Scenario: Unchanged remote add with host alias When I successfully run `hub remote add myremote server:/git/repo.git` Then the git command should be unchanged - And there should be no output + And the output should not contain anything Scenario: Add new remote for Enterprise repo Given "git.my.org" is a whitelisted Enterprise host @@ -48,7 +48,7 @@ And the "origin" remote has url "git@git.my.org:mislav/topsekrit.git" When I successfully run `hub remote add another` Then the url for "another" should be "git@git.my.org:another/topsekrit.git" - And there should be no output + And the output should not contain anything Scenario: Add public remote Given the GitHub API server: @@ -61,7 +61,7 @@ """ When I successfully run `hub remote add mislav` Then the url for "mislav" should be "git://github.com/mislav/dotfiles.git" - And there should be no output + And the output should not contain anything Scenario: Add detected private remote Given the GitHub API server: @@ -74,7 +74,7 @@ """ When I successfully run `hub remote add mislav` Then the url for "mislav" should be "git@github.com:mislav/dotfiles.git" - And there should be no output + And the output should not contain anything Scenario: Add remote with push access Given the GitHub API server: @@ -87,7 +87,7 @@ """ When I successfully run `hub remote add mislav` Then the url for "mislav" should be "git@github.com:mislav/dotfiles.git" - And there should be no output + And the output should not contain anything Scenario: Add remote for missing repo Given the GitHub API server: @@ -106,12 +106,12 @@ Scenario: Add explicitly private remote When I successfully run `hub remote add -p mislav` Then the url for "mislav" should be "git@github.com:mislav/dotfiles.git" - And there should be no output + And the output should not contain anything Scenario: Remote for my own repo is automatically private When I successfully run `hub remote add evilchelu` Then the url for "evilchelu" should be "git@github.com:EvilChelu/dotfiles.git" - And there should be no output + And the output should not contain anything Scenario: Add remote with arguments Given the GitHub API server: @@ -124,7 +124,7 @@ """ When I successfully run `hub remote add -f mislav` Then "git remote add -f mislav git://github.com/mislav/dotfiles.git" should be run - And there should be no output + And the output should not contain anything Scenario: Add remote with branch argument Given the GitHub API server: @@ -137,7 +137,7 @@ """ When I successfully run `hub remote add -f -t feature mislav` Then "git remote add -f -t feature mislav git://github.com/mislav/dotfiles.git" should be run - And there should be no output + And the output should not contain anything Scenario: Add HTTPS protocol remote Given the GitHub API server: @@ -151,7 +151,7 @@ Given HTTPS is preferred When I successfully run `hub remote add mislav` Then the url for "mislav" should be "https://github.com/mislav/dotfiles.git" - And there should be no output + And the output should not contain anything Scenario: Add named public remote Given the GitHub API server: @@ -164,7 +164,7 @@ """ When I successfully run `hub remote add mm mislav` Then the url for "mm" should be "git://github.com/mislav/dotfiles.git" - And there should be no output + And the output should not contain anything Scenario: set-url Given the GitHub API server: @@ -178,7 +178,7 @@ Given the "origin" remote has url "git://github.com/evilchelu/dotfiles.git" When I successfully run `hub remote set-url origin mislav` Then the url for "origin" should be "git://github.com/mislav/dotfiles.git" - And there should be no output + And the output should not contain anything Scenario: Add public remote including repo name Given the GitHub API server: @@ -191,7 +191,7 @@ """ When I successfully run `hub remote add mislav/dotfilez.js` Then the url for "mislav" should be "git://github.com/mislav/dotfilez.js.git" - And there should be no output + And the output should not contain anything Scenario: Add named public remote including repo name Given the GitHub API server: @@ -204,24 +204,28 @@ """ When I successfully run `hub remote add mm mislav/dotfilez.js` Then the url for "mm" should be "git://github.com/mislav/dotfilez.js.git" - And there should be no output + And the output should not contain anything Scenario: Add named private remote When I successfully run `hub remote add -p mm mislav` Then the url for "mm" should be "git@github.com:mislav/dotfiles.git" - And there should be no output + And the output should not contain anything Scenario: Add private remote including repo name When I successfully run `hub remote add -p mislav/dotfilez.js` Then the url for "mislav" should be "git@github.com:mislav/dotfilez.js.git" - And there should be no output + And the output should not contain anything Scenario: Add named private remote including repo name When I successfully run `hub remote add -p mm mislav/dotfilez.js` Then the url for "mm" should be "git@github.com:mislav/dotfilez.js.git" - And there should be no output + And the output should not contain anything Scenario: Add named private remote for my own repo including repo name When I successfully run `hub remote add ec evilchelu/dotfilez.js` Then the url for "ec" should be "git@github.com:EvilChelu/dotfilez.js.git" - And there should be no output + And the output should not contain anything + + Scenario: Avoid crash in argument parsing + When I successfully run `hub --noop remote add a b evilchelu` + Then the output should contain exactly "git remote add a b evilchelu\n" diff -Nru hub-2.7.0~ds1/features/steps.rb hub-2.14.2~ds1/features/steps.rb --- hub-2.7.0~ds1/features/steps.rb 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/steps.rb 2020-03-05 17:48:23.000000000 +0000 @@ -1,31 +1,28 @@ require 'fileutils' Given(/^HTTPS is preferred$/) do - run_silent %(git config --global hub.protocol https) + run_command_and_stop %(git config --global hub.protocol https) end Given(/^there are no remotes$/) do - result = run_silent('git remote') - expect(result).to be_empty + run_command_and_stop 'git remote' + expect(last_command_started).not_to have_output end Given(/^"([^"]*)" is a whitelisted Enterprise host$/) do |host| - run_silent %(git config --global --add hub.host "#{host}") + run_command_and_stop %(git config --global --add hub.host "#{host}") end Given(/^git "(.+?)" is set to "(.+?)"$/) do |key, value| - run_silent %(git config #{key} "#{value}") + run_command_and_stop %(git config #{key} "#{value}") end Given(/^the "([^"]*)" remote has(?: (push))? url "([^"]*)"$/) do |remote_name, push, url| - remotes = run_silent('git remote').split("\n") - if push - push = "--push" - end - unless remotes.include? remote_name - run_silent %(git remote add #{remote_name} "#{url}") + run_command_and_stop 'git remote' + unless last_command_started.stdout.split("\n").include? remote_name + run_command_and_stop %(git remote add #{remote_name} "#{url}") else - run_silent %(git remote set-url #{push} #{remote_name} "#{url}") + run_command_and_stop %(git remote set-url #{"--push" if push} #{remote_name} "#{url}") end end @@ -40,7 +37,8 @@ end Given(/^\$(\w+) is "([^"]*)"$/) do |name, value| - set_env name, value.gsub(/\$([A-Z_]+)/) { ENV.fetch($1) } + expanded_value = value.gsub(/\$([A-Z_]+)/) { aruba.environment[$1] } + set_environment_variable(name, expanded_value) end Given(/^I am in "([^"]*)" git repo$/) do |dir_name| @@ -54,23 +52,18 @@ end Given(/^a (bare )?git repo in "([^"]*)"$/) do |bare, dir_name| - step %(a directory named "#{dir_name}") - dirs << dir_name - step %(I successfully run `git init --quiet #{"--bare" if bare}`) - dirs.pop + run_command_and_stop %(git init --quiet #{"--bare" if bare} '#{dir_name}') end Given(/^a git bundle named "([^"]*)"$/) do |file| - in_current_dir do - FileUtils.mkdir_p File.dirname(file) - dest = File.expand_path(file) - - Dir.mktmpdir do |tmpdir| - dirs << tmpdir - run_silent %(git init --quiet) - empty_commit - run_silent %(git bundle create "#{dest}" master) - dirs.pop + dest = expand_path(file) + FileUtils.mkdir_p(File.dirname(dest)) + + Dir.mktmpdir do |tmpdir| + Dir.chdir(tmpdir) do + `git init --quiet` + `GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=b git commit --quiet -m 'empty' --allow-empty --author='a '` + `git bundle create "#{dest}" master 2>&1` end end end @@ -78,19 +71,19 @@ Given(/^there is a commit named "([^"]+)"$/) do |name| empty_commit empty_commit - run_silent %(git tag #{name}) - run_silent %(git reset --quiet --hard HEAD^) + run_command_and_stop %(git tag #{name}) + run_command_and_stop %(git reset --quiet --hard HEAD^) end Given(/^there is a git FETCH_HEAD$/) do empty_commit empty_commit - in_current_dir do + cd('.') do File.open(".git/FETCH_HEAD", "w") do |fetch_head| fetch_head.puts "%s\t\t'refs/heads/made-up' of git://github.com/made/up.git" % `git rev-parse HEAD`.chomp end end - run_silent %(git reset --quiet --hard HEAD^) + run_command_and_stop %(git reset --quiet --hard HEAD^) end When(/^I make (a|\d+) commits?(?: with message "([^"]+)")?$/) do |num, msg| @@ -107,32 +100,35 @@ step %(the output should contain exactly "#{subject}\\n") end +# expand `<$HOME>` etc. in matched text +Then(/^(the (?:output|stderr|stdout)) with expanded variables( should contain(?: exactly)?:)/) do |prefix, postfix, text| + step %(#{prefix}#{postfix}), text.gsub(/<\$(\w+)>/) { aruba.environment[$1] } +end + Given(/^the "([^"]+)" branch is pushed to "([^"]+)"$/) do |name, upstream| full_upstream = ".git/refs/remotes/#{upstream}" - in_current_dir do + cd('.') do FileUtils.mkdir_p File.dirname(full_upstream) FileUtils.cp ".git/refs/heads/#{name}", full_upstream end end Given(/^I am on the "([^"]+)" branch(?: (pushed to|with upstream) "([^"]+)")?$/) do |name, type, upstream| - run_silent %(git checkout --quiet -b #{shell_escape name}) + run_command_and_stop %(git checkout --quiet -b #{shell_escape name}) empty_commit if upstream - unless upstream == 'refs/heads/master' - full_upstream = upstream.start_with?('refs/') ? upstream : "refs/remotes/#{upstream}" - run_silent %(git update-ref #{shell_escape full_upstream} HEAD) - end + full_upstream = upstream.start_with?('refs/') ? upstream : "refs/remotes/#{upstream}" + run_command_and_stop %(git update-ref #{shell_escape full_upstream} HEAD) if type == 'with upstream' - run_silent %(git branch --set-upstream-to #{shell_escape upstream}) + run_command_and_stop %(git branch --set-upstream-to #{shell_escape upstream}) end end end Given(/^the default branch for "([^"]+)" is "([^"]+)"$/) do |remote, branch| - in_current_dir do + cd('.') do ref_file = ".git/refs/remotes/#{remote}/#{branch}" unless File.exist? ref_file empty_commit unless File.exist? '.git/refs/heads/master' @@ -140,26 +136,17 @@ FileUtils.cp '.git/refs/heads/master', ref_file end end - run_silent %(git remote set-head #{remote} #{branch}) + run_command_and_stop %(git remote set-head #{remote} #{branch}) end Given(/^I am in detached HEAD$/) do empty_commit empty_commit - run_silent %(git checkout HEAD^) + run_command_and_stop %(git checkout HEAD^) end Given(/^the current dir is not a repo$/) do - in_current_dir do - FileUtils.rm_rf '.git' - end -end - -When(/^I move the file named "([^"]+)" to "([^"]+)"?$/) do |source, dest| - in_current_dir do - FileUtils.mkdir_p(File.dirname(dest)) - FileUtils.mv(source, dest) - end + FileUtils.rm_rf(expand_path('.git')) end Given(/^the GitHub API server:$/) do |endpoints_str| @@ -167,17 +154,11 @@ eval endpoints_str, binding end # hit our Sinatra server instead of github.com - set_env 'HUB_TEST_HOST', "http://127.0.0.1:#{@server.port}" -end - -Given(/^I use a debugging proxy(?: at "(.+?)")?$/) do |address| - address ||= 'localhost:8888' - set_env 'HTTP_PROXY', address - set_env 'HTTPS_PROXY', address + set_environment_variable 'HUB_TEST_HOST', "http://127.0.0.1:#{@server.port}" end Then(/^shell$/) do - in_current_dir do + cd('.') do system '/bin/bash -i' end end @@ -198,57 +179,45 @@ history.each { |h| expect(h).to_not include(pattern) } end -Then(/^there should be no output$/) do - assert_exact_output('', all_output) -end - Then(/^the git command should be unchanged$/) do expect(@commands).to_not be_empty assert_command_run @commands.last.sub(/^hub\b/, 'git') end Then(/^the url for "([^"]*)" should be "([^"]*)"$/) do |name, url| - found = run_silent %(git config --get-all remote.#{name}.url) - expect(found).to eql(url) + run_command_and_stop %(git config --get-all remote.#{name}.url) + expect(last_command_started).to have_output(url) end Then(/^the "([^"]*)" submodule url should be "([^"]*)"$/) do |name, url| - found = run_silent %(git config --get-all submodule."#{name}".url) - expect(found).to eql(url) + run_command_and_stop %(git config --get-all submodule."#{name}".url) + expect(last_command_started).to have_output(url) end Then(/^"([^"]*)" should merge "([^"]*)" from remote "([^"]*)"$/) do |name, merge, remote| - actual_remote = run_silent %(git config --get-all branch.#{name}.remote) - expect(remote).to eql(actual_remote) + run_command_and_stop %(git config --get-all branch.#{name}.remote) + expect(last_command_started).to have_output(remote) - actual_merge = run_silent %(git config --get-all branch.#{name}.merge) - expect(merge).to eql(actual_merge) + run_command_and_stop %(git config --get-all branch.#{name}.merge) + expect(last_command_started).to have_output(merge) end Then(/^there should be no "([^"]*)" remote$/) do |remote_name| - remotes = run_silent('git remote').split("\n") - expect(remotes).to_not include(remote_name) + run_command_and_stop 'git remote' + expect(last_command_started.output.split("\n")).to_not include(remote_name) end Then(/^the file "([^"]*)" should have mode "([^"]*)"$/) do |file, expected_mode| - prep_for_fs_check do - mode = File.stat(file).mode - expect(mode.to_s(8)).to match(/#{expected_mode}$/) - end -end - -Given(/^the file named "(.+?)" is older than hub source$/) do |file| - prep_for_fs_check do - time = File.mtime(File.expand_path('../../lib/hub/commands.rb', __FILE__)) - 60 - File.utime(time, time, file) - end + mode = File.stat(expand_path(file)).mode + expect(mode.to_s(8)).to match(/#{expected_mode}$/) end Given(/^the remote commit states of "(.*?)" "(.*?)" are:$/) do |proj, ref, json_value| if ref == 'HEAD' empty_commit end - rev = run_silent %(git rev-parse #{ref}) + run_command_and_stop %(git rev-parse #{ref}) + rev = last_command_started.output.chomp host, owner, repo = proj.split('/', 3) if repo.nil? @@ -302,23 +271,47 @@ When(/^I pass in:$/) do |input| type(input) - @interactive.stdin.close + close_input end Given(/^the git commit editor is "([^"]+)"$/) do |cmd| - set_env('GIT_EDITOR', cmd) + set_environment_variable('GIT_EDITOR', cmd) end Given(/^the SSH config:$/) do |config_lines| - ssh_config = "#{ENV['HOME']}/.ssh/config" + ssh_config = expand_path('~/.ssh/config') FileUtils.mkdir_p(File.dirname(ssh_config)) File.open(ssh_config, 'w') {|f| f << config_lines } end Given(/^the SHAs and timestamps are normalized in "([^"]+)"$/) do |file| - in_current_dir do - contents = File.read(file) - contents.gsub!(/[0-9a-f]{7} \(Hub, \d seconds? ago\)/, "SHA1SHA (Hub, 0 seconds ago)") - File.open(file, "w") { |f| f.write(contents) } + file = expand_path(file) + contents = File.read(file) + contents.gsub!(/[0-9a-f]{7} \(Hub, \d seconds? ago\)/, "SHA1SHA (Hub, 0 seconds ago)") + File.open(file, "w") { |f| f.write(contents) } +end + +Then(/^its (output|stderr|stdout) should( not)? contain( exactly)?:$/) do |channel, negated, exactly, expected| + matcher = case channel.to_sym + when :output + :have_output + when :stderr + :have_output_on_stderr + when :stdout + :have_output_on_stdout + end + + commands = [last_command_started] + + output_string_matcher = if exactly + :an_output_string_being_eq + else + :an_output_string_including + end + + if negated + expect(commands).not_to include_an_object send(matcher, send(output_string_matcher, expected)) + else + expect(commands).to include_an_object send(matcher, send(output_string_matcher, expected)) end end diff -Nru hub-2.7.0~ds1/features/submodule_add.feature hub-2.14.2~ds1/features/submodule_add.feature --- hub-2.7.0~ds1/features/submodule_add.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/submodule_add.feature 2020-03-05 17:48:23.000000000 +0000 @@ -4,6 +4,9 @@ Given I am in "dotfiles" git repo # make existing repo in subdirectory so git clone isn't triggered Given a git repo in "vendor/grit" + And I cd to "vendor/grit" + And I make 1 commit + And I cd to "../.." Scenario: Add public submodule Given the GitHub API server: diff -Nru hub-2.7.0~ds1/features/support/aruba_command.rb hub-2.14.2~ds1/features/support/aruba_command.rb --- hub-2.7.0~ds1/features/support/aruba_command.rb 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/features/support/aruba_command.rb 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,81 @@ +require 'open3' +require 'shellwords' + +module Aruba + remove_const :Command + class Command + attr_reader :commandline, :stdout, :stderr + attr_reader :exit_timeout, :io_wait_timeout, :startup_wait_time, :environment, :stop_signal, :exit_status + + def initialize(cmd, mode:, exit_timeout:, io_wait_timeout:, + working_directory:, environment:, main_class:, stop_signal:, + startup_wait_time:, event_bus:) + @commandline = cmd + @working_directory = working_directory + @event_bus = event_bus + @exit_timeout = exit_timeout + @io_wait_timeout = io_wait_timeout + @startup_wait_time = startup_wait_time + @environment = environment + @stop_signal = stop_signal + + @stopped = false + @exit_status = nil + @stdout = nil + @stderr = nil + end + + def inspect + %(#) + end + + def output + stdout + stderr + end + + def start + @event_bus.notify Events::CommandStarted.new(self) + cmd = Shellwords.split @commandline + @stdin_io, @stdout_io, @stderr_io, @thread = Open3.popen3(@environment, *cmd, chdir: @working_directory) + end + + def write(input) + @stdin_io.write input + @stdin_io.flush + end + + def close_io(io) + case io + when :stdin then @stdin_io.close + else + raise ArgumentError, io.to_s + end + end + + def stop + return if @exit_status + @event_bus.notify Events::CommandStopped.new(self) + terminate + end + + def terminate + return if @exit_status + + close_io(:stdin) + @stdout = @stdout_io.read + @stderr = @stderr_io.read + + status = @thread.value + @exit_status = status.exitstatus + @thread = nil + end + + def interactive? + true + end + + def timed_out? + false + end + end +end diff -Nru hub-2.7.0~ds1/features/support/env.rb hub-2.14.2~ds1/features/support/env.rb --- hub-2.7.0~ds1/features/support/env.rb 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/support/env.rb 2020-03-05 17:48:23.000000000 +0000 @@ -5,101 +5,88 @@ system_git = `which git 2>/dev/null`.chomp bin_dir = File.expand_path('../fakebin', __FILE__) + +tmpdir = Dir.mktmpdir('hub_test') +tmp_bin_dir = "#{tmpdir}/bin" +Aruba.configure do |aruba| + aruba.send(:find_option, :root_directory).value = tmpdir +end + hub_dir = Dir.mktmpdir('hub_build') raise 'hub build failed' unless system("./script/build -o #{hub_dir}/hub") Before do - # speed up load time by skipping RubyGems - set_env 'RUBYOPT', '--disable-gems' if RUBY_VERSION > '1.9' - # put fakebin on the PATH - set_env 'PATH', "#{hub_dir}:#{bin_dir}:#{ENV['PATH']}" - # clear out GIT if it happens to be set - set_env 'GIT', nil - # exclude this project's git directory from use in testing - set_env 'GIT_CEILING_DIRECTORIES', File.expand_path('../../..', __FILE__) - # sabotage git commands that might try to access a remote host - set_env 'GIT_PROXY_COMMAND', 'echo' - # avoids reading from current user's "~/.gitconfig" - set_env 'HOME', File.expand_path(File.join(current_dir, 'home')) - # https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables - set_env 'XDG_CONFIG_HOME', nil - set_env 'XDG_CONFIG_DIRS', nil - # used in fakebin/git - set_env 'HUB_SYSTEM_GIT', system_git - # ensure that api.github.com is actually never hit in tests - set_env 'HUB_TEST_HOST', 'http://127.0.0.1:0' - # ensure we use fakebin `open` to test browsing - set_env 'BROWSER', 'open' - # sabotage opening a commit message editor interactively - set_env 'GIT_EDITOR', 'false' - # reset current localization settings - set_env 'LANG', nil - set_env 'LANGUAGE', nil - set_env 'LC_ALL', 'en_US.UTF-8' - # ignore current user's token - set_env 'GITHUB_TOKEN', nil - set_env 'GITHUB_USER', nil - set_env 'GITHUB_PASSWORD', nil - set_env 'GITHUB_HOST', nil - author_name = "Hub" author_email = "hub@test.local" - set_env 'GIT_AUTHOR_NAME', author_name - set_env 'GIT_COMMITTER_NAME', author_name - set_env 'GIT_AUTHOR_EMAIL', author_email - set_env 'GIT_COMMITTER_EMAIL', author_email - - set_env 'HUB_VERSION', 'dev' - set_env 'HUB_REPORT_CRASH', 'never' - set_env 'HUB_PROTOCOL', nil - FileUtils.mkdir_p ENV['HOME'] + aruba.environment.update( + # speed up load time by skipping RubyGems + 'RUBYOPT' => '--disable-gems', + # put fakebin on the PATH + 'PATH' => "#{hub_dir}:#{tmp_bin_dir}:#{bin_dir}:#{ENV['PATH']}", + # clear out GIT if it happens to be set + 'GIT' => nil, + # exclude this project's git directory from use in testing + 'GIT_CEILING_DIRECTORIES' => File.expand_path('../../..', __FILE__), + # sabotage git commands that might try to access a remote host + 'GIT_PROXY_COMMAND' => 'echo', + # avoids reading from current user's "~/.gitconfig" + 'HOME' => expand_path('home'), + 'TMPDIR' => tmpdir, + # https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables + 'XDG_CONFIG_HOME' => nil, + 'XDG_CONFIG_DIRS' => nil, + # used in fakebin/git + 'HUB_SYSTEM_GIT' => system_git, + # ensure that api.github.com is actually never hit in tests + 'HUB_TEST_HOST' => 'http://127.0.0.1:0', + # ensure we use fakebin `open` to test browsing + 'BROWSER' => 'open', + # sabotage opening a commit message editor interactively + 'GIT_EDITOR' => 'false', + # reset current localization settings + 'LANG' => nil, + 'LANGUAGE' => nil, + 'LC_ALL' => 'C.UTF-8', + # ignore current user's token + 'GITHUB_TOKEN' => nil, + 'GITHUB_USER' => nil, + 'GITHUB_PASSWORD' => nil, + 'GITHUB_HOST' => nil, + 'GITHUB_REPOSITORY' => nil, + + 'GIT_AUTHOR_NAME' => author_name, + 'GIT_COMMITTER_NAME' => author_name, + 'GIT_AUTHOR_EMAIL' => author_email, + 'GIT_COMMITTER_EMAIL' => author_email, + + 'HUB_VERSION' => 'dev', + 'HUB_REPORT_CRASH' => 'never', + 'HUB_PROTOCOL' => nil, + ) - # increase process exit timeout from the default of 3 seconds - @aruba_timeout_seconds = 10 + FileUtils.mkdir_p(expand_path('~')) end After do @server.stop if defined? @server and @server - FileUtils.rm_f("#{bin_dir}/vim") + FileUtils.rm_f("#{tmp_bin_dir}/vim") end -RSpec::Matchers.define :be_successful_command do - match do |cmd| - cmd.success? - end - - failure_message do |cmd| - %(command "#{cmd}" exited with status #{cmd.status}:) << - cmd.output.gsub(/^/, ' ' * 2) - end +After('@cache_clear') do + FileUtils.rm_rf("#{tmpdir}/hub/api") end -class SimpleCommand - attr_reader :output - extend Forwardable - - def_delegator :@status, :exitstatus, :status - def_delegators :@status, :success? - - def initialize cmd - @cmd = cmd - end - - def to_s - @cmd - end - - def self.run cmd - command = new(cmd) - command.run - command +RSpec::Matchers.define :be_successfully_executed do + match do |cmd| + expect(cmd).to have_exit_status(0) end - def run - @output = `#{@cmd} 2>&1`.chomp - @status = $? - $?.success? + failure_message do |cmd| + msg = %(command `#{cmd.commandline}` exited with status #{cmd.exit_status}) + stderr = cmd.stderr + msg << ":\n" << stderr.gsub(/^/, ' ') unless stderr.empty? + msg end end @@ -113,7 +100,7 @@ end def history - histfile = File.join(ENV['HOME'], '.history') + histfile = expand_path('~/.history') if File.exist? histfile File.readlines histfile else @@ -127,7 +114,7 @@ end def edit_hub_config - config = File.join(ENV['HOME'], '.config/hub') + config = expand_path('~/.config/hub') FileUtils.mkdir_p File.dirname(config) if File.exist? config data = YAML.load File.read(config) @@ -139,47 +126,23 @@ end define_method(:text_editor_script) do |bash_code| - File.open("#{bin_dir}/vim", 'w', 0755) { |exe| + FileUtils.mkdir_p(tmp_bin_dir) + File.open("#{tmp_bin_dir}/vim", 'w', 0755) { |exe| exe.puts "#!/bin/bash" exe.puts "set -e" exe.puts bash_code } end - def run_silent cmd - in_current_dir do - command = SimpleCommand.run(cmd) - expect(command).to be_successful_command - command.output - end - end - def empty_commit(message = nil) unless message @empty_commit_count = defined?(@empty_commit_count) ? @empty_commit_count + 1 : 1 message = "empty #{@empty_commit_count}" end - run_silent "git commit --quiet -m '#{message}' --allow-empty" - end - - # Aruba unnecessarily creates new Announcer instance on each invocation - def announcer - @announcer ||= super + run_command_and_stop "git commit --quiet -m '#{message}' --allow-empty" end def shell_escape(message) message.to_s.gsub(/['"\\ $]/) { |m| "\\#{m}" } end - - %w[output_from stdout_from stderr_from all_stdout all_stderr].each do |m| - define_method(m) do |*args| - home = ENV['HOME'].to_s - output = super(*args) - if home.empty? - output - else - output.gsub(home, '$HOME') - end - end - end } diff -Nru hub-2.7.0~ds1/features/support/fakebin/git hub-2.14.2~ds1/features/support/fakebin/git --- hub-2.7.0~ds1/features/support/fakebin/git 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/support/fakebin/git 2020-03-05 17:48:23.000000000 +0000 @@ -2,9 +2,18 @@ # A wrapper for system git that prevents commands such as `clone` or `fetch` to be # executed in testing. It logs commands to "~/.history" so afterwards it can be # asserted that they ran. +set -e command="$1" -[ "$command" = "config" ] || echo git "$@" >> "$HOME"/.history +case "$command" in + "config" ) ;; + "web--browse" ) + echo git web--browse PATH/$(basename "$2") >> "$HOME"/.history + ;; + * ) + echo git "$@" >> "$HOME"/.history + ;; +esac case "$command" in "--list-cmds="* ) @@ -12,9 +21,28 @@ echo branch echo commit ;; - "clone" | "fetch" | "pull" | "push" ) + "fetch" ) + [[ $2 != -* && $3 == *:* && $3 != -* ]] || exit 0 + refspec="$3" + dest="${refspec#*:}" + head="$(git rev-parse --verify -q HEAD || true)" + if [ -z "$head" ]; then + git commit --allow-empty -m "auto-commit" + head="$(git rev-parse --verify -q HEAD)" + fi + if [[ $dest == refs/remotes/* ]]; then + mkdir -p ".git/${dest%/*}" + cat >".git/${dest}" <<<"$head" + cat >".git/FETCH_HEAD" <<<"$head" + else + "$HUB_SYSTEM_GIT" checkout -b "${dest#refs/heads/}" HEAD + cat >".git/FETCH_HEAD" <<<"$head" + fi + exit 0 + ;; + "clone" | "pull" | "push" | "web--browse" ) # don't actually execute these commands - exit + exit 0 ;; * ) # note: `submodule add` also initiates a clone, but we work around it diff -Nru hub-2.7.0~ds1/features/support/local_server.rb hub-2.14.2~ds1/features/support/local_server.rb --- hub-2.7.0~ds1/features/support/local_server.rb 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/support/local_server.rb 2020-03-05 17:48:23.000000000 +0000 @@ -67,22 +67,21 @@ JSON.generate value end - def assert(expected) + def assert(expected, data = params) expected.each do |key, value| if :no == value halt 422, json( - :message => "expected %s not to be passed; got %s" % [ - key.inspect, - params[key].inspect - ] - ) if params.key?(key.to_s) - elsif params[key] != value + :message => "did not expect any value for %p; got %p" % [key, data[key]] + ) if data.key?(key.to_s) + elsif Regexp === value halt 422, json( - :message => "expected %s to be %s; got %s" % [ - key.inspect, - value.inspect, - params[key].inspect - ] + :message => "expected %p to match %p; got %p" % [key, value, data[key] ] + ) unless value =~ data[key] + elsif Hash === value + assert(value, data[key]) + elsif data[key] != value + halt 422, json( + :message => "expected %p to be %p; got %p" % [key, value, data[key]] ) end end @@ -142,16 +141,21 @@ @port = self.class.ports[app.object_id] if not @port or not responsive? - @server_thread = start_handler(Identify.new(app)) do |server, host, port| - self.server = server - @port = self.class.ports[app.object_id] = port - end + tries = 0 + begin + @server_thread = start_handler(Identify.new(app)) do |server, host, port| + self.server = server + @port = self.class.ports[app.object_id] = port + end - Timeout.timeout(60) { @server_thread.join(0.01) until responsive? } + Timeout.timeout(5) { @server_thread.join(0.01) until responsive? } + rescue Timeout::Error + tries += 1 + retry if tries < 3 + raise "Rack application timed out during boot after #{tries} tries" + end end - rescue TimeoutError - raise "Rack application timed out during boot" - else + self end diff -Nru hub-2.7.0~ds1/features/sync.feature hub-2.14.2~ds1/features/sync.feature --- hub-2.7.0~ds1/features/sync.feature 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/features/sync.feature 2020-03-05 17:48:23.000000000 +0000 @@ -32,7 +32,7 @@ When I successfully run `hub sync` Then the stderr should contain exactly: """ - warning: `feature' seems to contain unpushed commits\n + warning: 'feature' seems to contain unpushed commits\n """ Scenario: Deletes local branch that had its upstream deleted @@ -52,5 +52,5 @@ When I successfully run `hub sync` Then the stderr should contain exactly: """ - warning: `feature' was deleted on origin, but appears not merged into master\n + warning: 'feature' was deleted on origin, but appears not merged into 'master'\n """ diff -Nru hub-2.7.0~ds1/fixtures/test_configs.go hub-2.14.2~ds1/fixtures/test_configs.go --- hub-2.7.0~ds1/fixtures/test_configs.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/fixtures/test_configs.go 2020-03-05 17:48:23.000000000 +0000 @@ -49,7 +49,7 @@ content := `--- github.com: - user: jingweno - oauth_token: 123 + oauth_token: "123" protocol: http` ioutil.WriteFile(file.Name(), []byte(content), os.ModePerm) os.Setenv("HUB_CONFIG", file.Name()) @@ -63,7 +63,48 @@ content := `--- github.com: - user: jingweno - oauth_token: 123 + oauth_token: "123" + protocol: http + unix_socket: /tmp/go.sock` + ioutil.WriteFile(file.Name(), []byte(content), os.ModePerm) + os.Setenv("HUB_CONFIG", file.Name()) + + return &TestConfigs{file.Name()} +} + +func SetupTestConfigsInvalidHostName() *TestConfigs { + file, _ := ioutil.TempFile("", "test-gh-config-") + + content := `--- +123: +- user: jingweno + oauth_token: "123" + protocol: http + unix_socket: /tmp/go.sock` + ioutil.WriteFile(file.Name(), []byte(content), os.ModePerm) + os.Setenv("HUB_CONFIG", file.Name()) + + return &TestConfigs{file.Name()} +} + +func SetupTestConfigsInvalidHostEntry() *TestConfigs { + file, _ := ioutil.TempFile("", "test-gh-config-") + + content := `--- +github.com: hello` + ioutil.WriteFile(file.Name(), []byte(content), os.ModePerm) + os.Setenv("HUB_CONFIG", file.Name()) + + return &TestConfigs{file.Name()} +} + +func SetupTestConfigsInvalidPropertyValue() *TestConfigs { + file, _ := ioutil.TempFile("", "test-gh-config-") + + content := `--- +github.com: +- user: + oauth_token: "123" protocol: http unix_socket: /tmp/go.sock` ioutil.WriteFile(file.Name(), []byte(content), os.ModePerm) diff -Nru hub-2.7.0~ds1/Gemfile hub-2.14.2~ds1/Gemfile --- hub-2.7.0~ds1/Gemfile 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/Gemfile 2020-03-05 17:48:23.000000000 +0000 @@ -1,6 +1,5 @@ source 'https://rubygems.org' -gem 'aruba', '~> 0.5.3' -gem 'cucumber', '~> 1.3.9' +gem 'aruba', '~> 1.0.0.pre.alpha.4' +gem 'cucumber', '~> 3.1.2' gem 'sinatra' -gem 'ronn' diff -Nru hub-2.7.0~ds1/Gemfile.lock hub-2.14.2~ds1/Gemfile.lock --- hub-2.7.0~ds1/Gemfile.lock 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/Gemfile.lock 2020-03-05 17:48:23.000000000 +0000 @@ -1,60 +1,65 @@ GEM remote: https://rubygems.org/ specs: - aruba (0.5.4) - childprocess (>= 0.3.6) - cucumber (>= 1.1.1) - rspec-expectations (>= 2.7.0) + aruba (1.0.0.pre.alpha.4) + childprocess (~> 1.0) + contracts (~> 0.13) + cucumber (>= 2.4, < 4.0) + ffi (~> 1.9) + rspec-expectations (~> 3.4) + thor (~> 0.19) + backports (3.15.0) builder (3.2.3) - childprocess (0.9.0) - ffi (~> 1.0, >= 1.0.11) - cucumber (1.3.20) + childprocess (1.0.1) + rake (< 13.0) + contracts (0.16.0) + cucumber (3.1.2) builder (>= 2.1.2) - diff-lcs (>= 1.1.3) - gherkin (~> 2.12) + cucumber-core (~> 3.2.0) + cucumber-expressions (~> 6.0.1) + cucumber-wire (~> 0.0.1) + diff-lcs (~> 1.3) + gherkin (~> 5.1.0) multi_json (>= 1.7.5, < 2.0) multi_test (>= 0.1.2) + cucumber-core (3.2.1) + backports (>= 3.8.0) + cucumber-tag_expressions (~> 1.1.0) + gherkin (~> 5.0) + cucumber-expressions (6.0.1) + cucumber-tag_expressions (1.1.1) + cucumber-wire (0.0.1) diff-lcs (1.3) - ffi (1.9.25) - ffi (1.9.25-java) - gherkin (2.12.2) - multi_json (~> 1.3) - gherkin (2.12.2-java) - multi_json (~> 1.3) - hpricot (0.8.4) - hpricot (0.8.4-java) + ffi (1.11.1) + ffi (1.11.1-java) + gherkin (5.1.0) multi_json (1.13.1) multi_test (0.1.2) - mustache (0.99.4) - mustermann (1.0.2) - rack (2.0.5) - rack-protection (2.0.3) + mustermann (1.0.3) + rack (2.0.7) + rack-protection (2.0.5) rack - rdiscount (1.6.8) - ronn (0.7.3) - hpricot (>= 0.8.2) - mustache (>= 0.7.0) - rdiscount (>= 1.5.8) - rspec-expectations (3.7.0) + rake (12.3.3) + rspec-expectations (3.8.4) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.7.0) - rspec-support (3.7.1) - sinatra (2.0.3) + rspec-support (~> 3.8.0) + rspec-support (3.8.2) + sinatra (2.0.5) mustermann (~> 1.0) rack (~> 2.0) - rack-protection (= 2.0.3) + rack-protection (= 2.0.5) tilt (~> 2.0) - tilt (2.0.8) + thor (0.20.3) + tilt (2.0.9) PLATFORMS java ruby DEPENDENCIES - aruba (~> 0.5.3) - cucumber (~> 1.3.9) - ronn + aruba (~> 1.0.0.pre.alpha.4) + cucumber (~> 3.1.2) sinatra BUNDLED WITH - 1.16.0 + 1.17.1 diff -Nru hub-2.7.0~ds1/git/git.go hub-2.14.2~ds1/git/git.go --- hub-2.7.0~ds1/git/git.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/git/git.go 2020-03-05 17:48:23.000000000 +0000 @@ -2,7 +2,6 @@ import ( "fmt" - "io/ioutil" "os" "path/filepath" "strings" @@ -13,12 +12,12 @@ var GlobalFlags []string func Version() (string, error) { - output, err := gitOutput("version") - if err == nil { - return output[0], nil - } else { + versionCmd := gitCmd("version") + output, err := versionCmd.Output() + if err != nil { return "", fmt.Errorf("error running git version: %s", err) } + return firstLine(output), nil } var cachedDir string @@ -28,7 +27,9 @@ return cachedDir, nil } - output, err := gitOutput("rev-parse", "-q", "--git-dir") + dirCmd := gitCmd("rev-parse", "-q", "--git-dir") + dirCmd.Stderr = nil + output, err := dirCmd.Output() if err != nil { return "", fmt.Errorf("Not a git repository (or any of the parent directories): .git") } @@ -45,7 +46,7 @@ } } - gitDir := output[0] + gitDir := firstLine(output) if !filepath.IsAbs(gitDir) { if chdir != "" { @@ -65,24 +66,25 @@ } func WorkdirName() (string, error) { - output, err := gitOutput("rev-parse", "--show-toplevel") - if err == nil { - if len(output) > 0 { - return output[0], nil - } else { - return "", fmt.Errorf("unable to determine git working directory") - } - } else { - return "", err + toplevelCmd := gitCmd("rev-parse", "--show-toplevel") + toplevelCmd.Stderr = nil + output, err := toplevelCmd.Output() + dir := firstLine(output) + if dir == "" { + return "", fmt.Errorf("unable to determine git working directory") } + return dir, err } func HasFile(segments ...string) bool { // The blessed way to resolve paths within git dir since Git 2.5.0 - output, err := gitOutput("rev-parse", "-q", "--git-path", filepath.Join(segments...)) - if err == nil && output[0] != "--git-path" { - if _, err := os.Stat(output[0]); err == nil { - return true + pathCmd := gitCmd("rev-parse", "-q", "--git-path", filepath.Join(segments...)) + pathCmd.Stderr = nil + if output, err := pathCmd.Output(); err == nil { + if lines := outputLines(output); len(lines) == 1 { + if _, err := os.Stat(lines[0]); err == nil { + return true + } } } @@ -102,80 +104,77 @@ return false } -func BranchAtRef(paths ...string) (name string, err error) { - dir, err := Dir() - if err != nil { - return - } - - segments := []string{dir} - segments = append(segments, paths...) - path := filepath.Join(segments...) - b, err := ioutil.ReadFile(path) - if err != nil { - return - } - - n := string(b) - refPrefix := "ref: " - if strings.HasPrefix(n, refPrefix) { - name = strings.TrimPrefix(n, refPrefix) - name = strings.TrimSpace(name) - } else { - err = fmt.Errorf("No branch info in %s: %s", path, n) - } - - return -} - func Editor() (string, error) { - output, err := gitOutput("var", "GIT_EDITOR") + varCmd := gitCmd("var", "GIT_EDITOR") + varCmd.Stderr = nil + output, err := varCmd.Output() if err != nil { return "", fmt.Errorf("Can't load git var: GIT_EDITOR") } - return os.ExpandEnv(output[0]), nil + return os.ExpandEnv(firstLine(output)), nil } func Head() (string, error) { - return BranchAtRef("HEAD") + return SymbolicRef("HEAD") } +// SymbolicRef reads a branch name from a ref such as "HEAD" +func SymbolicRef(ref string) (string, error) { + refCmd := gitCmd("symbolic-ref", ref) + refCmd.Stderr = nil + output, err := refCmd.Output() + return firstLine(output), err +} + +// SymbolicFullName reads a branch name from a ref such as "@{upstream}" func SymbolicFullName(name string) (string, error) { - output, err := gitOutput("rev-parse", "--symbolic-full-name", name) + parseCmd := gitCmd("rev-parse", "--symbolic-full-name", name) + parseCmd.Stderr = nil + output, err := parseCmd.Output() if err != nil { return "", fmt.Errorf("Unknown revision or path not in the working tree: %s", name) } - return output[0], nil + return firstLine(output), nil } func Ref(ref string) (string, error) { - output, err := gitOutput("rev-parse", "-q", ref) + parseCmd := gitCmd("rev-parse", "-q", ref) + parseCmd.Stderr = nil + output, err := parseCmd.Output() if err != nil { return "", fmt.Errorf("Unknown revision or path not in the working tree: %s", ref) } - return output[0], nil + return firstLine(output), nil } func RefList(a, b string) ([]string, error) { ref := fmt.Sprintf("%s...%s", a, b) - output, err := gitOutput("rev-list", "--cherry-pick", "--right-only", "--no-merges", ref) + listCmd := gitCmd("rev-list", "--cherry-pick", "--right-only", "--no-merges", ref) + listCmd.Stderr = nil + output, err := listCmd.Output() if err != nil { - return []string{}, fmt.Errorf("Can't load rev-list for %s", ref) + return nil, fmt.Errorf("Can't load rev-list for %s", ref) } - return output, nil + return outputLines(output), nil } func NewRange(a, b string) (*Range, error) { - output, err := gitOutput("rev-parse", "-q", a, b) + parseCmd := gitCmd("rev-parse", "-q", a, b) + parseCmd.Stderr = nil + output, err := parseCmd.Output() if err != nil { return nil, err } - return &Range{output[0], output[1]}, nil + lines := outputLines(output) + if len(lines) != 2 { + return nil, fmt.Errorf("Can't parse range %s..%s", a, b) + } + return &Range{lines[0], lines[1]}, nil } type Range struct { @@ -216,13 +215,12 @@ func Show(sha string) (string, error) { cmd := cmd.New("git") + cmd.Stderr = nil cmd.WithArg("-c").WithArg("log.showSignature=false") cmd.WithArg("show").WithArg("-s").WithArg("--format=%s%n%+b").WithArg(sha) - output, err := cmd.CombinedOutput() - output = strings.TrimSpace(output) - - return output, err + output, err := cmd.Output() + return strings.TrimSpace(output), err } func Log(sha1, sha2 string) (string, error) { @@ -233,7 +231,7 @@ shaRange := fmt.Sprintf("%s...%s", sha1, sha2) execCmd.WithArg(shaRange) - outputs, err := execCmd.CombinedOutput() + outputs, err := execCmd.Output() if err != nil { return "", fmt.Errorf("Can't load git log %s..%s", sha1, sha2) } @@ -242,7 +240,10 @@ } func Remotes() ([]string, error) { - return gitOutput("remote", "-v") + remoteCmd := gitCmd("remote", "-v") + remoteCmd.Stderr = nil + output, err := remoteCmd.Output() + return outputLines(output), err } func Config(name string) (string, error) { @@ -255,11 +256,12 @@ mode = "--get-regexp" } - lines, err := gitOutput(gitConfigCommand([]string{mode, name})...) + configCmd := gitCmd(gitConfigCommand([]string{mode, name})...) + output, err := configCmd.Output() if err != nil { - err = fmt.Errorf("Unknown config %s", name) + return nil, fmt.Errorf("Unknown config %s", name) } - return lines, err + return outputLines(output), nil } func GlobalConfig(name string) (string, error) { @@ -272,20 +274,19 @@ } func gitGetConfig(args ...string) (string, error) { - output, err := gitOutput(gitConfigCommand(args)...) + configCmd := gitCmd(gitConfigCommand(args)...) + output, err := configCmd.Output() if err != nil { return "", fmt.Errorf("Unknown config %s", args[len(args)-1]) } - if len(output) == 0 { - return "", nil - } - - return output[0], nil + return firstLine(output), nil } func gitConfig(args ...string) ([]string, error) { - return gitOutput(gitConfigCommand(args)...) + configCmd := gitCmd(gitConfigCommand(args)...) + output, err := configCmd.Output() + return outputLines(output), err } func gitConfigCommand(args []string) []string { @@ -319,27 +320,34 @@ } func LocalBranches() ([]string, error) { - lines, err := gitOutput("branch", "--list") - if err == nil { - for i, line := range lines { - lines[i] = strings.TrimPrefix(line, "* ") - lines[i] = strings.TrimPrefix(lines[i], " ") - } + branchesCmd := gitCmd("branch", "--list") + output, err := branchesCmd.Output() + if err != nil { + return nil, err } - return lines, err -} -func gitOutput(input ...string) (outputs []string, err error) { - cmd := gitCmd(input...) + branches := []string{} + for _, branch := range outputLines(output) { + branches = append(branches, branch[2:]) + } + return branches, nil +} - out, err := cmd.CombinedOutput() - for _, line := range strings.Split(out, "\n") { - if strings.TrimSpace(line) != "" { - outputs = append(outputs, string(line)) - } +func outputLines(output string) []string { + output = strings.TrimSuffix(output, "\n") + if output == "" { + return []string{} + } else { + return strings.Split(output, "\n") } +} - return outputs, err +func firstLine(output string) string { + if i := strings.Index(output, "\n"); i >= 0 { + return output[0:i] + } else { + return output + } } func gitCmd(args ...string) *cmd.Cmd { @@ -357,15 +365,18 @@ } func IsBuiltInGitCommand(command string) bool { - helpCommandOutput, err := gitOutput("help", "--no-verbose", "-a") + helpCommand := gitCmd("help", "--no-verbose", "-a") + helpCommand.Stderr = nil + helpCommandOutput, err := helpCommand.Output() if err != nil { // support git versions that don't recognize --no-verbose - helpCommandOutput, err = gitOutput("help", "-a") + helpCommand := gitCmd("help", "-a") + helpCommandOutput, err = helpCommand.Output() } if err != nil { return false } - for _, helpCommandOutputLine := range helpCommandOutput { + for _, helpCommandOutputLine := range outputLines(helpCommandOutput) { if strings.HasPrefix(helpCommandOutputLine, " ") { for _, gitCommand := range strings.Split(helpCommandOutputLine, " ") { if gitCommand == command { diff -Nru hub-2.7.0~ds1/git/git_test.go hub-2.14.2~ds1/git/git_test.go --- hub-2.7.0~ds1/git/git_test.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/git/git_test.go 2020-03-05 17:48:23.000000000 +0000 @@ -85,11 +85,11 @@ repo := fixtures.SetupTestRepo() defer repo.TearDown() - v, err := GlobalConfig("hub.test") + _, err := GlobalConfig("hub.test") assert.NotEqual(t, nil, err) SetGlobalConfig("hub.test", "1") - v, err = GlobalConfig("hub.test") + v, err := GlobalConfig("hub.test") assert.Equal(t, nil, err) assert.Equal(t, "1", v) @@ -181,6 +181,6 @@ assert.Equal(t, nil, err) assert.Equal(t, "@", char) - char, err = CommentChar("#\n;\n@\n!\n$\n%\n^\n&\n|\n:") + _, err = CommentChar("#\n;\n@\n!\n$\n%\n^\n&\n|\n:") assert.Equal(t, "unable to select a comment character that is not used in the current message", err.Error()) } diff -Nru hub-2.7.0~ds1/git/ssh_config.go hub-2.14.2~ds1/git/ssh_config.go --- hub-2.7.0~ds1/git/ssh_config.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/git/ssh_config.go 2020-03-05 17:48:23.000000000 +0000 @@ -6,6 +6,8 @@ "path/filepath" "regexp" "strings" + + "github.com/mitchellh/go-homedir" ) const ( @@ -15,12 +17,16 @@ type SSHConfig map[string]string func newSSHConfigReader() *SSHConfigReader { + configFiles := []string{ + "/etc/ssh_config", + "/etc/ssh/ssh_config", + } + if homedir, err := homedir.Dir(); err == nil { + userConfig := filepath.Join(homedir, ".ssh", "config") + configFiles = append([]string{userConfig}, configFiles...) + } return &SSHConfigReader{ - Files: []string{ - filepath.Join(os.Getenv("HOME"), ".ssh/config"), - "/etc/ssh_config", - "/etc/ssh/ssh_config", - }, + Files: configFiles, } } diff -Nru hub-2.7.0~ds1/github/branch.go hub-2.14.2~ds1/github/branch.go --- hub-2.7.0~ds1/github/branch.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/github/branch.go 2020-03-05 17:48:23.000000000 +0000 @@ -23,41 +23,6 @@ return reg.ReplaceAllString(b.Name, "") } -func (b *Branch) PushTarget(owner string, preferUpstream bool) (branch *Branch) { - var err error - pushDefault, _ := git.Config("push.default") - if pushDefault == "upstream" || pushDefault == "tracking" { - branch, err = b.Upstream() - if err != nil { - return - } - } else { - shortName := b.ShortName() - remotes := b.Repo.remotesForPublish(owner) - - var remotesInOrder []Remote - if preferUpstream { - // reverse the remote lookup order - // see OriginNamesInLookupOrder - for i := len(remotes) - 1; i >= 0; i-- { - remotesInOrder = append(remotesInOrder, remotes[i]) - } - } else { - remotesInOrder = remotes - } - - for _, remote := range remotesInOrder { - if git.HasFile("refs", "remotes", remote.Name, shortName) { - name := fmt.Sprintf("refs/remotes/%s/%s", remote.Name, shortName) - branch = &Branch{b.Repo, name} - break - } - } - } - - return -} - func (b *Branch) RemoteName() string { reg := regexp.MustCompile("^refs/remotes/([^/]+)") if reg.MatchString(b.Name) { diff -Nru hub-2.7.0~ds1/github/client.go hub-2.14.2~ds1/github/client.go --- hub-2.7.0~ds1/github/client.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/github/client.go 2020-03-05 17:48:23.000000000 +0000 @@ -1,13 +1,19 @@ package github import ( + "bytes" + "encoding/json" + "errors" "fmt" "io" + "io/ioutil" "net/http" "net/url" "os" "os/exec" + "path" "path/filepath" + "sort" "strings" "time" @@ -26,11 +32,27 @@ } func NewClientWithHost(host *Host) *Client { - return &Client{host} + return &Client{Host: host} } type Client struct { - Host *Host + Host *Host + cachedClient *simpleClient +} + +type Gist struct { + Files map[string]GistFile `json:"files"` + Description string `json:"description,omitempty"` + Id string `json:"id,omitempty"` + Public bool `json:"public"` + HtmlUrl string `json:"html_url"` +} + +type GistFile struct { + Type string `json:"type,omitempty"` + Language string `json:"language,omitempty"` + Content string `json:"content"` + RawUrl string `json:"raw_url"` } func (client *Client) FetchPullRequests(project *Project, filterParams map[string]interface{}, limit int, filter func(*PullRequest) bool) (pulls []PullRequest, err error) { @@ -41,21 +63,14 @@ path := fmt.Sprintf("repos/%s/%s/pulls?per_page=%d", project.Owner, project.Name, perPage(limit, 100)) if filterParams != nil { - query := url.Values{} - for key, value := range filterParams { - switch v := value.(type) { - case string: - query.Add(key, v) - } - } - path += "&" + query.Encode() + path = addQuery(path, filterParams) } pulls = []PullRequest{} var res *simpleResponse for path != "" { - res, err = api.Get(path) + res, err = api.GetFile(path, draftsType) if err = checkStatus(200, "fetching pull requests", res, err); err != nil { return } @@ -116,7 +131,7 @@ return } - res, err := api.PostJSON(fmt.Sprintf("repos/%s/%s/pulls", project.Owner, project.Name), params) + res, err := api.PostJSONPreview(fmt.Sprintf("repos/%s/%s/pulls", project.Owner, project.Name), params, draftsType) if err = checkStatus(201, "creating pull request", res, err); err != nil { if res != nil && res.StatusCode == 404 { projectUrl := strings.SplitN(project.WebURL("", "", ""), "://", 2)[1] @@ -160,13 +175,6 @@ return res.Body, nil } -type Gist struct { - Files map[string]GistFile `json:"files"` -} -type GistFile struct { - RawUrl string `json:"raw_url"` -} - func (client *Client) GistPatch(id string) (patch io.ReadCloser, err error) { api, err := client.simpleApi() if err != nil { @@ -373,26 +381,70 @@ return } -func (client *Client) UploadReleaseAsset(release *Release, filename, label string) (asset *ReleaseAsset, err error) { +type LocalAsset struct { + Name string + Label string + Contents io.Reader + Size int64 +} + +func (client *Client) UploadReleaseAssets(release *Release, assets []LocalAsset) (doneAssets []*ReleaseAsset, err error) { api, err := client.simpleApi() if err != nil { return } - parts := strings.SplitN(release.UploadUrl, "{", 2) - uploadUrl := parts[0] - uploadUrl += "?name=" + url.QueryEscape(filepath.Base(filename)) - if label != "" { - uploadUrl += "&label=" + url.QueryEscape(label) - } + idx := strings.Index(release.UploadUrl, "{") + uploadURL := release.UploadUrl[0:idx] - res, err := api.PostFile(uploadUrl, filename) - if err = checkStatus(201, "uploading release asset", res, err); err != nil { - return + for _, asset := range assets { + for _, existingAsset := range release.Assets { + if existingAsset.Name == asset.Name { + if err = client.DeleteReleaseAsset(&existingAsset); err != nil { + return + } + break + } + } + + params := map[string]interface{}{"name": filepath.Base(asset.Name)} + if asset.Label != "" { + params["label"] = asset.Label + } + uploadPath := addQuery(uploadURL, params) + + var res *simpleResponse + attempts := 0 + maxAttempts := 3 + body := asset.Contents + for { + res, err = api.PostFile(uploadPath, body, asset.Size) + if err == nil && res.StatusCode >= 500 && res.StatusCode < 600 && attempts < maxAttempts { + attempts++ + time.Sleep(time.Second * time.Duration(attempts)) + var f *os.File + f, err = os.Open(asset.Name) + if err != nil { + return + } + defer f.Close() + body = f + continue + } + if err = checkStatus(201, "uploading release asset", res, err); err != nil { + return + } + break + } + + newAsset := ReleaseAsset{} + err = res.Unmarshal(&newAsset) + if err != nil { + return + } + doneAssets = append(doneAssets, &newAsset) } - asset = &ReleaseAsset{} - err = res.Unmarshal(asset) return } @@ -460,6 +512,20 @@ return } + sortStatuses := func() { + sort.Slice(status.Statuses, func(a, b int) bool { + sA := status.Statuses[a] + sB := status.Statuses[b] + cmp := strings.Compare(strings.ToLower(sA.Context), strings.ToLower(sB.Context)) + if cmp == 0 { + return strings.Compare(sA.TargetUrl, sB.TargetUrl) < 0 + } else { + return cmp < 0 + } + }) + } + sortStatuses() + res, err = api.GetFile(fmt.Sprintf("repos/%s/%s/commits/%s/check-runs", project.Owner, project.Name, sha), checksType) if err == nil && (res.StatusCode == 403 || res.StatusCode == 404 || res.StatusCode == 422) { return @@ -486,6 +552,8 @@ status.Statuses = append(status.Statuses, checkStatus) } + sortStatuses() + return } @@ -544,6 +612,7 @@ MergeCommitSha string `json:"merge_commit_sha"` MaintainerCanModify bool `json:"maintainer_can_modify"` + Draft bool `json:"draft"` Comments int `json:"comments"` Labels []IssueLabel `json:"labels"` @@ -622,14 +691,7 @@ path := fmt.Sprintf("repos/%s/%s/issues?per_page=%d", project.Owner, project.Name, perPage(limit, 100)) if filterParams != nil { - query := url.Values{} - for key, value := range filterParams { - switch v := value.(type) { - case string: - query.Add(key, v) - } - } - path += "&" + query.Encode() + path = addQuery(path, filterParams) } issues = []Issue{} @@ -723,6 +785,18 @@ return } +type sortedLabels []IssueLabel + +func (s sortedLabels) Len() int { + return len(s) +} +func (s sortedLabels) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} +func (s sortedLabels) Less(i, j int) bool { + return strings.Compare(strings.ToLower(s[i].Name), strings.ToLower(s[j].Name)) < 0 +} + func (client *Client) FetchLabels(project *Project) (labels []IssueLabel, err error) { api, err := client.simpleApi() if err != nil { @@ -748,6 +822,8 @@ labels = append(labels, labelsPage...) } + sort.Sort(sortedLabels(labels)) + return } @@ -779,6 +855,78 @@ return } +func (client *Client) GenericAPIRequest(method, path string, data interface{}, headers map[string]string, ttl int) (*simpleResponse, error) { + api, err := client.simpleApi() + if err != nil { + return nil, err + } + api.CacheTTL = ttl + + var body io.Reader + switch d := data.(type) { + case map[string]interface{}: + if method == "GET" { + path = addQuery(path, d) + } else if len(d) > 0 { + json, err := json.Marshal(d) + if err != nil { + return nil, err + } + body = bytes.NewBuffer(json) + } + case io.Reader: + body = d + } + + return api.performRequest(method, path, body, func(req *http.Request) { + if body != nil { + req.Header.Set("Content-Type", "application/json; charset=utf-8") + } + for key, value := range headers { + req.Header.Set(key, value) + } + }) +} + +// GraphQL facilitates performing a GraphQL request and parsing the response +func (client *Client) GraphQL(query string, variables interface{}, data interface{}) error { + api, err := client.simpleApi() + if err != nil { + return err + } + + payload := map[string]interface{}{ + "query": query, + "variables": variables, + } + resp, err := api.PostJSON("graphql", payload) + if err = checkStatus(200, "performing GraphQL", resp, err); err != nil { + return err + } + + responseData := struct { + Data interface{} + Errors []struct { + Message string + } + }{ + Data: data, + } + err = resp.Unmarshal(&responseData) + if err != nil { + return err + } + + if len(responseData.Errors) > 0 { + messages := []string{} + for _, e := range responseData.Errors { + messages = append(messages, e.Message) + } + return fmt.Errorf("API error: %s", strings.Join(messages, "; ")) + } + return nil +} + func (client *Client) CurrentUser() (user *User, err error) { api, err := client.simpleApi() if err != nil { @@ -819,7 +967,7 @@ } params := map[string]interface{}{ - "scopes": []string{"repo"}, + "scopes": []string{"repo", "gist"}, "note_url": OAuthAppURL, } @@ -867,14 +1015,15 @@ return } -func (client *Client) ensureAccessToken() (err error) { +func (client *Client) ensureAccessToken() error { if client.Host.AccessToken == "" { host, err := CurrentConfig().PromptForHost(client.Host.Host) - if err == nil { - client.Host = host + if err != nil { + return err } + client.Host = host } - return + return nil } func (client *Client) simpleApi() (c *simpleClient, err error) { @@ -883,10 +1032,24 @@ return } + if client.cachedClient != nil { + c = client.cachedClient + return + } + c = client.apiClient() c.PrepareRequest = func(req *http.Request) { - req.Header.Set("Authorization", "token "+client.Host.AccessToken) + clientDomain := normalizeHost(client.Host.Host) + if strings.HasPrefix(clientDomain, "api.github.") { + clientDomain = strings.TrimPrefix(clientDomain, "api.") + } + requestHost := strings.ToLower(req.URL.Host) + if requestHost == clientDomain || strings.HasSuffix(requestHost, "."+clientDomain) { + req.Header.Set("Authorization", "token "+client.Host.AccessToken) + } } + + client.cachedClient = c return } @@ -905,13 +1068,69 @@ } func (client *Client) absolute(host string) *url.URL { - u, _ := url.Parse("https://" + host + "/") - if client.Host != nil && client.Host.Protocol != "" { + u, err := url.Parse("https://" + host + "/") + if err != nil { + panic(err) + } else if client.Host != nil && client.Host.Protocol != "" { u.Scheme = client.Host.Protocol } return u } +func (client *Client) FetchGist(id string) (gist *Gist, err error) { + api, err := client.simpleApi() + if err != nil { + return + } + + response, err := api.Get(fmt.Sprintf("gists/%s", id)) + if err = checkStatus(200, "getting gist", response, err); err != nil { + return + } + + response.Unmarshal(&gist) + return +} + +func (client *Client) CreateGist(filenames []string, public bool) (gist *Gist, err error) { + api, err := client.simpleApi() + if err != nil { + return + } + files := map[string]GistFile{} + var basename string + var content []byte + var gf GistFile + + for _, file := range filenames { + if file == "-" { + content, err = ioutil.ReadAll(os.Stdin) + basename = "gistfile1.txt" + } else { + content, err = ioutil.ReadFile(file) + basename = path.Base(file) + } + if err != nil { + return + } + gf = GistFile{Content: string(content)} + files[basename] = gf + } + + g := Gist{ + Files: files, + Public: public, + } + + res, err := api.PostJSON("gists", &g) + if err = checkStatus(201, "creating gist", res, err); err != nil { + return + } + + err = res.Unmarshal(&gist) + return +} + func normalizeHost(host string) string { if host == "" { return GitHubHost @@ -924,65 +1143,165 @@ } } +func reverseNormalizeHost(host string) string { + switch host { + case "api.github.com": + return GitHubHost + case "api.github.localhost": + return "github.localhost" + default: + return host + } +} + func checkStatus(expectedStatus int, action string, response *simpleResponse, err error) error { if err != nil { - return fmt.Errorf("Error %s: %s", action, err.Error()) + errStr := err.Error() + if urlErr, isURLErr := err.(*url.Error); isURLErr { + errStr = fmt.Sprintf("%s %s: %s", urlErr.Op, urlErr.URL, urlErr.Err) + } + return fmt.Errorf("Error %s: %s", action, errStr) } else if response.StatusCode != expectedStatus { errInfo, err := response.ErrorInfo() - if err == nil { - return FormatError(action, errInfo) - } else { + if err != nil { return fmt.Errorf("Error %s: %s (HTTP %d)", action, err.Error(), response.StatusCode) } - } else { - return nil + return FormatError(action, errInfo) } + return nil } -func FormatError(action string, err error) (ee error) { - switch e := err.(type) { - default: - ee = err - case *errorInfo: - statusCode := e.Response.StatusCode - var reason string - if s := strings.SplitN(e.Response.Status, " ", 2); len(s) >= 2 { - reason = strings.TrimSpace(s[1]) - } - - errStr := fmt.Sprintf("Error %s: %s (HTTP %d)", action, reason, statusCode) - - var errorSentences []string - for _, err := range e.Errors { - switch err.Code { - case "custom": - errorSentences = append(errorSentences, err.Message) - case "missing_field": - errorSentences = append(errorSentences, fmt.Sprintf("Missing field: \"%s\"", err.Field)) - case "already_exists": - errorSentences = append(errorSentences, fmt.Sprintf("Duplicate value for \"%s\"", err.Field)) - case "invalid": - errorSentences = append(errorSentences, fmt.Sprintf("Invalid value for \"%s\"", err.Field)) - case "unauthorized": - errorSentences = append(errorSentences, fmt.Sprintf("Not allowed to change field \"%s\"", err.Field)) - } - } +// FormatError annotates an HTTP response error with user-friendly messages +func FormatError(action string, err error) error { + if e, ok := err.(*errorInfo); ok { + return formatError(action, e) + } + return err +} - var errorMessage string - if len(errorSentences) > 0 { - errorMessage = strings.Join(errorSentences, "\n") - } else { - errorMessage = e.Message +func formatError(action string, e *errorInfo) error { + var reason string + if s := strings.SplitN(e.Response.Status, " ", 2); len(s) >= 2 { + reason = strings.TrimSpace(s[1]) + } + + errStr := fmt.Sprintf("Error %s: %s (HTTP %d)", action, reason, e.Response.StatusCode) + + var errorSentences []string + for _, err := range e.Errors { + switch err.Code { + case "custom": + errorSentences = append(errorSentences, err.Message) + case "missing_field": + errorSentences = append(errorSentences, fmt.Sprintf("Missing field: \"%s\"", err.Field)) + case "already_exists": + errorSentences = append(errorSentences, fmt.Sprintf("Duplicate value for \"%s\"", err.Field)) + case "invalid": + errorSentences = append(errorSentences, fmt.Sprintf("Invalid value for \"%s\"", err.Field)) + case "unauthorized": + errorSentences = append(errorSentences, fmt.Sprintf("Not allowed to change field \"%s\"", err.Field)) } + } - if errorMessage != "" { - errStr = fmt.Sprintf("%s\n%s", errStr, errorMessage) + var errorMessage string + if len(errorSentences) > 0 { + errorMessage = strings.Join(errorSentences, "\n") + } else { + errorMessage = e.Message + if action == "getting current user" && e.Message == "Resource not accessible by integration" { + errorMessage = errorMessage + "\nYou must specify GITHUB_USER via environment variable." } + } + if errorMessage != "" { + errStr = fmt.Sprintf("%s\n%s", errStr, errorMessage) + } - ee = fmt.Errorf(errStr) + if ssoErr := ValidateGitHubSSO(e.Response); ssoErr != nil { + return fmt.Errorf("%s\n%s", errStr, ssoErr) } - return + if scopeErr := ValidateSufficientOAuthScopes(e.Response); scopeErr != nil { + return fmt.Errorf("%s\n%s", errStr, scopeErr) + } + + return errors.New(errStr) +} + +// ValidateGitHubSSO checks for the challenge via `X-Github-Sso` header +func ValidateGitHubSSO(res *http.Response) error { + if res.StatusCode != 403 { + return nil + } + + sso := res.Header.Get("X-Github-Sso") + if !strings.HasPrefix(sso, "required; url=") { + return nil + } + + url := sso[strings.IndexByte(sso, '=')+1:] + return fmt.Errorf("You must authorize your token to access this organization:\n%s", url) +} + +// ValidateSufficientOAuthScopes warns about insufficient OAuth scopes +func ValidateSufficientOAuthScopes(res *http.Response) error { + if res.StatusCode != 404 && res.StatusCode != 403 { + return nil + } + + needScopes := newScopeSet(res.Header.Get("X-Accepted-Oauth-Scopes")) + if len(needScopes) == 0 && isGistWrite(res.Request) { + // compensate for a GitHub bug: gist APIs omit proper `X-Accepted-Oauth-Scopes` in responses + needScopes = newScopeSet("gist") + } + + haveScopes := newScopeSet(res.Header.Get("X-Oauth-Scopes")) + if len(needScopes) == 0 || needScopes.Intersects(haveScopes) { + return nil + } + + return fmt.Errorf("Your access token may have insufficient scopes. Visit %s://%s/settings/tokens\n"+ + "to edit the 'hub' token and enable one of the following scopes: %s", + res.Request.URL.Scheme, + reverseNormalizeHost(res.Request.Host), + needScopes) +} + +func isGistWrite(req *http.Request) bool { + if req.Method == "GET" { + return false + } + path := strings.TrimPrefix(req.URL.Path, "/v3") + return strings.HasPrefix(path, "/gists") +} + +type scopeSet map[string]struct{} + +func (s scopeSet) String() string { + scopes := make([]string, 0, len(s)) + for scope := range s { + scopes = append(scopes, scope) + } + sort.Sort(sort.StringSlice(scopes)) + return strings.Join(scopes, ", ") +} + +func (s scopeSet) Intersects(other scopeSet) bool { + for scope := range s { + if _, found := other[scope]; found { + return true + } + } + return false +} + +func newScopeSet(s string) scopeSet { + scopes := scopeSet{} + for _, s := range strings.SplitN(s, ",", -1) { + if s = strings.TrimSpace(s); s != "" { + scopes[s] = struct{}{} + } + } + return scopes } func authTokenNote(num int) (string, error) { @@ -1022,3 +1341,29 @@ } return max } + +func addQuery(path string, params map[string]interface{}) string { + if len(params) == 0 { + return path + } + + query := url.Values{} + for key, value := range params { + switch v := value.(type) { + case string: + query.Add(key, v) + case nil: + query.Add(key, "") + case int: + query.Add(key, fmt.Sprintf("%d", v)) + case bool: + query.Add(key, fmt.Sprintf("%v", v)) + } + } + + sep := "?" + if strings.Contains(path, sep) { + sep = "&" + } + return path + sep + query.Encode() +} diff -Nru hub-2.7.0~ds1/github/config_decoder.go hub-2.14.2~ds1/github/config_decoder.go --- hub-2.7.0~ds1/github/config_decoder.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/github/config_decoder.go 2020-03-05 17:48:23.000000000 +0000 @@ -1,6 +1,7 @@ package github import ( + "fmt" "io" "io/ioutil" @@ -29,24 +30,44 @@ return err } - yc := make(yamlConfig) + yc := yaml.MapSlice{} err = yaml.Unmarshal(d, &yc) if err != nil { return err } - for h, v := range yc { + for _, hostEntry := range yc { + v, ok := hostEntry.Value.([]interface{}) + if !ok { + return fmt.Errorf("value of host entry is must be array but got %#v", hostEntry.Value) + } if len(v) < 1 { continue } - vv := v[0] - host := &Host{ - Host: h, - User: vv.User, - AccessToken: vv.OAuthToken, - Protocol: vv.Protocol, - UnixSocket: vv.UnixSocket, + hostName, ok := hostEntry.Key.(string) + if !ok { + return fmt.Errorf("host name is must be string but got %#v", hostEntry.Key) + } + host := &Host{Host: hostName} + for _, prop := range v[0].(yaml.MapSlice) { + propName, ok := prop.Key.(string) + if !ok { + return fmt.Errorf("property name is must be string but got %#v", prop.Key) + } + switch propName { + case "user": + host.User, ok = prop.Value.(string) + case "oauth_token": + host.AccessToken, ok = prop.Value.(string) + case "protocol": + host.Protocol, ok = prop.Value.(string) + case "unix_socket": + host.UnixSocket, ok = prop.Value.(string) + } + if !ok { + return fmt.Errorf("%s is must be string but got %#v", propName, prop.Value) + } } c.Hosts = append(c.Hosts, host) } diff -Nru hub-2.7.0~ds1/github/config_encoder.go hub-2.14.2~ds1/github/config_encoder.go --- hub-2.7.0~ds1/github/config_encoder.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/github/config_encoder.go 2020-03-05 17:48:23.000000000 +0000 @@ -23,16 +23,19 @@ } func (y *yamlConfigEncoder) Encode(w io.Writer, c *Config) error { - yc := make(yamlConfig) + yc := yaml.MapSlice{} for _, h := range c.Hosts { - yc[h.Host] = []yamlHost{ - { - User: h.User, - OAuthToken: h.AccessToken, - Protocol: h.Protocol, - UnixSocket: h.UnixSocket, + yc = append(yc, yaml.MapItem{ + Key: h.Host, + Value: []yamlHost{ + { + User: h.User, + OAuthToken: h.AccessToken, + Protocol: h.Protocol, + UnixSocket: h.UnixSocket, + }, }, - } + }) } d, err := yaml.Marshal(yc) diff -Nru hub-2.7.0~ds1/github/config.go hub-2.14.2~ds1/github/config.go --- hub-2.7.0~ds1/github/config.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/github/config.go 2020-03-05 17:48:23.000000000 +0000 @@ -4,6 +4,7 @@ "bufio" "fmt" "io/ioutil" + "net/url" "os" "os/signal" "path/filepath" @@ -24,8 +25,6 @@ UnixSocket string `yaml:"unix_socket,omitempty"` } -type yamlConfig map[string][]yamlHost - type Host struct { Host string `toml:"host"` User string `toml:"user"` @@ -42,6 +41,13 @@ token := c.DetectToken() tokenFromEnv := token != "" + if host != GitHubHost { + if _, e := url.Parse("https://" + host); e != nil { + err = fmt.Errorf("invalid hostname: %q", host) + return + } + } + h = c.Find(host) if h != nil { if h.User == "" { @@ -81,11 +87,24 @@ } } - currentUser, err := client.CurrentUser() - if err != nil { - return + userFromEnv := os.Getenv("GITHUB_USER") + repoFromEnv := os.Getenv("GITHUB_REPOSITORY") + if userFromEnv == "" && repoFromEnv != "" { + repoParts := strings.SplitN(repoFromEnv, "/", 2) + if len(repoParts) > 0 { + userFromEnv = repoParts[0] + } + } + if tokenFromEnv && userFromEnv != "" { + h.User = userFromEnv + } else { + var currentUser *User + currentUser, err = client.CurrentUser() + if err != nil { + return + } + h.User = currentUser.Login } - h.User = currentUser.Login if !tokenFromEnv { err = newConfigService().Save(configsFile(), c) @@ -180,7 +199,7 @@ } c := make(chan os.Signal) - signal.Notify(c, os.Interrupt, os.Kill) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { s := <-c terminal.Restore(stdin, initialTermState) @@ -325,6 +344,18 @@ return } +func (c *Config) DefaultHostNoPrompt() (*Host, error) { + if GitHubHostEnv != "" { + return c.PromptForHost(GitHubHostEnv) + } else if len(c.Hosts) > 0 { + host := c.Hosts[0] + // HACK: forces host to inherit GITHUB_TOKEN if applicable + return c.PromptForHost(host.Host) + } else { + return c.PromptForHost(GitHubHost) + } +} + // CheckWriteable checks if config file is writeable. This should // be called before asking for credentials and only if current // operation needs to update the file. See issue #1314 for details. diff -Nru hub-2.7.0~ds1/github/config_service_test.go hub-2.14.2~ds1/github/config_service_test.go --- hub-2.7.0~ds1/github/config_service_test.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/github/config_service_test.go 2020-03-05 17:48:23.000000000 +0000 @@ -94,6 +94,51 @@ assert.Equal(t, "/tmp/go.sock", host.UnixSocket) } +func TestConfigService_YamlLoad_Invalid_HostName(t *testing.T) { + testConfigInvalidHostName := fixtures.SetupTestConfigsInvalidHostName() + defer testConfigInvalidHostName.TearDown() + + cc := &Config{} + cs := &configService{ + Encoder: &yamlConfigEncoder{}, + Decoder: &yamlConfigDecoder{}, + } + + err := cs.Load(testConfigInvalidHostName.Path, cc) + assert.NotEqual(t, nil, err) + assert.Equal(t, "host name is must be string but got 123", err.Error()) +} + +func TestConfigService_YamlLoad_Invalid_HostEntry(t *testing.T) { + testConfigInvalidHostEntry := fixtures.SetupTestConfigsInvalidHostEntry() + defer testConfigInvalidHostEntry.TearDown() + + cc := &Config{} + cs := &configService{ + Encoder: &yamlConfigEncoder{}, + Decoder: &yamlConfigDecoder{}, + } + + err := cs.Load(testConfigInvalidHostEntry.Path, cc) + assert.NotEqual(t, nil, err) + assert.Equal(t, "value of host entry is must be array but got \"hello\"", err.Error()) +} + +func TestConfigService_YamlLoad_Invalid_PropertyValue(t *testing.T) { + testConfigInvalidPropertyValue := fixtures.SetupTestConfigsInvalidPropertyValue() + defer testConfigInvalidPropertyValue.TearDown() + + cc := &Config{} + cs := &configService{ + Encoder: &yamlConfigEncoder{}, + Decoder: &yamlConfigDecoder{}, + } + + err := cs.Load(testConfigInvalidPropertyValue.Path, cc) + assert.NotEqual(t, nil, err) + assert.Equal(t, "user is must be string but got ", err.Error()) +} + func TestConfigService_TomlSave(t *testing.T) { file, _ := ioutil.TempFile("", "test-gh-config-") defer os.RemoveAll(file.Name()) diff -Nru hub-2.7.0~ds1/github/crash_report.go hub-2.14.2~ds1/github/crash_report.go --- hub-2.7.0~ds1/github/crash_report.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/github/crash_report.go 2020-03-05 17:48:23.000000000 +0000 @@ -1,6 +1,7 @@ package github import ( + "bufio" "bytes" "errors" "fmt" @@ -23,42 +24,50 @@ func CaptureCrash() { if rec := recover(); rec != nil { - if err, ok := rec.(error); ok { + switch err := rec.(type) { + case error: reportCrash(err) - } else if err, ok := rec.(string); ok { + case string: reportCrash(errors.New(err)) + default: + return } + os.Exit(1) } } func reportCrash(err error) { - if err == nil { - return - } - buf := make([]byte, 10000) runtime.Stack(buf, false) stack := formatStack(buf) - switch reportCrashConfig() { - case "always": - report(err, stack) - case "never": - printError(err, stack) - default: - printError(err, stack) - fmt.Print("Would you like to open an issue? ([Y]es/[N]o/[A]lways/N[e]ver): ") - var confirm string - fmt.Scan(&confirm) - - always := utils.IsOption(confirm, "a", "always") - if always || utils.IsOption(confirm, "y", "yes") { - report(err, stack) - } + ui.Errorf("%v\n\n", err) + ui.Errorln(stack) + + isTerm := ui.IsTerminal(os.Stdin) && ui.IsTerminal(os.Stdout) + if !isTerm || reportCrashConfig() == "never" { + return + } - saveReportConfiguration(confirm, always) + ui.Print("Would you like to open an issue? ([y]es / [N]o / n[e]ver): ") + var confirm string + prompt := bufio.NewScanner(os.Stdin) + if prompt.Scan() { + confirm = prompt.Text() + } + if prompt.Err() != nil { + return } - os.Exit(1) + + if isOption(confirm, "y", "yes") { + report(err, stack) + } else if isOption(confirm, "e", "never") { + git.SetGlobalConfig(hubReportCrashConfig, "never") + } +} + +func isOption(confirm, short, long string) bool { + return strings.EqualFold(confirm, short) || strings.EqualFold(confirm, long) } func report(reportedError error, stack string) { @@ -85,11 +94,14 @@ "Error (%s): `%v`\n\n" + "Stack:\n\n```\n%s\n```\n\n" + "Runtime:\n\n```\n%s\n```\n\n" + - "Version:\n\n```\n%s\n```\n" + "Version:\n\n```\n%s\nhub version %s\n```\n" func reportTitleAndBody(reportedError error, stack string) (title, body string, err error) { errType := reflect.TypeOf(reportedError).String() - fullVersion, _ := version.FullVersion() + gitVersion, gitErr := git.Version() + if gitErr != nil { + gitVersion = "git unavailable!" + } message := fmt.Sprintf( crashReportTmpl, reportedError, @@ -97,7 +109,8 @@ reportedError, stack, runtimeInfo(), - fullVersion, + gitVersion, + version.Version, ) messageBuilder := &MessageBuilder{ @@ -135,19 +148,6 @@ return strings.Join(stack, "\n") } -func printError(err error, stack string) { - ui.Printf("%v\n\n", err) - ui.Println(stack) -} - -func saveReportConfiguration(confirm string, always bool) { - if always { - git.SetGlobalConfig(hubReportCrashConfig, "always") - } else if utils.IsOption(confirm, "e", "never") { - git.SetGlobalConfig(hubReportCrashConfig, "never") - } -} - func reportCrashConfig() (opt string) { opt = os.Getenv("HUB_REPORT_CRASH") if opt == "" { diff -Nru hub-2.7.0~ds1/github/crash_report_test.go hub-2.14.2~ds1/github/crash_report_test.go --- hub-2.7.0~ds1/github/crash_report_test.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/github/crash_report_test.go 2020-03-05 17:48:23.000000000 +0000 @@ -4,7 +4,6 @@ "testing" "github.com/bmizerany/assert" - "github.com/github/hub/fixtures" ) func TestStackRemoveSelfAndPanic(t *testing.T) { @@ -31,31 +30,3 @@ s := formatStack([]byte(actual)) assert.Equal(t, expected, s) } - -func TestSaveAlwaysReportOption(t *testing.T) { - checkSavedReportCrashOption(t, true, "a", "always") - checkSavedReportCrashOption(t, true, "always", "always") -} - -func TestSaveNeverReportOption(t *testing.T) { - checkSavedReportCrashOption(t, false, "e", "never") - checkSavedReportCrashOption(t, false, "never", "never") -} - -func TestDoesntSaveYesReportOption(t *testing.T) { - checkSavedReportCrashOption(t, false, "y", "") - checkSavedReportCrashOption(t, false, "yes", "") -} - -func TestDoesntSaveNoReportOption(t *testing.T) { - checkSavedReportCrashOption(t, false, "n", "") - checkSavedReportCrashOption(t, false, "no", "") -} - -func checkSavedReportCrashOption(t *testing.T, always bool, confirm, expected string) { - repo := fixtures.SetupTestRepo() - defer repo.TearDown() - - saveReportConfiguration(confirm, always) - assert.Equal(t, expected, reportCrashConfig()) -} diff -Nru hub-2.7.0~ds1/github/editor.go hub-2.14.2~ds1/github/editor.go --- hub-2.7.0~ds1/github/editor.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/github/editor.go 2020-03-05 17:48:23.000000000 +0000 @@ -12,6 +12,7 @@ "github.com/github/hub/cmd" "github.com/github/hub/git" + "github.com/kballard/go-shellquote" ) const Scissors = "------------------------ >8 ------------------------" @@ -137,7 +138,11 @@ } func openTextEditor(program, file string) error { - editCmd := cmd.New(program) + programArgs, err := shellquote.Split(program) + if err != nil { + return err + } + editCmd := cmd.NewWithArray(programArgs) r := regexp.MustCompile(`\b(?:[gm]?vim)(?:\.exe)?$`) if r.MatchString(editCmd.Name) { editCmd.WithArg("--cmd") diff -Nru hub-2.7.0~ds1/github/http.go hub-2.14.2~ds1/github/http.go --- hub-2.7.0~ds1/github/http.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/github/http.go 2020-03-05 17:48:23.000000000 +0000 @@ -3,7 +3,9 @@ import ( "bytes" "context" + "crypto/md5" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -11,22 +13,39 @@ "net/http" "net/url" "os" + "path" + "path/filepath" "regexp" + "sort" + "strconv" "strings" "time" "github.com/github/hub/ui" "github.com/github/hub/utils" + "golang.org/x/net/http/httpproxy" ) const apiPayloadVersion = "application/vnd.github.v3+json;charset=utf-8" const patchMediaType = "application/vnd.github.v3.patch;charset=utf-8" const textMediaType = "text/plain;charset=utf-8" const checksType = "application/vnd.github.antiope-preview+json;charset=utf-8" +const draftsType = "application/vnd.github.shadow-cat-preview+json;charset=utf-8" +const cacheVersion = 2 + +const ( + rateLimitRemainingHeader = "X-Ratelimit-Remaining" + rateLimitResetHeader = "X-Ratelimit-Reset" +) var inspectHeaders = []string{ "Authorization", "X-GitHub-OTP", + "X-GitHub-SSO", + "X-Oauth-Scopes", + "X-Accepted-Oauth-Scopes", + "X-Oauth-Client-Id", + "X-GitHub-Enterprise-Version", "Location", "Link", "Accept", @@ -72,10 +91,12 @@ info := fmt.Sprintf("> %s %s://%s%s", req.Method, req.URL.Scheme, req.URL.Host, req.URL.RequestURI()) t.verbosePrintln(info) t.dumpHeaders(req.Header, ">") - body := t.dumpBody(req.Body) - if body != nil { - // reset body since it's been read - req.Body = body + if inspectableType(req.Header.Get("content-type")) { + body := t.dumpBody(req.Body) + if body != nil { + // reset body since it's been read + req.Body = body + } } } @@ -83,10 +104,12 @@ info := fmt.Sprintf("< HTTP %d", resp.StatusCode) t.verbosePrintln(info) t.dumpHeaders(resp.Header, "<") - body := t.dumpBody(resp.Body) - if body != nil { - // reset body since it's been read - resp.Body = body + if inspectableType(resp.Header.Get("content-type")) { + body := t.dumpBody(resp.Body) + if body != nil { + // reset body since it's been read + resp.Body = body + } } } @@ -136,6 +159,12 @@ fmt.Fprintln(t.Out, msg) } +var jsonTypeRE = regexp.MustCompile(`[/+]json($|;)`) + +func inspectableType(ct string) bool { + return strings.HasPrefix(ct, "text/") || jsonTypeRE.MatchString(ct) +} + func newHttpClient(testHost string, verbose bool, unixSocket string) *http.Client { var testURL *url.URL if testHost != "" { @@ -175,10 +204,35 @@ } return &http.Client{ - Transport: tr, + Transport: tr, + CheckRedirect: checkRedirect, } } +func checkRedirect(req *http.Request, via []*http.Request) error { + var recommendedCode int + switch req.Response.StatusCode { + case 301: + recommendedCode = 308 + case 302: + recommendedCode = 307 + } + + origMethod := via[len(via)-1].Method + if recommendedCode != 0 && !strings.EqualFold(req.Method, origMethod) { + return fmt.Errorf( + "refusing to follow HTTP %d redirect for a %s request\n"+ + "Have your site admin use HTTP %d for this kind of redirect", + req.Response.StatusCode, origMethod, recommendedCode) + } + + // inherited from stdlib defaultCheckRedirect + if len(via) >= 10 { + return errors.New("stopped after 10 redirects") + } + return nil +} + func cloneRequest(req *http.Request) *http.Request { dup := new(http.Request) *dup = *req @@ -190,37 +244,28 @@ return dup } -// An implementation of http.ProxyFromEnvironment that isn't broken -func proxyFromEnvironment(req *http.Request) (*url.URL, error) { - proxy := os.Getenv("http_proxy") - if proxy == "" { - proxy = os.Getenv("HTTP_PROXY") - } - if proxy == "" { - return nil, nil - } - - proxyURL, err := url.Parse(proxy) - if err != nil || !strings.HasPrefix(proxyURL.Scheme, "http") { - if proxyURL, err := url.Parse("http://" + proxy); err == nil { - return proxyURL, nil - } - } +var proxyFunc func(*url.URL) (*url.URL, error) - if err != nil { - return nil, fmt.Errorf("invalid proxy address %q: %v", proxy, err) +func proxyFromEnvironment(req *http.Request) (*url.URL, error) { + if proxyFunc == nil { + proxyFunc = httpproxy.FromEnvironment().ProxyFunc() } - - return proxyURL, nil + return proxyFunc(req.URL) } type simpleClient struct { httpClient *http.Client rootUrl *url.URL PrepareRequest func(*http.Request) + CacheTTL int } func (c *simpleClient) performRequest(method, path string, body io.Reader, configure func(*http.Request)) (*simpleResponse, error) { + if path == "graphql" { + // FIXME: This dirty workaround cancels out the "v3" portion of the + // "/api/v3" prefix used for Enterprise. Find a better place for this. + path = "../graphql" + } url, err := url.Parse(path) if err == nil { url = c.rootUrl.ResolveReference(url) @@ -245,10 +290,10 @@ configure(req) } - var bodyBackup io.ReadWriter - if req.Body != nil { - bodyBackup = &bytes.Buffer{} - req.Body = ioutil.NopCloser(io.TeeReader(req.Body, bodyBackup)) + key := cacheKey(req) + if cachedResponse := c.cacheRead(key, req); cachedResponse != nil { + res = &simpleResponse{cachedResponse} + return } httpResponse, err := c.httpClient.Do(req) @@ -256,11 +301,143 @@ return } + c.cacheWrite(key, httpResponse) res = &simpleResponse{httpResponse} return } +func isGraphQL(req *http.Request) bool { + return req.URL.Path == "/graphql" +} + +func canCache(req *http.Request) bool { + return strings.EqualFold(req.Method, "GET") || isGraphQL(req) +} + +func (c *simpleClient) cacheRead(key string, req *http.Request) (res *http.Response) { + if c.CacheTTL > 0 && canCache(req) { + f := cacheFile(key) + cacheInfo, err := os.Stat(f) + if err != nil { + return + } + if time.Since(cacheInfo.ModTime()).Seconds() > float64(c.CacheTTL) { + return + } + cf, err := os.Open(f) + if err != nil { + return + } + defer cf.Close() + + cb, err := ioutil.ReadAll(cf) + if err != nil { + return + } + parts := strings.SplitN(string(cb), "\r\n\r\n", 2) + if len(parts) < 2 { + return + } + + res = &http.Response{ + Body: ioutil.NopCloser(bytes.NewBufferString(parts[1])), + Header: http.Header{}, + Request: req, + } + headerLines := strings.Split(parts[0], "\r\n") + if len(headerLines) < 1 { + return + } + if proto := strings.SplitN(headerLines[0], " ", 3); len(proto) >= 3 { + res.Proto = proto[0] + res.Status = fmt.Sprintf("%s %s", proto[1], proto[2]) + if code, _ := strconv.Atoi(proto[1]); code > 0 { + res.StatusCode = code + } + } + for _, line := range headerLines[1:] { + kv := strings.SplitN(line, ":", 2) + if len(kv) >= 2 { + res.Header.Add(kv[0], strings.TrimLeft(kv[1], " ")) + } + } + } + return +} + +func (c *simpleClient) cacheWrite(key string, res *http.Response) { + if c.CacheTTL > 0 && canCache(res.Request) && res.StatusCode < 500 && res.StatusCode != 403 { + bodyCopy := &bytes.Buffer{} + bodyReplacement := readCloserCallback{ + Reader: io.TeeReader(res.Body, bodyCopy), + Closer: res.Body, + Callback: func() { + f := cacheFile(key) + err := os.MkdirAll(filepath.Dir(f), 0771) + if err != nil { + return + } + cf, err := os.OpenFile(f, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return + } + defer cf.Close() + fmt.Fprintf(cf, "%s %s\r\n", res.Proto, res.Status) + res.Header.Write(cf) + fmt.Fprintf(cf, "\r\n") + io.Copy(cf, bodyCopy) + }, + } + res.Body = &bodyReplacement + } +} + +type readCloserCallback struct { + Callback func() + Closer io.Closer + io.Reader +} + +func (rc *readCloserCallback) Close() error { + err := rc.Closer.Close() + if err == nil { + rc.Callback() + } + return err +} + +func cacheKey(req *http.Request) string { + path := strings.Replace(req.URL.EscapedPath(), "/", "-", -1) + if len(path) > 1 { + path = strings.TrimPrefix(path, "-") + } + host := req.Host + if host == "" { + host = req.URL.Host + } + hash := md5.New() + fmt.Fprintf(hash, "%d:", cacheVersion) + io.WriteString(hash, req.Header.Get("Accept")) + io.WriteString(hash, req.Header.Get("Authorization")) + queryParts := strings.Split(req.URL.RawQuery, "&") + sort.Strings(queryParts) + for _, q := range queryParts { + fmt.Fprintf(hash, "%s&", q) + } + if isGraphQL(req) && req.Body != nil { + if b, err := ioutil.ReadAll(req.Body); err == nil { + req.Body = ioutil.NopCloser(bytes.NewBuffer(b)) + hash.Write(b) + } + } + return fmt.Sprintf("%s/%s_%x", host, path, hash.Sum(nil)) +} + +func cacheFile(key string) string { + return path.Join(os.TempDir(), "hub", "api", key) +} + func (c *simpleClient) jsonRequest(method, path string, body interface{}, configure func(*http.Request)) (*simpleResponse, error) { json, err := json.Marshal(body) if err != nil { @@ -294,24 +471,21 @@ return c.jsonRequest("POST", path, payload, nil) } +func (c *simpleClient) PostJSONPreview(path string, payload interface{}, mimeType string) (*simpleResponse, error) { + return c.jsonRequest("POST", path, payload, func(req *http.Request) { + req.Header.Set("Accept", mimeType) + }) +} + func (c *simpleClient) PatchJSON(path string, payload interface{}) (*simpleResponse, error) { return c.jsonRequest("PATCH", path, payload, nil) } -func (c *simpleClient) PostFile(path, filename string) (*simpleResponse, error) { - stat, err := os.Stat(filename) - if err != nil { - return nil, err - } - - file, err := os.Open(filename) - if err != nil { - return nil, err - } - defer file.Close() - - return c.performRequest("POST", path, file, func(req *http.Request) { - req.ContentLength = stat.Size() +func (c *simpleClient) PostFile(path string, contents io.Reader, fileSize int64) (*simpleResponse, error) { + return c.performRequest("POST", path, contents, func(req *http.Request) { + if fileSize > 0 { + req.ContentLength = fileSize + } req.Header.Set("Content-Type", "application/octet-stream") }) } @@ -390,3 +564,21 @@ } return "" } + +func (res *simpleResponse) RateLimitRemaining() int { + if v := res.Header.Get(rateLimitRemainingHeader); len(v) > 0 { + if num, err := strconv.Atoi(v); err == nil { + return num + } + } + return -1 +} + +func (res *simpleResponse) RateLimitReset() int { + if v := res.Header.Get(rateLimitResetHeader); len(v) > 0 { + if ts, err := strconv.Atoi(v); err == nil { + return ts + } + } + return -1 +} diff -Nru hub-2.7.0~ds1/github/localrepo.go hub-2.14.2~ds1/github/localrepo.go --- hub-2.7.0~ds1/github/localrepo.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/github/localrepo.go 2020-03-05 17:48:23.000000000 +0000 @@ -113,14 +113,16 @@ } func (r *GitHubRepo) DefaultBranch(remote *Remote) *Branch { - var name string - if remote != nil { - name, _ = git.BranchAtRef("refs", "remotes", remote.Name, "HEAD") + b := Branch{ + Repo: r, + Name: "refs/heads/master", } - if name == "" { - name = "refs/heads/master" + if remote != nil { + if name, err := git.SymbolicRef(fmt.Sprintf("refs/remotes/%s/HEAD", remote.Name)); err == nil { + b.Name = name + } } - return &Branch{r, name} + return &b } func (r *GitHubRepo) RemoteBranchAndProject(owner string, preferUpstream bool) (branch *Branch, project *Project, err error) { @@ -140,22 +142,65 @@ return } - if project != nil { - branch = branch.PushTarget(owner, preferUpstream) - if branch != nil && branch.IsRemote() { - remote, e := r.RemoteByName(branch.RemoteName()) + if project == nil { + return + } + + pushDefault, _ := git.Config("push.default") + if pushDefault == "upstream" || pushDefault == "tracking" { + upstream, e := branch.Upstream() + if e == nil && upstream.IsRemote() { + remote, e := r.RemoteByName(upstream.RemoteName()) if e == nil { - project, err = remote.Project() - if err != nil { + p, e := remote.Project() + if e == nil { + branch = upstream + project = p return } } } } + shortName := branch.ShortName() + remotes := r.remotesForPublish(owner) + if preferUpstream { + // reverse the remote lookup order; see OriginNamesInLookupOrder + remotesInOrder := []Remote{} + for i := len(remotes) - 1; i >= 0; i-- { + remotesInOrder = append(remotesInOrder, remotes[i]) + } + remotes = remotesInOrder + } + + for _, remote := range remotes { + p, e := remote.Project() + if e != nil { + continue + } + // NOTE: this is similar RemoteForBranch + if git.HasFile("refs", "remotes", remote.Name, shortName) { + name := fmt.Sprintf("refs/remotes/%s/%s", remote.Name, shortName) + branch = &Branch{r, name} + project = p + return + } + } + + branch = nil return } +func (r *GitHubRepo) RemoteForBranch(branch *Branch, owner string) *Remote { + branchName := branch.ShortName() + for _, remote := range r.remotesForPublish(owner) { + if git.HasFile("refs", "remotes", remote.Name, branchName) { + return &remote + } + } + return nil +} + func (r *GitHubRepo) RemoteForRepo(repo *Repository) (*Remote, error) { if err := r.loadRemotes(); err != nil { return nil, err diff -Nru hub-2.7.0~ds1/github/project.go hub-2.14.2~ds1/github/project.go --- hub-2.7.0~ds1/github/project.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/github/project.go 2020-03-05 17:48:23.000000000 +0000 @@ -23,9 +23,9 @@ } func (p *Project) SameAs(other *Project) bool { - return strings.ToLower(p.Owner) == strings.ToLower(other.Owner) && - strings.ToLower(p.Name) == strings.ToLower(other.Name) && - strings.ToLower(p.Host) == strings.ToLower(other.Host) + return strings.EqualFold(p.Owner, other.Owner) && + strings.EqualFold(p.Name, other.Name) && + strings.EqualFold(p.Host, other.Host) } func (p *Project) WebURL(name, owner, path string) string { diff -Nru hub-2.7.0~ds1/github/project_test.go hub-2.14.2~ds1/github/project_test.go --- hub-2.7.0~ds1/github/project_test.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/github/project_test.go 2020-03-05 17:48:23.000000000 +0000 @@ -9,6 +9,53 @@ "github.com/github/hub/fixtures" ) +func TestSameAs(t *testing.T) { + tests := []struct { + name string + project1 *Project + project2 *Project + want bool + }{ + { + name: "same project", + project1: &Project{ + Owner: "fOo", + Name: "baR", + Host: "gItHUb.com", + }, + project2: &Project{ + Owner: "FoO", + Name: "BAr", + Host: "GithUB.com", + }, + want: true, + }, + { + name: "different project", + project1: &Project{ + Owner: "foo", + Name: "bar", + Host: "github.com", + }, + project2: &Project{ + Owner: "foo", + Name: "baz", + Host: "github.com", + }, + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := test.project1.SameAs(test.project2) + want := test.want + + assert.Equal(t, want, got) + }) + } +} + func TestProject_WebURL(t *testing.T) { project := Project{ Name: "foo", diff -Nru hub-2.7.0~ds1/github/util.go hub-2.14.2~ds1/github/util.go --- hub-2.7.0~ds1/github/util.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/github/util.go 2020-03-05 17:48:23.000000000 +0000 @@ -6,14 +6,5 @@ func IsHttpsProtocol() bool { httpProtocol, _ := git.Config("hub.protocol") - if httpProtocol == "https" { - return true - } - - httpClone, _ := git.Config("--bool hub.http-clone") - if httpClone == "true" { - return true - } - - return false + return httpProtocol == "https" } diff -Nru hub-2.7.0~ds1/.github/ISSUE_TEMPLATE/bug_report.md hub-2.14.2~ds1/.github/ISSUE_TEMPLATE/bug_report.md --- hub-2.7.0~ds1/.github/ISSUE_TEMPLATE/bug_report.md 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/.github/ISSUE_TEMPLATE/bug_report.md 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,17 @@ +--- +name: Bug report +about: Unexpected or broken behavior of "hub" command-line tool +title: '' +labels: bug +assignees: '' + +--- + +**Command attempted:** + + +**What happened:** + + +**More info:** + diff -Nru hub-2.7.0~ds1/.github/ISSUE_TEMPLATE/feature_request.md hub-2.14.2~ds1/.github/ISSUE_TEMPLATE/feature_request.md --- hub-2.7.0~ds1/.github/ISSUE_TEMPLATE/feature_request.md 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/.github/ISSUE_TEMPLATE/feature_request.md 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,14 @@ +--- +name: Feature request +about: Suggest new functionality for "hub" command-line tool +title: '' +labels: feature +assignees: '' + +--- + +**The problem I'm trying to solve:** + + +**How I imagine hub could expose this functionality:** + diff -Nru hub-2.7.0~ds1/.github/workflows/ci.yml hub-2.14.2~ds1/.github/workflows/ci.yml --- hub-2.7.0~ds1/.github/workflows/ci.yml 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/.github/workflows/ci.yml 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,52 @@ +name: CI + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + go: [ '1.11', '1.12', '1.13', '1.14' ] + + steps: + - uses: actions/checkout@v1 + + - name: Set up Ruby + uses: actions/setup-ruby@v1 + with: + ruby-version: 2.6.x + + - name: Cache gems + uses: actions/cache@v1 + with: + path: vendor/bundle + key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gem- + + - name: Bundle install + run: | + bundle install --path vendor/bundle + bundle binstub cucumber --path bin + + - name: Set up Go + uses: actions/setup-go@v1 + with: + go-version: ${{ matrix.go }} + + # - name: Install system packages + # if: runner.os == 'Linux' + # run: sudo apt-get install -y zsh fish + + - name: Run tests + shell: bash + run: | + export GOPATH="$HOME"/go + mkdir -p "$GOPATH"/src/github.com/github + ln -svf "$PWD" "$GOPATH"/src/github.com/github/hub + cd "$GOPATH"/src/github.com/github/hub + make test-all + env: + CI: true diff -Nru hub-2.7.0~ds1/.github/workflows/release.yml hub-2.14.2~ds1/.github/workflows/release.yml --- hub-2.7.0~ds1/.github/workflows/release.yml 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/.github/workflows/release.yml 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,31 @@ +name: Release +on: + push: + tags: 'v*' + +jobs: + release: + name: Publish release + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v1 + with: + go-version: "1.13" + + - name: Publish release script + run: script/publish-release + env: + GOFLAGS: -mod=vendor + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: mislav/bump-homebrew-formula-action@v1 + if: "!contains(github.ref, '-')" # skip prereleases + with: + formula-name: hub + env: + COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff -Nru hub-2.7.0~ds1/.gitignore hub-2.14.2~ds1/.gitignore --- hub-2.7.0~ds1/.gitignore 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/.gitignore 2020-03-05 17:48:23.000000000 +0000 @@ -3,8 +3,9 @@ /bin .bundle bundle/ +share/doc/* share/man/* -!share/man/man1/hub.1.ronn +!share/man/man1/hub.1.md tmp/ *.test target @@ -12,3 +13,4 @@ tags /site /hub +.vscode diff -Nru hub-2.7.0~ds1/go.mod hub-2.14.2~ds1/go.mod --- hub-2.7.0~ds1/go.mod 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/go.mod 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,21 @@ +module github.com/github/hub + +go 1.11 + +require ( + github.com/BurntSushi/toml v0.3.0 + github.com/atotto/clipboard v0.0.0-20171229224153-bc5958e1c833 + github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 + github.com/kballard/go-shellquote v0.0.0-20170619183022-cd60e84ee657 + github.com/kr/pretty v0.0.0-20160823170715-cfb55aafdaf3 // indirect + github.com/kr/text v0.0.0-20160504234017-7cafcd837844 // indirect + github.com/mattn/go-colorable v0.0.9 + github.com/mattn/go-isatty v0.0.3 + github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 + github.com/russross/blackfriday v0.0.0-20180526075726-670777b536d3 + github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 + golang.org/x/net v0.0.0-20191002035440-2ec189313ef0 + golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 // indirect + gopkg.in/yaml.v2 v2.0.0-20190319135612-7b8349ac747c +) diff -Nru hub-2.7.0~ds1/Gopkg.lock hub-2.14.2~ds1/Gopkg.lock --- hub-2.7.0~ds1/Gopkg.lock 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/Gopkg.lock 1970-01-01 00:00:00.000000000 +0000 @@ -1,90 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - name = "github.com/BurntSushi/toml" - packages = ["."] - revision = "b26d9c308763d68093482582cea63d69be07a0f0" - version = "v0.3.0" - -[[projects]] - branch = "master" - name = "github.com/atotto/clipboard" - packages = ["."] - revision = "bc5958e1c8339112fc3347a89f3c482f416a69d3" - -[[projects]] - branch = "master" - name = "github.com/bmizerany/assert" - packages = ["."] - revision = "b7ed37b82869576c289d7d97fb2bbd8b64a0cb28" - -[[projects]] - branch = "master" - name = "github.com/kballard/go-shellquote" - packages = ["."] - revision = "cd60e84ee657ff3dc51de0b4f55dd299a3e136f2" - -[[projects]] - branch = "master" - name = "github.com/kr/pretty" - packages = ["."] - revision = "cfb55aafdaf3ec08f0db22699ab822c50091b1c4" - -[[projects]] - branch = "master" - name = "github.com/kr/text" - packages = ["."] - revision = "7cafcd837844e784b526369c9bce262804aebc60" - -[[projects]] - name = "github.com/mattn/go-colorable" - packages = ["."] - revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" - version = "v0.0.9" - -[[projects]] - name = "github.com/mattn/go-isatty" - packages = ["."] - revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" - version = "v0.0.3" - -[[projects]] - branch = "master" - name = "github.com/mitchellh/go-homedir" - packages = ["."] - revision = "b8bc1bf767474819792c23f32d8286a45736f1c6" - -[[projects]] - name = "github.com/ogier/pflag" - packages = ["."] - revision = "32a05c62658bd1d7c7e75cbc8195de5d585fde0f" - version = "v0.0.1" - -[[projects]] - branch = "master" - name = "golang.org/x/crypto" - packages = ["ssh/terminal"] - revision = "1875d0a70c90e57f11972aefd42276df65e895b9" - -[[projects]] - branch = "master" - name = "golang.org/x/sys" - packages = [ - "unix", - "windows" - ] - revision = "ff2a66f350cefa5c93a634eadb5d25bb60c85a9c" - -[[projects]] - name = "gopkg.in/yaml.v2" - packages = ["."] - revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" - version = "v2.2.1" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - inputs-digest = "d72e76ebb777976d767ff7edb15fc2cfb345fb9e91017ad8ae2823da3e2fcd10" - solver-name = "gps-cdcl" - solver-version = 1 diff -Nru hub-2.7.0~ds1/Gopkg.toml hub-2.14.2~ds1/Gopkg.toml --- hub-2.7.0~ds1/Gopkg.toml 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/Gopkg.toml 1970-01-01 00:00:00.000000000 +0000 @@ -1,43 +0,0 @@ -[[constraint]] - name = "github.com/BurntSushi/toml" - version = "0.3.0" - -[[constraint]] - branch = "master" - name = "github.com/atotto/clipboard" - -[[constraint]] - branch = "master" - name = "github.com/bmizerany/assert" - -[[constraint]] - branch = "master" - name = "github.com/kballard/go-shellquote" - -[[constraint]] - name = "github.com/mattn/go-colorable" - version = "0.0.9" - -[[constraint]] - name = "github.com/mattn/go-isatty" - version = "0.0.3" - -[[constraint]] - branch = "master" - name = "github.com/mitchellh/go-homedir" - -[[constraint]] - name = "github.com/ogier/pflag" - version = "0.0.1" - -[[constraint]] - branch = "master" - name = "golang.org/x/crypto" - -[[constraint]] - branch = "v2" - name = "gopkg.in/yaml.v2" - -[prune] - go-tests = true - unused-packages = true diff -Nru hub-2.7.0~ds1/go.sum hub-2.14.2~ds1/go.sum --- hub-2.7.0~ds1/go.sum 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/go.sum 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,37 @@ +github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY= +github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/atotto/clipboard v0.0.0-20171229224153-bc5958e1c833 h1:h/E5ryZTJAtOY6T3K6u/JA1OURt0nk1C4fITywxOp4E= +github.com/atotto/clipboard v0.0.0-20171229224153-bc5958e1c833/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/kballard/go-shellquote v0.0.0-20170619183022-cd60e84ee657 h1:vE7J1m7cCpiRVEIr1B5ccDxRpbPsWT5JU3if2Di5nE4= +github.com/kballard/go-shellquote v0.0.0-20170619183022-cd60e84ee657/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kr/pretty v0.0.0-20160823170715-cfb55aafdaf3 h1:dhwb1Ev84SKKVBfLuhR4bw/29yYHzwtTyTLUWWnvYxI= +github.com/kr/pretty v0.0.0-20160823170715-cfb55aafdaf3/go.mod h1:Bvhd+E3laJ0AVkG0c9rmtZcnhV0HQ3+c3YxxqTvc/gA= +github.com/kr/text v0.0.0-20160504234017-7cafcd837844 h1:kpzneEBeC0dMewP3gr/fADv1OlblH9r1goWVwpOt3TU= +github.com/kr/text v0.0.0-20160504234017-7cafcd837844/go.mod h1:sjUstKUATFIcff4qlB53Kml0wQPtJVc/3fWrmuUmcfA= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 h1:eQox4Rh4ewJF+mqYPxCkmBAirRnPaHEB26UkNuPyjlk= +github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/russross/blackfriday v0.0.0-20180526075726-670777b536d3 h1:vZXiDtLzqEDYbeAt94qcQZ2H9SGHwbZiOFdsRT5rrng= +github.com/russross/blackfriday v0.0.0-20180526075726-670777b536d3/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +golang.org/x/crypto v0.0.0-20180127211104-1875d0a70c90 h1:DNyuYmiOz3AH2rGH1n4YsZUvxVhkeMvSs8s31jiWpm0= +golang.org/x/crypto v0.0.0-20180127211104-1875d0a70c90/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20191002035440-2ec189313ef0 h1:2mqDk8w/o6UmeUCu5Qiq2y7iMf6anbx+YA8d1JFoFrs= +golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 h1:T5DasATyLQfmbTpfEXx/IOL9vfjzW6up+ZDkmHvIf2s= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.0.0-20190319135612-7b8349ac747c h1:nsJYChHWxeFA6+48RmvBXEvQNanNyh933kZYWiu2HBE= +gopkg.in/yaml.v2 v2.0.0-20190319135612-7b8349ac747c/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff -Nru hub-2.7.0~ds1/main.go hub-2.14.2~ds1/main.go --- hub-2.7.0~ds1/main.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/main.go 2020-03-05 17:48:23.000000000 +0000 @@ -4,6 +4,8 @@ import ( "os" + "os/exec" + "syscall" "github.com/github/hub/commands" "github.com/github/hub/github" @@ -12,10 +14,30 @@ func main() { defer github.CaptureCrash() + err := commands.CmdRunner.Execute(os.Args) + exitCode := handleError(err) + os.Exit(exitCode) +} + +func handleError(err error) int { + if err == nil { + return 0 + } - err := commands.CmdRunner.Execute() - if !err.Ran { - ui.Errorln(err.Error()) + switch e := err.(type) { + case *exec.ExitError: + if status, ok := e.Sys().(syscall.WaitStatus); ok { + return status.ExitStatus() + } else { + return 1 + } + case *commands.ErrHelp: + ui.Println(err) + return 0 + default: + if errString := err.Error(); errString != "" { + ui.Errorln(err) + } + return 1 } - os.Exit(err.ExitCode) } diff -Nru hub-2.7.0~ds1/Makefile hub-2.14.2~ds1/Makefile --- hub-2.7.0~ds1/Makefile 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/Makefile 2020-03-05 17:48:23.000000000 +0000 @@ -1,15 +1,29 @@ -SOURCES = $(shell script/build files) +SOURCES = $(shell go list -f '{{range .GoFiles}}{{$$.Dir}}/{{.}}\ +{{end}}' ./...) +SOURCE_DATE_EPOCH ?= $(shell date +%s) +BUILD_DATE = $(shell date -u -d "@$(SOURCE_DATE_EPOCH)" '+%d %b %Y' 2>/dev/null || date -u -r "$(SOURCE_DATE_EPOCH)" '+%d %b %Y') +HUB_VERSION = $(shell bin/hub version | tail -1) + +export GOFLAGS := -mod=vendor +export GO111MODULE=on +unexport GOPATH + +export LDFLAGS := -extldflags '$(LDFLAGS)' +export GCFLAGS := all=-trimpath '$(PWD)' +export ASMFLAGS := all=-trimpath '$(PWD)' -MIN_COVERAGE = 89.4 +MIN_COVERAGE = 90.2 HELP_CMD = \ share/man/man1/hub-alias.1 \ + share/man/man1/hub-api.1 \ share/man/man1/hub-browse.1 \ share/man/man1/hub-ci-status.1 \ share/man/man1/hub-compare.1 \ share/man/man1/hub-create.1 \ share/man/man1/hub-delete.1 \ share/man/man1/hub-fork.1 \ + share/man/man1/hub-gist.1 \ share/man/man1/hub-pr.1 \ share/man/man1/hub-pull-request.1 \ share/man/man1/hub-release.1 \ @@ -37,6 +51,9 @@ bin/hub: $(SOURCES) script/build -o $@ +bin/md2roff: $(SOURCES) + go build -o $@ github.com/github/hub/md2roff-bin + test: go test ./... @@ -47,26 +64,31 @@ script/test endif -bin/ronn bin/cucumber: +bin/cucumber: script/bootstrap fmt: go fmt ./... -man-pages: $(HELP_ALL:=.ronn) $(HELP_ALL) $(HELP_ALL:=.txt) +man-pages: $(HELP_ALL:=.md) $(HELP_ALL) $(HELP_ALL:=.txt) -%.txt: %.ronn +%.txt: % groff -Wall -mtty-char -mandoc -Tutf8 -rLL=$(TEXT_WIDTH)n $< | col -b >$@ $(HELP_ALL): share/man/.man-pages.stamp -share/man/.man-pages.stamp: bin/ronn $(HELP_ALL:=.ronn) - bin/ronn --organization=GITHUB --manual="Hub Manual" share/man/man1/*.ronn +share/man/.man-pages.stamp: $(HELP_ALL:=.md) ./man-template.html bin/md2roff + bin/md2roff --manual="hub manual" \ + --date="$(BUILD_DATE)" --version="$(HUB_VERSION)" \ + --template=./man-template.html \ + share/man/man1/*.md + mkdir -p share/doc/hub-doc + mv share/man/*/*.html share/doc/hub-doc/ touch $@ -%.1.ronn: bin/hub - bin/hub help $(*F) --plain-text | script/format-ronn $(*F) $@ +%.1.md: bin/hub + bin/hub help $(*F) --plain-text >$@ -share/man/man1/hub.1.ronn: +share/man/man1/hub.1.md: true install: bin/hub man-pages diff -Nru hub-2.7.0~ds1/man-template.html hub-2.14.2~ds1/man-template.html --- hub-2.7.0~ds1/man-template.html 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/man-template.html 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,95 @@ + +{{.Name}}({{.Section}}) - {{.Title}} + + + + +
+
    +
  1. {{.Name}}({{.Section}})
  2. +
  3. {{.Manual}}
  4. +
  5. {{.Name}}({{.Section}})
  6. +
+
+ +
+

{{.Title}}

+ {{.Contents}} +
+ +
+
    +
  1. {{.Version}}
  2. +
  3. {{.Date}}
  4. +
  5. +
+
diff -Nru hub-2.7.0~ds1/md2roff/renderer.go hub-2.14.2~ds1/md2roff/renderer.go --- hub-2.7.0~ds1/md2roff/renderer.go 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/md2roff/renderer.go 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,223 @@ +package md2roff + +import ( + "bytes" + "fmt" + "io" + "regexp" + "strconv" + "strings" + + "github.com/russross/blackfriday" +) + +// https://github.com/russross/blackfriday/blob/v2/markdown.go +const ( + PARSER_EXTENSIONS = blackfriday.NoIntraEmphasis | + blackfriday.FencedCode | + blackfriday.SpaceHeadings | + blackfriday.AutoHeadingIDs | + blackfriday.DefinitionLists +) + +var ( + backslash = []byte{'\\'} + enterVar = []byte("") + closeVar = []byte("") + tilde = []byte(`\(ti`) + htmlEscape = regexp.MustCompile(`<([A-Za-z][A-Za-z0-9_-]*)>`) + roffEscape = regexp.MustCompile(`[&'\_-]`) + headingEscape = regexp.MustCompile(`["]`) + titleRe = regexp.MustCompile(`(?P[A-Za-z][A-Za-z0-9_-]+)\((?P\d)\) -- (?P.+)`) +) + +func escape(src []byte, re *regexp.Regexp) []byte { + return re.ReplaceAllFunc(src, func(c []byte) []byte { + return append(backslash, c...) + }) +} + +func roffText(src []byte) []byte { + return bytes.Replace(escape(src, roffEscape), []byte{'~'}, tilde, -1) +} + +type RoffRenderer struct { + Manual string + Version string + Date string + Title string + Name string + Section uint8 + + listWasTerm bool +} + +func (r *RoffRenderer) RenderHeader(buf io.Writer, ast *blackfriday.Node) { +} + +func (r *RoffRenderer) RenderFooter(buf io.Writer, ast *blackfriday.Node) { +} + +func (r *RoffRenderer) RenderNode(buf io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus { + if entering { + switch node.Type { + case blackfriday.Emph: + io.WriteString(buf, `\fI`) + case blackfriday.Strong: + io.WriteString(buf, `\fB`) + case blackfriday.Link: + io.WriteString(buf, `\[la]`) + case blackfriday.Code: + io.WriteString(buf, `\fB\fC`) + case blackfriday.Hardbreak: + io.WriteString(buf, "\n.br\n") + case blackfriday.Paragraph: + if node.Parent.Type != blackfriday.Item { + io.WriteString(buf, ".P\n") + } else if node.Parent.FirstChild != node { + io.WriteString(buf, ".sp\n") + if node.Prev.Type == blackfriday.List { + io.WriteString(buf, ".PP\n") + } + } + case blackfriday.CodeBlock: + io.WriteString(buf, ".PP\n.RS 4\n.nf\n") + case blackfriday.Item: + if node.ListFlags&blackfriday.ListTypeDefinition == 0 { + if node.Parent.ListData.Tight && node.Parent.FirstChild != node { + io.WriteString(buf, ".sp -1\n") + } + if node.Parent.ListData.Tight { + io.WriteString(buf, ".IP \\(bu 2.3\n") + } else { + io.WriteString(buf, ".IP \\(bu 4\n") + } + } else { + if node.ListFlags&blackfriday.ListTypeTerm != 0 { + io.WriteString(buf, ".PP\n") + } else { + io.WriteString(buf, ".RS 4\n") + } + } + case blackfriday.Heading: + r.renderHeading(buf, node) + return blackfriday.SkipChildren + } + } + + leaf := len(node.Literal) > 0 + if leaf { + if bytes.Equal(node.Literal, enterVar) { + io.WriteString(buf, `\fI`) + } else if bytes.Equal(node.Literal, closeVar) { + io.WriteString(buf, `\fP`) + } else { + buf.Write(roffText(node.Literal)) + } + } + + if !entering || leaf { + switch node.Type { + case blackfriday.Emph, + blackfriday.Strong: + io.WriteString(buf, `\fP`) + case blackfriday.Link: + io.WriteString(buf, `\[ra]`) + case blackfriday.Code: + io.WriteString(buf, `\fR`) + case blackfriday.CodeBlock: + io.WriteString(buf, ".fi\n.RE\n") + case blackfriday.HTMLSpan, + blackfriday.Del, + blackfriday.Image: + case blackfriday.List: + io.WriteString(buf, ".br\n") + case blackfriday.Item: + if node.ListFlags&blackfriday.ListTypeDefinition != 0 && + node.ListFlags&blackfriday.ListTypeTerm == 0 { + io.WriteString(buf, ".RE\n") + } + default: + if !leaf { + io.WriteString(buf, "\n") + } + } + } + + return blackfriday.GoToNext +} + +func textContent(node *blackfriday.Node) []byte { + var buf bytes.Buffer + node.Walk(func(n *blackfriday.Node, entering bool) blackfriday.WalkStatus { + if entering && len(n.Literal) > 0 { + buf.Write(n.Literal) + } + return blackfriday.GoToNext + }) + return buf.Bytes() +} + +func (r *RoffRenderer) renderHeading(buf io.Writer, node *blackfriday.Node) { + text := textContent(node) + switch node.HeadingData.Level { + case 1: + var name []byte + var num []byte + if match := titleRe.FindAllSubmatch(text, 1); match != nil { + name, num, text = match[0][1], match[0][2], match[0][3] + r.Name = string(name) + if sectionNum, err := strconv.Atoi(string(num)); err == nil { + r.Section = uint8(sectionNum) + } + r.Title = string(text) + } + fmt.Fprintf(buf, ".TH \"%s\" \"%s\" \"%s\" \"%s\" \"%s\"\n", + escape(name, headingEscape), + num, + escape([]byte(r.Date), headingEscape), + escape([]byte(r.Version), headingEscape), + escape([]byte(r.Manual), headingEscape), + ) + io.WriteString(buf, ".nh\n") // disable hyphenation + io.WriteString(buf, ".ad l\n") // disable justification + io.WriteString(buf, ".SH \"NAME\"\n") + fmt.Fprintf(buf, "%s \\- %s\n", + roffText(name), + roffText(text), + ) + case 2: + fmt.Fprintf(buf, ".SH \"%s\"\n", strings.ToUpper(string(escape(text, headingEscape)))) + case 3: + fmt.Fprintf(buf, ".SS \"%s\"\n", escape(text, headingEscape)) + } +} + +func sanitizeInput(src []byte) []byte { + return htmlEscape.ReplaceAllFunc(src, func(match []byte) []byte { + res := append(enterVar, match[1:len(match)-1]...) + return append(res, closeVar...) + }) +} + +type renderOption struct { + renderer blackfriday.Renderer + buffer io.Writer +} + +func Opt(buffer io.Writer, renderer blackfriday.Renderer) *renderOption { + return &renderOption{renderer, buffer} +} + +func Generate(src []byte, opts ...*renderOption) { + parser := blackfriday.New(blackfriday.WithExtensions(PARSER_EXTENSIONS)) + ast := parser.Parse(sanitizeInput(src)) + + for _, opt := range opts { + opt.renderer.RenderHeader(opt.buffer, ast) + ast.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus { + return opt.renderer.RenderNode(opt.buffer, node, entering) + }) + opt.renderer.RenderFooter(opt.buffer, ast) + } +} diff -Nru hub-2.7.0~ds1/md2roff-bin/cmd.go hub-2.14.2~ds1/md2roff-bin/cmd.go --- hub-2.7.0~ds1/md2roff-bin/cmd.go 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/md2roff-bin/cmd.go 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,164 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "path" + "regexp" + "strings" + "text/template" + + "github.com/github/hub/md2roff" + "github.com/github/hub/utils" + "github.com/russross/blackfriday" +) + +var ( + flagManual, + flagVersion, + flagTemplate, + flagDate string + + xRefRe = regexp.MustCompile(`\b(?P<name>[a-z][\w-]*)\((?P<section>\d)\)`) + + pageIndex map[string]bool +) + +func init() { + pageIndex = make(map[string]bool) +} + +type templateData struct { + Contents string + Manual string + Date string + Version string + Title string + Name string + Section uint8 +} + +func generateFromFile(mdFile string) error { + content, err := ioutil.ReadFile(mdFile) + if err != nil { + return fmt.Errorf("%s (%q)", err, mdFile) + } + + roffFile := strings.TrimSuffix(mdFile, ".md") + roffBuf, err := os.OpenFile(roffFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("%s (%q)", err, roffFile) + } + defer roffBuf.Close() + + htmlFile := strings.TrimSuffix(mdFile, ".md") + ".html" + htmlBuf, err := os.OpenFile(htmlFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("%s (%q)", err, htmlFile) + } + defer htmlBuf.Close() + + html := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{ + Flags: blackfriday.HTMLFlagsNone, + }) + roff := &md2roff.RoffRenderer{ + Manual: flagManual, + Version: flagVersion, + Date: flagDate, + } + + htmlGenBuf := &bytes.Buffer{} + var htmlWriteBuf io.Writer = htmlBuf + if flagTemplate != "" { + htmlWriteBuf = htmlGenBuf + } + + md2roff.Generate(content, + md2roff.Opt(roffBuf, roff), + md2roff.Opt(htmlWriteBuf, html), + ) + + if flagTemplate != "" { + htmlGenBytes, err := ioutil.ReadAll(htmlGenBuf) + if err != nil { + return fmt.Errorf("%s [%s]", err, "htmlGenBuf") + } + content := "" + if contentLines := strings.Split(string(htmlGenBytes), "\n"); len(contentLines) > 1 { + content = strings.Join(contentLines[1:], "\n") + } + + currentPage := fmt.Sprintf("%s(%d)", roff.Name, roff.Section) + content = xRefRe.ReplaceAllStringFunc(content, func(match string) string { + if match == currentPage { + return match + } else { + matches := xRefRe.FindAllStringSubmatch(match, 1) + fileName := fmt.Sprintf("%s.%s", matches[0][1], matches[0][2]) + if pageIndex[fileName] { + return fmt.Sprintf(`<a href="./%s.html">%s</a>`, fileName, match) + } else { + return match + } + } + }) + + tmplData := templateData{ + Manual: flagManual, + Date: flagDate, + Contents: content, + Title: roff.Title, + Section: roff.Section, + Name: roff.Name, + Version: flagVersion, + } + + templateFile, err := ioutil.ReadFile(flagTemplate) + if err != nil { + return fmt.Errorf("%s (%q)", err, flagTemplate) + } + tmpl, err := template.New("test").Parse(string(templateFile)) + if err != nil { + return err + } + err = tmpl.Execute(htmlBuf, tmplData) + if err != nil { + return err + } + } + + return nil +} + +func main() { + p := utils.NewArgsParserWithUsage(` + --manual NAME + --version STR + --template FILE + --date DATE + `) + files, err := p.Parse(os.Args[1:]) + if err != nil { + panic(err) + } + flagManual = p.Value("--manual") + flagVersion = p.Value("--version") + flagTemplate = p.Value("--template") + flagDate = p.Value("--date") + + for _, infile := range files { + name := path.Base(infile) + name = strings.TrimSuffix(name, ".md") + pageIndex[name] = true + } + + for _, infile := range files { + err := generateFromFile(infile) + if err != nil { + panic(err) + } + } +} diff -Nru hub-2.7.0~ds1/README.md hub-2.14.2~ds1/README.md --- hub-2.7.0~ds1/README.md 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/README.md 2020-03-05 17:48:23.000000000 +0000 @@ -1,98 +1,109 @@ -git + hub = github -================== - hub is a command line tool that wraps `git` in order to extend it with extra features and commands that make working with GitHub easier. +This repository and its issue tracker is **not for reporting problems with +GitHub.com** web interface. If you have a problem with GitHub itself, please +[contact Support](https://github.com/contact). + +Usage +----- + ``` sh $ hub clone rtomayko/tilt +#=> git clone git://github.com/rtomayko/tilt.git -# expands to: -$ git clone git://github.com/rtomayko/tilt.git +# if you prefer HTTPS to git/SSH protocols: +$ git config --global hub.protocol https +$ hub clone rtomayko/tilt +#=> git clone https://github.com/rtomayko/tilt.git ``` -hub is best aliased as `git`, so you can type `$ git <command>` in the shell and -get all the usual `hub` features. See "Aliasing" below. +See [usage examples](https://hub.github.com/#developer) or the [full reference +documentation](https://hub.github.com/hub.1.html) to see all available commands +and flags. + +hub can also be used to make shell scripts that [directly interact with the +GitHub API](https://hub.github.com/#scripting). -See [Usage documentation](https://hub.github.com/hub.1.html) for the list of all -commands and their arguments. +hub can be safely [aliased](#aliasing) as `git`, so you can type `$ git +<command>` in the shell and have it expanded with `hub` features. Installation ------------ -Dependencies: - -* **git 1.7.3** or newer - -#### Homebrew - -`hub` can be installed through [Homebrew](https://docs.brew.sh/Installation) on macOS: - -``` sh -$ brew install hub -$ hub version -git version 1.7.6 -hub version 2.2.3 -``` - -#### Windows - -`hub` can be installed through [Scoop](http://scoop.sh/) on Windows: - -``` sh -> scoop install hub -``` - -#### Fedora Linux +The `hub` executable has no dependencies, but since it was designed to wrap +`git`, it's recommended to have at least **git 1.7.3** or newer. -On Fedora you can install `hub` through DNF: +platform | manager | command to run +---------|---------|--------------- +macOS, Linux | [Homebrew](https://docs.brew.sh/Installation) | `brew install hub` +Windows | [Scoop](http://scoop.sh/) | `scoop install hub` +Windows | [Chocolatey](https://chocolatey.org/) | `choco install hub` +Fedora Linux | [DNF](https://fedoraproject.org/wiki/DNF) | `sudo dnf install hub` +Arch Linux | [pacman](https://wiki.archlinux.org/index.php/pacman) | `sudo pacman -S hub` +FreeBSD | [pkg(8)](http://man.freebsd.org/pkg/8) | `pkg install hub` +Debian | [apt(8)](https://manpages.debian.org/buster/apt/apt.8.en.html) | `sudo apt install hub` +Ubuntu | [Snap](https://snapcraft.io) | `snap install hub --classic` + +Packages other than Homebrew are community-maintained (thank you!) and they +are not guaranteed to match the [latest hub release][latest]. Check `hub +version` after installing a community package. -``` sh -$ sudo dnf install hub -$ hub version -git version 2.9.3 -hub version 2.2.9 -``` +#### Standalone -#### Arch Linux +`hub` can be easily installed as an executable. Download the [latest +binary][latest] for your system and put it anywhere in your executable path. -On Arch Linux you can install `hub` from official repository: +#### GitHub Actions -```sh -$ sudo pacman -S hub -``` +hub can be used for automation through [GitHub Actions][] workflows: +```yaml +steps: +- uses: actions/checkout@v2 + +- name: hub example + shell: bash + run: | + curl -fsSL https://github.com/github/hub/raw/master/script/get | bash -s 2.14.1 + bin/hub pr list # list pull requests in the current repo + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +``` + +Note that the default GITHUB_TOKEN will only work for API operations within _the +same repo that runs this workflow_. If you need to access or write to other +repositories, [generate a Personal Access Token][pat] with `repo` scope and add +it to your [repository secrets][]. + + +[github actions]: https://help.github.com/en/actions/automating-your-workflow-with-github-actions +[pat]: https://github.com/settings/tokens +[repository secrets]: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets -#### Standalone +#### Source -`hub` can be easily installed as an executable. Download the latest -[compiled binaries](https://github.com/github/hub/releases) and put it anywhere -in your executable path. +Prerequisites for building from source are: -#### Source +* `make` +* [Go 1.11+](https://golang.org/doc/install) -With your [GOPATH](https://github.com/golang/go/wiki/GOPATH) already set up: +Clone this repository and run `make install`: ```sh -mkdir -p "$GOPATH"/src/github.com/github git clone \ --config transfer.fsckobjects=false \ --config receive.fsckobjects=false \ --config fetch.fsckobjects=false \ - https://github.com/github/hub.git "$GOPATH"/src/github.com/github/hub -cd "$GOPATH"/src/github.com/github/hub + https://github.com/github/hub.git + +cd hub make install prefix=/usr/local ``` -Prerequisites for compilation are: - -* `make` -* [Go 1.8+](http://golang.org/doc/install) -* Ruby 1.9+ with Bundler - for generating man pages - Aliasing -------- -Using hub feels best when it's aliased as `git`. This is not dangerous; your +Some hub features feel best when it's aliased as `git`. This is not dangerous; your _normal git commands will all work_. hub merely adds some sugar. `hub alias` displays instructions for the current shell. With the `-s` flag, it @@ -130,18 +141,15 @@ ### Shell tab-completion -hub repository contains tab-completion scripts for bash, zsh and fish. +hub repository contains [tab-completion scripts](./etc) for bash, zsh and fish. These scripts complement existing completion scripts that ship with git. -[Installation instructions](etc) - -* [hub bash completion](https://github.com/github/hub/blob/master/etc/hub.bash_completion.sh) -* [hub zsh completion](https://github.com/github/hub/blob/master/etc/hub.zsh_completion) -* [hub fish completion](https://github.com/github/hub/blob/master/etc/hub.fish_completion) - Meta ---- -* Home: <https://github.com/github/hub> * Bugs: <https://github.com/github/hub/issues> * Authors: <https://github.com/github/hub/contributors> +* Our [Code of Conduct](https://github.com/github/hub/blob/master/CODE_OF_CONDUCT.md) + + +[latest]: https://github.com/github/hub/releases/latest diff -Nru hub-2.7.0~ds1/script/bootstrap hub-2.14.2~ds1/script/bootstrap --- hub-2.7.0~ds1/script/bootstrap 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/script/bootstrap 2020-03-05 17:48:23.000000000 +0000 @@ -3,26 +3,9 @@ STATUS=0 -if [ -n "$TRAVIS" ] && [ ! -x ~/bin/tmux ]; then - case "$TRAVIS_OS_NAME" in - linux ) cache_name="tmux-zsh.ubuntu" ;; - osx ) cache_name="tmux.osx" ;; - * ) - echo "unknown OS: $TRAVIS_OS_NAME" >&2 - exit 1 - ;; - esac - - curl -fsSL "https://${AMAZON_S3_BUCKET}.s3.amazonaws.com/${cache_name}.tgz" | tar -xz -C ~ -fi - { ruby --version - if [ -n "$TRAVIS" ]; then - script/cached-bundle install --deployment --jobs=3 --retry=3 --path bundle - else - bundle install --path bundle - fi - bundle binstub cucumber ronn --path bin + bundle install + bundle binstub cucumber --path bin } || { echo "You need Ruby 1.9 or higher and Bundler to run hub tests" >&2 STATUS=1 diff -Nru hub-2.7.0~ds1/script/build hub-2.14.2~ds1/script/build --- hub-2.7.0~ds1/script/build 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/script/build 2020-03-05 17:48:23.000000000 +0000 @@ -13,7 +13,11 @@ build_hub() { mkdir -p "$(dirname "$1")" - go build -ldflags "-X github.com/github/hub/version.Version=`./script/version`" -o "$1" + go build \ + -ldflags "-X github.com/github/hub/version.Version=`./script/version` $LDFLAGS" \ + -gcflags "$GCFLAGS" \ + -asmflags "$ASMFLAGS" \ + -o "$1" } [ $# -gt 0 ] || set -- -o "bin/hub${windows:+.exe}" diff -Nru hub-2.7.0~ds1/script/cached-bundle hub-2.14.2~ds1/script/cached-bundle --- hub-2.7.0~ds1/script/cached-bundle 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/script/cached-bundle 1970-01-01 00:00:00.000000000 +0000 @@ -1,48 +0,0 @@ -#!/usr/bin/env bash -# Usage: cached-bundle install --deployment -# -# After running `bundle`, caches the `bundle/` directory to S3. -# On the next run, restores the cached directory before running `bundle`. -# When `Gemfile.lock` changes, the cache gets rebuilt. -# -# Requirements: -# - Gemfile.lock -# - TRAVIS_REPO_SLUG -# - TRAVIS_RUBY_VERSION -# - AMAZON_S3_BUCKET -# - script/s3-put -# - bundle -# - curl -# -# Author: Mislav Marohnić - -set -e - -compute_md5() { - local output="$(openssl md5)" - echo "${output##* }" -} - -download() { - curl --tcp-nodelay -qsfL "$1" -o "$2" -} - -bundle_path="bundle" -gemfile_hash="$(compute_md5 <"${BUNDLE_GEMFILE:-Gemfile}.lock")" -cache_name="${TRAVIS_RUBY_VERSION}-${gemfile_hash}.${TRAVIS_OS_NAME}.tgz" -fetch_url="http://${AMAZON_S3_BUCKET}.s3.amazonaws.com/${TRAVIS_REPO_SLUG}/${cache_name}" -bundle_log="$(mktemp /tmp/bundle.XXXX)" - -if download "$fetch_url" "$cache_name"; then - echo "Reusing cached bundle ${cache_name}" - tar xzf "$cache_name" -fi - -set -o pipefail -bundle "$@" | tee "$bundle_log" - -if grep -q '^Installing' "$bundle_log" && [ -n "$AMAZON_SECRET_ACCESS_KEY" ]; then - echo "Caching \`${bundle_path}' to S3" - tar czf "$cache_name" "$bundle_path" - script/s3-put "$cache_name" "${AMAZON_S3_BUCKET}:${TRAVIS_REPO_SLUG}/${cache_name}" -fi diff -Nru hub-2.7.0~ds1/script/changelog hub-2.14.2~ds1/script/changelog --- hub-2.7.0~ds1/script/changelog 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/script/changelog 2020-03-05 17:48:23.000000000 +0000 @@ -5,17 +5,16 @@ # Show changes to runtime files between HEAD and previous release tag. set -e -head="${1:-HEAD}" +current_tag="${GITHUB_REF#refs/tags/}" +start_ref="HEAD" -for sha in `git rev-list -n 100 --first-parent "$head"^`; do - previous_tag="$(git tag -l --points-at "$sha" 'v*' 2>/dev/null || true)" - [ -z "$previous_tag" ] || break +# Find the previous release on the same branch, skipping prereleases if the +# current tag is a full release +previous_tag="" +while [[ -z $previous_tag || ( $previous_tag == *-* && $current_tag != *-* ) ]]; do + previous_tag="$(git describe --tags "$start_ref"^ --abbrev=0)" + start_ref="$previous_tag" done -if [ -z "$previous_tag" ]; then - echo "Couldn't detect previous version tag" >&2 - exit 1 -fi - -git log --no-merges --format='%C(auto,green)* %s%C(auto,reset)%n%w(0,2,2)%+b' \ - --reverse "${previous_tag}..${head}" -- `script/build files` +git log --first-parent --format='%C(auto,green)* %s%C(auto,reset)%n%w(0,2,2)%+b' \ + --reverse "${previous_tag}.." -- `script/build files` etc share diff -Nru hub-2.7.0~ds1/script/coverage hub-2.14.2~ds1/script/coverage --- hub-2.7.0~ds1/script/coverage 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/script/coverage 2020-03-05 17:48:23.000000000 +0000 @@ -2,7 +2,7 @@ set -e source_files() { - script/build files | grep -vE '^\./(coverage|fixtures)/' + script/build files | grep -vE '^\./(coverage|fixtures|version)/' } prepare() { diff -Nru hub-2.7.0~ds1/script/docker hub-2.14.2~ds1/script/docker --- hub-2.7.0~ds1/script/docker 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/script/docker 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,20 @@ +#!/bin/bash +# Usage: script/docker [<cucumber-args>] +set -e + +container=hub-test +workdir=/home/app/workdir + +docker build -t "$container" . + +docker run -it --rm -v "$PWD":"$workdir" -w "$workdir" "$container" \ + /bin/bash -c " +# Enables running WEBrick server (see local_server.rb) +# https://stackoverflow.com/a/45899937/11687 +cp /etc/hosts /tmp/hosts.new \ + && sed -i 's/::1\\tlocalhost/::1/' /tmp/hosts.new \ + && sudo cp -f /tmp/hosts.new /etc/hosts || exit 1 + +go test ./... +bundle exec cucumber $@ +" \ No newline at end of file diff -Nru hub-2.7.0~ds1/script/format-ronn hub-2.14.2~ds1/script/format-ronn --- hub-2.7.0~ds1/script/format-ronn 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/script/format-ronn 1970-01-01 00:00:00.000000000 +0000 @@ -1,62 +0,0 @@ -#!/bin/bash -# Usage: script/format-ronn <COMMAND> [<FILE>] -# -# Transform inline text of hub commands to ronn-format(7). -set -e - -AWK="$(type -p gawk awk | head -1)" - -para() { - "$AWK" -v wants=$2 " - BEGIN { para=1 } - /^\$/ { para++ } - { if (para $1 wants) print \$0 } - " -} - -trim() { - sed 's/^ \{1,\}//; s/ \{1,\}$//' -} - -format_shortdesc() { - tr $'\n' " " | trim -} - -format_synopsis() { - local cmd subcmd rest - sed 's/^Usage://' | while read -r cmd subcmd rest; do - printf '`%s %s` %s \n' "$cmd" "$subcmd" "$rest" - done -} - -format_rest() { - "$AWK" ' - /^#/ { - title=toupper(substr($0, length($1) + 2, length($0))) - sub(/:$/, "", title) - options=title == "OPTIONS" - print $1, title - next - } - options && /^ [^ ]/ { - printf " * %s:\n", substr($0, 3, length($0)) - next - } - { print $0 } - ' -} - -cmd="${1?}" -file="$2" -text="$(cat - | sed $'s/\t/ /g')" -[ -n "$text" ] || exit 1 -[ -z "$file" ] || exec 1<> "$file" - -echo "${cmd}(1) -- $(para == 2 <<<"$text" | format_shortdesc)" -echo "===" -echo -echo "## SYNOPSIS" -echo -para == 1 <<<"$text" | format_synopsis -echo -para '>=' 3 <<<"$text" | format_rest diff -Nru hub-2.7.0~ds1/script/get hub-2.14.2~ds1/script/get --- hub-2.7.0~ds1/script/get 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/script/get 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,54 @@ +#!/bin/bash +# Usage: curl -fsSL https://github.com/github/hub/raw/master/script/get | bash -s <HUB_VERSION> +# +# Downloads the hub binary into `bin/hub` within the current directory. + +set -e + +latest-version() { + curl -fsi https://github.com/github/hub/releases/latest | awk -F/ '/^Location:/ {print $(NF)}' +} + +HUB_VERSION="${1#v}" +if [ -z "$HUB_VERSION" ]; then + latest=$(latest-version) || true + [ -n "$latest" ] || latest="v2.14.1" + cat <<MSG >&2 +Error: You must specify a version of hub via the first argument. Example: + curl -L <script> | bash -s ${latest#v} +MSG + exit 1 +fi + +ARCH="amd64" +OS="$(uname -s | tr '[:upper:]' '[:lower:]')" +case "$OS" in +mingw* | msys* ) OS=windows ;; +esac + +download() { + case "$OS" in + windows ) + zip="${1%.tgz}.zip" + curl -fsSLO "$zip" + unzip "$(basename "$zip")" bin/hub.exe + rm -f "$(basename "$zip")" + ;; + darwin ) + curl -fsSL "$1" | tar xz --strip-components=1 '*/bin/hub' + ;; + * ) + curl -fsSL "$1" | tar xz --strip-components=1 --wildcards '*/bin/hub' + ;; + esac +} + +download "https://github.com/github/hub/releases/download/v$HUB_VERSION/hub-$OS-$ARCH-$HUB_VERSION.tgz" + +bin/hub version +if [ -z "$GITHUB_TOKEN" ]; then + cat <<MSG >&2 +Warning: We recommend supplying the GITHUB_TOKEN environment variable to avoid +being prompted for authentication. +MSG +fi diff -Nru hub-2.7.0~ds1/script/github-release hub-2.14.2~ds1/script/github-release --- hub-2.7.0~ds1/script/github-release 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/script/github-release 2020-03-05 17:48:23.000000000 +0000 @@ -1,27 +1,29 @@ #!/bin/bash -# Usage: script/cross-compile | script/github-release <name> <version> +# Usage: script/cross-compile | script/github-release <name> <tag> # # Takes in a list of asset filenames + labels via stdin and uploads them to the # corresponding release on GitHub. The release is created as a draft first if # missing and its body is the git changelog since the previous tagged release. set -e -export GITHUB_TOKEN="${GITHUB_OAUTH?}" - project_name="${1?}" -version="${2?}" -[[ $version == *-* ]] && pre=1 || pre= +tag_name="${2?}" +[[ $tag_name == *-* ]] && pre=1 || pre= assets=() while read -r filename label; do assets+=( -a "${filename}#${label}" ) done -if hub release --include-drafts | grep -q "^v${version}\$"; then - hub release edit "v${version}" "${assets[@]}" +notes="$(git tag --list "$tag_name" --format='%(contents:subject)%0a%0a%(contents:body)')" + +if hub release --include-drafts | grep -q "^${tag_name}\$"; then + hub release edit "$tag_name" -m "" "${assets[@]}" +elif [ $(wc -l <<<"$notes") -gt 1 ]; then + hub release create ${pre:+--prerelease} -F - "$tag_name" "${assets[@]}" <<<"$notes" else - { echo "${project_name} ${version}" + { echo "${project_name} ${tag_name#v}" echo script/changelog - } | hub release create --draft ${pre:+--prerelease} -F - "v${version}" "${assets[@]}" + } | hub release create --draft ${pre:+--prerelease} -F - "$tag_name" "${assets[@]}" fi diff -Nru hub-2.7.0~ds1/script/install.sh hub-2.14.2~ds1/script/install.sh --- hub-2.7.0~ds1/script/install.sh 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/script/install.sh 2020-03-05 17:48:23.000000000 +0000 @@ -16,7 +16,7 @@ prefix="${PREFIX:-$prefix}" prefix="${prefix:-/usr/local}" -for src in bin/hub share/man/*/*.1 share/vim/vimfiles/*/*.vim; do +for src in bin/hub share/man/*/*.1 share/doc/*/*.html share/vim/vimfiles/*/*.vim; do dest="${DESTDIR}${prefix}/${src}" mkdir -p "${dest%/*}" [[ $src == share/* ]] && mode="644" || mode=755 diff -Nru hub-2.7.0~ds1/script/package hub-2.14.2~ds1/script/package --- hub-2.7.0~ds1/script/package 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/script/package 2020-03-05 17:48:23.000000000 +0000 @@ -38,13 +38,14 @@ if [ "$os" = "windows" ]; then crlf README.md "${tmpdir}/README.txt" crlf LICENSE "${tmpdir}/LICENSE.txt" - mkdir "${tmpdir}/help" - for man in share/man/*/*.html; do crlf "$man" "${tmpdir}/help/${man##*/}"; done + for man in share/doc/*/*.html; do + mkdir -p "${tmpdir}/${man%/*}" + cp "$man" "${tmpdir}/${man}" + done crlf script/install.bat "${tmpdir}/install.bat" else cp -R README.md LICENSE etc share "$tmpdir" - rm -rf "${tmpdir}/share/man/"*/*.html - rm -rf "${tmpdir}/share/man/"*/*.ronn + rm -rf "${tmpdir}/share/man/"*/*.md cp script/install.sh "${tmpdir}/install" chmod +x "${tmpdir}/install" fi diff -Nru hub-2.7.0~ds1/script/publish-release hub-2.14.2~ds1/script/publish-release --- hub-2.7.0~ds1/script/publish-release 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/script/publish-release 2020-03-05 17:48:23.000000000 +0000 @@ -1,41 +1,41 @@ #!/usr/bin/env bash set -e -# Asserts that this build runs in the context of the Go version that appears -# first in build configuration. -latest_go_version() { - local go_version="$(grep '^go:' -A1 .travis.yml | tail -1)" - go_version="${go_version#* - }" - go_version="${go_version%.x}" - [ -n "$go_version" ] && [[ "$(go version)" == *" go${go_version}".* ]] -} - publish_documentation() { + local version="$1" local doc_dir="site" local doc_branch="gh-pages" - local remote_url="$(git config remote.origin.url)" git fetch origin "${doc_branch}:${doc_branch}" git worktree add "$doc_dir" "$doc_branch" pushd "$doc_dir" git rm hub*.html >/dev/null - cp ../share/man/*/*.html . + cp ../share/doc/*/*.html . + + local tracking="$(sed -n '/googletagmanager/,/^<\/script/p' index.html)" + local man_page + for man_page in hub*.html; do cat <<<"$tracking" >>"$man_page"; done + git add hub*.html - GIT_COMMITTER_NAME='Travis CI' GIT_COMMITTER_EMAIL='travis@travis-ci.org' \ + GIT_COMMITTER_NAME='GitHub Actions' GIT_COMMITTER_EMAIL='mislav+actions@github.com' \ GIT_AUTHOR_NAME='Mislav Marohnić' GIT_AUTHOR_EMAIL='mislav@github.com' \ - git commit -m "Update documentation for $TRAVIS_TAG" - - git push "https://${GITHUB_OAUTH}@${remote_url#https://}" HEAD + git commit -m "Update documentation for $version" + git push origin HEAD popd } -if [[ $TRAVIS_TAG == v* ]] && [ "$TRAVIS_OS_NAME" = "linux" ] && latest_go_version && [ -n "$GITHUB_OAUTH" ]; then - version="${TRAVIS_TAG#v}" - make man-pages - script/cross-compile "$version" | \ - PATH="bin:$PATH" script/github-release hub "$version" +in_default_branch() { + git fetch origin master --depth 10 + git merge-base --is-ancestor "$1" FETCH_HEAD +} + +tag_name="${GITHUB_REF#refs/tags/}" +make man-pages +script/cross-compile "${tag_name#v}" | \ + PATH="bin:$PATH" script/github-release hub "$tag_name" - publish_documentation +if [[ $tag_name != *-* ]] && in_default_branch "$tag_name"; then + publish_documentation "$tag_name" fi diff -Nru hub-2.7.0~ds1/script/ruby-test hub-2.14.2~ds1/script/ruby-test --- hub-2.7.0~ds1/script/ruby-test 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/script/ruby-test 2020-03-05 17:48:23.000000000 +0000 @@ -1,27 +1,7 @@ #!/usr/bin/env bash set -e -STATUS=0 -TMPDIR="${TMPDIR:-/tmp}" -warnings="${TMPDIR%/}/gh-warnings.$$" - -run() { - # Save warnings on stderr to a separate file - RUBYOPT="$RUBYOPT -w" bundle exec "$@" \ - 2> >(tee >(grep 'warning:' >>"$warnings") | grep -v 'warning:') || STATUS=$? -} - -check_warnings() { - # Display Ruby warnings from this project's source files. Abort if any were found. - num="$(grep -F "$PWD" "$warnings" | grep -v "${PWD}/bundle" | sort | uniq -c | sort -rn | tee /dev/stderr | wc -l)" - rm -f "$warnings" - if [ "$num" -gt 0 ]; then - echo "FAILED: this test suite doesn't tolerate Ruby syntax warnings!" >&2 - exit 1 - fi -} - -if [ -z "$TRAVIS" ] && tmux -V; then +if [ -z "$GITHUB_ACTIONS" ] && tmux -V; then if [ -n "$CI" ]; then git --version bash --version | head -1 @@ -34,7 +14,4 @@ profile="default" fi -run cucumber -p "$profile" "$@" -check_warnings - -exit $STATUS +bin/cucumber -p "$profile" "$@" diff -Nru hub-2.7.0~ds1/script/s3-put hub-2.14.2~ds1/script/s3-put --- hub-2.7.0~ds1/script/s3-put 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/script/s3-put 1970-01-01 00:00:00.000000000 +0000 @@ -1,71 +0,0 @@ -#!/usr/bin/env bash -# Usage: s3-put <FILE> <S3_BUCKET>[:<PATH>] [<CONTENT_TYPE>] -# -# Uploads a file to the Amazon S3 service. -# Outputs the URL for the newly uploaded file. -# -# Requirements: -# - AMAZON_ACCESS_KEY_ID -# - AMAZON_SECRET_ACCESS_KEY -# - openssl -# - curl -# -# Author: Mislav Marohnić - -set -e - -authorization() { - local signature="$(string_to_sign | hmac_sha1 | base64)" - echo "AWS ${AMAZON_ACCESS_KEY_ID?}:${signature}" -} - -hmac_sha1() { - openssl dgst -binary -sha1 -hmac "${AMAZON_SECRET_ACCESS_KEY?}" -} - -base64() { - openssl enc -base64 -} - -bin_md5() { - openssl dgst -binary -md5 -} - -string_to_sign() { - echo "$http_method" - echo "$content_md5" - echo "$content_type" - echo "$date" - echo "x-amz-acl:$acl" - printf "/$bucket/$remote_path" -} - -date_string() { - LC_TIME=C date "+%a, %d %h %Y %T %z" -} - -file="$1" -bucket="${2%%:*}" -remote_path="${2#*:}" -content_type="$3" - -if [ -z "$remote_path" ] || [ "$remote_path" = "$bucket" ]; then - remote_path="${file##*/}" -fi - -http_method=PUT -acl="public-read" -content_md5="$(bin_md5 < "$file" | base64)" -date="$(date_string)" - -url="https://$bucket.s3.amazonaws.com/$remote_path" - -curl -qsSf -T "$file" \ - -H "Authorization: $(authorization)" \ - -H "x-amz-acl: $acl" \ - -H "Date: $date" \ - -H "Content-MD5: $content_md5" \ - -H "Content-Type: $content_type" \ - "$url" - -echo "$url" diff -Nru hub-2.7.0~ds1/script/tag-release hub-2.14.2~ds1/script/tag-release --- hub-2.7.0~ds1/script/tag-release 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/script/tag-release 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,24 @@ +#!/bin/bash +set -e + +version_file="version/version.go" + +if git diff --exit-code >/dev/null -- "$version_file"; then + echo "Update the version in $version_file and try again." >&2 + exit 1 +fi + +version="$(grep -w 'Version =' "$version_file" | cut -d'"' -f2)" + +git commit -m "hub $version" -- "$version_file" + +notes_file="$(mktemp)" +{ echo "hub $version" + echo + GITHUB_REF="refs/tags/v$version" script/changelog +} >"$notes_file" +trap "rm -f '$notes_file'" EXIT + +git tag "v${version}" -F "$notes_file" --edit + +git push origin HEAD "v${version}" diff -Nru hub-2.7.0~ds1/script/test hub-2.14.2~ds1/script/test --- hub-2.7.0~ds1/script/test 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/script/test 2020-03-05 17:48:23.000000000 +0000 @@ -32,8 +32,8 @@ trap "exit 1" INT check_formatting() { - [[ "$(go version)" != *" go1.8."* ]] || return 0 - make fmt >/dev/null + gofmt -l -w . >/dev/null + git checkout -- vendor if ! git diff -U1 --exit-code; then echo echo "Some go code was not formatted properly." >&2 @@ -43,12 +43,15 @@ } install_test() { - touch share/man/man1/hub.1 + mkdir -p share/doc/hub-doc + touch share/man/man1/hub.1 share/doc/hub-doc/hub.1.html DESTDIR="$PWD/tmp/destdir" prefix=/my/prefix bash < script/install.sh test -x tmp/destdir/my/prefix/bin/hub test -e tmp/destdir/my/prefix/share/man/man1/hub.1 test ! -x tmp/destdir/my/prefix/share/man/man1/hub.1 - rm share/man/man1/hub.1 + test -e tmp/destdir/my/prefix/share/doc/hub-doc/hub.1.html + test ! -x tmp/destdir/my/prefix/share/doc/hub-doc/hub.1.html + rm share/man/man1/hub.1 share/doc/hub-doc/hub.1.html } [ -z "$HUB_COVERAGE" ] || script/coverage prepare diff -Nru hub-2.7.0~ds1/script/version hub-2.14.2~ds1/script/version --- hub-2.7.0~ds1/script/version 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/script/version 2020-03-05 17:48:23.000000000 +0000 @@ -2,10 +2,16 @@ # Displays hub's release version set -e +if [ -n "$GITHUB_REF" ]; then + echo "${GITHUB_REF#refs/tags/v}" + exit +fi + +export GIT_CEILING_DIRECTORIES=${PWD%/*} version="$(git describe --tags HEAD 2>/dev/null || true)" if [ -z "$version" ]; then - version="$(grep Version version/version.go | head -1 | cut -d '"' -f2)" + version="$(grep 'Version =' version/version.go | head -1 | cut -d '"' -f2)" sha="$(git rev-parse --short HEAD 2>/dev/null || true)" [ -z "$sha" ] || version="${version}-g${sha}" fi diff -Nru hub-2.7.0~ds1/share/man/man1/hub.1.md hub-2.14.2~ds1/share/man/man1/hub.1.md --- hub-2.7.0~ds1/share/man/man1/hub.1.md 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/share/man/man1/hub.1.md 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,212 @@ +hub(1) -- make git easier with GitHub +===================================== + +## Synopsis + +`hub` [--noop] <COMMAND> [<OPTIONS>] +`hub alias` [-s] [<SHELL>] +`hub help` hub-<COMMAND> + +## Description + +Hub is a tool that wraps git in order to extend it with extra functionality that +makes it better when working with GitHub. + +## Commands + +Available commands are split into two groups: those that are already present in +git but that are extended through hub, and custom ones that hub provides. + +### Extended git commands + +hub-am(1) +: Replicate commits from a GitHub pull request locally. + +hub-apply(1) +: Download a patch from GitHub and apply it locally. + +hub-checkout(1) +: Check out the head of a pull request as a local branch. + +hub-cherry-pick(1) +: Cherry-pick a commit from a fork on GitHub. + +hub-clone(1) +: Clone a repository from GitHub. + +hub-fetch(1) +: Add missing remotes prior to performing git fetch. + +hub-init(1) +: Initialize a git repository and add a remote pointing to GitHub. + +hub-merge(1) +: Merge a pull request locally with a message like the GitHub Merge Button. + +hub-push(1) +: Push a git branch to each of the listed remotes. + +hub-remote(1) +: Add a git remote for a GitHub repository. + +hub-submodule(1) +: Add a git submodule for a GitHub repository. + +### New commands provided by hub + +hub-alias(1) +: Show shell instructions for wrapping git. + +hub-api(1) +: Low-level GitHub API request interface. + +hub-browse(1) +: Open a GitHub repository in a web browser. + +hub-ci-status(1) +: Display status of GitHub checks for a commit. + +hub-compare(1) +: Open a GitHub compare page in a web browser. + +hub-create(1) +: Create a new repository on GitHub and add a git remote for it. + +hub-delete(1) +: Delete a repository on GitHub. + +hub-fork(1) +: Fork the current repository on GitHub and add a git remote for it. + +hub-gist(1) +: Create and print GitHub Gists. + +hub-pull-request(1) +: Create a GitHub Pull Request. + +hub-pr(1) +: Manage GitHub Pull Requests for the current repository. + +hub-issue(1) +: Manage GitHub Issues for the current repository. + +hub-release(1) +: Manage GitHub Releases for the current repository. + +hub-sync(1) +: Fetch git objects from upstream and update local branches. + +## Conventions + +Most hub commands are supposed to be run in a context of an existing local git +repository. Hub will automatically detect the GitHub repository that the current +working directory belongs to by scanning its git remotes. + +In case there are multiple git remotes that are all pointing to GitHub, hub +assumes that the main one is named "upstream", "github", or "origin", in that +order of preference. + +When working with forks, it's recommended that the git remote for your own fork +is named "origin" and that the git remote for the upstream repository is named +"upstream". See <https://help.github.com/articles/configuring-a-remote-for-a-fork/> + +The default branch (usually "master") for the current repository is detected +like so: + + git symbolic-ref refs/remotes/origin/HEAD + +where <origin> is the name of the git remote for the upstream repository. + +The destination where the currently checked out branch is considered to be +pushed to depends on the `git config push.default` setting. If the value is +"upstream" or "tracking", the tracking information for a branch is read like so: + + git rev-parse --symbolic-full-name BRANCH@{upstream} + +Otherwise, hub scans git remotes to find the first one for which +`refs/remotes/REMOTE/BRANCH` exists. The "origin", "github", and "upstream" +remotes are searched last because hub assumes that it's more likely that the +current branch is pushed to your fork rather than to the canonical repo. + +## Configuration + +### GitHub OAuth authentication + +Hub will prompt for GitHub username & password the first time it needs to access +the API and exchange it for an OAuth token, which it saves in `~/.config/hub`. + +To avoid being prompted, use `GITHUB_USER` and `GITHUB_PASSWORD` environment +variables. + +Alternatively, you may provide `GITHUB_TOKEN`, an access token with +**repo** permissions. This will not be written to `~/.config/hub`. + +### HTTPS instead of git protocol + +If you prefer the HTTPS protocol for git operations, you can configure hub to +generate all URLs with `https:` instead of `git:` or `ssh:`: + + $ git config --global hub.protocol https + +This will affect `clone`, `fork`, `remote add` and other hub commands that +expand shorthand references to GitHub repo URLs. + +### GitHub Enterprise + +By default, hub will only work with repositories that have remotes which +point to `github.com`. GitHub Enterprise hosts need to be whitelisted to +configure hub to treat such remotes same as github.com: + + $ git config --global --add hub.host MY.GIT.ORG + +The default host for commands like `init` and `clone` is still `github.com`, but +this can be affected with the `GITHUB_HOST` environment variable: + + $ GITHUB_HOST=my.git.org git clone myproject + +### Environment variables + +`HUB_VERBOSE` +: If this environment variable is set, verbose logging will be printed to + stderr. + +`HUB_CONFIG` +: The file path where hub configuration is read from and stored. If + `XDG_CONFIG_HOME` is present, the default is `$XDG_CONFIG_HOME/hub`; + otherwise it's `$HOME/.config/hub`. The configuration file is also + searched for in `XDG_CONFIG_DIRS` per XDG Base Directory Specification. + +`HUB_PROTOCOL` +: One of "https", "ssh", or "git" as preferred protocol for git clone/push. + +`GITHUB_HOST` +: The GitHub hostname to default to instead of "github.com". + +`GITHUB_TOKEN` +: OAuth token to use for GitHub API requests. + +`GITHUB_USER` +: The GitHub username of the actor of GitHub API operations. + +`GITHUB_PASSWORD` +: The GitHub password used to exchange user credentials for an OAuth token + that gets stored in hub configuration. If not set, it may be interactively + prompted for on first run. + +`GITHUB_REPOSITORY` +: A value in "OWNER/REPO" format that specifies the repository that API + operations should be performed against. Currently only used to infer the + default value of `GITHUB_USER` for API requests. + +## Bugs + +<https://github.com/github/hub/issues> + +## Authors + +<https://github.com/github/hub/contributors> + +## See also + +git(1), git-clone(1), git-remote(1), git-init(1), +<https://github.com/github/hub> diff -Nru hub-2.7.0~ds1/share/man/man1/hub.1.ronn hub-2.14.2~ds1/share/man/man1/hub.1.ronn --- hub-2.7.0~ds1/share/man/man1/hub.1.ronn 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/share/man/man1/hub.1.ronn 1970-01-01 00:00:00.000000000 +0000 @@ -1,186 +0,0 @@ -hub(1) -- make git easier with GitHub -===================================== - -## SYNOPSIS - -`hub` [`--noop`] <COMMAND> [<OPTIONS>] -`hub alias` [`-s`] [<SHELL>] -`hub help` hub-<COMMAND> - -## DESCRIPTION - -Hub is a tool that wraps git in order to extend it with extra functionality that -makes it better when working with GitHub. - -## COMMANDS - -Available commands are split into two groups: those that are already present in -git but that are extended through hub, and custom ones that hub provides. - -### Extended git commands - * hub-am(1): - Replicate commits from a GitHub pull request locally. - - * hub-apply(1): - Download a patch from GitHub and apply it locally. - - * hub-checkout(1): - Check out the head of a pull request as a local branch. - - * hub-cherry-pick(1): - Cherry-pick a commit from a fork on GitHub. - - * hub-clone(1): - Clone a repository from GitHub. - - * hub-fetch(1): - Add missing remotes prior to performing git fetch. - - * hub-init(1): - Initialize a git repository and add a remote pointing to GitHub. - - * hub-merge(1): - Merge a pull request locally with a message like the GitHub Merge Button. - - * hub-push(1): - Push a git branch to each of the listed remotes. - - * hub-remote(1): - Add a git remote for a GitHub repository. - - * hub-submodule(1): - Add a git submodule for a GitHub repository. - -### New commands provided by hub - * hub-alias(1): - Show shell instructions for wrapping git. - - * hub-browse(1): - Open a GitHub repository in a web browser. - - * hub-ci-status(1): - Display GitHub Status information for a commit. - - * hub-compare(1): - Open a GitHub compare page in a web browser. - - * hub-create(1): - Create a new repository on GitHub and add a git remote for it. - - * hub-delete(1): - Delete a repository on GitHub. - - * hub-fork(1): - Fork the current project on GitHub and add a git remote for it. - - * hub-pull-request(1): - Create a GitHub pull request. - - * hub-pr(1): - List and checkout GitHub pull requests. - - * hub-issue(1): - List and create GitHub issues. - - * hub-release(1): - List and create GitHub releases. - - * hub-sync(1): - Fetch from upstream and update local branches. - -## CONVENTIONS - -Most hub commands are supposed to be run in a context of an existing local git -repository. Hub will automatically detect the GitHub repository the current -project belongs to by scanning its git remotes. - -In case there are multiple git remotes that are all pointing to GitHub, hub -assumes that the main one is named "upstream", "github", or "origin", in that -order of preference. - -When working with forks, it's recommended that the git remote for your own fork -is named "origin" and that the git remote for the upstream repository is named -"upstream". See <https://help.github.com/articles/configuring-a-remote-for-a-fork/> - -The default branch (usually "master") for the project is detected like so: - - git symbolic-ref refs/remotes/origin/HEAD - -where <origin> is the name of the git remote for the upstream repository. - -The destination where the currently checked out branch is considered to be -pushed to depends on the `git config push.default` setting. If the value is -"upstream" or "tracking", the tracking information for a branch is read like so: - - git rev-parse --symbolic-full-name BRANCH@{upstream} - -Otherwise, hub scans git remotes to find the first one for which -`refs/remotes/REMOTE/BRANCH` exists. The "origin", "github", and "upstream" -remotes are searched last because hub assumes that it's more likely that the -current branch is pushed to your fork rather than to the canonical repo. - -## CONFIGURATION - -### GitHub OAuth authentication - -Hub will prompt for GitHub username & password the first time it needs to access -the API and exchange it for an OAuth token, which it saves in `~/.config/hub`. - -To avoid being prompted, use `GITHUB_USER` and `GITHUB_PASSWORD` environment -variables. - -Alternatively, you may provide `GITHUB_TOKEN`, an access token with -**repo** permissions. This will not be written to `~/.config/hub`. - -### HTTPS instead of git protocol - -If you prefer the HTTPS protocol for git operations, you can configure hub to -generate all URLs with `https:` instead of `git:` or `ssh:`: - - $ git config --global hub.protocol https - -This will affect `clone`, `fork`, `remote add` and other hub commands that -expand shorthand references to GitHub repo URLs. - -### GitHub Enterprise - -By default, hub will only work with repositories that have remotes which -point to `github.com`. GitHub Enterprise hosts need to be whitelisted to -configure hub to treat such remotes same as github.com: - - $ git config --global --add hub.host MY.GIT.ORG - -The default host for commands like `init` and `clone` is still `github.com`, but -this can be affected with the `GITHUB_HOST` environment variable: - - $ GITHUB_HOST=my.git.org git clone myproject - -### Environment variables - - * `HUB_VERBOSE`: - Enable verbose output from hub commands. - - * `HUB_CONFIG`: - The file path where hub configuration is read from and stored. If - `XDG_CONFIG_HOME` is present, the default is `$XDG_CONFIG_HOME/hub`; - otherwise it's `$HOME/.config/hub`. The configuration file is also - searched for in `XDG_CONFIG_DIRS` per XDG Base Directory Specification. - - * `HUB_PROTOCOL`: - Use one of "https|ssh|git" as preferred protocol for git clone/push. - - * `GITHUB_TOKEN`: - OAuth token to use for GitHub API requests. - -## BUGS - -<https://github.com/github/hub/issues> - -## AUTHORS - -<https://github.com/github/hub/contributors> - -## SEE ALSO - -git(1), git-clone(1), git-remote(1), git-init(1), -<https://github.com/github/hub> diff -Nru hub-2.7.0~ds1/.travis.yml hub-2.14.2~ds1/.travis.yml --- hub-2.7.0~ds1/.travis.yml 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/.travis.yml 1970-01-01 00:00:00.000000000 +0000 @@ -1,20 +0,0 @@ -sudo: false -before_install: - - export BUNDLE_GEMFILE=$PWD/Gemfile - - export TRAVIS_RUBY_VERSION="$(ruby -e 'puts RUBY_VERSION')-travis" - - export PATH=~/bin:"$PATH" -language: go -go: - - 1.11.x - - 1.10.x - - 1.9.x - - 1.8.x -script: make test-all -install: script/bootstrap -after_success: script/publish-release -env: - global: - - AMAZON_S3_BUCKET=ci-cache - - AMAZON_ACCESS_KEY_ID=AKIAJQCVTDEWQHRPBPGQ - - secure: "XAZv5xyNjWt7F9hG0MZhDANVJ5h/ajEZvfJOEIZRQlE3X5x6oVgI8blLh/MmlRSF0kIyLckcn6t2ccjSOvwN2hca5bwZSjIqoKbJyNe2cmkxfi2432vEOu3Ve6PT5hZWX4R5RgT+xWyMjIJcdF3gUMP7ErXl64aEncBzeW6OoXM=" - - secure: "eroPaeI0ohBaUjuc/y22VgyN+GDHeWXPIMAZTvSkNZfwL+Oxy861aeawi0zQeduEYon3fSfMBbOxJbrA+6IMU0W+DlR7TTBJ9dbGmeTFCu6ypJRJJtaE5/Kn9PwKjyG6XiPR/YR6818Jiv6yVCLQspjFbhCuKeFoHcu7RAmazKE=" diff -Nru hub-2.7.0~ds1/ui/format.go hub-2.14.2~ds1/ui/format.go --- hub-2.7.0~ds1/ui/format.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/ui/format.go 2020-03-05 17:48:23.000000000 +0000 @@ -254,9 +254,9 @@ switch p.truncing { case truncRight: - return ".." + s[len(s)-numLeft:len(s)] + return ".." + s[len(s)-numLeft:] case truncMiddle: - return s[:numLeft/2] + ".." + s[len(s)-(numLeft+1)/2:len(s)] + return s[:numLeft/2] + ".." + s[len(s)-(numLeft+1)/2:] } // Trunc left by default. diff -Nru hub-2.7.0~ds1/utils/args_parser.go hub-2.14.2~ds1/utils/args_parser.go --- hub-2.7.0~ds1/utils/args_parser.go 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/utils/args_parser.go 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,215 @@ +package utils + +import ( + "fmt" + "regexp" + "strconv" + "strings" +) + +type argsFlag struct { + expectsValue bool + values []string +} + +func (f *argsFlag) addValue(v string) { + f.values = append(f.values, v) +} + +func (f *argsFlag) lastValue() string { + l := len(f.values) + if l > 0 { + return f.values[l-1] + } else { + return "" + } +} + +func (f *argsFlag) reset() { + if len(f.values) > 0 { + f.values = []string{} + } +} + +type ArgsParser struct { + flagMap map[string]*argsFlag + flagAliases map[string]string + PositionalIndices []int + HasTerminated bool +} + +func (p *ArgsParser) Parse(args []string) ([]string, error) { + var flagName string + var flagValue string + var hasFlagValue bool + var i int + var arg string + + p.HasTerminated = false + for _, f := range p.flagMap { + f.reset() + } + if len(p.PositionalIndices) > 0 { + p.PositionalIndices = []int{} + } + + positional := []string{} + var parseError error + logError := func(f string, p ...interface{}) { + if parseError == nil { + parseError = fmt.Errorf(f, p...) + } + } + + acknowledgeFlag := func() bool { + canonicalFlagName := flagName + if n, found := p.flagAliases[flagName]; found { + canonicalFlagName = n + } + f := p.flagMap[canonicalFlagName] + if f == nil { + if len(flagName) == 2 { + logError("unknown shorthand flag: '%s' in %s", flagName[1:], arg) + } else { + logError("unknown flag: '%s'", flagName) + } + return true + } + if f.expectsValue { + if !hasFlagValue { + i++ + if i < len(args) { + flagValue = args[i] + } else { + logError("no value given for '%s'", flagName) + return true + } + } + } else if hasFlagValue && len(flagName) <= 2 { + flagValue = "" + } + f.addValue(flagValue) + return f.expectsValue + } + + for i = 0; i < len(args); i++ { + arg = args[i] + + if p.HasTerminated || len(arg) == 0 || arg == "-" { + } else if arg == "--" { + if !p.HasTerminated { + p.HasTerminated = true + continue + } + } else if strings.HasPrefix(arg, "--") { + flagName = arg + eq := strings.IndexByte(arg, '=') + hasFlagValue = eq >= 0 + if hasFlagValue { + flagName = arg[:eq] + flagValue = arg[eq+1:] + } + acknowledgeFlag() + continue + } else if arg[0] == '-' { + for j := 1; j < len(arg); j++ { + flagName = "-" + arg[j:j+1] + flagValue = "" + hasFlagValue = j+1 < len(arg) + if hasFlagValue { + flagValue = arg[j+1:] + } + if acknowledgeFlag() { + break + } + } + continue + } + + p.PositionalIndices = append(p.PositionalIndices, i) + positional = append(positional, arg) + } + + return positional, parseError +} + +func (p *ArgsParser) RegisterValue(name string, aliases ...string) { + f := &argsFlag{expectsValue: true} + p.flagMap[name] = f + for _, alias := range aliases { + p.flagAliases[alias] = name + } +} + +func (p *ArgsParser) RegisterBool(name string, aliases ...string) { + f := &argsFlag{expectsValue: false} + p.flagMap[name] = f + for _, alias := range aliases { + p.flagAliases[alias] = name + } +} + +func (p *ArgsParser) Value(name string) string { + if f, found := p.flagMap[name]; found { + return f.lastValue() + } else { + return "" + } +} + +func (p *ArgsParser) AllValues(name string) []string { + if f, found := p.flagMap[name]; found { + return f.values + } else { + return []string{} + } +} + +func (p *ArgsParser) Bool(name string) bool { + if f, found := p.flagMap[name]; found { + return len(f.values) > 0 && f.lastValue() != "false" + } else { + return false + } +} + +func (p *ArgsParser) Int(name string) int { + i, _ := strconv.Atoi(p.Value(name)) + return i +} + +func (p *ArgsParser) HasReceived(name string) bool { + f, found := p.flagMap[name] + return found && len(f.values) > 0 +} + +func NewArgsParser() *ArgsParser { + return &ArgsParser{ + flagMap: make(map[string]*argsFlag), + flagAliases: make(map[string]string), + } +} + +func NewArgsParserWithUsage(usage string) *ArgsParser { + p := NewArgsParser() + f := `(-[a-zA-Z0-9@^]|--[a-z][a-z0-9-]+)(?:\[?[ =]([a-zA-Z_<>:=-]+\]?))?` + re := regexp.MustCompile(fmt.Sprintf(`(?m)^\s*%s(?:,\s*%s)?$`, f, f)) + for _, match := range re.FindAllStringSubmatch(usage, -1) { + n1 := match[1] + n2 := match[3] + hasValue := !(match[2] == "" || strings.HasSuffix(match[2], "]")) || match[4] != "" + var aliases []string + if len(n1) == 2 && len(n2) > 2 { + aliases = []string{n1} + n1 = n2 + } else if n2 != "" { + aliases = []string{n2} + } + if hasValue { + p.RegisterValue(n1, aliases...) + } else { + p.RegisterBool(n1, aliases...) + } + } + return p +} diff -Nru hub-2.7.0~ds1/utils/args_parser_test.go hub-2.14.2~ds1/utils/args_parser_test.go --- hub-2.7.0~ds1/utils/args_parser_test.go 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/utils/args_parser_test.go 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,201 @@ +package utils + +import ( + "errors" + "reflect" + "testing" +) + +func equal(t *testing.T, expected, got interface{}) { + t.Helper() + if !reflect.DeepEqual(expected, got) { + t.Errorf("expected: %#v, got: %#v", expected, got) + } +} + +func TestArgsParser(t *testing.T) { + p := NewArgsParser() + p.RegisterValue("--hello", "-e") + p.RegisterValue("--origin", "-o") + args := []string{"--hello", "world", "one", "--", "--two"} + rest, err := p.Parse(args) + equal(t, nil, err) + equal(t, []string{"one", "--two"}, rest) + equal(t, "world", p.Value("--hello")) + equal(t, true, p.HasReceived("--hello")) + equal(t, "", p.Value("-e")) + equal(t, false, p.HasReceived("-e")) + equal(t, "", p.Value("--origin")) + equal(t, false, p.HasReceived("--origin")) + equal(t, []int{2, 4}, p.PositionalIndices) +} + +func TestArgsParser_RepeatedInvocation(t *testing.T) { + p := NewArgsParser() + p.RegisterValue("--hello", "-e") + p.RegisterValue("--origin", "-o") + + rest, err := p.Parse([]string{"--hello", "world", "--", "one"}) + equal(t, nil, err) + equal(t, []string{"one"}, rest) + equal(t, []int{3}, p.PositionalIndices) + equal(t, true, p.HasReceived("--hello")) + equal(t, "world", p.Value("--hello")) + equal(t, false, p.HasReceived("--origin")) + equal(t, true, p.HasTerminated) + + rest, err = p.Parse([]string{"two", "-oupstream"}) + equal(t, nil, err) + equal(t, []string{"two"}, rest) + equal(t, []int{0}, p.PositionalIndices) + equal(t, false, p.HasReceived("--hello")) + equal(t, true, p.HasReceived("--origin")) + equal(t, "upstream", p.Value("--origin")) + equal(t, false, p.HasTerminated) +} + +func TestArgsParser_UnknownFlag(t *testing.T) { + p := NewArgsParser() + p.RegisterValue("--hello") + p.RegisterBool("--yes", "-y") + + args := []string{"--hello", "world", "--nonexist", "one", "--", "--two"} + rest, err := p.Parse(args) + equal(t, errors.New("unknown flag: '--nonexist'"), err) + equal(t, []string{"one", "--two"}, rest) + + rest, err = p.Parse([]string{"one", "-yelp"}) + equal(t, errors.New("unknown shorthand flag: 'e' in -yelp"), err) + equal(t, []string{"one"}, rest) + equal(t, true, p.Bool("--yes")) +} + +func TestArgsParser_BlankArgs(t *testing.T) { + p := NewArgsParser() + rest, err := p.Parse([]string{"", ""}) + equal(t, nil, err) + equal(t, []string{"", ""}, rest) + equal(t, []int{0, 1}, p.PositionalIndices) +} + +func TestArgsParser_Values(t *testing.T) { + p := NewArgsParser() + p.RegisterValue("--origin", "-o") + args := []string{"--origin=a=b", "--origin=", "--origin", "c", "-o"} + rest, err := p.Parse(args) + equal(t, errors.New("no value given for '-o'"), err) + equal(t, []string{}, rest) + equal(t, []string{"a=b", "", "c"}, p.AllValues("--origin")) +} + +func TestArgsParser_Bool(t *testing.T) { + p := NewArgsParser() + p.RegisterBool("--noop") + p.RegisterBool("--color") + p.RegisterBool("--draft", "-d") + args := []string{"-d", "--draft=false", "--color=auto"} + rest, err := p.Parse(args) + equal(t, nil, err) + equal(t, []string{}, rest) + equal(t, false, p.Bool("--draft")) + equal(t, true, p.HasReceived("--draft")) + equal(t, false, p.HasReceived("-d")) + equal(t, false, p.HasReceived("--noop")) + equal(t, false, p.Bool("--noop")) + equal(t, true, p.HasReceived("--color")) + equal(t, "auto", p.Value("--color")) +} + +func TestArgsParser_BoolValue(t *testing.T) { + p := NewArgsParser() + p.RegisterBool("--draft") + args := []string{"--draft=yes pls"} + rest, err := p.Parse(args) + equal(t, nil, err) + equal(t, []string{}, rest) + equal(t, true, p.HasReceived("--draft")) + equal(t, true, p.Bool("--draft")) + equal(t, "yes pls", p.Value("--draft")) +} + +func TestArgsParser_Shorthand(t *testing.T) { + p := NewArgsParser() + p.RegisterValue("--origin", "-o") + p.RegisterBool("--draft", "-d") + p.RegisterBool("--copy", "-c") + args := []string{"-co", "one", "-dotwo"} + rest, err := p.Parse(args) + equal(t, nil, err) + equal(t, []string{}, rest) + equal(t, []string{"one", "two"}, p.AllValues("--origin")) + equal(t, true, p.Bool("--draft")) + equal(t, true, p.Bool("--copy")) +} + +func TestArgsParser_ShorthandEdgeCase(t *testing.T) { + p := NewArgsParser() + p.RegisterBool("--draft", "-d") + p.RegisterBool("-f") + p.RegisterBool("-a") + p.RegisterBool("-l") + p.RegisterBool("-s") + p.RegisterBool("-e") + args := []string{"-dfalse"} + rest, err := p.Parse(args) + equal(t, nil, err) + equal(t, []string{}, rest) + equal(t, true, p.Bool("--draft")) +} + +func TestArgsParser_Dashes(t *testing.T) { + p := NewArgsParser() + p.RegisterValue("--file", "-F") + args := []string{"-F-", "-", "--", "-F", "--"} + rest, err := p.Parse(args) + equal(t, nil, err) + equal(t, []string{"-", "-F", "--"}, rest) + equal(t, "-", p.Value("--file")) +} + +func TestArgsParser_RepeatedArg(t *testing.T) { + p := NewArgsParser() + p.RegisterValue("--msg", "-m") + args := []string{"--msg=hello", "-m", "world", "--msg", "how", "-mare you?"} + rest, err := p.Parse(args) + equal(t, nil, err) + equal(t, []string{}, rest) + equal(t, "are you?", p.Value("--msg")) + equal(t, []string{"hello", "world", "how", "are you?"}, p.AllValues("--msg")) +} + +func TestArgsParser_Int(t *testing.T) { + p := NewArgsParser() + p.RegisterValue("--limit", "-L") + p.RegisterValue("--depth", "-d") + args := []string{"-L24", "-d", "-3"} + rest, err := p.Parse(args) + equal(t, nil, err) + equal(t, []string{}, rest) + equal(t, true, p.HasReceived("--limit")) + equal(t, 24, p.Int("--limit")) + equal(t, true, p.HasReceived("--depth")) + equal(t, -3, p.Int("--depth")) +} + +func TestArgsParser_WithUsage(t *testing.T) { + p := NewArgsParserWithUsage(` + -L, --limit N + retrieve at most N records + -d, --draft + save as draft + --message=<msg>, -m <msg> + set message body + `) + args := []string{"-L24", "-d", "-mhello"} + rest, err := p.Parse(args) + equal(t, nil, err) + equal(t, []string{}, rest) + equal(t, "24", p.Value("--limit")) + equal(t, true, p.Bool("--draft")) + equal(t, "hello", p.Value("--message")) +} diff -Nru hub-2.7.0~ds1/utils/color.go hub-2.14.2~ds1/utils/color.go --- hub-2.7.0~ds1/utils/color.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/utils/color.go 2020-03-05 17:48:23.000000000 +0000 @@ -7,50 +7,78 @@ "strconv" ) +var ( + Black, White *Color +) + func init() { initColorCube() + Black, _ = NewColor("000000") + White, _ = NewColor("ffffff") } type Color struct { - Red int64 - Green int64 - Blue int64 + Red uint8 + Green uint8 + Blue uint8 } func NewColor(hex string) (*Color, error) { - red, err := strconv.ParseInt(hex[0:2], 16, 16) + red, err := strconv.ParseUint(hex[0:2], 16, 8) if err != nil { return nil, err } - green, err := strconv.ParseInt(hex[2:4], 16, 16) + green, err := strconv.ParseUint(hex[2:4], 16, 8) if err != nil { return nil, err } - blue, err := strconv.ParseInt(hex[4:6], 16, 16) + blue, err := strconv.ParseUint(hex[4:6], 16, 8) if err != nil { return nil, err } return &Color{ - Red: red, - Green: green, - Blue: blue, + Red: uint8(red), + Green: uint8(green), + Blue: uint8(blue), }, nil } -func (c *Color) Brightness() float32 { - return (0.299*float32(c.Red) + - 0.587*float32(c.Green) + - 0.114*float32(c.Blue)) / 255 -} - func (c *Color) Distance(other *Color) float64 { - return math.Sqrt(float64(math.Pow(float64(c.Red-other.Red), 2) + + return math.Sqrt(math.Pow(float64(c.Red-other.Red), 2) + math.Pow(float64(c.Green-other.Green), 2) + - math.Pow(float64(c.Blue-other.Blue), 2))) + math.Pow(float64(c.Blue-other.Blue), 2)) +} + +func rgbComponentToBoldValue(component uint8) float64 { + srgb := float64(component) / 255 + if srgb <= 0.03928 { + return srgb / 12.92 + } else { + return math.Pow(((srgb + 0.055) / 1.055), 2.4) + } +} + +func (c *Color) Luminance() float64 { + return 0.2126*rgbComponentToBoldValue(c.Red) + + 0.7152*rgbComponentToBoldValue(c.Green) + + 0.0722*rgbComponentToBoldValue(c.Blue) +} + +func (c *Color) ContrastRatio(other *Color) float64 { + L := c.Luminance() + otherL := other.Luminance() + var L1, L2 float64 + if L > otherL { + L1, L2 = L, otherL + } else { + L1, L2 = otherL, L + } + ratio := (L1 + 0.05) / (L2 + 0.05) + return ratio } -var x6colorIndexes = [6]int64{0, 95, 135, 175, 215, 255} +var x6colorIndexes = [6]uint8{0, 95, 135, 175, 215, 255} var x6colorCube [216]Color func initColorCube() { diff -Nru hub-2.7.0~ds1/utils/color_test.go hub-2.14.2~ds1/utils/color_test.go --- hub-2.7.0~ds1/utils/color_test.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/utils/color_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,13 +0,0 @@ -package utils - -import ( - "github.com/bmizerany/assert" - "testing" -) - -func TestColorBrightness(t *testing.T) { - c, err := NewColor("880000") - assert.Equal(t, nil, err) - actual := c.Brightness() - assert.Equal(t, float32(0.15946665406227112), actual) -} diff -Nru hub-2.7.0~ds1/utils/json.go hub-2.14.2~ds1/utils/json.go --- hub-2.7.0~ds1/utils/json.go 1970-01-01 00:00:00.000000000 +0000 +++ hub-2.14.2~ds1/utils/json.go 2020-03-05 17:48:23.000000000 +0000 @@ -0,0 +1,104 @@ +package utils + +import ( + "encoding/json" + "fmt" + "io" + "strings" +) + +type state struct { + isObject bool + isArray bool + arrayIndex int + objectKey string + parentState *state +} + +func stateKey(s *state) string { + k := "" + if s.parentState != nil { + k = stateKey(s.parentState) + } + if s.isObject { + return fmt.Sprintf("%s.%s", k, s.objectKey) + } else if s.isArray { + return fmt.Sprintf("%s.[%d]", k, s.arrayIndex) + } else { + return k + } +} + +func JSONPath(out io.Writer, src io.Reader, colorize bool) (hasNextPage bool, endCursor string) { + dec := json.NewDecoder(src) + dec.UseNumber() + + s := &state{} + postEmit := func() { + if s.isObject { + s.objectKey = "" + } else if s.isArray { + s.arrayIndex++ + } + } + + color := func(c string, t interface{}) string { + if colorize { + return fmt.Sprintf("\033[%sm%s\033[m", c, t) + } else if tt, ok := t.(string); ok { + return tt + } else { + return fmt.Sprintf("%s", t) + } + } + + for { + token, err := dec.Token() + if err == io.EOF { + break + } else if err != nil { + panic(err) + } + if delim, ok := token.(json.Delim); ok { + switch delim { + case '{': + s = &state{isObject: true, parentState: s} + case '[': + s = &state{isArray: true, parentState: s} + case '}', ']': + s = s.parentState + postEmit() + default: + panic("unknown delim") + } + } else { + if s.isObject && s.objectKey == "" { + s.objectKey = token.(string) + } else { + k := stateKey(s) + fmt.Fprintf(out, "%s\t", color("0;36", k)) + + switch tt := token.(type) { + case string: + fmt.Fprintf(out, "%s\n", strings.Replace(tt, "\n", "\\n", -1)) + if strings.HasSuffix(k, ".pageInfo.endCursor") { + endCursor = tt + } + case json.Number: + fmt.Fprintf(out, "%s\n", color("0;35", tt)) + case nil: + fmt.Fprintf(out, "\n") + case bool: + fmt.Fprintf(out, "%s\n", color("1;33", fmt.Sprintf("%v", tt))) + if strings.HasSuffix(k, ".pageInfo.hasNextPage") { + hasNextPage = tt + } + default: + panic("unknown type") + } + postEmit() + } + } + } + return +} diff -Nru hub-2.7.0~ds1/utils/utils.go hub-2.14.2~ds1/utils/utils.go --- hub-2.7.0~ds1/utils/utils.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/utils/utils.go 2020-03-05 17:48:23.000000000 +0000 @@ -11,6 +11,7 @@ "time" "github.com/github/hub/ui" + "github.com/kballard/go-shellquote" ) var timeNow = time.Now @@ -30,13 +31,15 @@ browser := os.Getenv("BROWSER") if browser == "" { browser = searchBrowserLauncher(runtime.GOOS) + } else { + browser = os.ExpandEnv(browser) } if browser == "" { return nil, errors.New("Please set $BROWSER to a web launcher") } - return strings.Split(browser, " "), nil + return shellquote.Split(browser) } func searchBrowserLauncher(goos string) (browser string) { @@ -61,7 +64,7 @@ } func CommandPath(cmd string) (string, error) { - if runtime.GOOS == "windows" { + if runtime.GOOS == "windows" && !strings.HasSuffix(cmd, ".exe") { cmd = cmd + ".exe" } @@ -78,10 +81,6 @@ return filepath.EvalSymlinks(path) } -func IsOption(confirm, short, long string) bool { - return strings.EqualFold(confirm, short) || strings.EqualFold(confirm, long) -} - func TimeAgo(t time.Time) string { duration := timeNow().Sub(t) minutes := duration.Minutes() diff -Nru hub-2.7.0~ds1/Vagrantfile hub-2.14.2~ds1/Vagrantfile --- hub-2.7.0~ds1/Vagrantfile 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/Vagrantfile 1970-01-01 00:00:00.000000000 +0000 @@ -1,67 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # -# -# Place this Vagrantfile in your src folder and run: -# -# vagrant up -# -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - -GO_ARCHIVES = { - "linux" => "go1.4.2.linux-amd64.tar.gz", -} - -INSTALL = { - "linux" => "apt-get update -qq; apt-get install -qq -y git mercurial bzr curl", -} - -# location of the Vagrantfile -def src_path - ENV["GOPATH"] -end - -# shell script to bootstrap Go -def bootstrap(box) - install = INSTALL[box] - archive = GO_ARCHIVES[box] - vagrant_home = "/home/vagrant" - - profile = <<-PROFILE - export GOROOT=#{vagrant_home}/go - export GOPATH=#{vagrant_home}/gocode - export PATH=$GOPATH/bin:$GOROOT/bin:$PATH - PROFILE - - <<-SCRIPT - #{install} - - if ! [ -f /home/vagrant/#{archive} ]; then - curl -O# https://storage.googleapis.com/golang/#{archive} - fi - tar -C /home/vagrant -xzf #{archive} - chown -R vagrant:vagrant #{vagrant_home}/go - - if ! grep -q GOPATH #{vagrant_home}/.bashrc; then - echo '#{profile}' >> #{vagrant_home}/.bashrc - fi - source #{vagrant_home}/.bashrc - - chown -R vagrant:vagrant #{vagrant_home}/gocode - - apt-get update -qq - apt-get install -qq ruby1.9.1-dev tmux zsh git - gem install bundler - - echo "\nRun: vagrant ssh #{box} -c 'cd project/path; go test ./...'" - SCRIPT -end - -Vagrant.configure("2") do |config| - config.vm.define "linux" do |linux| - linux.vm.box = "ubuntu/trusty64" - linux.vm.synced_folder "#{src_path}/src/github.com/github/hub", "/home/vagrant/gocode/src/github.com/github/hub" - linux.vm.provision :shell, :inline => bootstrap("linux") - end -end diff -Nru hub-2.7.0~ds1/version/version.go hub-2.14.2~ds1/version/version.go --- hub-2.7.0~ds1/version/version.go 2018-12-28 07:07:53.000000000 +0000 +++ hub-2.14.2~ds1/version/version.go 2020-03-05 17:48:23.000000000 +0000 @@ -1,17 +1,4 @@ package version -import ( - "fmt" - - "github.com/github/hub/git" -) - -var Version = "2.7.0" - -func FullVersion() (string, error) { - gitVersion, err := git.Version() - if err != nil { - gitVersion = "git version (unavailable)" - } - return fmt.Sprintf("%s\nhub version %s", gitVersion, Version), err -} +// Version represents the hub version number +var Version = "2.14.2"