git2svn 0.4.0__py3-none-any.whl
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/__init__.py +56 -0
- git2svn/__main__.py +6 -0
- git2svn/cli.py +481 -0
- git2svn/core.py +485 -0
- git2svn/eol.py +67 -0
- git2svn/git.py +286 -0
- git2svn/patcher.py +58 -0
- git2svn/state.py +65 -0
- git2svn/svn.py +225 -0
- git2svn-0.4.0.dist-info/METADATA +148 -0
- git2svn-0.4.0.dist-info/RECORD +15 -0
- git2svn-0.4.0.dist-info/WHEEL +5 -0
- git2svn-0.4.0.dist-info/entry_points.txt +2 -0
- git2svn-0.4.0.dist-info/licenses/LICENSE.md +201 -0
- git2svn-0.4.0.dist-info/top_level.txt +1 -0
git2svn/__init__.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
git2svn: A consolidated CLI utility to synchronize changes from a local Git repository
|
|
3
|
+
to a local Subversion (SVN) working copy.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from .cli import build_parser, main, parse_cli_args
|
|
9
|
+
from .core import Synchronizer
|
|
10
|
+
from .eol import detect_file_eol, normalize_file_eol
|
|
11
|
+
from .git import FileChange, GitRepo, parse_name_status, parse_name_status_z, parse_ref_arguments
|
|
12
|
+
from .patcher import Patcher
|
|
13
|
+
from .state import (
|
|
14
|
+
clean_conflict_artifacts,
|
|
15
|
+
clear_replay_state,
|
|
16
|
+
find_conflict_artifacts,
|
|
17
|
+
load_replay_state,
|
|
18
|
+
save_replay_state,
|
|
19
|
+
)
|
|
20
|
+
from .svn import SvnWorkspace
|
|
21
|
+
|
|
22
|
+
__version__ = "0.4.0"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_version() -> str:
|
|
26
|
+
"""Return package version from installed distribution metadata or fallback to __version__."""
|
|
27
|
+
try:
|
|
28
|
+
from importlib.metadata import version
|
|
29
|
+
|
|
30
|
+
return version("git2svn")
|
|
31
|
+
except Exception:
|
|
32
|
+
return __version__
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"Synchronizer",
|
|
37
|
+
"GitRepo",
|
|
38
|
+
"SvnWorkspace",
|
|
39
|
+
"Patcher",
|
|
40
|
+
"FileChange",
|
|
41
|
+
"parse_ref_arguments",
|
|
42
|
+
"parse_name_status",
|
|
43
|
+
"parse_name_status_z",
|
|
44
|
+
"detect_file_eol",
|
|
45
|
+
"normalize_file_eol",
|
|
46
|
+
"save_replay_state",
|
|
47
|
+
"load_replay_state",
|
|
48
|
+
"clear_replay_state",
|
|
49
|
+
"find_conflict_artifacts",
|
|
50
|
+
"clean_conflict_artifacts",
|
|
51
|
+
"build_parser",
|
|
52
|
+
"parse_cli_args",
|
|
53
|
+
"main",
|
|
54
|
+
"get_version",
|
|
55
|
+
"__version__",
|
|
56
|
+
]
|
git2svn/__main__.py
ADDED
git2svn/cli.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
from .core import Synchronizer
|
|
12
|
+
from .git import GitRepo
|
|
13
|
+
from .patcher import Patcher
|
|
14
|
+
from .state import load_replay_state
|
|
15
|
+
from .svn import SvnWorkspace
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("git2svn")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def find_default_git_dir() -> Path:
|
|
21
|
+
"""Find the Git repository root from current working directory."""
|
|
22
|
+
cwd = Path.cwd()
|
|
23
|
+
try:
|
|
24
|
+
res = subprocess.run(
|
|
25
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
26
|
+
cwd=cwd,
|
|
27
|
+
check=False,
|
|
28
|
+
capture_output=True,
|
|
29
|
+
text=True,
|
|
30
|
+
encoding="utf-8",
|
|
31
|
+
errors="replace",
|
|
32
|
+
)
|
|
33
|
+
if res.returncode == 0 and res.stdout.strip():
|
|
34
|
+
return Path(res.stdout.strip())
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
return cwd
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
41
|
+
common_parser = argparse.ArgumentParser(add_help=False)
|
|
42
|
+
common_parser.add_argument(
|
|
43
|
+
"--git-dir",
|
|
44
|
+
"-g",
|
|
45
|
+
type=Path,
|
|
46
|
+
default=argparse.SUPPRESS,
|
|
47
|
+
help="Path to Git repository (default: detected git root or current directory)",
|
|
48
|
+
)
|
|
49
|
+
common_parser.add_argument(
|
|
50
|
+
"--svn-dir",
|
|
51
|
+
"-s",
|
|
52
|
+
type=Path,
|
|
53
|
+
default=argparse.SUPPRESS,
|
|
54
|
+
help="Path to SVN working copy (default: $SVN_DIR environment variable)",
|
|
55
|
+
)
|
|
56
|
+
common_parser.add_argument(
|
|
57
|
+
"--dry-run",
|
|
58
|
+
"-n",
|
|
59
|
+
action="store_true",
|
|
60
|
+
default=argparse.SUPPRESS,
|
|
61
|
+
help="Print actions without modifying files or executing SVN commands",
|
|
62
|
+
)
|
|
63
|
+
common_parser.add_argument(
|
|
64
|
+
"--verbose",
|
|
65
|
+
"-v",
|
|
66
|
+
action="store_true",
|
|
67
|
+
default=argparse.SUPPRESS,
|
|
68
|
+
help="Enable verbose output",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
parser = argparse.ArgumentParser(
|
|
72
|
+
prog="git2svn",
|
|
73
|
+
description=(
|
|
74
|
+
"Consolidated utility to synchronize Git revisions to an SVN workspace.\n\n"
|
|
75
|
+
"Core Actions:\n"
|
|
76
|
+
" stage NEVER commits. Applies Git changes to the SVN working copy (svn add/rm)\n"
|
|
77
|
+
" leaving files uncommitted for inspection, manual review, or squashing.\n"
|
|
78
|
+
" replay ALWAYS commits. Sequentially ports individual Git commits into SVN history,\n"
|
|
79
|
+
" preserving original author messages, commit order, and providing stateful\n"
|
|
80
|
+
" conflict pause/continue lifecycle management."
|
|
81
|
+
),
|
|
82
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
83
|
+
parents=[common_parser],
|
|
84
|
+
)
|
|
85
|
+
from . import get_version
|
|
86
|
+
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"-V",
|
|
89
|
+
"--version",
|
|
90
|
+
action="version",
|
|
91
|
+
version=f"%(prog)s {get_version()}",
|
|
92
|
+
help="Show program's version number and exit",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
subparsers = parser.add_subparsers(
|
|
96
|
+
dest="command",
|
|
97
|
+
title="commands",
|
|
98
|
+
description="Available commands",
|
|
99
|
+
required=True,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# stage
|
|
103
|
+
parser_stage = subparsers.add_parser(
|
|
104
|
+
"stage",
|
|
105
|
+
parents=[common_parser],
|
|
106
|
+
help="Stage changes in SVN workspace without committing (for review or squash)",
|
|
107
|
+
description=(
|
|
108
|
+
"Stage changes from a Git commit or range in the SVN workspace without committing.\n"
|
|
109
|
+
"Leaves the SVN working copy dirty (uncommitted) so you can inspect diffs,\n"
|
|
110
|
+
"review changes in TortoiseSVN/CLI, or commit manually."
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
parser_stage.add_argument(
|
|
114
|
+
"ref1",
|
|
115
|
+
nargs="?",
|
|
116
|
+
default=None,
|
|
117
|
+
help="Commit hash, branch, or start ref (e.g. 'abc1234' or 'main..feature' or 'main')",
|
|
118
|
+
)
|
|
119
|
+
parser_stage.add_argument("ref2", nargs="?", default=None, help="End ref if range given as two arguments")
|
|
120
|
+
parser_stage.add_argument(
|
|
121
|
+
"--copy",
|
|
122
|
+
action="store_true",
|
|
123
|
+
default=argparse.SUPPRESS,
|
|
124
|
+
help="Brute-force copy modified/added files using Git object DB (bypasses patch; ideal for binaries/conflicts)",
|
|
125
|
+
)
|
|
126
|
+
parser_stage.add_argument(
|
|
127
|
+
"--snapshot",
|
|
128
|
+
action="store_true",
|
|
129
|
+
default=argparse.SUPPRESS,
|
|
130
|
+
help="Mirror the exact tree of ref1 onto SVN without knowing base ref (adds new, removes missing, updates modified)",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# replay
|
|
134
|
+
parser_replay = subparsers.add_parser(
|
|
135
|
+
"replay",
|
|
136
|
+
parents=[common_parser],
|
|
137
|
+
help="Replay commit(s) sequentially onto SVN, committing each with its Git message",
|
|
138
|
+
description=(
|
|
139
|
+
"Replay one or more Git commits sequentially into SVN history.\n"
|
|
140
|
+
"Each commit is applied and committed with its original Git message and metadata.\n"
|
|
141
|
+
"If a patch conflict occurs, replay pauses and persists state for interactive\n"
|
|
142
|
+
"resolution via --continue, --abort, or --skip."
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
parser_replay.add_argument("ref1", nargs="?", default=None, help="Commit hash or start ref")
|
|
146
|
+
parser_replay.add_argument("ref2", nargs="?", default=None, help="End ref if range given as two arguments")
|
|
147
|
+
update_group = parser_replay.add_mutually_exclusive_group()
|
|
148
|
+
update_group.add_argument(
|
|
149
|
+
"-u",
|
|
150
|
+
"--update",
|
|
151
|
+
dest="update",
|
|
152
|
+
action="store_true",
|
|
153
|
+
default=argparse.SUPPRESS,
|
|
154
|
+
help="Run 'svn update' upon successful replay completion (enabled by default)",
|
|
155
|
+
)
|
|
156
|
+
update_group.add_argument(
|
|
157
|
+
"--no-update",
|
|
158
|
+
dest="update",
|
|
159
|
+
action="store_false",
|
|
160
|
+
default=argparse.SUPPRESS,
|
|
161
|
+
help="Disable automatic 'svn update' upon successful replay completion",
|
|
162
|
+
)
|
|
163
|
+
action_group = parser_replay.add_mutually_exclusive_group()
|
|
164
|
+
action_group.add_argument(
|
|
165
|
+
"--continue",
|
|
166
|
+
dest="replay_action",
|
|
167
|
+
action="store_const",
|
|
168
|
+
const="continue",
|
|
169
|
+
default=argparse.SUPPRESS,
|
|
170
|
+
help="Continue an in-progress replay after resolving conflicts",
|
|
171
|
+
)
|
|
172
|
+
action_group.add_argument(
|
|
173
|
+
"--abort",
|
|
174
|
+
dest="replay_action",
|
|
175
|
+
action="store_const",
|
|
176
|
+
const="abort",
|
|
177
|
+
default=argparse.SUPPRESS,
|
|
178
|
+
help="Abort in-progress replay and revert uncommitted changes",
|
|
179
|
+
)
|
|
180
|
+
action_group.add_argument(
|
|
181
|
+
"--skip",
|
|
182
|
+
dest="replay_action",
|
|
183
|
+
action="store_const",
|
|
184
|
+
const="skip",
|
|
185
|
+
default=argparse.SUPPRESS,
|
|
186
|
+
help="Skip the current failed commit and continue with the next",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# setup
|
|
190
|
+
parser_setup = subparsers.add_parser(
|
|
191
|
+
"setup",
|
|
192
|
+
parents=[common_parser],
|
|
193
|
+
help="Automate initial repository configuration, tracking branches, and git aliases",
|
|
194
|
+
description=(
|
|
195
|
+
"Configure repository settings for git2svn on a fresh clone.\n"
|
|
196
|
+
"Automatically detects local trunk and remote SVN mirror tracking branches,\n"
|
|
197
|
+
"sets git2svn.svnDir, git2svn.defaultRange, git2svn.autoUpdate, pull.ff only,\n"
|
|
198
|
+
"and configures git alias.svn-push and alias.svn-pull."
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
parser_setup.add_argument(
|
|
202
|
+
"setup_svn_dir",
|
|
203
|
+
nargs="?",
|
|
204
|
+
type=Path,
|
|
205
|
+
default=None,
|
|
206
|
+
metavar="SVN_DIR",
|
|
207
|
+
help="Path to SVN working copy (optional if already configured or set via --svn-dir)",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return parser
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def handle_setup(git_repo: GitRepo, svn_dir_path: Optional[Path | str]) -> int:
|
|
214
|
+
"""Automate repository configuration, branch detection, and productivity aliases."""
|
|
215
|
+
if not git_repo.is_valid_repo():
|
|
216
|
+
print(f"Error: '{git_repo.root_dir}' is not a valid Git repository.", file=sys.stderr)
|
|
217
|
+
return 1
|
|
218
|
+
|
|
219
|
+
# 1. Resolve SVN working copy path
|
|
220
|
+
resolved_svn: Optional[Path] = None
|
|
221
|
+
if svn_dir_path:
|
|
222
|
+
resolved_svn = Path(svn_dir_path).resolve()
|
|
223
|
+
else:
|
|
224
|
+
# Check existing config or environment variable
|
|
225
|
+
cfg_svn = git_repo.get_config("git2svn.svnDir")
|
|
226
|
+
if cfg_svn:
|
|
227
|
+
resolved_svn = Path(cfg_svn).resolve()
|
|
228
|
+
elif "SVN_DIR" in os.environ:
|
|
229
|
+
resolved_svn = Path(os.environ["SVN_DIR"]).resolve()
|
|
230
|
+
|
|
231
|
+
if not resolved_svn:
|
|
232
|
+
# Interactive prompt if stdin is a tty, otherwise display error
|
|
233
|
+
if sys.stdin.isatty():
|
|
234
|
+
try:
|
|
235
|
+
entered = input("Path to SVN working copy: ").strip()
|
|
236
|
+
if entered:
|
|
237
|
+
resolved_svn = Path(entered).resolve()
|
|
238
|
+
except (EOFError, KeyboardInterrupt):
|
|
239
|
+
print("", file=sys.stderr)
|
|
240
|
+
return 1
|
|
241
|
+
|
|
242
|
+
if not resolved_svn:
|
|
243
|
+
print(
|
|
244
|
+
"Error: SVN working copy path must be provided: 'git2svn setup <path/to/svn>'.",
|
|
245
|
+
file=sys.stderr,
|
|
246
|
+
)
|
|
247
|
+
return 1
|
|
248
|
+
|
|
249
|
+
if not (resolved_svn / ".svn").exists():
|
|
250
|
+
print(
|
|
251
|
+
f"Error: '{resolved_svn}' does not appear to be an SVN working copy (no .svn found).",
|
|
252
|
+
file=sys.stderr,
|
|
253
|
+
)
|
|
254
|
+
return 1
|
|
255
|
+
|
|
256
|
+
# 2. Auto-detect local trunk branch
|
|
257
|
+
local_branches = git_repo.get_local_branches()
|
|
258
|
+
detected_trunk: Optional[str] = None
|
|
259
|
+
if "trunk" in local_branches:
|
|
260
|
+
detected_trunk = "trunk"
|
|
261
|
+
elif "main" in local_branches:
|
|
262
|
+
detected_trunk = "main"
|
|
263
|
+
elif "master" in local_branches:
|
|
264
|
+
detected_trunk = "master"
|
|
265
|
+
else:
|
|
266
|
+
# Use current branch if available
|
|
267
|
+
current = git_repo.get_current_branch()
|
|
268
|
+
if current and current in local_branches:
|
|
269
|
+
detected_trunk = current
|
|
270
|
+
elif local_branches:
|
|
271
|
+
detected_trunk = local_branches[0]
|
|
272
|
+
|
|
273
|
+
# 3. Auto-detect remote tracking mirror branch
|
|
274
|
+
remote_branches = git_repo.get_remote_branches()
|
|
275
|
+
detected_mirror: Optional[str] = None
|
|
276
|
+
preferred_remotes = ["svn-mirror/trunk", "origin/trunk", "svn/trunk", "mirror/trunk"]
|
|
277
|
+
for pref in preferred_remotes:
|
|
278
|
+
if pref in remote_branches:
|
|
279
|
+
detected_mirror = pref
|
|
280
|
+
break
|
|
281
|
+
|
|
282
|
+
if not detected_mirror and detected_trunk:
|
|
283
|
+
# Check for remote branch matching <remote>/<detected_trunk>
|
|
284
|
+
candidates = [b for b in remote_branches if b.endswith(f"/{detected_trunk}")]
|
|
285
|
+
if candidates:
|
|
286
|
+
# Prefer svn-mirror if present
|
|
287
|
+
svn_cand = [c for c in candidates if "svn" in c.lower() or "mirror" in c.lower()]
|
|
288
|
+
detected_mirror = svn_cand[0] if svn_cand else candidates[0]
|
|
289
|
+
|
|
290
|
+
# 4. Apply Git configuration
|
|
291
|
+
# SVN directory path (use forward slashes for cross-platform consistency in git config)
|
|
292
|
+
svn_dir_str = str(resolved_svn).replace("\\", "/")
|
|
293
|
+
git_repo.set_config("git2svn.svnDir", svn_dir_str)
|
|
294
|
+
git_repo.set_config("pull.ff", "only")
|
|
295
|
+
|
|
296
|
+
default_range: Optional[str] = None
|
|
297
|
+
if detected_mirror and detected_trunk:
|
|
298
|
+
default_range = f"{detected_mirror}..{detected_trunk}"
|
|
299
|
+
git_repo.set_config("git2svn.defaultRange", default_range)
|
|
300
|
+
|
|
301
|
+
# 5. Configure productivity aliases
|
|
302
|
+
mirror_remote = detected_mirror.split("/")[0] if detected_mirror else "svn-mirror"
|
|
303
|
+
trunk_name = detected_trunk or "trunk"
|
|
304
|
+
|
|
305
|
+
push_script = (
|
|
306
|
+
f"!f() {{ git2svn replay && run-svn2git-sync && git fetch {mirror_remote} && "
|
|
307
|
+
f"git checkout {trunk_name} && git reset --hard {detected_mirror or f'{mirror_remote}/{trunk_name}'}; }}; f"
|
|
308
|
+
)
|
|
309
|
+
pull_script = (
|
|
310
|
+
f"!f() {{ run-svn2git-sync && git fetch {mirror_remote} && "
|
|
311
|
+
f"git checkout {trunk_name} && git reset --hard {detected_mirror or f'{mirror_remote}/{trunk_name}'}; }}; f"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
git_repo.set_config("alias.svn-push", push_script)
|
|
315
|
+
git_repo.set_config("alias.svn-pull", pull_script)
|
|
316
|
+
|
|
317
|
+
print("Successfully configured git2svn:")
|
|
318
|
+
print(f" git2svn.svnDir = {svn_dir_str}")
|
|
319
|
+
if default_range:
|
|
320
|
+
print(f" git2svn.defaultRange= {default_range}")
|
|
321
|
+
else:
|
|
322
|
+
print(" git2svn.defaultRange= (not set; could not detect mirror branch)")
|
|
323
|
+
print(" pull.ff = only")
|
|
324
|
+
print(f" alias.svn-push = {push_script}")
|
|
325
|
+
print(f" alias.svn-pull = {pull_script}")
|
|
326
|
+
return 0
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def parse_cli_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
|
|
330
|
+
parser = build_parser()
|
|
331
|
+
effective_argv = sys.argv[1:] if argv is None else argv
|
|
332
|
+
if not effective_argv:
|
|
333
|
+
parser.print_help(sys.stderr)
|
|
334
|
+
sys.exit(1)
|
|
335
|
+
|
|
336
|
+
namespace = argparse.Namespace(
|
|
337
|
+
git_dir=None,
|
|
338
|
+
svn_dir=None,
|
|
339
|
+
dry_run=False,
|
|
340
|
+
verbose=False,
|
|
341
|
+
copy=False,
|
|
342
|
+
snapshot=False,
|
|
343
|
+
replay_action=None,
|
|
344
|
+
update=None,
|
|
345
|
+
)
|
|
346
|
+
return parser.parse_args(argv, namespace=namespace)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
350
|
+
args = parse_cli_args(argv)
|
|
351
|
+
|
|
352
|
+
log_level = logging.DEBUG if args.verbose else logging.INFO
|
|
353
|
+
logging.basicConfig(level=log_level, format="[%(levelname)s] %(message)s")
|
|
354
|
+
|
|
355
|
+
git_dir = args.git_dir
|
|
356
|
+
if not git_dir and args.command == "replay" and getattr(args, "replay_action", None):
|
|
357
|
+
cwd = Path.cwd()
|
|
358
|
+
state = load_replay_state(cwd)
|
|
359
|
+
if state and "git_dir" in state:
|
|
360
|
+
git_dir = Path(state["git_dir"])
|
|
361
|
+
|
|
362
|
+
git_dir = git_dir or find_default_git_dir()
|
|
363
|
+
git_repo = GitRepo(git_dir)
|
|
364
|
+
if not git_repo.is_valid_repo():
|
|
365
|
+
print(f"Error: '{git_dir}' is not a valid Git repository.", file=sys.stderr)
|
|
366
|
+
return 1
|
|
367
|
+
|
|
368
|
+
if args.command == "setup":
|
|
369
|
+
svn_arg = getattr(args, "setup_svn_dir", None) or getattr(args, "svn_dir", None)
|
|
370
|
+
return handle_setup(git_repo, svn_arg)
|
|
371
|
+
|
|
372
|
+
# 1. SVN workspace directory resolution: CLI arg -> $SVN_DIR -> git config -> replay cwd
|
|
373
|
+
svn_dir = args.svn_dir or (Path(os.environ["SVN_DIR"]) if "SVN_DIR" in os.environ else None)
|
|
374
|
+
if not svn_dir:
|
|
375
|
+
config_svn = git_repo.get_config("git2svn.svnDir")
|
|
376
|
+
if config_svn:
|
|
377
|
+
svn_dir = Path(config_svn)
|
|
378
|
+
|
|
379
|
+
# Auto-detect svn_dir from cwd for replay actions if cwd is an SVN checkout
|
|
380
|
+
if not svn_dir and args.command == "replay" and getattr(args, "replay_action", None):
|
|
381
|
+
cwd = Path.cwd()
|
|
382
|
+
if (cwd / ".svn").exists() or load_replay_state(cwd):
|
|
383
|
+
svn_dir = cwd
|
|
384
|
+
|
|
385
|
+
if not svn_dir:
|
|
386
|
+
print(
|
|
387
|
+
"Error: SVN workspace directory must be specified via --svn-dir, SVN_DIR env, or 'git config git2svn.svnDir <path>'.",
|
|
388
|
+
file=sys.stderr,
|
|
389
|
+
)
|
|
390
|
+
return 1
|
|
391
|
+
|
|
392
|
+
# Config fallbacks for dry_run and copy
|
|
393
|
+
dry_run = args.dry_run
|
|
394
|
+
if not dry_run:
|
|
395
|
+
config_dry_run = git_repo.get_config_bool("git2svn.dryRun")
|
|
396
|
+
if config_dry_run is not None:
|
|
397
|
+
dry_run = config_dry_run
|
|
398
|
+
|
|
399
|
+
use_copy = getattr(args, "copy", False)
|
|
400
|
+
if not use_copy:
|
|
401
|
+
config_copy = git_repo.get_config_bool("git2svn.copy")
|
|
402
|
+
if config_copy is not None:
|
|
403
|
+
use_copy = config_copy
|
|
404
|
+
|
|
405
|
+
# auto_update defaults to True unless CLI --no-update or git config git2svn.autoUpdate false
|
|
406
|
+
auto_update = getattr(args, "update", None)
|
|
407
|
+
if auto_update is None:
|
|
408
|
+
config_update = git_repo.get_config_bool("git2svn.autoUpdate")
|
|
409
|
+
if config_update is not None:
|
|
410
|
+
auto_update = config_update
|
|
411
|
+
else:
|
|
412
|
+
auto_update = True
|
|
413
|
+
|
|
414
|
+
svn_workspace = SvnWorkspace(svn_dir, dry_run=dry_run)
|
|
415
|
+
if not svn_workspace.is_valid_workspace() and not dry_run:
|
|
416
|
+
print(f"Error: '{svn_dir}' does not appear to be an SVN working copy (no .svn found).", file=sys.stderr)
|
|
417
|
+
return 1
|
|
418
|
+
|
|
419
|
+
patcher = Patcher(svn_dir, dry_run=dry_run)
|
|
420
|
+
sync_mgr = Synchronizer(git_repo, svn_workspace, patcher, dry_run=dry_run, auto_update=auto_update)
|
|
421
|
+
|
|
422
|
+
# Fallback to git2svn.defaultRange if ref1 is omitted
|
|
423
|
+
ref1 = args.ref1
|
|
424
|
+
ref2 = getattr(args, "ref2", None)
|
|
425
|
+
if not ref1 and args.command in ("stage", "replay"):
|
|
426
|
+
# Replay actions (--continue, --abort, --skip) do not need ref1
|
|
427
|
+
if args.command == "replay" and getattr(args, "replay_action", None):
|
|
428
|
+
pass
|
|
429
|
+
else:
|
|
430
|
+
default_range = git_repo.get_config("git2svn.defaultRange")
|
|
431
|
+
if default_range:
|
|
432
|
+
ref1 = default_range
|
|
433
|
+
ref2 = None
|
|
434
|
+
logger.info("Using configured default range from git2svn.defaultRange: '%s'", default_range)
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
if args.command == "stage":
|
|
438
|
+
if not ref1:
|
|
439
|
+
print(
|
|
440
|
+
"Error: stage requires a commit or range (e.g. 'git2svn stage main..feature') "
|
|
441
|
+
"or 'git config git2svn.defaultRange <range>'.",
|
|
442
|
+
file=sys.stderr,
|
|
443
|
+
)
|
|
444
|
+
return 1
|
|
445
|
+
sync_mgr.stage(
|
|
446
|
+
ref1,
|
|
447
|
+
ref2,
|
|
448
|
+
use_copy=use_copy,
|
|
449
|
+
snapshot=getattr(args, "snapshot", False),
|
|
450
|
+
)
|
|
451
|
+
elif args.command == "replay":
|
|
452
|
+
action = getattr(args, "replay_action", None)
|
|
453
|
+
if action == "continue":
|
|
454
|
+
sync_mgr.replay_continue()
|
|
455
|
+
elif action == "abort":
|
|
456
|
+
sync_mgr.replay_abort()
|
|
457
|
+
elif action == "skip":
|
|
458
|
+
sync_mgr.replay_skip()
|
|
459
|
+
else:
|
|
460
|
+
if not ref1:
|
|
461
|
+
print(
|
|
462
|
+
"Error: replay requires a commit or range unless using --continue, --abort, or --skip. "
|
|
463
|
+
"You can also configure a default range with 'git config git2svn.defaultRange <range>'.",
|
|
464
|
+
file=sys.stderr,
|
|
465
|
+
)
|
|
466
|
+
return 1
|
|
467
|
+
sync_mgr.replay(ref1, ref2)
|
|
468
|
+
else:
|
|
469
|
+
return 1
|
|
470
|
+
except subprocess.CalledProcessError as e:
|
|
471
|
+
logger.error("Process failed with returncode %s", e.returncode)
|
|
472
|
+
return e.returncode
|
|
473
|
+
except Exception as e:
|
|
474
|
+
logger.error("Operation failed: %s", e)
|
|
475
|
+
if args.verbose:
|
|
476
|
+
import traceback
|
|
477
|
+
|
|
478
|
+
traceback.print_exc()
|
|
479
|
+
return 1
|
|
480
|
+
|
|
481
|
+
return 0
|