phase: 136_force_push_reset
Everything is verified green and consistent. Final report: **Phase 136 — final verification pass (all tasks already complete in `complete/136_force_push_reset/`):** - **Verified implemented:** unified ff-or-reset update sequence in `scripts/git_sync.py::clone_or_pull` (phase-135 HEAD guard → shallow self-heal → `fetch origin <ref>` → `merge --ff-only` → on non-ff: SHA capture + `reset --hard FETCH_HEAD` + one WARNING line, returns normally); detached-HEAD fail-loud; `branch=None` legacy `git pull` deliberately gone - **Completion criteria — all met:** 1. ✅ Force push no longer fails sync (integration `test_clone_or_pull_force_push_resets_to_the_new_head` + API-seam `test_sync_pipeline_survives_a_force_push` + E2E run 2 asserts `success` and KB serves the V2 sentinel) 2. ✅ Unrelated-history force push handled by the same reset (`test_clone_or_pull_unrelated_history_force_push_resets`) 3. ✅ Broken fetch still fails loud (fetch-failure unit pins raise with no merge/reset argv; `test_main_git_failure_aborts_before_import`) 4. ✅ WARNING line pinned: one line, 7-char SHAs, checkout path, `origin/<ref>`, no URL (unit exact-string pin + integration caplog pin) 5. ✅ `uv run pytest --cov=app --cov-report=term-missing` → **3231 passed, app/ 99%**; `uv run pytest tests/e2e/test_force_push_resync.py -v --no-cov` → **2 passed in isolation** (10.6 s) 6. ✅ Phase-135 pins preserved (mismatch guard, `--branch` clone argv, `branch=None` fresh-clone argv all green in the 53-test run) - **Test/lint results:** `uv run pytest tests/unit/test_git_sync.py tests/integration/test_import_docs_git.py -v` → 53 passed; full suite 3231 passed (72 s); `uv run ruff check . && uv run pyright` → clean (0 errors) - **Documentation:** README.md git-sources section already updated this phase to the ff-or-reset contract; all `.env.example` vars documented; pages table matches router; AGENTS.md rules and quick-reference commands verified accurate (rule 5 correctly documents the phase-100 §7.1 supersession) — **no changes needed** - **Notable:** E2E "teeth" verification (fails on pre-phase code) is recorded in the task report; nothing was committed per harness protocol - **Next pending phase:** `todo/137_pdf_image_context`
This commit is contained in:
@@ -33,25 +33,46 @@ and the ``_resolve_sources`` seam with a ``branch='release'`` row
|
||||
(importing file B, not file A) + an env-origin row without a branch
|
||||
(the default branch, byte-identical pre-phase behavior). Skipped, not
|
||||
failed, on a machine without the git CLI (the house pattern).
|
||||
|
||||
Phase 136 (the ff-or-reset) extends the REAL-git section: the owner's
|
||||
force-push repro (the origin's history REWRITTEN to a new root commit
|
||||
between two ``clone_or_pull`` runs — normal divergence AND fully
|
||||
unrelated history) now resets the checkout to the origin's new head
|
||||
instead of failing (the loud WARNING line pinned alongside), a
|
||||
branched row's force push lands ``origin/<branch>``'s head (main's
|
||||
content not involved), ``file_commit_dates`` over the post-reset
|
||||
checkout reflects the NEW head's dates, and the SYNC PIPELINE END-
|
||||
TO-END over the API seam (``POST /api/sync`` + the status poll — the
|
||||
seam ``test_sync_api.py`` exercises) lands ``success`` after a force
|
||||
push, with the checkout (and therefore the KB) holding the new head.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from collections.abc import Iterator
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api import sync as sync_api
|
||||
from app.config import Settings
|
||||
from app.db import SessionLocal
|
||||
from app.main import app as fastapi_app
|
||||
from app.models import GitSource
|
||||
from app.rag import git_sources as git_sources_resolver
|
||||
from app.rag.importer import ImportSummary
|
||||
from scripts import import_docs
|
||||
from scripts.git_sync import GitSyncError, clone_or_pull
|
||||
from scripts.git_sync import GitSyncError, clone_or_pull, file_commit_dates
|
||||
from tests.conftest import ADMIN_PASSWORD
|
||||
from tests.fakes import FakeEmbedder
|
||||
|
||||
|
||||
@@ -788,14 +809,22 @@ def two_branch_repo(tmp_path: Path) -> Path:
|
||||
return repo
|
||||
|
||||
|
||||
def _git(cwd: Path, *argv: str) -> None:
|
||||
def _git(cwd: Path, *argv: str, date: str | None = None) -> None:
|
||||
"""Run one git command for the test harness (fixture setup); a
|
||||
non-zero exit fails the fixture, not the test under test."""
|
||||
non-zero exit fails the fixture, not the test under test.
|
||||
|
||||
``date`` pins the commit date (``GIT_COMMITTER_DATE`` =
|
||||
``GIT_AUTHOR_DATE``) — deterministic per-file dates for the
|
||||
phase-136 ``file_commit_dates`` pin (scratch commits would
|
||||
otherwise share the wall clock's second)."""
|
||||
env = os.environ.copy()
|
||||
env["GIT_AUTHOR_NAME"] = "T"
|
||||
env["GIT_AUTHOR_EMAIL"] = "t@example.com"
|
||||
env["GIT_COMMITTER_NAME"] = "T"
|
||||
env["GIT_COMMITTER_EMAIL"] = "t@example.com"
|
||||
if date is not None:
|
||||
env["GIT_AUTHOR_DATE"] = date
|
||||
env["GIT_COMMITTER_DATE"] = date
|
||||
proc = subprocess.run(
|
||||
["git", *argv], cwd=cwd, env=env, capture_output=True, text=True, check=False
|
||||
)
|
||||
@@ -947,3 +976,358 @@ def test_resolve_sources_row_without_branch_clones_default_branch(
|
||||
assert sources == [root]
|
||||
assert (root / "a.md").is_file() # main's file — the default branch
|
||||
assert not (root / "b.md").exists()
|
||||
|
||||
|
||||
# --- phase 136: the ff-or-reset (real git, the force-push repro) -----------
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def single_branch_repo(tmp_path: Path) -> Path:
|
||||
"""A real-git scratch origin on ``main`` with one file (``a.md``)
|
||||
(the phase-136 base shape — the owner's single-branch source;
|
||||
the root commit's date is pinned for the deterministic
|
||||
``file_commit_dates`` pin below)."""
|
||||
repo = tmp_path / "origin"
|
||||
repo.mkdir()
|
||||
_git(repo, "init", "-q", "-b", "main")
|
||||
_git(repo, "config", "commit.gpgsign", "false")
|
||||
(repo / "a.md").write_text("# A\noriginal content\n", encoding="utf-8")
|
||||
_git(repo, "add", "a.md")
|
||||
_git(repo, "commit", "-q", "-m", "root: add a.md", date="2026-01-05T10:00:00+00:00")
|
||||
return repo
|
||||
|
||||
|
||||
def _force_push_new_root(
|
||||
origin: Path,
|
||||
files: dict[str, str | None],
|
||||
message: str,
|
||||
*,
|
||||
branch: str = "main",
|
||||
date: str | None = None,
|
||||
) -> None:
|
||||
"""Rewrite the origin's ``branch`` to a NEW ROOT commit (the
|
||||
phase-136 force push): ``git checkout --orphan`` + commit, so the
|
||||
branch now points at a root with NO shared ancestry with the old
|
||||
history — the strongest divergence, exactly what ``--ff-only``
|
||||
refuses. ``files`` maps name → new content (the orphan commit
|
||||
holds exactly that set — the old files are dropped)."""
|
||||
_git(origin, "checkout", "--orphan", "rewritten")
|
||||
for p in sorted(origin.iterdir()):
|
||||
if p.is_file() and p.name != ".git":
|
||||
p.unlink()
|
||||
for name, content in files.items():
|
||||
if content is not None:
|
||||
(origin / name).write_text(content, encoding="utf-8")
|
||||
_git(origin, "add", "-A")
|
||||
_git(origin, "commit", "-q", "-m", message, date=date)
|
||||
_git(origin, "branch", "-D", branch)
|
||||
_git(origin, "branch", "-m", "rewritten", branch)
|
||||
_git(origin, "checkout", "-q", "main")
|
||||
|
||||
|
||||
@pytest.mark.skipif(not GIT, reason="git CLI not available")
|
||||
def test_clone_or_pull_normal_ff_updates_without_reset(
|
||||
tmp_path: Path, single_branch_repo: Path
|
||||
) -> None:
|
||||
"""Phase 136: a normal second commit (a true fast-forward) → the
|
||||
unified sequence ff-merges: the checkout's HEAD is the origin's
|
||||
head, the second commit's message is the HEAD subject, NO merge
|
||||
commit (a pure ff), and the working tree holds the new content —
|
||||
the reset tail never runs on a clean ff."""
|
||||
origin = single_branch_repo
|
||||
url = f"file://{origin}"
|
||||
dest = tmp_path / "bor" / "origin"
|
||||
|
||||
clone_or_pull(url, dest)
|
||||
(origin / "b.md").write_text("# B\nsecond commit\n", encoding="utf-8")
|
||||
_git(origin, "add", "b.md")
|
||||
_git(origin, "commit", "-q", "-m", "second: add b.md", date="2026-02-01T09:00:00+00:00")
|
||||
|
||||
clone_or_pull(url, dest)
|
||||
|
||||
dest_head = _git_out(dest, "rev-parse", "HEAD").strip()
|
||||
origin_head = _git_out(origin, "rev-parse", "main").strip()
|
||||
assert dest_head == origin_head
|
||||
subjects = _git_out(dest, "log", "--format=%s").splitlines()
|
||||
assert subjects[0] == "second: add b.md" # the second commit's message is the HEAD
|
||||
assert subjects == ["second: add b.md", "root: add a.md"] # pure ff: no merge commit
|
||||
assert (dest / "b.md").read_text(encoding="utf-8") == "# B\nsecond commit\n"
|
||||
|
||||
|
||||
@pytest.mark.skipif(not GIT, reason="git CLI not available")
|
||||
def test_clone_or_pull_force_push_resets_to_the_new_head(
|
||||
tmp_path: Path, single_branch_repo: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Phase 136 (the owner's repro, TODO L5): after the first clone,
|
||||
the origin's history is REWRITTEN to a NEW ROOT commit (same
|
||||
file, new content — the branch now diverges; pre-phase, the next
|
||||
sync died on the non-ff ``git pull --ff-only`` → the whole sync
|
||||
landed ``failed``) → ``clone_or_pull`` NO LONGER raises: the
|
||||
checkout is reset to the origin's NEW head (the checkout's HEAD ==
|
||||
the origin's head), the working tree holds the NEW content, and
|
||||
ONE WARNING line (PLAN §9) is the only side effect — no
|
||||
user-facing error (the log carries the checkout path, never the
|
||||
URL)."""
|
||||
origin = single_branch_repo
|
||||
url = f"file://{origin}"
|
||||
dest = tmp_path / "bor" / "origin"
|
||||
new_content = "# A\nrewritten content\n"
|
||||
|
||||
clone_or_pull(url, dest)
|
||||
_force_push_new_root(
|
||||
origin, {"a.md": new_content}, "force: rewritten root",
|
||||
date="2026-03-10T12:00:00+00:00",
|
||||
)
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="scripts.git_sync"):
|
||||
result = clone_or_pull(url, dest) # NO exception (the pre-phase break)
|
||||
|
||||
assert result == dest
|
||||
dest_head = _git_out(dest, "rev-parse", "HEAD").strip()
|
||||
origin_head = _git_out(origin, "rev-parse", "main").strip()
|
||||
assert dest_head == origin_head
|
||||
assert (dest / "a.md").read_text(encoding="utf-8") == new_content
|
||||
# ONE WARNING line: the checkout path + the tracked ref, never the URL.
|
||||
warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
|
||||
assert len(warnings) == 1
|
||||
msg = warnings[0].getMessage()
|
||||
assert "git sync: divergent remote head on " in msg
|
||||
assert str(dest) in msg
|
||||
assert "to origin/main" in msg
|
||||
assert url not in msg # credential-safety: the log never carries the URL
|
||||
|
||||
|
||||
@pytest.mark.skipif(not GIT, reason="git CLI not available")
|
||||
def test_clone_or_pull_unrelated_history_force_push_resets(
|
||||
tmp_path: Path, single_branch_repo: Path
|
||||
) -> None:
|
||||
"""Phase 136: the rewritten origin has a DIFFERENT root commit
|
||||
with a fully UNRELATED tree (a different file, NO shared ancestry
|
||||
— the ff merge of an unrelated history is exactly what
|
||||
``--ff-only`` refuses) → the SAME reset path handles it: the
|
||||
checkout's HEAD is the origin's new head and the working tree IS
|
||||
the new tree (the old file GONE — the reset is a hard one, not a
|
||||
merge)."""
|
||||
origin = single_branch_repo
|
||||
url = f"file://{origin}"
|
||||
dest = tmp_path / "bor" / "origin"
|
||||
|
||||
clone_or_pull(url, dest)
|
||||
_force_push_new_root(
|
||||
origin, {"z.md": "# Z\nunrelated root\n"}, "force: unrelated root",
|
||||
date="2026-03-11T12:00:00+00:00",
|
||||
)
|
||||
|
||||
result = clone_or_pull(url, dest) # NO exception — the same reset path
|
||||
|
||||
assert result == dest
|
||||
dest_head = _git_out(dest, "rev-parse", "HEAD").strip()
|
||||
origin_head = _git_out(origin, "rev-parse", "main").strip()
|
||||
assert dest_head == origin_head
|
||||
assert (dest / "z.md").read_text(encoding="utf-8") == "# Z\nunrelated root\n"
|
||||
assert not (dest / "a.md").exists() # the hard reset dropped the old tree's file
|
||||
|
||||
|
||||
@pytest.mark.skipif(not GIT, reason="git CLI not available")
|
||||
def test_clone_or_pull_branched_force_push_resets_to_the_branch_head(
|
||||
tmp_path: Path, two_branch_repo: Path
|
||||
) -> None:
|
||||
"""Phase 136 + phase 135: a two-branch origin (the phase-135
|
||||
shape), a row-shaped call ``clone_or_pull(url, dest,
|
||||
"release")`` after a force push of ONLY ``release`` → the reset
|
||||
lands ``origin/release``'s head (the owner's verbatim "reset
|
||||
origin/<branch>") and ``main``'s content is not involved (file A
|
||||
is still absent — the checkout holds the branch's tree only)."""
|
||||
origin = two_branch_repo
|
||||
url = f"file://{origin}"
|
||||
dest = tmp_path / "bor" / "origin"
|
||||
forced = "# B\nrelease branch content FORCED\n"
|
||||
|
||||
clone_or_pull(url, dest, "release")
|
||||
_force_push_new_root(
|
||||
origin, {"b.md": forced}, "release: force-pushed root",
|
||||
branch="release", date="2026-03-15T08:00:00+00:00",
|
||||
)
|
||||
|
||||
clone_or_pull(url, dest, "release") # NO exception — the reset landed
|
||||
|
||||
assert _git_out(dest, "rev-parse", "--abbrev-ref", "HEAD").strip() == "release"
|
||||
dest_head = _git_out(dest, "rev-parse", "HEAD").strip()
|
||||
release_head = _git_out(origin, "rev-parse", "release").strip()
|
||||
assert dest_head == release_head
|
||||
assert (dest / "b.md").read_text(encoding="utf-8") == forced
|
||||
assert not (dest / "a.md").exists() # main's content is not involved
|
||||
|
||||
|
||||
@pytest.mark.skipif(not GIT, reason="git CLI not available")
|
||||
def test_file_commit_dates_after_reset_reflects_the_new_head(
|
||||
tmp_path: Path, single_branch_repo: Path
|
||||
) -> None:
|
||||
"""Phase 136: ``file_commit_dates`` over the post-reset checkout
|
||||
yields the NEW head's per-file dates — the new root commit's date
|
||||
is what the walk returns for its files (the head-of-branch dates
|
||||
the owner asked for: the KB is rebuilt from the reset checkout on
|
||||
the same run, so its document dates are the NEW history's, not
|
||||
the old one's)."""
|
||||
origin = single_branch_repo
|
||||
url = f"file://{origin}"
|
||||
dest = tmp_path / "bor" / "origin"
|
||||
|
||||
clone_or_pull(url, dest)
|
||||
old_dates = file_commit_dates(dest)
|
||||
assert old_dates["a.md"] == datetime.fromisoformat("2026-01-05T10:00:00+00:00")
|
||||
|
||||
_force_push_new_root(
|
||||
origin, {"a.md": "# A\nrewritten content\n"}, "force: rewritten root",
|
||||
date="2026-03-10T12:00:00+00:00",
|
||||
)
|
||||
clone_or_pull(url, dest)
|
||||
|
||||
new_dates = file_commit_dates(dest)
|
||||
assert set(new_dates) == {"a.md"} # the new tree's files only
|
||||
assert new_dates["a.md"] == datetime.fromisoformat("2026-03-10T12:00:00+00:00")
|
||||
assert new_dates["a.md"] != old_dates["a.md"] # the NEW head dates the file
|
||||
|
||||
|
||||
# --- phase 136: the sync pipeline end to end (the API seam) ---------------
|
||||
|
||||
|
||||
def _reset_sources_meta() -> None:
|
||||
"""Pin the single-row ``sources_meta`` counter to the
|
||||
migration-0010 seed (0) — the runner bumps it through its own
|
||||
short-lived ``SessionLocal`` (the ``test_sync_api`` pattern)."""
|
||||
session = SessionLocal()
|
||||
try:
|
||||
session.execute(text("UPDATE sources_meta SET version = 0 WHERE id = 1"))
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def fresh_sync_state() -> Iterator[None]:
|
||||
"""Phase 136 (the API-seam pin): the sync runner's module-level
|
||||
status object + task + cancel event are process-global — reset
|
||||
them around the test (the ``test_sync_api`` autouse pattern, the
|
||||
full suite runs both files in one process)."""
|
||||
sync_api._status = sync_api.SyncStatus()
|
||||
sync_api._task = None
|
||||
sync_api._cancel_event = asyncio.Event()
|
||||
yield
|
||||
sync_api._status = sync_api.SyncStatus()
|
||||
sync_api._task = None
|
||||
sync_api._cancel_event = asyncio.Event()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def clean_sync_seam(db: Session) -> Iterator[None]:
|
||||
"""Phase 136 (the API-seam pin): the real sync runner writes
|
||||
global tables — ``git_sources`` (seeded), ``documents``/
|
||||
``chunks`` (the real import), and ``folder_summaries`` (the real
|
||||
generator) — truncated around the test; the ``sources_meta``
|
||||
counter is pinned to 0 before and after. Skips like the ``db``
|
||||
fixture when Postgres is down."""
|
||||
db.execute(text("TRUNCATE git_sources, chunks, documents, folder_summaries"))
|
||||
db.commit()
|
||||
_reset_sources_meta()
|
||||
yield
|
||||
db.execute(text("TRUNCATE git_sources, chunks, documents, folder_summaries"))
|
||||
db.commit()
|
||||
_reset_sources_meta()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def sync_client() -> Iterator[TestClient]:
|
||||
"""Context-managed TestClient — one app event loop across
|
||||
requests (the background sync task must survive between the POST
|
||||
and the polls — the ``test_sync_api`` pattern; a TestClient
|
||||
without the context manager would cancel the task on request
|
||||
exit)."""
|
||||
with TestClient(fastapi_app) as client:
|
||||
yield client
|
||||
|
||||
|
||||
def _poll_sync(client: TestClient, want: str, timeout: float = 10.0) -> dict:
|
||||
"""Poll ``GET /api/sync/status`` until ``state == want``
|
||||
(terminal). Any state other than ``running`` before the deadline
|
||||
fails loudly — an unexpected ``failed`` must never be masked by
|
||||
the wait (the pre-phase force push landed exactly that)."""
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
body = client.get("/api/sync/status").json()
|
||||
if body["state"] == want:
|
||||
return body
|
||||
assert body["state"] == "running", (
|
||||
f"unexpected state {body['state']!r} while waiting for {want!r}: {body}"
|
||||
)
|
||||
time.sleep(0.05)
|
||||
raise AssertionError(f"sync did not reach {want!r} within {timeout}s")
|
||||
|
||||
|
||||
@pytest.mark.skipif(not GIT, reason="git CLI not available")
|
||||
def test_sync_pipeline_survives_a_force_push(
|
||||
sync_client: TestClient,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
db: Session,
|
||||
tmp_path: Path,
|
||||
single_branch_repo: Path,
|
||||
fresh_sync_state: None,
|
||||
clean_sync_seam: None,
|
||||
) -> None:
|
||||
"""Phase 136, the SYNC PIPELINE END-TO-END (the seam
|
||||
``tests/integration/test_sync_api.py`` already exercises — the
|
||||
pre-phase force push landed the button sync in ``failed``; this
|
||||
test is the API-seam regression pin that it now lands
|
||||
``success``): a real-git ``file://`` origin + a seeded git row;
|
||||
sync #1 clones and imports the original content; the origin's
|
||||
history is REWRITTEN to a new root commit (the owner's exact
|
||||
repro); sync #2 → ``state == "success"`` (NOT ``failed``), the
|
||||
rewritten file is re-imported (``updated=1``), and the checkout
|
||||
(and therefore the KB) holds the NEW head. The REAL
|
||||
``clone_or_pull`` runs (no fake — the reset is what is under
|
||||
test) over the real import + real overview/folder-summary steps
|
||||
(deterministic in-process ``FakeEmbedder``, no network)."""
|
||||
origin = single_branch_repo
|
||||
url = f"file://{origin}"
|
||||
db.add(GitSource(url=url, kind="git"))
|
||||
db.commit()
|
||||
# The resolver's env fallback pinned empty (the DB row wins — the
|
||||
# dev ``.env`` never leaks in; the ``test_sync_api`` pattern).
|
||||
monkeypatch.setattr(
|
||||
git_sources_resolver,
|
||||
"get_settings",
|
||||
lambda: Settings(_env_file=None, git_sources=""), # pyright: ignore[reportCallIssue]
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
sync_api,
|
||||
"get_settings",
|
||||
lambda: Settings(_env_file=None, sources_dir=str(tmp_path / "bor")), # pyright: ignore[reportCallIssue]
|
||||
)
|
||||
monkeypatch.setattr(sync_api, "LLMClient", lambda: FakeEmbedder()) # probe passes against it
|
||||
|
||||
r = sync_client.post("/api/login", json={"password": ADMIN_PASSWORD})
|
||||
assert r.status_code == 204, f"admin login failed: {r.status_code} {r.text}"
|
||||
|
||||
# Sync #1: the fresh clone imports the original content.
|
||||
assert sync_client.post("/api/sync").status_code == 202
|
||||
body1 = _poll_sync(sync_client, "success")
|
||||
assert body1["detail"]["added"] == 1
|
||||
|
||||
# The owner's repro: force push the origin (a NEW ROOT commit).
|
||||
new_content = "# A\nrewritten content\n"
|
||||
_force_push_new_root(origin, {"a.md": new_content}, "force: rewritten root")
|
||||
|
||||
# Sync #2: pre-phase this landed ``failed`` (the non-ff
|
||||
# ``git pull --ff-only`` died) — the reset is now self-healing
|
||||
# and the run SUCCEEDS.
|
||||
assert sync_client.post("/api/sync").status_code == 202
|
||||
body2 = _poll_sync(sync_client, "success") # NOT ``failed`` — the regression pin
|
||||
assert body2["error"] is None
|
||||
assert body2["detail"]["updated"] == 1 # the rewritten file is re-imported
|
||||
|
||||
# The checkout (and therefore the KB) holds the NEW head.
|
||||
root = tmp_path / "bor" / "origin"
|
||||
root_head = _git_out(root, "rev-parse", "HEAD").strip()
|
||||
origin_head = _git_out(origin, "rev-parse", "main").strip()
|
||||
assert root_head == origin_head
|
||||
assert (root / "a.md").read_text(encoding="utf-8") == new_content
|
||||
|
||||
Reference in New Issue
Block a user