Skip to content

feat: enhance branch cleanup to identify and handle local branches without upstream#19

Open
sergiotejon wants to merge 1 commit into
mainfrom
feat/local-no-push
Open

feat: enhance branch cleanup to identify and handle local branches without upstream#19
sergiotejon wants to merge 1 commit into
mainfrom
feat/local-no-push

Conversation

@sergiotejon
Copy link
Copy Markdown
Contributor

  • 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.
@sergiotejon sergiotejon self-assigned this Jan 12, 2026
Comment thread cmd/branches.go

// Convert maps to display list
// Local branches (without upstream) are treated as unmerged even if merged
var branchesToDelete []string
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Se crea feature-x localmente, se hace merge a main vía PR
  2. La rama local no tiene upstream (nunca se hizo push o el upstream fue borrado)
  3. getMergedBranches() la identifica correctamente como merged → entra en safeToDeleteMap
  4. Pero como branchesWithoutUpstream[branch] es true → se le añade el prefijo (*) y requiere confirmación DELETE

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.

Comment thread cmd/branches.go
}
}

// Delete local unmerged branches (force delete, local only)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread cmd/branches.go
if len(parts) == 1 {
// Solo tiene nombre, no tiene upstream
noUpstream[parts[0]] = true
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
}

Copy link
Copy Markdown
Contributor

@Muriano Muriano left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. UX confusa: El usuario ve ramas seguras marcadas como peligrosas
  2. Confirmación innecesaria: Requiere escribir DELETE para ramas que ya están merged
  3. Método de borrado excesivo: Se usa git branch -D (force) en lugar de git branch -d para 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. 👍

Copy link
Copy Markdown

@dieloren dieloren left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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:519 — getBranchesWithoutUpstream does not exclude the current branch or the default branch. Defense-in-depth: exclude them explicitly to prevent future regressions if upstream logic changes.
🟡 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants