feat(orphans): make kill command handle both commits and processes
The gt orphans kill command now performs a unified cleanup that removes orphaned commits via git gc AND kills orphaned Claude processes in one operation, with a single confirmation prompt. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -54,20 +54,22 @@ var (
|
|||||||
// Commit orphan kill command
|
// Commit orphan kill command
|
||||||
var orphansKillCmd = &cobra.Command{
|
var orphansKillCmd = &cobra.Command{
|
||||||
Use: "kill",
|
Use: "kill",
|
||||||
Short: "Remove orphaned commits permanently",
|
Short: "Remove all orphans (commits and processes)",
|
||||||
Long: `Remove orphaned commits by running git garbage collection.
|
Long: `Remove orphaned commits and kill orphaned Claude processes.
|
||||||
|
|
||||||
This command finds orphaned commits and then runs 'git gc --prune=now'
|
This command performs a complete orphan cleanup:
|
||||||
to permanently delete unreachable objects from the repository.
|
1. Finds and removes orphaned commits via 'git gc --prune=now'
|
||||||
|
2. Finds and kills orphaned Claude processes (PPID=1)
|
||||||
|
|
||||||
WARNING: This operation is irreversible. Once commits are pruned,
|
WARNING: This operation is irreversible. Once commits are pruned,
|
||||||
they cannot be recovered.
|
they cannot be recovered.
|
||||||
|
|
||||||
The command will:
|
The command will:
|
||||||
1. Find orphaned commits (same as 'gt orphans')
|
1. Find orphaned commits (same as 'gt orphans')
|
||||||
2. Show what will be removed
|
2. Find orphaned Claude processes (same as 'gt orphans procs')
|
||||||
3. Ask for confirmation (unless --force)
|
3. Show what will be removed/killed
|
||||||
4. Run git gc --prune=now
|
4. Ask for confirmation (unless --force)
|
||||||
|
5. Run git gc and kill processes
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
gt orphans kill # Kill orphans from last 7 days (default)
|
gt orphans kill # Kill orphans from last 7 days (default)
|
||||||
@@ -345,7 +347,7 @@ func formatAge(t time.Time) string {
|
|||||||
return fmt.Sprintf("%d days ago", days)
|
return fmt.Sprintf("%d days ago", days)
|
||||||
}
|
}
|
||||||
|
|
||||||
// runOrphansKill removes orphaned commits by running git gc
|
// runOrphansKill removes orphaned commits and kills orphaned processes
|
||||||
func runOrphansKill(cmd *cobra.Command, args []string) error {
|
func runOrphansKill(cmd *cobra.Command, args []string) error {
|
||||||
townRoot, err := workspace.FindFromCwdOrError()
|
townRoot, err := workspace.FindFromCwdOrError()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -358,36 +360,59 @@ func runOrphansKill(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mayorPath := r.Path + "/mayor/rig"
|
mayorPath := r.Path + "/mayor/rig"
|
||||||
fmt.Printf("Scanning for orphaned commits in %s...\n\n", rigName)
|
|
||||||
|
|
||||||
orphans, err := findOrphanCommits(mayorPath)
|
// Find orphaned commits
|
||||||
|
fmt.Printf("Scanning for orphaned commits in %s...\n", rigName)
|
||||||
|
commitOrphans, err := findOrphanCommits(mayorPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("finding orphans: %w", err)
|
return fmt.Errorf("finding orphan commits: %w", err)
|
||||||
}
|
|
||||||
|
|
||||||
if len(orphans) == 0 {
|
|
||||||
fmt.Printf("%s No orphaned commits found\n", style.Bold.Render("✓"))
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter commits by date
|
||||||
cutoff := time.Now().AddDate(0, 0, -orphansKillDays)
|
cutoff := time.Now().AddDate(0, 0, -orphansKillDays)
|
||||||
var filtered []OrphanCommit
|
var filteredCommits []OrphanCommit
|
||||||
for _, o := range orphans {
|
for _, o := range commitOrphans {
|
||||||
if orphansKillAll || o.Date.After(cutoff) {
|
if orphansKillAll || o.Date.After(cutoff) {
|
||||||
filtered = append(filtered, o)
|
filteredCommits = append(filteredCommits, o)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(filtered) == 0 {
|
// Find orphaned processes
|
||||||
fmt.Printf("%s No orphaned commits in the last %d days\n", style.Bold.Render("✓"), orphansKillDays)
|
fmt.Printf("Scanning for orphaned Claude processes...\n\n")
|
||||||
fmt.Printf("%s Use --days=N or --all to target older orphans\n", style.Dim.Render("Hint:"))
|
procOrphans, err := findOrphanProcesses()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("finding orphan processes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's anything to do
|
||||||
|
if len(filteredCommits) == 0 && len(procOrphans) == 0 {
|
||||||
|
fmt.Printf("%s No orphans found\n", style.Bold.Render("✓"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Found %d orphaned commit(s) to remove:\n\n", style.Warning.Render("⚠"), len(filtered))
|
// Show orphaned commits
|
||||||
for _, o := range filtered {
|
if len(filteredCommits) > 0 {
|
||||||
fmt.Printf(" %s %s\n", style.Bold.Render(o.SHA[:8]), o.Subject)
|
fmt.Printf("%s Found %d orphaned commit(s) to remove:\n\n", style.Warning.Render("⚠"), len(filteredCommits))
|
||||||
fmt.Printf(" %s by %s\n\n", style.Dim.Render(formatAge(o.Date)), o.Author)
|
for _, o := range filteredCommits {
|
||||||
|
fmt.Printf(" %s %s\n", style.Bold.Render(o.SHA[:8]), o.Subject)
|
||||||
|
fmt.Printf(" %s by %s\n\n", style.Dim.Render(formatAge(o.Date)), o.Author)
|
||||||
|
}
|
||||||
|
} else if len(commitOrphans) > 0 {
|
||||||
|
fmt.Printf("%s No orphaned commits in the last %d days (use --days=N or --all)\n\n",
|
||||||
|
style.Dim.Render("ℹ"), orphansKillDays)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show orphaned processes
|
||||||
|
if len(procOrphans) > 0 {
|
||||||
|
fmt.Printf("%s Found %d orphaned Claude process(es) to kill:\n\n", style.Warning.Render("⚠"), len(procOrphans))
|
||||||
|
for _, o := range procOrphans {
|
||||||
|
displayArgs := o.Args
|
||||||
|
if len(displayArgs) > 80 {
|
||||||
|
displayArgs = displayArgs[:77] + "..."
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s %s\n", style.Bold.Render(fmt.Sprintf("PID %d", o.PID)), displayArgs)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
if orphansKillDryRun {
|
if orphansKillDryRun {
|
||||||
@@ -395,9 +420,11 @@ func runOrphansKill(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Confirmation
|
||||||
if !orphansKillForce {
|
if !orphansKillForce {
|
||||||
fmt.Printf("%s\n", style.Warning.Render("WARNING: This operation is irreversible!"))
|
fmt.Printf("%s\n", style.Warning.Render("WARNING: This operation is irreversible!"))
|
||||||
fmt.Printf("Remove %d orphaned commit(s)? [y/N] ", len(filtered))
|
total := len(filteredCommits) + len(procOrphans)
|
||||||
|
fmt.Printf("Remove %d orphan(s)? [y/N] ", total)
|
||||||
var response string
|
var response string
|
||||||
_, _ = fmt.Scanln(&response)
|
_, _ = fmt.Scanln(&response)
|
||||||
if strings.ToLower(strings.TrimSpace(response)) != "y" {
|
if strings.ToLower(strings.TrimSpace(response)) != "y" {
|
||||||
@@ -406,16 +433,53 @@ func runOrphansKill(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\nRunning git gc --prune=now...\n")
|
// Kill orphaned commits
|
||||||
gcCmd := exec.Command("git", "gc", "--prune=now")
|
if len(filteredCommits) > 0 {
|
||||||
gcCmd.Dir = mayorPath
|
fmt.Printf("\nRunning git gc --prune=now...\n")
|
||||||
gcCmd.Stdout = os.Stdout
|
gcCmd := exec.Command("git", "gc", "--prune=now")
|
||||||
gcCmd.Stderr = os.Stderr
|
gcCmd.Dir = mayorPath
|
||||||
if err := gcCmd.Run(); err != nil {
|
gcCmd.Stdout = os.Stdout
|
||||||
return fmt.Errorf("git gc failed: %w", err)
|
gcCmd.Stderr = os.Stderr
|
||||||
|
if err := gcCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("git gc failed: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s Removed %d orphaned commit(s)\n", style.Bold.Render("✓"), len(filteredCommits))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n%s Removed %d orphaned commit(s)\n", style.Bold.Render("✓"), len(filtered))
|
// Kill orphaned processes
|
||||||
|
if len(procOrphans) > 0 {
|
||||||
|
fmt.Printf("\nKilling orphaned processes...\n")
|
||||||
|
var killed, failed int
|
||||||
|
for _, o := range procOrphans {
|
||||||
|
proc, err := os.FindProcess(o.PID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" %s PID %d: %v\n", style.Error.Render("✗"), o.PID, err)
|
||||||
|
failed++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := proc.Signal(syscall.SIGTERM); err != nil {
|
||||||
|
if err == os.ErrProcessDone {
|
||||||
|
fmt.Printf(" %s PID %d: already terminated\n", style.Dim.Render("○"), o.PID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s PID %d: %v\n", style.Error.Render("✗"), o.PID, err)
|
||||||
|
failed++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(" %s PID %d killed\n", style.Bold.Render("✓"), o.PID)
|
||||||
|
killed++
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s %d process(es) killed", style.Bold.Render("✓"), killed)
|
||||||
|
if failed > 0 {
|
||||||
|
fmt.Printf(", %d failed", failed)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n%s Orphan cleanup complete\n", style.Bold.Render("✓"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user