Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion scripts/ci/prune_merged_worktrees.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,39 @@ def is_source_dirty(path: str) -> bool:
return any(p.endswith(ext) for ext in SOURCE_EXTENSIONS)


def same_worktree_path(a: str, b: str) -> bool:
"""Deux chemins de worktree designent-ils le meme repertoire ?

Les deux cotes de la comparaison `is_current` viennent de sources qui
n'ecrivent PAS les chemins de la meme facon :

- `git worktree list --porcelain` rend toujours des slash avant
(`D:/CoursIA/.worktrees/x`), y compris sur Windows ;
- `Path(cwd).resolve()` rend la forme native, donc a antislash sur
Windows (`D:` + separateur natif + `CoursIA` + ...).

Une egalite de chaines entre ces deux formes est donc **toujours fausse
sur Windows** : `SKIP_CURRENT` etait inatteignable. Mesure du 2026-09-03
sur ai-01 (64 worktrees) : `skipped=0` meme en lancant le script depuis
`.worktrees/ai01-gate-current`, dont la PR #14459 est MERGED -- ce
worktree etait donc programme `WOULD REMOVE`, c'est-a-dire que `--apply`
aurait tente `git worktree remove` sur le repertoire courant du process.

Ce garde n'est pas fail-closed : il ne refuse pas trop, il ne refuse
jamais. La comparaison se fait donc sur les chemins **resolus**, et
`Path.__eq__` est insensible a la casse sous Windows (ce qui couvre au
passage `d:/` vs `D:/`).
"""
try:
return Path(a).resolve() == Path(b).resolve()
except OSError:
# Chemin inaccessible (lecteur demonte, worktree efface a la main) :
# on retombe sur une normalisation textuelle plutot que de rendre
# False, qui reintroduirait exactement le defaut ci-dessus.
return (a.replace("\\", "/").rstrip("/").lower()
== b.replace("\\", "/").rstrip("/").lower())


def get_worktree_info(wt_path: str, current_path: str) -> dict:
"""Recupere branch + ahead count + dirty status d'un worktree."""
# Branche (peut etre None si HEAD detaché)
Expand Down Expand Up @@ -281,7 +314,7 @@ def get_worktree_info(wt_path: str, current_path: str) -> dict:
"ahead_count": ahead_count,
"untracked": untracked,
"has_source_dirty": has_source,
"is_current": wt_path == current_path,
"is_current": same_worktree_path(wt_path, current_path),
}


Expand Down
50 changes: 50 additions & 0 deletions scripts/tests/test_prune_merged_worktrees.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,56 @@ def test_to_dict_round_trip(self):
assert k in d, f"missing key: {k}"


# ---------------------------------------------------------------------------
# same_worktree_path -- le drapeau is_current se CALCULE
# ---------------------------------------------------------------------------


class TestSameWorktreePath:
r"""`test_skip_current` ne pinne que l'AVAL du drapeau, jamais son calcul.

Le drapeau etait `wt_path == current_path`, entre deux ecritures
differentes du meme chemin : `git worktree list --porcelain` rend des
slash avant, `Path(cwd).resolve()` rend la forme native (antislash sous
Windows). L'egalite ne pouvait donc jamais etre vraie sur Windows, et
`SKIP_CURRENT` etait inatteignable -- mesure du 2026-09-03 sur ai-01,
64 worktrees, `skipped=0` meme lance depuis un worktree dont la PR est
MERGED, c'est-a-dire un `--apply` qui aurait tente `worktree remove`
sur son propre repertoire courant.
"""

def test_separator_mismatch_is_the_same_worktree(self, tmp_path):
native = str(tmp_path)
porcelain = native.replace(os.sep, "/")
if os.sep != "/":
# Controle positif : sans ca, le test passerait sur une paire
# identique et ne mesurerait rien du defaut qu'il pinne.
assert porcelain != native, "le cas teste ne se reproduit pas ici"
assert pmw.same_worktree_path(porcelain, native) is True

def test_trailing_separator_is_the_same_worktree(self, tmp_path):
assert pmw.same_worktree_path(str(tmp_path) + "/", str(tmp_path)) is True

def test_distinct_worktrees_are_not_current(self, tmp_path):
a = tmp_path / "wt-a"
b = tmp_path / "wt-b"
a.mkdir()
b.mkdir()
assert pmw.same_worktree_path(str(a), str(b)) is False

def test_get_worktree_info_uses_the_comparison(self, monkeypatch, tmp_path):
"""Pinne le CABLAGE : un retour a `==` en l.284 doit rougir ici."""
class _Proc:
returncode = 0
stdout = ""

monkeypatch.setattr(pmw, "run_git", lambda *a, **k: _Proc())
native = str(tmp_path)
porcelain = native.replace(os.sep, "/")
info = pmw.get_worktree_info(porcelain, native)
assert info["is_current"] is True


# ---------------------------------------------------------------------------
# Tests lookup_pr_for_detached_head (#14476) -- anti faux-positifs
# ---------------------------------------------------------------------------
Expand Down
Loading