dsh-ide-git 0.14.3 → 0.14.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/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-ide-git",
3
- "version": "0.14.3",
3
+ "version": "0.14.5",
4
4
  "main": "./src/index.js",
5
5
  "description": "IDE 级 Git 操作面板:分支树 + 提交图谱 + 变更/暂存/提交 + 提交详情。装了 dsh-better-sidebar 时挂成它的原生 tab(右侧栏 + 底部工作台);没装时直接挂 DSH 原生右侧栏,单独安装即可用。JetBrains 风格 Git 工具窗口 for DeepSeek Harness.",
6
6
  "engines": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ide-git",
3
- "version": "0.14.3",
3
+ "version": "0.14.5",
4
4
  "description": "DSH plugin: an IDE-grade Git tool window as a native dsh-better-sidebar tab — branch tree, commit graph, changes, commit details, JetBrains-style actions, in the right sidebar and the bottom panel. | DSH 插件:IDE 级 Git 工具窗口,以 dsh-better-sidebar 原生 Tab 挂载——分支树 / 提交图谱 / 变更与提交详情 / JetBrains 风格操作,右侧栏与底部面板双布局自适应。",
5
5
  "type": "module",
6
6
  "repository": {
package/src/client.js CHANGED
@@ -6659,7 +6659,15 @@ window.__ModuleLoader__.load({
6659
6659
  E('span', { className: 'dig-lf-number' }, row.old === undefined ? '' : String(row.old)),
6660
6660
  E('span', { className: 'dig-lf-number' }, row.new === undefined ? '' : String(row.new)),
6661
6661
  E('span', { className: 'dig-lf-sign' }, row.kind === 'add' ? '+' : row.kind === 'del' ? '\u2212' : ' '),
6662
- E(DiffText, { text: row.text, spans: row.kind === 'add' ? spans.new.get(row.new) : spans.old.get(row.old) }))
6662
+ /* The guards are load-bearing (FileDiff.tsx uses `highlighted?.new?.get`):
6663
+ highlightLines returns undefined for unknown or not-yet-loaded
6664
+ grammars, so the first frame (and every plain-text file) renders
6665
+ with spans === undefined until the grammar store bumps a re-render.
6666
+ Without the guard the slot entry crashes and 0.2.x unmounts the
6667
+ whole preview pane (blank "Git 预览"). */
6668
+ E(DiffText, { text: row.text, spans: row.kind === 'add'
6669
+ ? (spans.new === undefined ? undefined : spans.new.get(row.new))
6670
+ : (spans.old === undefined ? undefined : spans.old.get(row.old)) }))
6663
6671
  }
6664
6672
 
6665
6673
  /* The wrapped split row: two cells, each its own number + text. */
@@ -7972,13 +7980,19 @@ window.__ModuleLoader__.load({
7972
7980
  setAiNote(null)
7973
7981
  try {
7974
7982
  const settings = props.commitSettings === undefined ? { model: '', reasoning: '', prompt: '' } : props.commitSettings
7975
- const data = await props.request('commit-message', {
7976
- cwd: props.cwd,
7977
- sessionId: props.sessionId,
7978
- model: settings.model,
7979
- reasoningEffort: settings.reasoning,
7980
- prompt: settings.prompt,
7981
- })
7983
+ /* `base` carries the picked repository ({ cwd, repoRoot }) exactly like
7984
+ every other request: without it the host resolves the repository from
7985
+ the session cwd, which need not be one (several sibling repos). */
7986
+ const data = await props.request('commit-message', Object.assign(
7987
+ {},
7988
+ props.base === undefined ? { cwd: props.cwd } : props.base,
7989
+ {
7990
+ sessionId: props.sessionId,
7991
+ model: settings.model,
7992
+ reasoningEffort: settings.reasoning,
7993
+ prompt: settings.prompt,
7994
+ },
7995
+ ))
7982
7996
  setAiUndo(message === '' ? null : message)
7983
7997
  setMessage(typeof data.message === 'string' ? data.message : '')
7984
7998
  /* Say what the model did NOT see, instead of quietly sending less. */
@@ -8876,8 +8890,8 @@ window.__ModuleLoader__.load({
8876
8890
  try {
8877
8891
  const scopeObject = scope || {}
8878
8892
  previewStore.set(commitHash !== null
8879
- ? { hash: commitHash, subject: seed.subject || '', cwd: scopeObject.cwd, repoRoot: repoRoot, sessionId: scopeObject.sessionId }
8880
- : { path: seed.path, staged: seed.staged === true, cwd: scopeObject.cwd, repoRoot: repoRoot, sessionId: scopeObject.sessionId })
8893
+ ? { hash: commitHash, subject: seed.subject || '', path: seed.path !== undefined && seed.path !== null ? seed.path : '', cwd: scopeObject.cwd, repoRoot: repoRoot, sessionId: scopeObject.sessionId }
8894
+ : { path: seed.path, staged: seed.staged === true, untracked: seed.untracked === true ? true : undefined, cwd: scopeObject.cwd, repoRoot: repoRoot, sessionId: scopeObject.sessionId })
8881
8895
  sidebarRight.openTab(NATIVE_PREVIEW_KIND)
8882
8896
  return true
8883
8897
  } catch (error) { void error }
@@ -9360,7 +9374,7 @@ window.__ModuleLoader__.load({
9360
9374
  const commitSettings = readCommitSettings()
9361
9375
  const changesPane = E(ChangesPanel, {
9362
9376
  t: t, summary: summary, busy: busy, compact: compact, hideHeader: compact,
9363
- request: request, cwd: cwd, sessionId: sessionId, commitSettings: commitSettings,
9377
+ request: request, cwd: cwd, sessionId: sessionId, commitSettings: commitSettings, base: base,
9364
9378
  showIgnored: showIgnored,
9365
9379
  onToggleIgnored: () => setShowIgnored((value) => !value),
9366
9380
  // Commit-and-push carries the confirm flag with it: the button already
@@ -9439,7 +9453,7 @@ window.__ModuleLoader__.load({
9439
9453
  ['untracked', changes.untracked, false],
9440
9454
  ]
9441
9455
  for (const entry of order) {
9442
- if (entry[1].length > 0) { showDiff({ path: entry[1][0].path, staged: entry[2] }); return }
9456
+ if (entry[1].length > 0) { showDiff({ path: entry[1][0].path, staged: entry[2], untracked: entry[0] === 'untracked' }); return }
9443
9457
  }
9444
9458
  setNote(t('note.noChanges'))
9445
9459
  }
@@ -10509,7 +10523,9 @@ window.__ModuleLoader__.load({
10509
10523
  useEffect(() => previewStore.subscribe(() => { setSeed(previewStore.seed) }), [])
10510
10524
  const seedKey = seed === null
10511
10525
  ? ''
10512
- : (seed.hash !== undefined && seed.hash !== null ? 'c:' + String(seed.hash) : 'w:' + String(seed.staged === true) + ':' + String(seed.path))
10526
+ : (seed.hash !== undefined && seed.hash !== null
10527
+ ? 'c:' + String(seed.hash) + ':' + String(seed.path !== undefined && seed.path !== null ? seed.path : '')
10528
+ : 'w:' + String(seed.staged === true) + ':' + String(seed.untracked === true) + ':' + String(seed.path))
10513
10529
  const [state, setState] = useState({ loading: false, patch: '', error: null })
10514
10530
  const [refreshTick, setRefreshTick] = useState(0)
10515
10531
  const [copied, setCopied] = useState(false)
@@ -10523,8 +10539,18 @@ window.__ModuleLoader__.load({
10523
10539
  try {
10524
10540
  const payload = { cwd: seed.cwd }
10525
10541
  if (seed.repoRoot !== undefined && seed.repoRoot !== null && seed.repoRoot !== '') payload.repoRoot = seed.repoRoot
10526
- if (seed.hash !== undefined && seed.hash !== null && seed.hash !== '') payload.hash = seed.hash
10527
- else { payload.path = seed.path; payload.staged = seed.staged === true }
10542
+ if (seed.hash !== undefined && seed.hash !== null && seed.hash !== '') {
10543
+ payload.hash = seed.hash
10544
+ /* hash+path rides `git show <hash> -- <path>` (host-supported
10545
+ since the route exists): the preview follows the exact file
10546
+ the user clicked instead of the commit's first file. */
10547
+ if (seed.path !== undefined && seed.path !== null && seed.path !== '') payload.path = seed.path
10548
+ } else {
10549
+ payload.path = seed.path; payload.staged = seed.staged === true
10550
+ /* untracked files never ride `git diff -- path` (no index entry):
10551
+ the host switches to a --no-index /dev/null new-file patch. */
10552
+ if (seed.untracked === true) payload.untracked = true
10553
+ }
10528
10554
  const data = await request('diff', payload)
10529
10555
  if (alive === true) setState({ loading: false, patch: data === undefined || data === null || typeof data.patch !== 'string' ? '' : data.patch, error: null })
10530
10556
  } catch (error) {
package/src/index.js CHANGED
@@ -726,6 +726,38 @@ async function commitDetail(payload) {
726
726
  async function diff(payload) {
727
727
  const cwd = cwdOf(payload)
728
728
  const root = await repoRootOf(cwd, payload)
729
+ /* Untracked files have no index entry, so plain `git diff [--cached] -- path`
730
+ is legitimately empty for them — the panel's change list still shows the
731
+ file and the preview must not go blank. `--no-index /dev/null <file>`
732
+ produces the honest "new file" patch; its exit code is 1 when differences
733
+ exist, which is data, not failure (only >1 rejects). */
734
+ if (payload.untracked === true) {
735
+ const rel = requireRelativePath(payload.path)
736
+ const absolute = path.isAbsolute(rel) ? rel : path.join(root, rel)
737
+ /* --no-index reads the file for real, so unlike the plain `git diff --`
738
+ form an absolute path must NOT be allowed to escape the repository. */
739
+ if (absolute !== root && !absolute.startsWith(root + path.sep)) throw badRequest('path must stay inside the repository')
740
+ const result = await runGit(root, ['diff', '--no-color', '--no-ext-diff', '-U3', '--no-index', '--', '/dev/null', absolute])
741
+ if (result.code !== 0 && result.code !== 1) {
742
+ const detail = result.stderr.trim() === '' ? result.stdout.trim() : result.stderr.trim()
743
+ throw new PanelError('git-failed', 'git diff --no-index failed: ' + (detail === '' ? 'exit ' + result.code : detail), 409)
744
+ }
745
+ /* --no-index spells the b/ side with the absolute path we passed; rewrite
746
+ the header lines back to the repository-relative spelling so the preview
747
+ reads like every other patch (body lines are left untouched). */
748
+ let out = result.stdout
749
+ if (out.indexOf(absolute) >= 0 && out.indexOf('\n') !== -1) {
750
+ out = out.split('\n').map((line) => {
751
+ if (line.startsWith('diff --git ') && line.indexOf(absolute) >= 0) return 'diff --git a/' + rel + ' b/' + rel
752
+ /* the b/ side of --no-index carries the absolute path verbatim (its
753
+ leading slash merged into the separator): match '+++ b' + absolute. */
754
+ if (line.startsWith('+++ b' + absolute)) return '+++ b/' + rel
755
+ return line
756
+ }).join('\n')
757
+ }
758
+ const truncated = out.length > MAX_DIFF_CHARS
759
+ return { patch: truncated ? out.slice(0, MAX_DIFF_CHARS) : out, truncated }
760
+ }
729
761
  const args = ['diff', '--no-color', '--no-ext-diff', '-U3']
730
762
  if (payload.staged === true) args.push('--cached')
731
763
  if (typeof payload.hash === 'string' && payload.hash.trim() !== '') {
@@ -1547,10 +1579,17 @@ async function commitMessage(payload) {
1547
1579
  commitBusy = true
1548
1580
  try {
1549
1581
  const cwd = cwdOf(payload)
1582
+ /* The picked repository travels as `repoRoot`, and every other git call
1583
+ threads it through `repoRootOf`. Resolving the root from `cwd` alone made
1584
+ this the one route that answered not-a-repo in a workspace whose own root
1585
+ is not a repository — a workspace holding several sibling repos, which is
1586
+ exactly what the repo picker exists for. */
1587
+ const repoRoot = await repoRootOf(cwd, payload)
1588
+ const scope = { cwd: cwd, repoRoot: repoRoot }
1550
1589
  const route = await commitRouteOf(payload)
1551
- const sums = await summary({ cwd: cwd })
1552
- const stagedPatch = sums.changes.staged.length > 0 ? (await diff({ cwd: cwd, staged: true })).patch : ''
1553
- const input = commitInputOf(stagedPatch, (await diff({ cwd: cwd })).patch)
1590
+ const sums = await summary(scope)
1591
+ const stagedPatch = sums.changes.staged.length > 0 ? (await diff(Object.assign({}, scope, { staged: true }))).patch : ''
1592
+ const input = commitInputOf(stagedPatch, (await diff(scope)).patch)
1554
1593
  if (input.files.length === 0) {
1555
1594
  if (input.dropped > 0) {
1556
1595
  throw new PanelError('only-ignored-changes', 'the only changes are lock files or binary files, which are not sent to the model; stage a source change first')