blob: b8c8da37315519385fe9cbe78340853564693e61 [file] [log] [blame]
Guido van Rossumf06ee5f1996-11-27 19:52:01 +00001#! /usr/bin/env python
Guido van Rossum9cf8f331992-03-30 10:54:51 +00002
3# A window-oriented recursive diff utility.
4# NB: This uses undocumented window classing modules.
5
6# TO DO:
7# - faster update after moving/copying one file
8# - diff flags (-b, etc.) should be global or maintained per window
9# - use a few fixed windows instead of creating new ones all the time
10# - ways to specify patterns to skip
11# (best by pointing at a file and clicking a special menu entry!)
12# - add rcsdiff menu commands
13# - add a way to view status of selected files without opening them
14# - add a way to diff two files with different names
15# - add a way to rename files
16# - keep backups of overwritten/deleted files
17# - a way to mark specified files as uninteresting for dircmp
18
19import sys
20import os
21import rand
22import commands
23import dircache
24import statcache
25import cmp
26import cmpcache
27import stdwin
28import gwin
29import textwin
30import filewin
31import tablewin
32import anywin
33
34mkarg = commands.mkarg
35mk2arg = commands.mk2arg
36
37# List of names to ignore in dircmp()
38#
Sjoerd Mullender9f023311995-07-10 11:58:31 +000039skiplist = ['RCS', 'CVS', '.Amake', 'tags', 'TAGS', '.', '..']
Guido van Rossum9cf8f331992-03-30 10:54:51 +000040
41# Function to determine whether a name should be ignored in dircmp().
42#
43def skipthis(file):
44 return file[-1:] == '~' or file in skiplist
45
46
47def anydiff(a, b, flags): # Display differences between any two objects
48 print 'diff', flags, a, b
49 if os.path.isdir(a) and os.path.isdir(b):
50 w = dirdiff(a, b, flags)
51 else:
52 w = filediff(a, b, flags)
53 addstatmenu(w, [a, b])
54 w.original_close = w.close
55 w.close = close_dirwin
56 return w
57
58def close_dirwin(w):
59 close_subwindows(w, (), 0)
60 w.original_close(w)
61
62def filediff(a, b, flags): # Display differences between two text files
63 diffcmd = 'diff'
64 if flags: diffcmd = diffcmd + mkarg(flags)
65 diffcmd = diffcmd + mkarg(a) + mkarg(b)
66 difftext = commands.getoutput(diffcmd)
67 return textwin.open_readonly(mktitle(a, b), difftext)
68
69def dirdiff(a, b, flags): # Display differences between two directories
70 data = diffdata(a, b, flags)
71 w = tablewin.open(mktitle(a, b), data)
72 w.flags = flags
73 w.a = a
74 w.b = b
75 addviewmenu(w)
76 addactionmenu(w)
77 return w
78
79def diffdata(a, b, flags): # Compute directory differences.
80 #
81 a_only = [('A only:', header_action), ('', header_action)]
82 b_only = [('B only:', header_action), ('', header_action)]
83 ab_diff = [('A <> B:', header_action), ('', header_action)]
84 ab_same = [('A == B:', header_action), ('', header_action)]
85 data = [a_only, b_only, ab_diff, ab_same]
86 #
87 a_list = dircache.listdir(a)[:]
88 b_list = dircache.listdir(b)[:]
89 dircache.annotate(a, a_list)
90 dircache.annotate(b, b_list)
91 a_list.sort()
92 b_list.sort()
93 #
94 for x in a_list:
95 if x in ['./', '../']:
96 pass
97 elif x not in b_list:
98 a_only.append(x, a_only_action)
99 else:
100 ax = os.path.join(a, x)
101 bx = os.path.join(b, x)
102 if os.path.isdir(ax) and os.path.isdir(bx):
103 if flags == '-r':
104 same = dircmp(ax, bx)
105 else:
106 same = 0
107 else:
108 try:
109 same = cmp.cmp(ax, bx)
110 except (RuntimeError, os.error):
111 same = 0
112 if same:
113 ab_same.append(x, ab_same_action)
114 else:
115 ab_diff.append(x, ab_diff_action)
116 #
117 for x in b_list:
118 if x in ['./', '../']:
119 pass
120 elif x not in a_list:
121 b_only.append(x, b_only_action)
122 #
123 return data
124
125# Re-read the directory.
126# Attempt to find the selected item back.
127
128def update(w):
129 setbusy(w)
130 icol, irow = w.selection
131 if 0 <= icol < len(w.data) and 2 <= irow < len(w.data[icol]):
132 selname = w.data[icol][irow][0]
133 else:
134 selname = ''
135 statcache.forget_dir(w.a)
136 statcache.forget_dir(w.b)
137 tablewin.select(w, (-1, -1))
138 tablewin.update(w, diffdata(w.a, w.b, w.flags))
139 if selname:
140 for icol in range(len(w.data)):
141 for irow in range(2, len(w.data[icol])):
142 if w.data[icol][irow][0] == selname:
143 tablewin.select(w, (icol, irow))
144 break
145
146# Action functions for table items in directory diff windows
147
148def header_action(w, string, (icol, irow), (pos, clicks, button, mask)):
149 tablewin.select(w, (-1, -1))
150
151def a_only_action(w, string, (icol, irow), (pos, clicks, button, mask)):
152 tablewin.select(w, (icol, irow))
153 if clicks == 2:
154 w2 = anyopen(os.path.join(w.a, string))
155 if w2:
156 w2.parent = w
157
158def b_only_action(w, string, (icol, irow), (pos, clicks, button, mask)):
159 tablewin.select(w, (icol, irow))
160 if clicks == 2:
161 w2 = anyopen(os.path.join(w.b, string))
162 if w2:
163 w2.parent = w
164
165def ab_diff_action(w, string, (icol, irow), (pos, clicks, button, mask)):
166 tablewin.select(w, (icol, irow))
167 if clicks == 2:
168 w2 = anydiff(os.path.join(w.a, string), os.path.join(w.b, string),'')
169 w2.parent = w
170
171def ab_same_action(w, string, sel, detail):
172 ax = os.path.join(w.a, string)
173 if os.path.isdir(ax):
174 ab_diff_action(w, string, sel, detail)
175 else:
176 a_only_action(w, string, sel, detail)
177
178def anyopen(name): # Open any kind of document, ignore errors
179 try:
180 w = anywin.open(name)
181 except (RuntimeError, os.error):
182 stdwin.message('Can\'t open ' + name)
183 return 0
184 addstatmenu(w, [name])
185 return w
186
187def dircmp(a, b): # Compare whether two directories are the same
188 # To make this as fast as possible, it uses the statcache
189 print ' dircmp', a, b
190 a_list = dircache.listdir(a)
191 b_list = dircache.listdir(b)
192 for x in a_list:
193 if skipthis(x):
194 pass
195 elif x not in b_list:
196 return 0
197 else:
198 ax = os.path.join(a, x)
199 bx = os.path.join(b, x)
200 if statcache.isdir(ax) and statcache.isdir(bx):
201 if not dircmp(ax, bx): return 0
202 else:
203 try:
204 if not cmpcache.cmp(ax, bx): return 0
205 except (RuntimeError, os.error):
206 return 0
207 for x in b_list:
208 if skipthis(x):
209 pass
210 elif x not in a_list:
211 return 0
212 return 1
213
214
215# View menu (for dir diff windows only)
216
217def addviewmenu(w):
218 w.viewmenu = m = w.menucreate('View')
219 m.action = []
220 add(m, 'diff -r A B', diffr_ab)
221 add(m, 'diff A B', diff_ab)
222 add(m, 'diff -b A B', diffb_ab)
223 add(m, 'diff -c A B', diffc_ab)
224 add(m, 'gdiff A B', gdiff_ab)
225 add(m, ('Open A ', 'A'), open_a)
226 add(m, ('Open B ', 'B'), open_b)
227 add(m, 'Rescan', rescan)
228 add(m, 'Rescan -r', rescan_r)
229
230# Action menu (for dir diff windows only)
231
232def addactionmenu(w):
233 w.actionmenu = m = w.menucreate('Action')
234 m.action = []
235 add(m, 'cp A B', cp_ab)
236 add(m, 'rm B', rm_b)
237 add(m, '', nop)
238 add(m, 'cp B A', cp_ba)
239 add(m, 'rm A', rm_a)
240
241# Main menu (global):
242
243def mainmenu():
244 m = stdwin.menucreate('Wdiff')
245 m.action = []
246 add(m, ('Quit wdiff', 'Q'), quit_wdiff)
247 add(m, 'Close subwindows', close_subwindows)
248 return m
249
250def add(m, text, action):
251 m.additem(text)
252 m.action.append(action)
253
254def quit_wdiff(w, m, item):
255 if askyesno('Really quit wdiff altogether?', 1):
256 sys.exit(0)
257
258def close_subwindows(w, m, item):
259 while 1:
260 for w2 in gwin.windows:
261 if w2.parent == w:
262 close_subwindows(w2, m, item)
263 w2.close(w2)
264 break # inner loop, continue outer loop
265 else:
266 break # outer loop
267
268def diffr_ab(w, m, item):
269 dodiff(w, '-r')
270
271def diff_ab(w, m, item):
272 dodiff(w, '')
273
274def diffb_ab(w, m, item):
275 dodiff(w, '-b')
276
277def diffc_ab(w, m, item):
278 dodiff(w, '-c')
279
280def gdiff_ab(w, m, item): # Call SGI's gdiff utility
281 x = getselection(w)
282 if x:
283 a, b = os.path.join(w.a, x), os.path.join(w.b, x)
284 if os.path.isdir(a) or os.path.isdir(b):
285 stdwin.fleep() # This is for files only
286 else:
287 diffcmd = 'gdiff'
288 diffcmd = diffcmd + mkarg(a) + mkarg(b) + ' &'
289 print diffcmd
290 sts = os.system(diffcmd)
291 if sts: print 'Exit status', sts
292
293def dodiff(w, flags):
294 x = getselection(w)
295 if x:
296 w2 = anydiff(os.path.join(w.a, x), os.path.join(w.b, x), flags)
297 w2.parent = w
298
299def open_a(w, m, item):
300 x = getselection(w)
301 if x:
302 w2 = anyopen(os.path.join(w.a, x))
303 if w2:
304 w2.parent = w
305
306def open_b(w, m, item):
307 x = getselection(w)
308 if x:
309 w2 = anyopen(os.path.join(w.b, x))
310 if w2:
311 w2.parent = w
312
313def rescan(w, m, item):
314 w.flags = ''
315 update(w)
316
317def rescan_r(w, m, item):
318 w.flags = '-r'
319 update(w)
320
321def rm_a(w, m, item):
322 x = getselection(w)
323 if x:
324 if x[-1:] == '/': x = x[:-1]
325 x = os.path.join(w.a, x)
326 if os.path.isdir(x):
327 if askyesno('Recursively remove A directory ' + x, 1):
328 runcmd('rm -rf' + mkarg(x))
329 else:
330 runcmd('rm -f' + mkarg(x))
331 update(w)
332
333def rm_b(w, m, item):
334 x = getselection(w)
335 if x:
336 if x[-1:] == '/': x = x[:-1]
337 x = os.path.join(w.b, x)
338 if os.path.isdir(x):
339 if askyesno('Recursively remove B directory ' + x, 1):
340 runcmd('rm -rf' + mkarg(x))
341 else:
342 runcmd('rm -f' + mkarg(x))
343 update(w)
344
345def cp_ab(w, m, item):
346 x = getselection(w)
347 if x:
348 if x[-1:] == '/': x = x[:-1]
349 ax = os.path.join(w.a, x)
350 bx = os.path.join(w.b, x)
351 if os.path.isdir(ax):
352 if os.path.exists(bx):
353 m = 'Can\'t copy directory to existing target'
354 stdwin.message(m)
355 return
356 runcmd('cp -r' + mkarg(ax) + mkarg(w.b))
357 else:
358 runcmd('cp' + mkarg(ax) + mk2arg(w.b, x))
359 update(w)
360
361def cp_ba(w, m, item):
362 x = getselection(w)
363 if x:
364 if x[-1:] == '/': x = x[:-1]
365 ax = os.path.join(w.a, x)
366 bx = os.path.join(w.b, x)
367 if os.path.isdir(bx):
368 if os.path.exists(ax):
369 m = 'Can\'t copy directory to existing target'
370 stdwin.message(m)
371 return
372 runcmd('cp -r' + mkarg(bx) + mkarg(w.a))
373 else:
374 runcmd('cp' + mk2arg(w.b, x) + mkarg(ax))
375 update(w)
376
377def nop(args):
378 pass
379
380def getselection(w):
381 icol, irow = w.selection
382 if 0 <= icol < len(w.data):
383 if 0 <= irow < len(w.data[icol]):
384 return w.data[icol][irow][0]
385 stdwin.message('no selection')
386 return ''
387
388def runcmd(cmd):
389 print cmd
390 sts, output = commands.getstatusoutput(cmd)
391 if sts or output:
392 if not output:
393 output = 'Exit status ' + `sts`
394 stdwin.message(output)
395
396
397# Status menu (for all kinds of windows)
398
399def addstatmenu(w, files):
400 w.statmenu = m = w.menucreate('Stat')
401 m.files = files
402 m.action = []
403 for file in files:
404 m.additem(commands.getstatus(file))
405 m.action.append(stataction)
406
407def stataction(w, m, item): # Menu item action for stat menu
408 file = m.files[item]
409 try:
410 m.setitem(item, commands.getstatus(file))
411 except os.error:
412 stdwin.message('Can\'t get status for ' + file)
413
414
415# Compute a suitable window title from two paths
416
417def mktitle(a, b):
418 if a == b: return a
419 i = 1
420 while a[-i:] == b[-i:]: i = i+1
421 i = i-1
422 if not i:
423 return a + ' ' + b
424 else:
425 return '{' + a[:-i] + ',' + b[:-i] + '}' + a[-i:]
426
427
428# Ask a confirmation question
429
430def askyesno(prompt, default):
431 try:
432 return stdwin.askync(prompt, default)
433 except KeyboardInterrupt:
434 return 0
435
436
437# Display a message "busy" in a window, and mark it for updating
438
439def setbusy(w):
440 left, top = w.getorigin()
441 width, height = w.getwinsize()
442 right, bottom = left + width, top + height
443 d = w.begindrawing()
444 d.erase((0, 0), (10000, 10000))
445 text = 'Busy...'
446 textwidth = d.textwidth(text)
447 textheight = d.lineheight()
448 h, v = left + (width-textwidth)/2, top + (height-textheight)/2
449 d.text((h, v), text)
450 del d
451 w.change((0, 0), (10000, 10000))
452
453
454# Main function
455
456def main():
457 print 'wdiff: warning: this program does NOT make backups'
458 argv = sys.argv
459 flags = ''
460 if len(argv) >= 2 and argv[1][:1] == '-':
461 flags = argv[1]
462 del argv[1]
Guido van Rossum37f142d1994-10-08 19:09:41 +0000463 stdwin.setdefscrollbars(0, 1)
Guido van Rossum9cf8f331992-03-30 10:54:51 +0000464 m = mainmenu() # Create menu earlier than windows
465 if len(argv) == 2: # 1 argument
466 w = anyopen(argv[1])
467 if not w: return
468 elif len(argv) == 3: # 2 arguments
469 w = anydiff(argv[1], argv[2], flags)
470 w.parent = ()
471 else:
472 sys.stdout = sys.stderr
473 print 'usage:', argv[0], '[diff-flags] dir-1 [dir-2]'
474 sys.exit(2)
475 del w # It's preserved in gwin.windows
476 while 1:
477 try:
478 gwin.mainloop()
479 break
480 except KeyboardInterrupt:
481 pass # Just continue...
482
483# Start the main function (this is a script)
484main()