dsh-git-branch-manage 1.0.3 → 1.0.5
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/lib/client.js +810 -187
- package/lib/git.d.ts +30 -6
- package/lib/git.js +41 -13
- package/lib/index.d.ts +16 -0
- package/lib/index.js +28 -1
- package/lib/normalize.d.ts +28 -3
- package/lib/normalize.js +38 -3
- package/lib/routes.d.ts +10 -1
- package/lib/routes.js +23 -6
- package/package.json +1 -1
package/lib/git.d.ts
CHANGED
|
@@ -148,15 +148,17 @@ export declare function cutoutBranchName(exec: Exec, repoRoot: string, base: str
|
|
|
148
148
|
*/
|
|
149
149
|
export declare function addWorktreeCutout(exec: Exec, repoRoot: string, base: string, newBranch: string, targetPath: string): Promise<void>;
|
|
150
150
|
/**
|
|
151
|
-
* In-place branch switch of
|
|
151
|
+
* In-place branch switch of one checkout. A remote display name relies
|
|
152
152
|
* on git's dwim: `git switch <local>` creates the tracking branch when
|
|
153
153
|
* exactly one remote has it.
|
|
154
154
|
* @param exec - executor seam.
|
|
155
|
-
* @param
|
|
155
|
+
* @param checkoutDir - the directory whose OWN checkout moves: the queried
|
|
156
|
+
* session's toplevel, so a linked worktree switches itself rather than the
|
|
157
|
+
* main checkout. `worktree prune` still runs here (it is repo-wide).
|
|
156
158
|
* @param branch - branch display name.
|
|
157
159
|
* @returns the local branch name now checked out.
|
|
158
160
|
*/
|
|
159
|
-
export declare function switchBranch(exec: Exec,
|
|
161
|
+
export declare function switchBranch(exec: Exec, checkoutDir: string, branch: string): Promise<string>;
|
|
160
162
|
/**
|
|
161
163
|
* Create a NEW branch. Three shapes:
|
|
162
164
|
*
|
|
@@ -205,6 +207,20 @@ export declare function renameBranch(exec: Exec, repoRoot: string, name: string,
|
|
|
205
207
|
* @returns the name of the branch that was deleted.
|
|
206
208
|
*/
|
|
207
209
|
export declare function deleteBranch(exec: Exec, repoRoot: string, name: string): Promise<string>;
|
|
210
|
+
/**
|
|
211
|
+
* argv prefix for network READ commands when cn-accelerate is on: the
|
|
212
|
+
* per-command `url.<base>.insteadOf=https://github.com/` rewrite. Per-command
|
|
213
|
+
* `git -c` (before the subcommand) rather than the user's global git config —
|
|
214
|
+
* the plugin must never persist settings on the user's behalf, and one
|
|
215
|
+
* injected value cannot leak into unrelated git runs. Rewrites
|
|
216
|
+
* `https://github.com/o/r.git` → `<base>https://github.com/o/r.git`, which is
|
|
217
|
+
* how public mirror prefixes consume GitHub URLs.
|
|
218
|
+
* @param prefix - the accelerate URL (raw or normalized); undefined = off.
|
|
219
|
+
* @returns the `-c` pair to prepend, or `[]` when off or the value is
|
|
220
|
+
* unusable (an invalid value fails SAFE to a direct connection — config
|
|
221
|
+
* validation rejects such values at load; this is the defensive path).
|
|
222
|
+
*/
|
|
223
|
+
export declare function accelArgs(prefix: string | undefined): readonly string[];
|
|
208
224
|
/**
|
|
209
225
|
* Sync remote-tracking refs: fetch every remote and prune tracking branches
|
|
210
226
|
* the remotes no longer carry (the CLI equivalent of IDEA's "synchronize
|
|
@@ -213,8 +229,10 @@ export declare function deleteBranch(exec: Exec, repoRoot: string, name: string)
|
|
|
213
229
|
* executor's hard timeout as a GitError.
|
|
214
230
|
* @param exec - executor seam.
|
|
215
231
|
* @param repoRoot - main worktree directory (fetch is repository-wide).
|
|
232
|
+
* @param accelPrefix - optional cn-accelerate mirror prefix (see
|
|
233
|
+
* {@link accelArgs}); only the READ half of the network commands gets it.
|
|
216
234
|
*/
|
|
217
|
-
export declare function fetchAll(exec: Exec, repoRoot: string): Promise<void>;
|
|
235
|
+
export declare function fetchAll(exec: Exec, repoRoot: string, accelPrefix?: string): Promise<void>;
|
|
218
236
|
/** Outcome of {@link updateBranch}: the branch touched and whether it moved. */
|
|
219
237
|
export interface UpdateOutcome {
|
|
220
238
|
branch: string;
|
|
@@ -231,10 +249,12 @@ export interface UpdateOutcome {
|
|
|
231
249
|
* branch ITS worktree holds.
|
|
232
250
|
* @param exec - executor seam.
|
|
233
251
|
* @param cwd - directory whose checked-out branch is updated.
|
|
252
|
+
* @param accelPrefix - optional cn-accelerate mirror prefix; feeds ONLY the
|
|
253
|
+
* fetch phase ({@link fetchAll}) — the `--ff-only` merge is local.
|
|
234
254
|
* @returns the branch name and whether HEAD actually moved (false = the
|
|
235
255
|
* upstream already contained it).
|
|
236
256
|
*/
|
|
237
|
-
export declare function updateBranch(exec: Exec, cwd: string): Promise<UpdateOutcome>;
|
|
257
|
+
export declare function updateBranch(exec: Exec, cwd: string, accelPrefix?: string): Promise<UpdateOutcome>;
|
|
238
258
|
/** Outcome of {@link pushBranch}: the branch touched and whether it moved. */
|
|
239
259
|
export interface PushOutcome {
|
|
240
260
|
branch: string;
|
|
@@ -247,7 +267,11 @@ export interface PushOutcome {
|
|
|
247
267
|
* git's own message rather than silently picking one), and `--force` is
|
|
248
268
|
* never passed — a diverged remote refuses rather than losing commits.
|
|
249
269
|
* Runs at the queried directory's own toplevel, so a linked-worktree
|
|
250
|
-
* session pushes the branch ITS worktree holds.
|
|
270
|
+
* session pushes the branch ITS worktree holds. The cn-accelerate rewrite is
|
|
271
|
+
* deliberately NEVER injected here (no {@link accelArgs} parameter exists):
|
|
272
|
+
* public mirrors have no `receive-pack`, and rewriting would turn a
|
|
273
|
+
* diagnosable direct failure into an opaque mirror failure while leaking the
|
|
274
|
+
* pushed content to a third party.
|
|
251
275
|
* @param exec - executor seam.
|
|
252
276
|
* @param cwd - directory whose checked-out branch is pushed.
|
|
253
277
|
* @returns the branch and whether the remote-tracking ref changed. A branch
|
package/lib/git.js
CHANGED
|
@@ -7,7 +7,7 @@ import { execFile } from 'node:child_process';
|
|
|
7
7
|
import { existsSync, readdirSync } from 'node:fs';
|
|
8
8
|
import { appendFile, mkdir, readFile, rm } from 'node:fs/promises';
|
|
9
9
|
import { basename, dirname, isAbsolute, join, normalize, resolve } from 'node:path';
|
|
10
|
-
import { localBranchName } from './normalize.js';
|
|
10
|
+
import { accelerateUrlError, localBranchName, normalizeAccelerateUrl } from './normalize.js';
|
|
11
11
|
/** Failure of one git invocation: non-zero exit with the stderr text. */
|
|
12
12
|
export class GitError extends Error {
|
|
13
13
|
args;
|
|
@@ -374,20 +374,22 @@ export async function addWorktreeCutout(exec, repoRoot, base, newBranch, targetP
|
|
|
374
374
|
await git(exec, repoRoot, ['worktree', 'add', targetPath, '-b', newBranch, base]);
|
|
375
375
|
}
|
|
376
376
|
/**
|
|
377
|
-
* In-place branch switch of
|
|
377
|
+
* In-place branch switch of one checkout. A remote display name relies
|
|
378
378
|
* on git's dwim: `git switch <local>` creates the tracking branch when
|
|
379
379
|
* exactly one remote has it.
|
|
380
380
|
* @param exec - executor seam.
|
|
381
|
-
* @param
|
|
381
|
+
* @param checkoutDir - the directory whose OWN checkout moves: the queried
|
|
382
|
+
* session's toplevel, so a linked worktree switches itself rather than the
|
|
383
|
+
* main checkout. `worktree prune` still runs here (it is repo-wide).
|
|
382
384
|
* @param branch - branch display name.
|
|
383
385
|
* @returns the local branch name now checked out.
|
|
384
386
|
*/
|
|
385
|
-
export async function switchBranch(exec,
|
|
386
|
-
const resolved = await resolveBranch(exec,
|
|
387
|
+
export async function switchBranch(exec, checkoutDir, branch) {
|
|
388
|
+
const resolved = await resolveBranch(exec, checkoutDir, branch);
|
|
387
389
|
if (resolved === undefined)
|
|
388
390
|
throw new GitError(['switch'], 1, `branch "${branch}" not found`);
|
|
389
391
|
try {
|
|
390
|
-
await git(exec,
|
|
392
|
+
await git(exec, checkoutDir, ['switch', resolved.local]);
|
|
391
393
|
}
|
|
392
394
|
catch (error) {
|
|
393
395
|
// A stale worktree registration claims the branch; prune (no-op on a
|
|
@@ -395,8 +397,8 @@ export async function switchBranch(exec, repoRoot, branch) {
|
|
|
395
397
|
const stale = error instanceof GitError && error.stderr.includes('already used by worktree');
|
|
396
398
|
if (!stale)
|
|
397
399
|
throw error;
|
|
398
|
-
await git(exec,
|
|
399
|
-
await git(exec,
|
|
400
|
+
await git(exec, checkoutDir, ['worktree', 'prune']);
|
|
401
|
+
await git(exec, checkoutDir, ['switch', resolved.local]);
|
|
400
402
|
}
|
|
401
403
|
return resolved.local;
|
|
402
404
|
}
|
|
@@ -462,6 +464,24 @@ export async function deleteBranch(exec, repoRoot, name) {
|
|
|
462
464
|
await git(exec, repoRoot, ['branch', '-d', name]);
|
|
463
465
|
return name;
|
|
464
466
|
}
|
|
467
|
+
/**
|
|
468
|
+
* argv prefix for network READ commands when cn-accelerate is on: the
|
|
469
|
+
* per-command `url.<base>.insteadOf=https://github.com/` rewrite. Per-command
|
|
470
|
+
* `git -c` (before the subcommand) rather than the user's global git config —
|
|
471
|
+
* the plugin must never persist settings on the user's behalf, and one
|
|
472
|
+
* injected value cannot leak into unrelated git runs. Rewrites
|
|
473
|
+
* `https://github.com/o/r.git` → `<base>https://github.com/o/r.git`, which is
|
|
474
|
+
* how public mirror prefixes consume GitHub URLs.
|
|
475
|
+
* @param prefix - the accelerate URL (raw or normalized); undefined = off.
|
|
476
|
+
* @returns the `-c` pair to prepend, or `[]` when off or the value is
|
|
477
|
+
* unusable (an invalid value fails SAFE to a direct connection — config
|
|
478
|
+
* validation rejects such values at load; this is the defensive path).
|
|
479
|
+
*/
|
|
480
|
+
export function accelArgs(prefix) {
|
|
481
|
+
if (prefix === undefined || accelerateUrlError(prefix) !== undefined)
|
|
482
|
+
return [];
|
|
483
|
+
return ['-c', `url.${normalizeAccelerateUrl(prefix)}.insteadOf=https://github.com/`];
|
|
484
|
+
}
|
|
465
485
|
/**
|
|
466
486
|
* Sync remote-tracking refs: fetch every remote and prune tracking branches
|
|
467
487
|
* the remotes no longer carry (the CLI equivalent of IDEA's "synchronize
|
|
@@ -470,9 +490,11 @@ export async function deleteBranch(exec, repoRoot, name) {
|
|
|
470
490
|
* executor's hard timeout as a GitError.
|
|
471
491
|
* @param exec - executor seam.
|
|
472
492
|
* @param repoRoot - main worktree directory (fetch is repository-wide).
|
|
493
|
+
* @param accelPrefix - optional cn-accelerate mirror prefix (see
|
|
494
|
+
* {@link accelArgs}); only the READ half of the network commands gets it.
|
|
473
495
|
*/
|
|
474
|
-
export async function fetchAll(exec, repoRoot) {
|
|
475
|
-
await git(exec, repoRoot, ['fetch', '--all', '--prune'], NETWORK_TIMEOUT_MS);
|
|
496
|
+
export async function fetchAll(exec, repoRoot, accelPrefix) {
|
|
497
|
+
await git(exec, repoRoot, [...accelArgs(accelPrefix), 'fetch', '--all', '--prune'], NETWORK_TIMEOUT_MS);
|
|
476
498
|
}
|
|
477
499
|
/**
|
|
478
500
|
* Update the CURRENT checkout: fetch every remote, then fast-forward the
|
|
@@ -485,11 +507,13 @@ export async function fetchAll(exec, repoRoot) {
|
|
|
485
507
|
* branch ITS worktree holds.
|
|
486
508
|
* @param exec - executor seam.
|
|
487
509
|
* @param cwd - directory whose checked-out branch is updated.
|
|
510
|
+
* @param accelPrefix - optional cn-accelerate mirror prefix; feeds ONLY the
|
|
511
|
+
* fetch phase ({@link fetchAll}) — the `--ff-only` merge is local.
|
|
488
512
|
* @returns the branch name and whether HEAD actually moved (false = the
|
|
489
513
|
* upstream already contained it).
|
|
490
514
|
*/
|
|
491
|
-
export async function updateBranch(exec, cwd) {
|
|
492
|
-
await fetchAll(exec, cwd);
|
|
515
|
+
export async function updateBranch(exec, cwd, accelPrefix) {
|
|
516
|
+
await fetchAll(exec, cwd, accelPrefix);
|
|
493
517
|
const before = (await git(exec, cwd, ['rev-parse', 'HEAD'])).trim();
|
|
494
518
|
await git(exec, cwd, ['merge', '--ff-only', '@{u}']);
|
|
495
519
|
const after = (await git(exec, cwd, ['rev-parse', 'HEAD'])).trim();
|
|
@@ -503,7 +527,11 @@ export async function updateBranch(exec, cwd) {
|
|
|
503
527
|
* git's own message rather than silently picking one), and `--force` is
|
|
504
528
|
* never passed — a diverged remote refuses rather than losing commits.
|
|
505
529
|
* Runs at the queried directory's own toplevel, so a linked-worktree
|
|
506
|
-
* session pushes the branch ITS worktree holds.
|
|
530
|
+
* session pushes the branch ITS worktree holds. The cn-accelerate rewrite is
|
|
531
|
+
* deliberately NEVER injected here (no {@link accelArgs} parameter exists):
|
|
532
|
+
* public mirrors have no `receive-pack`, and rewriting would turn a
|
|
533
|
+
* diagnosable direct failure into an opaque mirror failure while leaking the
|
|
534
|
+
* pushed content to a third party.
|
|
507
535
|
* @param exec - executor seam.
|
|
508
536
|
* @param cwd - directory whose checked-out branch is pushed.
|
|
509
537
|
* @returns the branch and whether the remote-tracking ref changed. A branch
|
package/lib/index.d.ts
CHANGED
|
@@ -25,6 +25,11 @@ export interface Config {
|
|
|
25
25
|
/** Post-create configuration files to copy when .worktreeinclude is absent;
|
|
26
26
|
* absent = none. */
|
|
27
27
|
postCreateCopyFiles?: Volatile<string[]> | string[];
|
|
28
|
+
/** cn-accelerate mirror rewrite for network READ commands (fetch/update);
|
|
29
|
+
* absent = off. PUSH is never rewritten. */
|
|
30
|
+
cnAccelerate?: Volatile<boolean> | boolean;
|
|
31
|
+
/** cn-accelerate mirror prefix; absent = the shipped default. */
|
|
32
|
+
cnAccelerateUrl?: Volatile<string> | string;
|
|
28
33
|
}
|
|
29
34
|
/** The shipped prune cap (whole storage root, valid git worktrees only). */
|
|
30
35
|
export declare const KEEP_WORKTREES_DEFAULT = 30;
|
|
@@ -36,6 +41,13 @@ export declare const KEEP_WORKTREES_DEFAULT = 30;
|
|
|
36
41
|
export declare const Config: z;
|
|
37
42
|
/** Reject stale or misspelled config keys before defaults can hide them. */
|
|
38
43
|
export declare function validateConfig(config: Config): void;
|
|
44
|
+
/**
|
|
45
|
+
* The accelerate prefix must be an http(s) URL without whitespace — it is
|
|
46
|
+
* concatenated with `https://github.com/` into one `url.<base>.insteadOf`
|
|
47
|
+
* config value, so a space would smuggle arguments and a non-http scheme is
|
|
48
|
+
* not a fetchable remote. Absent = the shipped default, which passes.
|
|
49
|
+
*/
|
|
50
|
+
export declare function validateCnAccelerateUrl(value: string | undefined): void;
|
|
39
51
|
/**
|
|
40
52
|
* The prune cap must be an integer >= 1: 0 would read as "create then
|
|
41
53
|
* immediately self-delete" (the fresh worktree is excluded, but every other
|
|
@@ -60,6 +72,10 @@ export interface SectionConfig {
|
|
|
60
72
|
keepWorktrees?: number;
|
|
61
73
|
/** Post-create configuration files to copy when .worktreeinclude is absent. */
|
|
62
74
|
postCreateCopyFiles?: string[];
|
|
75
|
+
/** cn-accelerate mirror rewrite for network READ commands; absent = off. */
|
|
76
|
+
cnAccelerate?: boolean;
|
|
77
|
+
/** cn-accelerate mirror prefix; absent = the shipped default. */
|
|
78
|
+
cnAccelerateUrl?: string;
|
|
63
79
|
}
|
|
64
80
|
/** The section-shaped view of a config: absent keys stay absent
|
|
65
81
|
* (`exactOptionalPropertyTypes`) except the switches, which spell their
|
package/lib/index.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import z from '@deepseek-ai/schemastery';
|
|
10
10
|
import { childProcessExec } from './git.js';
|
|
11
|
+
import { ACCEL_DEFAULT_URL, accelerateUrlError } from './normalize.js';
|
|
11
12
|
import { handleAiCommitMessage, handleChanges, handleCommit, handleCommitFiles, handleCreateBranch, handleCreateWorktree, handleDeleteBranch, handleEnsureDirectory, handleFetch, handleGroupWorktrees, handleInspectWorktree, handleLog, handlePathExists, handlePurgeDirectory, handleRemoveWorktree, handleRenameBranch, handleStage, handleStatus, handleSwitch, handleUpdate, handlePush, handleWorktreesAll, } from './routes.js';
|
|
12
13
|
import { validateRootDir } from './settings.js';
|
|
13
14
|
import { ROUTE_AI_COMMIT_MESSAGE, ROUTE_BRANCH, ROUTE_BRANCH_DELETE, ROUTE_BRANCH_RENAME, ROUTE_CHANGES, ROUTE_COMMIT, ROUTE_COMMIT_FILES, ROUTE_ENSURE_DIRECTORY, ROUTE_EXISTS, ROUTE_FETCH, ROUTE_GROUP, ROUTE_INSPECT, ROUTE_LOG, ROUTE_PUSH, ROUTE_PURGE, ROUTE_REMOVE, ROUTE_STAGE, ROUTE_STATUS, ROUTE_SWITCH, ROUTE_UPDATE, ROUTE_WORKTREE, ROUTE_WORKTREES_ALL } from './wire.js';
|
|
@@ -37,10 +38,12 @@ export const Config = z.object({
|
|
|
37
38
|
autoPruneWorktrees: z.boolean().default(false).volatile(),
|
|
38
39
|
keepWorktrees: z.number().default(KEEP_WORKTREES_DEFAULT).volatile(),
|
|
39
40
|
postCreateCopyFiles: z.array(z.string()).default([]).volatile(),
|
|
41
|
+
cnAccelerate: z.boolean().default(false).volatile(),
|
|
42
|
+
cnAccelerateUrl: z.string().default(ACCEL_DEFAULT_URL).volatile(),
|
|
40
43
|
});
|
|
41
44
|
/** Reject stale or misspelled config keys before defaults can hide them. */
|
|
42
45
|
export function validateConfig(config) {
|
|
43
|
-
const unknown = Object.keys(config).find(key => key !== 'rootDir' && key !== 'groupSidebar' && key !== 'fetchBeforeCreate' && key !== 'autoPruneWorktrees' && key !== 'keepWorktrees' && key !== 'postCreateCopyFiles');
|
|
46
|
+
const unknown = Object.keys(config).find(key => key !== 'rootDir' && key !== 'groupSidebar' && key !== 'fetchBeforeCreate' && key !== 'autoPruneWorktrees' && key !== 'keepWorktrees' && key !== 'postCreateCopyFiles' && key !== 'cnAccelerate' && key !== 'cnAccelerateUrl');
|
|
44
47
|
if (unknown !== undefined) {
|
|
45
48
|
throw new Error(`GitWorktreeConfig: unknown key "${unknown}"`);
|
|
46
49
|
}
|
|
@@ -66,9 +69,27 @@ export function validateConfig(config) {
|
|
|
66
69
|
throw new Error('GitWorktreeConfig: "postCreateCopyFiles" must be an array of strings');
|
|
67
70
|
}
|
|
68
71
|
}
|
|
72
|
+
const cnAccelerate = readVolatile(config.cnAccelerate);
|
|
73
|
+
if (cnAccelerate !== undefined && typeof cnAccelerate !== 'boolean') {
|
|
74
|
+
throw new Error('GitWorktreeConfig: "cnAccelerate" must be a boolean');
|
|
75
|
+
}
|
|
76
|
+
validateCnAccelerateUrl(readVolatile(config.cnAccelerateUrl));
|
|
69
77
|
validateKeepWorktrees(readVolatile(config.keepWorktrees));
|
|
70
78
|
validateRootDir(root);
|
|
71
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* The accelerate prefix must be an http(s) URL without whitespace — it is
|
|
82
|
+
* concatenated with `https://github.com/` into one `url.<base>.insteadOf`
|
|
83
|
+
* config value, so a space would smuggle arguments and a non-http scheme is
|
|
84
|
+
* not a fetchable remote. Absent = the shipped default, which passes.
|
|
85
|
+
*/
|
|
86
|
+
export function validateCnAccelerateUrl(value) {
|
|
87
|
+
if (value === undefined)
|
|
88
|
+
return;
|
|
89
|
+
if (typeof value !== 'string' || accelerateUrlError(value) !== undefined) {
|
|
90
|
+
throw new Error('GitWorktreeConfig: "cnAccelerateUrl" must be an http(s) URL without whitespace');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
72
93
|
/**
|
|
73
94
|
* The prune cap must be an integer >= 1: 0 would read as "create then
|
|
74
95
|
* immediately self-delete" (the fresh worktree is excluded, but every other
|
|
@@ -94,14 +115,18 @@ export function sectionOf(config) {
|
|
|
94
115
|
const fetchBeforeCreate = readVolatile(config.fetchBeforeCreate);
|
|
95
116
|
const autoPruneWorktrees = readVolatile(config.autoPruneWorktrees);
|
|
96
117
|
const keepWorktrees = readVolatile(config.keepWorktrees);
|
|
118
|
+
const cnAccelerate = readVolatile(config.cnAccelerate);
|
|
119
|
+
const cnAccelerateUrl = readVolatile(config.cnAccelerateUrl);
|
|
97
120
|
return {
|
|
98
121
|
...(rootDir === undefined ? {} : { rootDir }),
|
|
99
122
|
...(postCreateCopyFiles === undefined ? {} : { postCreateCopyFiles }),
|
|
123
|
+
...(cnAccelerateUrl === undefined ? {} : { cnAccelerateUrl }),
|
|
100
124
|
// The composition layer spells the shipped defaults so a user-layer
|
|
101
125
|
// unset can always fall back to them.
|
|
102
126
|
groupSidebar: groupSidebar ?? true,
|
|
103
127
|
fetchBeforeCreate: fetchBeforeCreate ?? false,
|
|
104
128
|
autoPruneWorktrees: autoPruneWorktrees ?? false,
|
|
129
|
+
cnAccelerate: cnAccelerate ?? false,
|
|
105
130
|
keepWorktrees: keepWorktrees ?? KEEP_WORKTREES_DEFAULT,
|
|
106
131
|
};
|
|
107
132
|
}
|
|
@@ -119,6 +144,8 @@ export function apply(ctx, config = {}) {
|
|
|
119
144
|
sectionRootDir: () => readVolatile(currentConfig().rootDir),
|
|
120
145
|
sectionFetchBeforeCreate: () => readVolatile(currentConfig().fetchBeforeCreate),
|
|
121
146
|
sectionPostCreateCopyFiles: () => readVolatile(currentConfig().postCreateCopyFiles),
|
|
147
|
+
sectionCnAccelerate: () => readVolatile(currentConfig().cnAccelerate),
|
|
148
|
+
sectionCnAccelerateUrl: () => readVolatile(currentConfig().cnAccelerateUrl),
|
|
122
149
|
home: () => homedir(),
|
|
123
150
|
envHome: () => process.env.DSH_HOME,
|
|
124
151
|
});
|
package/lib/normalize.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Pure name/URL normalization shared by the host (git command assembly,
|
|
3
|
+
* config validation) and the client (forms). Windows-forbidden characters
|
|
4
|
+
* govern worktree directory placement: a worktree folder name must survive
|
|
5
|
+
* on every platform the host runs on.
|
|
5
6
|
*/
|
|
6
7
|
/**
|
|
7
8
|
* Turn a branch display name into a single safe directory segment.
|
|
@@ -56,3 +57,27 @@ export type BranchNameIssue = 'empty' | 'leadingDash' | 'illegal';
|
|
|
56
57
|
* @returns the issue kind, or null when the name is acceptable.
|
|
57
58
|
*/
|
|
58
59
|
export declare function branchNameIssue(name: string): BranchNameIssue | null;
|
|
60
|
+
/**
|
|
61
|
+
* Shipped default for the cn-accelerate mirror prefix (the design doc's
|
|
62
|
+
* `cnAccelerateUrl` default and the popover's「恢复默认」target).
|
|
63
|
+
*/
|
|
64
|
+
export declare const ACCEL_DEFAULT_URL = "https://gh-proxy.com/";
|
|
65
|
+
/** Why a user-typed accelerate URL is not acceptable yet (undefined = fine). */
|
|
66
|
+
export type AccelerateUrlIssue = 'empty' | 'invalid';
|
|
67
|
+
/**
|
|
68
|
+
* Pre-flight check of the cn-accelerate prefix against what the rewrite can
|
|
69
|
+
* actually use: non-empty after trim, `http://` or `https://` scheme, and no
|
|
70
|
+
* whitespace anywhere (a space would break the `url.<base>.insteadOf=<target>`
|
|
71
|
+
* git config value and smuggle a second argument).
|
|
72
|
+
* @param value - raw draft text.
|
|
73
|
+
* @returns the issue kind, or undefined when the value is acceptable.
|
|
74
|
+
*/
|
|
75
|
+
export declare function accelerateUrlError(value: string): AccelerateUrlIssue | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Normalize an accepted accelerate URL: trim, and append the trailing `/`
|
|
78
|
+
* the prefix concatenation requires (`https://gh-proxy.com/` + `o/r.git`
|
|
79
|
+
* must stay one valid URL). Callers pass the result to `url.<base>.insteadOf`.
|
|
80
|
+
* @param value - an accepted (non-empty, http(s), whitespace-free) URL.
|
|
81
|
+
* @returns the prefix ending in `/`.
|
|
82
|
+
*/
|
|
83
|
+
export declare function normalizeAccelerateUrl(value: string): string;
|
package/lib/normalize.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Pure name/URL normalization shared by the host (git command assembly,
|
|
3
|
+
* config validation) and the client (forms). Windows-forbidden characters
|
|
4
|
+
* govern worktree directory placement: a worktree folder name must survive
|
|
5
|
+
* on every platform the host runs on.
|
|
5
6
|
*/
|
|
6
7
|
/**
|
|
7
8
|
* Turn a branch display name into a single safe directory segment.
|
|
@@ -91,3 +92,37 @@ export function branchNameIssue(name) {
|
|
|
91
92
|
return 'illegal';
|
|
92
93
|
return null;
|
|
93
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Shipped default for the cn-accelerate mirror prefix (the design doc's
|
|
97
|
+
* `cnAccelerateUrl` default and the popover's「恢复默认」target).
|
|
98
|
+
*/
|
|
99
|
+
export const ACCEL_DEFAULT_URL = 'https://gh-proxy.com/';
|
|
100
|
+
/**
|
|
101
|
+
* Pre-flight check of the cn-accelerate prefix against what the rewrite can
|
|
102
|
+
* actually use: non-empty after trim, `http://` or `https://` scheme, and no
|
|
103
|
+
* whitespace anywhere (a space would break the `url.<base>.insteadOf=<target>`
|
|
104
|
+
* git config value and smuggle a second argument).
|
|
105
|
+
* @param value - raw draft text.
|
|
106
|
+
* @returns the issue kind, or undefined when the value is acceptable.
|
|
107
|
+
*/
|
|
108
|
+
export function accelerateUrlError(value) {
|
|
109
|
+
const trimmed = value.trim();
|
|
110
|
+
if (trimmed === '')
|
|
111
|
+
return 'empty';
|
|
112
|
+
if (/\s/.test(trimmed))
|
|
113
|
+
return 'invalid';
|
|
114
|
+
if (!trimmed.startsWith('http://') && !trimmed.startsWith('https://'))
|
|
115
|
+
return 'invalid';
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Normalize an accepted accelerate URL: trim, and append the trailing `/`
|
|
120
|
+
* the prefix concatenation requires (`https://gh-proxy.com/` + `o/r.git`
|
|
121
|
+
* must stay one valid URL). Callers pass the result to `url.<base>.insteadOf`.
|
|
122
|
+
* @param value - an accepted (non-empty, http(s), whitespace-free) URL.
|
|
123
|
+
* @returns the prefix ending in `/`.
|
|
124
|
+
*/
|
|
125
|
+
export function normalizeAccelerateUrl(value) {
|
|
126
|
+
const trimmed = value.trim();
|
|
127
|
+
return trimmed.endsWith('/') ? trimmed : `${trimmed}/`;
|
|
128
|
+
}
|
package/lib/routes.d.ts
CHANGED
|
@@ -14,6 +14,10 @@ export interface RouteDeps {
|
|
|
14
14
|
sectionFetchBeforeCreate: () => boolean | undefined;
|
|
15
15
|
/** Post-create configuration files to copy when .worktreeinclude is absent. */
|
|
16
16
|
sectionPostCreateCopyFiles?: () => string[] | undefined;
|
|
17
|
+
/** Whether the cn-accelerate mirror rewrite is on (absent = off). */
|
|
18
|
+
sectionCnAccelerate?: () => boolean | undefined;
|
|
19
|
+
/** The cn-accelerate mirror prefix (absent = the shipped default). */
|
|
20
|
+
sectionCnAccelerateUrl?: () => string | undefined;
|
|
17
21
|
/** User home directory seam. */
|
|
18
22
|
home: () => string;
|
|
19
23
|
/** `$DSH_HOME` environment value seam. */
|
|
@@ -120,7 +124,12 @@ export declare function handleStatus(deps: RouteDeps, path: string | undefined):
|
|
|
120
124
|
*/
|
|
121
125
|
export declare function handleCreateWorktree(deps: RouteDeps, body: unknown): Promise<RouteOutcome>;
|
|
122
126
|
/**
|
|
123
|
-
* POST /switch
|
|
127
|
+
* POST /switch — in-place branch switch of the QUERIED directory's own
|
|
128
|
+
* checkout. A main-checkout session switches the main checkout; a linked
|
|
129
|
+
* worktree session switches ITS worktree. (It used to always run in the
|
|
130
|
+
* main checkout: a worktree session's checkout appeared to succeed while its
|
|
131
|
+
* own branch never moved. git itself refuses a branch another worktree
|
|
132
|
+
* holds — an honest error beats moving a checkout nobody was looking at.)
|
|
124
133
|
* @param deps - host dependencies.
|
|
125
134
|
* @param body - parsed request body.
|
|
126
135
|
*/
|
package/lib/routes.js
CHANGED
|
@@ -6,7 +6,7 @@ import { basename, dirname, join, resolve } from 'node:path';
|
|
|
6
6
|
import { mkdir, readdir, rm, stat } from 'node:fs/promises';
|
|
7
7
|
import { runPostCreateCopy } from './copy-files.js';
|
|
8
8
|
import { appendWorktreeExclude, fsExcludeSeams, GitError, addWorktree, addWorktreeCutout, collectAiDiff, commitFiles, commitIndex, createBranch, cutoutBranchName, deleteBranch, fetchAll, fsDirExists, inspectWorktree, isAbsoluteDir, isStageablePath, logHistory, probeRepo, probeWorkspaceGit, removeWorktree, renameBranch, resolveBranch, pushBranch, stagePaths, switchBranch, updateBranch, workingChanges } from './git.js';
|
|
9
|
-
import { isAbsoluteConfigPath, sanitizeBranchDir } from './normalize.js';
|
|
9
|
+
import { ACCEL_DEFAULT_URL, accelerateUrlError, isAbsoluteConfigPath, normalizeAccelerateUrl, sanitizeBranchDir } from './normalize.js';
|
|
10
10
|
import { resolveRootDir } from './settings.js';
|
|
11
11
|
import { COMMIT_MESSAGE_LIMIT } from './wire.js';
|
|
12
12
|
/** Real fs-backed one-level directory listing (names only). Plain FILES are
|
|
@@ -39,6 +39,18 @@ export async function fsMkdirRecursive(path) {
|
|
|
39
39
|
function fail(status, error) {
|
|
40
40
|
return { status, body: { error } };
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The rewrite prefix for one request's network READ commands: the cn-accelerate
|
|
44
|
+
* mirror prefix when the switch is on and the URL setting is usable (the
|
|
45
|
+
* shipped default fills an absent URL), else undefined — no injection and the
|
|
46
|
+
* direct connection runs unchanged. PUSH never consults this helper.
|
|
47
|
+
*/
|
|
48
|
+
function accelPrefix(deps) {
|
|
49
|
+
if (deps.sectionCnAccelerate?.() !== true)
|
|
50
|
+
return undefined;
|
|
51
|
+
const url = deps.sectionCnAccelerateUrl?.() ?? ACCEL_DEFAULT_URL;
|
|
52
|
+
return accelerateUrlError(url) === undefined ? normalizeAccelerateUrl(url) : undefined;
|
|
53
|
+
}
|
|
42
54
|
/** Upper bound on distinct paths one /group request may probe. */
|
|
43
55
|
const GROUP_PATHS_LIMIT = 256;
|
|
44
56
|
/** Probes per in-flight batch — polite on Windows, where each git call is a
|
|
@@ -414,7 +426,7 @@ export async function handleCreateWorktree(deps, body) {
|
|
|
414
426
|
const resolved = await resolveBranch(deps.exec, facts.repoRoot, branch);
|
|
415
427
|
if (resolved?.remote !== undefined) {
|
|
416
428
|
try {
|
|
417
|
-
await fetchAll(deps.exec, facts.repoRoot);
|
|
429
|
+
await fetchAll(deps.exec, facts.repoRoot, accelPrefix(deps));
|
|
418
430
|
}
|
|
419
431
|
catch (error) {
|
|
420
432
|
fetchWarning = error instanceof GitError ? error.stderr.trim() : error instanceof Error ? error.message : String(error);
|
|
@@ -471,7 +483,12 @@ export async function handleCreateWorktree(deps, body) {
|
|
|
471
483
|
}
|
|
472
484
|
}
|
|
473
485
|
/**
|
|
474
|
-
* POST /switch
|
|
486
|
+
* POST /switch — in-place branch switch of the QUERIED directory's own
|
|
487
|
+
* checkout. A main-checkout session switches the main checkout; a linked
|
|
488
|
+
* worktree session switches ITS worktree. (It used to always run in the
|
|
489
|
+
* main checkout: a worktree session's checkout appeared to succeed while its
|
|
490
|
+
* own branch never moved. git itself refuses a branch another worktree
|
|
491
|
+
* holds — an honest error beats moving a checkout nobody was looking at.)
|
|
475
492
|
* @param deps - host dependencies.
|
|
476
493
|
* @param body - parsed request body.
|
|
477
494
|
*/
|
|
@@ -488,7 +505,7 @@ export async function handleSwitch(deps, body) {
|
|
|
488
505
|
const facts = await probeRepo(deps.exec, repoPath, deps.dirExists);
|
|
489
506
|
if (facts === undefined)
|
|
490
507
|
return fail(400, `"${repoPath}" is not inside a git repository`);
|
|
491
|
-
const switched = await switchBranch(deps.exec, facts.
|
|
508
|
+
const switched = await switchBranch(deps.exec, facts.toplevel, branch);
|
|
492
509
|
const result = { branch: switched };
|
|
493
510
|
return { status: 200, body: result };
|
|
494
511
|
}
|
|
@@ -644,7 +661,7 @@ export async function handleFetch(deps, body) {
|
|
|
644
661
|
const facts = await probeRepo(deps.exec, repoPath, deps.dirExists);
|
|
645
662
|
if (facts === undefined)
|
|
646
663
|
return fail(400, `"${repoPath}" is not inside a git repository`);
|
|
647
|
-
await fetchAll(deps.exec, facts.repoRoot);
|
|
664
|
+
await fetchAll(deps.exec, facts.repoRoot, accelPrefix(deps));
|
|
648
665
|
const result = { remote: 'all' };
|
|
649
666
|
return { status: 200, body: result };
|
|
650
667
|
}
|
|
@@ -677,7 +694,7 @@ export async function handleUpdate(deps, body) {
|
|
|
677
694
|
// session "update current branch" means the branch that worktree holds.
|
|
678
695
|
// `repoRoot` would fast-forward the main checkout behind the user's back
|
|
679
696
|
// (both are the same directory when the session IS the main checkout).
|
|
680
|
-
const outcome = await updateBranch(deps.exec, facts.toplevel);
|
|
697
|
+
const outcome = await updateBranch(deps.exec, facts.toplevel, accelPrefix(deps));
|
|
681
698
|
const result = { branch: outcome.branch, updated: outcome.updated };
|
|
682
699
|
return { status: 200, body: result };
|
|
683
700
|
}
|
package/package.json
CHANGED