dsh-git-branch-manage 1.0.4 → 1.0.6

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
@@ -182,6 +182,28 @@ export declare function switchBranch(exec: Exec, checkoutDir: string, branch: st
182
182
  * start point with `checkout` false).
183
183
  */
184
184
  export declare function createBranch(exec: Exec, cwd: string, name: string, from?: string, checkout?: boolean): Promise<string>;
185
+ /** Outcome of {@link createTag}: the tag created and the commit it points at. */
186
+ export interface TagOutcome {
187
+ tag: string;
188
+ hash: string;
189
+ }
190
+ /**
191
+ * Create a tag at an existing commit (`git tag`). Tags are repository-wide
192
+ * refs, so the command runs at the repository root.
193
+ *
194
+ * A message makes it an ANNOTATED tag (`-a -m`) — the only shape that carries
195
+ * an author and date, and what `git describe` prefers; without one the tag is
196
+ * lightweight and is just an alias for the commit. Both are created locally
197
+ * only: pushing a tag is a deliberate, separate publish step that this panel
198
+ * does not perform on a menu click.
199
+ * @param exec - executor seam.
200
+ * @param repoRoot - main worktree directory.
201
+ * @param name - tag name (git validates; failures raise GitError).
202
+ * @param target - commit to tag; absent tags the repository's current HEAD.
203
+ * @param message - optional annotation message; non-empty = annotated tag.
204
+ * @returns the tag name and the commit hash it points at.
205
+ */
206
+ export declare function createTag(exec: Exec, repoRoot: string, name: string, target?: string, message?: string): Promise<TagOutcome>;
185
207
  /**
186
208
  * Rename a LOCAL branch (`git branch -m`): branch refs are repository-wide,
187
209
  * so the command runs at the repository root and every worktree HEAD pointing
@@ -207,6 +229,20 @@ export declare function renameBranch(exec: Exec, repoRoot: string, name: string,
207
229
  * @returns the name of the branch that was deleted.
208
230
  */
209
231
  export declare function deleteBranch(exec: Exec, repoRoot: string, name: string): Promise<string>;
232
+ /**
233
+ * argv prefix for network READ commands when cn-accelerate is on: the
234
+ * per-command `url.<base>.insteadOf=https://github.com/` rewrite. Per-command
235
+ * `git -c` (before the subcommand) rather than the user's global git config —
236
+ * the plugin must never persist settings on the user's behalf, and one
237
+ * injected value cannot leak into unrelated git runs. Rewrites
238
+ * `https://github.com/o/r.git` → `<base>https://github.com/o/r.git`, which is
239
+ * how public mirror prefixes consume GitHub URLs.
240
+ * @param prefix - the accelerate URL (raw or normalized); undefined = off.
241
+ * @returns the `-c` pair to prepend, or `[]` when off or the value is
242
+ * unusable (an invalid value fails SAFE to a direct connection — config
243
+ * validation rejects such values at load; this is the defensive path).
244
+ */
245
+ export declare function accelArgs(prefix: string | undefined): readonly string[];
210
246
  /**
211
247
  * Sync remote-tracking refs: fetch every remote and prune tracking branches
212
248
  * the remotes no longer carry (the CLI equivalent of IDEA's "synchronize
@@ -215,8 +251,10 @@ export declare function deleteBranch(exec: Exec, repoRoot: string, name: string)
215
251
  * executor's hard timeout as a GitError.
216
252
  * @param exec - executor seam.
217
253
  * @param repoRoot - main worktree directory (fetch is repository-wide).
254
+ * @param accelPrefix - optional cn-accelerate mirror prefix (see
255
+ * {@link accelArgs}); only the READ half of the network commands gets it.
218
256
  */
219
- export declare function fetchAll(exec: Exec, repoRoot: string): Promise<void>;
257
+ export declare function fetchAll(exec: Exec, repoRoot: string, accelPrefix?: string): Promise<void>;
220
258
  /** Outcome of {@link updateBranch}: the branch touched and whether it moved. */
221
259
  export interface UpdateOutcome {
222
260
  branch: string;
@@ -233,10 +271,12 @@ export interface UpdateOutcome {
233
271
  * branch ITS worktree holds.
234
272
  * @param exec - executor seam.
235
273
  * @param cwd - directory whose checked-out branch is updated.
274
+ * @param accelPrefix - optional cn-accelerate mirror prefix; feeds ONLY the
275
+ * fetch phase ({@link fetchAll}) — the `--ff-only` merge is local.
236
276
  * @returns the branch name and whether HEAD actually moved (false = the
237
277
  * upstream already contained it).
238
278
  */
239
- export declare function updateBranch(exec: Exec, cwd: string): Promise<UpdateOutcome>;
279
+ export declare function updateBranch(exec: Exec, cwd: string, accelPrefix?: string): Promise<UpdateOutcome>;
240
280
  /** Outcome of {@link pushBranch}: the branch touched and whether it moved. */
241
281
  export interface PushOutcome {
242
282
  branch: string;
@@ -249,7 +289,11 @@ export interface PushOutcome {
249
289
  * git's own message rather than silently picking one), and `--force` is
250
290
  * never passed — a diverged remote refuses rather than losing commits.
251
291
  * Runs at the queried directory's own toplevel, so a linked-worktree
252
- * session pushes the branch ITS worktree holds.
292
+ * session pushes the branch ITS worktree holds. The cn-accelerate rewrite is
293
+ * deliberately NEVER injected here (no {@link accelArgs} parameter exists):
294
+ * public mirrors have no `receive-pack`, and rewriting would turn a
295
+ * diagnosable direct failure into an opaque mirror failure while leaking the
296
+ * pushed content to a third party.
253
297
  * @param exec - executor seam.
254
298
  * @param cwd - directory whose checked-out branch is pushed.
255
299
  * @returns the branch and whether the remote-tracking ref changed. A branch
@@ -425,18 +469,34 @@ export interface CommitEntryRaw {
425
469
  body: string;
426
470
  refs: string[];
427
471
  }
472
+ /** The extra log filters the history card's toolbar drives. */
473
+ export interface LogFilter {
474
+ /** Every ref (local + remote branches, tags) instead of the checkout's
475
+ * HEAD — the history card's default view. Ignored when `ref` is given. */
476
+ all?: boolean;
477
+ /** Committer-author substring/regex, forwarded as `--author=<value>`. */
478
+ author?: string;
479
+ /** Lower/upper bound on the commit date, epoch milliseconds. */
480
+ since?: number;
481
+ until?: number;
482
+ }
428
483
  /**
429
484
  * Paginated history of the queried checkout. `ref` is verified with
430
485
  * `rev-parse --verify` first, so an arbitrary string can never reach the log
431
486
  * arguments as anything but a resolvable rev. An unborn checkout (no commits
432
487
  * yet) answers an empty page instead of an error.
488
+ *
489
+ * `--topo-order` is unconditional: the history card draws lane lines from
490
+ * `parents`, and a pure date order would interleave a branch's commits
491
+ * before their merge base, leaving edges pointing at rows not yet painted.
433
492
  * @param exec - executor seam.
434
493
  * @param cwd - the checkout directory.
435
494
  * @param limit - page size (1..200, validated upstream).
436
495
  * @param skip - offset (>= 0, validated upstream).
437
496
  * @param ref - optional starting rev; absent = the checkout's HEAD.
497
+ * @param filter - optional all-refs/author/date window.
438
498
  */
439
- export declare function logHistory(exec: Exec, cwd: string, limit: number, skip: number, ref?: string): Promise<CommitEntryRaw[]>;
499
+ export declare function logHistory(exec: Exec, cwd: string, limit: number, skip: number, ref?: string, filter?: LogFilter): Promise<CommitEntryRaw[]>;
440
500
  /** One file row of {@link commitFiles}. */
441
501
  export interface CommitFileRow {
442
502
  path: string;
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;
@@ -433,6 +433,35 @@ export async function createBranch(exec, cwd, name, from, checkout = false) {
433
433
  await git(exec, cwd, ['branch', name, from]);
434
434
  return name;
435
435
  }
436
+ /**
437
+ * Create a tag at an existing commit (`git tag`). Tags are repository-wide
438
+ * refs, so the command runs at the repository root.
439
+ *
440
+ * A message makes it an ANNOTATED tag (`-a -m`) — the only shape that carries
441
+ * an author and date, and what `git describe` prefers; without one the tag is
442
+ * lightweight and is just an alias for the commit. Both are created locally
443
+ * only: pushing a tag is a deliberate, separate publish step that this panel
444
+ * does not perform on a menu click.
445
+ * @param exec - executor seam.
446
+ * @param repoRoot - main worktree directory.
447
+ * @param name - tag name (git validates; failures raise GitError).
448
+ * @param target - commit to tag; absent tags the repository's current HEAD.
449
+ * @param message - optional annotation message; non-empty = annotated tag.
450
+ * @returns the tag name and the commit hash it points at.
451
+ */
452
+ export async function createTag(exec, repoRoot, name, target, message) {
453
+ const text = message?.trim() ?? '';
454
+ const args = ['tag'];
455
+ if (text !== '')
456
+ args.push('-a', name, '-m', text);
457
+ else
458
+ args.push(name);
459
+ if (target !== undefined && target !== '')
460
+ args.push(target);
461
+ await git(exec, repoRoot, args);
462
+ const hash = (await git(exec, repoRoot, ['rev-list', '-n', '1', name])).trim();
463
+ return { tag: name, hash };
464
+ }
436
465
  /**
437
466
  * Rename a LOCAL branch (`git branch -m`): branch refs are repository-wide,
438
467
  * so the command runs at the repository root and every worktree HEAD pointing
@@ -464,6 +493,24 @@ export async function deleteBranch(exec, repoRoot, name) {
464
493
  await git(exec, repoRoot, ['branch', '-d', name]);
465
494
  return name;
466
495
  }
496
+ /**
497
+ * argv prefix for network READ commands when cn-accelerate is on: the
498
+ * per-command `url.<base>.insteadOf=https://github.com/` rewrite. Per-command
499
+ * `git -c` (before the subcommand) rather than the user's global git config —
500
+ * the plugin must never persist settings on the user's behalf, and one
501
+ * injected value cannot leak into unrelated git runs. Rewrites
502
+ * `https://github.com/o/r.git` → `<base>https://github.com/o/r.git`, which is
503
+ * how public mirror prefixes consume GitHub URLs.
504
+ * @param prefix - the accelerate URL (raw or normalized); undefined = off.
505
+ * @returns the `-c` pair to prepend, or `[]` when off or the value is
506
+ * unusable (an invalid value fails SAFE to a direct connection — config
507
+ * validation rejects such values at load; this is the defensive path).
508
+ */
509
+ export function accelArgs(prefix) {
510
+ if (prefix === undefined || accelerateUrlError(prefix) !== undefined)
511
+ return [];
512
+ return ['-c', `url.${normalizeAccelerateUrl(prefix)}.insteadOf=https://github.com/`];
513
+ }
467
514
  /**
468
515
  * Sync remote-tracking refs: fetch every remote and prune tracking branches
469
516
  * the remotes no longer carry (the CLI equivalent of IDEA's "synchronize
@@ -472,9 +519,11 @@ export async function deleteBranch(exec, repoRoot, name) {
472
519
  * executor's hard timeout as a GitError.
473
520
  * @param exec - executor seam.
474
521
  * @param repoRoot - main worktree directory (fetch is repository-wide).
522
+ * @param accelPrefix - optional cn-accelerate mirror prefix (see
523
+ * {@link accelArgs}); only the READ half of the network commands gets it.
475
524
  */
476
- export async function fetchAll(exec, repoRoot) {
477
- await git(exec, repoRoot, ['fetch', '--all', '--prune'], NETWORK_TIMEOUT_MS);
525
+ export async function fetchAll(exec, repoRoot, accelPrefix) {
526
+ await git(exec, repoRoot, [...accelArgs(accelPrefix), 'fetch', '--all', '--prune'], NETWORK_TIMEOUT_MS);
478
527
  }
479
528
  /**
480
529
  * Update the CURRENT checkout: fetch every remote, then fast-forward the
@@ -487,11 +536,13 @@ export async function fetchAll(exec, repoRoot) {
487
536
  * branch ITS worktree holds.
488
537
  * @param exec - executor seam.
489
538
  * @param cwd - directory whose checked-out branch is updated.
539
+ * @param accelPrefix - optional cn-accelerate mirror prefix; feeds ONLY the
540
+ * fetch phase ({@link fetchAll}) — the `--ff-only` merge is local.
490
541
  * @returns the branch name and whether HEAD actually moved (false = the
491
542
  * upstream already contained it).
492
543
  */
493
- export async function updateBranch(exec, cwd) {
494
- await fetchAll(exec, cwd);
544
+ export async function updateBranch(exec, cwd, accelPrefix) {
545
+ await fetchAll(exec, cwd, accelPrefix);
495
546
  const before = (await git(exec, cwd, ['rev-parse', 'HEAD'])).trim();
496
547
  await git(exec, cwd, ['merge', '--ff-only', '@{u}']);
497
548
  const after = (await git(exec, cwd, ['rev-parse', 'HEAD'])).trim();
@@ -505,7 +556,11 @@ export async function updateBranch(exec, cwd) {
505
556
  * git's own message rather than silently picking one), and `--force` is
506
557
  * never passed — a diverged remote refuses rather than losing commits.
507
558
  * Runs at the queried directory's own toplevel, so a linked-worktree
508
- * session pushes the branch ITS worktree holds.
559
+ * session pushes the branch ITS worktree holds. The cn-accelerate rewrite is
560
+ * deliberately NEVER injected here (no {@link accelArgs} parameter exists):
561
+ * public mirrors have no `receive-pack`, and rewriting would turn a
562
+ * diagnosable direct failure into an opaque mirror failure while leaking the
563
+ * pushed content to a third party.
509
564
  * @param exec - executor seam.
510
565
  * @param cwd - directory whose checked-out branch is pushed.
511
566
  * @returns the branch and whether the remote-tracking ref changed. A branch
@@ -853,13 +908,18 @@ export function parseLogOutput(out) {
853
908
  * `rev-parse --verify` first, so an arbitrary string can never reach the log
854
909
  * arguments as anything but a resolvable rev. An unborn checkout (no commits
855
910
  * yet) answers an empty page instead of an error.
911
+ *
912
+ * `--topo-order` is unconditional: the history card draws lane lines from
913
+ * `parents`, and a pure date order would interleave a branch's commits
914
+ * before their merge base, leaving edges pointing at rows not yet painted.
856
915
  * @param exec - executor seam.
857
916
  * @param cwd - the checkout directory.
858
917
  * @param limit - page size (1..200, validated upstream).
859
918
  * @param skip - offset (>= 0, validated upstream).
860
919
  * @param ref - optional starting rev; absent = the checkout's HEAD.
920
+ * @param filter - optional all-refs/author/date window.
861
921
  */
862
- export async function logHistory(exec, cwd, limit, skip, ref) {
922
+ export async function logHistory(exec, cwd, limit, skip, ref, filter) {
863
923
  let start;
864
924
  if (ref !== undefined && ref !== '' && ref !== 'HEAD') {
865
925
  const verified = await gitMaybe(exec, cwd, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]);
@@ -868,10 +928,21 @@ export async function logHistory(exec, cwd, limit, skip, ref) {
868
928
  }
869
929
  start = verified.trim();
870
930
  }
931
+ // A ref narrows the walk to that ref's ancestry; `--all` is only the
932
+ // no-ref default. Combining them would list every branch under a
933
+ // "this branch" filter.
934
+ const all = filter?.all === true && start === undefined;
935
+ const author = filter?.author?.trim() ?? '';
936
+ const since = filter?.since;
937
+ const until = filter?.until;
871
938
  try {
872
939
  const out = await git(exec, cwd, [
873
- 'log', `--max-count=${String(limit)}`, `--skip=${String(skip)}`,
940
+ 'log', `--max-count=${String(limit)}`, `--skip=${String(skip)}`, '--topo-order',
874
941
  '--date=iso-strict', `--pretty=format:${LOG_FORMAT}`,
942
+ ...(all ? ['--all'] : []),
943
+ ...(author === '' ? [] : [`--author=${author}`]),
944
+ ...(since === undefined ? [] : [`--since=${new Date(since).toISOString()}`]),
945
+ ...(until === undefined ? [] : [`--until=${new Date(until).toISOString()}`]),
875
946
  ...(start === undefined ? [] : [start]),
876
947
  ]);
877
948
  return parseLogOutput(out);
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,9 +8,10 @@
8
8
  import { homedir } from 'node:os';
9
9
  import z from '@deepseek-ai/schemastery';
10
10
  import { childProcessExec } from './git.js';
11
- 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';
11
+ import { ACCEL_DEFAULT_URL, accelerateUrlError } from './normalize.js';
12
+ import { handleAiCommitMessage, handleChanges, handleCommit, handleCommitFiles, handleCreateBranch, handleCreateTag, 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
- 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';
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_TAG, ROUTE_UPDATE, ROUTE_WORKTREE, ROUTE_WORKTREES_ALL } from './wire.js';
14
15
  export const name = 'dsh-git-branch';
15
16
  export const inject = [];
16
17
  /** Largest accepted request body (bytes) — these payloads are a few strings. */
@@ -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
  });
@@ -200,6 +227,7 @@ export function apply(ctx, config = {}) {
200
227
  postRoute(ROUTE_COMMIT, 'commit', handleCommit);
201
228
  postRoute(ROUTE_LOG, 'log', handleLog);
202
229
  postRoute(ROUTE_COMMIT_FILES, 'commit-files', handleCommitFiles);
230
+ postRoute(ROUTE_TAG, 'tag', handleCreateTag);
203
231
  // The AI commit-message route exists only where the host tree provides
204
232
  // the dsh-llm service: the injection resolves once the service is up and
205
233
  // never fires without it, so an llm-less deployment answers 404 and the
@@ -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
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { type CopyFilesSeams } from './copy-files.js';
6
6
  import { type DirExists, type ExcludeSeams, type Exec } from './git.js';
7
- import type { AiCommitMessageResult, ChangesResult, CommitFilesResult, CommitResult, CreateBranchResult, CreateWorktreeResult, EnsureDirectoryResult, FetchResult, GroupWorkspacesResult, InspectWorktreeResult, LogResult, PathExistsResult, PurgeDirectoryResult, RemoveWorktreeResult, RepoStatus, RouteError, StageResult, SwitchResult, UpdateResult, WorktreesAllResult } from './wire.js';
7
+ import type { AiCommitMessageResult, ChangesResult, CommitFilesResult, CommitResult, CreateBranchResult, CreateTagResult, CreateWorktreeResult, EnsureDirectoryResult, FetchResult, GroupWorkspacesResult, InspectWorktreeResult, LogResult, PathExistsResult, PurgeDirectoryResult, RemoveWorktreeResult, RepoStatus, RouteError, StageResult, SwitchResult, UpdateResult, WorktreesAllResult } from './wire.js';
8
8
  /** Everything the handlers need from the host half. */
9
9
  export interface RouteDeps {
10
10
  exec: Exec;
@@ -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. */
@@ -46,7 +50,7 @@ export declare function fsMkdirRecursive(path: string): Promise<void>;
46
50
  /** One route outcome: HTTP status plus the JSON body. */
47
51
  export interface RouteOutcome {
48
52
  status: number;
49
- body: RepoStatus | CreateWorktreeResult | SwitchResult | CreateBranchResult | FetchResult | UpdateResult | GroupWorkspacesResult | InspectWorktreeResult | RemoveWorktreeResult | PathExistsResult | EnsureDirectoryResult | WorktreesAllResult | PurgeDirectoryResult | ChangesResult | StageResult | CommitResult | LogResult | CommitFilesResult | AiCommitMessageResult | RouteError;
53
+ body: RepoStatus | CreateWorktreeResult | SwitchResult | CreateBranchResult | FetchResult | UpdateResult | GroupWorkspacesResult | InspectWorktreeResult | RemoveWorktreeResult | PathExistsResult | EnsureDirectoryResult | WorktreesAllResult | PurgeDirectoryResult | ChangesResult | StageResult | CommitResult | LogResult | CommitFilesResult | CreateTagResult | AiCommitMessageResult | RouteError;
50
54
  }
51
55
  /**
52
56
  * POST /group — git belonging facts for a batch of workspace directories.
@@ -270,10 +274,25 @@ export declare const LOG_LIMIT_MAX = 200;
270
274
  * POST /log — one page of the queried checkout's history. The optional `ref`
271
275
  * is verified server-side (`rev-parse --verify`), an unborn checkout answers
272
276
  * an empty page, and everything else surfaces git's own stderr.
277
+ *
278
+ * `all` walks every ref instead of HEAD (the history card's default view: one
279
+ * graph holding all branches, tags, and their merge edges). `author` and the
280
+ * `since`/`until` window are pushed down to git rather than filtered in the
281
+ * browser — the card paginates, so a client-side filter would only narrow the
282
+ * page it already has and silently hide matches on later pages.
273
283
  * @param deps - host dependencies.
274
- * @param body - parsed request body: `{ repoPath, limit?, skip?, ref? }`.
284
+ * @param body - parsed request body: `{ repoPath, limit?, skip?, ref?, all?, author?, since?, until? }`.
275
285
  */
276
286
  export declare function handleLog(deps: RouteDeps, body: unknown): Promise<RouteOutcome>;
287
+ /**
288
+ * POST /tag — create a tag at an existing commit of the queried repository.
289
+ * The target is restricted to a commit hash (the only shape the history card
290
+ * sends), so an arbitrary string can never reach `git tag` as a flag; git
291
+ * validates the tag name itself and a duplicate surfaces as 400.
292
+ * @param deps - host dependencies.
293
+ * @param body - parsed request body: `{ repoPath, name, target?, message? }`.
294
+ */
295
+ export declare function handleCreateTag(deps: RouteDeps, body: unknown): Promise<RouteOutcome>;
277
296
  /**
278
297
  * POST /commit-files — the files one commit of the queried checkout changed.
279
298
  * The hash is verified server-side; unknown revisions answer 400.