Create git client (#6354)
This commit is contained in:
parent
4c3b123db6
commit
2944f7c3ab
16 changed files with 1223 additions and 828 deletions
611
git/client.go
Normal file
611
git/client.go
Normal file
|
|
@ -0,0 +1,611 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
"github.com/cli/safeexec"
|
||||
)
|
||||
|
||||
var remoteRE = regexp.MustCompile(`(.+)\s+(.+)\s+\((push|fetch)\)`)
|
||||
|
||||
// ErrNotOnAnyBranch indicates that the user is in detached HEAD state.
|
||||
var ErrNotOnAnyBranch = errors.New("git: not on any branch")
|
||||
|
||||
type NotInstalled struct {
|
||||
message string
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *NotInstalled) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (e *NotInstalled) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
type GitError struct {
|
||||
stderr string
|
||||
err error
|
||||
}
|
||||
|
||||
func (ge *GitError) Error() string {
|
||||
stderr := ge.stderr
|
||||
if stderr == "" {
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(ge.err, &exitError) {
|
||||
stderr = string(exitError.Stderr)
|
||||
}
|
||||
}
|
||||
if stderr == "" {
|
||||
return fmt.Sprintf("failed to run git: %v", ge.err)
|
||||
}
|
||||
return fmt.Sprintf("failed to run git: %s", stderr)
|
||||
}
|
||||
|
||||
func (ge *GitError) Unwrap() error {
|
||||
return ge.err
|
||||
}
|
||||
|
||||
type gitCommand struct {
|
||||
*exec.Cmd
|
||||
}
|
||||
|
||||
// This is a hack in order to not break the hundreds of
|
||||
// existing tests that rely on `run.PrepareCmd` to be invoked.
|
||||
func (gc *gitCommand) Run() error {
|
||||
return run.PrepareCmd(gc.Cmd).Run()
|
||||
}
|
||||
|
||||
// This is a hack in order to not break the hundreds of
|
||||
// existing tests that rely on `run.PrepareCmd` to be invoked.
|
||||
func (gc *gitCommand) Output() ([]byte, error) {
|
||||
return run.PrepareCmd(gc.Cmd).Output()
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
GhPath string
|
||||
RepoDir string
|
||||
GitPath string
|
||||
Stderr io.Writer
|
||||
Stdin io.Reader
|
||||
Stdout io.Writer
|
||||
|
||||
commandContext func(ctx context.Context, name string, args ...string) *exec.Cmd
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (c *Client) Command(ctx context.Context, args ...string) (*gitCommand, error) {
|
||||
if c.RepoDir != "" {
|
||||
args = append([]string{"-C", c.RepoDir}, args...)
|
||||
}
|
||||
commandContext := exec.CommandContext
|
||||
if c.commandContext != nil {
|
||||
commandContext = c.commandContext
|
||||
}
|
||||
var err error
|
||||
c.mu.Lock()
|
||||
if c.GitPath == "" {
|
||||
c.GitPath, err = resolveGitPath()
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd := commandContext(ctx, c.GitPath, args...)
|
||||
cmd.Stderr = c.Stderr
|
||||
cmd.Stdin = c.Stdin
|
||||
cmd.Stdout = c.Stdout
|
||||
return &gitCommand{cmd}, nil
|
||||
}
|
||||
|
||||
func resolveGitPath() (string, error) {
|
||||
path, err := safeexec.LookPath("git")
|
||||
if err != nil {
|
||||
if errors.Is(err, exec.ErrNotFound) {
|
||||
programName := "git"
|
||||
if runtime.GOOS == "windows" {
|
||||
programName = "Git for Windows"
|
||||
}
|
||||
return "", &NotInstalled{
|
||||
message: fmt.Sprintf("unable to find git executable in PATH; please install %s before retrying", programName),
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// AuthenticatedCommand is a wrapper around Command that included configuration to use gh
|
||||
// as the credential helper for git.
|
||||
func (c *Client) AuthenticatedCommand(ctx context.Context, args ...string) (*gitCommand, error) {
|
||||
preArgs := []string{}
|
||||
preArgs = append(preArgs, "-c", "credential.helper=")
|
||||
if c.GhPath == "" {
|
||||
// Assumes that gh is in PATH.
|
||||
c.GhPath = "gh"
|
||||
}
|
||||
credHelper := fmt.Sprintf("!%q auth git-credential", c.GhPath)
|
||||
preArgs = append(preArgs, "-c", fmt.Sprintf("credential.helper=%s", credHelper))
|
||||
args = append(preArgs, args...)
|
||||
return c.Command(ctx, args...)
|
||||
}
|
||||
|
||||
func (c *Client) Remotes(ctx context.Context) (RemoteSet, error) {
|
||||
remoteArgs := []string{"remote", "-v"}
|
||||
remoteCmd, err := c.Command(ctx, remoteArgs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remoteOut, remoteErr := remoteCmd.Output()
|
||||
if remoteErr != nil {
|
||||
return nil, &GitError{err: remoteErr}
|
||||
}
|
||||
|
||||
configArgs := []string{"config", "--get-regexp", `^remote\..*\.gh-resolved$`}
|
||||
configCmd, err := c.Command(ctx, configArgs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configOut, configErr := configCmd.Output()
|
||||
if configErr != nil {
|
||||
return nil, &GitError{err: configErr}
|
||||
}
|
||||
|
||||
remotes := parseRemotes(outputLines(remoteOut))
|
||||
populateResolvedRemotes(remotes, outputLines(configOut))
|
||||
sort.Sort(remotes)
|
||||
return remotes, nil
|
||||
}
|
||||
|
||||
func (c *Client) AddRemote(ctx context.Context, name, urlStr string, trackingBranches []string) (*Remote, error) {
|
||||
args := []string{"remote", "add"}
|
||||
for _, branch := range trackingBranches {
|
||||
args = append(args, "-t", branch)
|
||||
}
|
||||
args = append(args, "-f", name, urlStr)
|
||||
//TODO: Use AuthenticatedCommand
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var urlParsed *url.URL
|
||||
if strings.HasPrefix(urlStr, "https") {
|
||||
urlParsed, err = url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
urlParsed, err = ParseURL(urlStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
remote := &Remote{
|
||||
Name: name,
|
||||
FetchURL: urlParsed,
|
||||
PushURL: urlParsed,
|
||||
}
|
||||
return remote, nil
|
||||
}
|
||||
|
||||
func (c *Client) UpdateRemoteURL(ctx context.Context, name, url string) error {
|
||||
args := []string{"remote", "set-url", name, url}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (c *Client) SetRemoteResolution(ctx context.Context, name, resolution string) error {
|
||||
args := []string{"config", "--add", fmt.Sprintf("remote.%s.gh-resolved", name), resolution}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// CurrentBranch reads the checked-out branch for the git repository.
|
||||
func (c *Client) CurrentBranch(ctx context.Context) (string, error) {
|
||||
args := []string{"symbolic-ref", "--quiet", "HEAD"}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
errBuf := bytes.Buffer{}
|
||||
cmd.Stderr = &errBuf
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
if errBuf.Len() == 0 {
|
||||
return "", &GitError{err: err, stderr: "not on any branch"}
|
||||
}
|
||||
return "", &GitError{err: err, stderr: errBuf.String()}
|
||||
}
|
||||
branch := firstLine(out)
|
||||
return strings.TrimPrefix(branch, "refs/heads/"), nil
|
||||
}
|
||||
|
||||
// ShowRefs resolves fully-qualified refs to commit hashes.
|
||||
func (c *Client) ShowRefs(ctx context.Context, ref ...string) ([]Ref, error) {
|
||||
args := append([]string{"show-ref", "--verify", "--"}, ref...)
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, &GitError{err: err}
|
||||
}
|
||||
var refs []Ref
|
||||
for _, line := range outputLines(out) {
|
||||
parts := strings.SplitN(line, " ", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
refs = append(refs, Ref{
|
||||
Hash: parts[0],
|
||||
Name: parts[1],
|
||||
})
|
||||
}
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
func (c *Client) Config(ctx context.Context, name string) (string, error) {
|
||||
args := []string{"config", name}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
errBuf := bytes.Buffer{}
|
||||
cmd.Stderr = &errBuf
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if ok := errors.As(err, &exitError); ok && exitError.Error() == "1" {
|
||||
return "", &GitError{err: err, stderr: fmt.Sprintf("unknown config key %s", name)}
|
||||
}
|
||||
return "", &GitError{err: err, stderr: errBuf.String()}
|
||||
}
|
||||
return firstLine(out), nil
|
||||
}
|
||||
|
||||
func (c *Client) UncommittedChangeCount(ctx context.Context) (int, error) {
|
||||
args := []string{"status", "--porcelain"}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, &GitError{err: err}
|
||||
}
|
||||
lines := strings.Split(string(out), "\n")
|
||||
count := 0
|
||||
for _, l := range lines {
|
||||
if l != "" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (c *Client) Commits(ctx context.Context, baseRef, headRef string) ([]*Commit, error) {
|
||||
args := []string{"-c", "log.ShowSignature=false", "log", "--pretty=format:%H,%s", "--cherry", fmt.Sprintf("%s...%s", baseRef, headRef)}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, &GitError{err: err}
|
||||
}
|
||||
commits := []*Commit{}
|
||||
sha := 0
|
||||
title := 1
|
||||
for _, line := range outputLines(out) {
|
||||
split := strings.SplitN(line, ",", 2)
|
||||
if len(split) != 2 {
|
||||
continue
|
||||
}
|
||||
commits = append(commits, &Commit{
|
||||
Sha: split[sha],
|
||||
Title: split[title],
|
||||
})
|
||||
}
|
||||
if len(commits) == 0 {
|
||||
return nil, fmt.Errorf("could not find any commits between %s and %s", baseRef, headRef)
|
||||
}
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
func (c *Client) lookupCommit(ctx context.Context, sha, format string) ([]byte, error) {
|
||||
args := []string{"-c", "log.ShowSignature=false", "show", "-s", "--pretty=format:" + format, sha}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, &GitError{err: err}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *Client) LastCommit(ctx context.Context) (*Commit, error) {
|
||||
output, err := c.lookupCommit(ctx, "HEAD", "%H,%s")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idx := bytes.IndexByte(output, ',')
|
||||
return &Commit{
|
||||
Sha: string(output[0:idx]),
|
||||
Title: strings.TrimSpace(string(output[idx+1:])),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) CommitBody(ctx context.Context, sha string) (string, error) {
|
||||
output, err := c.lookupCommit(ctx, sha, "%b")
|
||||
return string(output), err
|
||||
}
|
||||
|
||||
// Push publishes a git ref to a remote and sets up upstream configuration.
|
||||
func (c *Client) Push(ctx context.Context, remote string, ref string) error {
|
||||
args := []string{"push", "--set-upstream", remote, ref}
|
||||
//TODO: Use AuthenticatedCommand
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// ReadBranchConfig parses the `branch.BRANCH.(remote|merge)` part of git config.
|
||||
func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (cfg BranchConfig) {
|
||||
prefix := regexp.QuoteMeta(fmt.Sprintf("branch.%s.", branch))
|
||||
args := []string{"config", "--get-regexp", fmt.Sprintf("^%s(remote|merge)$", prefix)}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, line := range outputLines(out) {
|
||||
parts := strings.SplitN(line, " ", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
keys := strings.Split(parts[0], ".")
|
||||
switch keys[len(keys)-1] {
|
||||
case "remote":
|
||||
if strings.Contains(parts[1], ":") {
|
||||
u, err := ParseURL(parts[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
cfg.RemoteURL = u
|
||||
} else if !isFilesystemPath(parts[1]) {
|
||||
cfg.RemoteName = parts[1]
|
||||
}
|
||||
case "merge":
|
||||
cfg.MergeRef = parts[1]
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) DeleteLocalBranch(ctx context.Context, branch string) error {
|
||||
args := []string{"branch", "-D", branch}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (c *Client) HasLocalBranch(ctx context.Context, branch string) bool {
|
||||
args := []string{"rev-parse", "--verify", "refs/heads/" + branch}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
err = cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (c *Client) CheckoutBranch(ctx context.Context, branch string) error {
|
||||
args := []string{"checkout", branch}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (c *Client) CheckoutNewBranch(ctx context.Context, remoteName, branch string) error {
|
||||
track := fmt.Sprintf("%s/%s", remoteName, branch)
|
||||
args := []string{"checkout", "-b", branch, "--track", track}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (c *Client) Pull(ctx context.Context, remote, branch string) error {
|
||||
args := []string{"pull", "--ff-only", remote, branch}
|
||||
//TODO: Use AuthenticatedCommand
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (c *Client) Clone(ctx context.Context, cloneURL string, args []string) (target string, err error) {
|
||||
cloneArgs, target := parseCloneArgs(args)
|
||||
cloneArgs = append(cloneArgs, cloneURL)
|
||||
// If the args contain an explicit target, pass it to clone
|
||||
// otherwise, parse the URL to determine where git cloned it to so we can return it
|
||||
if target != "" {
|
||||
cloneArgs = append(cloneArgs, target)
|
||||
} else {
|
||||
target = path.Base(strings.TrimSuffix(cloneURL, ".git"))
|
||||
}
|
||||
cloneArgs = append([]string{"clone"}, cloneArgs...)
|
||||
//TODO: Use AuthenticatedCommand
|
||||
cmd, err := c.Command(ctx, cloneArgs...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = cmd.Run()
|
||||
return
|
||||
}
|
||||
|
||||
// ToplevelDir returns the top-level directory path of the current repository.
|
||||
func (c *Client) ToplevelDir(ctx context.Context) (string, error) {
|
||||
args := []string{"rev-parse", "--show-toplevel"}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", &GitError{err: err}
|
||||
}
|
||||
return firstLine(out), nil
|
||||
}
|
||||
|
||||
func (c *Client) GitDir(ctx context.Context) (string, error) {
|
||||
args := []string{"rev-parse", "--git-dir"}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", &GitError{err: err}
|
||||
}
|
||||
return firstLine(out), nil
|
||||
}
|
||||
|
||||
// Show current directory relative to the top-level directory of repository.
|
||||
func (c *Client) PathFromRoot(ctx context.Context) string {
|
||||
args := []string{"rev-parse", "--show-prefix"}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if path := firstLine(out); path != "" {
|
||||
return path[:len(path)-1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isFilesystemPath(p string) bool {
|
||||
return p == "." || strings.HasPrefix(p, "./") || strings.HasPrefix(p, "/")
|
||||
}
|
||||
|
||||
func outputLines(output []byte) []string {
|
||||
lines := strings.TrimSuffix(string(output), "\n")
|
||||
return strings.Split(lines, "\n")
|
||||
}
|
||||
|
||||
func firstLine(output []byte) string {
|
||||
if i := bytes.IndexAny(output, "\n"); i >= 0 {
|
||||
return string(output)[0:i]
|
||||
}
|
||||
return string(output)
|
||||
}
|
||||
|
||||
func parseCloneArgs(extraArgs []string) (args []string, target string) {
|
||||
args = extraArgs
|
||||
if len(args) > 0 {
|
||||
if !strings.HasPrefix(args[0], "-") {
|
||||
target, args = args[0], args[1:]
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseRemotes(remotesStr []string) RemoteSet {
|
||||
remotes := RemoteSet{}
|
||||
for _, r := range remotesStr {
|
||||
match := remoteRE.FindStringSubmatch(r)
|
||||
if match == nil {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(match[1])
|
||||
urlStr := strings.TrimSpace(match[2])
|
||||
urlType := strings.TrimSpace(match[3])
|
||||
|
||||
url, err := ParseURL(urlStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var rem *Remote
|
||||
if len(remotes) > 0 {
|
||||
rem = remotes[len(remotes)-1]
|
||||
if name != rem.Name {
|
||||
rem = nil
|
||||
}
|
||||
}
|
||||
if rem == nil {
|
||||
rem = &Remote{Name: name}
|
||||
remotes = append(remotes, rem)
|
||||
}
|
||||
|
||||
switch urlType {
|
||||
case "fetch":
|
||||
rem.FetchURL = url
|
||||
case "push":
|
||||
rem.PushURL = url
|
||||
}
|
||||
}
|
||||
return remotes
|
||||
}
|
||||
|
||||
func populateResolvedRemotes(remotes RemoteSet, resolved []string) {
|
||||
for _, l := range resolved {
|
||||
parts := strings.SplitN(l, " ", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
rp := strings.SplitN(parts[0], ".", 3)
|
||||
if len(rp) < 2 {
|
||||
continue
|
||||
}
|
||||
name := rp[1]
|
||||
for _, r := range remotes {
|
||||
if r.Name == name {
|
||||
r.Resolved = parts[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
365
git/client_test.go
Normal file
365
git/client_test.go
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestClientCommand(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
repoDir string
|
||||
gitPath string
|
||||
wantExe string
|
||||
wantArgs []string
|
||||
}{
|
||||
{
|
||||
name: "creates command",
|
||||
gitPath: "path/to/git",
|
||||
wantExe: "path/to/git",
|
||||
wantArgs: []string{"path/to/git", "ref-log"},
|
||||
},
|
||||
{
|
||||
name: "adds repo directory configuration",
|
||||
repoDir: "path/to/repo",
|
||||
gitPath: "path/to/git",
|
||||
wantExe: "path/to/git",
|
||||
wantArgs: []string{"path/to/git", "-C", "path/to/repo", "ref-log"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
in, out, errOut := &bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{}
|
||||
client := Client{
|
||||
Stdin: in,
|
||||
Stdout: out,
|
||||
Stderr: errOut,
|
||||
RepoDir: tt.repoDir,
|
||||
GitPath: tt.gitPath,
|
||||
}
|
||||
cmd, err := client.Command(context.Background(), "ref-log")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantExe, cmd.Path)
|
||||
assert.Equal(t, tt.wantArgs, cmd.Args)
|
||||
assert.Equal(t, in, cmd.Stdin)
|
||||
assert.Equal(t, out, cmd.Stdout)
|
||||
assert.Equal(t, errOut, cmd.Stderr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientAuthenticatedCommand(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
wantArgs []string
|
||||
}{
|
||||
{
|
||||
name: "adds credential helper config options",
|
||||
path: "path/to/gh",
|
||||
wantArgs: []string{"git", "-c", "credential.helper=", "-c", "credential.helper=!\"path/to/gh\" auth git-credential", "fetch"},
|
||||
},
|
||||
{
|
||||
name: "fallback when GhPath is not set",
|
||||
wantArgs: []string{"git", "-c", "credential.helper=", "-c", "credential.helper=!\"gh\" auth git-credential", "fetch"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := Client{
|
||||
GhPath: tt.path,
|
||||
GitPath: "git",
|
||||
}
|
||||
cmd, err := client.AuthenticatedCommand(context.Background(), "fetch")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantArgs, cmd.Args)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientRemotes(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
initRepo(t, tempDir)
|
||||
gitDir := filepath.Join(tempDir, ".git")
|
||||
remoteFile := filepath.Join(gitDir, "config")
|
||||
remotes := `
|
||||
[remote "origin"]
|
||||
url = git@example.com:monalisa/origin.git
|
||||
[remote "test"]
|
||||
url = git://github.com/hubot/test.git
|
||||
gh-resolved = other
|
||||
[remote "upstream"]
|
||||
url = https://github.com/monalisa/upstream.git
|
||||
gh-resolved = base
|
||||
[remote "github"]
|
||||
url = git@github.com:hubot/github.git
|
||||
`
|
||||
f, err := os.OpenFile(remoteFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0755)
|
||||
assert.NoError(t, err)
|
||||
_, err = f.Write([]byte(remotes))
|
||||
assert.NoError(t, err)
|
||||
err = f.Close()
|
||||
assert.NoError(t, err)
|
||||
client := Client{
|
||||
RepoDir: tempDir,
|
||||
}
|
||||
rs, err := client.Remotes(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 4, len(rs))
|
||||
assert.Equal(t, "upstream", rs[0].Name)
|
||||
assert.Equal(t, "base", rs[0].Resolved)
|
||||
assert.Equal(t, "github", rs[1].Name)
|
||||
assert.Equal(t, "", rs[1].Resolved)
|
||||
assert.Equal(t, "origin", rs[2].Name)
|
||||
assert.Equal(t, "", rs[2].Resolved)
|
||||
assert.Equal(t, "test", rs[3].Name)
|
||||
assert.Equal(t, "other", rs[3].Resolved)
|
||||
}
|
||||
|
||||
func TestParseRemotes(t *testing.T) {
|
||||
remoteList := []string{
|
||||
"mona\tgit@github.com:monalisa/myfork.git (fetch)",
|
||||
"origin\thttps://github.com/monalisa/octo-cat.git (fetch)",
|
||||
"origin\thttps://github.com/monalisa/octo-cat-push.git (push)",
|
||||
"upstream\thttps://example.com/nowhere.git (fetch)",
|
||||
"upstream\thttps://github.com/hubot/tools (push)",
|
||||
"zardoz\thttps://example.com/zed.git (push)",
|
||||
"koke\tgit://github.com/koke/grit.git (fetch)",
|
||||
"koke\tgit://github.com/koke/grit.git (push)",
|
||||
}
|
||||
|
||||
r := parseRemotes(remoteList)
|
||||
assert.Equal(t, 5, len(r))
|
||||
|
||||
assert.Equal(t, "mona", r[0].Name)
|
||||
assert.Equal(t, "ssh://git@github.com/monalisa/myfork.git", r[0].FetchURL.String())
|
||||
assert.Nil(t, r[0].PushURL)
|
||||
|
||||
assert.Equal(t, "origin", r[1].Name)
|
||||
assert.Equal(t, "/monalisa/octo-cat.git", r[1].FetchURL.Path)
|
||||
assert.Equal(t, "/monalisa/octo-cat-push.git", r[1].PushURL.Path)
|
||||
|
||||
assert.Equal(t, "upstream", r[2].Name)
|
||||
assert.Equal(t, "example.com", r[2].FetchURL.Host)
|
||||
assert.Equal(t, "github.com", r[2].PushURL.Host)
|
||||
|
||||
assert.Equal(t, "zardoz", r[3].Name)
|
||||
assert.Nil(t, r[3].FetchURL)
|
||||
assert.Equal(t, "https://example.com/zed.git", r[3].PushURL.String())
|
||||
|
||||
assert.Equal(t, "koke", r[4].Name)
|
||||
assert.Equal(t, "/koke/grit.git", r[4].FetchURL.Path)
|
||||
assert.Equal(t, "/koke/grit.git", r[4].PushURL.Path)
|
||||
}
|
||||
|
||||
func TestClientLastCommit(t *testing.T) {
|
||||
client := Client{
|
||||
RepoDir: "./fixtures/simple.git",
|
||||
}
|
||||
c, err := client.LastCommit(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "6f1a2405cace1633d89a79c74c65f22fe78f9659", c.Sha)
|
||||
assert.Equal(t, "Second commit", c.Title)
|
||||
}
|
||||
|
||||
func TestClientCommitBody(t *testing.T) {
|
||||
client := Client{
|
||||
RepoDir: "./fixtures/simple.git",
|
||||
}
|
||||
body, err := client.CommitBody(context.Background(), "6f1a2405cace1633d89a79c74c65f22fe78f9659")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "I'm starting to get the hang of things\n", body)
|
||||
}
|
||||
|
||||
func TestClientUncommittedChangeCount(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expected int
|
||||
output string
|
||||
}{
|
||||
{
|
||||
name: "no changes",
|
||||
expected: 0,
|
||||
output: "",
|
||||
},
|
||||
{
|
||||
name: "one change",
|
||||
expected: 1,
|
||||
output: " M poem.txt",
|
||||
},
|
||||
{
|
||||
name: "untracked file",
|
||||
expected: 2,
|
||||
output: " M poem.txt\n?? new.txt",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cs, restore := run.Stub()
|
||||
defer restore(t)
|
||||
cs.Register(`git status --porcelain`, 0, tt.output)
|
||||
client := Client{}
|
||||
ucc, err := client.UncommittedChangeCount(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, ucc)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientCurrentBranch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
stub string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "branch name",
|
||||
stub: "branch-name\n",
|
||||
expected: "branch-name",
|
||||
},
|
||||
{
|
||||
name: "ref",
|
||||
stub: "refs/heads/branch-name\n",
|
||||
expected: "branch-name",
|
||||
},
|
||||
{
|
||||
name: "escaped ref",
|
||||
stub: "refs/heads/branch\u00A0with\u00A0non\u00A0breaking\u00A0space\n",
|
||||
expected: "branch\u00A0with\u00A0non\u00A0breaking\u00A0space",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cs, teardown := run.Stub()
|
||||
defer teardown(t)
|
||||
cs.Register(`git symbolic-ref --quiet HEAD`, 0, tt.stub)
|
||||
client := Client{}
|
||||
branch, err := client.CurrentBranch(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, branch)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientCurrentBranch_detached_head(t *testing.T) {
|
||||
cs, teardown := run.Stub()
|
||||
defer teardown(t)
|
||||
cs.Register(`git symbolic-ref --quiet HEAD`, 1, "")
|
||||
client := Client{}
|
||||
_, err := client.CurrentBranch(context.Background())
|
||||
assert.EqualError(t, err, "failed to run git: not on any branch")
|
||||
}
|
||||
|
||||
func TestParseCloneArgs(t *testing.T) {
|
||||
type wanted struct {
|
||||
args []string
|
||||
dir string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want wanted
|
||||
}{
|
||||
{
|
||||
name: "args and target",
|
||||
args: []string{"target_directory", "-o", "upstream", "--depth", "1"},
|
||||
want: wanted{
|
||||
args: []string{"-o", "upstream", "--depth", "1"},
|
||||
dir: "target_directory",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only args",
|
||||
args: []string{"-o", "upstream", "--depth", "1"},
|
||||
want: wanted{
|
||||
args: []string{"-o", "upstream", "--depth", "1"},
|
||||
dir: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only target",
|
||||
args: []string{"target_directory"},
|
||||
want: wanted{
|
||||
args: []string{},
|
||||
dir: "target_directory",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no args",
|
||||
args: []string{},
|
||||
want: wanted{
|
||||
args: []string{},
|
||||
dir: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
args, dir := parseCloneArgs(tt.args)
|
||||
got := wanted{args: args, dir: dir}
|
||||
assert.Equal(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientAddRemote(t *testing.T) {
|
||||
tests := []struct {
|
||||
title string
|
||||
name string
|
||||
url string
|
||||
dir string
|
||||
branches []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
title: "fetch all",
|
||||
name: "test",
|
||||
url: "URL",
|
||||
dir: "DIRECTORY",
|
||||
branches: []string{},
|
||||
want: "git -C DIRECTORY remote add -f test URL",
|
||||
},
|
||||
{
|
||||
title: "fetch specific branches only",
|
||||
name: "test",
|
||||
url: "URL",
|
||||
dir: "DIRECTORY",
|
||||
branches: []string{"trunk", "dev"},
|
||||
want: "git -C DIRECTORY remote add -t trunk -t dev -f test URL",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.title, func(t *testing.T) {
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
cs.Register(tt.want, 0, "")
|
||||
client := Client{
|
||||
RepoDir: tt.dir,
|
||||
}
|
||||
_, err := client.AddRemote(context.Background(), tt.name, tt.url, tt.branches)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func initRepo(t *testing.T, dir string) {
|
||||
errBuf := &bytes.Buffer{}
|
||||
inBuf := &bytes.Buffer{}
|
||||
outBuf := &bytes.Buffer{}
|
||||
client := Client{
|
||||
RepoDir: dir,
|
||||
Stderr: errBuf,
|
||||
Stdin: inBuf,
|
||||
Stdout: outBuf,
|
||||
}
|
||||
cmd, err := client.Command(context.Background(), []string{"init", "--quiet"}...)
|
||||
assert.NoError(t, err)
|
||||
err = cmd.Run()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
452
git/git.go
452
git/git.go
|
|
@ -1,442 +1,154 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"context"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
"github.com/cli/safeexec"
|
||||
)
|
||||
|
||||
// ErrNotOnAnyBranch indicates that the user is in detached HEAD state
|
||||
var ErrNotOnAnyBranch = errors.New("git: not on any branch")
|
||||
|
||||
// Ref represents a git commit reference
|
||||
type Ref struct {
|
||||
Hash string
|
||||
Name string
|
||||
func GitCommand(args ...string) (*gitCommand, error) {
|
||||
c := &Client{}
|
||||
return c.Command(context.Background(), args...)
|
||||
}
|
||||
|
||||
// TrackingRef represents a ref for a remote tracking branch
|
||||
type TrackingRef struct {
|
||||
RemoteName string
|
||||
BranchName string
|
||||
}
|
||||
|
||||
func (r TrackingRef) String() string {
|
||||
return "refs/remotes/" + r.RemoteName + "/" + r.BranchName
|
||||
}
|
||||
|
||||
// ShowRefs resolves fully-qualified refs to commit hashes
|
||||
func ShowRefs(ref ...string) ([]Ref, error) {
|
||||
args := append([]string{"show-ref", "--verify", "--"}, ref...)
|
||||
showRef, err := GitCommand(args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output, err := run.PrepareCmd(showRef).Output()
|
||||
|
||||
var refs []Ref
|
||||
for _, line := range outputLines(output) {
|
||||
parts := strings.SplitN(line, " ", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
refs = append(refs, Ref{
|
||||
Hash: parts[0],
|
||||
Name: parts[1],
|
||||
})
|
||||
}
|
||||
|
||||
return refs, err
|
||||
c := &Client{}
|
||||
return c.ShowRefs(context.Background(), ref...)
|
||||
}
|
||||
|
||||
// CurrentBranch reads the checked-out branch for the git repository
|
||||
func CurrentBranch() (string, error) {
|
||||
refCmd, err := GitCommand("symbolic-ref", "--quiet", "HEAD")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
stderr := bytes.Buffer{}
|
||||
refCmd.Stderr = &stderr
|
||||
|
||||
output, err := run.PrepareCmd(refCmd).Output()
|
||||
if err == nil {
|
||||
// Found the branch name
|
||||
return getBranchShortName(output), nil
|
||||
}
|
||||
|
||||
if stderr.Len() == 0 {
|
||||
// Detached head
|
||||
return "", ErrNotOnAnyBranch
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("%sgit: %s", stderr.String(), err)
|
||||
}
|
||||
|
||||
func listRemotesForPath(path string) ([]string, error) {
|
||||
remoteCmd, err := GitCommand("-C", path, "remote", "-v")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output, err := run.PrepareCmd(remoteCmd).Output()
|
||||
return outputLines(output), err
|
||||
}
|
||||
|
||||
func listRemotes() ([]string, error) {
|
||||
remoteCmd, err := GitCommand("remote", "-v")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output, err := run.PrepareCmd(remoteCmd).Output()
|
||||
return outputLines(output), err
|
||||
c := &Client{}
|
||||
return c.CurrentBranch(context.Background())
|
||||
}
|
||||
|
||||
func Config(name string) (string, error) {
|
||||
configCmd, err := GitCommand("config", name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output, err := run.PrepareCmd(configCmd).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unknown config key: %s", name)
|
||||
}
|
||||
|
||||
return firstLine(output), nil
|
||||
|
||||
}
|
||||
|
||||
type NotInstalled struct {
|
||||
message string
|
||||
error
|
||||
}
|
||||
|
||||
func (e *NotInstalled) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func GitCommand(args ...string) (*exec.Cmd, error) {
|
||||
gitExe, err := safeexec.LookPath("git")
|
||||
if err != nil {
|
||||
if errors.Is(err, exec.ErrNotFound) {
|
||||
programName := "git"
|
||||
if runtime.GOOS == "windows" {
|
||||
programName = "Git for Windows"
|
||||
}
|
||||
return nil, &NotInstalled{
|
||||
message: fmt.Sprintf("unable to find git executable in PATH; please install %s before retrying", programName),
|
||||
error: err,
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return exec.Command(gitExe, args...), nil
|
||||
c := &Client{}
|
||||
return c.Config(context.Background(), name)
|
||||
}
|
||||
|
||||
func UncommittedChangeCount() (int, error) {
|
||||
statusCmd, err := GitCommand("status", "--porcelain")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
output, err := run.PrepareCmd(statusCmd).Output()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
lines := strings.Split(string(output), "\n")
|
||||
|
||||
count := 0
|
||||
|
||||
for _, l := range lines {
|
||||
if l != "" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
type Commit struct {
|
||||
Sha string
|
||||
Title string
|
||||
c := &Client{}
|
||||
return c.UncommittedChangeCount(context.Background())
|
||||
}
|
||||
|
||||
func Commits(baseRef, headRef string) ([]*Commit, error) {
|
||||
logCmd, err := GitCommand(
|
||||
"-c", "log.ShowSignature=false",
|
||||
"log", "--pretty=format:%H,%s",
|
||||
"--cherry", fmt.Sprintf("%s...%s", baseRef, headRef))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output, err := run.PrepareCmd(logCmd).Output()
|
||||
if err != nil {
|
||||
return []*Commit{}, err
|
||||
}
|
||||
|
||||
commits := []*Commit{}
|
||||
sha := 0
|
||||
title := 1
|
||||
for _, line := range outputLines(output) {
|
||||
split := strings.SplitN(line, ",", 2)
|
||||
if len(split) != 2 {
|
||||
continue
|
||||
}
|
||||
commits = append(commits, &Commit{
|
||||
Sha: split[sha],
|
||||
Title: split[title],
|
||||
})
|
||||
}
|
||||
|
||||
if len(commits) == 0 {
|
||||
return commits, fmt.Errorf("could not find any commits between %s and %s", baseRef, headRef)
|
||||
}
|
||||
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
func lookupCommit(sha, format string) ([]byte, error) {
|
||||
logCmd, err := GitCommand("-c", "log.ShowSignature=false", "show", "-s", "--pretty=format:"+format, sha)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return run.PrepareCmd(logCmd).Output()
|
||||
c := &Client{}
|
||||
return c.Commits(context.Background(), baseRef, headRef)
|
||||
}
|
||||
|
||||
func LastCommit() (*Commit, error) {
|
||||
output, err := lookupCommit("HEAD", "%H,%s")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idx := bytes.IndexByte(output, ',')
|
||||
return &Commit{
|
||||
Sha: string(output[0:idx]),
|
||||
Title: strings.TrimSpace(string(output[idx+1:])),
|
||||
}, nil
|
||||
c := &Client{}
|
||||
return c.LastCommit(context.Background())
|
||||
}
|
||||
|
||||
func CommitBody(sha string) (string, error) {
|
||||
output, err := lookupCommit(sha, "%b")
|
||||
return string(output), err
|
||||
c := &Client{}
|
||||
return c.CommitBody(context.Background(), sha)
|
||||
}
|
||||
|
||||
// Push publishes a git ref to a remote and sets up upstream configuration
|
||||
func Push(remote string, ref string, cmdIn io.ReadCloser, cmdOut, cmdErr io.Writer) error {
|
||||
pushCmd, err := GitCommand("push", "--set-upstream", remote, ref)
|
||||
if err != nil {
|
||||
return err
|
||||
//TODO: Replace with factory GitClient and use AuthenticatedCommand
|
||||
c := &Client{
|
||||
Stdin: cmdIn,
|
||||
Stdout: cmdOut,
|
||||
Stderr: cmdErr,
|
||||
}
|
||||
pushCmd.Stdin = cmdIn
|
||||
pushCmd.Stdout = cmdOut
|
||||
pushCmd.Stderr = cmdErr
|
||||
return run.PrepareCmd(pushCmd).Run()
|
||||
return c.Push(context.Background(), remote, ref)
|
||||
}
|
||||
|
||||
type BranchConfig struct {
|
||||
RemoteName string
|
||||
RemoteURL *url.URL
|
||||
MergeRef string
|
||||
}
|
||||
|
||||
// ReadBranchConfig parses the `branch.BRANCH.(remote|merge)` part of git config
|
||||
func ReadBranchConfig(branch string) (cfg BranchConfig) {
|
||||
prefix := regexp.QuoteMeta(fmt.Sprintf("branch.%s.", branch))
|
||||
configCmd, err := GitCommand("config", "--get-regexp", fmt.Sprintf("^%s(remote|merge)$", prefix))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output, err := run.PrepareCmd(configCmd).Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, line := range outputLines(output) {
|
||||
parts := strings.SplitN(line, " ", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
keys := strings.Split(parts[0], ".")
|
||||
switch keys[len(keys)-1] {
|
||||
case "remote":
|
||||
if strings.Contains(parts[1], ":") {
|
||||
u, err := ParseURL(parts[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
cfg.RemoteURL = u
|
||||
} else if !isFilesystemPath(parts[1]) {
|
||||
cfg.RemoteName = parts[1]
|
||||
}
|
||||
case "merge":
|
||||
cfg.MergeRef = parts[1]
|
||||
}
|
||||
}
|
||||
return
|
||||
c := &Client{}
|
||||
return c.ReadBranchConfig(context.Background(), branch)
|
||||
}
|
||||
|
||||
func DeleteLocalBranch(branch string) error {
|
||||
branchCmd, err := GitCommand("branch", "-D", branch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return run.PrepareCmd(branchCmd).Run()
|
||||
c := &Client{}
|
||||
return c.DeleteLocalBranch(context.Background(), branch)
|
||||
}
|
||||
|
||||
func HasLocalBranch(branch string) bool {
|
||||
configCmd, err := GitCommand("rev-parse", "--verify", "refs/heads/"+branch)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = run.PrepareCmd(configCmd).Output()
|
||||
return err == nil
|
||||
c := &Client{}
|
||||
return c.HasLocalBranch(context.Background(), branch)
|
||||
}
|
||||
|
||||
func CheckoutBranch(branch string) error {
|
||||
configCmd, err := GitCommand("checkout", branch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return run.PrepareCmd(configCmd).Run()
|
||||
c := &Client{}
|
||||
return c.CheckoutBranch(context.Background(), branch)
|
||||
}
|
||||
|
||||
func CheckoutNewBranch(remoteName, branch string) error {
|
||||
track := fmt.Sprintf("%s/%s", remoteName, branch)
|
||||
configCmd, err := GitCommand("checkout", "-b", branch, "--track", track)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return run.PrepareCmd(configCmd).Run()
|
||||
c := &Client{}
|
||||
return c.CheckoutNewBranch(context.Background(), remoteName, branch)
|
||||
}
|
||||
|
||||
// pull changes from remote branch without version history
|
||||
func Pull(remote, branch string) error {
|
||||
pullCmd, err := GitCommand("pull", "--ff-only", remote, branch)
|
||||
if err != nil {
|
||||
return err
|
||||
//TODO: Replace with factory GitClient and use AuthenticatedCommand
|
||||
c := &Client{
|
||||
Stdin: os.Stdin,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
}
|
||||
|
||||
pullCmd.Stdout = os.Stdout
|
||||
pullCmd.Stderr = os.Stderr
|
||||
pullCmd.Stdin = os.Stdin
|
||||
return run.PrepareCmd(pullCmd).Run()
|
||||
}
|
||||
|
||||
func parseCloneArgs(extraArgs []string) (args []string, target string) {
|
||||
args = extraArgs
|
||||
|
||||
if len(args) > 0 {
|
||||
if !strings.HasPrefix(args[0], "-") {
|
||||
target, args = args[0], args[1:]
|
||||
}
|
||||
}
|
||||
return
|
||||
return c.Pull(context.Background(), remote, branch)
|
||||
}
|
||||
|
||||
func RunClone(cloneURL string, args []string) (target string, err error) {
|
||||
cloneArgs, target := parseCloneArgs(args)
|
||||
|
||||
cloneArgs = append(cloneArgs, cloneURL)
|
||||
|
||||
// If the args contain an explicit target, pass it to clone
|
||||
// otherwise, parse the URL to determine where git cloned it to so we can return it
|
||||
if target != "" {
|
||||
cloneArgs = append(cloneArgs, target)
|
||||
} else {
|
||||
target = path.Base(strings.TrimSuffix(cloneURL, ".git"))
|
||||
//TODO: Replace with factory GitClient and use AuthenticatedCommand
|
||||
c := &Client{
|
||||
Stdin: os.Stdin,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
}
|
||||
|
||||
cloneArgs = append([]string{"clone"}, cloneArgs...)
|
||||
|
||||
cloneCmd, err := GitCommand(cloneArgs...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cloneCmd.Stdin = os.Stdin
|
||||
cloneCmd.Stdout = os.Stdout
|
||||
cloneCmd.Stderr = os.Stderr
|
||||
|
||||
err = run.PrepareCmd(cloneCmd).Run()
|
||||
return
|
||||
return c.Clone(context.Background(), cloneURL, args)
|
||||
}
|
||||
|
||||
func AddNamedRemote(url, name, dir string, branches []string) error {
|
||||
args := []string{"-C", dir, "remote", "add"}
|
||||
for _, branch := range branches {
|
||||
args = append(args, "-t", branch)
|
||||
}
|
||||
args = append(args, "-f", name, url)
|
||||
cloneCmd, err := GitCommand(args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cloneCmd.Stdout = os.Stdout
|
||||
cloneCmd.Stderr = os.Stderr
|
||||
return run.PrepareCmd(cloneCmd).Run()
|
||||
}
|
||||
|
||||
func isFilesystemPath(p string) bool {
|
||||
return p == "." || strings.HasPrefix(p, "./") || strings.HasPrefix(p, "/")
|
||||
}
|
||||
|
||||
// ToplevelDir returns the top-level directory path of the current repository
|
||||
func ToplevelDir() (string, error) {
|
||||
showCmd, err := GitCommand("rev-parse", "--show-toplevel")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output, err := run.PrepareCmd(showCmd).Output()
|
||||
return firstLine(output), err
|
||||
|
||||
c := &Client{}
|
||||
return c.ToplevelDir(context.Background())
|
||||
}
|
||||
|
||||
// ToplevelDirFromPath returns the top-level given path of the current repository
|
||||
func GetDirFromPath(p string) (string, error) {
|
||||
showCmd, err := GitCommand("-C", p, "rev-parse", "--git-dir")
|
||||
if err != nil {
|
||||
return "", err
|
||||
func GetDirFromPath(repoDir string) (string, error) {
|
||||
c := &Client{
|
||||
RepoDir: repoDir,
|
||||
}
|
||||
output, err := run.PrepareCmd(showCmd).Output()
|
||||
return firstLine(output), err
|
||||
return c.GitDir(context.Background())
|
||||
}
|
||||
|
||||
func PathFromRepoRoot() string {
|
||||
showCmd, err := GitCommand("rev-parse", "--show-prefix")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
output, err := run.PrepareCmd(showCmd).Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if path := firstLine(output); path != "" {
|
||||
return path[:len(path)-1]
|
||||
}
|
||||
return ""
|
||||
c := &Client{}
|
||||
return c.PathFromRoot(context.Background())
|
||||
}
|
||||
|
||||
func outputLines(output []byte) []string {
|
||||
lines := strings.TrimSuffix(string(output), "\n")
|
||||
return strings.Split(lines, "\n")
|
||||
|
||||
func Remotes() (RemoteSet, error) {
|
||||
c := &Client{}
|
||||
return c.Remotes(context.Background())
|
||||
}
|
||||
|
||||
func firstLine(output []byte) string {
|
||||
if i := bytes.IndexAny(output, "\n"); i >= 0 {
|
||||
return string(output)[0:i]
|
||||
func RemotesForPath(repoDir string) (RemoteSet, error) {
|
||||
c := &Client{
|
||||
RepoDir: repoDir,
|
||||
}
|
||||
return string(output)
|
||||
return c.Remotes(context.Background())
|
||||
}
|
||||
|
||||
func getBranchShortName(output []byte) string {
|
||||
branch := firstLine(output)
|
||||
return strings.TrimPrefix(branch, "refs/heads/")
|
||||
func AddRemote(name, url string) (*Remote, error) {
|
||||
c := &Client{}
|
||||
return c.AddRemote(context.Background(), name, url, []string{})
|
||||
}
|
||||
|
||||
func AddNamedRemote(url, name, repoDir string, branches []string) error {
|
||||
c := &Client{
|
||||
RepoDir: repoDir,
|
||||
}
|
||||
_, err := c.AddRemote(context.Background(), name, url, branches)
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateRemoteURL(name, url string) error {
|
||||
c := &Client{}
|
||||
return c.UpdateRemoteURL(context.Background(), name, url)
|
||||
}
|
||||
|
||||
func SetRemoteResolution(name, resolution string) error {
|
||||
c := &Client{}
|
||||
return c.SetRemoteResolution(context.Background(), name, resolution)
|
||||
}
|
||||
|
|
|
|||
213
git/git_test.go
213
git/git_test.go
|
|
@ -1,213 +0,0 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
)
|
||||
|
||||
func TestLastCommit(t *testing.T) {
|
||||
t.Setenv("GIT_DIR", "./fixtures/simple.git")
|
||||
c, err := LastCommit()
|
||||
if err != nil {
|
||||
t.Fatalf("LastCommit error: %v", err)
|
||||
}
|
||||
if c.Sha != "6f1a2405cace1633d89a79c74c65f22fe78f9659" {
|
||||
t.Errorf("expected sha %q, got %q", "6f1a2405cace1633d89a79c74c65f22fe78f9659", c.Sha)
|
||||
}
|
||||
if c.Title != "Second commit" {
|
||||
t.Errorf("expected title %q, got %q", "Second commit", c.Title)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitBody(t *testing.T) {
|
||||
t.Setenv("GIT_DIR", "./fixtures/simple.git")
|
||||
body, err := CommitBody("6f1a2405cace1633d89a79c74c65f22fe78f9659")
|
||||
if err != nil {
|
||||
t.Fatalf("CommitBody error: %v", err)
|
||||
}
|
||||
if body != "I'm starting to get the hang of things\n" {
|
||||
t.Errorf("expected %q, got %q", "I'm starting to get the hang of things\n", body)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
NOTE: below this are stubbed git tests, i.e. those that do not actually invoke `git`. If possible, utilize
|
||||
`setGitDir()` to allow new tests to interact with `git`. For write operations, you can use `t.TempDir()` to
|
||||
host a temporary git repository that is safe to be changed.
|
||||
*/
|
||||
|
||||
func Test_UncommittedChangeCount(t *testing.T) {
|
||||
type c struct {
|
||||
Label string
|
||||
Expected int
|
||||
Output string
|
||||
}
|
||||
cases := []c{
|
||||
{Label: "no changes", Expected: 0, Output: ""},
|
||||
{Label: "one change", Expected: 1, Output: " M poem.txt"},
|
||||
{Label: "untracked file", Expected: 2, Output: " M poem.txt\n?? new.txt"},
|
||||
}
|
||||
|
||||
for _, v := range cases {
|
||||
t.Run(v.Label, func(t *testing.T) {
|
||||
cs, restore := run.Stub()
|
||||
defer restore(t)
|
||||
cs.Register(`git status --porcelain`, 0, v.Output)
|
||||
|
||||
ucc, _ := UncommittedChangeCount()
|
||||
if ucc != v.Expected {
|
||||
t.Errorf("UncommittedChangeCount() = %d, expected %d", ucc, v.Expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CurrentBranch(t *testing.T) {
|
||||
type c struct {
|
||||
Stub string
|
||||
Expected string
|
||||
}
|
||||
cases := []c{
|
||||
{
|
||||
Stub: "branch-name\n",
|
||||
Expected: "branch-name",
|
||||
},
|
||||
{
|
||||
Stub: "refs/heads/branch-name\n",
|
||||
Expected: "branch-name",
|
||||
},
|
||||
{
|
||||
Stub: "refs/heads/branch\u00A0with\u00A0non\u00A0breaking\u00A0space\n",
|
||||
Expected: "branch\u00A0with\u00A0non\u00A0breaking\u00A0space",
|
||||
},
|
||||
}
|
||||
|
||||
for _, v := range cases {
|
||||
cs, teardown := run.Stub()
|
||||
cs.Register(`git symbolic-ref --quiet HEAD`, 0, v.Stub)
|
||||
|
||||
result, err := CurrentBranch()
|
||||
if err != nil {
|
||||
t.Errorf("got unexpected error: %v", err)
|
||||
}
|
||||
if result != v.Expected {
|
||||
t.Errorf("unexpected branch name: %s instead of %s", result, v.Expected)
|
||||
}
|
||||
teardown(t)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CurrentBranch_detached_head(t *testing.T) {
|
||||
cs, teardown := run.Stub()
|
||||
defer teardown(t)
|
||||
cs.Register(`git symbolic-ref --quiet HEAD`, 1, "")
|
||||
|
||||
_, err := CurrentBranch()
|
||||
if err == nil {
|
||||
t.Fatal("expected an error, got nil")
|
||||
}
|
||||
if err != ErrNotOnAnyBranch {
|
||||
t.Errorf("got unexpected error: %s instead of %s", err, ErrNotOnAnyBranch)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExtraCloneArgs(t *testing.T) {
|
||||
type Wanted struct {
|
||||
args []string
|
||||
dir string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want Wanted
|
||||
}{
|
||||
{
|
||||
name: "args and target",
|
||||
args: []string{"target_directory", "-o", "upstream", "--depth", "1"},
|
||||
want: Wanted{
|
||||
args: []string{"-o", "upstream", "--depth", "1"},
|
||||
dir: "target_directory",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only args",
|
||||
args: []string{"-o", "upstream", "--depth", "1"},
|
||||
want: Wanted{
|
||||
args: []string{"-o", "upstream", "--depth", "1"},
|
||||
dir: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only target",
|
||||
args: []string{"target_directory"},
|
||||
want: Wanted{
|
||||
args: []string{},
|
||||
dir: "target_directory",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no args",
|
||||
args: []string{},
|
||||
want: Wanted{
|
||||
args: []string{},
|
||||
dir: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
args, dir := parseCloneArgs(tt.args)
|
||||
got := Wanted{
|
||||
args: args,
|
||||
dir: dir,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("got %#v want %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddNamedRemote(t *testing.T) {
|
||||
tests := []struct {
|
||||
title string
|
||||
name string
|
||||
url string
|
||||
dir string
|
||||
branches []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
title: "fetch all",
|
||||
name: "test",
|
||||
url: "URL",
|
||||
dir: "DIRECTORY",
|
||||
branches: []string{},
|
||||
want: "git -C DIRECTORY remote add -f test URL",
|
||||
},
|
||||
{
|
||||
title: "fetch specific branches only",
|
||||
name: "test",
|
||||
url: "URL",
|
||||
dir: "DIRECTORY",
|
||||
branches: []string{"trunk", "dev"},
|
||||
want: "git -C DIRECTORY remote add -t trunk -t dev -f test URL",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.title, func(t *testing.T) {
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Register(tt.want, 0, "")
|
||||
|
||||
err := AddNamedRemote(tt.url, tt.name, tt.dir, tt.branches)
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `git remote add -f`: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
76
git/objects.go
Normal file
76
git/objects.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RemoteSet is a slice of git remotes.
|
||||
type RemoteSet []*Remote
|
||||
|
||||
func (r RemoteSet) Len() int { return len(r) }
|
||||
func (r RemoteSet) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
||||
func (r RemoteSet) Less(i, j int) bool {
|
||||
return remoteNameSortScore(r[i].Name) > remoteNameSortScore(r[j].Name)
|
||||
}
|
||||
|
||||
func remoteNameSortScore(name string) int {
|
||||
switch strings.ToLower(name) {
|
||||
case "upstream":
|
||||
return 3
|
||||
case "github":
|
||||
return 2
|
||||
case "origin":
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Remote is a parsed git remote.
|
||||
type Remote struct {
|
||||
Name string
|
||||
Resolved string
|
||||
FetchURL *url.URL
|
||||
PushURL *url.URL
|
||||
}
|
||||
|
||||
func (r *Remote) String() string {
|
||||
return r.Name
|
||||
}
|
||||
|
||||
func NewRemote(name string, u string) *Remote {
|
||||
pu, _ := url.Parse(u)
|
||||
return &Remote{
|
||||
Name: name,
|
||||
FetchURL: pu,
|
||||
PushURL: pu,
|
||||
}
|
||||
}
|
||||
|
||||
// Ref represents a git commit reference.
|
||||
type Ref struct {
|
||||
Hash string
|
||||
Name string
|
||||
}
|
||||
|
||||
// TrackingRef represents a ref for a remote tracking branch.
|
||||
type TrackingRef struct {
|
||||
RemoteName string
|
||||
BranchName string
|
||||
}
|
||||
|
||||
func (r TrackingRef) String() string {
|
||||
return "refs/remotes/" + r.RemoteName + "/" + r.BranchName
|
||||
}
|
||||
|
||||
type Commit struct {
|
||||
Sha string
|
||||
Title string
|
||||
}
|
||||
|
||||
type BranchConfig struct {
|
||||
RemoteName string
|
||||
RemoteURL *url.URL
|
||||
MergeRef string
|
||||
}
|
||||
169
git/remote.go
169
git/remote.go
|
|
@ -1,169 +0,0 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
)
|
||||
|
||||
var remoteRE = regexp.MustCompile(`(.+)\s+(.+)\s+\((push|fetch)\)`)
|
||||
|
||||
// RemoteSet is a slice of git remotes
|
||||
type RemoteSet []*Remote
|
||||
|
||||
func NewRemote(name string, u string) *Remote {
|
||||
pu, _ := url.Parse(u)
|
||||
return &Remote{
|
||||
Name: name,
|
||||
FetchURL: pu,
|
||||
PushURL: pu,
|
||||
}
|
||||
}
|
||||
|
||||
// Remote is a parsed git remote
|
||||
type Remote struct {
|
||||
Name string
|
||||
Resolved string
|
||||
FetchURL *url.URL
|
||||
PushURL *url.URL
|
||||
}
|
||||
|
||||
func (r *Remote) String() string {
|
||||
return r.Name
|
||||
}
|
||||
|
||||
func remotes(path string, remoteList []string) (RemoteSet, error) {
|
||||
remotes := parseRemotes(remoteList)
|
||||
|
||||
// this is affected by SetRemoteResolution
|
||||
remoteCmd, err := GitCommand("-C", path, "config", "--get-regexp", `^remote\..*\.gh-resolved$`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output, _ := run.PrepareCmd(remoteCmd).Output()
|
||||
for _, l := range outputLines(output) {
|
||||
parts := strings.SplitN(l, " ", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
rp := strings.SplitN(parts[0], ".", 3)
|
||||
if len(rp) < 2 {
|
||||
continue
|
||||
}
|
||||
name := rp[1]
|
||||
for _, r := range remotes {
|
||||
if r.Name == name {
|
||||
r.Resolved = parts[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return remotes, nil
|
||||
}
|
||||
|
||||
func RemotesForPath(path string) (RemoteSet, error) {
|
||||
list, err := listRemotesForPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return remotes(path, list)
|
||||
}
|
||||
|
||||
// Remotes gets the git remotes set for the current repo
|
||||
func Remotes() (RemoteSet, error) {
|
||||
list, err := listRemotes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return remotes(".", list)
|
||||
}
|
||||
|
||||
func parseRemotes(gitRemotes []string) (remotes RemoteSet) {
|
||||
for _, r := range gitRemotes {
|
||||
match := remoteRE.FindStringSubmatch(r)
|
||||
if match == nil {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(match[1])
|
||||
urlStr := strings.TrimSpace(match[2])
|
||||
urlType := strings.TrimSpace(match[3])
|
||||
|
||||
var rem *Remote
|
||||
if len(remotes) > 0 {
|
||||
rem = remotes[len(remotes)-1]
|
||||
if name != rem.Name {
|
||||
rem = nil
|
||||
}
|
||||
}
|
||||
if rem == nil {
|
||||
rem = &Remote{Name: name}
|
||||
remotes = append(remotes, rem)
|
||||
}
|
||||
|
||||
u, err := ParseURL(urlStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch urlType {
|
||||
case "fetch":
|
||||
rem.FetchURL = u
|
||||
case "push":
|
||||
rem.PushURL = u
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AddRemote adds a new git remote and auto-fetches objects from it
|
||||
func AddRemote(name, u string) (*Remote, error) {
|
||||
addCmd, err := GitCommand("remote", "add", "-f", name, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = run.PrepareCmd(addCmd).Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var urlParsed *url.URL
|
||||
if strings.HasPrefix(u, "https") {
|
||||
urlParsed, err = url.Parse(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
} else {
|
||||
urlParsed, err = ParseURL(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return &Remote{
|
||||
Name: name,
|
||||
FetchURL: urlParsed,
|
||||
PushURL: urlParsed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func UpdateRemoteURL(name, u string) error {
|
||||
addCmd, err := GitCommand("remote", "set-url", name, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return run.PrepareCmd(addCmd).Run()
|
||||
}
|
||||
|
||||
func SetRemoteResolution(name, resolution string) error {
|
||||
addCmd, err := GitCommand("config", "--add", fmt.Sprintf("remote.%s.gh-resolved", name), resolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return run.PrepareCmd(addCmd).Run()
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_parseRemotes(t *testing.T) {
|
||||
remoteList := []string{
|
||||
"mona\tgit@github.com:monalisa/myfork.git (fetch)",
|
||||
"origin\thttps://github.com/monalisa/octo-cat.git (fetch)",
|
||||
"origin\thttps://github.com/monalisa/octo-cat-push.git (push)",
|
||||
"upstream\thttps://example.com/nowhere.git (fetch)",
|
||||
"upstream\thttps://github.com/hubot/tools (push)",
|
||||
"zardoz\thttps://example.com/zed.git (push)",
|
||||
}
|
||||
r := parseRemotes(remoteList)
|
||||
assert.Equal(t, 4, len(r))
|
||||
|
||||
assert.Equal(t, "mona", r[0].Name)
|
||||
assert.Equal(t, "ssh://git@github.com/monalisa/myfork.git", r[0].FetchURL.String())
|
||||
if r[0].PushURL != nil {
|
||||
t.Errorf("expected no PushURL, got %q", r[0].PushURL)
|
||||
}
|
||||
assert.Equal(t, "origin", r[1].Name)
|
||||
assert.Equal(t, "/monalisa/octo-cat.git", r[1].FetchURL.Path)
|
||||
assert.Equal(t, "/monalisa/octo-cat-push.git", r[1].PushURL.Path)
|
||||
|
||||
assert.Equal(t, "upstream", r[2].Name)
|
||||
assert.Equal(t, "example.com", r[2].FetchURL.Host)
|
||||
assert.Equal(t, "github.com", r[2].PushURL.Host)
|
||||
|
||||
assert.Equal(t, "zardoz", r[3].Name)
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ import (
|
|||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
"github.com/google/shlex"
|
||||
)
|
||||
|
||||
|
|
@ -82,7 +81,7 @@ func (flow *GitCredentialFlow) gitCredentialSetup(hostname, username, password s
|
|||
configErr = err
|
||||
break
|
||||
}
|
||||
if err = run.PrepareCmd(preConfigureCmd).Run(); err != nil {
|
||||
if err = preConfigureCmd.Run(); err != nil {
|
||||
configErr = err
|
||||
break
|
||||
}
|
||||
|
|
@ -96,7 +95,7 @@ func (flow *GitCredentialFlow) gitCredentialSetup(hostname, username, password s
|
|||
if err != nil {
|
||||
configErr = err
|
||||
} else {
|
||||
configErr = run.PrepareCmd(configureCmd).Run()
|
||||
configErr = configureCmd.Run()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -114,7 +113,7 @@ func (flow *GitCredentialFlow) gitCredentialSetup(hostname, username, password s
|
|||
host=%s
|
||||
`, hostname))
|
||||
|
||||
err = run.PrepareCmd(rejectCmd).Run()
|
||||
err = rejectCmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -131,7 +130,7 @@ func (flow *GitCredentialFlow) gitCredentialSetup(hostname, username, password s
|
|||
password=%s
|
||||
`, hostname, username, password))
|
||||
|
||||
err = run.PrepareCmd(approveCmd).Run()
|
||||
err = approveCmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ func New(appVersion string) *cmdutil.Factory {
|
|||
f.BaseRepo = BaseRepoFunc(f) // Depends on Remotes
|
||||
f.Prompter = newPrompter(f) // Depends on Config and IOStreams
|
||||
f.Browser = newBrowser(f) // Depends on Config, and IOStreams
|
||||
f.GitClient = newGitClient(f) // Depends on IOStreams, and Executable
|
||||
f.ExtensionManager = extensionManager(f) // Depends on Config, HttpClient, and IOStreams
|
||||
|
||||
return f
|
||||
|
|
@ -106,6 +107,18 @@ func httpClientFunc(f *cmdutil.Factory, appVersion string) func() (*http.Client,
|
|||
}
|
||||
}
|
||||
|
||||
func newGitClient(f *cmdutil.Factory) *git.Client {
|
||||
io := f.IOStreams
|
||||
ghPath := f.Executable()
|
||||
client := &git.Client{
|
||||
GhPath: ghPath,
|
||||
Stderr: io.ErrOut,
|
||||
Stdin: io.In,
|
||||
Stdout: io.Out,
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func newBrowser(f *cmdutil.Factory) browser.Browser {
|
||||
io := f.IOStreams
|
||||
return browser.New("", io.Out, io.ErrOut)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/git"
|
||||
|
|
@ -435,6 +436,44 @@ func TestSSOURL(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNewGitClient(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config config.Config
|
||||
executable string
|
||||
wantAuthHosts []string
|
||||
wantGhPath string
|
||||
}{
|
||||
{
|
||||
name: "creates git client",
|
||||
config: defaultConfig(),
|
||||
executable: filepath.Join("path", "to", "gh"),
|
||||
wantAuthHosts: []string{"nonsense.com"},
|
||||
wantGhPath: filepath.Join("path", "to", "gh"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := New("1")
|
||||
f.Config = func() (config.Config, error) {
|
||||
if tt.config == nil {
|
||||
return config.NewBlankConfig(), nil
|
||||
} else {
|
||||
return tt.config, nil
|
||||
}
|
||||
}
|
||||
f.ExecutableName = tt.executable
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f.IOStreams = ios
|
||||
c := newGitClient(f)
|
||||
assert.Equal(t, tt.wantGhPath, c.GhPath)
|
||||
assert.Equal(t, ios.In, c.Stdin)
|
||||
assert.Equal(t, ios.Out, c.Stdout)
|
||||
assert.Equal(t, ios.ErrOut, c.Stderr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func defaultConfig() *config.ConfigMock {
|
||||
cfg := config.NewFromString("")
|
||||
cfg.Set("nonsense.com", "oauth_token", "BLAH")
|
||||
|
|
|
|||
|
|
@ -1,22 +1,19 @@
|
|||
package checkout
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/context"
|
||||
cliContext "github.com/cli/cli/v2/context"
|
||||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/safeexec"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -24,7 +21,7 @@ type CheckoutOptions struct {
|
|||
HttpClient func() (*http.Client, error)
|
||||
Config func() (config.Config, error)
|
||||
IO *iostreams.IOStreams
|
||||
Remotes func() (context.Remotes, error)
|
||||
Remotes func() (cliContext.Remotes, error)
|
||||
Branch func() (string, error)
|
||||
|
||||
Finder shared.PRFinder
|
||||
|
|
@ -131,7 +128,7 @@ func checkoutRun(opts *CheckoutOptions) error {
|
|||
cmdQueue = append(cmdQueue, []string{"git", "submodule", "update", "--init", "--recursive"})
|
||||
}
|
||||
|
||||
err = executeCmds(cmdQueue)
|
||||
err = executeCmds(cmdQueue, opts.IO)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -139,7 +136,7 @@ func checkoutRun(opts *CheckoutOptions) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func cmdsForExistingRemote(remote *context.Remote, pr *api.PullRequest, opts *CheckoutOptions) [][]string {
|
||||
func cmdsForExistingRemote(remote *cliContext.Remote, pr *api.PullRequest, opts *CheckoutOptions) [][]string {
|
||||
var cmds [][]string
|
||||
remoteBranch := fmt.Sprintf("%s/%s", remote.Name, pr.HeadRefName)
|
||||
|
||||
|
|
@ -241,17 +238,19 @@ func localBranchExists(b string) bool {
|
|||
return err == nil
|
||||
}
|
||||
|
||||
func executeCmds(cmdQueue [][]string) error {
|
||||
func executeCmds(cmdQueue [][]string, ios *iostreams.IOStreams) error {
|
||||
//TODO: Replace with factory GitClient
|
||||
//TODO: Use AuthenticatedCommand
|
||||
client := git.Client{
|
||||
Stdout: ios.Out,
|
||||
Stderr: ios.ErrOut,
|
||||
}
|
||||
for _, args := range cmdQueue {
|
||||
// TODO: reuse the result of this lookup across loop iteration
|
||||
exe, err := safeexec.LookPath(args[0])
|
||||
cmd, err := client.Command(context.Background(), args[1:]...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command(exe, args[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := run.PrepareCmd(cmd).Run(); err != nil {
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import (
|
|||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmd/release/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
|
|
@ -475,7 +474,7 @@ func gitTagInfo(tagName string) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b, err := run.PrepareCmd(cmd).Output()
|
||||
b, err := cmd.Output()
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
|
|
@ -484,7 +483,7 @@ func detectPreviousTag(headRef string) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b, err := run.PrepareCmd(cmd).Output()
|
||||
b, err := cmd.Output()
|
||||
return strings.TrimSpace(string(b)), err
|
||||
}
|
||||
|
||||
|
|
@ -498,7 +497,7 @@ func changelogForRange(refRange string) ([]logEntry, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, err := run.PrepareCmd(cmd).Output()
|
||||
b, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import (
|
|||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
"github.com/cli/cli/v2/pkg/cmd/repo/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
|
|
@ -552,7 +551,7 @@ func createFromLocal(opts *CreateOptions) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = run.PrepareCmd(repoPush).Run()
|
||||
err = repoPush.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -574,7 +573,7 @@ func sourceInit(io *iostreams.IOStreams, remoteURL, baseRemote, repoPath string)
|
|||
return err
|
||||
}
|
||||
|
||||
err = run.PrepareCmd(remoteAdd).Run()
|
||||
err = remoteAdd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s Unable to add remote %q", cs.FailureIcon(), baseRemote)
|
||||
}
|
||||
|
|
@ -590,8 +589,7 @@ func hasCommits(repoPath string) (bool, error) {
|
|||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
prepareCmd := run.PrepareCmd(hasCommitsCmd)
|
||||
err = prepareCmd.Run()
|
||||
err = hasCommitsCmd.Run()
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
|
@ -636,7 +634,7 @@ func localInit(io *iostreams.IOStreams, remoteURL, path, checkoutBranch string)
|
|||
gitInit.Stdout = io.Out
|
||||
}
|
||||
gitInit.Stderr = io.ErrOut
|
||||
err = run.PrepareCmd(gitInit).Run()
|
||||
err = gitInit.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -647,7 +645,7 @@ func localInit(io *iostreams.IOStreams, remoteURL, path, checkoutBranch string)
|
|||
}
|
||||
gitRemoteAdd.Stdout = io.Out
|
||||
gitRemoteAdd.Stderr = io.ErrOut
|
||||
err = run.PrepareCmd(gitRemoteAdd).Run()
|
||||
err = gitRemoteAdd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -662,7 +660,7 @@ func localInit(io *iostreams.IOStreams, remoteURL, path, checkoutBranch string)
|
|||
}
|
||||
gitFetch.Stdout = io.Out
|
||||
gitFetch.Stderr = io.ErrOut
|
||||
err = run.PrepareCmd(gitFetch).Run()
|
||||
err = gitFetch.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -673,7 +671,7 @@ func localInit(io *iostreams.IOStreams, remoteURL, path, checkoutBranch string)
|
|||
}
|
||||
gitCheckout.Stdout = io.Out
|
||||
gitCheckout.Stderr = io.ErrOut
|
||||
return run.PrepareCmd(gitCheckout).Run()
|
||||
return gitCheckout.Run()
|
||||
}
|
||||
|
||||
func interactiveGitIgnore(client *http.Client, hostname string, prompter iprompter) (string, error) {
|
||||
|
|
|
|||
|
|
@ -492,18 +492,18 @@ func Test_createRun(t *testing.T) {
|
|||
return config.NewBlankConfig(), nil
|
||||
}
|
||||
|
||||
cs, restoreRun := run.Stub()
|
||||
defer restoreRun(t)
|
||||
if tt.execStubs != nil {
|
||||
tt.execStubs(cs)
|
||||
}
|
||||
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
ios.SetStdinTTY(tt.tty)
|
||||
ios.SetStdoutTTY(tt.tty)
|
||||
tt.opts.IO = ios
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cs, restoreRun := run.Stub()
|
||||
defer restoreRun(t)
|
||||
if tt.execStubs != nil {
|
||||
tt.execStubs(cs)
|
||||
}
|
||||
|
||||
defer reg.Verify(t)
|
||||
err := createRun(tt.opts)
|
||||
if tt.wantErr {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import (
|
|||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
"github.com/cli/cli/v2/pkg/cmd/repo/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
|
|
@ -279,7 +278,7 @@ func forkRun(opts *ForkOptions) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = run.PrepareCmd(renameCmd).Run()
|
||||
err = renameCmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/context"
|
||||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
|
|
@ -19,6 +20,7 @@ type Factory struct {
|
|||
IOStreams *iostreams.IOStreams
|
||||
Prompter prompter.Prompter
|
||||
Browser browser.Browser
|
||||
GitClient *git.Client
|
||||
|
||||
HttpClient func() (*http.Client, error)
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue