diff options
Diffstat (limited to 'elpa/magit-4.3.1/magit-ediff.el')
-rw-r--r-- | elpa/magit-4.3.1/magit-ediff.el | 606 |
1 files changed, 606 insertions, 0 deletions
diff --git a/elpa/magit-4.3.1/magit-ediff.el b/elpa/magit-4.3.1/magit-ediff.el new file mode 100644 index 0000000..8c8d026 --- /dev/null +++ b/elpa/magit-4.3.1/magit-ediff.el @@ -0,0 +1,606 @@ +;;; magit-ediff.el --- Ediff extension for Magit -*- lexical-binding:t -*- + +;; Copyright (C) 2008-2025 The Magit Project Contributors + +;; Author: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev> +;; Maintainer: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev> + +;; 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 provides basic support for Ediff. + +;;; Code: + +(require 'magit) + +(require 'ediff) +(require 'smerge-mode) + +(defvar smerge-ediff-buf) +(defvar smerge-ediff-windows) + +;;; Options + +(defgroup magit-ediff nil + "Ediff support for Magit." + :link '(info-link "(magit)Ediffing") + :group 'magit-extensions) + +(defcustom magit-ediff-quit-hook + (list #'magit-ediff-cleanup-auxiliary-buffers + #'magit-ediff-restore-previous-winconf) + "Hooks to run after finishing Ediff, when that was invoked using Magit. +The hooks are run in the Ediff control buffer. This is similar +to `ediff-quit-hook' but takes the needs of Magit into account. +The `ediff-quit-hook' is ignored by Ediff sessions which were +invoked using Magit." + :package-version '(magit . "2.2.0") + :group 'magit-ediff + :type 'hook + :get #'magit-hook-custom-get + :options (list #'magit-ediff-cleanup-auxiliary-buffers + #'magit-ediff-restore-previous-winconf)) + +(defcustom magit-ediff-dwim-resolve-function #'magit-ediff-resolve-rest + "The function `magit-ediff-dwim' uses to resolve conflicts." + :package-version '(magit . "4.0.0") + :group 'magit-ediff + :type '(choice (const magit-ediff-resolve-rest) + (const magit-ediff-resolve-all) + (const magit-git-mergetool))) + +(defcustom magit-ediff-dwim-show-on-hunks nil + "Whether `magit-ediff-dwim' runs show variants on hunks. +If non-nil, `magit-ediff-show-staged' or +`magit-ediff-show-unstaged' are called based on what section the +hunk is in. Otherwise, `magit-ediff-dwim' runs +`magit-ediff-stage' when point is on an uncommitted hunk." + :package-version '(magit . "2.2.0") + :group 'magit-ediff + :type 'boolean) + +(defcustom magit-ediff-show-stash-with-index t + "Whether `magit-ediff-show-stash' shows the state of the index. + +If non-nil, use a third Ediff buffer to distinguish which changes +in the stash were staged. In cases where the stash contains no +staged changes, fall back to a two-buffer Ediff. + +More specifically, a stash is a merge commit, stash@{N}, with +potentially three parents. + +* stash@{N}^1 represents the `HEAD' commit at the time the stash + was created. + +* stash@{N}^2 records any changes that were staged when the stash + was made. + +* stash@{N}^3, if it exists, contains files that were untracked + when stashing. + +If this option is non-nil, `magit-ediff-show-stash' will run +Ediff on a file using three buffers: one for stash@{N}, another +for stash@{N}^1, and a third for stash@{N}^2. + +Otherwise, Ediff uses two buffers, comparing +stash@{N}^1..stash@{N}. Along with any unstaged changes, changes +in the index commit, stash@{N}^2, will be shown in this +comparison unless they conflicted with changes in the working +tree at the time of stashing." + :package-version '(magit . "2.6.0") + :group 'magit-ediff + :type 'boolean) + +(defvar magit-ediff-use-indirect-buffers nil + "Whether to use indirect buffers. +Ediff already does a lot of buffer and file shuffling and I +recommend you do not further complicate that by enabling this.") + +;;; Commands + +(defvar magit-ediff-previous-winconf nil) + +;;;###autoload (autoload 'magit-ediff "magit-ediff" nil) +(transient-define-prefix magit-ediff () + "Show differences using the Ediff package." + :info-manual "(ediff)" + ["Ediff" + [("E" "Dwim" magit-ediff-dwim) + ("s" "Stage" magit-ediff-stage)] + [("m" "Resolve rest" magit-ediff-resolve-rest) + ("M" "Resolve all conflicts" magit-ediff-resolve-all) + ("t" "Resolve using mergetool" magit-git-mergetool)] + [("u" "Show unstaged" magit-ediff-show-unstaged) + ("i" "Show staged" magit-ediff-show-staged) + ("w" "Show worktree" magit-ediff-show-working-tree)] + [("c" "Show commit" magit-ediff-show-commit) + ("r" "Show range" magit-ediff-compare) + ("z" "Show stash" magit-ediff-show-stash)]]) + +(defmacro magit-ediff-buffers (a b &optional c setup quit file) + "Run Ediff on two or three buffers. +This is a wrapper around `ediff-buffers-internal'. + +A, B and C have the form (GET-BUFFER CREATE-BUFFER). If +GET-BUFFER returns a non-nil value, then that buffer is used and +it is not killed when exiting Ediff. Otherwise CREATE-BUFFER +must return a buffer and that is killed when exiting Ediff. + +If non-nil, SETUP must be a function. It is called without +arguments after Ediff is done setting up buffers. + +If non-nil, QUIT must be a function. It is added to +`ediff-quit-hook' and is called without arguments. + +If FILE is non-nil, then perform a merge. The merge result +is put in FILE." + (let (get make kill (char ?A)) + (dolist (spec (list a b c)) + (if (not spec) + (push nil make) + (pcase-let ((`(,g ,m) spec)) + (let ((b (intern (format "buf%c" char)))) + (push `(,b ,g) get) + ;; This is an unfortunate complication that I have added for + ;; the benefit of one user. Pretend we used this instead: + ;; (push `(or ,b ,m) make) + (push `(if ,b + (if magit-ediff-use-indirect-buffers + (prog1 (make-indirect-buffer + ,b + (generate-new-buffer-name (buffer-name ,b)) + t) + (setq ,b nil)) + ,b) + ,m) + make) + (push `(unless ,b + ;; For merge jobs Ediff switches buffer names around. + ;; See (if ediff-merge-job ...) in `ediff-setup'. + (let ((var ,(if (and file (= char ?C)) + 'ediff-ancestor-buffer + (intern (format "ediff-buffer-%c" char))))) + (ediff-kill-buffer-carefully var))) + kill)) + (cl-incf char)))) + (setq get (nreverse get)) + (setq make (nreverse make)) + (setq kill (nreverse kill)) + (let ((mconf (gensym "conf")) + (mfile (gensym "file"))) + `(magit-with-toplevel + (let ((,mconf (current-window-configuration)) + (,mfile ,file) + ,@get) + (ediff-buffers-internal + ,@make + (list ,@(and setup (list setup)) + (lambda () + ;; We do not want to kill buffers that existed before + ;; Ediff was invoked, so we cannot use Ediff's default + ;; quit functions. Ediff splits quitting across two + ;; hooks for merge jobs but we only ever use one. + (setq-local ediff-quit-merge-hook nil) + (setq-local ediff-quit-hook + (list + ,@(and quit (list quit)) + (lambda () + ,@kill + (let ((magit-ediff-previous-winconf ,mconf)) + (run-hooks 'magit-ediff-quit-hook))))))) + (pcase (list ,(and c t) (and ,mfile t)) + ('(nil nil) 'ediff-buffers) + ('(nil t) 'ediff-merge-buffers) + ('(t nil) 'ediff-buffers3) + ('(t t) 'ediff-merge-buffers-with-ancestor)) + ,mfile)))))) + +;;;###autoload +(defun magit-ediff-resolve-all (file) + "Resolve all conflicts in the FILE at point using Ediff. + +If there is no file at point or if it doesn't have any unmerged +changes, then prompt for a file. + +See info node `(magit) Ediffing' for more information about this +and alternative commands." + (interactive (list (magit-read-unmerged-file))) + (magit-with-toplevel + (let* ((dir (magit-gitdir)) + (revA (or (magit-name-branch "HEAD") + (magit-commit-p "HEAD"))) + (revB (cl-find-if (lambda (head) + (file-exists-p (expand-file-name head dir))) + '("MERGE_HEAD" "CHERRY_PICK_HEAD" "REVERT_HEAD"))) + (revB (or (magit-name-branch revB) + (magit-commit-p revB))) + (revC (magit-commit-p (magit-git-string "merge-base" revA revB))) + (fileA (magit--rev-file-name file revA revB)) + (fileB (magit--rev-file-name file revB revA)) + (fileC (or (magit--rev-file-name file revC revA) + (magit--rev-file-name file revC revB)))) + ;; Ediff assumes that the FILE where it is going to store the merge + ;; result does not exist yet, so move the existing file out of the + ;; way. If a buffer visits FILE, then we have to kill that upfront. + (when-let ((buffer (find-buffer-visiting file))) + (when (and (buffer-modified-p buffer) + (not (y-or-n-p (format "Save buffer %s %s? " + (buffer-name buffer) + "(cannot continue otherwise)")))) + (user-error "Abort")) + (kill-buffer buffer)) + (let ((orig (concat file ".ORIG"))) + (when (file-exists-p orig) + (rename-file orig (make-temp-name (concat orig "_")))) + (rename-file file orig)) + (let ((setup (lambda () + ;; Use the same conflict marker style as Git uses. + (setq-local ediff-combination-pattern + '("<<<<<<< HEAD" A + ,(format "||||||| %s" revC) Ancestor + "=======" B + ,(format ">>>>>>> %s" revB))))) + (quit (lambda () + ;; For merge jobs Ediff switches buffer names around. + ;; At this point `ediff-buffer-C' no longer refer to + ;; the ancestor buffer but to the merge result buffer. + ;; See (if ediff-merge-job ...) in `ediff-setup'. + (when (buffer-live-p ediff-buffer-C) + (with-current-buffer ediff-buffer-C + (save-buffer) + (save-excursion + (goto-char (point-min)) + (unless (re-search-forward "^<<<<<<< " nil t) + (magit-stage-file file)))))))) + (if fileC + (magit-ediff-buffers + ((magit-get-revision-buffer revA fileA) + (magit-find-file-noselect revA fileA)) + ((magit-get-revision-buffer revB fileB) + (magit-find-file-noselect revB fileB)) + ((magit-get-revision-buffer revC fileC) + (magit-find-file-noselect revC fileC)) + setup quit file) + (magit-ediff-buffers + ((magit-get-revision-buffer revA fileA) + (magit-find-file-noselect revA fileA)) + ((magit-get-revision-buffer revB fileB) + (magit-find-file-noselect revB fileB)) + nil setup quit file)))))) + +;;;###autoload +(defun magit-ediff-resolve-rest (file) + "Resolve outstanding conflicts in the FILE at point using Ediff. + +If there is no file at point or if it doesn't have any unmerged +changes, then prompt for a file. + +See info node `(magit) Ediffing' for more information about this +and alternative commands." + (interactive (list (magit-read-unmerged-file))) + (magit-with-toplevel + (with-current-buffer (find-file-noselect file) + (smerge-ediff) + (setq-local + ediff-quit-hook + (lambda () + (let ((bufC ediff-buffer-C) + (bufS smerge-ediff-buf)) + (with-current-buffer bufS + (when (yes-or-no-p (format "Conflict resolution finished; save %s? " + buffer-file-name)) + (erase-buffer) + (insert-buffer-substring bufC) + (save-buffer)))) + (when (buffer-live-p ediff-buffer-A) (kill-buffer ediff-buffer-A)) + (when (buffer-live-p ediff-buffer-B) (kill-buffer ediff-buffer-B)) + (when (buffer-live-p ediff-buffer-C) (kill-buffer ediff-buffer-C)) + (when (buffer-live-p ediff-ancestor-buffer) + (kill-buffer ediff-ancestor-buffer)) + (let ((magit-ediff-previous-winconf smerge-ediff-windows)) + (run-hooks 'magit-ediff-quit-hook))))))) + +;;;###autoload +(defun magit-ediff-stage (file) + "Stage and unstage changes to FILE using Ediff. +FILE has to be relative to the top directory of the repository." + (interactive + (let ((files (magit-tracked-files))) + (list (magit-completing-read "Selectively stage file" files nil t nil nil + (car (member (magit-current-file) files)))))) + (magit-with-toplevel + (let* ((bufA (magit-get-revision-buffer "HEAD" file)) + (bufB (magit-get-revision-buffer "{index}" file)) + (lockB (and bufB (buffer-local-value 'buffer-read-only bufB))) + (bufC (get-file-buffer file)) + ;; Use the same encoding for all three buffers or we + ;; may end up changing the file in an unintended way. + (bufC* (or bufC (find-file-noselect file))) + (coding-system-for-read + (buffer-local-value 'buffer-file-coding-system bufC*)) + (bufA* (magit-find-file-noselect-1 "HEAD" file t)) + (bufB* (magit-find-file-index-noselect file t))) + (with-current-buffer bufB* (setq buffer-read-only nil)) + (magit-ediff-buffers + (bufA bufA*) + (bufB bufB*) + (bufC bufC*) + nil + (lambda () + (when (buffer-live-p ediff-buffer-B) + (when lockB + (with-current-buffer bufB (setq buffer-read-only t))) + (when (buffer-modified-p ediff-buffer-B) + (with-current-buffer ediff-buffer-B + (magit-update-index)))) + (when (and (buffer-live-p ediff-buffer-C) + (buffer-modified-p ediff-buffer-C)) + (with-current-buffer ediff-buffer-C + (when (y-or-n-p (format "Save file %s? " buffer-file-name)) + (save-buffer))))))))) + +;;;###autoload +(defun magit-ediff-compare (revA revB fileA fileB) + "Compare REVA:FILEA with REVB:FILEB using Ediff. + +FILEA and FILEB have to be relative to the top directory of the +repository. If REVA or REVB is nil, then this stands for the +working tree state. + +If the region is active, use the revisions on the first and last +line of the region. 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 + (pcase-let ((`(,revA ,revB) (magit-ediff-compare--read-revisions + nil current-prefix-arg))) + (nconc (list revA revB) + (magit-ediff-read-files revA revB)))) + (magit-ediff-buffers + ((if revA (magit-get-revision-buffer revA fileA) (get-file-buffer fileA)) + (if revA (magit-find-file-noselect revA fileA) (find-file-noselect fileA))) + ((if revB (magit-get-revision-buffer revB fileB) (get-file-buffer fileB)) + (if revB (magit-find-file-noselect revB fileB) (find-file-noselect fileB))))) + +(defun magit-ediff-compare--read-revisions (&optional arg mbase) + (let ((input (or arg (magit-diff-read-range-or-commit + "Compare range or commit" + nil mbase)))) + (if-let ((range (magit-split-range input))) + (list (car range) (cdr range)) + (list input nil)))) + +(defun magit-ediff-read-files (revA revB &optional fileB) + "Read file in REVB, return it and the corresponding file in REVA. +When FILEB is non-nil, use this as REVB's file instead of +prompting for it." + (unless (and fileB (member fileB (magit-revision-files revB))) + (setq fileB + (or (and fileB + magit-buffer-log-files + (derived-mode-p 'magit-log-mode) + (member "--follow" magit-buffer-log-args) + (cdr (assoc fileB + (magit-renamed-files + revB + (oref (car (oref magit-root-section children)) + value))))) + (magit-read-file-choice + (format "File to compare between %s and %s" + revA (or revB "the working tree")) + (magit-changed-files revA revB) + (format "No changed files between %s and %s" + revA (or revB "the working tree")))))) + (list (or (car (member fileB (magit-revision-files revA))) + (cdr (assoc fileB (magit-renamed-files revB revA))) + (magit-read-file-choice + (format "File in %s to compare with %s in %s" + revA fileB (or revB "the working tree")) + (magit-changed-files revB revA) + (format "No files have changed between %s and %s" + revA revB))) + fileB)) + +;;;###autoload +(defun magit-ediff-dwim () + "Compare, stage, or resolve using Ediff. +This command tries to guess what file, and what commit or range +the user wants to compare, stage, or resolve using Ediff. It +might only be able to guess either the file, or range or commit, +in which case the user is asked about the other. It might not +always guess right, in which case the appropriate `magit-ediff-*' +command has to be used explicitly. If it cannot read the user's +mind at all, then it asks the user for a command to run." + (interactive) + (magit-section-case + (hunk (save-excursion + (goto-char (oref (oref it parent) start)) + (magit-ediff-dwim))) + (t + (let ((range (magit-diff--dwim)) + (file (magit-current-file)) + command revA revB) + (pcase range + ((and (guard (not magit-ediff-dwim-show-on-hunks)) + (or 'unstaged 'staged)) + (setq command (if (magit-anything-unmerged-p) + magit-ediff-dwim-resolve-function + #'magit-ediff-stage))) + ('unstaged (setq command #'magit-ediff-show-unstaged)) + ('staged (setq command #'magit-ediff-show-staged)) + (`(commit . ,value) + (setq command #'magit-ediff-show-commit) + (setq revB value)) + (`(stash . ,value) + (setq command #'magit-ediff-show-stash) + (setq revB value)) + ((pred stringp) + (pcase-let ((`(,a ,b) (magit-ediff-compare--read-revisions range))) + (setq command #'magit-ediff-compare) + (setq revA a) + (setq revB b))) + (_ + (when (derived-mode-p 'magit-diff-mode) + (pcase (magit-diff-type) + ('committed (pcase-let ((`(,a ,b) + (magit-ediff-compare--read-revisions + magit-buffer-range))) + (setq revA a) + (setq revB b))) + ((guard (not magit-ediff-dwim-show-on-hunks)) + (setq command #'magit-ediff-stage)) + ('unstaged (setq command #'magit-ediff-show-unstaged)) + ('staged (setq command #'magit-ediff-show-staged)) + ('undefined (setq command nil)) + (_ (setq command nil)))))) + (cond ((not command) + (call-interactively + (magit-read-char-case + "Failed to read your mind; do you want to " t + (?c "[c]ommit" #'magit-ediff-show-commit) + (?r "[r]ange" #'magit-ediff-compare) + (?s "[s]tage" #'magit-ediff-stage) + (?m "[m] resolve remaining conflicts" + #'magit-ediff-resolve-rest) + (?M "[M] resolve all conflicts" + #'magit-ediff-resolve-all)))) + ((eq command #'magit-ediff-compare) + (apply #'magit-ediff-compare revA revB + (magit-ediff-read-files revA revB file))) + ((eq command #'magit-ediff-show-commit) + (magit-ediff-show-commit revB)) + ((eq command #'magit-ediff-show-stash) + (magit-ediff-show-stash revB)) + (file + (funcall command file)) + (t + (call-interactively command))))))) + +;;;###autoload +(defun magit-ediff-show-staged (file) + "Show staged changes using Ediff. + +This only allows looking at the changes; to stage, unstage, +and discard changes using Ediff, use `magit-ediff-stage'. + +FILE must be relative to the top directory of the repository." + (interactive + (list (magit-read-file-choice "Show staged changes for file" + (magit-staged-files) + "No staged files"))) + (magit-ediff-buffers ((magit-get-revision-buffer "HEAD" file) + (magit-find-file-noselect "HEAD" file)) + ((get-buffer (concat file ".~{index}~")) + (magit-find-file-index-noselect file t)))) + +;;;###autoload +(defun magit-ediff-show-unstaged (file) + "Show unstaged changes using Ediff. + +This only allows looking at the changes; to stage, unstage, +and discard changes using Ediff, use `magit-ediff-stage'. + +FILE must be relative to the top directory of the repository." + (interactive + (list (magit-read-file-choice "Show unstaged changes for file" + (magit-unstaged-files) + "No unstaged files"))) + (magit-ediff-buffers ((get-buffer (concat file ".~{index}~")) + (magit-find-file-index-noselect file t)) + ((get-file-buffer file) + (find-file-noselect file)))) + +;;;###autoload +(defun magit-ediff-show-working-tree (file) + "Show changes between `HEAD' and working tree using Ediff. +FILE must be relative to the top directory of the repository." + (interactive + (list (magit-read-file-choice "Show changes in file" + (magit-changed-files "HEAD") + "No changed files"))) + (magit-ediff-buffers ((magit-get-revision-buffer "HEAD" file) + (magit-find-file-noselect "HEAD" file)) + ((get-file-buffer file) + (find-file-noselect file)))) + +;;;###autoload +(defun magit-ediff-show-commit (commit) + "Show changes introduced by COMMIT using Ediff." + (interactive (list (magit-read-branch-or-commit "Revision"))) + (let ((revA (concat commit "^")) + (revB commit)) + (apply #'magit-ediff-compare + revA revB + (magit-ediff-read-files revA revB (magit-current-file))))) + +;;;###autoload +(defun magit-ediff-show-stash (stash) + "Show changes introduced by STASH using Ediff. +`magit-ediff-show-stash-with-index' controls whether a +three-buffer Ediff is used in order to distinguish changes in the +stash that were staged." + (interactive (list (magit-read-stash "Stash"))) + (pcase-let* ((revA (concat stash "^1")) + (revB (concat stash "^2")) + (revC stash) + (`(,fileA ,fileC) (magit-ediff-read-files revA revC)) + (fileB fileC)) + (if (and magit-ediff-show-stash-with-index + (member fileA (magit-changed-files revB revA))) + (magit-ediff-buffers + ((magit-get-revision-buffer revA fileA) + (magit-find-file-noselect revA fileA)) + ((magit-get-revision-buffer revB fileB) + (magit-find-file-noselect revB fileB)) + ((magit-get-revision-buffer revC fileC) + (magit-find-file-noselect revC fileC))) + (magit-ediff-compare revA revC fileA fileC)))) + +(defun magit-ediff-cleanup-auxiliary-buffers () + (let* ((ctl-buf ediff-control-buffer) + (ctl-win (ediff-get-visible-buffer-window ctl-buf)) + (ctl-frm ediff-control-frame) + (main-frame (cond ((window-live-p ediff-window-A) + (window-frame ediff-window-A)) + ((window-live-p ediff-window-B) + (window-frame ediff-window-B))))) + (ediff-kill-buffer-carefully ediff-diff-buffer) + (ediff-kill-buffer-carefully ediff-custom-diff-buffer) + (ediff-kill-buffer-carefully ediff-fine-diff-buffer) + (ediff-kill-buffer-carefully ediff-tmp-buffer) + (ediff-kill-buffer-carefully ediff-error-buffer) + (ediff-kill-buffer-carefully ediff-msg-buffer) + (ediff-kill-buffer-carefully ediff-debug-buffer) + (when (boundp 'ediff-patch-diagnostics) + (ediff-kill-buffer-carefully ediff-patch-diagnostics)) + (cond ((and (display-graphic-p) + (frame-live-p ctl-frm)) + (delete-frame ctl-frm)) + ((window-live-p ctl-win) + (delete-window ctl-win))) + (ediff-kill-buffer-carefully ctl-buf) + (when (frame-live-p main-frame) + (select-frame main-frame)))) + +(defun magit-ediff-restore-previous-winconf () + (set-window-configuration magit-ediff-previous-winconf)) + +;;; _ +(provide 'magit-ediff) +;;; magit-ediff.el ends here |