config

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

magit-diff.el (148777B)


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