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.
Build the actions and context menu at startup, disable actions as appropriate when the selection changes. Set a default action for when a row is activated, give each action a keyboard shortcut.
# grep.py - Working copy and history search## Copyright 2010 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.importosimportrefrommercurialimportui,hg,error,commands,match,utilfromtortoisehg.hgqtimporthtmlui,visdiff,qtlib,htmldelegate,thgrepo,cmduifromtortoisehg.utilimportpaths,hglib,thread2fromtortoisehg.hgqt.i18nimport_fromPyQt4.QtCoreimport*fromPyQt4.QtGuiimport*# This widget can be embedded in any application that would like to# provide search featuresclassSearchWidget(QWidget):'''Working copy and repository search widget'''showMessage=pyqtSignal(QString)progress=pyqtSignal(QString,object,QString,QString,object)revisionSelected=pyqtSignal(int)def__init__(self,upats,repo,parent=None,**opts):QWidget.__init__(self,parent)self.thread=Nonemainvbox=QVBoxLayout()mainvbox.setSpacing(6)self.setLayout(mainvbox)hbox=QHBoxLayout()hbox.setMargin(2)le=QLineEdit()ifhasattr(le,'setPlaceholderText'):# Qt >= 4.7 le.setPlaceholderText('### regular expression search pattern ###')else:lbl=QLabel(_('Regexp:'))lbl.setBuddy(le)hbox.addWidget(lbl)chk=QCheckBox(_('Ignore case'))bt=QPushButton(_('Search'))bt.setDefault(True)cbt=QPushButton(_('Stop'))cbt.setEnabled(False)cbt.clicked.connect(self.stopClicked)hbox.addWidget(le,1)hbox.addWidget(chk)hbox.addWidget(bt)hbox.addWidget(cbt)incle=QLineEdit()excle=QLineEdit()working=QRadioButton(_('Working Copy'))revision=QRadioButton(_('Revision'))history=QRadioButton(_('All History'))singlematch=QCheckBox(_('Report only the first match per file'))follow=QCheckBox(_('Follow copies and renames'))revle=QLineEdit()grid=QGridLayout()grid.addWidget(working,0,0)grid.addWidget(history,1,0)grid.addWidget(revision,2,0)grid.addWidget(revle,2,1)grid.addWidget(singlematch,0,3)grid.addWidget(follow,0,4)ilabel=QLabel(_('Includes:'))ilabel.setBuddy(incle)elabel=QLabel(_('Excludes:'))elabel.setBuddy(excle)ehelpstr=_('Comma separated list of exclusion patterns. ''Exclusion patterns are applied after inclusion patterns.')ihelpstr=_('Comma separated list of inclusion patterns. ''By default, the entire repository is searched.')ifhasattr(incle,'setPlaceholderText'):# Qt >= 4.7 incle.setPlaceholderText(u' '.join([u'###',ihelpstr,u'###']))else:incle.setToolTip(ihelpstr)ifhasattr(excle,'setPlaceholderText'):# Qt >= 4.7 excle.setPlaceholderText(u' '.join([u'###',ehelpstr,u'###']))else:excle.setToolTip(ehelpstr)grid.addWidget(ilabel,1,2)grid.addWidget(incle,1,3,1,2)grid.addWidget(elabel,2,2)grid.addWidget(excle,2,3,1,2)grid.setColumnStretch(3,1)grid.setColumnStretch(1,0)frame=QFrame()frame.setFrameStyle(QFrame.StyledPanel)defrevisiontoggled(checked):revle.setEnabled(checked)ifchecked:revle.selectAll()QTimer.singleShot(0,lambda:revle.setFocus())revision.toggled.connect(revisiontoggled)history.toggled.connect(singlematch.setDisabled)revle.setEnabled(False)revle.returnPressed.connect(self.searchActivated)excle.returnPressed.connect(self.searchActivated)incle.returnPressed.connect(self.searchActivated)bt.clicked.connect(self.searchActivated)working.setChecked(True)defupdatefollow():slowpath=bool(incle.text()orexcle.text())follow.setEnabled(history.isChecked()andnotslowpath)ifslowpath:follow.setChecked(False)history.toggled.connect(updatefollow)incle.textChanged.connect(updatefollow)excle.textChanged.connect(updatefollow)updatefollow()mainvbox.addLayout(hbox)frame.setLayout(grid) mainvbox.addWidget(frame)
tv = MatchTree(repo, self)
- tm = MatchModel(self)- tv.setModel(tm) tv.revisionSelected.connect(self.revisionSelected)
tv.setColumnHidden(COL_REVISION, True)
tv.setColumnHidden(COL_USER, True)
mainvbox.addWidget(tv)le.returnPressed.connect(self.searchActivated)self.repo=repoself.tv,self.regexple,self.chk=tv,le,chkself.incle,self.excle,self.revle=incle,excle,revleself.wctxradio,self.ctxradio,self.aradio=working,revision,historyself.singlematch,self.follow,self.eframe=singlematch,follow,frameself.searchbutton,self.cancelbutton=bt,cbtself.regexple.setFocus()if'rev'inoptsor'all'inopts:self.setSearch(upats[0],**opts)eliflen(upats)>=1:le.setText(upats[0])iflen(upats)>1:incle.setText(','.join(upats[1:]))chk.setChecked(opts.get('ignorecase',False))repoid=str(repo[0])s=QSettings()sh=list(s.value('grep/search-'+repoid).toStringList())ph=list(s.value('grep/paths-'+repoid).toStringList())self.pathshistory=[pforpinphifp]self.searchhistory=[sforsinshifs]self.regexple.setCompleter(QCompleter(self.searchhistory,self))self.incle.setCompleter(QCompleter(self.pathshistory,self))self.excle.setCompleter(QCompleter(self.pathshistory,self))mainvbox.setContentsMargins(2,2,2,2)ifparent:self.closeonesc=Falseelse:self.setWindowTitle(_('TortoiseHg Search'))self.resize(800,550)self.closeonesc=Trueself.stbar=cmdui.ThgStatusBar()mainvbox.addWidget(self.stbar)self.showMessage.connect(self.stbar.showMessage)self.progress.connect(self.stbar.progress)defaddHistory(self,search,incpaths,excpaths):ifsearch:usearch=hglib.tounicode(search)ifusearchinself.searchhistory:self.searchhistory.remove(usearch)self.searchhistory=[usearch]+self.searchhistory[:9]forpinincpaths+excpaths:up=hglib.tounicode(p)ifupinself.pathshistory:self.pathshistory.remove(up)self.pathshistory=[up]+self.pathshistory[:9]self.regexple.setCompleter(QCompleter(self.searchhistory,self))self.incle.setCompleter(QCompleter(self.pathshistory,self))self.excle.setCompleter(QCompleter(self.pathshistory,self))defsetRevision(self,rev):ifisinstance(rev,basestring):# unapplied patchreturnelifrevisNone:self.wctxradio.setChecked(True)else:self.ctxradio.setChecked(True)self.revle.setText(str(rev))defsetSearch(self,upattern,**opts):self.regexple.setText(upattern)ifopts.get('all'):self.aradio.setChecked(True)elifopts.get('rev'):self.ctxradio.setChecked(True)self.revle.setText(opts['rev'])defstopClicked(self):ifself.threadandself.thread.isRunning():self.thread.cancel()ifself.thread.wait(2000):self.thread=NonedefkeyPressEvent(self,event):ifevent.key()==Qt.Key_Escape:ifself.threadandself.thread.isRunning():self.stopClicked()elifself.closeonesc:self.close()else:returnsuper(SearchWidget,self).keyPressEvent(event)defcloseEvent(self,event):repoid=str(self.repo[0])s=QSettings()s.setValue('grep/search-'+repoid,self.searchhistory)s.setValue('grep/paths-'+repoid,self.pathshistory)defsearchActivated(self):'User pressed [Return] in QLineEdit'ifself.threadandself.thread.isRunning():returnmodel=self.tv.model()model.reset()pattern=hglib.fromunicode(self.regexple.text())ifnotpattern:returntry:icase=self.chk.isChecked()regexp=re.compile(pattern,icaseandre.Ior0)exceptException,inst:msg=_('grep: invalid match pattern: %s\n')% \
hglib.tounicode(str(inst))self.showMessage.emit(msg)returnself.tv.setSortingEnabled(False)self.tv.pattern=patternself.regexple.selectAll()inc=hglib.fromunicode(self.incle.text())ifinc:inc=inc.split(', ')exc=hglib.fromunicode(self.excle.text())ifexc:exc=exc.split(', ')rev=hglib.fromunicode(self.revle.text()).strip()self.addHistory(pattern,incor[],excor[])ifself.wctxradio.isChecked():self.tv.setColumnHidden(COL_REVISION,True)self.tv.setColumnHidden(COL_USER,True)ctx=self.repo[None]self.thread=CtxSearchThread(self.repo,regexp,ctx,inc,exc,once=self.singlematch.isChecked())elifself.ctxradio.isChecked():self.tv.setColumnHidden(COL_REVISION,True)self.tv.setColumnHidden(COL_USER,True)try:ctx=self.repo[revor'.']excepterror.RepoError,e:msg=_('grep: %s\n')%hglib.tounicode(str(e))self.showMessage.emit(msg)returnself.thread=CtxSearchThread(self.repo,regexp,ctx,inc,exc,once=self.singlematch.isChecked())else:assertself.aradio.isChecked()self.tv.setColumnHidden(COL_REVISION,False)self.tv.setColumnHidden(COL_USER,False)self.thread=HistorySearchThread(self.repo,pattern,icase,inc,exc,follow=self.follow.isChecked())self.showMessage.emit('')self.regexple.setEnabled(False)self.searchbutton.setEnabled(False)self.cancelbutton.setEnabled(True)self.thread.finished.connect(self.searchfinished)self.thread.showMessage.connect(self.showMessage)self.thread.progress.connect(self.progress)self.thread.matchedRow.connect(lambdawrapper:model.appendRow(*wrapper.data))self.thread.start()defreload(self):# TODOpassdefsearchfinished(self):count=self.tv.model().rowCount(None)ifnotcount:self.showMessage.emit(_('No matches found'))else:self.showMessage.emit(_('%d matches found')%count)forcolinxrange(COL_TEXT):self.tv.resizeColumnToContents(col)self.tv.setSortingEnabled(True)self.cancelbutton.setEnabled(False)self.searchbutton.setEnabled(True)self.regexple.setEnabled(True)self.regexple.setFocus()classDataWrapper(object):def__init__(self,data):self.data=dataclassHistorySearchThread(QThread):'''Background thread for searching repository history'''matchedRow=pyqtSignal(DataWrapper)showMessage=pyqtSignal(unicode)progress=pyqtSignal(QString,object,QString,QString,object)def__init__(self,repo,pattern,icase,inc,exc,follow):super(HistorySearchThread,self).__init__()self.repo=hg.repository(repo.ui,repo.root)self.pattern=patternself.icase=icaseself.inc=incself.exc=excself.follow=followdefcancel(self):ifself.isRunning()andhasattr(self,'thread_id'):try:thread2._async_raise(self.thread_id,KeyboardInterrupt)exceptValueError:passdefrun(self):self.thread_id=int(QThread.currentThreadId())defemitrow(row):w=DataWrapper(row)self.matchedRow.emit(w)defemitprog(topic,pos,item,unit,total):self.progress.emit(topic,pos,item,unit,total)classincrui(ui.ui):fullmsg=''defwrite(self,msg,*args,**opts):self.fullmsg+=msgifself.fullmsg.endswith('\0'):try:fname,line,rev,addremove,user,text= \
self.fullmsg.split('\0',5)text=hglib.tounicode(text)text=Qt.escape(text)text='<b>%s</b> <span>%s</span>'%(addremove,text[:-1])row=[fname,rev,line,user,text]emitrow(row)exceptValueError:passself.fullmsg=''defprogress(topic,pos,item='',unit='',total=None):emitprog(topic,pos,item,unit,total)cwd=os.getcwd()os.chdir(self.repo.root)self.progress.emit(*cmdui.startProgress(_('Searching'),_('history')))try:# hg grep [-i] -afn regexpopts={'all':True,'user':True,'follow':self.follow,'rev':[],'line_number':True,'print0':True,'ignore_case':self.icase,'include':self.inc,'exclude':self.exc}u=incrui()commands.grep(u,self.repo,self.pattern,**opts)exceptException,e:self.showMessage.emit(str(e))exceptKeyboardInterrupt:self.showMessage.emit(_('Interrupted'))self.progress.emit(*cmdui.stopProgress(_('Searching')))os.chdir(cwd)classCtxSearchThread(QThread):'''Background thread for searching a changectx'''matchedRow=pyqtSignal(object)showMessage=pyqtSignal(unicode)progress=pyqtSignal(QString,object,QString,QString,object)def__init__(self,repo,regexp,ctx,inc,exc,once):super(CtxSearchThread,self).__init__()self.repo=hg.repository(repo.ui,repo.root)self.regexp=regexpself.ctx=ctxself.inc=incself.exc=excself.once=onceself.canceled=Falsedefcancel(self):self.canceled=Truedefrun(self):hu=htmlui.htmlui()rev=self.ctx.rev()# generate match function relative to repo rootmatchfn=match.match(self.repo.root,'',[],self.inc,self.exc)defbadfn(f,msg):e=hglib.tounicode("%s: %s"%(matchfn.rel(f),msg))self.showMessage.emit(e)matchfn.bad=badfntopic=_('Searching')unit=_('files')total=len(self.ctx.manifest())count=0forwfileinself.ctx:# walk manifestifself.canceled:breakself.progress.emit(topic,count,wfile,unit,total)count+=1ifnotmatchfn(wfile):continuedata=self.ctx[wfile].data()# load file dataifutil.binary(data):continuefori,lineinenumerate(data.splitlines()):pos=0forminself.regexp.finditer(line):# perform regexphu.write(line[pos:m.start()],label='ui.status')hu.write(line[m.start():m.end()],label='grep.match')pos=m.end()ifpos:hu.write(line[pos:],label='ui.status')row=[wfile,i+1,rev,None,hu.getdata()[0]]w=DataWrapper(row)self.matchedRow.emit(w)ifself.once:breakself.progress.emit(topic,None,'','',None)COL_PATH=0COL_LINE=1COL_REVISION=2# Hidden if ctxCOL_USER=3# Hidden if ctxCOL_TEXT=4classMatchTree(QTableView):revisionSelected=pyqtSignal(int)contextmenu=None def __init__(self, repo, parent):
QTableView.__init__(self, parent)
+ self.repo = repo
+ self.pattern = None+ self.embedded = parent.parent() is not None+ self.delegate = htmldelegate.HTMLDelegate(self)
self.setItemDelegateForColumn(COL_TEXT, self.delegate)
self.setSelectionMode(QTableView.ExtendedSelection)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.setShowGrid(False)
- self.embedded = parent.parent() is not None vh = self.verticalHeader()
vh.hide()
vh.setDefaultSectionSize(20)
- self.horizontalHeader().setStretchLastSection(True)
+ self.actions = {}+ self.contextmenu = QMenu(self)+ for key, name, func, shortcut in (+ ('edit', _('View file'), self.onViewFile, 'CTRL+E'),+ ('ctx', _('View Changeset'), self.onViewChangeset, 'CTRL+V'),+ ('vdiff', _('Visual Diff'), self.onVisualDiff, 'CTRL+D'),+ ('ann', _('Annotate file'), self.onAnnotateFile, 'CTRL+F')):+ action = QAction(name, self)+ action.triggered.connect(func)+ action.setShortcut(QKeySequence(shortcut))+ self.actions[key] = action+ self.addAction(action)+ self.contextmenu.addAction(action)+ self.activated.connect(self.onRowActivated) self.customContextMenuRequested.connect(self.menuRequest)
- self.pattern = None- self.searchwidget=parent++ self.setModel(MatchModel(self))+ self.selectionModel().selectionChanged.connect(self.onSelectionChanged) def dragObject(self):
snapshots = {}
- for index in self.selectedRows():
+ for index in self.selectionModel().selectedRows():
path, line, rev, user, text = self.model().getRow(index)
if rev not in snapshots:
snapshots[rev] = [path]
else:snapshots[rev].append(path)urls=[]forrev,pathsinsnapshots.iteritems():ifrevisnotNone:base,_=visdiff.snapshot(self.repo,paths,self.repo[rev])else:base=self.repo.rootforpinpaths:u=QUrl()u.setPath('file://'+os.path.join(base,path))urls.append(u)ifurls:d=QDrag(self)m=QMimeData()m.setUrls(urls)d.setMimeData(m)d.start(Qt.CopyAction)defmousePressEvent(self,event):self.pressPos=event.pos()self.pressTime=QTime.currentTime()returnQTableView.mousePressEvent(self,event)defmouseMoveEvent(self,event):d=event.pos()-self.pressPosifd.manhattanLength()<QApplication.startDragDistance():returnQTableView.mouseMoveEvent(self,event)elapsed=self.pressTime.msecsTo(QTime.currentTime())ifelapsed<QApplication.startDragTime():returnQTableView.mouseMoveEvent(self,event)self.dragObject() return QTableView.mouseMoveEvent(self, event)
def menuRequest(self, point):
+ if not self.selectionModel().selectedRows():+ return+ point = self.mapToGlobal(point)+ self.contextmenu.exec_(point)++ def onSelectionChanged(self, selected, deselected): selrows = []
wctxonly = True
allhistory = False
- for index in self.selectedRows():
+ for index in self.selectionModel().selectedRows():
path, line, rev, user, text = self.model().getRow(index)
if rev is not None:
wctxonly = False
if user is not None:
allhistory = True
selrows.append((rev, path, line))
- if not selrows:- return- point = self.mapToGlobal(point)- menus = [(_('View file'), self.view), (_('Annotate file'), self.ann)]- if not wctxonly and self.embedded:- menus.append((_('View Changeset'), self.ctx))- if allhistory:- # need to know files were modified at specified revision- menus.append((_('Visual Diff'), self.vdiff))- if self.contextmenu:- self.contextmenu.clear()- else:- self.contextmenu=QMenu(self)- for name, func in menus:- def add(name, func):- action = self.contextmenu.addAction(name)- action.triggered.connect(lambda: func(selrows))- add(name, func)
- self.contextmenu.exec_(point)
+ self.selectedRows = selrows+ self.actions['ctx'].setEnabled(notwctxonlyand self.embedded)
+ self.actions['vdiff'].setEnabled(allhistory)
- def ann(self, rows):
+ def onRowActivated(self, index):
+ saved = self.selectedRows+ path, line, rev, user, text = self.model().getRow(index)+ self.selectedRows = [(rev, path, line)]+ self.onAnnotateFile()+ self.selectedRows = saved++ def onAnnotateFile(self): from tortoisehg.hgqt import annotate
repo, ui, pattern = self.repo, self.repo.ui, self.pattern
seen = set()
- for rev, path, line in rows:
+ for rev, path, line in self.selectedRows:
# Only open one annotate instance per file
if path in seen:
continue
else: seen.add(path)
dlg = annotate.AnnotateDialog(path, rev=rev, line=line,
pattern=pattern, parent=self,
- searchwidget=self.searchwidget,
+ searchwidget=self.parent(),
root=repo.root)
dlg.show()
- def ctx(self, rows):
- for rev, path, line in rows:
+ def onViewChangeset(self):
+ for rev, path, line in self.selectedRows:
self.revisionSelected.emit(int(rev))
return
- def view(self, rows):
+ def onViewFile(self):
from tortoisehg.hgqt import wctxactions
repo, ui, pattern = self.repo, self.repo.ui, self.pattern
seen = set()
- for rev, path, line in rows:
+ for rev, path, line in self.selectedRows:
# Only open one editor instance per file
if path in seen:
continue
else:seen.add(path)ifrevisNone:files=[repo.wjoin(path)]wctxactions.edit(self,ui,repo,files,line,pattern)else:base,_=visdiff.snapshot(repo,[path],repo[rev]) files = [os.path.join(base, path)]
wctxactions.edit(self, ui, repo, files, line, pattern)
- def vdiff(self, rows):
+ def onVisualDiff(self):
+ rows = self.selectedRows[:] repo, ui = self.repo, self.repo.ui
while rows:
defer = []
crev=rows[0][0]files=set([rows[0][1]])forrev,path,lineinrows[1:]:ifrev==crev:files.add(path)else:defer.append([rev,path,line])ifcrevisnotNone:dlg=visdiff.visualdiff(ui,repo,list(files),{'change':crev})ifdlg: dlg.exec_()
rows = defer
- def selectedRows(self):- return self.selectionModel().selectedRows()--class MatchModel(QAbstractTableModel):
def __init__(self, parent):
QAbstractTableModel.__init__(self,parent)self.rows=[]self.headers=(_('File'),_('Line'),_('Rev'),_('User'),_('Match Text'))defrowCount(self,parent):returnlen(self.rows)defcolumnCount(self,parent):returnlen(self.headers)defdata(self,index,role):ifnotindex.isValid():returnQVariant()ifrole==Qt.DisplayRole:returnQVariant(self.rows[index.row()][index.column()])returnQVariant()defheaderData(self,col,orientation,role):ifrole!=Qt.DisplayRoleororientation!=Qt.Horizontal:returnQVariant()else:returnQVariant(self.headers[col])defflags(self,index):flags=Qt.ItemIsSelectable|Qt.ItemIsEnabled|Qt.ItemIsDragEnabledreturnflagsdefsort(self,col,order):self.layoutAboutToBeChanged.emit()self.rows.sort(lambdax,y:cmp(x[col],y[col]))iforder==Qt.DescendingOrder:self.rows.reverse()self.layoutChanged.emit()## Custom methodsdefappendRow(self,*args):l=len(self.rows)self.beginInsertRows(QModelIndex(),l,l)self.rows.append(args)self.endInsertRows()self.layoutChanged.emit()defreset(self):self.beginRemoveRows(QModelIndex(),0,len(self.rows)-1)self.rows=[]self.endRemoveRows()self.layoutChanged.emit()defgetRow(self,index):assertindex.isValid()returnself.rows[index.row()]defrun(ui,*pats,**opts):repo=thgrepo.repository(ui,path=paths.find_root())upats=[hglib.tounicode(p)forpinpats]returnSearchWidget(upats,repo,**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.