@signalridge/pi-worktree 0.49.4 → 1.2.1
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/CHANGELOG.md +30 -0
- package/README.md +4 -2
- package/package.json +1 -1
- package/src/command.ts +120 -25
- package/src/git.ts +68 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.1
|
|
4
|
+
### Patch Changes
|
|
5
|
+
|
|
6
|
+
- 24a8af4: Remove references to the maintainer's personal dotfile setup from shipped docs
|
|
7
|
+
and source comments. Each constraint is restated as a property of the package
|
|
8
|
+
itself: which manager owns a settings file, whether a project permits git
|
|
9
|
+
worktrees, and installing a package from a single source. No runtime behavior
|
|
10
|
+
changes.
|
|
11
|
+
|
|
12
|
+
## 1.0.0
|
|
13
|
+
### Major Changes
|
|
14
|
+
|
|
15
|
+
- Adopt a unified 1.0.0 across every published package.
|
|
16
|
+
|
|
17
|
+
The version numbers no longer track their upstream forks individually; from this release each package
|
|
18
|
+
is versioned on its own merit against the Signalridge line, and 1.0.0 is the shared starting point.
|
|
19
|
+
Packages whose behavior changed in this release document that change in their own changeset entries;
|
|
20
|
+
the remainder are re-released unchanged so the whole set shares one baseline.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Strip Unicode bidirectional overrides — and DCS/PM/APC payloads in the footer and recall picker — from untrusted text before display, so a crafted branch name, worktree path, model id, or recalled message can no longer visually reverse what the user reads and acts on.
|
|
25
|
+
- Wire the hardened terminal sanitizers to the fields that actually carry hostile text, bound the escape-sequence skippers, and give worktree Add base-commit provenance.
|
|
26
|
+
|
|
27
|
+
- `pi-statusline`: the `branch`, `provider`, and `tools` segments now pass through `sanitizeTerminalText`. The branch is sanitized before the GitHub PR context is composed in, so a hostile ref name can no longer emit raw OSC/CSI/bidi into the footer while the PR hyperlink keeps working.
|
|
28
|
+
- `pi-statusline` / `pi-recall`: an escape-sequence introducer with no terminator now drops only itself instead of consuming the rest of the string, so a single `0x90` in a branch name or saved message no longer blanks a footer row, a preview, or the fuzzy-search text. SOS is handled alongside DCS/PM/APC.
|
|
29
|
+
- `pi-recall`: previews are sanitized before truncation, so the preview budget is spent on visible characters instead of invisible escape bytes.
|
|
30
|
+
- `pi-worktree`: line separators are replaced with a space instead of being dropped, so multi-line Git output no longer welds two records together inside a destructive confirmation body.
|
|
31
|
+
- `pi-worktree`: Add resolves the base commit for both the create and the attach case (the attach case previously captured none, letting Git re-resolve the branch at exec time), shows the full base OID in the confirmation, and re-reads `git worktree list`, the local branch ref, and the base OID under the worktree mutation lock immediately before `git worktree add`. A base that moved, or a branch or path claimed while the dialog was open, refuses the add with a message naming both OIDs instead of silently creating a worktree from an unapproved base.
|
|
32
|
+
|
|
3
33
|
## 0.49.4
|
|
4
34
|
### Patch Changes
|
|
5
35
|
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Pi cannot change its parent process working directory with `cd`. This extension performs the safe equivalent: it prepares a Pi session whose cwd is the selected worktree and switches to that session, preserving the current conversation when it has already been persisted.
|
|
8
8
|
|
|
9
|
-
This package is the full interactive worktree manager. It is publishable and stable,
|
|
9
|
+
This package is the full interactive worktree manager. It is publishable and stable, and opt-in: install it only if your project allows git worktrees. Some repositories forbid them outright — a worktree placed inside the source tree can be picked up by tooling that scans the tree, so check your repository's own guidance before enabling it.
|
|
10
10
|
|
|
11
11
|
## ✨ Features
|
|
12
12
|
|
|
@@ -52,7 +52,7 @@ Run the command without arguments:
|
|
|
52
52
|
|
|
53
53
|
Choose one action:
|
|
54
54
|
|
|
55
|
-
- **Add worktree** — enter a branch, optional start point, and optional path;
|
|
55
|
+
- **Add worktree** — enter a branch, optional start point, and optional path; the confirmation names the branch, base ref, full base commit, and target path before creation, then optionally switch.
|
|
56
56
|
- **Switch worktree** — select another existing worktree and continue this Pi conversation there.
|
|
57
57
|
- **Remove worktree** — remove a linked worktree without deleting its branch; local ignored data must be removed first.
|
|
58
58
|
- **Prune stale metadata** — inspect Git's dry-run output, then optionally run the matching prune.
|
|
@@ -129,8 +129,10 @@ A successfully created Git worktree is never rolled back merely because Pi sessi
|
|
|
129
129
|
- Removal and prune inspect reflogs, pseudorefs, per-worktree refs, and `FETCH_HEAD`. Historical commits reachable only through this administrative recovery state are listed by full OID in the destructive confirmation; approval removes those recovery pointers, so Git may later garbage-collect the commits. Create a branch or tag instead when any listed commit should survive.
|
|
130
130
|
- Staged-only administrative index state, a missing attached branch ref, or an unreachable current detached HEAD still blocks prune without an override.
|
|
131
131
|
- Removal never deletes a branch and never uses `--force`.
|
|
132
|
+
- Worktree paths, branches, and lock reasons are stripped of terminal control characters and bidirectional overrides before display, so a crafted branch name cannot make the menu read as a different worktree than the one a removal acts on. Line separators become a single space rather than disappearing, so multi-line Git output in a destructive confirmation never welds two records into one.
|
|
132
133
|
- Safe removal invokes argv-based `git worktree move <path> <quarantine>` before validation, moves the real tree to a private tombstone, reserves the registered path with an exclusive non-directory entry, and uses an isolated `git worktree prune --expire now` for metadata only. Failed pre-removal recovery uses the inverse Git move and retains an unsafe quarantine; production runtime never invokes a shell, `rm`, or `rm -rf`.
|
|
133
134
|
- Prune always runs `git worktree prune --dry-run --verbose` before confirmation, inspects candidates omitted from porcelain, rechecks the exact preview and recovery-risk set after confirmation, and uses Git's default expiry. Remove likewise rechecks worktree identity, filesystem path identity, inventory immediately before deletion, administrative path, and the approved recovery-risk set before mutation.
|
|
135
|
+
- Add resolves the base commit before confirmation — for a new branch from the requested start point, and for an existing branch from `refs/heads/<branch>` so a same-named tag cannot stand in — shows branch, base ref, full base OID, and path in the confirmation, and then re-reads `git worktree list`, the local branch ref, and the base OID under the worktree mutation lock immediately before `git worktree add`. A base that moved, a branch that was created, deleted, or checked out elsewhere, or a path that was claimed while the dialog was open refuses the add instead of creating a worktree from a base the user did not approve; the message names both OIDs so you can re-run Add to approve the new base. The created worktree is verified against the approved branch ref and base OID.
|
|
134
136
|
- The extension does not expose commit, push, rebase, repair, user-requested move, lock, or unlock worktree actions.
|
|
135
137
|
|
|
136
138
|
Use Git directly when you intentionally need force removal, branch deletion, custom prune expiry, detach/orphan creation, move, repair, lock, or unlock behavior.
|
package/package.json
CHANGED
package/src/command.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
formatWorktree,
|
|
15
15
|
listWorktrees,
|
|
16
16
|
localBranchExists,
|
|
17
|
+
localBranchRef,
|
|
17
18
|
pathEntryExists,
|
|
18
19
|
pathIdentity,
|
|
19
20
|
pathsEqual,
|
|
@@ -58,6 +59,15 @@ interface AdministrativeHistoryRisk {
|
|
|
58
59
|
oids: string[];
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/** The base commit the user approved, captured before the confirmation and re-checked after it. */
|
|
63
|
+
interface AddBaseProvenance {
|
|
64
|
+
kind: "create" | "attach";
|
|
65
|
+
/** Human-facing base, e.g. "main" for a new branch or "refs/heads/feature" for an attach. */
|
|
66
|
+
label: string;
|
|
67
|
+
/** Lowercased full OID that `label` resolved to when the preview was built. */
|
|
68
|
+
oid: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
export function registerWorktreeCommand(
|
|
62
72
|
pi: ExtensionAPI,
|
|
63
73
|
settings: WorktreeSettingsRuntime,
|
|
@@ -179,9 +189,18 @@ async function addFlow(
|
|
|
179
189
|
throw new Error(`Branch ${branch} is already checked out at ${occupied.path}.`);
|
|
180
190
|
}
|
|
181
191
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
192
|
+
// Capture the base commit for BOTH cases. The attach case never had one: argv is
|
|
193
|
+
// ["worktree", "add", <path>, <branch>], so Git resolves the branch at exec time and a
|
|
194
|
+
// concurrent `git branch -f` or rebase silently moved the base out from under the user.
|
|
195
|
+
let provenance: AddBaseProvenance;
|
|
196
|
+
if (branchExists) {
|
|
197
|
+
const ref = localBranchRef(branch);
|
|
198
|
+
provenance = {
|
|
199
|
+
kind: "attach",
|
|
200
|
+
label: ref,
|
|
201
|
+
oid: (await resolveCommit(pi, ctx.cwd, ref, ctx.signal)).toLowerCase(),
|
|
202
|
+
};
|
|
203
|
+
} else {
|
|
185
204
|
const defaultStart = await symbolicBranch(pi, ctx.cwd, ctx.signal);
|
|
186
205
|
const requestedStart = await ctx.ui.input(
|
|
187
206
|
stripTerminalControls(
|
|
@@ -192,9 +211,13 @@ async function addFlow(
|
|
|
192
211
|
stripTerminalControls(defaultStart ?? "commit-ish"),
|
|
193
212
|
);
|
|
194
213
|
if (requestedStart === undefined) return;
|
|
195
|
-
startLabel = requestedStart.trim() || defaultStart;
|
|
214
|
+
const startLabel = requestedStart.trim() || defaultStart;
|
|
196
215
|
if (!startLabel) throw new Error("An explicit start point is required from detached HEAD.");
|
|
197
|
-
|
|
216
|
+
provenance = {
|
|
217
|
+
kind: "create",
|
|
218
|
+
label: startLabel,
|
|
219
|
+
oid: (await resolveCommit(pi, ctx.cwd, startLabel, ctx.signal)).toLowerCase(),
|
|
220
|
+
};
|
|
198
221
|
}
|
|
199
222
|
|
|
200
223
|
const suggestedPath = defaultWorktreePath(main.path, branch, worktreeRoot);
|
|
@@ -210,27 +233,95 @@ async function addFlow(
|
|
|
210
233
|
throw new Error(`The target path is already registered as a worktree: ${pathCollision.path}.`);
|
|
211
234
|
}
|
|
212
235
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
236
|
+
// Sanitize each line, then join: stripTerminalControls maps a newline to a space, so stripping
|
|
237
|
+
// the joined string would collapse the preview back onto one line.
|
|
238
|
+
const summary = [
|
|
239
|
+
provenance.kind === "attach" ? `Attach existing branch ${branch}.` : `Create branch ${branch}.`,
|
|
240
|
+
`Branch: ${branch}`,
|
|
241
|
+
`Base: ${provenance.label}`,
|
|
242
|
+
`Base commit: ${provenance.oid}`,
|
|
243
|
+
`Path: ${targetPath}`,
|
|
244
|
+
]
|
|
245
|
+
.map((line) => stripTerminalControls(line))
|
|
246
|
+
.join("\n");
|
|
247
|
+
if (!(await ctx.ui.confirm("Create Git worktree", summary))) return;
|
|
248
|
+
|
|
249
|
+
// Re-check under the mutation lock, mirroring pruneFlow: `records` was captured when the menu
|
|
250
|
+
// opened, so every pre-confirmation check above is stale by the whole menu lifetime. The lock
|
|
251
|
+
// cannot stop an external `git branch -f` — that is what the base-OID check is for — but it does
|
|
252
|
+
// shrink the window against this package's own flows, and it never spans a dialog.
|
|
253
|
+
const created = await withWorktreeMutationLock(
|
|
254
|
+
ctx.cwd,
|
|
255
|
+
async () => {
|
|
256
|
+
assertTargetFilesystemAvailable(targetPath);
|
|
217
257
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
258
|
+
const latest = await listWorktrees(pi, ctx.cwd, ctx.signal);
|
|
259
|
+
const occupiedNow = worktreeForBranch(latest, branch);
|
|
260
|
+
if (occupiedNow) {
|
|
261
|
+
throw new Error(`Branch ${branch} was checked out at ${occupiedNow.path} after confirmation; add was refused.`);
|
|
262
|
+
}
|
|
263
|
+
const collisionNow = latest.find((record) => pathsEqual(record.path, targetPath));
|
|
264
|
+
if (collisionNow) {
|
|
265
|
+
throw new Error(`The target path became a registered worktree after confirmation: ${collisionNow.path}.`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const existsNow = await localBranchExists(pi, ctx.cwd, branch, ctx.signal);
|
|
269
|
+
if (existsNow !== branchExists) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
existsNow
|
|
272
|
+
? `Branch ${branch} was created by another process after confirmation; add was refused.`
|
|
273
|
+
: `Branch ${branch} was deleted after confirmation; add was refused.`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const currentOid = (
|
|
278
|
+
await resolveCommit(
|
|
279
|
+
pi,
|
|
280
|
+
ctx.cwd,
|
|
281
|
+
provenance.kind === "attach" ? localBranchRef(branch) : provenance.label,
|
|
282
|
+
ctx.signal,
|
|
283
|
+
)
|
|
284
|
+
).toLowerCase();
|
|
285
|
+
if (!sameOid(currentOid, provenance.oid)) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`${provenance.label} moved from ${provenance.oid} to ${currentOid} after confirmation; add was refused. Run Add again to approve the new base.`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
await addWorktree(
|
|
292
|
+
pi,
|
|
293
|
+
ctx.cwd,
|
|
294
|
+
{
|
|
295
|
+
path: targetPath,
|
|
296
|
+
branch,
|
|
297
|
+
startOid: provenance.kind === "create" ? provenance.oid : undefined,
|
|
298
|
+
},
|
|
299
|
+
ctx.signal,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const updated = await listWorktrees(pi, ctx.cwd, ctx.signal);
|
|
304
|
+
const verified = updated.find((record) => pathsEqual(record.path, targetPath));
|
|
305
|
+
// branchRef is stricter than branch: a deleted branch shadowed by a same-named tag produces
|
|
306
|
+
// a detached record whose branchRef is undefined.
|
|
307
|
+
if (
|
|
308
|
+
!verified ||
|
|
309
|
+
verified.branchRef !== localBranchRef(branch) ||
|
|
310
|
+
!verified.head ||
|
|
311
|
+
!sameOid(verified.head, provenance.oid)
|
|
312
|
+
) {
|
|
313
|
+
throw new Error("the expected path, branch, and base commit were not present in Git porcelain output");
|
|
314
|
+
}
|
|
315
|
+
return verified;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
`Git add completed, so the worktree was retained at ${targetPath}, but verification failed: ${formatError(error)}. Inspect git worktree list before retrying.`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
ctx.signal,
|
|
323
|
+
);
|
|
324
|
+
safeNotify(ctx, `Created worktree ${targetPath} on branch ${branch} at ${provenance.oid}.`, "info");
|
|
234
325
|
|
|
235
326
|
if (
|
|
236
327
|
await ctx.ui.confirm("Switch Pi workspace?", stripTerminalControls(`Continue this conversation in ${targetPath}?`))
|
|
@@ -243,6 +334,10 @@ async function addFlow(
|
|
|
243
334
|
}
|
|
244
335
|
}
|
|
245
336
|
|
|
337
|
+
function sameOid(left: string, right: string): boolean {
|
|
338
|
+
return left.toLowerCase() === right.toLowerCase();
|
|
339
|
+
}
|
|
340
|
+
|
|
246
341
|
function assertTargetFilesystemAvailable(targetPath: string): void {
|
|
247
342
|
if (pathEntryExists(targetPath)) {
|
|
248
343
|
throw new Error(`The target path already exists: ${targetPath}.`);
|
package/src/git.ts
CHANGED
|
@@ -137,8 +137,17 @@ export function parseWorktreePorcelain(output: string): WorktreeRecord[] {
|
|
|
137
137
|
return records;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Fully qualify a local branch. Provenance must be resolved through this ref rather than the bare
|
|
142
|
+
* name: `git rev-parse` prefers `refs/tags/<name>`, so a same-named tag would otherwise yield the
|
|
143
|
+
* OID of an object `git worktree add <path> <branch>` will not check out.
|
|
144
|
+
*/
|
|
145
|
+
export function localBranchRef(branch: string): string {
|
|
146
|
+
return `${LOCAL_BRANCH_PREFIX}${branch}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
140
149
|
export function worktreeForBranch(records: readonly WorktreeRecord[], branch: string): WorktreeRecord | undefined {
|
|
141
|
-
const branchRef =
|
|
150
|
+
const branchRef = localBranchRef(branch);
|
|
142
151
|
return records.find((record) => record.branchRef === branchRef);
|
|
143
152
|
}
|
|
144
153
|
|
|
@@ -263,12 +272,7 @@ export async function localBranchExists(
|
|
|
263
272
|
branch: string,
|
|
264
273
|
signal?: AbortSignal,
|
|
265
274
|
): Promise<boolean> {
|
|
266
|
-
const result = await runGitAllowFailure(
|
|
267
|
-
pi,
|
|
268
|
-
["show-ref", "--verify", "--quiet", `${LOCAL_BRANCH_PREFIX}${branch}`],
|
|
269
|
-
cwd,
|
|
270
|
-
signal,
|
|
271
|
-
);
|
|
275
|
+
const result = await runGitAllowFailure(pi, ["show-ref", "--verify", "--quiet", localBranchRef(branch)], cwd, signal);
|
|
272
276
|
if (result.killed) throw killedError(["show-ref", "--verify", "--quiet"]);
|
|
273
277
|
if (result.code === 0) return true;
|
|
274
278
|
if (result.code === 1) return false;
|
|
@@ -627,7 +631,9 @@ export async function administrativePruneCandidates(
|
|
|
627
631
|
candidates.push(candidate);
|
|
628
632
|
administrativeIdentities.set(candidate, metadataSnapshot(candidate.administrativePath));
|
|
629
633
|
};
|
|
630
|
-
for (const entry of readdirSync(administrativeRoot, {
|
|
634
|
+
for (const entry of readdirSync(administrativeRoot, {
|
|
635
|
+
withFileTypes: true,
|
|
636
|
+
})) {
|
|
631
637
|
const administrativePath = resolve(administrativeRoot, entry.name);
|
|
632
638
|
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
633
639
|
throw new GitWorktreeError(`Unexpected Git worktree administrative entry: ${administrativePath}.`);
|
|
@@ -948,15 +954,55 @@ export function formatWorktree(record: WorktreeRecord, currentPath?: string): st
|
|
|
948
954
|
return stripTerminalControls(`${record.path} [${labels.join(", ") || "unknown"}] ${head}`);
|
|
949
955
|
}
|
|
950
956
|
|
|
957
|
+
/**
|
|
958
|
+
* Layout-preserving sanitizer contract (the same shape pi-statusline implements, duplicated per the
|
|
959
|
+
* package boundary rule; pi-recall implements the prose variant that also collapses whitespace):
|
|
960
|
+
* - line separators become one space, so multi-line Git output stays readable as a single line
|
|
961
|
+
* instead of welding the end of one record onto the start of the next;
|
|
962
|
+
* - every other unsafe code point (C0, DEL/C1, bidi overrides) is dropped with no replacement;
|
|
963
|
+
* - whitespace is neither collapsed nor trimmed.
|
|
964
|
+
* There is no escape-sequence parser here: dropping ESC and the C1 introducers already leaves any
|
|
965
|
+
* residual payload as inert text, and consuming it would let one byte erase the rest of a
|
|
966
|
+
* destructive confirmation prompt.
|
|
967
|
+
*/
|
|
951
968
|
export function stripTerminalControls(value: string): string {
|
|
952
969
|
return [...value]
|
|
953
|
-
.
|
|
954
|
-
const
|
|
955
|
-
|
|
970
|
+
.map((character) => {
|
|
971
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
972
|
+
if (isLineSeparator(codePoint)) return " ";
|
|
973
|
+
return isUnsafeTerminalCodePoint(codePoint) ? "" : character;
|
|
956
974
|
})
|
|
957
975
|
.join("");
|
|
958
976
|
}
|
|
959
977
|
|
|
978
|
+
// Git output is joined with newlines before it reaches a confirmation body. Dropping them outright
|
|
979
|
+
// glued "Removing a: reason" onto "Removing b: reason", changing what the user reads before
|
|
980
|
+
// approving an irreversible prune or removal.
|
|
981
|
+
function isLineSeparator(codePoint: number): boolean {
|
|
982
|
+
return (
|
|
983
|
+
codePoint === 0x09 ||
|
|
984
|
+
codePoint === 0x0a ||
|
|
985
|
+
codePoint === 0x0d ||
|
|
986
|
+
codePoint === 0x85 ||
|
|
987
|
+
codePoint === 0x2028 ||
|
|
988
|
+
codePoint === 0x2029
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Bidi controls reorder what the menu shows without changing the path we act on, so a removal
|
|
993
|
+
// prompt could name one worktree while the user reads another.
|
|
994
|
+
function isUnsafeTerminalCodePoint(codePoint: number): boolean {
|
|
995
|
+
return (
|
|
996
|
+
codePoint <= 0x1f ||
|
|
997
|
+
(codePoint >= 0x7f && codePoint <= 0x9f) ||
|
|
998
|
+
codePoint === 0x061c ||
|
|
999
|
+
codePoint === 0x200e ||
|
|
1000
|
+
codePoint === 0x200f ||
|
|
1001
|
+
(codePoint >= 0x202a && codePoint <= 0x202e) ||
|
|
1002
|
+
(codePoint >= 0x2066 && codePoint <= 0x2069)
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
960
1006
|
async function runGit(
|
|
961
1007
|
pi: Pick<ExtensionAPI, "exec">,
|
|
962
1008
|
args: string[],
|
|
@@ -1089,7 +1135,11 @@ function runGitWithInputAllowFailure(
|
|
|
1089
1135
|
}
|
|
1090
1136
|
return new Promise((resolveResult, reject) => {
|
|
1091
1137
|
// ExtensionAPI.exec has no stdin channel, so this read-only check uses an argv-only child.
|
|
1092
|
-
const child = spawn("git", args, {
|
|
1138
|
+
const child = spawn("git", args, {
|
|
1139
|
+
cwd,
|
|
1140
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1141
|
+
windowsHide: true,
|
|
1142
|
+
});
|
|
1093
1143
|
let stdout = "";
|
|
1094
1144
|
let stderr = "";
|
|
1095
1145
|
let killed = false;
|
|
@@ -1132,7 +1182,12 @@ function runGitWithInputAllowFailure(
|
|
|
1132
1182
|
});
|
|
1133
1183
|
child.once("error", fail);
|
|
1134
1184
|
child.once("close", (code, closeSignal) => {
|
|
1135
|
-
finish({
|
|
1185
|
+
finish({
|
|
1186
|
+
stdout,
|
|
1187
|
+
stderr,
|
|
1188
|
+
code: code ?? 1,
|
|
1189
|
+
killed: killed || closeSignal !== null,
|
|
1190
|
+
});
|
|
1136
1191
|
});
|
|
1137
1192
|
signal?.addEventListener("abort", stop, { once: true });
|
|
1138
1193
|
if (signal?.aborted) stop();
|