chore: 升级 Trellis 并精简规则(09-05-trellis-upgrade-rule-cleanup)

这个提交包含在:
2026-09-06 11:14:41 +08:00
父节点 ab82fe30c0
当前提交 c3cef5cfc4
共修改 48 个文件,包含 4588 行新增和 1053 行删除
+129
查看文件
@@ -7,9 +7,25 @@ Single source of truth for running git commands across all Trellis scripts.
from __future__ import annotations
import subprocess
import time
from pathlib import Path
# Bounded retry for transient `.git/index.lock` contention. Another process
# (IDE git integration, status daemon, a concurrent Trellis session) can hold
# the lock for a fraction of a second; three attempts spread over ~1.5s ride
# that out without making a genuinely stuck lock hang the command. One sleep
# per retry, so the attempt count follows the backoff tuple.
INDEX_LOCK_RETRY_BACKOFF = (0.5, 1.0)
INDEX_LOCK_RETRY_ATTEMPTS = len(INDEX_LOCK_RETRY_BACKOFF) + 1
# Whether a checkout is a linked worktree cannot change while a script runs, and
# the answer costs two subprocesses. Developer-identity resolution asks several
# times per command whenever the local `.developer` file is absent, so memoize.
_CACHE_MISS = object()
_MAIN_WORKTREE_CACHE: dict[Path, Path | None] = {}
def run_git(
args: list[str],
cwd: Path | None = None,
@@ -38,6 +54,49 @@ def run_git(
return 1, "", str(e)
def stderr_indicates_index_lock(stderr: str) -> bool:
"""git failed because another process holds `.git/index.lock`."""
if not stderr:
return False
return "index.lock" in stderr.lower()
def run_git_retry_index_lock(
args: list[str],
cwd: Path | None = None,
timeout: float | None = None,
) -> tuple[int, str, str]:
"""Run a git command, retrying only while `.git/index.lock` is held.
Any other non-zero exit returns immediately — a retry loop around real
failures (bad path, nothing to commit, hook rejection) just delays the
error. Returns the last (returncode, stdout, stderr).
"""
rc, out, err = run_git(args, cwd=cwd, timeout=timeout)
attempt = 1
while (
rc != 0
and attempt < INDEX_LOCK_RETRY_ATTEMPTS
and stderr_indicates_index_lock(err)
):
time.sleep(INDEX_LOCK_RETRY_BACKOFF[attempt - 1])
attempt += 1
rc, out, err = run_git(args, cwd=cwd, timeout=timeout)
return rc, out, err
def index_lock_path(repo_root: Path) -> str:
"""Path of the lock file git is contending on, for diagnostics.
Asks git so worktrees and `GIT_DIR` setups name the real file rather
than a `.git/` guess that does not exist there.
"""
rc, out, _ = run_git(["rev-parse", "--git-path", "index.lock"], cwd=repo_root)
if rc == 0 and out.strip():
return out.strip()
return ".git/index.lock"
def resolve_default_branch(repo_root: Path) -> str | None:
"""Resolve the repository's default branch (origin/HEAD target).
@@ -63,6 +122,76 @@ def resolve_default_branch(repo_root: Path) -> str | None:
return None
def current_branch_name(repo_root: Path) -> str | None:
"""Return the checked-out branch name, or None when there isn't one.
Empty output covers detached HEAD and "not a git repository" alike, and
callers treat both the same way: there is no branch worth recording.
"""
rc, out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
if rc != 0:
return None
return out.strip() or None
def has_git_remote(repo_root: Path) -> bool:
"""Whether the repository has at least one configured remote."""
rc, out, _ = run_git(["remote"], cwd=repo_root)
return rc == 0 and bool(out.strip())
def main_worktree_root(repo_root: Path) -> Path | None:
"""Root of the main working tree when `repo_root` is a linked worktree.
Returns None in the main working tree itself, outside a git repository, and
for a linked worktree of a bare repository (no main checkout to point at).
`git worktree list --porcelain` reports the main working tree as its first
record, so git identifies it rather than this code deriving it from the
`.git` layout. Deriving it — taking the parent of `--git-common-dir` — is
wrong for a bare repository that happens to sit inside an unrelated
checkout (`~/repos/project.git` under a `~/repos` that is itself a repo):
the parent is a real checkout with a real `.developer`, so the guess is
indistinguishable from a hit and identity leaks across repositories.
"""
cached = _MAIN_WORKTREE_CACHE.get(repo_root, _CACHE_MISS)
if cached is not _CACHE_MISS:
return cached # type: ignore[return-value]
result = _probe_main_worktree_root(repo_root)
_MAIN_WORKTREE_CACHE[repo_root] = result
return result
def _probe_main_worktree_root(repo_root: Path) -> Path | None:
rc_list, listing, _ = run_git(["worktree", "list", "--porcelain"], cwd=repo_root)
rc_top, toplevel, _ = run_git(["rev-parse", "--show-toplevel"], cwd=repo_root)
if rc_list != 0 or rc_top != 0:
return None
lines = listing.splitlines()
if not lines or not lines[0].startswith("worktree "):
return None
# Records are blank-line separated; a `bare` attribute on the first one
# means the "main working tree" is a bare repo with nothing to inherit.
for line in lines[1:]:
if not line.strip():
break
if line.strip() == "bare":
return None
try:
main_root = Path(lines[0][len("worktree ") :].strip()).resolve()
current_root = Path(toplevel.strip()).resolve()
except (OSError, ValueError):
return None
if main_root == current_root:
return None
return main_root
def branch_exists_locally(branch: str, repo_root: Path) -> bool:
"""Check whether a local branch ref exists in the repository."""
if not branch: