Files
wehub-resource-sync 4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

312 lines
12 KiB
Python

"""Thin, safe local-git client (branch / commit / push / status / hashing).
Every call shells out to the ``git`` binary in the target *workspace* with an
explicit argument list (never ``shell=True``) and a bounded timeout, and raises a
neutral :class:`GitCommandError` on failure. The push path is deliberately narrow:
it refuses to create or push a *protected* branch (``main``/``master``/the repo
default) and never uses ``--force`` — the structural half of a "never push to the
base branch" guarantee.
Vendor-neutral: callers pass a token for HTTPS auth, but nothing here is
GitHub-specific.
"""
from __future__ import annotations
import base64
import os
import subprocess
from collections.abc import Sequence
from urllib.parse import urlsplit
from integrations.git.errors import (
BRANCH_FAILED,
COMMIT_FAILED,
GIT_UNAVAILABLE,
NOT_A_GIT_REPO,
PROTECTED_BRANCH,
PUSH_FAILED,
GitCommandError,
)
_GIT_TIMEOUT_SEC = 60
# Networked lookups get a tighter bound so a slow/unreachable remote can't stall
# the whole flow (they always have a safe local fallback).
_REMOTE_TIMEOUT_SEC = 15
# Branch names we refuse to create or push to, on top of the resolved default.
_PROTECTED_BRANCHES = frozenset({"main", "master", "develop", "trunk"})
def _run_git(
workspace: str,
*args: str,
env: dict[str, str] | None = None,
timeout: float = _GIT_TIMEOUT_SEC,
) -> subprocess.CompletedProcess[str]:
"""Run ``git <args>`` in *workspace*; raise GitCommandError if git is missing."""
try:
return subprocess.run( # nosemgrep: dangerous-subprocess-use-audit
["git", *args],
cwd=workspace,
capture_output=True,
text=True,
timeout=timeout,
env=env,
)
except FileNotFoundError as exc:
raise GitCommandError(GIT_UNAVAILABLE, "git is not installed or not on PATH.") from exc
except subprocess.TimeoutExpired as exc:
raise GitCommandError(
GIT_UNAVAILABLE, f"git command timed out after {timeout:.0f}s."
) from exc
def _remote_https_base(workspace: str, remote: str = "origin") -> str:
"""``https://host/`` of *remote* when it uses HTTPS, else "" (http/SSH/file/etc.).
Only HTTPS qualifies: injecting the token for a plaintext ``http://`` remote
would send the credential in cleartext on the wire.
"""
result = _run_git(workspace, "remote", "get-url", remote)
if result.returncode != 0:
return ""
parsed = urlsplit(result.stdout.strip())
if parsed.scheme == "https" and parsed.hostname:
return f"https://{parsed.hostname}/"
return ""
def _token_auth_env(token: str, base_url: str) -> dict[str, str]:
"""Env that injects an HTTP Authorization header scoped to *base_url* for this call.
Uses git's ``GIT_CONFIG_*`` env-config so the token never appears in argv, the
remote URL, .git/config, or git's output. The header is scoped via
``http.<base_url>.extraheader`` so the token is only sent to that host and never
forwarded to other HTTPS remotes or redirects. This makes the request use the
*provided* token instead of whatever stale credential the local git credential
helper might have cached (the usual cause of a 403 on push).
"""
basic = base64.b64encode(f"x-access-token:{token}".encode()).decode()
env = dict(os.environ)
# Append at the next free index rather than clobbering an existing
# GIT_CONFIG_COUNT / GIT_CONFIG_KEY_* the caller may already rely on.
try:
count = int(env.get("GIT_CONFIG_COUNT", "0") or "0")
except ValueError:
count = 0
env[f"GIT_CONFIG_KEY_{count}"] = f"http.{base_url}.extraheader"
env[f"GIT_CONFIG_VALUE_{count}"] = f"Authorization: Basic {basic}"
env["GIT_CONFIG_COUNT"] = str(count + 1)
return env
def is_git_repo(workspace: str) -> bool:
"""True when *workspace* is inside a git work tree."""
result = _run_git(workspace, "rev-parse", "--is-inside-work-tree")
return result.returncode == 0 and result.stdout.strip() == "true"
def ensure_git_repo(workspace: str) -> None:
if not is_git_repo(workspace):
raise GitCommandError(NOT_A_GIT_REPO, f"{workspace} is not a git repository.")
def current_branch(workspace: str) -> str:
"""Name of the currently checked-out branch (empty on detached HEAD)."""
result = _run_git(workspace, "rev-parse", "--abbrev-ref", "HEAD")
branch = result.stdout.strip()
return "" if branch in ("", "HEAD") else branch
def _remote_default_branch(workspace: str, token: str | None) -> str:
"""The remote's default branch via ``ls-remote --symref`` (authoritative).
Bounded by a short timeout and returns "" on any failure/timeout, so a slow or
unreachable remote never stalls or aborts the caller — they fall back locally.
"""
base = _remote_https_base(workspace, "origin")
env = _token_auth_env(token, base) if (token and base) else None
try:
result = _run_git(
workspace,
"ls-remote",
"--symref",
"origin",
"HEAD",
env=env,
timeout=_REMOTE_TIMEOUT_SEC,
)
except GitCommandError:
return ""
if result.returncode != 0:
return ""
for line in result.stdout.splitlines():
# "ref: refs/heads/main\tHEAD"
if line.startswith("ref:"):
parts = line.split()
if len(parts) >= 2:
return parts[1].removeprefix("refs/heads/")
return ""
def default_branch(workspace: str, *, token: str | None = None) -> str:
"""Resolve the repo's default branch (the usual PR base), or "" if unknown.
Prefers the local ``origin/HEAD`` pointer; if it isn't configured (common on
fresh clones), asks the remote directly. Returns "" when neither is available
(e.g. offline) rather than guessing the current branch — callers must decide
what to do so a PR never silently targets the wrong base.
"""
result = _run_git(workspace, "symbolic-ref", "--short", "refs/remotes/origin/HEAD")
if result.returncode == 0 and result.stdout.strip():
return result.stdout.strip().removeprefix("origin/")
return _remote_default_branch(workspace, token)
def short_head(workspace: str) -> str:
"""Short SHA of HEAD, or "" if it can't be resolved (e.g. an unborn HEAD)."""
result = _run_git(workspace, "rev-parse", "--short", "HEAD")
return result.stdout.strip() if result.returncode == 0 else ""
def changed_paths(workspace: str) -> list[str]:
"""Paths with staged/unstaged/untracked changes (individual files, not dirs).
Uses ``-z`` (NUL-separated) so paths are returned verbatim: git's default
porcelain C-quotes filenames with spaces, quotes, or non-ASCII bytes, which
would then not match on ``git add``/``hash-object``.
"""
result = _run_git(workspace, "status", "--porcelain", "-z", "--untracked-files=all")
tokens = result.stdout.split("\0")
paths: list[str] = []
i = 0
while i < len(tokens):
record = tokens[i]
i += 1
if len(record) < 3:
continue
# Porcelain: "XY <path>". Rename/copy (R/C) records carry the original path
# in the next NUL-terminated token.
path = record[3:]
if path:
paths.append(path)
if record[0] in ("R", "C"):
orig = tokens[i] if i < len(tokens) else ""
i += 1
# A rename deletes the original, so it must be committed too; a copy
# leaves the original untouched, so it is excluded.
if record[0] == "R" and orig:
paths.append(orig)
return paths
def file_fingerprints(workspace: str, paths: Sequence[str]) -> dict[str, str]:
"""Map each path to a git hash of its current worktree content ("" if unreadable).
Lets a caller tell whether a file that was already dirty before a run was
actually *changed* (hash differs) versus left untouched (same hash).
"""
fingerprints: dict[str, str] = dict.fromkeys(paths, "")
# Hash all files in a single git invocation (one process, not one per file).
# Deleted/unreadable paths are filtered out first so they don't fail the batch;
# they keep the "" fingerprint.
existing = [p for p in paths if os.path.isfile(os.path.join(workspace, p))]
if not existing:
return fingerprints
result = _run_git(workspace, "hash-object", "--", *existing)
hashes = result.stdout.splitlines()
if result.returncode == 0 and len(hashes) == len(existing):
for path, digest in zip(existing, hashes):
fingerprints[path] = digest.strip()
return fingerprints
def assert_not_protected(branch: str, *, protected_extra: str = "") -> None:
"""Raise unless *branch* is a safe, non-base feature branch to push to."""
name = branch.strip()
protected = set(_PROTECTED_BRANCHES)
if protected_extra.strip():
protected.add(protected_extra.strip())
if not name or name in protected:
raise GitCommandError(
PROTECTED_BRANCH,
f"Refusing to create or push protected branch '{name or '(empty)'}'. "
"Work is always shipped on a fresh namespaced branch, never the base branch.",
)
def create_branch(workspace: str, branch: str, *, base_default: str = "") -> None:
"""Create and switch to *branch* off the current HEAD (protected-name guarded)."""
assert_not_protected(branch, protected_extra=base_default)
result = _run_git(workspace, "checkout", "-b", branch)
if result.returncode != 0:
raise GitCommandError(
BRANCH_FAILED, f"Could not create branch '{branch}': {result.stderr.strip()}"
)
def checkout_branch(workspace: str, branch: str) -> None:
"""Switch to an already-existing local *branch* (does not create one).
Used to put the workspace on a known branch (typically the resolved base)
before creating a new branch off it, so the new branch's parent is never
whatever unrelated branch the workspace happened to have checked out.
"""
result = _run_git(workspace, "checkout", branch)
if result.returncode != 0:
raise GitCommandError(
BRANCH_FAILED, f"Could not check out branch '{branch}': {result.stderr.strip()}"
)
def commit_paths(workspace: str, paths: Sequence[str], message: str) -> None:
"""Stage and commit *only* the given paths, excluding any other WIP in the tree.
``git add`` registers the paths (so newly created files are tracked), and
``git commit --only`` commits exactly those paths — disregarding any other
staged or unstaged changes the developer may have in the working tree.
"""
if not paths:
raise GitCommandError(COMMIT_FAILED, "no files to commit.")
# Register the paths that still exist (new/modified files) so ``--only`` can
# commit them; deleted paths (e.g. a rename's original) are skipped here and
# handled by ``git commit --only``, which records their removal.
existing = [p for p in paths if os.path.isfile(os.path.join(workspace, p))]
if existing:
add = _run_git(workspace, "add", "--", *existing)
if add.returncode != 0:
raise GitCommandError(COMMIT_FAILED, f"git add failed: {add.stderr.strip()}")
commit = _run_git(workspace, "commit", "--only", "-m", message, "--", *paths)
if commit.returncode != 0:
raise GitCommandError(COMMIT_FAILED, f"git commit failed: {commit.stderr.strip()}")
def push_branch(
workspace: str,
branch: str,
*,
remote: str = "origin",
base_default: str = "",
token: str | None = None,
) -> None:
"""Push *branch* to *remote* with upstream tracking. Never force, never base branch.
When *token* is given and *remote* is an HTTPS URL, the push authenticates with
that token (via an ephemeral, host-scoped HTTP header) instead of the machine's
cached git credentials. For SSH/other remotes the token is not injected (the
transport authenticates itself).
"""
assert_not_protected(branch, protected_extra=base_default)
env = None
if token:
base = _remote_https_base(workspace, remote)
if base:
env = _token_auth_env(token, base)
result = _run_git(workspace, "push", "--set-upstream", remote, branch, env=env)
if result.returncode != 0:
raise GitCommandError(
PUSH_FAILED, f"git push to {remote}/{branch} failed: {result.stderr.strip()}"
)