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  }