/
patch_util.go
352 lines (317 loc) · 10.4 KB
/
patch_util.go
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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
package operations
import (
"bytes"
"context"
"fmt"
"net/http"
"os/exec"
"strings"
"text/template"
"github.com/evergreen-ci/evergreen/model"
"github.com/evergreen-ci/evergreen/model/patch"
"github.com/evergreen-ci/evergreen/rest/client"
"github.com/evergreen-ci/evergreen/util"
"github.com/mongodb/grip"
"github.com/pkg/errors"
)
// Above this size, the user must explicitly use --large to submit the patch (or confirm)
const largePatchThreshold = 1024 * 1024 * 16
// This is the template used to render a patch's summary in a human-readable output format.
var patchDisplayTemplate = template.Must(template.New("patch").Parse(`
ID : {{.Patch.Id.Hex}}
Created : {{.Patch.CreateTime}}
Description : {{if .Patch.Description}}{{.Patch.Description}}{{else}}<none>{{end}}
Build : {{.Link}}
Finalized : {{if .Patch.Activated}}Yes{{else}}No{{end}}
{{if .ShowSummary}}
Summary :
{{range .Patch.Patches}}{{if not (eq .ModuleName "") }}Module:{{.ModuleName}}{{end}}
Base Commit : {{.Githash}}
{{range .PatchSet.Summary}}+{{.Additions}} -{{.Deletions}} {{.Name}}
{{end}}
{{end}}
{{end}}
`))
type localDiff struct {
fullPatch string
patchSummary string
log string
base string
}
type patchParams struct {
Project string
Variants []string
Tasks []string
Description string
Alias string
SkipConfirm bool
Finalize bool
Large bool
ShowSummary bool
}
type patchSubmission struct {
projectId string
patchData string
description string
base string
alias string
variants string
tasks []string
finalize bool
}
func (p *patchParams) createPatch(ac *legacyClient, conf *ClientSettings, diffData *localDiff) error {
if err := validatePatchSize(diffData, p.Large); err != nil {
return err
}
if !p.SkipConfirm && len(diffData.fullPatch) == 0 {
if !confirm("Patch submission is empty. Continue?(y/n)", true) {
return nil
}
} else if !p.SkipConfirm && diffData.patchSummary != "" {
grip.Info(diffData.patchSummary)
if diffData.log != "" {
grip.Info(diffData.log)
}
if !confirm("This is a summary of the patch to be submitted. Continue? (y/n):", true) {
return nil
}
}
variantsStr := strings.Join(p.Variants, ",")
patchSub := patchSubmission{
projectId: p.Project,
patchData: diffData.fullPatch,
description: p.Description,
base: diffData.base,
variants: variantsStr,
tasks: p.Tasks,
finalize: p.Finalize,
alias: p.Alias,
}
newPatch, err := ac.PutPatch(patchSub)
if err != nil {
return err
}
patchDisp, err := getPatchDisplay(newPatch, p.ShowSummary, conf.UIServerHost)
if err != nil {
return err
}
grip.Info("Patch successfully created.")
grip.Info(patchDisp)
return nil
}
// Performs validation for patch or patch-file
func (p *patchParams) validatePatchCommand(ctx context.Context, conf *ClientSettings, ac *legacyClient, comm client.Communicator) (ref *model.ProjectRef, err error) {
if p.Project == "" {
p.Project = conf.FindDefaultProject()
} else {
if conf.FindDefaultProject() == "" &&
!p.SkipConfirm && confirm(fmt.Sprintf("Make %v your default project?", p.Project), true) {
conf.SetDefaultProject(p.Project)
if err = conf.Write(""); err != nil {
grip.Warningf("warning - failed to set default project: %v\n", err)
}
}
}
if p.Project == "" {
err = errors.Errorf("Need to specify a project.")
return
}
if err = p.loadAlias(conf); err != nil {
grip.Warningf("warning - failed to set default alias: %v\n", err)
}
// Validate the alias if it exists
if p.Alias != "" {
validAlias := false
var aliases []model.ProjectAlias
aliases, err = comm.ListAliases(ctx, p.Project)
if err != nil {
err = errors.Wrap(err, "error contacting API server")
return
}
for _, alias := range aliases {
if alias.Alias == p.Alias {
validAlias = true
break
}
}
if !validAlias {
err = errors.Errorf("%s is not a valid alias", p.Alias)
return
}
}
ref, err = ac.GetProjectRef(p.Project)
if err != nil {
if apiErr, ok := err.(APIError); ok && apiErr.code == http.StatusNotFound {
err = errors.Errorf("%v \nRun `evergreen list --projects` to see all valid projects", err)
}
return
}
// update variants
if len(p.Variants) == 0 && p.Alias == "" {
p.Variants = conf.FindDefaultVariants(p.Project)
if len(p.Variants) == 0 && p.Finalize {
err = errors.Errorf("Need to specify at least one buildvariant with -v when finalizing." +
" Run with `-v all` to finalize against all variants.")
return
}
} else if p.Alias == "" {
defaultVariants := conf.FindDefaultVariants(p.Project)
if len(defaultVariants) == 0 && !p.SkipConfirm &&
confirm(fmt.Sprintf("Set %v as the default variants for project '%v'?",
p.Variants, p.Project), false) {
conf.SetDefaultVariants(p.Project, p.Variants...)
if err = conf.Write(""); err != nil {
grip.Warningf("warning - failed to set default variants: %v\n", err)
}
}
}
// update tasks
if len(p.Tasks) == 0 {
p.Tasks = conf.FindDefaultTasks(p.Project)
if len(p.Tasks) == 0 && p.Alias == "" && p.Finalize {
err = errors.Errorf("Need to specify at least one task or alias when finalizing." +
" Run with `-t all` to finalize against all tasks.")
return
}
} else if p.Alias == "" {
defaultTasks := conf.FindDefaultTasks(p.Project)
if len(defaultTasks) == 0 && !p.SkipConfirm &&
confirm(fmt.Sprintf("Set %v as the default tasks for project '%v'?",
p.Tasks, p.Project), false) {
conf.SetDefaultTasks(p.Project, p.Tasks...)
if err := conf.Write(""); err != nil {
grip.Warningf("warning - failed to set default tasks: %v\n", err)
}
}
}
if p.Description == "" && !p.SkipConfirm {
p.Description = prompt("Enter a description for this patch (optional):")
}
return
}
// Sets the patch's alias to either the passed in option or the default
func (p *patchParams) loadAlias(conf *ClientSettings) error {
// If somebody passed an --alias
if p.Alias != "" {
// Check if there's an alias as the default, and if not, ask to save the cl one
defaultAlias := conf.FindDefaultAlias(p.Project)
if defaultAlias == "" && !p.SkipConfirm &&
confirm(fmt.Sprintf("Set %v as the default alias for project '%v'?",
p.Alias, p.Project), false) {
conf.SetDefaultAlias(p.Project, p.Alias)
if err := conf.Write(""); err != nil {
return err
}
}
} else {
// No --alias was passed, use the default
p.Alias = conf.FindDefaultAlias(p.Project)
}
return nil
}
// Returns an error if the diff is greater than the system limit, or if it's above the large
// patch threhsold and allowLarge is not set.
func validatePatchSize(diff *localDiff, allowLarge bool) error {
patchLen := len(diff.fullPatch)
if patchLen > patch.SizeLimit {
return errors.Errorf("Patch is greater than the system limit (%v > %v bytes).", patchLen, patch.SizeLimit)
} else if patchLen > largePatchThreshold && !allowLarge {
return errors.Errorf("Patch is larger than the default threshold (%v > %v bytes).\n"+
"To allow submitting this patch, use the --large flag.", patchLen, largePatchThreshold)
}
// Patch is small enough and/or allowLarge is true, so no error
return nil
}
// getPatchDisplay returns a human-readable summary representation of a patch object
// which can be written to the terminal.
func getPatchDisplay(p *patch.Patch, summarize bool, uiHost string) (string, error) {
var out bytes.Buffer
var url string
if p.Activated {
url = uiHost + "/version/" + p.Id.Hex()
} else {
url = uiHost + "/patch/" + p.Id.Hex()
}
err := patchDisplayTemplate.Execute(&out, struct {
Patch *patch.Patch
ShowSummary bool
Link string
}{
Patch: p,
ShowSummary: summarize,
Link: url,
})
if err != nil {
return "", err
}
return out.String(), nil
}
// loadGitData inspects the current git working directory and returns a patch and its summary.
// The branch argument is used to determine where to generate the merge base from, and any extra
// arguments supplied are passed directly in as additional args to git diff.
func loadGitData(branch string, extraArgs ...string) (*localDiff, error) {
// branch@{upstream} refers to the branch that the branch specified by branchname is set to
// build on top of. This allows automatically detecting a branch based on the correct remote,
// if the user's repo is a fork, for example.
// For details see: https://git-scm.com/docs/gitrevisions
mergeBase, err := gitMergeBase(branch+"@{upstream}", "HEAD")
if err != nil {
return nil, errors.Errorf("Error getting merge base: %v", err)
}
statArgs := []string{"--stat"}
if len(extraArgs) > 0 {
statArgs = append(statArgs, extraArgs...)
}
stat, err := gitDiff(mergeBase, statArgs...)
if err != nil {
return nil, errors.Errorf("Error getting diff summary: %v", err)
}
log, err := gitLog(mergeBase)
if err != nil {
return nil, errors.Errorf("git log: %v", err)
}
if !util.StringSliceContains(extraArgs, "--binary") {
extraArgs = append(extraArgs, "--binary")
}
patch, err := gitDiff(mergeBase, extraArgs...)
if err != nil {
return nil, errors.Errorf("Error getting patch: %v", err)
}
return &localDiff{patch, stat, log, mergeBase}, nil
}
// gitMergeBase runs "git merge-base <branch1> <branch2>" and returns the
// resulting githash as string
func gitMergeBase(branch1, branch2 string) (string, error) {
cmd := exec.Command("git", "merge-base", branch1, branch2)
out, err := cmd.CombinedOutput()
if err != nil {
return "", errors.Wrapf(err, "'git merge-base %s %s' failed: %s (%s)", branch1, branch2, out, err)
}
return strings.TrimSpace(string(out)), err
}
// gitDiff runs "git diff <base> <diffargs ...>" and returns the output of the command as a string
func gitDiff(base string, diffArgs ...string) (string, error) {
args := append([]string{
"--no-ext-diff",
}, diffArgs...)
return gitCmd("diff", base, args...)
}
// getLog runs "git log <base>
func gitLog(base string, logArgs ...string) (string, error) {
args := append(logArgs, "--oneline")
return gitCmd("log", fmt.Sprintf("...%v", base), args...)
}
func gitCmd(cmdName, base string, gitArgs ...string) (string, error) {
args := make([]string, 0, 1+len(gitArgs))
args = append(args, cmdName)
if base != "" {
args = append(args, base)
}
args = append(args, gitArgs...)
cmd := exec.Command("git", args...)
out, err := cmd.CombinedOutput()
if err != nil {
return "", errors.Errorf("'git %v %v' failed with err %v", base, strings.Join(args, " "), err)
}
return string(out), err
}