blob: 990b5a72ff76d5a16207622498aef1da0ecca3d9 [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 Neusfc3b5772019-07-03 11:18:42 -060015)
16
17var (
Jack Neus07511722019-07-12 15:41:10 -060018 CommandRunnerImpl cmd.CommandRunner = cmd.RealCommandRunner{}
Jack Neusfc3b5772019-07-03 11:18:42 -060019)
20
Jack Neusfc3b5772019-07-03 11:18:42 -060021type CommandOutput struct {
22 Stdout string
23 Stderr string
24}
25
26// Struct representing a remote ref.
27type RemoteRef struct {
28 Remote string
29 Ref string
30}
31
32// RunGit the specified git command in the specified repo. It returns
33// stdout and stderr.
34func RunGit(gitRepo string, cmd []string) (CommandOutput, error) {
35 ctx := context.Background()
36 var stdoutBuf, stderrBuf bytes.Buffer
Jack Neus07511722019-07-12 15:41:10 -060037 err := CommandRunnerImpl.RunCommand(ctx, &stdoutBuf, &stderrBuf, gitRepo, "git", cmd...)
Jack Neusfc3b5772019-07-03 11:18:42 -060038 cmdOutput := CommandOutput{stdoutBuf.String(), stderrBuf.String()}
39 return cmdOutput, err
40}
41
Jack Neus2bcf4a62019-07-25 10:35:05 -060042// RunGitIgnore the specified git command in the specified repo and returns
43// only an error, not the command output.
44func RunGitIgnoreOutput(gitRepo string, cmd []string) error {
45 _, err := RunGit(gitRepo, cmd)
46 return err
47}
48
Jack Neusfc3b5772019-07-03 11:18:42 -060049// GetCurrentBranch returns current branch of a repo, and an empty string
50// if repo is on detached HEAD.
51func GetCurrentBranch(cwd string) string {
52 output, err := RunGit(cwd, []string{"symbolic-ref", "-q", "HEAD"})
53 if err != nil {
54 return ""
55 }
56 return StripRefsHead(strings.TrimSpace(output.Stdout))
57}
58
59// MatchBranchName returns the names of branches who match the specified
60// regular expression.
61func MatchBranchName(gitRepo string, pattern *regexp.Regexp) ([]string, error) {
Jack Neus10e6a122019-07-18 10:17:44 -060062 // MatchBranchWithNamespace trims the namespace off the branches it returns.
63 // Here, we need a namespace that matches every string but doesn't match any character
64 // (so that nothing is trimmed).
65 nullNamespace := regexp.MustCompile("")
66 return MatchBranchNameWithNamespace(gitRepo, pattern, nullNamespace)
67}
68
69// MatchBranchNameWithNamespace returns the names of branches who match the specified
70// pattern and start with the specified namespace.
71func MatchBranchNameWithNamespace(gitRepo string, pattern, namespace *regexp.Regexp) ([]string, error) {
Jack Neusfc3b5772019-07-03 11:18:42 -060072 // Regex should be case insensitive.
Jack Neus10e6a122019-07-18 10:17:44 -060073 namespace = regexp.MustCompile("(?i)^" + namespace.String())
74 pattern = regexp.MustCompile("(?i)" + pattern.String())
Jack Neusfc3b5772019-07-03 11:18:42 -060075
Jack Neus901a6bf2019-07-22 08:30:07 -060076 output, err := RunGit(gitRepo, []string{"show-ref"})
Jack Neusfc3b5772019-07-03 11:18:42 -060077 if err != nil {
Jack Neus901a6bf2019-07-22 08:30:07 -060078 if strings.Contains(err.Error(), "exit status 1") {
79 // Not a fatal error, just no branches.
80 return []string{}, nil
81 }
Jack Neusfc3b5772019-07-03 11:18:42 -060082 // Could not read branches.
Jack Neus901a6bf2019-07-22 08:30:07 -060083 return []string{}, fmt.Errorf("git error: %s\nstdout: %s stderr: %s", err.Error(), output.Stdout, output.Stderr)
Jack Neusfc3b5772019-07-03 11:18:42 -060084 }
85 // Find all branches that match the pattern.
86 branches := strings.Split(output.Stdout, "\n")
87 matchedBranches := []string{}
88 for _, branch := range branches {
89 branch = strings.TrimSpace(branch)
90 if branch == "" {
91 continue
92 }
93 branch = strings.Fields(branch)[1]
Jack Neus10e6a122019-07-18 10:17:44 -060094
95 // Only look at branches which match the namespace.
96 if !namespace.Match([]byte(branch)) {
97 continue
98 }
99 branch = namespace.ReplaceAllString(branch, "")
100
Jack Neusfc3b5772019-07-03 11:18:42 -0600101 if pattern.Match([]byte(branch)) {
102 matchedBranches = append(matchedBranches, branch)
103 }
104 }
105 return matchedBranches, nil
106}
107
108// GetGitRepoRevision finds and returns the revision of a branch.
Jack Neusa7287522019-07-23 16:36:18 -0600109func GetGitRepoRevision(cwd, branch string) (string, error) {
110 if branch == "" {
111 branch = "HEAD"
Jack Neus8b770832019-08-01 15:33:04 -0600112 } else {
113 branch = NormalizeRef(branch)
Jack Neusa7287522019-07-23 16:36:18 -0600114 }
115 output, err := RunGit(cwd, []string{"rev-parse", branch})
Jack Neusfc3b5772019-07-03 11:18:42 -0600116 return strings.TrimSpace(output.Stdout), err
117}
118
Jack Neusa7287522019-07-23 16:36:18 -0600119// IsReachable determines whether one commit ref is reachable from another.
120func IsReachable(cwd, to_ref, from_ref string) (bool, error) {
121 _, err := RunGit(cwd, []string{"merge-base", "--is-ancestor", to_ref, from_ref})
122 if err != nil {
123 if strings.Contains(err.Error(), "exit status 1") {
124 return false, nil
125 }
126 return false, err
127 }
128 return true, nil
129}
130
Jack Neus7440c762019-07-22 10:45:18 -0600131// StripRefsHead removes leading 'refs/heads/' from a ref name.
Jack Neusfc3b5772019-07-03 11:18:42 -0600132func StripRefsHead(ref string) string {
133 return strings.TrimPrefix(ref, "refs/heads/")
134}
135
136// NormalizeRef converts git branch refs into fully qualified form.
137func NormalizeRef(ref string) string {
138 if ref == "" || strings.HasPrefix(ref, "refs/") {
139 return ref
140 }
141 return fmt.Sprintf("refs/heads/%s", ref)
142}
143
144// StripRefs removes leading 'refs/heads/', 'refs/remotes/[^/]+/' from a ref name.
145func StripRefs(ref string) string {
146 ref = StripRefsHead(ref)
147 // If the ref starts with ref/remotes/, then we want the part of the string
148 // that comes after the third "/".
149 // Example: refs/remotes/origin/master --> master
150 // Example: refs/remotse/origin/foo/bar --> foo/bar
151 if strings.HasPrefix(ref, "refs/remotes/") {
152 refParts := strings.SplitN(ref, "/", 4)
153 return refParts[len(refParts)-1]
154 }
155 return ref
156}
Jack Neusabdbe192019-07-15 12:23:22 -0600157
158// CreateBranch creates a branch.
159func CreateBranch(gitRepo, branch string) error {
Jack Neus0296c732019-07-17 09:35:01 -0600160 output, err := RunGit(gitRepo, []string{"checkout", "-B", branch})
Jack Neus10e6a122019-07-18 10:17:44 -0600161 if err != nil {
162 if strings.Contains(output.Stderr, "not a valid branch name") {
163 return fmt.Errorf("%s is not a valid branch name", branch)
164 } else {
165 return fmt.Errorf(output.Stderr)
166 }
Jack Neus0296c732019-07-17 09:35:01 -0600167 }
168 return err
169}
170
171// CreateTrackingBranch creates a tracking branch.
172func CreateTrackingBranch(gitRepo, branch string, remoteRef RemoteRef) error {
173 refspec := fmt.Sprintf("%s/%s", remoteRef.Remote, remoteRef.Ref)
174 output, err := RunGit(gitRepo, []string{"fetch", remoteRef.Remote, remoteRef.Ref})
175 if err != nil {
176 return fmt.Errorf("could not fetch %s: %s", refspec, output.Stderr)
177 }
178 output, err = RunGit(gitRepo, []string{"checkout", "-b", branch, "-t", refspec})
179 if err != nil {
180 if strings.Contains(output.Stderr, "not a valid branch name") {
181 return fmt.Errorf("%s is not a valid branch name", branch)
182 } else {
183 return fmt.Errorf(output.Stderr)
184 }
185 }
Jack Neusabdbe192019-07-15 12:23:22 -0600186 return err
187}
188
189// CommitAll adds all local changes and commits them.
Jack Neusf116fae2019-07-24 15:05:03 -0600190// Returns the sha1 of the commit.
191func CommitAll(gitRepo, commitMsg string) (string, error) {
Jack Neus0296c732019-07-17 09:35:01 -0600192 if output, err := RunGit(gitRepo, []string{"add", "-A"}); err != nil {
Jack Neusf116fae2019-07-24 15:05:03 -0600193 return "", fmt.Errorf(output.Stderr)
Jack Neusabdbe192019-07-15 12:23:22 -0600194 }
Jack Neus0296c732019-07-17 09:35:01 -0600195 if output, err := RunGit(gitRepo, []string{"commit", "-m", commitMsg}); err != nil {
196 if strings.Contains(output.Stdout, "nothing to commit") {
Jack Neusf116fae2019-07-24 15:05:03 -0600197 return "", fmt.Errorf(output.Stdout)
Jack Neus0296c732019-07-17 09:35:01 -0600198 } else {
Jack Neusf116fae2019-07-24 15:05:03 -0600199 return "", fmt.Errorf(output.Stderr)
Jack Neus0296c732019-07-17 09:35:01 -0600200 }
Jack Neusabdbe192019-07-15 12:23:22 -0600201 }
Jack Neusf116fae2019-07-24 15:05:03 -0600202 output, err := RunGit(gitRepo, []string{"rev-parse", "HEAD"})
203 if err != nil {
204 return "", err
205 }
206 return strings.TrimSpace(output.Stdout), nil
Jack Neusabdbe192019-07-15 12:23:22 -0600207}
208
Jack Neus7440c762019-07-22 10:45:18 -0600209// CommitEmpty makes an empty commit (assuming nothing is staged).
Jack Neusf116fae2019-07-24 15:05:03 -0600210// Returns the sha1 of the commit.
211func CommitEmpty(gitRepo, commitMsg string) (string, error) {
Jack Neus7440c762019-07-22 10:45:18 -0600212 if output, err := RunGit(gitRepo, []string{"commit", "-m", commitMsg, "--allow-empty"}); err != nil {
Jack Neusf116fae2019-07-24 15:05:03 -0600213 return "", fmt.Errorf(output.Stderr)
Jack Neus7440c762019-07-22 10:45:18 -0600214 }
Jack Neusf116fae2019-07-24 15:05:03 -0600215 output, err := RunGit(gitRepo, []string{"rev-parse", "HEAD"})
216 if err != nil {
217 return "", nil
218 }
219 return strings.TrimSpace(output.Stdout), nil
Jack Neus7440c762019-07-22 10:45:18 -0600220}
221
Jack Neusf116fae2019-07-24 15:05:03 -0600222// PushChanges stages and commits any local changes before pushing the commit
223// to the specified remote ref. Returns the sha1 of the commit.
224func PushChanges(gitRepo, localRef, commitMsg string, dryRun bool, pushTo RemoteRef) (string, error) {
225 commit, err := CommitAll(gitRepo, commitMsg)
Jack Neus0296c732019-07-17 09:35:01 -0600226 // It's ok if there's nothing to commit, we can still try to push.
227 if err != nil && !strings.Contains(err.Error(), "nothing to commit") {
Jack Neusf116fae2019-07-24 15:05:03 -0600228 return "", err
Jack Neusabdbe192019-07-15 12:23:22 -0600229 }
Jack Neusf116fae2019-07-24 15:05:03 -0600230 return commit, PushRef(gitRepo, localRef, dryRun, pushTo)
Jack Neus7440c762019-07-22 10:45:18 -0600231}
232
Jack Neuseb25f722019-07-19 16:33:20 -0600233// PushRef pushes the specified local ref to the specified remote ref.
234func PushRef(gitRepo, localRef string, dryRun bool, pushTo RemoteRef) error {
Jack Neusabdbe192019-07-15 12:23:22 -0600235 ref := fmt.Sprintf("%s:%s", localRef, pushTo.Ref)
236 cmd := []string{"push", pushTo.Remote, ref}
237 if dryRun {
238 cmd = append(cmd, "--dry-run")
239 }
Jack Neus7440c762019-07-22 10:45:18 -0600240 _, err := RunGit(gitRepo, cmd)
Jack Neus0296c732019-07-17 09:35:01 -0600241 return err
242}
243
244// Init initializes a repo.
245func Init(gitRepo string, bare bool) error {
246 cmd := []string{"init"}
247 if bare {
248 cmd = append(cmd, "--bare")
249 }
Jack Neusabdbe192019-07-15 12:23:22 -0600250 _, err := RunGit(gitRepo, cmd)
251 return err
252}
Jack Neus0296c732019-07-17 09:35:01 -0600253
254// AddRemote adds a remote.
255func AddRemote(gitRepo, remote, remoteLocation string) error {
256 output, err := RunGit(gitRepo, []string{"remote", "add", remote, remoteLocation})
257 if err != nil {
Jack Neuseb25f722019-07-19 16:33:20 -0600258 if strings.Contains(output.Stderr, "already exists") {
259 return fmt.Errorf("remote already exists")
260 }
Jack Neus0296c732019-07-17 09:35:01 -0600261 }
262 return err
263}
264
265// Checkout checkouts a branch.
266func Checkout(gitRepo, branch string) error {
267 output, err := RunGit(gitRepo, []string{"checkout", branch})
268 if err != nil && strings.Contains(output.Stderr, "did not match any") {
269 return fmt.Errorf(output.Stderr)
270 }
271 return err
272}
273
274// DeleteBranch checks out to master and then deletes the current branch.
275func DeleteBranch(gitRepo, branch string, force bool) error {
276 cmd := []string{"branch"}
277 if force {
278 cmd = append(cmd, "-D")
279 } else {
280 cmd = append(cmd, "-d")
281 }
282 cmd = append(cmd, branch)
283 output, err := RunGit(gitRepo, cmd)
284
285 if err != nil {
286 if strings.Contains(output.Stderr, "checked out at") {
287 return fmt.Errorf(output.Stderr)
288 }
289 if strings.Contains(output.Stderr, "not fully merged") {
290 return fmt.Errorf("branch %s is not fully merged. use the force parameter if you wish to proceed", branch)
291 }
292 }
293 return err
294}
295
296// Clone clones the remote into the specified dir.
297func Clone(remote, dir string) error {
298 output, err := RunGit(filepath.Dir(dir), []string{"clone", remote, filepath.Base(dir)})
299 if err != nil {
300 return fmt.Errorf(output.Stderr)
301 }
302 return nil
303}