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