Retry fetching repo from template

Fixes #7055
This commit is contained in:
Heath Stewart 2023-03-13 15:15:38 -07:00
parent 3b23978114
commit 36436cb8b8
No known key found for this signature in database
2 changed files with 129 additions and 22 deletions

View file

@ -9,8 +9,10 @@ import (
"path/filepath"
"sort"
"strings"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/cenkalti/backoff/v4"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/config"
@ -21,6 +23,10 @@ import (
"github.com/spf13/cobra"
)
type errWithExitCode interface {
ExitCode() int
}
type iprompter interface {
Input(string, string) (string, error)
Select(string, string, []string) (int, error)
@ -33,6 +39,7 @@ type CreateOptions struct {
Config func() (config.Config, error)
IO *iostreams.IOStreams
Prompter iprompter
BackOff backoff.BackOff
Name string
Description string
@ -328,7 +335,6 @@ func createFromScratch(opts *CreateOptions) error {
InitReadme: opts.AddReadme,
}
var templateRepoMainBranch string
if opts.Template != "" {
var templateRepo ghrepo.Interface
apiClient := api.NewClientFromHTTP(httpClient)
@ -352,7 +358,6 @@ func createFromScratch(opts *CreateOptions) error {
}
input.TemplateRepositoryID = repo.ID
templateRepoMainBranch = repo.DefaultBranchRef.Name
}
repo, err := repoCreate(httpClient, repoToCreate.RepoHost(), input)
@ -387,17 +392,12 @@ func createFromScratch(opts *CreateOptions) error {
remoteURL := ghrepo.FormatRemoteURL(repo, protocol)
if opts.LicenseTemplate == "" && opts.GitIgnoreTemplate == "" {
if opts.LicenseTemplate == "" && opts.GitIgnoreTemplate == "" && opts.Template == "" {
// cloning empty repository or template
checkoutBranch := ""
if opts.Template != "" {
// use the template's default branch
checkoutBranch = templateRepoMainBranch
}
if err := localInit(opts.GitClient, remoteURL, repo.RepoName(), checkoutBranch); err != nil {
if err := localInit(opts.GitClient, remoteURL, repo.RepoName()); err != nil {
return err
}
} else if _, err := opts.GitClient.Clone(context.Background(), remoteURL, []string{}); err != nil {
} else if err := cloneWithRetry(opts, remoteURL); err != nil {
return err
}
}
@ -563,6 +563,25 @@ func createFromLocal(opts *CreateOptions) error {
return nil
}
func cloneWithRetry(opts *CreateOptions, remoteURL string) error {
// Allow injecting alternative BackOff in tests.
if opts.BackOff == nil {
opts.BackOff = backoff.NewConstantBackOff(3 * time.Second)
}
ctx := context.Background()
return backoff.Retry(func() error {
_, err := opts.GitClient.Clone(ctx, remoteURL, []string{})
var execError errWithExitCode
if errors.As(err, &execError) && execError.ExitCode() == 128 {
return err
}
return backoff.Permanent(err)
}, backoff.WithContext(backoff.WithMaxRetries(opts.BackOff, 3), ctx))
}
func sourceInit(gitClient *git.Client, io *iostreams.IOStreams, remoteURL, baseRemote string) error {
cs := io.ColorScheme()
remoteAdd, err := gitClient.Command(context.Background(), "remote", "add", baseRemote, remoteURL)
@ -620,7 +639,7 @@ func isLocalRepo(gitClient *git.Client) (bool, error) {
}
// clone the checkout branch to specified path
func localInit(gitClient *git.Client, remoteURL, path, checkoutBranch string) error {
func localInit(gitClient *git.Client, remoteURL, path string) error {
ctx := context.Background()
gitInit, err := gitClient.Command(ctx, "init", path)
if err != nil {
@ -644,17 +663,7 @@ func localInit(gitClient *git.Client, remoteURL, path, checkoutBranch string) er
return err
}
if checkoutBranch == "" {
return nil
}
refspec := fmt.Sprintf("+refs/heads/%[1]s:refs/remotes/origin/%[1]s", checkoutBranch)
err = gc.Fetch(ctx, "origin", refspec)
if err != nil {
return err
}
return gc.CheckoutBranch(ctx, checkoutBranch)
return nil
}
func interactiveGitIgnore(client *http.Client, hostname string, prompter iprompter) (string, error) {

View file

@ -119,6 +119,18 @@ func TestNewCmdCreate(t *testing.T) {
IncludeAllBranches: true,
},
},
{
name: "template with .gitignore",
cli: "template-repo --template mytemplate --gitignore ../.gitignore --public",
wantsErr: true,
errMsg: ".gitignore and license templates are not added when template is provided",
},
{
name: "template with license",
cli: "template-repo --template mytemplate --license ../.license --public",
wantsErr: true,
errMsg: ".gitignore and license templates are not added when template is provided",
},
}
for _, tt := range tests {
@ -557,6 +569,92 @@ func Test_createRun(t *testing.T) {
},
wantStdout: "https://github.com/OWNER/REPO\n",
},
{
name: "noninteractive clone from scratch",
opts: &CreateOptions{
Interactive: false,
Name: "REPO",
Visibility: "PRIVATE",
Clone: true,
},
tty: false,
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`mutation RepositoryCreate\b`),
httpmock.StringResponse(`
{
"data": {
"createRepository": {
"repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login":"OWNER"},
"url": "https://github.com/OWNER/REPO"
}
}
}
}`))
},
execStubs: func(cs *run.CommandStubber) {
cs.Register(`git init REPO`, 0, "")
cs.Register(`git -C REPO remote add origin https://github.com/OWNER/REPO`, 0, "")
},
wantStdout: "https://github.com/OWNER/REPO\n",
},
{
name: "noninteractive create from template",
opts: &CreateOptions{
Interactive: false,
Name: "REPO",
Visibility: "PRIVATE",
Clone: true,
Template: "mytemplate",
},
tty: false,
httpStubs: func(reg *httpmock.Registry) {
// Test resolving repo owner from repo name only.
reg.Register(
httpmock.GraphQL(`query UserCurrent\b`),
httpmock.StringResponse(`{"data":{"viewer":{"login":"OWNER"}}}`))
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.GraphQLQuery(`{
"data": {
"repository": {
"id": "REPOID"
}
}
}`, func(s string, m map[string]interface{}) {
assert.Equal(t, "OWNER", m["owner"])
assert.Equal(t, "mytemplate", m["name"])
}),
)
reg.Register(
httpmock.GraphQL(`query UserCurrent\b`),
httpmock.StringResponse(`{"data":{"viewer":{"id":"OWNERID"}}}`))
reg.Register(
httpmock.GraphQL(`mutation CloneTemplateRepository\b`),
httpmock.GraphQLMutation(`
{
"data": {
"cloneTemplateRepository": {
"repository": {
"id": "REPOID",
"name": "REPO",
"owner": {"login":"OWNER"},
"url": "https://github.com/OWNER/REPO"
}
}
}
}`, func(m map[string]interface{}) {
assert.Equal(t, "REPOID", m["repositoryId"])
}))
},
execStubs: func(cs *run.CommandStubber) {
cs.Register(`git clone https://github.com/OWNER/REPO`, 0, "")
},
wantStdout: "https://github.com/OWNER/REPO\n",
},
}
for _, tt := range tests {
prompterMock := &prompter.PrompterMock{}