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.
My attempts to pass the data via DataWrapper objects was met with SIGSEGV at random times, and bizarre tracebacks from signal handler functions about QObject instances not having 'data' members.
Not sure what to make of it, but this impl seems to be solid. Bits of this (ProgUi) should eventually be moved to common code.
# guess.py - TortoiseHg's dialogs for detecting copies and renames## 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.importosfrommercurialimporthg,ui,mdiff,cmdutil,util,error,similarfrom tortoisehg.util import hglib, shlib, paths
from tortoisehg.hgqt.i18n import _
-from tortoisehg.hgqt import qtlib, htmlui
+from tortoisehg.hgqt import qtlib, htmlui, cmduifrom PyQt4.QtCore import *
from PyQt4.QtGui import *
# Technical Debt
-# Add a progress/status bar, connect to thread errors-# Give simularity routines a repo.ui that catches progress reports# Disable buttons when lists are empty
class DetectRenameDialog(QDialog):
'Detect renames after they occur'matchAccepted=pyqtSignal()def__init__(self,parent=None,root=None,*pats):QDialog.__init__(self,parent)repo=hg.repository(ui.ui(),path=paths.find_root(root))self.repo=repoself.pats=patsself.thread=Nonereponame=hglib.get_reponame(repo)self.setWindowTitle(_('Detect Copies/Renames in %s')%reponame)s=QSettings()self.restoreGeometry(s.value('guess/geom').toByteArray())layout=QVBoxLayout()self.setLayout(layout)# vsplit for top & diffvsplit=QSplitter(Qt.Horizontal)vsplit.restoreState(s.value('guess/vsplit-state').toByteArray())utframe=QFrame(vsplit)matchframe=QFrame(vsplit)utvbox=QVBoxLayout()utframe.setLayout(utvbox)matchvbox=QVBoxLayout()matchframe.setLayout(matchvbox)hsplit=QSplitter(Qt.Vertical)hsplit.restoreState(s.value('guess/hsplit-state').toByteArray())layout.addWidget(hsplit)hsplit.addWidget(vsplit)utlbl=QLabel(_('<b>Unrevisioned Files</b>'))utvbox.addWidget(utlbl)self.unrevlist=QListWidget()self.unrevlist.setSelectionMode(QAbstractItemView.ExtendedSelection)utvbox.addWidget(self.unrevlist)simhbox=QHBoxLayout()utvbox.addLayout(simhbox)lbl=QLabel()slider=QSlider(Qt.Horizontal)slider.setRange(0,100)slider.setTickInterval(10)slider.setPageStep(10)slider.setTickPosition(QSlider.TicksBelow)slider.changefunc=lambdav:lbl.setText(_('Min Simularity: %d%%')%v)slider.valueChanged.connect(slider.changefunc)slider.setValue(s.value('guess/simslider').toInt()[0])self.simslider=sliderlbl.setBuddy(slider)simhbox.addWidget(lbl)simhbox.addWidget(slider,1)buthbox=QHBoxLayout()utvbox.addLayout(buthbox)copycheck=QCheckBox(_('Only consider deleted files'))copycheck.setToolTip(_('Uncheck to consider all revisioned files'' for copy sources'))copycheck.setChecked(True)findrenames=QPushButton(_('Find Rename'))findrenames.setToolTip(_('Find copy and/or rename sources'))findrenames.clicked.connect(self.findRenames)buthbox.addWidget(copycheck)buthbox.addStretch(1)buthbox.addWidget(findrenames)self.findbtn,self.copycheck=findrenames,copycheckmatchlbl=QLabel(_('<b>Candidate Matches</b>'))matchvbox.addWidget(matchlbl) self.matchlv = QTreeView()
self.matchlv.setItemsExpandable(False)
self.matchlv.setRootIsDecorated(False)
- self.matchlv.setModel(MatchModel())
+ self.model = MatchModel()+ self.matchlv.setModel(self.model)
self.matchlv.clicked.connect(self.showDiff)
buthbox = QHBoxLayout()
matchbtn = QPushButton(_('Accept Selected Matches'))
matchbtn.clicked.connect(self.acceptMatch)matchbtn.setEnabled(False)self.matchbtn=matchbtnbuthbox.addStretch(1)buthbox.addWidget(matchbtn)matchvbox.addWidget(self.matchlv)matchvbox.addLayout(buthbox)diffframe=QFrame(hsplit)diffvbox=QVBoxLayout()diffframe.setLayout(diffvbox)difflabel=QLabel(_('<b>Differences from Source to Dest</b>'))diffvbox.addWidget(difflabel)difftb=QTextBrowser()difftb.document().setDefaultStyleSheet(qtlib.thgstylesheet) diffvbox.addWidget(difftb)
self.difftb = difftb
-BB = QDialogButtonBox- bb = QDialogButtonBox(BB.Close)
- self.connect(bb, SIGNAL("accepted()"), self, SLOT("accept()"))- self.connect(bb, SIGNAL("rejected()"), self, SLOT("reject()"))
- layout.addWidget(bb)
- self.bb = bb+self.pmon = cmdui.ProgressMonitor()
+ self.pmon.hide()
+ layout.addWidget(self.pmon)
self.vsplit, self.hsplit = vsplit, hsplit
QTimer.singleShot(0, self.refresh)
defrefresh(self):hglib.invalidaterepo(self.repo)wctx=self.repo[None]wctx.status(unknown=True)self.unrevlist.clear()dests=[]foruinwctx.unknown():dests.append(u)forainwctx.added():ifnotwctx[a].renamed():dests.append(a)forxindests:item=QListWidgetItem(hglib.tounicode(x))item.orig=xself.unrevlist.addItem(item) self.unrevlist.setItemSelected(item, x in self.pats)
self.difftb.clear()
self.pats = []
+ self.pmon.clear_progress() def findRenames(self):
'User pressed "find renames" button'
ifself.threadandself.thread.isRunning():QMessageBox.information(self,_('Search already in progress'),_('Cannot start a new search'))returnulist=[]foriteminself.unrevlist.selectedItems():ulist.append(item.orig)ifnotulist:QMessageBox.information(self,_('No rows selected'),_('Select one or more rows for search'))return pct = self.simslider.value() / 100.0
copies = not self.copycheck.isChecked()
- model = self.matchlv.model()- model.clear() self.findbtn.setEnabled(False)
self.matchbtn.setEnabled(False)
+ self.errorstr = None+ self.matchlv.model().clear() self.thread = RenameSearchThread(self.repo, ulist, pct, copies)
- self.thread.match.connect(model.appendRow)
-#self.thread.error.connect(print)
-#self.thread.progress.connect(print)
+ self.connect(self.thread, SIGNAL('match'), self.rowReceived)
+self.connect(self.thread, SIGNAL('progress'), self.progressReceived)
+self.connect(self.thread, SIGNAL('error'), self.errorReceived)
self.thread.searchComplete.connect(self.finished)
self.thread.start()
def finished(self):
+ self.pmon.fillup_progress()+ if self.errorstr:+ self.pmon.set_text(self.errorstr)+ else:+ self.pmon.hide() for col in xrange(3):
self.matchlv.resizeColumnToContents(col)
self.findbtn.setEnabled(True)
- self.matchbtn.setDisabled(model.isEmpty())
+ self.matchbtn.setDisabled(self.matchlv.model().isEmpty())
++ def rowReceived(self, args):+ self.matchlv.model().appendRow(*args)++ def errorReceived(self, qstr):+ self.errorstr = qstr+ self.pmon.set_text(qstr)++ def progressReceived(self, data):+ if self.thread.isFinished():+ return+ self.pmon.show()+ counting = False+ topic, item, pos, total, unit = data+ if pos is None:+ self.pmon.clear_progress()+ return+ if total is None:+ count = '%d' % pos+ counting = True+ else:+ self.pmon.pbar.setMaximum(total)+ self.pmon.pbar.setValue(pos)+ count = '%d / %d' % (pos, total)+ if unit:+ count += ' ' + unit+ self.pmon.prog_label.setText(hglib.tounicode(count))+ if item:+ status = '%s: %s' % (topic, item)+ else:+ status = _('Status: %s') % topic+ self.pmon.status_label.setText(hglib.tounicode(status))+ self.pmon.inprogress = True++ if not self.pmon.inprogress or counting:+ # use indeterminate mode+ self.pmon.pbar.setMinimum(0) def acceptMatch(self):
'User pressed "accept match" button'
hglib.invalidaterepo(self.repo)
- sel = self.matchlv.selectionModel()- for index in sel.selectedIndexes():
+ for index in self.matchlv.selectedIndexes():
src, dest, percent = self.matchlv.model().getRow(index)
if not os.path.exists(self.repo.wjoin(src)):
# Mark missing rename source as removed
self.repo.remove([src])self.repo.copy(src,dest)shlib.shell_notify([self.repo.wjoin(src),self.repo.wjoin(dest)])# Mark all rows with this target file as non-sensitive#for row in self.matchlv.model().getRows():# if row[1] == dest:# row[5] = Falseself.matchAccepted.emit()self.refresh()defshowDiff(self,index):'User selected a row in the candidate tree'hglib.invalidaterepo(self.repo)ctx=self.repo['.']hu=htmlui.htmlui()row=self.matchlv.model().getRow(index)src,dest,percent=self.matchlv.model().getRow(index)aa=self.repo.wread(dest)rr=ctx.filectx(src).data()opts=mdiff.defaultoptsdifftext=mdiff.unidiff(rr,'',aa,'',src,dest,None,opts=opts)ifnotdifftext:t=_('%s and %s have identical contents\n\n')%(src,dest)hu.write(t,label='ui.error')else:fort,linqtlib.difflabel(difftext.splitlines,True):hu.write(t,label=l)self.difftb.setHtml(hu.getdata()[0])defaccept(self):s=QSettings()s.setValue('guess/geom',self.saveGeometry())s.setValue('guess/vsplit-state',self.vsplit.saveState())s.setValue('guess/hsplit-state',self.hsplit.saveState())s.setValue('guess/simslider',self.simslider.value())QDialog.accept(self)defreject(self):ifself.threadandself.thread.isRunning():self.thread.terminate()# This can lockup, so stop waiting after 2secself.thread.wait(2000)self.finished()self.thread=Noneelse:s=QSettings()s.setValue('guess/geom',self.saveGeometry())s.setValue('guess/vsplit-state',self.vsplit.saveState())s.setValue('guess/hsplit-state',self.hsplit.saveState())s.setValue('guess/simslider',self.simslider.value())QDialog.reject(self)classMatchModel(QAbstractTableModel):def__init__(self,parent=None):QAbstractTableModel.__init__(self,parent)self.rows=[]self.headers=(_('Source'),_('Dest'),_('% Match'))defrowCount(self,parent):returnlen(self.rows)defcolumnCount(self,parent):returnlen(self.headers)defdata(self,index,role):ifnotindex.isValid():returnQVariant()ifrole==Qt.DisplayRole:s=self.rows[index.row()][index.column()]returnQVariant(hglib.tounicode(s))''' elif role == Qt.TextColorRole: src, dst, pct = self.rows[index.row()] if pct == 1.0: return QColor('green') else: return QColor('black') elif role == Qt.ToolTipRole: # explain what row means? '''returnQVariant()defheaderData(self,col,orientation,role):ifrole!=Qt.DisplayRoleororientation!=Qt.Horizontal:returnQVariant()else:returnQVariant(self.headers[col])defflags(self,index):returnQt.ItemIsSelectable|Qt.ItemIsEnabled# Custom methodsdefgetRow(self,index):assertindex.isValid()returnself.rows[index.row()] def appendRow(self, *args):
self.beginInsertRows(QModelIndex(), len(self.rows), len(self.rows))
- vals = [str(a) for a in args] # PyQt is upgrading to QString- self.rows.append(vals)
+ self.rows.append(args)
self.endInsertRows()
self.emit(SIGNAL("dataChanged()"))
defclear(self):self.beginRemoveRows(QModelIndex(),0,len(self.rows)-1)self.rows=[]self.endRemoveRows()self.emit(SIGNAL("dataChanged()"))defsort(self,col,order):self.emit(SIGNAL("layoutAboutToBeChanged()"))ifcol==COL_PATH:c=self.checkedself.rows.sort(lambdax,y:cmp(c[x[col]],c[y[col]]))else:self.rows.sort(lambdax,y:cmp(x[col],y[col]))iforder==Qt.DescendingOrder:self.rows.reverse()self.emit(SIGNAL("layoutChanged()"))self.reset()defisEmpty(self):returnnotbool(self.rows)class RenameSearchThread(QThread):
'''Background thread for searching repository history'''
- match = pyqtSignal(str, str, str)- error = pyqtSignal(QString)- progress = pyqtSignal() searchComplete = pyqtSignal()
def __init__(self, repo, ufiles, minpct, copies):
super(RenameSearchThread,self).__init__()self.repo=repoself.ufiles=ufilesself.minpct=minpct self.copies = copies
def run(self):
+ class ProgUi(ui.ui):+ def __init__(self, src=None):+ super(ProgUi, self).__init__(src)+ self.setconfig('ui', 'interactive', 'off')+ self.setconfig('progress', 'disable', 'True')+ os.environ['TERM'] = 'dumb'+ if src:+ self.sig = src.sig+ else:+ self.sig = QObject() # dummy object to emit signals+ def progress(self, topic, pos, item='', unit='', total=None):+ self.sig.emit(SIGNAL('progress'),+ [topic, item, pos, total, unit])+ progui = ProgUi()+ self.connect(progui.sig, SIGNAL('progress'), self.progress)+ storeui = self.repo.ui+ self.progui = progui+ self.repo.ui = progui try:
self.search(self.repo)
except Exception, e:
- self.error.emit(hglib.tounicode(str(e)))
- print e+ self.emit(SIGNAL('error'), hglib.tounicode(str(e)))
+self.repo.ui=storeui self.searchComplete.emit()
+ def progress(self, wr):+ self.emit(SIGNAL('progress'), wr)+ def search(self, repo):
hglib.invalidaterepo(repo)
wctx = repo[None]
pctx=repo['.']ifself.copies:wctx.status(clean=True)srcs=wctx.removed()+wctx.deleted()srcs+=wctx.modified()+wctx.clean()else:srcs=wctx.removed()+wctx.deleted()added=[wctx[a]forainself.ufiles]removed=[pctx[a]forainsrcsifainpctx]# do not consider files of zero length added = sorted([fctx for fctx in added if fctx.size() > 0])
removed = sorted([fctx for fctx in removed if fctx.size() > 0])
exacts = []
- for o, n in similar._findexactmatches(repo, added, removed):
+ gen = similar._findexactmatches(repo, added, removed)+ for o, n in gen:
old, new = o.path(), n.path()
exacts.append(old)
- self.match.emit(old, new, '100%')
+ self.emit(SIGNAL('match'), [old, new, '100%'])
+ if self.minpct == 1.0:+ return removed = [r for r in removed if r.path() not in exacts]
-if self.minpct < 1.0:- for o, n, s in similar._findsimilarmatches(repo, added, removed,- self.minpct):
- old, new = o.path(), n.path()
- self.match.emit(old, new, '%d%%' % (s*100))
+gen = similar._findsimilarmatches(repo, added, removed, self.minpct)+ for o, n, s in gen:
+ old, new, sim = o.path(), n.path(), '%d%%' % (s*100)
+ self.emit(SIGNAL('match'), [old, new, sim])
def run(ui, *pats, **opts):
return DetectRenameDialog(None, None, *pats)
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.