chore: 升级 Trellis 并精简规则(09-05-trellis-upgrade-rule-cleanup)
这个提交包含在:
@@ -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:
|
||||
|
||||
在新工单中引用
屏蔽一个用户