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 }
func SetForBranch(ver *Version, branch string) (act action.Action, err error) { var mainTask = fmt.Sprintf("Bump version to %v for branch '%v'", ver, branch) // Make sure the repository is clean (don't check untracked files). task := "Make sure the repository is clean" if err := git.EnsureCleanWorkingTree(false); err != nil { return nil, errs.NewError(task, err) } // Remember the current branch. currentBranch, err := gitutil.CurrentBranch() if err != nil { return nil, err } // Remember the current position of the target branch. task = fmt.Sprintf("Remember the position of branch '%v'", branch) originalPosition, err := git.Hexsha("refs/heads/" + branch) if err != nil { return nil, errs.NewError(task, err) } // Checkout the target branch. task = fmt.Sprintf("Checkout branch '%v'", branch) if err := git.Checkout(branch); err != nil { return nil, errs.NewError(task, err) } defer func() { // Checkout the original branch on return. task := fmt.Sprintf("Checkout branch '%v'", currentBranch) if ex := git.Checkout(currentBranch); ex != nil { if err == nil { err = ex } else { errs.LogError(task, ex) } } }() // Set the project version to the desired value. if err := Set(ver); err != nil { if ex, ok := err.(*scripts.ErrNotFound); ok { return nil, fmt.Errorf( "custom SalsaFlow script '%v' not found on branch '%v'", ex.ScriptName(), branch) } return nil, err } // Commit changes. _, err = git.RunCommand("commit", "-a", "-m", fmt.Sprintf("Bump version to %v", ver), "-m", fmt.Sprintf("Story-Id: %v", git.StoryIdUnassignedTagValue)) if err != nil { task := "Reset the working tree to the original state" if err := git.Reset("--hard"); err != nil { errs.LogError(task, err) } return nil, err } return action.ActionFunc(func() (err error) { // On rollback, reset the target branch to the original position. log.Rollback(mainTask) task := fmt.Sprintf("Reset branch '%v' to the original position", branch) // Get the current branch name. currentBranch, err := gitutil.CurrentBranch() if err != nil { return errs.NewError(task, err) } // Use SetBranch in case we are not on the branch to be modified. if branch != currentBranch { return errs.Wrap(task, git.SetBranch(branch, originalPosition)) } // Otherwise use git reset --hard since currentBranch == branch. return errs.Wrap(task, git.Reset("--hard", originalPosition)) }), nil }