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

barerepo / server / internal/gitread/file.go
174 lines · 4.3kb · master
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
package gitread
133728e barerepo 1mo
2
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"context"
133728e barerepo 1mo
5
"strconv"
133728e barerepo 1mo
6
"strings"
133728e barerepo 1mo
7
"time"
133728e barerepo 1mo
8
133728e barerepo 1mo
9
"github.com/barerepo/server/internal/cache"
133728e barerepo 1mo
10
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
11
)
133728e barerepo 1mo
12
133728e barerepo 1mo
13
// File carries blame already, because "what is this line" and "why" are one question. 24.
133728e barerepo 1mo
14
type File struct {
133728e barerepo 1mo
15
Path string
133728e barerepo 1mo
16
Blob string
133728e barerepo 1mo
17
Size int64
133728e barerepo 1mo
18
Binary bool
133728e barerepo 1mo
19
// TooBig is chapter 42.4's cap, rather than sending a browser what it will choke on.
133728e barerepo 1mo
20
TooBig bool
133728e barerepo 1mo
21
Lines []FileLine
133728e barerepo 1mo
22
}
133728e barerepo 1mo
23
133728e barerepo 1mo
24
type FileLine struct {
133728e barerepo 1mo
25
Number int
133728e barerepo 1mo
26
Text string
133728e barerepo 1mo
27
SHA string // the commit that last touched this line
133728e barerepo 1mo
28
Short string
133728e barerepo 1mo
29
Author string
133728e barerepo 1mo
30
When time.Time
133728e barerepo 1mo
31
}
133728e barerepo 1mo
32
133728e barerepo 1mo
33
// renderCap is the largest file that is rendered whole. Chapter 42.4.
133728e barerepo 1mo
34
const renderCap = 1 << 20
133728e barerepo 1mo
35
133728e barerepo 1mo
36
// binarySniff is how many bytes are checked for a null byte. Chapter 42.4.
133728e barerepo 1mo
37
const binarySniff = 8000
133728e barerepo 1mo
38
133728e barerepo 1mo
39
// Open reads a file at a revision.
133728e barerepo 1mo
40
func Open(ctx context.Context, dir, rev, path string) (*File, error) {
133728e barerepo 1mo
41
if !gitx.ValidRev(rev) || !gitx.ValidPath(path) {
133728e barerepo 1mo
42
return nil, nil
133728e barerepo 1mo
43
}
133728e barerepo 1mo
44
// One process for hash, size and content, because three is what chapter 25 cannot afford.
133728e barerepo 1mo
45
obj, err := gitx.CatFile(ctx, dir, rev+":"+path)
133728e barerepo 1mo
46
if err != nil || obj.Type != "blob" {
133728e barerepo 1mo
47
return nil, nil // no such file at that revision
133728e barerepo 1mo
48
}
133728e barerepo 1mo
49
f := &File{Path: path, Size: obj.Size, Blob: obj.SHA}
133728e barerepo 1mo
50
body := obj.Body
133728e barerepo 1mo
51
head := body
133728e barerepo 1mo
52
if len(head) > binarySniff {
133728e barerepo 1mo
53
head = head[:binarySniff]
133728e barerepo 1mo
54
}
133728e barerepo 1mo
55
if strings.IndexByte(head, 0) >= 0 {
133728e barerepo 1mo
56
f.Binary = true
133728e barerepo 1mo
57
return f, nil
133728e barerepo 1mo
58
}
133728e barerepo 1mo
59
if obj.Size > renderCap {
133728e barerepo 1mo
60
f.TooBig = true
133728e barerepo 1mo
61
return f, nil
133728e barerepo 1mo
62
}
133728e barerepo 1mo
63
133728e barerepo 1mo
64
text := strings.Split(strings.TrimSuffix(body, "\n"), "\n")
133728e barerepo 1mo
65
blame, err := blameAt(ctx, dir, rev, path)
133728e barerepo 1mo
66
if err != nil {
133728e barerepo 1mo
67
blame = nil // a file with no history still renders
133728e barerepo 1mo
68
}
133728e barerepo 1mo
69
f.Lines = make([]FileLine, len(text))
133728e barerepo 1mo
70
for i, line := range text {
133728e barerepo 1mo
71
f.Lines[i] = FileLine{Number: i + 1, Text: line}
133728e barerepo 1mo
72
if i < len(blame) {
133728e barerepo 1mo
73
b := blame[i]
133728e barerepo 1mo
74
f.Lines[i].SHA = b.SHA
133728e barerepo 1mo
75
f.Lines[i].Short = b.Short
133728e barerepo 1mo
76
f.Lines[i].Author = b.Author
133728e barerepo 1mo
77
f.Lines[i].When = b.When
133728e barerepo 1mo
78
}
133728e barerepo 1mo
79
}
133728e barerepo 1mo
80
return f, nil
133728e barerepo 1mo
81
}
133728e barerepo 1mo
82
133728e barerepo 1mo
83
// BlameLine is who last touched one line.
133728e barerepo 1mo
84
type BlameLine struct {
133728e barerepo 1mo
85
SHA string
133728e barerepo 1mo
86
Short string
133728e barerepo 1mo
87
Author string
133728e barerepo 1mo
88
When time.Time
133728e barerepo 1mo
89
}
133728e barerepo 1mo
90
133728e barerepo 1mo
91
// Cache holds answers that cannot change. Nil means compute every time.
133728e barerepo 1mo
92
var Cache *cache.Disk
133728e barerepo 1mo
93
133728e barerepo 1mo
94
// Blame reads porcelain output, cached by commit and path, because that is the question it answers.
133728e barerepo 1mo
95
func Blame(ctx context.Context, dir, rev, path string) ([]BlameLine, error) {
133728e barerepo 1mo
96
if !gitx.ValidRev(rev) || !gitx.ValidPath(path) {
133728e barerepo 1mo
97
return nil, nil
133728e barerepo 1mo
98
}
133728e barerepo 1mo
99
return blameAt(ctx, dir, rev, path)
133728e barerepo 1mo
100
}
133728e barerepo 1mo
101
133728e barerepo 1mo
102
// blameAt keys on commit and path, not blob: two paths can share a blob and a revert reuses one, and either serves the wrong author and commit on every line.
133728e barerepo 1mo
103
func blameAt(ctx context.Context, dir, rev, path string) ([]BlameLine, error) {
133728e barerepo 1mo
104
key := ""
133728e barerepo 1mo
105
if sha := revObject(dir, rev); sha != "" {
133728e barerepo 1mo
106
key = sha + ":" + path
133728e barerepo 1mo
107
}
133728e barerepo 1mo
108
if key != "" {
133728e barerepo 1mo
109
if body, ok := Cache.Get("blame", key); ok {
133728e barerepo 1mo
110
return parseBlame(string(body)), nil
133728e barerepo 1mo
111
}
133728e barerepo 1mo
112
}
133728e barerepo 1mo
113
out, err := gitx.Run(ctx, dir, "blame", "--porcelain", rev, "--", path)
133728e barerepo 1mo
114
if err != nil {
133728e barerepo 1mo
115
return nil, err
133728e barerepo 1mo
116
}
133728e barerepo 1mo
117
if key != "" {
133728e barerepo 1mo
118
Cache.Put("blame", key, []byte(out))
133728e barerepo 1mo
119
}
133728e barerepo 1mo
120
return parseBlame(out), nil
133728e barerepo 1mo
121
}
133728e barerepo 1mo
122
133728e barerepo 1mo
123
// parseBlame remembers each commit's details as they pass, since the format states them once.
133728e barerepo 1mo
124
func parseBlame(out string) []BlameLine {
133728e barerepo 1mo
125
type meta struct {
133728e barerepo 1mo
126
author string
133728e barerepo 1mo
127
when time.Time
133728e barerepo 1mo
128
}
133728e barerepo 1mo
129
seen := map[string]meta{}
133728e barerepo 1mo
130
133728e barerepo 1mo
131
var lines []BlameLine
133728e barerepo 1mo
132
var cur BlameLine
133728e barerepo 1mo
133
var curMeta meta
133728e barerepo 1mo
134
inEntry := false
133728e barerepo 1mo
135
133728e barerepo 1mo
136
for _, line := range strings.Split(out, "\n") {
133728e barerepo 1mo
137
switch {
133728e barerepo 1mo
138
case strings.HasPrefix(line, "\t"):
133728e barerepo 1mo
139
// The content line ends an entry.
133728e barerepo 1mo
140
if inEntry {
133728e barerepo 1mo
141
if curMeta.author == "" {
133728e barerepo 1mo
142
curMeta = seen[cur.SHA]
133728e barerepo 1mo
143
} else {
133728e barerepo 1mo
144
seen[cur.SHA] = curMeta
133728e barerepo 1mo
145
}
133728e barerepo 1mo
146
cur.Author = curMeta.author
133728e barerepo 1mo
147
cur.When = curMeta.when
133728e barerepo 1mo
148
lines = append(lines, cur)
133728e barerepo 1mo
149
inEntry = false
133728e barerepo 1mo
150
curMeta = meta{}
133728e barerepo 1mo
151
}
133728e barerepo 1mo
152
case strings.HasPrefix(line, "author "):
133728e barerepo 1mo
153
curMeta.author = strings.TrimPrefix(line, "author ")
133728e barerepo 1mo
154
case strings.HasPrefix(line, "author-time "):
133728e barerepo 1mo
155
if secs, err := strconv.ParseInt(strings.TrimPrefix(line, "author-time "), 10, 64); err == nil {
133728e barerepo 1mo
156
curMeta.when = time.Unix(secs, 0)
133728e barerepo 1mo
157
}
133728e barerepo 1mo
158
case len(line) >= 40 && isHex(line[:40]):
133728e barerepo 1mo
159
cur = BlameLine{SHA: line[:40], Short: line[:7]}
133728e barerepo 1mo
160
inEntry = true
133728e barerepo 1mo
161
}
133728e barerepo 1mo
162
}
133728e barerepo 1mo
163
return lines
133728e barerepo 1mo
164
}
133728e barerepo 1mo
165
133728e barerepo 1mo
166
func isHex(s string) bool {
133728e barerepo 1mo
167
for i := 0; i < len(s); i++ {
133728e barerepo 1mo
168
c := s[i]
133728e barerepo 1mo
169
if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') {
133728e barerepo 1mo
170
return false
133728e barerepo 1mo
171
}
133728e barerepo 1mo
172
}
133728e barerepo 1mo
173
return true
133728e barerepo 1mo
174
}
history · rawbarerepo 0.1.0