config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

magit-diff.el (148130B)


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