mirror of https://github.com/Chizi123/.emacs.d.git

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