/
system_git.go
157 lines (130 loc) · 4.57 KB
/
system_git.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package git
import (
"bytes"
"errors"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
const (
// GitCommandDateLayout corresponds to `git log --date=format:%FT%T%z` date format.
GitCommandDateLayout = "2006-01-02T15:04:05-0700"
gitPrettyFormat = "%H\n%an\n%cn\n%cd\n%s"
gitDateFormat = "format:%FT%T%z"
)
var (
gitPrettyFormatFieldsNum = strings.Count(gitPrettyFormat, "\n") + 1
errUnexpectedExit = errors.New("unexpected exit")
)
type systemGit string
// SystemGit returns an object that wraps system git command and implements git.Command interface.
// If git is not found in PATH an error will be returned.
func SystemGit() (systemGit, error) {
cmd, err := exec.LookPath("git")
if err != nil {
return systemGit(""), errors.New("git is not found in PATH")
}
return systemGit(cmd), nil
}
// Exec runs specified Git command in path and returns its output. If Git returns a non-zero
// status, an error is returned and output contains error details.
func (gitCmd systemGit) Exec(path, command string, args ...string) (output []byte, err error) {
cmd := exec.Command(string(gitCmd), append([]string{command}, args...)...)
cmd.Dir = path
output, err = cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return nil, errors.New(string(exitErr.Stderr))
} else {
return nil, errUnexpectedExit
}
}
return bytes.TrimSpace(output), nil
}
// IsRepository checks if there if `path` is a git repository.
func (gitCmd systemGit) IsRepository(path string) bool {
if fileInfo, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return false
}
log.Printf("[WARN] failed to stat %s (%s)", path, err)
return false
} else if !fileInfo.IsDir() {
return false
}
output, err := gitCmd.Exec(path, "rev-parse", "--is-inside-git-dir")
if err == errUnexpectedExit {
log.Printf("[WARN] git rev-parse --is-inside-git-dir returned %s for %s (%s)", err, path, string(output))
} else if err != nil {
return false
}
switch string(output) {
case "true":
return true
case "false":
return false
default:
log.Printf("[WARN] git rev-parse --is-inside-git-dir returned unexpected output for %s: %q", path, string(output))
return false
}
}
// CurrentBranch returns the name of current branch in `path`.
func (gitCmd systemGit) CurrentBranch(path string) string {
refName, err := gitCmd.Exec(path, "symbolic-ref", "HEAD")
if err != nil {
log.Printf("[WARN] git symbolic-ref HEAD returned %s for %s (%s)", err, path, string(refName))
return DefaultMaster
}
if !bytes.HasPrefix(refName, []byte("refs/heads/")) {
log.Printf("[WARN] unexpected reference name for %s (%q)", path, refName)
return DefaultMaster
}
return string(bytes.TrimPrefix(refName, []byte("refs/heads/")))
}
// LastCommit returns the latest commit from `path`.
func (gitCmd systemGit) LastCommit(path string) (commit Commit, err error) {
output, err := gitCmd.Exec(path, "log", "-n", "1", "--pretty="+gitPrettyFormat, "--date="+gitDateFormat)
if err != nil {
log.Printf("[WARN] git log returned error %s for %s (%s)", err, path, string(output))
return commit, nil
}
lines := strings.SplitN(string(output), "\n", gitPrettyFormatFieldsNum)
if len(lines) < gitPrettyFormatFieldsNum {
log.Printf("[WARN] unexpected output from git log for %s (%s)", path, string(output))
return commit, nil
}
commit.SHA, commit.Author, commit.Committer, commit.Message = lines[0], lines[1], lines[2], lines[4]
commit.Date, err = time.Parse(GitCommandDateLayout, lines[3])
if err != nil {
log.Printf("[WARN] unexpected date format from git log for %s (%s)", path, lines[3])
commit.Date = time.Time{}
}
return commit, nil
}
// CloneMirror performs mirror clone of specified git URL to `path`.
func (gitCmd systemGit) CloneMirror(gitURL, path string) error {
dir, projectName := filepath.Dir(path), filepath.Base(path)
if err := os.MkdirAll(dir, 0755); err != nil {
log.Printf("failed to create %s (%s)", dir, err)
return fmt.Errorf("failed to clone %s to %s", gitURL, path)
}
output, err := gitCmd.Exec(dir, "clone", "--mirror", gitURL, projectName)
if err != nil {
log.Printf("git clone --mirror %s to %s returned %s (%s)", gitURL, path, err, string(output))
return fmt.Errorf("failed to clone %s to %s", gitURL, path)
}
return nil
}
// UpdateRemote does `git remote update` in specified `path`.
func (gitCmd systemGit) UpdateRemote(path string) error {
output, err := gitCmd.Exec(path, "remote", "update")
if err != nil {
log.Printf("[WARN] git remote update returned %s for %s (%s)", err, path, string(output))
return errors.New("update failed")
}
return nil
}