Beispiel #1
0
// CheckOrCreateTrackingBranch tries to make sure that a local branch
// of the given name exists and is in sync with the given remote.
//
// So, in case the right remote branch exists and the local does not,
// the local tracking branch is created. In case the local branch
// exists already, it is ensured that it is up to date.
//
// In case the remote branch does not exist, *ErrRefNotFound is returned.
// In case the branch is not up to date, *ErrRefNotInSync is returned.
func CheckOrCreateTrackingBranch(branch, remote string) error {
	// Get the data on the local branch.
	localExists, err := LocalBranchExists(branch)
	if err != nil {
		return err
	}

	// Check whether the remote counterpart exists.
	remoteExists, err := RemoteBranchExists(branch, remote)
	if err != nil {
		return err
	}
	if !remoteExists {
		if localExists {
			log.Warn(fmt.Sprintf(
				"Local branch '%v' found, but the remote counterpart is missing", branch))
			log.NewLine(fmt.Sprintf(
				"Please delete or push local branch '%v'", branch))
		}
		return &ErrRefNotFound{remote + "/" + branch}
	}

	// Check whether the local branch exists.
	if !localExists {
		return CreateTrackingBranch(branch, remote)
	}

	// In case it exists, make sure that it is up to date.
	return EnsureBranchSynchronized(branch, remote)
}
Beispiel #2
0
func splitBranchesNotInSync(storyBranches []*git.GitBranch) ([]*git.GitBranch, error) {
	branches := make([]*git.GitBranch, 0, len(storyBranches))
	for _, branch := range storyBranches {
		upToDate, err := branch.IsUpToDate()
		if err != nil {
			return nil, err
		}
		if upToDate {
			branches = append(branches, branch)
			continue
		}

		// In case the branch is not up to date, we split the local and remote
		// reference into their own branch records to treat them separately.
		var (
			branchName       = branch.BranchName
			remoteBranchName = branch.RemoteBranchName
			remote           = branch.Remote
		)
		log.Warn(fmt.Sprintf("Branch '%s' is not up to date", branchName))
		log.NewLine(fmt.Sprintf("Treating '%v' and '%v/%v' as separate branches",
			branchName, remote, remoteBranchName))

		localBranch := &git.GitBranch{
			BranchName: branchName,
		}
		remoteBranch := &git.GitBranch{
			RemoteBranchName: remoteBranchName,
			Remote:           remote,
		}
		branches = append(branches, localBranch, remoteBranch)
	}
	return branches, nil
}
Beispiel #3
0
// StoryChanges returns the list of changes grouped by Story-Id.
func StoryChanges(stories []common.Story) ([]*StoryChangeGroup, error) {
	// Prepare the regexp to use to select commits by commit messages.
	// This regexp is ORing the chosen Story-Id tag values.
	var grepFlag bytes.Buffer
	fmt.Fprintf(&grepFlag, "^Story-Id: (%v", stories[0].Tag())
	for _, story := range stories[1:] {
		fmt.Fprintf(&grepFlag, "|%v", story.Tag())
	}
	fmt.Fprint(&grepFlag, ")$")

	// Get the relevant commits.
	commits, err := git.GrepCommitsCaseInsensitive(grepFlag.String(), "--all")
	if err != nil {
		return nil, err
	}

	okCommits := make([]*git.Commit, 0, len(commits))
	for _, commit := range commits {
		if commit.StoryIdTag == "" {
			log.Warn(fmt.Sprintf(
				"Found story commit %v, but failed to parse the Story-Id tag.", commit.SHA))
			log.NewLine("Please check that commit manually.")
			continue
		}
		okCommits = append(okCommits, commit)
	}
	commits = okCommits

	// Return the change groups.
	return StoryChangesFromCommits(commits)
}
Beispiel #4
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
}
Beispiel #5
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
}
Beispiel #6
0
func bootstrapLocalConfig(spec ConfigSpec) error {
	task := "Bootstrap local configuration according to the spec"

	// Some handy variables.
	configKey := spec.ConfigKey()
	container := spec.LocalConfig()
	moduleSpec, isModuleSpec := spec.(ModuleConfigSpec)

	// Pre-read local config.
	// It is needed the pre-write hook as well.
	local, err := config.ReadLocalConfig()
	readConfig := func() (configFile, error) {
		return local, err
	}

	// Handle module config specs a bit differently.
	var preWriteHook func() (bool, error)
	if isModuleSpec {
		// Make sure the config container is not nil in case this is a module config.
		// In case the local config container is nil, the pre-write hook is not executed
		// and the active module ID is not set, and that would be a problem.
		// Returning a nil local config container is a valid choice, but we still
		// need the pre-write hook to be executed to set the active module ID.
		if container == nil {
			container = newEmptyModuleConfigContainer(configKey, moduleSpec.ModuleKind())
		}

		// In case this is a module config spec, set the the pre-write hook
		// to modify the local config file to activate the module being configured.
		preWriteHook = func() (bool, error) {
			return SetActiveModule(local, moduleSpec.ModuleKind(), configKey)
		}
	}

	// The post-write hook simply tells the user to commit the local config file.
	postWriteHook := func() error {
		fmt.Println()
		log.Warn("Local configuration file modified, please commit it.")
		return nil
	}

	// Run the common loading function with the right arguments.
	if err := load(&loadArgs{
		configKind:      "local",
		configKey:       configKey,
		configContainer: container,
		readConfig:      readConfig,
		emptyConfig:     emptyLocalConfig,
		preWriteHook:    preWriteHook,
		postWriteHook:   postWriteHook,
	}); err != nil {
		return errs.NewError(task, err)
	}
	return nil
}
Beispiel #7
0
// Stage can be used to stage the items associated with this release.
//
// The rollback function is a NOOP in this case since there is no way
// how to delete a deployment once created in Sprintly.
func (release *runningRelease) Stage() (action.Action, error) {
	task := "Ping Sprintly to register the deployment"
	log.Run(task)

	// Create the Sprintly deployment.
	if err := release.deploy(release.config.StagingEnvironment()); err != nil {
		return nil, errs.NewError(task, err)
	}

	// Return the rollback function, which is empty in this case.
	return action.ActionFunc(func() error {
		log.Rollback(task)
		log.Warn("It is not possible to delete a Sprintly deployment, skipping ...")
		return nil
	}), nil

}
Beispiel #8
0
func collectStoryBranches(remoteName string) ([]*git.GitBranch, error) {
	// Load Git branches.
	branches, err := git.Branches()
	if err != nil {
		return nil, err
	}

	// Get the current branch name so that it can be excluded.
	currentBranch, err := gitutil.CurrentBranch()
	if err != nil {
		return nil, err
	}

	// Filter the branches.
	storyBranches := make([]*git.GitBranch, 0, len(branches))
	for _, branch := range branches {
		// Drop branches not corresponding to the project remote.
		if branch.Remote != "" && branch.Remote != remoteName {
			continue
		}

		var (
			isLocalStoryBranch  = strings.HasPrefix(branch.BranchName, StoryBranchPrefix)
			isRemoteStoryBranch = strings.HasPrefix(branch.RemoteBranchName, StoryBranchPrefix)
		)

		// Exclude the current branch.
		if isLocalStoryBranch && branch.BranchName == currentBranch {
			log.Warn(fmt.Sprintf("Branch '%v' is checked out, it cannot be deleted", currentBranch))
			continue
		}

		// Keep the story branches only.
		if isLocalStoryBranch || isRemoteStoryBranch {
			storyBranches = append(storyBranches, branch)
		}
	}

	// Return the result.
	return storyBranches, nil
}
Beispiel #9
0
func (release *runningRelease) Release() error {
	// TODO: Get rid of this unholy ugliness.
	if release.issues == nil {
		panic("bug(release.issues == nil)")
	}

	// Release all issues that are accepted.
	issues := make([]*jira.Issue, 0, len(release.issues))
	for _, issue := range release.issues {
		if issue.Fields.Status.Id == stateIdAccepted {
			issues = append(issues, issue)
		}
	}
	if len(issues) == 0 {
		log.Warn("No accepted stories found in JIRA")
		return nil
	}

	return performBulkTransition(
		newClient(release.tracker.config), issues, transitionIdRelease, "")
}
Beispiel #10
0
func commitsToReviewContexts(commits []*git.Commit) ([]*common.ReviewContext, error) {
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return nil, err
	}

	// Fetch the stories from the issue tracker.
	tags := storyTags(tracker, commits)
	stories, err := tracker.ListStoriesByTag(tags)
	if err != nil {
		return nil, err
	}

	// Build the story map.
	storiesByTag := make(map[string]common.Story, 1)
	for i, story := range stories {
		tag := tags[i]
		if story == nil {
			log.Warn(fmt.Sprintf("Story for tag '%v' was not found in the issue tracker", tag))
			continue
		}
		storiesByTag[tag] = story
	}

	// Build the final list of review contexts.
	ctxs := make([]*common.ReviewContext, 0, len(commits))
	for _, commit := range commits {
		// Story can be set to nil here in case the story is unassigned.
		// In that case there will be, obviously, no story object in the map.
		ctxs = append(ctxs, &common.ReviewContext{
			Commit: commit,
			Story:  storiesByTag[commit.StoryIdTag],
		})
	}

	// Return the commit review contexts.
	return ctxs, nil
}
Beispiel #11
0
// copyFileContents copies the contents of the file named src to the file named
// by dst. The file will be created if it does not already exist. If the
// destination file exists, all it's contents will be replaced by the contents
// of the source file.
func copyFileContents(src, dst string) (err error) {
	in, err := os.Open(src)
	if err != nil {
		return
	}
	defer in.Close()
	out, err := os.Create(dst)
	if err != nil {
		return
	}
	defer func() {
		cerr := out.Close()
		if err == nil {
			err = cerr
		} else {
			log.Warn(cerr.Error())
		}
	}()
	if _, err = io.Copy(out, in); err != nil {
		return
	}
	err = out.Sync()
	return
}
Beispiel #12
0
// PromptUserToConfirm is a part of common.NextRelease interface.
func (release *nextRelease) PromptUserToConfirmStart() (bool, error) {
	// Fetch the stories already assigned to the release.
	var (
		ver       = release.trunkVersion
		verString = ver.BaseString()
	)
	task := fmt.Sprintf("Fetch GitHub issues already assigned to release %v", verString)
	log.Run(task)
	assignedIssues, err := release.tracker.issuesByRelease(ver)
	if err != nil {
		return false, errs.NewError(task, err)
	}

	// Collect the issues that modified trunk since the last release.
	task = "Collect the issues that modified trunk since the last release"
	log.Run(task)
	issueNumsString, err := releases.ListStoryIdsToBeAssigned(release.tracker)
	if err != nil {
		return false, errs.NewError(task, err)
	}

	// Turn []string into []int.
	issueNums := make([]int, len(issueNumsString))
	for i, numString := range issueNumsString {
		// numString is #ISSUE_NUMBER.
		num, err := strconv.Atoi(numString[1:])
		if err != nil {
			panic(err)
		}
		issueNums[i] = num
	}

	// Drop the stories that are already assigned.
	numSet := make(map[int]struct{}, len(assignedIssues))
	for _, issue := range assignedIssues {
		numSet[*issue.Number] = struct{}{}
	}
	nums := make([]int, 0, len(issueNums))
	for _, num := range issueNums {
		if _, ok := numSet[num]; !ok {
			nums = append(nums, num)
		}
	}
	issueNums = nums

	// Fetch the collected issues from GitHub, if necessary.
	var additionalIssues []*github.Issue
	if len(issueNums) != 0 {
		task = "Fetch the collected issues from GitHub"
		log.Run(task)

		var err error
		additionalIssues, err = release.tracker.issuesByNumber(issueNums)
		if err != nil {
			return false, errs.NewError(task, err)
		}

		// Drop stories already assigned to another release.
		notAssigned := make([]*github.Issue, 0, len(additionalIssues))
		for _, issue := range additionalIssues {
			switch {
			case issue.Milestone == nil:
				notAssigned = append(notAssigned, issue)
			default:
				log.Warn(fmt.Sprintf(
					"Skipping issue #%v: modified trunk, but already assigned to milestone '%v'",
					*issue.Number, *issue.Milestone.Title))
			}
		}
		additionalIssues = notAssigned
	}

	// Print the summary into the console.
	summary := []struct {
		header string
		issues []*github.Issue
	}{
		{
			"The following issues were manually assigned to the release:",
			assignedIssues,
		},
		{
			"The following issues were added automatically (modified trunk):",
			additionalIssues,
		},
	}
	for _, item := range summary {
		if len(item.issues) != 0 {
			fmt.Println()
			fmt.Println(item.header)
			fmt.Println()
			err := storyprompt.ListStories(
				toCommonStories(item.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?", verString), false)
	if err == nil {
		release.additionalIssues = additionalIssues
	}
	return ok, err
}
Beispiel #13
0
func filterBranches(storyBranches []*git.GitBranch, trunkName string) ([]*gitBranch, error) {
	// Pair the branches with commit ranges specified by trunk..story
	task := "Collected commits associated with the story branches"
	branches := make([]*gitBranch, 0, len(storyBranches))
	for _, branch := range storyBranches {
		var revRange string
		if branch.BranchName != "" {
			// Handle branches that exist locally.
			revRange = fmt.Sprintf("%v..%v", trunkName, branch.BranchName)
		} else {
			// Handle branches that exist only in the remote repository.
			// We can use trunkName here since trunk is up to date.
			revRange = fmt.Sprintf("%v..%v/%v", trunkName, branch.Remote, branch.RemoteBranchName)
		}

		commits, err := git.ShowCommitRange(revRange)
		if err != nil {
			return nil, errs.NewError(task, err)
		}
		branches = append(branches, &gitBranch{
			tip:     branch,
			commits: commits,
		})
		continue
	}

	// Collect story tags.
	task = "Collect affected story tags"
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	tags := make([]string, 0, len(storyBranches))
BranchLoop:
	for _, branch := range branches {
		for _, commit := range branch.commits {
			commitTag := commit.StoryIdTag

			// Make sure the tag is not in the list already.
			for _, tag := range tags {
				if tag == commitTag {
					continue BranchLoop
				}
			}

			// Drop tags not recognized by the current issue tracker.
			_, err := tracker.StoryTagToReadableStoryId(commitTag)
			if err == nil {
				tags = append(tags, commitTag)
			}
		}
	}

	// Fetch the collected stories.
	task = "Fetch associated stories from the issue tracker"
	log.Run(task)
	stories, err := tracker.ListStoriesByTag(tags)
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	// Filter the branches according to the story state.
	storyByTag := make(map[string]common.Story, len(stories))
	for i, story := range stories {
		// tags[i] corresponds to stories[i]
		tag := tags[i]
		if story != nil {
			storyByTag[tag] = story
		} else {
			log.Warn(fmt.Sprintf("Story for tag '%v' was not found in the issue tracker", tag))
		}
	}

	allowedStates := allowedStoryStates()

	// checkCommits returns whether the commits passed in are ok
	// considering the state of the stories found in these commits,
	// whether the branch containing these commits can be deleted.
	checkCommits := func(commits []*git.Commit) (common.StoryState, bool) {
		var storyFound bool
		for _, commit := range commits {
			// Skip commits with empty Story-Id tag.
			if commit.StoryIdTag == "" {
				continue
			}

			// In case the story is not found, the tag is not recognized
			// by the current issue tracker. In that case we just skip the commit.
			story, ok := storyByTag[commit.StoryIdTag]
			if !ok {
				continue
			}

			// When the story state associated with the commit is not ok,
			// we can return false here to reject the branch.
			storyState := story.State()
			if _, ok := allowedStates[storyState]; !ok {
				return storyState, false
			}

			storyFound = true
		}

		// We went through all the commits and they are fine, check passed.
		return common.StoryStateInvalid, storyFound
	}

	// Go through the branches and only return these that
	// comply with the story state requirements.
	bs := make([]*gitBranch, 0, len(branches))
	for _, branch := range branches {
		tip := branch.tip

		logger := log.V(log.Verbose)
		if logger {
			logger.Log(fmt.Sprintf("Processing branch %v", tip.CanonicalName()))
		}

		// The branch can be for sure deleted in case there are no commits
		// contained in the commit range. That means the branch is merged into trunk.
		if len(branch.commits) == 0 {
			if logger {
				logger.Log("  Include the branch (reason: merged into trunk)")
			}
			branch.reason = "merged"
			bs = append(bs, branch)
			continue
		}

		// In case the commit check passed, we append the branch.
		state, ok := checkCommits(branch.commits)
		if ok {
			if logger {
				logger.Log("  Include the branch (reason: branch check passed)")
			}
			branch.reason = "check passed"
			bs = append(bs, branch)
			continue
		}

		// Otherwise we print the skip warning.
		if logger {
			if state == common.StoryStateInvalid {
				logger.Log(
					"  Exclude the branch (reason: no story commits found on the branch)")
			} else {
				logger.Log(fmt.Sprintf(
					"  Exclude the branch (reason: story state is '%v')", state))
			}
		}
	}

	return bs, nil
}
Beispiel #14
0
// pourSkeleton counts on the fact that skeleton is a valid skeleton
// that is available in the local cache directory.
func pourSkeleton(skeletonName string, localConfigDir string) (err error) {
	// Get the skeleton src path.
	cacheDir, err := cacheDirectoryAbsolutePath()
	if err != nil {
		return err
	}
	skeletonDir := filepath.Join(cacheDir, "github.com", skeletonName)

	// Make sure src is a directory, just to be sure.
	skeletonInfo, err := os.Stat(skeletonDir)
	if err != nil {
		return err
	}
	if !skeletonInfo.IsDir() {
		return fmt.Errorf("skeleton source path not a directory: %v", skeletonDir)
	}

	// Get the list of script files.
	srcScriptsDir := filepath.Join(skeletonDir, "scripts")
	scripts, err := filepath.Glob(srcScriptsDir + "/*")
	if err != nil {
		return err
	}
	if len(scripts) == 0 {
		log.Warn("No script files found in the skeleton repository")
		return nil
	}

	// Create the destination directory.
	dstScriptsDir := filepath.Join(localConfigDir, "scripts")
	if err := os.MkdirAll(dstScriptsDir, 0755); err != nil {
		return err
	}
	// Delete the directory on error.
	defer action.RollbackOnError(&err, action.ActionFunc(func() error {
		log.Rollback("Create the local scripts directory")
		if err := os.RemoveAll(dstScriptsDir); err != nil {
			return errs.NewError("Remove the local scripts directory", err)
		}
		return nil
	}))

	for _, script := range scripts {
		err := func(script string) error {
			// Skip directories.
			scriptInfo, err := os.Stat(script)
			if err != nil {
				return err
			}
			if scriptInfo.IsDir() {
				return nil
			}

			// Copy the file.
			filename := script[len(srcScriptsDir)+1:]
			fmt.Println("---> Copy", filepath.Join("scripts", filename))

			srcFd, err := os.Open(script)
			if err != nil {
				return err
			}
			defer srcFd.Close()

			dstPath := filepath.Join(dstScriptsDir, filename)
			dstFd, err := os.OpenFile(
				dstPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, scriptInfo.Mode())
			if err != nil {
				return err
			}
			defer dstFd.Close()

			_, err = io.Copy(dstFd, srcFd)
			return err
		}(script)
		if err != nil {
			return err
		}

	}

	return nil
}
Beispiel #15
0
func (release *nextRelease) PromptUserToConfirmStart() (bool, error) {
	// Fetch the stories already assigned to the release.
	var (
		ver       = release.trunkVersion
		verString = ver.BaseString()
		verLabel  = ver.ReleaseTagString()
	)
	task := fmt.Sprintf("Fetch the stories already assigned to release %v", verString)
	log.Run(task)
	assignedStories, err := release.tracker.storiesByRelease(ver)
	if err != nil {
		return false, errs.NewError(task, err)
	}

	// Collect the story IDs associated with the commits that
	// modified trunk since the last release.
	task = "Collect the stories that modified trunk since the last release"
	log.Run(task)
	storyIds, err := releases.ListStoryIdsToBeAssigned(release.tracker)
	if err != nil {
		return false, errs.NewError(task, err)
	}

	// Drop the stories that are already assigned.
	idSet := make(map[string]struct{}, len(assignedStories))
	for _, story := range assignedStories {
		idSet[strconv.Itoa(story.Id)] = struct{}{}
	}
	ids := make([]string, 0, len(storyIds))
	for _, id := range storyIds {
		if _, ok := idSet[id]; !ok {
			ids = append(ids, id)
		}
	}
	storyIds = ids

	// Fetch the collected stories from Pivotal Tracker, if necessary.
	var additionalStories []*pivotal.Story
	if len(storyIds) != 0 {
		task = "Fetch the collected stories from Pivotal Tracker"
		log.Run(task)

		var err error
		additionalStories, err = release.tracker.storiesById(storyIds)
		if len(additionalStories) == 0 && err != nil {
			return false, errs.NewError(task, err)
		}
		if len(additionalStories) != len(storyIds) {
			log.Warn("Some stories were dropped since they were not found in PT")
			log.NewLine("or they were filtered out by a story include label.")
		}

		// Drop stories already assigned to another release.
		notAssigned := make([]*pivotal.Story, 0, len(additionalStories))
	NotAssignedLoop:
		for _, story := range additionalStories {
			for _, label := range story.Labels {
				if isReleaseLabel(label.Name) {
					log.Warn(fmt.Sprintf(
						"Skipping story %v: modified trunk, but already labeled '%v'",
						story.Id, label.Name))
					continue NotAssignedLoop
				}
			}
			notAssigned = append(notAssigned, story)
		}
		additionalStories = notAssigned
	}

	// Check the Point Me label.
	task = "Make sure there are no unpointed stories"
	log.Run(task)
	pmLabel := release.tracker.config.PointMeLabel

	// Fetch the already assigned but unpointed stories.
	pmStories, err := release.tracker.searchStories(
		"label:\"%v\" AND label:\"%v\"", verLabel, pmLabel)
	if err != nil {
		return false, errs.NewError(task, err)
	}
	// Also add these that are to be added but are unpointed.
	for _, story := range additionalStories {
		if labeled(story, pmLabel) {
			pmStories = append(pmStories, story)
		}
	}
	// In case there are some unpointed stories, stop the release.
	if len(pmStories) != 0 {
		fmt.Println("\nThe following stories are still yet to be pointed:\n")
		err := storyprompt.ListStories(toCommonStories(pmStories, release.tracker), os.Stdout)
		if err != nil {
			return false, err
		}
		fmt.Println()
		return false, errs.NewError(task, errors.New("unpointed stories detected"))
	}

	// Print the summary into the console.
	summary := []struct {
		header  string
		stories []*pivotal.Story
	}{
		{
			"The following stories were manually assigned to the release:",
			assignedStories,
		},
		{
			"The following stories were added automatically (modified trunk):",
			additionalStories,
		},
	}
	for _, item := range summary {
		if len(item.stories) != 0 {
			fmt.Println()
			fmt.Println(item.header)
			fmt.Println()
			err := storyprompt.ListStories(toCommonStories(item.stories, 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()), false)
	if err == nil {
		release.additionalStories = additionalStories
	}
	return ok, err
}
Beispiel #16
0
func (release *nextRelease) PromptUserToConfirmStart() (bool, error) {
	var (
		config       = release.tracker.config
		client       = pivotal.NewClient(config.UserToken())
		releaseLabel = getReleaseLabel(release.trunkVersion)
	)

	// Collect the commits that modified trunk since the last release.
	task := "Collect the stories that modified trunk"
	log.Run(task)
	ids, err := releases.ListStoryIdsToBeAssigned(release.tracker)
	if err != nil {
		return false, errs.NewError(task, err)
	}

	// Fetch the collected stories from Pivotal Tracker, if necessary.
	var additional []*pivotal.Story
	if len(ids) != 0 {
		task = "Fetch the collected stories from Pivotal Tracker"
		log.Run(task)

		var err error
		additional, err = listStoriesById(client, config.ProjectId(), ids)
		if len(additional) == 0 && err != nil {
			return false, errs.NewError(task, err)
		}
		if len(additional) != len(ids) {
			log.Warn("Some stories were dropped since they were not found in PT")
		}

		// Drop the issues that are already assigned to the right release.
		unassignedStories := make([]*pivotal.Story, 0, len(additional))
		for _, story := range additional {
			if labeled(story, releaseLabel) {
				continue
			}
			unassignedStories = append(unassignedStories, story)
		}
		additional = unassignedStories
	}

	// Check the Point Me label.
	task = "Make sure there are no unpointed stories"
	log.Run(task)
	pmLabel := config.PointMeLabel()

	// Fetch the already assigned but unpointed stories.
	pmStories, err := searchStories(client, config.ProjectId(),
		"label:\"%v\" AND label:\"%v\"", releaseLabel, pmLabel)
	if err != nil {
		return false, errs.NewError(task, err)
	}
	// Also add these that are to be added but are unpointed.
	for _, story := range additional {
		if labeled(story, pmLabel) {
			pmStories = append(pmStories, story)
		}
	}
	// In case there are some unpointed stories, stop the release.
	if len(pmStories) != 0 {
		fmt.Println("\nThe following stories are still yet to be pointed:\n")
		err := prompt.ListStories(toCommonStories(pmStories, release.tracker), os.Stdout)
		if err != nil {
			return false, err
		}
		fmt.Println()
		return false, errs.NewError(task, errors.New("unpointed stories detected"))
	}

	// Print the stories to be added to the release.
	if len(additional) != 0 {
		fmt.Println("\nThe following stories are going to be added to the release:\n")
		err := prompt.ListStories(toCommonStories(additional, 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.additionalStories = additional
	}
	return ok, err
}
Beispiel #17
0
func runMain(args []string) (err error) {
	// Get the commit hashes.
	// We need to do this before we checkout the target branch,
	// because relative refs like HEAD change by doing so.
	hashes, err := git.RevisionsToCommitList(args...)
	if err != nil {
		return err
	}

	// Load git-related config.
	gitConfig, err := git.LoadConfig()
	if err != nil {
		return err
	}
	var (
		remoteName    = gitConfig.RemoteName
		releaseBranch = gitConfig.ReleaseBranchName
	)

	// Get the target branch name.
	targetBranch := flagTarget
	if targetBranch == "" {
		targetBranch = releaseBranch
	}

	// Fetch the remote repository if requested.
	if flagFetch {
		task := "Fetch the remote repository"
		log.Run(task)
		if err := git.UpdateRemotes(remoteName); err != nil {
			return errs.NewError(task, err)
		}
	}

	// Make sure the target branch is up to date.
	if err := ensureTargetBranchExists(targetBranch, remoteName); err != nil {
		return err
	}

	// Get the current branch name.
	currentBranch, err := gitutil.CurrentBranch()
	if err != nil {
		return err
	}

	// Checkout the target branch in case we are not at it already.
	if currentBranch != targetBranch {
		task := fmt.Sprintf("Checkout branch '%v'", targetBranch)
		log.Run(task)
		if err := git.Checkout(targetBranch); err != nil {
			return errs.NewError(task, err)
		}
		// Checkout the current branch on return, unless there is an error.
		// In that case we want to stay on the target branch.
		defer func() {
			if err == nil {
				task := fmt.Sprintf("Checkout branch '%v'", currentBranch)
				log.Run(task)
				if ex := git.Checkout(currentBranch); ex != nil {
					err = errs.NewError(task, ex)
				}
			} else {
				log.Warn(fmt.Sprintf("An error detected, staying on branch '%v'", targetBranch))
			}
		}()
	}

	// Run git cherry-pick.
	task := fmt.Sprintf("Cherry-pick the chosen commits into '%v'", targetBranch)
	log.Run(task)
	if err := git.CherryPick(hashes...); err != nil {
		return errs.NewError(task, err)
	}

	log.Warn(fmt.Sprintf("Make sure to push branch '%v' to publish the changes", targetBranch))
	return nil
}
Beispiel #18
0
func runMain() (err error) {
	// Load repo config.
	gitConfig, err := git.LoadConfig()
	if err != nil {
		return err
	}

	var (
		remoteName    = gitConfig.RemoteName
		stagingBranch = gitConfig.StagingBranchName
		stableBranch  = gitConfig.StableBranchName
	)

	// Fetch the repository.
	if !flagNoFetch {
		task := "Fetch the remote repository"
		log.Run(task)
		if err := git.UpdateRemotes(remoteName); err != nil {
			return errs.NewError(task, err)
		}
	}

	// Check branches.
	checkBranch := func(branchName string) error {
		// Make sure the branch exists.
		task := fmt.Sprintf("Make sure that branch '%v' exists and is up to date", branchName)
		log.Run(task)
		if err := git.CheckOrCreateTrackingBranch(branchName, remoteName); err != nil {
			return errs.NewError(task, err)
		}

		// Make sure we are not on the branch.
		task = fmt.Sprintf("Make sure that branch '%v' is not checked out", branchName)
		log.Run(task)
		currentBranch, err := gitutil.CurrentBranch()
		if err != nil {
			return errs.NewError(task, err)
		}
		if currentBranch == branchName {
			err := fmt.Errorf("cannot deploy while on branch '%v'", branchName)
			return errs.NewError(task, err)
		}
		return nil
	}

	for _, branch := range []string{stableBranch, stagingBranch} {
		if err := checkBranch(branch); err != nil {
			return err
		}
	}

	// Make sure the current staging branch can be released.
	task := fmt.Sprintf("Make sure that branch '%v' can be released", stagingBranch)
	log.Run(task)
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return errs.NewError(task, err)
	}
	codeReviewTool, err := modules.GetCodeReviewTool()
	if err != nil {
		return errs.NewError(task, err)
	}

	stagingVersion, err := version.GetByBranch(stagingBranch)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Make sure the release can be closed in the issue tracker.
	issueTrackerRelease := tracker.RunningRelease(stagingVersion)
	if err := issueTrackerRelease.EnsureClosable(); err != nil {
		return err
	}

	// Make sure the release can be closed in the code review tool.
	codeReviewRelease := codeReviewTool.NewRelease(stagingVersion)
	if err := codeReviewRelease.EnsureClosable(); err != nil {
		return err
	}

	// Reset the stable branch to point to stage.
	task = fmt.Sprintf("Reset branch '%v' to point to branch '%v'", stableBranch, stagingBranch)
	log.Run(task)
	act, err := git.CreateOrResetBranch(stableBranch, stagingBranch)
	if err != nil {
		return errs.NewError(task, err)
	}
	defer action.RollbackTaskOnError(&err, task, act)

	// Bump version for the stable branch.
	stableVersion, err := stagingVersion.ToStableVersion()
	if err != nil {
		return err
	}

	task = fmt.Sprintf("Bump version (branch '%v' -> %v)", stableBranch, stableVersion)
	log.Run(task)
	act, err = version.SetForBranch(stableVersion, stableBranch)
	if err != nil {
		return errs.NewError(task, err)
	}
	defer action.RollbackTaskOnError(&err, task, act)

	// Tag the stable branch.
	tag := stableVersion.ReleaseTagString()
	task = fmt.Sprintf("Tag branch '%v' with tag '%v'", stableBranch, tag)
	log.Run(task)
	if err := git.Tag(tag, stableBranch); err != nil {
		return errs.NewError(task, err)
	}
	defer action.RollbackTaskOnError(&err, task, action.ActionFunc(func() error {
		task := fmt.Sprintf("Delete tag '%v'", tag)
		if err := git.Tag("-d", tag); err != nil {
			return errs.NewError(task, err)
		}
		return nil
	}))

	// Generate the release notes.
	// We try to do as much as possible before pushing.
	task = fmt.Sprintf("Generate release notes for version '%v'", stableVersion)
	log.Run(task)
	rnm, err := modules.GetReleaseNotesManager()
	if err != nil {
		if _, ok := err.(*modules.ErrModuleNotSet); !ok {
			return errs.NewError(task, err)
		}
	}
	var nts *common.ReleaseNotes
	// rnm will be nil in case the module is disabled.
	if rnm != nil {
		// Get the relevant stories.
		stories, err := tracker.ListStoriesByRelease(stableVersion)
		if err != nil {
			return errs.NewError(task, err)
		}
		// Generate the release notes.
		nts = notes.GenerateReleaseNotes(stableVersion, stories)
	} else {
		log.Log("Release notes module disabled, not doing anything")
	}

	// Close the release in the issue tracker.
	act, err = issueTrackerRelease.Close()
	if err != nil {
		return err
	}
	defer action.RollbackOnError(&err, act)

	// Close the release in the code review tool.
	act, err = codeReviewRelease.Close()
	if err != nil {
		return err
	}
	defer action.RollbackOnError(&err, act)

	// Push the changes to the remote repository.
	task = "Push changes to the remote repository"
	log.Run(task)
	toPush := []string{
		"--tags",
		fmt.Sprintf("%v:%v", stableBranch, stableBranch),
	}
	if err := git.PushForce(remoteName, toPush...); err != nil {
		return errs.NewError(task, err)
	}

	// Post the release notes.
	task = fmt.Sprintf("Post the release notes for version '%v'", stableVersion)
	if rnm != nil {
		log.Run(task)
		if _, err := rnm.PostReleaseNotes(nts); err != nil {
			errs.LogError(task, err)
			log.Warn("Failed to post the release notes, continuing anyway ...")
		}
	}

	// Tell the user we succeeded.
	color.Green("\n-----> Release %v deployed successfully!\n\n", stableVersion)
	color.Cyan("Let's check whether the next release branch can be staged already.\n")
	color.Cyan("In other words, we will try to run `release stage` and see what happens.\n\n")

	// Now we proceed to the staging step. We do not roll back
	// the previous changes on error since this is a separate step.
	tryToStageRunningRelease()
	return nil
}
Beispiel #19
0
func Branches() ([]*GitBranch, error) {
	// Get local branches.
	local, err := localBranches()
	if err != nil {
		return nil, err
	}

	// Get remote branches.
	remote, err := remoteBranches()
	if err != nil {
		return nil, err
	}

	// Clean up the local branches.
	// In can happen that the tracked branch fields are set while the branch
	// itself doesn't exist any more since the git calls are only consulting
	// .git/config. They don't really care whether the branch actually exists.
LocalLoop:
	for _, localBranch := range local {
		// In case the remote record is empty, we are obviously cool.
		if localBranch.RemoteBranchName == "" {
			continue
		}

		// Otherwise go through the remote branches and only continue
		// when the corresponding remote branch is found.
		for _, remoteBranch := range remote {
			if remoteBranch.RemoteBranchName == localBranch.RemoteBranchName {
				continue LocalLoop
			}
		}

		// In case the remote branch is missing, clean up the record in .git/config.
		branchName := localBranch.BranchName
		log.Warn(fmt.Sprintf(
			"Branch '%v' not found", localBranch.FullRemoteBranchName()))
		log.NewLine(fmt.Sprintf("Unsetting upstream for local branch '%v'", branchName))

		task := fmt.Sprintf("Unset upstream branch for branch '%v'", branchName)
		if err := Branch("--unset-upstream", branchName); err != nil {
			return nil, errs.NewError(task, err)
		}

		// Unset the remote branch fields.
		localBranch.RemoteBranchName = ""
		localBranch.Remote = ""
	}

	// Append the remote branch records to the local ones.
	// Only include these that are not already included in the local records.
	branches := local
RemoteLoop:
	for _, remoteBranch := range remote {
		for _, localBranch := range local {
			if localBranch.RemoteBranchName == remoteBranch.RemoteBranchName {
				continue RemoteLoop
			}
		}
		branches = append(branches, remoteBranch)
	}

	// Return branches.
	return branches, nil
}
Beispiel #20
0
func Upgrade(opts *InstallOptions) error {
	// Get GitHub owner and repository names.
	var (
		owner = DefaultGitHubOwner
		repo  = DefaultGitHubRepo
	)
	if opts != nil {
		if opts.GitHubOwner != "" {
			owner = opts.GitHubOwner
		}
		if opts.GitHubRepo != "" {
			repo = opts.GitHubRepo
		}
	}

	// Instantiate a GitHub client.
	task := "Instantiate a GitHub client"
	client, err := newGitHubClient()
	if err != nil {
		return errs.NewError(task, err)
	}

	// Fetch the list of available GitHub releases.
	task = fmt.Sprintf("Fetch GitHub releases for %v/%v", owner, repo)
	log.Run(task)
	releases, _, err := client.Repositories.ListReleases(owner, repo, nil)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Sort the releases by version and get the most recent release.
	task = "Select the most suitable GitHub release"
	var rs releaseSlice
	for i, release := range releases {
		// Skip drafts and pre-releases.
		if *release.Draft || *release.Prerelease {
			continue
		}
		// We expect the tag to be "v" + semver version string.
		version, err := version.Parse((*release.TagName)[1:])
		if err != nil {
			log.Warn(fmt.Sprintf("Tag format invalid for '%v', skipping...", release.TagName))
			continue
		}
		// Append the release to the list of releases.
		rs = append(rs, &githubRelease{
			version:  version,
			resource: &releases[i],
		})
	}
	if rs.Len() == 0 {
		return errs.NewError(task, errors.New("no suitable GitHub releases found"))
	}

	sort.Sort(rs)
	release := rs[len(rs)-1]

	// Make sure the selected release is more recent than this executable.
	currentVersion, err := version.Parse(metadata.Version)
	if err != nil {
		panic(err)
	}
	if release.version.String() == metadata.Version || release.version.LT(currentVersion.Version) {
		log.Log("SalsaFlow is up to date")
		asciiart.PrintThumbsUp()
		fmt.Println()
		return nil
	}

	// Prompt the user to confirm the upgrade.
	task = "Prompt the user to confirm upgrade"
	fmt.Println()
	confirmed, err := prompt.Confirm(fmt.Sprintf(
		"SalsaFlow version %v is available. Upgrade now?", release.version))
	if err != nil {
		return errs.NewError(task, err)
	}
	if !confirmed {
		return ErrAborted
	}
	fmt.Println()

	// Proceed to actually install the executables.
	return doInstall(client, owner, repo, release.resource.Assets, release.version.String())
}
Beispiel #21
0
func runMain() (err error) {
	// Load repo config.
	gitConfig, err := git.LoadConfig()
	if err != nil {
		return err
	}

	var (
		remoteName    = gitConfig.RemoteName()
		stagingBranch = gitConfig.StagingBranchName()
		stableBranch  = gitConfig.StableBranchName()
	)

	// Fetch the repository.
	if !flagNoFetch {
		if err := git.UpdateRemotes(remoteName); err != nil {
			return err
		}
	}

	// Check branches.
	checkBranch := func(branchName string) error {
		// Make sure the branch exists.
		task := fmt.Sprintf("Make sure that branch '%v' exists and is up to date", branchName)
		if err := git.CheckOrCreateTrackingBranch(branchName, remoteName); err != nil {
			return errs.NewError(task, err)
		}

		// Make sure we are not on the branch.
		task = fmt.Sprintf("Make sure that branch '%v' is not checked out", branchName)
		currentBranch, err := git.CurrentBranch()
		if err != nil {
			return errs.NewError(task, err)
		}
		if currentBranch == branchName {
			err := fmt.Errorf("cannot deploy while on branch '%v'", branchName)
			return errs.NewError(task, err)
		}
		return nil
	}

	for _, branch := range []string{stableBranch, stagingBranch} {
		if err := checkBranch(branch); err != nil {
			return err
		}
	}

	// Make sure the current staging branch can be released.
	task := fmt.Sprintf("Make sure that branch '%v' can be released", stagingBranch)
	log.Run(task)
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return errs.NewError(task, err)
	}

	stagingVersion, err := version.GetByBranch(stagingBranch)
	if err != nil {
		return errs.NewError(task, err)
	}

	release, err := tracker.RunningRelease(stagingVersion)
	if err != nil {
		return err
	}

	if err := release.EnsureReleasable(); err != nil {
		return err
	}

	// Reset the stable branch to point to stage.
	task = fmt.Sprintf("Reset branch '%v' to point to branch '%v'", stableBranch, stagingBranch)
	log.Run(task)
	act, err := git.CreateOrResetBranch(stableBranch, stagingBranch)
	if err != nil {
		return errs.NewError(task, err)
	}
	defer action.RollbackTaskOnError(&err, task, act)

	// Bump version for the stable branch.
	stableVersion, err := stagingVersion.ToStableVersion()
	if err != nil {
		return err
	}

	task = fmt.Sprintf("Bump version (branch '%v' -> %v)", stableBranch, stableVersion)
	log.Run(task)
	act, err = version.SetForBranch(stableVersion, stableBranch)
	if err != nil {
		return errs.NewError(task, err)
	}
	defer action.RollbackTaskOnError(&err, task, act)

	// Tag the stable branch.
	tag := stableVersion.ReleaseTagString()
	task = fmt.Sprintf("Tag branch '%v' with tag '%v'", stableBranch, tag)
	log.Run(task)
	if err := git.Tag(tag, stableBranch); err != nil {
		return errs.NewError(task, err)
	}
	defer action.RollbackTaskOnError(&err, task, action.ActionFunc(func() error {
		task := fmt.Sprintf("Delete tag '%v'", tag)
		if err := git.Tag("-d", tag); err != nil {
			return errs.NewError(task, err)
		}
		return nil
	}))

	// Generate the release notes.
	// We try to do as much as possible before pushing.
	task = fmt.Sprintf("Generate release notes for version '%v'", stableVersion)
	log.Run(task)
	rnm, err := modules.GetReleaseNotesManager()
	if err != nil {
		return errs.NewError(task, err)
	}
	var nts *common.ReleaseNotes
	// rnm will be nil in case the module is disabled.
	if rnm != nil {
		// Get the relevant stories.
		stories, err := tracker.ListStoriesByRelease(stableVersion)
		if err != nil {
			return errs.NewError(task, err)
		}
		// Generate the release notes.
		nts = notes.GenerateReleaseNotes(stableVersion, stories)
	} else {
		log.Log("Release notes module disabled, skipping ...")
	}

	// Push the changes to the remote repository.
	task = "Push changes to the remote repository"
	log.Run(task)
	toPush := []string{
		"--tags",
		fmt.Sprintf("%v:%v", stableBranch, stableBranch),
	}
	if err := git.PushForce(remoteName, toPush...); err != nil {
		return errs.NewError(task, err)
	}

	// Post the release notes.
	task = fmt.Sprintf("Post the release notes for version '%v'", stableVersion)
	if rnm != nil {
		log.Run(task)
		if _, err := rnm.PostReleaseNotes(nts); err != nil {
			errs.LogError(task, err)
			log.Warn("Failed to post the release notes, continuing anyway ...")
		}
	}

	// Now we proceed to the staging step. We do not roll back
	// the previous changes on error since this is a separate step.
	tryToStageRunningRelease()
	return nil
}