Auto-fork on pr create if no pushable target found
This commit is contained in:
parent
2aaffc69a2
commit
e2a825effb
3 changed files with 113 additions and 11 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue