package proposal import ( "context" "sort" "strconv" "strings" "time" "github.com/barerepo/server/internal/gitx" "github.com/barerepo/server/internal/thread" ) // Expire drops proposal refs nothing touched inside the window, keeping every thread. Chapter 26. func Expire(ctx context.Context, dir string, window time.Duration, now time.Time) ([]int, error) { if window <= 0 { return nil, nil } refs, err := gitx.ListRefs(dir, "refs/proposals") if err != nil || len(refs) == 0 { return nil, err } list, err := thread.List(ctx, dir) if err != nil { return nil, err } touched := make(map[int]time.Time, len(list)) for _, s := range list { touched[s.N] = s.Updated } var gone []int for name, sha := range refs { n := Number(name) if n == 0 { continue } last, ok := touched[n] if !ok || last.IsZero() { // A ref with no thread behind it is dated by the commit it points at. last = commitTime(ctx, dir, sha) } if last.IsZero() || now.Sub(last) <= window { continue } if _, err := gitx.Run(ctx, dir, "update-ref", "-d", name, sha); err != nil { continue } gone = append(gone, n) } sort.Ints(gone) return gone, nil } // commitTime reads a commit's own date through the object pool, so a sweep starts no process for it. func commitTime(ctx context.Context, dir, sha string) time.Time { objs, err := gitx.Batch(ctx, dir, []string{sha}) if err != nil || objs[sha] == nil { return time.Time{} } head, _, _ := strings.Cut(objs[sha].Body, "\n\n") for _, line := range strings.Split(head, "\n") { rest, ok := strings.CutPrefix(line, "committer ") if !ok { continue } if i := strings.LastIndex(rest, "> "); i >= 0 { rest = rest[i+2:] } stamp, _, _ := strings.Cut(rest, " ") if secs, err := strconv.ParseInt(stamp, 10, 64); err == nil { return time.Unix(secs, 0) } } return time.Time{} } // RevisionsKept is chapter 26's five, which is enough to see what a proposal looked like before. const RevisionsKept = 5 // PruneRevisions drops retained tips past the keep count, except the ones a comment is anchored to. func PruneRevisions(ctx context.Context, dir string, n, keep int) ([]int, error) { refs, err := gitx.ListRefs(dir, RevisionPrefix(n)) if err != nil || len(refs) == 0 { return nil, err } ks := make([]int, 0, len(refs)) at := make(map[int]string, len(refs)) for name, sha := range refs { k, err := strconv.Atoi(strings.TrimPrefix(name, RevisionPrefix(n)+"/")) if err != nil || k <= 0 { continue } ks = append(ks, k) at[k] = sha } sort.Sort(sort.Reverse(sort.IntSlice(ks))) if len(ks) <= keep { return nil, nil } needed, unknown := anchoredRevisions(ctx, dir, n) // A comment that does not say which revision it was written against pins all of them. 43.4. if unknown { return nil, nil } var gone []int for _, k := range ks[keep:] { if needed[k] { continue } if _, err := gitx.Run(ctx, dir, "update-ref", "-d", RevisionRef(n, k), at[k]); err != nil { continue } gone = append(gone, k) } sort.Ints(gone) return gone, nil } // anchoredRevisions reports which revisions a comment is anchored to, and whether one will not say. func anchoredRevisions(ctx context.Context, dir string, n int) (map[int]bool, bool) { _, comments, err := thread.Read(ctx, dir, n) if err != nil { return nil, true } needed := map[int]bool{} unknown := false for _, c := range comments { if c.Blob == "" { continue } if c.Revision <= 0 { unknown = true continue } needed[c.Revision] = true } return needed, unknown } // Numbers lists every proposal a repository has retained anything for, whether or not its ref lives. func Numbers(dir string) []int { refs, err := gitx.ListRefs(dir, "refs/revisions") if err != nil { return nil } seen := map[int]bool{} for name := range refs { rest := strings.TrimPrefix(name, "refs/revisions/") head, _, ok := strings.Cut(rest, "/") if !ok { continue } if n, err := strconv.Atoi(head); err == nil && n > 0 { seen[n] = true } } out := make([]int, 0, len(seen)) for n := range seen { out = append(out, n) } sort.Ints(out) return out }