gitfluff 0.8.0__tar.gz → 0.9.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: gitfluff
3
- Version: 0.8.0
3
+ Version: 0.9.1
4
4
  Summary: Commit message linter and formatter
5
5
  Author-email: Na'aman Hirschfeld <nhirschfeld@gmail.com>
6
6
  License: MIT
@@ -25,9 +25,9 @@ Requires-Dist: certifi>=2024.7.4
25
25
 
26
26
  # gitfluff
27
27
 
28
- Commit message linter with presets, custom formats, and cleanup automation. Fully compliant with [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/). Skips linting while Git is creating a merge commit.
28
+ Commit message linter with presets, custom formats, and cleanup automation. Fully compliant with [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/). Skips linting the repository’s `COMMIT_EDITMSG` while Git is creating a merge commit.
29
29
 
30
- This Python package distributes prebuilt `gitfluff` binaries. On first use, the correct release binary for your platform is downloaded and cached under `~/.cache/gitfluff`.
30
+ This Python package distributes prebuilt `gitfluff` binaries. On first use, the correct release binary for your platform is downloaded and cached under `~/.cache/gitfluff`. The binary's SHA-256 checksum is verified against the release's published checksums manifest before extraction.
31
31
 
32
32
  ## Quick Start
33
33
 
@@ -83,6 +83,8 @@ gitfluff hook install commit-msg
83
83
  gitfluff hook install commit-msg --write
84
84
  ```
85
85
 
86
+ Hook installation respects `core.hooksPath` (including relative paths) and works correctly in linked git worktrees.
87
+
86
88
  ### pre-commit Framework
87
89
 
88
90
  **Add to `.pre-commit-config.yaml`:**
@@ -94,7 +96,7 @@ default_install_hook_types:
94
96
 
95
97
  repos:
96
98
  - repo: https://github.com/Goldziher/gitfluff
97
- rev: v0.8.0
99
+ rev: v0.9.1
98
100
  hooks:
99
101
  - id: gitfluff-lint
100
102
  stages: [commit-msg]
@@ -122,7 +124,7 @@ commit-msg:
122
124
  **Install hooks:**
123
125
 
124
126
  ```bash
125
- npx lefthook install
127
+ pnpm dlx lefthook install
126
128
  ```
127
129
 
128
130
  ## Optional configuration
@@ -135,15 +137,16 @@ write = true
135
137
 
136
138
  [rules]
137
139
  no_emojis = true
140
+ ai_cleanup = true # set to false to disable AI attribution cleanup
138
141
  title_suffix = "\\(JIRA-[0-9]+\\)"
139
142
  exit_nonzero_on_rewrite = true
140
143
  ```
141
144
 
142
- Any setting can be left out; omit the file entirely to keep defaults.
145
+ Any setting can be left out; omit the file entirely to keep defaults. Unknown config keys now trigger an error rather than being silently ignored.
143
146
 
144
147
  ## Advanced usage
145
148
 
146
- - Override rules per-invocation using CLI flags (e.g. `--preset`, `--msg-pattern`, `--cleanup-pattern`, `--exclude`, `--cleanup`, `--single-line`, `--no-emojis`, `--ascii-only`, `--title-prefix`, `--title-suffix`).
149
+ - Override rules per-invocation using CLI flags (e.g. `--preset`, `--msg-pattern`, `--cleanup-pattern`, `--exclude`, `--cleanup`, `--single-line`, `--no-emojis`, `--ascii-only`, `--title-prefix`, `--title-suffix`, `--no-ai-cleanup`).
147
150
  - Set `GITFLUFF_BINARY` to point at a custom build when testing unpublished versions.
148
151
  - Clear the cache (`rm ~/.cache/gitfluff/gitfluff*`) to force a fresh download.
149
152
 
@@ -1,8 +1,8 @@
1
1
  # gitfluff
2
2
 
3
- Commit message linter with presets, custom formats, and cleanup automation. Fully compliant with [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/). Skips linting while Git is creating a merge commit.
3
+ Commit message linter with presets, custom formats, and cleanup automation. Fully compliant with [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/). Skips linting the repository’s `COMMIT_EDITMSG` while Git is creating a merge commit.
4
4
 
5
- This Python package distributes prebuilt `gitfluff` binaries. On first use, the correct release binary for your platform is downloaded and cached under `~/.cache/gitfluff`.
5
+ This Python package distributes prebuilt `gitfluff` binaries. On first use, the correct release binary for your platform is downloaded and cached under `~/.cache/gitfluff`. The binary's SHA-256 checksum is verified against the release's published checksums manifest before extraction.
6
6
 
7
7
  ## Quick Start
8
8
 
@@ -58,6 +58,8 @@ gitfluff hook install commit-msg
58
58
  gitfluff hook install commit-msg --write
59
59
  ```
60
60
 
61
+ Hook installation respects `core.hooksPath` (including relative paths) and works correctly in linked git worktrees.
62
+
61
63
  ### pre-commit Framework
62
64
 
63
65
  **Add to `.pre-commit-config.yaml`:**
@@ -69,7 +71,7 @@ default_install_hook_types:
69
71
 
70
72
  repos:
71
73
  - repo: https://github.com/Goldziher/gitfluff
72
- rev: v0.8.0
74
+ rev: v0.9.1
73
75
  hooks:
74
76
  - id: gitfluff-lint
75
77
  stages: [commit-msg]
@@ -97,7 +99,7 @@ commit-msg:
97
99
  **Install hooks:**
98
100
 
99
101
  ```bash
100
- npx lefthook install
102
+ pnpm dlx lefthook install
101
103
  ```
102
104
 
103
105
  ## Optional configuration
@@ -110,15 +112,16 @@ write = true
110
112
 
111
113
  [rules]
112
114
  no_emojis = true
115
+ ai_cleanup = true # set to false to disable AI attribution cleanup
113
116
  title_suffix = "\\(JIRA-[0-9]+\\)"
114
117
  exit_nonzero_on_rewrite = true
115
118
  ```
116
119
 
117
- Any setting can be left out; omit the file entirely to keep defaults.
120
+ Any setting can be left out; omit the file entirely to keep defaults. Unknown config keys now trigger an error rather than being silently ignored.
118
121
 
119
122
  ## Advanced usage
120
123
 
121
- - Override rules per-invocation using CLI flags (e.g. `--preset`, `--msg-pattern`, `--cleanup-pattern`, `--exclude`, `--cleanup`, `--single-line`, `--no-emojis`, `--ascii-only`, `--title-prefix`, `--title-suffix`).
124
+ - Override rules per-invocation using CLI flags (e.g. `--preset`, `--msg-pattern`, `--cleanup-pattern`, `--exclude`, `--cleanup`, `--single-line`, `--no-emojis`, `--ascii-only`, `--title-prefix`, `--title-suffix`, `--no-ai-cleanup`).
122
125
  - Set `GITFLUFF_BINARY` to point at a custom build when testing unpublished versions.
123
126
  - Clear the cache (`rm ~/.cache/gitfluff/gitfluff*`) to force a fresh download.
124
127
 
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.8.0"
2
+ __version__ = "0.9.1"
@@ -0,0 +1,192 @@
1
+ """Fetch gitfluff release binaries for the Python wrapper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import os
7
+ import platform
8
+ import ssl
9
+ import subprocess
10
+ import sys
11
+ import tarfile
12
+ import tempfile
13
+ import zipfile
14
+ from pathlib import Path, PurePosixPath
15
+ from urllib.error import URLError
16
+ from urllib.request import Request, urlopen
17
+
18
+ import certifi
19
+
20
+
21
+ def _platform_triple() -> str:
22
+ system = platform.system().lower()
23
+ machine = platform.machine().lower()
24
+
25
+ if system == "windows":
26
+ if machine in {"amd64", "x86_64"}:
27
+ return "x86_64-pc-windows-gnu"
28
+ if machine in {"x86", "i386", "i686"}:
29
+ raise RuntimeError("32-bit Windows is not supported")
30
+ elif system == "linux":
31
+ if machine in {"amd64", "x86_64"}:
32
+ return "x86_64-unknown-linux-gnu"
33
+ if machine in {"aarch64", "arm64"}:
34
+ return "aarch64-unknown-linux-gnu"
35
+ elif system == "darwin":
36
+ if machine in {"amd64", "x86_64"}:
37
+ return "x86_64-apple-darwin"
38
+ if machine in {"aarch64", "arm64"}:
39
+ return "aarch64-apple-darwin"
40
+
41
+ raise RuntimeError(f"Unsupported platform: {system} {machine}")
42
+
43
+
44
+ def _python_version_to_tag(version: str) -> str:
45
+ if "rc" in version:
46
+ core, suffix = version.split("rc")
47
+ return f"{core}-rc.{suffix}"
48
+ return version
49
+
50
+
51
+ def _asset(version: str) -> tuple[str, str, str]:
52
+ """Return the (url, ext, name) of the release archive for this platform.
53
+
54
+ `name` is returned rather than recomputed by the caller: it is the key the checksum manifest
55
+ is looked up by, and a name derived separately from the URL could drift out of step with it.
56
+ """
57
+ tag = _python_version_to_tag(version)
58
+ triple = _platform_triple()
59
+ ext = "zip" if "windows" in triple else "tar.gz"
60
+ name = f"gitfluff-{triple}.{ext}"
61
+ url = f"https://github.com/Goldziher/gitfluff/releases/download/v{tag}/{name}"
62
+ return url, ext, name
63
+
64
+
65
+ def _download(url: str, destination: Path) -> None:
66
+ request = Request(url, headers={"User-Agent": "gitfluff-python-wrapper"})
67
+ context = ssl.create_default_context(cafile=certifi.where())
68
+ try:
69
+ with urlopen(request, timeout=30, context=context) as response:
70
+ if response.status != 200:
71
+ raise RuntimeError(f"HTTP {response.status}: {response.reason}")
72
+ destination.write_bytes(response.read())
73
+ except URLError as exc:
74
+ raise RuntimeError(f"Failed to download binary: {exc}") from exc
75
+
76
+
77
+ def _extract(archive: Path, ext: str, destination: Path) -> None:
78
+ if ext == "zip":
79
+ with zipfile.ZipFile(archive) as zf:
80
+ for member in zf.infolist():
81
+ if not member.is_dir() and PurePosixPath(member.filename).name == destination.name:
82
+ with zf.open(member) as src, destination.open("wb") as dst:
83
+ dst.write(src.read())
84
+ return
85
+ else:
86
+ with tarfile.open(archive, "r:gz") as tar:
87
+ for member in tar.getmembers():
88
+ if member.isfile() and PurePosixPath(member.name).name == destination.name:
89
+ with tar.extractfile(member) as src, destination.open("wb") as dst:
90
+ dst.write(src.read())
91
+ return
92
+ raise RuntimeError("Binary not found in downloaded archive")
93
+
94
+
95
+ def _download_text(url: str) -> str:
96
+ request = Request(url, headers={"User-Agent": "gitfluff-python-wrapper"})
97
+ context = ssl.create_default_context(cafile=certifi.where())
98
+ try:
99
+ with urlopen(request, timeout=30, context=context) as response:
100
+ if response.status != 200:
101
+ raise RuntimeError(f"HTTP {response.status}: {response.reason}")
102
+ return response.read().decode("utf-8")
103
+ except URLError as exc:
104
+ raise RuntimeError(f"Failed to download: {exc}") from exc
105
+
106
+
107
+ def _sha256_file(path: Path) -> str:
108
+ sha256_hash = hashlib.sha256()
109
+ with path.open("rb") as f:
110
+ for byte_block in iter(lambda: f.read(4096), b""):
111
+ sha256_hash.update(byte_block)
112
+ return sha256_hash.hexdigest()
113
+
114
+
115
+ def _verify_checksum(archive_path: Path, archive_name: str, version: str) -> None:
116
+ # The manifest is named after the release TAG, not the PyPI version string -- for a release
117
+ # candidate those differ (0.8.0rc1 vs 0.8.0-rc.1), and naming it after the version would
118
+ # request a file that does not exist.
119
+ tag = _python_version_to_tag(version)
120
+ checksum_url = f"https://github.com/Goldziher/gitfluff/releases/download/v{tag}/gitfluff_{tag}_checksums.txt"
121
+
122
+ print("Downloading checksums manifest...", file=sys.stderr)
123
+ checksum_text = _download_text(checksum_url)
124
+
125
+ # sha256sum format is "<hash> <name>". Match the name field exactly rather than with a
126
+ # substring test, so a manifest entry such as "<name>.sig" can never satisfy the lookup.
127
+ entry = next(
128
+ (
129
+ fields
130
+ for fields in (line.strip().split() for line in checksum_text.splitlines())
131
+ if len(fields) >= 2 and PurePosixPath(fields[-1]).name == archive_name
132
+ ),
133
+ None,
134
+ )
135
+
136
+ if entry is None:
137
+ raise RuntimeError(f"Checksum not found for {archive_name} in manifest")
138
+
139
+ expected_hash = entry[0]
140
+
141
+ print("Verifying archive checksum...", file=sys.stderr)
142
+ actual_hash = _sha256_file(archive_path)
143
+
144
+ if actual_hash != expected_hash:
145
+ raise RuntimeError(f"Checksum mismatch for {archive_name}\nExpected: {expected_hash}\nActual: {actual_hash}")
146
+
147
+ print("Checksum verified successfully.", file=sys.stderr)
148
+
149
+
150
+ def _cache_path(version: str) -> Path:
151
+ """
152
+ Return a versioned cache path so upgrades download matching binaries.
153
+ """
154
+ cache_dir = Path.home() / ".cache" / "gitfluff" / version
155
+ cache_dir.mkdir(parents=True, exist_ok=True)
156
+ suffix = ".exe" if platform.system().lower() == "windows" else ""
157
+ return cache_dir / f"gitfluff{suffix}"
158
+
159
+
160
+ def ensure_binary() -> str:
161
+ from . import __version__
162
+
163
+ override = os.getenv("GITFLUFF_BINARY")
164
+ if override:
165
+ return override
166
+
167
+ binary_path = _cache_path(__version__)
168
+ if binary_path.exists() and os.access(binary_path, os.X_OK):
169
+ return str(binary_path)
170
+
171
+ url, ext, archive_name = _asset(__version__)
172
+ print(f"Downloading gitfluff binary v{__version__}...", file=sys.stderr)
173
+
174
+ with tempfile.TemporaryDirectory(dir=binary_path.parent) as tmpdir:
175
+ archive_path = Path(tmpdir) / f"gitfluff.{ext}"
176
+ staged_binary = Path(tmpdir) / binary_path.name
177
+ _download(url, archive_path)
178
+ _verify_checksum(archive_path, archive_name, __version__)
179
+ _extract(archive_path, ext, staged_binary)
180
+
181
+ if platform.system().lower() != "windows":
182
+ staged_binary.chmod(0o755)
183
+ staged_binary.replace(binary_path)
184
+
185
+ print("Binary downloaded successfully!", file=sys.stderr)
186
+ return str(binary_path)
187
+
188
+
189
+ def run_gitfluff(args: list[str]) -> int:
190
+ binary = ensure_binary()
191
+ status = subprocess.call([binary, *args])
192
+ return 128 - status if status < 0 else status
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: gitfluff
3
- Version: 0.8.0
3
+ Version: 0.9.1
4
4
  Summary: Commit message linter and formatter
5
5
  Author-email: Na'aman Hirschfeld <nhirschfeld@gmail.com>
6
6
  License: MIT
@@ -25,9 +25,9 @@ Requires-Dist: certifi>=2024.7.4
25
25
 
26
26
  # gitfluff
27
27
 
28
- Commit message linter with presets, custom formats, and cleanup automation. Fully compliant with [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/). Skips linting while Git is creating a merge commit.
28
+ Commit message linter with presets, custom formats, and cleanup automation. Fully compliant with [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/). Skips linting the repository’s `COMMIT_EDITMSG` while Git is creating a merge commit.
29
29
 
30
- This Python package distributes prebuilt `gitfluff` binaries. On first use, the correct release binary for your platform is downloaded and cached under `~/.cache/gitfluff`.
30
+ This Python package distributes prebuilt `gitfluff` binaries. On first use, the correct release binary for your platform is downloaded and cached under `~/.cache/gitfluff`. The binary's SHA-256 checksum is verified against the release's published checksums manifest before extraction.
31
31
 
32
32
  ## Quick Start
33
33
 
@@ -83,6 +83,8 @@ gitfluff hook install commit-msg
83
83
  gitfluff hook install commit-msg --write
84
84
  ```
85
85
 
86
+ Hook installation respects `core.hooksPath` (including relative paths) and works correctly in linked git worktrees.
87
+
86
88
  ### pre-commit Framework
87
89
 
88
90
  **Add to `.pre-commit-config.yaml`:**
@@ -94,7 +96,7 @@ default_install_hook_types:
94
96
 
95
97
  repos:
96
98
  - repo: https://github.com/Goldziher/gitfluff
97
- rev: v0.8.0
99
+ rev: v0.9.1
98
100
  hooks:
99
101
  - id: gitfluff-lint
100
102
  stages: [commit-msg]
@@ -122,7 +124,7 @@ commit-msg:
122
124
  **Install hooks:**
123
125
 
124
126
  ```bash
125
- npx lefthook install
127
+ pnpm dlx lefthook install
126
128
  ```
127
129
 
128
130
  ## Optional configuration
@@ -135,15 +137,16 @@ write = true
135
137
 
136
138
  [rules]
137
139
  no_emojis = true
140
+ ai_cleanup = true # set to false to disable AI attribution cleanup
138
141
  title_suffix = "\\(JIRA-[0-9]+\\)"
139
142
  exit_nonzero_on_rewrite = true
140
143
  ```
141
144
 
142
- Any setting can be left out; omit the file entirely to keep defaults.
145
+ Any setting can be left out; omit the file entirely to keep defaults. Unknown config keys now trigger an error rather than being silently ignored.
143
146
 
144
147
  ## Advanced usage
145
148
 
146
- - Override rules per-invocation using CLI flags (e.g. `--preset`, `--msg-pattern`, `--cleanup-pattern`, `--exclude`, `--cleanup`, `--single-line`, `--no-emojis`, `--ascii-only`, `--title-prefix`, `--title-suffix`).
149
+ - Override rules per-invocation using CLI flags (e.g. `--preset`, `--msg-pattern`, `--cleanup-pattern`, `--exclude`, `--cleanup`, `--single-line`, `--no-emojis`, `--ascii-only`, `--title-prefix`, `--title-suffix`, `--no-ai-cleanup`).
147
150
  - Set `GITFLUFF_BINARY` to point at a custom build when testing unpublished versions.
148
151
  - Clear the cache (`rm ~/.cache/gitfluff/gitfluff*`) to force a fresh download.
149
152
 
@@ -9,4 +9,5 @@ gitfluff.egg-info/SOURCES.txt
9
9
  gitfluff.egg-info/dependency_links.txt
10
10
  gitfluff.egg-info/entry_points.txt
11
11
  gitfluff.egg-info/requires.txt
12
- gitfluff.egg-info/top_level.txt
12
+ gitfluff.egg-info/top_level.txt
13
+ tests/test_downloader.py
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gitfluff"
7
+ version = "0.9.1"
8
+ description = "Commit message linter and formatter"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Na'aman Hirschfeld", email = "nhirschfeld@gmail.com" }]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.8",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Software Development :: Build Tools",
23
+ "Environment :: Console",
24
+ ]
25
+ keywords = ["git", "commit", "lint", "conventional", "format"]
26
+ requires-python = ">=3.8"
27
+ dependencies = ["certifi>=2024.7.4"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/Goldziher/gitfluff"
31
+ Repository = "https://github.com/Goldziher/gitfluff.git"
32
+ Issues = "https://github.com/Goldziher/gitfluff/issues"
33
+
34
+ [project.scripts]
35
+ gitfluff = "gitfluff.cli:main"
36
+
37
+ [tool.setuptools]
38
+ packages = ["gitfluff"]
@@ -0,0 +1,78 @@
1
+ import io
2
+ import os
3
+ import signal
4
+ import sys
5
+ import tarfile
6
+ import tempfile
7
+ import unittest
8
+ import zipfile
9
+ from contextlib import ExitStack
10
+ from pathlib import Path
11
+ from unittest.mock import patch
12
+
13
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
14
+
15
+ from gitfluff import downloader
16
+
17
+
18
+ class DownloaderTests(unittest.TestCase):
19
+ def test_should_ignore_suffix_lookalikes_when_extracting_binary(self) -> None:
20
+ with tempfile.TemporaryDirectory() as directory:
21
+ archive = Path(directory) / "release.tar.gz"
22
+ destination = Path(directory) / "gitfluff"
23
+ with tarfile.open(archive, "w:gz") as release:
24
+ for name, content in [("notgitfluff", b"wrong"), ("bin/gitfluff", b"correct")]:
25
+ member = tarfile.TarInfo(name)
26
+ member.size = len(content)
27
+ release.addfile(member, io.BytesIO(content))
28
+ downloader._extract(archive, "tar.gz", destination)
29
+ self.assertEqual(destination.read_bytes(), b"correct")
30
+
31
+ def test_should_extract_exact_binary_name_from_zip(self) -> None:
32
+ with tempfile.TemporaryDirectory() as directory:
33
+ archive = Path(directory) / "release.zip"
34
+ destination = Path(directory) / "gitfluff.exe"
35
+ with zipfile.ZipFile(archive, "w") as release:
36
+ release.writestr("notgitfluff.exe", b"wrong")
37
+ release.writestr("bin/gitfluff.exe", b"correct")
38
+ downloader._extract(archive, "zip", destination)
39
+ self.assertEqual(destination.read_bytes(), b"correct")
40
+
41
+ def test_should_reject_directory_when_extracting_binary(self) -> None:
42
+ with tempfile.TemporaryDirectory() as directory:
43
+ archive = Path(directory) / "release.tar.gz"
44
+ destination = Path(directory) / "gitfluff"
45
+ with tarfile.open(archive, "w:gz") as release:
46
+ member = tarfile.TarInfo("gitfluff")
47
+ member.type = tarfile.DIRTYPE
48
+ release.addfile(member)
49
+ with self.assertRaisesRegex(RuntimeError, "Binary not found"):
50
+ downloader._extract(archive, "tar.gz", destination)
51
+
52
+ def test_should_leave_no_cached_binary_when_extraction_fails(self) -> None:
53
+ with tempfile.TemporaryDirectory() as directory:
54
+ destination = Path(directory) / "gitfluff"
55
+
56
+ def fail_extraction(_archive: Path, _extension: str, output: Path) -> None:
57
+ output.write_bytes(b"partial")
58
+ raise OSError("interrupted extraction")
59
+
60
+ with ExitStack() as stack:
61
+ stack.enter_context(patch.dict(os.environ, {"GITFLUFF_BINARY": ""}))
62
+ stack.enter_context(patch.object(downloader, "_cache_path", return_value=destination))
63
+ stack.enter_context(patch.object(downloader, "_download"))
64
+ stack.enter_context(patch.object(downloader, "_verify_checksum"))
65
+ stack.enter_context(patch.object(downloader, "_extract", side_effect=fail_extraction))
66
+ stack.enter_context(self.assertRaisesRegex(OSError, "interrupted extraction"))
67
+ downloader.ensure_binary()
68
+ self.assertEqual(destination.exists(), False)
69
+
70
+ @unittest.skipIf(os.name == "nt", "POSIX signals required")
71
+ def test_should_return_failure_status_when_child_is_terminated(self) -> None:
72
+ with patch.dict(os.environ, {"GITFLUFF_BINARY": sys.executable}):
73
+ status = downloader.run_gitfluff(["-c", "import os, signal; os.kill(os.getpid(), signal.SIGTERM)"])
74
+ self.assertEqual(status, 128 + signal.SIGTERM)
75
+
76
+
77
+ if __name__ == "__main__":
78
+ unittest.main()
@@ -1,132 +0,0 @@
1
- """Fetch gitfluff release binaries for the Python wrapper."""
2
-
3
- from __future__ import annotations
4
-
5
- import os
6
- import platform
7
- import subprocess
8
- import sys
9
- import tempfile
10
- import tarfile
11
- import zipfile
12
- import ssl
13
-
14
- import certifi
15
- from pathlib import Path
16
- from urllib.error import URLError
17
- from urllib.request import Request, urlopen
18
-
19
-
20
- def _platform_triple() -> str:
21
- system = platform.system().lower()
22
- machine = platform.machine().lower()
23
-
24
- if system == "windows":
25
- if machine in {"amd64", "x86_64"}:
26
- return "x86_64-pc-windows-gnu"
27
- if machine in {"x86", "i386", "i686"}:
28
- raise RuntimeError("32-bit Windows is not supported")
29
- elif system == "linux":
30
- if machine in {"amd64", "x86_64"}:
31
- return "x86_64-unknown-linux-gnu"
32
- if machine in {"aarch64", "arm64"}:
33
- return "aarch64-unknown-linux-gnu"
34
- elif system == "darwin":
35
- if machine in {"amd64", "x86_64"}:
36
- return "x86_64-apple-darwin"
37
- if machine in {"aarch64", "arm64"}:
38
- return "aarch64-apple-darwin"
39
-
40
- raise RuntimeError(f"Unsupported platform: {system} {machine}")
41
-
42
-
43
- def _python_version_to_tag(version: str) -> str:
44
- if "rc" in version:
45
- core, suffix = version.split("rc")
46
- return f"{core}-rc.{suffix}"
47
- return version
48
-
49
-
50
- def _asset(version: str) -> tuple[str, str]:
51
- tag = _python_version_to_tag(version)
52
- triple = _platform_triple()
53
- ext = "zip" if "windows" in triple else "tar.gz"
54
- url = (
55
- f"https://github.com/Goldziher/gitfluff/releases/download/"
56
- f"v{tag}/gitfluff-{triple}.{ext}"
57
- )
58
- return url, ext
59
-
60
-
61
- def _download(url: str, destination: Path) -> None:
62
- request = Request(url, headers={"User-Agent": "gitfluff-python-wrapper"})
63
- context = ssl.create_default_context(cafile=certifi.where())
64
- try:
65
- with urlopen(request, timeout=30, context=context) as response:
66
- if response.status != 200:
67
- raise RuntimeError(f"HTTP {response.status}: {response.reason}")
68
- destination.write_bytes(response.read())
69
- except URLError as exc:
70
- raise RuntimeError(f"Failed to download binary: {exc}") from exc
71
-
72
-
73
- def _extract(archive: Path, ext: str, destination: Path) -> None:
74
- if ext == "zip":
75
- with zipfile.ZipFile(archive) as zf:
76
- for name in zf.namelist():
77
- if name.endswith("gitfluff") or name.endswith("gitfluff.exe"):
78
- with zf.open(name) as src, destination.open("wb") as dst:
79
- dst.write(src.read())
80
- return
81
- else:
82
- with tarfile.open(archive, "r:gz") as tar:
83
- for member in tar.getmembers():
84
- if member.name.endswith("gitfluff") or member.name.endswith(
85
- "gitfluff.exe"
86
- ):
87
- with tar.extractfile(member) as src, destination.open("wb") as dst:
88
- dst.write(src.read())
89
- return
90
- raise RuntimeError("Binary not found in downloaded archive")
91
-
92
-
93
- def _cache_path(version: str) -> Path:
94
- """
95
- Return a versioned cache path so upgrades download matching binaries.
96
- """
97
- cache_dir = Path.home() / ".cache" / "gitfluff" / version
98
- cache_dir.mkdir(parents=True, exist_ok=True)
99
- suffix = ".exe" if platform.system().lower() == "windows" else ""
100
- return cache_dir / f"gitfluff{suffix}"
101
-
102
-
103
- def ensure_binary() -> str:
104
- from . import __version__
105
-
106
- # Allow override via environment for power users/tests
107
- override = os.getenv("GITFLUFF_BINARY")
108
- if override:
109
- return override
110
-
111
- binary_path = _cache_path(__version__)
112
- if binary_path.exists() and os.access(binary_path, os.X_OK):
113
- return str(binary_path)
114
-
115
- url, ext = _asset(__version__)
116
- print(f"Downloading gitfluff binary v{__version__}...", file=sys.stderr)
117
-
118
- with tempfile.TemporaryDirectory() as tmpdir:
119
- archive_path = Path(tmpdir) / "gitfluff.tar.gz"
120
- _download(url, archive_path)
121
- _extract(archive_path, ext, binary_path)
122
-
123
- if platform.system().lower() != "windows":
124
- binary_path.chmod(0o755)
125
-
126
- print("Binary downloaded successfully!", file=sys.stderr)
127
- return str(binary_path)
128
-
129
-
130
- def run_gitfluff(args: list[str]) -> int:
131
- binary = ensure_binary()
132
- return subprocess.call([binary, *args])
@@ -1,42 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools>=61.0", "wheel"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "gitfluff"
7
- version = "0.8.0"
8
- description = "Commit message linter and formatter"
9
- readme = "README.md"
10
- license = {text = "MIT"}
11
- authors = [
12
- {name = "Na'aman Hirschfeld", email = "nhirschfeld@gmail.com"}
13
- ]
14
- classifiers = [
15
- "Development Status :: 3 - Alpha",
16
- "Intended Audience :: Developers",
17
- "License :: OSI Approved :: MIT License",
18
- "Programming Language :: Python :: 3",
19
- "Programming Language :: Python :: 3.8",
20
- "Programming Language :: Python :: 3.9",
21
- "Programming Language :: Python :: 3.10",
22
- "Programming Language :: Python :: 3.11",
23
- "Programming Language :: Python :: 3.12",
24
- "Topic :: Software Development :: Build Tools",
25
- "Environment :: Console"
26
- ]
27
- keywords = ["git", "commit", "lint", "conventional", "format"]
28
- requires-python = ">=3.8"
29
- dependencies = [
30
- "certifi>=2024.7.4"
31
- ]
32
-
33
- [project.urls]
34
- Homepage = "https://github.com/Goldziher/gitfluff"
35
- Repository = "https://github.com/Goldziher/gitfluff.git"
36
- Issues = "https://github.com/Goldziher/gitfluff/issues"
37
-
38
- [project.scripts]
39
- gitfluff = "gitfluff.cli:main"
40
-
41
- [tool.setuptools]
42
- packages = ["gitfluff"]
File without changes
File without changes
File without changes