Example #1
0
// postAssignedReviewRequest can be used to post
// the commits associated with the given story for review.
func postAssignedReviewRequest(
	config *moduleConfig,
	owner string,
	repo string,
	story common.Story,
	commits []*git.Commit,
	opts map[string]interface{},
) (*github.Issue, []*git.Commit, error) {

	// Search for an existing review issue for the given story.
	task := fmt.Sprintf("Search for an existing review issue for story %v", story.ReadableId())
	log.Run(task)

	client := ghutil.NewClient(config.Token)
	issue, err := ghissues.FindReviewIssueForStory(client, owner, repo, story.ReadableId())
	if err != nil {
		return nil, nil, errs.NewError(task, err)
	}

	// Decide what to do next based on the search results.
	if issue == nil {
		// No review issue found for the given story, create a new issue.
		issue, err := createAssignedReviewRequest(config, owner, repo, story, commits, opts)
		if err != nil {
			return nil, nil, err
		}
		return issue, commits, nil
	}

	// An existing review issue found, extend it.
	return extendReviewRequest(config, owner, repo, issue, commits, opts)
}
Example #2
0
// postAssignedReviewRequest can be used to post
// the commits associated with the given story for review.
func postAssignedReviewRequest(
	config Config,
	owner string,
	repo string,
	story common.Story,
	commits []*git.Commit,
	opts map[string]interface{},
) error {

	// Search for an existing review issue for the given story.
	task := fmt.Sprintf("Search for an existing review issue for story %v", story.ReadableId())
	log.Run(task)

	query := fmt.Sprintf(
		"\"Review story %v\" repo:%v/%v label:%v type:issue in:title",
		story.ReadableId(), owner, repo, config.ReviewLabel())

	client := ghutil.NewClient(config.Token())
	result, _, err := client.Search.Issues(query, &github.SearchOptions{})
	if err != nil {
		return errs.NewError(task, err)
	}

	// Decide what to do next based on the search results.
	switch len(result.Issues) {
	case 0:
		// No review issue found for the given story, create a new issue.
		return createAssignedReviewRequest(config, owner, repo, story, commits, opts)
	case 1:
		// An existing review issue found, extend it.
		return extendReviewRequest(config, owner, repo, &result.Issues[0], commits, opts)
	default:
		// Multiple review issue found for the given story, that is clearly wrong
		// since there is always just a single review issue for every story.
		err := errors.New("inconsistency detected: multiple story review issues found")
		return errs.NewError("Make sure the review issue can be created", err)
	}
}
Example #3
0
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
}
Example #4
0
// 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
}
Example #5
0
// 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
}