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