1 #! /usr/local/bin/python
3 # A window-oriented recursive diff utility.
4 # NB: This uses undocumented window classing modules.
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
34 mkarg
= commands
.mkarg
35 mk2arg
= commands
.mk2arg
37 # List of names to ignore in dircmp()
39 skiplist
= ['RCS', 'CVS', '.Amake', 'tags', 'TAGS', '.', '..']
41 # Function to determine whether a name should be ignored in dircmp().
44 return file[-1:] == '~' or file in skiplist
47 def 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
)
52 w
= filediff(a
, b
, flags
)
53 addstatmenu(w
, [a
, b
])
54 w
.original_close
= w
.close
55 w
.close
= close_dirwin
59 close_subwindows(w
, (), 0)
62 def filediff(a
, b
, flags
): # Display differences between two text files
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
)
69 def dirdiff(a
, b
, flags
): # Display differences between two directories
70 data
= diffdata(a
, b
, flags
)
71 w
= tablewin
.open(mktitle(a
, b
), data
)
79 def diffdata(a
, b
, flags
): # Compute directory differences.
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
]
87 a_list
= dircache
.listdir(a
)[:]
88 b_list
= dircache
.listdir(b
)[:]
89 dircache
.annotate(a
, a_list
)
90 dircache
.annotate(b
, b_list
)
95 if x
in ['./', '../']:
98 a_only
.append(x
, a_only_action
)
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
):
104 same
= dircmp(ax
, bx
)
109 same
= cmp.cmp(ax
, bx
)
110 except (RuntimeError, os
.error
):
113 ab_same
.append(x
, ab_same_action
)
115 ab_diff
.append(x
, ab_diff_action
)
118 if x
in ['./', '../']:
120 elif x
not in a_list
:
121 b_only
.append(x
, b_only_action
)
125 # Re-read the directory.
126 # Attempt to find the selected item back.
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]
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
))
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
))
146 # Action functions for table items in directory diff windows
148 def header_action(w
, string
, (icol
, irow
), (pos
, clicks
, button
, mask
)):
149 tablewin
.select(w
, (-1, -1))
151 def a_only_action(w
, string
, (icol
, irow
), (pos
, clicks
, button
, mask
)):
152 tablewin
.select(w
, (icol
, irow
))
154 w2
= anyopen(os
.path
.join(w
.a
, string
))
158 def b_only_action(w
, string
, (icol
, irow
), (pos
, clicks
, button
, mask
)):
159 tablewin
.select(w
, (icol
, irow
))
161 w2
= anyopen(os
.path
.join(w
.b
, string
))
165 def ab_diff_action(w
, string
, (icol
, irow
), (pos
, clicks
, button
, mask
)):
166 tablewin
.select(w
, (icol
, irow
))
168 w2
= anydiff(os
.path
.join(w
.a
, string
), os
.path
.join(w
.b
, string
),'')
171 def 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
)
176 a_only_action(w
, string
, sel
, detail
)
178 def anyopen(name
): # Open any kind of document, ignore errors
180 w
= anywin
.open(name
)
181 except (RuntimeError, os
.error
):
182 stdwin
.message('Can\'t open ' + name
)
184 addstatmenu(w
, [name
])
187 def 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
)
195 elif x
not in b_list
:
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
204 if not cmpcache
.cmp(ax
, bx
): return 0
205 except (RuntimeError, os
.error
):
210 elif x
not in a_list
:
215 # View menu (for dir diff windows only)
218 w
.viewmenu
= m
= w
.menucreate('View')
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
)
230 # Action menu (for dir diff windows only)
232 def addactionmenu(w
):
233 w
.actionmenu
= m
= w
.menucreate('Action')
235 add(m
, 'cp A B', cp_ab
)
238 add(m
, 'cp B A', cp_ba
)
241 # Main menu (global):
244 m
= stdwin
.menucreate('Wdiff')
246 add(m
, ('Quit wdiff', 'Q'), quit_wdiff
)
247 add(m
, 'Close subwindows', close_subwindows
)
250 def add(m
, text
, action
):
252 m
.action
.append(action
)
254 def quit_wdiff(w
, m
, item
):
255 if askyesno('Really quit wdiff altogether?', 1):
258 def close_subwindows(w
, m
, item
):
260 for w2
in gwin
.windows
:
262 close_subwindows(w2
, m
, item
)
264 break # inner loop, continue outer loop
268 def diffr_ab(w
, m
, item
):
271 def diff_ab(w
, m
, item
):
274 def diffb_ab(w
, m
, item
):
277 def diffc_ab(w
, m
, item
):
280 def gdiff_ab(w
, m
, item
): # Call SGI's gdiff utility
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
288 diffcmd
= diffcmd
+ mkarg(a
) + mkarg(b
) + ' &'
290 sts
= os
.system(diffcmd
)
291 if sts
: print 'Exit status', sts
293 def dodiff(w
, flags
):
296 w2
= anydiff(os
.path
.join(w
.a
, x
), os
.path
.join(w
.b
, x
), flags
)
299 def open_a(w
, m
, item
):
302 w2
= anyopen(os
.path
.join(w
.a
, x
))
306 def open_b(w
, m
, item
):
309 w2
= anyopen(os
.path
.join(w
.b
, x
))
313 def rescan(w
, m
, item
):
317 def rescan_r(w
, m
, item
):
321 def rm_a(w
, m
, item
):
324 if x
[-1:] == '/': x
= x
[:-1]
325 x
= os
.path
.join(w
.a
, x
)
327 if askyesno('Recursively remove A directory ' + x
, 1):
328 runcmd('rm -rf' + mkarg(x
))
330 runcmd('rm -f' + mkarg(x
))
333 def rm_b(w
, m
, item
):
336 if x
[-1:] == '/': x
= x
[:-1]
337 x
= os
.path
.join(w
.b
, x
)
339 if askyesno('Recursively remove B directory ' + x
, 1):
340 runcmd('rm -rf' + mkarg(x
))
342 runcmd('rm -f' + mkarg(x
))
345 def cp_ab(w
, m
, item
):
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'
356 runcmd('cp -r' + mkarg(ax
) + mkarg(w
.b
))
358 runcmd('cp' + mkarg(ax
) + mk2arg(w
.b
, x
))
361 def cp_ba(w
, m
, item
):
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'
372 runcmd('cp -r' + mkarg(bx
) + mkarg(w
.a
))
374 runcmd('cp' + mk2arg(w
.b
, x
) + mkarg(ax
))
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')
390 sts
, output
= commands
.getstatusoutput(cmd
)
393 output
= 'Exit status ' + `sts`
394 stdwin
.message(output
)
397 # Status menu (for all kinds of windows)
399 def addstatmenu(w
, files
):
400 w
.statmenu
= m
= w
.menucreate('Stat')
404 m
.additem(commands
.getstatus(file))
405 m
.action
.append(stataction
)
407 def stataction(w
, m
, item
): # Menu item action for stat menu
410 m
.setitem(item
, commands
.getstatus(file))
412 stdwin
.message('Can\'t get status for ' + file)
415 # Compute a suitable window title from two paths
420 while a
[-i
:] == b
[-i
:]: i
= i
+1
425 return '{' + a
[:-i
] + ',' + b
[:-i
] + '}' + a
[-i
:]
428 # Ask a confirmation question
430 def askyesno(prompt
, default
):
432 return stdwin
.askync(prompt
, default
)
433 except KeyboardInterrupt:
437 # Display a message "busy" in a window, and mark it for updating
440 left
, top
= w
.getorigin()
441 width
, height
= w
.getwinsize()
442 right
, bottom
= left
+ width
, top
+ height
444 d
.erase((0, 0), (10000, 10000))
446 textwidth
= d
.textwidth(text
)
447 textheight
= d
.lineheight()
448 h
, v
= left
+ (width
-textwidth
)/2, top
+ (height
-textheight
)/2
451 w
.change((0, 0), (10000, 10000))
457 print 'wdiff: warning: this program does NOT make backups'
460 if len(argv
) >= 2 and argv
[1][:1] == '-':
463 stdwin
.setdefscrollbars(0, 1)
464 m
= mainmenu() # Create menu earlier than windows
465 if len(argv
) == 2: # 1 argument
468 elif len(argv
) == 3: # 2 arguments
469 w
= anydiff(argv
[1], argv
[2], flags
)
472 sys
.stdout
= sys
.stderr
473 print 'usage:', argv
[0], '[diff-flags] dir-1 [dir-2]'
475 del w
# It's preserved in gwin.windows
480 except KeyboardInterrupt:
481 pass # Just continue...
483 # Start the main function (this is a script)