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 }
// 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 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 }
// 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 (tool *codeReviewTool) PostReviewRequests( ctxs []*common.ReviewContext, opts map[string]interface{}, ) (err error) { // Use PostReviewRequestForCommit for every commit on the branch. // Try to post a review request for every commit and keep printing the errors. // Return a common error in case there is any partial error encountered. for _, ctx := range ctxs { if ex := postReviewRequestForCommit(ctx, opts); ex != nil { log.NewLine("") errs.Log(ex) err = errors.New("failed to post a review request") } } return }
func postCommitsForReview(commits []*git.Commit) error { // Pick the commits to be posted for review. if flagPick { task := "Select the commits to be posted for review" var err error commits, err = selectCommitsForReview(commits) if err != nil { return errs.NewError(task, err) } if len(commits) == 0 { log.NewLine("") log.Log("No commits selected, aborting...") prompt.PanicCancel() } } // Print Snoopy. asciiart.PrintSnoopy() // Turn Commits into ReviewContexts. task := "Fetch stories for the commits to be posted for review" log.Run(task) ctxs, err := commitsToReviewContexts(commits) if err != nil { return errs.NewError(task, err) } // Mark the stories as implemented, potentially. task = "Mark the stories as implemented, optionally" implemented, act, err := implementedDialog(ctxs) if err != nil { return errs.NewError(task, err) } defer action.RollbackTaskOnError(&err, task, act) // Post the review requests. task = "Post the review requests" if err := sendReviewRequests(ctxs, implemented); err != nil { return errs.NewError(task, err) } return nil }
func runMain(cmd *gocli.Command) (err error) { // Validate CL flags. task := "Check the command line flags" switch { case flagSkeleton == "" && !flagNoSkeleton: cmd.Usage() return errs.NewError( task, errors.New("-no_skeleton must be specified when no skeleton is given")) case flagSkeletonOnly && flagSkeleton == "": cmd.Usage() return errs.NewError( task, errors.New("-skeleton must be specified when -skeleton_only is set")) } // Make sure the local config directory exists. act, err := ensureLocalConfigDirectoryExists() if err != nil { return err } defer action.RollbackOnError(&err, act) // Set up the global and local configuration file unless -skeleton_only. if !flagSkeletonOnly { if err := assembleAndWriteConfig(); err != nil { return err } } // Install the skeleton into the local config directory if desired. if skeleton := flagSkeleton; skeleton != "" { if err := getAndPourSkeleton(skeleton); err != nil { return err } } fmt.Println() log.Log("Successfully bootstrapped the repository for SalsaFlow") log.NewLine("Do not forget to commit modified configuration files!") return nil }
func rewriteCommits(commits []*git.Commit, firstMissingOffset int) ([]*git.Commit, error) { // Fetch the stories in progress from the issue tracker. storiesTask := "Missing Story-Id detected, fetch stories from the issue tracker" log.Run(storiesTask) tracker, err := modules.GetIssueTracker() if err != nil { return nil, errs.NewError(storiesTask, err) } task := "Fetch the user record from the issue tracker" me, err := tracker.CurrentUser() if err != nil { return nil, errs.NewError(task, err) } stories, err := tracker.ReviewableStories() if err != nil { return nil, errs.NewError(storiesTask, err) } reviewedStories, err := tracker.ReviewedStories() if err != nil { return nil, errs.NewError(storiesTask, err) } // Show only the stories owned by the current user. // Note: Go sucks here, badly. filterStories := func(stories []common.Story, filter func(common.Story) bool) []common.Story { ss := make([]common.Story, 0, len(stories)) for _, story := range stories { if filter(story) { ss = append(ss, story) } } return ss } mine := func(story common.Story) bool { for _, assignee := range story.Assignees() { if assignee.Id() == me.Id() { return true } } return false } stories = filterStories(stories, mine) reviewedStories = filterStories(reviewedStories, mine) // Tell the user what is happening. log.Run("Prepare a temporary branch to rewrite commit messages") // Get the current branch name. currentBranch, err := gitutil.CurrentBranch() if err != nil { return nil, err } // Get the parent of the first commit in the chain. task = "Get the parent commit of the commit chain to be posted" var parentSHA string if firstMissingOffset != 0 { // In case there are multiple commits being posted // and the first missing offset is not pointing to the first commit, // we can easily get the parent SHA by just accessing the commit list. parentSHA = commits[firstMissingOffset-1].SHA } else { // Otherwise we have to ask git for help. stdout, err := git.Log("--pretty=%P", "-n", "1", commits[firstMissingOffset].SHA) if err != nil { return nil, errs.NewError(task, err) } parentSHA = strings.Fields(stdout.String())[0] } // Prepare a temporary branch that will be used to amend commit messages. task = "Create a temporary branch to rewrite commit messages" if err := git.SetBranch(constants.TempBranchName, parentSHA); err != nil { return nil, errs.NewError(task, err) } defer func() { // Delete the temporary branch on exit. task := "Delete the temporary branch" if err := git.Branch("-D", constants.TempBranchName); err != nil { errs.LogError(task, err) } }() // Checkout the temporary branch. task = "Checkout the temporary branch" if err := git.Checkout(constants.TempBranchName); err != nil { return nil, errs.NewError(task, err) } defer func() { // Checkout the original branch on exit. task := fmt.Sprintf("Checkout branch '%v'", currentBranch) if err := git.Checkout(currentBranch); err != nil { errs.LogError(task, err) } }() // Loop and rewrite the commit messages. var story common.Story if flagAskOnce { header := ` Some of the commits listed above are not assigned to any story. Please pick up the story that these commits will be assigned to. You can also insert 'u' to mark the commits as unassigned:` selectedStory, err := promptForStory(header, stories, reviewedStories) if err != nil { return nil, err } story = selectedStory } // The temp branch is pointing to the parent of the first commit missing // the Story-Id tag. So we only need to cherry-pick the commits that // follow the first commit missing the Story-Id tag. commitsToCherryPick := commits[firstMissingOffset:] for _, commit := range commitsToCherryPick { // Cherry-pick the commit. task := fmt.Sprintf("Move commit %v onto the temporary branch", commit.SHA) if err := git.CherryPick(commit.SHA); err != nil { return nil, errs.NewError(task, err) } if commit.StoryIdTag == "" { if !flagAskOnce { commitMessageTitle := prompt.ShortenCommitTitle(commit.MessageTitle) // Ask for the story ID for the current commit. header := fmt.Sprintf(` The following commit is not assigned to any story: commit hash: %v commit title: %v Please pick up the story to assign the commit to. Inserting 'u' will mark the commit as unassigned:`, commit.SHA, commitMessageTitle) selectedStory, err := promptForStory(header, stories, reviewedStories) if err != nil { return nil, err } story = selectedStory } // Use the unassigned tag value in case no story is selected. storyTag := git.StoryIdUnassignedTagValue if story != nil { storyTag = story.Tag() } // Extend the commit message to include Story-Id. commitMessage := fmt.Sprintf("%v\nStory-Id: %v\n", commit.Message, storyTag) // Amend the cherry-picked commit to include the new commit message. task = "Amend the commit message for " + commit.SHA stderr := new(bytes.Buffer) cmd := exec.Command("git", "commit", "--amend", "-F", "-") cmd.Stdin = bytes.NewBufferString(commitMessage) cmd.Stderr = stderr if err := cmd.Run(); err != nil { return nil, errs.NewErrorWithHint(task, err, stderr.String()) } } } // Reset the current branch to point to the new branch. task = "Reset the current branch to point to the temporary branch" if err := git.SetBranch(currentBranch, constants.TempBranchName); err != nil { return nil, errs.NewError(task, err) } // Parse the commits again since the commit hashes have changed. newCommits, err := git.ShowCommitRange(parentSHA + "..") if err != nil { return nil, err } log.NewLine("") log.Log("Commit messages amended successfully") // And we are done! return newCommits, 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 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 load(args *loadArgs) error { // Save the args into regular variables. var ( configKind = args.configKind configKey = args.configKey container = args.configContainer readConfig = args.readConfig emptyConfig = args.emptyConfig disallowPrompt = args.disallowPrompt ) // Do nothing in case the container is nil. if container == nil { return nil } // Read the configuration file. configFile, err := readConfig() if err != nil { if emptyConfig == nil || !os.IsNotExist(errs.RootCause(err)) { return err } // In case the file is not there, initialise a new one. configFile = emptyConfig() } prompt := func(err error) error { if disallowPrompt { dialogTask := "Prompt the user for configuration according to the spec" dialogErr := errors.New("configuration dialog is disabled") dialogHint := ` The configuration dialog for the local configuration file can only be run during 'repo bootstrap' so that the configuration file is not modified without anybody noticing in the middle of other work being done. Please fix the issues manually, either by manually editing the local configuration file or by re-running 'repo bootstrap' command. Don't forget to commit the changes. ` if ex, ok := err.(errs.Err); ok { err := errs.NewErrorWithHint(dialogTask, dialogErr, dialogHint) return errs.NewErrorWithHint(ex.Task(), err, ex.Hint()) } return errs.NewErrorWithHint(dialogTask, err, dialogHint) } return promptAndWrite(configFile, args) } // Find the config record for the given key. // In case there is an error and the prompt is allowed, prompt the user. section, err := configFile.ConfigRecord(configKey) if err != nil { return prompt(err) } // Unmarshal the record according to the spec. // In case there is an error and the prompt is allowed, prompt the user. if err := unmarshal(section.RawConfig, container); err != nil { fmt.Println() log.Log(fmt.Sprintf( "Failed to unmarshal %v configuration, will try to run the bootstrap dialog", configKind)) log.NewLine(fmt.Sprintf("(err = %v)", err.Error())) return prompt(err) } // Validate the returned object according to the spec. // In case there is an error and the prompt is allowed, prompt the user. if err := validate(container, section.Path()); err != nil { fmt.Println() log.Log(fmt.Sprintf( "%v configuration section invalid, will try to run the bootstrap dialog", strings.Title(configKind))) log.NewLine(fmt.Sprintf("(error = %v)", err.Error())) return prompt(err) } return nil }
func Init(force bool) error { // Check whether the repository has been initialised yet. task := "Check whether the repository has been initialised" versionString, err := git.GetConfigString("salsaflow.initialised") if err != nil { return errs.NewError(task, err) } if versionString == metadata.Version && !force { return errs.NewError(task, ErrInitialised) } log.Log("Initialising the repository for SalsaFlow") // Make sure the user is using the right version of Git. // // The check is here and not in app.Init because it is highly improbable // that the check would pass once and then fail later. Once the right // version of git is installed, it most probably stays. task = "Check the git version being used" log.Run(task) stdout, err := git.Run("--version") if err != nil { return errs.NewError(task, err) } pattern := regexp.MustCompile("^git version (([0-9]+)[.]([0-9]+).*)") parts := pattern.FindStringSubmatch(stdout.String()) if len(parts) != 4 { return errs.NewError(task, errors.New("unexpected git --version output")) } gitVersion := parts[1] // This cannot fail since we matched the regexp. major, _ := strconv.Atoi(parts[2]) minor, _ := strconv.Atoi(parts[3]) // We need Git version 1.8.5.4+, so let's require 1.9+. switch { case major >= 2: // OK case major == 1 && minor >= 9: // OK default: hint := ` You need Git version 1.9.0 or newer. ` return errs.NewErrorWithHint( task, errors.New("unsupported git version detected: "+gitVersion), hint) } // Get hold of a git config instance. gitConfig, err := git.LoadConfig() if err != nil { return err } var ( remoteName = gitConfig.RemoteName trunkBranch = gitConfig.TrunkBranchName stableBranch = gitConfig.StableBranchName ) // Make sure that the stable branch exists. task = fmt.Sprintf("Make sure branch '%v' exists", stableBranch) log.Run(task) err = git.CheckOrCreateTrackingBranch(stableBranch, remoteName) if err != nil { if ex, ok := err.(*git.ErrRefNotFound); ok { hint := fmt.Sprintf( "Make sure that branch '%v' exists and run init again.\n", ex.Ref) return errs.NewErrorWithHint(task, err, hint) } else if _, ok := err.(*git.ErrRefNotInSync); !ok { // We ignore ErrRefNotInSync here, so return the error // in case it is of some other kind. return errs.NewError(task, err) } } // Make sure that the trunk branch exists. task = fmt.Sprintf("Make sure branch '%v' exists", trunkBranch) log.Run(task) err = git.CheckOrCreateTrackingBranch(trunkBranch, remoteName) if err != nil { if _, ok := err.(*git.ErrRefNotFound); ok { task := fmt.Sprintf("Create branch '%v'", trunkBranch) log.Log(fmt.Sprintf( "Branch '%v' not found. Will create one for you for free!", trunkBranch)) if err := git.Branch(trunkBranch, stableBranch); err != nil { return errs.NewError(task, err) } log.NewLine(fmt.Sprintf( "The newly created branch is pointing to '%v'.", stableBranch)) task = fmt.Sprintf("Push branch '%v' to remote '%v'", trunkBranch, remoteName) log.Run(task) if err := git.Push(remoteName, trunkBranch+":"+trunkBranch); err != nil { return errs.NewError(task, err) } } else if _, ok := err.(*git.ErrRefNotInSync); !ok { // We ignore ErrRefNotInSync here, so return the error // in case it is of some other kind. return errs.NewError(task, err) } } // Verify our git hooks are installed and used. for _, kind := range hooks.HookTypes { task := fmt.Sprintf("Check the current git %v hook", kind) log.Run(task) if err := hooks.CheckAndUpsert(kind, force); err != nil { return errs.NewError(task, err) } } // Run other registered init hooks. task = "Running the registered repository init hooks" log.Log(task) if err := executeInitHooks(); err != nil { return errs.NewError(task, err) } // Success! Mark the repository as initialised in git config. task = "Mark the repository as initialised" if err := git.SetConfigString("salsaflow.initialised", metadata.Version); err != nil { return err } asciiart.PrintThumbsUp() fmt.Println() log.Log("The repository is initialised") return nil }