Merge pull request #7080 from heaths/issue7055
Retry fetching repo from template
This commit is contained in:
commit
72f64cc634
2 changed files with 146 additions and 20 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue