1# rebase.py - Rebase dialog for TortoiseHg 2# 3# Copyright 2010 Steve Borho <steve@borho.org> 4# 5# This software may be used and distributed according to the terms of the 6# GNU General Public License version 2, incorporated herein by reference. 7 8from __future__ import absolute_import 9 10from .qtcore import ( 11 QSettings, 12 QTimer, 13 Qt, 14 pyqtSlot, 15) 16from .qtgui import ( 17 QButtonGroup, 18 QCheckBox, 19 QDialog, 20 QDialogButtonBox, 21 QGroupBox, 22 QLabel, 23 QMessageBox, 24 QRadioButton, 25 QVBoxLayout, 26) 27 28from ..util import hglib 29from ..util.i18n import _ 30from . import ( 31 cmdcore, 32 cmdui, 33 csinfo, 34 qtlib, 35 resolve, 36 thgrepo, 37 wctxcleaner, 38) 39 40if hglib.TYPE_CHECKING: 41 from typing import ( 42 Any, 43 List, 44 Optional, 45 Text, 46 ) 47 from .qtgui import ( 48 QWidget, 49 ) 50 from .thgrepo import ( 51 RepoAgent, 52 ) 53 54 55BB = QDialogButtonBox 56 57_SOURCE_SPECS = [ 58 # name, title, option label 59 ('source', _('Rebase source and descendants'), '-s/--source'), 60 ('base', _('Rebase entire source branch'), '-b/--base'), 61 ('rev', _('Rebase exact revision'), '-r/--rev'), 62] 63 64class RebaseDialog(QDialog): 65 66 def __init__(self, repoagent, parent, **opts): 67 # type: (RepoAgent, Optional[QWidget], Any) -> None 68 super(RebaseDialog, self).__init__(parent) 69 self.setWindowIcon(qtlib.geticon('hg-rebase')) 70 self.setWindowFlags(self.windowFlags() 71 & ~Qt.WindowContextHelpButtonHint) 72 self._repoagent = repoagent 73 self._cmdsession = cmdcore.nullCmdSession() 74 repo = repoagent.rawRepo() 75 self.opts = opts 76 77 box = QVBoxLayout() 78 box.setSpacing(8) 79 box.setContentsMargins(*(6,)*4) 80 self.setLayout(box) 81 82 style = csinfo.panelstyle(selectable=True) 83 84 self._sourcebox = srcb = QGroupBox(self) 85 srcb.setLayout(QVBoxLayout()) 86 srcb.layout().setContentsMargins(*(2,)*4) 87 s = opts.get('source', '.') 88 source = csinfo.create(self.repo, s, style, withupdate=True) 89 srcb.layout().addWidget(source) 90 self.sourcecsinfo = source 91 box.addWidget(srcb) 92 93 destb = QGroupBox(_('To rebase destination')) 94 destb.setLayout(QVBoxLayout()) 95 destb.layout().setContentsMargins(*(2,)*4) 96 d = opts.get('dest', '.') 97 dest = csinfo.create(self.repo, d, style, withupdate=True) 98 destb.layout().addWidget(dest) 99 self.destcsinfo = dest 100 box.addWidget(destb) 101 102 self.swaplabel = QLabel('<a href="X">%s</a>' # don't care href 103 % _('Swap source and destination')) 104 self.swaplabel.linkActivated.connect(self.swap) 105 box.addWidget(self.swaplabel) 106 107 sep = qtlib.LabeledSeparator(_('Options')) 108 box.addWidget(sep) 109 110 self._sourcegroup = QButtonGroup(self) 111 self._sourcegroup.buttonClicked.connect(self._updateSourceSelector) 112 for i, (name, title, oplabel) in enumerate(_SOURCE_SPECS): 113 w = QRadioButton('%s (%s)' % (title, oplabel), self) 114 w.setChecked(name == 'source') 115 self._sourcegroup.addButton(w, i) 116 box.addWidget(w) 117 118 self.keepchk = QCheckBox(_('Keep original changesets (--keep)')) 119 self.keepchk.setChecked(opts.get('keep', False)) 120 box.addWidget(self.keepchk) 121 122 self.keepbrancheschk = QCheckBox(_('Keep original branch names ' 123 '(--keepbranches)')) 124 self.keepbrancheschk.setChecked(opts.get('keepbranches', False)) 125 box.addWidget(self.keepbrancheschk) 126 127 self.collapsechk = QCheckBox(_('Collapse the rebased changesets ' 128 '(--collapse)')) 129 self.collapsechk.setChecked(opts.get('collapse', False)) 130 box.addWidget(self.collapsechk) 131 132 self.autoresolvechk = QCheckBox(_('Automatically resolve merge ' 133 'conflicts where possible')) 134 box.addWidget(self.autoresolvechk) 135 136 self.svnchk = QCheckBox(_('Rebase unpublished onto Subversion head ' 137 '(override source, destination)')) 138 self.svnchk.setVisible(b'hgsubversion' in repo.extensions()) 139 box.addWidget(self.svnchk) 140 141 self._cmdlog = cmdui.LogWidget(self) 142 self._cmdlog.hide() 143 box.addWidget(self._cmdlog, 2) 144 self._stbar = cmdui.ThgStatusBar(self) 145 self._stbar.setSizeGripEnabled(False) 146 self._stbar.linkActivated.connect(self.linkActivated) 147 box.addWidget(self._stbar) 148 149 bbox = QDialogButtonBox() 150 self.cancelbtn = bbox.addButton(QDialogButtonBox.Cancel) 151 self.cancelbtn.clicked.connect(self.reject) 152 self.rebasebtn = bbox.addButton(_('Rebase'), 153 QDialogButtonBox.ActionRole) 154 self.rebasebtn.clicked.connect(self.rebase) 155 self.abortbtn = bbox.addButton(_('Abort'), 156 QDialogButtonBox.ActionRole) 157 self.abortbtn.clicked.connect(self.abort) 158 box.addWidget(bbox) 159 self.bbox = bbox 160 161 self._wctxcleaner = wctxcleaner.WctxCleaner(repoagent, self) 162 self._wctxcleaner.checkFinished.connect(self._onCheckFinished) 163 if self.checkResolve() or not (s or d): 164 for w in (srcb, destb, sep, self.keepchk, 165 self.collapsechk, self.keepbrancheschk): 166 w.setHidden(True) 167 self._cmdlog.show() 168 else: 169 self._stbar.showMessage(_('Checking...')) 170 self.abortbtn.setEnabled(False) 171 self.rebasebtn.setEnabled(False) 172 QTimer.singleShot(0, self._wctxcleaner.check) 173 174 self.setMinimumWidth(480) 175 self.setMaximumHeight(800) 176 self.resize(0, 340) 177 self.setWindowTitle(_('Rebase - %s') % repoagent.displayName()) 178 self._updateSourceSelector() 179 self._readSettings() 180 181 @property 182 def repo(self): 183 return self._repoagent.rawRepo() 184 185 def _readSettings(self): 186 # type: () -> None 187 qs = QSettings() 188 qs.beginGroup('rebase') 189 self.autoresolvechk.setChecked( 190 self._repoagent.configBool('tortoisehg', 'autoresolve', 191 qtlib.readBool(qs, 'autoresolve', True))) 192 qs.endGroup() 193 194 def _writeSettings(self): 195 # type: () -> None 196 qs = QSettings() 197 qs.beginGroup('rebase') 198 qs.setValue('autoresolve', self.autoresolvechk.isChecked()) 199 qs.endGroup() 200 201 @pyqtSlot(bool) 202 def _onCheckFinished(self, clean): 203 # type: (bool) -> None 204 if not clean: 205 self.rebasebtn.setEnabled(False) 206 txt = _('Before rebase, you must ' 207 '<a href="commit"><b>commit</b></a>, ' 208 '<a href="shelve"><b>shelve</b></a> to patch, ' 209 'or <a href="discard"><b>discard</b></a> changes.') 210 else: 211 self.rebasebtn.setEnabled(True) 212 txt = _('You may continue the rebase') 213 self._stbar.showMessage(txt) 214 215 @pyqtSlot() 216 def _updateSourceSelector(self): 217 # type: () -> None 218 _name, title, _oplabel = _SOURCE_SPECS[self._sourcegroup.checkedId()] 219 self._sourcebox.setTitle(title) 220 221 def rebase(self): 222 # type: () -> None 223 self.rebasebtn.setEnabled(False) 224 self.cancelbtn.setVisible(False) 225 self.keepchk.setEnabled(False) 226 self.keepbrancheschk.setEnabled(False) 227 for w in self._sourcegroup.buttons(): 228 w.setEnabled(False) 229 self.collapsechk.setEnabled(False) 230 self.swaplabel.setVisible(False) 231 232 itool = 'merge' if self.autoresolvechk.isChecked() else 'fail' 233 opts = {'config': 'ui.merge=internal:%s' % itool} 234 if hglib.rebase_in_progress(self.repo): 235 opts['continue'] = True 236 else: 237 opts.update({ 238 'keep': self.keepchk.isChecked(), 239 'keepbranches': self.keepbrancheschk.isChecked(), 240 'collapse': self.collapsechk.isChecked(), 241 }) 242 if self.svnchk.isChecked(): 243 opts['svn'] = True 244 else: 245 sourcearg = _SOURCE_SPECS[self._sourcegroup.checkedId()][0] 246 opts[sourcearg] = hglib.tounicode(str(self.opts.get('source'))) 247 opts['dest'] = hglib.tounicode(str(self.opts.get('dest'))) 248 cmdline = hglib.buildcmdargs('rebase', **opts) 249 sess = self._runCommand(cmdline) 250 sess.commandFinished.connect(self._rebaseFinished) 251 252 def swap(self): 253 # type: () -> None 254 oldsource = self.opts.get('source', '.') 255 olddest = self.opts.get('dest', '.') 256 257 self.sourcecsinfo.update(target=olddest) 258 self.destcsinfo.update(target=oldsource) 259 260 self.opts['source'] = olddest 261 self.opts['dest'] = oldsource 262 263 def abort(self): 264 # type: () -> None 265 cmdline = hglib.buildcmdargs('rebase', abort=True) 266 sess = self._runCommand(cmdline) 267 sess.commandFinished.connect(self._abortFinished) 268 269 def _runCommand(self, cmdline): 270 # type: (List[Text]) -> cmdcore.CmdSession 271 assert self._cmdsession.isFinished() 272 self._cmdsession = sess = self._repoagent.runCommand(cmdline, self) 273 sess.commandFinished.connect(self._stbar.clearProgress) 274 sess.outputReceived.connect(self._cmdlog.appendLog) 275 sess.progressReceived.connect(self._stbar.setProgress) 276 cmdui.updateStatusMessage(self._stbar, sess) 277 return sess 278 279 @pyqtSlot(int) 280 def _rebaseFinished(self, ret): 281 # type: (int) -> None 282 # TODO since hg 2.6, rebase will end with ret=1 in case of "unresolved 283 # conflicts", so we can fine-tune checkResolve() later. 284 if self.checkResolve() is False: 285 msg = _('Rebase is complete') 286 if ret == 255: 287 msg = _('Rebase failed') 288 self._cmdlog.show() # contains hint 289 self._stbar.showMessage(msg) 290 self._makeCloseButton() 291 292 @pyqtSlot() 293 def _abortFinished(self): 294 # type: () -> None 295 if self.checkResolve() is False: 296 self._stbar.showMessage(_('Rebase aborted')) 297 self._makeCloseButton() 298 299 def _makeCloseButton(self): 300 # type: () -> None 301 self.rebasebtn.setEnabled(True) 302 self.rebasebtn.setText(_('Close')) 303 self.rebasebtn.clicked.disconnect(self.rebase) 304 self.rebasebtn.clicked.connect(self.accept) 305 306 def checkResolve(self): 307 # type: () -> bool 308 for root, path, status in thgrepo.recursiveMergeStatus(self.repo): 309 if status == b'u': 310 txt = _('Rebase generated merge <b>conflicts</b> that must ' 311 'be <a href="resolve"><b>resolved</b></a>') 312 self.rebasebtn.setEnabled(False) 313 break 314 else: 315 self.rebasebtn.setEnabled(True) 316 txt = _('You may continue the rebase') 317 self._stbar.showMessage(txt) 318 319 if hglib.rebase_in_progress(self.repo): 320 self.swaplabel.setVisible(False) 321 self.abortbtn.setEnabled(True) 322 self.rebasebtn.setText('Continue') 323 return True 324 else: 325 self.abortbtn.setEnabled(False) 326 return False 327 328 def linkActivated(self, cmd): 329 # type: (Text) -> None 330 if cmd == 'resolve': 331 dlg = resolve.ResolveDialog(self._repoagent, self) 332 dlg.exec_() 333 self.checkResolve() 334 else: 335 self._wctxcleaner.runCleaner(cmd) 336 337 def reject(self): 338 # type: () -> None 339 if hglib.rebase_in_progress(self.repo): 340 main = _('Exiting with an unfinished rebase is not recommended.') 341 text = _('Consider aborting the rebase first.') 342 labels = ((QMessageBox.Yes, _('&Exit')), 343 (QMessageBox.No, _('Cancel'))) 344 if not qtlib.QuestionMsgBox(_('Confirm Exit'), main, text, 345 labels=labels, parent=self): 346 return 347 super(RebaseDialog, self).reject() 348 349 def done(self, r): 350 # type: (int) -> None 351 self._writeSettings() 352 super(RebaseDialog, self).done(r) 353