コード例 #1
0
ファイル: common.go プロジェクト: salsaflow/salsaflow
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
}
コード例 #2
0
ファイル: stage.go プロジェクト: salsaflow/salsaflow
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
}
コード例 #3
0
ファイル: common.go プロジェクト: salsaflow/salsaflow
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
}
コード例 #4
0
ファイル: dialog.go プロジェクト: salsaflow/salsaflow
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!")
	}
}
コード例 #5
0
ファイル: dialog.go プロジェクト: salsaflow/salsaflow
// 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"))
	}
}
コード例 #6
0
ファイル: config.go プロジェクト: salsaflow/salsaflow
// 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
}
コード例 #7
0
ファイル: command.go プロジェクト: salsaflow/salsaflow
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
}
コード例 #8
0
ファイル: command.go プロジェクト: salsaflow/salsaflow
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())
}