|
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) |