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 }
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 }
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 }
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 }
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 }