// GetFirstCommit returns the first commit that is included in the review func (review differentialReview) GetFirstCommit(repo repository.Repo) *repository.Revision { var commits []string for _, hashPair := range review.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 { details, err := repo.GetDetails(repository.Revision(commit)) if err == nil { timestamp, err := strconv.Atoi(details.Time) if err == 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 nil } sort.Ints(commitTimestamps) revision := repository.Revision(commitsByTimestamp[commitTimestamps[0]]) return &revision }
// 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 repository.Revision) ([]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.GetRawDiff(from, to) 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) }
func (arc Arcanist) mirrorCommentsIntoReview(repo repository.Repo, review differentialReview, comments comment.CommentMap) { existingComments := review.LoadComments() newComments := comments.FilterOverlapping(existingComments) var lastCommitForLastDiff string var latestDiffForReview int commitToDiffMap := make(map[string]string) for _, diffIDString := range review.Diffs { lastCommit := findCommitForDiff(diffIDString) commitToDiffMap[lastCommit] = diffIDString diffID, err := strconv.Atoi(diffIDString) if err == nil && diffID > latestDiffForReview { lastCommitForLastDiff = lastCommit latestDiffForReview = diffID } } report := ci.GetLatestCIReport(repo.GetNotes(ci.Ref, repository.Revision(lastCommitForLastDiff))) log.Printf("The latest CI report for diff %d is %+v ", latestDiffForReview, report) if report.URL != "" { unitDiffProperty := differentialUnitDiffProperty{ Name: report.Agent, Link: report.URL, Result: translateReportStatusToDifferentialUnitResult(report.Status), } // Note that although the unit tests property is a JSON object, Phabricator // expects there to be a list of such objects for any given diff. Therefore // we wrap the object in a list before marshaling it to send to the server. // TODO(ojarjur): We should take advantage of the fact that this is a list, // and include the latest CI report for each agent. That would allow us to // display results from multiple test runners in a code review. propertyBytes, err := json.Marshal([]differentialUnitDiffProperty{unitDiffProperty}) if err == nil { err = arc.setDiffProperty(latestDiffForReview, unitDiffPropertyName, string(propertyBytes)) } if err != nil { log.Fatal(err.Error()) } } inlineRequests, commentRequests := review.buildCommentRequests(newComments, commitToDiffMap) for _, request := range inlineRequests { var response createInlineResponse runArcCommandOrDie("differential.createinline", request, &response) if response.Error != "" { log.Println(response.ErrorMessage) } } for _, request := range commentRequests { var response createCommentResponse runArcCommandOrDie("differential.createcomment", request, &response) if response.Error != "" { log.Println(response.ErrorMessage) } } }
// 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 (arc Arcanist) mirrorCommentsIntoReview(repo repository.Repo, review differentialReview, comments comment.CommentMap) { existingComments := review.LoadComments() newComments := comments.FilterOverlapping(existingComments) var lastCommitForLastDiff string var latestDiffForReview string commitToDiffMap := make(map[string]string) for _, diffIDString := range review.Diffs { lastCommit := findCommitForDiff(diffIDString) commitToDiffMap[lastCommit] = diffIDString if diffIDString > latestDiffForReview { lastCommitForLastDiff = lastCommit latestDiffForReview = diffIDString } } report := ci.GetLatestCIReport(repo.GetNotes(ci.Ref, repository.Revision(lastCommitForLastDiff))) log.Printf("The latest CI report for diff %s is %+v ", latestDiffForReview, report) if report.URL != "" { updateUnitResultsRequest := differentialUpdateUnitResultsRequest{ DiffID: latestDiffForReview, Result: report.Status, Link: report.URL, //TODO(ckerur): Link does not work for some reason. Remove putting URL in Message once it does Message: report.URL, } var unitResultsResponse differentialUpdateUnitResultsResponse runArcCommandOrDie("differential.updateunitresults", updateUnitResultsRequest, &unitResultsResponse) if unitResultsResponse.Error != "" { log.Fatal(unitResultsResponse.ErrorMessage) } } inlineRequests, commentRequests := review.buildCommentRequests(newComments, commitToDiffMap) for _, request := range inlineRequests { var response createInlineResponse runArcCommandOrDie("differential.createinline", request, &response) if response.Error != "" { log.Println(response.ErrorMessage) } } for _, request := range commentRequests { var response createCommentResponse runArcCommandOrDie("differential.createcomment", request, &response) if response.Error != "" { log.Println(response.ErrorMessage) } } }
// 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, review differentialReview, headCommit string, req request.Request, comments map[string]comment.Comment) { if review.isClosed() { return } headRevision := repository.Revision(headCommit) mergeBase, err := repo.GetMergeBase(repository.Revision(req.TargetRef), headRevision) if err != nil { // This can happen if the target ref has been deleted while we were performing the updates. return } for _, hashPair := range review.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, review, comments) return } } diff, err := arc.createDifferentialDiff(repo, mergeBase, headRevision, req, review.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: review.ID, DiffID: strconv.Itoa(diff.ID)} var updateResponse differentialUpdateRevisionResponse runArcCommandOrDie("differential.updaterevision", updateRequest, &updateResponse) if updateResponse.Error != "" { log.Fatal(updateResponse.ErrorMessage) } }
func mirrorRepoToReview(repo repository.Repo, tool review.Tool, syncToRemote bool) { if syncToRemote { if err := repo.PullUpdates(); err != nil { log.Printf("Failed to pull updates for the repo %v: %v\n", repo, err) return } } stateHash := repo.GetRepoStateHash() if processedStates[repo.GetPath()] != stateHash { log.Print("Mirroring repo: ", repo) for _, revision := range repo.ListNotedRevisions(request.Ref) { existingComments[revision] = comment.ParseAllValid(repo.GetNotes(comment.Ref, revision)) for _, req := range request.ParseAllValid(repo.GetNotes(request.Ref, revision)) { tool.EnsureRequestExists(repo, revision, req, existingComments[revision]) } } openReviews[repo.GetPath()] = tool.ListOpenReviews(repo) processedStates[repo.GetPath()] = stateHash tool.Refresh(repo) } for _, review := range openReviews[repo.GetPath()] { if reviewCommit := review.GetFirstCommit(repo); reviewCommit != nil { log.Println("Processing review: ", *reviewCommit) revisionComments := getExistingComments(*reviewCommit) log.Printf("Loaded %d comments for %v\n", len(revisionComments), *reviewCommit) for _, c := range review.LoadComments() { if !hasOverlap(c, revisionComments) { // The comment is new. note, err := c.Write() if err != nil { log.Fatal(err) } log.Printf("Appending a comment: %s", string(note)) repo.AppendNote(comment.Ref, *reviewCommit, note, c.Author) } else { log.Printf("Skipping '%v', as it has already been written\n", c) } } } } if syncToRemote { if err := repo.PushUpdates(); err != nil { log.Printf("Failed to push updates to the repo %v: %v\n", repo, err) } } }
// EnsureRequestExists runs the "arcanist" command-line tool to create a Differential diff for the given request, if one does not already exist. func (arc Arcanist) EnsureRequestExists(repo repository.Repo, revision repository.Revision, req request.Request, comments map[string]comment.Comment) { // If this revision has been previously closed shortcut all processing if closedRevisionsMap[revision] { return } mergeBase, err := repo.GetMergeBase(repository.Revision(req.TargetRef), revision) if err != nil { // There are lots of reasons that we might not be able to compute a merge base, // (e.g. the revision already being merged in, or being dropped and garbage collected), // but they all indicate that the review request is no longer valid. log.Printf("Ignoring review request '%v', because we could not compute a merge base", req) return } existingReviews := arc.listDifferentialReviewsOrDie(req.ReviewRef, revision) if mergeBase == revision { // The change has already been merged in, so we should simply close any open reviews. for _, review := range existingReviews { if !review.isClosed() { review.close() } } closedRevisionsMap[revision] = true return } headDetails, err := repo.GetDetails(repository.Revision(req.ReviewRef)) if err != nil { // The given review ref has been deleted (or never existed), but the change wasn't merged. // TODO(ojarjur): We should mark the existing reviews as abandoned. log.Printf("Ignoring review because the review ref '%s' does not exist", req.ReviewRef) return } if len(existingReviews) > 0 { // The change is still pending, but we already have existing reviews, so we should just update those. for _, review := range existingReviews { arc.updateReviewDiffs(repo, review, headDetails.Commit, req, comments) } return } diff, err := arc.createDifferentialDiff(repo, mergeBase, revision, req, []string{}) if err != nil { log.Fatal(err) } if diff == nil { // The revision is already merged in, ignore it. return } rev, err := arc.createDifferentialRevision(repo, revision, diff.ID, req) if err != nil { log.Fatal(err) } log.Printf("Created diff %v and revision %v for the review of %s", diff, rev, revision) // If the review already contains multiple commits by the time we mirror it, then // we need to ensure that at least the first and last ones are added. existingReviews = arc.listDifferentialReviewsOrDie(req.ReviewRef, revision) for _, review := range existingReviews { arc.updateReviewDiffs(repo, review, headDetails.Commit, req, comments) } }
// createDifferentialDiff generates a Phabricator resource that represents a diff between two revisions. // // The generated resource includes metadata about how the diff was generated, and a JSON representation // of the changes from the diff, as parsed by Phabricator. func (arc Arcanist) createDifferentialDiff(repo repository.Repo, mergeBase, revision repository.Revision, req request.Request, priorDiffs []string) (*differentialDiff, error) { revisionDetails, err := repo.GetDetails(revision) if err != nil { return nil, err } changes, err := arc.getDiffChanges(repo, mergeBase, revision) if err != nil { return nil, err } createRequest := differentialCreateDiffRequest{ Branch: abbreviateRefName(req.ReviewRef), SourceControlSystem: "git", SourceControlBaseRevision: string(mergeBase), SourcePath: repo.GetPath(), LintStatus: "4", // Status code 5 means "linter skipped" UnitStatus: "4", // Status code 5 means "unit tests have been skipped" Changes: changes, } var createResponse differentialCreateDiffResponse runArcCommandOrDie("differential.creatediff", createRequest, &createResponse) if createResponse.Error != "" { return nil, fmt.Errorf(createResponse.ErrorMessage) } localCommits := make(map[string]interface{}) for _, priorDiff := range priorDiffs { diffID, err := strconv.Atoi(priorDiff) if err != nil { return nil, err } queryRequest := differentialQueryDiffsRequest{[]int{diffID}} var queryResponse differentialQueryDiffsResponse runArcCommandOrDie("differential.querydiffs", queryRequest, &queryResponse) if queryResponse.Error != "" { return nil, fmt.Errorf(queryResponse.ErrorMessage) } priorProperty := queryResponse.Response[priorDiff].Properties if priorPropertyMap, ok := priorProperty.(map[string]interface{}); ok { if localCommitsProperty, ok := priorPropertyMap["local:commits"]; ok { if priorLocalCommits, ok := localCommitsProperty.(map[string]interface{}); ok { for id, val := range priorLocalCommits { localCommits[id] = val } } } } } localCommits[string(revision)] = *revisionDetails localCommitsProperty, err := json.Marshal(localCommits) if err != nil { return nil, err } if err := arc.setDiffProperty(createResponse.Response.ID, "local:commits", string(localCommitsProperty)); err != nil { return nil, err } if err := arc.setDiffProperty(createResponse.Response.ID, "arc:unit", "{}"); err != nil { return nil, err } return &createResponse.Response, nil }