133728e barerepo 1mo
1
package proposal
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"context"
133728e barerepo 1mo
5
"sort"
133728e barerepo 1mo
6
"strconv"
133728e barerepo 1mo
7
"strings"
133728e barerepo 1mo
8
"time"
133728e barerepo 1mo
10
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
11
"github.com/barerepo/server/internal/thread"
133728e barerepo 1mo
14
// Expire drops proposal refs nothing touched inside the window, keeping every thread. Chapter 26.
133728e barerepo 1mo
15
func Expire(ctx context.Context, dir string, window time.Duration, now time.Time) ([]int, error) {
133728e barerepo 1mo
16
if window <= 0 {
133728e barerepo 1mo
17
return nil, nil
133728e barerepo 1mo
19
refs, err := gitx.ListRefs(dir, "refs/proposals")
133728e barerepo 1mo
20
if err != nil || len(refs) == 0 {
133728e barerepo 1mo
21
return nil, err
133728e barerepo 1mo
23
list, err := thread.List(ctx, dir)
133728e barerepo 1mo
24
if err != nil {
133728e barerepo 1mo
25
return nil, err
133728e barerepo 1mo
27
touched := make(map[int]time.Time, len(list))
133728e barerepo 1mo
28
for _, s := range list {
133728e barerepo 1mo
29
touched[s.N] = s.Updated
133728e barerepo 1mo
32
var gone []int
133728e barerepo 1mo
33
for name, sha := range refs {
133728e barerepo 1mo
34
n := Number(name)
133728e barerepo 1mo
35
if n == 0 {
133728e barerepo 1mo
36
continue
133728e barerepo 1mo
38
last, ok := touched[n]
133728e barerepo 1mo
39
if !ok || last.IsZero() {
133728e barerepo 1mo
40
// A ref with no thread behind it is dated by the commit it points at.
133728e barerepo 1mo
41
last = commitTime(ctx, dir, sha)
133728e barerepo 1mo
43
if last.IsZero() || now.Sub(last) <= window {
133728e barerepo 1mo
44
continue
133728e barerepo 1mo
46
if _, err := gitx.Run(ctx, dir, "update-ref", "-d", name, sha); err != nil {
133728e barerepo 1mo
47
continue
133728e barerepo 1mo
49
gone = append(gone, n)
133728e barerepo 1mo
51
sort.Ints(gone)
133728e barerepo 1mo
52
return gone, nil
133728e barerepo 1mo
55
// commitTime reads a commit's own date through the object pool, so a sweep starts no process for it.
133728e barerepo 1mo
56
func commitTime(ctx context.Context, dir, sha string) time.Time {
133728e barerepo 1mo
57
objs, err := gitx.Batch(ctx, dir, []string{sha})
133728e barerepo 1mo
58
if err != nil || objs[sha] == nil {
133728e barerepo 1mo
59
return time.Time{}
133728e barerepo 1mo
61
head, _, _ := strings.Cut(objs[sha].Body, "\n\n")
133728e barerepo 1mo
62
for _, line := range strings.Split(head, "\n") {
133728e barerepo 1mo
63
rest, ok := strings.CutPrefix(line, "committer ")
133728e barerepo 1mo
64
if !ok {
133728e barerepo 1mo
65
continue
133728e barerepo 1mo
67
if i := strings.LastIndex(rest, "> "); i >= 0 {
133728e barerepo 1mo
68
rest = rest[i+2:]
133728e barerepo 1mo
70
stamp, _, _ := strings.Cut(rest, " ")
133728e barerepo 1mo
71
if secs, err := strconv.ParseInt(stamp, 10, 64); err == nil {
133728e barerepo 1mo
72
return time.Unix(secs, 0)
133728e barerepo 1mo
75
return time.Time{}
133728e barerepo 1mo
78
// RevisionsKept is chapter 26's five, which is enough to see what a proposal looked like before.
133728e barerepo 1mo
79
const RevisionsKept = 5
133728e barerepo 1mo
81
// PruneRevisions drops retained tips past the keep count, except the ones a comment is anchored to.
133728e barerepo 1mo
82
func PruneRevisions(ctx context.Context, dir string, n, keep int) ([]int, error) {
133728e barerepo 1mo
83
refs, err := gitx.ListRefs(dir, RevisionPrefix(n))
133728e barerepo 1mo
84
if err != nil || len(refs) == 0 {
133728e barerepo 1mo
85
return nil, err
133728e barerepo 1mo
87
ks := make([]int, 0, len(refs))
133728e barerepo 1mo
88
at := make(map[int]string, len(refs))
133728e barerepo 1mo
89
for name, sha := range refs {
133728e barerepo 1mo
90
k, err := strconv.Atoi(strings.TrimPrefix(name, RevisionPrefix(n)+"/"))
133728e barerepo 1mo
91
if err != nil || k <= 0 {
133728e barerepo 1mo
92
continue
133728e barerepo 1mo
94
ks = append(ks, k)
133728e barerepo 1mo
95
at[k] = sha
133728e barerepo 1mo
97
sort.Sort(sort.Reverse(sort.IntSlice(ks)))
133728e barerepo 1mo
98
if len(ks) <= keep {
133728e barerepo 1mo
99
return nil, nil
133728e barerepo 1mo
100
}
133728e barerepo 1mo
102
needed, unknown := anchoredRevisions(ctx, dir, n)
133728e barerepo 1mo
103
// A comment that does not say which revision it was written against pins all of them. 43.4.
133728e barerepo 1mo
104
if unknown {
133728e barerepo 1mo
105
return nil, nil
133728e barerepo 1mo
106
}
133728e barerepo 1mo
108
var gone []int
133728e barerepo 1mo
109
for _, k := range ks[keep:] {
133728e barerepo 1mo
110
if needed[k] {
133728e barerepo 1mo
111
continue
133728e barerepo 1mo
112
}
133728e barerepo 1mo
113
if _, err := gitx.Run(ctx, dir, "update-ref", "-d", RevisionRef(n, k), at[k]); err != nil {
133728e barerepo 1mo
114
continue
133728e barerepo 1mo
115
}
133728e barerepo 1mo
116
gone = append(gone, k)
133728e barerepo 1mo
117
}
133728e barerepo 1mo
118
sort.Ints(gone)
133728e barerepo 1mo
119
return gone, nil
133728e barerepo 1mo
122
// anchoredRevisions reports which revisions a comment is anchored to, and whether one will not say.
133728e barerepo 1mo
123
func anchoredRevisions(ctx context.Context, dir string, n int) (map[int]bool, bool) {
133728e barerepo 1mo
124
_, comments, err := thread.Read(ctx, dir, n)
133728e barerepo 1mo
125
if err != nil {
133728e barerepo 1mo
126
return nil, true
133728e barerepo 1mo
127
}
133728e barerepo 1mo
128
needed := map[int]bool{}
133728e barerepo 1mo
129
unknown := false
133728e barerepo 1mo
130
for _, c := range comments {
133728e barerepo 1mo
131
if c.Blob == "" {
133728e barerepo 1mo
132
continue
133728e barerepo 1mo
133
}
133728e barerepo 1mo
134
if c.Revision <= 0 {
133728e barerepo 1mo
135
unknown = true
133728e barerepo 1mo
136
continue
133728e barerepo 1mo
137
}
133728e barerepo 1mo
138
needed[c.Revision] = true
133728e barerepo 1mo
139
}
133728e barerepo 1mo
140
return needed, unknown
133728e barerepo 1mo
143
// Numbers lists every proposal a repository has retained anything for, whether or not its ref lives.
133728e barerepo 1mo
144
func Numbers(dir string) []int {
133728e barerepo 1mo
145
refs, err := gitx.ListRefs(dir, "refs/revisions")
133728e barerepo 1mo
146
if err != nil {
133728e barerepo 1mo
147
return nil
133728e barerepo 1mo
148
}
133728e barerepo 1mo
149
seen := map[int]bool{}
133728e barerepo 1mo
150
for name := range refs {
133728e barerepo 1mo
151
rest := strings.TrimPrefix(name, "refs/revisions/")
133728e barerepo 1mo
152
head, _, ok := strings.Cut(rest, "/")
133728e barerepo 1mo
153
if !ok {
133728e barerepo 1mo
154
continue
133728e barerepo 1mo
155
}
133728e barerepo 1mo
156
if n, err := strconv.Atoi(head); err == nil && n > 0 {
133728e barerepo 1mo
157
seen[n] = true
133728e barerepo 1mo
158
}
133728e barerepo 1mo
159
}
133728e barerepo 1mo
160
out := make([]int, 0, len(seen))
133728e barerepo 1mo
161
for n := range seen {
133728e barerepo 1mo
162
out = append(out, n)
133728e barerepo 1mo
163
}
133728e barerepo 1mo
164
sort.Ints(out)
133728e barerepo 1mo
165
return out