git-patch 0.1.3 → 0.1.4
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/README.md +4 -0
- package/lib/commands/list.js +1 -1
- package/lib/commands/stage.js +1 -1
- package/lib/git.js +56 -13
- package/package.json +1 -1
- package/test/coverage-edges.test.js +6 -1
- package/test/integration.test.js +129 -2
package/README.md
CHANGED
|
@@ -29,6 +29,8 @@ git-patch list --staged # Show staged hunks
|
|
|
29
29
|
git-patch list -- src/main.rs # Filter to specific files
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
Untracked files are included in unstaged output, so brand new files get hunk IDs without needing `git add -N`.
|
|
33
|
+
|
|
32
34
|
Each hunk gets a sequential ID. Change lines within each hunk are numbered too — these are what you use for line-level selection.
|
|
33
35
|
|
|
34
36
|
### Stage hunks
|
|
@@ -43,6 +45,8 @@ git-patch stage --all # Stage everything
|
|
|
43
45
|
git-patch stage --matching "TODO" # Stage hunks matching a regex
|
|
44
46
|
```
|
|
45
47
|
|
|
48
|
+
This also works for untracked files directly; no intent-to-add prep step required.
|
|
49
|
+
|
|
46
50
|
### Unstage hunks
|
|
47
51
|
|
|
48
52
|
Same selectors, operates on staged diff:
|
package/lib/commands/list.js
CHANGED
|
@@ -41,7 +41,7 @@ function summarizeHunks(fileDiffs) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export function run({ json = false, staged = false, files = [], summary = false } = {}) {
|
|
44
|
-
let raw = getDiff({ staged, files });
|
|
44
|
+
let raw = getDiff({ staged, files, includeUntracked: !staged });
|
|
45
45
|
let fileDiffs = parseDiff(raw);
|
|
46
46
|
|
|
47
47
|
if (json) {
|
package/lib/commands/stage.js
CHANGED
|
@@ -4,7 +4,7 @@ import { buildPatchFromHunks, buildPatchFromLines } from "../patch-builder.js";
|
|
|
4
4
|
import { parseSelector } from "../selector.js";
|
|
5
5
|
|
|
6
6
|
export function run({ selector, all = false, matching = null, files = [] } = {}) {
|
|
7
|
-
let raw = getDiff({ staged: false, files });
|
|
7
|
+
let raw = getDiff({ staged: false, files, includeUntracked: true });
|
|
8
8
|
let fileDiffs = parseDiff(raw);
|
|
9
9
|
|
|
10
10
|
if (fileDiffs.length === 0) {
|
package/lib/git.js
CHANGED
|
@@ -1,41 +1,84 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
2
|
|
|
3
|
-
function
|
|
4
|
-
return
|
|
3
|
+
function execGit(args, opts = {}) {
|
|
4
|
+
return execFileSync("git", args, {
|
|
5
5
|
encoding: "utf-8",
|
|
6
6
|
maxBuffer: 50 * 1024 * 1024,
|
|
7
7
|
...opts,
|
|
8
8
|
});
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
function execGitAllowStatus(args, allowedStatuses = [0], opts = {}) {
|
|
12
|
+
try {
|
|
13
|
+
return execGit(args, opts);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (allowedStatuses.includes(error.status)) {
|
|
16
|
+
return String(error.stdout || "");
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getUntrackedFiles(files) {
|
|
23
|
+
let args = ["ls-files", "--others", "--exclude-standard"];
|
|
24
|
+
if (files.length > 0) {
|
|
25
|
+
args.push("--");
|
|
26
|
+
args.push(...files);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return execGit(args)
|
|
30
|
+
.split("\n")
|
|
31
|
+
.map((line) => line.trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getUntrackedDiff(files) {
|
|
36
|
+
let patches = [];
|
|
37
|
+
|
|
38
|
+
for (let file of getUntrackedFiles(files)) {
|
|
39
|
+
let patch = execGitAllowStatus(["diff", "--no-index", "--", "/dev/null", file], [0, 1]).trim();
|
|
40
|
+
if (patch) patches.push(patch);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (patches.length === 0) return "";
|
|
44
|
+
return `${patches.join("\n")}\n`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getDiff({ staged = false, files = [], includeUntracked = false } = {}) {
|
|
48
|
+
let args = ["diff"];
|
|
13
49
|
if (staged) args.push("--cached");
|
|
14
|
-
// Full diff context isn't needed — default 3-line context is fine
|
|
15
50
|
if (files.length > 0) {
|
|
16
51
|
args.push("--");
|
|
17
52
|
args.push(...files);
|
|
18
53
|
}
|
|
19
|
-
|
|
54
|
+
let trackedDiff = execGit(args);
|
|
55
|
+
|
|
56
|
+
if (!includeUntracked || staged) {
|
|
57
|
+
return trackedDiff;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let untrackedDiff = getUntrackedDiff(files);
|
|
61
|
+
if (!untrackedDiff) return trackedDiff;
|
|
62
|
+
if (!trackedDiff.trim()) return untrackedDiff;
|
|
63
|
+
|
|
64
|
+
return `${trackedDiff.trim()}\n${untrackedDiff}`;
|
|
20
65
|
}
|
|
21
66
|
|
|
22
67
|
export function applyPatch(patch, { cached = false, reverse = false } = {}) {
|
|
23
|
-
let args = ["
|
|
68
|
+
let args = ["apply"];
|
|
24
69
|
if (cached) args.push("--cached");
|
|
25
70
|
if (reverse) args.push("--reverse");
|
|
26
71
|
args.push("-");
|
|
27
72
|
|
|
28
|
-
return
|
|
73
|
+
return execGit(args, {
|
|
29
74
|
input: patch,
|
|
30
|
-
encoding: "utf-8",
|
|
31
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
32
75
|
});
|
|
33
76
|
}
|
|
34
77
|
|
|
35
78
|
export function getStatus() {
|
|
36
|
-
return
|
|
79
|
+
return execGit(["status", "--porcelain"]);
|
|
37
80
|
}
|
|
38
81
|
|
|
39
82
|
export function getRoot() {
|
|
40
|
-
return
|
|
83
|
+
return execGit(["rev-parse", "--show-toplevel"]).trim();
|
|
41
84
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
3
|
import { parseDiff } from "../lib/diff-parser.js";
|
|
4
|
-
import { getRoot } from "../lib/git.js";
|
|
4
|
+
import { getDiff, getRoot } from "../lib/git.js";
|
|
5
5
|
import { buildPatchFromHunks, buildPatchFromLines } from "../lib/patch-builder.js";
|
|
6
6
|
import { parseSelector } from "../lib/selector.js";
|
|
7
7
|
|
|
@@ -138,4 +138,9 @@ describe("coverage edges", () => {
|
|
|
138
138
|
let root = getRoot();
|
|
139
139
|
assert.equal(root, process.cwd());
|
|
140
140
|
});
|
|
141
|
+
|
|
142
|
+
it("allows includeUntracked option with staged diffs", () => {
|
|
143
|
+
let out = getDiff({ staged: true, includeUntracked: true });
|
|
144
|
+
assert.equal(typeof out, "string");
|
|
145
|
+
});
|
|
141
146
|
});
|
package/test/integration.test.js
CHANGED
|
@@ -200,6 +200,45 @@ describe("git-patch integration", () => {
|
|
|
200
200
|
assert.equal(out.files[0].hunks[0].id, 1);
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
+
it("lists untracked files as unstaged hunks", () => {
|
|
204
|
+
writeFile(
|
|
205
|
+
"src/new-untracked.js",
|
|
206
|
+
['module.exports = "new";', "module.exports += '!';", ""].join("\n"),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
let out = JSON.parse(gp("list --json"));
|
|
210
|
+
assert.equal(out.type, "unstaged");
|
|
211
|
+
assert.equal(out.files.length, 1);
|
|
212
|
+
assert.equal(out.files[0].file, "src/new-untracked.js");
|
|
213
|
+
assert.equal(out.files[0].hunks.length, 1);
|
|
214
|
+
assert.equal(out.files[0].hunks[0].oldStart, 0);
|
|
215
|
+
assert.equal(out.files[0].hunks[0].oldCount, 0);
|
|
216
|
+
assert.equal(out.files[0].hunks[0].addedCount, 2);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("lists tracked and untracked hunks together", () => {
|
|
220
|
+
writeFile(
|
|
221
|
+
"src/app.js",
|
|
222
|
+
[
|
|
223
|
+
"function greet(name) {",
|
|
224
|
+
' return "Hi, " + name;',
|
|
225
|
+
"}",
|
|
226
|
+
"",
|
|
227
|
+
"function farewell(name) {",
|
|
228
|
+
' return "Goodbye, " + name;',
|
|
229
|
+
"}",
|
|
230
|
+
"",
|
|
231
|
+
"module.exports = { greet, farewell };",
|
|
232
|
+
"",
|
|
233
|
+
].join("\n"),
|
|
234
|
+
);
|
|
235
|
+
writeFile("src/added.js", "module.exports = 1;\n");
|
|
236
|
+
|
|
237
|
+
let out = JSON.parse(gp("list --json"));
|
|
238
|
+
let files = out.files.map((file) => file.file).sort();
|
|
239
|
+
assert.deepEqual(files, ["src/added.js", "src/app.js"]);
|
|
240
|
+
});
|
|
241
|
+
|
|
203
242
|
it("outputs hunk summaries with --json --summary", () => {
|
|
204
243
|
writeFile(
|
|
205
244
|
"src/app.js",
|
|
@@ -320,7 +359,7 @@ describe("git-patch integration", () => {
|
|
|
320
359
|
fakeGit,
|
|
321
360
|
[
|
|
322
361
|
"#!/bin/sh",
|
|
323
|
-
'if [
|
|
362
|
+
'if [ "$1" = "diff" ]; then',
|
|
324
363
|
"cat <<'EOF'",
|
|
325
364
|
"diff --git a/demo.js b/demo.js",
|
|
326
365
|
"index 1111111..2222222 100644",
|
|
@@ -332,7 +371,10 @@ describe("git-patch integration", () => {
|
|
|
332
371
|
"EOF",
|
|
333
372
|
"exit 0",
|
|
334
373
|
"fi",
|
|
335
|
-
'
|
|
374
|
+
'if [ "$1" = "ls-files" ]; then',
|
|
375
|
+
"exit 0",
|
|
376
|
+
"fi",
|
|
377
|
+
'echo "unsupported command" >&2',
|
|
336
378
|
"exit 1",
|
|
337
379
|
"",
|
|
338
380
|
].join("\n"),
|
|
@@ -347,6 +389,78 @@ describe("git-patch integration", () => {
|
|
|
347
389
|
});
|
|
348
390
|
assert.match(out, /function demo\(\)/);
|
|
349
391
|
});
|
|
392
|
+
|
|
393
|
+
it("surfaces unexpected untracked-diff failures", () => {
|
|
394
|
+
let fakeBin = join(tmp, "fake-bin-fail");
|
|
395
|
+
mkdirSync(fakeBin, { recursive: true });
|
|
396
|
+
|
|
397
|
+
let fakeGit = join(fakeBin, "git");
|
|
398
|
+
writeFileSync(
|
|
399
|
+
fakeGit,
|
|
400
|
+
[
|
|
401
|
+
"#!/bin/sh",
|
|
402
|
+
'if [ "$1" = "diff" ] && [ "$2" = "--no-index" ]; then',
|
|
403
|
+
'echo "no-index failure" >&2',
|
|
404
|
+
"exit 2",
|
|
405
|
+
"fi",
|
|
406
|
+
'if [ "$1" = "diff" ]; then',
|
|
407
|
+
"exit 0",
|
|
408
|
+
"fi",
|
|
409
|
+
'if [ "$1" = "ls-files" ]; then',
|
|
410
|
+
'echo "src/new-file.js"',
|
|
411
|
+
"exit 0",
|
|
412
|
+
"fi",
|
|
413
|
+
'echo "unsupported command" >&2',
|
|
414
|
+
"exit 1",
|
|
415
|
+
"",
|
|
416
|
+
].join("\n"),
|
|
417
|
+
);
|
|
418
|
+
chmodSync(fakeGit, 0o755);
|
|
419
|
+
|
|
420
|
+
let result = gpResult("list", {
|
|
421
|
+
env: {
|
|
422
|
+
...process.env,
|
|
423
|
+
PATH: `${fakeBin}:${process.env.PATH}`,
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
assert.equal(result.status, 1);
|
|
427
|
+
assert.match(result.stderr, /no-index failure/);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("ignores empty untracked diff output", () => {
|
|
431
|
+
let fakeBin = join(tmp, "fake-bin-empty");
|
|
432
|
+
mkdirSync(fakeBin, { recursive: true });
|
|
433
|
+
|
|
434
|
+
let fakeGit = join(fakeBin, "git");
|
|
435
|
+
writeFileSync(
|
|
436
|
+
fakeGit,
|
|
437
|
+
[
|
|
438
|
+
"#!/bin/sh",
|
|
439
|
+
'if [ "$1" = "diff" ] && [ "$2" = "--no-index" ]; then',
|
|
440
|
+
"exit 1",
|
|
441
|
+
"fi",
|
|
442
|
+
'if [ "$1" = "diff" ]; then',
|
|
443
|
+
"exit 0",
|
|
444
|
+
"fi",
|
|
445
|
+
'if [ "$1" = "ls-files" ]; then',
|
|
446
|
+
'echo "src/new-file.js"',
|
|
447
|
+
"exit 0",
|
|
448
|
+
"fi",
|
|
449
|
+
'echo "unsupported command" >&2',
|
|
450
|
+
"exit 1",
|
|
451
|
+
"",
|
|
452
|
+
].join("\n"),
|
|
453
|
+
);
|
|
454
|
+
chmodSync(fakeGit, 0o755);
|
|
455
|
+
|
|
456
|
+
let out = gp("list", {
|
|
457
|
+
env: {
|
|
458
|
+
...process.env,
|
|
459
|
+
PATH: `${fakeBin}:${process.env.PATH}`,
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
assert.equal(out, "No unstaged changes.");
|
|
463
|
+
});
|
|
350
464
|
});
|
|
351
465
|
|
|
352
466
|
describe("cli", () => {
|
|
@@ -725,6 +839,19 @@ describe("git-patch integration", () => {
|
|
|
725
839
|
assert.equal(stagedNames, "D\tsrc/utils.js");
|
|
726
840
|
assert.doesNotMatch(stagedNames, /dev\/null/);
|
|
727
841
|
});
|
|
842
|
+
|
|
843
|
+
it("stages untracked files without intent-to-add", () => {
|
|
844
|
+
writeFile(
|
|
845
|
+
"src/new-stage.js",
|
|
846
|
+
['module.exports = "new";', "module.exports += '!';", ""].join("\n"),
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
gp("stage 1");
|
|
850
|
+
|
|
851
|
+
let stagedNames = git("diff --cached --name-status");
|
|
852
|
+
assert.equal(stagedNames, "A\tsrc/new-stage.js");
|
|
853
|
+
assert.equal(gp("list"), "No unstaged changes.");
|
|
854
|
+
});
|
|
728
855
|
});
|
|
729
856
|
|
|
730
857
|
describe("unstage", () => {
|