X Tutup
Skip to content

Commit e2a825e

Browse files
committed
Auto-fork on pr create if no pushable target found
1 parent 2aaffc6 commit e2a825e

File tree

3 files changed

+113
-11
lines changed

3 files changed

+113
-11
lines changed

api/queries_repo.go

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ import (
1414
type Repository struct {
1515
ID string
1616
Name string
17-
Owner struct {
18-
Login string
19-
}
17+
Owner RepositoryOwner
2018

2119
IsPrivate bool
2220
HasIssuesEnabled bool
@@ -31,6 +29,11 @@ type Repository struct {
3129
Parent *Repository
3230
}
3331

32+
// RepositoryOwner is the owner of a GitHub repository
33+
type RepositoryOwner struct {
34+
Login string
35+
}
36+
3437
// RepoOwner is the login name of the owner
3538
func (r Repository) RepoOwner() string {
3639
return r.Owner.Login
@@ -182,3 +185,32 @@ func RepoNetwork(client *Client, repos []Repo) (RepoNetworkResult, error) {
182185
}
183186
return result, nil
184187
}
188+
189+
// repositoryV3 is the repository result from GitHub API v3
190+
type repositoryV3 struct {
191+
NodeID string
192+
Name string
193+
Owner struct {
194+
Login string
195+
}
196+
}
197+
198+
// ForkRepo forks the repository on GitHub and returns the new repository
199+
func ForkRepo(client *Client, repo Repo) (*Repository, error) {
200+
path := fmt.Sprintf("repos/%s/%s/forks", repo.RepoOwner(), repo.RepoName())
201+
body := bytes.NewBufferString(`{}`)
202+
result := repositoryV3{}
203+
err := client.REST("POST", path, body, &result)
204+
if err != nil {
205+
return nil, err
206+
}
207+
208+
return &Repository{
209+
ID: result.NodeID,
210+
Name: result.Name,
211+
Owner: RepositoryOwner{
212+
Login: result.Owner.Login,
213+
},
214+
ViewerPermission: "WRITE",
215+
}, nil
216+
}

command/pr_create.go

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net/url"
66
"sort"
77
"strings"
8+
"time"
89

910
"github.com/github/gh-cli/api"
1011
"github.com/github/gh-cli/context"
@@ -51,27 +52,61 @@ func prCreate(cmd *cobra.Command, _ []string) error {
5152
baseBranch = baseRepo.DefaultBranchRef.Name
5253
}
5354

55+
didForkRepo := false
56+
var headRemote *context.Remote
5457
headRepo, err := repoContext.HeadRepo()
5558
if err != nil {
56-
// TODO: auto-fork repository and add new git remote
57-
return errors.Wrap(err, "could not determine the head repository")
59+
if baseRepo.IsPrivate {
60+
return fmt.Errorf("cannot write to private repository '%s/%s'", baseRepo.RepoOwner(), baseRepo.RepoName())
61+
}
62+
headRepo, err = api.ForkRepo(client, baseRepo)
63+
if err != nil {
64+
return fmt.Errorf("error forking repo: %w", err)
65+
}
66+
didForkRepo = true
67+
// TODO: support non-HTTPS git remote URLs
68+
baseRepoURL := fmt.Sprintf("https://github.com/%s/%s.git", baseRepo.RepoOwner(), baseRepo.RepoName())
69+
headRepoURL := fmt.Sprintf("https://github.com/%s/%s.git", headRepo.RepoOwner(), headRepo.RepoName())
70+
// TODO: figure out what to name the new git remote
71+
gitRemote, err := git.AddRemote("fork", baseRepoURL, headRepoURL)
72+
if err != nil {
73+
return fmt.Errorf("error adding remote: %w", err)
74+
}
75+
headRemote = &context.Remote{
76+
Remote: gitRemote,
77+
Owner: headRepo.RepoOwner(),
78+
Repo: headRepo.RepoName(),
79+
}
5880
}
5981

6082
if headBranch == baseBranch && isSameRepo(baseRepo, headRepo) {
6183
return fmt.Errorf("must be on a branch named differently than %q", baseBranch)
6284
}
6385

64-
headRemote, err := repoContext.RemoteForRepo(headRepo)
65-
if err != nil {
66-
return errors.Wrap(err, "git remote not found for head repository")
86+
if headRemote == nil {
87+
headRemote, err = repoContext.RemoteForRepo(headRepo)
88+
if err != nil {
89+
return errors.Wrap(err, "git remote not found for head repository")
90+
}
6791
}
6892

6993
if ucc, err := git.UncommittedChangeCount(); err == nil && ucc > 0 {
7094
fmt.Fprintf(cmd.ErrOrStderr(), "Warning: %s\n", utils.Pluralize(ucc, "uncommitted change"))
7195
}
72-
// TODO: respect existing upstream configuration of the current branch
73-
if err = git.Push(headRemote.Name, fmt.Sprintf("HEAD:%s", headBranch)); err != nil {
74-
return err
96+
pushTries := 0
97+
maxPushTries := 3
98+
for {
99+
// TODO: respect existing upstream configuration of the current branch
100+
if err := git.Push(headRemote.Name, fmt.Sprintf("HEAD:%s", headBranch)); err != nil {
101+
if didForkRepo && pushTries < maxPushTries {
102+
pushTries++
103+
// first wait 2 seconds after forking, then 4s, then 6s
104+
time.Sleep(time.Duration(2*pushTries) * time.Second)
105+
continue
106+
}
107+
return err
108+
}
109+
break
75110
}
76111

77112
isWeb, err := cmd.Flags().GetBool("web")

git/remote.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package git
22

33
import (
44
"net/url"
5+
"os/exec"
56
"regexp"
67
"strings"
8+
9+
"github.com/github/gh-cli/utils"
710
)
811

912
var remoteRE = regexp.MustCompile(`(.+)\s+(.+)\s+\((push|fetch)\)`)
@@ -67,3 +70,35 @@ func parseRemotes(gitRemotes []string) (remotes RemoteSet) {
6770
}
6871
return
6972
}
73+
74+
// AddRemote adds a new git remote. The initURL is the remote URL with which the
75+
// automatic fetch is made and finalURL, if non-blank, is set as the remote URL
76+
// after the fetch.
77+
func AddRemote(name, initURL, finalURL string) (*Remote, error) {
78+
addCmd := exec.Command("git", "remote", "add", "-f", name, initURL)
79+
err := utils.PrepareCmd(addCmd).Run()
80+
if err != nil {
81+
return nil, err
82+
}
83+
84+
if finalURL == "" {
85+
finalURL = initURL
86+
} else {
87+
setCmd := exec.Command("git", "remote", "set-url", name, finalURL)
88+
err := utils.PrepareCmd(setCmd).Run()
89+
if err != nil {
90+
return nil, err
91+
}
92+
}
93+
94+
finalURLParsed, err := url.Parse(initURL)
95+
if err != nil {
96+
return nil, err
97+
}
98+
99+
return &Remote{
100+
Name: name,
101+
FetchURL: finalURLParsed,
102+
PushURL: finalURLParsed,
103+
}, nil
104+
}

0 commit comments

Comments
 (0)
X Tutup