484 lines
11 KiB
Python
484 lines
11 KiB
Python
|
#! /usr/local/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', '.Amake', '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]
|
||
|
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()
|