Пример #1
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
}
Пример #2
0
func (release *nextRelease) PromptUserToConfirmStart() (bool, error) {
	// Collect the issues to be added to the current release.
	task := "Collect the issues that modified trunk since the last release"
	log.Run(task)
	ids, err := releases.ListStoryIdsToBeAssigned(release.tracker)
	if err != nil {
		return false, errs.NewError(task, err)
	}

	// Fetch the additional issues from JIRA.
	task = "Fetch the collected issues from JIRA"
	log.Run(task)
	issues, err := listStoriesById(newClient(release.tracker.config), ids)
	if len(issues) == 0 && err != nil {
		return false, errs.NewError(task, err)
	}
	if len(issues) != len(ids) {
		log.Warn("Some issues were dropped since they were not found in JIRA")
	}

	// Drop the issues that were already assigned to the right version.
	releaseLabel := release.trunkVersion.ReleaseTagString()
	filteredIssues := make([]*jira.Issue, 0, len(issues))
IssueLoop:
	for _, issue := range issues {
		// Add only the parent tasks, i.e. skip sub-tasks.
		if issue.Fields.Parent != nil {
			continue
		}
		// Add only the issues that have not been assigned to the release yet.
		for _, label := range issue.Fields.Labels {
			if label == releaseLabel {
				continue IssueLoop
			}
		}
		filteredIssues = append(filteredIssues, issue)
	}
	issues = filteredIssues

	// Present the issues to the user.
	if len(issues) != 0 {
		fmt.Println("\nThe following issues are going to be added to the release:\n")
		err := prompt.ListStories(toCommonStories(issues, release.tracker), os.Stdout)
		if err != nil {
			return false, err
		}
	}

	// Ask the user to confirm.
	ok, err := prompt.Confirm(
		fmt.Sprintf(
			"\nAre you sure you want to start release %v?",
			release.trunkVersion.BaseString()))
	if err == nil {
		release.additionalIssues = issues
	}
	return ok, err
}
Пример #3
0
func fetchOrUpdateSkeleton(skeleton string) error {
	// Parse the skeleton string.
	parts := strings.SplitN(skeleton, "/", 2)
	if len(parts) != 2 {
		return fmt.Errorf("not a valid repository path string: %v", skeleton)
	}
	owner, repo := parts[0], parts[1]

	// Create the cache directory if necessary.
	task := "Make sure the local cache directory exists"
	cacheDir, err := cacheDirectoryAbsolutePath()
	if err != nil {
		return errs.NewError(task, err)
	}
	if err := os.MkdirAll(cacheDir, 0755); err != nil {
		return errs.NewError(task, err)
	}

	// Pull or close the given skeleton.
	task = "Pull or clone the given skeleton"
	skeletonDir := filepath.Join(cacheDir, "github.com", owner)

	if err := os.MkdirAll(skeletonDir, 0755); err != nil {
		return errs.NewError(task, err)
	}

	skeletonPath := filepath.Join(skeletonDir, repo)
	if _, err := os.Stat(skeletonPath); err != nil {
		if !os.IsNotExist(err) {
			return errs.NewError(task, err)
		}

		// The directory does not exist, hence we clone.
		task := fmt.Sprintf("Clone skeleton '%v'", skeleton)
		log.Run(task)
		args := []string{
			"clone",
			"--single-branch",
			fmt.Sprintf("https://github.com/%v/%v", owner, repo),
			skeletonPath,
		}
		if _, err := git.Run(args...); err != nil {
			return errs.NewError(task, err)
		}
		return nil
	}

	// The skeleton directory exists, hence we pull.
	task = fmt.Sprintf("Pull skeleton '%v'", skeleton)
	log.Run(task)
	cmd, _, stderr := shell.Command("git", "pull")
	cmd.Dir = skeletonPath
	if err := cmd.Run(); err != nil {
		return errs.NewErrorWithHint(task, err, stderr.String())
	}
	return nil
}
Пример #4
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)
		}
	}
}
Пример #5
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
}
Пример #6
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)
}
Пример #7
0
func (release *runningRelease) Release() error {
	task := "Ping Sprintly to register the deployment"
	log.Run(task)

	// Create the Sprintly deployment.
	return release.deploy(release.config.ProductionEnvironment())
}
Пример #8
0
func (release *runningRelease) EnsureStageable() error {
	task := "Make sure the stories can be staged"
	log.Run(task)

	// Load the assigned stories.
	stories, err := release.loadStories()
	if err != nil {
		return errs.NewError(task, err)
	}

	var details bytes.Buffer
	tw := tabwriter.NewWriter(&details, 0, 8, 4, '\t', 0)
	io.WriteString(tw, "\n")
	io.WriteString(tw, "Story URL\tError\n")
	io.WriteString(tw, "=========\t=====\n")

	// For a story to be stageable, it must be in the Finished stage.
	// That by definition means that it has been reviewed and verified.
	for _, story := range stories {
		if !stateAtLeast(story, pivotal.StoryStateFinished) {
			fmt.Fprintf(tw, "%v\t%v\n", story.URL, "story not finished yet")
			err = common.ErrNotStageable
		}
	}
	if err != nil {
		io.WriteString(tw, "\n")
		tw.Flush()
		return errs.NewErrorWithHint(task, err, details.String())
	}
	return nil
}
Пример #9
0
func getAndPourSkeleton(skeleton string) error {
	// Get or update given skeleton.
	task := fmt.Sprintf("Get or update skeleton '%v'", skeleton)
	log.Run(task)
	if err := getOrUpdateSkeleton(flagSkeleton); err != nil {
		return errs.NewError(task, err)
	}

	// Move the skeleton files into place.
	task = "Copy the skeleton into the configuration directory"
	log.Go(task)

	localConfigDir, err := config.LocalConfigDirectoryAbsolutePath()
	if err != nil {
		return errs.NewError(task, err)
	}

	log.NewLine("")
	if err := pourSkeleton(flagSkeleton, localConfigDir); err != nil {
		return errs.NewError(task, err)
	}
	log.NewLine("")
	log.Ok(task)

	return nil
}
Пример #10
0
func FindMilestoneByTitle(
	client *github.Client,
	owner string,
	repo string,
	title string,
) (*github.Milestone, error) {

	// Fetch milestones for the given repository.
	task := fmt.Sprintf("Search for GitHub milestone '%v'", title)
	log.Run(task)
	milestones, _, err := client.Issues.ListMilestones(owner, repo, nil)
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	// Find the right one.
	for _, milestone := range milestones {
		if *milestone.Title == title {
			return &milestone, nil
		}
	}

	// Milestone not found.
	return nil, nil
}
Пример #11
0
func CreateMilestone(
	client *github.Client,
	owner string,
	repo string,
	title string,
) (*github.Milestone, action.Action, error) {

	// Create the milestone.
	milestoneTask := fmt.Sprintf("Create GitHub milestone '%v'", title)
	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 milestone '%v'", title)
		_, err := client.Issues.DeleteMilestone(owner, repo, *milestone.Number)
		if err != nil {
			return errs.NewError(task, err)
		}
		return nil
	}), nil
}
Пример #12
0
func (release *nextRelease) Start() (action.Action, error) {
	var (
		client         = release.client
		productId      = release.tracker.config.ProductId()
		itemReleaseTag = getItemReleaseTag(release.trunkVersion)
	)

	// Add the release tag to the relevant Sprintly items.
	task := "Tag relevant items with the release tag"
	log.Run(task)
	items, err := addTag(client, productId, release.additionalItems, itemReleaseTag)
	if err != nil {
		return nil, errs.NewError(task, err)
	}
	release.additionalItems = nil

	// Return the rollback action, which removes the release tags that were added.
	return action.ActionFunc(func() error {
		log.Rollback(task)
		_, err := removeTag(client, productId, items, itemReleaseTag)
		if err != nil {
			return errs.NewError("Remove the release tag from relevant items", err, nil)
		}
		return nil
	}), nil
}
Пример #13
0
func (r *release) EnsureClosable() error {
	// Prepare for API calls.
	_, owner, repo, err := r.prepareForApiCalls()
	if err != nil {
		return err
	}

	// Get the relevant review milestone.
	releaseString := r.v.BaseString()
	task := fmt.Sprintf("Get GitHub review milestone for release %v", releaseString)
	log.Run(task)
	milestone, err := milestoneForVersion(r.tool.config, owner, repo, r.v)
	if err != nil {
		return errs.NewError(task, err)
	}
	if milestone == nil {
		return errs.NewErrorWithHint(task, errors.New("milestone not found"),
			fmt.Sprintf("\nMake sure the review milestone for release %v exists\n\n", r.v))
	}

	// 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 {
		hint := fmt.Sprintf(
			"\nreview milestone for release %v cannot be closed: %v issue(s) open\n\n",
			releaseString, num)
		return errs.NewErrorWithHint(task, common.ErrNotClosable, hint)
	}
	r.closingMilestone = milestone
	return nil
}
Пример #14
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
}
Пример #15
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
}
Пример #16
0
func (tool *codeReviewTool) InitialiseRelease(v *version.Version) (action.Action, error) {
	// Get necessary config.
	config, err := LoadConfig()
	if err != nil {
		return nil, err
	}

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

	// Check whether the review milestone exists or not.
	// People can create milestones manually, so this makes the thing more robust.
	task := fmt.Sprintf("Check whether GitHub review milestone exists for release %v", v.BaseString())
	log.Run(task)
	milestone, err := milestoneForVersion(config, owner, repo, v)
	if err != nil {
		return nil, errs.NewError(task, err)
	}
	if milestone != nil {
		// Milestone already exists, we are done.
		log.Log(fmt.Sprintf("GitHub review milestone '%v' already exists", milestoneTitle(v)))
		return nil, nil
	}

	// Create the review milestone.
	_, act, err := createMilestone(config, owner, repo, v)
	return act, err
}
Пример #17
0
func (release *runningRelease) EnsureStageable() error {
	versionString := release.releaseVersion.BaseString()

	var task = fmt.Sprintf(
		"Make sure that release %v can be staged", versionString)
	log.Run(task)

	var details bytes.Buffer
	tw := tabwriter.NewWriter(&details, 0, 8, 4, '\t', 0)
	io.WriteString(tw, "\n")
	io.WriteString(tw, "Issue Key\tError\n")
	io.WriteString(tw, "=========\t=====\n")

	var err error
	for _, issue := range release.issues {
		if ex := ensureStageableIssue(issue); ex != nil {
			fmt.Fprintf(tw, "%v\t%v\n", issue.Key, ex)
			err = common.ErrNotStageable
		}
	}

	if err != nil {
		io.WriteString(tw, "\n")
		tw.Flush()
		return errs.NewErrorWithHint(task, err, details.String())
	}
	return nil
}
Пример #18
0
func (release *runningRelease) Stage() (action.Action, error) {

	var (
		api           = newClient(release.tracker.config)
		versionString = release.releaseVersion.BaseString()
		stageTask     = fmt.Sprintf("Stage JIRA issues associated with release %v", versionString)
	)
	log.Run(stageTask)

	// Make sure we only try to stage the issues that are in Tested.
	var issuesToStage []*jira.Issue
	for _, issue := range release.issues {
		if issue.Fields.Status.Id == stateIdTested {
			issuesToStage = append(issuesToStage, issue)
		}
	}

	// Perform the transition.
	err := performBulkTransition(api, issuesToStage, transitionIdStage, transitionIdUnstage)
	if err != nil {
		return nil, errs.NewError(stageTask, err)
	}

	return action.ActionFunc(func() error {
		log.Rollback(stageTask)
		unstageTask := fmt.Sprintf("Unstage JIRA issues associated with release %v", versionString)
		if err := performBulkTransition(api, issuesToStage, transitionIdUnstage, ""); err != nil {
			return errs.NewError(unstageTask, err)
		}
		return nil
	}), nil
}
Пример #19
0
func (release *nextRelease) Start() (action.Action, error) {
	// In case there are no additional stories, we are done.
	if len(release.additionalStories) == 0 {
		return action.Noop, nil
	}

	// Add release labels to the relevant stories.
	var (
		config    = release.tracker.config
		client    = pivotal.NewClient(config.UserToken)
		projectId = config.ProjectId
	)

	task := "Label the stories with the release label"
	log.Run(task)
	releaseLabel := getReleaseLabel(release.trunkVersion)
	stories, err := addLabel(client, projectId,
		release.additionalStories, releaseLabel)
	if err != nil {
		return nil, errs.NewError(task, err)
	}
	release.additionalStories = nil

	// Return the rollback action, which removes the release labels that were appended.
	return action.ActionFunc(func() error {
		log.Rollback(task)
		_, err := removeLabel(client, projectId, stories, releaseLabel)
		if err != nil {
			return errs.NewError("Remove the release label from the stories", err)
		}
		return nil
	}), nil
}
Пример #20
0
func rebase(currentBranch, parentBranch string) ([]*git.Commit, error) {
	// Tell the user what is happening.
	task := fmt.Sprintf("Rebase branch '%v' onto '%v'", currentBranch, parentBranch)
	log.Run(task)

	// Do the rebase.
	if err := git.Rebase(parentBranch); err != nil {
		ex := errs.Log(errs.NewError(task, err))
		asciiart.PrintGrimReaper("GIT REBASE FAILED")
		fmt.Printf(`Git failed to rebase your branch onto '%v'.

The repository might have been left in the middle of the rebase process.
In case you do not know how to handle this, just execute

  $ git rebase --abort

to make your repository clean again.

In any case, you have to rebase your current branch onto '%v'
if you want to continue and post a review request. In the edge cases
you can as well use -no_rebase to skip this step, but try not to do it.
`, parentBranch, parentBranch)
		return nil, ex
	}

	// Reload the commits.
	task = "Get the commits to be posted for code review, again"
	commits, err := git.ShowCommitRange(parentBranch + "..")
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	// Return new commits.
	return commits, nil
}
Пример #21
0
func EnsureDirectoryExists(path string) (action.Action, error) {
	// Check whether the directory exists already.
	task := fmt.Sprintf("Check whether '%v' exists and is a directory", path)
	info, err := os.Stat(path)
	if err != nil {
		if !os.IsNotExist(err) {
			return nil, errs.NewError(task, err)
		}
	} else {
		// In case the path exists, make sure it is a directory.
		if !info.IsDir() {
			return nil, errs.NewError(task, errors.New("not a directory: "+path))
		}
		// We are done.
		return action.Noop, nil
	}

	// Now we know that path does not exist, so we need to create it.
	createTask := fmt.Sprintf("Create directory '%v'", path)
	log.Run(createTask)
	if err := os.MkdirAll(path, 0755); err != nil {
		return nil, errs.NewError(createTask, err)
	}

	return action.ActionFunc(func() error {
		log.Rollback(createTask)
		task := fmt.Sprintf("Remove directory '%v'", path)
		if err := os.RemoveAll(path); err != nil {
			return errs.NewError(task, err)
		}
		return nil
	}), nil
}
Пример #22
0
// Start is a part of common.NextRelease interface.
func (release *nextRelease) Start() (act action.Action, err error) {
	// In case there are no additional issues, we are done.
	if len(release.additionalIssues) == 0 {
		return action.Noop, nil
	}

	// Set milestone for the additional issues.
	task := fmt.Sprintf(
		"Set milestone to '%v' for the issues added automatically",
		release.trunkVersion.BaseString())
	log.Run(task)

	// Get the milestone corresponding to the release branch version string.
	chain := action.NewActionChain()
	defer chain.RollbackOnError(&err)

	milestone, act, err := release.tracker.getOrCreateMilestone(release.trunkVersion)
	if err != nil {
		return nil, errs.NewError(task, err)
	}
	chain.Push(act)

	// Update the issues.
	issues, act, err := release.tracker.updateIssues(
		release.additionalIssues, setMilestone(milestone), unsetMilestone())
	if err != nil {
		return nil, errs.NewError(task, err)
	}
	chain.Push(act)
	release.additionalIssues = issues

	return chain, nil
}
Пример #23
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
}
Пример #24
0
func ensureRbtVersion() error {
	hint := `
You need to install RBTools version 0.7. Please run

  $ pip install rbtools==0.7 --allow-external rbtools --allow-unverified rbtools

to install the correct version.

`

	// Load configuration and check the RBTools version only if Review Board is being used.
	config, err := common.LoadConfig()
	if err != nil {
		return err
	}
	if config.CodeReviewToolId() != Id {
		return nil
	}

	// Check the RBTools version being used.
	task := "Check the RBTools version being used"
	log.Run(task)

	// rbt 0.5.x prints the version string to stdout,
	// rbt 0.6.x prints the version string to stderr.
	stdout, stderr, err := shell.Run("rbt", "--version")
	if err != nil {
		// Return the hint instead of stderr.
		// Failing to run rbt --version probably means that it's not installed.
		return errs.NewErrorWithHint(task, err, hint)
	}

	var outputBuffer *bytes.Buffer
	if stdout.Len() != 0 {
		outputBuffer = stdout
	} else {
		outputBuffer = stderr
	}
	output := outputBuffer.String()

	pattern := regexp.MustCompile("^RBTools (([0-9]+)[.]([0-9]+).*)")
	parts := pattern.FindStringSubmatch(output)
	if len(parts) != 4 {
		err := fmt.Errorf("failed to parse 'rbt --version' output: %v", output)
		return errs.NewError(task, err)
	}
	rbtVersion := parts[1]
	// No need to check errors, we know the format is correct.
	major, _ := strconv.Atoi(parts[2])
	minor, _ := strconv.Atoi(parts[3])

	if !(major == 0 && minor == 7) {
		return errs.NewErrorWithHint(
			task, errors.New("unsupported rbt version detected: "+rbtVersion), hint)
	}

	return nil
}
Пример #25
0
func listItemsByTag(
	client *sprintly.Client,
	productId int,
	tags []string,
) ([]sprintly.Item, error) {

	task := "Fetch Sprintly items by tag"
	log.Run(task)

	// Since Sprintly API is not exactly powerful, we need to get all the items.
	// Then we need to locally pair stories with their sub-items.
	itms, err := listItems(client, productId, &sprintly.ItemListArgs{
		Status: []sprintly.ItemStatus{
			sprintly.ItemStatusSomeday,
			sprintly.ItemStatusBacklog,
			sprintly.ItemStatusInProgress,
			sprintly.ItemStatusCompleted,
		},
	})

	var (
		items     []sprintly.Item
		itemIndex = make(map[int]struct{}, 0)
	)
	for _, item := range itms {
		// In case the tag matches, add the item to the list.
		// Also add the item to the index of potential parent items.
		for _, tag := range tags {
			if tagged(&item, tag) {
				items = append(items, item)
				itemIndex[item.Number] = struct{}{}
			}
		}
	}
	for _, item := range itms {
		// In case the parent is not empty and it matches an item
		// that is already in the list, add the current item to the list as well,
		// but only if the item is not there yet.
		number, err := item.ParentNumber()
		if err != nil {
			return nil, errs.NewError(task, err)
		}
		if number != 0 {
			if _, ok := itemIndex[number]; ok {
				if _, ok := itemIndex[item.Number]; !ok {
					items = append(items, item)
				}
			}
		}
	}

	if err != nil {
		return nil, errs.NewError(task, err)
	}
	return items, nil
}
Пример #26
0
func checkCommits(
	tracker common.IssueTracker,
	release common.RunningRelease,
	releaseBranch string,
) error {

	var task = "Make sure no changes are being left behind"
	log.Run(task)

	stories, err := release.Stories()
	if err != nil {
		return errs.NewError(task, err)
	}
	if len(stories) == 0 {
		return nil
	}

	groups, err := changes.StoryChanges(stories)
	if err != nil {
		return errs.NewError(task, err)
	}

	toCherryPick, err := releases.StoryChangesToCherryPick(groups)
	if err != nil {
		return errs.NewError(task, err)
	}

	// In case there are some changes being left behind,
	// ask the user to confirm whether to proceed or not.
	if len(toCherryPick) == 0 {
		return nil
	}

	fmt.Println(`
Some changes are being left behind!

In other words, some changes that are assigned to the current release
have not been cherry-picked onto the release branch yet.
	`)
	if err := changes.DumpStoryChanges(os.Stdout, toCherryPick, tracker, false); err != nil {
		panic(err)
	}
	fmt.Println()

	confirmed, err := prompt.Confirm("Are you sure you really want to stage the release?", false)
	if err != nil {
		return errs.NewError(task, err)
	}
	if !confirmed {
		prompt.PanicCancel()
	}
	fmt.Println()

	return nil
}
Пример #27
0
func runMain(storyId string) error {
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return err
	}

	task := fmt.Sprintf("Open story %s", storyId)
	log.Run(task)
	if err := tracker.OpenStory(storyId); err != nil {
		return errs.NewError(task, err)
	}
	return nil
}
Пример #28
0
func (release *runningRelease) Stage() (action.Action, error) {
	stageTask := fmt.Sprintf(
		"Mark the stories as %v in Pivotal Tracker", pivotal.StoryStateDelivered)
	log.Run(stageTask)

	// Load the assigned stories.
	stories, err := release.loadStories()
	if err != nil {
		return nil, errs.NewError(stageTask, err)
	}

	// Pick only the stories that are in the right state.
	ss := make([]*pivotal.Story, 0, len(stories))
	for _, s := range stories {
		if release.tracker.canStoryBeStaged(s) {
			ss = append(ss, s)
		}
	}
	stories = ss

	// Mark the selected stories as delivered. Leave the labels as they are.
	updateRequest := &pivotal.StoryRequest{State: pivotal.StoryStateDelivered}
	updateFunc := func(story *pivotal.Story) *pivotal.StoryRequest {
		return updateRequest
	}
	// On rollback, set the story state to finished again.
	rollbackFunc := func(story *pivotal.Story) *pivotal.StoryRequest {
		return &pivotal.StoryRequest{State: pivotal.StoryStateFinished}
	}

	// Update the stories.
	updatedStories, err := release.tracker.updateStories(stories, updateFunc, rollbackFunc)
	if err != nil {
		return nil, errs.NewError(stageTask, err)
	}
	release.stories = updatedStories

	// Return the rollback function.
	return action.ActionFunc(func() error {
		// On error, set the states back to the original ones.
		log.Rollback(stageTask)
		task := fmt.Sprintf("Reset the story states back to %v", pivotal.StoryStateFinished)
		updatedStories, err := release.tracker.updateStories(release.stories, rollbackFunc, nil)
		if err != nil {
			return errs.NewError(task, err)
		}
		release.stories = updatedStories
		return nil
	}), nil
}
Пример #29
0
func (release *runningRelease) loadStories() ([]*pivotal.Story, error) {
	// Fetch the stories unless cached.
	if release.stories == nil {
		task := "Fetch data from Pivotal Tracker"
		log.Run(task)
		stories, err := release.tracker.storiesByRelease(release.version)
		if err != nil {
			return nil, errs.NewError(task, err)
		}
		release.stories = stories
	}

	// Return the cached stories.
	return release.stories, nil
}
Пример #30
0
func newRunningRelease(
	tracker *issueTracker,
	releaseVersion *version.Version,
) (*runningRelease, error) {

	// Fetch relevant issues from JIRA.
	task := fmt.Sprintf("Fetch issues for release '%v'", releaseVersion)
	log.Run(task)
	issues, err := tracker.issuesByRelease(releaseVersion)
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	// Return a new release instance.
	return &runningRelease{tracker, releaseVersion, issues}, nil
}