This instance will be upgraded to Heptapod 0.21.0rc2 on 2021-04-16 at 11:00 UTC+2 (a few minutes of down time)

commit.py 59.4 KB
Newer Older
1 2 3 4 5 6 7
# commit.py - TortoiseHg's commit widget and standalone dialog
#
# 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.

8 9
from __future__ import absolute_import

10
import os
11
import re
12
import tempfile
13
import time
14

15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
from .qsci import (
    QsciAPIs,
)
from .qtcore import (
    QSettings,
    QSize,
    QTimer,
    Qt,
    pyqtSignal,
    pyqtSlot,
)
from .qtgui import (
    QAction,
    QActionGroup,
    QCheckBox,
    QComboBox,
    QDialog,
    QDialogButtonBox,
    QFont,
    QFrame,
    QHBoxLayout,
    QKeySequence,
    QLabel,
    QLineEdit,
    QMenu,
    QMessageBox,
    QPushButton,
    QShortcut,
    QSizePolicy,
    QStyle,
    QStyleFactory,
    QStyleOptionToolButton,
    QSplitter,
    QToolBar,
    QToolButton,
    QVBoxLayout,
    QWidget,
)
53

54 55 56 57
from mercurial import (
    error,
    obsolete,  # delete if obsolete becomes enabled by default
    phases,
58
    pycompat,
59
    scmutil,
60
)
61 62 63
from mercurial.utils import (
    dateutil,
)
64

65 66 67 68 69 70 71 72 73
from ..util import (
    hglib,
    i18n,
    shlib,
    wconfig,
)
from ..util.i18n import _
from . import (
    branchop,
74
    csinfo,
75 76 77 78 79 80 81 82 83 84 85
    cmdcore,
    cmdui,
    hgrcutil,
    lfprompt,
    qscilib,
    qtlib,
    revpanel,
    status,
    thgrepo,
)
from .messageentry import MessageEntry
86

87
if os.name == 'nt':
88
    from ..util import bugtraq
89 90 91
    _hasbugtraq = True
else:
    _hasbugtraq = False
92

93 94 95 96 97 98
def readopts(repoagent):
    opts = {
        'ciexclude': repoagent.configString('tortoisehg', 'ciexclude'),
        'pushafter': repoagent.configString('tortoisehg', 'cipushafter'),
        'autoinc': repoagent.configString('tortoisehg', 'autoinc'),
        'recurseinsubrepos': repoagent.configBool('tortoisehg', 'recurseinsubrepos'),
99 100 101 102
        'bugtraqplugin': repoagent.configString('tortoisehg',
                                                'issue.bugtraqplugin'),
        'bugtraqparameters': repoagent.configString('tortoisehg',
                                                    'issue.bugtraqparameters')
103
    }
104
    if opts['bugtraqparameters']:
Yuya Nishihara's avatar
Yuya Nishihara committed
105 106
        opts['bugtraqparameters'] = os.path.expandvars(
            opts['bugtraqparameters'])
107
    opts['bugtraqtrigger'] = repoagent.configString('tortoisehg', 'issue.bugtraqtrigger')
108 109 110 111 112

    return opts

def commitopts2str(opts, mode='commit'):
    optslist = []
113
    for opt, value in opts.items():
114 115 116 117 118 119 120 121 122 123 124
        if opt in ['user', 'date', 'pushafter', 'autoinc',
                   'recurseinsubrepos']:
            if mode == 'merge' and opt == 'autoinc':
                # autoinc does not apply to merge commits
                continue
            if value is True:
                optslist.append('--' + opt)
            elif value:
                optslist.append('--%s=%s' % (opt, value))
    return ' '.join(optslist)

125 126
def mergecommitmessage(repo):
    wctx = repo[None]
127
    engmsg = repo.ui.configbool(b'tortoisehg', b'engmsg')
128 129 130
    if wctx.p1().branch() == wctx.p2().branch():
        msgset = i18n.keepgettext()._('Merge')
        text = engmsg and msgset['id'] or msgset['str']
131
        text = pycompat.unicode(text)
132 133 134
    else:
        msgset = i18n.keepgettext()._('Merge with %s')
        text = engmsg and msgset['id'] or msgset['str']
135
        text = pycompat.unicode(text) % hglib.tounicode(wctx.p2().branch())
136 137
    return text

138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
def _getUserOptions(opts, *optionlist):
    out = []
    for opt in optionlist:
        if opt not in opts:
            continue
        val = opts[opt]
        if val is False:
            continue
        elif val is True:
            out.append('--' + opt)
        else:
            out.append('--' + opt)
            out.append(val)
    return out

def _mqNewRefreshCommand(repo, isnew, stwidget, pnwidget, message, opts, olist):
    if isnew:
        name = hglib.fromunicode(pnwidget.text())
        if not name:
            qtlib.ErrorMsgBox(_('Patch Name Required'),
                              _('You must enter a patch name'))
            pnwidget.setFocus()
            return
        cmdline = ['qnew', name]
    else:
        cmdline = ['qrefresh']
    if message:
        cmdline += ['--message=' + hglib.fromunicode(message)]
    cmdline += _getUserOptions(opts, *olist)
    files = ['--'] + [repo.wjoin(x) for x in stwidget.getChecked()]
    addrem = [repo.wjoin(x) for x in stwidget.getChecked('!?')]
    if len(files) > 1:
        cmdline += files
    else:
        cmdline += ['--exclude', repo.root]
    if addrem:
        cmdlines = [['addremove'] + addrem, cmdline]
    else:
        cmdlines = [cmdline]
    return cmdlines

179 180 181 182 183 184 185 186
_topicmap = {
    'amend': _('Commit', 'start progress'),
    'commit': _('Commit', 'start progress'),
    'qnew': _('MQ Action', 'start progress'),
    'qref': _('MQ Action', 'start progress'),
    'rollback': _('Rollback', 'start progress'),
    }

Steve Borho's avatar
Steve Borho committed
187
# Technical Debt for CommitWidget
188
#  disable commit button while no message is entered or no files are selected
Steve Borho's avatar
Steve Borho committed
189
#  qtlib decode failure dialog (ask for retry locale, suggest HGENCODING)
Steve Borho's avatar
Steve Borho committed
190 191 192
#  spell check / tab completion
#  in-memory patching / committing chunk selected files

193
class CommitWidget(QWidget, qtlib.TaskWidget):
194
    'A widget that encompasses a StatusWidget and commit extras'
195
    commitButtonEnable = pyqtSignal(bool)
196
    linkActivated = pyqtSignal(str)
197 198
    showMessage = pyqtSignal(str)
    grepRequested = pyqtSignal(str, dict)
199
    runCustomCommandRequested = pyqtSignal(str, list)
200 201
    commitComplete = pyqtSignal()

202
    progress = pyqtSignal(str, object, str, str, object)
203

204
    def __init__(self, repoagent, pats, opts, parent=None, rev=None):
205
        QWidget.__init__(self, parent)
206

207 208 209 210
        repoagent.configChanged.connect(self.refresh)
        repoagent.repositoryChanged.connect(self.repositoryChanged)
        self._repoagent = repoagent
        repo = repoagent.rawRepo()
211
        self._cmdsession = cmdcore.nullCmdSession()
212
        self._rev = rev
213
        self.lastAction = None
214
        # Dictionary storing the last (commit message, modified flag)
215 216
        # 'commit' is used for 'commit' and 'qnew', while
        # 'amend' is used for 'amend' and 'qrefresh'
217
        self.lastCommitMsgs = {'commit': ('', False), 'amend': ('', False)}
218
        self.currentAction = None
219

220
        self.opts = opts = readopts(repoagent) # user, date
221

222
        self.stwidget = status.StatusWidget(repoagent, pats, opts, self)
223
        self.stwidget.showMessage.connect(self.showMessage)
224
        self.stwidget.progress.connect(self.progress)
225
        self.stwidget.linkActivated.connect(self.linkActivated)
226
        self.stwidget.fileDisplayed.connect(self.fileDisplayed)
227
        self.stwidget.grepRequested.connect(self.grepRequested)
228 229
        self.stwidget.runCustomCommandRequested.connect(
            self.runCustomCommandRequested)
230
        self.msghistory = []
231

232
        layout = QVBoxLayout()
233
        layout.setContentsMargins(2, 2, 2, 2)
234
        layout.setSpacing(0)
235 236
        layout.addWidget(self.stwidget)
        self.setLayout(layout)
237 238

        vbox = QVBoxLayout()
239
        vbox.setSpacing(0)
240
        vbox.setContentsMargins(*(0,)*4)
241

242
        hbox = QHBoxLayout()
243
        hbox.setContentsMargins(*(0,)*4)
244
        tbar = QToolBar(_("Commit Dialog Toolbar"), self)
245
        tbar.setStyleSheet(qtlib.tbstylesheet)
246 247 248
        hbox.addWidget(tbar)

        self.branchbutton = tbar.addAction(_('Branch: '))
249 250 251
        font = self.branchbutton.font()
        font.setBold(True)
        self.branchbutton.setFont(font)
252
        self.branchbutton.triggered.connect(self.branchOp)
253
        self.branchop = None
254

255 256
        self.recentMessagesButton = QToolButton(
            text=_('Copy message'),
257
            popupMode=QToolButton.InstantPopup,
258
            toolTip=_('Copy one of the recent commit messages'))
259 260 261
        m = QMenu(self.recentMessagesButton)
        m.triggered.connect(self.msgSelected)
        self.recentMessagesButton.setMenu(m)
262 263 264
        tbar.addWidget(self.recentMessagesButton)
        self.updateRecentMessages()

265
        tbar.addAction(_('Options')).triggered.connect(self.details)
266
        tbar.setIconSize(qtlib.smallIconSize())
267

268
        if _hasbugtraq and self.opts['bugtraqplugin']:
269 270
            # We create the "Show Issues" button, but we delay its setup
            # because creating the bugtraq object is slow and blocks the GUI,
Yuya Nishihara's avatar
Yuya Nishihara committed
271 272
            # which would result in a noticeable slow down while creating
            # the commit widget
273 274 275 276 277 278 279 280
            self.showIssues = tbar.addAction(_('Show Issues'))
            self.showIssues.setEnabled(False)
            self.showIssues.setToolTip(_('Please wait...'))
            def setupBugTraqButton():
                self.bugtraq = self.createBugTracker()
                try:
                    parameters = self.opts['bugtraqparameters']
                    linktext = self.bugtraq.get_link_text(parameters)
281
                except Exception as e:
282 283 284 285 286 287 288 289 290 291 292
                    tracker = self.opts['bugtraqplugin'].split(' ', 1)[1]
                    errormsg =  _('Failed to load issue tracker \'%s\': %s') \
                                 % (tracker, hglib.tounicode(str(e)))
                    self.showIssues.setToolTip(errormsg)
                    qtlib.ErrorMsgBox(_('Issue Tracker'), errormsg,
                                      parent=self)
                    self.bugtraq = None
                else:
                    # connect UI because we have a valid bug tracker
                    self.commitComplete.connect(self.bugTrackerPostCommit)
                    self.showIssues.setText(linktext)
Yuya Nishihara's avatar
Yuya Nishihara committed
293 294
                    self.showIssues.triggered.connect(
                        self.getBugTrackerCommitMessage)
295 296 297
                    self.showIssues.setToolTip(_('Show Issues...'))
                    self.showIssues.setEnabled(True)
            QTimer.singleShot(100, setupBugTraqButton)
298

299 300 301 302
        self.stopAction = tbar.addAction(_('Stop'))
        self.stopAction.triggered.connect(self.stop)
        self.stopAction.setIcon(qtlib.geticon('process-stop'))
        self.stopAction.setEnabled(False)
303

304
        hbox.addStretch(1)
305

Steve Borho's avatar
Steve Borho committed
306
        vbox.addLayout(hbox, 0)
307
        self.buttonHBox = hbox
308

309
        if b'mq' in self.repo.extensions():
310 311 312 313
            self.hasmqbutton = True
            pnhbox = QHBoxLayout()
            self.pnlabel = QLabel()
            pnhbox.addWidget(self.pnlabel)
314 315 316
            self.pnedit = QLineEdit()
            if hasattr(self.pnedit, 'setPlaceholderText'):  # Qt >= 4.7
                self.pnedit.setPlaceholderText(_('### patch name ###'))
317 318 319 320 321 322 323
            self.pnedit.setMaximumWidth(250)
            pnhbox.addWidget(self.pnedit)
            pnhbox.addStretch()
            vbox.addLayout(pnhbox)
        else:
            self.hasmqbutton = False

324 325 326
        self.optionslabel = QLabel()
        self.optionslabel.setSizePolicy(QSizePolicy.Ignored,
                                        QSizePolicy.Preferred)
327
        vbox.addWidget(self.optionslabel, 0)
328

329
        self.pcsinfo = revpanel.ParentWidget(repo)
330
        vbox.addWidget(self.pcsinfo, 0)
331

332
        msgte = MessageEntry(self, self.stwidget.getChecked)
333
        msgte.installEventFilter(qscilib.KeyPressInterceptor(self))
334 335
        vbox.addWidget(msgte, 1)
        upperframe = QFrame()
336 337

        SP = QSizePolicy
338 339 340
        sp = SP(SP.Expanding, SP.Expanding)
        sp.setHorizontalStretch(1)
        upperframe.setSizePolicy(sp)
341 342 343
        upperframe.setLayout(vbox)

        self.split = QSplitter(Qt.Vertical)
344 345
        if os.name == 'nt':
            self.split.setStyle(QStyleFactory.create('Plastique'))
346 347 348 349
        sp = SP(SP.Expanding, SP.Expanding)
        sp.setHorizontalStretch(1)
        sp.setVerticalStretch(0)
        self.split.setSizePolicy(sp)
350 351
        # Add our widgets to the top of our splitter
        self.split.addWidget(upperframe)
352
        self.split.setCollapsible(0, False)
353 354 355 356 357 358
        # Add status widget document frame below our splitter
        # this reparents the docf from the status splitter
        self.split.addWidget(self.stwidget.docf)

        # add our splitter where the docf used to be
        self.stwidget.split.addWidget(self.split)
359
        self.msgte = msgte
360

361 362 363 364
    @property
    def repo(self):
        return self._repoagent.rawRepo()

365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
    @property
    def rev(self):
        """Return current revision"""
        return self._rev

    def selectRev(self, rev):
        """
        Select the revision that must be set when the dialog is shown again
        """
        self._rev = rev

    @pyqtSlot(int)
    @pyqtSlot(object)
    def setRev(self, rev):
        """Change revision to show"""
        self.selectRev(rev)
        if self.hasmqbutton:
            preferredActionName = self._getPreferredActionName()
            curractionName = self.mqgroup.checkedAction()._name
            if curractionName != preferredActionName:
385
                self.commitSetAction(refresh=True,
386 387 388 389 390 391 392
                    actionName=preferredActionName)

    def _getPreferredActionName(self):
        """Select the preferred action, depending on the selected revision"""
        if not self.hasmqbutton:
            return 'commit'
        else:
393
            pctx = self.repo[b'.']
394
            ispatch = b'qtip' in pctx.tags()
395 396 397 398 399 400 401 402 403 404
            if not ispatch:
                # Set the button to Commit
                return 'commit'
            elif self.rev is None:
                # Set the button to QNew
                return 'qnew'
            else:
                # Set the button to QRefresh
                return 'qref'

405
    def commitSetupButton(self):
kiilerix's avatar
kiilerix committed
406 407
        ispatch = lambda r: b'qtip' in r[b'.'].tags()
        notpatch = lambda r: b'qtip' not in r[b'.'].tags()
408 409 410
        def canamend(r):
            if ispatch(r):
                return False
kiilerix's avatar
kiilerix committed
411
            ctx = r[b'.']
412
            return (ctx.phase() != phases.public) \
413
                and len(r[None].parents()) < 2 \
414 415
                and (obsolete.isenabled(self.repo, obsolete.allowunstableopt)
                     or not ctx.children())
416

417
        acts = [
418
            ('commit', _('Commit changes'), _('Commit'), notpatch),
419
            ('amend', _('Amend current revision'), _('Amend'), canamend),
420
        ]
421
        if self.hasmqbutton:
422 423 424 425 426 427 428
            acts += [
                ('qnew', _('Create a new patch'), _('QNew'), None),
                ('qref', _('Refresh current patch'), _('QRefresh'), ispatch),
            ]
        acts = tuple(acts)

        class CommitToolButton(QToolButton):
429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447
            def styleOption(self):
                opt = QStyleOptionToolButton()
                opt.initFrom(self)
                return opt
            def menuButtonWidth(self):
                style = self.style()
                opt = self.styleOption()
                opt.features = QStyleOptionToolButton.MenuButtonPopup
                rect = style.subControlRect(QStyle.CC_ToolButton, opt,
                                            QStyle.SC_ToolButtonMenu, self)
                return rect.width()
            def setBold(self):
                f = self.font()
                f.setWeight(QFont.Bold)
                self.setFont(f)
            def sizeHint(self):
                # Set the desired width to keep the button from resizing
                return QSize(self._width, QToolButton.sizeHint(self).height())

448 449 450 451
        self.committb = committb = CommitToolButton(self)
        committb.setBold()
        committb.setPopupMode(QToolButton.MenuButtonPopup)
        fmk = lambda s: committb.fontMetrics().width(hglib.tounicode(s[2]))
452 453
        committb._width = (max(pycompat.maplist(fmk, acts))
                           + 4*committb.menuButtonWidth())
454

455
        class CommitButtonMenu(QMenu):
456 457
            def __init__(self, parent, repo):
                self.repo = repo
458
                QMenu.__init__(self, parent)
459 460 461 462 463 464 465 466
            def getActionByName(self, act):
                return [a for a in self.actions() if a._name == act][0]
            def showEvent(self, event):
                for a in self.actions():
                    if a._enablefunc:
                        a.setEnabled(a._enablefunc(self.repo))
                return QMenu.showEvent(self, event)
        self.mqgroup = QActionGroup(self)
467 468
        commitbmenu = CommitButtonMenu(committb, self.repo)
        menurefresh = lambda: self.commitSetAction(refresh=True)
469 470 471 472 473 474 475
        for a in acts:
            action = QAction(a[1], self.mqgroup)
            action._name = a[0]
            action._text = a[2]
            action._enablefunc = a[3]
            action.triggered.connect(menurefresh)
            action.setCheckable(True)
476 477 478 479 480
            commitbmenu.addAction(action)
        committb.setMenu(commitbmenu)
        committb.clicked.connect(self.mqPerformAction)
        self.commitButtonEnable.connect(committb.setEnabled)
        self.commitSetAction(actionName=self._getPreferredActionName())
481 482 483 484
        sc = QShortcut(QKeySequence('Ctrl+Return'), self, self.mqPerformAction)
        sc.setContext(Qt.WidgetWithChildrenShortcut)
        sc = QShortcut(QKeySequence('Ctrl+Enter'), self, self.mqPerformAction)
        sc.setContext(Qt.WidgetWithChildrenShortcut)
485
        return committb
486 487

    @pyqtSlot(bool)
488
    def commitSetAction(self, refresh=False, actionName=None):
489
        allowcs = False
490 491 492 493 494
        if actionName:
            selectedAction = \
                [act for act in self.mqgroup.actions() \
                    if act._name == actionName][0]
            selectedAction.setChecked(True)
495 496
        curraction = self.mqgroup.checkedAction()
        oldpctx = self.stwidget.pctx
497
        pctx = self.repo[b'.']
498 499 500 501
        if curraction._name == 'qnew':
            self.pnlabel.setVisible(True)
            self.pnedit.setVisible(True)
            self.pnedit.setFocus()
502
            pn = time.strftime('%Y-%m-%d_%H-%M-%S')
503
            pn += '_r%d+.diff' % self.repo[b'.'].rev()
504
            self.pnedit.setText(pn)
505 506 507 508
            self.pnedit.selectAll()
            self.stwidget.setPatchContext(None)
            refreshwctx = refresh and oldpctx is not None
        else:
509 510 511
            if self.hasmqbutton:
                self.pnlabel.setVisible(False)
                self.pnedit.setVisible(False)
512
            ispatch = b'qtip' in pctx.tags()
513 514
            def switchAction(action, name):
                action.setChecked(False)
515
                action = self.committb.menu().getActionByName(name)
516 517 518 519 520 521
                action.setChecked(True)
                return action
            if curraction._name == 'qref' and not ispatch:
                curraction = switchAction(curraction, 'commit')
            elif curraction._name == 'commit' and ispatch:
                curraction = switchAction(curraction, 'qref')
522
            if curraction._name in ('qref', 'amend'):
523 524 525 526 527
                refreshwctx = refresh
                self.stwidget.setPatchContext(pctx)
            elif curraction._name == 'commit':
                refreshwctx = refresh and oldpctx is not None
                self.stwidget.setPatchContext(None)
528
                allowcs = len(self.repo[None].parents()) == 1
529 530
        if curraction._name in ('qref', 'amend'):
            if self.lastAction not in ('qref', 'amend'):
531 532 533
                self.lastCommitMsgs['commit'] = (self.msgte.text(),
                                                 self.msgte.isModified())
            if self.lastCommitMsgs['amend'][0]:
534
                self.setMessage(*self.lastCommitMsgs['amend'])
535 536
            elif oldpctx is None or oldpctx.node() != pctx.node():
                # pctx must be refreshed if hash changes
537
                self.setMessage(hglib.tounicode(pctx.description()))
538
        else:
539
            if self.lastAction in ('qref', 'amend'):
540 541
                self.lastCommitMsgs['amend'] = (self.msgte.text(),
                                                self.msgte.isModified())
542
                self.setMessage(*self.lastCommitMsgs['commit'])
543 544
            elif len(self.repo[None].parents()) > 1:
                self.setMessage(mergecommitmessage(self.repo))
545
        if curraction._name == 'amend':
546 547 548
            self.stwidget.defcheck = 'amend'
        else:
            self.stwidget.defcheck = 'commit'
549 550
        self.stwidget.fileview.enableChangeSelection(allowcs)
        if not allowcs:
551
            self.stwidget.partials = {}
552 553
        if refreshwctx:
            self.stwidget.refreshWctx()
554 555 556
        custom = self.pcsinfo.custom.copy()
        custom['isAmend'] = curraction._name == 'amend'
        self.pcsinfo.update(custom=custom)
557
        self.committb.setText(curraction._text)
558 559
        self.lastAction = curraction._name

560
    def getBranchCommandLine(self):
561 562 563
        '''
        Create the command line to change or create the selected branch unless
        it is the selected branch
564

565 566 567 568 569 570 571
        Verify whether a branch exists on a repo. If it doesn't ask the user
        to confirm that it wants to create the branch. If it does and it is not
        the current branch as the user whether it wants to change to that branch.
        Depending on the user input, create the command line which will perform
        the selected action
        '''
        # This function is used both by commit() and mqPerformAction()
572
        repo = self.repo
573 574 575
        commandlines = []
        newbranch = False
        branch = hglib.fromunicode(self.branchop)
576
        if branch in repo.branchmap():
577
            # response: 0=Yes, 1=No, 2=Cancel
578
            if branch in [p.branch() for p in repo[None].parents()]:
579 580
                resp = 0
            else:
581 582
                # TODO: should look up only in branch namespace
                rev = scmutil.revsymbol(repo, branch).rev()
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600
                resp = qtlib.CustomPrompt(_('Confirm Branch Change'),
                    _('Named branch "%s" already exists, '
                      'last used in revision %d\n'
                      ) % (self.branchop, rev),
                    self,
                    (_('Restart &Branch'),
                     _('&Commit to current branch'),
                     _('Cancel')), 2, 2).run()
        else:
            resp = qtlib.CustomPrompt(_('Confirm New Branch'),
                _('Create new named branch "%s" with this commit?\n'
                  ) % self.branchop,
                self,
                (_('Create &Branch'),
                 _('&Commit to current branch'),
                 _('Cancel')), 2, 2).run()
        if resp == 0:
            newbranch = True
601
            commandlines.append(['branch', '--force', branch])
602 603 604
        elif resp == 2:
            return None, False
        return commandlines, newbranch
605

606 607 608 609 610
    @pyqtSlot()
    def mqPerformAction(self):
        curraction = self.mqgroup.checkedAction()
        if curraction._name == 'commit':
            return self.commit()
611 612
        elif curraction._name == 'amend':
            return self.commit(amend=True)
613 614

        # Check if we need to change branch first
615
        wholecmdlines = []  # [[cmd1, ...], [cmd2, ...], ...]
616
        if self.branchop:
617
            cmdlines, newbranch = self.getBranchCommandLine()
618
            if cmdlines is None:
619
                return
620 621
            wholecmdlines.extend(cmdlines)

622
        olist = ('user', 'date')
623 624 625 626 627
        cmdlines = _mqNewRefreshCommand(self.repo,
                                        curraction._name == 'qnew',
                                        self.stwidget, self.pnedit,
                                        self.msgte.text(), self.opts,
                                        olist)
628 629
        if not cmdlines:
            return
630
        wholecmdlines.extend(cmdlines)
631
        self._runCommand(curraction._name, wholecmdlines)
632

633
    @pyqtSlot(str, str)
634
    def fileDisplayed(self, wfile, contents):
635 636 637
        'Status widget is displaying a new file'
        if not (wfile and contents):
            return
638
        if self.msgte.autoCompletionThreshold() <= 0:
639 640 641
            # do not search for tokens if auto completion is disabled
            # pygments has several infinite loop problems we'd like to avoid
            return
642 643 644
        if self.msgte.lexer() is None:
            # qsci will crash if None is passed to QsciAPIs constructor
            return
645
        wfile = pycompat.unicode(wfile)
646 647
        self._apis = QsciAPIs(self.msgte.lexer())
        tokens = set()
648
        for e in self.stwidget.getChecked():
649
            e = hglib.tounicode(e)
650 651
            tokens.add(e)
            tokens.add(os.path.basename(e))
652 653
        tokens.add(wfile)
        tokens.add(os.path.basename(wfile))
654
        try:
655 656 657 658 659 660 661 662
            from pygments.lexers import (  # pytype: disable=import-error
                guess_lexer_for_filename
            )
            from pygments.token import Token  # pytype: disable=import-error
            from pygments.util import (  # pytype: disable=import-error
                ClassNotFound
            )

663
            try:
664
                contents = pycompat.unicode(contents)
665 666
                lexer = guess_lexer_for_filename(wfile, contents)
                for tokentype, value in lexer.get_tokens(contents):
667
                    if tokentype in Token.Name and len(value) > 4:
668
                        tokens.add(value)
669
            except (ClassNotFound, TypeError):
670 671
                pass
        except ImportError:
672 673 674 675 676 677 678 679 680 681
            pass
        for n in sorted(list(tokens)):
            self._apis.add(n)
        self._apis.apiPreparationFinished.connect(self.apiPrepFinished)
        self._apis.prepare()

    def apiPrepFinished(self):
        'QsciAPIs has finished parsing displayed file'
        self.msgte.lexer().setAPIs(self._apis)

682
    def bugTrackerPostCommit(self):
683
        if not _hasbugtraq or self.opts['bugtraqtrigger'] != 'commit':
684
            return
685 686 687
        # commit already happened, get last message in history
        message = self.lastmessage
        error = self.bugtraq.on_commit_finished(message)
688
        if error:
689 690 691 692 693 694 695 696 697 698 699
            qtlib.ErrorMsgBox(_('Issue Tracker'), error, parent=self)
        # recreate bug tracker to get new COM object for next commit
        self.bugtraq = self.createBugTracker()

    def createBugTracker(self):
        bugtraqid = self.opts['bugtraqplugin'].split(' ', 1)[0]
        result = bugtraq.BugTraq(bugtraqid)
        return result

    def getBugTrackerCommitMessage(self):
        parameters = self.opts['bugtraqparameters']
700
        message = self.getMessage(True)
701 702 703
        newMessage = self.bugtraq.get_commit_message(parameters, message)
        self.setMessage(newMessage)

704
    def details(self):
705
        mode = 'commit'
706
        if len(self.repo[None].parents()) > 1:
707
            mode = 'merge'
708 709
        dlg = DetailsDialog(self._repoagent, self.opts, self.userhist, self,
                            mode=mode)
710
        dlg.finished.connect(dlg.deleteLater)
711 712
        dlg.setWindowFlags(Qt.Sheet)
        dlg.setWindowModality(Qt.WindowModal)
713 714
        if dlg.exec_() == QDialog.Accepted:
            self.opts.update(dlg.outopts)
715
            self.refresh()
716

717 718 719 720
    @pyqtSlot(int)
    def repositoryChanged(self, flags):
        if flags & thgrepo.WorkingParentChanged:
            self._refreshWorkingState()
721 722
        elif flags & thgrepo.WorkingBranchChanged:
            self.refresh()
723 724

    def _refreshWorkingState(self):
725 726 727 728 729
        curraction = self.mqgroup.checkedAction()
        if curraction._name == 'commit' and not self.msgte.isModified():
            # default merge or close-branch message is outdated if new commit
            # was made by other widget or process
            self.msgte.clear()
730
        self.lastCommitMsgs['amend'] = ('', False)  # avoid loading stale cache
731
        # refresh() may load/save the stale 'amend' message in commitSetAction()
732
        self.refresh()
733 734 735 736 737 738

        # Updating after refreshWctx() leaves stale files in the commit list
        # after amend + evolve + update.
        if curraction._name == 'amend':
            self.commitSetAction()

739
        self.stwidget.refreshWctx() # Trigger reload of working context
740 741 742 743 744
        # clear the last 'amend' message
        # do not clear the last 'commit' message because there are many cases
        # in which we may write a commit message first, modify the repository
        # (e.g. amend or update and merge uncommitted changes) and then do the
        # actual commit
745
        self.lastCommitMsgs['amend'] = ('', False)  # clear saved stale cache
746

747 748 749 750 751
    @pyqtSlot()
    def refreshWctx(self):
        'User has requested a working context refresh'
        self.stwidget.refreshWctx() # Trigger reload of working context

752
    @pyqtSlot()
753
    def reload(self):
754 755 756 757
        'User has requested a reload'
        self.repo.thginvalidate()
        self.refresh()
        self.stwidget.refreshWctx() # Trigger reload of working context
758

759
    @pyqtSlot()
760
    def refresh(self):
761
        ispatch = self.repo[b'.'].thgmqappliedpatch()
762 763
        if not self.hasmqbutton:
            self.commitButtonEnable.emit(not ispatch)
764
        self.msgte.refresh(self.repo)
765

766
        # Update branch operation button
767
        branchu = hglib.tounicode(self.repo[None].branch())
768
        if self.branchop is None:
769
            title = _('Branch: ') + branchu
770
        elif self.branchop == False:
771
            title = _('Close Branch: ') + branchu
772 773 774 775
        else:
            title = _('New Branch: ') + self.branchop
        self.branchbutton.setText(title)

776
        # Update options label, showing only whitelisted options.
777
        opts = commitopts2str(self.opts)
778
        self.optionslabelfmt = _('<b>Selected Options:</b> %s')
779
        self.optionslabel.setText(self.optionslabelfmt
Manuel Jacob's avatar
Manuel Jacob committed
780 781
                                  % qtlib.htmlescape(hglib.tounicode(opts),
                                                     False))
782 783
        self.optionslabel.setVisible(bool(opts))

784 785 786
        # Update parent csinfo widget
        self.pcsinfo.set_revision(None)
        self.pcsinfo.update()
787

788 789 790 791 792
        # This is ugly, but want pnlabel to have the same alignment/style/etc
        # as pcsinfo, so extract the needed parts of pcsinfo's markup.  Would
        # be nicer if csinfo exposed this information, or if csinfo could hold
        # widgets like pnlabel.
        if self.hasmqbutton:
793 794
            parent = _('Parent:')
            patchname = _('Patch name:')
795
            text = pycompat.unicode(self.pcsinfo.revlabel.text())
796 797 798 799 800 801 802 803
            cellend = '</td>'
            firstidx = text.find(cellend) + len(cellend)
            secondidx = text[firstidx:].rfind('</tr>')
            if firstidx >= 0 and secondidx >= 0:
                start = text[0:firstidx].replace(parent, patchname)
                self.pnlabel.setText(start + text[firstidx+secondidx:])
            else:
                self.pnlabel.setText(patchname)
804
            self.commitSetAction()
805

806
    def branchOp(self):
807
        d = branchop.BranchOpDialog(self._repoagent, self.branchop, self)
808 809
        d.setWindowFlags(Qt.Sheet)
        d.setWindowModality(Qt.WindowModal)
810 811
        if d.exec_() == QDialog.Accepted:
            self.branchop = d.branchop
812 813
            if self.branchop is False:
                if not self.getMessage(True).strip():
814
                    engmsg = self._repoagent.configBool(
815
                        'tortoisehg', 'engmsg')
816 817
                    msgset = i18n.keepgettext()._('Close %s branch')
                    text = engmsg and msgset['id'] or msgset['str']
818
                    self.setMessage(pycompat.unicode(text) %
819
                                    hglib.tounicode(self.repo[None].branch()))
820
            self.msgte.setFocus()
821
            self.refresh()
822

823 824
    def canUndo(self):
        'Returns undo description or None if not valid'
825 826 827
        desc, oldlen = hglib.readundodesc(self.repo)
        if desc == 'commit':
            return _('Rollback commit to revision %d') % (oldlen - 1)
828 829 830 831 832 833 834 835 836 837
        return None

    def rollback(self):
        msg = self.canUndo()
        if not msg:
            return
        d = QMessageBox.question(self, _('Confirm Undo'), msg,
                                 QMessageBox.Ok | QMessageBox.Cancel)
        if d != QMessageBox.Ok:
            return
838
        self._runCommand('rollback', [['rollback']])
839

840 841
    def updateRecentMessages(self):
        # Define a menu that lists recent messages
842 843
        m = self.recentMessagesButton.menu()
        m.clear()
844 845
        for s in self.msghistory:
            title = s.split('\n', 1)[0][:70]
846 847
            a = m.addAction(title)
            a.setData(s)
848

849
    def getMessage(self, allowreplace):
850
        text = self.msgte.text()
851
        try:
852
            return hglib.fromunicode(text, 'strict')
853
        except UnicodeEncodeError:
854 855 856 857
            if allowreplace:
                return hglib.fromunicode(text, 'replace')
            else:
                raise
858

859 860
    @pyqtSlot(QAction)
    def msgSelected(self, action):