// ListNewTrunkCommits returns the list of commits that are new since the last release. // By the last release we mean the last release being tested, staged or released. func ListNewTrunkCommits() ([]*git.Commit, error) { // Get git config. config, err := git.LoadConfig() if err != nil { return nil, err } var ( remoteName = config.RemoteName trunkBranch = config.TrunkBranchName releaseBranch = config.ReleaseBranchName stagingBranch = config.StagingBranchName ) // By default, use the staging branch as the --not part. // In other words, list commits that are on trunk, // but which are not reachable from the staging branch. // In case the staging branch doesn't exist, take the whole trunk. // That probably means that no release has ever been started, // so the staging branch has not been created yet. var revRange string for _, branch := range [...]string{releaseBranch, stagingBranch} { err := git.CheckOrCreateTrackingBranch(branch, remoteName) // In case the branch is ok, we use it. if err == nil { revRange = fmt.Sprintf("%v..%v", branch, trunkBranch) break } // In case the branch does not exist, it's ok and we continue. if _, ok := err.(*git.ErrRefNotFound); ok { continue } // Otherwise we return the error since something has just exploded. // This can mean that the branch is not up to date, but that is an error as well. return nil, err } if revRange == "" { revRange = trunkBranch } // Get the commits in range. commits, err := git.ShowCommitRange(revRange) if err != nil { return nil, err } // Limit the commits by date. repoConfig, err := repo.LoadConfig() if err != nil { return nil, err } enabledTimestamp := repoConfig.SalsaFlowEnabledTimestamp commits = git.FilterCommits(commits, func(commit *git.Commit) bool { return commit.AuthorDate.After(enabledTimestamp) }) return commits, nil }
func ensureCommitsPushed(commits []*git.Commit) error { task := "Make sure that all commits exist in the upstream repository" // Load git-related config. gitConfig, err := git.LoadConfig() if err != nil { return errs.NewError(task, err) } remoteName := gitConfig.RemoteName remotePrefix := remoteName + "/" // Check each commit one by one. // // We run `git branch -r --contains HASH` for each commit, // then we check the output. In case there is a branch prefixed // with the right upstream name, the commit is treated as pushed. var ( hint = bytes.NewBufferString("\n") missing bool ) CommitLoop: for _, commit := range commits { // Get `git branch -r --contains HASH` output. stdout, err := git.Run("branch", "-r", "--contains", commit.SHA) if err != nil { return errs.NewError(task, err) } // Parse `git branch` output line by line. scanner := bufio.NewScanner(stdout) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(strings.TrimSpace(line), remotePrefix) { // The commit is contained in a remote branch, continue. continue CommitLoop } } if err := scanner.Err(); err != nil { return errs.NewError(task, err) } // The commit is not contained in any remote branch, bummer. fmt.Fprintf(hint, "Commit %v has not been pushed into remote '%v' yet.\n", commit.SHA, remoteName) missing = true } fmt.Fprintf(hint, "\n") fmt.Fprintf(hint, "All selected commits need to be pushed into the upstream pository.\n") fmt.Fprintf(hint, "Please make sure that is the case before trying again.\n") fmt.Fprintf(hint, "\n") // Return an error in case there is any commit that is not pushed. if missing { return errs.NewErrorWithHint( task, fmt.Errorf("some commits not found in upstream '%v'", remoteName), hint.String()) } return nil }
func StoryChangesToCherryPick( groups []*changes.StoryChangeGroup, ) ([]*changes.StoryChangeGroup, error) { // Get the changes that are reachable from the release branch. gitConfig, err := git.LoadConfig() if err != nil { return nil, err } releaseBranch := gitConfig.ReleaseBranchName reachableCommits, err := git.ShowCommitRange(releaseBranch) if err != nil { return nil, err } reachableChanges := make(map[string]struct{}, len(reachableCommits)) for _, commit := range reachableCommits { // Would not probably harm much not to have the condition here, // but hey, let's keep the Change-Id set clean. if commit.ChangeIdTag != "" { reachableChanges[commit.ChangeIdTag] = struct{}{} } } // Get the changes that needs to be cherry-picked. var toCherryPick []*changes.StoryChangeGroup for _, group := range groups { // Prepare a new StoryChangeGroup to hold missing changes. storyGroup := &changes.StoryChangeGroup{ StoryIdTag: group.StoryIdTag, } // A change needs cherry-picking in case it's not reachable // from the release branch, right? for _, change := range group.Changes { // Skip the group representing commits with no Change-Id tag. if change.ChangeIdTag == "" { continue } // Append the group in case the Change-Id is not reachable. if _, ok := reachableChanges[change.ChangeIdTag]; !ok { storyGroup.Changes = append(storyGroup.Changes, change) } } // Append the whole story group in case any associated change group // is not reachable from the release branch. if len(storyGroup.Changes) != 0 { toCherryPick = append(toCherryPick, storyGroup) } } return toCherryPick, nil }
// ParseUpstreamURL parses the URL of the git upstream being used by SalsaFlow // and returns the given GitHub owner and repository. func ParseUpstreamURL() (owner, repo string, err error) { // Load the Git config. gitConfig, err := git.LoadConfig() if err != nil { return "", "", err } remoteName := gitConfig.RemoteName // Get the upstream URL. task := fmt.Sprintf("Get URL for git remote '%v'", remoteName) remoteURL, err := git.GetConfigString(fmt.Sprintf("remote.%v.url", remoteName)) if err != nil { return "", "", errs.NewError(task, err) } // Parse it and return the result. return parseUpstreamURL(remoteURL) }
func runMain() error { // Load config. config, err := git.LoadConfig() if err != nil { return err } var ( remoteName = config.RemoteName trunkName = config.TrunkBranchName ) // Make sure trunk is up to date. task := fmt.Sprintf("Make sure branch '%v' is up to date", trunkName) log.Run(task) if err := git.CheckOrCreateTrackingBranch(trunkName, remoteName); err != nil { return errs.NewError(task, err) } // Collect the story branches. task = "Collect the story branches" log.Run(task) storyBranches, err := collectStoryBranches(remoteName) if err != nil { return errs.NewError(task, err) } // Split the branches that are not up to date. task = "Split the branches that are not up to date" log.Run(task) storyBranches, err = splitBranchesNotInSync(storyBranches) if err != nil { return errs.NewError(task, err) } // Filter branches according to the story state. task = "Filter branches according to the story state" log.Run(task) filteredBranches, err := filterBranches(storyBranches, trunkName) if err != nil { return errs.NewError(task, err) } if len(storyBranches) == 0 { log.Log("No branches left to be deleted") return nil } // Prompt the user to choose what branches to delete. task = "Prompt the user to choose what branches to delete" localToDelete, remoteToDelete, err := promptUserToChooseBranches(filteredBranches) if err != nil { return errs.NewError(task, err) } // Delete chosen local branches. if len(localToDelete) != 0 { task := "Delete chosen local branches" log.Run(task) args := make([]string, 1, 1+len(localToDelete)) args[0] = "-D" args = append(args, localToDelete...) if ex := git.Branch(args...); ex != nil { errs.LogError(task, ex) err = errors.New("failed to delete local branches") } } // Delete chosen remote branches. if len(remoteToDelete) != 0 { task := "Delete chosen remote branches" log.Run(task) args := make([]string, 1, 1+len(remoteToDelete)) args[0] = "--delete" args = append(args, remoteToDelete...) if ex := git.Push(remoteName, args...); ex != nil { errs.LogError(task, ex) err = errors.New("failed to delete remote branches") } } return err }
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() }
func run(remoteName, pushURL string) error { // Load the git-related SalsaFlow config. gitConfig, err := git.LoadConfig() if err != nil { return err } // Load the other necessary SalsaFlow config. repoConfig, err := repo.LoadConfig() if err != nil { return err } enabledTimestamp := repoConfig.SalsaFlowEnabledTimestamp // Only check the project remote. if remoteName != gitConfig.RemoteName { log.Log( fmt.Sprintf( "Not pushing to the main project remote (%v), check skipped", gitConfig.RemoteName)) return nil } // The commits that are being pushed are listed on stdin. // The format is <local ref> <local sha1> <remote ref> <remote sha1>, // so we parse the input and collect all the local hexshas. var coreRefs = []string{ "refs/heads/" + gitConfig.TrunkBranchName, "refs/heads/" + gitConfig.ReleaseBranchName, "refs/heads/" + gitConfig.StagingBranchName, "refs/heads/" + gitConfig.StableBranchName, } parseTask := "Parse the hook input" var revRanges []*revisionRange scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { var ( line = scanner.Text() parts = strings.Split(line, " ") ) if len(parts) != 4 { return errs.NewError(parseTask, errors.New("invalid input line: "+line)) } localRef, localSha, remoteRef, remoteSha := parts[0], parts[1], parts[2], parts[3] // Skip the refs that are being deleted. if localSha == git.ZeroHash { continue } // Check only updates to the core branches, // i.e. trunk, release, client or master. var isCoreBranch bool for _, ref := range coreRefs { if remoteRef == ref { isCoreBranch = true } } if !isCoreBranch { continue } // Make sure the reference is up to date. // In this case the reference is not up to date when // the remote hash cannot be found in the local clone. if remoteSha != git.ZeroHash { task := fmt.Sprintf("Make sure remote ref '%s' is up to date", remoteRef) if _, err := git.Run("cat-file", "-t", remoteSha); err != nil { hint := fmt.Sprintf(` Commit %v does not exist locally. This is probably because '%v' is not up to date. Please update the reference from the remote repository, perhaps by executing 'git pull'. `, remoteSha, remoteRef) return errs.NewErrorWithHint(task, err, hint) } } // Append the revision range for this input line. var revRange *revisionRange if remoteSha == git.ZeroHash { // In case we are pushing a new branch, check commits up to trunk. // There is probably no better guess that we can do in general. revRange = &revisionRange{gitConfig.TrunkBranchName, localRef} } else { // Otherwise check the commits that are new compared to the remote ref. revRange = &revisionRange{remoteSha, localRef} } revRanges = append(revRanges, revRange) } if err := scanner.Err(); err != nil { return errs.NewError(parseTask, err) } // Check the missing Story-Id tags. var missing []*git.Commit for _, revRange := range revRanges { // Get the commit objects for the relevant range. task := "Get the commit objects to be pushed" commits, err := git.ShowCommitRange(fmt.Sprintf("%v..%v", revRange.From, revRange.To)) if err != nil { return errs.NewError(task, err) } // Check every commit in the range. for _, commit := range commits { // Do not check merge commits. if commit.Merge != "" { continue } // Do not check commits that happened before SalsaFlow. if commit.AuthorDate.Before(enabledTimestamp) { continue } // Check the Story-Id tag. if commit.StoryIdTag == "" { missing = append(missing, commit) } } } // Prompt for confirmation in case that is needed. if len(missing) != 0 { // Fill in the commit sources. task := "Fix commit sources" if err := git.FixCommitSources(missing); err != nil { return errs.NewError(task, err) } // Prompt the user for confirmation. task = "Prompt the user for confirmation" confirmed, err := promptUserForConfirmation(missing) if err != nil { return errs.NewError(task, err) } if !confirmed { return prompt.ErrCanceled } } return nil }
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 }
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 }
func Init(force bool) error { // Check whether the repository has been initialised yet. task := "Check whether the repository has been initialised" versionString, err := git.GetConfigString("salsaflow.initialised") if err != nil { return errs.NewError(task, err) } if versionString == metadata.Version && !force { return errs.NewError(task, ErrInitialised) } log.Log("Initialising the repository for SalsaFlow") // Make sure the user is using the right version of Git. // // The check is here and not in app.Init because it is highly improbable // that the check would pass once and then fail later. Once the right // version of git is installed, it most probably stays. task = "Check the git version being used" log.Run(task) stdout, err := git.Run("--version") if err != nil { return errs.NewError(task, err) } pattern := regexp.MustCompile("^git version (([0-9]+)[.]([0-9]+).*)") parts := pattern.FindStringSubmatch(stdout.String()) if len(parts) != 4 { return errs.NewError(task, errors.New("unexpected git --version output")) } gitVersion := parts[1] // This cannot fail since we matched the regexp. major, _ := strconv.Atoi(parts[2]) minor, _ := strconv.Atoi(parts[3]) // We need Git version 1.8.5.4+, so let's require 1.9+. switch { case major >= 2: // OK case major == 1 && minor >= 9: // OK default: hint := ` You need Git version 1.9.0 or newer. ` return errs.NewErrorWithHint( task, errors.New("unsupported git version detected: "+gitVersion), hint) } // Get hold of a git config instance. gitConfig, err := git.LoadConfig() if err != nil { return err } var ( remoteName = gitConfig.RemoteName trunkBranch = gitConfig.TrunkBranchName stableBranch = gitConfig.StableBranchName ) // Make sure that the stable branch exists. task = fmt.Sprintf("Make sure branch '%v' exists", stableBranch) log.Run(task) err = git.CheckOrCreateTrackingBranch(stableBranch, remoteName) if err != nil { if ex, ok := err.(*git.ErrRefNotFound); ok { hint := fmt.Sprintf( "Make sure that branch '%v' exists and run init again.\n", ex.Ref) return errs.NewErrorWithHint(task, err, hint) } else if _, ok := err.(*git.ErrRefNotInSync); !ok { // We ignore ErrRefNotInSync here, so return the error // in case it is of some other kind. return errs.NewError(task, err) } } // Make sure that the trunk branch exists. task = fmt.Sprintf("Make sure branch '%v' exists", trunkBranch) log.Run(task) err = git.CheckOrCreateTrackingBranch(trunkBranch, remoteName) if err != nil { if _, ok := err.(*git.ErrRefNotFound); ok { task := fmt.Sprintf("Create branch '%v'", trunkBranch) log.Log(fmt.Sprintf( "Branch '%v' not found. Will create one for you for free!", trunkBranch)) if err := git.Branch(trunkBranch, stableBranch); err != nil { return errs.NewError(task, err) } log.NewLine(fmt.Sprintf( "The newly created branch is pointing to '%v'.", stableBranch)) task = fmt.Sprintf("Push branch '%v' to remote '%v'", trunkBranch, remoteName) log.Run(task) if err := git.Push(remoteName, trunkBranch+":"+trunkBranch); err != nil { return errs.NewError(task, err) } } else if _, ok := err.(*git.ErrRefNotInSync); !ok { // We ignore ErrRefNotInSync here, so return the error // in case it is of some other kind. return errs.NewError(task, err) } } // Verify our git hooks are installed and used. for _, kind := range hooks.HookTypes { task := fmt.Sprintf("Check the current git %v hook", kind) log.Run(task) if err := hooks.CheckAndUpsert(kind, force); err != nil { return errs.NewError(task, err) } } // Run other registered init hooks. task = "Running the registered repository init hooks" log.Log(task) if err := executeInitHooks(); err != nil { return errs.NewError(task, err) } // Success! Mark the repository as initialised in git config. task = "Mark the repository as initialised" if err := git.SetConfigString("salsaflow.initialised", metadata.Version); err != nil { return err } asciiart.PrintThumbsUp() fmt.Println() log.Log("The repository is initialised") return nil }
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 }
func runMain() (err error) { // Load repo config. gitConfig, err := git.LoadConfig() if err != nil { return err } var ( remoteName = gitConfig.RemoteName releaseBranch = gitConfig.ReleaseBranchName ) // Make sure that the local release branch exists. task := "Make sure that the local release branch exists" if err := git.CheckOrCreateTrackingBranch(releaseBranch, remoteName); err != nil { return errs.NewError(task, err) } // Get the current release version string. task = "Get the release branch version string" releaseVersion, err := version.GetByBranch(releaseBranch) 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 story changes. task = "Collect the story 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) if flagToCherryPick { groups, err = releases.StoryChangesToCherryPick(groups) if err != nil { return errs.NewError(task, err) } } // Dump the change details into the console. if !flagPorcelain { fmt.Println() } changes.DumpStoryChanges(os.Stdout, groups, tracker, flagPorcelain) if !flagPorcelain { fmt.Println() } 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 { 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 }
func postTip() (err error) { // Load Git-related config. gitConfig, err := git.LoadConfig() if err != nil { return err } var ( remoteName = gitConfig.RemoteName ) // Get the current branch. currentBranch, err := gitutil.CurrentBranch() if err != nil { return err } // Get the commit to be posted task := "Get the commit to be posted for code review" commits, err := git.ShowCommits(currentBranch) if err != nil { return errs.NewError(task, err) } // Assert that things are consistent. if numCommits := len(commits); numCommits != 1 { panic(fmt.Sprintf("len(commits): expected 1, got %v", numCommits)) } // Make sure the commit is not a merge commit. if err := ensureNoMergeCommits(commits); err != nil { return err } // Prompt the user to confirm. if err := promptUserToConfirmCommits(commits); err != nil { return err } // Make sure the Story-Id tag is there. commits, changed, err := ensureStoryId(commits) if err != nil { return err } // Push the current branch if necessary. doPush := changed if !doPush { // Check whether the remote branch actually exists. task := fmt.Sprintf( "Make sure branch '%v' exists in remote '%v'", currentBranch, remoteName) exists, err := git.RemoteBranchExists(currentBranch, remoteName) if err != nil { return errs.NewError(task, err) } doPush = !exists } if !doPush { // In case the branch was not modified and it exists remotely, // check whether it is up to date. task := fmt.Sprintf("Check whether branch '%v' is up to date", currentBranch) upToDate, err := git.IsBranchSynchronized(currentBranch, remoteName) if err != nil { return errs.NewError(task, err) } doPush = !upToDate } // Push the branch. if doPush { if err := push(remoteName, currentBranch); err != nil { return err } } // In case the commit was changed, reload. if changed { commits, err = git.ShowCommits(currentBranch) if err != nil { return err } } // Post the commit for review. if err := postCommitsForReview(commits); err != nil { return err } // Print the followup dialog. return printFollowup() }
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 }
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 }
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 }