/tortoisehg/hgtk/status.py
Python | 1336 lines | 1290 code | 19 blank | 27 comment | 21 complexity | e1756b0e35d3121a518aef24b2ac322f MD5 | raw file
Possible License(s): GPL-2.0
- # status.py - status dialog for TortoiseHg
- #
- # Copyright 2007 Brad Schick, brad at gmail . com
- # Copyright 2007 TK Soh <teekaysoh@gmail.com>
- # Copyright 2008 Steve Borho <steve@borho.org>
- # Copyright 2008 Emmanuel Rosa <goaway1000@gmail.com>
- #
- # This software may be used and distributed according to the terms of the
- # GNU General Public License version 2, incorporated herein by reference.
- import os
- import cStringIO
- import gtk
- import gobject
- import threading
- from mercurial import cmdutil, util, patch, error, hg
- from mercurial import merge as merge_, filemerge
- from tortoisehg.util.i18n import _
- from tortoisehg.util import hglib, paths, hgshelve
- from tortoisehg.hgtk import dialog, gdialog, gtklib, guess, hgignore, statusbar, statusact
- from tortoisehg.hgtk import chunks
- # file model row enumerations
- FM_CHECKED = 0
- FM_STATUS = 1
- FM_PATH_UTF8 = 2
- FM_PATH = 3
- FM_MERGE_STATUS = 4
- FM_PARTIAL_SELECTED = 5
- class GStatus(gdialog.GWindow):
- """GTK+ based dialog for displaying repository status
- Also provides related operations like add, delete, remove, revert, refresh,
- ignore, diff, and edit.
- The following methods are meant to be overridden by subclasses. At this
- point GCommit is really the only intended subclass.
- auto_check(self)
- """
- ### Following methods are meant to be overridden by subclasses ###
- def init(self):
- gdialog.GWindow.init(self)
- self.mode = 'status'
- self.ready = False
- self.status = ([],) * 7
- self.status_error = None
- self.preview_tab_name_label = None
- self.subrepos = []
- self.colorstyle = self.repo.ui.config('tortoisehg', 'diffcolorstyle')
- self.act = statusact.statusact(self)
- def auto_check(self):
- # Only auto-check files once, and only if a pattern was given.
- if self.pats and self.opts.get('check'):
- for entry in self.filemodel:
- if entry[FM_PATH] not in self.excludes:
- entry[FM_CHECKED] = True
- self.update_check_count()
- self.opts['check'] = False
- def get_custom_menus(self):
- return []
- ### End of overridable methods ###
- ### Overrides of base class methods ###
- def parse_opts(self):
- # Disable refresh while we toggle checkboxes
- self.ready = False
- # Determine which files to display
- if self.test_opt('all'):
- for check in self._show_checks.values():
- check.set_active(True)
- else:
- for opt in self.opts:
- if opt in self._show_checks and self.opts[opt]:
- self._show_checks[opt].set_active(True)
- self.ready = True
- def get_title(self):
- root = self.get_reponame()
- revs = self.opts.get('rev')
- name = self.pats and _('filtered status') or _('status')
- r = revs and ':'.join(revs) or ''
- return root + ' - ' + ' '.join([name, r])
- def get_icon(self):
- return 'menushowchanged.ico'
- def get_defsize(self):
- return self._setting_defsize
- def get_tbbuttons(self):
- tbuttons = []
- if self.count_revs() == 2:
- tbuttons += [
- self.make_toolbutton(gtk.STOCK_SAVE_AS, _('Save As'),
- self.save_clicked, tip=_('Save selected changes'))]
- else:
- tbuttons += [
- self.make_toolbutton(gtk.STOCK_JUSTIFY_FILL, _('_Diff'),
- self.diff_clicked, name='diff',
- tip=_('Visual diff checked files')),
- self.make_toolbutton(gtk.STOCK_MEDIA_REWIND, _('Re_vert'),
- self.revert_clicked, name='revert',
- tip=_('Revert checked files')),
- self.make_toolbutton(gtk.STOCK_ADD, _('_Add'),
- self.add_clicked, name='add',
- tip=_('Add checked files')),
- self.make_toolbutton(gtk.STOCK_JUMP_TO, _('Move'),
- self.move_clicked, name='move',
- tip=_('Move checked files to other directory')),
- self.make_toolbutton(gtk.STOCK_DELETE, _('_Remove'),
- self.remove_clicked, name='remove',
- tip=_('Remove or delete checked files')),
- self.make_toolbutton(gtk.STOCK_CLEAR, _('_Forget'),
- self.forget_clicked, name='forget',
- tip=_('Forget checked files on next commit')),
- gtk.SeparatorToolItem(),
- self.make_toolbutton(gtk.STOCK_REFRESH, _('Re_fresh'),
- self.refresh_clicked,
- tip=_('refresh')),
- gtk.SeparatorToolItem()]
- return tbuttons
- def save_settings(self):
- settings = gdialog.GWindow.save_settings(self)
- settings['gstatus-hpane'] = self.diffpane.get_position()
- settings['gstatus-lastpos'] = self.setting_lastpos
- settings['gstatus-type-expander'] = self.types_expander.get_expanded()
- return settings
- def load_settings(self, settings):
- gdialog.GWindow.load_settings(self, settings)
- self.setting_pos = 270
- self.setting_lastpos = 64000
- self.setting_types_expanded = False
- try:
- self.setting_pos = settings['gstatus-hpane']
- self.setting_lastpos = settings['gstatus-lastpos']
- self.setting_types_expanded = settings['gstatus-type-expander']
- except KeyError:
- pass
- self.mqmode, repo = None, self.repo
- if hasattr(repo, 'mq') and repo.mq.applied and repo['.'] == repo['qtip']:
- self.mqmode = True
- def is_merge(self):
- try:
- numparents = len(self.repo.parents())
- except error.Abort, e:
- self.stbar.set_text(str(e) + _(', please refresh'))
- numparents = 1
- return self.count_revs() < 2 and numparents == 2
- def get_accelgroup(self):
- accelgroup = gtk.AccelGroup()
- mod = gtklib.get_thg_modifier()
-
- gtklib.add_accelerator(self.filetree, 'thg-diff', accelgroup, mod+'d')
- self.filetree.connect('thg-diff', self.thgdiff)
- self.connect('thg-refresh', self.thgrefresh)
- # set CTRL-c accelerator for copy-clipboard
- gtklib.add_accelerator(self.chunks.difftree(), 'copy-clipboard', accelgroup, mod+'c')
- def scroll_diff_notebook(widget, direction=gtk.SCROLL_PAGE_DOWN):
- page_num = self.diff_notebook.get_current_page()
- page = self.diff_notebook.get_nth_page(page_num)
- page.emit("scroll-child", direction, False)
- def toggle_filetree_selection(*arguments):
- self.sel_clicked(not self.selcb.get_active())
- def next_diff_notebook_page(*arguments):
- notebook = self.diff_notebook
- if notebook.get_current_page() >= len(notebook) - 1:
- notebook.set_current_page(0)
- else:
- notebook.next_page()
-
- def previous_diff_notebook_page(*arguments):
- notebook = self.diff_notebook
- if notebook.get_current_page() <= 0:
- notebook.set_current_page(len(notebook) - 1)
- else:
- notebook.prev_page()
-
- # signal, accelerator key, handler, (parameters)
- status_accelerators = [
- ('status-scroll-down', 'bracketright', scroll_diff_notebook,
- (gtk.SCROLL_PAGE_DOWN,)),
- ('status-scroll-up', 'bracketleft', scroll_diff_notebook,
- (gtk.SCROLL_PAGE_UP,)),
- ('status-next-file', 'period', gtklib.move_treeview_selection,
- (self.filetree, 1)),
- ('status-previous-file', 'comma', gtklib.move_treeview_selection,
- (self.filetree, -1)),
- ('status-select-all', 'u', toggle_filetree_selection, ()),
- ('status-next-page', 'p', next_diff_notebook_page, ()),
- ('status-previous-page', '<Shift>p',
- previous_diff_notebook_page, ()),
- ]
-
- for signal, accelerator, handler, parameters in status_accelerators:
- gtklib.add_accelerator(self, signal, accelgroup,
- mod + accelerator)
- self.connect(signal, handler, *parameters)
- return accelgroup
-
- def get_body(self):
- is_merge = self.is_merge()
- # model stores the file list.
- fm = gtk.ListStore(
- bool, # FM_CHECKED
- str, # FM_STATUS
- str, # FM_PATH_UTF8
- str, # FM_PATH
- str, # FM_MERGE_STATUS
- bool # FM_PARTIAL_SELECTED
- )
- fm.set_sort_func(1001, self.sort_by_stat)
- fm.set_default_sort_func(self.sort_by_stat)
- self.filemodel = fm
- self.filetree = gtk.TreeView(self.filemodel)
- self.filetree.connect('popup-menu', self.tree_popup_menu)
- self.filetree.connect('button-press-event', self.tree_button_press)
- self.filetree.connect('button-release-event', self.tree_button_release)
- self.filetree.connect('row-activated', self.tree_row_act)
- self.filetree.connect('key-press-event', self.tree_key_press)
- self.filetree.set_reorderable(False)
- self.filetree.set_enable_search(True)
- self.filetree.set_search_equal_func(self.search_filelist)
- if hasattr(self.filetree, 'set_rubber_banding'):
- self.filetree.set_rubber_banding(True)
- self.filetree.modify_font(self.fonts['list'])
- self.filetree.set_headers_clickable(True)
- toggle_cell = gtk.CellRendererToggle()
- toggle_cell.connect('toggled', self.select_toggle)
- toggle_cell.set_property('activatable', True)
- path_cell = gtk.CellRendererText()
- stat_cell = gtk.CellRendererText()
- # file selection checkboxes
- col0 = gtk.TreeViewColumn('', toggle_cell)
- col0.set_visible(not is_merge) # hide when merging
- col0.add_attribute(toggle_cell, 'active', FM_CHECKED)
- col0.add_attribute(toggle_cell, 'radio', FM_PARTIAL_SELECTED)
- col0.set_resizable(False)
- self.filetree.append_column(col0)
- self.selcb = self.add_header_checkbox(col0, self.sel_clicked)
- self.file_sel_column = col0
- col1 = gtk.TreeViewColumn(_('st'), stat_cell)
- col1.add_attribute(stat_cell, 'text', FM_STATUS)
- col1.set_cell_data_func(stat_cell, self.text_color)
- col1.set_sort_column_id(1001)
- col1.set_resizable(False)
- self.filetree.append_column(col1)
- # merge status column
- col = gtk.TreeViewColumn(_('ms'), stat_cell)
- col.set_visible(self.count_revs() <= 1)
- col.add_attribute(stat_cell, 'text', FM_MERGE_STATUS)
- col.set_sort_column_id(4)
- col.set_resizable(False)
- self.filetree.append_column(col)
- self.merge_state_column = col
- col2 = gtk.TreeViewColumn(_('path'), path_cell)
- col2.add_attribute(path_cell, 'text', FM_PATH_UTF8)
- col2.set_cell_data_func(path_cell, self.text_color)
- col2.set_sort_column_id(2)
- col2.set_resizable(True)
- self.filetree.append_column(col2)
- scroller = gtk.ScrolledWindow()
- scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
- scroller.add(self.filetree)
- # Status Types expander
- # We don't assign an expander child. We instead monitor the
- # expanded property and do the hiding ourselves
- expander = gtk.Expander(_('View'))
- self.types_expander = expander
- expander.connect("notify::expanded", self.types_expander_expanded)
- exp_labelbox = gtk.HBox()
- exp_labelbox.pack_start(expander, False, False)
- exp_labelbox.pack_start(gtk.Label(), True, True)
- self.counter = gtk.Label('')
- exp_labelbox.pack_end(self.counter, False, False, 2)
- self.status_types = self.get_status_types()
- if self.setting_types_expanded:
- expander.set_expanded(True)
- self.status_types.show()
- else:
- self.status_types.hide()
- expander_box = gtk.VBox()
- expander_box.pack_start(exp_labelbox)
- expander_box.pack_start(self.status_types)
- tvbox = gtk.VBox()
- tvbox.pack_start(scroller, True, True, 0)
- tvbox.pack_start(gtk.HSeparator(), False, False)
- tvbox.pack_start(expander_box, False, False)
- if self.pats:
- button = gtk.Button(_('Remove filter, show root'))
- button.connect('pressed', self.remove_filter)
- tvbox.pack_start( button, False, False, 2)
- tree_frame = gtk.Frame()
- tree_frame.set_shadow_type(gtk.SHADOW_ETCHED_IN)
- tree_frame.add(tvbox)
- diff_frame = gtk.Frame()
- diff_frame.set_shadow_type(gtk.SHADOW_ETCHED_IN)
- self.diff_notebook = gtk.Notebook()
- self.diff_notebook.set_tab_pos(gtk.POS_BOTTOM)
- self.diff_notebook_pages = {}
- self.difffont = self.fonts['diff']
- self.clipboard = None
- self.diff_text = gtk.TextView()
- self.diff_text.set_wrap_mode(gtk.WRAP_NONE)
- self.diff_text.set_editable(False)
- self.diff_text.modify_font(self.difffont)
- scroller = gtk.ScrolledWindow()
- scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
- scroller.add(self.diff_text)
- self.append_page('text-diff', scroller, gtk.Label(_('Text Diff')))
- # use treeview to show selectable diff hunks
- self.clipboard = gtk.Clipboard()
- # create chunks object
- self.chunks = chunks.chunks(self)
- scroller = gtk.ScrolledWindow()
- scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
- scroller.add(self.chunks.difftree())
- self.append_page('hunk-selection', scroller, gtk.Label(_('Hunk Selection')))
- # Add a page for commit preview
- self.preview_text = gtk.TextView()
- self.preview_text.set_wrap_mode(gtk.WRAP_NONE)
- self.preview_text.set_editable(False)
- self.preview_text.modify_font(self.difffont)
- scroller = gtk.ScrolledWindow()
- scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
- scroller.add(self.preview_text)
- self.preview_tab_name_label = gtk.Label(self.get_preview_tab_name())
- self.append_page('commit-preview', scroller,
- self.preview_tab_name_label)
- diff_frame.add(self.diff_notebook)
- if self.diffbottom:
- self.diffpane = gtk.VPaned()
- else:
- self.diffpane = gtk.HPaned()
- self.diffpane.pack1(tree_frame, shrink=False)
- self.diffpane.pack2(diff_frame, shrink=False)
- self.filetree.set_headers_clickable(True)
- sel = self.filetree.get_selection()
- sel.set_mode(gtk.SELECTION_MULTIPLE)
- self.treeselid = sel.connect('changed', self.tree_sel_changed)
- self.diff_notebook.connect('switch-page', self.page_switched, sel)
- # add keyboard accelerators
- accelgroup = self.get_accelgroup()
- self.add_accel_group(accelgroup)
-
- return self.diffpane
- def append_page(self, name, child, label):
- num = self.diff_notebook.append_page(child, label)
- self.diff_notebook_pages[num] = name
- def page_switched(self, notebook, page, page_num, filesel):
- self.tree_sel_changed(filesel, page_num)
- def get_extras(self):
- self.stbar = statusbar.StatusBar()
- return self.stbar
- def add_header_checkbox(self, col, post=None, pre=None, toggle=False):
- def cbclick(hdr, cb):
- state = cb.get_active()
- if pre:
- pre(state)
- if toggle:
- cb.set_active(not state)
- if post:
- post(not state)
- cb = gtk.CheckButton(col.get_title())
- cb.show()
- col.set_widget(cb)
- wgt = cb.get_parent()
- while wgt:
- if type(wgt) == gtk.Button:
- wgt.connect('clicked', cbclick, cb)
- return cb
- wgt = wgt.get_parent()
- return
- def update_check_count(self):
- file_count = 0
- check_count = 0
- for row in self.filemodel:
- file_count = file_count + 1
- if row[FM_CHECKED]:
- check_count = check_count + 1
- self.counter.set_text(_('%d selected, %d total') % (check_count,
- file_count))
- if self.selcb:
- self.selcb.set_active(file_count and file_count == check_count)
- if self.count_revs() == 2:
- return
- sensitive = check_count and not self.is_merge()
- for cmd in ('diff', 'revert', 'add', 'remove', 'move', 'forget'):
- self.cmd_set_sensitive(cmd, sensitive)
- if self.diff_notebook.get_current_page() == 2:
- self.update_commit_preview()
- def prepare_display(self):
- val = self.repo.ui.config('tortoisehg', 'ciexclude', '')
- self.excludes = [i.strip() for i in val.split(',') if i.strip()]
- gtklib.idle_add_single_call(self.realize_status_settings)
- def refresh_complete(self):
- pass
- def get_preview_tab_name(self):
- if self.count_revs() == 2:
- res = _('Save Preview')
- elif self.mqmode:
- res = _('Patch Preview')
- elif self.mode == 'shelve':
- res = _('Shelf Preview')
- else:
- res = _('Commit Preview')
- return res
- ### End of overrides ###
- def set_preview_tab_name(self, name=None):
- if self.preview_tab_name_label == None:
- return
- if name == None:
- name = self.get_preview_tab_name()
- self.preview_tab_name_label.set_text(name)
- def types_expander_expanded(self, expander, dummy):
- if expander.get_expanded():
- self.status_types.show()
- else:
- self.status_types.hide()
- def get_status_types(self):
- # Tuple: (onmerge, ctype, translated label)
- allchecks = [(False, 'unknown', _('?: unknown')),
- (True, 'modified', _('M: modified')),
- (False, 'ignored', _('I: ignored')),
- (True, 'added', _('A: added')),
- (False, 'clean', _('C: clean')),
- (True, 'removed', _('R: removed')),
- (False, 'deleted', _('!: deleted')),
- (True, 'subrepo', _('S: subrepo'))]
- checks = []
- nomerge = (self.count_revs() <= 1)
- for onmerge, button, text in allchecks:
- if onmerge or nomerge:
- checks.append((button, text))
- table = gtk.Table(rows=2, columns=3)
- table.set_col_spacings(8)
- self._show_checks = {}
- row, col = 0, 0
- for name, labeltext in checks:
- button = gtk.CheckButton(labeltext)
- widget = button
- button.connect('toggled', self.show_toggle, name)
- self._show_checks[name] = button
- table.attach(widget, col, col+1, row, row+1)
- col += row
- row = not row
- hbox = gtk.HBox()
- hbox.pack_start(table, False, False)
- return hbox
- def realize_status_settings(self):
- if not self.types_expander.get_expanded():
- self.status_types.hide()
- self.diffpane.set_position(self.setting_pos)
- try:
- tab = self.ui.config('tortoisehg', 'statustab', '0')
- tab = int(tab)
- self.diff_notebook.set_current_page(tab)
- except (error.ConfigError, ValueError):
- pass
- self.reload_status()
- def remove_filter(self, button):
- button.hide()
- self.pats = []
- for name, check in self._show_checks.iteritems():
- check.set_sensitive(True)
- self.set_title(self.get_title())
- self.reload_status()
- def search_filelist(self, model, column, key, iter):
- 'case insensitive filename search'
- key = key.lower()
- if key in model.get_value(iter, FM_PATH).lower():
- return False
- return True
- def thgdiff(self, treeview):
- selection = treeview.get_selection()
- model, tpaths = selection.get_selected_rows()
- files = [model[p][FM_PATH] for p in tpaths]
- self._do_diff(files, self.opts)
- def thgrefresh(self, window):
- self.reload_status()
- def refresh_file_tree(self):
- """Clear out the existing ListStore model and reload it from the
- repository status. Also recheck and reselect files that remain
- in the list.
- """
- is_merge = self.is_merge()
- self.file_sel_column.set_visible(not is_merge)
- self.merge_state_column.set_visible(self.count_revs() <= 1)
- selection = self.filetree.get_selection()
- if selection is None:
- return
- (M, A, R, D, U, I, C) = self.status
- changetypes = (('M', 'modified', M),
- ('A', 'added', A),
- ('R', 'removed', R),
- ('!', 'deleted', D),
- ('?', 'unknown', U),
- ('I', 'ignored', I),
- ('C', 'clean', C))
- # List of the currently checked and selected files to pass on to
- # the new data
- model, tpaths = selection.get_selected_rows()
- model = self.filemodel
- reselect = [model[path][FM_PATH] for path in tpaths]
- waschecked = {}
- for row in model:
- waschecked[row[FM_PATH]] = row[FM_CHECKED], row[FM_PARTIAL_SELECTED]
- # merge-state of files
- ms = merge_.mergestate(self.repo)
- # Load the new data into the tree's model
- self.filetree.hide()
- selection.handler_block(self.treeselid)
- self.filemodel.clear()
- types = [ct for ct in changetypes if self.opts.get(ct[1])]
- for stat, _, wfiles in types:
- for wfile in wfiles:
- mst = wfile in ms and ms[wfile].upper() or ""
- lfile = util.localpath(wfile)
- defcheck = stat in 'MAR' and lfile not in self.excludes
- ck, p = waschecked.get(lfile, (defcheck, False))
- model.append([ck, stat, hglib.toutf(lfile), lfile, mst, p])
- if self.test_opt('subrepo') or self.is_merge():
- for sdir in self.subrepos:
- lfile = util.localpath(sdir)
- defcheck = lfile not in self.excludes
- ck, p = waschecked.get(lfile, (defcheck, False))
- model.append([ck, 'S', hglib.toutf(lfile), lfile, '', p])
- self.auto_check() # may check more files
- for row in model:
- if row[FM_PARTIAL_SELECTED]:
- # force refresh of partially selected files
- self.chunks.update_hunk_model(row[FM_PATH], row[FM_CHECKED])
- self.chunks.clear()
- else:
- # demand refresh of full or non selection
- self.chunks.del_file(row[FM_PATH])
- # recover selections
- firstrow = None
- for i, row in enumerate(model):
- if row[FM_PATH] in reselect:
- if firstrow is None:
- firstrow = i
- else:
- selection.select_iter(row.iter)
- selection.handler_unblock(self.treeselid)
- if len(model):
- selection.select_path((firstrow or 0,))
- else:
- # clear diff pane if no files
- self.diff_text.set_buffer(gtk.TextBuffer())
- self.preview_text.set_buffer(gtk.TextBuffer())
- if not is_merge:
- self.chunks.clear()
- self.filetree.show()
- if self.mode == 'commit':
- self.text.grab_focus()
- else:
- self.filetree.grab_focus()
- return True
- def reload_status(self):
- if not self.ready: return False
- def get_repo_status():
- # Create a new repo object
- repo = hg.repository(self.ui, path=self.repo.root)
- self.newrepo = repo
- self.subrepos = []
- try:
- if self.mqmode and self.mode != 'status':
- # when a patch is applied, show diffs to parent of top patch
- qtip = repo['.']
- n1 = qtip.parents()[0].node()
- n2 = None
- else:
- # node2 is None (working dir) when 0 or 1 rev is specified
- n1, n2 = cmdutil.revpair(repo, self.opts.get('rev'))
- except (util.Abort, error.RepoError), e:
- self.status_error = str(e)
- return
- self._node1, self._node2 = n1, n2
- self.status_error = None
- matcher = cmdutil.match(repo, self.pats, self.opts)
- unknown = self.test_opt('unknown') and not self.is_merge()
- clean = self.test_opt('clean') and not self.is_merge()
- ignored = self.test_opt('ignored') and not self.is_merge()
- try:
- status = repo.status(node1=n1, node2=n2, match=matcher,
- ignored=ignored,
- clean=clean,
- unknown=unknown)
- self.status = status
- except (OSError, IOError, util.Abort), e:
- self.status_error = str(e)
- if n2 is not None or self.mqmode:
- return
- wctx = repo[None]
- try:
- for s in wctx.substate:
- if matcher(s) and wctx.sub(s).dirty():
- self.subrepos.append(s)
- except (error.ConfigError, error.RepoError), e:
- self.status_error = str(e)
- except (OSError, IOError, util.Abort), e:
- self.status_error = str(e)
- def status_wait(thread):
- if thread.isAlive():
- return True
- else:
- if self.status_error:
- self.ready = True
- self.update_check_count()
- self.stbar.end()
- self.stbar.set_text(self.status_error)
- return False
- self.repo = self.newrepo
- self.ui = self.repo.ui
- self.refresh_file_tree()
- self.update_check_count()
- self.refresh_complete()
- self.ready = True
- self.stbar.end()
- return False
- self.set_preview_tab_name()
- repo = self.repo
- hglib.invalidaterepo(repo)
- if hasattr(repo, 'mq'):
- self.mqmode = repo.mq.applied and repo['.'] == repo['qtip']
- self.set_title(self.get_title())
- self.ready = False
- self.stbar.begin()
- thread = threading.Thread(target=get_repo_status)
- thread.setDaemon(True)
- thread.start()
- gobject.timeout_add(50, status_wait, thread)
- return True
- def nodes(self):
- return (self._node1, self._node2)
- def get_ctx(self):
- 'Return current changectx or workingctx'
- if self._node2 == None and not self.mqmode:
- return self.repo[None]
- else:
- return self.repo[self._node1]
- def set_file_states(self, paths, state=True):
- for p in paths:
- self.filemodel[p][FM_CHECKED] = state
- self.update_chunk_state(self.filemodel[p])
- self.update_check_count()
- def select_toggle(self, cellrenderer, path):
- 'User manually toggled file status via checkbox'
- self.filemodel[path][FM_CHECKED] = not self.filemodel[path][FM_CHECKED]
- self.update_chunk_state(self.filemodel[path])
- self.update_check_count()
- return True
- def update_chunk_state(self, fileentry):
- 'Update chunk toggle state to match file toggle state'
- fileentry[FM_PARTIAL_SELECTED] = False
- wfile = fileentry[FM_PATH]
- selected = fileentry[FM_CHECKED]
- self.chunks.update_chunk_state(wfile, selected)
- def updated_codes(self):
- types = [('modified', 'M'),
- ('added', 'A'),
- ('removed', 'R'),
- ('unknown', '?'),
- ('deleted', '!'),
- ('ignored', 'I'),
- ('clean', 'C') ]
- codes = ''
- try:
- for name, code in types:
- if self.opts[name]:
- codes += code
- except KeyError:
- pass
- self.types_expander.set_label(_("View '%s'") % codes)
- def show_toggle(self, check, toggletype):
- self.opts[toggletype] = check.get_active()
- self.reload_status()
- self.updated_codes()
- return True
- def sort_by_stat(self, model, iter1, iter2):
- order = 'MAR!?SIC'
- lhs, rhs = (model.get_value(iter1, FM_STATUS),
- model.get_value(iter2, FM_STATUS))
- # GTK+ bug that calls sort before a full row is inserted causing
- # values to be None. When this happens, just return any value
- # since the call is irrelevant and will be followed by another
- # with the correct (non-None) value
- if None in (lhs, rhs):
- return 0
- result = order.find(lhs) - order.find(rhs)
- return min(max(result, -1), 1)
- def text_color(self, column, text_renderer, model, row_iter):
- stat = model[row_iter][FM_STATUS]
- if stat == 'M':
- text_renderer.set_property('foreground', gtklib.DBLUE)
- elif stat == 'A':
- text_renderer.set_property('foreground', gtklib.DGREEN)
- elif stat == 'R':
- text_renderer.set_property('foreground', gtklib.DRED)
- elif stat == 'C':
- text_renderer.set_property('foreground', gtklib.NORMAL)
- elif stat == '!':
- text_renderer.set_property('foreground', gtklib.RED)
- elif stat == '?':
- text_renderer.set_property('foreground', gtklib.DORANGE)
- elif stat == 'I':
- text_renderer.set_property('foreground', gtklib.DGRAY)
- else:
- text_renderer.set_property('foreground', gtklib.NORMAL)
- def tree_sel_changed(self, selection, page_num=None):
- 'Selection changed in file tree'
- # page_num may be supplied, if called from switch-page event
- model, paths = selection.get_selected_rows()
- if not paths:
- return
- row = paths[0]
- # desensitize the text diff and hunk selection tabs
- # if a non-MAR file is selected
- status = model[row][FM_STATUS]
- enable = (status in 'MAR')
- self.enable_page('text-diff', enable)
- self.enable_page('hunk-selection', enable and not self.is_merge())
- if page_num is None:
- page_num = self.diff_notebook.get_current_page()
- pname = self.get_page_name(page_num)
- if pname == 'text-diff':
- buf = self.generate_text_diffs(row)
- self.diff_text.set_buffer(buf)
- elif pname == 'hunk-selection':
- fmrow = self.filemodel[row]
- self.chunks.update_hunk_model(fmrow[FM_PATH], fmrow[FM_CHECKED])
- if not self.is_merge() and self.chunks.len():
- self.chunks.difftree().scroll_to_cell(0, use_align=True, row_align=0.0)
- elif pname == 'commit-preview':
- self.update_commit_preview()
- def get_page_name(self, num):
- try:
- return self.diff_notebook_pages[num]
- except KeyError:
- return ''
- def enable_page(self, name, enable):
- for pnum in self.diff_notebook_pages:
- pname = self.get_page_name(pnum)
- if pname == name:
- child = self.diff_notebook.get_nth_page(pnum)
- if child:
- child.set_sensitive(enable)
- lb = self.diff_notebook.get_tab_label(child)
- lb.set_sensitive(enable)
- return
- def update_commit_preview(self):
- if self.is_merge():
- opts = patch.diffopts(self.ui, self.opts)
- opts.git = True
- wctx = self.repo[None]
- pctx1, pctx2 = wctx.parents()
- difftext = [_('===== Diff to first parent %d:%s =====\n') % (
- pctx1.rev(), str(pctx1))]
- try:
- for s in patch.diff(self.repo, pctx1.node(), None, opts=opts):
- difftext.extend(s.splitlines(True))
- difftext.append(_('\n===== Diff to second parent %d:%s =====\n') % (
- pctx2.rev(), str(pctx2)))
- for s in patch.diff(self.repo, pctx2.node(), None, opts=opts):
- difftext.extend(s.splitlines(True))
- except (IOError, error.RepoError, error.LookupError, util.Abort), e:
- self.stbar.set_text(str(e))
- else:
- buf = cStringIO.StringIO()
- for row in self.filemodel:
- if not row[FM_CHECKED]:
- continue
- wfile = row[FM_PATH]
- chunks = self.chunks.get_chunks(wfile)
- for i, chunk in enumerate(chunks):
- if i == 0:
- chunk.write(buf)
- elif chunk.active:
- chunk.write(buf)
- difftext = buf.getvalue().splitlines(True)
- self.preview_text.set_buffer(self.diff_highlight_buffer(difftext))
- def diff_highlight_buffer(self, difftext):
- buf = gtk.TextBuffer()
- if self.colorstyle == 'background':
- buf.create_tag('removed', paragraph_background=gtklib.PRED)
- buf.create_tag('added', paragraph_background=gtklib.PGREEN)
- elif self.colorstyle == 'none':
- buf.create_tag('removed')
- buf.create_tag('added')
- else:
- buf.create_tag('removed', foreground=gtklib.DRED)
- buf.create_tag('added', foreground=gtklib.DGREEN)
- buf.create_tag('position', foreground=gtklib.DORANGE)
- buf.create_tag('header', foreground=gtklib.DBLUE)
- bufiter = buf.get_start_iter()
- for line in difftext:
- line = hglib.toutf(line)
- if line.startswith('---') or line.startswith('+++'):
- buf.insert_with_tags_by_name(bufiter, line, 'header')
- elif line.startswith('-'):
- line = hglib.diffexpand(line)
- buf.insert_with_tags_by_name(bufiter, line, 'removed')
- elif line.startswith('+'):
- line = hglib.diffexpand(line)
- buf.insert_with_tags_by_name(bufiter, line, 'added')
- elif line.startswith('@@'):
- buf.insert_with_tags_by_name(bufiter, line, 'position')
- else:
- line = hglib.diffexpand(line)
- buf.insert(bufiter, line)
- return buf
- def generate_text_diffs(self, row):
- wfile = self.filemodel[row][FM_PATH]
- pfile = util.pconvert(wfile)
- lines = chunks.check_max_diff(self.get_ctx(), pfile)
- if lines:
- return self.diff_highlight_buffer(lines)
- matcher = cmdutil.matchfiles(self.repo, [pfile])
- opts = patch.diffopts(self.ui, self.opts)
- opts.git = True
- difftext = []
- if self.is_merge():
- wctx = self.repo[None]
- pctx1, pctx2 = wctx.parents()
- difftext = [_('===== Diff to first parent %d:%s =====\n') % (
- pctx1.rev(), str(pctx1))]
- try:
- for s in patch.diff(self.repo, pctx1.node(), None,
- match=matcher, opts=opts):
- difftext.extend(s.splitlines(True))
- difftext.append(_('\n===== Diff to second parent %d:%s =====\n') % (
- pctx2.rev(), str(pctx2)))
- for s in patch.diff(self.repo, pctx2.node(), None,
- match=matcher, opts=opts):
- difftext.extend(s.splitlines(True))
- except (IOError, error.RepoError, error.LookupError, util.Abort), e:
- self.stbar.set_text(str(e))
- else:
- try:
- for s in patch.diff(self.repo, self._node1, self._node2,
- match=matcher, opts=opts):
- difftext.extend(s.splitlines(True))
- except (IOError, error.RepoError, error.LookupError, util.Abort), e:
- self.stbar.set_text(str(e))
- return self.diff_highlight_buffer(difftext)
- def update_check_state(self, wfile, partial, newvalue):
- for fr in self.filemodel:
- if fr[FM_PATH] == wfile:
- if fr[FM_PARTIAL_SELECTED] != partial:
- fr[FM_PARTIAL_SELECTED] = partial
- if fr[FM_CHECKED] != newvalue:
- fr[FM_CHECKED] = newvalue
- self.update_check_count()
- return
- def get_checked(self, wfile):
- for fr in self.filemodel:
- if fr[FM_PATH] == wfile:
- return fr[FM_CHECKED]
- return False
- def refresh_clicked(self, toolbutton, data=None):
- self.reload_status()
- return True
- def save_clicked(self, toolbutton, data=None):
- 'Write selected diff hunks to a patch file'
- revrange = self.opts.get('rev')[0]
- filename = "%s.patch" % revrange.replace(':', '_to_')
- result = gtklib.NativeSaveFileDialogWrapper(title=_('Save patch to'),
- initial=self.repo.root,
- filename=filename).run()
- if not result:
- return
- buf = cStringIO.StringIO()
- files = []
- for row in self.filemodel:
- if not row[FM_CHECKED]:
- continue
- files.append(row[FM_PATH])
- self.chunks.save(files, result)
- def diff_clicked(self, toolbutton, data=None):
- diff_list = self.relevant_checked_files('MAR!')
- if len(diff_list) > 0:
- self._do_diff(diff_list, self.opts)
- else:
- gdialog.Prompt(_('Nothing Diffed'),
- _('No diffable files selected'), self).run()
- return True
- def revert_clicked(self, toolbutton, data=None):
- revert_list = self.relevant_checked_files('MAR!')
- if len(revert_list) > 0:
- self.act.hg_revert(revert_list)
- else:
- gdialog.Prompt(_('Nothing Reverted'),
- _('No revertable files selected'), self).run()
- return True
- def add_clicked(self, toolbutton, data=None):
- add_list = self.relevant_checked_files('?IR')
- if len(add_list) > 0:
- self.act.hg_add(add_list)
- else:
- gdialog.Prompt(_('Nothing Added'),
- _('No addable files selected'), self).run()
- return True
- def remove_clicked(self, toolbutton, data=None):
- remove_list = self.relevant_checked_files('C!')
- delete_list = self.relevant_checked_files('?I')
- if len(remove_list) > 0:
- self.act.hg_remove(remove_list)
- if len(delete_list) > 0:
- self.act.delete_files(delete_list)
- if not remove_list and not delete_list:
- gdialog.Prompt(_('Nothing Removed'),
- _('No removable files selected'), self).run()
- return True
- def move_clicked(self, toolbutton, data=None):
- move_list = self.relevant_checked_files('C')
- if move_list:
- # get destination directory to files into
- dlg = gtklib.NativeFolderSelectDialog(
- title=_('Move files to directory...'),
- initial=self.repo.root)
- destdir = dlg.run()
- if not destdir:
- return True
- # verify directory
- destroot = paths.find_root(destdir)
- if destroot != self.repo.root:
- gdialog.Prompt(_('Nothing Moved'),
- _('Cannot move outside repo!'), self).run()
- return True
- # move the files to dest directory
- move_list.append(hglib.fromutf(destdir))
- self.act.hg_move(move_list)
- else:
- gdialog.Prompt(_('Nothing Moved'), _('No movable files selected\n\n'
- 'Note: only clean files can be moved.'), self).run()
- return True
- def forget_clicked(self, toolbutton, data=None):
- forget_list = self.relevant_checked_files('CM')
- if len(forget_list) > 0:
- self.act.hg_forget(forget_list)
- else:
- gdialog.Prompt(_('Nothing Forgotten'),
- _('No clean files selected'), self).run()
- def ignoremask_updated(self):
- '''User has changed the ignore mask in hgignore dialog'''
- self.opts['check'] = True
- self.reload_status()
- def relevant_checked_files(self, stats):
- return [item[FM_PATH] for item in self.filemodel \
- if item[FM_CHECKED] and item[FM_STATUS] in stats]
- def sel_clicked(self, state):
- 'selection header checkbox clicked'
- for entry in self.filemodel:
- if entry[FM_CHECKED] != state:
- entry[FM_CHECKED] = state
- self.update_chunk_state(entry)
- self.update_check_count()
- def tree_button_press(self, treeview, event):
- '''Selection management for filetree right-click
- If the user right-clicks on a currently-selected item in the
- filetree, preserve their entire existing selection for the popup menu.
- http://www.daa.com.au/pipermail/pygtk/2005-June/010465.html
- '''
- if event.button != 3:
- return False
- clicked_row = treeview.get_path_at_pos(int(event.x),
- int(event.y))
- if not clicked_row:
- return False
- selection = treeview.get_selection()
- selected_rows = selection.get_selected_rows()[1]
- # If they didn't right-click on a currently selected row,
- # change the selection
- if clicked_row[0] not in selected_rows:
- selection.unselect_all()
- selection.select_path(clicked_row[0])
- return True
- def tree_button_release(self, treeview, event):
- if event.button != 3:
- return False
- self.tree_popup_menu(treeview)
- return True
- def tree_popup_menu(self, treeview):
- model, tpaths = treeview.get_selection().get_selected_rows()
- types = {'M':[], 'A':[], 'R':[], '!':[], 'I':[], '?':[], 'C':[],
- 'r':[], 'u':[], 'S':[]}
- all = []
- pathmap = {}
- for p in tpaths:
- row = model[p]
- file = util.pconvert(row[FM_PATH])
- ms = row[FM_MERGE_STATUS]
- if ms == 'R':
- types['r'].append(file)
- elif ms == 'U':
- types['u'].append(file)
- else:
- types[row[FM_STATUS]].append(file)
- all.append(file)
- pathmap[file] = p
- def make(label, func, stats, icon=None, sens=True, paths=False):
- files = []
- for t in stats:
- files.extend(types[t])
- if not files:
- return
- args = [files]
- if paths:
- p = [pathmap[f] for f in files]
- args.append(p)
- item = menu.append(label, func, icon, args=args, sensitive=sens)
- return files
- def vdiff(menuitem, files):
- self._do_diff(files, self.opts)
- def viewmissing(menuitem, files):
- self._view_files(files, True)
- def edit(menuitem, files):
- self._view_files(files, False)
- def other(menuitem, files):
- self._view_files(files, True)
- def revert(menuitem, files):
- self.act.hg_revert(files)
- def remove(menuitem, files):
- self.act.hg_remove(files)
- def log(menuitem, files):
- from tortoisehg.hgtk import history
- dlg = history.run(self.ui, canonpats=files)
- dlg.display()
- def annotate(menuitem, files):
- from tortoisehg.hgtk import datamine
- dlg = datamine.run(self.ui, *files)
- dlg.display()
- def forget(menuitem, files, paths):
- self.act.hg_forget(files)
- self.set_file_states(paths, state=False)
- def add(menuitem, files, paths):
- self.act.hg_add(files)
- self.set_file_states(paths, state=True)
- def delete(menuitem, files):
- self.act.delete_files(files)
- def unmark(menuitem, files):
- ms = merge_.mergestate(self.repo)
- for wfile in files:
- ms.mark(wfile, "u")
- ms.commit()
- self.reload_status()
- def mark(menuitem, files):
- ms = merge_.mergestate(self.repo)
- for wfile in files:
- ms.mark(wfile, "r")
- ms.commit()
- self.reload_status()
- def resolve(stat, files):
- wctx = self.repo[None]
- mctx = wctx.parents()[-1]
- ms = merge_.mergestate(self.repo)
- for wfile in files:
- ms.resolve(wfile, wctx, mctx)
- ms.commit()
- self.reload_status()
- def resolve_with(stat, tool, files):
- if tool:
- exe = filemerge._findtool(self.repo.ui, tool)
- oldmergeenv = os.environ.get('HGMERGE')
- os.environ['HGMERGE'] = exe
- resolve(stat, files)
- if tool:
- if oldmergeenv:
- os.environ['HGMERGE'] = oldmergeenv
- else:
- del os.environ['HGMERGE']
- def rename(menuitem, files):
- self.act.rename_file(files[0])
- def copy(menuitem, files):
- self.act.copy_file(files[0])
- def guess_rename(menuitem, files):
- dlg = guess.DetectRenameDialog()
- dlg.show_all()
- dlg.set_notify_func(self.ignoremask_updated)
- def ignore(menuitem, files):
- dlg = hgignore.HgIgnoreDialog(files[0])
- dlg.show_all()
- dlg.set_notify_func(self.ignoremask_updated)
- menu = gtklib.MenuBuilder()
- make(_('_Visual Diff'), vdiff, 'MAR!ru', gtk.STOCK_JUSTIFY_FILL)
- make(_('Edit'), edit, 'MACI?ru', gtk.STOCK_EDIT)
- make(_('View missing'), viewmissing, 'R!')
- make(_('View other'), other, 'MAru', None, self.is_merge())
- menu.append_sep()
- make(_('_Revert'), revert, 'MAR!ru', gtk.STOCK_MEDIA_REWIND)
- make(_('_Add'), add, 'R', gtk.STOCK_ADD, paths=True)
- menu.append_sep()
- make(_('File History'), log, 'MARC!ru', 'menulog.ico')
- make(_('Annotate'), annotate, 'MARC!ru', 'menublame.ico')
- menu.append_sep()
- make(_('_Forget'), forget, 'MAC!ru', gtk.STOCK_CLEAR, paths=True)
- make(_('_Add'), add, 'I?', gtk.STOCK_ADD, paths=True)
- make(_('_Guess Rename...'), guess_rename, 'A?!', 'detect_rename.ico')
- make(_('_Ignore'), ignore, '?', 'ignore.ico')
- make(_('Remove versioned'), remove, 'C', 'menudelete.ico')
- make(_('_Delete unversioned'), delete, '?I', gtk.STOCK_DELETE)
- if len(all) == 1:
- menu.append_sep()
- make(_('_Copy...'), copy, 'MC', gtk.STOCK_COPY)
- make(_('Rename...'), rename, 'MC', 'general.ico')
- menu.append_sep()
- f = make(_('Restart Merge...'), resolve, 'u', 'menumerge.ico')
- make(_('Mark unresolved'), unmark, 'r', gtk.STOCK_NO)
- make(_('Mark resolved'), mark, 'u', gtk.STOCK_YES)
- if f:
- rmenu = gtk.Menu()
- for tool in hglib.mergetools(self.repo.ui):
- item = gtk.MenuItem(tool, True)
- item.connect('activate', resolve_with, tool, f)
- item.set_border_width(1)
- rmenu.append(item)
- menu.append_submenu(_('Restart merge with'), rmenu,
- 'menumerge.ico')
- for label, func, stats, icon in self.get_custom_menus():
- make(label, func, stats, icon)
- menu = menu.build()
- if len(menu.get_children()) > 0:
- menu.show_all()
- menu.popup(None, None, None, 0, 0)
- return True
- def tree_key_press(self, tree, event):
- 'Make spacebar toggle selected rows'
- if event.keyval == 32:
- def toggler(model, path, bufiter):
- model[path][FM_CHECKED] = not model[path][FM_CHECKED]
- self.update_chunk_state(model[path])
- selection = self.filetree.get_selection()
- selection.selected_foreach(toggler)
- self.update_check_count()
- return True
- return False
- def tree_row_act(self, tree, path, column):
- 'Activation (return) triggers visual diff of selected rows'
- # Ignore activations (like double click) on the first column
- if column.get_sort_column_id() == 0:
- return True
- model, tpaths = self.filetree.get_selection().get_selected_rows()
- files = [model[p][FM_PATH] for p in tpaths]
- self._do_diff(files, self.opts)
- return True
- def isuptodate(self):
- oldparents = self.repo.dirstate.parents()
- self.repo.dirstate.invalidate()
- if oldparents == self.repo.dirstate.parents():
- return True
- response = gdialog.CustomPrompt(_('not up to date'),
- _('The parents have changed since the last refresh.\n'
- 'Continue anyway?'),
- self, (_('&Yes'), _('&Refresh'), _('&Cancel')), 1, 2).run()
- if response == 0: # Yes
- return True
- if response == 1:
- self.reload_status()
- return False
- def run(ui, *pats, **opts):
- showclean = util.any(os.path.isfile(e) for e in pats)
- rev = opts.get('rev', [])
- cmdoptions = {
- 'all':False, 'clean':showclean, 'ignored':False, 'modified':True,
- 'added':True, 'removed':True, 'deleted':True, 'unknown':True,
- 'exclude':[], 'include':[], 'debug':True, 'verbose':True, 'git':False,
- 'rev':rev, 'check':True, 'subrepo':True
- }
- return GStatus(ui, None, None, pats, cmdoptions)