Exemple #1
0
func GetByBranch(branch string) (ver *Version, err error) {
	// Remember the current branch.
	currentBranch, err := git.CurrentBranch()
	if err != nil {
		return nil, err
	}

	// Checkout the target branch.
	if err := git.Checkout(branch); err != nil {
		return nil, err
	}
	defer func() {
		// Checkout the original branch on return.
		if ex := git.Checkout(currentBranch); ex != nil {
			if err == nil {
				err = ex
			} else {
				errs.Log(ex)
			}
		}
	}()

	// Get the version.
	v, err := Get()
	if err != nil {
		if ex, ok := err.(*scripts.ErrNotFound); ok {
			return nil, fmt.Errorf(
				"custom SalsaFlow script '%v' not found on branch '%v'", ex.ScriptName(), branch)
		}
		return nil, err
	}
	return v, nil
}
Exemple #2
0
func SetForBranch(ver *Version, branch string) (act action.Action, err error) {
	var mainTask = fmt.Sprintf("Bump version to %v for branch '%v'", ver, branch)

	// Make sure the repository is clean (don't check untracked files).
	task := "Make sure the repository is clean"
	if err := git.EnsureCleanWorkingTree(false); err != nil {
		return nil, errs.NewError(task, err)
	}

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

	// Remember the current position of the target branch.
	task = fmt.Sprintf("Remember the position of branch '%v'", branch)
	originalPosition, err := git.Hexsha("refs/heads/" + branch)
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	// Checkout the target branch.
	task = fmt.Sprintf("Checkout branch '%v'", branch)
	if err := git.Checkout(branch); err != nil {
		return nil, errs.NewError(task, err)
	}
	defer func() {
		// Checkout the original branch on return.
		task := fmt.Sprintf("Checkout branch '%v'", currentBranch)
		if ex := git.Checkout(currentBranch); ex != nil {
			if err == nil {
				err = ex
			} else {
				errs.LogError(task, ex)
			}
		}
	}()

	// Set the project version to the desired value.
	if err := Set(ver); err != nil {
		if ex, ok := err.(*scripts.ErrNotFound); ok {
			return nil, fmt.Errorf(
				"custom SalsaFlow script '%v' not found on branch '%v'", ex.ScriptName(), branch)
		}
		return nil, err
	}

	// Commit changes.
	_, err = git.RunCommand("commit", "-a",
		"-m", fmt.Sprintf("Bump version to %v", ver),
		"-m", fmt.Sprintf("Story-Id: %v", git.StoryIdUnassignedTagValue))
	if err != nil {
		task := "Reset the working tree to the original state"
		if err := git.Reset("--keep"); err != nil {
			errs.LogError(task, err)
		}
		return nil, err
	}

	return action.ActionFunc(func() (err error) {
		// On rollback, reset the target branch to the original position.
		log.Rollback(mainTask)
		return git.ResetKeep(branch, originalPosition)
	}), nil
}
Exemple #3
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 := git.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, err := tracker.RunningRelease(releaseVersion)
	if err != nil {
		return nil, err
	}
	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)

	// Finalise the release in the code review tool.
	codeReviewTool, err := modules.GetCodeReviewTool()
	if err != nil {
		return nil, err
	}
	act, err = codeReviewTool.FinaliseRelease(releaseVersion)
	if err != nil {
		return nil, err
	}
	// No need to pass any task string, the module rollback functions
	// are expected to take care of printing messages on their own.
	chain.Push(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
}
Exemple #4
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 := git.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, err := tracker.RunningRelease(releaseVersion)
	if err != nil {
		return errs.NewError(task, err)
	}
	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?")
	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
}
Exemple #5
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
}