Skip to content
Open
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
5 changes: 4 additions & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- master
- screen-recording-2026-07-12 # [DO NOT MERGE] temporary docs preview
workflow_dispatch:

permissions:
Expand All @@ -30,14 +31,16 @@ jobs:
- README.*
docs:
- 'docs/**'
- 'docs/_static/demos/asciinema/*.gif'
- 'examples/**'
python_files:
- 'src/vcspull/**'
- pyproject.toml
- uv.lock

- name: Should publish
if: github.event_name == 'workflow_dispatch' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.root_docs == 'true' || steps.changes.outputs.python_files == 'true'
# [DO NOT MERGE] force publish on the preview branch regardless of changed paths
if: github.ref == 'refs/heads/screen-recording-2026-07-12' || github.event_name == 'workflow_dispatch' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.root_docs == 'true' || steps.changes.outputs.python_files == 'true'
run: echo "PUBLISH=$(echo true)" >> $GITHUB_ENV

- name: Install uv
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ See the [documentation](https://vcspull.git-pull.com/), [configuration](https://
[myrepos]: http://myrepos.branchable.com/
[mu-repo]: http://fabioz.github.io/mu-repo/

<img src="https://raw.githubusercontent.com/vcs-python/vcspull/master/docs/_static/demos/asciinema/vcspull-sync.gif" alt="vcspull sync cloning a workspace of repositories" width="100%" />

# How to

## Install
Expand Down Expand Up @@ -109,6 +111,8 @@ $ vcspull add ~/projects/libs/my-lib
yourself instead of having vcspull merge them automatically.
- Follow with `vcspull sync my-lib` to clone or update the working tree after registration.

<img src="https://raw.githubusercontent.com/vcs-python/vcspull/master/docs/_static/demos/asciinema/vcspull-add.gif" alt="vcspull add registering a repository from its checkout" width="100%" />

### Discover local checkouts and add en masse

Have a directory tree full of cloned Git repositories? Scan and append them to
Expand All @@ -123,6 +127,8 @@ The scan shows each repository before import unless you opt into `--yes`. Add
than the default `~/.vcspull.yaml`. Duplicate workspace roots are merged by
default; include `--no-merge` to keep them separate while you review the log.

<img src="https://raw.githubusercontent.com/vcs-python/vcspull/master/docs/_static/demos/asciinema/vcspull-discover.gif" alt="vcspull discover scanning a directory tree for repositories" width="100%" />

### Import from remote services

Pull repository lists from GitHub, GitLab, Codeberg, Gitea, Forgejo, or AWS
Expand Down Expand Up @@ -158,6 +164,8 @@ $ 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.

<img src="https://raw.githubusercontent.com/vcs-python/vcspull/master/docs/_static/demos/asciinema/vcspull-list.gif" alt="vcspull list showing configured repositories" width="100%" />

Search across repositories with an rg-like query syntax:

```console
Expand All @@ -166,6 +174,8 @@ $ vcspull search name:django url:github
$ vcspull search --fixed-strings 'git+https://github.com/org/repo.git'
```

<img src="https://raw.githubusercontent.com/vcs-python/vcspull/master/docs/_static/demos/asciinema/vcspull-search.gif" alt="vcspull search narrowing a set of repositories to matches" width="100%" />

### Check repository status

Get a quick health check for all configured workspaces:
Expand All @@ -176,6 +186,8 @@ $ vcspull status --detailed
$ vcspull status --ndjson | jq --slurp 'map(select(.reason == "summary"))'
```

<img src="https://raw.githubusercontent.com/vcs-python/vcspull/master/docs/_static/demos/asciinema/vcspull-status.gif" alt="vcspull status reporting clean, dirty, and missing repositories" width="100%" />

The status command respects `--workspace/-w` filters and the global
`--color {auto,always,never}` flag. JSON and NDJSON output mirrors the list
command for automation workflows.
Expand Down Expand Up @@ -285,8 +297,6 @@ $ vcspull sync "$HOME/code/*"

[libvcs]: https://github.com/vcs-python/libvcs

<img src="https://raw.githubusercontent.com/vcs-python/vcspull/master/docs/_static/vcspull-demo.gif" class="align-center" style="width:45.0%" alt="image" />

# Donations

Your donations fund development of new features, testing and support.
Expand Down
65 changes: 65 additions & 0 deletions docs/_ext/aspect_ratio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Reserve demo image space via aspect-ratio so pages don't shift on load (CLS).

Sphinx's HTML writer renders image ``:width:`` as an inline pixel style, which
overrides the theme's ``height: auto`` and distorts responsive images. Instead
this stamps each demo ``<img>`` with ``width:<W>px; max-width:100%;
aspect-ratio:W/H`` (computed from the source GIF): the gif shows at its real
size, shrinks on a narrow screen, never scales up, and the browser reserves the
correct box before it loads (no layout shift).
"""

from __future__ import annotations

import re
import typing as t
from pathlib import Path

from sphinx.util.images import get_image_size

if t.TYPE_CHECKING:
from docutils import nodes
from sphinx.application import Sphinx

_IMG = re.compile(r'<img\b[^>]*?\bsrc="[^"]+?/([^"/]+?\.gif)"[^>]*?/?>')
_STYLE = re.compile(r'style="[^"]*"')
_sizes: dict[str, tuple[int, int]] = {}


def _index_sizes(app: Sphinx) -> None:
"""Map each source GIF's basename to its intrinsic ``(width, height)``."""
for path in Path(app.srcdir).rglob("*.gif"):
size = get_image_size(path)
if size is not None:
_sizes[path.name] = size


def _inject(
app: Sphinx,
pagename: str,
templatename: str,
context: dict[str, t.Any],
doctree: nodes.document | None,
) -> None:
"""Rewrite demo ``<img>`` tags to carry ``width:100%; aspect-ratio:W/H``."""
body = context.get("body")
if not body or "_images/" not in body:
return

def repl(match: re.Match[str]) -> str:
tag, name = match.group(0), match.group(1)
size = _sizes.get(name)
if size is None or "aspect-ratio" in tag:
return tag
style = f"width:{size[0]}px;max-width:100%;aspect-ratio:{size[0]}/{size[1]}"
if 'style="' in tag:
return _STYLE.sub(f'style="{style}"', tag)
return tag[:-1].rstrip("/") + f' style="{style}" />'

context["body"] = _IMG.sub(repl, body)


def setup(app: Sphinx) -> dict[str, t.Any]:
"""Register the aspect-ratio injector."""
app.connect("builder-inited", _index_sizes)
app.connect("html-page-context", _inject)
return {"parallel_read_safe": True, "parallel_write_safe": True}
43 changes: 43 additions & 0 deletions docs/_static/demos/asciinema/vcspull-add.cast
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{"version":3,"term":{"cols":120,"rows":9},"timestamp":1783863920,"idle_time_limit":1.0,"command":"vcspull add ~/code/rust/ripgrep -y; sleep 2","title":"vcspull add","env":{"SHELL":"/bin/zsh"}}
[0.15,"o","\u001b[34m$\u001b[0m "]
[0.05,"o","v"]
[0.05,"o","c"]
[0.05,"o","s"]
[0.05,"o","p"]
[0.05,"o","u"]
[0.05,"o","l"]
[0.05,"o","l"]
[0.05,"o"," "]
[0.05,"o","a"]
[0.05,"o","d"]
[0.05,"o","d"]
[0.05,"o"," "]
[0.05,"o","~"]
[0.05,"o","/"]
[0.05,"o","c"]
[0.05,"o","o"]
[0.05,"o","d"]
[0.05,"o","e"]
[0.05,"o","/"]
[0.05,"o","r"]
[0.05,"o","u"]
[0.05,"o","s"]
[0.05,"o","t"]
[0.05,"o","/"]
[0.05,"o","r"]
[0.05,"o","i"]
[0.05,"o","p"]
[0.05,"o","g"]
[0.05,"o","r"]
[0.05,"o","e"]
[0.05,"o","p"]
[0.05,"o"," "]
[0.05,"o","-"]
[0.05,"o","y"]
[0.20,"o","\r\n"]
[0.121,"o","\u001b[32mFound new repository to import:\u001b[0m\r\n \u001b[32m+\u001b[0m \u001b[36mripgrep\u001b[0m (\u001b[33mhttps://github.com/BurntSushi/ripgrep.git\u001b[0m)\r\n \u001b[34m•\u001b[0m workspace: \u001b[35m~/code/rust/\u001b[0m"]
[0.000,"o","\r\n"]
[0.000,"o"," \u001b[34m↳\u001b[0m path: \u001b[34m~/code/rust/ripgrep\u001b[0m\r\n"]
[0.001,"o","\u001b[36m?\u001b[0m Import this repository? [y/N]: \u001b[32my (auto-confirm)\u001b[0m\r\n"]
[0.003,"o","\u001b[32m✓\u001b[0m Successfully added \u001b[36m'ripgrep'\u001b[0m (\u001b[33mgit+https://github.com/BurntSushi/ripgrep.git\u001b[0m) to \u001b[34m~/.vcspull.yaml\u001b[0m under '\u001b[35m~/code/rust/\u001b[0m'.\r\n"]
[2.027,"x","0"]
Binary file added docs/_static/demos/asciinema/vcspull-add.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 54 additions & 0 deletions docs/_static/demos/asciinema/vcspull-discover.cast
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{"version":3,"term":{"cols":120,"rows":14},"timestamp":1783863911,"idle_time_limit":1.0,"command":"vcspull discover ~/code --recursive --dry-run; sleep 2","title":"vcspull discover","env":{"SHELL":"/bin/zsh"}}
[0.15,"o","\u001b[34m$\u001b[0m "]
[0.05,"o","v"]
[0.05,"o","c"]
[0.05,"o","s"]
[0.05,"o","p"]
[0.05,"o","u"]
[0.05,"o","l"]
[0.05,"o","l"]
[0.05,"o"," "]
[0.05,"o","d"]
[0.05,"o","i"]
[0.05,"o","s"]
[0.05,"o","c"]
[0.05,"o","o"]
[0.05,"o","v"]
[0.05,"o","e"]
[0.05,"o","r"]
[0.05,"o"," "]
[0.05,"o","~"]
[0.05,"o","/"]
[0.05,"o","c"]
[0.05,"o","o"]
[0.05,"o","d"]
[0.05,"o","e"]
[0.05,"o"," "]
[0.05,"o","-"]
[0.05,"o","-"]
[0.05,"o","r"]
[0.05,"o","e"]
[0.05,"o","c"]
[0.05,"o","u"]
[0.05,"o","r"]
[0.05,"o","s"]
[0.05,"o","i"]
[0.05,"o","v"]
[0.05,"o","e"]
[0.05,"o"," "]
[0.05,"o","-"]
[0.05,"o","-"]
[0.05,"o","d"]
[0.05,"o","r"]
[0.05,"o","y"]
[0.05,"o","-"]
[0.05,"o","r"]
[0.05,"o","u"]
[0.05,"o","n"]
[0.20,"o","\r\n"]
[0.137,"o","\r\n\u001b[32mFound 4 new repositories to preview:\u001b[0m\r\n \u001b[32m+\u001b[0m \u001b[36mripgrep\u001b[0m (\u001b[33mhttps://github.com/BurntSushi/ripgrep.git\u001b[0m)\r\n \u001b[32m+\u001b[0m \u001b[36mflask\u001b[0m (\u001b[33mhttps://github.com/pallets/flask.git\u001b[0m)\r\n \u001b[34m•\u001b[0m shallow: \u001b[33mtrue\u001b[0m\r\n"]
[0.000,"o"," \u001b[32m+\u001b[0m \u001b[36mclick\u001b[0m (\u001b[33mhttps://github.com/pallets/click.git\u001b[0m)\r\n"]
[0.000,"o"," \u001b[34m•\u001b[0m shallow: \u001b[33mtrue\u001b[0m\r\n \u001b[32m+\u001b[0m \u001b[36mhttpx\u001b[0m (\u001b[33mhttps://github.com/encode/httpx.git\u001b[0m)\r\n"]
[0.000,"o"," \u001b[34m•\u001b[0m shallow: \u001b[33mtrue\u001b[0m\r\n"]
[0.000,"o","\r\n\u001b[33m→\u001b[0m Dry run complete. No changes made to \u001b[34m~/.vcspull.yaml\u001b[0m.\r\n"]
[2.026,"x","0"]
Binary file added docs/_static/demos/asciinema/vcspull-discover.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions docs/_static/demos/asciinema/vcspull-fmt.cast
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{"version":3,"term":{"cols":120,"rows":9},"timestamp":1783863913,"idle_time_limit":1.0,"command":"vcspull fmt; sleep 2","title":"vcspull fmt","env":{"SHELL":"/bin/zsh"}}
[0.15,"o","\u001b[34m$\u001b[0m "]
[0.05,"o","v"]
[0.05,"o","c"]
[0.05,"o","s"]
[0.05,"o","p"]
[0.05,"o","u"]
[0.05,"o","l"]
[0.05,"o","l"]
[0.05,"o"," "]
[0.05,"o","f"]
[0.05,"o","m"]
[0.05,"o","t"]
[0.20,"o","\r\n"]
[0.116,"o","\u001b[36mi\u001b[0m Found \u001b[33m9\u001b[0m formatting issues in \u001b[34m~/.vcspull.yaml\u001b[0m\r\n \u001b[34m•\u001b[0m 5 repositories from compact to verbose format\r\n \u001b[34m•\u001b[0m 3 repositories from 'url' to 'repo' key\r\n \u001b[34m•\u001b[0m Directories will be sorted alphabetically\r\n\r\n\u001b[33m→\u001b[0m Run with \u001b[36m--write\u001b[0m to apply these formatting changes.\r\n"]
[2.027,"x","0"]
Binary file added docs/_static/demos/asciinema/vcspull-fmt.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
95 changes: 95 additions & 0 deletions docs/_static/demos/asciinema/vcspull-import.cast
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{"version":3,"term":{"cols":120,"rows":20},"timestamp":1783863922,"idle_time_limit":1.0,"command":"vcspull import github pallets --mode org -w ~/code/python --limit 5 --https --dry-run; sleep 2","title":"vcspull import","env":{"SHELL":"/bin/zsh"}}
[0.15,"o","\u001b[34m$\u001b[0m "]
[0.05,"o","v"]
[0.05,"o","c"]
[0.05,"o","s"]
[0.05,"o","p"]
[0.05,"o","u"]
[0.05,"o","l"]
[0.05,"o","l"]
[0.05,"o"," "]
[0.05,"o","i"]
[0.05,"o","m"]
[0.05,"o","p"]
[0.05,"o","o"]
[0.05,"o","r"]
[0.05,"o","t"]
[0.05,"o"," "]
[0.05,"o","g"]
[0.05,"o","i"]
[0.05,"o","t"]
[0.05,"o","h"]
[0.05,"o","u"]
[0.05,"o","b"]
[0.05,"o"," "]
[0.05,"o","p"]
[0.05,"o","a"]
[0.05,"o","l"]
[0.05,"o","l"]
[0.05,"o","e"]
[0.05,"o","t"]
[0.05,"o","s"]
[0.05,"o"," "]
[0.05,"o","-"]
[0.05,"o","-"]
[0.05,"o","m"]
[0.05,"o","o"]
[0.05,"o","d"]
[0.05,"o","e"]
[0.05,"o"," "]
[0.05,"o","o"]
[0.05,"o","r"]
[0.05,"o","g"]
[0.05,"o"," "]
[0.05,"o","-"]
[0.05,"o","w"]
[0.05,"o"," "]
[0.05,"o","~"]
[0.05,"o","/"]
[0.05,"o","c"]
[0.05,"o","o"]
[0.05,"o","d"]
[0.05,"o","e"]
[0.05,"o","/"]
[0.05,"o","p"]
[0.05,"o","y"]
[0.05,"o","t"]
[0.05,"o","h"]
[0.05,"o","o"]
[0.05,"o","n"]
[0.05,"o"," "]
[0.05,"o","-"]
[0.05,"o","-"]
[0.05,"o","l"]
[0.05,"o","i"]
[0.05,"o","m"]
[0.05,"o","i"]
[0.05,"o","t"]
[0.05,"o"," "]
[0.05,"o","5"]
[0.05,"o"," "]
[0.05,"o","-"]
[0.05,"o","-"]
[0.05,"o","h"]
[0.05,"o","t"]
[0.05,"o","t"]
[0.05,"o","p"]
[0.05,"o","s"]
[0.05,"o"," "]
[0.05,"o","-"]
[0.05,"o","-"]
[0.05,"o","d"]
[0.05,"o","r"]
[0.05,"o","y"]
[0.05,"o","-"]
[0.05,"o","r"]
[0.05,"o","u"]
[0.05,"o","n"]
[0.20,"o","\r\n"]
[0.119,"o","\u001b[36m→\u001b[0m Fetching repositories from \u001b[35mGitHub\u001b[0m...\r\n"]
[0.626,"o","Showing 5 repositories; more may be available (use --limit 0 to fetch all)\r\n\r\n\u001b[32m✓\u001b[0m Found \u001b[36m5\u001b[0m repositories\r\n \u001b[32m+\u001b[0m \u001b[36mjinja\u001b[0m\u001b[34m [Python]\u001b[0m\u001b[34m ★11684\u001b[0m\r\n \u001b[32m+\u001b[0m \u001b[36mflask\u001b[0m\u001b[34m [Python]\u001b[0m\u001b[34m ★71941\u001b[0m\r\n \u001b[32m+\u001b[0m \u001b[36mclick\u001b[0m\u001b[34m [Python]\u001b[0m\u001b[34m ★17578\u001b[0m\r\n \u001b[32m+\u001b[0m \u001b[36mitsdangerous\u001b[0m\u001b[34m [Python]\u001b[0m\u001b[34m ★3125\u001b[0m\r\n \u001b[32m+\u001b[0m \u001b[36mwerkzeug\u001b[0m\u001b[34m [Python]\u001b[0m\u001b[34m ★6876\u001b[0m\r\n"]
[0.002,"o","[DRY-RUN] Would add: jinja → git+https://github.com/pallets/jinja.git\r\n[DRY-RUN] Would add: itsdangerous → git+https://github.com/pallets/itsdangerous.git\r\n[DRY-RUN] Would add: werkzeug → git+https://github.com/pallets/werkzeug.git\r\n"]
[0.000,"o","[DRY-RUN] Would add \u001b[36m3\u001b[0m repositories\r\n[DRY-RUN] Would tag \u001b[36m2\u001b[0m repositories with import provenance\r\n[DRY-RUN] \u001b[36m2\u001b[0m repositories unchanged\r\n"]
[0.000,"o","\r\n"]
[0.000,"o","\u001b[33m→\u001b[0m Dry run complete. Would write to \u001b[34m~/.vcspull.yaml\u001b[0m\r\n"]
[2.027,"x","0"]
Binary file added docs/_static/demos/asciinema/vcspull-import.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions docs/_static/demos/asciinema/vcspull-list-tree.cast
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{"version":3,"term":{"cols":120,"rows":17},"timestamp":1783863904,"idle_time_limit":1.0,"command":"vcspull list --tree; sleep 2","title":"vcspull list-tree","env":{"SHELL":"/bin/zsh"}}
[0.15,"o","\u001b[34m$\u001b[0m "]
[0.05,"o","v"]
[0.05,"o","c"]
[0.05,"o","s"]
[0.05,"o","p"]
[0.05,"o","u"]
[0.05,"o","l"]
[0.05,"o","l"]
[0.05,"o"," "]
[0.05,"o","l"]
[0.05,"o","i"]
[0.05,"o","s"]
[0.05,"o","t"]
[0.05,"o"," "]
[0.05,"o","-"]
[0.05,"o","-"]
[0.05,"o","t"]
[0.05,"o","r"]
[0.05,"o","e"]
[0.05,"o","e"]
[0.20,"o","\r\n"]
[0.125,"o","\r\n\u001b[35m~/code/c/\u001b[0m\r\n \u001b[34m•\u001b[0m \u001b[36mjq\u001b[0m \u001b[34m→\u001b[0m ~/code/c/jq\r\n \u001b[34m•\u001b[0m \u001b[36mtmux\u001b[0m \u001b[34m→\u001b[0m ~/code/c/tmux\r\n\r\n\u001b[35m~/code/go/\u001b[0m\r\n \u001b[34m•\u001b[0m \u001b[36mbubbletea\u001b[0m \u001b[34m→\u001b[0m ~/code/go/bubbletea\r\n \u001b[34m•\u001b[0m \u001b[36mfzf\u001b[0m \u001b[34m→\u001b[0m ~/code/go/fzf\r\n \u001b[34m•\u001b[0m \u001b[36mgin\u001b[0m \u001b[34m→\u001b[0m ~/code/go/gin\r\n\r\n\u001b[35m~/code/python/\u001b[0m\r\n \u001b[34m•\u001b[0m \u001b[36mclick\u001b[0m \u001b[34m→\u001b[0m ~/code/python/click\r\n \u001b[34m•\u001b[0m \u001b[36mflask\u001b[0m \u001b[34m→\u001b[0m ~/code/python/flask\r\n"]
[0.000,"o"," \u001b[34m•\u001b[0m \u001b[36mhttpx\u001b[0m \u001b[34m→\u001b[0m ~/code/python/httpx\r\n"]
[2.022,"x","0"]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading