diff --git a/commands/pull_request.go b/commands/pull_request.go index 9a9cb1989c8ed8cd99816967a6d0c766d264e8c0..87579e85d8882eabee67f22c6d283b12d3cf4a45 100644 --- a/commands/pull_request.go +++ b/commands/pull_request.go @@ -35,14 +35,23 @@ of title you can paste a full URL to an issue on GitHub. `, } -var flagPullRequestBase, flagPullRequestHead, flagPullRequestIssue, flagPullRequestMessage, flagPullRequestFile string +var ( + flagPullRequestBase, + flagPullRequestHead, + flagPullRequestIssue, + flagPullRequestMessage, + flagPullRequestFile string + flagPullRequestForce bool +) func init() { cmdPullRequest.Flag.StringVar(&flagPullRequestBase, "b", "", "BASE") cmdPullRequest.Flag.StringVar(&flagPullRequestHead, "h", "", "HEAD") cmdPullRequest.Flag.StringVar(&flagPullRequestIssue, "i", "", "ISSUE") cmdPullRequest.Flag.StringVar(&flagPullRequestMessage, "m", "", "MESSAGE") + cmdPullRequest.Flag.BoolVar(&flagPullRequestForce, "f", false, "FORCE") cmdPullRequest.Flag.StringVar(&flagPullRequestFile, "F", "", "FILE") + cmdPullRequest.Flag.StringVar(&flagPullRequestFile, "file", "", "FILE") } /* @@ -79,14 +88,15 @@ func pullRequest(cmd *Command, args *Args) { gh.Project = baseProject var ( - base, head string - explicitOwner bool + base, head string + force, explicitOwner bool ) + force = flagPullRequestForce if flagPullRequestBase != "" { if strings.Contains(flagPullRequestBase, ":") { split := strings.SplitN(flagPullRequestBase, ":", 2) base = split[1] - baseProject.Owner = split[0] + baseProject = github.NewProjectFromString(split[0]) } else { base = flagPullRequestBase } @@ -96,7 +106,7 @@ func pullRequest(cmd *Command, args *Args) { if strings.Contains(flagPullRequestHead, ":") { split := strings.SplitN(flagPullRequestHead, ":", 2) head = split[1] - headProject.Owner = split[0] + headProject = github.NewProjectFromString(split[0]) explicitOwner = true } else { head = flagPullRequestHead @@ -144,7 +154,7 @@ func pullRequest(cmd *Command, args *Args) { // when no tracking, assume remote branch is published under active user's fork if tberr != nil && !explicitOwner && gh.Config.User != headProject.Owner { - headProject = github.NewProjectFromNameAndOwner(headProject.Name, "") + headProject = github.NewProjectFromString(headProject.Name) } var title, body string @@ -169,8 +179,15 @@ func pullRequest(cmd *Command, args *Args) { fullBase := fmt.Sprintf("%s:%s", baseProject.Owner, base) fullHead := fmt.Sprintf("%s:%s", headProject.Owner, head) + commits, _ := git.RefList(base, head) + if !force && tberr == nil && len(commits) > 0 { + err = fmt.Errorf("Aborted: %d commits are not yet pushed to %s", len(commits), trackedBranch.LongName()) + err = fmt.Errorf("%s\n(use `-f` to force submit a pull request anyway)", err) + utils.Check(err) + } + if title == "" && flagPullRequestIssue == "" { - t, b, err := writePullRequestTitleAndBody(base, head, fullBase, fullHead) + t, b, err := writePullRequestTitleAndBody(base, head, fullBase, fullHead, commits) utils.Check(err) title = t body = b @@ -201,14 +218,14 @@ func pullRequest(cmd *Command, args *Args) { args.Replace("echo", "", pullRequestURL) } -func writePullRequestTitleAndBody(base, head, fullBase, fullHead string) (title, body string, err error) { +func writePullRequestTitleAndBody(base, head, fullBase, fullHead string, commits []string) (title, body string, err error) { messageFile, err := git.PullReqMsgFile() if err != nil { return } defer os.Remove(messageFile) - err = writePullRequestChanges(base, head, fullBase, fullHead, messageFile) + err = writePullRequestChanges(base, head, fullBase, fullHead, commits, messageFile) if err != nil { return } @@ -232,9 +249,7 @@ func writePullRequestTitleAndBody(base, head, fullBase, fullHead string) (title, return } -func writePullRequestChanges(base, head, fullBase, fullHead string, messageFile string) error { - commits, _ := git.RefList(base, head) - +func writePullRequestChanges(base, head, fullBase, fullHead string, commits []string, messageFile string) error { var defaultMsg, commitSummary string if len(commits) == 1 { defaultMsg, err := git.Show(commits[0]) diff --git a/github/branch.go b/github/branch.go index e0f240a3e920f68890bf27c7e95b1c31002c58de..09baaf5ec54f3b7a74f31b6041163011e0d8567a 100644 --- a/github/branch.go +++ b/github/branch.go @@ -14,6 +14,11 @@ func (b Branch) ShortName() string { return reg.ReplaceAllString(string(b), "") } +func (b Branch) LongName() string { + reg := regexp.MustCompile("refs/(remotes/)?") + return reg.ReplaceAllString(string(b), "") +} + func (b Branch) Upstream() (u Branch, err error) { name, err := git.SymbolicFullName(fmt.Sprintf("%s@{upstream}", b.ShortName())) if err != nil { diff --git a/github/branch_test.go b/github/branch_test.go index 4fab1fa1ccc41f5bdc30065188c1dda10cf9e9d8..340e365f9b74cc19a93b436dbf1210f39472f418 100644 --- a/github/branch_test.go +++ b/github/branch_test.go @@ -10,6 +10,14 @@ func TestBranch_ShortName(t *testing.T) { assert.Equal(t, "master", b.ShortName()) } +func TestBranch_LongName(t *testing.T) { + b := Branch("refs/heads/master") + assert.Equal(t, "heads/master", b.LongName()) + + b = Branch("refs/remotes/origin/master") + assert.Equal(t, "origin/master", b.LongName()) +} + func TestBranch_RemoveName(t *testing.T) { b := Branch("refs/remotes/origin/master") assert.Equal(t, "origin", b.RemoteName()) diff --git a/github/project.go b/github/project.go index b9104fdb12e663a5d9c690da307261b448044a42..918fbc9ac673281168be746f457c99dad2dfeb1a 100644 --- a/github/project.go +++ b/github/project.go @@ -95,6 +95,32 @@ func NewProjectFromURL(url *url.URL) (*Project, error) { return &Project{Name: name, Owner: parts[1]}, nil } +// NewProjectFromURL creates a new Project from a string +// +// If the string is in the format of OWNER/NAME, it's split and used as the owner and name of the Project. +// Otherwise the string is used as the name of the Project and the current user is used as the owner. +// If the string is empty, the current dir name is used as the name of the Project. +func NewProjectFromString(nameAndOwner string) *Project { + var name, owner string + if strings.Contains(nameAndOwner, "/") { + result := strings.SplitN(nameAndOwner, "/", 2) + owner = result[0] + name = result[1] + } else { + name = nameAndOwner + } + + if owner == "" { + owner = CurrentConfig().FetchUser() + } + + if name == "" { + name, _ = utils.DirName() + } + + return &Project{Name: name, Owner: owner} +} + func NewProjectFromNameAndOwner(name, owner string) *Project { if strings.Contains(owner, "/") { result := strings.SplitN(owner, "/", 2) diff --git a/github/project_test.go b/github/project_test.go index ac9fb2b5d4fe84bab7c14f04a09cd9d2d03a305a..6e3ea5a55be15967bb1da2510cb51637ed027f29 100644 --- a/github/project_test.go +++ b/github/project_test.go @@ -30,6 +30,28 @@ func TestNewProjectOwnerAndName(t *testing.T) { assert.Equal(t, "gh/foo", project.Name) } +func TestNewProjectFromString(t *testing.T) { + DefaultConfigFile = "./test_support/clone_gh" + config := Config{User: "jingweno", Token: "123"} + SaveConfig(&config) + defer os.RemoveAll(filepath.Dir(DefaultConfigFile)) + + project := NewProjectFromString("jingweno/gh") + + assert.Equal(t, "jingweno", project.Owner) + assert.Equal(t, "gh", project.Name) + + project = NewProjectFromString("gh") + + assert.Equal(t, "jingweno", project.Owner) + assert.Equal(t, "gh", project.Name) + + project = NewProjectFromString("jingweno/gh/foo") + + assert.Equal(t, "jingweno", project.Owner) + assert.Equal(t, "gh/foo", project.Name) +} + func TestWebURL(t *testing.T) { project := Project{"foo", "bar"} url := project.WebURL("", "", "baz")