コード例 #1
0
ファイル: command.go プロジェクト: salsaflow/salsaflow
func runMain(versionString string) error {
	// Make sure the version string is correct.
	task := "Parse the command line VERSION argument"
	ver, err := version.Parse(versionString)
	if err != nil {
		hint := `
The version string must be in the form of Major.Minor.Patch
and no part of the version string can be omitted.

`
		return errs.NewErrorWithHint(task, err, hint)
	}

	// In case -commit is set, set and commit the version string.
	if flagCommit {
		currentBranch, err := gitutil.CurrentBranch()
		if err != nil {
			return err
		}

		_, err = version.SetForBranch(ver, currentBranch)
		return err
	}

	// Otherwise just set the version.
	return version.Set(ver)
}
コード例 #2
0
ファイル: script_utils.go プロジェクト: salsaflow/salsaflow
func GetByBranch(branch string) (ver *Version, err error) {
	// Remember the current branch.
	currentBranch, err := gitutil.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
}
コード例 #3
0
ファイル: config.go プロジェクト: salsaflow/salsaflow
func readAndUnmarshalConfig(absolutePath string, v interface{}) error {
	currentBranch := func() string {
		branch, err := gitutil.CurrentBranch()
		if err != nil {
			return err.Error()
		}
		return branch
	}

	// Read the file.
	task := "Read given configuration file"
	content, err := ioutil.ReadFile(absolutePath)
	if err != nil {
		hint := fmt.Sprintf(`
Failed to read the configuration file expected to be located at

  %v

The Git branch where the error occurred: %v

Make sure the configuration file exists and is committed
at the Git branch mentioned above.

You might want to check 'repo bootstrap' command
to see how the given configuration file can be generated.

`, absolutePath, currentBranch())
		return errs.NewErrorWithHint(task, err, hint)
	}

	// Unmarshall the content.
	task = "Unmarshal given configuration file"
	if err := Unmarshal(content, v); err != nil {
		hint := fmt.Sprintf(`
Failed to parse the configuration file located at

  %v

The Git branch where the error occurred: %v

Make sure the configuration file is valid JSON
that follows the right configuration schema.

You might want to check 'repo bootstrap' command
to see how the given configuration file can be re-generated.

`, absolutePath, currentBranch())

		return errs.NewErrorWithHint(task, err, hint)
	}

	return nil
}
コード例 #4
0
ファイル: common.go プロジェクト: salsaflow/salsaflow
func sendReviewRequests(ctxs []*common.ReviewContext, implemented bool) error {
	// Instantiate the code review module.
	tool, err := modules.GetCodeReviewTool()
	if err != nil {
		return err
	}

	// Collect the command line flags into a map.
	var postOpts = make(map[string]interface{}, 2)
	if flagFixes != 0 {
		postOpts["fixes"] = flagFixes
	}
	if flagReviewer != "" {
		postOpts["reviewer"] = flagReviewer
	}
	if flagOpen {
		postOpts["open"] = true
	}
	if implemented {
		postOpts["implemented"] = true
	}

	// Only post a single commit in case -parent is not being used.
	// By definition it must be only a single commit anyway.
	if flagParent == "" {
		task := "Post review request for commit " + ctxs[0].Commit.SHA
		log.Run(task)
		if err := tool.PostReviewRequests(ctxs, postOpts); err != nil {
			return errs.NewError(task, err)
		}
		return nil
	}

	// Post the review for the whole branch.
	currentBranch, err := gitutil.CurrentBranch()
	if err != nil {
		return err
	}

	task := fmt.Sprintf("Post review request for branch '%v'", currentBranch)
	if err := tool.PostReviewRequests(ctxs, postOpts); err != nil {
		return errs.NewError(task, err)
	}

	return nil
}
コード例 #5
0
ファイル: command.go プロジェクト: salsaflow/salsaflow
func collectStoryBranches(remoteName string) ([]*git.GitBranch, error) {
	// Load Git branches.
	branches, err := git.Branches()
	if err != nil {
		return nil, err
	}

	// Get the current branch name so that it can be excluded.
	currentBranch, err := gitutil.CurrentBranch()
	if err != nil {
		return nil, err
	}

	// Filter the branches.
	storyBranches := make([]*git.GitBranch, 0, len(branches))
	for _, branch := range branches {
		// Drop branches not corresponding to the project remote.
		if branch.Remote != "" && branch.Remote != remoteName {
			continue
		}

		var (
			isLocalStoryBranch  = strings.HasPrefix(branch.BranchName, StoryBranchPrefix)
			isRemoteStoryBranch = strings.HasPrefix(branch.RemoteBranchName, StoryBranchPrefix)
		)

		// Exclude the current branch.
		if isLocalStoryBranch && branch.BranchName == currentBranch {
			log.Warn(fmt.Sprintf("Branch '%v' is checked out, it cannot be deleted", currentBranch))
			continue
		}

		// Keep the story branches only.
		if isLocalStoryBranch || isRemoteStoryBranch {
			storyBranches = append(storyBranches, branch)
		}
	}

	// Return the result.
	return storyBranches, nil
}
コード例 #6
0
ファイル: common.go プロジェクト: salsaflow/salsaflow
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
}
コード例 #7
0
ファイル: mode_tip.go プロジェクト: salsaflow/salsaflow
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()
}
コード例 #8
0
ファイル: mode_parent.go プロジェクト: salsaflow/salsaflow
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
}
コード例 #9
0
ファイル: mode_parent.go プロジェクト: salsaflow/salsaflow
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()
}
コード例 #10
0
ファイル: script_utils.go プロジェクト: salsaflow/salsaflow
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 := gitutil.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("--hard"); 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)
		task := fmt.Sprintf("Reset branch '%v' to the original position", branch)

		// Get the current branch name.
		currentBranch, err := gitutil.CurrentBranch()
		if err != nil {
			return errs.NewError(task, err)
		}

		// Use SetBranch in case we are not on the branch to be modified.
		if branch != currentBranch {
			return errs.Wrap(task, git.SetBranch(branch, originalPosition))
		}

		// Otherwise use git reset --hard since currentBranch == branch.
		return errs.Wrap(task, git.Reset("--hard", originalPosition))
	}), nil
}
コード例 #11
0
ファイル: command.go プロジェクト: salsaflow/salsaflow
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
}
コード例 #12
0
ファイル: stage.go プロジェクト: salsaflow/salsaflow
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
}
コード例 #13
0
ファイル: command.go プロジェクト: salsaflow/salsaflow
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
}
コード例 #14
0
ファイル: command.go プロジェクト: salsaflow/salsaflow
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
}
コード例 #15
0
ファイル: git.go プロジェクト: salsaflow/salsaflow
// EnsureBranchSynchronized makes sure the given branch is up to date.
// If that is not the case, *ErrRefNoInSync is returned.
func EnsureBranchSynchronized(branch, remote string) (err error) {
	// Check whether the remote counterpart actually exists.
	exists, err := RemoteBranchExists(branch, remote)
	if err != nil {
		return err
	}
	if !exists {
		return nil
	}

	// Get the data needed.
	var (
		localRef  = fmt.Sprintf("refs/heads/%v", branch)
		remoteRef = fmt.Sprintf("refs/remotes/%v/%v", remote, branch)
	)
	localHexsha, err := Hexsha(localRef)
	if err != nil {
		return err
	}
	remoteHexsha, err := Hexsha(remoteRef)
	if err != nil {
		return err
	}
	if localHexsha == remoteHexsha {
		// The branch is up to date, we are done here.
		return nil
	}

	// Check whether the local branch can be fast-forwarded.
	remoteBranch := fmt.Sprintf("%v/%v", remote, branch)
	_, err = Run("merge-base", "--is-ancestor", branch, remoteBranch)
	if err != nil {
		// --is-ancestor returns exit status 0 on true, 1 on false, some other on error.
		// We cannot check the value in a platform-independent way, but we count on the fact that
		// stderr will be non-empty on error.
		ex, ok := err.(errs.Err)
		if !ok || len(ex.Hint()) != 0 {
			// In case err is not implementing errs.Err or len(stderr) != 0, we return the error.
			return err
		}
		// Otherwise the error means that --is-ancestor returned false,
		// so we cannot fast-forward and we have to return an error.
		return &ErrRefNotInSync{branch}
	}

	// Perform a fast-forward merge.
	// Ask the user before doing so.
	fmt.Println()
	fmt.Printf("Branch '%v' is behind '%v', and can be fast-forwarded.\n", branch, remoteBranch)
	proceed, err := prompt.Confirm("Shall we perform the merge? It's all safe!", true)
	fmt.Println()
	if err != nil {
		return err
	}
	if !proceed {
		return &ErrRefNotInSync{branch}
	}

	// Make sure the right branch is checked out.
	currentBranch, err := gitutil.CurrentBranch()
	if err != nil {
		return err
	}
	if branch != currentBranch {
		// Checkout the branch to be merged.
		task := fmt.Sprintf("Checkout branch '%v'", branch)
		if err := Checkout(branch); err != nil {
			return errs.NewError(task, err)
		}
		defer func() {
			// Checkout the original branch on return.
			task := fmt.Sprintf("Checkout branch '%v'", currentBranch)
			if ex := Checkout(currentBranch); ex != nil {
				if err == nil {
					err = ex
				} else {
					errs.LogError(task, err)
				}
			}
		}()
	}

	// Merge. Use --ff-only, just to be sure.
	// But we have already checked that this will be a fast-forward merge.
	_, err = Run("merge", "--ff-only", remoteBranch)
	if err != nil {
		return err
	}

	log.Log(fmt.Sprintf("Branch '%v' fast-forwarded onto '%v'", branch, remoteBranch))
	return nil
}
コード例 #16
0
ファイル: command.go プロジェクト: salsaflow/salsaflow
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
}