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.
# visdiff.py - launch external visual diff tools## Copyright 2009 Steve Borho <steve@borho.org>## This software may be used and distributed according to the terms of the# GNU General Public License version 2, incorporated herein by reference.importgtkimportgobjectimportosimportsubprocessimportstatimportshutilimportthreadingimporttempfileimportrefrommercurialimporthg,ui,cmdutil,util,error,match,copiesfromtortoisehg.util.i18nimport_fromtortoisehg.utilimporthglib,settings,pathsfromtortoisehg.hgtkimportgdialog,gtklibtry:importwin32conopenflags=win32con.CREATE_NO_WINDOWexceptImportError:openflags=0# Match parent2 first, so 'parent1?' will match both parent1 and parent_regex='\$(parent2|parent1?|child|plabel1|plabel2|clabel|ancestor|alabel)'_nonexistant=_('[non-existant]')defsnapshot(repo,files,ctx,tmproot):'''snapshot files as of some revision'''dirname=os.path.basename(repo.root)or'root'ifctx.rev()isnotNone:dirname='%s.%s'%(dirname,str(ctx))base=os.path.join(tmproot,dirname)os.mkdir(base)fns_and_mtime=[]forfninfiles:wfn=util.pconvert(fn)ifnotwfninctx:# File doesn't exist; could be a bogus modifycontinuedest=os.path.join(base,wfn)destdir=os.path.dirname(dest)ifnotos.path.isdir(destdir):os.makedirs(destdir)data=repo.wwritedata(wfn,ctx[wfn].data())f=open(dest,'wb')f.write(data)f.close()ifctx.rev()isNone:fns_and_mtime.append((dest,repo.wjoin(fn),os.path.getmtime(dest)))elifos.name!='nt':# Make file read/only, to indicate it's static (archival) natureos.chmod(dest,stat.S_IREAD)returnbase,fns_and_mtimedeflaunchtool(cmd,opts,replace,block):defquote(match):key=match.group()[1:]returnutil.shellquote(replace[key])args=' '.join(opts)args=re.sub(_regex,quote,args)cmdline=util.shellquote(cmd)+' '+argscmdline=util.quotecommand(cmdline)try:proc=subprocess.Popen(cmdline,shell=True,creationflags=openflags,stderr=subprocess.PIPE,stdout=subprocess.PIPE,stdin=subprocess.PIPE)ifblock:proc.communicate()except(OSError,EnvironmentError),e:gdialog.Prompt(_('Tool launch failure'),_('%s : %s')%(cmd,str(e)),None).run()deffilemerge(ui,fname,patchedfname):'Launch the preferred visual diff tool for two text files'detectedtools=hglib.difftools(ui)ifnotdetectedtools:gdialog.Prompt(_('No diff tool found'),_('No visual diff tools were detected'),None).run()returnNonepreferred=besttool(ui,detectedtools)diffcmd,diffopts,mergeopts=detectedtools[preferred]replace=dict(parent=fname,parent1=fname,plabel1=fname+_('[working copy]'),child=patchedfname,clabel=_('[original]'))launchtool(diffcmd,diffopts,replace,True)defbesttool(ui,tools):'Select preferred or highest priority tool from dictionary'preferred=ui.config('tortoisehg','vdiff')orui.config('ui','merge')ifpreferredandpreferredintools:returnpreferredpris=[]fortintools.keys():p=int(ui.config('merge-tools',t+'.priority',0))pris.append((-p,t))tools=sorted(pris)returntools[0][1]defvisualdiff(ui,repo,pats,opts):revs=opts.get('rev')change=opts.get('change')try:ctx1b=Noneifchange:ctx2=repo[change]p=ctx2.parents()iflen(p)>1:ctx1a,ctx1b=pelse:ctx1a=p[0]else:n1,n2=cmdutil.revpair(repo,revs)ctx1a,ctx2=repo[n1],repo[n2]p=ctx2.parents()ifnotrevsandlen(p)>1:ctx1b=p[1]except(error.LookupError,error.RepoError):gdialog.Prompt(_('Unable to find changeset'),_('You likely need to refresh this application'),None).run()returnNonelpats=[util.localpath(f)forfinpats]m=match.match(repo.root,repo.root,lpats)n2=ctx2.node()mod_a,add_a,rem_a=map(set,repo.status(ctx1a.node(),n2,m)[:3])ifctx1b:mod_b,add_b,rem_b=map(set,repo.status(ctx1b.node(),n2,m)[:3])cpy=copies.copies(repo,ctx1a,ctx1b,ctx1a.ancestor(ctx1b))[0]else:cpy=copies.copies(repo,ctx1a,ctx2,repo[-1])[0]mod_b,add_b,rem_b=set(),set(),set()MA=mod_a|add_a|mod_b|add_bMAR=MA|rem_a|rem_bifnotMAR:gdialog.Prompt(_('No file changes'),_('There are no file changes to view'),None).run()returnNonedetectedtools=hglib.difftools(repo.ui)ifnotdetectedtools:gdialog.Prompt(_('No diff tool found'),_('No visual diff tools were detected'),None).run()returnNonepreferred=besttool(repo.ui,detectedtools) # Build tool list based on diff-patterns matches
toollist = set()
- patterns = ui.configitems('diff-patterns')
+ patterns = repo.ui.configitems('diff-patterns')
patterns = [(p, t) for p,t in patterns if t in detectedtools]
for path in MAR:
for pat, tool in patterns:
mf=match.match(repo.root,'',[pat])ifmf(path):toollist.add(tool)breakelse:toollist.add(preferred)cto=cpy.keys()cfrom=cpy.values()forpathinMAR:ifpathinctoorpathincfrom:hascopies=Truebreakelse:hascopies=Falseiflen(toollist)>1orhascopies:usewin=Trueelse:preferred=toollist.pop()dirdiff=repo.ui.configbool('merge-tools',preferred+'.dirdiff')dir3diff=repo.ui.configbool('merge-tools',preferred+'.dir3diff')usewin=repo.ui.configbool('merge-tools',preferred+'.usewin')ifnotusewinandlen(MAR)>1:ifctx1bisnotNone:usewin=notdir3diffelse:usewin=notdirdiffifusewin:# Multiple required tools, or tool does not support directory diffssa=[mod_a,add_a,rem_a]sb=[mod_b,add_b,rem_b]dlg=FileSelectionDialog(repo,pats,ctx1a,sa,ctx1b,sb,ctx2,cpy)returndlg# We can directly use the selected tool, without a visual diff windowdiffcmd,diffopts,mergeopts=detectedtools[preferred]# Disable 3-way merge if there is only one parent or no tool supportdo3way=bool(mergeopts)andctx1bisnotNoneifdo3way:args=mergeoptselse:args=diffoptsdefdodiff(tmproot):fns_and_mtime=[]# Always make a copy of ctx1a (and ctx1b, if applicable)files=mod_a|rem_a|((mod_b|add_b)-add_a)dir1a=snapshot(repo,files,ctx1a,tmproot)[0]label1a='@%d'%ctx1a.rev()ifdo3way:files=mod_b|rem_b|((mod_a|add_a)-add_b)dir1b=snapshot(repo,files,ctx1b,tmproot)[0]label1b='@%d'%ctx1b.rev()# snapshot for ancestor revisionctxa=ctx1a.ancestor(ctx1b)ifctxa==ctx1a:dira=dir1aelifctxa==ctx1b:dira=dir1belse:dira=snapshot(repo,MAR,ctxa,tmproot)[0]labela='@%d'%ctxa.rev()else:dir1b,dira=None,Nonelabel1b,labela='',''ifctx2.rev()isnotNone:# If ctx2 is not the working copy, create a snapshot for itdir2=snapshot(repo,MA,ctx2,tmproot)[0]label2='@%d'%ctx2.rev()eliflen(MAR)==1:# This lets the diff tool open the changed file directlylabel2=''dir2=repo.rootelse:# Create a snapshot, record mtime to detect mods made by# diff tooldir2,fns_and_mtime=snapshot(repo,MA,ctx2,tmproot)label2='working files'defgetfile(fname,dir,label):file=os.path.join(tmproot,dir,fname)ifos.path.isfile(file):returnfname+label,filenullfile=os.path.join(tmproot,'empty')fp=open(nullfile,'w')fp.close()return_nonexistant+label,nullfile# If only one change, diff the files instead of the directories# Handle bogus modifies correctly by checking if the files existiflen(MAR)==1:lfile=util.localpath(MAR.pop())label1a,dir1a=getfile(lfile,dir1a,label1a)ifdo3way:label1b,dir1b=getfile(lfile,dir1b,label1b)labela,dira=getfile(lfile,dira,labela)label2,dir2=getfile(lfile,dir2,label2)ifdo3way:label1a+='[local]'label1b+='[other]'labela+='[ancestor]'label2+='[merged]'# Function to quote file/dir names in the argument stringreplace=dict(parent=dir1a,parent1=dir1a,parent2=dir1b,plabel1=label1a,plabel2=label1b,ancestor=dira,alabel=labela,clabel=label2,child=dir2)launchtool(diffcmd,args,replace,True)# detect if changes were made to mirrored working filesforcopy_fn,working_fn,mtimeinfns_and_mtime:ifos.path.getmtime(copy_fn)!=mtime:ui.debug('file changed while diffing. ''Overwriting: %s (src: %s)\n'%(working_fn,copy_fn))util.copyfile(copy_fn,working_fn)defdodiffwrapper():try:dodiff(tmproot)finally:ui.note(_('cleaning up temp directory\n'))shutil.rmtree(tmproot)tmproot=tempfile.mkdtemp(prefix='visualdiff.')ifopts.get('mainapp'):dodiffwrapper()else:# We are not the main application, so this must be done in a# background threadthread=threading.Thread(target=dodiffwrapper,name='visualdiff')thread.setDaemon(True)thread.start()classFileSelectionDialog(gtk.Dialog):'Dialog for selecting visual diff candidates'def__init__(self,repo,pats,ctx1a,sa,ctx1b,sb,ctx2,cpy):'Initialize the Dialog'gtk.Dialog.__init__(self,title=_('Visual Diffs'))gtklib.set_tortoise_icon(self,'menushowchanged.ico')gtklib.set_tortoise_keys(self)ifctx2.rev()isNone:title=_('working changes')elifctx1a==ctx2.parents()[0]:title=_('changeset ')+str(ctx2.rev())else:title=_('revisions %d to %d')%(ctx1a.rev(),ctx2.rev())title=_('Visual Diffs - ')+titleifpats:title+=_(' filtered')self.set_title(title)self.set_default_size(400,250)self.set_has_separator(False)ifctx1b:ctxa=ctx1a.ancestor(ctx1b)else:ctxa=ctx1aself.ctxs=(ctx1a,ctx1b,ctxa,ctx2)self.copies=cpyself.ui=repo.uilbl=gtk.Label(_('Temporary files are removed when this dialog'' is closed'))self.vbox.pack_start(lbl,False,False,2)scroller=gtk.ScrolledWindow()scroller.set_shadow_type(gtk.SHADOW_IN)scroller.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC)treeview=gtk.TreeView()self.treeview=treeviewtreeview.get_selection().set_mode(gtk.SELECTION_SINGLE)treeview.set_search_equal_func(self.search_filelist)scroller.add(treeview)self.vbox.pack_start(scroller,True,True,2)treeview.connect('row-activated',self.rowactivated)treeview.set_headers_visible(False)treeview.set_property('enable-grid-lines',True)treeview.set_enable_search(False)accelgroup=gtk.AccelGroup()self.add_accel_group(accelgroup)mod=gtklib.get_thg_modifier()key,modifier=gtk.accelerator_parse(mod+'d')treeview.add_accelerator('thg-diff',accelgroup,key,modifier,gtk.ACCEL_VISIBLE)treeview.connect('thg-diff',self.rowactivated)cell=gtk.CellRendererText()stcol=gtk.TreeViewColumn('Status',cell)stcol.set_resizable(True)stcol.add_attribute(cell,'text',0)treeview.append_column(stcol)cell=gtk.CellRendererText()fcol=gtk.TreeViewColumn('Filename',cell)fcol.set_resizable(True)fcol.add_attribute(cell,'text',1)treeview.append_column(fcol)model=gtk.ListStore(str,str)treeview.set_model(model)tools=hglib.difftools(repo.ui)preferred=besttool(repo.ui,tools)self.diffpath,self.diffopts,self.mergeopts=tools[preferred]hbox=gtk.HBox()self.vbox.pack_start(hbox,False,False,2)ifctx2.rev()isNone:pass# Do not offer directory diffs when the working directory# is being referenced directlyelifctx1b:self.p1button=gtk.Button(_('Dir diff to p1'))self.p1button.connect('pressed',self.p1dirdiff)self.p2button=gtk.Button(_('Dir diff to p2'))self.p2button.connect('pressed',self.p2dirdiff)self.p3button=gtk.Button(_('3-way dir diff'))self.p3button.connect('pressed',self.threewaydirdiff)hbox.pack_end(self.p3button,False,False)hbox.pack_end(self.p2button,False,False)hbox.pack_end(self.p1button,False,False)else:self.dbutton=gtk.Button(_('Directory diff'))self.dbutton.connect('pressed',self.p1dirdiff)hbox.pack_end(self.dbutton,False,False)self.update_diff_buttons(preferred)iflen(tools)>1:combo=gtk.combo_box_new_text()fori,nameinenumerate(tools.iterkeys()):combo.append_text(name)ifname==preferred:defrow=icombo.set_active(defrow)combo.connect('changed',self.toolselect,tools)hbox.pack_start(combo,False,False,2)patterns=repo.ui.configitems('diff-patterns')patterns=[(p,t)forp,tinpatternsiftintools]filesel=treeview.get_selection()filesel.connect('changed',self.fileselect,repo,combo,tools,patterns,preferred)gobject.idle_add(self.fillmodel,repo,model,sa,sb)deffillmodel(self,repo,model,sa,sb):ctx1a,ctx1b,ctxa,ctx2=self.ctxsmod_a,add_a,rem_a=samod_b,add_b,rem_b=sbsources=set(self.copies.values())MA=mod_a|add_a|mod_b|add_bMAR=MA|rem_a|rem_b|sourcestmproot=tempfile.mkdtemp(prefix='visualdiff.')self.tmproot=tmproot# Always make a copy of node1a (and node1b, if applicable)files=sources|mod_a|rem_a|((mod_b|add_b)-add_a)dir1a=snapshot(repo,files,ctx1a,tmproot)[0]rev1a='@%d'%ctx1a.rev()ifctx1b:files=sources|mod_b|rem_b|((mod_a|add_a)-add_b)dir1b=snapshot(repo,files,ctx1b,tmproot)[0]rev1b='@%d'%ctx1b.rev()ifctxa==ctx1a:dira=dir1aelifctxa==ctx1b:dira=dir1belse:# snapshot for ancestor revisiondira=snapshot(repo,MAR,ctxa,tmproot)[0]reva='@%d'%ctxa.rev()else:dir1b,dira=None,Nonerev1b,reva='',''# If ctx2 is the working copy, use it directlyifctx2.rev()isNone:dir2=repo.rootrev2=''else:dir2=snapshot(repo,MA,ctx2,tmproot)[0]rev2='@%d'%ctx2.rev()self.dirs=(dir1a,dir1b,dira,dir2)self.revs=(rev1a,rev1b,reva,rev2)defget_status(file,mod,add,rem):iffileinmod:return'M'iffileinadd:return'A'iffileinrem:return'R'return' 'forfinmod_a|add_a|rem_a:model.append([get_status(f,mod_a,add_a,rem_a),hglib.toutf(f)])self.connect('response',self.response)defsearch_filelist(self,model,column,key,iter):'case insensitive filename search'key=key.lower()ifkeyinmodel.get_value(iter,1).lower():returnFalsereturnTruedeftoolselect(self,combo,tools):'user selected a tool from the tool combo'sel=combo.get_active_text()ifselintools:self.diffpath,self.diffopts,self.mergeopts=tools[sel]self.update_diff_buttons(sel)defupdate_diff_buttons(self,tool):ifhasattr(self,'p1button'):d2=self.ui.configbool('merge-tools',tool+'.dirdiff')d3=self.ui.configbool('merge-tools',tool+'.dir3diff')self.p1button.set_sensitive(d2)self.p2button.set_sensitive(d2)self.p3button.set_sensitive(d3)elifhasattr(self,'dbutton'):d2=self.ui.configbool('merge-tools',tool+'.dirdiff')self.dbutton.set_sensitive(d2)deffileselect(self,selection,repo,combo,tools,patterns,preferred):'user selected a file, pick an appropriate tool from combo'model,path=selection.get_selected()ifnotpath:returnrow=model[path]fname=row[-1]forpat,toolinpatterns:mf=match.match(repo.root,'',[pat])ifmf(fname):selected=toolbreakelse:selected=preferredfori,nameinenumerate(tools.iterkeys()):ifname==selected:combo.set_active(i)defresponse(self,window,resp):self.should_live()defshould_live(self):whileself.tmproot:try:shutil.rmtree(self.tmproot)returnFalseexcept(IOError,OSError),e:resp=gdialog.CustomPrompt(_('Unable to delete temp files'),_('Close diff tools and try again, or quit to leak files?'),self,(_('Try &Again'),_('&Quit')),1).run()ifresp==0:continueelse:returnFalsereturnFalsedefrowactivated(self,tree,*args):selection=tree.get_selection()ifselection.count_selected_rows()!=1:returnFalsemodel,paths=selection.get_selected_rows()self.launch(*model[paths[0]])deflaunch(self,st,fname):fname=hglib.fromutf(fname)source=self.copies.get(fname,None)dir1a,dir1b,dira,dir2=self.dirsrev1a,rev1b,reva,rev2=self.revsctx1a,ctx1b,ctxa,ctx2=self.ctxsdefgetfile(ctx,dir,fname,source):m=ctx.manifest()iffnameinm:path=os.path.join(dir,util.localpath(fname))returnfname,pathelifsourceandsourceinm:path=os.path.join(dir,util.localpath(source))returnsource,pathelse:nullfile=os.path.join(self.tmproot,'empty')fp=open(nullfile,'w')fp.close()return_nonexistant,nullfilelocal,file1a=getfile(ctx1a,dir1a,fname,source)ifctx1b:other,file1b=getfile(ctx1b,dir1b,fname,source)ancestor,filea=getfile(ctxa,dira,fname,source)else:other,ancestor=fname,fnamefile1b,filea=None,Nonefname,file2=getfile(ctx2,dir2,fname,None)label1a=local+rev1alabel1b=other+rev1blabela=ancestor+revalabel2=fname+rev2ifctx1b:label1a+='[local]'label1b+='[other]'labela+='[ancestor]'label2+='[merged]'# Function to quote file/dir names in the argument stringreplace=dict(parent=file1a,parent1=file1a,plabel1=label1a,parent2=file1b,plabel2=label1b,ancestor=filea,alabel=labela,clabel=label2,child=file2)args=ctx1bandself.mergeoptsorself.diffoptslaunchtool(self.diffpath,args,replace,False)defp1dirdiff(self,button):dir1a,dir1b,dira,dir2=self.dirsrev1a,rev1b,reva,rev2=self.revsreplace=dict(parent=dir1a,parent1=dir1a,plabel1=rev1a,parent2='',plabel2='',ancestor='',alabel='',clabel=rev2,child=dir2)launchtool(self.diffpath,self.diffopts,replace,False)defp2dirdiff(self,button):dir1a,dir1b,dira,dir2=self.dirsrev1a,rev1b,reva,rev2=self.revsreplace=dict(parent=dir1b,parent1=dir1b,plabel1=rev1b,parent2='',plabel2='',ancestor='',alabel='',clabel=rev2,child=dir2)launchtool(self.diffpath,self.diffopts,replace,False)defthreewaydirdiff(self,button):dir1a,dir1b,dira,dir2=self.dirsrev1a,rev1b,reva,rev2=self.revsreplace=dict(parent=dir1a,parent1=dir1a,plabel1=rev1a,parent2=dir1b,plabel2=rev1b,ancestor=dira,alabel=reva,clabel=dir2,child=rev2)launchtool(self.diffpath,self.mergeopts,replace,False)defrun(ui,*pats,**opts):try:path=opts.get('bundle')orpaths.find_root()repo=hg.repository(ui,path=path)excepterror.RepoError:ui.warn(_('No repository found here')+'\n')returnNonepats=hglib.canonpaths(pats)ifopts.get('canonpats'):pats=list(pats)+opts['canonpats']returnvisualdiff(ui,repo,pats,opts)
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.