init-git.el 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. ;;; init-git.el --- VCS/Git Configuration File -*- lexical-binding: t -*-
  2. ;;; Commentary:
  3. ;;; Code:
  4. (use-feature ediff
  5. :custom
  6. (ediff-setup-windows-plain 'ediff-setup-windows-plain))
  7. (use-package diff-hl
  8. :custom
  9. (diff-hl-flydiff-mode t)
  10. :hook
  11. (elpaca-after-init . global-diff-hl-mode)
  12. (dired-mode . diff-hl-dired-mode)
  13. (magit-post-refresh . diff-hl-magit-post-refresh))
  14. (use-package gitconfig)
  15. (use-package git-modes)
  16. (use-package git-timemachine
  17. :bind
  18. ("C-x v t" . git-timemachine-toggle))
  19. (use-feature vc
  20. :bind
  21. (("C-x v C-r" . my/vc-refresh-state)
  22. ("C-x v C-m" . my/update-git-master))
  23. :custom (vc-follow-symlinks nil)
  24. :config
  25. (defun my/vc-refresh-state ()
  26. (interactive)
  27. (when-let ((root-dir (vc-root-dir)))
  28. (dolist (buf (buffer-list))
  29. (when (and (not (buffer-modified-p buf))
  30. (buffer-file-name buf)
  31. (file-exists-p (buffer-file-name buf))
  32. (file-in-directory-p (buffer-file-name buf) root-dir))
  33. (with-current-buffer buf
  34. (vc-refresh-state))))))
  35. ;; [alias]
  36. ;; update-master = !git fetch origin master:master
  37. ;; update-main = !git fetch origin main:main
  38. (defun my/update-git-master ()
  39. "Update git master or main branch."
  40. (interactive)
  41. (if-let ((root (vc-root-dir)))
  42. (let* ((branches (vc-git-branches))
  43. (main-p (member "main" branches))
  44. (master-p (member "master" branches))
  45. (current-branch (car branches))
  46. (on-master-p (member current-branch '("master" "main")))
  47. (command (if main-p "update-main" "update-master"))
  48. (buffer "*vc-update-master*"))
  49. (if on-master-p
  50. (vc-pull)
  51. ;; based on vc-git--pushpull
  52. (require 'vc-dispatcher)
  53. (apply #'vc-do-async-command buffer root vc-git-program command nil)
  54. (with-current-buffer buffer
  55. (vc-run-delayed
  56. (vc-compilation-mode 'git)
  57. (setq-local compile-command
  58. (concat vc-git-program " " command))
  59. (setq-local compilation-directory root)
  60. (setq-local compilation-arguments
  61. (list compile-command nil
  62. (lambda (_name-of-mode) buffer)
  63. nil))))
  64. (vc-set-async-update buffer)))
  65. (message "not a git repository"))))
  66. (use-package magit
  67. :bind
  68. ("C-c g g" . magit-dispatch) ;; magit-file-dispatch is C-c M-g
  69. ("C-c g u" . my/magit-set-upstream)
  70. ("C-c g r" . my/magit-refresh-state)
  71. ("C-c g m" . my/magit-update-master)
  72. ("C-c g C-c" . my/magit-stage-and-commit-file)
  73. ;; Used by eshell-prompt-function (see init-shell.el)
  74. :commands (magit-get-shortname magit-file-status)
  75. :config
  76. ;; Requires the following gitconfig:
  77. ;; [alias]
  78. ;; upstream = !git push -u origin HEAD
  79. ;; TODO - this is useful after setting push remote, but is there a better way?
  80. (defun my/magit-set-upstream ()
  81. (interactive)
  82. (magit-shell-command-topdir "git upstream"))
  83. ;; update stale git info on the modeline (based on code removed from doom modeline)
  84. (defun my/magit-refresh-state ()
  85. "Update modeline git branch information."
  86. (interactive)
  87. (dolist (buf (buffer-list))
  88. (when (and (not (buffer-modified-p buf))
  89. (buffer-file-name buf)
  90. (file-exists-p (buffer-file-name buf))
  91. (file-in-directory-p (buffer-file-name buf) (magit-toplevel)))
  92. (with-current-buffer buf
  93. (vc-refresh-state)))))
  94. ;; [alias]
  95. ;; update-master = !git fetch origin master:master
  96. ;; update-main = !git fetch origin main:main
  97. (defun my/magit-update-master ()
  98. "Update git master or main branch."
  99. (interactive)
  100. (if (magit-toplevel)
  101. (let* ((branches (vc-git-branches))
  102. (main-p (member "main" branches))
  103. (current-branch (car branches))
  104. (on-master-p (member current-branch '("master" "main")))
  105. (command (concat "git " (if main-p "update-main" "update-master"))))
  106. (if on-master-p
  107. (vc-pull)
  108. (magit-shell-command-topdir command)))
  109. (message "Not a git repository")))
  110. (defun my/magit-stage-and-commit-file ()
  111. "Stage and commit the current the currently visited file."
  112. (interactive)
  113. (magit-stage-file (magit-file-relative-name))
  114. (magit-commit-create))
  115. ;; difftastic code copied from https://tsdh.org/posts/2022-08-01-difftastic-diffing-with-magit.html
  116. (defun my/magit--with-difftastic (buffer command)
  117. "Run COMMAND with GIT_EXTERNAL_DIFF=difft then show result in BUFFER."
  118. (let ((process-environment
  119. (cons (concat "GIT_EXTERNAL_DIFF=difft --width="
  120. (number-to-string (frame-width)))
  121. process-environment)))
  122. ;; Clear the result buffer (we might regenerate a diff, e.g., for
  123. ;; the current changes in our working directory).
  124. (with-current-buffer buffer
  125. (setq buffer-read-only nil)
  126. (erase-buffer))
  127. ;; Now spawn a process calling the git COMMAND.
  128. (make-process
  129. :name (buffer-name buffer)
  130. :buffer buffer
  131. :command command
  132. ;; Don't query for running processes when emacs is quit.
  133. :noquery t
  134. ;; Show the result buffer once the process has finished.
  135. :sentinel (lambda (proc event)
  136. (when (eq (process-status proc) 'exit)
  137. (with-current-buffer (process-buffer proc)
  138. (goto-char (point-min))
  139. (ansi-color-apply-on-region (point-min) (point-max))
  140. (setq buffer-read-only t)
  141. (view-mode)
  142. (end-of-line)
  143. ;; difftastic diffs are usually 2-column side-by-side,
  144. ;; so ensure our window is wide enough.
  145. (let ((width (current-column)))
  146. (while (zerop (forward-line 1))
  147. (end-of-line)
  148. (setq width (max (current-column) width)))
  149. ;; Add column size of fringes
  150. (setq width (+ width
  151. (fringe-columns 'left)
  152. (fringe-columns 'right)))
  153. (goto-char (point-min))
  154. (pop-to-buffer
  155. (current-buffer)
  156. `(;; If the buffer is that wide that splitting the frame in
  157. ;; two side-by-side windows would result in less than
  158. ;; 80 columns left, ensure it's shown at the bottom.
  159. ,(when (> 80 (- (frame-width) width))
  160. #'display-buffer-at-bottom)
  161. (window-width
  162. . ,(min width (frame-width))))))))))))
  163. (defun my/magit-show-with-difftastic (rev)
  164. "Show the result of \"git show REV\" with GIT_EXTERNAL_DIFF=difft."
  165. (interactive
  166. (list (or
  167. ;; If REV is given, just use it.
  168. (when (boundp 'rev) rev)
  169. ;; If not invoked with prefix arg, try to guess the REV from
  170. ;; point's position.
  171. (and (not current-prefix-arg)
  172. (or (magit-thing-at-point 'git-revision t)
  173. (magit-branch-or-commit-at-point)))
  174. ;; Otherwise, query the user.
  175. (magit-read-branch-or-commit "Revision"))))
  176. (if (not rev)
  177. (error "No revision specified")
  178. (my/magit--with-difftastic
  179. (get-buffer-create (concat "*git show difftastic " rev "*"))
  180. (list "git" "--no-pager" "show" "--ext-diff" rev))))
  181. (defun my/magit-diff-with-difftastic (arg)
  182. "Show the result of \"git diff ARG\" with GIT_EXTERNAL_DIFF=difft."
  183. (interactive
  184. (list (or
  185. ;; If RANGE is given, just use it.
  186. (when (boundp 'range) range)
  187. ;; If prefix arg is given, query the user.
  188. (and current-prefix-arg
  189. (magit-diff-read-range-or-commit "Range"))
  190. ;; Otherwise, auto-guess based on position of point, e.g., based on
  191. ;; if we are in the Staged or Unstaged section.
  192. (pcase (magit-diff--dwim)
  193. ('unmerged (error "Unmerged is not yet implemented"))
  194. ('unstaged nil)
  195. ('staged "--cached")
  196. (`(stash . ,value) (error "Stash is not yet implemented"))
  197. (`(commit . ,value) (format "%s^..%s" value value))
  198. ((and range (pred stringp)) range)
  199. (_ (magit-diff-read-range-or-commit "Range/Commit"))))))
  200. (let ((name (concat "*git diff difftastic"
  201. (if arg (concat " " arg) "")
  202. "*")))
  203. (my/magit--with-difftastic
  204. (get-buffer-create name)
  205. `("git" "--no-pager" "diff" "--ext-diff" ,@(when arg (list arg))))))
  206. (require 'ansi-color)
  207. ;; https://tsdh.org/posts/2022-07-20-using-eldoc-with-magit-async.html
  208. ;; https://tsdh.org/posts/2021-06-21-using-eldoc-with-magit.html
  209. (defvar my/eldoc-git-show-stat--process nil)
  210. (defun my/eldoc-git-show-stat (callback commit)
  211. "Compute diffstat for COMMIT asynchronously, then call CALLBACK with it."
  212. ;; Kill the possibly still running old process and its buffer.
  213. (when (processp my/eldoc-git-show-stat--process)
  214. (let ((buf (process-buffer my/eldoc-git-show-stat--process)))
  215. (when (process-live-p my/eldoc-git-show-stat--process)
  216. (let (confirm-kill-processes)
  217. (kill-process my/eldoc-git-show-stat--process)))
  218. (when (buffer-live-p buf)
  219. (kill-buffer buf))))
  220. ;; Spawn a new "git show" process.
  221. (let* ((cmd (list "git" "--no-pager" "show"
  222. "--no-color"
  223. ;; Author Name <author@email.com>, <date-and-time>
  224. "--format=format:%an <%ae>, %aD"
  225. "--stat=80"
  226. commit)))
  227. ;; An async eldoc-documentation-function must also return a non-nil,
  228. ;; non-string result if it's applicable for computing a documentation
  229. ;; string, so we set and return the new process here.
  230. (setq my/eldoc-git-show-stat--process
  231. (make-process
  232. :name "eldoc-git-show"
  233. :buffer (generate-new-buffer " *git-show-stat*")
  234. :noquery t
  235. :command cmd
  236. :sentinel (lambda (proc event)
  237. (when (eq (process-status proc) 'exit)
  238. (with-current-buffer (process-buffer proc)
  239. (goto-char (point-min))
  240. (put-text-property (point-min)
  241. (line-end-position)
  242. 'face 'bold)
  243. (funcall callback (buffer-string)))))))))
  244. (defvar my/magit-eldoc-last-commit nil)
  245. (defun my/magit-eldoc-for-commit (callback)
  246. (let ((commit (magit-commit-at-point)))
  247. (when (and commit
  248. (not (equal commit my/magit-eldoc-last-commit)))
  249. (setq my/magit-eldoc-last-commit commit)
  250. (my/eldoc-git-show-stat callback commit))))
  251. (defun my/magit-eldoc-setup ()
  252. (add-hook 'eldoc-documentation-functions
  253. #'my/magit-eldoc-for-commit nil t))
  254. (add-hook 'magit-status-mode-hook #'my/magit-eldoc-setup)
  255. (add-hook 'magit-log-mode-hook #'my/magit-eldoc-setup)
  256. (eldoc-add-command 'magit-next-line)
  257. (eldoc-add-command 'magit-previous-line)
  258. ;; Based on https://tsdh.org/posts/2022-08-01-difftastic-diffing-with-magit.html
  259. (transient-define-prefix my/magit-extra-commands ()
  260. "Extra magit commands."
  261. ["Extra commands"
  262. ("u" "Set upstream" my/magit-set-upstream)
  263. ("r" "Refresh state (update modeline)" my/magit-refresh-state)
  264. ("m" "Update master/main" my/magit-update-master)
  265. ("d" "Difftastic Diff (dwim)" my/magit-diff-with-difftastic)
  266. ("s" "Difftastic Show" my/magit-show-with-difftastic)
  267. ("D" "Toggle magit-delta-mode" my/toggle-delta-mode)])
  268. (transient-append-suffix 'magit-dispatch "!"
  269. '("#" "Extra Magit Cmds" my/magit-extra-commands))
  270. (define-key magit-status-mode-map (kbd "#") #'my/magit-extra-commands)
  271. :custom
  272. (magit-diff-refine-hunk 'all)
  273. (magit-diff-paint-whitespace-lines 'all)
  274. (magit-diff-refine-ignore-whitespace nil)
  275. (magit-diff-highlight-trailing t))
  276. (use-package magit-delta
  277. :after magit
  278. :demand t
  279. :config
  280. (defun my/toggle-delta-mode ()
  281. (interactive)
  282. (call-interactively #'magit-delta-mode)
  283. (magit-refresh)))
  284. (use-package forge
  285. :after magit
  286. :bind (:map forge-pullreq-list-mode-map ("C-w" . forge-copy-url-at-point-as-kill)))
  287. (use-package git-link
  288. :config
  289. (defun git-link-on-branch ()
  290. "Like `git-link', but force linking to the branch rather than a commit."
  291. (interactive)
  292. (let ((git-link-use-commit nil))
  293. (call-interactively 'git-link)))
  294. (defun git-link-branch ()
  295. "Create a URL representing the current buffer's branch in its
  296. GitHub/Bitbucket/GitLab/... The URL will be added to the kill ring. If
  297. `git-link-open-in-browser' is non-nil also call `browse-url'."
  298. (interactive)
  299. (let* ((remote-info (git-link--parse-remote (git-link--remote-url (git-link--select-remote))))
  300. (branch (git-link--branch)))
  301. (if (null (car remote-info))
  302. (message "Remote `%s' contains an unsupported URL" remote)
  303. (git-link--new (format "https://%s/%s/tree/%s" (car remote-info) (cadr remote-info) branch)))))
  304. ;; https://clojurians.slack.com/archives/C099W16KZ/p1699983189128519?thread_ts=1699981599.260029&cid=C099W16KZ
  305. (defun git-link-blame ()
  306. (interactive)
  307. (cl-flet ((git-link--new* (x) (replace-regexp-in-string "/blob/" "/blame/" x)))
  308. (advice-add 'git-link--new :override #'git-link--new*)
  309. (let ((link (call-interactively 'git-link)))
  310. (advice-remove 'git-link--new #'git-link--new*)
  311. (git-link--new link))))
  312. :custom (git-link-use-commit t)
  313. :bind
  314. ("C-c g s" . git-link)
  315. ("C-c g S" . git-link-on-branch)
  316. ("C-c g c" . git-link-commit)
  317. ("C-c g b" . git-link-branch))
  318. (use-feature git-link-transient
  319. :bind ("C-c g d" . git-link-dispatch))
  320. (use-feature git-related
  321. :bind
  322. ("C-c g #" . git-related-find-file)
  323. ("C-c g ~" . git-related-update))
  324. (provide 'init-git)
  325. ;;; init-git.el ends here