gitx.do 0.1.1 → 0.1.3

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 (376) hide show
  1. package/README.md +40 -353
  2. package/dist/do/logger.d.ts +50 -0
  3. package/dist/do/logger.d.ts.map +1 -0
  4. package/dist/do/logger.js +122 -0
  5. package/dist/do/logger.js.map +1 -0
  6. package/dist/{durable-object → do}/schema.d.ts +3 -3
  7. package/dist/do/schema.d.ts.map +1 -0
  8. package/dist/{durable-object → do}/schema.js +4 -3
  9. package/dist/do/schema.js.map +1 -0
  10. package/dist/do/types.d.ts +267 -0
  11. package/dist/do/types.d.ts.map +1 -0
  12. package/dist/do/types.js +62 -0
  13. package/dist/do/types.js.map +1 -0
  14. package/dist/index.d.ts +15 -469
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +39 -481
  17. package/dist/index.js.map +1 -1
  18. package/dist/mcp/auth.d.ts +77 -0
  19. package/dist/mcp/auth.d.ts.map +1 -0
  20. package/dist/mcp/auth.js +278 -0
  21. package/dist/mcp/auth.js.map +1 -0
  22. package/dist/mcp/index.d.ts +13 -0
  23. package/dist/mcp/index.d.ts.map +1 -0
  24. package/dist/mcp/index.js +19 -0
  25. package/dist/mcp/index.js.map +1 -0
  26. package/dist/mcp/server.d.ts +200 -0
  27. package/dist/mcp/server.d.ts.map +1 -0
  28. package/dist/mcp/server.js +275 -0
  29. package/dist/mcp/server.js.map +1 -0
  30. package/dist/mcp/tool-registry.d.ts +47 -0
  31. package/dist/mcp/tool-registry.d.ts.map +1 -0
  32. package/dist/mcp/tool-registry.js +284 -0
  33. package/dist/mcp/tool-registry.js.map +1 -0
  34. package/dist/mcp/tools.d.ts +103 -515
  35. package/dist/mcp/tools.d.ts.map +1 -1
  36. package/dist/mcp/tools.js +676 -3087
  37. package/dist/mcp/tools.js.map +1 -1
  38. package/dist/mcp/types.d.ts +124 -0
  39. package/dist/mcp/types.d.ts.map +1 -0
  40. package/dist/mcp/types.js +9 -0
  41. package/dist/mcp/types.js.map +1 -0
  42. package/package.json +19 -21
  43. package/dist/cli/commands/add.d.ts +0 -176
  44. package/dist/cli/commands/add.d.ts.map +0 -1
  45. package/dist/cli/commands/add.js +0 -979
  46. package/dist/cli/commands/add.js.map +0 -1
  47. package/dist/cli/commands/blame.d.ts +0 -259
  48. package/dist/cli/commands/blame.d.ts.map +0 -1
  49. package/dist/cli/commands/blame.js +0 -609
  50. package/dist/cli/commands/blame.js.map +0 -1
  51. package/dist/cli/commands/branch.d.ts +0 -249
  52. package/dist/cli/commands/branch.d.ts.map +0 -1
  53. package/dist/cli/commands/branch.js +0 -693
  54. package/dist/cli/commands/branch.js.map +0 -1
  55. package/dist/cli/commands/checkout.d.ts +0 -73
  56. package/dist/cli/commands/checkout.d.ts.map +0 -1
  57. package/dist/cli/commands/checkout.js +0 -725
  58. package/dist/cli/commands/checkout.js.map +0 -1
  59. package/dist/cli/commands/commit.d.ts +0 -182
  60. package/dist/cli/commands/commit.d.ts.map +0 -1
  61. package/dist/cli/commands/commit.js +0 -457
  62. package/dist/cli/commands/commit.js.map +0 -1
  63. package/dist/cli/commands/diff.d.ts +0 -464
  64. package/dist/cli/commands/diff.d.ts.map +0 -1
  65. package/dist/cli/commands/diff.js +0 -959
  66. package/dist/cli/commands/diff.js.map +0 -1
  67. package/dist/cli/commands/log.d.ts +0 -239
  68. package/dist/cli/commands/log.d.ts.map +0 -1
  69. package/dist/cli/commands/log.js +0 -535
  70. package/dist/cli/commands/log.js.map +0 -1
  71. package/dist/cli/commands/merge.d.ts +0 -106
  72. package/dist/cli/commands/merge.d.ts.map +0 -1
  73. package/dist/cli/commands/merge.js +0 -852
  74. package/dist/cli/commands/merge.js.map +0 -1
  75. package/dist/cli/commands/review.d.ts +0 -457
  76. package/dist/cli/commands/review.d.ts.map +0 -1
  77. package/dist/cli/commands/review.js +0 -558
  78. package/dist/cli/commands/review.js.map +0 -1
  79. package/dist/cli/commands/stash.d.ts +0 -157
  80. package/dist/cli/commands/stash.d.ts.map +0 -1
  81. package/dist/cli/commands/stash.js +0 -655
  82. package/dist/cli/commands/stash.js.map +0 -1
  83. package/dist/cli/commands/status.d.ts +0 -269
  84. package/dist/cli/commands/status.d.ts.map +0 -1
  85. package/dist/cli/commands/status.js +0 -492
  86. package/dist/cli/commands/status.js.map +0 -1
  87. package/dist/cli/commands/web.d.ts +0 -199
  88. package/dist/cli/commands/web.d.ts.map +0 -1
  89. package/dist/cli/commands/web.js +0 -697
  90. package/dist/cli/commands/web.js.map +0 -1
  91. package/dist/cli/fs-adapter.d.ts +0 -656
  92. package/dist/cli/fs-adapter.d.ts.map +0 -1
  93. package/dist/cli/fs-adapter.js +0 -1177
  94. package/dist/cli/fs-adapter.js.map +0 -1
  95. package/dist/cli/fsx-cli-adapter.d.ts +0 -359
  96. package/dist/cli/fsx-cli-adapter.d.ts.map +0 -1
  97. package/dist/cli/fsx-cli-adapter.js +0 -619
  98. package/dist/cli/fsx-cli-adapter.js.map +0 -1
  99. package/dist/cli/index.d.ts +0 -387
  100. package/dist/cli/index.d.ts.map +0 -1
  101. package/dist/cli/index.js +0 -579
  102. package/dist/cli/index.js.map +0 -1
  103. package/dist/cli/ui/components/DiffView.d.ts +0 -12
  104. package/dist/cli/ui/components/DiffView.d.ts.map +0 -1
  105. package/dist/cli/ui/components/DiffView.js +0 -11
  106. package/dist/cli/ui/components/DiffView.js.map +0 -1
  107. package/dist/cli/ui/components/ErrorDisplay.d.ts +0 -10
  108. package/dist/cli/ui/components/ErrorDisplay.d.ts.map +0 -1
  109. package/dist/cli/ui/components/ErrorDisplay.js +0 -11
  110. package/dist/cli/ui/components/ErrorDisplay.js.map +0 -1
  111. package/dist/cli/ui/components/FuzzySearch.d.ts +0 -15
  112. package/dist/cli/ui/components/FuzzySearch.d.ts.map +0 -1
  113. package/dist/cli/ui/components/FuzzySearch.js +0 -12
  114. package/dist/cli/ui/components/FuzzySearch.js.map +0 -1
  115. package/dist/cli/ui/components/LoadingSpinner.d.ts +0 -10
  116. package/dist/cli/ui/components/LoadingSpinner.d.ts.map +0 -1
  117. package/dist/cli/ui/components/LoadingSpinner.js +0 -10
  118. package/dist/cli/ui/components/LoadingSpinner.js.map +0 -1
  119. package/dist/cli/ui/components/NavigationList.d.ts +0 -14
  120. package/dist/cli/ui/components/NavigationList.d.ts.map +0 -1
  121. package/dist/cli/ui/components/NavigationList.js +0 -11
  122. package/dist/cli/ui/components/NavigationList.js.map +0 -1
  123. package/dist/cli/ui/components/ScrollableContent.d.ts +0 -13
  124. package/dist/cli/ui/components/ScrollableContent.d.ts.map +0 -1
  125. package/dist/cli/ui/components/ScrollableContent.js +0 -11
  126. package/dist/cli/ui/components/ScrollableContent.js.map +0 -1
  127. package/dist/cli/ui/components/index.d.ts +0 -7
  128. package/dist/cli/ui/components/index.d.ts.map +0 -1
  129. package/dist/cli/ui/components/index.js +0 -9
  130. package/dist/cli/ui/components/index.js.map +0 -1
  131. package/dist/cli/ui/terminal-ui.d.ts +0 -85
  132. package/dist/cli/ui/terminal-ui.d.ts.map +0 -1
  133. package/dist/cli/ui/terminal-ui.js +0 -121
  134. package/dist/cli/ui/terminal-ui.js.map +0 -1
  135. package/dist/do/BashModule.d.ts +0 -871
  136. package/dist/do/BashModule.d.ts.map +0 -1
  137. package/dist/do/BashModule.js +0 -1143
  138. package/dist/do/BashModule.js.map +0 -1
  139. package/dist/do/FsModule.d.ts +0 -612
  140. package/dist/do/FsModule.d.ts.map +0 -1
  141. package/dist/do/FsModule.js +0 -1120
  142. package/dist/do/FsModule.js.map +0 -1
  143. package/dist/do/GitModule.d.ts +0 -635
  144. package/dist/do/GitModule.d.ts.map +0 -1
  145. package/dist/do/GitModule.js +0 -784
  146. package/dist/do/GitModule.js.map +0 -1
  147. package/dist/do/GitRepoDO.d.ts +0 -281
  148. package/dist/do/GitRepoDO.d.ts.map +0 -1
  149. package/dist/do/GitRepoDO.js +0 -479
  150. package/dist/do/GitRepoDO.js.map +0 -1
  151. package/dist/do/bash-ast.d.ts +0 -246
  152. package/dist/do/bash-ast.d.ts.map +0 -1
  153. package/dist/do/bash-ast.js +0 -888
  154. package/dist/do/bash-ast.js.map +0 -1
  155. package/dist/do/container-executor.d.ts +0 -491
  156. package/dist/do/container-executor.d.ts.map +0 -1
  157. package/dist/do/container-executor.js +0 -731
  158. package/dist/do/container-executor.js.map +0 -1
  159. package/dist/do/index.d.ts +0 -53
  160. package/dist/do/index.d.ts.map +0 -1
  161. package/dist/do/index.js +0 -91
  162. package/dist/do/index.js.map +0 -1
  163. package/dist/do/tiered-storage.d.ts +0 -403
  164. package/dist/do/tiered-storage.d.ts.map +0 -1
  165. package/dist/do/tiered-storage.js +0 -689
  166. package/dist/do/tiered-storage.js.map +0 -1
  167. package/dist/do/withBash.d.ts +0 -231
  168. package/dist/do/withBash.d.ts.map +0 -1
  169. package/dist/do/withBash.js +0 -244
  170. package/dist/do/withBash.js.map +0 -1
  171. package/dist/do/withFs.d.ts +0 -237
  172. package/dist/do/withFs.d.ts.map +0 -1
  173. package/dist/do/withFs.js +0 -387
  174. package/dist/do/withFs.js.map +0 -1
  175. package/dist/do/withGit.d.ts +0 -180
  176. package/dist/do/withGit.d.ts.map +0 -1
  177. package/dist/do/withGit.js +0 -271
  178. package/dist/do/withGit.js.map +0 -1
  179. package/dist/durable-object/object-store.d.ts +0 -633
  180. package/dist/durable-object/object-store.d.ts.map +0 -1
  181. package/dist/durable-object/object-store.js +0 -1164
  182. package/dist/durable-object/object-store.js.map +0 -1
  183. package/dist/durable-object/schema.d.ts.map +0 -1
  184. package/dist/durable-object/schema.js.map +0 -1
  185. package/dist/durable-object/wal.d.ts +0 -416
  186. package/dist/durable-object/wal.d.ts.map +0 -1
  187. package/dist/durable-object/wal.js +0 -445
  188. package/dist/durable-object/wal.js.map +0 -1
  189. package/dist/mcp/adapter.d.ts +0 -772
  190. package/dist/mcp/adapter.d.ts.map +0 -1
  191. package/dist/mcp/adapter.js +0 -895
  192. package/dist/mcp/adapter.js.map +0 -1
  193. package/dist/mcp/sandbox/miniflare-evaluator.d.ts +0 -22
  194. package/dist/mcp/sandbox/miniflare-evaluator.d.ts.map +0 -1
  195. package/dist/mcp/sandbox/miniflare-evaluator.js +0 -140
  196. package/dist/mcp/sandbox/miniflare-evaluator.js.map +0 -1
  197. package/dist/mcp/sandbox/object-store-proxy.d.ts +0 -32
  198. package/dist/mcp/sandbox/object-store-proxy.d.ts.map +0 -1
  199. package/dist/mcp/sandbox/object-store-proxy.js +0 -30
  200. package/dist/mcp/sandbox/object-store-proxy.js.map +0 -1
  201. package/dist/mcp/sandbox/template.d.ts +0 -17
  202. package/dist/mcp/sandbox/template.d.ts.map +0 -1
  203. package/dist/mcp/sandbox/template.js +0 -71
  204. package/dist/mcp/sandbox/template.js.map +0 -1
  205. package/dist/mcp/sandbox.d.ts +0 -764
  206. package/dist/mcp/sandbox.d.ts.map +0 -1
  207. package/dist/mcp/sandbox.js +0 -1362
  208. package/dist/mcp/sandbox.js.map +0 -1
  209. package/dist/mcp/sdk-adapter.d.ts +0 -835
  210. package/dist/mcp/sdk-adapter.d.ts.map +0 -1
  211. package/dist/mcp/sdk-adapter.js +0 -974
  212. package/dist/mcp/sdk-adapter.js.map +0 -1
  213. package/dist/mcp/tools/do.d.ts +0 -32
  214. package/dist/mcp/tools/do.d.ts.map +0 -1
  215. package/dist/mcp/tools/do.js +0 -117
  216. package/dist/mcp/tools/do.js.map +0 -1
  217. package/dist/ops/blame.d.ts +0 -551
  218. package/dist/ops/blame.d.ts.map +0 -1
  219. package/dist/ops/blame.js +0 -1037
  220. package/dist/ops/blame.js.map +0 -1
  221. package/dist/ops/branch.d.ts +0 -766
  222. package/dist/ops/branch.d.ts.map +0 -1
  223. package/dist/ops/branch.js +0 -950
  224. package/dist/ops/branch.js.map +0 -1
  225. package/dist/ops/commit-traversal.d.ts +0 -349
  226. package/dist/ops/commit-traversal.d.ts.map +0 -1
  227. package/dist/ops/commit-traversal.js +0 -821
  228. package/dist/ops/commit-traversal.js.map +0 -1
  229. package/dist/ops/commit.d.ts +0 -555
  230. package/dist/ops/commit.d.ts.map +0 -1
  231. package/dist/ops/commit.js +0 -826
  232. package/dist/ops/commit.js.map +0 -1
  233. package/dist/ops/merge-base.d.ts +0 -397
  234. package/dist/ops/merge-base.d.ts.map +0 -1
  235. package/dist/ops/merge-base.js +0 -691
  236. package/dist/ops/merge-base.js.map +0 -1
  237. package/dist/ops/merge.d.ts +0 -855
  238. package/dist/ops/merge.d.ts.map +0 -1
  239. package/dist/ops/merge.js +0 -1551
  240. package/dist/ops/merge.js.map +0 -1
  241. package/dist/ops/tag.d.ts +0 -247
  242. package/dist/ops/tag.d.ts.map +0 -1
  243. package/dist/ops/tag.js +0 -649
  244. package/dist/ops/tag.js.map +0 -1
  245. package/dist/ops/tree-builder.d.ts +0 -178
  246. package/dist/ops/tree-builder.d.ts.map +0 -1
  247. package/dist/ops/tree-builder.js +0 -271
  248. package/dist/ops/tree-builder.js.map +0 -1
  249. package/dist/ops/tree-diff.d.ts +0 -291
  250. package/dist/ops/tree-diff.d.ts.map +0 -1
  251. package/dist/ops/tree-diff.js +0 -705
  252. package/dist/ops/tree-diff.js.map +0 -1
  253. package/dist/pack/delta.d.ts +0 -248
  254. package/dist/pack/delta.d.ts.map +0 -1
  255. package/dist/pack/delta.js +0 -740
  256. package/dist/pack/delta.js.map +0 -1
  257. package/dist/pack/format.d.ts +0 -446
  258. package/dist/pack/format.d.ts.map +0 -1
  259. package/dist/pack/format.js +0 -572
  260. package/dist/pack/format.js.map +0 -1
  261. package/dist/pack/full-generation.d.ts +0 -612
  262. package/dist/pack/full-generation.d.ts.map +0 -1
  263. package/dist/pack/full-generation.js +0 -1378
  264. package/dist/pack/full-generation.js.map +0 -1
  265. package/dist/pack/generation.d.ts +0 -441
  266. package/dist/pack/generation.d.ts.map +0 -1
  267. package/dist/pack/generation.js +0 -707
  268. package/dist/pack/generation.js.map +0 -1
  269. package/dist/pack/index.d.ts +0 -502
  270. package/dist/pack/index.d.ts.map +0 -1
  271. package/dist/pack/index.js +0 -833
  272. package/dist/pack/index.js.map +0 -1
  273. package/dist/refs/branch.d.ts +0 -683
  274. package/dist/refs/branch.d.ts.map +0 -1
  275. package/dist/refs/branch.js +0 -881
  276. package/dist/refs/branch.js.map +0 -1
  277. package/dist/refs/storage.d.ts +0 -833
  278. package/dist/refs/storage.d.ts.map +0 -1
  279. package/dist/refs/storage.js +0 -1023
  280. package/dist/refs/storage.js.map +0 -1
  281. package/dist/refs/tag.d.ts +0 -860
  282. package/dist/refs/tag.d.ts.map +0 -1
  283. package/dist/refs/tag.js +0 -996
  284. package/dist/refs/tag.js.map +0 -1
  285. package/dist/storage/backend.d.ts +0 -425
  286. package/dist/storage/backend.d.ts.map +0 -1
  287. package/dist/storage/backend.js +0 -41
  288. package/dist/storage/backend.js.map +0 -1
  289. package/dist/storage/fsx-adapter.d.ts +0 -204
  290. package/dist/storage/fsx-adapter.d.ts.map +0 -1
  291. package/dist/storage/fsx-adapter.js +0 -518
  292. package/dist/storage/fsx-adapter.js.map +0 -1
  293. package/dist/storage/lru-cache.d.ts +0 -691
  294. package/dist/storage/lru-cache.d.ts.map +0 -1
  295. package/dist/storage/lru-cache.js +0 -813
  296. package/dist/storage/lru-cache.js.map +0 -1
  297. package/dist/storage/object-index.d.ts +0 -585
  298. package/dist/storage/object-index.d.ts.map +0 -1
  299. package/dist/storage/object-index.js +0 -532
  300. package/dist/storage/object-index.js.map +0 -1
  301. package/dist/storage/r2-pack.d.ts +0 -1257
  302. package/dist/storage/r2-pack.d.ts.map +0 -1
  303. package/dist/storage/r2-pack.js +0 -1773
  304. package/dist/storage/r2-pack.js.map +0 -1
  305. package/dist/tiered/cdc-pipeline.d.ts +0 -1888
  306. package/dist/tiered/cdc-pipeline.d.ts.map +0 -1
  307. package/dist/tiered/cdc-pipeline.js +0 -1880
  308. package/dist/tiered/cdc-pipeline.js.map +0 -1
  309. package/dist/tiered/migration.d.ts +0 -1104
  310. package/dist/tiered/migration.d.ts.map +0 -1
  311. package/dist/tiered/migration.js +0 -1217
  312. package/dist/tiered/migration.js.map +0 -1
  313. package/dist/tiered/parquet-writer.d.ts +0 -1145
  314. package/dist/tiered/parquet-writer.d.ts.map +0 -1
  315. package/dist/tiered/parquet-writer.js +0 -1183
  316. package/dist/tiered/parquet-writer.js.map +0 -1
  317. package/dist/tiered/read-path.d.ts +0 -835
  318. package/dist/tiered/read-path.d.ts.map +0 -1
  319. package/dist/tiered/read-path.js +0 -487
  320. package/dist/tiered/read-path.js.map +0 -1
  321. package/dist/types/capability.d.ts +0 -1385
  322. package/dist/types/capability.d.ts.map +0 -1
  323. package/dist/types/capability.js +0 -36
  324. package/dist/types/capability.js.map +0 -1
  325. package/dist/types/index.d.ts +0 -13
  326. package/dist/types/index.d.ts.map +0 -1
  327. package/dist/types/index.js +0 -18
  328. package/dist/types/index.js.map +0 -1
  329. package/dist/types/interfaces.d.ts +0 -673
  330. package/dist/types/interfaces.d.ts.map +0 -1
  331. package/dist/types/interfaces.js +0 -26
  332. package/dist/types/interfaces.js.map +0 -1
  333. package/dist/types/objects.d.ts +0 -692
  334. package/dist/types/objects.d.ts.map +0 -1
  335. package/dist/types/objects.js +0 -837
  336. package/dist/types/objects.js.map +0 -1
  337. package/dist/types/storage.d.ts +0 -603
  338. package/dist/types/storage.d.ts.map +0 -1
  339. package/dist/types/storage.js +0 -191
  340. package/dist/types/storage.js.map +0 -1
  341. package/dist/types/worker-loader.d.ts +0 -60
  342. package/dist/types/worker-loader.d.ts.map +0 -1
  343. package/dist/types/worker-loader.js +0 -62
  344. package/dist/types/worker-loader.js.map +0 -1
  345. package/dist/utils/hash.d.ts +0 -198
  346. package/dist/utils/hash.d.ts.map +0 -1
  347. package/dist/utils/hash.js +0 -272
  348. package/dist/utils/hash.js.map +0 -1
  349. package/dist/utils/sha1.d.ts +0 -325
  350. package/dist/utils/sha1.d.ts.map +0 -1
  351. package/dist/utils/sha1.js +0 -635
  352. package/dist/utils/sha1.js.map +0 -1
  353. package/dist/wire/capabilities.d.ts +0 -1044
  354. package/dist/wire/capabilities.d.ts.map +0 -1
  355. package/dist/wire/capabilities.js +0 -941
  356. package/dist/wire/capabilities.js.map +0 -1
  357. package/dist/wire/path-security.d.ts +0 -157
  358. package/dist/wire/path-security.d.ts.map +0 -1
  359. package/dist/wire/path-security.js +0 -307
  360. package/dist/wire/path-security.js.map +0 -1
  361. package/dist/wire/pkt-line.d.ts +0 -345
  362. package/dist/wire/pkt-line.d.ts.map +0 -1
  363. package/dist/wire/pkt-line.js +0 -381
  364. package/dist/wire/pkt-line.js.map +0 -1
  365. package/dist/wire/receive-pack.d.ts +0 -1059
  366. package/dist/wire/receive-pack.d.ts.map +0 -1
  367. package/dist/wire/receive-pack.js +0 -1414
  368. package/dist/wire/receive-pack.js.map +0 -1
  369. package/dist/wire/smart-http.d.ts +0 -799
  370. package/dist/wire/smart-http.d.ts.map +0 -1
  371. package/dist/wire/smart-http.js +0 -945
  372. package/dist/wire/smart-http.js.map +0 -1
  373. package/dist/wire/upload-pack.d.ts +0 -727
  374. package/dist/wire/upload-pack.d.ts.map +0 -1
  375. package/dist/wire/upload-pack.js +0 -1141
  376. package/dist/wire/upload-pack.js.map +0 -1
package/dist/ops/blame.js DELETED
@@ -1,1037 +0,0 @@
1
- /**
2
- * @fileoverview Git Blame Algorithm
3
- *
4
- * This module provides functionality for attributing each line of a file
5
- * to the commit that last modified it. It implements a blame algorithm
6
- * similar to Git's native blame command.
7
- *
8
- * ## Features
9
- *
10
- * - Line-by-line commit attribution
11
- * - Rename tracking across commits
12
- * - Line range filtering
13
- * - Whitespace-insensitive comparison
14
- * - Date range filtering
15
- * - Commit exclusion (ignore revisions)
16
- * - Binary file detection
17
- * - Porcelain and human-readable output formats
18
- *
19
- * ## Usage Example
20
- *
21
- * ```typescript
22
- * import { blame, formatBlame } from './ops/blame'
23
- *
24
- * // Get blame information for a file
25
- * const result = await blame(storage, 'src/main.ts', 'HEAD', {
26
- * followRenames: true,
27
- * ignoreWhitespace: true
28
- * })
29
- *
30
- * // Format for display
31
- * const output = formatBlame(result, { showLineNumbers: true })
32
- * console.log(output)
33
- * ```
34
- *
35
- * @module ops/blame
36
- */
37
- // ============================================================================
38
- // Helper Functions
39
- // ============================================================================
40
- const decoder = new TextDecoder();
41
- /**
42
- * Checks if content is likely binary (contains null bytes).
43
- *
44
- * Uses a heuristic similar to Git's binary detection:
45
- * checks the first 8000 bytes for null characters.
46
- *
47
- * @param data - The content to check
48
- * @returns True if the content appears to be binary
49
- *
50
- * @internal
51
- */
52
- function isBinaryContent(data) {
53
- // Check first 8000 bytes or entire file if smaller
54
- const checkLength = Math.min(data.length, 8000);
55
- for (let i = 0; i < checkLength; i++) {
56
- // Null byte is a strong indicator of binary
57
- if (data[i] === 0)
58
- return true;
59
- }
60
- return false;
61
- }
62
- /**
63
- * Splits content into lines, handling various line ending styles.
64
- *
65
- * Handles both Unix (\n) and Windows (\r\n) line endings,
66
- * normalizing output to not include trailing carriage returns.
67
- *
68
- * @param content - The string content to split
69
- * @returns Array of lines (without line terminators)
70
- *
71
- * @internal
72
- */
73
- function splitLines(content) {
74
- if (content === '')
75
- return [];
76
- // Split by \n but handle \r\n as well
77
- const lines = content.split('\n');
78
- // If there's a trailing newline, the split will create an empty final element
79
- // which we should remove to match expected behavior
80
- if (lines.length > 0 && lines[lines.length - 1] === '') {
81
- lines.pop();
82
- }
83
- return lines.map(line => line.replace(/\r$/, ''));
84
- }
85
- /**
86
- * Normalizes a line for comparison (optionally ignoring whitespace).
87
- *
88
- * @param line - The line to normalize
89
- * @param ignoreWhitespace - Whether to normalize whitespace
90
- * @returns The normalized line
91
- *
92
- * @internal
93
- */
94
- function normalizeLine(line, ignoreWhitespace) {
95
- if (ignoreWhitespace) {
96
- return line.trim().replace(/\s+/g, ' ');
97
- }
98
- return line;
99
- }
100
- /**
101
- * Gets file content at a specific path within a commit.
102
- *
103
- * Handles nested paths by traversing the tree structure.
104
- *
105
- * @param storage - The storage interface
106
- * @param commit - The commit object
107
- * @param path - The file path to retrieve
108
- * @returns The file content, or null if not found
109
- *
110
- * @internal
111
- */
112
- async function getFileAtPath(storage, commit, path) {
113
- // Try the direct storage method first
114
- const directResult = await storage.getFileAtCommit(commit.tree, path);
115
- if (directResult)
116
- return directResult;
117
- // Handle nested paths manually
118
- const parts = path.split('/');
119
- let currentTreeSha = commit.tree;
120
- for (let i = 0; i < parts.length; i++) {
121
- const tree = await storage.getTree(currentTreeSha);
122
- if (!tree)
123
- return null;
124
- const entry = tree.entries.find(e => e.name === parts[i]);
125
- if (!entry)
126
- return null;
127
- if (i === parts.length - 1) {
128
- // Final part - should be a file
129
- return storage.getBlob(entry.sha);
130
- }
131
- else {
132
- // Intermediate part - should be a directory
133
- if (entry.mode !== '040000')
134
- return null;
135
- currentTreeSha = entry.sha;
136
- }
137
- }
138
- return null;
139
- }
140
- /**
141
- * Computes line mapping between two file versions using LCS algorithm.
142
- *
143
- * Returns a mapping of (oldLineIndex -> newLineIndex) for unchanged lines,
144
- * enabling tracking of line movements between versions.
145
- *
146
- * @param oldLines - Lines from the older version
147
- * @param newLines - Lines from the newer version
148
- * @param ignoreWhitespace - Whether to ignore whitespace differences
149
- * @returns Map of old line indices to new line indices
150
- *
151
- * @internal
152
- */
153
- function computeLineMapping(oldLines, newLines, ignoreWhitespace = false) {
154
- // Build a map of unchanged line positions
155
- const mapping = new Map();
156
- // Normalize lines for comparison if needed
157
- const normalizedOld = oldLines.map(l => normalizeLine(l, ignoreWhitespace));
158
- const normalizedNew = newLines.map(l => normalizeLine(l, ignoreWhitespace));
159
- // Use a simple greedy LCS approach for line matching
160
- // Build LCS table
161
- const m = oldLines.length;
162
- const n = newLines.length;
163
- if (m === 0 || n === 0)
164
- return mapping;
165
- // Create LCS table
166
- const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
167
- for (let i = 1; i <= m; i++) {
168
- for (let j = 1; j <= n; j++) {
169
- if (normalizedOld[i - 1] === normalizedNew[j - 1]) {
170
- dp[i][j] = dp[i - 1][j - 1] + 1;
171
- }
172
- else {
173
- dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
174
- }
175
- }
176
- }
177
- // Backtrack to find the matching lines
178
- let i = m, j = n;
179
- while (i > 0 && j > 0) {
180
- if (normalizedOld[i - 1] === normalizedNew[j - 1]) {
181
- mapping.set(i - 1, j - 1); // 0-indexed
182
- i--;
183
- j--;
184
- }
185
- else if (dp[i - 1][j] > dp[i][j - 1]) {
186
- i--;
187
- }
188
- else {
189
- j--;
190
- }
191
- }
192
- return mapping;
193
- }
194
- /**
195
- * Parses a line range specification (git-style -L option).
196
- *
197
- * Supports multiple formats:
198
- * - "start,end": Explicit line range
199
- * - "start,+offset": Relative offset from start
200
- * - "/pattern1/,/pattern2/": Regex-based range
201
- *
202
- * @param lineRange - The range specification string
203
- * @param lines - The file content lines (for pattern matching)
204
- * @returns Object with start and end line numbers (1-indexed)
205
- *
206
- * @internal
207
- */
208
- function parseLineRange(lineRange, lines) {
209
- const totalLines = lines.length;
210
- // Handle regex patterns like /pattern1/,/pattern2/
211
- if (lineRange.startsWith('/')) {
212
- const parts = lineRange.match(/^\/(.+)\/,\/(.+)\/$/);
213
- if (parts) {
214
- const startPattern = new RegExp(parts[1]);
215
- const endPattern = new RegExp(parts[2]);
216
- let start = -1;
217
- let end = -1;
218
- for (let i = 0; i < lines.length; i++) {
219
- if (start === -1 && startPattern.test(lines[i])) {
220
- start = i + 1; // 1-indexed
221
- }
222
- if (start !== -1 && endPattern.test(lines[i])) {
223
- end = i + 1; // 1-indexed
224
- break;
225
- }
226
- }
227
- if (start === -1)
228
- start = 1;
229
- if (end === -1)
230
- end = totalLines;
231
- return { start, end };
232
- }
233
- }
234
- // Handle numeric ranges like "2,4" or "2,+3"
235
- const [startStr, endStr] = lineRange.split(',');
236
- const start = parseInt(startStr, 10);
237
- let end;
238
- if (endStr.startsWith('+')) {
239
- // Relative offset: start + offset lines
240
- end = start + parseInt(endStr.slice(1), 10);
241
- }
242
- else {
243
- end = parseInt(endStr, 10);
244
- }
245
- return { start, end };
246
- }
247
- /**
248
- * Calculates similarity between two strings (0-1).
249
- *
250
- * Uses line-based comparison with the LCS algorithm to determine
251
- * what percentage of lines are shared between the two versions.
252
- *
253
- * @param a - First string
254
- * @param b - Second string
255
- * @returns Similarity score from 0 to 1
256
- *
257
- * @internal
258
- */
259
- function calculateSimilarity(a, b) {
260
- if (a === b)
261
- return 1;
262
- if (a.length === 0 || b.length === 0)
263
- return 0;
264
- const aLines = splitLines(a);
265
- const bLines = splitLines(b);
266
- if (aLines.length === 0 && bLines.length === 0)
267
- return 1;
268
- if (aLines.length === 0 || bLines.length === 0)
269
- return 0;
270
- // Count matching lines
271
- const mapping = computeLineMapping(aLines, bLines, false);
272
- const matchCount = mapping.size;
273
- const maxLines = Math.max(aLines.length, bLines.length);
274
- return matchCount / maxLines;
275
- }
276
- // ============================================================================
277
- // Main Functions
278
- // ============================================================================
279
- /**
280
- * Computes blame for a file at a specific commit.
281
- *
282
- * Traverses commit history to attribute each line of the file to the
283
- * commit that last modified it. Supports various options for filtering
284
- * and tracking behavior.
285
- *
286
- * @description
287
- * The blame algorithm works by:
288
- * 1. Starting at the specified commit and getting the file content
289
- * 2. Initially attributing all lines to the starting commit
290
- * 3. Walking backwards through commit history
291
- * 4. For each parent commit, computing line mappings using LCS
292
- * 5. Re-attributing lines that exist unchanged in the parent
293
- * 6. Continuing until all lines are attributed or history is exhausted
294
- *
295
- * @param storage - The storage interface for accessing Git objects
296
- * @param path - The file path to blame
297
- * @param commit - The commit SHA to start from
298
- * @param options - Optional blame configuration
299
- * @returns The blame result with line attributions
300
- *
301
- * @throws {Error} If the commit is not found
302
- * @throws {Error} If the file is not found at the specified commit
303
- * @throws {Error} If the file is binary
304
- *
305
- * @example
306
- * ```typescript
307
- * // Basic blame
308
- * const result = await blame(storage, 'src/main.ts', 'abc123')
309
- *
310
- * // Blame with options
311
- * const result = await blame(storage, 'README.md', 'HEAD', {
312
- * followRenames: true,
313
- * maxCommits: 500,
314
- * ignoreWhitespace: true
315
- * })
316
- *
317
- * // Blame specific line range
318
- * const result = await blame(storage, 'config.json', 'main', {
319
- * lineRange: '10,20'
320
- * })
321
- * ```
322
- */
323
- export async function blame(storage, path, commit, options) {
324
- const opts = options ?? {};
325
- // Get the commit object
326
- const commitObj = await storage.getCommit(commit);
327
- if (!commitObj) {
328
- throw new Error(`Commit not found: ${commit}`);
329
- }
330
- // Get the file content at this commit
331
- const fileContent = await getFileAtPath(storage, commitObj, path);
332
- if (fileContent === null) {
333
- throw new Error(`File not found: ${path} at commit ${commit}`);
334
- }
335
- // Check for binary file
336
- if (isBinaryContent(fileContent)) {
337
- throw new Error(`Cannot blame binary file: ${path}`);
338
- }
339
- const contentStr = decoder.decode(fileContent);
340
- let lines = splitLines(contentStr);
341
- // Handle empty file
342
- if (lines.length === 0) {
343
- return {
344
- path,
345
- lines: [],
346
- commits: new Map(),
347
- options: opts
348
- };
349
- }
350
- // Parse line range if specified
351
- let startLine = 1;
352
- let endLine = lines.length;
353
- if (opts.lineRange) {
354
- const range = parseLineRange(opts.lineRange, lines);
355
- startLine = range.start;
356
- endLine = range.end;
357
- }
358
- // Initialize blame info for each line (all attributed to current commit initially)
359
- const blameInfo = lines.map((content, idx) => ({
360
- commitSha: commit,
361
- author: commitObj.author.name,
362
- email: commitObj.author.email,
363
- timestamp: commitObj.author.timestamp,
364
- content,
365
- lineNumber: idx + 1,
366
- originalLineNumber: idx + 1,
367
- originalPath: path
368
- }));
369
- // Track which lines still need attribution
370
- const lineNeedsAttribution = new Array(lines.length).fill(true);
371
- // Track the current path (for rename following)
372
- let currentPath = path;
373
- // Track commits for the result
374
- const commitsMap = new Map();
375
- // Add current commit info
376
- commitsMap.set(commit, {
377
- sha: commit,
378
- author: commitObj.author.name,
379
- email: commitObj.author.email,
380
- timestamp: commitObj.author.timestamp,
381
- summary: commitObj.message.split('\n')[0],
382
- boundary: commitObj.parents.length === 0
383
- });
384
- // Walk through commit history
385
- let currentCommit = commit;
386
- let currentLines = lines;
387
- let commitCount = 0;
388
- const maxCommits = opts.maxCommits ?? Infinity;
389
- // Handle the followRenames option
390
- const followRenames = opts.followRenames ?? false;
391
- // For merge commits, we need to explore both parents
392
- const commitQueue = [];
393
- // Initialize with current commit's parents
394
- const currentCommitObj = await storage.getCommit(currentCommit);
395
- if (currentCommitObj && currentCommitObj.parents.length > 0) {
396
- for (const parentSha of currentCommitObj.parents) {
397
- // Identity mapping for first level
398
- const identityMapping = new Map();
399
- for (let i = 0; i < currentLines.length; i++) {
400
- identityMapping.set(i, i);
401
- }
402
- commitQueue.push({
403
- sha: parentSha,
404
- lines: currentLines,
405
- path: currentPath,
406
- lineMapping: identityMapping,
407
- childCommitSha: currentCommit
408
- });
409
- }
410
- }
411
- // Process commit queue (BFS through history)
412
- while (commitQueue.length > 0 && commitCount < maxCommits) {
413
- const item = commitQueue.shift();
414
- const { sha: parentSha, lines: childLines, path: childPath, lineMapping: childToOriginal, childCommitSha } = item;
415
- // Check if this commit should be ignored
416
- if (opts.ignoreRevisions?.includes(parentSha)) {
417
- // Skip this commit but continue to its parents
418
- const parentCommitObj = await storage.getCommit(parentSha);
419
- if (parentCommitObj && parentCommitObj.parents.length > 0) {
420
- for (const grandparentSha of parentCommitObj.parents) {
421
- commitQueue.push({
422
- sha: grandparentSha,
423
- lines: childLines,
424
- path: childPath,
425
- lineMapping: childToOriginal,
426
- childCommitSha: parentSha
427
- });
428
- }
429
- }
430
- continue;
431
- }
432
- commitCount++;
433
- // Check date filters
434
- const parentCommitObj = await storage.getCommit(parentSha);
435
- if (!parentCommitObj)
436
- continue;
437
- if (opts.since && parentCommitObj.author.timestamp * 1000 < opts.since.getTime()) {
438
- continue;
439
- }
440
- if (opts.until && parentCommitObj.author.timestamp * 1000 > opts.until.getTime()) {
441
- continue;
442
- }
443
- // Track path through renames
444
- // Renames are stored in the child commit (the one that did the rename)
445
- // So we check the childCommitSha to find what the file was called in the parent
446
- let pathInParent = childPath;
447
- if (followRenames) {
448
- // Check renames in the child commit (where the rename happened)
449
- const childRenames = await storage.getRenamesInCommit(childCommitSha);
450
- // Find reverse rename: oldPath -> newPath means in parent it was oldPath
451
- for (const [oldPath, newPath] of childRenames) {
452
- if (newPath === childPath) {
453
- pathInParent = oldPath;
454
- break;
455
- }
456
- }
457
- }
458
- // Get file content in parent
459
- const parentContent = await getFileAtPath(storage, parentCommitObj, pathInParent);
460
- // If file doesn't exist in parent, all remaining lines are from the first commit that has them
461
- if (!parentContent) {
462
- continue;
463
- }
464
- const parentContentStr = decoder.decode(parentContent);
465
- const parentLines = splitLines(parentContentStr);
466
- // Compute line mapping between parent and child
467
- const mapping = computeLineMapping(parentLines, childLines, opts.ignoreWhitespace ?? false);
468
- // Add commit info
469
- if (!commitsMap.has(parentSha)) {
470
- commitsMap.set(parentSha, {
471
- sha: parentSha,
472
- author: parentCommitObj.author.name,
473
- email: parentCommitObj.author.email,
474
- timestamp: parentCommitObj.author.timestamp,
475
- summary: parentCommitObj.message.split('\n')[0],
476
- boundary: parentCommitObj.parents.length === 0
477
- });
478
- }
479
- // Update blame for lines that came from parent
480
- // mapping: parentLineIdx -> childLineIdx
481
- for (const [parentIdx, childIdx] of mapping) {
482
- // Convert childIdx to original index
483
- for (const [origIdx, mappedChildIdx] of childToOriginal) {
484
- if (mappedChildIdx === childIdx && lineNeedsAttribution[origIdx]) {
485
- // This line exists in parent - attribute to parent
486
- blameInfo[origIdx].commitSha = parentSha;
487
- blameInfo[origIdx].author = parentCommitObj.author.name;
488
- blameInfo[origIdx].email = parentCommitObj.author.email;
489
- blameInfo[origIdx].timestamp = parentCommitObj.author.timestamp;
490
- blameInfo[origIdx].originalLineNumber = parentIdx + 1;
491
- if (pathInParent !== childPath) {
492
- blameInfo[origIdx].originalPath = pathInParent;
493
- }
494
- }
495
- }
496
- }
497
- // Build new mapping from original indices to parent indices
498
- const newMapping = new Map();
499
- for (const [origIdx, childIdx] of childToOriginal) {
500
- // Find if this child line maps to a parent line
501
- for (const [parentIdx, mappedChildIdx] of mapping) {
502
- if (mappedChildIdx === childIdx) {
503
- newMapping.set(origIdx, parentIdx);
504
- break;
505
- }
506
- }
507
- }
508
- // Add parent's parents to queue if there are still lines to attribute
509
- if (parentCommitObj.parents.length > 0 && newMapping.size > 0) {
510
- for (const grandparentSha of parentCommitObj.parents) {
511
- commitQueue.push({
512
- sha: grandparentSha,
513
- lines: parentLines,
514
- path: pathInParent,
515
- lineMapping: newMapping,
516
- childCommitSha: parentSha
517
- });
518
- }
519
- }
520
- }
521
- // Filter to requested line range
522
- let resultLines = blameInfo;
523
- if (opts.lineRange) {
524
- resultLines = blameInfo.filter(l => l.lineNumber >= startLine && l.lineNumber <= endLine);
525
- }
526
- return {
527
- path,
528
- lines: resultLines,
529
- commits: commitsMap,
530
- options: opts
531
- };
532
- }
533
- /**
534
- * Alias for blame - get full file blame.
535
- *
536
- * This function is identical to `blame` and exists for API compatibility.
537
- *
538
- * @param storage - The storage interface
539
- * @param path - The file path to blame
540
- * @param commit - The commit SHA to start from
541
- * @param options - Optional blame configuration
542
- * @returns The blame result
543
- *
544
- * @see {@link blame} for full documentation
545
- */
546
- export async function blameFile(storage, path, commit, options) {
547
- return blame(storage, path, commit, options);
548
- }
549
- /**
550
- * Gets blame information for a specific line.
551
- *
552
- * Convenience function that performs a full blame and extracts
553
- * the information for a single line.
554
- *
555
- * @param storage - The storage interface
556
- * @param path - The file path
557
- * @param lineNumber - The line number (1-indexed)
558
- * @param commit - The commit SHA
559
- * @param options - Optional blame configuration
560
- * @returns Blame information for the specified line
561
- *
562
- * @throws {Error} If lineNumber is less than 1
563
- * @throws {Error} If lineNumber exceeds file length
564
- *
565
- * @example
566
- * ```typescript
567
- * const lineInfo = await blameLine(storage, 'src/main.ts', 42, 'HEAD')
568
- * console.log(`Line 42 was last modified by ${lineInfo.author}`)
569
- * ```
570
- */
571
- export async function blameLine(storage, path, lineNumber, commit, options) {
572
- if (lineNumber < 1) {
573
- throw new Error(`Invalid line number: ${lineNumber}. Line numbers start at 1.`);
574
- }
575
- const result = await blame(storage, path, commit, options);
576
- if (lineNumber > result.lines.length) {
577
- throw new Error(`Invalid line number: ${lineNumber}. File has ${result.lines.length} lines.`);
578
- }
579
- return result.lines[lineNumber - 1];
580
- }
581
- /**
582
- * Gets blame for a specific line range.
583
- *
584
- * More efficient than using the lineRange option when you know
585
- * the exact numeric range you want.
586
- *
587
- * @param storage - The storage interface
588
- * @param path - The file path
589
- * @param startLine - Starting line number (1-indexed, inclusive)
590
- * @param endLine - Ending line number (1-indexed, inclusive)
591
- * @param commit - The commit SHA
592
- * @param options - Optional blame configuration
593
- * @returns Blame result for the specified range
594
- *
595
- * @throws {Error} If startLine is less than 1
596
- * @throws {Error} If endLine is less than startLine
597
- * @throws {Error} If endLine exceeds file length
598
- *
599
- * @example
600
- * ```typescript
601
- * // Get blame for lines 10-20
602
- * const result = await blameRange(storage, 'file.ts', 10, 20, 'HEAD')
603
- * ```
604
- */
605
- export async function blameRange(storage, path, startLine, endLine, commit, options) {
606
- if (startLine < 1) {
607
- throw new Error(`Invalid start line: ${startLine}. Line numbers start at 1.`);
608
- }
609
- if (endLine < startLine) {
610
- throw new Error(`Invalid range: end (${endLine}) is before start (${startLine}).`);
611
- }
612
- const fullResult = await blame(storage, path, commit, options);
613
- if (endLine > fullResult.lines.length) {
614
- throw new Error(`Invalid end line: ${endLine}. File has ${fullResult.lines.length} lines.`);
615
- }
616
- return {
617
- path: fullResult.path,
618
- lines: fullResult.lines.slice(startLine - 1, endLine),
619
- commits: fullResult.commits,
620
- options: fullResult.options
621
- };
622
- }
623
- /**
624
- * Gets blame at a specific historical commit.
625
- *
626
- * Alias for `blame` - provided for semantic clarity when you want
627
- * to emphasize you're looking at a specific point in history.
628
- *
629
- * @param storage - The storage interface
630
- * @param path - The file path
631
- * @param commit - The commit SHA
632
- * @param options - Optional blame configuration
633
- * @returns The blame result
634
- *
635
- * @see {@link blame} for full documentation
636
- */
637
- export async function getBlameForCommit(storage, path, commit, options) {
638
- return blame(storage, path, commit, options);
639
- }
640
- /**
641
- * Tracks file path across renames through history.
642
- *
643
- * Walks through commit history and records each path the file
644
- * had at different points in time.
645
- *
646
- * @param storage - The storage interface
647
- * @param path - Current file path
648
- * @param commit - Starting commit SHA
649
- * @param _options - Unused options parameter (reserved for future use)
650
- * @returns Array of path history entries, newest first
651
- *
652
- * @example
653
- * ```typescript
654
- * const history = await trackContentAcrossRenames(storage, 'src/new-name.ts', 'HEAD')
655
- * // history might contain:
656
- * // [
657
- * // { commit: 'abc123', path: 'src/new-name.ts' },
658
- * // { commit: 'def456', path: 'src/old-name.ts' }
659
- * // ]
660
- * ```
661
- */
662
- export async function trackContentAcrossRenames(storage, path, commit, _options) {
663
- const history = [];
664
- let currentPath = path;
665
- let currentCommitSha = commit;
666
- while (currentCommitSha) {
667
- history.push({ commit: currentCommitSha, path: currentPath });
668
- const commitObj = await storage.getCommit(currentCommitSha);
669
- if (!commitObj || commitObj.parents.length === 0)
670
- break;
671
- // Check for renames in this commit
672
- const renames = await storage.getRenamesInCommit(currentCommitSha);
673
- // Find if our current path was renamed from something
674
- for (const [oldPath, newPath] of renames) {
675
- if (newPath === currentPath) {
676
- currentPath = oldPath;
677
- break;
678
- }
679
- }
680
- currentCommitSha = commitObj.parents[0];
681
- }
682
- return history;
683
- }
684
- /**
685
- * Detects file renames between two commits.
686
- *
687
- * Compares two commits to find files that were renamed based on
688
- * SHA matching (exact renames) and content similarity (renames with modifications).
689
- *
690
- * @param storage - The storage interface
691
- * @param fromCommit - The older commit SHA
692
- * @param toCommit - The newer commit SHA
693
- * @param options - Configuration options
694
- * @param options.threshold - Similarity threshold (0-1) for content-based detection
695
- * @returns Map of old paths to new paths for detected renames
696
- *
697
- * @example
698
- * ```typescript
699
- * const renames = await detectRenames(storage, 'abc123', 'def456', {
700
- * threshold: 0.5
701
- * })
702
- *
703
- * for (const [oldPath, newPath] of renames) {
704
- * console.log(`${oldPath} -> ${newPath}`)
705
- * }
706
- * ```
707
- */
708
- export async function detectRenames(storage, fromCommit, toCommit, options) {
709
- const threshold = options?.threshold ?? 0.5;
710
- const renames = new Map();
711
- const fromCommitObj = await storage.getCommit(fromCommit);
712
- const toCommitObj = await storage.getCommit(toCommit);
713
- if (!fromCommitObj || !toCommitObj)
714
- return renames;
715
- const fromTree = await storage.getTree(fromCommitObj.tree);
716
- const toTree = await storage.getTree(toCommitObj.tree);
717
- if (!fromTree || !toTree)
718
- return renames;
719
- // Find files that were deleted in 'from' and added in 'to'
720
- const fromFiles = new Map(); // name -> sha
721
- const toFiles = new Map();
722
- for (const entry of fromTree.entries) {
723
- if (entry.mode !== '040000') { // Skip directories
724
- fromFiles.set(entry.name, entry.sha);
725
- }
726
- }
727
- for (const entry of toTree.entries) {
728
- if (entry.mode !== '040000') {
729
- toFiles.set(entry.name, entry.sha);
730
- }
731
- }
732
- // Find deleted files (in from but not in to)
733
- const deletedFiles = [];
734
- for (const name of fromFiles.keys()) {
735
- if (!toFiles.has(name)) {
736
- deletedFiles.push(name);
737
- }
738
- }
739
- // Find added files (in to but not in from)
740
- const addedFiles = [];
741
- for (const name of toFiles.keys()) {
742
- if (!fromFiles.has(name)) {
743
- addedFiles.push(name);
744
- }
745
- }
746
- // Check for exact SHA matches (pure renames)
747
- for (const deleted of deletedFiles) {
748
- const deletedSha = fromFiles.get(deleted);
749
- for (const added of addedFiles) {
750
- const addedSha = toFiles.get(added);
751
- if (deletedSha === addedSha) {
752
- renames.set(deleted, added);
753
- break;
754
- }
755
- }
756
- }
757
- // Check for content similarity (renames with modifications)
758
- for (const deleted of deletedFiles) {
759
- if (renames.has(deleted))
760
- continue;
761
- const deletedSha = fromFiles.get(deleted);
762
- const deletedContent = await storage.getBlob(deletedSha);
763
- if (!deletedContent || isBinaryContent(deletedContent))
764
- continue;
765
- const deletedStr = decoder.decode(deletedContent);
766
- for (const added of addedFiles) {
767
- // Check if already matched
768
- let alreadyMatched = false;
769
- for (const [, v] of renames) {
770
- if (v === added) {
771
- alreadyMatched = true;
772
- break;
773
- }
774
- }
775
- if (alreadyMatched)
776
- continue;
777
- const addedSha = toFiles.get(added);
778
- const addedContent = await storage.getBlob(addedSha);
779
- if (!addedContent || isBinaryContent(addedContent))
780
- continue;
781
- const addedStr = decoder.decode(addedContent);
782
- const similarity = calculateSimilarity(deletedStr, addedStr);
783
- if (similarity >= threshold) {
784
- renames.set(deleted, added);
785
- break;
786
- }
787
- }
788
- }
789
- return renames;
790
- }
791
- /**
792
- * Builds complete blame history for a specific line.
793
- *
794
- * Tracks a single line through history, recording its content
795
- * at each commit where it existed.
796
- *
797
- * @param storage - The storage interface
798
- * @param path - The file path
799
- * @param lineNumber - The line number to track (1-indexed)
800
- * @param commit - Starting commit SHA
801
- * @param options - Optional blame configuration
802
- * @returns Array of history entries, newest first
803
- *
804
- * @example
805
- * ```typescript
806
- * const history = await buildBlameHistory(storage, 'main.ts', 10, 'HEAD')
807
- *
808
- * for (const entry of history) {
809
- * console.log(`${entry.commitSha}: ${entry.content}`)
810
- * }
811
- * ```
812
- */
813
- export async function buildBlameHistory(storage, path, lineNumber, commit, options) {
814
- const history = [];
815
- let currentCommitSha = commit;
816
- let currentPath = path;
817
- let currentLineNumber = lineNumber;
818
- while (currentCommitSha) {
819
- const commitObj = await storage.getCommit(currentCommitSha);
820
- if (!commitObj)
821
- break;
822
- const fileContent = await getFileAtPath(storage, commitObj, currentPath);
823
- if (!fileContent)
824
- break;
825
- const contentStr = decoder.decode(fileContent);
826
- const lines = splitLines(contentStr);
827
- if (currentLineNumber > lines.length || currentLineNumber < 1)
828
- break;
829
- history.push({
830
- commitSha: currentCommitSha,
831
- content: lines[currentLineNumber - 1],
832
- lineNumber: currentLineNumber,
833
- author: commitObj.author.name,
834
- timestamp: commitObj.author.timestamp
835
- });
836
- // Move to parent
837
- if (commitObj.parents.length === 0)
838
- break;
839
- const parentSha = commitObj.parents[0];
840
- const parentCommitObj = await storage.getCommit(parentSha);
841
- if (!parentCommitObj)
842
- break;
843
- // Check for renames
844
- const renames = await storage.getRenamesInCommit(currentCommitSha);
845
- for (const [oldPath, newPath] of renames) {
846
- if (newPath === currentPath) {
847
- currentPath = oldPath;
848
- break;
849
- }
850
- }
851
- // Get parent content and find corresponding line
852
- const parentContent = await getFileAtPath(storage, parentCommitObj, currentPath);
853
- if (!parentContent)
854
- break;
855
- const parentContentStr = decoder.decode(parentContent);
856
- const parentLines = splitLines(parentContentStr);
857
- // Find which line in parent corresponds to our current line
858
- const mapping = computeLineMapping(parentLines, lines, options?.ignoreWhitespace ?? false);
859
- let foundParentLine = false;
860
- for (const [parentIdx, childIdx] of mapping) {
861
- if (childIdx === currentLineNumber - 1) {
862
- currentLineNumber = parentIdx + 1;
863
- foundParentLine = true;
864
- break;
865
- }
866
- }
867
- // If we didn't find a content match but the parent has the line at the same position,
868
- // assume it's the same line (content was modified). This is important for tracking
869
- // history of lines that change content in every commit.
870
- if (!foundParentLine) {
871
- if (currentLineNumber <= parentLines.length) {
872
- // Line exists at same position in parent - assume it's the same logical line
873
- foundParentLine = true;
874
- // currentLineNumber stays the same
875
- }
876
- else {
877
- break;
878
- }
879
- }
880
- currentCommitSha = parentSha;
881
- }
882
- return history;
883
- }
884
- /**
885
- * Formats blame result for display.
886
- *
887
- * Converts a BlameResult into a human-readable or machine-parseable string format.
888
- *
889
- * @param result - The blame result to format
890
- * @param options - Formatting options
891
- * @returns Formatted string output
892
- *
893
- * @example
894
- * ```typescript
895
- * const result = await blame(storage, 'main.ts', 'HEAD')
896
- *
897
- * // Human-readable format
898
- * const output = formatBlame(result, {
899
- * showLineNumbers: true,
900
- * showDate: true
901
- * })
902
- *
903
- * // Machine-readable format
904
- * const porcelain = formatBlame(result, { format: 'porcelain' })
905
- * ```
906
- */
907
- export function formatBlame(result, options) {
908
- const opts = options ?? {};
909
- const lines = [];
910
- if (opts.format === 'porcelain') {
911
- // Porcelain format - machine readable
912
- for (const line of result.lines) {
913
- const commitInfo = result.commits.get(line.commitSha);
914
- lines.push(`${line.commitSha} ${line.originalLineNumber} ${line.lineNumber} 1`);
915
- lines.push(`author ${line.author}`);
916
- lines.push(`author-mail <${line.email || commitInfo?.email || ''}>`);
917
- lines.push(`author-time ${line.timestamp}`);
918
- lines.push(`author-tz +0000`);
919
- lines.push(`committer ${line.author}`);
920
- lines.push(`committer-mail <${line.email || commitInfo?.email || ''}>`);
921
- lines.push(`committer-time ${line.timestamp}`);
922
- lines.push(`committer-tz +0000`);
923
- lines.push(`filename ${result.path}`);
924
- lines.push(`\t${line.content}`);
925
- }
926
- }
927
- else {
928
- // Default format - human readable
929
- for (const line of result.lines) {
930
- const sha = line.commitSha.substring(0, 8);
931
- const author = line.author.padEnd(15).substring(0, 15);
932
- let datePart = '';
933
- if (opts.showDate) {
934
- const date = new Date(line.timestamp * 1000);
935
- datePart = ` ${date.toISOString().substring(0, 10)}`;
936
- }
937
- let authorPart = author;
938
- if (opts.showEmail) {
939
- const email = line.email || result.commits.get(line.commitSha)?.email || '';
940
- authorPart = email.padEnd(25).substring(0, 25);
941
- }
942
- let lineNumPart = '';
943
- if (opts.showLineNumbers) {
944
- lineNumPart = `${line.lineNumber}) `;
945
- }
946
- lines.push(`${sha} (${authorPart}${datePart} ${lineNumPart}${line.content}`);
947
- }
948
- }
949
- return lines.join('\n');
950
- }
951
- /**
952
- * Parses porcelain blame output back into a BlameResult.
953
- *
954
- * Useful for consuming blame output from external sources or
955
- * for round-trip serialization.
956
- *
957
- * @param output - Porcelain format blame output string
958
- * @returns Parsed blame result
959
- *
960
- * @example
961
- * ```typescript
962
- * const porcelainOutput = formatBlame(result, { format: 'porcelain' })
963
- * const parsed = parseBlameOutput(porcelainOutput)
964
- * ```
965
- */
966
- export function parseBlameOutput(output) {
967
- const lines = [];
968
- const commits = new Map();
969
- const outputLines = output.split('\n');
970
- let i = 0;
971
- while (i < outputLines.length) {
972
- const headerLine = outputLines[i];
973
- if (!headerLine || headerLine.trim() === '') {
974
- i++;
975
- continue;
976
- }
977
- // Parse header: <sha> <orig-line> <final-line> <num-lines>
978
- // Accept any 40-char alphanumeric SHA (to support test fixtures using makeSha)
979
- const headerMatch = headerLine.match(/^([0-9a-zA-Z]{40}) (\d+) (\d+)/);
980
- if (!headerMatch) {
981
- i++;
982
- continue;
983
- }
984
- const commitSha = headerMatch[1];
985
- const originalLineNumber = parseInt(headerMatch[2], 10);
986
- const lineNumber = parseInt(headerMatch[3], 10);
987
- // Parse metadata lines until we hit the content line (starts with tab)
988
- let author = '';
989
- let email = '';
990
- let timestamp = 0;
991
- let content = '';
992
- i++;
993
- while (i < outputLines.length) {
994
- const metaLine = outputLines[i];
995
- if (metaLine.startsWith('\t')) {
996
- content = metaLine.substring(1);
997
- i++;
998
- break;
999
- }
1000
- if (metaLine.startsWith('author ')) {
1001
- author = metaLine.substring(7);
1002
- }
1003
- else if (metaLine.startsWith('author-mail ')) {
1004
- email = metaLine.substring(12).replace(/[<>]/g, '');
1005
- }
1006
- else if (metaLine.startsWith('author-time ')) {
1007
- timestamp = parseInt(metaLine.substring(12), 10);
1008
- }
1009
- i++;
1010
- }
1011
- lines.push({
1012
- commitSha,
1013
- author,
1014
- email,
1015
- timestamp,
1016
- content,
1017
- lineNumber,
1018
- originalLineNumber
1019
- });
1020
- // Add commit info if not already present
1021
- if (!commits.has(commitSha)) {
1022
- commits.set(commitSha, {
1023
- sha: commitSha,
1024
- author,
1025
- email,
1026
- timestamp,
1027
- summary: ''
1028
- });
1029
- }
1030
- }
1031
- return {
1032
- path: '',
1033
- lines,
1034
- commits
1035
- };
1036
- }
1037
- //# sourceMappingURL=blame.js.map