git-lfs/git/git.go

463 lines
13 KiB
Go
Raw Normal View History

2014-09-24 17:10:29 +00:00
// Package git contains various commands that shell out to git
package git
import (
"bufio"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
2015-05-13 19:43:41 +00:00
2015-05-25 18:20:50 +00:00
"github.com/github/git-lfs/vendor/_nuts/github.com/rubyist/tracerx"
)
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")
}
2015-04-24 17:43:29 +00:00
if remoteRef == "" {
2015-06-15 11:52:35 +00:00
return simpleExec("git", "ls-remote", remote)
}
2015-06-15 11:52:35 +00:00
return simpleExec("git", "ls-remote", remote, remoteRef)
}
func ResolveRef(ref string) (*Ref, error) {
outp, err := simpleExec("git", "rev-parse", ref, "--symbolic-full-name", ref)
if err != nil {
return nil, err
}
lines := strings.Split(outp, "\n")
if len(lines) <= 1 {
return nil, fmt.Errorf("Git can't resolve ref: %q", ref)
}
fullref := &Ref{Sha: lines[0]}
fullref.Type, fullref.Name = ParseRefToTypeAndName(lines[1])
return fullref, nil
}
func CurrentRef() (*Ref, error) {
return ResolveRef("HEAD")
}
func CurrentBranch() (string, error) {
2015-06-15 11:52:35 +00:00
return simpleExec("git", "rev-parse", "--abbrev-ref", "HEAD")
}
func CurrentRemoteRef() (*Ref, error) {
remote, err := CurrentRemote()
if err != nil {
return nil, err
}
return ResolveRef(remote)
}
2014-10-27 16:52:28 +00:00
func CurrentRemote() (string, error) {
branch, err := CurrentBranch()
if err != nil {
return "", err
}
if branch == "HEAD" {
return "", errors.New("not on a branch")
}
remote := Config.Find(fmt.Sprintf("branch.%s.remote", branch))
if remote == "" {
return "", errors.New("remote not found")
}
return remote + "/" + branch, nil
}
func UpdateIndex(file string) error {
2015-06-15 11:52:35 +00:00
_, err := simpleExec("git", "update-index", "-q", "--refresh", file)
return err
}
type gitConfig struct {
}
var Config = &gitConfig{}
2014-09-24 17:10:29 +00:00
// Find returns the git config value for the key
func (c *gitConfig) Find(val string) string {
2015-06-15 11:52:35 +00:00
output, _ := simpleExec("git", "config", val)
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 {
output, _ := simpleExec("git", "config", "--local", val)
return output
}
2014-09-24 17:10:29 +00:00
// SetGlobal sets the git config value for the key in the global config
func (c *gitConfig) SetGlobal(key, val string) {
simpleExec("git", "config", "--global", key, val)
}
2015-05-29 23:49:11 +00:00
// UnsetGlobal removes the git config value for the key from the global config
func (c *gitConfig) UnsetGlobal(key string) {
2015-06-15 11:52:35 +00:00
simpleExec("git", "config", "--global", "--unset", key)
}
func (c *gitConfig) UnsetGlobalSection(key string) {
simpleExec("git", "config", "--global", "--remove-section", key)
}
// SetLocal sets the git config value for the key in the specified config file
func (c *gitConfig) SetLocal(file, key, val string) {
args := make([]string, 1, 5)
args[0] = "config"
if len(file) > 0 {
args = append(args, "--file", file)
}
args = append(args, key, val)
simpleExec("git", args...)
}
// UnsetLocalKey removes the git config value for the key from the specified config file
func (c *gitConfig) UnsetLocalKey(file, key string) {
args := make([]string, 1, 5)
args[0] = "config"
if len(file) > 0 {
args = append(args, "--file", file)
}
args = append(args, "--unset", key)
simpleExec("git", args...)
}
2014-09-24 17:10:29 +00:00
// List lists all of the git config values
func (c *gitConfig) List() (string, error) {
2015-06-15 11:52:35 +00:00
return simpleExec("git", "config", "-l")
}
2014-09-24 17:10:29 +00:00
// ListFromFile lists all of the git config values in the given config file
func (c *gitConfig) ListFromFile(f string) (string, error) {
if _, err := os.Stat(f); os.IsNotExist(err) {
return "", nil
}
2015-06-15 11:52:35 +00:00
return simpleExec("git", "config", "-l", "-f", f)
}
2014-09-24 17:10:29 +00:00
// Version returns the git version
func (c *gitConfig) Version() (string, error) {
2015-06-15 11:52:35 +00:00
return simpleExec("git", "version")
}
// 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-06-15 11:52:35 +00:00
// simpleExec is a small wrapper around os/exec.Command.
2015-06-15 11:55:03 +00:00
func simpleExec(name string, args ...string) (string, error) {
tracerx.Printf("run_command: '%s' %s", name, strings.Join(args, " "))
cmd := execCommand(name, args...)
output, err := cmd.Output()
if _, ok := err.(*exec.ExitError); ok {
return "", nil
2015-05-12 08:45:06 +00:00
}
if err != nil {
2015-06-15 11:55:03 +00:00
return fmt.Sprintf("Error running %s %s", name, args), err
}
return strings.Trim(string(output), " \n"), nil
}
// RecentBranches returns branches with commit dates on or after the given date/time
// Return full Ref type for easier detection of duplicate SHAs etc
// 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
func RecentBranches(since time.Time, includeRemoteBranches bool, onlyRemote string) ([]*Ref, error) {
cmd := execCommand("git", "for-each-ref",
`--sort=-committerdate`,
`--format=%(refname) %(objectname) %(committerdate:iso)`,
"refs")
outp, err := cmd.StdoutPipe()
if err != nil {
return nil, fmt.Errorf("Failed to call git for-each-ref: %v", err)
}
cmd.Start()
scanner := bufio.NewScanner(outp)
// Output is like this:
// refs/heads/master f03686b324b29ff480591745dbfbbfa5e5ac1bd5 2015-08-19 16:50:37 +0100
// refs/remotes/origin/master ad3b29b773e46ad6870fdf08796c33d97190fe93 2015-08-13 16:50:37 +0100
// Output is ordered by latest commit date first, so we can stop at the threshold
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)
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
commitDate, err := ParseGitDate(match[3])
if err != nil {
return ret, err
}
if commitDate.Before(since) {
// the end
break
}
2015-10-14 14:27:11 +00:00
tracerx.Printf("RECENT: %v (%v)", ref, commitDate)
ret = append(ret, &Ref{ref, reftype, sha})
}
}
cmd.Wait()
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")
}
// Get summary information about a commit
func GetCommitSummary(commit string) (*CommitSummary, error) {
cmd := execCommand("git", "show", "-s",
`--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))
}
// 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))
return nil, errors.New(msg)
}
}
// 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)
}
// 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)
}
}
return actual >= atleast
}