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