]> code.delx.au - gnu-emacs-elpa/blob - ztree-diff.el
3793cbb699af0f530e1dce8bfa2af37f48d2c4ec
[gnu-emacs-elpa] / ztree-diff.el
1 ;;; ztree-diff.el --- Text mode diff for directory trees -*- lexical-binding: t; -*-
2
3 ;; Copyright (C) 2013-2016 Free Software Foundation, Inc.
4 ;;
5 ;; Author: Alexey Veretennikov <alexey.veretennikov@gmail.com>
6 ;;
7 ;; Created: 2013-11-11
8 ;;
9 ;; Keywords: files tools
10 ;; URL: https://github.com/fourier/ztree
11 ;; Compatibility: GNU Emacs 24.x
12 ;;
13 ;; This file is part of GNU Emacs.
14 ;;
15 ;; GNU Emacs is free software: you can redistribute it and/or modify
16 ;; it under the terms of the GNU General Public License as published by
17 ;; the Free Software Foundation, either version 3 of the License, or
18 ;; (at your option) any later version.
19 ;;
20 ;; GNU Emacs is distributed in the hope that it will be useful,
21 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
22 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 ;; GNU General Public License for more details.
24 ;;
25 ;; You should have received a copy of the GNU General Public License
26 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
27 ;;
28 ;;; Commentary:
29
30 ;;; Code:
31 (require 'ztree-view)
32 (require 'ztree-diff-model)
33
34 (defconst ztree-diff-hidden-files-regexp "^\\."
35 "Hidden files regexp.
36 By default all filest starting with dot '.', including . and ..")
37
38 (defface ztreep-diff-header-face
39 '((((type tty pc) (class color)) :foreground "lightblue" :weight bold)
40 (((background dark)) (:height 1.2 :foreground "lightblue" :weight bold))
41 (t :height 1.2 :foreground "darkblue" :weight bold))
42 "*Face used for the header in Ztree Diff buffer."
43 :group 'Ztree-diff :group 'font-lock-highlighting-faces)
44 (defvar ztreep-diff-header-face 'ztreep-diff-header-face)
45
46 (defface ztreep-diff-header-small-face
47 '((((type tty pc) (class color)) :foreground "lightblue" :weight bold)
48 (((background dark)) (:foreground "lightblue" :weight bold))
49 (t :weight bold :foreground "darkblue"))
50 "*Face used for the header in Ztree Diff buffer."
51 :group 'Ztree-diff :group 'font-lock-highlighting-faces)
52 (defvar ztreep-diff-header-small-face 'ztreep-diff-header-small-face)
53
54 (defface ztreep-diff-model-diff-face
55 '((t (:foreground "red")))
56 "*Face used for different files in Ztree-diff."
57 :group 'Ztree-diff :group 'font-lock-highlighting-faces)
58 (defvar ztreep-diff-model-diff-face 'ztreep-diff-model-diff-face)
59
60 (defface ztreep-diff-model-add-face
61 '((t (:foreground "blue")))
62 "*Face used for added files in Ztree-diff."
63 :group 'Ztree-diff :group 'font-lock-highlighting-faces)
64 (defvar ztreep-diff-model-add-face 'ztreep-diff-model-add-face)
65
66 (defface ztreep-diff-model-ignored-face
67 '((((type tty pc) (class color) (min-colors 256)) :foreground "#2f2f2f")
68 (((type tty pc) (class color) (min-colors 8)) :foreground "white")
69 (t (:foreground "#7f7f7f" :strike-through t)))
70 "*Face used for non-modified files in Ztree-diff."
71 :group 'Ztree-diff :group 'font-lock-highlighting-faces)
72 (defvar ztreep-diff-model-ignored-face 'ztreep-diff-model-ignored-face)
73
74 (defface ztreep-diff-model-normal-face
75 '((((type tty pc) (class color) (min-colors 8)) :foreground "white")
76 (t (:foreground "#7f7f7f")))
77 "*Face used for non-modified files in Ztree-diff."
78 :group 'Ztree-diff :group 'font-lock-highlighting-faces)
79 (defvar ztreep-diff-model-normal-face 'ztreep-diff-model-normal-face)
80
81
82 (defvar-local ztree-diff-filter-list (list ztree-diff-hidden-files-regexp)
83 "List of regexp file names to filter out.
84 By default paths starting with dot (like .git) are ignored")
85
86 (defvar-local ztree-diff-dirs-pair nil
87 "Pair of the directories stored. Used to perform the full rescan.")
88
89 (defvar-local ztree-diff-show-equal-files t
90 "Show or not equal files/directories on both sides.")
91
92 (defvar-local ztree-diff-show-filtered-files nil
93 "Show or not files from the filtered list.")
94
95 (defvar-local ztree-diff-wait-message nil
96 "Message showing while constructing the diff tree.")
97
98
99 ;;;###autoload
100 (define-minor-mode ztreediff-mode
101 "A minor mode for displaying the difference of the directory trees in text mode."
102 ;; initial value
103 nil
104 ;; modeline name
105 " Diff"
106 ;; The minor mode keymap
107 `(
108 (,(kbd "C") . ztree-diff-copy)
109 (,(kbd "h") . ztree-diff-toggle-show-equal-files)
110 (,(kbd "H") . ztree-diff-toggle-show-filtered-files)
111 (,(kbd "D") . ztree-diff-delete-file)
112 (,(kbd "v") . ztree-diff-view-file)
113 (,(kbd "d") . ztree-diff-simple-diff-files)
114 (,(kbd "r") . ztree-diff-partial-rescan)
115 (,(kbd "R") . ztree-diff-full-rescan)
116 ([f5] . ztree-diff-full-rescan)))
117
118
119 (defun ztree-diff-node-face (node)
120 "Return the face for the NODE depending on diff status."
121 (let ((diff (ztree-diff-node-different node)))
122 (cond ((eq diff 'ignore) ztreep-diff-model-ignored-face)
123 ((eq diff 'diff) ztreep-diff-model-diff-face)
124 ((eq diff 'new) ztreep-diff-model-add-face)
125 ((eq diff 'same) ztreep-diff-model-normal-face))))
126
127 (defun ztree-diff-insert-buffer-header ()
128 "Insert the header to the ztree buffer."
129 (ztree-insert-with-face "Differences tree" ztreep-diff-header-face)
130 (insert "\n")
131 (when ztree-diff-dirs-pair
132 (ztree-insert-with-face (concat "Left: " (car ztree-diff-dirs-pair))
133 ztreep-diff-header-small-face)
134 (insert "\n")
135 (ztree-insert-with-face (concat "Right: " (cdr ztree-diff-dirs-pair))
136 ztreep-diff-header-small-face)
137 (insert "\n"))
138 (ztree-insert-with-face "Legend:" ztreep-diff-header-small-face)
139 (insert "\n")
140 (ztree-insert-with-face " Normal file " ztreep-diff-model-normal-face)
141 (ztree-insert-with-face "- same on both sides" ztreep-diff-header-small-face)
142 (insert "\n")
143 (ztree-insert-with-face " Orphan file " ztreep-diff-model-add-face)
144 (ztree-insert-with-face "- does not exist on other side" ztreep-diff-header-small-face)
145 (insert "\n")
146 (ztree-insert-with-face " Mismatch file " ztreep-diff-model-diff-face)
147 (ztree-insert-with-face "- different from other side" ztreep-diff-header-small-face)
148 (insert "\n ")
149 (ztree-insert-with-face "Ignored file" ztreep-diff-model-ignored-face)
150 (ztree-insert-with-face " - ignored from comparison" ztreep-diff-header-small-face)
151 (insert "\n")
152
153 (ztree-insert-with-face "==============" ztreep-diff-header-face)
154 (insert "\n"))
155
156 (defun ztree-diff-full-rescan ()
157 "Force full rescan of the directory trees."
158 (interactive)
159 (when (and ztree-diff-dirs-pair
160 (yes-or-no-p (format "Force full rescan?")))
161 (ztree-diff (car ztree-diff-dirs-pair) (cdr ztree-diff-dirs-pair))))
162
163
164
165 (defun ztree-diff-existing-common (node)
166 "Return the NODE if both left and right sides exist."
167 (let ((left (ztree-diff-node-left-path node))
168 (right (ztree-diff-node-right-path node)))
169 (if (and left right
170 (file-exists-p left)
171 (file-exists-p right))
172 node
173 nil)))
174
175 (defun ztree-diff-existing-common-parent (node)
176 "Return the first node in up in hierarchy of the NODE which has both sides."
177 (let ((common (ztree-diff-existing-common node)))
178 (if common
179 common
180 (ztree-diff-existing-common-parent (ztree-diff-node-parent node)))))
181
182 (defun ztree-diff-do-partial-rescan (node)
183 "Partly rescan the NODE."
184 (let* ((common (ztree-diff-existing-common-parent node))
185 (parent (ztree-diff-node-parent common)))
186 (if (not parent)
187 (when ztree-diff-dirs-pair
188 (ztree-diff (car ztree-diff-dirs-pair) (cdr ztree-diff-dirs-pair)))
189 (ztree-diff-update-wait-message
190 (concat "Updating " (ztree-diff-node-short-name common) " ..."))
191 (ztree-diff-model-partial-rescan common)
192 (message "Done")
193 (ztree-refresh-buffer (line-number-at-pos)))))
194
195
196 (defun ztree-diff-partial-rescan ()
197 "Perform partial rescan on the current node."
198 (interactive)
199 (let ((found (ztree-find-node-at-point)))
200 (when found
201 (ztree-diff-do-partial-rescan (car found)))))
202
203
204 (defun ztree-diff-simple-diff (node)
205 "Create a simple diff buffer for files from left and right panels.
206 Argument NODE node containing paths to files to call a diff on."
207 (let* ((node-left (ztree-diff-node-left-path node))
208 (node-right (ztree-diff-node-right-path node)))
209 (when (and
210 node-left
211 node-right
212 (not (file-directory-p node-left)))
213 ;; show the diff window on the bottom
214 ;; to not to crush tree appearance
215 (let ((split-width-threshold nil))
216 (diff node-left node-right)))))
217
218
219 (defun ztree-diff-simple-diff-files ()
220 "Create a simple diff buffer for files from left and right panels."
221 (interactive)
222 (let ((found (ztree-find-node-at-point)))
223 (when found
224 (let ((node (car found)))
225 (ztree-diff-simple-diff node)))))
226
227 (defun ztree-diff-node-action (node hard)
228 "Perform action on NODE:
229 1 if both left and right sides present:
230 1.1 if they are differend
231 1.1.1 if HARD ediff
232 1.1.2 simple diff otherwiste
233 1.2 if they are the same - view left
234 2 if left or right present - view left or rigth"
235 (let ((left (ztree-diff-node-left-path node))
236 (right (ztree-diff-node-right-path node))
237 (open-f #'(lambda (path) (if hard (find-file path)
238 (let ((split-width-threshold nil))
239 (view-file-other-window path))))))
240 (cond ((and left right)
241 (if (eql (ztree-diff-node-different node) 'same)
242 (funcall open-f left)
243 (if hard
244 (ediff left right)
245 (ztree-diff-simple-diff node))))
246 (left (funcall open-f left))
247 (right (funcall open-f right))
248 (t nil))))
249
250
251
252 (defun ztree-diff-copy-file (node source-path destination-path copy-to-right)
253 "Update the NODE status and copy the file.
254 File copied from SOURCE-PATH to DESTINATION-PATH.
255 COPY-TO-RIGHT specifies which side of the NODE to update."
256 (let ((target-path (concat
257 (file-name-as-directory destination-path)
258 (file-name-nondirectory
259 (directory-file-name source-path)))))
260 (let ((err (condition-case error-trap
261 (progn
262 ;; don't ask for overwrite
263 ;; keep time stamp
264 (copy-file source-path target-path t t)
265 nil)
266 (error error-trap))))
267 ;; error message if failed
268 (if err (message (concat "Error: " (nth 2 err)))
269 ;; otherwise:
270 ;; assuming all went ok when left and right nodes are the same
271 ;; set both as not different if they were not ignored
272 (unless (eq (ztree-diff-node-different node) 'ignore)
273 (ztree-diff-node-set-different node 'same))
274 ;; update left/right paths
275 (if copy-to-right
276 (ztree-diff-node-set-right-path node target-path)
277 (ztree-diff-node-set-left-path node target-path))
278 (ztree-diff-node-update-all-parents-diff node)
279 (ztree-refresh-buffer (line-number-at-pos))))))
280
281
282 (defun ztree-diff-copy-dir (node source-path destination-path copy-to-right)
283 "Update the NODE status and copy the directory.
284 Directory copied from SOURCE-PATH to DESTINATION-PATH.
285 COPY-TO-RIGHT specifies which side of the NODE to update."
286 (let* ((src-path (file-name-as-directory source-path))
287 (target-path (file-name-as-directory destination-path))
288 (target-full-path (concat
289 target-path
290 (file-name-nondirectory
291 (directory-file-name source-path)))))
292 (let ((err (condition-case error-trap
293 (progn
294 ;; keep time stamp
295 ;; ask for overwrite
296 (copy-directory src-path target-path t t)
297 nil)
298 (error error-trap))))
299 ;; error message if failed
300 (if err
301 (progn
302 (message (concat "Error: " (nth 1 err)))
303 ;; and do rescan of the node
304 (ztree-diff-do-partial-rescan node))
305 ;; if everything is ok, update statuses
306 (message target-full-path)
307 (if copy-to-right
308 (ztree-diff-node-set-right-path node
309 target-full-path)
310 (ztree-diff-node-set-left-path node
311 target-full-path))
312 (ztree-diff-update-wait-message
313 (concat "Updating " (ztree-diff-node-short-name node) " ..."))
314 ;; TODO: do not rescan the node. Use some logic like in delete
315 (ztree-diff-model-update-node node)
316 (message "Done.")
317 (ztree-diff-node-update-all-parents-diff node)
318 (ztree-refresh-buffer (line-number-at-pos))))))
319
320
321 (defun ztree-diff-copy ()
322 "Copy the file under the cursor to other side."
323 (interactive)
324 (let ((found (ztree-find-node-at-point)))
325 (when found
326 (let* ((node (car found))
327 (side (cdr found))
328 (node-side (ztree-diff-node-side node))
329 (copy-to-right t) ; copy from left to right
330 (node-left (ztree-diff-node-left-path node))
331 (node-right (ztree-diff-node-right-path node))
332 (source-path nil)
333 (destination-path nil)
334 (parent (ztree-diff-node-parent node)))
335 (when parent ; do not copy the root node
336 ;; determine a side to copy from/to
337 ;; algorithm:
338 ;; 1) if both side are present, use the side
339 ;; variable
340 (setq copy-to-right (if (eq node-side 'both)
341 (eq side 'left)
342 ;; 2) if one of sides is absent, copy from
343 ;; the side where the file is present
344 (eq node-side 'left)))
345 ;; 3) in both cases determine if the destination
346 ;; directory is in place
347 (setq source-path (if copy-to-right node-left node-right)
348 destination-path (if copy-to-right
349 (ztree-diff-node-right-path parent)
350 (ztree-diff-node-left-path parent)))
351 (when (and source-path destination-path
352 (yes-or-no-p (format "Copy [%s]%s to [%s]%s/ ?"
353 (if copy-to-right "LEFT" "RIGHT")
354 (ztree-diff-node-short-name node)
355 (if copy-to-right "RIGHT" "LEFT")
356 destination-path)))
357 (if (file-directory-p source-path)
358 (ztree-diff-copy-dir node
359 source-path
360 destination-path
361 copy-to-right)
362 (ztree-diff-copy-file node
363 source-path
364 destination-path
365 copy-to-right))))))))
366
367 (defun ztree-diff-view-file ()
368 "View file at point, depending on side."
369 (interactive)
370 (let ((found (ztree-find-node-at-point)))
371 (when found
372 (let* ((node (car found))
373 (side (cdr found))
374 (node-side (ztree-diff-node-side node))
375 (node-left (ztree-diff-node-left-path node))
376 (node-right (ztree-diff-node-right-path node)))
377 (when (or (eq node-side 'both)
378 (eq side node-side))
379 (cond ((and (eq side 'left)
380 node-left)
381 (view-file node-left))
382 ((and (eq side 'right)
383 node-right)
384 (view-file node-right))))))))
385
386
387 (defun ztree-diff-delete-file ()
388 "Delete the file under the cursor."
389 (interactive)
390 (let ((found (ztree-find-node-at-point)))
391 (when found
392 (let* ((node (car found))
393 (side (cdr found))
394 (node-side (ztree-diff-node-side node))
395 (parent (ztree-diff-node-parent node))
396 ;; algorithm for determining what to delete similar to copy:
397 ;; 1. if the file is present on both sides, delete
398 ;; from the side currently selected
399 ;; 2. if one of sides is absent, delete
400 ;; from the side where the file is present
401 (delete-from-left
402 (or (eql node-side 'left)
403 (and (eql node-side 'both)
404 (eql side 'left))))
405 (remove-path (if delete-from-left
406 (ztree-diff-node-left-path node)
407 (ztree-diff-node-right-path node))))
408 (when (and parent ; do not delete the root node
409 (yes-or-no-p (format "Delete the file [%s]%s ?"
410 (if delete-from-left "LEFT" "RIGHT")
411 remove-path)))
412 (let* ((delete-command
413 (if (file-directory-p remove-path)
414 #'delete-directory
415 #'delete-file))
416 (children (ztree-diff-node-children parent))
417 (err
418 (condition-case error-trap
419 (progn
420 (funcall delete-command remove-path t)
421 nil)
422 (error error-trap))))
423 (if err
424 (progn
425 (message (concat "Error: " (nth 2 err)))
426 ;; when error happened while deleting the
427 ;; directory, rescan the node
428 ;; and update the parents with a new status
429 ;; of this node
430 (when (file-directory-p remove-path)
431 (ztree-diff-model-partial-rescan node)))
432 ;; if everything ok
433 ;; if was only on one side
434 ;; remove the node from children
435 (if (or (and (eql node-side 'left)
436 delete-from-left)
437 (and (eql node-side 'right)
438 (not delete-from-left)))
439 (ztree-diff-node-set-children parent
440 (ztree-filter
441 (lambda (x) (not (ztree-diff-node-equal x node)))
442 children))
443 ;; otherwise update only one side
444 (let ((update-fun
445 (if delete-from-left
446 #'ztree-diff-node-set-left-path
447 #'ztree-diff-node-set-right-path)))
448 (mapc (lambda (x) (funcall update-fun x nil))
449 (cons node (ztree-diff-node-children node))))
450 ;; and update diff status
451 ;; if was ignored keep the old status
452 (unless (eql (ztree-diff-node-different node) 'ignore)
453 (ztree-diff-node-set-different node 'new))
454 ;; finally update all children statuses
455 (ztree-diff-node-update-diff-from-parent node)))
456 (ztree-diff-node-update-all-parents-diff node)
457 (ztree-refresh-buffer (line-number-at-pos))))))))
458
459
460
461 (defun ztree-diff-node-ignore-p (node)
462 "Determine if the NODE is in filter list.
463 If the node is in the filter list it shall not be visible,
464 unless it is a parent node."
465 (let ((name (ztree-diff-node-short-name node)))
466 ;; ignore then
467 ;; not a root and is in filter list
468 (and (ztree-diff-node-parent node)
469 (ztree-find ztree-diff-filter-list #'(lambda (rx) (string-match rx name))))))
470
471
472 (defun ztree-node-is-visible (node)
473 "Determine if the NODE should be visible."
474 (let ((diff (ztree-diff-node-different node)))
475 ;; visible then
476 ;; either it is a root. root have no parent
477 (or (not (ztree-diff-node-parent node)) ; parent is always visible
478 ;; or the files are different or orphan
479 (or (eql diff 'new)
480 (eql diff 'diff))
481 ;; or it is ignored but we show ignored for now
482 (and (eql diff 'ignore)
483 ztree-diff-show-filtered-files)
484 ;; or they are same but we show same for now
485 (and (eql diff 'same)
486 ztree-diff-show-equal-files))))
487
488 (defun ztree-diff-toggle-show-equal-files ()
489 "Toggle visibility of the equal files."
490 (interactive)
491 (setq ztree-diff-show-equal-files (not ztree-diff-show-equal-files))
492 (message (concat (if ztree-diff-show-equal-files "Show" "Hide") " equal files"))
493 (ztree-refresh-buffer))
494
495 (defun ztree-diff-toggle-show-filtered-files ()
496 "Toggle visibility of the filtered files."
497 (interactive)
498 (setq ztree-diff-show-filtered-files (not ztree-diff-show-filtered-files))
499 (message (concat (if ztree-diff-show-filtered-files "Show" "Hide") " filtered files"))
500 (ztree-refresh-buffer))
501
502
503 (defun ztree-diff-update-wait-message (&optional msg)
504 "Update the wait mesage with one more '.' progress indication."
505 (if msg
506 (setq ztree-diff-wait-message msg)
507 (when ztree-diff-wait-message
508 (setq ztree-diff-wait-message (concat ztree-diff-wait-message "."))))
509 (message ztree-diff-wait-message))
510
511 ;;;###autoload
512 (defun ztree-diff (dir1 dir2)
513 "Create an interactive buffer with the directory tree of the path given.
514 Argument DIR1 left directory.
515 Argument DIR2 right directory."
516 (interactive "DLeft directory \nDRight directory ")
517 (unless (and dir1 (file-directory-p dir1))
518 (error "Path %s is not a directory" dir1))
519 (unless (file-exists-p dir1)
520 (error "Path %s does not exist" dir1))
521 (unless (and dir2 (file-directory-p dir2))
522 (error "Path %s is not a directory" dir2))
523 (unless (file-exists-p dir2)
524 (error "Path %s does not exist" dir2))
525 (let* ((model
526 (ztree-diff-node-create nil dir1 dir2
527 (ztree-file-short-name dir1)
528 (ztree-file-short-name dir2)
529 nil
530 nil))
531 (buf-name (concat "*"
532 (ztree-diff-node-short-name model)
533 " <--> "
534 (ztree-diff-node-right-short-name model)
535 "*")))
536 ;; after this command we are in a new buffer,
537 ;; so all buffer-local vars are valid
538 (ztree-view buf-name
539 model
540 'ztree-node-is-visible
541 'ztree-diff-insert-buffer-header
542 'ztree-diff-node-short-name-wrapper
543 'ztree-diff-node-is-directory
544 'ztree-diff-node-equal
545 'ztree-diff-node-children
546 'ztree-diff-node-face
547 'ztree-diff-node-action
548 'ztree-diff-node-side)
549 (ztreediff-mode)
550 (ztree-diff-model-set-ignore-fun #'ztree-diff-node-ignore-p)
551 (ztree-diff-model-set-progress-fun #'ztree-diff-update-wait-message)
552 (setq ztree-diff-dirs-pair (cons dir1 dir2))
553 (ztree-diff-update-wait-message (concat "Comparing " dir1 " and " dir2 " ..."))
554 (ztree-diff-node-recreate model)
555 (message "Done.")
556
557 (ztree-refresh-buffer)))
558
559
560
561
562
563
564 (provide 'ztree-diff)
565 ;;; ztree-diff.el ends here