Merge pull request #7080 from heaths/issue7055

Retry fetching repo from template
This commit is contained in:
Nate Smith 2023-03-29 16:09:51 -07:00 committed by GitHub
commit 72f64cc634
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 146 additions and 20 deletions

View file

@ -1,16 +1,20 @@
package create
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"os/exec"
"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 +25,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 +41,7 @@ type CreateOptions struct {
Config func() (config.Config, error)
IO *iostreams.IOStreams
Prompter iprompter
BackOff backoff.BackOff
Name string
Description string
@ -387,17 +396,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, templateRepoMainBranch); err != nil {
return err
}
}
@ -563,6 +567,33 @@ func createFromLocal(opts *CreateOptions) error {
return nil
}
func cloneWithRetry(opts *CreateOptions, remoteURL, branch string) error {
// Allow injecting alternative BackOff in tests.
if opts.BackOff == nil {
opts.BackOff = backoff.NewConstantBackOff(3 * time.Second)
}
var args []string
if branch != "" {
args = append(args, "--branch", branch)
}
ctx := context.Background()
return backoff.Retry(func() error {
stderr := &bytes.Buffer{}
_, err := opts.GitClient.Clone(ctx, remoteURL, args, git.WithStderr(stderr))
var execError errWithExitCode
if errors.As(err, &execError) && execError.ExitCode() == 128 {
return err
} else {
_, _ = io.Copy(opts.IO.ErrOut, stderr)
}
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 +651,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 +675,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

@ -6,6 +6,7 @@ import (
"net/http"
"testing"
"github.com/cenkalti/backoff/v4"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/prompter"
@ -119,6 +120,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 +570,98 @@ 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 with retry",
opts: &CreateOptions{
Interactive: false,
Name: "REPO",
Visibility: "PRIVATE",
Clone: true,
Template: "mytemplate",
BackOff: &backoff.ZeroBackOff{},
},
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",
"defaultBranchRef": {
"name": "main"
}
}
}
}`, 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) {
// fatal: Remote branch main not found in upstream origin
cs.Register(`git clone --branch main https://github.com/OWNER/REPO`, 128, "")
cs.Register(`git clone --branch main https://github.com/OWNER/REPO`, 0, "")
},
wantStdout: "https://github.com/OWNER/REPO\n",
},
}
for _, tt := range tests {
prompterMock := &prompter.PrompterMock{}