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 createMilestone( config Config, owner string, repo string, v *version.Version, ) (*github.Milestone, action.Action, error) { // Create the review milestone. var ( title = milestoneTitle(v) milestoneTask = fmt.Sprintf("Create GitHub review milestone '%v'", title) client = ghutil.NewClient(config.Token()) ) log.Run(milestoneTask) milestone, _, err := client.Issues.CreateMilestone(owner, repo, &github.Milestone{ Title: github.String(title), }) if err != nil { return nil, nil, errs.NewError(milestoneTask, err) } // Return a rollback function. return milestone, action.ActionFunc(func() error { log.Rollback(milestoneTask) task := fmt.Sprintf("Delete GitHub review milestone '%v'", title) _, err := client.Issues.DeleteMilestone(owner, repo, *milestone.Number) if err != nil { return errs.NewError(task, err) } return nil }), nil }
func (item *item) Start() *errs.Error { task := fmt.Sprintf("Mark Sprintly item %v as being in progress", item.Number) // Check whether we are not finished already. switch item.Status { case sprintly.ItemStatusSomeday: case sprintly.ItemStatusBacklog: case sprintly.ItemStatusInProgress: fallthrough case sprintly.ItemStatusCompleted: fallthrough case sprintly.ItemStatusAccepted: // Nothing to do. return nil } // Load the Sprintly config. config, err := LoadConfig() if err != nil { return errs.NewError(task, err) } // Instantiate the Sprintly client. client := sprintly.NewClient(config.Username(), config.Token()) // Move the item to in-progress. _, _, err = client.Items.Update(config.ProductId(), item.Number, &sprintly.ItemUpdateArgs{ Status: sprintly.ItemStatusInProgress, }) if err != nil { return errs.NewError(task, err) } return nil }
func (story *story) SetAssignees(users []common.User) error { task := fmt.Sprintf("Set owners for story %v", story.Story.Id) ownerIds := make([]int, len(users)) for i, user := range users { id, err := strconv.Atoi(user.Id()) if err != nil { return errs.NewError(task, err) } ownerIds[i] = id } var ( config = story.tracker.config client = pivotal.NewClient(config.UserToken) projectId = config.ProjectId ) updateRequest := &pivotal.StoryRequest{OwnerIds: &ownerIds} updatedStory, _, err := client.Stories.Update(projectId, story.Story.Id, updateRequest) if err != nil { return errs.NewError(task, err) } story.Story = updatedStory return nil }
// ListTags returns the list of all release tags, sorted by the versions they represent. func ListTags() (tags []string, err error) { var task = "Get release tags" // Get all release tags. stdout, err := git.RunCommand("tag", "--list", "v*.*.*") if err != nil { return nil, errs.NewError(task, err) } // Parse the output to get sortable versions. var vers []*version.Version scanner := bufio.NewScanner(stdout) for scanner.Scan() { line := scanner.Text() if line == "" { continue } line = line[1:] // strip "v" ver, _ := version.Parse(line) vers = append(vers, ver) } if err := scanner.Err(); err != nil { return nil, errs.NewError(task, err) } // Sort the versions. sort.Sort(version.Versions(vers)) // Convert versions back to tag names and return. tgs := make([]string, 0, len(vers)) for _, ver := range vers { tgs = append(tgs, "v"+ver.String()) } return tgs, nil }
// postUnassignedReviewRequest can be used to post the given commit for review. // This function is to be used to post commits that are not associated with any story. func postUnassignedReviewRequest( config *moduleConfig, owner string, repo string, commit *git.Commit, opts map[string]interface{}, ) (*github.Issue, []*git.Commit, error) { // Search for an existing issue. task := fmt.Sprintf("Search for an existing review issue for commit %v", commit.SHA) log.Run(task) client := ghutil.NewClient(config.Token) issue, err := ghissues.FindReviewIssueForCommit(client, owner, repo, commit.SHA) if err != nil { return nil, nil, errs.NewError(task, err) } // Return an error in case the issue for the given commit already exists. if issue != nil { issueNum := *issue.Number err = fmt.Errorf("existing review issue found for commit %v: %v", commit.SHA, issueNum) return nil, nil, errs.NewError("Make sure the review issue can be created", err) } // Create a new unassigned review request. issue, err = createUnassignedReviewRequest(config, owner, repo, commit, opts) if err != nil { return nil, nil, err } return issue, []*git.Commit{commit}, nil }
func CreateMilestone( client *github.Client, owner string, repo string, title string, ) (*github.Milestone, action.Action, error) { // Create the milestone. milestoneTask := fmt.Sprintf("Create GitHub milestone '%v'", title) log.Run(milestoneTask) milestone, _, err := client.Issues.CreateMilestone(owner, repo, &github.Milestone{ Title: github.String(title), }) if err != nil { return nil, nil, errs.NewError(milestoneTask, err) } // Return a rollback function. return milestone, action.ActionFunc(func() error { log.Rollback(milestoneTask) task := fmt.Sprintf("Delete GitHub milestone '%v'", title) _, err := client.Issues.DeleteMilestone(owner, repo, *milestone.Number) if err != nil { return errs.NewError(task, err) } return nil }), nil }
func getAndPourSkeleton(skeleton string) error { // Get or update given skeleton. task := fmt.Sprintf("Get or update skeleton '%v'", skeleton) log.Run(task) if err := getOrUpdateSkeleton(flagSkeleton); err != nil { return errs.NewError(task, err) } // Move the skeleton files into place. task = "Copy the skeleton into the configuration directory" log.Go(task) localConfigDir, err := config.LocalConfigDirectoryAbsolutePath() if err != nil { return errs.NewError(task, err) } log.NewLine("") if err := pourSkeleton(flagSkeleton, localConfigDir); err != nil { return errs.NewError(task, err) } log.NewLine("") log.Ok(task) return nil }
func (release *nextRelease) Start() (action.Action, error) { var ( client = release.client productId = release.tracker.config.ProductId() itemReleaseTag = getItemReleaseTag(release.trunkVersion) ) // Add the release tag to the relevant Sprintly items. task := "Tag relevant items with the release tag" log.Run(task) items, err := addTag(client, productId, release.additionalItems, itemReleaseTag) if err != nil { return nil, errs.NewError(task, err) } release.additionalItems = nil // Return the rollback action, which removes the release tags that were added. return action.ActionFunc(func() error { log.Rollback(task) _, err := removeTag(client, productId, items, itemReleaseTag) if err != nil { return errs.NewError("Remove the release tag from relevant items", err, nil) } return nil }), nil }
func (tracker *issueTracker) CurrentUser() (common.User, error) { task := "Fetch the current user from Sprintly" var ( productId = tracker.config.ProductId() username = tracker.config.Username() ) // Fetch all members of this product. users, _, err := tracker.client.People.List(productId) if err != nil { return nil, errs.NewError(task, err) } // Find the user with matching username. for _, usr := range users { if usr.Email == username { return &user{&usr}, nil } } // In case there is no such user, they were not invited yet. return nil, errs.NewError( task, fmt.Errorf("user '%v' not a member of this product", username), nil) }
// doInstall performs the common step that both install and upgrade need to do. // // Given a GitHub release, it downloads and unpacks the fitting artifacts // and replaces the current executables with the ones just downloaded. func doInstall( client *github.Client, owner string, repo string, assets []github.ReleaseAsset, version string, dstDir string, ) (err error) { // Choose the asset to be downloaded. task := "Pick the most suitable release asset" var ( assetName = getAssetName(version) assetURL string ) for _, asset := range assets { if *asset.Name == assetName { assetURL = *asset.BrowserDownloadURL } } if assetURL == "" { return errs.NewError(task, errors.New("no suitable release asset found")) } // Make sure the destination folder exists. task = "Make sure the destination directory exists" dstDir, act, err := ensureDstDirExists(dstDir) if err != nil { return errs.NewError(task, err) } defer action.RollbackOnError(&err, act) // Download the selected release asset. return downloadAndInstallAsset(assetName, assetURL, dstDir) }
// Start is a part of common.NextRelease interface. func (release *nextRelease) Start() (act action.Action, err error) { // In case there are no additional issues, we are done. if len(release.additionalIssues) == 0 { return action.Noop, nil } // Set milestone for the additional issues. task := fmt.Sprintf( "Set milestone to '%v' for the issues added automatically", release.trunkVersion.BaseString()) log.Run(task) // Get the milestone corresponding to the release branch version string. chain := action.NewActionChain() defer chain.RollbackOnError(&err) milestone, act, err := release.tracker.getOrCreateMilestone(release.trunkVersion) if err != nil { return nil, errs.NewError(task, err) } chain.Push(act) // Update the issues. issues, act, err := release.tracker.updateIssues( release.additionalIssues, setMilestone(milestone), unsetMilestone()) if err != nil { return nil, errs.NewError(task, err) } chain.Push(act) release.additionalIssues = issues return chain, nil }
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 }
// WriteLocalConfig writes the given configuration struct // into the local configuration file. // // In case the target path does not exist, it is created, // including the parent directories. // // In case the file exists, it is truncated. func writeConfig(absolutePath string, content interface{}, perm os.FileMode) error { task := "Write a configuration file" // Check the configuration directory and make sure it exists. configDir := filepath.Dir(absolutePath) info, err := os.Stat(configDir) if err != nil { if !os.IsNotExist(err) { return errs.NewError(task, err) } // The directory doesn't exist. if err := os.MkdirAll(configDir, 0750); err != nil { return errs.NewError(task, err) } } if !info.IsDir() { return errs.NewError(task, errors.New("not a directory: "+configDir)) } // Marshal the content. raw, err := Marshal(content) if err != nil { return errs.NewError(task, err) } // Write the raw content into the file. if err := ioutil.WriteFile(absolutePath, raw, perm); err != nil { return errs.NewError(task, err) } return nil }
// createUnassignedReviewRequest created a new review issue // for the given commit that is not associated with any story. func createUnassignedReviewRequest( config *moduleConfig, owner string, repo string, commit *git.Commit, opts map[string]interface{}, ) (*github.Issue, error) { task := fmt.Sprintf("Create review issue for commit %v", commit.SHA) // Prepare the issue object. reviewIssue := ghissues.NewCommitReviewIssue(commit.SHA, commit.MessageTitle) // Get the right review milestone to add the issue into. milestone, err := getOrCreateMilestoneForCommit(config, owner, repo, commit.SHA) if err != nil { return nil, errs.NewError(task, err) } // Create a new review issue. issue, err := createIssue( task, config, owner, repo, reviewIssue.FormatTitle(), reviewIssue.FormatBody(), optValueString(opts["reviewer"]), milestone, true) if err != nil { return nil, errs.NewError(task, err) } return issue, nil }
func (item *item) AddAssignee(user common.User) *errs.Error { task := "Assign the current user to the selected story" // Load the Sprintly config. config, err := LoadConfig() if err != nil { return errs.NewError(task, err) } // Parse the user ID. userId, err := strconv.Atoi(user.Id()) if err != nil { panic(err) } // Instantiate the Sprintly client. client := sprintly.NewClient(config.Username(), config.Token()) // Assign the user to the selected story. _, _, err = client.Items.Update(config.ProductId(), item.Number, &sprintly.ItemUpdateArgs{ AssignedTo: userId, }) if err != nil { return errs.NewError(task, err) } return nil }
func EnsureDirectoryExists(path string) (action.Action, error) { // Check whether the directory exists already. task := fmt.Sprintf("Check whether '%v' exists and is a directory", path) info, err := os.Stat(path) if err != nil { if !os.IsNotExist(err) { return nil, errs.NewError(task, err) } } else { // In case the path exists, make sure it is a directory. if !info.IsDir() { return nil, errs.NewError(task, errors.New("not a directory: "+path)) } // We are done. return action.Noop, nil } // Now we know that path does not exist, so we need to create it. createTask := fmt.Sprintf("Create directory '%v'", path) log.Run(createTask) if err := os.MkdirAll(path, 0755); err != nil { return nil, errs.NewError(createTask, err) } return action.ActionFunc(func() error { log.Rollback(createTask) task := fmt.Sprintf("Remove directory '%v'", path) if err := os.RemoveAll(path); err != nil { return errs.NewError(task, err) } return nil }), nil }
func (release *nextRelease) Start() (action.Action, error) { // In case there are no additional stories, we are done. if len(release.additionalStories) == 0 { return action.Noop, nil } // Add release labels to the relevant stories. var ( config = release.tracker.config client = pivotal.NewClient(config.UserToken) projectId = config.ProjectId ) task := "Label the stories with the release label" log.Run(task) releaseLabel := getReleaseLabel(release.trunkVersion) stories, err := addLabel(client, projectId, release.additionalStories, releaseLabel) if err != nil { return nil, errs.NewError(task, err) } release.additionalStories = nil // Return the rollback action, which removes the release labels that were appended. return action.ActionFunc(func() error { log.Rollback(task) _, err := removeLabel(client, projectId, stories, releaseLabel) if err != nil { return errs.NewError("Remove the release label from the stories", err) } return nil }), nil }
func rebase(currentBranch, parentBranch string) ([]*git.Commit, error) { // Tell the user what is happening. task := fmt.Sprintf("Rebase branch '%v' onto '%v'", currentBranch, parentBranch) log.Run(task) // Do the rebase. if err := git.Rebase(parentBranch); err != nil { ex := errs.Log(errs.NewError(task, err)) asciiart.PrintGrimReaper("GIT REBASE FAILED") fmt.Printf(`Git failed to rebase your branch onto '%v'. The repository might have been left in the middle of the rebase process. In case you do not know how to handle this, just execute $ git rebase --abort to make your repository clean again. In any case, you have to rebase your current branch onto '%v' if you want to continue and post a review request. In the edge cases you can as well use -no_rebase to skip this step, but try not to do it. `, parentBranch, parentBranch) return nil, ex } // Reload the commits. task = "Get the commits to be posted for code review, again" commits, err := git.ShowCommitRange(parentBranch + "..") if err != nil { return nil, errs.NewError(task, err) } // Return new commits. return commits, nil }
func (release *runningRelease) Stage() (action.Action, error) { var ( api = newClient(release.tracker.config) versionString = release.releaseVersion.BaseString() stageTask = fmt.Sprintf("Stage JIRA issues associated with release %v", versionString) ) log.Run(stageTask) // Make sure we only try to stage the issues that are in Tested. var issuesToStage []*jira.Issue for _, issue := range release.issues { if issue.Fields.Status.Id == stateIdTested { issuesToStage = append(issuesToStage, issue) } } // Perform the transition. err := performBulkTransition(api, issuesToStage, transitionIdStage, transitionIdUnstage) if err != nil { return nil, errs.NewError(stageTask, err) } return action.ActionFunc(func() error { log.Rollback(stageTask) unstageTask := fmt.Sprintf("Unstage JIRA issues associated with release %v", versionString) if err := performBulkTransition(api, issuesToStage, transitionIdUnstage, ""); err != nil { return errs.NewError(unstageTask, 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 ensureCommitsPushed(commits []*git.Commit) error { task := "Make sure that all commits exist in the upstream repository" // Load git-related config. gitConfig, err := git.LoadConfig() if err != nil { return errs.NewError(task, err) } remoteName := gitConfig.RemoteName remotePrefix := remoteName + "/" // Check each commit one by one. // // We run `git branch -r --contains HASH` for each commit, // then we check the output. In case there is a branch prefixed // with the right upstream name, the commit is treated as pushed. var ( hint = bytes.NewBufferString("\n") missing bool ) CommitLoop: for _, commit := range commits { // Get `git branch -r --contains HASH` output. stdout, err := git.Run("branch", "-r", "--contains", commit.SHA) if err != nil { return errs.NewError(task, err) } // Parse `git branch` output line by line. scanner := bufio.NewScanner(stdout) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(strings.TrimSpace(line), remotePrefix) { // The commit is contained in a remote branch, continue. continue CommitLoop } } if err := scanner.Err(); err != nil { return errs.NewError(task, err) } // The commit is not contained in any remote branch, bummer. fmt.Fprintf(hint, "Commit %v has not been pushed into remote '%v' yet.\n", commit.SHA, remoteName) missing = true } fmt.Fprintf(hint, "\n") fmt.Fprintf(hint, "All selected commits need to be pushed into the upstream pository.\n") fmt.Fprintf(hint, "Please make sure that is the case before trying again.\n") fmt.Fprintf(hint, "\n") // Return an error in case there is any commit that is not pushed. if missing { return errs.NewErrorWithHint( task, fmt.Errorf("some commits not found in upstream '%v'", remoteName), hint.String()) } return nil }
func fetchOrUpdateSkeleton(skeleton string) error { // Parse the skeleton string. parts := strings.SplitN(skeleton, "/", 2) if len(parts) != 2 { return fmt.Errorf("not a valid repository path string: %v", skeleton) } owner, repo := parts[0], parts[1] // Create the cache directory if necessary. task := "Make sure the local cache directory exists" cacheDir, err := cacheDirectoryAbsolutePath() if err != nil { return errs.NewError(task, err) } if err := os.MkdirAll(cacheDir, 0755); err != nil { return errs.NewError(task, err) } // Pull or close the given skeleton. task = "Pull or clone the given skeleton" skeletonDir := filepath.Join(cacheDir, "github.com", owner) if err := os.MkdirAll(skeletonDir, 0755); err != nil { return errs.NewError(task, err) } skeletonPath := filepath.Join(skeletonDir, repo) if _, err := os.Stat(skeletonPath); err != nil { if !os.IsNotExist(err) { return errs.NewError(task, err) } // The directory does not exist, hence we clone. task := fmt.Sprintf("Clone skeleton '%v'", skeleton) log.Run(task) args := []string{ "clone", "--single-branch", fmt.Sprintf("https://github.com/%v/%v", owner, repo), skeletonPath, } if _, err := git.Run(args...); err != nil { return errs.NewError(task, err) } return nil } // The skeleton directory exists, hence we pull. task = fmt.Sprintf("Pull skeleton '%v'", skeleton) log.Run(task) cmd, _, stderr := shell.Command("git", "pull") cmd.Dir = skeletonPath if err := cmd.Run(); err != nil { return errs.NewErrorWithHint(task, err, stderr.String()) } return nil }
func listItemsByTag( client *sprintly.Client, productId int, tags []string, ) ([]sprintly.Item, error) { task := "Fetch Sprintly items by tag" log.Run(task) // Since Sprintly API is not exactly powerful, we need to get all the items. // Then we need to locally pair stories with their sub-items. itms, err := listItems(client, productId, &sprintly.ItemListArgs{ Status: []sprintly.ItemStatus{ sprintly.ItemStatusSomeday, sprintly.ItemStatusBacklog, sprintly.ItemStatusInProgress, sprintly.ItemStatusCompleted, }, }) var ( items []sprintly.Item itemIndex = make(map[int]struct{}, 0) ) for _, item := range itms { // In case the tag matches, add the item to the list. // Also add the item to the index of potential parent items. for _, tag := range tags { if tagged(&item, tag) { items = append(items, item) itemIndex[item.Number] = struct{}{} } } } for _, item := range itms { // In case the parent is not empty and it matches an item // that is already in the list, add the current item to the list as well, // but only if the item is not there yet. number, err := item.ParentNumber() if err != nil { return nil, errs.NewError(task, err) } if number != 0 { if _, ok := itemIndex[number]; ok { if _, ok := itemIndex[item.Number]; !ok { items = append(items, item) } } } } if err != nil { return nil, errs.NewError(task, err) } return items, nil }
func (local *LocalConfig) validate() error { task := "Validate the local modules config" switch { case local.IssueTrackerId == "": return errs.NewError(task, &config.ErrKeyNotSet{"issue_tracker"}) case local.CodeReviewToolId == "": return errs.NewError(task, &config.ErrKeyNotSet{"code_review_tool"}) } return nil }
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 }
// copyHook installs the SalsaFlow git hook by copying the hook executable // from the expected absolute path to the git config hook directory. func copyHook(hookType HookType, hookExecutable, hookDestPath string) error { task := fmt.Sprintf("Install the SalsaFlow git %v hook", hookType) if err := fileutil.CopyFile(hookExecutable, hookDestPath); err != nil { return errs.NewError(task, err) } if err := os.Chmod(hookDestPath, 0750); err != nil { return errs.NewError(task, err) } log.Log(fmt.Sprintf("SalsaFlow git %v hook installed", hookType)) return nil }
func Confirm(question string, defaultChoice bool) (bool, error) { // Opening the console for O_RDWR doesn't work on Windows, // hence we one the device twice with a different flag set. // This works everywhere. task := "Open console for reading" stdin, err := OpenConsole(os.O_RDONLY) if err != nil { return false, errs.NewError(task, err) } defer stdin.Close() task = "Open console for writing" stdout, err := OpenConsole(os.O_WRONLY) if err != nil { return false, errs.NewError(task, err) } defer stdout.Close() printQuestion := func() { fmt.Fprint(stdout, question) if defaultChoice { fmt.Fprint(stdout, " [Y/n]: ") } else { fmt.Fprint(stdout, " [y/N]: ") } } printQuestion() var choice bool scanner := bufio.NewScanner(stdin) for scanner.Scan() { switch strings.ToLower(scanner.Text()) { case "": choice = defaultChoice case "y": choice = true case "n": choice = false default: printQuestion() continue } break } if err := scanner.Err(); err != nil { return false, err } return choice, nil }
func (release *runningRelease) Stage() (action.Action, error) { stageTask := fmt.Sprintf( "Mark the stories as %v in Pivotal Tracker", pivotal.StoryStateDelivered) log.Run(stageTask) // Load the assigned stories. stories, err := release.loadStories() if err != nil { return nil, errs.NewError(stageTask, err) } // Pick only the stories that are in the right state. ss := make([]*pivotal.Story, 0, len(stories)) for _, s := range stories { if release.tracker.canStoryBeStaged(s) { ss = append(ss, s) } } stories = ss // Mark the selected stories as delivered. Leave the labels as they are. updateRequest := &pivotal.StoryRequest{State: pivotal.StoryStateDelivered} updateFunc := func(story *pivotal.Story) *pivotal.StoryRequest { return updateRequest } // On rollback, set the story state to finished again. rollbackFunc := func(story *pivotal.Story) *pivotal.StoryRequest { return &pivotal.StoryRequest{State: pivotal.StoryStateFinished} } // Update the stories. updatedStories, err := release.tracker.updateStories(stories, updateFunc, rollbackFunc) if err != nil { return nil, errs.NewError(stageTask, err) } release.stories = updatedStories // Return the rollback function. return action.ActionFunc(func() error { // On error, set the states back to the original ones. log.Rollback(stageTask) task := fmt.Sprintf("Reset the story states back to %v", pivotal.StoryStateFinished) updatedStories, err := release.tracker.updateStories(release.stories, rollbackFunc, nil) if err != nil { return errs.NewError(task, err) } release.stories = updatedStories return nil }), nil }
func (tracker *issueTracker) StartableStories() (stories []common.Story, err error) { task := "Fetch the startable items from Sprintly" var ( productId = tracker.config.ProductId() username = tracker.config.Username() ) // Fetch the items from Sprintly. items, err := listItems(tracker.client, productId, &sprintly.ItemListArgs{ Status: []sprintly.ItemStatus{sprintly.ItemStatusBacklog}, }) if err != nil { return nil, errs.NewError(task, err) } // Drop the items that were already assigned. // However, keep the items that are assigned to the current user. for i, item := range items { if item.AssignedTo == nil { continue } if item.AssignedTo.Email == username { continue } items = append(items[:i], items[:i+1]...) } // Wrap the result as []common.Story return toCommonStories(items), nil }