diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..8bf0cf8 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,167 @@ +# Contributing + +Thanks for looking. A bug report with a reproduction — the directory layout +and the exact `g` invocation — is the most useful thing you can send; a +correction to somewhere the documentation misled you is a close second. + +How this project writes prose — README, `CHANGES`, commit messages, +docstrings, source comments, and CLI/error text — is set out separately in +[WRITING.md](WRITING.md). Read that before changing any of it. The +constraints every change is held to, and the map of what is where, are in +[AGENTS.md](../AGENTS.md). + +## Getting set up + +Install [uv], then sync the dev and docs extras: + +```console +$ uv sync --all-extras --dev +``` + +[uv]: https://github.com/astral-sh/uv + +## The gates + +CI is the order of record; every gate it runs has to pass before a change is +done. + +Format: + +```console +$ uv run ruff format . +``` + +Lint: + +```console +$ uv run ruff check . --fix --show-fixes +``` + +Type-check (`[tool.mypy] strict = true`): + +```console +$ uv run mypy . +``` + +Test: + +```console +$ uv run py.test +``` + +Documentation is a gate, not a courtesy. Examples in docstrings under +`src/g` and pages under `docs/` are executed by `pytest`/`py.test` — the +doctest flags live in `pyproject.toml`, so there is no separate doctest +step and a green test run is the proof. `README.md` is not included, so its +examples are never executed. Which blocks qualify, and the one mistake that +silently removes a test, are in +[WRITING.md](WRITING.md#documented-examples-that-run). + +Ruff's isort config requires `from __future__ import annotations` in every +module (`required-imports`, backed by the `FA100` rule) — the linter, not +this file, catches a missing one. Import stdlib modules by namespace +(`import typing as t`, `import logging`) rather than `from typing import +…`; third-party packages may use `from X import Y`. Nothing enforces this +one — it is a convention, not a lint rule. + +Before claiming a test or a gate works, show it failing. A gate that has +never been red is an assumption. + +## Tests + +Tests live in `tests/test_cli.py`, parametrized through a +`CommandLineTestFixture` `NamedTuple`. The autouse `setup` fixture in +`conftest.py` sets `G_IS_TEST=1` for every test, which makes `run()` return +the `subprocess.Popen` object instead of `None` so assertions can inspect +it — outside tests, `run()` always returns `None`. + +CLI tests invoke real VCS binaries (at minimum `git`) rather than mocking +the subprocess call; use `tmp_path` and `monkeypatch` to simulate a +non-repo directory. + +`find_repo_type()` requires a `.git`, `.svn`, or `.hg` **directory**. A git +worktree checkout's top-level `.git` is a file, not a directory, so +`test_command_line[g-cmd-inside-git-dir]` and its `--help` sibling fail +there even though nothing is broken. Work from a normal clone (not `git +worktree add`) to run the full suite green. + +When subprocess output seems swallowed, set `G_IS_TEST=1` and call +`run(wait=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)` to capture +it for inspection. + +Assert on `caplog.records` attributes, not string matching on `caplog.text` +— `caplog.record_tuples` cannot see `extra` fields. Scope capture with +`caplog.at_level(logging.DEBUG, logger="g")` — `g`'s modules log through +`logging.getLogger(__name__)`, so the logger name is the module's own +dotted path, `g` at the package root. Filter records rather than index by +position: `[r for r in caplog.records if hasattr(r, "vcs_cmd")]`. + +## Documentation + +Build once: + +```console +$ make build_docs +``` + +Live-reloading preview at `http://localhost:8034`: + +```console +$ make start_docs +``` + +From inside `docs/`, the same tasks are `just html` and `just start`; `just +--list` in `docs/` shows the rest (`watch`, `serve`, `linkcheck`, +`doctest`). `make build_docs` and `just html` both wrap +`sphinx-build`; nothing under `docs/_build` is hand-edited. + +`AGENTS.md` and `CLAUDE.md` are excluded from the build +(`exclude_patterns` in `docs/conf.py`) — they are agent guidance, not site +pages. `make build_docs` catches a broken cross-reference; the test suite +does not, so build the docs before committing a documentation change. + +## Releasing + +Never create tags. Never push tags. The owner handles tagging and tag +pushes, because a tag triggers the publish workflow. See +[Release commits](WRITING.md#release-commits). + +1. Update `CHANGES` with the release notes. +2. Bump the version in `src/g/__about__.py` and `pyproject.toml`. +3. Commit the release files with the subject `Tag v`. +4. Tag (`git tag v`) and push the branch, then the tag + (`git push --tags`). +5. CI builds and publishes to PyPI automatically over OIDC trusted + publishing once the tag lands. + +Full detail: [docs/project/releasing.md](../docs/project/releasing.md). + +## 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). + +Merge once you have the sign-off of one other developer. If you do not have +permission to merge, ask a maintainer to merge it for you. + +## Decorum + +- Participants will be tolerant of opposing views. +- Participants must ensure that their language and actions are free of + personal attacks and disparaging personal remarks. +- When interpreting the words and actions of others, participants should + always assume good intentions. +- Behaviour which can be reasonably considered harassment will not be + tolerated. + +Based on [Ruby's Community Conduct Guideline](https://www.ruby-lang.org/en/conduct/). + +## Security + +This repository has no `SECURITY.md`. Please do not open a public issue for +a vulnerability — report it privately via [GitHub's security +advisories](https://github.com/vcs-python/g/security/advisories/new). diff --git a/.github/WRITING.md b/.github/WRITING.md new file mode 100644 index 0000000..730f88d --- /dev/null +++ b/.github/WRITING.md @@ -0,0 +1,739 @@ +# Writing + +How this project writes prose, for humans and agents alike. It governs +`README.md`, `CHANGES`, commit messages, CLI and error-message text, +docstrings, source comments, logging, and the Sphinx docs under `docs/` +— every surface a reader reaches. + +For environment setup, the gates, and pull request workflow, see +[CONTRIBUTING.md](CONTRIBUTING.md). + +## Voice + +Three surfaces, one voice. A docstring says what a caller may rely on; a +`CHANGES` entry says what changed; prose says what happens. All three are +present tense, lead with the thing being described, and stop. Why it was built +that way belongs in the commit message, which is timestamped and attached to +the diff. + +The most useful editing operation is deleting the introductory sentence. + +Lead with verbs and name concrete things. Put identifiers in backticks. Prefer +short declarative sentences, one operational fact each. Do not explain Python +to Python developers; do explain this project's semantics. + +Type annotations describe shape. Documentation describes meaning. A sentence +that restates a signature has said nothing. + +Use MUST, SHOULD, and MAY only where the normative sense is meant. Say what +actually happens rather than that something is "supported". + +| Instead of | Prefer | +| --------------------------------- | --------------------------------- | +| "We added…" | "`g` now proxies…" | +| "New and improved" | "`g` 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 works at a shell prompt inside a checkout and runs `g` +where they would otherwise type `git`, `svn`, or `hg`. They are fluent in +their own VCS — status, commit, log, diff — but you cannot assume they read +Python or know g's internals: `find_repo_type()`, the `vcspath_registry` +marker mapping, or the `G_IS_TEST` escape hatch the tests use. + +A second, smaller reader writes Python: they call `run()` directly, build on +`create_parser()`, or contribute to g itself. Serve them too, but mark their +material opt-in — "for the rarer cases", "internal" — 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 run `g status`", not "the + command is proxied". Address the reader who is doing the thing. +- **Concept before mechanics.** Open by saying what g *does* for the reader — + one command that becomes the right VCS command. The generated option + listing, the marker table, and the exit behaviour are the last details they + need, not the first. A page that opens with argparse output has buried the + idea under its plumbing. +- **Say when they can stop.** g has no configuration; most pages owe the + reader one reassurance up front — run `g` where you'd run your VCS, and + you're done. Let a skimmer leave after one sentence. +- **Grant permission, do not demand attention.** "Reach for this when…", "for + the rarer cases" — tell readers they are in the right place without + implying they must read on. +- **Progressive disclosure.** Order by how many readers need it: the everyday + command, then the one flag g intercepts (`--version`/`-V`), then how + detection works, then the Python entry points. Each step is for a smaller + audience than the last. +- **Lean on the pipeline.** The reader's mental model is a straight line: + current directory → walk up the parents → a `.git`, `.svn`, or `.hg` + marker → the matching VCS command, with arguments forwarded. Reinforce + that chain whenever explaining detection or dispatch; it is the whole + tool. +- **Name the trade-off.** If a call costs something, say so and say what it + buys — `--version`/`-V` never reaches the underlying VCS, and g's own exit + status does not mirror the wrapped command's (see + [CLI and error messages](#cli-and-error-messages)). State it; do not sell + it. +- **Frame by concept, not by mechanism.** Do not headline a feature by its + directory marker or argparse detail; name the concept — detection, + forwarding. The mechanics vocabulary — the marker-to-command table, the + generated option reference — belongs in the CLI reference, 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 this package apart from the +neighbouring one. + +Get to a runnable command or snippet before anything the reader can skip. A +logo, a mission statement, a comparison matrix and three paragraphs of +history in front of the install line all cost the same thing. + +State the minimum Python version in prose, not only in badges. +`requires-python` in `pyproject.toml` is the authority; the README must +agree with it. + +Document the semantic model, not the flag list. `--help` already enumerates +flags (or, here, forwards straight to the VCS's own `--help` — see +[CLI and error messages](#cli-and-error-messages)); what it cannot say is +what goes to stdout versus stderr and what a non-zero exit means. + +State defaults explicitly — defaults are API. State negative guarantees +where they exist: g has no configuration file, makes no network calls of its +own, and never invokes anything but the VCS binary the marker maps to. They +establish boundaries faster than any amount of description. + +Headings stay conventional and stable, because people deep-link them. Badges +are few and load-bearing. + +## CLI and error messages + +g's whole surface is one command, so its error-message and exit-status +contract carries more weight than its flag list. This section is precise on +purpose; verify a claim against the source before repeating it elsewhere. + +**Only `--version`/`-V` is intercepted.** g checks whether the first argument +is `--version` or `-V` before doing anything else; if so, it prints +`g ` to stdout and exits `0`. Every other argument — including +`-h`/`--help` — is forwarded to the detected VCS untouched. `g --help` runs +`git --help` (or `svn`/`hg`), not g's own help text. + +**No detection, no error.** Outside any `.git`, `.svn`, or `.hg` directory, g +writes `No VCS found in current directory.` to stderr through the `logging` +module and returns — this is not treated as a failure. Keep that message +string exact; docs and tests refer to it literally. + +**Output is inherited, not reshaped.** g does not capture, filter, or +reformat the VCS's own stdout or stderr. The subprocess inherits the +parent's file descriptors, so a proxied command prints exactly what the VCS +itself would print, byte for byte. + +**g's own exit status does not mirror the wrapped command's.** After a +proxied VCS command completes, `run()` returns `None` and the process exits +`0` regardless of whether the VCS itself failed — a failing `git` command +still leaves `g` reporting success to the shell. If the mapped binary is +missing entirely (a `.hg` marker with no `hg` on `PATH`), `subprocess.Popen` +raises `FileNotFoundError`, an unhandled traceback prints to stderr, and the +process exits `1`. State this behaviour plainly wherever it comes up; do not +imply it is a bug being fixed or a feature being sold. + +## Logging + +g logs through the standard `logging` module rather than `print()`; log +output is a surface downstream tooling can parse, so treat its shape like +any other documented output. + +### Structured context via `extra` + +Pass structured data on a log call whenever it helps 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_stderr` | `list[str]` | VCS stderr lines (same caveats) | + +Treat these keys as compatibility-sensitive — downstream users may build +dashboards and alerts on them. Change them deliberately: `snake_case`, not +dotted, `vcs_` prefix, scalars over ad-hoc objects. + +### Lazy formatting + +`logger.debug("msg %s", val)`, not an f-string. Deferred interpolation is +skipped entirely when the level is filtered, and a `"Running %s"` template +groups as one signature in an aggregator instead of one unique line per call. +Guard an expensive `val` with `if logger.isEnabledFor(logging.DEBUG)`. + +### stacklevel and persistent context + +Increment `stacklevel` for each wrapper layer so `%(filename)s:%(lineno)d` +points at the real caller; verify whenever call depth changes. For an object +with stable identity, prefer a `LoggerAdapter` (override `process()` to +merge `extra`) over repeating the same `extra` on every call. + +### 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 a surprising or +user-actionable config issue goes to `WARNING`. + +### Message style + +Lowercase, past tense for events — `"repository cloned"`, `"vcs command +failed"` — no trailing punctuation. Keep the message short; put detail in +`extra`, not the string. + +### Exception logging + +Use `logger.exception()` only inside an `except` block you are not +re-raising from. Use `logger.error(..., exc_info=True)` when you need the +traceback outside an `except` block. Avoid `logger.exception()` followed by +`raise` — it duplicates the traceback; add context via `extra` instead, or +let the exception propagate. + +### Avoid + +f-strings or `.format()` in log calls; unguarded logging in hot loops; +catch-log-reraise with no new context; `print()` for diagnostics; logging a +secret env var's value (log the key name only); non-scalar ad-hoc objects in +`extra`; a custom `extra` field referenced in a format string with no safe +default (a missing key raises `KeyError`). + +## Documented examples that run + +Examples in this project are tests. This section is the contract for +writing one the test suite can actually see, and it states this repo's real +mechanism — read `[tool.pytest.ini_options]` in `pyproject.toml` before +assuming otherwise. + +**A fence tag is cosmetic. Only a `>>> ` prompt executes.** A block written +as + + ```python + parser = create_parser() + ``` + +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 + >>> parser = create_parser() + ``` + +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, precisely.** `pyproject.toml` sets +`addopts = "--doctest-modules …"`, `doctest_optionflags = "ELLIPSIS +NORMALIZE_WHITESPACE"`, and `testpaths = ["src/g", "tests", "docs"]`. +Docstring examples under `src/g` run as part of every `pytest`/`py.test` +invocation. Markdown under `docs/` is also collected and executed — this was +verified directly: a `>>> ` prompt added to a page under `docs/` is +collected as its own test item and fails on a wrong expected value, exactly +like a docstring doctest. **`README.md` is not in `testpaths`.** A `>>> ` +prompt there is never executed — there are none in the current README, and +adding one would silently do nothing rather than add a test, so do not +promise a reader that a README example is checked. + +**No shared fixtures outside `src/g`.** This repository defines no +`doctest_namespace` fixture. A module doctest under `src/g` runs with that +module's own globals, so a name already imported or defined at module level +(`pathlib`, `create_parser`, `find_repo_type`, …) is available without +importing it again inside the block — see `create_parser`'s own docstring +for a working example. A prompted block anywhere else (`docs/`, if one is +ever added) starts bare: import or define every name the block uses. + +**`# doctest: +SKIP` is not permitted.** It is a workaround that tests +nothing. + +**Do not downgrade a doctest to a non-executed block to make it pass.** A +`.. code-block::` or an unprompted fence does not run. If an example cannot +pass, fix the example or fix the code. + +**Option flags.** `ELLIPSIS` and `NORMALIZE_WHITESPACE` are enabled +globally, so `...` elides variable output and whitespace differences do not +fail a comparison. Reach for an inline `# doctest: +FLAG` only for the block +that needs it. + +**Docstring examples** use the NumPy `Examples` section: + + Examples + -------- + >>> parser = create_parser() + >>> parser.prog + 'g' + +**Console blocks are not examples that run.** A ```` ```console ```` block +at a `$` prompt is not collected by anything — `--doctest-modules` finds +doctests in Python modules, and the `docs/` collector runs `>>> ` blocks, +neither of which touches a shell transcript. Run a `console` block by hand +before committing it, and re-run it whenever you reshape the page around it; +nothing catches drift automatically. + +## The changelog + +`CHANGES` is the changelog, rendered as the Sphinx history page +(`docs/history.md`, published at `/history.html`). Its shape follows +Django's release-notes model — +deliverables get titles and prose, not bullets — because CHANGES here does +double duty as both the permanent ledger and the editorial release note; +there is no separate release page. + +**Release entry boilerplate.** Every release header is +`## g X.Y.Z (YYYY-MM-DD)`. The file opens with a `## g X.Y.Z (unreleased)` +placeholder fenced by `` and +`` HTML comments — new entries land immediately +below the END marker, never above it. + +**Open a release entry with a multi-sentence lead paragraph.** Plain prose, +no italics. Open with the version as the sentence subject ("g X.Y.Z +ships…") so the lead is self-contained when excerpted. Two to four +sentences on what shipped and who cares — user-visible takeaways, not +internal mechanism. Cross-reference detail docs with `{ref}` to keep the +lead compact. + +**The unreleased entry carries no lead paragraph and no version summary** — +sections only (`### Breaking changes`, `### What's new` deliverables, +`### Fixes`, …). Speaking for a 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 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. Do +not wrap a paragraph in `- ` — bullets are for enumerable lists, not +paragraph containers. + +**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 the pull request description 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 a breaking change, show the migration path +with a concrete `# Before` / `# After` code block. Dependency floor bumps +use ``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. A bullet that swells past two +lines gets promoted to a `#### Title (#NN)` heading with a prose body. + +**Always link autodoc'd APIs** — any class, method, function, exception, or +attribute with its own rendered page — via the roles in +[Documentation cross-references](#documentation-cross-references), never +with plain backticks. Plain backticks stay correct for code syntax, env +vars, parameter names, and file paths that have no autodoc destination. + +**Semantic-versioning meaning applies to the whole documented public +API** — command names, options, exit statuses, not only imported Python +symbols. A change to what `-V` prints or what a non-zero exit means is a +compatibility break here just as much as a signature change. + +**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. +Walls of text dressed up as bullets. A breaking change buried mid-entry +instead of given its own subheading at the top. + +**Summarization style.** Asked "what changed in the latest version?", lead +with the entry's lead paragraph (paraphrased if needed), then each `####` +deliverable heading under `### What's new` with a one-sentence summary. Cite +`(#NN)` only if asked for source links. Do not invent versions, dates, or +numbers absent from `CHANGES`; do not quote line numbers, which shift as the +file evolves. + +## Docstrings + +The prime directive: never restate the type. The annotation is the source of +truth; the docstring carries what the annotation cannot. + +This is documentation debt wearing a docstring: + + def get_id(pane: Pane) -> str: + """Get the pane's identifier. + + Parameters + ---------- + pane : Pane + The pane. + + Returns + ------- + str + The identifier. + """ + +Document instead the dimensions the type system cannot encode: mutation, +ownership, ordering, timing, failure, idempotence, concurrency, units and +ranges, boundary behaviour, platform differences, and any security boundary +— what is executed versus what is only read. + +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. + +Ruff's `pydocstyle` rule (`D`, `convention = "numpy"`) is the enforced +dialect — do not relitigate NumPy versus Google style in review. + +**Classes with fields** — `NamedTuple`, dataclasses — document every field +in an `Attributes` section: + + class CommandLineTestFixture(t.NamedTuple): + """Test fixture for CLI params, environment, and expected result. + + Attributes + ---------- + test_id : str + pytest parametrization id for the case. + env : EnvFlag + Directory state to simulate before invoking the CLI. + argv_args : list[str] + Arguments passed through to ``g`` on the command line. + expect_cmd : str | None + VCS command line expected, or ``None`` when none should run. + """ + +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. + +## 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. + +### 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. +- Transient observations — "currently", "for now", "the latest release" — + that go stale with no nearby edit. + +### The upkeep gate in practice + +It reaches values that track our own code. It does not reach frozen +external facts. + +Bad (Delete): + + # There are 321 tests to complete for servers. + +Good (Keep): + + # CPython < 3.11 has no ExceptionGroup, so this branch stays. + +### Documentation exception + +Doctests, minimal usage examples, and `Parameters`/`Returns`/`Raises` +entries on public API are exempt from the loss gate — they serve the +caller, not the maintainer. They are exempt from nothing else. Ceiling: a +good man page entry. + +## Terminology and capitalization + +Pick the domain noun and keep it. Call the marker directories `.git`, +`.svn`, `.hg` and the thing they select the "VCS type" or "VCS command" +consistently, rather than alternating with "backend", "handler", or +"engine". + +Python and PyPI keep their own capitalisation. Distribution names are +written as they are published. + +Do not write counts into prose — how many tests exist, how many functions +are exported. They go stale silently and no reader needs them. + +## Documentation cross-references + +`docs/` is built with Sphinx and MyST. Class references use `{class}`, +methods `{meth}`, functions `{func}`, exceptions `{exc}`, attributes +`{attr}`, internal anchors `{ref}`, and doc-path links `{doc}`. A `{ref}` +must match its target's anchor exactly — anchors here are lowercase and +hyphenated (`cli-main`, `cli-supported-vcs`, `developmental-releases`). + +Link the first prose mention of any symbol with a useful destination on +that page — a Python object, a CLI reference anchor, a project page, or an +external tool. Use the most specific role available for an API object; +`{ref}` or `{doc}` for a documentation page or section anchor; a plain +Markdown link for an external project. Do not rely on a later reference +section to satisfy the first-mention rule — if the first occurrence would +be a heading or a grid-card teaser, link that occurrence or retitle the +heading so the first prose mention can carry the link. Leave command +examples, code blocks, and literal values as code; link the surrounding +prose instead. + +What stays exact, never paraphrased: the marker-to-command table, the +message strings named in [CLI and error messages](#cli-and-error-messages), +version strings, and function cross-references. Warm the framing sentences +around a precise block; never the block itself. + +`make build_docs` catches a broken cross-reference; the test suite does +not — build the docs before committing a documentation change. +`docs/cli/index.md` is the worked example of all of the above: a +concept-first opening line, a three-step "How it works" before any +generated reference, and the precise marker table left exact below +everything the everyday reader needs. + +## 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. 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: + +``` +cli(feat[version]): Print version for -V + +why: -V is the common short flag for version and was unhandled. + +what: +- Treat -V as an alias for --version in run() +- Add a CLI test for the short flag +``` + +For a multi-line message, use a heredoc so the formatting survives: + +```console +$ git commit -m "$(cat <<'EOF' +Scope(feat[detail]): Concise description + +why: Explanation of the change. + +what: +- First change +- Second change +EOF +)" +``` + +### Release commits + +Never create tags. Never push tags. The owner handles tagging and tag +pushes, because a tag triggers the publish workflow. + +A release commit subject is plain and short: `Tag v`. The detailed +why and what go in the body. Do not use the `Scope(type[detail]):` format +for a release — it buries the lede. + +## Slop prevention + +Treat AI slop as review-hostile noise, not as proof that text or code is +wrong. The goal is to maximise information density. + +- **AI signatures.** No "Generated by", no conversational filler, no + unexplained emoji, no tool metadata. +- **Brittle references.** No hard-coded line numbers, fragile file counts, + dated "as of" claims, bare SHAs, or local absolute paths — unless they + are strict evidentiary artefacts such as a benchmark log. +- **Diff narration.** Do not restate what moved, was renamed, or was + removed in anything the reader holds alongside the diff: code, + docstrings, README, or a pull request description. The diff and the + commit message already carry it. +- **Branch-internal narrative.** Do not mention intermediate states, + abandoned approaches, or "no longer" behaviour unless users of a + published release actually experienced the old state. +- **Low-value scaffolding.** No ownerless TODOs, unused future-proofing, + debug artefacts, or defensive wrappers around failure modes nothing can + reach. +- **Prose inflation.** The diction table under [Voice](#voice) governs; + replace an inflated word with a concrete description of behaviour, + constraints, or trade-offs. +- **Coded labels.** Write rules and findings as plain imperatives. No + `[R1]`, `Option B`, or any index a reader has to decode. + +Preserve the "why". Never delete a comment documenting an invariant, a +protocol constraint, a platform quirk, or an upstream workaround — those +are the facts [Source comments](#source-comments) keeps, and every other +comment is judged by it. + +### Durable source links + +Link to a pinned revision, never to trunk, for anything cited as evidence. +`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-character commit SHA (`blob/9a29b1a/…`) reachable from + trunk, for a claim about unreleased code. Never a pull-request-head SHA + — it can be rebased or garbage-collected. +- Reserve `blob/master/…` for a living document meant to always show the + latest state — `.github/CONTRIBUTING.md` and this file are exactly that + case. +- Line anchors (`#L120-L145`) are only safe on a pinned ref. diff --git a/.github/contributing.md b/.github/contributing.md deleted file mode 100644 index c0eddac..0000000 --- a/.github/contributing.md +++ /dev/null @@ -1,27 +0,0 @@ -# Contributing - -When contributing to this repository, please first discuss the change you wish to make via issue, -email, or any other method with the maintainers of this repository before making a change. - -See [developing](../docs/developing.md) for environment setup and [AGENTS.md](../AGENTS.md) for -detailed coding standards. - -## Pull Request Process - -1. **Format and lint**: `uv run ruff format .` then `uv run ruff check . --fix --show-fixes` -2. **Type check**: `uv run mypy` -3. **Test**: `uv run pytest` — all tests must pass before submitting -4. **Document**: Update docs if your change affects the public interface -5. You may merge the Pull Request once you have the sign-off of one other developer. If you - do not have permission to do that, you may request a reviewer to merge it for you. - -## Decorum - -- Participants will be tolerant of opposing views. -- Participants must ensure that their language and actions are free of personal - attacks and disparaging personal remarks. -- When interpreting the words and actions of others, participants should always - assume good intentions. -- Behaviour which can be reasonably considered harassment will not be tolerated. - -Based on [Ruby's Community Conduct Guideline](https://www.ruby-lang.org/en/conduct/) diff --git a/AGENTS.md b/AGENTS.md index df9371d..21dca84 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,348 +1,53 @@ # AGENTS.md -This file provides guidance to AI agents (including Claude Code, Cursor, and other LLM-powered tools) when working with code in this repository. The tooling and docs rely on the gp-libs ecosystem; treat gp-libs as the shared dev toolkit that underpins this project. +g is a lightweight CLI wrapper: it detects the current directory's VCS +(git, svn, or hg) and proxies your arguments straight to it. -## CRITICAL REQUIREMENTS +Follow the conventions already in the tree, and keep a change scoped to what +was asked for. -### 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 +## What is here -## Project Overview +| Path | What it is | +| ---- | ---------- | +| `src/g/__init__.py` | Everything: `find_repo_type()`, `create_parser()`, `run()` (console-script entry point) | +| `src/g/__about__.py` | Package metadata (`__version__`, URLs); exec'd by `docs/conf.py` | +| `tests/test_cli.py` | Parametrized CLI tests | +| `conftest.py` | Autouse fixture setting `G_IS_TEST=1` | +| `docs/` | Sphinx (MyST) site; `docs/cli/index.md` is the CLI reference | +| `CHANGES` | Changelog, rendered as the docs history page | +| `Makefile`, `justfile`, `docs/justfile` | Task runners wrapping `uv`/`just` | -g is a lightweight CLI wrapper that proxies to the current directory's VCS command (git, svn, or hg). It auto-detects the repo type, forwards user arguments, and exits after invoking the native tool. The project lives in the gp-libs family of git-pull utilities and uses gp-libs packages for docs and development helpers. +## Which policy applies -Key features: -- Detects VCS by walking parent directories and mapping `.git`, `.svn`, or `.hg` -- Proxies CLI arguments to the detected VCS binary (--version/-V is handled by g) -- Minimal surface area: primary logic lives in `src/g/__init__.py` -- Test fixtures cover CLI behavior for both repo and non-repo directories +- Documentation, user-facing text, `CHANGES`, commit messages, docstrings, + source comments, logging, and CLI/error text: + [.github/WRITING.md](.github/WRITING.md) +- Environment, the gates, tests, documentation builds, releases, and pull + requests: [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) -## Development Environment +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. -This project uses: -- Python 3.10+ -- [uv](https://github.com/astral-sh/uv) for dependency management and execution -- [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/) (invoked as `py.test`) for testing -- [gp-libs](https://gp-libs.git-pull.com/) for shared Sphinx/test helpers (included in dev/docs extras) +## Change discipline -## Common Commands - -### Setting Up Environment - -```bash -# Install all dev and doc dependencies -uv sync --all-extras --dev -``` - -### Running Tests - -```bash -# Run full suite -make test -# or directly -uv run py.test - -# Watch tests (pytest-watcher) -make start # runs tests once then ptw . - -# Watch tests via entr (requires entr(1)) -make watch_test -``` - -### Linting and Type Checking - -```bash -# Lint and format with ruff -uv run ruff check . -uv run ruff format . - -# make targets -make ruff -make ruff_format -make watch_ruff - -# Type checking -uv run mypy . -make mypy -make watch_mypy -``` - -### Documentation - -```bash -# Build docs -make build_docs - -# Live docs server with autoreload -make start_docs - -# Docs design assets -make design_docs -``` - -## Code Architecture - -``` -src/g/__init__.py - ├─ find_repo_type(): detect VCS by walking parent directories - ├─ run(): CLI entrypoint; proxies args to detected VCS, honors G_IS_TEST - └─ DEFAULT + vcspath_registry helpers - -tests/test_cli.py - └─ Parametrized CLI tests for git/non-repo scenarios -``` - -## Testing Strategy - -- Tests live in `tests/test_cli.py` and use `pytest` with parametrized fixtures. -- `G_IS_TEST` env flag forces `run()` to return the subprocess so output can be asserted; set when modifying run logic. -- CLI tests rely on actual VCS binaries (e.g., `git`) being available on PATH. If adding tests for svn/hg, ensure binaries are installed or skip appropriately. -- Use `tmp_path` and `monkeypatch` to simulate non-repo directories instead of mocks where possible. -- Prefer pytest-watcher (`make start`) for TDD loops; for file-watch without ptw, use `make watch_test` (requires entr). - -## Coding Standards - -- Include `from __future__ import annotations` at the top of Python modules. -- Use namespace imports for stdlib: `import typing as t`, `import logging`, etc.; third-party packages may use `from X import Y`. -- Follow NumPy-style docstrings (see existing docstrings in `run` and pytest config requiring `pydocstyle` via ruff). -- Ruff is the source of truth for lint rules; see `pyproject.toml` for enabled checks (E, F, I, UP, A, B, C4, COM, EM, Q, PTH, SIM, TRY, PERF, RUF, D, FA100). -- Type checking is strict (`mypy --strict`); favor precise types and avoid `Any` unless necessary. - -**Classes with fields** — `NamedTuple`, dataclasses — document every field in -an `Attributes` section: - -```python -class CommandLineTestFixture(t.NamedTuple): - """Test fixture for CLI params, environment, and expected result. - - Attributes - ---------- - test_id : str - pytest parametrization id for the case. - env : EnvFlag - Directory state to simulate before invoking the CLI. - argv_args : list[str] - Arguments passed through to ``g`` on the command line. - expect_cmd : str | None - VCS command line expected, or ``None`` when none should run. - """ -``` - -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. - -## 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="g.cli")` -- Filter records rather than index by position: `[r for r in caplog.records if hasattr(r, "vcs_cmd")]` -- Assert on schema: `record.vcs_exit_code == 0` not `"exit code 0" in caplog.text` -- `caplog.record_tuples` cannot access extra fields — always use `caplog.records` - -### Avoid - -- f-strings/`.format()` in log calls -- Unguarded logging in hot loops (guard with `isEnabledFor()`) -- Catch-log-reraise without adding new context -- `print()` for diagnostics -- Logging secret env var values (log key names only) -- Non-scalar ad-hoc objects in `extra` -- Requiring custom `extra` fields in format strings without safe defaults (missing keys raise `KeyError`) - -### 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 -- **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 - -#### 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. - -## Doctests - -**All functions and methods MUST have working doctests.** Doctests serve as both documentation and tests. - -**CRITICAL RULES:** -- Doctests MUST actually execute - never comment out function calls or similar -- Doctests MUST NOT be converted to `.. code-block::` as a workaround (code-blocks don't run) -- If you cannot create a working doctest, **STOP and ask for help** - -**Available tools for doctests:** -- `doctest_namespace` fixtures: `tmp_path` -- Ellipsis for variable output: `# doctest: +ELLIPSIS` -- Update `conftest.py` to add new fixtures to `doctest_namespace` - -**`# doctest: +SKIP` is NOT permitted** - it's just another workaround that doesn't test anything. If a VCS binary might not be installed, use proper skip markers in pytest. - -**Using fixtures in doctests:** -```python ->>> from g import find_repo_type ->>> find_repo_type('/some/git/repo') # doctest: +ELLIPSIS -'git' -``` - -**When output varies, use ellipsis:** -```python ->>> import pathlib ->>> pathlib.Path.cwd() # doctest: +ELLIPSIS -PosixPath('...') -``` - -## Changelog Conventions - -These rules apply when authoring entries in `CHANGES`, which is rendered as the Sphinx changelog page. Modeled on Django's release-notes shape — deliverables get titles and prose, not bullets. Older entries used a flat `### Section` + bullet shape; new entries follow the Django shape below. - -**Release entry boilerplate.** Every release header is `## g X.Y.Z (YYYY-MM-DD)`. The file opens with a `## g 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 (*"g 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}`, 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 - -- Add logging with `logging` configured in `run`; keep output minimal because the CLI forwards to underlying VCS. -- When diagnosing repo detection, log the path iteration in `find_repo_type` or unit-test with synthetic directory trees. -- If subprocess output is swallowed, run with `G_IS_TEST=1` and `wait=True` to capture stdout/stderr in tests. +- 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. Keep + a new API private until a caller outside the module needs it. +- Add a file only for a durable boundary — a distinct responsibility, + independent reuse, or splitting an oversized module — not for a + single-use helper or a one-line re-export. +- Add a test for every user-visible behaviour change, and a `CHANGES` entry + for every change to the public API, CLI, configuration, or output. +- A passing gate is evidence only once it has been shown capable of + failing. Pair a new test with a deliberate break that proves it bites. +- `find_repo_type()` requires a `.git`/`.svn`/`.hg` **directory**; it will + not detect a repo from a git worktree checkout, whose top-level `.git` is + a file. This is a real limitation, not a bug in a test. +- Keep this file lean: delete a line whose removal would not cause a + mistake; push a multi-step procedure into a skill and a path-specific + rule into a nested `AGENTS.md`. ## References @@ -351,253 +56,3 @@ These rules apply when authoring entries in `CHANGES`, which is rendered as the - Changelog: https://g.git-pull.com/history.html - Repository: https://github.com/vcs-python/g - Shared tooling (gp-libs): https://gp-libs.git-pull.com/ - -## 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 -``` - -## 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. -- 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. diff --git a/README.md b/README.md index 71ad293..ec2ba6f 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,11 @@ Passthrough to your current directory's VCS. [![Code Coverage](https://codecov.io/gh/vcs-python/g/branch/master/graph/badge.svg)](https://codecov.io/gh/vcs-python/g) [![License](https://img.shields.io/github/license/vcs-python/g.svg)](https://github.com/vcs-python/g/blob/master/LICENSE) -Shortcut / powertool for developers to access current repos' VCS, whether it's -git, subversion (`svn`), or mercurial (`hg`). +A command-line shortcut that runs your current directory's VCS command, +whether it's git, subversion (`svn`), or mercurial (`hg`). Requires Python +3.10 or newer. + +## Install ```console $ pip install --user g @@ -39,7 +42,7 @@ $ g ### Developmental releases -You can test the unpublished version of g before its released. +You can test the unpublished version of g before its release. - [pip](https://pip.pypa.io/en/stable/): @@ -50,15 +53,15 @@ You can test the unpublished version of g before its released. - [uv](https://docs.astral.sh/uv/): ```console - $ uv tool install g + $ uv tool install --prerelease=allow g ``` ```console - $ uv add g + $ uv add g --prerelease allow ``` ```console - $ uvx g + $ uvx --from 'g' --prerelease allow g ``` - [pipx](https://pypa.github.io/pipx/docs/): @@ -69,11 +72,28 @@ You can test the unpublished version of g before its released. Then use `g@next --help`. -# Credits +## Usage + +```console +$ g status +``` + +Inside a git checkout, that runs `git status`; inside svn, `svn status`; +inside mercurial, `hg status`. g intercepts only `-V`/`--version` — +everything else, including `-h`/`--help`, forwards straight to the detected +VCS. The VCS's own stdout and stderr print through unchanged, but g's exit +status does not mirror the wrapped command's: a completed run always exits +`0`. Outside a VCS directory, g prints `No VCS found in current +directory.` to stderr and stops. + +Full detection order and command reference: +. + +## Credits 2021-12-05: Thanks to [John Shanahan](https://github.com/shanahanjrs) ([@\_shanahanjrs](https://twitter.com/_shanahanjrs)) for giving g use [g](https://pypi.org/project/g/) -# Donations +## Donations Your donations fund development of new features, testing and support. Your money will go directly to maintenance and development of the @@ -82,9 +102,9 @@ right for the value you get out of the project. See donation options at . -# More information +## More information -- Python support: >= 3.9, pypy +- Python support: >= 3.10 - VCS supported: git(1), svn(1), hg(1) - Source: - Docs: diff --git a/docs/AGENTS.md b/docs/AGENTS.md deleted file mode 100644 index 8dab30c..0000000 --- a/docs/AGENTS.md +++ /dev/null @@ -1,131 +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 mechanics. It complements -the repository-root `AGENTS.md`, which already governs doctests, -changelog conventions, MyST roles, and commit messages. 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 works at a shell prompt inside a checkout and runs -`g` where they would type `git`, `svn`, or `hg`. They are fluent in -their own VCS — status, commit, log, diff — but you cannot assume they -read Python or know g's internals: `find_repo_type()`, the -`vcspath_registry` marker mapping, or the `G_IS_TEST` escape hatch the -tests use. - -A second, smaller reader writes Python: they call `run()` directly, -build on `create_parser()`, or contribute to g itself. Serve them too, -but mark their material opt-in ("for the rarer cases", "internal") 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 run `g status`", not - "The command is proxied". Address the reader who is doing the thing. -- **Concept before mechanics.** Open by saying what g *does* for the - reader — one command that becomes the right VCS command. The - generated option listing, the marker table, the exit behavior are - the last details they need, not the first. A page that opens with - argparse output has buried the idea under its plumbing. -- **Say when they can stop.** g has no configuration; most pages owe - the reader one reassurance up front — run `g` where you'd run your - VCS, and you're done. Let a skimmer leave after one sentence. -- **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 - everyday command → the one flag g intercepts (`--version`/`-V`) → - how detection works → the Python entry points. Each step is for a - smaller audience than the last. -- **Lean on the pipeline.** The reader's mental model is a straight - line: current directory → walk up the parents → a `.git`, `.svn`, - or `.hg` marker → the matching VCS command, with your arguments - forwarded. Reinforce that chain when you explain detection or - dispatch; it is the whole tool. -- **Name the trade-off.** If the thinness costs something, say so - plainly: `--version`/`-V` never reaches the underlying VCS, and - outside any repository g prints "No VCS found in current directory." - and stops. State it; don't sell it. -- **Frame by concept, not by mechanism.** Don't headline a feature by - its directory marker or argparse detail in prose; that names the - implementation surface, which is the reader's last concern. Name the - concept — detection, forwarding. The mechanics vocabulary — the - marker-to-command table, the generated option reference — belongs in - the CLI reference, and only there. - -## Keeping examples correct - -Nothing executes the examples under `docs/` — `--doctest-modules` -runs the doctests in `src/g`'s docstrings, and `testpaths` includes -`docs/`, but Markdown prose is never collected. A ```` ```console ```` -block is trusted as written, so it drifts silently. Run a command -before you commit it as an example, and when you reshape a page, -re-check that its examples still match what g prints today. Keep -shell examples in ```` ```console ```` blocks at a `$` prompt, one -command per block, as the existing pages do. - -## What stays precise - -Warm the framing, never the facts. The marker-to-command table, exact -message strings ("No VCS found in current directory."), version -strings, and function cross-references carry meaning in their exact -form — leave them alone. The friendly voice belongs in the sentences -*around* a precise block, introducing it, not inside it paraphrasing -it into vagueness. - -## Cross-references - -Point the curious reader at the deep-dive rather than inlining it, and -put the link where their interest peaks — on the phrase that made them -curious ("how detection works", "call it from Python") — not as a -standalone footnote the eye skips. Use the MyST roles listed in the -root `AGENTS.md` (`{func}`, `{class}`, `{meth}`, `{attr}`, `{exc}`, -`{ref}`, `{doc}`). A `{ref}` must match its target's anchor exactly — -anchors here are lowercase and hyphenated (`cli-main`, -`cli-supported-vcs`, `developmental-releases`). `make build_docs` -catches a broken cross-reference; the test suite does 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, g's API, CLI -reference anchors, project pages, and external tools or projects. Use -the most specific target available: `{func}`, `{class}`, `{meth}`, -`{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 values as code; link the surrounding prose -instead. - -## A page that does this - -`docs/cli/index.md` is the worked example: a concept-first line — g -proxies to your current directory's VCS — a three-step "How it works" -before any generated reference, the `--version`/`-V` interception -stated plainly, everyday usage in ```` ```console ```` blocks, and the -precise directory-marker table left exact, below everything the -everyday reader needs. Read it before reshaping another page. - -## Before you commit - -- Does the page open with what g *does* for the reader, or with how - the command line is parsed? -- Can a reader who only wants `g status` stop after the first - paragraph? -- Is anything framed by its directory marker or argparse surface that - should be named by concept instead? -- Are the Python-only and contributor parts clearly marked opt-in? -- Did you re-run any shell example you touched, and leave every table, - message string, and cross-reference exact? -- Did `make 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 47dc3e3..0000000 --- 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 948cc7d..e96b29f 100644 --- a/docs/project/code-style.md +++ b/docs/project/code-style.md @@ -1,24 +1,7 @@ # Code Style -Use this page when you are changing g itself and want the same local checks CI -expects. - -## Formatting - -Run [ruff](https://github.com/astral-sh/ruff) before committing Python changes. - -```console -$ uv run ruff check . --fix -``` - -```console -$ uv run ruff format . -``` - -## Type Checking - -Run [mypy](https://mypy-lang.org/) for static type checking. - -```console -$ uv run mypy . -``` +This page split in two. The formatting, linting, and type-checking commands +now live in +[.github/CONTRIBUTING.md](https://github.com/vcs-python/g/blob/master/.github/CONTRIBUTING.md#the-gates) +("The gates"). Docstring and comment conventions now live in +[.github/WRITING.md](https://github.com/vcs-python/g/blob/master/.github/WRITING.md#docstrings). diff --git a/docs/project/contributing.md b/docs/project/contributing.md index 047e52c..bd13e1a 100644 --- a/docs/project/contributing.md +++ b/docs/project/contributing.md @@ -1,255 +1,7 @@ # Development -Use this page when you want to change g itself. If you only want to install and -run the command, start with {doc}`/quickstart`. - -## Bootstrap the project - -Install [git] and [uv]. - -Clone: - -```console -$ git clone https://github.com/vcs-python/g.git -``` - -```console -$ cd g -``` - -Install packages: - -```console -$ uv sync --all-extras --dev -``` - -[installation documentation]: https://docs.astral.sh/uv/getting-started/installation/ -[git]: https://git-scm.com/ - -## Tests - -```console -$ uv run py.test -``` - -The Makefile wrapper runs the same test command. - -```console -$ make test -``` - -## Automatically run tests on file save - -Run tests once, then keep watching with [pytest-watcher]: - -```console -$ make start -``` - -Watch through [entr(1)] if you have it installed: - -```console -$ make watch_test -``` - -[pytest-watcher]: https://github.com/olzhasar/pytest-watcher - -## Documentation - -Default preview server: http://localhost:8034 - -[sphinx-autobuild] builds the docs, watches for file changes, and launches a -server. - -From the project root: - -```console -$ make start_docs -``` - -From inside `docs/`: - -```console -$ make start -``` - -[sphinx-autobuild]: https://github.com/executablebooks/sphinx-autobuild - -### Manual documentation - -Enter the docs directory: - -```console -$ cd docs -``` - -Build the docs: - -```console -$ make html -``` - -Start the HTTP server: - -```console -$ make serve -``` - -Project-root helpers run the same docs tasks: - -```console -$ make build_docs -``` - -```console -$ make serve_docs -``` - -Rebuild docs on file change with [entr(1)]: - -```console -$ make watch_docs -``` - -Rebuild docs and run the server through one terminal when your [GNU Make] has -`-J` support: - -```console -$ make dev_docs -``` - -## Formatting / Linting - -### Linting and formatting - -The project uses [ruff] to handle formatting, sorting imports and linting. - -````{tab} Command - -uv: - -```console -$ uv run ruff check . -``` - -If you set up manually: - -```console -$ ruff check . -``` - -```` - -````{tab} make - -```console -$ make ruff -``` - -```` - -````{tab} Watch - -```console -$ make watch_ruff -``` - -requires [`entr(1)`]. - -```` - -````{tab} Fix files - -uv: - -```console -$ uv run ruff check . --fix -``` - -If you set up manually: - -```console -$ ruff check . --fix -``` - -```` - -#### Code formatting - -Use [ruff format] for formatting. - -````{tab} Command - -uv: - -```console -$ uv run ruff format . -``` - -If you set up manually: - -```console -$ ruff format . -``` - -```` - -````{tab} make - -```console -$ make ruff_format -``` - -```` - -### Type checking - -Use [mypy] for static type checking. - -````{tab} Command - -uv: - -```console -$ uv run mypy . -``` - -If you set up manually: - -```console -$ mypy . -``` - -```` - -````{tab} make - -```console -$ make mypy -``` - -```` - -````{tab} Watch - -```console -$ make watch_mypy -``` - -requires [`entr(1)`]. -```` - -## Releasing - -[uv] handles virtualenv creation, package requirements, versioning, -building, and publishing. There is no `setup.py` or requirements file. - -See {doc}`/project/releasing` before preparing a release. - -[uv]: https://github.com/astral-sh/uv -[entr(1)]: http://eradman.com/entrproject/ -[`entr(1)`]: http://eradman.com/entrproject/ -[GNU Make]: https://www.gnu.org/software/make/ -[ruff format]: https://docs.astral.sh/ruff/formatter/ -[ruff]: https://ruff.rs -[mypy]: http://mypy-lang.org/ +Environment setup, the gates, tests, documentation builds, and the release +process now live in +[.github/CONTRIBUTING.md](https://github.com/vcs-python/g/blob/master/.github/CONTRIBUTING.md), +which is also what GitHub shows when you open a pull request. Read it there +— this page only keeps the published URL working.