Skip to content

Commit 8050634

Browse files
committed
Add interactive stale branch pruning with PIN confirmation
Enhances the git prune stale command to interactively display stale branches, allow users to select which branches to delete, and require PIN confirmation before deletion. Adds helpers for generating a random PIN and multi-select prompts, with corresponding tests for PIN generation.
1 parent 5b5b8c7 commit 8050634

3 files changed

Lines changed: 158 additions & 13 deletions

File tree

internal/commands/git.go

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ var GitPruneStaleCmd = &cobra.Command{
6262
}
6363
currentBranch := strings.TrimSpace(string(currentOutput))
6464

65-
// Parse local branches
65+
// Parse local branches and identify stale branches
6666
localBranches := strings.Split(string(localOutput), "\n")
67-
deletedCount := 0
67+
var staleBranches []string
6868

6969
for _, branch := range localBranches {
7070
branch = strings.TrimSpace(branch)
@@ -87,24 +87,99 @@ var GitPruneStaleCmd = &cobra.Command{
8787
continue
8888
}
8989

90-
// If branch doesn't exist on remote, delete it
90+
// If branch doesn't exist on remote, add to stale list
9191
if strings.TrimSpace(string(remoteOutput)) == "" {
92-
fmt.Printf("Deleting stale branch: %s\n", branch)
93-
deleteCmd := exec.Command("git", "branch", "-D", branch)
94-
if err := deleteCmd.Run(); err != nil {
95-
fmt.Printf("⚠️ Failed to delete %s: %v\n", branch, err)
96-
} else {
97-
deletedCount++
98-
}
92+
staleBranches = append(staleBranches, branch)
9993
}
10094
}
10195

102-
if deletedCount == 0 {
96+
// If no stale branches, exit early
97+
if len(staleBranches) == 0 {
10398
fmt.Println("✅ No stale branches found!")
104-
} else {
105-
fmt.Printf("✅ Deleted %d stale branch(es)!\n", deletedCount)
99+
return nil
100+
}
101+
102+
// Display stale branches count
103+
fmt.Printf("\n⚠️ Found %d stale branch(es):\n", len(staleBranches))
104+
for _, branch := range staleBranches {
105+
fmt.Printf(" - %s\n", branch)
106+
}
107+
fmt.Println()
108+
109+
// Stern warning
110+
fmt.Println("⚠️ WARNING: This is a DESTRUCTIVE action that CANNOT be reversed!")
111+
fmt.Println("⚠️ Deleted branches cannot be recovered unless backed up elsewhere.")
112+
fmt.Println()
113+
114+
// Prompt user for action
115+
action, err := utils.PromptSelect(
116+
"How would you like to proceed?",
117+
[]string{"Prune All", "Select Branches to Prune", "Abort"},
118+
)
119+
if err != nil {
120+
return err
106121
}
107122

123+
var branchesToDelete []string
124+
125+
switch action {
126+
case "Abort":
127+
fmt.Println("❌ Operation aborted. No branches were deleted.")
128+
return nil
129+
130+
case "Prune All":
131+
branchesToDelete = staleBranches
132+
133+
case "Select Branches to Prune":
134+
selected, err := utils.PromptMultiSelect(
135+
"Select branches to delete:",
136+
staleBranches,
137+
)
138+
if err != nil {
139+
return err
140+
}
141+
142+
if len(selected) == 0 {
143+
fmt.Println("❌ No branches selected. Operation aborted.")
144+
return nil
145+
}
146+
147+
branchesToDelete = selected
148+
}
149+
150+
// Generate PIN for confirmation
151+
pin, err := utils.GeneratePIN()
152+
if err != nil {
153+
return fmt.Errorf("failed to generate PIN: %w", err)
154+
}
155+
156+
// Display PIN and ask for confirmation
157+
fmt.Printf("\n🔐 To confirm deletion of %d branch(es), enter this PIN: %s\n", len(branchesToDelete), pin)
158+
userPIN, err := utils.SurveyInput("Enter PIN to confirm", "")
159+
if err != nil {
160+
return err
161+
}
162+
163+
if userPIN != pin {
164+
fmt.Println("❌ Incorrect PIN. Operation aborted. No branches were deleted.")
165+
return nil
166+
}
167+
168+
// Delete selected branches
169+
fmt.Println("\n🗑️ Deleting branches...")
170+
deletedCount := 0
171+
for _, branch := range branchesToDelete {
172+
fmt.Printf(" Deleting: %s\n", branch)
173+
deleteCmd := exec.Command("git", "branch", "-D", branch)
174+
if err := deleteCmd.Run(); err != nil {
175+
fmt.Printf(" ⚠️ Failed to delete %s: %v\n", branch, err)
176+
} else {
177+
deletedCount++
178+
}
179+
}
180+
181+
fmt.Printf("\n✅ Successfully deleted %d branch(es)!\n", deletedCount)
182+
108183
return nil
109184
},
110185
}

internal/utils/helpers.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,24 @@ func SurveyConfirm(message string, defaultValue bool) (bool, error) {
205205
err := survey.AskOne(prompt, &result)
206206
return result, err
207207
}
208+
209+
// GeneratePIN generates a random 6-digit PIN
210+
func GeneratePIN() (string, error) {
211+
num, err := rand.Int(rand.Reader, big.NewInt(1000000))
212+
if err != nil {
213+
return "", err
214+
}
215+
// Ensure 6 digits with leading zeros if needed
216+
return fmt.Sprintf("%06d", num.Int64()), nil
217+
}
218+
219+
// PromptMultiSelect prompts the user to select multiple items from a list
220+
func PromptMultiSelect(message string, options []string) ([]string, error) {
221+
var result []string
222+
prompt := &survey.MultiSelect{
223+
Message: message,
224+
Options: options,
225+
}
226+
err := survey.AskOne(prompt, &result)
227+
return result, err
228+
}

internal/utils/helpers_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,52 @@ func TestGeneratePasswordByType(t *testing.T) {
239239
})
240240
}
241241
}
242+
243+
func TestGeneratePIN(t *testing.T) {
244+
// Test that PIN is generated without error
245+
pin, err := GeneratePIN()
246+
if err != nil {
247+
t.Errorf("GeneratePIN() error = %v", err)
248+
return
249+
}
250+
251+
// Test that PIN is exactly 6 characters
252+
if len(pin) != 6 {
253+
t.Errorf("GeneratePIN() length = %v, want 6", len(pin))
254+
}
255+
256+
// Test that PIN contains only digits
257+
for _, c := range pin {
258+
if c < '0' || c > '9' {
259+
t.Errorf("GeneratePIN() contains non-digit character: %c", c)
260+
}
261+
}
262+
263+
// Test that multiple calls generate different PINs (with high probability)
264+
pin2, err := GeneratePIN()
265+
if err != nil {
266+
t.Errorf("GeneratePIN() second call error = %v", err)
267+
return
268+
}
269+
270+
// Generate several more PINs to check randomness
271+
// Note: There's a tiny chance all could be the same, but highly unlikely
272+
allSame := pin == pin2
273+
if allSame {
274+
// Try a few more times
275+
for i := 0; i < 5; i++ {
276+
pin3, err := GeneratePIN()
277+
if err != nil {
278+
t.Errorf("GeneratePIN() call %d error = %v", i+3, err)
279+
return
280+
}
281+
if pin3 != pin {
282+
allSame = false
283+
break
284+
}
285+
}
286+
if allSame {
287+
t.Errorf("GeneratePIN() appears to not be random - all 7 calls returned same PIN: %s", pin)
288+
}
289+
}
290+
}

0 commit comments

Comments
 (0)