-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathownership.go
More file actions
215 lines (196 loc) · 4.9 KB
/
ownership.go
File metadata and controls
215 lines (196 loc) · 4.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
// ownership.go — run-length encoded per-file line ownership and repo state.
package main
import "strings"
// Seg is a run of N consecutive lines all owned by the same author.
type Seg struct {
Author string
N int
}
// mergeSegs collapses adjacent segments with the same author.
func mergeSegs(in []Seg) []Seg {
out := make([]Seg, 0, len(in))
for _, s := range in {
if s.N <= 0 {
continue
}
if len(out) > 0 && out[len(out)-1].Author == s.Author {
out[len(out)-1].N += s.N
} else {
out = append(out, s)
}
}
return out
}
// deleteRange removes `count` lines starting at `pos` (0-indexed).
// Returns the updated segments and a map of author → lines removed.
func deleteRange(segs []Seg, pos, count int) ([]Seg, map[string]int) {
removed := make(map[string]int)
if count == 0 || len(segs) == 0 {
return segs, removed
}
end := pos + count
out := make([]Seg, 0, len(segs))
cur := 0
for _, s := range segs {
hi := cur + s.N
if hi <= pos || cur >= end {
out = append(out, s) // entirely outside deletion zone
} else {
if cur < pos { // prefix before zone
out = append(out, Seg{s.Author, pos - cur})
}
dFrom := max(cur, pos)
dTo := min(hi, end)
removed[s.Author] += dTo - dFrom
if hi > end { // suffix after zone
out = append(out, Seg{s.Author, hi - end})
}
}
cur = hi
}
return mergeSegs(out), removed
}
// insertAt inserts `count` lines by `author` at position `pos` (0-indexed).
func insertAt(segs []Seg, pos int, author string, count int) []Seg {
if count == 0 {
return segs
}
out := make([]Seg, 0, len(segs)+2)
cur := 0
done := false
for _, s := range segs {
if !done && cur+s.N >= pos {
pre := pos - cur
if pre > 0 {
out = append(out, Seg{s.Author, pre})
}
out = append(out, Seg{author, count})
suf := s.N - pre
if suf > 0 {
out = append(out, Seg{s.Author, suf})
}
done = true
} else {
out = append(out, s)
}
cur += s.N
}
if !done {
out = append(out, Seg{author, count})
}
return mergeSegs(out)
}
// State is the full ownership picture of the repo at a point in time.
type State struct {
Files map[string][]Seg // filepath → RLE ownership
Totals map[string]int // author email → total lines
}
func newState() *State {
return &State{Files: make(map[string][]Seg), Totals: make(map[string]int)}
}
func (st *State) totalLines() int {
n := 0
for _, v := range st.Totals {
n += v
}
return n
}
func (st *State) copyTotals() map[string]int {
m := make(map[string]int, len(st.Totals))
for k, v := range st.Totals {
m[k] = v
}
return m
}
// applyHunk applies one diff hunk to a single file.
//
// oldStart is the 1-indexed line number in the pre-patch file.
// offset is the running delta for this file in this diff (caller must track).
//
// After a deletion of D lines and insertion of A lines:
//
// offset += A - D
func (st *State) applyHunk(file string, oldStart, oldCount, newCount int, author string, offset *int) {
segs := st.Files[file]
if oldCount > 0 {
delPos := oldStart - 1 + *offset
if delPos < 0 {
delPos = 0
}
newSegs, removed := deleteRange(segs, delPos, oldCount)
segs = newSegs
for a, n := range removed {
st.Totals[a] -= n
if st.Totals[a] <= 0 {
delete(st.Totals, a)
}
}
}
if newCount > 0 {
var insertPos int
if oldCount == 0 {
// Pure insertion: git says "after line oldStart" → 0-indexed = oldStart
insertPos = oldStart + *offset
} else {
// Replacement: insert where we just deleted
insertPos = oldStart - 1 + *offset
}
if insertPos < 0 {
insertPos = 0
}
segs = insertAt(segs, insertPos, author, newCount)
st.Totals[author] += newCount
}
*offset += newCount - oldCount
if len(segs) == 0 {
delete(st.Files, file)
} else {
st.Files[file] = segs
}
}
func (st *State) renameFile(from, to string) {
if segs, ok := st.Files[from]; ok {
st.Files[to] = segs
delete(st.Files, from)
}
}
// computeDirTotals returns per-directory author line counts for a targeted set
// of directories. Only directories present in trackDirs are computed, so the
// caller controls memory by passing only the directories it cares about.
func (st *State) computeDirTotals(trackDirs map[string]bool) map[string]map[string]int {
result := make(map[string]map[string]int)
for file, segs := range st.Files {
// Walk every ancestor directory of this file (deepest → shallowest).
prefix := file
for {
i := strings.LastIndexByte(prefix, '/')
if i <= 0 {
break
}
prefix = prefix[:i]
if !trackDirs[prefix] {
continue
}
m := result[prefix]
if m == nil {
m = make(map[string]int)
result[prefix] = m
}
for _, seg := range segs {
m[seg.Author] += seg.N
}
}
}
return result
}
func (st *State) deleteFile(file string) {
if segs, ok := st.Files[file]; ok {
for _, s := range segs {
st.Totals[s.Author] -= s.N
if st.Totals[s.Author] <= 0 {
delete(st.Totals, s.Author)
}
}
delete(st.Files, file)
}
}