├── README.md ├── LICENSE └── magit-prime.el /README.md: -------------------------------------------------------------------------------- 1 | # magit-prime 2 | 3 | A Magit extension that primes the magit cache in parallel before refresh, reducing refresh times. 4 | Speeds up magit-refresh by ~100ms on my system. 5 | 6 | Tramp support is experimental. 7 | 8 | ## Usage 9 | 10 | Magit prime is on MELPA: 11 | ```elisp 12 | (use-package magit-prime 13 | :config 14 | (add-hook 'magit-pre-refresh-hook 'magit-prime-refresh-cache)) 15 | ``` 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Romain Ouabdelkader 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /magit-prime.el: -------------------------------------------------------------------------------- 1 | ;;; magit-prime.el --- Prime cache before Magit refresh -*- lexical-binding: t; -*- 2 | 3 | ;; Author: Romain Ouabdelkader 4 | ;; URL: https://github.com/Azkae/magit-prime 5 | ;; Version: 0.1 6 | ;; Package-Requires: ((emacs "27.1") (magit "3.0.0")) 7 | 8 | ;; SPDX-License-Identifier: MIT 9 | 10 | ;;; Commentary: 11 | 12 | ;; A Magit extension that primes caches in parallel before refresh operations, 13 | ;; reducing Magit buffer refresh times. 14 | 15 | ;; Usage: 16 | ;; (add-hook 'magit-pre-refresh-hook 'magit-prime-refresh-cache) 17 | 18 | ;;; Code: 19 | 20 | (require 'magit) 21 | (require 'tramp-sh) 22 | 23 | (defconst magit-prime--commands-phase-one 24 | '(("symbolic-ref" "--short" "HEAD") 25 | (t "describe" "--long" "--tags") 26 | ("describe" "--contains" "HEAD") 27 | ("rev-parse" "--git-dir") 28 | ("rev-parse" "--is-bare-repository") 29 | ("rev-parse" "--short" "HEAD") 30 | ("rev-parse" "--short" "HEAD~") 31 | ("rev-parse" "--verify" "--abbrev-ref" "master@{upstream}") 32 | ("rev-parse" "--verify" "--abbrev-ref" "main@{upstream}") 33 | (t "describe" "--contains" "HEAD") 34 | (t "rev-parse" "--verify" "HEAD") 35 | (t "rev-parse" "--verify" "refs/stash") 36 | (t "rev-parse" "--verify" "HEAD~10")) 37 | "Returns a list of git commands that will be used to prime magit's cache. 38 | 39 | Commands prefixed with t are cached even on failure.") 40 | 41 | (defun magit-prime--commands-phase-two () 42 | "Return a list of git commands that will be used to prime magit's cache. 43 | The commands depend on the current branch, remotes, 44 | and upstream configurations fetched from `magit-prime--commands-phase-one''. 45 | 46 | Commands prefixed with t are cached even on failure." 47 | (let* ((branch (magit-get-current-branch)) 48 | (main (magit-main-branch)) 49 | (push-main (magit-get-push-branch main)) 50 | (push-branch (magit-get-push-branch branch)) 51 | (upstream-main (magit-get-upstream-branch main)) 52 | (push-remote (magit-get-push-remote branch)) 53 | (primary-remote (magit-primary-remote))) 54 | (cl-remove-duplicates 55 | `(("rev-parse" "--verify" "--abbrev-ref" ,(concat main "@{upstream}")) 56 | ("rev-parse" "--verify" "--abbrev-ref" ,(concat branch "@{upstream}")) 57 | ("rev-parse" "--verify" "--abbrev-ref" ,(concat push-branch "^{commit}")) 58 | ("log" "--no-walk" "--format=%s" ,(concat upstream-main"^{commit}") "--") 59 | ("log" "--no-walk" "--format=%s" ,(concat push-main "^{commit}") "--") 60 | ("log" "--no-walk" "--format=%s" ,(concat push-branch "^{commit}") "--") 61 | ("log" "--no-walk" "--format=%h %s" "HEAD^{commit}" "--") 62 | (t "symbolic-ref" ,(format "refs/remotes/%s/HEAD" primary-remote)) 63 | (t "symbolic-ref" ,(format "refs/remotes/%s/HEAD" push-remote)) 64 | (t "rev-parse" "--verify" ,(concat "refs/tags/" branch)) 65 | (t "rev-parse" "--verify" ,(concat "refs/tags/" main)) 66 | (t "rev-parse" "--verify" ,(concat "refs/tags/" push-branch)) 67 | (t "rev-parse" "--verify" ,(concat "refs/tags/" push-main)) 68 | (t "rev-parse" "--verify" ,(format "refs/tags/%s/HEAD" primary-remote)) 69 | (t "rev-parse" "--verify" ,push-branch)) 70 | :test #'equal))) 71 | 72 | (defun magit-prime-refresh-cache () 73 | "Prime the refresh cache if possible." 74 | (when (and (or magit-refresh-status-buffer 75 | (derived-mode-p 'magit-status-mode)) 76 | magit--refresh-cache) 77 | (let ((elapsed 78 | (benchmark-elapse 79 | (magit-prime--refresh-cache magit-prime--commands-phase-one) 80 | (magit-prime--refresh-cache (magit-prime--commands-phase-two))))) 81 | (when magit-refresh-verbose 82 | (message "Refresh cached primed in %.3fs" elapsed))))) 83 | 84 | (defun magit-prime--refresh-cache (commands) 85 | "Execute git COMMANDS to prime Magit's refresh cache." 86 | (if (file-remote-p default-directory) 87 | (magit-prime--refresh-cache-remote commands) 88 | (magit-prime--refresh-cache-local commands))) 89 | 90 | (defun magit-prime--refresh-cache-local (commands) 91 | "Prime the refresh cache with the provided COMMANDS." 92 | (let* ((repo-path (magit-toplevel)) 93 | (running 0) 94 | (buffers 95 | (mapcar 96 | (lambda (command) 97 | (let* ((buffer (generate-new-buffer " *magit-prime-refresh-cache*")) 98 | (cachep (and (eq (car command) t) (pop command))) 99 | (process-environment (magit-process-environment)) 100 | (default-process-coding-system (magit--process-coding-system))) 101 | (make-process 102 | :name (buffer-name buffer) 103 | :buffer buffer 104 | :noquery t 105 | :connection-type 'pipe 106 | :command (cons magit-git-executable 107 | (magit-process-git-arguments command)) 108 | :sentinel 109 | (lambda (proc _event) 110 | (when (eq (process-status proc) 'exit) 111 | (when-let* ((buf (process-buffer proc)) 112 | ((buffer-live-p buf)) 113 | ((or cachep 114 | (zerop (process-exit-status proc))))) 115 | (push (cons (cons repo-path command) 116 | (with-current-buffer buf 117 | (and (zerop (process-exit-status proc)) 118 | (not (bobp)) 119 | (progn 120 | (goto-char (point-min)) 121 | (buffer-substring-no-properties 122 | (point) (line-end-position)))))) 123 | (cdr magit--refresh-cache))) 124 | (cl-decf running)))) 125 | (cl-incf running) 126 | buffer)) 127 | commands))) 128 | 129 | (with-timeout (1) 130 | (while (> running 0) 131 | (sit-for 0.01) 132 | (accept-process-output))) 133 | 134 | (mapc #'kill-buffer buffers))) 135 | 136 | (defconst magit-prime--batch-commands-script " 137 | COMMANDS=\"$1\" 138 | REPO_DIR=\"$2\" 139 | 140 | run_command() { 141 | local id=\"$1\" 142 | local command=\"$2\" 143 | 144 | local output 145 | local exit_code 146 | 147 | output=$(cd \"$REPO_DIR\" && eval \"$command\" 2>/dev/null) 148 | exit_code=$? 149 | 150 | output=$(echo \"$output\" | head -n1) 151 | 152 | echo \"($id $exit_code \\\"$output\\\")\" 153 | } 154 | 155 | echo \\( 156 | 157 | echo \"$COMMANDS\" | { 158 | while IFS=':' read -r id command; do 159 | run_command \"$id\" \"$command\" & 160 | done 161 | 162 | wait 163 | } 164 | 165 | echo \\) 166 | ") 167 | 168 | (defun magit-prime--refresh-cache-remote (commands) 169 | "Prime the refresh cache with the provided COMMANDS using tramp." 170 | (let ((vec (tramp-dissect-file-name default-directory)) 171 | (str-commands (magit-prime--format-commands-for-bash commands)) 172 | (repo-dir (file-remote-p default-directory 'localname)) 173 | (repo-path (magit-toplevel))) 174 | (tramp-maybe-send-script vec magit-prime--batch-commands-script 175 | "magit_prime__batch_commands") 176 | (let ((results 177 | (tramp-send-command-and-read 178 | vec (format "magit_prime__batch_commands '%s' '%s'" str-commands repo-dir)))) 179 | (dolist (item results) 180 | (let* ((command (nth 0 item)) 181 | (cachep (and (eq (car command) t) (pop command))) 182 | (status-code (nth 1 item)) 183 | (output (nth 2 item))) 184 | (when (or cachep (zerop status-code)) 185 | (push (cons (cons repo-path command) (and (zerop status-code) output)) 186 | (cdr magit--refresh-cache)))))))) 187 | 188 | (defun magit-prime--format-commands-for-bash (commands) 189 | "Convert COMMANDS list to bash script input format. 190 | Each command becomes `LISP-FORM:git ARGS' 191 | where LISP-FORM is the original command." 192 | (mapconcat 193 | (lambda (command) 194 | (let* ((original-command command) 195 | (_ (and (eq (car command) t) (pop command))) 196 | (clean-command (mapcar #'substring-no-properties command)) 197 | (git-command (mapconcat #'shell-quote-argument clean-command " ")) 198 | (line (format "%S:%s %s" original-command magit-remote-git-executable git-command))) 199 | line)) 200 | commands 201 | "\n")) 202 | 203 | ;;;###autoload 204 | (define-minor-mode magit-prime-mode 205 | "Global minor mode to enable magit-prime cache priming. 206 | When enabled, automatically primes caches before Magit refresh operations." 207 | :global t 208 | :group 'magit-prime 209 | (if magit-prime-mode 210 | (add-hook 'magit-pre-refresh-hook #'magit-prime-refresh-cache) 211 | (remove-hook 'magit-pre-refresh-hook #'magit-prime-refresh-cache))) 212 | 213 | (provide 'magit-prime) 214 | 215 | ;;; magit-prime.el ends here 216 | --------------------------------------------------------------------------------