Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 41 additions & 24 deletions actions/review.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,13 @@ func Review(c *cli.Context) error {
Transport: &authTransport{Token: token},
})

if err := checkCleanWorktree(ctx, gitHubRepo); err != nil {
isClean, err := isCleanWorktree(ctx, gitHubRepo)
if err != nil {
return err
}
if !isClean {
return errors.Errorf("index is not clean")
}

// Validate reviewer usernames.
reviewers := c.StringSlice("reviewer")
Expand Down Expand Up @@ -135,7 +139,7 @@ func Review(c *cli.Context) error {
return nil
}

func checkCleanWorktree(ctx context.Context, gitHubRepo *gitHubRepo) error {
func isCleanWorktree(ctx context.Context, gitHubRepo *gitHubRepo) (bool, error) {
// Worktree.Status() is very slow so fall back to the command line instead.
// https://github.com/go-git/go-git/issues/181
cmd := exec.Command("git", "status", "--porcelain")
Expand All @@ -145,25 +149,21 @@ func checkCleanWorktree(ctx context.Context, gitHubRepo *gitHubRepo) error {
if err != nil {
var exitErr exec.ExitError
if errors.As(err, &exitErr) {
return errors.Errorf("git status failed: %s", exitErr.Stderr)
return false, errors.Errorf("git status failed: %s", exitErr.Stderr)
}
return errors.WithStack(err)
}
if stdout.Len() > 0 {
return errors.Errorf("index is not clean")
return false, errors.WithStack(err)
}
return nil
return stdout.Len() == 0, nil
}

func getReviewInfo(
// getCommitRange determine the common ancestor between headHash and the default
// branch, and returns the base and head commits.
func getCommitRange(
ctx context.Context,
gitHubRepo *gitHubRepo,
graphqlClient *graphql.Client,
headHash plumbing.Hash,
) ([]*reviewInfo, error) {
) ([]*object.Commit, error) {
deps := deps.FromContext(ctx)

// Determine the common ancestor between the current branch and master
repo := gitHubRepo.GitRepo()
defaultBranchCommit, err := repo.CommitObject(
gitHubRepo.DefaultBranchRef().Hash(),
Expand All @@ -187,11 +187,36 @@ func getReviewInfo(
return nil, errors.New("no new commits")
}
deps.DebugLog.Println("found base commit at", baseCommit.Hash)
var result []*object.Commit
commit := headCommit
for commit.Hash != baseCommit.Hash {
result = append(result, commit)
if len(commit.ParentHashes) != 1 {
return nil, errors.Errorf("commit %v has multiple hashes", commit.Hash)
}
commit, err = repo.CommitObject(commit.ParentHashes[0])
if err != nil {
return nil, errors.WithStack(err)
}
}
return result, nil
}

func getReviewInfo(
ctx context.Context,
gitHubRepo *gitHubRepo,
graphqlClient *graphql.Client,
headHash plumbing.Hash,
) ([]*reviewInfo, error) {
deps := deps.FromContext(ctx)
commitRange, err := getCommitRange(ctx, gitHubRepo, headHash)
if err != nil {
return nil, err
}

// Collect sequence of commits of interest
commit := headCommit
var ris []*reviewInfo
for commit.Hash != baseCommit.Hash {
for _, commit := range commitRange {
ri, err := makeReviewInfo(ctx, gitHubRepo, graphqlClient, commit)
if err != nil {
return nil, err
Expand All @@ -203,14 +228,6 @@ func getReviewInfo(
status = "reviewID: " + ri.reviewID + " pr: " + ri.pr.GetHTMLURL()
}
deps.DebugLog.Println("examined", commit.Hash, status)

if len(commit.ParentHashes) != 1 {
return nil, errors.Errorf("commit %v has multiple hashes", commit.Hash)
}
commit, err = repo.CommitObject(commit.ParentHashes[0])
if err != nil {
return nil, errors.WithStack(err)
}
}
// Reverse the array so the tip commit is at the end
for i, j := 0, len(ris)-1; i < j; i, j = i+1, j-1 {
Expand Down Expand Up @@ -449,7 +466,7 @@ func printReviewInfo(ctx context.Context, ris []*reviewInfo) {
if ri.updatedCommit != nil {
commit = ri.updatedCommit
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", commit.Hash.String()[:8], title, reviewURL, status)
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", commit.Hash.String()[:8], title, status, reviewURL)
}
w.Flush()
}
Expand Down
115 changes: 115 additions & 0 deletions actions/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package actions

import (
"fmt"
"io"
"net/http"
"strings"
"text/tabwriter"

"github.com/bitcomplete/plz-cli/client/deps"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/pkg/errors"
"github.com/shurcooL/graphql"
"github.com/urfave/cli/v2"
)

type statusReview struct {
ID string `graphql:"id"`
RevisionList struct {
Revisions []struct {
HeadCommitSHA string `graphql:"headCommitSha"`
} `graphql:"revisions"`
} `graphql:"revisionList"`
}

func Status(c *cli.Context) error {
ctx := c.Context
deps := deps.FromContext(ctx)

token, err := deps.Auth.Token()
if err != nil {
return err
}
gitHubRepo, err := newGitHubRepo(ctx, token)
if err != nil {
return err
}
graphqlClient := graphql.NewClient(deps.PlzAPIBaseURL+"/api/v1", &http.Client{
Transport: &authTransport{Token: token},
})

headRef, err := gitHubRepo.GitRepo().Head()
if err != nil {
return errors.WithStack(err)
}
deps.DebugLog.Println("HEAD is at", headRef.Hash())

commitRange, err := getCommitRange(ctx, gitHubRepo, headRef.Hash())
if err != nil {
return err
}
if len(commitRange) == 0 {
return nil
}

isClean, err := isCleanWorktree(ctx, gitHubRepo)
if err != nil {
return err
}
if !isClean {
deps.InfoLog.Println("unstaged changes")
}

w := tabwriter.NewWriter(deps.InfoLog.Writer(), 0, 0, 1, ' ', 0)
for _, commit := range commitRange {
var review *statusReview
if reviewID := readReviewIDFromCommitMessage(commit.Message); reviewID != "" {
deps.DebugLog.Println("loading review", reviewID)
var query struct {
Review statusReview `graphql:"review(id: $reviewID)"`
}
err := graphqlClient.Query(ctx, &query, map[string]interface{}{
"reviewID": graphql.ID(reviewID),
})
if err != nil {
return errors.WithStack(err)
}
review = &query.Review
}
printReviewStatus(w, commit, review)
}
w.Flush()
return nil
}

func printReviewStatus(w io.Writer, commit *object.Commit, review *statusReview) {
status := "new"
if review != nil {
revisionIndex := -1
for i, revision := range review.RevisionList.Revisions {
if revision.HeadCommitSHA == commit.Hash.String() {
revisionIndex = i
break
}
}
switch revisionIndex {
case -1:
status = "modified"
case 0:
status = "current"
default:
status = "outdated"
}
}
parts := strings.SplitN(commit.Message, "\n", 2)
title := strings.TrimSpace(parts[0])
if len(title) > 47 {
title = title[:47] + "..."
}
reviewURL := ""
if review != nil {
reviewURL = "https://plz.review/review/" + review.ID
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", commit.Hash.String()[:8], title, status, reviewURL)
}
8 changes: 7 additions & 1 deletion actions/switch.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,15 @@ func Switch(c *cli.Context) error {
if err != nil {
return err
}
if err := checkCleanWorktree(ctx, gitHubRepo); err != nil {

isClean, err := isCleanWorktree(ctx, gitHubRepo)
if err != nil {
return err
}
if !isClean {
return errors.Errorf("index is not clean")
}

repo := gitHubRepo.GitRepo()
headRef, err := repo.Head()
if err != nil {
Expand Down
6 changes: 5 additions & 1 deletion actions/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,13 @@ func Sync(c *cli.Context) error {
Transport: &authTransport{Token: token},
})

if err := checkCleanWorktree(ctx, gitHubRepo); err != nil {
isClean, err := isCleanWorktree(ctx, gitHubRepo)
if err != nil {
return err
}
if !isClean {
return errors.Errorf("index is not clean")
}

headRef, err := gitHubRepo.GitRepo().Head()
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions cmd/plz/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ func main() {
Usage: "update local review branches",
Action: actions.Sync,
},
{
Name: "status",
Usage: "list local review status",
Action: actions.Status,
},
},
Flags: []cli.Flag{
&cli.BoolFlag{
Expand Down