#! /usr/bin/env python # A window-oriented recursive diff utility. # NB: This uses undocumented window classing modules. # TO DO: # - faster update after moving/copying one file # - diff flags (-b, etc.) should be global or maintained per window # - use a few fixed windows instead of creating new ones all the time # - ways to specify patterns to skip # (best by pointing at a file and clicking a special menu entry!) # - add rcsdiff menu commands # - add a way to view status of selected files without opening them # - add a way to diff two files with different names # - add a way to rename files # - keep backups of overwritten/deleted files # - a way to mark specified files as uninteresting for dircmp import sys import os import rand import commands import dircache import statcache import cmp import cmpcache import stdwin import gwin import textwin import filewin import tablewin import anywin mkarg = commands.mkarg mk2arg = commands.mk2arg # List of names to ignore in dircmp() # skiplist = ['RCS', 'CVS', '.Amake', 'tags', 'TAGS', '.', '..'] # Function to determine whether a name should be ignored in dircmp(). # def skipthis(file): return file[-1:] == '~' or file in skiplist def anydiff(a, b, flags): # Display differences between any two objects print 'diff', flags, a, b if os.path.isdir(a) and os.path.isdir(b): w = dirdiff(a, b, flags) else: w = filediff(a, b, flags) addstatmenu(w, [a, b]) w.original_close = w.close w.close = close_dirwin return w def close_dirwin(w): close_subwindows(w, (), 0) w.original_close(w) def filediff(a, b, flags): # Display differences between two text files diffcmd = 'diff' if flags: diffcmd = diffcmd + mkarg(flags) diffcmd = diffcmd + mkarg(a) + mkarg(b) difftext = commands.getoutput(diffcmd) return textwin.open_readonly(mktitle(a, b), difftext) def dirdiff(a, b, flags): # Display differences between two directories data = diffdata(a, b, flags) w = tablewin.open(mktitle(a, b), data) w.flags = flags w.a = a w.b = b addviewmenu(w) addactionmenu(w) return w def diffdata(a, b, flags): # Compute directory differences. # a_only = [('A only:', header_action), ('', header_action)] b_only = [('B only:', header_action), ('', header_action)] ab_diff = [('A <> B:', header_action), ('', header_action)] ab_same = [('A == B:', header_action), ('', header_action)] data = [a_only, b_only, ab_diff, ab_same] # a_list = dircache.listdir(a)[:] b_list = dircache.listdir(b)[:] dircache.annotate(a, a_list) dircache.annotate(b, b_list) a_list.sort() b_list.sort() # for x in a_list: if x in ['./', '../']: pass elif x not in b_list: a_only.append((x, a_only_action)) else: ax = os.path.join(a, x) bx = os.path.join(b, x) if os.path.isdir(ax) and os.path.isdir(bx): if flags == '-r': same = dircmp(ax, bx) else: same = 0 else: try: same = cmp.cmp(ax, bx) except (RuntimeError, os.error): same = 0 if same: ab_same.append((x, ab_same_action)) else: ab_diff.append((x, ab_diff_action)) # for x in b_list: if x in ['./', '../']: pass elif x not in a_list: b_only.append((x, b_only_action)) # return data # Re-read the directory. # Attempt to find the selected item back. def update(w): setbusy(w) icol, irow = w.selection if 0 <= icol < len(w.data) and 2 <= irow < len(w.data[icol]): selname = w.data[icol][irow][0] else: selname = '' statcache.forget_dir(w.a) statcache.forget_dir(w.b) tablewin.select(w, (-1, -1)) tablewin.update(w, diffdata(w.a, w.b, w.flags)) if selname: for icol in range(len(w.data)): for irow in range(2, len(w.data[icol])): if w.data[icol][irow][0] == selname: tablewin.select(w, (icol, irow)) break # Action functions for table items in directory diff windows def header_action(w, string, (icol, irow), (pos, clicks, button, mask)): tablewin.select(w, (-1, -1)) def a_only_action(w, string, (icol, irow), (pos, clicks, button, mask)): tablewin.select(w, (icol, irow)) if clicks == 2: w2 = anyopen(os.path.join(w.a, string)) if w2: w2.parent = w def b_only_action(w, string, (icol, irow), (pos, clicks, button, mask)): tablewin.select(w, (icol, irow)) if clicks == 2: w2 = anyopen(os.path.join(w.b, string)) if w2: w2.parent = w def ab_diff_action(w, string, (icol, irow), (pos, clicks, button, mask)): tablewin.select(w, (icol, irow)) if clicks == 2: w2 = anydiff(os.path.join(w.a, string), os.path.join(w.b, string),'') w2.parent = w def ab_same_action(w, string, sel, detail): ax = os.path.join(w.a, string) if os.path.isdir(ax): ab_diff_action(w, string, sel, detail) else: a_only_action(w, string, sel, detail) def anyopen(name): # Open any kind of document, ignore errors try: w = anywin.open(name) except (RuntimeError, os.error): stdwin.message('Can\'t open ' + name) return 0 addstatmenu(w, [name]) return w def dircmp(a, b): # Compare whether two directories are the same # To make this as fast as possible, it uses the statcache print ' dircmp', a, b a_list = dircache.listdir(a) b_list = dircache.listdir(b) for x in a_list: if skipthis(x): pass elif x not in b_list: return 0 else: ax = os.path.join(a, x) bx = os.path.join(b, x) if statcache.isdir(ax) and statcache.isdir(bx): if not dircmp(ax, bx): return 0 else: try: if not cmpcache.cmp(ax, bx): return 0 except (RuntimeError, os.error): return 0 for x in b_list: if skipthis(x): pass elif x not in a_list: return 0 return 1 # View menu (for dir diff windows only) def addviewmenu(w): w.viewmenu = m = w.menucreate('View') m.action = [] add(m, 'diff -r A B', diffr_ab) add(m, 'diff A B', diff_ab) add(m, 'diff -b A B', diffb_ab) add(m, 'diff -c A B', diffc_ab) add(m, 'gdiff A B', gdiff_ab) add(m, ('Open A ', 'A'), open_a) add(m, ('Open B ', 'B'), open_b) add(m, 'Rescan', rescan) add(m, 'Rescan -r', rescan_r) # Action menu (for dir diff windows only) def addactionmenu(w): w.actionmenu = m = w.menucreate('Action') m.action = [] add(m, 'cp A B', cp_ab) add(m, 'rm B', rm_b) add(m, '', nop) add(m, 'cp B A', cp_ba) add(m, 'rm A', rm_a) # Main menu (global): def mainmenu(): m = stdwin.menucreate('Wdiff') m.action = [] add(m, ('Quit wdiff', 'Q'), quit_wdiff) add(m, 'Close subwindows', close_subwindows) return m def add(m, text, action): m.additem(text) m.action.append(action) def quit_wdiff(w, m, item): if askyesno('Really quit wdiff altogether?', 1): sys.exit(0) def close_subwindows(w, m, item): while 1: for w2 in gwin.windows: if w2.parent == w: close_subwindows(w2, m, item) w2.close(w2) break # inner loop, continue outer loop else: break # outer loop def diffr_ab(w, m, item): dodiff(w, '-r') def diff_ab(w, m, item): dodiff(w, '') def diffb_ab(w, m, item): dodiff(w, '-b') def diffc_ab(w, m, item): dodiff(w, '-c') def gdiff_ab(w, m, item): # Call SGI's gdiff utility x = getselection(w) if x: a, b = os.path.join(w.a, x), os.path.join(w.b, x) if os.path.isdir(a) or os.path.isdir(b): stdwin.fleep() # This is for files only else: diffcmd = 'gdiff' diffcmd = diffcmd + mkarg(a) + mkarg(b) + ' &' print diffcmd sts = os.system(diffcmd) if sts: print 'Exit status', sts def dodiff(w, flags): x = getselection(w) if x: w2 = anydiff(os.path.join(w.a, x), os.path.join(w.b, x), flags) w2.parent = w def open_a(w, m, item): x = getselection(w) if x: w2 = anyopen(os.path.join(w.a, x)) if w2: w2.parent = w def open_b(w, m, item): x = getselection(w) if x: w2 = anyopen(os.path.join(w.b, x)) if w2: w2.parent = w def rescan(w, m, item): w.flags = '' update(w) def rescan_r(w, m, item): w.flags = '-r' update(w) def rm_a(w, m, item): x = getselection(w) if x: if x[-1:] == '/': x = x[:-1] x = os.path.join(w.a, x) if os.path.isdir(x): if askyesno('Recursively remove A directory ' + x, 1): runcmd('rm -rf' + mkarg(x)) else: runcmd('rm -f' + mkarg(x)) update(w) def rm_b(w, m, item): x = getselection(w) if x: if x[-1:] == '/': x = x[:-1] x = os.path.join(w.b, x) if os.path.isdir(x): if askyesno('Recursively remove B directory ' + x, 1): runcmd('rm -rf' + mkarg(x)) else: runcmd('rm -f' + mkarg(x)) update(w) def cp_ab(w, m, item): x = getselection(w) if x: if x[-1:] == '/': x = x[:-1] ax = os.path.join(w.a, x) bx = os.path.join(w.b, x) if os.path.isdir(ax): if os.path.exists(bx): m = 'Can\'t copy directory to existing target' stdwin.message(m) return runcmd('cp -r' + mkarg(ax) + mkarg(w.b)) else: runcmd('cp' + mkarg(ax) + mk2arg(w.b, x)) update(w) def cp_ba(w, m, item): x = getselection(w) if x: if x[-1:] == '/': x = x[:-1] ax = os.path.join(w.a, x) bx = os.path.join(w.b, x) if os.path.isdir(bx): if os.path.exists(ax): m = 'Can\'t copy directory to existing target' stdwin.message(m) return runcmd('cp -r' + mkarg(bx) + mkarg(w.a)) else: runcmd('cp' + mk2arg(w.b, x) + mkarg(ax)) update(w) def nop(args): pass def getselection(w): icol, irow = w.selection if 0 <= icol < len(w.data): if 0 <= irow < len(w.data[icol]): return w.data[icol][irow][0] stdwin.message('no selection') return '' def runcmd(cmd): print cmd sts, output = commands.getstatusoutput(cmd) if sts or output: if not output: output = 'Exit status ' + `sts` stdwin.message(output) # Status menu (for all kinds of windows) def addstatmenu(w, files): w.statmenu = m = w.menucreate('Stat') m.files = files m.action = [] for file in files: m.additem(commands.getstatus(file)) m.action.append(stataction) def stataction(w, m, item): # Menu item action for stat menu file = m.files[item] try: m.setitem(item, commands.getstatus(file)) except os.error: stdwin.message('Can\'t get status for ' + file) # Compute a suitable window title from two paths def mktitle(a, b): if a == b: return a i = 1 while a[-i:] == b[-i:]: i = i+1 i = i-1 if not i: return a + ' ' + b else: return '{' + a[:-i] + ',' + b[:-i] + '}' + a[-i:] # Ask a confirmation question def askyesno(prompt, default): try: return stdwin.askync(prompt, default) except KeyboardInterrupt: return 0 # Display a message "busy" in a window, and mark it for updating def setbusy(w): left, top = w.getorigin() width, height = w.getwinsize() right, bottom = left + width, top + height d = w.begindrawing() d.erase((0, 0), (10000, 10000)) text = 'Busy...' textwidth = d.textwidth(text) textheight = d.lineheight() h, v = left + (width-textwidth)/2, top + (height-textheight)/2 d.text((h, v), text) del d w.change((0, 0), (10000, 10000)) # Main function def main(): print 'wdiff: warning: this program does NOT make backups' argv = sys.argv flags = '' if len(argv) >= 2 and argv[1][:1] == '-': flags = argv[1] del argv[1] stdwin.setdefscrollbars(0, 1) m = mainmenu() # Create menu earlier than windows if len(argv) == 2: # 1 argument w = anyopen(argv[1]) if not w: return elif len(argv) == 3: # 2 arguments w = anydiff(argv[1], argv[2], flags) w.parent = () else: sys.stdout = sys.stderr print 'usage:', argv[0], '[diff-flags] dir-1 [dir-2]' sys.exit(2) del w # It's preserved in gwin.windows while 1: try: gwin.mainloop() break except KeyboardInterrupt: pass # Just continue... # Start the main function (this is a script) main()