func promptUserToConfirmCommits(commits []*git.Commit) error { // Make sure there are actually some commits to be posted. task := "Make sure there are actually some commits to be posted" if len(commits) == 0 { return errs.NewError(task, ErrNoCommits) } // Tell the user what is going to happen. fmt.Print(` You are about to post some of the following commits for code review: `) mustListCommits(os.Stdout, commits, " ") // Ask the user for confirmation. task = "Prompt the user for confirmation" confirmed, err := prompt.Confirm("\nYou cool with that?", true) if err != nil { return errs.NewError(task, err) } if !confirmed { prompt.PanicCancel() } fmt.Println() return nil }
func checkCommits( tracker common.IssueTracker, release common.RunningRelease, releaseBranch string, ) error { var task = "Make sure no changes are being left behind" log.Run(task) stories, err := release.Stories() if err != nil { return errs.NewError(task, err) } if len(stories) == 0 { return nil } groups, err := changes.StoryChanges(stories) if err != nil { return errs.NewError(task, err) } toCherryPick, err := releases.StoryChangesToCherryPick(groups) if err != nil { return errs.NewError(task, err) } // In case there are some changes being left behind, // ask the user to confirm whether to proceed or not. if len(toCherryPick) == 0 { return nil } fmt.Println(` Some changes are being left behind! In other words, some changes that are assigned to the current release have not been cherry-picked onto the release branch yet. `) if err := changes.DumpStoryChanges(os.Stdout, toCherryPick, tracker, false); err != nil { panic(err) } fmt.Println() confirmed, err := prompt.Confirm("Are you sure you really want to stage the release?", false) if err != nil { return errs.NewError(task, err) } if !confirmed { prompt.PanicCancel() } fmt.Println() return nil }
func postCommitsForReview(commits []*git.Commit) error { // Pick the commits to be posted for review. if flagPick { task := "Select the commits to be posted for review" var err error commits, err = selectCommitsForReview(commits) if err != nil { return errs.NewError(task, err) } if len(commits) == 0 { log.NewLine("") log.Log("No commits selected, aborting...") prompt.PanicCancel() } } // Print Snoopy. asciiart.PrintSnoopy() // Turn Commits into ReviewContexts. task := "Fetch stories for the commits to be posted for review" log.Run(task) ctxs, err := commitsToReviewContexts(commits) if err != nil { return errs.NewError(task, err) } // Mark the stories as implemented, potentially. task = "Mark the stories as implemented, optionally" implemented, act, err := implementedDialog(ctxs) if err != nil { return errs.NewError(task, err) } defer action.RollbackTaskOnError(&err, task, act) // Post the review requests. task = "Post the review requests" if err := sendReviewRequests(ctxs, implemented); err != nil { return errs.NewError(task, err) } return nil }
func promptUserToSelectModule(modules []Module, optional bool) (Module, error) { // Prompt the user to select a module. kind := modules[0].Kind() for { // Write the dialog into the console. ctx := &dialogTemplateContext{ Kind: string(kind), Modules: modules, Optional: optional, } if err := dialogTemplate.Execute(os.Stdout, ctx); err != nil { return nil, err } // Prompt the user for the answer. // An empty answer is aborting the dialog. answer, err := prompt.Prompt("You choice: ") if err != nil { if err == prompt.ErrCanceled { prompt.PanicCancel() } return nil, err } if optional && answer == "s" { fmt.Println() color.Cyan("Skipping module kind '%v'", kind) return nil, nil } // Parse the index and return the associated module. i, err := strconv.Atoi(answer) if err == nil { if 0 < i && i <= len(modules) { return modules[i-1], nil } } // In case we failed to parse the index or something, run the dialog again. color.Yellow("Not a valid choice, please try again!") } }
// Run starts the dialog after the options are set. It uses the given story list // to prompt the user for a story using the given options. func (dialog *Dialog) Run(stories []common.Story) (common.Story, error) { // Return an error when no options are set. if len(dialog.opts) == 0 { return nil, errors.New("storyprompt.Dialog.Run(): no options were specified") } // Increment the dialog depth on enter. dialog.depth++ // Decrement the dialog depth on return. defer func() { dialog.depth-- }() // Enter the dialog loop. DialogLoop: for { var ( opts = dialog.opts depth = dialog.depth ) // Present the stories to the user. if err := ListStories(stories, os.Stdout); err != nil { return nil, err } // Print the options based on the dialog depth. fmt.Println() fmt.Println("Now you can do one of the following:") fmt.Println() // Collect the list of active options. activeOpts := make([]*DialogOption, 0, len(opts)) for _, opt := range opts { if isActive := opt.IsActive; isActive != nil && isActive(stories, depth) { activeOpts = append(activeOpts, opt) } } // Print the description for the active options. for _, opt := range activeOpts { if desc := opt.Description; len(desc) != 0 { fmt.Println(" -", strings.Join(desc, "\n ")) } } fmt.Println() // Prompt the user for their choice. fmt.Println("Current dialog depth:", depth) input, err := prompt.Prompt("Choose what to do next: ") // We ignore prompt.ErrCanceled here and simply continue. // That is because an empty input is a valid input here as well. if err != nil && err != prompt.ErrCanceled { return nil, err } input = strings.TrimSpace(input) // Find the first matching option. var matchingOpt *DialogOption for _, opt := range activeOpts { if matchFunc := opt.MatchesInput; matchFunc != nil && matchFunc(input, stories) { matchingOpt = opt break } } // Loop again in case no match is found. if matchingOpt == nil { fmt.Println() fmt.Println("Error: no matching option found") fmt.Println() continue DialogLoop } // Run the selected select function. if selectFunc := matchingOpt.SelectStory; selectFunc != nil { story, err := selectFunc(input, stories, dialog) if err != nil { switch err { case ErrContinue: // Continue looping on ErrContinue. fmt.Println() continue DialogLoop case ErrReturn: // Go one dialog up by returning ErrContinue. // This makes the dialog loop of the parent dialog continue, // effectively re-printing and re-running that dialog. if dialog.isSub { return nil, ErrContinue } // In case this is a top-level dialog, abort. fallthrough case ErrAbort: // Panic prompt.ErrCanceled on ErrAbort, returning immediately // from any dialog depth. prompt.PanicCancel() } // In case the error is not any of the recognized control errors, // print the error and loop again, making the user choose again. fmt.Println() fmt.Println("Error:", err) fmt.Println() continue DialogLoop } return story, nil } // No SelectStory function specified for the matching option, // that is a programming error, let's just panic. panic(errors.New("SelectStory function not specified")) } }
// PromptUserForConfig is a part of loader.ConfigContainer interface. func (local *LocalConfig) PromptUserForConfig() error { c := LocalConfig{spec: local.spec} // Prompt for the project ID. task := "Fetch available Pivotal Tracker projects" log.Run(task) client := pivotal.NewClient(local.spec.global.UserToken) projects, _, err := client.Projects.List() if err != nil { return errs.NewError(task, err) } sort.Sort(ptProjects(projects)) fmt.Println() fmt.Println("Available Pivotal Tracker projects:") fmt.Println() for i, project := range projects { fmt.Printf(" [%v] %v\n", i+1, project.Name) } fmt.Println() fmt.Println("Choose the project to associate this repository with.") index, err := prompt.PromptIndex("Project number: ", 1, len(projects)) if err != nil { if err == prompt.ErrCanceled { prompt.PanicCancel() } return err } fmt.Println() c.ProjectId = projects[index-1].Id // Prompt for the labels. promptForLabel := func(dst *string, labelName, defaultValue string) { if err != nil { return } question := fmt.Sprintf("%v label", labelName) var label string label, err = prompt.PromptDefault(question, defaultValue) if err == nil { *dst = label } } var componentLabel string promptForLabel(&componentLabel, "Component", "") c.ComponentLabel = &componentLabel promptForLabel(&c.Labels.PointMeLabel, "Point me", DefaultPointMeLabel) promptForLabel(&c.Labels.ReviewedLabel, "Reviewed", DefaultReviewedLabel) promptForLabel(&c.Labels.SkipReviewLabel, "Skip review", DefaultSkipReviewLabel) promptForLabel(&c.Labels.TestedLabel, "Testing passed", DefaultTestedLabel) promptForLabel(&c.Labels.SkipTestingLabel, "Skip testing", DefaultSkipTestingLabel) if err != nil { return err } // Prompt for the release skip check labels. skipCheckLabelsString, err := prompt.Prompt(fmt.Sprintf( "Skip check labels, comma-separated (%v always included): ", strings.Join(DefaultSkipCheckLabels, ", "))) if err != nil { if err != prompt.ErrCanceled { return err } } // Append the new labels to the default ones. // Make sure there are no duplicates and empty strings. var ( insertedLabels = strings.Split(skipCheckLabelsString, ",") lenDefault = len(DefaultSkipCheckLabels) lenInserted = len(insertedLabels) ) // Save a few allocations. skipCheckLabels := make([]string, lenDefault, lenDefault+lenInserted) copy(skipCheckLabels, DefaultSkipCheckLabels) LabelLoop: for _, insertedLabel := range insertedLabels { // Trim spaces. insertedLabel = strings.TrimSpace(insertedLabel) // Skip empty strings. if insertedLabel == "" { continue } // Make sure there are no duplicates. for _, existingLabel := range skipCheckLabels { if insertedLabel == existingLabel { continue LabelLoop } } // Append the label. skipCheckLabels = append(skipCheckLabels, insertedLabel) } c.Labels.SkipCheckLabels = skipCheckLabels // Success! *local = c return nil }
func runMain() error { // Load git-related config. gitConfig, err := git.LoadConfig() if err != nil { return err } var ( remoteName = gitConfig.RemoteName trunkBranch = gitConfig.TrunkBranchName releaseBranch = gitConfig.ReleaseBranchName ) // Fetch the remote repository unless explicitly skipped. if !flagNoFetch { task := "Fetch the remote repository" log.Run(task) if err := git.UpdateRemotes(remoteName); err != nil { return errs.NewError(task, err) } } log.Run("Make sure all important branches are up to date") // Check branches. checkBranch := func(branchName string) error { task := fmt.Sprintf("Make sure that branch '%v' exists and is up to date", branchName) if err := git.CheckOrCreateTrackingBranch(branchName, remoteName); err != nil { return errs.NewError(task, err) } return nil } for _, branch := range [...]string{releaseBranch, trunkBranch} { if err := checkBranch(branch); err != nil { return err } } // Remember the current branch. currentBranch, err := gitutil.CurrentBranch() if err != nil { return err } // Checkout the release branch. task := "Checkout the release branch" if err := git.Checkout(releaseBranch); err != nil { return errs.NewError(task, err) } defer func() { // Do not checkout the original branch in case the name is empty. // This is later used to disable the checkout of the original branch. if currentBranch == "" { return } // Otherwise checkout the original branch. task := fmt.Sprintf("Checkout the original branch (%v)", currentBranch) if err := git.Checkout(currentBranch); err != nil { errs.LogError(task, err) } }() // Get the current release version string. // It is enough to just call version.Get since // we are already on the release branch. task = "Get the release branch version string" releaseVersion, err := version.Get() if err != nil { return errs.NewError(task, err) } // Get the stories associated with the current release. task = "Fetch the stories associated with the current release" log.Run(task) tracker, err := modules.GetIssueTracker() if err != nil { return errs.NewError(task, err) } release := tracker.RunningRelease(releaseVersion) stories, err := release.Stories() if err != nil { return errs.NewError(task, err) } if len(stories) == 0 { return errs.NewError(task, errors.New("no relevant stories found")) } // Get the release changes. task = "Collect the release changes" log.Run(task) groups, err := changes.StoryChanges(stories) if err != nil { return errs.NewError(task, err) } // Just return in case there are no relevant commits found. if len(groups) == 0 { return errs.NewError(task, errors.New("no relevant commits found")) } // Sort the change groups. groups = changes.SortStoryChanges(groups, stories) groups, err = releases.StoryChangesToCherryPick(groups) if err != nil { return errs.NewError(task, err) } var ( // Collect the changes not reachable from trunk. // In case there are any, we abort the cherry-picking process. unreachable = make([]*changes.StoryChangeGroup, 0, len(groups)) // When we are at iterating, we also collect all release commits // so that we know what trunk commits to cherry-pick later. releaseCommits = make(map[string]struct{}) trunkRef = fmt.Sprintf("refs/heads/%v", trunkBranch) ) for _, group := range groups { g := &changes.StoryChangeGroup{ StoryIdTag: group.StoryIdTag, } for _, ch := range group.Changes { var ok bool for _, c := range ch.Commits { // Add the commit to the map of release commits. releaseCommits[c.SHA] = struct{}{} // Look for a commit that is on trunk. if c.Source == trunkRef { ok = true } } if !ok { // In case there is none, remember the change. g.Changes = append(g.Changes, ch) } } // In case there are some changes not reachable from trunk, // add the story change to the list of unreachable story changes. if len(g.Changes) != 0 { unreachable = append(unreachable, g) } } // In case there are some changes not reachable from the trunk branch, // abort the process and tell the user to get the changes into trunk first. if len(unreachable) != 0 { var details bytes.Buffer fmt.Fprint(&details, ` The following story changes are not reachable from the trunk branch: `) changes.DumpStoryChanges(&details, unreachable, tracker, false) fmt.Fprint(&details, ` Please cherry-pick these changes onto the trunk branch. Only then we can proceed and cherry-pick the changes. `) return errs.NewErrorWithHint( task, errors.New("commits not reachable from trunk detected"), details.String()) } // Everything seems fine, let's continue with the process // by dumping the change details into the console. fmt.Println() changes.DumpStoryChanges(os.Stdout, groups, tracker, false) // Ask the user to confirm before doing any cherry-picking. task = "Ask the user to confirm cherry-picking" fmt.Println(` The changes listed above will be cherry-picked into the release branch.`) confirmed, err := prompt.Confirm("Are you sure you want to continue?", false) if err != nil { return errs.NewError(task, err) } if !confirmed { prompt.PanicCancel() } fmt.Println() // Collect the trunk commits that were created since the last release. task = "Collect the trunk commits added since the last release" trunkCommits, err := releases.ListNewTrunkCommits() if err != nil { return errs.NewError(task, err) } // We need the list to start with the oldest commit. for i, j := 0, len(trunkCommits)-1; i < j; i, j = i+1, j-1 { trunkCommits[i], trunkCommits[j] = trunkCommits[j], trunkCommits[i] } // Collect the commits to cherry pick. These are the commits // that are on trunk and they are associated with the release. hashesToCherryPick := make([]string, 0, len(trunkCommits)) for _, commit := range trunkCommits { if _, ok := releaseCommits[commit.SHA]; ok { hashesToCherryPick = append(hashesToCherryPick, commit.SHA) } } // Perform the cherry-pick itself. task = "Cherry-pick the missing changes into the release branch" log.Run(task) if err := git.CherryPick(hashesToCherryPick...); err != nil { hint := ` It was not possible to cherry-pick the missing changes into the release branch. The cherry-picking process might be still in progress, though. Please check the repository status and potentially resolve the cherry-picking manually. ` // Do not checkout the original branch. currentBranch = "" return errs.NewErrorWithHint(task, err, hint) } log.Log("All missing changes cherry-picked into the release branch") fmt.Println(` ################################################################### # IMPORTANT: The release branch is not being pushed automatically # ################################################################### `) return nil }
func runMain() (err error) { tracker, err := modules.GetIssueTracker() if err != nil { return err } // Fetch stories from the issue tracker. task := "Fetch stories from the issue tracker" log.Run(task) stories, err := tracker.StartableStories() if err != nil { return errs.NewError(task, err) } if len(stories) == 0 { return errs.NewError(task, errors.New("no startable stories found")) } // Filter out the stories that are not relevant, // i.e. not owned by the current user or assigned to someone else. task = "Fetch the current user record from the issue tracker" user, err := tracker.CurrentUser() if err != nil { return errs.NewError(task, err) } var filteredStories []common.Story StoryLoop: for _, story := range stories { assignees := story.Assignees() // Include the story in case there is no assignee set yet. if len(assignees) == 0 { filteredStories = append(filteredStories, story) continue StoryLoop } // Include the story in case the current user is assigned. for _, assignee := range assignees { if assignee.Id() == user.Id() { filteredStories = append(filteredStories, story) continue StoryLoop } } } stories = filteredStories // Prompt the user to select a story. story, err := dialog( "\nYou can start working on one of the following stories:", stories) if err != nil { switch err { case prompt.ErrNoStories: return errors.New("no startable stories found") case prompt.ErrCanceled: prompt.PanicCancel() default: return err } } fmt.Println() // Create the story branch, optionally. if flagNoBranch { log.Log("Not creating any feature branch") } else { var act action.Action act, err = createBranch() if err != nil { return err } // Roll back on error. defer action.RollbackTaskOnError(&err, task, act) } // Add the current user to the list of story assignees. task = "Amend the list of story assignees" log.Run(task) originalAssignees := story.Assignees() if err := story.AddAssignee(user); err != nil { return errs.NewError(task, err) } defer action.RollbackTaskOnError(&err, task, action.ActionFunc(func() error { task := "Reset the list of story assignees" if err := story.SetAssignees(originalAssignees); err != nil { return errs.NewError(task, err) } return nil })) // Start the selected story. No need to roll back. task = "Start the selected story" log.Run(task) return errs.Wrap(task, story.Start()) }