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

300 lines
12 KiB
Python

"""Tests for the vendor-neutral local-git client (integrations/git).
Exercises the git primitives against a real temp repo with a local bare remote:
repo detection, status parsing (incl. quoted paths), content fingerprints,
protected-branch guards, branch/commit/push, default-branch resolution, and the
host-scoped token auth header.
"""
from __future__ import annotations
import subprocess
from pathlib import Path
from typing import Any
from unittest.mock import patch
import pytest
from integrations.git import local as gitlocal
from integrations.git.errors import BRANCH_FAILED, NOT_A_GIT_REPO, PROTECTED_BRANCH, GitCommandError
def _git(cwd: Path, *args: str) -> None:
subprocess.run(["git", *args], cwd=cwd, check=True, capture_output=True, text=True)
def _init_repo(tmp_path: Path) -> Path:
"""A work tree on 'main' with an initial commit and a pushable bare 'origin'."""
bare = tmp_path / "remote.git"
work = tmp_path / "work"
_git(tmp_path, "init", "--bare", str(bare))
_git(tmp_path, "init", "-b", "main", str(work))
_git(work, "config", "user.email", "t@example.com")
_git(work, "config", "user.name", "Tester")
(work / "README.md").write_text("hello\n")
_git(work, "add", "-A")
_git(work, "commit", "-m", "init")
_git(work, "remote", "add", "origin", str(bare))
_git(work, "push", "-u", "origin", "main")
_git(work, "remote", "set-head", "origin", "main")
return work
def test_detects_repo_and_default_branch(tmp_path: Path) -> None:
work = _init_repo(tmp_path)
assert gitlocal.is_git_repo(str(work)) is True
assert gitlocal.current_branch(str(work)) == "main"
assert gitlocal.default_branch(str(work)) == "main"
assert gitlocal.changed_paths(str(work)) == []
def test_ensure_git_repo_raises_on_non_repo(tmp_path: Path) -> None:
with pytest.raises(GitCommandError) as exc:
gitlocal.ensure_git_repo(str(tmp_path))
assert exc.value.kind == NOT_A_GIT_REPO
def test_changed_paths_reports_new_and_modified(tmp_path: Path) -> None:
work = _init_repo(tmp_path)
(work / "app").mkdir()
(work / "app" / "handlers.py").write_text("x = 1\n")
(work / "README.md").write_text("changed\n")
paths = set(gitlocal.changed_paths(str(work)))
assert paths == {"app/handlers.py", "README.md"}
def test_changed_paths_handles_quoted_filenames(tmp_path: Path) -> None:
work = _init_repo(tmp_path)
# A space + non-ASCII byte makes git C-quote the path in default porcelain output.
weird = "wéird name.txt"
(work / weird).write_text("x\n")
# -z returns the path verbatim (unquoted), so downstream git ops match it.
assert weird in gitlocal.changed_paths(str(work))
assert gitlocal.file_fingerprints(str(work), [weird])[weird] # hashable
gitlocal.create_branch(str(work), "opensre/sentry-fix-1-x", base_default="main")
gitlocal.commit_paths(str(work), [weird], "add weird")
assert weird not in gitlocal.changed_paths(str(work))
def test_changed_paths_includes_both_sides_of_a_staged_rename(tmp_path: Path) -> None:
work = _init_repo(tmp_path)
(work / "old.txt").write_text("data\n")
_git(work, "add", "old.txt")
_git(work, "commit", "-m", "add old")
# Stage a rename (git mv) -> porcelain reports an "R" record with new + orig.
_git(work, "mv", "old.txt", "new.txt")
paths = set(gitlocal.changed_paths(str(work)))
assert paths == {"new.txt", "old.txt"} # the deleted original is included
# Committing exactly those paths leaves no straggler: old.txt is gone, new.txt exists.
gitlocal.create_branch(str(work), "opensre/sentry-fix-1-x", base_default="main")
gitlocal.commit_paths(str(work), list(paths), "rename old -> new")
tracked = subprocess.run(
["git", "ls-files"], cwd=work, capture_output=True, text=True
).stdout.split()
assert "new.txt" in tracked
assert "old.txt" not in tracked
assert gitlocal.changed_paths(str(work)) == [] # clean tree, no leftover
def test_file_fingerprints_batches_and_tolerates_missing(tmp_path: Path) -> None:
work = _init_repo(tmp_path)
(work / "a.txt").write_text("aaa\n")
(work / "b.txt").write_text("bbb\n")
fps = gitlocal.file_fingerprints(str(work), ["a.txt", "b.txt", "gone.txt"])
assert fps["a.txt"] and fps["b.txt"]
assert fps["a.txt"] != fps["b.txt"]
assert fps["gone.txt"] == ""
(work / "c.txt").write_text("aaa\n")
assert gitlocal.file_fingerprints(str(work), ["c.txt"])["c.txt"] == fps["a.txt"]
@pytest.mark.parametrize("branch", ["main", "master", "develop", "trunk", "", " "])
def test_assert_not_protected_rejects_base_and_empty(branch: str) -> None:
with pytest.raises(GitCommandError) as exc:
gitlocal.assert_not_protected(branch, protected_extra="main")
assert exc.value.kind == PROTECTED_BRANCH
def test_assert_not_protected_rejects_resolved_default() -> None:
with pytest.raises(GitCommandError):
gitlocal.assert_not_protected("release-1.0", protected_extra="release-1.0")
def test_create_branch_refuses_protected(tmp_path: Path) -> None:
work = _init_repo(tmp_path)
with pytest.raises(GitCommandError) as exc:
gitlocal.create_branch(str(work), "main", base_default="main")
assert exc.value.kind == PROTECTED_BRANCH
assert gitlocal.current_branch(str(work)) == "main" # still on base
def test_checkout_branch_switches_to_existing_branch(tmp_path: Path) -> None:
work = _init_repo(tmp_path)
gitlocal.create_branch(str(work), "feature-a", base_default="main")
gitlocal.checkout_branch(str(work), "main")
assert gitlocal.current_branch(str(work)) == "main"
gitlocal.checkout_branch(str(work), "feature-a")
assert gitlocal.current_branch(str(work)) == "feature-a"
def test_checkout_branch_raises_for_missing_branch(tmp_path: Path) -> None:
work = _init_repo(tmp_path)
with pytest.raises(GitCommandError) as exc:
gitlocal.checkout_branch(str(work), "does-not-exist")
assert exc.value.kind == BRANCH_FAILED
def test_branch_commit_push_roundtrip(tmp_path: Path) -> None:
work = _init_repo(tmp_path)
(work / "app").mkdir()
(work / "app" / "handlers.py").write_text("x = 1\n")
branch = "opensre/sentry-fix-12345-abc"
gitlocal.create_branch(str(work), branch, base_default="main")
gitlocal.commit_paths(str(work), ["app/handlers.py"], "fix: something")
gitlocal.push_branch(str(work), branch, base_default="main")
assert gitlocal.current_branch(str(work)) == branch
remote_branches = subprocess.run(
["git", "branch", "-r"], cwd=work, capture_output=True, text=True
).stdout
assert f"origin/{branch}" in remote_branches
def test_commit_paths_isolates_to_given_files(tmp_path: Path) -> None:
work = _init_repo(tmp_path)
(work / "other.txt").write_text("orig\n")
_git(work, "add", "other.txt")
_git(work, "commit", "-m", "add other")
(work / "app").mkdir()
(work / "app" / "handlers.py").write_text("fix\n")
(work / "README.md").write_text("wip unstaged\n")
(work / "other.txt").write_text("wip staged\n")
_git(work, "add", "other.txt")
gitlocal.commit_paths(str(work), ["app/handlers.py"], "fix: only that file")
committed = subprocess.run(
["git", "show", "--name-only", "--pretty=format:", "HEAD"],
cwd=work,
capture_output=True,
text=True,
).stdout.split()
assert committed == ["app/handlers.py"]
status = subprocess.run(
["git", "status", "--porcelain"], cwd=work, capture_output=True, text=True
).stdout
assert "README.md" in status
assert "other.txt" in status
def test_token_auth_env_injects_host_scoped_header_without_leaking_token(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("GIT_CONFIG_COUNT", raising=False)
env = gitlocal._token_auth_env("secret-token", "https://github.com/")
assert env["GIT_CONFIG_KEY_0"] == "http.https://github.com/.extraheader"
header = env["GIT_CONFIG_VALUE_0"]
assert header.startswith("Authorization: Basic ")
assert "secret-token" not in header
def test_token_auth_env_preserves_existing_git_config(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("GIT_CONFIG_COUNT", "1")
monkeypatch.setenv("GIT_CONFIG_KEY_0", "user.name")
monkeypatch.setenv("GIT_CONFIG_VALUE_0", "Someone")
env = gitlocal._token_auth_env("tok", "https://github.com/")
assert env["GIT_CONFIG_COUNT"] == "2"
assert env["GIT_CONFIG_KEY_0"] == "user.name" # preserved
assert env["GIT_CONFIG_VALUE_0"] == "Someone"
assert env["GIT_CONFIG_KEY_1"] == "http.https://github.com/.extraheader" # appended
def test_default_branch_falls_back_to_remote_head(tmp_path: Path) -> None:
work = _init_repo(tmp_path)
bare = tmp_path / "remote.git"
_git(work, "remote", "set-head", "origin", "--delete")
subprocess.run(
["git", "--git-dir", str(bare), "symbolic-ref", "HEAD", "refs/heads/main"],
check=True,
capture_output=True,
text=True,
)
_git(work, "checkout", "-b", "feature-x")
assert gitlocal.default_branch(str(work)) == "main"
def test_default_branch_empty_when_unresolvable(tmp_path: Path) -> None:
work = _init_repo(tmp_path)
# No local origin/HEAD and an unreachable remote -> cannot resolve.
_git(work, "remote", "set-head", "origin", "--delete")
_git(work, "remote", "set-url", "origin", str(tmp_path / "does-not-exist.git"))
_git(work, "checkout", "-b", "feature-x")
# Returns "" rather than guessing the current feature branch.
assert gitlocal.default_branch(str(work)) == ""
def _fake_run_git_factory(captured: dict[str, Any], origin_url: str):
def _fake(_ws: str, *args: str, env: Any = None, timeout: Any = None) -> Any:
if args[:2] == ("remote", "get-url"):
return subprocess.CompletedProcess(list(args), 0, f"{origin_url}\n", "")
captured["args"] = args
captured["env"] = env
return subprocess.CompletedProcess(list(args), 0, "", "")
return _fake
def test_push_branch_scopes_token_header_to_https_origin_host(tmp_path: Path) -> None:
work = _init_repo(tmp_path)
captured: dict[str, Any] = {}
with patch.object(
gitlocal, "_run_git", _fake_run_git_factory(captured, "https://github.com/acme/app.git")
):
gitlocal.push_branch(str(work), "opensre/sentry-fix-1-x", base_default="main", token="tok")
assert captured["args"][0] == "push"
env = captured["env"]
assert env is not None
count = int(env["GIT_CONFIG_COUNT"])
keys = [env[f"GIT_CONFIG_KEY_{i}"] for i in range(count)]
assert "http.https://github.com/.extraheader" in keys
assert "http.extraheader" not in keys
@pytest.mark.parametrize(
"origin_url",
[
"git@github.com:acme/app.git", # SSH
"http://gitea.local/acme/app.git", # plaintext HTTP -> never send the token
"/srv/repos/app.git", # local/file
],
)
def test_push_branch_skips_token_header_for_non_https_origin(
tmp_path: Path, origin_url: str
) -> None:
work = _init_repo(tmp_path)
captured: dict[str, Any] = {}
with patch.object(gitlocal, "_run_git", _fake_run_git_factory(captured, origin_url)):
gitlocal.push_branch(str(work), "opensre/sentry-fix-1-x", base_default="main", token="tok")
assert captured["env"] is None
def test_push_branch_no_env_without_token(tmp_path: Path) -> None:
work = _init_repo(tmp_path)
captured: dict[str, Any] = {}
def _fake_run_git(_ws: str, *args: str, env: Any = None, timeout: Any = None) -> Any:
captured["env"] = env
return subprocess.CompletedProcess(list(args), 0, "", "")
with patch.object(gitlocal, "_run_git", _fake_run_git):
gitlocal.push_branch(str(work), "opensre/sentry-fix-1-x", base_default="main")
assert captured["env"] is None