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 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 }
// 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 }