Merge branch 'master' into reviewers-in-pr-view

This commit is contained in:
Toshiya Doi 2020-04-30 00:31:04 +09:00
commit d2d0b47ecc
22 changed files with 416 additions and 136 deletions

View file

@ -504,7 +504,11 @@ func issueProjectList(issue api.Issue) string {
projectNames := make([]string, 0, len(issue.ProjectCards.Nodes))
for _, project := range issue.ProjectCards.Nodes {
projectNames = append(projectNames, fmt.Sprintf("%s (%s)", project.Project.Name, project.Column.Name))
colName := project.Column.Name
if colName == "" {
colName = "Awaiting triage"
}
projectNames = append(projectNames, fmt.Sprintf("%s (%s)", project.Project.Name, colName))
}
list := strings.Join(projectNames, ", ")

View file

@ -312,7 +312,7 @@ func TestIssueView_Preview(t *testing.T) {
`Open • marseilles opened about 292 years ago • 9 comments`,
`Assignees: marseilles, monaco\n`,
`Labels: one, two, three, four, five\n`,
`Projects: Project 1 \(column A\), Project 2 \(column B\), Project 3 \(column C\)\n`,
`Projects: Project 1 \(column A\), Project 2 \(column B\), Project 3 \(column C\), Project 4 \(Awaiting triage\)\n`,
`Milestone: uluru\n`,
`bold story`,
`View this issue on GitHub: https://github.com/OWNER/REPO/issues/123`,

View file

@ -531,7 +531,11 @@ func prProjectList(pr api.PullRequest) string {
projectNames := make([]string, 0, len(pr.ProjectCards.Nodes))
for _, project := range pr.ProjectCards.Nodes {
projectNames = append(projectNames, fmt.Sprintf("%s (%s)", project.Project.Name, project.Column.Name))
colName := project.Column.Name
if colName == "" {
colName = "Awaiting triage"
}
projectNames = append(projectNames, fmt.Sprintf("%s (%s)", project.Project.Name, colName))
}
list := strings.Join(projectNames, ", ")

View file

@ -47,7 +47,7 @@ func prCheckout(cmd *cobra.Command, args []string) error {
baseRemote, _ := remotes.FindByRepo(baseRepo.RepoOwner(), baseRepo.RepoName())
// baseRemoteSpec is a repository URL or a remote name to be used in git fetch
baseURLOrName := fmt.Sprintf("https://github.com/%s.git", ghrepo.FullName(baseRepo))
baseURLOrName := formatRemoteURL(cmd, ghrepo.FullName(baseRepo))
if baseRemote != nil {
baseURLOrName = baseRemote.Name
}
@ -97,7 +97,7 @@ func prCheckout(cmd *cobra.Command, args []string) error {
remote := baseURLOrName
mergeRef := ref
if pr.MaintainerCanModify {
remote = fmt.Sprintf("https://github.com/%s/%s.git", pr.HeadRepositoryOwner.Login, pr.HeadRepository.Name)
remote = formatRemoteURL(cmd, fmt.Sprintf("%s/%s", pr.HeadRepositoryOwner.Login, pr.HeadRepository.Name))
mergeRef = fmt.Sprintf("refs/heads/%s", pr.HeadRefName)
}
if mc, err := git.Config(fmt.Sprintf("branch.%s.merge", newBranchName)); err != nil || mc == "" {

View file

@ -250,8 +250,8 @@ func prCreate(cmd *cobra.Command, _ []string) error {
// In either case, we want to add the head repo as a new git remote so we
// can push to it.
if headRemote == nil {
// TODO: support non-HTTPS git remote URLs
headRepoURL := fmt.Sprintf("https://github.com/%s.git", ghrepo.FullName(headRepo))
headRepoURL := formatRemoteURL(cmd, ghrepo.FullName(headRepo))
// TODO: prevent clashes with another remote of a same name
gitRemote, err := git.AddRemote("fork", headRepoURL)
if err != nil {

View file

@ -430,7 +430,7 @@ func TestPRView_Preview(t *testing.T) {
`Reviewers: 2 \(Approved\), 3 \(Commented\), 1 \(Requested\)\n`,
`Assignees: marseilles, monaco\n`,
`Labels: one, two, three, four, five\n`,
`Projects: Project 1 \(column A\), Project 2 \(column B\), Project 3 \(column C\)\n`,
`Projects: Project 1 \(column A\), Project 2 \(column B\), Project 3 \(column C\), Project 4 \(Awaiting triage\)\n`,
`Milestone: uluru\n`,
`blueberries taste good`,
`View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12\n`,

View file

@ -127,7 +127,7 @@ func runClone(cloneURL string, args []string) (target string, err error) {
func repoClone(cmd *cobra.Command, args []string) error {
cloneURL := args[0]
if !strings.Contains(cloneURL, ":") {
cloneURL = fmt.Sprintf("https://github.com/%s.git", cloneURL)
cloneURL = formatRemoteURL(cmd, cloneURL)
}
var repo ghrepo.Interface
@ -158,7 +158,7 @@ func repoClone(cmd *cobra.Command, args []string) error {
}
if parentRepo != nil {
err := addUpstreamRemote(parentRepo, cloneDir)
err := addUpstreamRemote(cmd, parentRepo, cloneDir)
if err != nil {
return err
}
@ -167,9 +167,8 @@ func repoClone(cmd *cobra.Command, args []string) error {
return nil
}
func addUpstreamRemote(parentRepo ghrepo.Interface, cloneDir string) error {
// TODO: support SSH remote URLs
upstreamURL := fmt.Sprintf("https://github.com/%s.git", ghrepo.FullName(parentRepo))
func addUpstreamRemote(cmd *cobra.Command, parentRepo ghrepo.Interface, cloneDir string) error {
upstreamURL := formatRemoteURL(cmd, ghrepo.FullName(parentRepo))
cloneCmd := git.GitCommand("-C", cloneDir, "remote", "add", "-f", "upstream", upstreamURL)
cloneCmd.Stdout = os.Stdout
@ -267,14 +266,10 @@ func repoCreate(cmd *cobra.Command, args []string) error {
fmt.Fprintln(out, repo.URL)
}
remoteURL := repo.URL + ".git"
remoteURL := formatRemoteURL(cmd, ghrepo.FullName(repo))
if projectDirErr == nil {
// TODO: use git.AddRemote
remoteAdd := git.GitCommand("remote", "add", "origin", remoteURL)
remoteAdd.Stdout = os.Stdout
remoteAdd.Stderr = os.Stderr
err = run.PrepareCmd(remoteAdd).Run()
_, err = git.AddRemote("origin", remoteURL)
if err != nil {
return err
}
@ -338,7 +333,7 @@ func repoFork(cmd *cobra.Command, args []string) error {
return fmt.Errorf("unable to create client: %w", err)
}
var toFork ghrepo.Interface
var repoToFork ghrepo.Interface
inParent := false // whether or not we're forking the repo we're currently "in"
if len(args) == 0 {
baseRepo, err := determineBaseRepo(cmd, ctx)
@ -346,7 +341,7 @@ func repoFork(cmd *cobra.Command, args []string) error {
return fmt.Errorf("unable to determine base repository: %w", err)
}
inParent = true
toFork = baseRepo
repoToFork = baseRepo
} else {
repoArg := args[0]
@ -356,14 +351,23 @@ func repoFork(cmd *cobra.Command, args []string) error {
return fmt.Errorf("did not understand argument: %w", err)
}
toFork, err = ghrepo.FromURL(parsedURL)
repoToFork, err = ghrepo.FromURL(parsedURL)
if err != nil {
return fmt.Errorf("did not understand argument: %w", err)
}
} else if strings.HasPrefix(repoArg, "git@") {
parsedURL, err := git.ParseURL(repoArg)
if err != nil {
return fmt.Errorf("did not understand argument: %w", err)
}
repoToFork, err = ghrepo.FromURL(parsedURL)
if err != nil {
return fmt.Errorf("did not understand argument: %w", err)
}
} else {
toFork = ghrepo.FromFullName(repoArg)
if toFork.RepoName() == "" || toFork.RepoOwner() == "" {
repoToFork = ghrepo.FromFullName(repoArg)
if repoToFork.RepoName() == "" || repoToFork.RepoOwner() == "" {
return fmt.Errorf("could not parse owner or repo name from %s", repoArg)
}
}
@ -372,12 +376,12 @@ func repoFork(cmd *cobra.Command, args []string) error {
greenCheck := utils.Green("✓")
out := colorableOut(cmd)
s := utils.Spinner(out)
loading := utils.Gray("Forking ") + utils.Bold(utils.Gray(ghrepo.FullName(toFork))) + utils.Gray("...")
loading := utils.Gray("Forking ") + utils.Bold(utils.Gray(ghrepo.FullName(repoToFork))) + utils.Gray("...")
s.Suffix = " " + loading
s.FinalMSG = utils.Gray(fmt.Sprintf("- %s\n", loading))
s.Start()
forkedRepo, err := api.ForkRepo(apiClient, toFork)
forkedRepo, err := api.ForkRepo(apiClient, repoToFork)
if err != nil {
s.Stop()
return fmt.Errorf("failed to fork: %w", err)
@ -437,7 +441,9 @@ func repoFork(cmd *cobra.Command, args []string) error {
fmt.Fprintf(out, "%s Renamed %s remote to %s\n", greenCheck, utils.Bold(remoteName), utils.Bold(renameTarget))
}
_, err = git.AddRemote(remoteName, forkedRepo.CloneURL)
forkedRepoCloneURL := formatRemoteURL(cmd, ghrepo.FullName(forkedRepo))
_, err = git.AddRemote(remoteName, forkedRepoCloneURL)
if err != nil {
return fmt.Errorf("failed to add remote: %w", err)
}
@ -453,12 +459,13 @@ func repoFork(cmd *cobra.Command, args []string) error {
}
}
if cloneDesired {
cloneDir, err := runClone(forkedRepo.CloneURL, []string{})
forkedRepoCloneURL := formatRemoteURL(cmd, ghrepo.FullName(forkedRepo))
cloneDir, err := runClone(forkedRepoCloneURL, []string{})
if err != nil {
return fmt.Errorf("failed to clone fork: %w", err)
}
err = addUpstreamRemote(toFork, cloneDir)
err = addUpstreamRemote(cmd, repoToFork, cloneDir)
if err != nil {
return err
}

View file

@ -153,7 +153,7 @@ func TestRepoFork_in_parent_yes(t *testing.T) {
expectedCmds := []string{
"git remote rename origin upstream",
"git remote add -f origin https://github.com/someone/repo.git",
"git remote add -f origin https://github.com/someone/REPO.git",
}
for x, cmd := range seenCmds {
@ -185,8 +185,8 @@ func TestRepoFork_outside_yes(t *testing.T) {
eq(t, output.Stderr(), "")
eq(t, strings.Join(cs.Calls[0].Args, " "), "git clone https://github.com/someone/repo.git")
eq(t, strings.Join(cs.Calls[1].Args, " "), "git -C repo remote add -f upstream https://github.com/OWNER/REPO.git")
eq(t, strings.Join(cs.Calls[0].Args, " "), "git clone https://github.com/someone/REPO.git")
eq(t, strings.Join(cs.Calls[1].Args, " "), "git -C REPO remote add -f upstream https://github.com/OWNER/REPO.git")
test.ExpectLines(t, output.String(),
"Created fork someone/REPO",
@ -218,8 +218,8 @@ func TestRepoFork_outside_survey_yes(t *testing.T) {
eq(t, output.Stderr(), "")
eq(t, strings.Join(cs.Calls[0].Args, " "), "git clone https://github.com/someone/repo.git")
eq(t, strings.Join(cs.Calls[1].Args, " "), "git -C repo remote add -f upstream https://github.com/OWNER/REPO.git")
eq(t, strings.Join(cs.Calls[0].Args, " "), "git clone https://github.com/someone/REPO.git")
eq(t, strings.Join(cs.Calls[1].Args, " "), "git -C REPO remote add -f upstream https://github.com/OWNER/REPO.git")
test.ExpectLines(t, output.String(),
"Created fork someone/REPO",
@ -287,7 +287,7 @@ func TestRepoFork_in_parent_survey_yes(t *testing.T) {
expectedCmds := []string{
"git remote rename origin upstream",
"git remote add -f origin https://github.com/someone/repo.git",
"git remote add -f origin https://github.com/someone/REPO.git",
}
for x, cmd := range seenCmds {
@ -499,7 +499,11 @@ func TestRepoCreate(t *testing.T) {
{ "data": { "createRepository": {
"repository": {
"id": "REPOID",
"url": "https://github.com/OWNER/REPO"
"url": "https://github.com/OWNER/REPO",
"name": "REPO",
"owner": {
"login": "OWNER"
}
}
} } }
`))
@ -522,7 +526,7 @@ func TestRepoCreate(t *testing.T) {
if seenCmd == nil {
t.Fatal("expected a command to run")
}
eq(t, strings.Join(seenCmd.Args, " "), "git remote add origin https://github.com/OWNER/REPO.git")
eq(t, strings.Join(seenCmd.Args, " "), "git remote add -f origin https://github.com/OWNER/REPO.git")
var reqBody struct {
Query string
@ -564,7 +568,11 @@ func TestRepoCreate_org(t *testing.T) {
{ "data": { "createRepository": {
"repository": {
"id": "REPOID",
"url": "https://github.com/ORG/REPO"
"url": "https://github.com/ORG/REPO",
"name": "REPO",
"owner": {
"login": "ORG"
}
}
} } }
`))
@ -587,7 +595,7 @@ func TestRepoCreate_org(t *testing.T) {
if seenCmd == nil {
t.Fatal("expected a command to run")
}
eq(t, strings.Join(seenCmd.Args, " "), "git remote add origin https://github.com/ORG/REPO.git")
eq(t, strings.Join(seenCmd.Args, " "), "git remote add -f origin https://github.com/ORG/REPO.git")
var reqBody struct {
Query string
@ -629,7 +637,11 @@ func TestRepoCreate_orgWithTeam(t *testing.T) {
{ "data": { "createRepository": {
"repository": {
"id": "REPOID",
"url": "https://github.com/ORG/REPO"
"url": "https://github.com/ORG/REPO",
"name": "REPO",
"owner": {
"login": "ORG"
}
}
} } }
`))
@ -652,7 +664,7 @@ func TestRepoCreate_orgWithTeam(t *testing.T) {
if seenCmd == nil {
t.Fatal("expected a command to run")
}
eq(t, strings.Join(seenCmd.Args, " "), "git remote add origin https://github.com/ORG/REPO.git")
eq(t, strings.Join(seenCmd.Args, " "), "git remote add -f origin https://github.com/ORG/REPO.git")
var reqBody struct {
Query string

View file

@ -51,6 +51,8 @@ func init() {
// TODO:
// RootCmd.PersistentFlags().BoolP("verbose", "V", false, "enable verbose output")
RootCmd.SetHelpFunc(rootHelpFunc)
RootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
if err == pflag.ErrHelp {
return err
@ -74,12 +76,9 @@ func (fe FlagError) Unwrap() error {
// RootCmd is the entry point of command-line execution
var RootCmd = &cobra.Command{
Use: "gh",
Use: "gh <command> <subcommand> [flags]",
Short: "GitHub CLI",
Long: `Work seamlessly with GitHub from the command line.
GitHub CLI is in early stages of development, and we'd love to hear your
feedback at <https://forms.gle/umxd3h31c7aMQFKG7>`,
Long: `Work seamlessly with GitHub from the command line.`,
SilenceErrors: true,
SilenceUsage: true,
@ -111,20 +110,11 @@ func BasicClient() (*api.Client, error) {
}
opts = append(opts, api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", Version)))
c, err := config.ParseDefaultConfig()
if err != nil {
return nil, err
if c, err := config.ParseDefaultConfig(); err == nil {
if token, _ := c.Get(defaultHostname, "oauth_token"); token != "" {
opts = append(opts, api.AddHeader("Authorization", fmt.Sprintf("token %s", token)))
}
}
token, err := c.Get(defaultHostname, "oauth_token")
if err != nil {
return nil, err
}
if token == "" {
return nil, fmt.Errorf("no oauth_token set in config")
}
opts = append(opts, api.AddHeader("Authorization", fmt.Sprintf("token %s", token)))
return api.NewClient(opts...), nil
}
@ -142,12 +132,47 @@ var apiClientForContext = func(ctx context.Context) (*api.Client, error) {
if err != nil {
return nil, err
}
var opts []api.ClientOption
if verbose := os.Getenv("DEBUG"); verbose != "" {
opts = append(opts, apiVerboseLog())
}
getAuthValue := func() string {
return fmt.Sprintf("token %s", token)
}
checkScopesFunc := func(appID string) error {
if config.IsGitHubApp(appID) && utils.IsTerminal(os.Stdin) && utils.IsTerminal(os.Stderr) {
newToken, loginHandle, err := config.AuthFlow("Notice: additional authorization required")
if err != nil {
return err
}
cfg, err := ctx.Config()
if err != nil {
return err
}
_ = cfg.Set(defaultHostname, "oauth_token", newToken)
_ = cfg.Set(defaultHostname, "user", loginHandle)
// update config file on disk
err = cfg.Write()
if err != nil {
return err
}
// update configuration in memory
token = newToken
config.AuthFlowComplete()
} else {
fmt.Fprintln(os.Stderr, "Warning: gh now requires the `read:org` OAuth scope.")
fmt.Fprintln(os.Stderr, "Visit https://github.com/settings/tokens and edit your token to enable `read:org`")
fmt.Fprintln(os.Stderr, "or generate a new token and paste it via `gh config set -h github.com oauth_token MYTOKEN`")
}
return nil
}
opts = append(opts,
api.AddHeader("Authorization", fmt.Sprintf("token %s", token)),
api.CheckScopes("read:org", checkScopesFunc),
api.AddHeaderFunc("Authorization", getAuthValue),
api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", Version)),
// antiope-preview: Checks
api.AddHeader("Accept", "application/vnd.github.antiope-preview+json"),
@ -222,3 +247,87 @@ func determineBaseRepo(cmd *cobra.Command, ctx context.Context) (ghrepo.Interfac
return baseRepo, nil
}
func rootHelpFunc(command *cobra.Command, s []string) {
type helpEntry struct {
Title string
Body string
}
coreCommandNames := []string{"issue", "pr", "repo"}
var coreCommands []string
var additionalCommands []string
for _, c := range command.Commands() {
if c.Short == "" {
continue
}
s := " " + rpad(c.Name()+":", c.NamePadding()) + c.Short
if includes(coreCommandNames, c.Name()) {
coreCommands = append(coreCommands, s)
} else {
additionalCommands = append(additionalCommands, s)
}
}
helpEntries := []helpEntry{
{
"",
command.Long},
{"USAGE", command.Use},
{"CORE COMMANDS", strings.Join(coreCommands, "\n")},
{"ADDITIONAL COMMANDS", strings.Join(additionalCommands, "\n")},
{"FLAGS", strings.TrimRight(command.LocalFlags().FlagUsages(), "\n")},
{"EXAMPLES", `
$ gh issue create
$ gh repo clone
$ gh pr checkout 321`},
{"LEARN MORE", `
Use "gh <command> <subcommand> --help" for more information about a command.
Read the manual at <http://cli.github.com/manual>`},
{"FEEDBACK", `
Fill out our feedback form <https://forms.gle/umxd3h31c7aMQFKG7>
Open an issue using gh issue create -R cli/cli`},
}
out := colorableOut(command)
for _, e := range helpEntries {
if e.Title != "" {
fmt.Fprintln(out, utils.Bold(e.Title))
}
fmt.Fprintln(out, strings.TrimLeft(e.Body, "\n")+"\n")
}
}
// rpad adds padding to the right of a string.
func rpad(s string, padding int) string {
template := fmt.Sprintf("%%-%ds ", padding)
return fmt.Sprintf(template, s)
}
func includes(a []string, s string) bool {
for _, x := range a {
if x == s {
return true
}
}
return false
}
func formatRemoteURL(cmd *cobra.Command, fullRepoName string) string {
ctx := contextForCommand(cmd)
protocol := "https"
cfg, err := ctx.Config()
if err != nil {
fmt.Fprintf(colorableErr(cmd), "%s failed to load config: %s. using defaults\n", utils.Yellow("!"), err)
} else {
cfgProtocol, _ := cfg.Get(defaultHostname, "git_protocol")
protocol = cfgProtocol
}
if protocol == "ssh" {
return fmt.Sprintf("git@%s:%s.git", defaultHostname, fullRepoName)
}
return fmt.Sprintf("https://%s/%s.git", defaultHostname, fullRepoName)
}

View file

@ -40,3 +40,22 @@ func TestChangelogURL(t *testing.T) {
t.Errorf("expected %s to create url %s but got %s", tag, url, result)
}
}
func TestRemoteURLFormatting_no_config(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
result := formatRemoteURL(repoForkCmd, "OWNER/REPO")
eq(t, result, "https://github.com/OWNER/REPO.git")
}
func TestRemoteURLFormatting_ssh_config(t *testing.T) {
cfg := `---
hosts:
github.com:
user: OWNER
oauth_token: MUSTBEHIGHCUZIMATOKEN
git_protocol: ssh
`
initBlankContext(cfg, "OWNER/REPO", "master")
result := formatRemoteURL(repoForkCmd, "OWNER/REPO")
eq(t, result, "git@github.com:OWNER/REPO.git")
}

View file

@ -2,6 +2,7 @@ package command
import (
"fmt"
"os"
"github.com/AlecAivazis/survey/v2"
"github.com/cli/cli/pkg/githubtemplate"
@ -82,6 +83,16 @@ func selectTemplate(templatePaths []string) (string, error) {
}
func titleBodySurvey(cmd *cobra.Command, providedTitle, providedBody string, defs defaults, templatePaths []string) (*titleBody, error) {
editorCommand := os.Getenv("GH_EDITOR")
if editorCommand == "" {
ctx := contextForCommand(cmd)
cfg, err := ctx.Config()
if err != nil {
return nil, fmt.Errorf("could not read config: %w", err)
}
editorCommand, _ = cfg.Get(defaultHostname, "editor")
}
var inProgress titleBody
inProgress.Title = defs.Title
templateContents := ""
@ -109,6 +120,7 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle, providedBody string, def
bodyQuestion := &survey.Question{
Name: "body",
Prompt: &surveyext.GhEditor{
EditorCommand: editorCommand,
Editor: &survey.Editor{
Message: "Body",
FileName: "*.md",