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.
stable
backout: add support for backing out merge revisions
When the dialog detects that the revision being backed out is a merge revision, two radio buttons are added to the dialog, which let the user select the parent of the merge revision that will be backed out to (i.e. whose changes will be kept).
# backout.py - Backout dialog for TortoiseHg## Copyright 2010 Yuki KODAMA <endflow.net@gmail.com>## This software may be used and distributed according to the terms of the# GNU General Public License version 2, incorporated herein by reference.frommercurialimporthg,mergeasmergemodfromtortoisehg.utilimporthglibfromtortoisehg.hgqt.i18nimport_fromtortoisehg.hgqtimportqtlib,csinfo,i18n,cmdui,status,resolvefromtortoisehg.hgqtimportqscilib,thgrepo,messageentryfromPyQt4.QtCoreimport*fromPyQt4.QtGuiimport*classBackoutDialog(QWizard):def__init__(self,rev,repo,parent):super(BackoutDialog,self).__init__(parent)f=self.windowFlags()self.setWindowFlags(f&~Qt.WindowContextHelpButtonHint) self.backoutrev = rev
self.parentbackout = False
+ self.backoutmergeparentrev = None self.setWindowTitle(_('Backout - %s') % repo.displayname)
self.setWindowIcon(qtlib.geticon('hg-revert'))
self.setOption(QWizard.NoBackButtonOnStartPage,True)self.setOption(QWizard.NoBackButtonOnLastPage,True)self.setOption(QWizard.IndependentPages,True)self.addPage(SummaryPage(repo,self))self.addPage(BackoutPage(repo,self))self.addPage(CommitPage(repo,self))self.addPage(ResultPage(repo,self))self.currentIdChanged.connect(self.pageChanged)self.resize(QSize(700,489).expandedTo(self.minimumSizeHint()))repo.repositoryChanged.connect(self.repositoryChanged)repo.configChanged.connect(self.configChanged)defrepositoryChanged(self):self.currentPage().repositoryChanged()defconfigChanged(self):self.currentPage().configChanged()defpageChanged(self,id):ifid!=-1:self.currentPage().currentPage()defreject(self):ifself.currentPage().canExit():super(BackoutDialog,self).reject()classBasePage(QWizardPage):def__init__(self,repo,parent):super(BasePage,self).__init__(parent)self.repo=repodefvalidatePage(self):'user pressed NEXT button, can we proceed?'returnTruedefisComplete(self):'should NEXT button be sensitive?'returnTruedefrepositoryChanged(self):'repository has detected a change to changelog or parents'passdefconfigChanged(self):'repository has detected a change to config files'passdefcurrentPage(self):passdefcanExit(self):returnTrueclassSummaryPage(BasePage):def__init__(self,repo,parent):super(SummaryPage,self).__init__(repo,parent)self.clean=Falseself.th=NonedefinitializePage(self):ifself.layout():returnself.setTitle(_('Prepare to backout'))self.setSubTitle(_('Verify backout revision and ensure your working ''directory is clean.'))self.setLayout(QVBoxLayout())repo=self.repotry:bctx=repo[self.wizard().backoutrev]pctx=repo['.']excepterror.RepoLookupError:qtlib.InfoMsgBox(_('Unable to backout'),_('Backout revision not found'))QTimer.singleShot(0,self.wizard().close)ifpctx==bctx:lbl=_('Backing out a parent revision is a single step operation')self.layout().addWidget(QLabel(u'<b>%s</b>'%lbl))self.wizard().parentbackout=Trueop1,op2=repo.dirstate.parents()a=repo.changelog.ancestor(op1,bctx.node())ifa!=bctx.node():qtlib.InfoMsgBox(_('Unable to backout'),_('Cannot backout change on a different branch'))QTimer.singleShot(0,self.wizard().close)## backout revisionstyle=csinfo.panelstyle(contents=csinfo.PANEL_DEFAULT)create=csinfo.factory(repo,None,style,withupdate=True)sep=qtlib.LabeledSeparator(_('Backout revision'))self.layout().addWidget(sep)backoutCsInfo=create(bctx.rev())self.layout().addWidget(backoutCsInfo)## current revisioncontents=('ishead',)+csinfo.PANEL_DEFAULTstyle=csinfo.panelstyle(contents=contents)defmarkup_func(widget,item,value):ifitem=='ishead'andvalueisFalse:text=_('Not a head, backout will create a new head!')returnqtlib.markup(text,fg='red',weight='bold')raisecsinfo.UnknownItem(item)custom=csinfo.custom(markup=markup_func)create=csinfo.factory(repo,custom,style,withupdate=True)sep=qtlib.LabeledSeparator(_('Current local revision'))self.layout().addWidget(sep)localCsInfo=create(pctx.rev()) self.layout().addWidget(localCsInfo)
self.localCsInfo = localCsInfo
+ ## Merge revision backout handling+ if len(bctx.parents()) > 1:+ # Show two radio buttons letting the user which merge revision+ # parent to backout to+ p1rev = bctx.p1().rev()+ p2rev = bctx.p2().rev()++ def setBackoutMergeParentRev(rev):+ self.wizard().backoutmergeparentrev = rev++ setBackoutMergeParentRev(p1rev)++ sep = qtlib.LabeledSeparator(_('Merge parent to backout to'))+ self.layout().addWidget(sep)+ self.layout().addWidget(QLabel(+ _('To backout a <b>merge</b> revision you must select which '+ 'parent to backout to '+ '(i.e. whose changes will be <i>kept</i>)')))++ self.actionFirstParent = QRadioButton(+ _('First Parent: revision %s (%s)') \+ % (p1rev, str(bctx.p1())), self)+ self.actionFirstParent.setCheckable(True)+ self.actionFirstParent.setChecked(True)+ self.actionFirstParent.setShortcut('CTRL+1')+ self.actionFirstParent.setToolTip(+ _('Backout to the first parent of the merge revision'))+ self.actionFirstParent.clicked.connect(+ lambda: setBackoutMergeParentRev(p1rev))++ self.actionSecondParent = QRadioButton(+ _('Second Parent: revision %s (%s)')+ % (p2rev, str(bctx.p2())), self)+ self.actionSecondParent.setCheckable(True)+ self.actionSecondParent.setShortcut('CTRL+2')+ self.actionSecondParent.setToolTip(+ _('Backout to the second parent of the merge revision'))+ self.actionSecondParent.clicked.connect(+ lambda: setBackoutMergeParentRev(p2rev))++ self.layout().addWidget(self.actionFirstParent)+ self.layout().addWidget(self.actionSecondParent)+ ## working directory status
sep = qtlib.LabeledSeparator(_('Working directory status'))
self.layout().addWidget(sep)
self.groups=qtlib.WidgetGroups()wdbox=QHBoxLayout()self.layout().addLayout(wdbox)self.wd_status=qtlib.StatusLabel()self.wd_status.set_status(_('Checking...'))wdbox.addWidget(self.wd_status)wd_prog=QProgressBar()wd_prog.setMaximum(0)wd_prog.setTextVisible(False)self.groups.add(wd_prog,'prog')wdbox.addWidget(wd_prog,1)text=_('Before backout, you must <a href="commit"><b>commit</b></a>, ''<a href="shelve"><b>shelve</b></a> to patch, ''or <a href="discard"><b>discard</b></a> changes.')wd_text=QLabel(text)wd_text.setWordWrap(True)wd_text.linkActivated.connect(self.onLinkActivated)self.wd_text=wd_textself.groups.add(wd_text,'dirty')self.layout().addWidget(wd_text)## auto-resolveautoresolve_chk=QCheckBox(_('Automatically resolve merge conflicts ''where possible'))autoresolve_chk.setChecked(repo.ui.configbool('tortoisehg','autoresolve',False))self.registerField('autoresolve',autoresolve_chk)self.layout().addWidget(autoresolve_chk)self.autoresolve_chk=autoresolve_chkself.groups.set_visible(False,'dirty')defisComplete(self):'should Next button be sensitive?'returnself.cleandefrepositoryChanged(self):'repository has detected a change to changelog or parents'pctx=self.repo['.']self.localCsInfo.update(pctx)self.wizard().localrev=str(pctx.rev())defcanExit(self):'can backout tool be closed?'ifself.thisnotNoneandself.th.isRunning():self.th.cancel()self.th.wait()returnTruedefcurrentPage(self):self.refresh()defrefresh(self):ifself.thisNone:self.th=CheckThread(self.repo,self)self.th.finished.connect(self.threadFinished)ifself.th.isRunning():returnself.groups.set_visible(True,'prog')self.th.start()defthreadFinished(self):self.groups.set_visible(False,'prog')ifself.th.canceled:returndirty,parents=self.th.resultsself.clean=notdirtyifdirty:self.groups.set_visible(True,'dirty')self.wd_status.set_status(_('<b>Uncommitted local changes ''are detected</b>'),'thg-warning')else:self.groups.set_visible(False,'dirty')self.wd_status.set_status(_('Clean'),True)self.completeChanged.emit()@pyqtSlot(QString)defonLinkActivated(self,cmd):cmd=hglib.fromunicode(cmd)repo=self.repoifcmd=='commit':fromtortoisehg.hgqtimportcommitdlg=commit.CommitDialog(repo,[],{},self.wizard())dlg.finished.connect(dlg.deleteLater)dlg.exec_()self.refresh()elifcmd=='shelve':fromtortoisehg.hgqtimportshelvedlg=shelve.ShelveDialog(repo,self.wizard())dlg.finished.connect(dlg.deleteLater)dlg.exec_()self.refresh()elifcmd.startswith('discard'):ifcmd!='discard:noconfirm':labels=[(QMessageBox.Yes,_('&Discard')),(QMessageBox.No,_('Cancel'))]ifnotqtlib.QuestionMsgBox(_('Confirm Discard'),_('Discard outstanding changes to working directory?'),labels=labels,parent=self):returndeffinished(ret):repo.decrementBusyCount()self.refresh()cmdline=['update','--clean','--repository',repo.root,'--rev','.']self.runner=cmdui.Runner(True,self)self.runner.commandFinished.connect(finished)repo.incrementBusyCount()self.runner.run(cmdline)elifcmd=='view':dlg=status.StatusDialog(repo,[],{},self)dlg.exec_()self.refresh()else:raise'unknown command: %s'%cmdclassBackoutPage(BasePage):def__init__(self,repo,parent):super(BackoutPage,self).__init__(repo,parent)self.backoutcomplete=Falseself.setTitle(_('Backing out, then merging...'))self.setSubTitle(_('All conflicting files will be marked unresolved.'))self.setLayout(QVBoxLayout())self.cmd=cmdui.Widget(True,False,self)self.cmd.commandFinished.connect(self.onCommandFinished)self.cmd.setShowOutput(True)self.layout().addWidget(self.cmd)self.reslabel=QLabel()self.reslabel.linkActivated.connect(self.onLinkActivated)self.reslabel.setWordWrap(True)self.layout().addWidget(self.reslabel)self.autonext=QCheckBox(_('Automatically advance to next page ''when backout and merge are complete.'))checked=QSettings().value('backout/autoadvance',False).toBool()self.autonext.setChecked(checked)self.autonext.toggled.connect(self.tryAutoAdvance)self.layout().addWidget(self.autonext)defcurrentPage(self):ifself.wizard().parentbackout:self.wizard().next()returncmdline=['--repository',self.repo.root,'backout'] tool = self.field('autoresolve').toBool() and 'merge' or 'fail'
cmdline += ['--tool=internal:' + tool]
cmdline += ['--rev', str(self.wizard().backoutrev)]
+ if self.wizard().backoutmergeparentrev:+ cmdline += ['--parent', str(self.wizard().backoutmergeparentrev)] self.repo.incrementBusyCount()
self.cmd.core.clearOutput()
self.cmd.run(cmdline)
defisComplete(self):'should Next button be sensitive?'ifnotself.backoutcomplete:returnFalsecount=0forroot,path,statusinthgrepo.recursiveMergeStatus(self.repo):ifstatus=='u':count+=1ifcount:# if autoresolve is enabled, we know these were real conflictsself.reslabel.setText(_('%d files have <b>merge conflicts</b> ''that must be <a href="resolve">''<b>resolved</b></a>')%count)returnFalseelse:self.reslabel.setText(_('No merge conflicts, ready to commit'))returnTruedeftryAutoAdvance(self,checked):ifcheckedandself.isComplete():self.wizard().next()defcleanupPage(self):QSettings().setValue('backout/autoadvance',self.autonext.isChecked())defonCommandFinished(self,ret):self.repo.decrementBusyCount()ifretin(0,1):self.backoutcomplete=Trueifself.autonext.isChecked():self.tryAutoAdvance(True)self.completeChanged.emit()@pyqtSlot(QString)defonLinkActivated(self,cmd):ifcmd=='resolve':dlg=resolve.ResolveDialog(self.repo,self)dlg.finished.connect(dlg.deleteLater)dlg.exec_()ifself.autonext.isChecked():self.tryAutoAdvance(True)self.completeChanged.emit()classCommitPage(BasePage):def__init__(self,repo,parent):super(CommitPage,self).__init__(repo,parent)self.commitComplete=Falseself.setTitle(_('Commit backout and merge results'))self.setSubTitle(' ')self.setLayout(QVBoxLayout())self.setCommitPage(True)# csinfodeflabel_func(widget,item,ctx):ifitem=='rev':return_('Revision:')elifitem=='parents':return_('Parents')raisecsinfo.UnknownItem()defdata_func(widget,item,ctx):ifitem=='rev':return_('Working Directory'),str(ctx)elifitem=='parents':parents=[]cbranch=ctx.branch()forpctxinctx.parents():branch=Noneifhasattr(pctx,'branch')andpctx.branch()!=cbranch:branch=pctx.branch()parents.append((str(pctx.rev()),str(pctx),branch,pctx))returnparentsraisecsinfo.UnknownItem()defmarkup_func(widget,item,value):ifitem=='rev':text,rev=valueifself.wizard()andself.wizard().parentbackout:return'%s (%s)'%(text,rev)else:return'<a href="view">%s</a> (%s)'%(text,rev)elifitem=='parents':defbranch_markup(branch):opts=dict(fg='black',bg='#aaffaa')returnqtlib.markup(' %s '%branch,**opts)csets=[]forrnum,rid,branch,pctxinvalue:line='%s (%s)'%(rnum,rid)ifbranch:line='%s%s'%(line,branch_markup(branch))msg=widget.info.get_data('summary',widget,pctx,widget.custom)ifmsg:line='%s%s'%(line,msg)csets.append(line)returncsetsraisecsinfo.UnknownItem()custom=csinfo.custom(label=label_func,data=data_func,markup=markup_func)contents=('rev','user','dateage','branch','parents')style=csinfo.panelstyle(contents=contents,margin=6)# merged filesrev_sep=qtlib.LabeledSeparator(_('Working Directory (merged)'))self.layout().addWidget(rev_sep)bkCsInfo=csinfo.create(repo,None,style,custom=custom,withupdate=True)bkCsInfo.linkActivated.connect(self.onLinkActivated)self.layout().addWidget(bkCsInfo)# commit message areamsg_sep=qtlib.LabeledSeparator(_('Commit message'))self.layout().addWidget(msg_sep)msgEntry=messageentry.MessageEntry(self)msgEntry.installEventFilter(qscilib.KeyPressInterceptor(self))msgEntry.refresh(repo)msgEntry.loadSettings(QSettings(),'backout/message')msgEntry.textChanged.connect(self.completeChanged)self.layout().addWidget(msgEntry)self.msgEntry=msgEntryself.cmd=cmdui.Widget(True,False,self)self.cmd.commandFinished.connect(self.onCommandFinished)self.cmd.setShowOutput(False)self.layout().addWidget(self.cmd)deftryperform():ifself.isComplete():self.wizard().next()actionEnter=QAction('alt-enter',self)actionEnter.setShortcuts([Qt.CTRL+Qt.Key_Return,Qt.CTRL+Qt.Key_Enter])actionEnter.triggered.connect(tryperform)self.addAction(actionEnter)self.skiplast=QCheckBox(_('Skip final confirmation page, ''close after commit.'))checked=QSettings().value('backout/skiplast',False).toBool()self.skiplast.setChecked(checked)self.layout().addWidget(self.skiplast) def eng_toggled(checked):
if self.isComplete():
oldmsg = self.msgEntry.text()
- msgset = i18n.keepgettext()._('Backed out changeset: ')
+ if self.wizard().backoutmergeparentrev:+ msgset = i18n.keepgettext()._(+ 'Backed out merge changeset: ')+ else:+ msgset = i18n.keepgettext()._('Backed out changeset: ')
msg = checked and msgset['id'] or msgset['str']
if oldmsg and oldmsg != msg:
if not qtlib.QuestionMsgBox(_('Confirm Discard Message'),
_('Discard current backout message?'),parent=self):self.engChk.blockSignals(True)self.engChk.setChecked(notchecked)self.engChk.blockSignals(False)returnself.msgEntry.setText(msg+str(self.repo[self.wizard().backoutrev]))self.msgEntry.moveCursorToEnd()self.engChk=QCheckBox(_('Use English backout message'))self.engChk.toggled.connect(eng_toggled)engmsg=self.repo.ui.configbool('tortoisehg','engmsg',False)self.engChk.setChecked(engmsg)self.layout().addWidget(self.engChk)defrefresh(self):passdefcleanupPage(self):s=QSettings()s.setValue('backout/skiplast',self.skiplast.isChecked())self.msgEntry.saveSettings(s,'backout/message') def currentPage(self):
engmsg = self.repo.ui.configbool('tortoisehg', 'engmsg', False)
- msgset = i18n.keepgettext()._('Backed out changeset: ')
+ mergeparentrev = self.wizard().backoutmergeparentrev+ if mergeparentrev:+ msgset = i18n.keepgettext()._(+ 'Backed out merge changeset: ')+ else:+ msgset = i18n.keepgettext()._('Backed out changeset: ')
msg = engmsg and msgset['id'] or msgset['str']
-self.msgEntry.setText(msg + str(self.repo[self.wizard().backoutrev]))+ msg += str(self.repo[self.wizard().backoutrev])
+ if mergeparentrev:+ msg += '\n\n'+ bctx = self.repo[self.wizard().backoutrev]+ isp1 = (bctx.p1().rev() == mergeparentrev)+ if isp1:+ msg += _('Backed out merge revision '+ 'to its first parent (%s)') % str(bctx.p1())+ else:+ msg += _('Backed out merge revision '+ 'to its second parent (%s)') % str(bctx.p2())+ self.msgEntry.setText(msg) self.msgEntry.moveCursorToEnd()
@pyqtSlot(QString)
defonLinkActivated(self,cmd):ifcmd=='view':dlg=status.StatusDialog(self.repo,[],{},self)dlg.exec_()self.refresh()defisComplete(self):returnlen(self.msgEntry.text())>0defvalidatePage(self):ifself.commitComplete:# commit succeeded, repositoryChanged() called wizard().next()ifself.skiplast.isChecked():self.wizard().close()returnTrueifself.cmd.core.running():returnFalseuser=qtlib.getCurrentUsername(self,self.repo)ifnotuser:returnFalseifself.wizard().parentbackout:self.setTitle(_('Backing out and committing...'))self.setSubTitle(_('Please wait while making backout.'))message=hglib.fromunicode(self.msgEntry.text()) cmdline = ['backout', '--verbose', '--message', message, '--rev',
str(self.wizard().backoutrev), '--user', user,
'--repository', self.repo.root]
+ if self.wizard().backoutmergeparentrev:+ cmdline += ['--parent', str(self.wizard().backoutmergeparentrev)] else:
self.setTitle(_('Committing...'))
self.setSubTitle(_('Please wait while committing merged files.'))
message=hglib.fromunicode(self.msgEntry.text())cmdline=['commit','--verbose','--message',message,'--repository',self.repo.root,'--user',user]commandlines=[cmdline]pushafter=self.repo.ui.config('tortoisehg','cipushafter')ifpushafter:cmd=['push','--repository',self.repo.root,pushafter]commandlines.append(cmd)self.repo.incrementBusyCount()self.cmd.setShowOutput(True)self.cmd.run(*commandlines)returnFalsedefonCommandFinished(self,ret):self.repo.decrementBusyCount()ifret==0:self.commitComplete=Trueself.wizard().next()classResultPage(BasePage):def__init__(self,repo,parent):super(ResultPage,self).__init__(repo,parent)self.setTitle(_('Finished'))self.setSubTitle(' ')self.setFinalPage(True)self.setLayout(QVBoxLayout())sep=qtlib.LabeledSeparator(_('Backout changeset'))self.layout().addWidget(sep)bkCsInfo=csinfo.create(self.repo,'tip',withupdate=True)self.layout().addWidget(bkCsInfo)self.bkCsInfo=bkCsInfoself.layout().addStretch(1)defcurrentPage(self):self.bkCsInfo.update(self.repo['tip'])self.wizard().setOption(QWizard.NoCancelButton,True)classCheckThread(QThread):def__init__(self,repo,parent):QThread.__init__(self,parent)self.repo=hg.repository(repo.ui,repo.root)self.results=(False,1)self.canceled=Falsedefrun(self):self.repo.dirstate.invalidate()unresolved=Falseforroot,path,statusinthgrepo.recursiveMergeStatus(self.repo):ifself.canceled:returnifstatus=='u':unresolved=Truebreakwctx=self.repo[None]dirty=bool(wctx.dirty())orunresolvedself.results=(dirty,len(wctx.parents()))defcancel(self):self.canceled=Truedefrun(ui,*pats,**opts):fromtortoisehg.utilimportpathsrepo=thgrepo.repository(ui,path=paths.find_root())ifopts.get('rev'):rev=opts.get('rev')eliflen(pats)==1:rev=pats[0]else:rev='tip'returnBackoutDialog(rev,repo,None)
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.