func PrintUnassignedWarning(writer io.Writer, commits []*git.Commit) (n int64, err error) { var output bytes.Buffer // Let's be colorful! redBold := color.New(color.FgRed).Add(color.Bold).SprintFunc() fmt.Fprint(&output, redBold("Warning: There are some commits missing the Story-Id tag.\n")) red := color.New(color.FgRed).SprintFunc() fmt.Fprint(&output, red("Make sure that this is alright before proceeding further.\n\n")) hashRe := regexp.MustCompile("^[0-9a-f]{40}$") yellow := color.New(color.FgYellow).SprintFunc() for _, commit := range commits { var ( sha = commit.SHA source = commit.Source title = prompt.ShortenCommitTitle(commit.MessageTitle) ) if hashRe.MatchString(commit.Source) { source = "unknown commit source branch" } fmt.Fprintf(&output, " %v | %v | %v\n", yellow(sha), yellow(source), title) } // Write the output to the writer. return io.Copy(ansicolor.NewAnsiColorWriter(writer), &output) }
func mustListCommits(writer io.Writer, commits []*git.Commit, prefix string) { must := func(n int, err error) error { if err != nil { panic(err) } return err } tw := tabwriter.NewWriter(writer, 0, 8, 4, '\t', 0) must(fmt.Fprintf(tw, "%vCommit SHA\tCommit Title\n", prefix)) must(fmt.Fprintf(tw, "%v==========\t============\n", prefix)) for _, commit := range commits { commitMessageTitle := prompt.ShortenCommitTitle(commit.MessageTitle) must(fmt.Fprintf(tw, "%v%v\t%v\n", prefix, commit.SHA, commitMessageTitle)) } must(0, tw.Flush()) }
func PrintUnassignedWarning(writer io.Writer, commits []*git.Commit) (n int64, err error) { var output bytes.Buffer // Let's be colorful! redBold := color.New(color.FgRed).Add(color.Bold).SprintFunc() fmt.Fprint(&output, redBold("Warning: There are some commits missing the Story-Id tag.\n")) red := color.New(color.FgRed).SprintFunc() fmt.Fprint(&output, red("Make sure that this is alright before proceeding further.\n\n")) yellow := color.New(color.FgYellow).SprintFunc() refPrefixLen := len("refs/heads/") for _, commit := range commits { fmt.Fprintf(&output, " %v | %v | %v\n", yellow(commit.SHA), yellow(commit.Source[refPrefixLen:]), prompt.ShortenCommitTitle(commit.MessageTitle)) } // Write the output to the writer. return io.Copy(ansicolor.NewAnsiColorWriter(writer), &output) }
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 }
// DumpStoryChanges writes a nicely formatted output to the io.Writer passed in. // // In case the porcelain argument is true, the output is printed in a more machine-friendly way. func DumpStoryChanges( writer io.Writer, groups []*StoryChangeGroup, tracker common.IssueTracker, porcelain bool, ) error { tw := tabwriter.NewWriter(writer, 0, 8, 2, '\t', 0) if !porcelain { _, err := io.WriteString(tw, "Story\tChange\tCommit SHA\tCommit Source\tCommit Title\n") if err != nil { return err } _, err = io.WriteString(tw, "=====\t======\t==========\t=============\t============\n") if err != nil { return err } } for _, group := range groups { storyId, err := tracker.StoryTagToReadableStoryId(group.StoryIdTag) if err != nil { return err } for _, change := range group.Changes { changeId := change.ChangeIdTag // Print the first line. var ( commit = change.Commits[0] commitMessageTitle = prompt.ShortenCommitTitle(commit.MessageTitle) ) printChange := func(commit *git.Commit) error { _, err := fmt.Fprintf(tw, "%v\t%v\t%v\t%v\t%v\n", storyId, changeId, commit.SHA, commit.Source, commitMessageTitle) return err } if err := printChange(commit); err != nil { return err } // Make some of the columns empty in case we are not porcelain. if !porcelain { storyId = "" changeId = "" } // Print the rest with the chosen columns being empty. for _, commit := range change.Commits[1:] { if err := printChange(commit); err != nil { return err } } } } return tw.Flush() }