* feat(go): make go fmt git-aware by default - By default, only check changed Go files (modified, staged, untracked) - Add --all flag to check all files (previous behaviour) - Reduces noise when running fmt on large codebases Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(build): minimal output by default, add missing i18n - Default output now shows single line: "Success Built N artifacts (dir)" - Add --verbose/-v flag to show full detailed output - Add all missing i18n translations for build commands - Errors still show failure reason in minimal mode Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add root-level `core git` command - Create pkg/gitcmd with git workflow commands as root menu - Export command builders from pkg/dev (AddCommitCommand, etc.) - Commands available under both `core git` and `core dev` for compatibility - Git commands: health, commit, push, pull, work, sync, apply - GitHub orchestration stays in dev: issues, reviews, ci, impact Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(qa): add docblock coverage checking Implement docblock/docstring coverage analysis for Go code: - New `core qa docblock` command to check coverage - Shows compact file:line list when under threshold - Integrate with `core go qa` as a default check - Add --docblock-threshold flag (default 80%) The checker uses Go AST parsing to find exported symbols (functions, types, consts, vars) without documentation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address CodeRabbit review feedback - Fix doc comment: "status" → "health" in gitcmd package - Implement --check flag for `core go fmt` (exits non-zero if files need formatting) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: add docstrings for 100% coverage Add documentation comments to all exported symbols: - pkg/build: ProjectType constants - pkg/cli: LogLevel, RenderStyle, TableStyle - pkg/framework: ServiceFor, MustServiceFor, Core.Core - pkg/git: GitError.Error, GitError.Unwrap - pkg/i18n: Handler Match/Handle methods - pkg/log: Level constants - pkg/mcp: Tool input/output types - pkg/php: Service constants, QA types, service methods - pkg/process: ServiceError.Error - pkg/repos: RepoType constants - pkg/setup: ChangeType, ChangeCategory constants - pkg/workspace: AddWorkspaceCommands Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: standardize line endings to LF Add .gitattributes to enforce LF line endings for all text files. Normalize all existing files to use Unix-style line endings. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address CodeRabbit review feedback - cmd_format.go: validate --check/--fix mutual exclusivity, capture stderr - cmd_docblock.go: return error instead of os.Exit(1) for proper error handling Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address CodeRabbit review feedback (round 2) - linuxkit.go: propagate state update errors, handle cmd.Wait() errors in waitForExit - mcp.go: guard against empty old_string in editDiff to prevent runaway edits - cmd_docblock.go: log parse errors instead of silently skipping Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
288 lines
7.3 KiB
Go
288 lines
7.3 KiB
Go
// github_diff.go provides change tracking for dry-run output.
|
|
|
|
package setup
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/host-uk/core/pkg/cli"
|
|
"github.com/host-uk/core/pkg/i18n"
|
|
)
|
|
|
|
// ChangeType indicates the type of change being made.
|
|
type ChangeType string
|
|
|
|
// Change type constants for GitHub configuration diffs.
|
|
const (
|
|
// ChangeCreate indicates a new resource to be created.
|
|
ChangeCreate ChangeType = "create"
|
|
// ChangeUpdate indicates an existing resource to be updated.
|
|
ChangeUpdate ChangeType = "update"
|
|
// ChangeDelete indicates a resource to be deleted.
|
|
ChangeDelete ChangeType = "delete"
|
|
// ChangeSkip indicates a resource that requires no changes.
|
|
ChangeSkip ChangeType = "skip"
|
|
)
|
|
|
|
// ChangeCategory groups changes by type.
|
|
type ChangeCategory string
|
|
|
|
// Change category constants for grouping GitHub configuration changes.
|
|
const (
|
|
// CategoryLabel indicates label-related changes.
|
|
CategoryLabel ChangeCategory = "label"
|
|
// CategoryWebhook indicates webhook-related changes.
|
|
CategoryWebhook ChangeCategory = "webhook"
|
|
// CategoryProtection indicates branch protection changes.
|
|
CategoryProtection ChangeCategory = "protection"
|
|
// CategorySecurity indicates security settings changes.
|
|
CategorySecurity ChangeCategory = "security"
|
|
)
|
|
|
|
// Change represents a single change to be made.
|
|
type Change struct {
|
|
Category ChangeCategory
|
|
Type ChangeType
|
|
Name string
|
|
Description string
|
|
Details map[string]string // Key-value details about the change
|
|
}
|
|
|
|
// ChangeSet tracks all changes for a repository.
|
|
type ChangeSet struct {
|
|
Repo string
|
|
Changes []Change
|
|
}
|
|
|
|
// NewChangeSet creates a new change set for a repository.
|
|
func NewChangeSet(repo string) *ChangeSet {
|
|
return &ChangeSet{
|
|
Repo: repo,
|
|
Changes: make([]Change, 0),
|
|
}
|
|
}
|
|
|
|
// Add adds a change to the set.
|
|
func (cs *ChangeSet) Add(category ChangeCategory, changeType ChangeType, name, description string) {
|
|
cs.Changes = append(cs.Changes, Change{
|
|
Category: category,
|
|
Type: changeType,
|
|
Name: name,
|
|
Description: description,
|
|
Details: make(map[string]string),
|
|
})
|
|
}
|
|
|
|
// AddWithDetails adds a change with additional details.
|
|
func (cs *ChangeSet) AddWithDetails(category ChangeCategory, changeType ChangeType, name, description string, details map[string]string) {
|
|
cs.Changes = append(cs.Changes, Change{
|
|
Category: category,
|
|
Type: changeType,
|
|
Name: name,
|
|
Description: description,
|
|
Details: details,
|
|
})
|
|
}
|
|
|
|
// HasChanges returns true if there are any non-skip changes.
|
|
func (cs *ChangeSet) HasChanges() bool {
|
|
for _, c := range cs.Changes {
|
|
if c.Type != ChangeSkip {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Count returns the number of changes by type.
|
|
func (cs *ChangeSet) Count() (creates, updates, deletes, skips int) {
|
|
for _, c := range cs.Changes {
|
|
switch c.Type {
|
|
case ChangeCreate:
|
|
creates++
|
|
case ChangeUpdate:
|
|
updates++
|
|
case ChangeDelete:
|
|
deletes++
|
|
case ChangeSkip:
|
|
skips++
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// CountByCategory returns changes grouped by category.
|
|
func (cs *ChangeSet) CountByCategory() map[ChangeCategory]int {
|
|
counts := make(map[ChangeCategory]int)
|
|
for _, c := range cs.Changes {
|
|
if c.Type != ChangeSkip {
|
|
counts[c.Category]++
|
|
}
|
|
}
|
|
return counts
|
|
}
|
|
|
|
// Print outputs the change set to the console.
|
|
func (cs *ChangeSet) Print(verbose bool) {
|
|
creates, updates, deletes, skips := cs.Count()
|
|
|
|
// Print header
|
|
fmt.Printf("\n%s %s\n", dimStyle.Render(i18n.Label("repo")), repoNameStyle.Render(cs.Repo))
|
|
|
|
if !cs.HasChanges() {
|
|
fmt.Printf(" %s\n", dimStyle.Render(i18n.T("cmd.setup.github.no_changes")))
|
|
return
|
|
}
|
|
|
|
// Print summary
|
|
var parts []string
|
|
if creates > 0 {
|
|
parts = append(parts, successStyle.Render(fmt.Sprintf("+%d", creates)))
|
|
}
|
|
if updates > 0 {
|
|
parts = append(parts, warningStyle.Render(fmt.Sprintf("~%d", updates)))
|
|
}
|
|
if deletes > 0 {
|
|
parts = append(parts, errorStyle.Render(fmt.Sprintf("-%d", deletes)))
|
|
}
|
|
if skips > 0 && verbose {
|
|
parts = append(parts, dimStyle.Render(fmt.Sprintf("=%d", skips)))
|
|
}
|
|
fmt.Printf(" %s\n", strings.Join(parts, " "))
|
|
|
|
// Print details if verbose
|
|
if verbose {
|
|
cs.printByCategory(CategoryLabel, "Labels")
|
|
cs.printByCategory(CategoryWebhook, "Webhooks")
|
|
cs.printByCategory(CategoryProtection, "Branch protection")
|
|
cs.printByCategory(CategorySecurity, "Security")
|
|
}
|
|
}
|
|
|
|
func (cs *ChangeSet) printByCategory(category ChangeCategory, title string) {
|
|
var categoryChanges []Change
|
|
for _, c := range cs.Changes {
|
|
if c.Category == category && c.Type != ChangeSkip {
|
|
categoryChanges = append(categoryChanges, c)
|
|
}
|
|
}
|
|
|
|
if len(categoryChanges) == 0 {
|
|
return
|
|
}
|
|
|
|
fmt.Printf("\n %s:\n", dimStyle.Render(title))
|
|
for _, c := range categoryChanges {
|
|
icon := getChangeIcon(c.Type)
|
|
style := getChangeStyle(c.Type)
|
|
fmt.Printf(" %s %s", style.Render(icon), c.Name)
|
|
if c.Description != "" {
|
|
fmt.Printf(" %s", dimStyle.Render(c.Description))
|
|
}
|
|
fmt.Println()
|
|
|
|
// Print details (sorted for deterministic output)
|
|
keys := make([]string, 0, len(c.Details))
|
|
for k := range c.Details {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
fmt.Printf(" %s: %s\n", dimStyle.Render(k), c.Details[k])
|
|
}
|
|
}
|
|
}
|
|
|
|
func getChangeIcon(t ChangeType) string {
|
|
switch t {
|
|
case ChangeCreate:
|
|
return "+"
|
|
case ChangeUpdate:
|
|
return "~"
|
|
case ChangeDelete:
|
|
return "-"
|
|
default:
|
|
return "="
|
|
}
|
|
}
|
|
|
|
func getChangeStyle(t ChangeType) *cli.AnsiStyle {
|
|
switch t {
|
|
case ChangeCreate:
|
|
return successStyle
|
|
case ChangeUpdate:
|
|
return warningStyle
|
|
case ChangeDelete:
|
|
return errorStyle
|
|
default:
|
|
return dimStyle
|
|
}
|
|
}
|
|
|
|
// Aggregate combines multiple change sets into a summary.
|
|
type Aggregate struct {
|
|
Sets []*ChangeSet
|
|
}
|
|
|
|
// NewAggregate creates a new aggregate.
|
|
func NewAggregate() *Aggregate {
|
|
return &Aggregate{
|
|
Sets: make([]*ChangeSet, 0),
|
|
}
|
|
}
|
|
|
|
// Add adds a change set to the aggregate.
|
|
func (a *Aggregate) Add(cs *ChangeSet) {
|
|
a.Sets = append(a.Sets, cs)
|
|
}
|
|
|
|
// TotalChanges returns the total number of changes across all sets.
|
|
func (a *Aggregate) TotalChanges() (creates, updates, deletes, skips int) {
|
|
for _, cs := range a.Sets {
|
|
c, u, d, s := cs.Count()
|
|
creates += c
|
|
updates += u
|
|
deletes += d
|
|
skips += s
|
|
}
|
|
return
|
|
}
|
|
|
|
// ReposWithChanges returns the number of repos that have changes.
|
|
func (a *Aggregate) ReposWithChanges() int {
|
|
count := 0
|
|
for _, cs := range a.Sets {
|
|
if cs.HasChanges() {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
// PrintSummary outputs the aggregate summary.
|
|
func (a *Aggregate) PrintSummary() {
|
|
creates, updates, deletes, _ := a.TotalChanges()
|
|
reposWithChanges := a.ReposWithChanges()
|
|
|
|
fmt.Println()
|
|
fmt.Printf("%s\n", dimStyle.Render(i18n.Label("summary")))
|
|
fmt.Printf(" %s: %d\n", i18n.T("cmd.setup.github.repos_checked"), len(a.Sets))
|
|
|
|
if reposWithChanges == 0 {
|
|
fmt.Printf(" %s\n", dimStyle.Render(i18n.T("cmd.setup.github.all_up_to_date")))
|
|
return
|
|
}
|
|
|
|
fmt.Printf(" %s: %d\n", i18n.T("cmd.setup.github.repos_with_changes"), reposWithChanges)
|
|
if creates > 0 {
|
|
fmt.Printf(" %s: %s\n", i18n.T("cmd.setup.github.to_create"), successStyle.Render(fmt.Sprintf("%d", creates)))
|
|
}
|
|
if updates > 0 {
|
|
fmt.Printf(" %s: %s\n", i18n.T("cmd.setup.github.to_update"), warningStyle.Render(fmt.Sprintf("%d", updates)))
|
|
}
|
|
if deletes > 0 {
|
|
fmt.Printf(" %s: %s\n", i18n.T("cmd.setup.github.to_delete"), errorStyle.Render(fmt.Sprintf("%d", deletes)))
|
|
}
|
|
}
|