3
3
from __future__ import annotations
4
4
5
5
import argparse
6
+ import json
6
7
import sys
7
8
import typing as t
9
+ from pathlib import Path
10
+
11
+ from colorama import init
8
12
9
13
from vcspull ._internal import logger
10
14
from vcspull .config import load_config , resolve_includes
15
+ from vcspull .operations import detect_repositories , sync_repositories
16
+
17
+ # Initialize colorama
18
+ init (autoreset = True )
11
19
12
20
13
21
def cli (argv : list [str ] | None = None ) -> int :
@@ -31,6 +39,7 @@ def cli(argv: list[str] | None = None) -> int:
31
39
# Add subparsers for each command
32
40
add_info_command (subparsers )
33
41
add_sync_command (subparsers )
42
+ add_detect_command (subparsers )
34
43
35
44
args = parser .parse_args (argv if argv is not None else sys .argv [1 :])
36
45
@@ -43,6 +52,8 @@ def cli(argv: list[str] | None = None) -> int:
43
52
return info_command (args )
44
53
if args .command == "sync" :
45
54
return sync_command (args )
55
+ if args .command == "detect" :
56
+ return detect_command (args )
46
57
47
58
return 0
48
59
@@ -62,6 +73,12 @@ def add_info_command(subparsers: argparse._SubParsersAction[t.Any]) -> None:
62
73
help = "Path to configuration file" ,
63
74
default = "~/.config/vcspull/vcspull.yaml" ,
64
75
)
76
+ parser .add_argument (
77
+ "-j" ,
78
+ "--json" ,
79
+ action = "store_true" ,
80
+ help = "Output in JSON format" ,
81
+ )
65
82
66
83
67
84
def add_sync_command (subparsers : argparse ._SubParsersAction [t .Any ]) -> None :
@@ -79,6 +96,66 @@ def add_sync_command(subparsers: argparse._SubParsersAction[t.Any]) -> None:
79
96
help = "Path to configuration file" ,
80
97
default = "~/.config/vcspull/vcspull.yaml" ,
81
98
)
99
+ parser .add_argument (
100
+ "-p" ,
101
+ "--path" ,
102
+ action = "append" ,
103
+ help = "Sync only repositories at the specified path(s)" ,
104
+ dest = "paths" ,
105
+ )
106
+ parser .add_argument (
107
+ "-s" ,
108
+ "--sequential" ,
109
+ action = "store_true" ,
110
+ help = "Sync repositories sequentially instead of in parallel" ,
111
+ )
112
+ parser .add_argument (
113
+ "-v" ,
114
+ "--verbose" ,
115
+ action = "store_true" ,
116
+ help = "Enable verbose output" ,
117
+ )
118
+
119
+
120
+ def add_detect_command (subparsers : argparse ._SubParsersAction [t .Any ]) -> None :
121
+ """Add the detect command to the parser.
122
+
123
+ Parameters
124
+ ----------
125
+ subparsers : argparse._SubParsersAction
126
+ Subparsers action to add the command to
127
+ """
128
+ parser = subparsers .add_parser ("detect" , help = "Detect repositories in directories" )
129
+ parser .add_argument (
130
+ "directories" ,
131
+ nargs = "*" ,
132
+ help = "Directories to search for repositories" ,
133
+ default = ["." ],
134
+ )
135
+ parser .add_argument (
136
+ "-r" ,
137
+ "--recursive" ,
138
+ action = "store_true" ,
139
+ help = "Search directories recursively" ,
140
+ )
141
+ parser .add_argument (
142
+ "-d" ,
143
+ "--depth" ,
144
+ type = int ,
145
+ default = 2 ,
146
+ help = "Maximum directory depth when searching recursively" ,
147
+ )
148
+ parser .add_argument (
149
+ "-j" ,
150
+ "--json" ,
151
+ action = "store_true" ,
152
+ help = "Output in JSON format" ,
153
+ )
154
+ parser .add_argument (
155
+ "-o" ,
156
+ "--output" ,
157
+ help = "Write detected repositories to config file" ,
158
+ )
82
159
83
160
84
161
def info_command (args : argparse .Namespace ) -> int :
@@ -98,13 +175,29 @@ def info_command(args: argparse.Namespace) -> int:
98
175
config = load_config (args .config )
99
176
config = resolve_includes (config , args .config )
100
177
101
- for _repo in config .repositories :
102
- pass
178
+ if args .json :
179
+ # JSON output
180
+ config .model_dump ()
181
+ else :
182
+ # Human-readable output
183
+
184
+ # Show settings
185
+ for _key , _value in config .settings .model_dump ().items ():
186
+ pass
187
+
188
+ # Show repositories
189
+ for repo in config .repositories :
190
+ if repo .remotes :
191
+ for _remote_name , _remote_url in repo .remotes .items ():
192
+ pass
193
+
194
+ if repo .rev :
195
+ pass
196
+
197
+ return 0
103
198
except Exception as e :
104
199
logger .error (f"Error: { e } " )
105
200
return 1
106
- else :
107
- return 0
108
201
109
202
110
203
def sync_command (args : argparse .Namespace ) -> int :
@@ -124,9 +217,111 @@ def sync_command(args: argparse.Namespace) -> int:
124
217
config = load_config (args .config )
125
218
config = resolve_includes (config , args .config )
126
219
127
- # TODO: Implement actual sync logic
220
+ # Set up some progress reporting
221
+ len (config .repositories )
222
+ if args .paths :
223
+ filtered_repos = [
224
+ repo
225
+ for repo in config .repositories
226
+ if any (
227
+ Path (repo .path )
228
+ .expanduser ()
229
+ .resolve ()
230
+ .as_posix ()
231
+ .startswith (Path (p ).expanduser ().resolve ().as_posix ())
232
+ for p in args .paths
233
+ )
234
+ ]
235
+ len (filtered_repos )
236
+
237
+ # Run the sync operation
238
+ results = sync_repositories (
239
+ config ,
240
+ paths = args .paths ,
241
+ parallel = not args .sequential ,
242
+ )
243
+
244
+ # Report results
245
+ sum (1 for success in results .values () if success )
246
+ failure_count = sum (1 for success in results .values () if not success )
247
+
248
+ # Use a shorter line to address E501
249
+
250
+ # Return non-zero if any sync failed
251
+ if failure_count == 0 :
252
+ return 0
253
+ return 1
128
254
except Exception as e :
129
255
logger .error (f"Error: { e } " )
130
256
return 1
131
- else :
257
+
258
+
259
+ def detect_command (args : argparse .Namespace ) -> int :
260
+ """Handle the detect command.
261
+
262
+ Parameters
263
+ ----------
264
+ args : argparse.Namespace
265
+ Command line arguments
266
+
267
+ Returns
268
+ -------
269
+ int
270
+ Exit code
271
+ """
272
+ try :
273
+ # Detect repositories
274
+ repos = detect_repositories (
275
+ args .directories ,
276
+ recursive = args .recursive ,
277
+ depth = args .depth ,
278
+ )
279
+
280
+ if not repos :
281
+ return 0
282
+
283
+ # Output results
284
+ if args .json :
285
+ # JSON output
286
+ [repo .model_dump () for repo in repos ]
287
+ else :
288
+ # Human-readable output
289
+ for _repo in repos :
290
+ pass
291
+
292
+ # Optionally write to configuration file
293
+ if args .output :
294
+ from vcspull .config .models import Settings , VCSPullConfig
295
+
296
+ output_path = Path (args .output ).expanduser ().resolve ()
297
+ output_dir = output_path .parent
298
+
299
+ # Create directory if it doesn't exist
300
+ if not output_dir .exists ():
301
+ output_dir .mkdir (parents = True )
302
+
303
+ # Create config with detected repositories
304
+ config = VCSPullConfig (
305
+ settings = Settings (),
306
+ repositories = repos ,
307
+ )
308
+
309
+ # Write config to file
310
+ with output_path .open ("w" , encoding = "utf-8" ) as f :
311
+ if output_path .suffix .lower () in {".yaml" , ".yml" }:
312
+ import yaml
313
+
314
+ yaml .dump (config .model_dump (), f , default_flow_style = False )
315
+ elif output_path .suffix .lower () == ".json" :
316
+ json .dump (config .model_dump (), f , indent = 2 )
317
+ else :
318
+ error_msg = f"Unsupported file format: { output_path .suffix } "
319
+ raise ValueError (error_msg )
320
+
321
+ # Split the line to avoid E501
322
+
323
+ return 0
132
324
return 0
325
+ except Exception as e :
326
+ logger .error (f"Error: { e } " )
327
+ return 1
0 commit comments