@goodandready/dsh-github-ops 0.6.6 → 0.6.7
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 +67 -0
- package/lib/routes.js +23 -2
- package/lib/scm-write.js +29 -1
- package/lib/scm.js +35 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -147,6 +147,11 @@ window.__ModuleLoader__.load({
|
|
|
147
147
|
scmStageHunk: 'Stage Hunk',
|
|
148
148
|
scmUnstageHunk: 'Unstage Hunk',
|
|
149
149
|
scmHunk: 'Hunk',
|
|
150
|
+
scmCherryPick: 'Cherry-pick',
|
|
151
|
+
scmRevert: 'Revert',
|
|
152
|
+
scmSubmodules: 'Submodules',
|
|
153
|
+
scmSubmoduleUpdate: 'Update',
|
|
154
|
+
scmSubmoduleUpdateAll: 'Update All Submodules',
|
|
150
155
|
scmGraphTitle: 'Git Graph',
|
|
151
156
|
scmGraphRefresh: 'Refresh Graph',
|
|
152
157
|
scmGraphEmpty: 'No commit history available.',
|
|
@@ -269,6 +274,11 @@ window.__ModuleLoader__.load({
|
|
|
269
274
|
scmStageHunk: '暂存代码块',
|
|
270
275
|
scmUnstageHunk: '取消暂存块',
|
|
271
276
|
scmHunk: '代码块',
|
|
277
|
+
scmCherryPick: '择优拣选',
|
|
278
|
+
scmRevert: '还原提交',
|
|
279
|
+
scmSubmodules: '子模块',
|
|
280
|
+
scmSubmoduleUpdate: '更新',
|
|
281
|
+
scmSubmoduleUpdateAll: '更新所有子模块',
|
|
272
282
|
scmGraphTitle: 'Git 提交图',
|
|
273
283
|
scmGraphRefresh: '刷新提交图',
|
|
274
284
|
scmGraphEmpty: '暂无可用的提交历史。',
|
|
@@ -584,6 +594,62 @@ window.__ModuleLoader__.load({
|
|
|
584
594
|
),
|
|
585
595
|
) : null
|
|
586
596
|
|
|
597
|
+
const graphBar = React.createElement('div', { className: 'gho-scm-bar' },
|
|
598
|
+
button(
|
|
599
|
+
showGraph ? `${t('scmGraphTitle')} ▾` : `${t('scmGraphTitle')} ▸`,
|
|
600
|
+
() => {
|
|
601
|
+
const next = !showGraph
|
|
602
|
+
setShowGraph(next)
|
|
603
|
+
if (next && graphData.phase === 'idle') loadGraph()
|
|
604
|
+
},
|
|
605
|
+
),
|
|
606
|
+
showGraph ? button(t('scmGraphRefresh'), loadGraph, { disabled: graphData.phase === 'loading' }) : null,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
const graphView = showGraph
|
|
610
|
+
? React.createElement('div', { className: 'gho-git-graph' },
|
|
611
|
+
graphData.phase === 'loading'
|
|
612
|
+
? React.createElement('div', { className: 'gho-scm-meta' }, t('scmLoading'))
|
|
613
|
+
: (graphData.entries && graphData.entries.length > 0
|
|
614
|
+
? graphData.entries.map((entry, idx) => {
|
|
615
|
+
const sha = entry.hash || entry.sha
|
|
616
|
+
const subject = entry.subject || entry.message
|
|
617
|
+
return React.createElement('div', { key: sha || idx, className: 'gho-graph-row' },
|
|
618
|
+
entry.graph ? React.createElement('span', { className: 'gho-graph-conn' }, entry.graph) : null,
|
|
619
|
+
sha ? React.createElement('span', { className: 'gho-graph-sha' }, sha.slice(0, 7)) : null,
|
|
620
|
+
entry.refs && entry.refs.length ? React.createElement('span', { className: 'gho-graph-refs' }, entry.refs.map((r, rIdx) => React.createElement('span', { key: rIdx, className: 'gho-graph-ref' }, r))) : null,
|
|
621
|
+
subject ? React.createElement('span', { className: 'gho-graph-msg' }, subject) : null,
|
|
622
|
+
sha ? React.createElement('span', { className: 'gho-scm-actions' },
|
|
623
|
+
button(
|
|
624
|
+
confirming === `cp:${sha}` ? t('scmConfirm') : t('scmCherryPick'),
|
|
625
|
+
() => armed(`cp:${sha}`, 'cherry_pick', { commit: sha, confirm: true }),
|
|
626
|
+
),
|
|
627
|
+
button(
|
|
628
|
+
confirming === `rev:${sha}` ? t('scmConfirm') : t('scmRevert'),
|
|
629
|
+
() => armed(`rev:${sha}`, 'revert', { commit: sha, confirm: true }),
|
|
630
|
+
{ warn: true },
|
|
631
|
+
),
|
|
632
|
+
) : null,
|
|
633
|
+
entry.date ? React.createElement('span', { className: 'gho-graph-meta' }, entry.date) : null,
|
|
634
|
+
)
|
|
635
|
+
})
|
|
636
|
+
: React.createElement('div', { className: 'gho-scm-meta' }, t('scmGraphEmpty'))
|
|
637
|
+
)
|
|
638
|
+
)
|
|
639
|
+
: null
|
|
640
|
+
|
|
641
|
+
const submoduleBar = (payload.submodules && payload.submodules.length > 0)
|
|
642
|
+
? React.createElement('div', { className: 'gho-scm-bar' },
|
|
643
|
+
React.createElement('span', { className: 'gho-scm-counts' }, t('scmSubmodules')),
|
|
644
|
+
button(t('scmSubmoduleUpdateAll'), () => runAction('submodule_update', {})),
|
|
645
|
+
payload.submodules.map((sm, smIdx) => React.createElement('span', { key: sm.path || smIdx, style: { display: 'inline-flex', gap: '4px', alignItems: 'center' } },
|
|
646
|
+
React.createElement('span', { className: 'gho-scm-path' }, `${sm.path} (${sm.shortCommit || ''})`),
|
|
647
|
+
React.createElement('span', { className: `gho-badge gho-badge-${sm.status === 'in-sync' ? 'healthy' : (sm.status === 'conflict' ? 'failing' : 'building')}` }, sm.status),
|
|
648
|
+
button(t('scmSubmoduleUpdate'), () => runAction('submodule_update', { path: sm.path })),
|
|
649
|
+
)),
|
|
650
|
+
)
|
|
651
|
+
: null
|
|
652
|
+
|
|
587
653
|
const worktreeBar = (payload.worktrees && payload.worktrees.length > 0)
|
|
588
654
|
? React.createElement('div', { className: 'gho-scm-bar' },
|
|
589
655
|
React.createElement('span', { className: 'gho-scm-counts' }, t('scmWorktrees')),
|
|
@@ -687,6 +753,7 @@ window.__ModuleLoader__.load({
|
|
|
687
753
|
graphView,
|
|
688
754
|
branchBar,
|
|
689
755
|
stashBar,
|
|
756
|
+
submoduleBar,
|
|
690
757
|
worktreeBar,
|
|
691
758
|
mirrorBar,
|
|
692
759
|
React.createElement('div', { className: 'gho-scm-meta' },
|
package/lib/routes.js
CHANGED
|
@@ -10,7 +10,7 @@ import { join as joinPath } from 'node:path'
|
|
|
10
10
|
|
|
11
11
|
import { statusPayload, isTrustedRequest, isTrustedWriteRequest, getRejectionStatus, getWriteRejectionStatus, sendJson } from './status.js'
|
|
12
12
|
import { buildAction, validateCwd, validateStashRef, resolveConflictFile } from './scm-write.js'
|
|
13
|
-
import { parseStatus, parseBranches, parseStashes, parseWorktrees, parseCount, scmPayload, mergeStateFrom, hasConflicts, parseConflictBlocks, parseDiffHunks, parseGitGraph } from './scm.js'
|
|
13
|
+
import { parseStatus, parseBranches, parseStashes, parseWorktrees, parseSubmodules, parseCount, scmPayload, mergeStateFrom, hasConflicts, parseConflictBlocks, parseDiffHunks, parseGitGraph } from './scm.js'
|
|
14
14
|
import { SseEventManager } from './events.js'
|
|
15
15
|
import { encodeRepoPath } from './github.js'
|
|
16
16
|
import { listIssues } from './tools/issues.js'
|
|
@@ -221,6 +221,25 @@ export function registerRoutes(ctx, deps = {}) {
|
|
|
221
221
|
stderr: String(answer.stderr || '').slice(0, 4000),
|
|
222
222
|
})
|
|
223
223
|
if (answer.code !== 0) {
|
|
224
|
+
const gitDirAnswer = await git(['rev-parse', '--absolute-git-dir'], { cwd })
|
|
225
|
+
const gitDir = gitDirAnswer.code === 0 ? gitDirAnswer.stdout.trim() : joinPath(cwd, '.git')
|
|
226
|
+
const merge = mergeStateFrom({
|
|
227
|
+
mergeHead: existsSync(joinPath(gitDir, 'MERGE_HEAD')),
|
|
228
|
+
rebaseDir: existsSync(joinPath(gitDir, 'rebase-merge')) || existsSync(joinPath(gitDir, 'rebase-apply')),
|
|
229
|
+
cherryPickHead: existsSync(joinPath(gitDir, 'CHERRY_PICK_HEAD')),
|
|
230
|
+
revertHead: existsSync(joinPath(gitDir, 'REVERT_HEAD')),
|
|
231
|
+
})
|
|
232
|
+
if (merge.inProgress) {
|
|
233
|
+
return sendJson(res, 200, {
|
|
234
|
+
ok: false,
|
|
235
|
+
action: built.action,
|
|
236
|
+
summary: built.summary,
|
|
237
|
+
steps,
|
|
238
|
+
mergeState: merge,
|
|
239
|
+
hasConflicts: true,
|
|
240
|
+
error: `${built.action} encountered merge conflicts`,
|
|
241
|
+
})
|
|
242
|
+
}
|
|
224
243
|
return sendJson(res, 200, { ok: false, action: built.action, summary: built.summary, steps, error: 'git refused the step' })
|
|
225
244
|
}
|
|
226
245
|
}
|
|
@@ -285,12 +304,13 @@ export function registerRoutes(ctx, deps = {}) {
|
|
|
285
304
|
ahead = aheadAnswer.code === 0 ? parseCount(aheadAnswer.stdout) : null
|
|
286
305
|
behind = behindAnswer.code === 0 ? parseCount(behindAnswer.stdout) : null
|
|
287
306
|
}
|
|
288
|
-
const [statusAnswer, branchesAnswer, tagsAnswer, stashesAnswer, worktreesAnswer] = await Promise.all([
|
|
307
|
+
const [statusAnswer, branchesAnswer, tagsAnswer, stashesAnswer, worktreesAnswer, submodulesAnswer] = await Promise.all([
|
|
289
308
|
git(['status', '--porcelain=v1'], { cwd }),
|
|
290
309
|
git(['branch', '--format=%(refname:short)\t%(HEAD)'], { cwd }),
|
|
291
310
|
git(['tag', '--list'], { cwd }),
|
|
292
311
|
git(['stash', 'list', '--format=%gd\t%gs'], { cwd }),
|
|
293
312
|
git(['worktree', 'list', '--porcelain'], { cwd }),
|
|
313
|
+
git(['submodule', 'status'], { cwd }),
|
|
294
314
|
])
|
|
295
315
|
const gitDirAnswer = await git(['rev-parse', '--absolute-git-dir'], { cwd })
|
|
296
316
|
const gitDir = gitDirAnswer.code === 0 ? gitDirAnswer.stdout.trim() : joinPath(cwd, '.git')
|
|
@@ -311,6 +331,7 @@ export function registerRoutes(ctx, deps = {}) {
|
|
|
311
331
|
tags: tagsAnswer.code === 0 ? tagsAnswer.stdout.split('\n').map((t) => t.trim()).filter(Boolean).slice(0, 50) : [],
|
|
312
332
|
stashes: parseStashes(stashesAnswer.code === 0 ? stashesAnswer.stdout : ''),
|
|
313
333
|
worktrees: parseWorktrees(worktreesAnswer.code === 0 ? worktreesAnswer.stdout : ''),
|
|
334
|
+
submodules: parseSubmodules(submodulesAnswer.code === 0 ? submodulesAnswer.stdout : ''),
|
|
314
335
|
merge,
|
|
315
336
|
}))
|
|
316
337
|
} catch (err) {
|
package/lib/scm-write.js
CHANGED
|
@@ -17,6 +17,15 @@ const BRANCH_RE = /^(?!-)(?!.*\.\.)[A-Za-z0-9._/-]{1,120}$/
|
|
|
17
17
|
/** A repository-relative path: no absolute paths, no traversal, no control characters. */
|
|
18
18
|
const PATH_RE = /^(?!-)(?!\/)(?!.*\.\.)[^\u0000-\u001f]{1,500}$/
|
|
19
19
|
|
|
20
|
+
/** A commit sha: 7 to 40 hexadecimal characters. */
|
|
21
|
+
const SHA_RE = /^[0-9a-fA-F]{7,40}$/
|
|
22
|
+
|
|
23
|
+
export function validateCommitSha(sha) {
|
|
24
|
+
const value = String(sha == null ? '' : sha).trim()
|
|
25
|
+
if (!SHA_RE.test(value)) throw new Error(`refusing an invalid commit hash: "${String(sha).slice(0, 40)}"`)
|
|
26
|
+
return value
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
export function validateBranch(name) {
|
|
21
30
|
const value = String(name == null ? '' : name).trim()
|
|
22
31
|
if (!BRANCH_RE.test(value)) throw new Error(`refusing an unsafe branch name: "${String(name).slice(0, 60)}"`)
|
|
@@ -175,12 +184,30 @@ export function buildAction(action, args = {}) {
|
|
|
175
184
|
step.push(path)
|
|
176
185
|
return result(name, [step], destructive, `remove worktree at ${path}`)
|
|
177
186
|
}
|
|
187
|
+
case 'cherryPick':
|
|
188
|
+
case 'cherry_pick': {
|
|
189
|
+
const sha = validateCommitSha(args.commit || args.sha)
|
|
190
|
+
return result('cherry_pick', [['cherry-pick', sha]], destructive, `cherry-pick ${sha}`)
|
|
191
|
+
}
|
|
192
|
+
case 'revert': {
|
|
193
|
+
const sha = validateCommitSha(args.commit || args.sha)
|
|
194
|
+
const noEdit = args.noEdit !== false
|
|
195
|
+
const step = noEdit ? ['revert', '--no-edit', sha] : ['revert', sha]
|
|
196
|
+
return result('revert', [step], destructive, `revert ${sha}`)
|
|
197
|
+
}
|
|
198
|
+
case 'submoduleUpdate':
|
|
199
|
+
case 'submodule_update': {
|
|
200
|
+
const path = args.path ? validatePath(args.path) : ''
|
|
201
|
+
const step = ['submodule', 'update', '--init', '--recursive']
|
|
202
|
+
if (path) step.push('--', path)
|
|
203
|
+
return result('submodule_update', [step], false, `update submodules${path ? ` in ${path}` : ''}`)
|
|
204
|
+
}
|
|
178
205
|
default:
|
|
179
206
|
throw new Error(`unknown action: "${name.slice(0, 40)}"`)
|
|
180
207
|
}
|
|
181
208
|
}
|
|
182
209
|
|
|
183
|
-
const DESTRUCTIVE = new Set(['discard', 'branchDelete', 'stashDrop', 'stash_drop', 'abort', 'worktreeRemove'])
|
|
210
|
+
const DESTRUCTIVE = new Set(['discard', 'branchDelete', 'stashDrop', 'stash_drop', 'abort', 'worktreeRemove', 'cherry_pick', 'cherryPick', 'revert'])
|
|
184
211
|
|
|
185
212
|
function result(action, steps, destructive, summary) {
|
|
186
213
|
return { action, steps, mutating: true, destructive, needsConfirm: destructive, summary }
|
|
@@ -194,6 +221,7 @@ export const ACTIONS = Object.freeze([
|
|
|
194
221
|
'worktreeAdd', 'worktreeRemove',
|
|
195
222
|
'continue', 'abort',
|
|
196
223
|
'resolveConflict',
|
|
224
|
+
'cherry_pick', 'revert', 'submodule_update',
|
|
197
225
|
])
|
|
198
226
|
|
|
199
227
|
/** The verbs the panel offers for an unfinished operation. */
|
package/lib/scm.js
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
export function parseSubmodules(stdout) {
|
|
2
|
+
if (!stdout || typeof stdout !== "string") return []
|
|
3
|
+
const lines = stdout.split("\n")
|
|
4
|
+
const submodules = []
|
|
5
|
+
for (const rawLine of lines) {
|
|
6
|
+
const line = rawLine.trimEnd()
|
|
7
|
+
if (!line) continue
|
|
8
|
+
const prefix = line[0]
|
|
9
|
+
let status = "in-sync"
|
|
10
|
+
if (prefix === "-") status = "uninitialized"
|
|
11
|
+
else if (prefix === "+") status = "modified"
|
|
12
|
+
else if (prefix === "U") status = "conflict"
|
|
13
|
+
|
|
14
|
+
const rest = (prefix === " " || prefix === "-" || prefix === "+" || prefix === "U") ? line.slice(1).trim() : line.trim()
|
|
15
|
+
const parts = rest.split(/\s+/)
|
|
16
|
+
const commit = parts[0] || ""
|
|
17
|
+
const path = parts[1] || ""
|
|
18
|
+
const describeMatch = rest.match(/\(([^)]+)\)/)
|
|
19
|
+
const describe = describeMatch ? describeMatch[1] : null
|
|
20
|
+
|
|
21
|
+
if (commit && path) {
|
|
22
|
+
submodules.push({
|
|
23
|
+
status,
|
|
24
|
+
commit,
|
|
25
|
+
shortCommit: commit.slice(0, 7),
|
|
26
|
+
path,
|
|
27
|
+
describe,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return submodules
|
|
32
|
+
}
|
|
33
|
+
|
|
1
34
|
// Reading a working copy, for the Source Control tab.
|
|
2
35
|
//
|
|
3
36
|
// The parsers are pure: they take the text git prints and return a structure. That keeps the
|
|
@@ -123,7 +156,7 @@ export function parseCount(text) {
|
|
|
123
156
|
* ahead/behind, and that is reported as such rather than as an error.
|
|
124
157
|
*/
|
|
125
158
|
export function scmPayload({
|
|
126
|
-
cwd, branch, upstream, ahead, behind, status, branches, tags, stashes, worktrees, merge, error,
|
|
159
|
+
cwd, branch, upstream, ahead, behind, status, branches, tags, stashes, worktrees, submodules, merge, error,
|
|
127
160
|
} = {}) {
|
|
128
161
|
return {
|
|
129
162
|
cwd: cwd || '',
|
|
@@ -137,6 +170,7 @@ export function scmPayload({
|
|
|
137
170
|
tags: Array.isArray(tags) ? tags : [],
|
|
138
171
|
stashes: Array.isArray(stashes) ? stashes : [],
|
|
139
172
|
worktrees: Array.isArray(worktrees) ? worktrees : [],
|
|
173
|
+
submodules: Array.isArray(submodules) ? submodules : [],
|
|
140
174
|
mergeState: merge || { inProgress: false, kind: '' },
|
|
141
175
|
error: error ? String(error).slice(0, 300) : '',
|
|
142
176
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-github-ops",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"description": "GitHub operations for DeepSeek Harness: releases, tags, a guarded generic API pass-through, workflow runs, secrets and sanitized mirror publication.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|