init-git.el 14 KB

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