git.go 8.8 KB
Newer Older
1
package git
J
Jingwen Owen Ou 已提交
2 3

import (
J
Jingwen Owen Ou 已提交
4
	"fmt"
J
Jingwen Owen Ou 已提交
5 6
	"io/ioutil"
	"os"
J
Jingwen Owen Ou 已提交
7
	"path/filepath"
J
Jingwen Owen Ou 已提交
8
	"strings"
J
Jingwen Owen Ou 已提交
9 10

	"github.com/github/hub/cmd"
J
Jingwen Owen Ou 已提交
11 12
)

13
var GlobalFlags []string
14

15
func Version() (string, error) {
16 17 18
	versionCmd := gitCmd("version")
	output, err := versionCmd.Output()
	if err != nil {
19
		return "", fmt.Errorf("error running git version: %s", err)
J
Jingwen Owen Ou 已提交
20
	}
21
	return firstLine(output), nil
J
Jingwen Owen Ou 已提交
22 23
}

24 25
var cachedDir string

26
func Dir() (string, error) {
27 28 29 30
	if cachedDir != "" {
		return cachedDir, nil
	}

31 32 33
	dirCmd := gitCmd("rev-parse", "-q", "--git-dir")
	dirCmd.Stderr = nil
	output, err := dirCmd.Output()
J
Jingwen Owen Ou 已提交
34
	if err != nil {
J
Jingwen Owen Ou 已提交
35
		return "", fmt.Errorf("Not a git repository (or any of the parent directories): .git")
J
Jingwen Owen Ou 已提交
36 37
	}

38 39
	var chdir string
	for i, flag := range GlobalFlags {
M
Mislav Marohnić 已提交
40
		if flag == "-C" {
41
			dir := GlobalFlags[i+1]
M
Mislav Marohnić 已提交
42
			if filepath.IsAbs(dir) {
43 44 45 46 47 48 49
				chdir = dir
			} else {
				chdir = filepath.Join(chdir, dir)
			}
		}
	}

50
	gitDir := firstLine(output)
51

M
Mislav Marohnić 已提交
52 53
	if !filepath.IsAbs(gitDir) {
		if chdir != "" {
54 55 56 57 58 59 60 61 62
			gitDir = filepath.Join(chdir, gitDir)
		}

		gitDir, err = filepath.Abs(gitDir)
		if err != nil {
			return "", err
		}

		gitDir = filepath.Clean(gitDir)
63 64
	}

65
	cachedDir = gitDir
66
	return gitDir, nil
J
Jingwen Owen Ou 已提交
67 68
}

69
func WorkdirName() (string, error) {
70 71 72 73 74 75
	toplevelCmd := gitCmd("rev-parse", "--show-toplevel")
	toplevelCmd.Stderr = nil
	output, err := toplevelCmd.Output()
	dir := firstLine(output)
	if dir == "" {
		return "", fmt.Errorf("unable to determine git working directory")
76
	}
77
	return dir, err
78 79
}

J
Jingwen Owen Ou 已提交
80
func HasFile(segments ...string) bool {
81
	// The blessed way to resolve paths within git dir since Git 2.5.0
82 83 84 85 86 87 88
	pathCmd := gitCmd("rev-parse", "-q", "--git-path", filepath.Join(segments...))
	pathCmd.Stderr = nil
	if output, err := pathCmd.Output(); err == nil {
		if lines := outputLines(output); len(lines) == 1 {
			if _, err := os.Stat(lines[0]); err == nil {
				return true
			}
89 90 91 92
		}
	}

	// Fallback for older git versions
J
Jingwen Owen Ou 已提交
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
	dir, err := Dir()
	if err != nil {
		return false
	}

	s := []string{dir}
	s = append(s, segments...)
	path := filepath.Join(s...)
	if _, err := os.Stat(path); err == nil {
		return true
	}

	return false
}

J
Jingwen Owen Ou 已提交
108
func BranchAtRef(paths ...string) (name string, err error) {
J
Jingwen Owen Ou 已提交
109 110 111 112 113 114
	dir, err := Dir()
	if err != nil {
		return
	}

	segments := []string{dir}
J
Jingwen Owen Ou 已提交
115
	segments = append(segments, paths...)
J
Jingwen Owen Ou 已提交
116 117 118 119 120 121 122
	path := filepath.Join(segments...)
	b, err := ioutil.ReadFile(path)
	if err != nil {
		return
	}

	n := string(b)
J
Jingwen Owen Ou 已提交
123 124 125
	refPrefix := "ref: "
	if strings.HasPrefix(n, refPrefix) {
		name = strings.TrimPrefix(n, refPrefix)
J
Jingwen Owen Ou 已提交
126 127 128 129 130 131 132 133
		name = strings.TrimSpace(name)
	} else {
		err = fmt.Errorf("No branch info in %s: %s", path, n)
	}

	return
}

134
func Editor() (string, error) {
135 136 137
	varCmd := gitCmd("var", "GIT_EDITOR")
	varCmd.Stderr = nil
	output, err := varCmd.Output()
138
	if err != nil {
J
Jingwen Owen Ou 已提交
139
		return "", fmt.Errorf("Can't load git var: GIT_EDITOR")
140 141
	}

142
	return os.ExpandEnv(firstLine(output)), nil
J
Jingwen Owen Ou 已提交
143 144
}

145
func Head() (string, error) {
J
Jingwen Owen Ou 已提交
146
	return BranchAtRef("HEAD")
147 148 149
}

func SymbolicFullName(name string) (string, error) {
150 151 152
	parseCmd := gitCmd("rev-parse", "--symbolic-full-name", name)
	parseCmd.Stderr = nil
	output, err := parseCmd.Output()
153
	if err != nil {
J
Jingwen Owen Ou 已提交
154
		return "", fmt.Errorf("Unknown revision or path not in the working tree: %s", name)
155 156
	}

157
	return firstLine(output), nil
J
Jingwen Owen Ou 已提交
158 159
}

160
func Ref(ref string) (string, error) {
161 162 163
	parseCmd := gitCmd("rev-parse", "-q", ref)
	parseCmd.Stderr = nil
	output, err := parseCmd.Output()
J
Jingwen Owen Ou 已提交
164
	if err != nil {
J
Jingwen Owen Ou 已提交
165
		return "", fmt.Errorf("Unknown revision or path not in the working tree: %s", ref)
J
Jingwen Owen Ou 已提交
166 167
	}

168
	return firstLine(output), nil
J
Jingwen Owen Ou 已提交
169 170
}

J
Jingwen Owen Ou 已提交
171 172
func RefList(a, b string) ([]string, error) {
	ref := fmt.Sprintf("%s...%s", a, b)
173 174 175
	listCmd := gitCmd("rev-list", "--cherry-pick", "--right-only", "--no-merges", ref)
	listCmd.Stderr = nil
	output, err := listCmd.Output()
J
Jingwen Owen Ou 已提交
176
	if err != nil {
177
		return nil, fmt.Errorf("Can't load rev-list for %s", ref)
J
Jingwen Owen Ou 已提交
178 179
	}

180
	return outputLines(output), nil
J
Jingwen Owen Ou 已提交
181 182
}

183
func NewRange(a, b string) (*Range, error) {
184 185 186
	parseCmd := gitCmd("rev-parse", "-q", a, b)
	parseCmd.Stderr = nil
	output, err := parseCmd.Output()
187 188 189 190
	if err != nil {
		return nil, err
	}

191 192 193 194 195
	lines := outputLines(output)
	if len(lines) != 2 {
		return nil, fmt.Errorf("Can't parse range %s..%s", a, b)
	}
	return &Range{lines[0], lines[1]}, nil
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
}

type Range struct {
	A string
	B string
}

func (r *Range) IsIdentical() bool {
	return strings.EqualFold(r.A, r.B)
}

func (r *Range) IsAncestor() bool {
	cmd := gitCmd("merge-base", "--is-ancestor", r.A, r.B)
	return cmd.Success()
}

212
func CommentChar(text string) (string, error) {
J
Jingwen Owen Ou 已提交
213 214
	char, err := Config("core.commentchar")
	if err != nil {
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
		return "#", nil
	} else if char == "auto" {
		lines := strings.Split(text, "\n")
		commentCharCandidates := strings.Split("#;@!$%^&|:", "")
	candidateLoop:
		for _, candidate := range commentCharCandidates {
			for _, line := range lines {
				if strings.HasPrefix(line, candidate) {
					continue candidateLoop
				}
			}
			return candidate, nil
		}
		return "", fmt.Errorf("unable to select a comment character that is not used in the current message")
	} else {
		return char, nil
J
Jingwen Owen Ou 已提交
231 232 233
	}
}

234
func Show(sha string) (string, error) {
J
Jingwen Owen Ou 已提交
235
	cmd := cmd.New("git")
236
	cmd.Stderr = nil
237
	cmd.WithArg("-c").WithArg("log.showSignature=false")
J
Jingwen Owen Ou 已提交
238
	cmd.WithArg("show").WithArg("-s").WithArg("--format=%s%n%+b").WithArg(sha)
239

240 241
	output, err := cmd.Output()
	return strings.TrimSpace(output), err
242 243
}

244 245
func Log(sha1, sha2 string) (string, error) {
	execCmd := cmd.New("git")
246
	execCmd.WithArg("-c").WithArg("log.showSignature=false").WithArg("log").WithArg("--no-color")
J
Jingwen Owen Ou 已提交
247 248 249 250 251
	execCmd.WithArg("--format=%h (%aN, %ar)%n%w(78,3,3)%s%n%+b")
	execCmd.WithArg("--cherry")
	shaRange := fmt.Sprintf("%s...%s", sha1, sha2)
	execCmd.WithArg(shaRange)

252
	outputs, err := execCmd.Output()
J
Jingwen Owen Ou 已提交
253
	if err != nil {
J
Jingwen Owen Ou 已提交
254
		return "", fmt.Errorf("Can't load git log %s..%s", sha1, sha2)
J
Jingwen Owen Ou 已提交
255 256
	}

257
	return outputs, nil
J
Jingwen Owen Ou 已提交
258 259
}

J
Jingwen Owen Ou 已提交
260
func Remotes() ([]string, error) {
261 262 263 264
	remoteCmd := gitCmd("remote", "-v")
	remoteCmd.Stderr = nil
	output, err := remoteCmd.Output()
	return outputLines(output), err
J
Jingwen Owen Ou 已提交
265 266
}

J
Jingwen Owen Ou 已提交
267
func Config(name string) (string, error) {
268 269 270
	return gitGetConfig(name)
}

271
func ConfigAll(name string) ([]string, error) {
272 273 274 275 276
	mode := "--get-all"
	if strings.Contains(name, "*") {
		mode = "--get-regexp"
	}

277 278
	configCmd := gitCmd(gitConfigCommand([]string{mode, name})...)
	output, err := configCmd.Output()
279
	if err != nil {
280
		return nil, fmt.Errorf("Unknown config %s", name)
281
	}
282
	return outputLines(output), nil
283 284
}

285 286 287 288 289 290 291 292 293 294
func GlobalConfig(name string) (string, error) {
	return gitGetConfig("--global", name)
}

func SetGlobalConfig(name, value string) error {
	_, err := gitConfig("--global", name, value)
	return err
}

func gitGetConfig(args ...string) (string, error) {
295 296
	configCmd := gitCmd(gitConfigCommand(args)...)
	output, err := configCmd.Output()
J
Jingwen Owen Ou 已提交
297
	if err != nil {
298
		return "", fmt.Errorf("Unknown config %s", args[len(args)-1])
J
Jingwen Owen Ou 已提交
299 300
	}

301
	return firstLine(output), nil
J
Jingwen Owen Ou 已提交
302 303
}

304
func gitConfig(args ...string) ([]string, error) {
305 306 307
	configCmd := gitCmd(gitConfigCommand(args)...)
	output, err := configCmd.Output()
	return outputLines(output), err
308
}
309

310 311 312
func gitConfigCommand(args []string) []string {
	cmd := []string{"config"}
	return append(cmd, args...)
313 314
}

315 316 317 318
func Alias(name string) (string, error) {
	return Config(fmt.Sprintf("alias.%s", name))
}

319 320 321 322
func Run(args ...string) error {
	cmd := gitCmd(args...)
	return cmd.Run()
}
323

324 325 326 327
func Spawn(args ...string) error {
	cmd := gitCmd(args...)
	return cmd.Spawn()
}
328

329 330 331
func Quiet(args ...string) bool {
	cmd := gitCmd(args...)
	return cmd.Success()
332 333
}

334 335
func IsGitDir(dir string) bool {
	cmd := cmd.New("git")
M
go fmt  
Mislav Marohnić 已提交
336
	cmd.WithArgs("--git-dir="+dir, "rev-parse", "--git-dir")
337 338 339
	return cmd.Success()
}

340
func LocalBranches() ([]string, error) {
341 342 343 344
	branchesCmd := gitCmd("branch", "--list")
	output, err := branchesCmd.Output()
	if err != nil {
		return nil, err
345 346
	}

347 348 349 350 351 352
	branches := []string{}
	for _, branch := range outputLines(output) {
		branches = append(branches, branch[2:])
	}
	return branches, nil
}
J
Jingwen Owen Ou 已提交
353

354 355 356 357 358 359
func outputLines(output string) []string {
	output = strings.TrimSuffix(output, "\n")
	if output == "" {
		return []string{}
	} else {
		return strings.Split(output, "\n")
J
Jingwen Owen Ou 已提交
360
	}
361
}
J
Jingwen Owen Ou 已提交
362

363 364 365 366 367 368
func firstLine(output string) string {
	if i := strings.Index(output, "\n"); i >= 0 {
		return output[0:i]
	} else {
		return output
	}
J
Jingwen Owen Ou 已提交
369
}
370 371 372 373 374 375 376 377 378 379 380 381 382 383

func gitCmd(args ...string) *cmd.Cmd {
	cmd := cmd.New("git")

	for _, v := range GlobalFlags {
		cmd.WithArg(v)
	}

	for _, a := range args {
		cmd.WithArg(a)
	}

	return cmd
}
384 385

func IsBuiltInGitCommand(command string) bool {
386 387 388
	helpCommand := gitCmd("help", "--no-verbose", "-a")
	helpCommand.Stderr = nil
	helpCommandOutput, err := helpCommand.Output()
389 390
	if err != nil {
		// support git versions that don't recognize --no-verbose
391 392
		helpCommand := gitCmd("help", "-a")
		helpCommandOutput, err = helpCommand.Output()
393
	}
394 395 396
	if err != nil {
		return false
	}
397
	for _, helpCommandOutputLine := range outputLines(helpCommandOutput) {
398 399 400 401 402 403 404 405 406 407
		if strings.HasPrefix(helpCommandOutputLine, "  ") {
			for _, gitCommand := range strings.Split(helpCommandOutputLine, " ") {
				if gitCommand == command {
					return true
				}
			}
		}
	}
	return false
}