git2svn 0.5.0__tar.gz → 0.5.1__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.
Files changed (40) hide show
  1. {git2svn-0.5.0 → git2svn-0.5.1}/.github/workflows/ci.yml +3 -1
  2. {git2svn-0.5.0/git2svn.egg-info → git2svn-0.5.1}/PKG-INFO +2 -2
  3. {git2svn-0.5.0 → git2svn-0.5.1}/README.md +1 -1
  4. {git2svn-0.5.0 → git2svn-0.5.1}/git2svn/_version.py +2 -2
  5. {git2svn-0.5.0 → git2svn-0.5.1}/git2svn/git.py +6 -6
  6. {git2svn-0.5.0 → git2svn-0.5.1}/git2svn/patcher.py +23 -16
  7. {git2svn-0.5.0 → git2svn-0.5.1/git2svn.egg-info}/PKG-INFO +2 -2
  8. {git2svn-0.5.0 → git2svn-0.5.1}/tests/test_e2e.py +148 -0
  9. {git2svn-0.5.0 → git2svn-0.5.1}/.githooks/pre-commit +0 -0
  10. {git2svn-0.5.0 → git2svn-0.5.1}/.github/dependabot.yml +0 -0
  11. {git2svn-0.5.0 → git2svn-0.5.1}/.github/workflows/release.yml +0 -0
  12. {git2svn-0.5.0 → git2svn-0.5.1}/.gitignore +0 -0
  13. {git2svn-0.5.0 → git2svn-0.5.1}/.pre-commit-config.yaml +0 -0
  14. {git2svn-0.5.0 → git2svn-0.5.1}/CONTRIBUTING.md +0 -0
  15. {git2svn-0.5.0 → git2svn-0.5.1}/LICENSE.md +0 -0
  16. {git2svn-0.5.0 → git2svn-0.5.1}/docs/arc42/README.md +0 -0
  17. {git2svn-0.5.0 → git2svn-0.5.1}/docs/arc42/adrs.md +0 -0
  18. {git2svn-0.5.0 → git2svn-0.5.1}/docs/arc42/concepts.md +0 -0
  19. {git2svn-0.5.0 → git2svn-0.5.1}/docs/req42.md +0 -0
  20. {git2svn-0.5.0 → git2svn-0.5.1}/docs/user-guide/README.md +0 -0
  21. {git2svn-0.5.0 → git2svn-0.5.1}/docs/user-guide/commands.md +0 -0
  22. {git2svn-0.5.0 → git2svn-0.5.1}/docs/user-guide/integration-workflow.md +0 -0
  23. {git2svn-0.5.0 → git2svn-0.5.1}/docs/user-guide/troubleshooting.md +0 -0
  24. {git2svn-0.5.0 → git2svn-0.5.1}/git2svn/__init__.py +0 -0
  25. {git2svn-0.5.0 → git2svn-0.5.1}/git2svn/__main__.py +0 -0
  26. {git2svn-0.5.0 → git2svn-0.5.1}/git2svn/cli.py +0 -0
  27. {git2svn-0.5.0 → git2svn-0.5.1}/git2svn/core.py +0 -0
  28. {git2svn-0.5.0 → git2svn-0.5.1}/git2svn/eol.py +0 -0
  29. {git2svn-0.5.0 → git2svn-0.5.1}/git2svn/state.py +0 -0
  30. {git2svn-0.5.0 → git2svn-0.5.1}/git2svn/svn.py +0 -0
  31. {git2svn-0.5.0 → git2svn-0.5.1}/git2svn.egg-info/SOURCES.txt +0 -0
  32. {git2svn-0.5.0 → git2svn-0.5.1}/git2svn.egg-info/dependency_links.txt +0 -0
  33. {git2svn-0.5.0 → git2svn-0.5.1}/git2svn.egg-info/entry_points.txt +0 -0
  34. {git2svn-0.5.0 → git2svn-0.5.1}/git2svn.egg-info/requires.txt +0 -0
  35. {git2svn-0.5.0 → git2svn-0.5.1}/git2svn.egg-info/scm_file_list.json +0 -0
  36. {git2svn-0.5.0 → git2svn-0.5.1}/git2svn.egg-info/top_level.txt +0 -0
  37. {git2svn-0.5.0 → git2svn-0.5.1}/git2svn.py +0 -0
  38. {git2svn-0.5.0 → git2svn-0.5.1}/pyproject.toml +0 -0
  39. {git2svn-0.5.0 → git2svn-0.5.1}/setup.cfg +0 -0
  40. {git2svn-0.5.0 → git2svn-0.5.1}/tests/test_git2svn.py +0 -0
@@ -65,7 +65,9 @@ jobs:
65
65
  echo "C:\Program Files\TortoiseSVN\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
66
66
 
67
67
  - name: Install Dependencies
68
- run: pip install -e .[dev]
68
+ run: |
69
+ python -m pip install --upgrade pip setuptools wheel
70
+ pip install --retries 5 -e .[dev]
69
71
 
70
72
  - name: Run Unit & Integration Tests (with Coverage)
71
73
  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.0
3
+ Version: 0.5.1
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
@@ -35,7 +35,7 @@ Dynamic: license-file
35
35
  <p align="center">
36
36
  <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
37
  <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=blue" alt="PyPI Version"></a>
38
+ <a href="https://pypi.org/project/git2svn/"><img src="https://img.shields.io/pypi/v/git2svn?color=green" alt="PyPI Version"></a>
39
39
  <img src="https://img.shields.io/badge/Python-v3.11+-3776AB?logo=python&logoColor=white" alt="Python Version">
40
40
  <img src="https://img.shields.io/endpoint?url=https%3A%2F%2Ftokei.kojix2.net%2Fbadge%2Fgithub%2Fsimon123h%2Fgit2svn%2Flines" alt="Lines of Code">
41
41
  <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=blue" alt="PyPI Version"></a>
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>
@@ -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.0'
22
- __version_tuple__ = version_tuple = (0, 5, 0)
21
+ __version__ = version = '0.5.1'
22
+ __version_tuple__ = version_tuple = (0, 5, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -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) -> str:
222
- """Get unified diff for the given reference specification."""
223
- res = self.run_cmd(["diff", "--binary", ref_spec])
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) -> 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.run_cmd(["diff", "--binary", empty_tree_hash, commit_hash])
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 not diff_content.strip():
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
- print(f"[DRY-RUN] (in {self.target_dir}) {' '.join(cmd)} << EOF\n{diff_content.strip()[:200]}...\nEOF")
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=diff_content,
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, proc.stdout, proc.stderr
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 proc.stdout:
47
- print(proc.stdout, file=sys.stderr)
48
- if proc.stderr:
49
- print(proc.stderr, file=sys.stderr)
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, proc.stdout, proc.stderr)
61
+ raise subprocess.CalledProcessError(proc.returncode, cmd, stdout_str, stderr_str)
55
62
  else:
56
- if proc.stdout or proc.stderr:
57
- msg = (proc.stdout + "\n" + proc.stderr).strip()
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git2svn
3
- Version: 0.5.0
3
+ Version: 0.5.1
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
@@ -35,7 +35,7 @@ Dynamic: license-file
35
35
  <p align="center">
36
36
  <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
37
  <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=blue" alt="PyPI Version"></a>
38
+ <a href="https://pypi.org/project/git2svn/"><img src="https://img.shields.io/pypi/v/git2svn?color=green" alt="PyPI Version"></a>
39
39
  <img src="https://img.shields.io/badge/Python-v3.11+-3776AB?logo=python&logoColor=white" alt="Python Version">
40
40
  <img src="https://img.shields.io/endpoint?url=https%3A%2F%2Ftokei.kojix2.net%2Fbadge%2Fgithub%2Fsimon123h%2Fgit2svn%2Flines" alt="Lines of Code">
41
41
  <a href="https://github.com/simon123h/git2svn/releases"><img src="https://badgen.net/github/release/simon123h/git2svn?color=orange" alt="Latest Release"></a>
@@ -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