| 1 | ;;; magit-blame.el --- Blame support for Magit -*- lexical-binding:t -*- |
| 2 | |
| 3 | ;; Copyright (C) 2008-2026 The Magit Project Contributors |
| 4 | |
| 5 | ;; Author: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev> |
| 6 | ;; Maintainer: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev> |
| 7 | |
| 8 | ;; SPDX-License-Identifier: GPL-3.0-or-later |
| 9 | |
| 10 | ;; Magit is free software: you can redistribute it and/or modify it |
| 11 | ;; under the terms of the GNU General Public License as published by |
| 12 | ;; the Free Software Foundation, either version 3 of the License, or |
| 13 | ;; (at your option) any later version. |
| 14 | ;; |
| 15 | ;; Magit is distributed in the hope that it will be useful, but WITHOUT |
| 16 | ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY |
| 17 | ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public |
| 18 | ;; License for more details. |
| 19 | ;; |
| 20 | ;; You should have received a copy of the GNU General Public License |
| 21 | ;; along with Magit. If not, see <https://www.gnu.org/licenses/>. |
| 22 | |
| 23 | ;;; Commentary: |
| 24 | |
| 25 | ;; Annotates each line in file-visiting buffer with information from |
| 26 | ;; the revision which last modified the line. |
| 27 | |
| 28 | ;;; Code: |
| 29 | |
| 30 | (require 'magit) |
| 31 | |
| 32 | ;;; Options |
| 33 | |
| 34 | (defgroup magit-blame nil |
| 35 | "Blame support for Magit." |
| 36 | :link '(info-link "(magit)Blaming") |
| 37 | :group 'magit-modes) |
| 38 | |
| 39 | (defcustom magit-blame-styles |
| 40 | '((headings |
| 41 | (heading-format . "%-20a %C %s\n")) |
| 42 | (highlight |
| 43 | (highlight-face . magit-blame-highlight)) |
| 44 | (lines |
| 45 | (show-lines . t) |
| 46 | (show-message . t))) |
| 47 | "List of styles used to visualize blame information. |
| 48 | |
| 49 | The style used in the current buffer can be cycled from the blame |
| 50 | popup. Blame commands (except `magit-blame-echo') use the first |
| 51 | style as the initial style when beginning to blame in a buffer. |
| 52 | |
| 53 | Each entry has the form (IDENT (KEY . VALUE)...). IDENT has |
| 54 | to be a symbol uniquely identifying the style. The following |
| 55 | KEYs are recognized: |
| 56 | |
| 57 | `show-lines' |
| 58 | Whether to prefix each chunk of lines with a thin line. |
| 59 | This has no effect if `heading-format' is non-nil. |
| 60 | `show-message' |
| 61 | Whether to display a commit's summary line in the echo area |
| 62 | when crossing chunks. |
| 63 | `highlight-face' |
| 64 | Face used to highlight the first line of each chunk. |
| 65 | If this is nil, then those lines are not highlighted. |
| 66 | `heading-format' |
| 67 | String specifying the information to be shown above each |
| 68 | chunk of lines. It must end with a newline character. |
| 69 | `margin-format' |
| 70 | String specifying the information to be shown in the left |
| 71 | buffer margin. It must NOT end with a newline character. |
| 72 | This can also be a list of formats used for the lines at |
| 73 | the same positions within the chunk. If the chunk has |
| 74 | more lines than formats are specified, then the last is |
| 75 | repeated. WARNING: Adding this key affects performance; |
| 76 | see the note at the end of this docstring. |
| 77 | `margin-width' |
| 78 | Width of the margin, provided `margin-format' is non-nil. |
| 79 | `margin-face' |
| 80 | Face used in the margin, provided `margin-format' is |
| 81 | non-nil. This face is used in combination with the faces |
| 82 | that are specific to the used %-specs. If this is nil, |
| 83 | then `magit-blame-margin' is used. |
| 84 | `margin-body-face' |
| 85 | Face used in the margin for all but first line of a chunk. |
| 86 | This face is used in combination with the faces that are |
| 87 | specific to the used %-specs. This can also be a list of |
| 88 | faces (usually one face), in which case only these faces |
| 89 | are used and the %-spec faces are ignored. A good value |
| 90 | might be `(magit-blame-dimmed)'. If this is nil, then |
| 91 | the same face as for the first line is used. |
| 92 | |
| 93 | The following %-specs can be used in `heading-format' and |
| 94 | `margin-format': |
| 95 | |
| 96 | %H hash using face `magit-blame-hash' |
| 97 | %h truncated hash using face `magit-blame-hash' |
| 98 | %s summary using face `magit-blame-summary' |
| 99 | %a author using face `magit-blame-name' |
| 100 | %A author time using face `magit-blame-date' |
| 101 | %c committer using face `magit-blame-name' |
| 102 | %C committer time using face `magit-blame-date' |
| 103 | |
| 104 | Note that for performance reasons %h results in truncated |
| 105 | hashes, as opposed to properly abbreviated hashes that are |
| 106 | guaranteed to uniquely identify a commit. |
| 107 | |
| 108 | Additionally if `margin-format' ends with %f, then the string |
| 109 | that is displayed in the margin is made at least `margin-width' |
| 110 | characters wide, which may be desirable if the used face sets |
| 111 | the background color. |
| 112 | |
| 113 | Blame information is displayed using overlays. Such extensive |
| 114 | use of overlays is known to slow down even basic operations, such |
| 115 | as moving the cursor. To reduce the number of overlays the margin |
| 116 | style had to be removed from the default value of this option. |
| 117 | |
| 118 | Note that the margin overlays are created even if another style |
| 119 | is currently active. This can only be prevented by not even |
| 120 | defining a style that uses the margin. If you want to use this |
| 121 | style anyway, you can restore this definition, which used to be |
| 122 | part of the default value: |
| 123 | |
| 124 | (margin |
| 125 | (margin-format . (\" %s%f\" \" %C %a\" \" %H\")) |
| 126 | (margin-width . 42) |
| 127 | (margin-face . magit-blame-margin) |
| 128 | (margin-body-face . (magit-blame-dimmed)))" |
| 129 | :package-version '(magit . "2.13.0") |
| 130 | :group 'magit-blame |
| 131 | :type '(alist :key-type symbol |
| 132 | :value-type (alist :key-type symbol :value-type sexp))) |
| 133 | |
| 134 | (defcustom magit-blame-echo-style 'lines |
| 135 | "The blame visualization style used by `magit-blame-echo'. |
| 136 | A symbol that has to be used as the identifier for one of the |
| 137 | styles defined in `magit-blame-styles'." |
| 138 | :package-version '(magit . "2.13.0") |
| 139 | :group 'magit-blame |
| 140 | :type 'symbol) |
| 141 | |
| 142 | (defcustom magit-blame-time-format "%F %H:%M" |
| 143 | "Format for time strings in blame headings." |
| 144 | :group 'magit-blame |
| 145 | :type 'string) |
| 146 | |
| 147 | (defcustom magit-blame-read-only t |
| 148 | "Whether to initially make the blamed buffer read-only." |
| 149 | :package-version '(magit . "2.13.0") |
| 150 | :group 'magit-blame |
| 151 | :type 'boolean) |
| 152 | |
| 153 | (defcustom magit-blame-disable-modes '(fci-mode yascroll-bar-mode) |
| 154 | "List of modes not compatible with Magit-Blame mode. |
| 155 | This modes are turned off when Magit-Blame mode is turned on, |
| 156 | and then turned on again when turning off the latter." |
| 157 | :group 'magit-blame |
| 158 | :type '(repeat (symbol :tag "Mode"))) |
| 159 | |
| 160 | (defcustom magit-blame-mode-lighter " Blame" |
| 161 | "The mode-line lighter of the Magit-Blame mode." |
| 162 | :group 'magit-blame |
| 163 | :type '(choice (const :tag "No lighter" "") string)) |
| 164 | |
| 165 | (defcustom magit-blame-goto-chunk-hook |
| 166 | (list #'magit-blame-maybe-update-revision-buffer |
| 167 | #'magit-blame-maybe-show-message) |
| 168 | "Hook run after point entered another chunk." |
| 169 | :package-version '(magit . "2.13.0") |
| 170 | :group 'magit-blame |
| 171 | :type 'hook |
| 172 | :get #'magit-hook-custom-get |
| 173 | :options (list #'magit-blame-maybe-update-revision-buffer |
| 174 | #'magit-blame-maybe-show-message)) |
| 175 | |
| 176 | ;;; Faces |
| 177 | |
| 178 | (defface magit-blame-highlight |
| 179 | '((((class color) (background light)) |
| 180 | :extend t |
| 181 | :background "grey80" |
| 182 | :foreground "black") |
| 183 | (((class color) (background dark)) |
| 184 | :extend t |
| 185 | :background "grey25" |
| 186 | :foreground "white")) |
| 187 | "Face used for highlighting when blaming. |
| 188 | Also see option `magit-blame-styles'." |
| 189 | :group 'magit-faces) |
| 190 | |
| 191 | (defface magit-blame-margin |
| 192 | '((t :inherit magit-blame-highlight |
| 193 | :weight normal |
| 194 | :slant normal)) |
| 195 | "Face used for the blame margin by default when blaming. |
| 196 | Also see option `magit-blame-styles'." |
| 197 | :group 'magit-faces) |
| 198 | |
| 199 | (defface magit-blame-dimmed |
| 200 | '((t :inherit magit-dimmed |
| 201 | :weight normal |
| 202 | :slant normal)) |
| 203 | "Face used for the blame margin in some cases when blaming. |
| 204 | Also see option `magit-blame-styles'." |
| 205 | :group 'magit-faces) |
| 206 | |
| 207 | (defface magit-blame-heading |
| 208 | '((t :extend t |
| 209 | :inherit magit-blame-highlight |
| 210 | :weight normal |
| 211 | :slant normal)) |
| 212 | "Face used for blame headings by default when blaming. |
| 213 | Also see option `magit-blame-styles'." |
| 214 | :group 'magit-faces) |
| 215 | |
| 216 | (defface magit-blame-summary '((t nil)) |
| 217 | "Face used for commit summaries when blaming." |
| 218 | :group 'magit-faces) |
| 219 | |
| 220 | (defface magit-blame-hash '((t nil)) |
| 221 | "Face used for commit hashes when blaming." |
| 222 | :group 'magit-faces) |
| 223 | |
| 224 | (defface magit-blame-name '((t nil)) |
| 225 | "Face used for author and committer names when blaming." |
| 226 | :group 'magit-faces) |
| 227 | |
| 228 | (defface magit-blame-date '((t nil)) |
| 229 | "Face used for dates when blaming." |
| 230 | :group 'magit-faces) |
| 231 | |
| 232 | ;;; Variables |
| 233 | |
| 234 | (defvar-local magit-blame-buffer-read-only nil) |
| 235 | (defvar-local magit-blame-cache nil) |
| 236 | (defvar-local magit-blame-disabled-modes nil) |
| 237 | (defvar-local magit-blame-process nil) |
| 238 | (defvar-local magit-blame-recursive-p nil) |
| 239 | (defvar-local magit-blame-type nil) |
| 240 | (defvar-local magit-blame-separator nil) |
| 241 | (defvar-local magit-blame-previous-chunk nil) |
| 242 | |
| 243 | (defvar-local magit-blame--make-margin-overlays nil) |
| 244 | (defvar-local magit-blame--style nil) |
| 245 | |
| 246 | ;;; Chunks |
| 247 | |
| 248 | (defclass magit-blame-chunk () |
| 249 | (;; <orig-rev> <orig-line> <final-line> <num-lines> |
| 250 | (orig-rev :initarg :orig-rev) |
| 251 | (orig-line :initarg :orig-line) |
| 252 | (final-line :initarg :final-line) |
| 253 | (num-lines :initarg :num-lines) |
| 254 | ;; previous <prev-rev> <prev-file> |
| 255 | (prev-rev :initform nil) |
| 256 | (prev-file :initform nil) |
| 257 | ;; filename <orig-file> |
| 258 | (orig-file))) |
| 259 | |
| 260 | (defun magit-current-blame-chunk (&optional type noerror) |
| 261 | (or (and (not (and type (not (eq type magit-blame-type)))) |
| 262 | (magit-blame-chunk-at (point))) |
| 263 | (and type |
| 264 | (let ((rev magit-buffer-revision) |
| 265 | (file (and (not (derived-mode-p 'dired-mode)) |
| 266 | (magit-file-relative-name |
| 267 | nil (not magit-buffer-file-name)))) |
| 268 | (line (format "%d,+1" (line-number-at-pos)))) |
| 269 | (cond (file |
| 270 | (with-temp-buffer |
| 271 | (magit-with-toplevel |
| 272 | (magit-git-insert |
| 273 | "blame" "--porcelain" |
| 274 | (if (memq magit-blame-type '(final removal)) |
| 275 | (cons "--reverse" (magit-blame-arguments)) |
| 276 | (magit-blame-arguments)) |
| 277 | "-L" line rev "--" file) |
| 278 | (goto-char (point-min)) |
| 279 | (cond ((not (eobp)) |
| 280 | (car (magit-blame--parse-chunk type))) |
| 281 | ((not noerror) |
| 282 | (error "Cannot get blame chunk at eob")))))) |
| 283 | ((not noerror) |
| 284 | (error "Buffer does not visit a tracked file"))))))) |
| 285 | |
| 286 | (defun magit-blame-chunk-at (pos) |
| 287 | (seq-some (##overlay-get % 'magit-blame-chunk) |
| 288 | (overlays-at pos))) |
| 289 | |
| 290 | (defun magit-blame--overlay-at (&optional pos key) |
| 291 | (unless pos |
| 292 | (setq pos (point))) |
| 293 | (seq-find (##overlay-get % (or key 'magit-blame-chunk)) |
| 294 | (nconc (overlays-at pos) |
| 295 | (overlays-in pos pos)))) |
| 296 | |
| 297 | ;;; Keymaps |
| 298 | |
| 299 | (defvar-keymap magit-blame-mode-map |
| 300 | :doc "Keymap for `magit-blame-mode'. |
| 301 | Note that most blaming key bindings are defined |
| 302 | in `magit-blame-read-only-mode-map' instead." |
| 303 | "C-c C-q" #'magit-blame-quit) |
| 304 | |
| 305 | (defvar-keymap magit-blame-read-only-mode-map |
| 306 | :doc "Keymap for `magit-blame-read-only-mode'." |
| 307 | "C-m" #'magit-show-commit |
| 308 | "p" #'magit-blame-previous-chunk |
| 309 | "P" #'magit-blame-previous-chunk-same-commit |
| 310 | "n" #'magit-blame-next-chunk |
| 311 | "N" #'magit-blame-next-chunk-same-commit |
| 312 | "b" #'magit-blame-addition |
| 313 | "r" #'magit-blame-removal |
| 314 | "f" #'magit-blame-reverse |
| 315 | "B" #'magit-blame |
| 316 | "c" #'magit-blame-cycle-style |
| 317 | "q" #'magit-blame-quit |
| 318 | "M-w" #'magit-blame-copy-hash |
| 319 | "SPC" #'magit-diff-show-or-scroll-up |
| 320 | "S-SPC" #'magit-diff-show-or-scroll-down |
| 321 | "DEL" #'magit-diff-show-or-scroll-down) |
| 322 | |
| 323 | ;;; Modes |
| 324 | ;;;; Base Mode |
| 325 | |
| 326 | (define-minor-mode magit-blame-mode |
| 327 | "Display blame information inline." |
| 328 | :lighter magit-blame-mode-lighter |
| 329 | :interactive nil |
| 330 | (cond (magit-blame-mode |
| 331 | (add-hook 'after-save-hook #'magit-blame--refresh t t) |
| 332 | (add-hook 'post-command-hook #'magit-blame-goto-chunk-hook t t) |
| 333 | (add-hook 'before-revert-hook #'magit-blame--remove-overlays t t) |
| 334 | (add-hook 'after-revert-hook #'magit-blame--refresh t t) |
| 335 | (add-hook 'read-only-mode-hook #'magit-blame-toggle-read-only t t) |
| 336 | (setq magit-blame-buffer-read-only buffer-read-only) |
| 337 | (when (or magit-blame-read-only magit-buffer-file-name) |
| 338 | (read-only-mode 1)) |
| 339 | (dolist (mode magit-blame-disable-modes) |
| 340 | (when (and (boundp mode) (symbol-value mode)) |
| 341 | (funcall mode -1) |
| 342 | (push mode magit-blame-disabled-modes))) |
| 343 | (setq magit-blame-separator (magit-blame--format-separator)) |
| 344 | (unless magit-blame--style |
| 345 | (setq magit-blame--style (car magit-blame-styles))) |
| 346 | (setq magit-blame--make-margin-overlays |
| 347 | (and (cl-find-if (##assq 'margin-format (cdr %)) |
| 348 | magit-blame-styles) |
| 349 | t)) |
| 350 | (magit-blame--update-margin 'enable)) |
| 351 | (t |
| 352 | (when (process-live-p magit-blame-process) |
| 353 | (kill-process magit-blame-process) |
| 354 | (while magit-blame-process |
| 355 | (sit-for 0.01))) ; avoid racing the sentinel |
| 356 | (remove-hook 'after-save-hook #'magit-blame--refresh t) |
| 357 | (remove-hook 'post-command-hook #'magit-blame-goto-chunk-hook t) |
| 358 | (remove-hook 'before-revert-hook #'magit-blame--remove-overlays t) |
| 359 | (remove-hook 'after-revert-hook #'magit-blame--refresh t) |
| 360 | (remove-hook 'read-only-mode-hook #'magit-blame-toggle-read-only t) |
| 361 | (unless magit-blame-buffer-read-only |
| 362 | (read-only-mode -1)) |
| 363 | (magit-blame-read-only-mode -1) |
| 364 | (dolist (mode magit-blame-disabled-modes) |
| 365 | (funcall mode 1)) |
| 366 | (kill-local-variable 'magit-blame-disabled-modes) |
| 367 | (kill-local-variable 'magit-blame-type) |
| 368 | (kill-local-variable 'magit-blame--style) |
| 369 | (magit-blame--update-margin 'disable) |
| 370 | (magit-blame--remove-overlays)))) |
| 371 | |
| 372 | (defun magit-blame--refresh () |
| 373 | (magit-blame--run (magit-blame-arguments))) |
| 374 | |
| 375 | (defun magit-blame-goto-chunk-hook () |
| 376 | (let ((chunk (magit-blame-chunk-at (point)))) |
| 377 | (when (cl-typep chunk 'magit-blame-chunk) |
| 378 | (unless (eq chunk magit-blame-previous-chunk) |
| 379 | (run-hooks 'magit-blame-goto-chunk-hook)) |
| 380 | (setq magit-blame-previous-chunk chunk)))) |
| 381 | |
| 382 | (defun magit-blame-toggle-read-only () |
| 383 | (magit-blame-read-only-mode (if buffer-read-only 1 -1))) |
| 384 | |
| 385 | ;;;; Read-Only Mode |
| 386 | |
| 387 | (define-minor-mode magit-blame-read-only-mode |
| 388 | "Provide keybindings for Magit-Blame mode. |
| 389 | |
| 390 | This minor-mode provides the key bindings for Magit-Blame mode, |
| 391 | but only when Read-Only mode is also enabled because these key |
| 392 | bindings would otherwise conflict badly with regular bindings. |
| 393 | |
| 394 | When both Magit-Blame mode and Read-Only mode are enabled, then |
| 395 | this mode gets automatically enabled too and when one of these |
| 396 | modes is toggled, then this mode also gets toggled automatically. |
| 397 | |
| 398 | \\{magit-blame-read-only-mode-map}") |
| 399 | |
| 400 | ;;;; Kludges |
| 401 | |
| 402 | (defun magit-blame-put-keymap-before-view-mode () |
| 403 | "Put `magit-blame-read-only-mode' ahead of `view-mode' in `minor-mode-map-alist'." |
| 404 | (when-let ((entry (assq 'magit-blame-read-only-mode |
| 405 | (cl-member 'view-mode minor-mode-map-alist |
| 406 | :key #'car)))) |
| 407 | (setq minor-mode-map-alist |
| 408 | (cons entry |
| 409 | (delq entry minor-mode-map-alist)))) |
| 410 | (remove-hook 'view-mode-hook #'magit-blame-put-keymap-before-view-mode)) |
| 411 | |
| 412 | (add-hook 'view-mode-hook #'magit-blame-put-keymap-before-view-mode) |
| 413 | |
| 414 | ;;; Process |
| 415 | |
| 416 | (defun magit-blame--run (args) |
| 417 | (magit-with-toplevel |
| 418 | (unless magit-blame-mode |
| 419 | (magit-blame-mode 1)) |
| 420 | (message "Blaming...") |
| 421 | (magit-blame-run-process |
| 422 | (and (not (equal magit-buffer-revision "{index}")) |
| 423 | magit-buffer-revision) |
| 424 | (magit-file-relative-name nil (not magit-buffer-file-name)) |
| 425 | (if (memq magit-blame-type '(final removal)) |
| 426 | (cons "--reverse" args) |
| 427 | args) |
| 428 | (list (line-number-at-pos (window-start)) |
| 429 | (line-number-at-pos (1- (window-end nil t))))) |
| 430 | (set-process-sentinel magit-this-process |
| 431 | #'magit-blame-process-quickstart-sentinel))) |
| 432 | |
| 433 | (defun magit-blame-run-process (rev file args &optional lines) |
| 434 | (let ((process (magit-parse-git-async |
| 435 | "blame" "--incremental" args |
| 436 | (and lines (list "-L" (apply #'format "%s,%s" lines))) |
| 437 | rev "--" file))) |
| 438 | (set-process-filter process #'magit-blame-process-filter) |
| 439 | (set-process-sentinel process #'magit-blame-process-sentinel) |
| 440 | (process-put process 'arguments (list rev file args)) |
| 441 | (setq magit-blame-cache (make-hash-table :test #'equal)) |
| 442 | (setq magit-blame-process process))) |
| 443 | |
| 444 | (defun magit-blame-process-quickstart-sentinel (process event) |
| 445 | (when (memq (process-status process) '(exit signal)) |
| 446 | (magit-blame-process-sentinel process event t) |
| 447 | (magit-blame-assert-buffer process) |
| 448 | (with-current-buffer (process-get process 'command-buf) |
| 449 | (when magit-blame-mode |
| 450 | (let ((default-directory (magit-toplevel))) |
| 451 | (apply #'magit-blame-run-process |
| 452 | (process-get process 'arguments))))))) |
| 453 | |
| 454 | (defun magit-blame-process-sentinel (process _event &optional quiet) |
| 455 | (let ((status (process-status process))) |
| 456 | (when (memq status '(exit signal)) |
| 457 | (kill-buffer (process-buffer process)) |
| 458 | (kill-buffer (process-get process 'stderr-buf)) |
| 459 | (if (and (eq status 'exit) |
| 460 | (zerop (process-exit-status process))) |
| 461 | (unless quiet |
| 462 | (message "Blaming...done")) |
| 463 | (magit-blame-assert-buffer process) |
| 464 | (with-current-buffer (process-get process 'command-buf) |
| 465 | (cond (magit-blame-mode |
| 466 | (magit-blame-mode -1) |
| 467 | (message "Blaming...failed")) |
| 468 | ((message "Blaming...aborted"))))) |
| 469 | (kill-local-variable 'magit-blame-process)))) |
| 470 | |
| 471 | (defun magit-blame-process-filter (process string) |
| 472 | (internal-default-process-filter process string) |
| 473 | (let ((buf (process-get process 'command-buf)) |
| 474 | (pos (process-get process 'parsed)) |
| 475 | (mark (process-mark process)) |
| 476 | type cache) |
| 477 | (with-current-buffer buf |
| 478 | (setq type magit-blame-type) |
| 479 | (setq cache magit-blame-cache)) |
| 480 | (with-current-buffer (process-buffer process) |
| 481 | (goto-char pos) |
| 482 | (while (and (< (point) mark) |
| 483 | (save-excursion (re-search-forward "^filename .+\n" nil t))) |
| 484 | (pcase-let* ((`(,chunk ,revinfo) |
| 485 | (magit-blame--parse-chunk type)) |
| 486 | (rev (oref chunk orig-rev))) |
| 487 | (if revinfo |
| 488 | (puthash rev revinfo cache) |
| 489 | (setq revinfo |
| 490 | (or (gethash rev cache) |
| 491 | (puthash rev (magit-blame--commit-alist rev) cache)))) |
| 492 | (magit-blame--make-overlays buf chunk revinfo)) |
| 493 | (process-put process 'parsed (point)))))) |
| 494 | |
| 495 | (defun magit-blame--parse-chunk (type) |
| 496 | (let (chunk revinfo) |
| 497 | (unless (looking-at "^\\(.\\{40,\\}\\) \\([0-9]+\\) \\([0-9]+\\) \\([0-9]+\\)") |
| 498 | (error "Blaming failed due to unexpected output: %s" |
| 499 | (buffer-substring-no-properties (point) (line-end-position)))) |
| 500 | (with-slots (orig-rev orig-file prev-rev prev-file) |
| 501 | (setq chunk (magit-blame-chunk |
| 502 | :orig-rev (match-str 1) |
| 503 | :orig-line (string-to-number (match-str 2)) |
| 504 | :final-line (string-to-number (match-str 3)) |
| 505 | :num-lines (string-to-number (match-str 4)))) |
| 506 | (forward-line) |
| 507 | (let (done) |
| 508 | (while (not done) |
| 509 | (cond ((looking-at "^filename \\(.+\\)") |
| 510 | (setq done t) |
| 511 | (setf orig-file (magit-decode-git-path (match-str 1)))) |
| 512 | ((looking-at "^previous \\(.\\{40,\\}\\) \\(.+\\)") |
| 513 | (setf prev-rev (match-str 1)) |
| 514 | (setf prev-file (magit-decode-git-path (match-str 2)))) |
| 515 | ((looking-at "^\\([^ ]+\\) \\(.+\\)") |
| 516 | (push (cons (match-str 1) |
| 517 | (match-str 2)) |
| 518 | revinfo))) |
| 519 | (forward-line))) |
| 520 | (when (and (eq type 'removal) prev-rev) |
| 521 | (cl-rotatef orig-rev prev-rev) |
| 522 | (cl-rotatef orig-file prev-file) |
| 523 | (setq revinfo nil))) |
| 524 | (list chunk revinfo))) |
| 525 | |
| 526 | (defun magit-blame--commit-alist (rev) |
| 527 | (cl-mapcar 'cons |
| 528 | '("summary" |
| 529 | "author" "author-time" "author-tz" |
| 530 | "committer" "committer-time" "committer-tz") |
| 531 | (split-string (magit-rev-format "%s\v%an\v%ad\v%cn\v%cd" rev |
| 532 | "--date=format:%s\v%z") |
| 533 | "\v"))) |
| 534 | |
| 535 | (defun magit-blame-assert-buffer (process) |
| 536 | (unless (buffer-live-p (process-get process 'command-buf)) |
| 537 | (kill-process process) |
| 538 | (user-error "Buffer being blamed has been killed"))) |
| 539 | |
| 540 | ;;; Display |
| 541 | |
| 542 | (defvar-local magit-blame--previous-margin-width nil) |
| 543 | |
| 544 | (defsubst magit-blame--style-get (key) |
| 545 | (cdr (assoc key (cdr magit-blame--style)))) |
| 546 | |
| 547 | (defun magit-blame--make-overlays (buf chunk revinfo) |
| 548 | (with-current-buffer buf |
| 549 | (save-excursion |
| 550 | (save-restriction |
| 551 | (widen) |
| 552 | (let* ((line (oref chunk final-line)) |
| 553 | (beg (magit-blame--line-beginning-position line)) |
| 554 | (end (magit-blame--line-beginning-position |
| 555 | (+ line (oref chunk num-lines)))) |
| 556 | (before (magit-blame-chunk-at (1- beg)))) |
| 557 | (when (and before |
| 558 | (equal (oref before orig-rev) |
| 559 | (oref chunk orig-rev))) |
| 560 | (setq beg (magit-blame--line-beginning-position |
| 561 | (oset chunk final-line (oref before final-line)))) |
| 562 | (incf (oref chunk num-lines) |
| 563 | (oref before num-lines))) |
| 564 | (magit-blame--remove-overlays beg end) |
| 565 | (when magit-blame--make-margin-overlays |
| 566 | (magit-blame--make-margin-overlays chunk revinfo beg end)) |
| 567 | (magit-blame--make-heading-overlay chunk revinfo beg end) |
| 568 | (magit-blame--make-highlight-overlay chunk beg)))))) |
| 569 | |
| 570 | (defun magit-blame--line-beginning-position (line) |
| 571 | (save-excursion |
| 572 | (goto-char (point-min)) |
| 573 | (forward-line (1- line)) |
| 574 | (point))) |
| 575 | |
| 576 | (defun magit-blame--make-margin-overlays (chunk revinfo beg end) |
| 577 | (save-excursion |
| 578 | (let ((line 0)) |
| 579 | (goto-char beg) |
| 580 | (while (< (point) end) |
| 581 | (magit-blame--make-margin-overlay chunk revinfo line) |
| 582 | (forward-line) |
| 583 | (incf line))))) |
| 584 | |
| 585 | (defun magit-blame--make-margin-overlay (chunk revinfo line) |
| 586 | (let* ((end (line-end-position)) |
| 587 | ;; If possible avoid putting this on the first character |
| 588 | ;; of the line to avoid a conflict with the line overlay. |
| 589 | (beg (min (1+ (line-beginning-position)) end)) |
| 590 | (ov (make-overlay beg end))) |
| 591 | (overlay-put ov 'magit-blame-chunk chunk) |
| 592 | (overlay-put ov 'magit-blame-revinfo revinfo) |
| 593 | (overlay-put ov 'magit-blame-margin line) |
| 594 | (magit-blame--update-margin-overlay ov))) |
| 595 | |
| 596 | (defun magit-blame--make-heading-overlay (chunk revinfo beg end) |
| 597 | (let ((ov (make-overlay beg end))) |
| 598 | (overlay-put ov 'magit-blame-chunk chunk) |
| 599 | (overlay-put ov 'magit-blame-revinfo revinfo) |
| 600 | (overlay-put ov 'magit-blame-heading t) |
| 601 | (magit-blame--update-heading-overlay ov))) |
| 602 | |
| 603 | (defun magit-blame--make-highlight-overlay (chunk beg) |
| 604 | (let ((ov (make-overlay beg (1+ (magit--eol-position beg))))) |
| 605 | (overlay-put ov 'magit-blame-chunk chunk) |
| 606 | (overlay-put ov 'magit-blame-highlight t) |
| 607 | (magit-blame--update-highlight-overlay ov))) |
| 608 | |
| 609 | (defun magit-blame--update-margin (&optional action) |
| 610 | (when (eq action 'enable) |
| 611 | (setq magit-blame--previous-margin-width left-margin-width)) |
| 612 | (setq left-margin-width |
| 613 | (if (eq action 'disable) |
| 614 | (prog1 magit-blame--previous-margin-width |
| 615 | (setq magit-blame--previous-margin-width nil)) |
| 616 | (or (magit-blame--style-get 'margin-width) |
| 617 | magit-blame--previous-margin-width))) |
| 618 | (set-window-buffer (selected-window) (current-buffer))) |
| 619 | |
| 620 | (defun magit-blame--update-overlays () |
| 621 | (save-restriction |
| 622 | (widen) |
| 623 | (dolist (ov (overlays-in (point-min) (point-max))) |
| 624 | (cond ((overlay-get ov 'magit-blame-heading) |
| 625 | (magit-blame--update-heading-overlay ov)) |
| 626 | ((overlay-get ov 'magit-blame-margin) |
| 627 | (magit-blame--update-margin-overlay ov)) |
| 628 | ((overlay-get ov 'magit-blame-highlight) |
| 629 | (magit-blame--update-highlight-overlay ov)))))) |
| 630 | |
| 631 | (defun magit-blame--update-margin-overlay (ov) |
| 632 | (overlay-put |
| 633 | ov 'before-string |
| 634 | (and (magit-blame--style-get 'margin-width) |
| 635 | (propertize |
| 636 | "o" 'display |
| 637 | (list (list 'margin 'left-margin) |
| 638 | (let ((line (overlay-get ov 'magit-blame-margin)) |
| 639 | (format (magit-blame--style-get 'margin-format)) |
| 640 | (face (magit-blame--style-get 'margin-face))) |
| 641 | (magit-blame--format-string |
| 642 | ov |
| 643 | (or (and (atom format) |
| 644 | format) |
| 645 | (nth line format) |
| 646 | (car (last format))) |
| 647 | (or (and (not (zerop line)) |
| 648 | (magit-blame--style-get 'margin-body-face)) |
| 649 | face |
| 650 | 'magit-blame-margin)))))))) |
| 651 | |
| 652 | (defun magit-blame--update-heading-overlay (ov) |
| 653 | (overlay-put |
| 654 | ov 'before-string |
| 655 | (if-let ((format (magit-blame--style-get 'heading-format))) |
| 656 | ;; Use `default' as the last face to avoid picking up any face |
| 657 | ;; attributes from the first character of the text on which we |
| 658 | ;; put the overlay. See #5233. |
| 659 | (magit-blame--format-string ov format '(magit-blame-heading default)) |
| 660 | (and (magit-blame--style-get 'show-lines) |
| 661 | (or (not (magit-blame--style-get 'margin-format)) |
| 662 | (save-excursion |
| 663 | (goto-char (overlay-start ov)) |
| 664 | ;; Special case of the special case described in |
| 665 | ;; `magit-blame--make-margin-overlay'. For empty |
| 666 | ;; lines it is not possible to show both overlays |
| 667 | ;; without the line being too high. |
| 668 | (not (= (point) (line-end-position))))) |
| 669 | magit-blame-separator)))) |
| 670 | |
| 671 | (defun magit-blame--update-highlight-overlay (ov) |
| 672 | (overlay-put ov 'font-lock-face (magit-blame--style-get 'highlight-face))) |
| 673 | |
| 674 | (defun magit-blame--format-string (ov format face) |
| 675 | (let* ((chunk (overlay-get ov 'magit-blame-chunk)) |
| 676 | (revinfo (overlay-get ov 'magit-blame-revinfo)) |
| 677 | (key (list format face)) |
| 678 | (string (cdr (assoc key revinfo)))) |
| 679 | (unless string |
| 680 | (setq string |
| 681 | (and format |
| 682 | (magit-blame--format-string-1 (oref chunk orig-rev) |
| 683 | revinfo format face))) |
| 684 | (nconc revinfo (list (cons key string)))) |
| 685 | string)) |
| 686 | |
| 687 | (defun magit-blame--format-string-1 (rev revinfo format face) |
| 688 | (let ((str |
| 689 | (if (string-match-p "\\`0\\{40,\\}\\'" rev) |
| 690 | (propertize (concat (if (string-prefix-p "\s" format) "\s" "") |
| 691 | "Not Yet Committed" |
| 692 | (if (string-suffix-p "\n" format) "\n" "")) |
| 693 | 'font-lock-face face) |
| 694 | (magit--format-spec |
| 695 | (propertize format 'font-lock-face face) |
| 696 | (cl-flet* ((p0 (s f) |
| 697 | (propertize s 'font-lock-face |
| 698 | (if face (cons f (ensure-list face)) f))) |
| 699 | (p1 (k f) |
| 700 | (p0 (cdr (assoc k revinfo)) f)) |
| 701 | (p2 (k1 k2 f) |
| 702 | (p0 (magit-blame--format-time-string |
| 703 | (cdr (assoc k1 revinfo)) |
| 704 | (cdr (assoc k2 revinfo))) |
| 705 | f))) |
| 706 | `((?H . ,(p0 rev 'magit-blame-hash)) |
| 707 | (?h . ,(p0 (magit-blame--abbrev-hash rev) 'magit-blame-hash)) |
| 708 | (?s . ,(p1 "summary" 'magit-blame-summary)) |
| 709 | (?a . ,(p1 "author" 'magit-blame-name)) |
| 710 | (?c . ,(p1 "committer" 'magit-blame-name)) |
| 711 | (?A . ,(p2 "author-time" "author-tz" 'magit-blame-date)) |
| 712 | (?C . ,(p2 "committer-time" "committer-tz" 'magit-blame-date)) |
| 713 | (?f . ""))))))) |
| 714 | (if-let ((width (and (string-suffix-p "%f" format) |
| 715 | (magit-blame--style-get 'margin-width)))) |
| 716 | (concat str |
| 717 | (propertize (make-string (max 0 (- width (length str))) ?\s) |
| 718 | 'font-lock-face face)) |
| 719 | str))) |
| 720 | |
| 721 | (defun magit-blame--format-separator () |
| 722 | (propertize (concat (propertize "\s" 'display '(space :height (2))) |
| 723 | (propertize "\n" 'line-height t)) |
| 724 | 'font-lock-face |
| 725 | `( :extend t |
| 726 | :background |
| 727 | ,(face-attribute 'magit-blame-heading :background nil t)))) |
| 728 | |
| 729 | (defun magit-blame--format-time-string (time tz) |
| 730 | (let* ((time-format (or (magit-blame--style-get 'time-format) |
| 731 | magit-blame-time-format)) |
| 732 | (tz-in-second (and (string-search "%z" time-format) |
| 733 | (car (last (parse-time-string tz)))))) |
| 734 | (format-time-string time-format |
| 735 | (seconds-to-time (string-to-number time)) |
| 736 | tz-in-second))) |
| 737 | |
| 738 | (defvar-local magit-blame--abbrev-length nil) |
| 739 | |
| 740 | (defun magit-blame--abbrev-hash (rev) |
| 741 | (substring rev 0 (or magit-blame--abbrev-length |
| 742 | (setq magit-blame--abbrev-length |
| 743 | (magit-abbrev-length))))) |
| 744 | |
| 745 | (defun magit-blame--remove-overlays (&optional beg end) |
| 746 | (save-restriction |
| 747 | (widen) |
| 748 | (dolist (ov (overlays-in (or beg (point-min)) |
| 749 | (or end (point-max)))) |
| 750 | (when (overlay-get ov 'magit-blame-chunk) |
| 751 | (delete-overlay ov))))) |
| 752 | |
| 753 | (defun magit-blame-maybe-show-message () |
| 754 | (cond-let |
| 755 | ((not (magit-blame--style-get 'show-message))) |
| 756 | ([msg (cdr (assoc "summary" |
| 757 | (gethash (oref (magit-current-blame-chunk) orig-rev) |
| 758 | magit-blame-cache)))] |
| 759 | (set-text-properties 0 (length msg) nil msg) |
| 760 | (magit-msg "%S" msg)) |
| 761 | ((magit-msg "Commit data not available yet. Still blaming.")))) |
| 762 | |
| 763 | ;;; Commands |
| 764 | |
| 765 | ;;;###autoload(autoload 'magit-blame-echo "magit-blame" nil t) |
| 766 | (transient-define-suffix magit-blame-echo (args) |
| 767 | "For each line show the revision in which it was added. |
| 768 | Show the information about the chunk at point in the echo area |
| 769 | when moving between chunks. Unlike other blaming commands, do |
| 770 | not turn on `read-only-mode'." |
| 771 | :if (##and buffer-file-name |
| 772 | (or (not magit-blame-mode) |
| 773 | buffer-read-only)) |
| 774 | (interactive (list (magit-blame-arguments))) |
| 775 | (when magit-buffer-file-name |
| 776 | (user-error "Blob buffers aren't supported")) |
| 777 | (setq-local magit-blame--style |
| 778 | (assq magit-blame-echo-style magit-blame-styles)) |
| 779 | (setq-local magit-blame-disable-modes |
| 780 | (cons 'eldoc-mode magit-blame-disable-modes)) |
| 781 | (if (not magit-blame-mode) |
| 782 | (let ((magit-blame-read-only nil)) |
| 783 | (magit-blame--pre-blame-assert 'addition) |
| 784 | (magit-blame--pre-blame-setup 'addition) |
| 785 | (magit-blame--run args)) |
| 786 | (read-only-mode -1) |
| 787 | (magit-blame--update-overlays))) |
| 788 | |
| 789 | ;;;###autoload(autoload 'magit-blame-addition "magit-blame" nil t) |
| 790 | (transient-define-suffix magit-blame-addition (args) |
| 791 | "For each line show the revision in which it was added." |
| 792 | (interactive (list (magit-blame-arguments))) |
| 793 | (magit-blame--pre-blame-assert 'addition) |
| 794 | (magit-blame--pre-blame-setup 'addition) |
| 795 | (magit-blame--run args)) |
| 796 | |
| 797 | ;;;###autoload(autoload 'magit-blame-removal "magit-blame" nil t) |
| 798 | (transient-define-suffix magit-blame-removal (args) |
| 799 | "For each line show the revision in which it was removed." |
| 800 | :if-nil 'buffer-file-name |
| 801 | (interactive (list (magit-blame-arguments))) |
| 802 | (unless magit-buffer-file-name |
| 803 | (user-error "Only blob buffers can be blamed in reverse")) |
| 804 | (magit-blame--pre-blame-assert 'removal) |
| 805 | (magit-blame--pre-blame-setup 'removal) |
| 806 | (magit-blame--run args)) |
| 807 | |
| 808 | ;;;###autoload(autoload 'magit-blame-reverse "magit-blame" nil t) |
| 809 | (transient-define-suffix magit-blame-reverse (args) |
| 810 | "For each line show the last revision in which it still exists." |
| 811 | :if-nil 'buffer-file-name |
| 812 | (interactive (list (magit-blame-arguments))) |
| 813 | (unless magit-buffer-file-name |
| 814 | (user-error "Only blob buffers can be blamed in reverse")) |
| 815 | (magit-blame--pre-blame-assert 'final) |
| 816 | (magit-blame--pre-blame-setup 'final) |
| 817 | (magit-blame--run args)) |
| 818 | |
| 819 | (defun magit-blame--pre-blame-assert (type) |
| 820 | (unless (magit-toplevel) |
| 821 | (magit--not-inside-repository-error)) |
| 822 | (cond ((and magit-blame-mode |
| 823 | (eq type magit-blame-type)) |
| 824 | (if-let ((chunk (magit-current-blame-chunk))) |
| 825 | (unless (oref chunk prev-rev) |
| 826 | (user-error "Chunk has no further history")) |
| 827 | (message "Still blaming, commit data not available yet"))) |
| 828 | ((not (magit-file-relative-name nil (not magit-buffer-file-name))) |
| 829 | (if buffer-file-name |
| 830 | (user-error "Buffer isn't visiting a tracked file") |
| 831 | (user-error "Buffer isn't visiting a file"))))) |
| 832 | |
| 833 | (defun magit-blame--pre-blame-setup (type) |
| 834 | (when magit-blame-mode |
| 835 | (if (eq type magit-blame-type) |
| 836 | (let ((style magit-blame--style)) |
| 837 | (magit-blame-visit-other-file) |
| 838 | (setq-local magit-blame--style style) |
| 839 | (setq-local magit-blame-recursive-p t) |
| 840 | ;; Set window-start for the benefit of quickstart. |
| 841 | (redisplay)) |
| 842 | (magit-blame--remove-overlays))) |
| 843 | (setq magit-blame-type type)) |
| 844 | |
| 845 | (defun magit-blame-visit-other-file () |
| 846 | "Visit another blob related to the current chunk." |
| 847 | (interactive) |
| 848 | (with-slots (prev-rev prev-file orig-line) |
| 849 | (magit-current-blame-chunk) |
| 850 | (unless prev-rev |
| 851 | (user-error "Chunk has no further history")) |
| 852 | (magit-with-toplevel |
| 853 | (magit-find-file prev-rev prev-file)) |
| 854 | ;; TODO Adjust line like magit-diff-visit-file. |
| 855 | (goto-char (point-min)) |
| 856 | (forward-line (1- orig-line)))) |
| 857 | |
| 858 | (defun magit-blame-visit-file () |
| 859 | "Visit the blob related to the current chunk." |
| 860 | (interactive) |
| 861 | (with-slots (orig-rev orig-file orig-line) |
| 862 | (magit-current-blame-chunk) |
| 863 | (magit-with-toplevel |
| 864 | (magit-find-file orig-rev orig-file)) |
| 865 | (goto-char (point-min)) |
| 866 | (forward-line (1- orig-line)))) |
| 867 | |
| 868 | (transient-define-suffix magit-blame-quit () |
| 869 | "Turn off Magit-Blame mode. |
| 870 | If the buffer was created during a recursive blame, |
| 871 | then also kill the buffer." |
| 872 | :if-non-nil 'magit-blame-mode |
| 873 | (interactive) |
| 874 | (magit-blame-mode -1) |
| 875 | (when magit-blame-recursive-p |
| 876 | (kill-buffer))) |
| 877 | |
| 878 | (defun magit-blame-next-chunk () |
| 879 | "Move to the next chunk." |
| 880 | (interactive) |
| 881 | (if-let ((next (next-single-char-property-change |
| 882 | (point) 'magit-blame-chunk))) |
| 883 | (goto-char next) |
| 884 | (user-error "No more chunks"))) |
| 885 | |
| 886 | (defun magit-blame-previous-chunk () |
| 887 | "Move to the previous chunk." |
| 888 | (interactive) |
| 889 | (if-let ((prev (previous-single-char-property-change |
| 890 | (point) 'magit-blame-chunk))) |
| 891 | (goto-char prev) |
| 892 | (user-error "No more chunks"))) |
| 893 | |
| 894 | (defun magit-blame-next-chunk-same-commit (&optional previous) |
| 895 | "Move to the next chunk from the same commit. |
| 896 | \n(fn)" |
| 897 | (interactive) |
| 898 | (if-let ((rev (oref (magit-current-blame-chunk) orig-rev))) |
| 899 | (let ((pos (point)) ov) |
| 900 | (save-excursion |
| 901 | (while (and (not ov) |
| 902 | (not (= pos (if previous (point-min) (point-max)))) |
| 903 | (setq pos (funcall |
| 904 | (if previous |
| 905 | #'previous-single-char-property-change |
| 906 | #'next-single-char-property-change) |
| 907 | pos 'magit-blame-chunk))) |
| 908 | (when-let |
| 909 | ((o (magit-blame--overlay-at pos)) |
| 910 | (_(equal (oref (magit-blame-chunk-at pos) orig-rev) rev))) |
| 911 | (setq ov o)))) |
| 912 | (if ov |
| 913 | (goto-char (overlay-start ov)) |
| 914 | (user-error "No more chunks from same commit"))) |
| 915 | (user-error "This chunk hasn't been blamed yet"))) |
| 916 | |
| 917 | (defun magit-blame-previous-chunk-same-commit () |
| 918 | "Move to the previous chunk from the same commit." |
| 919 | (interactive) |
| 920 | (magit-blame-next-chunk-same-commit #'previous-single-char-property-change)) |
| 921 | |
| 922 | (defun magit-blame-cycle-style () |
| 923 | "Change how blame information is visualized. |
| 924 | Cycle through the elements of option `magit-blame-styles'." |
| 925 | (interactive) |
| 926 | (setq magit-blame--style |
| 927 | (or (cadr (cl-member (car magit-blame--style) |
| 928 | magit-blame-styles :key #'car)) |
| 929 | (car magit-blame-styles))) |
| 930 | (magit-blame--update-margin) |
| 931 | (magit-blame--update-overlays)) |
| 932 | |
| 933 | (defun magit-blame-copy-hash () |
| 934 | "Save hash of the current chunk's commit to the kill ring. |
| 935 | |
| 936 | When the region is active, then save the region's content |
| 937 | instead of the hash, like `kill-ring-save' would." |
| 938 | (interactive) |
| 939 | (if (use-region-p) |
| 940 | (call-interactively #'copy-region-as-kill) |
| 941 | (kill-new (message "%s" (oref (magit-current-blame-chunk) orig-rev))))) |
| 942 | |
| 943 | ;;; Menu |
| 944 | |
| 945 | ;;;###autoload(autoload 'magit-blame "magit-blame" nil t) |
| 946 | (transient-define-prefix magit-blame () |
| 947 | "Show the commits that added or removed lines in the visited file." |
| 948 | :man-page "git-blame" |
| 949 | :value '("-w") |
| 950 | ["Arguments" |
| 951 | ("-w" "Ignore whitespace" "-w") |
| 952 | ("-r" "Do not treat root commits as boundaries" "--root") |
| 953 | ("-P" "Follow only first parent" "--first-parent") |
| 954 | (magit-blame:-M) |
| 955 | (magit-blame:-C)] |
| 956 | ["Actions" |
| 957 | ("b" "Show commits adding lines" magit-blame-addition) |
| 958 | ("r" "Show commits removing lines" magit-blame-removal) |
| 959 | ("f" "Show last commits that still have lines" magit-blame-reverse) |
| 960 | ("m" "Blame echo" magit-blame-echo) |
| 961 | ("q" "Quit blaming" magit-blame-quit)] |
| 962 | ["Refresh" |
| 963 | :if-non-nil magit-blame-mode |
| 964 | ("c" "Cycle style" magit-blame-cycle-style :transient t)]) |
| 965 | |
| 966 | (defun magit-blame-arguments () |
| 967 | (transient-args 'magit-blame)) |
| 968 | |
| 969 | (transient-define-argument magit-blame:-M () |
| 970 | :description "Detect lines moved or copied within a file" |
| 971 | :class 'transient-option |
| 972 | :argument "-M" |
| 973 | :allow-empty t |
| 974 | :reader #'transient-read-number-N+) |
| 975 | |
| 976 | (transient-define-argument magit-blame:-C () |
| 977 | :description "Detect lines moved or copied between files" |
| 978 | :class 'transient-option |
| 979 | :argument "-C" |
| 980 | :allow-empty t |
| 981 | :reader #'transient-read-number-N+) |
| 982 | |
| 983 | ;;; Utilities |
| 984 | |
| 985 | (defun magit-blame-maybe-update-revision-buffer () |
| 986 | (when-let* ((chunk (magit-current-blame-chunk)) |
| 987 | (commit (oref chunk orig-rev)) |
| 988 | (buffer (magit-get-mode-buffer 'magit-revision-mode nil t))) |
| 989 | (if magit--update-revision-buffer |
| 990 | (setq magit--update-revision-buffer (list commit buffer)) |
| 991 | (setq magit--update-revision-buffer (list commit buffer)) |
| 992 | (run-with-idle-timer |
| 993 | magit-update-other-window-delay nil |
| 994 | (lambda () |
| 995 | (pcase-let ((`(,rev ,buf) magit--update-revision-buffer)) |
| 996 | (setq magit--update-revision-buffer nil) |
| 997 | (when (buffer-live-p buf) |
| 998 | (let ((magit-display-buffer-noselect t)) |
| 999 | (apply #'magit-show-commit rev |
| 1000 | (magit-diff-arguments 'magit-revision-mode)))))))))) |
| 1001 | |
| 1002 | ;;; _ |
| 1003 | (provide 'magit-blame) |
| 1004 | ;; Local Variables: |
| 1005 | ;; read-symbol-shorthands: ( |
| 1006 | ;; ("and$" . "cond-let--and$") |
| 1007 | ;; ("thread$" . "cond-let--thread$") |
| 1008 | ;; ("when$" . "cond-let--when$") |
| 1009 | ;; ("and-let*" . "cond-let--and-let*") |
| 1010 | ;; ("and-let" . "cond-let--and-let") |
| 1011 | ;; ("if-let*" . "cond-let--if-let*") |
| 1012 | ;; ("if-let" . "cond-let--if-let") |
| 1013 | ;; ("when-let*" . "cond-let--when-let*") |
| 1014 | ;; ("when-let" . "cond-let--when-let") |
| 1015 | ;; ("while-let*" . "cond-let--while-let*") |
| 1016 | ;; ("while-let" . "cond-let--while-let") |
| 1017 | ;; ("match-string" . "match-string") |
| 1018 | ;; ("match-str" . "match-string-no-properties")) |
| 1019 | ;; End: |
| 1020 | ;;; magit-blame.el ends here |