github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/cli/patch.go (about) 1 package cli 2 3 import ( 4 "bytes" 5 "fmt" 6 "io/ioutil" 7 "net/http" 8 "os" 9 "os/exec" 10 "sort" 11 "strings" 12 "text/tabwriter" 13 "text/template" 14 "time" 15 16 "github.com/evergreen-ci/evergreen/model" 17 "github.com/evergreen-ci/evergreen/model/patch" 18 "github.com/evergreen-ci/evergreen/model/version" 19 "github.com/evergreen-ci/evergreen/validator" 20 "github.com/mongodb/grip" 21 "github.com/pkg/errors" 22 ) 23 24 var noProjectError = errors.New("must specify a project with -p/--project or a path to a config file with -f/--file") 25 26 // Above this size, the user must explicitly use --large to submit the patch (or confirm) 27 const largePatchThreshold = 1024 * 1024 * 16 28 29 // This is the template used to render a patch's summary in a human-readable output format. 30 var patchDisplayTemplate = template.Must(template.New("patch").Parse(` 31 ID : {{.Patch.Id.Hex}} 32 Created : {{.Now.Sub .Patch.CreateTime}} ago 33 Description : {{if .Patch.Description}}{{.Patch.Description}}{{else}}<none>{{end}} 34 Link : {{.Link}} 35 Finalized : {{if .Patch.Activated}}Yes{{else}}No{{end}} 36 {{if .ShowSummary}} 37 Summary : 38 {{range .Patch.Patches}}{{if not (eq .ModuleName "") }}Module:{{.ModuleName}}{{end}} 39 Base Commit : {{.Githash}} 40 {{range .PatchSet.Summary}}+{{.Additions}} -{{.Deletions}} {{.Name}} 41 {{end}} 42 {{end}} 43 {{end}} 44 `)) 45 46 // lastGreenTemplate helps return readable information for the last-green command. 47 var lastGreenTemplate = template.Must(template.New("last_green").Parse(` 48 Revision : {{.Version.Revision}} 49 Message : {{.Version.Message}} 50 Link : {{.UIURL}}/version/{{.Version.Id}} 51 52 `)) 53 54 var defaultPatchesReturned = 5 55 56 type localDiff struct { 57 fullPatch string 58 patchSummary string 59 log string 60 base string 61 } 62 63 type patchSubmission struct { 64 projectId string 65 patchData string 66 description string 67 base string 68 variants string 69 tasks []string 70 finalize bool 71 } 72 73 // ListPatchesCommand is used to list a user's existing patches. 74 type ListPatchesCommand struct { 75 GlobalOpts *Options `no-flag:"true"` 76 Variants []string `short:"v" long:"variants" description:"variants to run the patch on. may be specified multiple times, or use the value 'all'"` 77 PatchId string `short:"i" description:"show details for only the patch with this ID"` 78 ShowSummary bool `short:"s" long:"show-summary" description:"show a summary of the diff for each patch"` 79 Number *int `short:"n" long:"number" description:"number of patches to show (0 for all patches)"` 80 } 81 82 type ListCommand struct { 83 GlobalOpts *Options `no-flag:"true"` 84 Project string `short:"p" long:"project" description:"project whose variants or tasks should be listed (use with --variants/--tasks)"` 85 File string `short:"f" long:"file" description:"path to config file whose variants or tasks should be listed (use with --variants/--tasks)"` 86 Projects bool `long:"projects" description:"list all available projects"` 87 Variants bool `long:"variants" description:"list all variants for a project"` 88 Tasks bool `long:"tasks" description:"list all tasks for a project"` 89 } 90 91 // ValidateCommand is used to verify that a config file is valid. 92 type ValidateCommand struct { 93 GlobalOpts *Options `no-flag:"true"` 94 Positional struct { 95 FileName string `positional-arg-name:"filename" description:"path to an evergreen project file"` 96 } `positional-args:"1" required:"yes"` 97 } 98 99 // CancelPatchCommand is used to cancel a patch. 100 type CancelPatchCommand struct { 101 GlobalOpts *Options `no-flag:"true"` 102 PatchId string `short:"i" description:"id of the patch to modify" required:"true"` 103 } 104 105 // FinalizePatchCommand is used to finalize a patch, allowing it to be scheduled. 106 type FinalizePatchCommand struct { 107 GlobalOpts *Options `no-flag:"true"` 108 PatchId string `short:"i" description:"id of the patch to modify" required:"true"` 109 } 110 111 // PatchCommand is used to submit a new patch to the API server. 112 type PatchCommand struct { 113 PatchCommandParams 114 } 115 116 // PatchFileCommand is used to submit a new patch to the API server using a diff file. 117 type PatchFileCommand struct { 118 PatchCommandParams 119 DiffFile string `long:"diff-file" description:"file containing the diff for the patch"` 120 Base string `short:"b" long:"base" description:"githash of base"` 121 } 122 123 // PatchCommandParams contains parameters common to PatchCommand and PatchFileCommand 124 type PatchCommandParams struct { 125 GlobalOpts *Options `no-flag:"true"` 126 Project string `short:"p" long:"project" description:"project to submit patch for"` 127 Variants []string `short:"v" long:"variants"` 128 Tasks []string `short:"t" long:"tasks"` 129 SkipConfirm bool `short:"y" long:"yes" description:"skip confirmation text"` 130 Description string `short:"d" long:"description" description:"description of patch (optional)"` 131 Finalize bool `short:"f" long:"finalize" description:"schedule tasks immediately"` 132 Large bool `long:"large" description:"enable submitting larger patches (>16MB)"` 133 } 134 135 // LastGreenCommand contains parameters for the finding a project's most recent passing version. 136 type LastGreenCommand struct { 137 GlobalOpts *Options `no-flag:"true"` 138 Project string `short:"p" long:"project" description:"project to search" required:"true"` 139 Variants []string `short:"v" long:"variants" description:"variant that must be passing" required:"true"` 140 } 141 142 // SetModuleCommand adds or updates a module in an existing patch. 143 type SetModuleCommand struct { 144 GlobalOpts *Options `no-flag:"true"` 145 Module string `short:"m" long:"module" description:"name of the module to set patch for"` 146 PatchId string `short:"i" description:"id of the patch to modify" required:"true" ` 147 Project string `short:"p" long:"project" description:"project name"` 148 SkipConfirm bool `short:"y" long:"yes" description:"skip confirmation text"` 149 Large bool `long:"large" description:"enable submitting larger patches (>16MB)"` 150 } 151 152 // RemoveModuleCommand removes module information from an existing patch. 153 type RemoveModuleCommand struct { 154 GlobalOpts *Options `no-flag:"true"` 155 Module string `short:"m" long:"module" description:"name of the module to remove from patch" required:"true" ` 156 PatchId string `short:"i" description:"name of the module to remove from patch" required:"true" ` 157 } 158 159 func (lpc *ListPatchesCommand) Execute(_ []string) error { 160 ac, _, settings, err := getAPIClients(lpc.GlobalOpts) 161 if err != nil { 162 return err 163 } 164 notifyUserUpdate(ac) 165 if lpc.Number == nil { 166 lpc.Number = &defaultPatchesReturned 167 } 168 patches, err := ac.GetPatches(*lpc.Number) 169 if err != nil { 170 return err 171 } 172 for _, p := range patches { 173 disp, err := getPatchDisplay(&p, lpc.ShowSummary, settings.UIServerHost) 174 if err != nil { 175 return err 176 } 177 fmt.Println(disp) 178 } 179 return nil 180 } 181 182 // getPatchDisplay returns a human-readable summary representation of a patch object 183 // which can be written to the terminal. 184 func getPatchDisplay(p *patch.Patch, summarize bool, uiHost string) (string, error) { 185 var out bytes.Buffer 186 187 err := patchDisplayTemplate.Execute(&out, struct { 188 Patch *patch.Patch 189 ShowSummary bool 190 Link string 191 Now time.Time 192 }{p, summarize, uiHost + "/patch/" + p.Id.Hex(), time.Now()}) 193 if err != nil { 194 return "", err 195 } 196 return out.String(), nil 197 } 198 199 func (rmc *RemoveModuleCommand) Execute(_ []string) error { 200 ac, _, _, err := getAPIClients(rmc.GlobalOpts) 201 if err != nil { 202 return err 203 } 204 notifyUserUpdate(ac) 205 206 err = ac.DeletePatchModule(rmc.PatchId, rmc.Module) 207 if err != nil { 208 return err 209 } 210 fmt.Println("Module removed.") 211 return nil 212 } 213 214 func (vc *ValidateCommand) Execute(_ []string) error { 215 if vc.Positional.FileName == "" { 216 return errors.New("must supply path to a file to validate.") 217 } 218 219 ac, _, _, err := getAPIClients(vc.GlobalOpts) 220 if err != nil { 221 return err 222 } 223 notifyUserUpdate(ac) 224 225 confFile, err := ioutil.ReadFile(vc.Positional.FileName) 226 if err != nil { 227 return err 228 } 229 projErrors, err := ac.ValidateLocalConfig(confFile) 230 if err != nil { 231 return nil 232 } 233 numErrors, numWarnings := 0, 0 234 if len(projErrors) > 0 { 235 for i, e := range projErrors { 236 if e.Level == validator.Warning { 237 numWarnings++ 238 } else if e.Level == validator.Error { 239 numErrors++ 240 } 241 fmt.Printf("%v) %v: %v\n\n", i+1, e.Level, e.Message) 242 } 243 244 return errors.Errorf("Project file has %d warnings, %d errors.", numWarnings, numErrors) 245 } 246 fmt.Println("Valid!") 247 return nil 248 } 249 250 // getModuleBranch returns the branch for the config. 251 func getModuleBranch(moduleName string, proj *model.Project) (string, error) { 252 // find the module of the patch 253 for _, module := range proj.Modules { 254 if module.Name == moduleName { 255 return module.Branch, nil 256 } 257 } 258 return "", errors.Errorf("module '%s' unknown or not found", moduleName) 259 } 260 261 func (smc *SetModuleCommand) Execute(args []string) error { 262 ac, rc, _, err := getAPIClients(smc.GlobalOpts) 263 if err != nil { 264 return err 265 } 266 notifyUserUpdate(ac) 267 268 proj, err := rc.GetPatchedConfig(smc.PatchId) 269 if err != nil { 270 return err 271 } 272 273 moduleBranch, err := getModuleBranch(smc.Module, proj) 274 if err != nil { 275 grip.Error(err) 276 mods, merr := ac.GetPatchModules(smc.PatchId, proj.Identifier) 277 if merr != nil { 278 return errors.Wrap(merr, "errors fetching list of available modules") 279 } 280 281 if len(mods) != 0 { 282 grip.Noticef("known modules includes:\n\t%s", strings.Join(mods, "\n\t")) 283 } 284 285 return errors.Errorf("could not set specified module: \"%s\"", smc.Module) 286 } 287 288 // diff against the module branch. 289 diffData, err := loadGitData(moduleBranch, args...) 290 if err != nil { 291 return err 292 } 293 if err = validatePatchSize(diffData, smc.Large); err != nil { 294 return err 295 } 296 297 if !smc.SkipConfirm { 298 fmt.Printf("Using branch %v for module %v \n", moduleBranch, smc.Module) 299 if diffData.patchSummary != "" { 300 fmt.Println(diffData.patchSummary) 301 } 302 303 if !confirm("This is a summary of the patch to be submitted. Continue? (y/n):", true) { 304 return nil 305 } 306 } 307 308 err = ac.UpdatePatchModule(smc.PatchId, smc.Module, diffData.fullPatch, diffData.base) 309 if err != nil { 310 mods, err := ac.GetPatchModules(smc.PatchId, smc.Project) 311 var msg string 312 if err != nil { 313 msg = fmt.Sprintf("could not find module named %s or retrieve list of modules", 314 smc.Module) 315 } else if len(mods) == 0 { 316 msg = fmt.Sprintf("could not find modules for this project. %s is not a module. "+ 317 "see the evergreen configuration file for module configuration.", 318 smc.Module) 319 } else { 320 msg = fmt.Sprintf("could not find module named '%s', select correct module from:\n\t%s", 321 smc.Module, strings.Join(mods, "\n\t")) 322 } 323 grip.Error(msg) 324 return err 325 326 } 327 fmt.Println("Module updated.") 328 return nil 329 } 330 331 func (pc *PatchCommand) Execute(args []string) error { 332 ac, settings, ref, err := validatePatchCommand(&pc.PatchCommandParams) 333 if err != nil { 334 return err 335 } 336 337 diffData, err := loadGitData(ref.Branch, args...) 338 if err != nil { 339 return err 340 } 341 342 return createPatch(pc.PatchCommandParams, ac, settings, diffData) 343 } 344 345 func (pfc *PatchFileCommand) Execute(_ []string) error { 346 ac, settings, _, err := validatePatchCommand(&pfc.PatchCommandParams) 347 if err != nil { 348 return err 349 } 350 351 fullPatch, err := ioutil.ReadFile(pfc.DiffFile) 352 if err != nil { 353 return errors.Errorf("Error reading diff file: %v", err) 354 } 355 diffData := &localDiff{string(fullPatch), "", "", pfc.Base} 356 357 return createPatch(pfc.PatchCommandParams, ac, settings, diffData) 358 } 359 360 func (cpc *CancelPatchCommand) Execute(_ []string) error { 361 ac, _, _, err := getAPIClients(cpc.GlobalOpts) 362 if err != nil { 363 return err 364 } 365 notifyUserUpdate(ac) 366 367 err = ac.CancelPatch(cpc.PatchId) 368 if err != nil { 369 return err 370 } 371 fmt.Println("Patch canceled.") 372 return nil 373 } 374 375 func (fpc *FinalizePatchCommand) Execute(_ []string) error { 376 ac, _, _, err := getAPIClients(fpc.GlobalOpts) 377 if err != nil { 378 return err 379 } 380 notifyUserUpdate(ac) 381 382 err = ac.FinalizePatch(fpc.PatchId) 383 if err != nil { 384 return err 385 } 386 fmt.Println("Patch finalized.") 387 return nil 388 } 389 390 func (lgc *LastGreenCommand) Execute(_ []string) error { 391 ac, rc, settings, err := getAPIClients(lgc.GlobalOpts) 392 if err != nil { 393 return err 394 } 395 notifyUserUpdate(ac) 396 v, err := rc.GetLastGreen(lgc.Project, lgc.Variants) 397 if err != nil { 398 return err 399 } 400 return lastGreenTemplate.Execute(os.Stdout, struct { 401 Version *version.Version 402 UIURL string 403 }{v, settings.UIServerHost}) 404 } 405 406 func (lc *ListCommand) Execute(_ []string) error { 407 // stop the user from using > 1 type flag 408 if (lc.Projects && (lc.Variants || lc.Tasks)) || (lc.Tasks && lc.Variants) { 409 return errors.Errorf("list command takes only one of --projects, --variants, or --tasks") 410 } 411 if lc.Projects { 412 return lc.listProjects() 413 } 414 if lc.Tasks { 415 return lc.listTasks() 416 } 417 if lc.Variants { 418 return lc.listVariants() 419 } 420 return errors.Errorf("must specify one of --projects, --variants, or --tasks") 421 } 422 423 func (lc *ListCommand) listProjects() error { 424 ac, _, _, err := getAPIClients(lc.GlobalOpts) 425 if err != nil { 426 return errors.WithStack(err) 427 } 428 notifyUserUpdate(ac) 429 430 projs, err := ac.ListProjects() 431 if err != nil { 432 return err 433 } 434 ids := make([]string, 0, len(projs)) 435 names := make(map[string]string) 436 for _, proj := range projs { 437 // Only list projects that are enabled 438 if proj.Enabled { 439 ids = append(ids, proj.Identifier) 440 names[proj.Identifier] = proj.DisplayName 441 } 442 } 443 sort.Strings(ids) 444 fmt.Println(len(ids), "projects:") 445 w := new(tabwriter.Writer) 446 // Format in tab-separated columns with a tab stop of 8. 447 w.Init(os.Stdout, 0, 8, 0, '\t', 0) 448 for _, id := range ids { 449 line := fmt.Sprintf("\t%v\t", id) 450 if len(names[id]) > 0 && names[id] != id { 451 line = line + fmt.Sprintf("%v", names[id]) 452 } 453 fmt.Fprintln(w, line) 454 } 455 return errors.WithStack(w.Flush()) 456 } 457 458 // LoadLocalConfig loads the local project config into a project 459 func loadLocalConfig(filepath string) (*model.Project, error) { 460 configBytes, err := ioutil.ReadFile(filepath) 461 if err != nil { 462 return nil, errors.Wrap(err, "error reading project config") 463 } 464 465 project := &model.Project{} 466 err = model.LoadProjectInto(configBytes, "", project) 467 if err != nil { 468 return nil, errors.Wrap(err, "error loading project") 469 } 470 471 return project, nil 472 } 473 474 func (lc *ListCommand) listTasks() error { 475 var tasks []model.ProjectTask 476 if lc.Project != "" { 477 ac, _, _, err := getAPIClients(lc.GlobalOpts) 478 if err != nil { 479 return err 480 } 481 notifyUserUpdate(ac) 482 tasks, err = ac.ListTasks(lc.Project) 483 if err != nil { 484 return err 485 } 486 } else if lc.File != "" { 487 project, err := loadLocalConfig(lc.File) 488 if err != nil { 489 return err 490 } 491 tasks = project.Tasks 492 } else { 493 return noProjectError 494 } 495 fmt.Println(len(tasks), "tasks:") 496 w := new(tabwriter.Writer) 497 w.Init(os.Stdout, 0, 8, 0, '\t', 0) 498 for _, t := range tasks { 499 line := fmt.Sprintf("\t%v\t", t.Name) 500 fmt.Fprintln(w, line) 501 } 502 503 return w.Flush() 504 } 505 506 func (lc *ListCommand) listVariants() error { 507 var variants []model.BuildVariant 508 if lc.Project != "" { 509 ac, _, _, err := getAPIClients(lc.GlobalOpts) 510 if err != nil { 511 return err 512 } 513 notifyUserUpdate(ac) 514 variants, err = ac.ListVariants(lc.Project) 515 if err != nil { 516 return err 517 } 518 } else if lc.File != "" { 519 project, err := loadLocalConfig(lc.File) 520 if err != nil { 521 return err 522 } 523 variants = project.BuildVariants 524 } else { 525 return noProjectError 526 } 527 528 names := make([]string, 0, len(variants)) 529 displayNames := make(map[string]string) 530 for _, variant := range variants { 531 names = append(names, variant.Name) 532 displayNames[variant.Name] = variant.DisplayName 533 } 534 sort.Strings(names) 535 fmt.Println(len(names), "variants:") 536 w := new(tabwriter.Writer) 537 // Format in tab-separated columns with a tab stop of 8. 538 w.Init(os.Stdout, 0, 8, 0, '\t', 0) 539 for _, name := range names { 540 line := fmt.Sprintf("\t%v\t", name) 541 if len(displayNames[name]) > 0 && displayNames[name] != name { 542 line = line + fmt.Sprintf("%v", displayNames[name]) 543 } 544 fmt.Fprintln(w, line) 545 } 546 547 return w.Flush() 548 } 549 550 // Performs validation for patch or patch-file 551 func validatePatchCommand(params *PatchCommandParams) (ac *APIClient, settings *model.CLISettings, ref *model.ProjectRef, err error) { 552 ac, _, settings, err = getAPIClients(params.GlobalOpts) 553 if err != nil { 554 return 555 } 556 notifyUserUpdate(ac) 557 558 if params.Project == "" { 559 params.Project = settings.FindDefaultProject() 560 } else { 561 if settings.FindDefaultProject() == "" && 562 !params.SkipConfirm && confirm(fmt.Sprintf("Make %v your default project?", params.Project), true) { 563 settings.SetDefaultProject(params.Project) 564 if err = WriteSettings(settings, params.GlobalOpts); err != nil { 565 fmt.Printf("warning - failed to set default project: %v\n", err) 566 } 567 } 568 } 569 570 if params.Project == "" { 571 err = errors.Errorf("Need to specify a project.") 572 return 573 } 574 575 ref, err = ac.GetProjectRef(params.Project) 576 if err != nil { 577 if apiErr, ok := err.(APIError); ok && apiErr.code == http.StatusNotFound { 578 err = errors.Errorf("%v \nRun `evergreen list --projects` to see all valid projects", err) 579 } 580 return 581 } 582 583 // update variants 584 if len(params.Variants) == 0 { 585 params.Variants = settings.FindDefaultVariants(params.Project) 586 if len(params.Variants) == 0 && params.Finalize { 587 err = errors.Errorf("Need to specify at least one buildvariant with -v when finalizing." + 588 " Run with `-v all` to finalize against all variants.") 589 return 590 } 591 } else { 592 defaultVariants := settings.FindDefaultVariants(params.Project) 593 if len(defaultVariants) == 0 && !params.SkipConfirm && 594 confirm(fmt.Sprintf("Set %v as the default variants for project '%v'?", 595 params.Variants, params.Project), false) { 596 settings.SetDefaultVariants(params.Project, params.Variants...) 597 if err := WriteSettings(settings, params.GlobalOpts); err != nil { 598 fmt.Printf("warning - failed to set default variants: %v\n", err) 599 } 600 } 601 } 602 603 // update tasks 604 if len(params.Tasks) == 0 { 605 params.Tasks = settings.FindDefaultTasks(params.Project) 606 if len(params.Tasks) == 0 && params.Finalize { 607 err = errors.Errorf("Need to specify at least one task with -t when finalizing." + 608 " Run with `-t all` to finalize against all tasks.") 609 return 610 } 611 } else { 612 defaultTasks := settings.FindDefaultTasks(params.Project) 613 if len(defaultTasks) == 0 && !params.SkipConfirm && 614 confirm(fmt.Sprintf("Set %v as the default tasks for project '%v'?", 615 params.Tasks, params.Project), false) { 616 settings.SetDefaultTasks(params.Project, params.Tasks...) 617 if err := WriteSettings(settings, params.GlobalOpts); err != nil { 618 fmt.Printf("warning - failed to set default tasks: %v\n", err) 619 } 620 } 621 } 622 623 if params.Description == "" && !params.SkipConfirm { 624 params.Description = prompt("Enter a description for this patch (optional):") 625 } 626 627 return 628 } 629 630 // Creates a patch using diffData 631 func createPatch(params PatchCommandParams, ac *APIClient, settings *model.CLISettings, diffData *localDiff) error { 632 if err := validatePatchSize(diffData, params.Large); err != nil { 633 return err 634 } 635 if !params.SkipConfirm && len(diffData.fullPatch) == 0 { 636 if !confirm("Patch submission is empty. Continue?(y/n)", true) { 637 return nil 638 } 639 } else if !params.SkipConfirm && diffData.patchSummary != "" { 640 fmt.Println(diffData.patchSummary) 641 if diffData.log != "" { 642 fmt.Println(diffData.log) 643 } 644 645 if !confirm("This is a summary of the patch to be submitted. Continue? (y/n):", true) { 646 return nil 647 } 648 } 649 650 variantsStr := strings.Join(params.Variants, ",") 651 patchSub := patchSubmission{ 652 params.Project, diffData.fullPatch, params.Description, 653 diffData.base, variantsStr, params.Tasks, params.Finalize, 654 } 655 656 newPatch, err := ac.PutPatch(patchSub) 657 if err != nil { 658 return err 659 } 660 patchDisp, err := getPatchDisplay(newPatch, true, settings.UIServerHost) 661 if err != nil { 662 return err 663 } 664 665 fmt.Println("Patch successfully created.") 666 fmt.Print(patchDisp) 667 return nil 668 } 669 670 // Returns an error if the diff is greater than the system limit, or if it's above the large 671 // patch threhsold and allowLarge is not set. 672 func validatePatchSize(diff *localDiff, allowLarge bool) error { 673 patchLen := len(diff.fullPatch) 674 if patchLen > patch.SizeLimit { 675 return errors.Errorf("Patch is greater than the system limit (%v > %v bytes).", patchLen, patch.SizeLimit) 676 } else if patchLen > largePatchThreshold && !allowLarge { 677 return errors.Errorf("Patch is larger than the default threshold (%v > %v bytes).\n"+ 678 "To allow submitting this patch, use the --large flag.", patchLen, largePatchThreshold) 679 } 680 681 // Patch is small enough and/or allowLarge is true, so no error 682 return nil 683 } 684 685 // loadGitData inspects the current git working directory and returns a patch and its summary. 686 // The branch argument is used to determine where to generate the merge base from, and any extra 687 // arguments supplied are passed directly in as additional args to git diff. 688 func loadGitData(branch string, extraArgs ...string) (*localDiff, error) { 689 // branch@{upstream} refers to the branch that the branch specified by branchname is set to 690 // build on top of. This allows automatically detecting a branch based on the correct remote, 691 // if the user's repo is a fork, for example. 692 // For details see: https://git-scm.com/docs/gitrevisions 693 mergeBase, err := gitMergeBase(branch+"@{upstream}", "HEAD") 694 if err != nil { 695 return nil, errors.Errorf("Error getting merge base: %v", err) 696 } 697 statArgs := []string{"--stat"} 698 if len(extraArgs) > 0 { 699 statArgs = append(statArgs, extraArgs...) 700 } 701 stat, err := gitDiff(mergeBase, statArgs...) 702 if err != nil { 703 return nil, errors.Errorf("Error getting diff summary: %v", err) 704 } 705 log, err := gitLog(mergeBase) 706 if err != nil { 707 return nil, errors.Errorf("git log: %v", err) 708 } 709 710 patch, err := gitDiff(mergeBase, extraArgs...) 711 if err != nil { 712 return nil, errors.Errorf("Error getting patch: %v", err) 713 } 714 return &localDiff{patch, stat, log, mergeBase}, nil 715 } 716 717 // gitMergeBase runs "git merge-base <branch1> <branch2>" and returns the 718 // resulting githash as string 719 func gitMergeBase(branch1, branch2 string) (string, error) { 720 cmd := exec.Command("git", "merge-base", branch1, branch2) 721 out, err := cmd.Output() 722 if err != nil { 723 return "", errors.Errorf("'git merge-base %v %v' failed: %v", branch1, branch2, err) 724 } 725 return strings.TrimSpace(string(out)), err 726 } 727 728 // gitDiff runs "git diff <base> <diffargs ...>" and returns the output of the command as a string 729 func gitDiff(base string, diffArgs ...string) (string, error) { 730 args := make([]string, 0, 1+len(diffArgs)) 731 args = append(args, "--no-ext-diff") 732 args = append(args, diffArgs...) 733 return gitCmd("diff", base, args...) 734 } 735 736 // getLog runs "git log <base> 737 func gitLog(base string, logArgs ...string) (string, error) { 738 args := append(logArgs, "--oneline") 739 return gitCmd("log", fmt.Sprintf("...%v", base), args...) 740 } 741 742 func gitCmd(cmdName, base string, gitArgs ...string) (string, error) { 743 args := make([]string, 0, 1+len(gitArgs)) 744 args = append(args, cmdName) 745 if base != "" { 746 args = append(args, base) 747 } 748 args = append(args, gitArgs...) 749 cmd := exec.Command("git", args...) 750 out, err := cmd.CombinedOutput() 751 if err != nil { 752 return "", errors.Errorf("'git %v %v' failed with err %v", base, strings.Join(args, " "), err) 753 } 754 return string(out), err 755 }