dotemacs

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

magit-ediff.el (19342B)


      1 ;;; magit-ediff.el --- Ediff extension for Magit  -*- lexical-binding: t -*-
      2 
      3 ;; Copyright (C) 2010-2021  The Magit Project Contributors
      4 ;;
      5 ;; You should have received a copy of the AUTHORS.md file which
      6 ;; lists all contributors.  If not, see http://magit.vc/authors.
      7 
      8 ;; Author: Jonas Bernoulli <jonas@bernoul.li>
      9 ;; Maintainer: Jonas Bernoulli <jonas@bernoul.li>
     10 
     11 ;; SPDX-License-Identifier: GPL-3.0-or-later
     12 
     13 ;; Magit is free software; you can redistribute it and/or modify it
     14 ;; under the terms of the GNU General Public License as published by
     15 ;; the Free Software Foundation; either version 3, or (at your option)
     16 ;; any later version.
     17 ;;
     18 ;; Magit is distributed in the hope that it will be useful, but WITHOUT
     19 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     20 ;; or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
     21 ;; License for more details.
     22 ;;
     23 ;; You should have received a copy of the GNU General Public License
     24 ;; along with Magit.  If not, see http://www.gnu.org/licenses.
     25 
     26 ;;; Commentary:
     27 
     28 ;; This library provides basic support for Ediff.
     29 
     30 ;;; Code:
     31 
     32 (require 'magit)
     33 
     34 (require 'ediff)
     35 (require 'smerge-mode)
     36 
     37 (defvar smerge-ediff-buf)
     38 (defvar smerge-ediff-windows)
     39 
     40 ;;; Options
     41 
     42 (defgroup magit-ediff nil
     43   "Ediff support for Magit."
     44   :link '(info-link "(magit)Ediffing")
     45   :group 'magit-extensions)
     46 
     47 (defcustom magit-ediff-quit-hook
     48   '(magit-ediff-cleanup-auxiliary-buffers
     49     magit-ediff-restore-previous-winconf)
     50   "Hooks to run after finishing Ediff, when that was invoked using Magit.
     51 The hooks are run in the Ediff control buffer.  This is similar
     52 to `ediff-quit-hook' but takes the needs of Magit into account.
     53 The `ediff-quit-hook' is ignored by Ediff sessions which were
     54 invoked using Magit."
     55   :package-version '(magit . "2.2.0")
     56   :group 'magit-ediff
     57   :type 'hook
     58   :get 'magit-hook-custom-get
     59   :options '(magit-ediff-cleanup-auxiliary-buffers
     60              magit-ediff-restore-previous-winconf))
     61 
     62 (defcustom magit-ediff-dwim-show-on-hunks nil
     63   "Whether `magit-ediff-dwim' runs show variants on hunks.
     64 If non-nil, `magit-ediff-show-staged' or
     65 `magit-ediff-show-unstaged' are called based on what section the
     66 hunk is in.  Otherwise, `magit-ediff-dwim' runs
     67 `magit-ediff-stage' when point is on an uncommitted hunk."
     68   :package-version '(magit . "2.2.0")
     69   :group 'magit-ediff
     70   :type 'boolean)
     71 
     72 (defcustom magit-ediff-show-stash-with-index t
     73   "Whether `magit-ediff-show-stash' shows the state of the index.
     74 
     75 If non-nil, use a third Ediff buffer to distinguish which changes
     76 in the stash were staged.  In cases where the stash contains no
     77 staged changes, fall back to a two-buffer Ediff.
     78 
     79 More specifically, a stash is a merge commit, stash@{N}, with
     80 potentially three parents.
     81 
     82 * stash@{N}^1 represents the `HEAD' commit at the time the stash
     83   was created.
     84 
     85 * stash@{N}^2 records any changes that were staged when the stash
     86   was made.
     87 
     88 * stash@{N}^3, if it exists, contains files that were untracked
     89   when stashing.
     90 
     91 If this option is non-nil, `magit-ediff-show-stash' will run
     92 Ediff on a file using three buffers: one for stash@{N}, another
     93 for stash@{N}^1, and a third for stash@{N}^2.
     94 
     95 Otherwise, Ediff uses two buffers, comparing
     96 stash@{N}^1..stash@{N}.  Along with any unstaged changes, changes
     97 in the index commit, stash@{N}^2, will be shown in this
     98 comparison unless they conflicted with changes in the working
     99 tree at the time of stashing."
    100   :package-version '(magit . "2.6.0")
    101   :group 'magit-ediff
    102   :type 'boolean)
    103 
    104 (defcustom magit-ediff-use-indirect-buffers nil
    105   "Whether to use indirect buffers."
    106   :package-version '(magit . "3.1.0")
    107   :group 'magit-ediff
    108   :type 'boolean)
    109 
    110 ;;; Commands
    111 
    112 (defvar magit-ediff-previous-winconf nil)
    113 
    114 ;;;###autoload (autoload 'magit-ediff "magit-ediff" nil)
    115 (transient-define-prefix magit-ediff ()
    116   "Show differences using the Ediff package."
    117   :info-manual "(ediff)"
    118   ["Ediff"
    119    [("E" "Dwim"          magit-ediff-dwim)
    120     ("s" "Stage"         magit-ediff-stage)
    121     ("m" "Resolve"       magit-ediff-resolve)]
    122    [("u" "Show unstaged" magit-ediff-show-unstaged)
    123     ("i" "Show staged"   magit-ediff-show-staged)
    124     ("w" "Show worktree" magit-ediff-show-working-tree)]
    125    [("c" "Show commit"   magit-ediff-show-commit)
    126     ("r" "Show range"    magit-ediff-compare)
    127     ("z" "Show stash"    magit-ediff-show-stash)]])
    128 
    129 ;;;###autoload
    130 (defun magit-ediff-resolve (file)
    131   "Resolve outstanding conflicts in FILE using Ediff.
    132 FILE has to be relative to the top directory of the repository.
    133 
    134 In the rare event that you want to manually resolve all
    135 conflicts, including those already resolved by Git, use
    136 `ediff-merge-revisions-with-ancestor'."
    137   (interactive
    138    (let ((current  (magit-current-file))
    139          (unmerged (magit-unmerged-files)))
    140      (unless unmerged
    141        (user-error "There are no unresolved conflicts"))
    142      (list (magit-completing-read "Resolve file" unmerged nil t nil nil
    143                                   (car (member current unmerged))))))
    144   (magit-with-toplevel
    145     (with-current-buffer (find-file-noselect file)
    146       (smerge-ediff)
    147       (setq-local
    148        ediff-quit-hook
    149        (lambda ()
    150          (let ((bufC ediff-buffer-C)
    151                (bufS smerge-ediff-buf))
    152            (with-current-buffer bufS
    153              (when (yes-or-no-p (format "Conflict resolution finished; save %s? "
    154                                         buffer-file-name))
    155                (erase-buffer)
    156                (insert-buffer-substring bufC)
    157                (save-buffer))))
    158          (when (buffer-live-p ediff-buffer-A) (kill-buffer ediff-buffer-A))
    159          (when (buffer-live-p ediff-buffer-B) (kill-buffer ediff-buffer-B))
    160          (when (buffer-live-p ediff-buffer-C) (kill-buffer ediff-buffer-C))
    161          (when (buffer-live-p ediff-ancestor-buffer)
    162            (kill-buffer ediff-ancestor-buffer))
    163          (let ((magit-ediff-previous-winconf smerge-ediff-windows))
    164            (run-hooks 'magit-ediff-quit-hook)))))))
    165 
    166 (defmacro magit-ediff-buffers (quit &rest spec)
    167   (declare (indent 1))
    168   (let ((fn (if (= (length spec) 3) 'ediff-buffers3 'ediff-buffers))
    169         (char ?@)
    170         get make kill)
    171     (pcase-dolist (`(,g ,m) spec)
    172       (let ((b (intern (format "buf%c" (cl-incf char)))))
    173         (push `(,b ,g) get)
    174         (push `(if ,b
    175                    (if magit-ediff-use-indirect-buffers
    176                        (prog1
    177                            (make-indirect-buffer
    178                             ,b (generate-new-buffer-name (buffer-name ,b)) t)
    179                          (setq ,b nil))
    180                      ,b)
    181                  ,m)
    182               make)
    183         (push `(unless ,b
    184                  (ediff-kill-buffer-carefully
    185                   ,(intern (format "ediff-buffer-%c" char))))
    186               kill)))
    187     (setq get  (nreverse get))
    188     (setq make (nreverse make))
    189     (setq kill (nreverse kill))
    190     `(magit-with-toplevel
    191        (let ((conf (current-window-configuration))
    192              ,@get)
    193          (,fn
    194           ,@make
    195           (list (lambda ()
    196                   (setq-local
    197                    ediff-quit-hook
    198                    (list ,@(and quit (list quit))
    199                          (lambda ()
    200                            ,@kill
    201                            (let ((magit-ediff-previous-winconf conf))
    202                              (run-hooks 'magit-ediff-quit-hook)))))))
    203           ',fn)))))
    204 
    205 ;;;###autoload
    206 (defun magit-ediff-stage (file)
    207   "Stage and unstage changes to FILE using Ediff.
    208 FILE has to be relative to the top directory of the repository."
    209   (interactive
    210    (let ((files (magit-tracked-files)))
    211      (list (magit-completing-read "Selectively stage file" files nil t nil nil
    212                                   (car (member (magit-current-file) files))))))
    213   (magit-with-toplevel
    214     (let* ((bufA  (magit-get-revision-buffer "HEAD" file))
    215            (bufB  (magit-get-revision-buffer "{index}" file))
    216            (lockB (and bufB (buffer-local-value 'buffer-read-only bufB)))
    217            (bufC  (get-file-buffer file))
    218            ;; Use the same encoding for all three buffers or we
    219            ;; may end up changing the file in an unintended way.
    220            (bufC* (or bufC (find-file-noselect file)))
    221            (coding-system-for-read
    222             (buffer-local-value 'buffer-file-coding-system bufC*))
    223            (bufA* (magit-find-file-noselect-1 "HEAD" file t))
    224            (bufB* (magit-find-file-index-noselect file t)))
    225       (setf (buffer-local-value 'buffer-read-only bufB*) nil)
    226       (magit-ediff-buffers
    227           (lambda ()
    228             (when (buffer-live-p ediff-buffer-B)
    229               (when lockB
    230                 (setf (buffer-local-value 'buffer-read-only bufB) t))
    231               (when (buffer-modified-p ediff-buffer-B)
    232                 (with-current-buffer ediff-buffer-B
    233                   (magit-update-index))))
    234             (when (and (buffer-live-p ediff-buffer-C)
    235                        (buffer-modified-p ediff-buffer-C))
    236               (with-current-buffer ediff-buffer-C
    237                 (when (y-or-n-p (format "Save file %s? " buffer-file-name))
    238                   (save-buffer)))))
    239         (bufA bufA*)
    240         (bufB bufB*)
    241         (bufC bufC*)))))
    242 
    243 ;;;###autoload
    244 (defun magit-ediff-compare (revA revB fileA fileB)
    245   "Compare REVA:FILEA with REVB:FILEB using Ediff.
    246 
    247 FILEA and FILEB have to be relative to the top directory of the
    248 repository.  If REVA or REVB is nil, then this stands for the
    249 working tree state.
    250 
    251 If the region is active, use the revisions on the first and last
    252 line of the region.  With a prefix argument, instead of diffing
    253 the revisions, choose a revision to view changes along, starting
    254 at the common ancestor of both revisions (i.e., use a \"...\"
    255 range)."
    256   (interactive
    257    (pcase-let ((`(,revA ,revB) (magit-ediff-compare--read-revisions
    258                                 nil current-prefix-arg)))
    259      (nconc (list revA revB)
    260             (magit-ediff-read-files revA revB))))
    261   (magit-ediff-buffers nil
    262     ((if revA (magit-get-revision-buffer revA fileA) (get-file-buffer    fileA))
    263      (if revA (magit-find-file-noselect  revA fileA) (find-file-noselect fileA)))
    264     ((if revB (magit-get-revision-buffer revB fileB) (get-file-buffer    fileB))
    265      (if revB (magit-find-file-noselect  revB fileB) (find-file-noselect fileB)))))
    266 
    267 (defun magit-ediff-compare--read-revisions (&optional arg mbase)
    268   (let ((input (or arg (magit-diff-read-range-or-commit
    269                         "Compare range or commit"
    270                         nil mbase))))
    271     (--if-let (magit-split-range input)
    272         (-cons-to-list it)
    273       (list input nil))))
    274 
    275 (defun magit-ediff-read-files (revA revB &optional fileB)
    276   "Read file in REVB, return it and the corresponding file in REVA.
    277 When FILEB is non-nil, use this as REVB's file instead of
    278 prompting for it."
    279   (unless fileB
    280     (setq fileB (magit-read-file-choice
    281                  (format "File to compare between %s and %s"
    282                          revA (or revB "the working tree"))
    283                  (magit-changed-files revA revB)
    284                  (format "No changed files between %s and %s"
    285                          revA (or revB "the working tree")))))
    286   (list (or (car (member fileB (magit-revision-files revA)))
    287             (cdr (assoc fileB (magit-renamed-files revB revA)))
    288             (magit-read-file-choice
    289              (format "File in %s to compare with %s in %s"
    290                      revA fileB (or revB "the working tree"))
    291              (magit-changed-files revB revA)
    292              (format "No files have changed between %s and %s"
    293                      revA revB)))
    294         fileB))
    295 
    296 ;;;###autoload
    297 (defun magit-ediff-dwim ()
    298   "Compare, stage, or resolve using Ediff.
    299 This command tries to guess what file, and what commit or range
    300 the user wants to compare, stage, or resolve using Ediff.  It
    301 might only be able to guess either the file, or range or commit,
    302 in which case the user is asked about the other.  It might not
    303 always guess right, in which case the appropriate `magit-ediff-*'
    304 command has to be used explicitly.  If it cannot read the user's
    305 mind at all, then it asks the user for a command to run."
    306   (interactive)
    307   (magit-section-case
    308     (hunk (save-excursion
    309             (goto-char (oref (oref it parent) start))
    310             (magit-ediff-dwim)))
    311     (t
    312      (let ((range (magit-diff--dwim))
    313            (file (magit-current-file))
    314            command revA revB)
    315        (pcase range
    316          ((and (guard (not magit-ediff-dwim-show-on-hunks))
    317                (or `unstaged `staged))
    318           (setq command (if (magit-anything-unmerged-p)
    319                             #'magit-ediff-resolve
    320                           #'magit-ediff-stage)))
    321          (`unstaged (setq command #'magit-ediff-show-unstaged))
    322          (`staged (setq command #'magit-ediff-show-staged))
    323          (`(commit . ,value)
    324           (setq command #'magit-ediff-show-commit)
    325           (setq revB value))
    326          (`(stash . ,value)
    327           (setq command #'magit-ediff-show-stash)
    328           (setq revB value))
    329          ((pred stringp)
    330           (pcase-let ((`(,a ,b) (magit-ediff-compare--read-revisions range)))
    331             (setq command #'magit-ediff-compare)
    332             (setq revA a)
    333             (setq revB b)))
    334          (_
    335           (when (derived-mode-p 'magit-diff-mode)
    336             (pcase (magit-diff-type)
    337               (`committed (pcase-let ((`(,a ,b)
    338                                        (magit-ediff-compare--read-revisions
    339                                         magit-buffer-range)))
    340                             (setq revA a)
    341                             (setq revB b)))
    342               ((guard (not magit-ediff-dwim-show-on-hunks))
    343                (setq command #'magit-ediff-stage))
    344               (`unstaged  (setq command #'magit-ediff-show-unstaged))
    345               (`staged    (setq command #'magit-ediff-show-staged))
    346               (`undefined (setq command nil))
    347               (_          (setq command nil))))))
    348        (cond ((not command)
    349               (call-interactively
    350                (magit-read-char-case
    351                    "Failed to read your mind; do you want to " t
    352                  (?c "[c]ommit"  'magit-ediff-show-commit)
    353                  (?r "[r]ange"   'magit-ediff-compare)
    354                  (?s "[s]tage"   'magit-ediff-stage)
    355                  (?v "resol[v]e" 'magit-ediff-resolve))))
    356              ((eq command 'magit-ediff-compare)
    357               (apply 'magit-ediff-compare revA revB
    358                      (magit-ediff-read-files revA revB file)))
    359              ((eq command 'magit-ediff-show-commit)
    360               (magit-ediff-show-commit revB))
    361              ((eq command 'magit-ediff-show-stash)
    362               (magit-ediff-show-stash revB))
    363              (file
    364               (funcall command file))
    365              (t
    366               (call-interactively command)))))))
    367 
    368 ;;;###autoload
    369 (defun magit-ediff-show-staged (file)
    370   "Show staged changes using Ediff.
    371 
    372 This only allows looking at the changes; to stage, unstage,
    373 and discard changes using Ediff, use `magit-ediff-stage'.
    374 
    375 FILE must be relative to the top directory of the repository."
    376   (interactive
    377    (list (magit-read-file-choice "Show staged changes for file"
    378                                  (magit-staged-files)
    379                                  "No staged files")))
    380   (magit-ediff-buffers nil
    381     ((magit-get-revision-buffer "HEAD" file)
    382      (magit-find-file-noselect "HEAD" file))
    383     ((get-buffer (concat file ".~{index}~"))
    384      (magit-find-file-index-noselect file t))))
    385 
    386 ;;;###autoload
    387 (defun magit-ediff-show-unstaged (file)
    388   "Show unstaged changes using Ediff.
    389 
    390 This only allows looking at the changes; to stage, unstage,
    391 and discard changes using Ediff, use `magit-ediff-stage'.
    392 
    393 FILE must be relative to the top directory of the repository."
    394   (interactive
    395    (list (magit-read-file-choice "Show unstaged changes for file"
    396                                  (magit-unstaged-files)
    397                                  "No unstaged files")))
    398   (magit-ediff-buffers nil
    399     ((get-buffer (concat file ".~{index}~"))
    400      (magit-find-file-index-noselect file t))
    401     ((get-file-buffer file)
    402      (find-file-noselect file))))
    403 
    404 ;;;###autoload
    405 (defun magit-ediff-show-working-tree (file)
    406   "Show changes between `HEAD' and working tree using Ediff.
    407 FILE must be relative to the top directory of the repository."
    408   (interactive
    409    (list (magit-read-file-choice "Show changes in file"
    410                                  (magit-changed-files "HEAD")
    411                                  "No changed files")))
    412   (magit-ediff-buffers nil
    413     ((magit-get-revision-buffer "HEAD" file)
    414      (magit-find-file-noselect  "HEAD" file))
    415     ((get-file-buffer file)
    416      (find-file-noselect file))))
    417 
    418 ;;;###autoload
    419 (defun magit-ediff-show-commit (commit)
    420   "Show changes introduced by COMMIT using Ediff."
    421   (interactive (list (magit-read-branch-or-commit "Revision")))
    422   (let ((revA (concat commit "^"))
    423         (revB commit))
    424     (apply #'magit-ediff-compare
    425            revA revB
    426            (magit-ediff-read-files revA revB (magit-current-file)))))
    427 
    428 ;;;###autoload
    429 (defun magit-ediff-show-stash (stash)
    430   "Show changes introduced by STASH using Ediff.
    431 `magit-ediff-show-stash-with-index' controls whether a
    432 three-buffer Ediff is used in order to distinguish changes in the
    433 stash that were staged."
    434   (interactive (list (magit-read-stash "Stash")))
    435   (pcase-let* ((revA (concat stash "^1"))
    436                (revB (concat stash "^2"))
    437                (revC stash)
    438                (`(,fileA ,fileC) (magit-ediff-read-files revA revC))
    439                (fileB fileC))
    440     (if (and magit-ediff-show-stash-with-index
    441              (member fileA (magit-changed-files revB revA)))
    442         (magit-ediff-buffers nil
    443           ((magit-get-revision-buffer revA fileA)
    444            (magit-find-file-noselect  revA fileA))
    445           ((magit-get-revision-buffer revB fileB)
    446            (magit-find-file-noselect  revB fileB))
    447           ((magit-get-revision-buffer revC fileC)
    448            (magit-find-file-noselect  revC fileC)))
    449       (magit-ediff-compare revA revC fileA fileC))))
    450 
    451 (defun magit-ediff-cleanup-auxiliary-buffers ()
    452   (let* ((ctl-buf ediff-control-buffer)
    453          (ctl-win (ediff-get-visible-buffer-window ctl-buf))
    454          (ctl-frm ediff-control-frame)
    455          (main-frame (cond ((window-live-p ediff-window-A)
    456                             (window-frame ediff-window-A))
    457                            ((window-live-p ediff-window-B)
    458                             (window-frame ediff-window-B)))))
    459     (ediff-kill-buffer-carefully ediff-diff-buffer)
    460     (ediff-kill-buffer-carefully ediff-custom-diff-buffer)
    461     (ediff-kill-buffer-carefully ediff-fine-diff-buffer)
    462     (ediff-kill-buffer-carefully ediff-tmp-buffer)
    463     (ediff-kill-buffer-carefully ediff-error-buffer)
    464     (ediff-kill-buffer-carefully ediff-msg-buffer)
    465     (ediff-kill-buffer-carefully ediff-debug-buffer)
    466     (when (boundp 'ediff-patch-diagnostics)
    467       (ediff-kill-buffer-carefully ediff-patch-diagnostics))
    468     (cond ((and (ediff-window-display-p)
    469                 (frame-live-p ctl-frm))
    470            (delete-frame ctl-frm))
    471           ((window-live-p ctl-win)
    472            (delete-window ctl-win)))
    473     (ediff-kill-buffer-carefully ctl-buf)
    474     (when (frame-live-p main-frame)
    475       (select-frame main-frame))))
    476 
    477 (defun magit-ediff-restore-previous-winconf ()
    478   (set-window-configuration magit-ediff-previous-winconf))
    479 
    480 ;;; _
    481 (provide 'magit-ediff)
    482 ;;; magit-ediff.el ends here