Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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 }