blob: 50ca0325d9670854fc52243eada1ac2c7bab2194 [file] [log] [blame]
#! /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()