blob: be5e613e9772add2e786debfaae62c6804d54aac [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 Neusfc3b5772019-07-03 11:18:42 -060010 "regexp"
11 "strings"
Jack Neus07511722019-07-12 15:41:10 -060012
13 "go.chromium.org/chromiumos/infra/go/internal/cmd"
Jack Neusfc3b5772019-07-03 11:18:42 -060014)
15
16var (
Jack Neus07511722019-07-12 15:41:10 -060017 CommandRunnerImpl cmd.CommandRunner = cmd.RealCommandRunner{}
Jack Neusfc3b5772019-07-03 11:18:42 -060018)
19
Jack Neusfc3b5772019-07-03 11:18:42 -060020type CommandOutput struct {
21 Stdout string
22 Stderr string
23}
24
25// Struct representing a remote ref.
26type RemoteRef struct {
27 Remote string
28 Ref string
29}
30
31// RunGit the specified git command in the specified repo. It returns
32// stdout and stderr.
33func RunGit(gitRepo string, cmd []string) (CommandOutput, error) {
34 ctx := context.Background()
35 var stdoutBuf, stderrBuf bytes.Buffer
Jack Neus07511722019-07-12 15:41:10 -060036 err := CommandRunnerImpl.RunCommand(ctx, &stdoutBuf, &stderrBuf, gitRepo, "git", cmd...)
Jack Neusfc3b5772019-07-03 11:18:42 -060037 cmdOutput := CommandOutput{stdoutBuf.String(), stderrBuf.String()}
38 return cmdOutput, err
39}
40
41// GetCurrentBranch returns current branch of a repo, and an empty string
42// if repo is on detached HEAD.
43func GetCurrentBranch(cwd string) string {
44 output, err := RunGit(cwd, []string{"symbolic-ref", "-q", "HEAD"})
45 if err != nil {
46 return ""
47 }
48 return StripRefsHead(strings.TrimSpace(output.Stdout))
49}
50
51// MatchBranchName returns the names of branches who match the specified
52// regular expression.
53func MatchBranchName(gitRepo string, pattern *regexp.Regexp) ([]string, error) {
54 // Regex should be case insensitive.
55 if !strings.HasPrefix(pattern.String(), "(?i)") {
56 pattern = regexp.MustCompile("(?i)" + pattern.String())
57 }
58
59 output, err := RunGit(gitRepo, []string{"ls-remote", gitRepo})
60 if err != nil {
61 // Could not read branches.
62 return []string{}, fmt.Errorf("git error: %s\nstderr: %s", err.Error(), output.Stderr)
63 }
64 // Find all branches that match the pattern.
65 branches := strings.Split(output.Stdout, "\n")
66 matchedBranches := []string{}
67 for _, branch := range branches {
68 branch = strings.TrimSpace(branch)
69 if branch == "" {
70 continue
71 }
72 branch = strings.Fields(branch)[1]
73 if pattern.Match([]byte(branch)) {
74 matchedBranches = append(matchedBranches, branch)
75 }
76 }
77 return matchedBranches, nil
78}
79
80// GetGitRepoRevision finds and returns the revision of a branch.
81func GetGitRepoRevision(cwd string) (string, error) {
82 output, err := RunGit(cwd, []string{"rev-parse", "HEAD"})
83 return strings.TrimSpace(output.Stdout), err
84}
85
86// StipRefsHead removes leading 'refs/heads/' from a ref name.
87func StripRefsHead(ref string) string {
88 return strings.TrimPrefix(ref, "refs/heads/")
89}
90
91// NormalizeRef converts git branch refs into fully qualified form.
92func NormalizeRef(ref string) string {
93 if ref == "" || strings.HasPrefix(ref, "refs/") {
94 return ref
95 }
96 return fmt.Sprintf("refs/heads/%s", ref)
97}
98
99// StripRefs removes leading 'refs/heads/', 'refs/remotes/[^/]+/' from a ref name.
100func StripRefs(ref string) string {
101 ref = StripRefsHead(ref)
102 // If the ref starts with ref/remotes/, then we want the part of the string
103 // that comes after the third "/".
104 // Example: refs/remotes/origin/master --> master
105 // Example: refs/remotse/origin/foo/bar --> foo/bar
106 if strings.HasPrefix(ref, "refs/remotes/") {
107 refParts := strings.SplitN(ref, "/", 4)
108 return refParts[len(refParts)-1]
109 }
110 return ref
111}
Jack Neusabdbe192019-07-15 12:23:22 -0600112
113// CreateBranch creates a branch.
114func CreateBranch(gitRepo, branch string) error {
115 _, err := RunGit(gitRepo, []string{"checkout", "-B", branch, "HEAD"})
116 return err
117}
118
119// CommitAll adds all local changes and commits them.
120func CommitAll(gitRepo, commitMsg string) error {
121 if _, err := RunGit(gitRepo, []string{"add", "-A"}); err != nil {
122 return err
123 }
124 if _, err := RunGit(gitRepo, []string{"commit", "-m", commitMsg}); err != nil {
125 return err
126 }
127 return nil
128}
129
130// PushGitChanges stages and commits any local changes before pushing the commit
131// to the specified remote ref.
132func PushChanges(gitRepo, localRef, commitMsg string, dryRun bool, pushTo RemoteRef) error {
133 if err := CommitAll(gitRepo, commitMsg); err != nil {
134 return err
135 }
136 ref := fmt.Sprintf("%s:%s", localRef, pushTo.Ref)
137 cmd := []string{"push", pushTo.Remote, ref}
138 if dryRun {
139 cmd = append(cmd, "--dry-run")
140 }
141 _, err := RunGit(gitRepo, cmd)
142 return err
143}