mpatch 1.7.0__tar.gz → 1.8.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.
- {mpatch-1.7.0 → mpatch-1.8.0}/.github/workflows/ci.yml +5 -5
- mpatch-1.8.0/.github/workflows/crates.yml +59 -0
- {mpatch-1.7.0 → mpatch-1.8.0}/.github/workflows/docs.yml +5 -3
- {mpatch-1.7.0 → mpatch-1.8.0}/.github/workflows/pypi.yml +2 -2
- {mpatch-1.7.0 → mpatch-1.8.0}/.github/workflows/python-ci.yml +3 -3
- {mpatch-1.7.0 → mpatch-1.8.0}/.github/workflows/release.yml +14 -7
- {mpatch-1.7.0 → mpatch-1.8.0}/CHANGELOG.md +12 -1
- {mpatch-1.7.0 → mpatch-1.8.0}/Cargo.lock +2 -2
- {mpatch-1.7.0 → mpatch-1.8.0}/Cargo.toml +1 -1
- {mpatch-1.7.0 → mpatch-1.8.0}/PKG-INFO +1 -1
- {mpatch-1.7.0 → mpatch-1.8.0}/README.md +3 -3
- {mpatch-1.7.0 → mpatch-1.8.0}/bindings/python/Cargo.toml +1 -1
- {mpatch-1.7.0 → mpatch-1.8.0}/bindings/python/mpatch.pyi +2 -2
- {mpatch-1.7.0 → mpatch-1.8.0}/bindings/python/src/lib.rs +2 -2
- {mpatch-1.7.0 → mpatch-1.8.0}/pyproject.toml +1 -1
- {mpatch-1.7.0 → mpatch-1.8.0}/scripts/release.py +113 -69
- {mpatch-1.7.0 → mpatch-1.8.0}/src/lib.rs +667 -43
- {mpatch-1.7.0 → mpatch-1.8.0}/src/main.rs +266 -1
- {mpatch-1.7.0 → mpatch-1.8.0}/tests/patch_apply_tests.rs +1211 -312
- {mpatch-1.7.0 → mpatch-1.8.0}/.github/workflows/test-report.yml +0 -0
- {mpatch-1.7.0 → mpatch-1.8.0}/.gitignore +0 -0
- {mpatch-1.7.0 → mpatch-1.8.0}/LICENSE +0 -0
- {mpatch-1.7.0 → mpatch-1.8.0}/benches/mpatch_bench.rs +0 -0
- {mpatch-1.7.0 → mpatch-1.8.0}/bindings/python/.gitignore +0 -0
- {mpatch-1.7.0 → mpatch-1.8.0}/bindings/python/README.md +0 -0
- {mpatch-1.7.0 → mpatch-1.8.0}/bindings/python/tests/test_mpatch.py +0 -0
|
@@ -21,10 +21,10 @@ jobs:
|
|
|
21
21
|
runs-on: ubuntu-latest
|
|
22
22
|
steps:
|
|
23
23
|
- name: Checkout repository
|
|
24
|
-
uses: actions/checkout@
|
|
24
|
+
uses: actions/checkout@v7
|
|
25
25
|
|
|
26
26
|
- name: Install Rust toolchain
|
|
27
|
-
uses: actions-rust-lang/setup-rust-toolchain@
|
|
27
|
+
uses: actions-rust-lang/setup-rust-toolchain@v2
|
|
28
28
|
with:
|
|
29
29
|
toolchain: stable
|
|
30
30
|
components: rustfmt
|
|
@@ -43,7 +43,7 @@ jobs:
|
|
|
43
43
|
|
|
44
44
|
steps:
|
|
45
45
|
- name: Checkout repository
|
|
46
|
-
uses: actions/checkout@
|
|
46
|
+
uses: actions/checkout@v7
|
|
47
47
|
with: { fetch-depth: 0 }
|
|
48
48
|
|
|
49
49
|
# --- Smart Change Detection ---
|
|
@@ -60,7 +60,7 @@ jobs:
|
|
|
60
60
|
|
|
61
61
|
- name: Get changed files
|
|
62
62
|
id: changed_files
|
|
63
|
-
uses: tj-actions/changed-files@
|
|
63
|
+
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46
|
|
64
64
|
with:
|
|
65
65
|
base_sha: ${{ steps.last_successful_commit.outputs.base }}
|
|
66
66
|
files_yaml: |
|
|
@@ -74,7 +74,7 @@ jobs:
|
|
|
74
74
|
# --- Setup ---
|
|
75
75
|
- name: Install Rust toolchain
|
|
76
76
|
if: steps.changed_files.outputs.code_any_changed == 'true'
|
|
77
|
-
uses: actions-rust-lang/setup-rust-toolchain@
|
|
77
|
+
uses: actions-rust-lang/setup-rust-toolchain@v2
|
|
78
78
|
with:
|
|
79
79
|
toolchain: stable
|
|
80
80
|
cache: true
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: Publish Rust Crate to crates.io
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
env:
|
|
12
|
+
CARGO_TERM_COLOR: always
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
publish-crate:
|
|
16
|
+
name: Upload release to crates.io
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
|
|
19
|
+
environment:
|
|
20
|
+
name: crates-io
|
|
21
|
+
url: https://crates.io/crates/mpatch
|
|
22
|
+
|
|
23
|
+
permissions:
|
|
24
|
+
id-token: write
|
|
25
|
+
contents: read
|
|
26
|
+
|
|
27
|
+
steps:
|
|
28
|
+
- name: Checkout repository
|
|
29
|
+
uses: actions/checkout@v7
|
|
30
|
+
|
|
31
|
+
- name: Install Rust toolchain
|
|
32
|
+
uses: actions-rust-lang/setup-rust-toolchain@v2
|
|
33
|
+
with:
|
|
34
|
+
toolchain: stable
|
|
35
|
+
cache: false
|
|
36
|
+
|
|
37
|
+
# Refuse to publish if the release tag and the crate version disagree.
|
|
38
|
+
- name: Check Tag Matches Crate Version
|
|
39
|
+
if: github.event_name == 'release'
|
|
40
|
+
shell: bash
|
|
41
|
+
env:
|
|
42
|
+
REF_NAME: ${{ github.ref_name }}
|
|
43
|
+
run: |
|
|
44
|
+
CRATE_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "mpatch") | .version')
|
|
45
|
+
if [[ "${REF_NAME#v}" != "$CRATE_VERSION" ]]; then
|
|
46
|
+
echo "Error: Tag '$REF_NAME' does not match crate version '$CRATE_VERSION'"
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Trusted Publishing: exchanges the workflow's OIDC token for a short-lived
|
|
51
|
+
# crates.io token, so no long-lived CARGO_REGISTRY_TOKEN secret is needed.
|
|
52
|
+
- name: Authenticate with crates.io
|
|
53
|
+
id: auth
|
|
54
|
+
uses: rust-lang/crates-io-auth-action@v1
|
|
55
|
+
|
|
56
|
+
- name: Publish crate
|
|
57
|
+
run: cargo publish -p mpatch --locked
|
|
58
|
+
env:
|
|
59
|
+
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
|
|
@@ -18,7 +18,7 @@ jobs:
|
|
|
18
18
|
runs-on: ubuntu-latest
|
|
19
19
|
steps:
|
|
20
20
|
- name: Checkout repository
|
|
21
|
-
uses: actions/checkout@
|
|
21
|
+
uses: actions/checkout@v7
|
|
22
22
|
with:
|
|
23
23
|
# Required for tj-actions/changed-files to diff against the correct base.
|
|
24
24
|
fetch-depth: 0
|
|
@@ -36,10 +36,12 @@ jobs:
|
|
|
36
36
|
- .github/workflows/docs.yml
|
|
37
37
|
|
|
38
38
|
- name: Install Rust toolchain
|
|
39
|
-
uses: actions-rust-lang/setup-rust-toolchain@
|
|
39
|
+
uses: actions-rust-lang/setup-rust-toolchain@v2
|
|
40
40
|
with:
|
|
41
41
|
toolchain: stable
|
|
42
|
-
cache:
|
|
42
|
+
cache: true
|
|
43
|
+
# sccache (below) caches compiled crates, so only cache the cargo registry here.
|
|
44
|
+
cache-targets: false
|
|
43
45
|
|
|
44
46
|
- name: Setup sccache
|
|
45
47
|
if: steps.changed_files.outputs.docs_relevant_any_changed == 'true'
|
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
- { os: windows-latest, target: aarch64-pc-windows-msvc }
|
|
32
32
|
|
|
33
33
|
steps:
|
|
34
|
-
- uses: actions/checkout@
|
|
34
|
+
- uses: actions/checkout@v7
|
|
35
35
|
|
|
36
36
|
- name: Set up QEMU
|
|
37
37
|
if: runner.os == 'Linux'
|
|
@@ -64,7 +64,7 @@ jobs:
|
|
|
64
64
|
name: Build source distribution
|
|
65
65
|
runs-on: ubuntu-latest
|
|
66
66
|
steps:
|
|
67
|
-
- uses: actions/checkout@
|
|
67
|
+
- uses: actions/checkout@v7
|
|
68
68
|
|
|
69
69
|
- name: Build sdist
|
|
70
70
|
uses: PyO3/maturin-action@v1
|
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
runs-on: ubuntu-latest
|
|
32
32
|
steps:
|
|
33
33
|
- name: Checkout repository
|
|
34
|
-
uses: actions/checkout@
|
|
34
|
+
uses: actions/checkout@v7
|
|
35
35
|
|
|
36
36
|
- name: Setup Python
|
|
37
37
|
uses: actions/setup-python@v6
|
|
@@ -63,10 +63,10 @@ jobs:
|
|
|
63
63
|
|
|
64
64
|
steps:
|
|
65
65
|
- name: Checkout repository
|
|
66
|
-
uses: actions/checkout@
|
|
66
|
+
uses: actions/checkout@v7
|
|
67
67
|
|
|
68
68
|
- name: Install Rust toolchain
|
|
69
|
-
uses: actions-rust-lang/setup-rust-toolchain@
|
|
69
|
+
uses: actions-rust-lang/setup-rust-toolchain@v2
|
|
70
70
|
with:
|
|
71
71
|
toolchain: stable
|
|
72
72
|
cache: true
|
|
@@ -67,14 +67,21 @@ jobs:
|
|
|
67
67
|
|
|
68
68
|
steps:
|
|
69
69
|
- name: Checkout repository
|
|
70
|
-
uses: actions/checkout@
|
|
70
|
+
uses: actions/checkout@v7
|
|
71
71
|
|
|
72
72
|
- name: Install Rust toolchain
|
|
73
|
-
uses: actions-rust-lang/setup-rust-toolchain@
|
|
73
|
+
uses: actions-rust-lang/setup-rust-toolchain@v2
|
|
74
74
|
with:
|
|
75
75
|
toolchain: stable
|
|
76
76
|
target: ${{ matrix.target }}
|
|
77
|
-
|
|
77
|
+
# Caches saved from a tag build can't be restored by later releases,
|
|
78
|
+
# so caching here would only use up the repository's cache storage.
|
|
79
|
+
cache: false
|
|
80
|
+
# v2 defaults to 'deny', which fails on any warning. cross's
|
|
81
|
+
# aarch64-unknown-linux-musl image wraps the linker in a 'set -x'
|
|
82
|
+
# script, and rustc reports that stderr output as a warning.
|
|
83
|
+
# Lints are enforced by CI, not by release builds.
|
|
84
|
+
build-warnings: warn
|
|
78
85
|
|
|
79
86
|
# --- Auto-detect Binary Name ---
|
|
80
87
|
- name: Get Binary Name
|
|
@@ -222,14 +229,14 @@ jobs:
|
|
|
222
229
|
runs-on: macos-latest
|
|
223
230
|
steps:
|
|
224
231
|
- name: Checkout
|
|
225
|
-
uses: actions/checkout@
|
|
232
|
+
uses: actions/checkout@v7
|
|
226
233
|
|
|
227
234
|
# We need Rust installed here just to run 'cargo metadata' to get the name.
|
|
228
235
|
- name: Install Rust toolchain
|
|
229
|
-
uses: actions-rust-lang/setup-rust-toolchain@
|
|
236
|
+
uses: actions-rust-lang/setup-rust-toolchain@v2
|
|
230
237
|
with:
|
|
231
238
|
toolchain: stable
|
|
232
|
-
cache:
|
|
239
|
+
cache: false
|
|
233
240
|
|
|
234
241
|
# --- Auto-detect Binary Name ---
|
|
235
242
|
- name: Get Binary Name
|
|
@@ -345,7 +352,7 @@ jobs:
|
|
|
345
352
|
runs-on: ubuntu-latest
|
|
346
353
|
steps:
|
|
347
354
|
- name: Checkout
|
|
348
|
-
uses: actions/checkout@
|
|
355
|
+
uses: actions/checkout@v7
|
|
349
356
|
with:
|
|
350
357
|
# Fetch full history so we can find the previous tag for comparison links
|
|
351
358
|
fetch-depth: 0
|
|
@@ -7,9 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.8.0] - 2026-10-24
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Patch Target Paths:** Resolve targets the way `patch -p1` does for directory-tree diffs (`diff -Naur pkg.orig pkg`, including created and deleted files) and for backup-file diffs (`diff -u file.orig file`). When a target is missing but exists without its leading directory, patch that file and print a warning.
|
|
15
|
+
- **Already-Applied Hunks:** Skip a hunk with a warning when it can only be placed by fuzzy matching and its result is already in the file, so re-applying a patch no longer duplicates its changes. Hunks whose context still matches exactly always apply, so intentional duplicates keep working. Hunks that only delete lines, and hunks whose context still matches exactly after they were applied (such as appends at the end of a list), are not detected.
|
|
16
|
+
- **Git Executable Mode:** Apply Git mode changes between `100644` and `100755` on Unix after the content is written, including created files (`new file mode 100755`), mode-only patches, `-R`, and `--dry-run`. A mode change for a file that neither exists nor is created by the patch fails before anything is written, and `--atomic` skips modes when the batch is discarded. Library callers can read the changes with `parse_git_mode_changes()`.
|
|
17
|
+
|
|
10
18
|
### Fixed
|
|
11
19
|
|
|
12
|
-
- **
|
|
20
|
+
- **Unified Diff Parsing:** Recognize `diff -Naur` headers, strip timestamps from `---` / `+++` file headers (including timestamps whose tab separator was expanded to spaces), decode Git C-quoted paths, and distinguish file headers from hunk content that begins with `---` or `+++`.
|
|
21
|
+
- **Line Endings:** Preserve CRLF and mixed line endings when patching a file. Previously the whole file was rewritten with LF endings; now unchanged lines keep their own ending and added lines use the file's most common one.
|
|
22
|
+
- **Final Newline:** Keep a file's final newline unless the patch changes its last line. Previously any hunk that reached the end of the file applied the patch's `` state, so a hunk ending in unchanged context could add or remove the final newline. Now a line kept from the file keeps its own newline and a line added by the patch follows the patch's marker, matching GNU `patch`.
|
|
23
|
+
- **CLI Failure Reporting:** Exit with an error instead of succeeding silently when the input contains context diffs (`diff -c`), Git renames or copies, Git binary patches, or symlink and submodule modes. Warn when a Git executable-mode change is not applied.
|
|
13
24
|
|
|
14
25
|
## [1.7.0] - 2026-10-12
|
|
15
26
|
|
|
@@ -694,7 +694,7 @@ dependencies = [
|
|
|
694
694
|
|
|
695
695
|
[[package]]
|
|
696
696
|
name = "mpatch"
|
|
697
|
-
version = "1.
|
|
697
|
+
version = "1.8.0"
|
|
698
698
|
dependencies = [
|
|
699
699
|
"anyhow",
|
|
700
700
|
"arboard",
|
|
@@ -712,7 +712,7 @@ dependencies = [
|
|
|
712
712
|
|
|
713
713
|
[[package]]
|
|
714
714
|
name = "mpatch_py"
|
|
715
|
-
version = "1.
|
|
715
|
+
version = "1.8.0"
|
|
716
716
|
dependencies = [
|
|
717
717
|
"mpatch",
|
|
718
718
|
"pyo3",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/romelium/mpatch/actions/workflows/ci.yml)
|
|
4
4
|
[](https://github.com/romelium/mpatch/releases/latest)
|
|
5
|
-
[](https://crates.io/crates/mpatch)
|
|
5
|
+
[](https://crates.io/crates/mpatch)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://crates.io/crates/mpatch)
|
|
8
8
|
|
|
@@ -102,7 +102,7 @@ The integrity of all released binaries is verified with GPG. The `.sig` signatur
|
|
|
102
102
|
2. **Verify the archive:**
|
|
103
103
|
```bash
|
|
104
104
|
# Example for Linux x64
|
|
105
|
-
gpg --verify mpatch-x86_64-unknown-linux-gnu-v1.
|
|
105
|
+
gpg --verify mpatch-x86_64-unknown-linux-gnu-v1.8.0.tar.gz.sig mpatch-x86_64-unknown-linux-gnu-v1.8.0.tar.gz
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
### Option 2: Build from Source
|
|
@@ -201,7 +201,7 @@ mpatch -vvvv changes.md ./src
|
|
|
201
201
|
Add to `Cargo.toml`:
|
|
202
202
|
```toml
|
|
203
203
|
[dependencies]
|
|
204
|
-
mpatch = "1.
|
|
204
|
+
mpatch = "1.8.0"
|
|
205
205
|
```
|
|
206
206
|
|
|
207
207
|
### 1. Simple One-Shot (String to String)
|
|
@@ -132,7 +132,7 @@ class Patch:
|
|
|
132
132
|
Args:
|
|
133
133
|
file_path (str | os.PathLike): The relative path of the file to be patched.
|
|
134
134
|
hunks (list[Hunk]): A list of hunks to be applied to the file.
|
|
135
|
-
ends_with_newline (bool, optional):
|
|
135
|
+
ends_with_newline (bool, optional): Whether the patch's last line ends with a newline. It applies only when a line added by the patch ends up last in the file. Defaults to True.
|
|
136
136
|
"""
|
|
137
137
|
...
|
|
138
138
|
@classmethod
|
|
@@ -179,7 +179,7 @@ class Patch:
|
|
|
179
179
|
...
|
|
180
180
|
@property
|
|
181
181
|
def ends_with_newline(self) -> bool:
|
|
182
|
-
"""
|
|
182
|
+
"""Whether the patch's last line ends with a newline. It applies only when a line added by the patch ends up last in the file."""
|
|
183
183
|
...
|
|
184
184
|
@ends_with_newline.setter
|
|
185
185
|
def ends_with_newline(self, ends_with_newline: bool) -> None:
|
|
@@ -296,7 +296,7 @@ impl PyPatch {
|
|
|
296
296
|
/// Args:
|
|
297
297
|
/// file_path (str | os.PathLike): The relative path of the file to be patched.
|
|
298
298
|
/// hunks (list[Hunk]): A list of hunks to be applied to the file.
|
|
299
|
-
/// ends_with_newline (bool, optional):
|
|
299
|
+
/// ends_with_newline (bool, optional): Whether the patch's last line ends with a newline. It applies only when a line added by the patch ends up last in the file. Defaults to True.
|
|
300
300
|
#[new]
|
|
301
301
|
#[pyo3(signature = (file_path, hunks, *, ends_with_newline=true))]
|
|
302
302
|
fn py_new(file_path: PathBuf, hunks: Vec<PyHunk>, ends_with_newline: bool) -> Self {
|
|
@@ -377,7 +377,7 @@ impl PyPatch {
|
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
#[getter]
|
|
380
|
-
///
|
|
380
|
+
/// Whether the patch's last line ends with a newline. It applies only when a line added by the patch ends up last in the file.
|
|
381
381
|
fn ends_with_newline(&self) -> bool {
|
|
382
382
|
self.inner.ends_with_newline
|
|
383
383
|
}
|
|
@@ -5,7 +5,7 @@ build-backend = "maturin"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mpatch"
|
|
7
7
|
# Keep this in sync with your Rust crate version
|
|
8
|
-
version = "1.
|
|
8
|
+
version = "1.8.0"
|
|
9
9
|
description = "A smart, context-aware patch tool that applies diffs using fuzzy matching, ideal for AI-generated code."
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
authors = [
|
|
@@ -1,4 +1,102 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
"""Release orchestrator for mpatch.
|
|
3
|
+
|
|
4
|
+
Verifies the build, bumps the version, and commits, tags and pushes a
|
|
5
|
+
release. Pushing the tag starts the GitHub workflows that build and
|
|
6
|
+
publish everything else.
|
|
7
|
+
|
|
8
|
+
Usage
|
|
9
|
+
-----
|
|
10
|
+
python scripts/release.py <level> [options]
|
|
11
|
+
|
|
12
|
+
<level> is 'patch', 'minor', 'major', or an explicit version such as
|
|
13
|
+
'1.8.0' (it must be greater than the current version). Choose it from
|
|
14
|
+
the [Unreleased] section of CHANGELOG.md: only "Fixed" entries -> patch,
|
|
15
|
+
any "Added" entries -> minor, breaking changes -> major.
|
|
16
|
+
|
|
17
|
+
-n, --dry-run Show what would change without touching files or git
|
|
18
|
+
(the checks and tests still run).
|
|
19
|
+
-y, --yes Answer yes to every prompt; also pushes unless
|
|
20
|
+
--no-push is given.
|
|
21
|
+
--push / --no-push Push the commit and tag without asking / don't push.
|
|
22
|
+
--skip-tests Skip fmt, clippy, doc, package, test, ruff and pytest.
|
|
23
|
+
--skip-git-check Skip the branch, clean-tree, remote and tag checks.
|
|
24
|
+
|
|
25
|
+
One-time setup
|
|
26
|
+
--------------
|
|
27
|
+
Local tools (the script checks for these before every release):
|
|
28
|
+
- git, with push access to the repository and an identity set:
|
|
29
|
+
`git config --global user.name "Your Name"` and
|
|
30
|
+
`git config --global user.email "you@example.com"`
|
|
31
|
+
- Rust stable from https://rustup.rs, plus
|
|
32
|
+
`rustup component add rustfmt clippy`
|
|
33
|
+
- Python 3 with ruff: `pip install ruff`
|
|
34
|
+
- Optional, to also run the Python tests:
|
|
35
|
+
`pip install -e bindings/python[test]`
|
|
36
|
+
|
|
37
|
+
crates.io (used by .github/workflows/crates.yml):
|
|
38
|
+
- As an owner of the mpatch crate, open its Settings > Trusted Publishing
|
|
39
|
+
page on crates.io and add a GitHub publisher: owner romelium, repository
|
|
40
|
+
mpatch, workflow crates.yml, environment crates-io.
|
|
41
|
+
- Trusted Publishing can only be configured for a crate that exists on
|
|
42
|
+
crates.io. If the crate has never been published, publish the first
|
|
43
|
+
version by hand with an API token: `cargo login`, then
|
|
44
|
+
`cargo publish -p mpatch`.
|
|
45
|
+
|
|
46
|
+
PyPI (used by .github/workflows/pypi.yml):
|
|
47
|
+
- As an owner of the mpatch project, open its Manage > Publishing page on
|
|
48
|
+
PyPI and add a GitHub publisher: owner romelium, repository mpatch,
|
|
49
|
+
workflow pypi.yml, environment pypi.
|
|
50
|
+
- If the project does not exist on PyPI yet, add the same details as a
|
|
51
|
+
pending publisher from your account's Publishing page instead; the first
|
|
52
|
+
upload then creates the project.
|
|
53
|
+
|
|
54
|
+
GitHub (optional):
|
|
55
|
+
- Signing: release.yml signs the release archives and attaches public.key
|
|
56
|
+
only when the repository secrets GPG_PRIVATE_KEY and GPG_PASSPHRASE are
|
|
57
|
+
set. Create a key with `gpg --full-generate-key`, then under Settings >
|
|
58
|
+
Secrets and variables > Actions store the output of
|
|
59
|
+
`gpg --armor --export-secret-keys <key-id>` as GPG_PRIVATE_KEY and the
|
|
60
|
+
key's passphrase as GPG_PASSPHRASE.
|
|
61
|
+
- Approval: the publish jobs run in the crates-io and pypi environments.
|
|
62
|
+
To approve each upload before it runs, add required reviewers to those
|
|
63
|
+
environments under Settings > Environments.
|
|
64
|
+
|
|
65
|
+
Each release
|
|
66
|
+
------------
|
|
67
|
+
Before running the script:
|
|
68
|
+
1. Merge everything into main, run `git checkout main && git pull`, and
|
|
69
|
+
make sure CI passes. The working tree must be clean.
|
|
70
|
+
2. Write the release notes under `## [Unreleased]` in CHANGELOG.md. The
|
|
71
|
+
script renames that section to `## [X.Y.Z] - <date>`, and release.yml
|
|
72
|
+
uses it as the GitHub release notes.
|
|
73
|
+
3. Do a dry run, e.g. `python scripts/release.py minor --dry-run`.
|
|
74
|
+
|
|
75
|
+
What the script does:
|
|
76
|
+
1. Checks the tools, the git identity, that you are on main with a clean
|
|
77
|
+
tree that is not behind the remote, and that the tag does not exist yet.
|
|
78
|
+
2. Runs cargo fmt --check, cargo clippy (-D warnings), cargo doc
|
|
79
|
+
(-D warnings), cargo package, cargo test, ruff on bindings/python and
|
|
80
|
+
scripts/, and pytest when the bindings are installed.
|
|
81
|
+
3. Bumps the version in Cargo.toml, bindings/python/Cargo.toml,
|
|
82
|
+
bindings/python/pyproject.toml, CHANGELOG.md, src/lib.rs and README.md,
|
|
83
|
+
and syncs Cargo.lock.
|
|
84
|
+
4. Commits "chore: release vX.Y.Z", creates the annotated tag vX.Y.Z, and
|
|
85
|
+
pushes the branch and the tag.
|
|
86
|
+
If a step fails before the commit, the changed files are restored.
|
|
87
|
+
|
|
88
|
+
After the script:
|
|
89
|
+
1. The pushed tag runs .github/workflows/release.yml, which builds the
|
|
90
|
+
binaries for every target and creates a *draft* GitHub release.
|
|
91
|
+
2. When that finishes, open the draft under Releases on GitHub, check its
|
|
92
|
+
assets and notes, and click "Publish release".
|
|
93
|
+
3. Publishing runs .github/workflows/crates.yml (crates.io) and
|
|
94
|
+
.github/workflows/pypi.yml (PyPI) in parallel.
|
|
95
|
+
4. Check the new version on crates.io, docs.rs and PyPI.
|
|
96
|
+
If a publish workflow fails, re-run its failed jobs from the Actions tab.
|
|
97
|
+
To start one manually instead, use "Run workflow" and pick the release tag
|
|
98
|
+
as the ref; running it on main publishes whatever version main contains.
|
|
99
|
+
"""
|
|
2
100
|
|
|
3
101
|
from __future__ import annotations
|
|
4
102
|
|
|
@@ -248,25 +346,8 @@ def check_python_module(module_name: str) -> bool:
|
|
|
248
346
|
return False
|
|
249
347
|
|
|
250
348
|
|
|
251
|
-
def verify_crates_io_auth() -> bool:
|
|
252
|
-
"""Checks if credentials exist for publishing to crates.io."""
|
|
253
|
-
if os.environ.get("CARGO_REGISTRY_TOKEN") or os.environ.get(
|
|
254
|
-
"CARGO_REGISTRIES_CRATES_IO_TOKEN"
|
|
255
|
-
):
|
|
256
|
-
return True
|
|
257
|
-
home = Path.home()
|
|
258
|
-
cred_toml = home / ".cargo" / "credentials.toml"
|
|
259
|
-
cred_plain = home / ".cargo" / "credentials"
|
|
260
|
-
for p in [cred_toml, cred_plain]:
|
|
261
|
-
if p.exists() and "token" in p.read_text(encoding="utf-8", errors="ignore"):
|
|
262
|
-
return True
|
|
263
|
-
return False
|
|
264
|
-
|
|
265
|
-
|
|
266
349
|
# --- Pre-flight Tool Diagnostics ---
|
|
267
|
-
def run_tool_diagnostics(
|
|
268
|
-
root: Path, skip_tests: bool, publish_cargo: bool
|
|
269
|
-
) -> list[str]:
|
|
350
|
+
def run_tool_diagnostics(root: Path, skip_tests: bool) -> list[str]:
|
|
270
351
|
"""Validates all required tools and runtime environments before execution."""
|
|
271
352
|
info("Step 1: Running Pre-flight Environment & Tool Diagnostics")
|
|
272
353
|
issues: list[str] = []
|
|
@@ -375,13 +456,6 @@ def run_tool_diagnostics(
|
|
|
375
456
|
else:
|
|
376
457
|
print(" - pytest: not installed in active Python environment (optional)")
|
|
377
458
|
|
|
378
|
-
# 6. Crates.io authentication check (if cargo publish requested)
|
|
379
|
-
if publish_cargo and not verify_crates_io_auth():
|
|
380
|
-
warn(
|
|
381
|
-
"No crates.io token detected in CARGO_REGISTRY_TOKEN or ~/.cargo/credentials[.toml]. "
|
|
382
|
-
"'cargo publish' may prompt or fail if unauthenticated."
|
|
383
|
-
)
|
|
384
|
-
|
|
385
459
|
return issues
|
|
386
460
|
|
|
387
461
|
|
|
@@ -428,7 +502,11 @@ def apply_version_bumps(ctx: ReleaseContext, current_ver: str, new_ver: str) ->
|
|
|
428
502
|
"""Applies version updates across Cargo.toml files, pyproject.toml, CHANGELOG, docs, and lockfile."""
|
|
429
503
|
root = ctx.root
|
|
430
504
|
dry_run = ctx.dry_run
|
|
431
|
-
today =
|
|
505
|
+
today = (
|
|
506
|
+
datetime.datetime.now(datetime.timezone(datetime.timedelta(0)))
|
|
507
|
+
.date()
|
|
508
|
+
.isoformat()
|
|
509
|
+
)
|
|
432
510
|
c_esc = re.escape(current_ver)
|
|
433
511
|
|
|
434
512
|
# 1. Root Cargo.toml
|
|
@@ -676,13 +754,6 @@ def main() -> None:
|
|
|
676
754
|
help="Do not push git commit and tag to remote.",
|
|
677
755
|
)
|
|
678
756
|
|
|
679
|
-
parser.add_argument(
|
|
680
|
-
"--publish-cargo",
|
|
681
|
-
action="store_true",
|
|
682
|
-
default=False,
|
|
683
|
-
help="Publish the core crate to crates.io after tagging.",
|
|
684
|
-
)
|
|
685
|
-
|
|
686
757
|
args = parser.parse_args()
|
|
687
758
|
root = get_repo_root()
|
|
688
759
|
os.chdir(root)
|
|
@@ -700,7 +771,7 @@ def main() -> None:
|
|
|
700
771
|
|
|
701
772
|
try:
|
|
702
773
|
# --- Step 1: Pre-flight Tool Diagnostics ---
|
|
703
|
-
issues = run_tool_diagnostics(root, args.skip_tests
|
|
774
|
+
issues = run_tool_diagnostics(root, args.skip_tests)
|
|
704
775
|
if issues:
|
|
705
776
|
error("Pre-flight checks identified blocking issues:")
|
|
706
777
|
for issue in issues:
|
|
@@ -919,38 +990,7 @@ def main() -> None:
|
|
|
919
990
|
f"&& git push {remote_name} {tag_name}"
|
|
920
991
|
)
|
|
921
992
|
|
|
922
|
-
# --- Step 7:
|
|
923
|
-
print()
|
|
924
|
-
info("Step 7: Publishing to Crates.io")
|
|
925
|
-
pkg_name = read_package_name(root)
|
|
926
|
-
should_publish_cargo = args.publish_cargo
|
|
927
|
-
if not should_publish_cargo and not args.dry_run and not args.yes:
|
|
928
|
-
ans = (
|
|
929
|
-
input("Do you want to publish the Rust crate to Crates.io now? [y/N]: ")
|
|
930
|
-
.strip()
|
|
931
|
-
.lower()
|
|
932
|
-
)
|
|
933
|
-
should_publish_cargo = ans == "y"
|
|
934
|
-
|
|
935
|
-
if should_publish_cargo:
|
|
936
|
-
if args.dry_run:
|
|
937
|
-
info(f"[DRY RUN] Would execute: cargo publish -p {pkg_name}")
|
|
938
|
-
else:
|
|
939
|
-
try:
|
|
940
|
-
run_cmd(["cargo", "publish", "-p", pkg_name], cwd=root)
|
|
941
|
-
success(f"Published {pkg_name} to Crates.io!")
|
|
942
|
-
except (RuntimeError, OSError) as e:
|
|
943
|
-
warn(
|
|
944
|
-
f"Crates.io publish failed ({e}). "
|
|
945
|
-
f"You can publish manually later with: cargo publish -p {pkg_name}"
|
|
946
|
-
)
|
|
947
|
-
else:
|
|
948
|
-
info(
|
|
949
|
-
f"Skipped cargo publish. Publish manually later using: "
|
|
950
|
-
f"cargo publish -p {pkg_name}"
|
|
951
|
-
)
|
|
952
|
-
|
|
953
|
-
# --- Step 8: Summary & Guidance ---
|
|
993
|
+
# --- Step 7: Summary & Guidance ---
|
|
954
994
|
print(f"\n{Style.BOLD}{'=' * 56}{Style.RESET}")
|
|
955
995
|
print(
|
|
956
996
|
f"{Style.GREEN}{Style.BOLD} "
|
|
@@ -966,14 +1006,18 @@ def main() -> None:
|
|
|
966
1006
|
f" It compiles multi-platform binaries and creates a "
|
|
967
1007
|
f"{Style.BOLD}Draft Release{Style.RESET}."
|
|
968
1008
|
)
|
|
969
|
-
print(
|
|
1009
|
+
print(
|
|
1010
|
+
f" 2. {Style.BOLD}Publish Draft on GitHub & Trigger crates.io + PyPI:"
|
|
1011
|
+
f"{Style.RESET}"
|
|
1012
|
+
)
|
|
970
1013
|
print(" • Navigate to your repository -> Releases.")
|
|
971
1014
|
print(
|
|
972
1015
|
f" • Open Draft Release {tag_name} and click "
|
|
973
1016
|
f"{Style.GREEN}'Publish Release'{Style.RESET}."
|
|
974
1017
|
)
|
|
975
|
-
print(" • This automatically triggers `.github/workflows/
|
|
976
|
-
print("
|
|
1018
|
+
print(" • This automatically triggers `.github/workflows/crates.yml` to")
|
|
1019
|
+
print(" publish the crate to crates.io and `.github/workflows/pypi.yml`")
|
|
1020
|
+
print(" to build and publish all Python wheels to PyPI.")
|
|
977
1021
|
print()
|
|
978
1022
|
|
|
979
1023
|
except KeyboardInterrupt:
|