Skip to content

Commit ac99b84

Browse files
committed
cli(feat[commands]): add import/fmt subcommands and streaming fixes
why: - expose configuration management (import, import --scan, fmt) through the main CLI - keep user-facing output visible after the logging refactor - ensure default config readers use UTF-8 consistently what: - wire import/import --scan/fmt subcommands into `vcspull` CLI entry and parser - implement `cli/import_.py`, `cli/fmt.py` plus extensive tests - centralize config saves via `save_config_yaml` and open configs as UTF-8 - refactor `setup_logger` to replace NullHandler with stdout stream and add regression test for CLI output - expand logging test suite to cover formatter and propagation behavior This keeps the subject succinct while the bullets break down the “why/what” of the large diff. Adjust component prefixes as needed if you prefer more granularity (e.g., separate commits per concern), but this structure documents all the key changes in one place.
1 parent ffc9b70 commit ac99b84

File tree

11 files changed

+3009
-31
lines changed

11 files changed

+3009
-31
lines changed

src/vcspull/_internal/config_reader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def _from_file(cls, path: pathlib.Path) -> dict[str, t.Any]:
104104
{'session_name': 'my session'}
105105
"""
106106
assert isinstance(path, pathlib.Path)
107-
content = path.open().read()
107+
content = path.open(encoding="utf-8").read()
108108

109109
if path.suffix in {".yaml", ".yml"}:
110110
fmt: FormatLiteral = "yaml"

src/vcspull/cli/__init__.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import argparse
66
import logging
7+
import pathlib
78
import textwrap
89
import typing as t
910
from typing import overload
@@ -13,6 +14,12 @@
1314
from vcspull.__about__ import __version__
1415
from vcspull.log import setup_logger
1516

17+
from ._import import (
18+
create_import_subparser,
19+
import_from_filesystem,
20+
import_repo,
21+
)
22+
from .fmt import create_fmt_subparser, format_config_file
1623
from .sync import create_sync_subparser, sync
1724

1825
log = logging.getLogger(__name__)
@@ -73,14 +80,36 @@ def create_parser(
7380
)
7481
create_sync_subparser(sync_parser)
7582

83+
import_parser = subparsers.add_parser(
84+
"import",
85+
help="import repository or scan filesystem for repositories",
86+
formatter_class=argparse.RawDescriptionHelpFormatter,
87+
description="Import a repository to the vcspull configuration file. "
88+
"Can import a single repository by name and URL, or scan a directory "
89+
"to discover and import multiple repositories.",
90+
)
91+
create_import_subparser(import_parser)
92+
93+
fmt_parser = subparsers.add_parser(
94+
"fmt",
95+
help="format vcspull configuration files",
96+
formatter_class=argparse.RawDescriptionHelpFormatter,
97+
description="Format vcspull configuration files for consistency. "
98+
"Normalizes compact format to verbose format, standardizes on 'repo' key, "
99+
"and sorts directories and repositories alphabetically.",
100+
)
101+
create_fmt_subparser(fmt_parser)
102+
76103
if return_subparsers:
77-
return parser, sync_parser
104+
# Return all parsers needed by cli() function
105+
return parser, (sync_parser, import_parser, fmt_parser)
78106
return parser
79107

80108

81109
def cli(_args: list[str] | None = None) -> None:
82110
"""CLI entry point for vcspull."""
83-
parser, sync_parser = create_parser(return_subparsers=True)
111+
parser, subparsers = create_parser(return_subparsers=True)
112+
sync_parser, _import_parser, _fmt_parser = subparsers
84113
args = parser.parse_args(_args)
85114

86115
setup_logger(log=log, level=args.log_level.upper())
@@ -91,7 +120,33 @@ def cli(_args: list[str] | None = None) -> None:
91120
if args.subparser_name == "sync":
92121
sync(
93122
repo_patterns=args.repo_patterns,
94-
config=args.config,
123+
config=pathlib.Path(args.config) if args.config else None,
95124
exit_on_error=args.exit_on_error,
96125
parser=sync_parser,
97126
)
127+
elif args.subparser_name == "import":
128+
# Unified import command
129+
if args.scan_dir:
130+
# Filesystem scan mode
131+
import_from_filesystem(
132+
scan_dir_str=args.scan_dir,
133+
config_file_path_str=args.config,
134+
recursive=args.recursive,
135+
base_dir_key_arg=args.base_dir_key,
136+
yes=args.yes,
137+
)
138+
elif args.name and args.url:
139+
# Manual import mode
140+
import_repo(
141+
name=args.name,
142+
url=args.url,
143+
config_file_path_str=args.config,
144+
path=args.path,
145+
base_dir=args.base_dir,
146+
)
147+
else:
148+
# Error: need either name+url or --scan
149+
log.error("Either provide NAME and URL, or use --scan DIR")
150+
parser.exit(status=2)
151+
elif args.subparser_name == "fmt":
152+
format_config_file(args.config, args.write, args.all)

0 commit comments

Comments
 (0)