# sync.py - TortoiseHg's sync widget
#
# Copyright 2010 Adrian Buehlmann <adrian@cadifra.com>
# 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 or any later version.



import os
import re
import tempfile

from .qtcore import (
    QAbstractTableModel,
    QDir,
    QMimeData,
    QModelIndex,
    QPoint,
    QSettings,
    QTimer,
    QUrl,
    Qt,
    pyqtSignal,
    pyqtSlot,
)
from .qtgui import (
    QAction,
    QApplication,
    QCheckBox,
    QComboBox,
    QDesktopServices,
    QDialog,
    QDialogButtonBox,
    QFileDialog,
    QFormLayout,
    QFrame,
    QGroupBox,
    QHBoxLayout,
    QKeySequence,
    QLabel,
    QLineEdit,
    QMenu,
    QPushButton,
    QRadioButton,
    QSizePolicy,
    QStackedLayout,
    QStyle,
    QToolBar,
    QTreeView,
    QVBoxLayout,
    QWidget,
)

from mercurial import (
    encoding,
    hg,
    httpconnection,
    pycompat,
    util,
)

from mercurial.utils import (
    urlutil,
)

from ..util import (
    hglib,
    paths,
    wconfig,
)
from ..util.i18n import _
from . import (
    bookmark,
    cmdcore,
    cmdui,
    hgemail,
    hgrcutil,
    qtlib,
    rebase,
    resolve,
    thgrepo,
)

if hglib.TYPE_CHECKING:
    from typing import (
        Dict,
        Iterable,
        List,
        Set,
        Tuple,
        Union,
        Optional,
    )
    from mercurial import (
        localrepo,
    )


def parseurl(url):
    # _type: (str) -> urlutil.url
    # TODO: re-enable this after dropping 5.7 support
    assert isinstance(url, str), repr(url)
    return urlutil.url(hglib.fromunicode(url))

def linkify(url):
    # type: (str) -> str
    assert isinstance(url, str), repr(url)
    u = urlutil.url(hglib.fromunicode(url))
    if u.scheme in (b'http', b'https'):
        safe = urlutil.hidepassword(hglib.fromunicode(url))
        return u'<a href="%s">%s</a>' % (url, hglib.tounicode(safe))
    elif u.scheme is None or u.scheme == b'file':
        return u'<a href="%s">%s</a>' % (url, hglib.tounicode(u.path))
    else:
        return url

# ignore preceding white spaces because ui.prompt() for username/password
# writes extra " "s to the output channel. (hg 3.1)
_extractnodeids = re.compile(br'^\s*([0-9a-f]{40})$', re.MULTILINE).findall

class SyncWidget(QWidget, qtlib.TaskWidget):
    newCommand = pyqtSignal(cmdcore.CmdSession)
    outgoingNodes = pyqtSignal(object)
    incomingBundle = pyqtSignal(str, str)
    showMessage = pyqtSignal(str)
    pullCompleted = pyqtSignal()
    pushCompleted = pyqtSignal()

    switchToRequest = pyqtSignal(str)

    def __init__(self, repoagent, parent=None):
        QWidget.__init__(self, parent)

        layout = QVBoxLayout()
        layout.setContentsMargins(2, 2, 2, 2)
        layout.setSpacing(4)
        self.setLayout(layout)
        self.setAcceptDrops(True)

        self._repoagent = repoagent
        self._cmdsession = cmdcore.nullCmdSession()
        # peer repository of last command
        self._lasturl = None  # type: Optional[str]
        self._lastbfile = None  # output bundle of last incoming command
        self.opts = {}  # type: Dict[str, Union[bool, bytes]]
        self.cmenu = None

        s = QSettings()
        for opt in ('force', 'new-branch', 'noproxy', 'debug', 'mq'):
            val = qtlib.readBool(s, 'sync/' + opt)
            if val:
                if opt != 'mq' or b'mq' in self.repo.extensions():
                    self.opts[opt] = val
        for opt in ('remotecmd', 'branch'):
            val = hglib.fromunicode(qtlib.readString(s, 'sync/' + opt))
            if val:
                self.opts[opt] = val

        self._repoagent.configChanged.connect(self.reload)
        self._repoagent.repositoryChanged.connect(self._onRepositoryChanged)

        tb = QToolBar(self)
        tb.setIconSize(qtlib.toolBarIconSize())
        tb.setStyleSheet(qtlib.tbstylesheet)
        self.layout().addWidget(tb)
        self.opbuttons = []

        def newaction(tip, icon, cb):
            a = QAction(self)
            a.setToolTip(tip)
            a.setIcon(qtlib.geticon(icon))
            a.triggered.connect(cb)
            self.opbuttons.append(a)
            tb.addAction(a)
            return a

        self.incomingAction = \
        newaction(_('Check for incoming changes from selected URL'),
             'hg-incoming', self.inclicked)
        self.pullAction = \
        newaction(_('Pull incoming changes from selected URL'),
             'hg-pull', lambda: self.pullclicked())
        self.outgoingAction = \
        newaction(_('Detect outgoing changes to selected URL'),
             'hg-outgoing', self.outclicked)
        self.pushAction = \
        newaction(_('Push outgoing changes to selected URL'),
             'hg-push', lambda: self.pushclicked(None))
        newaction(_('Sync Bookmarks'), 'thg-sync-bookmarks', self.syncBookmark)
        newaction(_('Email outgoing changesets for remote repository'),
             'mail-forward', self.emailclicked)

        if b'perfarce' in self.repo.extensions():
            a = QAction(self)
            a.setToolTip(_('Manage pending perforce changelists'))
            a.setText('P4')
            a.triggered.connect(self.p4pending)
            self.opbuttons.append(a)
            tb.addAction(a)
        tb.addSeparator()
        newaction(_('Unbundle'),
             'hg-unbundle', self.unbundle)
        tb.addSeparator()
        self.stopAction = a = QAction(self)
        a.setToolTip(_('Stop current operation'))
        a.setIcon(qtlib.geticon('process-stop'))
        a.triggered.connect(self.stopclicked)
        tb.addAction(a)

        tb.addSeparator()
        self.optionsbutton = QPushButton(_('Options'))
        self.postpullbutton = QPushButton()
        tb.addWidget(self.postpullbutton)
        tb.addWidget(self.optionsbutton)

        self.targetcombo = QComboBox()
        self.targetcombo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
        self.targetcombo.setSizeAdjustPolicy(
            QComboBox.SizeAdjustPolicy.AdjustToContents)
        self.targetcombo.setEnabled(False)
        self.targetcheckbox = QCheckBox(_('Target:'))
        self.targetcheckbox.toggled.connect(self.targetcombo.setEnabled)
        tb.addSeparator()
        tb.addWidget(self.targetcheckbox)
        tb.addWidget(self.targetcombo)

        hbox = QHBoxLayout()
        hbox.setContentsMargins(0, 0, 0, 0)
        layout.addLayout(hbox)
        self.optionshdrlabel = lbl = QLabel(_('<b>Selected Options:</b>'))
        hbox.addWidget(lbl)
        self.optionslabel = QLabel()
        self.optionslabel.setAcceptDrops(False)
        hbox.addWidget(self.optionslabel)
        hbox.addStretch()

        self.pathEditToolbar = tbar = QToolBar(_('Path Edit Toolbar'))
        tbar.setStyleSheet(qtlib.tbstylesheet)
        tbar.setIconSize(qtlib.smallIconSize())
        layout.addWidget(tbar)

        a = tbar.addAction(qtlib.geticon('thg-password'), _('Security'))
        a.setToolTip(_('Manage HTTPS connection security and user '
                       'authentication'))
        self.securebutton = a
        tbar.addWidget(qtlib.Spacer(2, 2))

        style = QApplication.style()
        a = tbar.addAction(style.standardIcon(QStyle.StandardPixmap.SP_DialogSaveButton),
                          _('Save'))
        a.setToolTip(_('Save current URL under an alias'))
        self.savebutton = a
        tbar.addWidget(qtlib.Spacer(2, 2))

        self.urlentry = QLineEdit()
        self.urlentry.textChanged.connect(self.urlChanged)
        self.urlentry.returnPressed.connect(self.saveclicked)
        tbar.addWidget(self.urlentry)
        tbar.addWidget(qtlib.Spacer(2, 2))

        self.browsebutton = QPushButton(_('Browse...'))
        self.browsebutton.setAutoDefault(False)
        self.browsebutton.clicked.connect(self._browseUrl)
        tbar.addWidget(self.browsebutton)

        # even though currentRowChanged fires pathSelected, clicked signal is
        # also connected to it. otherwise urlentry won't be updated when the
        # selection moves between hgrctv and reltv.

        hbox = QHBoxLayout()
        hbox.setContentsMargins(0, 0, 0, 0)
        self.hgrctv = PathsTree(self, True)
        self.hgrctv.clicked.connect(self.pathSelected)
        self.hgrctv.removeAlias.connect(self.removeAlias)
        self.hgrctv.menuRequest.connect(self.menuRequest)
        pathsframe = QFrame()
        pathsframe.setFrameStyle(QFrame.Shape.StyledPanel|QFrame.Shadow.Raised)
        pathsbox = QVBoxLayout()
        pathsbox.setContentsMargins(0, 0, 0, 0)
        pathsframe.setLayout(pathsbox)
        lbl = QLabel(_('Paths in Repository Settings:'))
        pathsbox.addWidget(lbl)
        pathsbox.addWidget(self.hgrctv)
        hbox.addWidget(pathsframe)

        self.reltv = PathsTree(self, False)
        self.reltv.clicked.connect(self.pathSelected)
        self.reltv.menuRequest.connect(self.menuRequest)
        self.reltv.clicked.connect(self.hgrctv.clearSelection)
        self.hgrctv.clicked.connect(self.reltv.clearSelection)
        pathsframe = QFrame()
        pathsframe.setFrameStyle(QFrame.Shape.StyledPanel|QFrame.Shadow.Raised)
        pathsbox = QVBoxLayout()
        pathsbox.setContentsMargins(0, 0, 0, 0)
        pathsframe.setLayout(pathsbox)
        lbl = QLabel(_('Related Paths:'))
        pathsbox.addWidget(lbl)
        pathsbox.addWidget(self.reltv)
        hbox.addWidget(pathsframe)

        layout.addLayout(hbox, 1)

        self.savebutton.triggered.connect(self.saveclicked)
        self.securebutton.triggered.connect(self.secureclicked)
        self.postpullbutton.clicked.connect(self.postpullclicked)
        self.optionsbutton.clicked.connect(self.editOptions)

        self._dialogs = qtlib.DialogKeeper(
            lambda self, dlgmeth, *args: dlgmeth(self, *args), parent=self)

        self.curalias = None  # type: Optional[bytes]
        self.reload()
        if b'default' in self.paths:
            self.setUrl('default')
        else:
            self.setEditUrl('')
        self._updateUi()

    @property
    def repo(self):
        return self._repoagent.rawRepo()

    def canswitch(self):
        return False

    def _loadTargets(self):
        self.targetcombo.clear()
        # itemData(role=UserRole) is the argument list to pass to hg
        self.targetcombo.addItem('', ('--rev', 'null'))  # placeholder

        for name in hglib.namedbranches(self.repo):
            uname = hglib.tounicode(name)
            self.targetcombo.addItem(_('branch: ') + uname, ('--branch', name))
            self.targetcombo.setItemData(self.targetcombo.count() - 1, name,
                                         Qt.ItemDataRole.ToolTipRole)
        for name in sorted(self.repo._bookmarks):
            uname = hglib.tounicode(name)
            self.targetcombo.addItem(_('bookmark: ') + uname,
                                     ('--bookmark', name))
            self.targetcombo.setItemData(self.targetcombo.count() - 1, name,
                                         Qt.ItemDataRole.ToolTipRole)

    def _findTargetIndex(self, ctx):
        for name in ctx.bookmarks():
            uname = hglib.tounicode(name)
            return self.targetcombo.findText(_('bookmark: ') + uname)
        if ctx.node() in self.repo.branchheads(ctx.branch()):
            uname = hglib.tounicode(ctx.branch())
            return self.targetcombo.findText(_('branch: ') + uname)
        return 0

    def refreshTargets(self, rev):
        if not isinstance(rev, int):
            return

        if rev >= len(self.repo):
            return

        ctx = self.repo[rev]
        if self.targetcombo.count() <= 0:
            self._loadTargets()
        self.targetcombo.setItemText(0, _('rev: %d (%s)') % (ctx.rev(), ctx))
        self.targetcombo.setItemData(0, ('--rev', str(ctx.rev())))
        self.targetcombo.setCurrentIndex(self._findTargetIndex(ctx))

    def isTargetSelected(self):
        return self.targetcheckbox.isChecked()

    @pyqtSlot(int)
    def _onRepositoryChanged(self, flags):
        if flags & thgrepo.LogChanged:
            self._loadTargets()

    def editOptions(self):
        dlg = OptionsDialog(self._repoagent, self.opts, self)
        dlg.setWindowFlags(Qt.WindowType.Sheet)
        dlg.setWindowModality(Qt.WindowModality.WindowModal)
        if dlg.exec() == QDialog.DialogCode.Accepted:
            self.opts.update(dlg.outopts)
            self.refreshUrl()

            s = QSettings()
            for opt, val in self.opts.items():
                if isinstance(val, bytes):
                    val = hglib.tounicode(val)
                s.setValue('sync/' + opt, val)

    @pyqtSlot()
    def reload(self):
        # Refresh configured paths
        self.paths = {}  # type: Dict[bytes, bytes]
        fn = self.repo.vfs.join(b'hgrc')
        fn, cfg = hgrcutil.loadIniFile([fn], self)
        if b'paths' in cfg:
            for alias, path in cfg.items(b'paths'):
                self.paths[alias] = path
        tm = PathsModel(self.paths.items(), self)
        self.hgrctv.setModel(tm)
        sm = self.hgrctv.selectionModel()
        sm.currentRowChanged.connect(self.pathSelected)

        # Refresh post-pull
        self.cachedpp = hglib.tounicode(self.repo.postpull)
        name = _('Post Pull: ') + self.cachedpp.title()
        self.postpullbutton.setText(name)

        # Refresh related paths
        known = set()  # type: Set[bytes]
        known.add(os.path.abspath(self.repo.root).lower())
        for path in self.paths.values():
            if not urlutil.hasscheme(path):
                known.add(os.path.abspath(util.localpath(path)).lower())
            else:
                known.add(path)
        related = {}  # type: Dict[bytes, bytes]
        repoid = hglib.repoidnode(self.repo)
        for root, shortname in thgrepo.relatedRepositories(repoid):
            if root == self.repo.root:
                continue
            abs = os.path.abspath(root).lower()
            if abs not in known:
                related[root] = shortname
                known.add(abs)
            if root in thgrepo._repocache:
                # repositories already opened keep their ui instances in sync
                repo = thgrepo._repocache[root]
                ui = repo.ui
            elif paths.is_on_fixed_drive(root):
                # directly read the repository's configuration file
                tempui = self.repo.ui.copy()
                tempui.readconfig(os.path.join(root, b'.hg', b'hgrc'))
                ui = tempui
            else:
                continue
            for alias, path in ui.configitems(b'paths'):
                if not urlutil.hasscheme(path):
                    abs = os.path.abspath(util.localpath(path)).lower()
                else:
                    abs = path
                if abs not in known:
                    related[path] = alias
                    known.add(abs)
        pairs = [(alias, path) for path, alias in related.items()]
        tm = PathsModel(pairs, self)
        self.reltv.setModel(tm)
        sm = self.reltv.selectionModel()
        sm.currentRowChanged.connect(self.pathSelected)

    def currentUrl(self):
        # type: () -> str
        return self.urlentry.text()

    def urlChanged(self):
        self.securebutton.setEnabled('https://' in self.currentUrl())

    def refreshUrl(self):
        'User has selected a new URL'
        self.urlChanged()

        opts = []
        for opt, value in self.opts.items():
            if value is True:
                opts.append('--'+opt)
            elif value:
                opts.append('--'+opt+'='+hglib.tounicode(value))
        self.optionslabel.setText(' '.join(opts))
        self.optionslabel.setVisible(bool(opts))
        self.optionshdrlabel.setVisible(bool(opts))

    def pathSelected(self, index):
        aliasindex = index.sibling(index.row(), 0)
        alias = aliasindex.data(Qt.ItemDataRole.DisplayRole)
        self.curalias = hglib.fromunicode(alias)
        path = index.model().realUrl(index)
        self.setEditUrl(hglib.tounicode(path))

    def setEditUrl(self, newurl):
        # type: (str) -> None
        'Set the current URL without changing the alias [unicode]'
        self.urlentry.setText(newurl)
        self.refreshUrl()

    def setUrl(self, newurl):
        # type: (str) -> None
        'Set the current URL to the given alias or URL [unicode]'
        model = self.hgrctv.model()
        assert model is not None
        for col in (0, 1):  # search known (alias, url)
            ixs = model.match(model.index(0, col), Qt.ItemDataRole.DisplayRole, newurl, 1,
                              Qt.MatchFlag.MatchFixedString | Qt.MatchFlag.MatchCaseSensitive)
            if ixs:
                self.hgrctv.setCurrentIndex(ixs[0])
                self.pathSelected(ixs[0])  # in case of row not changed
                return

        self.setEditUrl(newurl)

    def dragEnterEvent(self, event):
        data = event.mimeData()
        if data.hasUrls() or data.hasText():
            event.setDropAction(Qt.DropAction.CopyAction)
            event.acceptProposedAction()

    def dragMoveEvent(self, event):
        data = event.mimeData()
        if data.hasUrls() or data.hasText():
            event.setDropAction(Qt.DropAction.CopyAction)
            event.acceptProposedAction()

    def dropEvent(self, event):
        data = event.mimeData()
        if data.hasUrls():
            url = data.urls()[0].toString()
            event.setDropAction(Qt.DropAction.CopyAction)
            event.accept()
        elif data.hasText():
            url = data.text()
            event.setDropAction(Qt.DropAction.CopyAction)
            event.accept()
        else:
            return
        if url.startswith('file:///'):
            url = url[8:]
        self.setUrl(url)

    def canExit(self):
        return self._cmdsession.isFinished()

    @pyqtSlot(QPoint, str, str, bool)
    def menuRequest(self, point, url, alias, editable):
        # type: (QPoint, str, str, bool) -> None
        'menu event emitted by one of the two URL lists'
        if not self.cmenu:
            separator = (None, None, None)
            acts = []
            menu = QMenu(self)
            for text, cb, icon in (
                (_('E&xplore'), self.exploreurl, 'system-file-manager'),
                (_('&Terminal'), self.terminalurl, 'utilities-terminal'),
                (_('Copy &Path'), self.copypath, ''),
                separator,
                (_('&Edit...'), self.editurl, 'general'),
                (_('&Remove...'), self.removeurl, 'hg-strip')):
                if text is None:
                    menu.addSeparator()
                    continue
                act = QAction(text, self)
                if icon:
                    act.setIcon(qtlib.geticon(icon))
                act.triggered.connect(cb)
                acts.append(act)
                menu.addAction(act)
            self.cmenu = menu
            self.acts = acts

        self.menuurl = url
        self.menualias = alias
        for act in self.acts[-2:]:
            act.setEnabled(editable)
        self.cmenu.exec(point)

    def exploreurl(self):
        url = self.menuurl
        u = parseurl(url)
        if not u.scheme or u.scheme == b'file':
            qtlib.openlocalurl(u.path)
        else:
            QDesktopServices.openUrl(QUrl(url))

    def terminalurl(self):
        url = self.menuurl
        u = parseurl(url)
        if u.scheme and u.scheme != b'file':
            qtlib.InfoMsgBox(_('Repository not local'),
                        _('A terminal shell cannot be opened for remote'))
            return
        qtlib.openshell(u.path, b'repo ' + u.path)

    def editurl(self):
        alias = hglib.fromunicode(self.menualias)
        urlu = self.menuurl
        dlg = SaveDialog(self._repoagent, alias, urlu, self, edit=True)
        dlg.setWindowFlags(Qt.WindowType.Sheet)
        dlg.setWindowModality(Qt.WindowModality.WindowModal)
        if dlg.exec() == QDialog.DialogCode.Accepted:
            self.curalias = hglib.fromunicode(dlg.aliasentry.text())
            self.setEditUrl(dlg.urlentry.text())
            self.reload()

    def removeurl(self):
        if qtlib.QuestionMsgBox(_('Confirm path delete'),
            _('Delete %s from your repo configuration file?') % self.menualias,
            parent=self):
            self.removeAlias(self.menualias)

    def copypath(self):
        QApplication.clipboard().setText(self.menuurl)

    def keyPressEvent(self, event):
        sess = self._cmdsession
        if event.matches(QKeySequence.StandardKey.Refresh):
            self.reload()
        elif event.key() == Qt.Key.Key_Escape and not sess.isFinished():
            sess.abort()
        else:
            return super(SyncWidget, self).keyPressEvent(event)

    def stopclicked(self):
        self._cmdsession.abort()

    def saveclicked(self):
        if self.curalias:
            alias = self.curalias
        elif b'default' not in self.paths:
            alias = b'default'
        else:
            alias = b'new'
        dlg = SaveDialog(self._repoagent, alias, self.currentUrl(), self)
        dlg.setWindowFlags(Qt.WindowType.Sheet)
        dlg.setWindowModality(Qt.WindowModality.WindowModal)
        if dlg.exec() == QDialog.DialogCode.Accepted:
            self.curalias = hglib.fromunicode(dlg.aliasentry.text())
            self.reload()

    @pyqtSlot()
    def _browseUrl(self):
        FD = QFileDialog
        caption = _("Select repository")
        path = FD.getExistingDirectory(self, caption, self.urlentry.text())
        if path:
            self.urlentry.setText(QDir.toNativeSeparators(path))

    def secureclicked(self):
        if not parseurl(self.currentUrl()).host:
            qtlib.WarningMsgBox(_('No host specified'),
                                _('Please set a valid URL to continue.'),
                                parent=self)
            return
        dlg = SecureDialog(self._repoagent, self.currentUrl(), self)
        dlg.setWindowFlags(Qt.WindowType.Sheet)
        dlg.setWindowModality(Qt.WindowModality.WindowModal)
        dlg.exec()

    @pyqtSlot()
    def _updateUi(self):
        sess = self._cmdsession
        for b in self.opbuttons:
            b.setEnabled(sess.isFinished())
        self.stopAction.setEnabled(not sess.isFinished())

    def run(self, cmdline, details):
        # type: (List[Union[str, bytes]], Iterable[str]) -> cmdcore.CmdSession
        if not self._cmdsession.isFinished():
            return cmdcore.nullCmdSession()
        self.lastcmdline = list(cmdline)
        for name in list(details) + ['remotecmd']:
            val = self.opts.get(name)
            if not val:
                continue
            if isinstance(val, bool):
                if val:
                    cmdline.append('--' + name)
            elif val:
                cmdline.append('--' + name)
                cmdline.append(val)

        if 'rev' in details and '--rev' not in cmdline:
            if self.targetcheckbox.isChecked():
                idx = self.targetcombo.currentIndex()
                if idx != -1:
                    args = self.targetcombo.itemData(idx)
                    if args[0][2:] not in details:
                        args = ('--rev',) + args[1:]
                    cmdline += args
        if self.opts.get('noproxy'):
            cmdline += ['--config', 'http_proxy.host=']
        if self.opts.get('debug'):
            cmdline.append('--debug')

        cururl = self.currentUrl()
        lurl = hglib.fromunicode(cururl)
        u = parseurl(cururl)

        if not u.host and not u.path:
            self.switchToRequest.emit('sync')
            qtlib.WarningMsgBox(_('No remote repository URL or path set'),
                    _('No valid <i>default</i> remote repository URL or path '
                      'has been configured for this repository.<p>Please type '
                      'and save a remote repository path on the Sync widget.'),
                    parent=self)
            return cmdcore.nullCmdSession()

        if u.scheme == b'https':
            if self.repo.ui.configbool(b'insecurehosts', u.host):
                cmdline.append('--insecure')
            if u.user:
                cleanurl = urlutil.removeauth(lurl)
                res = httpconnection.readauthforuri(self.repo.ui, cleanurl,
                                                    u.user)
                if res:
                    group, auth = res
                    if auth.get(b'username'):
                        if qtlib.QuestionMsgBox(
                            _('Redundant authentication info'),
                            _('You have authentication info configured for '
                              'this host and inside this URL.  Remove '
                              'authentication info from this URL?'),
                            parent=self):
                            self.setEditUrl(hglib.tounicode(cleanurl))
                            self.saveclicked()

        if not self.opts.get('mq'):
            cmdline.append(lurl)
        ucmdline = pycompat.maplist(hglib.tounicode, cmdline)
        # bypass overlay of incoming bundle to pull changes
        overlay = ucmdline[0] not in ('fetch', 'incoming', 'pull')
        self._cmdsession = sess = self._repoagent.runCommand(ucmdline, self,
                                                             overlay=overlay)
        sess.commandFinished.connect(self._updateUi)
        self._lasturl = cururl
        self._updateUi()
        self.newCommand.emit(sess)
        return sess

    ##
    ## Workbench toolbar buttons
    ##

    def incoming(self):
        if not self._cmdsession.isFinished():
            self.showMessage.emit(_('sync command already running'))
        else:
            self.inclicked()

    def pull(self):
        if not self._cmdsession.isFinished():
            self.showMessage.emit(_('sync command already running'))
        else:
            self.pullclicked()

    def outgoing(self):
        if not self._cmdsession.isFinished():
            self.showMessage.emit(_('sync command already running'))
        else:
            self.outclicked()

    def push(self, confirm, **kwargs):
        if not self._cmdsession.isFinished():
            self.showMessage.emit(_('sync command already running'))
        else:
            self.pushclicked(confirm, **kwargs)

    def pullBundle(self, bundle, rev, bsource=None):
        # type: (str, Optional[bytes], Optional[str]) -> None
        'accept bundle changesets'
        if not self._cmdsession.isFinished():
            self.showMessage.emit(_('sync command already running'))
            return
        save = self.currentUrl()
        orev = self.opts.get('rev')
        # XXX hack to ignore incoming bundle because it can't apply phase
        # movement, pull bookmarks and largefiles. further cleanups should
        # go on default branch.
        self.setEditUrl(bsource or bundle)
        if rev is not None:
            self.opts['rev'] = rev
        self.pullclicked(bsource)
        self.setEditUrl(save)
        self.opts['rev'] = orev

    ##
    ## Sync dialog buttons
    ##

    def linkifyWithTarget(self, url):
        # type: (str) -> str
        link = linkify(url)
        if self.targetcheckbox.isChecked():
            link += u" (%s)" % self.targetcombo.currentText()
        return link

    def inclicked(self):
        url = self.currentUrl()
        link = self.linkifyWithTarget(url)
        if not url.startswith('p4://'):
            bfile = url.replace('/', '_')
            for badchar in (':', '*', '\\', '?', '#'):
                bfile = bfile.replace(badchar, '')
            fd, bfile = tempfile.mkstemp(
                suffix=b'.hg',
                prefix=hglib.fromunicode(bfile) + b'_',
                dir=qtlib.gettempdir())
            os.close(fd)
            self._lastbfile = hglib.tounicode(bfile)
            cmdline = ['incoming', '--quiet', '--bundle', bfile]
            sess = self.run(cmdline, ('force', 'branch', 'rev'))
            sess.commandFinished.connect(self._onIncomingFinished)
        else:
            self._lastbfile = None
            cmdline = ['incoming']
            sess = self.run(cmdline, ('force', 'branch', 'rev'))
            sess.commandFinished.connect(self._onIncomingFinished)

        self.showMessage.emit(_('Getting incoming changesets from %s...')
                              % link)

    @pyqtSlot(int)
    def _onIncomingFinished(self, ret):
        link = self.linkifyWithTarget(self._lasturl)
        if ret == 0:
            self.showMessage.emit(_('Found incoming changesets from %s') % link)
            # hg-git 'incoming' gives no data for bundle
            if (self._lastbfile and os.path.exists(self._lastbfile)
                and os.path.getsize(self._lastbfile) > 0):
                self.incomingBundle.emit(self._lastbfile, self._lasturl)
        elif ret == 1:
            self.showMessage.emit(_('No incoming changesets from %s') % link)
        else:
            self.showMessage.emit(_('Incoming from %s aborted, ret %d')
                                  % (link, ret))

    def pullclicked(self, url=None):
        # type: (Optional[str]) -> None
        link = self.linkifyWithTarget(url or self.currentUrl())

        cmdline = ['pull', '--verbose']
        if self._repoagent.configBool('tortoisehg', 'autoresolve'):
            uimerge = 'ui.merge=internal:merge'
        else:
            uimerge = 'ui.merge=internal:fail'
        if self.cachedpp == 'rebase':
            cmdline += ['--rebase', '--config', uimerge]
        elif self.cachedpp == 'update':
            cmdline += ['--update', '--config', uimerge]
        elif self.cachedpp == 'updateorrebase':
            cmdline += ['--update', '--rebase', '--config', uimerge]
        elif self.cachedpp == 'fetch':
            cmdline[0] = 'fetch'
        elif self.opts.get('mq'):
            # force the tool to update to the pulled changeset
            cmdline += ['--update', '--config', uimerge]
        sess = self.run(cmdline, ('force', 'branch', 'rev', 'bookmark', 'mq'))
        sess.commandFinished.connect(self._onPullFinished)
        self.showMessage.emit(_('Pulling from %s...') % link)
        if url:
            self._lasturl = url  # overwrite by user-visible (source) URL

    @pyqtSlot(int)
    def _onPullFinished(self, ret):
        link = self.linkifyWithTarget(self._lasturl)
        if ret == 0:
            self.showMessage.emit(_('Pull from %s completed') % link)
        else:
            self.showMessage.emit(_('Pull from %s aborted, ret %d')
                                  % (link, ret))
        self.pullCompleted.emit()
        # handle file conflicts during rebase
        if self.cachedpp in ('rebase', 'updateorrebase'):
            if os.path.exists(self.repo.vfs.join(b'rebasestate')):
                dlg = rebase.RebaseDialog(self._repoagent, self)
                dlg.exec()
                return
        # handle file conflicts during update
        for root, path, status in thgrepo.recursiveMergeStatus(self.repo):
            if status == b'u':
                qtlib.InfoMsgBox(_('Merge caused file conflicts'),
                                _('File conflicts need to be resolved'))
                dlg = resolve.ResolveDialog(self._repoagent, self)
                dlg.exec()
                return

    def outclicked(self):
        link = self.linkifyWithTarget(self.currentUrl())
        cmdline = ['outgoing', '--template', '{node}\n']
        sess = self.run(cmdline, ('force', 'branch', 'rev'))
        sess.setCaptureOutput(True)
        sess.commandFinished.connect(self._onOutgoingFinished)
        self.showMessage.emit(_('Finding outgoing changesets to %s...') % link)

    @pyqtSlot(int)
    def _onOutgoingFinished(self, ret):
        link = self.linkifyWithTarget(self._lasturl)
        if ret == 0:
            data = bytes(self._cmdsession.readAll())
            nodes = [pycompat.sysstr(node) for node in _extractnodeids(data)]
            self.showMessage.emit(_('%d outgoing changesets to %s') %
                                  (len(nodes), link))
            self.outgoingNodes.emit(nodes)
        elif ret == 1:
            self.showMessage.emit(_('No outgoing changesets to %s') % link)
        else:
            self.showMessage.emit(_('Outgoing to %s aborted, ret %d')
                                  % (link, ret))

    def p4pending(self):
        sess = self.run(['p4pending', '--verbose'], ())
        sess.setCaptureOutput(True)
        sess.commandFinished.connect(self._onP4pendingFinished)
        self.showMessage.emit(_('Perforce pending...'))

    @pyqtSlot(int)
    def _onP4pendingFinished(self, ret):
        pending = {}
        if ret == 0:
            output = str(self._cmdsession.readAll())  # decode?
            for line in output.splitlines():
                try:
                    hashes = line.split(' ')
                    changelist = hashes.pop(0)
                    clnum = int(changelist)
                    if len(hashes) > 1 and len(hashes[0]) == 1:
                        state = hashes.pop(0)
                        if state == 's':
                            changelist = _('%s (submitted)') % changelist
                        elif state == 'p':
                            changelist = _('%s (pending)') % changelist
                        else:
                            raise ValueError
                        pending[changelist] = hashes
                except (ValueError, IndexError):
                    # XXX: This is unconditionally overwritten before use
                    text = _('Unable to parse p4pending output')
            if pending:
                text = _('%d pending changelists found') % len(pending)
            else:
                text = _('No pending Perforce changelists')
        elif ret is None:
            text = _('Aborted p4pending')
        else:
            text = _('Unable to determine pending changesets')
        self.showMessage.emit(text)
        if pending:
            from tortoisehg.hgqt.p4pending import PerforcePending
            p4url = hglib.fromunicode(self._lasturl)
            dlg = PerforcePending(self._repoagent, pending, p4url, self)
            dlg.showMessage.connect(self.showMessage)
            dlg.exec()

    def pushclicked(self, confirm, rev=None, branch=None, pushall=False):
        if confirm is None:
            confirm = self._repoagent.configBool('tortoisehg', 'confirmpush')
        if rev == '':
            rev = None
        if branch == '':
            branch = None
        if pushall and (rev is not None or branch is not None):
            raise ValueError('inconsistent call with pushall=%r, rev=%r and '
                             'branch=%r' % (pushall, rev, branch))
        validopts = ('force', 'new-branch', 'rev', 'bookmark', 'mq')

        lurl = hglib.fromunicode(self.currentUrl())
        link = self.linkifyWithTarget(self.currentUrl())
        if (not hg.islocal(lurl) and confirm
            and not self.targetcheckbox.isChecked()):
            r = qtlib.QuestionMsgBox(_('Confirm Push to remote Repository'),
                                     _('Push to remote repository\n%s\n?')
                                     % link, parent=self)
            if not r:
                self.newCommand.emit(cmdcore.nullCmdSession())
                self.showMessage.emit(_('Push to %s aborted') % link)
                self.pushCompleted.emit()
                return

        # Precedence of conflicting revision specifiers:
        #
        # rev bra all  description
        # --- --- ---  --------------------------------------------------------
        #  x   x   x   1. method arguments (temporarily set by context menu)
        #  x           2. target combobox (temporarily set)
        #      x       3. opts table (set by OptionsDialog, saved in QSettings)
        #  x   x   x   4. tortoisehg.defaultpush (saved in hgrc)
        #
        # Note: "pushall" is set to True even if 2. or 3. is specified

        if branch is None:
            branch = self.opts.get('branch')
        if not pushall and rev is None and branch is None:
            defaultpush = self._repoagent.configString(
                'tortoisehg', 'defaultpush')
            if self.targetcheckbox.isChecked():
                pass
            elif defaultpush == 'all':
                # This is the default
                pass
            elif defaultpush == 'branch':
                branch = '.'
            elif defaultpush == 'revision':
                rev = '.'
            else:
                self.newCommand.emit(cmdcore.nullCmdSession())
                self.showMessage.emit(_('Invalid default push revision: %s. '
                                        'Please check your Mercurial '
                                        'configuration '
                                        '(tortoisehg.defaultpush)')
                                      % defaultpush)
                self.pushCompleted.emit()
                return

        cmdline = ['push']
        if rev:
            cmdline.extend(['--rev', str(rev)])
        if branch:
            cmdline.extend(['--branch', branch])
        sess = self.run(cmdline, validopts)
        sess.commandFinished.connect(self._onPushFinished)
        self.showMessage.emit(_('Pushing to %s...') % link)

    @pyqtSlot(int)
    def _onPushFinished(self, ret):
        link = self.linkifyWithTarget(self._lasturl)
        if ret == 0:
            self.showMessage.emit(_('Push to %s completed') % link)
        elif ret == 1:
            self.showMessage.emit(_('No outgoing changesets to %s') % link)
        else:
            self.showMessage.emit(_('Push to %s aborted, ret %d') % (link, ret))
            if ("'hg push --new-branch'" in self._cmdsession.errorString()
                and '--new-branch' not in self.lastcmdline):
                r = qtlib.QuestionMsgBox(_('Confirm New Branch'),
                                         _('One or more of the changesets that '
                                           'you are attempting to push involve '
                                           'the creation of a new branch.  '
                                           'Do you want to create a new branch '
                                           'in the remote repository?'),
                                         parent=self)
                if r:
                    cmdline = self.lastcmdline
                    cmdline.extend(['--new-branch'])
                    sess = self.run(cmdline, ('force', 'new-branch',
                                              'rev', 'bookmark', 'mq'))
                    sess.commandFinished.connect(self._onPushFinished)
                    return
        self.pushCompleted.emit()

    def postpullclicked(self):
        dlg = PostPullDialog(self._repoagent, self)
        dlg.setWindowFlags(Qt.WindowType.Sheet)
        dlg.setWindowModality(Qt.WindowModality.WindowModal)
        dlg.exec()

    def emailclicked(self):
        cmdline = ['outgoing', '--template', '{node}\n']
        sess = self.run(cmdline, ('force', 'branch', 'rev'))
        sess.setCaptureOutput(True)
        sess.commandFinished.connect(self._onOutgoingEmailFinished)
        self.showMessage.emit(_('Determining outgoing changesets to email...'))

    def syncBookmark(self):
        dlg = bookmark.SyncBookmarkDialog(self._repoagent, self.currentUrl(),
                                          self)
        dlg.exec()

    @pyqtSlot(int)
    def _onOutgoingEmailFinished(self, ret):
        if ret == 0:
            cmdline = self.lastcmdline
            data = bytes(self._cmdsession.readAll())
            revs = tuple(self.repo[n].rev() for n in _extractnodeids(data))
            self.showMessage.emit(_('%d outgoing changesets') % len(revs))
            try:
                outgoingrevs = (cmdline[cmdline.index('--rev') + 1],)
            except ValueError:
                outgoingrevs = None
            self._dialogs.open(SyncWidget._createEmailDialog, revs,
                               outgoingrevs)
        elif ret == 1:
            self.showMessage.emit(_('No outgoing changesets'))
        else:
            self.showMessage.emit(_('Outgoing aborted, ret %d') % ret)

    def _createEmailDialog(self, revs, outgoingrevs):
        return hgemail.EmailDialog(self._repoagent, revs, outgoing=True,
                                   outgoingrevs=outgoingrevs)

    def unbundle(self):
        caption = _("Select bundle file")
        _FILE_FILTER = ';;'.join([_("Bundle files (*.hg)"),
                                  _("All files (*)")])
        bundlefile, _filter = QFileDialog.getOpenFileName(
            self, caption, hglib.tounicode(self.repo.root), _FILE_FILTER)
        if bundlefile:
            # Set the pull source to the selected bundle file
            self.urlentry.setText(bundlefile)
            # Execute the incoming command, which will show the revisions in
            # the bundle, and let the user accept or reject them
            self.inclicked()

    @pyqtSlot(str)
    def removeAlias(self, alias):
        alias = hglib.fromunicode(alias)
        fn = self.repo.vfs.join(b'hgrc')
        fn, cfg = hgrcutil.loadIniFile([fn], self)
        if not hasattr(cfg, 'write'):
            qtlib.WarningMsgBox(_('Unable to remove URL'),
                   _('Iniparse must be installed.'), parent=self)
            return
        if fn is None:
            return
        if alias in cfg[b'paths']:
            del cfg[b'paths'][alias]
        try:
            wconfig.writefile(cfg, fn)
            self._repoagent.pollStatus()
        except EnvironmentError as e:
            qtlib.WarningMsgBox(_('Unable to write configuration file'),
                                hglib.exception_str(e), parent=self)
        self.reload()


class PostPullDialog(QDialog):
    def __init__(self, repoagent, parent):
        super(PostPullDialog, self).__init__(parent)
        self._repoagent = repoagent
        repo = repoagent.rawRepo()
        layout = QVBoxLayout()
        self.setLayout(layout)
        self.setWindowTitle(_('Post Pull Behavior'))
        self.setWindowFlags(self.windowFlags() &
                            ~Qt.WindowType.WindowContextHelpButtonHint)

        lbl = QLabel(_('Select post-pull operation for this repository'))
        layout.addWidget(lbl)

        self._opchecks = {
            'none': QRadioButton(_('None - simply pull changesets')),
            'update': QRadioButton(_('Update - pull, then try to update')),
            }
        layout.addWidget(self._opchecks['none'])
        layout.addWidget(self._opchecks['update'])

        if b'fetch' in repo.extensions():
            btntxt = _('Fetch - use fetch (auto merge pulled changes)')
        else:
            btntxt = _('Fetch - use fetch extension (fetch is not active!)')
        self._opchecks['fetch'] = chk = QRadioButton(btntxt)
        layout.addWidget(chk)
        chk.setVisible(b'fetch' in repo.extensions())

        if b'rebase' in repo.extensions():
            rebasetxt = _('Rebase - rebase local commits above pulled changes')
            updateorrebasetxt = _('UpdateOrRebase - pull, then try to update '
                                  'or rebase')
        else:
            rebasetxt = _('Rebase - use rebase extension (rebase is not '
                          'active!)')
            updateorrebasetxt = _('UpdateOrRebase - use rebase extension '
                                  '(rebase is not active!)')
        self._opchecks['rebase'] = chk = QRadioButton(rebasetxt)
        layout.addWidget(chk)
        chk.setVisible(b'rebase' in repo.extensions())
        self._opchecks['updateorrebase'] = chk = QRadioButton(updateorrebasetxt)
        layout.addWidget(chk)
        chk.setVisible(b'rebase' in repo.extensions())

        chk = self._opchecks[hglib.tounicode(repo.postpull)]
        chk.setChecked(True)
        chk.show()

        self.autoresolve_chk = QCheckBox(_('Automatically resolve merge '
                                           'conflicts where possible'))
        self.autoresolve_chk.setChecked(
            repoagent.configBool('tortoisehg', 'autoresolve'))
        layout.addWidget(self.autoresolve_chk)

        cfglabel = QLabel(_('<a href="config">Launch settings tool...</a>'))
        cfglabel.linkActivated.connect(self.linkactivated)
        layout.addWidget(cfglabel)

        bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Save|QDialogButtonBox.StandardButton.Cancel)
        bb.accepted.connect(self.accept)
        bb.rejected.connect(self.reject)

        self.bb = bb
        layout.addWidget(bb)

    @property
    def repo(self):
        return self._repoagent.rawRepo()

    def linkactivated(self, command):
        # type: (str) -> None
        if command == 'config':
            from tortoisehg.hgqt.settings import SettingsDialog
            sd = SettingsDialog(configrepo=False, focus='tortoisehg.postpull',
                            parent=self, root=self.repo.root)
            sd.exec()

    def getValue(self):
        # type: () -> str
        return next(iter(op for op, chk in self._opchecks.items()
                    if chk.isChecked()))

    def accept(self):
        path = self.repo.vfs.join(b'hgrc')
        fn, cfg = hgrcutil.loadIniFile([path], self)
        if not hasattr(cfg, 'write'):
            qtlib.WarningMsgBox(_('Unable to save post pull operation'),
                   _('Iniparse must be installed.'), parent=self)
            return
        if fn is None:
            return
        try:
            cfg.set(b'tortoisehg', b'postpull',
                    encoding.strtolocal(self.getValue()))
            cfg.set(b'tortoisehg', b'autoresolve',
                    self.autoresolve_chk.isChecked() and b'True' or b'False')
            wconfig.writefile(cfg, fn)
            self._repoagent.pollStatus()
        except EnvironmentError as e:
            qtlib.WarningMsgBox(_('Unable to write configuration file'),
                                hglib.exception_str(e), parent=self)
        super(PostPullDialog, self).accept()


class SaveDialog(QDialog):
    def __init__(self, repoagent, alias, urlu, parent, edit=False):
        super(SaveDialog, self).__init__(parent)
        self._repoagent = repoagent

        self.setWindowTitle(_('Save Path'))
        self.setWindowFlags(self.windowFlags() &
                            ~Qt.WindowType.WindowContextHelpButtonHint)

        self.origurl = hglib.fromunicode(urlu)
        self.setLayout(QFormLayout())
        self.layout().setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)

        self.origalias = alias
        self.aliasentry = QLineEdit(hglib.tounicode(self.origalias))
        self.aliasentry.selectAll()
        self.aliasentry.textChanged.connect(self._updateUi)
        self.layout().addRow(_('Alias'), self.aliasentry)

        self.edit = edit
        stack = QStackedLayout()  # 0: read-only masked URL, 1: editable URL
        self.urllabel = QLabel(urlu)
        stack.addWidget(self.urllabel)
        self.urlentry = QLineEdit(urlu)
        self.urlentry.textChanged.connect(self._updateUi)
        stack.addWidget(self.urlentry)
        stack.setCurrentIndex(int(edit))
        self.layout().addRow(_('URL'), stack)

        u = parseurl(urlu)
        clearable = bool(not edit and (u.user or u.passwd)
                         and u.scheme in (b'http', b'https'))
        self.clearcb = QCheckBox(_('Remove authentication data from URL'))
        self.clearcb.setToolTip(
            _('User authentication data should be associated with the '
              'hostname using the security dialog.'))
        self.clearcb.setChecked(clearable)
        self.clearcb.setVisible(clearable)
        self.clearcb.toggled.connect(self._removeAuthData)
        self.layout().addRow(self.clearcb)

        s = QSettings()
        self.updatesubpaths = QCheckBox(_('Update subrepo paths'))
        self.updatesubpaths.setChecked(
            qtlib.readBool(s, 'sync/updatesubpaths', True))
        self.updatesubpaths.setToolTip(
            _('Update or create a path alias called \'%s\' on all subrepos, '
              'using this URL as the base URL, '
              'appending the local relative subrepo path to it')
            % hglib.tounicode(alias))
        self.layout().addRow(self.updatesubpaths)

        bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Save|QDialogButtonBox.StandardButton.Cancel)
        bb.accepted.connect(self.accept)
        bb.rejected.connect(self.reject)
        bb.button(QDialogButtonBox.StandardButton.Save).setAutoDefault(True)
        self.bb = bb
        self.layout().addRow(bb)

        self._removeAuthData(self.clearcb.isChecked())
        self._updateUi()

    def savePath(self, repo, alias, path, confirm=True):
        # type: (localrepo.localrepository, bytes, bytes, bool) -> None
        fn = repo.vfs.join(b'hgrc')
        fn, cfg = hgrcutil.loadIniFile([fn], self)
        if not hasattr(cfg, 'write'):
            qtlib.WarningMsgBox(_('Unable to save an URL'),
                   _('Iniparse must be installed.'), parent=self)
            return
        if fn is None:
            return
        if (confirm and (not self.edit or path != self.origurl)
            and alias in cfg[b'paths']):
            if not qtlib.QuestionMsgBox(_('Confirm URL replace'),
                _('%s already exists, replace URL?') % hglib.tounicode(alias),
                parent=self):
                return
        cfg.set(b'paths', alias, path)
        if self.edit and alias != self.origalias:
            cfg.remove(b'paths', self.origalias)
        try:
            wconfig.writefile(cfg, fn)
        except EnvironmentError as e:
            qtlib.WarningMsgBox(_('Unable to write configuration file'),
                                hglib.exception_str(e), parent=self)
        if self.updatesubpaths.isChecked():
            ctx = repo[b'.']
            for subname in ctx.substate:  # type: bytes
                if ctx.substate[subname][2] != b'hg':
                    continue
                if not os.path.exists(repo.wjoin(subname)):
                    continue
                defaultsubpath = ctx.substate[subname][0]
                pathurl = urlutil.url(path)
                if pathurl.scheme:
                    subpath = bytes(pathurl).rstrip(b'/') + b'/' + subname
                else:
                    subpath = os.path.normpath(os.path.join(path, subname))
                if defaultsubpath != subname:
                    if not qtlib.QuestionMsgBox(
                            _('Confirm URL replace'),
                            _('Subrepo \'%s\' has a non trivial '
                              'default sync URL:<p>%s<p>'
                              'Replace it with the following URL?:'
                              '<p>%s')
                                % (hglib.tounicode(subname),
                                    hglib.tounicode(defaultsubpath),
                                    hglib.tounicode(subpath)),
                            parent=self):
                        continue
                subrepo = hg.repository(repo.ui, path=repo.wjoin(subname))
                self.savePath(subrepo, alias, subpath, confirm=False)

    def accept(self):
        alias = hglib.fromunicode(self.aliasentry.text())
        path = hglib.fromunicode(self.urlentry.text())
        repo = self._repoagent.rawRepo()
        self.savePath(repo, alias, path)
        self._repoagent.pollStatus()
        s = QSettings()
        s.setValue('sync/updatesubpaths', self.updatesubpaths.isChecked())
        super(SaveDialog, self).accept()

    @pyqtSlot(bool)
    def _removeAuthData(self, showclean):
        if showclean:
            cleanurl = hglib.tounicode(urlutil.removeauth(self.origurl))
            self.urllabel.setText(cleanurl)
            self.urlentry.setText(cleanurl)
        else:
            safeurl = hglib.tounicode(urlutil.hidepassword(self.origurl))
            self.urllabel.setText(safeurl)
            self.urlentry.setText(hglib.tounicode(self.origurl))

    @pyqtSlot()
    def _updateUi(self):
        savebtn = self.bb.button(QDialogButtonBox.StandardButton.Save)
        savebtn.setEnabled(bool(self.aliasentry.text()
                                and self.urlentry.text()))


def _addBrowseButton(edit, slot):
    button = QPushButton(_('Browse...'))
    button.setAutoDefault(False)
    button.clicked.connect(slot)
    hbox = QHBoxLayout()
    hbox.addWidget(edit)
    hbox.addWidget(button)
    return hbox


class SecureDialog(QDialog):
    def __init__(self, repoagent, urlu, parent):
        # type: (thgrepo.RepoAgent, str, QWidget) -> None
        super(SecureDialog, self).__init__(parent)
        self._repoagent = repoagent
        self._querysess = cmdcore.nullCmdSession()
        repo = repoagent.rawRepo()
        self._url = urlu
        u = parseurl(urlu)
        assert u.host, repr(u)
        uhost = hglib.tounicode(u.host)
        self.setWindowTitle(_('Security: ') + uhost)
        self.setWindowFlags(self.windowFlags() & \
                            ~Qt.WindowType.WindowContextHelpButtonHint)

        # if the already user has an [auth] configuration for this URL, use it
        cleanurl = urlutil.removeauth(hglib.fromunicode(urlu))
        res = httpconnection.readauthforuri(repo.ui, cleanurl, u.user)  # type: Optional[Tuple[bytes, Dict[bytes, bytes]]]
        self.schemes = None  # type: Optional[bytes]
        if res:
            self.alias, auth = res
        else:
            self.alias, auth = u.host, {}
        self.host = u.host  # type: bytes
        if cleanurl.startswith(b'svn+https://'):
            self.schemes = b'svn+https'
        else:
            self.schemes = None

        self.setLayout(QVBoxLayout())
        self.layout().addWidget(QLabel(_('<b>Host:</b> %s') % uhost))

        securebox = QGroupBox(_('Secure HTTPS Connection'))
        self.layout().addWidget(securebox)
        vbox = QVBoxLayout()
        securebox.setLayout(vbox)
        self.layout().addWidget(securebox)

        self.cacertradio = QRadioButton(
            _('Verify with Certificate Authority certificates (best)'))
        self.fprintradio = QRadioButton(
            _('Verify with stored host fingerprint (good)'))
        self.insecureradio = QRadioButton(
            _('No host validation, but still encrypted (bad)'))
        hbox = QHBoxLayout()
        fprint = repo.ui.config(b'hostsecurity', u.host + b':fingerprints')
        if not fprint:
            fprint = repo.ui.config(b'hostfingerprints', u.host, b'')
            if fprint:
                fprint = b'sha1:' + fprint
        self.fprintentry = le = QLineEdit(hglib.tounicode(fprint))
        self.fprintradio.toggled.connect(self.fprintentry.setEnabled)
        self.fprintentry.setEnabled(False)
        le.setPlaceholderText(_('### host certificate fingerprint ###'))
        hbox.addWidget(le)
        self._querybutton = qb = QPushButton(_('Query'))
        qb.clicked.connect(self._queryFingerprint)
        hbox.addWidget(qb)
        vbox.addWidget(self.cacertradio)
        vbox.addWidget(self.fprintradio)
        vbox.addLayout(hbox)
        vbox.addWidget(self.insecureradio)

        self.cacertradio.setChecked(True) # default
        if fprint:
            self.fprintradio.setChecked(True)
        elif repo.ui.config(b'insecurehosts', u.host):
            self.insecureradio.setChecked(True)

        self.fprintradio.toggled.connect(self._updateUi)
        self.insecureradio.toggled.connect(self._updateUi)

        self._protocolcombo = e = QComboBox(self)
        e.addItem(_('<unspecified>'), '')
        e.addItem(_('TLS 1.0'), 'tls1.0')
        e.addItem(_('TLS 1.1'), 'tls1.1')
        e.addItem(_('TLS 1.2'), 'tls1.2')
        protocol = repo.ui.config(b'hostsecurity',
                                  b'%s:minimumprotocol' % u.host)
        e.setCurrentIndex(e.findData(hglib.tounicode(protocol or b'')))
        hbox = QHBoxLayout()
        hbox.addWidget(QLabel(_('Minimum Protocol')))
        hbox.addWidget(self._protocolcombo)
        vbox.addLayout(hbox)

        self._authentries = {}  # type: Dict[bytes, QLineEdit]
        authbox = QGroupBox(_('User Authentication'))
        form = QFormLayout()
        authbox.setLayout(form)
        self.layout().addWidget(authbox)

        k = b'username'
        c = hglib.tounicode(u.user or auth.get(k, b''))
        self._authentries[k] = e = QLineEdit(c)
        e.setToolTip(
_('''Optional. Username to authenticate with. If not given, and the remote
site requires basic or digest authentication, the user will be prompted for
it. Environment variables are expanded in the username letting you do
foo.username = $USER.'''))
        form.addRow(_('Username'), e)

        k = b'password'
        c = hglib.tounicode(u.passwd or auth.get(k, b''))
        self._authentries[k] = e = QLineEdit(c)
        e.setEchoMode(QLineEdit.EchoMode.Password)
        e.setToolTip(
_('''Optional. Password to authenticate with. If not given, and the remote
site requires basic or digest authentication, the user will be prompted for
it.'''))
        form.addRow(_('Password'), e)
        if b'mercurial_keyring' in repo.extensions():
            e.clear()
            e.setEnabled(False)
            e.setToolTip(_('Mercurial keyring extension is enabled. '
                           'Passwords will be stored in a platform-native '
                           'secure method.'))

        k = b'key'
        c = hglib.tounicode(auth.get(k, b''))
        self._authentries[k] = e = QLineEdit(c)
        e.setToolTip(
_('''Optional. PEM encoded client certificate key file. Environment variables
are expanded in the filename.'''))
        form.addRow(_('User Certificate Key'),
                    _addBrowseButton(e, self._browseClientKey))

        k = b'cert'
        c = hglib.tounicode(auth.get(k, b''))
        self._authentries[k] = e = QLineEdit(c)
        e.setToolTip(
_('''Optional. PEM encoded client certificate chain file. Environment variables
are expanded in the filename.'''))
        form.addRow(_('User Certificate Chain'),
                    _addBrowseButton(e, self._browseClientCert))

        bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Help|QDialogButtonBox.StandardButton.Save|QDialogButtonBox.StandardButton.Cancel)
        bb.rejected.connect(self.reject)
        bb.accepted.connect(self.accept)
        bb.helpRequested.connect(self.keyringHelp)
        self.bb = bb
        self.layout().addWidget(bb)

        self._updateUi()
        e = self._authentries[b'username']
        e.selectAll()
        QTimer.singleShot(0, e.setFocus)

    @pyqtSlot()
    def _queryFingerprint(self):
        cmdline = hglib.buildcmdargs('debuggethostfingerprint', self._url,
                                     insecure=True)
        self._querysess = sess = self._repoagent.runCommand(cmdline, self)
        sess.setCaptureOutput(True)
        sess.commandFinished.connect(self._onQueryFingerprintFinished)
        self._updateUi()

    @pyqtSlot(int)
    def _onQueryFingerprintFinished(self, ret):
        sess = self._querysess
        if ret == 0:
            data = bytes(sess.readAll())
            self.fprintentry.setText(hglib.tounicode(data).strip())
        else:
            cmdui.errorMessageBox(sess, self, _('Certificate Query Error'))
        self._updateUi()

    def keyringHelp(self):
        qtlib.openhelpcontents('sync.html#security')

    @pyqtSlot()
    def _browseClientKey(self):
        e = self._authentries[b'key']
        n, _f = QFileDialog.getOpenFileName(
            self, _('Select User Certificate Key File'), e.text(),
            ';;'.join([_('PEM files (*.pem *.key)'), _('All files (*)')]))
        if n:
            e.setText(n)

    @pyqtSlot()
    def _browseClientCert(self):
        e = self._authentries[b'cert']
        n, _f = QFileDialog.getOpenFileName(
            self, _('Select User Certificate Chain File'), e.text(),
            ';;'.join([_('PEM files (*.pem *.crt *.cer)'), _('All files (*)')]))
        if n:
            e.setText(n)

    def accept(self):
        path = hglib.userrcpath()
        fn, cfg = hgrcutil.loadIniFile(path, self)
        if not hasattr(cfg, 'write'):
            qtlib.WarningMsgBox(_('Unable to save authentication'),
                   _('Iniparse must be installed.'), parent=self)
            return
        if fn is None:
            return

        def setorclear(section, item, value):
            # type: (bytes, bytes, Optional[bytes]) -> None
            if value:
                cfg.set(section, item, value)
            elif not value and item in cfg[section]:
                del cfg[section][item]

        if self.cacertradio.isChecked():
            fprint = None
            insecure = None
        elif self.fprintradio.isChecked():
            fprint = hglib.fromunicode(self.fprintentry.text())
            insecure = None
        else:
            fprint = None
            insecure = b'1'
        setorclear(b'hostsecurity', b'%s:fingerprints' % self.host, fprint)
        setorclear(b'insecurehosts', self.host, insecure)

        e = self._protocolcombo
        protocol = hglib.fromunicode(e.itemData(e.currentIndex()))
        setorclear(b'hostsecurity', b'%s:minimumprotocol' % self.host, protocol)

        cfg.set(b'auth', b'%s.prefix' % self.alias, self.host)
        for k in [b'username', b'password', b'key', b'cert']:
            setorclear(b'auth', b'%s.%s' % (self.alias, k),
                       hglib.fromunicode(self._authentries[k].text()))
        setorclear(b'auth', b'%s.schemes' % self.alias, self.schemes)

        try:
            wconfig.writefile(cfg, fn)
            self._repoagent.pollStatus()
        except EnvironmentError as e:
            qtlib.WarningMsgBox(_('Unable to write configuration file'),
                                hglib.exception_str(e), parent=self)
        super(SecureDialog, self).accept()

    @pyqtSlot()
    def _updateUi(self):
        self._querybutton.setEnabled(self.fprintradio.isChecked()
                                     and self._querysess.isFinished())
        self._protocolcombo.setEnabled(not self.insecureradio.isChecked())


class PathsTree(QTreeView):
    removeAlias = pyqtSignal(str)
    menuRequest = pyqtSignal(QPoint, str, str, bool)

    def __init__(self, parent, editable):
        QTreeView.__init__(self, parent)
        self.setDragDropMode(QTreeView.DragDropMode.DragOnly)
        self.setSelectionMode(QTreeView.SelectionMode.SingleSelection)
        self.editable = editable

    def contextMenuEvent(self, event):
        for index in self.selectedRows():
            alias = index.data(Qt.ItemDataRole.DisplayRole)
            url = index.sibling(index.row(), 1).data(Qt.ItemDataRole.DisplayRole)
            self.menuRequest.emit(event.globalPos(), url, alias, self.editable)
            return

    def keyPressEvent(self, event):
        if self.editable and event.matches(QKeySequence.StandardKey.Delete):
            self.deleteSelected()
        else:
            return super(PathsTree, self).keyPressEvent(event)

    def deleteSelected(self):
        for index in self.selectedRows():
            alias = index.data(Qt.ItemDataRole.DisplayRole)
            r = qtlib.QuestionMsgBox(_('Confirm path delete'),
                    _('Delete %s from your repo configuration file?') % alias,
                    parent=self)
            if r:
                self.removeAlias.emit(alias)

    def selectedRows(self):
        return self.selectionModel().selectedRows()

class PathsModel(QAbstractTableModel):
    def __init__(self, pathlist, parent=None):
        # type: (Iterable[Tuple[bytes, bytes]], Optional[QWidget]) -> None
        QAbstractTableModel.__init__(self, parent)
        self.headers = (_('Alias'), _('URL'))
        self.rows = []
        for alias, path in sorted(pathlist):
            safepath = urlutil.hidepassword(path)
            ualias = hglib.tounicode(alias)
            usafepath = hglib.tounicode(safepath)
            self.rows.append([ualias, usafepath, path])

    def rowCount(self, parent=QModelIndex()):
        if parent.isValid():
            return 0 # no child
        return len(self.rows)

    def columnCount(self, parent=QModelIndex()):
        if parent.isValid():
            return 0 # no child
        return len(self.headers)

    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
        if not index.isValid():
            return None
        if role == Qt.ItemDataRole.DisplayRole:
            return self.rows[index.row()][index.column()]
        return None

    def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole):
        if role != Qt.ItemDataRole.DisplayRole or orientation != Qt.Orientation.Horizontal:
            return None
        else:
            return self.headers[col]

    def mimeData(self, indexes):
        urls = []
        for i in indexes:
            u = QUrl()
            u.setPath(self.rows[i.row()][1])
            urls.append(u)

        m = QMimeData()
        m.setUrls(urls)
        return m

    def mimeTypes(self):
        return ['text/uri-list']

    def flags(self, index):
        flags = Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsDragEnabled
        return flags

    def realUrl(self, index):
        return self.rows[index.row()][2]



class OptionsDialog(QDialog):
    'Utility dialog for configuring uncommon options'
    def __init__(self, repoagent, opts, parent):
        # type: (thgrepo.RepoAgent, Dict[str, bytes], QWidget) -> None
        QDialog.__init__(self, parent)
        self.setWindowTitle(_('%s - sync options') % repoagent.displayName())

        layout = QVBoxLayout()
        self.setLayout(layout)

        self.newbranchcb = QCheckBox(
            _('Allow push of a new branch (--new-branch)'))
        self.newbranchcb.setChecked(opts.get('new-branch', False))
        layout.addWidget(self.newbranchcb)

        self.forcecb = QCheckBox(
            _('Force push or pull (override safety checks, --force)'))
        self.forcecb.setChecked(opts.get('force', False))
        layout.addWidget(self.forcecb)

        repo = repoagent.rawRepo()
        self.noproxycb = QCheckBox(
            _('Temporarily disable configured HTTP proxy'))
        self.noproxycb.setChecked(opts.get('noproxy', False))
        layout.addWidget(self.noproxycb)
        proxy = repoagent.configString('http_proxy', 'host')
        self.noproxycb.setEnabled(bool(proxy))

        self.debugcb = QCheckBox(
            _('Emit debugging output (--debug)'))
        self.debugcb.setChecked(opts.get('debug', False))
        layout.addWidget(self.debugcb)

        self.mqcb = QCheckBox(_('Work on patch queue (--mq)'))
        self.mqcb.setChecked(opts.get('mq', False))
        self.mqcb.setVisible(b'mq' in repo.extensions())
        layout.addWidget(self.mqcb)

        form = QFormLayout()
        layout.addLayout(form)

        lbl = QLabel(_('Remote command:'))
        self.remotele = QLineEdit()
        if opts.get('remotecmd'):
            self.remotele.setText(hglib.tounicode(opts['remotecmd']))
        form.addRow(lbl, self.remotele)

        lbl = QLabel(_('Branch:'))
        self.branchle = QLineEdit()
        if opts.get('branch'):
            self.branchle.setText(hglib.tounicode(opts['branch']))
        form.addRow(lbl, self.branchle)

        bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Save|QDialogButtonBox.StandardButton.Cancel)
        bb.accepted.connect(self.accept)
        bb.rejected.connect(self.reject)
        self.bb = bb
        layout.addWidget(bb)

    def accept(self):
        outopts = {}  # type: Dict[str, Union[bool, bytes]]
        for name, le in (('remotecmd', self.remotele),
                         ('branch', self.branchle)):
            outopts[name] = hglib.fromunicode(le.text()).strip()

        outopts['force'] = self.forcecb.isChecked()
        outopts['new-branch'] = self.newbranchcb.isChecked()
        outopts['noproxy'] = self.noproxycb.isChecked()
        outopts['debug'] = self.debugcb.isChecked()
        if self.mqcb.isVisibleTo(self):
            outopts['mq'] = self.mqcb.isChecked()

        self.outopts = outopts
        QDialog.accept(self)
