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).