Skip to content

Commit 4fa14e3

Browse files
authored
Merge pull request #3 from reactnativecn/feat/covers-api-and-build-improvements
Add diffWithCovers/patchSingleStream APIs, robust loading, full CI matrix
2 parents 6ee6c4c + f5a0bc5 commit 4fa14e3

19 files changed

Lines changed: 1187 additions & 47 deletions
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
name: CI Failure Issue
2+
3+
on:
4+
workflow_run:
5+
workflows:
6+
- Publish Package to npmjs
7+
types: [completed]
8+
9+
jobs:
10+
notify:
11+
if: contains(fromJSON('["failure","timed_out","action_required"]'), github.event.workflow_run.conclusion)
12+
permissions:
13+
issues: write
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Create or update failure issue
17+
env:
18+
GH_TOKEN: ${{ github.token }}
19+
REPOSITORY: ${{ github.repository }}
20+
WORKFLOW_NAME: ${{ github.event.workflow_run.name }}
21+
CONCLUSION: ${{ github.event.workflow_run.conclusion }}
22+
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
23+
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
24+
ACTOR: ${{ github.event.workflow_run.actor.login }}
25+
RUN_URL: ${{ github.event.workflow_run.html_url }}
26+
run: |
27+
reported_at="$(TZ=Asia/Shanghai date '+%Y-%m-%d %H:%M:%S %z')"
28+
issue_title_prefix="[CI failed] $REPOSITORY / $WORKFLOW_NAME at "
29+
body_file="$(mktemp)"
30+
cat > "$body_file" <<EOF
31+
@sunnylqm CI run failed.
32+
33+
Repository: $REPOSITORY
34+
Workflow: $WORKFLOW_NAME
35+
Conclusion: $CONCLUSION
36+
Branch: $HEAD_BRANCH
37+
Commit: $HEAD_SHA
38+
Actor: $ACTOR
39+
Reported at: $reported_at
40+
Run: $RUN_URL
41+
EOF
42+
43+
issue_title="$issue_title_prefix$reported_at"
44+
issue_numbers="$(
45+
gh issue list \
46+
--repo "$REPOSITORY" \
47+
--state open \
48+
--limit 1000 \
49+
--json number,title |
50+
jq -r --arg prefix "$issue_title_prefix" '.[] | select(.title | startswith($prefix)) | .number'
51+
)"
52+
53+
if [ -n "$issue_numbers" ]; then
54+
while IFS= read -r issue_number; do
55+
[ -n "$issue_number" ] || continue
56+
gh issue edit "$issue_number" \
57+
--repo "$REPOSITORY" \
58+
--title "$issue_title" \
59+
--body-file "$body_file"
60+
done <<< "$issue_numbers"
61+
exit 0
62+
fi
63+
64+
gh issue create \
65+
--repo "$REPOSITORY" \
66+
--title "$issue_title" \
67+
--body-file "$body_file"
68+
69+
close:
70+
if: github.event.workflow_run.conclusion == 'success'
71+
permissions:
72+
issues: write
73+
runs-on: ubuntu-latest
74+
steps:
75+
- name: Close recovered failure issues
76+
env:
77+
GH_TOKEN: ${{ github.token }}
78+
REPOSITORY: ${{ github.repository }}
79+
WORKFLOW_NAME: ${{ github.event.workflow_run.name }}
80+
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
81+
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
82+
RUN_URL: ${{ github.event.workflow_run.html_url }}
83+
run: |
84+
issue_title_prefix="[CI failed] $REPOSITORY / $WORKFLOW_NAME at "
85+
fixed_at="$(TZ=Asia/Shanghai date '+%Y-%m-%d %H:%M:%S %z')"
86+
issue_numbers="$(
87+
gh issue list \
88+
--repo "$REPOSITORY" \
89+
--state open \
90+
--limit 1000 \
91+
--json number,title |
92+
jq -r --arg prefix "$issue_title_prefix" '.[] | select(.title | startswith($prefix)) | .number'
93+
)"
94+
95+
if [ -z "$issue_numbers" ]; then
96+
exit 0
97+
fi
98+
99+
comment_body="$(cat <<EOF
100+
CI recovered at $fixed_at.
101+
102+
Repository: $REPOSITORY
103+
Workflow: $WORKFLOW_NAME
104+
Branch: $HEAD_BRANCH
105+
Commit: $HEAD_SHA
106+
Run: $RUN_URL
107+
EOF
108+
)"
109+
110+
while IFS= read -r issue_number; do
111+
[ -n "$issue_number" ] || continue
112+
gh issue close "$issue_number" \
113+
--repo "$REPOSITORY" \
114+
--comment "$comment_body"
115+
done <<< "$issue_numbers"

.github/workflows/ci.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
strategy:
11+
fail-fast: false
12+
matrix:
13+
os:
14+
- ubuntu-latest
15+
- ubuntu-24.04-arm
16+
- macos-15-intel
17+
- macos-latest
18+
- windows-latest
19+
runs-on: ${{ matrix.os }}
20+
steps:
21+
- uses: actions/checkout@v5
22+
with:
23+
submodules: recursive
24+
- uses: actions/setup-node@v6
25+
with:
26+
node-version: 24
27+
- uses: oven-sh/setup-bun@v2
28+
with:
29+
bun-version: latest
30+
# bun 在 Windows 上生成 exe shim,而 prebuildify 硬编码 spawn node-gyp.cmd
31+
- if: runner.os == 'Windows'
32+
run: npm install -g node-gyp
33+
- run: bun install --ignore-scripts
34+
- run: bun run prebuild
35+
- run: node test/test.js
36+
- run: bun ./test/test.js

.github/workflows/publish.yml

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ jobs:
1717
arch: x64
1818
- os: ubuntu-24.04-arm
1919
arch: arm64
20-
# - os: macos-15-intel
21-
# arch: x64
20+
- os: macos-15-intel
21+
arch: x64
2222
- os: macos-latest
2323
arch: arm64
24-
# - os: windows-latest
25-
# arch: x64
24+
- os: windows-latest
25+
arch: x64
2626
runs-on: ${{ matrix.os }}
2727
steps:
2828
- uses: actions/checkout@v5
@@ -34,9 +34,14 @@ jobs:
3434
- uses: oven-sh/setup-bun@v2
3535
with:
3636
bun-version: latest
37+
# bun 在 Windows 上生成 exe shim,而 prebuildify 硬编码 spawn node-gyp.cmd
38+
- if: runner.os == 'Windows'
39+
run: npm install -g node-gyp
3740
- run: bun install --ignore-scripts
3841
- run: bun run prebuild
39-
- uses: actions/upload-artifact@v6
42+
- run: node test/test.js
43+
- run: bun ./test/test.js
44+
- uses: actions/upload-artifact@v7.0.1
4045
with:
4146
name: prebuilds-${{ matrix.os }}-${{ matrix.arch }}
4247
path: prebuilds
@@ -62,4 +67,4 @@ jobs:
6267
path: prebuilds
6368
merge-multiple: true
6469
- run: bun scripts/prepublish.ts
65-
- run: npm publish --access public --tag latest
70+
- run: npm publish --access public --tag latest --provenance

.gitmodules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[submodule "HDiffPatch"]
22
path = HDiffPatch
3-
url = https://github.com/sisong/HDiffPatch.git
3+
url = https://github.com/reactnativecn/HDiffPatch.git
44
[submodule "lzma"]
55
path = lzma
66
url = https://github.com/sisong/lzma.git

.npmignore

Lines changed: 0 additions & 2 deletions
This file was deleted.

HDiffPatch

README.md

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,26 @@ Patch compatible with HDiffPatch -SD
77
## Installation
88

99
```bash
10+
npm install node-hdiffpatch
11+
# or
1012
bun add node-hdiffpatch
1113
```
1214

13-
## Prebuilds
15+
Prebuilt binaries are bundled for: `darwin-arm64`, `darwin-x64`, `linux-x64`,
16+
`linux-arm64` (glibc), and `win32-x64`. Other platforms are not supported by
17+
the published package.
1418

15-
Prebuilds are generated per target platform/arch. Build them on each target OS:
19+
## Development
20+
21+
Clone with submodules, then build and test:
1622

1723
```bash
18-
bun run prebuild
24+
git clone --recursive https://github.com/reactnativecn/node-hdiffpatch.git
25+
cd node-hdiffpatch
26+
bun install --ignore-scripts
27+
bun run prebuild # builds prebuilds/<platform>-<arch>/ for the current machine
28+
bun run test # run tests under Node
29+
bun run test:bun # run the same tests under the Bun runtime
1930
```
2031

2132
## Usage
@@ -24,6 +35,60 @@ bun run prebuild
2435

2536
Compare two buffers and return a new hdiffpatch patch as return value.
2637

38+
### diffWithCovers(originBuf, newBuf, covers[, options])
39+
40+
Create a standard hdiffpatch patch while using caller-provided cover lines when
41+
possible. By default, the supplied covers replace HDiffPatch's internal cover
42+
selection.
43+
44+
Each cover is `{ oldPos, newPos, len }`. Values may be numbers, decimal strings,
45+
or bigint values. The returned object is:
46+
47+
```js
48+
{
49+
diff: Buffer,
50+
usedCovers: boolean,
51+
requestedCoverCount: number,
52+
nativeCoverCapacity: number,
53+
finalCoverCount: number,
54+
coverMode: 'replace' | 'merge' | 'native-coalesce',
55+
nativeCovers?: HpatchCover[],
56+
finalCovers?: HpatchCover[]
57+
}
58+
```
59+
60+
The `diff` buffer is still a normal hdiffpatch payload and can be applied with
61+
`patch(originBuf, diff)` or an HDiffPatch-compatible apply side.
62+
63+
Set `options.mode` to `merge` to keep HDiffPatch's native covers and only add
64+
caller covers in new-file ranges not already covered by native covers:
65+
66+
```js
67+
const merged = hdiff.diffWithCovers(oldBuf, newBuf, covers, { mode: 'merge' });
68+
```
69+
70+
Set `options.mode` to `native-coalesce` to keep HDiffPatch's native cover
71+
selection but coalesce adjacent native covers that have the same old/new offset
72+
delta across small gaps. This remains a standard hdiffpatch payload and is useful
73+
as a costed post-processing experiment:
74+
75+
```js
76+
const coalesced = hdiff.diffWithCovers(oldBuf, newBuf, [], {
77+
mode: 'native-coalesce',
78+
});
79+
```
80+
81+
Set `options.debugCovers` to `true` to include the native HDiffPatch cover list
82+
and the final listener cover list in the return value. This is for diagnostics;
83+
the default return shape avoids copying cover arrays.
84+
85+
### patchSingleStream(oldPath, diffPath, outNewPath[, cb])
86+
87+
Apply a single-compressed hpatch payload created by `diff` or `diffWithCovers`
88+
from files. This is the file-level apply path for the normal in-memory `diff`
89+
format. In sync mode returns `outNewPath`. In async mode, callback signature is
90+
`(err, outNewPath)`.
91+
2792
### diffStream(oldPath, newPath, outDiffPath[, cb])
2893

2994
Create diff file by streaming file paths (low memory). In sync mode returns
@@ -43,3 +108,13 @@ After install, you can run:
43108
hdp diff <oldFile> <newFile> <outDiff>
44109
hdp patch <oldFile> <diffFile> <outNew>
45110
```
111+
112+
Note: `hdp patch` auto-detects the diff format by its header, so it can apply
113+
both streaming diffs created by `hdp diff` and single-compressed diffs created
114+
by the in-memory `diff()` / `diffWithCovers()` APIs.
115+
116+
## License
117+
118+
MIT. The prebuilt binaries statically include
119+
[HDiffPatch](https://github.com/sisong/HDiffPatch) (MIT) and the
120+
[LZMA SDK](https://github.com/sisong/lzma) (public domain).

bin/hdiffpatch.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env node
22
'use strict';
33

4+
const fs = require('fs');
45
const hdiffpatch = require('..');
56

67
function usage() {
@@ -12,11 +13,28 @@ function usage() {
1213
'',
1314
'Notes:',
1415
' - Uses streaming diff/patch for low memory usage.',
16+
' - patch auto-detects the diff format (diffStream or diff/diffWithCovers output).',
1517
' - Outputs are files specified by <outDiff>/<outNew>.',
1618
].join('\n')
1719
);
1820
}
1921

22+
// 两种 diff 格式的文件头:流式为 "HDIFF13",单压缩(diff()/diffWithCovers() 产物)为 "HDIFFSF20"
23+
function detectDiffFormat(diffFile) {
24+
const header = Buffer.alloc(9);
25+
const fd = fs.openSync(diffFile, 'r');
26+
let bytesRead;
27+
try {
28+
bytesRead = fs.readSync(fd, header, 0, header.length, 0);
29+
} finally {
30+
fs.closeSync(fd);
31+
}
32+
const magic = header.slice(0, bytesRead).toString('latin1');
33+
if (magic.startsWith('HDIFFSF20')) return 'single';
34+
if (magic.startsWith('HDIFF13')) return 'stream';
35+
return null;
36+
}
37+
2038
function fail(msg) {
2139
if (msg) console.error(`Error: ${msg}`);
2240
usage();
@@ -50,7 +68,14 @@ if (cmd === 'patch') {
5068
const diffFile = args[2];
5169
const outNew = args[3];
5270
try {
53-
hdiffpatch.patchStream(oldFile, diffFile, outNew);
71+
const format = detectDiffFormat(diffFile);
72+
if (format === 'single') {
73+
hdiffpatch.patchSingleStream(oldFile, diffFile, outNew);
74+
} else if (format === 'stream') {
75+
hdiffpatch.patchStream(oldFile, diffFile, outNew);
76+
} else {
77+
throw new Error(`${diffFile} is not a recognized hdiffpatch diff file.`);
78+
}
5479
console.log(outNew);
5580
} catch (err) {
5681
fail(err && err.message ? err.message : String(err));

bun.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)