Merge pull request #9905 from cli/9904-gh-repo-create-to-work-on-bare-repos
Support bare repo creation
This commit is contained in:
commit
9decf1b526
3 changed files with 217 additions and 16 deletions
35
acceptance/testdata/repo/repo-create-bare.txtar
vendored
Normal file
35
acceptance/testdata/repo/repo-create-bare.txtar
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# It's unclear what we want to do with these acceptance tests beyond our GHEC discovery, so skip new ones by default
|
||||
skip
|
||||
|
||||
# Set up env var
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
|
||||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Initialise a local repository with two branches
|
||||
# We expect a bare repo to have all refs pushed with --mirror
|
||||
mkdir ${REPO}
|
||||
cd ${REPO}
|
||||
exec git init
|
||||
exec git checkout -b feature-1
|
||||
exec git commit --allow-empty -m 'Empty Commit 1'
|
||||
|
||||
exec git checkout -b feature-2
|
||||
exec git commit --allow-empty -m 'Empty Commit 2'
|
||||
|
||||
# Clone a bare repo from that local repo
|
||||
cd ..
|
||||
exec git clone --bare ${REPO} ${REPO}-bare
|
||||
cd ${REPO}-bare
|
||||
|
||||
# Create a GitHub repository from that bare repo
|
||||
exec gh repo create ${ORG}/${REPO} --private --source . --push --remote bare
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes ${ORG}/${REPO}
|
||||
|
||||
# Check the remote repo has both branches
|
||||
exec gh api /repos/${ORG}/${REPO}/branches
|
||||
stdout 'feature-1'
|
||||
stdout 'feature-2'
|
||||
|
|
@ -94,7 +94,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
To create a remote repository from an existing local repository, specify the source directory with %[1]s--source%[1]s.
|
||||
By default, the remote repository name will be the name of the source directory.
|
||||
|
||||
Pass %[1]s--push%[1]s to push any local commits to the new repository.
|
||||
Pass %[1]s--push%[1]s to push any local commits to the new repository. If the repo is bare, this will mirror all refs.
|
||||
|
||||
For language or platform .gitignore templates to use with %[1]s--gitignore%[1]s, <https://github.com/github/gitignore>.
|
||||
|
||||
|
|
@ -556,11 +556,11 @@ func createFromLocal(opts *CreateOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
isRepo, err := isLocalRepo(opts.GitClient)
|
||||
repoType, err := localRepoType(opts.GitClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isRepo {
|
||||
if repoType == unknown {
|
||||
if repoPath == "." {
|
||||
return fmt.Errorf("current directory is not a git repository. Run `git init` to initialize it")
|
||||
}
|
||||
|
|
@ -652,22 +652,43 @@ func createFromLocal(opts *CreateOptions) error {
|
|||
|
||||
// don't prompt for push if there are no commits
|
||||
if opts.Interactive && committed {
|
||||
msg := fmt.Sprintf("Would you like to push commits from the current branch to %q?", baseRemote)
|
||||
if repoType == bare {
|
||||
msg = fmt.Sprintf("Would you like to mirror all refs to %q?", baseRemote)
|
||||
}
|
||||
|
||||
var err error
|
||||
opts.Push, err = opts.Prompter.Confirm(fmt.Sprintf("Would you like to push commits from the current branch to %q?", baseRemote), true)
|
||||
opts.Push, err = opts.Prompter.Confirm(msg, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Push {
|
||||
if opts.Push && repoType == working {
|
||||
err := opts.GitClient.Push(context.Background(), baseRemote, "HEAD")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isTTY {
|
||||
fmt.Fprintf(stdout, "%s Pushed commits to %s\n", cs.SuccessIcon(), remoteURL)
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Push && repoType == bare {
|
||||
cmd, err := opts.GitClient.AuthenticatedCommand(context.Background(), "push", baseRemote, "--mirror")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isTTY {
|
||||
fmt.Fprintf(stdout, "%s Mirrored all refs to %s\n", cs.SuccessIcon(), remoteURL)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -736,22 +757,34 @@ func hasCommits(gitClient *git.Client) (bool, error) {
|
|||
return false, nil
|
||||
}
|
||||
|
||||
// check if path is the top level directory of a git repo
|
||||
func isLocalRepo(gitClient *git.Client) (bool, error) {
|
||||
type repoType int
|
||||
|
||||
const (
|
||||
unknown repoType = iota
|
||||
working
|
||||
bare
|
||||
)
|
||||
|
||||
func localRepoType(gitClient *git.Client) (repoType, error) {
|
||||
projectDir, projectDirErr := gitClient.GitDir(context.Background())
|
||||
if projectDirErr != nil {
|
||||
var execError *exec.ExitError
|
||||
var execError errWithExitCode
|
||||
if errors.As(projectDirErr, &execError) {
|
||||
if exitCode := int(execError.ExitCode()); exitCode == 128 {
|
||||
return false, nil
|
||||
return unknown, nil
|
||||
}
|
||||
return false, projectDirErr
|
||||
return unknown, projectDirErr
|
||||
}
|
||||
}
|
||||
if projectDir != ".git" {
|
||||
return false, nil
|
||||
|
||||
switch projectDir {
|
||||
case ".":
|
||||
return bare, nil
|
||||
case ".git":
|
||||
return working, nil
|
||||
default:
|
||||
return unknown, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// clone the checkout branch to specified path
|
||||
|
|
|
|||
|
|
@ -443,6 +443,74 @@ func Test_createRun(t *testing.T) {
|
|||
},
|
||||
wantStdout: "✓ Created repository OWNER/REPO on GitHub\n https://github.com/OWNER/REPO\n",
|
||||
},
|
||||
{
|
||||
name: "interactive with existing bare repository public and push",
|
||||
opts: &CreateOptions{Interactive: true},
|
||||
tty: true,
|
||||
promptStubs: func(p *prompter.PrompterMock) {
|
||||
p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) {
|
||||
switch message {
|
||||
case "Add a remote?":
|
||||
return true, nil
|
||||
case `Would you like to mirror all refs to "origin"?`:
|
||||
return true, nil
|
||||
default:
|
||||
return false, fmt.Errorf("unexpected confirm prompt: %s", message)
|
||||
}
|
||||
}
|
||||
p.InputFunc = func(message, defaultValue string) (string, error) {
|
||||
switch message {
|
||||
case "Path to local repository":
|
||||
return defaultValue, nil
|
||||
case "Repository name":
|
||||
return "REPO", nil
|
||||
case "Description":
|
||||
return "my new repo", nil
|
||||
case "What should the new remote be called?":
|
||||
return defaultValue, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unexpected input prompt: %s", message)
|
||||
}
|
||||
}
|
||||
p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
|
||||
switch message {
|
||||
case "What would you like to do?":
|
||||
return prompter.IndexFor(options, "Push an existing local repository to GitHub")
|
||||
case "Visibility":
|
||||
return prompter.IndexFor(options, "Private")
|
||||
default:
|
||||
return 0, fmt.Errorf("unexpected select prompt: %s", message)
|
||||
}
|
||||
}
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data":{"viewer":{"login":"someuser","organizations":{"nodes": []}}}}`))
|
||||
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 -C . rev-parse --git-dir`, 0, ".")
|
||||
cs.Register(`git -C . rev-parse HEAD`, 0, "commithash")
|
||||
cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "")
|
||||
cs.Register(`git -C . push origin --mirror`, 0, "")
|
||||
},
|
||||
wantStdout: "✓ Created repository OWNER/REPO on GitHub\n https://github.com/OWNER/REPO\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Mirrored all refs to https://github.com/OWNER/REPO.git\n",
|
||||
},
|
||||
{
|
||||
name: "interactive with existing repository public add remote and push",
|
||||
opts: &CreateOptions{Interactive: true},
|
||||
|
|
@ -696,6 +764,71 @@ func Test_createRun(t *testing.T) {
|
|||
},
|
||||
wantStdout: "https://github.com/OWNER/REPO\n",
|
||||
},
|
||||
{
|
||||
name: "noninteractive create bare from source and push",
|
||||
opts: &CreateOptions{
|
||||
Interactive: false,
|
||||
Source: ".",
|
||||
Push: true,
|
||||
Name: "REPO",
|
||||
Visibility: "PRIVATE",
|
||||
},
|
||||
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 -C . rev-parse --git-dir`, 0, ".")
|
||||
cs.Register(`git -C . rev-parse HEAD`, 0, "commithash")
|
||||
cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "")
|
||||
cs.Register(`git -C . push origin --mirror`, 0, "")
|
||||
},
|
||||
wantStdout: "https://github.com/OWNER/REPO\n",
|
||||
},
|
||||
{
|
||||
name: "noninteractive create from cwd that isn't a git repo",
|
||||
opts: &CreateOptions{
|
||||
Interactive: false,
|
||||
Source: ".",
|
||||
Name: "REPO",
|
||||
Visibility: "PRIVATE",
|
||||
},
|
||||
tty: false,
|
||||
execStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git -C . rev-parse --git-dir`, 128, "")
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "current directory is not a git repository. Run `git init` to initialize it",
|
||||
},
|
||||
{
|
||||
name: "noninteractive create from cwd that isn't a git repo",
|
||||
opts: &CreateOptions{
|
||||
Interactive: false,
|
||||
Source: "some-dir",
|
||||
Name: "REPO",
|
||||
Visibility: "PRIVATE",
|
||||
},
|
||||
tty: false,
|
||||
execStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git -C some-dir rev-parse --git-dir`, 128, "")
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "some-dir is not a git repository. Run `git -C \"some-dir\" init` to initialize it",
|
||||
},
|
||||
{
|
||||
name: "noninteractive clone from scratch",
|
||||
opts: &CreateOptions{
|
||||
|
|
@ -856,11 +989,11 @@ func Test_createRun(t *testing.T) {
|
|||
defer reg.Verify(t)
|
||||
err := createRun(tt.opts)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.errMsg, err.Error())
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.errMsg)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantStdout, stdout.String())
|
||||
assert.Equal(t, "", stderr.String())
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue