From f5a95be425c27f8eec295290785c6e878e854e69 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 09:40:03 -0500 Subject: [PATCH] Docs(refactor[agents]): Route instead of restate why: AGENTS.md is read on every task, so a policy that matters to one class of change costs context on all the others. Splitting how we work from how we write lets each be loaded when it applies, and keeps both discoverable to humans rather than only to agents. what: - Replace the AGENTS.md body with a router: the project map, universal change discipline, then one pointer per class of change - Add .github/WRITING.md for prose policy, specialized with vcspull's CLI exit-status table, stdout/stderr split, destructive-operation invariants, and the two-tier doctest/structural-check mechanism for docstrings vs docs/*.md vs README.md - Add .github/CONTRIBUTING.md for workflow policy, with the real gate commands, libvcs test fixtures, logging conventions, and the actual tag-triggered PyPI release flow - Delete docs/AGENTS.md, docs/CLAUDE.md, and .github/contributing.md now that their content lives in the two files above - Point docs/project/contributing.md and docs/project/code-style.md at the canonical files instead of duplicating their content - Fix a README.md typo, an unclosed glob quote, and a stale bare `vcspull` example that no longer syncs anything --- .github/CONTRIBUTING.md | 288 +++++++++++++ .github/WRITING.md | 796 +++++++++++++++++++++++++++++++++++ .github/contributing.md | 27 -- AGENTS.md | 780 +++------------------------------- README.md | 37 +- docs/AGENTS.md | 141 ------- docs/CLAUDE.md | 1 - docs/project/code-style.md | 33 +- docs/project/contributing.md | 362 +--------------- 9 files changed, 1169 insertions(+), 1296 deletions(-) create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/WRITING.md delete mode 100644 .github/contributing.md delete mode 100644 docs/AGENTS.md delete mode 120000 docs/CLAUDE.md diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 000000000..d717a851a --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,288 @@ +# Contributing + +Thanks for looking. A bug report with a reproduction, or a pull request that +fixes a verified problem, is the most useful contribution right now. Discuss +a substantial change via an issue before making it. + +How this project writes prose — README, `CHANGES`, release notes, commit +messages, docstrings, and source comments — is set out separately in +[WRITING.md](WRITING.md). Read that before changing any of it. The constraints +every change is held to, and the map of what is where, are in +[AGENTS.md](../AGENTS.md). + +## Getting set up + +Install [git](https://git-scm.com/) and +[uv](https://docs.astral.sh/uv/getting-started/installation/), clone the +repository, then install the dependency groups: + +```console +$ uv sync --all-extras --dev +``` + +## The gates + +CI is the order of record; every gate it runs has to pass before a change is +done (see `.github/workflows/tests.yml`). + +Format: + +```console +$ uv run ruff format . +``` + +Lint: + +```console +$ uv run ruff check . --fix --show-fixes +``` + +Type-check: + +```console +$ uv run mypy . +``` + +`mypy` runs in strict mode (`[tool.mypy]` in `pyproject.toml`). + +Test: + +```console +$ uv run py.test +``` + +Documentation is a gate, not a courtesy. Doctests under `src/vcspull`, +`docs/_ext`, and `scripts` are executed by `pytest`; `vcspull …` commands +shown in `docs/*.md` and in the CLI's help text are checked against the real +argument parser by the same test run. `README.md` is verified by neither +mechanism and stays honest by hand. Which check applies to which file, and +the one edit that silently deletes a doctest, are in +[WRITING.md](WRITING.md#documented-examples-that-run). + +Before claiming a test or a gate works, show it failing. A gate that has +never been red is an assumption. + +### Imports and typing + +- `from __future__ import annotations` at the top of every file — `ruff`'s + isort configuration (`required-imports` in `[tool.ruff.lint.isort]`) + enforces this; a missing one is a lint failure, not a style note. +- Namespace imports for the standard library: `import pathlib`, not + `from pathlib import Path`. Third-party packages may use idiomatic + `from X import Y` imports. +- `import typing as t`, accessed via the namespace: `t.NamedTuple`, + `t.TYPE_CHECKING`, and so on. + +Docstring conventions (NumPy style, doctest requirements) are in +[WRITING.md](WRITING.md#docstrings) — they are prose policy, not a workflow +step. + +## Tests + +Tests are written as standalone functions (`test_*`), not grouped into +`class TestFoo:` blocks — use descriptive function names and file +organization instead. This applies to pytest tests, not doctests. + +**libvcs fixtures.** The suite leans on libvcs's pytest plugin: +`create_git_remote_repo`, `create_svn_remote_repo`, `create_hg_remote_repo` +(factory fixtures), `git_repo`, `svn_repo`, `hg_repo` (pre-made repository +instances), and `set_home`, `gitconfig`, `hgconfig`, `git_commit_envvars` +(environment fixtures). Reach for these before writing a new one. + +**Parametrized CLI tests** use `typing.NamedTuple` fixtures: + +```python +class CLIFixture(t.NamedTuple): + test_id: str + cli_args: list[str] + expected_exit_code: int + + +@pytest.mark.parametrize( + list(CLIFixture._fields), + CLI_FIXTURES, + ids=[test.test_id for test in CLI_FIXTURES], +) +def test_cli_subcommands(...): + ... +``` + +**Mocking.** `monkeypatch` for environment variables, globals, and +attributes; `mocker` (from `pytest-mock`) for application code. Document +every mock with a comment explaining what is mocked and why. + +**Configuration file tests** go through the project's own helpers — +`vcspull.tests.helpers.write_config` or `save_config_yaml` — rather than a +direct `yaml.dump` or `file.write_text`. + +**Logging assertions** read `caplog.records`, not `caplog.text`: scope +capture with `caplog.at_level(logging.DEBUG, logger="vcspull.cli")`, filter +records rather than index by position +(`[r for r in caplog.records if hasattr(r, "vcs_cmd")]`), and assert on the +structured fields (`record.vcs_exit_code == 0`) instead of string-matching +the rendered message. `caplog.record_tuples` cannot see `extra` fields — use +`caplog.records`. + +**Runtime dependency smoke test.** Verifies the published wheel runs without +the dev/test extras by importing every `vcspull` module and exercising each +CLI subcommand with `--help` in an isolated environment: + +```console +$ uvx \ + --isolated \ + --no-cache \ + --from . \ + python scripts/runtime_dep_smoketest.py +``` + +The same check has a pytest wrapper behind a dedicated marker, and both are +network-dependent because `uvx` builds the package in an isolated +environment: + +```console +$ uv run pytest \ + -m scripts__runtime_dep_smoketest \ + scripts/test_runtime_dep_smoketest.py +``` + +**Debugging a failing test.** Rerun on every file change with `just start` +(wraps [pytest-watcher](https://github.com/olzhasar/pytest-watcher)). Drop +into `pdb` on the first failure by setting `PYTEST_ADDOPTS`: + +```console +$ env PYTEST_ADDOPTS="-x -s --pdb" just start +``` + +With [ipython](https://ipython.org/) installed, use its debugger instead: + +```console +$ env PYTEST_ADDOPTS="--pdbcls=IPython.terminal.debugger:TerminalPdb" \ + just start +``` + +## Logging conventions + +These rules guide new and changed logging code; existing code may not yet +conform. + +- `logging.getLogger(__name__)` in every module; a `NullHandler` in library + `__init__.py` files. Never configure handlers, levels, or formatters in + library code — that is the application's job. The CLI's own configuration + is in `vcspull.log.setup_logger`. +- Pass structured context via `extra` rather than folding it into the message + string. Core keys are stable, scalar, and safe at any level: `vcs_cmd`, + `vcs_type`, `vcs_url`, `vcs_exit_code`, `vcs_repo_path`, + `vcspull_config_path`. Treat them as compatibility-sensitive — downstream + users build dashboards and alerts on them. Heavy keys (`vcs_stdout`, + `vcs_stderr`, both `list[str]`) are DEBUG-only and should be capped or + truncated. +- `snake_case`, `vcs_`-prefixed keys; prefer stable scalars over ad-hoc + objects. +- Lazy formatting: `logger.debug("msg %s", val)`, not an f-string. This skips + the interpolation entirely when the level is filtered, and keeps + aggregator grouping intact (an f-string makes every call site a unique + message). Guard an expensive `val` with + `if logger.isEnabledFor(logging.DEBUG)`. +- Increment `stacklevel` for each wrapper layer so `%(filename)s:%(lineno)d` + and OTel's `code.filepath` point at the real caller; re-check whenever call + depth changes. +- For an object with stable identity (a repository, a remote, a sync run), + use `LoggerAdapter` instead of repeating the same `extra` on every call. +- Level by audience, not severity of code path: + + | Level | Use for | Examples | + | ----- | ------- | -------- | + | `DEBUG` | Internal mechanics, VCS I/O | VCS command + stdout, URL parsing steps | + | `INFO` | Repository lifecycle, user-visible operations | Repository cloned, sync completed | + | `WARNING` | Recoverable issues, deprecation, user-actionable config | Deprecated VCS option, unrecognized remote | + | `ERROR` | Failures that stop an operation | VCS command failed, invalid URL | + + Config discovery noise belongs in `DEBUG`; only a surprising or + user-actionable config issue rises to `WARNING`. +- `logger.exception()` only inside an `except` block you are not + re-raising from. `logger.error(..., exc_info=True)` when the traceback is + needed outside an `except` block. Avoid `logger.exception()` followed by + `raise` — it duplicates the traceback. +- Avoid: f-strings/`.format()` in log calls; unguarded logging in hot loops; + catch-log-reraise without adding context; `print()` for diagnostics; + logging a secret env var's value (log the key name only); non-scalar + ad-hoc objects in `extra`; requiring custom `extra` fields in a format + string without a safe default (a missing key raises `KeyError`). + +Message wording — lowercase, past tense, no trailing punctuation — and the +stdout/stderr split are prose policy, not a workflow step: +[WRITING.md](WRITING.md#cli-output-and-error-messages). + +## Documentation + +[Sphinx](https://www.sphinx-doc.org/) generates the documentation. Build it: + +```console +$ just build-docs +``` + +Preview with live reload while editing: + +```console +$ cd docs +``` + +```console +$ just start +``` + +`just build-docs` is also the only check that catches a broken MyST +cross-reference — build the docs before committing a change under `docs/`. +See [MyST roles](WRITING.md#markdown-and-cross-references) for the role and +anchor conventions the build enforces. + +## Releasing + +Never create tags. Never push tags. The owner handles tagging and tag +pushes, because a tag triggers the publish workflow. See +[Release commits](WRITING.md#release-commits). + +1. Update `CHANGES`: add the `## vcspull vX.Y.Z (YYYY-MM-DD)` header below + the unreleased placeholder's `END PLACEHOLDER` marker. +2. Bump `version` in `pyproject.toml` and `__version__` in + `src/vcspull/__about__.py` — both are hardcoded and must match; neither is + derived from the other. +3. Commit the bump, then create a signed tag: `git tag -s v`. +4. Push the branch, then push the tag: `git push --tags`. + +Pushing the tag is what starts the release: `.github/workflows/tests.yml`'s +`release` job runs on `push` to a `refs/tags/*` ref, builds the package, and +publishes it to PyPI via trusted publishing. There is no separate manual +`uv build` / `uv publish` step. + +## Pull requests + +One subject per pull request. Unrelated cleanup found along the way belongs +in its own commit, and usually in its own pull request. + +Discuss a substantial change via an issue before making it. + +Commit format is in [WRITING.md](WRITING.md#commits). + +You may merge the pull request once you have the sign-off of one other +developer. If you do not have permission to do that, request a reviewer to +merge it for you. + +## Decorum + +- Participants will be tolerant of opposing views. +- Participants must ensure that their language and actions are free of + personal attacks and disparaging personal remarks. +- When interpreting the words and actions of others, participants should + always assume good intentions. +- Behaviour which can be reasonably considered harassment will not be + tolerated. + +Based on [Ruby's Community Conduct Guideline](https://www.ruby-lang.org/en/conduct/). + +## Security + +Please do not open a public issue for a vulnerability. Report it privately +through the repository's +[Security tab](https://github.com/vcs-python/vcspull/security/advisories/new) +on GitHub. diff --git a/.github/WRITING.md b/.github/WRITING.md new file mode 100644 index 000000000..9d467fc7a --- /dev/null +++ b/.github/WRITING.md @@ -0,0 +1,796 @@ +# Writing + +How this project writes prose, for humans and agents alike. It governs +`README.md`, `CHANGES`, release notes, commit messages, CLI and help text, +error messages, docstrings, source comments, and migration guides — every +surface a reader reaches. + +For environment setup, the gates, and pull request workflow, see +[CONTRIBUTING.md](CONTRIBUTING.md). + +## Voice + +Three surfaces, one voice. A docstring says what a caller may rely on; a +`CHANGES` entry says what changed; prose says what happens. All three are +present tense, lead with the thing being described, and stop. Why it was built +that way belongs in the commit message, which is timestamped and attached to +the diff. + +The most useful editing operation is deleting the introductory sentence. + +Lead with verbs and name concrete things. Put identifiers in backticks. Prefer +short declarative sentences, one operational fact each. Do not explain Python +to Python developers; do explain this project's semantics. + +Type annotations describe shape. Documentation describes meaning. A sentence +that restates a signature has said nothing. + +Use MUST, SHOULD, and MAY only where the normative sense is meant. Say what +actually happens rather than that something is "supported". + +| Instead of | Prefer | +| --------------------------------- | ----------------------------------- | +| "We added…" | "`vcspull sync` now accepts…" | +| "New and improved" | "`vcspull list` now…" | +| "powerful", "seamless" | state the capability | +| "easily", "simply", "just" | omit | +| "simple", "obvious", "intuitive" | omit | +| "robust" | name the failure that is handled | +| "comprehensive" | name what is covered | +| "production-ready" | state the guarantee | +| "optimized", "blazingly fast" | give the magnitude | +| "various fixes" | name the components | +| "under the hood" | omit unless observable | +| "please note that", "note that" | state the fact | +| "leverage", "utilize" | "use" | +| "delve into" | "read", or omit | +| "best practices" | name the practice | +| "in order to" | "to" | + +## Who you are writing for + +The default reader runs `vcspull` from a shell and keeps a configuration file +in YAML or JSON — `~/.vcspull.yaml` or a file under `~/.config/vcspull/`. They +are fluent in git (often hg or svn too) and comfortable at a prompt, but you +cannot assume they read Python, know libvcs, or have heard of `load_configs`, +`extract_repos`, or the internal config reader. Serve them first. + +A second, smaller reader writes Python: code against `vcspull.config`, the +modules under `docs/internals/`, or a contribution. Serve them too, but mark +their material opt-in — "for the rarer cases", "advanced" — so the default +reader knows they can stop. Never make the common case pay a comprehension tax +for the advanced one. + +Rules that follow: + +- **Second person, present tense, active.** "You pin the entry", not "The + entry is pinned". Address the reader who is doing the thing. +- **Concept before configuration or API surface.** Open by saying what the + thing *is* and what it does for the reader. The YAML keys or the function + signature are the last detail they need, not the first. A page that opens + with "set these keys" or a signature has buried the idea under its + mechanics. +- **Say when they can stop.** Lead with the default and the reassurance: most + readers never touch this, the defaults work, everything here is optional. + Let a skimmer leave after one paragraph. +- **Grant permission, do not demand attention.** "Reach for this when…" tells + readers they are in the right place without implying they must read on. +- **Progressive disclosure.** Order by how many readers need it: the plain + `vcspull sync '*'`, then the one flag a few will tune, then the + per-repository `options:` block, then the Python API. Each step is for a + smaller audience than the last. +- **Lean on the pipeline.** The reader thinks configuration file → workspace + root → repository entry → sync; reinforce that chain when you explain where + a key lives or which repositories a command touches. It is the mental model + the whole tool hangs on. +- **Name the trade-off.** If an option costs something — `options.shallow` + trades git history for disk and time, `--exit-on-error` stops the whole run + at the first failure — say so, and say what it buys. State it; do not sell + it. +- **Frame by concept, not by mechanism.** Do not headline a feature as "the + `--dry-run` flag" or "the `options:` block" in prose; that names the + implementation surface, which is the reader's last concern. Name the + concept: previewing a sync, pinning an entry. The mechanics vocabulary — a + pin-key table, the generated flag listing — is correct in a reference table, + and only there. + +### What stays precise + +Warm the framing, never the facts. Config search-order lists, pin-key tables, +exact warning strings (`No repo found in config(s) for …`), YAML schema +fragments, exit-status meanings, and class or function cross-references carry +meaning in their exact form — leave them alone. The friendly voice belongs in +the sentences *around* a precise block, introducing it, not inside it +paraphrasing it into vagueness. + +## README + +A README is the shortest path from "what is this?" to competent use, not the +project's autobiography. + +The first sentence is a contract. It says what abstraction the reader has been +handed, concretely enough to tell this package apart from the neighbouring +one. + +Get to a runnable command or snippet before anything the reader can skip. A +logo, a mission statement, a comparison matrix and three paragraphs of history +in front of the install line all cost the same thing. + +State the minimum Python version and meaningful platform constraints in prose, +not only in badges. `requires-python` in `pyproject.toml` is the authority; +the README must agree with it. + +Name the distribution, the import, and the executable separately wherever +they differ. That distinction prevents a Python-specific class of confusion. + +Examples are executable or, where the file is not collected by pytest, +honest — never `vcspull `. See +[Documented examples that run](#documented-examples-that-run) for which +blocks are checked and how. + +Document the semantic model, not the flag list. `--help` already enumerates +flags; what it cannot say is precedence, filesystem effects, what goes to +stdout versus stderr, and what a non-zero exit means. + +State defaults explicitly — defaults are API. State negative guarantees where +they exist: "does not modify your configuration file without `--write` or +confirmation", "no network access", "never discards uncommitted changes". +They establish boundaries faster than any amount of description. + +Headings stay conventional and stable, because people deep-link them. Badges +are few and load-bearing. + +## CLI output and error messages + +One console script ships from this package — `vcspull` +(`[project.scripts]` in `pyproject.toml`) — with subcommands for `sync`, +`add`, `discover`, `import`, `list`, `search`, `status`, `fmt`, `migrate`, +`worktree`, and `completion`. The conventions below apply to all of them. + +**Exit statuses.** + +| Status | Meaning | +| ------ | ------- | +| `0` | Success. This includes `vcspull sync` runs where some repositories failed but `--exit-on-error`/`-x` was not passed — the summary reports the failures, but the process exits clean so a script can inspect per-repository results instead of aborting the batch. | +| `1` | A fatal error: an unrecoverable condition (bad config, `import` handler failure), or `--exit-on-error` stopped a `sync` at the first failure. | +| `2` | argparse rejected the arguments — an unknown flag, a missing required value, or similar usage error. | +| `130` | Interrupted by `SIGINT` (Ctrl-C). On POSIX this is a real signal death (`WIFSIGNALED`), not a plain `SystemExit`, so shells stop a `cmd1; cmd2` sequence the way they would for any other signalled child. | + +**stdout versus stderr.** vcspull's own log messages — the `INFO`/`WARNING` +lines a normal run prints — write to stdout by design, interleaved with the +human-readable summary. `--json` and `--ndjson` payloads also write to +stdout. Only three things go to stderr: argparse usage errors, a fatal +`SystemExit` message, and the "Interrupted by user" notice a Ctrl-C prints. +Do not move CLI log output to stderr without updating this table — scripts +that parse stdout depend on the current split. + +**Message style.** Lowercase, past tense for events: `"repository cloned"`, +`"vcs command failed"`. No trailing punctuation. Keep the message short; put +identifiers and structured detail in the logging call's `extra`, not the +message string. + +**Destructive-operation invariant.** vcspull never discards work without an +explicit signal from the caller: + +- Commands that write a configuration file (`add`, `discover`, `import`, + `fmt --write`) prompt for confirmation before writing, or skip the prompt + only when `--yes` is passed. +- `fmt` and `migrate` default to a preview; they touch disk only when + `--write` is passed. +- `--dry-run` never touches disk, on any command that supports it. +- `vcspull sync --include-worktrees` refuses to update a worktree with + uncommitted changes rather than overwrite them; if the dirty check itself + fails, vcspull treats the worktree as dirty rather than risk data loss. +- Batch `sync` sets `GIT_TERMINAL_PROMPT=0` so a missing credential fails the + repository instead of blocking the whole run on stdin. + +## Documented examples that run + +Examples in this project are tests where the file they live in is collected — +and vcspull collects only part of its documentation, so read this section +before assuming a block runs. + +**A fence tag is cosmetic. Only a `>>> ` prompt executes, and only inside a +collected file.** A block written as + + ```python + server = Server() + ``` + +is prose that looks like a test. Nothing collects it, nothing runs it, and it +can be wrong for years. The same block written with prompts is a test — *if* +its file is collected: + + ```python + >>> server = Server() + ``` + +**Where doctests run.** `pyproject.toml` sets +`addopts = "... --doctest-modules"` and `testpaths = ["src/vcspull", "tests", +"docs/_ext", "scripts"]`. A `>>> ` block in a docstring under `src/vcspull`, +`docs/_ext`, or `scripts` is collected and executed by `pytest`. The root +`conftest.py` wires libvcs's `add_doctest_fixtures` into every doctest, so a +docstring example may use `tmp_path`, `create_git_remote_repo` (and +`create_git_remote_repo_bare`), `example_git_repo`, `create_svn_remote_repo` +(and `_bare`), and `create_hg_remote_repo` (and `_bare`) without importing +them — each pair is only added when the matching VCS binary (`git`, `svn` + +`svnadmin`, `hg`) is on `PATH`, so a doctest using one must tolerate that VCS +being absent in some CI environments. + +**`README.md` and `docs/*.md` pages are not doctested.** Neither path is in +`testpaths`, so a `>>> ` prompt added to either one is not collected and does +not run — this repository does not use doctested Markdown. Adding one is not +a mistake that breaks anything, but it also does not add the test coverage a +contributor might expect; do not claim a README or docs example is "tested" +on that basis. + +**`docs/*.md` command examples are checked structurally instead.** Two pytest +files harvest every `vcspull …` command shown in the docs and in the CLI's own +help text, then feed each one to the real argparse parser +(`create_parser(return_subparsers=False).parse_args(...)`) and fail if +argparse rejects it: + +- `tests/docs/test_markdown_conventions.py` reads every Markdown file under + `docs/`, requires shell commands to use `console` fences (not `bash`, `sh`, + `shell`, or `zsh`), requires a plain `console` fence to hold only the + command (mixed output belongs in `vcspull-console` or `vcspull-output` + instead — see [Markdown](#markdown-and-cross-references)), and parses every + `$ vcspull …` line it finds in a `console` or `vcspull-console` fence. +- `tests/cli/test_help_examples.py` does the same for every `vcspull …` line + embedded in the CLI's `*_DESCRIPTION` help-text constants in + `src/vcspull/cli/__init__.py`. + +This catches a renamed flag or a typo'd subcommand. It does **not** run the +command or check its output — a `vcspull sync --dry-run "*"` example that +parses fine can still show output that no longer matches a real run. + +**`README.md` examples are checked by neither mechanism.** `README.md` lives +outside `docs/`, so the harvesters above never see it, and it is not in +`testpaths`. A `vcspull …` command in the README is verified only by whoever +last copied it from a real terminal. Copy the command and its output from an +actual run, and re-check the block whenever the flags or output it shows +change — see +[Examples that stay honest](#examples-that-stay-honest). + +**`# doctest: +SKIP` is not permitted** in the doctests that do run. It is a +workaround that tests nothing. Use the fixtures, or gate on the VCS binary the +way `add_doctest_fixtures` already does. + +**Do not downgrade a doctest to a non-executed block to make it pass.** A +`.. code-block::` or an unprompted fence does not run. If an example cannot +pass, fix the example or fix the code. + +**Option flags.** `ELLIPSIS` and `NORMALIZE_WHITESPACE` are enabled globally +for the doctests that do run, so `...` elides variable output and whitespace +differences do not fail a comparison. Reach for an inline `# doctest: +FLAG` +only for the block that needs it. + +**Docstring examples** use the NumPy `Examples` section: + + Examples + -------- + >>> from vcspull.config import extract_repos + >>> config = {'~/code/': {'myrepo': 'git+https://github.com/user/repo'}} + >>> repos = extract_repos(config) + >>> len(repos) + 1 + +### Examples that stay honest + +Sphinx does not execute code blocks under `docs/`, and neither harvester above +checks output. Honesty there is manual: copy commands and output from a run +you actually made, keep YAML consistent with the real schema (workspace root +→ repository entry), and re-check a page's examples whenever the flags or +keys they show change. + +## The changelog + +`CHANGES` is the changelog. Not `CHANGELOG.md`. It is rendered as the +project's changelog page (`docs/history.md` is a bare +` ```{include} ../CHANGES ``` `). It is modeled on Django's release-notes +shape — deliverables get titles and prose, not bullets. + +A ledger, not a narrative. It is scanned, and the question a reader is asking +is whether an entry affects them. + +**Release entry boilerplate.** Every release header is +`## vcspull vX.Y.Z (YYYY-MM-DD)` — note the `v` prefix on the version. The +file opens with a `## vcspull vX.Y.Z (unreleased)` placeholder block fenced by +`` and `` HTML +comments. New entries land immediately below the `END` marker, never above +it. + +**Open with a multi-sentence lead paragraph.** Plain prose, no italic. Open +with the version as sentence subject ("vcspull vX.Y.Z ships …") so the lead is +self-contained when excerpted. Two to four sentences telling the reader what +shipped and who cares — user-visible takeaways, not internal mechanism. +Cross-reference detail docs with `{ref}` to keep the lead compact. + +**Unreleased entries carry no lead paragraph and no version summary.** +Sections only (`### Breaking changes`, `### What's new` deliverables, +`### Fixes`, …). Speaking for the release — what the version "is", "ships", +or "focuses on" — is presumptuous before its scope is final. Only the person +cutting the release writes that, and only when the release is actually +happening. Never write or edit a lead paragraph from a feature branch, and +never ask or imply that a release should happen. + +**Each deliverable is a section, not a bullet.** Inside `### What's new`, +every distinct deliverable gets a `#### Deliverable title (#NN)` heading +naming it in user vocabulary, followed by one to three prose paragraphs +explaining what shipped. Do not wrap a paragraph in `- ` — bullets are for +enumerable lists, not paragraph containers. Cross-link detail docs +(`See {ref}\`foo\` for details.`) so prose stays focused. + +**The deliverable test.** Before writing an entry, ask: "What's the +deliverable, in user vocabulary?" If you cannot answer in one sentence, the +entry is not ready. Mechanism — helper internals, byte counters, schema +validation locations — belongs in PR descriptions and code comments, not the +changelog. + +**Fixed subheadings**, in this order when present: `### Breaking changes`, +`### Dependencies`, `### What's new`, `### Fixes`, `### Documentation`, +`### Development`. Dev tooling (helper scripts, internal automation) lives +under `### Development`. For breaking changes, show the migration path with +concrete inline code (a `# Before` / `# After` fenced block). Dependency floor +bumps use the form ``Minimum `pkg>=X.Y.Z` (was `>=X.Y.W`)``. + +**PR refs `(#NN)`** sit in each deliverable's `####` heading. + +**When bullets are appropriate.** Catch-all sections (`### Fixes`, +occasionally `### Documentation`) with three or more genuinely small items use +bullets — one line each, never paragraphs. If a bullet swells past two lines, +promote it to a `#### Title (#NN)` heading with prose body. + +**Anti-patterns.** Fragile metrics that go stale silently — token ceilings, +third-party version pins, percent benchmarks, exact byte counts. Describe the +capability, not the math. Internal jargon: private symbols (leading-underscore +identifiers), algorithm names exposed for the first time, backend scaffolding. +Walls of text dressed up as bullets. Breaking changes buried mid-entry instead +of given their own subheading at the top. + +**Summarizing `CHANGES` on request.** When asked what changed in the latest +version, lead with the entry's lead paragraph (paraphrased if needed), +followed by each `####` deliverable heading under `### What's new` with a +one-sentence summary. Cite `(#NN)` only if source links are requested. Do not +invent versions, dates, or numbers not present in `CHANGES`, and do not quote +line numbers or file offsets — those shift as the file evolves. + +## Release notes + +`CHANGES` is the permanent ledger; a release page is editorial. Lead with one +paragraph naming the headline change, then three to five highlights, then link +the full changelog. + +Numbers over adjectives. "Cold start 41 ms to 6 ms" is a sentence; "much +faster startup" is a smell. + +A list of merged commit subjects is a merge log wearing a release-note hat. +Put the hand-written highlights above it. + +Versions are PEP 440 identifiers. Semantic-versioning meaning is applied to +the documented public API — which includes command names, options, exit +statuses, configuration keys, environment variables, and serialized formats, +not only imported Python symbols. + +## Docstrings + +The prime directive: never restate the type. The annotation is the source of +truth; the docstring carries what the annotation cannot. + +This is documentation debt wearing a docstring: + + def get_id(repo: Repo) -> str: + """Get the repo's identifier. + + Parameters + ---------- + repo : Repo + The repo. + + Returns + ------- + str + The identifier. + """ + +Document instead the dimensions the type system cannot encode: + +- **Mutation.** What it changes in place. +- **Ownership.** What the caller must close, release, or keep alive. +- **Ordering.** Whether results come back in a guaranteed order. +- **Timing.** What has finished by the time the call returns. +- **Failure.** Which exceptions are raised and what triggers each. +- **Idempotence.** Whether calling twice does anything the second time. +- **Concurrency.** Whether calls are coalesced, queued, or independent. +- **Units and ranges.** What a number means and what values are accepted. +- **Boundary behaviour.** What zero, empty, and the maximum do. +- **Platform.** Behaviour that differs by operating system, VCS binary, or + dependency version. +- **Security boundary.** What is executed, and what is only read. + +The ambiguity worth resolving by example: whether "retry three times" means +three attempts or four. State it. + +The first sentence stands alone; tooling truncates there. PEP 257 applies: +triple double quotes, an imperative one-line summary ending in a period, a +blank line before any extended description. Follow the +[NumPy docstring convention](https://numpydoc.readthedocs.io/en/latest/format.html) +throughout — `ruff`'s `pydocstyle` rule enforces the `numpy` convention, so +the dialect is not relitigated in review. + +**`NamedTuple` and dataclass fields document every field, in an `Attributes` +section:** + + class ConfigFileResolution(t.NamedTuple): + """Outcome of deciding which config file ``add`` should write to. + + Attributes + ---------- + path : pathlib.Path | None + Config file to write to, or ``None`` when the choice was + ambiguous. + """ + +Autodoc renders every field whether or not you describe it, so an +undocumented `NamedTuple` field ships to the API docs as "Alias for field +number 0", and a dataclass field ships bare. Document all of them — a class +with three fields and two documented still ships a stub for the third. + +**Every function and method carries a working doctest.** A doctest is both +documentation and a test; see +[Documented examples that run](#documented-examples-that-run) for the fixtures +available and the collection rules. If a working doctest genuinely cannot be +written for a function, say so in the pull request rather than shipping +`# doctest: +SKIP` or a silently undocumented function. + +## Source comments + +A comment ships only if it passes all three gates. Fail any: delete or +rewrite. Borderline: delete — borderline means the information is +reconstructible, which is what makes deletion cheap. + +**Loss.** Three years from now, would losing this cost a maintainer real time +rediscovering intent, an invariant, a constraint, or a failure mode the code +and tests do not already make obvious? + +**Elite.** Would SQLite, Redis, the Go standard library, or CPython write this +comment, at this length? Those projects state the constraint and stop. They do +not argue with an imagined objector. + +**Upkeep.** Will it stay true without maintenance? A comment that hand-syncs a +value the code owns — a count, an offset, a line reference, a duplicated +constant — is false the first time that value moves. + +### Ceiling + +One or two lines. A comment reaching four is either carrying several facts, in +which case split it, or arguing, in which case cut it to the fact. + +Rationale, alternatives weighed, and the story of how the code got here belong +in the commit message: timestamped, attached to the exact diff, and free to +maintain. + +A comment often holds both a constraint and the deliberation that found it. +Keep the constraint, cut the deliberation. "Runs at most once per second" +survives; "this is the right trade for now" does not. + +### Keep + +- Why over how: upstream quirks, protocol and compatibility constraints, + performance tradeoffs still part of the contract. +- Invariants, preconditions, ordering, lifetime, and concurrency requirements + that types and tests cannot express. +- Code that looks wrong but is not, so a later cleanup does not reintroduce + the bug. +- A high-level sketch of an algorithm whose local operations do not reveal the + whole. + +### Delete + +- Narration of the next lines; code translated into English. +- Restated names, types, defaults, or control flow. +- Values duplicated from the code and hand-synced. +- Justification, hedging, or apology for a choice. +- Speculation about future requirements. +- History version control already holds, including commented-out code. +- Ticket and issue numbers. They say nothing to a reader without tracker + access, and they rot when the tracker moves. Unfinished work goes in the + tracker, not the source. +- Transient observations — "currently", "for now", "the latest release" — + that go stale with no nearby edit. + +### The upkeep gate in practice + +It reaches values that track our own code. It does not reach frozen external +facts. + +Bad (Delete): + + # There are 321 tests to complete for servers. + +Good (Keep): + + # git < 2.28 has no --initial-branch, so this falls back to + # renaming the branch after init. + +### Documentation exception + +Minimal usage examples, and parameter, return, and raises entries on public +API are exempt from the loss gate — they serve the caller, not the +maintainer. They are exempt from nothing else. Ceiling: a good man page +entry. + +## Terminology and capitalization + +Pick the domain noun and keep it. The pipeline is configuration file → +workspace root → repository entry → sync; use those four terms and no others +for those four things. Do not call a repository entry a "checkout" in one +paragraph and an "entry" in the next, and do not alternate "sync" with +"clone/update", "pull", or "fetch" for the CLI operation — those words name +what a single sync may do internally (clone if absent, update if present), +not the command itself. + +Stable vocabulary is what makes search, deep links, and an agent's retrieval +work at all. + +Python and PyPI keep their own capitalisation. Distribution names are written +as they are published. + +Do not write counts into prose — how many symbols exist, how many tests there +are. They go stale silently and no reader needs them. Counts that pin a +fixture or guard an invariant are different, and belong in code. + +## Markdown and cross-references + +Prose wraps at 80 columns. Table rows, badge lines, and long links are +exempt, because breaking them harms rendering. A pull request or issue body +does not wrap at all: GitHub renders a single newline as a space in a file +and as a line break in a comment, so a wrapped comment body arrives as ragged +stubs. + +GitHub alert blocks — `> [!NOTE]`, `> [!WARNING]` — render as literal text +outside GitHub, so reserve them for at most one load-bearing warning per +document. Write the sentence so it carries the fact on its own, and a +renderer that drops the marker loses nothing. + +Do not use a local absolute path or an email address in anything published. + +**Console block flavors.** Three, and they are not interchangeable: +` ```console ` for a command at a `$` prompt and nothing else, +` ```vcspull-console ` for a command plus vcspull's own styled output, and +` ```vcspull-output ` for output alone. The last two are custom Pygments +lexers registered from `docs/_ext`. `tests/docs/test_markdown_conventions.py` +enforces the split — see +[Documented examples that run](#documented-examples-that-run). + +**Reference blocks are generated, never paraphrased.** CLI pages embed the +live parser with an `{eval-rst}` block wrapping `.. argparse::`, and the +`docs/internals/api/**` pages document modules with `.. automodule::`. +Introduce them in prose; a sentence that restates their content will drift +out of sync with the code that generates them. + +**MyST roles.** Any class, method, function, exception, or attribute that has +its own rendered page is cited with the matching role — `{class}`, `{meth}`, +`{func}`, `{exc}`, `{attr}` — never with plain backticks. `{mod}` covers a +module, `{ref}` an internal anchor, `{doc}` a page without an explicit ref +label. Plain backticks stay correct for code syntax, environment variables, +parameter names, and file paths that are not doc pages. + +Link the first prose mention of any symbol that has a useful destination on +that page — Python objects, vcspull APIs, libvcs APIs, CLI command pages, +configuration pages, and external tools or projects. After the first linked +mention on a page, later mentions can stay plain unless distance or context +makes another link useful. Do not rely on a later reference section to +satisfy the first-mention rule: if the first occurrence would be a heading, +grid-card teaser, or introductory sentence, link that occurrence or retitle +the heading so the first prose mention can carry the link. Leave command +examples, code blocks, and literal configuration values as code; link the +surrounding prose instead. + +A `{ref}` must match its target's anchor exactly. Page anchors are hyphenated +(`cli-sync`, `config-pin`) except for a few underscore holdouts in the +internals (`api_cli`). `just build-docs` catches a broken cross-reference; +nothing else does — build the docs before committing a change to `docs/`. + +## Code blocks + +Code blocks are paste-and-run units: pasting one block runs exactly one +intended action. Executed examples are exempt — the test suite runs them, +nobody pastes them. + +- **One command per block.** Multiple steps may share a block only when + explicitly chained with `&&`, `;`, or `\` continuations — the chain is then + one logical command. +- **Explanations go in prose above the block**, never as `#` comments inside + it. +- **Command menus are per-command blocks with prose lead-ins**, not tables. +- **Shell commands use the `console` tag with a `$ ` prefix.** This separates + interactive commands from scripts and enables prompt-aware copy. +- **Split long commands with `\`** — one flag or flag+value pair per indented + continuation line, positional arguments last. +- **Prefer longform flags** in prose and docs examples — `--workspace` not + `-w`, `--file` not `-f`. A `--help` listing is the right place for the + short forms. + +Good — show the last ten commits as a graph: + +```console +$ git log \ + --max-count=10 \ + --graph \ + --oneline +``` + +Bad: + +```console +# Show the last ten commits as a graph +$ git log --max-count=10 --graph --oneline +``` + +## Commits + +``` +Scope(type[detail]): concise description + +why: Explanation of necessity or impact. + +what: +- Specific technical changes made +- Focused on a single topic +``` + +Keep the subject to 50 characters or fewer, excluding any trailing `(#NN)` +pull request reference, and wrap body lines at 72. Separate the `why:` and +`what:` blocks with a blank line. + +Routine maintenance commits drop the colon and take a capitalised +description, which is what distinguishes them at a glance in +`git log --oneline` — this repo's history uses this form for +`py(deps[dev])`, `ai(rules[AGENTS])`, and `ai(claude[commands])`: + +``` +py(deps[dev]) Bump dev packages +ai(rules[AGENTS]) Judge comments by three gates +ai(claude[commands]) Add /update-libvcs skill +``` + +Everything that changes behaviour keeps the colon. + +The `why:` is the pragmatic, contextual reason for the change — never cite +`AGENTS.md`, `CLAUDE.md`, or another rule file as the justification. "AGENTS.md +says…" or "CLAUDE.md requires…" is not a reason; look at `git log -n 10 -p`, +the PR description, and the linked issue for the real engineering reason +("function had no doctest coverage", not "the house style requires +doctests"). + +Common types: + +- **feat**: New features or enhancements +- **fix**: Bug fixes +- **refactor**: Code restructuring without functional change +- **docs**: Documentation updates +- **chore**: Maintenance (dependencies, tooling, config) +- **test**: Test-related updates +- **style**: Code style and formatting +- **ci**: Workflow and pipeline changes +- **py(deps)**: Dependencies +- **py(deps[dev])**: Dev dependencies +- **ai(rules[AGENTS])**: AI rule updates +- **ai(claude[commands])**: Claude Code slash-command changes + (`.claude/commands/`) + +For a change under `docs/_ext`, use `docs` as the top-level component: + +``` +docs(sphinx_argparse_neo[renderer]): Escape asterisks in quoted strings + +why: Glob patterns like "django-*" cause RST emphasis issues + +what: +- Add _escape_glob_asterisks() helper method +- Call it before RST parsing in _parse_text() +``` + +Example: + +``` +cli(add[repo]): Add support for custom remote URLs + +why: Enable users to specify alternative remote URLs for repositories + +what: +- Add remote_url parameter to add_repo function +- Update CLI argument parser to accept --remote-url option +- Add tests for the new functionality +``` + +For a multi-line message, use a heredoc so the formatting survives: + +```console +$ git commit -m "$(cat <<'EOF' +Scope(feat[detail]): Concise description + +why: Explanation of the change. + +what: +- First change +- Second change +EOF +)" +``` + +### Release commits + +Never create tags. Never push tags. The owner handles tagging and tag pushes, +because a tag triggers the publish workflow. + +A release commit subject is plain and short: `Tag v`. The detailed +why and what go in the body. Do not use the `Scope(type[detail]):` format for +a release — it buries the lede. + +## Slop prevention + +Treat AI slop as review-hostile noise, not as proof that text or code is +wrong. The goal is to maximise information density. + +- **AI signatures.** No "Generated by", no conversational filler, no + unexplained emoji, no tool metadata. +- **Brittle references.** No hard-coded line numbers, fragile file counts, + dated "as of" claims, bare SHAs, or local absolute paths — unless they are + strict evidentiary artefacts, such as a benchmark log, a stack trace, a + release note, or a lockfile, where the exact count, date, or SHA is the + evidence. +- **Diff narration.** Do not restate what moved, was renamed, or was removed + in anything the reader holds alongside the diff: code, docstrings, README, + or a pull request description. The diff and the commit message already + carry it. +- **Branch-internal narrative.** Do not mention intermediate states, + abandoned approaches, or "no longer" behaviour unless users of a published + release actually experienced the old state — the published-release test + below. +- **Low-value scaffolding.** No ownerless TODOs, unused future-proofing, + debug artefacts, or defensive wrappers around failure modes nothing can + reach. +- **Prose inflation.** The diction table under [Voice](#voice) governs; + replace an inflated word with a concrete description of behaviour, + constraints, or trade-offs. +- **Coded labels.** Write rules and findings as plain imperatives. No `[R1]`, + `Option B`, or any index a reader has to decode. + +Preserve the "why". Never delete a comment documenting an invariant, a +protocol constraint, a platform quirk, or an upstream workaround — those are +the facts [Source comments](#source-comments) keeps, and every other comment +is judged by it. + +### Durable source links + +Link to a pinned revision, never to trunk. A pinned permalink is not a +brittle reference; an unlinked SHA dropped into prose is. `blob/master/…` +links rot silently — the file moves, lines shift, and the anchor lands on +unrelated code while still resolving. + +- Prefer a release tag (`blob/v1.66.0/…`). Most durable, and it tells the + reader which released version the claim held for. +- Otherwise use a 7-character commit ref (`blob/9a29b1a/…`) reachable from + trunk. Use this when there is no tag or the claim is about unreleased code. + Never a PR-head SHA — it can be rebased or garbage-collected. +- Reserve `blob/master/…` for living documents meant to always show the + latest state, such as this file or `CONTRIBUTING.md`. +- Line anchors (`#L120-L145`) are only safe on a pinned ref. + +### The published-release test + +Long-running branches accumulate tactical decisions — renames, refactors, +attempts-then-reverts. When deciding what counts as branch-internal, use +trunk as the baseline, not an intermediate state inside the current branch. +Ask: did users of the most recently published release ever experience this +old name, old behaviour, or bug? If the answer is no, it is branch-internal +narrative — move it to the commit message and describe only the final state +in the artefact that ships. + +Keep in shipped artefacts: deprecations and migration guides for symbols +that actually shipped; `### Fixes` entries for bugs that affected users of a +published release; and comments explaining why the current code looks this +way (invariants, platform quirks) that make sense to a reader who never saw +the previous version. diff --git a/.github/contributing.md b/.github/contributing.md deleted file mode 100644 index c0eddac2c..000000000 --- a/.github/contributing.md +++ /dev/null @@ -1,27 +0,0 @@ -# Contributing - -When contributing to this repository, please first discuss the change you wish to make via issue, -email, or any other method with the maintainers of this repository before making a change. - -See [developing](../docs/developing.md) for environment setup and [AGENTS.md](../AGENTS.md) for -detailed coding standards. - -## Pull Request Process - -1. **Format and lint**: `uv run ruff format .` then `uv run ruff check . --fix --show-fixes` -2. **Type check**: `uv run mypy` -3. **Test**: `uv run pytest` — all tests must pass before submitting -4. **Document**: Update docs if your change affects the public interface -5. You may merge the Pull Request once you have the sign-off of one other developer. If you - do not have permission to do that, you may request a reviewer to merge it for you. - -## Decorum - -- Participants will be tolerant of opposing views. -- Participants must ensure that their language and actions are free of personal - attacks and disparaging personal remarks. -- When interpreting the words and actions of others, participants should always - assume good intentions. -- Behaviour which can be reasonably considered harassment will not be tolerated. - -Based on [Ruby's Community Conduct Guideline](https://www.ruby-lang.org/en/conduct/) diff --git a/AGENTS.md b/AGENTS.md index cba0d7278..5f9f3c797 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,727 +1,59 @@ # AGENTS.md -This file provides guidance to LLM Agents such as Codex, Gemini, Claude Code (claude.ai/code), etc. when working with code in this repository. - -## CRITICAL REQUIREMENTS - -### Test Success -- ALL tests MUST pass for code to be considered complete and working -- Never describe code as "working as expected" if there are ANY failing tests -- Even if specific feature tests pass, failing tests elsewhere indicate broken functionality -- Changes that break existing tests must be fixed before considering implementation complete -- A successful implementation must pass linting, type checking, AND all existing tests - -## Project Overview - -vcspull is a Python tool for managing and synchronizing multiple git, svn, and mercurial repositories via YAML or JSON configuration files. It allows users to pull/update multiple repositories in a single command, optionally filtering by repository name, path, or VCS URL. - -## Development Environment - -### Setup and Installation - -```bash -# Install development dependencies with uv -uv pip install -e . - -# Alternative: Use uv sync to install from pyproject.toml -uv sync -``` - -### Common Commands - -#### Testing - -```bash -# Run all tests -uv run pytest - -# Run specific test(s) -uv run pytest tests/test_cli.py -uv run pytest tests/test_cli.py::test_sync - -# Watch mode for tests (auto re-run on file changes) -uv run ptw . -# or -just start - -# Run tests with coverage -uv run py.test --cov -v -``` - -#### Code Quality - -```bash -# Format code with ruff -uv run ruff format . -# or -just ruff-format - -# Run ruff linting with auto-fixes -uv run ruff check . --fix --show-fixes -# or -just ruff - -# Run mypy type checking -uv run mypy -# or -just mypy - -# Watch mode for linting (using entr) -just watch-ruff -just watch-mypy -``` - -#### Documentation - -```bash -# Build documentation -just build-docs - -# Start documentation server (auto-reload) -just start-docs -``` - -## Development Process - -Follow this workflow for code changes: - -1. **Format First**: `uv run ruff format .` -2. **Run Tests**: `uv run py.test` -3. **Run Linting**: `uv run ruff check . --fix --show-fixes` -4. **Check Types**: `uv run mypy` -5. **Verify Tests Again**: `uv run py.test` - -## Code Architecture - -### Core Components - -1. **Configuration** - - `config.py`: Handles loading and parsing of YAML/JSON configuration files - - `_internal/config_reader.py`: Low-level config file reading - -2. **CLI** - - `cli/__init__.py`: Main CLI entry point with argument parsing - - `cli/sync.py`: Repository synchronization functionality - - `cli/add.py`: Adding new repositories to configuration - -3. **Repository Management** - - Uses `libvcs` package for VCS operations (git, svn, hg) - - Supports custom remotes and URL schemes - -### Configuration Format - -Configuration files are stored as YAML or JSON in either: -- `~/.vcspull.yaml`/`.json` (home directory) -- `~/.config/vcspull/` directory (XDG config) - -Example format: -```yaml -~/code/: - flask: "git+https://github.com/mitsuhiko/flask.git" -~/study/c: - awesome: "git+git://git.naquadah.org/awesome.git" -``` - -## Coding Standards - -### Imports - -- Use namespace imports for stdlib: `import enum` instead of `from enum import Enum`; third-party packages may use `from X import Y` -- For typing, use `import typing as t` and access via namespace: `t.NamedTuple`, etc. - -**For third-party packages:** Use idiomatic import styles for each library (e.g., `from pygments.token import Token` is fine). - -**Always:** Use `from __future__ import annotations` at the top of all Python files. - -### Docstrings - -Follow NumPy docstring style for all functions and methods: - -```python -"""Short description of the function or class. - -Detailed description using reStructuredText format. - -Parameters ----------- -param1 : type - Description of param1 -param2 : type - Description of param2 - -Returns -------- -type - Description of return value -""" -``` - -**Classes with fields** — `NamedTuple`, dataclasses — document every field in -an `Attributes` section: - -```python -class ConfigFileResolution(t.NamedTuple): - """Outcome of deciding which config file ``add`` should write to. - - Attributes - ---------- - path : pathlib.Path | None - Config file to write to, or ``None`` when the choice was ambiguous. - """ -``` - -Autodoc renders every field whether or not you describe it, so an -undocumented `NamedTuple` field ships to the API docs as "Alias for field -number 0" and a dataclass field ships bare. Document all of them — a class -with three fields and two documented still ships a stub for the third. - -### Doctests - -**All functions and methods MUST have working doctests.** Doctests serve as both documentation and tests. - -**CRITICAL RULES:** -- Doctests MUST actually execute - never comment out function calls or similar -- Doctests MUST NOT be converted to `.. code-block::` as a workaround (code-blocks don't run) -- If you cannot create a working doctest, **STOP and ask for help** - -**Available tools for doctests:** -- `doctest_namespace` fixtures (inherited from libvcs): `tmp_path`, `create_git_remote_repo`, `create_hg_remote_repo`, `create_svn_remote_repo` -- Ellipsis for variable output: `# doctest: +ELLIPSIS` -- Update `conftest.py` to add new fixtures to `doctest_namespace` - -**`# doctest: +SKIP` is NOT permitted** - it's just another workaround that doesn't test anything. If a VCS binary might not be installed, pytest already handles skipping via `skip_if_binaries_missing`. Use the fixtures properly. - -**Using fixtures in doctests:** -```python ->>> from vcspull.config import extract_repos ->>> config = {'~/code/': {'myrepo': 'git+https://github.com/user/repo'}} ->>> repos = extract_repos(config) # doctest: +ELLIPSIS ->>> len(repos) -1 -``` - -**When output varies, use ellipsis:** -```python ->>> repo_dir = tmp_path / 'repo' # tmp_path from doctest_namespace ->>> repo_dir.mkdir() ->>> repo_dir # doctest: +ELLIPSIS -PosixPath('.../repo') -``` - -### Logging Standards - -These rules guide future logging changes; existing code may not yet conform. - -#### Logger setup - -- Use `logging.getLogger(__name__)` in every module -- Add `NullHandler` in library `__init__.py` files -- Never configure handlers, levels, or formatters in library code — that's the application's job - -#### Structured context via `extra` - -Pass structured data on every log call where useful for filtering, searching, or test assertions. - -**Core keys** (stable, scalar, safe at any log level): - -| Key | Type | Context | -|-----|------|---------| -| `vcs_cmd` | `str` | VCS command line | -| `vcs_type` | `str` | VCS type (git, svn, hg) | -| `vcs_url` | `str` | repository URL | -| `vcs_exit_code` | `int` | VCS process exit code | -| `vcs_repo_path` | `str` | local repository path | -| `vcspull_config_path` | `str` | workspace config file path | - -**Heavy/optional keys** (DEBUG only, potentially large): - -| Key | Type | Context | -|-----|------|---------| -| `vcs_stdout` | `list[str]` | VCS stdout lines (truncate or cap; `%(vcs_stdout)s` produces repr) | -| `vcs_stderr` | `list[str]` | VCS stderr lines (same caveats) | - -Treat established keys as compatibility-sensitive — downstream users may build dashboards and alerts on them. Change deliberately. - -#### Key naming rules - -- `snake_case`, not dotted; `vcs_` prefix -- Prefer stable scalars; avoid ad-hoc objects -- Heavy keys (`vcs_stdout`, `vcs_stderr`) are DEBUG-only; consider companion `vcs_stdout_len` fields or hard truncation (e.g. `stdout[:100]`) - -#### Lazy formatting - -`logger.debug("msg %s", val)` not f-strings. Two rationales: -- Deferred string interpolation: skipped entirely when level is filtered -- Aggregator message template grouping: `"Running %s"` is one signature grouped ×10,000; f-strings make each line unique - -When computing `val` itself is expensive, guard with `if logger.isEnabledFor(logging.DEBUG)`. - -#### stacklevel for wrappers - -Increment for each wrapper layer so `%(filename)s:%(lineno)d` and OTel `code.filepath` point to the real caller. Verify whenever call depth changes. - -#### LoggerAdapter for persistent context - -For objects with stable identity (Repository, Remote, Sync), use `LoggerAdapter` to avoid repeating the same `extra` on every call. Lead with the portable pattern (override `process()` to merge); `merge_extra=True` simplifies this on Python 3.13+. - -#### Log levels - -| Level | Use for | Examples | -|-------|---------|----------| -| `DEBUG` | Internal mechanics, VCS I/O | VCS command + stdout, URL parsing steps | -| `INFO` | Repository lifecycle, user-visible operations | Repository cloned, sync completed | -| `WARNING` | Recoverable issues, deprecation, user-actionable config | Deprecated VCS option, unrecognized remote | -| `ERROR` | Failures that stop an operation | VCS command failed, invalid URL | - -Config discovery noise belongs in `DEBUG`; only surprising/user-actionable config issues → `WARNING`. - -#### Message style - -- Lowercase, past tense for events: `"repository cloned"`, `"vcs command failed"` -- No trailing punctuation -- Keep messages short; put details in `extra`, not the message string - -#### Exception logging - -- Use `logger.exception()` only inside `except` blocks when you are **not** re-raising -- Use `logger.error(..., exc_info=True)` when you need the traceback outside an `except` block -- Avoid `logger.exception()` followed by `raise` — this duplicates the traceback. Either add context via `extra` that would otherwise be lost, or let the exception propagate - -#### Testing logs - -Assert on `caplog.records` attributes, not string matching on `caplog.text`: -- Scope capture: `caplog.at_level(logging.DEBUG, logger="vcspull.cli")` -- Filter records rather than index by position: `[r for r in caplog.records if hasattr(r, "vcs_cmd")]` -- Assert on schema: `record.vcs_exit_code == 0` not `"exit code 0" in caplog.text` -- `caplog.record_tuples` cannot access extra fields — always use `caplog.records` - -#### Avoid - -- f-strings/`.format()` in log calls -- Unguarded logging in hot loops (guard with `isEnabledFor()`) -- Catch-log-reraise without adding new context -- `print()` for diagnostics -- Logging secret env var values (log key names only) -- Non-scalar ad-hoc objects in `extra` -- Requiring custom `extra` fields in format strings without safe defaults (missing keys raise `KeyError`) - -### Testing - -**Use functional tests only**: Write tests as standalone functions (`test_*`), not classes. Avoid `class TestFoo:` groupings - use descriptive function names and file organization instead. This applies to pytest tests, not doctests. - -#### Using libvcs Fixtures - -When writing tests, leverage libvcs's pytest plugin fixtures: - -- `create_git_remote_repo`, `create_svn_remote_repo`, `create_hg_remote_repo`: Factory fixtures -- `git_repo`, `svn_repo`, `hg_repo`: Pre-made repository instances -- `set_home`, `gitconfig`, `hgconfig`, `git_commit_envvars`: Environment fixtures - -Example: -```python -def test_vcspull_sync(git_repo): - # git_repo is already a GitSync instance with a clean repository - # Use it directly in your tests -``` -#### Release commits - -Never create tags. Never push tags. The user handles tagging and tag -pushes (tags trigger the CI publish workflow). - -Release commit subjects are plain and short: `Tag v`. Put -the detailed why/what in the commit body. Don't use the -`Scope(type[detail]):` format for releases — don't bury the lede. - -For multi-line commits, use heredoc to preserve formatting: -```bash -git commit -m "$(cat <<'EOF' -feat(Component[method]) add feature description - -why: Explanation of the change. - -what: -- First change -- Second change -EOF -)" -``` - -#### Test Structure - -Use `typing.NamedTuple` for parameterized tests: - -```python -class CLIFixture(t.NamedTuple): - test_id: str # For test naming - cli_args: list[str] - expected_exit_code: int - expected_in_out: ExpectedOutput = None - -@pytest.mark.parametrize( - list(CLIFixture._fields), - CLI_FIXTURES, - ids=[test.test_id for test in CLI_FIXTURES], -) -def test_cli_subcommands( - # Parameters and fixtures... -): - # Test implementation -``` - -#### Mocking Strategy - -- Use `monkeypatch` for environment, globals, attributes -- Use `mocker` (from pytest-mock) for application code -- Document every mock with comments explaining WHAT is being mocked and WHY - -#### Configuration File Testing - -- Use project helper functions like `vcspull.tests.helpers.write_config` or `save_config_yaml` -- Avoid direct `yaml.dump` or `file.write_text` for config creation - -### Git Commit Standards - -Format commit messages as: -``` -Scope(type[detail]): concise description - -why: Explanation of necessity or impact. - -what: -- Specific technical changes made -- Focused on a single topic -``` - -Keep the subject ≤50 chars (excluding any trailing `(#NN)` PR ref); wrap -body lines at ≤72 chars. Separate the `why:` and `what:` blocks with a -blank line. - -The `why:` must be the pragmatic, contextual reason behind the change — never cite AGENTS.md, CLAUDE.md, or other rule files as the justification. If you feel compelled to write "AGENTS.md says..." or "CLAUDE.md requires...", look at `git log -n 10 -p`, the PR description, and the ticket for the real engineering reason (e.g., "function had no doctest coverage" not "CLAUDE.md requires doctests"). - -Common commit types: -- **feat**: New features or enhancements -- **fix**: Bug fixes -- **refactor**: Code restructuring without functional change -- **docs**: Documentation updates -- **chore**: Maintenance (dependencies, tooling, config) -- **test**: Test-related updates -- **style**: Code style and formatting -- **ai(rules[AGENTS])**: AI rule updates -- **ai(claude[rules])**: Claude Code rules (CLAUDE.md) -- **ai(claude[command])**: Claude Code command changes - -Examples: -``` -cli(add[repo]) Add support for custom remote URLs - -why: Enable users to specify alternative remote URLs for repositories - -what: -- Add remote_url parameter to add_repo function -- Update CLI argument parser to accept --remote-url option -- Add tests for the new functionality -``` - -For docs/_ext changes, use `docs` as the top-level component: -``` -docs(sphinx_argparse_neo[renderer]) Escape asterisks in quoted strings - -why: Glob patterns like "django-*" cause RST emphasis issues - -what: -- Add _escape_glob_asterisks() helper method -- Call it before RST parsing in _parse_text() -``` - -## Documentation Standards - -### Code Blocks - -Code blocks are paste-and-run units: pasting one block runs exactly one -intended action. Doctests and other executed examples are exempt — the test -suite runs them, nobody pastes them. - -- **One command per block.** Multiple steps may share a block only when - explicitly chained with `&&`, `;`, or `\` continuations — the chain is - then one logical command. -- **Explanations go in prose above the block**, never as `#` comments inside it. -- **Command menus are per-command blocks with prose lead-ins**, not tables. -- **Shell commands use the `console` tag with a `$ ` prefix.** This separates - interactive commands from scripts and enables prompt-aware copy. -- **Split long commands with `\`** — one flag or flag+value pair per indented - continuation line, positional arguments last. -- **Prefer longform flags** — use `--workspace` not `-w`, `--file` not `-f`. - -Good: - -Show the last ten commits as a graph: - -```console -$ git log \ - --max-count=10 \ - --graph \ - --oneline -``` - -Bad: - -```console -# Show the last ten commits as a graph -$ git log --max-count=10 --graph --oneline -``` - -### Changelog Conventions - -These rules apply when authoring entries in `CHANGES`, which is rendered as the Sphinx changelog page. Modeled on Django's release-notes shape — deliverables get titles and prose, not bullets. Older entries used a flat `### Section` + bullet shape; new entries follow the Django shape below. - -**Release entry boilerplate.** Every release header is `## vcspull vX.Y.Z (YYYY-MM-DD)` (note the `v` prefix on the version). The file opens with a `## vcspull vX.Y.Z (unreleased)` placeholder block fenced by `` and `` HTML comments — new release entries land immediately below the END marker, never above it. - -**Open with a multi-sentence lead paragraph.** Plain prose, no italic. Open with the version as sentence subject (*"vcspull vX.Y.Z ships …"*) so the lead is self-contained when excerpted. Two to four sentences telling the reader what shipped and who cares — user-visible takeaways, not internal mechanism. Cross-reference detail docs with `{ref}` to keep the lead compact. - -**Lead paragraphs are release-time material — off-limits to branches and PRs.** The unreleased entry carries no lead paragraph and no version summary: sections only (`### Breaking changes`, `### What's new` deliverables, `### Fixes`, …). Speaking for the release — what the version "is", "ships", or "focuses on" — is presumptuous before its scope is final; only the person cutting the release writes that, and only when the user explicitly asks to release. Never write or edit a lead from a feature branch, and never ask or imply that a release should happen. - -**Each deliverable is a section, not a bullet.** Inside `### What's new`, every distinct deliverable gets a `#### Deliverable title (#NN)` heading naming it in user vocabulary, followed by 1-3 prose paragraphs explaining what shipped. Don't wrap a paragraph in `- ` — bullets are for enumerable lists, not paragraph containers. Cross-link detail docs (`See {ref}\`foo\` for details.`) so prose stays focused. - -**The deliverable test.** Before writing an entry, ask: "What's the deliverable, in user vocabulary?" If you can't answer in one sentence, the entry isn't ready. Mechanism (helper internals, byte counters, schema-validation locations) belongs in PR descriptions and code comments, not the changelog. - -**Fixed subheadings**, in this order when present: `### Breaking changes`, `### Dependencies`, `### What's new`, `### Fixes`, `### Documentation`, `### Development`. Dev tooling (helper scripts, internal automation) lives under `### Development`. For breaking changes, show the migration path with concrete inline code (e.g. a `# Before` / `# After` fenced code block). Dependency floor bumps use the form ``Minimum `pkg>=X.Y.Z` (was `>=X.Y.W`)``. - -**PR refs `(#NN)`** sit in each deliverable's `####` heading. - -**When bullets are appropriate.** Catch-all sections (`### Fixes`, occasionally `### Documentation`) with 3+ genuinely small items use bullets — one line each, never paragraphs. If a bullet swells past two lines, promote it to a `#### Title (#NN)` heading with prose body. - -**Anti-patterns.** - -- Fragile metrics: token ceilings, third-party version pins, percent benchmarks, exact byte counts. Describe the *capability*, not the math. -- Internal jargon: private symbols (leading-underscore identifiers), algorithm names exposed for the first time, backend scaffolding. -- Walls of text dressed up as bullets. -- Buried breaking changes — they get their own subheading at the top of the entry. - -**Always link autodoc'd APIs.** Any class, method, function, exception, or attribute that has its own rendered page must be cited via the appropriate role (`{class}`, `{meth}`, `{func}`, `{exc}`, `{attr}`) — never with plain backticks. Doc pages without explicit ref labels use `{doc}`. Plain backticks are correct for code syntax, env vars, parameter names, and file paths that aren't doc pages — anything without an autodoc destination. - -**MyST roles.** Class references use `{class}` (e.g. `{class}\`~vcspull.config.ConfigReader\``), methods use `{meth}`, functions use `{func}`, exceptions use `{exc}`, attributes use `{attr}`, internal anchors use `{ref}`, doc-path links use `{doc}`. - -**Summarization style.** When a user asks "what changed in the latest version?" or similar, lead with the entry's lead paragraph (paraphrased if needed), followed by each `####` deliverable heading under `### What's new` with a one-sentence summary. Cite `(#NN)` only if the user asks for source links. Don't invent versions, dates, or numbers not present in `CHANGES`. Don't quote line numbers or file offsets — those shift as the file evolves. - -## Debugging Tips - -When stuck in debugging loops: - -1. **Pause and acknowledge the loop** -2. **Minimize to MVP**: Remove all debugging cruft and experimental code -3. **Document the issue** comprehensively for a fresh approach -4. Format for portability (using quadruple backticks) - -## Comments earn their maintenance cost - -A comment ships only if it passes all three gates. Fail any: delete or rewrite. -Borderline: delete — borderline means the information is reconstructible, which -is what makes deletion cheap. - -**Loss.** Three years from now, would losing this cost a maintainer real time -rediscovering intent, an invariant, a constraint, or a failure mode the code and -tests do not already make obvious? - -**Elite.** Would SQLite, Redis, the Go standard library, or CPython write this -comment, at this length? Those projects state the constraint and stop. They do -not argue with an imagined objector. - -**Upkeep.** Will it stay true without maintenance? A comment that hand-syncs a -value the code owns — a count, an offset, a line reference, a duplicated -constant — is false the first time that value moves. - -### Ceiling - -One or two lines. A comment reaching four is either carrying several facts, in -which case split it, or arguing, in which case cut it to the fact. - -Rationale, alternatives weighed, and the story of how the code got here belong -in the commit message: timestamped, attached to the exact diff, and free to -maintain. - -A comment often holds both a constraint and the deliberation that found it. Keep -the constraint, cut the deliberation. "Runs at most once per second" survives; -"this is the right trade for now" does not. - -### Keep - -- Why over how: upstream quirks, protocol and compatibility constraints, - performance tradeoffs still part of the contract. -- Invariants, preconditions, ordering, lifetime, and concurrency requirements - that types and tests cannot express. -- Code that looks wrong but is not, so a later cleanup does not reintroduce the - bug. -- A high-level sketch of an algorithm whose local operations do not reveal the - whole. - -### Delete - -- Narration of the next lines; code translated into English. -- Restated names, types, defaults, or control flow. -- Values duplicated from the code and hand-synced. -- Justification, hedging, or apology for a choice. -- Speculation about future requirements. -- History version control already holds, including commented-out code. -- Ticket and issue numbers. They say nothing to a reader without tracker access, - and they rot when the tracker moves. Unfinished work goes in the tracker, not - the source. -- Transient observations — "currently", "for now", "the latest release" — - that go stale with no nearby edit. - -### The upkeep gate in practice - -It reaches values that track our own code. It does not reach frozen external -facts. - -Bad (Delete): - -```python -# There are 321 tests to complete for servers. -``` - -Good (Keep): - -```python -# CPython < 3.11 has no ExceptionGroup, so this branch stays. -``` - -### Documentation exception - -Doctests, minimal usage examples, and param, return, and raises lines on public -API are exempt from the loss gate — they serve the caller, not the maintainer. -They are exempt from nothing else. Ceiling: a good man page entry. - -NumPy-style `Parameters`, `Returns`, and `Attributes` sections and executable -doctests fall under this exception — autodoc ships every field whether or not -you describe it, and a doctest that runs is also a test. - -## AI Slop Prevention - -Treat AI slop as **review-hostile noise**, not as proof that text or -code is wrong. The goal is to maximize information density by removing -artifacts that make the repository harder to trust or navigate. - -### The Anti-Slop Rubric - -Before committing, audit all AI-assisted changes for these noise -patterns: - -- **AI Signatures:** Remove "Generated by", footers, conversational - filler ("Certainly!", "Here is..."), unexplained emojis (🤖, ✨), and - AI-tool metadata. -- **Brittle References:** Avoid hard-coded line numbers, fragile - file/test counts, dated "as of" claims, bare SHAs, and local - absolute paths unless they are strict evidentiary artifacts (e.g., - benchmark logs). -- **Diff Narration:** Do not restate what moved, was renamed, or was - removed in artifacts the downstream reader holds: code, docstrings, - README, CHANGES, PR descriptions, or release notes. The diff and - commit message already carry this history. -- **Branch-Internal Narrative:** Do not mention intermediate branch - states, abandoned approaches, or "no longer" behavior unless users - of a published release actually experienced the old state (**The - Published-Release Test**). -- **Low-Value Scaffolding:** Remove ownerless TODOs (`TODO: revisit`), - unused future-proofing, debug artifacts, and defensive wrappers that - do not protect a currently reachable failure mode. -- **Prose Inflation:** Replace generic AI "tells" like *comprehensive, - robust, seamless, production-ready, leverage, delve, tapestry,* and - *best practices* with concrete descriptions of behavior, - constraints, or trade-offs. -- **Coded Labels:** Write rules, options, and findings as plain - imperatives. Don't tag them with codes like `[R1]`, `A1`, or - `Option B` in artifacts a human reads — the reader shouldn't have to - decode an index. Internal agent bookkeeping may use ids; shipped text - may not. - -### Durable Source Links - -Link to a pinned revision, never to trunk. A pinned permalink is not a -brittle reference; an unlinked SHA dropped into prose is. `blob/master/…` -links rot silently — the file moves, lines shift, and the anchor lands -on unrelated code while still resolving. - -- Prefer a release tag (`blob/v1.4.0/…`). Most durable, and it tells - the reader which released version the claim held for. -- Otherwise use a 7-char commit ref (`blob/9a29b1a/…`) reachable from - trunk. Use when there is no tag or the claim is about unreleased - code. Never a PR-head SHA — it can be rebased or garbage-collected. -- Reserve `blob/master/…` for living documents meant to always show the - latest state, such as a contributing guide. -- Line anchors (`#L120-L145`) are only safe on a pinned ref. - -### Preservation & Context - -Subjective cleanup must never remove load-bearing rationale. Adjudicate -comments with the comment policy above; borderline cases are deleted, not -kept. - -- **Preserve the "Why":** You MUST NOT delete comments that document - invariants, protocol constraints, platform quirks, security - boundaries, and upstream workarounds. -- **Evidence is Immune:** Preserve exact counts, dates, and SHAs when - they serve as evidence in benchmark results, release notes, stack - traces, or lockfiles. -- **Behavior Over Inventory:** A useful description explains what - changed for the *system or user*; it does not provide an inventory - of files or functions the diff already shows. - -### The Published-Release Test - -Long-running branches accumulate tactical decisions — renames, -refactors, attempts-then-reverts. When deciding what counts as -branch-internal, use trunk or the parent branch as the baseline — not -intermediate states inside the current branch. Ask: - -> Did users of the most recently published release ever experience -> this old name, old behavior, or bug? - -If the answer is **no**, it is branch-internal narrative. Move it to -the commit message and describe only the final state in the artifact. - -**Keep in shipped artifacts:** -- Deprecations and migration guides for symbols that actually shipped. -- `### Fixes` entries for bugs that affected users of a published - release. -- Comments explaining *why the current code looks this way* - (invariants, platform quirks) that make sense to a reader who never - saw the previous version. - -### Cleanup in Hindsight - -When applying these rules retroactively from inside a feature branch, -first establish scope by diffing against the parent branch (or trunk) -to identify which commits this branch actually introduced. Then: - -- **In-branch commits:** Prompt the user with two options: `fixup!` - commits with `git rebase --autosquash` to address each causal commit - at its source, or a single cleanup commit at branch tip. -- **Trunk/Parent commits:** Default to leaving them alone. Act only on - explicit user instruction. If the user opts in, fold the cleanup - into a single commit at branch tip; do not rewrite shared history. -- **Scope guard:** If cleaning prior slop would touch a colleague's - work or expand the branch beyond its stated goal, stay in lane: - protect the current goal and leave prior slop alone. - -### Change Discipline - -- Make the smallest coherent change that solves the verified problem; - keep unrelated cleanup out of it. -- Reuse an existing file, component, helper, API, or test before adding - a new one. Modify in place when the change fits the file's - responsibility. -- Keep new APIs private until a caller outside the module needs them. +vcspull manages and synchronizes many git, svn, and mercurial repositories +from a single YAML or JSON configuration file, via the `vcspull` CLI. + +Follow the conventions already in the tree, and keep a change scoped to what +was asked for. + +## What is here + +| Path | What it is | +| ---- | ---------- | +| `src/vcspull/cli/` | CLI subcommands: `sync`, `add`, `discover`, `import`, `list`, `search`, `status`, `fmt`, `migrate`, `worktree` | +| `src/vcspull/config.py` | Load and parse the YAML/JSON workspace configuration | +| `src/vcspull/_internal/` | Implementation detail; no stability guarantee across versions | +| `src/vcspull/exc.py` | Exceptions | +| `src/vcspull/log.py` | CLI logging setup and formatters | +| `tests/` | pytest suite | +| `docs/` | Sphinx documentation source | +| `docs/_ext/` | Custom Pygments lexers and doctested extension code | +| `scripts/` | Runtime dependency smoke test | +| `CHANGES` | Changelog, rendered at `docs/history.md` | + +## Which policy applies + +- Documentation, user-facing text, `CHANGES`, release notes, commit messages, + docstrings, and source comments: + [.github/WRITING.md](.github/WRITING.md) +- Environment, the gates, tests, documentation builds, releases, and pull + requests: [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) + +Each of those is the single home for its subject. Where a rule seems to be +stated twice, the file listed above is the one that governs. + +## Change discipline + +- Make the smallest coherent change that solves the verified problem; keep + unrelated cleanup out of it. +- Reuse an existing file, helper, API, or test before adding a new one. - Add a file only for a durable boundary — a distinct responsibility, - independent reuse, or splitting an oversized high-touch module — not - for a single-use helper or a one-line re-export. - -### Keep Instructions Lean - -Treat this file like code and prune it. - -- Delete a line whose removal would not cause a mistake. -- Move multi-step procedures into skills, path-specific rules into - nested AGENTS.md files, and hard limits into hooks or CI. -- Keep only non-obvious, broadly applicable defaults here. Anything a - reader can infer from the code, a manifest, or a linter does not - belong. + independent reuse, or splitting an oversized module — not for a single-use + helper or a one-line re-export. +- Add a test for every user-visible behaviour change, and a `CHANGES` entry + for every change to the public API, CLI, configuration, or output. +- A passing gate is evidence only once it has been shown capable of failing. + Pair a new test with a deliberate break that proves it bites. + +vcspull's release cadence is coupled to `libvcs`, which does the actual git, +svn, and hg work; `[tool.uv.exclude-newer-package]` in `pyproject.toml` +exempts `libvcs` and the documentation toolchain from the `exclude-newer` +cooldown so `uv sync` is never blocked on it. Never discard uncommitted work +without an explicit `--write`/`--yes`/confirmation from the caller — see +[the destructive-operation invariant](.github/WRITING.md#cli-output-and-error-messages). + +## References + +- Changelog: `CHANGES` (rendered at ) +- Documentation: +- Upstream VCS layer: [libvcs](https://github.com/vcs-python/libvcs) diff --git a/README.md b/README.md index bc21d3f7a..d5afc1555 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # $ vcspull · [![Python Package](https://img.shields.io/pypi/v/vcspull.svg)](https://pypi.org/project/vcspull/) [![License](https://img.shields.io/github/license/vcs-python/vcspull.svg)](https://github.com/vcs-python/vcspull/blob/master/LICENSE) [![Code Coverage](https://codecov.io/gh/vcs-python/vcspull/branch/master/graph/badge.svg)](https://codecov.io/gh/vcs-python/vcspull) -Manage and sync multiple git, svn, and mercurial repos via JSON or YAML file. Compare to -[myrepos], [mu-repo]. Built on [libvcs]. +Manage and sync multiple git, svn, and mercurial repos via JSON or YAML +file. Compare to [myrepos], [mu-repo]. Built on [libvcs]. Great if you use the same repos at the same locations across multiple machines or want to clone / update a pattern of repos without having to `cd` into each one. -- clone / update to the latest repos with `$ vcspull` +- clone / update to the latest repos with `$ vcspull sync --all` - use filters to specify a location, repo url or pattern in the manifest to clone / update - supports svn, git, hg version control systems @@ -43,7 +43,7 @@ $ uvx vcspull ### Developmental releases -You can test the unpublished version of vcspull before its released. +You can test the unpublished version of vcspull before it's released. - [pip](https://pip.pypa.io/en/stable/): @@ -107,7 +107,8 @@ $ vcspull add ~/projects/libs/my-lib - `-f/--file` selects an alternate configuration file. - Append `--no-merge` if you prefer to review duplicate workspace roots yourself instead of having vcspull merge them automatically. -- Follow with `vcspull sync my-lib` to clone or update the working tree after registration. +- Follow with `vcspull sync my-lib` to clone or update the working tree + after registration. ### Discover local checkouts and add en masse @@ -118,10 +119,11 @@ your configuration: $ vcspull discover ~/code --recursive ``` -The scan shows each repository before import unless you opt into `--yes`. Add -`--workspace ~/code/` to pin the resulting workspace root or `-f/--file` to write somewhere other -than the default `~/.vcspull.yaml`. Duplicate workspace roots are merged by -default; include `--no-merge` to keep them separate while you review the log. +The scan shows each repository before import unless you opt into `--yes`. +Add `--workspace ~/code/` to pin the resulting workspace root or +`-f/--file` to write somewhere other than the default `~/.vcspull.yaml`. +Duplicate workspace roots are merged by default; include `--no-merge` to +keep them separate while you review the log. ### Import from remote services @@ -217,8 +219,8 @@ summary (`+/~/✓/⚠/✗`) grouped by workspace. Use `--summary-only`, `--relative-paths`, `--long`, or `-v/-vv` for alternate views, and `--fetch`/`--offline` to control how remote metadata is refreshed. -Keep nested VCS repositories updated too, lets say you have a mercurial -or svn project with a git dependency: +Keep nested VCS repositories updated too — say you have a mercurial or svn +project with a git dependency: `external_deps.yaml` in your project root (any filename will do): @@ -238,14 +240,11 @@ more. ## Pulling specific repos -Have a lot of repos? - -you can choose to update only select repos through +Have a lot of repos? Choose to update only select repos through [fnmatch](http://pubs.opengroup.org/onlinepubs/009695399/functions/fnmatch.html) -patterns. remember to add the repos to your `~/.vcspull.{json,yaml}` -first. +patterns. Add the repos to your `~/.vcspull.{json,yaml}` first. -The patterns can be filtered by by directory, repo name or vcs url. +The patterns filter by directory, repo name, or vcs url. Any repo starting with "fla": @@ -265,10 +264,10 @@ Search by vcs + url, since urls are in this format +://: $ vcspull sync "git+*" ``` -Any git repo with python in the vcspull: +Any git repo with python in the vcs url: ```console -$ vcspull sync "git+*python* +$ vcspull sync "git+*python*" ``` Any git repo with django in the vcs url: diff --git a/docs/AGENTS.md b/docs/AGENTS.md deleted file mode 100644 index 1f187d270..000000000 --- a/docs/AGENTS.md +++ /dev/null @@ -1,141 +0,0 @@ -# Documentation voice - -This file covers the *voice* of prose under `docs/` — how to frame a -page so a reader meets the idea before its configuration. It -complements the repository-root `AGENTS.md`, which already governs -code blocks, shell-command formatting, changelog conventions, and MyST -roles. When the two overlap, the root file wins; this one only answers -the question it leaves open: how should the prose sound? - -## Who you are writing for - -The default reader runs vcspull from a shell and keeps a configuration -file in YAML or JSON — `~/.vcspull.yaml` or a file under -`~/.config/vcspull/`. They are fluent in git (often hg or svn too) and -comfortable at a prompt, but you cannot assume they read Python, know -libvcs, or have heard of `load_configs`, `extract_repos`, or the -internal config reader. - -A second, smaller reader writes Python: code against `vcspull.config`, -the modules under `docs/internals/`, or a contribution. Serve them -too, but mark their material opt-in ("for the rarer cases", -"advanced") so the default reader knows they can stop. Never make the -common case pay a comprehension tax for the advanced one. - -## Voice - -- **Second person, present tense, active.** "You pin the entry", not - "The entry is pinned". Address the reader who is doing the thing. -- **Concept before configuration.** Open by saying what the thing *is* - and what it does for the reader. The YAML surface — the keys, the - flags — is the last detail they need, not the first. A page that - opens with "set these keys" has buried the idea under its mechanics. -- **Say when they can stop.** Lead with the default and the - reassurance: most readers never touch this, the defaults work, - everything here is optional. Let a skimmer leave after one sentence. -- **Progressive disclosure.** Order by how many readers need it: the - plain `vcspull sync '*'` → the one flag a few will tune → the - per-repository `options:` block → the Python API. Each step is for - a smaller audience than the last. -- **Lean on the pipeline.** The reader thinks configuration file → - workspace root → repository entry → sync; reinforce that chain when - you explain where a key lives or which repositories a command - touches. It is the mental model the whole tool hangs on. -- **Name the trade-off.** If an option costs something — - `options.shallow` trades git history for disk and time, - `--exit-on-error` stops the whole run at the first failure — say - so, and say what it buys. State it; don't sell it. -- **Frame by concept, not by mechanism.** Don't headline a feature as - "the `--dry-run` flag" or "the `options:` block" in prose; that - names the implementation surface, which is the reader's last - concern. Name the concept: previewing a sync, pinning an entry. The - mechanics vocabulary — a pin-key table, the generated flag listing — - is correct in a reference table, and only there. - -## What stays precise - -Warm the framing, never the facts. Config search-order lists, pin-key -tables, exact warning strings ("No repo found in config(s) for …"), -YAML schema fragments, and class or function cross-references carry -meaning in their exact form — leave them alone. The friendly voice -belongs in the sentences *around* a precise block, introducing it, -not inside it paraphrasing it into vagueness. - -## Examples that stay honest - -Sphinx does not execute code blocks under `docs/`. Pytest checks the -Markdown fence conventions and parses documented `vcspull ...` commands with -the real argparse tree, but it does not run mutating, networked, or VCS -commands from the pages. Honesty is still manual: copy commands and output -from a run you actually made, keep YAML consistent with the real schema -(workspace root → repository entry), and re-check a page's examples whenever -the flags or keys they show change. - -## Console blocks and reference pages - -Two mechanical conventions, separate from voice: - -- **Console blocks** come in three flavors: ```` ```console ```` for - a command at a `$` prompt (the root `AGENTS.md` shape), - ```` ```vcspull-console ```` for a command *plus* vcspull's styled - output, and ```` ```vcspull-output ```` for output alone. The last - two are custom lexers registered from `docs/_ext`. -- **Reference blocks are generated.** CLI pages embed the live parser - with an `{eval-rst}` block wrapping `.. argparse::`, and the - `docs/internals/api/**` pages document modules with - `.. automodule::`. Introduce them in prose; never paraphrase their - content into sentences that will drift. - -## Cross-references - -Point the advanced reader at the deep-dive rather than inlining it, -and put the link where their interest peaks — on the phrase that made -them curious ("pin the entry", "bulk import") — not as a standalone -footnote the eye skips. Use the MyST roles listed in the root -`AGENTS.md` (`{class}`, `{meth}`, `{func}`, `{exc}`, `{attr}`, -`{ref}`, `{doc}`). A `{ref}` must match its target's anchor exactly — -page anchors are hyphenated (`cli-sync`, `config-pin`) except for a -few underscore holdouts in the internals (`api_cli`). -`just build-docs` catches a broken cross-reference; nothing else -does — so build the docs before you commit. - -Link the first prose mention of any symbol that has a useful destination on -that page. This includes Python objects, vcspull APIs, libvcs APIs, CLI -command pages, configuration pages, and external tools or projects. Use the -most specific target available: `{class}`, `{meth}`, `{func}`, `{mod}`, -`{exc}`, or `{attr}` for API objects; `{ref}` or `{doc}` for documentation -pages and section anchors; and a Markdown link or reference link for external -projects. After the first linked mention on a page, later mentions can stay -plain unless the distance or context makes another link useful. - -Do not rely on a later reference section to satisfy the first-mention rule. -If the first occurrence would be a heading, grid-card teaser, or introductory -sentence, link that occurrence or retitle the heading so the first prose -mention can carry the link. Leave command examples, code blocks, and literal -configuration values as code; link the surrounding prose instead. - -## A page that does this - -`docs/cli/add.md` is the worked example: a concept-first intro that -says what the command does for you — register a checkout in your -configuration — before any flag, an early note routing the bulk cases -to the `cli-discover` and `cli-import` targets right when the reader -would wonder, the generated argparse reference left exact, sections -ordered basic usage → overrides → automation, real output shown in -`vcspull-console` blocks, and honest behavior notes (it prompts before -writing; dry runs still show merge diagnostics). Read it before -reshaping another page. - -## Before you commit - -- Does the page open with what the feature *is*, or with how to - configure it? -- Can a reader who needs only the default stop after the first - paragraph? -- Is anything framed as "the keys/flags" that should be named by - concept instead? -- Are the advanced and Python-only parts clearly marked opt-in? -- Did you leave every table, warning string, and cross-reference - exact — and does every console example match a run you actually made? -- Did `just build-docs` stay clean — no new warning, no broken - cross-reference? diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md deleted file mode 120000 index 47dc3e3d8..000000000 --- a/docs/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md \ No newline at end of file diff --git a/docs/project/code-style.md b/docs/project/code-style.md index 9e167ccca..df53ea7bc 100644 --- a/docs/project/code-style.md +++ b/docs/project/code-style.md @@ -2,32 +2,9 @@ # Code Style -## Formatting and linting +Formatting, linting, type-checking, and import conventions moved to +[CONTRIBUTING.md](https://github.com/vcs-python/vcspull/blob/master/.github/CONTRIBUTING.md#the-gates). -vcspull uses [ruff](https://docs.astral.sh/ruff/) for formatting and linting. - -```console -$ uv run ruff format . -``` - -```console -$ uv run ruff check . --fix --show-fixes -``` - -## Type checking - -[mypy](https://mypy-lang.org/) runs in strict mode. - -```console -$ uv run mypy -``` - -## Docstrings - -Follow [NumPy docstring convention](https://numpydoc.readthedocs.io/en/latest/format.html). - -## Imports - -- Use `from __future__ import annotations` in every file. -- Prefer namespace imports for stdlib: `import pathlib` not `from pathlib import Path`. -- Use `import typing as t` and access via `t.NamedTuple`, `t.TYPE_CHECKING`, etc. +Docstring conventions — NumPy style, the doctest requirement, and the +`Attributes` section for `NamedTuple` and dataclass fields — moved to +[WRITING.md](https://github.com/vcs-python/vcspull/blob/master/.github/WRITING.md#docstrings). diff --git a/docs/project/contributing.md b/docs/project/contributing.md index 4a8214b03..c5aefcb8e 100644 --- a/docs/project/contributing.md +++ b/docs/project/contributing.md @@ -1,359 +1,9 @@ # Development -Developing python projects associated with [git-pull.com] all use the same -structure and workflow. At a later point these will refer to that website for documentation. +Environment setup, the gates, tests, documentation builds, releases, and +pull request process moved to +[CONTRIBUTING.md](https://github.com/vcs-python/vcspull/blob/master/.github/CONTRIBUTING.md). -[git-pull.com]: https://git-pull.com - -## Bootstrap the project - -Install [git] and [uv] (see uv's [installation documentation]). - -Clone: - -```console -$ git clone https://github.com/vcs-python/vcspull.git -``` - -```console -$ cd vcspull -``` - -Install packages: - -```console -$ uv sync --all-extras --dev -``` - -[installation documentation]: https://docs.astral.sh/uv/getting-started/installation/ -[git]: https://git-scm.com/ - -## Development loop - -### Tests - -Tests run on [pytest]. - -[pytest]: https://pytest.org/ - -#### Rerun on file change - -via [pytest-watcher] (works out of the box): - -```console -$ just start -``` - -via [entr(1)] (requires installation): - -```console -$ just watch-test -``` - -[pytest-watcher]: https://github.com/olzhasar/pytest-watcher - -#### Manual (just the command, please) - -```console -$ uv run py.test -``` - -or: - -```console -$ just test -``` - -#### Runtime dependency smoke test - -Verify that the published wheel runs without dev/test extras: - -```console -$ uvx \ - --isolated \ - --no-cache \ - --from . \ - python scripts/runtime_dep_smoketest.py -``` - -The script imports every ``vcspull`` module and exercises each CLI sub-command -with ``--help``. There is also a pytest wrapper guarded by a dedicated marker: - -```console -$ uv run pytest \ - -m scripts__runtime_dep_smoketest \ - scripts/test_runtime_dep_smoketest.py -``` - -These checks are network-dependent because they rely on ``uvx`` to build the -package in an isolated environment. - -#### pytest options - -`PYTEST_ADDOPTS` can be set in the commands below. For more -information read [docs.pytest.com] for the latest documentation. - -[docs.pytest.com]: https://docs.pytest.org/ - -Verbose: - -```console -$ env PYTEST_ADDOPTS="-verbose" just start -``` - -Drop into {mod}`pdb` on first error: - -```console -$ env PYTEST_ADDOPTS="-x -s --pdb" just start -``` - -If you have [ipython] installed: - -```console -$ env PYTEST_ADDOPTS="--pdbcls=IPython.terminal.debugger:TerminalPdb" \ - just start -``` - -[ipython]: https://ipython.org/ - -### Documentation - -[sphinx] generates the documentation. In the future this may change to -[docusaurus]. - -Default preview server: http://localhost:8022 - -[sphinx]: https://www.sphinx-doc.org/ -[docusaurus]: https://docusaurus.io/ - -#### Rerun on file change - -[sphinx-autobuild] will automatically build the docs, it also handles launching -a server, rebuilding file changes, and updating content in the browser: - -```console -$ cd docs -``` - -```console -$ just start -``` - -If doing css adjustments: - -```console -$ just design -``` - -[sphinx-autobuild]: https://github.com/executablebooks/sphinx-autobuild - -Rebuild docs on file change (requires [entr(1)]): - -```console -$ cd docs -``` - -```console -$ just dev -``` - -Use two terminals if needed: - -```console -$ just watch -``` - -```console -$ just serve -``` - -#### Manual (just the command, please) - -```console -$ cd docs -``` - -Build: - -```console -$ just html -``` - -Launch server: - -```console -$ just serve -``` - -## Linting - -### ruff - -[ruff] handles formatting, import sorting, and linting. - -````{tab} Command - -uv: - -```console -$ uv run ruff check . -``` - -If you setup manually: - -```console -$ ruff check . -``` - -```` - -````{tab} just - -```console -$ just ruff -``` - -```` - -````{tab} Watch - -```console -$ just watch-ruff -``` - -requires [`entr(1)`]. - -```` - -````{tab} Fix files - -uv: - -```console -$ uv run ruff check . --fix -``` - -If you setup manually: - -```console -$ ruff check . --fix -``` - -```` - -#### ruff format - -[ruff format] formats the code. - -````{tab} Command - -uv: - -```console -$ uv run ruff format . -``` - -If you setup manually: - -```console -$ ruff format . -``` - -```` - -````{tab} just - -```console -$ just ruff-format -``` - -```` - -### mypy - -[mypy] checks static types. - -````{tab} Command - -uv: - -```console -$ uv run mypy . -``` - -If you setup manually: - -```console -$ mypy . -``` - -```` - -````{tab} just - -```console -$ just mypy -``` - -```` - -````{tab} Watch - -```console -$ just watch-mypy -``` - -requires [`entr(1)`]. -```` - -````{tab} Configuration - -See `[tool.mypy]` in pyproject.toml. - -```{literalinclude} ../../pyproject.toml -:language: toml -:start-at: "[tool.mypy]" -:end-before: "[tool" - -``` - -```` - -## Publishing to PyPI - -[uv] handles virtualenv creation, package requirements, versioning, -building, and publishing. Therefore there is no setup.py or requirements files. - -Update `__version__` in `__about__.py` and `pyproject.toml`, then commit the -bump: - -```console -$ git commit -m 'build(vcspull): Tag v0.1.1' -``` - -Tag it: - -```console -$ git tag v0.1.1 -``` - -Push the branch and the tag: - -```console -$ git push -``` - -```console -$ git push --tags -``` - -[GitHub Actions](https://github.com/features/actions) will detect the new -git tag, and in its own workflow run `uv build` and push to -[PyPI](https://pypi.org/). - -[uv]: https://github.com/astral-sh/uv -[entr(1)]: http://eradman.com/entrproject/ -[`entr(1)`]: http://eradman.com/entrproject/ -[ruff format]: https://docs.astral.sh/ruff/formatter/ -[ruff]: https://ruff.rs -[mypy]: http://mypy-lang.org/ +Prose conventions — README, `CHANGES`, commit messages, docstrings, and +source comments — are in +[WRITING.md](https://github.com/vcs-python/vcspull/blob/master/.github/WRITING.md).