linecite 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- linecite-0.1.1/.gitattributes +1 -0
- linecite-0.1.1/.github/workflows/ci.yml +36 -0
- linecite-0.1.1/.github/workflows/release.yml +48 -0
- linecite-0.1.1/.gitignore +6 -0
- linecite-0.1.1/.pre-commit-hooks.yaml +18 -0
- linecite-0.1.1/LICENSE +21 -0
- linecite-0.1.1/PKG-INFO +205 -0
- linecite-0.1.1/README.md +178 -0
- linecite-0.1.1/action/comment.py +209 -0
- linecite-0.1.1/action.yml +129 -0
- linecite-0.1.1/docs/how-it-works.md +42 -0
- linecite-0.1.1/pyproject.toml +48 -0
- linecite-0.1.1/src/linecite/__init__.py +3 -0
- linecite-0.1.1/src/linecite/adopt.py +195 -0
- linecite-0.1.1/src/linecite/affected.py +198 -0
- linecite-0.1.1/src/linecite/audit.py +359 -0
- linecite-0.1.1/src/linecite/cli.py +333 -0
- linecite-0.1.1/src/linecite/config.py +128 -0
- linecite-0.1.1/src/linecite/errors.py +6 -0
- linecite-0.1.1/src/linecite/history.py +144 -0
- linecite-0.1.1/src/linecite/repo.py +323 -0
- linecite-0.1.1/src/linecite/scan.py +442 -0
- linecite-0.1.1/src/linecite/spec.py +255 -0
- linecite-0.1.1/tests/conftest.py +89 -0
- linecite-0.1.1/tests/test_action.py +350 -0
- linecite-0.1.1/tests/test_adopt.py +212 -0
- linecite-0.1.1/tests/test_affected_report.py +162 -0
- linecite-0.1.1/tests/test_audit.py +355 -0
- linecite-0.1.1/tests/test_dogfood.py +15 -0
- linecite-0.1.1/tests/test_hooks.py +21 -0
- linecite-0.1.1/tests/test_legacy_affected_config.py +120 -0
- linecite-0.1.1/tests/test_links.py +209 -0
- linecite-0.1.1/tests/test_refs.py +115 -0
- linecite-0.1.1/tests/test_release.py +33 -0
- linecite-0.1.1/tests/test_sync.py +286 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* text=auto eol=lf
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ${{ matrix.os }}
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
os: [ubuntu-latest, windows-latest]
|
|
18
|
+
python: ["3.11", "3.13"]
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v7
|
|
21
|
+
- uses: actions/setup-python@v7
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python }}
|
|
24
|
+
- run: python -m pip install -e ".[dev]"
|
|
25
|
+
- run: python -m pytest -q
|
|
26
|
+
|
|
27
|
+
docs:
|
|
28
|
+
# linecite on its own docs, through its own action: check, and on pull requests a comment
|
|
29
|
+
# listing the paragraphs whose cited code the pull request changed
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
permissions:
|
|
32
|
+
contents: read
|
|
33
|
+
pull-requests: write
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v7
|
|
36
|
+
- uses: ./
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Publishing a GitHub release uploads that tag's build to PyPI through trusted publishing: PyPI trusts
|
|
4
|
+
# this workflow by name, so no API token is stored anywhere.
|
|
5
|
+
on:
|
|
6
|
+
release:
|
|
7
|
+
types: [published]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v7
|
|
17
|
+
- uses: actions/setup-python@v7
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.13"
|
|
20
|
+
- name: The tag names the version being released
|
|
21
|
+
run: |
|
|
22
|
+
version=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
|
|
23
|
+
if [ "v$version" != "$GITHUB_REF_NAME" ]; then
|
|
24
|
+
echo "::error::tag $GITHUB_REF_NAME does not match pyproject.toml version $version"
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
- run: python -m pip install build twine
|
|
28
|
+
- run: python -m build
|
|
29
|
+
- run: python -m twine check dist/*
|
|
30
|
+
- uses: actions/upload-artifact@v7
|
|
31
|
+
with:
|
|
32
|
+
name: dist
|
|
33
|
+
path: dist/
|
|
34
|
+
|
|
35
|
+
publish:
|
|
36
|
+
needs: build
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
environment:
|
|
39
|
+
name: pypi
|
|
40
|
+
url: https://pypi.org/p/linecite
|
|
41
|
+
permissions:
|
|
42
|
+
id-token: write # the OIDC token PyPI checks instead of a password
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/download-artifact@v8
|
|
45
|
+
with:
|
|
46
|
+
name: dist
|
|
47
|
+
path: dist/
|
|
48
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# A commit that touches only code can move the lines a doc cites, so both hooks read every document in
|
|
2
|
+
# the configuration on every commit instead of the staged files.
|
|
3
|
+
- id: linecite-check
|
|
4
|
+
name: linecite check
|
|
5
|
+
description: Fail when doc citations drifted, no longer resolve, or cite code by number only.
|
|
6
|
+
entry: linecite check
|
|
7
|
+
language: python
|
|
8
|
+
pass_filenames: false
|
|
9
|
+
always_run: true
|
|
10
|
+
require_serial: true
|
|
11
|
+
- id: linecite-sync
|
|
12
|
+
name: linecite sync
|
|
13
|
+
description: Rewrite drifted line numbers in doc citations, then fail on what still needs a human.
|
|
14
|
+
entry: linecite sync
|
|
15
|
+
language: python
|
|
16
|
+
pass_filenames: false
|
|
17
|
+
always_run: true
|
|
18
|
+
require_serial: true
|
linecite-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gotoUSA
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
linecite-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: linecite
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Code citations in docs that re-derive their line numbers from the source.
|
|
5
|
+
Project-URL: Homepage, https://github.com/gotoUSA/linecite
|
|
6
|
+
Project-URL: Source, https://github.com/gotoUSA/linecite
|
|
7
|
+
Project-URL: Issues, https://github.com/gotoUSA/linecite/issues
|
|
8
|
+
Author: gotoUSA
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ci,code citations,docs,documentation,line numbers,pre-commit,stale docs
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Documentation
|
|
20
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
21
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pre-commit>=3; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# linecite
|
|
29
|
+
|
|
30
|
+
Docs that cite code by line number (`orders.py:310`<!--@-->) go stale on the next commit that touches the lines above.
|
|
31
|
+
linecite lets a citation name the code it means — a **symbol and a quoted fragment** — derives the line number
|
|
32
|
+
from the source, and tells you which paragraphs to re-read after the code changes.
|
|
33
|
+
|
|
34
|
+
```md
|
|
35
|
+
Locks are taken in product order at [orders.py:310](app/orders.py#L310 "create_order: order_by(\"product_id\")").
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
That is an ordinary markdown link: it renders as `orders.py:310`<!--@-->, clicks through to the line on GitHub, and its
|
|
39
|
+
title says what the line is. When code above it moves, `linecite sync` rewrites both the `#L310` fragment and the
|
|
40
|
+
`:310` in the link text; when the quoted code disappears, `linecite check` fails.
|
|
41
|
+
|
|
42
|
+
| command | what it does |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `linecite check` | reports drifted numbers, citations that no longer resolve, and number-only citations; exit 1 if any (CI) |
|
|
45
|
+
| `linecite sync` | rewrites drifted numbers in place (pre-commit), then reports what still needs a human |
|
|
46
|
+
| `linecite affected <rev>` | lists doc lines whose citations point into code changed since `<rev>` — the prose to re-read |
|
|
47
|
+
| `linecite locate <path> <line>` | proposes a citation for an existing `path:line` |
|
|
48
|
+
| `linecite list` / `where <spec>` | inspect what citations resolve to |
|
|
49
|
+
| `linecite audit` | traces existing number-only citations through git history: which ones already point at the wrong line |
|
|
50
|
+
| `linecite adopt [--write]` | converts number-only citations into links (markdown) or anchors (other files) |
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
pip install linecite # Python 3.11+, git
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Run it where your configuration is (see [Configuration](#configuration)), locally, as a
|
|
57
|
+
[pre-commit hook](#pre-commit), or in [GitHub Actions](#github-action).
|
|
58
|
+
|
|
59
|
+
## Existing docs: audit, then adopt
|
|
60
|
+
|
|
61
|
+
<!-- linecite-ignore-start -->
|
|
62
|
+
Docs you already have cite code as `orders.py:310` or `[orders.py:310](app/orders.py#L310)`. `linecite audit`
|
|
63
|
+
judges them without changing anything. For each citation it asks git when the doc line was written, reads line
|
|
64
|
+
310 of the code *as it was then*, and follows that line to today's code:
|
|
65
|
+
<!-- linecite-ignore-end -->
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
docs/design.md:14 ok orders.py:310 written against 9b2f41d0c3
|
|
69
|
+
docs/design.md:31 stale orders.py:118 -> orders.py:131 written against 4e1a9c2b07
|
|
70
|
+
docs/design.md:40 gone orders.py:77 `row.lock()` is gone; written against 4e1a9c2b07
|
|
71
|
+
docs/design.md:52 unknown orders.py:12 line 12 was blank: too little to identify; written against 4e1a9c2b07
|
|
72
|
+
|
|
73
|
+
number-only citations 4 · ok 1 · stale 1 · gone 1 · unknown 1 · unverifiable 0
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Today's line 310 is never the reference — it holds *some* code, so judging by it would pass numbers that are
|
|
77
|
+
already wrong. Where docs and code live in separate repositories, the code is read as of the doc line's
|
|
78
|
+
date (following the first-parent line of `HEAD`); lines not committed yet are read against the working tree.
|
|
79
|
+
The results are estimates: a doc line edited later (a typo fix) is dated by that edit. In a shallow clone (CI
|
|
80
|
+
checkouts often fetch one commit) lines older than the clone are reported `unverifiable` — fetch full history.
|
|
81
|
+
|
|
82
|
+
`linecite adopt` uses the same trace to propose conversions — a titled link in markdown, an anchor elsewhere —
|
|
83
|
+
with the number set to where the cited line is now. It writes nothing until `--write`; every proposal is
|
|
84
|
+
resolved before it is shown, so converted citations pass `check`. Citations that are gone, ambiguous or
|
|
85
|
+
undatable are listed and left alone.
|
|
86
|
+
|
|
87
|
+
## Citation forms
|
|
88
|
+
|
|
89
|
+
**Link** (markdown) — a link to a code file with a `#L<n>` or `#L<a>-L<b>` fragment and a title:
|
|
90
|
+
|
|
91
|
+
| title | meaning |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `"create_order: row.lock()"` | the one line in `create_order` containing `row.lock()` (whitespace-insensitive) |
|
|
94
|
+
| `"OrderService.create_order"` | the whole symbol, as a range |
|
|
95
|
+
| `": xs.reduce"` | no symbol — for languages without symbol support, cite by fragment only |
|
|
96
|
+
| ``"OrderService `return`#2"`` | backtick grammar: the 2nd hit; also `` `a` .. `b` `` for a range |
|
|
97
|
+
|
|
98
|
+
A link with a `#L` fragment but no title is reported as legacy: nothing records which code it meant.
|
|
99
|
+
Links inside fenced code blocks are examples and are not checked.
|
|
100
|
+
|
|
101
|
+
**Anchor** (hidden comment) — for HTML, for numbers in running prose, and for comments in code excerpts:
|
|
102
|
+
|
|
103
|
+
<!-- linecite-ignore-start -->
|
|
104
|
+
```md
|
|
105
|
+
the lock is taken at line 310<!--@ app/orders.py::create_order `order_by("product_id")` -->
|
|
106
|
+
```
|
|
107
|
+
<!-- linecite-ignore-end -->
|
|
108
|
+
|
|
109
|
+
The anchor sits right after the number it owns. Its spec grammar:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
[@<sha>:]<path>[::<symbol>] [<quote>[#n] [.. <quote>[#n]]]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- **path** — suffix of a tracked file; must match exactly one file.
|
|
116
|
+
- **symbol** — Python: qualified name or a unique suffix of one, or a module-level assignment. YAML: dotted key path.
|
|
117
|
+
- **quote** — `` `fragment` `` (widen to ``` `` ``` when the code holds a backtick) or `「fragment」`; `#n` picks the n-th hit.
|
|
118
|
+
- **`@sha:`** — pin to a commit for code that no longer exists; pinned specs may use plain integers.
|
|
119
|
+
- A bare `<!--@-->` marks a number that is not a code line.
|
|
120
|
+
|
|
121
|
+
**Examples** — docs that teach the syntax (a contributing guide, this README) wrap their examples in
|
|
122
|
+
ignore markers, each on a line of its own and outside code blocks (a marker shown in a code block is an
|
|
123
|
+
example itself); a marker that pairs with nothing fails `check`:
|
|
124
|
+
|
|
125
|
+
```md
|
|
126
|
+
<!-- linecite-ignore-start -->
|
|
127
|
+
Cite code as [orders.py:310](app/orders.py#L310 "create_order: row.lock()").
|
|
128
|
+
<!-- linecite-ignore-end -->
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
<!-- linecite-ignore-start -->
|
|
132
|
+
**Symbol reference** — `` `orders.py::OrderService.cancel` `` in prose fails `check` once the method is gone.
|
|
133
|
+
<!-- linecite-ignore-end -->
|
|
134
|
+
|
|
135
|
+
## Configuration
|
|
136
|
+
|
|
137
|
+
`.linecite.toml` (top-level keys) or `[tool.linecite]` in `pyproject.toml`:
|
|
138
|
+
|
|
139
|
+
<!-- linecite-ignore-start -->
|
|
140
|
+
```toml
|
|
141
|
+
code_root = "." # git repo of the cited code, relative to this file
|
|
142
|
+
docs = ["docs/**/*.md", "README.md"]
|
|
143
|
+
number_suffixes = ["`"] # text allowed between a number and its anchor: `orders.py:12`<!--@ … -->
|
|
144
|
+
legacy = "error" # number-only citations: "error" | "warn" | "off"
|
|
145
|
+
ignore_patterns = [] # regexes of regions to skip (the legacy scan also skips fenced code)
|
|
146
|
+
```
|
|
147
|
+
<!-- linecite-ignore-end -->
|
|
148
|
+
|
|
149
|
+
## pre-commit
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
repos:
|
|
153
|
+
- repo: https://github.com/gotoUSA/linecite
|
|
154
|
+
rev: v0.1.1
|
|
155
|
+
hooks:
|
|
156
|
+
- id: linecite-sync # or linecite-check, to report without rewriting
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Both hooks read every configured document on every commit, whatever is staged: a commit that touches only
|
|
160
|
+
code can move the lines a doc cites. `linecite-sync` rewrites drifted numbers and pre-commit stops the
|
|
161
|
+
commit so you can stage the rewrite; citations whose code is gone still fail it.
|
|
162
|
+
|
|
163
|
+
## GitHub Action
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
on: pull_request
|
|
167
|
+
permissions:
|
|
168
|
+
contents: read
|
|
169
|
+
pull-requests: write # for the comment
|
|
170
|
+
jobs:
|
|
171
|
+
linecite:
|
|
172
|
+
runs-on: ubuntu-latest
|
|
173
|
+
steps:
|
|
174
|
+
- uses: actions/checkout@v7
|
|
175
|
+
- uses: gotoUSA/linecite@v0.1.1
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The job fails when `linecite check` does. On a pull request, the action also comments with the doc lines
|
|
179
|
+
whose cited code the pull request changed — one comment per use of the action, rewritten on every push,
|
|
180
|
+
so a paragraph a later push made irrelevant drops off the list. The job summary always carries the full
|
|
181
|
+
report; a failure to comment (pull requests from forks get a read-only token) is a warning, not a failed
|
|
182
|
+
job.
|
|
183
|
+
|
|
184
|
+
| input | default | |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| `check` | `true` | run `linecite check` and fail on its findings |
|
|
187
|
+
| `comment` | `true` | comment on pull requests |
|
|
188
|
+
| `base` | the pull request's base commit | revision the changes are measured from |
|
|
189
|
+
| `working-directory` | `.` | where the configuration is |
|
|
190
|
+
| `config` | | configuration file, if not the default |
|
|
191
|
+
| `github-token` | `github.token` | needs `pull-requests: write` |
|
|
192
|
+
|
|
193
|
+
The output `affected` is the number of doc citations pointing into changed code.
|
|
194
|
+
|
|
195
|
+
**Shallow checkouts.** `actions/checkout` fetches a single commit by default. `affected` needs only the
|
|
196
|
+
base commit's files, which the action fetches itself; `audit` needs the history that dated every doc line,
|
|
197
|
+
so run it after `actions/checkout` with `fetch-depth: 0` — in a shallow clone it reports old lines as
|
|
198
|
+
`unverifiable` instead of guessing.
|
|
199
|
+
|
|
200
|
+
Other CI systems can post the same report: `linecite affected origin/main --format markdown --link-base
|
|
201
|
+
https://example.com/owner/repo/blob/<sha>` prints it as markdown with links to the doc lines.
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
MIT
|
linecite-0.1.1/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# linecite
|
|
2
|
+
|
|
3
|
+
Docs that cite code by line number (`orders.py:310`<!--@-->) go stale on the next commit that touches the lines above.
|
|
4
|
+
linecite lets a citation name the code it means — a **symbol and a quoted fragment** — derives the line number
|
|
5
|
+
from the source, and tells you which paragraphs to re-read after the code changes.
|
|
6
|
+
|
|
7
|
+
```md
|
|
8
|
+
Locks are taken in product order at [orders.py:310](app/orders.py#L310 "create_order: order_by(\"product_id\")").
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
That is an ordinary markdown link: it renders as `orders.py:310`<!--@-->, clicks through to the line on GitHub, and its
|
|
12
|
+
title says what the line is. When code above it moves, `linecite sync` rewrites both the `#L310` fragment and the
|
|
13
|
+
`:310` in the link text; when the quoted code disappears, `linecite check` fails.
|
|
14
|
+
|
|
15
|
+
| command | what it does |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `linecite check` | reports drifted numbers, citations that no longer resolve, and number-only citations; exit 1 if any (CI) |
|
|
18
|
+
| `linecite sync` | rewrites drifted numbers in place (pre-commit), then reports what still needs a human |
|
|
19
|
+
| `linecite affected <rev>` | lists doc lines whose citations point into code changed since `<rev>` — the prose to re-read |
|
|
20
|
+
| `linecite locate <path> <line>` | proposes a citation for an existing `path:line` |
|
|
21
|
+
| `linecite list` / `where <spec>` | inspect what citations resolve to |
|
|
22
|
+
| `linecite audit` | traces existing number-only citations through git history: which ones already point at the wrong line |
|
|
23
|
+
| `linecite adopt [--write]` | converts number-only citations into links (markdown) or anchors (other files) |
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pip install linecite # Python 3.11+, git
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Run it where your configuration is (see [Configuration](#configuration)), locally, as a
|
|
30
|
+
[pre-commit hook](#pre-commit), or in [GitHub Actions](#github-action).
|
|
31
|
+
|
|
32
|
+
## Existing docs: audit, then adopt
|
|
33
|
+
|
|
34
|
+
<!-- linecite-ignore-start -->
|
|
35
|
+
Docs you already have cite code as `orders.py:310` or `[orders.py:310](app/orders.py#L310)`. `linecite audit`
|
|
36
|
+
judges them without changing anything. For each citation it asks git when the doc line was written, reads line
|
|
37
|
+
310 of the code *as it was then*, and follows that line to today's code:
|
|
38
|
+
<!-- linecite-ignore-end -->
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
docs/design.md:14 ok orders.py:310 written against 9b2f41d0c3
|
|
42
|
+
docs/design.md:31 stale orders.py:118 -> orders.py:131 written against 4e1a9c2b07
|
|
43
|
+
docs/design.md:40 gone orders.py:77 `row.lock()` is gone; written against 4e1a9c2b07
|
|
44
|
+
docs/design.md:52 unknown orders.py:12 line 12 was blank: too little to identify; written against 4e1a9c2b07
|
|
45
|
+
|
|
46
|
+
number-only citations 4 · ok 1 · stale 1 · gone 1 · unknown 1 · unverifiable 0
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Today's line 310 is never the reference — it holds *some* code, so judging by it would pass numbers that are
|
|
50
|
+
already wrong. Where docs and code live in separate repositories, the code is read as of the doc line's
|
|
51
|
+
date (following the first-parent line of `HEAD`); lines not committed yet are read against the working tree.
|
|
52
|
+
The results are estimates: a doc line edited later (a typo fix) is dated by that edit. In a shallow clone (CI
|
|
53
|
+
checkouts often fetch one commit) lines older than the clone are reported `unverifiable` — fetch full history.
|
|
54
|
+
|
|
55
|
+
`linecite adopt` uses the same trace to propose conversions — a titled link in markdown, an anchor elsewhere —
|
|
56
|
+
with the number set to where the cited line is now. It writes nothing until `--write`; every proposal is
|
|
57
|
+
resolved before it is shown, so converted citations pass `check`. Citations that are gone, ambiguous or
|
|
58
|
+
undatable are listed and left alone.
|
|
59
|
+
|
|
60
|
+
## Citation forms
|
|
61
|
+
|
|
62
|
+
**Link** (markdown) — a link to a code file with a `#L<n>` or `#L<a>-L<b>` fragment and a title:
|
|
63
|
+
|
|
64
|
+
| title | meaning |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `"create_order: row.lock()"` | the one line in `create_order` containing `row.lock()` (whitespace-insensitive) |
|
|
67
|
+
| `"OrderService.create_order"` | the whole symbol, as a range |
|
|
68
|
+
| `": xs.reduce"` | no symbol — for languages without symbol support, cite by fragment only |
|
|
69
|
+
| ``"OrderService `return`#2"`` | backtick grammar: the 2nd hit; also `` `a` .. `b` `` for a range |
|
|
70
|
+
|
|
71
|
+
A link with a `#L` fragment but no title is reported as legacy: nothing records which code it meant.
|
|
72
|
+
Links inside fenced code blocks are examples and are not checked.
|
|
73
|
+
|
|
74
|
+
**Anchor** (hidden comment) — for HTML, for numbers in running prose, and for comments in code excerpts:
|
|
75
|
+
|
|
76
|
+
<!-- linecite-ignore-start -->
|
|
77
|
+
```md
|
|
78
|
+
the lock is taken at line 310<!--@ app/orders.py::create_order `order_by("product_id")` -->
|
|
79
|
+
```
|
|
80
|
+
<!-- linecite-ignore-end -->
|
|
81
|
+
|
|
82
|
+
The anchor sits right after the number it owns. Its spec grammar:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
[@<sha>:]<path>[::<symbol>] [<quote>[#n] [.. <quote>[#n]]]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- **path** — suffix of a tracked file; must match exactly one file.
|
|
89
|
+
- **symbol** — Python: qualified name or a unique suffix of one, or a module-level assignment. YAML: dotted key path.
|
|
90
|
+
- **quote** — `` `fragment` `` (widen to ``` `` ``` when the code holds a backtick) or `「fragment」`; `#n` picks the n-th hit.
|
|
91
|
+
- **`@sha:`** — pin to a commit for code that no longer exists; pinned specs may use plain integers.
|
|
92
|
+
- A bare `<!--@-->` marks a number that is not a code line.
|
|
93
|
+
|
|
94
|
+
**Examples** — docs that teach the syntax (a contributing guide, this README) wrap their examples in
|
|
95
|
+
ignore markers, each on a line of its own and outside code blocks (a marker shown in a code block is an
|
|
96
|
+
example itself); a marker that pairs with nothing fails `check`:
|
|
97
|
+
|
|
98
|
+
```md
|
|
99
|
+
<!-- linecite-ignore-start -->
|
|
100
|
+
Cite code as [orders.py:310](app/orders.py#L310 "create_order: row.lock()").
|
|
101
|
+
<!-- linecite-ignore-end -->
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
<!-- linecite-ignore-start -->
|
|
105
|
+
**Symbol reference** — `` `orders.py::OrderService.cancel` `` in prose fails `check` once the method is gone.
|
|
106
|
+
<!-- linecite-ignore-end -->
|
|
107
|
+
|
|
108
|
+
## Configuration
|
|
109
|
+
|
|
110
|
+
`.linecite.toml` (top-level keys) or `[tool.linecite]` in `pyproject.toml`:
|
|
111
|
+
|
|
112
|
+
<!-- linecite-ignore-start -->
|
|
113
|
+
```toml
|
|
114
|
+
code_root = "." # git repo of the cited code, relative to this file
|
|
115
|
+
docs = ["docs/**/*.md", "README.md"]
|
|
116
|
+
number_suffixes = ["`"] # text allowed between a number and its anchor: `orders.py:12`<!--@ … -->
|
|
117
|
+
legacy = "error" # number-only citations: "error" | "warn" | "off"
|
|
118
|
+
ignore_patterns = [] # regexes of regions to skip (the legacy scan also skips fenced code)
|
|
119
|
+
```
|
|
120
|
+
<!-- linecite-ignore-end -->
|
|
121
|
+
|
|
122
|
+
## pre-commit
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
repos:
|
|
126
|
+
- repo: https://github.com/gotoUSA/linecite
|
|
127
|
+
rev: v0.1.1
|
|
128
|
+
hooks:
|
|
129
|
+
- id: linecite-sync # or linecite-check, to report without rewriting
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Both hooks read every configured document on every commit, whatever is staged: a commit that touches only
|
|
133
|
+
code can move the lines a doc cites. `linecite-sync` rewrites drifted numbers and pre-commit stops the
|
|
134
|
+
commit so you can stage the rewrite; citations whose code is gone still fail it.
|
|
135
|
+
|
|
136
|
+
## GitHub Action
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
on: pull_request
|
|
140
|
+
permissions:
|
|
141
|
+
contents: read
|
|
142
|
+
pull-requests: write # for the comment
|
|
143
|
+
jobs:
|
|
144
|
+
linecite:
|
|
145
|
+
runs-on: ubuntu-latest
|
|
146
|
+
steps:
|
|
147
|
+
- uses: actions/checkout@v7
|
|
148
|
+
- uses: gotoUSA/linecite@v0.1.1
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The job fails when `linecite check` does. On a pull request, the action also comments with the doc lines
|
|
152
|
+
whose cited code the pull request changed — one comment per use of the action, rewritten on every push,
|
|
153
|
+
so a paragraph a later push made irrelevant drops off the list. The job summary always carries the full
|
|
154
|
+
report; a failure to comment (pull requests from forks get a read-only token) is a warning, not a failed
|
|
155
|
+
job.
|
|
156
|
+
|
|
157
|
+
| input | default | |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `check` | `true` | run `linecite check` and fail on its findings |
|
|
160
|
+
| `comment` | `true` | comment on pull requests |
|
|
161
|
+
| `base` | the pull request's base commit | revision the changes are measured from |
|
|
162
|
+
| `working-directory` | `.` | where the configuration is |
|
|
163
|
+
| `config` | | configuration file, if not the default |
|
|
164
|
+
| `github-token` | `github.token` | needs `pull-requests: write` |
|
|
165
|
+
|
|
166
|
+
The output `affected` is the number of doc citations pointing into changed code.
|
|
167
|
+
|
|
168
|
+
**Shallow checkouts.** `actions/checkout` fetches a single commit by default. `affected` needs only the
|
|
169
|
+
base commit's files, which the action fetches itself; `audit` needs the history that dated every doc line,
|
|
170
|
+
so run it after `actions/checkout` with `fetch-depth: 0` — in a shallow clone it reports old lines as
|
|
171
|
+
`unverifiable` instead of guessing.
|
|
172
|
+
|
|
173
|
+
Other CI systems can post the same report: `linecite affected origin/main --format markdown --link-base
|
|
174
|
+
https://example.com/owner/repo/blob/<sha>` prints it as markdown with links to the doc lines.
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT
|