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