dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

magit-diff.el (139224B)


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