|
#
# changeset.py - Changeset dialog for TortoiseHg
#
# Copyright 2008 Steve Borho <steve@borho.org>
#
import os
import gtk
import gobject
import pango
import StringIO
from mercurial.node import short, nullrev
from mercurial import cmdutil, context, util, ui, hg, patch
from gdialog import GDialog, Confirm
from hgcmd import CmdDialog
from hglib import toutf, fromutf, displaytime, hgcmd_toq, diffexpand, LookupError
from gtklib import StatusBar
from shlib import shell_notify
class ChangeSet(GDialog):
"""GTK+ based dialog for displaying repository logs
"""
def __init__(self, ui, repo, cwd, pats, opts, main, stbar=None):
GDialog.__init__(self, ui, repo, cwd, pats, opts, main)
self.stbar = stbar
self.glog_parent = None
def get_title(self):
title = os.path.basename(self.repo.root) + ' changeset '
title += self.opts['rev'][0]
return title
def get_icon(self):
return 'menushowchanged.ico'
def get_tbbuttons(self):
self.parent_toggle = gtk.ToggleToolButton(gtk.STOCK_UNDO)
self.parent_toggle.set_use_underline(True)
self.parent_toggle.set_label('_other parent')
self.parent_toggle.set_tooltip(self.tooltips, 'diff other parent')
self.parent_toggle.set_sensitive(False)
self.parent_toggle.set_active(False)
self.parent_toggle.connect('toggled', self._parent_toggled)
return [self.parent_toggle]
def _parent_toggled(self, button):
self.load_details(self.currev)
def prepare_display(self):
self.currow = None
self.graphview = None
self.glog_parent = None
node0, node1 = cmdutil.revpair(self.repo, self.opts.get('rev'))
self.load_details(self.repo.changelog.rev(node0))
def save_settings(self):
settings = GDialog.save_settings(self)
settings['changeset'] = self._hpaned.get_position()
return settings
def load_settings(self, settings):
GDialog.load_settings(self, settings)
if settings and 'changeset' in settings:
self._setting_hpos = settings['changeset']
else:
self._setting_hpos = -1
def load_details(self, rev):
'''Load selected changeset details into buffer and filelist'''
self.currev = rev
self._buffer.set_text('')
self._filelist.clear()
parents = [x for x in self.repo.changelog.parentrevs(rev) \
if x != nullrev]
self.parents = parents
title = self.get_title()
if len(parents) == 2:
self.parent_toggle.set_sensitive(True)
if self.parent_toggle.get_active():
title += ':' + str(self.parents[1])
else:
title += ':' + str(self.parents[0])
else:
self.parent_toggle.set_sensitive(False)
if self.parent_toggle.get_active():
# Parent button must be pushed out, but this
# will cause load_details to be called again
# so we exit out to prevent recursion.
self.parent_toggle.set_active(False)
return
ctx = self.repo.changectx(rev)
if not ctx:
self._last_rev = None
return False
self.set_title(title)
self.textview.freeze_child_notify()
try:
self._fill_buffer(self._buffer, rev, ctx, self._filelist)
finally:
self.textview.thaw_child_notify()
def _fill_buffer(self, buf, rev, ctx, filelist):
self.stbar.begin('Retrieving changeset data...')
def title_line(title, text, tag):
pad = ' ' * (12 - len(title))
utext = toutf(title + pad + text)
buf.insert_with_tags_by_name(eob, utext, tag)
buf.insert(eob, "\n")
eob = buf.get_end_iter()
date = displaytime(ctx.date())
if self.clipboard:
self.clipboard.set_text(short(ctx.node()))
change = str(rev) + ':' + short(ctx.node())
tags = ' '.join(ctx.tags())
parents = self.parents
title_line('changeset:', change, 'changeset')
if ctx.branch() != 'default':
title_line('branch:', ctx.branch(), 'greybg')
title_line('user/date:', ctx.user() + '\t' + date, 'changeset')
for p in parents:
pctx = self.repo.changectx(p)
try:
summary = pctx.description().splitlines()[0]
summary = toutf(summary)
except:
summary = ""
change = str(p) + ':' + short(self.repo.changelog.node(p))
title = 'parent:'
title += ' ' * (12 - len(title))
buf.insert_with_tags_by_name(eob, title, 'parent')
buf.insert_with_tags_by_name(eob, change, 'link')
buf.insert_with_tags_by_name(eob, ' ' + summary, 'parent')
buf.insert(eob, "\n")
for n in self.repo.changelog.children(ctx.node()):
cctx = self.repo.changectx(n)
try:
summary = cctx.description().splitlines()[0]
summary = toutf(summary)
except:
summary = ""
childrev = self.repo.changelog.rev(n)
change = str(childrev) + ':' + short(n)
title = 'child:'
title += ' ' * (12 - len(title))
buf.insert_with_tags_by_name(eob, title, 'parent')
buf.insert_with_tags_by_name(eob, change, 'link')
buf.insert_with_tags_by_name(eob, ' ' + summary, 'parent')
buf.insert(eob, "\n")
for n in self.repo.changelog.children(ctx.node()):
childrev = self.repo.changelog.rev(n)
if tags: title_line('tags:', tags, 'tag')
log = toutf(ctx.description())
buf.insert(eob, '\n' + log + '\n\n')
if self.parent_toggle.get_active():
parent = self.repo.changelog.node(parents[1])
elif parents:
parent = self.repo.changelog.node(parents[0])
else:
parent = nullid
buf.create_mark('begmark', buf.get_start_iter())
filelist.append(('*', '[Description]', 'begmark', False, ()))
pctx = self.repo.changectx(parent)
nodes = parent, ctx.node()
iterator = self.diff_generator(*nodes)
gobject.idle_add(self.get_diffs, iterator, nodes, pctx, buf, filelist)
self.curnodes = nodes
def get_diffs(self, iterator, nodes, pctx, buf, filelist):
if self.curnodes != nodes:
return False
try:
status, wfile, txt = iterator.next()
except StopIteration:
self.stbar.end()
return False
lines = txt.splitlines()
eob = buf.get_end_iter()
offset = eob.get_offset()
fileoffs, tags, lines, statmax = self.prepare_diff(lines, offset, wfile)
for l in lines:
buf.insert(eob, l)
# inserts the tags
for name, p0, p1 in tags:
i0 = buf.get_iter_at_offset(p0)
i1 = buf.get_iter_at_offset(p1)
txt = buf.get_text(i0, i1)
buf.apply_tag_by_name(name, i0, i1)
# inserts the marks
for mark, offset, stats in fileoffs:
pos = buf.get_iter_at_offset(offset)
mark = 'mark_%d' % offset
buf.create_mark(mark, pos)
filelist.append((status, toutf(wfile), mark, True, stats))
sob, eob = buf.get_bounds()
buf.apply_tag_by_name("mono", pos, eob)
return True
# Hacked up version of mercurial.patch.diff()
# Use git mode by default (to show copies, renames, permissions) but
# never show binary diffs. It operates as a generator, so it can be
# called iteratively to get file diffs from a changeset
def diff_generator(self, node1, node2):
repo = self.repo
ccache = {}
def getctx(r):
if r not in ccache:
ccache[r] = context.changectx(repo, r)
return ccache[r]
flcache = {}
def getfilectx(f, ctx):
flctx = ctx.filectx(f, filelog=flcache.get(f))
if f not in flcache:
flcache[f] = flctx._filelog
return flctx
ctx1 = context.changectx(repo, node1) # parent
ctx2 = context.changectx(repo, node2) # current
changes = repo.status(node1, node2, None)[:5]
modified, added, removed, deleted, unknown = changes
filelist = modified + added + removed
# force manifest reading
man1 = ctx1.manifest()
date1 = util.datestr(ctx1.date())
flags2 = ctx2.manifest().flags
# returns False if there was no rename between ctx1 and ctx2
# returns None if the file was created between ctx1 and ctx2
# returns the (file, node) present in ctx1 that was renamed to f in ctx2
# This will only really work if c1 is the Nth 1st parent of c2.
def renamed(c1, c2, man, f):
startrev = c1.rev()
c = c2
crev = c.rev()
if crev is None:
crev = len(repo.changelog)
orig = f
files = (f,)
while crev > startrev:
if f in files:
try:
src = getfilectx(f, c).renamed()
except LookupError:
return None
if src:
f = src[0]
crev = c.parents()[0].rev()
# try to reuse
c = getctx(crev)
files = c.files()
if f not in man:
return None
if f == orig:
return False
return f
status = {}
def filestatus(f):
if f in status:
return status[f]
try:
# Determine file status by presence in manifests
s = 'R'
ctx2.filectx(f)
s = 'A'
ctx1.filectx(f)
s = 'M'
except LookupError:
pass
status[f] = s
return s
copied = {}
for f in filelist:
src = renamed(ctx1, ctx2, man1, f)
if src:
copied[f] = src
srcs = [x[1] for x in copied.iteritems() if filestatus(x[0]) == 'A']
gitmode = {'l': '120000', 'x': '100755', '': '100644'}
gone = {}
for f in filelist:
s = filestatus(f)
to = None
tn = None
dodiff = True
header = []
if f in man1:
to = getfilectx(f, ctx1).data()
if s != 'R':
tn = getfilectx(f, ctx2).data()
a, b = f, f
def addmodehdr(header, omode, nmode):
if omode != nmode:
header.append('old mode %s\n' % omode)
header.append('new mode %s\n' % nmode)
if s == 'A':
mode = gitmode[flags2(f)]
if f in copied:
a = copied[f]
omode = gitmode[man1.flags(a)]
addmodehdr(header, omode, mode)
if filestatus(a) == 'R' and a not in gone:
op = 'rename'
gone[a] = 1
else:
op = 'copy'
header.append('%s from %s\n' % (op, a))
header.append('%s to %s\n' % (op, f))
to = getfilectx(a, ctx1).data()
else:
header.append('new file mode %s\n' % mode)
if util.binary(tn):
dodiff = 'binary'
elif s == 'R':
if f in srcs:
dodiff = False
else:
mode = gitmode[man1.flags(f)]
header.append('deleted file mode %s\n' % mode)
else:
omode = gitmode[man1.flags(f)]
nmode = gitmode[flags2(f)]
addmodehdr(header, omode, nmode)
if util.binary(to) or util.binary(tn):
dodiff = 'binary'
header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
if dodiff == 'binary':
text = 'binary file has changed.\n'
elif dodiff:
try:
text = patch.mdiff.unidiff(to, date1,
tn, util.datestr(ctx2.date()),
fn1=a, fn2=b, r=None,
opts=patch.mdiff.defaultopts)
except TypeError:
# hg-0.9.5 and before
text = patch.mdiff.unidiff(to, date1,
tn, util.datestr(ctx2.date()),
f, None, opts=patch.mdiff.defaultopts)
else:
text = ''
if header or text: yield (s, f, ''.join(header) + text)
def prepare_diff(self, difflines, offset, fname):
'''Borrowed from hgview; parses changeset diffs'''
DIFFHDR = "=== %s ===\n"
idx = 0
outlines = []
tags = []
filespos = []
def addtag( name, offset, length ):
if tags and tags[-1][0] == name and tags[-1][2]==offset:
tags[-1][2] += length
else:
tags.append( [name, offset, offset+length] )
stats = [0,0]
statmax = 0
for i,l1 in enumerate(difflines):
l = toutf(l1)
if l.startswith("diff"):
txt = toutf(DIFFHDR % fname)
addtag( "greybg", offset, len(txt) )
outlines.append(txt)
markname = "file%d" % idx
idx += 1
statmax = max( statmax, stats[0]+stats[1] )
stats = [0,0]
filespos.append(( markname, offset, stats ))
offset += len(txt.decode('utf-8'))
continue
elif l.startswith("+++"):
continue
elif l.startswith("---"):
continue
elif l.startswith("@@"):
tag = "blue"
elif l.startswith("+"):
tag = "green"
stats[0] += 1
l = diffexpand(l)
elif l.startswith("-"):
stats[1] += 1
tag = "red"
l = diffexpand(l)
else:
tag = "black"
l = diffexpand(l)
l = l+"\n"
length = len(l.decode('utf-8'))
addtag( tag, offset, length )
outlines.append( l )
offset += length
statmax = max( statmax, stats[0]+stats[1] )
return filespos, tags, outlines, statmax
def link_event(self, tag, widget, event, liter):
if event.type != gtk.gdk.BUTTON_RELEASE:
return
text = self.get_link_text(tag, widget, liter)
if not text:
return
linkrev = long(text.split(':')[0])
if self.graphview:
self.graphview.set_revision_id(linkrev)
self.graphview.scroll_to_revision(linkrev)
else:
self.load_details(linkrev)
def get_link_text(self, tag, widget, liter):
"""handle clicking on a link in a textview"""
text_buffer = widget.get_buffer()
beg = liter.copy()
while not beg.begins_tag(tag):
beg.backward_char()
end = liter.copy()
while not end.ends_tag(tag):
end.forward_char()
text = text_buffer.get_text(beg, end)
return text
def file_context_menu(self):
def create_menu(label, callback):
menuitem = gtk.MenuItem(label, True)
menuitem.connect('activate', callback)
menuitem.set_border_width(1)
return menuitem
_menu = gtk.Menu()
_menu.append(create_menu('_visual diff', self._diff_file_rev))
_menu.append(create_menu('diff to _local', self._diff_to_local))
_menu.append(create_menu('_view at revision', self._view_file_rev))
self._save_menu = create_menu('_save at revision', self._save_file_rev)
_menu.append(self._save_menu)
_menu.append(create_menu('_file history', self._file_history))
self._ann_menu = create_menu('_annotate file', self._ann_file)
_menu.append(self._ann_menu)
_menu.append(create_menu('_revert file contents', self._revert_file))
_menu.show_all()
return _menu
def get_body(self):
self.curfile = None
if self.repo.ui.configbool('tortoisehg', 'copyhash'):
sel = (os.name == 'nt') and 'CLIPBOARD' or 'PRIMARY'
self.clipboard = gtk.Clipboard(selection=sel)
else:
self.clipboard = None
self._filemenu = self.file_context_menu()
details_frame = gtk.Frame()
details_frame.set_shadow_type(gtk.SHADOW_ETCHED_IN)
scroller = gtk.ScrolledWindow()
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
details_frame.add(scroller)
details_text = gtk.TextView()
details_text.set_wrap_mode(gtk.WRAP_NONE)
details_text.set_editable(False)
details_text.modify_font(pango.FontDescription(self.fontcomment))
scroller.add(details_text)
self._buffer = gtk.TextBuffer()
self.setup_tags()
details_text.set_buffer(self._buffer)
self.textview = details_text
filelist_tree = gtk.TreeView()
filesel = filelist_tree.get_selection()
filesel.connect("changed", self._filelist_rowchanged)
filelist_tree.connect('button-release-event',
self._file_button_release)
filelist_tree.connect('popup-menu', self._file_popup_menu)
filelist_tree.connect('row-activated', self._file_row_act)
filelist_tree.set_search_equal_func(self.search_filelist)
accelgroup = gtk.AccelGroup()
if self.glog_parent:
self.glog_parent.add_accel_group(accelgroup)
else:
self.add_accel_group(accelgroup)
key, modifier = gtk.accelerator_parse('<Control>d')
filelist_tree.add_accelerator('thg-diff', accelgroup, key,
modifier, gtk.ACCEL_VISIBLE)
filelist_tree.connect('thg-diff', self.thgdiff)
self._filelist = gtk.ListStore(
gobject.TYPE_STRING, # MAR status
gobject.TYPE_STRING, # filename (utf-8 encoded)
gobject.TYPE_PYOBJECT, # mark
gobject.TYPE_PYOBJECT, # give cmenu
gobject.TYPE_PYOBJECT, # diffstats
)
filelist_tree.set_model(self._filelist)
column = gtk.TreeViewColumn('Stat', gtk.CellRendererText(), text=0)
filelist_tree.append_column(column)
column = gtk.TreeViewColumn('Files', gtk.CellRendererText(), text=1)
filelist_tree.append_column(column)
list_frame = gtk.Frame()
list_frame.set_shadow_type(gtk.SHADOW_ETCHED_IN)
scroller = gtk.ScrolledWindow()
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
scroller.add(filelist_tree)
list_frame.add(scroller)
self._hpaned = gtk.HPaned()
self._hpaned.pack1(list_frame, True, True)
self._hpaned.pack2(details_frame, True, True)
self._hpaned.set_position(self._setting_hpos)
if self.stbar:
# embedded by changelog browser
return self._hpaned
else:
# add status bar for main app
vbox = gtk.VBox()
vbox.pack_start(self._hpaned, True, True)
self.stbar = StatusBar()
self.stbar.show()
vbox.pack_start(gtk.HSeparator(), False, False)
vbox.pack_start(self.stbar, False, False)
return vbox
def search_filelist(self, model, column, key, iter):
'case insensitive filename search'
key = key.lower()
if key in model.get_value(iter, 1).lower():
return False
return True
def setup_tags(self):
"""Creates the tags to be used inside the TextView"""
def make_texttag( name, **kwargs ):
"""Helper function generating a TextTag"""
tag = gtk.TextTag(name)
for key, value in kwargs.iteritems():
key = key.replace("_","-")
try:
tag.set_property( key, value )
except TypeError:
print "Warning the property %s is unsupported in" % key
print "this version of pygtk"
return tag
tag_table = self._buffer.get_tag_table()
tag_table.add( make_texttag('changeset', foreground='#000090',
paragraph_background='#F0F0F0'))
tag_table.add(make_texttag('date', foreground='#000090',
paragraph_background='#F0F0F0'))
tag_table.add(make_texttag('tag', foreground='#000090',
paragraph_background='#F0F0F0'))
tag_table.add(make_texttag('files', foreground='#5C5C5C',
paragraph_background='#F0F0F0'))
tag_table.add(make_texttag('parent', foreground='#000090',
paragraph_background='#F0F0F0'))
tag_table.add( make_texttag( "mono", family="Monospace" ))
tag_table.add( make_texttag( "blue", foreground='blue' ))
tag_table.add( make_texttag( "red", foreground='red' ))
tag_table.add( make_texttag( "green", foreground='darkgreen' ))
tag_table.add( make_texttag( "black", foreground='black' ))
tag_table.add( make_texttag( "greybg",
paragraph_background='grey',
weight=pango.WEIGHT_BOLD ))
tag_table.add( make_texttag( "yellowbg", background='yellow' ))
link_tag = make_texttag( "link", foreground="blue",
underline=pango.UNDERLINE_SINGLE )
link_tag.connect("event", self.link_event )
tag_table.add( link_tag )
def _filelist_rowchanged(self, sel):
model, path = sel.get_selected()
if not path:
return
# scroll to file in details window
mark = self._buffer.get_mark(model[path][2])
self.textview.scroll_to_mark(mark, 0.0, True, 0.0, 0.0)
if model[path][3]:
self.curfile = fromutf(model[path][1])
else:
self.curfile = None
def _file_button_release(self, widget, event):
if event.button == 3 and not (event.state & (gtk.gdk.SHIFT_MASK |
gtk.gdk.CONTROL_MASK)):
self._file_popup_menu(widget, event.button, event.time)
return False
def _file_popup_menu(self, treeview, button=0, time=0):
if self.curfile is None:
return
self._filemenu.popup(None, None, None, button, time)
# If the filelog entry this changeset references does not link
# back to this changeset, it means this changeset did not
# actually change the contents of this file, and thus the file
# cannot be annotated at this revision (since this changeset
# does not appear in the filelog)
ctx = self.repo.changectx(self.currev)
try:
fctx = ctx.filectx(self.curfile)
has_filelog = fctx.filelog().linkrev(fctx.filerev()) == ctx.rev()
except LookupError:
has_filelog = False
self._ann_menu.set_sensitive(has_filelog)
self._save_menu.set_sensitive(has_filelog)
return True
def thgdiff(self, treeview):
# Do not steal ctrl-d from changelog treeview
if not treeview.is_focus() and self.glog_parent:
w = self.glog_parent.get_focus()
if isinstance(w, gtk.TreeView):
w.emit('thg-diff')
return False
if self.curfile is None:
return False
self._diff_file('M', self.curfile)
def _file_row_act(self, tree, path, column) :
"""Default action is the first entry in the context menu
"""
self._filemenu.get_children()[0].activate()
return True
def _save_file_rev(self, menuitem):
wfile = util.localpath(self.curfile)
wfile, ext = os.path.splitext(os.path.basename(wfile))
filename = "%s@%d%s" % (wfile, self.currev, ext)
fd = NativeSaveFileDialogWrapper(Title = "Save file to",
InitialDir=self.cwd,
FileName=filename)
result = fd.run()
if result:
import Queue
q = Queue.Queue()
cpath = util.canonpath(self.repo.root, self.cwd, self.curfile)
hgcmd_toq(self.repo.root, q, 'cat', '--rev',
str(self.currev), '--output', result, cpath)
def _diff_to_local(self, menuitem):
if not self.curfile:
# ignore view events for the [Description] row
return
self.opts['rev'] = [str(self.currev), '.']
self._diff_file('M', self.curfile)
def _diff_file_rev(self, menuitem):
'User selected visual diff file revision from the file list context menu'
if not self.curfile:
# ignore view events for the [Description] row
return
self.opts['change'] = str(self.currev)
self._diff_file('M', self.curfile)
del self.opts['change']
def _view_file_rev(self, menuitem):
'''User selected view file revision from the file list context menu'''
if not self.curfile:
# ignore view events for the [Description] row
return
rev = self.currev
parents = self.parents
if len(parents) == 0:
parent = rev-1
else:
parent = parents[0]
pair = '%u:%u' % (parent, rev)
self._node1, self._node2 = cmdutil.revpair(self.repo, [pair])
self._view_file('M', self.curfile, force_left=False)
def _ann_file(self, menuitem):
'''User selected annotate file from the file list context menu'''
from datamine import DataMineDialog
rev = self.currev
dialog = DataMineDialog(self.ui, self.repo, self.cwd, [], {}, False)
dialog.display()
dialog.add_annotate_page(self.curfile, str(rev))
def _file_history(self, menuitem):
'''User selected file history from file list context menu'''
if self.glog_parent:
# If this changeset browser is embedded in glog, send
# send this event to the main app
opts = {'pats' : [self.curfile]}
self.glog_parent.custombutton.set_active(True)
self.glog_parent.reload_log(opts)
else:
# Else launch our own GLog instance
import history
dialog = history.GLog(self.ui, self.repo, self.cwd,
[self.repo.root], {}, False)
dialog.open_with_file(self.curfile)
dialog.display()
def _revert_file(self, menuitem):
'''User selected file revert from the file list context menu'''
rev = self.currev
dialog = Confirm('revert file to old revision', [], self,
'Revert %s to contents at revision %d?' % (self.curfile, rev))
if dialog.run() == gtk.RESPONSE_NO:
return
cmdline = ['hg', 'revert', '--verbose', '--rev', str(rev), self.curfile]
dlg = CmdDialog(cmdline)
dlg.run()
dlg.hide()
shell_notify([self.curfile])
|
Loading...