git2svn 0.5.0__tar.gz → 0.5.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {git2svn-0.5.0 → git2svn-0.5.2}/.github/workflows/ci.yml +10 -5
- {git2svn-0.5.0/git2svn.egg-info → git2svn-0.5.2}/PKG-INFO +3 -2
- {git2svn-0.5.0 → git2svn-0.5.2}/README.md +1 -1
- {git2svn-0.5.0 → git2svn-0.5.2}/git2svn/__init__.py +3 -3
- {git2svn-0.5.0 → git2svn-0.5.2}/git2svn/_version.py +2 -2
- {git2svn-0.5.0 → git2svn-0.5.2}/git2svn/cli.py +1 -1
- {git2svn-0.5.0 → git2svn-0.5.2}/git2svn/core.py +1 -1
- {git2svn-0.5.0 → git2svn-0.5.2}/git2svn/git.py +6 -6
- {git2svn-0.5.0 → git2svn-0.5.2}/git2svn/patcher.py +23 -16
- git2svn-0.5.2/git2svn/py.typed +1 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/git2svn/state.py +4 -2
- {git2svn-0.5.0 → git2svn-0.5.2/git2svn.egg-info}/PKG-INFO +3 -2
- {git2svn-0.5.0 → git2svn-0.5.2}/git2svn.egg-info/SOURCES.txt +1 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/git2svn.egg-info/requires.txt +1 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/git2svn.egg-info/scm_file_list.json +1 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/pyproject.toml +18 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/tests/test_e2e.py +148 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/.githooks/pre-commit +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/.github/dependabot.yml +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/.github/workflows/release.yml +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/.gitignore +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/.pre-commit-config.yaml +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/CONTRIBUTING.md +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/LICENSE.md +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/docs/arc42/README.md +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/docs/arc42/adrs.md +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/docs/arc42/concepts.md +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/docs/req42.md +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/docs/user-guide/README.md +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/docs/user-guide/commands.md +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/docs/user-guide/integration-workflow.md +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/docs/user-guide/troubleshooting.md +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/git2svn/__main__.py +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/git2svn/eol.py +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/git2svn/svn.py +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/git2svn.egg-info/dependency_links.txt +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/git2svn.egg-info/entry_points.txt +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/git2svn.egg-info/top_level.txt +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/git2svn.py +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/setup.cfg +0 -0
- {git2svn-0.5.0 → git2svn-0.5.2}/tests/test_git2svn.py +0 -0
|
@@ -7,8 +7,8 @@ on:
|
|
|
7
7
|
branches: [ "main" ]
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
10
|
-
lint-
|
|
11
|
-
name: Lint &
|
|
10
|
+
lint-format-typecheck:
|
|
11
|
+
name: Lint, Format & Type Check
|
|
12
12
|
runs-on: ubuntu-latest
|
|
13
13
|
steps:
|
|
14
14
|
- name: Check out repository
|
|
@@ -19,8 +19,8 @@ jobs:
|
|
|
19
19
|
with:
|
|
20
20
|
python-version: "3.12"
|
|
21
21
|
|
|
22
|
-
- name: Install
|
|
23
|
-
run: pip install ruff
|
|
22
|
+
- name: Install Tooling
|
|
23
|
+
run: pip install ruff mypy
|
|
24
24
|
|
|
25
25
|
- name: Run Ruff Lint Checks
|
|
26
26
|
run: ruff check .
|
|
@@ -28,6 +28,9 @@ jobs:
|
|
|
28
28
|
- name: Verify Formatting with Ruff
|
|
29
29
|
run: ruff format --check .
|
|
30
30
|
|
|
31
|
+
- name: Type Check with MyPy
|
|
32
|
+
run: mypy git2svn
|
|
33
|
+
|
|
31
34
|
test:
|
|
32
35
|
name: Test on Python ${{ matrix.python-version }} (${{ matrix.os }})
|
|
33
36
|
runs-on: ${{ matrix.os }}
|
|
@@ -65,7 +68,9 @@ jobs:
|
|
|
65
68
|
echo "C:\Program Files\TortoiseSVN\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
|
66
69
|
|
|
67
70
|
- name: Install Dependencies
|
|
68
|
-
run:
|
|
71
|
+
run: |
|
|
72
|
+
python -m pip install --upgrade pip setuptools wheel
|
|
73
|
+
pip install --retries 5 -e .[dev]
|
|
69
74
|
|
|
70
75
|
- name: Run Unit & Integration Tests (with Coverage)
|
|
71
76
|
if: runner.os == 'Linux' && matrix.python-version == '3.12'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git2svn
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: CLI utility to synchronize changes from a local Git repository to a local SVN working copy
|
|
5
5
|
Author: Simon
|
|
6
6
|
License: Apache-2.0
|
|
@@ -26,6 +26,7 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
License-File: LICENSE.md
|
|
27
27
|
Provides-Extra: dev
|
|
28
28
|
Requires-Dist: coverage>=7.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy>=1.9.0; extra == "dev"
|
|
29
30
|
Requires-Dist: ruff>=0.8.0; extra == "dev"
|
|
30
31
|
Requires-Dist: setuptools-scm>=8.0; extra == "dev"
|
|
31
32
|
Dynamic: license-file
|
|
@@ -35,7 +36,7 @@ Dynamic: license-file
|
|
|
35
36
|
<p align="center">
|
|
36
37
|
<a href="https://github.com/simon123h/git2svn/actions/workflows/ci.yml"><img src="https://github.com/simon123h/git2svn/actions/workflows/ci.yml/badge.svg?branch=main" alt="Build Status"></a>
|
|
37
38
|
<a href="https://codecov.io/gh/simon123h/git2svn"><img src="https://codecov.io/gh/simon123h/git2svn/branch/main/graph/badge.svg" alt="Coverage"></a>
|
|
38
|
-
<a href="https://pypi.org/project/git2svn/"><img src="https://img.shields.io/pypi/v/git2svn?color=
|
|
39
|
+
<a href="https://pypi.org/project/git2svn/"><img src="https://img.shields.io/pypi/v/git2svn?color=green" alt="PyPI Version"></a>
|
|
39
40
|
<img src="https://img.shields.io/badge/Python-v3.11+-3776AB?logo=python&logoColor=white" alt="Python Version">
|
|
40
41
|
<img src="https://img.shields.io/endpoint?url=https%3A%2F%2Ftokei.kojix2.net%2Fbadge%2Fgithub%2Fsimon123h%2Fgit2svn%2Flines" alt="Lines of Code">
|
|
41
42
|
<a href="https://github.com/simon123h/git2svn/releases"><img src="https://badgen.net/github/release/simon123h/git2svn?color=orange" alt="Latest Release"></a>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<a href="https://github.com/simon123h/git2svn/actions/workflows/ci.yml"><img src="https://github.com/simon123h/git2svn/actions/workflows/ci.yml/badge.svg?branch=main" alt="Build Status"></a>
|
|
5
5
|
<a href="https://codecov.io/gh/simon123h/git2svn"><img src="https://codecov.io/gh/simon123h/git2svn/branch/main/graph/badge.svg" alt="Coverage"></a>
|
|
6
|
-
<a href="https://pypi.org/project/git2svn/"><img src="https://img.shields.io/pypi/v/git2svn?color=
|
|
6
|
+
<a href="https://pypi.org/project/git2svn/"><img src="https://img.shields.io/pypi/v/git2svn?color=green" alt="PyPI Version"></a>
|
|
7
7
|
<img src="https://img.shields.io/badge/Python-v3.11+-3776AB?logo=python&logoColor=white" alt="Python Version">
|
|
8
8
|
<img src="https://img.shields.io/endpoint?url=https%3A%2F%2Ftokei.kojix2.net%2Fbadge%2Fgithub%2Fsimon123h%2Fgit2svn%2Flines" alt="Lines of Code">
|
|
9
9
|
<a href="https://github.com/simon123h/git2svn/releases"><img src="https://badgen.net/github/release/simon123h/git2svn?color=orange" alt="Latest Release"></a>
|
|
@@ -20,7 +20,7 @@ from .state import (
|
|
|
20
20
|
from .svn import SvnError, SvnLockError, SvnOutOfDateError, SvnWorkspace
|
|
21
21
|
|
|
22
22
|
try:
|
|
23
|
-
from ._version import __version__
|
|
23
|
+
from ._version import __version__ # type: ignore[import-not-found]
|
|
24
24
|
except ImportError:
|
|
25
25
|
__version__ = "0.0.0.dev0"
|
|
26
26
|
|
|
@@ -30,9 +30,9 @@ def get_version() -> str:
|
|
|
30
30
|
try:
|
|
31
31
|
from importlib.metadata import version
|
|
32
32
|
|
|
33
|
-
return version("git2svn")
|
|
33
|
+
return str(version("git2svn"))
|
|
34
34
|
except Exception:
|
|
35
|
-
return __version__
|
|
35
|
+
return str(__version__)
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
__all__ = [
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.5.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 5,
|
|
21
|
+
__version__ = version = '0.5.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 5, 2)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -224,7 +224,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
224
224
|
def handle_setup(git_repo: GitRepo, svn_dir_path: Optional[Path | str]) -> int:
|
|
225
225
|
"""Automate repository configuration, branch detection, and productivity aliases."""
|
|
226
226
|
if not git_repo.is_valid_repo():
|
|
227
|
-
print(f"Error: '{git_repo.
|
|
227
|
+
print(f"Error: '{git_repo.repo_dir}' is not a valid Git repository.", file=sys.stderr)
|
|
228
228
|
return 1
|
|
229
229
|
|
|
230
230
|
# 1. Resolve SVN working copy path
|
|
@@ -591,7 +591,7 @@ class Synchronizer:
|
|
|
591
591
|
is_single, start_ref, end_ref = parse_ref_arguments(default_range)
|
|
592
592
|
if is_single:
|
|
593
593
|
commits = [start_ref] if self.git.ref_exists(start_ref) else []
|
|
594
|
-
merges = []
|
|
594
|
+
merges: List[str] = []
|
|
595
595
|
else:
|
|
596
596
|
assert end_ref is not None
|
|
597
597
|
if not self.git.ref_exists(start_ref):
|
|
@@ -218,15 +218,15 @@ class GitRepo:
|
|
|
218
218
|
return res.stdout.strip()
|
|
219
219
|
return None
|
|
220
220
|
|
|
221
|
-
def get_diff(self, ref_spec: str) ->
|
|
222
|
-
"""Get unified diff for the given reference specification."""
|
|
223
|
-
res = self.
|
|
221
|
+
def get_diff(self, ref_spec: str) -> bytes:
|
|
222
|
+
"""Get unified diff for the given reference specification as raw bytes."""
|
|
223
|
+
res = self.run_cmd_bytes(["diff", "--binary", ref_spec])
|
|
224
224
|
return res.stdout
|
|
225
225
|
|
|
226
|
-
def get_diff_root(self, commit_hash: str) ->
|
|
227
|
-
"""Get unified diff for a root commit."""
|
|
226
|
+
def get_diff_root(self, commit_hash: str) -> bytes:
|
|
227
|
+
"""Get unified diff for a root commit as raw bytes."""
|
|
228
228
|
empty_tree_hash = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
|
229
|
-
res = self.
|
|
229
|
+
res = self.run_cmd_bytes(["diff", "--binary", empty_tree_hash, commit_hash])
|
|
230
230
|
return res.stdout
|
|
231
231
|
|
|
232
232
|
def get_name_status(self, ref_spec: str) -> List[FileChange]:
|
|
@@ -4,6 +4,7 @@ import logging
|
|
|
4
4
|
import subprocess
|
|
5
5
|
import sys
|
|
6
6
|
from pathlib import Path
|
|
7
|
+
from typing import Union
|
|
7
8
|
|
|
8
9
|
logger = logging.getLogger("git2svn")
|
|
9
10
|
|
|
@@ -16,43 +17,49 @@ class Patcher:
|
|
|
16
17
|
self.git_bin = git_bin
|
|
17
18
|
self.dry_run = dry_run
|
|
18
19
|
|
|
19
|
-
def apply_diff(self, diff_content: str) -> None:
|
|
20
|
-
"""Apply unified diff text to target_dir using git apply."""
|
|
21
|
-
if
|
|
20
|
+
def apply_diff(self, diff_content: Union[str, bytes]) -> None:
|
|
21
|
+
"""Apply unified diff text or bytes to target_dir using git apply."""
|
|
22
|
+
if isinstance(diff_content, str):
|
|
23
|
+
diff_bytes = diff_content.encode("utf-8")
|
|
24
|
+
else:
|
|
25
|
+
diff_bytes = diff_content
|
|
26
|
+
|
|
27
|
+
if not diff_bytes.strip():
|
|
22
28
|
logger.info("Diff is empty. Nothing to patch.")
|
|
23
29
|
return
|
|
24
30
|
|
|
25
31
|
cmd = [self.git_bin, "apply", "--ignore-whitespace", "--unsafe-paths", "--reject"]
|
|
26
32
|
if self.dry_run:
|
|
27
|
-
|
|
33
|
+
preview = diff_bytes.strip()[:200].decode("utf-8", errors="replace")
|
|
34
|
+
print(f"[DRY-RUN] (in {self.target_dir}) {' '.join(cmd)} << EOF\n{preview}...\nEOF")
|
|
28
35
|
return
|
|
29
36
|
|
|
30
37
|
logger.debug("Applying patch to %s using %s", self.target_dir, " ".join(cmd))
|
|
31
38
|
proc = subprocess.run(
|
|
32
39
|
cmd,
|
|
33
40
|
cwd=self.target_dir,
|
|
34
|
-
input=
|
|
41
|
+
input=diff_bytes,
|
|
35
42
|
capture_output=True,
|
|
36
|
-
text=True,
|
|
37
|
-
encoding="utf-8",
|
|
38
|
-
errors="replace",
|
|
39
43
|
)
|
|
40
44
|
|
|
45
|
+
stdout_str = proc.stdout.decode("utf-8", errors="replace") if proc.stdout else ""
|
|
46
|
+
stderr_str = proc.stderr.decode("utf-8", errors="replace") if proc.stderr else ""
|
|
47
|
+
|
|
41
48
|
if proc.returncode != 0:
|
|
42
49
|
logger.error(
|
|
43
|
-
"git apply failed with code %d:\nSTDOUT:\n%s\nSTDERR:\n%s", proc.returncode,
|
|
50
|
+
"git apply failed with code %d:\nSTDOUT:\n%s\nSTDERR:\n%s", proc.returncode, stdout_str, stderr_str
|
|
44
51
|
)
|
|
45
52
|
print(f"Error: git apply failed (exit code {proc.returncode}).", file=sys.stderr)
|
|
46
|
-
if
|
|
47
|
-
print(
|
|
48
|
-
if
|
|
49
|
-
print(
|
|
53
|
+
if stdout_str:
|
|
54
|
+
print(stdout_str, file=sys.stderr)
|
|
55
|
+
if stderr_str:
|
|
56
|
+
print(stderr_str, file=sys.stderr)
|
|
50
57
|
print(
|
|
51
58
|
"Tip: Check for .rej reject files in the SVN workspace or use 'git2svn stage --copy' to copy files directly.",
|
|
52
59
|
file=sys.stderr,
|
|
53
60
|
)
|
|
54
|
-
raise subprocess.CalledProcessError(proc.returncode, cmd,
|
|
61
|
+
raise subprocess.CalledProcessError(proc.returncode, cmd, stdout_str, stderr_str)
|
|
55
62
|
else:
|
|
56
|
-
if
|
|
57
|
-
msg = (
|
|
63
|
+
if stdout_str or stderr_str:
|
|
64
|
+
msg = (stdout_str + "\n" + stderr_str).strip()
|
|
58
65
|
logger.info("Patch applied successfully:\n%s", msg)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561. The git2svn package uses inline type annotations.
|
|
@@ -4,7 +4,7 @@ import json
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Any, Dict, List, Optional
|
|
7
|
+
from typing import Any, Dict, List, Optional, cast
|
|
8
8
|
|
|
9
9
|
logger = logging.getLogger("git2svn")
|
|
10
10
|
|
|
@@ -28,7 +28,9 @@ def load_replay_state(workspace_dir: Path) -> Optional[Dict[str, Any]]:
|
|
|
28
28
|
path = get_replay_state_path(workspace_dir)
|
|
29
29
|
if path.is_file():
|
|
30
30
|
try:
|
|
31
|
-
|
|
31
|
+
val = json.loads(path.read_text(encoding="utf-8"))
|
|
32
|
+
if isinstance(val, dict):
|
|
33
|
+
return cast(Dict[str, Any], val)
|
|
32
34
|
except Exception as e:
|
|
33
35
|
logger.error("Failed to parse replay state file: %s", e)
|
|
34
36
|
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git2svn
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: CLI utility to synchronize changes from a local Git repository to a local SVN working copy
|
|
5
5
|
Author: Simon
|
|
6
6
|
License: Apache-2.0
|
|
@@ -26,6 +26,7 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
License-File: LICENSE.md
|
|
27
27
|
Provides-Extra: dev
|
|
28
28
|
Requires-Dist: coverage>=7.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy>=1.9.0; extra == "dev"
|
|
29
30
|
Requires-Dist: ruff>=0.8.0; extra == "dev"
|
|
30
31
|
Requires-Dist: setuptools-scm>=8.0; extra == "dev"
|
|
31
32
|
Dynamic: license-file
|
|
@@ -35,7 +36,7 @@ Dynamic: license-file
|
|
|
35
36
|
<p align="center">
|
|
36
37
|
<a href="https://github.com/simon123h/git2svn/actions/workflows/ci.yml"><img src="https://github.com/simon123h/git2svn/actions/workflows/ci.yml/badge.svg?branch=main" alt="Build Status"></a>
|
|
37
38
|
<a href="https://codecov.io/gh/simon123h/git2svn"><img src="https://codecov.io/gh/simon123h/git2svn/branch/main/graph/badge.svg" alt="Coverage"></a>
|
|
38
|
-
<a href="https://pypi.org/project/git2svn/"><img src="https://img.shields.io/pypi/v/git2svn?color=
|
|
39
|
+
<a href="https://pypi.org/project/git2svn/"><img src="https://img.shields.io/pypi/v/git2svn?color=green" alt="PyPI Version"></a>
|
|
39
40
|
<img src="https://img.shields.io/badge/Python-v3.11+-3776AB?logo=python&logoColor=white" alt="Python Version">
|
|
40
41
|
<img src="https://img.shields.io/endpoint?url=https%3A%2F%2Ftokei.kojix2.net%2Fbadge%2Fgithub%2Fsimon123h%2Fgit2svn%2Flines" alt="Lines of Code">
|
|
41
42
|
<a href="https://github.com/simon123h/git2svn/releases"><img src="https://badgen.net/github/release/simon123h/git2svn?color=orange" alt="Latest Release"></a>
|
|
@@ -37,6 +37,7 @@ Changelog = "https://github.com/simon123h/git2svn/releases"
|
|
|
37
37
|
[project.optional-dependencies]
|
|
38
38
|
dev = [
|
|
39
39
|
"coverage>=7.0.0",
|
|
40
|
+
"mypy>=1.9.0",
|
|
40
41
|
"ruff>=0.8.0",
|
|
41
42
|
"setuptools-scm>=8.0",
|
|
42
43
|
]
|
|
@@ -85,3 +86,20 @@ exclude_lines = [
|
|
|
85
86
|
"if __name__ == .__main__.:",
|
|
86
87
|
]
|
|
87
88
|
|
|
89
|
+
[tool.setuptools.package-data]
|
|
90
|
+
git2svn = ["py.typed"]
|
|
91
|
+
|
|
92
|
+
[tool.mypy]
|
|
93
|
+
python_version = "3.11"
|
|
94
|
+
strict = true
|
|
95
|
+
warn_unused_configs = true
|
|
96
|
+
disallow_untyped_defs = true
|
|
97
|
+
disallow_incomplete_defs = true
|
|
98
|
+
check_untyped_defs = true
|
|
99
|
+
no_implicit_optional = true
|
|
100
|
+
warn_redundant_casts = true
|
|
101
|
+
warn_unused_ignores = true
|
|
102
|
+
warn_return_any = true
|
|
103
|
+
show_error_codes = true
|
|
104
|
+
|
|
105
|
+
|
|
@@ -232,3 +232,151 @@ class TestGit2SvnE2E(unittest.TestCase):
|
|
|
232
232
|
status = self._svn_status()
|
|
233
233
|
self.assertIn("D obsolete.txt", status)
|
|
234
234
|
self.assertIn("A brand_new.txt", status)
|
|
235
|
+
|
|
236
|
+
def test_e2e_replay_conflict_pause_and_continue(self):
|
|
237
|
+
"""Test replay pausing on a patch conflict in a real SVN WC and resuming with --continue."""
|
|
238
|
+
# 1. Base commit in Git and SVN
|
|
239
|
+
f_git = self.git_dir / "conflict_target.txt"
|
|
240
|
+
f_git.write_text("line 1\nline 2\nline 3\n", encoding="utf-8")
|
|
241
|
+
base_hash = self._git_commit("base: conflict target")
|
|
242
|
+
|
|
243
|
+
(self.svn_wc_dir / "conflict_target.txt").write_text("line 1\nline 2\nline 3\n", encoding="utf-8")
|
|
244
|
+
subprocess.run([SVN_BIN, "add", "conflict_target.txt"], cwd=self.svn_wc_dir, check=True)
|
|
245
|
+
subprocess.run([SVN_BIN, "commit", "-m", "init conflict target"], cwd=self.svn_wc_dir, check=True)
|
|
246
|
+
|
|
247
|
+
# 2. Modify in SVN out-of-band to cause conflict
|
|
248
|
+
(self.svn_wc_dir / "conflict_target.txt").write_text("line 1 DIFFERENT\nline 2\nline 3\n", encoding="utf-8")
|
|
249
|
+
subprocess.run([SVN_BIN, "commit", "-m", "remote edit in svn"], cwd=self.svn_wc_dir, check=True)
|
|
250
|
+
subprocess.run([SVN_BIN, "update"], cwd=self.svn_wc_dir, check=True)
|
|
251
|
+
|
|
252
|
+
# 3. Git commit 1 (touches line 1 and 2, which conflicts)
|
|
253
|
+
f_git.write_text("line 1 GIT_EDIT\nline 2 GIT_EDIT\nline 3\n", encoding="utf-8")
|
|
254
|
+
self._git_commit("feat: conflicting git commit")
|
|
255
|
+
|
|
256
|
+
# 4. Git commit 2 (subsequent non-conflicting commit)
|
|
257
|
+
f_next = self.git_dir / "subsequent.txt"
|
|
258
|
+
f_next.write_text("subsequent content\n", encoding="utf-8")
|
|
259
|
+
c2_hash = self._git_commit("feat: subsequent commit")
|
|
260
|
+
|
|
261
|
+
# 5. Run replay range - should pause with non-zero exit code due to conflict
|
|
262
|
+
exit_code = cli_main(["replay", f"{base_hash}..{c2_hash}", "-g", str(self.git_dir), "-s", str(self.svn_wc_dir)])
|
|
263
|
+
self.assertNotEqual(exit_code, 0)
|
|
264
|
+
|
|
265
|
+
# Verify .rej artifact exists
|
|
266
|
+
rej_file = self.svn_wc_dir / "conflict_target.txt.rej"
|
|
267
|
+
self.assertTrue(rej_file.exists())
|
|
268
|
+
|
|
269
|
+
# 6. Resolve conflict manually in SVN working copy and remove .rej artifact
|
|
270
|
+
(self.svn_wc_dir / "conflict_target.txt").write_text(
|
|
271
|
+
"line 1 RESOLVED\nline 2 GIT_EDIT\nline 3\n", encoding="utf-8"
|
|
272
|
+
)
|
|
273
|
+
rej_file.unlink()
|
|
274
|
+
|
|
275
|
+
# 7. Continue replay
|
|
276
|
+
cont_code = cli_main(["replay", "--continue", "-g", str(self.git_dir), "-s", str(self.svn_wc_dir)])
|
|
277
|
+
self.assertEqual(cont_code, 0)
|
|
278
|
+
|
|
279
|
+
# Verify SVN working copy is clean and both commits are in SVN log
|
|
280
|
+
self.assertEqual(self._svn_status().strip(), "")
|
|
281
|
+
_, full_log = self._svn_log_messages()
|
|
282
|
+
self.assertIn("conflicting git commit", full_log)
|
|
283
|
+
self.assertIn("subsequent commit", full_log)
|
|
284
|
+
|
|
285
|
+
# Verify final files on disk in SVN
|
|
286
|
+
self.assertEqual(
|
|
287
|
+
(self.svn_wc_dir / "conflict_target.txt").read_text(encoding="utf-8"),
|
|
288
|
+
"line 1 RESOLVED\nline 2 GIT_EDIT\nline 3\n",
|
|
289
|
+
)
|
|
290
|
+
self.assertEqual((self.svn_wc_dir / "subsequent.txt").read_text(encoding="utf-8"), "subsequent content\n")
|
|
291
|
+
|
|
292
|
+
def test_e2e_file_renames_and_moves(self):
|
|
293
|
+
"""Test replay correctly handles file renames and moves across directories in real SVN."""
|
|
294
|
+
# 1. Base commit: create src/old_name.txt
|
|
295
|
+
src_dir = self.git_dir / "src"
|
|
296
|
+
src_dir.mkdir()
|
|
297
|
+
orig_file = src_dir / "old_name.txt"
|
|
298
|
+
orig_file.write_text("print('hello rename')\n", encoding="utf-8")
|
|
299
|
+
base_hash = self._git_commit("init: original file")
|
|
300
|
+
|
|
301
|
+
# Replay base to SVN
|
|
302
|
+
code_base = cli_main(["replay", base_hash, "-g", str(self.git_dir), "-s", str(self.svn_wc_dir)])
|
|
303
|
+
self.assertEqual(code_base, 0)
|
|
304
|
+
|
|
305
|
+
# 2. Git commit: rename src/old_name.txt -> src/nested/renamed.txt
|
|
306
|
+
nested_dir = src_dir / "nested"
|
|
307
|
+
nested_dir.mkdir()
|
|
308
|
+
subprocess.run([GIT_BIN, "mv", "src/old_name.txt", "src/nested/renamed.txt"], cwd=self.git_dir, check=True)
|
|
309
|
+
rename_hash = self._git_commit("refactor: move and rename file")
|
|
310
|
+
|
|
311
|
+
# 3. Replay rename commit to SVN
|
|
312
|
+
code_rename = cli_main(
|
|
313
|
+
["replay", f"{base_hash}..{rename_hash}", "-g", str(self.git_dir), "-s", str(self.svn_wc_dir)]
|
|
314
|
+
)
|
|
315
|
+
self.assertEqual(code_rename, 0)
|
|
316
|
+
|
|
317
|
+
# 4. Verify SVN state
|
|
318
|
+
self.assertFalse((self.svn_wc_dir / "src" / "old_name.txt").exists())
|
|
319
|
+
renamed_svn = self.svn_wc_dir / "src" / "nested" / "renamed.txt"
|
|
320
|
+
self.assertTrue(renamed_svn.is_file())
|
|
321
|
+
self.assertEqual(renamed_svn.read_text(encoding="utf-8"), "print('hello rename')\n")
|
|
322
|
+
self.assertEqual(self._svn_status().strip(), "")
|
|
323
|
+
|
|
324
|
+
def test_e2e_binary_files(self):
|
|
325
|
+
"""Test replay correctly handles adding and modifying binary files with null bytes in real SVN."""
|
|
326
|
+
# 1. Root commit
|
|
327
|
+
(self.git_dir / "init.txt").write_text("init\n")
|
|
328
|
+
root_hash = self._git_commit("init: root commit")
|
|
329
|
+
|
|
330
|
+
# Replay root commit to SVN
|
|
331
|
+
code_root = cli_main(["replay", root_hash, "-g", str(self.git_dir), "-s", str(self.svn_wc_dir)])
|
|
332
|
+
self.assertEqual(code_root, 0)
|
|
333
|
+
|
|
334
|
+
# 2. Add binary file
|
|
335
|
+
bin_git = self.git_dir / "image.bin"
|
|
336
|
+
initial_bytes = bytes([0x00, 0xFF, 0xFE, 0x01, 0x00, 0x42, 0xAA, 0x55] * 32)
|
|
337
|
+
bin_git.write_bytes(initial_bytes)
|
|
338
|
+
self._git_commit("feat: add binary image")
|
|
339
|
+
|
|
340
|
+
# 3. Modify binary file
|
|
341
|
+
modified_bytes = bytes([0x00, 0x00, 0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA] * 40)
|
|
342
|
+
bin_git.write_bytes(modified_bytes)
|
|
343
|
+
mod_hash = self._git_commit("fix: update binary bytes")
|
|
344
|
+
|
|
345
|
+
# 4. Replay range to SVN
|
|
346
|
+
code = cli_main(["replay", f"{root_hash}..{mod_hash}", "-g", str(self.git_dir), "-s", str(self.svn_wc_dir)])
|
|
347
|
+
self.assertEqual(code, 0)
|
|
348
|
+
|
|
349
|
+
# 4. Verify SVN file content is bit-for-bit identical
|
|
350
|
+
bin_svn = self.svn_wc_dir / "image.bin"
|
|
351
|
+
self.assertTrue(bin_svn.is_file())
|
|
352
|
+
self.assertEqual(bin_svn.read_bytes(), modified_bytes)
|
|
353
|
+
self.assertEqual(self._svn_status().strip(), "")
|
|
354
|
+
|
|
355
|
+
def test_e2e_status_real_repo(self):
|
|
356
|
+
"""Test git2svn status command execution against real Git and real SVN repositories."""
|
|
357
|
+
# Setup base commit in Git and SVN
|
|
358
|
+
f = self.git_dir / "status_test.txt"
|
|
359
|
+
f.write_text("status test\n", encoding="utf-8")
|
|
360
|
+
base_hash = self._git_commit("init: status test file")
|
|
361
|
+
|
|
362
|
+
cli_main(["replay", base_hash, "-g", str(self.git_dir), "-s", str(self.svn_wc_dir)])
|
|
363
|
+
|
|
364
|
+
# Setup pending commit in Git
|
|
365
|
+
f.write_text("status test modified\n", encoding="utf-8")
|
|
366
|
+
pending_hash = self._git_commit("feat: pending feature for status")
|
|
367
|
+
|
|
368
|
+
# Configure defaultRange and svnDir in Git config
|
|
369
|
+
subprocess.run(
|
|
370
|
+
[GIT_BIN, "config", "git2svn.defaultRange", f"{base_hash}..{pending_hash}"],
|
|
371
|
+
cwd=self.git_dir,
|
|
372
|
+
check=True,
|
|
373
|
+
)
|
|
374
|
+
subprocess.run(
|
|
375
|
+
[GIT_BIN, "config", "git2svn.svnDir", str(self.svn_wc_dir)],
|
|
376
|
+
cwd=self.git_dir,
|
|
377
|
+
check=True,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Run git2svn status
|
|
381
|
+
exit_code = cli_main(["status", "-g", str(self.git_dir)])
|
|
382
|
+
self.assertEqual(exit_code, 0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|