vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
refactor
c5902451905acdaa478f8476844ccd4a510d9ca9 · Keenan Nemetz · 17d ago · parent ec2aba4b
client/review.go +133
| @@ -0,0 +1,133 @@ | |||
| 1 | package client | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | "strings" | ||
| 6 | |||
| 7 | "github.com/rodent-software/vole/internal/format" | ||
| 8 | "github.com/rodent-software/vole/metadata" | ||
| 9 | ) | ||
| 10 | |||
| 11 | // ResolveHead turns a branch, tag or commit into the full id an approval will | ||
| 12 | // name. Ambiguous short ids are refused rather than guessed. | ||
| 13 | func ResolveHead(b *Branch, rev string) (string, error) { | ||
| 14 | if rev == "" { | ||
| 15 | rev = "HEAD" | ||
| 16 | } | ||
| 17 | out, err := b.Repo.RevParseFull(rev) | ||
| 18 | if err != nil || out == "" { | ||
| 19 | return "", fmt.Errorf("%q does not name a commit", rev) | ||
| 20 | } | ||
| 21 | return out, nil | ||
| 22 | } | ||
| 23 | |||
| 24 | // ResolveRequestCommentID resolves a typed comment id against one request's | ||
| 25 | // thread, the way issue and comment ids are resolved: a prefix is enough, and an | ||
| 26 | // ambiguous one names its matches instead of guessing. | ||
| 27 | func ResolveRequestCommentID(b *Branch, requestID, given string) (string, error) { | ||
| 28 | given = strings.ToUpper(strings.TrimSpace(given)) | ||
| 29 | if given == "" { | ||
| 30 | return "", fmt.Errorf("a comment id is required") | ||
| 31 | } | ||
| 32 | r, err := metadata.FindRequest(b.Repo, requestID) | ||
| 33 | if err != nil { | ||
| 34 | return "", err | ||
| 35 | } | ||
| 36 | var matches []string | ||
| 37 | for _, cm := range r.Comments { | ||
| 38 | if cm.ID == given { | ||
| 39 | return cm.ID, nil | ||
| 40 | } | ||
| 41 | if strings.HasPrefix(cm.ID, given) { | ||
| 42 | matches = append(matches, cm.ID) | ||
| 43 | } | ||
| 44 | } | ||
| 45 | switch len(matches) { | ||
| 46 | case 1: | ||
| 47 | return matches[0], nil | ||
| 48 | case 0: | ||
| 49 | return "", fmt.Errorf("no comment matching %q on %s", given, requestID) | ||
| 50 | default: | ||
| 51 | return "", fmt.Errorf("%q matches %d comments: %s", given, len(matches), strings.Join(matches, ", ")) | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | // MergeLine answers the question the review page answers, from the clone: does | ||
| 56 | // this head still merge into its base? It says nothing it cannot check, and says | ||
| 57 | // so when the head has not been fetched — a reviewer who has not run `git fetch` | ||
| 58 | // should learn that rather than read an unchecked branch as clean. | ||
| 59 | func MergeLine(b *Branch, r *metadata.Request) string { | ||
| 60 | if !r.Open() || r.Head == "" { | ||
| 61 | return "" | ||
| 62 | } | ||
| 63 | if head, _ := b.Repo.RevParse(r.Head); head == "" { | ||
| 64 | return fmt.Sprintf("cannot check: %s is not in this clone (git fetch %s)", format.ID(r.Head), b.Remote) | ||
| 65 | } | ||
| 66 | base := "" | ||
| 67 | for _, ref := range []string{"refs/remotes/" + b.Remote + "/" + r.Base, "refs/heads/" + r.Base} { | ||
| 68 | if sha, _ := b.Repo.RevParse(ref); sha != "" { | ||
| 69 | base = sha | ||
| 70 | break | ||
| 71 | } | ||
| 72 | } | ||
| 73 | if base == "" { | ||
| 74 | return "" | ||
| 75 | } | ||
| 76 | paths, err := b.Repo.MergeConflicts(base, r.Head) | ||
| 77 | if err != nil { | ||
| 78 | return "" | ||
| 79 | } | ||
| 80 | if len(paths) > 0 { | ||
| 81 | return fmt.Sprintf("CONFLICT with %s in %s — rebase before landing", r.Base, strings.Join(paths, ", ")) | ||
| 82 | } | ||
| 83 | return fmt.Sprintf("clean against %s", r.Base) | ||
| 84 | } | ||
| 85 | |||
| 86 | // AnchorFor computes the anchor a review comment will carry: the blob of the file | ||
| 87 | // on the side being reviewed, and a check that the line exists in it. It requires | ||
| 88 | // the commits in the clone — the reviewer was looking at them — and says so rather | ||
| 89 | // than writing an anchor it cannot compute. | ||
| 90 | func AnchorFor(b *Branch, r *metadata.Request, path string, line int, side string) (*metadata.Anchor, error) { | ||
| 91 | if side != "new" && side != "old" { | ||
| 92 | return nil, fmt.Errorf("--side must be new (the head's file) or old (what it changed from)") | ||
| 93 | } | ||
| 94 | head, _ := b.Repo.RevParse(r.Head) | ||
| 95 | if head == "" { | ||
| 96 | return nil, fmt.Errorf("%s is not in this clone; git fetch %s before commenting on a line", | ||
| 97 | format.ID(r.Head), b.Remote) | ||
| 98 | } | ||
| 99 | commit := head | ||
| 100 | if side == "old" { | ||
| 101 | base := "" | ||
| 102 | for _, ref := range []string{"refs/remotes/" + b.Remote + "/" + r.Base, "refs/heads/" + r.Base} { | ||
| 103 | if sha, _ := b.Repo.RevParse(ref); sha != "" { | ||
| 104 | base = sha | ||
| 105 | break | ||
| 106 | } | ||
| 107 | } | ||
| 108 | if base == "" { | ||
| 109 | return nil, fmt.Errorf("cannot find %s in this clone; it is the old side of the review (git fetch %s)", r.Base, b.Remote) | ||
| 110 | } | ||
| 111 | mb, err := b.Repo.MergeBase(base, head) | ||
| 112 | if err != nil || mb == "" { | ||
| 113 | return nil, fmt.Errorf("cannot find where %s diverged from %s", r.Base, format.ID(head)) | ||
| 114 | } | ||
| 115 | commit = mb | ||
| 116 | } | ||
| 117 | blob, err := b.Repo.RevParseAny(commit + ":" + path) | ||
| 118 | if err != nil || blob == "" { | ||
| 119 | return nil, fmt.Errorf("%s does not exist on the %s side (%s)", path, side, format.ID(commit)) | ||
| 120 | } | ||
| 121 | content, err := b.Repo.BlobBySHA(blob) | ||
| 122 | if err != nil { | ||
| 123 | return nil, err | ||
| 124 | } | ||
| 125 | lines := strings.Count(string(content), "\n") | ||
| 126 | if !strings.HasSuffix(string(content), "\n") && len(content) > 0 { | ||
| 127 | lines++ | ||
| 128 | } | ||
| 129 | if line > lines { | ||
| 130 | return nil, fmt.Errorf("%s has %d line(s) on the %s side; --line %d is past the end", path, lines, side, line) | ||
| 131 | } | ||
| 132 | return &metadata.Anchor{Path: path, Side: side, Line: line, Blob: blob}, nil | ||
| 133 | } | ||