git-reattribute 0.1.0__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.
Files changed (36) hide show
  1. git_reattribute-0.1.0/.github/ISSUE_TEMPLATE/bug_report.md +35 -0
  2. git_reattribute-0.1.0/.github/ISSUE_TEMPLATE/config.yml +1 -0
  3. git_reattribute-0.1.0/.github/ISSUE_TEMPLATE/feature_request.md +14 -0
  4. git_reattribute-0.1.0/.github/dependabot.yml +11 -0
  5. git_reattribute-0.1.0/.github/pull_request_template.md +8 -0
  6. git_reattribute-0.1.0/.github/workflows/ci.yml +38 -0
  7. git_reattribute-0.1.0/.github/workflows/publish.yml +41 -0
  8. git_reattribute-0.1.0/.gitignore +7 -0
  9. git_reattribute-0.1.0/CHANGELOG.md +19 -0
  10. git_reattribute-0.1.0/CONTRIBUTING.md +49 -0
  11. git_reattribute-0.1.0/LICENSE +21 -0
  12. git_reattribute-0.1.0/PKG-INFO +315 -0
  13. git_reattribute-0.1.0/README.md +287 -0
  14. git_reattribute-0.1.0/SECURITY.md +39 -0
  15. git_reattribute-0.1.0/pyproject.toml +45 -0
  16. git_reattribute-0.1.0/src/git_reattribute/__init__.py +1 -0
  17. git_reattribute-0.1.0/src/git_reattribute/__main__.py +4 -0
  18. git_reattribute-0.1.0/src/git_reattribute/branches.py +55 -0
  19. git_reattribute-0.1.0/src/git_reattribute/cli.py +314 -0
  20. git_reattribute-0.1.0/src/git_reattribute/contributors.py +81 -0
  21. git_reattribute-0.1.0/src/git_reattribute/errors.py +61 -0
  22. git_reattribute-0.1.0/src/git_reattribute/gitwrapper.py +47 -0
  23. git_reattribute-0.1.0/src/git_reattribute/identities.py +32 -0
  24. git_reattribute-0.1.0/src/git_reattribute/models.py +73 -0
  25. git_reattribute-0.1.0/src/git_reattribute/push.py +30 -0
  26. git_reattribute-0.1.0/src/git_reattribute/repository.py +38 -0
  27. git_reattribute-0.1.0/src/git_reattribute/rewrite.py +155 -0
  28. git_reattribute-0.1.0/src/git_reattribute/verify.py +52 -0
  29. git_reattribute-0.1.0/tests/conftest.py +108 -0
  30. git_reattribute-0.1.0/tests/test_branches.py +17 -0
  31. git_reattribute-0.1.0/tests/test_contributors.py +44 -0
  32. git_reattribute-0.1.0/tests/test_identities.py +32 -0
  33. git_reattribute-0.1.0/tests/test_push.py +31 -0
  34. git_reattribute-0.1.0/tests/test_repository.py +49 -0
  35. git_reattribute-0.1.0/tests/test_rewrite.py +129 -0
  36. git_reattribute-0.1.0/tests/test_verify.py +35 -0
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: Bug report
3
+ about: Something didn't work as expected
4
+ title: ""
5
+ labels: bug
6
+ ---
7
+
8
+ **What happened**
9
+
10
+ A clear description of the problem.
11
+
12
+ **Command run**
13
+
14
+ ```
15
+ git-reattribute ...
16
+ ```
17
+
18
+ **Expected behavior**
19
+
20
+ **Environment**
21
+
22
+ - git-reattribute version: (`git-reattribute --version`)
23
+ - Git version: (`git --version`)
24
+ - OS:
25
+ - Python version: (`python --version`)
26
+
27
+ **Repository context (if relevant)**
28
+
29
+ - Approximate branch/history size:
30
+ - Any shallow clone, submodules, signed commits, or Git LFS involved?
31
+
32
+ **Additional context**
33
+
34
+ Logs, `--verbose` output, or anything else that helps reproduce the issue.
35
+ Please redact any sensitive commit content, emails, or tokens before pasting.
@@ -0,0 +1 @@
1
+ blank_issues_enabled: true
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for git-reattribute
4
+ title: ""
5
+ labels: enhancement
6
+ ---
7
+
8
+ **What problem would this solve?**
9
+
10
+ **Proposed solution**
11
+
12
+ **Alternatives considered**
13
+
14
+ **Additional context**
@@ -0,0 +1,11 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "pip"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+
8
+ - package-ecosystem: "github-actions"
9
+ directory: "/"
10
+ schedule:
11
+ interval: "weekly"
@@ -0,0 +1,8 @@
1
+ ## What does this change?
2
+
3
+ ## Why?
4
+
5
+ ## Testing
6
+
7
+ - [ ] `pytest` passes locally
8
+ - [ ] Manually verified against a scratch repository (for changes touching `rewrite.py`, `push.py`, or `cli.py`)
@@ -0,0 +1,38 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ os: [ubuntu-latest, macos-latest, windows-latest]
15
+ python-version: ["3.10", "3.11", "3.12"]
16
+ runs-on: ${{ matrix.os }}
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Configure git identity for tests
25
+ run: |
26
+ git config --global user.name "CI"
27
+ git config --global user.email "ci@example.com"
28
+
29
+ - name: Install package with dev dependencies
30
+ run: python -m pip install -e ".[dev]"
31
+
32
+ - name: Run tests
33
+ run: pytest -q
34
+
35
+ - name: Verify console script
36
+ run: |
37
+ git-reattribute --version
38
+ git-reattribute --help
@@ -0,0 +1,41 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+
17
+ - name: Install build tooling
18
+ run: python -m pip install build
19
+
20
+ - name: Build sdist and wheel
21
+ run: python -m build
22
+
23
+ - uses: actions/upload-artifact@v4
24
+ with:
25
+ name: dist
26
+ path: dist/
27
+
28
+ publish:
29
+ needs: build
30
+ runs-on: ubuntu-latest
31
+ environment: pypi
32
+ permissions:
33
+ id-token: write # required for PyPI trusted publishing (OIDC)
34
+ steps:
35
+ - uses: actions/download-artifact@v4
36
+ with:
37
+ name: dist
38
+ path: dist/
39
+
40
+ - name: Publish to PyPI
41
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ .venv/
5
+ *.egg-info/
6
+ build/
7
+ dist/
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ ## [0.1.0] - Unreleased
6
+
7
+ ### Added
8
+
9
+ - Initial MVP: interactive and non-interactive contributor-identity
10
+ replacement using `git-filter-repo`.
11
+ - Branch discovery and selection.
12
+ - Contributor discovery (author/committer, raw identity fields, no mailmap).
13
+ - Backup ref creation before every rewrite.
14
+ - Author/committer/both replacement scope via `--identity-type`.
15
+ - Co-authored-by trailer stripping for the replaced identity.
16
+ - Post-rewrite verification.
17
+ - `--force-with-lease` push support.
18
+ - Dry-run mode.
19
+ - Signed-commit and shallow-repository warnings.
@@ -0,0 +1,49 @@
1
+ # Contributing to Git Reattribute
2
+
3
+ ## Development setup
4
+
5
+ ```bash
6
+ git clone https://github.com/drk1rd/git-reattribute.git
7
+ cd git-reattribute
8
+ python -m venv .venv
9
+ source .venv/bin/activate # .venv\Scripts\activate on Windows
10
+ pip install -e ".[dev]"
11
+ ```
12
+
13
+ ## Running tests
14
+
15
+ ```bash
16
+ pytest
17
+ ```
18
+
19
+ Tests build temporary Git repositories per test case (see `tests/conftest.py`)
20
+ — nothing is ever run against a real or shared repository. If you're adding
21
+ a test that exercises `RewriteEngine`, prefer extending the existing
22
+ `basic_repo` fixture's history over hand-rolling a new one, unless your test
23
+ needs a distinct topology (e.g. `mailmap_repo`, `bare_remote`).
24
+
25
+ ## Making changes
26
+
27
+ - Keep runtime dependencies minimal — this is meant to stay a small, focused
28
+ CLI. If you're adding a dependency, explain why an existing one (or the
29
+ standard library) doesn't cover it.
30
+ - History-rewrite logic lives in `rewrite.py` and goes through
31
+ `git-filter-repo`; don't hand-roll Git object rewriting.
32
+ - Any change to `rewrite.py`, `push.py`, or `cli.py`'s confirmation/warning
33
+ flow should be manually verified against a scratch repository in addition
34
+ to the test suite, since these paths are destructive by design.
35
+ - Update `README.md` and `CHANGELOG.md` alongside behavior changes — the
36
+ README is expected to match what's actually implemented.
37
+
38
+ ## Submitting a pull request
39
+
40
+ 1. Fork the repo and create a branch from `main`.
41
+ 2. Make your change, with tests.
42
+ 3. Run `pytest` locally.
43
+ 4. Open a PR describing what changed and why (the PR template will prompt
44
+ you for this).
45
+
46
+ ## Reporting bugs / requesting features
47
+
48
+ Use the GitHub issue templates. For anything security-related, see
49
+ `SECURITY.md` instead of opening a public issue.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 git-reattribute contributors
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.
@@ -0,0 +1,315 @@
1
+ Metadata-Version: 2.5
2
+ Name: git-reattribute
3
+ Version: 0.1.0
4
+ Summary: Interactively replace one Git contributor identity with another, safely.
5
+ Project-URL: Homepage, https://github.com/drk1rd/git-reattribute
6
+ Project-URL: Issues, https://github.com/drk1rd/git-reattribute/issues
7
+ Author: git-reattribute contributors
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: attribution,cli,git,history,rewrite
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Version Control :: Git
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: git-filter-repo>=2.38
22
+ Requires-Dist: questionary>=2.0
23
+ Requires-Dist: typer>=0.12
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
26
+ Requires-Dist: pytest>=8.0; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Git Reattribute
30
+
31
+ [![CI](https://github.com/drk1rd/git-reattribute/actions/workflows/ci.yml/badge.svg)](https://github.com/drk1rd/git-reattribute/actions/workflows/ci.yml)
32
+ [![PyPI](https://img.shields.io/pypi/v/git-reattribute.svg)](https://pypi.org/project/git-reattribute/)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
34
+
35
+ A small, cross-platform CLI for replacing one Git identity with another
36
+ across a repository's history, safely — without requiring you to hand-write
37
+ a `git-filter-repo` invocation.
38
+
39
+ > **⚠️ This tool rewrites Git history.** Rewriting history changes commit
40
+ > SHAs, invalidates existing signatures on rewritten commits, and requires a
41
+ > force push to update any already-published branch. A force push can
42
+ > disrupt collaborators and invalidate their existing clones. Only use this
43
+ > on repositories and history you are authorized to modify. This tool does
44
+ > not modify GitHub (or any other host's) account data or contributor
45
+ > database directly — hosts may independently recompute contributor
46
+ > attribution from the resulting commit metadata.
47
+
48
+ ## Why?
49
+
50
+ A Git "identity" isn't always a person — it can be a placeholder, a bot
51
+ account, a shared machine account, or an AI agent that paired on the work.
52
+ Correcting a mis-attributed one usually means hand-writing a
53
+ `git-filter-repo` invocation. Git Reattribute turns that into one guided
54
+ command.
55
+
56
+ ## Features
57
+
58
+ - Interactive branch, contributor, and replacement-identity selection.
59
+ - Non-interactive / scriptable usage via flags.
60
+ - Replaces author, committer, or both (default: both).
61
+ - Removes matching `Co-authored-by:` trailers for the replaced identity.
62
+ - Dry-run mode — preview the impact with no changes made.
63
+ - Creates a backup ref before every rewrite.
64
+ - Mandatory post-rewrite verification.
65
+ - `--force-with-lease` push only, never `--force`, never silent.
66
+ - Warns about signed commits and shallow repositories before rewriting.
67
+
68
+ ## Requirements
69
+
70
+ - Python 3.10+
71
+ - Git 2.x
72
+ - `git-filter-repo` — installed automatically as a dependency when you
73
+ `pip install git-reattribute`; you don't need to install it separately.
74
+
75
+ ## Installation
76
+
77
+ ```bash
78
+ python -m pip install git-reattribute
79
+ ```
80
+
81
+ ## Quick Start
82
+
83
+ ```bash
84
+ cd your-repo
85
+ git-reattribute --version
86
+ git-reattribute --help
87
+ git-reattribute
88
+ ```
89
+
90
+ ## Example
91
+
92
+ ```text
93
+ $ git-reattribute
94
+
95
+ Git Reattribute
96
+
97
+ Repository: /home/alice/projects/example
98
+ Remote: origin
99
+ Branch: main
100
+
101
+ Contributors on main:
102
+
103
+ 1. Claude <claude@example.com> 47 commits
104
+ 2. Alice <alice@example.com> 18 commits
105
+ 3. Bob <bob@example.com> 6 commits
106
+
107
+ Select contributor to replace: Claude <claude@example.com> 47 commits
108
+
109
+ Replace with:
110
+ Current Git identity
111
+ Alice <alice@example.com>
112
+ Bob <bob@example.com>
113
+ Enter a custom identity
114
+
115
+ Select replacement: Alice <alice@example.com>
116
+
117
+ Summary
118
+ -------
119
+ Branch: main
120
+ From: Claude <claude@example.com>
121
+ To: Alice <alice@example.com>
122
+ Author commits affected: 47
123
+ Committer commits affected: 47
124
+ Co-authored-by trailers to remove: 3
125
+
126
+ WARNING: This operation rewrites Git history.
127
+
128
+ Type REWRITE to continue: REWRITE
129
+
130
+ History rewrite completed.
131
+
132
+ Recovery reference:
133
+ refs/backup/git-reattribute/2026-08-22T10-30-00
134
+
135
+ To restore the original branch:
136
+ git reset --hard refs/backup/git-reattribute/2026-08-22T10-30-00
137
+
138
+ Keep this reference until you have verified the rewritten history.
139
+
140
+ Verification
141
+
142
+ Old identity commits before: 94
143
+ Remaining old identity commits: 0
144
+ New identity commits: 94
145
+ Remaining Co-authored-by trailers: 0
146
+
147
+ Verification: PASS
148
+
149
+ Push rewritten branch to origin with --force-with-lease? [y/N]
150
+ ```
151
+
152
+ ## Interactive Usage
153
+
154
+ Running `git-reattribute` with no flags walks you through: select a branch,
155
+ see its contributors, pick who to replace, pick the replacement, preview the
156
+ change, then type `REWRITE` to confirm.
157
+
158
+ ## Non-Interactive Usage
159
+
160
+ ```bash
161
+ git-reattribute \
162
+ --branch main \
163
+ --from-email claude@example.com \
164
+ --to-name Alice \
165
+ --to-email alice@example.com \
166
+ --identity-type both \
167
+ --yes --push
168
+ ```
169
+
170
+ `--yes` does not imply `--push` — pass both explicitly when you want a
171
+ scripted rewrite-and-push.
172
+
173
+ ## Dry Run
174
+
175
+ ```bash
176
+ git-reattribute --branch main --from-email claude@example.com --to-current-user --dry-run
177
+ ```
178
+
179
+ Shows affected author/committer commit counts and any `Co-authored-by:`
180
+ trailers that would be removed. No history or remote is touched.
181
+
182
+ ## Branches
183
+
184
+ Local branches are listed and selectable. Remote-tracking branches are not
185
+ rewritten directly — check out a local branch first. The rewrite scope is
186
+ the **entire reachable history of the selected branch** — every commit an
187
+ ancestor of that branch's tip, not just commits made while that branch was
188
+ checked out.
189
+
190
+ ## Authors vs Committers
191
+
192
+ Every commit has an author and a committer, and they can differ. Use
193
+ `--identity-type author`, `--identity-type committer`, or the default
194
+ `--identity-type both` to control which field(s) get rewritten. The
195
+ interactive UI shows both roles whenever they differ for a given contributor.
196
+
197
+ ## Co-authored-by Trailers
198
+
199
+ When an identity (for example an AI agent that paired with the human author)
200
+ appears as a `Co-authored-by:` trailer in a commit message, that trailer is
201
+ **removed** (not replaced) for the identity being replaced, by default. This
202
+ matters because rewriting only the author field of a commit like:
203
+
204
+ ```text
205
+ Author: Claude <claude@example.com>
206
+
207
+ Co-authored-by: Claude <claude@example.com>
208
+ ```
209
+
210
+ would otherwise leave the old identity behind in the message body even
211
+ after the commit object's author is fixed.
212
+
213
+ This runs alongside the author/committer rewrite for the same source
214
+ identity — there is no separate replacement target for trailers, and the
215
+ trailer is deleted, not rewritten to a new co-author. Disable it with
216
+ `--no-strip-coauthor-trailers`.
217
+
218
+ **Matching rule:** a trailer line matches when it starts with
219
+ `Co-authored-by:` (case-insensitive) followed by the source identity's name
220
+ and `<email>`, with any amount of whitespace tolerated around the colon,
221
+ name, and angle brackets — but the name and email themselves must match the
222
+ source identity's recorded name/email (case-insensitively), not a partial or
223
+ fuzzy match. Only matching trailer lines are removed; other trailers (e.g.
224
+ `Signed-off-by:`) and body text that merely mentions the same name are left
225
+ untouched.
226
+
227
+ ## History Rewriting Warning
228
+
229
+ This tool rewrites Git history using [`git-filter-repo`](https://github.com/newren/git-filter-repo).
230
+ Two different `--force` flags matter here, and they are not the same thing:
231
+
232
+ - **`git-filter-repo --force`** (used internally, always) only permits the
233
+ *local* rewrite to run on a non-fresh-clone repository. `git-filter-repo`
234
+ normally refuses to touch anything but a fresh clone as a generic safety
235
+ net; this tool creates its own backup ref (see **Recovery** below) *before*
236
+ that rewrite ever runs, so that backup is the safety net instead, and the
237
+ `--force` flag just lets the local rewrite proceed.
238
+ - **`git push --force`** is never used by this tool, anywhere. Publishing a
239
+ rewritten branch always uses `git push --force-with-lease` (see
240
+ **Push Behavior**), and only after you explicitly confirm it.
241
+
242
+ Contributor discovery uses Git's raw `%an`/`%ae`/`%cn`/`%ce` fields, not
243
+ mailmap-resolved fields — a repository's `.mailmap` is intentionally ignored
244
+ so what you see and rewrite always matches the actual commit-object bytes.
245
+
246
+ Signed commits lose their signatures when rewritten — a warning is shown
247
+ before you confirm. Merge commits are rewritten natively by `git-filter-repo`
248
+ as part of a full-branch rewrite; this requires no special handling in v1
249
+ since there is no commit-range filtering yet.
250
+
251
+ ## Push Behavior
252
+
253
+ Pushing always uses `git push --force-with-lease`, never `git push --force`.
254
+ Nothing is pushed unless you pass `--push` (scripted) or confirm the
255
+ interactive prompt. If the remote branch changed since the rewrite began,
256
+ the push is rejected and nothing is retried automatically.
257
+
258
+ ## Recovery
259
+
260
+ Before every rewrite — and before `git-filter-repo` is ever invoked — a
261
+ backup ref is created pointing at the original branch tip:
262
+
263
+ ```
264
+ refs/backup/git-reattribute/<timestamp>
265
+ ```
266
+
267
+ The rewrite is scoped (via `--refs <branch>`) to only the selected branch,
268
+ so the backup ref itself is never touched or rewritten by the same
269
+ operation. It is never deleted automatically. After a rewrite, the tool
270
+ prints the exact recovery command:
271
+
272
+ ```text
273
+ Recovery reference:
274
+ refs/backup/git-reattribute/2026-08-22T10-30-00
275
+
276
+ To restore the original branch:
277
+ git reset --hard refs/backup/git-reattribute/2026-08-22T10-30-00
278
+ ```
279
+
280
+ Keep the backup ref until you've verified the rewritten history (and pushed,
281
+ if applicable) — then it's safe to delete with
282
+ `git update-ref -d refs/backup/git-reattribute/<timestamp>`.
283
+
284
+ ## Limitations
285
+
286
+ - Shallow clones may show an incomplete contributor list and rewrite scope;
287
+ run `git fetch --unshallow` first.
288
+ - Submodules, Git LFS objects, and annotated tag signatures are not
289
+ specially handled in v1.
290
+ - Branch-protection or remote-policy push rejections are surfaced as-is;
291
+ this tool never attempts to bypass them.
292
+ - One source identity → one target identity per run.
293
+
294
+ ## Development
295
+
296
+ ```bash
297
+ python -m pip install -e ".[dev]"
298
+ ```
299
+
300
+ ## Testing
301
+
302
+ ```bash
303
+ pytest
304
+ ```
305
+
306
+ Tests build temporary Git repositories per-test; nothing is ever run against
307
+ a real/shared repository.
308
+
309
+ ## Release Process
310
+
311
+ See `CHANGELOG.md`. Versioning follows SemVer (`MAJOR.MINOR.PATCH`).
312
+
313
+ ## License
314
+
315
+ MIT — see `LICENSE`.