Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,73 @@ $ uvx --from 'vcspull' --prerelease allow vcspull

<!-- Maintainers, insert changes / features for the next release here -->

### New features

#### New command: `vcspull search` (#494)

Search across all configured repositories using ripgrep-inspired syntax with field-scoped queries, regex patterns, and flexible output formats.

**Basic usage:**

Search for a term across all fields:

```console
$ vcspull search django
```

Search by repository name:

```console
$ vcspull search "name:flask"
```

Search by URL:

```console
$ vcspull search "url:github.com"
```

**Key features:**

- **Field-scoped queries**: Target specific fields with `name:`, `url:`, `path:`, or `vcs:` prefixes
- **Regex patterns**: Full regex support with `-i/--ignore-case`, `-S/--smart-case`, `-F/--fixed-strings`, and `--word-regexp`
- **Boolean logic**: AND (default), OR (`--any`), and inverted matching (`-v/--invert-match`)
- **Context display**: Show matching fields with `--field` filtering
- **Output formats**: Human-readable (default), `--json`, or `--ndjson` for automation
- **Color control**: `--color {auto,always,never}` with `NO_COLOR` support

**Advanced examples:**

Case-insensitive search across all fields:

```console
$ vcspull search -i DJANGO
```

Find repos by VCS type:

```console
$ vcspull search "vcs:git"
```

Match any term (OR logic):

```console
$ vcspull search --any flask django requests
```

Invert match (exclude repos):

```console
$ vcspull search -v "url:gitlab"
```

JSON output for scripting:

```console
$ vcspull search --json "name:lib"
```

### Development

#### Makefile -> Justfile (#493)
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ $ vcspull list --json | jq '.[].name'
`--json` emits a single JSON array, while `--ndjson` streams newline-delimited
objects that are easy to consume from shell pipelines.

Search across repositories with an rg-like query syntax:

```console
$ vcspull search django
$ vcspull search name:django url:github
$ vcspull search --fixed-strings 'git+https://github.com/org/repo.git'
```

### Check repository status

Get a quick health check for all configured workspaces:
Expand Down
1 change: 1 addition & 0 deletions docs/api/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ sync
add
discover
list
search
status
fmt
```
Expand Down
3 changes: 3 additions & 0 deletions docs/api/cli/search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# vcspull search - `vcspull.cli.search`

.. automodule:: vcspull.cli.search
3 changes: 2 additions & 1 deletion docs/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ sync
add
discover
list
search
status
fmt
```
Expand All @@ -36,5 +37,5 @@ completion
:nodescription:

subparser_name : @replace
See :ref:`cli-sync`, :ref:`cli-add`, :ref:`cli-discover`, :ref:`cli-list`, :ref:`cli-status`, :ref:`cli-fmt`
See :ref:`cli-sync`, :ref:`cli-add`, :ref:`cli-discover`, :ref:`cli-list`, :ref:`cli-search`, :ref:`cli-status`, :ref:`cli-fmt`
```
102 changes: 102 additions & 0 deletions docs/cli/search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
(cli-search)=

# vcspull search

The `vcspull search` command looks up repositories across your vcspull
configuration with an rg-like query syntax. Queries are regex by default, can
scope to specific fields, and can emit structured JSON for automation.

## Command

```{eval-rst}
.. argparse::
:module: vcspull.cli
:func: create_parser
:prog: vcspull
:path: search
:nodescription:
```

## Basic usage

Search all fields (name, path, url, workspace) with regex:

```console
$ vcspull search django
• django → ~/code/django
```

## Field-scoped queries

Target specific fields with prefixes:

```console
$ vcspull search name:django url:github
• django → ~/code/django
url: git+https://github.com/django/django.git
```

Available field prefixes:
- `name:`
- `path:`
- `url:`
- `workspace:` (alias: `root:` or `ws:`)

## Literal matches

Use `-F/--fixed-strings` to match literal text instead of regex:

```console
$ vcspull search --fixed-strings 'git+https://github.com/org/repo.git'
```

## Case handling

`-i/--ignore-case` forces case-insensitive matching. `-S/--smart-case` matches
case-insensitively unless your query includes uppercase characters.

```console
$ vcspull search -S Django
```

## Boolean matching

By default all terms must match. Use `--any` to match if *any* term matches:

```console
$ vcspull search --any django flask
```

Invert matches with `-v/--invert-match`:

```console
$ vcspull search -v --fixed-strings github
```

## JSON output

Emit matches as JSON for automation:

```console
$ vcspull search --json django
```

Output format:

```json
[
{
"name": "django",
"url": "git+https://github.com/django/django.git",
"path": "~/code/django",
"workspace_root": "~/code/",
"matched_fields": ["name", "url"]
}
]
```

Use NDJSON for streaming:

```console
$ vcspull search --ndjson django
```
57 changes: 57 additions & 0 deletions src/vcspull/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .discover import create_discover_subparser, discover_repos
from .fmt import create_fmt_subparser, format_config_file
from .list import create_list_subparser, list_repos
from .search import create_search_subparser, search_repos
from .status import create_status_subparser, status_repos
from .sync import create_sync_subparser, sync

Expand Down Expand Up @@ -70,6 +71,15 @@ def build_description(
"vcspull list --json",
],
),
(
"search",
[
"vcspull search django",
"vcspull search name:django url:github",
"vcspull search --fixed-strings 'git+https://github.com/org/repo.git'",
"vcspull search --ignore-case --any django flask",
],
),
(
"add",
[
Expand Down Expand Up @@ -133,6 +143,26 @@ def build_description(
),
)

SEARCH_DESCRIPTION = build_description(
"""
Search configured repositories.

Query terms use regex by default, with optional field prefixes like
name:, path:, url:, or workspace:.
""",
(
(
None,
[
"vcspull search django",
"vcspull search name:django url:github",
"vcspull search --fixed-strings 'git+https://github.com/org/repo.git'",
"vcspull search --ignore-case --any django flask",
],
),
),
)

STATUS_DESCRIPTION = build_description(
"""
Check status of repositories.
Expand Down Expand Up @@ -267,6 +297,15 @@ def create_parser(
)
create_status_subparser(status_parser)

# Search command
search_parser = subparsers.add_parser(
"search",
help="search configured repositories",
formatter_class=VcspullHelpFormatter,
description=SEARCH_DESCRIPTION,
)
create_search_subparser(search_parser)

# Add command
add_parser = subparsers.add_parser(
"add",
Expand Down Expand Up @@ -300,6 +339,7 @@ def create_parser(
sync_parser,
list_parser,
status_parser,
search_parser,
add_parser,
discover_parser,
fmt_parser,
Expand All @@ -314,6 +354,7 @@ def cli(_args: list[str] | None = None) -> None:
sync_parser,
_list_parser,
_status_parser,
_search_parser,
_add_parser,
_discover_parser,
_fmt_parser,
Expand Down Expand Up @@ -367,6 +408,22 @@ def cli(_args: list[str] | None = None) -> None:
concurrent=not getattr(args, "no_concurrent", False),
max_concurrent=getattr(args, "max_concurrent", None),
)
elif args.subparser_name == "search":
search_repos(
query_terms=args.query_terms,
config_path=pathlib.Path(args.config) if args.config else None,
workspace_root=getattr(args, "workspace_root", None),
output_json=args.output_json,
output_ndjson=args.output_ndjson,
color=args.color,
fields=getattr(args, "fields", None),
ignore_case=getattr(args, "ignore_case", False),
smart_case=getattr(args, "smart_case", False),
fixed_strings=getattr(args, "fixed_strings", False),
word_regexp=getattr(args, "word_regexp", False),
invert_match=getattr(args, "invert_match", False),
match_any=getattr(args, "match_any", False),
)
elif args.subparser_name == "add":
handle_add_command(args)
elif args.subparser_name == "discover":
Expand Down
11 changes: 11 additions & 0 deletions src/vcspull/cli/_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"--log-level",
"--path",
"--color",
"--field",
}

OPTIONS_FLAG_ONLY = {
Expand All @@ -32,6 +33,16 @@
"--ndjson",
"--tree",
"--detailed",
"-i",
"--ignore-case",
"-S",
"--smart-case",
"-F",
"--fixed-strings",
"--word-regexp",
"-v",
"--invert-match",
"--any",
}


Expand Down
Loading