automagik-forge 0.1.13 → 0.1.14

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 (301) hide show
  1. package/README.md +143 -447
  2. package/dist/linux-x64/automagik-forge-mcp.zip +0 -0
  3. package/{npx-cli/automagik-forge-0.0.55.tgz → dist/linux-x64/automagik-forge.zip} +0 -0
  4. package/package.json +13 -23
  5. package/.cargo/config.toml +0 -13
  6. package/.claude/commands/commit.md +0 -376
  7. package/.claude/commands/prompt.md +0 -871
  8. package/.env.example +0 -20
  9. package/.github/actions/setup-node/action.yml +0 -29
  10. package/.github/images/automagik-logo.png +0 -0
  11. package/.github/workflows/pre-release.yml +0 -470
  12. package/.github/workflows/publish.yml +0 -145
  13. package/.github/workflows/test.yml +0 -63
  14. package/.mcp.json +0 -57
  15. package/AGENT.md +0 -40
  16. package/CLAUDE.md +0 -40
  17. package/CODE-OF-CONDUCT.md +0 -89
  18. package/Cargo.toml +0 -19
  19. package/Dockerfile +0 -43
  20. package/LICENSE +0 -201
  21. package/Makefile +0 -97
  22. package/backend/.sqlx/query-01b7e2bac1261d8be3d03c03df3e5220590da6c31c77f161074fc62752d63881.json +0 -12
  23. package/backend/.sqlx/query-03f2b02ba6dc5ea2b3cf6b1004caea0ad6bcc10ebd63f441d321a389f026e263.json +0 -12
  24. package/backend/.sqlx/query-0923b77d137a29fc54d399a873ff15fc4af894490bc65a4d344a7575cb0d8643.json +0 -12
  25. package/backend/.sqlx/query-0f808bcdb63c5f180836e448dd64c435c51758b2fc54a52ce9e67495b1ab200e.json +0 -68
  26. package/backend/.sqlx/query-1268afe9ca849daa6722e3df7ca8e9e61f0d37052e782bb5452ab8e1018d9b63.json +0 -12
  27. package/backend/.sqlx/query-1b082630a9622f8667ee7a9aba2c2d3176019a68c6bb83d33008594821415a57.json +0 -12
  28. package/backend/.sqlx/query-1c7b06ba1e112abf6b945a2ff08a0b40ec23f3738c2e7399f067b558cf8d490e.json +0 -12
  29. package/backend/.sqlx/query-1f619f01f46859a64ded531dd0ef61abacfe62e758abe7030a6aa745140b95ca.json +0 -104
  30. package/backend/.sqlx/query-1fca1ce14b4b20205364cd1f1f45ebe1d2e30cd745e59e189d56487b5639dfbb.json +0 -12
  31. package/backend/.sqlx/query-212828320e8d871ab9d83705a040b23bcf0393dc7252177fc539a74657f578ef.json +0 -32
  32. package/backend/.sqlx/query-290ce5c152be8d36e58ff42570f9157beb07ab9e77a03ec6fc30b4f56f9b8f6b.json +0 -56
  33. package/backend/.sqlx/query-2b471d2c2e8ffbe0cd42d2a91b814c0d79f9d09200f147e3cea33ba4ce673c8a.json +0 -68
  34. package/backend/.sqlx/query-354a48c705bb9bb2048c1b7f10fcb714e23f9db82b7a4ea6932486197b2ede6a.json +0 -92
  35. package/backend/.sqlx/query-36c9e3dd10648e94b949db5c91a774ecb1e10a899ef95da74066eccedca4d8b2.json +0 -12
  36. package/backend/.sqlx/query-36e4ba7bbd81b402d5a20b6005755eafbb174c8dda442081823406ac32809a94.json +0 -56
  37. package/backend/.sqlx/query-3a5b3c98a55ca183ab20c74708e3d7e579dda37972c059e7515c4ceee4bd8dd3.json +0 -62
  38. package/backend/.sqlx/query-3d0a1cabf2a52e9d90cdfd29c509ca89aeb448d0c1d2446c65cd43db40735e86.json +0 -62
  39. package/backend/.sqlx/query-3d6bd16fbce59efe30b7f67ea342e0e4ea6d1432389c02468ad79f1f742d4031.json +0 -56
  40. package/backend/.sqlx/query-4049ca413b285a05aca6b25385e9c8185575f01e9069e4e8581aa45d713f612f.json +0 -32
  41. package/backend/.sqlx/query-412bacd3477d86369082e90f52240407abce436cb81292d42b2dbe1e5c18eea1.json +0 -104
  42. package/backend/.sqlx/query-417a8b1ff4e51de82aea0159a3b97932224dc325b23476cb84153d690227fd8b.json +0 -62
  43. package/backend/.sqlx/query-461cc1b0bb6fd909afc9dd2246e8526b3771cfbb0b22ae4b5d17b51af587b9e2.json +0 -56
  44. package/backend/.sqlx/query-58408c7a8cdeeda0bef359f1f9bd91299a339dc2b191462fc58c9736a56d5227.json +0 -92
  45. package/backend/.sqlx/query-5a886026d75d515c01f347cc203c8d99dd04c61dc468e2e4c5aa548436d13834.json +0 -62
  46. package/backend/.sqlx/query-5b902137b11022d2e1a5c4f6a9c83fec1a856c6a710aff831abd2382ede76b43.json +0 -12
  47. package/backend/.sqlx/query-5ed1238e52e59bb5f76c0f153fd99a14093f7ce2585bf9843585608f17ec575b.json +0 -104
  48. package/backend/.sqlx/query-6e8b860b14decfc2227dc57213f38442943d3fbef5c8418fd6b634c6e0f5e2ea.json +0 -104
  49. package/backend/.sqlx/query-6ec414276994c4ccb2433eaa5b1b342168557d17ddf5a52dac84cb1b59b9de8f.json +0 -68
  50. package/backend/.sqlx/query-6ecfa16d0cf825aacf233544b5baf151e9adfdca26c226ad71020d291fd802d5.json +0 -62
  51. package/backend/.sqlx/query-72509d252c39fce77520aa816cb2acbc1fb35dc2605e7be893610599b2427f2e.json +0 -62
  52. package/backend/.sqlx/query-75239b2da188f749707d77f3c1544332ca70db3d6d6743b2601dc0d167536437.json +0 -62
  53. package/backend/.sqlx/query-83d10e29f8478aff33434f9ac67068e013b888b953a2657e2bb72a6f619d04f2.json +0 -50
  54. package/backend/.sqlx/query-8610803360ea18b9b9d078a6981ea56abfbfe84e6354fc1d5ae4c622e01410ed.json +0 -68
  55. package/backend/.sqlx/query-86d03eb70eef39c59296416867f2ee66c9f7cd8b7f961fbda2f89fc0a1c442c2.json +0 -12
  56. package/backend/.sqlx/query-87d0feb5a6b442bad9c60068ea7569599cc6fc91a0e2692ecb42e93b03201b9d.json +0 -68
  57. package/backend/.sqlx/query-8a67b3b3337248f06a57bdf8a908f7ef23177431eaed82dc08c94c3e5944340e.json +0 -12
  58. package/backend/.sqlx/query-8f01ebd64bdcde6a090479f14810d73ba23020e76fd70854ac57f2da251702c3.json +0 -12
  59. package/backend/.sqlx/query-90fd607fcb2dca72239ff25e618e21e174b195991eaa33722cbf5f76da84cfab.json +0 -62
  60. package/backend/.sqlx/query-92e8bdbcd80c5ff3db7a35cf79492048803ef305cbdef0d0a1fe5dc881ca8c71.json +0 -104
  61. package/backend/.sqlx/query-93a1605f90e9672dad29b472b6ad85fa9a55ea3ffa5abcb8724b09d61be254ca.json +0 -20
  62. package/backend/.sqlx/query-9472c8fb477958167f5fae40b85ac44252468c5226b2cdd7770f027332eed6d7.json +0 -104
  63. package/backend/.sqlx/query-96036c4f9e0f48bdc5a4a4588f0c5f288ac7aaa5425cac40fc33f337e1a351f2.json +0 -56
  64. package/backend/.sqlx/query-9edb2c01e91fd0f0fe7b56e988c7ae0393150f50be3f419a981e035c0121dfc7.json +0 -104
  65. package/backend/.sqlx/query-a157cf00616f703bfba21927f1eb1c9eec2a81c02da15f66efdba0b6c375de1b.json +0 -26
  66. package/backend/.sqlx/query-a31fff84f3b8e532fd1160447d89d700f06ae08821fee00c9a5b60492b05259c.json +0 -62
  67. package/backend/.sqlx/query-a5ba908419fb3e456bdd2daca41ba06cc3212ffffb8520fc7dbbcc8b60ada314.json +0 -12
  68. package/backend/.sqlx/query-a6d2961718dbc3b1a925e549f49a159c561bef58c105529275f274b27e2eba5b.json +0 -104
  69. package/backend/.sqlx/query-a9e93d5b09b29faf66e387e4d7596a792d81e75c4d3726e83c2963e8d7c9b56f.json +0 -104
  70. package/backend/.sqlx/query-ac5247c8d7fb86e4650c4b0eb9420031614c831b7b085083bac20c1af314c538.json +0 -12
  71. package/backend/.sqlx/query-afef9467be74c411c4cb119a8b2b1aea53049877dfc30cc60b486134ba4b4c9f.json +0 -68
  72. package/backend/.sqlx/query-b2b2c6b4d0b1a347b5c4cb63c3a46a265d4db53be9554989a814b069d0af82f2.json +0 -62
  73. package/backend/.sqlx/query-c50d2ff0b12e5bcc81e371089ee2d007e233e7db93aefba4fef08e7aa68f5ab7.json +0 -20
  74. package/backend/.sqlx/query-c614e6056b244ca07f1b9d44e7edc9d5819225c6f8d9e077070c6e518a17f50b.json +0 -12
  75. package/backend/.sqlx/query-c67259be8bf4ee0cfd32167b2aa3b7fe9192809181a8171bf1c2d6df731967ae.json +0 -12
  76. package/backend/.sqlx/query-d2d0a1b985ebbca6a2b3e882a221a219f3199890fa640afc946ef1a792d6d8de.json +0 -12
  77. package/backend/.sqlx/query-d30aa5786757f32bf2b9c5fe51a45e506c71c28c5994e430d9b0546adb15ffa2.json +0 -20
  78. package/backend/.sqlx/query-d3b9ea1de1576af71b312924ce7f4ea8ae5dbe2ac138ea3b4470f2d5cd734846.json +0 -12
  79. package/backend/.sqlx/query-ed8456646fa69ddd412441955f06ff22bfb790f29466450735e0b8bb1bc4ec94.json +0 -12
  80. package/backend/Cargo.toml +0 -71
  81. package/backend/build.rs +0 -32
  82. package/backend/migrations/20250617183714_init.sql +0 -44
  83. package/backend/migrations/20250620212427_execution_processes.sql +0 -25
  84. package/backend/migrations/20250620214100_remove_stdout_stderr_from_task_attempts.sql +0 -28
  85. package/backend/migrations/20250621120000_relate_activities_to_execution_processes.sql +0 -23
  86. package/backend/migrations/20250623120000_executor_sessions.sql +0 -17
  87. package/backend/migrations/20250623130000_add_executor_type_to_execution_processes.sql +0 -4
  88. package/backend/migrations/20250625000000_add_dev_script_to_projects.sql +0 -4
  89. package/backend/migrations/20250701000000_add_branch_to_task_attempts.sql +0 -2
  90. package/backend/migrations/20250701000001_add_pr_tracking_to_task_attempts.sql +0 -5
  91. package/backend/migrations/20250701120000_add_assistant_message_to_executor_sessions.sql +0 -2
  92. package/backend/migrations/20250708000000_add_base_branch_to_task_attempts.sql +0 -2
  93. package/backend/migrations/20250709000000_add_worktree_deleted_flag.sql +0 -2
  94. package/backend/migrations/20250710000000_add_setup_completion.sql +0 -3
  95. package/backend/migrations/20250715154859_add_task_templates.sql +0 -25
  96. package/backend/migrations/20250716143725_add_default_templates.sql +0 -174
  97. package/backend/migrations/20250716161432_update_executor_names_to_kebab_case.sql +0 -20
  98. package/backend/migrations/20250716170000_add_parent_task_to_tasks.sql +0 -7
  99. package/backend/migrations/20250717000000_drop_task_attempt_activities.sql +0 -9
  100. package/backend/migrations/20250719000000_add_cleanup_script_to_projects.sql +0 -2
  101. package/backend/migrations/20250720000000_add_cleanupscript_to_process_type_constraint.sql +0 -25
  102. package/backend/migrations/20250723000000_add_wish_to_tasks.sql +0 -7
  103. package/backend/migrations/20250724000000_remove_unique_wish_constraint.sql +0 -5
  104. package/backend/scripts/toast-notification.ps1 +0 -23
  105. package/backend/sounds/abstract-sound1.wav +0 -0
  106. package/backend/sounds/abstract-sound2.wav +0 -0
  107. package/backend/sounds/abstract-sound3.wav +0 -0
  108. package/backend/sounds/abstract-sound4.wav +0 -0
  109. package/backend/sounds/cow-mooing.wav +0 -0
  110. package/backend/sounds/phone-vibration.wav +0 -0
  111. package/backend/sounds/rooster.wav +0 -0
  112. package/backend/src/app_state.rs +0 -218
  113. package/backend/src/bin/generate_types.rs +0 -189
  114. package/backend/src/bin/mcp_task_server.rs +0 -191
  115. package/backend/src/execution_monitor.rs +0 -1193
  116. package/backend/src/executor.rs +0 -1053
  117. package/backend/src/executors/amp.rs +0 -697
  118. package/backend/src/executors/ccr.rs +0 -91
  119. package/backend/src/executors/charm_opencode.rs +0 -113
  120. package/backend/src/executors/claude.rs +0 -887
  121. package/backend/src/executors/cleanup_script.rs +0 -124
  122. package/backend/src/executors/dev_server.rs +0 -53
  123. package/backend/src/executors/echo.rs +0 -79
  124. package/backend/src/executors/gemini/config.rs +0 -67
  125. package/backend/src/executors/gemini/streaming.rs +0 -363
  126. package/backend/src/executors/gemini.rs +0 -765
  127. package/backend/src/executors/mod.rs +0 -23
  128. package/backend/src/executors/opencode_ai.rs +0 -113
  129. package/backend/src/executors/setup_script.rs +0 -130
  130. package/backend/src/executors/sst_opencode/filter.rs +0 -184
  131. package/backend/src/executors/sst_opencode/tools.rs +0 -139
  132. package/backend/src/executors/sst_opencode.rs +0 -756
  133. package/backend/src/lib.rs +0 -45
  134. package/backend/src/main.rs +0 -324
  135. package/backend/src/mcp/mod.rs +0 -1
  136. package/backend/src/mcp/task_server.rs +0 -850
  137. package/backend/src/middleware/mod.rs +0 -3
  138. package/backend/src/middleware/model_loaders.rs +0 -242
  139. package/backend/src/models/api_response.rs +0 -36
  140. package/backend/src/models/config.rs +0 -375
  141. package/backend/src/models/execution_process.rs +0 -430
  142. package/backend/src/models/executor_session.rs +0 -225
  143. package/backend/src/models/mod.rs +0 -12
  144. package/backend/src/models/project.rs +0 -356
  145. package/backend/src/models/task.rs +0 -345
  146. package/backend/src/models/task_attempt.rs +0 -1214
  147. package/backend/src/models/task_template.rs +0 -146
  148. package/backend/src/openapi.rs +0 -93
  149. package/backend/src/routes/auth.rs +0 -297
  150. package/backend/src/routes/config.rs +0 -385
  151. package/backend/src/routes/filesystem.rs +0 -228
  152. package/backend/src/routes/health.rs +0 -16
  153. package/backend/src/routes/mod.rs +0 -9
  154. package/backend/src/routes/projects.rs +0 -562
  155. package/backend/src/routes/stream.rs +0 -244
  156. package/backend/src/routes/task_attempts.rs +0 -1172
  157. package/backend/src/routes/task_templates.rs +0 -229
  158. package/backend/src/routes/tasks.rs +0 -353
  159. package/backend/src/services/analytics.rs +0 -216
  160. package/backend/src/services/git_service.rs +0 -1321
  161. package/backend/src/services/github_service.rs +0 -307
  162. package/backend/src/services/mod.rs +0 -13
  163. package/backend/src/services/notification_service.rs +0 -263
  164. package/backend/src/services/pr_monitor.rs +0 -214
  165. package/backend/src/services/process_service.rs +0 -940
  166. package/backend/src/utils/path.rs +0 -96
  167. package/backend/src/utils/shell.rs +0 -19
  168. package/backend/src/utils/text.rs +0 -24
  169. package/backend/src/utils/worktree_manager.rs +0 -578
  170. package/backend/src/utils.rs +0 -125
  171. package/backend/test.db +0 -0
  172. package/build-npm-package.sh +0 -61
  173. package/dev_assets_seed/config.json +0 -19
  174. package/frontend/.eslintrc.json +0 -25
  175. package/frontend/.prettierrc.json +0 -8
  176. package/frontend/components.json +0 -17
  177. package/frontend/index.html +0 -19
  178. package/frontend/package-lock.json +0 -7321
  179. package/frontend/package.json +0 -61
  180. package/frontend/postcss.config.js +0 -6
  181. package/frontend/public/android-chrome-192x192.png +0 -0
  182. package/frontend/public/android-chrome-512x512.png +0 -0
  183. package/frontend/public/apple-touch-icon.png +0 -0
  184. package/frontend/public/automagik-forge-logo-dark.svg +0 -3
  185. package/frontend/public/automagik-forge-logo.svg +0 -3
  186. package/frontend/public/automagik-forge-screenshot-overview.png +0 -0
  187. package/frontend/public/favicon-16x16.png +0 -0
  188. package/frontend/public/favicon-32x32.png +0 -0
  189. package/frontend/public/favicon.ico +0 -0
  190. package/frontend/public/site.webmanifest +0 -1
  191. package/frontend/public/viba-kanban-favicon.png +0 -0
  192. package/frontend/src/App.tsx +0 -157
  193. package/frontend/src/components/DisclaimerDialog.tsx +0 -106
  194. package/frontend/src/components/GitHubLoginDialog.tsx +0 -314
  195. package/frontend/src/components/OnboardingDialog.tsx +0 -185
  196. package/frontend/src/components/PrivacyOptInDialog.tsx +0 -130
  197. package/frontend/src/components/ProvidePatDialog.tsx +0 -98
  198. package/frontend/src/components/TaskTemplateManager.tsx +0 -336
  199. package/frontend/src/components/config-provider.tsx +0 -119
  200. package/frontend/src/components/context/TaskDetailsContextProvider.tsx +0 -470
  201. package/frontend/src/components/context/taskDetailsContext.ts +0 -125
  202. package/frontend/src/components/keyboard-shortcuts-demo.tsx +0 -35
  203. package/frontend/src/components/layout/navbar.tsx +0 -86
  204. package/frontend/src/components/logo.tsx +0 -44
  205. package/frontend/src/components/projects/ProjectCard.tsx +0 -155
  206. package/frontend/src/components/projects/project-detail.tsx +0 -251
  207. package/frontend/src/components/projects/project-form-fields.tsx +0 -238
  208. package/frontend/src/components/projects/project-form.tsx +0 -301
  209. package/frontend/src/components/projects/project-list.tsx +0 -200
  210. package/frontend/src/components/projects/projects-page.tsx +0 -20
  211. package/frontend/src/components/tasks/BranchSelector.tsx +0 -169
  212. package/frontend/src/components/tasks/DeleteFileConfirmationDialog.tsx +0 -94
  213. package/frontend/src/components/tasks/EditorSelectionDialog.tsx +0 -119
  214. package/frontend/src/components/tasks/TaskCard.tsx +0 -154
  215. package/frontend/src/components/tasks/TaskDetails/CollapsibleToolbar.tsx +0 -33
  216. package/frontend/src/components/tasks/TaskDetails/DiffCard.tsx +0 -109
  217. package/frontend/src/components/tasks/TaskDetails/DiffChunkSection.tsx +0 -135
  218. package/frontend/src/components/tasks/TaskDetails/DiffFile.tsx +0 -296
  219. package/frontend/src/components/tasks/TaskDetails/DiffTab.tsx +0 -32
  220. package/frontend/src/components/tasks/TaskDetails/DisplayConversationEntry.tsx +0 -392
  221. package/frontend/src/components/tasks/TaskDetails/LogsTab/Conversation.tsx +0 -256
  222. package/frontend/src/components/tasks/TaskDetails/LogsTab/ConversationEntry.tsx +0 -56
  223. package/frontend/src/components/tasks/TaskDetails/LogsTab/NormalizedConversationViewer.tsx +0 -92
  224. package/frontend/src/components/tasks/TaskDetails/LogsTab/Prompt.tsx +0 -22
  225. package/frontend/src/components/tasks/TaskDetails/LogsTab/SetupScriptRunning.tsx +0 -49
  226. package/frontend/src/components/tasks/TaskDetails/LogsTab.tsx +0 -186
  227. package/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx +0 -288
  228. package/frontend/src/components/tasks/TaskDetails/RelatedTasksTab.tsx +0 -216
  229. package/frontend/src/components/tasks/TaskDetails/TabNavigation.tsx +0 -93
  230. package/frontend/src/components/tasks/TaskDetailsHeader.tsx +0 -169
  231. package/frontend/src/components/tasks/TaskDetailsPanel.tsx +0 -126
  232. package/frontend/src/components/tasks/TaskDetailsToolbar.tsx +0 -302
  233. package/frontend/src/components/tasks/TaskFollowUpSection.tsx +0 -130
  234. package/frontend/src/components/tasks/TaskFormDialog.tsx +0 -400
  235. package/frontend/src/components/tasks/TaskKanbanBoard.tsx +0 -180
  236. package/frontend/src/components/tasks/Toolbar/CreateAttempt.tsx +0 -259
  237. package/frontend/src/components/tasks/Toolbar/CreatePRDialog.tsx +0 -243
  238. package/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx +0 -899
  239. package/frontend/src/components/tasks/index.ts +0 -2
  240. package/frontend/src/components/theme-provider.tsx +0 -82
  241. package/frontend/src/components/theme-toggle.tsx +0 -36
  242. package/frontend/src/components/ui/alert.tsx +0 -59
  243. package/frontend/src/components/ui/auto-expanding-textarea.tsx +0 -70
  244. package/frontend/src/components/ui/badge.tsx +0 -36
  245. package/frontend/src/components/ui/button.tsx +0 -56
  246. package/frontend/src/components/ui/card.tsx +0 -86
  247. package/frontend/src/components/ui/checkbox.tsx +0 -44
  248. package/frontend/src/components/ui/chip.tsx +0 -25
  249. package/frontend/src/components/ui/dialog.tsx +0 -124
  250. package/frontend/src/components/ui/dropdown-menu.tsx +0 -198
  251. package/frontend/src/components/ui/file-search-textarea.tsx +0 -292
  252. package/frontend/src/components/ui/folder-picker.tsx +0 -279
  253. package/frontend/src/components/ui/input.tsx +0 -25
  254. package/frontend/src/components/ui/label.tsx +0 -24
  255. package/frontend/src/components/ui/loader.tsx +0 -26
  256. package/frontend/src/components/ui/markdown-renderer.tsx +0 -75
  257. package/frontend/src/components/ui/select.tsx +0 -160
  258. package/frontend/src/components/ui/separator.tsx +0 -31
  259. package/frontend/src/components/ui/shadcn-io/kanban/index.tsx +0 -185
  260. package/frontend/src/components/ui/table.tsx +0 -117
  261. package/frontend/src/components/ui/tabs.tsx +0 -53
  262. package/frontend/src/components/ui/textarea.tsx +0 -22
  263. package/frontend/src/components/ui/tooltip.tsx +0 -28
  264. package/frontend/src/hooks/useNormalizedConversation.ts +0 -440
  265. package/frontend/src/index.css +0 -225
  266. package/frontend/src/lib/api.ts +0 -630
  267. package/frontend/src/lib/keyboard-shortcuts.ts +0 -266
  268. package/frontend/src/lib/responsive-config.ts +0 -70
  269. package/frontend/src/lib/types.ts +0 -39
  270. package/frontend/src/lib/utils.ts +0 -10
  271. package/frontend/src/main.tsx +0 -50
  272. package/frontend/src/pages/McpServers.tsx +0 -418
  273. package/frontend/src/pages/Settings.tsx +0 -610
  274. package/frontend/src/pages/project-tasks.tsx +0 -575
  275. package/frontend/src/pages/projects.tsx +0 -18
  276. package/frontend/src/vite-env.d.ts +0 -1
  277. package/frontend/tailwind.config.js +0 -125
  278. package/frontend/tsconfig.json +0 -26
  279. package/frontend/tsconfig.node.json +0 -10
  280. package/frontend/vite.config.ts +0 -33
  281. package/npx-cli/README.md +0 -159
  282. package/npx-cli/automagik-forge-0.1.0.tgz +0 -0
  283. package/npx-cli/automagik-forge-0.1.10.tgz +0 -0
  284. package/npx-cli/package.json +0 -17
  285. package/npx-cli/vibe-kanban-0.0.55.tgz +0 -0
  286. package/pnpm-workspace.yaml +0 -2
  287. package/rust-toolchain.toml +0 -11
  288. package/rustfmt.toml +0 -3
  289. package/scripts/load-env.js +0 -43
  290. package/scripts/mcp_test.js +0 -374
  291. package/scripts/prepare-db.js +0 -45
  292. package/scripts/setup-dev-environment.js +0 -274
  293. package/scripts/start-mcp-sse.js +0 -70
  294. package/scripts/test-debug.js +0 -32
  295. package/scripts/test-mcp-sse.js +0 -138
  296. package/scripts/test-simple.js +0 -44
  297. package/scripts/test-wish-final.js +0 -179
  298. package/scripts/test-wish-system.js +0 -221
  299. package/shared/types.ts +0 -182
  300. package/test-npm-package.sh +0 -42
  301. /package/{npx-cli/bin → bin}/cli.js +0 -0
@@ -1,1321 +0,0 @@
1
- use std::path::{Path, PathBuf};
2
-
3
- use git2::{
4
- build::CheckoutBuilder, BranchType, CherrypickOptions, Cred, DiffOptions, Error as GitError,
5
- FetchOptions, RemoteCallbacks, Repository, WorktreeAddOptions,
6
- };
7
- use regex;
8
- use tracing::{debug, info};
9
-
10
- use crate::{
11
- models::task_attempt::{DiffChunk, DiffChunkType, FileDiff, WorktreeDiff},
12
- utils::worktree_manager::WorktreeManager,
13
- };
14
-
15
- #[derive(Debug)]
16
- pub enum GitServiceError {
17
- Git(GitError),
18
- IoError(std::io::Error),
19
- InvalidRepository(String),
20
- BranchNotFound(String),
21
-
22
- MergeConflicts(String),
23
- InvalidPath(String),
24
- WorktreeDirty(String),
25
- }
26
-
27
- impl std::fmt::Display for GitServiceError {
28
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29
- match self {
30
- GitServiceError::Git(e) => write!(f, "Git error: {}", e),
31
- GitServiceError::IoError(e) => write!(f, "IO error: {}", e),
32
- GitServiceError::InvalidRepository(e) => write!(f, "Invalid repository: {}", e),
33
- GitServiceError::BranchNotFound(e) => write!(f, "Branch not found: {}", e),
34
-
35
- GitServiceError::MergeConflicts(e) => write!(f, "Merge conflicts: {}", e),
36
- GitServiceError::InvalidPath(e) => write!(f, "Invalid path: {}", e),
37
- GitServiceError::WorktreeDirty(e) => {
38
- write!(f, "Worktree has uncommitted changes: {}", e)
39
- }
40
- }
41
- }
42
- }
43
-
44
- impl std::error::Error for GitServiceError {}
45
-
46
- impl From<GitError> for GitServiceError {
47
- fn from(err: GitError) -> Self {
48
- GitServiceError::Git(err)
49
- }
50
- }
51
-
52
- impl From<std::io::Error> for GitServiceError {
53
- fn from(err: std::io::Error) -> Self {
54
- GitServiceError::IoError(err)
55
- }
56
- }
57
-
58
- /// Service for managing Git operations in task execution workflows
59
- pub struct GitService {
60
- repo_path: PathBuf,
61
- }
62
-
63
- impl GitService {
64
- /// Create a new GitService for the given repository path
65
- pub fn new<P: AsRef<Path>>(repo_path: P) -> Result<Self, GitServiceError> {
66
- let repo_path = repo_path.as_ref().to_path_buf();
67
-
68
- // Validate that the path exists and is a git repository
69
- if !repo_path.exists() {
70
- return Err(GitServiceError::InvalidPath(format!(
71
- "Repository path does not exist: {}",
72
- repo_path.display()
73
- )));
74
- }
75
-
76
- // Try to open the repository to validate it
77
- Repository::open(&repo_path).map_err(|e| {
78
- GitServiceError::InvalidRepository(format!(
79
- "Failed to open repository at {}: {}",
80
- repo_path.display(),
81
- e
82
- ))
83
- })?;
84
-
85
- Ok(Self { repo_path })
86
- }
87
-
88
- /// Open the repository
89
- fn open_repo(&self) -> Result<Repository, GitServiceError> {
90
- Repository::open(&self.repo_path).map_err(GitServiceError::from)
91
- }
92
-
93
- /// Create a worktree with a new branch
94
- pub fn create_worktree(
95
- &self,
96
- branch_name: &str,
97
- worktree_path: &Path,
98
- base_branch: Option<&str>,
99
- ) -> Result<(), GitServiceError> {
100
- let repo = self.open_repo()?;
101
-
102
- // Ensure parent directory exists
103
- if let Some(parent) = worktree_path.parent() {
104
- std::fs::create_dir_all(parent)?;
105
- }
106
-
107
- // Choose base reference
108
- let base_reference = if let Some(base_branch) = base_branch {
109
- let branch = repo
110
- .find_branch(base_branch, BranchType::Local)
111
- .map_err(|_| GitServiceError::BranchNotFound(base_branch.to_string()))?;
112
- branch.into_reference()
113
- } else {
114
- // Handle new repositories without any commits
115
- match repo.head() {
116
- Ok(head_ref) => head_ref,
117
- Err(e)
118
- if e.class() == git2::ErrorClass::Reference
119
- && e.code() == git2::ErrorCode::UnbornBranch =>
120
- {
121
- // Repository has no commits yet, create an initial commit
122
- self.create_initial_commit(&repo)?;
123
- repo.find_reference("refs/heads/main")?
124
- }
125
- Err(e) => return Err(e.into()),
126
- }
127
- };
128
-
129
- // Create branch
130
- repo.branch(branch_name, &base_reference.peel_to_commit()?, false)?;
131
-
132
- let branch = repo.find_branch(branch_name, BranchType::Local)?;
133
- let branch_ref = branch.into_reference();
134
- let mut worktree_opts = WorktreeAddOptions::new();
135
- worktree_opts.reference(Some(&branch_ref));
136
-
137
- // Create the worktree at the specified path
138
- repo.worktree(branch_name, worktree_path, Some(&worktree_opts))?;
139
-
140
- // Fix commondir for Windows/WSL compatibility
141
- let worktree_name = worktree_path
142
- .file_name()
143
- .and_then(|n| n.to_str())
144
- .unwrap_or(branch_name);
145
- if let Err(e) =
146
- WorktreeManager::fix_worktree_commondir_for_windows_wsl(&self.repo_path, worktree_name)
147
- {
148
- tracing::warn!("Failed to fix worktree commondir for Windows/WSL: {}", e);
149
- }
150
-
151
- info!(
152
- "Created worktree '{}' at path: {}",
153
- branch_name,
154
- worktree_path.display()
155
- );
156
- Ok(())
157
- }
158
-
159
- /// Create an initial commit for empty repositories
160
- fn create_initial_commit(&self, repo: &Repository) -> Result<(), GitServiceError> {
161
- let signature = repo.signature().unwrap_or_else(|_| {
162
- // Fallback if no Git config is set
163
- git2::Signature::now("Automagik Forge", "noreply@automagikforge.com")
164
- .expect("Failed to create fallback signature")
165
- });
166
-
167
- let tree_id = {
168
- let tree_builder = repo.treebuilder(None)?;
169
- tree_builder.write()?
170
- };
171
- let tree = repo.find_tree(tree_id)?;
172
-
173
- // Create initial commit on main branch
174
- let _commit_id = repo.commit(
175
- Some("refs/heads/main"),
176
- &signature,
177
- &signature,
178
- "Initial commit",
179
- &tree,
180
- &[],
181
- )?;
182
-
183
- // Set HEAD to point to main branch
184
- repo.set_head("refs/heads/main")?;
185
-
186
- info!("Created initial commit for empty repository");
187
- Ok(())
188
- }
189
-
190
- /// Merge changes from a worktree branch back to the main repository
191
- pub fn merge_changes(
192
- &self,
193
- worktree_path: &Path,
194
- branch_name: &str,
195
- base_branch_name: &str,
196
- commit_message: &str,
197
- ) -> Result<String, GitServiceError> {
198
- // Open the worktree repository
199
- let worktree_repo = Repository::open(worktree_path)?;
200
-
201
- // Check if worktree is dirty before proceeding
202
- self.check_worktree_clean(&worktree_repo)?;
203
-
204
- // Verify the task branch exists in the worktree
205
- let task_branch = worktree_repo
206
- .find_branch(branch_name, BranchType::Local)
207
- .map_err(|_| GitServiceError::BranchNotFound(branch_name.to_string()))?;
208
-
209
- // Get the base branch from the worktree
210
- let base_branch = worktree_repo
211
- .find_branch(base_branch_name, BranchType::Local)
212
- .map_err(|_| GitServiceError::BranchNotFound(base_branch_name.to_string()))?;
213
-
214
- // Get commits
215
- let base_commit = base_branch.get().peel_to_commit()?;
216
- let task_commit = task_branch.get().peel_to_commit()?;
217
-
218
- // Get the signature for the merge commit
219
- let signature = worktree_repo.signature()?;
220
-
221
- // Perform a squash merge - create a single commit with all changes
222
- let squash_commit_id = self.perform_squash_merge(
223
- &worktree_repo,
224
- &base_commit,
225
- &task_commit,
226
- &signature,
227
- commit_message,
228
- base_branch_name,
229
- )?;
230
-
231
- // Fix: Update main repo's HEAD if it's pointing to the base branch
232
- let main_repo = self.open_repo()?;
233
- let refname = format!("refs/heads/{}", base_branch_name);
234
-
235
- if let Ok(main_head) = main_repo.head() {
236
- if let Some(branch_name) = main_head.shorthand() {
237
- if branch_name == base_branch_name {
238
- // Only update main repo's HEAD if it's currently on the base branch
239
- main_repo.set_head(&refname)?;
240
- let mut co = CheckoutBuilder::new();
241
- co.force();
242
- main_repo.checkout_head(Some(&mut co))?;
243
- }
244
- }
245
- }
246
-
247
- info!("Created squash merge commit: {}", squash_commit_id);
248
- Ok(squash_commit_id.to_string())
249
- }
250
-
251
- /// Check if the worktree is clean (no uncommitted changes to tracked files)
252
- fn check_worktree_clean(&self, repo: &Repository) -> Result<(), GitServiceError> {
253
- let mut status_options = git2::StatusOptions::new();
254
- status_options
255
- .include_untracked(false) // Don't include untracked files
256
- .include_ignored(false); // Don't include ignored files
257
-
258
- let statuses = repo.statuses(Some(&mut status_options))?;
259
-
260
- if !statuses.is_empty() {
261
- let mut dirty_files = Vec::new();
262
- for entry in statuses.iter() {
263
- let status = entry.status();
264
- // Only consider files that are actually tracked and modified
265
- if status.intersects(
266
- git2::Status::INDEX_MODIFIED
267
- | git2::Status::INDEX_NEW
268
- | git2::Status::INDEX_DELETED
269
- | git2::Status::INDEX_RENAMED
270
- | git2::Status::INDEX_TYPECHANGE
271
- | git2::Status::WT_MODIFIED
272
- | git2::Status::WT_DELETED
273
- | git2::Status::WT_RENAMED
274
- | git2::Status::WT_TYPECHANGE,
275
- ) {
276
- if let Some(path) = entry.path() {
277
- dirty_files.push(path.to_string());
278
- }
279
- }
280
- }
281
-
282
- if !dirty_files.is_empty() {
283
- return Err(GitServiceError::WorktreeDirty(dirty_files.join(", ")));
284
- }
285
- }
286
-
287
- Ok(())
288
- }
289
-
290
- /// Perform a squash merge of task branch into base branch, but fail on conflicts
291
- fn perform_squash_merge(
292
- &self,
293
- repo: &Repository,
294
- base_commit: &git2::Commit,
295
- task_commit: &git2::Commit,
296
- signature: &git2::Signature,
297
- commit_message: &str,
298
- base_branch_name: &str,
299
- ) -> Result<git2::Oid, GitServiceError> {
300
- // Attempt an in-memory merge to detect conflicts
301
- let merge_opts = git2::MergeOptions::new();
302
- let mut index = repo.merge_commits(base_commit, task_commit, Some(&merge_opts))?;
303
-
304
- // If there are conflicts, return an error
305
- if index.has_conflicts() {
306
- return Err(GitServiceError::MergeConflicts(
307
- "Merge failed due to conflicts. Please resolve conflicts manually.".to_string(),
308
- ));
309
- }
310
-
311
- // Write the merged tree back to the repository
312
- let tree_id = index.write_tree_to(repo)?;
313
- let tree = repo.find_tree(tree_id)?;
314
-
315
- // Create a squash commit: use merged tree with base_commit as sole parent
316
- let squash_commit_id = repo.commit(
317
- None, // Don't update any reference yet
318
- signature, // Author
319
- signature, // Committer
320
- commit_message, // Custom message
321
- &tree, // Merged tree content
322
- &[base_commit], // Single parent: base branch commit
323
- )?;
324
-
325
- // Update the base branch reference to point to the new commit
326
- let refname = format!("refs/heads/{}", base_branch_name);
327
- repo.reference(&refname, squash_commit_id, true, "Squash merge")?;
328
-
329
- Ok(squash_commit_id)
330
- }
331
-
332
- /// Rebase a worktree branch onto a new base
333
- pub fn rebase_branch(
334
- &self,
335
- worktree_path: &Path,
336
- new_base_branch: Option<&str>,
337
- old_base_branch: &str,
338
- ) -> Result<String, GitServiceError> {
339
- let worktree_repo = Repository::open(worktree_path)?;
340
- let main_repo = self.open_repo()?;
341
-
342
- // Check if there's an existing rebase in progress and abort it
343
- let state = worktree_repo.state();
344
- if state == git2::RepositoryState::Rebase
345
- || state == git2::RepositoryState::RebaseInteractive
346
- || state == git2::RepositoryState::RebaseMerge
347
- {
348
- tracing::warn!("Existing rebase in progress, aborting it first");
349
- // Try to abort the existing rebase
350
- if let Ok(mut existing_rebase) = worktree_repo.open_rebase(None) {
351
- let _ = existing_rebase.abort();
352
- }
353
- }
354
-
355
- // Get the target base branch reference
356
- let base_branch_name = match new_base_branch {
357
- Some(branch) => branch.to_string(),
358
- None => main_repo
359
- .head()
360
- .ok()
361
- .and_then(|head| head.shorthand().map(|s| s.to_string()))
362
- .unwrap_or_else(|| "main".to_string()),
363
- };
364
- let base_branch_name = base_branch_name.as_str();
365
-
366
- // Handle remote branches by fetching them first and creating/updating local tracking branches
367
- let local_branch_name = if base_branch_name.starts_with("origin/") {
368
- // This is a remote branch, fetch it and create/update local tracking branch
369
- let remote_branch_name = base_branch_name.strip_prefix("origin/").unwrap();
370
-
371
- // First, fetch the latest changes from remote
372
- self.fetch_from_remote(&main_repo)?;
373
-
374
- // Try to find the remote branch after fetch
375
- let remote_branch = main_repo
376
- .find_branch(base_branch_name, BranchType::Remote)
377
- .map_err(|_| GitServiceError::BranchNotFound(base_branch_name.to_string()))?;
378
-
379
- // Check if local tracking branch exists
380
- match main_repo.find_branch(remote_branch_name, BranchType::Local) {
381
- Ok(mut local_branch) => {
382
- // Local tracking branch exists, update it to match remote
383
- let remote_commit = remote_branch.get().peel_to_commit()?;
384
- local_branch
385
- .get_mut()
386
- .set_target(remote_commit.id(), "Update local branch to match remote")?;
387
- }
388
- Err(_) => {
389
- // Local tracking branch doesn't exist, create it
390
- let remote_commit = remote_branch.get().peel_to_commit()?;
391
- main_repo.branch(remote_branch_name, &remote_commit, false)?;
392
- }
393
- }
394
-
395
- // Use the local branch name for rebase
396
- remote_branch_name
397
- } else {
398
- // This is already a local branch
399
- base_branch_name
400
- };
401
-
402
- // Get the local branch for rebase
403
- let base_branch = main_repo
404
- .find_branch(local_branch_name, BranchType::Local)
405
- .map_err(|_| GitServiceError::BranchNotFound(local_branch_name.to_string()))?;
406
-
407
- let new_base_commit_id = base_branch.get().peel_to_commit()?.id();
408
-
409
- // Get the HEAD commit of the worktree (the changes to rebase)
410
- let head = worktree_repo.head()?;
411
- let task_branch_commit_id = head.peel_to_commit()?.id();
412
-
413
- let signature = worktree_repo.signature()?;
414
-
415
- // Find the old base branch
416
- let old_base_branch_ref = if old_base_branch.starts_with("origin/") {
417
- // Remote branch - get local tracking branch name
418
- let remote_branch_name = old_base_branch.strip_prefix("origin/").unwrap();
419
- main_repo
420
- .find_branch(remote_branch_name, BranchType::Local)
421
- .map_err(|_| GitServiceError::BranchNotFound(remote_branch_name.to_string()))?
422
- } else {
423
- // Local branch
424
- main_repo
425
- .find_branch(old_base_branch, BranchType::Local)
426
- .map_err(|_| GitServiceError::BranchNotFound(old_base_branch.to_string()))?
427
- };
428
-
429
- let old_base_commit_id = old_base_branch_ref.get().peel_to_commit()?.id();
430
-
431
- // Find commits unique to the task branch
432
- let unique_commits = Self::find_unique_commits(
433
- &worktree_repo,
434
- task_branch_commit_id,
435
- old_base_commit_id,
436
- new_base_commit_id,
437
- )?;
438
-
439
- if !unique_commits.is_empty() {
440
- // Reset HEAD to the new base branch
441
- let new_base_commit = worktree_repo.find_commit(new_base_commit_id)?;
442
- worktree_repo.reset(new_base_commit.as_object(), git2::ResetType::Hard, None)?;
443
-
444
- // Cherry-pick the unique commits
445
- Self::cherry_pick_commits(&worktree_repo, &unique_commits, &signature)?;
446
- } else {
447
- // No unique commits to rebase, just reset to new base
448
- let new_base_commit = worktree_repo.find_commit(new_base_commit_id)?;
449
- worktree_repo.reset(new_base_commit.as_object(), git2::ResetType::Hard, None)?;
450
- }
451
-
452
- // Get the final commit ID after rebase
453
- let final_head = worktree_repo.head()?;
454
- let final_commit = final_head.peel_to_commit()?;
455
-
456
- info!("Rebase completed. New HEAD: {}", final_commit.id());
457
- Ok(final_commit.id().to_string())
458
- }
459
-
460
- /// Get enhanced diff for task attempts (from merge commit or worktree)
461
- pub fn get_enhanced_diff(
462
- &self,
463
- worktree_path: &Path,
464
- merge_commit_id: Option<&str>,
465
- base_branch: &str,
466
- ) -> Result<WorktreeDiff, GitServiceError> {
467
- let mut files = Vec::new();
468
-
469
- if let Some(merge_commit_id) = merge_commit_id {
470
- // Task attempt has been merged - show the diff from the merge commit
471
- self.get_merged_diff(merge_commit_id, &mut files)?;
472
- } else {
473
- // Task attempt not yet merged - get worktree diff
474
- self.get_worktree_diff(worktree_path, base_branch, &mut files)?;
475
- }
476
-
477
- Ok(WorktreeDiff { files })
478
- }
479
-
480
- /// Get diff from a merge commit
481
- fn get_merged_diff(
482
- &self,
483
- merge_commit_id: &str,
484
- files: &mut Vec<FileDiff>,
485
- ) -> Result<(), GitServiceError> {
486
- let main_repo = self.open_repo()?;
487
- let merge_commit = main_repo.find_commit(git2::Oid::from_str(merge_commit_id)?)?;
488
-
489
- // A merge commit has multiple parents - first parent is the main branch before merge,
490
- // second parent is the branch that was merged
491
- let parents: Vec<_> = merge_commit.parents().collect();
492
-
493
- // Create diff options with more context
494
- let mut diff_opts = DiffOptions::new();
495
- diff_opts.context_lines(10);
496
- diff_opts.interhunk_lines(0);
497
-
498
- let diff = if parents.len() >= 2 {
499
- let base_tree = parents[0].tree()?;
500
- let merged_tree = parents[1].tree()?;
501
- main_repo.diff_tree_to_tree(
502
- Some(&base_tree),
503
- Some(&merged_tree),
504
- Some(&mut diff_opts),
505
- )?
506
- } else {
507
- // Fast-forward merge or single parent
508
- let base_tree = if !parents.is_empty() {
509
- parents[0].tree()?
510
- } else {
511
- main_repo.find_tree(git2::Oid::zero())?
512
- };
513
- let merged_tree = merge_commit.tree()?;
514
- main_repo.diff_tree_to_tree(
515
- Some(&base_tree),
516
- Some(&merged_tree),
517
- Some(&mut diff_opts),
518
- )?
519
- };
520
-
521
- // Process each diff delta
522
- diff.foreach(
523
- &mut |delta, _progress| {
524
- if let Some(path_str) = delta.new_file().path().and_then(|p| p.to_str()) {
525
- let old_file = delta.old_file();
526
- let new_file = delta.new_file();
527
-
528
- if let Ok(diff_chunks) =
529
- self.generate_git_diff_chunks(&main_repo, &old_file, &new_file, path_str)
530
- {
531
- if !diff_chunks.is_empty() {
532
- files.push(FileDiff {
533
- path: path_str.to_string(),
534
- chunks: diff_chunks,
535
- });
536
- } else if delta.status() == git2::Delta::Added
537
- || delta.status() == git2::Delta::Deleted
538
- {
539
- files.push(FileDiff {
540
- path: path_str.to_string(),
541
- chunks: vec![DiffChunk {
542
- chunk_type: if delta.status() == git2::Delta::Added {
543
- DiffChunkType::Insert
544
- } else {
545
- DiffChunkType::Delete
546
- },
547
- content: format!(
548
- "{} file",
549
- if delta.status() == git2::Delta::Added {
550
- "Added"
551
- } else {
552
- "Deleted"
553
- }
554
- ),
555
- }],
556
- });
557
- }
558
- }
559
- }
560
- true
561
- },
562
- None,
563
- None,
564
- None,
565
- )?;
566
-
567
- Ok(())
568
- }
569
-
570
- /// Get diff for a worktree (before merge)
571
- fn get_worktree_diff(
572
- &self,
573
- worktree_path: &Path,
574
- base_branch: &str,
575
- files: &mut Vec<FileDiff>,
576
- ) -> Result<(), GitServiceError> {
577
- let worktree_repo = Repository::open(worktree_path)?;
578
- let main_repo = self.open_repo()?;
579
-
580
- // Get the base branch commit
581
- let base_branch_ref = main_repo
582
- .find_branch(base_branch, BranchType::Local)
583
- .map_err(|_| GitServiceError::BranchNotFound(base_branch.to_string()))?;
584
- let base_branch_oid = base_branch_ref.get().peel_to_commit()?.id();
585
-
586
- // Get the current worktree HEAD commit
587
- let worktree_head = worktree_repo.head()?;
588
- let worktree_head_oid = worktree_head.peel_to_commit()?.id();
589
-
590
- // Find the merge base (common ancestor) between the base branch and worktree head
591
- let base_oid = worktree_repo.merge_base(base_branch_oid, worktree_head_oid)?;
592
- let base_commit = worktree_repo.find_commit(base_oid)?;
593
- let base_tree = base_commit.tree()?;
594
-
595
- // Get the current tree from the worktree HEAD commit
596
- let current_commit = worktree_repo.find_commit(worktree_head_oid)?;
597
- let current_tree = current_commit.tree()?;
598
-
599
- // Create a diff between the base tree and current tree
600
- let mut diff_opts = DiffOptions::new();
601
- diff_opts.context_lines(10);
602
- diff_opts.interhunk_lines(0);
603
-
604
- let diff = worktree_repo.diff_tree_to_tree(
605
- Some(&base_tree),
606
- Some(&current_tree),
607
- Some(&mut diff_opts),
608
- )?;
609
-
610
- // Process committed changes
611
- diff.foreach(
612
- &mut |delta, _progress| {
613
- if let Some(path_str) = delta.new_file().path().and_then(|p| p.to_str()) {
614
- let old_file = delta.old_file();
615
- let new_file = delta.new_file();
616
-
617
- if let Ok(diff_chunks) = self.generate_git_diff_chunks(
618
- &worktree_repo,
619
- &old_file,
620
- &new_file,
621
- path_str,
622
- ) {
623
- if !diff_chunks.is_empty() {
624
- files.push(FileDiff {
625
- path: path_str.to_string(),
626
- chunks: diff_chunks,
627
- });
628
- } else if delta.status() == git2::Delta::Added
629
- || delta.status() == git2::Delta::Deleted
630
- {
631
- files.push(FileDiff {
632
- path: path_str.to_string(),
633
- chunks: vec![DiffChunk {
634
- chunk_type: if delta.status() == git2::Delta::Added {
635
- DiffChunkType::Insert
636
- } else {
637
- DiffChunkType::Delete
638
- },
639
- content: format!(
640
- "{} file",
641
- if delta.status() == git2::Delta::Added {
642
- "Added"
643
- } else {
644
- "Deleted"
645
- }
646
- ),
647
- }],
648
- });
649
- }
650
- }
651
- }
652
- true
653
- },
654
- None,
655
- None,
656
- None,
657
- )?;
658
-
659
- // Also get unstaged changes (working directory changes)
660
- let current_tree = worktree_repo.head()?.peel_to_tree()?;
661
-
662
- let mut unstaged_diff_opts = DiffOptions::new();
663
- unstaged_diff_opts.context_lines(10);
664
- unstaged_diff_opts.interhunk_lines(0);
665
- unstaged_diff_opts.include_untracked(true);
666
-
667
- let unstaged_diff = worktree_repo
668
- .diff_tree_to_workdir_with_index(Some(&current_tree), Some(&mut unstaged_diff_opts))?;
669
-
670
- // Process unstaged changes
671
- unstaged_diff.foreach(
672
- &mut |delta, _progress| {
673
- if let Some(path_str) = delta.new_file().path().and_then(|p| p.to_str()) {
674
- if let Err(e) = self.process_unstaged_file(
675
- files,
676
- &worktree_repo,
677
- base_oid,
678
- worktree_path,
679
- path_str,
680
- &delta,
681
- ) {
682
- eprintln!("Error processing unstaged file {}: {:?}", path_str, e);
683
- }
684
- }
685
- true
686
- },
687
- None,
688
- None,
689
- None,
690
- )?;
691
-
692
- Ok(())
693
- }
694
-
695
- /// Generate diff chunks using Git's native diff algorithm
696
- fn generate_git_diff_chunks(
697
- &self,
698
- repo: &Repository,
699
- old_file: &git2::DiffFile,
700
- new_file: &git2::DiffFile,
701
- file_path: &str,
702
- ) -> Result<Vec<DiffChunk>, GitServiceError> {
703
- let mut chunks = Vec::new();
704
-
705
- // Create a patch for the single file using Git's native diff
706
- let old_blob = if !old_file.id().is_zero() {
707
- Some(repo.find_blob(old_file.id())?)
708
- } else {
709
- None
710
- };
711
-
712
- let new_blob = if !new_file.id().is_zero() {
713
- Some(repo.find_blob(new_file.id())?)
714
- } else {
715
- None
716
- };
717
-
718
- // Generate patch using Git's diff algorithm
719
- let mut diff_opts = DiffOptions::new();
720
- diff_opts.context_lines(10);
721
- diff_opts.interhunk_lines(0);
722
-
723
- let patch = match (old_blob.as_ref(), new_blob.as_ref()) {
724
- (Some(old_b), Some(new_b)) => git2::Patch::from_blobs(
725
- old_b,
726
- Some(Path::new(file_path)),
727
- new_b,
728
- Some(Path::new(file_path)),
729
- Some(&mut diff_opts),
730
- )?,
731
- (None, Some(new_b)) => git2::Patch::from_buffers(
732
- &[],
733
- Some(Path::new(file_path)),
734
- new_b.content(),
735
- Some(Path::new(file_path)),
736
- Some(&mut diff_opts),
737
- )?,
738
- (Some(old_b), None) => git2::Patch::from_blob_and_buffer(
739
- old_b,
740
- Some(Path::new(file_path)),
741
- &[],
742
- Some(Path::new(file_path)),
743
- Some(&mut diff_opts),
744
- )?,
745
- (None, None) => {
746
- return Ok(chunks);
747
- }
748
- };
749
-
750
- // Process the patch hunks
751
- for hunk_idx in 0..patch.num_hunks() {
752
- let (_hunk, hunk_lines) = patch.hunk(hunk_idx)?;
753
-
754
- for line_idx in 0..hunk_lines {
755
- let line = patch.line_in_hunk(hunk_idx, line_idx)?;
756
- let content = String::from_utf8_lossy(line.content()).to_string();
757
-
758
- let chunk_type = match line.origin() {
759
- ' ' => DiffChunkType::Equal,
760
- '+' => DiffChunkType::Insert,
761
- '-' => DiffChunkType::Delete,
762
- _ => continue,
763
- };
764
-
765
- chunks.push(DiffChunk {
766
- chunk_type,
767
- content,
768
- });
769
- }
770
- }
771
-
772
- Ok(chunks)
773
- }
774
-
775
- /// Process unstaged file changes
776
- fn process_unstaged_file(
777
- &self,
778
- files: &mut Vec<FileDiff>,
779
- worktree_repo: &Repository,
780
- base_oid: git2::Oid,
781
- worktree_path: &Path,
782
- path_str: &str,
783
- delta: &git2::DiffDelta,
784
- ) -> Result<(), GitServiceError> {
785
- // Check if we already have a diff for this file from committed changes
786
- if let Some(existing_file) = files.iter_mut().find(|f| f.path == path_str) {
787
- // File already has committed changes, create a combined diff
788
- let base_content = self.get_base_file_content(worktree_repo, base_oid, path_str)?;
789
- let working_content = self.get_working_file_content(worktree_path, path_str, delta)?;
790
-
791
- if base_content != working_content {
792
- if let Ok(combined_chunks) =
793
- self.create_combined_diff_chunks(&base_content, &working_content, path_str)
794
- {
795
- existing_file.chunks = combined_chunks;
796
- }
797
- }
798
- } else {
799
- // File only has unstaged changes
800
- let base_content = self.get_base_file_content(worktree_repo, base_oid, path_str)?;
801
- let working_content = self.get_working_file_content(worktree_path, path_str, delta)?;
802
-
803
- if base_content != working_content || delta.status() != git2::Delta::Modified {
804
- if let Ok(chunks) =
805
- self.create_combined_diff_chunks(&base_content, &working_content, path_str)
806
- {
807
- if !chunks.is_empty() {
808
- files.push(FileDiff {
809
- path: path_str.to_string(),
810
- chunks,
811
- });
812
- }
813
- } else if delta.status() != git2::Delta::Modified {
814
- // Fallback for added/deleted files
815
- files.push(FileDiff {
816
- path: path_str.to_string(),
817
- chunks: vec![DiffChunk {
818
- chunk_type: if delta.status() == git2::Delta::Added {
819
- DiffChunkType::Insert
820
- } else {
821
- DiffChunkType::Delete
822
- },
823
- content: format!(
824
- "{} file",
825
- if delta.status() == git2::Delta::Added {
826
- "Added"
827
- } else {
828
- "Deleted"
829
- }
830
- ),
831
- }],
832
- });
833
- }
834
- }
835
- }
836
-
837
- Ok(())
838
- }
839
-
840
- /// Get the content of a file at the base commit
841
- fn get_base_file_content(
842
- &self,
843
- repo: &Repository,
844
- base_oid: git2::Oid,
845
- path_str: &str,
846
- ) -> Result<String, GitServiceError> {
847
- if let Ok(base_commit) = repo.find_commit(base_oid) {
848
- if let Ok(base_tree) = base_commit.tree() {
849
- if let Ok(entry) = base_tree.get_path(Path::new(path_str)) {
850
- if let Ok(blob) = repo.find_blob(entry.id()) {
851
- return Ok(String::from_utf8_lossy(blob.content()).to_string());
852
- }
853
- }
854
- }
855
- }
856
- Ok(String::new())
857
- }
858
-
859
- /// Get the content of a file in the working directory
860
- fn get_working_file_content(
861
- &self,
862
- worktree_path: &Path,
863
- path_str: &str,
864
- delta: &git2::DiffDelta,
865
- ) -> Result<String, GitServiceError> {
866
- if delta.status() != git2::Delta::Deleted {
867
- let file_path = worktree_path.join(path_str);
868
- std::fs::read_to_string(&file_path).map_err(GitServiceError::from)
869
- } else {
870
- Ok(String::new())
871
- }
872
- }
873
-
874
- /// Create diff chunks from two text contents
875
- fn create_combined_diff_chunks(
876
- &self,
877
- old_content: &str,
878
- new_content: &str,
879
- path_str: &str,
880
- ) -> Result<Vec<DiffChunk>, GitServiceError> {
881
- let mut diff_opts = DiffOptions::new();
882
- diff_opts.context_lines(10);
883
- diff_opts.interhunk_lines(0);
884
-
885
- let patch = git2::Patch::from_buffers(
886
- old_content.as_bytes(),
887
- Some(Path::new(path_str)),
888
- new_content.as_bytes(),
889
- Some(Path::new(path_str)),
890
- Some(&mut diff_opts),
891
- )?;
892
-
893
- let mut chunks = Vec::new();
894
-
895
- for hunk_idx in 0..patch.num_hunks() {
896
- let (_hunk, hunk_lines) = patch.hunk(hunk_idx)?;
897
-
898
- for line_idx in 0..hunk_lines {
899
- let line = patch.line_in_hunk(hunk_idx, line_idx)?;
900
- let content = String::from_utf8_lossy(line.content()).to_string();
901
-
902
- let chunk_type = match line.origin() {
903
- ' ' => DiffChunkType::Equal,
904
- '+' => DiffChunkType::Insert,
905
- '-' => DiffChunkType::Delete,
906
- _ => continue,
907
- };
908
-
909
- chunks.push(DiffChunk {
910
- chunk_type,
911
- content,
912
- });
913
- }
914
- }
915
-
916
- Ok(chunks)
917
- }
918
-
919
- /// Delete a file from the repository and commit the change
920
- pub fn delete_file_and_commit(
921
- &self,
922
- worktree_path: &Path,
923
- file_path: &str,
924
- ) -> Result<String, GitServiceError> {
925
- let repo = Repository::open(worktree_path)?;
926
-
927
- // Get the absolute path to the file within the worktree
928
- let file_full_path = worktree_path.join(file_path);
929
-
930
- // Check if file exists and delete it
931
- if file_full_path.exists() {
932
- std::fs::remove_file(&file_full_path).map_err(|e| {
933
- GitServiceError::IoError(std::io::Error::other(format!(
934
- "Failed to delete file {}: {}",
935
- file_path, e
936
- )))
937
- })?;
938
-
939
- debug!("Deleted file: {}", file_path);
940
- } else {
941
- info!("File {} does not exist, skipping deletion", file_path);
942
- }
943
-
944
- // Stage the deletion
945
- let mut index = repo.index()?;
946
- index.remove_path(Path::new(file_path))?;
947
- index.write()?;
948
-
949
- // Create a commit for the file deletion
950
- let signature = repo.signature()?;
951
- let tree_id = index.write_tree()?;
952
- let tree = repo.find_tree(tree_id)?;
953
-
954
- // Get the current HEAD commit
955
- let head = repo.head()?;
956
- let parent_commit = head.peel_to_commit()?;
957
-
958
- let commit_message = format!("Delete file: {}", file_path);
959
- let commit_id = repo.commit(
960
- Some("HEAD"),
961
- &signature,
962
- &signature,
963
- &commit_message,
964
- &tree,
965
- &[&parent_commit],
966
- )?;
967
-
968
- info!("File {} deleted and committed: {}", file_path, commit_id);
969
-
970
- Ok(commit_id.to_string())
971
- }
972
-
973
- /// Get the default branch name for the repository
974
- pub fn get_default_branch_name(&self) -> Result<String, GitServiceError> {
975
- let repo = self.open_repo()?;
976
-
977
- let result = match repo.head() {
978
- Ok(head_ref) => Ok(head_ref.shorthand().unwrap_or("main").to_string()),
979
- Err(e)
980
- if e.class() == git2::ErrorClass::Reference
981
- && e.code() == git2::ErrorCode::UnbornBranch =>
982
- {
983
- Ok("main".to_string()) // Repository has no commits yet
984
- }
985
- Err(_) => Ok("main".to_string()), // Fallback
986
- };
987
- result
988
- }
989
-
990
- /// Recreate a worktree from an existing branch (for cold task support)
991
- pub async fn recreate_worktree_from_branch(
992
- &self,
993
- branch_name: &str,
994
- stored_worktree_path: &Path,
995
- ) -> Result<PathBuf, GitServiceError> {
996
- let repo = self.open_repo()?;
997
-
998
- // Verify branch exists before proceeding
999
- let _branch = repo
1000
- .find_branch(branch_name, BranchType::Local)
1001
- .map_err(|_| GitServiceError::BranchNotFound(branch_name.to_string()))?;
1002
- drop(_branch);
1003
-
1004
- let stored_worktree_path_str = stored_worktree_path.to_string_lossy().to_string();
1005
-
1006
- info!(
1007
- "Recreating worktree using stored path: {} (branch: {})",
1008
- stored_worktree_path_str, branch_name
1009
- );
1010
-
1011
- // Clean up existing directory if it exists to avoid git sync issues
1012
- if stored_worktree_path.exists() {
1013
- debug!(
1014
- "Removing existing directory before worktree recreation: {}",
1015
- stored_worktree_path_str
1016
- );
1017
- std::fs::remove_dir_all(stored_worktree_path).map_err(|e| {
1018
- GitServiceError::IoError(std::io::Error::other(format!(
1019
- "Failed to remove existing worktree directory {}: {}",
1020
- stored_worktree_path_str, e
1021
- )))
1022
- })?;
1023
- }
1024
-
1025
- // Ensure parent directory exists - critical for session continuity
1026
- if let Some(parent) = stored_worktree_path.parent() {
1027
- std::fs::create_dir_all(parent).map_err(|e| {
1028
- GitServiceError::IoError(std::io::Error::other(format!(
1029
- "Failed to create parent directory for worktree path {}: {}",
1030
- stored_worktree_path_str, e
1031
- )))
1032
- })?;
1033
- }
1034
-
1035
- // Extract repository path for WorktreeManager
1036
- let repo_path = repo
1037
- .workdir()
1038
- .ok_or_else(|| {
1039
- GitServiceError::InvalidRepository(
1040
- "Repository has no working directory".to_string(),
1041
- )
1042
- })?
1043
- .to_str()
1044
- .ok_or_else(|| {
1045
- GitServiceError::InvalidRepository("Repository path is not valid UTF-8".to_string())
1046
- })?
1047
- .to_string();
1048
-
1049
- WorktreeManager::ensure_worktree_exists(
1050
- repo_path,
1051
- branch_name.to_string(),
1052
- stored_worktree_path.to_path_buf(),
1053
- )
1054
- .await
1055
- .map_err(|e| {
1056
- GitServiceError::IoError(std::io::Error::other(format!(
1057
- "WorktreeManager error: {}",
1058
- e
1059
- )))
1060
- })?;
1061
-
1062
- info!(
1063
- "Successfully recreated worktree at original path: {} -> {}",
1064
- branch_name, stored_worktree_path_str
1065
- );
1066
- Ok(stored_worktree_path.to_path_buf())
1067
- }
1068
-
1069
- /// Extract GitHub owner and repo name from git repo path
1070
- pub fn get_github_repo_info(&self) -> Result<(String, String), GitServiceError> {
1071
- let repo = self.open_repo()?;
1072
- let remote = repo.find_remote("origin").map_err(|_| {
1073
- GitServiceError::InvalidRepository("No 'origin' remote found".to_string())
1074
- })?;
1075
-
1076
- let url = remote.url().ok_or_else(|| {
1077
- GitServiceError::InvalidRepository("Remote origin has no URL".to_string())
1078
- })?;
1079
-
1080
- // Parse GitHub URL (supports both HTTPS and SSH formats)
1081
- let github_regex = regex::Regex::new(r"github\.com[:/]([^/]+)/(.+?)(?:\.git)?/?$")
1082
- .map_err(|e| GitServiceError::InvalidRepository(format!("Regex error: {}", e)))?;
1083
-
1084
- if let Some(captures) = github_regex.captures(url) {
1085
- let owner = captures.get(1).unwrap().as_str().to_string();
1086
- let repo_name = captures.get(2).unwrap().as_str().to_string();
1087
- Ok((owner, repo_name))
1088
- } else {
1089
- Err(GitServiceError::InvalidRepository(format!(
1090
- "Not a GitHub repository: {}",
1091
- url
1092
- )))
1093
- }
1094
- }
1095
-
1096
- /// Push the branch to GitHub remote
1097
- pub fn push_to_github(
1098
- &self,
1099
- worktree_path: &Path,
1100
- branch_name: &str,
1101
- github_token: &str,
1102
- ) -> Result<(), GitServiceError> {
1103
- let repo = Repository::open(worktree_path)?;
1104
-
1105
- // Get the remote
1106
- let remote = repo.find_remote("origin")?;
1107
- let remote_url = remote.url().ok_or_else(|| {
1108
- GitServiceError::InvalidRepository("Remote origin has no URL".to_string())
1109
- })?;
1110
-
1111
- // Convert SSH URL to HTTPS URL if necessary
1112
- let https_url = if remote_url.starts_with("git@github.com:") {
1113
- // Convert git@github.com:owner/repo.git to https://github.com/owner/repo.git
1114
- remote_url.replace("git@github.com:", "https://github.com/")
1115
- } else if remote_url.starts_with("ssh://git@github.com/") {
1116
- // Convert ssh://git@github.com/owner/repo.git to https://github.com/owner/repo.git
1117
- remote_url.replace("ssh://git@github.com/", "https://github.com/")
1118
- } else {
1119
- remote_url.to_string()
1120
- };
1121
-
1122
- // Create a temporary remote with HTTPS URL for pushing
1123
- let temp_remote_name = "temp_https_origin";
1124
-
1125
- // Remove any existing temp remote
1126
- let _ = repo.remote_delete(temp_remote_name);
1127
-
1128
- // Create temporary HTTPS remote
1129
- let mut temp_remote = repo.remote(temp_remote_name, &https_url)?;
1130
-
1131
- // Create refspec for pushing the branch
1132
- let refspec = format!("refs/heads/{}:refs/heads/{}", branch_name, branch_name);
1133
-
1134
- // Set up authentication callback using the GitHub token
1135
- let mut callbacks = git2::RemoteCallbacks::new();
1136
- callbacks.credentials(|_url, username_from_url, _allowed_types| {
1137
- git2::Cred::userpass_plaintext(username_from_url.unwrap_or("git"), github_token)
1138
- });
1139
-
1140
- // Configure push options
1141
- let mut push_options = git2::PushOptions::new();
1142
- push_options.remote_callbacks(callbacks);
1143
-
1144
- // Push the branch
1145
- let push_result = temp_remote.push(&[&refspec], Some(&mut push_options));
1146
-
1147
- // Clean up the temporary remote
1148
- let _ = repo.remote_delete(temp_remote_name);
1149
-
1150
- // Check push result
1151
- push_result?;
1152
-
1153
- info!("Pushed branch {} to GitHub using HTTPS", branch_name);
1154
- Ok(())
1155
- }
1156
-
1157
- /// Fetch from remote repository, with SSH authentication callbacks
1158
- fn fetch_from_remote(&self, repo: &Repository) -> Result<(), GitServiceError> {
1159
- // Find the “origin” remote
1160
- let mut remote = repo.find_remote("origin").map_err(|_| {
1161
- GitServiceError::Git(git2::Error::from_str("Remote 'origin' not found"))
1162
- })?;
1163
-
1164
- // Prepare callbacks for authentication
1165
- let mut callbacks = RemoteCallbacks::new();
1166
- callbacks.credentials(|_url, username_from_url, _| {
1167
- // Try SSH agent first
1168
- if let Some(username) = username_from_url {
1169
- if let Ok(cred) = Cred::ssh_key_from_agent(username) {
1170
- return Ok(cred);
1171
- }
1172
- }
1173
- // Fallback to key file (~/.ssh/id_rsa)
1174
- let home = dirs::home_dir()
1175
- .ok_or_else(|| git2::Error::from_str("Could not find home directory"))?;
1176
- let key_path = home.join(".ssh").join("id_rsa");
1177
- Cred::ssh_key(username_from_url.unwrap_or("git"), None, &key_path, None)
1178
- });
1179
-
1180
- // Set up fetch options with our callbacks
1181
- let mut fetch_opts = FetchOptions::new();
1182
- fetch_opts.remote_callbacks(callbacks);
1183
-
1184
- // Actually fetch (no specific refspecs = fetch all configured)
1185
- remote
1186
- .fetch(&[] as &[&str], Some(&mut fetch_opts), None)
1187
- .map_err(GitServiceError::Git)?;
1188
- Ok(())
1189
- }
1190
-
1191
- /// Find the merge-base between two commits
1192
- fn get_merge_base(
1193
- repo: &Repository,
1194
- commit1: git2::Oid,
1195
- commit2: git2::Oid,
1196
- ) -> Result<git2::Oid, GitServiceError> {
1197
- repo.merge_base(commit1, commit2)
1198
- .map_err(GitServiceError::Git)
1199
- }
1200
-
1201
- /// Find commits that are unique to the task branch (not in either base branch)
1202
- fn find_unique_commits(
1203
- repo: &Repository,
1204
- task_branch_commit: git2::Oid,
1205
- old_base_commit: git2::Oid,
1206
- new_base_commit: git2::Oid,
1207
- ) -> Result<Vec<git2::Oid>, GitServiceError> {
1208
- // Find merge-base between task branch and old base branch
1209
- let task_old_base_merge_base =
1210
- Self::get_merge_base(repo, task_branch_commit, old_base_commit)?;
1211
-
1212
- // Find merge-base between old base and new base
1213
- let old_new_base_merge_base = Self::get_merge_base(repo, old_base_commit, new_base_commit)?;
1214
-
1215
- // Get all commits from task branch back to the merge-base with old base
1216
- let mut walker = repo.revwalk()?;
1217
- walker.push(task_branch_commit)?;
1218
- walker.hide(task_old_base_merge_base)?;
1219
-
1220
- let mut task_commits = Vec::new();
1221
- for commit_id in walker {
1222
- let commit_id = commit_id?;
1223
-
1224
- // Check if this commit is not in the old base branch lineage
1225
- // (i.e., it's not between old_new_base_merge_base and old_base_commit)
1226
- let is_in_old_base = repo
1227
- .graph_descendant_of(commit_id, old_new_base_merge_base)
1228
- .unwrap_or(false)
1229
- && repo
1230
- .graph_descendant_of(old_base_commit, commit_id)
1231
- .unwrap_or(false);
1232
-
1233
- if !is_in_old_base {
1234
- task_commits.push(commit_id);
1235
- }
1236
- }
1237
-
1238
- // Reverse to get chronological order for cherry-picking
1239
- task_commits.reverse();
1240
- Ok(task_commits)
1241
- }
1242
-
1243
- /// Cherry-pick specific commits onto a new base
1244
- fn cherry_pick_commits(
1245
- repo: &Repository,
1246
- commits: &[git2::Oid],
1247
- signature: &git2::Signature,
1248
- ) -> Result<(), GitServiceError> {
1249
- for &commit_id in commits {
1250
- let commit = repo.find_commit(commit_id)?;
1251
-
1252
- // Cherry-pick the commit
1253
- let mut cherrypick_opts = CherrypickOptions::new();
1254
- repo.cherrypick(&commit, Some(&mut cherrypick_opts))?;
1255
-
1256
- // Check for conflicts
1257
- let mut index = repo.index()?;
1258
- if index.has_conflicts() {
1259
- return Err(GitServiceError::MergeConflicts(format!(
1260
- "Cherry-pick failed due to conflicts on commit {}",
1261
- commit_id
1262
- )));
1263
- }
1264
-
1265
- // Commit the cherry-pick
1266
- let tree_id = index.write_tree()?;
1267
- let tree = repo.find_tree(tree_id)?;
1268
- let head_commit = repo.head()?.peel_to_commit()?;
1269
-
1270
- repo.commit(
1271
- Some("HEAD"),
1272
- signature,
1273
- signature,
1274
- commit.message().unwrap_or("Cherry-picked commit"),
1275
- &tree,
1276
- &[&head_commit],
1277
- )?;
1278
- }
1279
-
1280
- Ok(())
1281
- }
1282
- }
1283
-
1284
- #[cfg(test)]
1285
- mod tests {
1286
- use tempfile::TempDir;
1287
-
1288
- use super::*;
1289
-
1290
- fn create_test_repo() -> (TempDir, Repository) {
1291
- let temp_dir = TempDir::new().unwrap();
1292
- let repo = Repository::init(temp_dir.path()).unwrap();
1293
-
1294
- // Configure the repository
1295
- let mut config = repo.config().unwrap();
1296
- config.set_str("user.name", "Test User").unwrap();
1297
- config.set_str("user.email", "test@example.com").unwrap();
1298
-
1299
- (temp_dir, repo)
1300
- }
1301
-
1302
- #[test]
1303
- fn test_git_service_creation() {
1304
- let (temp_dir, _repo) = create_test_repo();
1305
- let _git_service = GitService::new(temp_dir.path()).unwrap();
1306
- }
1307
-
1308
- #[test]
1309
- fn test_invalid_repository_path() {
1310
- let result = GitService::new("/nonexistent/path");
1311
- assert!(result.is_err());
1312
- }
1313
-
1314
- #[test]
1315
- fn test_default_branch_name() {
1316
- let (temp_dir, _repo) = create_test_repo();
1317
- let git_service = GitService::new(temp_dir.path()).unwrap();
1318
- let branch_name = git_service.get_default_branch_name().unwrap();
1319
- assert_eq!(branch_name, "main");
1320
- }
1321
- }