예제 #1
0
func runMain() (err error) {
	var (
		task   string
		stderr *bytes.Buffer
	)
	defer func() {
		// Print error details.
		if err != nil {
			log.FailWithDetails(task, stderr)
		}
	}()

	// Fetch the remote repository unless we are restricted to the local branches only.
	if !flagLocalOnly {
		task = "Fetch the remote repository"
		log.Run(task)
		stderr, err = git.UpdateRemotes(config.OriginName)
		if err != nil {
			return
		}
	}

	// Get the list of story references.
	task = "Collect all story branches"
	log.Run(task)
	localRefs, remoteRefs, stderr, err := git.ListStoryRefs()
	if err != nil {
		return
	}

	var refs []string
	switch {
	case flagLocalOnly:
		refs = localRefs
	case flagRemoteOnly:
		refs = remoteRefs
	default:
		refs = append(localRefs, remoteRefs...)
	}

	if len(refs) == 0 {
		task = ""
		log.Println("\nNo story branches found, exiting...")
		return
	}

	// Collect all the story IDs.
	idMap := make(map[string]struct{})
	for _, ref := range refs {
		// This cannot fail here since we got the refs using ListStoryRefs.
		id, _ := git.RefToStoryId(ref)
		idMap[id] = struct{}{}
	}

	var ids []string
	for id := range idMap {
		ids = append(ids, id)
	}

	// Get the list of active story IDs.
	activeIds, err := modules.GetIssueTracker().SelectActiveStoryIds(ids)
	if err != nil {
		return
	}
	ids = activeIds

	// Select only the refs that can be safely deleted.
	refs = selectInactiveRefs(refs, ids)

	if len(refs) == 0 {
		task = ""
		log.Println("\nThere are no branches to be deleted, exiting...")
		return
	}

	// Sort the refs.
	sort.Sort(sort.StringSlice(refs))

	// Prompt the user to confirm the delete operation.
	var (
		toDeleteLocally  []string
		toDeleteRemotely []string
		ok               bool
	)

	// Go through the local branches.
	if strings.HasPrefix(refs[0], "refs/heads/") {
		fmt.Println("\n---> Local branches\n")
	}
	for len(refs) > 0 {
		ref := refs[0]
		if !strings.HasPrefix(ref, "refs/heads/") {
			break
		}
		branch := ref[len("refs/heads/"):]
		question := fmt.Sprintf("Delete local branch '%v'", branch)
		ok, err = prompt.Confirm(question)
		if err != nil {
			return
		}
		if ok {
			toDeleteLocally = append(toDeleteLocally, branch)
		}
		refs = refs[1:]
	}

	// All that is left are remote branches.
	if len(refs) != 0 {
		fmt.Println("\n---> Remote branches\n")
	}
	for _, ref := range refs {
		branch := ref[len("refs/remotes/origin/"):]
		question := fmt.Sprintf("Delete remote branch '%v'", branch)
		ok, err = prompt.Confirm(question)
		if err != nil {
			return
		}
		if ok {
			toDeleteRemotely = append(toDeleteRemotely, branch)
		}
	}
	fmt.Println()

	if len(toDeleteLocally) == 0 && len(toDeleteRemotely) == 0 {
		task = ""
		fmt.Println("No branches selected, exiting...")
		return
	}

	// Delete the local branches.
	if len(toDeleteLocally) != 0 {
		task = "Delete the chosen local branches"
		log.Run(task)

		// Remember the position of the branches to be deleted.
		// This is used in case we need to perform a rollback.
		var (
			currentPositions []string
			hexsha           string
		)
		for _, branchName := range toDeleteLocally {
			hexsha, stderr, err = git.Hexsha("refs/heads/" + branchName)
			if err != nil {
				return
			}
			currentPositions = append(currentPositions, hexsha)
		}

		// Delete the selected local branches.
		args := append([]string{"-d"}, toDeleteLocally...)
		stderr, err = git.Branch(args...)
		if err != nil {
			return
		}
		defer func(taskMsg string) {
			// On error, try to restore the local branches that were deleted.
			if err != nil {
				log.Rollback(taskMsg)
				for i, branchName := range toDeleteLocally {
					out, ex := git.ResetKeep(branchName, currentPositions[i])
					if ex != nil {
						log.FailWithDetails(task, out)
					}
				}
			}
		}(task)
	}

	// Delete the remote branches.
	if len(toDeleteRemotely) != 0 {
		task = "Delete the chosen remote branches"
		log.Run(task)
		var refs []string
		for _, branchName := range toDeleteRemotely {
			refs = append(refs, ":"+branchName)
		}
		stderr, err = git.Push(config.OriginName, refs...)
	}
	return
}
예제 #2
0
func runMain() (err error) {
	// Load repo config.
	gitConfig, err := git.LoadConfig()
	if err != nil {
		return err
	}
	var (
		remote        = gitConfig.RemoteName
		trunkBranch   = gitConfig.TrunkBranchName
		releaseBranch = gitConfig.ReleaseBranchName
		stagingBranch = gitConfig.StagingBranchName
	)

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

	// Make sure trunk is up to date.
	task := fmt.Sprintf("Make sure that branch '%v' is up to date", trunkBranch)
	if err := git.CheckOrCreateTrackingBranch(trunkBranch, remote); err != nil {
		return errs.NewError(task, err)
	}

	// Make sure the staging branch is up to date, in case it exists.
	//
	// We check stage here as well since it is otherwise checked later
	// in releases.ListNewTrunkCommits(), which is usually called in
	// release.PromptUserToConfirmStart().
	task = fmt.Sprintf("Make sure that branch '%v' is up to date", stagingBranch)
	if err := git.CheckOrCreateTrackingBranch(stagingBranch, remote); err != nil {
		// The staging branch actually doesn't need to exist.
		if _, ok := err.(*git.ErrRefNotFound); !ok {
			return errs.NewError(task, err)
		}
	}

	// Make sure that the release branch does not exist.
	task = fmt.Sprintf("Make sure that branch '%v' does not exist", releaseBranch)
	if err := git.EnsureBranchNotExist(releaseBranch, remote); err != nil {
		return errs.NewError(task, err)
	}

	// Get the current trunk version string.
	task = "Get the current trunk version string"
	trunkVersion, err := version.GetByBranch(trunkBranch)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Get the next trunk version (the future release version).
	var nextTrunkVersion *version.Version
	if !flagNextTrunk.Zero() {
		// Make sure it's only major, minor and patch that are set.
		// Make sure the new version is actually incrementing the current one.
		var (
			current = trunkVersion
			next    = flagNextTrunk
		)

		var part string
		switch {
		case len(next.Pre) != 0:
			part = "Pre"
		case len(next.Build) != 0:
			part = "Build"
		}
		if part != "" {
			return fmt.Errorf("invalid future version string: %v version part cannot be set", part)
		}

		if current.GE(next.Version) {
			return fmt.Errorf("future version string not an increment: %v <= %v", next, current)
		}

		nextTrunkVersion = &flagNextTrunk
	} else {
		nextTrunkVersion = trunkVersion.IncrementMinor()
	}

	// Make sure the next trunk version has the right format.
	nextTrunkVersion, err = nextTrunkVersion.ToTrunkVersion()
	if err != nil {
		return err
	}

	// Fetch the stories from the issue tracker.
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return errs.NewError(task, err)
	}
	release := tracker.NextRelease(trunkVersion, nextTrunkVersion)

	// Prompt the user to confirm the release.
	fmt.Printf(`
You are about to start a new release branch.
The relevant version strings are:

  current release (current trunk version): %v
  future release (next trunk version):     %v

`, trunkVersion, nextTrunkVersion)
	ok, err := release.PromptUserToConfirmStart()
	if err != nil {
		return err
	}
	if !ok {
		fmt.Println("\nYour wish is my command, exiting now!")
		return nil
	}
	fmt.Println()

	// Create the release branch on top of the trunk branch.
	task = fmt.Sprintf("Create branch '%v' on top of branch '%v'", releaseBranch, trunkBranch)
	log.Run(task)
	if err := git.Branch(releaseBranch, trunkBranch); err != nil {
		return errs.NewError(task, err)
	}
	defer action.RollbackTaskOnError(&err, task, action.ActionFunc(func() error {
		task := fmt.Sprintf("Delete branch '%v'", releaseBranch)
		if err := git.Branch("-D", releaseBranch); err != nil {
			errs.NewError(task, err)
		}
		return nil
	}))

	// Bump the release branch version.
	testingVersion, err := trunkVersion.ToTestingVersion()
	if err != nil {
		return err
	}

	task = fmt.Sprintf("Bump version (branch '%v' -> %v)", releaseBranch, testingVersion)
	log.Run(task)
	_, err = version.SetForBranch(testingVersion, releaseBranch)
	if err != nil {
		return errs.NewError(task, err)
	}
	// No need for a rollback function here, git branch -d specified as a rollback
	// for the previous step will take care of deleting this change as well.

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

	// Initialise the next release in the code review tool.
	codeReviewTool, err := modules.GetCodeReviewTool()
	if err != nil {
		return err
	}
	act, err = codeReviewTool.NewRelease(nextTrunkVersion).Initialise()
	if err != nil {
		return err
	}
	defer action.RollbackTaskOnError(&err, task, act)

	// Start the release in the issue tracker.
	act, err = release.Start()
	if err != nil {
		return err
	}
	defer action.RollbackTaskOnError(&err, task, act)

	// Push the modified branches.
	task = "Push changes to the remote repository"
	log.Run(task)
	err = git.Push(remote, trunkBranch+":"+trunkBranch, releaseBranch+":"+releaseBranch)
	if err != nil {
		return errs.NewError(task, err)
	}

	return nil
}
예제 #3
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
}
예제 #4
0
func postBranch(parentBranch string) (err error) {
	// Load the git-related config.
	gitConfig, err := git.LoadConfig()
	if err != nil {
		return err
	}
	var (
		remoteName = gitConfig.RemoteName
	)

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

	if !flagNoFetch {
		// Fetch the remote repository.
		task := "Fetch the remote repository"
		log.Run(task)

		if err := git.UpdateRemotes(remoteName); err != nil {
			return errs.NewError(task, err)
		}
	}

	// Make sure the parent branch is up to date.
	task := fmt.Sprintf("Make sure reference '%v' is up to date", parentBranch)
	log.Run(task)
	if err := git.EnsureBranchSynchronized(parentBranch, remoteName); err != nil {
		return errs.NewError(task, err)
	}

	// Make sure the current branch is up to date.
	task = fmt.Sprintf("Make sure branch '%v' is up to date", currentBranch)
	log.Run(task)
	if err = git.EnsureBranchSynchronized(currentBranch, remoteName); err != nil {
		return errs.NewError(task, err)
	}

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

	// Make sure there are no merge commits.
	if err := ensureNoMergeCommits(commits); err != nil {
		return err
	}

	// Prompt the user to confirm.
	if err := promptUserToConfirmCommits(commits); err != nil {
		return err
	}

	// Rebase the current branch on top the parent branch.
	if !flagNoRebase {
		commits, err = rebase(currentBranch, parentBranch)
		if err != nil {
			return err
		}
	}

	// Ensure the Story-Id tag is there.
	commits, _, err = ensureStoryId(commits)
	if err != nil {
		return err
	}

	// Get data on the current branch.
	task = fmt.Sprintf("Get data on branch '%v'", currentBranch)
	remoteCurrentExists, err := git.RemoteBranchExists(currentBranch, remoteName)
	if err != nil {
		return errs.NewError(task, err)
	}
	currentUpToDate, err := git.IsBranchSynchronized(currentBranch, remoteName)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Merge the current branch into the parent branch unless -no_merge.
	pushTask := "Push the current branch"
	if flagNoMerge {
		// In case the user doesn't want to merge,
		// we need to push the current branch.
		if !remoteCurrentExists || !currentUpToDate {
			if err := push(remoteName, currentBranch); err != nil {
				return errs.NewError(pushTask, err)
			}
		}
	} else {
		// Still push the current branch if necessary.
		if remoteCurrentExists && !currentUpToDate {
			if err := push(remoteName, currentBranch); err != nil {
				return errs.NewError(pushTask, err)
			}
		}

		// Merge the branch into the parent branch
		mergeTask := fmt.Sprintf("Merge branch '%v' into branch '%v'", currentBranch, parentBranch)
		log.Run(mergeTask)
		act, err := merge(mergeTask, currentBranch, parentBranch)
		if err != nil {
			return err
		}

		// Push the parent branch.
		if err := push(remoteName, parentBranch); err != nil {
			// In case the push fails, we revert the merge as well.
			if err := act.Rollback(); err != nil {
				errs.Log(err)
			}
			return errs.NewError(mergeTask, err)
		}

		// Register a rollback function that just says that
		// a pushed merge cannot be reverted.
		defer action.RollbackOnError(&err, action.ActionFunc(func() error {
			log.Rollback(mergeTask)
			hint := "\nCannot revert merge that has already been pushed.\n"
			return errs.NewErrorWithHint(
				"Revert the merge", errors.New("merge commit already pushed"), hint)
		}))
	}

	// Post the review requests.
	if err := postCommitsForReview(commits); err != nil {
		return err
	}

	// In case there is no error, tell the user they can do next.
	return printFollowup()
}
예제 #5
0
func Stage(options *StageOptions) (act action.Action, err error) {
	// Rollback machinery.
	chain := action.NewActionChain()
	defer chain.RollbackOnError(&err)

	// Make sure opts are not nil.
	if options == nil {
		options = DefaultStageOptions
	}

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

	// Instantiate the issue tracker.
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return nil, err
	}

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

	// Cannot be on the release branch, it will be deleted.
	task = fmt.Sprintf("Make sure that branch '%v' is not checked out", releaseBranch)
	if currentBranch == releaseBranch {
		return nil, errs.NewError(
			task, fmt.Errorf("cannot stage the release while on branch '%v'", releaseBranch))
	}

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

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

	// Read the current release version.
	task = "Read the current release version"
	releaseVersion, err := version.GetByBranch(releaseBranch)
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	// Make sure the release is stageable.
	release := tracker.RunningRelease(releaseVersion)
	if err := release.EnsureStageable(); err != nil {
		return nil, err
	}

	// Make sure there are no commits being left behind,
	// e.g. make sure no commits are forgotten on the trunk branch,
	// i.e. make sure that everything necessary was cherry-picked.
	if err := checkCommits(tracker, release, releaseBranch); err != nil {
		return nil, err
	}

	// Reset the staging branch to point to the newly created tag.
	task = fmt.Sprintf("Reset branch '%v' to point to branch '%v'", stagingBranch, releaseBranch)
	log.Run(task)
	act, err = git.CreateOrResetBranch(stagingBranch, releaseBranch)
	if err != nil {
		return nil, errs.NewError(task, err)
	}
	chain.PushTask(task, act)

	// Delete the local release branch.
	task = fmt.Sprintf("Delete branch '%v'", releaseBranch)
	log.Run(task)
	if err := git.Branch("-D", releaseBranch); err != nil {
		return nil, errs.NewError(task, err)
	}
	chain.PushTask(task, action.ActionFunc(func() error {
		task := fmt.Sprintf("Recreate branch '%v'", releaseBranch)

		// In case the release branch exists locally, do nothing.
		// This might look like an extra and useless check, but it looks like
		// the final git push at the end of the command function actually creates
		// the release branch locally when it is aborted from the pre-push hook.
		// Not sure why and how that is happening.
		exists, err := git.LocalBranchExists(releaseBranch)
		if err != nil {
			return errs.NewError(task, err)
		}
		if exists {
			return nil
		}

		// In case the branch indeed does not exist, create it.
		if err := git.Branch(releaseBranch, remoteName+"/"+releaseBranch); err != nil {
			return errs.NewError(task, err)
		}
		return nil
	}))

	// Update the version string on the staging branch.
	stagingVersion, err := releaseVersion.ToStageVersion()
	if err != nil {
		return nil, err
	}

	task = fmt.Sprintf("Bump version (branch '%v' -> %v)", stagingBranch, stagingVersion)
	log.Run(task)
	act, err = version.SetForBranch(stagingVersion, stagingBranch)
	if err != nil {
		return nil, errs.NewError(task, err)
	}
	chain.PushTask(task, act)

	// Stage the release in the issue tracker.
	act, err = release.Stage()
	if err != nil {
		return nil, err
	}
	chain.Push(act)

	// Push to create the tag, reset client and delete release in the remote repository.
	task = "Push changes to the remote repository"
	log.Run(task)
	err = git.Push(remoteName,
		"-f",                            // Use the Force, Luke.
		":"+releaseBranch,               // Delete the release branch.
		stagingBranch+":"+stagingBranch) // Push the staging branch.
	if err != nil {
		return nil, err
	}
	return chain, nil
}
예제 #6
0
func runMain() error {
	// Load git-related config.
	gitConfig, err := git.LoadConfig()
	if err != nil {
		return err
	}
	var (
		remoteName    = gitConfig.RemoteName
		trunkBranch   = gitConfig.TrunkBranchName
		releaseBranch = gitConfig.ReleaseBranchName
	)

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

	log.Run("Make sure all important branches are up to date")

	// Check branches.
	checkBranch := func(branchName string) error {
		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)
		}
		return nil
	}

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

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

	// Checkout the release branch.
	task := "Checkout the release branch"
	if err := git.Checkout(releaseBranch); err != nil {
		return errs.NewError(task, err)
	}
	defer func() {
		// Do not checkout the original branch in case the name is empty.
		// This is later used to disable the checkout of the original branch.
		if currentBranch == "" {
			return
		}
		// Otherwise checkout the original branch.
		task := fmt.Sprintf("Checkout the original branch (%v)", currentBranch)
		if err := git.Checkout(currentBranch); err != nil {
			errs.LogError(task, err)
		}
	}()

	// Get the current release version string.
	// It is enough to just call version.Get since
	// we are already on the release branch.
	task = "Get the release branch version string"
	releaseVersion, err := version.Get()
	if err != nil {
		return errs.NewError(task, err)
	}

	// Get the stories associated with the current release.
	task = "Fetch the stories associated with the current release"
	log.Run(task)
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return errs.NewError(task, err)
	}
	release := tracker.RunningRelease(releaseVersion)
	stories, err := release.Stories()
	if err != nil {
		return errs.NewError(task, err)
	}

	if len(stories) == 0 {
		return errs.NewError(task, errors.New("no relevant stories found"))
	}

	// Get the release changes.
	task = "Collect the release changes"
	log.Run(task)
	groups, err := changes.StoryChanges(stories)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Just return in case there are no relevant commits found.
	if len(groups) == 0 {
		return errs.NewError(task, errors.New("no relevant commits found"))
	}

	// Sort the change groups.
	groups = changes.SortStoryChanges(groups, stories)
	groups, err = releases.StoryChangesToCherryPick(groups)
	if err != nil {
		return errs.NewError(task, err)
	}

	var (
		// Collect the changes not reachable from trunk.
		// In case there are any, we abort the cherry-picking process.
		unreachable = make([]*changes.StoryChangeGroup, 0, len(groups))

		// When we are at iterating, we also collect all release commits
		// so that we know what trunk commits to cherry-pick later.
		releaseCommits = make(map[string]struct{})

		trunkRef = fmt.Sprintf("refs/heads/%v", trunkBranch)
	)
	for _, group := range groups {
		g := &changes.StoryChangeGroup{
			StoryIdTag: group.StoryIdTag,
		}

		for _, ch := range group.Changes {
			var ok bool
			for _, c := range ch.Commits {
				// Add the commit to the map of release commits.
				releaseCommits[c.SHA] = struct{}{}

				// Look for a commit that is on trunk.
				if c.Source == trunkRef {
					ok = true
				}
			}
			if !ok {
				// In case there is none, remember the change.
				g.Changes = append(g.Changes, ch)
			}
		}

		// In case there are some changes not reachable from trunk,
		// add the story change to the list of unreachable story changes.
		if len(g.Changes) != 0 {
			unreachable = append(unreachable, g)
		}
	}

	// In case there are some changes not reachable from the trunk branch,
	// abort the process and tell the user to get the changes into trunk first.
	if len(unreachable) != 0 {
		var details bytes.Buffer
		fmt.Fprint(&details, `
The following story changes are not reachable from the trunk branch:

`)
		changes.DumpStoryChanges(&details, unreachable, tracker, false)
		fmt.Fprint(&details, `
Please cherry-pick these changes onto the trunk branch.
Only then we can proceed and cherry-pick the changes.

`)
		return errs.NewErrorWithHint(
			task, errors.New("commits not reachable from trunk detected"), details.String())
	}

	// Everything seems fine, let's continue with the process
	// by dumping the change details into the console.
	fmt.Println()
	changes.DumpStoryChanges(os.Stdout, groups, tracker, false)

	// Ask the user to confirm before doing any cherry-picking.
	task = "Ask the user to confirm cherry-picking"
	fmt.Println(`
The changes listed above will be cherry-picked into the release branch.`)
	confirmed, err := prompt.Confirm("Are you sure you want to continue?", false)
	if err != nil {
		return errs.NewError(task, err)
	}
	if !confirmed {
		prompt.PanicCancel()
	}
	fmt.Println()

	// Collect the trunk commits that were created since the last release.
	task = "Collect the trunk commits added since the last release"
	trunkCommits, err := releases.ListNewTrunkCommits()
	if err != nil {
		return errs.NewError(task, err)
	}
	// We need the list to start with the oldest commit.
	for i, j := 0, len(trunkCommits)-1; i < j; i, j = i+1, j-1 {
		trunkCommits[i], trunkCommits[j] = trunkCommits[j], trunkCommits[i]
	}

	// Collect the commits to cherry pick. These are the commits
	// that are on trunk and they are associated with the release.
	hashesToCherryPick := make([]string, 0, len(trunkCommits))
	for _, commit := range trunkCommits {
		if _, ok := releaseCommits[commit.SHA]; ok {
			hashesToCherryPick = append(hashesToCherryPick, commit.SHA)
		}
	}

	// Perform the cherry-pick itself.
	task = "Cherry-pick the missing changes into the release branch"
	log.Run(task)
	if err := git.CherryPick(hashesToCherryPick...); err != nil {
		hint := `
It was not possible to cherry-pick the missing changes into the release branch.
The cherry-picking process might be still in progress, though. Please check
the repository status and potentially resolve the cherry-picking manually.

`
		// Do not checkout the original branch.
		currentBranch = ""
		return errs.NewErrorWithHint(task, err, hint)
	}

	log.Log("All missing changes cherry-picked into the release branch")
	fmt.Println(`
  ###################################################################
  # IMPORTANT: The release branch is not being pushed automatically #
  ###################################################################
`)
	return nil
}
예제 #7
0
func createBranch() (action.Action, error) {
	// Get the current branch name.
	originalBranch, err := gitutil.CurrentBranch()
	if err != nil {
		return nil, err
	}

	// Fetch the remote repository.
	task := "Fetch the remote repository"
	log.Run(task)

	gitConfig, err := git.LoadConfig()
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	var (
		remoteName = gitConfig.RemoteName
		baseBranch = gitConfig.TrunkBranchName
	)
	if flagBase != "" {
		baseBranch = flagBase
	}

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

	// Make sure the trunk branch is up to date.
	task = fmt.Sprintf("Make sure branch '%v' is up to date", baseBranch)
	log.Run(task)
	if err := git.CheckOrCreateTrackingBranch(baseBranch, remoteName); err != nil {
		return nil, errs.NewError(task, err)
	}

	// Prompt the user for the branch name.
	task = "Prompt the user for the branch name"
	line, err := prompt.Prompt(`
Please insert the branch slug now.
Insert an empty string to skip the branch creation step: `)
	if err != nil && err != prompt.ErrCanceled {
		return nil, errs.NewError(task, err)
	}

	sluggedLine := slug.Slug(line)
	if sluggedLine == "" {
		fmt.Println()
		log.Log("Not creating any feature branch")
		return nil, nil
	}

	branchName := "story/" + sluggedLine
	ok, err := prompt.Confirm(
		fmt.Sprintf(
			"\nThe branch that is going to be created will be called '%s'.\nIs that alright?",
			branchName),
		true)
	if err != nil {
		return nil, errs.NewError(task, err)
	}
	if !ok {
		panic(prompt.ErrCanceled)
	}
	fmt.Println()

	createTask := fmt.Sprintf(
		"Create branch '%v' on top of branch '%v'", branchName, baseBranch)
	log.Run(createTask)
	if err := git.Branch(branchName, baseBranch); err != nil {
		return nil, errs.NewError(createTask, err)
	}

	deleteTask := fmt.Sprintf("Delete branch '%v'", branchName)
	deleteBranch := func() error {
		// Roll back and delete the newly created branch.
		log.Rollback(createTask)
		if err := git.Branch("-D", branchName); err != nil {
			return errs.NewError(deleteTask, err)
		}
		return nil
	}

	// Checkout the newly created branch.
	checkoutTask := fmt.Sprintf("Checkout branch '%v'", branchName)
	log.Run(checkoutTask)
	if err := git.Checkout(branchName); err != nil {
		if err := deleteBranch(); err != nil {
			errs.Log(err)
		}
		return nil, errs.NewError(checkoutTask, err)
	}

	// Push the newly created branch unless -no_push.
	pushTask := fmt.Sprintf("Push branch '%v' to remote '%v'", branchName, remoteName)
	if flagPush {
		log.Run(pushTask)
		if err := git.Push(remoteName, branchName); err != nil {
			if err := deleteBranch(); err != nil {
				errs.Log(err)
			}
			return nil, errs.NewError(pushTask, err)
		}
	}

	return action.ActionFunc(func() error {
		// Checkout the original branch.
		log.Rollback(checkoutTask)
		if err := git.Checkout(originalBranch); err != nil {
			return errs.NewError(
				fmt.Sprintf("Checkout the original branch '%v'", originalBranch), err)
		}

		// Delete the newly created branch.
		deleteErr := deleteBranch()

		// In case we haven't pushed anything, we are done.
		if !flagPush {
			return deleteErr
		}

		// Delete the branch from the remote repository.
		log.Rollback(pushTask)
		if _, err := git.Run("push", "--delete", remoteName, branchName); err != nil {
			// In case deleteBranch failed, tell the user now
			// since we are not going to return that error.
			if deleteErr != nil {
				errs.Log(deleteErr)
			}

			return errs.NewError(
				fmt.Sprintf("Delete branch '%v' from remote '%v'", branchName, remoteName), err)
		}

		// Return deleteErr to make sure it propagates up.
		return deleteErr
	}), nil
}
예제 #8
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
}
예제 #9
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
}