blob: 095754e8e037b50850a07c7a13eac8ad8876fcbb [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
42// GetCurrentBranch returns current branch of a repo, and an empty string
43// if repo is on detached HEAD.
44func GetCurrentBranch(cwd string) string {
45 output, err := RunGit(cwd, []string{"symbolic-ref", "-q", "HEAD"})
46 if err != nil {
47 return ""
48 }
49 return StripRefsHead(strings.TrimSpace(output.Stdout))
50}
51
52// MatchBranchName returns the names of branches who match the specified
53// regular expression.
54func MatchBranchName(gitRepo string, pattern *regexp.Regexp) ([]string, error) {
Jack Neus10e6a122019-07-18 10:17:44 -060055 // MatchBranchWithNamespace trims the namespace off the branches it returns.
56 // Here, we need a namespace that matches every string but doesn't match any character
57 // (so that nothing is trimmed).
58 nullNamespace := regexp.MustCompile("")
59 return MatchBranchNameWithNamespace(gitRepo, pattern, nullNamespace)
60}
61
62// MatchBranchNameWithNamespace returns the names of branches who match the specified
63// pattern and start with the specified namespace.
64func MatchBranchNameWithNamespace(gitRepo string, pattern, namespace *regexp.Regexp) ([]string, error) {
Jack Neusfc3b5772019-07-03 11:18:42 -060065 // Regex should be case insensitive.
Jack Neus10e6a122019-07-18 10:17:44 -060066 namespace = regexp.MustCompile("(?i)^" + namespace.String())
67 pattern = regexp.MustCompile("(?i)" + pattern.String())
Jack Neusfc3b5772019-07-03 11:18:42 -060068
Jack Neus901a6bf2019-07-22 08:30:07 -060069 output, err := RunGit(gitRepo, []string{"show-ref"})
Jack Neusfc3b5772019-07-03 11:18:42 -060070 if err != nil {
Jack Neus901a6bf2019-07-22 08:30:07 -060071 if strings.Contains(err.Error(), "exit status 1") {
72 // Not a fatal error, just no branches.
73 return []string{}, nil
74 }
Jack Neusfc3b5772019-07-03 11:18:42 -060075 // Could not read branches.
Jack Neus901a6bf2019-07-22 08:30:07 -060076 return []string{}, fmt.Errorf("git error: %s\nstdout: %s stderr: %s", err.Error(), output.Stdout, output.Stderr)
Jack Neusfc3b5772019-07-03 11:18:42 -060077 }
78 // Find all branches that match the pattern.
79 branches := strings.Split(output.Stdout, "\n")
80 matchedBranches := []string{}
81 for _, branch := range branches {
82 branch = strings.TrimSpace(branch)
83 if branch == "" {
84 continue
85 }
86 branch = strings.Fields(branch)[1]
Jack Neus10e6a122019-07-18 10:17:44 -060087
88 // Only look at branches which match the namespace.
89 if !namespace.Match([]byte(branch)) {
90 continue
91 }
92 branch = namespace.ReplaceAllString(branch, "")
93
Jack Neusfc3b5772019-07-03 11:18:42 -060094 if pattern.Match([]byte(branch)) {
95 matchedBranches = append(matchedBranches, branch)
96 }
97 }
98 return matchedBranches, nil
99}
100
101// GetGitRepoRevision finds and returns the revision of a branch.
Jack Neusa7287522019-07-23 16:36:18 -0600102func GetGitRepoRevision(cwd, branch string) (string, error) {
103 if branch == "" {
104 branch = "HEAD"
105 }
106 output, err := RunGit(cwd, []string{"rev-parse", branch})
Jack Neusfc3b5772019-07-03 11:18:42 -0600107 return strings.TrimSpace(output.Stdout), err
108}
109
Jack Neusa7287522019-07-23 16:36:18 -0600110// IsReachable determines whether one commit ref is reachable from another.
111func IsReachable(cwd, to_ref, from_ref string) (bool, error) {
112 _, err := RunGit(cwd, []string{"merge-base", "--is-ancestor", to_ref, from_ref})
113 if err != nil {
114 if strings.Contains(err.Error(), "exit status 1") {
115 return false, nil
116 }
117 return false, err
118 }
119 return true, nil
120}
121
Jack Neus7440c762019-07-22 10:45:18 -0600122// StripRefsHead removes leading 'refs/heads/' from a ref name.
Jack Neusfc3b5772019-07-03 11:18:42 -0600123func StripRefsHead(ref string) string {
124 return strings.TrimPrefix(ref, "refs/heads/")
125}
126
127// NormalizeRef converts git branch refs into fully qualified form.
128func NormalizeRef(ref string) string {
129 if ref == "" || strings.HasPrefix(ref, "refs/") {
130 return ref
131 }
132 return fmt.Sprintf("refs/heads/%s", ref)
133}
134
135// StripRefs removes leading 'refs/heads/', 'refs/remotes/[^/]+/' from a ref name.
136func StripRefs(ref string) string {
137 ref = StripRefsHead(ref)
138 // If the ref starts with ref/remotes/, then we want the part of the string
139 // that comes after the third "/".
140 // Example: refs/remotes/origin/master --> master
141 // Example: refs/remotse/origin/foo/bar --> foo/bar
142 if strings.HasPrefix(ref, "refs/remotes/") {
143 refParts := strings.SplitN(ref, "/", 4)
144 return refParts[len(refParts)-1]
145 }
146 return ref
147}
Jack Neusabdbe192019-07-15 12:23:22 -0600148
149// CreateBranch creates a branch.
150func CreateBranch(gitRepo, branch string) error {
Jack Neus0296c732019-07-17 09:35:01 -0600151 output, err := RunGit(gitRepo, []string{"checkout", "-B", branch})
Jack Neus10e6a122019-07-18 10:17:44 -0600152 if err != nil {
153 if strings.Contains(output.Stderr, "not a valid branch name") {
154 return fmt.Errorf("%s is not a valid branch name", branch)
155 } else {
156 return fmt.Errorf(output.Stderr)
157 }
Jack Neus0296c732019-07-17 09:35:01 -0600158 }
159 return err
160}
161
162// CreateTrackingBranch creates a tracking branch.
163func CreateTrackingBranch(gitRepo, branch string, remoteRef RemoteRef) error {
164 refspec := fmt.Sprintf("%s/%s", remoteRef.Remote, remoteRef.Ref)
165 output, err := RunGit(gitRepo, []string{"fetch", remoteRef.Remote, remoteRef.Ref})
166 if err != nil {
167 return fmt.Errorf("could not fetch %s: %s", refspec, output.Stderr)
168 }
169 output, err = RunGit(gitRepo, []string{"checkout", "-b", branch, "-t", refspec})
170 if err != nil {
171 if strings.Contains(output.Stderr, "not a valid branch name") {
172 return fmt.Errorf("%s is not a valid branch name", branch)
173 } else {
174 return fmt.Errorf(output.Stderr)
175 }
176 }
Jack Neusabdbe192019-07-15 12:23:22 -0600177 return err
178}
179
180// CommitAll adds all local changes and commits them.
181func CommitAll(gitRepo, commitMsg string) error {
Jack Neus0296c732019-07-17 09:35:01 -0600182 if output, err := RunGit(gitRepo, []string{"add", "-A"}); err != nil {
183 return fmt.Errorf(output.Stderr)
Jack Neusabdbe192019-07-15 12:23:22 -0600184 }
Jack Neus0296c732019-07-17 09:35:01 -0600185 if output, err := RunGit(gitRepo, []string{"commit", "-m", commitMsg}); err != nil {
186 if strings.Contains(output.Stdout, "nothing to commit") {
187 return fmt.Errorf(output.Stdout)
188 } else {
189 return fmt.Errorf(output.Stderr)
190 }
Jack Neusabdbe192019-07-15 12:23:22 -0600191 }
192 return nil
193}
194
Jack Neus7440c762019-07-22 10:45:18 -0600195// CommitEmpty makes an empty commit (assuming nothing is staged).
196func CommitEmpty(gitRepo, commitMsg string) error {
197 if output, err := RunGit(gitRepo, []string{"commit", "-m", commitMsg, "--allow-empty"}); err != nil {
198 return fmt.Errorf(output.Stderr)
199 }
200 return nil
201}
202
Jack Neusabdbe192019-07-15 12:23:22 -0600203// PushGitChanges stages and commits any local changes before pushing the commit
204// to the specified remote ref.
205func PushChanges(gitRepo, localRef, commitMsg string, dryRun bool, pushTo RemoteRef) error {
Jack Neus0296c732019-07-17 09:35:01 -0600206 err := CommitAll(gitRepo, commitMsg)
207 // It's ok if there's nothing to commit, we can still try to push.
208 if err != nil && !strings.Contains(err.Error(), "nothing to commit") {
Jack Neusabdbe192019-07-15 12:23:22 -0600209 return err
210 }
Jack Neuseb25f722019-07-19 16:33:20 -0600211 return PushRef(gitRepo, localRef, dryRun, pushTo)
Jack Neus7440c762019-07-22 10:45:18 -0600212}
213
Jack Neuseb25f722019-07-19 16:33:20 -0600214// PushRef pushes the specified local ref to the specified remote ref.
215func PushRef(gitRepo, localRef string, dryRun bool, pushTo RemoteRef) error {
Jack Neusabdbe192019-07-15 12:23:22 -0600216 ref := fmt.Sprintf("%s:%s", localRef, pushTo.Ref)
217 cmd := []string{"push", pushTo.Remote, ref}
218 if dryRun {
219 cmd = append(cmd, "--dry-run")
220 }
Jack Neus7440c762019-07-22 10:45:18 -0600221 _, err := RunGit(gitRepo, cmd)
Jack Neus0296c732019-07-17 09:35:01 -0600222 return err
223}
224
225// Init initializes a repo.
226func Init(gitRepo string, bare bool) error {
227 cmd := []string{"init"}
228 if bare {
229 cmd = append(cmd, "--bare")
230 }
Jack Neusabdbe192019-07-15 12:23:22 -0600231 _, err := RunGit(gitRepo, cmd)
232 return err
233}
Jack Neus0296c732019-07-17 09:35:01 -0600234
235// AddRemote adds a remote.
236func AddRemote(gitRepo, remote, remoteLocation string) error {
237 output, err := RunGit(gitRepo, []string{"remote", "add", remote, remoteLocation})
238 if err != nil {
Jack Neuseb25f722019-07-19 16:33:20 -0600239 if strings.Contains(output.Stderr, "already exists") {
240 return fmt.Errorf("remote already exists")
241 }
Jack Neus0296c732019-07-17 09:35:01 -0600242 }
243 return err
244}
245
246// Checkout checkouts a branch.
247func Checkout(gitRepo, branch string) error {
248 output, err := RunGit(gitRepo, []string{"checkout", branch})
249 if err != nil && strings.Contains(output.Stderr, "did not match any") {
250 return fmt.Errorf(output.Stderr)
251 }
252 return err
253}
254
255// DeleteBranch checks out to master and then deletes the current branch.
256func DeleteBranch(gitRepo, branch string, force bool) error {
257 cmd := []string{"branch"}
258 if force {
259 cmd = append(cmd, "-D")
260 } else {
261 cmd = append(cmd, "-d")
262 }
263 cmd = append(cmd, branch)
264 output, err := RunGit(gitRepo, cmd)
265
266 if err != nil {
267 if strings.Contains(output.Stderr, "checked out at") {
268 return fmt.Errorf(output.Stderr)
269 }
270 if strings.Contains(output.Stderr, "not fully merged") {
271 return fmt.Errorf("branch %s is not fully merged. use the force parameter if you wish to proceed", branch)
272 }
273 }
274 return err
275}
276
277// Clone clones the remote into the specified dir.
278func Clone(remote, dir string) error {
279 output, err := RunGit(filepath.Dir(dir), []string{"clone", remote, filepath.Base(dir)})
280 if err != nil {
281 return fmt.Errorf(output.Stderr)
282 }
283 return nil
284}