cli/git/git.go
vilmibm 1831d95433 isolated clone command
This commit hacks the existing repo clone tests into something usable by
the new isolated command. It went ok and was less effort than trying to
introduce the same kind of test format as repo view and gist create.
2020-07-23 16:40:31 -05:00

293 lines
6.8 KiB
Go

package git
import (
"bytes"
"errors"
"fmt"
"net/url"
"os"
"os/exec"
"path"
"regexp"
"strings"
"github.com/cli/cli/internal/run"
)
// ErrNotOnAnyBranch indicates that the users 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
}
// 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 := exec.Command("git", args...)
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
}
// CurrentBranch reads the checked-out branch for the git repository
func CurrentBranch() (string, error) {
refCmd := GitCommand("symbolic-ref", "--quiet", "--short", "HEAD")
output, err := run.PrepareCmd(refCmd).Output()
if err == nil {
// Found the branch name
return firstLine(output), nil
}
var cmdErr *run.CmdError
if errors.As(err, &cmdErr) {
if cmdErr.Stderr.Len() == 0 {
// Detached head
return "", ErrNotOnAnyBranch
}
}
// Unknown error
return "", err
}
func listRemotes() ([]string, error) {
remoteCmd := exec.Command("git", "remote", "-v")
output, err := run.PrepareCmd(remoteCmd).Output()
return outputLines(output), err
}
func Config(name string) (string, error) {
configCmd := exec.Command("git", "config", name)
output, err := run.PrepareCmd(configCmd).Output()
if err != nil {
return "", fmt.Errorf("unknown config key: %s", name)
}
return firstLine(output), nil
}
var GitCommand = func(args ...string) *exec.Cmd {
return exec.Command("git", args...)
}
func UncommittedChangeCount() (int, error) {
statusCmd := GitCommand("status", "--porcelain")
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
}
func Commits(baseRef, headRef string) ([]*Commit, error) {
logCmd := GitCommand(
"-c", "log.ShowSignature=false",
"log", "--pretty=format:%H,%s",
"--cherry", fmt.Sprintf("%s...%s", baseRef, headRef))
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 CommitBody(sha string) (string, error) {
showCmd := GitCommand("-c", "log.ShowSignature=false", "show", "-s", "--pretty=format:%b", sha)
output, err := run.PrepareCmd(showCmd).Output()
if err != nil {
return "", err
}
return string(output), nil
}
// Push publishes a git ref to a remote and sets up upstream configuration
func Push(remote string, ref string) error {
pushCmd := GitCommand("push", "--set-upstream", remote, ref)
pushCmd.Stdout = os.Stdout
pushCmd.Stderr = os.Stderr
return run.PrepareCmd(pushCmd).Run()
}
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 := GitCommand("config", "--get-regexp", fmt.Sprintf("^%s(remote|merge)$", prefix))
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
}
func DeleteLocalBranch(branch string) error {
branchCmd := GitCommand("branch", "-D", branch)
err := run.PrepareCmd(branchCmd).Run()
return err
}
func HasLocalBranch(branch string) bool {
configCmd := GitCommand("rev-parse", "--verify", "refs/heads/"+branch)
_, err := run.PrepareCmd(configCmd).Output()
return err == nil
}
func CheckoutBranch(branch string) error {
configCmd := GitCommand("checkout", branch)
err := run.PrepareCmd(configCmd).Run()
return err
}
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 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"))
}
cloneArgs = append([]string{"clone"}, cloneArgs...)
cloneCmd := GitCommand(cloneArgs...)
cloneCmd.Stdin = os.Stdin
cloneCmd.Stdout = os.Stdout
cloneCmd.Stderr = os.Stderr
err = run.PrepareCmd(cloneCmd).Run()
return
}
func AddUpstreamRemote(upstreamURL, cloneDir string) error {
cloneCmd := GitCommand("-C", cloneDir, "remote", "add", "-f", "upstream", upstreamURL)
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 := exec.Command("git", "rev-parse", "--show-toplevel")
output, err := run.PrepareCmd(showCmd).Output()
return firstLine(output), err
}
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)
}