blob: 044fab164b01603424f9154de4d150508066c202 [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/chromiumos/infra/go/internal/test_util"
16 "go.chromium.org/luci/common/errors"
Jack Neusfc3b5772019-07-03 11:18:42 -060017)
18
19var (
Jack Neus07511722019-07-12 15:41:10 -060020 CommandRunnerImpl cmd.CommandRunner = cmd.RealCommandRunner{}
Jack Neusfc3b5772019-07-03 11:18:42 -060021)
22
Jack Neusfc3b5772019-07-03 11:18:42 -060023type CommandOutput struct {
24 Stdout string
25 Stderr string
26}
27
28// Struct representing a remote ref.
29type RemoteRef struct {
30 Remote string
31 Ref string
32}
33
34// RunGit the specified git command in the specified repo. It returns
35// stdout and stderr.
36func RunGit(gitRepo string, cmd []string) (CommandOutput, error) {
37 ctx := context.Background()
38 var stdoutBuf, stderrBuf bytes.Buffer
Jack Neus07511722019-07-12 15:41:10 -060039 err := CommandRunnerImpl.RunCommand(ctx, &stdoutBuf, &stderrBuf, gitRepo, "git", cmd...)
Jack Neusfc3b5772019-07-03 11:18:42 -060040 cmdOutput := CommandOutput{stdoutBuf.String(), stderrBuf.String()}
41 return cmdOutput, err
42}
43
Jack Neus2bcf4a62019-07-25 10:35:05 -060044// RunGitIgnore the specified git command in the specified repo and returns
45// only an error, not the command output.
46func RunGitIgnoreOutput(gitRepo string, cmd []string) error {
47 _, err := RunGit(gitRepo, cmd)
48 return err
49}
50
Jack Neusfc3b5772019-07-03 11:18:42 -060051// GetCurrentBranch returns current branch of a repo, and an empty string
52// if repo is on detached HEAD.
53func GetCurrentBranch(cwd string) string {
54 output, err := RunGit(cwd, []string{"symbolic-ref", "-q", "HEAD"})
55 if err != nil {
56 return ""
57 }
58 return StripRefsHead(strings.TrimSpace(output.Stdout))
59}
60
61// MatchBranchName returns the names of branches who match the specified
62// regular expression.
63func MatchBranchName(gitRepo string, pattern *regexp.Regexp) ([]string, error) {
Jack Neus10e6a122019-07-18 10:17:44 -060064 // MatchBranchWithNamespace trims the namespace off the branches it returns.
65 // Here, we need a namespace that matches every string but doesn't match any character
66 // (so that nothing is trimmed).
67 nullNamespace := regexp.MustCompile("")
68 return MatchBranchNameWithNamespace(gitRepo, pattern, nullNamespace)
69}
70
71// MatchBranchNameWithNamespace returns the names of branches who match the specified
72// pattern and start with the specified namespace.
73func MatchBranchNameWithNamespace(gitRepo string, pattern, namespace *regexp.Regexp) ([]string, error) {
Jack Neusfc3b5772019-07-03 11:18:42 -060074 // Regex should be case insensitive.
Jack Neus10e6a122019-07-18 10:17:44 -060075 namespace = regexp.MustCompile("(?i)^" + namespace.String())
76 pattern = regexp.MustCompile("(?i)" + pattern.String())
Jack Neusfc3b5772019-07-03 11:18:42 -060077
Jack Neus901a6bf2019-07-22 08:30:07 -060078 output, err := RunGit(gitRepo, []string{"show-ref"})
Jack Neusfc3b5772019-07-03 11:18:42 -060079 if err != nil {
Jack Neus901a6bf2019-07-22 08:30:07 -060080 if strings.Contains(err.Error(), "exit status 1") {
81 // Not a fatal error, just no branches.
82 return []string{}, nil
83 }
Jack Neusfc3b5772019-07-03 11:18:42 -060084 // Could not read branches.
Jack Neus901a6bf2019-07-22 08:30:07 -060085 return []string{}, fmt.Errorf("git error: %s\nstdout: %s stderr: %s", err.Error(), output.Stdout, output.Stderr)
Jack Neusfc3b5772019-07-03 11:18:42 -060086 }
87 // Find all branches that match the pattern.
88 branches := strings.Split(output.Stdout, "\n")
89 matchedBranches := []string{}
90 for _, branch := range branches {
91 branch = strings.TrimSpace(branch)
92 if branch == "" {
93 continue
94 }
95 branch = strings.Fields(branch)[1]
Jack Neus10e6a122019-07-18 10:17:44 -060096
97 // Only look at branches which match the namespace.
98 if !namespace.Match([]byte(branch)) {
99 continue
100 }
101 branch = namespace.ReplaceAllString(branch, "")
102
Jack Neusfc3b5772019-07-03 11:18:42 -0600103 if pattern.Match([]byte(branch)) {
104 matchedBranches = append(matchedBranches, branch)
105 }
106 }
107 return matchedBranches, nil
108}
109
Jack Neuse2370372019-08-14 17:25:16 -0600110// IsSHA checks whether or not the given ref is a SHA.
111func IsSHA(ref string) bool {
112 shaRegexp := regexp.MustCompile("^[0-9a-f]{40}$")
113 return shaRegexp.MatchString(ref)
114}
115
Jack Neusfc3b5772019-07-03 11:18:42 -0600116// GetGitRepoRevision finds and returns the revision of a branch.
Jack Neusa7287522019-07-23 16:36:18 -0600117func GetGitRepoRevision(cwd, branch string) (string, error) {
118 if branch == "" {
119 branch = "HEAD"
Jack Neus1f489f62019-08-06 12:01:54 -0600120 } else if branch != "HEAD" {
Jack Neus8b770832019-08-01 15:33:04 -0600121 branch = NormalizeRef(branch)
Jack Neusa7287522019-07-23 16:36:18 -0600122 }
123 output, err := RunGit(cwd, []string{"rev-parse", branch})
Jack Neus1f489f62019-08-06 12:01:54 -0600124 return strings.TrimSpace(output.Stdout), errors.Annotate(err, output.Stderr).Err()
Jack Neusfc3b5772019-07-03 11:18:42 -0600125}
126
Jack Neusa7287522019-07-23 16:36:18 -0600127// IsReachable determines whether one commit ref is reachable from another.
128func IsReachable(cwd, to_ref, from_ref string) (bool, error) {
129 _, err := RunGit(cwd, []string{"merge-base", "--is-ancestor", to_ref, from_ref})
130 if err != nil {
131 if strings.Contains(err.Error(), "exit status 1") {
132 return false, nil
133 }
134 return false, err
135 }
136 return true, nil
137}
138
Jack Neus7440c762019-07-22 10:45:18 -0600139// StripRefsHead removes leading 'refs/heads/' from a ref name.
Jack Neusfc3b5772019-07-03 11:18:42 -0600140func StripRefsHead(ref string) string {
141 return strings.TrimPrefix(ref, "refs/heads/")
142}
143
144// NormalizeRef converts git branch refs into fully qualified form.
145func NormalizeRef(ref string) string {
146 if ref == "" || strings.HasPrefix(ref, "refs/") {
147 return ref
148 }
149 return fmt.Sprintf("refs/heads/%s", ref)
150}
151
152// StripRefs removes leading 'refs/heads/', 'refs/remotes/[^/]+/' from a ref name.
153func StripRefs(ref string) string {
154 ref = StripRefsHead(ref)
155 // If the ref starts with ref/remotes/, then we want the part of the string
156 // that comes after the third "/".
157 // Example: refs/remotes/origin/master --> master
158 // Example: refs/remotse/origin/foo/bar --> foo/bar
159 if strings.HasPrefix(ref, "refs/remotes/") {
160 refParts := strings.SplitN(ref, "/", 4)
161 return refParts[len(refParts)-1]
162 }
163 return ref
164}
Jack Neusabdbe192019-07-15 12:23:22 -0600165
166// CreateBranch creates a branch.
167func CreateBranch(gitRepo, branch string) error {
Jack Neus0296c732019-07-17 09:35:01 -0600168 output, err := RunGit(gitRepo, []string{"checkout", "-B", branch})
Jack Neus10e6a122019-07-18 10:17:44 -0600169 if err != nil {
170 if strings.Contains(output.Stderr, "not a valid branch name") {
171 return fmt.Errorf("%s is not a valid branch name", branch)
172 } else {
173 return fmt.Errorf(output.Stderr)
174 }
Jack Neus0296c732019-07-17 09:35:01 -0600175 }
176 return err
177}
178
179// CreateTrackingBranch creates a tracking branch.
180func CreateTrackingBranch(gitRepo, branch string, remoteRef RemoteRef) error {
181 refspec := fmt.Sprintf("%s/%s", remoteRef.Remote, remoteRef.Ref)
182 output, err := RunGit(gitRepo, []string{"fetch", remoteRef.Remote, remoteRef.Ref})
183 if err != nil {
184 return fmt.Errorf("could not fetch %s: %s", refspec, output.Stderr)
185 }
186 output, err = RunGit(gitRepo, []string{"checkout", "-b", branch, "-t", refspec})
187 if err != nil {
188 if strings.Contains(output.Stderr, "not a valid branch name") {
189 return fmt.Errorf("%s is not a valid branch name", branch)
190 } else {
191 return fmt.Errorf(output.Stderr)
192 }
193 }
Jack Neusabdbe192019-07-15 12:23:22 -0600194 return err
195}
196
197// CommitAll adds all local changes and commits them.
Jack Neusf116fae2019-07-24 15:05:03 -0600198// Returns the sha1 of the commit.
199func CommitAll(gitRepo, commitMsg string) (string, error) {
Jack Neus0296c732019-07-17 09:35:01 -0600200 if output, err := RunGit(gitRepo, []string{"add", "-A"}); err != nil {
Jack Neusf116fae2019-07-24 15:05:03 -0600201 return "", fmt.Errorf(output.Stderr)
Jack Neusabdbe192019-07-15 12:23:22 -0600202 }
Jack Neus0296c732019-07-17 09:35:01 -0600203 if output, err := RunGit(gitRepo, []string{"commit", "-m", commitMsg}); err != nil {
204 if strings.Contains(output.Stdout, "nothing to commit") {
Jack Neusf116fae2019-07-24 15:05:03 -0600205 return "", fmt.Errorf(output.Stdout)
Jack Neus0296c732019-07-17 09:35:01 -0600206 } else {
Jack Neusf116fae2019-07-24 15:05:03 -0600207 return "", fmt.Errorf(output.Stderr)
Jack Neus0296c732019-07-17 09:35:01 -0600208 }
Jack Neusabdbe192019-07-15 12:23:22 -0600209 }
Jack Neusf116fae2019-07-24 15:05:03 -0600210 output, err := RunGit(gitRepo, []string{"rev-parse", "HEAD"})
211 if err != nil {
212 return "", err
213 }
214 return strings.TrimSpace(output.Stdout), nil
Jack Neusabdbe192019-07-15 12:23:22 -0600215}
216
Jack Neus7440c762019-07-22 10:45:18 -0600217// CommitEmpty makes an empty commit (assuming nothing is staged).
Jack Neusf116fae2019-07-24 15:05:03 -0600218// Returns the sha1 of the commit.
219func CommitEmpty(gitRepo, commitMsg string) (string, error) {
Jack Neus7440c762019-07-22 10:45:18 -0600220 if output, err := RunGit(gitRepo, []string{"commit", "-m", commitMsg, "--allow-empty"}); err != nil {
Jack Neusf116fae2019-07-24 15:05:03 -0600221 return "", fmt.Errorf(output.Stderr)
Jack Neus7440c762019-07-22 10:45:18 -0600222 }
Jack Neusf116fae2019-07-24 15:05:03 -0600223 output, err := RunGit(gitRepo, []string{"rev-parse", "HEAD"})
224 if err != nil {
225 return "", nil
226 }
227 return strings.TrimSpace(output.Stdout), nil
Jack Neus7440c762019-07-22 10:45:18 -0600228}
229
Jack Neusf116fae2019-07-24 15:05:03 -0600230// PushChanges stages and commits any local changes before pushing the commit
231// to the specified remote ref. Returns the sha1 of the commit.
232func PushChanges(gitRepo, localRef, commitMsg string, dryRun bool, pushTo RemoteRef) (string, error) {
233 commit, err := CommitAll(gitRepo, commitMsg)
Jack Neus0296c732019-07-17 09:35:01 -0600234 // It's ok if there's nothing to commit, we can still try to push.
235 if err != nil && !strings.Contains(err.Error(), "nothing to commit") {
Jack Neusf116fae2019-07-24 15:05:03 -0600236 return "", err
Jack Neusabdbe192019-07-15 12:23:22 -0600237 }
Jack Neusf116fae2019-07-24 15:05:03 -0600238 return commit, PushRef(gitRepo, localRef, dryRun, pushTo)
Jack Neus7440c762019-07-22 10:45:18 -0600239}
240
Jack Neuseb25f722019-07-19 16:33:20 -0600241// PushRef pushes the specified local ref to the specified remote ref.
242func PushRef(gitRepo, localRef string, dryRun bool, pushTo RemoteRef) error {
Jack Neusabdbe192019-07-15 12:23:22 -0600243 ref := fmt.Sprintf("%s:%s", localRef, pushTo.Ref)
244 cmd := []string{"push", pushTo.Remote, ref}
245 if dryRun {
246 cmd = append(cmd, "--dry-run")
247 }
Jack Neus7440c762019-07-22 10:45:18 -0600248 _, err := RunGit(gitRepo, cmd)
Jack Neus0296c732019-07-17 09:35:01 -0600249 return err
250}
251
252// Init initializes a repo.
253func Init(gitRepo string, bare bool) error {
254 cmd := []string{"init"}
255 if bare {
256 cmd = append(cmd, "--bare")
257 }
Jack Neusabdbe192019-07-15 12:23:22 -0600258 _, err := RunGit(gitRepo, cmd)
259 return err
260}
Jack Neus0296c732019-07-17 09:35:01 -0600261
262// AddRemote adds a remote.
263func AddRemote(gitRepo, remote, remoteLocation string) error {
264 output, err := RunGit(gitRepo, []string{"remote", "add", remote, remoteLocation})
265 if err != nil {
Jack Neuseb25f722019-07-19 16:33:20 -0600266 if strings.Contains(output.Stderr, "already exists") {
267 return fmt.Errorf("remote already exists")
268 }
Jack Neus0296c732019-07-17 09:35:01 -0600269 }
270 return err
271}
272
273// Checkout checkouts a branch.
274func Checkout(gitRepo, branch string) error {
275 output, err := RunGit(gitRepo, []string{"checkout", branch})
Jack Neus91de2402019-08-14 08:26:03 -0600276 if err != nil {
Jack Neus0296c732019-07-17 09:35:01 -0600277 return fmt.Errorf(output.Stderr)
278 }
279 return err
280}
281
282// DeleteBranch checks out to master and then deletes the current branch.
283func DeleteBranch(gitRepo, branch string, force bool) error {
284 cmd := []string{"branch"}
285 if force {
286 cmd = append(cmd, "-D")
287 } else {
288 cmd = append(cmd, "-d")
289 }
290 cmd = append(cmd, branch)
291 output, err := RunGit(gitRepo, cmd)
292
293 if err != nil {
294 if strings.Contains(output.Stderr, "checked out at") {
295 return fmt.Errorf(output.Stderr)
296 }
297 if strings.Contains(output.Stderr, "not fully merged") {
298 return fmt.Errorf("branch %s is not fully merged. use the force parameter if you wish to proceed", branch)
299 }
300 }
301 return err
302}
303
304// Clone clones the remote into the specified dir.
305func Clone(remote, dir string) error {
306 output, err := RunGit(filepath.Dir(dir), []string{"clone", remote, filepath.Base(dir)})
307 if err != nil {
308 return fmt.Errorf(output.Stderr)
309 }
310 return nil
311}
Jack Neus1f489f62019-08-06 12:01:54 -0600312
313// RemoteBranches returns a list of branches on the specified remote.
314func RemoteBranches(gitRepo, remote string) ([]string, error) {
315 output, err := RunGit(gitRepo, []string{"ls-remote", remote})
316 if err != nil {
317 if strings.Contains(output.Stderr, "not appear to be a git repository") {
318 return []string{}, fmt.Errorf("%s is not a valid remote", remote)
319 }
Jack Neuse2370372019-08-14 17:25:16 -0600320 return []string{}, fmt.Errorf(output.Stderr)
Jack Neus1f489f62019-08-06 12:01:54 -0600321 }
322 remotes := []string{}
323 for _, line := range strings.Split(strings.TrimSpace(output.Stdout), "\n") {
324 if line == "" {
325 continue
326 }
327 remotes = append(remotes, StripRefs(strings.Fields(line)[1]))
328 }
329 return remotes, nil
330}
331
332// RemoteHasBranch checks whether or not a branch exists on a remote.
333func RemoteHasBranch(gitRepo, remote, branch string) (bool, error) {
Jack Neuse2370372019-08-14 17:25:16 -0600334 output, err := RunGit(gitRepo, []string{"ls-remote", remote, branch})
Jack Neus1f489f62019-08-06 12:01:54 -0600335 if err != nil {
Jack Neuse2370372019-08-14 17:25:16 -0600336 if strings.Contains(output.Stderr, "not appear to be a git repository") {
337 return false, fmt.Errorf("%s is not a valid remote", remote)
Jack Neus1f489f62019-08-06 12:01:54 -0600338 }
Jack Neuse2370372019-08-14 17:25:16 -0600339 return false, fmt.Errorf(output.Stderr)
Jack Neus1f489f62019-08-06 12:01:54 -0600340 }
Jack Neuse2370372019-08-14 17:25:16 -0600341 return output.Stdout != "", nil
Jack Neus1f489f62019-08-06 12:01:54 -0600342}
343
344// AssertGitBranches asserts that the git repo has the given branches (it may have others, too).
345func AssertGitBranches(gitRepo string, branches []string) error {
346 actual, err := MatchBranchNameWithNamespace(gitRepo, regexp.MustCompile(".*"), regexp.MustCompile("refs/heads/"))
347 if err != nil {
348 return errors.Annotate(err, "error getting branches").Err()
349 }
350 if !test_util.UnorderedContains(actual, branches) {
351 return fmt.Errorf("project branch mismatch. expected: %v got %v", branches, actual)
352 }
353 return nil
354}
355
356// AssertGitBranches asserts that the git repo has only the correct branches.
357func AssertGitBranchesExact(gitRepo string, branches []string) error {
358 actual, err := MatchBranchNameWithNamespace(gitRepo, regexp.MustCompile(".*"), regexp.MustCompile("refs/heads/"))
359 if err != nil {
360 return errors.Annotate(err, "error getting branches").Err()
361 }
362 // Remove duplicates from branches. This is OK because branch names are unique identifiers
363 // and so having a branch name twice in branches doesn't mean anything special.
364 branchMap := make(map[string]bool)
365 for _, branch := range branches {
366 branchMap[branch] = true
367 }
368 branches = []string{}
369 for branch := range branchMap {
370 branches = append(branches, branch)
371 }
372 if !test_util.UnorderedEqual(actual, branches) {
373 return fmt.Errorf("project branch mismatch. expected: %v got %v", branches, actual)
374 }
375 return nil
376}