git-patch 0.1.2 → 0.1.3
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.
- package/.github/workflows/ci.yml +9 -0
- package/lib/selector.js +27 -14
- package/package.json +2 -1
- package/test/coverage-edges.test.js +15 -0
- package/test/integration.test.js +38 -9
package/.github/workflows/ci.yml
CHANGED
|
@@ -28,3 +28,12 @@ jobs:
|
|
|
28
28
|
with:
|
|
29
29
|
node-version: ${{ matrix.node-version }}
|
|
30
30
|
- run: npm test
|
|
31
|
+
|
|
32
|
+
coverage:
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
- uses: actions/setup-node@v4
|
|
37
|
+
with:
|
|
38
|
+
node-version: 22
|
|
39
|
+
- run: npm run test:coverage
|
package/lib/selector.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Parse hunk/line selector strings.
|
|
3
3
|
*
|
|
4
4
|
* Formats:
|
|
5
|
-
* "1"
|
|
6
|
-
* "1,3,5"
|
|
7
|
-
* "1-5"
|
|
8
|
-
* "1-3,7,9"
|
|
9
|
-
* "1:2-4"
|
|
10
|
-
* "1:3,5,8"
|
|
5
|
+
* "1" -> { type: "hunks", ids: [1] }
|
|
6
|
+
* "1,3,5" -> { type: "hunks", ids: [1,3,5] }
|
|
7
|
+
* "1-5" -> { type: "hunks", ids: [1,2,3,4,5] }
|
|
8
|
+
* "1-3,7,9" -> { type: "hunks", ids: [1,2,3,7,9] }
|
|
9
|
+
* "1:2-4" -> { type: "lines", hunkId: 1, lines: [2,3,4] }
|
|
10
|
+
* "1:3,5,8" -> { type: "lines", hunkId: 1, lines: [3,5,8] }
|
|
11
11
|
*/
|
|
12
12
|
export function parseSelector(str) {
|
|
13
13
|
if (!str) throw new Error("No selector provided");
|
|
@@ -15,8 +15,12 @@ export function parseSelector(str) {
|
|
|
15
15
|
// Line-level: "hunkId:lineSpec"
|
|
16
16
|
if (str.includes(":")) {
|
|
17
17
|
let [hunkPart, linePart] = str.split(":", 2);
|
|
18
|
-
let
|
|
19
|
-
if (
|
|
18
|
+
let trimmedHunkPart = hunkPart.trim();
|
|
19
|
+
if (!/^\d+$/.test(trimmedHunkPart)) {
|
|
20
|
+
throw new Error(`Invalid hunk ID: ${hunkPart}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let hunkId = Number(trimmedHunkPart);
|
|
20
24
|
let lines = expandNumberSpec(linePart);
|
|
21
25
|
return { type: "lines", hunkId, lines };
|
|
22
26
|
}
|
|
@@ -29,17 +33,26 @@ export function parseSelector(str) {
|
|
|
29
33
|
function expandNumberSpec(spec) {
|
|
30
34
|
let parts = spec.split(",");
|
|
31
35
|
let result = [];
|
|
36
|
+
|
|
32
37
|
for (let part of parts) {
|
|
33
38
|
part = part.trim();
|
|
39
|
+
if (part.length === 0) throw new Error("Invalid number: ");
|
|
40
|
+
|
|
34
41
|
if (part.includes("-")) {
|
|
35
|
-
let
|
|
36
|
-
if (
|
|
42
|
+
let match = part.match(/^(\d+)\s*-\s*(\d+)$/);
|
|
43
|
+
if (!match) throw new Error(`Invalid range: ${part}`);
|
|
44
|
+
|
|
45
|
+
let start = Number(match[1]);
|
|
46
|
+
let end = Number(match[2]);
|
|
47
|
+
if (start > end) throw new Error(`Invalid range: ${part}`);
|
|
48
|
+
|
|
37
49
|
for (let i = start; i <= end; i++) result.push(i);
|
|
38
|
-
|
|
39
|
-
let n = parseInt(part, 10);
|
|
40
|
-
if (Number.isNaN(n)) throw new Error(`Invalid number: ${part}`);
|
|
41
|
-
result.push(n);
|
|
50
|
+
continue;
|
|
42
51
|
}
|
|
52
|
+
|
|
53
|
+
if (!/^\d+$/.test(part)) throw new Error(`Invalid number: ${part}`);
|
|
54
|
+
result.push(Number(part));
|
|
43
55
|
}
|
|
56
|
+
|
|
44
57
|
return result;
|
|
45
58
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-patch",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Non-interactive hunk staging for LLMs — stage, unstage, and discard git changes by hunk or line",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "node --test test/*.test.js",
|
|
11
|
+
"test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=100 --test-coverage-functions=100 --test-coverage-branches=100 '--test-coverage-include=bin/*.js' '--test-coverage-include=lib/**/*.js' test/*.test.js",
|
|
11
12
|
"lint": "biome check .",
|
|
12
13
|
"format": "biome format --write .",
|
|
13
14
|
"lint:fix": "biome check --write ."
|
|
@@ -56,6 +56,21 @@ describe("coverage edges", () => {
|
|
|
56
56
|
assert.throws(() => parseSelector("1:1-a"), /Invalid range: 1-a/);
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
+
it("throws for mixed hunk and line selectors", () => {
|
|
60
|
+
assert.throws(() => parseSelector("1,2:3"), /Invalid hunk ID: 1,2/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("throws for partially numeric selector tokens", () => {
|
|
64
|
+
assert.throws(() => parseSelector("1x"), /Invalid number: 1x/);
|
|
65
|
+
assert.throws(() => parseSelector("1-2x"), /Invalid range: 1-2x/);
|
|
66
|
+
assert.throws(() => parseSelector("1:2,3x"), /Invalid number: 3x/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("throws for empty selector segments and descending ranges", () => {
|
|
70
|
+
assert.throws(() => parseSelector("1,,2"), /Invalid number: /);
|
|
71
|
+
assert.throws(() => parseSelector("3-1"), /Invalid range: 3-1/);
|
|
72
|
+
});
|
|
73
|
+
|
|
59
74
|
it("finds line-level hunks across multiple files", () => {
|
|
60
75
|
let patch = buildPatchFromLines(
|
|
61
76
|
[
|
package/test/integration.test.js
CHANGED
|
@@ -7,6 +7,12 @@ import { after, before, beforeEach, describe, it } from "node:test";
|
|
|
7
7
|
|
|
8
8
|
let tmp;
|
|
9
9
|
let bin = join(import.meta.dirname, "..", "bin", "git-patch.js");
|
|
10
|
+
let gitIdentityEnv = {
|
|
11
|
+
GIT_AUTHOR_NAME: "Test",
|
|
12
|
+
GIT_AUTHOR_EMAIL: "test@test.com",
|
|
13
|
+
GIT_COMMITTER_NAME: "Test",
|
|
14
|
+
GIT_COMMITTER_EMAIL: "test@test.com",
|
|
15
|
+
};
|
|
10
16
|
|
|
11
17
|
function gp(args, opts = {}) {
|
|
12
18
|
return execSync(`node ${bin} ${args}`, {
|
|
@@ -14,10 +20,7 @@ function gp(args, opts = {}) {
|
|
|
14
20
|
encoding: "utf-8",
|
|
15
21
|
env: {
|
|
16
22
|
...process.env,
|
|
17
|
-
|
|
18
|
-
GIT_AUTHOR_EMAIL: "test@test.com",
|
|
19
|
-
GIT_COMMITTER_NAME: "Test",
|
|
20
|
-
GIT_COMMITTER_EMAIL: "test@test.com",
|
|
23
|
+
...gitIdentityEnv,
|
|
21
24
|
},
|
|
22
25
|
...opts,
|
|
23
26
|
}).trim();
|
|
@@ -30,10 +33,7 @@ function gpResult(args, opts = {}) {
|
|
|
30
33
|
encoding: "utf-8",
|
|
31
34
|
env: {
|
|
32
35
|
...process.env,
|
|
33
|
-
|
|
34
|
-
GIT_AUTHOR_EMAIL: "test@test.com",
|
|
35
|
-
GIT_COMMITTER_NAME: "Test",
|
|
36
|
-
GIT_COMMITTER_EMAIL: "test@test.com",
|
|
36
|
+
...gitIdentityEnv,
|
|
37
37
|
},
|
|
38
38
|
stdio: ["pipe", "pipe", "pipe"],
|
|
39
39
|
...opts,
|
|
@@ -49,7 +49,14 @@ function gpResult(args, opts = {}) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function git(args) {
|
|
52
|
-
return execSync(`git ${args}`, {
|
|
52
|
+
return execSync(`git ${args}`, {
|
|
53
|
+
cwd: tmp,
|
|
54
|
+
encoding: "utf-8",
|
|
55
|
+
env: {
|
|
56
|
+
...process.env,
|
|
57
|
+
...gitIdentityEnv,
|
|
58
|
+
},
|
|
59
|
+
}).trim();
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
function writeFile(name, content) {
|
|
@@ -428,6 +435,28 @@ describe("git-patch integration", () => {
|
|
|
428
435
|
assert.match(result.stderr, /Invalid hunk ID: x/);
|
|
429
436
|
});
|
|
430
437
|
|
|
438
|
+
it("fails on mixed hunk and line selectors", () => {
|
|
439
|
+
writeFile(
|
|
440
|
+
"src/app.js",
|
|
441
|
+
[
|
|
442
|
+
"function greet(name) {",
|
|
443
|
+
' return "Hi, " + name;',
|
|
444
|
+
"}",
|
|
445
|
+
"",
|
|
446
|
+
"function farewell(name) {",
|
|
447
|
+
' return "Goodbye, " + name;',
|
|
448
|
+
"}",
|
|
449
|
+
"",
|
|
450
|
+
"module.exports = { greet, farewell };",
|
|
451
|
+
"",
|
|
452
|
+
].join("\\n"),
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
let result = gpResult("stage 1,2:3");
|
|
456
|
+
assert.equal(result.status, 1);
|
|
457
|
+
assert.match(result.stderr, /Invalid hunk ID: 1,2/);
|
|
458
|
+
});
|
|
459
|
+
|
|
431
460
|
it("fails on invalid selector range", () => {
|
|
432
461
|
writeFile(
|
|
433
462
|
"src/app.js",
|