mirror of https://github.com/Chizi123/.emacs.d.git

Chizi123
2018-11-17 c4001ccd1864293b64aa37d83a9d9457eb875e70
commit | author | age
5cb5f7 1 ;;; magit-diff.el --- inspect Git diffs  -*- lexical-binding: t -*-
C 2
3 ;; Copyright (C) 2010-2018  The Magit Project Contributors
4 ;;
5 ;; You should have received a copy of the AUTHORS.md file which
6 ;; lists all contributors.  If not, see http://magit.vc/authors.
7
8 ;; Author: Jonas Bernoulli <jonas@bernoul.li>
9 ;; Maintainer: Jonas Bernoulli <jonas@bernoul.li>
10
11 ;; Magit is free software; you can redistribute it and/or modify it
12 ;; under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation; either version 3, or (at your option)
14 ;; any later version.
15 ;;
16 ;; Magit is distributed in the hope that it will be useful, but WITHOUT
17 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
18 ;; or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
19 ;; License for more details.
20 ;;
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with Magit.  If not, see http://www.gnu.org/licenses.
23
24 ;;; Commentary:
25
26 ;; This library implements support for looking at Git diffs and
27 ;; commits.
28
29 ;;; Code:
30
31 (eval-when-compile
32   (require 'subr-x))
33
34 (require 'git-commit)
35 (require 'magit-core)
36
37 ;; For `magit-diff-popup'
38 (declare-function magit-stash-show "magit-stash" (stash &optional args files))
39 ;; For `magit-diff-visit-file'
40 (declare-function dired-jump "dired-x" (&optional other-window file-name))
41 (declare-function magit-find-file-noselect "magit-files" (rev file))
42 (declare-function magit-status-internal "magit-status" (directory))
43 ;; For `magit-diff-while-committing'
44 (declare-function magit-commit-message-buffer "magit-commit" ())
45 ;; For `magit-insert-revision-gravatar'
46 (defvar gravatar-size)
47 ;; For `magit-show-commit' and `magit-diff-show-or-scroll'
48 (declare-function magit-current-blame-chunk "magit-blame" ())
49 (eval-when-compile
50   (cl-pushnew 'orig-rev eieio--known-slot-names))
51 (declare-function magit-blame-mode "magit-blame" (&optional arg))
52 (defvar magit-blame-mode)
53 (defvar git-rebase-line)
54 ;; For `magit-diff-unmerged'
55 (declare-function magit-merge-in-progress-p "magit-merge" ())
56 (declare-function magit--merge-range "magit-merge" (&optional head))
57
58 (require 'diff-mode)
59 (require 'smerge-mode)
60
61 (defvar bookmark-make-record-function)
62
63 ;;; Options
64 ;;;; Diff Mode
65
66 (defgroup magit-diff nil
67   "Inspect and manipulate Git diffs."
68   :link '(info-link "(magit)Diffing")
69   :group 'magit-modes)
70
71 (defcustom magit-diff-mode-hook nil
72   "Hook run after entering Magit-Diff mode."
73   :group 'magit-diff
74   :type 'hook)
75
76 (defcustom magit-diff-arguments '("--stat" "--no-ext-diff")
77   "The diff arguments used in buffers whose mode derives from `magit-diff-mode'."
78   :group 'magit-git-arguments
79   :group 'magit-diff
80   :type '(repeat (string :tag "Argument")))
81
82 (defcustom magit-diff-sections-hook
83   '(magit-insert-diff
84     magit-insert-xref-buttons)
85   "Hook run to insert sections into a `magit-diff-mode' buffer."
86   :package-version '(magit . "2.3.0")
87   :group 'magit-diff
88   :type 'hook)
89
90 (defcustom magit-diff-expansion-threshold 60
91   "After how many seconds not to expand anymore diffs.
92
93 Except in status buffers, diffs are usually start out fully
94 expanded.  Because that can take a long time, all diffs that
95 haven't been fontified during a refresh before the threshold
96 defined here are instead displayed with their bodies collapsed.
97
98 Note that this can cause sections that were previously expanded
99 to be collapsed.  So you should not pick a very low value here.
100
101 The hook function `magit-diff-expansion-threshold' has to be a
102 member of `magit-section-set-visibility-hook' for this option
103 to have any effect."
104   :package-version '(magit . "2.9.0")
105   :group 'magit-diff
106   :type 'float)
107
108 (defcustom magit-diff-highlight-hunk-body t
109   "Whether to highlight bodies of selected hunk sections.
110 This only has an effect if `magit-diff-highlight' is a
111 member of `magit-section-highlight-hook', which see."
112   :package-version '(magit . "2.1.0")
113   :group 'magit-diff
114   :type 'boolean)
115
116 (defcustom magit-diff-highlight-hunk-region-functions
117   '(magit-diff-highlight-hunk-region-dim-outside
118     magit-diff-highlight-hunk-region-using-overlays)
119   "The functions used to highlight the hunk-internal region.
120
121 `magit-diff-highlight-hunk-region-dim-outside' overlays the outside
122 of the hunk internal selection with a face that causes the added and
123 removed lines to have the same background color as context lines.
124 This function should not be removed from the value of this option.
125
126 `magit-diff-highlight-hunk-region-using-overlays' and
127 `magit-diff-highlight-hunk-region-using-underline' emphasize the
128 region by placing delimiting horizonal lines before and after it.
129 The underline variant was implemented because Eli said that is
130 how we should do it.  However the overlay variant actually works
131 better.  Also see https://github.com/magit/magit/issues/2758.
132
133 Instead of, or in addition to, using delimiting horizontal lines,
134 to emphasize the boundaries, you may which to emphasize the text
135 itself, using `magit-diff-highlight-hunk-region-using-face'.
136
137 In terminal frames it's not possible to draw lines as the overlay
138 and underline variants normally do, so there they fall back to
139 calling the face function instead."
140   :package-version '(magit . "2.9.0")
141   :set-after '(magit-diff-show-lines-boundaries)
142   :group 'magit-diff
143   :type 'hook
144   :options '(magit-diff-highlight-hunk-region-dim-outside
145              magit-diff-highlight-hunk-region-using-underline
146              magit-diff-highlight-hunk-region-using-overlays
147              magit-diff-highlight-hunk-region-using-face))
148
149 (defcustom magit-diff-unmarked-lines-keep-foreground t
150   "Whether `magit-diff-highlight-hunk-region-dim-outside' preserves foreground.
151 When this is set to nil, then that function only adjusts the
152 foreground color but added and removed lines outside the region
153 keep their distinct foreground colors."
154   :package-version '(magit . "2.9.0")
155   :group 'magit-diff
156   :type 'boolean)
157
158 (defcustom magit-diff-refine-hunk nil
159   "Whether to show word-granularity differences within diff hunks.
160
161 nil    never show fine differences.
162 t      show fine differences for the current diff hunk only.
163 `all'  show fine differences for all displayed diff hunks."
164   :group 'magit-diff
165   :safe (lambda (val) (memq val '(nil t all)))
166   :type '(choice (const :tag "Never" nil)
167                  (const :tag "Current" t)
168                  (const :tag "All" all)))
169
170 (put 'magit-diff-refine-hunk 'permanent-local t)
171
172 (defcustom magit-diff-adjust-tab-width nil
173   "Whether to adjust the width of tabs in diffs.
174
175 Determining the correct width can be expensive if it requires
176 opening large and/or many files, so the widths are cached in
177 the variable `magit-diff--tab-width-cache'.  Set that to nil
178 to invalidate the cache.
179
180 nil       Never ajust tab width.  Use `tab-width's value from
181           the Magit buffer itself instead.
182
183 t         If the corresponding file-visiting buffer exits, then
184           use `tab-width's value from that buffer.  Doing this is
185           cheap, so this value is used even if a corresponding
186           cache entry exists.
187
188 `always'  If there is no such buffer, then temporarily visit the
189           file to determine the value.
190
191 NUMBER    Like `always', but don't visit files larger than NUMBER
192           bytes."
193   :package-version '(magit . "2.12.0")
194   :group 'magit-diff
195   :type '(choice (const :tag "Never" nil)
196                  (const :tag "If file-visiting buffer exists" t)
197                  (const :tag "... or file isn't larger than bytes" all)
198                  (const :tag "Always" always)))
199
200 (defcustom magit-diff-paint-whitespace t
201   "Specify where to highlight whitespace errors.
202 See `magit-diff-highlight-trailing',
203 `magit-diff-highlight-indentation'.  The symbol t means in all
204 diffs, `status' means only in the status buffer, and nil means
205 nowhere."
206   :group 'magit-diff
207   :safe (lambda (val) (memq val '(t nil status)))
208   :type '(choice (const :tag "Always" t)
209                  (const :tag "Never" nil)
210                  (const :tag "In status buffer" status)))
211
212 (defcustom magit-diff-highlight-trailing t
213   "Whether to highlight whitespace at the end of a line in diffs.
214 Used only when `magit-diff-paint-whitespace' is non-nil."
215   :group 'magit-diff
216   :safe 'booleanp
217   :type 'boolean)
218
219 (defcustom magit-diff-highlight-indentation nil
220   "Highlight the \"wrong\" indentation style.
221 Used only when `magit-diff-paint-whitespace' is non-nil.
222
223 The value is a list of cons cells.  The car is a regular
224 expression, and the cdr is the value that applies to repositories
225 whose directory matches the regular expression.  If more than one
226 element matches, then the *last* element in the list applies.
227 The default value should therefore come first in the list.
228
229 If the value is `tabs', highlight indentation with tabs.  If the
230 value is an integer, highlight indentation with at least that
231 many spaces.  Otherwise, highlight neither."
232   :group 'magit-diff
233   :type `(repeat (cons (string :tag "Directory regexp")
234                        (choice (const :tag "Tabs" tabs)
235                                (integer :tag "Spaces" :value ,tab-width)
236                                (const :tag "Neither" nil)))))
237
238 (defcustom magit-diff-hide-trailing-cr-characters
239   (and (memq system-type '(ms-dos windows-nt)) t)
240   "Whether to hide ^M characters at the end of a line in diffs."
241   :package-version '(magit . "2.6.0")
242   :group 'magit-diff
243   :type 'boolean)
244
245 (defcustom magit-diff-visit-previous-blob t
246   "Whether `magit-diff-visit-file' may visit the previous blob.
247
248 When this is t and point is on a removed line in a diff for a
249 committed change, then `magit-diff-visit-file' visits the blob
250 from the last revision which still had that line.
251
252 Currently this is only supported for committed changes, for
253 staged and unstaged changes `magit-diff-visit-file' always
254 visits the file in the working tree."
255   :package-version '(magit . "2.9.0")
256   :group 'magit-diff
257   :type 'boolean)
258
259 (defcustom magit-diff-highlight-keywords t
260   "Whether to highlight bracketed keywords in commit messages."
261   :package-version '(magit . "2.12.0")
262   :group 'magit-diff
263   :type 'boolean)
264
265 ;;;; File Diff
266
267 (defcustom magit-diff-buffer-file-locked t
268   "Whether `magit-diff-buffer-file' uses a dedicated buffer."
269   :package-version '(magit . "2.7.0")
270   :group 'magit-commands
271   :group 'magit-diff
272   :type 'boolean)
273
274 ;;;; Revision Mode
275
276 (defgroup magit-revision nil
277   "Inspect and manipulate Git commits."
278   :link '(info-link "(magit)Revision Buffer")
279   :group 'magit-modes)
280
281 (defcustom magit-revision-mode-hook '(bug-reference-mode)
282   "Hook run after entering Magit-Revision mode."
283   :group 'magit-revision
284   :type 'hook
285   :options '(bug-reference-mode))
286
287 (defcustom magit-revision-sections-hook
288   '(magit-insert-revision-tag
289     magit-insert-revision-headers
290     magit-insert-revision-message
291     magit-insert-revision-notes
292     magit-insert-revision-diff
293     magit-insert-xref-buttons)
294   "Hook run to insert sections into a `magit-revision-mode' buffer."
295   :package-version '(magit . "2.3.0")
296   :group 'magit-revision
297   :type 'hook)
298
299 (defcustom magit-revision-headers-format "\
300 Author:     %aN <%aE>
301 AuthorDate: %ad
302 Commit:     %cN <%cE>
303 CommitDate: %cd
304 "
305   "Format string used to insert headers in revision buffers.
306
307 All headers in revision buffers are inserted by the section
308 inserter `magit-insert-revision-headers'.  Some of the headers
309 are created by calling `git show --format=FORMAT' where FORMAT
310 is the format specified here.  Other headers are hard coded or
311 subject to option `magit-revision-insert-related-refs'."
312   :package-version '(magit . "2.3.0")
313   :group 'magit-revision
314   :type 'string)
315
316 (defcustom magit-revision-insert-related-refs t
317   "Whether to show related branches in revision buffers
318
319 `nil'   Don't show any related branches.
320 `t'     Show related local branches.
321 `all'   Show related local and remote branches.
322 `mixed' Show all containing branches and local merged branches."
323   :package-version '(magit . "2.1.0")
324   :group 'magit-revision
325   :type '(choice (const :tag "don't" nil)
326                  (const :tag "local only" t)
327                  (const :tag "all related" all)
328                  (const :tag "all containing, local merged" mixed)))
329
330 (defcustom magit-revision-use-hash-sections 'quicker
331   "Whether to turn hashes inside the commit message into sections.
332
333 If non-nil, then hashes inside the commit message are turned into
334 `commit' sections.  There is a trade off to be made between
335 performance and reliability:
336
337 - `slow' calls git for every word to be absolutely sure.
338 - `quick' skips words less than seven characters long.
339 - `quicker' additionally skips words that don't contain a number.
340 - `quickest' uses all words that are at least seven characters
341   long and which contain at least one number as well as at least
342   one letter.
343
344 If nil, then no hashes are turned into sections, but you can
345 still visit the commit at point using \"RET\"."
346   :package-version '(magit . "2.12.0")
347   :group 'magit-revision
348   :type '(choice (const :tag "Use sections, quickest" quickest)
349                  (const :tag "Use sections, quicker" quicker)
350                  (const :tag "Use sections, quick" quick)
351                  (const :tag "Use sections, slow" slow)
352                  (const :tag "Don't use sections" nil)))
353
354 (defcustom magit-revision-show-gravatars nil
355   "Whether to show gravatar images in revision buffers.
356
357 If nil, then don't insert any gravatar images.  If t, then insert
358 both images.  If `author' or `committer', then insert only the
359 respective image.
360
361 If you have customized the option `magit-revision-header-format'
362 and want to insert the images then you might also have to specify
363 where to do so.  In that case the value has to be a cons-cell of
364 two regular expressions.  The car specifies where to insert the
365 author's image.  The top half of the image is inserted right
366 after the matched text, the bottom half on the next line in the
367 same column.  The cdr specifies where to insert the committer's
368 image, accordingly.  Either the car or the cdr may be nil."
369   :package-version '(magit . "2.3.0")
370   :group 'magit-revision
371   :type '(choice (const :tag "Don't show gravatars" nil)
372                  (const :tag "Show gravatars" t)
373                  (const :tag "Show author gravatar" author)
374                  (const :tag "Show committer gravatar" committer)
375                  (cons  :tag "Show gravatars using custom pattern."
376                         (regexp :tag "Author regexp"    "^Author:     ")
377                         (regexp :tag "Committer regexp" "^Commit:     "))))
378
379 (defcustom magit-revision-use-gravatar-kludge nil
380   "Whether to work around a bug which affects display of gravatars.
381
382 Gravatar images are spliced into two halves which are then
383 displayed on separate lines.  On OS X the splicing has a bug in
384 some Emacs builds, which causes the top and bottom halves to be
385 interchanged.  Enabling this option works around this issue by
386 interchanging the halves once more, which cancels out the effect
387 of the bug.
388
389 See https://github.com/magit/magit/issues/2265
390 and https://debbugs.gnu.org/cgi/bugreport.cgi?bug=7847.
391
392 Starting with Emacs 26.1 this kludge should not be required for
393 any build."
394   :package-version '(magit . "2.3.0")
395   :group 'magit-revision
396   :type 'boolean)
397
398 (defcustom magit-revision-fill-summary-line nil
399   "Whether to fill excessively long summary lines.
400
401 If this is an integer, then the summary line is filled if it is
402 longer than either the limit specified here or `window-width'.
403
404 You may want to only set this locally in \".dir-locals-2.el\" for
405 repositories known to contain bad commit messages.
406
407 The body of the message is left alone because (a) most people who
408 write excessively long summary lines usually don't add a body and
409 (b) even people who have the decency to wrap their lines may have
410 a good reason to include a long line in the body sometimes."
411   :package-version '(magit . "2.90.0")
412   :group 'magit-revision
413   :type '(choice (const   :tag "Don't fill" nil)
414                  (integer :tag "Fill if longer than")))
415
416 ;;;; Diff Sections
417
418 (defcustom magit-diff-section-arguments '("--no-ext-diff")
419   "The diff arguments used in buffers that show other things besides diffs."
420   :group 'magit-git-arguments
421   :group 'magit-diff
422   :group 'magit-status
423   :type '(repeat (string :tag "Argument")))
424
425 (put 'magit-diff-section-arguments 'permanent-local t)
426
427 ;;; Faces
428
429 (defface magit-diff-file-heading
430   '((t :weight bold))
431   "Face for diff file headings."
432   :group 'magit-faces)
433
434 (defface magit-diff-file-heading-highlight
435   '((t :inherit (magit-section-highlight)))
436   "Face for current diff file headings."
437   :group 'magit-faces)
438
439 (defface magit-diff-file-heading-selection
440   '((((class color) (background light))
441      :inherit magit-diff-file-heading-highlight
442      :foreground "salmon4")
443     (((class color) (background dark))
444      :inherit magit-diff-file-heading-highlight
445      :foreground "LightSalmon3"))
446   "Face for selected diff file headings."
447   :group 'magit-faces)
448
449 (defface magit-diff-hunk-heading
450   '((((class color) (background light))
451      :background "grey80"
452      :foreground "grey30")
453     (((class color) (background dark))
454      :background "grey25"
455      :foreground "grey70"))
456   "Face for diff hunk headings."
457   :group 'magit-faces)
458
459 (defface magit-diff-hunk-heading-highlight
460   '((((class color) (background light))
461      :background "grey75"
462      :foreground "grey30")
463     (((class color) (background dark))
464      :background "grey35"
465      :foreground "grey70"))
466   "Face for current diff hunk headings."
467   :group 'magit-faces)
468
469 (defface magit-diff-hunk-heading-selection
470   '((((class color) (background light))
471      :inherit magit-diff-hunk-heading-highlight
472      :foreground "salmon4")
473     (((class color) (background dark))
474      :inherit magit-diff-hunk-heading-highlight
475      :foreground "LightSalmon3"))
476   "Face for selected diff hunk headings."
477   :group 'magit-faces)
478
479 (defface magit-diff-hunk-region
480   '((t :inherit bold))
481   "Face used by `magit-diff-highlight-hunk-region-using-face'.
482
483 This face is overlayed over text that uses other hunk faces,
484 and those normally set the foreground and background colors.
485 The `:foreground' and especially the `:background' properties
486 should be avoided here.  Setting the latter would cause the
487 lose of information.  Good properties to set here are `:weight'
488 and `:slant'."
489   :group 'magit-faces)
490
491 (defface magit-diff-lines-heading
492   '((((class color) (background light))
493      :inherit magit-diff-hunk-heading-highlight
494      :background "LightSalmon3")
495     (((class color) (background dark))
496      :inherit magit-diff-hunk-heading-highlight
497      :foreground "grey80"
498      :background "salmon4"))
499   "Face for diff hunk heading when lines are marked."
500   :group 'magit-faces)
501
502 (defface magit-diff-lines-boundary
503   '((t :inherit magit-diff-lines-heading))
504   "Face for boundary of marked lines in diff hunk."
505   :group 'magit-faces)
506
507 (defface magit-diff-conflict-heading
508   '((t :inherit magit-diff-hunk-heading))
509   "Face for conflict markers."
510   :group 'magit-faces)
511
512 (defface magit-diff-added
513   '((((class color) (background light))
514      :background "#ddffdd"
515      :foreground "#22aa22")
516     (((class color) (background dark))
517      :background "#335533"
518      :foreground "#ddffdd"))
519   "Face for lines in a diff that have been added."
520   :group 'magit-faces)
521
522 (defface magit-diff-removed
523  '((((class color) (background light))
524     :background "#ffdddd"
525     :foreground "#aa2222")
526    (((class color) (background dark))
527     :background "#553333"
528     :foreground "#ffdddd"))
529   "Face for lines in a diff that have been removed."
530   :group 'magit-faces)
531
532 (defface magit-diff-our
533   '((t :inherit magit-diff-removed))
534   "Face for lines in a diff for our side in a conflict."
535   :group 'magit-faces)
536
537 (defface magit-diff-base
538   '((((class color) (background light))
539      :background "#ffffcc"
540      :foreground "#aaaa11")
541     (((class color) (background dark))
542      :background "#555522"
543      :foreground "#ffffcc"))
544   "Face for lines in a diff for the base side in a conflict."
545   :group 'magit-faces)
546
547 (defface magit-diff-their
548   '((t :inherit magit-diff-added))
549   "Face for lines in a diff for their side in a conflict."
550   :group 'magit-faces)
551
552 (defface magit-diff-context
553   '((((class color) (background light)) :foreground "grey50")
554     (((class color) (background  dark)) :foreground "grey70"))
555   "Face for lines in a diff that are unchanged."
556   :group 'magit-faces)
557
558 (defface magit-diff-added-highlight
559   '((((class color) (background light))
560      :background "#cceecc"
561      :foreground "#22aa22")
562     (((class color) (background dark))
563      :background "#336633"
564      :foreground "#cceecc"))
565   "Face for lines in a diff that have been added."
566   :group 'magit-faces)
567
568 (defface magit-diff-removed-highlight
569   '((((class color) (background light))
570      :background "#eecccc"
571      :foreground "#aa2222")
572     (((class color) (background dark))
573      :background "#663333"
574      :foreground "#eecccc"))
575   "Face for lines in a diff that have been removed."
576   :group 'magit-faces)
577
578 (defface magit-diff-our-highlight
579   '((t :inherit magit-diff-removed-highlight))
580   "Face for lines in a diff for our side in a conflict."
581   :group 'magit-faces)
582
583 (defface magit-diff-base-highlight
584   '((((class color) (background light))
585      :background "#eeeebb"
586      :foreground "#aaaa11")
587     (((class color) (background dark))
588      :background "#666622"
589      :foreground "#eeeebb"))
590   "Face for lines in a diff for the base side in a conflict."
591   :group 'magit-faces)
592
593 (defface magit-diff-their-highlight
594   '((t :inherit magit-diff-added-highlight))
595   "Face for lines in a diff for their side in a conflict."
596   :group 'magit-faces)
597
598 (defface magit-diff-context-highlight
599   '((((class color) (background light))
600      :background "grey95"
601      :foreground "grey50")
602     (((class color) (background dark))
603      :background "grey20"
604      :foreground "grey70"))
605   "Face for lines in the current context in a diff."
606   :group 'magit-faces)
607
608 (defface magit-diff-whitespace-warning
609   '((t :inherit trailing-whitespace))
610   "Face for highlighting whitespace errors added lines."
611   :group 'magit-faces)
612
613 (defface magit-diffstat-added
614   '((((class color) (background light)) :foreground "#22aa22")
615     (((class color) (background  dark)) :foreground "#448844"))
616   "Face for plus sign in diffstat."
617   :group 'magit-faces)
618
619 (defface magit-diffstat-removed
620   '((((class color) (background light)) :foreground "#aa2222")
621     (((class color) (background  dark)) :foreground "#aa4444"))
622   "Face for minus sign in diffstat."
623   :group 'magit-faces)
624
625 ;;; Commands
626 ;;;; Diff popups
627
628 (defconst magit-diff-popup-common-keywords
629   '(:variable magit-diff-arguments
630               :man-page "git-diff"))
631
632 (defconst magit-diff-popup-common-options
633   '((?f "Limit to files" "-- " magit-read-files)
634     (?u "Context lines"  "-U")
635     (?m "Detect renames" "-M")
636     (?c "Detect copies"  "-C")
637     (?a "Diff algorithm" "--diff-algorithm=" magit-diff-select-algorithm)
638     (?i "Ignore submodules" "--ignore-submodules="
639         magit-diff-select-ignore-submodules)))
640
641 (defconst magit-diff-popup-common-switches
642   '((?f "Show surrounding functions"     "--function-context")
643     (?b "Ignore whitespace changes"      "--ignore-space-change")
644     (?w "Ignore all whitespace"          "--ignore-all-space")
645     (?x "Disallow external diff drivers" "--no-ext-diff")))
646
647 (defvar magit-diff-popup
648   `(,@magit-diff-popup-common-keywords
649     :options  ,magit-diff-popup-common-options
650     :switches (,@magit-diff-popup-common-switches
651                (?s "Show stats" "--stat"))
652     :actions  ((?d "Dwim"          magit-diff-dwim)
653                (?u "Diff unstaged" magit-diff-unstaged)
654                (?c "Show commit"   magit-show-commit)
655                (?r "Diff range"    magit-diff-range)
656                (?s "Diff staged"   magit-diff-staged)
657                (?t "Show stash"    magit-stash-show)
658                (?p "Diff paths"    magit-diff-paths)
659                (?w "Diff worktree" magit-diff-working-tree))
660     :default-action magit-diff-dwim
661     :max-action-columns 3))
662
663 (defvar magit-diff-refresh-popup
664   `(,@magit-diff-popup-common-keywords
665     :options  ,magit-diff-popup-common-options
666     :switches ,magit-diff-popup-common-switches
667     :actions  ((?g "Refresh"                magit-diff-refresh)
668                (?t "Toggle hunk refinement" magit-diff-toggle-refine-hunk)
669                (?s "Set defaults"           magit-diff-set-default-arguments)
670                (?F "Toggle file filter"     magit-diff-toggle-file-filter)
671                (?w "Save defaults"          magit-diff-save-default-arguments))
672     :max-action-columns 2))
673
674 (defvar magit-diff-mode-refresh-popup
675   `(,@magit-diff-popup-common-keywords
676     :options  ,magit-diff-popup-common-options
677     :switches (,@magit-diff-popup-common-switches
678                (?s "Show stats" "--stat"))
679     :actions  ((?g "Refresh"                magit-diff-refresh)
680                (?t "Toggle hunk refinement" magit-diff-toggle-refine-hunk)
681                (?s "Set defaults"           magit-diff-set-default-arguments)
682                (?r "Switch range type"      magit-diff-switch-range-type)
683                (?w "Save defaults"          magit-diff-save-default-arguments)
684                (?f "Flip revisions"         magit-diff-flip-revs) nil
685                (?F "Toggle file filter"     magit-diff-toggle-file-filter))
686     :max-action-columns 2))
687
688 (defvar magit-revision-mode-refresh-popup
689   `(,@magit-diff-popup-common-keywords
690     :options  ,magit-diff-popup-common-options
691     :switches (,@magit-diff-popup-common-switches
692                (?s "Show stats" "--stat"))
693     :actions  ((?g "Refresh"                magit-diff-refresh)
694                (?t "Toggle hunk refinement" magit-diff-toggle-refine-hunk)
695                (?s "Set defaults"           magit-diff-set-default-arguments)
696                (?F "Toggle file filter"     magit-diff-toggle-file-filter)
697                (?w "Save defaults"          magit-diff-save-default-arguments))
698     :max-action-columns 2))
699
700 (magit-define-popup-keys-deferred 'magit-diff-popup)
701 (magit-define-popup-keys-deferred 'magit-diff-refresh-popup)
702 (magit-define-popup-keys-deferred 'magit-diff-mode-refresh-popup)
703 (magit-define-popup-keys-deferred 'magit-revision-mode-refresh-popup)
704
705 (defvar magit-diff-section-file-args nil)
706 (put 'magit-diff-section-file-args 'permanent-local t)
707 (put 'magit-diff-section-file-args 'safe-local-variable
708      (lambda (val)
709        (and (listp val)
710             (-all-p #'stringp val))))
711
712 (defun magit-diff-get-buffer-args ()
713   (cond ((and magit-use-sticky-arguments
714               (derived-mode-p 'magit-diff-mode))
715          (list (nth 2 magit-refresh-args)
716                (nth 3 magit-refresh-args)))
717         ((and (eq magit-use-sticky-arguments t)
718               (--when-let (magit-mode-get-buffer 'magit-diff-mode)
719                 (with-current-buffer it
720                   (list (nth 2 magit-refresh-args)
721                         (nth 3 magit-refresh-args))))))
722         (t
723          (list (default-value 'magit-diff-arguments) nil))))
724
725 (defun magit-diff-arguments (&optional refresh)
726   (cond ((memq magit-current-popup '(magit-diff-popup magit-diff-refresh-popup))
727          (magit-popup-export-file-args magit-current-popup-args))
728         ((and refresh (not (derived-mode-p 'magit-diff-mode)))
729          (list magit-diff-section-arguments
730                magit-diff-section-file-args))
731         (t
732          (magit-diff-get-buffer-args))))
733
734 ;;;###autoload
735 (defun magit-diff-popup (arg)
736   "Popup console for diff commands."
737   (interactive "P")
738   (let ((magit-diff-arguments
739          ;; We cannot possibly know what suffix command the user is
740          ;; about to invoke, so we also don't know from which buffer
741          ;; we should get the current values.  However it is much
742          ;; more likely that we will end up updating the diff buffer,
743          ;; and we therefore use the value from that buffer.
744          (apply #'magit-popup-import-file-args (magit-diff-get-buffer-args))))
745     (magit-invoke-popup 'magit-diff-popup nil arg)))
746
747 ;;;###autoload
748 (defun magit-diff-buffer-file-popup ()
749   "Popup console for diff commands.
750
751 This is a variant of `magit-diff-popup' which shows the same popup
752 but which limits the diff to the file being visited in the current
753 buffer."
754   (interactive)
755   (if-let ((file (magit-file-relative-name)))
756       (let ((magit-diff-arguments
757              (magit-popup-import-file-args
758               (if-let ((buffer (magit-mode-get-buffer 'magit-diff-mode)))
759                   (with-current-buffer buffer
760                     (nth 3 magit-refresh-args))
761                 (default-value 'magit-diff-arguments))
762               (list file))))
763         (magit-invoke-popup 'magit-diff-popup nil nil))
764     (user-error "Buffer isn't visiting a file")))
765
766 (defun magit-diff-refresh-popup (arg)
767   "Popup console for changing diff arguments in the current buffer."
768   (interactive "P")
769   (let ((magit-diff-refresh-popup
770          (pcase major-mode
771            (`magit-revision-mode magit-revision-mode-refresh-popup)
772            (`magit-diff-mode     magit-diff-mode-refresh-popup)
773            (_                    magit-diff-refresh-popup)))
774         (magit-diff-arguments
775          (if (derived-mode-p 'magit-diff-mode)
776              (magit-popup-import-file-args (nth 2 magit-refresh-args)
777                                            (nth 3 magit-refresh-args))
778            (magit-popup-import-file-args magit-diff-section-arguments
779                                          magit-diff-section-file-args))))
780     (magit-invoke-popup 'magit-diff-refresh-popup nil arg)))
781
782 (defun magit-diff-select-algorithm (&rest _ignore)
783   (magit-read-char-case nil t
784     (?d "[d]efault"   "default")
785     (?m "[m]inimal"   "minimal")
786     (?p "[p]atience"  "patience")
787     (?h "[h]istogram" "histogram")))
788
789 (defun magit-diff-select-ignore-submodules (&rest _ignored)
790   (magit-read-char-case "Ignore submodules " t
791     (?u "[u]ntracked" "untracked")
792     (?d "[d]irty"     "dirty")
793     (?a "[a]ll"       "all")))
794
795 ;;;; Diff commands
796
797 ;;;###autoload
798 (defun magit-diff-dwim (&optional args files)
799   "Show changes for the thing at point."
800   (interactive (magit-diff-arguments))
801   (pcase (magit-diff--dwim)
802     (`unmerged (magit-diff-unmerged args files))
803     (`unstaged (magit-diff-unstaged args files))
804     (`staged
805      (let ((file (magit-file-at-point)))
806        (if (and file (equal (cddr (car (magit-file-status file))) '(?D ?U)))
807            ;; File was deleted by us and modified by them.  Show the latter.
808            (magit-diff-unmerged args (list file))
809          (magit-diff-staged nil args files))))
810     (`(commit . ,value)
811      (magit-diff-range (format "%s^..%s" value value) args files))
812     (`(stash  . ,value) (magit-stash-show value args))
813     ((and range (pred stringp))
814      (magit-diff-range range args files))
815     (_
816      (call-interactively #'magit-diff-range))))
817
818 (defun magit-diff--dwim ()
819   "Return information for performing DWIM diff.
820
821 The information can be in three forms:
822 1. TYPE
823    A symbol describing a type of diff where no additional information
824    is needed to generate the diff.  Currently, this includes `staged',
825    `unstaged' and `unmerged'.
826 2. (TYPE . VALUE)
827    Like #1 but the diff requires additional information, which is
828    given by VALUE.  Currently, this includes `commit' and `stash',
829    where VALUE is the given commit or stash, respectively.
830 3. RANGE
831    A string indicating a diff range.
832
833 If no DWIM context is found, nil is returned."
834   (cond
835    ((--when-let (magit-region-values '(commit branch) t)
836       (deactivate-mark)
837       (concat (car (last it)) ".." (car it))))
838    (magit-buffer-refname
839     (cons 'commit magit-buffer-refname))
840    ((derived-mode-p 'magit-stash-mode)
841     (cons 'commit
842           (magit-section-case
843             (commit (oref it value))
844             (file (-> it
845                       (oref parent)
846                       (oref value)))
847             (hunk (-> it
848                       (oref parent)
849                       (oref parent)
850                       (oref value))))))
851    ((derived-mode-p 'magit-revision-mode)
852     (cons 'commit (car magit-refresh-args)))
853    ((derived-mode-p 'magit-diff-mode)
854     (nth 0 magit-refresh-args))
855    (t
856     (magit-section-case
857       ([* unstaged] 'unstaged)
858       ([* staged] 'staged)
859       (unmerged 'unmerged)
860       (unpushed (oref it value))
861       (unpulled (oref it value))
862       (branch (let ((current (magit-get-current-branch))
863                     (atpoint (oref it value)))
864                 (if (equal atpoint current)
865                     (--if-let (magit-get-upstream-branch)
866                         (format "%s...%s" it current)
867                       (if (magit-anything-modified-p)
868                           current
869                         (cons 'commit current)))
870                   (format "%s...%s"
871                           (or current "HEAD")
872                           atpoint))))
873       (commit (cons 'commit (oref it value)))
874       (stash (cons 'stash (oref it value)))))))
875
876 (defun magit-diff-read-range-or-commit (prompt &optional secondary-default mbase)
877   "Read range or revision with special diff range treatment.
878 If MBASE is non-nil, prompt for which rev to place at the end of
879 a \"revA...revB\" range.  Otherwise, always construct
880 \"revA..revB\" range."
881   (--if-let (magit-region-values '(commit branch) t)
882       (let ((revA (car (last it)))
883             (revB (car it)))
884         (deactivate-mark)
885         (if mbase
886             (let ((base (magit-git-string "merge-base" revA revB)))
887               (cond
888                ((string= (magit-rev-parse revA) base)
889                 (format "%s..%s" revA revB))
890                ((string= (magit-rev-parse revB) base)
891                 (format "%s..%s" revB revA))
892                (t
893                 (let ((main (magit-completing-read "View changes along"
894                                                    (list revA revB)
895                                                    nil t nil nil revB)))
896                   (format "%s...%s"
897                           (if (string= main revB) revA revB) main)))))
898           (format "%s..%s" revA revB)))
899     (magit-read-range prompt
900                       (or (pcase (magit-diff--dwim)
901                             (`(commit . ,value)
902                              (format "%s^..%s" value value))
903                             ((and range (pred stringp))
904                              range))
905                           secondary-default
906                           (magit-get-current-branch)))))
907
908 (defun magit-diff-setup (rev-or-range const args files)
909   (require 'magit)
910   (magit-mode-setup #'magit-diff-mode rev-or-range const args files))
911
912 ;;;###autoload
913 (defun magit-diff-range (rev-or-range &optional args files)
914   "Show differences between two commits.
915
916 REV-OR-RANGE should be a range or a single revision.  If it is a
917 revision, then show changes in the working tree relative to that
918 revision.  If it is a range, but one side is omitted, then show
919 changes relative to `HEAD'.
920
921 If the region is active, use the revisions on the first and last
922 line of the region as the two sides of the range.  With a prefix
923 argument, instead of diffing the revisions, choose a revision to
924 view changes along, starting at the common ancestor of both
925 revisions (i.e., use a \"...\" range)."
926   (interactive (cons (magit-diff-read-range-or-commit "Diff for range"
927                                                       nil current-prefix-arg)
928                      (magit-diff-arguments)))
929   (magit-diff-setup rev-or-range nil args files))
930
931 ;;;###autoload
932 (defun magit-diff-working-tree (&optional rev args files)
933   "Show changes between the current working tree and the `HEAD' commit.
934 With a prefix argument show changes between the working tree and
935 a commit read from the minibuffer."
936   (interactive
937    (cons (and current-prefix-arg
938               (magit-read-branch-or-commit "Diff working tree and commit"))
939          (magit-diff-arguments)))
940   (magit-diff-setup (or rev "HEAD") nil args files))
941
942 ;;;###autoload
943 (defun magit-diff-staged (&optional rev args files)
944   "Show changes between the index and the `HEAD' commit.
945 With a prefix argument show changes between the index and
946 a commit read from the minibuffer."
947   (interactive
948    (cons (and current-prefix-arg
949               (magit-read-branch-or-commit "Diff index and commit"))
950          (magit-diff-arguments)))
951   (magit-diff-setup rev (list "--cached") args files))
952
953 ;;;###autoload
954 (defun magit-diff-unstaged (&optional args files)
955   "Show changes between the working tree and the index."
956   (interactive (magit-diff-arguments))
957   (magit-diff-setup nil nil args files))
958
959 ;;;###autoload
960 (defun magit-diff-unmerged (&optional args files)
961   "Show changes that are being merged."
962   (interactive (magit-diff-arguments))
963   (unless (magit-merge-in-progress-p)
964     (user-error "No merge is in progress"))
965   (magit-diff-setup (magit--merge-range) nil args files))
966
967 ;;;###autoload
968 (defun magit-diff-while-committing (&optional args)
969   "While committing, show the changes that are about to be committed.
970 While amending, invoking the command again toggles between
971 showing just the new changes or all the changes that will
972 be committed."
973   (interactive (list (car (magit-diff-arguments))))
974   (unless (magit-commit-message-buffer)
975     (user-error "No commit in progress"))
976   (let ((magit-display-buffer-noselect t)
977         (diff-buf (magit-mode-get-buffer 'magit-diff-mode)))
978     (if (and diff-buf
979              (get-buffer-window diff-buf))
980         (with-current-buffer diff-buf
981           (pcase-let ((`(,rev ,arg . ,_) magit-refresh-args))
982             (cond ((and (equal rev "HEAD^")
983                         (equal arg '("--cached")))
984                    (magit-diff-staged nil args))
985                   ((and (equal rev nil)
986                         (equal arg '("--cached")))
987                    (magit-diff-while-amending args))
988                   ((magit-anything-staged-p)
989                    (magit-diff-staged nil args))
990                   (t
991                    (magit-diff-while-amending args)))))
992       (if (magit-anything-staged-p)
993           (magit-diff-staged nil args)
994         (magit-diff-while-amending args)))))
995
996 (define-key git-commit-mode-map
997   (kbd "C-c C-d") 'magit-diff-while-committing)
998
999 (defun magit-diff-while-amending (&optional args)
1000   (magit-diff-setup "HEAD^" (list "--cached") args nil))
1001
1002 ;;;###autoload
1003 (defun magit-diff-buffer-file ()
1004   "Show diff for the blob or file visited in the current buffer."
1005   (interactive)
1006   (require 'magit)
1007   (if-let ((file (magit-file-relative-name)))
1008       (magit-mode-setup-internal #'magit-diff-mode
1009                                  (list (or magit-buffer-refname
1010                                            (magit-get-current-branch)
1011                                            "HEAD")
1012                                        nil
1013                                        (cadr (magit-diff-arguments))
1014                                        (list file))
1015                                  magit-diff-buffer-file-locked)
1016     (user-error "Buffer isn't visiting a file")))
1017
1018 ;;;###autoload
1019 (defun magit-diff-paths (a b)
1020   "Show changes between any two files on disk."
1021   (interactive (list (read-file-name "First file: " nil nil t)
1022                      (read-file-name "Second file: " nil nil t)))
1023   (magit-diff-setup nil (list "--no-index")
1024                     nil (list (magit-convert-filename-for-git
1025                                (expand-file-name a))
1026                               (magit-convert-filename-for-git
1027                                (expand-file-name b)))))
1028
1029 (defvar-local magit-buffer-revision-hash nil)
1030
1031 (defun magit-show-commit--arguments ()
1032   (pcase-let ((`(,args ,diff-files) (magit-diff-arguments)))
1033     (list args (if (derived-mode-p 'magit-log-mode)
1034                    (and (not (member "--follow" (nth 1 magit-refresh-args)))
1035                         (nth 2 magit-refresh-args))
1036                  diff-files))))
1037
1038 ;;;###autoload
1039 (defun magit-show-commit (rev &optional args files module)
1040   "Visit the revision at point in another buffer.
1041 If there is no revision at point or with a prefix argument prompt
1042 for a revision."
1043   (interactive
1044    (pcase-let* ((mcommit (magit-section-value-if 'module-commit))
1045                 (atpoint (or (and (bound-and-true-p magit-blame-mode)
1046                                   (oref (magit-current-blame-chunk) orig-rev))
1047                              mcommit
1048                              (magit-branch-or-commit-at-point)))
1049                 (`(,args ,files) (magit-show-commit--arguments)))
1050      (list (or (and (not current-prefix-arg) atpoint)
1051                (magit-read-branch-or-commit "Show commit" atpoint))
1052            args
1053            files
1054            (and mcommit
1055                 (magit-section-parent-value (magit-current-section))))))
1056   (require 'magit)
1057   (magit-with-toplevel
1058     (when module
1059       (setq default-directory
1060             (expand-file-name (file-name-as-directory module))))
1061     (unless (magit-rev-verify-commit rev)
1062       (user-error "%s is not a commit" rev))
1063     (magit-mode-setup #'magit-revision-mode rev nil args files)))
1064
1065 ;;;; Setting commands
1066
1067 (defun magit-diff-refresh (args files)
1068   "Set the local diff arguments for the current buffer."
1069   (interactive (magit-diff-arguments t))
1070   (cond ((derived-mode-p 'magit-diff-mode)
1071          (setcdr (cdr magit-refresh-args) (list args files)))
1072         (t
1073          (setq-local magit-diff-section-arguments args)
1074          (setq-local magit-diff-section-file-args files)))
1075   (magit-refresh))
1076
1077 (defun magit-diff-set-default-arguments (args files)
1078   "Set the global diff arguments for the current buffer."
1079   (interactive (magit-diff-arguments t))
1080   (cond ((derived-mode-p 'magit-diff-mode)
1081          (customize-set-variable 'magit-diff-arguments args)
1082          (setcdr (cdr magit-refresh-args) (list args files)))
1083         (t
1084          (customize-set-variable 'magit-diff-section-arguments args)
1085          (kill-local-variable 'magit-diff-section-arguments)
1086          (kill-local-variable 'magit-diff-section-file-args)))
1087   (magit-refresh))
1088
1089 (defun magit-diff-save-default-arguments (args files)
1090   "Set and save the global diff arguments for the current buffer."
1091   (interactive (magit-diff-arguments t))
1092   (cond ((derived-mode-p 'magit-diff-mode)
1093          (customize-save-variable 'magit-diff-arguments args)
1094          (setcdr (cdr magit-refresh-args) (list args files)))
1095         (t
1096          (customize-save-variable 'magit-diff-section-arguments args)
1097          (kill-local-variable 'magit-diff-section-arguments)
1098          (kill-local-variable 'magit-diff-section-file-args)))
1099   (magit-refresh))
1100
1101 (defun magit-diff-switch-range-type ()
1102   "Convert diff range type.
1103 Change \"revA..revB\" to \"revB...revA\", or vice versa."
1104   (interactive)
1105   (let ((range (car magit-refresh-args)))
1106     (if (and range
1107              (derived-mode-p 'magit-diff-mode)
1108              (string-match magit-range-re range))
1109         (progn
1110           (setcar magit-refresh-args
1111                   (concat (match-string 1 range)
1112                           (if (string= (match-string 2 range) "..")
1113                               "..."
1114                             "..")
1115                           (match-string 3 range)))
1116           (magit-refresh))
1117       (user-error "No range to change"))))
1118
1119 (defun magit-diff-flip-revs ()
1120   "Swap revisions in diff range.
1121 Change \"revA..revB\" to \"revB..revA\"."
1122   (interactive)
1123   (let ((range (car magit-refresh-args)))
1124     (if (and range
1125              (derived-mode-p 'magit-diff-mode)
1126              (string-match magit-range-re range))
1127         (progn
1128           (setcar magit-refresh-args
1129                   (concat (match-string 3 range)
1130                           (match-string 2 range)
1131                           (match-string 1 range)))
1132           (magit-refresh))
1133       (user-error "No range to swap"))))
1134
1135 (defvar-local magit-diff--last-file-args nil)
1136 (defun magit-diff--toggle-file-args (files)
1137   (cond (files
1138          (setq magit-diff--last-file-args files)
1139                nil)
1140         (magit-diff--last-file-args)
1141         (t
1142          (user-error "No diff file filter to toggle"))))
1143
1144 (defun magit-diff-toggle-file-filter ()
1145   "Toggle the file restriction of the current buffer's diffs.
1146 If the current buffer's mode is derived from `magit-log-mode',
1147 toggle the file restriction in the repository's revision buffer
1148 instead."
1149   (interactive)
1150   (--if-let (and (derived-mode-p 'magit-log-mode)
1151                  (magit-mode-get-buffer 'magit-revision-mode))
1152       (with-current-buffer it
1153         (setf (nth 3 magit-refresh-args)
1154               (magit-diff--toggle-file-args (nth 3 magit-refresh-args)))
1155         (magit-refresh))
1156     (if (derived-mode-p 'magit-diff-mode)
1157         (setf (nth 3 magit-refresh-args)
1158               (magit-diff--toggle-file-args (nth 3 magit-refresh-args)))
1159       (setq-local magit-diff-section-file-args
1160                   (magit-diff--toggle-file-args magit-diff-section-file-args)))
1161     (magit-refresh)))
1162
1163 (defun magit-diff-less-context (&optional count)
1164   "Decrease the context for diff hunks by COUNT lines."
1165   (interactive "p")
1166   (magit-diff-set-context `(lambda (cur) (max 0 (- (or cur 0) ,count)))))
1167
1168 (defun magit-diff-more-context (&optional count)
1169   "Increase the context for diff hunks by COUNT lines."
1170   (interactive "p")
1171   (magit-diff-set-context `(lambda (cur) (+ (or cur 0) ,count))))
1172
1173 (defun magit-diff-default-context ()
1174   "Reset context for diff hunks to the default height."
1175   (interactive)
1176   (magit-diff-set-context #'ignore))
1177
1178 (defun magit-diff-set-context (fn)
1179   (let* ((def (--if-let (magit-get "diff.context") (string-to-number it) 3))
1180          (val (car (magit-diff-arguments t)))
1181          (arg (--first (string-match "^-U\\([0-9]+\\)?$" it) val))
1182          (num (--if-let (and arg (match-string 1 arg)) (string-to-number it) def))
1183          (val (delete arg val))
1184          (num (funcall fn num))
1185          (arg (and num (not (= num def)) (format "-U%i" num)))
1186          (val (if arg (cons arg val) val)))
1187     (if (derived-mode-p 'magit-diff-mode)
1188         (setcar (cddr magit-refresh-args) val)
1189       (setq magit-diff-section-arguments val)))
1190   (magit-refresh))
1191
1192 (defun magit-diff-context-p ()
1193   (--if-let (--first (string-match "^-U\\([0-9]+\\)$" it)
1194                      (car (magit-diff-arguments t)))
1195       (not (equal "-U0" it))
1196     t))
1197
1198 (defun magit-diff-ignore-any-space-p ()
1199   (let ((args (car (magit-diff-arguments t))))
1200     (--any-p (member it args)
1201              '("--ignore-cr-at-eol"
1202                "--ignore-space-at-eol"
1203                "--ignore-space-change" "-b"
1204                "--ignore-all-space" "-w"
1205                "--ignore-blank-space"))))
1206
1207 (defun magit-diff-toggle-refine-hunk (&optional style)
1208   "Turn diff-hunk refining on or off.
1209
1210 If hunk refining is currently on, then hunk refining is turned off.
1211 If hunk refining is off, then hunk refining is turned on, in
1212 `selected' mode (only the currently selected hunk is refined).
1213
1214 With a prefix argument, the \"third choice\" is used instead:
1215 If hunk refining is currently on, then refining is kept on, but
1216 the refining mode (`selected' or `all') is switched.
1217 If hunk refining is off, then hunk refining is turned on, in
1218 `all' mode (all hunks refined).
1219
1220 Customize variable `magit-diff-refine-hunk' to change the default mode."
1221   (interactive "P")
1222   (setq-local magit-diff-refine-hunk
1223               (if style
1224                   (if (eq magit-diff-refine-hunk 'all) t 'all)
1225                 (not magit-diff-refine-hunk)))
1226   (magit-diff-update-hunk-refinement))
1227
1228 ;;;; Visit commands
1229
1230 (defun magit-diff-visit-file
1231     (file &optional other-window force-worktree display-fn)
1232   "From a diff, visit the corresponding file at the appropriate position.
1233
1234 If the diff shows changes in the worktree, the index, or `HEAD',
1235 then visit the actual file.  Otherwise, when the diff is about an
1236 older commit or a range, then visit the appropriate blob.
1237
1238 If point is on a removed line, then visit the blob for the first
1239 parent of the commit which removed that line, i.e. the last
1240 commit where that line still existed.  Otherwise visit the blob
1241 for the commit whose changes are being shown.
1242
1243 Interactively, when the file or blob to be displayed is already
1244 being displayed in another window of the same frame, then just
1245 select that window and adjust point.  Otherwise, or with a prefix
1246 argument, display the buffer in another window.  The meaning of
1247 the prefix argument can be inverted or further modified using the
1248 option `magit-display-file-buffer-function'.
1249
1250 Non-interactively the optional OTHER-WINDOW argument is taken
1251 literally.  DISPLAY-FN can be used to specify the display
1252 function explicitly, in which case OTHER-WINDOW is ignored.
1253
1254 The optional FORCE-WORKTREE means to force visiting the worktree
1255 version of the file.  To do this interactively use the command
1256 `magit-diff-visit-file-worktree' instead."
1257   (interactive (list (--if-let (magit-file-at-point)
1258                          (expand-file-name it)
1259                        (user-error "No file at point"))
1260                      current-prefix-arg))
1261   (if (magit-file-accessible-directory-p file)
1262       (magit-diff-visit-directory file other-window)
1263     (let* ((hunk (magit-diff-visit--hunk))
1264            (last (and magit-diff-visit-previous-blob
1265                       (not force-worktree)
1266                       (magit-section-match 'hunk)
1267                       (save-excursion
1268                         (goto-char (line-beginning-position))
1269                         (looking-at "-"))))
1270            (line (and hunk (magit-diff-hunk-line   hunk)))
1271            (col  (and hunk (magit-diff-hunk-column hunk last)))
1272            (rev  (if last
1273                      (magit-diff-visit--range-beginning)
1274                    (magit-diff-visit--range-end)))
1275            (buf  (if (and (not force-worktree)
1276                           (stringp rev))
1277                      (magit-find-file-noselect rev file)
1278                    (or (get-file-buffer file)
1279                        (find-file-noselect file)))))
1280       (cond ((called-interactively-p 'any)
1281              (magit-display-file-buffer buf))
1282             (display-fn
1283              (funcall display-fn buf))
1284             ((or other-window (get-buffer-window buf))
1285              (switch-to-buffer-other-window buf))
1286             (t
1287              (pop-to-buffer buf)))
1288       (with-selected-window
1289           (or (get-buffer-window buf 'visible)
1290               (error "File buffer is not visible"))
1291         (when line
1292           (setq line
1293                 (cond ((eq rev 'staged)
1294                        (apply 'magit-diff-visit--offset file nil line))
1295                       ((and force-worktree
1296                             (stringp rev))
1297                        (apply 'magit-diff-visit--offset file rev line))
1298                       (t
1299                        (apply '+ line))))
1300           (let ((pos (save-restriction
1301                        (widen)
1302                        (goto-char (point-min))
1303                        (forward-line (1- line))
1304                        (move-to-column col)
1305                        (point))))
1306             (unless (<= (point-min) pos (point-max))
1307               (widen)
1308               (goto-char pos))))
1309         (when (magit-anything-unmerged-p file)
1310           (smerge-start-session))
1311         (run-hooks 'magit-diff-visit-file-hook)))))
1312
1313 (defun magit-diff-visit-file-other-window (file)
1314   "From a diff, visit the corresponding file at the appropriate position.
1315 The file is shown in another window.
1316
1317 If the diff shows changes in the worktree, the index, or `HEAD',
1318 then visit the actual file.  Otherwise, when the diff is about an
1319 older commit or a range, then visit the appropriate blob.
1320
1321 If point is on a removed line, then visit the blob for the first
1322 parent of the commit which removed that line, i.e. the last
1323 commit where that line still existed.  Otherwise visit the blob
1324 for the commit whose changes are being shown."
1325   (interactive (list (--if-let (magit-file-at-point)
1326                          (expand-file-name it)
1327                        (user-error "No file at point"))))
1328   (magit-diff-visit-file file t))
1329
1330 (defvar magit-display-file-buffer-function
1331   'magit-display-file-buffer-traditional
1332   "The function used by `magit-diff-visit-file' to display blob buffers.
1333
1334 Other commands such as `magit-find-file' do not use this
1335 function.  Instead they use high-level functions to select the
1336 window to be used to display the buffer.  This variable and the
1337 related functions are an experimental feature and should be
1338 treated as such.")
1339
1340 (defun magit-display-file-buffer (buffer)
1341   (funcall magit-display-file-buffer-function buffer))
1342
1343 (defun magit-display-file-buffer-traditional (buffer)
1344   "Display BUFFER in the current window.
1345 With a prefix argument display it in another window.
1346 Option `magit-display-file-buffer-function' controls
1347 whether `magit-diff-visit-file' uses this function."
1348   (if (or current-prefix-arg (get-buffer-window buffer))
1349       (pop-to-buffer buffer)
1350     (switch-to-buffer buffer)))
1351
1352 (defun magit-display-file-buffer-other-window (buffer)
1353   "Display BUFFER in another window.
1354 With a prefix argument display it in the current window.
1355 Option `magit-display-file-buffer-function' controls
1356 whether `magit-diff-visit-file' uses this function."
1357   (if current-prefix-arg
1358       (switch-to-buffer buffer)
1359     (pop-to-buffer buffer)))
1360
1361 (defun magit-diff-visit-file-worktree (file &optional other-window)
1362   "From a diff, visit the corresponding file at the appropriate position.
1363
1364 When the file is already being displayed in another window of the
1365 same frame, then just select that window and adjust point.  With
1366 a prefix argument also display in another window.
1367
1368 The actual file in the worktree is visited. The positions in the
1369 hunk headers get less useful the \"older\" the changes are, and
1370 as a result, jumping to the appropriate position gets less
1371 reliable.
1372
1373 Also see `magit-diff-visit-file' which visits the respective
1374 blob, unless the diff shows changes in the worktree, the index,
1375 or `HEAD'."
1376   (interactive (list (or (magit-file-at-point)
1377                          (user-error "No file at point"))
1378                      current-prefix-arg))
1379   (magit-diff-visit-file file other-window t))
1380
1381 (defun magit-diff-visit--range-end ()
1382   (let ((rev (magit-diff--dwim)))
1383     (if (symbolp rev)
1384         rev
1385       (setq rev (if (consp rev)
1386                     (cdr rev)
1387                   (cdr (magit-split-range rev))))
1388       (if (magit-rev-head-p rev)
1389           'unstaged
1390         rev))))
1391
1392 (defun magit-diff-visit--range-beginning ()
1393   (let ((rev (magit-diff--dwim)))
1394     (cond ((consp rev)
1395            (concat (cdr rev) "^"))
1396           ((stringp rev)
1397            (car (magit-split-range rev)))
1398           (t
1399            rev))))
1400
1401 (defun magit-diff-visit--hunk ()
1402   (when-let ((scope (magit-diff-scope)))
1403     (let ((section (magit-current-section)))
1404       (cl-case scope
1405         ((file files)
1406          (setq section (car (oref section children))))
1407         (list
1408          (setq section (car (oref section children)))
1409          (when section
1410            (setq section (car (oref section children))))))
1411       (and
1412        ;; Unmerged files appear in the list of staged changes
1413        ;; but unlike in the list of unstaged changes no diffs
1414        ;; are shown here.  In that case `section' is nil.
1415        section
1416        ;; Currently the `hunk' type is also abused for file
1417        ;; mode changes, which we are not interested in here.
1418        ;; Such sections have no value.
1419        (oref section value)
1420        section))))
1421
1422 (defun magit-diff-visit--offset (file rev hunk-start line-offset)
1423   (let ((offset 0))
1424     (with-temp-buffer
1425       (save-excursion
1426         (magit-with-toplevel
1427           (magit-git-insert "diff" rev "--" file)))
1428       (catch 'found
1429         (while (re-search-forward
1430                 "^@@ -\\([0-9]+\\),\\([0-9]+\\) \\+\\([0-9]+\\),\\([0-9]+\\) @@"
1431                 nil t)
1432           (let* ((abeg (string-to-number (match-string 1)))
1433                  (alen (string-to-number (match-string 2)))
1434                  (bbeg (string-to-number (match-string 3)))
1435                  (blen (string-to-number (match-string 4)))
1436                  (aend (+ abeg alen))
1437                  (bend (+ bbeg blen))
1438                  (hend (+ hunk-start line-offset)))
1439             (if (<= abeg hunk-start)
1440                 (if (or (>= aend hend)
1441                         (>= bend hend))
1442                     (let ((line 0))
1443                       (while (<= line alen)
1444                         (forward-line 1)
1445                         (cl-incf line)
1446                         (cond ((looking-at "^\\+") (cl-incf offset))
1447                               ((looking-at "^-")   (cl-decf offset)))))
1448                   (cl-incf offset (- blen alen)))
1449               (throw 'found nil))))))
1450     (+ hunk-start line-offset offset)))
1451
1452 (defun magit-diff-hunk-line (section)
1453   (let* ((value  (oref section value))
1454          (prefix (- (length value) 2))
1455          (cpos   (marker-position (oref section content)))
1456          (stop   (line-number-at-pos))
1457          (cstart (save-excursion (goto-char cpos)
1458                                  (line-number-at-pos)))
1459          (prior  (and (= (length value) 3)
1460                       (save-excursion (goto-char (line-beginning-position))
1461                                       (looking-at "-"))))
1462          (offset 0)
1463          (line   (if prior
1464                      (cadr value)
1465                    (car (last value)))))
1466     (string-match (format "^%s\\([0-9]+\\)" (if prior "-" "\\+")) line)
1467     (setq line (string-to-number (match-string 1 line)))
1468     (when (> cstart stop)
1469       (save-excursion
1470         (goto-char cpos)
1471         (re-search-forward "^[-+]")
1472         (setq stop (line-number-at-pos))))
1473     (save-excursion
1474       (goto-char cpos)
1475       (while (< (line-number-at-pos) stop)
1476         (unless (string-match-p
1477                  (if prior "\\+" "-")
1478                  (buffer-substring (point) (+ (point) prefix)))
1479           (cl-incf offset))
1480         (forward-line)))
1481     (list line offset)))
1482
1483 (defun magit-diff-hunk-column (section visit-beginning)
1484   (if (or (< (point)
1485              (oref section content))
1486           (and (not visit-beginning)
1487                (save-excursion (beginning-of-line) (looking-at-p "-"))))
1488       0
1489     (max 0 (- (+ (current-column) 2)
1490               (length (oref section value))))))
1491
1492 (defun magit-diff-visit-directory (directory &optional other-window)
1493   (if (equal (magit-toplevel directory)
1494              (magit-toplevel))
1495       (dired-jump other-window (concat directory "/."))
1496     (let ((display-buffer-overriding-action
1497            (if other-window
1498                '(nil (inhibit-same-window t))
1499              '(display-buffer-same-window))))
1500       (magit-status-internal directory))))
1501
1502 ;;;; Scroll commands
1503
1504 (defun magit-diff-show-or-scroll-up ()
1505   "Update the commit or diff buffer for the thing at point.
1506
1507 Either show the commit or stash at point in the appropriate
1508 buffer, or if that buffer is already being displayed in the
1509 current frame and contains information about that commit or
1510 stash, then instead scroll the buffer up.  If there is no
1511 commit or stash at point, then prompt for a commit."
1512   (interactive)
1513   (magit-diff-show-or-scroll 'scroll-up))
1514
1515 (defun magit-diff-show-or-scroll-down ()
1516   "Update the commit or diff buffer for the thing at point.
1517
1518 Either show the commit or stash at point in the appropriate
1519 buffer, or if that buffer is already being displayed in the
1520 current frame and contains information about that commit or
1521 stash, then instead scroll the buffer down.  If there is no
1522 commit or stash at point, then prompt for a commit."
1523   (interactive)
1524   (magit-diff-show-or-scroll 'scroll-down))
1525
1526 (defun magit-diff-show-or-scroll (fn)
1527   (let (rev cmd buf win)
1528     (cond
1529      (magit-blame-mode
1530       (setq rev (oref (magit-current-blame-chunk) orig-rev))
1531       (setq cmd 'magit-show-commit)
1532       (setq buf (magit-mode-get-buffer 'magit-revision-mode)))
1533      ((derived-mode-p 'git-rebase-mode)
1534       (save-excursion
1535         (goto-char (line-beginning-position))
1536         (--if-let (and git-rebase-line
1537                        (looking-at git-rebase-line)
1538                        (match-string 2))
1539             (progn (setq rev it)
1540                    (setq cmd 'magit-show-commit)
1541                    (setq buf (magit-mode-get-buffer 'magit-revision-mode)))
1542           (user-error "No commit on this line"))))
1543      (t
1544       (magit-section-case
1545         (branch
1546          (setq rev (magit-ref-maybe-qualify (oref it value)))
1547          (setq cmd 'magit-show-commit)
1548          (setq buf (magit-mode-get-buffer 'magit-revision-mode)))
1549         (commit
1550          (setq rev (oref it value))
1551          (setq cmd 'magit-show-commit)
1552          (setq buf (magit-mode-get-buffer 'magit-revision-mode)))
1553         (stash
1554          (setq rev (oref it value))
1555          (setq cmd 'magit-stash-show)
1556          (setq buf (magit-mode-get-buffer 'magit-stash-mode))))))
1557     (if rev
1558         (if (and buf
1559                  (setq win (get-buffer-window buf))
1560                  (with-current-buffer buf
1561                    (and (equal rev (car magit-refresh-args))
1562                         (equal (magit-rev-parse rev)
1563                                magit-buffer-revision-hash))))
1564             (with-selected-window win
1565               (condition-case nil
1566                   (funcall fn)
1567                 (error
1568                  (goto-char (pcase fn
1569                               (`scroll-up   (point-min))
1570                               (`scroll-down (point-max)))))))
1571           (let ((magit-display-buffer-noselect t))
1572             (if (eq cmd 'magit-show-commit)
1573                 (apply #'magit-show-commit rev (magit-show-commit--arguments))
1574               (funcall cmd rev))))
1575       (call-interactively #'magit-show-commit))))
1576
1577 ;;; Diff Mode
1578
1579 (defvar magit-diff-mode-map
1580   (let ((map (make-sparse-keymap)))
1581     (set-keymap-parent map magit-mode-map)
1582     (define-key map "\C-c\C-d" 'magit-diff-while-committing)
1583     (define-key map "\C-c\C-b" 'magit-go-backward)
1584     (define-key map "\C-c\C-f" 'magit-go-forward)
1585     (define-key map "\s" 'scroll-up)
1586     (define-key map "\d" 'scroll-down)
1587     (define-key map "j" 'magit-jump-to-diffstat-or-diff)
1588     (define-key map [remap write-file] 'magit-patch-save)
1589     map)
1590   "Keymap for `magit-diff-mode'.")
1591
1592 (define-derived-mode magit-diff-mode magit-mode "Magit Diff"
1593   "Mode for looking at a Git diff.
1594
1595 This mode is documented in info node `(magit)Diff Buffer'.
1596
1597 \\<magit-mode-map>\
1598 Type \\[magit-refresh] to refresh the current buffer.
1599 Type \\[magit-section-toggle] to expand or hide the section at point.
1600 Type \\[magit-visit-thing] to visit the hunk or file at point.
1601
1602 Staging and applying changes is documented in info node
1603 `(magit)Staging and Unstaging' and info node `(magit)Applying'.
1604
1605 \\<magit-hunk-section-map>Type \
1606 \\[magit-apply] to apply the change at point, \
1607 \\[magit-stage] to stage,
1608 \\[magit-unstage] to unstage, \
1609 \\[magit-discard] to discard, or \
1610 \\[magit-reverse] to reverse it.
1611
1612 \\{magit-diff-mode-map}"
1613   :group 'magit-diff
1614   (hack-dir-local-variables-non-file-buffer)
1615   (setq imenu-prev-index-position-function
1616         'magit-imenu--diff-prev-index-position-function)
1617   (setq imenu-extract-index-name-function
1618         'magit-imenu--diff-extract-index-name-function)
1619   (setq-local bookmark-make-record-function
1620               'magit-bookmark--diff-make-record))
1621
1622 (defun magit-diff-refresh-buffer (rev-or-range const _args files)
1623   "Refresh the current `magit-diff-mode' buffer.
1624
1625 In such buffers the buffer-local value of `magit-refresh-args'
1626 has the same form as the arguments of this function.  The value
1627 is set in `magit-mode-setup'."
1628   (magit-set-header-line-format
1629    (if (member "--no-index" const)
1630        (apply #'format "Differences between %s and %s" files)
1631      (concat (if rev-or-range
1632                  (if (string-match-p "\\(\\.\\.\\|\\^-\\)"
1633                                      rev-or-range)
1634                      (format "Changes in %s" rev-or-range)
1635                    (format "Changes from %s to working tree" rev-or-range))
1636                (if (member "--cached" const)
1637                    "Staged changes"
1638                  "Unstaged changes"))
1639              (pcase (length files)
1640                (0)
1641                (1 (concat " in file " (car files)))
1642                (_ (concat " in files "
1643                           (mapconcat #'identity files ", ")))))))
1644   (magit-insert-section (diffbuf)
1645     (magit-run-section-hook 'magit-diff-sections-hook rev-or-range)))
1646
1647 (defun magit-insert-diff (rev-or-range)
1648   "Insert the diff into this `magit-diff-mode' buffer."
1649   (let ((magit-git-global-arguments
1650          (remove "--literal-pathspecs" magit-git-global-arguments)))
1651     (magit-git-wash #'magit-diff-wash-diffs
1652       "diff" rev-or-range "-p" "--no-prefix"
1653       (and (member "--stat" (nth 2 magit-refresh-args)) "--numstat")
1654       (nth 1 magit-refresh-args)
1655       (nth 2 magit-refresh-args) "--"
1656       (nth 3 magit-refresh-args))))
1657
1658 (defvar magit-file-section-map
1659   (let ((map (make-sparse-keymap)))
1660     (unless (featurep 'jkl)
1661       (define-key map (kbd "C-j") 'magit-diff-visit-file-worktree))
1662     (define-key map [C-return] 'magit-diff-visit-file-worktree)
1663     (define-key map [remap magit-visit-thing]      'magit-diff-visit-file)
1664     (define-key map [remap magit-delete-thing]     'magit-discard)
1665     (define-key map [remap magit-revert-no-commit] 'magit-reverse)
1666     (define-key map "a" 'magit-apply)
1667     (define-key map "C" 'magit-commit-add-log)
1668     (define-key map "s" 'magit-stage)
1669     (define-key map "u" 'magit-unstage)
1670     (define-key map "&" 'magit-do-async-shell-command)
1671     (define-key map "\C-c\C-t" 'magit-diff-trace-definition)
1672     (define-key map "\C-c\C-e" 'magit-diff-edit-hunk-commit)
1673     map)
1674   "Keymap for `file' sections.")
1675
1676 (defvar magit-hunk-section-map
1677   (let ((map (make-sparse-keymap)))
1678     (unless (featurep 'jkl)
1679       (define-key map (kbd "C-j") 'magit-diff-visit-file-worktree))
1680     (define-key map [C-return] 'magit-diff-visit-file-worktree)
1681     (define-key map [remap magit-visit-thing]      'magit-diff-visit-file)
1682     (define-key map [remap magit-delete-thing]     'magit-discard)
1683     (define-key map [remap magit-revert-no-commit] 'magit-reverse)
1684     (define-key map "a" 'magit-apply)
1685     (define-key map "C" 'magit-commit-add-log)
1686     (define-key map "s" 'magit-stage)
1687     (define-key map "u" 'magit-unstage)
1688     (define-key map "&" 'magit-do-async-shell-command)
1689     (define-key map "\C-c\C-t" 'magit-diff-trace-definition)
1690     (define-key map "\C-c\C-e" 'magit-diff-edit-hunk-commit)
1691     map)
1692   "Keymap for `hunk' sections.")
1693
1694 (defconst magit-diff-headline-re
1695   (concat "^\\(@@@?\\|diff\\|Submodule\\|"
1696           "\\* Unmerged path\\|merged\\|changed in both\\|"
1697           "added in remote\\|removed in remote\\)"))
1698
1699 (defconst magit-diff-statline-re
1700   (concat "^ ?"
1701           "\\(.*\\)"     ; file
1702           "\\( +| +\\)"  ; separator
1703           "\\([0-9]+\\|Bin\\(?: +[0-9]+ -> [0-9]+ bytes\\)?$\\) ?"
1704           "\\(\\+*\\)"   ; add
1705           "\\(-*\\)$"))  ; del
1706
1707 (defun magit-diff-wash-diffs (args &optional limit)
1708   (when (member "--stat" args)
1709     (magit-diff-wash-diffstat))
1710   (when (re-search-forward magit-diff-headline-re limit t)
1711     (goto-char (line-beginning-position))
1712     (magit-wash-sequence (apply-partially 'magit-diff-wash-diff args))
1713     (insert ?\n)))
1714
1715 (defun magit-jump-to-diffstat-or-diff ()
1716   "Jump to the diffstat or diff.
1717 When point is on a file inside the diffstat section, then jump
1718 to the respective diff section, otherwise jump to the diffstat
1719 section or a child thereof."
1720   (interactive)
1721   (--if-let (magit-get-section
1722              (append (magit-section-case
1723                        ([file diffstat] `((file . ,(oref it value))))
1724                        (file `((file . ,(oref it value)) (diffstat)))
1725                        (t '((diffstat))))
1726                      (magit-section-ident magit-root-section)))
1727       (magit-section-goto it)
1728     (user-error "No diffstat in this buffer")))
1729
1730 (defun magit-diff-wash-diffstat ()
1731   (let (heading (beg (point)))
1732     (when (re-search-forward "^ ?\\([0-9]+ +files? change[^\n]*\n\\)" nil t)
1733       (setq heading (match-string 1))
1734       (magit-delete-match)
1735       (goto-char beg)
1736       (magit-insert-section (diffstat)
1737         (insert (propertize heading 'face 'magit-diff-file-heading))
1738         (magit-insert-heading)
1739         (let (files)
1740           (while (looking-at "^[-0-9]+\t[-0-9]+\t\\(.+\\)$")
1741             (push (magit-decode-git-path
1742                    (let ((f (match-string 1)))
1743                      (if (string-match " => " f)
1744                          (substring f (match-end 0))
1745                        f)))
1746                   files)
1747             (magit-delete-line))
1748           (setq files (nreverse files))
1749           (while (looking-at magit-diff-statline-re)
1750             (magit-bind-match-strings (file sep cnt add del) nil
1751               (magit-delete-line)
1752               (when (string-match " +$" file)
1753                 (setq sep (concat (match-string 0 file) sep))
1754                 (setq file (substring file 0 (match-beginning 0))))
1755               (let ((le (length file)) ld)
1756                 (setq file (magit-decode-git-path file))
1757                 (setq ld (length file))
1758                 (when (> le ld)
1759                   (setq sep (concat (make-string (- le ld) ?\s) sep))))
1760               (magit-insert-section (file (pop files))
1761                 (insert (propertize file 'face 'magit-filename) sep cnt " ")
1762                 (when add
1763                   (insert (propertize add 'face 'magit-diffstat-added)))
1764                 (when del
1765                   (insert (propertize del 'face 'magit-diffstat-removed)))
1766                 (insert "\n")))))
1767         (if (looking-at "^$") (forward-line) (insert "\n"))))))
1768
1769 (defun magit-diff-wash-diff (args)
1770   (cond
1771    ((looking-at "^Submodule")
1772     (magit-diff-wash-submodule))
1773    ((looking-at "^\\* Unmerged path \\(.*\\)")
1774     (let ((file (magit-decode-git-path (match-string 1))))
1775       (magit-delete-line)
1776       (unless (and (derived-mode-p 'magit-status-mode)
1777                    (not (member "--cached" args)))
1778         (magit-insert-section (file file)
1779           (insert (propertize
1780                    (format "unmerged   %s%s" file
1781                            (pcase (cddr (car (magit-file-status file)))
1782                              (`(?D ?D) " (both deleted)")
1783                              (`(?D ?U) " (deleted by us)")
1784                              (`(?U ?D) " (deleted by them)")
1785                              (`(?A ?A) " (both added)")
1786                              (`(?A ?U) " (added by us)")
1787                              (`(?U ?A) " (added by them)")
1788                              (`(?U ?U) "")))
1789                    'face 'magit-diff-file-heading))
1790           (insert ?\n))))
1791     t)
1792    ((looking-at (concat "^\\(merged\\|changed in both\\|"
1793                         "added in remote\\|removed in remote\\)"))
1794     (let ((status (pcase (match-string 1)
1795                     ("merged" "merged")
1796                     ("changed in both" "conflict")
1797                     ("added in remote" "new file")
1798                     ("removed in remote" "deleted")))
1799           file orig base modes)
1800       (magit-delete-line)
1801       (while (looking-at
1802               "^  \\([^ ]+\\) +[0-9]\\{6\\} \\([a-z0-9]\\{40\\}\\) \\(.+\\)$")
1803         (magit-bind-match-strings (side _blob name) nil
1804           (pcase side
1805             ("result" (setq file name))
1806             ("our"    (setq orig name))
1807             ("their"  (setq file name))
1808             ("base"   (setq base name))))
1809         (magit-delete-line))
1810       (when orig (setq orig (magit-decode-git-path orig)))
1811       (when file (setq file (magit-decode-git-path file)))
1812       (magit-diff-insert-file-section (or file base) orig status modes nil)))
1813    ((looking-at
1814      "^diff --\\(?:\\(git\\) \\(?:\\(.+?\\) \\2\\)?\\|\\(cc\\|combined\\) \\(.+\\)\\)")
1815     (let ((status (cond ((equal (match-string 1) "git")        "modified")
1816                         ((derived-mode-p 'magit-revision-mode) "resolved")
1817                         (t                                     "unmerged")))
1818           (file (or (match-string 2) (match-string 4)))
1819           (beg (point))
1820           orig header modes)
1821       (save-excursion
1822         (forward-line 1)
1823         (setq header (buffer-substring
1824                       beg (if (re-search-forward magit-diff-headline-re nil t)
1825                               (match-beginning 0)
1826                             (point-max)))))
1827       (magit-delete-line)
1828       (while (not (or (eobp) (looking-at magit-diff-headline-re)))
1829         (if (looking-at "^old mode \\([^\n]+\\)\nnew mode \\([^\n]+\\)\n")
1830             (progn (setq modes (match-string 0))
1831                    (magit-delete-match))
1832           (cond
1833            ((looking-at "^--- \\([^/].*?\\)\t?$") ; i.e. not /dev/null
1834             (setq orig (match-string 1)))
1835            ((looking-at "^\\+\\+\\+ \\([^/].*?\\)\t?$")
1836             (setq file (match-string 1)))
1837            ((looking-at "^\\(copy\\|rename\\) from \\(.+\\)$")
1838             (setq orig (match-string 2)))
1839            ((looking-at "^\\(copy\\|rename\\) to \\(.+\\)$")
1840             (setq file (match-string 2))
1841             (setq status (if (equal (match-string 1) "copy") "new file" "renamed")))
1842            ((looking-at "^\\(new file\\|deleted\\)")
1843             (setq status (match-string 1))))
1844           (magit-delete-line)))
1845       (when orig
1846         (setq orig (magit-decode-git-path orig)))
1847       (setq file (magit-decode-git-path file))
1848       ;; KLUDGE `git-log' ignores `--no-prefix' when `-L' is used.
1849       (when (and (derived-mode-p 'magit-log-mode)
1850                  (--first (string-match-p "\\`-L" it)
1851                           (nth 1 magit-refresh-args)))
1852         (setq file (substring file 2))
1853         (when orig
1854           (setq orig (substring orig 2))))
1855       (magit-diff-insert-file-section file orig status modes header)))))
1856
1857 (defun magit-diff-insert-file-section (file orig status modes header)
1858   (magit-insert-section section
1859     (file file (or (equal status "deleted")
1860                    (derived-mode-p 'magit-status-mode)))
1861     (insert (propertize (format "%-10s %s\n" status
1862                                 (if (or (not orig) (equal orig file))
1863                                     file
1864                                   (format "%s -> %s" orig file)))
1865                         'face 'magit-diff-file-heading))
1866     (magit-insert-heading)
1867     (unless (equal orig file)
1868       (oset section source orig))
1869     (oset section header header)
1870     (when modes
1871       (magit-insert-section (hunk)
1872         (insert modes)))
1873     (magit-wash-sequence #'magit-diff-wash-hunk)))
1874
1875 (defun magit-diff-wash-submodule ()
1876   ;; See `show_submodule_summary' in submodule.c and "this" commit.
1877   (when (looking-at "^Submodule \\([^ ]+\\)")
1878     (let ((module (match-string 1))
1879           untracked modified)
1880       (when (looking-at "^Submodule [^ ]+ contains untracked content$")
1881         (magit-delete-line)
1882         (setq untracked t))
1883       (when (looking-at "^Submodule [^ ]+ contains modified content$")
1884         (magit-delete-line)
1885         (setq modified t))
1886       (cond
1887        ((and (looking-at "^Submodule \\([^ ]+\\) \\([^ :]+\\)\\( (rewind)\\)?:$")
1888              (equal (match-string 1) module))
1889         (magit-bind-match-strings (_module range rewind) nil
1890           (magit-delete-line)
1891           (while (looking-at "^  \\([<>]\\) \\(.+\\)$")
1892             (magit-delete-line))
1893           (when rewind
1894             (setq range (replace-regexp-in-string "[^.]\\(\\.\\.\\)[^.]"
1895                                                   "..." range t t 1)))
1896           (magit-insert-section (magit-module-section module t)
1897             (magit-insert-heading
1898               (propertize (concat "modified   " module)
1899                           'face 'magit-diff-file-heading)
1900               " ("
1901               (cond (rewind "rewind")
1902                     ((string-match-p "\\.\\.\\." range) "non-ff")
1903                     (t "new commits"))
1904               (and (or modified untracked)
1905                    (concat ", "
1906                            (and modified "modified")
1907                            (and modified untracked " and ")
1908                            (and untracked "untracked")
1909                            " content"))
1910               ")")
1911             (let ((default-directory
1912                     (file-name-as-directory
1913                      (expand-file-name module (magit-toplevel)))))
1914               (magit-git-wash (apply-partially 'magit-log-wash-log 'module)
1915                 "log" "--oneline" "--left-right" range)
1916               (delete-char -1)))))
1917        ((and (looking-at "^Submodule \\([^ ]+\\) \\([^ ]+\\) (\\([^)]+\\))$")
1918              (equal (match-string 1) module))
1919         (magit-bind-match-strings (_module _range msg) nil
1920           (magit-delete-line)
1921           (magit-insert-section (magit-module-section module)
1922             (magit-insert-heading
1923               (propertize (concat "submodule  " module)
1924                           'face 'magit-diff-file-heading)
1925               " (" msg ")"))))
1926        (t
1927         (magit-insert-section (magit-module-section module)
1928           (magit-insert-heading
1929             (propertize (concat "modified   " module)
1930                         'face 'magit-diff-file-heading)
1931             " ("
1932             (and modified "modified")
1933             (and modified untracked " and ")
1934             (and untracked "untracked")
1935             " content)")))))))
1936
1937 (defun magit-diff-wash-hunk ()
1938   (when (looking-at "^@\\{2,\\} \\(.+?\\) @\\{2,\\}\\(?: \\(.*\\)\\)?")
1939     (let ((heading (match-string 0))
1940           (value (cons (match-string 2) (split-string (match-string 1)))))
1941       (magit-delete-line)
1942       (magit-insert-section it (hunk value)
1943         (insert (propertize (concat heading "\n") 'face 'magit-diff-hunk-heading))
1944         (magit-insert-heading)
1945         (while (not (or (eobp) (looking-at "^[^-+\s\\]")))
1946           (forward-line))
1947         (oset it end (point))
1948         (oset it washer 'magit-diff-paint-hunk)))
1949     t))
1950
1951 (defun magit-diff-expansion-threshold (section)
1952   "Keep new diff sections collapsed if washing takes too long."
1953   (and (magit-file-section-p section)
1954        (> (float-time (time-subtract (current-time) magit-refresh-start-time))
1955           magit-diff-expansion-threshold)
1956        'hide))
1957
1958 ;;; Revision Mode
1959
1960 (define-derived-mode magit-revision-mode magit-diff-mode "Magit Rev"
1961   "Mode for looking at a Git commit.
1962
1963 This mode is documented in info node `(magit)Revision Buffer'.
1964
1965 \\<magit-mode-map>\
1966 Type \\[magit-refresh] to refresh the current buffer.
1967 Type \\[magit-section-toggle] to expand or hide the section at point.
1968 Type \\[magit-visit-thing] to visit the hunk or file at point.
1969
1970 Staging and applying changes is documented in info node
1971 `(magit)Staging and Unstaging' and info node `(magit)Applying'.
1972
1973 \\<magit-hunk-section-map>Type \
1974 \\[magit-apply] to apply the change at point, \
1975 \\[magit-stage] to stage,
1976 \\[magit-unstage] to unstage, \
1977 \\[magit-discard] to discard, or \
1978 \\[magit-reverse] to reverse it.
1979
1980 \\{magit-revision-mode-map}"
1981   :group 'magit-revision
1982   (hack-dir-local-variables-non-file-buffer)
1983   (setq-local bookmark-make-record-function
1984               'magit-bookmark--revision-make-record))
1985
1986 (defun magit-revision-refresh-buffer (rev __const _args files)
1987   (magit-set-header-line-format
1988    (concat (capitalize (magit-object-type rev))
1989            " "
1990            rev
1991            (pcase (length files)
1992              (0)
1993              (1 (concat " limited to file " (car files)))
1994              (_ (concat " limited to files "
1995                         (mapconcat #'identity files ", "))))))
1996   (setq magit-buffer-revision-hash (magit-rev-parse rev))
1997   (magit-insert-section (commitbuf)
1998     (magit-run-section-hook 'magit-revision-sections-hook rev)))
1999
2000 (defun magit-insert-revision-diff (rev)
2001   "Insert the diff into this `magit-revision-mode' buffer."
2002   (let ((magit-git-global-arguments
2003          (remove "--literal-pathspecs" magit-git-global-arguments)))
2004     ;; Before v2.2.0, "--format=" did not mean "no output".
2005     ;; Instead the default format was used.  So use "--format=%n"
2006     ;; and then delete the empty lines.
2007     (magit-git-wash (lambda (args)
2008                       (delete-region (point) (progn (forward-line 3) (point)))
2009                       (magit-diff-wash-diffs args))
2010       "show" "-p" "--cc" "--format=%n" "--no-prefix"
2011       (and (member "--stat" (nth 2 magit-refresh-args)) "--numstat")
2012       (nth 2 magit-refresh-args) (concat rev "^{commit}") "--"
2013       (nth 3 magit-refresh-args))))
2014
2015 (defun magit-insert-revision-tag (rev)
2016   "Insert tag message and headers into a revision buffer.
2017 This function only inserts anything when `magit-show-commit' is
2018 called with a tag as argument, when that is called with a commit
2019 or a ref which is not a branch, then it inserts nothing."
2020   (when (equal (magit-object-type rev) "tag")
2021     (magit-insert-section (taginfo)
2022       (let ((beg (point)))
2023         ;; "git verify-tag -v" would output what we need, but the gpg
2024         ;; output is send to stderr and we have no control over the
2025         ;; order in which stdout and stderr are inserted, which would
2026         ;; make parsing hard.  We are forced to use "git cat-file tag"
2027         ;; instead, which inserts the signature instead of verifying
2028         ;; it.  We remove that later and then insert the verification
2029         ;; output using "git verify-tag" (without the "-v").
2030         (magit-git-insert "cat-file" "tag" rev)
2031         (goto-char beg)
2032         (forward-line 3)
2033         (delete-region beg (point)))
2034       (looking-at "^tagger \\([^<]+\\) <\\([^>]+\\)")
2035       (let ((heading (format "Tagger: %s <%s>"
2036                              (match-string 1)
2037                              (match-string 2))))
2038         (magit-delete-line)
2039         (insert (propertize heading 'face 'magit-section-secondary-heading)))
2040       (magit-insert-heading)
2041       (if (re-search-forward "-----BEGIN PGP SIGNATURE-----" nil t)
2042           (progn
2043             (let ((beg (match-beginning 0)))
2044               (re-search-forward "-----END PGP SIGNATURE-----")
2045               (delete-region beg (point)))
2046             (insert ?\n)
2047             (process-file magit-git-executable nil t nil "verify-tag" rev))
2048         (goto-char (point-max)))
2049       (insert ?\n))))
2050
2051 (defvar magit-commit-message-section-map
2052   (let ((map (make-sparse-keymap)))
2053     (define-key map [remap magit-visit-thing] 'magit-show-commit)
2054     map)
2055   "Keymap for `commit-message' sections.")
2056
2057 (defun magit-insert-revision-message (rev)
2058   "Insert the commit message into a revision buffer."
2059   (magit-insert-section section (commit-message)
2060     (oset section heading-highlight-face 'magit-diff-hunk-heading-highlight)
2061     (let ((beg (point)))
2062       (insert (save-excursion ; The risky var query can move point.
2063                 (with-temp-buffer
2064                   (magit-rev-insert-format "%B" rev)
2065                   (magit-revision--wash-message))))
2066       (if (= (point) (+ beg 2))
2067           (progn (backward-delete-char 2)
2068                  (insert "(no message)\n"))
2069         (goto-char beg)
2070         (save-excursion
2071           (while (search-forward "\r\n" nil t) ; Remove trailing CRs.
2072             (delete-region (match-beginning 0) (1+ (match-beginning 0)))))
2073         (when magit-revision-fill-summary-line
2074           (let ((fill-column (min magit-revision-fill-summary-line
2075                                   (window-width))))
2076             (fill-region (point) (line-end-position))))
2077         (when magit-revision-use-hash-sections
2078           (save-excursion
2079             (while (not (eobp))
2080               (re-search-forward "\\_<" nil 'move)
2081               (let ((beg (point)))
2082                 (re-search-forward "\\_>" nil t)
2083                 (when (> (point) beg)
2084                   (let ((text (buffer-substring-no-properties beg (point))))
2085                     (when (pcase magit-revision-use-hash-sections
2086                             (`quickest ; false negatives and positives
2087                              (and (>= (length text) 7)
2088                                   (string-match-p "[0-9]" text)
2089                                   (string-match-p "[a-z]" text)))
2090                             (`quicker  ; false negatives (number-less hashes)
2091                              (and (>= (length text) 7)
2092                                   (string-match-p "[0-9]" text)
2093                                   (magit-rev-verify-commit text)))
2094                             (`quick    ; false negatives (short hashes)
2095                              (and (>= (length text) 7)
2096                                   (magit-rev-verify-commit text)))
2097                             (`slow
2098                              (magit-rev-verify-commit text)))
2099                       (put-text-property beg (point) 'face 'magit-hash)
2100                       (let ((end (point)))
2101                         (goto-char beg)
2102                         (magit-insert-section (commit text)
2103                           (goto-char end))))))))))
2104         (save-excursion
2105           (forward-line)
2106           (add-face-text-property beg (point) 'magit-diff-hunk-heading)
2107           (magit-insert-heading))
2108         (when magit-diff-highlight-keywords
2109           (save-excursion
2110             (while (re-search-forward "\\[[^[]*\\]" nil t)
2111               (let ((beg (match-beginning 0))
2112                     (end (match-end 0)))
2113                 (put-text-property
2114                  beg end 'face
2115                  (if-let ((face (get-text-property beg 'face)))
2116                      (list face 'magit-keyword)
2117                    'magit-keyword))))))
2118         (goto-char (point-max))))))
2119
2120 (defun magit-insert-revision-notes (rev)
2121   "Insert commit notes into a revision buffer."
2122   (let* ((var "core.notesRef")
2123          (def (or (magit-get var) "refs/notes/commits")))
2124     (dolist (ref (or (magit-list-active-notes-refs)))
2125       (magit-insert-section section (notes ref (not (equal ref def)))
2126         (oset section heading-highlight-face 'magit-diff-hunk-heading-highlight)
2127         (let ((beg (point)))
2128           (insert (with-temp-buffer
2129                     (magit-git-insert "-c" (concat "core.notesRef=" ref)
2130                                       "notes" "show" rev)
2131                     (magit-revision--wash-message)))
2132           (if (= (point) beg)
2133               (magit-cancel-section)
2134             (goto-char beg)
2135             (end-of-line)
2136             (insert (format " (%s)"
2137                             (propertize (if (string-prefix-p "refs/notes/" ref)
2138                                             (substring ref 11)
2139                                           ref)
2140                                         'face 'magit-refname)))
2141             (forward-char)
2142             (add-face-text-property beg (point) 'magit-diff-hunk-heading)
2143             (magit-insert-heading)
2144             (goto-char (point-max))
2145             (insert ?\n)))))))
2146
2147 (defun magit-revision--wash-message ()
2148   (let ((major-mode 'git-commit-mode))
2149     (hack-dir-local-variables)
2150     (hack-local-variables-apply))
2151   (unless (memq git-commit-major-mode '(nil text-mode))
2152     (funcall git-commit-major-mode)
2153     (font-lock-ensure))
2154   (buffer-string))
2155
2156 (defun magit-insert-revision-headers (rev)
2157   "Insert headers about the commit into a revision buffer."
2158   (magit-insert-section (headers)
2159     ;; Before v2.2.0, "%D" was not supported.
2160     (--when-let (magit-rev-format "%d" rev "--decorate=full")
2161       (insert (magit-format-ref-labels it) ?\s))
2162     (insert (propertize (magit-rev-parse (concat rev "^{commit}"))
2163                         'face 'magit-hash))
2164     (magit-insert-heading)
2165     (let ((beg (point)))
2166       (magit-rev-insert-format magit-revision-headers-format rev)
2167       (magit-insert-revision-gravatars rev beg))
2168     (when magit-revision-insert-related-refs
2169       (dolist (parent (magit-commit-parents rev))
2170         (magit-insert-section (commit parent)
2171           (let ((line (magit-rev-format "%h %s" parent)))
2172             (string-match "^\\([^ ]+\\) \\(.*\\)" line)
2173             (magit-bind-match-strings (hash msg) line
2174               (insert "Parent:     ")
2175               (insert (propertize hash 'face 'magit-hash))
2176               (insert " " msg "\n")))))
2177       (magit--insert-related-refs
2178        rev "--merged" "Merged"
2179        (eq magit-revision-insert-related-refs 'all))
2180       (magit--insert-related-refs
2181        rev "--contains" "Contained"
2182        (eq magit-revision-insert-related-refs '(all mixed)))
2183       (when-let ((follows (magit-get-current-tag rev t)))
2184         (let ((tag (car  follows))
2185               (cnt (cadr follows)))
2186           (magit-insert-section (tag tag)
2187             (insert (format "Follows:    %s (%s)\n"
2188                             (propertize tag 'face 'magit-tag)
2189                             (propertize (number-to-string cnt)
2190                                         'face 'magit-branch-local))))))
2191       (when-let ((precedes (magit-get-next-tag rev t)))
2192         (let ((tag (car  precedes))
2193               (cnt (cadr precedes)))
2194           (magit-insert-section (tag tag)
2195             (insert (format "Precedes:   %s (%s)\n"
2196                             (propertize tag 'face 'magit-tag)
2197                             (propertize (number-to-string cnt)
2198                                         'face 'magit-tag))))))
2199       (insert ?\n))))
2200
2201 (defun magit--insert-related-refs (rev arg title remote)
2202   (when-let ((refs (magit-list-related-branches arg rev (and remote "-a"))))
2203     (insert title ":" (make-string (- 10 (length title)) ?\s))
2204     (dolist (branch refs)
2205       (if (<= (+ (current-column) 1 (length branch))
2206               (window-width))
2207           (insert ?\s)
2208         (insert ?\n (make-string 12 ?\s)))
2209       (magit-insert-section (branch branch)
2210         (insert (propertize branch 'face
2211                             (if (string-prefix-p "remotes/" branch)
2212                                 'magit-branch-remote
2213                               'magit-branch-local)))))
2214     (insert ?\n)))
2215
2216 (defun magit-insert-revision-gravatars (rev beg)
2217   (when (and magit-revision-show-gravatars
2218              (window-system))
2219     (require 'gravatar)
2220     (pcase-let ((`(,author . ,committer)
2221                  (pcase magit-revision-show-gravatars
2222                    (`t '("^Author:     " . "^Commit:     "))
2223                    (`author '("^Author:     " . nil))
2224                    (`committer '(nil . "^Commit:     "))
2225                    (_ magit-revision-show-gravatars))))
2226       (--when-let (and author (magit-rev-format "%aE" rev))
2227         (magit-insert-revision-gravatar beg rev it author))
2228       (--when-let (and committer (magit-rev-format "%cE" rev))
2229         (magit-insert-revision-gravatar beg rev it committer)))))
2230
2231 (defun magit-insert-revision-gravatar (beg rev email regexp)
2232   (save-excursion
2233     (goto-char beg)
2234     (when (re-search-forward regexp nil t)
2235       (let* ((column   (length (match-string 0)))
2236              (font-obj (query-font (font-at (point) (get-buffer-window))))
2237              (size     (* 2 (+ (aref font-obj 4)
2238                                (aref font-obj 5))))
2239              (align-to (+ column
2240                           (ceiling (/ size (aref font-obj 7) 1.0))
2241                           1))
2242              (gravatar-size (- size 2)))
2243         (ignore-errors ; service may be unreachable
2244           (gravatar-retrieve email 'magit-insert-revision-gravatar-cb
2245                              (list rev (point-marker) align-to column)))))))
2246
2247 (defun magit-insert-revision-gravatar-cb (image rev marker align-to column)
2248   (unless (eq image 'error)
2249     (when-let ((buffer (marker-buffer marker)))
2250       (with-current-buffer buffer
2251         (save-excursion
2252           (goto-char marker)
2253           ;; The buffer might display another revision by now or
2254           ;; it might have been refreshed, in which case another
2255           ;; process might already have inserted the image.
2256           (when (and (equal rev (car magit-refresh-args))
2257                      (not (eq (car-safe
2258                                (car-safe
2259                                 (get-text-property (point) 'display)))
2260                               'image)))
2261             (let ((top `((,@image :ascent center :relief 1)
2262                          (slice 0.0 0.0 1.0 0.5)))
2263                   (bot `((,@image :ascent center :relief 1)
2264                          (slice 0.0 0.5 1.0 1.0)))
2265                   (align `((space :align-to ,align-to))))
2266               (when magit-revision-use-gravatar-kludge
2267                 (cl-rotatef top bot))
2268               (let ((inhibit-read-only t))
2269                 (insert (propertize " " 'display top))
2270                 (insert (propertize " " 'display align))
2271                 (forward-line)
2272                 (forward-char column)
2273                 (insert (propertize " " 'display bot))
2274                 (insert (propertize " " 'display align))))))))))
2275
2276 ;;; Diff Sections
2277
2278 (defvar magit-unstaged-section-map
2279   (let ((map (make-sparse-keymap)))
2280     (define-key map [remap magit-visit-thing]  'magit-diff-unstaged)
2281     (define-key map [remap magit-delete-thing] 'magit-discard)
2282     (define-key map "s" 'magit-stage)
2283     (define-key map "u" 'magit-unstage)
2284     map)
2285   "Keymap for the `unstaged' section.")
2286
2287 (magit-define-section-jumper magit-jump-to-unstaged "Unstaged changes" unstaged)
2288
2289 (defun magit-insert-unstaged-changes ()
2290   "Insert section showing unstaged changes."
2291   (magit-insert-section (unstaged)
2292     (magit-insert-heading "Unstaged changes:")
2293     (magit-git-wash #'magit-diff-wash-diffs
2294       "diff" magit-diff-section-arguments "--no-prefix"
2295       "--" magit-diff-section-file-args)))
2296
2297 (defvar magit-staged-section-map
2298   (let ((map (make-sparse-keymap)))
2299     (define-key map [remap magit-visit-thing]      'magit-diff-staged)
2300     (define-key map [remap magit-delete-thing]     'magit-discard)
2301     (define-key map [remap magit-revert-no-commit] 'magit-reverse)
2302     (define-key map "s" 'magit-stage)
2303     (define-key map "u" 'magit-unstage)
2304     map)
2305   "Keymap for the `staged' section.")
2306
2307 (magit-define-section-jumper magit-jump-to-staged "Staged changes" staged)
2308
2309 (defun magit-insert-staged-changes ()
2310   "Insert section showing staged changes."
2311   ;; Avoid listing all files as deleted when visiting a bare repo.
2312   (unless (magit-bare-repo-p)
2313     (magit-insert-section (staged)
2314       (magit-insert-heading "Staged changes:")
2315       (magit-git-wash #'magit-diff-wash-diffs
2316         "diff" "--cached" magit-diff-section-arguments "--no-prefix"
2317         "--" magit-diff-section-file-args))))
2318
2319 ;;; Diff Type
2320
2321 (defun magit-diff-type (&optional section)
2322   "Return the diff type of SECTION.
2323
2324 The returned type is one of the symbols `staged', `unstaged',
2325 `committed', or `undefined'.  This type serves a similar purpose
2326 as the general type common to all sections (which is stored in
2327 the `type' slot of the corresponding `magit-section' struct) but
2328 takes additional information into account.  When the SECTION
2329 isn't related to diffs and the buffer containing it also isn't
2330 a diff-only buffer, then return nil.
2331
2332 Currently the type can also be one of `tracked' and `untracked'
2333 but these values are not handled explicitly everywhere they
2334 should be and a possible fix could be to just return nil here.
2335
2336 The section has to be a `diff' or `hunk' section, or a section
2337 whose children are of type `diff'.  If optional SECTION is nil,
2338 return the diff type for the current section.  In buffers whose
2339 major mode is `magit-diff-mode' SECTION is ignored and the type
2340 is determined using other means.  In `magit-revision-mode'
2341 buffers the type is always `committed'.
2342
2343 Do not confuse this with `magit-diff-scope' (which see)."
2344   (--when-let (or section (magit-current-section))
2345     (cond ((derived-mode-p 'magit-revision-mode 'magit-stash-mode) 'committed)
2346           ((derived-mode-p 'magit-diff-mode)
2347            (let ((range (nth 0 magit-refresh-args))
2348                  (const (nth 1 magit-refresh-args)))
2349              (cond ((member "--no-index" const) 'undefined)
2350                    ((or (not range)
2351                         (magit-rev-eq range "HEAD"))
2352                     (if (member "--cached" const)
2353                         'staged
2354                       'unstaged))
2355                    ((member "--cached" const)
2356                     (if (magit-rev-head-p range)
2357                         'staged
2358                       'undefined)) ; i.e. committed and staged
2359                    (t 'committed))))
2360           ((derived-mode-p 'magit-status-mode)
2361            (let ((stype (oref it type)))
2362              (if (memq stype '(staged unstaged tracked untracked))
2363                  stype
2364                (pcase stype
2365                  ((or `file `module)
2366                   (let* ((parent (oref it parent))
2367                          (type   (oref parent type)))
2368                     (if (memq type '(file module))
2369                         (magit-diff-type parent)
2370                       type)))
2371                  (`hunk (-> it
2372                             (oref parent)
2373                             (oref parent)
2374                             (oref type)))))))
2375           ((derived-mode-p 'magit-log-mode)
2376            (if (or (and (magit-section-match 'commit section)
2377                         (oref section children))
2378                    (magit-section-match [* file commit] section))
2379                'committed
2380            'undefined))
2381           (t 'undefined))))
2382
2383 (cl-defun magit-diff-scope (&optional (section nil ssection) strict)
2384   "Return the diff scope of SECTION or the selected section(s).
2385
2386 A diff's \"scope\" describes what part of a diff is selected, it is
2387 a symbol, one of `region', `hunk', `hunks', `file', `files', or
2388 `list'.  Do not confuse this with the diff \"type\", as returned by
2389 `magit-diff-type'.
2390
2391 If optional SECTION is non-nil, then return the scope of that,
2392 ignoring the sections selected by the region.  Otherwise return
2393 the scope of the current section, or if the region is active and
2394 selects a valid group of diff related sections, the type of these
2395 sections, i.e. `hunks' or `files'.  If SECTION, or if that is nil
2396 the current section, is a `hunk' section; and the region region
2397 starts and ends inside the body of a that section, then the type
2398 is `region'.  If the region is empty after a mouse click, then
2399 `hunk' is returned instead of `region'.
2400
2401 If optional STRICT is non-nil, then return nil if the diff type of
2402 the section at point is `untracked' or the section at point is not
2403 actually a `diff' but a `diffstat' section."
2404   (let ((siblings (and (not ssection) (magit-region-sections nil t))))
2405     (setq section (or section (car siblings) (magit-current-section)))
2406     (when (and section
2407                (or (not strict)
2408                    (and (not (eq (magit-diff-type section) 'untracked))
2409                         (not (eq (--when-let (oref section parent)
2410                                    (oref it type))
2411                                  'diffstat)))))
2412       (pcase (list (oref section type)
2413                    (and siblings t)
2414                    (magit-diff-use-hunk-region-p)
2415                    ssection)
2416         (`(hunk nil   t  ,_)
2417          (if (magit-section-internal-region-p section) 'region 'hunk))
2418         (`(hunk   t   t nil) 'hunks)
2419         (`(hunk  ,_  ,_  ,_) 'hunk)
2420         (`(file   t   t nil) 'files)
2421         (`(file  ,_  ,_  ,_) 'file)
2422         (`(module   t   t nil) 'files)
2423         (`(module  ,_  ,_  ,_) 'file)
2424         (`(,(or `staged `unstaged `untracked)
2425            nil ,_ ,_) 'list)))))
2426
2427 (defun magit-diff-use-hunk-region-p ()
2428   (and (region-active-p)
2429        (not (and (if (version< emacs-version "25.1")
2430                      (eq this-command 'mouse-drag-region)
2431                    ;; TODO implement this from first principals
2432                    ;; currently it's trial-and-error
2433                    (or (eq this-command 'mouse-drag-region)
2434                        (eq last-command 'mouse-drag-region)
2435                        ;; When another window was previously
2436                        ;; selected then the last-command is
2437                        ;; some byte-code function.
2438                        (byte-code-function-p last-command)))
2439                  (eq (region-end) (region-beginning))))))
2440
2441 ;;; Diff Highlight
2442
2443 (defun magit-diff-unhighlight (section selection)
2444   "Remove the highlighting of the diff-related SECTION."
2445   (when (magit-hunk-section-p section)
2446     (magit-diff-paint-hunk section selection nil)
2447     t))
2448
2449 (defun magit-diff-highlight (section selection)
2450   "Highlight the diff-related SECTION.
2451 If SECTION is not a diff-related section, then do nothing and
2452 return nil.  If SELECTION is non-nil, then it is a list of sections
2453 selected by the region, including SECTION.  All of these sections
2454 are highlighted."
2455   (if (and (magit-section-match 'commit section)
2456            (oref section children))
2457       (progn (if selection
2458                  (dolist (section selection)
2459                    (magit-diff-highlight-list section selection))
2460                (magit-diff-highlight-list section))
2461              t)
2462     (when-let ((scope (magit-diff-scope section t)))
2463       (cond ((eq scope 'region)
2464              (magit-diff-paint-hunk section selection t))
2465             (selection
2466              (dolist (section selection)
2467                (magit-diff-highlight-recursive section selection)))
2468             (t
2469              (magit-diff-highlight-recursive section)))
2470       t)))
2471
2472 (defun magit-diff-highlight-recursive (section &optional selection)
2473   (pcase (magit-diff-scope section)
2474     (`list (magit-diff-highlight-list section selection))
2475     (`file (magit-diff-highlight-file section selection))
2476     (`hunk (magit-diff-highlight-heading section selection)
2477            (magit-diff-paint-hunk section selection t))
2478     (_     (magit-section-highlight section nil))))
2479
2480 (defun magit-diff-highlight-list (section &optional selection)
2481   (let ((beg (oref section start))
2482         (cnt (oref section content))
2483         (end (oref section end)))
2484     (when (or (eq this-command 'mouse-drag-region)
2485               (not selection))
2486       (unless (and (region-active-p)
2487                    (<= (region-beginning) beg))
2488         (magit-section-make-overlay beg cnt 'magit-section-highlight))
2489       (unless (oref section hidden)
2490         (dolist (child (oref section children))
2491           (when (or (eq this-command 'mouse-drag-region)
2492                     (not (and (region-active-p)
2493                               (<= (region-beginning)
2494                                   (oref child start)))))
2495             (magit-diff-highlight-recursive child selection)))))
2496     (when magit-diff-highlight-hunk-body
2497       (magit-section-make-overlay (1- end) end 'magit-section-highlight))))
2498
2499 (defun magit-diff-highlight-file (section &optional selection)
2500   (magit-diff-highlight-heading section selection)
2501   (unless (oref section hidden)
2502     (dolist (child (oref section children))
2503       (magit-diff-highlight-recursive child selection))))
2504
2505 (defun magit-diff-highlight-heading (section &optional selection)
2506   (magit-section-make-overlay
2507    (oref section start)
2508    (or (oref section content)
2509        (oref section end))
2510    (pcase (list (oref section type)
2511                 (and (member section selection)
2512                      (not (eq this-command 'mouse-drag-region))))
2513      (`(file   t) 'magit-diff-file-heading-selection)
2514      (`(file nil) 'magit-diff-file-heading-highlight)
2515      (`(module   t) 'magit-diff-file-heading-selection)
2516      (`(module nil) 'magit-diff-file-heading-highlight)
2517      (`(hunk   t) 'magit-diff-hunk-heading-selection)
2518      (`(hunk nil) 'magit-diff-hunk-heading-highlight))))
2519
2520 ;;; Hunk Paint
2521
2522 (cl-defun magit-diff-paint-hunk
2523     (section &optional selection
2524              (highlight (magit-section-selected-p section selection)))
2525   (let (paint)
2526     (unless magit-diff-highlight-hunk-body
2527       (setq highlight nil))
2528     (cond (highlight
2529            (unless (oref section hidden)
2530              (add-to-list 'magit-section-highlighted-sections section)
2531              (cond ((memq section magit-section-unhighlight-sections)
2532                     (setq magit-section-unhighlight-sections
2533                           (delq section magit-section-unhighlight-sections)))
2534                    (magit-diff-highlight-hunk-body
2535                     (setq paint t)))))
2536           (t
2537            (cond ((and (oref section hidden)
2538                        (memq section magit-section-unhighlight-sections))
2539                   (add-to-list 'magit-section-highlighted-sections section)
2540                   (setq magit-section-unhighlight-sections
2541                         (delq section magit-section-unhighlight-sections)))
2542                  (t
2543                   (setq paint t)))))
2544     (when paint
2545       (save-excursion
2546         (goto-char (oref section start))
2547         (let ((end (oref section end))
2548               (merging (looking-at "@@@"))
2549               (stage nil)
2550               (tab-width (magit-diff-tab-width
2551                           (magit-section-parent-value section))))
2552           (forward-line)
2553           (while (< (point) end)
2554             (when (and magit-diff-hide-trailing-cr-characters
2555                        (char-equal ?\r (char-before (line-end-position))))
2556               (put-text-property (1- (line-end-position)) (line-end-position)
2557                                  'invisible t))
2558             (put-text-property
2559              (point) (1+ (line-end-position)) 'face
2560              (cond
2561               ((looking-at "^\\+\\+?\\([<=|>]\\)\\{7\\}")
2562                (setq stage (pcase (list (match-string 1) highlight)
2563                              (`("<" nil) 'magit-diff-our)
2564                              (`("<"   t) 'magit-diff-our-highlight)
2565                              (`("|" nil) 'magit-diff-base)
2566                              (`("|"   t) 'magit-diff-base-highlight)
2567                              (`("=" nil) 'magit-diff-their)
2568                              (`("="   t) 'magit-diff-their-highlight)
2569                              (`(">" nil) nil)))
2570                'magit-diff-conflict-heading)
2571               ((looking-at (if merging "^\\(\\+\\| \\+\\)" "^\\+"))
2572                (magit-diff-paint-tab merging tab-width)
2573                (magit-diff-paint-whitespace merging)
2574                (or stage
2575                    (if highlight 'magit-diff-added-highlight 'magit-diff-added)))
2576               ((looking-at (if merging "^\\(-\\| -\\)" "^-"))
2577                (magit-diff-paint-tab merging tab-width)
2578                (if highlight 'magit-diff-removed-highlight 'magit-diff-removed))
2579               (t
2580                (magit-diff-paint-tab merging tab-width)
2581                (if highlight 'magit-diff-context-highlight 'magit-diff-context))))
2582             (forward-line))))))
2583   (magit-diff-update-hunk-refinement section))
2584
2585 (defvar magit-diff--tab-width-cache nil)
2586
2587 (defun magit-diff-tab-width (file)
2588   (setq file (expand-file-name file))
2589   (cl-flet ((cache (value)
2590                    (let ((elt (assoc file magit-diff--tab-width-cache)))
2591                      (if elt
2592                          (setcdr elt value)
2593                        (setq magit-diff--tab-width-cache
2594                              (cons (cons file value)
2595                                    magit-diff--tab-width-cache))))
2596                    value))
2597     (cond
2598      ((not magit-diff-adjust-tab-width)
2599       tab-width)
2600      ((--when-let (find-buffer-visiting file)
2601         (cache (buffer-local-value 'tab-width it))))
2602      ((--when-let (assoc file magit-diff--tab-width-cache)
2603         (or (cdr it)
2604             tab-width)))
2605      ((or (eq magit-diff-adjust-tab-width 'always)
2606           (and (numberp magit-diff-adjust-tab-width)
2607                (>= magit-diff-adjust-tab-width
2608                    (nth 7 (file-attributes file)))))
2609       (cache (buffer-local-value 'tab-width (find-file-noselect file))))
2610      (t
2611       (cache nil)
2612       tab-width))))
2613
2614 (defun magit-diff-paint-tab (merging width)
2615   (save-excursion
2616     (forward-char (if merging 2 1))
2617     (while (= (char-after) ?\t)
2618       (put-text-property (point) (1+ (point))
2619                          'display (list (list 'space :width width)))
2620       (forward-char))))
2621
2622 (defun magit-diff-paint-whitespace (merging)
2623   (when (and magit-diff-paint-whitespace
2624              (or (derived-mode-p 'magit-status-mode)
2625                  (not (eq magit-diff-paint-whitespace 'status))))
2626     (let ((prefix (if merging "^[-\\+\s]\\{2\\}" "^[-\\+]"))
2627           (indent
2628            (if (local-variable-p 'magit-diff-highlight-indentation)
2629                magit-diff-highlight-indentation
2630              (setq-local
2631               magit-diff-highlight-indentation
2632               (cdr (--first (string-match-p (car it) default-directory)
2633                             (nreverse
2634                              (default-value
2635                                'magit-diff-highlight-indentation))))))))
2636       (when (and magit-diff-highlight-trailing
2637                  (looking-at (concat prefix ".*?\\([ \t]+\\)$")))
2638         (let ((ov (make-overlay (match-beginning 1) (match-end 1) nil t)))
2639           (overlay-put ov 'face 'magit-diff-whitespace-warning)
2640           (overlay-put ov 'evaporate t)))
2641       (when (or (and (eq indent 'tabs)
2642                      (looking-at (concat prefix "\\( *\t[ \t]*\\)")))
2643                 (and (integerp indent)
2644                      (looking-at (format "%s\\([ \t]* \\{%s,\\}[ \t]*\\)"
2645                                          prefix indent))))
2646         (let ((ov (make-overlay (match-beginning 1) (match-end 1) nil t)))
2647           (overlay-put ov 'face 'magit-diff-whitespace-warning)
2648           (overlay-put ov 'evaporate t))))))
2649
2650 (defun magit-diff-update-hunk-refinement (&optional section)
2651   (if section
2652       (unless (oref section hidden)
2653         (pcase (list magit-diff-refine-hunk
2654                      (oref section refined)
2655                      (eq section (magit-current-section)))
2656           ((or `(all nil ,_) `(t nil t))
2657            (oset section refined t)
2658            (save-excursion
2659              (goto-char (oref section start))
2660              ;; `diff-refine-hunk' does not handle combined diffs.
2661              (unless (looking-at "@@@")
2662                ;; Avoid fsyncing many small temp files
2663                (let ((write-region-inhibit-fsync t))
2664                  (diff-refine-hunk)))))
2665           ((or `(nil t ,_) `(t t nil))
2666            (oset section refined nil)
2667            (remove-overlays (oref section start)
2668                             (oref section end)
2669                             'diff-mode 'fine))))
2670     (cl-labels ((recurse (section)
2671                          (if (magit-section-match 'hunk section)
2672                              (magit-diff-update-hunk-refinement section)
2673                            (dolist (child (oref section children))
2674                              (recurse child)))))
2675       (recurse magit-root-section))))
2676
2677
2678 ;;; Hunk Region
2679
2680 (defun magit-diff-hunk-region-beginning ()
2681   (save-excursion (goto-char (region-beginning))
2682                   (line-beginning-position)))
2683
2684 (defun magit-diff-hunk-region-end ()
2685   (save-excursion (goto-char (region-end))
2686                   (line-end-position)))
2687
2688 (defun magit-diff-update-hunk-region (section)
2689   "Highlight the hunk-internal region if any."
2690   (when (eq (magit-diff-scope section t) 'region)
2691     (magit-diff--make-hunk-overlay
2692      (oref section start)
2693      (1- (oref section content))
2694      'face 'magit-diff-lines-heading
2695      'display (magit-diff-hunk-region-header section)
2696      'after-string (magit-diff--hunk-after-string 'magit-diff-lines-heading))
2697     (run-hook-with-args 'magit-diff-highlight-hunk-region-functions section)
2698     t))
2699
2700 (defun magit-diff-highlight-hunk-region-dim-outside (section)
2701   "Dim the parts of the hunk that are outside the hunk-internal region.
2702 This is done by using the same foreground and background color
2703 for added and removed lines as for context lines."
2704   (let ((face (if magit-diff-highlight-hunk-body
2705                   'magit-diff-context-highlight
2706                 'magit-diff-context)))
2707     (when magit-diff-unmarked-lines-keep-foreground
2708       (setq face (list :background (face-attribute face :background))))
2709     (magit-diff--make-hunk-overlay (oref section content)
2710                                    (magit-diff-hunk-region-beginning)
2711                                    'face face
2712                                    'priority 2)
2713     (magit-diff--make-hunk-overlay (1+ (magit-diff-hunk-region-end))
2714                                    (oref section end)
2715                                    'face face
2716                                    'priority 2)))
2717
2718 (defun magit-diff-highlight-hunk-region-using-face (_section)
2719   "Highlight the hunk-internal region by making it bold.
2720 Or rather highlight using the face `magit-diff-hunk-region', though
2721 changing only the `:weight' and/or `:slant' is recommended for that
2722 face."
2723   (magit-diff--make-hunk-overlay (magit-diff-hunk-region-beginning)
2724                                  (1+ (magit-diff-hunk-region-end))
2725                                  'face 'magit-diff-hunk-region))
2726
2727 (defun magit-diff-highlight-hunk-region-using-overlays (section)
2728   "Emphasize the hunk-internal region using delimiting horizontal lines.
2729 This is implemented as single-pixel newlines places inside overlays."
2730   (if (window-system)
2731       (let ((beg (magit-diff-hunk-region-beginning))
2732             (end (magit-diff-hunk-region-end))
2733             (str (propertize
2734                   (concat (propertize "\s" 'display '(space :height (1)))
2735                           (propertize "\n" 'line-height t))
2736                   'face 'magit-diff-lines-boundary)))
2737         (magit-diff--make-hunk-overlay beg (1+ beg) 'before-string str)
2738         (magit-diff--make-hunk-overlay end (1+ end) 'after-string  str))
2739     (magit-diff-highlight-hunk-region-using-face section)))
2740
2741 (defun magit-diff-highlight-hunk-region-using-underline (section)
2742   "Emphasize the hunk-internal region using delimiting horizontal lines.
2743 This is implemented by overlining and underlining the first and
2744 last (visual) lines of the region."
2745   (if (window-system)
2746       (let* ((beg (magit-diff-hunk-region-beginning))
2747              (end (magit-diff-hunk-region-end))
2748              (beg-eol (save-excursion (goto-char beg)
2749                                       (end-of-visual-line)
2750                                       (point)))
2751              (end-bol (save-excursion (goto-char end)
2752                                       (beginning-of-visual-line)
2753                                       (point)))
2754              (color (face-background 'magit-diff-lines-boundary nil t)))
2755         (cl-flet ((ln (b e &rest face)
2756                       (magit-diff--make-hunk-overlay
2757                        b e 'face face 'after-string
2758                        (magit-diff--hunk-after-string face))))
2759           (if (= beg end-bol)
2760               (ln beg beg-eol :overline color :underline color)
2761             (ln beg beg-eol :overline color)
2762             (ln end-bol end :underline color))))
2763     (magit-diff-highlight-hunk-region-using-face section)))
2764
2765 (defun magit-diff--make-hunk-overlay (start end &rest args)
2766   (let ((ov (make-overlay start end nil t)))
2767     (overlay-put ov 'evaporate t)
2768     (while args (overlay-put ov (pop args) (pop args)))
2769     (push ov magit-region-overlays)
2770     ov))
2771
2772 (defun magit-diff--hunk-after-string (face)
2773   (propertize "\s"
2774               'face face
2775               'display (list 'space :align-to
2776                              `(+ (0 . right)
2777                                  ,(min (window-hscroll)
2778                                        (- (line-end-position)
2779                                           (line-beginning-position)))))
2780               ;; This prevents the cursor from being rendered at the
2781               ;; edge of the window.
2782               'cursor t))
2783
2784 ;;; Hunk Utilities
2785
2786 (defun magit-diff-inside-hunk-body-p ()
2787   "Return non-nil if point is inside the body of a hunk."
2788   (and (magit-section-match 'hunk)
2789        (> (point)
2790           (oref (magit-current-section) content))))
2791
2792 ;;; Diff Extract
2793
2794 (defun magit-diff-file-header (section)
2795   (when (magit-hunk-section-p section)
2796     (setq section (oref section parent)))
2797   (when (magit-file-section-p section)
2798     (oref section header)))
2799
2800 (defun magit-diff-hunk-region-header (section)
2801   (let ((patch (magit-diff-hunk-region-patch section)))
2802     (string-match "\n" patch)
2803     (substring patch 0 (1- (match-end 0)))))
2804
2805 (defun magit-diff-hunk-region-patch (section &optional args)
2806   (let ((op (if (member "--reverse" args) "+" "-"))
2807         (sbeg (oref section start))
2808         (rbeg (magit-diff-hunk-region-beginning))
2809         (rend (region-end))
2810         (send (oref section end))
2811         (patch nil))
2812     (save-excursion
2813       (goto-char sbeg)
2814       (while (< (point) send)
2815         (looking-at "\\(.\\)\\([^\n]*\n\\)")
2816         (cond ((or (string-match-p "[@ ]" (match-string-no-properties 1))
2817                    (and (>= (point) rbeg)
2818                         (<= (point) rend)))
2819                (push (match-string-no-properties 0) patch))
2820               ((equal op (match-string-no-properties 1))
2821                (push (concat " " (match-string-no-properties 2)) patch)))
2822         (forward-line)))
2823     (with-temp-buffer
2824       (insert (mapconcat 'identity (reverse patch) ""))
2825       (diff-fixup-modifs (point-min) (point-max))
2826       (setq patch (buffer-string)))
2827     patch))
2828
2829 ;;; _
2830 (provide 'magit-diff)
2831 ;;; magit-diff.el ends here