git-patch 0.1.2 → 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/.github/workflows/ci.yml +9 -0
- 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/lib/selector.js +27 -14
- package/package.json +2 -1
- package/test/coverage-edges.test.js +21 -1
- package/test/integration.test.js +167 -11
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/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/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.4",
|
|
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 ."
|
|
@@ -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
|
|
|
@@ -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
|
[
|
|
@@ -123,4 +138,9 @@ describe("coverage edges", () => {
|
|
|
123
138
|
let root = getRoot();
|
|
124
139
|
assert.equal(root, process.cwd());
|
|
125
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
|
+
});
|
|
126
146
|
});
|
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) {
|
|
@@ -193,6 +200,45 @@ describe("git-patch integration", () => {
|
|
|
193
200
|
assert.equal(out.files[0].hunks[0].id, 1);
|
|
194
201
|
});
|
|
195
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
|
+
|
|
196
242
|
it("outputs hunk summaries with --json --summary", () => {
|
|
197
243
|
writeFile(
|
|
198
244
|
"src/app.js",
|
|
@@ -313,7 +359,7 @@ describe("git-patch integration", () => {
|
|
|
313
359
|
fakeGit,
|
|
314
360
|
[
|
|
315
361
|
"#!/bin/sh",
|
|
316
|
-
'if [
|
|
362
|
+
'if [ "$1" = "diff" ]; then',
|
|
317
363
|
"cat <<'EOF'",
|
|
318
364
|
"diff --git a/demo.js b/demo.js",
|
|
319
365
|
"index 1111111..2222222 100644",
|
|
@@ -325,7 +371,10 @@ describe("git-patch integration", () => {
|
|
|
325
371
|
"EOF",
|
|
326
372
|
"exit 0",
|
|
327
373
|
"fi",
|
|
328
|
-
'
|
|
374
|
+
'if [ "$1" = "ls-files" ]; then',
|
|
375
|
+
"exit 0",
|
|
376
|
+
"fi",
|
|
377
|
+
'echo "unsupported command" >&2',
|
|
329
378
|
"exit 1",
|
|
330
379
|
"",
|
|
331
380
|
].join("\n"),
|
|
@@ -340,6 +389,78 @@ describe("git-patch integration", () => {
|
|
|
340
389
|
});
|
|
341
390
|
assert.match(out, /function demo\(\)/);
|
|
342
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
|
+
});
|
|
343
464
|
});
|
|
344
465
|
|
|
345
466
|
describe("cli", () => {
|
|
@@ -428,6 +549,28 @@ describe("git-patch integration", () => {
|
|
|
428
549
|
assert.match(result.stderr, /Invalid hunk ID: x/);
|
|
429
550
|
});
|
|
430
551
|
|
|
552
|
+
it("fails on mixed hunk and line selectors", () => {
|
|
553
|
+
writeFile(
|
|
554
|
+
"src/app.js",
|
|
555
|
+
[
|
|
556
|
+
"function greet(name) {",
|
|
557
|
+
' return "Hi, " + name;',
|
|
558
|
+
"}",
|
|
559
|
+
"",
|
|
560
|
+
"function farewell(name) {",
|
|
561
|
+
' return "Goodbye, " + name;',
|
|
562
|
+
"}",
|
|
563
|
+
"",
|
|
564
|
+
"module.exports = { greet, farewell };",
|
|
565
|
+
"",
|
|
566
|
+
].join("\\n"),
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
let result = gpResult("stage 1,2:3");
|
|
570
|
+
assert.equal(result.status, 1);
|
|
571
|
+
assert.match(result.stderr, /Invalid hunk ID: 1,2/);
|
|
572
|
+
});
|
|
573
|
+
|
|
431
574
|
it("fails on invalid selector range", () => {
|
|
432
575
|
writeFile(
|
|
433
576
|
"src/app.js",
|
|
@@ -696,6 +839,19 @@ describe("git-patch integration", () => {
|
|
|
696
839
|
assert.equal(stagedNames, "D\tsrc/utils.js");
|
|
697
840
|
assert.doesNotMatch(stagedNames, /dev\/null/);
|
|
698
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
|
+
});
|
|
699
855
|
});
|
|
700
856
|
|
|
701
857
|
describe("unstage", () => {
|