// WriteNewReports takes a list of CI reports read from GitHub, and writes to the repo any that are new. // // The passed in logChan variable is used as our intermediary for logging, and allows us to // use the same logic for logging messages in either our CLI or our App Engine apps, even though // the two have different logging frameworks. func WriteNewReports(reportsMap map[string][]ci.Report, repo repository.Repo, logChan chan<- string) error { for commit, commitReports := range reportsMap { existingReports := ci.ParseAllValid(repo.GetNotes(ci.Ref, commit)) for _, report := range commitReports { bytes, err := json.Marshal(report) note := repository.Note(bytes) if err != nil { return err } missing := true for _, existing := range existingReports { if existing == report { missing = false } } if missing { logChan <- fmt.Sprintf("Found a new report for %.12s: %q", commit, string(bytes)) if err := repo.AppendNote(ci.Ref, commit, note); err != nil { return err } } } } return nil }
// updateReviewDiffs updates the status of a differential review so that it matches the state of the repo. // // This consists of making sure the latest commit pushed to the review ref has a corresponding // diff in the differential review. func (arc Arcanist) updateReviewDiffs(repo repository.Repo, differentialReview DifferentialReview, headCommit string, req request.Request, r review.Review) { if differentialReview.isClosed() { return } headRevision := headCommit mergeBase, err := repo.MergeBase(req.TargetRef, headRevision) if err != nil { log.Fatal(err) } for _, hashPair := range differentialReview.Hashes { if len(hashPair) == 2 && hashPair[0] == commitHashType && hashPair[1] == headCommit { // The review already has the hash of the HEAD commit, so we have nothing to do beyond mirroring comments // and build status if applicable arc.mirrorCommentsIntoReview(repo, differentialReview, r) return } } diff, err := arc.createDifferentialDiff(repo, mergeBase, headRevision, req, differentialReview.Diffs) if err != nil { log.Fatal(err) } if diff == nil { // This means that phabricator silently refused to create the diff. Just move on. return } updateRequest := differentialUpdateRevisionRequest{ID: differentialReview.ID, DiffID: strconv.Itoa(diff.ID)} var updateResponse differentialUpdateRevisionResponse runArcCommandOrDie("differential.updaterevision", updateRequest, &updateResponse) if updateResponse.Error != "" { log.Fatal(updateResponse.ErrorMessage) } }
// Validate that the user's request to rebase a review makes sense. // // This checks both that the request is well formed, and that the // corresponding review is in a state where rebasing is appropriate. func validateRebaseRequest(repo repository.Repo, args []string) (*review.Review, error) { var r *review.Review var err error if len(args) > 1 { return nil, errors.New("Only rebasing a single review is supported.") } if len(args) == 1 { r, err = review.Get(repo, args[0]) } else { r, err = review.GetCurrent(repo) } if err != nil { return nil, fmt.Errorf("Failed to load the review: %v\n", err) } if r == nil { return nil, errors.New("There is no matching review.") } if r.Submitted { return nil, errors.New("The review has already been submitted.") } target := r.Request.TargetRef if err := repo.VerifyGitRef(target); err != nil { return nil, err } return r, nil }
// getDiffChanges takes two revisions from which to generate a "git diff", and returns a // slice of "changes" objects that represent that diff as parsed by Phabricator. func (arc Arcanist) getDiffChanges(repo repository.Repo, from, to string) ([]interface{}, error) { // TODO(ojarjur): This is a big hack, but so far there does not seem to be a better solution: // We need to pass a list of "changes" JSON objects that contain the parsed diff contents. // The simplest way to do that parsing seems to be to create a rawDiff and have Phabricator // parse it on the server side. We then read back that diff, and return the changes from it. rawDiff, err := repo.Diff(from, to, "-M", "--no-ext-diff", "--no-textconv", "--src-prefix=a/", "--dst-prefix=b/", fmt.Sprintf("-U%d", 0x7fff), "--no-color") if err != nil { return nil, err } createRequest := differentialCreateRawDiffRequest{Diff: rawDiff} var createResponse differentialCreateRawDiffResponse runArcCommandOrDie("differential.createrawdiff", createRequest, &createResponse) if createResponse.Error != "" { return nil, fmt.Errorf(createResponse.ErrorMessage) } diffID := createResponse.Response.ID diff, err := readDiff(diffID) if err != nil { return nil, err } if diff != nil { return diff.Changes, nil } return nil, fmt.Errorf("Failed to retrieve the raw diff for %s..%s", from, to) }
// GetFirstCommit returns the first commit that is included in the review func (r DifferentialReview) GetFirstCommit(repo repository.Repo) string { var commits []string for _, hashPair := range r.Hashes { // We only care about the hashes for commits, which have exactly two // elements, the first of which is "gtcm". if len(hashPair) == 2 && hashPair[0] == commitHashType { commits = append(commits, hashPair[1]) } } var commitTimestamps []int commitsByTimestamp := make(map[int]string) for _, commit := range commits { if _, err := repo.GetLastParent(commit); err == nil { timeString, err1 := repo.GetCommitTime(commit) timestamp, err2 := strconv.Atoi(timeString) if err1 == nil && err2 == nil { commitTimestamps = append(commitTimestamps, timestamp) // If there are multiple, equally old commits, then the last one wins. commitsByTimestamp[timestamp] = commit } } } if len(commitTimestamps) == 0 { return "" } sort.Ints(commitTimestamps) revision := commitsByTimestamp[commitTimestamps[0]] return revision }
// WriteNewReviews takes a list of reviews read from GitHub, and writes to the repo any review // data that has not already been written to it. // // The passed in logChan variable is used as our intermediary for logging, and allows us to // use the same logic for logging messages in either our CLI or our App Engine apps, even though // the two have different logging frameworks. func WriteNewReviews(reviews []review.Review, repo repository.Repo, logChan chan<- string) error { existingReviews := review.ListAll(repo) for _, r := range reviews { requestNote, err := r.Request.Write() if err != nil { return err } if err != nil { return err } alreadyPresent := false if existing := findMatchingExistingReview(r, existingReviews); existing != nil { alreadyPresent = RequestsOverlap(existing.Request, r.Request) r.Revision = existing.Revision } if !alreadyPresent { requestJSON, err := r.GetJSON() if err != nil { return err } logChan <- fmt.Sprintf("Found a new review for %.12s:\n%s\n", r.Revision, requestJSON) if err := repo.AppendNote(request.Ref, r.Revision, requestNote); err != nil { return err } } if err := WriteNewComments(r, repo, logChan); err != nil { return err } } return nil }
// ListAll returns all reviews stored in the git-notes. func ListAll(repo repository.Repo) []Summary { var reviews []Summary for _, revision := range repo.ListNotedRevisions(request.Ref) { review, err := GetSummary(repo, revision) if err == nil && review != nil { reviews = append(reviews, *review) } } return reviews }
// ListAll returns all reviews stored in the git-notes. func ListAll(repo repository.Repo) []Review { var reviews []Review for _, revision := range repo.ListNotedRevisions(request.Ref) { review := Get(repo, revision) if review != nil { reviews = append(reviews, *review) } } return reviews }
// commentOnReview adds a comment to the current code review. func commentOnReview(repo repository.Repo, args []string) error { commentFlagSet.Parse(args) args = commentFlagSet.Args() if *commentLgtm && *commentNmw { return errors.New("You cannot combine the flags -lgtm and -nmw.") } if *commentLine != 0 && *commentFile == "" { return errors.New("Specifying a line number with the -l flag requires that you also specify a file name with the -f flag.") } var r *review.Review var err error if len(args) > 1 { return errors.New("Only accepting a single review is supported.") } if len(args) == 1 { r = review.Get(repo, args[0]) } else { r, err = review.GetCurrent(repo) } if err != nil { return fmt.Errorf("Failed to load the review: %v\n", err) } if r == nil { return errors.New("There is no matching review.") } commentedUponCommit, err := r.GetHeadCommit() if err != nil { return err } location := comment.Location{ Commit: commentedUponCommit, } if *commentFile != "" { location.Path = *commentFile if *commentLine != 0 { location.Range = &comment.Range{ StartLine: uint32(*commentLine), } } } c := comment.New(repo.GetUserEmail(), *commentMessage) c.Location = &location c.Parent = *commentParent if *commentLgtm || *commentNmw { resolved := *commentLgtm c.Resolved = &resolved } return r.AddComment(c) }
// checkCommentLocation verifies that the given location exists at the given commit. func checkCommentLocation(repo repository.Repo, commit, file string, line uint) error { contents, err := repo.Show(commit, file) if err != nil { return err } lines := strings.Split(contents, "\n") if line > uint(len(lines)) { return fmt.Errorf("Line number %d does not exist in file %q", line, file) } return nil }
// push pushes the local git-notes used for reviews to a remote repo. func push(repo repository.Repo, args []string) error { if len(args) > 1 { return errors.New("Only pushing to one remote at a time is supported.") } remote := "origin" if len(args) == 1 { remote = args[0] } return repo.PushNotes(remote, notesRefPattern) }
// rejectReview adds an NMW comment to the current code review. func rejectReview(repo repository.Repo, args []string) error { rejectFlagSet.Parse(args) args = rejectFlagSet.Args() var r *review.Review var err error if len(args) > 1 { return errors.New("Only rejecting a single review is supported.") } if len(args) == 1 { r, err = review.Get(repo, args[0]) } else { r, err = review.GetCurrent(repo) } if err != nil { return fmt.Errorf("Failed to load the review: %v\n", err) } if r == nil { return errors.New("There is no matching review.") } if *rejectMessageFile != "" && *rejectMessage == "" { *rejectMessage, err = input.FromFile(*rejectMessageFile) if err != nil { return err } } if *rejectMessageFile == "" && *rejectMessage == "" { *rejectMessage, err = input.LaunchEditor(repo, commentFilename) if err != nil { return err } } rejectedCommit, err := r.GetHeadCommit() if err != nil { return err } location := comment.Location{ Commit: rejectedCommit, } resolved := false userEmail, err := repo.GetUserEmail() if err != nil { return err } c := comment.New(userEmail, *rejectMessage) c.Location = &location c.Resolved = &resolved return r.AddComment(c) }
// Refresh advises the review tool that the code being reviewed has changed, and to reload it. // // This corresponds to calling the diffusion.looksoon API. func (arc Arcanist) Refresh(repo repository.Repo) { // We cannot determine the repo's callsign (the identifier Phabricator uses for the repo) // in all cases, but we can figure it out in the case that the mirror runs on the same // directories that Phabricator is using. In that scenario, the repo directories default // to being named "/var/repo/<CALLSIGN>", so if the repo path starts with that prefix then // we can try to strip out that prefix and use the rest as a callsign. if strings.HasPrefix(repo.GetPath(), defaultRepoDirPrefix) { possibleCallsign := strings.TrimPrefix(repo.GetPath(), defaultRepoDirPrefix) request := lookSoonRequest{Callsigns: []string{possibleCallsign}} response := make(map[string]interface{}) runArcCommandOrDie("diffusion.looksoon", request, &response) } }
func syncNotes(repo repository.Repo) error { var err error for attempt := 0; attempt < retryAttempts; attempt++ { err = repo.PullNotes(remoteName, notesRefPattern) if err == nil { err = repo.PushNotes(remoteName, notesRefPattern) if err == nil { return err } } } return err }
// pull updates the local git-notes used for reviews with those from a remote repo. func pull(repo repository.Repo, args []string) error { if len(args) > 1 { return errors.New("Only pulling from one remote at a time is supported.") } remote := "origin" if len(args) == 1 { remote = args[0] } repo.PullNotes(remote, notesRefPattern) return nil }
// computeReviewStartingCommit computes the first commit in the review. func computeReviewStartingCommit(pr github.PullRequest, repo repository.Repo) (string, error) { if pr.Base == nil || pr.Base.SHA == nil || pr.Head == nil || pr.Head.SHA == nil { return "", ErrInsufficientInfo } prCommits, err := repo.ListCommitsBetween(*pr.Base.SHA, *pr.Head.SHA) if err != nil { return "", err } if len(prCommits) == 0 { return *pr.Head.SHA, nil } return prCommits[0], nil }
// push pushes the local git-notes used for reviews to a remote repo. func push(repo repository.Repo, args []string) error { if len(args) > 1 { return errors.New("Only pushing to one remote at a time is supported.") } remote := "origin" if len(args) == 1 { remote = args[0] } if err := repo.PushNotesAndArchive(remote, notesRefPattern, archiveRefPattern); err != nil { return err } return nil }
// GetCurrent returns the current, open code review. // // If there are multiple matching reviews, then an error is returned. func GetCurrent(repo repository.Repo) (*Review, error) { reviewRef := repo.GetHeadRef() var matchingReviews []Review for _, review := range ListOpen(repo) { if review.Request.ReviewRef == reviewRef { matchingReviews = append(matchingReviews, review) } } if matchingReviews == nil { return nil, nil } if len(matchingReviews) != 1 { return nil, fmt.Errorf("There are %d open reviews for the ref \"%s\"", len(matchingReviews), reviewRef) } r := &matchingReviews[0] return r, nil }
// GetSummary returns the summary of the specified code review. // // If no review request exists, the returned review summary is nil. func GetSummary(repo repository.Repo, revision string) (*Summary, error) { requestNotes := repo.GetNotes(request.Ref, revision) commentNotes := repo.GetNotes(comment.Ref, revision) summary, err := getSummaryFromNotes(repo, revision, requestNotes, commentNotes) if err != nil { return nil, err } currentCommit := revision if summary.Request.Alias != "" { currentCommit = summary.Request.Alias } submitted, err := repo.IsAncestor(currentCommit, summary.Request.TargetRef) if err != nil { return nil, err } summary.Submitted = submitted return summary, nil }
// Get returns the specified code review. // // If no review request exists, the returned review is nil. func Get(repo repository.Repo, revision string) *Review { requestNotes := repo.GetNotes(request.Ref, revision) requests := request.ParseAllValid(requestNotes) if requests == nil { return nil } review := Review{ Repo: repo, Revision: revision, Request: requests[len(requests)-1], } review.Comments = review.loadComments() review.Resolved = updateThreadsStatus(review.Comments) review.Submitted = repo.IsAncestor(revision, review.Request.TargetRef) // TODO(ojarjur): Optionally fetch the CI status of the last commit // in the review for which there are comments. return &review }
// GetCurrent returns the current, open code review. // // If there are multiple matching reviews, then an error is returned. func GetCurrent(repo repository.Repo) (*Review, error) { reviewRef, err := repo.GetHeadRef() if err != nil { return nil, err } var matchingReviews []Summary for _, review := range ListOpen(repo) { if review.Request.ReviewRef == reviewRef { matchingReviews = append(matchingReviews, review) } } if matchingReviews == nil { return nil, nil } if len(matchingReviews) != 1 { return nil, fmt.Errorf("There are %d open reviews for the ref \"%s\"", len(matchingReviews), reviewRef) } return matchingReviews[0].Details() }
// Get returns the summary of the specified code review. // // If no review request exists, the returned review summary is nil. func GetSummary(repo repository.Repo, revision string) (*ReviewSummary, error) { requestNotes := repo.GetNotes(request.Ref, revision) requests := request.ParseAllValid(requestNotes) if requests == nil { return nil, nil } reviewSummary := ReviewSummary{ Repo: repo, Revision: revision, Request: requests[len(requests)-1], } reviewSummary.Comments = reviewSummary.loadComments() reviewSummary.Resolved = updateThreadsStatus(reviewSummary.Comments) submitted, err := repo.IsAncestor(revision, reviewSummary.Request.TargetRef) if err != nil { return nil, err } reviewSummary.Submitted = submitted return &reviewSummary, nil }
func getIsSubmittedCheck(repo repository.Repo) func(ref, commit string) bool { refCommitsMap := make(map[string]map[string]bool) getRefCommitsMap := func(ref string) map[string]bool { commitsMap, ok := refCommitsMap[ref] if ok { return commitsMap } commitsMap = make(map[string]bool) for _, commit := range repo.ListCommits(ref) { commitsMap[commit] = true } refCommitsMap[ref] = commitsMap return commitsMap } return func(ref, commit string) bool { return getRefCommitsMap(ref)[commit] } }
// acceptReview adds an LGTM comment to the current code review. func acceptReview(repo repository.Repo, args []string) error { acceptFlagSet.Parse(args) args = acceptFlagSet.Args() var r *review.Review var err error if len(args) > 1 { return errors.New("Only accepting a single review is supported.") } if len(args) == 1 { r, err = review.Get(repo, args[0]) } else { r, err = review.GetCurrent(repo) } if err != nil { return fmt.Errorf("Failed to load the review: %v\n", err) } if r == nil { return errors.New("There is no matching review.") } acceptedCommit, err := r.GetHeadCommit() if err != nil { return err } location := comment.Location{ Commit: acceptedCommit, } resolved := true userEmail, err := repo.GetUserEmail() if err != nil { return err } c := comment.New(userEmail, *acceptMessage) c.Location = &location c.Resolved = &resolved return r.AddComment(c) }
// Get the commit at which the review request should be anchored. func getReviewCommit(repo repository.Repo, r request.Request, args []string) (string, string, error) { if len(args) > 1 { return "", "", errors.New("Only updating a single review is supported.") } if len(args) == 1 { base, err := repo.MergeBase(r.TargetRef, args[0]) if err != nil { return "", "", err } return args[0], base, nil } base, err := repo.MergeBase(r.TargetRef, r.ReviewRef) if err != nil { return "", "", err } reviewCommits, err := repo.ListCommitsBetween(base, r.ReviewRef) if err != nil { return "", "", err } if reviewCommits == nil { return "", "", errors.New("There are no commits included in the review request") } return reviewCommits[0], base, nil }
func unsortedListAll(repo repository.Repo) []Summary { reviewNotesMap, err := repo.GetAllNotes(request.Ref) if err != nil { return nil } discussNotesMap, err := repo.GetAllNotes(comment.Ref) if err != nil { return nil } isSubmittedCheck := getIsSubmittedCheck(repo) var reviews []Summary for commit, notes := range reviewNotesMap { summary, err := getSummaryFromNotes(repo, commit, notes, discussNotesMap[commit]) if err != nil { continue } summary.Submitted = isSubmittedCheck(summary.Request.TargetRef, summary.getStartingCommit()) reviews = append(reviews, *summary) } return reviews }
// WriteNewComments takes a list of review comments read from GitHub, and writes to the repo any that are new. // // The passed in logChan variable is used as our intermediary for logging, and allows us to // use the same logic for logging messages in either our CLI or our App Engine apps, even though // the two have different logging frameworks. func WriteNewComments(r review.Review, repo repository.Repo, logChan chan<- string) error { existingComments := comment.ParseAllValid(repo.GetNotes(comment.Ref, r.Revision)) for _, commentThread := range r.Comments { commentNote, err := commentThread.Comment.Write() if err != nil { return err } missing := true for _, existing := range existingComments { if CommentsOverlap(existing, commentThread.Comment) { missing = false } } if missing { logChan <- fmt.Sprintf("Found a new comment: %q", string(commentNote)) if err := repo.AppendNote(comment.Ref, r.Revision, commentNote); err != nil { return err } } } return nil }
// LaunchEditor launches the default editor configured for the given repo. This // method blocks until the editor command has returned. // // The specified filename should be a temporary file and provided as a relative path // from the repo (e.g. "FILENAME" will be converted to ".git/FILENAME"). This file // will be deleted after the editor is closed and its contents have been read. // // This method returns the text that was read from the temporary file, or // an error if any step in the process failed. func LaunchEditor(repo repository.Repo, fileName string) (string, error) { editor, err := repo.GetCoreEditor() if err != nil { return "", fmt.Errorf("Unable to detect default git editor: %v\n", err) } path := fmt.Sprintf("%s/.git/%s", repo.GetPath(), fileName) cmd, err := startInlineCommand(editor, path) if err != nil { // Running the editor directly did not work. This might mean that // the editor string is not a path to an executable, but rather // a shell command (e.g. "emacsclient --tty"). As such, we'll try // to run the command through bash, and if that fails, try with sh args := []string{"-c", fmt.Sprintf("%s %q", editor, path)} cmd, err = startInlineCommand("bash", args...) if err != nil { cmd, err = startInlineCommand("sh", args...) } } if err != nil { return "", fmt.Errorf("Unable to start editor: %v\n", err) } if err := cmd.Wait(); err != nil { return "", fmt.Errorf("Editing finished with error: %v\n", err) } output, err := ioutil.ReadFile(path) if err != nil { os.Remove(path) return "", fmt.Errorf("Error reading edited file: %v\n", err) } os.Remove(path) return string(output), err }
// Get returns the specified code review. // // If no review request exists, the returned review is nil. func Get(repo repository.Repo, revision string) *Review { requestNotes := repo.GetNotes(request.Ref, revision) requests := request.ParseAllValid(requestNotes) if requests == nil { return nil } review := Review{ Repo: repo, Revision: revision, Request: requests[len(requests)-1], } review.Comments = review.loadComments() review.Resolved = updateThreadsStatus(review.Comments) review.Submitted = repo.IsAncestor(revision, review.Request.TargetRef) currentCommit, err := review.GetHeadCommit() if err == nil { review.Reports = ci.ParseAllValid(repo.GetNotes(ci.Ref, currentCommit)) review.Analyses = analyses.ParseAllValid(repo.GetNotes(analyses.Ref, currentCommit)) } return &review }
// GetCurrent returns the current, open code review. // // If there are multiple matching reviews, then an error is returned. func GetCurrent(repo repository.Repo) (*Review, error) { reviewRef := repo.GetHeadRef() currentCommit := repo.GetCommitHash(reviewRef) var matchingReviews []Review for _, review := range ListOpen(repo) { if review.Request.ReviewRef == reviewRef { matchingReviews = append(matchingReviews, review) } } if matchingReviews == nil { return nil, nil } if len(matchingReviews) != 1 { return nil, fmt.Errorf("There are %d open reviews for the ref \"%s\"", len(matchingReviews), reviewRef) } r := &matchingReviews[0] reports := ci.ParseAllValid(repo.GetNotes(ci.Ref, currentCommit)) r.Reports = reports return r, nil }