예제 #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)
}
예제 #2
0
func addReviewComment(
	config Config,
	owner string,
	repo string,
	issueNum int,
	commits []*git.Commit,
) error {

	// Generate the comment body.
	buffer := bytes.NewBufferString("The following commits were added to this issue:")
	for _, commit := range commits {
		fmt.Fprintf(buffer, "\n* %v: %v", commit.SHA, commit.MessageTitle)
	}

	// Call GitHub API.
	task := fmt.Sprintf("Add review comment for issue #%v", issueNum)
	client := ghutil.NewClient(config.Token())
	_, _, err := client.Issues.CreateComment(owner, repo, issueNum, &github.IssueComment{
		Body: github.String(buffer.String()),
	})
	if err != nil {
		return errs.NewError(task, err)
	}
	return nil
}
예제 #3
0
func createMilestone(
	config Config,
	owner string,
	repo string,
	v *version.Version,
) (*github.Milestone, action.Action, error) {

	// Create the review milestone.
	var (
		title         = milestoneTitle(v)
		milestoneTask = fmt.Sprintf("Create GitHub review milestone '%v'", title)
		client        = ghutil.NewClient(config.Token())
	)
	log.Run(milestoneTask)
	milestone, _, err := client.Issues.CreateMilestone(owner, repo, &github.Milestone{
		Title: github.String(title),
	})
	if err != nil {
		return nil, nil, errs.NewError(milestoneTask, err)
	}

	// Return a rollback function.
	return milestone, action.ActionFunc(func() error {
		log.Rollback(milestoneTask)
		task := fmt.Sprintf("Delete GitHub review milestone '%v'", title)
		_, err := client.Issues.DeleteMilestone(owner, repo, *milestone.Number)
		if err != nil {
			return errs.NewError(task, err)
		}
		return nil
	}), nil
}
예제 #4
0
func createIssue(
	task string,
	config Config,
	owner string,
	repo string,
	issueTitle string,
	issueBody string,
	milestone *github.Milestone,
) (issue *github.Issue, err error) {

	log.Run(task)
	client := ghutil.NewClient(config.Token())
	labels := []string{config.ReviewLabel()}
	issue, _, err = client.Issues.Create(owner, repo, &github.IssueRequest{
		Title:     github.String(issueTitle),
		Body:      github.String(issueBody),
		Labels:    &labels,
		Milestone: milestone.Number,
	})
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	log.Log(fmt.Sprintf("GitHub issue #%v created", *issue.Number))
	return issue, nil
}
예제 #5
0
func linkCommitsToReviewIssue(
	config *moduleConfig,
	owner string,
	repo string,
	issueNum int,
	commits []*git.Commit,
) {
	// Instantiate an API client.
	client := ghutil.NewClient(config.Token)

	// Loop over the commits and post a commit comment for each of them.
	for _, commit := range commits {
		task := fmt.Sprintf("Link commit %v to the associated review issue", commit.SHA)
		log.Run(task)

		body := fmt.Sprintf(
			"This commit is being reviewed as a part of review issue #%v.", issueNum)
		comment := &github.RepositoryComment{
			Body: &body,
		}
		_, _, err := client.Repositories.CreateComment(owner, repo, commit.SHA, comment)
		if err != nil {
			// Just print the error to the console.
			errs.LogError(task, err)
		}
	}
}
예제 #6
0
func milestoneForVersion(
	config Config,
	owner string,
	repo string,
	v *version.Version,
) (*github.Milestone, error) {

	// Fetch milestones for the given repository.
	var (
		task   = fmt.Sprintf("Fetch GitHub milestones for %v/%v", owner, repo)
		client = ghutil.NewClient(config.Token())
		title  = milestoneTitle(v)
	)
	milestones, _, err := client.Issues.ListMilestones(owner, repo, nil)
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	// Find the right one.
	task = fmt.Sprintf("Find review milestone for release %v", v)
	for _, milestone := range milestones {
		if *milestone.Title == title {
			return &milestone, nil
		}
	}

	// Milestone not found.
	return nil, nil
}
예제 #7
0
// postUnassignedReviewRequest can be used to post the given commit for review.
// This function is to be used to post commits that are not associated with any story.
func postUnassignedReviewRequest(
	config *moduleConfig,
	owner string,
	repo string,
	commit *git.Commit,
	opts map[string]interface{},
) (*github.Issue, []*git.Commit, error) {

	// Search for an existing issue.
	task := fmt.Sprintf("Search for an existing review issue for commit %v", commit.SHA)
	log.Run(task)

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

	// Return an error in case the issue for the given commit already exists.
	if issue != nil {
		issueNum := *issue.Number
		err = fmt.Errorf("existing review issue found for commit %v: %v", commit.SHA, issueNum)
		return nil, nil, errs.NewError("Make sure the review issue can be created", err)
	}

	// Create a new unassigned review request.
	issue, err = createUnassignedReviewRequest(config, owner, repo, commit, opts)
	if err != nil {
		return nil, nil, err
	}
	return issue, []*git.Commit{commit}, nil
}
예제 #8
0
func (tool *codeReviewTool) FinaliseRelease(v *version.Version) (action.Action, error) {
	// Get a GitHub client.
	config, err := LoadConfig()
	if err != nil {
		return nil, err
	}
	client := ghutil.NewClient(config.Token())

	owner, repo, err := git.ParseUpstreamURL()
	if err != nil {
		return nil, err
	}

	// Get the relevant review milestone.
	releaseString := v.BaseString()
	task := fmt.Sprintf("Get GitHub review milestone for release %v", releaseString)
	log.Run(task)
	milestone, err := milestoneForVersion(config, owner, repo, v)
	if err != nil {
		return nil, errs.NewError(task, err)
	}
	if milestone == nil {
		log.Warn(fmt.Sprintf(
			"Weird, GitHub review milestone for release %v not found", releaseString))
		return nil, nil
	}

	// Close the milestone unless there are some issues open.
	task = fmt.Sprintf(
		"Make sure the review milestone for release %v can be closed", releaseString)
	if num := *milestone.OpenIssues; num != 0 {
		return nil, errs.NewError(
			task,
			fmt.Errorf(
				"review milestone for release %v cannot be closed: %v issue(s) open",
				releaseString, num))
	}

	milestoneTask := fmt.Sprintf("Close GitHub review milestone for release %v", releaseString)
	log.Run(milestoneTask)
	milestone, _, err = client.Issues.EditMilestone(owner, repo, *milestone.Number, &github.Milestone{
		State: github.String("closed"),
	})
	if err != nil {
		return nil, errs.NewError(milestoneTask, err)
	}

	// Return a rollback function.
	return action.ActionFunc(func() error {
		log.Rollback(milestoneTask)
		task := fmt.Sprintf("Reopen GitHub review milestone for release %v", releaseString)
		_, _, err := client.Issues.EditMilestone(owner, repo, *milestone.Number, &github.Milestone{
			State: github.String("open"),
		})
		if err != nil {
			return errs.NewError(task, err)
		}
		return nil
	}), nil
}
예제 #9
0
func newGitHubClient() (*github.Client, error) {
	config, err := ghutil.LoadConfig()
	if err != nil {
		return nil, err
	}

	return ghutil.NewClient(config.ApiToken()), nil
}
예제 #10
0
// extendReviewRequest is a general function that can be used to extend
// the given review issue with the given list of commits.
func extendReviewRequest(
	config Config,
	owner string,
	repo string,
	issue *github.Issue,
	commits []*git.Commit,
	opts map[string]interface{},
) error {

	var (
		issueNum     = *issue.Number
		issueBody    = *issue.Body
		bodyBuffer   = bytes.NewBufferString(issueBody)
		addedCommits = make([]*git.Commit, 0, len(commits))
	)

	for _, commit := range commits {
		// Make sure the commit is not added yet.
		commitString := fmt.Sprintf("] %v: %v", commit.SHA, commit.MessageTitle)
		if strings.Contains(issueBody, commitString) {
			log.Log(fmt.Sprintf("Commit %v already listed in issue #%v", commit.SHA, issueNum))
			continue
		}

		// Extend the issue body.
		addedCommits = append(addedCommits, commit)
		fmt.Fprintf(bodyBuffer, "\n- [ ] %v: %v", commit.SHA, commit.MessageTitle)
	}

	if len(addedCommits) == 0 {
		log.Log(fmt.Sprintf("All commits already listed in issue #%v", issueNum))
		return nil
	}

	// Edit the issue.
	task := fmt.Sprintf("Update GitHub issue #%v", issueNum)
	log.Run(task)

	client := ghutil.NewClient(config.Token())
	newIssue, _, err := client.Issues.Edit(owner, repo, issueNum, &github.IssueRequest{
		Body:  github.String(bodyBuffer.String()),
		State: github.String("open"),
	})
	if err != nil {
		return errs.NewError(task, err)
	}

	// Add the review comment.
	if err := addReviewComment(config, owner, repo, issueNum, addedCommits); err != nil {
		return err
	}

	// Open the issue if requested.
	if _, open := opts["open"]; open {
		return openIssue(newIssue)
	}
	return nil
}
예제 #11
0
func newGitHubClient() (*gh.Client, error) {
	task := "Instantiate a GitHub API client"

	// Get the access token.
	spec := newConfigSpec()
	if err := loader.LoadConfig(spec); err != nil {
		return nil, errs.NewError(task, err)
	}

	// Return a new API client.
	return github.NewClient(spec.global.GitHubToken), nil
}
예제 #12
0
func createMilestone(
	config *moduleConfig,
	owner string,
	repo string,
	v *version.Version,
) (*github.Milestone, action.Action, error) {

	// Create the review milestone for the given version.
	var (
		client = ghutil.NewClient(config.Token)
		title  = milestoneTitle(v)
	)
	return ghissues.CreateMilestone(client, owner, repo, title)
}
예제 #13
0
func milestoneForVersion(
	config *moduleConfig,
	owner string,
	repo string,
	v *version.Version,
) (*github.Milestone, error) {

	// Find the milestone matching the version.
	var (
		client = ghutil.NewClient(config.Token)
		title  = milestoneTitle(v)
	)
	return ghissues.FindMilestoneByTitle(client, owner, repo, title)
}
예제 #14
0
func (r *release) prepareForApiCalls() (client *github.Client, owner, repo string, err error) {
	if r.client == nil {
		r.client = ghutil.NewClient(r.tool.config.Token)
	}

	if r.owner == "" || r.repo == "" {
		var err error
		r.owner, r.repo, err = ghutil.ParseUpstreamURL()
		if err != nil {
			return nil, "", "", err
		}
	}

	return r.client, r.owner, r.repo, nil
}
예제 #15
0
// postUnassignedReviewRequest can be used to post the given commit for review.
// This function is to be used to post commits that are not associated with any story.
func postUnassignedReviewRequest(
	config Config,
	owner string,
	repo string,
	commit *git.Commit,
	opts map[string]interface{},
) error {

	// Extend the specified review issue in case -fixes is specified.
	flagFixes, ok := opts["fixes"]
	if ok {
		if fixes, ok := flagFixes.(uint); ok && fixes != 0 {
			return extendUnassignedReviewRequest(config, owner, repo, int(fixes), commit, opts)
		}
	}

	// Search for an existing issue.
	task := fmt.Sprintf("Search for an existing review issue for commit %v", commit.SHA)
	log.Run(task)

	query := fmt.Sprintf(
		"\"Review commit %v\" repo:%v/%v label:%v type:issue in:title",
		commit.SHA, 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:
		// Create a new unassigned review request.
		return createUnassignedReviewRequest(config, owner, repo, commit, opts)
	case 1:
		// The issues already exists, return an error.
		issueNum := *result.Issues[0].Number
		err := fmt.Errorf("existing review issue found for commit %v: %v", commit.SHA, issueNum)
		return errs.NewError("Make sure the review issue can be created", err)
	default:
		// Inconsistency detected: multiple review issues found.
		err := fmt.Errorf(
			"inconsistency detected: multiple review issue found for commit %v", commit.SHA)
		return errs.NewError("Make sure the review issue can be created", err)
	}
}
예제 #16
0
func createIssue(
	task string,
	config *moduleConfig,
	owner string,
	repo string,
	issueTitle string,
	issueBody string,
	assignee string,
	milestone *github.Milestone,
	implemented bool,
) (issue *github.Issue, err error) {

	log.Run(task)
	client := ghutil.NewClient(config.Token)

	var labels []string
	if implemented {
		labels = []string{config.ReviewLabel, config.StoryImplementedLabel}
	} else {
		labels = []string{config.ReviewLabel}
	}

	var assigneePtr *string
	if assignee != "" {
		assigneePtr = &assignee
	}

	issue, _, err = client.Issues.Create(owner, repo, &github.IssueRequest{
		Title:     github.String(issueTitle),
		Body:      github.String(issueBody),
		Labels:    &labels,
		Assignee:  assigneePtr,
		Milestone: milestone.Number,
	})
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	log.Log(fmt.Sprintf("GitHub issue #%v created", *issue.Number))
	return issue, nil
}
예제 #17
0
func getOrCreateMilestoneForCommit(
	config *moduleConfig,
	owner string,
	repo string,
	sha string,
) (*github.Milestone, error) {

	// Get the version associated with the given commit.
	v, err := version.GetByBranch(sha)
	if err != nil {
		return nil, err
	}

	// Get or create the milestone for the given title.
	var (
		client = ghutil.NewClient(config.Token)
		title  = milestoneTitle(v)
	)
	milestone, _, err := ghissues.GetOrCreateMilestoneForTitle(client, owner, repo, title)
	return milestone, err
}
예제 #18
0
// extendUnassignedReviewRequest can be used to upload fixes for
// the specified unassigned review issue.
func extendUnassignedReviewRequest(
	config Config,
	owner string,
	repo string,
	issueNum int,
	commit *git.Commit,
	opts map[string]interface{},
) error {

	// Fetch the issue.
	task := fmt.Sprintf("Fetch GitHub issue #%v", issueNum)
	log.Run(task)
	client := ghutil.NewClient(config.Token())
	issue, _, err := client.Issues.Get(owner, repo, issueNum)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Extend the given review issue.
	return extendReviewRequest(config, owner, repo, issue, []*git.Commit{commit}, opts)
}
예제 #19
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)
	}
}
예제 #20
0
// extendReviewRequest is a general function that can be used to extend
// the given review issue with the given list of commits.
func extendReviewRequest(
	config *moduleConfig,
	owner string,
	repo string,
	issue *github.Issue,
	commits []*git.Commit,
	opts map[string]interface{},
) (*github.Issue, []*git.Commit, error) {

	issueNum := *issue.Number

	// Parse the issue.
	task := fmt.Sprintf("Parse review issue #%v", issueNum)
	reviewIssue, err := ghissues.ParseReviewIssue(issue)
	if err != nil {
		return nil, nil, errs.NewError(task, err)
	}

	// Add the commits.
	newCommits := make([]*git.Commit, 0, len(commits))
	for _, commit := range commits {
		if reviewIssue.AddCommit(false, commit.SHA, commit.MessageTitle) {
			newCommits = append(newCommits, commit)
		}
	}
	if len(newCommits) == 0 {
		log.Log(fmt.Sprintf("All commits already listed in issue #%v", issueNum))
		return issue, nil, nil
	}

	// Add the implemented label if necessary.
	var (
		implemented      bool
		implementedLabel = config.StoryImplementedLabel
		labelsPtr        *[]string
	)
	implementedOpt, ok := opts["implemented"]
	if ok {
		implemented = implementedOpt.(bool)
	}
	if implemented {
		labels := make([]string, 0, len(issue.Labels)+1)
		labelsPtr = &labels

		for _, label := range issue.Labels {
			if *label.Name == implementedLabel {
				// The label is already there, for some reason.
				// Set the pointer to nil so that we don't update labels.
				labelsPtr = nil
				break
			}
			labels = append(labels, *label.Name)
		}
		if labelsPtr != nil {
			labels = append(labels, implementedLabel)
		}
	}

	// Edit the issue.
	task = fmt.Sprintf("Update GitHub issue #%v", issueNum)
	log.Run(task)

	client := ghutil.NewClient(config.Token)
	updatedIssue, _, err := client.Issues.Edit(owner, repo, issueNum, &github.IssueRequest{
		Body:   github.String(reviewIssue.FormatBody()),
		State:  github.String("open"),
		Labels: labelsPtr,
	})
	if err != nil {
		return nil, nil, errs.NewError(task, err)
	}

	// Add the review comment.
	if err := addReviewComment(config, owner, repo, issueNum, newCommits); err != nil {
		return nil, nil, err
	}

	return updatedIssue, newCommits, nil
}
예제 #21
0
func (tracker *issueTracker) newClient() *github.Client {
	return ghutil.NewClient(tracker.config.UserToken)
}
예제 #22
0
func (manager *releaseNotesManager) PostReleaseNotes(
	releaseNotes *common.ReleaseNotes,
) (action.Action, error) {

	// Get the GitHub owner and repository from the upstream URL.
	owner, repo, err := github.ParseUpstreamURL()
	if err != nil {
		return nil, err
	}

	// Instantiate the API client.
	client := github.NewClient(manager.config.Token)

	// Format the release notes.
	task := "Format the release notes"
	body := bytes.NewBufferString(`
## Summary ##

**PLEASE FILL IN THE RELEASE SUMMARY**
`)
	encoder, err := notes.NewEncoder(notes.EncodingMarkdown, body)
	if err != nil {
		return nil, errs.NewError(task, err)
	}
	if err := encoder.Encode(releaseNotes, nil); err != nil {
		return nil, errs.NewError(task, err)
	}
	bodyString := body.String()

	// Create GitHub release for the given version.
	tag := releaseNotes.Version.ReleaseTagString()
	releaseTask := fmt.Sprintf("Create GitHub release for tag '%v'", tag)
	log.Run(releaseTask)
	release, _, err := client.Repositories.CreateRelease(owner, repo, &gh.RepositoryRelease{
		TagName: gh.String(tag),
		Name:    gh.String("Release " + releaseNotes.Version.BaseString()),
		Body:    &bodyString,
		Draft:   gh.Bool(true),
	})
	if err != nil {
		return nil, err
	}

	// Delete the GitHub release on rollback.
	rollback := func() error {
		log.Rollback(releaseTask)
		task := fmt.Sprintf("Delete GitHub release for tag '%v'", tag)
		_, err := client.Repositories.DeleteRelease(owner, repo, *release.ID)
		if err != nil {
			return errs.NewError(task, err)
		}
		return nil
	}

	// Open the release in the browser so that the user can fill in the details.
	task = "Open the release notes in the browser"
	if err := webbrowser.Open(*release.HTMLURL); err != nil {
		if ex := rollback(); ex != nil {
			errs.Log(ex)
		}
		return nil, errs.NewError(task, err)
	}

	// Return the rollback function.
	return action.ActionFunc(rollback), nil
}