blob: 9793f003001873668df7c00eb33f9d59dd090513 [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"
Jack Neus0296c732019-07-17 09:35:01 -060010 "path/filepath"
Jack Neusfc3b5772019-07-03 11:18:42 -060011 "regexp"
12 "strings"
Jack Neus07511722019-07-12 15:41:10 -060013
14 "go.chromium.org/chromiumos/infra/go/internal/cmd"
Jack Neus1f489f62019-08-06 12:01:54 -060015 "go.chromium.org/luci/common/errors"
Jack Neusfc3b5772019-07-03 11:18:42 -060016)
17
18var (
Jack Neus07511722019-07-12 15:41:10 -060019 CommandRunnerImpl cmd.CommandRunner = cmd.RealCommandRunner{}
Jack Neusfc3b5772019-07-03 11:18:42 -060020)
21
Jack Neusfc3b5772019-07-03 11:18:42 -060022type CommandOutput struct {
23 Stdout string
24 Stderr string
25}
26
27// Struct representing a remote ref.
28type RemoteRef struct {
29 Remote string
30 Ref string
31}
32
33// RunGit the specified git command in the specified repo. It returns
34// stdout and stderr.
35func RunGit(gitRepo string, cmd []string) (CommandOutput, error) {
36 ctx := context.Background()
37 var stdoutBuf, stderrBuf bytes.Buffer
Jack Neus07511722019-07-12 15:41:10 -060038 err := CommandRunnerImpl.RunCommand(ctx, &stdoutBuf, &stderrBuf, gitRepo, "git", cmd...)
Jack Neusfc3b5772019-07-03 11:18:42 -060039 cmdOutput := CommandOutput{stdoutBuf.String(), stderrBuf.String()}
Jack Neus758a70f2019-08-20 10:32:12 -060040 return cmdOutput, errors.Annotate(err, cmdOutput.Stderr).Err()
Jack Neusfc3b5772019-07-03 11:18:42 -060041}
42
Jack Neus2bcf4a62019-07-25 10:35:05 -060043// RunGitIgnore the specified git command in the specified repo and returns
44// only an error, not the command output.
45func RunGitIgnoreOutput(gitRepo string, cmd []string) error {
46 _, err := RunGit(gitRepo, cmd)
47 return err
48}
49
Jack Neusfc3b5772019-07-03 11:18:42 -060050// GetCurrentBranch returns current branch of a repo, and an empty string
51// if repo is on detached HEAD.
52func GetCurrentBranch(cwd string) string {
53 output, err := RunGit(cwd, []string{"symbolic-ref", "-q", "HEAD"})
54 if err != nil {
55 return ""
56 }
57 return StripRefsHead(strings.TrimSpace(output.Stdout))
58}
59
60// MatchBranchName returns the names of branches who match the specified
61// regular expression.
62func MatchBranchName(gitRepo string, pattern *regexp.Regexp) ([]string, error) {
Jack Neus10e6a122019-07-18 10:17:44 -060063 // MatchBranchWithNamespace trims the namespace off the branches it returns.
64 // Here, we need a namespace that matches every string but doesn't match any character
65 // (so that nothing is trimmed).
66 nullNamespace := regexp.MustCompile("")
67 return MatchBranchNameWithNamespace(gitRepo, pattern, nullNamespace)
68}
69
70// MatchBranchNameWithNamespace returns the names of branches who match the specified
71// pattern and start with the specified namespace.
72func MatchBranchNameWithNamespace(gitRepo string, pattern, namespace *regexp.Regexp) ([]string, error) {
Jack Neusfc3b5772019-07-03 11:18:42 -060073 // Regex should be case insensitive.
Jack Neus10e6a122019-07-18 10:17:44 -060074 namespace = regexp.MustCompile("(?i)^" + namespace.String())
75 pattern = regexp.MustCompile("(?i)" + pattern.String())
Jack Neusfc3b5772019-07-03 11:18:42 -060076
Jack Neus901a6bf2019-07-22 08:30:07 -060077 output, err := RunGit(gitRepo, []string{"show-ref"})
Jack Neusfc3b5772019-07-03 11:18:42 -060078 if err != nil {
Jack Neus901a6bf2019-07-22 08:30:07 -060079 if strings.Contains(err.Error(), "exit status 1") {
80 // Not a fatal error, just no branches.
81 return []string{}, nil
82 }
Jack Neusfc3b5772019-07-03 11:18:42 -060083 // Could not read branches.
Jack Neus901a6bf2019-07-22 08:30:07 -060084 return []string{}, fmt.Errorf("git error: %s\nstdout: %s stderr: %s", err.Error(), output.Stdout, output.Stderr)
Jack Neusfc3b5772019-07-03 11:18:42 -060085 }
86 // Find all branches that match the pattern.
87 branches := strings.Split(output.Stdout, "\n")
88 matchedBranches := []string{}
89 for _, branch := range branches {
90 branch = strings.TrimSpace(branch)
91 if branch == "" {
92 continue
93 }
94 branch = strings.Fields(branch)[1]
Jack Neus10e6a122019-07-18 10:17:44 -060095
96 // Only look at branches which match the namespace.
97 if !namespace.Match([]byte(branch)) {
98 continue
99 }
100 branch = namespace.ReplaceAllString(branch, "")
101
Jack Neusfc3b5772019-07-03 11:18:42 -0600102 if pattern.Match([]byte(branch)) {
103 matchedBranches = append(matchedBranches, branch)
104 }
105 }
106 return matchedBranches, nil
107}
108
Jack Neuse2370372019-08-14 17:25:16 -0600109// IsSHA checks whether or not the given ref is a SHA.
110func IsSHA(ref string) bool {
111 shaRegexp := regexp.MustCompile("^[0-9a-f]{40}$")
112 return shaRegexp.MatchString(ref)
113}
114
Jack Neusfc3b5772019-07-03 11:18:42 -0600115// GetGitRepoRevision finds and returns the revision of a branch.
Jack Neusa7287522019-07-23 16:36:18 -0600116func GetGitRepoRevision(cwd, branch string) (string, error) {
117 if branch == "" {
118 branch = "HEAD"
Jack Neus1f489f62019-08-06 12:01:54 -0600119 } else if branch != "HEAD" {
Jack Neus8b770832019-08-01 15:33:04 -0600120 branch = NormalizeRef(branch)
Jack Neusa7287522019-07-23 16:36:18 -0600121 }
122 output, err := RunGit(cwd, []string{"rev-parse", branch})
Jack Neus1f489f62019-08-06 12:01:54 -0600123 return strings.TrimSpace(output.Stdout), errors.Annotate(err, output.Stderr).Err()
Jack Neusfc3b5772019-07-03 11:18:42 -0600124}
125
Jack Neusa7287522019-07-23 16:36:18 -0600126// IsReachable determines whether one commit ref is reachable from another.
127func IsReachable(cwd, to_ref, from_ref string) (bool, error) {
128 _, err := RunGit(cwd, []string{"merge-base", "--is-ancestor", to_ref, from_ref})
129 if err != nil {
130 if strings.Contains(err.Error(), "exit status 1") {
131 return false, nil
132 }
133 return false, err
134 }
135 return true, nil
136}
137
Jack Neus7440c762019-07-22 10:45:18 -0600138// StripRefsHead removes leading 'refs/heads/' from a ref name.
Jack Neusfc3b5772019-07-03 11:18:42 -0600139func StripRefsHead(ref string) string {
140 return strings.TrimPrefix(ref, "refs/heads/")
141}
142
143// NormalizeRef converts git branch refs into fully qualified form.
144func NormalizeRef(ref string) string {
145 if ref == "" || strings.HasPrefix(ref, "refs/") {
146 return ref
147 }
148 return fmt.Sprintf("refs/heads/%s", ref)
149}
150
151// StripRefs removes leading 'refs/heads/', 'refs/remotes/[^/]+/' from a ref name.
152func StripRefs(ref string) string {
153 ref = StripRefsHead(ref)
154 // If the ref starts with ref/remotes/, then we want the part of the string
155 // that comes after the third "/".
156 // Example: refs/remotes/origin/master --> master
157 // Example: refs/remotse/origin/foo/bar --> foo/bar
158 if strings.HasPrefix(ref, "refs/remotes/") {
159 refParts := strings.SplitN(ref, "/", 4)
160 return refParts[len(refParts)-1]
161 }
162 return ref
163}
Jack Neusabdbe192019-07-15 12:23:22 -0600164
165// CreateBranch creates a branch.
166func CreateBranch(gitRepo, branch string) error {
Jack Neus0296c732019-07-17 09:35:01 -0600167 output, err := RunGit(gitRepo, []string{"checkout", "-B", branch})
Jack Neus10e6a122019-07-18 10:17:44 -0600168 if err != nil {
169 if strings.Contains(output.Stderr, "not a valid branch name") {
170 return fmt.Errorf("%s is not a valid branch name", branch)
171 } else {
172 return fmt.Errorf(output.Stderr)
173 }
Jack Neus0296c732019-07-17 09:35:01 -0600174 }
175 return err
176}
177
178// CreateTrackingBranch creates a tracking branch.
179func CreateTrackingBranch(gitRepo, branch string, remoteRef RemoteRef) error {
180 refspec := fmt.Sprintf("%s/%s", remoteRef.Remote, remoteRef.Ref)
181 output, err := RunGit(gitRepo, []string{"fetch", remoteRef.Remote, remoteRef.Ref})
182 if err != nil {
183 return fmt.Errorf("could not fetch %s: %s", refspec, output.Stderr)
184 }
185 output, err = RunGit(gitRepo, []string{"checkout", "-b", branch, "-t", refspec})
186 if err != nil {
187 if strings.Contains(output.Stderr, "not a valid branch name") {
188 return fmt.Errorf("%s is not a valid branch name", branch)
189 } else {
190 return fmt.Errorf(output.Stderr)
191 }
192 }
Jack Neusabdbe192019-07-15 12:23:22 -0600193 return err
194}
195
196// CommitAll adds all local changes and commits them.
Jack Neusf116fae2019-07-24 15:05:03 -0600197// Returns the sha1 of the commit.
198func CommitAll(gitRepo, commitMsg string) (string, error) {
Jack Neus0296c732019-07-17 09:35:01 -0600199 if output, err := RunGit(gitRepo, []string{"add", "-A"}); err != nil {
Jack Neusf116fae2019-07-24 15:05:03 -0600200 return "", fmt.Errorf(output.Stderr)
Jack Neusabdbe192019-07-15 12:23:22 -0600201 }
Jack Neus0296c732019-07-17 09:35:01 -0600202 if output, err := RunGit(gitRepo, []string{"commit", "-m", commitMsg}); err != nil {
203 if strings.Contains(output.Stdout, "nothing to commit") {
Jack Neusf116fae2019-07-24 15:05:03 -0600204 return "", fmt.Errorf(output.Stdout)
Jack Neus0296c732019-07-17 09:35:01 -0600205 } else {
Jack Neusf116fae2019-07-24 15:05:03 -0600206 return "", fmt.Errorf(output.Stderr)
Jack Neus0296c732019-07-17 09:35:01 -0600207 }
Jack Neusabdbe192019-07-15 12:23:22 -0600208 }
Jack Neusf116fae2019-07-24 15:05:03 -0600209 output, err := RunGit(gitRepo, []string{"rev-parse", "HEAD"})
210 if err != nil {
211 return "", err
212 }
213 return strings.TrimSpace(output.Stdout), nil
Jack Neusabdbe192019-07-15 12:23:22 -0600214}
215
Jack Neus7440c762019-07-22 10:45:18 -0600216// CommitEmpty makes an empty commit (assuming nothing is staged).
Jack Neusf116fae2019-07-24 15:05:03 -0600217// Returns the sha1 of the commit.
218func CommitEmpty(gitRepo, commitMsg string) (string, error) {
Jack Neus7440c762019-07-22 10:45:18 -0600219 if output, err := RunGit(gitRepo, []string{"commit", "-m", commitMsg, "--allow-empty"}); err != nil {
Jack Neusf116fae2019-07-24 15:05:03 -0600220 return "", fmt.Errorf(output.Stderr)
Jack Neus7440c762019-07-22 10:45:18 -0600221 }
Jack Neusf116fae2019-07-24 15:05:03 -0600222 output, err := RunGit(gitRepo, []string{"rev-parse", "HEAD"})
223 if err != nil {
224 return "", nil
225 }
226 return strings.TrimSpace(output.Stdout), nil
Jack Neus7440c762019-07-22 10:45:18 -0600227}
228
Jack Neuseb25f722019-07-19 16:33:20 -0600229// PushRef pushes the specified local ref to the specified remote ref.
230func PushRef(gitRepo, localRef string, dryRun bool, pushTo RemoteRef) error {
Jack Neusabdbe192019-07-15 12:23:22 -0600231 ref := fmt.Sprintf("%s:%s", localRef, pushTo.Ref)
232 cmd := []string{"push", pushTo.Remote, ref}
233 if dryRun {
234 cmd = append(cmd, "--dry-run")
235 }
Jack Neus7440c762019-07-22 10:45:18 -0600236 _, err := RunGit(gitRepo, cmd)
Jack Neus0296c732019-07-17 09:35:01 -0600237 return err
238}
239
240// Init initializes a repo.
241func Init(gitRepo string, bare bool) error {
242 cmd := []string{"init"}
243 if bare {
244 cmd = append(cmd, "--bare")
245 }
Jack Neusabdbe192019-07-15 12:23:22 -0600246 _, err := RunGit(gitRepo, cmd)
247 return err
248}
Jack Neus0296c732019-07-17 09:35:01 -0600249
250// AddRemote adds a remote.
251func AddRemote(gitRepo, remote, remoteLocation string) error {
252 output, err := RunGit(gitRepo, []string{"remote", "add", remote, remoteLocation})
253 if err != nil {
Jack Neuseb25f722019-07-19 16:33:20 -0600254 if strings.Contains(output.Stderr, "already exists") {
255 return fmt.Errorf("remote already exists")
256 }
Jack Neus0296c732019-07-17 09:35:01 -0600257 }
258 return err
259}
260
261// Checkout checkouts a branch.
262func Checkout(gitRepo, branch string) error {
263 output, err := RunGit(gitRepo, []string{"checkout", branch})
Jack Neus91de2402019-08-14 08:26:03 -0600264 if err != nil {
Jack Neus0296c732019-07-17 09:35:01 -0600265 return fmt.Errorf(output.Stderr)
266 }
267 return err
268}
269
270// DeleteBranch checks out to master and then deletes the current branch.
271func DeleteBranch(gitRepo, branch string, force bool) error {
272 cmd := []string{"branch"}
273 if force {
274 cmd = append(cmd, "-D")
275 } else {
276 cmd = append(cmd, "-d")
277 }
278 cmd = append(cmd, branch)
279 output, err := RunGit(gitRepo, cmd)
280
281 if err != nil {
282 if strings.Contains(output.Stderr, "checked out at") {
283 return fmt.Errorf(output.Stderr)
284 }
285 if strings.Contains(output.Stderr, "not fully merged") {
286 return fmt.Errorf("branch %s is not fully merged. use the force parameter if you wish to proceed", branch)
287 }
288 }
289 return err
290}
291
292// Clone clones the remote into the specified dir.
293func Clone(remote, dir string) error {
294 output, err := RunGit(filepath.Dir(dir), []string{"clone", remote, filepath.Base(dir)})
295 if err != nil {
296 return fmt.Errorf(output.Stderr)
297 }
298 return nil
299}
Jack Neus1f489f62019-08-06 12:01:54 -0600300
301// RemoteBranches returns a list of branches on the specified remote.
302func RemoteBranches(gitRepo, remote string) ([]string, error) {
303 output, err := RunGit(gitRepo, []string{"ls-remote", remote})
304 if err != nil {
305 if strings.Contains(output.Stderr, "not appear to be a git repository") {
306 return []string{}, fmt.Errorf("%s is not a valid remote", remote)
307 }
Jack Neuse2370372019-08-14 17:25:16 -0600308 return []string{}, fmt.Errorf(output.Stderr)
Jack Neus1f489f62019-08-06 12:01:54 -0600309 }
310 remotes := []string{}
311 for _, line := range strings.Split(strings.TrimSpace(output.Stdout), "\n") {
312 if line == "" {
313 continue
314 }
315 remotes = append(remotes, StripRefs(strings.Fields(line)[1]))
316 }
317 return remotes, nil
318}
319
320// RemoteHasBranch checks whether or not a branch exists on a remote.
321func RemoteHasBranch(gitRepo, remote, branch string) (bool, error) {
Jack Neuse2370372019-08-14 17:25:16 -0600322 output, err := RunGit(gitRepo, []string{"ls-remote", remote, branch})
Jack Neus1f489f62019-08-06 12:01:54 -0600323 if err != nil {
Jack Neuse2370372019-08-14 17:25:16 -0600324 if strings.Contains(output.Stderr, "not appear to be a git repository") {
325 return false, fmt.Errorf("%s is not a valid remote", remote)
Jack Neus1f489f62019-08-06 12:01:54 -0600326 }
Jack Neuse2370372019-08-14 17:25:16 -0600327 return false, fmt.Errorf(output.Stderr)
Jack Neus1f489f62019-08-06 12:01:54 -0600328 }
Jack Neuse2370372019-08-14 17:25:16 -0600329 return output.Stdout != "", nil
Jack Neus1f489f62019-08-06 12:01:54 -0600330}