gitcalver 20260418.5__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.
@@ -0,0 +1,71 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-24.04
10
+ strategy:
11
+ matrix:
12
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
13
+ steps:
14
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
15
+ with:
16
+ fetch-depth: 0
17
+ - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
18
+ with:
19
+ enable-cache: true
20
+ cache-suffix: ${{ matrix.python-version }}
21
+ - run: uv python install ${{ matrix.python-version }}
22
+ - run: make test
23
+ env:
24
+ UV_PYTHON: ${{ matrix.python-version }}
25
+
26
+ lint:
27
+ runs-on: ubuntu-24.04
28
+ steps:
29
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
30
+ with:
31
+ fetch-depth: 0
32
+ - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
33
+ with:
34
+ enable-cache: true
35
+ - run: uv python install 3.14
36
+ - run: make lint
37
+
38
+ build:
39
+ name: Build distribution
40
+ needs: [test, lint]
41
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
42
+ runs-on: ubuntu-24.04
43
+ steps:
44
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
45
+ with:
46
+ fetch-depth: 0
47
+ - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
48
+ with:
49
+ enable-cache: true
50
+ - run: uv python install 3.14
51
+ - run: uv build
52
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
53
+ with:
54
+ name: python-package-distributions
55
+ path: dist/
56
+
57
+ publish-to-pypi:
58
+ name: Publish to PyPI
59
+ needs: build
60
+ runs-on: ubuntu-24.04
61
+ environment:
62
+ name: pypi
63
+ url: https://pypi.org/p/gitcalver
64
+ permissions:
65
+ id-token: write
66
+ steps:
67
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
68
+ with:
69
+ name: python-package-distributions
70
+ path: dist/
71
+ - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
@@ -0,0 +1,10 @@
1
+ # Copyright © 2026 Michael Shields
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ __pycache__/
5
+ *.egg-info/
6
+ .coverage
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .venv/
10
+ dist/
@@ -0,0 +1,41 @@
1
+ # Copyright © 2026 Michael Shields
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ # Match requires-python: support all non-EOL Python versions.
5
+ target-version = "py310"
6
+
7
+ [lint]
8
+ select = [
9
+ "ALL",
10
+ ]
11
+
12
+ ignore = [
13
+ "ANN401", # typing.Any
14
+ "C90", # Cyclomatic complexity
15
+ "D", # pydocstyle (disable docstring rules)
16
+ "PLR09", # "Too many" branches, arguments, etc.
17
+ "S603", # subprocess output
18
+ "S607", # subprocess using path
19
+ "RUF001", # ambiguous Unicode character in string
20
+ "RUF002", # ambiguous Unicode character in docstring
21
+ "RUF003", # ambiguous Unicode character in comment
22
+ "A002", # shadowing builtins (dir, format) -- intentional API design
23
+ "COM812", # trailing comma -- conflicts with formatter
24
+ "FBT", # boolean arguments -- acceptable for internal functions
25
+ ]
26
+
27
+ [lint.per-file-ignores]
28
+ "src/gitcalver/_hatch_hooks.py" = [
29
+ "PLC0415", # import not at top level -- lazy import for plugin registration
30
+ ]
31
+ "src/gitcalver/cli.py" = [
32
+ "T201", # print() -- CLI entry point
33
+ ]
34
+ "tests/**" = [
35
+ "S101", # assert
36
+ "PLR2004", # magic values
37
+ "INP001", # implicit namespace package (tests/ is intentionally not a package)
38
+ ]
39
+
40
+ [lint.isort]
41
+ known-local-folder = ["_helpers"]
@@ -0,0 +1,19 @@
1
+ Copyright © 2026 Michael Shields
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,15 @@
1
+ .PHONY: sync test lint fmt
2
+
3
+ sync:
4
+ uv sync --frozen
5
+
6
+ test: sync
7
+ uv run pytest
8
+
9
+ lint: sync
10
+ uv run ruff check
11
+ uv run ruff format --check
12
+ uv run ty check
13
+
14
+ fmt: sync
15
+ uv run ruff format
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.4
2
+ Name: gitcalver
3
+ Version: 20260418.5
4
+ Summary: Deterministic calendar versioning from git history
5
+ Project-URL: Homepage, https://gitcalver.org
6
+ Project-URL: Source, https://github.com/gitcalver/python
7
+ Project-URL: Issues, https://github.com/gitcalver/python/issues
8
+ Author-email: Michael Shields <shields@gitcalver.org>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: calendar-versioning,calver,git,versioning
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Topic :: Software Development :: Build Tools
16
+ Classifier: Topic :: Software Development :: Version Control :: Git
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+
20
+ # gitcalver
21
+
22
+ A Python implementation of [GitCalVer](https://gitcalver.org), which derives
23
+ calendar-based version numbers from git history.
24
+
25
+ Each commit on the default branch gets a unique, strictly increasing version of
26
+ the form `YYYYMMDD.N`, where `N` is the number of commits on that UTC date.
27
+
28
+ See the [GitCalVer specification](https://gitcalver.org) for full details.
29
+
30
+ ## Installation
31
+
32
+ ```sh
33
+ uv add gitcalver
34
+ # or
35
+ pip install gitcalver
36
+ ```
37
+
38
+ See [Requirements](#requirements) below.
39
+
40
+ ## CLI usage
41
+
42
+ ```
43
+ gitcalver [OPTIONS] [REVISION | VERSION]
44
+ ```
45
+
46
+ With no arguments, prints the version for `HEAD`:
47
+
48
+ ```sh
49
+ $ gitcalver
50
+ 20260411.3
51
+ ```
52
+
53
+ Pass a revision to compute its version:
54
+
55
+ ```sh
56
+ $ gitcalver HEAD~1
57
+ 20260411.2
58
+ ```
59
+
60
+ ### Version prefix
61
+
62
+ Use `--prefix` to prepend a literal string:
63
+
64
+ | Use case | Command | Example output |
65
+ |----------|------------------------------|------------------|
66
+ | Default | `gitcalver` | `20260411.3` |
67
+ | SemVer | `gitcalver --prefix "0."` | `0.20260411.3` |
68
+ | Go | `gitcalver --prefix "v0."` | `v0.20260411.3` |
69
+
70
+ ### Dirty workspace
71
+
72
+ By default, `gitcalver` exits with status 2 if the workspace has uncommitted
73
+ changes. Use `--dirty STRING` to produce a version instead; the output will
74
+ include the given string and a short commit hash
75
+ (e.g. `--dirty "-dirty"` produces `20260411.3-dirty.abc1234`).
76
+
77
+ Use `--no-dirty-hash` with `--dirty` to suppress the hash suffix.
78
+ Use `--no-dirty` to explicitly refuse dirty versions (overrides `--dirty`).
79
+
80
+ Dirty versions are a convenience and are not necessarily unique.
81
+
82
+ ### Reverse lookup
83
+
84
+ Pass a version number to get the corresponding commit hash:
85
+
86
+ ```sh
87
+ $ gitcalver 20260411.3
88
+ a1b2c3d4e5f6...
89
+
90
+ $ gitcalver --short --prefix "0." 0.20260411.3
91
+ a1b2c3d
92
+ ```
93
+
94
+ If the version was generated with `--prefix`, pass the same `--prefix` for
95
+ reverse lookup. Dirty versions cannot be reversed.
96
+
97
+ ### Options
98
+
99
+ | Option | Description |
100
+ |---------------------|------------------------------------------------|
101
+ | `--prefix PREFIX` | Literal string prepended to version |
102
+ | `--dirty STRING` | Enable dirty versions; append `STRING.HASH` |
103
+ | `--no-dirty` | Refuse dirty versions (overrides `--dirty`) |
104
+ | `--no-dirty-hash` | Suppress `.HASH` suffix (requires `--dirty`) |
105
+ | `--branch BRANCH` | Base branch name; overrides auto-detection. This is the branch versions are minted on, not the branch you are working on. |
106
+ | `--short` | Output short commit hash (reverse lookup mode) |
107
+ | `--help` | Show help |
108
+
109
+ ### Exit codes
110
+
111
+ | Code | Meaning |
112
+ |------|----------------------------------------|
113
+ | 0 | Success |
114
+ | 1 | Error (not a git repo, no commits, non-monotonic dates, shallow clone) |
115
+ | 2 | Dirty workspace or off default branch (without `--dirty`) |
116
+ | 3 | Cannot trace to default branch |
117
+
118
+ ## Python API
119
+
120
+ ```python
121
+ import gitcalver
122
+
123
+ # Forward: compute a version for HEAD (or a specific revision).
124
+ version = gitcalver.get_version(repo="/path/to/repo")
125
+ # e.g. "20260411.3"
126
+
127
+ version = gitcalver.get_version(
128
+ repo="/path/to/repo",
129
+ revision="HEAD~1",
130
+ prefix="v0.",
131
+ dirty="-dirty",
132
+ )
133
+
134
+ # Reverse: resolve a version back to a commit hash.
135
+ commit = gitcalver.find_commit("20260411.3", repo="/path/to/repo")
136
+
137
+ # If the version was generated with --prefix, pass the same prefix:
138
+ commit = gitcalver.find_commit(
139
+ "v0.20260411.3", prefix="v0.", repo="/path/to/repo"
140
+ )
141
+ ```
142
+
143
+ Errors are raised as `gitcalver.ExitError`, which carries a `code` attribute
144
+ matching the CLI exit codes above.
145
+
146
+ ## Hatch plugin
147
+
148
+ `gitcalver` ships a [Hatch](https://hatch.pypa.io/) version source plugin. To
149
+ use it in `pyproject.toml`:
150
+
151
+ ```toml
152
+ [build-system]
153
+ requires = ["hatchling", "gitcalver"]
154
+ build-backend = "hatchling.build"
155
+
156
+ [tool.hatch.version]
157
+ source = "gitcalver"
158
+ # Optional:
159
+ # prefix = "0."
160
+ # dirty = "-dirty"
161
+ # no-dirty-hash = true
162
+ # branch = "main"
163
+ ```
164
+
165
+ ## Requirements
166
+
167
+ - Python 3.10+
168
+ - `git` on `$PATH`
169
+ - Full commit history (shallow clones made with `--depth` are rejected; partial
170
+ clones made with `--filter=blob:none` are fine)
171
+
172
+ ## License
173
+
174
+ MIT
@@ -0,0 +1,155 @@
1
+ # gitcalver
2
+
3
+ A Python implementation of [GitCalVer](https://gitcalver.org), which derives
4
+ calendar-based version numbers from git history.
5
+
6
+ Each commit on the default branch gets a unique, strictly increasing version of
7
+ the form `YYYYMMDD.N`, where `N` is the number of commits on that UTC date.
8
+
9
+ See the [GitCalVer specification](https://gitcalver.org) for full details.
10
+
11
+ ## Installation
12
+
13
+ ```sh
14
+ uv add gitcalver
15
+ # or
16
+ pip install gitcalver
17
+ ```
18
+
19
+ See [Requirements](#requirements) below.
20
+
21
+ ## CLI usage
22
+
23
+ ```
24
+ gitcalver [OPTIONS] [REVISION | VERSION]
25
+ ```
26
+
27
+ With no arguments, prints the version for `HEAD`:
28
+
29
+ ```sh
30
+ $ gitcalver
31
+ 20260411.3
32
+ ```
33
+
34
+ Pass a revision to compute its version:
35
+
36
+ ```sh
37
+ $ gitcalver HEAD~1
38
+ 20260411.2
39
+ ```
40
+
41
+ ### Version prefix
42
+
43
+ Use `--prefix` to prepend a literal string:
44
+
45
+ | Use case | Command | Example output |
46
+ |----------|------------------------------|------------------|
47
+ | Default | `gitcalver` | `20260411.3` |
48
+ | SemVer | `gitcalver --prefix "0."` | `0.20260411.3` |
49
+ | Go | `gitcalver --prefix "v0."` | `v0.20260411.3` |
50
+
51
+ ### Dirty workspace
52
+
53
+ By default, `gitcalver` exits with status 2 if the workspace has uncommitted
54
+ changes. Use `--dirty STRING` to produce a version instead; the output will
55
+ include the given string and a short commit hash
56
+ (e.g. `--dirty "-dirty"` produces `20260411.3-dirty.abc1234`).
57
+
58
+ Use `--no-dirty-hash` with `--dirty` to suppress the hash suffix.
59
+ Use `--no-dirty` to explicitly refuse dirty versions (overrides `--dirty`).
60
+
61
+ Dirty versions are a convenience and are not necessarily unique.
62
+
63
+ ### Reverse lookup
64
+
65
+ Pass a version number to get the corresponding commit hash:
66
+
67
+ ```sh
68
+ $ gitcalver 20260411.3
69
+ a1b2c3d4e5f6...
70
+
71
+ $ gitcalver --short --prefix "0." 0.20260411.3
72
+ a1b2c3d
73
+ ```
74
+
75
+ If the version was generated with `--prefix`, pass the same `--prefix` for
76
+ reverse lookup. Dirty versions cannot be reversed.
77
+
78
+ ### Options
79
+
80
+ | Option | Description |
81
+ |---------------------|------------------------------------------------|
82
+ | `--prefix PREFIX` | Literal string prepended to version |
83
+ | `--dirty STRING` | Enable dirty versions; append `STRING.HASH` |
84
+ | `--no-dirty` | Refuse dirty versions (overrides `--dirty`) |
85
+ | `--no-dirty-hash` | Suppress `.HASH` suffix (requires `--dirty`) |
86
+ | `--branch BRANCH` | Base branch name; overrides auto-detection. This is the branch versions are minted on, not the branch you are working on. |
87
+ | `--short` | Output short commit hash (reverse lookup mode) |
88
+ | `--help` | Show help |
89
+
90
+ ### Exit codes
91
+
92
+ | Code | Meaning |
93
+ |------|----------------------------------------|
94
+ | 0 | Success |
95
+ | 1 | Error (not a git repo, no commits, non-monotonic dates, shallow clone) |
96
+ | 2 | Dirty workspace or off default branch (without `--dirty`) |
97
+ | 3 | Cannot trace to default branch |
98
+
99
+ ## Python API
100
+
101
+ ```python
102
+ import gitcalver
103
+
104
+ # Forward: compute a version for HEAD (or a specific revision).
105
+ version = gitcalver.get_version(repo="/path/to/repo")
106
+ # e.g. "20260411.3"
107
+
108
+ version = gitcalver.get_version(
109
+ repo="/path/to/repo",
110
+ revision="HEAD~1",
111
+ prefix="v0.",
112
+ dirty="-dirty",
113
+ )
114
+
115
+ # Reverse: resolve a version back to a commit hash.
116
+ commit = gitcalver.find_commit("20260411.3", repo="/path/to/repo")
117
+
118
+ # If the version was generated with --prefix, pass the same prefix:
119
+ commit = gitcalver.find_commit(
120
+ "v0.20260411.3", prefix="v0.", repo="/path/to/repo"
121
+ )
122
+ ```
123
+
124
+ Errors are raised as `gitcalver.ExitError`, which carries a `code` attribute
125
+ matching the CLI exit codes above.
126
+
127
+ ## Hatch plugin
128
+
129
+ `gitcalver` ships a [Hatch](https://hatch.pypa.io/) version source plugin. To
130
+ use it in `pyproject.toml`:
131
+
132
+ ```toml
133
+ [build-system]
134
+ requires = ["hatchling", "gitcalver"]
135
+ build-backend = "hatchling.build"
136
+
137
+ [tool.hatch.version]
138
+ source = "gitcalver"
139
+ # Optional:
140
+ # prefix = "0."
141
+ # dirty = "-dirty"
142
+ # no-dirty-hash = true
143
+ # branch = "main"
144
+ ```
145
+
146
+ ## Requirements
147
+
148
+ - Python 3.10+
149
+ - `git` on `$PATH`
150
+ - Full commit history (shallow clones made with `--depth` are rejected; partial
151
+ clones made with `--filter=blob:none` are fine)
152
+
153
+ ## License
154
+
155
+ MIT
@@ -0,0 +1,59 @@
1
+ # Copyright © 2026 Michael Shields
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ [project]
5
+ name = "gitcalver"
6
+ dynamic = ["version"]
7
+ description = "Deterministic calendar versioning from git history"
8
+ # Support all non-EOL Python versions.
9
+ requires-python = ">=3.10"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ readme = "README.md"
13
+ authors = [
14
+ {name = "Michael Shields", email = "shields@gitcalver.org"},
15
+ ]
16
+ keywords = ["versioning", "calver", "git", "calendar-versioning"]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Topic :: Software Development :: Build Tools",
22
+ "Topic :: Software Development :: Version Control :: Git",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://gitcalver.org"
27
+ Source = "https://github.com/gitcalver/python"
28
+ Issues = "https://github.com/gitcalver/python/issues"
29
+
30
+ [project.scripts]
31
+ gitcalver = "gitcalver.cli:main"
32
+
33
+ [project.entry-points.hatch]
34
+ gitcalver = "gitcalver._hatch_hooks"
35
+
36
+ [build-system]
37
+ requires = ["hatchling"]
38
+ build-backend = "hatchling.build"
39
+
40
+ [tool.hatch.version]
41
+ source = "code"
42
+ path = "src/gitcalver/__init__.py"
43
+ expression = "get_version(dirty='+dirty')"
44
+ search-paths = ["src"]
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/gitcalver"]
48
+
49
+ [dependency-groups]
50
+ dev = [
51
+ "hatchling>=1.1.0",
52
+ "pytest>=8",
53
+ "pytest-cov>=7.1.0",
54
+ "ruff>=0.11",
55
+ "ty>=0.0.1a7",
56
+ ]
57
+
58
+ [tool.pytest.ini_options]
59
+ testpaths = ["tests"]
@@ -0,0 +1,50 @@
1
+ # Copyright © 2026 Michael Shields
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from gitcalver._errors import EXIT_DIRTY, EXIT_ERROR, EXIT_WRONG_BRANCH, ExitError
5
+ from gitcalver._format import Format
6
+ from gitcalver._version import forward, reverse
7
+
8
+
9
+ def get_version(
10
+ *,
11
+ revision: str | None = None,
12
+ prefix: str = "",
13
+ dirty: str = "",
14
+ dirty_hash: bool = True,
15
+ branch: str | None = None,
16
+ repo: str | None = None,
17
+ ) -> str:
18
+ fmt = Format(prefix=prefix, dirty_suffix=dirty or None, dirty_hash=dirty_hash)
19
+ return forward(
20
+ dir=repo,
21
+ revision=revision,
22
+ fmt=fmt,
23
+ branch_override=branch or None,
24
+ )
25
+
26
+
27
+ def find_commit(
28
+ version: str,
29
+ *,
30
+ prefix: str = "",
31
+ branch: str | None = None,
32
+ repo: str | None = None,
33
+ short: bool = False,
34
+ ) -> str:
35
+ return reverse(
36
+ dir=repo,
37
+ version_str=version.removeprefix(prefix),
38
+ branch_override=branch or None,
39
+ short=short,
40
+ )
41
+
42
+
43
+ __all__ = [
44
+ "EXIT_DIRTY",
45
+ "EXIT_ERROR",
46
+ "EXIT_WRONG_BRANCH",
47
+ "ExitError",
48
+ "find_commit",
49
+ "get_version",
50
+ ]
@@ -0,0 +1,7 @@
1
+ # Copyright © 2026 Michael Shields
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from gitcalver.cli import main
5
+
6
+ if __name__ == "__main__":
7
+ main()
@@ -0,0 +1,59 @@
1
+ # Copyright © 2026 Michael Shields
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from gitcalver import _git
5
+ from gitcalver._errors import ExitError
6
+
7
+
8
+ def detect_branch(
9
+ dir: str | None = None, override: str | None = None
10
+ ) -> tuple[str, str]:
11
+ if override is not None:
12
+ if "/" in override:
13
+ candidates = [override]
14
+ else:
15
+ candidates = [
16
+ f"refs/remotes/origin/{override}",
17
+ f"refs/heads/{override}",
18
+ ]
19
+ for ref in candidates:
20
+ hash_ = _git.try_ref_hash(ref, dir=dir)
21
+ if hash_ is not None:
22
+ name = override.rsplit("/", 1)[-1]
23
+ return name, hash_
24
+ msg = f"branch not found: {override}"
25
+ raise ExitError(msg)
26
+
27
+ target = _git.symbolic_ref("refs/remotes/origin/HEAD", dir=dir)
28
+ if target:
29
+ hash_ = _git.try_ref_hash(target, dir=dir)
30
+ if hash_ is not None:
31
+ name = target.removeprefix("refs/remotes/origin/")
32
+ return name, hash_
33
+
34
+ for name in ("main", "master"):
35
+ hash_ = _git.try_ref_hash(f"refs/remotes/origin/{name}", dir=dir)
36
+ if hash_ is not None:
37
+ return name, hash_
38
+
39
+ for name in ("main", "master"):
40
+ hash_ = _git.try_ref_hash(f"refs/heads/{name}", dir=dir)
41
+ if hash_ is not None:
42
+ return name, hash_
43
+
44
+ msg = "cannot determine default branch"
45
+ raise ExitError(msg)
46
+
47
+
48
+ def is_on_branch(
49
+ target_hash: str,
50
+ branch_hash: str,
51
+ dir: str | None = None,
52
+ ) -> bool:
53
+ # Optimization: git treats a commit as its own ancestor, so the
54
+ # is_ancestor call below would handle this case correctly, but
55
+ # this avoids spawning git for the common same-hash case.
56
+ if target_hash == branch_hash:
57
+ return True
58
+
59
+ return _git.is_ancestor(target_hash, branch_hash, dir=dir)
@@ -0,0 +1,13 @@
1
+ # Copyright © 2026 Michael Shields
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ EXIT_ERROR = 1
5
+ EXIT_DIRTY = 2
6
+ EXIT_WRONG_BRANCH = 3
7
+
8
+
9
+ class ExitError(Exception):
10
+ def __init__(self, message: str, code: int = EXIT_ERROR) -> None:
11
+ super().__init__(message)
12
+ self.code = code
13
+ self.message = message