fordon_greeman / voidfiles

Branch: 2079ec5
Switch branches/tags
main
theidioticdev add emacs 2079ec5 1d ago
4110 lines · 165.4 KB Raw Blame History
1 ;;; magit-diff.el --- Inspect Git diffs -*- 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 ;; This library implements support for looking at Git diffs and
26 ;; commits.
27
28 ;;; Code:
29
30 (eval-when-compile (require 'subr-x)) ; For macros thread-{first,last}.
31
32 (require 'magit-core)
33 (require 'git-commit)
34
35 (eval-when-compile (require 'ansi-color))
36 (require 'diff-mode)
37 (require 'image)
38 (require 'smerge-mode)
39 (require 'which-func)
40
41 ;; For `magit-diff--get-value'
42 (defvar magit-status-use-buffer-arguments)
43 ;; For `magit-diff-popup'
44 (declare-function magit-stash-show "magit-stash" (stash &optional args files))
45 ;; For `magit-diff-visit-file'
46 (declare-function magit-find-file-noselect "magit-files"
47 (rev file &optional no-restore-position volatile))
48 (declare-function magit-status-setup-buffer "magit-status" (&optional directory))
49 ;; For `magit-diff-wash-diff'
50 (defvar magit-log-heading-re)
51 ;; For `magit-diff-while-committing'
52 (declare-function magit-commit-diff--show "magit-commit" ())
53 (declare-function magit-commit-message-buffer "magit-commit" ())
54 ;; For `magit-insert-revision-gravatar'
55 (defvar gravatar-size)
56 ;; For `magit-show-commit' and `magit-diff-show-or-scroll'
57 (declare-function magit-current-blame-chunk "magit-blame" (&optional type noerror))
58 (declare-function magit-blame-mode "magit-blame" (&optional arg))
59 (defvar magit-blame-mode)
60 ;; For `magit-show-commit--arguments' and `magit-diff-wash-diff'
61 (defvar magit-buffer-log-args)
62 (defvar magit-buffer-log-files)
63 ;; For `magit-diff-show-or-scroll'
64 (declare-function git-rebase-current-line "git-rebase" (&optional batch))
65 ;; For `magit-diff-unmerged'
66 (declare-function magit-merge-in-progress-p "magit-merge" ())
67 (declare-function magit--merge-range "magit-merge" (&optional head))
68 ;; For `magit-diff--dwim'
69 (declare-function forge--pullreq-range "ext:forge-pullreq"
70 (pullreq &optional endpoints))
71 (declare-function forge--pullreq-ref "ext:forge-pullreq" (pullreq))
72 ;; For `magit-diff-wash-diff'
73 (declare-function ansi-color-apply-on-region "ansi-color")
74 ;; For `magit-diff-wash-submodule'
75 (declare-function magit-log-wash-log "magit-log" (style args))
76 ;; For keymaps and menus
77 (declare-function magit-apply "magit-apply" (&rest args))
78 (declare-function magit-stage "magit-apply" (&optional indent))
79 (declare-function magit-unstage "magit-apply" ())
80 (declare-function magit-discard "magit-apply" ())
81 (declare-function magit-reverse "magit-apply" (&rest args))
82 (declare-function magit-file-rename "magit-files" (file newname))
83 (declare-function magit-file-untrack "magit-files" (files &optional force))
84 (declare-function magit-commit-add-log "magit-commit" ())
85 (declare-function magit-diff-trace-definition "magit-log" ())
86 (declare-function magit-patch-save "magit-patch" (files &optional arg))
87 (declare-function magit-do-async-shell-command "magit-dired" (file))
88 (declare-function magit-add-change-log-entry "magit-extras"
89 (&optional whoami file-name other-window))
90 (declare-function magit-add-change-log-entry-other-window "magit-extras"
91 (&optional whoami file-name))
92 (declare-function magit-diff-edit-hunk-commit "magit-extras" ())
93 (declare-function magit-smerge-keep-current "magit-apply" ())
94 (declare-function magit-smerge-keep-all "magit-apply" ())
95 (declare-function magit-smerge-keep-upper "magit-apply" ())
96 (declare-function magit-smerge-keep-base "magit-apply" ())
97 (declare-function magit-smerge-keep-lower "magit-apply" ())
98
99 (eval-and-compile ;declare slot names
100 (cl-pushnew 'orig-rev eieio--known-slot-names)
101 (cl-pushnew 'action-type eieio--known-slot-names)
102 (cl-pushnew 'target eieio--known-slot-names))
103
104 (define-obsolete-variable-alias 'magit-diff-section-base-map
105 'magit-diff-section-map "Magit 4.0.0")
106
107 (define-obsolete-variable-alias 'magit-wash-message-hook
108 'magit-revision-wash-message-hook "Magit 4.3.0")
109
110 (make-obsolete-variable 'magit-diff-highlight-keywords
111 'magit-revision-wash-message-hook
112 "Magit 4.3.0")
113
114 ;;; Options
115 ;;;; Diff Mode
116
117 (defgroup magit-diff nil
118 "Inspect and manipulate Git diffs."
119 :link '(info-link "(magit)Diffing")
120 :group 'magit-commands
121 :group 'magit-modes)
122
123 (defcustom magit-diff-mode-hook nil
124 "Hook run after entering Magit-Diff mode."
125 :group 'magit-diff
126 :type 'hook)
127
128 (defcustom magit-diff-sections-hook
129 (list #'magit-insert-diff
130 #'magit-insert-xref-buttons)
131 "Hook run to insert sections into a `magit-diff-mode' buffer."
132 :package-version '(magit . "2.3.0")
133 :group 'magit-diff
134 :type 'hook)
135
136 (defcustom magit-diff-expansion-threshold 60
137 "After how many seconds not to expand anymore diffs.
138
139 Except in status buffers, diffs usually start out fully expanded.
140 Because that can take a long time, all diffs that haven't been
141 fontified during a refresh before the threshold defined here are
142 instead displayed with their bodies collapsed.
143
144 Note that this can cause sections that were previously expanded
145 to be collapsed. So you should not pick a very low value here.
146
147 The hook function `magit-diff-expansion-threshold' has to be a
148 member of `magit-section-set-visibility-hook' for this option
149 to have any effect."
150 :package-version '(magit . "2.9.0")
151 :group 'magit-diff
152 :type 'float)
153
154 (defcustom magit-diff-highlight-hunk-body t
155 "Whether to highlight bodies of selected hunk sections."
156 :package-version '(magit . "2.1.0")
157 :group 'magit-diff
158 :type 'boolean)
159
160 (defcustom magit-diff-highlight-hunk-region-functions
161 (list #'magit-diff-highlight-hunk-region-dim-outside
162 #'magit-diff-highlight-hunk-region-using-overlays)
163 "The functions used to highlight the hunk-internal region.
164
165 `magit-diff-highlight-hunk-region-dim-outside' overlays the outside
166 of the hunk internal selection with a face that causes the added and
167 removed lines to have the same background color as context lines.
168 This function should not be removed from the value of this option.
169
170 `magit-diff-highlight-hunk-region-using-overlays' and
171 `magit-diff-highlight-hunk-region-using-underline' emphasize the
172 region by placing delimiting horizontal lines before and after it.
173 The underline variant was implemented because Eli said that is
174 how we should do it. However the overlay variant actually works
175 better. Also see https://github.com/magit/magit/issues/2758.
176
177 Instead of, or in addition to, using delimiting horizontal lines,
178 to emphasize the boundaries, you may wish to emphasize the text
179 itself, using `magit-diff-highlight-hunk-region-using-face'.
180
181 In terminal frames it's not possible to draw lines as the overlay
182 and underline variants normally do, so there they fall back to
183 calling the face function instead."
184 :package-version '(magit . "2.9.0")
185 :set-after '(magit-diff-show-lines-boundaries)
186 :group 'magit-diff
187 :type 'hook
188 :options (list #'magit-diff-highlight-hunk-region-dim-outside
189 #'magit-diff-highlight-hunk-region-using-underline
190 #'magit-diff-highlight-hunk-region-using-overlays
191 #'magit-diff-highlight-hunk-region-using-face))
192
193 (defcustom magit-diff-unmarked-lines-keep-foreground t
194 "Whether `magit-diff-highlight-hunk-region-dim-outside' preserves foreground.
195 When this is set to nil, then that function only adjusts the
196 foreground color but added and removed lines outside the region
197 keep their distinct foreground colors."
198 :package-version '(magit . "2.9.0")
199 :group 'magit-diff
200 :type 'boolean)
201
202 (defcustom magit-diff-refine-hunk nil
203 "Whether to refine hunks to show word-granularity differences.
204
205 `nil' Never show fine differences.
206 `all' Show fine differences for all displayed diff hunks.
207 `t' Refine each hunk once it becomes the current section.
208 Keep the refinement when another section is selected.
209 Refreshing the buffer removes all refinement. This
210 variant is only provided for performance reasons."
211 :group 'magit-diff
212 :local 'permanent-only
213 :safe (##memq % '(nil t all))
214 :type '(choice (const :tag "No refinement" nil)
215 (const :tag "Immediately refine all hunks" all)
216 (const :tag "Refine each hunk when moving to it" t)))
217 (put 'magit-diff-refine-hunk 'permanent-local t) ;< 31.1
218
219 (defcustom magit-diff-fontify-hunk nil
220 "Whether to apply syntax highlighting to diff hunks.
221
222 `nil' Never fontify diff hunks.
223 `all' Fontify all diff hunks.
224 `t' Fontify each hunk once it becomes the current section.
225 Keep the fontification when another section is selected.
226 Refreshing the buffer removes all fontification. This
227 variant is only provided for performance reasons.
228
229 If this is enabled, then `magit-diff-specify-hunk-foreground' should
230 be disabled. Also consider enabling `magit-diff-use-indicator-faces'.
231 Emacs has to be restarted, after changing the value of the former.
232
233 This is considered experimental and is disabled by default, because the
234 fontification is done synchronously, and that can lead to a noticeable
235 delay. The plan is to make it asynchronous, probably with the help of
236 the new `futur' package, which itself still under heavy development."
237 :package-version '(magit . "4.6.0")
238 :group 'magit-diff
239 :local 'permanent-only
240 :safe (##memq % '(nil t all))
241 :type '(choice (const :tag "No fontification" nil)
242 (const :tag "Immediately fontify all hunks" all)
243 (const :tag "Fontify each hunk when moving to it" t)))
244 (put 'magit-diff-fontify-hunk 'permanent-local t) ;< 31.1
245
246 (defcustom magit-diff-refine-ignore-whitespace smerge-refine-ignore-whitespace
247 "Whether to ignore whitespace changes in word-granularity differences."
248 :package-version '(magit . "3.0.0")
249 :set-after '(smerge-refine-ignore-whitespace)
250 :group 'magit-diff
251 :safe 'booleanp
252 :type 'boolean)
253
254 (defcustom magit-diff-adjust-tab-width nil
255 "Whether to adjust the width of tabs in diffs.
256
257 Determining the correct width can be expensive if it requires
258 opening large and/or many files, so the widths are cached in
259 the variable `magit-diff--tab-width-cache'. Set that to `nil'
260 to invalidate the cache.
261
262 `nil' Never adjust tab width. Use `tab-width's value from
263 the Magit buffer itself instead.
264
265 `t' If the corresponding file-visiting buffer exits, then
266 use `tab-width's value from that buffer. Doing this is
267 cheap, so this value is used even if a corresponding
268 cache entry exists.
269
270 `always' If there is no such buffer, then temporarily visit the
271 file to determine the value.
272
273 NUMBER Like `always', but don't visit files larger than NUMBER
274 bytes."
275 :package-version '(magit . "2.12.0")
276 :group 'magit-diff
277 :type '(choice (const :tag "Never" nil)
278 (const :tag "If file-visiting buffer exists" t)
279 (integer :tag "If file isn't larger than N bytes")
280 (const :tag "Always" always)))
281
282 (defcustom magit-diff-paint-whitespace t
283 "Specify where to highlight whitespace errors.
284
285 `nil' Never highlight whitespace errors.
286 `t' Highlight whitespace errors everywhere.
287 `uncommitted' Only highlight whitespace errors in diffs
288 showing uncommitted changes.
289
290 For backward compatibility `status' is treated as a synonym
291 for `uncommitted'.
292
293 The option `magit-diff-paint-whitespace-lines' controls for
294 what lines (added/remove/context) errors are highlighted.
295
296 The options `magit-diff-highlight-trailing' and
297 `magit-diff-highlight-indentation' control what kind of
298 whitespace errors are highlighted."
299 :group 'magit-diff
300 :safe (##memq % '(t nil uncommitted status))
301 :type '(choice (const :tag "In all diffs" t)
302 (const :tag "Only in uncommitted changes" uncommitted)
303 (const :tag "Never" nil)))
304
305 (defcustom magit-diff-paint-whitespace-lines t
306 "Specify in what kind of lines to highlight whitespace errors.
307
308 `t' Highlight only in added lines.
309 `both' Highlight in added and removed lines.
310 `all' Highlight in added, removed and context lines."
311 :package-version '(magit . "3.0.0")
312 :group 'magit-diff
313 :safe (##memq % '(t both all))
314 :type '(choice (const :tag "In added lines" t)
315 (const :tag "In added and removed lines" both)
316 (const :tag "In added, removed and context lines" all)))
317
318 (defcustom magit-diff-highlight-trailing t
319 "Whether to highlight whitespace at the end of a line in diffs.
320 Used only when `magit-diff-paint-whitespace' is non-nil."
321 :group 'magit-diff
322 :safe 'booleanp
323 :type 'boolean)
324
325 (defcustom magit-diff-highlight-indentation nil
326 "Highlight the \"wrong\" indentation style.
327 Used only when `magit-diff-paint-whitespace' is non-nil.
328
329 The value is an alist of the form ((REGEXP . INDENT)...). The
330 path to the current repository is matched against each element
331 in reverse order. Therefore if a REGEXP matches, then earlier
332 elements are not tried.
333
334 If the used INDENT is `tabs', highlight indentation with tabs.
335 If INDENT is an integer, highlight indentation with at least
336 that many spaces. Otherwise, highlight neither."
337 :group 'magit-diff
338 :type `(alist :key-type (regexp :tag "Directory regexp")
339 :value-type (choice (const :tag "Tabs" tabs)
340 (natnum :tag "Spaces" :value ,tab-width)
341 (const :tag "Neither" nil))))
342
343 (defcustom magit-diff-hide-trailing-cr-characters
344 (and (memq system-type '(ms-dos windows-nt)) t)
345 "Whether to hide ^M characters at the end of a line in diffs."
346 :package-version '(magit . "2.6.0")
347 :group 'magit-diff
348 :type 'boolean)
349
350 (defcustom magit-diff-extra-stat-arguments nil
351 "Additional arguments to be used alongside \"--stat\".
352
353 A list of zero or more arguments or a function that takes no
354 argument and returns such a list. These arguments are allowed
355 here: \"--stat-width\", \"--stat-name-width\", \"--stat-graph-width\"
356 and \"--compact-summary\". See the git-diff(1) manpage."
357 :package-version '(magit . "3.0.0")
358 :group 'magit-diff
359 :type `(radio (function-item ,#'magit-diff-use-window-width-as-stat-width)
360 function
361 (list string)
362 (const :tag "None" nil)))
363
364 (defcustom magit-format-file-function #'magit-format-file-default
365 "Function used to format lines representing a file.
366
367 This function is used for file headings in diffs, in diffstats and for
368 lists of files (such as the untracked files). Depending on the caller,
369 it receives either three or five arguments; the signature has to be
370 \(kind file face &optional status orig). KIND is one of `diff',
371 `module', `stat' and `list'."
372 :package-version '(magit . "4.3.1")
373 :group 'magit-diff
374 :type `(radio (function-item ,#'magit-format-file-default)
375 (function-item ,#'magit-format-file-all-the-icons)
376 (function-item ,#'magit-format-file-nerd-icons)
377 function))
378
379 ;;;; File Diff
380
381 (defcustom magit-diff-buffer-file-locked t
382 "Whether `magit-diff-buffer-file' uses a dedicated buffer."
383 :package-version '(magit . "2.7.0")
384 :group 'magit-commands
385 :group 'magit-diff
386 :type 'boolean)
387
388 ;;;; Revision Mode
389
390 (defgroup magit-revision nil
391 "Inspect and manipulate Git commits."
392 :link '(info-link "(magit)Revision Buffer")
393 :group 'magit-modes)
394
395 (defcustom magit-revision-mode-hook
396 (list #'bug-reference-mode
397 #'goto-address-mode)
398 "Hook run after entering Magit-Revision mode."
399 :group 'magit-revision
400 :type 'hook
401 :options '(bug-reference-mode
402 goto-address-mode))
403
404 (defcustom magit-revision-sections-hook
405 (list #'magit-insert-revision-tag
406 #'magit-insert-revision-headers
407 #'magit-insert-revision-message
408 #'magit-insert-revision-notes
409 #'magit-insert-revision-diff
410 #'magit-insert-xref-buttons)
411 "Hook run to insert sections into a `magit-revision-mode' buffer."
412 :package-version '(magit . "2.3.0")
413 :group 'magit-revision
414 :type 'hook)
415
416 (defcustom magit-revision-wash-message-hook
417 (list #'magit-highlight-squash-markers
418 #'magit-highlight-bracket-keywords)
419 "Functions used to highlight parts of a commit message.
420
421 These functions are called in order, in a buffer narrowed to the commit
422 message. They should set text properties as they see fit, usually just
423 `font-lock-face'. Before each function is called, point is at the
424 beginning of the narrowed region of the buffer.
425
426 See also the related `magit-log-wash-summary-hook'. You likely want to
427 use the same functions for both hooks."
428 :package-version '(magit . "4.3.0")
429 :group 'magit-log
430 :type 'hook
431 :options (list #'magit-highlight-squash-markers
432 #'magit-highlight-bracket-keywords))
433
434 (defcustom magit-revision-headers-format "\
435 Author: %aN <%aE>
436 AuthorDate: %ad
437 Commit: %cN <%cE>
438 CommitDate: %cd
439 "
440 "Format string used to insert headers in revision buffers.
441
442 All headers in revision buffers are inserted by the section
443 inserter `magit-insert-revision-headers'. Some of the headers
444 are created by calling \"git show --format=FORMAT\" where FORMAT
445 is the format specified here. Other headers are hard coded or
446 subject to option `magit-revision-insert-related-refs'."
447 :package-version '(magit . "2.3.0")
448 :group 'magit-revision
449 :type 'string)
450
451 (defcustom magit-revision-insert-related-refs t
452 "Whether to show related branches in revision buffers.
453
454 `nil' Don't show any related branches.
455 `t' Show related local branches.
456 `all' Show related local and remote branches.
457 `mixed' Show all containing branches and local merged branches.
458
459 See user option `magit-revision-insert-related-refs-display-alist'
460 to hide specific sets of related branches."
461 :package-version '(magit . "2.1.0")
462 :group 'magit-revision
463 :type '(choice (const :tag "Do not" nil)
464 (const :tag "Local only" t)
465 (const :tag "All related" all)
466 (const :tag "All containing, local merged" mixed)))
467
468 (defcustom magit-revision-insert-related-refs-display-alist nil
469 "How `magit-insert-revision-headers' displays related branch types.
470
471 This is an alist, with recognised keys being the symbols
472 `parents', `merged', `contained', `follows', and `precedes';
473 and the supported values for each key being:
474
475 `nil' Hide these related branches.
476 `t' Show these related branches.
477
478 Keys which are not present in the alist have an implicit value `t'
479 \(so the default alist value of `nil' means all related branch types
480 will be shown.)
481
482 The types to be shown are additionally subject to user option
483 `magit-revision-insert-related-refs'."
484 :package-version '(magit . "3.3.1")
485 :group 'magit-revision
486 :type '(alist :key-type (symbol :tag "Type of related branch")
487 :value-type (boolean :tag "Display"))
488 :options (mapcar (lambda (sym)
489 `(,sym (choice (const :tag "Hide" nil)
490 (const :tag "Show" t))))
491 '(parents merged contained follows precedes)))
492
493 (defcustom magit-revision-use-hash-sections 'quicker
494 "Whether to turn hashes inside the commit message into sections.
495
496 If non-nil, then hashes inside the commit message are turned into
497 `commit' sections. There is a trade off to be made between
498 performance and reliability:
499
500 - `slow' calls git for every word to be absolutely sure.
501 - `quick' skips words less than seven characters long.
502 - `quicker' additionally skips words that don't contain a number.
503 - `quickest' uses all words that are at least seven characters
504 long and which contain at least one number as well as at least
505 one letter.
506
507 If `nil', then no hashes are turned into sections, but you can
508 still visit the commit at point using \"RET\"."
509 :package-version '(magit . "2.12.0")
510 :group 'magit-revision
511 :type '(choice (const :tag "Use sections, quickest" quickest)
512 (const :tag "Use sections, quicker" quicker)
513 (const :tag "Use sections, quick" quick)
514 (const :tag "Use sections, slow" slow)
515 (const :tag "Don't use sections" nil)))
516
517 (defcustom magit-revision-show-gravatars nil
518 "Whether to show gravatar images in revision buffers.
519
520 If `nil', then don't insert any gravatar images. If `t', then
521 insert both images. If `author' or `committer', then insert
522 only the respective image.
523
524 If you have customized the option `magit-revision-header-format'
525 and want to insert the images then you might also have to specify
526 where to do so. In that case the value has to be a cons-cell of
527 two regular expressions. The car specifies where to insert the
528 author's image. The top half of the image is inserted right
529 after the matched text, the bottom half on the next line in the
530 same column. The cdr specifies where to insert the committer's
531 image, accordingly. Either the car or the cdr may be `nil'."
532 :package-version '(magit . "2.3.0")
533 :group 'magit-revision
534 :type '(choice
535 (const :tag "Don't show gravatars" nil)
536 (const :tag "Show gravatars" t)
537 (const :tag "Show author gravatar" author)
538 (const :tag "Show committer gravatar" committer)
539 (cons :tag "Show gravatars using custom regexps"
540 (choice (const :tag "No author image" nil)
541 (regexp :tag "Author regexp" "^Author: "))
542 (choice (const :tag "No committer image" nil)
543 (regexp :tag "Committer regexp" "^Commit: ")))))
544
545 (defcustom magit-revision-fill-summary-line nil
546 "Whether to fill excessively long summary lines.
547
548 If this is an integer, then the summary line is filled if it is
549 longer than either the limit specified here or `window-width'.
550
551 You may want to only set this locally in \".dir-locals-2.el\" for
552 repositories known to contain bad commit messages.
553
554 The body of the message is left alone because (a) most people who
555 write excessively long summary lines usually don't add a body and
556 \(b) even people who have the decency to wrap their lines may have
557 a good reason to include a long line in the body sometimes."
558 :package-version '(magit . "2.90.0")
559 :group 'magit-revision
560 :type '(choice (const :tag "Don't fill" nil)
561 (integer :tag "Fill if longer than")))
562
563 (defcustom magit-revision-filter-files-on-follow nil
564 "Whether to honor file filter if log arguments include \"--follow\".
565
566 When a commit is displayed from a log buffer, the resulting revision
567 buffer usually shares the log's file arguments, restricting the diff to
568 those files. However, there's a complication when the log arguments
569 include \"--follow\": if the log follows a file across a rename event,
570 keeping the file restriction would mean showing an empty diff in
571 revision buffers for commits before the rename event.
572
573 When this option is nil, the revision buffer ignores the log's filter
574 if the log arguments include \"--follow\". If non-nil, the log's file filter
575 is always honored."
576 :package-version '(magit . "3.0.0")
577 :group 'magit-revision
578 :type 'boolean)
579
580 (defcustom magit-revision-use-dedicated-buffers nil
581 "Whether `magit-show-commit' uses a dedicated buffer for each revision.
582
583 If this is nil, then each revision is initially displayed in the same
584 per-repository buffer. You can then lock that buffer to the revision;
585 if you later display another revision, the locked buffer continues to
586 display the same revision as before and the new revision is displayed
587 in another buffer.
588
589 If this is t, then each revision is displayed in a dedicated buffer.
590 A revision buffer is only reused (i.e., raised), if it already displays
591 the requested revision. If you enable this, you will quickly accumulate
592 revision buffers and will have to find a way to keep that under control.
593 `magit-{blame,log,status}-maybe-update-revision-buffer' also will stop
594 working, if this is enabled. For these reasons it is recommended, that
595 you do not enable this."
596 :package-version '(magit . "4.7.0")
597 :group 'magit-revision
598 :type 'boolean)
599
600 ;;;; Visit Commands
601
602 (defcustom magit-diff-visit-prefer-worktree nil
603 "Whether `magit-diff-visit-file' prefers visiting the worktree file.
604
605 By default `magit-diff-visit-file' does not do that. Instead it
606 behaves for staged and unstaged changes as it does for committed
607 changes, by visiting a blob from the old/left or new/right side,
608 depending on whether point is on a removed line or not.
609
610 Setting this to nil, causes `magit-diff-visit-file' to always go to
611 the file in the worktree when invoked from anywhere within a staged
612 or unstaged change.
613
614 It is strongly recommended that instead of changing the value of
615 this option, you use the command `magit-diff-visit-worktree-file',
616 which was created for that very purpose. See the description of
617 this option in the manual for an explanation."
618 :package-version '(magit . "4.4.0")
619 :group 'magit-diff
620 :type 'boolean)
621
622 (defcustom magit-diff-visit-previous-blob t
623 "Whether `magit-diff-visit-file' may visit the previous blob.
624
625 When this is t (the default) and point is on a removed line, then
626 `magit-diff-visit-file' visits the blob from the old/left commit,
627 which still has that line, instead of going to the new/right blob,
628 which removes that line.
629
630 Setting this to nil, causes `magit-diff-visit-file' to always go to
631 the new/right blob, even when point is on a removed line. This is
632 very strongly discouraged. Instead place the cursor anywhere else
633 within the hunk but on a removed line, if you want to visit the new
634 side. That way you don't lose the ability to visit the old side."
635 :package-version '(magit . "2.9.0")
636 :group 'magit-diff
637 :type 'boolean)
638
639 ;;; Faces
640 ;;;; Headings
641
642 (defface magit-diff-file-heading
643 '((t :extend t :weight bold))
644 "Face for diff file headings."
645 :group 'magit-faces)
646
647 (defface magit-diff-file-heading-highlight
648 '((t :extend t :inherit magit-section-highlight))
649 "Face for current diff file headings."
650 :group 'magit-faces)
651
652 (defface magit-diff-file-heading-selection
653 '((((class color) (background light))
654 :extend t
655 :inherit magit-diff-file-heading-highlight
656 :foreground "salmon4")
657 (((class color) (background dark))
658 :extend t
659 :inherit magit-diff-file-heading-highlight
660 :foreground "LightSalmon3"))
661 "Face for selected diff file headings."
662 :group 'magit-faces)
663
664 (defface magit-diff-hunk-heading
665 '((((class color) (background light))
666 :extend t
667 :background "grey90"
668 :foreground "grey20")
669 (((class color) (background dark))
670 :extend t
671 :background "grey25"
672 :foreground "grey95"))
673 "Face for diff hunk headings."
674 :group 'magit-faces)
675
676 (defface magit-diff-hunk-heading-highlight
677 '((((class color) (background light))
678 :extend t
679 :background "grey80"
680 :foreground "grey20")
681 (((class color) (background dark))
682 :extend t
683 :background "grey35"
684 :foreground "grey95"))
685 "Face for current diff hunk headings."
686 :group 'magit-faces)
687
688 (defface magit-diff-hunk-heading-selection
689 '((((class color) (background light))
690 :extend t
691 :inherit magit-diff-hunk-heading-highlight
692 :foreground "salmon4")
693 (((class color) (background dark))
694 :extend t
695 :inherit magit-diff-hunk-heading-highlight
696 :foreground "LightSalmon3"))
697 "Face for selected diff hunk headings."
698 :group 'magit-faces)
699
700 (defface magit-diff-conflict-heading
701 '((t :inherit magit-diff-hunk-heading))
702 "Face for conflict markers."
703 :group 'magit-faces)
704
705 (defface magit-diff-conflict-heading-highlight
706 '((t :inherit magit-diff-hunk-heading-highlight))
707 "Face for conflict markers."
708 :group 'magit-faces)
709
710 (defface magit-diff-revision-summary
711 '((t :inherit magit-diff-hunk-heading))
712 "Face for commit message summaries."
713 :group 'magit-faces)
714
715 (defface magit-diff-revision-summary-highlight
716 '((t :inherit magit-diff-hunk-heading-highlight))
717 "Face for highlighted commit message summaries."
718 :group 'magit-faces)
719
720 (defface magit-diff-lines-heading
721 '((((class color) (background light))
722 :extend t
723 :inherit magit-diff-hunk-heading-highlight
724 :background "LightSalmon3")
725 (((class color) (background dark))
726 :extend t
727 :inherit magit-diff-hunk-heading-highlight
728 :foreground "grey80"
729 :background "salmon4"))
730 "Face for diff hunk heading when lines are marked."
731 :group 'magit-faces)
732
733 (defface magit-diff-our-heading
734 `((((class color) (background light))
735 :extend t
736 :background "#aa2222"
737 :foreground "#ffdddd")
738 (((class color) (background dark))
739 :extend t
740 :background "#ffdddd"
741 :foreground "#553333"))
742 "Face for headings of our side in merge conflicts."
743 :group 'magit-faces)
744
745 (defface magit-diff-base-heading
746 `((((class color) (background light))
747 :extend t
748 :background "#aaaa11"
749 :foreground "#ffffcc")
750 (((class color) (background dark))
751 :extend t
752 :background "#ffffcc"
753 :foreground "#555522"))
754 "Face for headings of common base in merge conflicts."
755 :group 'magit-faces)
756
757 (defface magit-diff-their-heading
758 `((((class color) (background light))
759 :extend t
760 :background "#22aa22"
761 :foreground "#ddffdd")
762 (((class color) (background dark))
763 :extend t
764 :background "#ddffdd"
765 :foreground "#335533"))
766 "Face for headings of their side in merge conflicts."
767 :group 'magit-faces)
768
769 ;;;; Lines
770
771 (defcustom magit-diff-specify-hunk-foreground t
772 "Whether to specify foreground colors for hunk faces.
773 Setting this only has an effect if done before Magit is loaded."
774 :package-version '(magit . "4.6.0")
775 :group 'magit-faces
776 :type 'boolean)
777
778 (defface magit-diff-context
779 `((((class color) (background light))
780 :extend t
781 ,@(and magit-diff-specify-hunk-foreground '(:foreground "grey50")))
782 (((class color) (background dark))
783 :extend t
784 ,@(and magit-diff-specify-hunk-foreground '(:foreground "grey70"))))
785 "Face for lines in a diff that are unchanged."
786 :group 'magit-faces)
787
788 (defface magit-diff-removed
789 `((((class color) (background light))
790 :extend t
791 :background "#ffdddd"
792 ,@(and magit-diff-specify-hunk-foreground '(:foreground "#aa2222")))
793 (((class color) (background dark))
794 :extend t
795 :background "#553333"
796 ,@(and magit-diff-specify-hunk-foreground '(:foreground "#ffdddd"))))
797 "Face for lines in a diff that have been removed."
798 :group 'magit-faces)
799
800 (defface magit-diff-added
801 `((((class color) (background light))
802 :extend t
803 :background "#ddffdd"
804 ,@(and magit-diff-specify-hunk-foreground '(:foreground "#22aa22")))
805 (((class color) (background dark))
806 :extend t
807 :background "#335533"
808 ,@(and magit-diff-specify-hunk-foreground '(:foreground "#ddffdd"))))
809 "Face for lines in a diff that have been added."
810 :group 'magit-faces)
811
812 (defface magit-diff-our
813 '((t :inherit magit-diff-removed))
814 "Face for lines in a diff for our side in a conflict."
815 :group 'magit-faces)
816
817 (defface magit-diff-base
818 `((((class color) (background light))
819 :extend t
820 :background "#ffffcc"
821 ,@(and magit-diff-specify-hunk-foreground '(:foreground "#aaaa11")))
822 (((class color) (background dark))
823 :extend t
824 :background "#555522"
825 ,@(and magit-diff-specify-hunk-foreground '(:foreground "#ffffcc"))))
826 "Face for lines in a diff for the base side in a conflict."
827 :group 'magit-faces)
828
829 (defface magit-diff-their
830 '((t :inherit magit-diff-added))
831 "Face for lines in a diff for their side in a conflict."
832 :group 'magit-faces)
833
834 ;;;; Highlights
835
836 (defface magit-diff-context-highlight
837 `((((class color) (background light))
838 :extend t
839 :background "grey95"
840 ,@(and magit-diff-specify-hunk-foreground '(:foreground "grey50")))
841 (((class color) (background dark))
842 :extend t
843 :background "grey20"
844 ,@(and magit-diff-specify-hunk-foreground '(:foreground "grey70"))))
845 "Face for lines in the current context in a diff."
846 :group 'magit-faces)
847
848 (defface magit-diff-removed-highlight
849 `((((class color) (background light))
850 :extend t
851 :background "#eecccc"
852 ,@(and magit-diff-specify-hunk-foreground '(:foreground "#aa2222")))
853 (((class color) (background dark))
854 :extend t
855 :background "#663333"
856 ,@(and magit-diff-specify-hunk-foreground '(:foreground "#eecccc"))))
857 "Face for lines in a diff that have been removed."
858 :group 'magit-faces)
859
860 (defface magit-diff-added-highlight
861 `((((class color) (background light))
862 :extend t
863 :background "#cceecc"
864 ,@(and magit-diff-specify-hunk-foreground '(:foreground "#22aa22")))
865 (((class color) (background dark))
866 :extend t
867 :background "#336633"
868 ,@(and magit-diff-specify-hunk-foreground '(:foreground "#cceecc"))))
869 "Face for lines in a diff that have been added."
870 :group 'magit-faces)
871
872 (defface magit-diff-our-highlight
873 '((t :inherit magit-diff-removed-highlight))
874 "Face for lines in a diff for our side in a conflict."
875 :group 'magit-faces)
876
877 (defface magit-diff-base-highlight
878 `((((class color) (background light))
879 :extend t
880 :background "#eeeebb"
881 ,@(and magit-diff-specify-hunk-foreground '(:foreground "#aaaa11")))
882 (((class color) (background dark))
883 :extend t
884 :background "#666622"
885 ,@(and magit-diff-specify-hunk-foreground '(:foreground "#eeeebb"))))
886 "Face for lines in a diff for the base side in a conflict."
887 :group 'magit-faces)
888
889 (defface magit-diff-their-highlight
890 '((t :inherit magit-diff-added-highlight))
891 "Face for lines in a diff for their side in a conflict."
892 :group 'magit-faces)
893
894 ;;;; Indicators
895
896 (defcustom magit-diff-use-indicator-faces nil
897 "Whether to use separate faces for diff side indicators.
898 If non-nil, use, for example, `magit-diff-removed-indicator' for the
899 plus sign at the beginning of a removed line. If nil, use the same
900 face as for the rest of the line."
901 :package-version '(magit . "4.6.0")
902 :group 'magit-faces
903 :type 'boolean)
904
905 (defface magit-diff-removed-indicator
906 `((((class color) (background light)) :foreground "#aa2222")
907 (((class color) (background dark)) :foreground "#eecccc"))
908 "Face for indicators on lines in a diff that have been removed.
909 Only used if `magit-diff-use-indicator-faces' is non-nil."
910 :group 'magit-faces)
911
912 (defface magit-diff-added-indicator
913 `((((class color) (background light)) :foreground "#22aa22")
914 (((class color) (background dark)) :foreground "#cceecc"))
915 "Face for indicators on lines in a diff that have been added.
916 Only used if `magit-diff-use-indicator-faces' is non-nil."
917 :group 'magit-faces)
918
919 (defface magit-diff-our-indicator
920 '((t :inherit magit-diff-removed-indicator))
921 "Face for indicators on lines in a diff for our side in a conflict.
922 Only used if `magit-diff-use-indicator-faces' is non-nil."
923 :group 'magit-faces)
924
925 (defface magit-diff-base-indicator
926 `((((class color) (background light)) :foreground "#aaaa11")
927 (((class color) (background dark)) :foreground "#ffffcc"))
928 "Face for indicators on lines in a diff for base side in a conflict.
929 Only used if `magit-diff-use-indicator-faces' is non-nil."
930 :group 'magit-faces)
931
932 (defface magit-diff-their-indicator
933 '((t :inherit magit-diff-added-indicator))
934 "Face for indicators on lines in a diff for their side in a conflict.
935 Only used if `magit-diff-use-indicator-faces' is non-nil."
936 :group 'magit-faces)
937
938 ;;;; Diffstats
939
940 (defface magit-diffstat-removed
941 '((((class color) (background light)) :foreground "#aa2222")
942 (((class color) (background dark)) :foreground "#aa4444"))
943 "Face for removal indicator in diffstat."
944 :group 'magit-faces)
945
946 (defface magit-diffstat-added
947 '((((class color) (background light)) :foreground "#22aa22")
948 (((class color) (background dark)) :foreground "#448844"))
949 "Face for addition indicator in diffstat."
950 :group 'magit-faces)
951
952 ;;;; Region
953
954 (defface magit-diff-lines-boundary
955 '((t :extend t :inherit magit-diff-lines-heading))
956 "Face for boundary of marked lines in diff hunk."
957 :group 'magit-faces)
958
959 (defface magit-diff-hunk-region
960 `((t :inherit bold
961 :extend ,(ignore-errors (face-attribute 'region :extend))))
962 "Face used by `magit-diff-highlight-hunk-region-using-face'.
963
964 This face is overlaid over text that uses other hunk faces,
965 and those normally set the foreground and background colors.
966 The `:foreground' and especially the `:background' properties
967 should be avoided here. Setting the latter would cause the
968 loss of information. Good properties to set here are `:weight'
969 and `:slant'."
970 :group 'magit-faces)
971
972 ;;;; Whitespace
973
974 (defface magit-diff-whitespace-warning
975 '((t :inherit trailing-whitespace))
976 "Face for highlighting whitespace errors added lines."
977 :group 'magit-faces)
978
979 ;;; Variables
980
981 (defvar-local magit-buffer-diff-range nil)
982 (defvar-local magit-buffer-diff-range-oids nil)
983 (defvar-local magit-buffer-diff-type nil)
984 (defvar-local magit-buffer-diff-typearg nil)
985 (defvar-local magit-buffer-diff-args nil)
986 (defvar-local magit-buffer-diff-files nil)
987 (defvar-local magit-buffer-diff-files-suspended nil)
988
989 ;; Preserve when refreshing status buffer.
990 (put 'magit-buffer-diff-files-suspended 'permanent-local t)
991
992 ;;; Arguments
993 ;;;; Prefix Classes
994
995 (defclass magit-diff-prefix (transient-prefix)
996 ((history-key :initform 'magit-diff)
997 (major-mode :initform 'magit-diff-mode)))
998
999 (defclass magit-diff-refresh-prefix (magit-diff-prefix)
1000 ((history-key :initform 'magit-diff)
1001 (major-mode :initform nil)))
1002
1003 ;;;; Prefix Methods
1004
1005 (cl-defmethod transient-prefix-value ((obj magit-diff-prefix))
1006 (let ((args (cl-call-next-method obj)))
1007 (list (seq-filter #'atom args)
1008 (cdr (assoc "--" args)))))
1009
1010 (cl-defmethod transient-init-value ((obj magit-diff-prefix))
1011 (pcase-let ((`(,args ,files)
1012 (magit-diff--get-value 'magit-diff-mode 'prefix)))
1013 (when-let ((_(not (eq transient-current-command 'magit-dispatch)))
1014 (file (magit-file-relative-name)))
1015 (setq files (list file)))
1016 (oset obj value (if files `(("--" ,@files) ,@args) args))))
1017
1018 (cl-defmethod transient-init-value ((obj magit-diff-refresh-prefix))
1019 (oset obj value (if magit-buffer-diff-files
1020 `(("--" ,@magit-buffer-diff-files)
1021 ,@magit-buffer-diff-args)
1022 magit-buffer-diff-args)))
1023
1024 (cl-defmethod transient-set-value ((obj magit-diff-prefix))
1025 (magit-diff--set-value obj))
1026
1027 (cl-defmethod transient-save-value ((obj magit-diff-prefix))
1028 (magit-diff--set-value obj 'save))
1029
1030 ;;;; Argument Access
1031
1032 (defun magit-diff-arguments (&optional mode)
1033 "Return the current diff arguments."
1034 (if (memq transient-current-command '(magit-diff magit-diff-refresh))
1035 (transient-args transient-current-command)
1036 (magit-diff--get-value (or mode 'magit-diff-mode) 'direct)))
1037
1038 (defun magit-diff--get-value (mode &optional use-buffer-args)
1039 (setq use-buffer-args
1040 (pcase-exhaustive use-buffer-args
1041 ('prefix magit-prefix-use-buffer-arguments)
1042 ('status magit-status-use-buffer-arguments)
1043 ('direct magit-direct-use-buffer-arguments)
1044 ('nil magit-direct-use-buffer-arguments)
1045 ((or 'always 'selected 'current 'never)
1046 use-buffer-args)))
1047 (cond-let
1048 ((and (memq use-buffer-args '(always selected current))
1049 (eq major-mode mode))
1050 (list magit-buffer-diff-args
1051 magit-buffer-diff-files))
1052 ([_(memq use-buffer-args '(always selected))]
1053 [buffer (magit-get-mode-buffer mode nil (eq use-buffer-args 'selected))]
1054 (list (buffer-local-value 'magit-buffer-diff-args buffer)
1055 (buffer-local-value 'magit-buffer-diff-files buffer)))
1056 ((plist-member (symbol-plist mode) 'magit-diff-current-arguments)
1057 (list (get mode 'magit-diff-current-arguments) nil))
1058 ([elt (assq (intern (format "magit-diff:%s" mode)) transient-values)]
1059 (list (cdr elt) nil))
1060 ((list (get mode 'magit-diff-default-arguments) nil))))
1061
1062 (defun magit-diff--set-value (obj &optional save)
1063 (pcase-let* ((obj (oref obj prototype))
1064 (mode (or (oref obj major-mode) major-mode))
1065 (key (intern (format "magit-diff:%s" mode)))
1066 (`(,args ,files) (transient-args (oref obj command))))
1067 (put mode 'magit-diff-current-arguments args)
1068 (when save
1069 (setf (alist-get key transient-values) args)
1070 (transient-save-values))
1071 (transient--history-push obj)
1072 (setq magit-buffer-diff-args args)
1073 (setq magit-buffer-diff-files files)
1074 (magit-refresh)))
1075
1076 ;;; Commands
1077 ;;;; Prefix Commands
1078
1079 (transient-define-group magit-diff-infix-arguments
1080 ["Limit arguments"
1081 (magit:--)
1082 (magit-diff:--ignore-submodules)
1083 ("-b" "Ignore whitespace changes" ("-b" "--ignore-space-change"))
1084 ("-w" "Ignore all whitespace" ("-w" "--ignore-all-space"))
1085 ("-D" "Omit preimage for deletes" ("-D" "--irreversible-delete")
1086 :level 5)]
1087 ["Context arguments"
1088 (magit-diff:-U)
1089 ("-W" "Show surrounding functions" ("-W" "--function-context"))]
1090 ["Tune arguments"
1091 (magit-diff:--diff-algorithm)
1092 (magit-diff:--diff-merges)
1093 (magit-diff:-M)
1094 (magit-diff:-C :level 5)
1095 ("-H" "Detect copies if source unmodified" "--find-copies-harder" :level 5)
1096 (magit-diff:-R :level 5)
1097 (magit-diff:--color-moved :level 5)
1098 (magit-diff:--color-moved-ws :level 5)
1099 (magit-diff:--no-ext-diff)
1100 (magit-diff:--stat)
1101 (magit-diff:--show-signature)])
1102
1103 ;;;###autoload(autoload 'magit-diff "magit-diff" nil t)
1104 (transient-define-prefix magit-diff ()
1105 "Show changes between different versions."
1106 :man-page "git-diff"
1107 :class 'magit-diff-prefix
1108 'magit-diff-infix-arguments
1109 ["Actions"
1110 [("d" "Dwim" magit-diff-dwim)
1111 ("r" "Diff range" magit-diff-range)
1112 ("p" "Diff paths" magit-diff-paths)]
1113 [("u" "Diff unstaged" magit-diff-unstaged)
1114 ("s" "Diff staged" magit-diff-staged)
1115 ("w" "Diff worktree" magit-diff-working-tree)]
1116 [("c" "Show commit" magit-show-commit)
1117 ("t" "Show stash" magit-stash-show)]])
1118
1119 ;;;###autoload(autoload 'magit-diff-refresh "magit-diff" nil t)
1120 (transient-define-prefix magit-diff-refresh ()
1121 "Change the arguments used for the diff(s) in the current buffer."
1122 :man-page "git-diff"
1123 :class 'magit-diff-refresh-prefix
1124 'magit-diff-infix-arguments
1125 [["Refresh"
1126 ("g" "buffer" magit-diff-refresh)
1127 ("s" "buffer and set defaults" transient-set-and-exit)
1128 ("w" "buffer and save defaults" transient-save-and-exit)]
1129 ["Toggle"
1130 ("t" "hunk refinement" magit-diff-toggle-refine-hunk)
1131 ("T" "hunk fontification" magit-diff-toggle-fontify-hunk)
1132 ("F" "file filter" magit-diff-toggle-file-filter)
1133 ("b" "buffer lock" magit-toggle-buffer-lock
1134 :if-mode (magit-diff-mode magit-revision-mode magit-stash-mode))]
1135 [:if-mode magit-diff-mode
1136 :description "Do"
1137 ("r" "switch range type" magit-diff-switch-range-type)
1138 ("f" "flip revisions" magit-diff-flip-revs)]]
1139 (interactive)
1140 (when (derived-mode-p 'magit-merge-preview-mode)
1141 (user-error "Cannot use %s in %s" this-command major-mode))
1142 (if (not (eq transient-current-command 'magit-diff-refresh))
1143 (transient-setup 'magit-diff-refresh)
1144 (pcase-let ((`(,args ,files) (magit-diff-arguments)))
1145 (setq magit-buffer-diff-args args)
1146 (setq magit-buffer-diff-files files))
1147 (magit-refresh)))
1148
1149 ;;;; Infix Commands
1150
1151 (transient-define-argument magit:-- ()
1152 :description "Limit to files"
1153 :class 'transient-files
1154 :key "--"
1155 :argument "--"
1156 :prompt "Limit to file,s: "
1157 :reader #'magit-read-files
1158 :multi-value t)
1159
1160 (defun magit-read-files (prompt initial-input history &optional list-fn)
1161 (magit-completing-read-multiple prompt
1162 (funcall (or list-fn #'magit-list-files))
1163 nil nil
1164 (or initial-input (magit-file-at-point))
1165 history))
1166
1167 (transient-define-argument magit-diff:-U ()
1168 :description "Context lines"
1169 :class 'transient-option
1170 :argument "-U"
1171 :reader #'transient-read-number-N0)
1172
1173 (transient-define-argument magit-diff:-M ()
1174 :description "Detect renames"
1175 :class 'transient-option
1176 :argument "-M"
1177 :allow-empty t
1178 :reader #'magit-read-similarity-index)
1179
1180 (transient-define-argument magit-diff:-C ()
1181 :description "Detect copies"
1182 :class 'transient-option
1183 :argument "-C"
1184 :allow-empty t
1185 :reader #'magit-read-similarity-index)
1186
1187 (defun magit-read-similarity-index (prompt initial-input history)
1188 "Read a similarity index. See \"--find-renames\" in git-diff(1) manpage."
1189 (catch 'valid
1190 (while t
1191 (let ((input (read-from-minibuffer prompt initial-input nil nil history)))
1192 (if (string-match-p "\\`[0-9]+%?\\'" input)
1193 (throw 'valid input)
1194 (message "Please enter a percentage ending in %%, %s"
1195 "or a fraction, omitting the implied leading \"0.\"")
1196 (sit-for 1))))))
1197
1198 (transient-define-argument magit-diff:--diff-algorithm ()
1199 :description "Diff algorithm"
1200 :class 'transient-option
1201 :key "-A"
1202 :argument "--diff-algorithm="
1203 :reader #'magit-diff-select-algorithm
1204 :always-read t)
1205
1206 (defun magit-diff-select-algorithm (&rest _ignore)
1207 (magit-read-char-case nil t
1208 (?u "[u]nspecified" nil)
1209 (?d "[d]efault" "default")
1210 (?m "[m]inimal" "minimal")
1211 (?p "[p]atience" "patience")
1212 (?h "[h]istogram" "histogram")))
1213
1214 (transient-define-argument magit-diff:--diff-merges ()
1215 :description "Diff merges"
1216 :class 'transient-option
1217 :key "-X"
1218 :argument "--diff-merges="
1219 :reader #'magit-diff-select-merges
1220 :always-read t)
1221
1222 (defun magit-diff-select-merges (&rest _ignore)
1223 (magit-read-char-case nil t
1224 (?u "[u]nspecified" nil)
1225 (?o "[o]ff" "off")
1226 (?f "[f]irst-parent" "first-parent")
1227 (?c "[c]ombined" "combined")
1228 (?d "[d]ense-combined" "dense-combined")))
1229
1230 (transient-define-argument magit-diff:--ignore-submodules ()
1231 :description "Ignore submodules"
1232 :class 'transient-option
1233 :key "-i"
1234 :argument "--ignore-submodules="
1235 :reader #'magit-diff-select-ignore-submodules)
1236
1237 (defun magit-diff-select-ignore-submodules (&rest _ignored)
1238 (magit-read-char-case "Ignore submodules " t
1239 (?n "[n]one" "none")
1240 (?u "[u]ntracked" "untracked")
1241 (?d "[d]irty" "dirty")
1242 (?a "[a]ll" "all")))
1243
1244 (transient-define-argument magit-diff:--color-moved ()
1245 :description "Color moved lines"
1246 :class 'transient-option
1247 :key "-m"
1248 :argument "--color-moved="
1249 :reader #'magit-diff-select-color-moved-mode)
1250
1251 (defun magit-diff-select-color-moved-mode (&rest _ignore)
1252 (magit-read-char-case "Color moved " t
1253 (?d "[d]efault" "default")
1254 (?p "[p]lain" "plain")
1255 (?b "[b]locks" "blocks")
1256 (?z "[z]ebra" "zebra")
1257 (?Z "[Z] dimmed-zebra" "dimmed-zebra")))
1258
1259 (transient-define-argument magit-diff:--color-moved-ws ()
1260 :description "Whitespace treatment for --color-moved"
1261 :class 'transient-option
1262 :key "=w"
1263 :argument "--color-moved-ws="
1264 :reader #'magit-diff-select-color-moved-ws-mode)
1265
1266 (defun magit-diff-select-color-moved-ws-mode (&rest _ignore)
1267 (magit-read-char-case "Ignore whitespace " t
1268 (?i "[i]ndentation" "allow-indentation-change")
1269 (?e "[e]nd of line" "ignore-space-at-eol")
1270 (?s "[s]pace change" "ignore-space-change")
1271 (?a "[a]ll space" "ignore-all-space")
1272 (?n "[n]o" "no")))
1273
1274 (transient-define-argument magit-diff:-R ()
1275 :description "Reverse sides"
1276 :class 'transient-switch
1277 :argument "-R"
1278 :if 'magit-diff-argument-predicate)
1279
1280 (transient-define-argument magit-diff:--no-ext-diff ()
1281 :description "Disallow external diff drivers"
1282 :class 'transient-switch
1283 :argument "--no-ext-diff"
1284 :key "-x")
1285
1286 (transient-define-argument magit-diff:--stat ()
1287 :description "Show stats"
1288 :class 'transient-switch
1289 :argument "--stat"
1290 :key "-s"
1291 :if 'magit-diff-argument-predicate)
1292
1293 (transient-define-argument magit-diff:--show-signature ()
1294 :description "Show signature"
1295 :class 'transient-switch
1296 :argument "--show-signature"
1297 :key "=g"
1298 :if 'magit-diff-argument-predicate)
1299
1300 (defun magit-diff-argument-predicate ()
1301 (or (eq (oref transient--prefix command) 'magit-diff)
1302 (derived-mode-p 'magit-diff-mode)))
1303
1304 ;;;; Setup Commands
1305
1306 ;;;###autoload
1307 (defun magit-diff-dwim (&optional args files)
1308 "Show changes for the thing at point.
1309
1310 For example, if point is on a commit, show the changes introduced by
1311 that commit. Likewise if point is on the section titled \"Unstaged
1312 changes\", then show those changes in a separate buffer. Generally
1313 speaking, compare the thing at point with the most logical, trivial
1314 and (in *any* situation) at least potentially useful other thing it
1315 could be compared to.
1316
1317 When the region selects commits, then compare the two commits at
1318 either end. There are different ways two commits can be compared.
1319 In the buffer showing the diff, you can control how the comparison,
1320 is done, using \"D r\" and \"D f\".
1321
1322 This function does not always show the changes that you might want
1323 to view in any given situation. You can think of the changes being
1324 shown as the smallest common denominator. There is no AI involved.
1325 If this command never does what you want, then ignore it, and instead
1326 use the commands that allow you to explicitly specify what you need."
1327 (interactive (magit-diff-arguments))
1328 (let ((default-directory default-directory)
1329 (section (magit-current-section)))
1330 (cond
1331 ((magit-section-match 'module section)
1332 (setq default-directory
1333 (expand-file-name
1334 (file-name-as-directory (oref section value))))
1335 (magit-diff-range (oref section range)))
1336 (t
1337 (when (magit-section-match 'module-commit section)
1338 (setq args nil)
1339 (setq files nil)
1340 (setq default-directory
1341 (expand-file-name
1342 (file-name-as-directory (magit-section-parent-value section)))))
1343 (pcase (magit-diff--dwim)
1344 ('unmerged (magit-diff-unmerged args files))
1345 ('unstaged (magit-diff-unstaged args files))
1346 ('staged
1347 (let ((file (magit-file-at-point)))
1348 (if (and file (equal (cddr (car (magit-file-status file))) '(?D ?U)))
1349 ;; File was deleted by us and modified by them. Show the latter.
1350 (magit-diff-unmerged args (list file))
1351 (magit-diff-staged nil args files))))
1352 (`(stash . ,value) (magit-stash-show value args))
1353 (`(commit . ,value)
1354 (magit-diff-range (format "%s^..%s" value value) args files))
1355 ((and range (pred stringp))
1356 (magit-diff-range range args files))
1357 (_ (call-interactively #'magit-diff-range)))))))
1358
1359 (defun magit-diff--dwim ()
1360 "Return information for performing DWIM diff.
1361
1362 The information can be in three forms:
1363 1. TYPE
1364 A symbol describing a type of diff where no additional information
1365 is needed to generate the diff. One of `staged', `unstaged',
1366 `unmerged', and `undefined'.
1367 2. (TYPE . VALUE)
1368 Like #1 but the diff requires additional information, which is
1369 given by VALUE. Currently, this includes `commit' and `stash',
1370 where VALUE is the given commit or stash, respectively.
1371 3. RANGE
1372 A string indicating a diff range.
1373
1374 If no DWIM context is found, nil is returned."
1375 (cond-let
1376 ([commits (magit-region-values '(commit branch) t)]
1377 (deactivate-mark)
1378 (concat (car (last commits)) ".." (car commits)))
1379 ((derived-mode-p 'magit-stash-mode)
1380 (cons 'commit
1381 (magit-section-case
1382 (commit (oref it value))
1383 (file (thread-first it
1384 (oref parent)
1385 (oref value)))
1386 (hunk (thread-first it
1387 (oref parent)
1388 (oref parent)
1389 (oref value))))))
1390 (magit-buffer-revision
1391 (cons 'commit magit-buffer-revision))
1392 ((derived-mode-p 'magit-diff-mode)
1393 (pcase-exhaustive magit-buffer-diff-type
1394 ('committed magit-buffer-diff-range)
1395 ((or 'unstaged 'staged 'undefined) magit-buffer-diff-type)))
1396 ((magit-section-case
1397 ([* unstaged] 'unstaged)
1398 ([* staged] 'staged)
1399 (unmerged 'unmerged)
1400 (unpushed (magit-diff--range-to-endpoints (oref it value)))
1401 (unpulled (magit-diff--range-to-endpoints (oref it value)))
1402 (branch (cond-let
1403 [[current (magit-get-current-branch)]
1404 [atpoint (oref it value)]]
1405 ((not (equal atpoint current))
1406 (format "%s...%s" (or current "HEAD") atpoint))
1407 ([upstream (magit-get-upstream-branch)]
1408 (format "%s...%s" upstream current))
1409 ((magit-anything-modified-p)
1410 current)
1411 ((cons 'commit current))))
1412 (commit (cons 'commit (oref it value)))
1413 ([file commit] (cons 'commit (oref (oref it parent) value)))
1414 ([hunk file commit]
1415 (cons 'commit (oref (oref (oref it parent) parent) value)))
1416 (stash (cons 'stash (oref it value)))
1417 (pullreq (forge--pullreq-range (oref it value) t))))))
1418
1419 (defun magit-diff--range-to-endpoints (range)
1420 (cond ((string-match "\\.\\.\\." range) (replace-match ".." nil nil range))
1421 ((string-match "\\.\\." range) (replace-match "..." nil nil range))
1422 (range)))
1423
1424 (defun magit-diff--region-range (&optional interactive mbase)
1425 (and-let* ((commits (magit-region-values '(commit branch) t))
1426 (revA (car (last commits)))
1427 (revB (car commits)))
1428 (progn
1429 (when interactive
1430 (deactivate-mark))
1431 (if mbase
1432 (let ((base (magit-merge-base revA revB)))
1433 (cond
1434 ((string= (magit-rev-parse revA) base)
1435 (format "%s..%s" revA revB))
1436 ((string= (magit-rev-parse revB) base)
1437 (format "%s..%s" revB revA))
1438 (interactive
1439 (let ((main (magit-completing-read "View changes along"
1440 (list revA revB)
1441 nil t nil nil revB)))
1442 (format "%s...%s"
1443 (if (string= main revB) revA revB) main)))
1444 ((format "%s...%s" revA revB))))
1445 (format "%s..%s" revA revB)))))
1446
1447 (defun magit-diff-read-range-or-commit (prompt &optional secondary-default mbase)
1448 "Read range or revision with special diff range treatment.
1449 If MBASE is non-nil, prompt for which rev to place at the end of
1450 a \"revA...revB\" range. Otherwise, always construct
1451 \"revA..revB\" range."
1452 (or (magit-diff--region-range t mbase)
1453 (magit-read-range prompt
1454 (or (pcase (magit-diff--dwim)
1455 (`(commit . ,value)
1456 (format "%s^..%s" value value))
1457 ((and range (pred stringp))
1458 range))
1459 secondary-default
1460 (magit-get-current-branch)))))
1461
1462 ;;;###autoload
1463 (defun magit-diff-range (rev-or-range &optional args files)
1464 "Show differences between two commits.
1465
1466 REV-OR-RANGE should be a range or a single revision. If it is a
1467 revision, then show changes in the working tree relative to that
1468 revision. If it is a range, but one side is omitted, then show
1469 changes relative to `HEAD'.
1470
1471 If the region is active, use the revisions on the first and last
1472 line of the region as the two sides of the range. With a prefix
1473 argument, instead of diffing the revisions, choose a revision to
1474 view changes along, starting at the common ancestor of both
1475 revisions (i.e., use a \"...\" range)."
1476 (interactive (cons (magit-diff-read-range-or-commit "Diff for range"
1477 nil current-prefix-arg)
1478 (magit-diff-arguments)))
1479 (magit-diff-setup-buffer rev-or-range nil args files 'committed))
1480
1481 ;;;###autoload
1482 (defun magit-diff-working-tree (&optional rev args files)
1483 "Show changes between the current working tree and the `HEAD' commit.
1484 With a prefix argument show changes between the working tree and
1485 a commit read from the minibuffer."
1486 (interactive
1487 (cons (and current-prefix-arg
1488 (magit-read-branch-or-commit "Diff working tree and commit"))
1489 (magit-diff-arguments)))
1490 (magit-diff-setup-buffer (or rev "HEAD") nil args files 'committed))
1491
1492 ;;;###autoload
1493 (defun magit-diff-staged (&optional rev args files)
1494 "Show changes between the index and the `HEAD' commit.
1495 With a prefix argument show changes between the index and
1496 a commit read from the minibuffer."
1497 (interactive
1498 (cons (and current-prefix-arg
1499 (magit-read-branch-or-commit "Diff index and commit"))
1500 (magit-diff-arguments)))
1501 (magit-diff-setup-buffer rev "--cached" args files 'staged))
1502
1503 ;;;###autoload
1504 (defun magit-diff-unstaged (&optional args files)
1505 "Show changes between the working tree and the index."
1506 (interactive (magit-diff-arguments))
1507 (magit-diff-setup-buffer nil nil args files 'unstaged))
1508
1509 ;;;###autoload
1510 (defun magit-diff-unmerged (&optional args files)
1511 "Show changes that are being merged."
1512 (interactive (magit-diff-arguments))
1513 (unless (magit-merge-in-progress-p)
1514 (user-error "No merge is in progress"))
1515 (magit-diff-setup-buffer (magit--merge-range) nil args files 'committed))
1516
1517 ;;;###autoload
1518 (defun magit-diff-while-committing ()
1519 "While committing, show the changes that are about to be committed.
1520 While amending, invoking the command again toggles between
1521 showing just the new changes or all the changes that will
1522 be committed."
1523 (interactive)
1524 (unless (magit-commit-message-buffer)
1525 (user-error "No commit in progress"))
1526 (magit-commit-diff--show))
1527
1528 ;;;###autoload
1529 (defun magit-diff-buffer-file (&optional unstaged-only)
1530 "Show diff for the blob or file visited in the current buffer.
1531
1532 Limit the diff to the file or blob.
1533
1534 When the buffer visits a blob, then show the respective commit.
1535 When the buffer visits a file, then show the differences between
1536 `HEAD' and the working tree, or the index with a prefix argument."
1537 (interactive (list current-prefix-arg))
1538 (require 'magit)
1539 (if-let ((file (magit-file-relative-name)))
1540 (if magit-buffer-revision
1541 (magit-show-commit magit-buffer-revision
1542 (car (magit-show-commit--arguments))
1543 (list file))
1544 (save-buffer)
1545 (let ((line (line-number-at-pos))
1546 (col (current-column)))
1547 (with-current-buffer
1548 (magit-diff-setup-buffer (and (not unstaged-only)
1549 (or (magit-get-current-branch)
1550 "HEAD"))
1551 nil
1552 (car (magit-diff-arguments))
1553 (list file)
1554 'unstaged
1555 magit-diff-buffer-file-locked)
1556 (magit-diff--goto-file-position file line col))))
1557 (user-error "Buffer isn't visiting a file")))
1558
1559 ;;;###autoload
1560 (defun magit-diff-paths (a b)
1561 "Show changes between any two files on disk."
1562 (interactive (list (read-file-name "First file: " nil nil t)
1563 (read-file-name "Second file: " nil nil t)))
1564 (magit-diff-setup-buffer nil "--no-index" nil
1565 (list (magit-convert-filename-for-git
1566 (expand-file-name a))
1567 (magit-convert-filename-for-git
1568 (expand-file-name b)))
1569 'undefined))
1570
1571 (defun magit-show-commit--arguments ()
1572 (pcase-let ((`(,args ,diff-files)
1573 (magit-diff-arguments 'magit-revision-mode)))
1574 (list args (if (derived-mode-p 'magit-log-mode)
1575 (and (or magit-revision-filter-files-on-follow
1576 (not (member "--follow" magit-buffer-log-args)))
1577 magit-buffer-log-files)
1578 diff-files))))
1579
1580 ;;;###autoload
1581 (defun magit-show-commit (rev &optional args files module)
1582 "Visit the revision at point in another buffer.
1583
1584 If there is no revision at point, or with a prefix argument, prompt
1585 for a revision.
1586
1587 By default the same per-repository revision buffer is reused for all
1588 revision. When you want to display multiple revisions at the same
1589 time, you can lock a revision buffer to its value, which prevents it
1590 from being reused to display another revision.
1591
1592 Alternatively, you can use a dedicated buffer for every revision.
1593 To do so, enable `magit-revision-use-dedicated-buffers', but only
1594 after readings its docstring, to make sure you are can live with
1595 the trade-offs."
1596 (interactive
1597 (pcase-let* ((mcommit (magit-section-value-if 'module-commit))
1598 (atpoint (or mcommit
1599 (magit-thing-at-point 'git-revision t)
1600 (magit-branch-or-commit-at-point)))
1601 (`(,args ,files) (magit-show-commit--arguments)))
1602 (list (or (and (not current-prefix-arg) atpoint)
1603 (magit-read-branch-or-commit "Show commit" atpoint))
1604 args
1605 files
1606 (and mcommit
1607 (magit-section-parent-value (magit-current-section))))))
1608 (require 'magit)
1609 (let* ((file (magit-file-relative-name))
1610 (line (and file (line-number-at-pos))))
1611 (magit-with-toplevel
1612 (when module
1613 (setq default-directory
1614 (expand-file-name (file-name-as-directory module))))
1615 (unless (magit-commit-oid rev t)
1616 (user-error "%s cannot be dereferenced as a commit" rev))
1617 (when file
1618 (save-buffer))
1619 (let ((buf (magit-revision-setup-buffer rev args files)))
1620 (when file
1621 (let ((line (magit-diff-visit--offset line file "-R" rev))
1622 (col (current-column)))
1623 (with-current-buffer buf
1624 (magit-diff--goto-file-position file line col))))))))
1625
1626 ;;;###autoload
1627 (defun magit-show-commit-removing-file (file &optional args)
1628 "Show the commit that removed FILE."
1629 (interactive
1630 (let* ((files (magit-removed-files))
1631 (default (magit-file-relative-name))
1632 (default (and default (member default files) default)))
1633 (list (magit-completing-read "Show commit removing file: "
1634 files nil nil nil nil default)
1635 (car (magit-show-commit--arguments)))))
1636 (if-let ((rev (magit-with-toplevel
1637 (magit-git-string "log" "--format=%H" "--diff-filter=D"
1638 "--full-history" "-n" "1" "--" file))))
1639 (magit-show-commit rev args)
1640 (error "%s has not been removed" file)))
1641
1642 (defun magit-diff--locate-file-position (file line column &optional parent)
1643 (and-let*
1644 ((parent (pcase parent
1645 ('unstaged (magit-get-section '((unstaged) (status))))
1646 ('staged (magit-get-section '((staged) (status))))
1647 ('nil (and (cl-typep (car (oref magit-root-section children))
1648 'magit-file-section)
1649 magit-root-section))
1650 (_ parent)))
1651 (diff (cl-find-if (##equal (oref % value) file)
1652 (oref parent children)))
1653 (hunks (oref diff children)))
1654 (let (hunk pos found)
1655 (while (and (setq hunk (pop hunks))
1656 (not pos))
1657 (when-let* ((range (oref hunk to-range))
1658 (beg (car range))
1659 (len (cadr range))
1660 (end (+ beg len)))
1661 (cond
1662 ((> beg line)
1663 (setq pos (oref diff start)))
1664 ((<= beg line end)
1665 (save-excursion
1666 (goto-char (oref hunk content))
1667 (let ((l beg))
1668 (while (or (< l line)
1669 (= (char-after) ?-))
1670 (unless (= (char-after) ?-)
1671 (incf l))
1672 (forward-line)))
1673 (setq found (if (= (char-after) ?+) 'line 'hunk))
1674 (forward-char (1+ column))
1675 (setq pos (point))))
1676 ((null hunks)
1677 (setq pos (oref hunk start))))))
1678 (and pos
1679 (list pos (or found file))))))
1680
1681 (defun magit-diff--goto-file-position (file line column &optional parent)
1682 (when-let ((pos (magit-diff--locate-file-position file line column parent)))
1683 (goto-char (car pos))
1684 (magit-section-reveal (magit-current-section))))
1685
1686 ;;;; Setting Commands
1687
1688 (defun magit-diff-switch-range-type ()
1689 "Convert diff range type.
1690 Change \"revA..revB\" to \"revA...revB\", or vice versa."
1691 (interactive)
1692 (cond ((and magit-buffer-diff-range
1693 (derived-mode-p 'magit-diff-mode)
1694 (string-match magit-range-re magit-buffer-diff-range))
1695 (setq magit-buffer-diff-range
1696 (replace-match
1697 (if (string= (match-str 2 magit-buffer-diff-range) "..")
1698 "..."
1699 "..")
1700 t t magit-buffer-diff-range 2))
1701 (magit-refresh))
1702 ((user-error "No range to change"))))
1703
1704 (defun magit-diff-flip-revs ()
1705 "Swap revisions in diff range.
1706 Change \"revA..revB\" to \"revB..revA\"."
1707 (interactive)
1708 (cond ((and magit-buffer-diff-range
1709 (derived-mode-p 'magit-diff-mode)
1710 (string-match magit-range-re magit-buffer-diff-range))
1711 (setq magit-buffer-diff-range
1712 (concat (match-str 3 magit-buffer-diff-range)
1713 (match-str 2 magit-buffer-diff-range)
1714 (match-str 1 magit-buffer-diff-range)))
1715 (magit-refresh))
1716 ((user-error "No range to swap"))))
1717
1718 (defun magit-diff-toggle-file-filter ()
1719 "Toggle the file restriction of the current buffer's diffs.
1720 If the current buffer's mode is derived from `magit-log-mode',
1721 toggle the file restriction in the repository's revision buffer
1722 instead."
1723 (interactive)
1724 (cl-flet ((toggle ()
1725 (if (or magit-buffer-diff-files
1726 magit-buffer-diff-files-suspended)
1727 (cl-rotatef magit-buffer-diff-files
1728 magit-buffer-diff-files-suspended)
1729 (setq magit-buffer-diff-files
1730 (transient-infix-read 'magit:--)))
1731 (magit-refresh)))
1732 (cond
1733 ((derived-mode-p 'magit-log-mode
1734 'magit-cherry-mode
1735 'magit-reflog-mode)
1736 (if-let ((buffer (magit-get-mode-buffer 'magit-revision-mode)))
1737 (with-current-buffer buffer (toggle))
1738 (message "No revision buffer")))
1739 ((local-variable-p 'magit-buffer-diff-files)
1740 (toggle))
1741 ((user-error "Cannot toggle file filter in this buffer")))))
1742
1743 (defun magit-diff-less-context (&optional count)
1744 "Decrease the context for diff hunks by COUNT lines."
1745 (interactive "p")
1746 (magit-diff-set-context (##max 0 (- (or % 0) count))))
1747
1748 (defun magit-diff-more-context (&optional count)
1749 "Increase the context for diff hunks by COUNT lines."
1750 (interactive "p")
1751 (magit-diff-set-context (##+ (or % 0) count)))
1752
1753 (defun magit-diff-default-context ()
1754 "Reset context for diff hunks to the default height."
1755 (interactive)
1756 (magit-diff-set-context #'ignore))
1757
1758 (defun magit-diff-set-context (fn)
1759 (when (derived-mode-p 'magit-merge-preview-mode)
1760 (user-error "Cannot use %s in %s" this-command major-mode))
1761 (let* ((def (if-let ((context (magit-get "diff.context")))
1762 (string-to-number context)
1763 3))
1764 (val magit-buffer-diff-args)
1765 (arg (seq-find (##string-match "^-U\\([0-9]+\\)?$" %) val))
1766 (num (if-let ((str (and arg (match-str 1 arg))))
1767 (string-to-number str)
1768 def))
1769 (val (delete arg val))
1770 (num (funcall fn num))
1771 (arg (and num (not (= num def)) (format "-U%d" num)))
1772 (val (if arg (cons arg val) val)))
1773 (setq magit-buffer-diff-args val))
1774 (magit-refresh))
1775
1776 (defun magit-diff-get-context ()
1777 (string-to-number
1778 (or (seq-some (##and (string-match "\\`-U\\([0-9]+\\)?\\'" %)
1779 (match-str 1 %))
1780 magit-buffer-diff-args)
1781 (magit-get "diff.context")
1782 "3")))
1783
1784 (defun magit-diff-context-p ()
1785 (if-let ((arg (seq-find (##string-match "^-U\\([0-9]+\\)$" %)
1786 magit-buffer-diff-args)))
1787 (not (equal arg "-U0"))
1788 t))
1789
1790 (defun magit-diff-ignore-any-space-p ()
1791 (seq-some (##member % magit-buffer-diff-args)
1792 '("--ignore-cr-at-eol"
1793 "--ignore-space-at-eol"
1794 "--ignore-space-change" "-b"
1795 "--ignore-all-space" "-w"
1796 "--ignore-blank-space")))
1797
1798 (defun magit-diff-toggle-refine-hunk (&optional style)
1799 "Turn hunk refinement on or off, or switch refinement method.
1800
1801 If hunk refinement is currently on, then turn off hunk refinement.
1802 If hunk refinement is off, then turn on immediate hunk refinement.
1803
1804 With a prefix argument, an alternative refinement method comes into
1805 play. When using that method, mode hunks are not refined immediately,
1806 instead each hunk is refined once it is selected, and then stays refined
1807 until the next refresh of the buffer. If hunk refinement is currently
1808 on, then toggle between refining all hunks up front or only once they
1809 are selected. If hunk refinement is off, then turn on hunk refinement,
1810 using the eventual refinement method.
1811
1812 Customize option `magit-diff-refine-hunk' to change the default method."
1813 (interactive "P")
1814 (setq-local magit-diff-refine-hunk
1815 (if style
1816 (if (eq magit-diff-refine-hunk t) 'all t)
1817 (if magit-diff-refine-hunk nil 'all)))
1818 (magit-diff-update-hunk-refinement))
1819
1820 (defun magit-diff-toggle-fontify-hunk (&optional style)
1821 "Turn hunk fontification on or off, or switch fontification method.
1822
1823 If hunk fontification is currently on, then turn off hunk fontification.
1824 If hunk fontification is off, then turn on immediate hunk fontification.
1825
1826 With a prefix argument, an alternative fontification method comes into
1827 play. When using that method, mode hunks are not refined immediately,
1828 instead each hunk is refined once it is selected, and then stays refined
1829 until the next refresh of the buffer. If hunk fontification is currently
1830 on, then toggle between refining all hunks up front or only once they
1831 are selected. If hunk fontification is off, then turn on fontification,
1832 using the eventual fontification method.
1833
1834 Customize option `magit-diff-fontify-hunk' to change the default method."
1835 (interactive "P")
1836 (setq-local magit-diff-fontify-hunk
1837 (if style
1838 (if (eq magit-diff-fontify-hunk t) 'all t)
1839 (if magit-diff-fontify-hunk nil 'all)))
1840 (magit-diff--update-hunk-syntax))
1841
1842 ;;;; Visit Commands
1843 ;;;;; Dwim Variants
1844
1845 (defun magit-diff-visit-file (&optional other-window)
1846 "From a diff, visit the appropriate version of the file at point.
1847
1848 Display the buffer in the selected window. With a prefix argument,
1849 OTHER-WINDOW, instead display the buffer in another window.
1850
1851 In the visited file or blob, go to the location corresponding to the
1852 location in the diff.
1853
1854 If point is on an added or context line, visit the blob corresponding
1855 to our side (i.e., the new/right side). If point is on a removed line,
1856 visit the blob corresponding to their side (i.e., the old/left side).
1857
1858 This applies to diffs of staged and unstaged changes as well. For
1859 staged changes the two sides are blobs from the index and the `HEAD'
1860 commit. For unstaged changes the two sides are the actual file in
1861 the worktree and the blob from the index.
1862
1863 To visit the file in the worktree, regardless of what the current diff
1864 is about, use \
1865 \\<magit-diff-section-map>\
1866 \\[magit-diff-visit-worktree-file] \
1867 (`magit-diff-visit-worktree-file').
1868
1869 In the past \\`<return>' (this command) used to go to the file in the
1870 worktree, if point is on an added or context line of a diff showing
1871 staged changes. Set `magit-diff-visit-prefer-worktree' to t to restore
1872 that behavior, but note that doing so makes the behavior inconsistent
1873 and you would give up on the ability to visit the index blob. If you
1874 already use \\[magit-diff-visit-worktree-file] to jump to the live \
1875 file from committed changes,
1876 it might be better to retrain muscle memory to do the same from staged
1877 changes.
1878
1879 This command only works when point is inside a diff; elsewhere use
1880 `magit-find-file'."
1881 (interactive "P")
1882 (magit-diff-visit-file--internal
1883 (and magit-diff-visit-prefer-worktree
1884 (memq (magit-diff--dwim) '(staged unstaged)))
1885 (and other-window t)))
1886
1887 (defun magit-diff-visit-file-other-window ()
1888 "From a diff visit a version of the file at point in another window.
1889 Like `magit-diff-visit-file' but always display in another window."
1890 (interactive)
1891 (magit-diff-visit-file--internal
1892 (and magit-diff-visit-prefer-worktree
1893 (memq (magit-diff--dwim) '(staged unstaged)))
1894 #'switch-to-buffer-other-window))
1895
1896 (defun magit-diff-visit-file-other-frame ()
1897 "From a diff visit a version of the file at point in another frame.
1898 Like `magit-diff-visit-file' but always display in another frame."
1899 (interactive)
1900 (magit-diff-visit-file--internal
1901 (and magit-diff-visit-prefer-worktree
1902 (memq (magit-diff--dwim) '(staged unstaged)))
1903 #'switch-to-buffer-other-frame))
1904
1905 ;;;;; Worktree Variants
1906
1907 (defun magit-diff-visit-worktree-file (&optional other-window)
1908 "From a diff visit the worktree version of the file at point.
1909
1910 Display the buffer in the selected window. With a prefix argument,
1911 OTHER-WINDOW, display the buffer in another window instead.
1912
1913 Visit the worktree version of the appropriate file. The location
1914 of point inside the diff determines which file is being visited.
1915
1916 Unlike `magit-diff-visit-file' always visits the \"real\" file in
1917 the working tree, i.e., the \"current version\" of the file.
1918
1919 In the file-visiting buffer also go to the line that corresponds
1920 to the line that point is on in the diff. Lines that were added
1921 or removed in the working tree, the index and other commits in
1922 between are automatically accounted for."
1923 (interactive "P")
1924 (magit-diff-visit-file--internal t (and other-window t)))
1925
1926 (defun magit-diff-visit-worktree-file-other-window ()
1927 "From a diff visit the file at point in another window.
1928 Like `magit-diff-visit-worktree-file' but display in another window."
1929 (interactive)
1930 (magit-diff-visit-file--internal t #'switch-to-buffer-other-window))
1931
1932 (defun magit-diff-visit-worktree-file-other-frame ()
1933 "From a diff visit the file at point in another frame.
1934 Like `magit-diff-visit-worktree-file' but display in another frame."
1935 (interactive)
1936 (magit-diff-visit-file--internal t #'switch-to-buffer-other-frame))
1937
1938 ;;;;; Internal
1939
1940 (defun magit-diff-visit-file--internal (force-worktree display)
1941 "From a diff visit the appropriate version of FILE.
1942 If FORCE-WORKTREE is non-nil, then visit the worktree version of the
1943 file, even if the diff is about a committed change. DISPLAY controls
1944 how the buffer is displayed. If nil display in the same window, if
1945 t display in another window, or if a function, use that to display."
1946 (let ((file (or (magit-diff--file)
1947 (user-error "Cannot determine file to visit"))))
1948 (if (file-accessible-directory-p file)
1949 (magit-diff-visit-directory file display)
1950 (pcase-let ((`(,buf ,pos)
1951 (magit-diff-visit-file--noselect force-worktree)))
1952 (pcase display
1953 ('nil (pop-to-buffer-same-window buf))
1954 ('t (switch-to-buffer-other-window buf))
1955 (_ (funcall display buf)))
1956 (magit-diff-visit-file--setup buf pos)
1957 buf))))
1958
1959 (defun magit-diff-visit-directory (directory &optional other-window)
1960 "Visit DIRECTORY in some window.
1961 Display the buffer in the selected window unless OTHER-WINDOW is
1962 non-nil. If DIRECTORY is the top-level directory of the current
1963 repository, then visit the containing directory using Dired and
1964 in the Dired buffer put point on DIRECTORY. Otherwise display
1965 the Magit-Status buffer for DIRECTORY."
1966 (if (equal (magit-toplevel directory)
1967 (magit-toplevel))
1968 (dired-jump other-window (concat directory "/."))
1969 (let ((display-buffer-overriding-action
1970 (if other-window
1971 '(nil (inhibit-same-window . t))
1972 '(display-buffer-same-window))))
1973 (magit-status-setup-buffer directory))))
1974
1975 (defun magit-diff-visit-file--setup (buf pos)
1976 (with-selected-window (or (get-buffer-window buf) (selected-window))
1977 (with-current-buffer buf
1978 (when pos
1979 (unless (<= (point-min) pos (point-max))
1980 (widen))
1981 (goto-char pos))
1982 (when (and buffer-file-name
1983 (magit-anything-unmerged-p buffer-file-name))
1984 (smerge-start-session))
1985 (run-hooks 'magit-diff-visit-file-hook))))
1986
1987 (defun magit-diff-visit-file--noselect (&optional goto-file)
1988 (pcase-let*
1989 ((`(,old ,new) (magit-diff-visit--sides))
1990 (goto-from (and (not goto-file) (magit-diff-on-removed-line-p)))
1991 (goto-file (or goto-file
1992 (equal magit-buffer-diff-typearg "--no-index")))
1993 (`(,rev ,file) (if goto-from old new))
1994 (buffer (magit-find-file-noselect (if goto-file "{worktree}" rev)
1995 file)))
1996 (list buffer
1997 (magit-diff-visit--position buffer rev file goto-from goto-file))))
1998
1999 (defun magit-diff-visit--sides ()
2000 (pcase-let* (((eieio source value)
2001 (magit-diff--file-section))
2002 (old-file (or source value))
2003 (new-file value)
2004 (spec (magit-diff--dwim))
2005 (`(,old-rev . ,new-rev)
2006 (pcase spec
2007 ((pred stringp)
2008 (magit-split-range spec t))
2009 (`(,(or 'commit 'stash) . ,rev)
2010 (cons (magit-rev-abbrev (concat rev "^"))
2011 (magit--abbrev-if-oid rev)))
2012 ('staged (cons (magit-rev-abbrev "HEAD") "{index}"))
2013 ('unstaged (cons (if (magit-anything-staged-p nil old-file)
2014 "{index}"
2015 (magit-rev-abbrev "HEAD"))
2016 "{worktree}"))
2017 ('nil (cons "{worktree}" "{worktree}"))
2018 ('unmerged (cons "{worktree}" "{worktree}"))
2019 ('undefined (cons "{worktree}" "{worktree}")) ;--no-index
2020 (_ (error "BUG: Unexpected diff type %s" spec)))))
2021 (when (equal magit-buffer-diff-typearg "--no-index")
2022 (setq old-file (concat "/" old-file))
2023 (setq new-file (concat "/" new-file)))
2024 (list (list old-rev old-file)
2025 (list new-rev new-file))))
2026
2027 (defun magit-diff-visit--position (buffer rev file goto-from goto-file)
2028 (and-let* ((hunk (magit-diff--hunk-section))
2029 (line (magit-diff-hunk-line hunk goto-from))
2030 (column (magit-diff-hunk-column hunk goto-from)))
2031 (with-current-buffer buffer
2032 (save-restriction
2033 (widen)
2034 (goto-char (point-min))
2035 (forward-line
2036 (1- (pcase rev
2037 ((guard (not goto-file)) line)
2038 ("{worktree}" line)
2039 ("{index}" (magit-diff-visit--offset line file))
2040 (_ (magit-diff-visit--offset line file rev)))))
2041 (move-to-column column)
2042 (point)))))
2043
2044 (defun magit-diff-hunk-line (section goto-from)
2045 (save-excursion
2046 (goto-char (line-beginning-position))
2047 (with-slots (content combined from-ranges from-range to-range) section
2048 (when (or from-range to-range)
2049 (when (< (point) content)
2050 (goto-char content)
2051 (re-search-forward "^[-+]"))
2052 (+ (car (if goto-from from-range to-range))
2053 (let ((prefix (if combined (length from-ranges) 1))
2054 (target (point))
2055 (offset 0))
2056 (goto-char content)
2057 (while (< (point) target)
2058 (unless (string-search
2059 (if goto-from "+" "-")
2060 (buffer-substring (point) (+ (point) prefix)))
2061 (incf offset))
2062 (forward-line))
2063 offset))))))
2064
2065 (defun magit-diff-hunk-column (section goto-from)
2066 (if (or (< (magit-point)
2067 (oref section content))
2068 (and (not goto-from)
2069 (= (char-after (line-beginning-position)) ?-)))
2070 0
2071 (max 0 (- (+ (current-column) 2)
2072 (length (oref section value))))))
2073
2074 (defun magit-diff-visit--offset (line file &rest args)
2075 (let ((offset 0))
2076 (with-temp-buffer
2077 (save-excursion
2078 (magit-with-toplevel
2079 (cond ((stringp file)
2080 (magit-git-insert "diff" args "--" file))
2081 ((magit-git-version< "2.50"))
2082 ((apply #'magit--diff-pair (current-buffer) file)))))
2083 (catch 'found
2084 (while (re-search-forward
2085 "^@@ -\\([0-9]+\\),\\([0-9]+\\) \\+\\([0-9]+\\),\\([0-9]+\\) @@.*\n"
2086 nil t)
2087 (let ((from-beg (string-to-number (match-str 1)))
2088 (from-len (string-to-number (match-str 2)))
2089 ( to-len (string-to-number (match-str 4))))
2090 (if (<= from-beg line)
2091 (if (< (+ from-beg from-len) line)
2092 (incf offset (- to-len from-len))
2093 (let ((rest (- line from-beg)))
2094 (while (> rest 0)
2095 (pcase (char-after)
2096 (?\s (decf rest))
2097 (?- (decf offset) (decf rest))
2098 (?+ (incf offset)))
2099 (forward-line))))
2100 (throw 'found nil))))))
2101 (+ line offset)))
2102
2103 (defun magit--diff-pair (buffer a b &optional file)
2104 (with-temp-buffer
2105 (insert (format ":100644 100644 %s %s M\0%s\0" a b (or file "blob")))
2106 (call-process-region (point-min) (point-max)
2107 (magit-git-executable) nil buffer nil
2108 "diff-pairs" "-z")))
2109
2110 ;;;;; Modified
2111
2112 (defun magit-diff--modified-defuns ()
2113 (let (value eof)
2114 (save-excursion
2115 (goto-char (point-min))
2116 (while (re-search-forward "^diff --git" nil t)
2117 (save-excursion
2118 (setq eof (and (re-search-forward "^diff --git" nil t)
2119 (match-beginning 0))))
2120 (let (-rev +rev)
2121 (while (progn (forward-line) (looking-at "^[a-z]"))
2122 (when (looking-at "^index \\([^.]+\\)\\.\\.\\([^ ]+\\)")
2123 (setq -rev (magit-rev-parse (match-string 1)))
2124 (setq +rev (magit-rev-parse (match-string 2)))))
2125 (pcase-let*
2126 ((-file (and (looking-at "^--- a/\\(.+\\)")
2127 (prog1 (match-str 1) (forward-line))))
2128 (+file (and (looking-at "^\\+\\+\\+ b/\\(.+\\)")
2129 (prog1 (match-str 1) (forward-line))))
2130 (`(,-lines ,+lines) (magit-diff--modified-lines eof))
2131 (-defuns (and -lines (magit-diff--modified-defuns-1
2132 -rev -file -lines)))
2133 (+defuns (and +lines (magit-diff--modified-defuns-1
2134 +rev +file +lines))))
2135 (if (equal -file +file)
2136 (when$ (nconc +defuns -defuns)
2137 (push (cons +file (delete-dups $)) value))
2138 (when -defuns (push (cons -file -defuns) value))
2139 (when +defuns (push (cons +file +defuns) value)))))))
2140 (nreverse value)))
2141
2142 (defun magit-diff--modified-defuns-1 (rev file lines)
2143 (with-current-buffer (magit-find-file-noselect rev file t t)
2144 (save-excursion
2145 (save-restriction
2146 (widen)
2147 (let (defuns)
2148 (while-let ((line (pop lines)))
2149 (goto-char (point-min))
2150 (forward-line line)
2151 (when-let ((def (which-function)))
2152 (cl-pushnew def defuns :test #'equal)
2153 (when-let ((end (condition-case nil (end-of-defun)
2154 (:success (line-number-at-pos))
2155 (error nil))))
2156 (while (and lines (> end (car lines)))
2157 (pop lines)))))
2158 (nreverse defuns))))))
2159
2160 (defun magit-diff--modified-lines (bound)
2161 (let (-lines +lines)
2162 (while (re-search-forward
2163 "^@@ -\\([0-9]+\\),[^ ]+ \\+\\([0-9]+\\),[^ ]+ @@.*\n" bound t)
2164 (let ((-line (string-to-number (match-string 1)))
2165 (+line (string-to-number (match-string 2))))
2166 (while (and (not (eobp))
2167 (memq (char-after) '(?\s ?- ?+)))
2168 (pcase (char-after)
2169 (?\s (incf -line)
2170 (incf +line))
2171 (?- (push (incf -line) -lines))
2172 (?+ (push (incf +line) +lines)))
2173 (forward-line))))
2174 (list (nreverse -lines)
2175 (nreverse +lines))))
2176
2177 ;;;; Scroll Commands
2178
2179 (defun magit-diff-show-or-scroll-up ()
2180 "Update the commit or diff buffer for the thing at point.
2181
2182 Either show the commit or stash at point in the appropriate
2183 buffer, or if that buffer is already being displayed in the
2184 current frame and contains information about that commit or
2185 stash, then instead scroll the buffer up. If there is no
2186 commit or stash at point, then prompt for a commit."
2187 (interactive)
2188 (magit-diff-show-or-scroll #'scroll-up))
2189
2190 (defun magit-diff-show-or-scroll-down ()
2191 "Update the commit or diff buffer for the thing at point.
2192
2193 Either show the commit or stash at point in the appropriate
2194 buffer, or if that buffer is already being displayed in the
2195 current frame and contains information about that commit or
2196 stash, then instead scroll the buffer down. If there is no
2197 commit or stash at point, then prompt for a commit."
2198 (interactive)
2199 (magit-diff-show-or-scroll #'scroll-down))
2200
2201 (defun magit-diff-show-or-scroll (fn)
2202 (let (rev cmd buf)
2203 (cond
2204 ((and (bound-and-true-p magit-blame-mode)
2205 (fboundp 'magit-current-blame-chunk))
2206 (setq rev (oref (magit-current-blame-chunk) orig-rev))
2207 (setq cmd #'magit-show-commit)
2208 (setq buf (magit-get-mode-buffer 'magit-revision-mode)))
2209 ((derived-mode-p 'git-rebase-mode)
2210 (with-slots (action-type target)
2211 (git-rebase-current-line)
2212 (if (not (eq action-type 'commit))
2213 (user-error "No commit on this line")
2214 (setq rev target)
2215 (setq cmd #'magit-show-commit)
2216 (setq buf (magit-get-mode-buffer 'magit-revision-mode)))))
2217 ((magit-section-case
2218 (branch
2219 (setq rev (magit-ref-maybe-qualify (oref it value)))
2220 (setq cmd #'magit-show-commit)
2221 (setq buf (magit-get-mode-buffer 'magit-revision-mode)))
2222 (commit
2223 (setq rev (oref it value))
2224 (setq cmd #'magit-show-commit)
2225 (setq buf (magit-get-mode-buffer 'magit-revision-mode)))
2226 (tag
2227 (setq rev (magit-commit-oid (oref it value)))
2228 (setq cmd #'magit-show-commit)
2229 (setq buf (magit-get-mode-buffer 'magit-revision-mode)))
2230 (stash
2231 (setq rev (oref it value))
2232 (setq cmd #'magit-stash-show)
2233 (setq buf (magit-get-mode-buffer 'magit-stash-mode))))))
2234 (cond-let*
2235 ((not rev)
2236 (call-interactively #'magit-show-commit))
2237 ([_ buf]
2238 [win (get-buffer-window buf)]
2239 [_ (equal (buffer-local-value 'magit-buffer-revision buf) rev)]
2240 [_ (equal (buffer-local-value 'magit-buffer-revision-oid buf)
2241 (magit-rev-parse rev))]
2242 (with-selected-window win
2243 (condition-case nil
2244 (funcall fn)
2245 (error
2246 (pcase-exhaustive fn
2247 ('scroll-up (goto-char (point-min)))
2248 ('scroll-down (goto-char (point-max))))))))
2249 [[magit-display-buffer-noselect t]]
2250 ((eq cmd #'magit-show-commit)
2251 (apply #'magit-show-commit rev (magit-show-commit--arguments)))
2252 ((funcall cmd rev)))))
2253
2254 ;;;; Section Commands
2255
2256 (defun magit-section-cycle-diffs ()
2257 "Cycle visibility of diff-related sections in the current buffer."
2258 (interactive)
2259 (cond-let*
2260 [[sections
2261 (cond ((derived-mode-p 'magit-status-mode)
2262 (mapcan (lambda (section)
2263 (and section
2264 (progn
2265 (when (oref section hidden)
2266 (magit-section-show section))
2267 (copy-sequence (oref section children)))))
2268 (list (magit-get-section '((staged) (status)))
2269 (magit-get-section '((unstaged) (status))))))
2270 ((derived-mode-p 'magit-diff-mode)
2271 (seq-filter #'magit-file-section-p
2272 (oref magit-root-section children))))]]
2273 ((not sections))
2274 ((seq-some (##oref % hidden) sections)
2275 (dolist (s sections)
2276 (magit-section-show s)
2277 (magit-section-hide-children s)))
2278 [[children (mapcan (##copy-sequence (oref % children)) sections)]]
2279 ((and (seq-some (##oref % hidden) children)
2280 (seq-some (##oref % children) children))
2281 (mapc #'magit-section-show-headings sections))
2282 ((seq-some #'magit-section-hidden-body children)
2283 (mapc #'magit-section-show-children sections))
2284 ((mapc #'magit-section-hide sections))))
2285
2286 ;;;; Jump Commands
2287
2288 (transient-define-prefix magit-revision-jump (&optional menu)
2289 "In a Magit-Revision buffer, jump to a section.
2290 Show a menu to choose a section, unless point is on a file
2291 heading, or with a prefix argument, in which case behave
2292 like 'magit-jump-to-diffstat-or-diff'."
2293 [["Jump to"
2294 ("h" magit-jump-to-revision-headers)
2295 ("m" magit-jump-to-revision-message)
2296 ("n" magit-jump-to-revision-notes)
2297 ("s" magit-jump-to-revision-diffstat)
2298 ("d" magit-jump-to-revision-diff)]
2299 ["Jump using"
2300 ("j" "Imenu" imenu)]]
2301 (interactive (list (or (not (magit-section-match 'file))
2302 current-prefix-arg)))
2303 (if menu
2304 (transient-setup 'magit-revision-jump)
2305 (magit-jump-to-diffstat-or-diff)))
2306
2307 (magit-define-section-jumper magit-jump-to-revision-headers
2308 "Headings" headers nil magit-insert-revision-headers)
2309
2310 (magit-define-section-jumper magit-jump-to-revision-message
2311 "Message" commit-message nil magit-insert-revision-message)
2312
2313 (magit-define-section-jumper magit-jump-to-revision-notes
2314 "Notes" notes nil magit-insert-revision-notes)
2315
2316 (transient-define-suffix magit-jump-to-revision-diffstat (&optional expand)
2317 :description "Diffstat"
2318 :inapt-if-not (##cl-find-if (##eq (oref % type) 'diffstat)
2319 (oref magit-root-section children))
2320 (interactive "P")
2321 (cond-let
2322 ([section (magit-get-section
2323 (append (magit-section-case
2324 (file `((file . ,(oref it value)) (diffstat)))
2325 (hunk `((file . ,(magit-section-parent-value it))
2326 (diffstat)))
2327 (t '((diffstat))))
2328 (magit-section-ident magit-root-section)))]
2329 (goto-char (oref section start))
2330 (when expand
2331 (with-local-quit (magit-section-show section))
2332 (recenter 0)))
2333 ((message (format "No diff sections found")))))
2334
2335 (transient-define-suffix magit-jump-to-revision-diff (&optional expand)
2336 :description "Diff"
2337 :inapt-if-not (##cl-find-if (##eq (oref % type) 'file)
2338 (oref magit-root-section children))
2339 (interactive "P")
2340 (cond-let
2341 ([section (cl-find-if (##eq (oref % type) 'file)
2342 (oref magit-root-section children))]
2343 (goto-char (oref section start))
2344 (when expand
2345 (with-local-quit (magit-section-show section))
2346 (recenter 0)))
2347 ((message (format "No diff sections found")))))
2348
2349 (defun magit-jump-to-diffstat-or-diff (&optional expand)
2350 "Jump to the diffstat or diff.
2351 When point is on a file inside the diffstat section, then jump
2352 to the respective diff section, otherwise jump to the diffstat
2353 section or a child thereof."
2354 (interactive)
2355 (cond-let
2356 ([section (magit-get-section
2357 (append (magit-section-case
2358 ([file diffstat] `((file . ,(oref it value))))
2359 (file `((file . ,(oref it value)) (diffstat)))
2360 (hunk `((file . ,(magit-section-parent-value it))
2361 (diffstat)))
2362 (t '((diffstat))))
2363 (magit-section-ident magit-root-section)))]
2364 (goto-char (oref section start))
2365 (when expand
2366 (with-local-quit (magit-section-show section))
2367 (recenter 0)))
2368 ((user-error "No diffstat in this buffer"))))
2369
2370 ;;; Diff Mode
2371
2372 (defvar-keymap magit-diff-mode-map
2373 :doc "Keymap for `magit-diff-mode'."
2374 :parent magit-mode-map
2375 "C-c C-d" #'magit-diff-while-committing
2376 "C-c C-b" #'magit-go-backward
2377 "C-c C-f" #'magit-go-forward
2378 "SPC" #'scroll-up
2379 "S-SPC" #'scroll-down
2380 "DEL" #'scroll-down
2381 "j" #'magit-jump-to-diffstat-or-diff
2382 "<remap> <write-file>" #'magit-patch-save)
2383
2384 (define-derived-mode magit-diff-mode magit-mode "Magit Diff"
2385 "Mode for looking at a Git diff.
2386
2387 This mode is documented in info node `(magit)Diff Buffer'.
2388
2389 \\<magit-mode-map>\
2390 Type \\[magit-refresh] to refresh the current buffer.
2391 Type \\[magit-section-toggle] to expand or hide the section at point.
2392 Type \\[magit-visit-thing] to visit the hunk or file at point.
2393
2394 Staging and applying changes is documented in info node
2395 `(magit)Staging and Unstaging' and info node `(magit)Applying'.
2396
2397 \\<magit-hunk-section-map>Type \
2398 \\[magit-apply] to apply the change at point, \
2399 \\[magit-stage] to stage,
2400 \\[magit-unstage] to unstage, \
2401 \\[magit-discard] to discard, or \
2402 \\[magit-reverse] to reverse it.
2403
2404 \\{magit-diff-mode-map}"
2405 :interactive nil
2406 :group 'magit-diff
2407 (magit-hack-dir-local-variables)
2408 (setq magit--imenu-item-types 'file))
2409
2410 (put 'magit-diff-mode 'magit-diff-default-arguments
2411 '("--stat" "--no-ext-diff"))
2412
2413 (defun magit-diff-setup-buffer ( range typearg args files
2414 &optional type locked)
2415 (require 'magit)
2416 (magit-setup-buffer #'magit-diff-mode locked
2417 (magit-buffer-diff-range range)
2418 (magit-buffer-diff-typearg typearg)
2419 (magit-buffer-diff-type type)
2420 (magit-buffer-diff-args args)
2421 (magit-buffer-diff-files files)
2422 (magit-buffer-diff-files-suspended nil)))
2423
2424 (defun magit-diff-refresh-buffer ()
2425 "Refresh the current `magit-diff-mode' buffer."
2426 (magit-set-header-line-format
2427 (if (equal magit-buffer-diff-typearg "--no-index")
2428 (apply #'format "Differences between %s and %s" magit-buffer-diff-files)
2429 (concat (cond (magit-buffer-diff-range
2430 (cond-let
2431 ((string-match-p "\\(\\.\\.\\|\\^-\\)"
2432 magit-buffer-diff-range)
2433 (format "Changes in %s" magit-buffer-diff-range))
2434 [[msg "Changes from %s to %s"]
2435 [end (if (equal magit-buffer-diff-typearg "--cached")
2436 "index"
2437 "working tree")]]
2438 ((member "-R" magit-buffer-diff-args)
2439 (format msg end magit-buffer-diff-range))
2440 ((format msg magit-buffer-diff-range end))))
2441 ((equal magit-buffer-diff-typearg "--cached")
2442 "Staged changes")
2443 ((and (magit-repository-local-get 'this-commit-command)
2444 (not (magit-anything-staged-p)))
2445 "Uncommitting changes")
2446 ("Unstaged changes"))
2447 (pcase (length magit-buffer-diff-files)
2448 (0)
2449 (1 (concat " in file " (car magit-buffer-diff-files)))
2450 (_ (concat " in files "
2451 (string-join magit-buffer-diff-files ", ")))))))
2452 (setq magit-buffer-diff-range-oids
2453 (and magit-buffer-diff-range
2454 (magit-hash-range magit-buffer-diff-range)))
2455 (magit-insert-section (diffbuf)
2456 (magit-run-section-hook 'magit-diff-sections-hook)))
2457
2458 (cl-defmethod magit-buffer-value (&context (major-mode magit-diff-mode))
2459 (nconc (cond
2460 (magit-buffer-diff-range
2461 (delq nil (list magit-buffer-diff-range magit-buffer-diff-typearg)))
2462 ((equal magit-buffer-diff-typearg "--cached")
2463 (list 'staged))
2464 ((list 'unstaged magit-buffer-diff-typearg)))
2465 (and magit-buffer-diff-files (cons "--" magit-buffer-diff-files))))
2466
2467 (cl-defmethod magit-menu-common-value ((_section magit-diff-section))
2468 (magit-diff-scope))
2469
2470 (defvar-keymap magit-diff-section-map
2471 :doc "Keymap for diff sections.
2472 The classes `magit-file-section' and `magit-hunk-section' derive
2473 from the abstract `magit-diff-section' class. Accordingly this
2474 keymap is the parent of their keymaps."
2475 "C-j" #'magit-diff-visit-worktree-file
2476 "C-<return>" #'magit-diff-visit-worktree-file
2477 "C-x 4 <return>" #'magit-diff-visit-file-other-window
2478 "C-x 5 <return>" #'magit-diff-visit-file-other-frame
2479 "&" #'magit-do-async-shell-command
2480 "C" #'magit-commit-add-log
2481 "C-x a" #'magit-add-change-log-entry
2482 "C-x 4 a" #'magit-add-change-log-entry-other-window
2483 "C-c C-t" #'magit-diff-trace-definition
2484 "C-c C-e" #'magit-diff-edit-hunk-commit
2485 "<remap> <magit-file-rename>" #'magit-file-rename
2486 "<remap> <magit-file-untrack>" #'magit-file-untrack
2487 "<remap> <magit-visit-thing>" #'magit-diff-visit-file
2488 "<remap> <magit-revert-no-commit>" #'magit-reverse
2489 "<remap> <magit-delete-thing>" #'magit-discard
2490 "<remap> <magit-unstage-files>" #'magit-unstage
2491 "<remap> <magit-stage-files>" #'magit-stage
2492 "<remap> <magit-cherry-apply>" #'magit-apply
2493 "<8>" (magit-menu-item "Rename file" #'magit-file-rename
2494 '(:enable (eq (magit-diff-scope) 'file)))
2495 "<7>" (magit-menu-item "Untrack %x" #'magit-file-untrack)
2496 "<6>" (magit-menu-item "Visit file" #'magit-diff-visit-file
2497 '(:enable (memq (magit-diff-scope) '(file files))))
2498 "<5>" (magit-menu-item "Reverse %x" #'magit-reverse
2499 '(:enable (not (memq (magit-diff-type)
2500 '(untracked unstaged)))))
2501 "<4>" (magit-menu-item "Discard %x" #'magit-discard
2502 '(:enable (not (memq (magit-diff-type)
2503 '(committed undefined)))))
2504 "<3>" (magit-menu-item "Unstage %x" #'magit-unstage
2505 '(:enable (eq (magit-diff-type) 'staged)))
2506 "<2>" (magit-menu-item "Stage %x" #'magit-stage
2507 '(:enable (eq (magit-diff-type) 'unstaged)))
2508 "<1>" (magit-menu-item "Apply %x" #'magit-apply
2509 '(:enable (not (memq (magit-diff-type)
2510 '(unstaged staged))))))
2511
2512 (defvar-keymap magit-file-section-map
2513 ;; Even though this derived map doesn't add any bindings by default,
2514 ;; it is quite possible that some users would want to add their own.
2515 :doc "Keymap for `file' sections."
2516 :parent magit-diff-section-base-map)
2517
2518 (defvar-keymap magit-hunk-section-smerge-map
2519 :doc "Keymap bound to `smerge-command-prefix' in `magit-hunk-section-map'."
2520 "RET" #'magit-smerge-keep-current
2521 "a" #'magit-smerge-keep-all
2522 "u" #'magit-smerge-keep-upper
2523 "b" #'magit-smerge-keep-base
2524 "l" #'magit-smerge-keep-lower)
2525
2526 (defvar magit-hunk-section-map
2527 (let ((map (make-sparse-keymap))
2528 (key (key-description smerge-command-prefix)))
2529 (when (key-valid-p key)
2530 (keymap-set map key magit-hunk-section-smerge-map))
2531 (set-keymap-parent map magit-diff-section-base-map)
2532 map)
2533 "Keymap for `hunk' sections.")
2534
2535 ;;; Diff Insert
2536
2537 (defvar magit-diff--reset-non-color-moved
2538 (list
2539 "-c" "color.diff.context=normal"
2540 "-c" "color.diff.plain=normal" ; historical synonym for context
2541 "-c" "color.diff.meta=normal"
2542 "-c" "color.diff.frag=normal"
2543 "-c" "color.diff.func=normal"
2544 "-c" "color.diff.old=normal"
2545 "-c" "color.diff.new=normal"
2546 "-c" "color.diff.commit=normal"
2547 "-c" "color.diff.whitespace=normal"
2548 ;; "git-range-diff" does not support "--color-moved", so we don't
2549 ;; need to reset contextDimmed, oldDimmed, newDimmed, contextBold,
2550 ;; oldBold, and newBold.
2551 ))
2552
2553 (defun magit-insert-diff ()
2554 "Insert the diff into this `magit-diff-mode' buffer."
2555 (magit--insert-diff t
2556 "diff" magit-buffer-diff-range "-p" "--no-prefix"
2557 (and (member "--stat" magit-buffer-diff-args) "--numstat")
2558 magit-buffer-diff-typearg
2559 magit-buffer-diff-args "--"
2560 magit-buffer-diff-files))
2561
2562 (defun magit--insert-diff (keep-error &rest args)
2563 (declare (indent 1))
2564 (pcase-let ((`(,cmd . ,args)
2565 (flatten-tree args))
2566 (magit-git-global-arguments
2567 (remove "--literal-pathspecs" magit-git-global-arguments)))
2568 ;; We need to generate diffs with "--ita-visible-in-index" so that
2569 ;; `magit-stage' can work with intent-to-add files (see #4026).
2570 ;; When "-C" is used don't add "--ita-visible-in-index" because
2571 ;; that renders the former ineffective (see ##5629).
2572 (unless (or (equal cmd "merge-tree")
2573 (seq-some (##string-prefix-p "-C" %) args))
2574 (push "--ita-visible-in-index" args))
2575 (setq args (magit-diff--maybe-add-stat-arguments args))
2576 (when (any (##string-prefix-p "--color-moved" %) args)
2577 (push "--color=always" args)
2578 (setq magit-git-global-arguments
2579 (append magit-diff--reset-non-color-moved
2580 magit-git-global-arguments)))
2581 (magit--git-wash #'magit-diff-wash-diffs
2582 (if (member "--no-index" args)
2583 'wash-anyway
2584 (or keep-error t))
2585 cmd args)))
2586
2587 (defun magit-diff--maybe-add-stat-arguments (args)
2588 (if (member "--stat" args)
2589 (append (if (functionp magit-diff-extra-stat-arguments)
2590 (funcall magit-diff-extra-stat-arguments)
2591 magit-diff-extra-stat-arguments)
2592 args)
2593 args))
2594
2595 ;;; Diff Wash
2596
2597 (defconst magit-diff-conflict-headline-re
2598 (concat "^" (regexp-opt
2599 ;; Defined in merge-tree.c in this order.
2600 '("merged"
2601 "added in remote"
2602 "added in both"
2603 "added in local"
2604 "removed in both"
2605 "changed in both"
2606 "removed in local"
2607 "removed in remote"))))
2608
2609 (defconst magit-diff-headline-re
2610 (concat "^\\(@@@?\\|diff\\|Submodule\\|"
2611 "\\* Unmerged path\\|"
2612 (substring magit-diff-conflict-headline-re 1)
2613 "\\)"))
2614
2615 (defconst magit-diff-statline-re
2616 (concat "^ ?"
2617 "\\(.*\\)" ; file
2618 "\\( +| +\\)" ; separator
2619 "\\([0-9]+\\|Bin\\(?: +[0-9]+ -> [0-9]+ bytes\\)?$\\) ?"
2620 "\\(\\+*\\)" ; add
2621 "\\(-*\\)$")) ; del
2622
2623 (defun magit-diff-use-window-width-as-stat-width ()
2624 "Use the `window-width' as the value of \"--stat-width\"."
2625 (and$ (get-buffer-window (current-buffer) 'visible)
2626 (list (format "--stat-width=%d" (window-width $)))))
2627
2628 (defun magit-diff-wash-diffs (args &optional limit)
2629 (run-hooks 'magit-diff-wash-diffs-hook)
2630 (when (member "--show-signature" args)
2631 (magit-diff-wash-signature magit-buffer-revision-oid))
2632 (when (any (##string-prefix-p "--color-moved" %) args)
2633 (require 'ansi-color)
2634 (ansi-color-apply-on-region (point-min) (point-max)))
2635 (when (member "--stat" args)
2636 (magit-diff-wash-diffstat))
2637 (when (re-search-forward magit-diff-headline-re limit t)
2638 (goto-char (line-beginning-position))
2639 (magit-wash-sequence (##magit-diff-wash-diff args))
2640 (insert ?\n)))
2641
2642 (defun magit-diff-wash-signature (object)
2643 (cond
2644 ((looking-at "^No signature")
2645 (delete-line))
2646 ((looking-at "^gpg: ")
2647 (let (title end)
2648 (save-excursion
2649 (while (looking-at "^\\(gpg: \\|\
2650 Primary key fingerprint: \\|\
2651 Subkey fingerprint: \\)")
2652 (cond
2653 ((looking-at "^gpg: Good signature from")
2654 (setq title (magit--propertize-face
2655 (buffer-substring (point) (line-end-position))
2656 'magit-signature-good)))
2657 ((looking-at "^gpg: Can't check signature")
2658 (setq title (magit--propertize-face
2659 (buffer-substring (point) (line-end-position))
2660 '(italic bold)))))
2661 (forward-line))
2662 (setq end (point-marker)))
2663 (magit-insert-section (signature object title)
2664 (when title
2665 (magit-insert-heading title))
2666 (goto-char end)
2667 (set-marker end nil)
2668 (insert "\n"))))))
2669
2670 (defun magit-diff-wash-diffstat ()
2671 (let (heading (beg (point)))
2672 (when (re-search-forward "^ ?\\([0-9]+ +files? change[^\n]*\n\\)" nil t)
2673 (setq heading (match-str 1))
2674 (magit-delete-match)
2675 (goto-char beg)
2676 (magit-insert-section (diffstat)
2677 (magit-insert-heading
2678 (propertize heading 'font-lock-face 'magit-diff-file-heading))
2679 (let (files)
2680 (while (looking-at "^[-0-9]+\t[-0-9]+\t\\(.+\\)$")
2681 (push (magit-decode-git-path
2682 (cond-let
2683 [[file (match-str 1)]]
2684 ((string-match "{.* => \\(.*\\)}" file)
2685 (replace-match (match-str 1 file) nil t file))
2686 ((string-match " => " file)
2687 (substring file (match-end 0)))
2688 (file)))
2689 files)
2690 (magit-delete-line))
2691 (setq files (nreverse files))
2692 (while (looking-at magit-diff-statline-re)
2693 (magit-bind-match-strings (file sep cnt add del) nil
2694 (magit-delete-line)
2695 (when (string-match " +$" file)
2696 (setq sep (concat (match-str 0 file) sep))
2697 (setq file (substring file 0 (match-beginning 0))))
2698 (let ((le (length file)) ld)
2699 (setq file (magit-decode-git-path file))
2700 (setq ld (length file))
2701 (when (> le ld)
2702 (setq sep (concat (make-string (- le ld) ?\s) sep))))
2703 (magit-insert-section (file (pop files))
2704 (insert (magit-format-file 'stat file 'magit-filename))
2705 (insert sep cnt " ")
2706 (when add
2707 (insert (propertize add 'font-lock-face
2708 'magit-diffstat-added)))
2709 (when del
2710 (insert (propertize del 'font-lock-face
2711 'magit-diffstat-removed)))
2712 (insert "\n")))))
2713 (if (looking-at "^$") (forward-line) (insert "\n"))))))
2714
2715 (defun magit-diff-wash-diff (args)
2716 (cond
2717 ((looking-at "^Submodule")
2718 (magit-diff-wash-submodule))
2719 ((looking-at "^\\* Unmerged path \\(.*\\)")
2720 (let ((file (magit-decode-git-path (match-str 1))))
2721 (magit-delete-line)
2722 (unless (and (derived-mode-p 'magit-status-mode)
2723 (not (member "--cached" args)))
2724 (magit-insert-section (file file)
2725 (insert (propertize
2726 (format "unmerged %s%s" file
2727 (pcase (cddr (car (magit-file-status file)))
2728 ('(?D ?D) " (both deleted)")
2729 ('(?D ?U) " (deleted by us)")
2730 ('(?U ?D) " (deleted by them)")
2731 ('(?A ?A) " (both added)")
2732 ('(?A ?U) " (added by us)")
2733 ('(?U ?A) " (added by them)")
2734 ('(?U ?U) "")))
2735 'font-lock-face 'magit-diff-file-heading))
2736 (insert ?\n))))
2737 t)
2738 ((looking-at magit-diff-conflict-headline-re)
2739 (let ((long-status (match-str 0))
2740 (status "BUG")
2741 file orig base)
2742 (cond ((equal long-status "merged")
2743 (setq status long-status)
2744 (setq long-status nil))
2745 ((setq status
2746 (pcase-exhaustive long-status
2747 ("added in remote" "new file")
2748 ("added in both" "new file")
2749 ("added in local" "new file")
2750 ("removed in both" "removed")
2751 ("changed in both" "changed")
2752 ("removed in local" "removed")
2753 ("removed in remote" "removed")))))
2754 (magit-delete-line)
2755 (while (looking-at
2756 "^ \\([^ ]+\\) +[0-9]\\{6\\} \\([a-z0-9]\\{40,\\}\\) \\(.+\\)$")
2757 (magit-bind-match-strings (side _blob name) nil
2758 (pcase side
2759 ("result" (setq file name))
2760 ("our" (setq orig name))
2761 ("their" (setq file name))
2762 ("base" (setq base name))))
2763 (magit-delete-line))
2764 (when orig (setq orig (magit-decode-git-path orig)))
2765 (when file (setq file (magit-decode-git-path file)))
2766 (magit-diff-insert-file-section
2767 (or file base) orig status nil nil nil nil long-status)))
2768 ;; The files on this line may be ambiguous due to whitespace.
2769 ;; That's okay. We can get their names from subsequent headers.
2770 ((looking-at "^diff --\
2771 \\(?:\\(?1:git\\) \\(?:\\(?2:.+?\\) \\2\\)?\
2772 \\|\\(?:cc\\|combined\\) \\(?3:.+\\)\\)")
2773 (let ((status (cond ((equal (match-str 1) "git") "modified")
2774 ((derived-mode-p 'magit-revision-mode) "resolved")
2775 (t "unmerged")))
2776 (orig nil)
2777 (file (or (match-str 2) (match-str 3)))
2778 (header (list (buffer-substring-no-properties
2779 (line-beginning-position) (1+ (line-end-position)))))
2780 (modes nil)
2781 (rename nil)
2782 (binary nil)
2783 (beg (point)))
2784 (forward-line)
2785 (while (not (or (eobp)
2786 (looking-at magit-diff-headline-re)
2787 (looking-at magit-log-heading-re)))
2788 (cond
2789 ((looking-at "old mode \\(?:[^\n]+\\)\nnew mode \\(?:[^\n]+\\)\n")
2790 (setq modes (match-str 0)))
2791 ((looking-at "deleted file .+\n")
2792 (setq status "deleted"))
2793 ((looking-at "new file .+\n")
2794 (setq status "new file"))
2795 ((looking-at "rename from \\(.+\\)\nrename to \\(.+\\)\n")
2796 (setq rename (match-str 0))
2797 (setq orig (match-str 1))
2798 (setq file (match-str 2))
2799 (setq status "renamed"))
2800 ((looking-at "copy from \\(.+\\)\ncopy to \\(.+\\)\n")
2801 (setq orig (match-str 1))
2802 (setq file (match-str 2))
2803 (setq status "copied"))
2804 ((looking-at "similarity index .+\n"))
2805 ((looking-at "dissimilarity index .+\n"))
2806 ((looking-at "index .+\n"))
2807 ((looking-at "--- \\(.+?\\)\t?\n")
2808 (unless (equal (match-str 1) "/dev/null")
2809 (setq orig (match-str 1))))
2810 ((looking-at "\\+\\+\\+ \\(.+?\\)\t?\n")
2811 (unless (equal (match-str 1) "/dev/null")
2812 (setq file (match-str 1))))
2813 ((looking-at "Binary files .+ and .+ differ\n")
2814 (setq binary t))
2815 ((looking-at "Binary files differ\n")
2816 (setq binary t))
2817 ;; TODO Use all combined diff extended headers.
2818 ((looking-at "mode .+\n"))
2819 ((error "BUG: Unknown extended header: %S"
2820 (buffer-substring (point) (line-end-position)))))
2821 ;; These headers are treated as some sort of special hunk.
2822 (unless (or (string-prefix-p "old mode" (match-str 0))
2823 (string-prefix-p "rename" (match-str 0)))
2824 (push (match-str 0) header))
2825 (goto-char (match-end 0)))
2826 (delete-region beg (point))
2827 (when orig
2828 (setq orig (magit-decode-git-path orig)))
2829 (setq file (magit-decode-git-path file))
2830 (setq header (nreverse header))
2831 ;; KLUDGE Before v2.54, "git log" ignored "--no-prefix"
2832 ;; when "-L" is used.
2833 (when (and (magit-git-version< "2.54")
2834 (derived-mode-p 'magit-log-mode)
2835 (seq-some (##string-prefix-p "-L" %)
2836 magit-buffer-log-args))
2837 (when orig
2838 (setq orig (substring orig 2)))
2839 (setq file (substring file 2))
2840 (setq header (list (save-excursion
2841 (string-match "diff [^ ]+" (car header))
2842 (format "%s %s %s\n"
2843 (match-str 0 (car header))
2844 (or orig file)
2845 (or file orig)))
2846 (format "--- %s\n" (or orig "/dev/null"))
2847 (format "+++ %s\n" (or file "/dev/null")))))
2848 (setq header (string-join header))
2849 (magit-diff-insert-file-section
2850 file orig status modes rename header binary nil)))))
2851
2852 (defun magit-diff-insert-file-section
2853 (file orig status modes rename header binary long-status)
2854 (magit-insert-section
2855 ( file file
2856 (or (equal status "deleted") (derived-mode-p 'magit-status-mode))
2857 :source (and (not (equal orig file)) orig)
2858 :header header
2859 :binary binary)
2860 (magit-insert-heading
2861 (magit-format-file 'diff file 'magit-diff-file-heading status
2862 (and (not (equal orig file)) orig))
2863 (cond ((and binary long-status)
2864 (format " (%s, binary)" long-status))
2865 ((or binary long-status)
2866 (format " (%s)" (if binary "binary" long-status))))
2867 "\n")
2868 (when modes
2869 (magit-insert-section (hunk '(chmod))
2870 (magit-insert-heading (magit--propertize-face modes 'default))))
2871 (when rename
2872 (magit-insert-section (hunk '(rename))
2873 (magit-insert-heading (magit--propertize-face rename 'default))))
2874 (magit-wash-sequence #'magit-diff-wash-hunk)))
2875
2876 (defun magit-format-file (kind file face &optional status orig)
2877 (funcall magit-format-file-function kind file face status orig))
2878
2879 (defun magit-format-file-default (_kind file face &optional status orig)
2880 "Show only the Git status and the filename."
2881 (propertize (concat (and status (format "%-11s" status))
2882 (if orig (format "%s -> %s" orig file) file))
2883 'font-lock-face face))
2884
2885 (defun magit-format-file-all-the-icons (kind file face &optional status orig)
2886 "Show the status, filename and icon (using the `all-the-icons' package).
2887 You have to explicitly install the `all-the-icons' package, else this
2888 function errors."
2889 (magit--get-file-icon kind file face status orig
2890 'all-the-icons-icon-for-file
2891 'all-the-icons-icon-for-dir
2892 'all-the-icons-dir-is-submodule))
2893
2894 (defun magit-format-file-nerd-icons (kind file face &optional status orig)
2895 "Show the status, filename and icon (using the `nerd-icons' package).
2896 You have to explicitly install the `nerd-icons' package, else this
2897 function errors."
2898 (magit--get-file-icon kind file face status orig
2899 'nerd-icons-icon-for-file
2900 'nerd-icons-icon-for-dir
2901 'nerd-icons-dir-is-submodule))
2902
2903 (defun magit--get-file-icon ( kind file face status orig
2904 icon-for-file icon-for-dir dir-is-submodule)
2905 (cl-flet ((icon (if (or (eq kind 'module) (string-suffix-p "/" file))
2906 icon-for-dir
2907 icon-for-file)))
2908 (cl-letf (((symbol-function dir-is-submodule)
2909 (if (eq kind 'module)
2910 (lambda (_) t)
2911 (symbol-function dir-is-submodule))))
2912 (if orig
2913 (format (magit--propertize-face "%s%s %s -> %s %s" face)
2914 (if status (format "%-11s" status) "")
2915 (icon orig) orig
2916 (icon file) file)
2917 (format (magit--propertize-face "%s%s %s" face)
2918 (if status (format "%-11s" status) "")
2919 (icon file)
2920 file)))))
2921
2922 (defun magit-diff-wash-submodule ()
2923 ;; See `show_submodule_summary' in submodule.c and "this" commit.
2924 (when (looking-at "^Submodule \\([^ ]+\\)")
2925 (let ((module (match-str 1))
2926 untracked modified)
2927 (when (looking-at "^Submodule [^ ]+ contains untracked content$")
2928 (magit-delete-line)
2929 (setq untracked t))
2930 (when (looking-at "^Submodule [^ ]+ contains modified content$")
2931 (magit-delete-line)
2932 (setq modified t))
2933 (cond
2934 ((and (looking-at "^Submodule \\([^ ]+\\) \\([^ :]+\\)\\( (rewind)\\)?:$")
2935 (equal (match-str 1) module))
2936 (magit-bind-match-strings (_module range rewind) nil
2937 (magit-delete-line)
2938 (while (looking-at "^ \\([<>]\\) \\(.*\\)$")
2939 (magit-delete-line))
2940 (when rewind
2941 (setq range (replace-regexp-in-string "[^.]\\(\\.\\.\\)[^.]"
2942 "..." range t t 1)))
2943 (magit-insert-section (module module t)
2944 (magit-insert-heading
2945 (magit-format-file 'module module 'magit-diff-file-heading
2946 "modified")
2947 " ("
2948 (cond (rewind "rewind")
2949 ((string-search "..." range) "non-ff")
2950 ("new commits"))
2951 (and (or modified untracked)
2952 (concat ", "
2953 (and modified "modified")
2954 (and modified untracked " and ")
2955 (and untracked "untracked")
2956 " content"))
2957 ")")
2958 (magit-insert-section-body
2959 (let ((default-directory
2960 (file-name-as-directory
2961 (expand-file-name module (magit-toplevel)))))
2962 (magit-git-wash (apply-partially #'magit-log-wash-log 'module)
2963 "log" "--oneline" "--left-right" range)
2964 (delete-char -1))))))
2965 ((and (looking-at "^Submodule \\([^ ]+\\) \\([^ ]+\\) (\\([^)]+\\))$")
2966 (equal (match-str 1) module))
2967 (magit-bind-match-strings (_module _range msg) nil
2968 (magit-delete-line)
2969 (magit-insert-section (module module)
2970 (magit-insert-heading
2971 (magit-format-file 'module module 'magit-diff-file-heading
2972 "submodule")
2973 " (" msg ")"))))
2974 (t
2975 (magit-insert-section (module module)
2976 (magit-insert-heading
2977 (magit-format-file 'module module 'magit-diff-file-heading
2978 "modified")
2979 " ("
2980 (and modified "modified")
2981 (and modified untracked " and ")
2982 (and untracked "untracked")
2983 " content)")))))))
2984
2985 (defun magit-diff-wash-hunk ()
2986 (when (looking-at "^@\\{2,\\} \\(.+?\\) @\\{2,\\}\\(?: \\(.*\\)\\)?")
2987 (let* ((ranges (mapcar
2988 (lambda (str)
2989 (let ((range
2990 (mapcar #'string-to-number
2991 (split-string (substring str 1) ","))))
2992 ;; A single line is +1 rather than +1,1.
2993 (if (length= range 1)
2994 (nconc range (list 1))
2995 range)))
2996 (split-string (match-str 1))))
2997 (about (match-str 2))
2998 (combined (length= ranges 3))
2999 (value (cons about ranges))
3000 (beg (point)))
3001 (magit-insert-section
3002 ( hunk value nil
3003 :combined combined
3004 :from-range (if combined (butlast ranges) (car ranges))
3005 :to-range (car (last ranges))
3006 :about about)
3007 (forward-line)
3008 (put-text-property beg (point) 'font-lock-face 'magit-diff-hunk-heading)
3009 (magit-insert-heading)
3010 (while (not (or (eobp) (looking-at "^[^-+\s\\]")))
3011 (forward-line))))
3012 t))
3013
3014 (defun magit-diff-expansion-threshold (section)
3015 "Keep new diff sections collapsed if washing takes too long."
3016 (and (magit-file-section-p section)
3017 (> (float-time (time-since magit--refresh-start-time))
3018 magit-diff-expansion-threshold)
3019 'hide))
3020
3021 (add-hook 'magit-section-set-visibility-hook #'magit-diff-expansion-threshold)
3022
3023 ;;; Revision Mode
3024
3025 (defvar-keymap magit-revision-mode-map
3026 :doc "Keymap for `magit-revision-mode'."
3027 :parent magit-diff-mode-map
3028 "j" #'magit-revision-jump)
3029
3030 (define-derived-mode magit-revision-mode magit-diff-mode "Magit Rev"
3031 "Mode for looking at a Git commit.
3032
3033 This mode is documented in info node `(magit)Revision Buffer'.
3034
3035 \\<magit-mode-map>\
3036 Type \\[magit-refresh] to refresh the current buffer.
3037 Type \\[magit-section-toggle] to expand or hide the section at point.
3038 Type \\[magit-visit-thing] to visit the hunk or file at point.
3039
3040 Staging and applying changes is documented in info node
3041 `(magit)Staging and Unstaging' and info node `(magit)Applying'.
3042
3043 \\<magit-hunk-section-map>Type \
3044 \\[magit-apply] to apply the change at point, \
3045 \\[magit-stage] to stage,
3046 \\[magit-unstage] to unstage, \
3047 \\[magit-discard] to discard, or \
3048 \\[magit-reverse] to reverse it.
3049
3050 \\{magit-revision-mode-map}"
3051 :interactive nil
3052 :group 'magit-revision
3053 (magit-hack-dir-local-variables))
3054
3055 (put 'magit-revision-mode 'magit-diff-default-arguments
3056 '("--stat" "--no-ext-diff"))
3057
3058 (defun magit-revision-setup-buffer (rev args files)
3059 (magit-setup-buffer #'magit-revision-mode
3060 magit-revision-use-dedicated-buffers
3061 (magit-buffer-revision rev)
3062 (magit-buffer-diff-range (format "%s^..%s" rev rev))
3063 (magit-buffer-diff-type 'committed)
3064 (magit-buffer-diff-args args)
3065 (magit-buffer-diff-files files)
3066 (magit-buffer-diff-files-suspended nil)))
3067
3068 (defun magit-revision-refresh-buffer ()
3069 (setq magit-buffer-revision-oid (magit-commit-oid magit-buffer-revision))
3070 (magit-set-header-line-format
3071 (concat (magit-object-type magit-buffer-revision)
3072 " " magit-buffer-revision
3073 (pcase (length magit-buffer-diff-files)
3074 (0)
3075 (1 (concat " limited to file " (car magit-buffer-diff-files)))
3076 (_ (concat " limited to files "
3077 (string-join magit-buffer-diff-files ", "))))))
3078 (magit-insert-section (commitbuf)
3079 (magit-run-section-hook 'magit-revision-sections-hook)))
3080
3081 (cl-defmethod magit-buffer-value (&context (major-mode magit-revision-mode))
3082 (cons magit-buffer-revision magit-buffer-diff-files))
3083
3084 ;;; Revision Sections
3085
3086 (defun magit-insert-revision-diff ()
3087 "Insert the diff into this `magit-revision-mode' buffer."
3088 (magit--insert-diff t
3089 "show" "-p" "--format=" "--no-prefix"
3090 (and (member "--stat" magit-buffer-diff-args) "--numstat")
3091 magit-buffer-diff-args
3092 (magit--rev-dereference magit-buffer-revision)
3093 "--" magit-buffer-diff-files))
3094
3095 (defun magit-insert-revision-tag ()
3096 "Insert tag message and headers into a revision buffer.
3097 This function only inserts anything when `magit-show-commit' is
3098 called with a tag as argument, when that is called with a commit
3099 or a ref which is not a branch, then it inserts nothing."
3100 (when (magit-annotated-tag-p magit-buffer-revision)
3101 (magit-insert-section (taginfo)
3102 (let ((beg (point)))
3103 ;; "git verify-tag -v" would output what we need, but the gpg
3104 ;; output is send to stderr and we have no control over the
3105 ;; order in which stdout and stderr are inserted, which would
3106 ;; make parsing hard. We are forced to use "git cat-file tag"
3107 ;; instead, which inserts the signature instead of verifying
3108 ;; it. We remove that later and then insert the verification
3109 ;; output using "git verify-tag" (without the "-v").
3110 (magit-git-insert "cat-file" "tag" magit-buffer-revision)
3111 (goto-char beg)
3112 (forward-line 3)
3113 (delete-region beg (point)))
3114 (looking-at "^tagger \\([^<]+\\) <\\([^>]+\\)")
3115 (let ((heading (format "Tagger: %s <%s>"
3116 (match-str 1)
3117 (match-str 2))))
3118 (magit-delete-line)
3119 (magit-insert-heading
3120 (propertize heading 'font-lock-face
3121 'magit-section-secondary-heading)))
3122 (forward-line)
3123 (magit-insert-section
3124 ( message nil nil
3125 :heading-highlight-face 'magit-diff-revision-summary-highlight)
3126 (let ((beg (point)))
3127 (forward-line)
3128 (magit--add-face-text-property
3129 beg (point) 'magit-diff-revision-summary))
3130 (magit-insert-heading)
3131 (if (re-search-forward "-----BEGIN PGP SIGNATURE-----" nil t)
3132 (goto-char (match-beginning 0))
3133 (goto-char (point-max)))
3134 (insert ?\n))
3135 (cond ((re-search-forward "-----BEGIN PGP SIGNATURE-----" nil t)
3136 (let ((beg (match-beginning 0)))
3137 (re-search-forward "-----END PGP SIGNATURE-----\n")
3138 (delete-region beg (point)))
3139 (save-excursion
3140 (magit-process-git t "verify-tag" magit-buffer-revision))
3141 (magit-diff-wash-signature magit-buffer-revision))
3142 ((goto-char (point-max))))
3143 (insert ?\n))))
3144
3145 (defvar-keymap magit-commit-message-section-map
3146 :doc "Keymap for `commit-message' sections."
3147 "<remap> <magit-visit-thing>" #'magit-show-commit
3148 "<1>" (magit-menu-item "Visit %t" #'magit-show-commit
3149 '(:enable (magit-thing-at-point 'git-revision t))))
3150
3151 (defun magit-insert-revision-message ()
3152 "Insert the commit message into a revision buffer."
3153 (magit-insert-section
3154 ( commit-message nil nil
3155 :heading-highlight-face 'magit-diff-revision-summary-highlight)
3156 (cond-let*
3157 ([rev magit-buffer-revision]
3158 [msg (with-temp-buffer
3159 (save-excursion (magit-rev-insert-format "%B" rev))
3160 (magit-revision--wash-message))]
3161 (save-excursion (insert msg))
3162 (magit-revision--wash-message-hashes)
3163 (save-excursion
3164 (magit--add-face-text-property (point)
3165 (progn (forward-line) (point))
3166 'magit-diff-revision-summary
3167 t nil t)
3168 (magit-insert-heading))
3169 (goto-char (point-max)))
3170 ((insert "(no message)\n")))))
3171
3172 (defun magit-insert-revision-notes ()
3173 "Insert commit notes into a revision buffer."
3174 (let ((default (or (magit-get "core.notesRef") "refs/notes/commits")))
3175 (dolist (ref (magit-list-active-notes-refs))
3176 (when-let* ((rev magit-buffer-revision)
3177 (msg (with-temp-buffer
3178 (save-excursion
3179 (magit-git-insert "-c" (concat "core.notesRef=" ref)
3180 "notes" "show" rev))
3181 (magit-revision--wash-message))))
3182 (magit-insert-section
3183 ( notes ref (not (equal ref default))
3184 :heading-highlight-face 'magit-diff-hunk-heading-highlight)
3185 (save-excursion (insert msg))
3186 (magit-revision--wash-message-hashes)
3187 (save-excursion
3188 (end-of-line)
3189 (insert (format " (%s)"
3190 (propertize (if (string-prefix-p "refs/notes/" ref)
3191 (substring ref 11)
3192 ref)
3193 'font-lock-face 'magit-refname))))
3194 (magit--add-face-text-property (point)
3195 (progn (forward-line) (point))
3196 'magit-diff-revision-summary
3197 t nil t)
3198 (magit-insert-heading)
3199 (goto-char (point-max))
3200 (insert ?\n))))))
3201
3202 (defun magit-revision--wash-message ()
3203 (let ((major-mode 'git-commit-mode))
3204 (hack-dir-local-variables)
3205 (hack-local-variables-apply))
3206 (unless (memq git-commit-major-mode '(nil text-mode))
3207 (funcall git-commit-major-mode)
3208 (font-lock-ensure))
3209 (when (> (point-max) (point-min))
3210 (save-excursion
3211 (while (search-forward "\r\n" nil t) ; Remove trailing CRs.
3212 (delete-region (match-beginning 0) (1+ (match-beginning 0)))))
3213 (when magit-revision-fill-summary-line
3214 (let ((fill-column (min magit-revision-fill-summary-line
3215 (window-width (get-buffer-window nil t)))))
3216 (fill-region (point) (line-end-position))))
3217 (run-hook-wrapped 'magit-revision-wash-message-hook
3218 (lambda (fn) (prog1 nil (save-excursion (funcall fn)))))
3219 (buffer-string)))
3220
3221 (defun magit-highlight-squash-markers ()
3222 "Highlight \"squash!\" and similar markers."
3223 (when (looking-at "\\(?:squash!\\|fixup!\\|amend!\\)")
3224 (magit--add-face-text-property (match-beginning 0) (match-end 0)
3225 'magit-keyword-squash)))
3226
3227 (defun magit-highlight-bracket-keywords ()
3228 "Highlight text between brackets."
3229 (while (re-search-forward "\\[[^][]*]" nil t)
3230 (put-text-property (match-beginning 0)
3231 (match-end 0)
3232 'font-lock-face 'magit-keyword)))
3233
3234 (defun magit-revision--wash-message-hashes ()
3235 (when magit-revision-use-hash-sections
3236 (save-excursion
3237 ;; Start after beg to prevent a (commit text) section from
3238 ;; starting at the same point as the (commit-message)
3239 ;; section.
3240 (while (not (eobp))
3241 (re-search-forward "\\_<" nil 'move)
3242 (let ((beg (point)))
3243 (re-search-forward "\\_>" nil t)
3244 (when (> (point) beg)
3245 (let ((text (buffer-substring-no-properties beg (point))))
3246 (when (pcase magit-revision-use-hash-sections
3247 ('quickest ; false negatives and positives
3248 (and (>= (length text) 7)
3249 (string-match-p "[0-9]" text)
3250 (string-match-p "[a-z]" text)))
3251 ('quicker ; false negatives (number-less hashes)
3252 (and (>= (length text) 7)
3253 (string-match-p "[0-9]" text)
3254 (magit-commit-p text)))
3255 ('quick ; false negatives (short hashes)
3256 (and (>= (length text) 7)
3257 (magit-commit-p text)))
3258 ('slow
3259 (magit-commit-p text)))
3260 (put-text-property beg (point)
3261 'font-lock-face 'magit-hash)
3262 (let ((end (point)))
3263 (goto-char beg)
3264 (magit-insert-section (commit text)
3265 (goto-char end)))))))))))
3266
3267 (defun magit-insert-revision-headers ()
3268 "Insert headers about the commit into a revision buffer."
3269 (magit-insert-section (headers)
3270 (magit-insert-heading nil
3271 (and$ (magit-rev-format "%D" magit-buffer-revision "--decorate=full")
3272 (concat (magit-format-ref-labels $) " "))
3273 (propertize
3274 (magit-rev-parse (magit--rev-dereference magit-buffer-revision))
3275 'font-lock-face 'magit-hash))
3276 (let ((beg (point)))
3277 (magit-rev-insert-format magit-revision-headers-format
3278 magit-buffer-revision)
3279 (magit-insert-revision-gravatars magit-buffer-revision beg))
3280 (when magit-revision-insert-related-refs
3281 (when (magit-revision-insert-related-refs-display-p 'parents)
3282 (dolist (parent (magit-commit-parents magit-buffer-revision))
3283 (magit-insert-section (commit parent)
3284 (let ((line (magit-rev-format "%h %s" parent)))
3285 (string-match "^\\([^ ]+\\) \\(.*\\)" line)
3286 (magit-bind-match-strings (hash msg) line
3287 (insert "Parent: ")
3288 (insert (propertize hash 'font-lock-face 'magit-hash))
3289 (insert " " msg "\n"))))))
3290 (when (magit-revision-insert-related-refs-display-p 'merged)
3291 (magit--insert-related-refs
3292 magit-buffer-revision "--merged" "Merged"
3293 (eq magit-revision-insert-related-refs 'all)))
3294 (when (magit-revision-insert-related-refs-display-p 'contained)
3295 (magit--insert-related-refs
3296 magit-buffer-revision "--contains" "Contained"
3297 (memq magit-revision-insert-related-refs '(all mixed))))
3298 (when-let ((_(magit-revision-insert-related-refs-display-p 'follows))
3299 (follows (magit-get-current-tag magit-buffer-revision t)))
3300 (let ((tag (car follows))
3301 (cnt (cadr follows)))
3302 (magit-insert-section (tag tag)
3303 (insert
3304 (format "Follows: %s (%s)\n"
3305 (propertize tag 'font-lock-face 'magit-tag)
3306 (propertize (number-to-string cnt)
3307 'font-lock-face 'magit-branch-local))))))
3308 (when-let ((_(magit-revision-insert-related-refs-display-p 'precedes))
3309 (precedes (magit-get-next-tag magit-buffer-revision t)))
3310 (let ((tag (car precedes))
3311 (cnt (cadr precedes)))
3312 (magit-insert-section (tag tag)
3313 (insert (format "Precedes: %s (%s)\n"
3314 (propertize tag 'font-lock-face 'magit-tag)
3315 (propertize (number-to-string cnt)
3316 'font-lock-face 'magit-tag))))))
3317 (insert ?\n))))
3318
3319 (defun magit-revision-insert-related-refs-display-p (sym)
3320 "Whether to display related branches of type SYM.
3321 Refer to user option `magit-revision-insert-related-refs-display-alist'."
3322 (if-let ((elt (assq sym magit-revision-insert-related-refs-display-alist)))
3323 (cdr elt)
3324 t))
3325
3326 (defun magit--insert-related-refs (rev arg title remote)
3327 (when-let ((refs (magit-list-related-branches arg rev (and remote "-a"))))
3328 (magit-insert-section (related-refs)
3329 (insert title ":" (make-string (- 10 (length title)) ?\s))
3330 (dolist (branch refs)
3331 (if (<= (+ (current-column) 1 (length branch))
3332 (window-width))
3333 (insert ?\s)
3334 (insert ?\n (make-string 12 ?\s)))
3335 (insert (propertize branch 'font-lock-face
3336 (if (string-prefix-p "remotes/" branch)
3337 'magit-branch-remote
3338 'magit-branch-local))))
3339 (insert ?\n))))
3340
3341 (defun magit-insert-revision-gravatars (rev beg)
3342 (when (and magit-revision-show-gravatars
3343 (window-system))
3344 (require 'gravatar)
3345 (pcase-let ((`(,author . ,committer)
3346 (pcase magit-revision-show-gravatars
3347 ('t '("^Author: " . "^Commit: "))
3348 ('author '("^Author: " . nil))
3349 ('committer '(nil . "^Commit: "))
3350 (_ magit-revision-show-gravatars))))
3351 (when-let ((email (and author (magit-rev-format "%aE" rev))))
3352 (magit-insert-revision-gravatar beg rev email author))
3353 (when-let ((email (and committer (magit-rev-format "%cE" rev))))
3354 (magit-insert-revision-gravatar beg rev email committer)))))
3355
3356 (defun magit-insert-revision-gravatar (beg rev email regexp)
3357 (save-excursion
3358 (goto-char beg)
3359 (when-let ((_(re-search-forward regexp nil t))
3360 (window (get-buffer-window)))
3361 (let* ((column (length (match-str 0)))
3362 (font-obj (query-font (font-at (point) window)))
3363 (size (* 2 (+ (aref font-obj 4)
3364 (aref font-obj 5))))
3365 (align-to (+ column
3366 (ceiling (/ size (aref font-obj 7) 1.0))
3367 1))
3368 (gravatar-size (- size 2)))
3369 (ignore-errors ; service may be unreachable
3370 (gravatar-retrieve email #'magit-insert-revision-gravatar-cb
3371 (list gravatar-size rev
3372 (point-marker)
3373 align-to column)))))))
3374
3375 (defun magit-insert-revision-gravatar-cb (image size rev marker align-to column)
3376 (cond-let
3377 ((eq image 'error))
3378 ([buffer (marker-buffer marker)]
3379 (with-current-buffer buffer
3380 (save-excursion
3381 (goto-char marker)
3382 ;; The buffer might display another revision by now or
3383 ;; it might have been refreshed, in which case another
3384 ;; process might already have inserted the image.
3385 (when (and (equal rev magit-buffer-revision)
3386 (not (eq (car-safe
3387 (car-safe
3388 (get-text-property (point) 'display)))
3389 'image)))
3390 (setf (image-property image :ascent) 'center)
3391 (setf (image-property image :relief) 1)
3392 (setf (image-property image :scale) 1)
3393 (setf (image-property image :height) size)
3394 (let ((top (list image '(slice 0.0 0.0 1.0 0.5)))
3395 (bot (list image '(slice 0.0 0.5 1.0 1.0)))
3396 (align `((space :align-to ,align-to))))
3397 (let ((inhibit-read-only t))
3398 (insert (propertize " " 'display top))
3399 (insert (propertize " " 'display align))
3400 (forward-line)
3401 (forward-char column)
3402 (insert (propertize " " 'display bot))
3403 (insert (propertize " " 'display align))))))))))
3404
3405 ;;; Merge-Preview Mode
3406
3407 (define-derived-mode magit-merge-preview-mode magit-diff-mode "Magit Merge"
3408 "Mode for previewing a merge."
3409 :interactive nil
3410 :group 'magit-diff
3411 (magit-hack-dir-local-variables))
3412
3413 (put 'magit-merge-preview-mode 'magit-diff-default-arguments
3414 '("--no-ext-diff"))
3415
3416 (defun magit-merge-preview-setup-buffer (rev)
3417 (magit-setup-buffer #'magit-merge-preview-mode nil
3418 (magit-buffer-revision rev)
3419 (magit-buffer-diff-range (format "%s^..%s" rev rev))))
3420
3421 (defun magit-merge-preview-refresh-buffer ()
3422 (let* ((branch (magit-get-current-branch))
3423 (head (or branch (magit-rev-verify "HEAD"))))
3424 (magit-set-header-line-format (format "Preview merge of %s into %s"
3425 magit-buffer-revision
3426 (or branch "HEAD")))
3427 (magit-insert-section (diffbuf)
3428 (magit--insert-diff t
3429 "merge-tree" (magit-merge-base head magit-buffer-revision)
3430 head magit-buffer-revision))))
3431
3432 (cl-defmethod magit-buffer-value (&context (major-mode magit-merge-preview-mode))
3433 magit-buffer-revision)
3434
3435 ;;; Hunk Section
3436
3437 (defun magit-hunk-set-window-start (section)
3438 "When SECTION is a `hunk', ensure that its beginning is visible.
3439 It the SECTION has a different type, then do nothing."
3440 (when (magit-hunk-section-p section)
3441 (magit-section-set-window-start section)))
3442
3443 (add-hook 'magit-section-movement-hook #'magit-hunk-set-window-start)
3444
3445 (cl-defmethod magit-section-get-relative-position ((_section magit-hunk-section))
3446 (nconc (cl-call-next-method)
3447 (and (region-active-p)
3448 (progn
3449 (goto-char (line-beginning-position))
3450 (when (looking-at "^[-+]") (forward-line))
3451 (while (looking-at "^[ @]") (forward-line))
3452 (let ((beg (magit-point)))
3453 (list (cond
3454 ((looking-at "^[-+]")
3455 (forward-line)
3456 (while (looking-at "^[-+]") (forward-line))
3457 (while (looking-at "^ ") (forward-line))
3458 (forward-line -1)
3459 (regexp-quote (buffer-substring-no-properties
3460 beg (line-end-position))))
3461 (t))))))))
3462
3463 (cl-defmethod magit-section-goto-successor ((section magit-hunk-section)
3464 line char &optional arg)
3465 (or (magit-section-goto-successor--same section line char)
3466 (and-let ((parent (magit-get-section
3467 (magit-section-ident
3468 (oref section parent)))))
3469 (let* ((children (oref parent children))
3470 (siblings (magit-section-siblings section 'prev))
3471 (previous (nth (length siblings) children)))
3472 (if (not arg)
3473 (when-let ((sibling (or previous (car (last children)))))
3474 (magit-section-goto sibling)
3475 t)
3476 (when previous
3477 (magit-section-goto previous))
3478 (if (and (stringp arg)
3479 (re-search-forward arg (oref parent end) t))
3480 (goto-char (match-beginning 0))
3481 (goto-char (oref (car (last children)) end))
3482 (forward-line -1)
3483 (while (looking-at "^ ") (forward-line -1))
3484 (while (looking-at "^[-+]") (forward-line -1))
3485 (forward-line)))))
3486 (magit-section-goto-successor--related section)))
3487
3488 ;;; Diff Sections
3489
3490 (defvar-keymap magit-unstaged-section-map
3491 :doc "Keymap for the `unstaged' section."
3492 "<remap> <magit-visit-thing>" #'magit-diff-unstaged
3493 "<remap> <magit-stage-files>" #'magit-stage
3494 "<remap> <magit-delete-thing>" #'magit-discard
3495 "<3>" (magit-menu-item "Discard all" #'magit-discard)
3496 "<2>" (magit-menu-item "Stage all" #'magit-stage)
3497 "<1>" (magit-menu-item "Visit diff" #'magit-diff-unstaged))
3498
3499 (magit-define-section-jumper magit-jump-to-unstaged
3500 "Unstaged changes" unstaged nil magit-insert-unstaged-changes)
3501
3502 (defun magit-insert-unstaged-changes ()
3503 "Insert section showing unstaged changes."
3504 (magit-insert-section (unstaged)
3505 (magit-insert-heading t "Unstaged changes")
3506 (magit--insert-diff nil
3507 "diff" magit-buffer-diff-args "--no-prefix"
3508 "--" magit-buffer-diff-files)))
3509
3510 (defvar-keymap magit-staged-section-map
3511 :doc "Keymap for the `staged' section."
3512 "<remap> <magit-revert-no-commit>" #'magit-reverse
3513 "<remap> <magit-delete-thing>" #'magit-discard
3514 "<remap> <magit-unstage-files>" #'magit-unstage
3515 "<remap> <magit-visit-thing>" #'magit-diff-staged
3516 "<4>" (magit-menu-item "Reverse all" #'magit-reverse)
3517 "<3>" (magit-menu-item "Discard all" #'magit-discard)
3518 "<2>" (magit-menu-item "Unstage all" #'magit-unstage)
3519 "<1>" (magit-menu-item "Visit diff" #'magit-diff-staged))
3520
3521 (magit-define-section-jumper magit-jump-to-staged
3522 "Staged changes" staged nil magit-insert-staged-changes)
3523
3524 (defun magit-insert-staged-changes ()
3525 "Insert section showing staged changes."
3526 ;; Avoid listing all files as deleted when visiting a bare repo.
3527 (unless (magit-bare-repo-p)
3528 (magit-insert-section (staged)
3529 (magit-insert-heading t "Staged changes")
3530 (magit--insert-diff nil
3531 "diff" "--cached" magit-buffer-diff-args "--no-prefix"
3532 "--" magit-buffer-diff-files))))
3533
3534 ;;; Diff Information
3535
3536 (defun magit-diff-type (&optional section)
3537 "Return the diff type of SECTION.
3538
3539 The returned type is one of the symbols `staged', `unstaged',
3540 `committed', or `undefined'. This type serves a similar purpose
3541 as the general type common to all sections (which is stored in
3542 the `type' slot of the corresponding `magit-section' struct) but
3543 takes additional information into account. When the SECTION
3544 isn't related to diffs and the buffer containing it also isn't
3545 a diff-only buffer, then return nil.
3546
3547 Currently the type can also be one of `tracked' and `untracked'
3548 but these values are not handled explicitly everywhere they
3549 should be and a possible fix could be to just return nil here.
3550
3551 The section has to be a `diff' or `hunk' section, or a section
3552 whose children are of type `diff'. If optional SECTION is nil,
3553 return the diff type for the current section. In buffers whose
3554 major mode is `magit-diff-mode' SECTION is ignored and the type
3555 is determined using other means. In `magit-revision-mode'
3556 buffers the type is always `committed'.
3557
3558 Do not confuse this with `magit-diff-scope' (which see)."
3559 (when-let ((section (or section (magit-current-section))))
3560 (cond ((derived-mode-p 'magit-revision-mode 'magit-stash-mode) 'committed)
3561 ((derived-mode-p 'magit-diff-mode)
3562 (cond
3563 (magit-buffer-diff-type)
3564 ((equal magit-buffer-diff-typearg "--no-index")
3565 'undefined)
3566 ((not magit-buffer-diff-range)
3567 'undefined)
3568 ((string-search "." magit-buffer-diff-range)
3569 'committed)
3570 ((magit-rev-head-p magit-buffer-diff-range)
3571 (if (equal magit-buffer-diff-typearg "--cached")
3572 'staged
3573 'unstaged))
3574 ('committed)))
3575 ((derived-mode-p 'magit-status-mode)
3576 (pcase (oref section type)
3577 ((and type (or 'staged 'unstaged 'tracked 'untracked))
3578 type)
3579 ((or 'file 'module)
3580 (let* ((parent (oref section parent))
3581 (type (oref parent type)))
3582 (if (memq type '(file module))
3583 (magit-diff-type parent)
3584 type)))
3585 ('hunk (thread-first section
3586 (oref parent)
3587 (oref parent)
3588 (oref type)))))
3589 ((derived-mode-p 'magit-log-mode)
3590 (if (or (and (magit-section-match 'commit section)
3591 (oref section children))
3592 (magit-section-match [* file commit] section))
3593 'committed
3594 'undefined))
3595 ('undefined))))
3596
3597 (cl-defun magit-diff-scope (&optional (section nil ssection) strict)
3598 "Return the diff scope of SECTION or the selected section(s).
3599
3600 A diff's \"scope\" describes what part of a diff is selected, it is
3601 a symbol, one of `region', `hunk', `hunks', `file', `files', or
3602 `list'. Do not confuse this with the diff \"type\", as returned by
3603 `magit-diff-type'.
3604
3605 If optional SECTION is non-nil, then return the scope of that,
3606 ignoring the sections selected by the region. Otherwise return
3607 the scope of the current section, or if the region is active and
3608 selects a valid group of diff related sections, the type of these
3609 sections, i.e., `hunks' or `files'. If SECTION, or if that is nil
3610 the current section, is a `hunk' section; and the region region
3611 starts and ends inside the body of a that section, then the type
3612 is `region'. If the region is empty after a mouse click, then
3613 `hunk' is returned instead of `region'.
3614
3615 If optional STRICT is non-nil, then return nil if the diff type of
3616 the section at point is `untracked' or the section at point is not
3617 actually a `diff' but a `diffstat' section."
3618 (let ((siblings (and (not ssection) (magit-region-sections nil t))))
3619 (setq section (or section (car siblings) (magit-current-section)))
3620 (when (and section
3621 (or (not strict)
3622 (and (not (eq (magit-diff-type section) 'untracked))
3623 (not (eq (and$ (oref section parent)
3624 (oref $ type))
3625 'diffstat)))))
3626 (pcase (list (oref section type)
3627 (and siblings t)
3628 (magit-diff-use-hunk-region-p)
3629 ssection)
3630 (`(hunk nil t ,_)
3631 (if (magit-section-internal-region-p section) 'region 'hunk))
3632 ('(hunk t t nil) 'hunks)
3633 (`(hunk ,_ ,_ ,_) 'hunk)
3634 ('(file t t nil) 'files)
3635 (`(file ,_ ,_ ,_) 'file)
3636 ('(module t t nil) 'files)
3637 (`(module ,_ ,_ ,_) 'file)
3638 (`(,(or 'staged 'unstaged 'untracked) nil ,_ ,_) 'list)))))
3639
3640 (defun magit-diff-use-hunk-region-p ()
3641 (and (region-active-p)
3642 ;; TODO implement this from first principals
3643 ;; currently it's trial-and-error
3644 (not (and (or (eq this-command #'mouse-drag-region)
3645 (eq last-command #'mouse-drag-region)
3646 ;; When another window was previously
3647 ;; selected then the last-command is
3648 ;; some byte-code function.
3649 (byte-code-function-p last-command))
3650 (eq (region-end) (region-beginning))))))
3651
3652 (defun magit-diff--hunk-section ()
3653 (and-let* ((section (magit-current-section))
3654 (scope (magit-diff-scope section)))
3655 ;; Currently the `hunk' type is also abused for file
3656 ;; mode changes, which we are not interested in here.
3657 (cl-flet ((first-hunk (file-section)
3658 (seq-find (##not (equal (oref % value) '(chmod)))
3659 (oref file-section children))))
3660 (pcase scope
3661 ('hunk section)
3662 ('file (first-hunk section))
3663 ('list (and$ (car (oref section children))
3664 (first-hunk $)))
3665 ('module nil)))))
3666
3667 (defun magit-diff--file-section ()
3668 (and-let* ((section (magit-current-section))
3669 (scope (magit-diff-scope section)))
3670 (pcase scope
3671 ('hunk (oref section parent))
3672 ('file section)
3673 ('list (car (oref section children)))
3674 ('module section))))
3675
3676 (defun magit-diff--file ()
3677 (and-let* ((file-section (magit-diff--file-section))
3678 (file (or (and (magit-section-match 'hunk)
3679 (magit-diff-on-removed-line-p)
3680 (oref file-section source))
3681 (oref file-section value))))
3682 (if (equal magit-buffer-diff-typearg "--no-index")
3683 (concat "/" file)
3684 (expand-file-name file (magit-toplevel)))))
3685
3686 ;;; Hunk Paint
3687 ;;;; Paint
3688
3689 (cl-defmethod magit-section-paint ((section magit-hunk-section) highlight)
3690 (unless magit-diff-highlight-hunk-body
3691 (setq highlight nil))
3692 (let ((end (oref section end))
3693 (merging (looking-at "@@@"))
3694 (diff-type (magit-diff-type))
3695 (sign-face-side nil)
3696 (line-face-side nil)
3697 (tab-width (magit-diff-tab-width
3698 (magit-section-parent-value section))))
3699 (forward-line)
3700 (while (< (point) end)
3701 (let ((bol (point))
3702 (eol (line-end-position))
3703 (sign-face nil)
3704 (line-face nil))
3705 (when (and magit-diff-hide-trailing-cr-characters
3706 (char-equal ?\r (char-before eol)))
3707 (put-text-property (1- eol) eol 'invisible t))
3708 (cond
3709 ((looking-at "^\\+\\+?\\([<=|>]\\)\\{7\\}")
3710 (setq line-face
3711 (pcase (match-str 1)
3712 ("<" 'magit-diff-our-heading)
3713 ("|" 'magit-diff-base-heading)
3714 ("=" 'magit-diff-their-heading)
3715 (">" 'magit-diff-their-heading)))
3716 (setq sign-face-side
3717 (pcase (match-str 1)
3718 ("<" 'magit-diff-our-indicator)
3719 ("|" 'magit-diff-base-indicator)
3720 ("=" 'magit-diff-their-indicator)
3721 (">" nil)))
3722 (setq line-face-side
3723 (pcase (list (match-str 1) highlight)
3724 ('("<" nil) 'magit-diff-our)
3725 ('("<" t) 'magit-diff-our-highlight)
3726 ('("|" nil) 'magit-diff-base)
3727 ('("|" t) 'magit-diff-base-highlight)
3728 ('("=" nil) 'magit-diff-their)
3729 ('("=" t) 'magit-diff-their-highlight)
3730 ;; `(">" ,_) results in bogus warning.
3731 ('(">" nil) nil)
3732 ('(">" t) nil))))
3733 ((looking-at (if merging "^\\(\\+\\| \\+\\)" "^\\+"))
3734 (magit-diff-paint-tab merging tab-width)
3735 (magit-diff-paint-whitespace merging 'added diff-type)
3736 (cond (line-face-side
3737 (setq sign-face sign-face-side)
3738 (setq line-face line-face-side))
3739 (t
3740 (setq sign-face 'magit-diff-added-indicator)
3741 (setq line-face (if highlight
3742 'magit-diff-added-highlight
3743 'magit-diff-added)))))
3744 ((looking-at (if merging "^\\(-\\| -\\)" "^-"))
3745 (magit-diff-paint-tab merging tab-width)
3746 (magit-diff-paint-whitespace merging 'removed diff-type)
3747 (setq sign-face 'magit-diff-removed-indicator)
3748 (setq line-face (if highlight
3749 'magit-diff-removed-highlight
3750 'magit-diff-removed)))
3751 (t
3752 (magit-diff-paint-tab merging tab-width)
3753 (magit-diff-paint-whitespace merging 'context diff-type)
3754 (setq line-face (if highlight
3755 'magit-diff-context-highlight
3756 'magit-diff-context))))
3757 (put-text-property bol (1+ eol) 'font-lock-face line-face)
3758 (when (and sign-face magit-diff-use-indicator-faces)
3759 (magit--add-face-text-property
3760 bol (+ bol (if merging 2 1)) sign-face)))
3761 (forward-line)))
3762 (unless (magit--meta-hunk-p section)
3763 (when (eq magit-diff-fontify-hunk 'all)
3764 (magit-diff--update-hunk-syntax section))
3765 (when (eq magit-diff-refine-hunk 'all)
3766 (magit-diff-update-hunk-refinement section)))
3767 (oset section painted (if highlight 'highlight 'plain)))
3768
3769 ;;;; Whitespace
3770
3771 (defvar magit-diff--tab-width-cache nil)
3772
3773 (defun magit-diff-tab-width (file)
3774 (cond-let
3775 ((not magit-diff-adjust-tab-width)
3776 tab-width)
3777 [[file (expand-file-name file)]]
3778 ((and$ (find-buffer-visiting file)
3779 (setf (alist-get file magit-diff--tab-width-cache nil nil #'equal)
3780 (buffer-local-value 'tab-width $))))
3781 ((and$ (assoc file magit-diff--tab-width-cache)
3782 (or (cdr $) tab-width)))
3783 ((or (eq magit-diff-adjust-tab-width 'always)
3784 (and (numberp magit-diff-adjust-tab-width)
3785 (>= magit-diff-adjust-tab-width
3786 (nth 7 (file-attributes file)))))
3787 (let* ((buf (find-file-noselect file))
3788 (val (buffer-local-value 'tab-width buf)))
3789 (kill-buffer buf)
3790 (setf (alist-get file magit-diff--tab-width-cache nil nil #'equal) val)))
3791 ((prog1 tab-width
3792 (setf (alist-get file magit-diff--tab-width-cache nil nil #'equal)
3793 nil))))) ; The buffer is too large. Don't check size again.
3794
3795 (defun magit-diff-paint-tab (merging width)
3796 (save-excursion
3797 (forward-char (if merging 2 1))
3798 (while (= (char-after) ?\t)
3799 (put-text-property (point) (1+ (point))
3800 'display (list 'space :width width))
3801 (forward-char))))
3802
3803 (defun magit-diff-paint-whitespace (merging line-type diff-type)
3804 (when (and magit-diff-paint-whitespace
3805 (or (not (memq magit-diff-paint-whitespace '(uncommitted status)))
3806 (memq diff-type '(staged unstaged)))
3807 (cl-case line-type
3808 (added t)
3809 (removed (memq magit-diff-paint-whitespace-lines '(all both)))
3810 (context (memq magit-diff-paint-whitespace-lines '(all)))))
3811 (let ((prefix (if merging "^[-\\+\s]\\{2\\}" "^[-\\+\s]"))
3812 (indent
3813 (if (local-variable-p 'magit-diff-highlight-indentation)
3814 magit-diff-highlight-indentation
3815 (setq-local
3816 magit-diff-highlight-indentation
3817 (cdr (seq-find (##string-match-p (car %) default-directory)
3818 (nreverse
3819 (default-value
3820 'magit-diff-highlight-indentation))))))))
3821 (when (and magit-diff-highlight-trailing
3822 (looking-at (concat prefix ".*?\\([ \t]+\\) ?$")))
3823 (let ((ov (make-overlay (match-beginning 1) (match-end 1) nil t)))
3824 (overlay-put ov 'font-lock-face 'magit-diff-whitespace-warning)
3825 (overlay-put ov 'priority 2)
3826 (overlay-put ov 'evaporate t)))
3827 (when (or (and (eq indent 'tabs)
3828 (looking-at (concat prefix "\\( *\t[ \t]*\\)")))
3829 (and (integerp indent)
3830 (looking-at (format "%s\\([ \t]* \\{%s,\\}[ \t]*\\)"
3831 prefix indent))))
3832 (let ((ov (make-overlay (match-beginning 1) (match-end 1) nil t)))
3833 (overlay-put ov 'font-lock-face 'magit-diff-whitespace-warning)
3834 (overlay-put ov 'priority 2)
3835 (overlay-put ov 'evaporate t))))))
3836
3837 ;;;; Refinement
3838
3839 (cl-defmethod magit-section--refine ((section magit-hunk-section))
3840 (unless (magit--meta-hunk-p section)
3841 (when (eq magit-diff-fontify-hunk t)
3842 (magit-diff--update-hunk-syntax section))
3843 (when (eq magit-diff-refine-hunk t)
3844 (magit-diff-update-hunk-refinement section))))
3845
3846 (defun magit-diff-update-hunk-refinement (&optional section allow-remove)
3847 (if section
3848 (unless (oref section hidden)
3849 (pcase (list magit-diff-refine-hunk
3850 (oref section refined)
3851 (eq section (magit-current-section)))
3852 ((or `(all nil ,_) '(t nil t))
3853 (oset section refined t)
3854 (save-excursion
3855 (goto-char (oref section start))
3856 ;; `diff-refine-hunk' does not handle combined diffs.
3857 (unless (looking-at "@@@")
3858 (let ((smerge-refine-ignore-whitespace
3859 magit-diff-refine-ignore-whitespace)
3860 ;; Avoid fsyncing many small temp files.
3861 (write-region-inhibit-fsync t))
3862 (diff-refine-hunk)))))
3863 ((and (guard allow-remove)
3864 (or `(nil t ,_) '(t t nil)))
3865 (oset section refined nil)
3866 (remove-overlays (oref section start)
3867 (oref section end)
3868 'diff-mode 'fine))))
3869 (named-let update ((section magit-root-section))
3870 (if (magit-section-match 'hunk section)
3871 (magit-diff-update-hunk-refinement section t)
3872 (dolist (child (oref section children))
3873 (update child))))))
3874
3875 ;;;; Syntax
3876
3877 (defun magit-diff--update-hunk-syntax (&optional hunk)
3878 (if hunk
3879 (pcase-let (((eieio fontified content end) hunk))
3880 (unless fontified
3881 (oset hunk fontified t)
3882 (save-excursion
3883 (goto-char content)
3884 (pcase-let*
3885 ((`(,old ,new) (magit-diff-visit--sides))
3886 (old (apply #'magit-diff--get-hunk-syntax hunk 'old old))
3887 (new (apply #'magit-diff--get-hunk-syntax hunk 'new new)))
3888 (while (< (point) end)
3889 (pcase-dolist (`(,b ,e ,face)
3890 (pcase (char-after (point))
3891 (?- (pop old))
3892 (?+ (pop new))
3893 (?\s (pop old)
3894 (pop new))))
3895 (let ((o (make-overlay (+ (point) 1 b) (+ (point) 1 e) nil t)))
3896 (overlay-put o 'evaporate t)
3897 (overlay-put o 'face face)))
3898 (forward-line 1))))))
3899 (named-let update ((section magit-root-section))
3900 (if (magit-section-match 'hunk section)
3901 (unless (magit--meta-hunk-p section)
3902 (magit-diff--update-hunk-syntax section))
3903 (dolist (child (oref section children))
3904 (update child))))))
3905
3906 (defun magit-diff--get-hunk-syntax (hunk side rev file)
3907 (let ((args (magit-diff--get-hunk-text hunk (eq side 'old))))
3908 (unless (listp (car (cadr args))) ; TODO Support unmerged changes.
3909 (with-current-buffer (magit-find-file-noselect rev file t t)
3910 (save-excursion
3911 (apply #'diff-syntax-fontify-props nil args))))))
3912
3913 (defun magit-diff--get-hunk-text (hunk from)
3914 (pcase-let* (((eieio start end from-range to-range) hunk)
3915 (`(,line ,lines) (if from from-range to-range)))
3916 (with-demoted-errors "Error getting hunk text: %S"
3917 (list (string-trim-right
3918 (diff-hunk-text (buffer-substring-no-properties start end)
3919 (not from) nil))
3920 (list line lines)))))
3921
3922 ;;; Hunk Region
3923
3924 (defun magit-diff-hunk-region-beginning ()
3925 (magit--bol-position (region-beginning)))
3926
3927 (defun magit-diff-hunk-region-end ()
3928 (magit--eol-position (region-end)))
3929
3930 (defun magit-diff-update-hunk-region (section)
3931 "Highlight the hunk-internal region if any."
3932 (when (and (eq (oref section type) 'hunk)
3933 (eq (magit-diff-scope section t) 'region))
3934 (magit-diff--make-hunk-overlay
3935 (oref section start)
3936 (1- (oref section content))
3937 'font-lock-face 'magit-diff-lines-heading
3938 'display (magit-diff-hunk-region-header section)
3939 'after-string (magit-diff--hunk-after-string 'magit-diff-lines-heading))
3940 (run-hook-with-args 'magit-diff-highlight-hunk-region-functions section)
3941 t))
3942
3943 (defun magit-diff-highlight-hunk-region-dim-outside (section)
3944 "Dim the parts of the hunk that are outside the hunk-internal region.
3945 This is done by using the same foreground and background color
3946 for added and removed lines as for context lines."
3947 (let ((face (if magit-diff-highlight-hunk-body
3948 'magit-diff-context-highlight
3949 'magit-diff-context)))
3950 (when magit-diff-unmarked-lines-keep-foreground
3951 (setq face `(:extend t :background ,(face-attribute face :background))))
3952 (magit-diff--make-hunk-overlay (oref section content)
3953 (magit-diff-hunk-region-beginning)
3954 'font-lock-face face
3955 'priority 2)
3956 (magit-diff--make-hunk-overlay (1+ (magit-diff-hunk-region-end))
3957 (oref section end)
3958 'font-lock-face face
3959 'priority 2)))
3960
3961 (defun magit-diff-highlight-hunk-region-using-face (_section)
3962 "Highlight the hunk-internal region by making it bold.
3963 Or rather highlight using the face `magit-diff-hunk-region', though
3964 changing only the `:weight' and/or `:slant' is recommended for that
3965 face."
3966 (magit-diff--make-hunk-overlay (magit-diff-hunk-region-beginning)
3967 (1+ (magit-diff-hunk-region-end))
3968 'font-lock-face 'magit-diff-hunk-region))
3969
3970 (defun magit-diff-highlight-hunk-region-using-overlays (section)
3971 "Emphasize the hunk-internal region using delimiting horizontal lines.
3972 This is implemented as single-pixel newlines places inside overlays."
3973 (if (window-system)
3974 (let ((beg (magit-diff-hunk-region-beginning))
3975 (end (magit-diff-hunk-region-end))
3976 (str (propertize
3977 (concat (propertize "\s" 'display '(space :height (1)))
3978 (propertize "\n" 'line-height t))
3979 'font-lock-face 'magit-diff-lines-boundary)))
3980 (magit-diff--make-hunk-overlay beg (1+ beg) 'before-string str)
3981 (magit-diff--make-hunk-overlay end (1+ end) 'after-string str))
3982 (magit-diff-highlight-hunk-region-using-face section)))
3983
3984 (defun magit-diff-highlight-hunk-region-using-underline (section)
3985 "Emphasize the hunk-internal region using delimiting horizontal lines.
3986 This is implemented by overlining and underlining the first and
3987 last (visual) lines of the region."
3988 (if (window-system)
3989 (let* ((beg (magit-diff-hunk-region-beginning))
3990 (end (magit-diff-hunk-region-end))
3991 (beg-eol (save-excursion (goto-char beg)
3992 (end-of-visual-line)
3993 (point)))
3994 (end-bol (save-excursion (goto-char end)
3995 (beginning-of-visual-line)
3996 (point)))
3997 (color (face-background 'magit-diff-lines-boundary nil t)))
3998 (cl-flet ((ln (b e &rest face)
3999 (magit-diff--make-hunk-overlay
4000 b e 'font-lock-face face 'after-string
4001 (magit-diff--hunk-after-string face))))
4002 (if (= beg end-bol)
4003 (ln beg beg-eol :overline color :underline color)
4004 (ln beg beg-eol :overline color)
4005 (ln end-bol end :underline color))))
4006 (magit-diff-highlight-hunk-region-using-face section)))
4007
4008 (defun magit-diff--make-hunk-overlay (start end &rest args)
4009 (let ((ov (make-overlay start end nil t)))
4010 (overlay-put ov 'evaporate t)
4011 (while args (overlay-put ov (pop args) (pop args)))
4012 (push ov magit-section--region-overlays)
4013 ov))
4014
4015 (defun magit-diff--hunk-after-string (face)
4016 (propertize "\s"
4017 'font-lock-face face
4018 'display (list 'space :align-to
4019 `(+ (0 . right)
4020 ,(min (window-hscroll)
4021 (- (line-end-position)
4022 (line-beginning-position)))))
4023 ;; This prevents the cursor from being rendered at the
4024 ;; edge of the window.
4025 'cursor t))
4026
4027 ;;; Utilities
4028
4029 (defun magit-diff-inside-hunk-body-p ()
4030 "Return t if point is inside the body of a hunk."
4031 (and-let* ((section (magit-current-section))
4032 (_(cl-typep section 'magit-hunk-section))
4033 (content (oref section content)))
4034 (> (magit-point) content)))
4035
4036 (defun magit-diff-on-removed-line-p ()
4037 "Return t if point is on a removed line inside the body of a hunk.
4038 If `magit-diff-visit-previous-blob' is nil, then always return nil."
4039 (let ((section (magit-current-section)))
4040 (and magit-diff-visit-previous-blob
4041 (cl-typep section 'magit-hunk-section)
4042 (not (oref section combined))
4043 (= (char-after (pos-bol)) ?-))))
4044
4045 (defun magit-diff--combined-p (section)
4046 (cl-assert (cl-typep section 'magit-file-section))
4047 (string-match-p "\\`diff --\\(combined\\|cc\\)" (oref section value)))
4048
4049 ;;; Diff Extract
4050
4051 (defun magit-diff-file-header (section &optional no-rename)
4052 (when (magit-hunk-section-p section)
4053 (setq section (oref section parent)))
4054 (and (magit-file-section-p section)
4055 (let ((header (oref section header)))
4056 (if (or (not no-rename)
4057 (string-match-p "^--- /dev/null" header))
4058 header
4059 (replace-regexp-in-string
4060 "^--- \\(.+\\)" (oref section value) header t t 1)))))
4061
4062 (defun magit-diff-hunk-region-header (section)
4063 (let ((patch (magit-diff-hunk-region-patch section)))
4064 (string-match "\n" patch)
4065 (substring patch 0 (1- (match-end 0)))))
4066
4067 (defun magit-diff-hunk-region-patch (section &optional args)
4068 (let ((op (if (member "--reverse" args) "+" "-"))
4069 (sbeg (oref section start))
4070 (rbeg (magit-diff-hunk-region-beginning))
4071 (rend (region-end))
4072 (send (oref section end))
4073 (patch nil))
4074 (save-excursion
4075 (goto-char sbeg)
4076 (while (< (point) send)
4077 (looking-at "\\(.\\)\\([^\n]*\n\\)")
4078 (cond ((or (string-match-p "[@ ]" (match-str 1))
4079 (and (>= (point) rbeg)
4080 (<= (point) rend)))
4081 (push (match-str 0) patch))
4082 ((equal op (match-str 1))
4083 (push (concat " " (match-str 2)) patch)))
4084 (forward-line)))
4085 (let ((buffer-list-update-hook nil)) ; #3759
4086 (with-temp-buffer
4087 (insert (string-join (reverse patch)))
4088 (diff-fixup-modifs (point-min) (point-max))
4089 (setq patch (buffer-string))))
4090 patch))
4091
4092 ;;; _
4093 (provide 'magit-diff)
4094 ;; Local Variables:
4095 ;; read-symbol-shorthands: (
4096 ;; ("and$" . "cond-let--and$")
4097 ;; ("thread$" . "cond-let--thread$")
4098 ;; ("when$" . "cond-let--when$")
4099 ;; ("and-let*" . "cond-let--and-let*")
4100 ;; ("and-let" . "cond-let--and-let")
4101 ;; ("if-let*" . "cond-let--if-let*")
4102 ;; ("if-let" . "cond-let--if-let")
4103 ;; ("when-let*" . "cond-let--when-let*")
4104 ;; ("when-let" . "cond-let--when-let")
4105 ;; ("while-let*" . "cond-let--while-let*")
4106 ;; ("while-let" . "cond-let--while-let")
4107 ;; ("match-string" . "match-string")
4108 ;; ("match-str" . "match-string-no-properties"))
4109 ;; End:
4110 ;;; magit-diff.el ends here