def get_body(self): 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) 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
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 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") # TODO: Add toggle for gmtime/localtime eob = buf.get_end_iter() date = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(ctx.date()[0])) 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) summary = pctx.description().splitlines()[0] summary = toutf(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) summary = cctx.description().splitlines()[0] summary = toutf(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, file, 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, file) 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(file), 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 if node1 == repo.changelog.parents(node2)[0]: filelist = ctx2.files() else: 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()) execf2 = ctx2.manifest().execf linkf2 = ctx2.manifest().linkf # 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 = repo.changelog.count() orig = f files = (f,) while crev > startrev: if f in files: try: src = getfilectx(f, c).renamed() except revlog.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 revlog.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'] 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 gitmode(x, l): return l and '120000' or (x and '100755' or '100644') 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(execf2(f), linkf2(f)) if f in copied: a = copied[f] omode = gitmode(man1.execf(a), man1.linkf(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.execf(f), man1.linkf(f)) header.append('deleted file mode %s\n' % mode) else: omode = gitmode(man1.execf(f), man1.linkf(f)) nmode = gitmode(execf2(f), linkf2(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 = "green" stats[0] += 1 elif l.startswith("-"): stats[1] += 1 tag = "red" elif l.startswith("@@"): tag = "blue" else: tag = "black" 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, iter): if event.type != gtk.gdk.BUTTON_RELEASE: return text = self.get_link_text(tag, widget, iter) 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, iter): """handle clicking on a link in a textview""" text_buffer = widget.get_buffer() beg = iter.copy() while not beg.begins_tag(tag): beg.backward_char() end = iter.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('_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)) self._file_diff_to_mark_menu = create_menu('_diff file to mark', self._diff_file_to_mark) self._file_diff_from_mark_menu = create_menu('diff file _from mark', self._diff_file_from_mark) _menu.append(self._file_diff_to_mark_menu) _menu.append(self._file_diff_from_mark_menu) _menu.show_all() return _menu def get_body(self): 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) 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 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, iter = sel.get_selected() if not iter: return # scroll to file in details window mark = self._buffer.get_mark(model[iter][2]) self.textview.scroll_to_mark(mark, 0.0, True, 0.0, 0.0) if model[iter][3]: self.curfile = fromutf(model[iter][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 if self.graphview: is_mark = self.graphview.get_mark_rev() is not None else: is_mark = False self._file_diff_to_mark_menu.set_sensitive(is_mark) self._file_diff_from_mark_menu.set_sensitive(is_mark) 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.filenode()) == ctx.rev() except revlog.LookupError: has_filelog = False self._ann_menu.set_sensitive(has_filelog) self._save_menu.set_sensitive(has_filelog) return True 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): file = util.localpath(self.curfile) file, ext = os.path.splitext(os.path.basename(file)) filename = "%s@%d%s" % (file, self.currev, ext) fd = NativeSaveFileDialogWrapper(Title = "Save file to", InitialDir=self.cwd, FileName=filename) result = fd.run() if result: import Queue import hglib q = Queue.Queue() cpath = util.canonpath(self.repo.root, self.cwd, self.curfile) hglib.hgcmd_toq(self.repo.root, q, 'cat', '--rev', str(self.currev), '--output', result, cpath) 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 _diff_file_to_mark(self, menuitem): '''User selected diff to mark from the file list context menu''' from status import GStatus from gtools import cmdtable rev0 = self.graphview.get_mark_rev() rev1 = self.currev statopts = self.merge_opts(cmdtable['gstatus|gst'][1], ('include', 'exclude', 'git')) statopts['rev'] = ['%u:%u' % (rev1, rev0)] statopts['modified'] = True statopts['added'] = True statopts['removed'] = True dialog = GStatus(self.ui, self.repo, self.cwd, [self.curfile], statopts, False) dialog.display() return True def _diff_file_from_mark(self, menuitem): '''User selected diff from mark from the file list context menu''' from status import GStatus from gtools import cmdtable rev0 = self.graphview.get_mark_rev() rev1 = self.currev statopts = self.merge_opts(cmdtable['gstatus|gst'][1], ('include', 'exclude', 'git')) statopts['rev'] = ['%u:%u' % (rev0, rev1)] statopts['modified'] = True statopts['added'] = True statopts['removed'] = True dialog = GStatus(self.ui, self.repo, self.cwd, [self.curfile], statopts, False) dialog.display() def _ann_file(self, menuitem): '''User selected diff from mark 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 = {'filehist' : self.curfile} self.glog_parent.custombutton.set_active(True) self.glog_parent.graphview.refresh(True, None, opts) else: # Else launch our own GLog instance from history import GLog dialog = 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])