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/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 the main checkout. A remote display name relies
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 repoRoot - main worktree directory.
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, repoRoot: string, branch: string): Promise<string>;
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 the main checkout. A remote display name relies
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 repoRoot - main worktree directory.
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, repoRoot, branch) {
386
- const resolved = await resolveBranch(exec, repoRoot, branch);
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, repoRoot, ['switch', resolved.local]);
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, repoRoot, ['worktree', 'prune']);
399
- await git(exec, repoRoot, ['switch', resolved.local]);
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
  });
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Pure branch/path name normalization for worktree directory placement.
3
- * Windows-forbidden characters are the governing constraint: a worktree
4
- * folder name must survive on every platform the host runs on.
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 branch/path name normalization for worktree directory placement.
3
- * Windows-forbidden characters are the governing constraint: a worktree
4
- * folder name must survive on every platform the host runs on.
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 鈥?in-place branch switch of the main checkout.
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 鈥?in-place branch switch of the main checkout.
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.repoRoot, branch);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-git-branch-manage",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "提供 git 分支与 worktree 管理:输入框工具栏显示当前分支、可原地切换;支持 IDEA 风格分支选择器、远程分支检出、右键操作菜单(新建/重命名/删除/复制)、推送与更新当前分支,以及把分支隔离到独立 worktree 工作区,并管理其创建与删除。",
5
5
  "keywords": [
6
6
  "dsh",