33from __future__ import annotations
44
55import argparse
6+ import json
67import sys
78import typing as t
9+ from pathlib import Path
10+
11+ from colorama import init
812
913from vcspull ._internal import logger
1014from vcspull .config import load_config , resolve_includes
15+ from vcspull .operations import detect_repositories , sync_repositories
16+
17+ # Initialize colorama
18+ init (autoreset = True )
1119
1220
1321def cli (argv : list [str ] | None = None ) -> int :
@@ -31,6 +39,7 @@ def cli(argv: list[str] | None = None) -> int:
3139 # Add subparsers for each command
3240 add_info_command (subparsers )
3341 add_sync_command (subparsers )
42+ add_detect_command (subparsers )
3443
3544 args = parser .parse_args (argv if argv is not None else sys .argv [1 :])
3645
@@ -43,6 +52,8 @@ def cli(argv: list[str] | None = None) -> int:
4352 return info_command (args )
4453 if args .command == "sync" :
4554 return sync_command (args )
55+ if args .command == "detect" :
56+ return detect_command (args )
4657
4758 return 0
4859
@@ -62,6 +73,12 @@ def add_info_command(subparsers: argparse._SubParsersAction[t.Any]) -> None:
6273 help = "Path to configuration file" ,
6374 default = "~/.config/vcspull/vcspull.yaml" ,
6475 )
76+ parser .add_argument (
77+ "-j" ,
78+ "--json" ,
79+ action = "store_true" ,
80+ help = "Output in JSON format" ,
81+ )
6582
6683
6784def add_sync_command (subparsers : argparse ._SubParsersAction [t .Any ]) -> None :
@@ -79,6 +96,66 @@ def add_sync_command(subparsers: argparse._SubParsersAction[t.Any]) -> None:
7996 help = "Path to configuration file" ,
8097 default = "~/.config/vcspull/vcspull.yaml" ,
8198 )
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+ )
82159
83160
84161def info_command (args : argparse .Namespace ) -> int :
@@ -98,13 +175,29 @@ def info_command(args: argparse.Namespace) -> int:
98175 config = load_config (args .config )
99176 config = resolve_includes (config , args .config )
100177
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
103198 except Exception as e :
104199 logger .error (f"Error: { e } " )
105200 return 1
106- else :
107- return 0
108201
109202
110203def sync_command (args : argparse .Namespace ) -> int :
@@ -124,9 +217,111 @@ def sync_command(args: argparse.Namespace) -> int:
124217 config = load_config (args .config )
125218 config = resolve_includes (config , args .config )
126219
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
128254 except Exception as e :
129255 logger .error (f"Error: { e } " )
130256 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
132324 return 0
325+ except Exception as e :
326+ logger .error (f"Error: { e } " )
327+ return 1
0 commit comments