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.
293 lines
6.8 KiB
Go
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)
|
|
}
|