// 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 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() 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 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 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 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 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 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 }