Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions .github/workflows/ci-failure-email.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
name: CI Failure Issue

on:
workflow_run:
workflows:
- Publish Package to npmjs
types: [completed]

jobs:
notify:
if: contains(fromJSON('["failure","timed_out","action_required"]'), github.event.workflow_run.conclusion)
permissions:
issues: write
runs-on: ubuntu-latest
steps:
- name: Create or update failure issue
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
WORKFLOW_NAME: ${{ github.event.workflow_run.name }}
CONCLUSION: ${{ github.event.workflow_run.conclusion }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
ACTOR: ${{ github.event.workflow_run.actor.login }}
RUN_URL: ${{ github.event.workflow_run.html_url }}
run: |
reported_at="$(TZ=Asia/Shanghai date '+%Y-%m-%d %H:%M:%S %z')"
issue_title_prefix="[CI failed] $REPOSITORY / $WORKFLOW_NAME at "
body_file="$(mktemp)"
cat > "$body_file" <<EOF
@sunnylqm CI run failed.

Repository: $REPOSITORY
Workflow: $WORKFLOW_NAME
Conclusion: $CONCLUSION
Branch: $HEAD_BRANCH
Commit: $HEAD_SHA
Actor: $ACTOR
Reported at: $reported_at
Run: $RUN_URL
EOF

issue_title="$issue_title_prefix$reported_at"
issue_numbers="$(
gh issue list \
--repo "$REPOSITORY" \
--state open \
--limit 1000 \
--json number,title |
jq -r --arg prefix "$issue_title_prefix" '.[] | select(.title | startswith($prefix)) | .number'
)"

if [ -n "$issue_numbers" ]; then
while IFS= read -r issue_number; do
[ -n "$issue_number" ] || continue
gh issue edit "$issue_number" \
--repo "$REPOSITORY" \
--title "$issue_title" \
--body-file "$body_file"
done <<< "$issue_numbers"
exit 0
fi

gh issue create \
--repo "$REPOSITORY" \
--title "$issue_title" \
--body-file "$body_file"

close:
if: github.event.workflow_run.conclusion == 'success'
permissions:
issues: write
runs-on: ubuntu-latest
steps:
- name: Close recovered failure issues
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
WORKFLOW_NAME: ${{ github.event.workflow_run.name }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
RUN_URL: ${{ github.event.workflow_run.html_url }}
run: |
issue_title_prefix="[CI failed] $REPOSITORY / $WORKFLOW_NAME at "
fixed_at="$(TZ=Asia/Shanghai date '+%Y-%m-%d %H:%M:%S %z')"
issue_numbers="$(
gh issue list \
--repo "$REPOSITORY" \
--state open \
--limit 1000 \
--json number,title |
jq -r --arg prefix "$issue_title_prefix" '.[] | select(.title | startswith($prefix)) | .number'
)"

if [ -z "$issue_numbers" ]; then
exit 0
fi

comment_body="$(cat <<EOF
CI recovered at $fixed_at.

Repository: $REPOSITORY
Workflow: $WORKFLOW_NAME
Branch: $HEAD_BRANCH
Commit: $HEAD_SHA
Run: $RUN_URL
EOF
)"

while IFS= read -r issue_number; do
[ -n "$issue_number" ] || continue
gh issue close "$issue_number" \
--repo "$REPOSITORY" \
--comment "$comment_body"
done <<< "$issue_numbers"
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Test

on:
push:
branches: [main]
pull_request:

jobs:
test:
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- ubuntu-24.04-arm
- macos-15-intel
- macos-latest
- windows-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- uses: actions/setup-node@v6
with:
node-version: 24
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
# bun 在 Windows 上生成 exe shim,而 prebuildify 硬编码 spawn node-gyp.cmd
- if: runner.os == 'Windows'
run: npm install -g node-gyp
- run: bun install --ignore-scripts
- run: bun run prebuild
- run: node test/test.js
- run: bun ./test/test.js
17 changes: 11 additions & 6 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ jobs:
arch: x64
- os: ubuntu-24.04-arm
arch: arm64
# - os: macos-15-intel
# arch: x64
- os: macos-15-intel
arch: x64
- os: macos-latest
arch: arm64
# - os: windows-latest
# arch: x64
- os: windows-latest
arch: x64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
Expand All @@ -34,9 +34,14 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
# bun 在 Windows 上生成 exe shim,而 prebuildify 硬编码 spawn node-gyp.cmd
- if: runner.os == 'Windows'
run: npm install -g node-gyp
- run: bun install --ignore-scripts
- run: bun run prebuild
- uses: actions/upload-artifact@v6
- run: node test/test.js
- run: bun ./test/test.js
- uses: actions/upload-artifact@v7.0.1
with:
name: prebuilds-${{ matrix.os }}-${{ matrix.arch }}
path: prebuilds
Expand All @@ -62,4 +67,4 @@ jobs:
path: prebuilds
merge-multiple: true
- run: bun scripts/prepublish.ts
- run: npm publish --access public --tag latest
- run: npm publish --access public --tag latest --provenance
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[submodule "HDiffPatch"]
path = HDiffPatch
url = https://github.com/sisong/HDiffPatch.git
url = https://github.com/reactnativecn/HDiffPatch.git

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

sha=$(git submodule status --cached HDiffPatch | awk '{print $1}' | sed 's/^[+-]//')
git ls-remote https://github.com/reactnativecn/HDiffPatch.git "$sha"

Repository: reactnativecn/node-hdiffpatch

Length of output: 167


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'cached submodule SHA: '
git submodule status --cached -- HDiffPatch | awk '{print $1}' | sed 's/^[+-]//'

sha=$(git submodule status --cached -- HDiffPatch | awk '{print $1}' | sed 's/^[+-]//')

printf 'matching refs on reactnativecn/HDiffPatch:\n'
git ls-remote https://github.com/reactnativecn/HDiffPatch.git "$sha" || true

printf '\nexistence check against the remote commit object:\n'
git ls-remote https://github.com/reactnativecn/HDiffPatch.git | awk -v sha="$sha" '$1 == sha {found=1} END {exit found ? 0 : 1}'
echo "found=$?"

Repository: reactnativecn/node-hdiffpatch

Length of output: 332


Keep the HDiffPatch submodule on a remote that contains the pinned commit. The fork at reactnativecn/HDiffPatch.git does not expose 4a7eb78aee3bd5acbe6fe55621391685648030d5, so fresh clones and CI submodule updates will fail unless the submodule is repinned to a commit that exists on that remote.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitmodules at line 3, The HDiffPatch submodule remote is pointing to a
repository that does not contain the pinned commit, so update the .gitmodules
entry to a remote that actually exposes the existing HDiffPatch commit or repin
the submodule to a commit available on reactnativecn/HDiffPatch.git. Make sure
the submodule reference remains valid for fresh clones and CI by keeping the
commit and remote in sync for the HDiffPatch submodule entry.

[submodule "lzma"]
path = lzma
url = https://github.com/sisong/lzma.git
2 changes: 0 additions & 2 deletions .npmignore

This file was deleted.

2 changes: 1 addition & 1 deletion HDiffPatch
81 changes: 78 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,26 @@ Patch compatible with HDiffPatch -SD
## Installation

```bash
npm install node-hdiffpatch
# or
bun add node-hdiffpatch
```

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

Prebuilds are generated per target platform/arch. Build them on each target OS:
## Development

Clone with submodules, then build and test:

```bash
bun run prebuild
git clone --recursive https://github.com/reactnativecn/node-hdiffpatch.git
cd node-hdiffpatch
bun install --ignore-scripts
bun run prebuild # builds prebuilds/<platform>-<arch>/ for the current machine
bun run test # run tests under Node
bun run test:bun # run the same tests under the Bun runtime
```

## Usage
Expand All @@ -24,6 +35,60 @@ bun run prebuild

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

### diffWithCovers(originBuf, newBuf, covers[, options])

Create a standard hdiffpatch patch while using caller-provided cover lines when
possible. By default, the supplied covers replace HDiffPatch's internal cover
selection.

Each cover is `{ oldPos, newPos, len }`. Values may be numbers, decimal strings,
or bigint values. The returned object is:

```js
{
diff: Buffer,
usedCovers: boolean,
requestedCoverCount: number,
nativeCoverCapacity: number,
finalCoverCount: number,
coverMode: 'replace' | 'merge' | 'native-coalesce',
nativeCovers?: HpatchCover[],
finalCovers?: HpatchCover[]
}
```

The `diff` buffer is still a normal hdiffpatch payload and can be applied with
`patch(originBuf, diff)` or an HDiffPatch-compatible apply side.

Set `options.mode` to `merge` to keep HDiffPatch's native covers and only add
caller covers in new-file ranges not already covered by native covers:

```js
const merged = hdiff.diffWithCovers(oldBuf, newBuf, covers, { mode: 'merge' });
```

Set `options.mode` to `native-coalesce` to keep HDiffPatch's native cover
selection but coalesce adjacent native covers that have the same old/new offset
delta across small gaps. This remains a standard hdiffpatch payload and is useful
as a costed post-processing experiment:

```js
const coalesced = hdiff.diffWithCovers(oldBuf, newBuf, [], {
mode: 'native-coalesce',
});
```

Set `options.debugCovers` to `true` to include the native HDiffPatch cover list
and the final listener cover list in the return value. This is for diagnostics;
the default return shape avoids copying cover arrays.

### patchSingleStream(oldPath, diffPath, outNewPath[, cb])

Apply a single-compressed hpatch payload created by `diff` or `diffWithCovers`
from files. This is the file-level apply path for the normal in-memory `diff`
format. In sync mode returns `outNewPath`. In async mode, callback signature is
`(err, outNewPath)`.

### diffStream(oldPath, newPath, outDiffPath[, cb])

Create diff file by streaming file paths (low memory). In sync mode returns
Expand All @@ -43,3 +108,13 @@ After install, you can run:
hdp diff <oldFile> <newFile> <outDiff>
hdp patch <oldFile> <diffFile> <outNew>
```

Note: `hdp patch` auto-detects the diff format by its header, so it can apply
both streaming diffs created by `hdp diff` and single-compressed diffs created
by the in-memory `diff()` / `diffWithCovers()` APIs.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
## License

MIT. The prebuilt binaries statically include
[HDiffPatch](https://github.com/sisong/HDiffPatch) (MIT) and the
[LZMA SDK](https://github.com/sisong/lzma) (public domain).
27 changes: 26 additions & 1 deletion bin/hdiffpatch.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node
'use strict';

const fs = require('fs');
const hdiffpatch = require('..');

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

// 两种 diff 格式的文件头:流式为 "HDIFF13",单压缩(diff()/diffWithCovers() 产物)为 "HDIFFSF20"
function detectDiffFormat(diffFile) {
const header = Buffer.alloc(9);
const fd = fs.openSync(diffFile, 'r');
let bytesRead;
try {
bytesRead = fs.readSync(fd, header, 0, header.length, 0);
} finally {
fs.closeSync(fd);
}
const magic = header.slice(0, bytesRead).toString('latin1');
if (magic.startsWith('HDIFFSF20')) return 'single';
if (magic.startsWith('HDIFF13')) return 'stream';
return null;
}

function fail(msg) {
if (msg) console.error(`Error: ${msg}`);
usage();
Expand Down Expand Up @@ -50,7 +68,14 @@ if (cmd === 'patch') {
const diffFile = args[2];
const outNew = args[3];
try {
hdiffpatch.patchStream(oldFile, diffFile, outNew);
const format = detectDiffFormat(diffFile);
if (format === 'single') {
hdiffpatch.patchSingleStream(oldFile, diffFile, outNew);
} else if (format === 'stream') {
hdiffpatch.patchStream(oldFile, diffFile, outNew);
} else {
throw new Error(`${diffFile} is not a recognized hdiffpatch diff file.`);
}
console.log(outNew);
} catch (err) {
fail(err && err.message ? err.message : String(err));
Expand Down
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading