// 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)
	}
}
Exemple #3
0
// 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
}
Exemple #7
0
// 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
}
Exemple #8
0
// 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
}
Exemple #9
0
// 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)
}
Exemple #10
0
// 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
}
Exemple #11
0
// 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)
}
Exemple #12
0
// 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
}
Exemple #15
0
// 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
}
Exemple #17
0
// 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
}
Exemple #18
0
// 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
}
Exemple #19
0
// 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
}
Exemple #20
0
// 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
}
Exemple #21
0
// 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()
}
Exemple #22
0
// 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
}
Exemple #23
0
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]
	}
}
Exemple #24
0
// 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)
}
Exemple #25
0
// 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
}
Exemple #26
0
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
}
Exemple #28
0
// 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
}
Exemple #29
0
// 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
}
Exemple #30
0
// 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
}