src/eric7/Plugins/VcsPlugins/vcsPySvn/subversion.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2006 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the version control systems interface to Subversion.
8 """
9
10 import re
11 import os
12 import shutil
13 import time
14 from urllib.parse import quote
15 import contextlib
16
17 from PyQt6.QtCore import pyqtSignal, Qt, QDateTime, QCoreApplication
18 from PyQt6.QtWidgets import QLineEdit, QDialog, QInputDialog, QApplication
19
20 from EricWidgets.EricApplication import ericApp
21 from EricWidgets import EricMessageBox
22
23 from EricUtilities.EricMutexLocker import EricMutexLocker
24
25 from VCS.VersionControl import VersionControl
26
27 import pysvn
28
29 from .SvnDialog import SvnDialog
30 from .SvnUtilities import getConfigPath, amendConfig, createDefaultConfig
31
32 import Preferences
33 import Utilities
34
35
36 class Subversion(VersionControl):
37 """
38 Class implementing the version control systems interface to Subversion.
39
40 @signal committed() emitted after the commit action has completed
41 """
42 committed = pyqtSignal()
43
44 def __init__(self, plugin, parent=None, name=None):
45 """
46 Constructor
47
48 @param plugin reference to the plugin object
49 @param parent parent widget (QWidget)
50 @param name name of this object (string)
51 """
52 VersionControl.__init__(self, parent, name)
53 self.defaultOptions = {
54 'global': [''],
55 'commit': [''],
56 'checkout': [''],
57 'update': [''],
58 'add': [''],
59 'remove': [''],
60 'diff': [''],
61 'log': [''],
62 'history': [''],
63 'status': [''],
64 'tag': [''],
65 'export': ['']
66 }
67 self.interestingDataKeys = [
68 "standardLayout",
69 ]
70
71 self.__plugin = plugin
72 self.__ui = parent
73
74 self.options = self.defaultOptions
75 self.otherData["standardLayout"] = True
76 self.tagsList = []
77 self.branchesList = []
78 self.allTagsBranchesList = []
79 self.mergeList = [[], [], []]
80 self.showedTags = False
81 self.showedBranches = False
82
83 self.tagTypeList = [
84 'tags',
85 'branches'
86 ]
87
88 self.commandHistory = []
89 self.wdHistory = []
90
91 if (
92 pysvn.version >= (1, 4, 3, 0) and
93 "SVN_ASP_DOT_NET_HACK" in os.environ
94 ):
95 self.adminDir = '_svn'
96 else:
97 self.adminDir = '.svn'
98
99 self.log = None
100 self.diff = None
101 self.sbsDiff = None
102 self.status = None
103 self.propList = None
104 self.tagbranchList = None
105 self.blame = None
106 self.repoBrowser = None
107 self.logBrowser = None
108
109 self.statusCache = {}
110
111 self.__commitData = {}
112 self.__commitDialog = None
113
114 self.__wcng = True
115 # assume new generation working copy metadata format
116
117 def getPlugin(self):
118 """
119 Public method to get a reference to the plugin object.
120
121 @return reference to the plugin object (VcsPySvnPlugin)
122 """
123 return self.__plugin
124
125 def getClient(self):
126 """
127 Public method to create and initialize the pysvn client object.
128
129 @return the pysvn client object (pysvn.Client)
130 """
131 configDir = ""
132 authCache = True
133 for arg in self.options['global']:
134 if arg.startswith("--config-dir"):
135 configDir = arg.split("=", 1)[1]
136 if arg.startswith("--no-auth-cache"):
137 authCache = False
138
139 client = pysvn.Client(configDir)
140 client.exception_style = 1
141 client.set_auth_cache(authCache)
142
143 return client
144
145 ###########################################################################
146 ## Methods of the VCS interface
147 ###########################################################################
148
149 def vcsShutdown(self):
150 """
151 Public method used to shutdown the Subversion interface.
152 """
153 if self.log is not None:
154 self.log.close()
155 if self.diff is not None:
156 self.diff.close()
157 if self.sbsDiff is not None:
158 self.sbsDiff.close()
159 if self.status is not None:
160 self.status.close()
161 if self.propList is not None:
162 self.propList.close()
163 if self.tagbranchList is not None:
164 self.tagbranchList.close()
165 if self.blame is not None:
166 self.blame.close()
167 if self.repoBrowser is not None:
168 self.repoBrowser.close()
169 if self.logBrowser is not None:
170 self.logBrowser.close()
171
172 def vcsExists(self):
173 """
174 Public method used to test for the presence of the svn executable.
175
176 @return flag indicating the existance (boolean) and an error message
177 (string)
178 """
179 self.versionStr = ".".join([str(v) for v in pysvn.svn_version[:-1]])
180 self.version = pysvn.svn_version[:-1]
181 return True, ""
182
183 def vcsInit(self, vcsDir, noDialog=False):
184 """
185 Public method used to initialize the subversion repository.
186
187 The subversion repository has to be initialized from outside eric
188 because the respective command always works locally. Therefore we
189 always return TRUE without doing anything.
190
191 @param vcsDir name of the VCS directory (string)
192 @param noDialog flag indicating quiet operations (boolean)
193 @return always TRUE
194 """
195 return True
196
197 def vcsConvertProject(self, vcsDataDict, project, addAll=True):
198 """
199 Public method to convert an uncontrolled project to a version
200 controlled project.
201
202 @param vcsDataDict dictionary of data required for the conversion
203 @type dict
204 @param project reference to the project object
205 @type Project
206 @param addAll flag indicating to add all files to the repository
207 @type bool
208 """
209 success = self.vcsImport(vcsDataDict, project.ppath, addAll=addAll)[0]
210 if not success:
211 EricMessageBox.critical(
212 self.__ui,
213 self.tr("Create project in repository"),
214 self.tr(
215 """The project could not be created in the repository."""
216 """ Maybe the given repository doesn't exist or the"""
217 """ repository server is down."""))
218 else:
219 cwdIsPpath = False
220 if os.getcwd() == project.ppath:
221 os.chdir(os.path.dirname(project.ppath))
222 cwdIsPpath = True
223 tmpProjectDir = "{0}_tmp".format(project.ppath)
224 shutil.rmtree(tmpProjectDir, True)
225 os.rename(project.ppath, tmpProjectDir)
226 os.makedirs(project.ppath)
227 self.vcsCheckout(vcsDataDict, project.ppath)
228 if cwdIsPpath:
229 os.chdir(project.ppath)
230 self.vcsCommit(project.ppath, vcsDataDict["message"], True)
231 pfn = project.pfile
232 if not os.path.isfile(pfn):
233 pfn += "z"
234 if not os.path.isfile(pfn):
235 EricMessageBox.critical(
236 self.__ui,
237 self.tr("New project"),
238 self.tr(
239 """The project could not be checked out of the"""
240 """ repository.<br />"""
241 """Restoring the original contents."""))
242 if os.getcwd() == project.ppath:
243 os.chdir(os.path.dirname(project.ppath))
244 cwdIsPpath = True
245 else:
246 cwdIsPpath = False
247 shutil.rmtree(project.ppath, True)
248 os.rename(tmpProjectDir, project.ppath)
249 project.pdata["VCS"] = 'None'
250 project.vcs = None
251 project.setDirty(True)
252 project.saveProject()
253 project.closeProject()
254 return
255 shutil.rmtree(tmpProjectDir, True)
256 project.closeProject(noSave=True)
257 project.openProject(pfn)
258
259 def vcsImport(self, vcsDataDict, projectDir, noDialog=False, addAll=True):
260 """
261 Public method used to import the project into the Subversion
262 repository.
263
264 @param vcsDataDict dictionary of data required for the import
265 @type dict
266 @param projectDir project directory (string)
267 @type str
268 @param noDialog flag indicating quiet operations
269 @type bool
270 @param addAll flag indicating to add all files to the repository
271 @type bool
272 @return tuple containing a flag indicating an execution without errors
273 and a flag indicating the version controll status
274 @rtype tuple of (bool, bool)
275 """
276 noDialog = False
277 msg = vcsDataDict["message"]
278 if not msg:
279 msg = '***'
280
281 vcsDir = self.svnNormalizeURL(vcsDataDict["url"])
282 if vcsDir.startswith('/'):
283 vcsDir = 'file://{0}'.format(vcsDir)
284 elif vcsDir[1] in ['|', ':']:
285 vcsDir = 'file:///{0}'.format(vcsDir)
286
287 project = vcsDir[vcsDir.rfind('/') + 1:]
288
289 # create the dir structure to be imported into the repository
290 tmpDir = '{0}_tmp'.format(projectDir)
291 try:
292 os.makedirs(tmpDir)
293 if self.otherData["standardLayout"]:
294 os.mkdir(os.path.join(tmpDir, project))
295 os.mkdir(os.path.join(tmpDir, project, 'branches'))
296 os.mkdir(os.path.join(tmpDir, project, 'tags'))
297 shutil.copytree(
298 projectDir, os.path.join(tmpDir, project, 'trunk'))
299 else:
300 shutil.copytree(projectDir, os.path.join(tmpDir, project))
301 except OSError:
302 if os.path.isdir(tmpDir):
303 shutil.rmtree(tmpDir, True)
304 return False, False
305
306 cwd = os.getcwd()
307 os.chdir(os.path.join(tmpDir, project))
308 opts = self.options['global']
309 recurse = "--non-recursive" not in opts
310 url = self.__svnURL(vcsDir)
311 client = self.getClient()
312 if not noDialog:
313 dlg = SvnDialog(
314 self.tr('Importing project into Subversion repository'),
315 "import{0} --message {1} .".format(
316 (not recurse) and " --non-recursive" or "", msg),
317 client)
318 QApplication.processEvents()
319 try:
320 with EricMutexLocker(self.vcsExecutionMutex):
321 rev = client.import_(".", url, msg, recurse, ignore=True)
322 status = True
323 except pysvn.ClientError as e:
324 status = False
325 rev = None
326 if not noDialog:
327 dlg.showError(e.args[0])
328 if not noDialog:
329 rev and dlg.showMessage(self.tr("Imported revision {0}.\n")
330 .format(rev.number))
331 dlg.finish()
332 dlg.exec()
333 os.chdir(cwd)
334
335 shutil.rmtree(tmpDir, True)
336 return status, False
337
338 def vcsCheckout(self, vcsDataDict, projectDir, noDialog=False):
339 """
340 Public method used to check the project out of the Subversion
341 repository.
342
343 @param vcsDataDict dictionary of data required for the checkout
344 @param projectDir project directory to create (string)
345 @param noDialog flag indicating quiet operations
346 @return flag indicating an execution without errors (boolean)
347 """
348 noDialog = False
349 try:
350 tag = vcsDataDict["tag"]
351 except KeyError:
352 tag = None
353 vcsDir = self.svnNormalizeURL(vcsDataDict["url"])
354 if vcsDir.startswith('/'):
355 vcsDir = 'file://{0}'.format(vcsDir)
356 elif vcsDir[1] in ['|', ':']:
357 vcsDir = 'file:///{0}'.format(vcsDir)
358
359 if self.otherData["standardLayout"]:
360 if tag is None or tag == '':
361 svnUrl = '{0}/trunk'.format(vcsDir)
362 else:
363 if (
364 not tag.startswith('tags') and
365 not tag.startswith('branches')
366 ):
367 type_, ok = QInputDialog.getItem(
368 None,
369 self.tr("Subversion Checkout"),
370 self.tr(
371 "The tag must be a normal tag (tags) or"
372 " a branch tag (branches)."
373 " Please select from the list."),
374 self.tagTypeList,
375 0, False)
376 if not ok:
377 return False
378 tag = '{0}/{1}'.format(type_, tag)
379 svnUrl = '{0}/{1}'.format(vcsDir, tag)
380 else:
381 svnUrl = vcsDir
382
383 opts = self.options['global'] + self.options['checkout']
384 recurse = "--non-recursive" not in opts
385 url = self.__svnURL(svnUrl)
386 client = self.getClient()
387 if not noDialog:
388 dlg = SvnDialog(
389 self.tr('Checking project out of Subversion repository'),
390 "checkout{0} {1} {2}".format(
391 (not recurse) and " --non-recursive" or "",
392 url, projectDir),
393 client)
394 QApplication.processEvents()
395 try:
396 with EricMutexLocker(self.vcsExecutionMutex):
397 client.checkout(url, projectDir, recurse)
398 status = True
399 except pysvn.ClientError as e:
400 status = False
401 if not noDialog:
402 dlg.showError(e.args[0])
403 if not noDialog:
404 dlg.finish()
405 dlg.exec()
406 return status
407
408 def vcsExport(self, vcsDataDict, projectDir):
409 """
410 Public method used to export a directory from the Subversion
411 repository.
412
413 @param vcsDataDict dictionary of data required for the checkout
414 @param projectDir project directory to create (string)
415 @return flag indicating an execution without errors (boolean)
416 """
417 try:
418 tag = vcsDataDict["tag"]
419 except KeyError:
420 tag = None
421 vcsDir = self.svnNormalizeURL(vcsDataDict["url"])
422 if vcsDir.startswith('/') or vcsDir[1] == '|':
423 vcsDir = 'file://{0}'.format(vcsDir)
424
425 if self.otherData["standardLayout"]:
426 if tag is None or tag == '':
427 svnUrl = '{0}/trunk'.format(vcsDir)
428 else:
429 if (
430 not tag.startswith('tags') and
431 not tag.startswith('branches')
432 ):
433 type_, ok = QInputDialog.getItem(
434 None,
435 self.tr("Subversion Export"),
436 self.tr(
437 "The tag must be a normal tag (tags) or"
438 " a branch tag (branches)."
439 " Please select from the list."),
440 self.tagTypeList,
441 0, False)
442 if not ok:
443 return False
444 tag = '{0}/{1}'.format(type_, tag)
445 svnUrl = '{0}/{1}'.format(vcsDir, tag)
446 else:
447 svnUrl = vcsDir
448
449 opts = self.options['global']
450 recurse = "--non-recursive" not in opts
451 url = self.__svnURL(svnUrl)
452 client = self.getClient()
453 dlg = SvnDialog(
454 self.tr('Exporting project from Subversion repository'),
455 "export --force{0} {1} {2}".format(
456 (not recurse) and " --non-recursive" or "",
457 url, projectDir),
458 client)
459 QApplication.processEvents()
460 try:
461 with EricMutexLocker(self.vcsExecutionMutex):
462 client.export(url, projectDir, force=True, recurse=recurse)
463 status = True
464 except pysvn.ClientError as e:
465 status = False
466 dlg.showError(e.args[0])
467 dlg.finish()
468 dlg.exec()
469 return status
470
471 def vcsCommit(self, name, message, noDialog=False):
472 """
473 Public method used to make the change of a file/directory permanent
474 in the Subversion repository.
475
476 @param name file/directory name to be committed (string or
477 list of strings)
478 @param message message for this operation (string)
479 @param noDialog flag indicating quiet operations
480 """
481 if not noDialog and not message:
482 # call CommitDialog and get message from there
483 if self.__commitDialog is None:
484 from .SvnCommitDialog import SvnCommitDialog
485 self.__commitDialog = SvnCommitDialog(self, self.__ui)
486 self.__commitDialog.accepted.connect(self.__vcsCommit_Step2)
487 self.__commitDialog.show()
488 self.__commitDialog.raise_()
489 self.__commitDialog.activateWindow()
490
491 self.__commitData["name"] = name
492 self.__commitData["msg"] = message
493 self.__commitData["noDialog"] = noDialog
494
495 if noDialog:
496 self.__vcsCommit_Step2()
497
498 def __vcsCommit_Step2(self):
499 """
500 Private slot performing the second step of the commit action.
501 """
502 name = self.__commitData["name"]
503 msg = self.__commitData["msg"]
504 noDialog = self.__commitData["noDialog"]
505
506 if not noDialog:
507 # check, if there are unsaved changes, that should be committed
508 if isinstance(name, list):
509 nameList = name
510 else:
511 nameList = [name]
512 ok = True
513 for nam in nameList:
514 # check for commit of the project
515 if os.path.isdir(nam):
516 project = ericApp().getObject("Project")
517 if nam == project.getProjectPath():
518 ok &= project.checkAllScriptsDirty(
519 reportSyntaxErrors=True) and project.checkDirty()
520 continue
521 elif os.path.isfile(nam):
522 editor = ericApp().getObject("ViewManager").getOpenEditor(
523 nam)
524 if editor:
525 ok &= editor.checkDirty()
526 if not ok:
527 break
528
529 if not ok:
530 res = EricMessageBox.yesNo(
531 self.__ui,
532 self.tr("Commit Changes"),
533 self.tr(
534 """The commit affects files, that have unsaved"""
535 """ changes. Shall the commit be continued?"""),
536 icon=EricMessageBox.Warning)
537 if not res:
538 return
539
540 if self.__commitDialog is not None:
541 msg = self.__commitDialog.logMessage()
542 if self.__commitDialog.hasChangelists():
543 changelists, keepChangelists = (
544 self.__commitDialog.changelistsData()
545 )
546 else:
547 changelists, keepChangelists = [], False
548 self.__commitDialog.deleteLater()
549 self.__commitDialog = None
550 else:
551 changelists, keepChangelists = [], False
552
553 if not msg:
554 msg = '***'
555
556 if isinstance(name, list):
557 dname, fnames = self.splitPathList(name)
558 else:
559 dname, fname = self.splitPath(name)
560 fnames = [fname]
561
562 if (
563 self.svnGetReposName(dname).startswith('http') or
564 self.svnGetReposName(dname).startswith('svn')
565 ):
566 noDialog = False
567
568 cwd = os.getcwd()
569 os.chdir(dname)
570 opts = self.options['global'] + self.options['commit']
571 recurse = "--non-recursive" not in opts
572 keeplocks = "--keep-locks" in opts
573 client = self.getClient()
574 if not noDialog:
575 dlg = SvnDialog(
576 self.tr('Commiting changes to Subversion repository'),
577 "commit{0}{1}{2}{3} --message {4} {5}".format(
578 (not recurse) and " --non-recursive" or "",
579 keeplocks and " --keep-locks" or "",
580 keepChangelists and " --keep-changelists" or "",
581 changelists and
582 " --changelist ".join([""] + changelists) or "",
583 msg, " ".join(fnames)),
584 client)
585 QApplication.processEvents()
586 try:
587 with EricMutexLocker(self.vcsExecutionMutex):
588 rev = (
589 client.checkin(
590 fnames, msg, recurse=recurse, keep_locks=keeplocks,
591 keep_changelist=keepChangelists,
592 changelists=changelists)
593 if changelists else
594 client.checkin(
595 fnames, msg, recurse=recurse, keep_locks=keeplocks)
596 )
597 except pysvn.ClientError as e:
598 rev = None
599 if not noDialog:
600 dlg.showError(e.args[0])
601 if not noDialog:
602 rev and dlg.showMessage(self.tr("Committed revision {0}.")
603 .format(rev.number))
604 dlg.finish()
605 dlg.exec()
606 os.chdir(cwd)
607 self.committed.emit()
608 self.checkVCSStatus()
609
610 def vcsCommitMessages(self):
611 """
612 Public method to get the list of saved commit messages.
613
614 @return list of saved commit messages
615 @rtype list of str
616 """
617 # try per project commit history first
618 messages = self._vcsProjectCommitMessages()
619 if not messages:
620 # empty list returned, try the vcs specific one
621 messages = self.getPlugin().getPreferences("Commits")
622
623 return messages
624
625 def vcsAddCommitMessage(self, message):
626 """
627 Public method to add a commit message to the list of saved messages.
628
629 @param message message to be added
630 @type str
631 """
632 if not self._vcsAddProjectCommitMessage(message):
633 commitMessages = self.vcsCommitMessages()
634 if message in commitMessages:
635 commitMessages.remove(message)
636 commitMessages.insert(0, message)
637 no = Preferences.getVCS("CommitMessages")
638 del commitMessages[no:]
639 self.getPlugin().setPreferences("Commits", commitMessages)
640
641 def vcsClearCommitMessages(self):
642 """
643 Public method to clear the list of saved messages.
644 """
645 if not self._vcsClearProjectCommitMessages():
646 self.getPlugin().setPreferences('Commits', [])
647
648 def vcsUpdate(self, name, noDialog=False):
649 """
650 Public method used to update a file/directory with the Subversion
651 repository.
652
653 @param name file/directory name to be updated (string or list of
654 strings)
655 @param noDialog flag indicating quiet operations (boolean)
656 @return flag indicating, that the update contained an add
657 or delete (boolean)
658 """
659 if isinstance(name, list):
660 dname, fnames = self.splitPathList(name)
661 else:
662 dname, fname = self.splitPath(name)
663 fnames = [fname]
664
665 cwd = os.getcwd()
666 os.chdir(dname)
667 opts = self.options['global'] + self.options['update']
668 recurse = "--non-recursive" not in opts
669 client = self.getClient()
670 if not noDialog:
671 dlg = SvnDialog(
672 self.tr('Synchronizing with the Subversion repository'),
673 "update{0} {1}".format(
674 (not recurse) and " --non-recursive" or "",
675 " ".join(fnames)),
676 client)
677 QApplication.processEvents()
678 try:
679 with EricMutexLocker(self.vcsExecutionMutex):
680 client.update(fnames, recurse)
681 except pysvn.ClientError as e:
682 dlg.showError(e.args[0])
683 if not noDialog:
684 dlg.finish()
685 dlg.exec()
686 res = dlg.hasAddOrDelete()
687 else:
688 res = False
689 os.chdir(cwd)
690 self.checkVCSStatus()
691 return res
692
693 def vcsAdd(self, name, isDir=False, noDialog=False):
694 """
695 Public method used to add a file/directory to the Subversion
696 repository.
697
698 @param name file/directory name to be added (string)
699 @param isDir flag indicating name is a directory (boolean)
700 @param noDialog flag indicating quiet operations (boolean)
701 """
702 if isinstance(name, list):
703 if isDir:
704 dname, fname = os.path.split(name[0])
705 else:
706 dname, fnames = self.splitPathList(name)
707 else:
708 if isDir:
709 dname, fname = os.path.split(name)
710 else:
711 dname, fname = self.splitPath(name)
712 names = []
713 tree = []
714 wdir = dname
715 if self.__wcng:
716 repodir = dname
717 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
718 repodir = os.path.dirname(repodir)
719 if os.path.splitdrive(repodir)[1] == os.sep:
720 return # oops, project is not version controlled
721 while (
722 os.path.normcase(dname) != os.path.normcase(repodir) and
723 (os.path.normcase(dname) not in self.statusCache or
724 self.statusCache[os.path.normcase(dname)] ==
725 self.canBeAdded)
726 ):
727 # add directories recursively, if they aren't in the
728 # repository already
729 tree.insert(-1, dname)
730 dname = os.path.dirname(dname)
731 wdir = dname
732 else:
733 while not os.path.exists(os.path.join(dname, self.adminDir)):
734 # add directories recursively, if they aren't in the
735 # repository already
736 tree.insert(-1, dname)
737 dname = os.path.dirname(dname)
738 wdir = dname
739 names.extend(tree)
740
741 if isinstance(name, list):
742 tree2 = []
743 for n in name:
744 d = os.path.dirname(n)
745 if self.__wcng:
746 repodir = d
747 while not os.path.isdir(
748 os.path.join(repodir, self.adminDir)):
749 repodir = os.path.dirname(repodir)
750 if os.path.splitdrive(repodir)[1] == os.sep:
751 return # oops, project is not version controlled
752 while (
753 (os.path.normcase(d) != os.path.normcase(repodir)) and
754 (d not in tree2 + tree) and
755 (os.path.normcase(d) not in self.statusCache or
756 self.statusCache[os.path.normcase(d)] ==
757 self.canBeAdded)
758 ):
759 tree2.append(d)
760 d = os.path.dirname(d)
761 else:
762 while not os.path.exists(os.path.join(d, self.adminDir)):
763 if d in tree2 + tree:
764 break
765 tree2.append(d)
766 d = os.path.dirname(d)
767 tree2.reverse()
768 names.extend(tree2)
769 names.extend(name)
770 else:
771 names.append(name)
772
773 cwd = os.getcwd()
774 os.chdir(wdir)
775 opts = self.options['global'] + self.options['add']
776 recurse = False
777 force = "--force" in opts or noDialog
778 noignore = "--no-ignore" in opts
779 client = self.getClient()
780 if not noDialog:
781 dlg = SvnDialog(
782 self.tr('Adding files/directories to the Subversion'
783 ' repository'),
784 "add --non-recursive{0}{1} {2}".format(
785 force and " --force" or "",
786 noignore and " --no-ignore" or "",
787 " ".join(names)),
788 client)
789 QApplication.processEvents()
790 try:
791 with EricMutexLocker(self.vcsExecutionMutex):
792 client.add(names, recurse=recurse, force=force,
793 ignore=not noignore)
794 except pysvn.ClientError as e:
795 if not noDialog:
796 dlg.showError(e.args[0])
797 if not noDialog:
798 dlg.finish()
799 dlg.exec()
800 os.chdir(cwd)
801
802 def vcsAddBinary(self, name, isDir=False):
803 """
804 Public method used to add a file/directory in binary mode to the
805 Subversion repository.
806
807 @param name file/directory name to be added (string)
808 @param isDir flag indicating name is a directory (boolean)
809 """
810 self.vcsAdd(name, isDir)
811
812 def vcsAddTree(self, path):
813 """
814 Public method to add a directory tree rooted at path to the Subversion
815 repository.
816
817 @param path root directory of the tree to be added (string or list of
818 strings))
819 """
820 tree = []
821 if isinstance(path, list):
822 dname, fnames = self.splitPathList(path)
823 for n in path:
824 d = os.path.dirname(n)
825 if self.__wcng:
826 repodir = d
827 while not os.path.isdir(
828 os.path.join(repodir, self.adminDir)):
829 repodir = os.path.dirname(repodir)
830 if os.path.splitdrive(repodir)[1] == os.sep:
831 return # oops, project is not version controlled
832 while (
833 (os.path.normcase(d) != os.path.normcase(repodir)) and
834 (d not in tree) and
835 (os.path.normcase(d) not in self.statusCache or
836 self.statusCache[os.path.normcase(d)] ==
837 self.canBeAdded)
838 ):
839 tree.append(d)
840 d = os.path.dirname(d)
841 else:
842 while not os.path.exists(os.path.join(d, self.adminDir)):
843 # add directories recursively,
844 # if they aren't in the repository already
845 if d in tree:
846 break
847 tree.append(d)
848 d = os.path.dirname(d)
849 tree.reverse()
850 else:
851 dname, fname = os.path.split(path)
852 if self.__wcng:
853 repodir = dname
854 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
855 repodir = os.path.dirname(repodir)
856 if os.path.splitdrive(repodir)[1] == os.sep:
857 return # oops, project is not version controlled
858 while (
859 (os.path.normcase(dname) != os.path.normcase(repodir)) and
860 (os.path.normcase(dname) not in self.statusCache or
861 self.statusCache[os.path.normcase(dname)] ==
862 self.canBeAdded)
863 ):
864 # add directories recursively, if they aren't in the
865 # repository already
866 tree.insert(-1, dname)
867 dname = os.path.dirname(dname)
868 else:
869 while not os.path.exists(os.path.join(dname, self.adminDir)):
870 # add directories recursively,
871 # if they aren't in the repository already
872 tree.insert(-1, dname)
873 dname = os.path.dirname(dname)
874 if tree:
875 self.vcsAdd(tree, True)
876
877 names = []
878 if isinstance(path, list):
879 names.extend(path)
880 else:
881 names.append(path)
882
883 cwd = os.getcwd()
884 os.chdir(dname)
885 opts = self.options['global'] + self.options['add']
886 recurse = True
887 force = "--force" in opts
888 ignore = "--ignore" in opts
889 client = self.getClient()
890 dlg = SvnDialog(
891 self.tr('Adding directory trees to the Subversion repository'),
892 "add{0}{1} {2}".format(
893 force and " --force" or "",
894 ignore and " --ignore" or "",
895 " ".join(names)),
896 client)
897 QApplication.processEvents()
898 try:
899 with EricMutexLocker(self.vcsExecutionMutex):
900 client.add(names, recurse=recurse, force=force, ignore=ignore)
901 except pysvn.ClientError as e:
902 dlg.showError(e.args[0])
903 dlg.finish()
904 dlg.exec()
905 os.chdir(cwd)
906
907 def vcsRemove(self, name, project=False, noDialog=False):
908 """
909 Public method used to remove a file/directory from the Subversion
910 repository.
911
912 The default operation is to remove the local copy as well.
913
914 @param name file/directory name to be removed (string or list of
915 strings))
916 @param project flag indicating deletion of a project tree (boolean)
917 (not needed)
918 @param noDialog flag indicating quiet operations
919 @return flag indicating successfull operation (boolean)
920 """
921 if not isinstance(name, list):
922 name = [name]
923 opts = self.options['global'] + self.options['remove']
924 force = "--force" in opts or noDialog
925 client = self.getClient()
926 if not noDialog:
927 dlg = SvnDialog(
928 self.tr('Removing files/directories from the Subversion'
929 ' repository'),
930 "remove{0} {1}".format(
931 force and " --force" or "",
932 " ".join(name)),
933 client)
934 QApplication.processEvents()
935 try:
936 with EricMutexLocker(self.vcsExecutionMutex):
937 client.remove(name, force=force)
938 res = True
939 except pysvn.ClientError as e:
940 res = False
941 if not noDialog:
942 dlg.showError(e.args[0])
943 if not noDialog:
944 dlg.finish()
945 dlg.exec()
946
947 return res
948
949 def vcsMove(self, name, project, target=None, noDialog=False):
950 """
951 Public method used to move a file/directory.
952
953 @param name file/directory name to be moved (string)
954 @param project reference to the project object
955 @param target new name of the file/directory (string)
956 @param noDialog flag indicating quiet operations
957 @return flag indicating successfull operation (boolean)
958 """
959 rx_prot = re.compile('(file:|svn:|svn+ssh:|http:|https:).+')
960 opts = self.options['global']
961 res = False
962
963 if noDialog:
964 if target is None:
965 return False
966 force = True
967 accepted = True
968 else:
969 from .SvnCopyDialog import SvnCopyDialog
970 dlg = SvnCopyDialog(name, None, True, "--force" in opts)
971 accepted = (dlg.exec() == QDialog.DialogCode.Accepted)
972 if accepted:
973 target, force = dlg.getData()
974 if not target:
975 return False
976
977 isDir = (os.path.isdir(name) if rx_prot.fullmatch(target) is None
978 else False)
979
980 if accepted:
981 client = self.getClient()
982 if rx_prot.fullmatch(target) is not None:
983 target = self.__svnURL(target)
984 log = "Moving {0} to {1}".format(name, target)
985 else:
986 log = ""
987 target = target
988 if not noDialog:
989 dlg = SvnDialog(
990 self.tr('Moving {0}').format(name),
991 "move{0}{1} {2} {3}".format(
992 force and " --force" or "",
993 log and (" --message {0}".format(log)) or "",
994 name, target),
995 client, log=log)
996 QApplication.processEvents()
997 try:
998 with EricMutexLocker(self.vcsExecutionMutex):
999 client.move(name, target, force=force)
1000 res = True
1001 except pysvn.ClientError as e:
1002 res = False
1003 if not noDialog:
1004 dlg.showError(e.args[0])
1005 if not noDialog:
1006 dlg.finish()
1007 dlg.exec()
1008 if res and rx_prot.fullmatch(target) is None:
1009 if target.startswith(project.getProjectPath()):
1010 if isDir:
1011 project.moveDirectory(name, target)
1012 else:
1013 project.renameFileInPdata(name, target)
1014 else:
1015 if isDir:
1016 project.removeDirectory(name)
1017 else:
1018 project.removeFile(name)
1019 return res
1020
1021 def vcsDiff(self, name):
1022 """
1023 Public method used to view the difference of a file/directory to the
1024 Subversion repository.
1025
1026 If name is a directory and is the project directory, all project files
1027 are saved first. If name is a file (or list of files), which is/are
1028 being edited and has unsaved modification, they can be saved or the
1029 operation may be aborted.
1030
1031 @param name file/directory name to be diffed (string)
1032 """
1033 names = name[:] if isinstance(name, list) else [name]
1034 for nam in names:
1035 if os.path.isfile(nam):
1036 editor = ericApp().getObject("ViewManager").getOpenEditor(nam)
1037 if editor and not editor.checkDirty():
1038 return
1039 else:
1040 project = ericApp().getObject("Project")
1041 if nam == project.ppath and not project.saveAllScripts():
1042 return
1043 if self.diff is None:
1044 from .SvnDiffDialog import SvnDiffDialog
1045 self.diff = SvnDiffDialog(self)
1046 self.diff.show()
1047 self.diff.raise_()
1048 QApplication.processEvents()
1049 self.diff.start(name, refreshable=True)
1050
1051 def vcsStatus(self, name):
1052 """
1053 Public method used to view the status of files/directories in the
1054 Subversion repository.
1055
1056 @param name file/directory name(s) to show the status of
1057 (string or list of strings)
1058 """
1059 if self.status is None:
1060 from .SvnStatusDialog import SvnStatusDialog
1061 self.status = SvnStatusDialog(self)
1062 self.status.show()
1063 self.status.raise_()
1064 QApplication.processEvents()
1065 self.status.start(name)
1066
1067 def vcsTag(self, name):
1068 """
1069 Public method used to set the tag of a file/directory in the
1070 Subversion repository.
1071
1072 @param name file/directory name to be tagged (string)
1073 """
1074 dname, fname = self.splitPath(name)
1075
1076 reposURL = self.svnGetReposName(dname)
1077 if reposURL is None:
1078 EricMessageBox.critical(
1079 self.__ui,
1080 self.tr("Subversion Error"),
1081 self.tr(
1082 """The URL of the project repository could not be"""
1083 """ retrieved from the working copy. The tag operation"""
1084 """ will be aborted"""))
1085 return
1086
1087 url = (
1088 None
1089 if self.otherData["standardLayout"] else
1090 self.svnNormalizeURL(reposURL)
1091 )
1092 from .SvnTagDialog import SvnTagDialog
1093 dlg = SvnTagDialog(self.allTagsBranchesList, url,
1094 self.otherData["standardLayout"])
1095 if dlg.exec() == QDialog.DialogCode.Accepted:
1096 tag, tagOp = dlg.getParameters()
1097 if tag in self.allTagsBranchesList:
1098 self.allTagsBranchesList.remove(tag)
1099 self.allTagsBranchesList.insert(0, tag)
1100 else:
1101 return
1102
1103 if self.otherData["standardLayout"]:
1104 rx_base = re.compile('(.+)/(trunk|tags|branches).*')
1105
1106 match = rx_base.fullmatch(reposURL)
1107 if match is None:
1108 EricMessageBox.critical(
1109 self.__ui,
1110 self.tr("Subversion Error"),
1111 self.tr(
1112 """The URL of the project repository has an"""
1113 """ invalid format. The tag operation will"""
1114 """ be aborted"""))
1115 return
1116
1117 reposRoot = match.group(1)
1118 if tagOp in [1, 4]:
1119 url = '{0}/tags/{1}'.format(reposRoot, quote(tag))
1120 elif tagOp in [2, 8]:
1121 url = '{0}/branches/{1}'.format(reposRoot, quote(tag))
1122 else:
1123 url = self.__svnURL(tag)
1124
1125 self.tagName = tag
1126 client = self.getClient()
1127 rev = None
1128 if tagOp in [1, 2]:
1129 log = 'Created tag <{0}>'.format(self.tagName)
1130 dlg = SvnDialog(
1131 self.tr('Tagging {0} in the Subversion repository')
1132 .format(name),
1133 "copy --message {0} {1} {2}".format(log, reposURL, url),
1134 client, log=log)
1135 QApplication.processEvents()
1136 try:
1137 with EricMutexLocker(self.vcsExecutionMutex):
1138 rev = client.copy(reposURL, url)
1139 except pysvn.ClientError as e:
1140 dlg.showError(e.args[0])
1141 else:
1142 log = 'Deleted tag <{0}>'.format(self.tagName)
1143 dlg = SvnDialog(
1144 self.tr('Tagging {0} in the Subversion repository')
1145 .format(name),
1146 "remove --message {0} {1}".format(log, url),
1147 client, log=log)
1148 QApplication.processEvents()
1149 try:
1150 with EricMutexLocker(self.vcsExecutionMutex):
1151 rev = client.remove(url)
1152 except pysvn.ClientError as e:
1153 dlg.showError(e.args[0])
1154 rev and dlg.showMessage(
1155 self.tr("Revision {0}.\n").format(rev.number))
1156 dlg.finish()
1157 dlg.exec()
1158
1159 def vcsRevert(self, name):
1160 """
1161 Public method used to revert changes made to a file/directory.
1162
1163 @param name file/directory name to be reverted
1164 @type str
1165 @return flag indicating, that the update contained an add
1166 or delete
1167 @rtype bool
1168 """
1169 recurse = False
1170 if not isinstance(name, list):
1171 name = [name]
1172 if os.path.isdir(name[0]):
1173 recurse = True
1174
1175 project = ericApp().getObject("Project")
1176 names = [project.getRelativePath(nam) for nam in name]
1177 if names[0]:
1178 from UI.DeleteFilesConfirmationDialog import (
1179 DeleteFilesConfirmationDialog
1180 )
1181 dia = DeleteFilesConfirmationDialog(
1182 self.parent(),
1183 self.tr("Revert changes"),
1184 self.tr(
1185 "Do you really want to revert all changes to these files"
1186 " or directories?"),
1187 name)
1188 yes = dia.exec() == QDialog.DialogCode.Accepted
1189 else:
1190 yes = EricMessageBox.yesNo(
1191 None,
1192 self.tr("Revert changes"),
1193 self.tr("""Do you really want to revert all changes of"""
1194 """ the project?"""))
1195 if yes:
1196 client = self.getClient()
1197 dlg = SvnDialog(
1198 self.tr('Reverting changes'),
1199 "revert {0} {1}".format(
1200 (not recurse) and " --non-recursive" or "",
1201 " ".join(name)),
1202 client)
1203 QApplication.processEvents()
1204 try:
1205 with EricMutexLocker(self.vcsExecutionMutex):
1206 client.revert(name, recurse)
1207 except pysvn.ClientError as e:
1208 dlg.showError(e.args[0])
1209 dlg.finish()
1210 dlg.exec()
1211 self.checkVCSStatus()
1212
1213 return False
1214
1215 def vcsForget(self, name):
1216 """
1217 Public method used to remove a file from the repository.
1218
1219 Note: svn does not support this operation. The method is implemented
1220 as a NoOp.
1221
1222 @param name file/directory name to be removed
1223 @type str or list of str
1224 """
1225 pass
1226
1227 def vcsSwitch(self, name):
1228 """
1229 Public method used to switch a directory to a different tag/branch.
1230
1231 @param name directory name to be switched (string)
1232 @return flag indicating, that the switch contained an add
1233 or delete (boolean)
1234 """
1235 dname, fname = self.splitPath(name)
1236
1237 reposURL = self.svnGetReposName(dname)
1238 if reposURL is None:
1239 EricMessageBox.critical(
1240 self.__ui,
1241 self.tr("Subversion Error"),
1242 self.tr(
1243 """The URL of the project repository could not be"""
1244 """ retrieved from the working copy. The switch"""
1245 """ operation will be aborted"""))
1246 return False
1247
1248 url = (
1249 None
1250 if self.otherData["standardLayout"] else
1251 self.svnNormalizeURL(reposURL)
1252 )
1253 from .SvnSwitchDialog import SvnSwitchDialog
1254 dlg = SvnSwitchDialog(self.allTagsBranchesList, url,
1255 self.otherData["standardLayout"])
1256 if dlg.exec() == QDialog.DialogCode.Accepted:
1257 tag, tagType = dlg.getParameters()
1258 if tag in self.allTagsBranchesList:
1259 self.allTagsBranchesList.remove(tag)
1260 self.allTagsBranchesList.insert(0, tag)
1261 else:
1262 return False
1263
1264 if self.otherData["standardLayout"]:
1265 rx_base = re.compile('(.+)/(trunk|tags|branches).*')
1266 match = rx_base.fullmatch(reposURL)
1267 if match is None:
1268 EricMessageBox.critical(
1269 self.__ui,
1270 self.tr("Subversion Error"),
1271 self.tr(
1272 """The URL of the project repository has an"""
1273 """ invalid format. The switch operation will"""
1274 """ be aborted"""))
1275 return False
1276
1277 reposRoot = match.group(1)
1278 tn = tag
1279 if tagType == 1:
1280 url = '{0}/tags/{1}'.format(reposRoot, quote(tag))
1281 elif tagType == 2:
1282 url = '{0}/branches/{1}'.format(reposRoot, quote(tag))
1283 elif tagType == 4:
1284 url = '{0}/trunk'.format(reposRoot)
1285 tn = 'HEAD'
1286 else:
1287 url = self.__svnURL(tag)
1288 tn = url
1289
1290 client = self.getClient()
1291 dlg = SvnDialog(self.tr('Switching to {0}').format(tn),
1292 "switch {0} {1}".format(url, name),
1293 client)
1294 QApplication.processEvents()
1295 try:
1296 with EricMutexLocker(self.vcsExecutionMutex):
1297 rev = client.switch(name, url)
1298 dlg.showMessage(self.tr("Revision {0}.\n").format(rev.number))
1299 except pysvn.ClientError as e:
1300 dlg.showError(e.args[0])
1301 dlg.finish()
1302 dlg.exec()
1303 res = dlg.hasAddOrDelete()
1304 self.checkVCSStatus()
1305 return res
1306
1307 def vcsMerge(self, name):
1308 """
1309 Public method used to merge a URL/revision into the local project.
1310
1311 @param name file/directory name to be merged (string)
1312 """
1313 dname, fname = self.splitPath(name)
1314
1315 opts = self.options['global']
1316 from .SvnMergeDialog import SvnMergeDialog
1317 dlg = SvnMergeDialog(self.mergeList[0], self.mergeList[1],
1318 self.mergeList[2], "--force" in opts)
1319 if dlg.exec() == QDialog.DialogCode.Accepted:
1320 urlrev1, urlrev2, target, force = dlg.getParameters()
1321 else:
1322 return
1323
1324 # remember URL or revision
1325 if urlrev1 in self.mergeList[0]:
1326 self.mergeList[0].remove(urlrev1)
1327 self.mergeList[0].insert(0, urlrev1)
1328 if urlrev2 in self.mergeList[1]:
1329 self.mergeList[1].remove(urlrev2)
1330 self.mergeList[1].insert(0, urlrev2)
1331
1332 rx_rev = re.compile('\\d+|HEAD|head')
1333
1334 cwd = os.getcwd()
1335 os.chdir(dname)
1336 recurse = "--non-recursive" not in opts
1337 if bool(rx_rev.fullmatch(urlrev1)):
1338 if urlrev1 in ["HEAD", "head"]:
1339 revision1 = pysvn.Revision(pysvn.opt_revision_kind.head)
1340 rev1 = "HEAD"
1341 else:
1342 revision1 = pysvn.Revision(
1343 pysvn.opt_revision_kind.number, int(urlrev1))
1344 rev1 = urlrev1
1345 if urlrev2 in ["HEAD", "head"]:
1346 revision2 = pysvn.Revision(pysvn.opt_revision_kind.head)
1347 rev2 = "HEAD"
1348 else:
1349 revision2 = pysvn.Revision(
1350 pysvn.opt_revision_kind.number, int(urlrev2))
1351 rev2 = urlrev2
1352 if not target:
1353 url1 = name
1354 url2 = name
1355 else:
1356 url1 = target
1357 url2 = target
1358
1359 # remember target
1360 if target in self.mergeList[2]:
1361 self.mergeList[2].remove(target)
1362 self.mergeList[2].insert(0, target)
1363 else:
1364 if "@" in urlrev1:
1365 url1, rev = urlrev1.split("@")
1366 if rev in ["HEAD", "head"]:
1367 revision1 = pysvn.Revision(pysvn.opt_revision_kind.head)
1368 rev1 = "HEAD"
1369 else:
1370 revision1 = pysvn.Revision(
1371 pysvn.opt_revision_kind.number, int(rev))
1372 rev1 = rev
1373 else:
1374 url1 = urlrev1
1375 revision1 = pysvn.Revision(pysvn.opt_revision_kind.unspecified)
1376 rev1 = ""
1377 if "@" in urlrev2:
1378 url2, rev = urlrev2.split("@")
1379 if rev in ["HEAD", "head"]:
1380 revision2 = pysvn.Revision(pysvn.opt_revision_kind.head)
1381 rev2 = "HEAD"
1382 else:
1383 revision2 = pysvn.Revision(
1384 pysvn.opt_revision_kind.number, int(rev))
1385 rev2 = rev
1386 else:
1387 url2 = urlrev2
1388 revision2 = pysvn.Revision(pysvn.opt_revision_kind.unspecified)
1389 rev2 = ""
1390 client = self.getClient()
1391 dlg = SvnDialog(
1392 self.tr('Merging {0}').format(name),
1393 "merge{0}{1} {2} {3} {4}".format(
1394 (not recurse) and " --non-recursive" or "",
1395 force and " --force" or "",
1396 "{0}{1}".format(url1, rev1 and ("@" + rev1) or ""),
1397 "{0}{1}".format(url2, rev2 and ("@" + rev2) or ""),
1398 fname),
1399 client)
1400 QApplication.processEvents()
1401 try:
1402 with EricMutexLocker(self.vcsExecutionMutex):
1403 client.merge(url1, revision1, url2, revision2, fname,
1404 recurse=recurse, force=force)
1405 except pysvn.ClientError as e:
1406 dlg.showError(e.args[0])
1407 dlg.finish()
1408 dlg.exec()
1409 os.chdir(cwd)
1410
1411 def vcsRegisteredState(self, name):
1412 """
1413 Public method used to get the registered state of a file in the vcs.
1414
1415 @param name filename to check (string)
1416 @return a combination of canBeCommited and canBeAdded
1417 """
1418 if self.__wcng:
1419 return self.__vcsRegisteredState_wcng(name)
1420 else:
1421 return self.__vcsRegisteredState_wc(name)
1422
1423 def __vcsRegisteredState_wcng(self, name):
1424 """
1425 Private method used to get the registered state of a file in the vcs.
1426
1427 This is the variant for subversion installations using the new
1428 working copy meta-data format.
1429
1430 @param name filename to check (string)
1431 @return a combination of canBeCommited and canBeAdded
1432 """
1433 if name.endswith(os.sep):
1434 name = name[:-1]
1435 name = os.path.normcase(name)
1436 dname, fname = self.splitPath(name)
1437
1438 if fname == '.' and os.path.isdir(os.path.join(dname, self.adminDir)):
1439 return self.canBeCommitted
1440
1441 if name in self.statusCache:
1442 return self.statusCache[name]
1443
1444 name = os.path.normcase(name)
1445 states = {name: 0}
1446 states = self.vcsAllRegisteredStates(states, dname, False)
1447 if states[name] == self.canBeCommitted:
1448 return self.canBeCommitted
1449 else:
1450 return self.canBeAdded
1451
1452 def __vcsRegisteredState_wc(self, name):
1453 """
1454 Private method used to get the registered state of a file in the vcs.
1455
1456 This is the variant for subversion installations using the old working
1457 copy meta-data format.
1458
1459 @param name filename to check (string)
1460 @return a combination of canBeCommited and canBeAdded
1461 """
1462 dname, fname = self.splitPath(name)
1463
1464 if fname == '.':
1465 if os.path.isdir(os.path.join(dname, self.adminDir)):
1466 return self.canBeCommitted
1467 else:
1468 return self.canBeAdded
1469
1470 name = os.path.normcase(name)
1471 states = {name: 0}
1472 states = self.vcsAllRegisteredStates(states, dname, False)
1473 if states[name] == self.canBeCommitted:
1474 return self.canBeCommitted
1475 else:
1476 return self.canBeAdded
1477
1478 def vcsAllRegisteredStates(self, names, dname, shortcut=True):
1479 """
1480 Public method used to get the registered states of a number of files
1481 in the vcs.
1482
1483 <b>Note:</b> If a shortcut is to be taken, the code will only check,
1484 if the named directory has been scanned already. If so, it is assumed,
1485 that the states for all files has been populated by the previous run.
1486
1487 @param names dictionary with all filenames to be checked as keys
1488 @param dname directory to check in (string)
1489 @param shortcut flag indicating a shortcut should be taken (boolean)
1490 @return the received dictionary completed with a combination of
1491 canBeCommited and canBeAdded or None in order to signal an error
1492 """
1493 if self.__wcng:
1494 return self.__vcsAllRegisteredStates_wcng(names, dname, shortcut)
1495 else:
1496 return self.__vcsAllRegisteredStates_wc(names, dname, shortcut)
1497
1498 def __vcsAllRegisteredStates_wcng(self, names, dname, shortcut=True):
1499 """
1500 Private method used to get the registered states of a number of files
1501 in the vcs.
1502
1503 This is the variant for subversion installations using the new working
1504 copy meta-data format.
1505
1506 <b>Note:</b> If a shortcut is to be taken, the code will only check,
1507 if the named directory has been scanned already. If so, it is assumed,
1508 that the states for all files has been populated by the previous run.
1509
1510 @param names dictionary with all filenames to be checked as keys
1511 @param dname directory to check in (string)
1512 @param shortcut flag indicating a shortcut should be taken (boolean)
1513 @return the received dictionary completed with a combination of
1514 canBeCommited and canBeAdded or None in order to signal an error
1515 """
1516 if dname.endswith(os.sep):
1517 dname = dname[:-1]
1518 dname = os.path.normcase(dname)
1519
1520 found = False
1521 for name in self.statusCache:
1522 if name in names:
1523 found = True
1524 names[name] = self.statusCache[name]
1525
1526 if not found:
1527 # find the root of the repo
1528 repodir = dname
1529 while not os.path.isdir(os.path.join(repodir, self.adminDir)):
1530 repodir = os.path.dirname(repodir)
1531 if os.path.splitdrive(repodir)[1] == os.sep:
1532 return names
1533
1534 from .SvnDialogMixin import SvnDialogMixin
1535 mixin = SvnDialogMixin()
1536 client = self.getClient()
1537 client.callback_get_login = mixin._clientLoginCallback
1538 client.callback_ssl_server_trust_prompt = (
1539 mixin._clientSslServerTrustPromptCallback
1540 )
1541
1542 with contextlib.suppress(pysvn.ClientError):
1543 with EricMutexLocker(self.vcsExecutionMutex):
1544 allFiles = client.status(dname, recurse=True, get_all=True,
1545 ignore=True, update=False)
1546 dirs = [x for x in names.keys() if os.path.isdir(x)]
1547 for file in allFiles:
1548 name = os.path.normcase(file.path)
1549 if self.__isVersioned(file):
1550 if name in names:
1551 names[name] = self.canBeCommitted
1552 dn = name
1553 while (
1554 os.path.splitdrive(dn)[1] != os.sep and
1555 dn != repodir
1556 ):
1557 dn = os.path.dirname(dn)
1558 if (
1559 dn in self.statusCache and
1560 self.statusCache[dn] ==
1561 self.canBeCommitted
1562 ):
1563 break
1564 self.statusCache[dn] = self.canBeCommitted
1565 self.statusCache[name] = self.canBeCommitted
1566 if dirs:
1567 for d in dirs:
1568 if name.startswith(d):
1569 names[d] = self.canBeCommitted
1570 self.statusCache[d] = self.canBeCommitted
1571 dirs.remove(d)
1572 break
1573 else:
1574 self.statusCache[name] = self.canBeAdded
1575
1576 return names
1577
1578 def __vcsAllRegisteredStates_wc(self, names, dname, shortcut=True):
1579 """
1580 Private method used to get the registered states of a number of files
1581 in the VCS.
1582
1583 This is the variant for subversion installations using the old working
1584 copy meta-data format.
1585
1586 <b>Note:</b> If a shortcut is to be taken, the code will only check,
1587 if the named directory has been scanned already. If so, it is assumed,
1588 that the states for all files has been populated by the previous run.
1589
1590 @param names dictionary with all filenames to be checked as keys
1591 @param dname directory to check in (string)
1592 @param shortcut flag indicating a shortcut should be taken (boolean)
1593 @return the received dictionary completed with a combination of
1594 canBeCommited and canBeAdded or None in order to signal an error
1595 """
1596 if not os.path.isdir(os.path.join(dname, self.adminDir)):
1597 # not under version control -> do nothing
1598 return names
1599
1600 found = False
1601 for name in self.statusCache:
1602 if os.path.dirname(name) == dname:
1603 if shortcut:
1604 found = True
1605 break
1606 if name in names:
1607 found = True
1608 names[name] = self.statusCache[name]
1609
1610 if not found:
1611 from .SvnDialogMixin import SvnDialogMixin
1612 mixin = SvnDialogMixin()
1613 client = self.getClient()
1614 client.callback_get_login = mixin._clientLoginCallback
1615 client.callback_ssl_server_trust_prompt = (
1616 mixin._clientSslServerTrustPromptCallback
1617 )
1618
1619 with contextlib.suppress(pysvn.ClientError):
1620 with EricMutexLocker(self.vcsExecutionMutex):
1621 allFiles = client.status(dname, recurse=True, get_all=True,
1622 ignore=True, update=False)
1623 for file in allFiles:
1624 name = os.path.normcase(file.path)
1625 if self.__isVersioned(file):
1626 if name in names:
1627 names[name] = self.canBeCommitted
1628 self.statusCache[name] = self.canBeCommitted
1629 else:
1630 self.statusCache[name] = self.canBeAdded
1631
1632 return names
1633
1634 def __isVersioned(self, status):
1635 """
1636 Private method to check, if the given status indicates a
1637 versioned state.
1638
1639 @param status status object to check (pysvn.PysvnStatus)
1640 @return flag indicating a versioned state (boolean)
1641 """
1642 return status["text_status"] in [
1643 pysvn.wc_status_kind.normal,
1644 pysvn.wc_status_kind.added,
1645 pysvn.wc_status_kind.missing,
1646 pysvn.wc_status_kind.deleted,
1647 pysvn.wc_status_kind.replaced,
1648 pysvn.wc_status_kind.modified,
1649 pysvn.wc_status_kind.merged,
1650 pysvn.wc_status_kind.conflicted,
1651 ]
1652
1653 def clearStatusCache(self):
1654 """
1655 Public method to clear the status cache.
1656 """
1657 self.statusCache = {}
1658
1659 def vcsInitConfig(self, project):
1660 """
1661 Public method to initialize the VCS configuration.
1662
1663 This method ensures, that eric specific files and directories are
1664 ignored.
1665
1666 @param project reference to the project (Project)
1667 """
1668 configPath = getConfigPath()
1669 if os.path.exists(configPath):
1670 amendConfig()
1671 else:
1672 createDefaultConfig()
1673
1674 def vcsName(self):
1675 """
1676 Public method returning the name of the vcs.
1677
1678 @return always 'Subversion' (string)
1679 """
1680 return "Subversion"
1681
1682 def vcsCleanup(self, name):
1683 """
1684 Public method used to cleanup the working copy.
1685
1686 @param name directory name to be cleaned up (string)
1687 """
1688 client = self.getClient()
1689 dlg = SvnDialog(self.tr('Cleaning up {0}').format(name),
1690 "cleanup {0}".format(name),
1691 client)
1692 QApplication.processEvents()
1693 try:
1694 with EricMutexLocker(self.vcsExecutionMutex):
1695 client.cleanup(name)
1696 except pysvn.ClientError as e:
1697 dlg.showError(e.args[0])
1698 dlg.finish()
1699 dlg.exec()
1700
1701 def vcsCommandLine(self, name):
1702 """
1703 Public method used to execute arbitrary subversion commands.
1704
1705 @param name directory name of the working directory (string)
1706 """
1707 from .SvnCommandDialog import SvnCommandDialog
1708 dlg = SvnCommandDialog(self.commandHistory, self.wdHistory, name)
1709 if dlg.exec() == QDialog.DialogCode.Accepted:
1710 command, wd = dlg.getData()
1711 commandList = Utilities.parseOptionString(command)
1712
1713 # This moves any previous occurrence of these arguments to the head
1714 # of the list.
1715 if command in self.commandHistory:
1716 self.commandHistory.remove(command)
1717 self.commandHistory.insert(0, command)
1718 if wd in self.wdHistory:
1719 self.wdHistory.remove(wd)
1720 self.wdHistory.insert(0, wd)
1721
1722 args = []
1723 self.addArguments(args, commandList)
1724
1725 from Plugins.VcsPlugins.vcsSubversion.SvnDialog import (
1726 SvnDialog as SvnProcessDialog
1727 )
1728 dia = SvnProcessDialog(self.tr('Subversion command'))
1729 res = dia.startProcess(args, wd)
1730 if res:
1731 dia.exec()
1732
1733 def vcsOptionsDialog(self, project, archive, editable=False, parent=None):
1734 """
1735 Public method to get a dialog to enter repository info.
1736
1737 @param project reference to the project object
1738 @param archive name of the project in the repository (string)
1739 @param editable flag indicating that the project name is editable
1740 (boolean)
1741 @param parent parent widget (QWidget)
1742 @return reference to the instantiated options dialog (SvnOptionsDialog)
1743 """
1744 from .SvnOptionsDialog import SvnOptionsDialog
1745 return SvnOptionsDialog(self, project, parent)
1746
1747 def vcsNewProjectOptionsDialog(self, parent=None):
1748 """
1749 Public method to get a dialog to enter repository info for getting a
1750 new project.
1751
1752 @param parent parent widget (QWidget)
1753 @return reference to the instantiated options dialog
1754 (SvnNewProjectOptionsDialog)
1755 """
1756 from .SvnNewProjectOptionsDialog import SvnNewProjectOptionsDialog
1757 return SvnNewProjectOptionsDialog(self, parent)
1758
1759 def vcsRepositoryInfos(self, ppath):
1760 """
1761 Public method to retrieve information about the repository.
1762
1763 @param ppath local path to get the repository infos (string)
1764 @return string with ready formated info for display (string)
1765 """
1766 try:
1767 entry = self.getClient().info(ppath)
1768 except pysvn.ClientError as e:
1769 return e.args[0]
1770
1771 apiVersion = (
1772 "{0} {1}".format(
1773 ".".join([str(v) for v in pysvn.svn_api_version[:3]]),
1774 pysvn.svn_api_version[3])
1775 if hasattr(pysvn, 'svn_api_version') else
1776 QCoreApplication.translate('subversion', "unknown")
1777 )
1778
1779 hmsz = time.strftime("%H:%M:%S %Z", time.localtime(entry.commit_time))
1780 return QCoreApplication.translate(
1781 'subversion',
1782 """<h3>Repository information</h3>"""
1783 """<table>"""
1784 """<tr><td><b>PySvn V.</b></td><td>{0}</td></tr>"""
1785 """<tr><td><b>Subversion V.</b></td><td>{1}</td></tr>"""
1786 """<tr><td><b>Subversion API V.</b></td><td>{2}</td></tr>"""
1787 """<tr><td><b>URL</b></td><td>{3}</td></tr>"""
1788 """<tr><td><b>Current revision</b></td><td>{4}</td></tr>"""
1789 """<tr><td><b>Committed revision</b></td><td>{5}</td></tr>"""
1790 """<tr><td><b>Committed date</b></td><td>{6}</td></tr>"""
1791 """<tr><td><b>Comitted time</b></td><td>{7}</td></tr>"""
1792 """<tr><td><b>Last author</b></td><td>{8}</td></tr>"""
1793 """</table>"""
1794 ).format(
1795 ".".join([str(v) for v in pysvn.version]),
1796 ".".join([str(v) for v in pysvn.svn_version[:3]]),
1797 apiVersion,
1798 entry.url,
1799 entry.revision.number,
1800 entry.commit_revision.number,
1801 time.strftime(
1802 "%Y-%m-%d", time.localtime(entry.commit_time)),
1803 hmsz,
1804 entry.commit_author
1805 )
1806
1807 ###########################################################################
1808 ## Public Subversion specific methods are below.
1809 ###########################################################################
1810
1811 def svnGetReposName(self, path):
1812 """
1813 Public method used to retrieve the URL of the subversion repository
1814 path.
1815
1816 @param path local path to get the svn repository path for (string)
1817 @return string with the repository path URL
1818 """
1819 client = pysvn.Client()
1820 try:
1821 with EricMutexLocker(self.vcsExecutionMutex):
1822 entry = client.info(path)
1823 url = entry.url
1824 except pysvn.ClientError:
1825 url = ""
1826 return url
1827
1828 def vcsResolved(self, name):
1829 """
1830 Public method used to resolve conflicts of a file/directory.
1831
1832 @param name file/directory name to be resolved (string)
1833 """
1834 if isinstance(name, list):
1835 dname, fnames = self.splitPathList(name)
1836 else:
1837 dname, fname = self.splitPath(name)
1838 fnames = [fname]
1839
1840 cwd = os.getcwd()
1841 os.chdir(dname)
1842 opts = self.options['global']
1843 recurse = "--non-recursive" not in opts
1844 client = self.getClient()
1845 dlg = SvnDialog(self.tr('Resolving conficts'),
1846 "resolved{0} {1}".format(
1847 (not recurse) and " --non-recursive" or "",
1848 " ".join(fnames)),
1849 client)
1850 QApplication.processEvents()
1851 try:
1852 with EricMutexLocker(self.vcsExecutionMutex):
1853 for name in fnames:
1854 client.resolved(name, recurse=recurse)
1855 except pysvn.ClientError as e:
1856 dlg.showError(e.args[0])
1857 dlg.finish()
1858 dlg.exec()
1859 os.chdir(cwd)
1860 self.checkVCSStatus()
1861
1862 def svnCopy(self, name, project):
1863 """
1864 Public method used to copy a file/directory.
1865
1866 @param name file/directory name to be copied (string)
1867 @param project reference to the project object
1868 @return flag indicating successfull operation (boolean)
1869 """
1870 from .SvnCopyDialog import SvnCopyDialog
1871 rx_prot = re.compile('(file:|svn:|svn+ssh:|http:|https:).+')
1872 dlg = SvnCopyDialog(name)
1873 res = False
1874 if dlg.exec() == QDialog.DialogCode.Accepted:
1875 target, force = dlg.getData()
1876
1877 client = self.getClient()
1878 if bool(rx_prot.fullmatch(target)):
1879 target = self.__svnURL(target)
1880 log = "Copying {0} to {1}".format(name, target)
1881 else:
1882 log = ""
1883 target = target
1884 dlg = SvnDialog(
1885 self.tr('Copying {0}').format(name),
1886 "copy{0} {1} {2}".format(
1887 log and (" --message {0}".format(log)) or "",
1888 name, target),
1889 client, log=log)
1890 QApplication.processEvents()
1891 try:
1892 with EricMutexLocker(self.vcsExecutionMutex):
1893 client.copy(name, target)
1894 res = True
1895 except pysvn.ClientError as e:
1896 res = False
1897 dlg.showError(e.args[0])
1898 dlg.finish()
1899 dlg.exec()
1900 if (
1901 res and
1902 not bool(rx_prot.fullmatch(target)) and
1903 target.startswith(project.getProjectPath())
1904 ):
1905 if os.path.isdir(name):
1906 project.copyDirectory(name, target)
1907 else:
1908 project.appendFile(target)
1909 return res
1910
1911 def svnListProps(self, name, recursive=False):
1912 """
1913 Public method used to list the properties of a file/directory.
1914
1915 @param name file/directory name (string or list of strings)
1916 @param recursive flag indicating a recursive list is requested
1917 """
1918 if self.propList is None:
1919 from .SvnPropListDialog import SvnPropListDialog
1920 self.propList = SvnPropListDialog(self)
1921 self.propList.show()
1922 self.propList.raise_()
1923 QApplication.processEvents()
1924 self.propList.start(name, recursive)
1925
1926 def svnSetProp(self, name, recursive=False):
1927 """
1928 Public method used to add a property to a file/directory.
1929
1930 @param name file/directory name (string or list of strings)
1931 @param recursive flag indicating a recursive set is requested
1932 """
1933 from .SvnPropSetDialog import SvnPropSetDialog
1934 dlg = SvnPropSetDialog(recursive)
1935 if dlg.exec() == QDialog.DialogCode.Accepted:
1936 propName, propValue, recurse = dlg.getData()
1937 if not propName:
1938 EricMessageBox.critical(
1939 self.__ui,
1940 self.tr("Subversion Set Property"),
1941 self.tr(
1942 """You have to supply a property name. Aborting."""))
1943 return
1944
1945 if isinstance(name, list):
1946 dname, fnames = self.splitPathList(name)
1947 else:
1948 dname, fname = self.splitPath(name)
1949 fnames = [fname]
1950
1951 cwd = os.getcwd()
1952 os.chdir(dname)
1953 opts = self.options['global']
1954 skipchecks = "--skip-checks" in opts
1955 client = self.getClient()
1956 dlg = SvnDialog(
1957 self.tr('Subversion Set Property'),
1958 "propset{0}{1} {2} {3} {4}".format(
1959 recurse and " --recurse" or "",
1960 skipchecks and " --skip-checks" or "",
1961 propName, propValue,
1962 " ".join(fnames)),
1963 client)
1964 QApplication.processEvents()
1965 try:
1966 with EricMutexLocker(self.vcsExecutionMutex):
1967 for name in fnames:
1968 client.propset(propName, propValue, name,
1969 recurse=recurse, skip_checks=skipchecks)
1970 except pysvn.ClientError as e:
1971 dlg.showError(e.args[0])
1972 dlg.showMessage(self.tr("Property set."))
1973 dlg.finish()
1974 dlg.exec()
1975 os.chdir(cwd)
1976
1977 def svnDelProp(self, name, recursive=False):
1978 """
1979 Public method used to delete a property of a file/directory.
1980
1981 @param name file/directory name (string or list of strings)
1982 @param recursive flag indicating a recursive list is requested
1983 """
1984 from .SvnPropDelDialog import SvnPropDelDialog
1985 dlg = SvnPropDelDialog(recursive)
1986 if dlg.exec() == QDialog.DialogCode.Accepted:
1987 propName, recurse = dlg.getData()
1988
1989 if not propName:
1990 EricMessageBox.critical(
1991 self.__ui,
1992 self.tr("Subversion Delete Property"),
1993 self.tr(
1994 """You have to supply a property name. Aborting."""))
1995 return
1996
1997 if isinstance(name, list):
1998 dname, fnames = self.splitPathList(name)
1999 else:
2000 dname, fname = self.splitPath(name)
2001 fnames = [fname]
2002
2003 cwd = os.getcwd()
2004 os.chdir(dname)
2005 opts = self.options['global']
2006 skipchecks = "--skip-checks" in opts
2007 client = self.getClient()
2008 dlg = SvnDialog(
2009 self.tr('Subversion Delete Property'),
2010 "propdel{0}{1} {2} {3}".format(
2011 recurse and " --recurse" or "",
2012 skipchecks and " --skip-checks" or "",
2013 propName, " ".join(fnames)),
2014 client)
2015 QApplication.processEvents()
2016 try:
2017 with EricMutexLocker(self.vcsExecutionMutex):
2018 for name in fnames:
2019 client.propdel(propName, name,
2020 recurse=recurse, skip_checks=skipchecks)
2021 except pysvn.ClientError as e:
2022 dlg.showError(e.args[0])
2023 dlg.showMessage(self.tr("Property deleted."))
2024 dlg.finish()
2025 dlg.exec()
2026 os.chdir(cwd)
2027
2028 def svnListTagBranch(self, path, tags=True):
2029 """
2030 Public method used to list the available tags or branches.
2031
2032 @param path directory name of the project (string)
2033 @param tags flag indicating listing of branches or tags
2034 (False = branches, True = tags)
2035 """
2036 if self.tagbranchList is None:
2037 from .SvnTagBranchListDialog import SvnTagBranchListDialog
2038 self.tagbranchList = SvnTagBranchListDialog(self)
2039 self.tagbranchList.show()
2040 self.tagbranchList.raise_()
2041 QApplication.processEvents()
2042 res = self.tagbranchList.start(path, tags)
2043 if res:
2044 if tags:
2045 self.tagsList = self.tagbranchList.getTagList()
2046 if not self.showedTags:
2047 self.allTagsBranchesList = (
2048 self.allTagsBranchesList +
2049 self.tagsList
2050 )
2051 self.showedTags = True
2052 elif not tags:
2053 self.branchesList = self.tagbranchList.getTagList()
2054 if not self.showedBranches:
2055 self.allTagsBranchesList = (
2056 self.allTagsBranchesList +
2057 self.branchesList
2058 )
2059 self.showedBranches = True
2060
2061 def svnBlame(self, name):
2062 """
2063 Public method to show the output of the svn blame command.
2064
2065 @param name file name to show the blame for (string)
2066 """
2067 if self.blame is None:
2068 from .SvnBlameDialog import SvnBlameDialog
2069 self.blame = SvnBlameDialog(self)
2070 self.blame.show()
2071 self.blame.raise_()
2072 QApplication.processEvents()
2073 self.blame.start(name)
2074
2075 def svnExtendedDiff(self, name):
2076 """
2077 Public method used to view the difference of a file/directory to the
2078 Subversion repository.
2079
2080 If name is a directory and is the project directory, all project files
2081 are saved first. If name is a file (or list of files), which is/are
2082 being edited and has unsaved modification, they can be saved or the
2083 operation may be aborted.
2084
2085 This method gives the chance to enter the revisions to be compared.
2086
2087 @param name file/directory name to be diffed (string)
2088 """
2089 names = name[:] if isinstance(name, list) else [name]
2090 for nam in names:
2091 if os.path.isfile(nam):
2092 editor = ericApp().getObject("ViewManager").getOpenEditor(nam)
2093 if editor and not editor.checkDirty():
2094 return
2095 else:
2096 project = ericApp().getObject("Project")
2097 if nam == project.ppath and not project.saveAllScripts():
2098 return
2099 from .SvnRevisionSelectionDialog import SvnRevisionSelectionDialog
2100 dlg = SvnRevisionSelectionDialog()
2101 if dlg.exec() == QDialog.DialogCode.Accepted:
2102 revisions = dlg.getRevisions()
2103 if self.diff is None:
2104 from .SvnDiffDialog import SvnDiffDialog
2105 self.diff = SvnDiffDialog(self)
2106 self.diff.show()
2107 self.diff.raise_()
2108 QApplication.processEvents()
2109 self.diff.start(name, revisions)
2110
2111 def svnUrlDiff(self, name):
2112 """
2113 Public method used to view the difference of a file/directory of two
2114 repository URLs.
2115
2116 If name is a directory and is the project directory, all project files
2117 are saved first. If name is a file (or list of files), which is/are
2118 being edited and has unsaved modification, they can be saved or the
2119 operation may be aborted.
2120
2121 This method gives the chance to enter the revisions to be compared.
2122
2123 @param name file/directory name to be diffed (string)
2124 """
2125 names = name[:] if isinstance(name, list) else [name]
2126 for nam in names:
2127 if os.path.isfile(nam):
2128 editor = ericApp().getObject("ViewManager").getOpenEditor(nam)
2129 if editor and not editor.checkDirty():
2130 return
2131 else:
2132 project = ericApp().getObject("Project")
2133 if nam == project.ppath and not project.saveAllScripts():
2134 return
2135
2136 dname = self.splitPath(names[0])[0]
2137
2138 from .SvnUrlSelectionDialog import SvnUrlSelectionDialog
2139 dlg = SvnUrlSelectionDialog(self, self.tagsList, self.branchesList,
2140 dname)
2141 if dlg.exec() == QDialog.DialogCode.Accepted:
2142 urls, summary = dlg.getURLs()
2143 if self.diff is None:
2144 from .SvnDiffDialog import SvnDiffDialog
2145 self.diff = SvnDiffDialog(self)
2146 self.diff.show()
2147 self.diff.raise_()
2148 QApplication.processEvents()
2149 self.diff.start(name, urls=urls, summary=summary)
2150
2151 def __svnGetFileForRevision(self, name, rev=""):
2152 """
2153 Private method to get a file for a specific revision from the
2154 repository.
2155
2156 @param name file name to get from the repository (string)
2157 @param rev revision to retrieve (integer or string)
2158 @return contents of the file (string) and an error message (string)
2159 """
2160 output = ""
2161 error = ""
2162
2163 client = self.getClient()
2164 try:
2165 if rev:
2166 if isinstance(rev, int) or rev.isdecimal():
2167 rev = pysvn.Revision(
2168 pysvn.opt_revision_kind.number, int(rev))
2169 elif rev.startswith("{"):
2170 dateStr = rev[1:-1]
2171 secs = QDateTime.fromString(
2172 dateStr, Qt.DateFormat.ISODate).toTime_t()
2173 rev = pysvn.Revision(pysvn.opt_revision_kind.date, secs)
2174 elif rev == "HEAD":
2175 rev = pysvn.Revision(pysvn.opt_revision_kind.head)
2176 elif rev == "COMMITTED":
2177 rev = pysvn.Revision(pysvn.opt_revision_kind.committed)
2178 elif rev == "BASE":
2179 rev = pysvn.Revision(pysvn.opt_revision_kind.base)
2180 elif rev == "WORKING":
2181 rev = pysvn.Revision(pysvn.opt_revision_kind.working)
2182 elif rev == "PREV":
2183 rev = pysvn.Revision(pysvn.opt_revision_kind.previous)
2184 else:
2185 rev = pysvn.Revision(pysvn.opt_revision_kind.unspecified)
2186 output = client.cat(name, revision=rev)
2187 else:
2188 output = client.cat(name)
2189 output = output.decode('utf-8')
2190 except pysvn.ClientError as e:
2191 error = str(e)
2192
2193 return output, error
2194
2195 def vcsSbsDiff(self, name, extended=False, revisions=None):
2196 """
2197 Public method used to view the difference of a file to the Mercurial
2198 repository side-by-side.
2199
2200 @param name file name to be diffed (string)
2201 @param extended flag indicating the extended variant (boolean)
2202 @param revisions tuple of two revisions (tuple of strings)
2203 @exception ValueError raised to indicate an invalid name parameter type
2204 """
2205 if isinstance(name, list):
2206 raise ValueError("Wrong parameter type")
2207
2208 if extended:
2209 from .SvnRevisionSelectionDialog import SvnRevisionSelectionDialog
2210 dlg = SvnRevisionSelectionDialog()
2211 if dlg.exec() == QDialog.DialogCode.Accepted:
2212 rev1, rev2 = dlg.getRevisions()
2213 if rev1 == "WORKING":
2214 rev1 = ""
2215 if rev2 == "WORKING":
2216 rev2 = ""
2217 else:
2218 return
2219 elif revisions:
2220 rev1, rev2 = revisions[0], revisions[1]
2221 else:
2222 rev1, rev2 = "", ""
2223
2224 output1, error = self.__svnGetFileForRevision(name, rev=rev1)
2225 if error:
2226 EricMessageBox.critical(
2227 self.__ui,
2228 self.tr("Subversion Side-by-Side Difference"),
2229 error)
2230 return
2231 name1 = "{0} (rev. {1})".format(name, rev1 and rev1 or ".")
2232
2233 if rev2:
2234 output2, error = self.__svnGetFileForRevision(name, rev=rev2)
2235 if error:
2236 EricMessageBox.critical(
2237 self.__ui,
2238 self.tr("Subversion Side-by-Side Difference"),
2239 error)
2240 return
2241 name2 = "{0} (rev. {1})".format(name, rev2)
2242 else:
2243 try:
2244 with open(name, "r", encoding="utf-8") as f1:
2245 output2 = f1.read()
2246 name2 = name
2247 except OSError:
2248 EricMessageBox.critical(
2249 self.__ui,
2250 self.tr("Subversion Side-by-Side Difference"),
2251 self.tr(
2252 """<p>The file <b>{0}</b> could not be read.</p>""")
2253 .format(name))
2254 return
2255
2256 if self.sbsDiff is None:
2257 from UI.CompareDialog import CompareDialog
2258 self.sbsDiff = CompareDialog()
2259 self.sbsDiff.show()
2260 self.sbsDiff.raise_()
2261 self.sbsDiff.compare(output1, output2, name1, name2)
2262
2263 def vcsLogBrowser(self, name, isFile=False):
2264 """
2265 Public method used to browse the log of a file/directory from the
2266 Subversion repository.
2267
2268 @param name file/directory name to show the log of (string)
2269 @param isFile flag indicating log for a file is to be shown (boolean)
2270 """
2271 if self.logBrowser is None:
2272 from .SvnLogBrowserDialog import SvnLogBrowserDialog
2273 self.logBrowser = SvnLogBrowserDialog(self)
2274 self.logBrowser.show()
2275 self.logBrowser.raise_()
2276 QApplication.processEvents()
2277 self.logBrowser.start(name, isFile=isFile)
2278
2279 def svnLock(self, name, stealIt=False, parent=None):
2280 """
2281 Public method used to lock a file in the Subversion repository.
2282
2283 @param name file/directory name to be locked (string or list of
2284 strings)
2285 @param stealIt flag indicating a forced operation (boolean)
2286 @param parent reference to the parent object of the subversion dialog
2287 (QWidget)
2288 """
2289 comment, ok = QInputDialog.getText(
2290 None,
2291 self.tr("Subversion Lock"),
2292 self.tr("Enter lock comment"),
2293 QLineEdit.EchoMode.Normal)
2294
2295 if not ok:
2296 return
2297
2298 if isinstance(name, list):
2299 dname, fnames = self.splitPathList(name)
2300 else:
2301 dname, fname = self.splitPath(name)
2302 fnames = [fname]
2303
2304 cwd = os.getcwd()
2305 os.chdir(dname)
2306 client = self.getClient()
2307 dlg = SvnDialog(
2308 self.tr('Locking in the Subversion repository'),
2309 "lock{0}{1} {2}".format(
2310 stealIt and " --force" or "",
2311 comment and (" --message {0}".format(comment)) or "",
2312 " ".join(fnames)),
2313 client, parent=parent)
2314 QApplication.processEvents()
2315 try:
2316 with EricMutexLocker(self.vcsExecutionMutex):
2317 client.lock(fnames, comment, force=stealIt)
2318 except pysvn.ClientError as e:
2319 dlg.showError(e.args[0])
2320 except AttributeError as e:
2321 dlg.showError(str(e))
2322 dlg.finish()
2323 dlg.exec()
2324 os.chdir(cwd)
2325
2326 def svnUnlock(self, name, breakIt=False, parent=None):
2327 """
2328 Public method used to unlock a file in the Subversion repository.
2329
2330 @param name file/directory name to be unlocked (string or list of
2331 strings)
2332 @param breakIt flag indicating a forced operation (boolean)
2333 @param parent reference to the parent object of the subversion dialog
2334 (QWidget)
2335 """
2336 if isinstance(name, list):
2337 dname, fnames = self.splitPathList(name)
2338 else:
2339 dname, fname = self.splitPath(name)
2340 fnames = [fname]
2341
2342 cwd = os.getcwd()
2343 os.chdir(dname)
2344 client = self.getClient()
2345 dlg = SvnDialog(
2346 self.tr('Unlocking in the Subversion repository'),
2347 "unlock{0} {1}".format(breakIt and " --force" or "",
2348 " ".join(fnames)),
2349 client, parent=parent)
2350 QApplication.processEvents()
2351 try:
2352 with EricMutexLocker(self.vcsExecutionMutex):
2353 client.unlock(fnames, force=breakIt)
2354 except pysvn.ClientError as e:
2355 dlg.showError(e.args[0])
2356 except AttributeError as e:
2357 dlg.showError(str(e))
2358 dlg.finish()
2359 dlg.exec()
2360 os.chdir(cwd)
2361
2362 def svnInfo(self, projectPath, name):
2363 """
2364 Public method to show repository information about a file or directory.
2365
2366 @param projectPath path name of the project (string)
2367 @param name file/directory name relative to the project (string)
2368 """
2369 from .SvnInfoDialog import SvnInfoDialog
2370 dlg = SvnInfoDialog(self)
2371 dlg.start(projectPath, name)
2372 dlg.exec()
2373
2374 def svnRelocate(self, projectPath):
2375 """
2376 Public method to relocate the working copy to a new repository URL.
2377
2378 @param projectPath path name of the project (string)
2379 """
2380 from .SvnRelocateDialog import SvnRelocateDialog
2381 currUrl = self.svnGetReposName(projectPath)
2382 dlg = SvnRelocateDialog(currUrl)
2383 if dlg.exec() == QDialog.DialogCode.Accepted:
2384 newUrl, inside = dlg.getData()
2385 if inside:
2386 msg = "switch {0} {1}".format(newUrl, projectPath)
2387 else:
2388 msg = "relocate {0} {1} {2}".format(currUrl, newUrl,
2389 projectPath)
2390 client = self.getClient()
2391 dlg = SvnDialog(self.tr('Relocating'), msg, client)
2392 QApplication.processEvents()
2393 try:
2394 with EricMutexLocker(self.vcsExecutionMutex):
2395 if inside:
2396 client.switch(projectPath, newUrl)
2397 else:
2398 client.relocate(currUrl, newUrl, projectPath,
2399 recurse=True)
2400 except pysvn.ClientError as e:
2401 dlg.showError(e.args[0])
2402 dlg.finish()
2403 dlg.exec()
2404
2405 def svnRepoBrowser(self, projectPath=None):
2406 """
2407 Public method to open the repository browser.
2408
2409 @param projectPath path name of the project (string)
2410 """
2411 url = self.svnGetReposName(projectPath) if projectPath else None
2412 if url is None:
2413 url, ok = QInputDialog.getText(
2414 None,
2415 self.tr("Repository Browser"),
2416 self.tr("Enter the repository URL."),
2417 QLineEdit.EchoMode.Normal)
2418 if not ok or not url:
2419 return
2420
2421 if self.repoBrowser is None:
2422 from .SvnRepoBrowserDialog import SvnRepoBrowserDialog
2423 self.repoBrowser = SvnRepoBrowserDialog(self)
2424 self.repoBrowser.start(url)
2425 self.repoBrowser.show()
2426 self.repoBrowser.raise_()
2427
2428 def svnRemoveFromChangelist(self, names):
2429 """
2430 Public method to remove a file or directory from its changelist.
2431
2432 Note: Directories will be removed recursively.
2433
2434 @param names name or list of names of file or directory to remove
2435 (string)
2436 """
2437 if not isinstance(names, list):
2438 names = [names]
2439 client = self.getClient()
2440 dlg = SvnDialog(
2441 self.tr('Remove from changelist'),
2442 "changelist --remove {0}".format(" ".join(names)),
2443 client)
2444 QApplication.processEvents()
2445 try:
2446 with EricMutexLocker(self.vcsExecutionMutex):
2447 for name in names:
2448 client.remove_from_changelists(name)
2449 except pysvn.ClientError as e:
2450 dlg.showError(e.args[0])
2451 dlg.finish()
2452 dlg.exec()
2453
2454 def svnAddToChangelist(self, names):
2455 """
2456 Public method to add a file or directory to a changelist.
2457
2458 Note: Directories will be added recursively.
2459
2460 @param names name or list of names of file or directory to add
2461 (string)
2462 """
2463 if not isinstance(names, list):
2464 names = [names]
2465
2466 clname, ok = QInputDialog.getItem(
2467 None,
2468 self.tr("Add to changelist"),
2469 self.tr("Enter name of the changelist:"),
2470 sorted(self.svnGetChangelists()),
2471 0, True)
2472 if not ok or not clname:
2473 return
2474
2475 client = self.getClient()
2476 dlg = SvnDialog(
2477 self.tr('Add to changelist'),
2478 "changelist {0}".format(" ".join(names)),
2479 client)
2480 QApplication.processEvents()
2481 try:
2482 with EricMutexLocker(self.vcsExecutionMutex):
2483 for name in names:
2484 client.add_to_changelist(
2485 name, clname, depth=pysvn.depth.infinity)
2486 except pysvn.ClientError as e:
2487 dlg.showError(e.args[0])
2488 dlg.finish()
2489 dlg.exec()
2490
2491 def svnShowChangelists(self, path):
2492 """
2493 Public method used to inspect the change lists defined for the project.
2494
2495 @param path directory name to show change lists for (string)
2496 """
2497 from .SvnChangeListsDialog import SvnChangeListsDialog
2498 self.changeLists = SvnChangeListsDialog(self)
2499 self.changeLists.show()
2500 QApplication.processEvents()
2501 self.changeLists.start(path)
2502
2503 def svnGetChangelists(self):
2504 """
2505 Public method to get a list of all defined change lists.
2506
2507 @return list of defined change list names (list of strings)
2508 """
2509 changelists = []
2510 client = self.getClient()
2511 if hasattr(client, 'get_changelist'):
2512 ppath = ericApp().getObject("Project").getProjectPath()
2513 with contextlib.suppress(pysvn.ClientError):
2514 with EricMutexLocker(self.vcsExecutionMutex):
2515 entries = client.get_changelist(
2516 ppath, depth=pysvn.depth.infinity)
2517 for entry in entries:
2518 changelist = entry[1]
2519 if changelist not in changelists:
2520 changelists.append(changelist)
2521
2522 return changelists
2523
2524 def svnUpgrade(self, path):
2525 """
2526 Public method to upgrade the working copy format.
2527
2528 @param path directory name to show change lists for (string)
2529 """
2530 client = self.getClient()
2531 dlg = SvnDialog(
2532 self.tr('Upgrade'),
2533 "upgrade {0}".format(path),
2534 client)
2535 QApplication.processEvents()
2536 try:
2537 with EricMutexLocker(self.vcsExecutionMutex):
2538 client.upgrade(path)
2539 except pysvn.ClientError as e:
2540 dlg.showError(e.args[0])
2541 dlg.finish()
2542 dlg.exec()
2543
2544 ###########################################################################
2545 ## Private Subversion specific methods are below.
2546 ###########################################################################
2547
2548 def __svnURL(self, url):
2549 """
2550 Private method to format a url for subversion.
2551
2552 @param url unformatted url string (string)
2553 @return properly formated url for subversion (string)
2554 """
2555 url = self.svnNormalizeURL(url)
2556 url = url.split(':', 2)
2557 if len(url) == 3:
2558 scheme = url[0]
2559 host = url[1]
2560 port, path = url[2].split("/", 1)
2561 return "{0}:{1}:{2}/{3}".format(scheme, host, port, quote(path))
2562 else:
2563 scheme = url[0]
2564 if scheme == "file":
2565 return "{0}:{1}".format(scheme, quote(url[1]))
2566 else:
2567 try:
2568 host, path = url[1][2:].split("/", 1)
2569 except ValueError:
2570 host = url[1][2:]
2571 path = ""
2572 return "{0}://{1}/{2}".format(scheme, host, quote(path))
2573
2574 def svnNormalizeURL(self, url):
2575 """
2576 Public method to normalize a url for subversion.
2577
2578 @param url url string (string)
2579 @return properly normalized url for subversion (string)
2580 """
2581 protocol, url = url.split("://", 1)
2582 if url.startswith("\\\\"):
2583 url = url[2:]
2584 if protocol == "file":
2585 url = os.path.normcase(url)
2586 if url[1] == ":":
2587 url = url.replace(":", "|", 1)
2588 url = url.replace('\\', '/')
2589 if url.endswith('/'):
2590 url = url[:-1]
2591 if not url.startswith("/") and url[1] in [":", "|"]:
2592 url = "/{0}".format(url)
2593 return "{0}://{1}".format(protocol, url)
2594
2595 ###########################################################################
2596 ## Methods to get the helper objects are below.
2597 ###########################################################################
2598
2599 def vcsGetProjectBrowserHelper(self, browser, project,
2600 isTranslationsBrowser=False):
2601 """
2602 Public method to instanciate a helper object for the different
2603 project browsers.
2604
2605 @param browser reference to the project browser object
2606 @param project reference to the project object
2607 @param isTranslationsBrowser flag indicating, the helper is requested
2608 for the translations browser (this needs some special treatment)
2609 @return the project browser helper object
2610 """
2611 from .ProjectBrowserHelper import SvnProjectBrowserHelper
2612 return SvnProjectBrowserHelper(self, browser, project,
2613 isTranslationsBrowser)
2614
2615 def vcsGetProjectHelper(self, project):
2616 """
2617 Public method to instanciate a helper object for the project.
2618
2619 @param project reference to the project object
2620 @return the project helper object
2621 """
2622 helper = self.__plugin.getProjectHelper()
2623 helper.setObjects(self, project)
2624 self.__wcng = (
2625 os.path.exists(
2626 os.path.join(project.getProjectPath(), ".svn", "format")) or
2627 os.path.exists(
2628 os.path.join(project.getProjectPath(), "_svn", "format")) or
2629 os.path.exists(
2630 os.path.join(project.getProjectPath(), ".svn", "wc.db")) or
2631 os.path.exists(
2632 os.path.join(project.getProjectPath(), "_svn", "wc.db"))
2633 )
2634 return helper
2635
2636 ###########################################################################
2637 ## Status Monitor Thread methods
2638 ###########################################################################
2639
2640 def _createStatusMonitorThread(self, interval, project):
2641 """
2642 Protected method to create an instance of the VCS status monitor
2643 thread.
2644
2645 @param interval check interval for the monitor thread in seconds
2646 (integer)
2647 @param project reference to the project object
2648 @return reference to the monitor thread (QThread)
2649 """
2650 from .SvnStatusMonitorThread import SvnStatusMonitorThread
2651 return SvnStatusMonitorThread(interval, project, self)

eric ide

mercurial