// CheckOrCreateTrackingBranch tries to make sure that a local branch // of the given name exists and is in sync with the given remote. // // So, in case the right remote branch exists and the local does not, // the local tracking branch is created. In case the local branch // exists already, it is ensured that it is up to date. // // In case the remote branch does not exist, *ErrRefNotFound is returned. // In case the branch is not up to date, *ErrRefNotInSync is returned. func CheckOrCreateTrackingBranch(branch, remote string) error { // Get the data on the local branch. localExists, err := LocalBranchExists(branch) if err != nil { return err } // Check whether the remote counterpart exists. remoteExists, err := RemoteBranchExists(branch, remote) if err != nil { return err } if !remoteExists { if localExists { log.Warn(fmt.Sprintf( "Local branch '%v' found, but the remote counterpart is missing", branch)) log.NewLine(fmt.Sprintf( "Please delete or push local branch '%v'", branch)) } return &ErrRefNotFound{remote + "/" + branch} } // Check whether the local branch exists. if !localExists { return CreateTrackingBranch(branch, remote) } // In case it exists, make sure that it is up to date. return EnsureBranchSynchronized(branch, remote) }
func splitBranchesNotInSync(storyBranches []*git.GitBranch) ([]*git.GitBranch, error) { branches := make([]*git.GitBranch, 0, len(storyBranches)) for _, branch := range storyBranches { upToDate, err := branch.IsUpToDate() if err != nil { return nil, err } if upToDate { branches = append(branches, branch) continue } // In case the branch is not up to date, we split the local and remote // reference into their own branch records to treat them separately. var ( branchName = branch.BranchName remoteBranchName = branch.RemoteBranchName remote = branch.Remote ) log.Warn(fmt.Sprintf("Branch '%s' is not up to date", branchName)) log.NewLine(fmt.Sprintf("Treating '%v' and '%v/%v' as separate branches", branchName, remote, remoteBranchName)) localBranch := &git.GitBranch{ BranchName: branchName, } remoteBranch := &git.GitBranch{ RemoteBranchName: remoteBranchName, Remote: remote, } branches = append(branches, localBranch, remoteBranch) } return branches, nil }
// StoryChanges returns the list of changes grouped by Story-Id. func StoryChanges(stories []common.Story) ([]*StoryChangeGroup, error) { // Prepare the regexp to use to select commits by commit messages. // This regexp is ORing the chosen Story-Id tag values. var grepFlag bytes.Buffer fmt.Fprintf(&grepFlag, "^Story-Id: (%v", stories[0].Tag()) for _, story := range stories[1:] { fmt.Fprintf(&grepFlag, "|%v", story.Tag()) } fmt.Fprint(&grepFlag, ")$") // Get the relevant commits. commits, err := git.GrepCommitsCaseInsensitive(grepFlag.String(), "--all") if err != nil { return nil, err } okCommits := make([]*git.Commit, 0, len(commits)) for _, commit := range commits { if commit.StoryIdTag == "" { log.Warn(fmt.Sprintf( "Found story commit %v, but failed to parse the Story-Id tag.", commit.SHA)) log.NewLine("Please check that commit manually.") continue } okCommits = append(okCommits, commit) } commits = okCommits // Return the change groups. return StoryChangesFromCommits(commits) }
func (tool *codeReviewTool) FinaliseRelease(v *version.Version) (action.Action, error) { // Get a GitHub client. config, err := LoadConfig() if err != nil { return nil, err } client := ghutil.NewClient(config.Token()) owner, repo, err := git.ParseUpstreamURL() if err != nil { return nil, err } // Get the relevant review milestone. releaseString := v.BaseString() task := fmt.Sprintf("Get GitHub review milestone for release %v", releaseString) log.Run(task) milestone, err := milestoneForVersion(config, owner, repo, v) if err != nil { return nil, errs.NewError(task, err) } if milestone == nil { log.Warn(fmt.Sprintf( "Weird, GitHub review milestone for release %v not found", releaseString)) return nil, nil } // Close the milestone unless there are some issues open. task = fmt.Sprintf( "Make sure the review milestone for release %v can be closed", releaseString) if num := *milestone.OpenIssues; num != 0 { return nil, errs.NewError( task, fmt.Errorf( "review milestone for release %v cannot be closed: %v issue(s) open", releaseString, num)) } milestoneTask := fmt.Sprintf("Close GitHub review milestone for release %v", releaseString) log.Run(milestoneTask) milestone, _, err = client.Issues.EditMilestone(owner, repo, *milestone.Number, &github.Milestone{ State: github.String("closed"), }) if err != nil { return nil, errs.NewError(milestoneTask, err) } // Return a rollback function. return action.ActionFunc(func() error { log.Rollback(milestoneTask) task := fmt.Sprintf("Reopen GitHub review milestone for release %v", releaseString) _, _, err := client.Issues.EditMilestone(owner, repo, *milestone.Number, &github.Milestone{ State: github.String("open"), }) if err != nil { return errs.NewError(task, err) } return nil }), 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 bootstrapLocalConfig(spec ConfigSpec) error { task := "Bootstrap local configuration according to the spec" // Some handy variables. configKey := spec.ConfigKey() container := spec.LocalConfig() moduleSpec, isModuleSpec := spec.(ModuleConfigSpec) // Pre-read local config. // It is needed the pre-write hook as well. local, err := config.ReadLocalConfig() readConfig := func() (configFile, error) { return local, err } // Handle module config specs a bit differently. var preWriteHook func() (bool, error) if isModuleSpec { // Make sure the config container is not nil in case this is a module config. // In case the local config container is nil, the pre-write hook is not executed // and the active module ID is not set, and that would be a problem. // Returning a nil local config container is a valid choice, but we still // need the pre-write hook to be executed to set the active module ID. if container == nil { container = newEmptyModuleConfigContainer(configKey, moduleSpec.ModuleKind()) } // In case this is a module config spec, set the the pre-write hook // to modify the local config file to activate the module being configured. preWriteHook = func() (bool, error) { return SetActiveModule(local, moduleSpec.ModuleKind(), configKey) } } // The post-write hook simply tells the user to commit the local config file. postWriteHook := func() error { fmt.Println() log.Warn("Local configuration file modified, please commit it.") return nil } // Run the common loading function with the right arguments. if err := load(&loadArgs{ configKind: "local", configKey: configKey, configContainer: container, readConfig: readConfig, emptyConfig: emptyLocalConfig, preWriteHook: preWriteHook, postWriteHook: postWriteHook, }); err != nil { return errs.NewError(task, err) } return nil }
// Stage can be used to stage the items associated with this release. // // The rollback function is a NOOP in this case since there is no way // how to delete a deployment once created in Sprintly. func (release *runningRelease) Stage() (action.Action, error) { task := "Ping Sprintly to register the deployment" log.Run(task) // Create the Sprintly deployment. if err := release.deploy(release.config.StagingEnvironment()); err != nil { return nil, errs.NewError(task, err) } // Return the rollback function, which is empty in this case. return action.ActionFunc(func() error { log.Rollback(task) log.Warn("It is not possible to delete a Sprintly deployment, skipping ...") return nil }), nil }
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 }
func (release *runningRelease) Release() error { // TODO: Get rid of this unholy ugliness. if release.issues == nil { panic("bug(release.issues == nil)") } // Release all issues that are accepted. issues := make([]*jira.Issue, 0, len(release.issues)) for _, issue := range release.issues { if issue.Fields.Status.Id == stateIdAccepted { issues = append(issues, issue) } } if len(issues) == 0 { log.Warn("No accepted stories found in JIRA") return nil } return performBulkTransition( newClient(release.tracker.config), issues, transitionIdRelease, "") }
func commitsToReviewContexts(commits []*git.Commit) ([]*common.ReviewContext, error) { tracker, err := modules.GetIssueTracker() if err != nil { return nil, err } // Fetch the stories from the issue tracker. tags := storyTags(tracker, commits) stories, err := tracker.ListStoriesByTag(tags) if err != nil { return nil, err } // Build the story map. storiesByTag := make(map[string]common.Story, 1) for i, story := range stories { tag := tags[i] if story == nil { log.Warn(fmt.Sprintf("Story for tag '%v' was not found in the issue tracker", tag)) continue } storiesByTag[tag] = story } // Build the final list of review contexts. ctxs := make([]*common.ReviewContext, 0, len(commits)) for _, commit := range commits { // Story can be set to nil here in case the story is unassigned. // In that case there will be, obviously, no story object in the map. ctxs = append(ctxs, &common.ReviewContext{ Commit: commit, Story: storiesByTag[commit.StoryIdTag], }) } // Return the commit review contexts. return ctxs, nil }
// copyFileContents copies the contents of the file named src to the file named // by dst. The file will be created if it does not already exist. If the // destination file exists, all it's contents will be replaced by the contents // of the source file. func copyFileContents(src, dst string) (err error) { in, err := os.Open(src) if err != nil { return } defer in.Close() out, err := os.Create(dst) if err != nil { return } defer func() { cerr := out.Close() if err == nil { err = cerr } else { log.Warn(cerr.Error()) } }() if _, err = io.Copy(out, in); err != nil { return } err = out.Sync() return }
// 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 filterBranches(storyBranches []*git.GitBranch, trunkName string) ([]*gitBranch, error) { // Pair the branches with commit ranges specified by trunk..story task := "Collected commits associated with the story branches" branches := make([]*gitBranch, 0, len(storyBranches)) for _, branch := range storyBranches { var revRange string if branch.BranchName != "" { // Handle branches that exist locally. revRange = fmt.Sprintf("%v..%v", trunkName, branch.BranchName) } else { // Handle branches that exist only in the remote repository. // We can use trunkName here since trunk is up to date. revRange = fmt.Sprintf("%v..%v/%v", trunkName, branch.Remote, branch.RemoteBranchName) } commits, err := git.ShowCommitRange(revRange) if err != nil { return nil, errs.NewError(task, err) } branches = append(branches, &gitBranch{ tip: branch, commits: commits, }) continue } // Collect story tags. task = "Collect affected story tags" tracker, err := modules.GetIssueTracker() if err != nil { return nil, errs.NewError(task, err) } tags := make([]string, 0, len(storyBranches)) BranchLoop: for _, branch := range branches { for _, commit := range branch.commits { commitTag := commit.StoryIdTag // Make sure the tag is not in the list already. for _, tag := range tags { if tag == commitTag { continue BranchLoop } } // Drop tags not recognized by the current issue tracker. _, err := tracker.StoryTagToReadableStoryId(commitTag) if err == nil { tags = append(tags, commitTag) } } } // Fetch the collected stories. task = "Fetch associated stories from the issue tracker" log.Run(task) stories, err := tracker.ListStoriesByTag(tags) if err != nil { return nil, errs.NewError(task, err) } // Filter the branches according to the story state. storyByTag := make(map[string]common.Story, len(stories)) for i, story := range stories { // tags[i] corresponds to stories[i] tag := tags[i] if story != nil { storyByTag[tag] = story } else { log.Warn(fmt.Sprintf("Story for tag '%v' was not found in the issue tracker", tag)) } } allowedStates := allowedStoryStates() // checkCommits returns whether the commits passed in are ok // considering the state of the stories found in these commits, // whether the branch containing these commits can be deleted. checkCommits := func(commits []*git.Commit) (common.StoryState, bool) { var storyFound bool for _, commit := range commits { // Skip commits with empty Story-Id tag. if commit.StoryIdTag == "" { continue } // In case the story is not found, the tag is not recognized // by the current issue tracker. In that case we just skip the commit. story, ok := storyByTag[commit.StoryIdTag] if !ok { continue } // When the story state associated with the commit is not ok, // we can return false here to reject the branch. storyState := story.State() if _, ok := allowedStates[storyState]; !ok { return storyState, false } storyFound = true } // We went through all the commits and they are fine, check passed. return common.StoryStateInvalid, storyFound } // Go through the branches and only return these that // comply with the story state requirements. bs := make([]*gitBranch, 0, len(branches)) for _, branch := range branches { tip := branch.tip logger := log.V(log.Verbose) if logger { logger.Log(fmt.Sprintf("Processing branch %v", tip.CanonicalName())) } // The branch can be for sure deleted in case there are no commits // contained in the commit range. That means the branch is merged into trunk. if len(branch.commits) == 0 { if logger { logger.Log(" Include the branch (reason: merged into trunk)") } branch.reason = "merged" bs = append(bs, branch) continue } // In case the commit check passed, we append the branch. state, ok := checkCommits(branch.commits) if ok { if logger { logger.Log(" Include the branch (reason: branch check passed)") } branch.reason = "check passed" bs = append(bs, branch) continue } // Otherwise we print the skip warning. if logger { if state == common.StoryStateInvalid { logger.Log( " Exclude the branch (reason: no story commits found on the branch)") } else { logger.Log(fmt.Sprintf( " Exclude the branch (reason: story state is '%v')", state)) } } } return bs, nil }
// pourSkeleton counts on the fact that skeleton is a valid skeleton // that is available in the local cache directory. func pourSkeleton(skeletonName string, localConfigDir string) (err error) { // Get the skeleton src path. cacheDir, err := cacheDirectoryAbsolutePath() if err != nil { return err } skeletonDir := filepath.Join(cacheDir, "github.com", skeletonName) // Make sure src is a directory, just to be sure. skeletonInfo, err := os.Stat(skeletonDir) if err != nil { return err } if !skeletonInfo.IsDir() { return fmt.Errorf("skeleton source path not a directory: %v", skeletonDir) } // Get the list of script files. srcScriptsDir := filepath.Join(skeletonDir, "scripts") scripts, err := filepath.Glob(srcScriptsDir + "/*") if err != nil { return err } if len(scripts) == 0 { log.Warn("No script files found in the skeleton repository") return nil } // Create the destination directory. dstScriptsDir := filepath.Join(localConfigDir, "scripts") if err := os.MkdirAll(dstScriptsDir, 0755); err != nil { return err } // Delete the directory on error. defer action.RollbackOnError(&err, action.ActionFunc(func() error { log.Rollback("Create the local scripts directory") if err := os.RemoveAll(dstScriptsDir); err != nil { return errs.NewError("Remove the local scripts directory", err) } return nil })) for _, script := range scripts { err := func(script string) error { // Skip directories. scriptInfo, err := os.Stat(script) if err != nil { return err } if scriptInfo.IsDir() { return nil } // Copy the file. filename := script[len(srcScriptsDir)+1:] fmt.Println("---> Copy", filepath.Join("scripts", filename)) srcFd, err := os.Open(script) if err != nil { return err } defer srcFd.Close() dstPath := filepath.Join(dstScriptsDir, filename) dstFd, err := os.OpenFile( dstPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, scriptInfo.Mode()) if err != nil { return err } defer dstFd.Close() _, err = io.Copy(dstFd, srcFd) return err }(script) if err != nil { return err } } return nil }
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 (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 runMain(args []string) (err error) { // Get the commit hashes. // We need to do this before we checkout the target branch, // because relative refs like HEAD change by doing so. hashes, err := git.RevisionsToCommitList(args...) if err != nil { return err } // Load git-related config. gitConfig, err := git.LoadConfig() if err != nil { return err } var ( remoteName = gitConfig.RemoteName releaseBranch = gitConfig.ReleaseBranchName ) // Get the target branch name. targetBranch := flagTarget if targetBranch == "" { targetBranch = releaseBranch } // Fetch the remote repository if requested. if flagFetch { task := "Fetch the remote repository" log.Run(task) if err := git.UpdateRemotes(remoteName); err != nil { return errs.NewError(task, err) } } // Make sure the target branch is up to date. if err := ensureTargetBranchExists(targetBranch, remoteName); err != nil { return err } // Get the current branch name. currentBranch, err := gitutil.CurrentBranch() if err != nil { return err } // Checkout the target branch in case we are not at it already. if currentBranch != targetBranch { task := fmt.Sprintf("Checkout branch '%v'", targetBranch) log.Run(task) if err := git.Checkout(targetBranch); err != nil { return errs.NewError(task, err) } // Checkout the current branch on return, unless there is an error. // In that case we want to stay on the target branch. defer func() { if err == nil { task := fmt.Sprintf("Checkout branch '%v'", currentBranch) log.Run(task) if ex := git.Checkout(currentBranch); ex != nil { err = errs.NewError(task, ex) } } else { log.Warn(fmt.Sprintf("An error detected, staying on branch '%v'", targetBranch)) } }() } // Run git cherry-pick. task := fmt.Sprintf("Cherry-pick the chosen commits into '%v'", targetBranch) log.Run(task) if err := git.CherryPick(hashes...); err != nil { return errs.NewError(task, err) } log.Warn(fmt.Sprintf("Make sure to push branch '%v' to publish the changes", targetBranch)) return nil }
func runMain() (err error) { // Load repo config. gitConfig, err := git.LoadConfig() if err != nil { return err } var ( remoteName = gitConfig.RemoteName stagingBranch = gitConfig.StagingBranchName stableBranch = gitConfig.StableBranchName ) // Fetch the repository. if !flagNoFetch { task := "Fetch the remote repository" log.Run(task) if err := git.UpdateRemotes(remoteName); err != nil { return errs.NewError(task, err) } } // Check branches. checkBranch := func(branchName string) error { // Make sure the branch exists. task := fmt.Sprintf("Make sure that branch '%v' exists and is up to date", branchName) log.Run(task) if err := git.CheckOrCreateTrackingBranch(branchName, remoteName); err != nil { return errs.NewError(task, err) } // Make sure we are not on the branch. task = fmt.Sprintf("Make sure that branch '%v' is not checked out", branchName) log.Run(task) currentBranch, err := gitutil.CurrentBranch() if err != nil { return errs.NewError(task, err) } if currentBranch == branchName { err := fmt.Errorf("cannot deploy while on branch '%v'", branchName) return errs.NewError(task, err) } return nil } for _, branch := range []string{stableBranch, stagingBranch} { if err := checkBranch(branch); err != nil { return err } } // Make sure the current staging branch can be released. task := fmt.Sprintf("Make sure that branch '%v' can be released", stagingBranch) log.Run(task) tracker, err := modules.GetIssueTracker() if err != nil { return errs.NewError(task, err) } codeReviewTool, err := modules.GetCodeReviewTool() if err != nil { return errs.NewError(task, err) } stagingVersion, err := version.GetByBranch(stagingBranch) if err != nil { return errs.NewError(task, err) } // Make sure the release can be closed in the issue tracker. issueTrackerRelease := tracker.RunningRelease(stagingVersion) if err := issueTrackerRelease.EnsureClosable(); err != nil { return err } // Make sure the release can be closed in the code review tool. codeReviewRelease := codeReviewTool.NewRelease(stagingVersion) if err := codeReviewRelease.EnsureClosable(); err != nil { return err } // Reset the stable branch to point to stage. task = fmt.Sprintf("Reset branch '%v' to point to branch '%v'", stableBranch, stagingBranch) log.Run(task) act, err := git.CreateOrResetBranch(stableBranch, stagingBranch) if err != nil { return errs.NewError(task, err) } defer action.RollbackTaskOnError(&err, task, act) // Bump version for the stable branch. stableVersion, err := stagingVersion.ToStableVersion() if err != nil { return err } task = fmt.Sprintf("Bump version (branch '%v' -> %v)", stableBranch, stableVersion) log.Run(task) act, err = version.SetForBranch(stableVersion, stableBranch) if err != nil { return errs.NewError(task, err) } defer action.RollbackTaskOnError(&err, task, act) // Tag the stable branch. tag := stableVersion.ReleaseTagString() task = fmt.Sprintf("Tag branch '%v' with tag '%v'", stableBranch, tag) log.Run(task) if err := git.Tag(tag, stableBranch); err != nil { return errs.NewError(task, err) } defer action.RollbackTaskOnError(&err, task, action.ActionFunc(func() error { task := fmt.Sprintf("Delete tag '%v'", tag) if err := git.Tag("-d", tag); err != nil { return errs.NewError(task, err) } return nil })) // Generate the release notes. // We try to do as much as possible before pushing. task = fmt.Sprintf("Generate release notes for version '%v'", stableVersion) log.Run(task) rnm, err := modules.GetReleaseNotesManager() if err != nil { if _, ok := err.(*modules.ErrModuleNotSet); !ok { return errs.NewError(task, err) } } var nts *common.ReleaseNotes // rnm will be nil in case the module is disabled. if rnm != nil { // Get the relevant stories. stories, err := tracker.ListStoriesByRelease(stableVersion) if err != nil { return errs.NewError(task, err) } // Generate the release notes. nts = notes.GenerateReleaseNotes(stableVersion, stories) } else { log.Log("Release notes module disabled, not doing anything") } // Close the release in the issue tracker. act, err = issueTrackerRelease.Close() if err != nil { return err } defer action.RollbackOnError(&err, act) // Close the release in the code review tool. act, err = codeReviewRelease.Close() if err != nil { return err } defer action.RollbackOnError(&err, act) // Push the changes to the remote repository. task = "Push changes to the remote repository" log.Run(task) toPush := []string{ "--tags", fmt.Sprintf("%v:%v", stableBranch, stableBranch), } if err := git.PushForce(remoteName, toPush...); err != nil { return errs.NewError(task, err) } // Post the release notes. task = fmt.Sprintf("Post the release notes for version '%v'", stableVersion) if rnm != nil { log.Run(task) if _, err := rnm.PostReleaseNotes(nts); err != nil { errs.LogError(task, err) log.Warn("Failed to post the release notes, continuing anyway ...") } } // Tell the user we succeeded. color.Green("\n-----> Release %v deployed successfully!\n\n", stableVersion) color.Cyan("Let's check whether the next release branch can be staged already.\n") color.Cyan("In other words, we will try to run `release stage` and see what happens.\n\n") // Now we proceed to the staging step. We do not roll back // the previous changes on error since this is a separate step. tryToStageRunningRelease() return nil }
func Branches() ([]*GitBranch, error) { // Get local branches. local, err := localBranches() if err != nil { return nil, err } // Get remote branches. remote, err := remoteBranches() if err != nil { return nil, err } // Clean up the local branches. // In can happen that the tracked branch fields are set while the branch // itself doesn't exist any more since the git calls are only consulting // .git/config. They don't really care whether the branch actually exists. LocalLoop: for _, localBranch := range local { // In case the remote record is empty, we are obviously cool. if localBranch.RemoteBranchName == "" { continue } // Otherwise go through the remote branches and only continue // when the corresponding remote branch is found. for _, remoteBranch := range remote { if remoteBranch.RemoteBranchName == localBranch.RemoteBranchName { continue LocalLoop } } // In case the remote branch is missing, clean up the record in .git/config. branchName := localBranch.BranchName log.Warn(fmt.Sprintf( "Branch '%v' not found", localBranch.FullRemoteBranchName())) log.NewLine(fmt.Sprintf("Unsetting upstream for local branch '%v'", branchName)) task := fmt.Sprintf("Unset upstream branch for branch '%v'", branchName) if err := Branch("--unset-upstream", branchName); err != nil { return nil, errs.NewError(task, err) } // Unset the remote branch fields. localBranch.RemoteBranchName = "" localBranch.Remote = "" } // Append the remote branch records to the local ones. // Only include these that are not already included in the local records. branches := local RemoteLoop: for _, remoteBranch := range remote { for _, localBranch := range local { if localBranch.RemoteBranchName == remoteBranch.RemoteBranchName { continue RemoteLoop } } branches = append(branches, remoteBranch) } // Return branches. return branches, nil }
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 runMain() (err error) { // Load repo config. gitConfig, err := git.LoadConfig() if err != nil { return err } var ( remoteName = gitConfig.RemoteName() stagingBranch = gitConfig.StagingBranchName() stableBranch = gitConfig.StableBranchName() ) // Fetch the repository. if !flagNoFetch { if err := git.UpdateRemotes(remoteName); err != nil { return err } } // Check branches. checkBranch := func(branchName string) error { // Make sure the branch exists. task := fmt.Sprintf("Make sure that branch '%v' exists and is up to date", branchName) if err := git.CheckOrCreateTrackingBranch(branchName, remoteName); err != nil { return errs.NewError(task, err) } // Make sure we are not on the branch. task = fmt.Sprintf("Make sure that branch '%v' is not checked out", branchName) currentBranch, err := git.CurrentBranch() if err != nil { return errs.NewError(task, err) } if currentBranch == branchName { err := fmt.Errorf("cannot deploy while on branch '%v'", branchName) return errs.NewError(task, err) } return nil } for _, branch := range []string{stableBranch, stagingBranch} { if err := checkBranch(branch); err != nil { return err } } // Make sure the current staging branch can be released. task := fmt.Sprintf("Make sure that branch '%v' can be released", stagingBranch) log.Run(task) tracker, err := modules.GetIssueTracker() if err != nil { return errs.NewError(task, err) } stagingVersion, err := version.GetByBranch(stagingBranch) if err != nil { return errs.NewError(task, err) } release, err := tracker.RunningRelease(stagingVersion) if err != nil { return err } if err := release.EnsureReleasable(); err != nil { return err } // Reset the stable branch to point to stage. task = fmt.Sprintf("Reset branch '%v' to point to branch '%v'", stableBranch, stagingBranch) log.Run(task) act, err := git.CreateOrResetBranch(stableBranch, stagingBranch) if err != nil { return errs.NewError(task, err) } defer action.RollbackTaskOnError(&err, task, act) // Bump version for the stable branch. stableVersion, err := stagingVersion.ToStableVersion() if err != nil { return err } task = fmt.Sprintf("Bump version (branch '%v' -> %v)", stableBranch, stableVersion) log.Run(task) act, err = version.SetForBranch(stableVersion, stableBranch) if err != nil { return errs.NewError(task, err) } defer action.RollbackTaskOnError(&err, task, act) // Tag the stable branch. tag := stableVersion.ReleaseTagString() task = fmt.Sprintf("Tag branch '%v' with tag '%v'", stableBranch, tag) log.Run(task) if err := git.Tag(tag, stableBranch); err != nil { return errs.NewError(task, err) } defer action.RollbackTaskOnError(&err, task, action.ActionFunc(func() error { task := fmt.Sprintf("Delete tag '%v'", tag) if err := git.Tag("-d", tag); err != nil { return errs.NewError(task, err) } return nil })) // Generate the release notes. // We try to do as much as possible before pushing. task = fmt.Sprintf("Generate release notes for version '%v'", stableVersion) log.Run(task) rnm, err := modules.GetReleaseNotesManager() if err != nil { return errs.NewError(task, err) } var nts *common.ReleaseNotes // rnm will be nil in case the module is disabled. if rnm != nil { // Get the relevant stories. stories, err := tracker.ListStoriesByRelease(stableVersion) if err != nil { return errs.NewError(task, err) } // Generate the release notes. nts = notes.GenerateReleaseNotes(stableVersion, stories) } else { log.Log("Release notes module disabled, skipping ...") } // Push the changes to the remote repository. task = "Push changes to the remote repository" log.Run(task) toPush := []string{ "--tags", fmt.Sprintf("%v:%v", stableBranch, stableBranch), } if err := git.PushForce(remoteName, toPush...); err != nil { return errs.NewError(task, err) } // Post the release notes. task = fmt.Sprintf("Post the release notes for version '%v'", stableVersion) if rnm != nil { log.Run(task) if _, err := rnm.PostReleaseNotes(nts); err != nil { errs.LogError(task, err) log.Warn("Failed to post the release notes, continuing anyway ...") } } // Now we proceed to the staging step. We do not roll back // the previous changes on error since this is a separate step. tryToStageRunningRelease() return nil }