retrovim 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/.conda/retrovim.yaml +259 -0
  2. package/.conda/terax.icns +0 -0
  3. package/.conda/terax.ico +0 -0
  4. package/.conda/terax.png +0 -0
  5. package/.github/workflows/release.yml +95 -0
  6. package/.gitmodules +3 -0
  7. package/.npm/kanata_touchcursor.js +28 -0
  8. package/.npm/nvim.js +26 -0
  9. package/.npm/yazi.js +26 -0
  10. package/.npm/zsh.js +17 -0
  11. package/.pip/kanata_touchcursor/__init__.py +28 -0
  12. package/.pip/nvim/__init__.py +31 -0
  13. package/.pip/yazi/__init__.py +31 -0
  14. package/.pip/zsh/__init__.py +22 -0
  15. package/CHANGELOG.md +26 -0
  16. package/LICENSE +674 -0
  17. package/README.md +336 -0
  18. package/kanata/touchcursor.kbd +164 -0
  19. package/nvim/init.lua +813 -0
  20. package/nvim/plugins/mini.nvim/.github/DISCUSSION_TEMPLATE/q-a.yml +72 -0
  21. package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/bug-report.yml +136 -0
  22. package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/config.yml +5 -0
  23. package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/feature-request.yml +75 -0
  24. package/nvim/plugins/mini.nvim/.github/PULL_REQUEST_TEMPLATE.md +2 -0
  25. package/nvim/plugins/mini.nvim/.github/workflows/quality-control.yml +150 -0
  26. package/nvim/plugins/mini.nvim/.pre-commit-config.yaml +20 -0
  27. package/nvim/plugins/mini.nvim/.stylua.toml +7 -0
  28. package/nvim/plugins/mini.nvim/.styluaignore +1 -0
  29. package/nvim/plugins/mini.nvim/CHANGELOG.md +1894 -0
  30. package/nvim/plugins/mini.nvim/CODE_OF_CONDUCT.md +132 -0
  31. package/nvim/plugins/mini.nvim/CONTRIBUTING.md +331 -0
  32. package/nvim/plugins/mini.nvim/LICENSE +21 -0
  33. package/nvim/plugins/mini.nvim/MAINTAINING.md +330 -0
  34. package/nvim/plugins/mini.nvim/Makefile +51 -0
  35. package/nvim/plugins/mini.nvim/README.md +261 -0
  36. package/nvim/plugins/mini.nvim/TESTING.md +1011 -0
  37. package/nvim/plugins/mini.nvim/benchmarks/starter/README.md +35 -0
  38. package/nvim/plugins/mini.nvim/benchmarks/starter/benchmark.sh +56 -0
  39. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-alpha.lua +21 -0
  40. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-original.lua +17 -0
  41. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-starter.lua +18 -0
  42. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_empty.lua +4 -0
  43. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_starter-default.lua +7 -0
  44. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-alpha.lua +10 -0
  45. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-original.lua +7 -0
  46. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-starter.lua +20 -0
  47. package/nvim/plugins/mini.nvim/benchmarks/starter/install.sh +10 -0
  48. package/nvim/plugins/mini.nvim/benchmarks/starter/make_summary.py +68 -0
  49. package/nvim/plugins/mini.nvim/benchmarks/starter/startup-summary.md +10 -0
  50. package/nvim/plugins/mini.nvim/colors/miniautumn.lua +55 -0
  51. package/nvim/plugins/mini.nvim/colors/minicyan.lua +95 -0
  52. package/nvim/plugins/mini.nvim/colors/minischeme.lua +95 -0
  53. package/nvim/plugins/mini.nvim/colors/minispring.lua +53 -0
  54. package/nvim/plugins/mini.nvim/colors/minisummer.lua +49 -0
  55. package/nvim/plugins/mini.nvim/colors/miniwinter.lua +51 -0
  56. package/nvim/plugins/mini.nvim/colors/randomhue.lua +16 -0
  57. package/nvim/plugins/mini.nvim/doc/mini-ai.txt +889 -0
  58. package/nvim/plugins/mini.nvim/doc/mini-align.txt +923 -0
  59. package/nvim/plugins/mini.nvim/doc/mini-animate.txt +937 -0
  60. package/nvim/plugins/mini.nvim/doc/mini-base16.txt +285 -0
  61. package/nvim/plugins/mini.nvim/doc/mini-basics.txt +359 -0
  62. package/nvim/plugins/mini.nvim/doc/mini-bracketed.txt +622 -0
  63. package/nvim/plugins/mini.nvim/doc/mini-bufremove.txt +123 -0
  64. package/nvim/plugins/mini.nvim/doc/mini-clue.txt +760 -0
  65. package/nvim/plugins/mini.nvim/doc/mini-cmdline.txt +392 -0
  66. package/nvim/plugins/mini.nvim/doc/mini-colors.txt +880 -0
  67. package/nvim/plugins/mini.nvim/doc/mini-comment.txt +230 -0
  68. package/nvim/plugins/mini.nvim/doc/mini-completion.txt +477 -0
  69. package/nvim/plugins/mini.nvim/doc/mini-cursorword.txt +105 -0
  70. package/nvim/plugins/mini.nvim/doc/mini-deps.txt +590 -0
  71. package/nvim/plugins/mini.nvim/doc/mini-diff.txt +661 -0
  72. package/nvim/plugins/mini.nvim/doc/mini-doc.txt +434 -0
  73. package/nvim/plugins/mini.nvim/doc/mini-extra.txt +856 -0
  74. package/nvim/plugins/mini.nvim/doc/mini-files.txt +1018 -0
  75. package/nvim/plugins/mini.nvim/doc/mini-fuzzy.txt +160 -0
  76. package/nvim/plugins/mini.nvim/doc/mini-git.txt +425 -0
  77. package/nvim/plugins/mini.nvim/doc/mini-hipatterns.txt +489 -0
  78. package/nvim/plugins/mini.nvim/doc/mini-hues.txt +420 -0
  79. package/nvim/plugins/mini.nvim/doc/mini-icons.txt +508 -0
  80. package/nvim/plugins/mini.nvim/doc/mini-indentscope.txt +485 -0
  81. package/nvim/plugins/mini.nvim/doc/mini-input.txt +932 -0
  82. package/nvim/plugins/mini.nvim/doc/mini-jump.txt +202 -0
  83. package/nvim/plugins/mini.nvim/doc/mini-jump2d.txt +525 -0
  84. package/nvim/plugins/mini.nvim/doc/mini-keymap.txt +449 -0
  85. package/nvim/plugins/mini.nvim/doc/mini-map.txt +717 -0
  86. package/nvim/plugins/mini.nvim/doc/mini-misc.txt +431 -0
  87. package/nvim/plugins/mini.nvim/doc/mini-move.txt +181 -0
  88. package/nvim/plugins/mini.nvim/doc/mini-notify.txt +434 -0
  89. package/nvim/plugins/mini.nvim/doc/mini-nvim.txt +431 -0
  90. package/nvim/plugins/mini.nvim/doc/mini-operators.txt +472 -0
  91. package/nvim/plugins/mini.nvim/doc/mini-pairs.txt +346 -0
  92. package/nvim/plugins/mini.nvim/doc/mini-pick.txt +1535 -0
  93. package/nvim/plugins/mini.nvim/doc/mini-sessions.txt +239 -0
  94. package/nvim/plugins/mini.nvim/doc/mini-snippets.txt +1291 -0
  95. package/nvim/plugins/mini.nvim/doc/mini-splitjoin.txt +536 -0
  96. package/nvim/plugins/mini.nvim/doc/mini-starter.txt +618 -0
  97. package/nvim/plugins/mini.nvim/doc/mini-statusline.txt +361 -0
  98. package/nvim/plugins/mini.nvim/doc/mini-surround.txt +863 -0
  99. package/nvim/plugins/mini.nvim/doc/mini-tabline.txt +151 -0
  100. package/nvim/plugins/mini.nvim/doc/mini-test.txt +966 -0
  101. package/nvim/plugins/mini.nvim/doc/mini-trailspace.txt +91 -0
  102. package/nvim/plugins/mini.nvim/doc/mini-visits.txt +937 -0
  103. package/nvim/plugins/mini.nvim/logo.png +0 -0
  104. package/nvim/plugins/mini.nvim/lua/mini/ai.lua +2244 -0
  105. package/nvim/plugins/mini.nvim/lua/mini/align.lua +2076 -0
  106. package/nvim/plugins/mini.nvim/lua/mini/animate.lua +2127 -0
  107. package/nvim/plugins/mini.nvim/lua/mini/base16.lua +1752 -0
  108. package/nvim/plugins/mini.nvim/lua/mini/basics.lua +772 -0
  109. package/nvim/plugins/mini.nvim/lua/mini/bracketed.lua +2013 -0
  110. package/nvim/plugins/mini.nvim/lua/mini/bufremove.lua +283 -0
  111. package/nvim/plugins/mini.nvim/lua/mini/clue.lua +2096 -0
  112. package/nvim/plugins/mini.nvim/lua/mini/cmdline.lua +1182 -0
  113. package/nvim/plugins/mini.nvim/lua/mini/colors.lua +2483 -0
  114. package/nvim/plugins/mini.nvim/lua/mini/comment.lua +586 -0
  115. package/nvim/plugins/mini.nvim/lua/mini/completion.lua +2094 -0
  116. package/nvim/plugins/mini.nvim/lua/mini/cursorword.lua +311 -0
  117. package/nvim/plugins/mini.nvim/lua/mini/deps.lua +1660 -0
  118. package/nvim/plugins/mini.nvim/lua/mini/diff.lua +1938 -0
  119. package/nvim/plugins/mini.nvim/lua/mini/doc.lua +1371 -0
  120. package/nvim/plugins/mini.nvim/lua/mini/extra.lua +2290 -0
  121. package/nvim/plugins/mini.nvim/lua/mini/files.lua +3145 -0
  122. package/nvim/plugins/mini.nvim/lua/mini/fuzzy.lua +356 -0
  123. package/nvim/plugins/mini.nvim/lua/mini/git.lua +1737 -0
  124. package/nvim/plugins/mini.nvim/lua/mini/hipatterns.lua +1042 -0
  125. package/nvim/plugins/mini.nvim/lua/mini/hues.lua +2088 -0
  126. package/nvim/plugins/mini.nvim/lua/mini/icons.lua +2254 -0
  127. package/nvim/plugins/mini.nvim/lua/mini/indentscope.lua +1158 -0
  128. package/nvim/plugins/mini.nvim/lua/mini/init.lua +427 -0
  129. package/nvim/plugins/mini.nvim/lua/mini/input.lua +2183 -0
  130. package/nvim/plugins/mini.nvim/lua/mini/jump.lua +609 -0
  131. package/nvim/plugins/mini.nvim/lua/mini/jump2d.lua +1244 -0
  132. package/nvim/plugins/mini.nvim/lua/mini/keymap.lua +880 -0
  133. package/nvim/plugins/mini.nvim/lua/mini/map.lua +1709 -0
  134. package/nvim/plugins/mini.nvim/lua/mini/misc.lua +969 -0
  135. package/nvim/plugins/mini.nvim/lua/mini/move.lua +509 -0
  136. package/nvim/plugins/mini.nvim/lua/mini/notify.lua +938 -0
  137. package/nvim/plugins/mini.nvim/lua/mini/operators.lua +1349 -0
  138. package/nvim/plugins/mini.nvim/lua/mini/pairs.lua +657 -0
  139. package/nvim/plugins/mini.nvim/lua/mini/pick.lua +3876 -0
  140. package/nvim/plugins/mini.nvim/lua/mini/sessions.lua +689 -0
  141. package/nvim/plugins/mini.nvim/lua/mini/snippets.lua +2828 -0
  142. package/nvim/plugins/mini.nvim/lua/mini/splitjoin.lua +1132 -0
  143. package/nvim/plugins/mini.nvim/lua/mini/starter.lua +1599 -0
  144. package/nvim/plugins/mini.nvim/lua/mini/statusline.lua +686 -0
  145. package/nvim/plugins/mini.nvim/lua/mini/surround.lua +2388 -0
  146. package/nvim/plugins/mini.nvim/lua/mini/tabline.lua +564 -0
  147. package/nvim/plugins/mini.nvim/lua/mini/test.lua +2516 -0
  148. package/nvim/plugins/mini.nvim/lua/mini/trailspace.lua +215 -0
  149. package/nvim/plugins/mini.nvim/lua/mini/visits.lua +1577 -0
  150. package/nvim/plugins/mini.nvim/readmes/mini-ai.md +190 -0
  151. package/nvim/plugins/mini.nvim/readmes/mini-align.md +203 -0
  152. package/nvim/plugins/mini.nvim/readmes/mini-animate.md +214 -0
  153. package/nvim/plugins/mini.nvim/readmes/mini-base16.md +198 -0
  154. package/nvim/plugins/mini.nvim/readmes/mini-basics.md +184 -0
  155. package/nvim/plugins/mini.nvim/readmes/mini-bracketed.md +193 -0
  156. package/nvim/plugins/mini.nvim/readmes/mini-bufremove.md +139 -0
  157. package/nvim/plugins/mini.nvim/readmes/mini-clue.md +260 -0
  158. package/nvim/plugins/mini.nvim/readmes/mini-cmdline.md +183 -0
  159. package/nvim/plugins/mini.nvim/readmes/mini-colors.md +169 -0
  160. package/nvim/plugins/mini.nvim/readmes/mini-comment.md +183 -0
  161. package/nvim/plugins/mini.nvim/readmes/mini-completion.md +217 -0
  162. package/nvim/plugins/mini.nvim/readmes/mini-cursorword.md +137 -0
  163. package/nvim/plugins/mini.nvim/readmes/mini-deps.md +271 -0
  164. package/nvim/plugins/mini.nvim/readmes/mini-diff.md +289 -0
  165. package/nvim/plugins/mini.nvim/readmes/mini-doc.md +198 -0
  166. package/nvim/plugins/mini.nvim/readmes/mini-extra.md +158 -0
  167. package/nvim/plugins/mini.nvim/readmes/mini-files.md +247 -0
  168. package/nvim/plugins/mini.nvim/readmes/mini-fuzzy.md +134 -0
  169. package/nvim/plugins/mini.nvim/readmes/mini-git.md +168 -0
  170. package/nvim/plugins/mini.nvim/readmes/mini-hipatterns.md +176 -0
  171. package/nvim/plugins/mini.nvim/readmes/mini-hues.md +285 -0
  172. package/nvim/plugins/mini.nvim/readmes/mini-icons.md +183 -0
  173. package/nvim/plugins/mini.nvim/readmes/mini-indentscope.md +191 -0
  174. package/nvim/plugins/mini.nvim/readmes/mini-input.md +163 -0
  175. package/nvim/plugins/mini.nvim/readmes/mini-jump.md +161 -0
  176. package/nvim/plugins/mini.nvim/readmes/mini-jump2d.md +197 -0
  177. package/nvim/plugins/mini.nvim/readmes/mini-keymap.md +205 -0
  178. package/nvim/plugins/mini.nvim/readmes/mini-map.md +191 -0
  179. package/nvim/plugins/mini.nvim/readmes/mini-misc.md +145 -0
  180. package/nvim/plugins/mini.nvim/readmes/mini-move.md +163 -0
  181. package/nvim/plugins/mini.nvim/readmes/mini-notify.md +175 -0
  182. package/nvim/plugins/mini.nvim/readmes/mini-operators.md +193 -0
  183. package/nvim/plugins/mini.nvim/readmes/mini-pairs.md +158 -0
  184. package/nvim/plugins/mini.nvim/readmes/mini-pick.md +340 -0
  185. package/nvim/plugins/mini.nvim/readmes/mini-sessions.md +164 -0
  186. package/nvim/plugins/mini.nvim/readmes/mini-snippets.md +373 -0
  187. package/nvim/plugins/mini.nvim/readmes/mini-splitjoin.md +194 -0
  188. package/nvim/plugins/mini.nvim/readmes/mini-starter.md +176 -0
  189. package/nvim/plugins/mini.nvim/readmes/mini-statusline.md +155 -0
  190. package/nvim/plugins/mini.nvim/readmes/mini-surround.md +190 -0
  191. package/nvim/plugins/mini.nvim/readmes/mini-tabline.md +156 -0
  192. package/nvim/plugins/mini.nvim/readmes/mini-test.md +175 -0
  193. package/nvim/plugins/mini.nvim/readmes/mini-trailspace.md +137 -0
  194. package/nvim/plugins/mini.nvim/readmes/mini-visits.md +243 -0
  195. package/nvim/plugins/mini.nvim/scripts/dual_log.sh +13 -0
  196. package/nvim/plugins/mini.nvim/scripts/dual_push.sh +15 -0
  197. package/nvim/plugins/mini.nvim/scripts/dual_release.sh +102 -0
  198. package/nvim/plugins/mini.nvim/scripts/dual_sync.sh +115 -0
  199. package/nvim/plugins/mini.nvim/scripts/init-deps-example.lua +59 -0
  200. package/nvim/plugins/mini.nvim/scripts/lint-filename-length.sh +16 -0
  201. package/nvim/plugins/mini.nvim/scripts/lintcommit-ci.sh +18 -0
  202. package/nvim/plugins/mini.nvim/scripts/lintcommit.lua +393 -0
  203. package/nvim/plugins/mini.nvim/scripts/minidoc.lua +57 -0
  204. package/nvim/plugins/mini.nvim/scripts/minimal_init.lua +28 -0
  205. package/nvim/plugins/mini.nvim/scripts/minitest.lua +4 -0
  206. package/package.json +16 -0
  207. package/pyproject.toml +43 -0
  208. package/yazi/init.lua +12 -0
  209. package/yazi/keymap.toml +114 -0
  210. package/yazi/lesskey +5 -0
  211. package/yazi/plugins/bookmarks.yazi/main.lua +86 -0
  212. package/yazi/plugins/cd-git-root.yazi/main.lua +31 -0
  213. package/yazi/plugins/smart-enter.yazi/main.lua +11 -0
  214. package/yazi/theme.toml +26 -0
  215. package/yazi/yazi.toml +15 -0
  216. package/zsh/.zshrc +145 -0
  217. package/zsh/starship.toml +20 -0
  218. package/zsh/zsh-patina.toml +2 -0
@@ -0,0 +1,1737 @@
1
+ --- *mini.git* Git integration
2
+ ---
3
+ --- MIT License Copyright (c) 2024 Evgeni Chasnovski
4
+
5
+ --- Features:
6
+ ---
7
+ --- - Automated tracking of Git related data: root path, status, HEAD, etc.
8
+ --- Exposes buffer-local variables for convenient use in statusline.
9
+ --- See |MiniGit.enable()| and |MiniGit.get_buf_data()| for more information.
10
+ ---
11
+ --- - |:Git| command for executing any `git` call inside file's repository root with
12
+ --- deeper current instance integration (show output as notification/buffer,
13
+ --- use to edit commit messages, etc.).
14
+ ---
15
+ --- - Helper functions to inspect Git history:
16
+ --- - |MiniGit.show_range_history()| shows how certain line range evolved.
17
+ --- - |MiniGit.show_diff_source()| shows file state as it was at diff entry.
18
+ --- - |MiniGit.show_at_cursor()| shows Git related data depending on context.
19
+ ---
20
+ --- What it doesn't do:
21
+ ---
22
+ --- - Replace fully featured Git client. Rule of thumb: if feature does not rely
23
+ --- on a state of current Neovim (opened buffers, etc.), it is out of scope.
24
+ --- For more functionality, use either |mini.diff| or fully featured Git client.
25
+ ---
26
+ --- Sources with more details:
27
+ --- - |:Git|
28
+ --- - |MiniGit-examples|
29
+ --- - |MiniGit.enable()|
30
+ --- - |MiniGit.get_buf_data()|
31
+ ---
32
+ --- # Setup ~
33
+ ---
34
+ --- This module needs a setup with `require('mini.git').setup({})` (replace `{}` with
35
+ --- your `config` table). It will create global Lua table `MiniGit` which you can use
36
+ --- for scripting or manually (with `:lua MiniGit.*`).
37
+ ---
38
+ --- See |MiniGit.config| for `config` structure and default values.
39
+ ---
40
+ --- # Comparisons ~
41
+ ---
42
+ --- - [tpope/vim-fugitive](https://github.com/tpope/vim-fugitive):
43
+ --- - Mostly a dedicated Git client, while this module is not (by design).
44
+ --- - Provides buffer-local Git data only through fixed statusline component,
45
+ --- while this module has richer data in the form of a Lua table.
46
+ --- - Both provide |:Git| command with `vim-fugitive` treating some cases
47
+ --- extra specially (like `:Git blame`, etc.), while this module mostly
48
+ --- treats all cases the same. See |MiniGit-examples| for how they can be
49
+ --- manually customized.
50
+ --- Also this module provides slightly different (usually richer)
51
+ --- completion suggestions.
52
+ ---
53
+ --- - [NeogitOrg/neogit](https://github.com/NeogitOrg/neogit):
54
+ --- - Similar to `tpope/vim-fugitive`, but without `:Git` command.
55
+ ---
56
+ --- - [lewis6991/gitsigns.nvim](https://github.com/lewis6991/gitsigns.nvim):
57
+ --- - Provides buffer-local Git data with emphasis on granular diff status,
58
+ --- while this module is more oriented towards repository and file level
59
+ --- data (root, HEAD, file status, etc.). Use |mini.diff| for diff tracking.
60
+ ---
61
+ --- # Disabling ~
62
+ ---
63
+ --- To prevent buffer(s) from being tracked, set `vim.g.minigit_disable` (globally)
64
+ --- or `vim.b.minigit_disable` (for a buffer) to `true`. Considering high number of
65
+ --- different scenarios and customization intentions, writing exact rules for
66
+ --- disabling module's functionality is left to user.
67
+ --- See |mini.nvim-disabling-recipes| for common recipes.
68
+ ---@tag MiniGit
69
+
70
+ --- # Statusline component ~
71
+ ---
72
+ --- Tracked buffer data can be used in statusline via `vim.b.minigit_summary_string`
73
+ --- buffer-local variable. It is expected to be used as is. To show another info,
74
+ --- tweak buffer-local variable directly inside `MiniGitUpdated` `User` event: >lua
75
+ ---
76
+ --- -- Use only HEAD name as summary string
77
+ --- local format_summary = function(data)
78
+ --- -- Utilize buffer-local table summary
79
+ --- local summary = vim.b[data.buf].minigit_summary
80
+ --- vim.b[data.buf].minigit_summary_string = summary.head_name or ''
81
+ --- end
82
+ ---
83
+ --- local au_opts = { pattern = 'MiniGitUpdated', callback = format_summary }
84
+ --- vim.api.nvim_create_autocmd('User', au_opts)
85
+ --- <
86
+ --- # Tweaking command output ~
87
+ ---
88
+ --- Buffer output of |:Git| command can be tweaked inside autocommand for
89
+ --- `MiniGitCommandSplit` `User` event (see |MiniGit-command-events|).
90
+ --- For example, to make `:vertical Git blame -- %` align blame output with the
91
+ --- current window state, use the following code: >lua
92
+ ---
93
+ --- local align_blame = function(au_data)
94
+ --- if au_data.data.git_subcommand ~= 'blame' then return end
95
+ ---
96
+ --- -- Align blame output with source
97
+ --- local win_src = au_data.data.win_source
98
+ --- vim.wo.wrap = false
99
+ --- vim.fn.winrestview({ topline = vim.fn.line('w0', win_src) })
100
+ --- vim.api.nvim_win_set_cursor(0, { vim.fn.line('.', win_src), 0 })
101
+ ---
102
+ --- -- Bind both windows so that they scroll together
103
+ --- vim.wo[win_src].scrollbind, vim.wo.scrollbind = true, true
104
+ --- end
105
+ ---
106
+ --- local au_opts = { pattern = 'MiniGitCommandSplit', callback = align_blame }
107
+ --- vim.api.nvim_create_autocmd('User', au_opts)
108
+ --- <
109
+ --- # History navigation ~
110
+ ---
111
+ --- Function |MiniGit.show_at_cursor()| is specifically exported to make Git
112
+ --- history navigation easier. Here are some different ways it can be used:
113
+ ---
114
+ --- - Call inside buffer for already committed file to show the evolution of
115
+ --- the current line (or visually selected range) through history.
116
+ --- It is essentially a `:Git log HEAD` with proper `-L` flag.
117
+ --- This also works inside output of |MiniGit.show_diff_source()|.
118
+ ---
119
+ --- - Call with cursor on commit hash to inspect that commit in full.
120
+ --- This is usually helpful in the output of `:Git log`.
121
+ ---
122
+ --- - Call with cursor inside diff entry to inspect its file in the state how it
123
+ --- was at certain commit. By default it shows state after commit, unless cursor
124
+ --- is on the "deleted" line (i.e. line starting with "-") in which case
125
+ --- state before commit is shown.
126
+ ---
127
+ --- This workflow can be made more interactive when used with mapping, like this: >lua
128
+ ---
129
+ --- local rhs = '<Cmd>lua MiniGit.show_at_cursor()<CR>'
130
+ --- vim.keymap.set({ 'n', 'x' }, '<Leader>gs', rhs, { desc = 'Show at cursor' })
131
+ --- <
132
+ ---@tag MiniGit-examples
133
+
134
+ --- The `:Git` user command runs `git` CLI call with extra integration for currently
135
+ --- opened Neovim process:
136
+ --- - Command is executed inside repository root of the currently active file
137
+ --- (or |current-directory| if file is not tracked by this module).
138
+ ---
139
+ --- - Command output is shown either in dedicated buffer in window split or as
140
+ --- notification via |vim.notify()|. Which method is used depends on whether
141
+ --- particular Git subcommand is supposed to show data for user to inspect
142
+ --- (like `log`, `status`, etc.) or not (like `commit`, `push`, etc.). This is
143
+ --- determined automatically based on the data Git itself provides.
144
+ --- Split window is made current after command execution.
145
+ ---
146
+ --- Use split-related |:command-modifiers| (|:vertical|, |:horizontal|, or |:tab|)
147
+ --- to force output in a particular type of split. Default split direction is
148
+ --- controlled by `command.split` in |MiniGit.config|.
149
+ ---
150
+ --- Use |:silent| command modifier to not show any output.
151
+ ---
152
+ --- Errors and warnings are always shown as notifications.
153
+ ---
154
+ --- See |MiniGit-examples| for the example of tweaking command output.
155
+ ---
156
+ --- - Editor for tasks that require interactive user input (like `:Git commit` or
157
+ --- `:Git rebase --interactive`) is opened inside current session in a separate
158
+ --- split. Make modifications as in regular buffer, |:write| changes followed by
159
+ --- |:close| / |:quit| for Git CLI command to resume.
160
+ ---
161
+ --- Examples of usage:
162
+ --- - `:Git log --oneline` - show compact log of current repository.
163
+ --- - `:vert Git blame -- %` - show latest commits per line in vertical split.
164
+ --- - `:Git help rebase` - show help page for `rebase` subcommand.
165
+ --- - `:Git -C <cwd> status` - execute `git status` inside |current-directory|.
166
+ ---
167
+ --- There is also a context aware completion which can be invoked with `<Tab>`:
168
+ --- - If completed word starts with "-", options for the current Git subcommand
169
+ --- are shown. Like completion at `:Git log -` will suggest `-L`, `--oneline`, etc.
170
+ --- - If there is an explicit " -- " to the cursor's left, incremental path
171
+ --- suggestions will be shown.
172
+ --- - If there is no recognized Git subcommand yet, show list of subcommands.
173
+ --- Otherwise for some common subcommands list of its targets will be suggested:
174
+ --- like for `:Git branch` it will be list of branches, etc.
175
+ ---
176
+ --- Notes:
177
+ --- - Paths are always treated as relative to command's execution directory
178
+ --- (file's repository root or |current-directory| if absent).
179
+ --- - Don't use quotes for entries containing space, escape it with `\` directly.
180
+ --- Like `:Git commit -m Hello\ world` and not `:Git commit -m 'Hello world'`
181
+ --- (which treats `'Hello` and `world'` as separate arguments).
182
+ ---
183
+ --- # Events ~
184
+ --- *MiniGit-command-events*
185
+ ---
186
+ --- There are several `User` events triggered during command execution:
187
+ ---
188
+ --- - `MiniGitCommandDone` - after command is done executing. For Lua callbacks it
189
+ --- provides a special `data` table with the following fields:
190
+ --- - <cmd_input> `(table)` - structured data about executed command.
191
+ --- Has same structure as Lua function input in |nvim_create_user_command()|.
192
+ --- - <cwd> `(string)` - directory path inside which Git command was executed.
193
+ --- - <exit_code> `(number)` - exit code of CLI process.
194
+ --- - <git_command> `(table)` - array with arguments of full executed command.
195
+ --- - <git_subcommand> `(string)` - detected Git subcommand (like "log", etc.).
196
+ --- - <stderr> `(string)` - `stderr` process output.
197
+ --- - <stdout> `(string)` - `stdout` process output.
198
+ ---
199
+ --- - `MiniGitCommandSplit` - after command showed its output in a split. Triggered
200
+ --- after `MiniGitCommandDone` and provides similar `data` table with extra fields:
201
+ --- - <win_source> `(number)` - window identifier of "source" window (current at
202
+ --- the moment before command execution).
203
+ --- - <win_stdout> `(number)` - window identifier of command output.
204
+ ---@tag :Git
205
+
206
+ ---@alias __git_buf_id number Target buffer identifier. Default: 0 for current buffer.
207
+ ---@alias __git_split_field <split> `(string)` - split direction. One of "horizontal", "vertical",
208
+ --- "tab", or "auto" (default). Value "auto" uses |:vertical| if only |mini.git|
209
+ --- buffers are shown in the tabpage and |:tab| otherwise.
210
+
211
+ ---@diagnostic disable:undefined-field
212
+ ---@diagnostic disable:discard-returns
213
+ ---@diagnostic disable:unused-local
214
+ ---@diagnostic disable:cast-local-type
215
+ ---@diagnostic disable:undefined-doc-name
216
+ ---@diagnostic disable:luadoc-miss-type-name
217
+
218
+ -- Module definition ==========================================================
219
+ local MiniGit = {}
220
+ local H = {}
221
+
222
+ --- Module setup
223
+ ---
224
+ --- Besides general side effects (see |mini.nvim|), it also:
225
+ --- - Sets up auto enabling in every normal buffer for an actual file on disk.
226
+ --- - Creates |:Git| command.
227
+ ---
228
+ ---@param config table|nil Module config table. See |MiniGit.config|.
229
+ ---
230
+ ---@usage >lua
231
+ --- require('mini.git').setup() -- use default config
232
+ --- -- OR
233
+ --- require('mini.git').setup({}) -- replace {} with your config table
234
+ --- <
235
+ MiniGit.setup = function(config)
236
+ -- TODO: Remove after Neovim=0.9 support is dropped
237
+ if vim.fn.has('nvim-0.10') == 0 then
238
+ vim.notify(
239
+ '(mini.git) Neovim<0.10 is soft deprecated (module works but is not supported).'
240
+ .. " It will be deprecated after the next 'mini.nvim' release (module might not work)."
241
+ .. ' Please update your Neovim version.'
242
+ )
243
+ end
244
+
245
+ -- Export module
246
+ _G.MiniGit = MiniGit
247
+
248
+ -- Setup config
249
+ config = H.setup_config(config)
250
+
251
+ -- Apply config
252
+ H.apply_config(config)
253
+
254
+ -- Ensure proper Git executable
255
+ local exec = config.job.git_executable
256
+ H.has_git = vim.fn.executable(exec) == 1
257
+ if not H.has_git then H.notify('There is no `' .. exec .. '` executable', 'WARN') end
258
+
259
+ -- Define behavior
260
+ H.create_autocommands()
261
+ for _, buf_id in ipairs(vim.api.nvim_list_bufs()) do
262
+ H.auto_enable({ buf = buf_id })
263
+ end
264
+
265
+ -- Create user commands
266
+ H.create_user_commands()
267
+ end
268
+
269
+ --stylua: ignore
270
+ --- Defaults ~
271
+ ---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
272
+ ---@text # Job ~
273
+ ---
274
+ --- `config.job` contains options for customizing CLI executions.
275
+ ---
276
+ --- `job.git_executable` defines a full path to Git executable. Default: "git".
277
+ ---
278
+ --- `job.timeout` is a duration (in ms) from job start until it is forced to stop.
279
+ --- Default: 30000.
280
+ ---
281
+ --- # Command ~
282
+ ---
283
+ --- `config.command` contains options for customizing |:Git| command.
284
+ ---
285
+ --- `command.split` defines default split direction for |:Git| command output. Can be
286
+ --- one of "horizontal", "vertical", "tab", or "auto". Value "auto" uses |:vertical|
287
+ --- if only |mini.git| buffers are shown in the tabpage and |:tab| otherwise.
288
+ --- Default: "auto".
289
+ MiniGit.config = {
290
+ -- General CLI execution
291
+ job = {
292
+ -- Path to Git executable
293
+ git_executable = 'git',
294
+
295
+ -- Timeout (in ms) for each job before force quit
296
+ timeout = 30000,
297
+ },
298
+
299
+ -- Options for `:Git` command
300
+ command = {
301
+ -- Default split direction
302
+ split = 'auto',
303
+ },
304
+ }
305
+ --minidoc_afterlines_end
306
+
307
+ --- Show Git related data at cursor
308
+ ---
309
+ --- - If inside |mini.deps| confirmation buffer, show in split relevant commit data.
310
+ --- - If there is a commit-like |<cword>|, show it in split.
311
+ --- - If possible, show diff source via |MiniGit.show_diff_source()|.
312
+ --- - If possible, show range history via |MiniGit.show_range_history()|.
313
+ --- - Otherwise throw an error.
314
+ ---
315
+ ---@param opts table|nil Options. Possible values:
316
+ --- - __git_split_field
317
+ --- - Fields appropriate for forwarding to other functions.
318
+ MiniGit.show_at_cursor = function(opts)
319
+ -- Try showing commit data at cursor
320
+ local commit, cwd
321
+ if vim.bo.filetype == 'minideps-confirm' then
322
+ commit, cwd = H.deps_pos_to_source()
323
+ else
324
+ local cword = vim.fn.expand('<cword>')
325
+ local is_commit = string.find(cword, '^%x%x%x%x%x%x%x+$') ~= nil and string.lower(cword) == cword
326
+ commit = is_commit and cword or nil
327
+ cwd = is_commit and H.get_git_cwd() or nil
328
+ end
329
+
330
+ if commit ~= nil and cwd ~= nil then
331
+ local split = H.normalize_split_opt((opts or {}).split or 'auto', 'opts.split')
332
+ local args = { 'show', '--stat', '--patch', commit }
333
+ local lines = H.git_cli_output(args, cwd)
334
+ if #lines == 0 then return H.notify('Can not show commit ' .. commit .. ' in repo ' .. cwd, 'WARN') end
335
+ H.show_in_split(split, lines, 'show', table.concat(args, ' '))
336
+ vim.bo.filetype = 'git'
337
+ return
338
+ end
339
+
340
+ -- Try showing diff source
341
+ if H.diff_pos_to_source() ~= nil then return MiniGit.show_diff_source(opts) end
342
+
343
+ -- Try showing range history if possible: either in Git repo (tracked or not;
344
+ -- after resolving symlinks) or diff source output.
345
+ local buf_id, buf_name = vim.api.nvim_get_current_buf(), vim.api.nvim_buf_get_name(0)
346
+ local path = vim.loop.fs_realpath(buf_name)
347
+ local path_dir = path == nil and '' or vim.fn.fnamemodify(path, ':h')
348
+
349
+ local is_in_git = H.is_buf_enabled(buf_id) or #H.git_cli_output({ 'rev-parse', '--show-toplevel' }, path_dir) > 0
350
+ local is_diff_source_output = H.parse_diff_source_buf_name(buf_name) ~= nil
351
+ if is_in_git or is_diff_source_output then return MiniGit.show_range_history(opts) end
352
+
353
+ H.notify('Nothing Git-related to show at cursor', 'WARN')
354
+ end
355
+
356
+ --- Show diff source
357
+ ---
358
+ --- When buffer contains text formatted as unified patch (like after
359
+ --- `:Git log --patch`, `:Git diff`, or |MiniGit.show_range_history()|),
360
+ --- show state of the file at the particular state. Target commit/state, path,
361
+ --- and line number are deduced from cursor position.
362
+ ---
363
+ --- Notes:
364
+ --- - Needs |current-directory| to be the Git root for relative paths to work.
365
+ --- - Needs cursor to be inside hunk lines or on "---" / "+++" lines with paths.
366
+ --- - Only basic forms of `:Git diff` output is supported: `:Git diff`,
367
+ --- `:Git diff --cached`, and `:Git diff <commit>`.
368
+ ---
369
+ ---@param opts table|nil Options. Possible values:
370
+ --- - __git_split_field
371
+ --- - <target> `(string)` - which file state to show. One of "before", "after",
372
+ --- "both" (both states in vertical split), "auto" (default). Value "auto"
373
+ --- shows "before" state if cursor line starts with "-", otherwise - "after".
374
+ MiniGit.show_diff_source = function(opts)
375
+ opts = vim.tbl_deep_extend('force', { split = 'auto', target = 'auto' }, opts or {})
376
+ local split = H.normalize_split_opt(opts.split, 'opts.split')
377
+ local target = opts.target
378
+ if not (target == 'auto' or target == 'before' or target == 'after' or target == 'both') then
379
+ H.error('`opts.target` should be one of "auto", "before", "after", "both".')
380
+ end
381
+
382
+ local src = H.diff_pos_to_source()
383
+ if src == nil then
384
+ return H.notify('Could not find diff source. Ensure that cursor is inside a valid diff lines of git log.', 'WARN')
385
+ end
386
+ if target == 'auto' then target = src.init_prefix == '-' and 'before' or 'after' end
387
+
388
+ local cwd = H.get_git_cwd()
389
+ local show = function(commit, path, mods)
390
+ local is_worktree, args, lines = commit == true, nil, nil
391
+ if is_worktree then
392
+ args, lines = { 'edit', vim.fn.fnameescape(path) }, vim.fn.readfile(path)
393
+ else
394
+ args = { 'show', commit .. ':' .. path }
395
+ lines = H.git_cli_output(args, cwd)
396
+ end
397
+ if #lines == 0 and not is_worktree then
398
+ return H.notify('Can not show ' .. path .. ' at commit ' .. commit, 'WARN')
399
+ end
400
+ H.show_in_split(mods, lines, 'show', table.concat(args, ' '))
401
+ end
402
+
403
+ local has_before_shown = false
404
+ if target ~= 'after' then
405
+ if src.path_before == nil then
406
+ H.notify('No "before" as file was created', 'WARN')
407
+ else
408
+ show(src.commit_before, src.path_before, split)
409
+ vim.api.nvim_win_set_cursor(0, { src.lnum_before, 0 })
410
+ has_before_shown = true
411
+ end
412
+ end
413
+
414
+ if target ~= 'before' then
415
+ if src.path_after == nil then
416
+ H.notify('No "after" as file was deleted', 'WARN')
417
+ else
418
+ local mods_after = has_before_shown and 'belowright vertical' or split
419
+ show(src.commit_after, src.path_after, mods_after)
420
+ vim.api.nvim_win_set_cursor(0, { src.lnum_after, 0 })
421
+ end
422
+ end
423
+ end
424
+
425
+ --- Show range history
426
+ ---
427
+ --- Compute and show in split data about how particular line range in current
428
+ --- buffer evolved through Git history. Essentially a `git log` with `-L` flag.
429
+ ---
430
+ --- Notes:
431
+ --- - Works well with |MiniGit.diff_foldexpr()|.
432
+ --- - Does not work if there are uncommitted changes, as there is no easy way to
433
+ --- compute effective range line numbers.
434
+ ---
435
+ ---@param opts table|nil Options. Possible fields:
436
+ --- - <line_start> `(number)` - range start line.
437
+ --- - <line_end> `(number)` - range end line.
438
+ --- If both <line_start> and <line_end> are not supplied, they default to
439
+ --- current line in Normal mode and visual selection in Visual mode.
440
+ --- - <log_args> `(table)` - array of options to append to `git log` call.
441
+ --- - __git_split_field
442
+ MiniGit.show_range_history = function(opts)
443
+ local default_opts = { line_start = nil, line_end = nil, log_args = nil, split = 'auto' }
444
+ opts = vim.tbl_deep_extend('force', default_opts, opts or {})
445
+ local line_start, line_end = H.normalize_range_lines(opts.line_start, opts.line_end)
446
+ local log_args = opts.log_args or {}
447
+ if not H.islist(log_args) then H.error('`opts.log_args` should be an array.') end
448
+ local split = H.normalize_split_opt(opts.split, 'opts.split')
449
+
450
+ -- Construct `:Git log` command that works both with regular files and
451
+ -- buffers from `show_diff_source()`
452
+ local buf_name, cwd = vim.api.nvim_buf_get_name(0), H.get_git_cwd()
453
+ local commit, rel_path = H.parse_diff_source_buf_name(buf_name)
454
+ if commit == nil then
455
+ commit = 'HEAD'
456
+ local cwd_pattern = '^' .. vim.pesc(cwd:gsub('\\', '/')) .. '/'
457
+ rel_path = H.get_buf_realpath(0):gsub('\\', '/'):gsub(cwd_pattern, '')
458
+ end
459
+
460
+ -- Ensure no uncommitted changes as they might result into improper `-L` arg
461
+ local diff = commit == 'HEAD' and H.git_cli_output({ 'diff', '-U0', 'HEAD', '--', rel_path }, cwd) or {}
462
+ if #diff ~= 0 then
463
+ return H.notify('Current file has uncommitted lines. Commit or stash before exploring history.', 'WARN')
464
+ end
465
+
466
+ -- Show log in split
467
+ local range_flag = string.format('-L%d,%d:%s', line_start, line_end, rel_path)
468
+ local args = { 'log', range_flag, commit, unpack(log_args) }
469
+ local history = H.git_cli_output(args, cwd)
470
+ if #history == 0 then return H.notify('Could not get range history', 'WARN') end
471
+ H.show_in_split(split, history, 'log', table.concat(args, ' '))
472
+ end
473
+
474
+ --- Fold expression for Git logs
475
+ ---
476
+ --- Folds contents of hunks, file patches, and log entries in unified diff.
477
+ --- Useful for filetypes "diff" (like after `:Git diff`) and "git" (like after
478
+ --- `:Git log --patch` or `:Git show` for commit).
479
+ --- Works well with |MiniGit.show_range_history()|.
480
+ ---
481
+ --- General idea of folding levels (use |zr| and |zm| to adjust interactively):
482
+ --- - At level 0 there is one line per whole patch or log entry.
483
+ --- - At level 1 there is one line per patched file.
484
+ --- - At level 2 there is one line per hunk.
485
+ --- - At level 3 there is no folds.
486
+ ---
487
+ --- For automated setup, set the following for "git" and "diff" filetypes (either
488
+ --- inside |FileType| autocommand or |ftplugin|): >vim
489
+ ---
490
+ --- setlocal foldmethod=expr foldexpr=v:lua.MiniGit.diff_foldexpr()
491
+ --- <
492
+ ---@param lnum number|nil Line number for which fold level is computed.
493
+ --- Default: |v:lnum|.
494
+ ---
495
+ ---@return number|string Line fold level. See |fold-expr|.
496
+ MiniGit.diff_foldexpr = function(lnum)
497
+ lnum = lnum or vim.v.lnum
498
+ if H.is_log_entry_header(lnum + 1) or H.is_log_entry_header(lnum) then return 0 end
499
+ if H.is_file_entry_header(lnum) then return 1 end
500
+ if H.is_hunk_header(lnum) then return 2 end
501
+ if H.is_hunk_header(lnum - 1) then return 3 end
502
+ return '='
503
+ end
504
+
505
+ --- Enable Git tracking in a file buffer
506
+ ---
507
+ --- Tracking is done by reacting to changes in file content or file's repository
508
+ --- in the form of keeping buffer data up to date. The data can be used via:
509
+ --- - |MiniGit.get_buf_data()|. See its help for a list of actually tracked data.
510
+ --- - `vim.b.minigit_summary` (table) and `vim.b.minigit_summary_string` (string)
511
+ --- buffer-local variables which are more suitable for statusline.
512
+ --- `vim.b.minigit_summary_string` contains information about HEAD, file status,
513
+ --- and in progress action (see |MiniGit.get_buf_data()| for more details).
514
+ --- See |MiniGit-examples| for how it can be tweaked and used in statusline.
515
+ ---
516
+ --- Note: this function is called automatically for all new normal buffers.
517
+ --- Use it explicitly if buffer was disabled.
518
+ ---
519
+ --- `User` event `MiniGitUpdated` is triggered whenever tracking data is updated.
520
+ --- Note that not all data listed in |MiniGit.get_buf_data()| can be present (yet)
521
+ --- at the point of event being triggered.
522
+ ---
523
+ ---@param buf_id __git_buf_id
524
+ MiniGit.enable = function(buf_id)
525
+ buf_id = H.validate_buf_id(buf_id)
526
+
527
+ -- Don't enable more than once
528
+ if H.is_buf_enabled(buf_id) or H.is_disabled(buf_id) or not H.has_git then return end
529
+
530
+ -- Enable only in buffers which *can* be part of Git repo
531
+ local path = H.get_buf_realpath(buf_id)
532
+ if path == '' or vim.fn.filereadable(path) ~= 1 then return end
533
+
534
+ -- Start tracking
535
+ H.cache[buf_id] = {}
536
+ H.setup_buf_behavior(buf_id)
537
+ H.start_tracking(buf_id, path)
538
+ end
539
+
540
+ --- Disable Git tracking in buffer
541
+ ---
542
+ ---@param buf_id __git_buf_id
543
+ MiniGit.disable = function(buf_id)
544
+ buf_id = H.validate_buf_id(buf_id)
545
+
546
+ local buf_cache = H.cache[buf_id]
547
+ if buf_cache == nil then return end
548
+ H.cache[buf_id] = nil
549
+
550
+ -- Cleanup
551
+ pcall(vim.api.nvim_del_augroup_by_id, buf_cache.augroup)
552
+ vim.b[buf_id].minigit_summary, vim.b[buf_id].minigit_summary_string = nil, nil
553
+
554
+ -- - Unregister buffer from repo watching with possibly more cleanup
555
+ local repo = buf_cache.repo
556
+ if H.repos[repo] == nil then return end
557
+ H.repos[repo].buffers[buf_id] = nil
558
+ if vim.tbl_count(H.repos[repo].buffers) == 0 then
559
+ H.teardown_repo_watch(repo)
560
+ H.repos[repo] = nil
561
+ end
562
+ end
563
+
564
+ --- Toggle Git tracking in buffer
565
+ ---
566
+ --- Enable if disabled, disable if enabled.
567
+ ---
568
+ ---@param buf_id __git_buf_id
569
+ MiniGit.toggle = function(buf_id)
570
+ buf_id = H.validate_buf_id(buf_id)
571
+ if H.is_buf_enabled(buf_id) then return MiniGit.disable(buf_id) end
572
+ return MiniGit.enable(buf_id)
573
+ end
574
+
575
+ --- Get buffer data
576
+ ---
577
+ ---@param buf_id __git_buf_id
578
+ ---
579
+ ---@return table|nil Table with buffer Git data or `nil` if buffer is not enabled.
580
+ --- If the file is not part of Git repo, table will be empty.
581
+ --- Table has the following fields:
582
+ --- - <repo> `(string)` - full path to `.git` directory.
583
+ --- - <root> `(string)` - full path to worktree root.
584
+ --- - <head> `(string)` - full commit of current HEAD.
585
+ --- - <head_name> `(string)` - short name of current HEAD (like "master").
586
+ --- For detached HEAD it is "HEAD".
587
+ --- - <status> `(string)` - two character file status as returned by `git status`.
588
+ --- - <in_progress> `(string)` - name of action(s) currently in progress
589
+ --- (bisect, merge, etc.). Can be a combination of those separated by ",".
590
+ MiniGit.get_buf_data = function(buf_id)
591
+ buf_id = H.validate_buf_id(buf_id)
592
+ local buf_cache = H.cache[buf_id]
593
+ if buf_cache == nil then return nil end
594
+ --stylua: ignore
595
+ return {
596
+ repo = buf_cache.repo, root = buf_cache.root,
597
+ head = buf_cache.head, head_name = buf_cache.head_name,
598
+ status = buf_cache.status, in_progress = buf_cache.in_progress,
599
+ }
600
+ end
601
+
602
+ -- Helper data ================================================================
603
+ -- Module default config
604
+ H.default_config = MiniGit.config
605
+
606
+ -- Cache per enabled buffer. Values are tables with fields:
607
+ -- - <augroup> - identifier of augroup defining buffer behavior.
608
+ -- - <repo> - path to buffer's repo ('.git' directory).
609
+ -- - <root> - path to worktree root.
610
+ -- - <head> - full commit of `HEAD`.
611
+ -- - <head_name> - short name of `HEAD` (`'HEAD'` for detached head).
612
+ -- - <status> - current file status.
613
+ -- - <in_progress> - string name of action in progress (bisect, merge, etc.)
614
+ H.cache = {}
615
+
616
+ -- Cache per repo (git directory) path. Values are tables with fields:
617
+ -- - <fs_event> - `vim.loop` event for watching repo dir.
618
+ -- - <timer> - timer to debounce repo changes.
619
+ -- - <buffers> - map of buffers which should are part of repo.
620
+ H.repos = {}
621
+
622
+ -- Temporary file used as config for `GIT_EDITOR`
623
+ H.git_editor_config = nil
624
+
625
+ -- Data about supported Git subcommands. Initialized lazily. Fields:
626
+ -- - <supported> - array of supported one word commands.
627
+ -- - <complete> - array of commands to complete directly after `:Git`.
628
+ -- - <info> - map with fields as commands which show something to user.
629
+ -- - <options> - map of cached options per command; initialized lazily.
630
+ -- - <alias> - map of alias command name to command it implements.
631
+ H.git_subcommands = nil
632
+
633
+ -- Whether to temporarily skip some checks (like when inside `GIT_EDITOR`)
634
+ H.skip_timeout = false
635
+ H.skip_sync = false
636
+
637
+ -- Helper functionality =======================================================
638
+ -- Settings -------------------------------------------------------------------
639
+ H.setup_config = function(config)
640
+ H.check_type('config', config, 'table', true)
641
+ config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {})
642
+
643
+ H.check_type('job', config.job, 'table')
644
+ H.check_type('command', config.command, 'table')
645
+
646
+ H.check_type('job.git_executable', config.job.git_executable, 'string')
647
+ H.check_type('job.timeout', config.job.timeout, 'number')
648
+ if not pcall(H.normalize_split_opt, config.command.split) then
649
+ H.error('`command.split` should be one of "auto", "horizontal", "vertical", "tab"')
650
+ end
651
+
652
+ return config
653
+ end
654
+
655
+ H.apply_config = function(config) MiniGit.config = config end
656
+
657
+ H.create_autocommands = function()
658
+ local gr = vim.api.nvim_create_augroup('MiniGit', {})
659
+
660
+ local au = function(event, pattern, callback, desc)
661
+ vim.api.nvim_create_autocmd(event, { group = gr, pattern = pattern, callback = callback, desc = desc })
662
+ end
663
+
664
+ -- NOTE: Try auto enabling buffer on every `BufEnter` to not have `:edit`
665
+ -- disabling buffer, as it calls `on_detach()` from buffer watcher
666
+ au('BufEnter', '*', H.auto_enable, 'Enable Git tracking')
667
+ end
668
+
669
+ H.is_disabled = function(buf_id) return vim.g.minigit_disable == true or vim.b[buf_id or 0].minigit_disable == true end
670
+
671
+ H.create_user_commands = function()
672
+ local opts = { bang = true, nargs = '+', complete = H.command_complete, desc = 'Execute Git command' }
673
+ vim.api.nvim_create_user_command('Git', H.command_impl, opts)
674
+ end
675
+
676
+ -- Autocommands ---------------------------------------------------------------
677
+ H.auto_enable = vim.schedule_wrap(function(data)
678
+ local buf = data.buf
679
+ if not (vim.api.nvim_buf_is_loaded(buf) and vim.bo[buf].buftype == '' and vim.bo[buf].buflisted) then return end
680
+ MiniGit.enable(data.buf)
681
+ end)
682
+
683
+ -- Command --------------------------------------------------------------------
684
+ H.command_impl = function(input)
685
+ if not H.has_git then
686
+ return H.notify('There is no `' .. MiniGit.config.job.git_executable .. '` executable', 'ERROR')
687
+ end
688
+
689
+ H.ensure_git_subcommands()
690
+
691
+ -- Define Git editor to be used if needed. The way it works is: execute
692
+ -- command, wait for it to exit, use content of edited file. So to properly
693
+ -- wait for user to finish edit, start fresh headless process which opens
694
+ -- file in current session/process. It exits after the user is done editing
695
+ -- (deletes the buffer or closes the window).
696
+ H.ensure_git_editor(input.mods)
697
+ -- NOTE: use `vim.v.progpath` to have same runtime
698
+ local editor = H.cli_escape(vim.v.progpath) .. ' --clean --headless -u ' .. H.cli_escape(H.git_editor_config)
699
+
700
+ -- Setup custom environment variables for better reproducibility
701
+ local env_vars = {}
702
+ -- - Use Git related variables to use instance for editing
703
+ env_vars.GIT_EDITOR, env_vars.GIT_SEQUENCE_EDITOR, env_vars.GIT_PAGER = editor, editor, ''
704
+ -- - Make output as much machine readable as possible
705
+ env_vars.NO_COLOR, env_vars.TERM = 1, 'dumb'
706
+ local env = H.make_spawn_env(env_vars)
707
+
708
+ -- Setup spawn arguments
709
+ local args = vim.tbl_map(H.expandcmd, input.fargs)
710
+ local command = { MiniGit.config.job.git_executable, unpack(args) }
711
+ local cwd = H.get_git_cwd()
712
+
713
+ local cmd_data = { cmd_input = input, git_command = command, cwd = cwd }
714
+ local is_done_track = { done = false }
715
+ local on_done = H.command_make_on_done(cmd_data, is_done_track)
716
+
717
+ H.cli_run(command, cwd, on_done, { env = env })
718
+
719
+ -- If needed, synchronously wait for job to finish
720
+ local sync_check = function() return H.skip_sync or is_done_track.done end
721
+ if not input.bang then vim.wait(MiniGit.config.job.timeout + 10, sync_check, 1) end
722
+ end
723
+
724
+ --stylua: ignore
725
+ H.ensure_git_subcommands = function()
726
+ if H.git_subcommands ~= nil then return end
727
+ local git_subcommands = {}
728
+
729
+ -- Compute all supported commands. All 'list-' are taken from Git source
730
+ -- 'command-list.txt' file. Be so granular and not just `main,nohelpers` in
731
+ -- order to not include purely man-page worthy items (like "remote-ext").
732
+ local lists_all = {
733
+ 'list-mainporcelain',
734
+ 'list-ancillarymanipulators', 'list-ancillaryinterrogators',
735
+ 'list-foreignscminterface',
736
+ 'list-plumbingmanipulators', 'list-plumbinginterrogators',
737
+ 'others', 'alias',
738
+ }
739
+ local supported = H.git_cli_output({ '--list-cmds=' .. table.concat(lists_all, ',') })
740
+ if #supported == 0 then
741
+ -- Fall back only on basics if previous one failed for some reason
742
+ supported = {
743
+ 'add', 'bisect', 'branch', 'clone', 'commit', 'diff', 'fetch', 'grep', 'init', 'log', 'merge',
744
+ 'mv', 'pull', 'push', 'rebase', 'reset', 'restore', 'rm', 'show', 'status', 'switch', 'tag',
745
+ }
746
+ end
747
+ table.sort(supported)
748
+ git_subcommands.supported = supported
749
+
750
+ -- Compute complete list for commands by enhancing with two word commands.
751
+ -- Keep those lists manual as there is no good way to compute lazily.
752
+ local complete = vim.deepcopy(supported)
753
+ local add_twoword = function(prefix, suffixes)
754
+ if not vim.tbl_contains(supported, prefix) then return end
755
+ for _, suf in ipairs(suffixes) do table.insert(complete, prefix .. ' ' .. suf) end
756
+ end
757
+ add_twoword('bundle', { 'create', 'list-heads', 'unbundle', 'verify' })
758
+ add_twoword('bisect', { 'bad', 'good', 'log', 'replay', 'reset', 'run', 'skip', 'start', 'terms', 'view', 'visualize' })
759
+ add_twoword('commit-graph', { 'verify', 'write' })
760
+ add_twoword('maintenance', { 'run', 'start', 'stop', 'register', 'unregister' })
761
+ add_twoword('multi-pack-index', { 'expire', 'repack', 'verify', 'write' })
762
+ add_twoword('notes', { 'add', 'append', 'copy', 'edit', 'get-ref', 'list', 'merge', 'prune', 'remove', 'show' })
763
+ add_twoword('p4', { 'clone', 'rebase', 'submit', 'sync' })
764
+ add_twoword('reflog', { 'delete', 'exists', 'expire', 'show' })
765
+ add_twoword('remote', { 'add', 'get-url', 'prune', 'remove', 'rename', 'rm', 'set-branches', 'set-head', 'set-url', 'show', 'update' })
766
+ add_twoword('rerere', { 'clear', 'diff', 'forget', 'gc', 'remaining', 'status' })
767
+ add_twoword('sparse-checkout', { 'add', 'check-rules', 'disable', 'init', 'list', 'reapply', 'set' })
768
+ add_twoword('stash', { 'apply', 'branch', 'clear', 'create', 'drop', 'list', 'pop', 'save', 'show', 'store' })
769
+ add_twoword('submodule', { 'absorbgitdirs', 'add', 'deinit', 'foreach', 'init', 'set-branch', 'set-url', 'status', 'summary', 'sync', 'update' })
770
+ add_twoword('subtree', { 'add', 'merge', 'pull', 'push', 'split' })
771
+ add_twoword('worktree', { 'add', 'list', 'lock', 'move', 'prune', 'remove', 'repair', 'unlock' })
772
+ git_subcommands.complete = complete
773
+
774
+ -- Compute commands which are meant to show information. These will show CLI
775
+ -- output in separate buffer opposed to `vim.notify`.
776
+ local info_args = { '--list-cmds=list-info,list-ancillaryinterrogators,list-plumbinginterrogators' }
777
+ local info_commands = H.git_cli_output(info_args)
778
+ if #info_commands == 0 then info_commands = { 'bisect', 'diff', 'grep', 'log', 'show', 'status' } end
779
+ local info = {}
780
+ for _, cmd in ipairs(info_commands) do
781
+ info[cmd] = true
782
+ end
783
+ git_subcommands.info = info
784
+
785
+ -- Compute commands which aliases rely on
786
+ local alias_data = H.git_cli_output({ 'config', '--get-regexp', 'alias.*' })
787
+ local alias = {}
788
+ for _, l in ipairs(alias_data) do
789
+ -- Assume simple alias of the form `alias.xxx subcommand ...`
790
+ local alias_cmd, cmd = string.match(l, '^alias%.(%S+) (%S+)')
791
+ if vim.tbl_contains(supported, cmd) then alias[alias_cmd] = cmd end
792
+ end
793
+ git_subcommands.alias = alias
794
+
795
+ -- Initialize cache for command options. Initialize with `false` so that
796
+ -- actual values are computed lazily when needed for a command.
797
+ local options = { git = false }
798
+ for _, command in ipairs(supported) do
799
+ options[command] = false
800
+ end
801
+ git_subcommands.options = options
802
+
803
+ -- Cache results
804
+ H.git_subcommands = git_subcommands
805
+ end
806
+
807
+ H.ensure_git_editor = function(mods)
808
+ if H.git_editor_config == nil or not vim.fn.filereadable(H.git_editor_config) == 0 then
809
+ H.git_editor_config = vim.fn.tempname()
810
+ end
811
+
812
+ -- Create a private function responsible for editing Git file
813
+ MiniGit._edit = function(path, servername)
814
+ -- Define editor state before and after editing path
815
+ H.skip_timeout, H.skip_sync = true, true
816
+ local cleanup = function()
817
+ local _, channel = pcall(vim.fn.sockconnect, 'pipe', servername, { rpc = true })
818
+ pcall(vim.rpcnotify, channel, 'nvim_exec2', 'quitall!', {})
819
+ H.skip_timeout, H.skip_sync = false, false
820
+ end
821
+
822
+ -- Start file edit with proper modifiers in a special window
823
+ mods = H.ensure_mods_is_split(mods)
824
+ vim.cmd(mods .. ' split ' .. vim.fn.fnameescape(path))
825
+ H.define_minigit_window(cleanup)
826
+ end
827
+
828
+ -- Start editing file from first argument (as how `GIT_EDITOR` works) in
829
+ -- current instance and don't close until explicitly closed later from this
830
+ -- instance as set up in `MiniGit._edit()`
831
+ local lines = {
832
+ 'lua << EOF',
833
+ string.format('local channel = vim.fn.sockconnect("pipe", %s, { rpc = true })', vim.inspect(vim.v.servername)),
834
+ 'local ins = vim.inspect',
835
+ 'local lua_cmd = string.format("MiniGit._edit(%s, %s)", ins(vim.fn.argv(0)), ins(vim.v.servername))',
836
+ 'vim.rpcrequest(channel, "nvim_exec_lua", lua_cmd, {})',
837
+ 'EOF',
838
+ }
839
+ vim.fn.writefile(lines, H.git_editor_config)
840
+ end
841
+
842
+ H.get_git_cwd = function()
843
+ local buf_cache = H.cache[vim.api.nvim_get_current_buf()] or {}
844
+ return buf_cache.root or vim.fn.getcwd()
845
+ end
846
+
847
+ H.command_make_on_done = function(cmd_data, is_done_track)
848
+ return vim.schedule_wrap(function(code, out, err)
849
+ -- Register that command is done executing (to enable sync execution)
850
+ is_done_track.done = true
851
+
852
+ -- Trigger "done" event
853
+ cmd_data.git_subcommand = H.command_parse_subcommand(cmd_data.git_command)
854
+ cmd_data.exit_code, cmd_data.stdout, cmd_data.stderr = code, out, err
855
+ H.trigger_event('MiniGitCommandDone', cmd_data)
856
+
857
+ -- Show stderr and stdout
858
+ if H.cli_err_notify(code, out, err) then return end
859
+ H.command_show_stdout(cmd_data)
860
+
861
+ -- Ensure that all buffers are up to date (avoids "The file has been
862
+ -- changed since reading it" warning)
863
+ vim.tbl_map(function(buf_id) vim.cmd('checktime ' .. buf_id) end, vim.api.nvim_list_bufs())
864
+ end)
865
+ end
866
+
867
+ H.command_show_stdout = function(cmd_data)
868
+ local stdout, mods, subcommand = cmd_data.stdout, cmd_data.cmd_input.mods, cmd_data.git_subcommand
869
+ if stdout == '' or (mods:find('silent') ~= nil and mods:find('unsilent') == nil) then return end
870
+
871
+ -- Show in split if explicitly forced or the command shows info.
872
+ -- Use `vim.notify` otherwise.
873
+ local should_split = H.mods_is_split(mods) or H.git_subcommands.info[subcommand]
874
+ if not should_split then return H.notify(stdout, 'INFO') end
875
+
876
+ local lines = vim.split(stdout, '\n')
877
+ local name = table.concat(cmd_data.git_command, ' ')
878
+ cmd_data.win_source, cmd_data.win_stdout = H.show_in_split(mods, lines, subcommand, name)
879
+
880
+ -- Trigger "split" event
881
+ H.trigger_event('MiniGitCommandSplit', cmd_data)
882
+ end
883
+
884
+ H.command_parse_subcommand = function(command)
885
+ local res
886
+ for _, cmd in ipairs(command) do
887
+ if res == nil and vim.tbl_contains(H.git_subcommands.supported, cmd) then res = cmd end
888
+ end
889
+ return H.git_subcommands.alias[res] or res
890
+ end
891
+
892
+ H.command_complete = function(_, line, col)
893
+ -- Compute completion base manually to be "at cursor" and respect `\ `
894
+ local base = H.get_complete_base(line:sub(1, col))
895
+ local candidates, compl_type = H.command_get_complete_candidates(line, col, base)
896
+ -- Allow several "//" at the end for path completion for easier "chaining"
897
+ if compl_type == 'path' then base = base:gsub('/+$', '/') end
898
+ return vim.tbl_filter(function(x) return vim.startswith(x, base) end, candidates)
899
+ end
900
+
901
+ H.get_complete_base = function(line)
902
+ local from, _, res = line:find('(%S*)$')
903
+ while from ~= nil do
904
+ local cur_from, _, cur_res = line:sub(1, from - 1):find('(%S*\\ )$')
905
+ if cur_res ~= nil then res = cur_res .. res end
906
+ from = cur_from
907
+ end
908
+ return (res:gsub([[\ ]], ' '))
909
+ end
910
+
911
+ H.command_get_complete_candidates = function(line, col, base)
912
+ H.ensure_git_subcommands()
913
+
914
+ -- Determine current Git subcommand as the earliest present supported one
915
+ local subcmd, subcmd_end = nil, math.huge
916
+ for _, cmd in pairs(H.git_subcommands.supported) do
917
+ local _, ind = line:find(' ' .. cmd .. ' ', 1, true)
918
+ if ind ~= nil and ind < subcmd_end then
919
+ subcmd, subcmd_end = cmd, ind
920
+ end
921
+ end
922
+
923
+ subcmd = subcmd or 'git'
924
+ local cwd = H.get_git_cwd()
925
+
926
+ -- Determine command candidates:
927
+ -- - Command options if complete base starts with "-".
928
+ -- - Paths if after explicit "--".
929
+ -- - Git commands if there is none fully formed yet or cursor is at the end
930
+ -- of the command (to also suggest subcommands).
931
+ -- - Command targets specific for each command (if present).
932
+ if vim.startswith(base, '-') then return H.command_complete_option(subcmd) end
933
+ if line:sub(1, col):find(' -- ') ~= nil then return H.command_complete_path(cwd, base) end
934
+ if subcmd_end == math.huge or (subcmd_end - 1) == col then return H.git_subcommands.complete, 'subcommand' end
935
+
936
+ subcmd = H.git_subcommands.alias[subcmd] or subcmd
937
+ local complete_targets = H.command_complete_subcommand_targets[subcmd]
938
+ if complete_targets == nil then return {}, nil end
939
+ return complete_targets(cwd, base, line)
940
+ end
941
+
942
+ H.command_complete_option = function(command)
943
+ local cached_candidates = H.git_subcommands.options[command]
944
+ if cached_candidates == nil then return {} end
945
+ if type(cached_candidates) == 'table' then return cached_candidates end
946
+
947
+ -- Use alias's command to compute the options but store cache for alias
948
+ local orig_command = command
949
+ command = H.git_subcommands.alias[command] or command
950
+
951
+ -- Find command's flag options by parsing its help page. Needs a bit
952
+ -- heuristic approach and ensuring proper `git help` output (as it is done
953
+ -- through `man`), but seems to work good enough.
954
+ -- Alternative is to call command with `--git-completion-helper-all` flag (as
955
+ -- is done in bash and vim-fugitive completion). This has both pros and cons:
956
+ -- - Pros: faster; more targeted suggestions (like for two word subcommands);
957
+ -- presumably more reliable.
958
+ -- - Cons: works on smaller number of commands (for example, `rev-parse` or
959
+ -- pure `git` do not work); does not provide single dash suggestions;
960
+ -- does not work when not inside Git repo; needs recognizing two word
961
+ -- commands before asking for completion.
962
+ local env = H.make_spawn_env({ MANPAGER = 'cat', NO_COLOR = 1, PAGER = 'cat' })
963
+ local lines = H.git_cli_output({ 'help', '--man', command }, nil, env)
964
+ -- - Exit early before caching to try again later
965
+ if #lines == 0 then return {} end
966
+ -- - On some systems (like Mac), output still might contain formatting
967
+ -- sequences, like "a\ba" and "_\ba" meaning bold and italic.
968
+ -- See https://github.com/nvim-mini/mini.nvim/issues/918
969
+ lines = vim.tbl_map(function(l) return l:gsub('.\b', '') end, lines)
970
+
971
+ -- Construct non-duplicating candidates by parsing lines of help page
972
+ local candidates_map = {}
973
+
974
+ -- Options are assumed to be listed inside "OPTIONS" or "XXX OPTIONS" (like
975
+ -- "MODE OPTIONS" of `git rebase`) section on dedicated lines. Whether a line
976
+ -- contains only options is determined heuristically: it is assumed to start
977
+ -- exactly with " -" indicating proper indent for subsection start.
978
+ -- Known not parsable options:
979
+ -- - `git reset <mode>` (--soft, --hard, etc.): not listed in "OPTIONS".
980
+ -- - All -<number> options, as they are not really completable.
981
+ local is_in_options_section = false
982
+ for _, l in ipairs(lines) do
983
+ if is_in_options_section and l:find('^%u[%u ]+$') ~= nil then is_in_options_section = false end
984
+ if not is_in_options_section and l:find('^%u?[%u ]*OPTIONS$') ~= nil then is_in_options_section = true end
985
+ if is_in_options_section and l:find('^ %-') ~= nil then H.parse_options(candidates_map, l) end
986
+ end
987
+
988
+ -- Finalize candidates. Should not contain "almost duplicates".
989
+ -- Should also be sorted by relevance: short flags before regular flags.
990
+ -- Inside groups sort alphabetically ignoring case.
991
+ candidates_map['--'] = nil
992
+ for cmd, _ in pairs(candidates_map) do
993
+ -- There can be two explicitly documented options "--xxx" and "--xxx=".
994
+ -- Use only one of them (without "=").
995
+ if cmd:sub(-1, -1) == '=' and candidates_map[cmd:sub(1, -2)] ~= nil then candidates_map[cmd] = nil end
996
+ end
997
+
998
+ local res = vim.tbl_keys(candidates_map)
999
+ table.sort(res, function(a, b)
1000
+ local a2, b2 = a:sub(2, 2) == '-', b:sub(2, 2) == '-'
1001
+ if a2 and not b2 then return false end
1002
+ if not a2 and b2 then return true end
1003
+ local a_low, b_low = a:lower(), b:lower()
1004
+ return a_low < b_low or (a_low == b_low and a < b)
1005
+ end)
1006
+
1007
+ -- Cache and return
1008
+ H.git_subcommands.options[orig_command] = res
1009
+ return res, 'option'
1010
+ end
1011
+
1012
+ H.parse_options = function(map, line)
1013
+ -- Options are standalone words starting as "-xxx" or "--xxx"
1014
+ -- Include possible "=" at the end indicating mandatory value
1015
+ line:gsub('%s(%-[-%w][-%w]*=?)', function(match) map[match] = true end)
1016
+
1017
+ -- Make exceptions for commonly documented "--[no-]xxx" two options
1018
+ line:gsub('%s%-%-%[no%-%]([-%w]+=?)', function(match)
1019
+ map['--' .. match], map['--no-' .. match] = true, true
1020
+ end)
1021
+ end
1022
+
1023
+ H.command_complete_path = function(cwd, base)
1024
+ -- Treat base only as path relative to the command's cwd
1025
+ cwd = cwd:gsub('/+$', '') .. '/'
1026
+ local cwd_len = cwd:len()
1027
+
1028
+ -- List elements from (absolute) target directory
1029
+ local target_dir = vim.fn.fnamemodify(base, ':h')
1030
+ target_dir = (cwd .. target_dir:gsub('^%.$', '')):gsub('/+$', '') .. '/'
1031
+ local ok, fs_entries = pcall(vim.fn.readdir, target_dir)
1032
+ if not ok then return {} end
1033
+
1034
+ -- List directories and files separately
1035
+ local dirs, files = {}, {}
1036
+ for _, entry in ipairs(fs_entries) do
1037
+ local entry_abs = target_dir .. entry
1038
+ local arr = vim.fn.isdirectory(entry_abs) == 1 and dirs or files
1039
+ table.insert(arr, entry_abs)
1040
+ end
1041
+ dirs = vim.tbl_map(function(x) return x .. '/' end, dirs)
1042
+
1043
+ -- List ordered directories first followed by ordered files
1044
+ local order_ignore_case = function(a, b) return a:lower() < b:lower() end
1045
+ table.sort(dirs, order_ignore_case)
1046
+ table.sort(files, order_ignore_case)
1047
+
1048
+ -- Return candidates relative to command's cwd
1049
+ local all = dirs
1050
+ vim.list_extend(all, files)
1051
+ local res = vim.tbl_map(function(x) return x:sub(cwd_len + 1) end, all)
1052
+ return res, 'path'
1053
+ end
1054
+
1055
+ H.command_complete_pullpush = function(cwd, _, line)
1056
+ -- Suggest remotes at `Git push |` and `Git push or|`, otherwise - references
1057
+ -- Ignore options when deciding which suggestion to compute
1058
+ local _, n_words = line:gsub(' (%-%S+)', ''):gsub('%S+ ', '')
1059
+ if n_words <= 2 then return H.git_cli_output({ 'remote' }, cwd), 'remote' end
1060
+ return H.git_cli_output({ 'rev-parse', '--symbolic', '--branches', '--tags' }, cwd), 'ref'
1061
+ end
1062
+
1063
+ H.make_git_cli_complete = function(args, complete_type)
1064
+ return function(cwd, _) return H.git_cli_output(args, cwd), complete_type end
1065
+ end
1066
+
1067
+ -- Cover at least all subcommands listed in `git help`
1068
+ --stylua: ignore
1069
+ H.command_complete_subcommand_targets = {
1070
+ -- clone - no targets
1071
+ -- init - no targets
1072
+
1073
+ -- Worktree
1074
+ add = H.command_complete_path,
1075
+ mv = H.command_complete_path,
1076
+ restore = H.command_complete_path,
1077
+ rm = H.command_complete_path,
1078
+
1079
+ -- Examine history
1080
+ -- bisect - no targets
1081
+ diff = H.command_complete_path,
1082
+ grep = H.command_complete_path,
1083
+ log = H.make_git_cli_complete({ 'rev-parse', '--symbolic', '--branches', '--tags' }, 'ref'),
1084
+ show = H.make_git_cli_complete({ 'rev-parse', '--symbolic', '--branches', '--tags' }, 'ref'),
1085
+ -- status - no targets
1086
+
1087
+ -- Modify history
1088
+ branch = H.make_git_cli_complete({ 'rev-parse', '--symbolic', '--branches' }, 'branch'),
1089
+ commit = H.command_complete_path,
1090
+ merge = H.make_git_cli_complete({ 'rev-parse', '--symbolic', '--branches' }, 'branch'),
1091
+ rebase = H.make_git_cli_complete({ 'rev-parse', '--symbolic', '--branches' }, 'branch'),
1092
+ reset = H.make_git_cli_complete({ 'rev-parse', '--symbolic', '--branches', '--tags' }, 'ref'),
1093
+ switch = H.make_git_cli_complete({ 'rev-parse', '--symbolic', '--branches' }, 'branch'),
1094
+ tag = H.make_git_cli_complete({ 'rev-parse', '--symbolic', '--tags' }, 'tag'),
1095
+
1096
+ -- Collaborate
1097
+ fetch = H.make_git_cli_complete({ 'remote' }, 'remote'),
1098
+ push = H.command_complete_pullpush,
1099
+ pull = H.command_complete_pullpush,
1100
+
1101
+ -- Miscellaneous
1102
+ checkout = H.make_git_cli_complete({ 'rev-parse', '--symbolic', '--branches', '--tags', '--remotes' }, 'checkout'),
1103
+ config = H.make_git_cli_complete({ 'help', '--config-for-completion' }, 'config'),
1104
+ help = function()
1105
+ local res = { 'git', 'everyday' }
1106
+ vim.list_extend(res, H.git_subcommands.supported)
1107
+ return res, 'help'
1108
+ end,
1109
+ }
1110
+
1111
+ H.ensure_mods_is_split = function(mods)
1112
+ if not H.mods_is_split(mods) then
1113
+ local split_val = H.normalize_split_opt(MiniGit.config.command.split, '`config.command.split`')
1114
+ mods = split_val .. ' ' .. mods
1115
+ end
1116
+ return mods
1117
+ end
1118
+
1119
+ -- NOTE: `mods` is already expanded, so this also covers abbreviated mods
1120
+ H.mods_is_split = function(mods) return mods:find('vertical') or mods:find('horizontal') or mods:find('tab') end
1121
+
1122
+ -- Show stdout ----------------------------------------------------------------
1123
+ H.show_in_split = function(mods, lines, subcmd, name)
1124
+ -- Create a target window split
1125
+ mods = H.ensure_mods_is_split(mods)
1126
+ local win_source = vim.api.nvim_get_current_win()
1127
+ vim.cmd(mods .. ' split')
1128
+ local win_stdout = vim.api.nvim_get_current_win()
1129
+
1130
+ -- Prepare buffer
1131
+ local buf_id = vim.api.nvim_create_buf(false, true)
1132
+ H.set_buf_name(buf_id, name)
1133
+ vim.api.nvim_buf_set_lines(buf_id, 0, -1, false, lines)
1134
+
1135
+ vim.api.nvim_set_current_buf(buf_id)
1136
+ H.define_minigit_window()
1137
+
1138
+ -- NOTE: set filetype when buffer is in window to allow setting window-local
1139
+ -- options in autocommands for `FileType` events
1140
+ local filetype
1141
+ if subcmd == 'diff' then filetype = 'diff' end
1142
+ if subcmd == 'log' or subcmd == 'blame' then filetype = 'git' end
1143
+ if subcmd == 'show' then
1144
+ -- Try detecting 'git' filetype by content first, as filetype detection can
1145
+ -- rely on the buffer name (i.e. command) having proper extension. It isn't
1146
+ -- good for cases like `:Git show HEAD file.lua` (which should be 'git').
1147
+ local l = lines[1]
1148
+ local is_diff = l:find(string.rep('%x', 40)) or l:find('ref:')
1149
+ filetype = is_diff and 'git' or vim.filetype.match({ buf = buf_id })
1150
+ end
1151
+
1152
+ local has_filetype = not (filetype == nil or filetype == '')
1153
+ if has_filetype then vim.bo[buf_id].filetype = filetype end
1154
+
1155
+ -- Completely unfold for no filetype output (like `:Git help`)
1156
+ if not has_filetype then vim.wo[win_stdout].foldlevel = 999 end
1157
+
1158
+ return win_source, win_stdout
1159
+ end
1160
+
1161
+ H.define_minigit_window = function(cleanup)
1162
+ local buf_id, win_id = vim.api.nvim_get_current_buf(), vim.api.nvim_get_current_win()
1163
+ vim.bo.swapfile, vim.bo.buflisted = false, false
1164
+
1165
+ -- Define action to finish editing Git related file
1166
+ local finish_au_id
1167
+ local finish = function(data)
1168
+ local should_close = data.buf == buf_id or (data.event == 'WinClosed' and tonumber(data.match) == win_id)
1169
+ if not should_close then return end
1170
+
1171
+ pcall(vim.api.nvim_del_autocmd, finish_au_id)
1172
+ pcall(vim.api.nvim_win_close, win_id, true)
1173
+ vim.schedule(function() pcall(vim.api.nvim_buf_delete, buf_id, { force = true }) end)
1174
+
1175
+ if vim.is_callable(cleanup) then vim.schedule(cleanup) end
1176
+ end
1177
+ -- - Use `nested` to allow other events (`WinEnter` for 'mini.statusline')
1178
+ local events = { 'WinClosed', 'BufDelete', 'BufWipeout', 'VimLeave' }
1179
+ local opts = { nested = true, callback = finish, desc = 'Cleanup window and buffer' }
1180
+ finish_au_id = vim.api.nvim_create_autocmd(events, opts)
1181
+ end
1182
+
1183
+ H.git_cli_output = function(args, cwd, env)
1184
+ if cwd ~= nil and (vim.fn.isdirectory(cwd) ~= 1 or cwd == '') then return {} end
1185
+ local command = { MiniGit.config.job.git_executable, '--no-pager', unpack(args) }
1186
+ local res = H.cli_run(command, cwd, nil, { env = env }).out
1187
+ if res == '' then return {} end
1188
+ return vim.split(res, '\n')
1189
+ end
1190
+
1191
+ -- Validators -----------------------------------------------------------------
1192
+ H.validate_buf_id = function(x)
1193
+ if x == nil or x == 0 then return vim.api.nvim_get_current_buf() end
1194
+ if not (type(x) == 'number' and vim.api.nvim_buf_is_valid(x)) then
1195
+ H.error('`buf_id` should be `nil` or valid buffer id.')
1196
+ end
1197
+ return x
1198
+ end
1199
+
1200
+ H.normalize_split_opt = function(x, x_name)
1201
+ if x == 'auto' then
1202
+ -- Show in same tabpage if only minigit buffers visible. Otherwise in new.
1203
+ for _, win_id in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
1204
+ local win_buf_id = vim.api.nvim_win_get_buf(win_id)
1205
+ local win_buf_name = vim.api.nvim_buf_get_name(win_buf_id)
1206
+ local is_minigit_win = win_buf_name:find('^minigit://%d+/') ~= nil
1207
+ local is_normal_win = vim.api.nvim_win_get_config(win_id).relative == ''
1208
+ if not is_minigit_win and is_normal_win then return 'tab' end
1209
+ end
1210
+ return 'vertical'
1211
+ end
1212
+ if x == 'horizontal' or x == 'vertical' or x == 'tab' then return x end
1213
+ H.error('`' .. x_name .. '` should be one of "auto", "horizontal", "vertical", "tab"')
1214
+ end
1215
+
1216
+ H.normalize_range_lines = function(line_start, line_end)
1217
+ if line_start == nil and line_end == nil then
1218
+ line_start = vim.fn.line('.')
1219
+ local is_visual = vim.tbl_contains({ 'v', 'V', '\22' }, vim.fn.mode())
1220
+ line_end = is_visual and vim.fn.line('v') or vim.fn.line('.')
1221
+ line_start, line_end = math.min(line_start, line_end), math.max(line_start, line_end)
1222
+ end
1223
+
1224
+ if not (type(line_start) == 'number' and type(line_end) == 'number' and line_start <= line_end) then
1225
+ H.error('`line_start` and `line_end` should be non-decreasing numbers.')
1226
+ end
1227
+ return line_start, line_end
1228
+ end
1229
+
1230
+ -- Enabling -------------------------------------------------------------------
1231
+ H.is_buf_enabled = function(buf_id) return H.cache[buf_id] ~= nil and vim.api.nvim_buf_is_valid(buf_id) end
1232
+
1233
+ H.setup_buf_behavior = function(buf_id)
1234
+ local augroup = vim.api.nvim_create_augroup('MiniGitBuffer' .. buf_id, { clear = true })
1235
+ H.cache[buf_id].augroup = augroup
1236
+
1237
+ vim.api.nvim_buf_attach(buf_id, false, {
1238
+ -- Called when buffer content is changed outside of current session
1239
+ -- Needed as otherwise `on_detach()` is called without later auto enabling
1240
+ on_reload = function()
1241
+ local buf_cache = H.cache[buf_id]
1242
+ if buf_cache == nil or buf_cache.root == nil then return end
1243
+ -- Don't update repo/root as it is tracked in 'BufFilePost' autocommand
1244
+ H.update_git_head(buf_cache.root, { buf_id })
1245
+ H.update_git_in_progress(buf_cache.repo, { buf_id })
1246
+ -- Don't update status as it is tracked in file watcher
1247
+ end,
1248
+
1249
+ -- Called when buffer is unloaded from memory (`:h nvim_buf_detach_event`),
1250
+ -- **including** `:edit` command. Together with auto enabling it makes
1251
+ -- `:edit` command serve as "restart".
1252
+ on_detach = function() MiniGit.disable(buf_id) end,
1253
+ })
1254
+
1255
+ local reset_if_enabled = vim.schedule_wrap(function(data)
1256
+ if not H.is_buf_enabled(data.buf) then return end
1257
+ MiniGit.disable(data.buf)
1258
+ MiniGit.enable(data.buf)
1259
+ end)
1260
+ local bufrename_opts = { group = augroup, buffer = buf_id, callback = reset_if_enabled, desc = 'Reset on rename' }
1261
+ -- NOTE: `BufFilePost` does not look like a proper event, but it (yet) works
1262
+ vim.api.nvim_create_autocmd('BufFilePost', bufrename_opts)
1263
+
1264
+ local buf_disable = function() MiniGit.disable(buf_id) end
1265
+ local bufdelete_opts = { group = augroup, buffer = buf_id, callback = buf_disable, desc = 'Disable on delete' }
1266
+ vim.api.nvim_create_autocmd('BufDelete', bufdelete_opts)
1267
+ end
1268
+
1269
+ -- Tracking -------------------------------------------------------------------
1270
+ H.start_tracking = function(buf_id, path)
1271
+ local command = H.git_cmd({ 'rev-parse', '--path-format=absolute', '--git-dir', '--show-toplevel' })
1272
+
1273
+ -- If path is not in Git, disable buffer but make sure that it will not try
1274
+ -- to re-attach until buffer is properly disabled
1275
+ local on_not_in_git = function()
1276
+ if H.is_buf_enabled(buf_id) then MiniGit.disable(buf_id) end
1277
+ H.cache[buf_id] = {}
1278
+ end
1279
+
1280
+ local on_done = vim.schedule_wrap(function(code, out, err)
1281
+ -- Watch git directory only if there was no error retrieving path to it
1282
+ if code ~= 0 then return on_not_in_git() end
1283
+ H.cli_err_notify(code, out, err)
1284
+
1285
+ -- Update buf data
1286
+ local repo, root = string.match(out, '^(.-)\n(.*)$')
1287
+ if repo == nil or root == nil then return H.notify('No initial data for buffer ' .. buf_id, 'WARN') end
1288
+ H.update_buf_data(buf_id, { repo = repo, root = root })
1289
+
1290
+ -- Set up repo watching to react to Git index changes
1291
+ H.setup_repo_watch(buf_id, repo)
1292
+
1293
+ -- Set up worktree watching to react to file changes
1294
+ H.setup_path_watch(buf_id)
1295
+
1296
+ -- Immediately update buffer tracking data
1297
+ H.update_git_head(root, { buf_id })
1298
+ H.update_git_in_progress(repo, { buf_id })
1299
+ H.update_git_status(root, { buf_id })
1300
+ end)
1301
+
1302
+ H.cli_run(command, vim.fn.fnamemodify(path, ':h'), on_done)
1303
+ end
1304
+
1305
+ H.setup_repo_watch = function(buf_id, repo)
1306
+ local repo_cache = H.repos[repo] or {}
1307
+
1308
+ -- Ensure repo is watched
1309
+ local is_set_up = repo_cache.fs_event ~= nil and repo_cache.fs_event:is_active()
1310
+ if not is_set_up then
1311
+ H.teardown_repo_watch(repo)
1312
+ local fs_event, timer = vim.loop.new_fs_event(), vim.loop.new_timer()
1313
+
1314
+ local on_change = vim.schedule_wrap(function() H.on_repo_change(repo) end)
1315
+ local watch = function(_, filename, _)
1316
+ -- Ignore temporary changes
1317
+ if vim.endswith(filename or '', 'lock') then return end
1318
+
1319
+ -- Debounce to not overload during incremental staging (like in script)
1320
+ timer:stop()
1321
+ timer:start(50, 0, on_change)
1322
+ end
1323
+ -- Watch only '.git' dir (non-recursively), as this seems to be both enough
1324
+ -- and not supported by libuv (`recursive` flag does nothing,
1325
+ -- see https://github.com/libuv/libuv/issues/1778)
1326
+ fs_event:start(repo, {}, watch)
1327
+
1328
+ repo_cache.fs_event, repo_cache.timer = fs_event, timer
1329
+ H.repos[repo] = repo_cache
1330
+ end
1331
+
1332
+ -- Register buffer to be updated on repo change
1333
+ local repo_buffers = repo_cache.buffers or {}
1334
+ repo_buffers[buf_id] = true
1335
+ repo_cache.buffers = repo_buffers
1336
+ end
1337
+
1338
+ H.teardown_repo_watch = function(repo)
1339
+ if H.repos[repo] == nil then return end
1340
+ pcall(vim.loop.fs_event_stop, H.repos[repo].fs_event)
1341
+ pcall(vim.loop.timer_stop, H.repos[repo].timer)
1342
+ end
1343
+
1344
+ H.setup_path_watch = function(buf_id, repo)
1345
+ if not H.is_buf_enabled(buf_id) then return end
1346
+
1347
+ local on_file_change = function(data) H.update_git_status(H.cache[buf_id].root, { buf_id }) end
1348
+ local opts =
1349
+ { desc = 'Update Git status', group = H.cache[buf_id].augroup, buffer = buf_id, callback = on_file_change }
1350
+ vim.api.nvim_create_autocmd({ 'BufWritePost', 'FileChangedShellPost' }, opts)
1351
+ end
1352
+
1353
+ H.on_repo_change = function(repo)
1354
+ if H.repos[repo] == nil then return end
1355
+
1356
+ -- Collect repo's worktrees with their buffers while doing cleanup
1357
+ local repo_bufs, root_bufs = H.repos[repo].buffers, {}
1358
+ for buf_id, _ in pairs(repo_bufs) do
1359
+ if H.is_buf_enabled(buf_id) then
1360
+ local root = H.cache[buf_id].root
1361
+ local bufs = root_bufs[root] or {}
1362
+ table.insert(bufs, buf_id)
1363
+ root_bufs[root] = bufs
1364
+ else
1365
+ repo_bufs[buf_id] = nil
1366
+ MiniGit.disable(buf_id)
1367
+ end
1368
+ end
1369
+
1370
+ -- Update Git data
1371
+ H.update_git_in_progress(repo, vim.tbl_keys(repo_bufs))
1372
+ for root, bufs in pairs(root_bufs) do
1373
+ H.update_git_head(root, bufs)
1374
+ -- Status could have also changed as it depends on the index
1375
+ H.update_git_status(root, bufs)
1376
+ end
1377
+ end
1378
+
1379
+ H.update_git_head = function(root, bufs)
1380
+ local command = H.git_cmd({ 'rev-parse', 'HEAD', '--abbrev-ref', 'HEAD' })
1381
+
1382
+ local on_done = vim.schedule_wrap(function(code, out, err)
1383
+ -- Ensure proper data
1384
+ if code ~= 0 then return end
1385
+ H.cli_err_notify(code, out, err)
1386
+
1387
+ local head, head_name = string.match(out, '^(.-)\n(.*)$')
1388
+ if head == nil or head_name == nil then
1389
+ return H.notify('Could not parse HEAD data for root ' .. root .. '\n' .. out, 'WARN')
1390
+ end
1391
+
1392
+ -- Update data for all buffers from target `root`
1393
+ local new_data = { head = head, head_name = head_name }
1394
+ for _, buf_id in ipairs(bufs) do
1395
+ H.update_buf_data(buf_id, new_data)
1396
+ end
1397
+
1398
+ -- Redraw statusline to have possible statusline component up to date
1399
+ H.redrawstatus()
1400
+ end)
1401
+
1402
+ H.cli_run(command, root, on_done)
1403
+ end
1404
+
1405
+ H.update_git_in_progress = function(repo, bufs)
1406
+ -- Get data about what process is in progress
1407
+ local in_progress = {}
1408
+ if H.is_fs_present(repo .. '/BISECT_LOG') then table.insert(in_progress, 'bisect') end
1409
+ if H.is_fs_present(repo .. '/CHERRY_PICK_HEAD') then table.insert(in_progress, 'cherry-pick') end
1410
+ if H.is_fs_present(repo .. '/MERGE_HEAD') then table.insert(in_progress, 'merge') end
1411
+ if H.is_fs_present(repo .. '/REVERT_HEAD') then table.insert(in_progress, 'revert') end
1412
+ if H.is_fs_present(repo .. '/rebase-apply') then table.insert(in_progress, 'apply') end
1413
+ if H.is_fs_present(repo .. '/rebase-merge') then table.insert(in_progress, 'rebase') end
1414
+
1415
+ -- Update data for all buffers from target `root`
1416
+ local new_data = { in_progress = table.concat(in_progress, ',') }
1417
+ for _, buf_id in ipairs(bufs) do
1418
+ H.update_buf_data(buf_id, new_data)
1419
+ end
1420
+
1421
+ -- Redraw statusline to have possible statusline component up to date
1422
+ H.redrawstatus()
1423
+ end
1424
+
1425
+ H.update_git_status = function(root, bufs)
1426
+ --stylua: ignore
1427
+ local command = H.git_cmd({
1428
+ -- NOTE: Use `--no-optional-locks` to reduce conflicts with other Git tasks
1429
+ '--no-optional-locks', 'status',
1430
+ '--verbose', '--untracked-files=all', '--ignored', '--porcelain', '-z',
1431
+ '--',
1432
+ })
1433
+ local root_len, path_data = string.len(root), {}
1434
+ for _, buf_id in ipairs(bufs) do
1435
+ -- Use paths relative to the root as in `git status --porcelain` output
1436
+ local rel_path = H.get_buf_realpath(buf_id):sub(root_len + 2)
1437
+ table.insert(command, rel_path)
1438
+ -- Completely not modified paths should be the only ones missing in the
1439
+ -- output. Use this status as default.
1440
+ path_data[rel_path] = { status = ' ', buf_id = buf_id }
1441
+ end
1442
+
1443
+ local on_done = vim.schedule_wrap(function(code, out, err)
1444
+ if code ~= 0 then return end
1445
+ H.cli_err_notify(code, out, err)
1446
+
1447
+ -- Parse CLI output, which is separated by `\0` to not escape "bad" paths
1448
+ for _, l in ipairs(vim.split(out, '\0')) do
1449
+ local status, rel_path = string.match(l, '^(..) (.*)$')
1450
+ if path_data[rel_path] ~= nil then path_data[rel_path].status = status end
1451
+ end
1452
+
1453
+ -- Update data for all buffers
1454
+ for _, data in pairs(path_data) do
1455
+ local new_data = { status = data.status }
1456
+ H.update_buf_data(data.buf_id, new_data)
1457
+ end
1458
+
1459
+ -- Redraw statusline to have possible statusline component up to date
1460
+ H.redrawstatus()
1461
+ end)
1462
+
1463
+ H.cli_run(command, root, on_done)
1464
+ end
1465
+
1466
+ H.update_buf_data = function(buf_id, new_data)
1467
+ if not H.is_buf_enabled(buf_id) then return end
1468
+
1469
+ local summary = vim.b[buf_id].minigit_summary or {}
1470
+ for key, val in pairs(new_data) do
1471
+ H.cache[buf_id][key], summary[key] = val, val
1472
+ end
1473
+ vim.b[buf_id].minigit_summary = summary
1474
+
1475
+ -- Format summary string
1476
+ local head = summary.head_name or ''
1477
+ head = head == 'HEAD' and summary.head:sub(1, 7) or head
1478
+
1479
+ local in_progress = summary.in_progress or ''
1480
+ if in_progress ~= '' then head = head .. '|' .. in_progress end
1481
+
1482
+ local summary_string = head
1483
+ local status = summary.status or ''
1484
+ if status ~= ' ' and status ~= '' then summary_string = string.format('%s (%s)', head, status) end
1485
+ vim.b[buf_id].minigit_summary_string = summary_string
1486
+
1487
+ -- Trigger dedicated event with target current buffer (for proper `data.buf`)
1488
+ vim.api.nvim_buf_call(buf_id, function() H.trigger_event('MiniGitUpdated') end)
1489
+ end
1490
+
1491
+ -- History navigation ---------------------------------------------------------
1492
+ -- Assuming buffer contains unified combined diff (with "commit" header),
1493
+ -- compute path, line number, and commit of both "before" and "after" files.
1494
+ -- Allow cursor to be between "--- a/xxx" line and last line of a hunk.
1495
+ H.diff_pos_to_source = function()
1496
+ local lines, lnum = vim.api.nvim_buf_get_lines(0, 0, -1, false), vim.fn.line('.')
1497
+
1498
+ local res = { init_prefix = lines[lnum]:sub(1, 1) }
1499
+ local paths_lnum = H.diff_parse_paths(res, lines, lnum)
1500
+ local hunk_lnum = H.diff_parse_hunk(res, lines, lnum)
1501
+ local commit_lnum = H.diff_parse_commits(res, lines, lnum)
1502
+
1503
+ -- Try fall back to inferring target commits from 'mini.git' buffer name
1504
+ if res.commit_before == nil or res.commit_after == nil then H.diff_parse_bufname(res) end
1505
+
1506
+ local all_present = (res.lnum_before and res.path_before and res.commit_before)
1507
+ or (res.lnum_after and res.path_after and res.commit_after)
1508
+ local is_in_order = commit_lnum <= paths_lnum and paths_lnum <= hunk_lnum
1509
+ if not (all_present and is_in_order) then return nil end
1510
+
1511
+ return res
1512
+ end
1513
+
1514
+ H.diff_parse_paths = function(out, lines, lnum)
1515
+ -- NOTE: with `diff.mnemonicPrefix=true` source and destination prefixes can
1516
+ -- be not only `a`/`b`, but other characters or none (if added/deleted file)
1517
+ local pattern_before, pattern_after = '^%-%-%- ([acio]?)/(.*)$', '^%+%+%+ ([biw]?)/(.*)$'
1518
+
1519
+ -- Allow placing cursor directly on path defining lines
1520
+ local cur_line = lines[lnum]
1521
+ local prefix_before, path_before = string.match(cur_line, pattern_before)
1522
+ local prefix_after, path_after = string.match(cur_line, pattern_after)
1523
+ if path_before ~= nil then
1524
+ out.path_before = path_before
1525
+ prefix_after, out.path_after = string.match(lines[lnum + 1] or '', pattern_after)
1526
+ out.lnum_before, out.lnum_after = 1, 1
1527
+ elseif path_after ~= nil then
1528
+ prefix_before, out.path_before = string.match(lines[lnum - 1] or '', pattern_before)
1529
+ out.path_after = path_after
1530
+ out.lnum_before, out.lnum_after = 1, 1
1531
+ else
1532
+ -- Iterate lines upward to find path patterns
1533
+ while out.path_after == nil and lnum > 0 do
1534
+ prefix_after, out.path_after = string.match(lines[lnum] or '', pattern_after)
1535
+ lnum = lnum - 1
1536
+ end
1537
+ prefix_before, out.path_before = string.match(lines[lnum] or '', pattern_before)
1538
+ end
1539
+
1540
+ -- - Don't treat '--- /dev/null' and '+++ /dev/null' matches as paths
1541
+ -- Need to check prefix to work in cases like '--- a/dev/null'
1542
+ if prefix_before == '' then out.path_before = nil end
1543
+ if prefix_after == '' then out.path_after = nil end
1544
+
1545
+ return lnum
1546
+ end
1547
+
1548
+ H.diff_parse_hunk = function(out, lines, lnum)
1549
+ if out.lnum_after ~= nil then return lnum end
1550
+
1551
+ local offsets = { [' '] = 0, ['-'] = 0, ['+'] = 0 }
1552
+ while lnum > 0 do
1553
+ local prefix = lines[lnum]:sub(1, 1)
1554
+ if not (prefix == ' ' or prefix == '-' or prefix == '+') then break end
1555
+ offsets[prefix] = offsets[prefix] + 1
1556
+ lnum = lnum - 1
1557
+ end
1558
+
1559
+ local hunk_start_before, hunk_start_after = string.match(lines[lnum] or '', '^@@ %-(%d+),?%d* %+(%d+),?%d* @@')
1560
+ if hunk_start_before ~= nil then
1561
+ out.lnum_before = math.max(1, tonumber(hunk_start_before) + offsets[' '] + offsets['-'] - 1)
1562
+ out.lnum_after = math.max(1, tonumber(hunk_start_after) + offsets[' '] + offsets['+'] - 1)
1563
+ end
1564
+ return lnum
1565
+ end
1566
+
1567
+ H.diff_parse_commits = function(out, lines, lnum)
1568
+ while out.commit_after == nil and lnum > 0 do
1569
+ out.commit_after = string.match(lines[lnum], '^commit (%x+)$')
1570
+ lnum = lnum - 1
1571
+ end
1572
+ if out.commit_after ~= nil then out.commit_before = out.commit_after .. '~' end
1573
+ return lnum + 1
1574
+ end
1575
+
1576
+ H.diff_parse_bufname = function(out)
1577
+ local buf_name = vim.api.nvim_buf_get_name(0)
1578
+ local diff_command = string.match(buf_name, '^minigit://%d+/.* diff ?(.*)$')
1579
+ if diff_command == nil then return end
1580
+
1581
+ -- Work with output of common `:Git diff` commands
1582
+ diff_command = vim.trim(diff_command)
1583
+ -- `Git diff` - compares index and work tree
1584
+ if diff_command == '' then
1585
+ out.commit_before, out.commit_after = ':0', true
1586
+ end
1587
+ -- `Git diff --cached` - compares HEAD and index
1588
+ if diff_command == '--cached' then
1589
+ out.commit_before, out.commit_after = 'HEAD', ':0'
1590
+ end
1591
+ -- `Git diff HEAD` - compares commit and work tree
1592
+ if diff_command:find('^[^-]%S*$') ~= nil then
1593
+ out.commit_before, out.commit_after = diff_command, true
1594
+ end
1595
+ end
1596
+
1597
+ H.parse_diff_source_buf_name = function(buf_name) return string.match(buf_name, '^minigit://%d+/.*show (%x+~?):(.*)$') end
1598
+
1599
+ H.deps_pos_to_source = function()
1600
+ local lines = vim.api.nvim_buf_get_lines(0, 0, vim.fn.line('.'), false)
1601
+ -- Do nothing if on the title (otherwise it operates on previous plugin info)
1602
+ if lines[#lines]:find('^[%+%-!]') ~= nil then return end
1603
+
1604
+ -- Locate lines with commit and repo path data
1605
+ local commit, commit_lnum = nil, #lines
1606
+ while commit == nil and commit_lnum >= 1 do
1607
+ local l = lines[commit_lnum]
1608
+ commit = l:match('^[><] (%x%x%x%x%x%x%x%x*) |') or l:match('^State[^:]*: %s*(%x+)')
1609
+ commit_lnum = commit_lnum - 1
1610
+ end
1611
+
1612
+ local cwd, cwd_lnum = nil, #lines
1613
+ while cwd == nil and cwd_lnum >= 1 do
1614
+ cwd, cwd_lnum = lines[cwd_lnum]:match('^Path: %s*(%S+)$'), cwd_lnum - 1
1615
+ end
1616
+
1617
+ -- Do nothing if something is not found or path corresponds to next repo
1618
+ if commit == nil or cwd == nil or commit_lnum <= cwd_lnum then return end
1619
+ return commit, cwd
1620
+ end
1621
+
1622
+ -- Folding --------------------------------------------------------------------
1623
+ H.is_hunk_header = function(lnum) return vim.fn.getline(lnum):find('^@@.*@@') ~= nil end
1624
+
1625
+ H.is_log_entry_header = function(lnum) return vim.fn.getline(lnum):find('^commit ') ~= nil end
1626
+
1627
+ H.is_file_entry_header = function(lnum) return vim.fn.getline(lnum):find('^diff %-%-git') ~= nil end
1628
+
1629
+ -- CLI ------------------------------------------------------------------------
1630
+ H.git_cmd = function(args)
1631
+ -- Use '-c gc.auto=0' to disable `stderr` "Auto packing..." messages
1632
+ return { MiniGit.config.job.git_executable, '-c', 'gc.auto=0', unpack(args) }
1633
+ end
1634
+
1635
+ H.make_spawn_env = function(env_vars)
1636
+ -- Setup all environment variables (`vim.loop.spawn()` by default has none)
1637
+ local environ = vim.tbl_deep_extend('force', vim.loop.os_environ(), env_vars)
1638
+ local res = {}
1639
+ for k, v in pairs(environ) do
1640
+ table.insert(res, string.format('%s=%s', k, tostring(v)))
1641
+ end
1642
+ return res
1643
+ end
1644
+
1645
+ H.cli_run = function(command, cwd, on_done, opts)
1646
+ local spawn_opts = opts or {}
1647
+ local executable, args = command[1], vim.list_slice(command, 2, #command)
1648
+ local process, stdout, stderr = nil, vim.loop.new_pipe(), vim.loop.new_pipe()
1649
+ spawn_opts.args, spawn_opts.cwd, spawn_opts.stdio = args, cwd or vim.fn.getcwd(), { nil, stdout, stderr }
1650
+
1651
+ -- Allow `on_done = nil` to mean synchronous execution
1652
+ local is_sync, res = false, nil
1653
+ if on_done == nil then
1654
+ is_sync = true
1655
+ on_done = function(code, out, err) res = { code = code, out = out, err = err } end
1656
+ end
1657
+
1658
+ local out, err, is_done = {}, {}, false
1659
+ local on_exit = function(code)
1660
+ -- Ensure calling this only once
1661
+ if is_done then return end
1662
+ is_done = true
1663
+
1664
+ if process:is_closing() then return end
1665
+ process:close()
1666
+
1667
+ -- Convert to strings appropriate for notifications
1668
+ out = H.cli_stream_tostring(out)
1669
+ err = H.cli_stream_tostring(err):gsub('\r+', '\n'):gsub('\n%s+\n', '\n\n')
1670
+ on_done(code, out, err)
1671
+ end
1672
+
1673
+ process = vim.loop.spawn(executable, spawn_opts, on_exit)
1674
+ H.cli_read_stream(stdout, out)
1675
+ H.cli_read_stream(stderr, err)
1676
+ vim.defer_fn(function()
1677
+ if H.skip_timeout or not process:is_active() then return end
1678
+ H.notify('PROCESS REACHED TIMEOUT', 'WARN')
1679
+ on_exit(1)
1680
+ end, MiniGit.config.job.timeout)
1681
+
1682
+ if is_sync then vim.wait(MiniGit.config.job.timeout + 10, function() return is_done end, 1) end
1683
+ return res
1684
+ end
1685
+
1686
+ H.cli_read_stream = function(stream, feed)
1687
+ local callback = function(err, data)
1688
+ if err then return table.insert(feed, 1, 'ERROR: ' .. err) end
1689
+ if data ~= nil then return table.insert(feed, data) end
1690
+ stream:close()
1691
+ end
1692
+ stream:read_start(callback)
1693
+ end
1694
+
1695
+ H.cli_stream_tostring = function(stream) return (table.concat(stream):gsub('\n+$', '')) end
1696
+
1697
+ H.cli_err_notify = function(code, out, err)
1698
+ local should_stop = code ~= 0
1699
+ if should_stop then H.notify(err .. (out == '' and '' or ('\n' .. out)), 'ERROR') end
1700
+ if not should_stop and err ~= '' then H.notify(err, 'WARN') end
1701
+ return should_stop
1702
+ end
1703
+
1704
+ H.cli_escape = function(x) return (string.gsub(x, '([ \\])', '\\%1')) end
1705
+
1706
+ -- Utilities ------------------------------------------------------------------
1707
+ H.error = function(msg) error('(mini.git) ' .. msg, 0) end
1708
+
1709
+ H.check_type = function(name, val, ref, allow_nil)
1710
+ if type(val) == ref or (ref == 'callable' and vim.is_callable(val)) or (allow_nil and val == nil) then return end
1711
+ H.error(string.format('`%s` should be %s, not %s', name, ref, type(val)))
1712
+ end
1713
+
1714
+ H.set_buf_name = function(buf_id, name) vim.api.nvim_buf_set_name(buf_id, 'minigit://' .. buf_id .. '/' .. name) end
1715
+
1716
+ H.notify = function(msg, level_name) vim.notify('(mini.git) ' .. msg, vim.log.levels[level_name]) end
1717
+
1718
+ H.trigger_event = function(event_name, data) vim.api.nvim_exec_autocmds('User', { pattern = event_name, data = data }) end
1719
+
1720
+ H.is_fs_present = function(path) return vim.loop.fs_stat(path) ~= nil end
1721
+
1722
+ H.expandcmd = function(x)
1723
+ if x == '<cwd>' then return vim.fn.getcwd() end
1724
+ local ok, res = pcall(vim.fn.expandcmd, x)
1725
+ return ok and res or x
1726
+ end
1727
+
1728
+ -- TODO: Remove after compatibility with Neovim=0.9 is dropped
1729
+ H.islist = vim.fn.has('nvim-0.10') == 1 and vim.islist or vim.tbl_islist
1730
+
1731
+ -- Try getting buffer's full real path (after resolving symlinks)
1732
+ H.get_buf_realpath = function(buf_id) return vim.loop.fs_realpath(vim.api.nvim_buf_get_name(buf_id)) or '' end
1733
+
1734
+ H.redrawstatus = function() vim.cmd('redrawstatus') end
1735
+ if vim.api.nvim__redraw ~= nil then H.redrawstatus = function() vim.api.nvim__redraw({ statusline = true }) end end
1736
+
1737
+ return MiniGit