1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2010 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing the version control systems interface to Mercurial.
8"""
9
10import os
11import shutil
12import contextlib
13
14from PyQt5.QtCore import (
15    pyqtSignal, QFileInfo, QFileSystemWatcher, QCoreApplication
16)
17from PyQt5.QtWidgets import QApplication, QDialog, QInputDialog
18
19from E5Gui.E5Application import e5App
20from E5Gui import E5MessageBox, E5FileDialog
21
22from QScintilla.MiniEditor import MiniEditor
23
24from VCS.VersionControl import VersionControl
25from VCS.RepositoryInfoDialog import VcsRepositoryInfoDialog
26
27from .HgDialog import HgDialog
28from .HgClient import HgClient
29
30import Utilities
31
32
33class Hg(VersionControl):
34    """
35    Class implementing the version control systems interface to Mercurial.
36
37    @signal committed() emitted after the commit action has completed
38    @signal activeExtensionsChanged() emitted when the list of active
39        extensions has changed
40    @signal iniFileChanged() emitted when a Mercurial/repo configuration file
41        has changed
42    """
43    committed = pyqtSignal()
44    activeExtensionsChanged = pyqtSignal()
45    iniFileChanged = pyqtSignal()
46
47    IgnoreFileName = ".hgignore"
48
49    def __init__(self, plugin, parent=None, name=None):
50        """
51        Constructor
52
53        @param plugin reference to the plugin object
54        @param parent parent widget (QWidget)
55        @param name name of this object (string)
56        """
57        VersionControl.__init__(self, parent, name)
58        self.defaultOptions = {
59            'global': [''],
60            'commit': [''],
61            'checkout': [''],
62            'update': [''],
63            'add': [''],
64            'remove': [''],
65            'diff': [''],
66            'log': [''],
67            'history': [''],
68            'status': [''],
69            'tag': [''],
70            'export': ['']
71        }
72
73        self.__plugin = plugin
74        self.__ui = parent
75
76        self.options = self.defaultOptions
77        self.tagsList = []
78        self.branchesList = []
79        self.allTagsBranchesList = []
80        self.bookmarksList = []
81        self.showedTags = False
82        self.showedBranches = False
83
84        self.tagTypeList = [
85            'tags',
86            'branches',
87        ]
88
89        self.commandHistory = []
90
91        if "HG_ASP_DOT_NET_HACK" in os.environ:
92            self.adminDir = '_hg'
93        else:
94            self.adminDir = '.hg'
95
96        self.logBrowser = None
97        self.logBrowserIncoming = None
98        self.logBrowserOutgoing = None
99        self.diff = None
100        self.sbsDiff = None
101        self.status = None
102        self.summary = None
103        self.tagbranchList = None
104        self.annotate = None
105        self.repoEditor = None
106        self.serveDlg = None
107        self.bookmarksListDlg = None
108        self.bookmarksInOutDlg = None
109        self.conflictsDlg = None
110
111        self.bundleFile = None
112        self.__lastChangeGroupPath = None
113
114        self.statusCache = {}
115
116        self.__commitData = {}
117        self.__commitDialog = None
118
119        self.__forgotNames = []
120
121        self.__activeExtensions = []
122
123        from .HgUtilities import getConfigPath
124        self.__iniWatcher = QFileSystemWatcher(self)
125        self.__iniWatcher.fileChanged.connect(self.__iniFileChanged)
126        cfgFile = getConfigPath()
127        if os.path.exists(cfgFile):
128            self.__iniWatcher.addPath(cfgFile)
129
130        self.__client = None
131        self.__createClient()
132        self.__projectHelper = None
133
134        self.__repoDir = ""
135        self.__repoIniFile = ""
136        self.__defaultConfigured = False
137        self.__defaultPushConfigured = False
138
139        # instantiate the extensions
140        from .QueuesExtension.queues import Queues
141        from .PurgeExtension.purge import Purge
142        from .GpgExtension.gpg import Gpg
143        from .RebaseExtension.rebase import Rebase
144        from .ShelveExtension.shelve import Shelve
145        from .LargefilesExtension.largefiles import Largefiles
146        from .StripExtension.strip import Strip
147        from .HisteditExtension.histedit import Histedit
148        from .CloseheadExtension.closehead import Closehead
149        self.__extensions = {
150            "mq": Queues(self),
151            "purge": Purge(self),
152            "gpg": Gpg(self),
153            "rebase": Rebase(self),
154            "shelve": Shelve(self),
155            "largefiles": Largefiles(self),
156            "strip": Strip(self),
157            "histedit": Histedit(self),
158            "closehead": Closehead(self),
159        }
160
161    def getPlugin(self):
162        """
163        Public method to get a reference to the plugin object.
164
165        @return reference to the plugin object (VcsMercurialPlugin)
166        """
167        return self.__plugin
168
169    def getEncoding(self):
170        """
171        Public method to get the encoding to be used by Mercurial.
172
173        @return encoding (string)
174        """
175        return self.__plugin.getPreferences("Encoding")
176
177    def vcsShutdown(self):
178        """
179        Public method used to shutdown the Mercurial interface.
180        """
181        if self.logBrowser is not None:
182            self.logBrowser.close()
183        if self.logBrowserIncoming is not None:
184            self.logBrowserIncoming.close()
185        if self.logBrowserOutgoing is not None:
186            self.logBrowserOutgoing.close()
187        if self.diff is not None:
188            self.diff.close()
189        if self.sbsDiff is not None:
190            self.sbsDiff.close()
191        if self.status is not None:
192            self.status.close()
193        if self.summary is not None:
194            self.summary.close()
195        if self.tagbranchList is not None:
196            self.tagbranchList.close()
197        if self.annotate is not None:
198            self.annotate.close()
199        if self.serveDlg is not None:
200            self.serveDlg.close()
201
202        if self.bookmarksListDlg is not None:
203            self.bookmarksListDlg.close()
204        if self.bookmarksInOutDlg is not None:
205            self.bookmarksInOutDlg.close()
206
207        if self.conflictsDlg is not None:
208            self.conflictsDlg.close()
209
210        if self.bundleFile and os.path.exists(self.bundleFile):
211            os.remove(self.bundleFile)
212
213        # shut down the project helpers
214        if self.__projectHelper is not None:
215            self.__projectHelper.shutdown()
216
217        # shut down the extensions
218        for extension in self.__extensions.values():
219            extension.shutdown()
220
221        # shut down the client
222        self.__client and self.__client.stopServer()
223
224    def initCommand(self, command):
225        """
226        Public method to initialize a command arguments list.
227
228        @param command command name (string)
229        @return list of command options (list of string)
230        """
231        args = [command]
232        self.addArguments(args, self.__plugin.getGlobalOptions())
233        return args
234
235    def vcsExists(self):
236        """
237        Public method used to test for the presence of the hg executable.
238
239        @return flag indicating the existence (boolean) and an error message
240            (string)
241        """
242        from .HgUtilities import hgVersion
243
244        self.versionStr, self.version, errMsg = hgVersion(self.__plugin)
245        hgExists = errMsg == ""
246        if hgExists:
247            self.__getExtensionsInfo()
248        return hgExists, errMsg
249
250    def vcsInit(self, vcsDir, noDialog=False):
251        """
252        Public method used to initialize the mercurial repository.
253
254        The initialization is done, when a project is converted into a
255        Mercurial controlled project. Therefore we always return TRUE without
256        doing anything.
257
258        @param vcsDir name of the VCS directory (string)
259        @param noDialog flag indicating quiet operations (boolean)
260        @return always TRUE
261        """
262        return True
263
264    def vcsConvertProject(self, vcsDataDict, project, addAll=True):
265        """
266        Public method to convert an uncontrolled project to a version
267        controlled project.
268
269        @param vcsDataDict dictionary of data required for the conversion
270        @type dict
271        @param project reference to the project object
272        @type Project
273        @param addAll flag indicating to add all files to the repository
274        @type bool
275        """
276        success = self.vcsImport(vcsDataDict, project.ppath, addAll=addAll)[0]
277        if not success:
278            E5MessageBox.critical(
279                self.__ui,
280                self.tr("Create project repository"),
281                self.tr(
282                    """The project repository could not be created."""))
283        else:
284            pfn = project.pfile
285            if not os.path.isfile(pfn):
286                pfn += "z"
287            project.closeProject()
288            project.openProject(pfn)
289
290    def vcsImport(self, vcsDataDict, projectDir, noDialog=False, addAll=True):
291        """
292        Public method used to import the project into the Mercurial repository.
293
294        @param vcsDataDict dictionary of data required for the import
295        @type dict
296        @param projectDir project directory (string)
297        @type str
298        @param noDialog flag indicating quiet operations
299        @type bool
300        @param addAll flag indicating to add all files to the repository
301        @type bool
302        @return tuple containing a flag indicating an execution without errors
303            and a flag indicating the version controll status
304        @rtype tuple of (bool, bool)
305        """
306        msg = vcsDataDict["message"]
307        if not msg:
308            msg = '***'
309
310        args = self.initCommand("init")
311        args.append(projectDir)
312        dia = HgDialog(self.tr('Creating Mercurial repository'), self)
313        res = dia.startProcess(args)
314        if res:
315            dia.exec()
316        status = dia.normalExit()
317
318        if status:
319            self.stopClient()
320            self.__repoDir = projectDir
321
322            ignoreName = os.path.join(projectDir, Hg.IgnoreFileName)
323            if not os.path.exists(ignoreName):
324                status = self.hgCreateIgnoreFile(projectDir)
325
326            if status and addAll:
327                args = self.initCommand("commit")
328                args.append('--addremove')
329                args.append('--message')
330                args.append(msg)
331                dia = HgDialog(
332                    self.tr('Initial commit to Mercurial repository'),
333                    self)
334                res = dia.startProcess(args)
335                if res:
336                    dia.exec()
337                status = dia.normalExit()
338
339        return status, False
340
341    def vcsCheckout(self, vcsDataDict, projectDir, noDialog=False):
342        """
343        Public method used to check the project out of a Mercurial repository
344        (clone).
345
346        @param vcsDataDict dictionary of data required for the checkout
347        @param projectDir project directory to create (string)
348        @param noDialog flag indicating quiet operations
349        @return flag indicating an execution without errors (boolean)
350        """
351        noDialog = False
352        try:
353            rev = vcsDataDict["revision"]
354        except KeyError:
355            rev = None
356        vcsUrl = self.hgNormalizeURL(vcsDataDict["url"])
357
358        args = self.initCommand("clone")
359        if rev:
360            args.append("--rev")
361            args.append(rev)
362        if vcsDataDict["largefiles"]:
363            args.append("--all-largefiles")
364        args.append(vcsUrl)
365        args.append(projectDir)
366
367        if noDialog:
368            out, err = self.__client.runcommand(args)
369            return err == ""
370        else:
371            dia = HgDialog(
372                self.tr('Cloning project from a Mercurial repository'),
373                self)
374            res = dia.startProcess(args)
375            if res:
376                dia.exec()
377            return dia.normalExit()
378
379    def vcsExport(self, vcsDataDict, projectDir):
380        """
381        Public method used to export a directory from the Mercurial repository.
382
383        @param vcsDataDict dictionary of data required for the checkout
384        @param projectDir project directory to create (string)
385        @return flag indicating an execution without errors (boolean)
386        """
387        status = self.vcsCheckout(vcsDataDict, projectDir)
388        shutil.rmtree(os.path.join(projectDir, self.adminDir), True)
389        if os.path.exists(os.path.join(projectDir, Hg.IgnoreFileName)):
390            os.remove(os.path.join(projectDir, Hg.IgnoreFileName))
391        return status
392
393    def vcsCommit(self, name, message, noDialog=False, closeBranch=False,
394                  mq=False, merge=False):
395        """
396        Public method used to make the change of a file/directory permanent
397        in the Mercurial repository.
398
399        @param name file/directory name to be committed (string or list of
400            strings)
401        @param message message for this operation (string)
402        @param noDialog flag indicating quiet operations
403        @param closeBranch flag indicating a close branch commit (boolean)
404        @param mq flag indicating a queue commit (boolean)
405        @param merge flag indicating a merge commit (boolean)
406        """
407        msg = message
408
409        if mq or merge:
410            # ensure dialog is shown for a queue commit
411            noDialog = False
412
413        if not noDialog:
414            # call CommitDialog and get message from there
415            if self.__commitDialog is None:
416                from .HgCommitDialog import HgCommitDialog
417                self.__commitDialog = HgCommitDialog(self, msg, mq, merge,
418                                                     self.__ui)
419                self.__commitDialog.accepted.connect(self.__vcsCommit_Step2)
420            self.__commitDialog.show()
421            self.__commitDialog.raise_()
422            self.__commitDialog.activateWindow()
423
424        self.__commitData["name"] = name
425        self.__commitData["msg"] = msg
426        self.__commitData["noDialog"] = noDialog
427        self.__commitData["closeBranch"] = closeBranch
428        self.__commitData["mq"] = mq
429        self.__commitData["merge"] = merge
430
431        if noDialog:
432            self.__vcsCommit_Step2()
433
434    def __vcsCommit_Step2(self):
435        """
436        Private slot performing the second step of the commit action.
437        """
438        name = self.__commitData["name"]
439        msg = self.__commitData["msg"]
440        noDialog = self.__commitData["noDialog"]
441        closeBranch = self.__commitData["closeBranch"]
442        mq = self.__commitData["mq"]
443        merge = self.__commitData["merge"]
444
445        if not noDialog:
446            # check, if there are unsaved changes, that should be committed
447            if isinstance(name, list):
448                nameList = name
449            else:
450                nameList = [name]
451            ok = True
452            for nam in nameList:
453                # check for commit of the project
454                if os.path.isdir(nam):
455                    project = e5App().getObject("Project")
456                    if nam == project.getProjectPath():
457                        ok &= (
458                            project.checkAllScriptsDirty(
459                                reportSyntaxErrors=True) and
460                            project.checkDirty()
461                        )
462                        continue
463                elif os.path.isfile(nam):
464                    editor = (
465                        e5App().getObject("ViewManager").getOpenEditor(nam)
466                    )
467                    if editor:
468                        ok &= editor.checkDirty()
469                if not ok:
470                    break
471
472            if not ok:
473                res = E5MessageBox.yesNo(
474                    self.__ui,
475                    self.tr("Commit Changes"),
476                    self.tr(
477                        """The commit affects files, that have unsaved"""
478                        """ changes. Shall the commit be continued?"""),
479                    icon=E5MessageBox.Warning)
480                if not res:
481                    return
482
483        if self.__commitDialog is not None:
484            (msg, amend, commitSubrepositories, author,
485             dateTime) = self.__commitDialog.getCommitData()
486            self.__commitDialog.deleteLater()
487            self.__commitDialog = None
488            if amend and not msg:
489                msg = self.__getMostRecentCommitMessage()
490        else:
491            amend = False
492            commitSubrepositories = False
493            author = ""
494            dateTime = ""
495
496        if not msg and not amend:
497            msg = '***'
498
499        args = self.initCommand("commit")
500        args.append("-v")
501        if mq:
502            args.append("--mq")
503        elif merge:
504            if author:
505                args.append("--user")
506                args.append(author)
507            if dateTime:
508                args.append("--date")
509                args.append(dateTime)
510        else:
511            if closeBranch:
512                args.append("--close-branch")
513            if amend:
514                args.append("--amend")
515            if commitSubrepositories:
516                args.append("--subrepos")
517            if author:
518                args.append("--user")
519                args.append(author)
520            if dateTime:
521                args.append("--date")
522                args.append(dateTime)
523        if msg:
524            args.append("--message")
525            args.append(msg)
526        if isinstance(name, list):
527            self.addArguments(args, name)
528        else:
529            args.append(name)
530
531        dia = HgDialog(
532            self.tr('Committing changes to Mercurial repository'),
533            self)
534        res = dia.startProcess(args)
535        if res:
536            dia.exec()
537        self.committed.emit()
538        if self.__forgotNames:
539            model = e5App().getObject("Project").getModel()
540            for name in self.__forgotNames:
541                model.updateVCSStatus(name)
542            self.__forgotNames = []
543        self.checkVCSStatus()
544
545    def __getMostRecentCommitMessage(self):
546        """
547        Private method to get the most recent commit message.
548
549        Note: This message is extracted from the parent commit of the
550        working directory.
551
552        @return most recent commit message
553        @rtype str
554        """
555        args = self.initCommand("log")
556        args.append("--rev")
557        args.append(".")
558        args.append('--template')
559        args.append('{desc}')
560
561        output, error = self.__client.runcommand(args)
562
563        return output
564
565    def vcsUpdate(self, name=None, noDialog=False, revision=None):
566        """
567        Public method used to update a file/directory with the Mercurial
568        repository.
569
570        @param name file/directory name to be updated (not used)
571        @param noDialog flag indicating quiet operations (boolean)
572        @param revision revision to update to (string)
573        @return flag indicating, that the update contained an add
574            or delete (boolean)
575        """
576        args = self.initCommand("update")
577        if "-v" not in args and "--verbose" not in args:
578            args.append("-v")
579        if revision:
580            args.append("-r")
581            args.append(revision)
582
583        if noDialog:
584            out, err = self.__client.runcommand(args)
585            res = False
586        else:
587            dia = HgDialog(self.tr(
588                'Synchronizing with the Mercurial repository'),
589                self)
590            res = dia.startProcess(args)
591            if res:
592                dia.exec()
593                res = dia.hasAddOrDelete()
594        self.checkVCSStatus()
595        return res
596
597    def vcsAdd(self, name, isDir=False, noDialog=False):
598        """
599        Public method used to add a file/directory to the Mercurial repository.
600
601        @param name file/directory name to be added (string)
602        @param isDir flag indicating name is a directory (boolean)
603        @param noDialog flag indicating quiet operations
604        """
605        args = self.initCommand("add")
606        args.append("-v")
607
608        if isinstance(name, list):
609            self.addArguments(args, name)
610        else:
611            args.append(name)
612
613        if noDialog:
614            out, err = self.__client.runcommand(args)
615        else:
616            dia = HgDialog(
617                self.tr(
618                    'Adding files/directories to the Mercurial repository'),
619                self)
620            res = dia.startProcess(args)
621            if res:
622                dia.exec()
623
624    def vcsAddBinary(self, name, isDir=False):
625        """
626        Public method used to add a file/directory in binary mode to the
627        Mercurial repository.
628
629        @param name file/directory name to be added (string)
630        @param isDir flag indicating name is a directory (boolean)
631        """
632        self.vcsAdd(name, isDir)
633
634    def vcsAddTree(self, path):
635        """
636        Public method to add a directory tree rooted at path to the Mercurial
637        repository.
638
639        @param path root directory of the tree to be added (string or list of
640            strings))
641        """
642        self.vcsAdd(path, isDir=False)
643
644    def vcsRemove(self, name, project=False, noDialog=False):
645        """
646        Public method used to remove a file/directory from the Mercurial
647        repository.
648
649        The default operation is to remove the local copy as well.
650
651        @param name file/directory name to be removed (string or list of
652            strings))
653        @param project flag indicating deletion of a project tree (boolean)
654            (not needed)
655        @param noDialog flag indicating quiet operations
656        @return flag indicating successfull operation (boolean)
657        """
658        args = self.initCommand("remove")
659        args.append("-v")
660        if noDialog and '--force' not in args:
661            args.append('--force')
662
663        if isinstance(name, list):
664            self.addArguments(args, name)
665        else:
666            args.append(name)
667
668        if noDialog:
669            out, err = self.__client.runcommand(args)
670            res = err == ""
671        else:
672            dia = HgDialog(
673                self.tr(
674                    'Removing files/directories from the Mercurial'
675                    ' repository'),
676                self)
677            res = dia.startProcess(args)
678            if res:
679                dia.exec()
680                res = dia.normalExitWithoutErrors()
681
682        return res
683
684    def vcsMove(self, name, project, target=None, noDialog=False):
685        """
686        Public method used to move a file/directory.
687
688        @param name file/directory name to be moved (string)
689        @param project reference to the project object
690        @param target new name of the file/directory (string)
691        @param noDialog flag indicating quiet operations
692        @return flag indicating successfull operation (boolean)
693        """
694        isDir = os.path.isdir(name)
695
696        res = False
697        if noDialog:
698            if target is None:
699                return False
700            force = True
701            accepted = True
702        else:
703            from .HgCopyDialog import HgCopyDialog
704            dlg = HgCopyDialog(name, None, True)
705            accepted = dlg.exec() == QDialog.DialogCode.Accepted
706            if accepted:
707                target, force = dlg.getData()
708
709        if accepted:
710            args = self.initCommand("rename")
711            args.append("-v")
712            if force:
713                args.append('--force')
714            args.append(name)
715            args.append(target)
716
717            if noDialog:
718                out, err = self.__client.runcommand(args)
719                res = err == ""
720            else:
721                dia = HgDialog(self.tr('Renaming {0}').format(name), self)
722                res = dia.startProcess(args)
723                if res:
724                    dia.exec()
725                    res = dia.normalExit()
726            if res:
727                if target.startswith(project.getProjectPath()):
728                    if isDir:
729                        project.moveDirectory(name, target)
730                    else:
731                        project.renameFileInPdata(name, target)
732                else:
733                    if isDir:
734                        project.removeDirectory(name)
735                    else:
736                        project.removeFile(name)
737        return res
738
739    def vcsDiff(self, name):
740        """
741        Public method used to view the difference of a file/directory to the
742        Mercurial repository.
743
744        If name is a directory and is the project directory, all project files
745        are saved first. If name is a file (or list of files), which is/are
746        being edited and has unsaved modification, they can be saved or the
747        operation may be aborted.
748
749        @param name file/directory name to be diffed (string)
750        """
751        names = name[:] if isinstance(name, list) else [name]
752        for nam in names:
753            if os.path.isfile(nam):
754                editor = e5App().getObject("ViewManager").getOpenEditor(nam)
755                if editor and not editor.checkDirty():
756                    return
757            else:
758                project = e5App().getObject("Project")
759                if nam == project.ppath and not project.saveAllScripts():
760                    return
761        if self.diff is None:
762            from .HgDiffDialog import HgDiffDialog
763            self.diff = HgDiffDialog(self)
764        self.diff.show()
765        self.diff.raise_()
766        QApplication.processEvents()
767        self.diff.start(name, refreshable=True)
768
769    def vcsStatus(self, name):
770        """
771        Public method used to view the status of files/directories in the
772        Mercurial repository.
773
774        @param name file/directory name(s) to show the status of
775            (string or list of strings)
776        """
777        if self.status is None:
778            from .HgStatusDialog import HgStatusDialog
779            self.status = HgStatusDialog(self)
780        self.status.show()
781        self.status.raise_()
782        self.status.start(name)
783
784    def hgSummary(self, mq=False, largefiles=False):
785        """
786        Public method used to show some summary information of the
787        working directory state.
788
789        @param mq flag indicating to show the queue status as well (boolean)
790        @param largefiles flag indicating to show the largefiles status as
791            well (boolean)
792        """
793        if self.summary is None:
794            from .HgSummaryDialog import HgSummaryDialog
795            self.summary = HgSummaryDialog(self)
796        self.summary.show()
797        self.summary.raise_()
798        self.summary.start(mq=mq, largefiles=largefiles)
799
800    def vcsTag(self, name=None, revision=None, tagName=None):
801        """
802        Public method used to set/remove a tag in the Mercurial repository.
803
804        @param name file/directory name to determine the repo root from
805            (string)
806        @param revision revision to set tag for (string)
807        @param tagName name of the tag (string)
808        @return flag indicating a performed tag action (boolean)
809        """
810        from .HgTagDialog import HgTagDialog
811        dlg = HgTagDialog(self.hgGetTagsList(withType=True),
812                          revision, tagName)
813        if dlg.exec() == QDialog.DialogCode.Accepted:
814            tag, revision, tagOp, force = dlg.getParameters()
815        else:
816            return False
817
818        args = self.initCommand("tag")
819        msgPart = ""
820        if tagOp in [HgTagDialog.CreateLocalTag, HgTagDialog.DeleteLocalTag]:
821            args.append('--local')
822            msgPart = "local "
823        else:
824            msgPart = "global "
825        if tagOp in [HgTagDialog.DeleteGlobalTag, HgTagDialog.DeleteLocalTag]:
826            args.append('--remove')
827        if (
828            tagOp in [
829                HgTagDialog.CreateGlobalTag, HgTagDialog.CreateLocalTag] and
830            revision
831        ):
832            args.append("--rev")
833            args.append(revision)
834        if force:
835            args.append("--force")
836        args.append('--message')
837        if tagOp in [HgTagDialog.CreateGlobalTag, HgTagDialog.CreateLocalTag]:
838            tag = tag.strip().replace(" ", "_")
839            args.append("Created {1}tag <{0}>.".format(tag, msgPart))
840        else:
841            args.append("Removed {1}tag <{0}>.".format(tag, msgPart))
842        args.append(tag)
843
844        dia = HgDialog(self.tr('Tagging in the Mercurial repository'),
845                       self)
846        res = dia.startProcess(args)
847        if res:
848            dia.exec()
849
850        return True
851
852    def hgRevert(self, name):
853        """
854        Public method used to revert changes made to a file/directory.
855
856        @param name file/directory name to be reverted (string)
857        @return flag indicating, that the update contained an add
858            or delete (boolean)
859        """
860        args = self.initCommand("revert")
861        if not self.getPlugin().getPreferences("CreateBackup"):
862            args.append("--no-backup")
863        args.append("-v")
864        if isinstance(name, list):
865            self.addArguments(args, name)
866            names = name[:]
867        else:
868            args.append(name)
869            names = [name]
870
871        project = e5App().getObject("Project")
872        names = [project.getRelativePath(nam) for nam in names]
873        if names[0]:
874            from UI.DeleteFilesConfirmationDialog import (
875                DeleteFilesConfirmationDialog
876            )
877            dlg = DeleteFilesConfirmationDialog(
878                self.parent(),
879                self.tr("Revert changes"),
880                self.tr(
881                    "Do you really want to revert all changes to these files"
882                    " or directories?"),
883                names)
884            yes = dlg.exec() == QDialog.DialogCode.Accepted
885        else:
886            yes = E5MessageBox.yesNo(
887                None,
888                self.tr("Revert changes"),
889                self.tr("""Do you really want to revert all changes of"""
890                        """ the project?"""))
891        if yes:
892            dia = HgDialog(self.tr('Reverting changes'), self)
893            res = dia.startProcess(args)
894            if res:
895                dia.exec()
896                res = dia.hasAddOrDelete()
897            self.checkVCSStatus()
898        else:
899            res = False
900
901        return res
902
903    def vcsMerge(self, name, rev=""):
904        """
905        Public method used to merge a URL/revision into the local project.
906
907        @param name file/directory name to be merged
908        @type str
909        @param rev revision to merge with
910        @type str
911        """
912        if not rev:
913            from .HgMergeDialog import HgMergeDialog
914            dlg = HgMergeDialog(self.hgGetTagsList(),
915                                self.hgGetBranchesList(),
916                                self.hgGetBookmarksList())
917            if dlg.exec() == QDialog.DialogCode.Accepted:
918                rev, force = dlg.getParameters()
919            else:
920                return
921        else:
922            force = False
923
924        args = self.initCommand("merge")
925        if force:
926            args.append("--force")
927        if self.getPlugin().getPreferences("InternalMerge"):
928            args.append("--tool")
929            args.append("internal:merge")
930        if rev:
931            args.append("--rev")
932            args.append(rev)
933
934        dia = HgDialog(self.tr('Merging'), self)
935        res = dia.startProcess(args)
936        if res:
937            dia.exec()
938        self.checkVCSStatus()
939
940    def hgReMerge(self, name):
941        """
942        Public method used to merge a URL/revision into the local project.
943
944        @param name file/directory name to be merged (string)
945        """
946        args = self.initCommand("resolve")
947        if self.getPlugin().getPreferences("InternalMerge"):
948            args.append("--tool")
949            args.append("internal:merge")
950        if isinstance(name, list):
951            self.addArguments(args, name)
952            names = name[:]
953        else:
954            args.append(name)
955            names = [name]
956
957        project = e5App().getObject("Project")
958        names = [project.getRelativePath(nam) for nam in names]
959        if names[0]:
960            from UI.DeleteFilesConfirmationDialog import (
961                DeleteFilesConfirmationDialog
962            )
963            dlg = DeleteFilesConfirmationDialog(
964                self.parent(),
965                self.tr("Re-Merge"),
966                self.tr(
967                    "Do you really want to re-merge these files"
968                    " or directories?"),
969                names)
970            yes = dlg.exec() == QDialog.DialogCode.Accepted
971        else:
972            yes = E5MessageBox.yesNo(
973                None,
974                self.tr("Re-Merge"),
975                self.tr("""Do you really want to re-merge the project?"""))
976        if yes:
977            dia = HgDialog(self.tr('Re-Merging').format(name), self)
978            res = dia.startProcess(args)
979            if res:
980                dia.exec()
981            self.checkVCSStatus()
982
983    def vcsSwitch(self, name):
984        """
985        Public method used to switch a working directory to a different
986        revision.
987
988        @param name directory name to be switched (string)
989        @return flag indicating, that the switch contained an add
990            or delete (boolean)
991        """
992        from .HgRevisionSelectionDialog import HgRevisionSelectionDialog
993        dlg = HgRevisionSelectionDialog(self.hgGetTagsList(),
994                                        self.hgGetBranchesList(),
995                                        self.hgGetBookmarksList(),
996                                        self.tr("Current branch tip"))
997        if dlg.exec() == QDialog.DialogCode.Accepted:
998            rev = dlg.getRevision()
999            return self.vcsUpdate(name, revision=rev)
1000
1001        return False
1002
1003    def vcsRegisteredState(self, name):
1004        """
1005        Public method used to get the registered state of a file in the vcs.
1006
1007        @param name file or directory name to check
1008        @type str
1009        @return a combination of canBeCommited and canBeAdded
1010        @rtype int
1011        """
1012        if name.endswith(os.sep):
1013            name = name[:-1]
1014        name = os.path.normcase(name)
1015
1016        if (
1017            os.path.isdir(name) and
1018            os.path.isdir(os.path.join(name, self.adminDir))
1019        ):
1020            return self.canBeCommitted
1021
1022        if name in self.statusCache:
1023            return self.statusCache[name]
1024        args = self.initCommand("status")
1025        args.append('--all')
1026        args.append('--noninteractive')
1027
1028        output, error = self.__client.runcommand(args)
1029
1030        if output:
1031            repodir = self.getClient().getRepository()
1032            for line in output.splitlines():
1033                if len(line) > 2 and line[0] in "MARC!?I" and line[1] == " ":
1034                    flag, path = line.split(" ", 1)
1035                    absname = Utilities.normcasepath(
1036                        os.path.join(repodir, path))
1037                    if flag not in "?I" and absname == name:
1038                        return self.canBeCommitted
1039
1040        return self.canBeAdded
1041
1042    def vcsAllRegisteredStates(self, names, dname, shortcut=True):
1043        """
1044        Public method used to get the registered states of a number of files
1045        in the vcs.
1046
1047        <b>Note:</b> If a shortcut is to be taken, the code will only check,
1048        if the named directory has been scanned already. If so, it is assumed,
1049        that the states for all files have been populated by the previous run.
1050
1051        @param names dictionary with all filenames to be checked as keys
1052        @param dname directory to check in (string)
1053        @param shortcut flag indicating a shortcut should be taken (boolean)
1054        @return the received dictionary completed with a combination of
1055            canBeCommited and canBeAdded or None in order to signal an error
1056        """
1057        if dname.endswith(os.sep):
1058            dname = dname[:-1]
1059        dname = os.path.normcase(dname)
1060
1061        found = False
1062        for name in list(self.statusCache.keys()):
1063            if name in names:
1064                found = True
1065                names[name] = self.statusCache[name]
1066
1067        if not found:
1068            args = self.initCommand("status")
1069            args.append('--all')
1070            args.append('--noninteractive')
1071
1072            output, error = self.__client.runcommand(args)
1073
1074            if output:
1075                repoPath = self.getClient().getRepository()
1076                dirs = [x for x in names.keys() if os.path.isdir(x)]
1077                for line in output.splitlines():
1078                    if line and line[0] in "MARC!?I":
1079                        flag, path = line.split(" ", 1)
1080                        name = os.path.normcase(os.path.join(repoPath, path))
1081                        dirName = os.path.dirname(name)
1082                        if name.startswith(dname) and flag not in "?I":
1083                            if name in names:
1084                                names[name] = self.canBeCommitted
1085                            if dirName in names:
1086                                names[dirName] = self.canBeCommitted
1087                            if dirs:
1088                                for d in dirs:
1089                                    if name.startswith(d):
1090                                        names[d] = self.canBeCommitted
1091                                        dirs.remove(d)
1092                                        break
1093                        if flag not in "?I":
1094                            self.statusCache[name] = self.canBeCommitted
1095                            self.statusCache[dirName] = self.canBeCommitted
1096                        else:
1097                            self.statusCache[name] = self.canBeAdded
1098                            if dirName not in self.statusCache:
1099                                self.statusCache[dirName] = self.canBeAdded
1100
1101        return names
1102
1103    def clearStatusCache(self):
1104        """
1105        Public method to clear the status cache.
1106        """
1107        self.statusCache = {}
1108
1109    def vcsName(self):
1110        """
1111        Public method returning the name of the vcs.
1112
1113        @return always 'Mercurial' (string)
1114        """
1115        return "Mercurial"
1116
1117    def vcsInitConfig(self, project):
1118        """
1119        Public method to initialize the VCS configuration.
1120
1121        This method ensures, that an ignore file exists.
1122
1123        @param project reference to the project (Project)
1124        """
1125        ppath = project.getProjectPath()
1126        if ppath:
1127            ignoreName = os.path.join(ppath, Hg.IgnoreFileName)
1128            if not os.path.exists(ignoreName):
1129                self.hgCreateIgnoreFile(project.getProjectPath(), autoAdd=True)
1130
1131    def vcsCleanup(self, name):
1132        """
1133        Public method used to cleanup the working directory.
1134
1135        @param name directory name to be cleaned up (string)
1136        """
1137        patterns = self.getPlugin().getPreferences("CleanupPatterns").split()
1138
1139        entries = []
1140        for pat in patterns:
1141            entries.extend(Utilities.direntries(name, True, pat))
1142
1143        for entry in entries:
1144            with contextlib.suppress(OSError):
1145                os.remove(entry)
1146
1147    def vcsCommandLine(self, name):
1148        """
1149        Public method used to execute arbitrary mercurial commands.
1150
1151        @param name directory name of the working directory (string)
1152        """
1153        from .HgCommandDialog import HgCommandDialog
1154        dlg = HgCommandDialog(self.commandHistory, name)
1155        if dlg.exec() == QDialog.DialogCode.Accepted:
1156            command = dlg.getData()
1157            commandList = Utilities.parseOptionString(command)
1158
1159            # This moves any previous occurrence of these arguments to the head
1160            # of the list.
1161            if command in self.commandHistory:
1162                self.commandHistory.remove(command)
1163            self.commandHistory.insert(0, command)
1164
1165            args = []
1166            self.addArguments(args, commandList)
1167
1168            dia = HgDialog(self.tr('Mercurial command'), self)
1169            res = dia.startProcess(args)
1170            if res:
1171                dia.exec()
1172
1173    def vcsOptionsDialog(self, project, archive, editable=False, parent=None):
1174        """
1175        Public method to get a dialog to enter repository info.
1176
1177        @param project reference to the project object
1178        @param archive name of the project in the repository (string)
1179        @param editable flag indicating that the project name is editable
1180            (boolean)
1181        @param parent parent widget (QWidget)
1182        @return reference to the instantiated options dialog (HgOptionsDialog)
1183        """
1184        from .HgOptionsDialog import HgOptionsDialog
1185        return HgOptionsDialog(self, project, parent)
1186
1187    def vcsNewProjectOptionsDialog(self, parent=None):
1188        """
1189        Public method to get a dialog to enter repository info for getting a
1190        new project.
1191
1192        @param parent parent widget (QWidget)
1193        @return reference to the instantiated options dialog
1194            (HgNewProjectOptionsDialog)
1195        """
1196        from .HgNewProjectOptionsDialog import HgNewProjectOptionsDialog
1197        return HgNewProjectOptionsDialog(self, parent)
1198
1199    def vcsRepositoryInfos(self, ppath):
1200        """
1201        Public method to retrieve information about the repository.
1202
1203        @param ppath local path to get the repository infos (string)
1204        @return string with ready formated info for display (string)
1205        """
1206        args = self.initCommand("parents")
1207        args.append('--template')
1208        args.append('{rev}:{node|short}@@@{tags}@@@{author|xmlescape}@@@'
1209                    '{date|isodate}@@@{branches}@@@{bookmarks}\n')
1210
1211        output, error = self.__client.runcommand(args)
1212
1213        infoBlock = []
1214        if output:
1215            for index, line in enumerate(output.splitlines(), start=1):
1216                (changeset, tags, author, date, branches,
1217                 bookmarks) = line.split("@@@")
1218                cdate, ctime = date.split()[:2]
1219                info = []
1220                info.append(QCoreApplication.translate(
1221                    "mercurial",
1222                    """<tr><td><b>Parent #{0}</b></td><td></td></tr>\n"""
1223                    """<tr><td><b>Changeset</b></td><td>{1}</td></tr>""")
1224                    .format(index, changeset))
1225                if tags:
1226                    info.append(QCoreApplication.translate(
1227                        "mercurial",
1228                        """<tr><td><b>Tags</b></td><td>{0}</td></tr>""")
1229                        .format('<br/>'.join(tags.split())))
1230                if bookmarks:
1231                    info.append(QCoreApplication.translate(
1232                        "mercurial",
1233                        """<tr><td><b>Bookmarks</b></td><td>{0}</td></tr>""")
1234                        .format('<br/>'.join(bookmarks.split())))
1235                if branches:
1236                    info.append(QCoreApplication.translate(
1237                        "mercurial",
1238                        """<tr><td><b>Branches</b></td><td>{0}</td></tr>""")
1239                        .format('<br/>'.join(branches.split())))
1240                info.append(QCoreApplication.translate(
1241                    "mercurial",
1242                    """<tr><td><b>Last author</b></td><td>{0}</td></tr>\n"""
1243                    """<tr><td><b>Committed date</b></td><td>{1}</td></tr>\n"""
1244                    """<tr><td><b>Committed time</b></td><td>{2}</td></tr>""")
1245                    .format(author, cdate, ctime))
1246                infoBlock.append("\n".join(info))
1247        infoStr = (
1248            """<tr></tr>{0}""".format("<tr></tr>".join(infoBlock))
1249            if infoBlock else
1250            ""
1251        )
1252
1253        url = ""
1254        args = self.initCommand("showconfig")
1255        args.append('paths.default')
1256
1257        output, error = self.__client.runcommand(args)
1258        url = output.splitlines()[0].strip() if output else ""
1259
1260        return QCoreApplication.translate(
1261            'mercurial',
1262            """<h3>Repository information</h3>\n"""
1263            """<p><table>\n"""
1264            """<tr><td><b>Mercurial V.</b></td><td>{0}</td></tr>\n"""
1265            """<tr></tr>\n"""
1266            """<tr><td><b>URL</b></td><td>{1}</td></tr>\n"""
1267            """{2}"""
1268            """</table></p>\n"""
1269        ).format(self.versionStr, url, infoStr)
1270
1271    def vcsSupportCommandOptions(self):
1272        """
1273        Public method to signal the support of user settable command options.
1274
1275        @return flag indicating the support  of user settable command options
1276            (boolean)
1277        """
1278        return False
1279
1280    ###########################################################################
1281    ## Private Mercurial specific methods are below.
1282    ###########################################################################
1283
1284    def hgNormalizeURL(self, url):
1285        """
1286        Public method to normalize a url for Mercurial.
1287
1288        @param url url string (string)
1289        @return properly normalized url for mercurial (string)
1290        """
1291        url = url.replace('\\', '/')
1292        if url.endswith('/'):
1293            url = url[:-1]
1294        urll = url.split('//')
1295        return "{0}//{1}".format(urll[0], '/'.join(urll[1:]))
1296
1297    def hgCopy(self, name, project):
1298        """
1299        Public method used to copy a file/directory.
1300
1301        @param name file/directory name to be copied (string)
1302        @param project reference to the project object
1303        @return flag indicating successful operation (boolean)
1304        """
1305        from .HgCopyDialog import HgCopyDialog
1306        dlg = HgCopyDialog(name)
1307        res = False
1308        if dlg.exec() == QDialog.DialogCode.Accepted:
1309            target, force = dlg.getData()
1310
1311            args = self.initCommand("copy")
1312            args.append("-v")
1313            args.append(name)
1314            args.append(target)
1315
1316            dia = HgDialog(
1317                self.tr('Copying {0}').format(name), self)
1318            res = dia.startProcess(args)
1319            if res:
1320                dia.exec()
1321                res = dia.normalExit()
1322                if (
1323                    res and
1324                    target.startswith(project.getProjectPath())
1325                ):
1326                    if os.path.isdir(name):
1327                        project.copyDirectory(name, target)
1328                    else:
1329                        project.appendFile(target)
1330        return res
1331
1332    def hgGetTagsList(self, withType=False):
1333        """
1334        Public method to get the list of tags.
1335
1336        @param withType flag indicating to get the tag type as well (boolean)
1337        @return list of tags (list of string) or list of tuples of
1338            tag name and flag indicating a local tag (list of tuple of string
1339            and boolean), if withType is True
1340        """
1341        args = self.initCommand("tags")
1342        args.append('--verbose')
1343
1344        output, error = self.__client.runcommand(args)
1345
1346        tagsList = []
1347        if output:
1348            for line in output.splitlines():
1349                li = line.strip().split()
1350                if li[-1][0] in "1234567890":
1351                    # last element is a rev:changeset
1352                    del li[-1]
1353                    isLocal = False
1354                else:
1355                    del li[-2:]
1356                    isLocal = True
1357                name = " ".join(li)
1358                if name not in ["tip", "default"]:
1359                    if withType:
1360                        tagsList.append((name, isLocal))
1361                    else:
1362                        tagsList.append(name)
1363
1364        if withType:
1365            return tagsList
1366        else:
1367            if tagsList:
1368                self.tagsList = tagsList
1369            return self.tagsList[:]
1370
1371    def hgGetBranchesList(self):
1372        """
1373        Public method to get the list of branches.
1374
1375        @return list of branches (list of string)
1376        """
1377        args = self.initCommand("branches")
1378        args.append('--closed')
1379
1380        output, error = self.__client.runcommand(args)
1381
1382        if output:
1383            self.branchesList = []
1384            for line in output.splitlines():
1385                li = line.strip().split()
1386                if li[-1][0] in "1234567890":
1387                    # last element is a rev:changeset
1388                    del li[-1]
1389                else:
1390                    del li[-2:]
1391                name = " ".join(li)
1392                if name not in ["tip", "default"]:
1393                    self.branchesList.append(name)
1394
1395        return self.branchesList[:]
1396
1397    def hgListTagBranch(self, tags=True):
1398        """
1399        Public method used to list the available tags or branches.
1400
1401        @param tags flag indicating listing of branches or tags
1402                (False = branches, True = tags)
1403        """
1404        from .HgTagBranchListDialog import HgTagBranchListDialog
1405        self.tagbranchList = HgTagBranchListDialog(self)
1406        self.tagbranchList.show()
1407        if tags:
1408            if not self.showedTags:
1409                self.showedTags = True
1410                allTagsBranchesList = self.allTagsBranchesList
1411            else:
1412                self.tagsList = []
1413                allTagsBranchesList = None
1414            self.tagbranchList.start(
1415                tags, self.tagsList, allTagsBranchesList)
1416        else:
1417            if not self.showedBranches:
1418                self.showedBranches = True
1419                allTagsBranchesList = self.allTagsBranchesList
1420            else:
1421                self.branchesList = []
1422                allTagsBranchesList = None
1423            self.tagbranchList.start(
1424                tags, self.branchesList, self.allTagsBranchesList)
1425
1426    def hgAnnotate(self, name):
1427        """
1428        Public method to show the output of the hg annotate command.
1429
1430        @param name file name to show the annotations for (string)
1431        """
1432        if self.annotate is None:
1433            from .HgAnnotateDialog import HgAnnotateDialog
1434            self.annotate = HgAnnotateDialog(self)
1435        self.annotate.show()
1436        self.annotate.raise_()
1437        self.annotate.start(name)
1438
1439    def hgExtendedDiff(self, name):
1440        """
1441        Public method used to view the difference of a file/directory to the
1442        Mercurial repository.
1443
1444        If name is a directory and is the project directory, all project files
1445        are saved first. If name is a file (or list of files), which is/are
1446        being edited and has unsaved modification, they can be saved or the
1447        operation may be aborted.
1448
1449        This method gives the chance to enter the revisions to be compared.
1450
1451        @param name file/directory name to be diffed (string)
1452        """
1453        names = name[:] if isinstance(name, list) else [name]
1454        for nam in names:
1455            if os.path.isfile(nam):
1456                editor = e5App().getObject("ViewManager").getOpenEditor(nam)
1457                if editor and not editor.checkDirty():
1458                    return
1459            else:
1460                project = e5App().getObject("Project")
1461                if nam == project.ppath and not project.saveAllScripts():
1462                    return
1463
1464        from .HgRevisionsSelectionDialog import HgRevisionsSelectionDialog
1465        dlg = HgRevisionsSelectionDialog(self.hgGetTagsList(),
1466                                         self.hgGetBranchesList(),
1467                                         self.hgGetBookmarksList())
1468        if dlg.exec() == QDialog.DialogCode.Accepted:
1469            revisions = dlg.getRevisions()
1470            if self.diff is None:
1471                from .HgDiffDialog import HgDiffDialog
1472                self.diff = HgDiffDialog(self)
1473            self.diff.show()
1474            self.diff.raise_()
1475            self.diff.start(name, revisions)
1476
1477    def __hgGetFileForRevision(self, name, rev=""):
1478        """
1479        Private method to get a file for a specific revision from the
1480        repository.
1481
1482        @param name file name to get from the repository (string)
1483        @param rev revision to retrieve (string)
1484        @return contents of the file (string) and an error message (string)
1485        """
1486        args = self.initCommand("cat")
1487        if rev:
1488            args.append("--rev")
1489            args.append(rev)
1490        args.append(name)
1491
1492        output, error = self.__client.runcommand(args)
1493
1494        # return file contents with 'universal newlines'
1495        return output.replace('\r\n', '\n').replace('\r', '\n'), error
1496
1497    def hgSbsDiff(self, name, extended=False, revisions=None):
1498        """
1499        Public method used to view the difference of a file to the Mercurial
1500        repository side-by-side.
1501
1502        @param name file name to be diffed (string)
1503        @param extended flag indicating the extended variant (boolean)
1504        @param revisions tuple of two revisions (tuple of strings)
1505        @exception ValueError raised to indicate an invalid name parameter
1506        """
1507        if isinstance(name, list):
1508            raise ValueError("Wrong parameter type")
1509
1510        if extended:
1511            from .HgRevisionsSelectionDialog import HgRevisionsSelectionDialog
1512            dlg = HgRevisionsSelectionDialog(self.hgGetTagsList(),
1513                                             self.hgGetBranchesList(),
1514                                             self.hgGetBookmarksList())
1515            if dlg.exec() == QDialog.DialogCode.Accepted:
1516                rev1, rev2 = dlg.getRevisions()
1517            else:
1518                return
1519        elif revisions:
1520            rev1, rev2 = revisions[0], revisions[1]
1521        else:
1522            rev1, rev2 = "", ""
1523
1524        output1, error = self.__hgGetFileForRevision(name, rev=rev1)
1525        if error:
1526            E5MessageBox.critical(
1527                self.__ui,
1528                self.tr("Mercurial Side-by-Side Difference"),
1529                error)
1530            return
1531        name1 = "{0} (rev. {1})".format(name, rev1 and rev1 or ".")
1532
1533        if rev2:
1534            output2, error = self.__hgGetFileForRevision(name, rev=rev2)
1535            if error:
1536                E5MessageBox.critical(
1537                    self.__ui,
1538                    self.tr("Mercurial Side-by-Side Difference"),
1539                    error)
1540                return
1541            name2 = "{0} (rev. {1})".format(name, rev2)
1542        else:
1543            try:
1544                with open(name, "r", encoding="utf-8") as f1:
1545                    output2 = f1.read()
1546                name2 = "{0} (Work)".format(name)
1547            except OSError:
1548                E5MessageBox.critical(
1549                    self.__ui,
1550                    self.tr("Mercurial Side-by-Side Difference"),
1551                    self.tr(
1552                        """<p>The file <b>{0}</b> could not be read.</p>""")
1553                    .format(name))
1554                return
1555
1556        if self.sbsDiff is None:
1557            from UI.CompareDialog import CompareDialog
1558            self.sbsDiff = CompareDialog()
1559        self.sbsDiff.show()
1560        self.sbsDiff.raise_()
1561        self.sbsDiff.compare(output1, output2, name1, name2)
1562
1563    def vcsLogBrowser(self, name=None, isFile=False):
1564        """
1565        Public method used to browse the log of a file/directory from the
1566        Mercurial repository.
1567
1568        @param name file/directory name to show the log of (string)
1569        @param isFile flag indicating log for a file is to be shown
1570            (boolean)
1571        """
1572        if name == self.getClient().getRepository():
1573            name = None
1574
1575        if self.logBrowser is None:
1576            from .HgLogBrowserDialog import HgLogBrowserDialog
1577            self.logBrowser = HgLogBrowserDialog(self)
1578        self.logBrowser.show()
1579        self.logBrowser.raise_()
1580        self.logBrowser.start(name=name, isFile=isFile)
1581
1582    def hgIncoming(self):
1583        """
1584        Public method used to view the log of incoming changes from the
1585        Mercurial repository.
1586        """
1587        if self.logBrowserIncoming is None:
1588            from .HgLogBrowserDialog import HgLogBrowserDialog
1589            self.logBrowserIncoming = HgLogBrowserDialog(
1590                self, mode="incoming")
1591        self.logBrowserIncoming.show()
1592        self.logBrowserIncoming.raise_()
1593        self.logBrowserIncoming.start()
1594
1595    def hgOutgoing(self):
1596        """
1597        Public method used to view the log of outgoing changes from the
1598        Mercurial repository.
1599        """
1600        if self.logBrowserOutgoing is None:
1601            from .HgLogBrowserDialog import HgLogBrowserDialog
1602            self.logBrowserOutgoing = HgLogBrowserDialog(
1603                self, mode="outgoing")
1604        self.logBrowserOutgoing.show()
1605        self.logBrowserOutgoing.raise_()
1606        self.logBrowserOutgoing.start()
1607
1608    def hgPull(self, revisions=None):
1609        """
1610        Public method used to pull changes from a remote Mercurial repository.
1611
1612        @param revisions list of revisions to be pulled
1613        @type list of str
1614        @return flag indicating, that the update contained an add
1615            or delete
1616        @rtype bool
1617        """
1618        if (
1619            self.getPlugin().getPreferences("PreferUnbundle") and
1620            self.bundleFile and
1621            os.path.exists(self.bundleFile) and
1622            revisions is None
1623        ):
1624            command = "unbundle"
1625            title = self.tr('Apply changegroups')
1626        else:
1627            command = "pull"
1628            title = self.tr('Pulling from a remote Mercurial repository')
1629
1630        args = self.initCommand(command)
1631        args.append('-v')
1632        if self.getPlugin().getPreferences("PullUpdate"):
1633            args.append('--update')
1634        if command == "unbundle":
1635            args.append(self.bundleFile)
1636        if revisions:
1637            for rev in revisions:
1638                args.append("--rev")
1639                args.append(rev)
1640
1641        dia = HgDialog(title, self)
1642        res = dia.startProcess(args)
1643        if res:
1644            dia.exec()
1645            res = dia.hasAddOrDelete()
1646        if (
1647            self.bundleFile and
1648            os.path.exists(self.bundleFile)
1649        ):
1650            os.remove(self.bundleFile)
1651            self.bundleFile = None
1652        self.checkVCSStatus()
1653        return res
1654
1655    def hgPush(self, force=False, newBranch=False, rev=None):
1656        """
1657        Public method used to push changes to a remote Mercurial repository.
1658
1659        @param force flag indicating a forced push (boolean)
1660        @param newBranch flag indicating to push a new branch (boolean)
1661        @param rev revision to be pushed (including all ancestors) (string)
1662        """
1663        args = self.initCommand("push")
1664        args.append('-v')
1665        if force:
1666            args.append('-f')
1667        if newBranch:
1668            args.append('--new-branch')
1669        if rev:
1670            args.append('--rev')
1671            args.append(rev)
1672
1673        dia = HgDialog(
1674            self.tr('Pushing to a remote Mercurial repository'), self)
1675        res = dia.startProcess(args)
1676        if res:
1677            dia.exec()
1678        self.checkVCSStatus()
1679
1680    def hgInfo(self, mode="heads"):
1681        """
1682        Public method to show information about the heads of the repository.
1683
1684        @param mode mode of the operation (string, one of heads, parents,
1685            tip)
1686        """
1687        if mode not in ("heads", "parents", "tip"):
1688            mode = "heads"
1689
1690        info = []
1691
1692        args = self.initCommand(mode)
1693        args.append('--template')
1694        args.append('{rev}:{node|short}@@@{tags}@@@{author|xmlescape}@@@'
1695                    '{date|isodate}@@@{branches}@@@{parents}@@@{bookmarks}\n')
1696
1697        output, error = self.__client.runcommand(args)
1698
1699        if output:
1700            for index, line in enumerate(output.splitlines(), start=1):
1701                (changeset, tags, author, date, branches, parents,
1702                 bookmarks) = line.split("@@@")
1703                cdate, ctime = date.split()[:2]
1704                info.append("""<p><table>""")
1705                if mode == "heads":
1706                    info.append(QCoreApplication.translate(
1707                        "mercurial",
1708                        """<tr><td><b>Head #{0}</b></td><td></td></tr>\n""")
1709                        .format(index))
1710                elif mode == "parents":
1711                    info.append(QCoreApplication.translate(
1712                        "mercurial",
1713                        """<tr><td><b>Parent #{0}</b></td><td></td></tr>\n""")
1714                        .format(index))
1715                elif mode == "tip":
1716                    info.append(QCoreApplication.translate(
1717                        "mercurial",
1718                        """<tr><td><b>Tip</b></td><td></td></tr>\n"""))
1719                info.append(QCoreApplication.translate(
1720                    "mercurial",
1721                    """<tr><td><b>Changeset</b></td><td>{0}</td></tr>""")
1722                    .format(changeset))
1723                if tags:
1724                    info.append(QCoreApplication.translate(
1725                        "mercurial",
1726                        """<tr><td><b>Tags</b></td><td>{0}</td></tr>""")
1727                        .format('<br/>'.join(tags.split())))
1728                if bookmarks:
1729                    info.append(QCoreApplication.translate(
1730                        "mercurial",
1731                        """<tr><td><b>Bookmarks</b></td><td>{0}</td></tr>""")
1732                        .format('<br/>'.join(bookmarks.split())))
1733                if branches:
1734                    info.append(QCoreApplication.translate(
1735                        "mercurial",
1736                        """<tr><td><b>Branches</b></td><td>{0}</td></tr>""")
1737                        .format('<br/>'.join(branches.split())))
1738                if parents:
1739                    info.append(QCoreApplication.translate(
1740                        "mercurial",
1741                        """<tr><td><b>Parents</b></td><td>{0}</td></tr>""")
1742                        .format('<br/>'.join(parents.split())))
1743                info.append(QCoreApplication.translate(
1744                    "mercurial",
1745                    """<tr><td><b>Last author</b></td><td>{0}</td></tr>\n"""
1746                    """<tr><td><b>Committed date</b></td><td>{1}</td></tr>\n"""
1747                    """<tr><td><b>Committed time</b></td><td>{2}</td></tr>\n"""
1748                    """</table></p>""")
1749                    .format(author, cdate, ctime))
1750
1751            dlg = VcsRepositoryInfoDialog(None, "\n".join(info))
1752            dlg.exec()
1753
1754    def hgConflicts(self):
1755        """
1756        Public method used to show a list of files containing conflicts.
1757        """
1758        if self.conflictsDlg is None:
1759            from .HgConflictsListDialog import HgConflictsListDialog
1760            self.conflictsDlg = HgConflictsListDialog(self)
1761        self.conflictsDlg.show()
1762        self.conflictsDlg.raise_()
1763        self.conflictsDlg.start()
1764
1765    def hgResolved(self, name, unresolve=False):
1766        """
1767        Public method used to resolve conflicts of a file/directory.
1768
1769        @param name file/directory name to be resolved (string)
1770        @param unresolve flag indicating to mark the file/directory as
1771            unresolved (boolean)
1772        """
1773        args = self.initCommand("resolve")
1774        if unresolve:
1775            args.append("--unmark")
1776        else:
1777            args.append("--mark")
1778
1779        if isinstance(name, list):
1780            self.addArguments(args, name)
1781        else:
1782            args.append(name)
1783
1784        title = (
1785            self.tr("Marking as 'unresolved'")
1786            if unresolve else
1787            self.tr("Marking as 'resolved'")
1788        )
1789        dia = HgDialog(title, self)
1790        res = dia.startProcess(args)
1791        if res:
1792            dia.exec()
1793        self.checkVCSStatus()
1794
1795    def hgAbortMerge(self):
1796        """
1797        Public method to abort an uncommitted merge.
1798
1799        @return flag indicating, that the abortion contained an add
1800            or delete (boolean)
1801        """
1802        if self.version >= (4, 5, 0):
1803            args = self.initCommand("merge")
1804            args.append("--abort")
1805        else:
1806            args = self.initCommand("update")
1807            args.append("--clean")
1808
1809        dia = HgDialog(
1810            self.tr('Aborting uncommitted merge'),
1811            self)
1812        res = dia.startProcess(args, showArgs=False)
1813        if res:
1814            dia.exec()
1815            res = dia.hasAddOrDelete()
1816        self.checkVCSStatus()
1817        return res
1818
1819    def hgBranch(self):
1820        """
1821        Public method used to create a branch in the Mercurial repository.
1822        """
1823        from .HgBranchInputDialog import HgBranchInputDialog
1824        dlg = HgBranchInputDialog(self.hgGetBranchesList())
1825        if dlg.exec() == QDialog.DialogCode.Accepted:
1826            name, commit = dlg.getData()
1827            name = name.strip().replace(" ", "_")
1828            args = self.initCommand("branch")
1829            args.append(name)
1830
1831            dia = HgDialog(
1832                self.tr('Creating branch in the Mercurial repository'),
1833                self)
1834            res = dia.startProcess(args)
1835            if res:
1836                dia.exec()
1837                if commit:
1838                    project = e5App().getObject("Project")
1839                    self.vcsCommit(
1840                        project.getProjectPath(),
1841                        self.tr("Created new branch <{0}>.").format(
1842                            name))
1843
1844    def hgShowBranch(self):
1845        """
1846        Public method used to show the current branch of the working directory.
1847        """
1848        args = self.initCommand("branch")
1849
1850        dia = HgDialog(self.tr('Showing current branch'), self)
1851        res = dia.startProcess(args, showArgs=False)
1852        if res:
1853            dia.exec()
1854
1855    def hgGetCurrentBranch(self):
1856        """
1857        Public method to get the current branch of the working directory.
1858
1859        @return name of the current branch
1860        @rtype str
1861        """
1862        args = self.initCommand("branch")
1863
1864        output, error = self.__client.runcommand(args)
1865
1866        return output.strip()
1867
1868    def hgEditUserConfig(self):
1869        """
1870        Public method used to edit the user configuration file.
1871        """
1872        from .HgUserConfigDialog import HgUserConfigDialog
1873        dlg = HgUserConfigDialog(version=self.version)
1874        dlg.exec()
1875
1876    def hgEditConfig(self, repoName=None,
1877                     withLargefiles=True, largefilesData=None):
1878        """
1879        Public method used to edit the repository configuration file.
1880
1881        @param repoName directory name containing the repository
1882        @type str
1883        @param withLargefiles flag indicating to configure the largefiles
1884            section
1885        @type bool
1886        @param largefilesData dictionary with data for the largefiles
1887            section of the data dialog
1888        @type dict
1889        """
1890        if repoName is None:
1891            repoName = self.getClient().getRepository()
1892
1893        cfgFile = os.path.join(repoName, self.adminDir, "hgrc")
1894        if not os.path.exists(cfgFile):
1895            # open dialog to enter the initial data
1896            withLargefiles = (self.isExtensionActive("largefiles") and
1897                              withLargefiles)
1898            from .HgRepoConfigDataDialog import HgRepoConfigDataDialog
1899            dlg = HgRepoConfigDataDialog(withLargefiles=withLargefiles,
1900                                         largefilesData=largefilesData)
1901            if dlg.exec() == QDialog.DialogCode.Accepted:
1902                createContents = True
1903                defaultUrl, defaultPushUrl = dlg.getData()
1904                if withLargefiles:
1905                    lfMinSize, lfPattern = dlg.getLargefilesData()
1906            else:
1907                createContents = False
1908            with contextlib.suppress(OSError):
1909                with open(cfgFile, "w") as cfg:
1910                    if createContents:
1911                        # write the data entered
1912                        cfg.write("[paths]\n")
1913                        if defaultUrl:
1914                            cfg.write("default = {0}\n".format(defaultUrl))
1915                        if defaultPushUrl:
1916                            cfg.write("default-push = {0}\n".format(
1917                                defaultPushUrl))
1918                        if (
1919                            withLargefiles and
1920                            (lfMinSize, lfPattern) != (None, None)
1921                        ):
1922                            cfg.write("\n[largefiles]\n")
1923                            if lfMinSize is not None:
1924                                cfg.write("minsize = {0}\n".format(lfMinSize))
1925                            if lfPattern is not None:
1926                                cfg.write("patterns =\n")
1927                                cfg.write("  {0}\n".format(
1928                                    "\n  ".join(lfPattern)))
1929                self.__monitorRepoIniFile(repoName)
1930                self.__iniFileChanged(cfgFile)
1931        self.repoEditor = MiniEditor(cfgFile, "Properties")
1932        self.repoEditor.show()
1933
1934    def hgVerify(self):
1935        """
1936        Public method to verify the integrity of the repository.
1937        """
1938        args = self.initCommand("verify")
1939
1940        dia = HgDialog(
1941            self.tr('Verifying the integrity of the Mercurial repository'),
1942            self)
1943        res = dia.startProcess(args)
1944        if res:
1945            dia.exec()
1946
1947    def hgShowConfig(self):
1948        """
1949        Public method to show the combined configuration.
1950        """
1951        args = self.initCommand("showconfig")
1952        args.append("--untrusted")
1953
1954        dia = HgDialog(
1955            self.tr('Showing the combined configuration settings'),
1956            self)
1957        res = dia.startProcess(args, showArgs=False)
1958        if res:
1959            dia.exec()
1960
1961    def hgShowPaths(self):
1962        """
1963        Public method to show the path aliases for remote repositories.
1964        """
1965        args = self.initCommand("paths")
1966
1967        dia = HgDialog(
1968            self.tr('Showing aliases for remote repositories'),
1969            self)
1970        res = dia.startProcess(args, showArgs=False)
1971        if res:
1972            dia.exec()
1973
1974    def hgRecover(self):
1975        """
1976        Public method to recover an interrupted transaction.
1977        """
1978        args = self.initCommand("recover")
1979
1980        dia = HgDialog(
1981            self.tr('Recovering from interrupted transaction'),
1982            self)
1983        res = dia.startProcess(args, showArgs=False)
1984        if res:
1985            dia.exec()
1986
1987    def hgIdentify(self):
1988        """
1989        Public method to identify the current working directory.
1990        """
1991        args = self.initCommand("identify")
1992
1993        dia = HgDialog(self.tr('Identifying project directory'), self)
1994        res = dia.startProcess(args, showArgs=False)
1995        if res:
1996            dia.exec()
1997
1998    def hgCreateIgnoreFile(self, name, autoAdd=False):
1999        """
2000        Public method to create the ignore file.
2001
2002        @param name directory name to create the ignore file in (string)
2003        @param autoAdd flag indicating to add it automatically (boolean)
2004        @return flag indicating success
2005        """
2006        status = False
2007        ignorePatterns = [
2008            "glob:.eric6project",
2009            "glob:.ropeproject",
2010            "glob:.directory",
2011            "glob:**.pyc",
2012            "glob:**.pyo",
2013            "glob:**.orig",
2014            "glob:**.bak",
2015            "glob:**.rej",
2016            "glob:**~",
2017            "glob:cur",
2018            "glob:tmp",
2019            "glob:__pycache__",
2020            "glob:**.DS_Store",
2021        ]
2022
2023        ignoreName = os.path.join(name, Hg.IgnoreFileName)
2024        res = (
2025            E5MessageBox.yesNo(
2026                self.__ui,
2027                self.tr("Create .hgignore file"),
2028                self.tr("""<p>The file <b>{0}</b> exists already."""
2029                        """ Overwrite it?</p>""").format(ignoreName),
2030                icon=E5MessageBox.Warning)
2031            if os.path.exists(ignoreName) else
2032            True
2033        )
2034        if res:
2035            try:
2036                # create a .hgignore file
2037                with open(ignoreName, "w") as ignore:
2038                    ignore.write("\n".join(ignorePatterns))
2039                    ignore.write("\n")
2040                status = True
2041            except OSError:
2042                status = False
2043
2044            if status and autoAdd:
2045                self.vcsAdd(ignoreName, noDialog=True)
2046                project = e5App().getObject("Project")
2047                project.appendFile(ignoreName)
2048
2049        return status
2050
2051    def hgBundle(self, bundleData=None):
2052        """
2053        Public method to create a changegroup file.
2054
2055        @param bundleData dictionary containing the bundle creation information
2056        @type dict
2057        """
2058        if bundleData is None:
2059            from .HgBundleDialog import HgBundleDialog
2060            dlg = HgBundleDialog(self.hgGetTagsList(),
2061                                 self.hgGetBranchesList(),
2062                                 self.hgGetBookmarksList(),
2063                                 version=self.version)
2064            if dlg.exec() != QDialog.DialogCode.Accepted:
2065                return
2066
2067            revs, baseRevs, compression, bundleAll = dlg.getParameters()
2068        else:
2069            revs = bundleData["revs"]
2070            if bundleData["base"]:
2071                baseRevs = [bundleData["base"]]
2072            else:
2073                baseRevs = []
2074            compression = ""
2075            bundleAll = bundleData["all"]
2076
2077        fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
2078            None,
2079            self.tr("Create changegroup"),
2080            self.__lastChangeGroupPath,
2081            self.tr("Mercurial Changegroup Files (*.hg)"),
2082            None,
2083            E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
2084
2085        if not fname:
2086            return  # user aborted
2087
2088        ext = QFileInfo(fname).suffix()
2089        if not ext:
2090            ex = selectedFilter.split("(*")[1].split(")")[0]
2091            if ex:
2092                fname += ex
2093        if QFileInfo(fname).exists():
2094            res = E5MessageBox.yesNo(
2095                self.__ui,
2096                self.tr("Create changegroup"),
2097                self.tr("<p>The Mercurial changegroup file <b>{0}</b> "
2098                        "already exists. Overwrite it?</p>")
2099                    .format(fname),
2100                icon=E5MessageBox.Warning)
2101            if not res:
2102                return
2103        fname = Utilities.toNativeSeparators(fname)
2104        self.__lastChangeGroupPath = os.path.dirname(fname)
2105
2106        args = self.initCommand("bundle")
2107        if bundleAll:
2108            args.append("--all")
2109        for rev in revs:
2110            args.append("--rev")
2111            args.append(rev)
2112        for baseRev in baseRevs:
2113            args.append("--base")
2114            args.append(baseRev)
2115        if compression:
2116            args.append("--type")
2117            args.append(compression)
2118        args.append(fname)
2119
2120        dia = HgDialog(self.tr('Create changegroup'), self)
2121        res = dia.startProcess(args)
2122        if res:
2123            dia.exec()
2124
2125    def hgPreviewBundle(self):
2126        """
2127        Public method used to view the log of incoming changes from a
2128        changegroup file.
2129        """
2130        file = E5FileDialog.getOpenFileName(
2131            None,
2132            self.tr("Preview changegroup"),
2133            self.__lastChangeGroupPath,
2134            self.tr("Mercurial Changegroup Files (*.hg);;All Files (*)"))
2135        if file:
2136            self.__lastChangeGroupPath = os.path.dirname(file)
2137
2138            if self.logBrowserIncoming is None:
2139                from .HgLogBrowserDialog import HgLogBrowserDialog
2140                self.logBrowserIncoming = HgLogBrowserDialog(
2141                    self, mode="incoming")
2142            self.logBrowserIncoming.show()
2143            self.logBrowserIncoming.raise_()
2144            self.logBrowserIncoming.start(bundle=file)
2145
2146    def hgUnbundle(self, files=None):
2147        """
2148        Public method to apply changegroup files.
2149
2150        @param files list of bundle files to be applied
2151        @type list of str
2152        @return flag indicating, that the update contained an add
2153            or delete
2154        @rtype bool
2155        """
2156        res = False
2157        if not files:
2158            files = E5FileDialog.getOpenFileNames(
2159                None,
2160                self.tr("Apply changegroups"),
2161                self.__lastChangeGroupPath,
2162                self.tr("Mercurial Changegroup Files (*.hg);;All Files (*)"))
2163
2164        if files:
2165            self.__lastChangeGroupPath = os.path.dirname(files[0])
2166
2167            update = E5MessageBox.yesNo(
2168                self.__ui,
2169                self.tr("Apply changegroups"),
2170                self.tr("""Shall the working directory be updated?"""),
2171                yesDefault=True)
2172
2173            args = self.initCommand("unbundle")
2174            if update:
2175                args.append("--update")
2176                args.append("--verbose")
2177            args.extend(files)
2178
2179            dia = HgDialog(self.tr('Apply changegroups'), self)
2180            res = dia.startProcess(args)
2181            if res:
2182                dia.exec()
2183                res = dia.hasAddOrDelete()
2184            self.checkVCSStatus()
2185
2186        return res
2187
2188    def hgBisect(self, subcommand):
2189        """
2190        Public method to perform bisect commands.
2191
2192        @param subcommand name of the subcommand (one of 'good', 'bad',
2193            'skip' or 'reset')
2194        @type str
2195        @exception ValueError raised to indicate an invalid bisect subcommand
2196        """
2197        if subcommand not in ("good", "bad", "skip", "reset"):
2198            raise ValueError(
2199                self.tr("Bisect subcommand ({0}) invalid.")
2200                    .format(subcommand))
2201
2202        rev = ""
2203        if subcommand in ("good", "bad", "skip"):
2204            from .HgRevisionSelectionDialog import HgRevisionSelectionDialog
2205            dlg = HgRevisionSelectionDialog(self.hgGetTagsList(),
2206                                            self.hgGetBranchesList(),
2207                                            self.hgGetBookmarksList())
2208            if dlg.exec() == QDialog.DialogCode.Accepted:
2209                rev = dlg.getRevision()
2210            else:
2211                return
2212
2213        args = self.initCommand("bisect")
2214        args.append("--{0}".format(subcommand))
2215        if rev:
2216            args.append(rev)
2217
2218        dia = HgDialog(
2219            self.tr('Mercurial Bisect ({0})').format(subcommand), self)
2220        res = dia.startProcess(args)
2221        if res:
2222            dia.exec()
2223
2224    def hgForget(self, name):
2225        """
2226        Public method used to remove a file from the Mercurial repository.
2227
2228        This will not remove the file from the project directory.
2229
2230        @param name file/directory name to be removed (string or list of
2231            strings))
2232        """
2233        args = self.initCommand("forget")
2234        args.append('-v')
2235
2236        if isinstance(name, list):
2237            self.addArguments(args, name)
2238        else:
2239            args.append(name)
2240
2241        dia = HgDialog(
2242            self.tr('Removing files from the Mercurial repository only'),
2243            self)
2244        res = dia.startProcess(args)
2245        if res:
2246            dia.exec()
2247            if isinstance(name, list):
2248                self.__forgotNames.extend(name)
2249            else:
2250                self.__forgotNames.append(name)
2251
2252    def hgBackout(self):
2253        """
2254        Public method used to backout an earlier changeset from the Mercurial
2255        repository.
2256        """
2257        from .HgBackoutDialog import HgBackoutDialog
2258        dlg = HgBackoutDialog(self.hgGetTagsList(),
2259                              self.hgGetBranchesList(),
2260                              self.hgGetBookmarksList())
2261        if dlg.exec() == QDialog.DialogCode.Accepted:
2262            rev, merge, date, user, message = dlg.getParameters()
2263            if not rev:
2264                E5MessageBox.warning(
2265                    self.__ui,
2266                    self.tr("Backing out changeset"),
2267                    self.tr("""No revision given. Aborting..."""))
2268                return
2269
2270            args = self.initCommand("backout")
2271            args.append('-v')
2272            if merge:
2273                args.append('--merge')
2274            if date:
2275                args.append('--date')
2276                args.append(date)
2277            if user:
2278                args.append('--user')
2279                args.append(user)
2280            args.append('--message')
2281            args.append(message)
2282            args.append(rev)
2283
2284            dia = HgDialog(self.tr('Backing out changeset'), self)
2285            res = dia.startProcess(args)
2286            if res:
2287                dia.exec()
2288
2289    def hgRollback(self):
2290        """
2291        Public method used to rollback the last transaction.
2292        """
2293        res = E5MessageBox.yesNo(
2294            None,
2295            self.tr("Rollback last transaction"),
2296            self.tr("""Are you sure you want to rollback the last"""
2297                    """ transaction?"""),
2298            icon=E5MessageBox.Warning)
2299        if res:
2300            dia = HgDialog(self.tr('Rollback last transaction'), self)
2301            res = dia.startProcess(["rollback"])
2302            if res:
2303                dia.exec()
2304
2305    def hgServe(self, repoPath):
2306        """
2307        Public method used to serve the project.
2308
2309        @param repoPath directory containing the repository
2310        @type str
2311        """
2312        from .HgServeDialog import HgServeDialog
2313        self.serveDlg = HgServeDialog(self, repoPath)
2314        self.serveDlg.show()
2315
2316    def hgImport(self):
2317        """
2318        Public method to import a patch file.
2319
2320        @return flag indicating, that the import contained an add, a delete
2321            or a change to the project file (boolean)
2322        """
2323        from .HgImportDialog import HgImportDialog
2324        dlg = HgImportDialog(self)
2325        if dlg.exec() == QDialog.DialogCode.Accepted:
2326            (patchFile, noCommit, message, date, user, withSecret, stripCount,
2327             force) = dlg.getParameters()
2328
2329            args = self.initCommand("import")
2330            args.append("--verbose")
2331            if noCommit:
2332                args.append("--no-commit")
2333            else:
2334                if message:
2335                    args.append('--message')
2336                    args.append(message)
2337                if date:
2338                    args.append('--date')
2339                    args.append(date)
2340                if user:
2341                    args.append('--user')
2342                    args.append(user)
2343            if stripCount != 1:
2344                args.append("--strip")
2345                args.append(str(stripCount))
2346            if force:
2347                args.append("--force")
2348            if withSecret:
2349                args.append("--secret")
2350            args.append(patchFile)
2351
2352            dia = HgDialog(self.tr("Import Patch"), self)
2353            res = dia.startProcess(args)
2354            if res:
2355                dia.exec()
2356                res = dia.hasAddOrDelete()
2357            self.checkVCSStatus()
2358        else:
2359            res = False
2360
2361        return res
2362
2363    def hgExport(self):
2364        """
2365        Public method to export patches to files.
2366        """
2367        from .HgExportDialog import HgExportDialog
2368        dlg = HgExportDialog(self.hgGetBookmarksList(),
2369                             self.version >= (4, 7, 0))
2370        if dlg.exec() == QDialog.DialogCode.Accepted:
2371            (filePattern, revisions, bookmark, switchParent, allText,
2372             noDates, git) = dlg.getParameters()
2373
2374            args = self.initCommand("export")
2375            args.append("--output")
2376            args.append(filePattern)
2377            args.append("--verbose")
2378            if switchParent:
2379                args.append("--switch-parent")
2380            if allText:
2381                args.append("--text")
2382            if noDates:
2383                args.append("--nodates")
2384            if git:
2385                args.append("--git")
2386            if bookmark:
2387                args.append("--bookmark")
2388                args.append(bookmark)
2389            else:
2390                for rev in revisions:
2391                    args.append(rev)
2392
2393            dia = HgDialog(self.tr("Export Patches"), self)
2394            res = dia.startProcess(args)
2395            if res:
2396                dia.exec()
2397
2398    def hgPhase(self, data=None):
2399        """
2400        Public method to change the phase of revisions.
2401
2402        @param data tuple giving phase data (list of revisions, phase, flag
2403            indicating a forced operation) (list of strings, string, boolean)
2404        @return flag indicating success (boolean)
2405        @exception ValueError raised to indicate an invalid phase
2406        """
2407        if data is None:
2408            from .HgPhaseDialog import HgPhaseDialog
2409            dlg = HgPhaseDialog()
2410            if dlg.exec() == QDialog.DialogCode.Accepted:
2411                data = dlg.getData()
2412
2413        if data:
2414            revs, phase, force = data
2415
2416            if phase not in ("p", "d", "s"):
2417                raise ValueError("Invalid phase given.")
2418
2419            args = self.initCommand("phase")
2420            if phase == "p":
2421                args.append("--public")
2422            elif phase == "d":
2423                args.append("--draft")
2424            else:
2425                args.append("--secret")
2426
2427            if force:
2428                args.append("--force")
2429            for rev in revs:
2430                args.append(rev)
2431
2432            dia = HgDialog(self.tr("Change Phase"), self)
2433            res = dia.startProcess(args)
2434            if res:
2435                dia.exec()
2436                res = dia.normalExitWithoutErrors()
2437        else:
2438            res = False
2439
2440        return res
2441
2442    def hgGraft(self, revs=None):
2443        """
2444        Public method to copy changesets from another branch.
2445
2446        @param revs list of revisions to show in the revisions pane (list of
2447            strings)
2448        @return flag indicating that the project should be reread (boolean)
2449        """
2450        from .HgGraftDialog import HgGraftDialog
2451        res = False
2452        dlg = HgGraftDialog(self, revs)
2453        if dlg.exec() == QDialog.DialogCode.Accepted:
2454            (revs,
2455             (userData, currentUser, userName),
2456             (dateData, currentDate, dateStr),
2457             log, dryrun, noCommit) = dlg.getData()
2458
2459            args = self.initCommand("graft")
2460            args.append("--verbose")
2461            if userData:
2462                if currentUser:
2463                    args.append("--currentuser")
2464                else:
2465                    args.append("--user")
2466                    args.append(userName)
2467            if dateData:
2468                if currentDate:
2469                    args.append("--currentdate")
2470                else:
2471                    args.append("--date")
2472                    args.append(dateStr)
2473            if log:
2474                args.append("--log")
2475            if dryrun:
2476                args.append("--dry-run")
2477            if noCommit:
2478                args.append("--no-commit")
2479            args.extend(revs)
2480
2481            dia = HgDialog(self.tr('Copy Changesets'), self)
2482            res = dia.startProcess(args)
2483            if res:
2484                dia.exec()
2485                res = dia.hasAddOrDelete()
2486                self.checkVCSStatus()
2487        return res
2488
2489    def __hgGraftSubCommand(self, subcommand, title):
2490        """
2491        Private method to perform a Mercurial graft subcommand.
2492
2493        @param subcommand subcommand flag
2494        @type str
2495        @param title tirle of the dialog
2496        @type str
2497        @return flag indicating that the project should be reread
2498        @rtype bool
2499        """
2500        args = self.initCommand("graft")
2501        args.append(subcommand)
2502        args.append("--verbose")
2503
2504        dia = HgDialog(title, self)
2505        res = dia.startProcess(args)
2506        if res:
2507            dia.exec()
2508            res = dia.hasAddOrDelete()
2509            self.checkVCSStatus()
2510        return res
2511
2512    def hgGraftContinue(self, path):
2513        """
2514        Public method to continue copying changesets from another branch.
2515
2516        @param path directory name of the project
2517        @type str
2518        @return flag indicating that the project should be reread
2519        @rtype bool
2520        """
2521        return self.__hgGraftSubCommand(
2522            "--continue", self.tr('Copy Changesets (Continue)'))
2523
2524    def hgGraftStop(self, path):
2525        """
2526        Public method to stop an interrupted copying session.
2527
2528        @param path directory name of the project
2529        @type str
2530        @return flag indicating that the project should be reread
2531        @rtype bool
2532        """
2533        return self.__hgGraftSubCommand(
2534            "--stop", self.tr('Copy Changesets (Stop)'))
2535
2536    def hgGraftAbort(self, path):
2537        """
2538        Public method to abort an interrupted copying session and perform
2539        a rollback.
2540
2541        @param path directory name of the project
2542        @type str
2543        @return flag indicating that the project should be reread
2544        @rtype bool
2545        """
2546        return self.__hgGraftSubCommand(
2547            "--abort", self.tr('Copy Changesets (Abort)'))
2548
2549    def hgArchive(self):
2550        """
2551        Public method to create an unversioned archive from the repository.
2552        """
2553        from .HgArchiveDialog import HgArchiveDialog
2554        dlg = HgArchiveDialog(self)
2555        if dlg.exec() == QDialog.DialogCode.Accepted:
2556            archive, type_, prefix, subrepos = dlg.getData()
2557
2558            args = self.initCommand("archive")
2559            if type_:
2560                args.append("--type")
2561                args.append(type_)
2562            if prefix:
2563                args.append("--prefix")
2564                args.append(prefix)
2565            if subrepos:
2566                args.append("--subrepos")
2567            args.append(archive)
2568
2569            dia = HgDialog(self.tr("Create Unversioned Archive"), self)
2570            res = dia.startProcess(args)
2571            if res:
2572                dia.exec()
2573
2574    def hgDeleteBackups(self):
2575        """
2576        Public method to delete all backup bundles in the backup area.
2577        """
2578        backupdir = os.path.join(self.getClient().getRepository(),
2579                                 self.adminDir, "strip-backup")
2580        yes = E5MessageBox.yesNo(
2581            self.__ui,
2582            self.tr("Delete All Backups"),
2583            self.tr("""<p>Do you really want to delete all backup bundles"""
2584                    """ stored the backup area <b>{0}</b>?</p>""").format(
2585                backupdir))
2586        if yes:
2587            shutil.rmtree(backupdir, True)
2588
2589    ###########################################################################
2590    ## Methods to deal with sub-repositories are below.
2591    ###########################################################################
2592
2593    def getHgSubPath(self):
2594        """
2595        Public method to get the path to the .hgsub file containing the
2596        definitions of sub-repositories.
2597
2598        @return full path of the .hgsub file (string)
2599        """
2600        ppath = self.__projectHelper.getProject().getProjectPath()
2601        return os.path.join(ppath, ".hgsub")
2602
2603    def hasSubrepositories(self):
2604        """
2605        Public method to check, if the project might have sub-repositories.
2606
2607        @return flag indicating the existence of sub-repositories (boolean)
2608        """
2609        hgsub = self.getHgSubPath()
2610        return os.path.isfile(hgsub) and os.stat(hgsub).st_size > 0
2611
2612    def hgAddSubrepository(self):
2613        """
2614        Public method to add a sub-repository.
2615        """
2616        from .HgAddSubrepositoryDialog import HgAddSubrepositoryDialog
2617        ppath = self.__projectHelper.getProject().getProjectPath()
2618        hgsub = self.getHgSubPath()
2619        dlg = HgAddSubrepositoryDialog(ppath)
2620        if dlg.exec() == QDialog.DialogCode.Accepted:
2621            relPath, subrepoType, subrepoUrl = dlg.getData()
2622            if subrepoType == "hg":
2623                url = subrepoUrl
2624            else:
2625                url = "[{0}]{1}".format(subrepoType, subrepoUrl)
2626            entry = "{0} = {1}\n".format(relPath, url)
2627
2628            contents = []
2629            if os.path.isfile(hgsub):
2630                # file exists; check, if such an entry exists already
2631                needsAdd = False
2632                try:
2633                    with open(hgsub, "r") as f:
2634                        contents = f.readlines()
2635                except OSError as err:
2636                    E5MessageBox.critical(
2637                        self.__ui,
2638                        self.tr("Add Sub-repository"),
2639                        self.tr(
2640                            """<p>The sub-repositories file .hgsub could not"""
2641                            """ be read.</p><p>Reason: {0}</p>""")
2642                        .format(str(err)))
2643                    return
2644
2645                if entry in contents:
2646                    E5MessageBox.critical(
2647                        self.__ui,
2648                        self.tr("Add Sub-repository"),
2649                        self.tr(
2650                            """<p>The sub-repositories file .hgsub already"""
2651                            """ contains an entry <b>{0}</b>."""
2652                            """ Aborting...</p>""").format(entry))
2653                    return
2654            else:
2655                needsAdd = True
2656
2657            if contents and not contents[-1].endswith("\n"):
2658                contents[-1] = contents[-1] + "\n"
2659            contents.append(entry)
2660            try:
2661                with open(hgsub, "w") as f:
2662                    f.writelines(contents)
2663            except OSError as err:
2664                E5MessageBox.critical(
2665                    self.__ui,
2666                    self.tr("Add Sub-repository"),
2667                    self.tr(
2668                        """<p>The sub-repositories file .hgsub could not"""
2669                        """ be written to.</p><p>Reason: {0}</p>""")
2670                    .format(str(err)))
2671                return
2672
2673            if needsAdd:
2674                self.vcsAdd(hgsub)
2675                self.__projectHelper.getProject().appendFile(hgsub)
2676
2677    def hgRemoveSubrepositories(self):
2678        """
2679        Public method to remove sub-repositories.
2680        """
2681        hgsub = self.getHgSubPath()
2682
2683        subrepositories = []
2684        if not os.path.isfile(hgsub):
2685            E5MessageBox.critical(
2686                self.__ui,
2687                self.tr("Remove Sub-repositories"),
2688                self.tr("""<p>The sub-repositories file .hgsub does not"""
2689                        """ exist. Aborting...</p>"""))
2690            return
2691
2692        try:
2693            with open(hgsub, "r") as f:
2694                subrepositories = [line.strip() for line in f.readlines()]
2695        except OSError as err:
2696            E5MessageBox.critical(
2697                self.__ui,
2698                self.tr("Remove Sub-repositories"),
2699                self.tr("""<p>The sub-repositories file .hgsub could not"""
2700                        """ be read.</p><p>Reason: {0}</p>""")
2701                .format(str(err)))
2702            return
2703
2704        from .HgRemoveSubrepositoriesDialog import (
2705            HgRemoveSubrepositoriesDialog
2706        )
2707        dlg = HgRemoveSubrepositoriesDialog(subrepositories)
2708        if dlg.exec() == QDialog.DialogCode.Accepted:
2709            subrepositories, removedSubrepos, deleteSubrepos = dlg.getData()
2710            contents = "\n".join(subrepositories) + "\n"
2711            try:
2712                with open(hgsub, "w") as f:
2713                    f.write(contents)
2714            except OSError as err:
2715                E5MessageBox.critical(
2716                    self.__ui,
2717                    self.tr("Remove Sub-repositories"),
2718                    self.tr(
2719                        """<p>The sub-repositories file .hgsub could not"""
2720                        """ be written to.</p><p>Reason: {0}</p>""")
2721                    .format(str(err)))
2722                return
2723
2724            if deleteSubrepos:
2725                ppath = self.__projectHelper.getProject().getProjectPath()
2726                for removedSubrepo in removedSubrepos:
2727                    subrepoPath = removedSubrepo.split("=", 1)[0].strip()
2728                    subrepoAbsPath = os.path.join(ppath, subrepoPath)
2729                    shutil.rmtree(subrepoAbsPath, True)
2730
2731    ###########################################################################
2732    ## Methods to handle configuration dependent stuff are below.
2733    ###########################################################################
2734
2735    def __checkDefaults(self):
2736        """
2737        Private method to check, if the default and default-push URLs
2738        have been configured.
2739        """
2740        args = self.initCommand("showconfig")
2741        args.append('paths')
2742
2743        output, error = self.__client.runcommand(args)
2744
2745        self.__defaultConfigured = False
2746        self.__defaultPushConfigured = False
2747        if output:
2748            for line in output.splitlines():
2749                line = line.strip()
2750                if (
2751                    line.startswith("paths.default=") and
2752                    not line.endswith("=")
2753                ):
2754                    self.__defaultConfigured = True
2755                if (
2756                    line.startswith("paths.default-push=") and
2757                    not line.endswith("=")
2758                ):
2759                    self.__defaultPushConfigured = True
2760
2761    def canCommitMerge(self):
2762        """
2763        Public method to check, if the working directory is an uncommitted
2764        merge.
2765
2766        @return flag indicating commit merge capability
2767        @rtype bool
2768        """
2769        args = self.initCommand("identify")
2770
2771        output, error = self.__client.runcommand(args)
2772
2773        return output.count('+') == 2
2774
2775    def canPull(self):
2776        """
2777        Public method to check, if pull is possible.
2778
2779        @return flag indicating pull capability (boolean)
2780        """
2781        return self.__defaultConfigured
2782
2783    def canPush(self):
2784        """
2785        Public method to check, if push is possible.
2786
2787        @return flag indicating push capability (boolean)
2788        """
2789        return self.__defaultPushConfigured or self.__defaultConfigured
2790
2791    def __iniFileChanged(self, path):
2792        """
2793        Private slot to handle a change of the Mercurial configuration file.
2794
2795        @param path name of the changed file (string)
2796        """
2797        if self.__client:
2798            ok, err = self.__client.restartServer()
2799            if not ok:
2800                E5MessageBox.warning(
2801                    None,
2802                    self.tr("Mercurial Command Server"),
2803                    self.tr(
2804                        """<p>The Mercurial Command Server could not be"""
2805                        """ restarted.</p><p>Reason: {0}</p>""").format(err))
2806
2807        self.__getExtensionsInfo()
2808
2809        if self.__repoIniFile and path == self.__repoIniFile:
2810            self.__checkDefaults()
2811
2812        self.iniFileChanged.emit()
2813
2814    def __monitorRepoIniFile(self, repodir):
2815        """
2816        Private slot to add a repository configuration file to the list of
2817        monitored files.
2818
2819        @param repodir directory name of the repository
2820        @type str
2821        """
2822        cfgFile = os.path.join(repodir, self.adminDir, "hgrc")
2823        if os.path.exists(cfgFile):
2824            self.__iniWatcher.addPath(cfgFile)
2825            self.__repoIniFile = cfgFile
2826            self.__checkDefaults()
2827
2828    ###########################################################################
2829    ## Methods to handle extensions are below.
2830    ###########################################################################
2831
2832    def __getExtensionsInfo(self):
2833        """
2834        Private method to get the active extensions from Mercurial.
2835        """
2836        activeExtensions = sorted(self.__activeExtensions)
2837        self.__activeExtensions = []
2838
2839        args = self.initCommand("showconfig")
2840        args.append('extensions')
2841
2842        output, error = self.__client.runcommand(args)
2843
2844        if output:
2845            for line in output.splitlines():
2846                extensionName = (
2847                    line.split("=", 1)[0].strip().split(".")[-1].strip()
2848                )
2849                self.__activeExtensions.append(extensionName)
2850        if self.version < (4, 8, 0) and "closehead" in self.__activeExtensions:
2851            self.__activeExtensions.remove["closehead"]
2852
2853        if activeExtensions != sorted(self.__activeExtensions):
2854            self.activeExtensionsChanged.emit()
2855
2856    def isExtensionActive(self, extensionName):
2857        """
2858        Public method to check, if an extension is active.
2859
2860        @param extensionName name of the extension to check for (string)
2861        @return flag indicating an active extension (boolean)
2862        """
2863        extensionName = extensionName.strip()
2864        isActive = extensionName in self.__activeExtensions
2865
2866        return isActive
2867
2868    def getExtensionObject(self, extensionName):
2869        """
2870        Public method to get a reference to an extension object.
2871
2872        @param extensionName name of the extension (string)
2873        @return reference to the extension object (boolean)
2874        """
2875        return self.__extensions[extensionName]
2876
2877    ###########################################################################
2878    ## Methods to get the helper objects are below.
2879    ###########################################################################
2880
2881    def vcsGetProjectBrowserHelper(self, browser, project,
2882                                   isTranslationsBrowser=False):
2883        """
2884        Public method to instantiate a helper object for the different
2885        project browsers.
2886
2887        @param browser reference to the project browser object
2888        @param project reference to the project object
2889        @param isTranslationsBrowser flag indicating, the helper is requested
2890            for the translations browser (this needs some special treatment)
2891        @return the project browser helper object
2892        """
2893        from .ProjectBrowserHelper import HgProjectBrowserHelper
2894        return HgProjectBrowserHelper(self, browser, project,
2895                                      isTranslationsBrowser)
2896
2897    def vcsGetProjectHelper(self, project):
2898        """
2899        Public method to instantiate a helper object for the project.
2900
2901        @param project reference to the project object
2902        @return the project helper object
2903        """
2904        # find the root of the repo
2905        repodir = project.getProjectPath()
2906        while not os.path.isdir(os.path.join(repodir, self.adminDir)):
2907            repodir = os.path.dirname(repodir)
2908            if not repodir or os.path.splitdrive(repodir)[1] == os.sep:
2909                repodir = ""
2910                break
2911
2912        self.__projectHelper = self.__plugin.getProjectHelper()
2913        self.__projectHelper.setObjects(self, project)
2914
2915        if repodir:
2916            self.__repoDir = repodir
2917            self.__createClient(repodir)
2918            self.__monitorRepoIniFile(repodir)
2919
2920        return self.__projectHelper
2921
2922    ###########################################################################
2923    ## Methods to handle the Mercurial command server are below.
2924    ###########################################################################
2925
2926    def __createClient(self, repodir=""):
2927        """
2928        Private method to create a Mercurial command server client.
2929
2930        @param repodir path of the local repository
2931        @type str
2932        """
2933        self.stopClient()
2934
2935        self.__client = HgClient(repodir, "utf-8", self)
2936        ok, err = self.__client.startServer()
2937        if not ok:
2938            E5MessageBox.warning(
2939                None,
2940                self.tr("Mercurial Command Server"),
2941                self.tr(
2942                    """<p>The Mercurial Command Server could not be"""
2943                    """ started.</p><p>Reason: {0}</p>""").format(err))
2944
2945    def getClient(self):
2946        """
2947        Public method to get a reference to the command server interface.
2948
2949        @return reference to the client (HgClient)
2950        """
2951        if self.__client is None:
2952            self.__createClient(self.__repoDir)
2953
2954        return self.__client
2955
2956    def stopClient(self):
2957        """
2958        Public method to stop the command server client.
2959        """
2960        if self.__client is not None:
2961            self.__client.stopServer()
2962            self.__client = None
2963
2964    ###########################################################################
2965    ## Status Monitor Thread methods
2966    ###########################################################################
2967
2968    def _createStatusMonitorThread(self, interval, project):
2969        """
2970        Protected method to create an instance of the VCS status monitor
2971        thread.
2972
2973        @param interval check interval for the monitor thread in seconds
2974            (integer)
2975        @param project reference to the project object (Project)
2976        @return reference to the monitor thread (QThread)
2977        """
2978        from .HgStatusMonitorThread import HgStatusMonitorThread
2979        return HgStatusMonitorThread(interval, project, self)
2980
2981    ###########################################################################
2982    ## Bookmarks methods
2983    ###########################################################################
2984
2985    def hgListBookmarks(self):
2986        """
2987        Public method used to list the available bookmarks.
2988        """
2989        self.bookmarksList = []
2990
2991        if self.bookmarksListDlg is None:
2992            from .HgBookmarksListDialog import HgBookmarksListDialog
2993            self.bookmarksListDlg = HgBookmarksListDialog(self)
2994        self.bookmarksListDlg.show()
2995        self.bookmarksListDlg.raise_()
2996        self.bookmarksListDlg.start(self.bookmarksList)
2997
2998    def hgGetBookmarksList(self):
2999        """
3000        Public method to get the list of bookmarks.
3001
3002        @return list of bookmarks (list of string)
3003        """
3004        args = self.initCommand("bookmarks")
3005
3006        client = self.getClient()
3007        output = client.runcommand(args)[0]
3008
3009        self.bookmarksList = []
3010        for line in output.splitlines():
3011            li = line.strip().split()
3012            if li[-1][0] in "1234567890":
3013                # last element is a rev:changeset
3014                del li[-1]
3015                if li[0] == "*":
3016                    del li[0]
3017                name = " ".join(li)
3018                self.bookmarksList.append(name)
3019
3020        return self.bookmarksList[:]
3021
3022    def hgBookmarkDefine(self, revision=None, bookmark=None):
3023        """
3024        Public method to define a bookmark.
3025
3026        @param revision revision to set bookmark for (string)
3027        @param bookmark name of the bookmark (string)
3028        """
3029        if bool(revision) and bool(bookmark):
3030            ok = True
3031        else:
3032            from .HgBookmarkDialog import HgBookmarkDialog
3033            dlg = HgBookmarkDialog(HgBookmarkDialog.DEFINE_MODE,
3034                                   self.hgGetTagsList(),
3035                                   self.hgGetBranchesList(),
3036                                   self.hgGetBookmarksList())
3037            if dlg.exec() == QDialog.DialogCode.Accepted:
3038                revision, bookmark = dlg.getData()
3039                ok = True
3040            else:
3041                ok = False
3042
3043        if ok:
3044            args = self.initCommand("bookmarks")
3045            if revision:
3046                args.append("--rev")
3047                args.append(revision)
3048            args.append(bookmark)
3049
3050            dia = HgDialog(self.tr('Mercurial Bookmark'), self)
3051            res = dia.startProcess(args)
3052            if res:
3053                dia.exec()
3054
3055    def hgBookmarkDelete(self, bookmark=None):
3056        """
3057        Public method to delete a bookmark.
3058
3059        @param bookmark name of the bookmark (string)
3060        """
3061        if bookmark:
3062            ok = True
3063        else:
3064            bookmark, ok = QInputDialog.getItem(
3065                None,
3066                self.tr("Delete Bookmark"),
3067                self.tr("Select the bookmark to be deleted:"),
3068                [""] + sorted(self.hgGetBookmarksList()),
3069                0, True)
3070        if ok and bookmark:
3071            args = self.initCommand("bookmarks")
3072            args.append("--delete")
3073            args.append(bookmark)
3074
3075            dia = HgDialog(self.tr('Delete Mercurial Bookmark'), self)
3076            res = dia.startProcess(args)
3077            if res:
3078                dia.exec()
3079
3080    def hgBookmarkRename(self, renameInfo=None):
3081        """
3082        Public method to rename a bookmark.
3083
3084        @param renameInfo old and new names of the bookmark
3085        @type tuple of str and str
3086        """
3087        if not renameInfo:
3088            from .HgBookmarkRenameDialog import HgBookmarkRenameDialog
3089            dlg = HgBookmarkRenameDialog(self.hgGetBookmarksList())
3090            if dlg.exec() == QDialog.DialogCode.Accepted:
3091                renameInfo = dlg.getData()
3092
3093        if renameInfo:
3094            args = self.initCommand("bookmarks")
3095            args.append("--rename")
3096            args.append(renameInfo[0])
3097            args.append(renameInfo[1])
3098
3099            dia = HgDialog(self.tr('Rename Mercurial Bookmark'), self)
3100            res = dia.startProcess(args)
3101            if res:
3102                dia.exec()
3103
3104    def hgBookmarkMove(self, revision=None, bookmark=None):
3105        """
3106        Public method to move a bookmark.
3107
3108        @param revision revision to set bookmark for (string)
3109        @param bookmark name of the bookmark (string)
3110        """
3111        if bool(revision) and bool(bookmark):
3112            ok = True
3113        else:
3114            from .HgBookmarkDialog import HgBookmarkDialog
3115            dlg = HgBookmarkDialog(HgBookmarkDialog.MOVE_MODE,
3116                                   self.hgGetTagsList(),
3117                                   self.hgGetBranchesList(),
3118                                   self.hgGetBookmarksList())
3119            if dlg.exec() == QDialog.DialogCode.Accepted:
3120                revision, bookmark = dlg.getData()
3121                ok = True
3122            else:
3123                ok = False
3124
3125        if ok:
3126            args = self.initCommand("bookmarks")
3127            args.append("--force")
3128            if revision:
3129                args.append("--rev")
3130                args.append(revision)
3131            args.append(bookmark)
3132
3133            dia = HgDialog(self.tr('Move Mercurial Bookmark'), self)
3134            res = dia.startProcess(args)
3135            if res:
3136                dia.exec()
3137
3138    def hgBookmarkIncoming(self):
3139        """
3140        Public method to show a list of incoming bookmarks.
3141        """
3142        from .HgBookmarksInOutDialog import HgBookmarksInOutDialog
3143        self.bookmarksInOutDlg = HgBookmarksInOutDialog(
3144            self, HgBookmarksInOutDialog.INCOMING)
3145        self.bookmarksInOutDlg.show()
3146        self.bookmarksInOutDlg.start()
3147
3148    def hgBookmarkOutgoing(self):
3149        """
3150        Public method to show a list of outgoing bookmarks.
3151        """
3152        from .HgBookmarksInOutDialog import HgBookmarksInOutDialog
3153        self.bookmarksInOutDlg = HgBookmarksInOutDialog(
3154            self, HgBookmarksInOutDialog.OUTGOING)
3155        self.bookmarksInOutDlg.show()
3156        self.bookmarksInOutDlg.start()
3157
3158    def __getInOutBookmarks(self, incoming):
3159        """
3160        Private method to get the list of incoming or outgoing bookmarks.
3161
3162        @param incoming flag indicating to get incoming bookmarks (boolean)
3163        @return list of bookmarks (list of string)
3164        """
3165        bookmarksList = []
3166
3167        args = (
3168            self.initCommand("incoming")
3169            if incoming else
3170            self.initCommand("outgoing")
3171        )
3172        args.append('--bookmarks')
3173
3174        client = self.getClient()
3175        output = client.runcommand(args)[0]
3176
3177        for line in output.splitlines():
3178            if line.startswith(" "):
3179                li = line.strip().split()
3180                del li[-1]
3181                name = " ".join(li)
3182                bookmarksList.append(name)
3183
3184        return bookmarksList
3185
3186    def hgBookmarkPull(self, current=False, bookmark=None):
3187        """
3188        Public method to pull a bookmark from a remote repository.
3189
3190        @param current flag indicating to pull the current bookmark
3191        @type bool
3192        @param bookmark name of the bookmark
3193        @type str
3194        """
3195        if current:
3196            bookmark = "."
3197            ok = True
3198        elif bookmark:
3199            ok = True
3200        else:
3201            bookmarks = self.__getInOutBookmarks(True)
3202            bookmark, ok = QInputDialog.getItem(
3203                None,
3204                self.tr("Pull Bookmark"),
3205                self.tr("Select the bookmark to be pulled:"),
3206                [""] + sorted(bookmarks),
3207                0, True)
3208
3209        if ok and bookmark:
3210            args = self.initCommand("pull")
3211            args.append('--bookmark')
3212            args.append(bookmark)
3213
3214            dia = HgDialog(self.tr(
3215                'Pulling bookmark from a remote Mercurial repository'),
3216                self)
3217            res = dia.startProcess(args)
3218            if res:
3219                dia.exec()
3220
3221    def hgBookmarkPush(self, current=False, bookmark=None, allBookmarks=False):
3222        """
3223        Public method to push a bookmark to a remote repository.
3224
3225        @param current flag indicating to push the current bookmark
3226        @type bool
3227        @param bookmark name of the bookmark
3228        @type str
3229        @param allBookmarks flag indicating to push all bookmarks
3230        @type bool
3231        """
3232        if current:
3233            bookmark = "."
3234            ok = True
3235        elif bookmark or allBookmarks:
3236            ok = True
3237        else:
3238            bookmarks = self.__getInOutBookmarks(False)
3239            bookmark, ok = QInputDialog.getItem(
3240                None,
3241                self.tr("Push Bookmark"),
3242                self.tr("Select the bookmark to be push:"),
3243                [""] + sorted(bookmarks),
3244                0, True)
3245
3246        if ok and (bool(bookmark) or all):
3247            args = self.initCommand("push")
3248            if allBookmarks:
3249                args.append('--all-bookmarks')
3250            else:
3251                args.append('--bookmark')
3252                args.append(bookmark)
3253
3254            dia = HgDialog(self.tr(
3255                'Pushing bookmark to a remote Mercurial repository'),
3256                self)
3257            res = dia.startProcess(args)
3258            if res:
3259                dia.exec()
3260