| 1 | ;;; magit-ediff.el --- Ediff extension for Magit -*- lexical-binding:t -*- |
| 2 | |
| 3 | ;; Copyright (C) 2008-2026 The Magit Project Contributors |
| 4 | |
| 5 | ;; Author: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev> |
| 6 | ;; Maintainer: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev> |
| 7 | |
| 8 | ;; SPDX-License-Identifier: GPL-3.0-or-later |
| 9 | |
| 10 | ;; Magit is free software: you can redistribute it and/or modify it |
| 11 | ;; under the terms of the GNU General Public License as published by |
| 12 | ;; the Free Software Foundation, either version 3 of the License, or |
| 13 | ;; (at your option) any later version. |
| 14 | ;; |
| 15 | ;; Magit is distributed in the hope that it will be useful, but WITHOUT |
| 16 | ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY |
| 17 | ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public |
| 18 | ;; License for more details. |
| 19 | ;; |
| 20 | ;; You should have received a copy of the GNU General Public License |
| 21 | ;; along with Magit. If not, see <https://www.gnu.org/licenses/>. |
| 22 | |
| 23 | ;;; Commentary: |
| 24 | |
| 25 | ;; This library provides basic support for Ediff. |
| 26 | |
| 27 | ;;; Code: |
| 28 | |
| 29 | (require 'magit) |
| 30 | |
| 31 | (require 'ediff) |
| 32 | (require 'smerge-mode) |
| 33 | |
| 34 | (defvar smerge-ediff-buf) |
| 35 | (defvar smerge-ediff-windows) |
| 36 | |
| 37 | ;;; Options |
| 38 | |
| 39 | (defgroup magit-ediff nil |
| 40 | "Ediff support for Magit." |
| 41 | :link '(info-link "(magit)Ediffing") |
| 42 | :group 'magit-extensions) |
| 43 | |
| 44 | (defcustom magit-ediff-quit-hook |
| 45 | (list #'magit-ediff-restore-previous-winconf) |
| 46 | "Hooks to run after finishing Ediff, when that was invoked using Magit. |
| 47 | The hooks are run in the Ediff control buffer. This is similar |
| 48 | to `ediff-quit-hook' but takes the needs of Magit into account. |
| 49 | The `ediff-quit-hook' is ignored by Ediff sessions which were |
| 50 | invoked using Magit." |
| 51 | :package-version '(magit . "4.6.0") |
| 52 | :group 'magit-ediff |
| 53 | :type 'hook |
| 54 | :get #'magit-hook-custom-get |
| 55 | :options (list #'magit-ediff-restore-previous-winconf)) |
| 56 | |
| 57 | (defcustom magit-ediff-dwim-resolve-function #'magit-ediff-resolve-rest |
| 58 | "The function `magit-ediff-dwim' uses to resolve conflicts." |
| 59 | :package-version '(magit . "4.0.0") |
| 60 | :group 'magit-ediff |
| 61 | :type '(choice (const magit-ediff-resolve-rest) |
| 62 | (const magit-ediff-resolve-all) |
| 63 | (const magit-git-mergetool))) |
| 64 | |
| 65 | (defcustom magit-ediff-dwim-show-on-hunks nil |
| 66 | "Whether `magit-ediff-dwim' runs show variants on hunks. |
| 67 | If non-nil, `magit-ediff-show-staged' or |
| 68 | `magit-ediff-show-unstaged' are called based on what section the |
| 69 | hunk is in. Otherwise, `magit-ediff-dwim' runs |
| 70 | `magit-ediff-stage' when point is on an uncommitted hunk." |
| 71 | :package-version '(magit . "2.2.0") |
| 72 | :group 'magit-ediff |
| 73 | :type 'boolean) |
| 74 | |
| 75 | (defcustom magit-ediff-show-stash-with-index t |
| 76 | "Whether `magit-ediff-show-stash' shows the state of the index. |
| 77 | |
| 78 | If non-nil, use a third Ediff buffer to distinguish which changes |
| 79 | in the stash were staged. In cases where the stash contains no |
| 80 | staged changes, fall back to a two-buffer Ediff. |
| 81 | |
| 82 | More specifically, a stash is a merge commit, stash@{N}, with |
| 83 | potentially three parents. |
| 84 | |
| 85 | * stash@{N}^1 represents the `HEAD' commit at the time the stash |
| 86 | was created. |
| 87 | |
| 88 | * stash@{N}^2 records any changes that were staged when the stash |
| 89 | was made. |
| 90 | |
| 91 | * stash@{N}^3, if it exists, contains files that were untracked |
| 92 | when stashing. |
| 93 | |
| 94 | If this option is non-nil, `magit-ediff-show-stash' will run |
| 95 | Ediff on a file using three buffers: one for stash@{N}, another |
| 96 | for stash@{N}^1, and a third for stash@{N}^2. |
| 97 | |
| 98 | Otherwise, Ediff uses two buffers, comparing |
| 99 | stash@{N}^1..stash@{N}. Along with any unstaged changes, changes |
| 100 | in the index commit, stash@{N}^2, will be shown in this |
| 101 | comparison unless they conflicted with changes in the working |
| 102 | tree at the time of stashing." |
| 103 | :package-version '(magit . "2.6.0") |
| 104 | :group 'magit-ediff |
| 105 | :type 'boolean) |
| 106 | |
| 107 | (defvar magit-ediff-use-indirect-buffers nil |
| 108 | "Whether to use indirect buffers. |
| 109 | Ediff already does a lot of buffer and file shuffling and I |
| 110 | recommend you do not further complicate that by enabling this.") |
| 111 | |
| 112 | ;;; Commands |
| 113 | |
| 114 | (defvar magit-ediff-previous-winconf nil) |
| 115 | |
| 116 | ;;;###autoload(autoload 'magit-ediff "magit-ediff" nil t) |
| 117 | (transient-define-prefix magit-ediff () |
| 118 | "Show differences using the Ediff package." |
| 119 | :info-manual "(ediff)" |
| 120 | ["Ediff" |
| 121 | [("E" "Dwim" magit-ediff-dwim) |
| 122 | ("s" "Stage" magit-ediff-stage)] |
| 123 | [("m" "Resolve rest" magit-ediff-resolve-rest) |
| 124 | ("M" "Resolve all conflicts" magit-ediff-resolve-all) |
| 125 | ("t" "Resolve using mergetool" magit-git-mergetool)] |
| 126 | [("u" "Show unstaged" magit-ediff-show-unstaged) |
| 127 | ("i" "Show staged" magit-ediff-show-staged) |
| 128 | ("w" "Show worktree" magit-ediff-show-working-tree)] |
| 129 | [("c" "Show commit" magit-ediff-show-commit) |
| 130 | ("r" "Show range" magit-ediff-compare) |
| 131 | ("z" "Show stash" magit-ediff-show-stash)]]) |
| 132 | |
| 133 | ;;;###autoload(autoload 'magit-ediff-resolve-all "magit-ediff" nil t) |
| 134 | (transient-define-suffix magit-ediff-resolve-all (file) |
| 135 | "Resolve all conflicts in the FILE at point using Ediff. |
| 136 | |
| 137 | If there is no file at point or if it doesn't have any unmerged |
| 138 | changes, then prompt for a file. |
| 139 | |
| 140 | See info node `(magit) Ediffing' for more information about this |
| 141 | and alternative commands." |
| 142 | :inapt-if-not #'magit-anything-unmerged-p |
| 143 | (interactive (list (magit-read-unmerged-file))) |
| 144 | (magit-with-toplevel |
| 145 | (let* ((dir (magit-gitdir)) |
| 146 | (revA (or (magit-name-branch "HEAD") |
| 147 | (magit-commit-oid "HEAD"))) |
| 148 | (revB (cl-find-if (##file-exists-p (expand-file-name % dir)) |
| 149 | '("MERGE_HEAD" "CHERRY_PICK_HEAD" "REVERT_HEAD"))) |
| 150 | (revB (or (magit-name-branch revB) |
| 151 | (magit-commit-oid revB))) |
| 152 | (revC (magit-commit-oid (magit-merge-base revA revB))) |
| 153 | (fileA (magit--rev-file-name file revA revB)) |
| 154 | (fileB (magit--rev-file-name file revB revA)) |
| 155 | (fileC (or (magit--rev-file-name file revC revA) |
| 156 | (magit--rev-file-name file revC revB)))) |
| 157 | ;; Ediff assumes that the FILE where it is going to store the merge |
| 158 | ;; result does not exist yet, so move the existing file out of the |
| 159 | ;; way. If a buffer visits FILE, then we have to kill that upfront. |
| 160 | (when-let ((buffer (find-buffer-visiting file))) |
| 161 | (when (and (buffer-modified-p buffer) |
| 162 | (not (y-or-n-p (format "Save buffer %s %s? " |
| 163 | (buffer-name buffer) |
| 164 | "(cannot continue otherwise)")))) |
| 165 | (user-error "Abort")) |
| 166 | (kill-buffer buffer)) |
| 167 | (let ((orig (concat file ".ORIG"))) |
| 168 | (when (file-exists-p orig) |
| 169 | (rename-file orig (make-temp-name (concat orig "_")))) |
| 170 | (rename-file file orig)) |
| 171 | (let ((setup (lambda () |
| 172 | ;; Use the same conflict marker style as Git uses. |
| 173 | (setq-local ediff-combination-pattern |
| 174 | '("<<<<<<< HEAD" A |
| 175 | ,(format "||||||| %s" revC) Ancestor |
| 176 | "=======" B |
| 177 | ,(format ">>>>>>> %s" revB))))) |
| 178 | (quit (lambda () |
| 179 | ;; For merge jobs Ediff switches buffer names around. |
| 180 | ;; At this point `ediff-buffer-C' no longer refers to |
| 181 | ;; the ancestor buffer but to the merge result buffer. |
| 182 | ;; See (if ediff-merge-job ...) in `ediff-setup'. |
| 183 | (when (buffer-live-p ediff-buffer-C) |
| 184 | (with-current-buffer ediff-buffer-C |
| 185 | (save-buffer) |
| 186 | (save-excursion |
| 187 | (goto-char (point-min)) |
| 188 | (unless (re-search-forward "^<<<<<<< " nil t) |
| 189 | (magit-stage-files (list file))))))))) |
| 190 | (magit-ediff-buffers |
| 191 | (magit-ediff--find-file revA fileA) |
| 192 | (magit-ediff--find-file revB fileB) |
| 193 | (and fileC (magit-ediff--find-file revC fileC)) |
| 194 | setup quit file))))) |
| 195 | |
| 196 | ;;;###autoload(autoload 'magit-ediff-resolve-rest "magit-ediff" nil t) |
| 197 | (transient-define-suffix magit-ediff-resolve-rest (file) |
| 198 | "Resolve outstanding conflicts in the FILE at point using Ediff. |
| 199 | |
| 200 | If there is no file at point or if it doesn't have any unmerged |
| 201 | changes, then prompt for a file. |
| 202 | |
| 203 | See info node `(magit) Ediffing' for more information about this |
| 204 | and alternative commands." |
| 205 | :inapt-if-not #'magit-anything-unmerged-p |
| 206 | (interactive (list (magit-read-unmerged-file))) |
| 207 | (magit-with-toplevel |
| 208 | (let ((winconf (current-window-configuration))) |
| 209 | (with-current-buffer (find-file-noselect file) |
| 210 | (smerge-ediff) |
| 211 | (setq-local ediff-quit-merge-hook nil) |
| 212 | (setq-local ediff-quit-hook nil) |
| 213 | (add-hook 'ediff-quit-hook |
| 214 | (lambda () |
| 215 | (let ((bufC ediff-buffer-C) |
| 216 | (bufS smerge-ediff-buf)) |
| 217 | (with-current-buffer bufS |
| 218 | (when (yes-or-no-p |
| 219 | (format "Conflict resolution finished; save %s? " |
| 220 | buffer-file-name)) |
| 221 | (erase-buffer) |
| 222 | (insert-buffer-substring bufC) |
| 223 | (save-buffer))))) |
| 224 | t t) |
| 225 | (add-hook 'ediff-quit-hook (##magit-ediff--cleanup-buffers t) t t) |
| 226 | (add-hook 'ediff-quit-hook |
| 227 | (lambda () |
| 228 | (let ((magit-ediff-previous-winconf winconf)) |
| 229 | (run-hooks 'magit-ediff-quit-hook))) |
| 230 | t t))))) |
| 231 | |
| 232 | ;;;###autoload(autoload 'magit-ediff-stage "magit-ediff" nil t) |
| 233 | (transient-define-suffix magit-ediff-stage (file) |
| 234 | "Stage and unstage changes to FILE using Ediff. |
| 235 | FILE has to be relative to the top directory of the repository." |
| 236 | :inapt-if-not #'magit-anything-modified-p |
| 237 | (interactive |
| 238 | (let ((files (magit-tracked-files))) |
| 239 | (list (magit-completing-read "Selectively stage file" files nil t nil nil |
| 240 | (car (member (magit-current-file) files)))))) |
| 241 | (magit-with-toplevel |
| 242 | (let* ((bufC (magit-ediff--find-file "{worktree}" file)) |
| 243 | ;; Use the same encoding for all three buffers or we |
| 244 | ;; may end up changing the file in an unintended way. |
| 245 | (coding-system-for-read |
| 246 | (buffer-local-value 'buffer-file-coding-system bufC)) |
| 247 | (bufA (magit-ediff--find-file "HEAD" file)) |
| 248 | (bufB (magit-ediff--find-file "{index}" file)) |
| 249 | (lockB (buffer-local-value 'buffer-read-only bufB)) |
| 250 | (modeB (buffer-local-value 'magit-blob-mode bufB))) |
| 251 | (with-current-buffer bufB |
| 252 | ;; Make writable and don't shadow self-insert-command. |
| 253 | (magit-blob-mode -1)) |
| 254 | (magit-ediff-buffers |
| 255 | bufA bufB bufC nil |
| 256 | (lambda () |
| 257 | (when (buffer-live-p ediff-buffer-B) |
| 258 | (when lockB |
| 259 | (with-current-buffer bufB |
| 260 | (if modeB |
| 261 | (magit-blob-mode 1) |
| 262 | (setq-local buffer-read-only t) |
| 263 | (setq-local read-only-mode--state t)))) |
| 264 | (when (buffer-modified-p ediff-buffer-B) |
| 265 | (with-current-buffer ediff-buffer-B |
| 266 | (magit-update-index)))) |
| 267 | (when (and (buffer-live-p ediff-buffer-C) |
| 268 | (buffer-modified-p ediff-buffer-C)) |
| 269 | (with-current-buffer ediff-buffer-C |
| 270 | (when (y-or-n-p (format "Save file %s? " buffer-file-name)) |
| 271 | (save-buffer))))))))) |
| 272 | |
| 273 | ;;;###autoload(autoload 'magit-ediff-compare "magit-ediff" nil t) |
| 274 | (transient-define-suffix magit-ediff-compare (revA revB fileA fileB) |
| 275 | "Compare REVA:FILEA with REVB:FILEB using Ediff. |
| 276 | |
| 277 | FILEA and FILEB have to be relative to the top directory of the |
| 278 | repository. |
| 279 | |
| 280 | If the region is active, use the revisions on the first and last |
| 281 | line of the region. With a prefix argument, instead of diffing |
| 282 | the revisions, choose a revision to view changes along, starting |
| 283 | at the common ancestor of both revisions (i.e., use a \"...\" |
| 284 | range)." |
| 285 | (interactive |
| 286 | (pcase-let ((`(,revA ,revB) (magit-ediff-compare--read-revisions |
| 287 | nil current-prefix-arg))) |
| 288 | (nconc (list revA revB) |
| 289 | (magit-ediff-read-files revA revB)))) |
| 290 | (magit-ediff-buffers |
| 291 | (magit-ediff--find-file revA fileA) |
| 292 | (magit-ediff--find-file revB fileB))) |
| 293 | |
| 294 | (defun magit-ediff-compare--read-revisions (&optional arg mbase) |
| 295 | (let ((input (or arg (magit-diff-read-range-or-commit |
| 296 | "Compare range or commit" |
| 297 | nil mbase)))) |
| 298 | (if-let ((range (magit-split-range input))) |
| 299 | (list (car range) (cdr range)) |
| 300 | (list input "{worktree}")))) |
| 301 | |
| 302 | (defun magit-ediff-read-files (revA revB &optional fileB) |
| 303 | "Read file in REVB, return it and the corresponding file in REVA. |
| 304 | When FILEB is non-nil, use this as REVB's file instead of |
| 305 | prompting for it." |
| 306 | (when (equal revA "{worktree}") (setq revA nil)) |
| 307 | (when (equal revB "{worktree}") (setq revB nil)) |
| 308 | (unless (and fileB (member fileB (magit-revision-files revB))) |
| 309 | (setq fileB |
| 310 | (or (and fileB |
| 311 | magit-buffer-log-files |
| 312 | (derived-mode-p 'magit-log-mode) |
| 313 | (member "--follow" magit-buffer-log-args) |
| 314 | (cdr (assoc fileB |
| 315 | (magit-renamed-files |
| 316 | revB |
| 317 | (oref (car (oref magit-root-section children)) |
| 318 | value))))) |
| 319 | (magit-read-file-choice |
| 320 | (format "File to compare between %s and %s" |
| 321 | revA (or revB "the working tree")) |
| 322 | (magit-changed-files revA revB) |
| 323 | (format "No changed files between %s and %s" |
| 324 | revA (or revB "the working tree")))))) |
| 325 | (list (or (car (member fileB (magit-revision-files revA))) |
| 326 | (cdr (assoc fileB (magit-renamed-files revB revA))) |
| 327 | (magit-read-file-choice |
| 328 | (format "File in %s to compare with %s in %s" |
| 329 | revA fileB (or revB "the working tree")) |
| 330 | (magit-changed-files revB revA) |
| 331 | (format "No files have changed between %s and %s" |
| 332 | revA revB))) |
| 333 | fileB)) |
| 334 | |
| 335 | ;;;###autoload(autoload 'magit-ediff-dwim "magit-ediff" nil t) |
| 336 | (transient-define-suffix magit-ediff-dwim () |
| 337 | "Compare, stage, or resolve using Ediff. |
| 338 | This command tries to guess what file, and what commit or range |
| 339 | the user wants to compare, stage, or resolve using Ediff. It |
| 340 | might only be able to guess either the file, or range or commit, |
| 341 | in which case the user is asked about the other. It might not |
| 342 | always guess right, in which case the appropriate `magit-ediff-*' |
| 343 | command has to be used explicitly. If it cannot read the user's |
| 344 | mind at all, then it asks the user for a command to run." |
| 345 | (interactive) |
| 346 | (magit-section-case |
| 347 | (hunk (save-excursion |
| 348 | (goto-char (oref (oref it parent) start)) |
| 349 | (magit-ediff-dwim))) |
| 350 | (t |
| 351 | (let ((range (magit-diff--dwim)) |
| 352 | (file (magit-current-file)) |
| 353 | command revA revB) |
| 354 | (pcase range |
| 355 | ((and (guard (not magit-ediff-dwim-show-on-hunks)) |
| 356 | (or 'unstaged 'staged)) |
| 357 | (setq command (if (magit-anything-unmerged-p) |
| 358 | magit-ediff-dwim-resolve-function |
| 359 | #'magit-ediff-stage))) |
| 360 | ('unstaged (setq command #'magit-ediff-show-unstaged)) |
| 361 | ('staged (setq command #'magit-ediff-show-staged)) |
| 362 | (`(commit . ,value) |
| 363 | (setq command #'magit-ediff-show-commit) |
| 364 | (setq revB value)) |
| 365 | (`(stash . ,value) |
| 366 | (setq command #'magit-ediff-show-stash) |
| 367 | (setq revB value)) |
| 368 | ((pred stringp) |
| 369 | (pcase-let ((`(,a ,b) (magit-ediff-compare--read-revisions range))) |
| 370 | (setq command #'magit-ediff-compare) |
| 371 | (setq revA a) |
| 372 | (setq revB b))) |
| 373 | (_ |
| 374 | (when (derived-mode-p 'magit-diff-mode) |
| 375 | (pcase (magit-diff-type) |
| 376 | ('committed (pcase-let ((`(,a ,b) |
| 377 | (magit-ediff-compare--read-revisions |
| 378 | magit-buffer-diff-range))) |
| 379 | (setq revA a) |
| 380 | (setq revB b))) |
| 381 | ((guard (not magit-ediff-dwim-show-on-hunks)) |
| 382 | (setq command #'magit-ediff-stage)) |
| 383 | ('unstaged (setq command #'magit-ediff-show-unstaged)) |
| 384 | ('staged (setq command #'magit-ediff-show-staged)) |
| 385 | ('undefined (setq command nil)) |
| 386 | (_ (setq command nil)))))) |
| 387 | (cond ((not command) |
| 388 | (call-interactively |
| 389 | (magit-read-char-case |
| 390 | "Failed to read your mind; do you want to " t |
| 391 | (?c "[c]ommit" #'magit-ediff-show-commit) |
| 392 | (?r "[r]ange" #'magit-ediff-compare) |
| 393 | (?s "[s]tage" #'magit-ediff-stage) |
| 394 | (?m "[m] resolve remaining conflicts" |
| 395 | #'magit-ediff-resolve-rest) |
| 396 | (?M "[M] resolve all conflicts" |
| 397 | #'magit-ediff-resolve-all)))) |
| 398 | ((eq command #'magit-ediff-compare) |
| 399 | (apply #'magit-ediff-compare revA revB |
| 400 | (magit-ediff-read-files revA revB file))) |
| 401 | ((eq command #'magit-ediff-show-commit) |
| 402 | (magit-ediff-show-commit revB)) |
| 403 | ((eq command #'magit-ediff-show-stash) |
| 404 | (magit-ediff-show-stash revB)) |
| 405 | (file |
| 406 | (funcall command file)) |
| 407 | ((call-interactively command))))))) |
| 408 | |
| 409 | ;;;###autoload(autoload 'magit-ediff-show-staged "magit-ediff" nil t) |
| 410 | (transient-define-suffix magit-ediff-show-staged (file) |
| 411 | "Show staged changes using Ediff. |
| 412 | |
| 413 | This only allows looking at the changes; to stage, unstage, |
| 414 | and discard changes using Ediff, use `magit-ediff-stage'. |
| 415 | |
| 416 | FILE must be relative to the top directory of the repository." |
| 417 | :inapt-if-not #'magit-anything-staged-p |
| 418 | (interactive |
| 419 | (list (magit-read-file-choice "Show staged changes for file" |
| 420 | (magit-staged-files) |
| 421 | "No staged files"))) |
| 422 | (magit-ediff-buffers (magit-ediff--find-file "HEAD" file) |
| 423 | (magit-ediff--find-file "{index}" file))) |
| 424 | |
| 425 | ;;;###autoload(autoload 'magit-ediff-show-unstaged "magit-ediff" nil t) |
| 426 | (transient-define-suffix magit-ediff-show-unstaged (file) |
| 427 | "Show unstaged changes using Ediff. |
| 428 | |
| 429 | This only allows looking at the changes; to stage, unstage, |
| 430 | and discard changes using Ediff, use `magit-ediff-stage'. |
| 431 | |
| 432 | FILE must be relative to the top directory of the repository." |
| 433 | :inapt-if-not #'magit-anything-unstaged-p |
| 434 | (interactive |
| 435 | (list (magit-read-file-choice "Show unstaged changes for file" |
| 436 | (magit-unstaged-files) |
| 437 | "No unstaged files"))) |
| 438 | (magit-ediff-buffers (magit-ediff--find-file "{index}" file) |
| 439 | (magit-ediff--find-file "{worktree}" file))) |
| 440 | |
| 441 | ;;;###autoload(autoload 'magit-ediff-show-working-tree "magit-ediff" nil t) |
| 442 | (transient-define-suffix magit-ediff-show-working-tree (file) |
| 443 | "Show changes between `HEAD' and working tree using Ediff. |
| 444 | FILE must be relative to the top directory of the repository." |
| 445 | :inapt-if-not #'magit-anything-modified-p |
| 446 | (interactive |
| 447 | (list (magit-read-file-choice "Show changes in file" |
| 448 | (magit-modified-files) |
| 449 | "No changed files"))) |
| 450 | (magit-ediff-buffers (magit-ediff--find-file "HEAD" file) |
| 451 | (magit-ediff--find-file "{worktree}" file))) |
| 452 | |
| 453 | ;;;###autoload(autoload 'magit-ediff-show-commit "magit-ediff" nil t) |
| 454 | (transient-define-suffix magit-ediff-show-commit (commit) |
| 455 | "Show changes introduced by COMMIT using Ediff." |
| 456 | (interactive (list (magit-read-branch-or-commit "Revision"))) |
| 457 | (let ((revA (concat commit "^")) |
| 458 | (revB commit)) |
| 459 | (apply #'magit-ediff-compare |
| 460 | revA revB |
| 461 | (magit-ediff-read-files revA revB (magit-current-file))))) |
| 462 | |
| 463 | ;;;###autoload(autoload 'magit-ediff-show-stash "magit-ediff" nil t) |
| 464 | (transient-define-suffix magit-ediff-show-stash (stash) |
| 465 | "Show changes introduced by STASH using Ediff. |
| 466 | `magit-ediff-show-stash-with-index' controls whether a three-buffer |
| 467 | Ediff is used in order to distinguish changes in the stash that were |
| 468 | staged." |
| 469 | :inapt-if-not #'magit-list-stashes |
| 470 | (interactive (list (magit-read-stash "Stash"))) |
| 471 | (pcase-let* ((revA (concat stash "^1")) |
| 472 | (revB (concat stash "^2")) |
| 473 | (revC stash) |
| 474 | (`(,fileA ,fileC) (magit-ediff-read-files revA revC)) |
| 475 | (fileB fileC)) |
| 476 | (if (and magit-ediff-show-stash-with-index |
| 477 | (member fileA (magit-changed-files revB revA))) |
| 478 | (magit-ediff-buffers |
| 479 | (magit-ediff--find-file revA fileA) |
| 480 | (magit-ediff--find-file revB fileB) |
| 481 | (magit-ediff--find-file revC fileC)) |
| 482 | (magit-ediff-compare revA revC fileA fileC)))) |
| 483 | |
| 484 | ;;; Setup |
| 485 | |
| 486 | (defun magit-ediff-buffers (a b &optional c setup quit file) |
| 487 | "Run Ediff on two or three buffers A, B and C. |
| 488 | |
| 489 | If optional FILE is non-nil, then perform a merge. The merge result |
| 490 | is put in FILE. |
| 491 | |
| 492 | Neutralize the hooks `ediff-quit-hook' and `ediff-quit-merge-hook' |
| 493 | because they usually feature functions that do not work for Magit. |
| 494 | Instead run optional QUIT (if non-nil), `magit-ediff--cleanup-buffers' |
| 495 | and `magit-ediff-quit-hook', with no arguments. |
| 496 | |
| 497 | Optional SETUP, if non-nil, is called with no arguments after Ediff |
| 498 | is done setting up buffers." |
| 499 | (magit-with-toplevel |
| 500 | (ediff-buffers-internal |
| 501 | a b c |
| 502 | (let ((winconf (current-window-configuration))) |
| 503 | (list (lambda () |
| 504 | (when setup |
| 505 | (funcall setup)) |
| 506 | (setq-local ediff-quit-merge-hook nil) |
| 507 | (setq-local ediff-quit-hook nil) |
| 508 | (when quit |
| 509 | (add-hook 'ediff-quit-hook quit nil t)) |
| 510 | (add-hook 'ediff-quit-hook #'magit-ediff--cleanup-buffers t t) |
| 511 | (add-hook 'ediff-quit-hook |
| 512 | (lambda () |
| 513 | (let ((magit-ediff-previous-winconf winconf)) |
| 514 | (run-hooks 'magit-ediff-quit-hook))) |
| 515 | t t)))) |
| 516 | (pcase (list (and c t) (and file t)) |
| 517 | ('(nil nil) 'ediff-buffers) |
| 518 | ('(nil t) 'ediff-merge-buffers) |
| 519 | ('(t nil) 'ediff-buffers3) |
| 520 | ('(t t) 'ediff-merge-buffers-with-ancestor)) |
| 521 | file))) |
| 522 | |
| 523 | (defun magit-ediff--find-file (rev file) |
| 524 | (let ((buffer (magit-find-file-noselect rev file t 'ediff))) |
| 525 | (when magit-ediff-use-indirect-buffers |
| 526 | (setq buffer (make-indirect-buffer |
| 527 | buffer (generate-new-buffer-name (buffer-name buffer)) t))) |
| 528 | buffer)) |
| 529 | |
| 530 | ;;; Quit |
| 531 | |
| 532 | (defun magit-ediff--cleanup-buffers (&optional smerge) |
| 533 | (cond (smerge |
| 534 | (when (buffer-live-p ediff-buffer-A) (kill-buffer ediff-buffer-A)) |
| 535 | (when (buffer-live-p ediff-buffer-B) (kill-buffer ediff-buffer-B)) |
| 536 | (when (buffer-live-p ediff-buffer-C) (kill-buffer ediff-buffer-C)) |
| 537 | (when (buffer-live-p ediff-ancestor-buffer) |
| 538 | (kill-buffer ediff-ancestor-buffer))) |
| 539 | (t |
| 540 | (magit-ediff--bury-buffer ediff-buffer-A) |
| 541 | (magit-ediff--bury-buffer ediff-buffer-B) |
| 542 | (magit-ediff--bury-buffer ediff-buffer-C) |
| 543 | (magit-ediff--bury-buffer ediff-ancestor-buffer))) |
| 544 | (let* ((ctl-buf ediff-control-buffer) |
| 545 | (ctl-win (ediff-get-visible-buffer-window ctl-buf)) |
| 546 | (ctl-frm ediff-control-frame) |
| 547 | (main-frame (cond ((window-live-p ediff-window-A) |
| 548 | (window-frame ediff-window-A)) |
| 549 | ((window-live-p ediff-window-B) |
| 550 | (window-frame ediff-window-B))))) |
| 551 | (ediff-kill-buffer-carefully ediff-diff-buffer) |
| 552 | (ediff-kill-buffer-carefully ediff-custom-diff-buffer) |
| 553 | (ediff-kill-buffer-carefully ediff-fine-diff-buffer) |
| 554 | (ediff-kill-buffer-carefully ediff-tmp-buffer) |
| 555 | (ediff-kill-buffer-carefully ediff-error-buffer) |
| 556 | (ediff-kill-buffer-carefully ediff-msg-buffer) |
| 557 | (ediff-kill-buffer-carefully ediff-debug-buffer) |
| 558 | (when (boundp 'ediff-patch-diagnostics) |
| 559 | (ediff-kill-buffer-carefully ediff-patch-diagnostics)) |
| 560 | (cond ((and (display-graphic-p) |
| 561 | (frame-live-p ctl-frm)) |
| 562 | (delete-frame ctl-frm)) |
| 563 | ((window-live-p ctl-win) |
| 564 | (delete-window ctl-win))) |
| 565 | (ediff-kill-buffer-carefully ctl-buf) |
| 566 | (when (frame-live-p main-frame) |
| 567 | (select-frame main-frame)))) |
| 568 | |
| 569 | (defun magit-ediff--bury-buffer (buffer) |
| 570 | (when (and (ediff-buffer-live-p buffer) |
| 571 | (eq (buffer-local-value 'magit-buffer--volatile buffer) 'ediff)) |
| 572 | (kill-buffer (get-buffer buffer)))) |
| 573 | |
| 574 | (defun magit-ediff-restore-previous-winconf () |
| 575 | (set-window-configuration magit-ediff-previous-winconf)) |
| 576 | |
| 577 | ;;; _ |
| 578 | (provide 'magit-ediff) |
| 579 | ;; Local Variables: |
| 580 | ;; read-symbol-shorthands: ( |
| 581 | ;; ("and$" . "cond-let--and$") |
| 582 | ;; ("thread$" . "cond-let--thread$") |
| 583 | ;; ("when$" . "cond-let--when$") |
| 584 | ;; ("and-let*" . "cond-let--and-let*") |
| 585 | ;; ("and-let" . "cond-let--and-let") |
| 586 | ;; ("if-let*" . "cond-let--if-let*") |
| 587 | ;; ("if-let" . "cond-let--if-let") |
| 588 | ;; ("when-let*" . "cond-let--when-let*") |
| 589 | ;; ("when-let" . "cond-let--when-let") |
| 590 | ;; ("while-let*" . "cond-let--while-let*") |
| 591 | ;; ("while-let" . "cond-let--while-let") |
| 592 | ;; ("match-string" . "match-string") |
| 593 | ;; ("match-str" . "match-string-no-properties")) |
| 594 | ;; End: |
| 595 | ;;; magit-ediff.el ends here |