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
42 changes: 38 additions & 4 deletions loopx/doctor_git.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Git source and revision diagnostics for the installation doctor."""
"""Source-owned Git identity for installation manifests and diagnostics."""

from __future__ import annotations

import os
import re
import subprocess
from enum import Enum
Expand All @@ -17,6 +18,34 @@ class GitRevisionRelation(str, Enum):
UNKNOWN = "unknown"


def _source_git_environment() -> dict[str, str]:
env = dict(os.environ)
# A caller (for example a Git hook) may target an unrelated checkout.
for key in ("GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE"):
env.pop(key, None)
return env


def _is_git_worktree_root(source_root: Path) -> bool:
"""Reject Git's upward discovery while accepting linked worktree gitfiles."""
if not (source_root / ".git").exists():
return False
try:
result = subprocess.run(
["git", "-C", str(source_root), "rev-parse", "--show-toplevel"],
check=False, capture_output=True, text=True,
encoding="utf-8", errors="replace",
env=_source_git_environment(),
)
return (
result.returncode == 0
and bool(result.stdout.strip())
and Path(result.stdout.strip()).resolve() == source_root
)
except OSError:
return False


def git_metadata_for_root(root: Path | None) -> dict[str, Any]:
if root is None:
return {
Expand All @@ -29,7 +58,7 @@ def git_metadata_for_root(root: Path | None) -> dict[str, Any]:
source_root = root.expanduser().resolve()
except OSError:
source_root = root.expanduser()
if not source_root.exists():
if not _is_git_worktree_root(source_root):
return {
"root": str(source_root),
"git_commit": None,
Expand All @@ -46,6 +75,7 @@ def _run(args: list[str]) -> str | None:
text=True,
encoding="utf-8",
errors="replace",
env=_source_git_environment(),
)
except OSError:
return None
Expand Down Expand Up @@ -92,7 +122,7 @@ def git_revision_relation(
source_root = root.expanduser().resolve()
except OSError:
source_root = root.expanduser()
if not source_root.exists():
if not _is_git_worktree_root(source_root):
return GitRevisionRelation.UNKNOWN

def _is_ancestor(ancestor: str, descendant: str) -> bool | None:
Expand All @@ -112,6 +142,7 @@ def _is_ancestor(ancestor: str, descendant: str) -> bool | None:
text=True,
encoding="utf-8",
errors="replace",
env=_source_git_environment(),
)
except OSError:
return None
Expand Down Expand Up @@ -157,7 +188,7 @@ def trusted_release_ref_for_root(
source_root = root.expanduser().resolve()
except OSError:
source_root = root.expanduser()
if not source_root.exists():
if not _is_git_worktree_root(source_root):
return None

try:
Expand All @@ -168,6 +199,7 @@ def trusted_release_ref_for_root(
text=True,
encoding="utf-8",
errors="replace",
env=_source_git_environment(),
)
except OSError:
return None
Expand All @@ -186,6 +218,7 @@ def trusted_release_ref_for_root(
text=True,
encoding="utf-8",
errors="replace",
env=_source_git_environment(),
)
except OSError:
continue
Expand All @@ -210,6 +243,7 @@ def trusted_release_ref_for_root(
text=True,
encoding="utf-8",
errors="replace",
env=_source_git_environment(),
)
commit = resolved.stdout.strip() if resolved.returncode == 0 else ""
if commit:
Expand Down
50 changes: 3 additions & 47 deletions loopx/release_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
import json
import os
from pathlib import Path
import subprocess
from typing import Any

from . import __version__
from .doctor_git import git_metadata_for_root


RELEASE_MANIFEST_SCHEMA_VERSION = "loopx_release_manifest_v0"
Expand Down Expand Up @@ -71,51 +71,6 @@ def _hash_tree(root: Path) -> dict[str, Any]:
}


def _run_git(source_root: Path, args: list[str]) -> str | None:
try:
result = subprocess.run(
["git", "-C", str(source_root), *args],
check=False,
capture_output=True,
text=True, encoding="utf-8", errors="replace",
)
except OSError:
return None
if result.returncode != 0:
return None
value = result.stdout.strip()
return value or None


def _git_metadata(source_root: Path | None) -> dict[str, Any]:
if source_root is None:
return {
"git_commit": None,
"git_ref": None,
"git_dirty": None,
}
root = source_root.expanduser().resolve()
if not root.exists():
return {
"git_commit": None,
"git_ref": None,
"git_dirty": None,
}
commit = _run_git(root, ["rev-parse", "HEAD"])
branch = _run_git(root, ["symbolic-ref", "--quiet", "--short", "HEAD"])
tag = _run_git(root, ["describe", "--tags", "--exact-match"])
status = _run_git(root, ["status", "--porcelain"])
if commit is None and branch is None and tag is None and status is None:
dirty: bool | None = None
else:
dirty = bool(status)
return {
"git_commit": commit,
"git_ref": branch or tag,
"git_dirty": dirty,
}


def _manifest_source_metadata(source_root: Path | None) -> dict[str, Any]:
if source_root is None:
return {
Expand Down Expand Up @@ -171,7 +126,8 @@ def _manifest_source_metadata(source_root: Path | None) -> dict[str, Any]:


def _source_metadata(source_root: Path | None) -> dict[str, Any]:
git_metadata = _git_metadata(source_root)
git_metadata = git_metadata_for_root(source_root)
git_metadata.pop("root")
if any(git_metadata.get(key) is not None for key in ("git_commit", "git_ref", "git_dirty")):
return {
**git_metadata,
Expand Down
23 changes: 4 additions & 19 deletions loopx/skill_install_readback.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
from email.parser import Parser
from pathlib import Path
import shutil
import subprocess
import tempfile
from typing import Any, Mapping, Sequence

from . import __version__
from .doctor_git import git_metadata_for_root

SKILL_INSTALL_READBACK_SCHEMA_VERSION = "loopx_skill_install_readback_v1"
SKILL_INSTALL_READBACK_FILENAME = ".loopx-skill-install.json"
Expand Down Expand Up @@ -287,21 +287,6 @@ def hash_skill_tree(
}


def _git_value(root: Path, *args: str) -> str | None:
try:
result = subprocess.run(
["git", "-C", str(root), *args],
check=False,
capture_output=True,
text=True, encoding="utf-8", errors="replace",
)
except OSError:
return None
if result.returncode != 0:
return None
return result.stdout.strip() or None


def _source_readback(
source_root: Path,
*,
Expand Down Expand Up @@ -363,13 +348,13 @@ def _source_readback(
"git_dirty": False,
}

commit = _git_value(source_root, "rev-parse", "HEAD")
status = _git_value(source_root, "status", "--porcelain")
metadata = git_metadata_for_root(source_root)
commit = metadata["git_commit"]
return {
"kind": "local_checkout",
"revision": commit,
"revision_kind": "git_commit" if commit else None,
"git_dirty": bool(status) if commit is not None else None,
"git_dirty": metadata["git_dirty"] if commit is not None else None,
}


Expand Down
27 changes: 24 additions & 3 deletions scripts/install-local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -578,10 +578,22 @@ if os.environ.get("LOOPX_SKILL_ENTRY_HOST_SURFACE") and result["status"] in {
PY
}

read_source_git_commit() {
PYTHONSAFEPATH=1 PYTHONPATH="$repo_root${PYTHONPATH:+:$PYTHONPATH}" \
"$LOOPX_PYTHON" - "$repo_root" <<'PY_SOURCE'
from pathlib import Path
import sys

from loopx.doctor_git import git_metadata_for_root

print(git_metadata_for_root(Path(sys.argv[1]))["git_commit"] or "")
PY_SOURCE
}

verify_default_promotion() {
LOOPX_VERIFY_LINK="$bin_dir/loopx" \
LOOPX_VERIFY_RELEASE_DIR="$release_dir" \
LOOPX_VERIFY_SOURCE_COMMIT="${LOOPX_RESOLVED_SOURCE_GIT_COMMIT:-$(git -C "$repo_root" rev-parse HEAD 2>/dev/null || true)}" \
LOOPX_VERIFY_SOURCE_COMMIT="${LOOPX_RESOLVED_SOURCE_GIT_COMMIT:-$source_git_commit}" \
"${LOOPX_PYTHON:-python3}" - <<'PY'
from pathlib import Path
import json
Expand Down Expand Up @@ -695,13 +707,14 @@ resolve_default_promotion() {
;;
esac

if ! git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
source_git_commit="$(read_source_git_commit)"
if [[ -z "$source_git_commit" ]]; then
promotion_mode="canary_only_unverified_source"
return 1
fi

local source_commit approved_commit source_status approved_ref
source_commit="$(git -C "$repo_root" rev-parse HEAD 2>/dev/null || true)"
source_commit="$source_git_commit"
source_status="$(git -C "$repo_root" status --porcelain 2>/dev/null || true)"
approved_ref="${LOOPX_APPROVED_DEFAULT_REF:-refs/remotes/origin/main}"
approved_commit="$(git -C "$repo_root" rev-parse "$approved_ref" 2>/dev/null || true)"
Expand All @@ -726,6 +739,9 @@ if [[ -z "$shell_profile" ]]; then
fi

configure_python_runtime
# Git hooks can pass selectors for their own repository into this installer.
unset GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_INDEX_FILE
source_git_commit=""
promote_default=0
if resolve_default_promotion; then
promote_default=1
Expand All @@ -744,6 +760,11 @@ if [[ "$install_canary" != "0" ]]; then
check_symlink_destination "$bin_dir/loopx-canary"
fi
if [[ "$promote_default" == "1" ]]; then
# Use the same source-root identity as the manifest and doctor, after rejecting
# unusable entry targets. A nested archive must not borrow its container's Git.
if [[ -z "$source_git_commit" ]]; then
source_git_commit="$(read_source_git_commit)"
fi
# Preparing shared Chat assets is part of the guarded installation.
mkdir -p "$releases_dir"
run_under_install_guard "$@"
Expand Down
1 change: 1 addition & 0 deletions tests/test_distribution_runtime_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def test_real_pip_compiled_and_uncompiled_wheel_have_the_same_identity(tmp_path:
contents = {
"loopx/__init__.py": Path(manifest.__file__).with_name("__init__.py").read_text(),
"loopx/release_manifest.py": Path(manifest.__file__).read_text(),
"loopx/doctor_git.py": Path(manifest.__file__).with_name("doctor_git.py").read_text(),
f"{metadata}/METADATA": f"Metadata-Version: 2.1\nName: loopx\nVersion: {version}\n",
f"{metadata}/WHEEL": "Wheel-Version: 1.0\nGenerator: test\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
}
Expand Down
5 changes: 4 additions & 1 deletion tests/test_doctor_git_encoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,15 @@ def test_trusted_release_preserves_unicode_remote_and_ref(git_repo, gbk_host):


@pytest.fixture
def git_output(monkeypatch, gbk_host):
def git_output(monkeypatch, gbk_host, tmp_path):
def install(*, returncode=0, fail_at=None):
calls = []
(tmp_path / ".git").mkdir(exist_ok=True)

def run(command, **kwargs):
args = command[3:]
if args == ["rev-parse", "--show-toplevel"]:
return SimpleNamespace(returncode=0, stdout=str(tmp_path) + "\n")
calls.append(args)
code = (
returncode[len(calls) - 1]
Expand Down
Loading
Loading