File view with blame information shown in the left gutter beside each line.

barerepo / server / internal/thread/anchor.go
159 lines · 4.6kb · master
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
package thread
133728e barerepo 1mo
2
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"context"
133728e barerepo 1mo
5
"fmt"
133728e barerepo 1mo
6
"strconv"
133728e barerepo 1mo
7
"strings"
133728e barerepo 1mo
8
133728e barerepo 1mo
9
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
10
)
133728e barerepo 1mo
11
133728e barerepo 1mo
12
// Anchor is chapter 43's four fields, of which the blob hash recovers what the commenter saw.
133728e barerepo 1mo
13
type Anchor struct {
133728e barerepo 1mo
14
Path string
133728e barerepo 1mo
15
Line int
133728e barerepo 1mo
16
Blob string // the file's blob hash at comment time
133728e barerepo 1mo
17
Side string // old | new
133728e barerepo 1mo
18
}
133728e barerepo 1mo
19
133728e barerepo 1mo
20
// ParseAnchor reads the "config.go:43" form.
133728e barerepo 1mo
21
func ParseAnchor(s string) (Anchor, bool) {
133728e barerepo 1mo
22
path, num, ok := strings.Cut(s, ":")
133728e barerepo 1mo
23
if !ok {
133728e barerepo 1mo
24
return Anchor{}, false
133728e barerepo 1mo
25
}
133728e barerepo 1mo
26
line, err := strconv.Atoi(num)
133728e barerepo 1mo
27
if err != nil || line <= 0 || !gitx.ValidPath(path) {
133728e barerepo 1mo
28
return Anchor{}, false
133728e barerepo 1mo
29
}
133728e barerepo 1mo
30
return Anchor{Path: path, Line: line}, true
133728e barerepo 1mo
31
}
133728e barerepo 1mo
32
133728e barerepo 1mo
33
func (a Anchor) String() string { return fmt.Sprintf("%s:%d", a.Path, a.Line) }
133728e barerepo 1mo
34
133728e barerepo 1mo
35
// Placement is where a comment lands once its file moved on, in chapter 43.3's three outcomes.
133728e barerepo 1mo
36
type Placement string
133728e barerepo 1mo
37
133728e barerepo 1mo
38
const (
133728e barerepo 1mo
39
// Exact means the blob is the one the commenter saw.
133728e barerepo 1mo
40
Exact Placement = "exact"
133728e barerepo 1mo
41
// Moved means the line survived at a different number.
133728e barerepo 1mo
42
Moved Placement = "moved"
133728e barerepo 1mo
43
// Outdated keeps the comment and shows the code it meant, never hidden or deleted. 43.4.
133728e barerepo 1mo
44
Outdated Placement = "outdated"
133728e barerepo 1mo
45
// Lost means the retained revision expired, and 43.5 asks this to degrade, not pretend.
133728e barerepo 1mo
46
Lost Placement = "lost"
133728e barerepo 1mo
47
)
133728e barerepo 1mo
48
133728e barerepo 1mo
49
// Resolved is an anchor after the file has changed.
133728e barerepo 1mo
50
type Resolved struct {
133728e barerepo 1mo
51
Placement Placement
133728e barerepo 1mo
52
Line int // where to show it now
133728e barerepo 1mo
53
Excerpt string // the line as the commenter saw it
133728e barerepo 1mo
54
}
133728e barerepo 1mo
55
133728e barerepo 1mo
56
// Resolve maps an anchor forward from its blob hash, because a bare line number retrieves nothing.
133728e barerepo 1mo
57
func Resolve(ctx context.Context, dir string, a Anchor, currentRef string) Resolved {
133728e barerepo 1mo
58
if a.Blob == "" {
133728e barerepo 1mo
59
return Resolved{Placement: Lost, Line: a.Line}
133728e barerepo 1mo
60
}
133728e barerepo 1mo
61
oldBody, err := gitx.Run(ctx, dir, "cat-file", "blob", a.Blob)
133728e barerepo 1mo
62
if err != nil {
133728e barerepo 1mo
63
return Resolved{Placement: Lost, Line: a.Line}
133728e barerepo 1mo
64
}
133728e barerepo 1mo
65
oldLines := strings.Split(strings.TrimSuffix(oldBody, "\n"), "\n")
133728e barerepo 1mo
66
excerpt := ""
133728e barerepo 1mo
67
if a.Line-1 < len(oldLines) {
133728e barerepo 1mo
68
excerpt = oldLines[a.Line-1]
133728e barerepo 1mo
69
}
133728e barerepo 1mo
70
133728e barerepo 1mo
71
newBlob, err := currentBlob(ctx, dir, currentRef, a.Path)
133728e barerepo 1mo
72
if err != nil {
133728e barerepo 1mo
73
return Resolved{Placement: Outdated, Line: a.Line, Excerpt: excerpt}
133728e barerepo 1mo
74
}
133728e barerepo 1mo
75
if newBlob == a.Blob {
133728e barerepo 1mo
76
return Resolved{Placement: Exact, Line: a.Line, Excerpt: excerpt}
133728e barerepo 1mo
77
}
133728e barerepo 1mo
78
133728e barerepo 1mo
79
line, ok := mapLine(ctx, dir, a.Blob, newBlob, a.Line)
133728e barerepo 1mo
80
switch {
133728e barerepo 1mo
81
case !ok:
133728e barerepo 1mo
82
return Resolved{Placement: Outdated, Line: a.Line, Excerpt: excerpt}
133728e barerepo 1mo
83
case line == a.Line:
133728e barerepo 1mo
84
return Resolved{Placement: Exact, Line: line, Excerpt: excerpt}
133728e barerepo 1mo
85
default:
133728e barerepo 1mo
86
return Resolved{Placement: Moved, Line: line, Excerpt: excerpt}
133728e barerepo 1mo
87
}
133728e barerepo 1mo
88
}
133728e barerepo 1mo
89
133728e barerepo 1mo
90
func currentBlob(ctx context.Context, dir, ref, path string) (string, error) {
133728e barerepo 1mo
91
if !gitx.ValidRev(ref) || !gitx.ValidPath(path) {
133728e barerepo 1mo
92
return "", fmt.Errorf("bad ref or path")
133728e barerepo 1mo
93
}
133728e barerepo 1mo
94
out, err := gitx.Run(ctx, dir, "rev-parse", "--verify", "--quiet", ref+":"+path)
133728e barerepo 1mo
95
return strings.TrimSpace(out), err
133728e barerepo 1mo
96
}
133728e barerepo 1mo
97
133728e barerepo 1mo
98
// mapLine walks a line between blobs through their diff, and reports false once it was touched.
133728e barerepo 1mo
99
func mapLine(ctx context.Context, dir, oldBlob, newBlob string, line int) (int, bool) {
133728e barerepo 1mo
100
patch, err := gitx.Run(ctx, dir, "diff", "--unified=0", "--no-color", oldBlob, newBlob)
133728e barerepo 1mo
101
if err != nil {
133728e barerepo 1mo
102
return 0, false
133728e barerepo 1mo
103
}
133728e barerepo 1mo
104
// A hunk before the line shifts it, and a hunk containing it makes the line outdated.
133728e barerepo 1mo
105
shift := 0
133728e barerepo 1mo
106
for _, h := range strings.Split(patch, "\n") {
133728e barerepo 1mo
107
if !strings.HasPrefix(h, "@@") {
133728e barerepo 1mo
108
continue
133728e barerepo 1mo
109
}
133728e barerepo 1mo
110
oldStart, oldCount, _, newCount, ok := parseHunkHeader(h)
133728e barerepo 1mo
111
if !ok {
133728e barerepo 1mo
112
continue
133728e barerepo 1mo
113
}
133728e barerepo 1mo
114
switch {
133728e barerepo 1mo
115
case oldCount == 0:
133728e barerepo 1mo
116
// git writes @@ -2,0 +3,2 @@ for lines put after old line 2, so line 2 stays put.
133728e barerepo 1mo
117
if oldStart < line {
133728e barerepo 1mo
118
shift += newCount
133728e barerepo 1mo
119
}
133728e barerepo 1mo
120
case oldStart+oldCount <= line:
133728e barerepo 1mo
121
shift += (newCount - oldCount)
133728e barerepo 1mo
122
case oldStart <= line:
133728e barerepo 1mo
123
// The line is inside this hunk's removed range.
133728e barerepo 1mo
124
return 0, false
133728e barerepo 1mo
125
}
133728e barerepo 1mo
126
}
133728e barerepo 1mo
127
return line + shift, true
133728e barerepo 1mo
128
}
133728e barerepo 1mo
129
133728e barerepo 1mo
130
// parseHunkHeader reads "@@ -a,b +c,d @@".
133728e barerepo 1mo
131
func parseHunkHeader(h string) (oldStart, oldCount, newStart, newCount int, ok bool) {
133728e barerepo 1mo
132
body := strings.TrimPrefix(h, "@@ ")
133728e barerepo 1mo
133
if i := strings.Index(body, " @@"); i >= 0 {
133728e barerepo 1mo
134
body = body[:i]
133728e barerepo 1mo
135
}
133728e barerepo 1mo
136
oldPart, newPart, found := strings.Cut(body, " ")
133728e barerepo 1mo
137
if !found {
133728e barerepo 1mo
138
return 0, 0, 0, 0, false
133728e barerepo 1mo
139
}
133728e barerepo 1mo
140
oldStart, oldCount, ok1 := parseRange(strings.TrimPrefix(oldPart, "-"))
133728e barerepo 1mo
141
newStart, newCount, ok2 := parseRange(strings.TrimPrefix(newPart, "+"))
133728e barerepo 1mo
142
return oldStart, oldCount, newStart, newCount, ok1 && ok2
133728e barerepo 1mo
143
}
133728e barerepo 1mo
144
133728e barerepo 1mo
145
func parseRange(s string) (start, count int, ok bool) {
133728e barerepo 1mo
146
startText, countText, found := strings.Cut(s, ",")
133728e barerepo 1mo
147
start, err := strconv.Atoi(startText)
133728e barerepo 1mo
148
if err != nil {
133728e barerepo 1mo
149
return 0, 0, false
133728e barerepo 1mo
150
}
133728e barerepo 1mo
151
count = 1
133728e barerepo 1mo
152
if found {
133728e barerepo 1mo
153
count, err = strconv.Atoi(countText)
133728e barerepo 1mo
154
if err != nil {
133728e barerepo 1mo
155
return 0, 0, false
133728e barerepo 1mo
156
}
133728e barerepo 1mo
157
}
133728e barerepo 1mo
158
return start, count, true
133728e barerepo 1mo
159
}
history · rawbarerepo 0.1.0