feat: enhance branch cleanup to identify and handle local branches without upstream#19
feat: enhance branch cleanup to identify and handle local branches without upstream#19sergiotejon wants to merge 1 commit into
Conversation
sergiotejon
commented
Jan 12, 2026
- Added functionality to detect local branches without upstream and treat them as unmerged.
- Introduced a new prefix for local unmerged branches in the output.
- Updated cleanup process to confirm deletion of local branches without remote.
- Enhanced user feedback with a legend for branch statuses during cleanup.
…thout upstream - Added functionality to detect local branches without upstream and treat them as unmerged. - Introduced a new prefix for local unmerged branches in the output. - Updated cleanup process to confirm deletion of local branches without remote. - Enhanced user feedback with a legend for branch statuses during cleanup.
|
|
||
| // Convert maps to display list | ||
| // Local branches (without upstream) are treated as unmerged even if merged | ||
| var branchesToDelete []string |
There was a problem hiding this comment.
BLOQUEANTE: Las ramas merged que no tienen upstream se tratan como 'unmerged' y requieren confirmación DELETE. Esto es incorrecto: si una rama ya está merged en la rama por defecto, es segura para borrar independientemente de si tiene upstream o no.
Ejemplo del problema:
- Se crea
feature-xlocalmente, se hace merge a main vía PR - La rama local no tiene upstream (nunca se hizo push o el upstream fue borrado)
getMergedBranches()la identifica correctamente como merged → entra ensafeToDeleteMap- Pero como
branchesWithoutUpstream[branch]es true → se le añade el prefijo(*)y requiere confirmaciónDELETE
Solución: Las ramas en safeToDeleteMap deberían permanecer como safe independientemente de su estado de upstream. Solo aplicar la lógica de localUnmergedPrefix a ramas que NO estén en safeToDeleteMap.
| } | ||
| } | ||
|
|
||
| // Delete local unmerged branches (force delete, local only) |
There was a problem hiding this comment.
BLOQUEANTE: Una rama merged que recibe el prefijo (*) acaba en localUnmergedSelected y se borra con forceDeleteLocalBranch() (git branch -D). Para una rama ya merged, con git branch -d bastaría y es más seguro. Esto es consecuencia directa del problema anterior: ramas merged sin upstream se categorizan incorrectamente.
| if len(parts) == 1 { | ||
| // Solo tiene nombre, no tiene upstream | ||
| noUpstream[parts[0]] = true | ||
| } |
There was a problem hiding this comment.
Sugerencia (defensa en profundidad): La función getBranchesWithoutUpstream() no excluye la rama actual ni la rama por defecto. Aunque estas nunca acaban en safeToDeleteMap ni en unmergedBranchesMap, sería más seguro excluirlas explícitamente aquí para evitar problemas futuros si la lógica de arriba cambia.
if len(parts) == 1 && parts[0] != defaultBranch && parts[0] != currentBranch {
noUpstream[parts[0]] = true
}
Muriano
left a comment
There was a problem hiding this comment.
Revisión PR #19 — Request Changes
Problema principal (BLOQUEANTE)
Las ramas merged que no tienen upstream se tratan incorrectamente como "unmerged" (prefijo (*)), lo que provoca:
- UX confusa: El usuario ve ramas seguras marcadas como peligrosas
- Confirmación innecesaria: Requiere escribir
DELETEpara ramas que ya están merged - Método de borrado excesivo: Se usa
git branch -D(force) en lugar degit branch -dpara ramas que ya están integradas
Solución propuesta
Separar la lógica: el estado de upstream solo debe afectar la categorización de ramas no merged. Las ramas en safeToDeleteMap deben permanecer como safe independientemente de si tienen upstream o no.
for branch := range safeToDeleteMap {
// Merged branches are always safe, regardless of upstream
branchesToDelete = append(branchesToDelete, branch)
}
for branch := range unmergedBranchesMap {
if branchesWithoutUpstream[branch] {
branchesToDelete = append(branchesToDelete, localUnmergedPrefix+branch)
} else {
branchesToDelete = append(branchesToDelete, unmergedPrefix+branch)
}
}Sugerencia adicional (no bloqueante)
Añadir defensa en profundidad excluyendo la rama actual y la por defecto en getBranchesWithoutUpstream().
El concepto de la feature es bueno, solo necesita ajustar la lógica de categorización. 👍
dieloren
left a comment
There was a problem hiding this comment.
🤖 Heimdallm AI Review
The PR adds detection and handling of local branches without upstream, but conflates 'no upstream' with 'unmerged', causing merged branches without upstream to be incorrectly flagged and force-deleted. Existing reviewer comments correctly identify these as blocking issues.
Issues
🔴 cmd/branches.go:144 — Merged branches without upstream are reclassified as unmerged (localUnmergedPrefix), requiring DELETE confirmation. Branches in safeToDeleteMap should remain safe regardless of upstream status. Only apply the localUnmergedPrefix logic to branches NOT in safeToDeleteMap.
🔴 cmd/branches.go:305 — forceDeleteLocalBranch uses 'git branch -D' for branches in localUnmergedSelected. If a merged branch lands here (due to the issue above), it is force-deleted unnecessarily. Use 'git branch -d' for merged branches and reserve -D for genuinely unmerged ones.
🟡 cmd/branches.go:119 — User-facing warning and code comments mix Spanish ('Detectar ramas sin upstream', 'Solo tiene nombre, no tiene upstream') with English. Keep comments and messages consistent (English is used elsewhere in the file).
🟡 cmd/branches.go:277 — When the user fails the DELETE confirmation but forceDelete is set, localUnmergedSelected is cleared but the code proceeds to the deletion loop later — verify that skipping is properly handled and the safe-branches-only path is clear.
Suggestions
- Restructure the classification: iterate safeToDeleteMap first without checking upstream, then for unmergedBranchesMap optionally add the (*) marker if also without upstream — or treat 'local only' as an orthogonal annotation rather than a separate category.
- Split deletion behavior: merged branches always use 'git branch -d' (with remote delete only if upstream exists); only genuinely unmerged branches use 'git branch -D'.
- Translate Spanish comments ('Detectar ramas sin upstream', 'Solo tiene nombre, no tiene upstream') to English for consistency.
- Exclude current branch and default branch in getBranchesWithoutUpstream as defense-in-depth.
- Add a unit/integration test covering: merged branch without upstream (should be safe), unmerged branch without upstream (should require confirmation, local-only delete), and unmerged branch with upstream (existing behavior).
Severity: HIGH · Reviewed by Heimdallm