Skip to content

Commit 17f218c

Browse files
committed
cli/fmt(feat[--all]): Add --all flag to format all discovered configs
why: Users need a way to format all their vcspull configuration files at once, similar to how sync discovers and processes all configs. what: - Add --all flag to fmt command argument parser - Refactor format_config_file() into format_single_config() for individual files - Update format_config_file() to handle both single file and --all modes - Discover configs using find_config_files(include_home=True) when --all is used - Include local .vcspull.yaml/json files in current directory - Add clear progress reporting showing all files found and their status - Add summary at the end showing success count - Update existing tests to pass new format_all parameter - Add comprehensive test for --all functionality with mocked config discovery - Fix type annotations for test helper functions refs: Enhancement to make vcspull fmt more powerful for managing multiple configs
1 parent 0ea1c60 commit 17f218c

File tree

3 files changed

+214
-43
lines changed

3 files changed

+214
-43
lines changed

src/vcspull/cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,4 @@ def cli(_args: list[str] | None = None) -> None:
145145
yes=args.yes,
146146
)
147147
elif args.subparser_name == "fmt":
148-
format_config_file(args.config, args.write)
148+
format_config_file(args.config, args.write, args.all)

src/vcspull/cli/fmt.py

Lines changed: 136 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from colorama import Fore, Style
1111

1212
from vcspull._internal.config_reader import ConfigReader
13-
from vcspull.config import find_home_config_files, save_config_yaml
13+
from vcspull.config import find_config_files, find_home_config_files, save_config_yaml
1414

1515
if t.TYPE_CHECKING:
1616
import argparse
@@ -33,6 +33,11 @@ def create_fmt_subparser(parser: argparse.ArgumentParser) -> None:
3333
action="store_true",
3434
help="Write formatted configuration back to file",
3535
)
36+
parser.add_argument(
37+
"--all",
38+
action="store_true",
39+
help="Format all discovered config files (home, config dir, and current dir)",
40+
)
3641

3742

3843
def normalize_repo_config(repo_data: t.Any) -> dict[str, t.Any]:
@@ -118,45 +123,24 @@ def format_config(config_data: dict[str, t.Any]) -> tuple[dict[str, t.Any], int]
118123
return formatted, changes
119124

120125

121-
def format_config_file(
122-
config_file_path_str: str | None,
126+
def format_single_config(
127+
config_file_path: pathlib.Path,
123128
write: bool,
124-
) -> None:
125-
"""Format a vcspull configuration file.
129+
) -> bool:
130+
"""Format a single vcspull configuration file.
126131
127132
Parameters
128133
----------
129-
config_file_path_str : str | None
130-
Path to config file, or None to use default
134+
config_file_path : pathlib.Path
135+
Path to config file
131136
write : bool
132137
Whether to write changes back to file
133-
"""
134-
# Determine config file
135-
config_file_path: pathlib.Path
136-
if config_file_path_str:
137-
config_file_path = pathlib.Path(config_file_path_str).expanduser().resolve()
138-
else:
139-
home_configs = find_home_config_files(filetype=["yaml"])
140-
if not home_configs:
141-
# Try local .vcspull.yaml
142-
local_config = pathlib.Path.cwd() / ".vcspull.yaml"
143-
if local_config.exists():
144-
config_file_path = local_config
145-
else:
146-
log.error(
147-
"%s✗%s No configuration file found. Create .vcspull.yaml first.",
148-
Fore.RED,
149-
Style.RESET_ALL,
150-
)
151-
return
152-
elif len(home_configs) > 1:
153-
log.error(
154-
"Multiple home config files found, please specify one with -c/--config",
155-
)
156-
return
157-
else:
158-
config_file_path = home_configs[0]
159138
139+
Returns
140+
-------
141+
bool
142+
True if formatting was successful, False otherwise
143+
"""
160144
# Check if file exists
161145
if not config_file_path.exists():
162146
log.error(
@@ -167,7 +151,7 @@ def format_config_file(
167151
config_file_path,
168152
Style.RESET_ALL,
169153
)
170-
return
154+
return False
171155

172156
# Load existing config
173157
try:
@@ -177,12 +161,12 @@ def format_config_file(
177161
"Config file %s is not a valid YAML dictionary.",
178162
config_file_path,
179163
)
180-
return
164+
return False
181165
except Exception:
182166
log.exception("Error loading config from %s", config_file_path)
183167
if log.isEnabledFor(logging.DEBUG):
184168
traceback.print_exc()
185-
return
169+
return False
186170

187171
# Format the configuration
188172
formatted_config, change_count = format_config(raw_config)
@@ -196,7 +180,7 @@ def format_config_file(
196180
config_file_path,
197181
Style.RESET_ALL,
198182
)
199-
return
183+
return True
200184

201185
# Show what would be changed
202186
log.info(
@@ -280,6 +264,7 @@ def format_config_file(
280264
log.exception("Error saving formatted config to %s", config_file_path)
281265
if log.isEnabledFor(logging.DEBUG):
282266
traceback.print_exc()
267+
return False
283268
else:
284269
log.info(
285270
"\n%s→%s Run with %s--write%s to apply these formatting changes.",
@@ -288,3 +273,117 @@ def format_config_file(
288273
Fore.CYAN,
289274
Style.RESET_ALL,
290275
)
276+
277+
return True
278+
279+
280+
def format_config_file(
281+
config_file_path_str: str | None,
282+
write: bool,
283+
format_all: bool = False,
284+
) -> None:
285+
"""Format vcspull configuration file(s).
286+
287+
Parameters
288+
----------
289+
config_file_path_str : str | None
290+
Path to config file, or None to use default
291+
write : bool
292+
Whether to write changes back to file
293+
format_all : bool
294+
If True, format all discovered config files
295+
"""
296+
if format_all:
297+
# Format all discovered config files
298+
config_files = find_config_files(include_home=True)
299+
300+
# Also check for local .vcspull.yaml
301+
local_yaml = pathlib.Path.cwd() / ".vcspull.yaml"
302+
if local_yaml.exists() and local_yaml not in config_files:
303+
config_files.append(local_yaml)
304+
305+
# Also check for local .vcspull.json
306+
local_json = pathlib.Path.cwd() / ".vcspull.json"
307+
if local_json.exists() and local_json not in config_files:
308+
config_files.append(local_json)
309+
310+
if not config_files:
311+
log.error(
312+
"%s✗%s No configuration files found.",
313+
Fore.RED,
314+
Style.RESET_ALL,
315+
)
316+
return
317+
318+
log.info(
319+
"%si%s Found %s%d%s configuration %s to format:",
320+
Fore.CYAN,
321+
Style.RESET_ALL,
322+
Fore.YELLOW,
323+
len(config_files),
324+
Style.RESET_ALL,
325+
"file" if len(config_files) == 1 else "files",
326+
)
327+
328+
for config_file in config_files:
329+
log.info(
330+
" %s•%s %s%s%s",
331+
Fore.BLUE,
332+
Style.RESET_ALL,
333+
Fore.CYAN,
334+
config_file,
335+
Style.RESET_ALL,
336+
)
337+
338+
log.info("") # Empty line for readability
339+
340+
success_count = 0
341+
for config_file in config_files:
342+
if format_single_config(config_file, write):
343+
success_count += 1
344+
345+
# Summary
346+
if success_count == len(config_files):
347+
log.info(
348+
"\n%s✓%s All %d configuration files processed successfully.",
349+
Fore.GREEN,
350+
Style.RESET_ALL,
351+
len(config_files),
352+
)
353+
else:
354+
log.info(
355+
"\n%si%s Processed %d/%d configuration files successfully.",
356+
Fore.CYAN,
357+
Style.RESET_ALL,
358+
success_count,
359+
len(config_files),
360+
)
361+
else:
362+
# Format single config file
363+
if config_file_path_str:
364+
config_file_path = pathlib.Path(config_file_path_str).expanduser().resolve()
365+
else:
366+
home_configs = find_home_config_files(filetype=["yaml"])
367+
if not home_configs:
368+
# Try local .vcspull.yaml
369+
local_config = pathlib.Path.cwd() / ".vcspull.yaml"
370+
if local_config.exists():
371+
config_file_path = local_config
372+
else:
373+
log.error(
374+
"%s✗%s No configuration file found. "
375+
"Create .vcspull.yaml first.",
376+
Fore.RED,
377+
Style.RESET_ALL,
378+
)
379+
return
380+
elif len(home_configs) > 1:
381+
log.error(
382+
"Multiple home config files found, "
383+
"please specify one with -c/--config",
384+
)
385+
return
386+
else:
387+
config_file_path = home_configs[0]
388+
389+
format_single_config(config_file_path, write)

tests/cli/test_fmt.py

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ def test_format_file_no_write(
211211
yaml.dump(original_config, f)
212212

213213
with caplog.at_level(logging.INFO):
214-
format_config_file(str(config_file), write=False)
214+
format_config_file(str(config_file), write=False, format_all=False)
215215

216216
# Check that file was not modified
217217
with config_file.open(encoding="utf-8") as f:
@@ -243,7 +243,7 @@ def test_format_file_with_write(
243243
yaml.dump(original_config, f)
244244

245245
with caplog.at_level(logging.INFO):
246-
format_config_file(str(config_file), write=True)
246+
format_config_file(str(config_file), write=True, format_all=False)
247247

248248
# Check that file was modified
249249
with config_file.open(encoding="utf-8") as f:
@@ -279,7 +279,7 @@ def test_already_formatted(
279279
yaml.dump(config, f)
280280

281281
with caplog.at_level(logging.INFO):
282-
format_config_file(str(config_file), write=False)
282+
format_config_file(str(config_file), write=False, format_all=False)
283283

284284
assert "already formatted correctly" in caplog.text
285285

@@ -320,7 +320,7 @@ def test_no_config_found(
320320
monkeypatch.chdir(tmp_path)
321321

322322
with caplog.at_level(logging.ERROR):
323-
format_config_file(None, write=False)
323+
format_config_file(None, write=False, format_all=False)
324324

325325
assert "No configuration file found" in caplog.text
326326

@@ -345,9 +345,81 @@ def test_detailed_change_reporting(
345345
config_file.write_text(yaml_content, encoding="utf-8")
346346

347347
with caplog.at_level(logging.INFO):
348-
format_config_file(str(config_file), write=False)
348+
format_config_file(str(config_file), write=False, format_all=False)
349349

350350
# Check detailed change reporting
351351
assert "3 repositories from compact to verbose format" in caplog.text
352352
assert "2 repositories from 'url' to 'repo' key" in caplog.text
353353
assert "Directories will be sorted alphabetically" in caplog.text
354+
355+
def test_format_all_configs(
356+
self,
357+
tmp_path: pathlib.Path,
358+
caplog: LogCaptureFixture,
359+
monkeypatch: pytest.MonkeyPatch,
360+
) -> None:
361+
"""Test formatting all discovered config files."""
362+
# Create test config directory structure
363+
config_dir = tmp_path / ".config" / "vcspull"
364+
config_dir.mkdir(parents=True)
365+
366+
# Create home config (already formatted correctly)
367+
home_config = tmp_path / ".vcspull.yaml"
368+
home_config.write_text(
369+
yaml.dump({"~/projects/": {"repo1": {"repo": "url1"}}}),
370+
encoding="utf-8",
371+
)
372+
373+
# Create config in config directory (needs sorting)
374+
config1 = config_dir / "work.yaml"
375+
config1_content = """~/work/:
376+
repo2: url2
377+
repo1: url1
378+
"""
379+
config1.write_text(config1_content, encoding="utf-8")
380+
381+
# Create local config
382+
local_config = tmp_path / "project" / ".vcspull.yaml"
383+
local_config.parent.mkdir()
384+
local_config.write_text(
385+
yaml.dump({"./": {"repo3": {"url": "url3"}}}),
386+
encoding="utf-8",
387+
)
388+
389+
# Mock find functions to return our test configs
390+
def mock_find_config_files(include_home: bool = False) -> list[pathlib.Path]:
391+
files = [config1]
392+
if include_home:
393+
files.insert(0, home_config)
394+
return files
395+
396+
def mock_find_home_config_files(filetype: list[str] | None = None) -> list[pathlib.Path]:
397+
return [home_config]
398+
399+
# Change to project directory
400+
monkeypatch.chdir(local_config.parent)
401+
monkeypatch.setattr(
402+
"vcspull.cli.fmt.find_config_files",
403+
mock_find_config_files,
404+
)
405+
monkeypatch.setattr(
406+
"vcspull.cli.fmt.find_home_config_files",
407+
mock_find_home_config_files,
408+
)
409+
410+
with caplog.at_level(logging.INFO):
411+
format_config_file(None, write=False, format_all=True)
412+
413+
# Check that all configs were found
414+
assert "Found 3 configuration files to format" in caplog.text
415+
assert str(home_config) in caplog.text
416+
assert str(config1) in caplog.text
417+
assert str(local_config) in caplog.text
418+
419+
# Check processing messages
420+
assert "already formatted correctly" in caplog.text # home_config
421+
assert "3 formatting issues" in caplog.text # config1 has 2 compact + needs sorting
422+
assert "2 repositories from compact to verbose format" in caplog.text # config1
423+
assert "Repositories in ~/work/ will be sorted" in caplog.text # config1
424+
assert "1 repository from 'url' to 'repo' key" in caplog.text # local_config
425+
assert "All 3 configuration files processed successfully" in caplog.text

0 commit comments

Comments
 (0)