-
Notifications
You must be signed in to change notification settings - Fork 44
Expand file tree
/
Copy pathgit.go
More file actions
222 lines (190 loc) · 5.99 KB
/
git.go
File metadata and controls
222 lines (190 loc) · 5.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
package gitclone
import (
"fmt"
"path/filepath"
"slices"
"strings"
"github.com/bitrise-io/go-utils/command/git"
"github.com/bitrise-io/go-utils/pathutil"
)
const (
checkoutFailedTag = "checkout_failed"
fetchFailedTag = "fetch_failed"
jobsFlag = "--jobs=10"
)
var runner CommandRunner = &DefaultRunner{}
func isOriginPresent(gitCmd git.Git, dir, repoURL string) (bool, error) {
absDir, err := pathutil.AbsPath(dir)
if err != nil {
return false, err
}
gitDir := filepath.Join(absDir, ".git")
if exist, err := pathutil.IsDirExists(gitDir); err != nil {
return false, err
} else if exist {
remotes, err := runner.RunForOutput(gitCmd.RemoteList())
if err != nil {
return false, err
}
if !strings.Contains(remotes, repoURL) {
return false, fmt.Errorf(".git folder exists in the directory (%s), but using a different remote", dir)
}
return true, nil
}
return false, nil
}
func resetRepo(gitCmd git.Git) error {
if err := runner.Run(gitCmd.Reset("--hard", "HEAD")); err != nil {
return err
}
if err := runner.Run(gitCmd.Clean("-x", "-d", "-f")); err != nil {
return err
}
if err := runner.Run(gitCmd.SubmoduleForeach(gitCmd.Reset("--hard", "HEAD"))); err != nil {
return err
}
return runner.Run(gitCmd.SubmoduleForeach(gitCmd.Clean("-x", "-d", "-f")))
}
func isFork(repoURL, prRepoURL string) bool {
return prRepoURL != "" && getRepo(repoURL) != getRepo(prRepoURL)
}
// formats:
// https://hostname/owner/repository.git
// git@hostname:owner/repository.git
// ssh://git@hostname:port/owner/repository.git
func getRepo(url string) string {
var host, repo string
switch {
case strings.HasPrefix(url, "https://"):
url = strings.TrimPrefix(url, "https://")
idx := strings.Index(url, "/")
host, repo = url[:idx], url[idx+1:]
case strings.HasPrefix(url, "git@"):
url = url[strings.Index(url, "@")+1:]
idx := strings.Index(url, ":")
host, repo = url[:idx], url[idx+1:]
case strings.HasPrefix(url, "ssh://"):
url = url[strings.Index(url, "@")+1:]
if strings.Contains(url, ":") {
idxColon, idxSlash := strings.Index(url, ":"), strings.Index(url, "/")
host, repo = url[:idxColon], url[idxSlash+1:]
} else {
idx := strings.Index(url, "/")
host, repo = url[:idx], url[idx+1:]
}
}
return host + "/" + strings.TrimSuffix(repo, ".git")
}
func isPrivate(repoURL string) bool {
return strings.HasPrefix(repoURL, "git")
}
type getAvailableBranches func() (map[string][]string, error)
func listBranches(gitCmd git.Git) getAvailableBranches {
return func() (map[string][]string, error) {
remoteList, err := runner.RunForOutput(gitCmd.RemoteList())
if err != nil {
return nil, err
}
remoteBranches, err := runner.RunForOutput(gitCmd.RemoteBranches())
if err != nil {
return nil, err
}
return parseListBranchesOutput(remoteList, remoteBranches), nil
}
}
func parseListBranchesOutput(remotes, branches string) map[string][]string {
parsedRemotes := extractRemotes(remotes)
branchesByRemote := extractBranches(parsedRemotes, branches)
return branchesByRemote
}
// extractRemotes parses the output of `git remote -v` and returns a map of remote URLs to their names
// Example of what such an output looks like:
//
// origin git_url1 (fetch)
// origin git_url1 (push)
// upstream git_url2 (fetch)
// upstream git_url2 (push)
//
// (Name of the remote, \t, URL, space, (fetch|push), \n)
func extractRemotes(remotes string) map[string]string {
parsedRemotes := make(map[string]string)
for _, line := range strings.Split(remotes, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
split := strings.Split(line, "\t")
if len(split) < 2 {
continue
}
name := split[0]
split = strings.Split(split[1], " ")
url := split[0]
parsedRemotes[url] = name
}
return parsedRemotes
}
// extractBranches parses the output of `git ls-remote -b` and returns a map of remote names to their branches
// Example of what such an output looks like:
//
// From git_url
// a50bdc5182e3e7c292f7c8c881a1a0d9476c8eda refs/heads/A
// 25c9d97e5c9e7f4c9ad25597f8e1265af07869d7 refs/heads/B
// 5b3dfe10c52cf5c762740ea5cfa619d82d70e205 refs/heads/C
// f6cdf8f2132f22516bb4d71e326e66a45850eb04 refs/heads/D
// b30b826ac9594330d77554f30103492409035aee refs/heads/master
//
// (From, space, URL, \n)
// (SHA, \t, refs/heads/, branch name, \n)
func extractBranches(remotes map[string]string, branches string) map[string][]string {
branchesByRemote := make(map[string][]string)
currentRemote := ""
for _, line := range strings.Split(branches, "\n") {
if strings.HasPrefix(line, "From ") {
repoURL := strings.TrimPrefix(line, "From ")
currentRemote = remotes[repoURL]
continue
}
split := strings.Split(line, "\t")
if len(split) < 2 || currentRemote == "" {
continue
}
branch := strings.TrimPrefix(split[1], refsHeadsPrefix)
branches := branchesByRemote[currentRemote]
branches = append(branches, branch)
branchesByRemote[currentRemote] = branches
}
return branchesByRemote
}
func handleCheckoutError(callback getAvailableBranches, tag string, err error, shortMsg string, branch string) error {
// We were checking out a branch (not tag or commit)
if branch != "" {
branchesByRemote, branchesErr := callback()
branches := branchesByRemote[originRemoteName]
// There was no error grabbing the available branches
// And the current branch is not present in the list
if branchesErr == nil && !slices.Contains(branches, branch) {
return newStepErrorWithBranchRecommendations(
tag,
err,
shortMsg,
branch,
branches,
)
}
}
return newStepError(
tag,
err,
shortMsg,
)
}
func isWorkingTreeClean(gitCmd git.Git) (bool, error) {
// Despite the flag name, `--porcelain` is the plumbing format to use in scripts:
// https://git-scm.com/docs/git-status#Documentation/git-status.txt---porcelainltversiongt
out, err := gitCmd.Status("--porcelain").RunAndReturnTrimmedOutput()
if err != nil {
return false, fmt.Errorf("git status check: %s", err)
}
return strings.TrimSpace(out) == "", nil
}