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.
# chunks.py - TortoiseHg patch/diff browser and editor## Copyright 2010 Steve Borho <steve@borho.org>## This software may be used and distributed according to the terms# of the GNU General Public License, incorporated herein by reference.importcStringIOimportosfrommercurialimporthg,util,patch,commands,cmdutilfrommercurialimportmatchasmatchmod,uiasuimodfromhgextimportrecordfromtortoisehg.utilimporthglibfromtortoisehg.util.patchctximportpatchctxfromtortoisehg.hgqt.i18nimport_fromtortoisehg.hgqtimportqtlib,thgrepo,qscilib,lexers,wctxactionsfromtortoisehg.hgqtimportfilelistmodel,filelistview,fileviewfromPyQt4.QtCoreimport*fromPyQt4.QtGuiimport*fromPyQt4importQsci# TODO# Add support for tools like TortoiseMerge that help resolve rejected chunksqsci=Qsci.QsciScintillaclassChunksWidget(QWidget):linkActivated=pyqtSignal(QString)showMessage=pyqtSignal(QString)chunksSelected=pyqtSignal(bool)fileSelected=pyqtSignal(bool)fileModelEmpty=pyqtSignal(bool)fileModified=pyqtSignal()def__init__(self,repo,parent):QWidget.__init__(self,parent)self.repo=repoself.currentFile=Nonelayout=QVBoxLayout(self)layout.setSpacing(0)layout.setMargin(0)layout.setContentsMargins(2,2,2,2)self.setLayout(layout)self.splitter=QSplitter(self)self.splitter.setOrientation(Qt.Vertical)self.splitter.setChildrenCollapsible(False)self.layout().addWidget(self.splitter)self.filelist=filelistview.HgFileListView(self)self.filelistmodel=filelistmodel.HgFileListModel(self.repo,self)self.filelist.setModel(self.filelistmodel)self.fileListFrame=QFrame(self.splitter)self.fileListFrame.setFrameShape(QFrame.NoFrame)vbox=QVBoxLayout()vbox.setSpacing(0)vbox.setMargin(0)vbox.addWidget(self.filelist)self.fileListFrame.setLayout(vbox)self.diffbrowse=DiffBrowser(self.splitter)self.diffbrowse.setFont(qtlib.getfont('fontdiff').font())self.diffbrowse.showMessage.connect(self.showMessage)self.diffbrowse.linkActivated.connect(self.linkActivated)self.diffbrowse.chunksSelected.connect(self.chunksSelected)self.filelist.fileRevSelected.connect(self.displayFile)self.filelist.clearDisplay.connect(self.diffbrowse.clearDisplay)self.splitter.setStretchFactor(0,0)self.splitter.setStretchFactor(1,3)self.timerevent=self.startTimer(500)deftimerEvent(self,event):'Periodic poll of currently displayed patch or working file'ifnothasattr(self,'filelistmodel'):returnctx=self.filelistmodel._ctxifctxisNone:returnifisinstance(ctx,patchctx):path=ctx._pathmtime=ctx._mtimeelifself.currentFile:path=self.repo.wjoin(self.currentFile)mtime=self.mtimeelse:returnifos.path.exists(path):newmtime=os.path.getmtime(path)ifmtime!=newmtime:self.mtime=newmtimeself.refresh()defrunPatcher(self,fp,wfile,updatestate):ui=self.repo.ui.copy()classwarncapt(ui.__class__):defwarn(self,msg,*args,**opts):self.write(msg)ui.__class__=warncaptok=Truerepo=self.repoui.pushbuffer()pfiles={}curdir=os.getcwd()try:os.chdir(repo.root)ifpatch.applydiff(ui,fp,pfiles)<0:ok=Falseself.showMessage.emit(_('Patch failed to apply'))exceptpatch.PatchError,err:ok=Falseself.showMessage.emit(hglib.tounicode(str(err)))os.chdir(curdir)forlineinui.popbuffer().splitlines():ifline.endswith(wfile+'.rej'):ifqtlib.QuestionMsgBox(_('Manually resolve rejected chunks?'),hglib.tounicode(line)+u'<br><br>'+_('Edit patched file and rejects?'),parent=self):fromtortoisehg.hgqtimportrejectsdlg=rejects.RejectsDialog(repo.wjoin(wfile),self)ifdlg.exec_()==QDialog.Accepted:ok=Truebreakifupdatestateandok:# Apply operations specified in git diff headerscmdutil.updatedir(repo.ui,repo,pfiles)returnokdefeditCurrentFile(self):ctx=self.filelistmodel._ctxifisinstance(ctx,patchctx):path=ctx._pathelse:path=self.repo.wjoin(self.currentFile)wctxactions.edit(self,self.repo.ui,self.repo,[path])defgetSelectedFileAndChunks(self):chunks=self.diffbrowse.curchunksifchunks:dchunks=[cforcinchunks[1:]ifc.selected]returnself.currentFile,[chunks[0]]+dchunkselse:returnself.currentFile,[]defdeleteSelectedChunks(self):'delete currently selected chunks'repo=self.repochunks=self.diffbrowse.curchunksdchunks=[cforcinchunks[1:]ifc.selected]ifnotdchunks:self.showMessage.emit(_('No deletable chunks'))returnkchunks=[cforcinchunks[1:]ifnotc.selected]revertall=Falseifnotkchunksandqtlib.QuestionMsgBox(_('No chunks remain'),_('Remove all file changes?')):revertall=Truectx=self.filelistmodel._ctxifisinstance(ctx,patchctx):repo.thgbackup(ctx._path)fp=util.atomictempfile(ctx._path,'wb')try:ifctx._ph.comments:fp.write('\n'.join(ctx._ph.comments))fp.write('\n\n')needsnewline=Falseforwfileinctx._fileorder:ifwfile==self.currentFile:ifrevertall:continuechunks[0].write(fp)forchunkinkchunks:chunk.write(fp)ifnotchunks[-1].selected:needsnewline=Trueelse:ifneedsnewline:fp.write('\n')needsnewline=Falseforchunkinctx._files[wfile]:chunk.write(fp)fp.rename()finally:delfpctx.invalidate()self.fileModified.emit()else:path=repo.wjoin(self.currentFile)ifnotos.path.exists(path):self.showMessage.emit(_('file has been deleted, refresh'))returnifself.mtime!=os.path.getmtime(path):self.showMessage.emit(_('file has been modified, refresh'))returnrepo.thgbackup(path)ifrevertall:commands.revert(repo.ui,repo,path,no_backup=True)else: wlock = repo.wlock()
try:
repo.wopener(self.currentFile, 'wb').write(
- self.diffbrowse.origcontents)
+hglib.fromunicode(self.diffbrowse.origcontents)) fp = cStringIO.StringIO()
chunks[0].write(fp)
for c in kchunks:
c.write(fp)fp.seek(0)self.runPatcher(fp,self.currentFile,False)finally:wlock.release()self.fileModified.emit()defmergeChunks(self,wfile,chunks):defisAorR(header):forlineinheader:ifline.startswith('--- /dev/null'):returnTrueifline.startswith('+++ /dev/null'):returnTruereturnFalserepo=self.repoctx=self.filelistmodel._ctxifisinstance(ctx,patchctx):ifwfileinctx._files:patchchunks=ctx._files[wfile]ifisAorR(chunks[0].header)orisAorR(patchchunks[0].header):qtlib.InfoMsgBox(_('Unable to merge chunks'),_('Add or remove patches must be merged ''in the working directory'))returnFalse# merge new chunks into existing chunks, sorting on start linenewchunks=[chunks[0]]pidx=nidx=1whilepidx<len(patchchunks)ornidx<len(chunks):ifpidx==len(patchchunks):newchunks.append(chunks[nidx])nidx+=1elifnidx==len(chunks):newchunks.append(patchchunks[pidx])pidx+=1elifchunks[nidx].fromline<patchchunks[pidx].fromline:newchunks.append(chunks[nidx])nidx+=1else:newchunks.append(patchchunks[pidx])pidx+=1ctx._files[wfile]=newchunkselse:# add file to patchctx._files[wfile]=chunksctx._fileorder.append(wfile)repo.thgbackup(ctx._path)fp=util.atomictempfile(ctx._path,'wb')try:ifctx._ph.comments:fp.write('\n'.join(ctx._ph.comments))fp.write('\n\n')forfileinctx._fileorder:forchunkinctx._files[file]:chunk.write(fp)fp.rename()ctx.invalidate()self.fileModified.emit()returnTruefinally:delfpreturnFalseelse:# Apply chunks to wfilerepo.thgbackup(repo.wjoin(wfile))fp=cStringIO.StringIO()forcinchunks:c.write(fp)fp.seek(0)wlock=repo.wlock()try:returnself.runPatcher(fp,wfile,True)finally:wlock.release()returnFalsedefgetFileList(self):returnself.filelistmodel._ctx.files()defremoveFile(self,wfile):repo=self.repoctx=self.filelistmodel._ctxifisinstance(ctx,patchctx):repo.thgbackup(ctx._path)fp=util.atomictempfile(ctx._path,'wb')try:ifctx._ph.comments:fp.write('\n'.join(ctx._ph.comments))fp.write('\n\n')forfileinctx._fileorder:iffile==wfile:continueforchunkinctx._files[file]:chunk.write(fp)fp.rename()finally:delfpctx.invalidate()else:repo.thgbackup(repo.wjoin(wfile))wasadded=wfileinrepo[None].added()commands.revert(repo.ui,repo,repo.wjoin(wfile),no_backup=True)ifwasadded:os.unlink(repo.wjoin(wfile))self.fileModified.emit()defgetChunksForFile(self,wfile):repo=self.repoctx=self.filelistmodel._ctxifisinstance(ctx,patchctx):ifwfileinctx._files:returnctx._files[wfile]else:return[]else:buf=cStringIO.StringIO()diffopts=patch.diffopts(repo.ui,{'git':True})m=matchmod.exact(repo.root,repo.root,[wfile])forpinpatch.diff(repo,ctx.p1().node(),None,match=m,opts=diffopts):buf.write(p)buf.seek(0)header=record.parsepatch(buf)[0]return[header]+header.hunks@pyqtSlot(object,object,object)defdisplayFile(self,file,rev,status):iffile:self.currentFile=filepath=self.repo.wjoin(file)ifos.path.exists(path):self.mtime=os.path.getmtime(path)else:self.mtime=Noneself.diffbrowse.displayFile(file,status)self.fileSelected.emit(True)else:self.currentFile=Noneself.diffbrowse.clearDisplay()self.diffbrowse.clearChunks()self.fileSelected.emit(False)defsetContext(self,ctx):self.diffbrowse.setContext(ctx)self.filelistmodel.setContext(ctx)empty=len(ctx.files())==0self.fileModelEmpty.emit(empty)self.fileSelected.emit(notempty)self.diffbrowse.updateSummary()defrefresh(self):ctx=self.filelistmodel._ctxifisinstance(ctx,patchctx):# if patch mtime has not changed, it could return the same ctxctx=self.repo.changectx(ctx._path)else:self.repo.thginvalidate()ctx=self.repo.changectx(ctx.node())self.setContext(ctx)ifself.currentFile:self.filelist.selectFile(self.currentFile)defloadSettings(self,qs,prefix):self.diffbrowse.loadSettings(qs,prefix)defsaveSettings(self,qs,prefix):self.diffbrowse.saveSettings(qs,prefix)# DO NOT USE. Sadly, this does not work.classElideLabel(QLabel):def__init__(self,text='',parent=None):QLabel.__init__(self,text,parent)defsizeHint(self):returnsuper(ElideLabel,self).sizeHint()defpaintEvent(self,event):p=QPainter()fm=QFontMetrics(self.font())iffm.width(self.text()):# > self.contentsRect().width():elided=fm.elidedText(self.text(),Qt.ElideLeft,self.rect().width(),0)p.drawText(self.rect(),Qt.AlignTop|Qt.AlignRight|Qt.TextSingleLine,elided)else:super(ElideLabel,self).paintEvent(event)classDiffBrowser(QFrame):"""diff browser"""linkActivated=pyqtSignal(QString)showMessage=pyqtSignal(QString)chunksSelected=pyqtSignal(bool)def__init__(self,parent):QFrame.__init__(self,parent)self.curchunks=[]self.countselected=0self._ctx=Noneself._lastfile=Nonevbox=QVBoxLayout()vbox.setContentsMargins(0,0,0,0)vbox.setSpacing(0)self.setLayout(vbox)self.labelhbox=hbox=QHBoxLayout()hbox.setContentsMargins(0,0,0,0)hbox.setSpacing(2)self.layout().addLayout(hbox)self.filenamelabel=w=QLabel()self.filenamelabel.hide()hbox.addWidget(w)w.setWordWrap(True)f=w.textInteractionFlags()w.setTextInteractionFlags(f|Qt.TextSelectableByMouse)w.linkActivated.connect(self.linkActivated)self.sumlabel=QLabel()self.allbutton=QToolButton()self.allbutton.setText(_('All','files'))self.allbutton.setShortcut(QKeySequence.SelectAll)self.allbutton.clicked.connect(self.selectAll)self.nonebutton=QToolButton()self.nonebutton.setText(_('None','files'))self.nonebutton.setShortcut(QKeySequence.New)self.nonebutton.clicked.connect(self.selectNone)hbox.addStretch(1)hbox.addWidget(self.sumlabel)hbox.addWidget(self.allbutton)hbox.addWidget(self.nonebutton)self.extralabel=w=QLabel()w.setWordWrap(True)w.linkActivated.connect(self.linkActivated)self.layout().addWidget(w)w.hide()self.sci=qscilib.Scintilla(self)self.sci.setFrameStyle(0)self.sci.setReadOnly(True)self.sci.setUtf8(True)self.sci.installEventFilter(qscilib.KeyPressInterceptor(self))self.sci.setContextMenuPolicy(Qt.CustomContextMenu)self.sci.customContextMenuRequested.connect(self.menuRequested)self.sci.setCaretLineVisible(False)self.sci.setMarginType(1,qsci.SymbolMargin)self.sci.setMarginLineNumbers(1,False)self.sci.setMarginWidth(1,QFontMetrics(self.font()).width('XX'))self.sci.setMarginSensitivity(1,True)self.sci.marginClicked.connect(self.marginClicked)self.selected=self.sci.markerDefine(qsci.Plus,-1)self.unselected=self.sci.markerDefine(qsci.Minus,-1)self.vertical=self.sci.markerDefine(qsci.VerticalLine,-1)self.divider=self.sci.markerDefine(qsci.Background,-1)self.selcolor=self.sci.markerDefine(qsci.Background,-1)self.sci.setMarkerBackgroundColor(QColor('#BBFFFF'),self.selcolor)self.sci.setMarkerBackgroundColor(QColor('#AAAAAA'),self.divider)mask=(1<<self.selected)|(1<<self.unselected)| \
(1<<self.vertical)|(1<<self.selcolor)|(1<<self.divider)self.sci.setMarginMarkerMask(1,mask)self.layout().addWidget(self.sci,1)lexer=lexers.get_diff_lexer(self)self.sci.setLexer(lexer)self.clearDisplay()defmenuRequested(self,point):point=self.sci.mapToGlobal(point)returnself.sci.createStandardContextMenu().exec_(point)defloadSettings(self,qs,prefix):self.sci.loadSettings(qs,prefix)defsaveSettings(self,qs,prefix):self.sci.saveSettings(qs,prefix)defupdateSummary(self):self.sumlabel.setText(_('Chunks selected: %d / %d')%(self.countselected,len(self.curchunks[1:])))self.chunksSelected.emit(self.countselected>0)@pyqtSlot()defselectAll(self):forchunkinself.curchunks[1:]:ifnotchunk.selected:self.sci.markerDelete(chunk.mline,-1)self.sci.markerAdd(chunk.mline,self.selected)chunk.selected=Trueself.countselected+=1foriinxrange(*chunk.lrange):self.sci.markerAdd(i,self.selcolor)self.updateSummary()@pyqtSlot()defselectNone(self):forchunkinself.curchunks[1:]:ifchunk.selected:self.sci.markerDelete(chunk.mline,-1)self.sci.markerAdd(chunk.mline,self.unselected)chunk.selected=Falseself.countselected-=1foriinxrange(*chunk.lrange):self.sci.markerDelete(i,self.selcolor)self.updateSummary()@pyqtSlot(int,int,Qt.KeyboardModifiers)defmarginClicked(self,margin,line,modifiers):forchunkinself.curchunks[1:]:ifline>=chunk.lrange[0]andline<chunk.lrange[1]:self.toggleChunk(chunk)self.updateSummary()returndeftoggleChunk(self,chunk):self.sci.markerDelete(chunk.mline,-1)ifchunk.selected:self.sci.markerAdd(chunk.mline,self.unselected)chunk.selected=Falseself.countselected-=1foriinxrange(*chunk.lrange):self.sci.markerDelete(i,self.selcolor)else:self.sci.markerAdd(chunk.mline,self.selected)chunk.selected=Trueself.countselected+=1foriinxrange(*chunk.lrange):self.sci.markerAdd(i,self.selcolor)defsetContext(self,ctx):self._ctx=ctxself.sci.setTabWidth(ctx._repo.tabwidth)defclearDisplay(self):self.sci.clear()self.filenamelabel.setText(' ')self.extralabel.hide()defclearChunks(self):self.curchunks=[]self.countselected=0self.updateSummary()defdisplayFile(self,filename,status):self.clearDisplay()iffilename==self._lastfile:reenable=[c.fromlineforcinself.curchunks[1:]ifc.selected]else:reenable=[]self._lastfile=filenameself.clearChunks()fd=fileview.FileData(self._ctx,None,filename,status)iffd.elabel:self.extralabel.setText(fd.elabel)self.extralabel.show()else:self.extralabel.hide()self.filenamelabel.setText(fd.flabel)ifnotfd.isValid()ornotfd.diff:self.sci.setText(fd.erroror'')returneliftype(self._ctx.rev())isstr:chunks=self._ctx._files[filename]else:header=record.parsepatch(cStringIO.StringIO(fd.diff))[0]chunks=[header]+header.hunksutext=[]forchunkinchunks[1:]:buf=cStringIO.StringIO()chunk.selected=Falsechunk.write(buf)chunk.lines=buf.getvalue().splitlines()utext+=[hglib.tounicode(l)forlinchunk.lines]utext.append('')self.sci.setText(u'\n'.join(utext))start=0self.sci.markerDeleteAll(-1)forchunkinchunks[1:]:chunk.lrange=(start,start+len(chunk.lines))chunk.mline=start+len(chunk.lines)/2ifstart:self.sci.markerAdd(start-1,self.divider)foriinxrange(1,len(chunk.lines)-1):ifstart+i==chunk.mline:self.sci.markerAdd(chunk.mline,self.unselected)else:self.sci.markerAdd(start+i,self.vertical)start+=len(chunk.lines)+1self.origcontents=fd.olddataself.countselected=0self.curchunks=chunksforcinchunks[1:]:ifc.fromlineinreenable:self.toggleChunk(c)self.updateSummary()
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.