gitx.do 0.1.1 → 0.1.2

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 (356) 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 +14 -469
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +31 -483
  17. package/dist/index.js.map +1 -1
  18. package/package.json +13 -21
  19. package/dist/cli/commands/add.d.ts +0 -176
  20. package/dist/cli/commands/add.d.ts.map +0 -1
  21. package/dist/cli/commands/add.js +0 -979
  22. package/dist/cli/commands/add.js.map +0 -1
  23. package/dist/cli/commands/blame.d.ts +0 -259
  24. package/dist/cli/commands/blame.d.ts.map +0 -1
  25. package/dist/cli/commands/blame.js +0 -609
  26. package/dist/cli/commands/blame.js.map +0 -1
  27. package/dist/cli/commands/branch.d.ts +0 -249
  28. package/dist/cli/commands/branch.d.ts.map +0 -1
  29. package/dist/cli/commands/branch.js +0 -693
  30. package/dist/cli/commands/branch.js.map +0 -1
  31. package/dist/cli/commands/checkout.d.ts +0 -73
  32. package/dist/cli/commands/checkout.d.ts.map +0 -1
  33. package/dist/cli/commands/checkout.js +0 -725
  34. package/dist/cli/commands/checkout.js.map +0 -1
  35. package/dist/cli/commands/commit.d.ts +0 -182
  36. package/dist/cli/commands/commit.d.ts.map +0 -1
  37. package/dist/cli/commands/commit.js +0 -457
  38. package/dist/cli/commands/commit.js.map +0 -1
  39. package/dist/cli/commands/diff.d.ts +0 -464
  40. package/dist/cli/commands/diff.d.ts.map +0 -1
  41. package/dist/cli/commands/diff.js +0 -959
  42. package/dist/cli/commands/diff.js.map +0 -1
  43. package/dist/cli/commands/log.d.ts +0 -239
  44. package/dist/cli/commands/log.d.ts.map +0 -1
  45. package/dist/cli/commands/log.js +0 -535
  46. package/dist/cli/commands/log.js.map +0 -1
  47. package/dist/cli/commands/merge.d.ts +0 -106
  48. package/dist/cli/commands/merge.d.ts.map +0 -1
  49. package/dist/cli/commands/merge.js +0 -852
  50. package/dist/cli/commands/merge.js.map +0 -1
  51. package/dist/cli/commands/review.d.ts +0 -457
  52. package/dist/cli/commands/review.d.ts.map +0 -1
  53. package/dist/cli/commands/review.js +0 -558
  54. package/dist/cli/commands/review.js.map +0 -1
  55. package/dist/cli/commands/stash.d.ts +0 -157
  56. package/dist/cli/commands/stash.d.ts.map +0 -1
  57. package/dist/cli/commands/stash.js +0 -655
  58. package/dist/cli/commands/stash.js.map +0 -1
  59. package/dist/cli/commands/status.d.ts +0 -269
  60. package/dist/cli/commands/status.d.ts.map +0 -1
  61. package/dist/cli/commands/status.js +0 -492
  62. package/dist/cli/commands/status.js.map +0 -1
  63. package/dist/cli/commands/web.d.ts +0 -199
  64. package/dist/cli/commands/web.d.ts.map +0 -1
  65. package/dist/cli/commands/web.js +0 -697
  66. package/dist/cli/commands/web.js.map +0 -1
  67. package/dist/cli/fs-adapter.d.ts +0 -656
  68. package/dist/cli/fs-adapter.d.ts.map +0 -1
  69. package/dist/cli/fs-adapter.js +0 -1177
  70. package/dist/cli/fs-adapter.js.map +0 -1
  71. package/dist/cli/fsx-cli-adapter.d.ts +0 -359
  72. package/dist/cli/fsx-cli-adapter.d.ts.map +0 -1
  73. package/dist/cli/fsx-cli-adapter.js +0 -619
  74. package/dist/cli/fsx-cli-adapter.js.map +0 -1
  75. package/dist/cli/index.d.ts +0 -387
  76. package/dist/cli/index.d.ts.map +0 -1
  77. package/dist/cli/index.js +0 -579
  78. package/dist/cli/index.js.map +0 -1
  79. package/dist/cli/ui/components/DiffView.d.ts +0 -12
  80. package/dist/cli/ui/components/DiffView.d.ts.map +0 -1
  81. package/dist/cli/ui/components/DiffView.js +0 -11
  82. package/dist/cli/ui/components/DiffView.js.map +0 -1
  83. package/dist/cli/ui/components/ErrorDisplay.d.ts +0 -10
  84. package/dist/cli/ui/components/ErrorDisplay.d.ts.map +0 -1
  85. package/dist/cli/ui/components/ErrorDisplay.js +0 -11
  86. package/dist/cli/ui/components/ErrorDisplay.js.map +0 -1
  87. package/dist/cli/ui/components/FuzzySearch.d.ts +0 -15
  88. package/dist/cli/ui/components/FuzzySearch.d.ts.map +0 -1
  89. package/dist/cli/ui/components/FuzzySearch.js +0 -12
  90. package/dist/cli/ui/components/FuzzySearch.js.map +0 -1
  91. package/dist/cli/ui/components/LoadingSpinner.d.ts +0 -10
  92. package/dist/cli/ui/components/LoadingSpinner.d.ts.map +0 -1
  93. package/dist/cli/ui/components/LoadingSpinner.js +0 -10
  94. package/dist/cli/ui/components/LoadingSpinner.js.map +0 -1
  95. package/dist/cli/ui/components/NavigationList.d.ts +0 -14
  96. package/dist/cli/ui/components/NavigationList.d.ts.map +0 -1
  97. package/dist/cli/ui/components/NavigationList.js +0 -11
  98. package/dist/cli/ui/components/NavigationList.js.map +0 -1
  99. package/dist/cli/ui/components/ScrollableContent.d.ts +0 -13
  100. package/dist/cli/ui/components/ScrollableContent.d.ts.map +0 -1
  101. package/dist/cli/ui/components/ScrollableContent.js +0 -11
  102. package/dist/cli/ui/components/ScrollableContent.js.map +0 -1
  103. package/dist/cli/ui/components/index.d.ts +0 -7
  104. package/dist/cli/ui/components/index.d.ts.map +0 -1
  105. package/dist/cli/ui/components/index.js +0 -9
  106. package/dist/cli/ui/components/index.js.map +0 -1
  107. package/dist/cli/ui/terminal-ui.d.ts +0 -85
  108. package/dist/cli/ui/terminal-ui.d.ts.map +0 -1
  109. package/dist/cli/ui/terminal-ui.js +0 -121
  110. package/dist/cli/ui/terminal-ui.js.map +0 -1
  111. package/dist/do/BashModule.d.ts +0 -871
  112. package/dist/do/BashModule.d.ts.map +0 -1
  113. package/dist/do/BashModule.js +0 -1143
  114. package/dist/do/BashModule.js.map +0 -1
  115. package/dist/do/FsModule.d.ts +0 -612
  116. package/dist/do/FsModule.d.ts.map +0 -1
  117. package/dist/do/FsModule.js +0 -1120
  118. package/dist/do/FsModule.js.map +0 -1
  119. package/dist/do/GitModule.d.ts +0 -635
  120. package/dist/do/GitModule.d.ts.map +0 -1
  121. package/dist/do/GitModule.js +0 -784
  122. package/dist/do/GitModule.js.map +0 -1
  123. package/dist/do/GitRepoDO.d.ts +0 -281
  124. package/dist/do/GitRepoDO.d.ts.map +0 -1
  125. package/dist/do/GitRepoDO.js +0 -479
  126. package/dist/do/GitRepoDO.js.map +0 -1
  127. package/dist/do/bash-ast.d.ts +0 -246
  128. package/dist/do/bash-ast.d.ts.map +0 -1
  129. package/dist/do/bash-ast.js +0 -888
  130. package/dist/do/bash-ast.js.map +0 -1
  131. package/dist/do/container-executor.d.ts +0 -491
  132. package/dist/do/container-executor.d.ts.map +0 -1
  133. package/dist/do/container-executor.js +0 -731
  134. package/dist/do/container-executor.js.map +0 -1
  135. package/dist/do/index.d.ts +0 -53
  136. package/dist/do/index.d.ts.map +0 -1
  137. package/dist/do/index.js +0 -91
  138. package/dist/do/index.js.map +0 -1
  139. package/dist/do/tiered-storage.d.ts +0 -403
  140. package/dist/do/tiered-storage.d.ts.map +0 -1
  141. package/dist/do/tiered-storage.js +0 -689
  142. package/dist/do/tiered-storage.js.map +0 -1
  143. package/dist/do/withBash.d.ts +0 -231
  144. package/dist/do/withBash.d.ts.map +0 -1
  145. package/dist/do/withBash.js +0 -244
  146. package/dist/do/withBash.js.map +0 -1
  147. package/dist/do/withFs.d.ts +0 -237
  148. package/dist/do/withFs.d.ts.map +0 -1
  149. package/dist/do/withFs.js +0 -387
  150. package/dist/do/withFs.js.map +0 -1
  151. package/dist/do/withGit.d.ts +0 -180
  152. package/dist/do/withGit.d.ts.map +0 -1
  153. package/dist/do/withGit.js +0 -271
  154. package/dist/do/withGit.js.map +0 -1
  155. package/dist/durable-object/object-store.d.ts +0 -633
  156. package/dist/durable-object/object-store.d.ts.map +0 -1
  157. package/dist/durable-object/object-store.js +0 -1164
  158. package/dist/durable-object/object-store.js.map +0 -1
  159. package/dist/durable-object/schema.d.ts.map +0 -1
  160. package/dist/durable-object/schema.js.map +0 -1
  161. package/dist/durable-object/wal.d.ts +0 -416
  162. package/dist/durable-object/wal.d.ts.map +0 -1
  163. package/dist/durable-object/wal.js +0 -445
  164. package/dist/durable-object/wal.js.map +0 -1
  165. package/dist/mcp/adapter.d.ts +0 -772
  166. package/dist/mcp/adapter.d.ts.map +0 -1
  167. package/dist/mcp/adapter.js +0 -895
  168. package/dist/mcp/adapter.js.map +0 -1
  169. package/dist/mcp/sandbox/miniflare-evaluator.d.ts +0 -22
  170. package/dist/mcp/sandbox/miniflare-evaluator.d.ts.map +0 -1
  171. package/dist/mcp/sandbox/miniflare-evaluator.js +0 -140
  172. package/dist/mcp/sandbox/miniflare-evaluator.js.map +0 -1
  173. package/dist/mcp/sandbox/object-store-proxy.d.ts +0 -32
  174. package/dist/mcp/sandbox/object-store-proxy.d.ts.map +0 -1
  175. package/dist/mcp/sandbox/object-store-proxy.js +0 -30
  176. package/dist/mcp/sandbox/object-store-proxy.js.map +0 -1
  177. package/dist/mcp/sandbox/template.d.ts +0 -17
  178. package/dist/mcp/sandbox/template.d.ts.map +0 -1
  179. package/dist/mcp/sandbox/template.js +0 -71
  180. package/dist/mcp/sandbox/template.js.map +0 -1
  181. package/dist/mcp/sandbox.d.ts +0 -764
  182. package/dist/mcp/sandbox.d.ts.map +0 -1
  183. package/dist/mcp/sandbox.js +0 -1362
  184. package/dist/mcp/sandbox.js.map +0 -1
  185. package/dist/mcp/sdk-adapter.d.ts +0 -835
  186. package/dist/mcp/sdk-adapter.d.ts.map +0 -1
  187. package/dist/mcp/sdk-adapter.js +0 -974
  188. package/dist/mcp/sdk-adapter.js.map +0 -1
  189. package/dist/mcp/tools/do.d.ts +0 -32
  190. package/dist/mcp/tools/do.d.ts.map +0 -1
  191. package/dist/mcp/tools/do.js +0 -117
  192. package/dist/mcp/tools/do.js.map +0 -1
  193. package/dist/mcp/tools.d.ts +0 -548
  194. package/dist/mcp/tools.d.ts.map +0 -1
  195. package/dist/mcp/tools.js +0 -3170
  196. package/dist/mcp/tools.js.map +0 -1
  197. package/dist/ops/blame.d.ts +0 -551
  198. package/dist/ops/blame.d.ts.map +0 -1
  199. package/dist/ops/blame.js +0 -1037
  200. package/dist/ops/blame.js.map +0 -1
  201. package/dist/ops/branch.d.ts +0 -766
  202. package/dist/ops/branch.d.ts.map +0 -1
  203. package/dist/ops/branch.js +0 -950
  204. package/dist/ops/branch.js.map +0 -1
  205. package/dist/ops/commit-traversal.d.ts +0 -349
  206. package/dist/ops/commit-traversal.d.ts.map +0 -1
  207. package/dist/ops/commit-traversal.js +0 -821
  208. package/dist/ops/commit-traversal.js.map +0 -1
  209. package/dist/ops/commit.d.ts +0 -555
  210. package/dist/ops/commit.d.ts.map +0 -1
  211. package/dist/ops/commit.js +0 -826
  212. package/dist/ops/commit.js.map +0 -1
  213. package/dist/ops/merge-base.d.ts +0 -397
  214. package/dist/ops/merge-base.d.ts.map +0 -1
  215. package/dist/ops/merge-base.js +0 -691
  216. package/dist/ops/merge-base.js.map +0 -1
  217. package/dist/ops/merge.d.ts +0 -855
  218. package/dist/ops/merge.d.ts.map +0 -1
  219. package/dist/ops/merge.js +0 -1551
  220. package/dist/ops/merge.js.map +0 -1
  221. package/dist/ops/tag.d.ts +0 -247
  222. package/dist/ops/tag.d.ts.map +0 -1
  223. package/dist/ops/tag.js +0 -649
  224. package/dist/ops/tag.js.map +0 -1
  225. package/dist/ops/tree-builder.d.ts +0 -178
  226. package/dist/ops/tree-builder.d.ts.map +0 -1
  227. package/dist/ops/tree-builder.js +0 -271
  228. package/dist/ops/tree-builder.js.map +0 -1
  229. package/dist/ops/tree-diff.d.ts +0 -291
  230. package/dist/ops/tree-diff.d.ts.map +0 -1
  231. package/dist/ops/tree-diff.js +0 -705
  232. package/dist/ops/tree-diff.js.map +0 -1
  233. package/dist/pack/delta.d.ts +0 -248
  234. package/dist/pack/delta.d.ts.map +0 -1
  235. package/dist/pack/delta.js +0 -740
  236. package/dist/pack/delta.js.map +0 -1
  237. package/dist/pack/format.d.ts +0 -446
  238. package/dist/pack/format.d.ts.map +0 -1
  239. package/dist/pack/format.js +0 -572
  240. package/dist/pack/format.js.map +0 -1
  241. package/dist/pack/full-generation.d.ts +0 -612
  242. package/dist/pack/full-generation.d.ts.map +0 -1
  243. package/dist/pack/full-generation.js +0 -1378
  244. package/dist/pack/full-generation.js.map +0 -1
  245. package/dist/pack/generation.d.ts +0 -441
  246. package/dist/pack/generation.d.ts.map +0 -1
  247. package/dist/pack/generation.js +0 -707
  248. package/dist/pack/generation.js.map +0 -1
  249. package/dist/pack/index.d.ts +0 -502
  250. package/dist/pack/index.d.ts.map +0 -1
  251. package/dist/pack/index.js +0 -833
  252. package/dist/pack/index.js.map +0 -1
  253. package/dist/refs/branch.d.ts +0 -683
  254. package/dist/refs/branch.d.ts.map +0 -1
  255. package/dist/refs/branch.js +0 -881
  256. package/dist/refs/branch.js.map +0 -1
  257. package/dist/refs/storage.d.ts +0 -833
  258. package/dist/refs/storage.d.ts.map +0 -1
  259. package/dist/refs/storage.js +0 -1023
  260. package/dist/refs/storage.js.map +0 -1
  261. package/dist/refs/tag.d.ts +0 -860
  262. package/dist/refs/tag.d.ts.map +0 -1
  263. package/dist/refs/tag.js +0 -996
  264. package/dist/refs/tag.js.map +0 -1
  265. package/dist/storage/backend.d.ts +0 -425
  266. package/dist/storage/backend.d.ts.map +0 -1
  267. package/dist/storage/backend.js +0 -41
  268. package/dist/storage/backend.js.map +0 -1
  269. package/dist/storage/fsx-adapter.d.ts +0 -204
  270. package/dist/storage/fsx-adapter.d.ts.map +0 -1
  271. package/dist/storage/fsx-adapter.js +0 -518
  272. package/dist/storage/fsx-adapter.js.map +0 -1
  273. package/dist/storage/lru-cache.d.ts +0 -691
  274. package/dist/storage/lru-cache.d.ts.map +0 -1
  275. package/dist/storage/lru-cache.js +0 -813
  276. package/dist/storage/lru-cache.js.map +0 -1
  277. package/dist/storage/object-index.d.ts +0 -585
  278. package/dist/storage/object-index.d.ts.map +0 -1
  279. package/dist/storage/object-index.js +0 -532
  280. package/dist/storage/object-index.js.map +0 -1
  281. package/dist/storage/r2-pack.d.ts +0 -1257
  282. package/dist/storage/r2-pack.d.ts.map +0 -1
  283. package/dist/storage/r2-pack.js +0 -1773
  284. package/dist/storage/r2-pack.js.map +0 -1
  285. package/dist/tiered/cdc-pipeline.d.ts +0 -1888
  286. package/dist/tiered/cdc-pipeline.d.ts.map +0 -1
  287. package/dist/tiered/cdc-pipeline.js +0 -1880
  288. package/dist/tiered/cdc-pipeline.js.map +0 -1
  289. package/dist/tiered/migration.d.ts +0 -1104
  290. package/dist/tiered/migration.d.ts.map +0 -1
  291. package/dist/tiered/migration.js +0 -1217
  292. package/dist/tiered/migration.js.map +0 -1
  293. package/dist/tiered/parquet-writer.d.ts +0 -1145
  294. package/dist/tiered/parquet-writer.d.ts.map +0 -1
  295. package/dist/tiered/parquet-writer.js +0 -1183
  296. package/dist/tiered/parquet-writer.js.map +0 -1
  297. package/dist/tiered/read-path.d.ts +0 -835
  298. package/dist/tiered/read-path.d.ts.map +0 -1
  299. package/dist/tiered/read-path.js +0 -487
  300. package/dist/tiered/read-path.js.map +0 -1
  301. package/dist/types/capability.d.ts +0 -1385
  302. package/dist/types/capability.d.ts.map +0 -1
  303. package/dist/types/capability.js +0 -36
  304. package/dist/types/capability.js.map +0 -1
  305. package/dist/types/index.d.ts +0 -13
  306. package/dist/types/index.d.ts.map +0 -1
  307. package/dist/types/index.js +0 -18
  308. package/dist/types/index.js.map +0 -1
  309. package/dist/types/interfaces.d.ts +0 -673
  310. package/dist/types/interfaces.d.ts.map +0 -1
  311. package/dist/types/interfaces.js +0 -26
  312. package/dist/types/interfaces.js.map +0 -1
  313. package/dist/types/objects.d.ts +0 -692
  314. package/dist/types/objects.d.ts.map +0 -1
  315. package/dist/types/objects.js +0 -837
  316. package/dist/types/objects.js.map +0 -1
  317. package/dist/types/storage.d.ts +0 -603
  318. package/dist/types/storage.d.ts.map +0 -1
  319. package/dist/types/storage.js +0 -191
  320. package/dist/types/storage.js.map +0 -1
  321. package/dist/types/worker-loader.d.ts +0 -60
  322. package/dist/types/worker-loader.d.ts.map +0 -1
  323. package/dist/types/worker-loader.js +0 -62
  324. package/dist/types/worker-loader.js.map +0 -1
  325. package/dist/utils/hash.d.ts +0 -198
  326. package/dist/utils/hash.d.ts.map +0 -1
  327. package/dist/utils/hash.js +0 -272
  328. package/dist/utils/hash.js.map +0 -1
  329. package/dist/utils/sha1.d.ts +0 -325
  330. package/dist/utils/sha1.d.ts.map +0 -1
  331. package/dist/utils/sha1.js +0 -635
  332. package/dist/utils/sha1.js.map +0 -1
  333. package/dist/wire/capabilities.d.ts +0 -1044
  334. package/dist/wire/capabilities.d.ts.map +0 -1
  335. package/dist/wire/capabilities.js +0 -941
  336. package/dist/wire/capabilities.js.map +0 -1
  337. package/dist/wire/path-security.d.ts +0 -157
  338. package/dist/wire/path-security.d.ts.map +0 -1
  339. package/dist/wire/path-security.js +0 -307
  340. package/dist/wire/path-security.js.map +0 -1
  341. package/dist/wire/pkt-line.d.ts +0 -345
  342. package/dist/wire/pkt-line.d.ts.map +0 -1
  343. package/dist/wire/pkt-line.js +0 -381
  344. package/dist/wire/pkt-line.js.map +0 -1
  345. package/dist/wire/receive-pack.d.ts +0 -1059
  346. package/dist/wire/receive-pack.d.ts.map +0 -1
  347. package/dist/wire/receive-pack.js +0 -1414
  348. package/dist/wire/receive-pack.js.map +0 -1
  349. package/dist/wire/smart-http.d.ts +0 -799
  350. package/dist/wire/smart-http.d.ts.map +0 -1
  351. package/dist/wire/smart-http.js +0 -945
  352. package/dist/wire/smart-http.js.map +0 -1
  353. package/dist/wire/upload-pack.d.ts +0 -727
  354. package/dist/wire/upload-pack.d.ts.map +0 -1
  355. package/dist/wire/upload-pack.js +0 -1141
  356. 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