Mercurial and Git clients can push and pull from this alias URL to interact with this repository. You can change to which repository an alias points by going to the Aliases link on the project page.
## status.py - status dialog for TortoiseHg## Copyright 2007 Brad Schick, brad at gmail . com# Copyright (C) 2007-8 TK Soh <teekaysoh@gmail.com># Copyright (C) 2008-9 Steve Borho <steve@borho.org>#importosimportcStringIOimportgtkimportgobjectimportpangofrommercurialimportcmdutil,util,commands,patch,mdifffrommercurialimportmergeasmerge_fromthgutil.i18nimport_fromthgutilimporthglib,shlib,pathsfromhggtkimportdialog,gdialog,hgshelve,gtklib,guess,hgignore# file model row enumerationsFM_CHECKED=0FM_STATUS=1FM_PATH_UTF8=2FM_PATH=3FM_MERGE_STATUS=4FM_PARTIAL_SELECTED=5# diffmodel row enumerationsDM_REJECTED=0DM_DISP_TEXT=1DM_IS_HEADER=2DM_PATH=3DM_CHUNK_ID=4DM_FONT=5defhunk_markup(text):'Format a diff hunk for display in a TreeView row with markup'hunk=""lines=text.splitlines()forlineinlines:line=gobject.markup_escape_text(hglib.toutf(line[:128]))ifline[-1]!='\n':line+='\n'ifline.startswith('---')orline.startswith('+++'):hunk+='<span foreground="#000090">%s</span>'%lineelifline.startswith('-'):hunk+='<span foreground="#900000">%s</span>'%lineelifline.startswith('+'):hunk+='<span foreground="#006400">%s</span>'%lineelifline.startswith('@@'):hunk='<span foreground="#FF8000">%s</span>'%lineelse:hunk+=linereturnhunkdefhunk_unmarkup(text):'Format a diff hunk for display in a TreeView row without markup'hunk=""lines=text.splitlines()forlineinlines:line=gobject.markup_escape_text(hglib.toutf(line[:128]))ifline[-1]!='\n':line+='\n'hunk+=linereturnhunkclassGStatus(gdialog.GDialog):"""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) get_menu_info(self) """### Following methods are meant to be overridden by subclasses ###definit(self):gdialog.GDialog.init(self)self.mode='status'self.ready=Trueself.filerowstart={}self.filechunks={}defauto_check(self):ifself.patsorself.opts.get('check'):forentryinself.filemodel:entry[FM_CHECKED]=Trueself.update_check_count()defget_menu_info(self):"""Returns menu info in this order: merge, addrem, unknown, clean, ignored, deleted, unresolved, resolved """return(# merge((_('_difference'),self._diff_file),(_('edit'),self._view_file),(_('view other'),self.view_left_file),(_('_revert'),self.revert_file),(_('l_og'),self.log_file)),# addrem((_('_difference'),self._diff_file),(_('_view'),self._view_file),(_('_revert'),self.revert_file),(_('l_og'),self.log_file)),# unknown((_('_view'),self._view_file),(_('_delete'),self.delete_file),(_('_add'),self.add_file),(_('_guess rename'),self.guess_rename),(_('_ignore'),self.ignore_file)),# clean((_('_view'),self._view_file),(_('re_move'),self.remove_file),(_('re_name'),self.rename_file),(_('_copy'),self.copy_file),(_('l_og'),self.log_file)),# ignored((_('_view'),self._view_file),(_('_delete'),self.delete_file)),# deleted((_('_view'),self._view_file),(_('_revert'),self.revert_file),(_('re_move'),self.remove_file),(_('l_og'),self.log_file)),# unresolved((_('_difference'),self._diff_file),(_('edit'),self._view_file),(_('view other'),self.view_left_file),(_('_revert'),self.revert_file),(_('l_og'),self.log_file),(_('resolve'),self.do_resolve),(_('mark resolved'),self.mark_resolved)),# resolved((_('_difference'),self._diff_file),(_('edit'),self._view_file),(_('view other'),self.view_left_file),(_('_revert'),self.revert_file),(_('l_og'),self.log_file),(_('mark unresolved'),self.unmark_resolved)),)### End of overridable methods ###### Overrides of base class methods ###defparse_opts(self):self.ready=False# Determine which files to displayifself.test_opt('all'):forcheckinself._show_checks.values():check.set_active(True)else:wasset=Falseforoptinself.opts:ifoptinself._show_checksandself.opts[opt]:wasset=Trueself._show_checks[opt].set_active(True)ifnotwasset:forcheckin[item[1]foriteminself._show_checks.iteritems()ifitem[0]in('modified','added','removed','deleted','unknown')]:check.set_active(True)ifself.pats:forname,checkinself._show_checks.iteritems():check.set_sensitive(False)defget_title(self):root=hglib.toutf(os.path.basename(self.repo.root))revs=self.opts.get('rev')name=self.patsand_('filtered status')or_('status')r=revsand':'.join(revs)or''return' '.join([root,name,r])defget_icon(self):return'menushowchanged.ico'defget_defsize(self):returnself._setting_defsizedefget_tbbuttons(self):tbuttons=[self.make_toolbutton(gtk.STOCK_REFRESH,_('Re_fresh'),self.refresh_clicked,tip=_('refresh')),gtk.SeparatorToolItem()]ifself.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_MEDIA_REWIND,_('Re_vert'),self.revert_clicked,tip=_('revert')),self.make_toolbutton(gtk.STOCK_ADD,_('_Add'),self.add_clicked,tip=_('add')),self.make_toolbutton(gtk.STOCK_JUMP_TO,_('Move'),self.move_clicked,tip=_('move selected files to other directory')),self.make_toolbutton(gtk.STOCK_DELETE,_('_Remove'),self.remove_clicked,tip=_('remove')),gtk.SeparatorToolItem()]returntbuttonsdefsave_settings(self):settings=gdialog.GDialog.save_settings(self)settings['gstatus-hpane']=self._diffpane.get_position()settings['gstatus-lastpos']=self._setting_lastposreturnsettingsdefload_settings(self,settings):gdialog.GDialog.load_settings(self,settings)self._setting_pos=270self._setting_lastpos=64000try:self._setting_pos=settings['gstatus-hpane']self._setting_lastpos=settings['gstatus-lastpos']exceptKeyError:passself.mqmode=Noneifhasattr(self.repo,'mq')andself.repo.mq.applied:self.mqmode=Truedefget_body(self):self.merging=len(self.repo.parents())==2# TODO: should generate menus dynamically during right-click, currently# there can be entires that are not always supported or relavant.merge,addrem,unknown,clean,ignored,deleted,unresolved,resolved \
=self.get_menu_info()merge_menu=self.make_menu(merge)addrem_menu=self.make_menu(addrem)unknown_menu=self.make_menu(unknown)clean_menu=self.make_menu(clean)ignored_menu=self.make_menu(ignored)deleted_menu=self.make_menu(deleted)resolved_menu=self.make_menu(resolved)unresolved_menu=self.make_menu(unresolved)# Dictionary with a key of file-stat and values containing context-menusself._menus={}self._menus['M']=merge_menuself._menus['A']=addrem_menuself._menus['R']=addrem_menuself._menus['?']=unknown_menuself._menus['C']=clean_menuself._menus['I']=ignored_menuself._menus['!']=deleted_menuself._menus['MR']=resolved_menu self._menus['MU'] = unresolved_menu
# model stores the file list.
- self.filemodel = gtk.ListStore(bool, str, str, str, str, bool)
- self.filemodel.set_sort_func(1001, self.sort_by_stat)- self.filemodel.set_default_sort_func(self.sort_by_stat)+ self.filemodel = self.newfilemodel()
self.filetree = gtk.TreeView(self.filemodel)
self.filetree.connect('button-press-event', self.tree_button_press)
self.filetree.connect('button-release-event',self.tree_button_release)self.filetree.connect('popup-menu',self.tree_popup_menu)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)ifhasattr(self.filetree,'set_rubber_banding'):self.filetree.set_rubber_banding(True)self.filetree.modify_font(pango.FontDescription(self.fontlist))self.filetree.set_headers_clickable(True)accelgroup=gtk.AccelGroup()self.add_accel_group(accelgroup)mod=gtklib.get_thg_modifier()key,modifier=gtk.accelerator_parse(mod+'d')self.filetree.add_accelerator('thg-diff',accelgroup,key,modifier,gtk.ACCEL_VISIBLE)self.filetree.connect('thg-diff',self.thgdiff)self.connect('thg-refresh',self.thgrefresh)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()ifself.merging:self.selcb=Noneelse:# show file selection checkboxes only when applicablecol0=gtk.TreeViewColumn('',toggle_cell)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)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)ifself.merging:col=gtk.TreeViewColumn(_('ms'),stat_cell)col.add_attribute(stat_cell,'text',FM_MERGE_STATUS)col.set_sort_column_id(4)col.set_resizable(False)self.filetree.append_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)tree_frame=gtk.Frame()tree_frame.set_shadow_type(gtk.SHADOW_ETCHED_IN)tree_frame.add(scroller)diff_frame=gtk.Frame()diff_frame.set_shadow_type(gtk.SHADOW_ETCHED_IN)scroller=gtk.ScrolledWindow()scroller.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC)self.difffont=pango.FontDescription(self.fontlist)ifself.merging:# display merge diffs in simple text viewself.clipboard=Noneself.merge_diff_text=gtk.TextView()self.merge_diff_text.set_wrap_mode(gtk.WRAP_NONE)self.merge_diff_text.set_editable(False)self.merge_diff_text.modify_font(self.difffont)self.filetree.get_selection().set_mode(gtk.SELECTION_SINGLE)self.filetree.get_selection().connect('changed',self.merge_sel_changed)scroller.add(self.merge_diff_text)diff_frame.add(scroller)else:# use treeview to show selectable diff hunkssel=(os.name=='nt')and'CLIPBOARD'or'PRIMARY'self.clipboard=gtk.Clipboard(selection=sel)self.diffmodel=gtk.ListStore(bool,# DM_REJECTEDstr,# DM_DISP_TEXTbool,# DM_IS_HEADERstr,# DM_PATHint,# DM_CHUNK_IDpango.FontDescription)difftree=gtk.TreeView(self.diffmodel)# set CTRL-c accelerator for copy-clipboardmod=gtklib.get_thg_modifier()key,modifier=gtk.accelerator_parse(mod+'c')difftree.add_accelerator('copy-clipboard',accelgroup,key,modifier,gtk.ACCEL_VISIBLE)difftree.connect('copy-clipboard',self.copy_to_clipboard)difftree.get_selection().set_mode(gtk.SELECTION_MULTIPLE)difftree.set_headers_visible(False)difftree.set_enable_search(False)difftree.set_property('enable-grid-lines',True)difftree.connect('row-activated',self.diff_tree_row_act)cell=gtk.CellRendererText()diffcol=gtk.TreeViewColumn('diff',cell)diffcol.set_resizable(True)diffcol.add_attribute(cell,'markup',DM_DISP_TEXT)# differentiate header chunkscell.set_property('cell-background','#DDDDDD')diffcol.add_attribute(cell,'cell_background_set',DM_IS_HEADER)self.headerfont=self.difffont.copy()self.headerfont.set_weight(pango.WEIGHT_HEAVY)# differentiate rejected hunksself.rejfont=self.difffont.copy()self.rejfont.set_weight(pango.WEIGHT_LIGHT)diffcol.add_attribute(cell,'font-desc',DM_FONT)cell.set_property('background','#EEEEEE')cell.set_property('foreground','#888888')diffcol.add_attribute(cell,'background-set',DM_REJECTED)diffcol.add_attribute(cell,'foreground-set',DM_REJECTED)difftree.append_column(diffcol)self.filetree.get_selection().set_mode(gtk.SELECTION_MULTIPLE)self.filetree.get_selection().connect('changed',self.tree_sel_changed)scroller.add(difftree)diff_frame.add(scroller)ifself.diffbottom:self._diffpane=gtk.VPaned()else:self._diffpane=gtk.HPaned()self._diffpane.pack1(tree_frame,True,False)self._diffpane.pack2(diff_frame,True,True)self.filetree.set_headers_clickable(True)gobject.idle_add(self.realize_status_settings)returnself._diffpanedefget_extras(self):table=gtk.Table(rows=2,columns=3)table.set_col_spacings(8)self._show_checks={}row,col=0,0# Tuple: (ctype, translated label)checks=(('modified',_('modified')),('added',_('added')),('removed',_('removed')))ifself.count_revs()<=1:checks+=(('deleted',_('deleted')),('unknown',_('unknown')),('close',_('clean')),('ignored',_('ignored')))forctupleinchecks:check=gtk.CheckButton(ctuple[1])check.connect('toggled',self.show_toggle,ctuple[0])table.attach(check,col,col+1,row,row+1)self._show_checks[ctuple[0]]=checkcol+=rowrow=notrowself.counter=gtk.Label('')self.counter.set_alignment(1.0,0.0)# right uphbox=gtk.HBox()hbox.pack_start(table,expand=False)hbox.pack_end(self.counter,expand=True,padding=2)returnhboxdefadd_header_checkbox(self,col,post=None,pre=None,toggle=False):defcbclick(hdr,cb):state=cb.get_active()ifpre:pre(state)iftoggle:cb.set_active(notstate)ifpost:post(notstate)cb=gtk.CheckButton(col.get_title())cb.show()col.set_widget(cb)wgt=cb.get_parent()whilewgt:iftype(wgt)==gtk.Button:wgt.connect('clicked',cbclick,cb)returncbwgt=wgt.get_parent()returndefupdate_check_count(self):file_count=0check_count=0forrowinself.filemodel:file_count=file_count+1ifrow[FM_CHECKED]:check_count=check_count+1self.counter.set_text(_('%d selected, %d total')%(check_count,file_count))ifself.selcb:self.selcb.set_active(file_countandfile_count==check_count)defprepare_display(self):self.ready=True# If the status load failed, no reason to continueifnotself.reload_status():raiseutil.Abort('could not load status') ### End of overrides ###
+ def newfilemodel(self):+ fm = gtk.ListStore(bool, str, str, str, str, bool)+ fm.set_sort_func(1001, self.sort_by_stat)+ fm.set_default_sort_func(self.sort_by_stat)+ return fm+ def realize_status_settings(self):
self._diffpane.set_position(self._setting_pos)
defsearch_filelist(self,model,column,key,iter):'case insensitive filename search'key=key.lower()ifkeyinmodel.get_value(iter,FM_PATH).lower():returnFalsereturnTruedefthgdiff(self,treeview):selection=treeview.get_selection()model,tpaths=selection.get_selected_rows()row=model[tpaths[0]]self._diff_file(row[FM_STATUS],row[FM_PATH])defthgrefresh(self,window):self.reload_status()defcopy_to_clipboard(self,treeview):'Write highlighted hunks to the clipboard'ifnottreeview.is_focus():w=self.get_focus()w.emit('copy-clipboard')returnFalsesaves={}model,tpaths=treeview.get_selection().get_selected_rows()forrow,intpaths:wfile,cid=model[row][DM_PATH],model[row][DM_CHUNK_ID]ifwfilenotinsaves:saves[wfile]=[cid]else:saves[wfile].append(cid)fp=cStringIO.StringIO()forwfileinsaves.keys():chunks=self.filechunks[wfile]chunks[0].write(fp)forcidinsaves[wfile]:ifcid!=0:chunks[cid].write(fp)fp.seek(0)self.clipboard.set_text(fp.read())defdo_reload_status(self):"""Clear out the existing ListStore model and reload it from the repository status. Also recheck and reselect files that remain in the list. """selection=self.filetree.get_selection()ifselectionisNone:returnrepo=self.repohglib.invalidaterepo(repo)ifhasattr(repo,'mq'):self.mqmode=repo.mq.appliedself.set_title(self.get_title())ifself.mqmodeandself.mode!='status':# when a patch is applied, show diffs to parent of top patchqtip=repo['.']n1=qtip.parents()[0].node()n2=Noneelse:# node2 is None (the working dir) when 0 or 1 rev is specificedn1,n2=cmdutil.revpair(repo,self.opts.get('rev'))matcher=cmdutil.match(repo,self.pats,self.opts)status=repo.status(node1=n1,node2=n2,match=matcher,ignored=self.test_opt('ignored'),clean=self.test_opt('clean'),unknown=self.test_opt('unknown'))(modified,added,removed,deleted,unknown,ignored,clean)=statusself._node1,self._node2,self.modified=n1,n2,modifiedchangetypes=(('modified','M',modified),('added','A',added),('removed','R',removed),('deleted','!',deleted),('unknown','?',unknown),('ignored','I',ignored))explicit_changetypes=changetypes+(('clean','C',clean),)# List of the currently checked and selected files to pass on to# the new datamodel,tpaths=selection.get_selected_rows()recheck=[entry[FM_PATH]forentryinmodelifentry[FM_CHECKED]]reselect=[model[path][FM_PATH]forpathintpaths]# merge-state of filesms=merge_.mergestate(repo) # Load the new data into the tree's model
self.filetree.hide()
- self.filemodel.clear()
++ # issue 181 hack, create new model rather than clearing existing model+ model = self.newfilemodel()+ self.filemodel = model+ self.filetree.set_model(model)+ selection = self.filetree.get_selection()
for opt, char, changes in ([ct for ct in explicit_changetypes
if self.test_opt(ct[0])] or changetypes):
forwfileinchanges:mst=wfileinmsandms[wfile].upper()or""wfile=util.localpath(wfile)self.filemodel.append([wfileinrecheck,char,hglib.toutf(wfile),wfile,mst,False]) self.auto_check()
selected = False
- for row in model:
- if row[FM_PATH] in reselect:
- selection.select_iter(row.iter)
- selected = True
+ if len(reselect) < 100: # issue 181 hack+ for row in model:
+if row[FM_PATH] in reselect:
+ selection.select_iter(row.iter)
+selected = True
if not selected:
selection.select_path((0,))
# clear buffer after a merge commitifnotlen(self.filemodel):ifself.merging:self.merge_diff_text.set_buffer(gtk.TextBuffer())else:self.diffmodel.clear()self.filetree.show()ifself.mode=='commit':self.text.grab_focus()else:self.filetree.grab_focus()returnTruedefreload_status(self):ifnotself.ready:returnFalseres,outtext=self._hg_call_wrapper('Status',self.do_reload_status)self.update_check_count()returnresdefmake_menu(self,entries):menu=gtk.Menu()forentryinentries:menu.append(self.make_menuitem(entry[0],entry[1]))menu.show_all()returnmenudefmake_menuitem(self,label,handler):menuitem=gtk.MenuItem(label,True)menuitem.connect('activate',self.context_menu_act,handler)menuitem.set_border_width(1)returnmenuitemdefselect_toggle(self,cellrenderer,path):'User manually toggled file status via checkbox'self.filemodel[path][FM_CHECKED]=notself.filemodel[path][FM_CHECKED]self.update_chunk_state(self.filemodel[path])self.update_check_count()returnTruedefupdate_chunk_state(self,fileentry):'Update chunk toggle state to match file toggle state'fileentry[FM_PARTIAL_SELECTED]=Falsewfile=fileentry[FM_PATH]selected=fileentry[FM_CHECKED]ifwfilenotinself.filechunks:returnchunks=self.filechunks[wfile]forchunkinchunks:chunk.active=selected# this file's chunks may not be in diffmodelifwfilenotinself.filerowstart:returnrowstart=self.filerowstart[wfile]forn,chunkinenumerate(chunks):ifn==0:continueself.diffmodel[rowstart+n][DM_REJECTED]=notselectedself.update_diff_hunk(self.diffmodel[rowstart+n])self.update_diff_header(self.diffmodel,wfile,selected)defupdate_diff_hunk(self,row):'Update the contents of a diff row based on its chunk state'wfile=row[DM_PATH]chunks=self.filechunks[wfile]chunk=chunks[row[DM_CHUNK_ID]]buf=cStringIO.StringIO()chunk.pretty(buf)buf.seek(0)ifchunk.active:row[DM_REJECTED]=Falserow[DM_FONT]=self.difffontrow[DM_DISP_TEXT]=hunk_markup(buf.read())else:row[DM_REJECTED]=Truerow[DM_FONT]=self.rejfontrow[DM_DISP_TEXT]=hunk_unmarkup(buf.read())defupdate_diff_header(self,dmodel,wfile,selected):try:hc=self.filerowstart[wfile]chunks=self.filechunks[wfile]exceptIndexError:returnlasthunk=len(chunks)-1sel=lambdax:x>=lasthunkornotdmodel[hc+x+1][DM_REJECTED]newtext=chunks[0].selpretty(sel)ifnotselected:newtext="<span foreground='#888888'>"+newtext+"</span>"dmodel[hc][DM_DISP_TEXT]=newtextdefshow_toggle(self,check,toggletype):self.opts[toggletype]=check.get_active()self.reload_status()returnTruedefsort_by_stat(self,model,iter1,iter2):order='MAR!?IC'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) valueifNonein(lhs,rhs):return0result=order.find(lhs)-order.find(rhs)returnmin(max(result,-1),1)deftext_color(self,column,text_renderer,model,row_iter):stat=model[row_iter][FM_STATUS]ifstat=='M':text_renderer.set_property('foreground','#000090')elifstat=='A':text_renderer.set_property('foreground','#006400')elifstat=='R':text_renderer.set_property('foreground','#900000')elifstat=='C':text_renderer.set_property('foreground','black')elifstat=='!':text_renderer.set_property('foreground','red')elifstat=='?':text_renderer.set_property('foreground','#AA5000')elifstat=='I':text_renderer.set_property('foreground','black')else:text_renderer.set_property('foreground','black')defview_left_file(self,stat,wfile):returnself._view_file(stat,wfile,True)defremove_file(self,stat,wfile):self.hg_remove([wfile])returnTruedefrename_file(self,stat,wfile):fdir,fname=os.path.split(wfile)newfile=dialog.entry_dialog(self,_('Rename file to:'),True,fname)ifnewfileandnewfile!=fname:self.hg_move([wfile,os.path.join(fdir,newfile)])returnTruedefcopy_file(self,stat,wfile):wfile=self.repo.wjoin(wfile)fdir,fname=os.path.split(wfile)dlg=gtk.FileChooserDialog(parent=self,title=_('Copy file to'),action=gtk.FILE_CHOOSER_ACTION_SAVE,buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_COPY,gtk.RESPONSE_OK))dlg.set_default_response(gtk.RESPONSE_OK)dlg.set_current_folder(fdir)dlg.set_current_name(fname)response=dlg.run()newfile=wfileifresponse==gtk.RESPONSE_OK:newfile=dlg.get_filename()dlg.destroy()ifnewfile!=wfile:self.hg_copy([wfile,newfile])returnTruedefhg_remove(self,files):wfiles=[self.repo.wjoin(x)forxinfiles]ifself.count_revs()>1:gdialog.Prompt(_('Nothing Removed'),_('Remove is not enabled when multiple revisions are specified.'),self).run()return# Create new opts, so nothing unintented gets throughremoveopts=self.merge_opts(commands.table['^remove|rm'][1],('include','exclude'))defdohgremove():commands.remove(self.ui,self.repo,*wfiles,**removeopts)success,outtext=self._hg_call_wrapper('Remove',dohgremove)ifsuccess:shlib.update_thgstatus(self.ui,self.repo.root)self.reload_status()defhg_move(self,files):wfiles=[self.repo.wjoin(x)forxinfiles]ifself.count_revs()>1:gdialog.Prompt(_('Nothing Moved'),_('Move is not enabled when ''multiple revisions are specified.'),self).run()return# Create new opts, so nothing unintented gets throughmoveopts=self.merge_opts(commands.table['rename|mv'][1],('include','exclude'))defdohgmove():#moveopts['force'] = Truecommands.rename(self.ui,self.repo,*wfiles,**moveopts)success,outtext=self._hg_call_wrapper('Move',dohgmove)ifsuccess:shlib.update_thgstatus(self.ui,self.repo.root,wait=True)self.reload_status()defhg_copy(self,files):wfiles=[self.repo.wjoin(x)forxinfiles]ifself.count_revs()>1:gdialog.Prompt(_('Nothing Copied'),_('Copy is not enabled when ''multiple revisions are specified.'),self).run()return# Create new opts, so nothing unintented gets throughcmdopts=self.merge_opts(commands.table['copy|cp'][1],('include','exclude'))defdohgcopy():commands.copy(self.ui,self.repo,*wfiles,**cmdopts)success,outtext=self._hg_call_wrapper('Copy',dohgcopy)ifsuccess:shlib.update_thgstatus(self.ui,self.repo.root,wait=True)self.reload_status()defmerge_sel_changed(self,selection):'Selected row in file tree activated changed (merge mode)'# Update the diff text with merge diff to both parentsmodel,paths=selection.get_selected_rows()ifnotpaths:returnwfile=self.filemodel[paths[0]][FM_PATH]difftext=[_('===== Diff to first parent =====\n')]wfiles=[self.repo.wjoin(wfile)]wctx=self.repo[None]matcher=cmdutil.match(self.repo,wfiles,self.opts)forsinpatch.diff(self.repo,wctx.p1().node(),None,match=matcher,opts=patch.diffopts(self.ui,self.opts)):difftext.extend(s.splitlines(True))difftext.append(_('\n===== Diff to second parent =====\n'))forsinpatch.diff(self.repo,wctx.p2().node(),None,match=matcher,opts=patch.diffopts(self.ui,self.opts)):difftext.extend(s.splitlines(True))buf=gtk.TextBuffer()buf.create_tag('removed',foreground='#900000')buf.create_tag('added',foreground='#006400')buf.create_tag('position',foreground='#FF8000')buf.create_tag('header',foreground='#000090')bufiter=buf.get_start_iter()forlineindifftext:line=hglib.toutf(line)ifline.startswith('---')orline.startswith('+++'):buf.insert_with_tags_by_name(bufiter,line,'header')elifline.startswith('-'):line=hglib.diffexpand(line)buf.insert_with_tags_by_name(bufiter,line,'removed')elifline.startswith('+'):line=hglib.diffexpand(line)buf.insert_with_tags_by_name(bufiter,line,'added')elifline.startswith('@@'):buf.insert_with_tags_by_name(bufiter,line,'position')else:line=hglib.diffexpand(line)buf.insert(bufiter,line)self.merge_diff_text.set_buffer(buf)deftree_sel_changed(self,selection):'Selected row in file tree activated changed'# Read this file's diffs into diff modelmodel,paths=selection.get_selected_rows()ifnotpaths:returnwfile=self.filemodel[paths[0]][FM_PATH]# TODO: this could be a for-loopself.filerowstart={}self.diffmodel.clear()self.append_diff_hunks(wfile)defread_file_chunks(self,wfile):'Get diffs of working file, parse into (c)hunks'difftext=cStringIO.StringIO()ctx=self.repo[self._node1]try:fctx=ctx.filectx(wfile)excepthglib.LookupError:fctx=Noneiffctxandfctx.size()>hglib.getmaxdiffsize(self.ui):# Fake patch that displays size warninglines=['diff --git -r a/%s b/%s\n'%(wfile,wfile)]lines.append('--- a/%s\n'%wfile)lines.append('+++ b/%s\n'%wfile)lines.append(_('File is larger than the specified max diff size\n'))difftext.writelines(lines)else:wfiles=[self.repo.wjoin(wfile)]matcher=cmdutil.match(self.repo,wfiles,self.opts)diffopts=mdiff.diffopts(git=True,nodates=True)forsinpatch.diff(self.repo,self._node1,self._node2,match=matcher,opts=diffopts):difftext.writelines(s.splitlines(True))difftext.seek(0)returnhgshelve.parsepatch(difftext)defappend_diff_hunks(self,wfile):'Append diff hunks of one file to the diffmodel'chunks=self.read_file_chunks(wfile)ifnotchunks:ifwfileinself.filechunks:delself.filechunks[wfile]returnforfrinself.filemodel:iffr[FM_PATH]==wfile:breakelse:# should not be possiblereturnrows=[]forn,chunkinenumerate(chunks):ifisinstance(chunk,hgshelve.header):# header chunk is always activechunk.active=Truerows.append([False,'',True,wfile,n,self.headerfont])ifchunk.special():chunks=chunks[:1]breakelse:# chunks take file's selection state by defaultchunk.active=fr[FM_CHECKED]rows.append([False,'',False,wfile,n,self.difffont])# recover old chunk selection/rejection states, match fromlineifwfileinself.filechunks:ochunks=self.filechunks[wfile]next=1forocinochunks[1:]:forninxrange(next,len(chunks)):nc=chunks[n]ifoc.fromline==nc.fromline:nc.active=oc.activenext=n+1breakelifnc.fromline>oc.fromline:breakself.filerowstart[wfile]=len(self.diffmodel)self.filechunks[wfile]=chunks# Set row status based on chunk staterej,nonrej=False,Falseforn,rowinenumerate(rows):ifnotrow[DM_IS_HEADER]:ifchunks[n].active:nonrej=Trueelse:rej=Truerow[DM_REJECTED]=notchunks[n].activeself.update_diff_hunk(row)self.diffmodel.append(row)iflen(rows)==1:newvalue=fr[FM_CHECKED]else:newvalue=nonrejpartial=rejandnonrejiffr[FM_PARTIAL_SELECTED]!=partial:fr[FM_PARTIAL_SELECTED]=partialiffr[FM_CHECKED]!=newvalue:fr[FM_CHECKED]=newvalueself.update_check_count()self.update_diff_header(self.diffmodel,wfile,newvalue)defdiff_tree_row_act(self,dtree,path,column):'Row in diff tree (hunk) activated/toggled'dmodel=dtree.get_model()row=dmodel[path]wfile=row[DM_PATH]try:startrow=self.filerowstart[wfile]chunks=self.filechunks[wfile]exceptIndexError:passchunkrows=xrange(startrow+1,startrow+len(chunks))forfrinself.filemodel:iffr[FM_PATH]==wfile:breakifrow[DM_IS_HEADER]:forn,chunkinenumerate(chunks[1:]):chunk.active=notfr[FM_CHECKED]self.update_diff_hunk(dmodel[startrow+n+1])newvalue=notfr[FM_CHECKED]partial=Falseelse:chunk=chunks[row[DM_CHUNK_ID]]chunk.active=notchunk.activeself.update_diff_hunk(row)rej=[nforninchunkrowsifdmodel[n][DM_REJECTED]]nonrej=[nforninchunkrowsifnotdmodel[n][DM_REJECTED]]newvalue=nonrejandTrueorFalsepartial=rejandnonrejandTrueorFalse# Update file's check statusiffr[FM_PARTIAL_SELECTED]!=partial:fr[FM_PARTIAL_SELECTED]=partialiffr[FM_CHECKED]!=newvalue:fr[FM_CHECKED]=newvalueself.update_check_count()self.update_diff_header(dmodel,wfile,newvalue)defrefresh_clicked(self,toolbutton,data=None):self.reload_status()returnTruedefsave_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_')fd=gtklib.NativeSaveFileDialogWrapper(Title=_('Save patch to'),InitialDir=self.repo.root,FileName=filename)result=fd.run()ifnotresult:returnbuf=cStringIO.StringIO()dmodel=self.diffmodelforrowinself.filemodel:ifnotrow[FM_CHECKED]:continuewfile=row[FM_PATH]ifwfilenotinself.filechunks:continuechunks=self.filechunks[wfile]fori,chunkinenumerate(chunks):ifi==0:chunk.write(buf)elifchunk.active:chunk.write(buf)buf.seek(0)try:fp=open(result,"w")fp.write(buf.read())exceptOSError:passfinally:fp.close()defrevert_clicked(self,toolbutton,data=None):revert_list=self.relevant_files('MAR!')iflen(revert_list)>0:self.hg_revert(revert_list)else:gdialog.Prompt(_('Nothing Reverted'),_('No revertable files selected'),self).run()returnTruedefrevert_file(self,stat,wfile):self.hg_revert([wfile])returnTruedeflog_file(self,stat,wfile):# Might want to include 'rev' here... trying withoutfromhggtkimporthistorydlg=history.GLog(self.ui,self.repo,self.cwd,[wfile],self.opts)dlg.display()returnTruedefhg_revert(self,files):wfiles=[self.repo.wjoin(x)forxinfiles]ifself.count_revs()>1:gdialog.Prompt(_('Nothing Reverted'),_('Revert not allowed when viewing revision range.'),self).run()return# Create new opts, so nothing unintented gets through.key='^revert'incommands.tableand'^revert'or'revert'revertopts=self.merge_opts(commands.table[key][1],('include','exclude','rev'))defdohgrevert():commands.revert(self.ui,self.repo,*wfiles,**revertopts)ifself.count_revs()==1:# rev options needs extra tweaking since is not an array for# revert commandrevertopts['rev']=revertopts['rev'][0]dlg=gdialog.Confirm(_('Confirm Revert'),files,self,_('Revert files to revision ')+revertopts['rev']+'?')elifself.merging:resp=gdialog.CustomPrompt(_('Which parent to revert to?'),_('Revert file(s) to local or other parent?'),self,(_('&local'),_('&other')),_('l')).run()ifresp==ord(_('l')):revertopts['rev']=self.repo[None].p1().rev()elifresp==ord(_('o')):revertopts['rev']=self.repo[None].p2().rev()else:returndlg=Noneelse:# rev options needs extra tweaking since it must be an empty# string when unspecified for revert commandrevertopts['rev']=''dlg=gdialog.Confirm('Confirm Revert',files,self)ifnotdlgordlg.run()==gtk.RESPONSE_YES:success,outtext=self._hg_call_wrapper('Revert',dohgrevert)ifsuccess:shlib.update_thgstatus(self.ui,self.repo.root,wait=True)shlib.shell_notify(wfiles)self.reload_status()defadd_clicked(self,toolbutton,data=None):add_list=self.relevant_files('?I')iflen(add_list)>0:self.hg_add(add_list)else:gdialog.Prompt(_('Nothing Added'),_('No addable files selected'),self).run()returnTruedefadd_file(self,stat,wfile):self.hg_add([wfile])returnTruedefhg_add(self,files):wfiles=[self.repo.wjoin(x)forxinfiles]# Create new opts, so nothing unintented gets throughaddopts=self.merge_opts(commands.table['^add'][1],('include','exclude'))defdohgadd():commands.add(self.ui,self.repo,*wfiles,**addopts)success,outtext=self._hg_call_wrapper('Add',dohgadd)ifsuccess:shlib.update_thgstatus(self.ui,self.repo.root)shlib.shell_notify(wfiles)self.reload_status()defremove_clicked(self,toolbutton,data=None):remove_list=self.relevant_files('C!')delete_list=self.relevant_files('?I')iflen(remove_list)>0:self.hg_remove(remove_list)iflen(delete_list)>0:self.delete_files(delete_list)ifnotremove_listandnotdelete_list:gdialog.Prompt(_('Nothing Removed'),_('No removable files selected'),self).run()returnTruedefmove_clicked(self,toolbutton,data=None):move_list=self.relevant_files('C')ifmove_list:# get destination directory to files intodlg=gtk.FileChooserDialog(title=_('Move files to diretory...'),parent=self,action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK))dlg.set_default_response(gtk.RESPONSE_OK)dlg.set_current_folder(self.repo.root)response=dlg.run()destdir=dlg.get_filename()dlg.destroy()ifresponse!=gtk.RESPONSE_OK:returnTrue# verify directorydestroot=paths.find_root(destdir)ifdestroot!=self.repo.root:gdialog.Prompt(_('Nothing Moved'),_('Cannot move outside repo!'),self).run()returnTrue# move the files to dest directorymove_list.append(hglib.fromutf(destdir))self.hg_move(move_list)else:gdialog.Prompt(_('Nothing Moved'),_('No movable files selected\n\n''Note: only clean files can be moved.'),self).run()returnTruedefdelete_file(self,stat,wfile):self.delete_files([wfile])defdelete_files(self,files):dlg=gdialog.Confirm(_('Confirm Delete Unrevisioned'),files,self)ifdlg.run()==gtk.RESPONSE_YES:errors=''forwfileinfiles:try:os.unlink(self.repo.wjoin(wfile))exceptException,inst:errors+=str(inst)+'\n\n'iferrors:errors=errors.replace('\\\\','\\')iflen(errors)>500:errors=errors[:errors.find('\n',500)]+'\n...'gdialog.Prompt(_('Delete Errors'),errors,self).run()self.reload_status()returnTruedefguess_rename(self,stat,wfile):dlg=guess.DetectRenameDialog()dlg.show_all()dlg.set_notify_func(self.ignoremask_updated)defignore_file(self,stat,wfile):dlg=hgignore.HgIgnoreDialog(self.repo.root,util.pconvert(wfile))dlg.show_all()dlg.set_notify_func(self.ignoremask_updated)returnTruedefignoremask_updated(self):'''User has changed the ignore mask in hgignore dialog'''self.reload_status()defmark_resolved(self,stat,wfile):ms=merge_.mergestate(self.repo)ms.mark(util.pconvert(wfile),"r")self.reload_status()defunmark_resolved(self,stat,wfile):ms=merge_.mergestate(self.repo)ms.mark(util.pconvert(wfile),"u")self.reload_status()defdo_resolve(self,stat,wfile):ms=merge_.mergestate(self.repo)wctx=self.repo[None]mctx=wctx.parents()[-1]ms.resolve(util.pconvert(wfile),wctx,mctx)self.reload_status()defsel_clicked(self,state):self.select_files(state)returnTruedefselect_files(self,state,ctype=None):forentryinself.filemodel:ifctypeandnotentry[FM_STATUS]inctype:continueifentry[FM_CHECKED]!=state:entry[FM_CHECKED]=stateself.update_chunk_state(entry)self.update_check_count()defrelevant_files(self,stats):return[item[FM_PATH]foriteminself.filemodel \
ifitem[FM_CHECKED]anditem[FM_STATUS]instats]defcontext_menu_act(self,menuitem,handler):selection=self.filetree.get_selection()assert(selection.count_selected_rows()==1)model,tpaths=selection.get_selected_rows()path=tpaths[0]handler(model[path][FM_STATUS],model[path][FM_PATH])returnTruedeftree_button_press(self,widget,event):# Set the flag to ignore the next activation when the# shift/control keys are pressed. This avoids activations with# multiple rows selected.ifevent.type==gtk.gdk._2BUTTON_PRESSand \
(event.state&(gtk.gdk.SHIFT_MASK|gtk.gdk.CONTROL_MASK)):self._ignore_next_act=Trueelse:self._ignore_next_act=FalsereturnFalsedeftree_button_release(self,widget,event):ifevent.button!=3:returnFalseifnot(event.state&(gtk.gdk.SHIFT_MASK|gtk.gdk.CONTROL_MASK)):self.tree_popup_menu(widget,event.button,event.time)returnFalsedefget_file_context_menu(self,rowdata):st=rowdata[FM_STATUS]ms=rowdata[FM_MERGE_STATUS]ifms:menu=self._menus['M'+ms]else:menu=self._menus[st]returnmenudeftree_popup_menu(self,widget,button=0,time=0):selection=self.filetree.get_selection()ifselection.count_selected_rows()!=1:returnFalsemodel,tpaths=selection.get_selected_rows()menu=self.get_file_context_menu(model[tpaths[0]])menu.popup(None,None,None,button,time)returnTruedeftree_key_press(self,tree,event):ifevent.keyval==32:deftoggler(model,path,bufiter):model[path][FM_CHECKED]=notmodel[path][FM_CHECKED]self.update_chunk_state(model[path])selection=self.filetree.get_selection()selection.selected_foreach(toggler)self.update_check_count()returnTruereturnFalsedeftree_row_act(self,tree,path,column):"""Default action is the first entry in the context menu """# Ignore activations (like double click) on the first column,# and ignore all actions if the flag is setifcolumn.get_sort_column_id()==0orself._ignore_next_act:self._ignore_next_act=FalsereturnTrueselection=self.filetree.get_selection()ifselection.count_selected_rows()!=1:returnFalsemodel,tpaths=selection.get_selected_rows()menu=self.get_file_context_menu(model[tpaths[0]])menu.get_children()[0].activate()returnTruedefrun(ui,*pats,**opts):showclean=patsandTrueorFalserev=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}returnGStatus(ui,None,None,pats,cmdoptions)
Attach a Trello Card
Add a tag
Your session has expired
You are no longer logged in. Please log in and try your request again.
Filter RSS Feed
This RSS feed URL allows you to see the contents of your current filter using any feed reader.
This link includes a special authentication token. If you share the URL with anyone else, they can see this RSS feed's activity. You can disable these tokens when needed.
Your current filter is unsaved; changing it won't affect this RSS feed.