Beispiel #1
0
func promptUserToConfirmCommits(commits []*git.Commit) error {
	// Make sure there are actually some commits to be posted.
	task := "Make sure there are actually some commits to be posted"
	if len(commits) == 0 {
		return errs.NewError(task, ErrNoCommits)
	}

	// Tell the user what is going to happen.
	fmt.Print(`
You are about to post some of the following commits for code review:

`)
	mustListCommits(os.Stdout, commits, "  ")

	// Ask the user for confirmation.
	task = "Prompt the user for confirmation"
	confirmed, err := prompt.Confirm("\nYou cool with that?", true)
	if err != nil {
		return errs.NewError(task, err)
	}
	if !confirmed {
		prompt.PanicCancel()
	}
	fmt.Println()
	return nil
}
Beispiel #2
0
func (release *nextRelease) PromptUserToConfirmStart() (bool, error) {
	// Collect the issues to be added to the current release.
	task := "Collect the issues that modified trunk since the last release"
	log.Run(task)
	ids, err := releases.ListStoryIdsToBeAssigned(release.tracker)
	if err != nil {
		return false, errs.NewError(task, err)
	}

	// Fetch the additional issues from JIRA.
	task = "Fetch the collected issues from JIRA"
	log.Run(task)
	issues, err := listStoriesById(newClient(release.tracker.config), ids)
	if len(issues) == 0 && err != nil {
		return false, errs.NewError(task, err)
	}
	if len(issues) != len(ids) {
		log.Warn("Some issues were dropped since they were not found in JIRA")
	}

	// Drop the issues that were already assigned to the right version.
	releaseLabel := release.trunkVersion.ReleaseTagString()
	filteredIssues := make([]*jira.Issue, 0, len(issues))
IssueLoop:
	for _, issue := range issues {
		// Add only the parent tasks, i.e. skip sub-tasks.
		if issue.Fields.Parent != nil {
			continue
		}
		// Add only the issues that have not been assigned to the release yet.
		for _, label := range issue.Fields.Labels {
			if label == releaseLabel {
				continue IssueLoop
			}
		}
		filteredIssues = append(filteredIssues, issue)
	}
	issues = filteredIssues

	// Present the issues to the user.
	if len(issues) != 0 {
		fmt.Println("\nThe following issues are going to be added to the release:\n")
		err := prompt.ListStories(toCommonStories(issues, release.tracker), os.Stdout)
		if err != nil {
			return false, err
		}
	}

	// Ask the user to confirm.
	ok, err := prompt.Confirm(
		fmt.Sprintf(
			"\nAre you sure you want to start release %v?",
			release.trunkVersion.BaseString()))
	if err == nil {
		release.additionalIssues = issues
	}
	return ok, err
}
Beispiel #3
0
func checkCommits(
	tracker common.IssueTracker,
	release common.RunningRelease,
	releaseBranch string,
) error {

	var task = "Make sure no changes are being left behind"
	log.Run(task)

	stories, err := release.Stories()
	if err != nil {
		return errs.NewError(task, err)
	}
	if len(stories) == 0 {
		return nil
	}

	groups, err := changes.StoryChanges(stories)
	if err != nil {
		return errs.NewError(task, err)
	}

	toCherryPick, err := releases.StoryChangesToCherryPick(groups)
	if err != nil {
		return errs.NewError(task, err)
	}

	// In case there are some changes being left behind,
	// ask the user to confirm whether to proceed or not.
	if len(toCherryPick) == 0 {
		return nil
	}

	fmt.Println(`
Some changes are being left behind!

In other words, some changes that are assigned to the current release
have not been cherry-picked onto the release branch yet.
	`)
	if err := changes.DumpStoryChanges(os.Stdout, toCherryPick, tracker, false); err != nil {
		panic(err)
	}
	fmt.Println()

	confirmed, err := prompt.Confirm("Are you sure you really want to stage the release?", false)
	if err != nil {
		return errs.NewError(task, err)
	}
	if !confirmed {
		prompt.PanicCancel()
	}
	fmt.Println()

	return nil
}
Beispiel #4
0
func promptUserToChooseBranches(branches []*gitBranch) (local, remote []string, err error) {
	// Go through the branches and ask the user for confirmation.
	var (
		localToDelete  = make([]string, 0, len(branches))
		remoteToDelete = make([]string, 0, len(branches))
	)

	defer fmt.Println()

	for _, branch := range branches {
		tip := branch.tip
		isLocal := tip.BranchName != ""
		isRemote := tip.RemoteBranchName != ""

		var msg string
		switch {
		case isLocal && isRemote:
			msg = fmt.Sprintf(
				"Processing local branch '%v' and its remote counterpart", tip.BranchName)
		case isLocal:
			msg = fmt.Sprintf(
				"Processing local branch '%v'", tip.BranchName)
		case isRemote:
			msg = fmt.Sprintf(
				"Processing remote branch '%v'", tip.FullRemoteBranchName())
		default:
			panic("bullshit")
		}
		fmt.Println()
		fmt.Println(msg)

		if branch.reason != "merged" {
			color.Yellow("Careful now, the branch has not been merged into trunk yet.")
		}

		confirmed, err := prompt.Confirm("Are you sure you want to delete the branch?", false)
		if err != nil {
			return nil, nil, err
		}
		if !confirmed {
			continue
		}

		if isLocal {
			localToDelete = append(localToDelete, tip.BranchName)
		}
		if isRemote {
			remoteToDelete = append(remoteToDelete, tip.RemoteBranchName)
		}
	}
	return localToDelete, remoteToDelete, nil
}
Beispiel #5
0
func promptUserForConfirmation(commits []*git.Commit) (bool, error) {
	// Open the console.
	console, err := prompt.OpenConsole(os.O_WRONLY)
	if err != nil {
		return false, err
	}
	defer console.Close()

	// Print the list of commits missing the Story-Id tag.
	fmt.Fprintln(console)
	hooks.PrintUnassignedWarning(console, commits)
	fmt.Fprintln(console)

	// Prompt the user for confirmation.
	defer fmt.Fprintln(console)
	return prompt.Confirm("Are you sure you want to push these commits?", false)
}
Beispiel #6
0
func selectCommitsForReview(commits []*git.Commit) ([]*git.Commit, error) {
	cs := make([]*git.Commit, 0, len(commits))

	fmt.Println("\nSelect the commits to be posted for code review (-pick flag set):\n")

	for _, commit := range commits {
		selected, err := prompt.Confirm(
			fmt.Sprintf("  %v | %v", commit.SHA, commit.MessageTitle), true)
		if err != nil {
			return nil, err
		}
		if selected {
			cs = append(cs, commit)
		}
	}

	return cs, nil
}
Beispiel #7
0
// PromptUserToConfirm is a part of common.NextRelease interface.
func (release *nextRelease) PromptUserToConfirmStart() (bool, error) {
	// Fetch the stories already assigned to the release.
	var (
		ver       = release.trunkVersion
		verString = ver.BaseString()
	)
	task := fmt.Sprintf("Fetch GitHub issues already assigned to release %v", verString)
	log.Run(task)
	assignedIssues, err := release.tracker.issuesByRelease(ver)
	if err != nil {
		return false, errs.NewError(task, err)
	}

	// Collect the issues that modified trunk since the last release.
	task = "Collect the issues that modified trunk since the last release"
	log.Run(task)
	issueNumsString, err := releases.ListStoryIdsToBeAssigned(release.tracker)
	if err != nil {
		return false, errs.NewError(task, err)
	}

	// Turn []string into []int.
	issueNums := make([]int, len(issueNumsString))
	for i, numString := range issueNumsString {
		// numString is #ISSUE_NUMBER.
		num, err := strconv.Atoi(numString[1:])
		if err != nil {
			panic(err)
		}
		issueNums[i] = num
	}

	// Drop the stories that are already assigned.
	numSet := make(map[int]struct{}, len(assignedIssues))
	for _, issue := range assignedIssues {
		numSet[*issue.Number] = struct{}{}
	}
	nums := make([]int, 0, len(issueNums))
	for _, num := range issueNums {
		if _, ok := numSet[num]; !ok {
			nums = append(nums, num)
		}
	}
	issueNums = nums

	// Fetch the collected issues from GitHub, if necessary.
	var additionalIssues []*github.Issue
	if len(issueNums) != 0 {
		task = "Fetch the collected issues from GitHub"
		log.Run(task)

		var err error
		additionalIssues, err = release.tracker.issuesByNumber(issueNums)
		if err != nil {
			return false, errs.NewError(task, err)
		}

		// Drop stories already assigned to another release.
		notAssigned := make([]*github.Issue, 0, len(additionalIssues))
		for _, issue := range additionalIssues {
			switch {
			case issue.Milestone == nil:
				notAssigned = append(notAssigned, issue)
			default:
				log.Warn(fmt.Sprintf(
					"Skipping issue #%v: modified trunk, but already assigned to milestone '%v'",
					*issue.Number, *issue.Milestone.Title))
			}
		}
		additionalIssues = notAssigned
	}

	// Print the summary into the console.
	summary := []struct {
		header string
		issues []*github.Issue
	}{
		{
			"The following issues were manually assigned to the release:",
			assignedIssues,
		},
		{
			"The following issues were added automatically (modified trunk):",
			additionalIssues,
		},
	}
	for _, item := range summary {
		if len(item.issues) != 0 {
			fmt.Println()
			fmt.Println(item.header)
			fmt.Println()
			err := storyprompt.ListStories(
				toCommonStories(item.issues, release.tracker), os.Stdout)
			if err != nil {
				return false, err
			}
		}
	}

	// Ask the user to confirm.
	ok, err := prompt.Confirm(
		fmt.Sprintf("\nAre you sure you want to start release %v?", verString), false)
	if err == nil {
		release.additionalIssues = additionalIssues
	}
	return ok, err
}
Beispiel #8
0
func Upgrade(opts *InstallOptions) error {
	// Get GitHub owner and repository names.
	var (
		owner = DefaultGitHubOwner
		repo  = DefaultGitHubRepo
	)
	if opts != nil {
		if opts.GitHubOwner != "" {
			owner = opts.GitHubOwner
		}
		if opts.GitHubRepo != "" {
			repo = opts.GitHubRepo
		}
	}

	// Instantiate a GitHub client.
	task := "Instantiate a GitHub client"
	client, err := newGitHubClient()
	if err != nil {
		return errs.NewError(task, err)
	}

	// Fetch the list of available GitHub releases.
	task = fmt.Sprintf("Fetch GitHub releases for %v/%v", owner, repo)
	log.Run(task)
	releases, _, err := client.Repositories.ListReleases(owner, repo, nil)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Sort the releases by version and get the most recent release.
	task = "Select the most suitable GitHub release"
	var rs releaseSlice
	for i, release := range releases {
		// Skip drafts and pre-releases.
		if *release.Draft || *release.Prerelease {
			continue
		}
		// We expect the tag to be "v" + semver version string.
		version, err := version.Parse((*release.TagName)[1:])
		if err != nil {
			log.Warn(fmt.Sprintf("Tag format invalid for '%v', skipping...", release.TagName))
			continue
		}
		// Append the release to the list of releases.
		rs = append(rs, &githubRelease{
			version:  version,
			resource: &releases[i],
		})
	}
	if rs.Len() == 0 {
		return errs.NewError(task, errors.New("no suitable GitHub releases found"))
	}

	sort.Sort(rs)
	release := rs[len(rs)-1]

	// Make sure the selected release is more recent than this executable.
	currentVersion, err := version.Parse(metadata.Version)
	if err != nil {
		panic(err)
	}
	if release.version.String() == metadata.Version || release.version.LT(currentVersion.Version) {
		log.Log("SalsaFlow is up to date")
		asciiart.PrintThumbsUp()
		fmt.Println()
		return nil
	}

	// Prompt the user to confirm the upgrade.
	task = "Prompt the user to confirm upgrade"
	fmt.Println()
	confirmed, err := prompt.Confirm(fmt.Sprintf(
		"SalsaFlow version %v is available. Upgrade now?", release.version))
	if err != nil {
		return errs.NewError(task, err)
	}
	if !confirmed {
		return ErrAborted
	}
	fmt.Println()

	// Proceed to actually install the executables.
	return doInstall(client, owner, repo, release.resource.Assets, release.version.String())
}
Beispiel #9
0
func (release *nextRelease) PromptUserToConfirmStart() (bool, error) {
	// Fetch the stories already assigned to the release.
	var (
		ver       = release.trunkVersion
		verString = ver.BaseString()
		verLabel  = ver.ReleaseTagString()
	)
	task := fmt.Sprintf("Fetch the stories already assigned to release %v", verString)
	log.Run(task)
	assignedStories, err := release.tracker.storiesByRelease(ver)
	if err != nil {
		return false, errs.NewError(task, err)
	}

	// Collect the story IDs associated with the commits that
	// modified trunk since the last release.
	task = "Collect the stories that modified trunk since the last release"
	log.Run(task)
	storyIds, err := releases.ListStoryIdsToBeAssigned(release.tracker)
	if err != nil {
		return false, errs.NewError(task, err)
	}

	// Drop the stories that are already assigned.
	idSet := make(map[string]struct{}, len(assignedStories))
	for _, story := range assignedStories {
		idSet[strconv.Itoa(story.Id)] = struct{}{}
	}
	ids := make([]string, 0, len(storyIds))
	for _, id := range storyIds {
		if _, ok := idSet[id]; !ok {
			ids = append(ids, id)
		}
	}
	storyIds = ids

	// Fetch the collected stories from Pivotal Tracker, if necessary.
	var additionalStories []*pivotal.Story
	if len(storyIds) != 0 {
		task = "Fetch the collected stories from Pivotal Tracker"
		log.Run(task)

		var err error
		additionalStories, err = release.tracker.storiesById(storyIds)
		if len(additionalStories) == 0 && err != nil {
			return false, errs.NewError(task, err)
		}
		if len(additionalStories) != len(storyIds) {
			log.Warn("Some stories were dropped since they were not found in PT")
			log.NewLine("or they were filtered out by a story include label.")
		}

		// Drop stories already assigned to another release.
		notAssigned := make([]*pivotal.Story, 0, len(additionalStories))
	NotAssignedLoop:
		for _, story := range additionalStories {
			for _, label := range story.Labels {
				if isReleaseLabel(label.Name) {
					log.Warn(fmt.Sprintf(
						"Skipping story %v: modified trunk, but already labeled '%v'",
						story.Id, label.Name))
					continue NotAssignedLoop
				}
			}
			notAssigned = append(notAssigned, story)
		}
		additionalStories = notAssigned
	}

	// Check the Point Me label.
	task = "Make sure there are no unpointed stories"
	log.Run(task)
	pmLabel := release.tracker.config.PointMeLabel

	// Fetch the already assigned but unpointed stories.
	pmStories, err := release.tracker.searchStories(
		"label:\"%v\" AND label:\"%v\"", verLabel, pmLabel)
	if err != nil {
		return false, errs.NewError(task, err)
	}
	// Also add these that are to be added but are unpointed.
	for _, story := range additionalStories {
		if labeled(story, pmLabel) {
			pmStories = append(pmStories, story)
		}
	}
	// In case there are some unpointed stories, stop the release.
	if len(pmStories) != 0 {
		fmt.Println("\nThe following stories are still yet to be pointed:\n")
		err := storyprompt.ListStories(toCommonStories(pmStories, release.tracker), os.Stdout)
		if err != nil {
			return false, err
		}
		fmt.Println()
		return false, errs.NewError(task, errors.New("unpointed stories detected"))
	}

	// Print the summary into the console.
	summary := []struct {
		header  string
		stories []*pivotal.Story
	}{
		{
			"The following stories were manually assigned to the release:",
			assignedStories,
		},
		{
			"The following stories were added automatically (modified trunk):",
			additionalStories,
		},
	}
	for _, item := range summary {
		if len(item.stories) != 0 {
			fmt.Println()
			fmt.Println(item.header)
			fmt.Println()
			err := storyprompt.ListStories(toCommonStories(item.stories, release.tracker), os.Stdout)
			if err != nil {
				return false, err
			}
		}
	}

	// Ask the user to confirm.
	ok, err := prompt.Confirm(
		fmt.Sprintf(
			"\nAre you sure you want to start release %v?",
			release.trunkVersion.BaseString()), false)
	if err == nil {
		release.additionalStories = additionalStories
	}
	return ok, err
}
Beispiel #10
0
func runMain() (err error) {
	var (
		task   string
		stderr *bytes.Buffer
	)
	defer func() {
		// Print error details.
		if err != nil {
			log.FailWithDetails(task, stderr)
		}
	}()

	// Fetch the remote repository unless we are restricted to the local branches only.
	if !flagLocalOnly {
		task = "Fetch the remote repository"
		log.Run(task)
		stderr, err = git.UpdateRemotes(config.OriginName)
		if err != nil {
			return
		}
	}

	// Get the list of story references.
	task = "Collect all story branches"
	log.Run(task)
	localRefs, remoteRefs, stderr, err := git.ListStoryRefs()
	if err != nil {
		return
	}

	var refs []string
	switch {
	case flagLocalOnly:
		refs = localRefs
	case flagRemoteOnly:
		refs = remoteRefs
	default:
		refs = append(localRefs, remoteRefs...)
	}

	if len(refs) == 0 {
		task = ""
		log.Println("\nNo story branches found, exiting...")
		return
	}

	// Collect all the story IDs.
	idMap := make(map[string]struct{})
	for _, ref := range refs {
		// This cannot fail here since we got the refs using ListStoryRefs.
		id, _ := git.RefToStoryId(ref)
		idMap[id] = struct{}{}
	}

	var ids []string
	for id := range idMap {
		ids = append(ids, id)
	}

	// Get the list of active story IDs.
	activeIds, err := modules.GetIssueTracker().SelectActiveStoryIds(ids)
	if err != nil {
		return
	}
	ids = activeIds

	// Select only the refs that can be safely deleted.
	refs = selectInactiveRefs(refs, ids)

	if len(refs) == 0 {
		task = ""
		log.Println("\nThere are no branches to be deleted, exiting...")
		return
	}

	// Sort the refs.
	sort.Sort(sort.StringSlice(refs))

	// Prompt the user to confirm the delete operation.
	var (
		toDeleteLocally  []string
		toDeleteRemotely []string
		ok               bool
	)

	// Go through the local branches.
	if strings.HasPrefix(refs[0], "refs/heads/") {
		fmt.Println("\n---> Local branches\n")
	}
	for len(refs) > 0 {
		ref := refs[0]
		if !strings.HasPrefix(ref, "refs/heads/") {
			break
		}
		branch := ref[len("refs/heads/"):]
		question := fmt.Sprintf("Delete local branch '%v'", branch)
		ok, err = prompt.Confirm(question)
		if err != nil {
			return
		}
		if ok {
			toDeleteLocally = append(toDeleteLocally, branch)
		}
		refs = refs[1:]
	}

	// All that is left are remote branches.
	if len(refs) != 0 {
		fmt.Println("\n---> Remote branches\n")
	}
	for _, ref := range refs {
		branch := ref[len("refs/remotes/origin/"):]
		question := fmt.Sprintf("Delete remote branch '%v'", branch)
		ok, err = prompt.Confirm(question)
		if err != nil {
			return
		}
		if ok {
			toDeleteRemotely = append(toDeleteRemotely, branch)
		}
	}
	fmt.Println()

	if len(toDeleteLocally) == 0 && len(toDeleteRemotely) == 0 {
		task = ""
		fmt.Println("No branches selected, exiting...")
		return
	}

	// Delete the local branches.
	if len(toDeleteLocally) != 0 {
		task = "Delete the chosen local branches"
		log.Run(task)

		// Remember the position of the branches to be deleted.
		// This is used in case we need to perform a rollback.
		var (
			currentPositions []string
			hexsha           string
		)
		for _, branchName := range toDeleteLocally {
			hexsha, stderr, err = git.Hexsha("refs/heads/" + branchName)
			if err != nil {
				return
			}
			currentPositions = append(currentPositions, hexsha)
		}

		// Delete the selected local branches.
		args := append([]string{"-d"}, toDeleteLocally...)
		stderr, err = git.Branch(args...)
		if err != nil {
			return
		}
		defer func(taskMsg string) {
			// On error, try to restore the local branches that were deleted.
			if err != nil {
				log.Rollback(taskMsg)
				for i, branchName := range toDeleteLocally {
					out, ex := git.ResetKeep(branchName, currentPositions[i])
					if ex != nil {
						log.FailWithDetails(task, out)
					}
				}
			}
		}(task)
	}

	// Delete the remote branches.
	if len(toDeleteRemotely) != 0 {
		task = "Delete the chosen remote branches"
		log.Run(task)
		var refs []string
		for _, branchName := range toDeleteRemotely {
			refs = append(refs, ":"+branchName)
		}
		stderr, err = git.Push(config.OriginName, refs...)
	}
	return
}
Beispiel #11
0
// Check whether SalsaFlow git hook is used. Prompts user to install our hook if it isn't.
//
// When the force argument is set to true, the hook is replaced when though the version matches.
func CheckAndUpsert(hookType HookType, force bool) error {
	// Declade some variables so that we can use goto.
	var confirmed bool

	// Ping the git hook with our secret argument.
	repoRoot, err := gitutil.RepositoryRootAbsolutePath()
	if err != nil {
		return err
	}

	hookDestPath := filepath.Join(repoRoot, ".git", "hooks", string(hookType))

	// Try to get the hook version.
	stdout, _, _ := shell.Run(hookDestPath, "-"+versionFlag)

	// In case the versions match, we are done here (unless force).
	installedVersion, err := version.Parse(strings.TrimSpace(stdout.String()))
	if !force && installedVersion != nil && installedVersion.String() == metadata.Version {
		return nil
	}

	// Get the hook executable absolute path. It's supposed to be installed
	// in the same directory as the salsaflow executable itself.
	task := "Get the executable folder absolute path"
	binDir, err := osext.ExecutableFolder()
	if err != nil {
		return errs.NewError(task, err)
	}
	hookExecutable := filepath.Join(binDir, getHookFileName(hookType))

	// Check whether there is a hook already present in the repository.
	// If there is no hook or there is a SalsaFlow hook returning a different version string,
	// we don't have to ask the user, we can just install the hook.
	task = fmt.Sprintf("Check whether there is a git %v hook already installed", hookType)
	if _, err := os.Stat(hookDestPath); err != nil {
		if os.IsNotExist(err) {
			return copyHook(hookType, hookExecutable, hookDestPath)
		}
		return errs.NewError(task, err)
	}
	if installedVersion != nil || force {
		return copyHook(hookType, hookExecutable, hookDestPath)
	}

	// Prompt the user to confirm the SalsaFlow git commit-task hook.
	task = fmt.Sprintf("Prompt the user to confirm the %v hook", hookType)
	confirmed, err = prompt.Confirm(`
I need my own git ` + string(hookType) + ` hook to be placed in the repository.
Shall I create or replace your current ` + string(hookType) + ` hook?`)
	fmt.Println()
	if err != nil {
		return errs.NewError(task, err)
	}
	if !confirmed {
		// User stubbornly refuses to let us overwrite their webhook.
		// Inform the init has failed and let them do their thing.
		fmt.Printf(`I need the hook in order to do my job!

Please make sure the executable located at

  %v

runs as your `+string(hookType)+` hook and run me again!

`, hookExecutable)
		return errs.NewError(task, fmt.Errorf("SalsaFlow git %v hook not detected", hookType))
	}

	return copyHook(hookType, hookExecutable, hookDestPath)
}
Beispiel #12
0
// 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
}
Beispiel #13
0
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
}
Beispiel #14
0
func Install(version string, opts *InstallOptions) error {
	// Get GitHub owner and repository names.
	var (
		owner     = DefaultGitHubOwner
		repo      = DefaultGitHubRepo
		targetDir string
	)
	if opts != nil {
		if opts.GitHubOwner != "" {
			owner = opts.GitHubOwner
		}
		if opts.GitHubRepo != "" {
			repo = opts.GitHubRepo
		}
		targetDir = opts.TargetDirectory
	}

	// Instantiate a GitHub client.
	task := "Instantiate a GitHub client"
	client, err := newGitHubClient()
	if err != nil {
		return errs.NewError(task, err)
	}

	// Fetch the list of available GitHub releases.
	task = fmt.Sprintf("Fetch GitHub releases for %v/%v", owner, repo)
	log.Run(task)
	releases, err := listReleases(client, owner, repo)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Get the release matching the chosen version string.
	tagName := "v" + version
	task = fmt.Sprintf("Search for the GitHub release associated with tag '%v'", tagName)

	var release *github.RepositoryRelease
	for _, r := range releases {
		if *r.TagName == tagName {
			release = &r
			break
		}
	}

	// Make sure we got a valid release.
	switch {
	case release == nil:
		return errs.NewError(task, fmt.Errorf("SalsaFlow version %v not found", version))
	case *release.Draft:
		return errs.NewError(task, fmt.Errorf("SalsaFlow version %v is a release draft", version))
	case *release.Prerelease:
		return errs.NewError(task, fmt.Errorf("SalsaFlow version %v is a pre-release", version))
	}

	// Prompt the user to confirm the the installation.
	task = "Prompt the user to confirm the installation"
	fmt.Println()
	confirmed, err := prompt.Confirm(fmt.Sprintf(
		"SalsaFlow version %v is about to be installed. Shall we proceed?", version), true)
	if err != nil {
		return errs.NewError(task, err)
	}
	if !confirmed {
		return ErrAborted
	}
	fmt.Println()

	// Proceed to actually install the executables.
	return doInstall(client, owner, repo, release.Assets, version, targetDir)
}
Beispiel #15
0
func (release *nextRelease) PromptUserToConfirmStart() (bool, error) {
	var (
		client         = release.client
		productId      = release.tracker.config.ProductId()
		itemReleaseTag = getItemReleaseTag(release.trunkVersion)
	)

	// Collect the commits that modified trunk since the last release.
	task := "Collect the stories that modified trunk"
	log.Run(task)
	ids, err := releases.ListStoryIdsToBeAssigned(release.tracker)
	if err != nil {
		return false, errs.NewError(task, err)
	}

	// Get the story ids associated with these commits.
	numbers := make([]int, 0, len(ids))
	for _, id := range ids {
		number, err := strconv.Atoi(id)
		if err != nil {
			return false, errs.NewError(task, fmt.Errorf("invalid item number: %v", id), nil)
		}
		numbers = append(numbers, number)
	}

	// Fetch the collected items from Sprintly, if necessary.
	var additional []sprintly.Item
	if len(numbers) != 0 {
		var err error
		// listItemsByNumber lists children as well, so there is no way we can miss a sub-item.
		additional, err = listItemsByNumber(client, productId, numbers)
		if err != nil {
			return false, err
		}

		// Drop the issues that are already assigned to the right release.
		unassignedItems := make([]sprintly.Item, 0, len(additional))
		for _, item := range additional {
			if tagged(&item, itemReleaseTag) {
				continue
			}
			unassignedItems = append(unassignedItems, item)
		}
		additional = unassignedItems
	}

	// Make sure there are no unrated items.
	task = "Make sure there are no unrated items"
	log.Run(task)

	// Check the additional items and collect the unrated ones.
	unrated := make([]sprintly.Item, 0)
	for _, item := range additional {
		if item.Score == sprintly.ItemScoreUnset {
			unrated = append(unrated, item)
		}
	}

	// Fetch the items that were assigned manually.
	assignedItems, err := listItemsByTag(client, productId, []string{itemReleaseTag})
	if err != nil {
		return false, errs.NewError(task, err)
	}

	// Check the manually assigned items and collect the unrated ones.
	for _, item := range assignedItems {
		if item.Score == sprintly.ItemScoreUnset {
			unrated = append(unrated, item)
		}

		// Also, since the sub-items of the assigned items are returned as well,
		// they may be missing the release tag, so let's register them to be tagged.
		if !tagged(&item, itemReleaseTag) {
			additional = append(additional, item)
		}
	}

	// In case there are some unrated items, abort the release process.
	if len(unrated) != 0 {
		fmt.Println("\nThe following items have not been rated yet:\n")
		err := prompt.ListStories(toCommonStories(unrated), os.Stdout)
		if err != nil {
			return false, err
		}
		fmt.Println()
		return false, errs.NewError(task, errors.New("unrated items detected"), nil)
	}

	// Print the items to be added to the release.
	if len(additional) != 0 {
		fmt.Println("\nThe following items are going to be added to the release:\n")
		err := prompt.ListStories(toCommonStories(additional), os.Stdout)
		if err != nil {
			return false, err
		}
	}

	// Ask the user to confirm.
	ok, err := prompt.Confirm(
		fmt.Sprintf("\nAre you sure you want to start release %v?", release.trunkVersion))
	if err == nil {
		release.additionalItems = additional
	}
	return ok, err
}
Beispiel #16
0
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
}
Beispiel #17
0
func (release *nextRelease) PromptUserToConfirmStart() (bool, error) {
	var (
		config       = release.tracker.config
		client       = pivotal.NewClient(config.UserToken())
		releaseLabel = getReleaseLabel(release.trunkVersion)
	)

	// Collect the commits that modified trunk since the last release.
	task := "Collect the stories that modified trunk"
	log.Run(task)
	ids, err := releases.ListStoryIdsToBeAssigned(release.tracker)
	if err != nil {
		return false, errs.NewError(task, err)
	}

	// Fetch the collected stories from Pivotal Tracker, if necessary.
	var additional []*pivotal.Story
	if len(ids) != 0 {
		task = "Fetch the collected stories from Pivotal Tracker"
		log.Run(task)

		var err error
		additional, err = listStoriesById(client, config.ProjectId(), ids)
		if len(additional) == 0 && err != nil {
			return false, errs.NewError(task, err)
		}
		if len(additional) != len(ids) {
			log.Warn("Some stories were dropped since they were not found in PT")
		}

		// Drop the issues that are already assigned to the right release.
		unassignedStories := make([]*pivotal.Story, 0, len(additional))
		for _, story := range additional {
			if labeled(story, releaseLabel) {
				continue
			}
			unassignedStories = append(unassignedStories, story)
		}
		additional = unassignedStories
	}

	// Check the Point Me label.
	task = "Make sure there are no unpointed stories"
	log.Run(task)
	pmLabel := config.PointMeLabel()

	// Fetch the already assigned but unpointed stories.
	pmStories, err := searchStories(client, config.ProjectId(),
		"label:\"%v\" AND label:\"%v\"", releaseLabel, pmLabel)
	if err != nil {
		return false, errs.NewError(task, err)
	}
	// Also add these that are to be added but are unpointed.
	for _, story := range additional {
		if labeled(story, pmLabel) {
			pmStories = append(pmStories, story)
		}
	}
	// In case there are some unpointed stories, stop the release.
	if len(pmStories) != 0 {
		fmt.Println("\nThe following stories are still yet to be pointed:\n")
		err := prompt.ListStories(toCommonStories(pmStories, release.tracker), os.Stdout)
		if err != nil {
			return false, err
		}
		fmt.Println()
		return false, errs.NewError(task, errors.New("unpointed stories detected"))
	}

	// Print the stories to be added to the release.
	if len(additional) != 0 {
		fmt.Println("\nThe following stories are going to be added to the release:\n")
		err := prompt.ListStories(toCommonStories(additional, release.tracker), os.Stdout)
		if err != nil {
			return false, err
		}
	}

	// Ask the user to confirm.
	ok, err := prompt.Confirm(
		fmt.Sprintf(
			"\nAre you sure you want to start release %v?",
			release.trunkVersion.BaseString()))
	if err == nil {
		release.additionalStories = additional
	}
	return ok, err
}
Beispiel #18
0
func implementedDialog(ctxs []*common.ReviewContext) (implemented bool, act action.Action, err error) {
	// Collect the affected stories.
	var (
		stories  = make([]common.Story, 0, len(ctxs))
		storySet = make(map[string]struct{}, len(ctxs))
	)
	for _, ctx := range ctxs {
		story := ctx.Story
		// Skip unassigned commits.
		if story == nil {
			continue
		}
		rid := story.ReadableId()
		if _, ok := storySet[rid]; ok {
			continue
		}
		// Collect only the stories that are Being Implemented.
		// The transition doesn't make sense for other story states.
		if story.State() != common.StoryStateBeingImplemented {
			continue
		}
		storySet[rid] = struct{}{}
		stories = append(stories, story)
	}
	// Do nothing in case there are no stories left.
	if len(stories) == 0 {
		return false, nil, nil
	}

	// Prompt the user for confirmation.
	fmt.Println("\nIt is possible to mark the affected stories as implemented.")
	fmt.Println("The following stories were associated with one or more commits:\n")
	storyprompt.ListStories(stories, os.Stdout)
	fmt.Println()
	confirmed, err := prompt.Confirm(
		"Do you wish to mark these stories as implemented?", false)
	if err != nil {
		return false, nil, err
	}
	fmt.Println()
	if !confirmed {
		return false, nil, nil
	}

	// Always update as many stories as possible.
	var (
		chain           = action.NewActionChain()
		errUpdateFailed = errors.New("failed to update stories in the issue tracker")
		ex              error
	)
	for _, story := range stories {
		task := fmt.Sprintf("Mark story %v as implemented", story.ReadableId())
		log.Run(task)
		act, err := story.MarkAsImplemented()
		if err != nil {
			errs.Log(errs.NewError(task, err))
			ex = errUpdateFailed
			continue
		}
		chain.PushTask(task, act)
	}
	if ex != nil {
		if err := chain.Rollback(); err != nil {
			errs.Log(err)
		}
		return false, nil, ex
	}

	return true, chain, nil
}
Beispiel #19
0
func Install(version string, opts *InstallOptions) error {
	// Get GitHub owner and repository names.
	var (
		owner = DefaultGitHubOwner
		repo  = DefaultGitHubRepo
	)
	if opts != nil {
		if opts.GitHubOwner != "" {
			owner = opts.GitHubOwner
		}
		if opts.GitHubRepo != "" {
			repo = opts.GitHubRepo
		}
	}

	// Instantiate a GitHub client.
	task := "Instantiate a GitHub client"
	client, err := newGitHubClient()
	if err != nil {
		return errs.NewError(task, err)
	}

	// Fetch the list of available GitHub releases.
	task = fmt.Sprintf("Fetch GitHub releases for %v/%v", owner, repo)
	log.Run(task)
	releases, _, err := client.Repositories.ListReleases(owner, repo, nil)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Get the release matching the chosen version string.
	task = "Get the release metadata"
	var (
		release *github.RepositoryRelease
		tagName = "v" + version
	)
	for _, r := range releases {
		if *r.TagName == tagName {
			release = &r
			break
		}
	}
	if release == nil {
		return errs.NewError(task, fmt.Errorf("SalsaFlow version %v not found", version))
	}

	// Prompt the user to confirm the the installation.
	task = "Prompt the user to confirm the installation"
	fmt.Println()
	confirmed, err := prompt.Confirm(fmt.Sprintf(
		"SalsaFlow version %v is about to be installed. Shall we proceed?", version))
	if err != nil {
		return errs.NewError(task, err)
	}
	if !confirmed {
		return ErrAborted
	}
	fmt.Println()

	// Proceed to actually install the executables.
	return doInstall(client, owner, repo, release.Assets, version)
}