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 { | |||
| 382 | 382 | 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 | 398 | tagsByRepo := make(map[string]map[string]time.Time) | |
| 399 | + | ||
| 386 | 400 | for _, img := range images { | |
| 387 | 401 | repo := strings.TrimSpace(img.Repository) | |
| 388 | 402 | tag := strings.TrimSpace(img.Tag) | |
| @@ -390,55 +404,73 @@ func estimateRegistryTagsToPrune(images []dto.Image, keepReleases int) int { | |||
| 390 | 404 | continue | |
| 391 | 405 | } | |
| 392 | 406 | ||
| 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) | |
| 397 | 409 | } | |
| 398 | 410 | ||
| 411 | + | repoTags := tagsByRepo[repo] | |
| 399 | 412 | if current, exists := repoTags[tag]; !exists || img.Created.After(current) { | |
| 400 | 413 | repoTags[tag] = img.Created | |
| 401 | 414 | } | |
| 402 | 415 | } | |
| 403 | 416 | ||
| 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 | + | } | |
| 410 | 419 | ||
| 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) | |
| 417 | 424 | ||
| 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 | |
| 424 | 440 | } | |
| 441 | + | return tagInfos[i].created.After(tagInfos[j].created) | |
| 442 | + | }) | |
| 425 | 443 | ||
| 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 | |
| 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 removed | |
| 473 | + | return kept | |
| 442 | 474 | } | |
| 443 | 475 | ||
| 444 | 476 | func buildPruneRequest(keepReleases int, pruneDangling, pruneRegistry bool) dto.ImagePruneRequest { | |
| @@ -382,7 +382,21 @@ func estimateRegistryTagsToPrune(images []dto.Image, keepReleases int) int { | |||||
| 382 | return 0 | 382 | 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 | continue | 404 | 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.Created | 413 | repoTags[tag] = img.Created | ||
| 401 | } | 414 | } | ||
| 402 | } | 415 | } | ||
| 403 | 416 | ||||
| 404 | - | removed := 0 | 417 | + | 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].name | 422 | + | 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 | - | break | 429 | + | |
| 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 := 0 | 444 | + | return tagInfos |
| 427 | - | for _, info := range tagInfos { | 445 | + | } |
| 428 | - | if info.name == "latest" { | 446 | + | |
| 429 | - | continue | 447 | + | // 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 | - | break | 450 | + | 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 removed | 473 | + | 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 { | ||