fordon_greeman / voidfiles

Branch: 2079ec5
Switch branches/tags
main
theidioticdev add emacs 2079ec5 23h ago
1020 lines · 39.2 KB Raw Blame History
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