config

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

magit-diff.el (147808B)


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