Auto-fork on pr create if no pushable target found

This commit is contained in:
Mislav Marohnić 2020-01-22 10:40:06 +01:00
parent 2aaffc69a2
commit e2a825effb
3 changed files with 113 additions and 11 deletions

View file

@ -14,9 +14,7 @@ import (
type Repository struct {
ID string
Name string
Owner struct {
Login string
}
Owner RepositoryOwner
IsPrivate bool
HasIssuesEnabled bool
@ -31,6 +29,11 @@ type Repository struct {
Parent *Repository
}
// RepositoryOwner is the owner of a GitHub repository
type RepositoryOwner struct {
Login string
}
// RepoOwner is the login name of the owner
func (r Repository) RepoOwner() string {
return r.Owner.Login
@ -182,3 +185,32 @@ func RepoNetwork(client *Client, repos []Repo) (RepoNetworkResult, error) {
}
return result, nil
}
// repositoryV3 is the repository result from GitHub API v3
type repositoryV3 struct {
NodeID string
Name string
Owner struct {
Login string
}
}
// ForkRepo forks the repository on GitHub and returns the new repository
func ForkRepo(client *Client, repo Repo) (*Repository, error) {
path := fmt.Sprintf("repos/%s/%s/forks", repo.RepoOwner(), repo.RepoName())
body := bytes.NewBufferString(`{}`)
result := repositoryV3{}
err := client.REST("POST", path, body, &result)
if err != nil {
return nil, err
}
return &Repository{
ID: result.NodeID,
Name: result.Name,
Owner: RepositoryOwner{
Login: result.Owner.Login,
},
ViewerPermission: "WRITE",
}, nil
}

View file

@ -5,6 +5,7 @@ import (
"net/url"
"sort"
"strings"
"time"
"github.com/github/gh-cli/api"
"github.com/github/gh-cli/context"
@ -51,27 +52,61 @@ func prCreate(cmd *cobra.Command, _ []string) error {
baseBranch = baseRepo.DefaultBranchRef.Name
}
didForkRepo := false
var headRemote *context.Remote
headRepo, err := repoContext.HeadRepo()
if err != nil {
// TODO: auto-fork repository and add new git remote
return errors.Wrap(err, "could not determine the head repository")
if baseRepo.IsPrivate {
return fmt.Errorf("cannot write to private repository '%s/%s'", baseRepo.RepoOwner(), baseRepo.RepoName())
}
headRepo, err = api.ForkRepo(client, baseRepo)
if err != nil {
return fmt.Errorf("error forking repo: %w", err)
}
didForkRepo = true
// TODO: support non-HTTPS git remote URLs
baseRepoURL := fmt.Sprintf("https://github.com/%s/%s.git", baseRepo.RepoOwner(), baseRepo.RepoName())
headRepoURL := fmt.Sprintf("https://github.com/%s/%s.git", headRepo.RepoOwner(), headRepo.RepoName())
// TODO: figure out what to name the new git remote
gitRemote, err := git.AddRemote("fork", baseRepoURL, headRepoURL)
if err != nil {
return fmt.Errorf("error adding remote: %w", err)
}
headRemote = &context.Remote{
Remote: gitRemote,
Owner: headRepo.RepoOwner(),
Repo: headRepo.RepoName(),
}
}
if headBranch == baseBranch && isSameRepo(baseRepo, headRepo) {
return fmt.Errorf("must be on a branch named differently than %q", baseBranch)
}
headRemote, err := repoContext.RemoteForRepo(headRepo)
if err != nil {
return errors.Wrap(err, "git remote not found for head repository")
if headRemote == nil {
headRemote, err = repoContext.RemoteForRepo(headRepo)
if err != nil {
return errors.Wrap(err, "git remote not found for head repository")
}
}
if ucc, err := git.UncommittedChangeCount(); err == nil && ucc > 0 {
fmt.Fprintf(cmd.ErrOrStderr(), "Warning: %s\n", utils.Pluralize(ucc, "uncommitted change"))
}
// TODO: respect existing upstream configuration of the current branch
if err = git.Push(headRemote.Name, fmt.Sprintf("HEAD:%s", headBranch)); err != nil {
return err
pushTries := 0
maxPushTries := 3
for {
// TODO: respect existing upstream configuration of the current branch
if err := git.Push(headRemote.Name, fmt.Sprintf("HEAD:%s", headBranch)); err != nil {
if didForkRepo && pushTries < maxPushTries {
pushTries++
// first wait 2 seconds after forking, then 4s, then 6s
time.Sleep(time.Duration(2*pushTries) * time.Second)
continue
}
return err
}
break
}
isWeb, err := cmd.Flags().GetBool("web")

View file

@ -2,8 +2,11 @@ package git
import (
"net/url"
"os/exec"
"regexp"
"strings"
"github.com/github/gh-cli/utils"
)
var remoteRE = regexp.MustCompile(`(.+)\s+(.+)\s+\((push|fetch)\)`)
@ -67,3 +70,35 @@ func parseRemotes(gitRemotes []string) (remotes RemoteSet) {
}
return
}
// AddRemote adds a new git remote. The initURL is the remote URL with which the
// automatic fetch is made and finalURL, if non-blank, is set as the remote URL
// after the fetch.
func AddRemote(name, initURL, finalURL string) (*Remote, error) {
addCmd := exec.Command("git", "remote", "add", "-f", name, initURL)
err := utils.PrepareCmd(addCmd).Run()
if err != nil {
return nil, err
}
if finalURL == "" {
finalURL = initURL
} else {
setCmd := exec.Command("git", "remote", "set-url", name, finalURL)
err := utils.PrepareCmd(setCmd).Run()
if err != nil {
return nil, err
}
}
finalURLParsed, err := url.Parse(initURL)
if err != nil {
return nil, err
}
return &Remote{
Name: name,
FetchURL: finalURLParsed,
PushURL: finalURLParsed,
}, nil
}