コード例 #1
0
ファイル: mode_parent.go プロジェクト: salsaflow/salsaflow
func rebase(currentBranch, parentBranch string) ([]*git.Commit, error) {
	// Tell the user what is happening.
	task := fmt.Sprintf("Rebase branch '%v' onto '%v'", currentBranch, parentBranch)
	log.Run(task)

	// Do the rebase.
	if err := git.Rebase(parentBranch); err != nil {
		ex := errs.Log(errs.NewError(task, err))
		asciiart.PrintGrimReaper("GIT REBASE FAILED")
		fmt.Printf(`Git failed to rebase your branch onto '%v'.

The repository might have been left in the middle of the rebase process.
In case you do not know how to handle this, just execute

  $ git rebase --abort

to make your repository clean again.

In any case, you have to rebase your current branch onto '%v'
if you want to continue and post a review request. In the edge cases
you can as well use -no_rebase to skip this step, but try not to do it.
`, parentBranch, parentBranch)
		return nil, ex
	}

	// Reload the commits.
	task = "Get the commits to be posted for code review, again"
	commits, err := git.ShowCommitRange(parentBranch + "..")
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	// Return new commits.
	return commits, nil
}
コード例 #2
0
ファイル: git.go プロジェクト: salsaflow/salsaflow
// ListNewTrunkCommits returns the list of commits that are new since the last release.
// By the last release we mean the last release being tested, staged or released.
func ListNewTrunkCommits() ([]*git.Commit, error) {
	// Get git config.
	config, err := git.LoadConfig()
	if err != nil {
		return nil, err
	}
	var (
		remoteName    = config.RemoteName
		trunkBranch   = config.TrunkBranchName
		releaseBranch = config.ReleaseBranchName
		stagingBranch = config.StagingBranchName
	)

	// By default, use the staging branch as the --not part.
	// In other words, list commits that are on trunk,
	// but which are not reachable from the staging branch.
	// In case the staging branch doesn't exist, take the whole trunk.
	// That probably means that no release has ever been started,
	// so the staging branch has not been created yet.
	var revRange string
	for _, branch := range [...]string{releaseBranch, stagingBranch} {
		err := git.CheckOrCreateTrackingBranch(branch, remoteName)
		// In case the branch is ok, we use it.
		if err == nil {
			revRange = fmt.Sprintf("%v..%v", branch, trunkBranch)
			break
		}
		// In case the branch does not exist, it's ok and we continue.
		if _, ok := err.(*git.ErrRefNotFound); ok {
			continue
		}
		// Otherwise we return the error since something has just exploded.
		// This can mean that the branch is not up to date, but that is an error as well.
		return nil, err
	}
	if revRange == "" {
		revRange = trunkBranch
	}

	// Get the commits in range.
	commits, err := git.ShowCommitRange(revRange)
	if err != nil {
		return nil, err
	}

	// Limit the commits by date.
	repoConfig, err := repo.LoadConfig()
	if err != nil {
		return nil, err
	}

	enabledTimestamp := repoConfig.SalsaFlowEnabledTimestamp
	commits = git.FilterCommits(commits, func(commit *git.Commit) bool {
		return commit.AuthorDate.After(enabledTimestamp)
	})

	return commits, nil
}
コード例 #3
0
ファイル: changes.go プロジェクト: salsaflow/salsaflow
func StoryChangesToCherryPick(
	groups []*changes.StoryChangeGroup,
) ([]*changes.StoryChangeGroup, error) {

	// Get the changes that are reachable from the release branch.
	gitConfig, err := git.LoadConfig()
	if err != nil {
		return nil, err
	}
	releaseBranch := gitConfig.ReleaseBranchName

	reachableCommits, err := git.ShowCommitRange(releaseBranch)
	if err != nil {
		return nil, err
	}

	reachableChanges := make(map[string]struct{}, len(reachableCommits))
	for _, commit := range reachableCommits {
		// Would not probably harm much not to have the condition here,
		// but hey, let's keep the Change-Id set clean.
		if commit.ChangeIdTag != "" {
			reachableChanges[commit.ChangeIdTag] = struct{}{}
		}
	}

	// Get the changes that needs to be cherry-picked.
	var toCherryPick []*changes.StoryChangeGroup

	for _, group := range groups {
		// Prepare a new StoryChangeGroup to hold missing changes.
		storyGroup := &changes.StoryChangeGroup{
			StoryIdTag: group.StoryIdTag,
		}

		// A change needs cherry-picking in case it's not reachable
		// from the release branch, right?
		for _, change := range group.Changes {
			// Skip the group representing commits with no Change-Id tag.
			if change.ChangeIdTag == "" {
				continue
			}
			// Append the group in case the Change-Id is not reachable.
			if _, ok := reachableChanges[change.ChangeIdTag]; !ok {
				storyGroup.Changes = append(storyGroup.Changes, change)
			}
		}

		// Append the whole story group in case any associated change group
		// is not reachable from the release branch.
		if len(storyGroup.Changes) != 0 {
			toCherryPick = append(toCherryPick, storyGroup)
		}
	}

	return toCherryPick, nil
}
コード例 #4
0
ファイル: main.go プロジェクト: jkotrlik/salsaflow
func hook() error {
	// There are always 3 arguments passed to this hook.
	prevRef, newRef, flag := os.Args[1], os.Args[2], os.Args[3]

	// Return in case prevRef is the zero hash since that means
	// that this hook is being run right after 'git clone'.
	if prevRef == git.ZeroHash {
		return nil
	}

	// Return in case flag is '0'. That signals retrieving a file from the index.
	if flag == "0" {
		return nil
	}

	// Return unless the new HEAD is a core branch.
	isCore, err := isCoreBranchHash(newRef)
	if err != nil {
		return err
	}
	if !isCore {
		return nil
	}

	// Return also in case we are doing something with a temporary branch.
	isNewRefTemp, err := isTempBranchHash(newRef)
	if err != nil {
		return err
	}
	isPrevRefTemp, err := isTempBranchHash(prevRef)
	if err != nil {
		return err
	}
	if isNewRefTemp || isPrevRefTemp {
		return nil
	}

	// Get the relevant commits.
	// These are the commits specified by newRef..prevRef, e.g. trunk..story/foobar.
	commits, err := git.ShowCommitRange(fmt.Sprintf("%v..%v", newRef, prevRef))
	if err != nil {
		return err
	}

	// Drop commits that happened before SalsaFlow bootstrap.
	repoConfig, err := repo.LoadConfig()
	if err != nil {
		return err
	}
	enabledTimestamp := repoConfig.SalsaFlowEnabledTimestamp()
	commits = git.FilterCommits(commits, func(commit *git.Commit) bool {
		return commit.AuthorDate.After(enabledTimestamp)
	})

	// Collect the commits with missing Story-Id tag.
	missing := make([]*git.Commit, 0, len(commits))
	for _, commit := range commits {
		// Skip merge commits.
		if commit.Merge != "" {
			continue
		}

		// Add the commit in case Story-Id tag is not set.
		if commit.StoryIdTag == "" {
			missing = append(missing, commit)
		}
	}
	if len(missing) == 0 {
		return nil
	}

	// Print the warning.
	return printWarning(missing)
}
コード例 #5
0
ファイル: common.go プロジェクト: salsaflow/salsaflow
func rewriteCommits(commits []*git.Commit, firstMissingOffset int) ([]*git.Commit, error) {
	// Fetch the stories in progress from the issue tracker.
	storiesTask := "Missing Story-Id detected, fetch stories from the issue tracker"
	log.Run(storiesTask)

	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return nil, errs.NewError(storiesTask, err)
	}

	task := "Fetch the user record from the issue tracker"
	me, err := tracker.CurrentUser()
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	stories, err := tracker.ReviewableStories()
	if err != nil {
		return nil, errs.NewError(storiesTask, err)
	}

	reviewedStories, err := tracker.ReviewedStories()
	if err != nil {
		return nil, errs.NewError(storiesTask, err)
	}

	// Show only the stories owned by the current user.
	// Note: Go sucks here, badly.
	filterStories := func(stories []common.Story, filter func(common.Story) bool) []common.Story {
		ss := make([]common.Story, 0, len(stories))
		for _, story := range stories {
			if filter(story) {
				ss = append(ss, story)
			}
		}
		return ss
	}

	mine := func(story common.Story) bool {
		for _, assignee := range story.Assignees() {
			if assignee.Id() == me.Id() {
				return true
			}
		}
		return false
	}

	stories = filterStories(stories, mine)
	reviewedStories = filterStories(reviewedStories, mine)

	// Tell the user what is happening.
	log.Run("Prepare a temporary branch to rewrite commit messages")

	// Get the current branch name.
	currentBranch, err := gitutil.CurrentBranch()
	if err != nil {
		return nil, err
	}

	// Get the parent of the first commit in the chain.
	task = "Get the parent commit of the commit chain to be posted"
	var parentSHA string
	if firstMissingOffset != 0 {
		// In case there are multiple commits being posted
		// and the first missing offset is not pointing to the first commit,
		// we can easily get the parent SHA by just accessing the commit list.
		parentSHA = commits[firstMissingOffset-1].SHA
	} else {
		// Otherwise we have to ask git for help.
		stdout, err := git.Log("--pretty=%P", "-n", "1", commits[firstMissingOffset].SHA)
		if err != nil {
			return nil, errs.NewError(task, err)
		}
		parentSHA = strings.Fields(stdout.String())[0]
	}

	// Prepare a temporary branch that will be used to amend commit messages.
	task = "Create a temporary branch to rewrite commit messages"
	if err := git.SetBranch(constants.TempBranchName, parentSHA); err != nil {
		return nil, errs.NewError(task, err)
	}
	defer func() {
		// Delete the temporary branch on exit.
		task := "Delete the temporary branch"
		if err := git.Branch("-D", constants.TempBranchName); err != nil {
			errs.LogError(task, err)
		}
	}()

	// Checkout the temporary branch.
	task = "Checkout the temporary branch"
	if err := git.Checkout(constants.TempBranchName); err != nil {
		return nil, errs.NewError(task, err)
	}
	defer func() {
		// Checkout the original branch on exit.
		task := fmt.Sprintf("Checkout branch '%v'", currentBranch)
		if err := git.Checkout(currentBranch); err != nil {
			errs.LogError(task, err)
		}
	}()

	// Loop and rewrite the commit messages.
	var story common.Story
	if flagAskOnce {
		header := `
Some of the commits listed above are not assigned to any story.
Please pick up the story that these commits will be assigned to.
You can also insert 'u' to mark the commits as unassigned:`
		selectedStory, err := promptForStory(header, stories, reviewedStories)
		if err != nil {
			return nil, err
		}
		story = selectedStory
	}

	// The temp branch is pointing to the parent of the first commit missing
	// the Story-Id tag. So we only need to cherry-pick the commits that
	// follow the first commit missing the Story-Id tag.
	commitsToCherryPick := commits[firstMissingOffset:]
	for _, commit := range commitsToCherryPick {
		// Cherry-pick the commit.
		task := fmt.Sprintf("Move commit %v onto the temporary branch", commit.SHA)
		if err := git.CherryPick(commit.SHA); err != nil {
			return nil, errs.NewError(task, err)
		}

		if commit.StoryIdTag == "" {
			if !flagAskOnce {
				commitMessageTitle := prompt.ShortenCommitTitle(commit.MessageTitle)

				// Ask for the story ID for the current commit.
				header := fmt.Sprintf(`
The following commit is not assigned to any story:

  commit hash:  %v
  commit title: %v

Please pick up the story to assign the commit to.
Inserting 'u' will mark the commit as unassigned:`, commit.SHA, commitMessageTitle)
				selectedStory, err := promptForStory(header, stories, reviewedStories)
				if err != nil {
					return nil, err
				}
				story = selectedStory
			}

			// Use the unassigned tag value in case no story is selected.
			storyTag := git.StoryIdUnassignedTagValue
			if story != nil {
				storyTag = story.Tag()
			}

			// Extend the commit message to include Story-Id.
			commitMessage := fmt.Sprintf("%v\nStory-Id: %v\n", commit.Message, storyTag)

			// Amend the cherry-picked commit to include the new commit message.
			task = "Amend the commit message for " + commit.SHA
			stderr := new(bytes.Buffer)
			cmd := exec.Command("git", "commit", "--amend", "-F", "-")
			cmd.Stdin = bytes.NewBufferString(commitMessage)
			cmd.Stderr = stderr
			if err := cmd.Run(); err != nil {
				return nil, errs.NewErrorWithHint(task, err, stderr.String())
			}
		}
	}

	// Reset the current branch to point to the new branch.
	task = "Reset the current branch to point to the temporary branch"
	if err := git.SetBranch(currentBranch, constants.TempBranchName); err != nil {
		return nil, errs.NewError(task, err)
	}

	// Parse the commits again since the commit hashes have changed.
	newCommits, err := git.ShowCommitRange(parentSHA + "..")
	if err != nil {
		return nil, err
	}

	log.NewLine("")
	log.Log("Commit messages amended successfully")

	// And we are done!
	return newCommits, nil
}
コード例 #6
0
ファイル: command.go プロジェクト: salsaflow/salsaflow
func filterBranches(storyBranches []*git.GitBranch, trunkName string) ([]*gitBranch, error) {
	// Pair the branches with commit ranges specified by trunk..story
	task := "Collected commits associated with the story branches"
	branches := make([]*gitBranch, 0, len(storyBranches))
	for _, branch := range storyBranches {
		var revRange string
		if branch.BranchName != "" {
			// Handle branches that exist locally.
			revRange = fmt.Sprintf("%v..%v", trunkName, branch.BranchName)
		} else {
			// Handle branches that exist only in the remote repository.
			// We can use trunkName here since trunk is up to date.
			revRange = fmt.Sprintf("%v..%v/%v", trunkName, branch.Remote, branch.RemoteBranchName)
		}

		commits, err := git.ShowCommitRange(revRange)
		if err != nil {
			return nil, errs.NewError(task, err)
		}
		branches = append(branches, &gitBranch{
			tip:     branch,
			commits: commits,
		})
		continue
	}

	// Collect story tags.
	task = "Collect affected story tags"
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	tags := make([]string, 0, len(storyBranches))
BranchLoop:
	for _, branch := range branches {
		for _, commit := range branch.commits {
			commitTag := commit.StoryIdTag

			// Make sure the tag is not in the list already.
			for _, tag := range tags {
				if tag == commitTag {
					continue BranchLoop
				}
			}

			// Drop tags not recognized by the current issue tracker.
			_, err := tracker.StoryTagToReadableStoryId(commitTag)
			if err == nil {
				tags = append(tags, commitTag)
			}
		}
	}

	// Fetch the collected stories.
	task = "Fetch associated stories from the issue tracker"
	log.Run(task)
	stories, err := tracker.ListStoriesByTag(tags)
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	// Filter the branches according to the story state.
	storyByTag := make(map[string]common.Story, len(stories))
	for i, story := range stories {
		// tags[i] corresponds to stories[i]
		tag := tags[i]
		if story != nil {
			storyByTag[tag] = story
		} else {
			log.Warn(fmt.Sprintf("Story for tag '%v' was not found in the issue tracker", tag))
		}
	}

	allowedStates := allowedStoryStates()

	// checkCommits returns whether the commits passed in are ok
	// considering the state of the stories found in these commits,
	// whether the branch containing these commits can be deleted.
	checkCommits := func(commits []*git.Commit) (common.StoryState, bool) {
		var storyFound bool
		for _, commit := range commits {
			// Skip commits with empty Story-Id tag.
			if commit.StoryIdTag == "" {
				continue
			}

			// In case the story is not found, the tag is not recognized
			// by the current issue tracker. In that case we just skip the commit.
			story, ok := storyByTag[commit.StoryIdTag]
			if !ok {
				continue
			}

			// When the story state associated with the commit is not ok,
			// we can return false here to reject the branch.
			storyState := story.State()
			if _, ok := allowedStates[storyState]; !ok {
				return storyState, false
			}

			storyFound = true
		}

		// We went through all the commits and they are fine, check passed.
		return common.StoryStateInvalid, storyFound
	}

	// Go through the branches and only return these that
	// comply with the story state requirements.
	bs := make([]*gitBranch, 0, len(branches))
	for _, branch := range branches {
		tip := branch.tip

		logger := log.V(log.Verbose)
		if logger {
			logger.Log(fmt.Sprintf("Processing branch %v", tip.CanonicalName()))
		}

		// The branch can be for sure deleted in case there are no commits
		// contained in the commit range. That means the branch is merged into trunk.
		if len(branch.commits) == 0 {
			if logger {
				logger.Log("  Include the branch (reason: merged into trunk)")
			}
			branch.reason = "merged"
			bs = append(bs, branch)
			continue
		}

		// In case the commit check passed, we append the branch.
		state, ok := checkCommits(branch.commits)
		if ok {
			if logger {
				logger.Log("  Include the branch (reason: branch check passed)")
			}
			branch.reason = "check passed"
			bs = append(bs, branch)
			continue
		}

		// Otherwise we print the skip warning.
		if logger {
			if state == common.StoryStateInvalid {
				logger.Log(
					"  Exclude the branch (reason: no story commits found on the branch)")
			} else {
				logger.Log(fmt.Sprintf(
					"  Exclude the branch (reason: story state is '%v')", state))
			}
		}
	}

	return bs, nil
}
コード例 #7
0
ファイル: mode_parent.go プロジェクト: salsaflow/salsaflow
func postBranch(parentBranch string) (err error) {
	// Load the git-related config.
	gitConfig, err := git.LoadConfig()
	if err != nil {
		return err
	}
	var (
		remoteName = gitConfig.RemoteName
	)

	// Get the current branch name.
	currentBranch, err := gitutil.CurrentBranch()
	if err != nil {
		return err
	}

	if !flagNoFetch {
		// Fetch the remote repository.
		task := "Fetch the remote repository"
		log.Run(task)

		if err := git.UpdateRemotes(remoteName); err != nil {
			return errs.NewError(task, err)
		}
	}

	// Make sure the parent branch is up to date.
	task := fmt.Sprintf("Make sure reference '%v' is up to date", parentBranch)
	log.Run(task)
	if err := git.EnsureBranchSynchronized(parentBranch, remoteName); err != nil {
		return errs.NewError(task, err)
	}

	// Make sure the current branch is up to date.
	task = fmt.Sprintf("Make sure branch '%v' is up to date", currentBranch)
	log.Run(task)
	if err = git.EnsureBranchSynchronized(currentBranch, remoteName); err != nil {
		return errs.NewError(task, err)
	}

	// Get the commits to be posted
	task = "Get the commits to be posted for code review"
	commits, err := git.ShowCommitRange(parentBranch + "..")
	if err != nil {
		return errs.NewError(task, err)
	}

	// Make sure there are no merge commits.
	if err := ensureNoMergeCommits(commits); err != nil {
		return err
	}

	// Prompt the user to confirm.
	if err := promptUserToConfirmCommits(commits); err != nil {
		return err
	}

	// Rebase the current branch on top the parent branch.
	if !flagNoRebase {
		commits, err = rebase(currentBranch, parentBranch)
		if err != nil {
			return err
		}
	}

	// Ensure the Story-Id tag is there.
	commits, _, err = ensureStoryId(commits)
	if err != nil {
		return err
	}

	// Get data on the current branch.
	task = fmt.Sprintf("Get data on branch '%v'", currentBranch)
	remoteCurrentExists, err := git.RemoteBranchExists(currentBranch, remoteName)
	if err != nil {
		return errs.NewError(task, err)
	}
	currentUpToDate, err := git.IsBranchSynchronized(currentBranch, remoteName)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Merge the current branch into the parent branch unless -no_merge.
	pushTask := "Push the current branch"
	if flagNoMerge {
		// In case the user doesn't want to merge,
		// we need to push the current branch.
		if !remoteCurrentExists || !currentUpToDate {
			if err := push(remoteName, currentBranch); err != nil {
				return errs.NewError(pushTask, err)
			}
		}
	} else {
		// Still push the current branch if necessary.
		if remoteCurrentExists && !currentUpToDate {
			if err := push(remoteName, currentBranch); err != nil {
				return errs.NewError(pushTask, err)
			}
		}

		// Merge the branch into the parent branch
		mergeTask := fmt.Sprintf("Merge branch '%v' into branch '%v'", currentBranch, parentBranch)
		log.Run(mergeTask)
		act, err := merge(mergeTask, currentBranch, parentBranch)
		if err != nil {
			return err
		}

		// Push the parent branch.
		if err := push(remoteName, parentBranch); err != nil {
			// In case the push fails, we revert the merge as well.
			if err := act.Rollback(); err != nil {
				errs.Log(err)
			}
			return errs.NewError(mergeTask, err)
		}

		// Register a rollback function that just says that
		// a pushed merge cannot be reverted.
		defer action.RollbackOnError(&err, action.ActionFunc(func() error {
			log.Rollback(mergeTask)
			hint := "\nCannot revert merge that has already been pushed.\n"
			return errs.NewErrorWithHint(
				"Revert the merge", errors.New("merge commit already pushed"), hint)
		}))
	}

	// Post the review requests.
	if err := postCommitsForReview(commits); err != nil {
		return err
	}

	// In case there is no error, tell the user they can do next.
	return printFollowup()
}
コード例 #8
0
ファイル: main.go プロジェクト: salsaflow/salsaflow
func run(remoteName, pushURL string) error {
	// Load the git-related SalsaFlow config.
	gitConfig, err := git.LoadConfig()
	if err != nil {
		return err
	}

	// Load the other necessary SalsaFlow config.
	repoConfig, err := repo.LoadConfig()
	if err != nil {
		return err
	}
	enabledTimestamp := repoConfig.SalsaFlowEnabledTimestamp

	// Only check the project remote.
	if remoteName != gitConfig.RemoteName {
		log.Log(
			fmt.Sprintf(
				"Not pushing to the main project remote (%v), check skipped",
				gitConfig.RemoteName))
		return nil
	}

	// The commits that are being pushed are listed on stdin.
	// The format is <local ref> <local sha1> <remote ref> <remote sha1>,
	// so we parse the input and collect all the local hexshas.
	var coreRefs = []string{
		"refs/heads/" + gitConfig.TrunkBranchName,
		"refs/heads/" + gitConfig.ReleaseBranchName,
		"refs/heads/" + gitConfig.StagingBranchName,
		"refs/heads/" + gitConfig.StableBranchName,
	}

	parseTask := "Parse the hook input"
	var revRanges []*revisionRange
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		var (
			line  = scanner.Text()
			parts = strings.Split(line, " ")
		)
		if len(parts) != 4 {
			return errs.NewError(parseTask, errors.New("invalid input line: "+line))
		}

		localRef, localSha, remoteRef, remoteSha := parts[0], parts[1], parts[2], parts[3]

		// Skip the refs that are being deleted.
		if localSha == git.ZeroHash {
			continue
		}

		// Check only updates to the core branches,
		// i.e. trunk, release, client or master.
		var isCoreBranch bool
		for _, ref := range coreRefs {
			if remoteRef == ref {
				isCoreBranch = true
			}
		}
		if !isCoreBranch {
			continue
		}

		// Make sure the reference is up to date.
		// In this case the reference is not up to date when
		// the remote hash cannot be found in the local clone.
		if remoteSha != git.ZeroHash {
			task := fmt.Sprintf("Make sure remote ref '%s' is up to date", remoteRef)
			if _, err := git.Run("cat-file", "-t", remoteSha); err != nil {
				hint := fmt.Sprintf(`
Commit %v does not exist locally.
This is probably because '%v' is not up to date.
Please update the reference from the remote repository,
perhaps by executing 'git pull'.

`, remoteSha, remoteRef)
				return errs.NewErrorWithHint(task, err, hint)
			}
		}

		// Append the revision range for this input line.
		var revRange *revisionRange
		if remoteSha == git.ZeroHash {
			// In case we are pushing a new branch, check commits up to trunk.
			// There is probably no better guess that we can do in general.
			revRange = &revisionRange{gitConfig.TrunkBranchName, localRef}
		} else {
			// Otherwise check the commits that are new compared to the remote ref.
			revRange = &revisionRange{remoteSha, localRef}
		}
		revRanges = append(revRanges, revRange)
	}
	if err := scanner.Err(); err != nil {
		return errs.NewError(parseTask, err)
	}

	// Check the missing Story-Id tags.
	var missing []*git.Commit

	for _, revRange := range revRanges {
		// Get the commit objects for the relevant range.
		task := "Get the commit objects to be pushed"
		commits, err := git.ShowCommitRange(fmt.Sprintf("%v..%v", revRange.From, revRange.To))
		if err != nil {
			return errs.NewError(task, err)
		}

		// Check every commit in the range.
		for _, commit := range commits {
			// Do not check merge commits.
			if commit.Merge != "" {
				continue
			}

			// Do not check commits that happened before SalsaFlow.
			if commit.AuthorDate.Before(enabledTimestamp) {
				continue
			}

			// Check the Story-Id tag.
			if commit.StoryIdTag == "" {
				missing = append(missing, commit)
			}
		}
	}

	// Prompt for confirmation in case that is needed.
	if len(missing) != 0 {
		// Fill in the commit sources.
		task := "Fix commit sources"
		if err := git.FixCommitSources(missing); err != nil {
			return errs.NewError(task, err)
		}

		// Prompt the user for confirmation.
		task = "Prompt the user for confirmation"
		confirmed, err := promptUserForConfirmation(missing)
		if err != nil {
			return errs.NewError(task, err)
		}
		if !confirmed {
			return prompt.ErrCanceled
		}
	}

	return nil
}