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 }
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 }
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 }
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 }
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) }
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 }
// 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 }
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()) }
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 }
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 }
// 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) }
// 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 }
func createBranch() (action.Action, error) { // Get the current branch name. originalBranch, err := gitutil.CurrentBranch() if err != nil { return nil, err } // Fetch the remote repository. task := "Fetch the remote repository" log.Run(task) gitConfig, err := git.LoadConfig() if err != nil { return nil, errs.NewError(task, err) } var ( remoteName = gitConfig.RemoteName baseBranch = gitConfig.TrunkBranchName ) if flagBase != "" { baseBranch = flagBase } // Fetch the remote repository. if err := git.UpdateRemotes(remoteName); err != nil { return nil, errs.NewError(task, err) } // Make sure the trunk branch is up to date. task = fmt.Sprintf("Make sure branch '%v' is up to date", baseBranch) log.Run(task) if err := git.CheckOrCreateTrackingBranch(baseBranch, remoteName); err != nil { return nil, errs.NewError(task, err) } // Prompt the user for the branch name. task = "Prompt the user for the branch name" line, err := prompt.Prompt(` Please insert the branch slug now. Insert an empty string to skip the branch creation step: `) if err != nil && err != prompt.ErrCanceled { return nil, errs.NewError(task, err) } sluggedLine := slug.Slug(line) if sluggedLine == "" { fmt.Println() log.Log("Not creating any feature branch") return nil, nil } branchName := "story/" + sluggedLine ok, err := prompt.Confirm( fmt.Sprintf( "\nThe branch that is going to be created will be called '%s'.\nIs that alright?", branchName), true) if err != nil { return nil, errs.NewError(task, err) } if !ok { panic(prompt.ErrCanceled) } fmt.Println() createTask := fmt.Sprintf( "Create branch '%v' on top of branch '%v'", branchName, baseBranch) log.Run(createTask) if err := git.Branch(branchName, baseBranch); err != nil { return nil, errs.NewError(createTask, err) } deleteTask := fmt.Sprintf("Delete branch '%v'", branchName) deleteBranch := func() error { // Roll back and delete the newly created branch. log.Rollback(createTask) if err := git.Branch("-D", branchName); err != nil { return errs.NewError(deleteTask, err) } return nil } // Checkout the newly created branch. checkoutTask := fmt.Sprintf("Checkout branch '%v'", branchName) log.Run(checkoutTask) if err := git.Checkout(branchName); err != nil { if err := deleteBranch(); err != nil { errs.Log(err) } return nil, errs.NewError(checkoutTask, err) } // Push the newly created branch unless -no_push. pushTask := fmt.Sprintf("Push branch '%v' to remote '%v'", branchName, remoteName) if flagPush { log.Run(pushTask) if err := git.Push(remoteName, branchName); err != nil { if err := deleteBranch(); err != nil { errs.Log(err) } return nil, errs.NewError(pushTask, err) } } return action.ActionFunc(func() error { // Checkout the original branch. log.Rollback(checkoutTask) if err := git.Checkout(originalBranch); err != nil { return errs.NewError( fmt.Sprintf("Checkout the original branch '%v'", originalBranch), err) } // Delete the newly created branch. deleteErr := deleteBranch() // In case we haven't pushed anything, we are done. if !flagPush { return deleteErr } // Delete the branch from the remote repository. log.Rollback(pushTask) if _, err := git.Run("push", "--delete", remoteName, branchName); err != nil { // In case deleteBranch failed, tell the user now // since we are not going to return that error. if deleteErr != nil { errs.Log(deleteErr) } return errs.NewError( fmt.Sprintf("Delete branch '%v' from remote '%v'", branchName, remoteName), err) } // Return deleteErr to make sure it propagates up. return deleteErr }), nil }
func 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) }
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 }
func runMain() error { // Load git-related config. gitConfig, err := git.LoadConfig() if err != nil { return err } var ( remoteName = gitConfig.RemoteName trunkBranch = gitConfig.TrunkBranchName releaseBranch = gitConfig.ReleaseBranchName ) // Fetch the remote repository unless explicitly skipped. if !flagNoFetch { task := "Fetch the remote repository" log.Run(task) if err := git.UpdateRemotes(remoteName); err != nil { return errs.NewError(task, err) } } log.Run("Make sure all important branches are up to date") // Check branches. checkBranch := func(branchName string) error { task := fmt.Sprintf("Make sure that branch '%v' exists and is up to date", branchName) if err := git.CheckOrCreateTrackingBranch(branchName, remoteName); err != nil { return errs.NewError(task, err) } return nil } for _, branch := range [...]string{releaseBranch, trunkBranch} { if err := checkBranch(branch); err != nil { return err } } // Remember the current branch. currentBranch, err := gitutil.CurrentBranch() if err != nil { return err } // Checkout the release branch. task := "Checkout the release branch" if err := git.Checkout(releaseBranch); err != nil { return errs.NewError(task, err) } defer func() { // Do not checkout the original branch in case the name is empty. // This is later used to disable the checkout of the original branch. if currentBranch == "" { return } // Otherwise checkout the original branch. task := fmt.Sprintf("Checkout the original branch (%v)", currentBranch) if err := git.Checkout(currentBranch); err != nil { errs.LogError(task, err) } }() // Get the current release version string. // It is enough to just call version.Get since // we are already on the release branch. task = "Get the release branch version string" releaseVersion, err := version.Get() if err != nil { return errs.NewError(task, err) } // Get the stories associated with the current release. task = "Fetch the stories associated with the current release" log.Run(task) tracker, err := modules.GetIssueTracker() if err != nil { return errs.NewError(task, err) } release := tracker.RunningRelease(releaseVersion) stories, err := release.Stories() if err != nil { return errs.NewError(task, err) } if len(stories) == 0 { return errs.NewError(task, errors.New("no relevant stories found")) } // Get the release changes. task = "Collect the release changes" log.Run(task) groups, err := changes.StoryChanges(stories) if err != nil { return errs.NewError(task, err) } // Just return in case there are no relevant commits found. if len(groups) == 0 { return errs.NewError(task, errors.New("no relevant commits found")) } // Sort the change groups. groups = changes.SortStoryChanges(groups, stories) groups, err = releases.StoryChangesToCherryPick(groups) if err != nil { return errs.NewError(task, err) } var ( // Collect the changes not reachable from trunk. // In case there are any, we abort the cherry-picking process. unreachable = make([]*changes.StoryChangeGroup, 0, len(groups)) // When we are at iterating, we also collect all release commits // so that we know what trunk commits to cherry-pick later. releaseCommits = make(map[string]struct{}) trunkRef = fmt.Sprintf("refs/heads/%v", trunkBranch) ) for _, group := range groups { g := &changes.StoryChangeGroup{ StoryIdTag: group.StoryIdTag, } for _, ch := range group.Changes { var ok bool for _, c := range ch.Commits { // Add the commit to the map of release commits. releaseCommits[c.SHA] = struct{}{} // Look for a commit that is on trunk. if c.Source == trunkRef { ok = true } } if !ok { // In case there is none, remember the change. g.Changes = append(g.Changes, ch) } } // In case there are some changes not reachable from trunk, // add the story change to the list of unreachable story changes. if len(g.Changes) != 0 { unreachable = append(unreachable, g) } } // In case there are some changes not reachable from the trunk branch, // abort the process and tell the user to get the changes into trunk first. if len(unreachable) != 0 { var details bytes.Buffer fmt.Fprint(&details, ` The following story changes are not reachable from the trunk branch: `) changes.DumpStoryChanges(&details, unreachable, tracker, false) fmt.Fprint(&details, ` Please cherry-pick these changes onto the trunk branch. Only then we can proceed and cherry-pick the changes. `) return errs.NewErrorWithHint( task, errors.New("commits not reachable from trunk detected"), details.String()) } // Everything seems fine, let's continue with the process // by dumping the change details into the console. fmt.Println() changes.DumpStoryChanges(os.Stdout, groups, tracker, false) // Ask the user to confirm before doing any cherry-picking. task = "Ask the user to confirm cherry-picking" fmt.Println(` The changes listed above will be cherry-picked into the release branch.`) confirmed, err := prompt.Confirm("Are you sure you want to continue?", false) if err != nil { return errs.NewError(task, err) } if !confirmed { prompt.PanicCancel() } fmt.Println() // Collect the trunk commits that were created since the last release. task = "Collect the trunk commits added since the last release" trunkCommits, err := releases.ListNewTrunkCommits() if err != nil { return errs.NewError(task, err) } // We need the list to start with the oldest commit. for i, j := 0, len(trunkCommits)-1; i < j; i, j = i+1, j-1 { trunkCommits[i], trunkCommits[j] = trunkCommits[j], trunkCommits[i] } // Collect the commits to cherry pick. These are the commits // that are on trunk and they are associated with the release. hashesToCherryPick := make([]string, 0, len(trunkCommits)) for _, commit := range trunkCommits { if _, ok := releaseCommits[commit.SHA]; ok { hashesToCherryPick = append(hashesToCherryPick, commit.SHA) } } // Perform the cherry-pick itself. task = "Cherry-pick the missing changes into the release branch" log.Run(task) if err := git.CherryPick(hashesToCherryPick...); err != nil { hint := ` It was not possible to cherry-pick the missing changes into the release branch. The cherry-picking process might be still in progress, though. Please check the repository status and potentially resolve the cherry-picking manually. ` // Do not checkout the original branch. currentBranch = "" return errs.NewErrorWithHint(task, err, hint) } log.Log("All missing changes cherry-picked into the release branch") fmt.Println(` ################################################################### # IMPORTANT: The release branch is not being pushed automatically # ################################################################### `) return nil }
func (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 }
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 }
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) }