1 ;;; ztree-diff-model.el --- diff model for directory trees
3 ;; Copyright (C) 2013 Alexey Veretennikov
5 ;; Author: Alexey Veretennikov <alexey dot veretennikov at gmail dot com>
9 ;; URL: https://github.com/fourier/ztree
10 ;; Compatibility: GNU Emacs GNU Emacs 24.x
12 ;; This file is NOT part of GNU Emacs.
14 ;; This program is free software; you can redistribute it and/or
15 ;; modify it under the terms of the GNU General Public License
16 ;; as published by the Free Software Foundation; either version 2
17 ;; of the License, or (at your option) any later version.
19 ;; This program is distributed in the hope that it will be useful,
20 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
21 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 ;; GNU General Public License for more details.
24 ;; You should have received a copy of the GNU General Public License
25 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
33 (defvar ztree-diff-model-wait-message nil
34 "Message showing while constructing the diff tree")
35 (make-variable-buffer-local 'ztree-diff-model-wait-message)
38 (defun ztree-diff-model-update-wait-message ()
39 (when ztree-diff-model-wait-message
40 (setq ztree-diff-model-wait-message (concat ztree-diff-model-wait-message "."))
41 (message ztree-diff-model-wait-message)))
45 ;; Create a record ztree-diff-node with defined fielsd and getters/setters
47 ;; parent - parent node
48 ;; left-path is the full path on the left side of the diff window,
49 ;; right-path is the full path of the right side,
50 ;; short-name - is the file or directory name
51 ;; children - list of nodes - files or directories if the node is a directory
52 ;; different = {nil, 'new, 'diff} - means comparison status
53 (defrecord ztree-diff-node (parent left-path right-path short-name right-short-name children different))
55 (defun ztree-diff-node-to-string (node)
56 (let* ((string-or-nil #'(lambda (x) (if x
59 ((eq x 'diff) "different")
60 (t (ztree-diff-node-short-name x)))
62 (children (ztree-diff-node-children node))
65 (setq ch-str (concat ch-str "\n * " (ztree-diff-node-short-name x))))
66 (concat "Node: " (ztree-diff-node-short-name node)
68 ;; " * Parent: " (let ((parent (ztree-diff-node-parent node)))
69 ;; (if parent (ztree-diff-node-short-name parent) "nil"))
70 " * Parent: " (funcall string-or-nil (ztree-diff-node-parent node))
72 " * Left path: " (funcall string-or-nil (ztree-diff-node-left-path node))
74 " * Right path: " (funcall string-or-nil (ztree-diff-node-right-path node))
76 " * Children: " ch-str
80 (defun ztree-diff-node-short-name-wrapper (node &optional right-side)
82 (ztree-diff-node-short-name node)
83 (ztree-diff-node-right-short-name node)))
86 (defun ztree-diff-node-is-directory (node)
87 (let ((left (ztree-diff-node-left-path node))
88 (right (ztree-diff-node-right-path node)))
90 (file-directory-p left)
91 (file-directory-p right))))
93 (defun ztree-diff-node-side (node)
94 (let ((left (ztree-diff-node-left-path node))
95 (right (ztree-diff-node-right-path node)))
96 (if (and left right) 'both
97 (if left 'left 'right))))
99 (defun ztree-diff-node-equal (node1 node2)
100 (and (string-equal (ztree-diff-node-short-name node1)
101 (ztree-diff-node-short-name node2))
102 (string-equal (ztree-diff-node-left-path node1)
103 (ztree-diff-node-left-path node2))
104 (string-equal (ztree-diff-node-right-path node1)
105 (ztree-diff-node-right-path node1))))
107 (defun ztree-diff-model-files-equal (file1 file2)
108 "Compare files using external diff. Returns t if equal"
109 (let ((diff-output (shell-command-to-string (concat "diff -q" " " file1 " " file2))))
110 (not (> (length diff-output) 2))))
113 (defun ztree-directory-files (dir)
114 "Returns the list of full paths of files in a directory, filtering out . and .."
115 (ztree-filter #'(lambda (file) (let ((simple-name (file-short-name file)))
116 (not (or (string-equal simple-name ".")
117 (string-equal simple-name "..")))))
118 (directory-files dir 'full)))
120 (defun ztree-diff-model-partial-rescan (node)
121 ;; assuming what parent is always exists
122 ;; otherwise the UI shall force the full rescan
123 (let ((parent (ztree-diff-node-parent node))
124 (isdir (ztree-diff-node-is-directory node))
125 (left (ztree-diff-node-left-path node))
126 (right (ztree-diff-node-right-path node)))
127 ;; if node is a directory - traverse
128 (when (and left right
130 (file-exists-p right))
132 (let ((traverse (ztree-diff-node-traverse
136 (ztree-diff-node-set-different node (car traverse))
137 (ztree-diff-node-set-children node (cdr traverse)))
139 (ztree-diff-node-set-different
141 (if (ztree-diff-model-files-equal left right)
145 (defun ztree-diff-model-subtree (parent path side)
146 "Creates a subtree for the given path for either 'left or 'right sides"
147 (let ((files (ztree-directory-files path))
150 (if (file-directory-p file)
151 (let* ((node (ztree-diff-node-create
153 (when (eq side 'left) file)
154 (when (eq side 'right) file)
155 (file-short-name file)
156 (file-short-name file)
159 (children (ztree-diff-model-subtree node file side)))
160 (ztree-diff-node-set-children node children)
162 (push (ztree-diff-node-create
164 (when (eq side 'left) file)
165 (when (eq side 'right) file)
166 (file-short-name file)
167 (file-short-name file)
173 (defun ztree-diff-node-update-diff-from-children (node)
174 (let ((children (ztree-diff-node-children node))
176 (dolist (child children)
178 (ztree-diff-model-update-diff
180 (ztree-diff-node-different child))))
181 (ztree-diff-node-set-different node diff)))
183 (defun ztree-diff-node-update-all-parents-diff (node)
185 (while (setq parent (ztree-diff-node-parent parent))
186 (ztree-diff-node-update-diff-from-children parent))))
189 (defun ztree-diff-model-update-diff (old new)
197 (defun ztree-diff-node-traverse (parent path1 path2)
198 "Function traversing 2 paths returning the list where the
199 first element is the difference status (nil, 'diff, 'new') and
200 the rest is the combined list of nodes"
201 (let ((list1 (ztree-directory-files path1))
202 (list2 (ztree-directory-files path2))
205 (ztree-diff-model-update-wait-message)
206 ;; first - adding all entries from left directory
207 (dolist (file1 list1)
208 ;; for every entry in the first directory
209 ;; we are creating the node
210 (let* ((simple-name (file-short-name file1))
211 (isdir (file-directory-p file1))
214 ;; create the current node to be set as parent to
216 (node (ztree-diff-node-create parent file1 nil simple-name simple-name nil nil))
217 ;; 1. find if the file is in the second directory and the type
218 ;; is the same - i.e. both are directories or both are files
219 (file2 (ztree-find list2
220 #'(lambda (x) (and (string-equal (file-short-name x)
222 (eq isdir (file-directory-p x)))))))
223 ;; 2. if it is not in the second directory, add it as a node
226 ;; 2.1 if it is a directory, add the whole subtree
227 (when (file-directory-p file1)
228 (setq children (ztree-diff-model-subtree node file1 'left)))
229 ;; 2.2 update the difference status for this entry
230 (setq different 'new))
231 ;; 3. if it is found in second directory and of the same type
232 ;; 3.1 if it is a file
233 (if (not (file-directory-p file1))
234 ;; 3.1.1 set difference status to this entry
235 (setq different (if (ztree-diff-model-files-equal file1 file2) nil 'diff))
236 ;; 3.2 if it is the directory
237 ;; 3.2.1 get the result of the directories comparison together with status
238 (let ((traverse (ztree-diff-node-traverse node file1 file2)))
239 ;; 3.2.2 update the difference status for whole comparison from
240 ;; difference result from the 2 subdirectories comparison
241 (setq different (car traverse))
242 ;; 3.2.3 set the children list from the 2 subdirectories comparison
243 (setq children (cdr traverse)))))
244 ;; 2.3 update difference status for the whole comparison
245 (setq different-dir (ztree-diff-model-update-diff different-dir different))
246 ;; update calculated parameters of the node
247 (ztree-diff-node-set-right-path node file2)
248 (ztree-diff-node-set-children node children)
249 (ztree-diff-node-set-different node different)
250 ;; push the created node to the result list
252 ;; second - adding entries from the right directory which are not present
253 ;; in the left directory
254 (dolist (file2 list2)
255 ;; for every entry in the second directory
256 ;; we are creating the node
257 (let* ((simple-name (file-short-name file2))
258 (isdir (file-directory-p file2))
260 ;; create the node to be added to the results list
261 (node (ztree-diff-node-create parent nil file2 simple-name simple-name nil 'new))
262 ;; 1. find if the file is in the first directory and the type
263 ;; is the same - i.e. both are directories or both are files
264 (file1 (ztree-find list1
265 #'(lambda (x) (and (string-equal (file-short-name x)
267 (eq isdir (file-directory-p x)))))))
268 ;; if it is not in the first directory, add it as a node
270 ;; if it is a directory, set the whole subtree to children
271 (when (file-directory-p file2)
272 (setq children (ztree-diff-model-subtree node file2 'right)))
273 ;; update the different status for the whole comparison
274 (setq different-dir (ztree-diff-model-update-diff different-dir 'new))
275 ;; set calculated children to the node
276 (ztree-diff-node-set-children node children)
277 ;; push the created node to the result list
278 (push node result))))
279 ;; result is a pair: difference status and nodes list
280 (cons different-dir result)))
282 (defun ztree-diff-model-create (dir1 dir2)
283 (when (not (file-directory-p dir1))
284 (error "Path %s is not a directory" dir1))
285 (when (not (file-directory-p dir2))
286 (error "Path %s is not a directory" dir2))
287 (setq ztree-diff-model-wait-message (concat "Comparing " dir1 " and " dir2 " ..."))
289 (ztree-diff-node-create nil dir1 dir2
290 (file-short-name dir1)
291 (file-short-name dir2)
294 (traverse (ztree-diff-node-traverse model dir1 dir2)))
295 (ztree-diff-node-set-children model (cdr traverse))
296 (ztree-diff-node-set-different model (car traverse))
300 (defun ztree-diff-model-update-node (node)
301 (setq ztree-diff-model-wait-message
302 (concat "Updating " (ztree-diff-node-short-name node) " ..."))
303 (let ((traverse (ztree-diff-node-traverse node
304 (ztree-diff-node-left-path node)
305 (ztree-diff-node-right-path node))))
306 (ztree-diff-node-set-children node (cdr traverse))
307 (ztree-diff-node-set-different node (car traverse))
312 (provide 'ztree-diff-model)