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