]> code.delx.au - gnu-emacs-elpa/blob - ztree-diff-model.el
Fixed copy of the files; added 'R' for full rescan
[gnu-emacs-elpa] / ztree-diff-model.el
1 ;;; ztree-diff-model.el --- diff model 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-1l
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 ;; Diff model
31
32 ;;; Code:
33 (require 'ztree-util)
34 (eval-when-compile (require 'cl-lib))
35
36 (defvar-local ztree-diff-model-ignore-fun nil
37 "Function which determines if the node should be excluded from comparison.")
38
39 (defvar-local ztree-diff-model-progress-fun nil
40 "Function which should be called whenever the progress indications is updated.")
41
42
43 (defun ztree-diff-model-update-progress ()
44 "Update the progress."
45 (when ztree-diff-model-progress-fun
46 (funcall ztree-diff-model-progress-fun)))
47
48 ;; Create a record ztree-diff-node with defined fields and getters/setters
49 ;; here:
50 ;; parent - parent node
51 ;; left-path is the full path on the left side of the diff window,
52 ;; right-path is the full path of the right side,
53 ;; short-name - is the file or directory name
54 ;; children - list of nodes - files or directories if the node is a directory
55 ;; different = {nil, 'same, 'new, 'diff, 'ignore} - means comparison status
56 (ztree-defrecord ztree-diff-node (parent left-path right-path short-name right-short-name children different))
57
58 (defun ztree-diff-model-ignore-p (node)
59 "Determine if the NODE should be excluded from comparison results."
60 (when ztree-diff-model-ignore-fun
61 (funcall ztree-diff-model-ignore-fun node)))
62
63 (defun ztree-diff-node-to-string (node)
64 "Construct the string with contents of the NODE given."
65 (let ((string-or-nil #'(lambda (x) (if x
66 (cond ((stringp x) x)
67 ((eq x 'new) "new")
68 ((eq x 'diff) "different")
69 ((eq x 'ignore) "ignored")
70 ((eq x 'same) "same")
71 (t (ztree-diff-node-short-name x)))
72 "(empty)")))
73 (children (ztree-diff-node-children node))
74 (ch-str ""))
75 (dolist (x children)
76 (setq ch-str (concat ch-str "\n * " (ztree-diff-node-short-name x)
77 ": "
78 (funcall string-or-nil (ztree-diff-node-different x)))))
79 (concat "Node: " (ztree-diff-node-short-name node)
80 "\n"
81 " * Parent: " (funcall string-or-nil (ztree-diff-node-parent node))
82 "\n"
83 " * Status: " (funcall string-or-nil (ztree-diff-node-different node))
84 "\n"
85 " * Left path: " (funcall string-or-nil (ztree-diff-node-left-path node))
86 "\n"
87 " * Right path: " (funcall string-or-nil (ztree-diff-node-right-path node))
88 "\n"
89 " * Children: " ch-str
90 "\n")))
91
92
93 (defun ztree-diff-node-short-name-wrapper (node &optional right-side)
94 "Return the short name of the NODE given.
95 If the RIGHT-SIDE is true, take the right leaf"
96 (if (not right-side)
97 (ztree-diff-node-short-name node)
98 (ztree-diff-node-right-short-name node)))
99
100
101 (defun ztree-diff-node-is-directory (node)
102 "Determines if the NODE is a directory."
103 (let ((left (ztree-diff-node-left-path node))
104 (right (ztree-diff-node-right-path node)))
105 (if left
106 (file-directory-p left)
107 (file-directory-p right))))
108
109 (defun ztree-diff-node-side (node)
110 "Determine the side there the file is present for NODE.
111 Return BOTH if the file present on both sides;
112 LEFT if only on the left side and
113 RIGHT if only on the right side."
114 (let ((left (ztree-diff-node-left-path node))
115 (right (ztree-diff-node-right-path node)))
116 (if (and left right) 'both
117 (if left 'left 'right))))
118
119 (defun ztree-diff-node-equal (node1 node2)
120 "Determines if NODE1 and NODE2 are equal."
121 (and (string-equal (ztree-diff-node-short-name node1)
122 (ztree-diff-node-short-name node2))
123 (string-equal (ztree-diff-node-left-path node1)
124 (ztree-diff-node-left-path node2))
125 (string-equal (ztree-diff-node-right-path node1)
126 (ztree-diff-node-right-path node1))))
127
128 (defun ztree-diff-untrampify-filename (file)
129 "Return FILE as the local file name."
130 (require 'tramp)
131 (if (not (tramp-tramp-file-p file))
132 file
133 (tramp-file-name-localname (tramp-dissect-file-name file))))
134
135 (defun ztree-diff-modef-quotify-string (x)
136 "Surround string X with quotes."
137 (concat "\"" x "\""))
138
139 (defun ztree-diff-model-files-equal (file1 file2)
140 "Compare files FILE1 and FILE2 using external diff.
141 Returns t if equal."
142 (let* ((file1-untrampified (ztree-diff-untrampify-filename (ztree-diff-modef-quotify-string file1)))
143 (file2-untrampified (ztree-diff-untrampify-filename (ztree-diff-modef-quotify-string file2)))
144 (diff-command (concat diff-command " -q" " " file1-untrampified " " file2-untrampified))
145 (diff-output (shell-command-to-string diff-command)))
146 (if (<= (length diff-output) 2) 'same 'diff)))
147
148 (defun ztree-directory-files (dir)
149 "Return the list of full paths of files in a directory DIR.
150 Filters out . and .."
151 (ztree-filter #'(lambda (file) (let ((simple-name (ztree-file-short-name file)))
152 (not (or (string-equal simple-name ".")
153 (string-equal simple-name "..")))))
154 (directory-files dir 'full)))
155
156 (defun ztree-diff-model-partial-rescan (node)
157 "Rescan the NODE.
158 The node is a either a file or directory with both
159 left and right parts existing."
160 ;; if a directory - recreate
161 (if (ztree-diff-node-is-directory node)
162 (ztree-diff-node-recreate node)
163 ;; if a file, change a status
164 (ztree-diff-node-set-different
165 node
166 (if (or (ztree-diff-model-ignore-p node) ; if should be ignored
167 (eql (ztree-diff-node-different node) 'ignore) ; was ignored
168 (eql (ztree-diff-node-different ; or parent was ignored
169 (ztree-diff-node-parent node)) 'ignore))
170 'ignore
171 (ztree-diff-model-files-equal (ztree-diff-node-left-path node)
172 (ztree-diff-node-right-path node)))))
173 ;; update all parents statuses
174 (ztree-diff-node-update-all-parents-diff node))
175
176 (defun ztree-diff-model-subtree (parent path side diff)
177 "Create a subtree with given PARENT for the given PATH.
178 Argument SIDE either 'left or 'right side.
179 Argument DIFF different status to be assigned to all created nodes."
180 (let ((files (ztree-directory-files path))
181 (result nil))
182 (dolist (file files)
183 (if (file-directory-p file)
184 (let* ((node (ztree-diff-node-create
185 parent
186 (when (eq side 'left) file)
187 (when (eq side 'right) file)
188 (ztree-file-short-name file)
189 (ztree-file-short-name file)
190 nil
191 diff))
192 (children (ztree-diff-model-subtree node file side diff)))
193 (ztree-diff-node-set-children node children)
194 (push node result))
195 (push (ztree-diff-node-create
196 parent
197 (when (eq side 'left) file)
198 (when (eq side 'right) file)
199 (ztree-file-short-name file)
200 (ztree-file-short-name file)
201 nil
202 diff)
203 result)))
204 result))
205
206 (defun ztree-diff-node-update-diff-from-children (node)
207 "Set the diff status for the NODE based on its children."
208 (unless (eql (ztree-diff-node-different node) 'ignore)
209 (let ((diff (cl-reduce 'ztree-diff-model-update-diff
210 (ztree-diff-node-children node)
211 :initial-value 'same
212 :key 'ztree-diff-node-different)))
213 (ztree-diff-node-set-different node diff))))
214
215 (defun ztree-diff-node-update-all-parents-diff (node)
216 "Recursively update all parents diff status for the NODE."
217 (let ((parent node))
218 (while (setq parent (ztree-diff-node-parent parent))
219 (ztree-diff-node-update-diff-from-children parent))))
220
221
222 (defun ztree-diff-model-update-diff (old new)
223 "Get the diff status depending if OLD or NEW is not nil.
224 If the OLD is 'ignore, do not change anything"
225 ;; if the old whole directory is ignored, ignore children's status
226 (cond ((eql old 'ignore) 'ignore)
227 ;; if the new status is ignored, use old
228 ((eql new 'ignore) old)
229 ;; if the old or new status is different, return different
230 ((or (eql old 'diff)
231 (eql new 'diff)) 'diff)
232 ;; if new is 'new, return new
233 ((eql new 'new) 'new)
234 ;; all other cases return old
235 (t old)))
236
237
238 (defun ztree-diff-model-find-in-files (list shortname is-dir)
239 "Find in LIST of files the file with name SHORTNAME.
240 If IS-DIR searching for directories; assume files otherwise"
241 (ztree-find list
242 (lambda (x) (and (string-equal (ztree-file-short-name x)
243 shortname)
244 (eq is-dir (file-directory-p x))))))
245
246
247 (defun ztree-diff-model-should-ignore (node)
248 "Determine if the NODE and its children should be ignored.
249 If no parent - never ignore;
250 if in ignore list - ignore
251 if parent has ignored status - ignore"
252 (let ((parent (ztree-diff-node-parent node)))
253 (and parent
254 (or (eql (ztree-diff-node-different parent) 'ignore)
255 (ztree-diff-model-ignore-p node)))))
256
257
258
259 (defun ztree-diff-node-recreate (node)
260 "Traverse 2 paths defined in the NODE updating its children and status."
261 (let* ((list1 (ztree-directory-files (ztree-diff-node-left-path node))) ;; left list of liles
262 (list2 (ztree-directory-files (ztree-diff-node-right-path node))) ;; right list of files
263 (should-ignore (ztree-diff-model-should-ignore node))
264 ;; status automatically assigned to children of the node
265 (children-status (if should-ignore 'ignore 'new))
266 (children nil)) ;; list of children
267 ;; update waiting status
268 (ztree-diff-model-update-progress)
269 ;; update node status ignore status either inhereted from the
270 ;; parent or the own
271 (when should-ignore
272 (ztree-diff-node-set-different node 'ignore))
273 ;; first - adding all entries from left directory
274 (dolist (file1 list1)
275 ;; for every entry in the first directory
276 ;; we are creating the node
277 (let* ((simple-name (ztree-file-short-name file1))
278 (isdir (file-directory-p file1))
279 ;; find if the file is in the second directory and the type
280 ;; is the same - i.e. both are directories or both are files
281 (file2 (ztree-diff-model-find-in-files list2 simple-name isdir))
282 ;; create a child. The current node is a parent
283 ;; new by default - will be overriden below if necessary
284 (child
285 (ztree-diff-node-create node file1 file2 simple-name simple-name nil children-status)))
286 ;; update child own ignore status
287 (when (ztree-diff-model-should-ignore child)
288 (ztree-diff-node-set-different child 'ignore))
289 ;; if exists on a right side with the same type,
290 ;; remove from the list of files on the right side
291 (when file2
292 (setf list2 (cl-delete file2 list2 :test 'string-equal)))
293 (cond
294 ;; when exist just on a left side and is a directory, add all
295 ((and isdir (not file2))
296 (ztree-diff-node-set-children child
297 (ztree-diff-model-subtree child
298 file1
299 'left
300 (ztree-diff-node-different child))))
301 ;; if 1) exists on both sides and 2) it is a file
302 ;; and 3) not ignored file
303 ((and file2 (not isdir) (not (eql (ztree-diff-node-different child) 'ignore)))
304 (ztree-diff-node-set-different child
305 (ztree-diff-model-files-equal file1 file2)))
306 ;; if exists on both sides and it is a directory, traverse further
307 ((and file2 isdir)
308 (ztree-diff-node-recreate child)))
309 ;; push the created node to the children list
310 (push child children)))
311 ;; second - adding entries from the right directory which are not present
312 ;; in the left directory
313 (dolist (file2 list2)
314 ;; for every entry in the second directory
315 ;; we are creating the node
316 (let* ((simple-name (ztree-file-short-name file2))
317 (isdir (file-directory-p file2))
318 ;; create the child to be added to the results list
319 (child
320 (ztree-diff-node-create node nil file2 simple-name simple-name nil children-status)))
321 ;; update ignore status of the child
322 (when (ztree-diff-model-should-ignore child)
323 (ztree-diff-node-set-different child 'ignore))
324 ;; if it is a directory, set the whole subtree to children
325 (when isdir
326 (ztree-diff-node-set-children child
327 (ztree-diff-model-subtree child
328 file2
329 'right
330 (ztree-diff-node-different child))))
331 ;; push the created node to the result list
332 (push child children)))
333 ;; finally set different status based on all children
334 ;; depending if the node should participate in overall result
335 (unless should-ignore
336 (ztree-diff-node-set-different node
337 (cl-reduce 'ztree-diff-model-update-diff
338 children
339 :initial-value 'same
340 :key 'ztree-diff-node-different)))
341 ;; and set children
342 (ztree-diff-node-set-children node children)))
343
344
345 (defun ztree-diff-model-update-node (node)
346 "Refresh the NODE."
347 (ztree-diff-node-recreate node))
348
349
350
351 (defun ztree-diff-model-set-ignore-fun (ignore-p)
352 "Set the buffer-local ignore function to IGNORE-P.
353 Ignore function is a function of one argument (ztree-diff-node)
354 which returns t if the node should be ignored (like files starting
355 with dot etc)."
356 (setf ztree-diff-model-ignore-fun ignore-p))
357
358 (defun ztree-diff-model-set-progress-fun (progess-fun)
359 (setf ztree-diff-model-progress-fun progess-fun))
360
361 (provide 'ztree-diff-model)
362
363 ;;; ztree-diff-model.el ends here