// createAssignedReviewRequest can be used to create a new review issue // for the given commits that is associated with the story passed in. func createAssignedReviewRequest( config *moduleConfig, owner string, repo string, story common.Story, commits []*git.Commit, opts map[string]interface{}, ) (*github.Issue, error) { task := fmt.Sprintf("Create review issue for story %v", story.ReadableId()) // Prepare the issue object. reviewIssue := ghissues.NewStoryReviewIssue( story.ReadableId(), story.URL(), story.Title(), story.IssueTracker().ServiceName(), story.Tag()) for _, commit := range commits { reviewIssue.AddCommit(false, commit.SHA, commit.MessageTitle) } // Get the right review milestone to add the issue into. milestone, err := getOrCreateMilestoneForCommit( config, owner, repo, commits[len(commits)-1].SHA) if err != nil { return nil, errs.NewError(task, err) } // Create a new review issue. var implemented bool implementedOpt, ok := opts["implemented"] if ok { implemented = implementedOpt.(bool) } issue, err := createIssue( task, config, owner, repo, reviewIssue.FormatTitle(), reviewIssue.FormatBody(), optValueString(opts["reviewer"]), milestone, implemented) if err != nil { return nil, errs.NewError(task, err) } return issue, nil }
// createAssignedReviewRequest can be used to create a new review issue // for the given commits that is associated with the story passed in. func createAssignedReviewRequest( config Config, owner string, repo string, story common.Story, commits []*git.Commit, opts map[string]interface{}, ) error { var ( task = fmt.Sprintf("Create review issue for story %v", story.ReadableId()) issueTitle = fmt.Sprintf("Review story %v: %v", story.ReadableId(), story.Title()) ) // Generate the issue body. var issueBody bytes.Buffer fmt.Fprintf(&issueBody, "Story being reviewed: [%v](%v)\n\n", story.ReadableId(), story.URL()) fmt.Fprintf(&issueBody, "SF-Issue-Tracker: %v\n", story.IssueTracker().ServiceName()) fmt.Fprintf(&issueBody, "SF-Story-Key: %v\n\n", story.Tag()) fmt.Fprintf(&issueBody, "The associated commits are following:") for _, commit := range commits { fmt.Fprintf(&issueBody, "\n- [ ] %v: %v", commit.SHA, commit.MessageTitle) } // Get the right review milestone to add the issue into. milestone, err := getOrCreateMilestoneForCommit( config, owner, repo, commits[len(commits)-1].SHA) if err != nil { return err } // Create a new review issue. issue, err := createIssue(task, config, owner, repo, issueTitle, issueBody.String(), milestone) if err != nil { return err } // Open the issue if requested. if _, open := opts["open"]; open { return openIssue(issue) } return nil }
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 }