dsh-ide-git 0.21.2 → 0.23.0
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/README.md +6 -5
- package/README_EN.md +8 -6
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +1045 -115
- package/src/index.js +114 -13
package/src/index.js
CHANGED
|
@@ -45,7 +45,7 @@ try {
|
|
|
45
45
|
|
|
46
46
|
/** Rail action ids, in rail order. MUST mirror RAIL_SPECS in src/client.js. */
|
|
47
47
|
const RAIL_CONFIG_IDS = [
|
|
48
|
-
'refresh', 'tree', 'float', 'split', 'fullscreen', 'newBranch', 'checkout', 'delete',
|
|
48
|
+
'refresh', 'tree', 'worktrees', 'float', 'split', 'fullscreen', 'newBranch', 'checkout', 'delete',
|
|
49
49
|
'compare', 'diff', 'stash', 'tag', 'favorite', 'fetch', 'pull', 'push',
|
|
50
50
|
]
|
|
51
51
|
|
|
@@ -84,6 +84,11 @@ function railConfigSchema(schema) {
|
|
|
84
84
|
back and said "设置没写进去". Any new row-Config field the client writes MUST
|
|
85
85
|
be declared (and volatile) here, or the write is refused before it lands. */
|
|
86
86
|
shape.composerStatus = live(schema.boolean().default(true))
|
|
87
|
+
/* Inline diff preview (issue #22): ON by default so the panel's own diff
|
|
88
|
+
pane — with its split / wrap switches — stays reachable even when
|
|
89
|
+
dsh-better-sidebar or the native preview seat is installed. OFF hands the
|
|
90
|
+
preview back to those external tabs, exactly the pre-0.23 behaviour. */
|
|
91
|
+
shape.inlineDiff = live(schema.boolean().default(true))
|
|
87
92
|
return schema.object(shape)
|
|
88
93
|
}
|
|
89
94
|
|
|
@@ -429,25 +434,54 @@ function sameDirectory(left, right) {
|
|
|
429
434
|
try { return realpathSync(one) === realpathSync(two) } catch { return false }
|
|
430
435
|
}
|
|
431
436
|
|
|
437
|
+
/* `--porcelain -z` is the only parseable form: a path may contain spaces and
|
|
438
|
+
newlines, and git realpaths the main worktree (/var -> /private/var on macOS),
|
|
439
|
+
so every record is NUL-terminated and an empty record closes the current
|
|
440
|
+
block. The flags describe the ADMIN view: `locked` and `prunable` say
|
|
441
|
+
whether removal is blocked or the directory is already gone. */
|
|
432
442
|
async function worktreesOf(root) {
|
|
433
|
-
const result = await runGit(root, ['worktree', 'list', '--porcelain'])
|
|
443
|
+
const result = await runGit(root, ['worktree', 'list', '--porcelain', '-z'])
|
|
434
444
|
if (result.code !== 0) return []
|
|
435
445
|
const trees = []
|
|
436
446
|
let current = null
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
447
|
+
const push = () => { if (current !== null) trees.push(current); current = null }
|
|
448
|
+
for (const record of result.stdout.split('\u0000')) {
|
|
449
|
+
if (record === '') { push(); continue }
|
|
450
|
+
if (record.startsWith('worktree ')) {
|
|
451
|
+
push()
|
|
452
|
+
current = {
|
|
453
|
+
path: record.slice('worktree '.length),
|
|
454
|
+
branch: null,
|
|
455
|
+
head: null,
|
|
456
|
+
detached: false,
|
|
457
|
+
bare: false,
|
|
458
|
+
locked: false,
|
|
459
|
+
lockReason: null,
|
|
460
|
+
prunable: false,
|
|
461
|
+
pruneReason: null,
|
|
462
|
+
}
|
|
441
463
|
continue
|
|
442
464
|
}
|
|
443
465
|
if (current === null) continue
|
|
444
|
-
if (
|
|
445
|
-
else if (
|
|
446
|
-
else if (
|
|
447
|
-
else if (
|
|
466
|
+
if (record.startsWith('HEAD ')) current.head = record.slice('HEAD '.length)
|
|
467
|
+
else if (record.startsWith('branch ')) current.branch = record.slice('branch '.length).replace('refs/heads/', '')
|
|
468
|
+
else if (record === 'detached') current.detached = true
|
|
469
|
+
else if (record === 'bare') current.bare = true
|
|
470
|
+
else if (record.startsWith('locked')) {
|
|
471
|
+
current.locked = true
|
|
472
|
+
const reason = record.slice('locked'.length).trim()
|
|
473
|
+
current.lockReason = reason === '' ? null : reason
|
|
474
|
+
} else if (record.startsWith('prunable')) {
|
|
475
|
+
current.prunable = true
|
|
476
|
+
const reason = record.slice('prunable'.length).trim()
|
|
477
|
+
current.pruneReason = reason === '' ? null : reason
|
|
478
|
+
}
|
|
448
479
|
}
|
|
449
|
-
|
|
450
|
-
return trees
|
|
480
|
+
push()
|
|
481
|
+
return trees.map((tree, index) => Object.assign(tree, {
|
|
482
|
+
primary: index === 0,
|
|
483
|
+
current: sameDirectory(tree.path, root),
|
|
484
|
+
}))
|
|
451
485
|
}
|
|
452
486
|
|
|
453
487
|
/* ============================== safety ============================== */
|
|
@@ -471,7 +505,7 @@ function withRepoLock(key, work) {
|
|
|
471
505
|
|
|
472
506
|
const WRITE_METHODS = new Set([
|
|
473
507
|
'stage', 'unstage', 'discard', 'commit', 'checkout', 'branchCreate', 'branchRename', 'branch-upstream',
|
|
474
|
-
'branchDelete', 'merge', 'rebase', 'cherryPick', 'revert', 'reset', 'fetch', 'pull',
|
|
508
|
+
'branchDelete', 'worktreeAdd', 'worktreeRemove', 'worktreePrune', 'merge', 'rebase', 'cherryPick', 'revert', 'reset', 'fetch', 'pull',
|
|
475
509
|
'push', 'stashPush', 'stashApply', 'stashDrop', 'stashClear', 'tagCreate', 'tagDelete', 'tagDeleteRemote', 'pushTags', 'clone', 'remoteAdd', 'remoteRemove', 'branchDeleteRemote', 'undoApply',
|
|
476
510
|
])
|
|
477
511
|
|
|
@@ -1025,6 +1059,70 @@ async function branchCreate(payload) {
|
|
|
1025
1059
|
return { branch: name }
|
|
1026
1060
|
}
|
|
1027
1061
|
|
|
1062
|
+
/* ---------- worktrees (v0.22, alpha.2) ----------
|
|
1063
|
+
A worktree is a second checkout of the same repository. These three verbs
|
|
1064
|
+
are the whole lifecycle the panel offers: create a checkout (on a new or an
|
|
1065
|
+
existing branch), remove one, and prune the admin records of directories
|
|
1066
|
+
that are already gone.
|
|
1067
|
+
|
|
1068
|
+
Removal is deliberately strict. The current and primary checkouts are
|
|
1069
|
+
refused outright (removing the tree the panel is reading is never what a
|
|
1070
|
+
click meant), a locked worktree must be unlocked first, and a dirty one
|
|
1071
|
+
needs a second request with force:true. The dirty check runs INSIDE the
|
|
1072
|
+
target, not the repository root: git status at the root says nothing
|
|
1073
|
+
about the linked checkout's files. */
|
|
1074
|
+
async function worktreeAdd(payload) {
|
|
1075
|
+
const cwd = cwdOf(payload)
|
|
1076
|
+
const root = await repoRootOf(cwd, payload)
|
|
1077
|
+
const target = requireAbsolute(payload.path, 'path')
|
|
1078
|
+
if (sameDirectory(target, root)) throw badRequest('path must differ from the current worktree')
|
|
1079
|
+
if (existsSync(target)) throw new PanelError('worktree-exists', 'path already exists: ' + target, 409)
|
|
1080
|
+
const branch = requireRef(payload.branch, 'branch')
|
|
1081
|
+
const checked = await runGit(root, ['check-ref-format', '--branch', branch])
|
|
1082
|
+
if (checked.code !== 0) throw badRequest('invalid branch name: ' + branch)
|
|
1083
|
+
const createBranch = payload.createBranch !== false
|
|
1084
|
+
const args = ['worktree', 'add']
|
|
1085
|
+
if (createBranch === true) {
|
|
1086
|
+
args.push('-b', branch, target)
|
|
1087
|
+
if (typeof payload.base === 'string' && payload.base.trim() !== '') args.push(requireRef(payload.base, 'base'))
|
|
1088
|
+
} else {
|
|
1089
|
+
args.push(target, branch)
|
|
1090
|
+
}
|
|
1091
|
+
await git(root, args)
|
|
1092
|
+
return { path: target, branch: branch, created: createBranch }
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
async function worktreeRemove(payload) {
|
|
1096
|
+
const cwd = cwdOf(payload)
|
|
1097
|
+
const root = await repoRootOf(cwd, payload)
|
|
1098
|
+
if (payload.confirm !== true) throw badRequest('removing a worktree requires confirm: true')
|
|
1099
|
+
const target = requireAbsolute(payload.path, 'path')
|
|
1100
|
+
const trees = await worktreesOf(root)
|
|
1101
|
+
const tree = trees.find((entry) => sameDirectory(entry.path, target))
|
|
1102
|
+
if (tree === undefined) throw new PanelError('worktree-missing', 'not a worktree of this repository: ' + target, 409)
|
|
1103
|
+
if (tree.current === true) throw new PanelError('protected-worktree', 'refusing to remove the worktree this panel is showing', 409)
|
|
1104
|
+
if (tree.primary === true) throw new PanelError('protected-worktree', 'refusing to remove the primary worktree', 409)
|
|
1105
|
+
if (tree.locked === true) throw new PanelError('worktree-locked', 'worktree is locked; unlock it before removing: ' + target, 409)
|
|
1106
|
+
if (tree.prunable === true || existsSync(target) === false) throw new PanelError('worktree-missing', 'worktree directory is gone; prune the record instead: ' + target, 409)
|
|
1107
|
+
const dirty = await runGit(target, ['status', '--porcelain=v1', '-z', '--untracked-files=all'])
|
|
1108
|
+
if (dirty.code !== 0) throw new PanelError('worktree-unreadable', 'could not inspect the worktree: ' + target, 409)
|
|
1109
|
+
const hasChanges = dirty.stdout !== ''
|
|
1110
|
+
if (hasChanges === true && payload.force !== true) throw new PanelError('worktree-dirty', 'worktree has uncommitted changes; force removal deletes them: ' + target, 409)
|
|
1111
|
+
const args = ['worktree', 'remove']
|
|
1112
|
+
if (payload.force === true) args.push('--force')
|
|
1113
|
+
args.push(target)
|
|
1114
|
+
await git(root, args)
|
|
1115
|
+
return { path: target, forced: payload.force === true, dirty: hasChanges }
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
async function worktreePrune(payload) {
|
|
1119
|
+
const cwd = cwdOf(payload)
|
|
1120
|
+
const root = await repoRootOf(cwd, payload)
|
|
1121
|
+
if (payload.confirm !== true) throw badRequest('pruning worktrees requires confirm: true')
|
|
1122
|
+
const output = await git(root, ['worktree', 'prune', '-v'])
|
|
1123
|
+
return { output: output.trim() }
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1028
1126
|
/* Upstream management (issue #18): rebind or drop a branch's tracking remote.
|
|
1029
1127
|
git refuses --set-upstream-to with a ref it cannot resolve, so a typo dies
|
|
1030
1128
|
here instead of silently writing branch.<name>.merge. */
|
|
@@ -2051,6 +2149,9 @@ const METHODS = {
|
|
|
2051
2149
|
branchRename,
|
|
2052
2150
|
'branch-upstream': branchUpstream,
|
|
2053
2151
|
branchDelete,
|
|
2152
|
+
worktreeAdd,
|
|
2153
|
+
worktreeRemove,
|
|
2154
|
+
worktreePrune,
|
|
2054
2155
|
merge,
|
|
2055
2156
|
rebase,
|
|
2056
2157
|
cherryPick,
|