difit 4.0.2 → 4.0.4

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.
Files changed (201) hide show
  1. package/README.ja.md +1 -0
  2. package/README.ko.md +1 -0
  3. package/README.md +1 -0
  4. package/README.zh.md +1 -0
  5. package/dist/cli/index.js +38 -28
  6. package/dist/cli/index.test.js +162 -77
  7. package/dist/cli/utils.js +4 -0
  8. package/dist/cli/utils.test.js +6 -1
  9. package/dist/client/assets/_baseFor-DKyA49xd.js +1 -0
  10. package/dist/client/assets/arc-COOp7iVe.js +1 -0
  11. package/dist/client/assets/architecture-YZFGNWBL-Cs2Q6RQP.js +1 -0
  12. package/dist/client/assets/architectureDiagram-Q4EWVU46-BO4dVPUA.js +36 -0
  13. package/dist/client/assets/blockDiagram-DXYQGD6D-CtNJnEWN.js +132 -0
  14. package/dist/client/assets/c4Diagram-AHTNJAMY-BqG-1m6C.js +10 -0
  15. package/dist/client/assets/channel-_xDT1u3-.js +1 -0
  16. package/dist/client/assets/chunk-2KRD3SAO-DqP2NJNd.js +1 -0
  17. package/dist/client/assets/chunk-336JU56O-D1qa7Qzb.js +2 -0
  18. package/dist/client/assets/chunk-426QAEUC-6J_A_wvD.js +1 -0
  19. package/dist/client/assets/{chunk-4BX2VUAB-CW45MZFx.js → chunk-4BX2VUAB-BT78EnQ6.js} +1 -1
  20. package/dist/client/assets/chunk-4TB4RGXK-C4w_Bwzw.js +206 -0
  21. package/dist/client/assets/{chunk-55IACEB6-Busc3sfI.js → chunk-55IACEB6-z3MQSTaj.js} +1 -1
  22. package/dist/client/assets/chunk-5FUZZQ4R-Chei69aj.js +62 -0
  23. package/dist/client/assets/chunk-5PVQY5BW-HgRiIs0X.js +2 -0
  24. package/dist/client/assets/chunk-67CJDMHE-B2q10-fp.js +1 -0
  25. package/dist/client/assets/chunk-7N4EOEYR-DPgxysWq.js +1 -0
  26. package/dist/client/assets/chunk-AA7GKIK3-BqmVmKLq.js +1 -0
  27. package/dist/client/assets/chunk-BSJP7CBP-CaIgleFn.js +1 -0
  28. package/dist/client/assets/chunk-CFjPhJqf.js +1 -0
  29. package/dist/client/assets/chunk-CIAEETIT-ByD-tlNF.js +1 -0
  30. package/dist/client/assets/{chunk-KX2RTZJC-DrhxxMOx.js → chunk-EDXVE4YY-d3RUKKAj.js} +1 -1
  31. package/dist/client/assets/chunk-ENJZ2VHE-CNq5Qmg9.js +10 -0
  32. package/dist/client/assets/{chunk-FMBD7UC4-BSsJVlRg.js → chunk-FMBD7UC4-DYfHJ6MV.js} +1 -1
  33. package/dist/client/assets/chunk-FOC6F5B3-BRpSWlZj.js +1 -0
  34. package/dist/client/assets/chunk-ICPOFSXX-B_MThwG6.js +122 -0
  35. package/dist/client/assets/chunk-K5T4RW27-DmamW1Ds.js +94 -0
  36. package/dist/client/assets/chunk-KGLVRYIC-CRbg4c4z.js +1 -0
  37. package/dist/client/assets/chunk-LIHQZDEY-CHQPSdB3.js +1 -0
  38. package/dist/client/assets/chunk-ORNJ4GCN-CIsQ4Zi4.js +1 -0
  39. package/dist/client/assets/{chunk-NQ4KR5QH-BZ86r2qK.js → chunk-OYMX7WX6-Cxi0kdGg.js} +25 -14
  40. package/dist/client/assets/chunk-QZHKN3VN-C0QzfgZ8.js +1 -0
  41. package/dist/client/assets/{chunk-PU5JKC2W-PQmA4K_y.js → chunk-U2HBQHQK-V_hneCfR.js} +6 -6
  42. package/dist/client/assets/{chunk-PQ6SQG4A-C9acTu_E.js → chunk-X2U36JSP-De4pvO-I.js} +1 -1
  43. package/dist/client/assets/{chunk-XPW4576I-CcqR6BsE.js → chunk-XPW4576I-B_osXKp6.js} +2 -2
  44. package/dist/client/assets/{chunk-JSJVCQXG-UCJub_Eo.js → chunk-YZCP3GAM-C_kqXssD.js} +1 -1
  45. package/dist/client/assets/chunk-ZZ45TVLE-B_xtlma5.js +1 -0
  46. package/dist/client/assets/classDiagram-6PBFFD2Q-5XrS-DAQ.js +1 -0
  47. package/dist/client/assets/classDiagram-v2-HSJHXN6E-Covl2vKy.js +1 -0
  48. package/dist/client/assets/clone-rhRH8pyW.js +1 -0
  49. package/dist/client/assets/cose-bilkent-S5V4N54A-BvXFc7Rr.js +1 -0
  50. package/dist/client/assets/cytoscape.esm-DRReFUEO.js +321 -0
  51. package/dist/client/assets/dagre-DU-XBdcU.js +1 -0
  52. package/dist/client/assets/dagre-KV5264BT-BWYGReXF.js +4 -0
  53. package/dist/client/assets/diagram-5BDNPKRD-DpUUhvWz.js +10 -0
  54. package/dist/client/assets/diagram-G4DWMVQ6-BJoTrUAx.js +24 -0
  55. package/dist/client/assets/diagram-MMDJMWI5-CAk1GW5g.js +43 -0
  56. package/dist/client/assets/diagram-TYMM5635-Cct6g7FA.js +24 -0
  57. package/dist/client/assets/dist-61sCfOmN.js +1 -0
  58. package/dist/client/assets/{erDiagram-INFDFZHY-ByL02DP-.js → erDiagram-SMLLAGMA-DHs2bXUj.js} +33 -18
  59. package/dist/client/assets/flatten-mnWyE-RB.js +1 -0
  60. package/dist/client/assets/flowDiagram-DWJPFMVM-DLu-6dfC.js +162 -0
  61. package/dist/client/assets/ganttDiagram-T4ZO3ILL-CMIzlKAR.js +292 -0
  62. package/dist/client/assets/gitGraph-7Q5UKJZL-A_wWsXju.js +1 -0
  63. package/dist/client/assets/gitGraphDiagram-UUTBAWPF-Bc_rL3_k.js +106 -0
  64. package/dist/client/assets/graphlib-BVMK0xYE.js +1 -0
  65. package/dist/client/assets/{index-Cn4K2uvR.css → index-Cq_APK7Y.css} +1 -1
  66. package/dist/client/assets/index-RcU838Ah.js +79 -0
  67. package/dist/client/assets/info-OMHHGYJF-Bv3kK2Bb.js +1 -0
  68. package/dist/client/assets/{infoDiagram-LFFYTUFH-CnmYkyCb.js → infoDiagram-42DDH7IO-Cf8u4jgP.js} +1 -1
  69. package/dist/client/assets/isEmpty-CiiIHfXR.js +1 -0
  70. package/dist/client/assets/ishikawaDiagram-UXIWVN3A-7n7DvfEb.js +70 -0
  71. package/dist/client/assets/{journeyDiagram-4ABVD52K-aRoH36nV.js → journeyDiagram-VCZTEJTY-BMkeQqJb.js} +2 -2
  72. package/dist/client/assets/{kanban-definition-K7BYSVSG-BGtGv5yb.js → kanban-definition-6JOO6SKY-B8KkeZLS.js} +2 -2
  73. package/dist/client/assets/katex-FOM3xZj7.js +257 -0
  74. package/dist/client/assets/line-CVpcI6kj.js +1 -0
  75. package/dist/client/assets/{linear-HJOLPv7E.js → linear-DmhiOOKU.js} +1 -1
  76. package/dist/client/assets/mermaid-parser.core-CnJ9Tv8l.js +4 -0
  77. package/dist/client/assets/mermaid.core-R7nXpPx-.js +11 -0
  78. package/dist/client/assets/{mindmap-definition-YRQLILUH-B8jMe7ir.js → mindmap-definition-QFDTVHPH-CwcHocMZ.js} +39 -11
  79. package/dist/client/assets/{ordinal-DIg8h6NI.js → ordinal-k--hYEme.js} +1 -1
  80. package/dist/client/assets/packet-4T2RLAQJ-D2q3-9ae.js +1 -0
  81. package/dist/client/assets/pie-ZZUOXDRM-GivlQcUF.js +1 -0
  82. package/dist/client/assets/pieDiagram-DEJITSTG-BVAn8Lmr.js +30 -0
  83. package/dist/client/assets/preload-helper-DSXbuxSR.js +1 -0
  84. package/dist/client/assets/prism-haskell-BP3SRvzt.js +1 -0
  85. package/dist/client/assets/prism-nix-CO4UPu3E.js +1 -0
  86. package/dist/client/assets/{quadrantDiagram-337W2JSQ-CQ1QKsru.js → quadrantDiagram-34T5L4WZ-C2XZ_zxa.js} +1 -1
  87. package/dist/client/assets/radar-PYXPWWZC-C9pD6VNR.js +1 -0
  88. package/dist/client/assets/reduce-BTlHjXna.js +1 -0
  89. package/dist/client/assets/requirementDiagram-MS252O5E-CfO16pkI.js +84 -0
  90. package/dist/client/assets/{sankeyDiagram-WA2Y5GQK-BQVbT6bS.js → sankeyDiagram-XADWPNL6-D_4_234M.js} +1 -1
  91. package/dist/client/assets/sequenceDiagram-FGHM5R23-B-yHKMuK.js +157 -0
  92. package/dist/client/assets/src-CjDs0_Ij.js +1 -0
  93. package/dist/client/assets/stateDiagram-FHFEXIEX-BeG2di4I.js +1 -0
  94. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-DvcSq7KE.js +1 -0
  95. package/dist/client/assets/timeline-definition-GMOUNBTQ-DhtnMGcE.js +120 -0
  96. package/dist/client/assets/treeView-SZITEDCU-BSNk8_yV.js +1 -0
  97. package/dist/client/assets/treemap-W4RFUUIX-ym4zQztE.js +1 -0
  98. package/dist/client/assets/vennDiagram-DHZGUBPP-CBn69TcQ.js +34 -0
  99. package/dist/client/assets/wardley-RL74JXVD-B02H6ReJ.js +1 -0
  100. package/dist/client/assets/wardleyDiagram-NUSXRM2D-CEoSJmN1.js +20 -0
  101. package/dist/client/assets/{xychartDiagram-JWTSCODW-DeYZhM2j.js → xychartDiagram-5P7HB3ND-BZ_X9tkn.js} +6 -6
  102. package/dist/client/index.html +4 -2
  103. package/dist/server/git-diff-tui.d.ts +2 -2
  104. package/dist/server/git-diff-tui.js +14 -11
  105. package/dist/server/git-diff-tui.test.js +20 -4
  106. package/dist/server/git-diff.d.ts +3 -2
  107. package/dist/server/git-diff.js +30 -7
  108. package/dist/server/git-diff.test.js +60 -5
  109. package/dist/server/server.d.ts +2 -3
  110. package/dist/server/server.js +80 -55
  111. package/dist/server/server.test.js +110 -60
  112. package/dist/tui/App.d.ts +2 -2
  113. package/dist/tui/App.js +4 -3
  114. package/dist/types/diff.d.ts +8 -0
  115. package/dist/utils/diffSelection.d.ts +6 -0
  116. package/dist/utils/diffSelection.js +30 -0
  117. package/package.json +9 -9
  118. package/dist/client/assets/_basePickBy-hOr-yGsE.js +0 -1
  119. package/dist/client/assets/_baseUniq-b7bzdUSn.js +0 -1
  120. package/dist/client/assets/arc-D65wG9gm.js +0 -1
  121. package/dist/client/assets/architecture-PBZL5I3N-DFdrPtRG.js +0 -1
  122. package/dist/client/assets/architectureDiagram-2XIMDMQ5-CXJTJFYJ.js +0 -36
  123. package/dist/client/assets/blockDiagram-WCTKOSBZ-B60owdAn.js +0 -132
  124. package/dist/client/assets/c4Diagram-IC4MRINW-4tg2D_Vt.js +0 -10
  125. package/dist/client/assets/channel-DogeU0Wo.js +0 -1
  126. package/dist/client/assets/chunk-7E7YKBS2-BVR-8Pma.js +0 -1
  127. package/dist/client/assets/chunk-7R4GIKGN-DneC7PwP.js +0 -80
  128. package/dist/client/assets/chunk-C72U2L5F-CJr98gus.js +0 -1
  129. package/dist/client/assets/chunk-EGIJ26TM-iD_CSqpR.js +0 -1
  130. package/dist/client/assets/chunk-GEFDOKGD-eDUrsRgt.js +0 -2
  131. package/dist/client/assets/chunk-GLR3WWYH-NUOKNaxd.js +0 -2
  132. package/dist/client/assets/chunk-HHEYEP7N-DhuxpkmW.js +0 -1
  133. package/dist/client/assets/chunk-KYZI473N-Brv52ZeO.js +0 -53
  134. package/dist/client/assets/chunk-L3YUKLVL-BkBigLhQ.js +0 -1
  135. package/dist/client/assets/chunk-MX3YWQON-DHRoNbgW.js +0 -1
  136. package/dist/client/assets/chunk-O4XLMI2P-Sr33dk8c.js +0 -7
  137. package/dist/client/assets/chunk-OZEHJAEY-3F2ff7sj.js +0 -1
  138. package/dist/client/assets/chunk-QZHKN3VN-DMRW-mur.js +0 -1
  139. package/dist/client/assets/chunk-R5LLSJPH-ChexuO_S.js +0 -1
  140. package/dist/client/assets/chunk-WL4C6EOR-oxNV_hhM.js +0 -189
  141. package/dist/client/assets/chunk-XIRO2GV7-C9gOnffv.js +0 -1
  142. package/dist/client/assets/chunk-XZSTWKYB-C5JJ0TZR.js +0 -94
  143. package/dist/client/assets/chunk-YBOYWFTD-B6kAkNgH.js +0 -1
  144. package/dist/client/assets/classDiagram-VBA2DB6C-DlDUg6JI.js +0 -1
  145. package/dist/client/assets/classDiagram-v2-RAHNMMFH-BxzJfV1S.js +0 -1
  146. package/dist/client/assets/clone-DuY6BQEm.js +0 -1
  147. package/dist/client/assets/cose-bilkent-S5V4N54A-hlDud6Ym.js +0 -1
  148. package/dist/client/assets/cytoscape.esm-B3gzQ1NF.js +0 -321
  149. package/dist/client/assets/dagre-BwDYerGQ.js +0 -1
  150. package/dist/client/assets/dagre-KLK3FWXG-KnkMUlUE.js +0 -4
  151. package/dist/client/assets/diagram-E7M64L7V-DcTCIFUG.js +0 -24
  152. package/dist/client/assets/diagram-IFDJBPK2-COcDQunj.js +0 -43
  153. package/dist/client/assets/diagram-P4PSJMXO-DmgET9pD.js +0 -24
  154. package/dist/client/assets/dist-v55TM3-O.js +0 -1
  155. package/dist/client/assets/flowDiagram-PKNHOUZH-CW-lseYE.js +0 -162
  156. package/dist/client/assets/ganttDiagram-A5KZAMGK-BxLjKRld.js +0 -292
  157. package/dist/client/assets/gitGraph-HDMCJU4V-CjAGJiCH.js +0 -1
  158. package/dist/client/assets/gitGraphDiagram-K3NZZRJ6-DLEDjokx.js +0 -65
  159. package/dist/client/assets/graphlib-WkJoBgka.js +0 -1
  160. package/dist/client/assets/index-CizZxdOT.js +0 -79
  161. package/dist/client/assets/info-3K5VOQVL-CB6KpH1K.js +0 -1
  162. package/dist/client/assets/isArrayLikeObject-icl0H0jo.js +0 -1
  163. package/dist/client/assets/isEmpty-Du8sNmkE.js +0 -1
  164. package/dist/client/assets/ishikawaDiagram-PHBUUO56-zycn1mVK.js +0 -70
  165. package/dist/client/assets/katex-BJrMXEjr.js +0 -261
  166. package/dist/client/assets/line-Cm3ZuldI.js +0 -1
  167. package/dist/client/assets/math-CNhlSIO3.js +0 -1
  168. package/dist/client/assets/mermaid-parser.core-BvMqHn4b.js +0 -4
  169. package/dist/client/assets/mermaid.core-C4SvQTx9.js +0 -11
  170. package/dist/client/assets/packet-RMMSAZCW-CzbC-tXD.js +0 -1
  171. package/dist/client/assets/pie-UPGHQEXC-CmhYIo8p.js +0 -1
  172. package/dist/client/assets/pieDiagram-SKSYHLDU-CGWbtgxq.js +0 -30
  173. package/dist/client/assets/radar-KQ55EAFF-BCa9lsCc.js +0 -1
  174. package/dist/client/assets/requirementDiagram-Z7DCOOCP-Co1LyL5T.js +0 -73
  175. package/dist/client/assets/sequenceDiagram-2WXFIKYE-DGIEkdPm.js +0 -145
  176. package/dist/client/assets/src-DsmFf7gO.js +0 -1
  177. package/dist/client/assets/stateDiagram-RAJIS63D-DgjKbXnG.js +0 -1
  178. package/dist/client/assets/stateDiagram-v2-FVOUBMTO-gPrpjL74.js +0 -1
  179. package/dist/client/assets/timeline-definition-YZTLITO2-Dz2dVWjY.js +0 -61
  180. package/dist/client/assets/treemap-KZPCXAKY-DXiPfAB6.js +0 -1
  181. package/dist/client/assets/vennDiagram-LZ73GAT5-IIH5S1B6.js +0 -34
  182. /package/dist/client/assets/{array-DOVTz2Mq.js → array-BNor45A1.js} +0 -0
  183. /package/dist/client/assets/{defaultLocale-Ck2Xxk-C.js → defaultLocale-DPzUsThw.js} +0 -0
  184. /package/dist/client/assets/{init-Bft5Ffpj.js → init-C0L3woqb.js} +0 -0
  185. /package/dist/client/assets/{path-DfRbCp9y.js → path-sMK4d_s9.js} +0 -0
  186. /package/dist/client/assets/{prism-bash-6uMTC0Q2.js → prism-bash-iQBez6et.js} +0 -0
  187. /package/dist/client/assets/{prism-csharp-Dkc2OSmh.js → prism-csharp-C1RDHXRk.js} +0 -0
  188. /package/dist/client/assets/{prism-dart-iZy_wlz-.js → prism-dart-nIH9vDUM.js} +0 -0
  189. /package/dist/client/assets/{prism-elixir-BIzI9WJK.js → prism-elixir-DUMUOd7H.js} +0 -0
  190. /package/dist/client/assets/{prism-hcl-Bx2FGBKG.js → prism-hcl-C-ZHJGEE.js} +0 -0
  191. /package/dist/client/assets/{prism-java-DBXf7fH0.js → prism-java-scuShSv5.js} +0 -0
  192. /package/dist/client/assets/{prism-markup-templating-DS0ksKLt.js → prism-markup-templating-BFXREXfb.js} +0 -0
  193. /package/dist/client/assets/{prism-perl-BlhPiMfT.js → prism-perl-BBDKnHRR.js} +0 -0
  194. /package/dist/client/assets/{prism-php-DVtOAJsW.js → prism-php-DjIafOi_.js} +0 -0
  195. /package/dist/client/assets/{prism-protobuf-BUsrNVvv.js → prism-protobuf-BE1MoFmZ.js} +0 -0
  196. /package/dist/client/assets/{prism-ruby-Saes64I6.js → prism-ruby-CZ-lrXfL.js} +0 -0
  197. /package/dist/client/assets/{prism-scala-ANOINMog.js → prism-scala-DgnxHuDn.js} +0 -0
  198. /package/dist/client/assets/{prism-solidity-C5Mx5y66.js → prism-solidity-5fSUcW9Y.js} +0 -0
  199. /package/dist/client/assets/{prism-sql-D5pwK0Dp.js → prism-sql-CKkohPI_.js} +0 -0
  200. /package/dist/client/assets/{prism-vim-BSZSu-gX.js → prism-vim-CkRmxTmK.js} +0 -0
  201. /package/dist/client/assets/{rough.esm-KjoEK0it.js → rough.esm-DeLgKbOI.js} +0 -0
@@ -7,8 +7,10 @@
7
7
  <link rel="icon" href="/favicon-white.svg" media="(prefers-color-scheme: dark)" />
8
8
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
9
9
  <title>difit - Git Diff Viewer</title>
10
- <script type="module" crossorigin src="/assets/index-CizZxdOT.js"></script>
11
- <link rel="stylesheet" crossorigin href="/assets/index-Cn4K2uvR.css">
10
+ <script type="module" crossorigin src="/assets/index-RcU838Ah.js"></script>
11
+ <link rel="modulepreload" crossorigin href="/assets/chunk-CFjPhJqf.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/preload-helper-DSXbuxSR.js">
13
+ <link rel="stylesheet" crossorigin href="/assets/index-Cq_APK7Y.css">
12
14
  </head>
13
15
  <body>
14
16
  <div id="root"></div>
@@ -1,2 +1,2 @@
1
- import type { FileDiff } from '../types/diff.js';
2
- export declare function loadGitDiff(targetCommitish: string, baseCommitish: string, repoPath?: string, contextLines?: number): Promise<FileDiff[]>;
1
+ import type { DiffSelection, FileDiff } from '../types/diff.js';
2
+ export declare function loadGitDiff(selection: DiffSelection, repoPath?: string, contextLines?: number): Promise<FileDiff[]>;
@@ -1,12 +1,17 @@
1
1
  import simpleGit from 'simple-git';
2
- import { validateDiffArguments, createCommitRangeString } from '../cli/utils.js';
3
- export async function loadGitDiff(targetCommitish, baseCommitish, repoPath, contextLines) {
2
+ import { validateDiffArguments } from '../cli/utils.js';
3
+ import { getMergeBaseTargetRef, normalizeBaseMode } from '../utils/diffSelection.js';
4
+ export async function loadGitDiff(selection, repoPath, contextLines) {
5
+ const { targetCommitish, baseCommitish } = selection;
4
6
  // Validate arguments
5
7
  const validation = validateDiffArguments(targetCommitish, baseCommitish);
6
8
  if (!validation.valid) {
7
9
  throw new Error(validation.error);
8
10
  }
9
11
  const git = simpleGit(repoPath);
12
+ const effectiveBaseCommitish = normalizeBaseMode(selection.baseMode) === 'merge-base'
13
+ ? (await git.raw(['merge-base', getMergeBaseTargetRef(targetCommitish), baseCommitish])).trim()
14
+ : baseCommitish;
10
15
  let diff;
11
16
  // Handle target special chars (base is always a regular commit)
12
17
  if (targetCommitish === 'working') {
@@ -15,18 +20,15 @@ export async function loadGitDiff(targetCommitish, baseCommitish, repoPath, cont
15
20
  }
16
21
  else if (targetCommitish === 'staged') {
17
22
  // Show staged changes against base commit
18
- diff = await git.diff(['--cached', baseCommitish, '--name-status']);
23
+ diff = await git.diff(['--cached', effectiveBaseCommitish, '--name-status']);
19
24
  }
20
25
  else if (targetCommitish === '.') {
21
26
  // Show all uncommitted changes against base commit
22
- diff = await git.diff([baseCommitish, '--name-status']);
27
+ diff = await git.diff([effectiveBaseCommitish, '--name-status']);
23
28
  }
24
29
  else {
25
30
  // Both are regular commits: standard commit-to-commit comparison
26
- diff = await git.diff([
27
- createCommitRangeString(baseCommitish, targetCommitish),
28
- '--name-status',
29
- ]);
31
+ diff = await git.diff([effectiveBaseCommitish, targetCommitish, '--name-status']);
30
32
  if (!diff.trim()) {
31
33
  // Try without parent (for initial commit)
32
34
  const diffInitial = await git.diff([targetCommitish, '--name-status']);
@@ -55,17 +57,18 @@ export async function loadGitDiff(targetCommitish, baseCommitish, repoPath, cont
55
57
  }
56
58
  else if (targetCommitish === 'staged') {
57
59
  // Show staged changes against base commit
58
- fileDiff = await git.diff(['--cached', baseCommitish, ...contextArgs, '--', path]);
60
+ fileDiff = await git.diff(['--cached', effectiveBaseCommitish, ...contextArgs, '--', path]);
59
61
  }
60
62
  else if (targetCommitish === '.') {
61
63
  // Show all uncommitted changes against base commit
62
- fileDiff = await git.diff([baseCommitish, ...contextArgs, '--', path]);
64
+ fileDiff = await git.diff([effectiveBaseCommitish, ...contextArgs, '--', path]);
63
65
  }
64
66
  else {
65
67
  try {
66
68
  // Both are regular commits: standard commit-to-commit comparison
67
69
  fileDiff = await git.diff([
68
- createCommitRangeString(baseCommitish, targetCommitish),
70
+ effectiveBaseCommitish,
71
+ targetCommitish,
69
72
  ...contextArgs,
70
73
  '--',
71
74
  path,
@@ -1,13 +1,15 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import { loadGitDiff } from './git-diff-tui.js';
3
3
  const mockDiff = vi.hoisted(() => vi.fn());
4
- const mockSimpleGit = vi.hoisted(() => vi.fn(() => ({ diff: mockDiff })));
4
+ const mockRaw = vi.hoisted(() => vi.fn());
5
+ const mockSimpleGit = vi.hoisted(() => vi.fn(() => ({ diff: mockDiff, raw: mockRaw })));
5
6
  vi.mock('simple-git', () => ({
6
7
  default: mockSimpleGit,
7
8
  }));
8
9
  describe('loadGitDiff', () => {
9
10
  beforeEach(() => {
10
11
  mockDiff.mockReset();
12
+ mockRaw.mockReset();
11
13
  mockSimpleGit.mockClear();
12
14
  });
13
15
  it.each([
@@ -36,14 +38,14 @@ describe('loadGitDiff', () => {
36
38
  name: 'commit comparisons',
37
39
  targetCommitish: 'HEAD',
38
40
  baseCommitish: 'HEAD^',
39
- expectedListArgs: ['HEAD^...HEAD', '--name-status'],
40
- expectedFileArgs: ['HEAD^...HEAD', '-U5', '--', 'src/file.ts'],
41
+ expectedListArgs: ['HEAD^', 'HEAD', '--name-status'],
42
+ expectedFileArgs: ['HEAD^', 'HEAD', '-U5', '--', 'src/file.ts'],
41
43
  },
42
44
  ])('passes context lines for $name', async ({ targetCommitish, baseCommitish, expectedListArgs, expectedFileArgs }) => {
43
45
  mockDiff
44
46
  .mockResolvedValueOnce('M\tsrc/file.ts')
45
47
  .mockResolvedValueOnce('@@ -1 +1 @@\n-old line\n+new line\n');
46
- const result = await loadGitDiff(targetCommitish, baseCommitish, '/repo', 5);
48
+ const result = await loadGitDiff({ targetCommitish, baseCommitish }, '/repo', 5);
47
49
  expect(mockSimpleGit).toHaveBeenCalledWith('/repo');
48
50
  expect(mockDiff).toHaveBeenNthCalledWith(1, expectedListArgs);
49
51
  expect(mockDiff).toHaveBeenNthCalledWith(2, expectedFileArgs);
@@ -57,4 +59,18 @@ describe('loadGitDiff', () => {
57
59
  },
58
60
  ]);
59
61
  });
62
+ it('uses merge-base for merge-base selections', async () => {
63
+ mockRaw.mockResolvedValue('mergebase123\n');
64
+ mockDiff
65
+ .mockResolvedValueOnce('M\tsrc/file.ts')
66
+ .mockResolvedValueOnce('@@ -1 +1 @@\n-old line\n+new line\n');
67
+ await loadGitDiff({
68
+ targetCommitish: '.',
69
+ baseCommitish: 'origin/main',
70
+ baseMode: 'merge-base',
71
+ }, '/repo', 5);
72
+ expect(mockRaw).toHaveBeenCalledWith(['merge-base', 'HEAD', 'origin/main']);
73
+ expect(mockDiff).toHaveBeenNthCalledWith(1, ['mergebase123', '--name-status']);
74
+ expect(mockDiff).toHaveBeenNthCalledWith(2, ['mergebase123', '-U5', '--', 'src/file.ts']);
75
+ });
60
76
  });
@@ -1,4 +1,4 @@
1
- import { type DiffResponse } from '../types/diff.js';
1
+ import { type DiffResponse, type DiffSelection } from '../types/diff.js';
2
2
  export declare class GitDiffParser {
3
3
  private git;
4
4
  private repoPath;
@@ -7,7 +7,8 @@ export declare class GitDiffParser {
7
7
  private static readonly GENERATED_HEADER_SCAN_BYTES;
8
8
  constructor(repoPath?: string);
9
9
  private normalizeRepositoryRelativePath;
10
- parseDiff(targetCommitish: string, baseCommitish: string, ignoreWhitespace?: boolean, contextLines?: number): Promise<DiffResponse>;
10
+ private resolveBaseCommitish;
11
+ parseDiff(selection: DiffSelection, ignoreWhitespace?: boolean, contextLines?: number): Promise<DiffResponse>;
11
12
  private parseUnifiedDiff;
12
13
  private decodeGitPath;
13
14
  private extractPathFromLine;
@@ -1,6 +1,7 @@
1
1
  import { simpleGit } from 'simple-git';
2
2
  import { isAbsolute, resolve, sep } from 'path';
3
3
  import { validateDiffArguments, shortHash, createCommitRangeString } from '../cli/utils.js';
4
+ import { getMergeBaseTargetRef, normalizeBaseMode } from '../utils/diffSelection.js';
4
5
  import { isGeneratedFile } from './generated-file-check.js';
5
6
  export class GitDiffParser {
6
7
  git;
@@ -28,15 +29,28 @@ export class GitDiffParser {
28
29
  }
29
30
  return normalizedFilepath;
30
31
  }
31
- async parseDiff(targetCommitish, baseCommitish, ignoreWhitespace = false, contextLines) {
32
+ async resolveBaseCommitish(selection) {
33
+ if (normalizeBaseMode(selection.baseMode) !== 'merge-base') {
34
+ return selection.baseCommitish;
35
+ }
36
+ const targetRef = getMergeBaseTargetRef(selection.targetCommitish);
37
+ const mergeBase = await this.git.raw(['merge-base', targetRef, selection.baseCommitish]);
38
+ return mergeBase.trim();
39
+ }
40
+ async parseDiff(selection, ignoreWhitespace = false, contextLines) {
41
+ const { targetCommitish, baseCommitish } = selection;
42
+ const requestedBaseMode = normalizeBaseMode(selection.baseMode) === 'merge-base' ? 'merge-base' : undefined;
32
43
  try {
33
44
  // Validate arguments
34
45
  const validation = validateDiffArguments(targetCommitish, baseCommitish);
35
46
  if (!validation.valid) {
36
47
  throw new Error(validation.error);
37
48
  }
49
+ const effectiveBaseCommitish = await this.resolveBaseCommitish(selection);
38
50
  let resolvedCommit;
39
51
  let diffArgs;
52
+ let resolvedBaseCommitish = effectiveBaseCommitish;
53
+ let resolvedTargetCommitish = targetCommitish;
40
54
  // Handle target special chars (base is always a regular commit)
41
55
  if (targetCommitish === 'working') {
42
56
  // Show unstaged changes (working vs staged)
@@ -45,22 +59,26 @@ export class GitDiffParser {
45
59
  }
46
60
  else if (targetCommitish === 'staged') {
47
61
  // Show staged changes against base commit
48
- const baseHash = await this.git.revparse([baseCommitish]);
62
+ const baseHash = await this.git.revparse([effectiveBaseCommitish]);
49
63
  resolvedCommit = `${shortHash(baseHash)} vs Staging Area (staged changes)`;
50
- diffArgs = ['--cached', baseCommitish];
64
+ resolvedBaseCommitish = shortHash(baseHash);
65
+ diffArgs = ['--cached', effectiveBaseCommitish];
51
66
  }
52
67
  else if (targetCommitish === '.') {
53
68
  // Show all uncommitted changes against base commit
54
- const baseHash = await this.git.revparse([baseCommitish]);
69
+ const baseHash = await this.git.revparse([effectiveBaseCommitish]);
55
70
  resolvedCommit = `${shortHash(baseHash)} vs Working Directory (all uncommitted changes)`;
56
- diffArgs = [baseCommitish];
71
+ resolvedBaseCommitish = shortHash(baseHash);
72
+ diffArgs = [effectiveBaseCommitish];
57
73
  }
58
74
  else {
59
75
  // Both are regular commits: standard commit-to-commit comparison
60
76
  const targetHash = await this.git.revparse([targetCommitish]);
61
- const baseHash = await this.git.revparse([baseCommitish]);
77
+ const baseHash = await this.git.revparse([effectiveBaseCommitish]);
62
78
  resolvedCommit = createCommitRangeString(shortHash(baseHash), shortHash(targetHash));
63
- diffArgs = [resolvedCommit];
79
+ resolvedBaseCommitish = shortHash(baseHash);
80
+ resolvedTargetCommitish = shortHash(targetHash);
81
+ diffArgs = [baseHash, targetHash];
64
82
  }
65
83
  if (ignoreWhitespace) {
66
84
  diffArgs.push('-w');
@@ -78,6 +96,11 @@ export class GitDiffParser {
78
96
  commit: resolvedCommit,
79
97
  files,
80
98
  isEmpty: files.length === 0,
99
+ baseCommitish: resolvedBaseCommitish,
100
+ targetCommitish: resolvedTargetCommitish,
101
+ requestedBaseCommitish: baseCommitish,
102
+ requestedTargetCommitish: targetCommitish,
103
+ requestedBaseMode,
81
104
  };
82
105
  }
83
106
  catch (error) {
@@ -5,6 +5,7 @@ vi.mock('simple-git', () => ({
5
5
  simpleGit: vi.fn(() => ({
6
6
  revparse: vi.fn(),
7
7
  diff: vi.fn(),
8
+ raw: vi.fn(),
8
9
  })),
9
10
  }));
10
11
  // Mock child_process
@@ -967,7 +968,10 @@ index abc123..def456 100644
967
968
  parser.git.revparse.mockResolvedValue('abc1234567890abcdef1234567890abcdef12');
968
969
  const getBlobContentSpy = vi.spyOn(parser, 'getBlobContent');
969
970
  getBlobContentSpy.mockResolvedValue(Buffer.from('// @generated\nconst x = 1;'));
970
- const response = await parser.parseDiff('HEAD', 'HEAD~1');
971
+ const response = await parser.parseDiff({
972
+ targetCommitish: 'HEAD',
973
+ baseCommitish: 'HEAD~1',
974
+ });
971
975
  expect(response.files[0].path).toBe(file);
972
976
  expect(response.files[0].isGenerated).toBe(false);
973
977
  expect(getBlobContentSpy).not.toHaveBeenCalled();
@@ -1078,9 +1082,13 @@ index abc123..def456 100644
1078
1082
  .mockResolvedValueOnce('1234567890abcdef1234567890abcdef12345678')
1079
1083
  .mockResolvedValueOnce('abcdef1234567890abcdef1234567890abcdef12');
1080
1084
  gitDiff.mockResolvedValue('');
1081
- const response = await parser.parseDiff('HEAD', 'HEAD~1', false, 5);
1085
+ const response = await parser.parseDiff({
1086
+ targetCommitish: 'HEAD',
1087
+ baseCommitish: 'HEAD~1',
1088
+ }, false, 5);
1082
1089
  expect(gitDiff).toHaveBeenCalledWith([
1083
- 'abcdef1...1234567',
1090
+ 'abcdef1234567890abcdef1234567890abcdef12',
1091
+ '1234567890abcdef1234567890abcdef12345678',
1084
1092
  '-U5',
1085
1093
  '--no-ext-diff',
1086
1094
  '--color=never',
@@ -1089,6 +1097,11 @@ index abc123..def456 100644
1089
1097
  commit: 'abcdef1...1234567',
1090
1098
  files: [],
1091
1099
  isEmpty: true,
1100
+ baseCommitish: 'abcdef1',
1101
+ targetCommitish: '1234567',
1102
+ requestedBaseCommitish: 'HEAD~1',
1103
+ requestedTargetCommitish: 'HEAD',
1104
+ requestedBaseMode: undefined,
1092
1105
  });
1093
1106
  });
1094
1107
  it('accepts branch refs with revision suffixes', async () => {
@@ -1098,14 +1111,56 @@ index abc123..def456 100644
1098
1111
  .mockResolvedValueOnce('1234567890abcdef1234567890abcdef12345678')
1099
1112
  .mockResolvedValueOnce('abcdef1234567890abcdef1234567890abcdef12');
1100
1113
  gitDiff.mockResolvedValue('');
1101
- const response = await parser.parseDiff('codex/comment-thread', 'codex/comment-thread^');
1114
+ const response = await parser.parseDiff({
1115
+ targetCommitish: 'codex/comment-thread',
1116
+ baseCommitish: 'codex/comment-thread^',
1117
+ });
1102
1118
  expect(gitRevparse).toHaveBeenNthCalledWith(1, ['codex/comment-thread']);
1103
1119
  expect(gitRevparse).toHaveBeenNthCalledWith(2, ['codex/comment-thread^']);
1104
- expect(gitDiff).toHaveBeenCalledWith(['abcdef1...1234567', '--no-ext-diff', '--color=never']);
1120
+ expect(gitDiff).toHaveBeenCalledWith([
1121
+ 'abcdef1234567890abcdef1234567890abcdef12',
1122
+ '1234567890abcdef1234567890abcdef12345678',
1123
+ '--no-ext-diff',
1124
+ '--color=never',
1125
+ ]);
1105
1126
  expect(response).toEqual({
1106
1127
  commit: 'abcdef1...1234567',
1107
1128
  files: [],
1108
1129
  isEmpty: true,
1130
+ baseCommitish: 'abcdef1',
1131
+ targetCommitish: '1234567',
1132
+ requestedBaseCommitish: 'codex/comment-thread^',
1133
+ requestedTargetCommitish: 'codex/comment-thread',
1134
+ requestedBaseMode: undefined,
1135
+ });
1136
+ });
1137
+ it('uses merge-base when baseMode is merge-base', async () => {
1138
+ const gitDiff = parser.git.diff;
1139
+ const gitRevparse = parser.git.revparse;
1140
+ const gitRaw = parser.git.raw;
1141
+ gitRaw.mockResolvedValue('fedcba9876543210fedcba9876543210fedcba98\n');
1142
+ gitRevparse.mockResolvedValueOnce('fedcba9876543210fedcba9876543210fedcba98');
1143
+ gitDiff.mockResolvedValue('');
1144
+ const response = await parser.parseDiff({
1145
+ targetCommitish: '.',
1146
+ baseCommitish: 'origin/main',
1147
+ baseMode: 'merge-base',
1148
+ });
1149
+ expect(gitRaw).toHaveBeenCalledWith(['merge-base', 'HEAD', 'origin/main']);
1150
+ expect(gitDiff).toHaveBeenCalledWith([
1151
+ 'fedcba9876543210fedcba9876543210fedcba98',
1152
+ '--no-ext-diff',
1153
+ '--color=never',
1154
+ ]);
1155
+ expect(response).toEqual({
1156
+ commit: 'fedcba9 vs Working Directory (all uncommitted changes)',
1157
+ files: [],
1158
+ isEmpty: true,
1159
+ baseCommitish: 'fedcba9',
1160
+ targetCommitish: '.',
1161
+ requestedBaseCommitish: 'origin/main',
1162
+ requestedTargetCommitish: '.',
1163
+ requestedBaseMode: 'merge-base',
1109
1164
  });
1110
1165
  });
1111
1166
  });
@@ -1,9 +1,8 @@
1
1
  import { type Server } from 'http';
2
2
  import { type DiffMode } from '../types/watch.js';
3
- import { type CommentImport } from '@/types/diff.js';
3
+ import { type CommentImport, type DiffSelection } from '@/types/diff.js';
4
4
  interface ServerOptions {
5
- targetCommitish?: string;
6
- baseCommitish?: string;
5
+ selection?: DiffSelection;
7
6
  stdinDiff?: string;
8
7
  preferredPort?: number;
9
8
  host?: string;
@@ -13,23 +13,56 @@ import { resolveEditorOption } from '../utils/editorOptions.js';
13
13
  import { getFileExtension } from '../utils/fileUtils.js';
14
14
  import { FileWatcherService } from './file-watcher.js';
15
15
  import { GitDiffParser } from './git-diff.js';
16
+ import { createDiffSelection, diffSelectionsEqual, getDiffSelectionKey, } from '../utils/diffSelection.js';
16
17
  const GENERATED_STATUS_CACHE_TTL_MS = 60_000;
18
+ const MAX_DIFF_CACHE_ENTRIES = 8;
19
+ function createDiffCacheKey(selection, ignoreWhitespace) {
20
+ return `${getDiffSelectionKey(selection)}\u0000${ignoreWhitespace ? '1' : '0'}`;
21
+ }
22
+ function getCachedDiffResponse(cache, key) {
23
+ const cached = cache.get(key);
24
+ if (!cached) {
25
+ return undefined;
26
+ }
27
+ // Refresh insertion order to keep the most recently used entry.
28
+ cache.delete(key);
29
+ cache.set(key, cached);
30
+ return cached;
31
+ }
32
+ function setCachedDiffResponse(cache, key, value) {
33
+ if (cache.has(key)) {
34
+ cache.delete(key);
35
+ }
36
+ cache.set(key, value);
37
+ while (cache.size > MAX_DIFF_CACHE_ENTRIES) {
38
+ const oldestKey = cache.keys().next().value;
39
+ if (typeof oldestKey !== 'string') {
40
+ break;
41
+ }
42
+ cache.delete(oldestKey);
43
+ }
44
+ }
17
45
  export async function startServer(options) {
18
46
  const app = express();
19
47
  const repositoryPath = resolve(options.repoPath ?? process.cwd());
20
48
  const repositoryId = createHash('sha256').update(repositoryPath).digest('hex');
21
49
  const initialCommentImports = options.commentImports || [];
22
- const initialBaseCommitish = options.baseCommitish ?? '';
23
- const initialTargetCommitish = options.targetCommitish ?? '';
50
+ const initialSelection = options.selection ?? createDiffSelection('', '');
24
51
  const commentImportId = initialCommentImports.length > 0
25
52
  ? createHash('sha256').update(serializeCommentImports(initialCommentImports)).digest('hex')
26
53
  : undefined;
27
54
  const parser = new GitDiffParser(repositoryPath);
28
55
  const fileWatcher = new FileWatcherService();
29
56
  const generatedStatusCache = new Map();
30
- let diffDataCache = null;
31
- let currentIgnoreWhitespace = options.ignoreWhitespace || false;
57
+ const diffDataCache = new Map();
58
+ const initialIgnoreWhitespace = options.ignoreWhitespace || false;
32
59
  const diffMode = normalizeDiffViewMode(options.mode);
60
+ const parseBaseMode = (value) => {
61
+ if (value === 'merge-base') {
62
+ return 'merge-base';
63
+ }
64
+ return undefined;
65
+ };
33
66
  app.use(express.json());
34
67
  app.use(express.text()); // For sendBeacon text/plain requests
35
68
  app.use((_req, res, next) => {
@@ -40,28 +73,29 @@ export async function startServer(options) {
40
73
  });
41
74
  // Skip validation if using stdin diff
42
75
  if (!options.stdinDiff) {
43
- const isValidCommit = await parser.validateCommit(options.targetCommitish ?? '');
76
+ const isValidCommit = await parser.validateCommit(initialSelection.targetCommitish);
44
77
  if (!isValidCommit) {
45
- throw new Error(`Invalid or non-existent commit: ${options.targetCommitish}`);
78
+ throw new Error(`Invalid or non-existent commit: ${initialSelection.targetCommitish}`);
46
79
  }
47
80
  }
48
81
  // Generate initial diff data for isEmpty check
82
+ let initialDiffData;
49
83
  if (options.stdinDiff) {
50
84
  // Parse stdin diff directly
51
- diffDataCache = parser.parseStdinDiff(options.stdinDiff);
85
+ initialDiffData = parser.parseStdinDiff(options.stdinDiff);
52
86
  }
53
87
  else {
54
- diffDataCache = await parser.parseDiff(options.targetCommitish ?? '', options.baseCommitish ?? '', currentIgnoreWhitespace, options.contextLines);
88
+ initialDiffData = await parser.parseDiff(initialSelection, initialIgnoreWhitespace, options.contextLines);
89
+ setCachedDiffResponse(diffDataCache, createDiffCacheKey(initialSelection, initialIgnoreWhitespace), initialDiffData);
55
90
  }
56
91
  // Function to invalidate cache when file changes are detected
57
92
  const invalidateCache = () => {
58
- diffDataCache = null;
93
+ diffDataCache.clear();
59
94
  generatedStatusCache.clear();
60
95
  parser.clearResolvedCommitCache();
61
96
  };
62
97
  // Track current revisions for cache invalidation
63
- let currentBaseCommitish = options.baseCommitish ?? '';
64
- let currentTargetCommitish = options.targetCommitish ?? '';
98
+ let currentSelection = initialSelection;
65
99
  function parseRepositoryRelativePath(filepath) {
66
100
  if (typeof filepath !== 'string' || filepath.length === 0) {
67
101
  return { ok: false, error: 'Invalid file path' };
@@ -79,56 +113,47 @@ export async function startServer(options) {
79
113
  }
80
114
  app.get('/api/diff', async (req, res) => {
81
115
  const ignoreWhitespace = req.query.ignoreWhitespace === 'true';
82
- const requestedBase = req.query.base || options.baseCommitish || '';
83
- const requestedTarget = req.query.target || options.targetCommitish || '';
116
+ const hasBase = typeof req.query.base === 'string';
117
+ const hasTarget = typeof req.query.target === 'string';
118
+ const hasBaseMode = typeof req.query.baseMode === 'string';
119
+ const requestedSelection = createDiffSelection(hasBase ? req.query.base : currentSelection.baseCommitish, hasTarget ? req.query.target : currentSelection.targetCommitish, hasBaseMode
120
+ ? parseBaseMode(req.query.baseMode)
121
+ : hasBase || hasTarget
122
+ ? undefined
123
+ : currentSelection.baseMode);
84
124
  const shouldIncludeCommentImports = initialCommentImports.length > 0 &&
85
- (Boolean(options.stdinDiff) ||
86
- (requestedBase === initialBaseCommitish && requestedTarget === initialTargetCommitish));
87
- // Check if revisions or whitespace setting changed
88
- const revisionsChanged = requestedBase !== currentBaseCommitish || requestedTarget !== currentTargetCommitish;
89
- const whitespaceChanged = ignoreWhitespace !== currentIgnoreWhitespace;
90
- // Regenerate diff data if cache is invalid or settings changed
91
- if (!diffDataCache || ((revisionsChanged || whitespaceChanged) && !options.stdinDiff)) {
92
- currentIgnoreWhitespace = ignoreWhitespace;
93
- currentBaseCommitish = requestedBase;
94
- currentTargetCommitish = requestedTarget;
95
- diffDataCache = await parser.parseDiff(requestedTarget, requestedBase, ignoreWhitespace, options.contextLines);
96
- generatedStatusCache.clear();
97
- }
98
- // Resolve symbolic refs like HEAD/HEAD^ to actual hashes for the UI
99
- let resolvedBase = currentBaseCommitish || 'stdin';
100
- let resolvedTarget = currentTargetCommitish || 'stdin';
101
- if (!options.stdinDiff &&
102
- currentBaseCommitish &&
103
- !['working', 'staged', '.'].includes(currentBaseCommitish)) {
104
- try {
105
- resolvedBase = await parser.resolveCommitish(currentBaseCommitish);
106
- }
107
- catch {
108
- // If resolution fails, keep original value
125
+ (Boolean(options.stdinDiff) || diffSelectionsEqual(requestedSelection, initialSelection));
126
+ currentSelection = requestedSelection;
127
+ let responseDiffData = initialDiffData;
128
+ if (!options.stdinDiff) {
129
+ const cacheKey = createDiffCacheKey(requestedSelection, ignoreWhitespace);
130
+ const cached = getCachedDiffResponse(diffDataCache, cacheKey);
131
+ if (cached) {
132
+ responseDiffData = cached;
109
133
  }
110
- }
111
- if (!options.stdinDiff &&
112
- currentTargetCommitish &&
113
- !['working', 'staged', '.'].includes(currentTargetCommitish)) {
114
- try {
115
- resolvedTarget = await parser.resolveCommitish(currentTargetCommitish);
116
- }
117
- catch {
118
- // If resolution fails, keep original value
134
+ else {
135
+ responseDiffData = await parser.parseDiff(requestedSelection, ignoreWhitespace, options.contextLines);
136
+ setCachedDiffResponse(diffDataCache, cacheKey, responseDiffData);
137
+ generatedStatusCache.clear();
119
138
  }
120
139
  }
121
- const requestedBaseCommitish = currentBaseCommitish || 'stdin';
122
- const requestedTargetCommitish = currentTargetCommitish || 'stdin';
140
+ const baseCommitish = responseDiffData.baseCommitish ?? (options.stdinDiff ? 'stdin' : undefined);
141
+ const targetCommitish = responseDiffData.targetCommitish ?? (options.stdinDiff ? 'stdin' : undefined);
142
+ const requestedBaseCommitish = responseDiffData.requestedBaseCommitish ??
143
+ (requestedSelection.baseCommitish || (options.stdinDiff ? 'stdin' : undefined));
144
+ const requestedTargetCommitish = responseDiffData.requestedTargetCommitish ??
145
+ (requestedSelection.targetCommitish || (options.stdinDiff ? 'stdin' : undefined));
146
+ const requestedBaseMode = responseDiffData.requestedBaseMode ?? requestedSelection.baseMode;
123
147
  res.json({
124
- ...diffDataCache,
148
+ ...responseDiffData,
125
149
  ignoreWhitespace,
126
150
  mode: diffMode,
127
151
  openInEditorAvailable: !options.stdinDiff,
128
- baseCommitish: resolvedBase,
129
- targetCommitish: resolvedTarget,
152
+ baseCommitish,
153
+ targetCommitish,
130
154
  requestedBaseCommitish,
131
155
  requestedTargetCommitish,
156
+ requestedBaseMode,
132
157
  clearComments: options.clearComments,
133
158
  repositoryId,
134
159
  commentImports: shouldIncludeCommentImports ? initialCommentImports : undefined,
@@ -147,7 +172,7 @@ export async function startServer(options) {
147
172
  return;
148
173
  }
149
174
  const normalizedFilepath = filepathResult.path;
150
- const ref = req.query.ref || currentTargetCommitish || 'HEAD';
175
+ const ref = req.query.ref || currentSelection.targetCommitish || 'HEAD';
151
176
  const cacheKey = `${ref}:${normalizedFilepath}`;
152
177
  const now = Date.now();
153
178
  const cached = generatedStatusCache.get(cacheKey);
@@ -179,7 +204,7 @@ export async function startServer(options) {
179
204
  return;
180
205
  }
181
206
  try {
182
- const { branches, commits, originDefaultBranch, resolvedBase, resolvedTarget } = await parser.getRevisionOptions(currentBaseCommitish, currentTargetCommitish);
207
+ const { branches, commits, originDefaultBranch, resolvedBase, resolvedTarget } = await parser.getRevisionOptions(currentSelection.baseCommitish, currentSelection.targetCommitish);
183
208
  const response = {
184
209
  specialOptions: [
185
210
  { value: '.', label: 'All Uncommitted Changes' },
@@ -528,7 +553,7 @@ export async function startServer(options) {
528
553
  }
529
554
  }
530
555
  // Check if diff is empty and skip browser opening
531
- if (diffDataCache?.isEmpty) {
556
+ if (initialDiffData.isEmpty) {
532
557
  // Don't open browser if no differences found
533
558
  }
534
559
  else if (options.openBrowser) {
@@ -539,7 +564,7 @@ export async function startServer(options) {
539
564
  console.warn('Failed to open browser automatically');
540
565
  }
541
566
  }
542
- return { port, url, isEmpty: diffDataCache?.isEmpty || false, server };
567
+ return { port, url, isEmpty: initialDiffData.isEmpty || false, server };
543
568
  }
544
569
  async function startServerWithFallback(app, preferredPort, host) {
545
570
  return new Promise((resolve, reject) => {