2014-09-24 17:10:29 +00:00
|
|
|
// Package git contains various commands that shell out to git
|
2016-05-18 10:43:42 +00:00
|
|
|
// NOTE: Subject to change, do not rely on this package from outside git-lfs source
|
2014-09-23 15:40:23 +00:00
|
|
|
package git
|
|
|
|
|
|
|
|
import (
|
2015-08-17 15:29:13 +00:00
|
|
|
"bufio"
|
2016-02-03 19:06:23 +00:00
|
|
|
"bytes"
|
2014-09-23 21:42:47 +00:00
|
|
|
"errors"
|
2014-09-23 15:40:23 +00:00
|
|
|
"fmt"
|
2015-10-08 16:41:07 +00:00
|
|
|
"io/ioutil"
|
2015-06-15 08:50:37 +00:00
|
|
|
"os"
|
2015-09-30 22:16:14 +00:00
|
|
|
"path/filepath"
|
2015-08-17 15:29:13 +00:00
|
|
|
"regexp"
|
2015-10-08 16:40:38 +00:00
|
|
|
"strconv"
|
2014-09-23 15:40:23 +00:00
|
|
|
"strings"
|
2015-08-17 15:29:13 +00:00
|
|
|
"time"
|
2015-05-13 19:43:41 +00:00
|
|
|
|
2016-03-02 15:16:06 +00:00
|
|
|
"github.com/github/git-lfs/subprocess"
|
2016-05-23 18:02:27 +00:00
|
|
|
"github.com/rubyist/tracerx"
|
2014-09-23 15:40:23 +00:00
|
|
|
)
|
|
|
|
|
2015-08-17 15:29:13 +00:00
|
|
|
type RefType int
|
|
|
|
|
|
|
|
const (
|
|
|
|
RefTypeLocalBranch = RefType(iota)
|
|
|
|
RefTypeRemoteBranch = RefType(iota)
|
|
|
|
RefTypeLocalTag = RefType(iota)
|
|
|
|
RefTypeRemoteTag = RefType(iota)
|
|
|
|
RefTypeHEAD = RefType(iota) // current checkout
|
|
|
|
RefTypeOther = RefType(iota) // stash or unknown
|
|
|
|
)
|
|
|
|
|
|
|
|
// A git reference (branch, tag etc)
|
|
|
|
type Ref struct {
|
|
|
|
Name string
|
|
|
|
Type RefType
|
|
|
|
Sha string
|
|
|
|
}
|
|
|
|
|
|
|
|
// Some top level information about a commit (only first line of message)
|
|
|
|
type CommitSummary struct {
|
|
|
|
Sha string
|
|
|
|
ShortSha string
|
|
|
|
Parents []string
|
|
|
|
CommitDate time.Time
|
|
|
|
AuthorDate time.Time
|
|
|
|
AuthorName string
|
|
|
|
AuthorEmail string
|
|
|
|
CommitterName string
|
|
|
|
CommitterEmail string
|
|
|
|
Subject string
|
|
|
|
}
|
|
|
|
|
2015-04-24 17:43:29 +00:00
|
|
|
func LsRemote(remote, remoteRef string) (string, error) {
|
|
|
|
if remote == "" {
|
|
|
|
return "", errors.New("remote required")
|
2014-09-29 19:35:59 +00:00
|
|
|
}
|
2015-04-24 17:43:29 +00:00
|
|
|
if remoteRef == "" {
|
2016-03-02 15:16:06 +00:00
|
|
|
return subprocess.SimpleExec("git", "ls-remote", remote)
|
2014-09-29 19:35:59 +00:00
|
|
|
|
|
|
|
}
|
2016-03-02 15:16:06 +00:00
|
|
|
return subprocess.SimpleExec("git", "ls-remote", remote, remoteRef)
|
2014-09-29 19:35:59 +00:00
|
|
|
}
|
|
|
|
|
2015-08-20 14:36:10 +00:00
|
|
|
func ResolveRef(ref string) (*Ref, error) {
|
2016-03-02 15:16:06 +00:00
|
|
|
outp, err := subprocess.SimpleExec("git", "rev-parse", ref, "--symbolic-full-name", ref)
|
2015-08-20 14:36:10 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
lines := strings.Split(outp, "\n")
|
2015-09-17 16:43:42 +00:00
|
|
|
if len(lines) <= 1 {
|
2015-09-17 21:03:11 +00:00
|
|
|
return nil, fmt.Errorf("Git can't resolve ref: %q", ref)
|
2015-09-17 16:43:42 +00:00
|
|
|
}
|
2015-09-17 21:03:11 +00:00
|
|
|
|
2015-08-20 14:36:10 +00:00
|
|
|
fullref := &Ref{Sha: lines[0]}
|
|
|
|
fullref.Type, fullref.Name = ParseRefToTypeAndName(lines[1])
|
|
|
|
return fullref, nil
|
2015-05-21 14:36:42 +00:00
|
|
|
}
|
|
|
|
|
2016-04-06 16:29:59 +00:00
|
|
|
func ResolveRefs(refnames []string) ([]*Ref, error) {
|
|
|
|
refs := make([]*Ref, len(refnames))
|
|
|
|
for i, name := range refnames {
|
|
|
|
ref, err := ResolveRef(name)
|
|
|
|
if err != nil {
|
|
|
|
return refs, err
|
|
|
|
}
|
|
|
|
|
|
|
|
refs[i] = ref
|
|
|
|
}
|
|
|
|
return refs, nil
|
|
|
|
}
|
|
|
|
|
2015-08-20 14:36:10 +00:00
|
|
|
func CurrentRef() (*Ref, error) {
|
2015-05-21 14:36:42 +00:00
|
|
|
return ResolveRef("HEAD")
|
2014-10-28 19:22:38 +00:00
|
|
|
}
|
|
|
|
|
2015-08-20 14:36:10 +00:00
|
|
|
func CurrentRemoteRef() (*Ref, error) {
|
2015-10-06 14:44:03 +00:00
|
|
|
remoteref, err := RemoteRefNameForCurrentBranch()
|
2014-10-28 19:22:38 +00:00
|
|
|
if err != nil {
|
2015-08-20 14:36:10 +00:00
|
|
|
return nil, err
|
2014-10-28 19:22:38 +00:00
|
|
|
}
|
|
|
|
|
2015-10-06 14:44:03 +00:00
|
|
|
return ResolveRef(remoteref)
|
2014-10-28 19:22:38 +00:00
|
|
|
}
|
|
|
|
|
2015-10-06 14:44:03 +00:00
|
|
|
// RemoteForCurrentBranch returns the name of the remote that the current branch is tracking
|
|
|
|
func RemoteForCurrentBranch() (string, error) {
|
2015-11-24 18:48:33 +00:00
|
|
|
ref, err := CurrentRef()
|
2015-10-06 14:44:03 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2015-11-24 18:48:33 +00:00
|
|
|
remote := RemoteForBranch(ref.Name)
|
2015-10-08 10:59:45 +00:00
|
|
|
if remote == "" {
|
2015-11-24 18:48:33 +00:00
|
|
|
return "", fmt.Errorf("remote not found for branch %q", ref.Name)
|
2015-10-08 10:59:45 +00:00
|
|
|
}
|
|
|
|
return remote, nil
|
2015-10-06 14:44:03 +00:00
|
|
|
}
|
|
|
|
|
2015-10-08 10:59:45 +00:00
|
|
|
// RemoteRefForCurrentBranch returns the full remote ref (remote/remotebranch) that the current branch is tracking
|
2015-10-06 14:44:03 +00:00
|
|
|
func RemoteRefNameForCurrentBranch() (string, error) {
|
2015-11-24 18:48:33 +00:00
|
|
|
ref, err := CurrentRef()
|
2014-10-27 16:52:28 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2015-11-24 18:48:33 +00:00
|
|
|
if ref.Type == RefTypeHEAD || ref.Type == RefTypeOther {
|
2014-10-27 16:52:28 +00:00
|
|
|
return "", errors.New("not on a branch")
|
|
|
|
}
|
|
|
|
|
2015-11-24 18:48:33 +00:00
|
|
|
remote := RemoteForBranch(ref.Name)
|
2014-10-27 16:52:28 +00:00
|
|
|
if remote == "" {
|
2015-11-24 18:48:33 +00:00
|
|
|
return "", fmt.Errorf("remote not found for branch %q", ref.Name)
|
2014-10-27 16:52:28 +00:00
|
|
|
}
|
|
|
|
|
2015-11-24 18:48:33 +00:00
|
|
|
remotebranch := RemoteBranchForLocalBranch(ref.Name)
|
2015-10-08 10:59:45 +00:00
|
|
|
|
|
|
|
return remote + "/" + remotebranch, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// RemoteForBranch returns the remote name that a given local branch is tracking (blank if none)
|
|
|
|
func RemoteForBranch(localBranch string) string {
|
|
|
|
return Config.Find(fmt.Sprintf("branch.%s.remote", localBranch))
|
|
|
|
}
|
|
|
|
|
|
|
|
// RemoteBranchForLocalBranch returns the name (only) of the remote branch that the local branch is tracking
|
|
|
|
// If no specific branch is configured, returns local branch name
|
|
|
|
func RemoteBranchForLocalBranch(localBranch string) string {
|
2015-10-06 14:44:03 +00:00
|
|
|
// get remote ref to track, may not be same name
|
2015-10-08 10:59:45 +00:00
|
|
|
merge := Config.Find(fmt.Sprintf("branch.%s.merge", localBranch))
|
2015-10-06 14:44:03 +00:00
|
|
|
if strings.HasPrefix(merge, "refs/heads/") {
|
2015-10-08 10:59:45 +00:00
|
|
|
return merge[11:]
|
2015-10-06 14:44:03 +00:00
|
|
|
} else {
|
2015-10-08 10:59:45 +00:00
|
|
|
return localBranch
|
2015-10-06 14:44:03 +00:00
|
|
|
}
|
|
|
|
|
2014-10-27 16:52:28 +00:00
|
|
|
}
|
|
|
|
|
2015-10-06 16:27:36 +00:00
|
|
|
func RemoteList() ([]string, error) {
|
2016-03-02 15:16:06 +00:00
|
|
|
cmd := subprocess.ExecCommand("git", "remote")
|
2015-10-06 16:27:36 +00:00
|
|
|
|
|
|
|
outp, err := cmd.StdoutPipe()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Failed to call git remote: %v", err)
|
|
|
|
}
|
|
|
|
cmd.Start()
|
2016-02-24 17:42:26 +00:00
|
|
|
defer cmd.Wait()
|
|
|
|
|
2015-10-06 16:27:36 +00:00
|
|
|
scanner := bufio.NewScanner(outp)
|
|
|
|
|
|
|
|
var ret []string
|
|
|
|
for scanner.Scan() {
|
|
|
|
ret = append(ret, strings.TrimSpace(scanner.Text()))
|
|
|
|
}
|
2016-02-24 17:42:26 +00:00
|
|
|
|
2015-10-06 16:27:36 +00:00
|
|
|
return ret, nil
|
|
|
|
}
|
|
|
|
|
2016-04-06 15:38:28 +00:00
|
|
|
// Refs returns all of the local and remote branches and tags for the current
|
|
|
|
// repository. Other refs (HEAD, refs/stash, git notes) are ignored.
|
|
|
|
func LocalRefs() ([]*Ref, error) {
|
|
|
|
cmd := subprocess.ExecCommand("git", "show-ref", "--heads", "--tags")
|
|
|
|
|
|
|
|
outp, err := cmd.StdoutPipe()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Failed to call git show-ref: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var refs []*Ref
|
2016-05-18 19:46:25 +00:00
|
|
|
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
|
|
return refs, err
|
|
|
|
}
|
|
|
|
|
2016-04-06 15:38:28 +00:00
|
|
|
scanner := bufio.NewScanner(outp)
|
|
|
|
for scanner.Scan() {
|
|
|
|
line := strings.TrimSpace(scanner.Text())
|
|
|
|
parts := strings.SplitN(line, " ", 2)
|
|
|
|
if len(parts) != 2 || len(parts[0]) != 40 || len(parts[1]) < 1 {
|
|
|
|
tracerx.Printf("Invalid line from git show-ref: %q", line)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
rtype, name := ParseRefToTypeAndName(parts[1])
|
|
|
|
if rtype != RefTypeLocalBranch && rtype != RefTypeLocalTag {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
refs = append(refs, &Ref{name, rtype, parts[0]})
|
|
|
|
}
|
|
|
|
|
2016-04-07 15:11:53 +00:00
|
|
|
return refs, cmd.Wait()
|
2016-04-06 15:38:28 +00:00
|
|
|
}
|
|
|
|
|
2015-10-06 16:35:58 +00:00
|
|
|
// ValidateRemote checks that a named remote is valid for use
|
|
|
|
// Mainly to check user-supplied remotes & fail more nicely
|
|
|
|
func ValidateRemote(remote string) error {
|
|
|
|
remotes, err := RemoteList()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for _, r := range remotes {
|
|
|
|
if r == remote {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return errors.New("Invalid remote name")
|
|
|
|
}
|
|
|
|
|
2015-10-06 16:27:36 +00:00
|
|
|
// DefaultRemote returns the default remote based on:
|
|
|
|
// 1. The currently tracked remote branch, if present
|
|
|
|
// 2. "origin", if defined
|
|
|
|
// 3. Any other SINGLE remote defined in .git/config
|
|
|
|
// Returns an error if all of these fail, i.e. no tracked remote branch, no
|
|
|
|
// "origin", and either no remotes defined or 2+ non-"origin" remotes
|
|
|
|
func DefaultRemote() (string, error) {
|
|
|
|
tracked, err := RemoteForCurrentBranch()
|
|
|
|
if err == nil {
|
|
|
|
return tracked, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, check what remotes are defined
|
|
|
|
remotes, err := RemoteList()
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
switch len(remotes) {
|
|
|
|
case 0:
|
|
|
|
return "", errors.New("No remotes defined")
|
|
|
|
case 1: // always use a single remote whether it's origin or otherwise
|
|
|
|
return remotes[0], nil
|
|
|
|
default:
|
|
|
|
for _, remote := range remotes {
|
|
|
|
// Use origin if present
|
|
|
|
if remote == "origin" {
|
|
|
|
return remote, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "", errors.New("Unable to pick default remote, too ambiguous")
|
2014-10-27 16:52:28 +00:00
|
|
|
}
|
|
|
|
|
2015-05-22 14:10:01 +00:00
|
|
|
func UpdateIndex(file string) error {
|
2016-03-02 15:16:06 +00:00
|
|
|
_, err := subprocess.SimpleExec("git", "update-index", "-q", "--refresh", file)
|
2015-05-22 14:10:01 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2014-09-23 21:42:47 +00:00
|
|
|
type gitConfig struct {
|
|
|
|
}
|
|
|
|
|
|
|
|
var Config = &gitConfig{}
|
|
|
|
|
2014-09-24 17:10:29 +00:00
|
|
|
// Find returns the git config value for the key
|
2014-09-23 21:42:47 +00:00
|
|
|
func (c *gitConfig) Find(val string) string {
|
2016-03-02 15:16:06 +00:00
|
|
|
output, _ := subprocess.SimpleExec("git", "config", val)
|
2014-09-23 21:42:47 +00:00
|
|
|
return output
|
|
|
|
}
|
|
|
|
|
2015-10-21 15:36:31 +00:00
|
|
|
// Find returns the git config value for the key
|
|
|
|
func (c *gitConfig) FindGlobal(val string) string {
|
2016-03-02 15:16:06 +00:00
|
|
|
output, _ := subprocess.SimpleExec("git", "config", "--global", val)
|
2015-10-21 15:36:31 +00:00
|
|
|
return output
|
|
|
|
}
|
|
|
|
|
2015-09-23 17:58:16 +00:00
|
|
|
// Find returns the git config value for the key
|
|
|
|
func (c *gitConfig) FindLocal(val string) string {
|
2016-03-02 15:16:06 +00:00
|
|
|
output, _ := subprocess.SimpleExec("git", "config", "--local", val)
|
2015-09-23 17:58:16 +00:00
|
|
|
return output
|
|
|
|
}
|
|
|
|
|
2014-09-24 17:10:29 +00:00
|
|
|
// SetGlobal sets the git config value for the key in the global config
|
2014-09-23 21:42:47 +00:00
|
|
|
func (c *gitConfig) SetGlobal(key, val string) {
|
2016-03-02 15:16:06 +00:00
|
|
|
subprocess.SimpleExec("git", "config", "--global", key, val)
|
2014-09-23 21:42:47 +00:00
|
|
|
}
|
|
|
|
|
2015-05-29 23:49:11 +00:00
|
|
|
// UnsetGlobal removes the git config value for the key from the global config
|
2014-09-23 21:42:47 +00:00
|
|
|
func (c *gitConfig) UnsetGlobal(key string) {
|
2016-03-02 15:16:06 +00:00
|
|
|
subprocess.SimpleExec("git", "config", "--global", "--unset", key)
|
2014-09-23 21:42:47 +00:00
|
|
|
}
|
|
|
|
|
2015-07-27 22:21:53 +00:00
|
|
|
func (c *gitConfig) UnsetGlobalSection(key string) {
|
2016-03-02 15:16:06 +00:00
|
|
|
subprocess.SimpleExec("git", "config", "--global", "--remove-section", key)
|
2015-07-27 22:21:53 +00:00
|
|
|
}
|
|
|
|
|
2015-06-02 15:19:59 +00:00
|
|
|
// SetLocal sets the git config value for the key in the specified config file
|
|
|
|
func (c *gitConfig) SetLocal(file, key, val string) {
|
2015-09-02 19:25:19 +00:00
|
|
|
args := make([]string, 1, 5)
|
|
|
|
args[0] = "config"
|
|
|
|
if len(file) > 0 {
|
|
|
|
args = append(args, "--file", file)
|
|
|
|
}
|
|
|
|
args = append(args, key, val)
|
2016-03-02 15:16:06 +00:00
|
|
|
subprocess.SimpleExec("git", args...)
|
2015-06-02 15:19:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// UnsetLocalKey removes the git config value for the key from the specified config file
|
|
|
|
func (c *gitConfig) UnsetLocalKey(file, key string) {
|
2015-09-02 19:25:19 +00:00
|
|
|
args := make([]string, 1, 5)
|
|
|
|
args[0] = "config"
|
|
|
|
if len(file) > 0 {
|
|
|
|
args = append(args, "--file", file)
|
|
|
|
}
|
|
|
|
args = append(args, "--unset", key)
|
2016-03-02 15:16:06 +00:00
|
|
|
subprocess.SimpleExec("git", args...)
|
2015-06-02 15:19:59 +00:00
|
|
|
}
|
|
|
|
|
2014-09-24 17:10:29 +00:00
|
|
|
// List lists all of the git config values
|
2014-09-23 21:42:47 +00:00
|
|
|
func (c *gitConfig) List() (string, error) {
|
2016-03-02 15:16:06 +00:00
|
|
|
return subprocess.SimpleExec("git", "config", "-l")
|
2014-09-23 21:42:47 +00:00
|
|
|
}
|
|
|
|
|
2014-09-24 17:10:29 +00:00
|
|
|
// ListFromFile lists all of the git config values in the given config file
|
2015-06-01 18:53:08 +00:00
|
|
|
func (c *gitConfig) ListFromFile(f string) (string, error) {
|
2016-03-02 15:16:06 +00:00
|
|
|
return subprocess.SimpleExec("git", "config", "-l", "-f", f)
|
2014-09-23 21:42:47 +00:00
|
|
|
}
|
|
|
|
|
2014-09-24 17:10:29 +00:00
|
|
|
// Version returns the git version
|
2014-09-23 21:42:47 +00:00
|
|
|
func (c *gitConfig) Version() (string, error) {
|
2016-03-02 15:16:06 +00:00
|
|
|
return subprocess.SimpleExec("git", "version")
|
2014-09-23 21:42:47 +00:00
|
|
|
}
|
|
|
|
|
2015-10-08 16:40:38 +00:00
|
|
|
// IsVersionAtLeast returns whether the git version is the one specified or higher
|
|
|
|
// argument is plain version string separated by '.' e.g. "2.3.1" but can omit minor/patch
|
|
|
|
func (c *gitConfig) IsGitVersionAtLeast(ver string) bool {
|
|
|
|
gitver, err := c.Version()
|
|
|
|
if err != nil {
|
|
|
|
tracerx.Printf("Error getting git version: %v", err)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return IsVersionAtLeast(gitver, ver)
|
|
|
|
}
|
|
|
|
|
2015-08-19 16:22:44 +00:00
|
|
|
// RecentBranches returns branches with commit dates on or after the given date/time
|
|
|
|
// Return full Ref type for easier detection of duplicate SHAs etc
|
2015-08-17 15:29:13 +00:00
|
|
|
// since: refs with commits on or after this date will be included
|
|
|
|
// includeRemoteBranches: true to include refs on remote branches
|
|
|
|
// onlyRemote: set to non-blank to only include remote branches on a single remote
|
2015-08-19 16:22:44 +00:00
|
|
|
func RecentBranches(since time.Time, includeRemoteBranches bool, onlyRemote string) ([]*Ref, error) {
|
2016-03-02 15:16:06 +00:00
|
|
|
cmd := subprocess.ExecCommand("git", "for-each-ref",
|
2015-08-17 15:29:13 +00:00
|
|
|
`--sort=-committerdate`,
|
2015-08-19 16:22:44 +00:00
|
|
|
`--format=%(refname) %(objectname) %(committerdate:iso)`,
|
2015-08-17 15:29:13 +00:00
|
|
|
"refs")
|
|
|
|
outp, err := cmd.StdoutPipe()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Failed to call git for-each-ref: %v", err)
|
|
|
|
}
|
|
|
|
cmd.Start()
|
2016-02-24 17:42:26 +00:00
|
|
|
defer cmd.Wait()
|
|
|
|
|
2015-08-17 15:29:13 +00:00
|
|
|
scanner := bufio.NewScanner(outp)
|
|
|
|
|
|
|
|
// Output is like this:
|
2015-08-19 16:22:44 +00:00
|
|
|
// refs/heads/master f03686b324b29ff480591745dbfbbfa5e5ac1bd5 2015-08-19 16:50:37 +0100
|
|
|
|
// refs/remotes/origin/master ad3b29b773e46ad6870fdf08796c33d97190fe93 2015-08-13 16:50:37 +0100
|
2015-08-17 15:29:13 +00:00
|
|
|
|
|
|
|
// Output is ordered by latest commit date first, so we can stop at the threshold
|
2015-08-19 16:22:44 +00:00
|
|
|
regex := regexp.MustCompile(`^(refs/[^/]+/\S+)\s+([0-9A-Za-z]{40})\s+(\d{4}-\d{2}-\d{2}\s+\d{2}\:\d{2}\:\d{2}\s+[\+\-]\d{4})`)
|
2015-10-14 14:27:11 +00:00
|
|
|
tracerx.Printf("RECENT: Getting refs >= %v", since)
|
2015-08-17 15:29:13 +00:00
|
|
|
var ret []*Ref
|
|
|
|
for scanner.Scan() {
|
|
|
|
line := scanner.Text()
|
|
|
|
if match := regex.FindStringSubmatch(line); match != nil {
|
|
|
|
fullref := match[1]
|
|
|
|
sha := match[2]
|
|
|
|
reftype, ref := ParseRefToTypeAndName(fullref)
|
|
|
|
if reftype == RefTypeRemoteBranch || reftype == RefTypeRemoteTag {
|
|
|
|
if !includeRemoteBranches {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if onlyRemote != "" && !strings.HasPrefix(ref, onlyRemote+"/") {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// This is a ref we might use
|
|
|
|
// Check the date
|
2015-08-19 16:22:44 +00:00
|
|
|
commitDate, err := ParseGitDate(match[3])
|
2015-08-17 15:29:13 +00:00
|
|
|
if err != nil {
|
|
|
|
return ret, err
|
|
|
|
}
|
2015-08-19 16:22:44 +00:00
|
|
|
if commitDate.Before(since) {
|
2015-08-17 15:29:13 +00:00
|
|
|
// the end
|
|
|
|
break
|
|
|
|
}
|
2015-10-14 14:27:11 +00:00
|
|
|
tracerx.Printf("RECENT: %v (%v)", ref, commitDate)
|
2015-08-17 15:29:13 +00:00
|
|
|
ret = append(ret, &Ref{ref, reftype, sha})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the type & name of a git reference
|
|
|
|
func ParseRefToTypeAndName(fullref string) (t RefType, name string) {
|
|
|
|
const localPrefix = "refs/heads/"
|
|
|
|
const remotePrefix = "refs/remotes/"
|
|
|
|
const remoteTagPrefix = "refs/remotes/tags/"
|
|
|
|
const localTagPrefix = "refs/tags/"
|
|
|
|
|
|
|
|
if fullref == "HEAD" {
|
|
|
|
name = fullref
|
|
|
|
t = RefTypeHEAD
|
|
|
|
} else if strings.HasPrefix(fullref, localPrefix) {
|
|
|
|
name = fullref[len(localPrefix):]
|
|
|
|
t = RefTypeLocalBranch
|
|
|
|
} else if strings.HasPrefix(fullref, remotePrefix) {
|
|
|
|
name = fullref[len(remotePrefix):]
|
|
|
|
t = RefTypeRemoteBranch
|
|
|
|
} else if strings.HasPrefix(fullref, remoteTagPrefix) {
|
|
|
|
name = fullref[len(remoteTagPrefix):]
|
|
|
|
t = RefTypeRemoteTag
|
|
|
|
} else if strings.HasPrefix(fullref, localTagPrefix) {
|
|
|
|
name = fullref[len(localTagPrefix):]
|
|
|
|
t = RefTypeLocalTag
|
|
|
|
} else {
|
|
|
|
name = fullref
|
|
|
|
t = RefTypeOther
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse a Git date formatted in ISO 8601 format (%ci/%ai)
|
|
|
|
func ParseGitDate(str string) (time.Time, error) {
|
|
|
|
|
|
|
|
// Unfortunately Go and Git don't overlap in their builtin date formats
|
|
|
|
// Go's time.RFC1123Z and Git's %cD are ALMOST the same, except that
|
|
|
|
// when the day is < 10 Git outputs a single digit, but Go expects a leading
|
|
|
|
// zero - this is enough to break the parsing. Sigh.
|
|
|
|
|
|
|
|
// Format is for 2 Jan 2006, 15:04:05 -7 UTC as per Go
|
|
|
|
return time.Parse("2006-01-02 15:04:05 -0700", str)
|
|
|
|
}
|
|
|
|
|
2015-08-21 14:19:16 +00:00
|
|
|
// FormatGitDate converts a Go date into a git command line format date
|
|
|
|
func FormatGitDate(tm time.Time) string {
|
|
|
|
// Git format is "Fri Jun 21 20:26:41 2013 +0900" but no zero-leading for day
|
|
|
|
return tm.Format("Mon Jan 2 15:04:05 2006 -0700")
|
|
|
|
}
|
|
|
|
|
2015-08-17 15:29:13 +00:00
|
|
|
// Get summary information about a commit
|
|
|
|
func GetCommitSummary(commit string) (*CommitSummary, error) {
|
2016-03-02 15:16:06 +00:00
|
|
|
cmd := subprocess.ExecCommand("git", "show", "-s",
|
2015-08-17 15:29:13 +00:00
|
|
|
`--format=%H|%h|%P|%ai|%ci|%ae|%an|%ce|%cn|%s`, commit)
|
|
|
|
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
|
|
if err != nil {
|
2015-08-21 14:46:01 +00:00
|
|
|
return nil, fmt.Errorf("Failed to call git show: %v %v", err, string(out))
|
2015-08-17 15:29:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// At most 10 substrings so subject line is not split on anything
|
|
|
|
fields := strings.SplitN(string(out), "|", 10)
|
|
|
|
// Cope with the case where subject is blank
|
|
|
|
if len(fields) >= 9 {
|
|
|
|
ret := &CommitSummary{}
|
|
|
|
// Get SHAs from output, not commit input, so we can support symbolic refs
|
|
|
|
ret.Sha = fields[0]
|
|
|
|
ret.ShortSha = fields[1]
|
|
|
|
ret.Parents = strings.Split(fields[2], " ")
|
|
|
|
// %aD & %cD (RFC2822) matches Go's RFC1123Z format
|
|
|
|
ret.AuthorDate, _ = ParseGitDate(fields[3])
|
|
|
|
ret.CommitDate, _ = ParseGitDate(fields[4])
|
|
|
|
ret.AuthorEmail = fields[5]
|
|
|
|
ret.AuthorName = fields[6]
|
|
|
|
ret.CommitterEmail = fields[7]
|
|
|
|
ret.CommitterName = fields[8]
|
|
|
|
if len(fields) > 9 {
|
|
|
|
ret.Subject = strings.TrimRight(fields[9], "\n")
|
|
|
|
}
|
|
|
|
return ret, nil
|
|
|
|
} else {
|
2015-08-21 14:46:01 +00:00
|
|
|
msg := fmt.Sprintf("Unexpected output from git show: %v", string(out))
|
2015-08-17 15:29:13 +00:00
|
|
|
return nil, errors.New(msg)
|
|
|
|
}
|
2016-02-01 18:20:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func GitAndRootDirs() (string, string, error) {
|
2016-03-02 15:16:06 +00:00
|
|
|
cmd := subprocess.ExecCommand("git", "rev-parse", "--git-dir", "--show-toplevel")
|
2016-02-03 19:06:23 +00:00
|
|
|
buf := &bytes.Buffer{}
|
|
|
|
cmd.Stderr = buf
|
|
|
|
|
|
|
|
out, err := cmd.Output()
|
2016-02-01 18:20:32 +00:00
|
|
|
output := string(out)
|
|
|
|
if err != nil {
|
2016-02-03 19:06:23 +00:00
|
|
|
return "", "", fmt.Errorf("Failed to call git rev-parse --git-dir --show-toplevel: %q", buf.String())
|
2016-02-01 18:20:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
paths := strings.Split(output, "\n")
|
|
|
|
pathLen := len(paths)
|
|
|
|
|
|
|
|
if pathLen == 0 {
|
|
|
|
return "", "", fmt.Errorf("Bad git rev-parse output: %q", output)
|
|
|
|
}
|
|
|
|
|
|
|
|
absGitDir, err := filepath.Abs(paths[0])
|
|
|
|
if err != nil {
|
|
|
|
return "", "", fmt.Errorf("Error converting %q to absolute: %s", paths[0], err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if pathLen == 1 || len(paths[1]) == 0 {
|
|
|
|
return absGitDir, "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
absRootDir, err := filepath.Abs(paths[1])
|
|
|
|
if err != nil {
|
|
|
|
return "", "", fmt.Errorf("Error converting %q to absolute: %s", paths[1], err)
|
|
|
|
}
|
2015-08-17 15:29:13 +00:00
|
|
|
|
2016-02-01 18:20:32 +00:00
|
|
|
return absGitDir, absRootDir, nil
|
2015-08-17 15:29:13 +00:00
|
|
|
}
|
2015-09-30 22:16:14 +00:00
|
|
|
|
|
|
|
func RootDir() (string, error) {
|
2016-03-02 15:16:06 +00:00
|
|
|
cmd := subprocess.ExecCommand("git", "rev-parse", "--show-toplevel")
|
2015-09-30 22:16:14 +00:00
|
|
|
out, err := cmd.Output()
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("Failed to call git rev-parse --show-toplevel: %v %v", err, string(out))
|
|
|
|
}
|
|
|
|
|
|
|
|
path := strings.TrimSpace(string(out))
|
|
|
|
if len(path) > 0 {
|
|
|
|
return filepath.Abs(path)
|
|
|
|
}
|
|
|
|
return "", nil
|
|
|
|
|
|
|
|
}
|
2015-11-09 17:46:29 +00:00
|
|
|
|
2015-09-30 22:16:14 +00:00
|
|
|
func GitDir() (string, error) {
|
2016-03-02 15:16:06 +00:00
|
|
|
cmd := subprocess.ExecCommand("git", "rev-parse", "--git-dir")
|
2015-09-30 22:16:14 +00:00
|
|
|
out, err := cmd.Output()
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("Failed to call git rev-parse --git-dir: %v %v", err, string(out))
|
|
|
|
}
|
|
|
|
path := strings.TrimSpace(string(out))
|
|
|
|
if len(path) > 0 {
|
|
|
|
return filepath.Abs(path)
|
|
|
|
}
|
|
|
|
return "", nil
|
2015-11-09 17:46:29 +00:00
|
|
|
}
|
|
|
|
|
2015-10-08 16:41:07 +00:00
|
|
|
// GetAllWorkTreeHEADs returns the refs that all worktrees are using as HEADs
|
|
|
|
// This returns all worktrees plus the master working copy, and works even if
|
|
|
|
// working dir is actually in a worktree right now
|
|
|
|
// Pass in the git storage dir (parent of 'objects') to work from
|
|
|
|
func GetAllWorkTreeHEADs(storageDir string) ([]*Ref, error) {
|
|
|
|
worktreesdir := filepath.Join(storageDir, "worktrees")
|
|
|
|
dirf, err := os.Open(worktreesdir)
|
|
|
|
if err != nil && !os.IsNotExist(err) {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var worktrees []*Ref
|
|
|
|
if err == nil {
|
|
|
|
// There are some worktrees
|
|
|
|
defer dirf.Close()
|
|
|
|
direntries, err := dirf.Readdir(0)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
for _, dirfi := range direntries {
|
|
|
|
if dirfi.IsDir() {
|
|
|
|
// to avoid having to chdir and run git commands to identify the commit
|
|
|
|
// just read the HEAD file & git rev-parse if necessary
|
|
|
|
// Since the git repo is shared the same rev-parse will work from this location
|
|
|
|
headfile := filepath.Join(worktreesdir, dirfi.Name(), "HEAD")
|
|
|
|
ref, err := parseRefFile(headfile)
|
|
|
|
if err != nil {
|
|
|
|
tracerx.Printf("Error reading %v for worktree, skipping: %v", headfile, err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
worktrees = append(worktrees, ref)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// This has only established the separate worktrees, not the original checkout
|
|
|
|
// If the storageDir contains a HEAD file then there is a main checkout
|
|
|
|
// as well; this mus tbe resolveable whether you're in the main checkout or
|
|
|
|
// a worktree
|
|
|
|
headfile := filepath.Join(storageDir, "HEAD")
|
|
|
|
ref, err := parseRefFile(headfile)
|
|
|
|
if err == nil {
|
|
|
|
worktrees = append(worktrees, ref)
|
|
|
|
} else if !os.IsNotExist(err) { // ok if not exists, probably bare repo
|
|
|
|
tracerx.Printf("Error reading %v for main checkout, skipping: %v", headfile, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return worktrees, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Manually parse a reference file like HEAD and return the Ref it resolves to
|
|
|
|
func parseRefFile(filename string) (*Ref, error) {
|
|
|
|
bytes, err := ioutil.ReadFile(filename)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
contents := strings.TrimSpace(string(bytes))
|
|
|
|
if strings.HasPrefix(contents, "ref:") {
|
|
|
|
contents = strings.TrimSpace(contents[4:])
|
|
|
|
}
|
|
|
|
return ResolveRef(contents)
|
|
|
|
}
|
|
|
|
|
2015-10-08 16:40:38 +00:00
|
|
|
// IsVersionAtLeast compares 2 version strings (ok to be prefixed with 'git version', ignores)
|
|
|
|
func IsVersionAtLeast(actualVersion, desiredVersion string) bool {
|
|
|
|
// Capture 1-3 version digits, optionally prefixed with 'git version' and possibly
|
|
|
|
// with suffixes which we'll ignore (e.g. unstable builds, MinGW versions)
|
|
|
|
verregex := regexp.MustCompile(`(?:git version\s+)?(\d+)(?:.(\d+))?(?:.(\d+))?.*`)
|
|
|
|
|
|
|
|
var atleast uint64
|
|
|
|
// Support up to 1000 in major/minor/patch digits
|
|
|
|
const majorscale = 1000 * 1000
|
|
|
|
const minorscale = 1000
|
|
|
|
|
|
|
|
if match := verregex.FindStringSubmatch(desiredVersion); match != nil {
|
|
|
|
// Ignore errors as regex won't match anything other than digits
|
|
|
|
major, _ := strconv.Atoi(match[1])
|
|
|
|
atleast += uint64(major * majorscale)
|
|
|
|
if len(match) > 2 {
|
|
|
|
minor, _ := strconv.Atoi(match[2])
|
|
|
|
atleast += uint64(minor * minorscale)
|
|
|
|
}
|
|
|
|
if len(match) > 3 {
|
|
|
|
patch, _ := strconv.Atoi(match[3])
|
|
|
|
atleast += uint64(patch)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var actual uint64
|
|
|
|
if match := verregex.FindStringSubmatch(actualVersion); match != nil {
|
|
|
|
major, _ := strconv.Atoi(match[1])
|
|
|
|
actual += uint64(major * majorscale)
|
|
|
|
if len(match) > 2 {
|
|
|
|
minor, _ := strconv.Atoi(match[2])
|
|
|
|
actual += uint64(minor * minorscale)
|
|
|
|
}
|
|
|
|
if len(match) > 3 {
|
|
|
|
patch, _ := strconv.Atoi(match[3])
|
|
|
|
actual += uint64(patch)
|
|
|
|
}
|
|
|
|
}
|
2015-09-30 22:16:14 +00:00
|
|
|
|
2015-10-08 16:40:38 +00:00
|
|
|
return actual >= atleast
|
2015-09-30 22:16:14 +00:00
|
|
|
}
|
2016-02-03 19:06:23 +00:00
|
|
|
|
2016-04-19 10:52:21 +00:00
|
|
|
// For compatibility with git clone we must mirror all flags in CloneWithoutFilters
|
|
|
|
type CloneFlags struct {
|
|
|
|
// --template <template_directory>
|
|
|
|
TemplateDirectory string
|
|
|
|
// -l --local
|
|
|
|
Local bool
|
|
|
|
// -s --shared
|
|
|
|
Shared bool
|
|
|
|
// --no-hardlinks
|
|
|
|
NoHardlinks bool
|
|
|
|
// -q --quiet
|
|
|
|
Quiet bool
|
|
|
|
// -n --no-checkout
|
|
|
|
NoCheckout bool
|
|
|
|
// --progress
|
|
|
|
Progress bool
|
|
|
|
// --bare
|
|
|
|
Bare bool
|
|
|
|
// --mirror
|
|
|
|
Mirror bool
|
|
|
|
// -o <name> --origin <name>
|
|
|
|
Origin string
|
|
|
|
// -b <name> --branch <name>
|
|
|
|
Branch string
|
|
|
|
// -u <upload-pack> --upload-pack <pack>
|
|
|
|
Upload string
|
|
|
|
// --reference <repository>
|
|
|
|
Reference string
|
|
|
|
// --dissociate
|
|
|
|
Dissociate bool
|
|
|
|
// --separate-git-dir <git dir>
|
|
|
|
SeparateGit string
|
|
|
|
// --depth <depth>
|
|
|
|
Depth string
|
|
|
|
// --recursive
|
|
|
|
Recursive bool
|
|
|
|
// --recurse-submodules
|
|
|
|
RecurseSubmodules bool
|
|
|
|
// -c <value> --config <value>
|
|
|
|
Config string
|
|
|
|
// --single-branch
|
|
|
|
SingleBranch bool
|
|
|
|
// --no-single-branch
|
|
|
|
NoSingleBranch bool
|
2016-04-19 11:01:49 +00:00
|
|
|
// --verbose
|
|
|
|
Verbose bool
|
|
|
|
// --ipv4
|
|
|
|
Ipv4 bool
|
|
|
|
// --ipv6
|
|
|
|
Ipv6 bool
|
2016-04-19 10:52:21 +00:00
|
|
|
}
|
|
|
|
|
2016-02-05 17:45:12 +00:00
|
|
|
// CloneWithoutFilters clones a git repo but without the smudge filter enabled
|
|
|
|
// so that files in the working copy will be pointers and not real LFS data
|
2016-04-19 10:52:21 +00:00
|
|
|
func CloneWithoutFilters(flags CloneFlags, args []string) error {
|
2016-02-05 17:45:12 +00:00
|
|
|
|
2016-04-27 10:36:18 +00:00
|
|
|
// Before git 2.8, setting filters to blank causes lots of warnings, so use cat instead (slightly slower)
|
|
|
|
// Also pre 2.2 it failed completely. We used to use it anyway in git 2.2-2.7 and
|
|
|
|
// suppress the messages in stderr, but doing that with standard StderrPipe suppresses
|
|
|
|
// the git clone output (git thinks it's not a terminal) and makes it look like it's
|
|
|
|
// not working. You can get around that with https://github.com/kr/pty but that
|
|
|
|
// causes difficult issues with passing through Stdin for login prompts
|
|
|
|
// This way is simpler & more practical.
|
2016-02-15 11:04:04 +00:00
|
|
|
filterOverride := ""
|
2016-04-27 10:36:18 +00:00
|
|
|
if !Config.IsGitVersionAtLeast("2.8.0") {
|
2016-02-15 11:04:04 +00:00
|
|
|
filterOverride = "cat"
|
2016-02-11 17:48:37 +00:00
|
|
|
}
|
2016-02-05 17:45:12 +00:00
|
|
|
// Disable the LFS filters while cloning to speed things up
|
|
|
|
// this is especially effective on Windows where even calling git-lfs at all
|
|
|
|
// with --skip-smudge is costly across many files in a checkout
|
|
|
|
cmdargs := []string{
|
2016-02-15 11:04:04 +00:00
|
|
|
"-c", fmt.Sprintf("filter.lfs.smudge=%v", filterOverride),
|
2016-02-05 17:45:12 +00:00
|
|
|
"-c", "filter.lfs.required=false",
|
|
|
|
"clone"}
|
2016-04-19 10:52:21 +00:00
|
|
|
|
|
|
|
// flags
|
|
|
|
if flags.Bare {
|
|
|
|
cmdargs = append(cmdargs, "--bare")
|
|
|
|
}
|
|
|
|
if len(flags.Branch) > 0 {
|
|
|
|
cmdargs = append(cmdargs, "--branch", flags.Branch)
|
|
|
|
}
|
|
|
|
if len(flags.Config) > 0 {
|
|
|
|
cmdargs = append(cmdargs, "--config", flags.Config)
|
|
|
|
}
|
|
|
|
if len(flags.Depth) > 0 {
|
|
|
|
cmdargs = append(cmdargs, "--depth", flags.Depth)
|
|
|
|
}
|
|
|
|
if flags.Dissociate {
|
|
|
|
cmdargs = append(cmdargs, "--dissociate")
|
|
|
|
}
|
2016-04-19 11:01:49 +00:00
|
|
|
if flags.Ipv4 {
|
|
|
|
cmdargs = append(cmdargs, "--ipv4")
|
|
|
|
}
|
|
|
|
if flags.Ipv6 {
|
|
|
|
cmdargs = append(cmdargs, "--ipv6")
|
|
|
|
}
|
2016-04-19 10:52:21 +00:00
|
|
|
if flags.Local {
|
|
|
|
cmdargs = append(cmdargs, "--local")
|
|
|
|
}
|
|
|
|
if flags.Mirror {
|
|
|
|
cmdargs = append(cmdargs, "--mirror")
|
|
|
|
}
|
|
|
|
if flags.NoCheckout {
|
|
|
|
cmdargs = append(cmdargs, "--no-checkout")
|
|
|
|
}
|
|
|
|
if flags.NoHardlinks {
|
|
|
|
cmdargs = append(cmdargs, "--no-hardlinks")
|
|
|
|
}
|
|
|
|
if flags.NoSingleBranch {
|
|
|
|
cmdargs = append(cmdargs, "--no-single-branch")
|
|
|
|
}
|
|
|
|
if len(flags.Origin) > 0 {
|
|
|
|
cmdargs = append(cmdargs, "--origin", flags.Origin)
|
|
|
|
}
|
|
|
|
if flags.Progress {
|
|
|
|
cmdargs = append(cmdargs, "--progress")
|
|
|
|
}
|
|
|
|
if flags.Quiet {
|
|
|
|
cmdargs = append(cmdargs, "--quiet")
|
|
|
|
}
|
|
|
|
if flags.Recursive {
|
|
|
|
cmdargs = append(cmdargs, "--recursive")
|
|
|
|
}
|
|
|
|
if flags.RecurseSubmodules {
|
|
|
|
cmdargs = append(cmdargs, "--recurse-submodules")
|
|
|
|
}
|
|
|
|
if len(flags.Reference) > 0 {
|
|
|
|
cmdargs = append(cmdargs, "--reference", flags.Reference)
|
|
|
|
}
|
|
|
|
if len(flags.SeparateGit) > 0 {
|
|
|
|
cmdargs = append(cmdargs, "--separate-git-dir", flags.SeparateGit)
|
|
|
|
}
|
|
|
|
if flags.Shared {
|
|
|
|
cmdargs = append(cmdargs, "--shared")
|
|
|
|
}
|
|
|
|
if flags.SingleBranch {
|
|
|
|
cmdargs = append(cmdargs, "--single-branch")
|
|
|
|
}
|
|
|
|
if len(flags.TemplateDirectory) > 0 {
|
|
|
|
cmdargs = append(cmdargs, "--template", flags.TemplateDirectory)
|
|
|
|
}
|
|
|
|
if len(flags.Upload) > 0 {
|
|
|
|
cmdargs = append(cmdargs, "--upload-pack", flags.Upload)
|
|
|
|
}
|
2016-04-19 11:01:49 +00:00
|
|
|
if flags.Verbose {
|
|
|
|
cmdargs = append(cmdargs, "--verbose")
|
|
|
|
}
|
2016-04-19 10:52:21 +00:00
|
|
|
|
|
|
|
// Now args
|
2016-02-05 17:45:12 +00:00
|
|
|
cmdargs = append(cmdargs, args...)
|
2016-03-02 15:16:06 +00:00
|
|
|
cmd := subprocess.ExecCommand("git", cmdargs...)
|
2016-02-05 17:45:12 +00:00
|
|
|
|
2016-04-27 10:36:18 +00:00
|
|
|
// Assign all streams direct
|
|
|
|
cmd.Stdout = os.Stdout
|
|
|
|
cmd.Stderr = os.Stderr
|
|
|
|
cmd.Stdin = os.Stdin
|
2016-02-12 16:26:53 +00:00
|
|
|
|
2016-04-27 10:36:18 +00:00
|
|
|
err := cmd.Start()
|
2016-02-05 17:45:12 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Failed to start git clone: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = cmd.Wait()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("git clone failed: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-03-01 10:01:24 +00:00
|
|
|
// CachedRemoteRefs returns the list of branches & tags for a remote which are
|
|
|
|
// currently cached locally. No remote request is made to verify them.
|
|
|
|
func CachedRemoteRefs(remoteName string) ([]*Ref, error) {
|
2016-02-24 16:41:15 +00:00
|
|
|
|
2016-02-25 15:49:43 +00:00
|
|
|
var ret []*Ref
|
2016-03-02 15:16:06 +00:00
|
|
|
cmd := subprocess.ExecCommand("git", "show-ref")
|
2016-02-24 16:41:15 +00:00
|
|
|
|
2016-03-01 10:01:24 +00:00
|
|
|
outp, err := cmd.StdoutPipe()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Failed to call git show-ref: %v", err)
|
|
|
|
}
|
|
|
|
cmd.Start()
|
|
|
|
scanner := bufio.NewScanner(outp)
|
2016-02-24 16:41:15 +00:00
|
|
|
|
2016-03-01 10:01:24 +00:00
|
|
|
r := regexp.MustCompile(fmt.Sprintf(`([0-9a-fA-F]{40})\s+refs/remotes/%v/(.*)`, remoteName))
|
|
|
|
for scanner.Scan() {
|
|
|
|
if match := r.FindStringSubmatch(scanner.Text()); match != nil {
|
|
|
|
name := strings.TrimSpace(match[2])
|
|
|
|
// Don't match head
|
2016-03-01 10:06:32 +00:00
|
|
|
if name == "HEAD" {
|
|
|
|
continue
|
2016-02-24 16:41:15 +00:00
|
|
|
}
|
2016-03-01 10:06:32 +00:00
|
|
|
|
|
|
|
sha := match[1]
|
|
|
|
ret = append(ret, &Ref{name, RefTypeRemoteBranch, sha})
|
2016-02-24 16:41:15 +00:00
|
|
|
}
|
2016-03-01 10:01:24 +00:00
|
|
|
}
|
|
|
|
return ret, nil
|
|
|
|
}
|
2016-02-24 16:41:15 +00:00
|
|
|
|
2016-03-01 10:01:24 +00:00
|
|
|
// RemoteRefs returns a list of branches & tags for a remote by actually
|
|
|
|
// accessing the remote vir git ls-remote
|
|
|
|
func RemoteRefs(remoteName string) ([]*Ref, error) {
|
2016-02-24 16:41:15 +00:00
|
|
|
|
2016-03-01 10:01:24 +00:00
|
|
|
var ret []*Ref
|
2016-03-02 15:16:06 +00:00
|
|
|
cmd := subprocess.ExecCommand("git", "ls-remote", "--heads", "--tags", "-q", remoteName)
|
2016-03-01 10:01:24 +00:00
|
|
|
|
|
|
|
outp, err := cmd.StdoutPipe()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Failed to call git ls-remote: %v", err)
|
|
|
|
}
|
|
|
|
cmd.Start()
|
|
|
|
scanner := bufio.NewScanner(outp)
|
|
|
|
|
|
|
|
r := regexp.MustCompile(`([0-9a-fA-F]{40})\s+refs/(heads|tags)/(.*)`)
|
|
|
|
for scanner.Scan() {
|
|
|
|
if match := r.FindStringSubmatch(scanner.Text()); match != nil {
|
|
|
|
name := strings.TrimSpace(match[3])
|
|
|
|
// Don't match head
|
2016-03-01 10:06:32 +00:00
|
|
|
if name == "HEAD" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
sha := match[1]
|
|
|
|
if match[2] == "heads" {
|
|
|
|
ret = append(ret, &Ref{name, RefTypeRemoteBranch, sha})
|
|
|
|
} else {
|
|
|
|
ret = append(ret, &Ref{name, RefTypeRemoteTag, sha})
|
2016-02-24 16:41:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ret, nil
|
|
|
|
}
|
2016-03-22 10:38:10 +00:00
|
|
|
|
|
|
|
// GetTrackedFiles returns a list of files which are tracked in Git which match
|
|
|
|
// the pattern specified (standard wildcard form)
|
|
|
|
// Both pattern and the results are relative to the current working directory, not
|
|
|
|
// the root of the repository
|
|
|
|
func GetTrackedFiles(pattern string) ([]string, error) {
|
|
|
|
var ret []string
|
|
|
|
cmd := subprocess.ExecCommand("git",
|
|
|
|
"-c", "core.quotepath=false", // handle special chars in filenames
|
|
|
|
"ls-files",
|
|
|
|
"--cached", // include things which are staged but not committed right now
|
|
|
|
"--", // no ambiguous patterns
|
|
|
|
pattern)
|
|
|
|
|
|
|
|
outp, err := cmd.StdoutPipe()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Failed to call git ls-files: %v", err)
|
|
|
|
}
|
|
|
|
cmd.Start()
|
|
|
|
scanner := bufio.NewScanner(outp)
|
|
|
|
for scanner.Scan() {
|
|
|
|
line := scanner.Text()
|
|
|
|
ret = append(ret, strings.TrimSpace(line))
|
|
|
|
}
|
2016-04-04 20:41:43 +00:00
|
|
|
return ret, cmd.Wait()
|
2016-03-22 10:38:10 +00:00
|
|
|
|
|
|
|
}
|