blob: 6b10dfb93e3876035d3ed7c7ddf8e619988cbc19 [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
Jack Neusab8df5b2019-08-16 13:43:12 -060033type GitOpts struct {
34 DryRun bool
35 Force bool
36}
37
Jack Neusfc3b5772019-07-03 11:18:42 -060038// RunGit the specified git command in the specified repo. It returns
39// stdout and stderr.
40func RunGit(gitRepo string, cmd []string) (CommandOutput, error) {
41 ctx := context.Background()
42 var stdoutBuf, stderrBuf bytes.Buffer
Jack Neus07511722019-07-12 15:41:10 -060043 err := CommandRunnerImpl.RunCommand(ctx, &stdoutBuf, &stderrBuf, gitRepo, "git", cmd...)
Jack Neusfc3b5772019-07-03 11:18:42 -060044 cmdOutput := CommandOutput{stdoutBuf.String(), stderrBuf.String()}
Jack Neus758a70f2019-08-20 10:32:12 -060045 return cmdOutput, errors.Annotate(err, cmdOutput.Stderr).Err()
Jack Neusfc3b5772019-07-03 11:18:42 -060046}
47
Jack Neus2bcf4a62019-07-25 10:35:05 -060048// RunGitIgnore the specified git command in the specified repo and returns
49// only an error, not the command output.
50func RunGitIgnoreOutput(gitRepo string, cmd []string) error {
51 _, err := RunGit(gitRepo, cmd)
52 return err
53}
54
Jack Neusfc3b5772019-07-03 11:18:42 -060055// GetCurrentBranch returns current branch of a repo, and an empty string
56// if repo is on detached HEAD.
57func GetCurrentBranch(cwd string) string {
58 output, err := RunGit(cwd, []string{"symbolic-ref", "-q", "HEAD"})
59 if err != nil {
60 return ""
61 }
62 return StripRefsHead(strings.TrimSpace(output.Stdout))
63}
64
65// MatchBranchName returns the names of branches who match the specified
66// regular expression.
67func MatchBranchName(gitRepo string, pattern *regexp.Regexp) ([]string, error) {
Jack Neus10e6a122019-07-18 10:17:44 -060068 // MatchBranchWithNamespace trims the namespace off the branches it returns.
69 // Here, we need a namespace that matches every string but doesn't match any character
70 // (so that nothing is trimmed).
71 nullNamespace := regexp.MustCompile("")
72 return MatchBranchNameWithNamespace(gitRepo, pattern, nullNamespace)
73}
74
75// MatchBranchNameWithNamespace returns the names of branches who match the specified
76// pattern and start with the specified namespace.
77func MatchBranchNameWithNamespace(gitRepo string, pattern, namespace *regexp.Regexp) ([]string, error) {
Jack Neusfc3b5772019-07-03 11:18:42 -060078 // Regex should be case insensitive.
Jack Neus10e6a122019-07-18 10:17:44 -060079 namespace = regexp.MustCompile("(?i)^" + namespace.String())
80 pattern = regexp.MustCompile("(?i)" + pattern.String())
Jack Neusfc3b5772019-07-03 11:18:42 -060081
Jack Neus901a6bf2019-07-22 08:30:07 -060082 output, err := RunGit(gitRepo, []string{"show-ref"})
Jack Neusfc3b5772019-07-03 11:18:42 -060083 if err != nil {
Jack Neus901a6bf2019-07-22 08:30:07 -060084 if strings.Contains(err.Error(), "exit status 1") {
85 // Not a fatal error, just no branches.
86 return []string{}, nil
87 }
Jack Neusfc3b5772019-07-03 11:18:42 -060088 // Could not read branches.
Jack Neus901a6bf2019-07-22 08:30:07 -060089 return []string{}, fmt.Errorf("git error: %s\nstdout: %s stderr: %s", err.Error(), output.Stdout, output.Stderr)
Jack Neusfc3b5772019-07-03 11:18:42 -060090 }
91 // Find all branches that match the pattern.
92 branches := strings.Split(output.Stdout, "\n")
93 matchedBranches := []string{}
94 for _, branch := range branches {
95 branch = strings.TrimSpace(branch)
96 if branch == "" {
97 continue
98 }
99 branch = strings.Fields(branch)[1]
Jack Neus10e6a122019-07-18 10:17:44 -0600100
101 // Only look at branches which match the namespace.
102 if !namespace.Match([]byte(branch)) {
103 continue
104 }
105 branch = namespace.ReplaceAllString(branch, "")
106
Jack Neusfc3b5772019-07-03 11:18:42 -0600107 if pattern.Match([]byte(branch)) {
108 matchedBranches = append(matchedBranches, branch)
109 }
110 }
111 return matchedBranches, nil
112}
113
Jack Neuse2370372019-08-14 17:25:16 -0600114// IsSHA checks whether or not the given ref is a SHA.
115func IsSHA(ref string) bool {
116 shaRegexp := regexp.MustCompile("^[0-9a-f]{40}$")
117 return shaRegexp.MatchString(ref)
118}
119
Jack Neusfc3b5772019-07-03 11:18:42 -0600120// GetGitRepoRevision finds and returns the revision of a branch.
Jack Neusa7287522019-07-23 16:36:18 -0600121func GetGitRepoRevision(cwd, branch string) (string, error) {
122 if branch == "" {
123 branch = "HEAD"
Jack Neus1f489f62019-08-06 12:01:54 -0600124 } else if branch != "HEAD" {
Jack Neus8b770832019-08-01 15:33:04 -0600125 branch = NormalizeRef(branch)
Jack Neusa7287522019-07-23 16:36:18 -0600126 }
127 output, err := RunGit(cwd, []string{"rev-parse", branch})
Jack Neus1f489f62019-08-06 12:01:54 -0600128 return strings.TrimSpace(output.Stdout), errors.Annotate(err, output.Stderr).Err()
Jack Neusfc3b5772019-07-03 11:18:42 -0600129}
130
Jack Neusa7287522019-07-23 16:36:18 -0600131// IsReachable determines whether one commit ref is reachable from another.
132func IsReachable(cwd, to_ref, from_ref string) (bool, error) {
133 _, err := RunGit(cwd, []string{"merge-base", "--is-ancestor", to_ref, from_ref})
134 if err != nil {
135 if strings.Contains(err.Error(), "exit status 1") {
136 return false, nil
137 }
138 return false, err
139 }
140 return true, nil
141}
142
Jack Neus7440c762019-07-22 10:45:18 -0600143// StripRefsHead removes leading 'refs/heads/' from a ref name.
Jack Neusfc3b5772019-07-03 11:18:42 -0600144func StripRefsHead(ref string) string {
145 return strings.TrimPrefix(ref, "refs/heads/")
146}
147
148// NormalizeRef converts git branch refs into fully qualified form.
149func NormalizeRef(ref string) string {
150 if ref == "" || strings.HasPrefix(ref, "refs/") {
151 return ref
152 }
153 return fmt.Sprintf("refs/heads/%s", ref)
154}
155
156// StripRefs removes leading 'refs/heads/', 'refs/remotes/[^/]+/' from a ref name.
157func StripRefs(ref string) string {
158 ref = StripRefsHead(ref)
159 // If the ref starts with ref/remotes/, then we want the part of the string
160 // that comes after the third "/".
161 // Example: refs/remotes/origin/master --> master
162 // Example: refs/remotse/origin/foo/bar --> foo/bar
163 if strings.HasPrefix(ref, "refs/remotes/") {
164 refParts := strings.SplitN(ref, "/", 4)
165 return refParts[len(refParts)-1]
166 }
167 return ref
168}
Jack Neusabdbe192019-07-15 12:23:22 -0600169
170// CreateBranch creates a branch.
171func CreateBranch(gitRepo, branch string) error {
Jack Neus0296c732019-07-17 09:35:01 -0600172 output, err := RunGit(gitRepo, []string{"checkout", "-B", branch})
Jack Neus10e6a122019-07-18 10:17:44 -0600173 if err != nil {
174 if strings.Contains(output.Stderr, "not a valid branch name") {
175 return fmt.Errorf("%s is not a valid branch name", branch)
176 } else {
177 return fmt.Errorf(output.Stderr)
178 }
Jack Neus0296c732019-07-17 09:35:01 -0600179 }
180 return err
181}
182
183// CreateTrackingBranch creates a tracking branch.
184func CreateTrackingBranch(gitRepo, branch string, remoteRef RemoteRef) error {
185 refspec := fmt.Sprintf("%s/%s", remoteRef.Remote, remoteRef.Ref)
186 output, err := RunGit(gitRepo, []string{"fetch", remoteRef.Remote, remoteRef.Ref})
187 if err != nil {
188 return fmt.Errorf("could not fetch %s: %s", refspec, output.Stderr)
189 }
190 output, err = RunGit(gitRepo, []string{"checkout", "-b", branch, "-t", refspec})
191 if err != nil {
192 if strings.Contains(output.Stderr, "not a valid branch name") {
193 return fmt.Errorf("%s is not a valid branch name", branch)
194 } else {
195 return fmt.Errorf(output.Stderr)
196 }
197 }
Jack Neusabdbe192019-07-15 12:23:22 -0600198 return err
199}
200
201// CommitAll adds all local changes and commits them.
Jack Neusf116fae2019-07-24 15:05:03 -0600202// Returns the sha1 of the commit.
203func CommitAll(gitRepo, commitMsg string) (string, error) {
Jack Neus0296c732019-07-17 09:35:01 -0600204 if output, err := RunGit(gitRepo, []string{"add", "-A"}); err != nil {
Jack Neusf116fae2019-07-24 15:05:03 -0600205 return "", fmt.Errorf(output.Stderr)
Jack Neusabdbe192019-07-15 12:23:22 -0600206 }
Jack Neus0296c732019-07-17 09:35:01 -0600207 if output, err := RunGit(gitRepo, []string{"commit", "-m", commitMsg}); err != nil {
208 if strings.Contains(output.Stdout, "nothing to commit") {
Jack Neusf116fae2019-07-24 15:05:03 -0600209 return "", fmt.Errorf(output.Stdout)
Jack Neus0296c732019-07-17 09:35:01 -0600210 } else {
Jack Neusf116fae2019-07-24 15:05:03 -0600211 return "", fmt.Errorf(output.Stderr)
Jack Neus0296c732019-07-17 09:35:01 -0600212 }
Jack Neusabdbe192019-07-15 12:23:22 -0600213 }
Jack Neusf116fae2019-07-24 15:05:03 -0600214 output, err := RunGit(gitRepo, []string{"rev-parse", "HEAD"})
215 if err != nil {
216 return "", err
217 }
218 return strings.TrimSpace(output.Stdout), nil
Jack Neusabdbe192019-07-15 12:23:22 -0600219}
220
Jack Neus7440c762019-07-22 10:45:18 -0600221// CommitEmpty makes an empty commit (assuming nothing is staged).
Jack Neusf116fae2019-07-24 15:05:03 -0600222// Returns the sha1 of the commit.
223func CommitEmpty(gitRepo, commitMsg string) (string, error) {
Jack Neus7440c762019-07-22 10:45:18 -0600224 if output, err := RunGit(gitRepo, []string{"commit", "-m", commitMsg, "--allow-empty"}); err != nil {
Jack Neusf116fae2019-07-24 15:05:03 -0600225 return "", fmt.Errorf(output.Stderr)
Jack Neus7440c762019-07-22 10:45:18 -0600226 }
Jack Neusf116fae2019-07-24 15:05:03 -0600227 output, err := RunGit(gitRepo, []string{"rev-parse", "HEAD"})
228 if err != nil {
Jack Neusab8df5b2019-08-16 13:43:12 -0600229 return "", err
Jack Neusf116fae2019-07-24 15:05:03 -0600230 }
231 return strings.TrimSpace(output.Stdout), nil
Jack Neus7440c762019-07-22 10:45:18 -0600232}
233
Jack Neuseb25f722019-07-19 16:33:20 -0600234// PushRef pushes the specified local ref to the specified remote ref.
Jack Neusab8df5b2019-08-16 13:43:12 -0600235func PushRef(gitRepo, localRef string, pushTo RemoteRef, opts GitOpts) error {
Jack Neusabdbe192019-07-15 12:23:22 -0600236 ref := fmt.Sprintf("%s:%s", localRef, pushTo.Ref)
237 cmd := []string{"push", pushTo.Remote, ref}
Jack Neusab8df5b2019-08-16 13:43:12 -0600238 if opts.DryRun {
Jack Neusabdbe192019-07-15 12:23:22 -0600239 cmd = append(cmd, "--dry-run")
240 }
Jack Neusab8df5b2019-08-16 13:43:12 -0600241 if opts.Force {
242 cmd = append(cmd, "--force")
243 }
Jack Neus7440c762019-07-22 10:45:18 -0600244 _, err := RunGit(gitRepo, cmd)
Jack Neus0296c732019-07-17 09:35:01 -0600245 return err
246}
247
248// Init initializes a repo.
249func Init(gitRepo string, bare bool) error {
250 cmd := []string{"init"}
251 if bare {
252 cmd = append(cmd, "--bare")
253 }
Jack Neusabdbe192019-07-15 12:23:22 -0600254 _, err := RunGit(gitRepo, cmd)
255 return err
256}
Jack Neus0296c732019-07-17 09:35:01 -0600257
258// AddRemote adds a remote.
259func AddRemote(gitRepo, remote, remoteLocation string) error {
260 output, err := RunGit(gitRepo, []string{"remote", "add", remote, remoteLocation})
261 if err != nil {
Jack Neuseb25f722019-07-19 16:33:20 -0600262 if strings.Contains(output.Stderr, "already exists") {
263 return fmt.Errorf("remote already exists")
264 }
Jack Neus0296c732019-07-17 09:35:01 -0600265 }
266 return err
267}
268
269// Checkout checkouts a branch.
270func Checkout(gitRepo, branch string) error {
271 output, err := RunGit(gitRepo, []string{"checkout", branch})
Jack Neus91de2402019-08-14 08:26:03 -0600272 if err != nil {
Jack Neus0296c732019-07-17 09:35:01 -0600273 return fmt.Errorf(output.Stderr)
274 }
275 return err
276}
277
278// DeleteBranch checks out to master and then deletes the current branch.
279func DeleteBranch(gitRepo, branch string, force bool) error {
280 cmd := []string{"branch"}
281 if force {
282 cmd = append(cmd, "-D")
283 } else {
284 cmd = append(cmd, "-d")
285 }
286 cmd = append(cmd, branch)
287 output, err := RunGit(gitRepo, cmd)
288
289 if err != nil {
290 if strings.Contains(output.Stderr, "checked out at") {
291 return fmt.Errorf(output.Stderr)
292 }
293 if strings.Contains(output.Stderr, "not fully merged") {
294 return fmt.Errorf("branch %s is not fully merged. use the force parameter if you wish to proceed", branch)
295 }
296 }
297 return err
298}
299
300// Clone clones the remote into the specified dir.
301func Clone(remote, dir string) error {
302 output, err := RunGit(filepath.Dir(dir), []string{"clone", remote, filepath.Base(dir)})
303 if err != nil {
304 return fmt.Errorf(output.Stderr)
305 }
306 return nil
307}
Jack Neus1f489f62019-08-06 12:01:54 -0600308
309// RemoteBranches returns a list of branches on the specified remote.
310func RemoteBranches(gitRepo, remote string) ([]string, error) {
311 output, err := RunGit(gitRepo, []string{"ls-remote", remote})
312 if err != nil {
313 if strings.Contains(output.Stderr, "not appear to be a git repository") {
314 return []string{}, fmt.Errorf("%s is not a valid remote", remote)
315 }
Jack Neuse2370372019-08-14 17:25:16 -0600316 return []string{}, fmt.Errorf(output.Stderr)
Jack Neus1f489f62019-08-06 12:01:54 -0600317 }
318 remotes := []string{}
319 for _, line := range strings.Split(strings.TrimSpace(output.Stdout), "\n") {
320 if line == "" {
321 continue
322 }
323 remotes = append(remotes, StripRefs(strings.Fields(line)[1]))
324 }
325 return remotes, nil
326}
327
328// RemoteHasBranch checks whether or not a branch exists on a remote.
329func RemoteHasBranch(gitRepo, remote, branch string) (bool, error) {
Jack Neuse2370372019-08-14 17:25:16 -0600330 output, err := RunGit(gitRepo, []string{"ls-remote", remote, branch})
Jack Neus1f489f62019-08-06 12:01:54 -0600331 if err != nil {
Jack Neuse2370372019-08-14 17:25:16 -0600332 if strings.Contains(output.Stderr, "not appear to be a git repository") {
333 return false, fmt.Errorf("%s is not a valid remote", remote)
Jack Neus1f489f62019-08-06 12:01:54 -0600334 }
Jack Neuse2370372019-08-14 17:25:16 -0600335 return false, fmt.Errorf(output.Stderr)
Jack Neus1f489f62019-08-06 12:01:54 -0600336 }
Jack Neuse2370372019-08-14 17:25:16 -0600337 return output.Stdout != "", nil
Jack Neus1f489f62019-08-06 12:01:54 -0600338}