detached 7c11f18

refactor(cli): simplify estimateRegistryTagsToPrune for lower cyclomatic complexity

Break down complex function into smaller helper functions:
- collectTagsByRepository: Group tags by repo
- countTagsToRemove: Count tags that would be removed
- toSortedTagInfos: Sort tags by creation time
- selectKeptTags: Apply retention policy

Fixes golangci-lint cyclomatic complexity > 15 warning.
commit
7c11f18c842b97791a81cc6d80e790f02796ea02
author
brice · · 2026-02-13 18:17 UTC
parent
808089b
signature
unsigned
1 file · +66 -34
M internal/adapters/in/cli/images.go +66 -34
@@ -382,7 +382,21 @@ func estimateRegistryTagsToPrune(images []dto.Image, keepReleases int) int {
382382 return 0
383383 }
384384
385+ tagsByRepo := collectTagsByRepository(images)
386+ removed := 0
387+
388+ for _, repoTags := range tagsByRepo {
389+ removed += countTagsToRemove(repoTags, keepReleases)
390+ }
391+
392+ return removed
393+}
394+
395+// collectTagsByRepository groups images by repository and returns
396+// map of repo -> tag name -> creation time
397+func collectTagsByRepository(images []dto.Image) map[string]map[string]time.Time {
385398 tagsByRepo := make(map[string]map[string]time.Time)
399+
386400 for _, img := range images {
387401 repo := strings.TrimSpace(img.Repository)
388402 tag := strings.TrimSpace(img.Tag)
@@ -390,55 +404,73 @@ func estimateRegistryTagsToPrune(images []dto.Image, keepReleases int) int {
390404 continue
391405 }
392406
393- repoTags, ok := tagsByRepo[repo]
394- if !ok {
395- repoTags = make(map[string]time.Time)
396- tagsByRepo[repo] = repoTags
407+ if _, ok := tagsByRepo[repo]; !ok {
408+ tagsByRepo[repo] = make(map[string]time.Time)
397409 }
398410
411+ repoTags := tagsByRepo[repo]
399412 if current, exists := repoTags[tag]; !exists || img.Created.After(current) {
400413 repoTags[tag] = img.Created
401414 }
402415 }
403416
404- removed := 0
405- for _, repoTags := range tagsByRepo {
406- tagInfos := make([]previewRegistryTag, 0, len(repoTags))
407- for name, created := range repoTags {
408- tagInfos = append(tagInfos, previewRegistryTag{name: name, created: created})
409- }
417+ return tagsByRepo
418+}
410419
411- sort.Slice(tagInfos, func(i, j int) bool {
412- if tagInfos[i].created.Equal(tagInfos[j].created) {
413- return tagInfos[i].name > tagInfos[j].name
414- }
415- return tagInfos[i].created.After(tagInfos[j].created)
416- })
420+// countTagsToRemove counts how many tags in a repository would be removed
421+// based on keepReleases retention policy
422+func countTagsToRemove(repoTags map[string]time.Time, keepReleases int) int {
423+ tagInfos := toSortedTagInfos(repoTags)
417424
418- kept := make(map[string]struct{})
419- for _, info := range tagInfos {
420- if info.name == "latest" {
421- kept["latest"] = struct{}{}
422- break
423- }
425+ kept := selectKeptTags(tagInfos, keepReleases)
426+
427+ return len(tagInfos) - len(kept)
428+}
429+
430+// toSortedTagInfos converts repo tags map to sorted slice
431+func toSortedTagInfos(repoTags map[string]time.Time) []previewRegistryTag {
432+ tagInfos := make([]previewRegistryTag, 0, len(repoTags))
433+ for name, created := range repoTags {
434+ tagInfos = append(tagInfos, previewRegistryTag{name: name, created: created})
435+ }
436+
437+ sort.Slice(tagInfos, func(i, j int) bool {
438+ if tagInfos[i].created.Equal(tagInfos[j].created) {
439+ return tagInfos[i].name > tagInfos[j].name
424440 }
441+ return tagInfos[i].created.After(tagInfos[j].created)
442+ })
425443
426- keptNonLatest := 0
427- for _, info := range tagInfos {
428- if info.name == "latest" {
429- continue
430- }
431- if keptNonLatest >= keepReleases {
432- break
433- }
434- kept[info.name] = struct{}{}
435- keptNonLatest++
444+ return tagInfos
445+}
446+
447+// selectKeptTags returns which tags would be kept based on
448+// retention policy (latest + keepReleases non-latest)
449+func selectKeptTags(tagInfos []previewRegistryTag, keepReleases int) map[string]struct{} {
450+ kept := make(map[string]struct{})
451+
452+ // Always keep latest if present
453+ for _, info := range tagInfos {
454+ if info.name == "latest" {
455+ kept["latest"] = struct{}{}
456+ break
436457 }
458+ }
437459
438- removed += len(tagInfos) - len(kept)
460+ // Keep most recent non-latest tags
461+ keptCount := 0
462+ for _, info := range tagInfos {
463+ if info.name == "latest" {
464+ continue
465+ }
466+ if keptCount >= keepReleases {
467+ break
468+ }
469+ kept[info.name] = struct{}{}
470+ keptCount++
439471 }
440472
441- return removed
473+ return kept
442474 }
443475
444476 func buildPruneRequest(keepReleases int, pruneDangling, pruneRegistry bool) dto.ImagePruneRequest {
@@ -382,7 +382,21 @@ func estimateRegistryTagsToPrune(images []dto.Image, keepReleases int) int {
382 return 0382 return 0
383 }383 }
384 384
385+ tagsByRepo := collectTagsByRepository(images)
386+ removed := 0
387+
388+ for _, repoTags := range tagsByRepo {
389+ removed += countTagsToRemove(repoTags, keepReleases)
390+ }
391+
392+ return removed
393+}
394+
395+// collectTagsByRepository groups images by repository and returns
396+// map of repo -> tag name -> creation time
397+func collectTagsByRepository(images []dto.Image) map[string]map[string]time.Time {
385 tagsByRepo := make(map[string]map[string]time.Time)398 tagsByRepo := make(map[string]map[string]time.Time)
399+
386 for _, img := range images {400 for _, img := range images {
387 repo := strings.TrimSpace(img.Repository)401 repo := strings.TrimSpace(img.Repository)
388 tag := strings.TrimSpace(img.Tag)402 tag := strings.TrimSpace(img.Tag)
@@ -390,55 +404,73 @@ func estimateRegistryTagsToPrune(images []dto.Image, keepReleases int) int {
390 continue404 continue
391 }405 }
392 406
393- repoTags, ok := tagsByRepo[repo]407+ if _, ok := tagsByRepo[repo]; !ok {
394- if !ok {408+ tagsByRepo[repo] = make(map[string]time.Time)
395- repoTags = make(map[string]time.Time)
396- tagsByRepo[repo] = repoTags
397 }409 }
398 410
411+ repoTags := tagsByRepo[repo]
399 if current, exists := repoTags[tag]; !exists || img.Created.After(current) {412 if current, exists := repoTags[tag]; !exists || img.Created.After(current) {
400 repoTags[tag] = img.Created413 repoTags[tag] = img.Created
401 }414 }
402 }415 }
403 416
404- removed := 0417+ return tagsByRepo
405- for _, repoTags := range tagsByRepo {418+}
406- tagInfos := make([]previewRegistryTag, 0, len(repoTags))
407- for name, created := range repoTags {
408- tagInfos = append(tagInfos, previewRegistryTag{name: name, created: created})
409- }
410 419
411- sort.Slice(tagInfos, func(i, j int) bool {420+// countTagsToRemove counts how many tags in a repository would be removed
412- if tagInfos[i].created.Equal(tagInfos[j].created) {421+// based on keepReleases retention policy
413- return tagInfos[i].name > tagInfos[j].name422+func countTagsToRemove(repoTags map[string]time.Time, keepReleases int) int {
414- }423+ tagInfos := toSortedTagInfos(repoTags)
415- return tagInfos[i].created.After(tagInfos[j].created)
416- })
417 424
418- kept := make(map[string]struct{})425+ kept := selectKeptTags(tagInfos, keepReleases)
419- for _, info := range tagInfos {426+
420- if info.name == "latest" {427+ return len(tagInfos) - len(kept)
421- kept["latest"] = struct{}{}428+}
422- break429+
423- }430+// toSortedTagInfos converts repo tags map to sorted slice
431+func toSortedTagInfos(repoTags map[string]time.Time) []previewRegistryTag {
432+ tagInfos := make([]previewRegistryTag, 0, len(repoTags))
433+ for name, created := range repoTags {
434+ tagInfos = append(tagInfos, previewRegistryTag{name: name, created: created})
435+ }
436+
437+ sort.Slice(tagInfos, func(i, j int) bool {
438+ if tagInfos[i].created.Equal(tagInfos[j].created) {
439+ return tagInfos[i].name > tagInfos[j].name
424 }440 }
441+ return tagInfos[i].created.After(tagInfos[j].created)
442+ })
425 443
426- keptNonLatest := 0444+ return tagInfos
427- for _, info := range tagInfos {445+}
428- if info.name == "latest" {446+
429- continue447+// selectKeptTags returns which tags would be kept based on
430- }448+// retention policy (latest + keepReleases non-latest)
431- if keptNonLatest >= keepReleases {449+func selectKeptTags(tagInfos []previewRegistryTag, keepReleases int) map[string]struct{} {
432- break450+ kept := make(map[string]struct{})
433- }451+
434- kept[info.name] = struct{}{}452+ // Always keep latest if present
435- keptNonLatest++453+ for _, info := range tagInfos {
454+ if info.name == "latest" {
455+ kept["latest"] = struct{}{}
456+ break
436 }457 }
458+ }
437 459
438- removed += len(tagInfos) - len(kept)460+ // Keep most recent non-latest tags
461+ keptCount := 0
462+ for _, info := range tagInfos {
463+ if info.name == "latest" {
464+ continue
465+ }
466+ if keptCount >= keepReleases {
467+ break
468+ }
469+ kept[info.name] = struct{}{}
470+ keptCount++
439 }471 }
440 472
441- return removed473+ return kept
442 }474 }
443 475
444 func buildPruneRequest(keepReleases int, pruneDangling, pruneRegistry bool) dto.ImagePruneRequest {476 func buildPruneRequest(keepReleases int, pruneDangling, pruneRegistry bool) dto.ImagePruneRequest {

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