blob: 0306f33ad04cdb635eb8624bcd24ff1f58eebcea [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) {
55 // Regex should be case insensitive.
56 if !strings.HasPrefix(pattern.String(), "(?i)") {
57 pattern = regexp.MustCompile("(?i)" + pattern.String())
58 }
59
60 output, err := RunGit(gitRepo, []string{"ls-remote", gitRepo})
61 if err != nil {
62 // Could not read branches.
63 return []string{}, fmt.Errorf("git error: %s\nstderr: %s", err.Error(), output.Stderr)
64 }
65 // Find all branches that match the pattern.
66 branches := strings.Split(output.Stdout, "\n")
67 matchedBranches := []string{}
68 for _, branch := range branches {
69 branch = strings.TrimSpace(branch)
70 if branch == "" {
71 continue
72 }
73 branch = strings.Fields(branch)[1]
74 if pattern.Match([]byte(branch)) {
75 matchedBranches = append(matchedBranches, branch)
76 }
77 }
78 return matchedBranches, nil
79}
80
81// GetGitRepoRevision finds and returns the revision of a branch.
82func GetGitRepoRevision(cwd string) (string, error) {
83 output, err := RunGit(cwd, []string{"rev-parse", "HEAD"})
84 return strings.TrimSpace(output.Stdout), err
85}
86
87// StipRefsHead removes leading 'refs/heads/' from a ref name.
88func StripRefsHead(ref string) string {
89 return strings.TrimPrefix(ref, "refs/heads/")
90}
91
92// NormalizeRef converts git branch refs into fully qualified form.
93func NormalizeRef(ref string) string {
94 if ref == "" || strings.HasPrefix(ref, "refs/") {
95 return ref
96 }
97 return fmt.Sprintf("refs/heads/%s", ref)
98}
99
100// StripRefs removes leading 'refs/heads/', 'refs/remotes/[^/]+/' from a ref name.
101func StripRefs(ref string) string {
102 ref = StripRefsHead(ref)
103 // If the ref starts with ref/remotes/, then we want the part of the string
104 // that comes after the third "/".
105 // Example: refs/remotes/origin/master --> master
106 // Example: refs/remotse/origin/foo/bar --> foo/bar
107 if strings.HasPrefix(ref, "refs/remotes/") {
108 refParts := strings.SplitN(ref, "/", 4)
109 return refParts[len(refParts)-1]
110 }
111 return ref
112}
Jack Neusabdbe192019-07-15 12:23:22 -0600113
114// CreateBranch creates a branch.
115func CreateBranch(gitRepo, branch string) error {
Jack Neus0296c732019-07-17 09:35:01 -0600116 output, err := RunGit(gitRepo, []string{"checkout", "-B", branch})
117 if err != nil && strings.Contains(output.Stderr, "not a valid branch name") {
118 return fmt.Errorf("%s is not a valid branch name", branch)
119 }
120 return err
121}
122
123// CreateTrackingBranch creates a tracking branch.
124func CreateTrackingBranch(gitRepo, branch string, remoteRef RemoteRef) error {
125 refspec := fmt.Sprintf("%s/%s", remoteRef.Remote, remoteRef.Ref)
126 output, err := RunGit(gitRepo, []string{"fetch", remoteRef.Remote, remoteRef.Ref})
127 if err != nil {
128 return fmt.Errorf("could not fetch %s: %s", refspec, output.Stderr)
129 }
130 output, err = RunGit(gitRepo, []string{"checkout", "-b", branch, "-t", refspec})
131 if err != nil {
132 if strings.Contains(output.Stderr, "not a valid branch name") {
133 return fmt.Errorf("%s is not a valid branch name", branch)
134 } else {
135 return fmt.Errorf(output.Stderr)
136 }
137 }
Jack Neusabdbe192019-07-15 12:23:22 -0600138 return err
139}
140
141// CommitAll adds all local changes and commits them.
142func CommitAll(gitRepo, commitMsg string) error {
Jack Neus0296c732019-07-17 09:35:01 -0600143 if output, err := RunGit(gitRepo, []string{"add", "-A"}); err != nil {
144 return fmt.Errorf(output.Stderr)
Jack Neusabdbe192019-07-15 12:23:22 -0600145 }
Jack Neus0296c732019-07-17 09:35:01 -0600146 if output, err := RunGit(gitRepo, []string{"commit", "-m", commitMsg}); err != nil {
147 if strings.Contains(output.Stdout, "nothing to commit") {
148 return fmt.Errorf(output.Stdout)
149 } else {
150 return fmt.Errorf(output.Stderr)
151 }
Jack Neusabdbe192019-07-15 12:23:22 -0600152 }
153 return nil
154}
155
156// PushGitChanges stages and commits any local changes before pushing the commit
157// to the specified remote ref.
158func PushChanges(gitRepo, localRef, commitMsg string, dryRun bool, pushTo RemoteRef) error {
Jack Neus0296c732019-07-17 09:35:01 -0600159 err := CommitAll(gitRepo, commitMsg)
160 // It's ok if there's nothing to commit, we can still try to push.
161 if err != nil && !strings.Contains(err.Error(), "nothing to commit") {
Jack Neusabdbe192019-07-15 12:23:22 -0600162 return err
163 }
164 ref := fmt.Sprintf("%s:%s", localRef, pushTo.Ref)
165 cmd := []string{"push", pushTo.Remote, ref}
166 if dryRun {
167 cmd = append(cmd, "--dry-run")
168 }
Jack Neus0296c732019-07-17 09:35:01 -0600169 _, err = RunGit(gitRepo, cmd)
170 return err
171}
172
173// Init initializes a repo.
174func Init(gitRepo string, bare bool) error {
175 cmd := []string{"init"}
176 if bare {
177 cmd = append(cmd, "--bare")
178 }
Jack Neusabdbe192019-07-15 12:23:22 -0600179 _, err := RunGit(gitRepo, cmd)
180 return err
181}
Jack Neus0296c732019-07-17 09:35:01 -0600182
183// AddRemote adds a remote.
184func AddRemote(gitRepo, remote, remoteLocation string) error {
185 output, err := RunGit(gitRepo, []string{"remote", "add", remote, remoteLocation})
186 if err != nil {
187 fmt.Printf("remote error: %s\n", output.Stderr)
188 }
189 return err
190}
191
192// Checkout checkouts a branch.
193func Checkout(gitRepo, branch string) error {
194 output, err := RunGit(gitRepo, []string{"checkout", branch})
195 if err != nil && strings.Contains(output.Stderr, "did not match any") {
196 return fmt.Errorf(output.Stderr)
197 }
198 return err
199}
200
201// DeleteBranch checks out to master and then deletes the current branch.
202func DeleteBranch(gitRepo, branch string, force bool) error {
203 cmd := []string{"branch"}
204 if force {
205 cmd = append(cmd, "-D")
206 } else {
207 cmd = append(cmd, "-d")
208 }
209 cmd = append(cmd, branch)
210 output, err := RunGit(gitRepo, cmd)
211
212 if err != nil {
213 if strings.Contains(output.Stderr, "checked out at") {
214 return fmt.Errorf(output.Stderr)
215 }
216 if strings.Contains(output.Stderr, "not fully merged") {
217 return fmt.Errorf("branch %s is not fully merged. use the force parameter if you wish to proceed", branch)
218 }
219 }
220 return err
221}
222
223// Clone clones the remote into the specified dir.
224func Clone(remote, dir string) error {
225 output, err := RunGit(filepath.Dir(dir), []string{"clone", remote, filepath.Base(dir)})
226 if err != nil {
227 return fmt.Errorf(output.Stderr)
228 }
229 return nil
230}