main bac21c6

registry.go RawHistoryFolder

1package pruneguard
2
3import (
4	"sort"
5	"time"
6
7	"github.com/bnema/gordon/internal/domain"
8)
9
10// RegistryTagCandidate is one registry tag as inventoried.
11type RegistryTagCandidate struct {
12	Ref domain.RegistryTagRef
13	// Digest is the manifest digest the tag resolved to; empty when
14	// unknown.
15	Digest string
16	// ModTime is the tag's last modification time.
17	ModTime time.Time
18	// ManifestKnown is false when the tag's manifest could not be read,
19	// so the content behind it cannot be proven unreferenced.
20	ManifestKnown bool
21}
22
23// RegistryPlanInput is the complete input of one registry tag plan.
24// KeepLast semantics match the product contract: latest plus the
25// keep_last newest non-latest tags per repository. KeepLast <= 0 skips
26// registry tag cleanup entirely, so the function returns no verdicts.
27type RegistryPlanInput struct {
28	Tags     []RegistryTagCandidate
29	Snapshot *domain.PruneProtectionSnapshot
30	KeepLast int
31	// Complete is false when the registry inventory could not be read
32	// in full. Candidates that would otherwise be eligible become
33	// unknown instead.
34	Complete bool
35}
36
37// PlanRegistryTags computes one verdict per candidate tag. Output is
38// ordered by repository then tag, so callers and reports are
39// deterministic regardless of input order.
40func PlanRegistryTags(input RegistryPlanInput) []domain.RegistryTagVerdict {
41	if input.KeepLast <= 0 {
42		return nil
43	}
44
45	kept := retentionWindow(input.Tags, input.KeepLast)
46
47	verdicts := make([]domain.RegistryTagVerdict, 0, len(input.Tags))
48	for _, candidate := range input.Tags {
49		verdicts = append(verdicts, planRegistryTag(candidate, kept, input))
50	}
51	sortRegistryTagVerdicts(verdicts)
52	return verdicts
53}
54
55func planRegistryTag(candidate RegistryTagCandidate, kept map[string]struct{}, input RegistryPlanInput) domain.RegistryTagVerdict {
56	verdict := domain.RegistryTagVerdict{Ref: candidate.Ref, Digest: candidate.Digest}
57
58	if !candidate.Ref.Valid() {
59		verdict.Verdict, verdict.Reasons = unknownResult(domain.PruneReasonUnknownIdentity)
60		return verdict
61	}
62
63	// A durable root wins over every other consideration: protected
64	// candidates stay protected even when the inventory is incomplete.
65	refs := []string{candidate.Ref.Key(), candidate.Ref.String()}
66	if candidate.Digest != "" {
67		refs = append(refs, candidate.Digest)
68	}
69	if matched, reasons := rootReasonsOrFallback(input.Snapshot, refs...); matched {
70		verdict.Verdict, verdict.Reasons = protected(reasons, domain.PruneReasonProtectedSharedContent)
71		return verdict
72	}
73
74	if candidate.Ref.Tag == "latest" {
75		verdict.Verdict, verdict.Reasons = protected(nil, domain.PruneReasonProtectedLatest)
76		return verdict
77	}
78	if _, inWindow := kept[candidate.Ref.Key()]; inWindow {
79		verdict.Verdict, verdict.Reasons = protected(nil, domain.PruneReasonProtectedRetentionWindow)
80		return verdict
81	}
82
83	if !candidate.ManifestKnown {
84		verdict.Verdict, verdict.Reasons = unknownResult(domain.PruneReasonUnknownManifest)
85		return verdict
86	}
87	// Safety is only provable against a complete view of both the
88	// registry and the durable facts that claim it. An incomplete app
89	// snapshot could be hiding a root for this very tag.
90	if !input.Complete || !input.Snapshot.Complete() {
91		verdict.Verdict, verdict.Reasons = unknownResult(domain.PruneReasonUnknownInventory)
92		return verdict
93	}
94
95	verdict.Verdict, verdict.Reasons = eligibleResult(domain.PruneReasonEligibleRetention)
96	return verdict
97}
98
99// retentionWindow returns the canonical keys of the latest tag plus the
100// keep_last newest non-latest tags of every repository. Ordering is by
101// modification time descending, then tag descending, so equal
102// timestamps still produce one deterministic window.
103func retentionWindow(tags []RegistryTagCandidate, keepLast int) map[string]struct{} {
104	byRepository := make(map[string][]RegistryTagCandidate)
105	for _, tag := range tags {
106		if !tag.Ref.Valid() {
107			continue
108		}
109		byRepository[tag.Ref.Repository] = append(byRepository[tag.Ref.Repository], tag)
110	}
111
112	kept := make(map[string]struct{})
113	for _, repositoryTags := range byRepository {
114		sort.SliceStable(repositoryTags, func(i, j int) bool {
115			if repositoryTags[i].ModTime.Equal(repositoryTags[j].ModTime) {
116				return repositoryTags[i].Ref.Tag > repositoryTags[j].Ref.Tag
117			}
118			return repositoryTags[i].ModTime.After(repositoryTags[j].ModTime)
119		})
120
121		count := 0
122		for _, tag := range repositoryTags {
123			if tag.Ref.Tag == "latest" {
124				kept[tag.Ref.Key()] = struct{}{}
125				continue
126			}
127			if count >= keepLast {
128				break
129			}
130			kept[tag.Ref.Key()] = struct{}{}
131			count++
132		}
133	}
134	return kept
135}
136
137func sortRegistryTagVerdicts(verdicts []domain.RegistryTagVerdict) {
138	sort.SliceStable(verdicts, func(i, j int) bool {
139		if verdicts[i].Ref.Repository != verdicts[j].Ref.Repository {
140			return verdicts[i].Ref.Repository < verdicts[j].Ref.Repository
141		}
142		return verdicts[i].Ref.Tag < verdicts[j].Ref.Tag
143	})
144}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close