|
| 1 | +# Contributing |
| 2 | + |
| 3 | +Thanks for looking. A bug report with a reproduction — the directory layout |
| 4 | +and the exact `g` invocation — is the most useful thing you can send; a |
| 5 | +correction to somewhere the documentation misled you is a close second. |
| 6 | + |
| 7 | +How this project writes prose — README, `CHANGES`, commit messages, |
| 8 | +docstrings, source comments, and CLI/error text — is set out separately in |
| 9 | +[WRITING.md](WRITING.md). Read that before changing any of it. The |
| 10 | +constraints every change is held to, and the map of what is where, are in |
| 11 | +[AGENTS.md](../AGENTS.md). |
| 12 | + |
| 13 | +## Getting set up |
| 14 | + |
| 15 | +Install [uv], then sync the dev and docs extras: |
| 16 | + |
| 17 | +```console |
| 18 | +$ uv sync --all-extras --dev |
| 19 | +``` |
| 20 | + |
| 21 | +[uv]: https://github.com/astral-sh/uv |
| 22 | + |
| 23 | +## The gates |
| 24 | + |
| 25 | +CI is the order of record; every gate it runs has to pass before a change is |
| 26 | +done. |
| 27 | + |
| 28 | +Format: |
| 29 | + |
| 30 | +```console |
| 31 | +$ uv run ruff format . |
| 32 | +``` |
| 33 | + |
| 34 | +Lint: |
| 35 | + |
| 36 | +```console |
| 37 | +$ uv run ruff check . --fix --show-fixes |
| 38 | +``` |
| 39 | + |
| 40 | +Type-check (`[tool.mypy] strict = true`): |
| 41 | + |
| 42 | +```console |
| 43 | +$ uv run mypy . |
| 44 | +``` |
| 45 | + |
| 46 | +Test: |
| 47 | + |
| 48 | +```console |
| 49 | +$ uv run py.test |
| 50 | +``` |
| 51 | + |
| 52 | +Documentation is a gate, not a courtesy. Examples in docstrings under |
| 53 | +`src/g` and pages under `docs/` are executed by `pytest`/`py.test` — the |
| 54 | +doctest flags live in `pyproject.toml`, so there is no separate doctest |
| 55 | +step and a green test run is the proof. `README.md` is not included, so its |
| 56 | +examples are never executed. Which blocks qualify, and the one mistake that |
| 57 | +silently removes a test, are in |
| 58 | +[WRITING.md](WRITING.md#documented-examples-that-run). |
| 59 | + |
| 60 | +Ruff's isort config requires `from __future__ import annotations` in every |
| 61 | +module (`required-imports`, backed by the `FA100` rule) — the linter, not |
| 62 | +this file, catches a missing one. Import stdlib modules by namespace |
| 63 | +(`import typing as t`, `import logging`) rather than `from typing import |
| 64 | +…`; third-party packages may use `from X import Y`. Nothing enforces this |
| 65 | +one — it is a convention, not a lint rule. |
| 66 | + |
| 67 | +Before claiming a test or a gate works, show it failing. A gate that has |
| 68 | +never been red is an assumption. |
| 69 | + |
| 70 | +## Tests |
| 71 | + |
| 72 | +Tests live in `tests/test_cli.py`, parametrized through a |
| 73 | +`CommandLineTestFixture` `NamedTuple`. The autouse `setup` fixture in |
| 74 | +`conftest.py` sets `G_IS_TEST=1` for every test, which makes `run()` return |
| 75 | +the `subprocess.Popen` object instead of `None` so assertions can inspect |
| 76 | +it — outside tests, `run()` always returns `None`. |
| 77 | + |
| 78 | +CLI tests invoke real VCS binaries (at minimum `git`) rather than mocking |
| 79 | +the subprocess call; use `tmp_path` and `monkeypatch` to simulate a |
| 80 | +non-repo directory. |
| 81 | + |
| 82 | +`find_repo_type()` requires a `.git`, `.svn`, or `.hg` **directory**. A git |
| 83 | +worktree checkout's top-level `.git` is a file, not a directory, so |
| 84 | +`test_command_line[g-cmd-inside-git-dir]` and its `--help` sibling fail |
| 85 | +there even though nothing is broken. Work from a normal clone (not `git |
| 86 | +worktree add`) to run the full suite green. |
| 87 | + |
| 88 | +When subprocess output seems swallowed, set `G_IS_TEST=1` and call |
| 89 | +`run(wait=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)` to capture |
| 90 | +it for inspection. |
| 91 | + |
| 92 | +Assert on `caplog.records` attributes, not string matching on `caplog.text` |
| 93 | +— `caplog.record_tuples` cannot see `extra` fields. Scope capture with |
| 94 | +`caplog.at_level(logging.DEBUG, logger="g")` — `g`'s modules log through |
| 95 | +`logging.getLogger(__name__)`, so the logger name is the module's own |
| 96 | +dotted path, `g` at the package root. Filter records rather than index by |
| 97 | +position: `[r for r in caplog.records if hasattr(r, "vcs_cmd")]`. |
| 98 | + |
| 99 | +## Documentation |
| 100 | + |
| 101 | +Build once: |
| 102 | + |
| 103 | +```console |
| 104 | +$ make build_docs |
| 105 | +``` |
| 106 | + |
| 107 | +Live-reloading preview at `http://localhost:8034`: |
| 108 | + |
| 109 | +```console |
| 110 | +$ make start_docs |
| 111 | +``` |
| 112 | + |
| 113 | +From inside `docs/`, the same tasks are `just html` and `just start`; `just |
| 114 | +--list` in `docs/` shows the rest (`watch`, `serve`, `linkcheck`, |
| 115 | +`doctest`). `make build_docs` and `just html` both wrap |
| 116 | +`sphinx-build`; nothing under `docs/_build` is hand-edited. |
| 117 | + |
| 118 | +`AGENTS.md` and `CLAUDE.md` are excluded from the build |
| 119 | +(`exclude_patterns` in `docs/conf.py`) — they are agent guidance, not site |
| 120 | +pages. `make build_docs` catches a broken cross-reference; the test suite |
| 121 | +does not, so build the docs before committing a documentation change. |
| 122 | + |
| 123 | +## Releasing |
| 124 | + |
| 125 | +Never create tags. Never push tags. The owner handles tagging and tag |
| 126 | +pushes, because a tag triggers the publish workflow. See |
| 127 | +[Release commits](WRITING.md#release-commits). |
| 128 | + |
| 129 | +1. Update `CHANGES` with the release notes. |
| 130 | +2. Bump the version in `src/g/__about__.py` and `pyproject.toml`. |
| 131 | +3. Commit the release files with the subject `Tag v<version>`. |
| 132 | +4. Tag (`git tag v<version>`) and push the branch, then the tag |
| 133 | + (`git push --tags`). |
| 134 | +5. CI builds and publishes to PyPI automatically over OIDC trusted |
| 135 | + publishing once the tag lands. |
| 136 | + |
| 137 | +Full detail: [docs/project/releasing.md](../docs/project/releasing.md). |
| 138 | + |
| 139 | +## Pull requests |
| 140 | + |
| 141 | +One subject per pull request. Unrelated cleanup found along the way belongs |
| 142 | +in its own commit, and usually in its own pull request. |
| 143 | + |
| 144 | +Discuss a substantial change via an issue before making it. |
| 145 | + |
| 146 | +Commit format is in [WRITING.md](WRITING.md#commits). |
| 147 | + |
| 148 | +Merge once you have the sign-off of one other developer. If you do not have |
| 149 | +permission to merge, ask a maintainer to merge it for you. |
| 150 | + |
| 151 | +## Decorum |
| 152 | + |
| 153 | +- Participants will be tolerant of opposing views. |
| 154 | +- Participants must ensure that their language and actions are free of |
| 155 | + personal attacks and disparaging personal remarks. |
| 156 | +- When interpreting the words and actions of others, participants should |
| 157 | + always assume good intentions. |
| 158 | +- Behaviour which can be reasonably considered harassment will not be |
| 159 | + tolerated. |
| 160 | + |
| 161 | +Based on [Ruby's Community Conduct Guideline](https://www.ruby-lang.org/en/conduct/). |
| 162 | + |
| 163 | +## Security |
| 164 | + |
| 165 | +This repository has no `SECURITY.md`. Please do not open a public issue for |
| 166 | +a vulnerability — report it privately via [GitHub's security |
| 167 | +advisories](https://github.com/vcs-python/g/security/advisories/new). |
0 commit comments