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..72d60424 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,9 +149,12 @@ 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. -[**Learn more about Pytest Fixtures**](https://libvcs.git-pull.com/pytest-plugin.html) +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/api/pytest-plugin/) ```python import pathlib @@ -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