func GetByBranch(branch string) (ver *Version, err error) { // Remember the current branch. currentBranch, err := git.CurrentBranch() if err != nil { return nil, err } // Checkout the target branch. if err := git.Checkout(branch); err != nil { return nil, err } defer func() { // Checkout the original branch on return. if ex := git.Checkout(currentBranch); ex != nil { if err == nil { err = ex } else { errs.Log(ex) } } }() // Get the version. v, err := Get() if err != nil { if ex, ok := err.(*scripts.ErrNotFound); ok { return nil, fmt.Errorf( "custom SalsaFlow script '%v' not found on branch '%v'", ex.ScriptName(), branch) } return nil, err } return v, nil }
func rewriteCommits(commits []*git.Commit, firstMissingOffset int) ([]*git.Commit, error) { // Fetch the stories in progress from the issue tracker. storiesTask := "Missing Story-Id detected, fetch stories from the issue tracker" log.Run(storiesTask) tracker, err := modules.GetIssueTracker() if err != nil { return nil, errs.NewError(storiesTask, err) } task := "Fetch the user record from the issue tracker" me, err := tracker.CurrentUser() if err != nil { return nil, errs.NewError(task, err) } stories, err := tracker.ReviewableStories() if err != nil { return nil, errs.NewError(storiesTask, err) } reviewedStories, err := tracker.ReviewedStories() if err != nil { return nil, errs.NewError(storiesTask, err) } // Show only the stories owned by the current user. // Note: Go sucks here, badly. filterStories := func(stories []common.Story, filter func(common.Story) bool) []common.Story { ss := make([]common.Story, 0, len(stories)) for _, story := range stories { if filter(story) { ss = append(ss, story) } } return ss } mine := func(story common.Story) bool { for _, assignee := range story.Assignees() { if assignee.Id() == me.Id() { return true } } return false } stories = filterStories(stories, mine) reviewedStories = filterStories(reviewedStories, mine) // Tell the user what is happening. log.Run("Prepare a temporary branch to rewrite commit messages") // Get the current branch name. currentBranch, err := gitutil.CurrentBranch() if err != nil { return nil, err } // Get the parent of the first commit in the chain. task = "Get the parent commit of the commit chain to be posted" var parentSHA string if firstMissingOffset != 0 { // In case there are multiple commits being posted // and the first missing offset is not pointing to the first commit, // we can easily get the parent SHA by just accessing the commit list. parentSHA = commits[firstMissingOffset-1].SHA } else { // Otherwise we have to ask git for help. stdout, err := git.Log("--pretty=%P", "-n", "1", commits[firstMissingOffset].SHA) if err != nil { return nil, errs.NewError(task, err) } parentSHA = strings.Fields(stdout.String())[0] } // Prepare a temporary branch that will be used to amend commit messages. task = "Create a temporary branch to rewrite commit messages" if err := git.SetBranch(constants.TempBranchName, parentSHA); err != nil { return nil, errs.NewError(task, err) } defer func() { // Delete the temporary branch on exit. task := "Delete the temporary branch" if err := git.Branch("-D", constants.TempBranchName); err != nil { errs.LogError(task, err) } }() // Checkout the temporary branch. task = "Checkout the temporary branch" if err := git.Checkout(constants.TempBranchName); err != nil { return nil, errs.NewError(task, err) } defer func() { // Checkout the original branch on exit. task := fmt.Sprintf("Checkout branch '%v'", currentBranch) if err := git.Checkout(currentBranch); err != nil { errs.LogError(task, err) } }() // Loop and rewrite the commit messages. var story common.Story if flagAskOnce { header := ` Some of the commits listed above are not assigned to any story. Please pick up the story that these commits will be assigned to. You can also insert 'u' to mark the commits as unassigned:` selectedStory, err := promptForStory(header, stories, reviewedStories) if err != nil { return nil, err } story = selectedStory } // The temp branch is pointing to the parent of the first commit missing // the Story-Id tag. So we only need to cherry-pick the commits that // follow the first commit missing the Story-Id tag. commitsToCherryPick := commits[firstMissingOffset:] for _, commit := range commitsToCherryPick { // Cherry-pick the commit. task := fmt.Sprintf("Move commit %v onto the temporary branch", commit.SHA) if err := git.CherryPick(commit.SHA); err != nil { return nil, errs.NewError(task, err) } if commit.StoryIdTag == "" { if !flagAskOnce { commitMessageTitle := prompt.ShortenCommitTitle(commit.MessageTitle) // Ask for the story ID for the current commit. header := fmt.Sprintf(` The following commit is not assigned to any story: commit hash: %v commit title: %v Please pick up the story to assign the commit to. Inserting 'u' will mark the commit as unassigned:`, commit.SHA, commitMessageTitle) selectedStory, err := promptForStory(header, stories, reviewedStories) if err != nil { return nil, err } story = selectedStory } // Use the unassigned tag value in case no story is selected. storyTag := git.StoryIdUnassignedTagValue if story != nil { storyTag = story.Tag() } // Extend the commit message to include Story-Id. commitMessage := fmt.Sprintf("%v\nStory-Id: %v\n", commit.Message, storyTag) // Amend the cherry-picked commit to include the new commit message. task = "Amend the commit message for " + commit.SHA stderr := new(bytes.Buffer) cmd := exec.Command("git", "commit", "--amend", "-F", "-") cmd.Stdin = bytes.NewBufferString(commitMessage) cmd.Stderr = stderr if err := cmd.Run(); err != nil { return nil, errs.NewErrorWithHint(task, err, stderr.String()) } } } // Reset the current branch to point to the new branch. task = "Reset the current branch to point to the temporary branch" if err := git.SetBranch(currentBranch, constants.TempBranchName); err != nil { return nil, errs.NewError(task, err) } // Parse the commits again since the commit hashes have changed. newCommits, err := git.ShowCommitRange(parentSHA + "..") if err != nil { return nil, err } log.NewLine("") log.Log("Commit messages amended successfully") // And we are done! return newCommits, nil }
func merge(mergeTask, current, parent string) (act action.Action, err error) { // Remember the current branch hash. currentSHA, err := git.BranchHexsha(current) if err != nil { return nil, err } // Checkout the parent branch so that we can perform the merge. if err := git.Checkout(parent); err != nil { return nil, err } // Checkout the current branch on return to be consistent. defer func() { if ex := git.Checkout(current); ex != nil { if err == nil { err = ex } else { errs.Log(ex) } } }() // Perform the merge. // Use --no-ff in case -merge_no_ff is set. if flagMergeNoFF { err = git.Merge(current, "--no-ff") } else { err = git.Merge(current) } if err != nil { return nil, err } // Return a rollback action. return action.ActionFunc(func() (err error) { log.Rollback(mergeTask) task := fmt.Sprintf("Reset branch '%v' to the original position", current) // Get the branch is the current branch now. currentNow, err := gitutil.CurrentBranch() if err != nil { return errs.NewError(task, err) } // Checkout current in case it is not the same as the current branch now. if currentNow != current { if err := git.Checkout(current); err != nil { return errs.NewError(task, err) } defer func() { if ex := git.Checkout(currentNow); ex != nil { if err == nil { err = ex } else { errs.Log(ex) } } }() } // Reset the branch to the original position. if err := git.Reset("--keep", currentSHA); err != nil { return errs.NewError(task, err) } return nil }), nil }
func SetForBranch(ver *Version, branch string) (act action.Action, err error) { var mainTask = fmt.Sprintf("Bump version to %v for branch '%v'", ver, branch) // Make sure the repository is clean (don't check untracked files). task := "Make sure the repository is clean" if err := git.EnsureCleanWorkingTree(false); err != nil { return nil, errs.NewError(task, err) } // Remember the current branch. currentBranch, err := git.CurrentBranch() if err != nil { return nil, err } // Remember the current position of the target branch. task = fmt.Sprintf("Remember the position of branch '%v'", branch) originalPosition, err := git.Hexsha("refs/heads/" + branch) if err != nil { return nil, errs.NewError(task, err) } // Checkout the target branch. task = fmt.Sprintf("Checkout branch '%v'", branch) if err := git.Checkout(branch); err != nil { return nil, errs.NewError(task, err) } defer func() { // Checkout the original branch on return. task := fmt.Sprintf("Checkout branch '%v'", currentBranch) if ex := git.Checkout(currentBranch); ex != nil { if err == nil { err = ex } else { errs.LogError(task, ex) } } }() // Set the project version to the desired value. if err := Set(ver); err != nil { if ex, ok := err.(*scripts.ErrNotFound); ok { return nil, fmt.Errorf( "custom SalsaFlow script '%v' not found on branch '%v'", ex.ScriptName(), branch) } return nil, err } // Commit changes. _, err = git.RunCommand("commit", "-a", "-m", fmt.Sprintf("Bump version to %v", ver), "-m", fmt.Sprintf("Story-Id: %v", git.StoryIdUnassignedTagValue)) if err != nil { task := "Reset the working tree to the original state" if err := git.Reset("--keep"); err != nil { errs.LogError(task, err) } return nil, err } return action.ActionFunc(func() (err error) { // On rollback, reset the target branch to the original position. log.Rollback(mainTask) return git.ResetKeep(branch, originalPosition) }), nil }
func 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 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 }