|
#!/usr/bin/env -S uv run --script |
|
# /// script |
|
# requires-python = ">=3.12" |
|
# /// |
|
"""Test for jj-rebase-rename.py. |
|
|
|
Creates temporary jj repos with rename scenarios and verifies the script works. |
|
""" |
|
|
|
import os |
|
import subprocess |
|
import sys |
|
import tempfile |
|
|
|
|
|
SCRIPT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "jj-rebase-rename.py") |
|
|
|
|
|
def jj(*args: str, cwd: str) -> str: |
|
result = subprocess.run(["jj", *args], capture_output=True, text=True, check=True, cwd=cwd) |
|
return result.stdout.strip() |
|
|
|
|
|
def jj_no_wc(*args: str, cwd: str) -> str: |
|
result = subprocess.run(["jj", "--ignore-working-copy", *args], capture_output=True, text=True, check=True, cwd=cwd) |
|
return result.stdout.strip() |
|
|
|
|
|
def write_file(repo: str, name: str, content: str) -> None: |
|
path = os.path.join(repo, name) |
|
os.makedirs(os.path.dirname(path) if os.path.dirname(path) else repo, exist_ok=True) |
|
with open(path, "w") as f: |
|
f.write(content) |
|
|
|
|
|
def read_file(repo: str, name: str) -> str: |
|
path = os.path.join(repo, name) |
|
with open(path) as f: |
|
return f.read() |
|
|
|
|
|
def run_script(repo: str, *args: str) -> subprocess.CompletedProcess: |
|
result = subprocess.run( |
|
[sys.executable, SCRIPT, *args], |
|
cwd=repo, |
|
capture_output=True, |
|
text=True, |
|
) |
|
print("STDOUT:", result.stdout) |
|
if result.stderr: |
|
print("STDERR:", result.stderr) |
|
return result |
|
|
|
|
|
def check_no_conflicts(repo: str) -> list[str]: |
|
log_output = jj_no_wc("log", "-r", "all()", "--no-graph", "-T", |
|
r'if(conflict, change_id ++ " CONFLICT\n", "")', cwd=repo) |
|
if "CONFLICT" in log_output: |
|
return [f"Conflicts found: {log_output}"] |
|
return [] |
|
|
|
|
|
def make_immutable(repo: str, change_id: str) -> None: |
|
config_path = os.path.join(repo, ".jj", "repo", "config.toml") |
|
with open(config_path, "a") as f: |
|
f.write(f'\n[revset-aliases]\n"immutable_heads()" = "{change_id} | trunk()"\n') |
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
# Test: basic rename scenario |
|
# --------------------------------------------------------------------------- |
|
|
|
def test_basic_rename() -> bool: |
|
"""Files A,B,C renamed to D,E,F on trunk. Branch modifies A and B.""" |
|
with tempfile.TemporaryDirectory() as tmpdir: |
|
repo = os.path.join(tmpdir, "testrepo") |
|
os.makedirs(repo) |
|
|
|
jj("git", "init", cwd=repo) |
|
|
|
# Base commit with initial files |
|
write_file(repo, "A.txt", "initial A content\n") |
|
write_file(repo, "B.txt", "initial B content\n") |
|
write_file(repo, "C.txt", "initial C content\n") |
|
jj("describe", "-m", "initial commit", cwd=repo) |
|
base = jj_no_wc("log", "-r", "@", "--no-graph", "-T", "change_id", cwd=repo) |
|
make_immutable(repo, base) |
|
|
|
# Feature branch: two commits modifying A and B |
|
jj("new", "-m", "feature: modify A and B", cwd=repo) |
|
write_file(repo, "A.txt", "initial A content\nfeature line in A\n") |
|
write_file(repo, "B.txt", "initial B content\nfeature line in B\n") |
|
feature1 = jj_no_wc("log", "-r", "@", "--no-graph", "-T", "change_id", cwd=repo) |
|
|
|
jj("new", "-m", "feature: more changes to A", cwd=repo) |
|
write_file(repo, "A.txt", "initial A content\nfeature line in A\nsecond feature line\n") |
|
feature2 = jj_no_wc("log", "-r", "@", "--no-graph", "-T", "change_id", cwd=repo) |
|
|
|
# Trunk: rename A->D, B->E, C->F |
|
jj("new", base, "-m", "rename files", cwd=repo) |
|
os.rename(os.path.join(repo, "A.txt"), os.path.join(repo, "D.txt")) |
|
os.rename(os.path.join(repo, "B.txt"), os.path.join(repo, "E.txt")) |
|
os.rename(os.path.join(repo, "C.txt"), os.path.join(repo, "F.txt")) |
|
trunk = jj_no_wc("log", "-r", "@", "--no-graph", "-T", "change_id", cwd=repo) |
|
jj("bookmark", "set", "trunk", "-r", trunk, cwd=repo) |
|
|
|
# Go back to feature head |
|
jj("edit", feature2, cwd=repo) |
|
|
|
print("\nBefore:") |
|
print(jj_no_wc("log", "-r", "all()", cwd=repo)) |
|
|
|
# Run script |
|
result = run_script(repo, "--rename", "A.txt:D.txt", "--rename", "B.txt:E.txt", |
|
"--rename", "C.txt:F.txt", "--onto", "trunk") |
|
if result.returncode != 0: |
|
print("FAILED: script exited with non-zero") |
|
return False |
|
|
|
print("\nAfter:") |
|
print(jj_no_wc("log", "-r", "all()", cwd=repo)) |
|
|
|
errors = check_no_conflicts(repo) |
|
|
|
jj("workspace", "update-stale", cwd=repo) |
|
jj("edit", feature2, cwd=repo) |
|
|
|
content = read_file(repo, "D.txt") |
|
if "feature line in A" not in content or "second feature line" not in content: |
|
errors.append(f"D.txt missing feature content: {content!r}") |
|
|
|
content = read_file(repo, "E.txt") |
|
if "feature line in B" not in content: |
|
errors.append(f"E.txt missing feature content: {content!r}") |
|
|
|
desc1 = jj_no_wc("log", "-r", feature1, "--no-graph", "-T", "description", cwd=repo) |
|
if "feature: modify A and B" not in desc1: |
|
errors.append(f"Feature1 description lost: {desc1!r}") |
|
|
|
desc2 = jj_no_wc("log", "-r", feature2, "--no-graph", "-T", "description", cwd=repo) |
|
if "feature: more changes to A" not in desc2: |
|
errors.append(f"Feature2 description lost: {desc2!r}") |
|
|
|
if errors: |
|
print("\nFAILED:") |
|
for e in errors: |
|
print(f" - {e}") |
|
return False |
|
return True |
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
# Test: later commit doesn't touch renamed files (inherited tree) |
|
# --------------------------------------------------------------------------- |
|
|
|
def test_inherited_files_not_conflicting() -> bool: |
|
"""A later commit modifies only one renamed file. The other renamed file |
|
is inherited in the tree but not changed by the commit. This must not |
|
produce spurious conflicts for the inherited file.""" |
|
with tempfile.TemporaryDirectory() as tmpdir: |
|
repo = os.path.join(tmpdir, "testrepo") |
|
os.makedirs(repo) |
|
|
|
jj("git", "init", cwd=repo) |
|
|
|
# Base: two files under sub/ |
|
write_file(repo, "sub/foo.rb", "foo original\n") |
|
write_file(repo, "sub/bar.rb", "bar original\n") |
|
write_file(repo, "other.txt", "other\n") |
|
jj("describe", "-m", "initial commit", cwd=repo) |
|
base = jj_no_wc("log", "-r", "@", "--no-graph", "-T", "change_id", cwd=repo) |
|
make_immutable(repo, base) |
|
|
|
# Feature: commit 1 modifies both files |
|
jj("new", "-m", "modify foo and bar", cwd=repo) |
|
write_file(repo, "sub/foo.rb", "foo original\nfoo feature\n") |
|
write_file(repo, "sub/bar.rb", "bar original\nbar feature\n") |
|
f1 = jj_no_wc("log", "-r", "@", "--no-graph", "-T", "change_id", cwd=repo) |
|
|
|
# Feature: commit 2 only modifies foo (bar is inherited but untouched) |
|
jj("new", "-m", "modify only foo", cwd=repo) |
|
write_file(repo, "sub/foo.rb", "foo original\nfoo feature\nfoo more\n") |
|
f2 = jj_no_wc("log", "-r", "@", "--no-graph", "-T", "change_id", cwd=repo) |
|
|
|
# Feature: commit 3 only modifies other.txt (neither renamed file touched) |
|
jj("new", "-m", "modify other.txt only", cwd=repo) |
|
write_file(repo, "other.txt", "other modified\n") |
|
f3 = jj_no_wc("log", "-r", "@", "--no-graph", "-T", "change_id", cwd=repo) |
|
|
|
# Trunk: rename sub/foo.rb -> foo.rb, sub/bar.rb -> bar.rb |
|
jj("new", base, "-m", "move files out of sub/", cwd=repo) |
|
os.rename(os.path.join(repo, "sub", "foo.rb"), os.path.join(repo, "foo.rb")) |
|
os.rename(os.path.join(repo, "sub", "bar.rb"), os.path.join(repo, "bar.rb")) |
|
os.rmdir(os.path.join(repo, "sub")) |
|
trunk = jj_no_wc("log", "-r", "@", "--no-graph", "-T", "change_id", cwd=repo) |
|
jj("bookmark", "set", "trunk", "-r", trunk, cwd=repo) |
|
|
|
jj("edit", f3, cwd=repo) |
|
|
|
print("\nBefore:") |
|
print(jj_no_wc("log", "-r", "all()", cwd=repo)) |
|
|
|
result = run_script(repo, "--rename", "sub/foo.rb:foo.rb", |
|
"--rename", "sub/bar.rb:bar.rb", "--onto", "trunk") |
|
if result.returncode != 0: |
|
print("FAILED: script exited with non-zero") |
|
return False |
|
|
|
print("\nAfter:") |
|
print(jj_no_wc("log", "-r", "all()", cwd=repo)) |
|
|
|
errors = check_no_conflicts(repo) |
|
|
|
jj("workspace", "update-stale", cwd=repo) |
|
jj("edit", f3, cwd=repo) |
|
|
|
# foo.rb should have all the feature content |
|
content = read_file(repo, "foo.rb") |
|
if "foo feature" not in content or "foo more" not in content: |
|
errors.append(f"foo.rb missing feature content: {content!r}") |
|
|
|
# bar.rb should have feature content from f1 |
|
content = read_file(repo, "bar.rb") |
|
if "bar feature" not in content: |
|
errors.append(f"bar.rb missing feature content: {content!r}") |
|
|
|
# other.txt should be modified |
|
content = read_file(repo, "other.txt") |
|
if "other modified" not in content: |
|
errors.append(f"other.txt missing content: {content!r}") |
|
|
|
# Descriptions preserved |
|
for cid, expected in [(f1, "modify foo and bar"), (f2, "modify only foo"), |
|
(f3, "modify other.txt only")]: |
|
desc = jj_no_wc("log", "-r", cid, "--no-graph", "-T", "description", cwd=repo) |
|
if expected not in desc: |
|
errors.append(f"Description lost for {cid[:12]}: {desc!r}") |
|
|
|
if errors: |
|
print("\nFAILED:") |
|
for e in errors: |
|
print(f" - {e}") |
|
return False |
|
return True |
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
|
|
def main() -> int: |
|
tests = [ |
|
("basic rename", test_basic_rename), |
|
("inherited files not conflicting", test_inherited_files_not_conflicting), |
|
] |
|
all_passed = True |
|
for name, test_fn in tests: |
|
print(f"\n{'='*60}") |
|
print(f"TEST: {name}") |
|
print(f"{'='*60}") |
|
passed = test_fn() |
|
print(f"\n{'PASSED' if passed else 'FAILED'}: {name}") |
|
if not passed: |
|
all_passed = False |
|
|
|
print(f"\n{'='*60}") |
|
print(f"{'ALL TESTS PASSED' if all_passed else 'SOME TESTS FAILED'}") |
|
return 0 if all_passed else 1 |
|
|
|
|
|
if __name__ == "__main__": |
|
sys.exit(main()) |