aboutsummaryrefslogtreecommitdiffstats
path: root/elpa/magit-20220503.1245/magit-diff.el
diff options
context:
space:
mode:
Diffstat (limited to 'elpa/magit-20220503.1245/magit-diff.el')
-rw-r--r--elpa/magit-20220503.1245/magit-diff.el3450
1 files changed, 3450 insertions, 0 deletions
diff --git a/elpa/magit-20220503.1245/magit-diff.el b/elpa/magit-20220503.1245/magit-diff.el
new file mode 100644
index 0000000..9e78b52
--- /dev/null
+++ b/elpa/magit-20220503.1245/magit-diff.el
@@ -0,0 +1,3450 @@
+;;; magit-diff.el --- Inspect Git diffs -*- lexical-binding:t -*-
+
+;; Copyright (C) 2008-2022 The Magit Project Contributors
+
+;; Author: Jonas Bernoulli <jonas@bernoul.li>
+;; Maintainer: Jonas Bernoulli <jonas@bernoul.li>
+
+;; SPDX-License-Identifier: GPL-3.0-or-later
+
+;; Magit is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Magit is distributed in the hope that it will be useful, but WITHOUT
+;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+;; License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Magit. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This library implements support for looking at Git diffs and
+;; commits.
+
+;;; Code:
+
+(require 'magit-core)
+(require 'git-commit)
+
+(eval-when-compile (require 'ansi-color))
+(require 'diff-mode)
+(require 'image)
+(require 'smerge-mode)
+
+;; For `magit-diff-popup'
+(declare-function magit-stash-show "magit-stash" (stash &optional args files))
+;; For `magit-diff-visit-file'
+(declare-function magit-find-file-noselect "magit-files" (rev file))
+(declare-function magit-status-setup-buffer "magit-status" (&optional directory))
+;; For `magit-diff-while-committing'
+(declare-function magit-commit-message-buffer "magit-commit" ())
+;; For `magit-insert-revision-gravatar'
+(defvar gravatar-size)
+;; For `magit-show-commit' and `magit-diff-show-or-scroll'
+(declare-function magit-current-blame-chunk "magit-blame" (&optional type noerror))
+(declare-function magit-blame-mode "magit-blame" (&optional arg))
+(defvar magit-blame-mode)
+;; For `magit-diff-show-or-scroll'
+(declare-function git-rebase-current-line "git-rebase" ())
+;; For `magit-diff-unmerged'
+(declare-function magit-merge-in-progress-p "magit-merge" ())
+(declare-function magit--merge-range "magit-merge" (&optional head))
+;; For `magit-diff--dwim'
+(declare-function forge--pullreq-range "forge-pullreq"
+ (pullreq &optional endpoints))
+(declare-function forge--pullreq-ref "forge-pullreq" (pullreq))
+;; For `magit-diff-wash-diff'
+(declare-function ansi-color-apply-on-region "ansi-color")
+;; For `magit-diff-wash-submodule'
+(declare-function magit-log-wash-log "magit-log" (style args))
+;; For keymaps and menus
+(declare-function magit-apply "magit-apply" (&rest args))
+(declare-function magit-stage "magit-apply" (&optional indent))
+(declare-function magit-unstage "magit-apply" ())
+(declare-function magit-discard "magit-apply" ())
+(declare-function magit-reverse "magit-apply" (&rest args))
+(declare-function magit-file-rename "magit-files" (file newname))
+(declare-function magit-file-untrack "magit-files" (files &optional force))
+(declare-function magit-commit-add-log "magit-commit" ())
+(declare-function magit-diff-trace-definition "magit-log" ())
+(declare-function magit-patch-save "magit-patch" (files &optional arg))
+(declare-function magit-do-async-shell-command "magit-extras" (file))
+(declare-function magit-add-change-log-entry "magit-extras"
+ (&optional whoami file-name other-window))
+(declare-function magit-add-change-log-entry-other-window "magit-extras"
+ (&optional whoami file-name))
+(declare-function magit-diff-edit-hunk-commit "magit-extras" (file))
+(declare-function magit-smerge-keep-current "magit-apply" ())
+(declare-function magit-smerge-keep-upper "magit-apply" ())
+(declare-function magit-smerge-keep-base "magit-apply" ())
+(declare-function magit-smerge-keep-lower "magit-apply" ())
+
+(eval-when-compile
+ (cl-pushnew 'orig-rev eieio--known-slot-names)
+ (cl-pushnew 'action-type eieio--known-slot-names)
+ (cl-pushnew 'target eieio--known-slot-names))
+
+;;; Options
+;;;; Diff Mode
+
+(defgroup magit-diff nil
+ "Inspect and manipulate Git diffs."
+ :link '(info-link "(magit)Diffing")
+ :group 'magit-commands
+ :group 'magit-modes)
+
+(defcustom magit-diff-mode-hook nil
+ "Hook run after entering Magit-Diff mode."
+ :group 'magit-diff
+ :type 'hook)
+
+(defcustom magit-diff-sections-hook
+ '(magit-insert-diff
+ magit-insert-xref-buttons)
+ "Hook run to insert sections into a `magit-diff-mode' buffer."
+ :package-version '(magit . "2.3.0")
+ :group 'magit-diff
+ :type 'hook)
+
+(defcustom magit-diff-expansion-threshold 60
+ "After how many seconds not to expand anymore diffs.
+
+Except in status buffers, diffs usually start out fully expanded.
+Because that can take a long time, all diffs that haven't been
+fontified during a refresh before the threshold defined here are
+instead displayed with their bodies collapsed.
+
+Note that this can cause sections that were previously expanded
+to be collapsed. So you should not pick a very low value here.
+
+The hook function `magit-diff-expansion-threshold' has to be a
+member of `magit-section-set-visibility-hook' for this option
+to have any effect."
+ :package-version '(magit . "2.9.0")
+ :group 'magit-diff
+ :type 'float)
+
+(defcustom magit-diff-highlight-hunk-body t
+ "Whether to highlight bodies of selected hunk sections.
+This only has an effect if `magit-diff-highlight' is a
+member of `magit-section-highlight-hook', which see."
+ :package-version '(magit . "2.1.0")
+ :group 'magit-diff
+ :type 'boolean)
+
+(defcustom magit-diff-highlight-hunk-region-functions
+ '(magit-diff-highlight-hunk-region-dim-outside
+ magit-diff-highlight-hunk-region-using-overlays)
+ "The functions used to highlight the hunk-internal region.
+
+`magit-diff-highlight-hunk-region-dim-outside' overlays the outside
+of the hunk internal selection with a face that causes the added and
+removed lines to have the same background color as context lines.
+This function should not be removed from the value of this option.
+
+`magit-diff-highlight-hunk-region-using-overlays' and
+`magit-diff-highlight-hunk-region-using-underline' emphasize the
+region by placing delimiting horizontal lines before and after it.
+The underline variant was implemented because Eli said that is
+how we should do it. However the overlay variant actually works
+better. Also see https://github.com/magit/magit/issues/2758.
+
+Instead of, or in addition to, using delimiting horizontal lines,
+to emphasize the boundaries, you may wish to emphasize the text
+itself, using `magit-diff-highlight-hunk-region-using-face'.
+
+In terminal frames it's not possible to draw lines as the overlay
+and underline variants normally do, so there they fall back to
+calling the face function instead."
+ :package-version '(magit . "2.9.0")
+ :set-after '(magit-diff-show-lines-boundaries)
+ :group 'magit-diff
+ :type 'hook
+ :options '(magit-diff-highlight-hunk-region-dim-outside
+ magit-diff-highlight-hunk-region-using-underline
+ magit-diff-highlight-hunk-region-using-overlays
+ magit-diff-highlight-hunk-region-using-face))
+
+(defcustom magit-diff-unmarked-lines-keep-foreground t
+ "Whether `magit-diff-highlight-hunk-region-dim-outside' preserves foreground.
+When this is set to nil, then that function only adjusts the
+foreground color but added and removed lines outside the region
+keep their distinct foreground colors."
+ :package-version '(magit . "2.9.0")
+ :group 'magit-diff
+ :type 'boolean)
+
+(defcustom magit-diff-refine-hunk nil
+ "Whether to show word-granularity differences within diff hunks.
+
+nil Never show fine differences.
+t Show fine differences for the current diff hunk only.
+`all' Show fine differences for all displayed diff hunks."
+ :group 'magit-diff
+ :safe (lambda (val) (memq val '(nil t all)))
+ :type '(choice (const :tag "Never" nil)
+ (const :tag "Current" t)
+ (const :tag "All" all)))
+
+(defcustom magit-diff-refine-ignore-whitespace smerge-refine-ignore-whitespace
+ "Whether to ignore whitespace changes in word-granularity differences."
+ :package-version '(magit . "3.0.0")
+ :set-after '(smerge-refine-ignore-whitespace)
+ :group 'magit-diff
+ :safe 'booleanp
+ :type 'boolean)
+
+(put 'magit-diff-refine-hunk 'permanent-local t)
+
+(defcustom magit-diff-adjust-tab-width nil
+ "Whether to adjust the width of tabs in diffs.
+
+Determining the correct width can be expensive if it requires
+opening large and/or many files, so the widths are cached in
+the variable `magit-diff--tab-width-cache'. Set that to nil
+to invalidate the cache.
+
+nil Never adjust tab width. Use `tab-width's value from
+ the Magit buffer itself instead.
+
+t If the corresponding file-visiting buffer exits, then
+ use `tab-width's value from that buffer. Doing this is
+ cheap, so this value is used even if a corresponding
+ cache entry exists.
+
+`always' If there is no such buffer, then temporarily visit the
+ file to determine the value.
+
+NUMBER Like `always', but don't visit files larger than NUMBER
+ bytes."
+ :package-version '(magit . "2.12.0")
+ :group 'magit-diff
+ :type '(choice (const :tag "Never" nil)
+ (const :tag "If file-visiting buffer exists" t)
+ (integer :tag "If file isn't larger than N bytes")
+ (const :tag "Always" always)))
+
+(defcustom magit-diff-paint-whitespace t
+ "Specify where to highlight whitespace errors.
+
+nil Never highlight whitespace errors.
+t Highlight whitespace errors everywhere.
+`uncommitted' Only highlight whitespace errors in diffs
+ showing uncommitted changes.
+
+For backward compatibility `status' is treated as a synonym
+for `uncommitted'.
+
+The option `magit-diff-paint-whitespace-lines' controls for
+what lines (added/remove/context) errors are highlighted.
+
+The options `magit-diff-highlight-trailing' and
+`magit-diff-highlight-indentation' control what kind of
+whitespace errors are highlighted."
+ :group 'magit-diff
+ :safe (lambda (val) (memq val '(t nil uncommitted status)))
+ :type '(choice (const :tag "In all diffs" t)
+ (const :tag "Only in uncommitted changes" uncommitted)
+ (const :tag "Never" nil)))
+
+(defcustom magit-diff-paint-whitespace-lines t
+ "Specify in what kind of lines to highlight whitespace errors.
+
+t Highlight only in added lines.
+`both' Highlight in added and removed lines.
+`all' Highlight in added, removed and context lines."
+ :package-version '(magit . "3.0.0")
+ :group 'magit-diff
+ :safe (lambda (val) (memq val '(t both all)))
+ :type '(choice (const :tag "in added lines" t)
+ (const :tag "in added and removed lines" both)
+ (const :tag "in added, removed and context lines" all)))
+
+(defcustom magit-diff-highlight-trailing t
+ "Whether to highlight whitespace at the end of a line in diffs.
+Used only when `magit-diff-paint-whitespace' is non-nil."
+ :group 'magit-diff
+ :safe 'booleanp
+ :type 'boolean)
+
+(defcustom magit-diff-highlight-indentation nil
+ "Highlight the \"wrong\" indentation style.
+Used only when `magit-diff-paint-whitespace' is non-nil.
+
+The value is an alist of the form ((REGEXP . INDENT)...). The
+path to the current repository is matched against each element
+in reverse order. Therefore if a REGEXP matches, then earlier
+elements are not tried.
+
+If the used INDENT is `tabs', highlight indentation with tabs.
+If INDENT is an integer, highlight indentation with at least
+that many spaces. Otherwise, highlight neither."
+ :group 'magit-diff
+ :type `(repeat (cons (string :tag "Directory regexp")
+ (choice (const :tag "Tabs" tabs)
+ (integer :tag "Spaces" :value ,tab-width)
+ (const :tag "Neither" nil)))))
+
+(defcustom magit-diff-hide-trailing-cr-characters
+ (and (memq system-type '(ms-dos windows-nt)) t)
+ "Whether to hide ^M characters at the end of a line in diffs."
+ :package-version '(magit . "2.6.0")
+ :group 'magit-diff
+ :type 'boolean)
+
+(defcustom magit-diff-highlight-keywords t
+ "Whether to highlight bracketed keywords in commit messages."
+ :package-version '(magit . "2.12.0")
+ :group 'magit-diff
+ :type 'boolean)
+
+(defcustom magit-diff-extra-stat-arguments nil
+ "Additional arguments to be used alongside `--stat'.
+
+A list of zero or more arguments or a function that takes no
+argument and returns such a list. These arguments are allowed
+here: `--stat-width', `--stat-name-width', `--stat-graph-width'
+and `--compact-summary'. See the git-diff(1) manpage."
+ :package-version '(magit . "3.0.0")
+ :group 'magit-diff
+ :type '(radio (function-item magit-diff-use-window-width-as-stat-width)
+ function
+ (list string)
+ (const :tag "None" nil)))
+
+;;;; File Diff
+
+(defcustom magit-diff-buffer-file-locked t
+ "Whether `magit-diff-buffer-file' uses a dedicated buffer."
+ :package-version '(magit . "2.7.0")
+ :group 'magit-commands
+ :group 'magit-diff
+ :type 'boolean)
+
+;;;; Revision Mode
+
+(defgroup magit-revision nil
+ "Inspect and manipulate Git commits."
+ :link '(info-link "(magit)Revision Buffer")
+ :group 'magit-modes)
+
+(defcustom magit-revision-mode-hook
+ '(bug-reference-mode
+ goto-address-mode)
+ "Hook run after entering Magit-Revision mode."
+ :group 'magit-revision
+ :type 'hook
+ :options '(bug-reference-mode
+ goto-address-mode))
+
+(defcustom magit-revision-sections-hook
+ '(magit-insert-revision-tag
+ magit-insert-revision-headers
+ magit-insert-revision-message
+ magit-insert-revision-notes
+ magit-insert-revision-diff
+ magit-insert-xref-buttons)
+ "Hook run to insert sections into a `magit-revision-mode' buffer."
+ :package-version '(magit . "2.3.0")
+ :group 'magit-revision
+ :type 'hook)
+
+(defcustom magit-revision-headers-format "\
+Author: %aN <%aE>
+AuthorDate: %ad
+Commit: %cN <%cE>
+CommitDate: %cd
+"
+ "Format string used to insert headers in revision buffers.
+
+All headers in revision buffers are inserted by the section
+inserter `magit-insert-revision-headers'. Some of the headers
+are created by calling `git show --format=FORMAT' where FORMAT
+is the format specified here. Other headers are hard coded or
+subject to option `magit-revision-insert-related-refs'."
+ :package-version '(magit . "2.3.0")
+ :group 'magit-revision
+ :type 'string)
+
+(defcustom magit-revision-insert-related-refs t
+ "Whether to show related branches in revision buffers
+
+`nil' Don't show any related branches.
+`t' Show related local branches.
+`all' Show related local and remote branches.
+`mixed' Show all containing branches and local merged branches."
+ :package-version '(magit . "2.1.0")
+ :group 'magit-revision
+ :type '(choice (const :tag "don't" nil)
+ (const :tag "local only" t)
+ (const :tag "all related" all)
+ (const :tag "all containing, local merged" mixed)))
+
+(defcustom magit-revision-use-hash-sections 'quicker
+ "Whether to turn hashes inside the commit message into sections.
+
+If non-nil, then hashes inside the commit message are turned into
+`commit' sections. There is a trade off to be made between
+performance and reliability:
+
+- `slow' calls git for every word to be absolutely sure.
+- `quick' skips words less than seven characters long.
+- `quicker' additionally skips words that don't contain a number.
+- `quickest' uses all words that are at least seven characters
+ long and which contain at least one number as well as at least
+ one letter.
+
+If nil, then no hashes are turned into sections, but you can
+still visit the commit at point using \"RET\"."
+ :package-version '(magit . "2.12.0")
+ :group 'magit-revision
+ :type '(choice (const :tag "Use sections, quickest" quickest)
+ (const :tag "Use sections, quicker" quicker)
+ (const :tag "Use sections, quick" quick)
+ (const :tag "Use sections, slow" slow)
+ (const :tag "Don't use sections" nil)))
+
+(defcustom magit-revision-show-gravatars nil
+ "Whether to show gravatar images in revision buffers.
+
+If nil, then don't insert any gravatar images. If t, then insert
+both images. If `author' or `committer', then insert only the
+respective image.
+
+If you have customized the option `magit-revision-header-format'
+and want to insert the images then you might also have to specify
+where to do so. In that case the value has to be a cons-cell of
+two regular expressions. The car specifies where to insert the
+author's image. The top half of the image is inserted right
+after the matched text, the bottom half on the next line in the
+same column. The cdr specifies where to insert the committer's
+image, accordingly. Either the car or the cdr may be nil."
+ :package-version '(magit . "2.3.0")
+ :group 'magit-revision
+ :type '(choice (const :tag "Don't show gravatars" nil)
+ (const :tag "Show gravatars" t)
+ (const :tag "Show author gravatar" author)
+ (const :tag "Show committer gravatar" committer)
+ (cons :tag "Show gravatars using custom pattern."
+ (regexp :tag "Author regexp" "^Author: ")
+ (regexp :tag "Committer regexp" "^Commit: "))))
+
+(defcustom magit-revision-use-gravatar-kludge nil
+ "Whether to work around a bug which affects display of gravatars.
+
+Gravatar images are spliced into two halves which are then
+displayed on separate lines. On OS X the splicing has a bug in
+some Emacs builds, which causes the top and bottom halves to be
+interchanged. Enabling this option works around this issue by
+interchanging the halves once more, which cancels out the effect
+of the bug.
+
+See https://github.com/magit/magit/issues/2265
+and https://debbugs.gnu.org/cgi/bugreport.cgi?bug=7847.
+
+Starting with Emacs 26.1 this kludge should not be required for
+any build."
+ :package-version '(magit . "2.3.0")
+ :group 'magit-revision
+ :type 'boolean)
+
+(defcustom magit-revision-fill-summary-line nil
+ "Whether to fill excessively long summary lines.
+
+If this is an integer, then the summary line is filled if it is
+longer than either the limit specified here or `window-width'.
+
+You may want to only set this locally in \".dir-locals-2.el\" for
+repositories known to contain bad commit messages.
+
+The body of the message is left alone because (a) most people who
+write excessively long summary lines usually don't add a body and
+(b) even people who have the decency to wrap their lines may have
+a good reason to include a long line in the body sometimes."
+ :package-version '(magit . "2.90.0")
+ :group 'magit-revision
+ :type '(choice (const :tag "Don't fill" nil)
+ (integer :tag "Fill if longer than")))
+
+(defcustom magit-revision-filter-files-on-follow nil
+ "Whether to honor file filter if log arguments include --follow.
+
+When a commit is displayed from a log buffer, the resulting
+revision buffer usually shares the log's file arguments,
+restricting the diff to those files. However, there's a
+complication when the log arguments include --follow: if the log
+follows a file across a rename event, keeping the file
+restriction would mean showing an empty diff in revision buffers
+for commits before the rename event.
+
+When this option is nil, the revision buffer ignores the log's
+filter if the log arguments include --follow. If non-nil, the
+log's file filter is always honored."
+ :package-version '(magit . "3.0.0")
+ :group 'magit-revision
+ :type 'boolean)
+
+;;;; Visit Commands
+
+(defcustom magit-diff-visit-previous-blob t
+ "Whether `magit-diff-visit-file' may visit the previous blob.
+
+When this is t and point is on a removed line in a diff for a
+committed change, then `magit-diff-visit-file' visits the blob
+from the last revision which still had that line.
+
+Currently this is only supported for committed changes, for
+staged and unstaged changes `magit-diff-visit-file' always
+visits the file in the working tree."
+ :package-version '(magit . "2.9.0")
+ :group 'magit-diff
+ :type 'boolean)
+
+(defcustom magit-diff-visit-avoid-head-blob nil
+ "Whether `magit-diff-visit-file' avoids visiting a blob from `HEAD'.
+
+By default `magit-diff-visit-file' always visits the blob that
+added the current line, while `magit-diff-visit-worktree-file'
+visits the respective file in the working tree. For the `HEAD'
+commit, the former command used to visit the worktree file too,
+but that made it impossible to visit a blob from `HEAD'.
+
+When point is on a removed line and that change has not been
+committed yet, then `magit-diff-visit-file' now visits the last
+blob that still had that line, which is a blob from `HEAD'.
+Previously this function used to visit the worktree file not
+only for added lines but also for such removed lines.
+
+If you prefer the old behaviors, then set this to t."
+ :package-version '(magit . "3.0.0")
+ :group 'magit-diff
+ :type 'boolean)
+
+;;; Faces
+
+(defface magit-diff-file-heading
+ `((t ,@(and (>= emacs-major-version 27) '(:extend t))
+ :weight bold))
+ "Face for diff file headings."
+ :group 'magit-faces)
+
+(defface magit-diff-file-heading-highlight
+ `((t ,@(and (>= emacs-major-version 27) '(:extend t))
+ :inherit magit-section-highlight))
+ "Face for current diff file headings."
+ :group 'magit-faces)
+
+(defface magit-diff-file-heading-selection
+ `((((class color) (background light))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :inherit magit-diff-file-heading-highlight
+ :foreground "salmon4")
+ (((class color) (background dark))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :inherit magit-diff-file-heading-highlight
+ :foreground "LightSalmon3"))
+ "Face for selected diff file headings."
+ :group 'magit-faces)
+
+(defface magit-diff-hunk-heading
+ `((((class color) (background light))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :background "grey90"
+ :foreground "grey20")
+ (((class color) (background dark))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :background "grey25"
+ :foreground "grey95"))
+ "Face for diff hunk headings."
+ :group 'magit-faces)
+
+(defface magit-diff-hunk-heading-highlight
+ `((((class color) (background light))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :background "grey80"
+ :foreground "grey20")
+ (((class color) (background dark))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :background "grey35"
+ :foreground "grey95"))
+ "Face for current diff hunk headings."
+ :group 'magit-faces)
+
+(defface magit-diff-hunk-heading-selection
+ `((((class color) (background light))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :inherit magit-diff-hunk-heading-highlight
+ :foreground "salmon4")
+ (((class color) (background dark))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :inherit magit-diff-hunk-heading-highlight
+ :foreground "LightSalmon3"))
+ "Face for selected diff hunk headings."
+ :group 'magit-faces)
+
+(defface magit-diff-hunk-region
+ `((t :inherit bold
+ ,@(and (>= emacs-major-version 27)
+ (list :extend (ignore-errors (face-attribute 'region :extend))))))
+ "Face used by `magit-diff-highlight-hunk-region-using-face'.
+
+This face is overlaid over text that uses other hunk faces,
+and those normally set the foreground and background colors.
+The `:foreground' and especially the `:background' properties
+should be avoided here. Setting the latter would cause the
+loss of information. Good properties to set here are `:weight'
+and `:slant'."
+ :group 'magit-faces)
+
+(defface magit-diff-revision-summary
+ '((t :inherit magit-diff-hunk-heading))
+ "Face for commit message summaries."
+ :group 'magit-faces)
+
+(defface magit-diff-revision-summary-highlight
+ '((t :inherit magit-diff-hunk-heading-highlight))
+ "Face for highlighted commit message summaries."
+ :group 'magit-faces)
+
+(defface magit-diff-lines-heading
+ `((((class color) (background light))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :inherit magit-diff-hunk-heading-highlight
+ :background "LightSalmon3")
+ (((class color) (background dark))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :inherit magit-diff-hunk-heading-highlight
+ :foreground "grey80"
+ :background "salmon4"))
+ "Face for diff hunk heading when lines are marked."
+ :group 'magit-faces)
+
+(defface magit-diff-lines-boundary
+ `((t ,@(and (>= emacs-major-version 27) '(:extend t)) ; !important
+ :inherit magit-diff-lines-heading))
+ "Face for boundary of marked lines in diff hunk."
+ :group 'magit-faces)
+
+(defface magit-diff-conflict-heading
+ '((t :inherit magit-diff-hunk-heading))
+ "Face for conflict markers."
+ :group 'magit-faces)
+
+(defface magit-diff-added
+ `((((class color) (background light))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :background "#ddffdd"
+ :foreground "#22aa22")
+ (((class color) (background dark))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :background "#335533"
+ :foreground "#ddffdd"))
+ "Face for lines in a diff that have been added."
+ :group 'magit-faces)
+
+(defface magit-diff-removed
+ `((((class color) (background light))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :background "#ffdddd"
+ :foreground "#aa2222")
+ (((class color) (background dark))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :background "#553333"
+ :foreground "#ffdddd"))
+ "Face for lines in a diff that have been removed."
+ :group 'magit-faces)
+
+(defface magit-diff-our
+ '((t :inherit magit-diff-removed))
+ "Face for lines in a diff for our side in a conflict."
+ :group 'magit-faces)
+
+(defface magit-diff-base
+ `((((class color) (background light))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :background "#ffffcc"
+ :foreground "#aaaa11")
+ (((class color) (background dark))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :background "#555522"
+ :foreground "#ffffcc"))
+ "Face for lines in a diff for the base side in a conflict."
+ :group 'magit-faces)
+
+(defface magit-diff-their
+ '((t :inherit magit-diff-added))
+ "Face for lines in a diff for their side in a conflict."
+ :group 'magit-faces)
+
+(defface magit-diff-context
+ `((((class color) (background light))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :foreground "grey50")
+ (((class color) (background dark))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :foreground "grey70"))
+ "Face for lines in a diff that are unchanged."
+ :group 'magit-faces)
+
+(defface magit-diff-added-highlight
+ `((((class color) (background light))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :background "#cceecc"
+ :foreground "#22aa22")
+ (((class color) (background dark))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :background "#336633"
+ :foreground "#cceecc"))
+ "Face for lines in a diff that have been added."
+ :group 'magit-faces)
+
+(defface magit-diff-removed-highlight
+ `((((class color) (background light))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :background "#eecccc"
+ :foreground "#aa2222")
+ (((class color) (background dark))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :background "#663333"
+ :foreground "#eecccc"))
+ "Face for lines in a diff that have been removed."
+ :group 'magit-faces)
+
+(defface magit-diff-our-highlight
+ '((t :inherit magit-diff-removed-highlight))
+ "Face for lines in a diff for our side in a conflict."
+ :group 'magit-faces)
+
+(defface magit-diff-base-highlight
+ `((((class color) (background light))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :background "#eeeebb"
+ :foreground "#aaaa11")
+ (((class color) (background dark))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :background "#666622"
+ :foreground "#eeeebb"))
+ "Face for lines in a diff for the base side in a conflict."
+ :group 'magit-faces)
+
+(defface magit-diff-their-highlight
+ '((t :inherit magit-diff-added-highlight))
+ "Face for lines in a diff for their side in a conflict."
+ :group 'magit-faces)
+
+(defface magit-diff-context-highlight
+ `((((class color) (background light))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :background "grey95"
+ :foreground "grey50")
+ (((class color) (background dark))
+ ,@(and (>= emacs-major-version 27) '(:extend t))
+ :background "grey20"
+ :foreground "grey70"))
+ "Face for lines in the current context in a diff."
+ :group 'magit-faces)
+
+(defface magit-diff-whitespace-warning
+ '((t :inherit trailing-whitespace))
+ "Face for highlighting whitespace errors added lines."
+ :group 'magit-faces)
+
+(defface magit-diffstat-added
+ '((((class color) (background light)) :foreground "#22aa22")
+ (((class color) (background dark)) :foreground "#448844"))
+ "Face for plus sign in diffstat."
+ :group 'magit-faces)
+
+(defface magit-diffstat-removed
+ '((((class color) (background light)) :foreground "#aa2222")
+ (((class color) (background dark)) :foreground "#aa4444"))
+ "Face for minus sign in diffstat."
+ :group 'magit-faces)
+
+;;; Arguments
+;;;; Prefix Classes
+
+(defclass magit-diff-prefix (transient-prefix)
+ ((history-key :initform 'magit-diff)
+ (major-mode :initform 'magit-diff-mode)))
+
+(defclass magit-diff-refresh-prefix (magit-diff-prefix)
+ ((history-key :initform 'magit-diff)
+ (major-mode :initform nil)))
+
+;;;; Prefix Methods
+
+(cl-defmethod transient-init-value ((obj magit-diff-prefix))
+ (pcase-let ((`(,args ,files)
+ (magit-diff--get-value 'magit-diff-mode
+ magit-prefix-use-buffer-arguments)))
+ (unless (eq transient-current-command 'magit-dispatch)
+ (when-let ((file (magit-file-relative-name)))
+ (setq files (list file))))
+ (oset obj value (if files `(("--" ,@files) ,args) args))))
+
+(cl-defmethod transient-init-value ((obj magit-diff-refresh-prefix))
+ (oset obj value (if magit-buffer-diff-files
+ `(("--" ,@magit-buffer-diff-files)
+ ,magit-buffer-diff-args)
+ magit-buffer-diff-args)))
+
+(cl-defmethod transient-set-value ((obj magit-diff-prefix))
+ (magit-diff--set-value obj))
+
+(cl-defmethod transient-save-value ((obj magit-diff-prefix))
+ (magit-diff--set-value obj 'save))
+
+;;;; Argument Access
+
+(defun magit-diff-arguments (&optional mode)
+ "Return the current diff arguments."
+ (if (memq transient-current-command '(magit-diff magit-diff-refresh))
+ (pcase-let ((`(,args ,alist)
+ (-separate #'atom (transient-get-value))))
+ (list args (cdr (assoc "--" alist))))
+ (magit-diff--get-value (or mode 'magit-diff-mode))))
+
+(defun magit-diff--get-value (mode &optional use-buffer-args)
+ (unless use-buffer-args
+ (setq use-buffer-args magit-direct-use-buffer-arguments))
+ (let (args files)
+ (cond
+ ((and (memq use-buffer-args '(always selected current))
+ (eq major-mode mode))
+ (setq args magit-buffer-diff-args)
+ (setq files magit-buffer-diff-files))
+ ((and (memq use-buffer-args '(always selected))
+ (when-let ((buffer (magit-get-mode-buffer
+ mode nil
+ (eq use-buffer-args 'selected))))
+ (setq args (buffer-local-value 'magit-buffer-diff-args buffer))
+ (setq files (buffer-local-value 'magit-buffer-diff-files buffer))
+ t)))
+ ((plist-member (symbol-plist mode) 'magit-diff-current-arguments)
+ (setq args (get mode 'magit-diff-current-arguments)))
+ ((when-let ((elt (assq (intern (format "magit-diff:%s" mode))
+ transient-values)))
+ (setq args (cdr elt))
+ t))
+ (t
+ (setq args (get mode 'magit-diff-default-arguments))))
+ (list args files)))
+
+(defun magit-diff--set-value (obj &optional save)
+ (pcase-let* ((obj (oref obj prototype))
+ (mode (or (oref obj major-mode) major-mode))
+ (key (intern (format "magit-diff:%s" mode)))
+ (`(,args ,alist)
+ (-separate #'atom (transient-get-value)))
+ (files (cdr (assoc "--" alist))))
+ (put mode 'magit-diff-current-arguments args)
+ (when save
+ (setf (alist-get key transient-values) args)
+ (transient-save-values))
+ (transient--history-push obj)
+ (setq magit-buffer-diff-args args)
+ (setq magit-buffer-diff-files files)
+ (magit-refresh)))
+
+;;; Commands
+;;;; Prefix Commands
+
+;;;###autoload (autoload 'magit-diff "magit-diff" nil t)
+(transient-define-prefix magit-diff ()
+ "Show changes between different versions."
+ :man-page "git-diff"
+ :class 'magit-diff-prefix
+ ["Limit arguments"
+ (magit:--)
+ (magit-diff:--ignore-submodules)
+ ("-b" "Ignore whitespace changes" ("-b" "--ignore-space-change"))
+ ("-w" "Ignore all whitespace" ("-w" "--ignore-all-space"))
+ (5 "-D" "Omit preimage for deletes" ("-D" "--irreversible-delete"))]
+ ["Context arguments"
+ (magit-diff:-U)
+ ("-W" "Show surrounding functions" ("-W" "--function-context"))]
+ ["Tune arguments"
+ (magit-diff:--diff-algorithm)
+ (magit-diff:-M)
+ (magit-diff:-C)
+ ("-x" "Disallow external diff drivers" "--no-ext-diff")
+ ("-s" "Show stats" "--stat")
+ ("=g" "Show signature" "--show-signature")
+ (5 "-R" "Reverse sides" "-R")
+ (5 magit-diff:--color-moved)
+ (5 magit-diff:--color-moved-ws)]
+ ["Actions"
+ [("d" "Dwim" magit-diff-dwim)
+ ("r" "Diff range" magit-diff-range)
+ ("p" "Diff paths" magit-diff-paths)]
+ [("u" "Diff unstaged" magit-diff-unstaged)
+ ("s" "Diff staged" magit-diff-staged)
+ ("w" "Diff worktree" magit-diff-working-tree)]
+ [("c" "Show commit" magit-show-commit)
+ ("t" "Show stash" magit-stash-show)]])
+
+;;;###autoload (autoload 'magit-diff-refresh "magit-diff" nil t)
+(transient-define-prefix magit-diff-refresh ()
+ "Change the arguments used for the diff(s) in the current buffer."
+ :man-page "git-diff"
+ :class 'magit-diff-refresh-prefix
+ ["Limit arguments"
+ (magit:--)
+ (magit-diff:--ignore-submodules)
+ ("-b" "Ignore whitespace changes" ("-b" "--ignore-space-change"))
+ ("-w" "Ignore all whitespace" ("-w" "--ignore-all-space"))
+ (5 "-D" "Omit preimage for deletes" ("-D" "--irreversible-delete"))]
+ ["Context arguments"
+ (magit-diff:-U)
+ ("-W" "Show surrounding functions" ("-W" "--function-context"))]
+ ["Tune arguments"
+ (magit-diff:--diff-algorithm)
+ (magit-diff:-M)
+ (magit-diff:-C)
+ ("-x" "Disallow external diff drivers" "--no-ext-diff")
+ ("-s" "Show stats" "--stat"
+ :if-derived magit-diff-mode)
+ ("=g" "Show signature" "--show-signature"
+ :if-derived magit-diff-mode)
+ (5 "-R" "Reverse sides" "-R"
+ :if-derived magit-diff-mode)
+ (5 magit-diff:--color-moved)
+ (5 magit-diff:--color-moved-ws)]
+ [["Refresh"
+ ("g" "buffer" magit-diff-refresh)
+ ("s" "buffer and set defaults" transient-set :transient nil)
+ ("w" "buffer and save defaults" transient-save :transient nil)]
+ ["Toggle"
+ ("t" "hunk refinement" magit-diff-toggle-refine-hunk)
+ ("F" "file filter" magit-diff-toggle-file-filter)
+ ("b" "buffer lock" magit-toggle-buffer-lock
+ :if-mode (magit-diff-mode magit-revision-mode magit-stash-mode))]
+ [:if-mode magit-diff-mode
+ :description "Do"
+ ("r" "switch range type" magit-diff-switch-range-type)
+ ("f" "flip revisions" magit-diff-flip-revs)]]
+ (interactive)
+ (if (not (eq transient-current-command 'magit-diff-refresh))
+ (transient-setup 'magit-diff-refresh)
+ (pcase-let ((`(,args ,files) (magit-diff-arguments)))
+ (setq magit-buffer-diff-args args)
+ (setq magit-buffer-diff-files files))
+ (magit-refresh)))
+
+;;;; Infix Commands
+
+(transient-define-argument magit:-- ()
+ :description "Limit to files"
+ :class 'transient-files
+ :key "--"
+ :argument "--"
+ :prompt "Limit to file,s: "
+ :reader #'magit-read-files
+ :multi-value t)
+
+(defun magit-read-files (prompt initial-input history &optional list-fn)
+ (magit-completing-read-multiple* prompt
+ (funcall (or list-fn #'magit-list-files))
+ nil nil
+ (or initial-input (magit-file-at-point))
+ history))
+
+(transient-define-argument magit-diff:-U ()
+ :description "Context lines"
+ :class 'transient-option
+ :argument "-U"
+ :reader #'transient-read-number-N0)
+
+(transient-define-argument magit-diff:-M ()
+ :description "Detect renames"
+ :class 'transient-option
+ :argument "-M"
+ :allow-empty t
+ :reader #'transient-read-number-N+)
+
+(transient-define-argument magit-diff:-C ()
+ :description "Detect copies"
+ :class 'transient-option
+ :argument "-C"
+ :allow-empty t
+ :reader #'transient-read-number-N+)
+
+(transient-define-argument magit-diff:--diff-algorithm ()
+ :description "Diff algorithm"
+ :class 'transient-option
+ :key "-A"
+ :argument "--diff-algorithm="
+ :reader #'magit-diff-select-algorithm)
+
+(defun magit-diff-select-algorithm (&rest _ignore)
+ (magit-read-char-case nil t
+ (?d "[d]efault" "default")
+ (?m "[m]inimal" "minimal")
+ (?p "[p]atience" "patience")
+ (?h "[h]istogram" "histogram")))
+
+(transient-define-argument magit-diff:--ignore-submodules ()
+ :description "Ignore submodules"
+ :class 'transient-option
+ :key "-i"
+ :argument "--ignore-submodules="
+ :reader #'magit-diff-select-ignore-submodules)
+
+(defun magit-diff-select-ignore-submodules (&rest _ignored)
+ (magit-read-char-case "Ignore submodules " t
+ (?u "[u]ntracked" "untracked")
+ (?d "[d]irty" "dirty")
+ (?a "[a]ll" "all")))
+
+(transient-define-argument magit-diff:--color-moved ()
+ :description "Color moved lines"
+ :class 'transient-option
+ :key "-m"
+ :argument "--color-moved="
+ :reader #'magit-diff-select-color-moved-mode)
+
+(defun magit-diff-select-color-moved-mode (&rest _ignore)
+ (magit-read-char-case "Color moved " t
+ (?d "[d]efault" "default")
+ (?p "[p]lain" "plain")
+ (?b "[b]locks" "blocks")
+ (?z "[z]ebra" "zebra")
+ (?Z "[Z] dimmed-zebra" "dimmed-zebra")))
+
+(transient-define-argument magit-diff:--color-moved-ws ()
+ :description "Whitespace treatment for --color-moved"
+ :class 'transient-option
+ :key "=w"
+ :argument "--color-moved-ws="
+ :reader #'magit-diff-select-color-moved-ws-mode)
+
+(defun magit-diff-select-color-moved-ws-mode (&rest _ignore)
+ (magit-read-char-case "Ignore whitespace " t
+ (?i "[i]ndentation" "allow-indentation-change")
+ (?e "[e]nd of line" "ignore-space-at-eol")
+ (?s "[s]pace change" "ignore-space-change")
+ (?a "[a]ll space" "ignore-all-space")
+ (?n "[n]o" "no")))
+
+;;;; Setup Commands
+
+;;;###autoload
+(defun magit-diff-dwim (&optional args files)
+ "Show changes for the thing at point."
+ (interactive (magit-diff-arguments))
+ (let ((default-directory default-directory)
+ (section (magit-current-section)))
+ (cond
+ ((magit-section-match 'module section)
+ (setq default-directory
+ (expand-file-name
+ (file-name-as-directory (oref section value))))
+ (magit-diff-range (oref section range)))
+ (t
+ (when (magit-section-match 'module-commit section)
+ (setq args nil)
+ (setq files nil)
+ (setq default-directory
+ (expand-file-name
+ (file-name-as-directory (magit-section-parent-value section)))))
+ (pcase (magit-diff--dwim)
+ ('unmerged (magit-diff-unmerged args files))
+ ('unstaged (magit-diff-unstaged args files))
+ ('staged
+ (let ((file (magit-file-at-point)))
+ (if (and file (equal (cddr (car (magit-file-status file))) '(?D ?U)))
+ ;; File was deleted by us and modified by them. Show the latter.
+ (magit-diff-unmerged args (list file))
+ (magit-diff-staged nil args files))))
+ (`(stash . ,value) (magit-stash-show value args))
+ (`(commit . ,value)
+ (magit-diff-range (format "%s^..%s" value value) args files))
+ ((and range (pred stringp))
+ (magit-diff-range range args files))
+ (_ (call-interactively #'magit-diff-range)))))))
+
+(defun magit-diff--dwim ()
+ "Return information for performing DWIM diff.
+
+The information can be in three forms:
+1. TYPE
+ A symbol describing a type of diff where no additional information
+ is needed to generate the diff. Currently, this includes `staged',
+ `unstaged' and `unmerged'.
+2. (TYPE . VALUE)
+ Like #1 but the diff requires additional information, which is
+ given by VALUE. Currently, this includes `commit' and `stash',
+ where VALUE is the given commit or stash, respectively.
+3. RANGE
+ A string indicating a diff range.
+
+If no DWIM context is found, nil is returned."
+ (cond
+ ((when-let* ((commits (magit-region-values '(commit branch) t)))
+ ;; Cannot use and-let* because of debbugs#31840.
+ (deactivate-mark)
+ (concat (car (last commits)) ".." (car commits))))
+ (magit-buffer-refname
+ (cons 'commit magit-buffer-refname))
+ ((derived-mode-p 'magit-stash-mode)
+ (cons 'commit
+ (magit-section-case
+ (commit (oref it value))
+ (file (thread-first it
+ (oref parent)
+ (oref value)))
+ (hunk (thread-first it
+ (oref parent)
+ (oref parent)
+ (oref value))))))
+ ((derived-mode-p 'magit-revision-mode)
+ (cons 'commit magit-buffer-revision))
+ ((derived-mode-p 'magit-diff-mode)
+ magit-buffer-range)
+ (t
+ (magit-section-case
+ ([* unstaged] 'unstaged)
+ ([* staged] 'staged)
+ (unmerged 'unmerged)
+ (unpushed (magit-diff--range-to-endpoints (oref it value)))
+ (unpulled (magit-diff--range-to-endpoints (oref it value)))
+ (branch (let ((current (magit-get-current-branch))
+ (atpoint (oref it value)))
+ (if (equal atpoint current)
+ (--if-let (magit-get-upstream-branch)
+ (format "%s...%s" it current)
+ (if (magit-anything-modified-p)
+ current
+ (cons 'commit current)))
+ (format "%s...%s"
+ (or current "HEAD")
+ atpoint))))
+ (commit (cons 'commit (oref it value)))
+ ([file commit] (cons 'commit (oref (oref it parent) value)))
+ ([hunk file commit]
+ (cons 'commit (oref (oref (oref it parent) parent) value)))
+ (stash (cons 'stash (oref it value)))
+ (pullreq (forge--pullreq-range (oref it value) t))))))
+
+(defun magit-diff--range-to-endpoints (range)
+ (cond ((string-match "\\.\\.\\." range) (replace-match ".." nil nil range))
+ ((string-match "\\.\\." range) (replace-match "..." nil nil range))
+ (t range)))
+
+(defun magit-diff--region-range (&optional interactive mbase)
+ (when-let* ((commits (magit-region-values '(commit branch) t)) ;debbugs#31840
+ (revA (car (last commits)))
+ (revB (car commits)))
+ (when interactive
+ (deactivate-mark))
+ (if mbase
+ (let ((base (magit-git-string "merge-base" revA revB)))
+ (cond
+ ((string= (magit-rev-parse revA) base)
+ (format "%s..%s" revA revB))
+ ((string= (magit-rev-parse revB) base)
+ (format "%s..%s" revB revA))
+ (interactive
+ (let ((main (magit-completing-read "View changes along"
+ (list revA revB)
+ nil t nil nil revB)))
+ (format "%s...%s"
+ (if (string= main revB) revA revB) main)))
+ (t "%s...%s" revA revB)))
+ (format "%s..%s" revA revB))))
+
+(defun magit-diff-read-range-or-commit (prompt &optional secondary-default mbase)
+ "Read range or revision with special diff range treatment.
+If MBASE is non-nil, prompt for which rev to place at the end of
+a \"revA...revB\" range. Otherwise, always construct
+\"revA..revB\" range."
+ (or (magit-diff--region-range t mbase)
+ (magit-read-range prompt
+ (or (pcase (magit-diff--dwim)
+ (`(commit . ,value)
+ (format "%s^..%s" value value))
+ ((and range (pred stringp))
+ range))
+ secondary-default
+ (magit-get-current-branch)))))
+
+;;;###autoload
+(defun magit-diff-range (rev-or-range &optional args files)
+ "Show differences between two commits.
+
+REV-OR-RANGE should be a range or a single revision. If it is a
+revision, then show changes in the working tree relative to that
+revision. If it is a range, but one side is omitted, then show
+changes relative to `HEAD'.
+
+If the region is active, use the revisions on the first and last
+line of the region as the two sides of the range. With a prefix
+argument, instead of diffing the revisions, choose a revision to
+view changes along, starting at the common ancestor of both
+revisions (i.e., use a \"...\" range)."
+ (interactive (cons (magit-diff-read-range-or-commit "Diff for range"
+ nil current-prefix-arg)
+ (magit-diff-arguments)))
+ (magit-diff-setup-buffer rev-or-range nil args files))
+
+;;;###autoload
+(defun magit-diff-working-tree (&optional rev args files)
+ "Show changes between the current working tree and the `HEAD' commit.
+With a prefix argument show changes between the working tree and
+a commit read from the minibuffer."
+ (interactive
+ (cons (and current-prefix-arg
+ (magit-read-branch-or-commit "Diff working tree and commit"))
+ (magit-diff-arguments)))
+ (magit-diff-setup-buffer (or rev "HEAD") nil args files))
+
+;;;###autoload
+(defun magit-diff-staged (&optional rev args files)
+ "Show changes between the index and the `HEAD' commit.
+With a prefix argument show changes between the index and
+a commit read from the minibuffer."
+ (interactive
+ (cons (and current-prefix-arg
+ (magit-read-branch-or-commit "Diff index and commit"))
+ (magit-diff-arguments)))
+ (magit-diff-setup-buffer rev "--cached" args files))
+
+;;;###autoload
+(defun magit-diff-unstaged (&optional args files)
+ "Show changes between the working tree and the index."
+ (interactive (magit-diff-arguments))
+ (magit-diff-setup-buffer nil nil args files))
+
+;;;###autoload
+(defun magit-diff-unmerged (&optional args files)
+ "Show changes that are being merged."
+ (interactive (magit-diff-arguments))
+ (unless (magit-merge-in-progress-p)
+ (user-error "No merge is in progress"))
+ (magit-diff-setup-buffer (magit--merge-range) nil args files))
+
+;;;###autoload
+(defun magit-diff-while-committing (&optional args)
+ "While committing, show the changes that are about to be committed.
+While amending, invoking the command again toggles between
+showing just the new changes or all the changes that will
+be committed."
+ (interactive (list (car (magit-diff-arguments))))
+ (unless (magit-commit-message-buffer)
+ (user-error "No commit in progress"))
+ (let ((magit-display-buffer-noselect t))
+ (if-let ((diff-buf (magit-get-mode-buffer 'magit-diff-mode 'selected)))
+ (with-current-buffer diff-buf
+ (cond ((and (equal magit-buffer-range "HEAD^")
+ (equal magit-buffer-typearg "--cached"))
+ (magit-diff-staged nil args))
+ ((and (equal magit-buffer-range nil)
+ (equal magit-buffer-typearg "--cached"))
+ (magit-diff-while-amending args))
+ ((magit-anything-staged-p)
+ (magit-diff-staged nil args))
+ (t
+ (magit-diff-while-amending args))))
+ (if (magit-anything-staged-p)
+ (magit-diff-staged nil args)
+ (magit-diff-while-amending args)))))
+
+(define-key git-commit-mode-map
+ (kbd "C-c C-d") #'magit-diff-while-committing)
+
+(defun magit-diff-while-amending (&optional args)
+ (magit-diff-setup-buffer "HEAD^" "--cached" args nil))
+
+;;;###autoload
+(defun magit-diff-buffer-file ()
+ "Show diff for the blob or file visited in the current buffer.
+
+When the buffer visits a blob, then show the respective commit.
+When the buffer visits a file, then show the differenced between
+`HEAD' and the working tree. In both cases limit the diff to
+the file or blob."
+ (interactive)
+ (require 'magit)
+ (if-let ((file (magit-file-relative-name)))
+ (if magit-buffer-refname
+ (magit-show-commit magit-buffer-refname
+ (car (magit-show-commit--arguments))
+ (list file))
+ (save-buffer)
+ (let ((line (line-number-at-pos))
+ (col (current-column)))
+ (with-current-buffer
+ (magit-diff-setup-buffer (or (magit-get-current-branch) "HEAD")
+ nil
+ (car (magit-diff-arguments))
+ (list file)
+ magit-diff-buffer-file-locked)
+ (magit-diff--goto-position file line col))))
+ (user-error "Buffer isn't visiting a file")))
+
+;;;###autoload
+(defun magit-diff-paths (a b)
+ "Show changes between any two files on disk."
+ (interactive (list (read-file-name "First file: " nil nil t)
+ (read-file-name "Second file: " nil nil t)))
+ (magit-diff-setup-buffer nil "--no-index"
+ nil (list (magit-convert-filename-for-git
+ (expand-file-name a))
+ (magit-convert-filename-for-git
+ (expand-file-name b)))))
+
+(defun magit-show-commit--arguments ()
+ (pcase-let ((`(,args ,diff-files)
+ (magit-diff-arguments 'magit-revision-mode)))
+ (list args (if (derived-mode-p 'magit-log-mode)
+ (and (or magit-revision-filter-files-on-follow
+ (not (member "--follow" magit-buffer-log-args)))
+ magit-buffer-log-files)
+ diff-files))))
+
+;;;###autoload
+(defun magit-show-commit (rev &optional args files module)
+ "Visit the revision at point in another buffer.
+If there is no revision at point or with a prefix argument prompt
+for a revision."
+ (interactive
+ (pcase-let* ((mcommit (magit-section-value-if 'module-commit))
+ (atpoint (or mcommit
+ (magit-thing-at-point 'git-revision t)
+ (magit-branch-or-commit-at-point)))
+ (`(,args ,files) (magit-show-commit--arguments)))
+ (list (or (and (not current-prefix-arg) atpoint)
+ (magit-read-branch-or-commit "Show commit" atpoint))
+ args
+ files
+ (and mcommit
+ (magit-section-parent-value (magit-current-section))))))
+ (require 'magit)
+ (let ((file (magit-file-relative-name)))
+ (magit-with-toplevel
+ (when module
+ (setq default-directory
+ (expand-file-name (file-name-as-directory module))))
+ (unless (magit-commit-p rev)
+ (user-error "%s is not a commit" rev))
+ (let ((buf (magit-revision-setup-buffer rev args files)))
+ (when file
+ (save-buffer)
+ (let ((line (magit-diff-visit--offset file (list "-R" rev)
+ (line-number-at-pos)))
+ (col (current-column)))
+ (with-current-buffer buf
+ (magit-diff--goto-position file line col))))))))
+
+(defun magit-diff--locate-hunk (file line &optional parent)
+ (and-let* ((diff (cl-find-if (lambda (section)
+ (and (cl-typep section 'magit-file-section)
+ (equal (oref section value) file)))
+ (oref (or parent magit-root-section) children))))
+ (let (hunk (hunks (oref diff children)))
+ (cl-block nil
+ (while (setq hunk (pop hunks))
+ (when-let ((range (oref hunk to-range)))
+ (pcase-let* ((`(,beg ,len) range)
+ (end (+ beg len)))
+ (cond ((> beg line) (cl-return (list diff nil)))
+ ((<= beg line end) (cl-return (list hunk t)))
+ ((null hunks) (cl-return (list hunk nil)))))))))))
+
+(defun magit-diff--goto-position (file line column &optional parent)
+ (when-let ((pos (magit-diff--locate-hunk file line parent)))
+ (pcase-let ((`(,section ,exact) pos))
+ (cond ((cl-typep section 'magit-file-section)
+ (goto-char (oref section start)))
+ (exact
+ (goto-char (oref section content))
+ (let ((pos (car (oref section to-range))))
+ (while (or (< pos line)
+ (= (char-after) ?-))
+ (unless (= (char-after) ?-)
+ (cl-incf pos))
+ (forward-line)))
+ (forward-char (1+ column)))
+ (t
+ (goto-char (oref section start))
+ (setq section (oref section parent))))
+ (while section
+ (when (oref section hidden)
+ (magit-section-show section))
+ (setq section (oref section parent))))
+ (magit-section-update-highlight)
+ t))
+
+;;;; Setting Commands
+
+(defun magit-diff-switch-range-type ()
+ "Convert diff range type.
+Change \"revA..revB\" to \"revA...revB\", or vice versa."
+ (interactive)
+ (if (and magit-buffer-range
+ (derived-mode-p 'magit-diff-mode)
+ (string-match magit-range-re magit-buffer-range))
+ (setq magit-buffer-range
+ (replace-match (if (string= (match-string 2 magit-buffer-range) "..")
+ "..."
+ "..")
+ t t magit-buffer-range 2))
+ (user-error "No range to change"))
+ (magit-refresh))
+
+(defun magit-diff-flip-revs ()
+ "Swap revisions in diff range.
+Change \"revA..revB\" to \"revB..revA\"."
+ (interactive)
+ (if (and magit-buffer-range
+ (derived-mode-p 'magit-diff-mode)
+ (string-match magit-range-re magit-buffer-range))
+ (progn
+ (setq magit-buffer-range
+ (concat (match-string 3 magit-buffer-range)
+ (match-string 2 magit-buffer-range)
+ (match-string 1 magit-buffer-range)))
+ (magit-refresh))
+ (user-error "No range to swap")))
+
+(defun magit-diff-toggle-file-filter ()
+ "Toggle the file restriction of the current buffer's diffs.
+If the current buffer's mode is derived from `magit-log-mode',
+toggle the file restriction in the repository's revision buffer
+instead."
+ (interactive)
+ (cl-flet ((toggle ()
+ (if (or magit-buffer-diff-files
+ magit-buffer-diff-files-suspended)
+ (cl-rotatef magit-buffer-diff-files
+ magit-buffer-diff-files-suspended)
+ (setq magit-buffer-diff-files
+ (transient-infix-read 'magit:--)))
+ (magit-refresh)))
+ (cond
+ ((derived-mode-p 'magit-log-mode
+ 'magit-cherry-mode
+ 'magit-reflog-mode)
+ (if-let ((buffer (magit-get-mode-buffer 'magit-revision-mode)))
+ (with-current-buffer buffer (toggle))
+ (message "No revision buffer")))
+ ((local-variable-p 'magit-buffer-diff-files)
+ (toggle))
+ (t
+ (user-error "Cannot toggle file filter in this buffer")))))
+
+(defun magit-diff-less-context (&optional count)
+ "Decrease the context for diff hunks by COUNT lines."
+ (interactive "p")
+ (magit-diff-set-context (lambda (cur) (max 0 (- (or cur 0) count)))))
+
+(defun magit-diff-more-context (&optional count)
+ "Increase the context for diff hunks by COUNT lines."
+ (interactive "p")
+ (magit-diff-set-context (lambda (cur) (+ (or cur 0) count))))
+
+(defun magit-diff-default-context ()
+ "Reset context for diff hunks to the default height."
+ (interactive)
+ (magit-diff-set-context #'ignore))
+
+(defun magit-diff-set-context (fn)
+ (let* ((def (--if-let (magit-get "diff.context") (string-to-number it) 3))
+ (val magit-buffer-diff-args)
+ (arg (--first (string-match "^-U\\([0-9]+\\)?$" it) val))
+ (num (--if-let (and arg (match-string 1 arg)) (string-to-number it) def))
+ (val (delete arg val))
+ (num (funcall fn num))
+ (arg (and num (not (= num def)) (format "-U%i" num)))
+ (val (if arg (cons arg val) val)))
+ (setq magit-buffer-diff-args val))
+ (magit-refresh))
+
+(defun magit-diff-context-p ()
+ (if-let ((arg (--first (string-match "^-U\\([0-9]+\\)$" it)
+ magit-buffer-diff-args)))
+ (not (equal arg "-U0"))
+ t))
+
+(defun magit-diff-ignore-any-space-p ()
+ (--any-p (member it magit-buffer-diff-args)
+ '("--ignore-cr-at-eol"
+ "--ignore-space-at-eol"
+ "--ignore-space-change" "-b"
+ "--ignore-all-space" "-w"
+ "--ignore-blank-space")))
+
+(defun magit-diff-toggle-refine-hunk (&optional style)
+ "Turn diff-hunk refining on or off.
+
+If hunk refining is currently on, then hunk refining is turned off.
+If hunk refining is off, then hunk refining is turned on, in
+`selected' mode (only the currently selected hunk is refined).
+
+With a prefix argument, the \"third choice\" is used instead:
+If hunk refining is currently on, then refining is kept on, but
+the refining mode (`selected' or `all') is switched.
+If hunk refining is off, then hunk refining is turned on, in
+`all' mode (all hunks refined).
+
+Customize variable `magit-diff-refine-hunk' to change the default mode."
+ (interactive "P")
+ (setq-local magit-diff-refine-hunk
+ (if style
+ (if (eq magit-diff-refine-hunk 'all) t 'all)
+ (not magit-diff-refine-hunk)))
+ (magit-diff-update-hunk-refinement))
+
+;;;; Visit Commands
+;;;;; Dwim Variants
+
+(defun magit-diff-visit-file (file &optional other-window)
+ "From a diff visit the appropriate version of FILE.
+
+Display the buffer in the selected window. With a prefix
+argument OTHER-WINDOW display the buffer in another window
+instead.
+
+Visit the worktree version of the appropriate file. The location
+of point inside the diff determines which file is being visited.
+The visited version depends on what changes the diff is about.
+
+1. If the diff shows uncommitted changes (i.e. stage or unstaged
+ changes), then visit the file in the working tree (i.e. the
+ same \"real\" file that `find-file' would visit. In all other
+ cases visit a \"blob\" (i.e. the version of a file as stored
+ in some commit).
+
+2. If point is on a removed line, then visit the blob for the
+ first parent of the commit that removed that line, i.e. the
+ last commit where that line still exists.
+
+3. If point is on an added or context line, then visit the blob
+ that adds that line, or if the diff shows from more than a
+ single commit, then visit the blob from the last of these
+ commits.
+
+In the file-visiting buffer also go to the line that corresponds
+to the line that point is on in the diff.
+
+Note that this command only works if point is inside a diff.
+In other cases `magit-find-file' (which see) has to be used."
+ (interactive (list (magit-file-at-point t t) current-prefix-arg))
+ (magit-diff-visit-file--internal file nil
+ (if other-window
+ #'switch-to-buffer-other-window
+ #'pop-to-buffer-same-window)))
+
+(defun magit-diff-visit-file-other-window (file)
+ "From a diff visit the appropriate version of FILE in another window.
+Like `magit-diff-visit-file' but use
+`switch-to-buffer-other-window'."
+ (interactive (list (magit-file-at-point t t)))
+ (magit-diff-visit-file--internal file nil #'switch-to-buffer-other-window))
+
+(defun magit-diff-visit-file-other-frame (file)
+ "From a diff visit the appropriate version of FILE in another frame.
+Like `magit-diff-visit-file' but use
+`switch-to-buffer-other-frame'."
+ (interactive (list (magit-file-at-point t t)))
+ (magit-diff-visit-file--internal file nil #'switch-to-buffer-other-frame))
+
+;;;;; Worktree Variants
+
+(defun magit-diff-visit-worktree-file (file &optional other-window)
+ "From a diff visit the worktree version of FILE.
+
+Display the buffer in the selected window. With a prefix
+argument OTHER-WINDOW display the buffer in another window
+instead.
+
+Visit the worktree version of the appropriate file. The location
+of point inside the diff determines which file is being visited.
+
+Unlike `magit-diff-visit-file' always visits the \"real\" file in
+the working tree, i.e the \"current version\" of the file.
+
+In the file-visiting buffer also go to the line that corresponds
+to the line that point is on in the diff. Lines that were added
+or removed in the working tree, the index and other commits in
+between are automatically accounted for."
+ (interactive (list (magit-file-at-point t t) current-prefix-arg))
+ (magit-diff-visit-file--internal file t
+ (if other-window
+ #'switch-to-buffer-other-window
+ #'pop-to-buffer-same-window)))
+
+(defun magit-diff-visit-worktree-file-other-window (file)
+ "From a diff visit the worktree version of FILE in another window.
+Like `magit-diff-visit-worktree-file' but use
+`switch-to-buffer-other-window'."
+ (interactive (list (magit-file-at-point t t)))
+ (magit-diff-visit-file--internal file t #'switch-to-buffer-other-window))
+
+(defun magit-diff-visit-worktree-file-other-frame (file)
+ "From a diff visit the worktree version of FILE in another frame.
+Like `magit-diff-visit-worktree-file' but use
+`switch-to-buffer-other-frame'."
+ (interactive (list (magit-file-at-point t t)))
+ (magit-diff-visit-file--internal file t #'switch-to-buffer-other-frame))
+
+;;;;; Internal
+
+(defun magit-diff-visit-file--internal (file force-worktree fn)
+ "From a diff visit the appropriate version of FILE.
+If FORCE-WORKTREE is non-nil, then visit the worktree version of
+the file, even if the diff is about a committed change. Use FN
+to display the buffer in some window."
+ (if (magit-file-accessible-directory-p file)
+ (magit-diff-visit-directory file force-worktree)
+ (pcase-let ((`(,buf ,pos)
+ (magit-diff-visit-file--noselect file force-worktree)))
+ (funcall fn buf)
+ (magit-diff-visit-file--setup buf pos)
+ buf)))
+
+(defun magit-diff-visit-directory (directory &optional other-window)
+ "Visit DIRECTORY in some window.
+Display the buffer in the selected window unless OTHER-WINDOW is
+non-nil. If DIRECTORY is the top-level directory of the current
+repository, then visit the containing directory using Dired and
+in the Dired buffer put point on DIRECTORY. Otherwise display
+the Magit-Status buffer for DIRECTORY."
+ (if (equal (magit-toplevel directory)
+ (magit-toplevel))
+ (dired-jump other-window (concat directory "/."))
+ (let ((display-buffer-overriding-action
+ (if other-window
+ '(nil (inhibit-same-window t))
+ '(display-buffer-same-window))))
+ (magit-status-setup-buffer directory))))
+
+(defun magit-diff-visit-file--setup (buf pos)
+ (if-let ((win (get-buffer-window buf 'visible)))
+ (with-selected-window win
+ (when pos
+ (unless (<= (point-min) pos (point-max))
+ (widen))
+ (goto-char pos))
+ (when (and buffer-file-name
+ (magit-anything-unmerged-p buffer-file-name))
+ (smerge-start-session))
+ (run-hooks 'magit-diff-visit-file-hook))
+ (error "File buffer is not visible")))
+
+(defun magit-diff-visit-file--noselect (&optional file goto-worktree)
+ (unless file
+ (setq file (magit-file-at-point t t)))
+ (let* ((hunk (magit-diff-visit--hunk))
+ (goto-from (and hunk
+ (magit-diff-visit--goto-from-p hunk goto-worktree)))
+ (line (and hunk (magit-diff-hunk-line hunk goto-from)))
+ (col (and hunk (magit-diff-hunk-column hunk goto-from)))
+ (spec (magit-diff--dwim))
+ (rev (if goto-from
+ (magit-diff-visit--range-from spec)
+ (magit-diff-visit--range-to spec)))
+ (buf (if (or goto-worktree
+ (and (not (stringp rev))
+ (or magit-diff-visit-avoid-head-blob
+ (not goto-from))))
+ (or (get-file-buffer file)
+ (find-file-noselect file))
+ (magit-find-file-noselect (if (stringp rev) rev "HEAD")
+ file))))
+ (if line
+ (with-current-buffer buf
+ (cond ((eq rev 'staged)
+ (setq line (magit-diff-visit--offset file nil line)))
+ ((and goto-worktree
+ (stringp rev))
+ (setq line (magit-diff-visit--offset file rev line))))
+ (list buf (save-restriction
+ (widen)
+ (goto-char (point-min))
+ (forward-line (1- line))
+ (move-to-column col)
+ (point))))
+ (list buf nil))))
+
+(defun magit-diff-visit--hunk ()
+ (when-let* ((scope (magit-diff-scope)) ;debbugs#31840
+ (section (magit-current-section)))
+ (cl-case scope
+ ((file files)
+ (setq section (car (oref section children))))
+ (list
+ (setq section (car (oref section children)))
+ (when section
+ (setq section (car (oref section children))))))
+ (and
+ ;; Unmerged files appear in the list of staged changes
+ ;; but unlike in the list of unstaged changes no diffs
+ ;; are shown here. In that case `section' is nil.
+ section
+ ;; Currently the `hunk' type is also abused for file
+ ;; mode changes, which we are not interested in here.
+ (not (equal (oref section value) '(chmod)))
+ section)))
+
+(defun magit-diff-visit--goto-from-p (section in-worktree)
+ (and magit-diff-visit-previous-blob
+ (not in-worktree)
+ (not (oref section combined))
+ (not (< (magit-point) (oref section content)))
+ (= (char-after (line-beginning-position)) ?-)))
+
+(defvar magit-diff-visit-jump-to-change t)
+
+(defun magit-diff-hunk-line (section goto-from)
+ (save-excursion
+ (goto-char (line-beginning-position))
+ (with-slots (content combined from-ranges from-range to-range) section
+ (when (or from-range to-range)
+ (when (and magit-diff-visit-jump-to-change (< (point) content))
+ (goto-char content)
+ (re-search-forward "^[-+]"))
+ (+ (car (if goto-from from-range to-range))
+ (let ((prefix (if combined (length from-ranges) 1))
+ (target (point))
+ (offset 0))
+ (goto-char content)
+ (while (< (point) target)
+ (unless (string-search
+ (if goto-from "+" "-")
+ (buffer-substring (point) (+ (point) prefix)))
+ (cl-incf offset))
+ (forward-line))
+ offset))))))
+
+(defun magit-diff-hunk-column (section goto-from)
+ (if (or (< (magit-point)
+ (oref section content))
+ (and (not goto-from)
+ (= (char-after (line-beginning-position)) ?-)))
+ 0
+ (max 0 (- (+ (current-column) 2)
+ (length (oref section value))))))
+
+(defun magit-diff-visit--range-from (spec)
+ (cond ((consp spec)
+ (concat (cdr spec) "^"))
+ ((stringp spec)
+ (car (magit-split-range spec)))
+ (t
+ spec)))
+
+(defun magit-diff-visit--range-to (spec)
+ (if (symbolp spec)
+ spec
+ (let ((rev (if (consp spec)
+ (cdr spec)
+ (cdr (magit-split-range spec)))))
+ (if (and magit-diff-visit-avoid-head-blob
+ (magit-rev-head-p rev))
+ 'unstaged
+ rev))))
+
+(defun magit-diff-visit--offset (file rev line)
+ (let ((offset 0))
+ (with-temp-buffer
+ (save-excursion
+ (magit-with-toplevel
+ (magit-git-insert "diff" rev "--" file)))
+ (catch 'found
+ (while (re-search-forward
+ "^@@ -\\([0-9]+\\),\\([0-9]+\\) \\+\\([0-9]+\\),\\([0-9]+\\) @@.*\n"
+ nil t)
+ (let ((from-beg (string-to-number (match-string 1)))
+ (from-len (string-to-number (match-string 2)))
+ ( to-len (string-to-number (match-string 4))))
+ (if (<= from-beg line)
+ (if (< (+ from-beg from-len) line)
+ (cl-incf offset (- to-len from-len))
+ (let ((rest (- line from-beg)))
+ (while (> rest 0)
+ (pcase (char-after)
+ (?\s (cl-decf rest))
+ (?- (cl-decf offset) (cl-decf rest))
+ (?+ (cl-incf offset)))
+ (forward-line))))
+ (throw 'found nil))))))
+ (+ line offset)))
+
+;;;; Scroll Commands
+
+(defun magit-diff-show-or-scroll-up ()
+ "Update the commit or diff buffer for the thing at point.
+
+Either show the commit or stash at point in the appropriate
+buffer, or if that buffer is already being displayed in the
+current frame and contains information about that commit or
+stash, then instead scroll the buffer up. If there is no
+commit or stash at point, then prompt for a commit."
+ (interactive)
+ (magit-diff-show-or-scroll #'scroll-up))
+
+(defun magit-diff-show-or-scroll-down ()
+ "Update the commit or diff buffer for the thing at point.
+
+Either show the commit or stash at point in the appropriate
+buffer, or if that buffer is already being displayed in the
+current frame and contains information about that commit or
+stash, then instead scroll the buffer down. If there is no
+commit or stash at point, then prompt for a commit."
+ (interactive)
+ (magit-diff-show-or-scroll #'scroll-down))
+
+(defun magit-diff-show-or-scroll (fn)
+ (let (rev cmd buf win)
+ (cond
+ (magit-blame-mode
+ (setq rev (oref (magit-current-blame-chunk) orig-rev))
+ (setq cmd #'magit-show-commit)
+ (setq buf (magit-get-mode-buffer 'magit-revision-mode)))
+ ((derived-mode-p 'git-rebase-mode)
+ (with-slots (action-type target)
+ (git-rebase-current-line)
+ (if (not (eq action-type 'commit))
+ (user-error "No commit on this line")
+ (setq rev target)
+ (setq cmd #'magit-show-commit)
+ (setq buf (magit-get-mode-buffer 'magit-revision-mode)))))
+ (t
+ (magit-section-case
+ (branch
+ (setq rev (magit-ref-maybe-qualify (oref it value)))
+ (setq cmd #'magit-show-commit)
+ (setq buf (magit-get-mode-buffer 'magit-revision-mode)))
+ (commit
+ (setq rev (oref it value))
+ (setq cmd #'magit-show-commit)
+ (setq buf (magit-get-mode-buffer 'magit-revision-mode)))
+ (stash
+ (setq rev (oref it value))
+ (setq cmd #'magit-stash-show)
+ (setq buf (magit-get-mode-buffer 'magit-stash-mode))))))
+ (if rev
+ (if (and buf
+ (setq win (get-buffer-window buf))
+ (with-current-buffer buf
+ (and (equal rev magit-buffer-revision)
+ (equal (magit-rev-parse rev)
+ magit-buffer-revision-hash))))
+ (with-selected-window win
+ (condition-case nil
+ (funcall fn)
+ (error
+ (goto-char (pcase fn
+ ('scroll-up (point-min))
+ ('scroll-down (point-max)))))))
+ (let ((magit-display-buffer-noselect t))
+ (if (eq cmd #'magit-show-commit)
+ (apply #'magit-show-commit rev (magit-show-commit--arguments))
+ (funcall cmd rev))))
+ (call-interactively #'magit-show-commit))))
+
+;;;; Section Commands
+
+(defun magit-section-cycle-diffs ()
+ "Cycle visibility of diff-related sections in the current buffer."
+ (interactive)
+ (when-let ((sections
+ (cond ((derived-mode-p 'magit-status-mode)
+ (--mapcat
+ (when it
+ (when (oref it hidden)
+ (magit-section-show it))
+ (oref it children))
+ (list (magit-get-section '((staged) (status)))
+ (magit-get-section '((unstaged) (status))))))
+ ((derived-mode-p 'magit-diff-mode)
+ (-filter #'magit-file-section-p
+ (oref magit-root-section children))))))
+ (if (--any-p (oref it hidden) sections)
+ (dolist (s sections)
+ (magit-section-show s)
+ (magit-section-hide-children s))
+ (let ((children (--mapcat (oref it children) sections)))
+ (cond ((and (--any-p (oref it hidden) children)
+ (--any-p (oref it children) children))
+ (mapc #'magit-section-show-headings sections))
+ ((seq-some #'magit-section-hidden-body children)
+ (mapc #'magit-section-show-children sections))
+ (t
+ (mapc #'magit-section-hide sections)))))))
+
+;;; Diff Mode
+
+(defvar magit-diff-mode-map
+ (let ((map (make-sparse-keymap)))
+ (set-keymap-parent map magit-mode-map)
+ (define-key map (kbd "C-c C-d") #'magit-diff-while-committing)
+ (define-key map (kbd "C-c C-b") #'magit-go-backward)
+ (define-key map (kbd "C-c C-f") #'magit-go-forward)
+ (define-key map (kbd "SPC") #'scroll-up)
+ (define-key map (kbd "DEL") #'scroll-down)
+ (define-key map (kbd "j") #'magit-jump-to-diffstat-or-diff)
+ (define-key map [remap write-file] #'magit-patch-save)
+ map)
+ "Keymap for `magit-diff-mode'.")
+
+(define-derived-mode magit-diff-mode magit-mode "Magit Diff"
+ "Mode for looking at a Git diff.
+
+This mode is documented in info node `(magit)Diff Buffer'.
+
+\\<magit-mode-map>\
+Type \\[magit-refresh] to refresh the current buffer.
+Type \\[magit-section-toggle] to expand or hide the section at point.
+Type \\[magit-visit-thing] to visit the hunk or file at point.
+
+Staging and applying changes is documented in info node
+`(magit)Staging and Unstaging' and info node `(magit)Applying'.
+
+\\<magit-hunk-section-map>Type \
+\\[magit-apply] to apply the change at point, \
+\\[magit-stage] to stage,
+\\[magit-unstage] to unstage, \
+\\[magit-discard] to discard, or \
+\\[magit-reverse] to reverse it.
+
+\\{magit-diff-mode-map}"
+ :group 'magit-diff
+ (hack-dir-local-variables-non-file-buffer)
+ (setq magit--imenu-item-types 'file))
+
+(put 'magit-diff-mode 'magit-diff-default-arguments
+ '("--stat" "--no-ext-diff"))
+
+(defun magit-diff-setup-buffer (range typearg args files &optional locked)
+ (require 'magit)
+ (magit-setup-buffer #'magit-diff-mode locked
+ (magit-buffer-range range)
+ (magit-buffer-typearg typearg)
+ (magit-buffer-diff-args args)
+ (magit-buffer-diff-files files)
+ (magit-buffer-diff-files-suspended nil)))
+
+(defun magit-diff-refresh-buffer ()
+ "Refresh the current `magit-diff-mode' buffer."
+ (magit-set-header-line-format
+ (if (equal magit-buffer-typearg "--no-index")
+ (apply #'format "Differences between %s and %s" magit-buffer-diff-files)
+ (concat (if magit-buffer-range
+ (cond
+ ((string-match-p "\\(\\.\\.\\|\\^-\\)"
+ magit-buffer-range)
+ (format "Changes in %s" magit-buffer-range))
+ ((member "-R" magit-buffer-diff-args)
+ (format "Changes from working tree to %s" magit-buffer-range))
+ (t
+ (format "Changes from %s to working tree" magit-buffer-range)))
+ (if (equal magit-buffer-typearg "--cached")
+ "Staged changes"
+ "Unstaged changes"))
+ (pcase (length magit-buffer-diff-files)
+ (0)
+ (1 (concat " in file " (car magit-buffer-diff-files)))
+ (_ (concat " in files "
+ (mapconcat #'identity magit-buffer-diff-files ", ")))))))
+ (setq magit-buffer-range-hashed
+ (and magit-buffer-range (magit-hash-range magit-buffer-range)))
+ (magit-insert-section (diffbuf)
+ (magit-run-section-hook 'magit-diff-sections-hook)))
+
+(cl-defmethod magit-buffer-value (&context (major-mode magit-diff-mode))
+ (nconc (cond (magit-buffer-range
+ (delq nil (list magit-buffer-range magit-buffer-typearg)))
+ ((equal magit-buffer-typearg "--cached")
+ (list 'staged))
+ (t
+ (list 'unstaged magit-buffer-typearg)))
+ (and magit-buffer-diff-files (cons "--" magit-buffer-diff-files))))
+
+(cl-defmethod magit-menu-common-value ((_section magit-diff-section))
+ (magit-diff-scope))
+
+(define-obsolete-variable-alias 'magit-diff-section-base-map
+ 'magit-diff-section-map "Magit-Section 3.4.0")
+(defvar magit-diff-section-map
+ (let ((map (make-sparse-keymap)))
+ (magit-menu-set map [magit-cherry-apply]
+ #'magit-apply "Apply %x"
+ '(:enable (not (memq (magit-diff-type) '(unstaged staged)))))
+ (magit-menu-set map [magit-stage-file]
+ #'magit-stage "Stage %x"
+ '(:enable (eq (magit-diff-type) 'unstaged)))
+ (magit-menu-set map [magit-unstage-file]
+ #'magit-unstage "Unstage %x"
+ '(:enable (eq (magit-diff-type) 'staged)))
+ (magit-menu-set map [magit-delete-thing]
+ #'magit-discard "Discard %x"
+ '(:enable (not (memq (magit-diff-type) '(committed undefined)))))
+ (magit-menu-set map [magit-revert-no-commit]
+ #'magit-reverse "Reverse %x"
+ '(:enable (not (memq (magit-diff-type) '(untracked unstaged)))))
+ (magit-menu-set map [magit-visit-thing]
+ #'magit-diff-visit-file "Visit file")
+ (magit-menu-set map [magit-file-untrack]
+ #'magit-file-untrack "Untrack %x"
+ '(:enable (memq (magit-diff-scope) '(file files))))
+ (magit-menu-set map [magit-file-rename]
+ #'magit-file-rename "Rename file"
+ '(:enable (eq (magit-diff-scope) 'file)))
+ (define-key map (kbd "C-j") #'magit-diff-visit-worktree-file)
+ (define-key map (kbd "C-<return>") #'magit-diff-visit-worktree-file)
+ (define-key map (kbd "C-x 4 <return>") #'magit-diff-visit-file-other-window)
+ (define-key map (kbd "C-x 5 <return>") #'magit-diff-visit-file-other-frame)
+ (define-key map "&" #'magit-do-async-shell-command)
+ (define-key map "C" #'magit-commit-add-log)
+ (define-key map (kbd "C-x a") #'magit-add-change-log-entry)
+ (define-key map (kbd "C-x 4 a") #'magit-add-change-log-entry-other-window)
+ (define-key map (kbd "C-c C-t") #'magit-diff-trace-definition)
+ (define-key map (kbd "C-c C-e") #'magit-diff-edit-hunk-commit)
+ map)
+ "Keymap for diff sections.
+The classes `magit-file-section' and `magit-hunk-section' derive
+from the abstract `magit-diff-section' class. Accordingly this
+keymap is the parent of their keymaps.")
+
+(defvar magit-file-section-map
+ (let ((map (make-sparse-keymap)))
+ (set-keymap-parent map magit-diff-section-base-map)
+ map)
+ "Keymap for `file' sections.")
+
+(defvar magit-hunk-section-map
+ (let ((map (make-sparse-keymap)))
+ (set-keymap-parent map magit-diff-section-base-map)
+ (let ((m (make-sparse-keymap)))
+ (define-key m (kbd "RET") #'magit-smerge-keep-current)
+ (define-key m (kbd "u") #'magit-smerge-keep-upper)
+ (define-key m (kbd "b") #'magit-smerge-keep-base)
+ (define-key m (kbd "l") #'magit-smerge-keep-lower)
+ (define-key map smerge-command-prefix m))
+ map)
+ "Keymap for `hunk' sections.")
+
+(defconst magit-diff-conflict-headline-re
+ (concat "^" (regexp-opt
+ ;; Defined in merge-tree.c in this order.
+ '("merged"
+ "added in remote"
+ "added in both"
+ "added in local"
+ "removed in both"
+ "changed in both"
+ "removed in local"
+ "removed in remote"))))
+
+(defconst magit-diff-headline-re
+ (concat "^\\(@@@?\\|diff\\|Submodule\\|"
+ "\\* Unmerged path\\|"
+ (substring magit-diff-conflict-headline-re 1)
+ "\\)"))
+
+(defconst magit-diff-statline-re
+ (concat "^ ?"
+ "\\(.*\\)" ; file
+ "\\( +| +\\)" ; separator
+ "\\([0-9]+\\|Bin\\(?: +[0-9]+ -> [0-9]+ bytes\\)?$\\) ?"
+ "\\(\\+*\\)" ; add
+ "\\(-*\\)$")) ; del
+
+(defvar magit-diff--reset-non-color-moved
+ (list
+ "-c" "color.diff.context=normal"
+ "-c" "color.diff.plain=normal" ; historical synonym for context
+ "-c" "color.diff.meta=normal"
+ "-c" "color.diff.frag=normal"
+ "-c" "color.diff.func=normal"
+ "-c" "color.diff.old=normal"
+ "-c" "color.diff.new=normal"
+ "-c" "color.diff.commit=normal"
+ "-c" "color.diff.whitespace=normal"
+ ;; "git-range-diff" does not support "--color-moved", so we don't
+ ;; need to reset contextDimmed, oldDimmed, newDimmed, contextBold,
+ ;; oldBold, and newBold.
+ ))
+
+(defun magit-insert-diff ()
+ "Insert the diff into this `magit-diff-mode' buffer."
+ (magit--insert-diff
+ "diff" magit-buffer-range "-p" "--no-prefix"
+ (and (member "--stat" magit-buffer-diff-args) "--numstat")
+ magit-buffer-typearg
+ magit-buffer-diff-args "--"
+ magit-buffer-diff-files))
+
+(defun magit--insert-diff (&rest args)
+ (declare (indent 0))
+ (pcase-let ((`(,cmd . ,args)
+ (flatten-tree args))
+ (magit-git-global-arguments
+ (remove "--literal-pathspecs" magit-git-global-arguments)))
+ ;; As of Git 2.19.0, we need to generate diffs with
+ ;; --ita-visible-in-index so that `magit-stage' can work with
+ ;; intent-to-add files (see #4026).
+ (when (and (not (equal cmd "merge-tree"))
+ (magit-git-version>= "2.19.0"))
+ (push "--ita-visible-in-index" args))
+ (setq args (magit-diff--maybe-add-stat-arguments args))
+ (when (cl-member-if (lambda (arg) (string-prefix-p "--color-moved" arg)) args)
+ (push "--color=always" args)
+ (setq magit-git-global-arguments
+ (append magit-diff--reset-non-color-moved
+ magit-git-global-arguments)))
+ (magit-git-wash #'magit-diff-wash-diffs cmd args)))
+
+(defun magit-diff--maybe-add-stat-arguments (args)
+ (if (member "--stat" args)
+ (append (if (functionp magit-diff-extra-stat-arguments)
+ (funcall magit-diff-extra-stat-arguments)
+ magit-diff-extra-stat-arguments)
+ args)
+ args))
+
+(defun magit-diff-use-window-width-as-stat-width ()
+ "Use the `window-width' as the value of `--stat-width'."
+ (and-let* ((window (get-buffer-window (current-buffer) 'visible)))
+ (list (format "--stat-width=%d" (window-width window)))))
+
+(defun magit-diff-wash-diffs (args &optional limit)
+ (run-hooks 'magit-diff-wash-diffs-hook)
+ (when (member "--show-signature" args)
+ (magit-diff-wash-signature magit-buffer-revision-hash))
+ (when (member "--stat" args)
+ (magit-diff-wash-diffstat))
+ (when (re-search-forward magit-diff-headline-re limit t)
+ (goto-char (line-beginning-position))
+ (magit-wash-sequence (apply-partially #'magit-diff-wash-diff args))
+ (insert ?\n)))
+
+(defun magit-jump-to-diffstat-or-diff ()
+ "Jump to the diffstat or diff.
+When point is on a file inside the diffstat section, then jump
+to the respective diff section, otherwise jump to the diffstat
+section or a child thereof."
+ (interactive)
+ (--if-let (magit-get-section
+ (append (magit-section-case
+ ([file diffstat] `((file . ,(oref it value))))
+ (file `((file . ,(oref it value)) (diffstat)))
+ (t '((diffstat))))
+ (magit-section-ident magit-root-section)))
+ (magit-section-goto it)
+ (user-error "No diffstat in this buffer")))
+
+(defun magit-diff-wash-signature (object)
+ (when (looking-at "^gpg: ")
+ (let (title end)
+ (save-excursion
+ (while (looking-at "^gpg: ")
+ (cond
+ ((looking-at "^gpg: Good signature from")
+ (setq title (propertize
+ (buffer-substring (point) (line-end-position))
+ 'face 'magit-signature-good)))
+ ((looking-at "^gpg: Can't check signature")
+ (setq title (propertize
+ (buffer-substring (point) (line-end-position))
+ 'face '(italic bold)))))
+ (forward-line))
+ (setq end (point-marker)))
+ (magit-insert-section (signature object title)
+ (when title
+ (magit-insert-heading title))
+ (goto-char end)
+ (set-marker end nil)
+ (insert "\n")))))
+
+(defun magit-diff-wash-diffstat ()
+ (let (heading (beg (point)))
+ (when (re-search-forward "^ ?\\([0-9]+ +files? change[^\n]*\n\\)" nil t)
+ (setq heading (match-string 1))
+ (magit-delete-match)
+ (goto-char beg)
+ (magit-insert-section (diffstat)
+ (insert (propertize heading 'font-lock-face 'magit-diff-file-heading))
+ (magit-insert-heading)
+ (let (files)
+ (while (looking-at "^[-0-9]+\t[-0-9]+\t\\(.+\\)$")
+ (push (magit-decode-git-path
+ (let ((f (match-string 1)))
+ (cond
+ ((string-match "\\`\\([^{]+\\){\\(.+\\) => \\(.+\\)}\\'" f)
+ (concat (match-string 1 f)
+ (match-string 3 f)))
+ ((string-match " => " f)
+ (substring f (match-end 0)))
+ (t f))))
+ files)
+ (magit-delete-line))
+ (setq files (nreverse files))
+ (while (looking-at magit-diff-statline-re)
+ (magit-bind-match-strings (file sep cnt add del) nil
+ (magit-delete-line)
+ (when (string-match " +$" file)
+ (setq sep (concat (match-string 0 file) sep))
+ (setq file (substring file 0 (match-beginning 0))))
+ (let ((le (length file)) ld)
+ (setq file (magit-decode-git-path file))
+ (setq ld (length file))
+ (when (> le ld)
+ (setq sep (concat (make-string (- le ld) ?\s) sep))))
+ (magit-insert-section (file (pop files))
+ (insert (propertize file 'font-lock-face 'magit-filename)
+ sep cnt " ")
+ (when add
+ (insert (propertize add 'font-lock-face
+ 'magit-diffstat-added)))
+ (when del
+ (insert (propertize del 'font-lock-face
+ 'magit-diffstat-removed)))
+ (insert "\n")))))
+ (if (looking-at "^$") (forward-line) (insert "\n"))))))
+
+(defun magit-diff-wash-diff (args)
+ (when (cl-member-if (lambda (arg) (string-prefix-p "--color-moved" arg)) args)
+ (require 'ansi-color)
+ (ansi-color-apply-on-region (point-min) (point-max)))
+ (cond
+ ((looking-at "^Submodule")
+ (magit-diff-wash-submodule))
+ ((looking-at "^\\* Unmerged path \\(.*\\)")
+ (let ((file (magit-decode-git-path (match-string 1))))
+ (magit-delete-line)
+ (unless (and (derived-mode-p 'magit-status-mode)
+ (not (member "--cached" args)))
+ (magit-insert-section (file file)
+ (insert (propertize
+ (format "unmerged %s%s" file
+ (pcase (cddr (car (magit-file-status file)))
+ ('(?D ?D) " (both deleted)")
+ ('(?D ?U) " (deleted by us)")
+ ('(?U ?D) " (deleted by them)")
+ ('(?A ?A) " (both added)")
+ ('(?A ?U) " (added by us)")
+ ('(?U ?A) " (added by them)")
+ ('(?U ?U) "")))
+ 'font-lock-face 'magit-diff-file-heading))
+ (insert ?\n))))
+ t)
+ ((looking-at magit-diff-conflict-headline-re)
+ (let ((long-status (match-string 0))
+ (status "BUG")
+ file orig base)
+ (if (equal long-status "merged")
+ (progn (setq status long-status)
+ (setq long-status nil))
+ (setq status (pcase-exhaustive long-status
+ ("added in remote" "new file")
+ ("added in both" "new file")
+ ("added in local" "new file")
+ ("removed in both" "removed")
+ ("changed in both" "changed")
+ ("removed in local" "removed")
+ ("removed in remote" "removed"))))
+ (magit-delete-line)
+ (while (looking-at
+ "^ \\([^ ]+\\) +[0-9]\\{6\\} \\([a-z0-9]\\{40,\\}\\) \\(.+\\)$")
+ (magit-bind-match-strings (side _blob name) nil
+ (pcase side
+ ("result" (setq file name))
+ ("our" (setq orig name))
+ ("their" (setq file name))
+ ("base" (setq base name))))
+ (magit-delete-line))
+ (when orig (setq orig (magit-decode-git-path orig)))
+ (when file (setq file (magit-decode-git-path file)))
+ (magit-diff-insert-file-section
+ (or file base) orig status nil nil nil long-status)))
+ ;; The files on this line may be ambiguous due to whitespace.
+ ;; That's okay. We can get their names from subsequent headers.
+ ((looking-at "^diff --\
+\\(?:\\(?1:git\\) \\(?:\\(?2:.+?\\) \\2\\)?\
+\\|\\(?:cc\\|combined\\) \\(?3:.+\\)\\)")
+ (let ((status (cond ((equal (match-string 1) "git") "modified")
+ ((derived-mode-p 'magit-revision-mode) "resolved")
+ (t "unmerged")))
+ (orig nil)
+ (file (or (match-string 2) (match-string 3)))
+ (header (list (buffer-substring-no-properties
+ (line-beginning-position) (1+ (line-end-position)))))
+ (modes nil)
+ (rename nil))
+ (magit-delete-line)
+ (while (not (or (eobp) (looking-at magit-diff-headline-re)))
+ (cond
+ ((looking-at "old mode \\(?:[^\n]+\\)\nnew mode \\(?:[^\n]+\\)\n")
+ (setq modes (match-string 0)))
+ ((looking-at "deleted file .+\n")
+ (setq status "deleted"))
+ ((looking-at "new file .+\n")
+ (setq status "new file"))
+ ((looking-at "rename from \\(.+\\)\nrename to \\(.+\\)\n")
+ (setq rename (match-string 0))
+ (setq orig (match-string 1))
+ (setq file (match-string 2))
+ (setq status "renamed"))
+ ((looking-at "copy from \\(.+\\)\ncopy to \\(.+\\)\n")
+ (setq orig (match-string 1))
+ (setq file (match-string 2))
+ (setq status "new file"))
+ ((looking-at "similarity index .+\n"))
+ ((looking-at "dissimilarity index .+\n"))
+ ((looking-at "index .+\n"))
+ ((looking-at "--- \\(.+?\\)\t?\n")
+ (unless (equal (match-string 1) "/dev/null")
+ (setq orig (match-string 1))))
+ ((looking-at "\\+\\+\\+ \\(.+?\\)\t?\n")
+ (unless (equal (match-string 1) "/dev/null")
+ (setq file (match-string 1))))
+ ((looking-at "Binary files .+ and .+ differ\n"))
+ ((looking-at "Binary files differ\n"))
+ ;; TODO Use all combined diff extended headers.
+ ((looking-at "mode .+\n"))
+ (t
+ (error "BUG: Unknown extended header: %S"
+ (buffer-substring (point) (line-end-position)))))
+ ;; These headers are treated as some sort of special hunk.
+ (unless (or (string-prefix-p "old mode" (match-string 0))
+ (string-prefix-p "rename" (match-string 0)))
+ (push (match-string 0) header))
+ (magit-delete-match))
+ (setq header (mapconcat #'identity (nreverse header) ""))
+ (when orig
+ (setq orig (magit-decode-git-path orig)))
+ (setq file (magit-decode-git-path file))
+ ;; KLUDGE `git-diff' ignores `--no-prefix' for new files and renames at
+ ;; least. And `git-log' ignores `--no-prefix' when `-L' is used.
+ (when (or (and file orig
+ (string-prefix-p "a/" orig)
+ (string-prefix-p "b/" file))
+ (and (derived-mode-p 'magit-log-mode)
+ (--first (string-prefix-p "-L" it)
+ magit-buffer-log-args)))
+ (setq file (substring file 2))
+ (when orig
+ (setq orig (substring orig 2))))
+ (magit-diff-insert-file-section file orig status modes rename header)))))
+
+(defun magit-diff-insert-file-section
+ (file orig status modes rename header &optional long-status)
+ (magit-insert-section section
+ (file file (or (equal status "deleted")
+ (derived-mode-p 'magit-status-mode)))
+ (insert (propertize (format "%-10s %s" status
+ (if (or (not orig) (equal orig file))
+ file
+ (format "%s -> %s" orig file)))
+ 'font-lock-face 'magit-diff-file-heading))
+ (when long-status
+ (insert (format " (%s)" long-status)))
+ (magit-insert-heading)
+ (unless (equal orig file)
+ (oset section source orig))
+ (oset section header header)
+ (when modes
+ (magit-insert-section (hunk '(chmod))
+ (insert modes)
+ (magit-insert-heading)))
+ (when rename
+ (magit-insert-section (hunk '(rename))
+ (insert rename)
+ (magit-insert-heading)))
+ (magit-wash-sequence #'magit-diff-wash-hunk)))
+
+(defun magit-diff-wash-submodule ()
+ ;; See `show_submodule_summary' in submodule.c and "this" commit.
+ (when (looking-at "^Submodule \\([^ ]+\\)")
+ (let ((module (match-string 1))
+ untracked modified)
+ (when (looking-at "^Submodule [^ ]+ contains untracked content$")
+ (magit-delete-line)
+ (setq untracked t))
+ (when (looking-at "^Submodule [^ ]+ contains modified content$")
+ (magit-delete-line)
+ (setq modified t))
+ (cond
+ ((and (looking-at "^Submodule \\([^ ]+\\) \\([^ :]+\\)\\( (rewind)\\)?:$")
+ (equal (match-string 1) module))
+ (magit-bind-match-strings (_module range rewind) nil
+ (magit-delete-line)
+ (while (looking-at "^ \\([<>]\\) \\(.*\\)$")
+ (magit-delete-line))
+ (when rewind
+ (setq range (replace-regexp-in-string "[^.]\\(\\.\\.\\)[^.]"
+ "..." range t t 1)))
+ (magit-insert-section (magit-module-section module t)
+ (magit-insert-heading
+ (propertize (concat "modified " module)
+ 'font-lock-face 'magit-diff-file-heading)
+ " ("
+ (cond (rewind "rewind")
+ ((string-search "..." range) "non-ff")
+ (t "new commits"))
+ (and (or modified untracked)
+ (concat ", "
+ (and modified "modified")
+ (and modified untracked " and ")
+ (and untracked "untracked")
+ " content"))
+ ")")
+ (let ((default-directory
+ (file-name-as-directory
+ (expand-file-name module (magit-toplevel)))))
+ (magit-git-wash (apply-partially #'magit-log-wash-log 'module)
+ "log" "--oneline" "--left-right" range)
+ (delete-char -1)))))
+ ((and (looking-at "^Submodule \\([^ ]+\\) \\([^ ]+\\) (\\([^)]+\\))$")
+ (equal (match-string 1) module))
+ (magit-bind-match-strings (_module _range msg) nil
+ (magit-delete-line)
+ (magit-insert-section (magit-module-section module)
+ (magit-insert-heading
+ (propertize (concat "submodule " module)
+ 'font-lock-face 'magit-diff-file-heading)
+ " (" msg ")"))))
+ (t
+ (magit-insert-section (magit-module-section module)
+ (magit-insert-heading
+ (propertize (concat "modified " module)
+ 'font-lock-face 'magit-diff-file-heading)
+ " ("
+ (and modified "modified")
+ (and modified untracked " and ")
+ (and untracked "untracked")
+ " content)")))))))
+
+(defun magit-diff-wash-hunk ()
+ (when (looking-at "^@\\{2,\\} \\(.+?\\) @\\{2,\\}\\(?: \\(.*\\)\\)?")
+ (let* ((heading (match-string 0))
+ (ranges (mapcar (lambda (str)
+ (mapcar #'string-to-number
+ (split-string (substring str 1) ",")))
+ (split-string (match-string 1))))
+ (about (match-string 2))
+ (combined (length= ranges 3))
+ (value (cons about ranges)))
+ (magit-delete-line)
+ (magit-insert-section section (hunk value)
+ (insert (propertize (concat heading "\n")
+ 'font-lock-face 'magit-diff-hunk-heading))
+ (magit-insert-heading)
+ (while (not (or (eobp) (looking-at "^[^-+\s\\]")))
+ (forward-line))
+ (oset section end (point))
+ (oset section washer #'magit-diff-paint-hunk)
+ (oset section combined combined)
+ (if combined
+ (oset section from-ranges (butlast ranges))
+ (oset section from-range (car ranges)))
+ (oset section to-range (car (last ranges)))
+ (oset section about about)))
+ t))
+
+(defun magit-diff-expansion-threshold (section)
+ "Keep new diff sections collapsed if washing takes too long."
+ (and (magit-file-section-p section)
+ (> (float-time (time-subtract (current-time) magit-refresh-start-time))
+ magit-diff-expansion-threshold)
+ 'hide))
+
+(add-hook 'magit-section-set-visibility-hook #'magit-diff-expansion-threshold)
+
+;;; Revision Mode
+
+(define-derived-mode magit-revision-mode magit-diff-mode "Magit Rev"
+ "Mode for looking at a Git commit.
+
+This mode is documented in info node `(magit)Revision Buffer'.
+
+\\<magit-mode-map>\
+Type \\[magit-refresh] to refresh the current buffer.
+Type \\[magit-section-toggle] to expand or hide the section at point.
+Type \\[magit-visit-thing] to visit the hunk or file at point.
+
+Staging and applying changes is documented in info node
+`(magit)Staging and Unstaging' and info node `(magit)Applying'.
+
+\\<magit-hunk-section-map>Type \
+\\[magit-apply] to apply the change at point, \
+\\[magit-stage] to stage,
+\\[magit-unstage] to unstage, \
+\\[magit-discard] to discard, or \
+\\[magit-reverse] to reverse it.
+
+\\{magit-revision-mode-map}"
+ :group 'magit-revision
+ (hack-dir-local-variables-non-file-buffer))
+
+(put 'magit-revision-mode 'magit-diff-default-arguments
+ '("--stat" "--no-ext-diff"))
+
+(defun magit-revision-setup-buffer (rev args files)
+ (magit-setup-buffer #'magit-revision-mode nil
+ (magit-buffer-revision rev)
+ (magit-buffer-range (format "%s^..%s" rev rev))
+ (magit-buffer-diff-args args)
+ (magit-buffer-diff-files files)
+ (magit-buffer-diff-files-suspended nil)))
+
+(defun magit-revision-refresh-buffer ()
+ (setq magit-buffer-revision-hash (magit-rev-hash magit-buffer-revision))
+ (magit-set-header-line-format
+ (concat (magit-object-type magit-buffer-revision-hash)
+ " " magit-buffer-revision
+ (pcase (length magit-buffer-diff-files)
+ (0)
+ (1 (concat " limited to file " (car magit-buffer-diff-files)))
+ (_ (concat " limited to files "
+ (mapconcat #'identity magit-buffer-diff-files ", "))))))
+ (magit-insert-section (commitbuf)
+ (magit-run-section-hook 'magit-revision-sections-hook)))
+
+(cl-defmethod magit-buffer-value (&context (major-mode magit-revision-mode))
+ (cons magit-buffer-revision magit-buffer-diff-files))
+
+(defun magit-insert-revision-diff ()
+ "Insert the diff into this `magit-revision-mode' buffer."
+ (magit--insert-diff
+ "show" "-p" "--cc" "--format=" "--no-prefix"
+ (and (member "--stat" magit-buffer-diff-args) "--numstat")
+ magit-buffer-diff-args
+ (magit--rev-dereference magit-buffer-revision)
+ "--" magit-buffer-diff-files))
+
+(defun magit-insert-revision-tag ()
+ "Insert tag message and headers into a revision buffer.
+This function only inserts anything when `magit-show-commit' is
+called with a tag as argument, when that is called with a commit
+or a ref which is not a branch, then it inserts nothing."
+ (when (equal (magit-object-type magit-buffer-revision) "tag")
+ (magit-insert-section (taginfo)
+ (let ((beg (point)))
+ ;; "git verify-tag -v" would output what we need, but the gpg
+ ;; output is send to stderr and we have no control over the
+ ;; order in which stdout and stderr are inserted, which would
+ ;; make parsing hard. We are forced to use "git cat-file tag"
+ ;; instead, which inserts the signature instead of verifying
+ ;; it. We remove that later and then insert the verification
+ ;; output using "git verify-tag" (without the "-v").
+ (magit-git-insert "cat-file" "tag" magit-buffer-revision)
+ (goto-char beg)
+ (forward-line 3)
+ (delete-region beg (point)))
+ (looking-at "^tagger \\([^<]+\\) <\\([^>]+\\)")
+ (let ((heading (format "Tagger: %s <%s>"
+ (match-string 1)
+ (match-string 2))))
+ (magit-delete-line)
+ (insert (propertize heading 'font-lock-face
+ 'magit-section-secondary-heading)))
+ (magit-insert-heading)
+ (forward-line)
+ (magit-insert-section section (message)
+ (oset section heading-highlight-face
+ 'magit-diff-revision-summary-highlight)
+ (let ((beg (point)))
+ (forward-line)
+ (magit--add-face-text-property
+ beg (point) 'magit-diff-revision-summary))
+ (magit-insert-heading)
+ (if (re-search-forward "-----BEGIN PGP SIGNATURE-----" nil t)
+ (goto-char (match-beginning 0))
+ (goto-char (point-max)))
+ (insert ?\n))
+ (if (re-search-forward "-----BEGIN PGP SIGNATURE-----" nil t)
+ (progn
+ (let ((beg (match-beginning 0)))
+ (re-search-forward "-----END PGP SIGNATURE-----\n")
+ (delete-region beg (point)))
+ (save-excursion
+ (magit-process-git t "verify-tag" magit-buffer-revision))
+ (magit-diff-wash-signature magit-buffer-revision))
+ (goto-char (point-max)))
+ (insert ?\n))))
+
+(defvar magit-commit-message-section-map
+ (let ((map (make-sparse-keymap)))
+ (magit-menu-set map [magit-visit-thing] #'magit-show-commit "Visit %t"
+ '(:enable (magit-thing-at-point 'git-revision t)))
+ map)
+ "Keymap for `commit-message' sections.")
+
+(defun magit-insert-revision-message ()
+ "Insert the commit message into a revision buffer."
+ (magit-insert-section section (commit-message)
+ (oset section heading-highlight-face 'magit-diff-revision-summary-highlight)
+ (let ((beg (point))
+ (rev magit-buffer-revision))
+ (insert (with-temp-buffer
+ (magit-rev-insert-format "%B" rev)
+ (magit-revision--wash-message)))
+ (if (= (point) (+ beg 2))
+ (progn (backward-delete-char 2)
+ (insert "(no message)\n"))
+ (goto-char beg)
+ (save-excursion
+ (while (search-forward "\r\n" nil t) ; Remove trailing CRs.
+ (delete-region (match-beginning 0) (1+ (match-beginning 0)))))
+ (when magit-revision-fill-summary-line
+ (let ((fill-column (min magit-revision-fill-summary-line
+ (window-width))))
+ (fill-region (point) (line-end-position))))
+ (when magit-revision-use-hash-sections
+ (save-excursion
+ ;; Start after beg to prevent a (commit text) section from
+ ;; starting at the same point as the (commit-message)
+ ;; section.
+ (goto-char (1+ beg))
+ (while (not (eobp))
+ (re-search-forward "\\_<" nil 'move)
+ (let ((beg (point)))
+ (re-search-forward "\\_>" nil t)
+ (when (> (point) beg)
+ (let ((text (buffer-substring-no-properties beg (point))))
+ (when (pcase magit-revision-use-hash-sections
+ ('quickest ; false negatives and positives
+ (and (>= (length text) 7)
+ (string-match-p "[0-9]" text)
+ (string-match-p "[a-z]" text)))
+ ('quicker ; false negatives (number-less hashes)
+ (and (>= (length text) 7)
+ (string-match-p "[0-9]" text)
+ (magit-commit-p text)))
+ ('quick ; false negatives (short hashes)
+ (and (>= (length text) 7)
+ (magit-commit-p text)))
+ ('slow
+ (magit-commit-p text)))
+ (put-text-property beg (point)
+ 'font-lock-face 'magit-hash)
+ (let ((end (point)))
+ (goto-char beg)
+ (magit-insert-section (commit text)
+ (goto-char end))))))))))
+ (save-excursion
+ (forward-line)
+ (magit--add-face-text-property
+ beg (point) 'magit-diff-revision-summary)
+ (magit-insert-heading))
+ (when magit-diff-highlight-keywords
+ (save-excursion
+ (while (re-search-forward "\\[[^[]*\\]" nil t)
+ (let ((beg (match-beginning 0))
+ (end (match-end 0)))
+ (put-text-property
+ beg end 'font-lock-face
+ (if-let ((face (get-text-property beg 'font-lock-face)))
+ (list face 'magit-keyword)
+ 'magit-keyword))))))
+ (goto-char (point-max))))))
+
+(defun magit-insert-revision-notes ()
+ "Insert commit notes into a revision buffer."
+ (let* ((var "core.notesRef")
+ (def (or (magit-get var) "refs/notes/commits")))
+ (dolist (ref (or (magit-list-active-notes-refs)))
+ (magit-insert-section section (notes ref (not (equal ref def)))
+ (oset section heading-highlight-face 'magit-diff-hunk-heading-highlight)
+ (let ((beg (point))
+ (rev magit-buffer-revision))
+ (insert (with-temp-buffer
+ (magit-git-insert "-c" (concat "core.notesRef=" ref)
+ "notes" "show" rev)
+ (magit-revision--wash-message)))
+ (if (= (point) beg)
+ (magit-cancel-section)
+ (goto-char beg)
+ (end-of-line)
+ (insert (format " (%s)"
+ (propertize (if (string-prefix-p "refs/notes/" ref)
+ (substring ref 11)
+ ref)
+ 'font-lock-face 'magit-refname)))
+ (forward-char)
+ (magit--add-face-text-property beg (point) 'magit-diff-hunk-heading)
+ (magit-insert-heading)
+ (goto-char (point-max))
+ (insert ?\n)))))))
+
+(defun magit-revision--wash-message ()
+ (let ((major-mode 'git-commit-mode))
+ (hack-dir-local-variables)
+ (hack-local-variables-apply))
+ (unless (memq git-commit-major-mode '(nil text-mode))
+ (funcall git-commit-major-mode)
+ (font-lock-ensure))
+ (buffer-string))
+
+(defun magit-insert-revision-headers ()
+ "Insert headers about the commit into a revision buffer."
+ (magit-insert-section (headers)
+ (--when-let (magit-rev-format "%D" magit-buffer-revision "--decorate=full")
+ (insert (magit-format-ref-labels it) ?\s))
+ (insert (propertize
+ (magit-rev-parse (magit--rev-dereference magit-buffer-revision))
+ 'font-lock-face 'magit-hash))
+ (magit-insert-heading)
+ (let ((beg (point)))
+ (magit-rev-insert-format magit-revision-headers-format
+ magit-buffer-revision)
+ (magit-insert-revision-gravatars magit-buffer-revision beg))
+ (when magit-revision-insert-related-refs
+ (dolist (parent (magit-commit-parents magit-buffer-revision))
+ (magit-insert-section (commit parent)
+ (let ((line (magit-rev-format "%h %s" parent)))
+ (string-match "^\\([^ ]+\\) \\(.*\\)" line)
+ (magit-bind-match-strings (hash msg) line
+ (insert "Parent: ")
+ (insert (propertize hash 'font-lock-face 'magit-hash))
+ (insert " " msg "\n")))))
+ (magit--insert-related-refs
+ magit-buffer-revision "--merged" "Merged"
+ (eq magit-revision-insert-related-refs 'all))
+ (magit--insert-related-refs
+ magit-buffer-revision "--contains" "Contained"
+ (memq magit-revision-insert-related-refs '(all mixed)))
+ (when-let ((follows (magit-get-current-tag magit-buffer-revision t)))
+ (let ((tag (car follows))
+ (cnt (cadr follows)))
+ (magit-insert-section (tag tag)
+ (insert
+ (format "Follows: %s (%s)\n"
+ (propertize tag 'font-lock-face 'magit-tag)
+ (propertize (number-to-string cnt)
+ 'font-lock-face 'magit-branch-local))))))
+ (when-let ((precedes (magit-get-next-tag magit-buffer-revision t)))
+ (let ((tag (car precedes))
+ (cnt (cadr precedes)))
+ (magit-insert-section (tag tag)
+ (insert (format "Precedes: %s (%s)\n"
+ (propertize tag 'font-lock-face 'magit-tag)
+ (propertize (number-to-string cnt)
+ 'font-lock-face 'magit-tag))))))
+ (insert ?\n))))
+
+(defun magit--insert-related-refs (rev arg title remote)
+ (when-let ((refs (magit-list-related-branches arg rev (and remote "-a"))))
+ (insert title ":" (make-string (- 10 (length title)) ?\s))
+ (dolist (branch refs)
+ (if (<= (+ (current-column) 1 (length branch))
+ (window-width))
+ (insert ?\s)
+ (insert ?\n (make-string 12 ?\s)))
+ (magit-insert-section (branch branch)
+ (insert (propertize branch 'font-lock-face
+ (if (string-prefix-p "remotes/" branch)
+ 'magit-branch-remote
+ 'magit-branch-local)))))
+ (insert ?\n)))
+
+(defun magit-insert-revision-gravatars (rev beg)
+ (when (and magit-revision-show-gravatars
+ (window-system))
+ (require 'gravatar)
+ (pcase-let ((`(,author . ,committer)
+ (pcase magit-revision-show-gravatars
+ ('t '("^Author: " . "^Commit: "))
+ ('author '("^Author: " . nil))
+ ('committer '(nil . "^Commit: "))
+ (_ magit-revision-show-gravatars))))
+ (--when-let (and author (magit-rev-format "%aE" rev))
+ (magit-insert-revision-gravatar beg rev it author))
+ (--when-let (and committer (magit-rev-format "%cE" rev))
+ (magit-insert-revision-gravatar beg rev it committer)))))
+
+(defun magit-insert-revision-gravatar (beg rev email regexp)
+ (save-excursion
+ (goto-char beg)
+ (when (re-search-forward regexp nil t)
+ (when-let ((window (get-buffer-window)))
+ (let* ((column (length (match-string 0)))
+ (font-obj (query-font (font-at (point) window)))
+ (size (* 2 (+ (aref font-obj 4)
+ (aref font-obj 5))))
+ (align-to (+ column
+ (ceiling (/ size (aref font-obj 7) 1.0))
+ 1))
+ (gravatar-size (- size 2)))
+ (ignore-errors ; service may be unreachable
+ (gravatar-retrieve email #'magit-insert-revision-gravatar-cb
+ (list gravatar-size rev
+ (point-marker)
+ align-to column))))))))
+
+(defun magit-insert-revision-gravatar-cb (image size rev marker align-to column)
+ (unless (eq image 'error)
+ (when-let ((buffer (marker-buffer marker)))
+ (with-current-buffer buffer
+ (save-excursion
+ (goto-char marker)
+ ;; The buffer might display another revision by now or
+ ;; it might have been refreshed, in which case another
+ ;; process might already have inserted the image.
+ (when (and (equal rev magit-buffer-revision)
+ (not (eq (car-safe
+ (car-safe
+ (get-text-property (point) 'display)))
+ 'image)))
+ (setf (image-property image :ascent) 'center)
+ (setf (image-property image :relief) 1)
+ (setf (image-property image :scale) 1)
+ (setf (image-property image :height) size)
+ (let ((top (list image '(slice 0.0 0.0 1.0 0.5)))
+ (bot (list image '(slice 0.0 0.5 1.0 1.0)))
+ (align `((space :align-to ,align-to))))
+ (when magit-revision-use-gravatar-kludge
+ (cl-rotatef top bot))
+ (let ((inhibit-read-only t))
+ (insert (propertize " " 'display top))
+ (insert (propertize " " 'display align))
+ (forward-line)
+ (forward-char column)
+ (insert (propertize " " 'display bot))
+ (insert (propertize " " 'display align))))))))))
+
+;;; Merge-Preview Mode
+
+(define-derived-mode magit-merge-preview-mode magit-diff-mode "Magit Merge"
+ "Mode for previewing a merge."
+ :group 'magit-diff
+ (hack-dir-local-variables-non-file-buffer))
+
+(put 'magit-merge-preview-mode 'magit-diff-default-arguments
+ '("--no-ext-diff"))
+
+(defun magit-merge-preview-setup-buffer (rev)
+ (magit-setup-buffer #'magit-merge-preview-mode nil
+ (magit-buffer-revision rev)
+ (magit-buffer-range (format "%s^..%s" rev rev))))
+
+(defun magit-merge-preview-refresh-buffer ()
+ (let* ((branch (magit-get-current-branch))
+ (head (or branch (magit-rev-verify "HEAD"))))
+ (magit-set-header-line-format (format "Preview merge of %s into %s"
+ magit-buffer-revision
+ (or branch "HEAD")))
+ (magit-insert-section (diffbuf)
+ (magit--insert-diff
+ "merge-tree" (magit-git-string "merge-base" head magit-buffer-revision)
+ head magit-buffer-revision))))
+
+(cl-defmethod magit-buffer-value (&context (major-mode magit-merge-preview-mode))
+ magit-buffer-revision)
+
+;;; Diff Sections
+
+(defun magit-hunk-set-window-start (section)
+ "When SECTION is a `hunk', ensure that its beginning is visible.
+It the SECTION has a different type, then do nothing."
+ (when (magit-hunk-section-p section)
+ (magit-section-set-window-start section)))
+
+(add-hook 'magit-section-movement-hook #'magit-hunk-set-window-start)
+
+(defun magit-hunk-goto-successor (section arg)
+ (and (magit-hunk-section-p section)
+ (and-let* ((parent (magit-get-section
+ (magit-section-ident
+ (oref section parent)))))
+ (let* ((children (oref parent children))
+ (siblings (magit-section-siblings section 'prev))
+ (previous (nth (length siblings) children)))
+ (if (not arg)
+ (--when-let (or previous (car (last children)))
+ (magit-section-goto it)
+ t)
+ (when previous
+ (magit-section-goto previous))
+ (if (and (stringp arg)
+ (re-search-forward arg (oref parent end) t))
+ (goto-char (match-beginning 0))
+ (goto-char (oref (car (last children)) end))
+ (forward-line -1)
+ (while (looking-at "^ ") (forward-line -1))
+ (while (looking-at "^[-+]") (forward-line -1))
+ (forward-line)))))))
+
+(add-hook 'magit-section-goto-successor-hook #'magit-hunk-goto-successor)
+
+(defvar magit-unstaged-section-map
+ (let ((map (make-sparse-keymap)))
+ (magit-menu-set map [magit-visit-thing] #'magit-diff-unstaged "Visit diff")
+ (magit-menu-set map [magit-stage-file] #'magit-stage "Stage all")
+ (magit-menu-set map [magit-delete-thing] #'magit-discard "Discard all")
+ map)
+ "Keymap for the `unstaged' section.")
+
+(magit-define-section-jumper magit-jump-to-unstaged "Unstaged changes" unstaged)
+
+(defun magit-insert-unstaged-changes ()
+ "Insert section showing unstaged changes."
+ (magit-insert-section (unstaged)
+ (magit-insert-heading "Unstaged changes:")
+ (magit--insert-diff
+ "diff" magit-buffer-diff-args "--no-prefix"
+ "--" magit-buffer-diff-files)))
+
+(defvar magit-staged-section-map
+ (let ((map (make-sparse-keymap)))
+ (magit-menu-set map [magit-visit-thing] #'magit-diff-staged "Visit diff")
+ (magit-menu-set map [magit-unstage-file] #'magit-unstage "Unstage all")
+ (magit-menu-set map [magit-delete-thing] #'magit-discard "Discard all")
+ (magit-menu-set map [magit-revert-no-commit] #'magit-reverse "Reverse all")
+ map)
+ "Keymap for the `staged' section.")
+
+(magit-define-section-jumper magit-jump-to-staged "Staged changes" staged)
+
+(defun magit-insert-staged-changes ()
+ "Insert section showing staged changes."
+ ;; Avoid listing all files as deleted when visiting a bare repo.
+ (unless (magit-bare-repo-p)
+ (magit-insert-section (staged)
+ (magit-insert-heading "Staged changes:")
+ (magit--insert-diff
+ "diff" "--cached" magit-buffer-diff-args "--no-prefix"
+ "--" magit-buffer-diff-files))))
+
+;;; Diff Type
+
+(defun magit-diff-type (&optional section)
+ "Return the diff type of SECTION.
+
+The returned type is one of the symbols `staged', `unstaged',
+`committed', or `undefined'. This type serves a similar purpose
+as the general type common to all sections (which is stored in
+the `type' slot of the corresponding `magit-section' struct) but
+takes additional information into account. When the SECTION
+isn't related to diffs and the buffer containing it also isn't
+a diff-only buffer, then return nil.
+
+Currently the type can also be one of `tracked' and `untracked'
+but these values are not handled explicitly everywhere they
+should be and a possible fix could be to just return nil here.
+
+The section has to be a `diff' or `hunk' section, or a section
+whose children are of type `diff'. If optional SECTION is nil,
+return the diff type for the current section. In buffers whose
+major mode is `magit-diff-mode' SECTION is ignored and the type
+is determined using other means. In `magit-revision-mode'
+buffers the type is always `committed'.
+
+Do not confuse this with `magit-diff-scope' (which see)."
+ (--when-let (or section (magit-current-section))
+ (cond ((derived-mode-p 'magit-revision-mode 'magit-stash-mode) 'committed)
+ ((derived-mode-p 'magit-diff-mode)
+ (let ((range magit-buffer-range)
+ (const magit-buffer-typearg))
+ (cond ((equal const "--no-index") 'undefined)
+ ((or (not range)
+ (magit-rev-eq range "HEAD"))
+ (if (equal const "--cached")
+ 'staged
+ 'unstaged))
+ ((equal const "--cached")
+ (if (magit-rev-head-p range)
+ 'staged
+ 'undefined)) ; i.e. committed and staged
+ (t 'committed))))
+ ((derived-mode-p 'magit-status-mode)
+ (let ((stype (oref it type)))
+ (if (memq stype '(staged unstaged tracked untracked))
+ stype
+ (pcase stype
+ ((or 'file 'module)
+ (let* ((parent (oref it parent))
+ (type (oref parent type)))
+ (if (memq type '(file module))
+ (magit-diff-type parent)
+ type)))
+ ('hunk (thread-first it
+ (oref parent)
+ (oref parent)
+ (oref type)))))))
+ ((derived-mode-p 'magit-log-mode)
+ (if (or (and (magit-section-match 'commit section)
+ (oref section children))
+ (magit-section-match [* file commit] section))
+ 'committed
+ 'undefined))
+ (t 'undefined))))
+
+(cl-defun magit-diff-scope (&optional (section nil ssection) strict)
+ "Return the diff scope of SECTION or the selected section(s).
+
+A diff's \"scope\" describes what part of a diff is selected, it is
+a symbol, one of `region', `hunk', `hunks', `file', `files', or
+`list'. Do not confuse this with the diff \"type\", as returned by
+`magit-diff-type'.
+
+If optional SECTION is non-nil, then return the scope of that,
+ignoring the sections selected by the region. Otherwise return
+the scope of the current section, or if the region is active and
+selects a valid group of diff related sections, the type of these
+sections, i.e. `hunks' or `files'. If SECTION, or if that is nil
+the current section, is a `hunk' section; and the region region
+starts and ends inside the body of a that section, then the type
+is `region'. If the region is empty after a mouse click, then
+`hunk' is returned instead of `region'.
+
+If optional STRICT is non-nil, then return nil if the diff type of
+the section at point is `untracked' or the section at point is not
+actually a `diff' but a `diffstat' section."
+ (let ((siblings (and (not ssection) (magit-region-sections nil t))))
+ (setq section (or section (car siblings) (magit-current-section)))
+ (when (and section
+ (or (not strict)
+ (and (not (eq (magit-diff-type section) 'untracked))
+ (not (eq (--when-let (oref section parent)
+ (oref it type))
+ 'diffstat)))))
+ (pcase (list (oref section type)
+ (and siblings t)
+ (magit-diff-use-hunk-region-p)
+ ssection)
+ (`(hunk nil t ,_)
+ (if (magit-section-internal-region-p section) 'region 'hunk))
+ ('(hunk t t nil) 'hunks)
+ (`(hunk ,_ ,_ ,_) 'hunk)
+ ('(file t t nil) 'files)
+ (`(file ,_ ,_ ,_) 'file)
+ ('(module t t nil) 'files)
+ (`(module ,_ ,_ ,_) 'file)
+ (`(,(or 'staged 'unstaged 'untracked) nil ,_ ,_) 'list)))))
+
+(defun magit-diff-use-hunk-region-p ()
+ (and (region-active-p)
+ ;; TODO implement this from first principals
+ ;; currently it's trial-and-error
+ (not (and (or (eq this-command #'mouse-drag-region)
+ (eq last-command #'mouse-drag-region)
+ ;; When another window was previously
+ ;; selected then the last-command is
+ ;; some byte-code function.
+ (byte-code-function-p last-command))
+ (eq (region-end) (region-beginning))))))
+
+;;; Diff Highlight
+
+(add-hook 'magit-section-unhighlight-hook #'magit-diff-unhighlight)
+(add-hook 'magit-section-highlight-hook #'magit-diff-highlight)
+
+(defun magit-diff-unhighlight (section selection)
+ "Remove the highlighting of the diff-related SECTION."
+ (when (magit-hunk-section-p section)
+ (magit-diff-paint-hunk section selection nil)
+ t))
+
+(defun magit-diff-highlight (section selection)
+ "Highlight the diff-related SECTION.
+If SECTION is not a diff-related section, then do nothing and
+return nil. If SELECTION is non-nil, then it is a list of sections
+selected by the region, including SECTION. All of these sections
+are highlighted."
+ (if (and (magit-section-match 'commit section)
+ (oref section children))
+ (progn (if selection
+ (dolist (section selection)
+ (magit-diff-highlight-list section selection))
+ (magit-diff-highlight-list section))
+ t)
+ (when-let ((scope (magit-diff-scope section t)))
+ (cond ((eq scope 'region)
+ (magit-diff-paint-hunk section selection t))
+ (selection
+ (dolist (section selection)
+ (magit-diff-highlight-recursive section selection)))
+ (t
+ (magit-diff-highlight-recursive section)))
+ t)))
+
+(defun magit-diff-highlight-recursive (section &optional selection)
+ (pcase (magit-diff-scope section)
+ ('list (magit-diff-highlight-list section selection))
+ ('file (magit-diff-highlight-file section selection))
+ ('hunk (magit-diff-highlight-heading section selection)
+ (magit-diff-paint-hunk section selection t))
+ (_ (magit-section-highlight section nil))))
+
+(defun magit-diff-highlight-list (section &optional selection)
+ (let ((beg (oref section start))
+ (cnt (oref section content))
+ (end (oref section end)))
+ (when (or (eq this-command #'mouse-drag-region)
+ (not selection))
+ (unless (and (region-active-p)
+ (<= (region-beginning) beg))
+ (magit-section-make-overlay beg cnt 'magit-section-highlight))
+ (if (oref section hidden)
+ (oset section washer #'ignore)
+ (dolist (child (oref section children))
+ (when (or (eq this-command #'mouse-drag-region)
+ (not (and (region-active-p)
+ (<= (region-beginning)
+ (oref child start)))))
+ (magit-diff-highlight-recursive child selection)))))
+ (when magit-diff-highlight-hunk-body
+ (magit-section-make-overlay (1- end) end 'magit-section-highlight))))
+
+(defun magit-diff-highlight-file (section &optional selection)
+ (magit-diff-highlight-heading section selection)
+ (when (or (not (oref section hidden))
+ (cl-typep section 'magit-module-section))
+ (dolist (child (oref section children))
+ (magit-diff-highlight-recursive child selection))))
+
+(defun magit-diff-highlight-heading (section &optional selection)
+ (magit-section-make-overlay
+ (oref section start)
+ (or (oref section content)
+ (oref section end))
+ (pcase (list (oref section type)
+ (and (member section selection)
+ (not (eq this-command #'mouse-drag-region))))
+ ('(file t) 'magit-diff-file-heading-selection)
+ ('(file nil) 'magit-diff-file-heading-highlight)
+ ('(module t) 'magit-diff-file-heading-selection)
+ ('(module nil) 'magit-diff-file-heading-highlight)
+ ('(hunk t) 'magit-diff-hunk-heading-selection)
+ ('(hunk nil) 'magit-diff-hunk-heading-highlight))))
+
+;;; Hunk Paint
+
+(cl-defun magit-diff-paint-hunk
+ (section &optional selection
+ (highlight (magit-section-selected-p section selection)))
+ (let (paint)
+ (unless magit-diff-highlight-hunk-body
+ (setq highlight nil))
+ (cond (highlight
+ (unless (oref section hidden)
+ (add-to-list 'magit-section-highlighted-sections section)
+ (cond ((memq section magit-section-unhighlight-sections)
+ (setq magit-section-unhighlight-sections
+ (delq section magit-section-unhighlight-sections)))
+ (magit-diff-highlight-hunk-body
+ (setq paint t)))))
+ (t
+ (cond ((and (oref section hidden)
+ (memq section magit-section-unhighlight-sections))
+ (add-to-list 'magit-section-highlighted-sections section)
+ (setq magit-section-unhighlight-sections
+ (delq section magit-section-unhighlight-sections)))
+ (t
+ (setq paint t)))))
+ (when paint
+ (save-excursion
+ (goto-char (oref section start))
+ (let ((end (oref section end))
+ (merging (looking-at "@@@"))
+ (diff-type (magit-diff-type))
+ (stage nil)
+ (tab-width (magit-diff-tab-width
+ (magit-section-parent-value section))))
+ (forward-line)
+ (while (< (point) end)
+ (when (and magit-diff-hide-trailing-cr-characters
+ (char-equal ?\r (char-before (line-end-position))))
+ (put-text-property (1- (line-end-position)) (line-end-position)
+ 'invisible t))
+ (put-text-property
+ (point) (1+ (line-end-position)) 'font-lock-face
+ (cond
+ ((looking-at "^\\+\\+?\\([<=|>]\\)\\{7\\}")
+ (setq stage (pcase (list (match-string 1) highlight)
+ ('("<" nil) 'magit-diff-our)
+ ('("<" t) 'magit-diff-our-highlight)
+ ('("|" nil) 'magit-diff-base)
+ ('("|" t) 'magit-diff-base-highlight)
+ ('("=" nil) 'magit-diff-their)
+ ('("=" t) 'magit-diff-their-highlight)
+ ('(">" nil) nil)))
+ 'magit-diff-conflict-heading)
+ ((looking-at (if merging "^\\(\\+\\| \\+\\)" "^\\+"))
+ (magit-diff-paint-tab merging tab-width)
+ (magit-diff-paint-whitespace merging 'added diff-type)
+ (or stage
+ (if highlight 'magit-diff-added-highlight 'magit-diff-added)))
+ ((looking-at (if merging "^\\(-\\| -\\)" "^-"))
+ (magit-diff-paint-tab merging tab-width)
+ (magit-diff-paint-whitespace merging 'removed diff-type)
+ (if highlight 'magit-diff-removed-highlight 'magit-diff-removed))
+ (t
+ (magit-diff-paint-tab merging tab-width)
+ (magit-diff-paint-whitespace merging 'context diff-type)
+ (if highlight 'magit-diff-context-highlight 'magit-diff-context))))
+ (forward-line))))))
+ (magit-diff-update-hunk-refinement section))
+
+(defvar magit-diff--tab-width-cache nil)
+
+(defun magit-diff-tab-width (file)
+ (setq file (expand-file-name file))
+ (cl-flet ((cache (value)
+ (let ((elt (assoc file magit-diff--tab-width-cache)))
+ (if elt
+ (setcdr elt value)
+ (setq magit-diff--tab-width-cache
+ (cons (cons file value)
+ magit-diff--tab-width-cache))))
+ value))
+ (cond
+ ((not magit-diff-adjust-tab-width)
+ tab-width)
+ ((and-let* ((buffer (find-buffer-visiting file)))
+ (cache (buffer-local-value 'tab-width buffer))))
+ ((and-let* ((elt (assoc file magit-diff--tab-width-cache)))
+ (or (cdr elt)
+ tab-width)))
+ ((or (eq magit-diff-adjust-tab-width 'always)
+ (and (numberp magit-diff-adjust-tab-width)
+ (>= magit-diff-adjust-tab-width
+ (nth 7 (file-attributes file)))))
+ (cache (buffer-local-value 'tab-width (find-file-noselect file))))
+ (t
+ (cache nil)
+ tab-width))))
+
+(defun magit-diff-paint-tab (merging width)
+ (save-excursion
+ (forward-char (if merging 2 1))
+ (while (= (char-after) ?\t)
+ (put-text-property (point) (1+ (point))
+ 'display (list (list 'space :width width)))
+ (forward-char))))
+
+(defun magit-diff-paint-whitespace (merging line-type diff-type)
+ (when (and magit-diff-paint-whitespace
+ (or (not (memq magit-diff-paint-whitespace '(uncommitted status)))
+ (memq diff-type '(staged unstaged)))
+ (cl-case line-type
+ (added t)
+ (removed (memq magit-diff-paint-whitespace-lines '(all both)))
+ (context (memq magit-diff-paint-whitespace-lines '(all)))))
+ (let ((prefix (if merging "^[-\\+\s]\\{2\\}" "^[-\\+\s]"))
+ (indent
+ (if (local-variable-p 'magit-diff-highlight-indentation)
+ magit-diff-highlight-indentation
+ (setq-local
+ magit-diff-highlight-indentation
+ (cdr (--first (string-match-p (car it) default-directory)
+ (nreverse
+ (default-value
+ 'magit-diff-highlight-indentation))))))))
+ (when (and magit-diff-highlight-trailing
+ (looking-at (concat prefix ".*?\\([ \t]+\\)$")))
+ (let ((ov (make-overlay (match-beginning 1) (match-end 1) nil t)))
+ (overlay-put ov 'font-lock-face 'magit-diff-whitespace-warning)
+ (overlay-put ov 'priority 2)
+ (overlay-put ov 'evaporate t)))
+ (when (or (and (eq indent 'tabs)
+ (looking-at (concat prefix "\\( *\t[ \t]*\\)")))
+ (and (integerp indent)
+ (looking-at (format "%s\\([ \t]* \\{%s,\\}[ \t]*\\)"
+ prefix indent))))
+ (let ((ov (make-overlay (match-beginning 1) (match-end 1) nil t)))
+ (overlay-put ov 'font-lock-face 'magit-diff-whitespace-warning)
+ (overlay-put ov 'priority 2)
+ (overlay-put ov 'evaporate t))))))
+
+(defun magit-diff-update-hunk-refinement (&optional section)
+ (if section
+ (unless (oref section hidden)
+ (pcase (list magit-diff-refine-hunk
+ (oref section refined)
+ (eq section (magit-current-section)))
+ ((or `(all nil ,_) '(t nil t))
+ (oset section refined t)
+ (save-excursion
+ (goto-char (oref section start))
+ ;; `diff-refine-hunk' does not handle combined diffs.
+ (unless (looking-at "@@@")
+ (let ((smerge-refine-ignore-whitespace
+ magit-diff-refine-ignore-whitespace)
+ ;; Avoid fsyncing many small temp files
+ (write-region-inhibit-fsync t))
+ (diff-refine-hunk)))))
+ ((or `(nil t ,_) '(t t nil))
+ (oset section refined nil)
+ (remove-overlays (oref section start)
+ (oref section end)
+ 'diff-mode 'fine))))
+ (cl-labels ((recurse (section)
+ (if (magit-section-match 'hunk section)
+ (magit-diff-update-hunk-refinement section)
+ (dolist (child (oref section children))
+ (recurse child)))))
+ (recurse magit-root-section))))
+
+
+;;; Hunk Region
+
+(defun magit-diff-hunk-region-beginning ()
+ (save-excursion (goto-char (region-beginning))
+ (line-beginning-position)))
+
+(defun magit-diff-hunk-region-end ()
+ (save-excursion (goto-char (region-end))
+ (line-end-position)))
+
+(defun magit-diff-update-hunk-region (section)
+ "Highlight the hunk-internal region if any."
+ (when (and (eq (oref section type) 'hunk)
+ (eq (magit-diff-scope section t) 'region))
+ (magit-diff--make-hunk-overlay
+ (oref section start)
+ (1- (oref section content))
+ 'font-lock-face 'magit-diff-lines-heading
+ 'display (magit-diff-hunk-region-header section)
+ 'after-string (magit-diff--hunk-after-string 'magit-diff-lines-heading))
+ (run-hook-with-args 'magit-diff-highlight-hunk-region-functions section)
+ t))
+
+(defun magit-diff-highlight-hunk-region-dim-outside (section)
+ "Dim the parts of the hunk that are outside the hunk-internal region.
+This is done by using the same foreground and background color
+for added and removed lines as for context lines."
+ (let ((face (if magit-diff-highlight-hunk-body
+ 'magit-diff-context-highlight
+ 'magit-diff-context)))
+ (when magit-diff-unmarked-lines-keep-foreground
+ (setq face `(,@(and (>= emacs-major-version 27) '(:extend t))
+ :background ,(face-attribute face :background))))
+ (magit-diff--make-hunk-overlay (oref section content)
+ (magit-diff-hunk-region-beginning)
+ 'font-lock-face face
+ 'priority 2)
+ (magit-diff--make-hunk-overlay (1+ (magit-diff-hunk-region-end))
+ (oref section end)
+ 'font-lock-face face
+ 'priority 2)))
+
+(defun magit-diff-highlight-hunk-region-using-face (_section)
+ "Highlight the hunk-internal region by making it bold.
+Or rather highlight using the face `magit-diff-hunk-region', though
+changing only the `:weight' and/or `:slant' is recommended for that
+face."
+ (magit-diff--make-hunk-overlay (magit-diff-hunk-region-beginning)
+ (1+ (magit-diff-hunk-region-end))
+ 'font-lock-face 'magit-diff-hunk-region))
+
+(defun magit-diff-highlight-hunk-region-using-overlays (section)
+ "Emphasize the hunk-internal region using delimiting horizontal lines.
+This is implemented as single-pixel newlines places inside overlays."
+ (if (window-system)
+ (let ((beg (magit-diff-hunk-region-beginning))
+ (end (magit-diff-hunk-region-end))
+ (str (propertize
+ (concat (propertize "\s" 'display '(space :height (1)))
+ (propertize "\n" 'line-height t))
+ 'font-lock-face 'magit-diff-lines-boundary)))
+ (magit-diff--make-hunk-overlay beg (1+ beg) 'before-string str)
+ (magit-diff--make-hunk-overlay end (1+ end) 'after-string str))
+ (magit-diff-highlight-hunk-region-using-face section)))
+
+(defun magit-diff-highlight-hunk-region-using-underline (section)
+ "Emphasize the hunk-internal region using delimiting horizontal lines.
+This is implemented by overlining and underlining the first and
+last (visual) lines of the region."
+ (if (window-system)
+ (let* ((beg (magit-diff-hunk-region-beginning))
+ (end (magit-diff-hunk-region-end))
+ (beg-eol (save-excursion (goto-char beg)
+ (end-of-visual-line)
+ (point)))
+ (end-bol (save-excursion (goto-char end)
+ (beginning-of-visual-line)
+ (point)))
+ (color (face-background 'magit-diff-lines-boundary nil t)))
+ (cl-flet ((ln (b e &rest face)
+ (magit-diff--make-hunk-overlay
+ b e 'font-lock-face face 'after-string
+ (magit-diff--hunk-after-string face))))
+ (if (= beg end-bol)
+ (ln beg beg-eol :overline color :underline color)
+ (ln beg beg-eol :overline color)
+ (ln end-bol end :underline color))))
+ (magit-diff-highlight-hunk-region-using-face section)))
+
+(defun magit-diff--make-hunk-overlay (start end &rest args)
+ (let ((ov (make-overlay start end nil t)))
+ (overlay-put ov 'evaporate t)
+ (while args (overlay-put ov (pop args) (pop args)))
+ (push ov magit-section--region-overlays)
+ ov))
+
+(defun magit-diff--hunk-after-string (face)
+ (propertize "\s"
+ 'font-lock-face face
+ 'display (list 'space :align-to
+ `(+ (0 . right)
+ ,(min (window-hscroll)
+ (- (line-end-position)
+ (line-beginning-position)))))
+ ;; This prevents the cursor from being rendered at the
+ ;; edge of the window.
+ 'cursor t))
+
+;;; Hunk Utilities
+
+(defun magit-diff-inside-hunk-body-p ()
+ "Return non-nil if point is inside the body of a hunk."
+ (and (magit-section-match 'hunk)
+ (and-let* ((content (oref (magit-current-section) content)))
+ (> (magit-point) content))))
+
+;;; Diff Extract
+
+(defun magit-diff-file-header (section &optional no-rename)
+ (when (magit-hunk-section-p section)
+ (setq section (oref section parent)))
+ (and (magit-file-section-p section)
+ (let ((header (oref section header)))
+ (if no-rename
+ (replace-regexp-in-string
+ "^--- \\(.+\\)" (oref section value) header t t 1)
+ header))))
+
+(defun magit-diff-hunk-region-header (section)
+ (let ((patch (magit-diff-hunk-region-patch section)))
+ (string-match "\n" patch)
+ (substring patch 0 (1- (match-end 0)))))
+
+(defun magit-diff-hunk-region-patch (section &optional args)
+ (let ((op (if (member "--reverse" args) "+" "-"))
+ (sbeg (oref section start))
+ (rbeg (magit-diff-hunk-region-beginning))
+ (rend (region-end))
+ (send (oref section end))
+ (patch nil))
+ (save-excursion
+ (goto-char sbeg)
+ (while (< (point) send)
+ (looking-at "\\(.\\)\\([^\n]*\n\\)")
+ (cond ((or (string-match-p "[@ ]" (match-string-no-properties 1))
+ (and (>= (point) rbeg)
+ (<= (point) rend)))
+ (push (match-string-no-properties 0) patch))
+ ((equal op (match-string-no-properties 1))
+ (push (concat " " (match-string-no-properties 2)) patch)))
+ (forward-line)))
+ (let ((buffer-list-update-hook nil)) ; #3759
+ (with-temp-buffer
+ (insert (mapconcat #'identity (reverse patch) ""))
+ (diff-fixup-modifs (point-min) (point-max))
+ (setq patch (buffer-string))))
+ patch))
+
+;;; _
+(provide 'magit-diff)
+;;; magit-diff.el ends here