Skip to content

Commit bf0a2c5

Browse files
authored
New commands: import, import --scan, fmt (#465)
# Changes ## New: Added `vcspull import` - Unified command for importing repositories into vcspull configuration, following the infrastructure-as-code pattern similar to Terraform's `import` command. - **Manual import**: Register a single repository with `vcspull import <name> <url>` - Optional `--dir`/`--path` helpers for base directory detection - Example: `uv run vcspull import myrepo [email protected]:user/myrepo.git --dir ~/code` - **Filesystem scan**: Discover and import existing repositories with `vcspull import --scan <dir>` - Recursively scan with `--recursive`/`-r` - Interactive confirmation prompt or `--yes` for unattended runs - Custom base directory with `--base-dir-key` - Example: `uv run vcspull import --scan ~/study/typescript --recursive --yes` ## New: Added `vcspull fmt` - Introduces a `fmt` subcommand to normalize configuration files (sort directories/repos, convert compact entries to verbose `{repo: ...}` form, and standardize keys). - Example: `uv run vcspull fmt --config ~/.vcspull.yaml --write` ## Logging Improvements - Enhanced logging system with better CLI module propagation and StreamHandler configuration. - Improved output visibility in tests and CLI usage with proper handler management. - Ensure handlers survive pytest stream rotation via safe flush/reset logic. - Added regression test `test_import_scan_stream_output` to guarantee user-facing output remains visible. ## Config & CLI Enhancements - `ConfigReader` now reads files using UTF-8 explicitly. - Shared helper `save_config_yaml` centralizes YAML writes with UTF-8 encoding. - CLI parser now wires in the new subcommands and routes to dedicated modules. - Module naming: `_import.py` (leading underscore) avoids Python's `import` keyword conflict. Fixes: #25, #333 # Testing - `uv run ruff check . --fix --show-fixes` - `uv run mypy` - `uv run pytest` (132 tests passing) # Additional Notes - Extensive new CLI tests cover import/fmt behaviors, including duplicate detection, confirmation prompts, and formatting summaries. - Logging test suite expanded to validate handler configuration, propagation, and formatter integration. - Command naming aligns with IaC terminology: `import` clearly communicates bringing repositories under vcspull management, whether manually specified or discovered from the filesystem.
2 parents e6d2c45 + e4eca51 commit bf0a2c5

File tree

19 files changed

+3192
-34
lines changed

19 files changed

+3192
-34
lines changed

CHANGES

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,29 @@ $ pipx install --suffix=@next 'vcspull' --pip-args '\--pre' --force
1717

1818
## vcspull v1.36.x (unreleased)
1919

20-
- _Notes on upcoming releases will be added here_
21-
2220
<!-- Maintainers, insert changes / features for the next release here -->
2321

22+
_Notes on upcoming releases will be added here_
23+
24+
### New features
25+
26+
#### New command: `vcspull import` (#465)
27+
28+
- **Manual import**: Register a single repository with `vcspull import <name> <url>`
29+
- Optional `--dir`/`--path` helpers for base-directory detection
30+
- **Filesystem scan**: Discover and import existing repositories with `vcspull import --scan <dir>`
31+
- Recursively scan with `--recursive`/`-r`
32+
- Interactive confirmation prompt or `--yes` for unattended runs
33+
- Custom base directory with `--base-dir-key`
34+
35+
#### New command: `vcspull fmt` (#465)
36+
37+
- Normalize configuration files by expanding compact entries to `{repo: ...}`, sorting directories/repos, and standardizing keys; pair with `--write` to persist the formatted output.
38+
39+
### Improvements
40+
41+
- Enhanced logging system with better CLI module propagation and StreamHandler configuration for improved output visibility in tests and CLI usage (#465).
42+
2443
### Development
2544

2645
- Add Python 3.14 to test matrix, trove classifiers (#469)

docs/api/cli/fmt.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# vcspull fmt - `vcspull.cli.fmt`
2+
3+
```{eval-rst}
4+
.. automodule:: vcspull.cli.fmt
5+
:members:
6+
:show-inheritance:
7+
:undoc-members:
8+
```

docs/api/cli/import.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# vcspull import - `vcspull.cli._import`
2+
3+
```{eval-rst}
4+
.. automodule:: vcspull.cli._import
5+
:members:
6+
:show-inheritance:
7+
:undoc-members:
8+
```

docs/api/cli/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
:maxdepth: 1
1010
1111
sync
12+
import
13+
fmt
1214
```
1315

1416
## vcspull CLI - `vcspull.cli`

docs/cli/fmt.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
(cli-fmt)=
2+
3+
# vcspull fmt
4+
5+
`vcspull fmt` normalizes configuration files so directory keys and repository
6+
entries stay consistent. By default the formatter prints the proposed changes to
7+
stdout. Apply the updates in place with `--write`.
8+
9+
## Command
10+
11+
```{eval-rst}
12+
.. argparse::
13+
:module: vcspull.cli
14+
:func: create_parser
15+
:prog: vcspull
16+
:path: fmt
17+
:nodescription:
18+
```
19+
20+
## What gets formatted
21+
22+
The formatter performs three main tasks:
23+
24+
- Expands string-only entries into verbose dictionaries using the `repo` key.
25+
- Converts legacy `url` keys to `repo` for consistency with the rest of the
26+
tooling.
27+
- Sorts directory keys and repository names alphabetically to minimize diffs.
28+
29+
For example:
30+
31+
```yaml
32+
~/code/:
33+
libvcs: git+https://github.com/vcspull/libvcs.git
34+
vcspull:
35+
url: git+https://github.com/vcspull/vcspull.git
36+
```
37+
38+
becomes:
39+
40+
```yaml
41+
~/code/:
42+
libvcs:
43+
repo: git+https://github.com/vcspull/libvcs.git
44+
vcspull:
45+
repo: git+https://github.com/vcspull/vcspull.git
46+
```
47+
48+
## Writing changes
49+
50+
Run the formatter in dry-run mode first to preview the adjustments, then add
51+
`--write` (or `-w`) to persist them back to disk:
52+
53+
```console
54+
$ vcspull fmt --config ~/.vcspull.yaml
55+
$ vcspull fmt --config ~/.vcspull.yaml --write
56+
```
57+
58+
Use `--all` to iterate over the default search locations: the current working
59+
directory, `~/.vcspull.*`, and the XDG configuration directory. Each formatted
60+
file is reported individually.
61+
62+
```console
63+
$ vcspull fmt --all --write
64+
```
65+
66+
Pair the formatter with [`vcspull import`](cli-import) after scanning the file
67+
system to keep newly added repositories ordered and normalized.

docs/cli/import.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
(cli-import)=
2+
3+
# vcspull import
4+
5+
The `vcspull import` command registers existing repositories with your vcspull
6+
configuration. You can either provide a single repository name and URL or scan
7+
directories for Git repositories that already live on disk.
8+
9+
## Command
10+
11+
```{eval-rst}
12+
.. argparse::
13+
:module: vcspull.cli
14+
:func: create_parser
15+
:prog: vcspull
16+
:path: import
17+
:nodescription:
18+
```
19+
20+
## Manual import
21+
22+
Provide a repository name and remote URL to append an entry to your
23+
configuration. Use `--path` when you already have a working tree on disk so the
24+
configured base directory matches its location. Override the inferred base
25+
directory with `--dir` when you need a specific configuration key.
26+
27+
```console
28+
$ vcspull import my-lib https://github.com/example/my-lib.git --path ~/code/my-lib
29+
```
30+
31+
With no `-c/--config` flag vcspull looks for the first YAML configuration file
32+
under `~/.config/vcspull/` or the current working directory. When none exist a
33+
new `.vcspull.yaml` is created next to where you run the command.
34+
35+
## Filesystem scanning
36+
37+
`vcspull import --scan` discovers Git repositories that already exist on disk
38+
and writes them to your configuration. The command prompts before adding each
39+
repository, showing the inferred name, directory key, and origin URL (when
40+
available).
41+
42+
```console
43+
$ vcspull import --scan ~/code --recursive
44+
? Add ~/code/vcspull (dir: ~/code/)? [y/N]: y
45+
? Add ~/code/libvcs (dir: ~/code/)? [y/N]: y
46+
```
47+
48+
- `--recursive`/`-r` searches nested directories.
49+
- `--base-dir-key` forces all discovered repositories to use the same base
50+
directory key, overriding the automatically expanded directory.
51+
- `--yes`/`-y` accepts every suggestion, which is useful for unattended
52+
migrations.
53+
54+
When vcspull detects a Git remote named `origin` it records the remote URL in
55+
the configuration. Repositories without a remote are still added, allowing you
56+
to fill the `repo` key later.
57+
58+
## Choosing configuration files
59+
60+
Pass `-c/--config` to import into a specific YAML file:
61+
62+
```console
63+
$ vcspull import --scan ~/company --recursive --config ~/company/.vcspull.yaml
64+
```
65+
66+
Use `--all` with the [`vcspull fmt`](cli-fmt) command after a large scan to keep
67+
configuration entries sorted and normalized.

docs/cli/index.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
:maxdepth: 1
88
99
sync
10+
import
11+
fmt
1012
```
1113

1214
```{toctree}
@@ -31,5 +33,5 @@ completion
3133
:nodescription:
3234
3335
subparser_name : @replace
34-
See :ref:`cli-sync`
36+
See :ref:`cli-sync`, :ref:`cli-import`, :ref:`cli-fmt`
3537
```

docs/quickstart.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@ YAML? Create a `~/.vcspull.yaml` file:
111111
"flask": "git+https://github.com/mitsuhiko/flask.git"
112112
```
113113
114+
Already have repositories cloned locally? Use
115+
`vcspull import --scan ~/code --recursive` to detect existing Git checkouts and
116+
append them to your configuration. See {ref}`cli-import` for more details and
117+
options such as `--base-dir-key` and `--yes` for unattended runs. After editing
118+
or importing, run `vcspull fmt --write` (documented in {ref}`cli-fmt`) to
119+
normalize keys and keep your configuration tidy.
120+
114121
The `git+` in front of the repository URL. Mercurial repositories use
115122
`hg+` and Subversion will use `svn+`. Repo type and address is
116123
specified in [pip vcs url][pip vcs url] format.

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)