From 4dde6637b4bcd78de13761d659867d16c3915f9b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 10:02:53 -0500 Subject: [PATCH 1/2] Docs(refactor[agents]): Route instead of restate why: AGENTS.md is read on every task, so a policy that matters to one class of change costs context on all the others. Splitting how we work from how we write lets each be loaded when it applies, and keeps both discoverable to humans rather than only to agents. what: - Replace the AGENTS.md body with a router: the project map, universal change discipline, then one pointer per class of change - Add .github/WRITING.md for prose policy, folding in docs/AGENTS.md's voice guidance and the prose-policy parts of the old AGENTS.md - Add .github/CONTRIBUTING.md for workflow policy, folding in the workflow parts of the old AGENTS.md plus Logging Standards (kept out of src/libvcs/ so it cannot leak into the built wheel) - Turn docs/project/contributing.md and docs/project/code-style.md into pointer pages to the canonical .github/ files; code-style.md keeps its one executing doctest, since testpaths includes docs/ but not .github/ - Delete docs/AGENTS.md and docs/CLAUDE.md, absorbed into WRITING.md - Rewrite README.md to the new prose policy; fix the stale version pin example (>=0.37,<0.38 -> >=0.45,<0.46); preserve every link and every heading anchor --- .github/CONTRIBUTING.md | 262 ++++++++++++ .github/WRITING.md | 675 ++++++++++++++++++++++++++++++ AGENTS.md | 789 +++-------------------------------- README.md | 54 ++- docs/AGENTS.md | 142 ------- docs/CLAUDE.md | 1 - docs/project/code-style.md | 43 +- docs/project/contributing.md | 10 +- 8 files changed, 1056 insertions(+), 920 deletions(-) create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/WRITING.md delete mode 100644 docs/AGENTS.md delete mode 120000 docs/CLAUDE.md diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..a294fdeb --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,262 @@ +# Contributing + +Thanks for looking. libvcs accepts contributions through +[GitHub](https://github.com/vcs-python/libvcs). libvcs is pre-1.0: bug +reports with a reproduction, and reports of where the API or the +documentation misled you, are the most useful contributions right now. + +How this project writes prose — README, `CHANGES`, 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 + +Development requires [uv](https://github.com/astral-sh/uv). + +```console +$ git clone https://github.com/vcs-python/libvcs.git +``` + +```console +$ cd libvcs +``` + +```console +$ uv sync --all-extras --dev +``` + +## The gates + +[ruff](https://ruff.rs) formats and lints in a single tool. The full rule +set is declared in `pyproject.toml` under `[tool.ruff]`. + +Format: + +```console +$ uv run ruff format . +``` + +Lint: + +```console +$ uv run ruff check . --fix --show-fixes +``` + +[mypy](http://mypy-lang.org/) runs in strict mode (`[tool.mypy] strict = +true`): + +```console +$ uv run mypy . +``` + +Test: + +```console +$ uv run pytest +``` + +Documentation is a gate, not a courtesy. Examples in docstrings, +documentation pages under `docs/`, and `README.md` are executed by +`pytest`; the doctest flags live in `pyproject.toml`, so there is no +separate doctest step and a green `pytest` is the proof. Which blocks +qualify, and the one mistake that silently removes a test, 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 + +- `from __future__ import annotations` at the top of every file. +- Standard-library modules use namespace imports: `import pathlib`, not + `from pathlib import Path`. Third-party packages may use + `from X import Y`. +- Typing: `import typing as t`, then access via namespace — + `t.NamedTuple`, `t.Any`. + +### Logging + +These rules guide future logging changes; existing code may not yet +conform. + +**Setup.** Use `logging.getLogger(__name__)` in every module. Add a +`NullHandler` in library `__init__.py` files. Never configure handlers, +levels, or formatters in library code — that is 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 are +stable, scalar, and safe at any log level: `vcs_cmd` (`str`, the VCS command +line), `vcs_type` (`str`, git/svn/hg), `vcs_url` (`str`), `vcs_exit_code` +(`int`), `vcs_repo_path` (`str`). Heavy keys — `vcs_stdout`, `vcs_stderr` +(`list[str]`) — are DEBUG-only; truncate or cap them (`stdout[:100]`). +Names are `snake_case` with a `vcs_` prefix. Treat established keys as +compatibility-sensitive — downstream users may build dashboards and alerts +on them. + +**Lazy formatting.** `logger.debug("msg %s", val)`, not f-strings: the +interpolation is skipped entirely when the level is filtered, and a +log-aggregator's message-template grouping treats `"Running %s"` as one +signature instead of one per f-string value. Guard an expensive `val` with +`if logger.isEnabledFor(logging.DEBUG)`. + +**`stacklevel` for wrappers.** Increment it 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` instead of +repeating the same `extra` on every call. + +**Log levels.** `DEBUG` for internal mechanics and VCS I/O; `INFO` for +repository lifecycle and user-visible operations; `WARNING` for recoverable +issues, deprecations, and user-actionable config; `ERROR` for failures that +stop an operation. Config-discovery noise is `DEBUG`; only a surprising or +user-actionable config issue escalates to `WARNING`. + +**Message style.** Lowercase, past tense for events — `"repository +cloned"`, `"vcs command failed"` — no trailing punctuation. Keep the +message short; put details in `extra`. + +**Exception logging.** Use `logger.exception()` only inside an `except` +block when not re-raising. Use `logger.error(..., exc_info=True)` for a +traceback outside an `except` block. Avoid `logger.exception()` followed by +`raise` — it duplicates the traceback. + +**Testing logs.** Assert on `caplog.records` attributes, not string +matching on `caplog.text`: scope capture with +`caplog.at_level(logging.DEBUG, logger="libvcs.cmd")`, filter records by +attribute rather than position, and assert on schema +(`record.vcs_exit_code == 0`, not `"exit code 0" in caplog.text`). +`caplog.record_tuples` cannot access extra fields. + +**Avoid:** f-strings or `.format()` in log calls; unguarded logging in hot +loops; catch-log-reraise without adding context; `print()` for +diagnostics; logging secret env var values; non-scalar objects in `extra`; +custom `extra` fields referenced in a format string without a safe default +(a missing key raises `KeyError`). + +## Tests + +The suite spawns real `git`, `hg`, and `svn` processes. A test that needs a +VCS binary is skipped automatically when that binary is not on `PATH` — you +do not need all three installed to contribute. + +**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. + +**Parameterized tests** use `typing.NamedTuple` for the fixture shape: + +```python +class RepoFixture(t.NamedTuple): + test_id: str # For test naming + repo_args: dict[str, t.Any] + expected_result: str + + +@pytest.mark.parametrize( + list(RepoFixture._fields), + REPO_FIXTURES, + ids=[test.test_id for test in REPO_FIXTURES], +) +def test_sync(...): ... +``` + +**Fixtures.** `src/libvcs/pytest_plugin.py` (registered as a `pytest11` +entry point) provides: + +- `create_git_remote_repo`, `create_hg_remote_repo`, `create_svn_remote_repo` + — build a temporary remote repository, each gated on its VCS binary being + installed. +- `git_repo`, `hg_repo`, `svn_repo` — a ready-to-use sync instance checked + out from a session-cached remote; every consumer gets an isolated copy, so + a test may mutate it freely, including under parallel runs. +- `set_home`, `vcs_gitconfig`, `vcs_hgconfig`, `git_commit_envvars` — + environment fixtures that isolate `$HOME` and VCS configuration from the + host running the tests. + +The full reference, including the doctest-only helpers each fixture backs, +is at +[the pytest plugin API page](https://libvcs.git-pull.com/api/pytest-plugin/). + +**Running in parallel.** On a multi-core machine, [pytest-xdist](https://pytest-xdist.readthedocs.io/) +spreads the real-subprocess tests across workers: + +```console +$ just test-parallel +``` + +This runs `uv run py.test -n auto`, where `auto` sizes the worker pool to +the machine's cores. Parallelism is opt-in — `just test` and `uv run pytest` +stay serial by default. + +**Order independence.** Tests must pass regardless of the order they run +in. Keep fixtures self-contained and reset any global state in teardown. +Check locally with a shuffled run: + +```console +$ uv run --with pytest-randomly py.test -p randomly +``` + +**Debugging.** When stuck in a debugging loop: pause and name the loop out +loud, strip the reproduction down to its minimum, and write down what you +tried before changing approach. Guessing repeatedly at the same fix wastes +more time than the pause does. + +## Documentation + +```console +$ just build-docs +``` + +runs Sphinx and fails the build on a broken cross-reference — the doctests +do not catch that, so build the docs before committing a page that adds or +moves a `{ref}`, `{doc}`, or other role target. + +```console +$ just start-docs +``` + +starts [sphinx-autobuild](https://github.com/executablebooks/sphinx-autobuild) +at , rebuilding on file changes. + +## 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). + +libvcs is pre-1.0: a minor version bump (0.39 to 0.40) may contain breaking +changes; a patch bump (0.39.0 to 0.39.1) is reserved for bug fixes and +documentation. The version is set in `src/libvcs/__about__.py` and +`pyproject.toml`. The maintainer's full release checklist is published at +[Releasing](https://libvcs.git-pull.com/project/releasing/). + +## 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). + +## 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 advisories](https://github.com/vcs-python/libvcs/security). diff --git a/.github/WRITING.md b/.github/WRITING.md new file mode 100644 index 00000000..c780ba43 --- /dev/null +++ b/.github/WRITING.md @@ -0,0 +1,675 @@ +# Writing + +How libvcs writes prose, for humans and agents alike. It governs `README.md`, +`CHANGES`, docstrings, source comments, Markdown under `docs/`, and commit +messages — 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 libvcs'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…" | "`GitSync.update_repo` now accepts…" | +| "New and improved" | "`Git.fetch` 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 writes Python and works with repositories through libvcs's +objects — `GitURL`, `Git`, `GitSync`, and their hg and svn counterparts. They +are fluent in their version control system — clones, remotes, branches, +revisions, checkouts — and comfortable in Python, but you cannot assume they +know libvcs's internals: `QueryList` filtering, the subprocess wrapper under +`_internal`, the URL rule registry, or the pytest plugin's fixture machinery. + +A second, smaller reader works *on* libvcs or against its lower layers: custom +URL rules, sync subclasses, tools built on top like vcspull, or contributing. +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 parse the URL", not "The URL + is parsed". Address the reader who is doing the thing. +- **Concept before API surface.** Open by saying what the object or method + *is* and what it does for the reader. The signature — the parameters, the + flags — is the last detail they need, not the first. A page that opens with + a method signature has buried the idea under its mechanics. +- **Say when they can stop.** Lead with the default and the reassurance: most + readers never reach for the advanced parts, the defaults work. 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 common + call, then the one argument a few will tune, then the lower-level + primitive — running the VCS binary directly via `run()` — last. Each step + is for a smaller audience than the last. +- **Lean on the layers.** The reader thinks in libvcs's three-module split: + `libvcs.url` detects and parses, `libvcs.cmd` wraps the git, hg, and svn + binaries, and `libvcs.sync` manages whole checkouts on top of `cmd`. + Reinforce that split when explaining where a feature lives or which layer + the reader should reach for. +- **Name the trade-off.** If a call costs something — a fresh subprocess per + command, a network round-trip on `obtain()` — say so, and say what it buys + ("never stale, but each call pays the spawn"). State it; do not sell it. +- **Frame by concept, not by mechanism.** Do not headline a feature by its + git flag or matcher pattern in prose; that names the implementation + surface, which is the reader's last concern. Name the concept. The + mechanics vocabulary — a parameter table, a `--force` flag, a regex + pattern — belongs in a reference table or the API docs, and only there. + +## 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 libvcs apart from the neighbouring +package. + +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. + +Examples are executable, not illustrative fiction. Never +`your-command `. See +[Documented examples that run](#documented-examples-that-run) for which +blocks are executed and how to write one that qualifies. + +Document the semantic model, not the flag list. Say what a sync call returns, +what an `obtain()` does on a repository that already exists, and what a +failed sync looks like — that is what a signature cannot say. + +State defaults explicitly — defaults are API. State negative guarantees +where they exist: "does not modify your configuration file", "no network +access", "never writes outside the destination". 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. + +## Documented examples that run + +Examples in libvcs are tests. This section is the contract for writing one +the test suite can actually see, and it describes libvcs's real mechanism — +read it before touching any fenced code block. + +**A fence tag is cosmetic. Only a `>>> ` prompt executes.** A block written as + + ```python + server = Git(path=".") + ``` + +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: + + ```python + >>> server = Git(path=".") + ``` + +This is the single most expensive mistake available when editing +documentation, because removing the prompts leaves a green test suite and a +silently deleted test. When editing a file that contains examples, count the +prompts before and after. + +**The fence tag is `python`.** Not `pycon`, not bare. + +**Where examples run.** `pyproject.toml`'s `[tool.pytest.ini_options]` sets +`addopts = ["--doctest-docutils-modules", "-p no:doctest", ...]` and +`testpaths = ["src/libvcs", "tests", "docs", "README.md"]`. That combination +means: + +- Every docstring `Examples` block under `src/libvcs/` runs. +- Every `>>> ` block in a Markdown or reStructuredText file under `docs/` + runs, because `--doctest-docutils-modules` collects docutils sources, not + only Python modules. +- `README.md` is itself in `testpaths`, so a `>>> ` block there would run + too — the current README has none; if you add one, it becomes a test. +- `doctest_optionflags = ["ELLIPSIS", "NORMALIZE_WHITESPACE"]` is set + globally, so `...` elides variable output and whitespace differences do + not fail a comparison. Reach for an inline `# doctest: +FLAG` only for the + block that needs something beyond that. + +**Fixtures available inside a doctest.** The root `conftest.py` requests +`add_doctest_fixtures` for every collected doctest item, and +`src/libvcs/pytest_plugin.py` populates `doctest_namespace` from it. A block +may use these names without importing or constructing them: + +- `tmp_path` — always available. +- `example_git_repo` and `create_git_remote_repo` (plus + `create_git_remote_repo_bare`) — only when `git` is on `PATH`. +- `create_svn_remote_repo` (plus `create_svn_remote_repo_bare`) — only when + both `svn` and `svnadmin` are on `PATH`. +- `create_hg_remote_repo` (plus `create_hg_remote_repo_bare`) — only when + `hg` is on `PATH`. + +Each VCS's helpers appear only when its binary is present, so an example +using `create_hg_remote_repo` is silently absent from the namespace on a +machine without Mercurial rather than failing every other VCS's examples. +Write one example per VCS instead of branching inside a block. The full +fixture reference — including the non-doctest fixtures like `git_repo`, +`svn_repo`, and `hg_repo` — is at +[the pytest plugin API page](https://libvcs.git-pull.com/api/pytest-plugin/). + +**`# doctest: +SKIP` is not permitted.** It is a workaround that tests +nothing. If a VCS binary might be missing, the fixtures above already handle +it — an example that needs `hg` and finds `create_hg_remote_repo` absent from +the namespace fails loudly, which is the signal to gate the example on the +right fixture, not to skip it. + +**Do not downgrade a doctest to a non-executed block to make it pass.** A +`` ```{eval-rst}`` block, a `.. code-block::`, or an unprompted fence does not +run. If an example cannot pass, fix the example or fix the code. + +**Docstring examples** use the NumPy `Examples` section: + + Examples + -------- + >>> git = Git(path=tmp_path) + >>> git.get_git_version() # doctest: +ELLIPSIS + '...' + +**Room to grow.** The docutils collector reads `.md` and `.rst` whenever it +is loaded, which is everywhere in this repository. A prompted block added to +a documentation page is executed from that moment with no configuration +change. Other formats the collector supports — the MyST `{doctest}` +directive and the reStructuredText `.. doctest::` directive — are available +if a case ever needs an explicitly marked block. + +## MyST roles and cross-references + +Any class, method, function, exception, or attribute that has its own +rendered API page must be cited with the matching role — `{class}`, `{meth}`, +`{func}`, `{exc}`, `{attr}` — never with plain backticks. A documentation +page without an explicit ref label uses `{doc}`; an anchor inside a page uses +`{ref}`. Plain backticks are correct for code syntax, environment variables, +parameter names, and file paths that are not doc pages — anything without an +autodoc destination. + +A `{ref}` target must match its anchor exactly — anchors mix underscore and +hyphen forms across pages (`pytest_plugin`, `url-parsing`). + +Link the first prose mention of any symbol that has a useful destination on +that page. Use the most specific target available. 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, a +grid-card teaser, or an introductory sentence, link that occurrence or +retitle the heading so the first prose mention can carry the link. Leave +code blocks and literal configuration values as code; link the surrounding +prose instead. + +`just build-docs` catches a broken cross-reference; the doctests do not — so +build the docs before committing a page that adds or moves one. + +## What stays precise + +Warm the framing, never the facts. Resolution-order lists, value tables, +exact error strings, matcher patterns, and class or method 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. + +`docs/topics/traversing_git.md` is the worked example: a concept-first intro +that says what Managers and Commands *are* before any signature, a runnable +example first, sections ordered by shrinking audience, and the reference +tables left exact with `{class}` cross-references. Read it before reshaping +another page. + +## The changelog + +`CHANGES` is the changelog, rendered as the Sphinx changelog page. 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 +`## libvcs X.Y.Z (YYYY-MM-DD)`. The file opens with a +`## libvcs X.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 ("libvcs X.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 +pull requests.** The unreleased entry carries no lead paragraph and no +version summary: sections only. 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. 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 pull request 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 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 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 a 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. Private symbols and internal jargon +(leading-underscore identifiers, algorithm names exposed for the first +time). Walls of text dressed up as bullets. Breaking changes buried mid-entry +instead of given their own subheading at the top. + +## Docstrings + +The prime directive: never restate the type. The annotation is the source of +truth; the docstring carries what the annotation cannot. + +All public functions and methods use **NumPy-style** docstrings, enforced by +`ruff`'s `pydocstyle` (`convention = "numpy"`): + +```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 VCSLocation(t.NamedTuple): + """Generic VCS Location (URL and optional revision). + + Attributes + ---------- + url : str + Repository URL, with any revision suffix stripped. + rev : str | None + Revision to check out, or ``None`` when unspecified. + """ +``` + +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. + +Document instead the dimensions the type system cannot encode: mutation, +ownership, ordering, timing, failure, idempotence, concurrency, units and +ranges, boundary behaviour, platform differences, and security boundary +(what is executed versus 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. Do not repeat an introspectable +signature. + +`Parameters`, `Returns`, and `Attributes` entries are exempt from the loss +gate in [Source comments](#source-comments) below — see the documentation +exception there. + +## 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): + +```python +# There are 321 tests to complete for servers. +``` + +Good (Keep): + +```python +# `SvnSync.url_rev` scrapes `svn info --xml`, so whether it yields a URL +# depends on the installed svn and the working copy layout. +``` + +### Documentation exception + +Doctests, minimal usage examples, and `Parameters`, `Returns`, 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. NumPy-style `Parameters`, `Returns`, and `Attributes` sections +fall under this exception for the same reason autodoc ships every field +whether or not you describe it, and a doctest that runs is also a test. + +## Terminology and capitalization + +Pick the domain noun and keep it. If the code calls something a remote, do +not call it an origin in one paragraph and a mirror in the next. If the +method is `obtain()`, write "obtain" everywhere rather than alternating with +"clone", "fetch", and "sync" — those are separate operations elsewhere in +the API. + +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 + +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. + +## 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. + +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`: + +``` +py(deps[dev]) Bump dev packages +ai(rules[AGENTS]) Judge comments by three gates +``` + +Everything that changes behaviour keeps the colon. + +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[rules])**: Claude Code rules (`CLAUDE.md`) +- **ai(claude[command])**: Claude Code command changes + +Example: + +``` +url/git(feat[GitURL]): Add support for custom SSH port syntax + +why: Enable parsing of Git URLs with custom SSH ports + +what: +- Add port capture to SCP_REGEX pattern +- Update GitURL.to_url() to include port if specified +- 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 or test + counts, dated "as of" claims, bare SHAs, or local absolute paths — unless + they are strict evidentiary artefacts such as a benchmark log. +- **Diff narration.** Do not restate what moved, was renamed, or was + removed in anything the reader holds alongside the diff: code, docstrings, + README, `CHANGES`, or a pull request description. The diff and 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 in shipped text. + +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/v0.45.1/…`). 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 when there is no tag or the claim is about unreleased code. + Never a pull-request-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. + +### 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 behaviour, or bug? If the answer is no, +it is branch-internal narrative — it belongs in the commit message, not the +artefact. + +Keep in shipped artefacts: 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 +that make sense to a reader who never saw the previous version. diff --git a/AGENTS.md b/AGENTS.md index 1a0fde67..a7249019 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,728 +1,67 @@ # 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 - -libvcs is a lite, typed Python tool for: -- Detecting and parsing URLs for Git, Mercurial, and Subversion repositories -- Providing command abstractions for git, hg, and svn -- Synchronizing repositories locally -- Creating pytest fixtures for testing with temporary repositories - -The library powers [vcspull](https://www.github.com/vcs-python/vcspull/), a tool for managing and synchronizing multiple git, svn, and mercurial repositories. - -## Development Environment - -This project uses: -- Python 3.9+ -- [uv](https://github.com/astral-sh/uv) for dependency management -- [ruff](https://github.com/astral-sh/ruff) for linting and formatting -- [mypy](https://github.com/python/mypy) for type checking -- [pytest](https://docs.pytest.org/) for testing - -## Common Commands - -### Setting Up Environment - -```bash -# Install dependencies -uv pip install --editable . -uv pip sync - -# Install with development dependencies -uv pip install --editable . -G dev -``` - -### Running Tests - -```bash -# Run all tests -just test -# or directly with pytest -uv run pytest - -# Run a single test file -uv run pytest tests/sync/test_git.py - -# Run a specific test -uv run pytest tests/sync/test_git.py::test_remotes - -# Run tests with test watcher -just start -# or -uv run ptw . -``` - -### Linting and Type Checking - -```bash -# Run ruff for linting -just ruff -# or directly -uv run ruff check . - -# Format code with ruff -just ruff-format -# or directly -uv run ruff format . - -# Run ruff linting with auto-fixes -uv run ruff check . --fix --show-fixes - -# Run mypy for type checking -just mypy -# or directly -uv run mypy src tests - -# Watch mode for linting (using entr) -just watch-ruff -just watch-mypy -``` - -### Development Workflow - -Follow this workflow for code changes: - -1. **Format First**: `uv run ruff format .` -2. **Run Tests**: `uv run pytest` -3. **Run Linting**: `uv run ruff check . --fix --show-fixes` -4. **Check Types**: `uv run mypy` -5. **Verify Tests Again**: `uv run pytest` - -### Documentation - -```bash -# Build documentation -just build-docs - -# Start documentation server with auto-reload -just start-docs - -# Update documentation CSS/JS -just design-docs -``` - -## Code Architecture - -libvcs is organized into three main modules: - -1. **URL Detection and Parsing** (`libvcs.url`) - - Base URL classes in `url/base.py` - - VCS-specific implementations in `url/git.py`, `url/hg.py`, and `url/svn.py` - - URL registry in `url/registry.py` - - Constants in `url/constants.py` - -2. **Command Abstraction** (`libvcs.cmd`) - - Command classes for git, hg, and svn in `cmd/git.py`, `cmd/hg.py`, and `cmd/svn.py` - - Built on top of Python's subprocess module (via `_internal/subprocess.py`) - -3. **Repository Synchronization** (`libvcs.sync`) - - Base sync classes in `sync/base.py` - - VCS-specific sync implementations in `sync/git.py`, `sync/hg.py`, and `sync/svn.py` - -4. **Internal Utilities** (`libvcs._internal`) - - Subprocess wrappers in `_internal/subprocess.py` - - Data structures in `_internal/dataclasses.py` and `_internal/query_list.py` - - Runtime helpers in `_internal/run.py` and `_internal/shortcuts.py` - -5. **pytest Plugin** (`libvcs.pytest_plugin`) - - Provides fixtures for creating temporary repositories for testing - -## Testing Strategy - -libvcs uses pytest for testing with many custom fixtures. The pytest plugin (`pytest_plugin.py`) defines fixtures for creating temporary repositories for testing. These include: - -- `create_git_remote_repo`: Creates a git repository for testing -- `create_hg_remote_repo`: Creates a Mercurial repository for testing -- `create_svn_remote_repo`: Creates a Subversion repository for testing -- `git_repo`, `svn_repo`, `hg_repo`: Pre-made repository instances -- `set_home`, `vcs_gitconfig`, `vcs_hgconfig`, `git_commit_envvars`: Environment fixtures - -These fixtures handle setup and teardown automatically, creating isolated test environments. - -For running tests with actual VCS commands, tests will be skipped if the corresponding VCS binary is not installed. - -### Testing Guidelines - -1. **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. - -### Example Fixture Usage - -```python -def test_repo_sync(git_repo): - # git_repo is already a GitSync instance with a clean repository - # Use it directly in your tests - assert git_repo.get_revision() == "initial" -``` - -### Parameterized Tests - -Use `typing.NamedTuple` for parameterized tests: - -```python -class RepoFixture(t.NamedTuple): - test_id: str # For test naming - repo_args: dict[str, t.Any] - expected_result: str - -@pytest.mark.parametrize( - list(RepoFixture._fields), - REPO_FIXTURES, - ids=[test.test_id for test in REPO_FIXTURES], -) -def test_sync( - # Parameters and fixtures... -): - # Test implementation -``` - -## 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. -- 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 VCSLocation(t.NamedTuple): - """Generic VCS Location (URL and optional revision). - - Attributes - ---------- - url : str - Repository URL, with any revision suffix stripped. - rev : str | None - Revision to check out, or ``None`` when unspecified. - """ -``` - -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 `asyncio.run()` or similar calls -- 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: `tmp_path`, `asyncio`, `create_git_remote_repo`, `create_hg_remote_repo`, `create_svn_remote_repo`, `example_git_repo` -- Ellipsis for variable output: `# doctest: +ELLIPSIS` -- Update `pytest_plugin.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. - -**Async doctest pattern:** -```python ->>> async def example(): -... result = await some_async_function() -... return result ->>> asyncio.run(example()) -'expected output' -``` - -**Using fixtures in doctests:** -```python ->>> git = Git(path=tmp_path) # tmp_path from doctest_namespace ->>> git.run(['status']) -'...' -``` - -**When output varies, use ellipsis:** -```python ->>> git.clone(url=f'file://{create_git_remote_repo()}') # doctest: +ELLIPSIS -'Cloning into ...' -``` - -### 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 | - -**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="libvcs.cmd")` -- 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`) - -### 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. - -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 - -Example: -``` -url/git(feat[GitURL]): Add support for custom SSH port syntax - -why: Enable parsing of Git URLs with custom SSH ports - -what: -- Add port capture to SCP_REGEX pattern -- Update GitURL.to_url() to include port if specified -- Add tests for the new functionality -``` -#### 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 -)" -``` - -## 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. - -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. - -**Release entry boilerplate.** Every release header is `## libvcs X.Y.Z (YYYY-MM-DD)`. The file opens with a `## libvcs X.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 (*"libvcs X.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}\`libvcs.cmd.git.Git\``), 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. +libvcs is a typed Python library that detects and parses Git, Mercurial, and +Subversion URLs, wraps their command-line tools, and synchronizes local +checkouts against a remote — plus a pytest plugin for testing against real, +disposable VCS repositories. It powers +[vcspull](https://github.com/vcs-python/vcspull). + +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/libvcs/url/` | URL detection and parsing for git, hg, svn; rule registry | +| `src/libvcs/cmd/` | Typed wrappers around the `git`, `hg`, `svn` binaries | +| `src/libvcs/sync/` | Repository sync (clone, update, obtain) built on `cmd/` | +| `src/libvcs/_internal/` | Subprocess runner, dataclasses, `QueryList`; no compatibility guarantee | +| `src/libvcs/pytest_plugin.py` | `pytest11` plugin: fixtures for disposable git/hg/svn repos | +| `tests/` | Test suite, laid out to mirror `src/libvcs/` | +| `docs/` | Sphinx (MyST) site; build with `just build-docs` | +| `CHANGES` | Changelog; rendered as the docs changelog page | +| `README.md` | Project overview; listed in `testpaths` (see below) | +| `justfile` | Task runner for tests, lint, mypy, and docs | + +## 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 or pytest fixtures. +- 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. + +## Domain facts + +- `pyproject.toml` runs `--doctest-docutils-modules` over + `testpaths = ["src/libvcs", "tests", "docs", "README.md"]`. A `>>> ` + prompt anywhere under those paths is a collected, executing test; deleting + the prompt deletes the test even if the surrounding prose survives. +- A test that shells out to `git`, `hg`, or `svn` skips automatically when + that binary is not on `PATH` — see `src/libvcs/pytest_plugin.py`. +- libvcs is pre-1.0: a minor version bump may break the public API. See + [Releasing](.github/CONTRIBUTING.md#releasing). + +## References + +- Changelog: [`CHANGES`](CHANGES) +- Docs: +- Source: +- Downstream consumer: [vcspull](https://github.com/vcs-python/vcspull) diff --git a/README.md b/README.md index 00cfd238..402afbfd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
libvcs logo

libvcs

-

The Swiss Army Knife for Version Control Systems in Python.

+

A typed Python interface for Git, Mercurial, and Subversion repositories.

PyPI version Python versions @@ -11,18 +11,29 @@

-**libvcs** provides a unified, [typed](https://docs.python.org/3/library/typing.html), and pythonic interface for managing Git, Mercurial, and Subversion repositories. Whether you're building a deployment tool, a developer utility, or just need to clone a repo in a script, libvcs handles the heavy lifting. +**libvcs** parses and validates Git, Mercurial, and Subversion URLs, wraps +each VCS's command-line tool in a [typed](https://docs.python.org/3/library/typing.html) +Python object, and synchronizes a local checkout against a remote — +cloning it if it does not exist, fetching and updating it if it does. It +also ships a pytest plugin for creating disposable repositories in your own +test suite. -It powers [vcspull](https://github.com/vcs-python/vcspull) and simplifies VCS interactions down to a few lines of code. +It powers [vcspull](https://github.com/vcs-python/vcspull), which uses it to +sync many repositories from a single config file. --- ## Features at a Glance -- **🔄 Repository Synchronization**: Clone, update, and manage local repository copies with a high-level API. -- **🛠 Command Abstraction**: Speak fluent `git`, `hg`, and `svn` through fully-typed Python objects. -- **🔗 URL Parsing**: Robustly validate, parse, and manipulate VCS URLs (including SCP-style). -- **🧪 Pytest Fixtures**: Batteries-included fixtures for spinning up temporary repositories in your test suite. +- **Repository synchronization**: One `obtain()` / `update_repo()` call + clones a repository if it is missing and fetches it if it already exists, + the same way for git, hg, and svn. +- **Command abstraction**: Call `git`, `hg`, and `svn` through typed Python + objects instead of shelling out and parsing text yourself. +- **URL parsing**: Parse, validate, and transform VCS URLs, including + SCP-style `git@host:path` remotes. +- **Pytest fixtures**: Create disposable local git, hg, and svn repositories + for your own tests, with per-test isolation. ## Installation @@ -42,17 +53,22 @@ Try it interactively: $ uvx --with libvcs ipython ``` -Tip: libvcs is pre-1.0. Pin a version range in projects to avoid surprises: +libvcs is pre-1.0: a minor version bump (0.45 to 0.46) may change the public +API. Pin a version range in projects to avoid surprises: ```toml # pyproject.toml -dependencies = ["libvcs>=0.37,<0.38"] +dependencies = ["libvcs>=0.45,<0.46"] ``` ## Usage ### 1. Synchronize Repositories -Clone and update repositories with a consistent API, regardless of the VCS. + +`GitSync`, `HgSync`, and `SvnSync` give the same two calls regardless of the +underlying VCS: `obtain()` clones if the path does not exist yet, and +`update_repo()` does that or fetches and updates an existing checkout — call +it either way and let libvcs decide. [**Learn more about Synchronization**](https://libvcs.git-pull.com/sync/) @@ -78,7 +94,11 @@ else: ``` ### 2. Command Abstraction -Traverse repository entities intuitively with ORM-like filtering, then run targeted commands against them. + +`Git`, `Hg`, and `Svn` wrap the binary directly — each call maps to one +subprocess invocation of the real VCS tool, so there is no divergent +reimplementation to trust. Branches, remotes, and tags are also reachable +through `QueryList`, which filters like a Django ORM queryset. [**Learn more about Command Abstraction**](https://libvcs.git-pull.com/cmd/) @@ -103,7 +123,10 @@ git.tags.create(name="v1.0.0", message="Release version 1.0.0") ``` ### 3. URL Parsing -Stop writing regex for Git URLs. Let `libvcs` handle the edge cases. + +`GitURL`, `HgURL`, and `SvnURL` parse and validate VCS URLs — including +SCP-style git remotes — without hand-written regular expressions, and let +you rewrite a parsed URL's parts back into a valid URL string. [**Learn more about URL Parsing**](https://libvcs.git-pull.com/url/) @@ -126,7 +149,10 @@ print(url.to_url()) # 'git@gitlab.com:vcs-python/libvcs.git' ``` ### 4. Testing with Pytest -Writing a tool that interacts with VCS? Use our fixtures to keep your tests clean and isolated. + +The bundled pytest plugin builds a real, temporary VCS repository per test +and tears it down after — no network access, no shared state between tests. +A VCS's fixtures are only available when its binary is installed. [**Learn more about Pytest Fixtures**](https://libvcs.git-pull.com/pytest-plugin.html) @@ -167,4 +193,4 @@ def test_my_git_tool(create_git_remote_repo: CreateRepoFn, tmp_path: pathlib.Pat Your donations fund development of new features, testing, and support. -- [Donation Options](https://tony.sh/support.html) \ No newline at end of file +- [Donation Options](https://tony.sh/support.html) diff --git a/docs/AGENTS.md b/docs/AGENTS.md deleted file mode 100644 index 23245d8f..00000000 --- a/docs/AGENTS.md +++ /dev/null @@ -1,142 +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 API surface. It complements -the repository-root `AGENTS.md`, which already governs code blocks, -shell-command formatting, doctests, 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 writes Python and works with repositories through -libvcs's objects — `GitURL`, `Git`, `GitSync`, and their hg and svn -counterparts. They are fluent in their version control system — -clones, remotes, branches, revisions, checkouts — and comfortable in -Python, but you cannot assume they know libvcs's internals: -`QueryList` filtering, the subprocess wrapper under `_internal`, the -URL rule registry, or the pytest plugin's fixture machinery. - -A second, smaller reader works *on* libvcs or against its lower -layers: custom URL rules, sync subclasses, tools built on top like -vcspull, or contributing. 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 parse the URL", not - "The URL is parsed". Address the reader who is doing the thing. -- **Concept before API surface.** Open by saying what the object or - method *is* and what it does for the reader. The signature — the - parameters, the flags — is the last detail they need, not the - first. A page that opens with a method signature has buried the - idea under its mechanics. -- **Say when they can stop.** Lead with the default and the - reassurance: most readers never reach for this, the defaults work, - the advanced parts are optional. Let a skimmer leave after one paragraph. -- **Grant permission, don't demand attention.** "Reach for this - when…", "for the rarer cases" — tell readers they're in the right - place without implying they must read on. -- **Progressive disclosure.** Order by how many readers need it: the - common call → the one argument a few will tune → the lower-level - primitive → running the VCS binary directly via `run()`. Each step - is for a smaller audience than the last. -- **Lean on the layers.** The reader thinks in libvcs's three-module - split: `libvcs.url` detects and parses, `libvcs.cmd` wraps the git, - hg, and svn binaries, and `libvcs.sync` manages whole checkouts on - top of `cmd`. Reinforce that split when you explain where a feature - lives or which layer the reader should reach for. -- **Name the trade-off.** If a call costs something — a fresh - subprocess per command, a network round-trip on `obtain()` — say - so, and say what it buys ("never stale, but each call pays the - spawn"). State it; don't sell it. -- **Frame by concept, not by mechanism.** Don't headline a feature by - its git flag or matcher pattern in prose; that names the - implementation surface, which is the reader's last concern. Name - the concept. The mechanics vocabulary — a parameter table, a - `--force` flag, a regex pattern — belongs in a reference table or - the API docs, and only there. - -## Examples that run - -Prose examples under `docs/` are doctests, and the root `AGENTS.md` -requires them to actually execute — `testpaths` includes `docs/` (and -`README.md`), so pytest runs every fenced `>>>` block. Lead with a -small, runnable example early rather than after paragraphs of prose; -libvcs is code-first. - -- Use the `doctest_namespace` fixtures — `tmp_path`, - `example_git_repo`, `create_git_remote_repo`, - `create_hg_remote_repo`, `create_svn_remote_repo` (each with a - `_bare` variant) — instead of building repositories by hand. A - VCS's fixtures only appear when its binary is installed. -- Fence a `>>>` session as a ```` ```python ```` block, and reach for - `# doctest: +ELLIPSIS` when output varies (clone messages, tmp - paths). Use a ```` ```console ```` block for shell commands at a - `$` prompt. -- Keep each code block self-contained — re-import and re-create - objects (`git = Git(path=example_git_repo.path)`) rather than rely - on state from an earlier block; every existing page does. - -## What stays precise - -Warm the framing, never the facts. Resolution-order lists, value -tables, exact error strings, matcher patterns, and class or method -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. - -## 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 ("write your own URL rule", "run git directly") — 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 — -anchors mix underscore and hyphen forms across pages -(`pytest_plugin`, `url-parsing`). `just build-docs` catches a broken -cross-reference; the doctests do not — 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, libvcs APIs, -topic 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/topics/traversing_git.md` is the worked example: a concept-first -intro that says what Managers and Commands *are* before any signature, -the manager tree laid out up front, a runnable example first, sections -ordered by shrinking audience, an honest before/after against parsing -raw `git` output, and the "When to Use" and manager reference tables -left exact, with `{class}` cross-references. Read it before reshaping -another page. - -## Before you commit - -- Does the page open with what the feature *is*, or how to call it? -- Can a reader who needs only the common case stop after the first - paragraph? -- Is anything framed by its git flag or matcher pattern that should - be named by concept instead? -- Are the advanced and lower-level parts clearly marked opt-in? -- Do the doctests run (`just test`), and did you leave every code - block, table, error string, and cross-reference exact? -- 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 47dc3e3d..00000000 --- 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 03bf9fee..3904fcc3 100644 --- a/docs/project/code-style.md +++ b/docs/project/code-style.md @@ -2,36 +2,11 @@ # Code Style -Use this page when you are changing Python code or docstrings and need the -project's formatting, typing, and import conventions. The command examples are -the common local checks; the root contributor guide still owns the full -pre-commit gate. - -## Formatting and linting - -libvcs uses [ruff](https://ruff.rs) for formatting **and** linting in a -single tool. The full rule set is declared in `pyproject.toml` under -`[tool.ruff]`. - -```console -$ uv run ruff format . -``` - -```console -$ uv run ruff check . --fix --show-fixes -``` - -## Type checking - -[mypy](http://mypy-lang.org/) runs in strict mode: - -```console -$ uv run mypy . -``` - -## Docstrings - -All public APIs use **NumPy-style** docstrings: +Formatting, typing, and import conventions moved to +[`.github/CONTRIBUTING.md`][contributing-file]; the NumPy docstring +convention moved to [`.github/WRITING.md`][writing-file]. This page keeps +the one runnable example that used to live here, because it is collected as +a test under `docs/` — moving it into `.github/` would stop it running. ```python >>> def fetch(url: str, *, branch: str | None = None) -> str: @@ -52,9 +27,5 @@ All public APIs use **NumPy-style** docstrings: ... return "abc123" ``` -## Imports - -- `from __future__ import annotations` at the top of every file. -- Standard-library modules use **namespace imports**: `import pathlib`, - not `from pathlib import Path`. -- Typing: `import typing as t`, then `t.Optional`, `t.Any`, etc. +[contributing-file]: https://github.com/vcs-python/libvcs/blob/master/.github/CONTRIBUTING.md +[writing-file]: https://github.com/vcs-python/libvcs/blob/master/.github/WRITING.md diff --git a/docs/project/contributing.md b/docs/project/contributing.md index dc8f7a31..d611a7f8 100644 --- a/docs/project/contributing.md +++ b/docs/project/contributing.md @@ -5,8 +5,14 @@ # Contributing As an open source project, libvcs accepts contributions through [GitHub]. +The contributor guide — environment setup, the gates, tests, releasing, and +pull requests — now lives at [`.github/CONTRIBUTING.md`][contributing-file] +in the repository; prose conventions live at +[`.github/WRITING.md`][writing-file]. -Ready to dive in? See the {ref}`Development Workflow ` for -environment setup, running tests, linting, and building docs. +Ready to dive in? See the {ref}`Development Workflow ` page for a +day-to-day loop through the same commands. [GitHub]: https://github.com/vcs-python/libvcs +[contributing-file]: https://github.com/vcs-python/libvcs/blob/master/.github/CONTRIBUTING.md +[writing-file]: https://github.com/vcs-python/libvcs/blob/master/.github/WRITING.md From 35cdd84749d54198d59128bf495147b5560326b8 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Aug 2026 10:28:16 -0500 Subject: [PATCH 2/2] Docs(fix[README]): Point the pytest plugin link at its canonical page why: The link used the pre-move `pytest-plugin.html` path, which only resolves through a 301. Naming the current page directly removes a hop and survives the next redirect cleanup. what: - Link the pytest fixtures reference at /api/pytest-plugin/ --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 402afbfd..72d60424 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ The bundled pytest plugin builds a real, temporary VCS repository per test and tears it down after — no network access, no shared state between tests. A VCS's fixtures are only available when its binary is installed. -[**Learn more about Pytest Fixtures**](https://libvcs.git-pull.com/pytest-plugin.html) +[**Learn more about Pytest Fixtures**](https://libvcs.git-pull.com/api/pytest-plugin/) ```python import pathlib