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