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 main() { // Set up the identification command line flag. hooks.IdentifyYourself() // Tell the user what is happening. fmt.Println("---> Running SalsaFlow pre-push hook") // The hook is always invoked as `pre-push <remote-name> <push-url>`. if len(os.Args) != 3 { fmt.Fprintf(os.Stderr, "Usage: %v <remote-name> <push-url>\n", os.Args[0]) errs.Fatal(fmt.Errorf("invalid arguments: %#v\n", os.Args[1:])) } // Run the main function. if err := run(os.Args[1], os.Args[2]); err != nil { if err != prompt.ErrCanceled { fmt.Println() errs.Log(err) } asciiart.PrintGrimReaper("PUSH ABORTED") os.Exit(1) } // Insert an empty line before git push output. fmt.Println() }
func GetByBranch(branch string) (ver *Version, err error) { // Remember the current branch. currentBranch, err := git.CurrentBranch() if err != nil { return nil, err } // Checkout the target branch. if err := git.Checkout(branch); err != nil { return nil, err } defer func() { // Checkout the original branch on return. if ex := git.Checkout(currentBranch); ex != nil { if err == nil { err = ex } else { errs.Log(ex) } } }() // Get the version. v, err := Get() if err != nil { if ex, ok := err.(*scripts.ErrNotFound); ok { return nil, fmt.Errorf( "custom SalsaFlow script '%v' not found on branch '%v'", ex.ScriptName(), branch) } return nil, err } return v, nil }
func ResetKeep(branch, ref string) (err error) { // Remember the current branch. currentBranch, err := CurrentBranch() if err != nil { return err } // Checkout the branch to be reset. if err := Checkout(branch); err != nil { return err } defer func() { // Checkout the original branch on return. if ex := Checkout(currentBranch); ex != nil { if err == nil { err = ex } else { errs.Log(ex) } } }() // Reset the branch. _, err = Run("reset", "--keep", ref) return err }
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 (chain *ActionChain) Rollback() error { var ex error for i := range chain.actions { act := chain.actions[len(chain.actions)-1-i] // Inform the user what is happening. if task := act.task; task != "" { log.Rollback(task) } // Run the rollback function registered. if err := act.action.Rollback(); err != nil { errs.Log(err) ex = ErrRollbackFailed } } return ex }
func implementedDialog(ctxs []*common.ReviewContext) (implemented bool, act action.Action, err error) { // Collect the affected stories. var ( stories = make([]common.Story, 0, len(ctxs)) storySet = make(map[string]struct{}, len(ctxs)) ) for _, ctx := range ctxs { story := ctx.Story // Skip unassigned commits. if story == nil { continue } rid := story.ReadableId() if _, ok := storySet[rid]; ok { continue } // Collect only the stories that are Being Implemented. // The transition doesn't make sense for other story states. if story.State() != common.StoryStateBeingImplemented { continue } storySet[rid] = struct{}{} stories = append(stories, story) } // Do nothing in case there are no stories left. if len(stories) == 0 { return false, nil, nil } // Prompt the user for confirmation. fmt.Println("\nIt is possible to mark the affected stories as implemented.") fmt.Println("The following stories were associated with one or more commits:\n") storyprompt.ListStories(stories, os.Stdout) fmt.Println() confirmed, err := prompt.Confirm( "Do you wish to mark these stories as implemented?", false) if err != nil { return false, nil, err } fmt.Println() if !confirmed { return false, nil, nil } // Always update as many stories as possible. var ( chain = action.NewActionChain() errUpdateFailed = errors.New("failed to update stories in the issue tracker") ex error ) for _, story := range stories { task := fmt.Sprintf("Mark story %v as implemented", story.ReadableId()) log.Run(task) act, err := story.MarkAsImplemented() if err != nil { errs.Log(errs.NewError(task, err)) ex = errUpdateFailed continue } chain.PushTask(task, act) } if ex != nil { if err := chain.Rollback(); err != nil { errs.Log(err) } return false, nil, ex } return true, chain, nil }
func (tool *codeReviewTool) PostReviewRequests( ctxs []*common.ReviewContext, opts map[string]interface{}, ) (err error) { // Load the GitHub config. config, err := LoadConfig() if err != nil { return err } // Get the GitHub owner and repository from the upstream URL. owner, repo, err := git.ParseUpstreamURL() if err != nil { return err } // Group commits by story ID. // // In case the commit is associated with a story, we add it to the relevant story group. // Otherwise the commit is marked as unassigned and added to the relevant list. var ( ctxsByStoryId = make(map[string][]*common.ReviewContext, 1) unassignedCommits = make([]*git.Commit, 0, 1) ) for _, ctx := range ctxs { story := ctx.Story if story != nil { sid := story.ReadableId() rcs, ok := ctxsByStoryId[sid] if ok { rcs = append(rcs, ctx) } else { rcs = []*common.ReviewContext{ctx} } ctxsByStoryId[sid] = rcs } else { unassignedCommits = append(unassignedCommits, ctx.Commit) } } // Post the assigned commits. for _, ctxs := range ctxsByStoryId { var ( story = ctxs[0].Story commits = make([]*git.Commit, 0, len(ctxs)) ) for _, ctx := range ctxs { commits = append(commits, ctx.Commit) } if ex := postAssignedReviewRequest(config, owner, repo, story, commits, opts); ex != nil { errs.Log(ex) err = errPostReviewRequest } } // Post the unassigned commits. for _, commit := range unassignedCommits { if ex := postUnassignedReviewRequest(config, owner, repo, commit, opts); ex != nil { errs.Log(ex) err = errPostReviewRequest } } return }
func merge(mergeTask, current, parent string) (act action.Action, err error) { // Remember the current branch hash. currentSHA, err := git.BranchHexsha(current) if err != nil { return nil, err } // Checkout the parent branch so that we can perform the merge. if err := git.Checkout(parent); err != nil { return nil, err } // Checkout the current branch on return to be consistent. defer func() { if ex := git.Checkout(current); ex != nil { if err == nil { err = ex } else { errs.Log(ex) } } }() // Perform the merge. // Use --no-ff in case -merge_no_ff is set. if flagMergeNoFF { err = git.Merge(current, "--no-ff") } else { err = git.Merge(current) } if err != nil { return nil, err } // Return a rollback action. return action.ActionFunc(func() (err error) { log.Rollback(mergeTask) task := fmt.Sprintf("Reset branch '%v' to the original position", current) // Get the branch is the current branch now. currentNow, err := gitutil.CurrentBranch() if err != nil { return errs.NewError(task, err) } // Checkout current in case it is not the same as the current branch now. if currentNow != current { if err := git.Checkout(current); err != nil { return errs.NewError(task, err) } defer func() { if ex := git.Checkout(currentNow); ex != nil { if err == nil { err = ex } else { errs.Log(ex) } } }() } // Reset the branch to the original position. if err := git.Reset("--keep", currentSHA); err != nil { return errs.NewError(task, err) } return nil }), nil }
func postBranch(parentBranch string) (err error) { // Load the git-related config. gitConfig, err := git.LoadConfig() if err != nil { return err } var ( remoteName = gitConfig.RemoteName ) // Get the current branch name. currentBranch, err := gitutil.CurrentBranch() if err != nil { return err } if !flagNoFetch { // Fetch the remote repository. task := "Fetch the remote repository" log.Run(task) if err := git.UpdateRemotes(remoteName); err != nil { return errs.NewError(task, err) } } // Make sure the parent branch is up to date. task := fmt.Sprintf("Make sure reference '%v' is up to date", parentBranch) log.Run(task) if err := git.EnsureBranchSynchronized(parentBranch, remoteName); err != nil { return errs.NewError(task, err) } // Make sure the current branch is up to date. task = fmt.Sprintf("Make sure branch '%v' is up to date", currentBranch) log.Run(task) if err = git.EnsureBranchSynchronized(currentBranch, remoteName); err != nil { return errs.NewError(task, err) } // Get the commits to be posted task = "Get the commits to be posted for code review" commits, err := git.ShowCommitRange(parentBranch + "..") if err != nil { return errs.NewError(task, err) } // Make sure there are no merge commits. if err := ensureNoMergeCommits(commits); err != nil { return err } // Prompt the user to confirm. if err := promptUserToConfirmCommits(commits); err != nil { return err } // Rebase the current branch on top the parent branch. if !flagNoRebase { commits, err = rebase(currentBranch, parentBranch) if err != nil { return err } } // Ensure the Story-Id tag is there. commits, _, err = ensureStoryId(commits) if err != nil { return err } // Get data on the current branch. task = fmt.Sprintf("Get data on branch '%v'", currentBranch) remoteCurrentExists, err := git.RemoteBranchExists(currentBranch, remoteName) if err != nil { return errs.NewError(task, err) } currentUpToDate, err := git.IsBranchSynchronized(currentBranch, remoteName) if err != nil { return errs.NewError(task, err) } // Merge the current branch into the parent branch unless -no_merge. pushTask := "Push the current branch" if flagNoMerge { // In case the user doesn't want to merge, // we need to push the current branch. if !remoteCurrentExists || !currentUpToDate { if err := push(remoteName, currentBranch); err != nil { return errs.NewError(pushTask, err) } } } else { // Still push the current branch if necessary. if remoteCurrentExists && !currentUpToDate { if err := push(remoteName, currentBranch); err != nil { return errs.NewError(pushTask, err) } } // Merge the branch into the parent branch mergeTask := fmt.Sprintf("Merge branch '%v' into branch '%v'", currentBranch, parentBranch) log.Run(mergeTask) act, err := merge(mergeTask, currentBranch, parentBranch) if err != nil { return err } // Push the parent branch. if err := push(remoteName, parentBranch); err != nil { // In case the push fails, we revert the merge as well. if err := act.Rollback(); err != nil { errs.Log(err) } return errs.NewError(mergeTask, err) } // Register a rollback function that just says that // a pushed merge cannot be reverted. defer action.RollbackOnError(&err, action.ActionFunc(func() error { log.Rollback(mergeTask) hint := "\nCannot revert merge that has already been pushed.\n" return errs.NewErrorWithHint( "Revert the merge", errors.New("merge commit already pushed"), hint) })) } // Post the review requests. if err := postCommitsForReview(commits); err != nil { return err } // In case there is no error, tell the user they can do next. return printFollowup() }
func (tool *codeReviewTool) PostReviewRequests( ctxs []*common.ReviewContext, opts map[string]interface{}, ) (err error) { // Get the GitHub owner and repository from the upstream URL. owner, repo, err := ghutil.ParseUpstreamURL() if err != nil { return err } // Group commits by story ID. // // In case the commit is associated with a story, we add it to the relevant story group. // Otherwise the commit is marked as unassigned and added to the relevant list. var ( ctxsByStoryId = make(map[string][]*common.ReviewContext, 1) unassignedCommits = make([]*git.Commit, 0, 1) ) for _, ctx := range ctxs { story := ctx.Story if story != nil { sid := story.ReadableId() rcs, ok := ctxsByStoryId[sid] if ok { rcs = append(rcs, ctx) } else { rcs = []*common.ReviewContext{ctx} } ctxsByStoryId[sid] = rcs } else { unassignedCommits = append(unassignedCommits, ctx.Commit) } } // Post the assigned commits. _, open := opts["open"] for _, ctxs := range ctxsByStoryId { var ( story = ctxs[0].Story commits = make([]*git.Commit, 0, len(ctxs)) ) for _, ctx := range ctxs { commits = append(commits, ctx.Commit) } // Create/update the review issue. issue, postedCommits, ex := postAssignedReviewRequest( tool.config, owner, repo, story, commits, opts) if ex != nil { errs.Log(ex) err = errPostReviewRequest continue } // Add comments to the commits posted for review. linkCommitsToReviewIssue(tool.config, owner, repo, *issue.Number, postedCommits) // Open the review issue in the browser if requested. if open { openIssue(issue) } } // Get the value of -fixes. var fixes int flagFixes, ok := opts["fixes"] if ok { if v, ok := flagFixes.(uint); ok && v != 0 { fixes = int(v) } } // Post the unassigned commits. for _, commit := range unassignedCommits { var ( issue *github.Issue postedCommits []*git.Commit ex error ) if fixes != 0 { // Extend the specified review issue. issue, postedCommits, ex = extendUnassignedReviewRequest( tool.config, owner, repo, fixes, commit, opts) } else { // Create/update the review issue. issue, postedCommits, ex = postUnassignedReviewRequest( tool.config, owner, repo, commit, opts) } if ex != nil { errs.Log(ex) err = errPostReviewRequest continue } // Add comments to the commits posted for review. linkCommitsToReviewIssue(tool.config, owner, repo, *issue.Number, postedCommits) // Open the review issue in the browser if requested. if open { openIssue(issue) } } return }
func (manager *releaseNotesManager) PostReleaseNotes( releaseNotes *common.ReleaseNotes, ) (action.Action, error) { // Get the GitHub owner and repository from the upstream URL. owner, repo, err := github.ParseUpstreamURL() if err != nil { return nil, err } // Instantiate the API client. client := github.NewClient(manager.config.Token) // Format the release notes. task := "Format the release notes" body := bytes.NewBufferString(` ## Summary ## **PLEASE FILL IN THE RELEASE SUMMARY** `) encoder, err := notes.NewEncoder(notes.EncodingMarkdown, body) if err != nil { return nil, errs.NewError(task, err) } if err := encoder.Encode(releaseNotes, nil); err != nil { return nil, errs.NewError(task, err) } bodyString := body.String() // Create GitHub release for the given version. tag := releaseNotes.Version.ReleaseTagString() releaseTask := fmt.Sprintf("Create GitHub release for tag '%v'", tag) log.Run(releaseTask) release, _, err := client.Repositories.CreateRelease(owner, repo, &gh.RepositoryRelease{ TagName: gh.String(tag), Name: gh.String("Release " + releaseNotes.Version.BaseString()), Body: &bodyString, Draft: gh.Bool(true), }) if err != nil { return nil, err } // Delete the GitHub release on rollback. rollback := func() error { log.Rollback(releaseTask) task := fmt.Sprintf("Delete GitHub release for tag '%v'", tag) _, err := client.Repositories.DeleteRelease(owner, repo, *release.ID) if err != nil { return errs.NewError(task, err) } return nil } // Open the release in the browser so that the user can fill in the details. task = "Open the release notes in the browser" if err := webbrowser.Open(*release.HTMLURL); err != nil { if ex := rollback(); ex != nil { errs.Log(ex) } return nil, errs.NewError(task, err) } // Return the rollback function. return action.ActionFunc(rollback), nil }
func createBranch() (action.Action, error) { // Get the current branch name. originalBranch, err := gitutil.CurrentBranch() if err != nil { return nil, err } // Fetch the remote repository. task := "Fetch the remote repository" log.Run(task) gitConfig, err := git.LoadConfig() if err != nil { return nil, errs.NewError(task, err) } var ( remoteName = gitConfig.RemoteName baseBranch = gitConfig.TrunkBranchName ) if flagBase != "" { baseBranch = flagBase } // Fetch the remote repository. if err := git.UpdateRemotes(remoteName); err != nil { return nil, errs.NewError(task, err) } // Make sure the trunk branch is up to date. task = fmt.Sprintf("Make sure branch '%v' is up to date", baseBranch) log.Run(task) if err := git.CheckOrCreateTrackingBranch(baseBranch, remoteName); err != nil { return nil, errs.NewError(task, err) } // Prompt the user for the branch name. task = "Prompt the user for the branch name" line, err := prompt.Prompt(` Please insert the branch slug now. Insert an empty string to skip the branch creation step: `) if err != nil && err != prompt.ErrCanceled { return nil, errs.NewError(task, err) } sluggedLine := slug.Slug(line) if sluggedLine == "" { fmt.Println() log.Log("Not creating any feature branch") return nil, nil } branchName := "story/" + sluggedLine ok, err := prompt.Confirm( fmt.Sprintf( "\nThe branch that is going to be created will be called '%s'.\nIs that alright?", branchName), true) if err != nil { return nil, errs.NewError(task, err) } if !ok { panic(prompt.ErrCanceled) } fmt.Println() createTask := fmt.Sprintf( "Create branch '%v' on top of branch '%v'", branchName, baseBranch) log.Run(createTask) if err := git.Branch(branchName, baseBranch); err != nil { return nil, errs.NewError(createTask, err) } deleteTask := fmt.Sprintf("Delete branch '%v'", branchName) deleteBranch := func() error { // Roll back and delete the newly created branch. log.Rollback(createTask) if err := git.Branch("-D", branchName); err != nil { return errs.NewError(deleteTask, err) } return nil } // Checkout the newly created branch. checkoutTask := fmt.Sprintf("Checkout branch '%v'", branchName) log.Run(checkoutTask) if err := git.Checkout(branchName); err != nil { if err := deleteBranch(); err != nil { errs.Log(err) } return nil, errs.NewError(checkoutTask, err) } // Push the newly created branch unless -no_push. pushTask := fmt.Sprintf("Push branch '%v' to remote '%v'", branchName, remoteName) if flagPush { log.Run(pushTask) if err := git.Push(remoteName, branchName); err != nil { if err := deleteBranch(); err != nil { errs.Log(err) } return nil, errs.NewError(pushTask, err) } } return action.ActionFunc(func() error { // Checkout the original branch. log.Rollback(checkoutTask) if err := git.Checkout(originalBranch); err != nil { return errs.NewError( fmt.Sprintf("Checkout the original branch '%v'", originalBranch), err) } // Delete the newly created branch. deleteErr := deleteBranch() // In case we haven't pushed anything, we are done. if !flagPush { return deleteErr } // Delete the branch from the remote repository. log.Rollback(pushTask) if _, err := git.Run("push", "--delete", remoteName, branchName); err != nil { // In case deleteBranch failed, tell the user now // since we are not going to return that error. if deleteErr != nil { errs.Log(deleteErr) } return errs.NewError( fmt.Sprintf("Delete branch '%v' from remote '%v'", branchName, remoteName), err) } // Return deleteErr to make sure it propagates up. return deleteErr }), nil }
func downloadAndInstallAsset(assetName, assetURL, dstDir string) error { // Download the asset. task := "Download " + assetName log.Run(task) resp, err := http.Get(assetURL) if err != nil { return errs.NewError(task, err) } defer resp.Body.Close() // Unpack the asset (in-memory). // We keep the asset in the memory since it is never going to be that big. task = "Read the asset into an internal buffer" var capacity = resp.ContentLength if capacity == -1 { capacity = 0 } bodyBuffer := bytes.NewBuffer(make([]byte, 0, capacity)) _, err = io.Copy(bodyBuffer, resp.Body) if err != nil { return errs.NewError(task, err) } task = "Replace SalsaFlow executables" archive, err := zip.NewReader(bytes.NewReader(bodyBuffer.Bytes()), int64(bodyBuffer.Len())) if err != nil { return errs.NewError(task, err) } var numThreads int errCh := make(chan errs.Err, len(archive.File)) // Uncompress all the executables in the archive and move them into place. // This part replaces the current executables with new ones just downloaded. for _, file := range archive.File { if file.CompressedSize64 == 0 { continue } numThreads++ go func(file *zip.File) { baseName := filepath.Base(file.Name) task := fmt.Sprintf("Uncompress executable '%v'", baseName) log.Go(task) src, err := file.Open() if err != nil { errCh <- errs.NewError(task, err) return } task = fmt.Sprintf("Move executable '%v' into place", baseName) log.Go(task) if err := replaceExecutable(src, dstDir, baseName); err != nil { src.Close() errCh <- errs.NewError(task, err) return } src.Close() errCh <- nil }(file) } task = "install given SalsaFlow package" var ex error for i := 0; i < numThreads; i++ { if err := <-errCh; err != nil { errs.Log(err) ex = errs.NewError(task, ErrInstallationFailed) } } return ex }