blob: 640bd3e33bca12fc55c12e3a160576c2d9f28c39 [file] [log] [blame]
Jack Neusfc3b5772019-07-03 11:18:42 -06001// Copyright 2019 The Chromium OS Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4package git
5
6import (
7 "bytes"
8 "context"
9 "fmt"
10 "os/exec"
11 "regexp"
12 "strings"
13)
14
15var (
16 commandRunnerImpl commandRunner = realCommandRunner{}
17)
18
19type commandRunner interface {
20 runCommand(ctx context.Context, stdoutBuf, stderrBuf *bytes.Buffer, dir, name string, args ...string) error
21}
22
23type realCommandRunner struct{}
24
25func (c realCommandRunner) runCommand(ctx context.Context, stdoutBuf, stderrBuf *bytes.Buffer, dir, name string, args ...string) error {
26 cmd := exec.CommandContext(ctx, name, args...)
27 cmd.Stdout = stdoutBuf
28 cmd.Stderr = stderrBuf
29 cmd.Dir = dir
30 return cmd.Run()
31}
32
33type CommandOutput struct {
34 Stdout string
35 Stderr string
36}
37
38// Struct representing a remote ref.
39type RemoteRef struct {
40 Remote string
41 Ref string
42}
43
44// RunGit the specified git command in the specified repo. It returns
45// stdout and stderr.
46func RunGit(gitRepo string, cmd []string) (CommandOutput, error) {
47 ctx := context.Background()
48 var stdoutBuf, stderrBuf bytes.Buffer
49 err := commandRunnerImpl.runCommand(ctx, &stdoutBuf, &stderrBuf, gitRepo, "git", cmd...)
50 cmdOutput := CommandOutput{stdoutBuf.String(), stderrBuf.String()}
51 return cmdOutput, err
52}
53
54// GetCurrentBranch returns current branch of a repo, and an empty string
55// if repo is on detached HEAD.
56func GetCurrentBranch(cwd string) string {
57 output, err := RunGit(cwd, []string{"symbolic-ref", "-q", "HEAD"})
58 if err != nil {
59 return ""
60 }
61 return StripRefsHead(strings.TrimSpace(output.Stdout))
62}
63
64// MatchBranchName returns the names of branches who match the specified
65// regular expression.
66func MatchBranchName(gitRepo string, pattern *regexp.Regexp) ([]string, error) {
67 // Regex should be case insensitive.
68 if !strings.HasPrefix(pattern.String(), "(?i)") {
69 pattern = regexp.MustCompile("(?i)" + pattern.String())
70 }
71
72 output, err := RunGit(gitRepo, []string{"ls-remote", gitRepo})
73 if err != nil {
74 // Could not read branches.
75 return []string{}, fmt.Errorf("git error: %s\nstderr: %s", err.Error(), output.Stderr)
76 }
77 // Find all branches that match the pattern.
78 branches := strings.Split(output.Stdout, "\n")
79 matchedBranches := []string{}
80 for _, branch := range branches {
81 branch = strings.TrimSpace(branch)
82 if branch == "" {
83 continue
84 }
85 branch = strings.Fields(branch)[1]
86 if pattern.Match([]byte(branch)) {
87 matchedBranches = append(matchedBranches, branch)
88 }
89 }
90 return matchedBranches, nil
91}
92
93// GetGitRepoRevision finds and returns the revision of a branch.
94func GetGitRepoRevision(cwd string) (string, error) {
95 output, err := RunGit(cwd, []string{"rev-parse", "HEAD"})
96 return strings.TrimSpace(output.Stdout), err
97}
98
99// StipRefsHead removes leading 'refs/heads/' from a ref name.
100func StripRefsHead(ref string) string {
101 return strings.TrimPrefix(ref, "refs/heads/")
102}
103
104// NormalizeRef converts git branch refs into fully qualified form.
105func NormalizeRef(ref string) string {
106 if ref == "" || strings.HasPrefix(ref, "refs/") {
107 return ref
108 }
109 return fmt.Sprintf("refs/heads/%s", ref)
110}
111
112// StripRefs removes leading 'refs/heads/', 'refs/remotes/[^/]+/' from a ref name.
113func StripRefs(ref string) string {
114 ref = StripRefsHead(ref)
115 // If the ref starts with ref/remotes/, then we want the part of the string
116 // that comes after the third "/".
117 // Example: refs/remotes/origin/master --> master
118 // Example: refs/remotse/origin/foo/bar --> foo/bar
119 if strings.HasPrefix(ref, "refs/remotes/") {
120 refParts := strings.SplitN(ref, "/", 4)
121 return refParts[len(refParts)-1]
122 }
123 return ref
124}