;;; tools/magit/config.el -*- lexical-binding: t; -*- (defvar +magit-open-windows-in-direction 'right "What direction to open new windows from the status buffer. For example, diffs and log buffers. Accepts `left', `right', `up', and `down'.") (defvar +magit-fringe-size '(13 . 1) "Size of the fringe in magit-mode buffers. Can be an integer or a cons cell whose CAR and CDR are integer widths for the left and right fringe. Only has an effect in GUI Emacs.") ;; ;;; Packages (use-package! magit :commands magit-file-delete :defer-incrementally (dash f s with-editor git-commit package eieio transient) :init (setq magit-auto-revert-mode nil) ; we do this ourselves further down ;; Must be set early to prevent ~/.config/emacs/transient from being created (setq transient-levels-file (concat doom-data-dir "transient/levels") transient-values-file (concat doom-data-dir "transient/values") transient-history-file (concat doom-data-dir "transient/history")) :config (add-to-list 'doom-debug-variables 'magit-refresh-verbose) (setq transient-default-level 5 magit-diff-refine-hunk t ; show granular diffs in selected hunk ;; Don't autosave repo buffers. This is too magical, and saving can ;; trigger a bunch of unwanted side-effects, like save hooks and ;; formatters. Trust the user to know what they're doing. magit-save-repository-buffers nil ;; Don't display parent/related refs in commit buffers; they are rarely ;; helpful and only add to runtime costs. magit-revision-insert-related-refs nil) (add-hook 'magit-process-mode-hook #'goto-address-mode) (defadvice! +magit-revert-repo-buffers-deferred-a (&rest _) :after '(magit-checkout magit-branch-and-checkout) ;; Since the project likely now contains new files, best we undo the ;; projectile cache so it can be regenerated later. (projectile-invalidate-cache nil) ;; Use a more efficient strategy to auto-revert buffers whose git state has ;; changed: refresh the visible buffers immediately... (+magit-mark-stale-buffers-h)) ;; ...then refresh the rest only when we switch to them, not all at once. (add-hook 'doom-switch-buffer-hook #'+magit-revert-buffer-maybe-h) ;; Center the target file, because it's poor UX to have it at the bottom of ;; the window after invoking `magit-status-here'. (advice-add #'magit-status-here :after #'doom-recenter-a) ;; The default location for git-credential-cache is in ;; ~/.cache/git/credential. However, if ~/.git-credential-cache/ exists, then ;; it is used instead. Magit seems to be hardcoded to use the latter, so here ;; we override it to have more correct behavior. (unless (file-exists-p "~/.git-credential-cache/") (setq magit-credential-cache-daemon-socket (doom-glob (or (getenv "XDG_CACHE_HOME") "~/.cache/") "git/credential/socket"))) ;; Prevent sudden window position resets when staging/unstaging/discarding/etc ;; hunks in `magit-status-mode' buffers. It's disorienting, especially on ;; larger projects. (defvar +magit--pos nil) (add-hook! 'magit-pre-refresh-hook (defun +magit--set-window-state-h () (setq-local +magit--pos (list (current-buffer) (point) (window-start))))) (add-hook! 'magit-post-refresh-hook (defun +magit--restore-window-state-h () (when (and +magit--pos (eq (current-buffer) (car +magit--pos))) (goto-char (cadr +magit--pos)) (set-window-start nil (caddr +magit--pos) t) (kill-local-variable '+magit--pos)))) ;; Magit uses `magit-display-buffer-traditional' to display windows, by ;; default, which is a little primitive. `+magit-display-buffer' marries ;; `magit-display-buffer-fullcolumn-most-v1' with ;; `magit-display-buffer-same-window-except-diff-v1', except: ;; ;; 1. Magit sub-buffers (like `magit-log') that aren't spawned from a status ;; screen are opened as popups. ;; 2. The status screen isn't buried when viewing diffs or logs from the ;; status screen. (setq transient-display-buffer-action '(display-buffer-below-selected) magit-display-buffer-function #'+magit-display-buffer-fn magit-bury-buffer-function #'magit-mode-quit-window) (set-popup-rule! "^\\(?:\\*magit\\|magit:\\| \\*transient\\*\\)" :ignore t) (add-hook 'magit-popup-mode-hook #'hide-mode-line-mode) ;; Add additional switches that seem common enough (transient-append-suffix 'magit-fetch "-p" '("-t" "Fetch all tags" ("-t" "--tags"))) (transient-append-suffix 'magit-pull "-r" '("-a" "Autostash" "--autostash")) ;; so magit buffers can be switched to (except for process buffers) (add-hook! 'doom-real-buffer-functions (defun +magit-buffer-p (buf) (with-current-buffer buf (and (derived-mode-p 'magit-mode) (not (eq major-mode 'magit-process-mode)))))) ;; Clean up after magit by killing leftover magit buffers and reverting ;; affected buffers (or at least marking them as need-to-be-reverted). (define-key magit-mode-map "q" #'+magit/quit) (define-key magit-mode-map "Q" #'+magit/quit-all) ;; Close transient with ESC (define-key transient-map [escape] #'transient-quit-one) (add-hook! 'magit-section-mode-hook (add-hook! 'window-configuration-change-hook :local (defun +magit-enlargen-fringe-h () "Make fringe larger in magit." (and (display-graphic-p) (derived-mode-p 'magit-section-mode) +magit-fringe-size (let ((left (or (car-safe +magit-fringe-size) +magit-fringe-size)) (right (or (cdr-safe +magit-fringe-size) +magit-fringe-size))) (set-window-fringes nil left right)))))) ;; An optimization that particularly affects macOS and Windows users: by ;; resolving `magit-git-executable' Emacs does less work to find the ;; executable in your PATH, which is great because it is called so frequently. ;; However, absolute paths will break magit in TRAMP/remote projects if the ;; git executable isn't in the exact same location. (add-hook! 'magit-status-mode-hook (defun +magit-optimize-process-calls-h () (when-let (path (executable-find magit-git-executable t)) (setq-local magit-git-executable path)))) (add-hook! 'magit-diff-visit-file-hook (defun +magit-reveal-point-if-invisible-h () "Reveal the point if in an invisible region." (if (derived-mode-p 'org-mode) (org-reveal '(4)) (require 'reveal) (reveal-post-command))))) (use-package! forge :when (modulep! +forge) ;; We defer loading even further because forge's dependencies will try to ;; compile emacsql, which is a slow and blocking operation. :after-call magit-status :commands forge-create-pullreq forge-create-issue :preface (setq forge-database-file (concat doom-data-dir "forge/forge-database.sqlite")) (setq forge-add-default-bindings (not (modulep! :editor evil +everywhere))) :config ;; All forge list modes are derived from `forge-topic-list-mode' (map! :map forge-topic-list-mode-map :n "q" #'kill-current-buffer) (when (not forge-add-default-bindings) (map! :map magit-mode-map [remap magit-browse-thing] #'forge-browse-dwim :map magit-remote-section-map [remap magit-browse-thing] #'forge-browse-remote :map magit-branch-section-map [remap magit-browse-thing] #'forge-browse-branch)) (set-popup-rule! "^\\*?[0-9]+:\\(?:new-\\|[0-9]+$\\)" :size 0.45 :modeline t :ttl 0 :quit nil) (set-popup-rule! "^\\*\\(?:[^/]+/[^ ]+ #[0-9]+\\*$\\|Issues\\|Pull-Requests\\|forge\\)" :ignore t) (defadvice! +magit--forge-get-repository-lazily-a (&rest _) "Make `forge-get-repository' return nil if the binary isn't built yet. This prevents emacsql getting compiled, which appears to come out of the blue and blocks Emacs for a short while." :before-while #'forge-get-repository (file-executable-p emacsql-sqlite-executable)) (defadvice! +magit--forge-build-binary-lazily-a (&rest _) "Make `forge-dispatch' only build emacsql if necessary. Annoyingly, the binary gets built as soon as Forge is loaded. Since we've disabled that in `+magit--forge-get-repository-lazily-a', we must manually ensure it is built when we actually use Forge." :before #'forge-dispatch (unless (file-executable-p emacsql-sqlite-executable) (emacsql-sqlite-compile 2) (if (not (file-executable-p emacsql-sqlite-executable)) (message (concat "Failed to build emacsql; forge may not work correctly.\n" "See *Compile-Log* buffer for details")) ;; HACK Due to changes upstream, forge doesn't initialize completely if ;; it doesn't find `emacsql-sqlite-executable', so we have to do it ;; manually after installing it. (setq forge--sqlite-available-p t) (magit-add-section-hook 'magit-status-sections-hook 'forge-insert-pullreqs nil t) (magit-add-section-hook 'magit-status-sections-hook 'forge-insert-issues nil t) (after! forge-topic (dolist (hook forge-bug-reference-hooks) (add-hook hook #'forge-bug-reference-setup))))))) (use-package! code-review :when (modulep! +forge) :after magit :init ;; TODO This needs to either a) be cleaned up or better b) better map things ;; to fit (after! evil-collection-magit (dolist (binding evil-collection-magit-mode-map-bindings) (pcase-let* ((`(,states _ ,evil-binding ,fn) binding)) (dolist (state states) (evil-collection-define-key state 'code-review-mode-map evil-binding fn)))) (evil-set-initial-state 'code-review-mode evil-default-state)) (setq code-review-db-database-file (concat doom-data-dir "code-review/code-review-db-file.sqlite") code-review-log-file (concat doom-data-dir "code-review/code-review-error.log") code-review-download-dir (concat doom-data-dir "code-review/")) :config (transient-append-suffix 'magit-merge "i" '("y" "Review pull request" +magit/start-code-review)) (after! forge (transient-append-suffix 'forge-dispatch "c u" '("c r" "Review pull request" +magit/start-code-review)))) (use-package! magit-todos :after magit :config (setq magit-todos-keyword-suffix "\\(?:([^)]+)\\)?:?") ; make colon optional (define-key magit-todos-section-map "j" nil)) (use-package! evil-collection-magit :when (modulep! :editor evil +everywhere) :defer t :init (defvar evil-collection-magit-use-z-for-folds t) :config ;; q is enough; ESC is way too easy for a vimmer to accidentally press, ;; especially when traversing modes in magit buffers. (evil-define-key* 'normal magit-status-mode-map [escape] nil) (after! code-review (map! :map code-review-mode-map :n "r" #'code-review-transient-api :n "RET" #'code-review-comment-add-or-edit)) ;; Some extra vim-isms I thought were missing from upstream (evil-define-key* '(normal visual) magit-mode-map "*" #'magit-worktree "zt" #'evil-scroll-line-to-top "zz" #'evil-scroll-line-to-center "zb" #'evil-scroll-line-to-bottom "g=" #'magit-diff-default-context "gi" #'forge-jump-to-issues "gm" #'forge-jump-to-pullreqs) ;; Fix these keybinds because they are blacklisted ;; REVIEW There must be a better way to exclude particular evil-collection ;; modules from the blacklist. (map! (:map magit-mode-map :nv "q" #'+magit/quit :nv "Q" #'+magit/quit-all :nv "]" #'magit-section-forward-sibling :nv "[" #'magit-section-backward-sibling :nv "gr" #'magit-refresh :nv "gR" #'magit-refresh-all) (:map magit-status-mode-map :nv "gz" #'magit-refresh) (:map magit-diff-mode-map :nv "gd" #'magit-jump-to-diffstat-or-diff)) ;; A more intuitive behavior for TAB in magit buffers: (define-key! 'normal (magit-status-mode-map magit-stash-mode-map magit-revision-mode-map magit-process-mode-map magit-diff-mode-map) [tab] #'magit-section-toggle) (after! git-rebase (dolist (key '(("M-k" . "gk") ("M-j" . "gj"))) (when-let (desc (assoc (car key) evil-collection-magit-rebase-commands-w-descriptions)) (setcar desc (cdr key)))) (evil-define-key* evil-collection-magit-state git-rebase-mode-map "gj" #'git-rebase-move-line-down "gk" #'git-rebase-move-line-up))) (use-package! evil-collection-magit-section :when (modulep! :editor evil +everywhere) :defer t :init (defvar evil-collection-magit-section-use-z-for-folds evil-collection-magit-use-z-for-folds) (after! magit-section ;; These numbered keys mask the numerical prefix keys. Since they've already ;; been replaced with z1, z2, z3, etc (and 0 with g=), there's no need to ;; keep them around: (undefine-key! magit-section-mode-map "M-1" "M-2" "M-3" "M-4" "1" "2" "3" "4" "0") ;; `evil-collection-magit-section' binds these redundant keys. (map! :map magit-section-mode-map :n "1" nil :n "2" nil :n "3" nil :n "4" nil)))