|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2003 - 2021 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 PyQt5.QtCore import pyqtSignal, QProcess, QCoreApplication |
|
16 from PyQt5.QtWidgets import QLineEdit, QDialog, QInputDialog, QApplication |
|
17 |
|
18 from E5Gui.E5Application import e5App |
|
19 from E5Gui import E5MessageBox |
|
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 E5MessageBox.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 E5MessageBox.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 = e5App().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 = e5App().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 = E5MessageBox.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=E5MessageBox.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 vcsUpdate(self, name, noDialog=False): |
|
575 """ |
|
576 Public method used to update a file/directory with the Subversion |
|
577 repository. |
|
578 |
|
579 @param name file/directory name to be updated (string or list of |
|
580 strings) |
|
581 @param noDialog flag indicating quiet operations (boolean) |
|
582 @return flag indicating, that the update contained an add |
|
583 or delete (boolean) |
|
584 """ |
|
585 args = [] |
|
586 args.append('update') |
|
587 self.addArguments(args, self.options['global']) |
|
588 self.addArguments(args, self.options['update']) |
|
589 if self.version >= (1, 5, 0): |
|
590 args.append('--accept') |
|
591 args.append('postpone') |
|
592 if isinstance(name, list): |
|
593 dname, fnames = self.splitPathList(name) |
|
594 self.addArguments(args, fnames) |
|
595 else: |
|
596 dname, fname = self.splitPath(name) |
|
597 args.append(fname) |
|
598 |
|
599 if noDialog: |
|
600 self.startSynchronizedProcess(QProcess(), "svn", args, dname) |
|
601 res = False |
|
602 else: |
|
603 dia = SvnDialog( |
|
604 self.tr('Synchronizing with the Subversion repository')) |
|
605 res = dia.startProcess(args, dname, True) |
|
606 if res: |
|
607 dia.exec() |
|
608 res = dia.hasAddOrDelete() |
|
609 self.checkVCSStatus() |
|
610 return res |
|
611 |
|
612 def vcsAdd(self, name, isDir=False, noDialog=False): |
|
613 """ |
|
614 Public method used to add a file/directory to the Subversion |
|
615 repository. |
|
616 |
|
617 @param name file/directory name to be added (string) |
|
618 @param isDir flag indicating name is a directory (boolean) |
|
619 @param noDialog flag indicating quiet operations |
|
620 """ |
|
621 args = [] |
|
622 args.append('add') |
|
623 self.addArguments(args, self.options['global']) |
|
624 self.addArguments(args, self.options['add']) |
|
625 args.append('--non-recursive') |
|
626 if noDialog and '--force' not in args: |
|
627 args.append('--force') |
|
628 |
|
629 if isinstance(name, list): |
|
630 if isDir: |
|
631 dname, fname = os.path.split(name[0]) |
|
632 else: |
|
633 dname, fnames = self.splitPathList(name) |
|
634 else: |
|
635 if isDir: |
|
636 dname, fname = os.path.split(name) |
|
637 else: |
|
638 dname, fname = self.splitPath(name) |
|
639 tree = [] |
|
640 wdir = dname |
|
641 if self.__wcng: |
|
642 repodir = dname |
|
643 while not os.path.isdir(os.path.join(repodir, self.adminDir)): |
|
644 repodir = os.path.dirname(repodir) |
|
645 if os.path.splitdrive(repodir)[1] == os.sep: |
|
646 return # oops, project is not version controlled |
|
647 while ( |
|
648 os.path.normcase(dname) != os.path.normcase(repodir) and |
|
649 (os.path.normcase(dname) not in self.statusCache or |
|
650 self.statusCache[os.path.normcase(dname)] == |
|
651 self.canBeAdded) |
|
652 ): |
|
653 # add directories recursively, if they aren't in the |
|
654 # repository already |
|
655 tree.insert(-1, dname) |
|
656 dname = os.path.dirname(dname) |
|
657 wdir = dname |
|
658 else: |
|
659 while not os.path.exists(os.path.join(dname, self.adminDir)): |
|
660 # add directories recursively, if they aren't in the |
|
661 # repository already |
|
662 tree.insert(-1, dname) |
|
663 dname = os.path.dirname(dname) |
|
664 wdir = dname |
|
665 self.addArguments(args, tree) |
|
666 |
|
667 if isinstance(name, list): |
|
668 tree2 = [] |
|
669 for n in name: |
|
670 d = os.path.dirname(n) |
|
671 if self.__wcng: |
|
672 repodir = d |
|
673 while not os.path.isdir( |
|
674 os.path.join(repodir, self.adminDir)): |
|
675 repodir = os.path.dirname(repodir) |
|
676 if os.path.splitdrive(repodir)[1] == os.sep: |
|
677 return # oops, project is not version controlled |
|
678 while ( |
|
679 os.path.normcase(d) != os.path.normcase(repodir) and |
|
680 (d not in tree2 + tree) and |
|
681 (os.path.normcase(d) not in self.statusCache or |
|
682 self.statusCache[os.path.normcase(d)] == |
|
683 self.canBeAdded) |
|
684 ): |
|
685 tree2.append(d) |
|
686 d = os.path.dirname(d) |
|
687 else: |
|
688 while not os.path.exists(os.path.join(d, self.adminDir)): |
|
689 if d in tree2 + tree: |
|
690 break |
|
691 tree2.append(d) |
|
692 d = os.path.dirname(d) |
|
693 tree2.reverse() |
|
694 self.addArguments(args, tree2) |
|
695 self.addArguments(args, name) |
|
696 else: |
|
697 args.append(name) |
|
698 |
|
699 if noDialog: |
|
700 self.startSynchronizedProcess(QProcess(), "svn", args, wdir) |
|
701 else: |
|
702 dia = SvnDialog( |
|
703 self.tr('Adding files/directories to the Subversion' |
|
704 ' repository')) |
|
705 res = dia.startProcess(args, wdir) |
|
706 if res: |
|
707 dia.exec() |
|
708 |
|
709 def vcsAddBinary(self, name, isDir=False): |
|
710 """ |
|
711 Public method used to add a file/directory in binary mode to the |
|
712 Subversion repository. |
|
713 |
|
714 @param name file/directory name to be added (string) |
|
715 @param isDir flag indicating name is a directory (boolean) |
|
716 """ |
|
717 self.vcsAdd(name, isDir) |
|
718 |
|
719 def vcsAddTree(self, path): |
|
720 """ |
|
721 Public method to add a directory tree rooted at path to the Subversion |
|
722 repository. |
|
723 |
|
724 @param path root directory of the tree to be added (string or list of |
|
725 strings)) |
|
726 """ |
|
727 args = [] |
|
728 args.append('add') |
|
729 self.addArguments(args, self.options['global']) |
|
730 self.addArguments(args, self.options['add']) |
|
731 |
|
732 tree = [] |
|
733 if isinstance(path, list): |
|
734 dname, fnames = self.splitPathList(path) |
|
735 for n in path: |
|
736 d = os.path.dirname(n) |
|
737 if self.__wcng: |
|
738 repodir = d |
|
739 while not os.path.isdir( |
|
740 os.path.join(repodir, self.adminDir)): |
|
741 repodir = os.path.dirname(repodir) |
|
742 if os.path.splitdrive(repodir)[1] == os.sep: |
|
743 return # oops, project is not version controlled |
|
744 while ( |
|
745 os.path.normcase(d) != os.path.normcase(repodir) and |
|
746 (d not in tree) and |
|
747 (os.path.normcase(d) not in self.statusCache or |
|
748 self.statusCache[os.path.normcase(d)] == |
|
749 self.canBeAdded) |
|
750 ): |
|
751 tree.append(d) |
|
752 d = os.path.dirname(d) |
|
753 else: |
|
754 while not os.path.exists(os.path.join(d, self.adminDir)): |
|
755 # add directories recursively, |
|
756 # if they aren't in the repository already |
|
757 if d in tree: |
|
758 break |
|
759 tree.append(d) |
|
760 d = os.path.dirname(d) |
|
761 tree.reverse() |
|
762 else: |
|
763 dname, fname = os.path.split(path) |
|
764 if self.__wcng: |
|
765 repodir = dname |
|
766 while not os.path.isdir(os.path.join(repodir, self.adminDir)): |
|
767 repodir = os.path.dirname(repodir) |
|
768 if os.path.splitdrive(repodir)[1] == os.sep: |
|
769 return # oops, project is not version controlled |
|
770 while ( |
|
771 os.path.normcase(dname) != os.path.normcase(repodir) and |
|
772 (os.path.normcase(dname) not in self.statusCache or |
|
773 self.statusCache[os.path.normcase(dname)] == |
|
774 self.canBeAdded) |
|
775 ): |
|
776 # add directories recursively, if they aren't in the |
|
777 # repository already |
|
778 tree.insert(-1, dname) |
|
779 dname = os.path.dirname(dname) |
|
780 else: |
|
781 while not os.path.exists(os.path.join(dname, self.adminDir)): |
|
782 # add directories recursively, |
|
783 # if they aren't in the repository already |
|
784 tree.insert(-1, dname) |
|
785 dname = os.path.dirname(dname) |
|
786 if tree: |
|
787 self.vcsAdd(tree, True) |
|
788 |
|
789 if isinstance(path, list): |
|
790 self.addArguments(args, path) |
|
791 else: |
|
792 args.append(path) |
|
793 |
|
794 dia = SvnDialog( |
|
795 self.tr('Adding directory trees to the Subversion repository')) |
|
796 res = dia.startProcess(args, dname) |
|
797 if res: |
|
798 dia.exec() |
|
799 |
|
800 def vcsRemove(self, name, project=False, noDialog=False): |
|
801 """ |
|
802 Public method used to remove a file/directory from the Subversion |
|
803 repository. |
|
804 |
|
805 The default operation is to remove the local copy as well. |
|
806 |
|
807 @param name file/directory name to be removed (string or list of |
|
808 strings)) |
|
809 @param project flag indicating deletion of a project tree (boolean) |
|
810 (not needed) |
|
811 @param noDialog flag indicating quiet operations |
|
812 @return flag indicating successfull operation (boolean) |
|
813 """ |
|
814 args = [] |
|
815 args.append('delete') |
|
816 self.addArguments(args, self.options['global']) |
|
817 self.addArguments(args, self.options['remove']) |
|
818 if noDialog and '--force' not in args: |
|
819 args.append('--force') |
|
820 |
|
821 if isinstance(name, list): |
|
822 self.addArguments(args, name) |
|
823 else: |
|
824 args.append(name) |
|
825 |
|
826 if noDialog: |
|
827 res = self.startSynchronizedProcess(QProcess(), "svn", args) |
|
828 else: |
|
829 dia = SvnDialog( |
|
830 self.tr('Removing files/directories from the Subversion' |
|
831 ' repository')) |
|
832 res = dia.startProcess(args) |
|
833 if res: |
|
834 dia.exec() |
|
835 res = dia.normalExit() |
|
836 |
|
837 return res |
|
838 |
|
839 def vcsMove(self, name, project, target=None, noDialog=False): |
|
840 """ |
|
841 Public method used to move a file/directory. |
|
842 |
|
843 @param name file/directory name to be moved (string) |
|
844 @param project reference to the project object |
|
845 @param target new name of the file/directory (string) |
|
846 @param noDialog flag indicating quiet operations |
|
847 @return flag indicating successfull operation (boolean) |
|
848 """ |
|
849 rx_prot = re.compile('(file:|svn:|svn+ssh:|http:|https:).+') |
|
850 opts = self.options['global'][:] |
|
851 force = '--force' in opts |
|
852 if force: |
|
853 del opts[opts.index('--force')] |
|
854 |
|
855 res = False |
|
856 if noDialog: |
|
857 if target is None: |
|
858 return False |
|
859 force = True |
|
860 accepted = True |
|
861 else: |
|
862 from .SvnCopyDialog import SvnCopyDialog |
|
863 dlg = SvnCopyDialog(name, None, True, force) |
|
864 accepted = (dlg.exec() == QDialog.DialogCode.Accepted) |
|
865 if accepted: |
|
866 target, force = dlg.getData() |
|
867 if not target: |
|
868 return False |
|
869 |
|
870 isDir = (os.path.isdir(name) if rx_prot.fullmatch(target) is None |
|
871 else False) |
|
872 |
|
873 if accepted: |
|
874 args = [] |
|
875 args.append('move') |
|
876 self.addArguments(args, opts) |
|
877 if force: |
|
878 args.append('--force') |
|
879 if rx_prot.fullmatch(target) is not None: |
|
880 args.append('--message') |
|
881 args.append('Moving {0} to {1}'.format(name, target)) |
|
882 target = self.__svnURL(target) |
|
883 args.append(name) |
|
884 args.append(target) |
|
885 |
|
886 if noDialog: |
|
887 res = self.startSynchronizedProcess(QProcess(), "svn", args) |
|
888 else: |
|
889 dia = SvnDialog(self.tr('Moving {0}') |
|
890 .format(name)) |
|
891 res = dia.startProcess(args) |
|
892 if res: |
|
893 dia.exec() |
|
894 res = dia.normalExit() |
|
895 if res and rx_prot.fullmatch(target) is None: |
|
896 if target.startswith(project.getProjectPath()): |
|
897 if isDir: |
|
898 project.moveDirectory(name, target) |
|
899 else: |
|
900 project.renameFileInPdata(name, target) |
|
901 else: |
|
902 if isDir: |
|
903 project.removeDirectory(name) |
|
904 else: |
|
905 project.removeFile(name) |
|
906 return res |
|
907 |
|
908 def vcsDiff(self, name): |
|
909 """ |
|
910 Public method used to view the difference of a file/directory to the |
|
911 Subversion repository. |
|
912 |
|
913 If name is a directory and is the project directory, all project files |
|
914 are saved first. If name is a file (or list of files), which is/are |
|
915 being edited and has unsaved modification, they can be saved or the |
|
916 operation may be aborted. |
|
917 |
|
918 @param name file/directory name to be diffed (string) |
|
919 """ |
|
920 names = name[:] if isinstance(name, list) else [name] |
|
921 for nam in names: |
|
922 if os.path.isfile(nam): |
|
923 editor = e5App().getObject("ViewManager").getOpenEditor(nam) |
|
924 if editor and not editor.checkDirty(): |
|
925 return |
|
926 else: |
|
927 project = e5App().getObject("Project") |
|
928 if nam == project.ppath and not project.saveAllScripts(): |
|
929 return |
|
930 if self.diff is None: |
|
931 from .SvnDiffDialog import SvnDiffDialog |
|
932 self.diff = SvnDiffDialog(self) |
|
933 self.diff.show() |
|
934 self.diff.raise_() |
|
935 QApplication.processEvents() |
|
936 self.diff.start(name, refreshable=True) |
|
937 |
|
938 def vcsStatus(self, name): |
|
939 """ |
|
940 Public method used to view the status of files/directories in the |
|
941 Subversion repository. |
|
942 |
|
943 @param name file/directory name(s) to show the status of |
|
944 (string or list of strings) |
|
945 """ |
|
946 if self.status is None: |
|
947 from .SvnStatusDialog import SvnStatusDialog |
|
948 self.status = SvnStatusDialog(self) |
|
949 self.status.show() |
|
950 self.status.raise_() |
|
951 self.status.start(name) |
|
952 |
|
953 def vcsTag(self, name): |
|
954 """ |
|
955 Public method used to set the tag of a file/directory in the |
|
956 Subversion repository. |
|
957 |
|
958 @param name file/directory name to be tagged (string) |
|
959 """ |
|
960 dname, fname = self.splitPath(name) |
|
961 |
|
962 reposURL = self.svnGetReposName(dname) |
|
963 if reposURL is None: |
|
964 E5MessageBox.critical( |
|
965 self.__ui, |
|
966 self.tr("Subversion Error"), |
|
967 self.tr( |
|
968 """The URL of the project repository could not be""" |
|
969 """ retrieved from the working copy. The tag operation""" |
|
970 """ will be aborted""")) |
|
971 return |
|
972 |
|
973 url = ( |
|
974 None |
|
975 if self.otherData["standardLayout"] else |
|
976 self.svnNormalizeURL(reposURL) |
|
977 ) |
|
978 from .SvnTagDialog import SvnTagDialog |
|
979 dlg = SvnTagDialog(self.allTagsBranchesList, url, |
|
980 self.otherData["standardLayout"]) |
|
981 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
982 tag, tagOp = dlg.getParameters() |
|
983 if tag in self.allTagsBranchesList: |
|
984 self.allTagsBranchesList.remove(tag) |
|
985 self.allTagsBranchesList.insert(0, tag) |
|
986 else: |
|
987 return |
|
988 |
|
989 if self.otherData["standardLayout"]: |
|
990 rx_base = re.compile('(.+)/(trunk|tags|branches).*') |
|
991 match = rx_base.fullmatch(reposURL) |
|
992 if match is None: |
|
993 E5MessageBox.critical( |
|
994 self.__ui, |
|
995 self.tr("Subversion Error"), |
|
996 self.tr( |
|
997 """The URL of the project repository has an""" |
|
998 """ invalid format. The tag operation will""" |
|
999 """ be aborted""")) |
|
1000 return |
|
1001 |
|
1002 reposRoot = match.group(1) |
|
1003 if tagOp in [1, 4]: |
|
1004 url = '{0}/tags/{1}'.format(reposRoot, quote(tag)) |
|
1005 elif tagOp in [2, 8]: |
|
1006 url = '{0}/branches/{1}'.format(reposRoot, quote(tag)) |
|
1007 else: |
|
1008 url = self.__svnURL(tag) |
|
1009 |
|
1010 args = [] |
|
1011 if tagOp in [1, 2]: |
|
1012 args.append('copy') |
|
1013 self.addArguments(args, self.options['global']) |
|
1014 self.addArguments(args, self.options['tag']) |
|
1015 args.append('--message') |
|
1016 args.append('Created tag <{0}>'.format(tag)) |
|
1017 args.append(reposURL) |
|
1018 args.append(url) |
|
1019 else: |
|
1020 args.append('delete') |
|
1021 self.addArguments(args, self.options['global']) |
|
1022 self.addArguments(args, self.options['tag']) |
|
1023 args.append('--message') |
|
1024 args.append('Deleted tag <{0}>'.format(tag)) |
|
1025 args.append(url) |
|
1026 |
|
1027 dia = SvnDialog(self.tr('Tagging {0} in the Subversion repository') |
|
1028 .format(name)) |
|
1029 res = dia.startProcess(args) |
|
1030 if res: |
|
1031 dia.exec() |
|
1032 |
|
1033 def vcsRevert(self, name): |
|
1034 """ |
|
1035 Public method used to revert changes made to a file/directory. |
|
1036 |
|
1037 @param name file/directory name to be reverted (string) |
|
1038 """ |
|
1039 args = [] |
|
1040 args.append('revert') |
|
1041 self.addArguments(args, self.options['global']) |
|
1042 if isinstance(name, list): |
|
1043 self.addArguments(args, name) |
|
1044 names = name[:] |
|
1045 else: |
|
1046 if os.path.isdir(name): |
|
1047 args.append('--recursive') |
|
1048 args.append(name) |
|
1049 names = [name] |
|
1050 |
|
1051 project = e5App().getObject("Project") |
|
1052 names = [project.getRelativePath(nam) for nam in names] |
|
1053 if names[0]: |
|
1054 from UI.DeleteFilesConfirmationDialog import ( |
|
1055 DeleteFilesConfirmationDialog |
|
1056 ) |
|
1057 dlg = DeleteFilesConfirmationDialog( |
|
1058 self.parent(), |
|
1059 self.tr("Revert changes"), |
|
1060 self.tr("Do you really want to revert all changes to" |
|
1061 " these files or directories?"), |
|
1062 names) |
|
1063 yes = dlg.exec() == QDialog.DialogCode.Accepted |
|
1064 else: |
|
1065 yes = E5MessageBox.yesNo( |
|
1066 None, |
|
1067 self.tr("Revert changes"), |
|
1068 self.tr("""Do you really want to revert all changes of""" |
|
1069 """ the project?""")) |
|
1070 if yes: |
|
1071 dia = SvnDialog(self.tr('Reverting changes')) |
|
1072 res = dia.startProcess(args) |
|
1073 if res: |
|
1074 dia.exec() |
|
1075 self.checkVCSStatus() |
|
1076 |
|
1077 def vcsSwitch(self, name): |
|
1078 """ |
|
1079 Public method used to switch a directory to a different tag/branch. |
|
1080 |
|
1081 @param name directory name to be switched (string) |
|
1082 @return flag indicating added or changed files (boolean) |
|
1083 """ |
|
1084 dname, fname = self.splitPath(name) |
|
1085 |
|
1086 reposURL = self.svnGetReposName(dname) |
|
1087 if reposURL is None: |
|
1088 E5MessageBox.critical( |
|
1089 self.__ui, |
|
1090 self.tr("Subversion Error"), |
|
1091 self.tr( |
|
1092 """The URL of the project repository could not be""" |
|
1093 """ retrieved from the working copy. The switch""" |
|
1094 """ operation will be aborted""")) |
|
1095 return False |
|
1096 |
|
1097 url = ( |
|
1098 None |
|
1099 if self.otherData["standardLayout"] else |
|
1100 self.svnNormalizeURL(reposURL) |
|
1101 ) |
|
1102 from .SvnSwitchDialog import SvnSwitchDialog |
|
1103 dlg = SvnSwitchDialog(self.allTagsBranchesList, url, |
|
1104 self.otherData["standardLayout"]) |
|
1105 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
1106 tag, tagType = dlg.getParameters() |
|
1107 if tag in self.allTagsBranchesList: |
|
1108 self.allTagsBranchesList.remove(tag) |
|
1109 self.allTagsBranchesList.insert(0, tag) |
|
1110 else: |
|
1111 return False |
|
1112 |
|
1113 if self.otherData["standardLayout"]: |
|
1114 rx_base = re.compile('(.+)/(trunk|tags|branches).*') |
|
1115 match = rx_base.fullmatch(reposURL) |
|
1116 if match is None: |
|
1117 E5MessageBox.critical( |
|
1118 self.__ui, |
|
1119 self.tr("Subversion Error"), |
|
1120 self.tr( |
|
1121 """The URL of the project repository has an""" |
|
1122 """ invalid format. The switch operation will""" |
|
1123 """ be aborted""")) |
|
1124 return False |
|
1125 |
|
1126 reposRoot = match.group(1) |
|
1127 tn = tag |
|
1128 if tagType == 1: |
|
1129 url = '{0}/tags/{1}'.format(reposRoot, quote(tag)) |
|
1130 elif tagType == 2: |
|
1131 url = '{0}/branches/{1}'.format(reposRoot, quote(tag)) |
|
1132 elif tagType == 4: |
|
1133 url = '{0}/trunk'.format(reposRoot) |
|
1134 tn = 'HEAD' |
|
1135 else: |
|
1136 url = self.__svnURL(tag) |
|
1137 tn = url |
|
1138 |
|
1139 args = [] |
|
1140 args.append('switch') |
|
1141 if self.version >= (1, 5, 0): |
|
1142 args.append('--accept') |
|
1143 args.append('postpone') |
|
1144 args.append(url) |
|
1145 args.append(name) |
|
1146 |
|
1147 dia = SvnDialog(self.tr('Switching to {0}') |
|
1148 .format(tn)) |
|
1149 res = dia.startProcess(args, setLanguage=True) |
|
1150 if res: |
|
1151 dia.exec() |
|
1152 res = dia.hasAddOrDelete() |
|
1153 self.checkVCSStatus() |
|
1154 return res |
|
1155 |
|
1156 def vcsMerge(self, name): |
|
1157 """ |
|
1158 Public method used to merge a URL/revision into the local project. |
|
1159 |
|
1160 @param name file/directory name to be merged (string) |
|
1161 """ |
|
1162 dname, fname = self.splitPath(name) |
|
1163 |
|
1164 opts = self.options['global'][:] |
|
1165 force = '--force' in opts |
|
1166 if force: |
|
1167 del opts[opts.index('--force')] |
|
1168 |
|
1169 from .SvnMergeDialog import SvnMergeDialog |
|
1170 dlg = SvnMergeDialog( |
|
1171 self.mergeList[0], self.mergeList[1], self.mergeList[2], force) |
|
1172 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
1173 urlrev1, urlrev2, target, force = dlg.getParameters() |
|
1174 else: |
|
1175 return |
|
1176 |
|
1177 # remember URL or revision |
|
1178 if urlrev1 in self.mergeList[0]: |
|
1179 self.mergeList[0].remove(urlrev1) |
|
1180 self.mergeList[0].insert(0, urlrev1) |
|
1181 if urlrev2 in self.mergeList[1]: |
|
1182 self.mergeList[1].remove(urlrev2) |
|
1183 self.mergeList[1].insert(0, urlrev2) |
|
1184 |
|
1185 rx_rev = re.compile('\\d+|HEAD') |
|
1186 |
|
1187 args = [] |
|
1188 args.append('merge') |
|
1189 self.addArguments(args, opts) |
|
1190 if self.version >= (1, 5, 0): |
|
1191 args.append('--accept') |
|
1192 args.append('postpone') |
|
1193 if force: |
|
1194 args.append('--force') |
|
1195 if rx_rev.fullmatch(urlrev1) is not None: |
|
1196 args.append('-r') |
|
1197 args.append('{0}:{1}'.format(urlrev1, urlrev2)) |
|
1198 if not target: |
|
1199 args.append(name) |
|
1200 else: |
|
1201 args.append(target) |
|
1202 |
|
1203 # remember target |
|
1204 if target in self.mergeList[2]: |
|
1205 self.mergeList[2].remove(target) |
|
1206 self.mergeList[2].insert(0, target) |
|
1207 else: |
|
1208 args.append(self.__svnURL(urlrev1)) |
|
1209 args.append(self.__svnURL(urlrev2)) |
|
1210 args.append(fname) |
|
1211 |
|
1212 dia = SvnDialog(self.tr('Merging {0}').format(name)) |
|
1213 res = dia.startProcess(args, dname) |
|
1214 if res: |
|
1215 dia.exec() |
|
1216 |
|
1217 def vcsRegisteredState(self, name): |
|
1218 """ |
|
1219 Public method used to get the registered state of a file in the vcs. |
|
1220 |
|
1221 @param name filename to check (string) |
|
1222 @return a combination of canBeCommited and canBeAdded |
|
1223 """ |
|
1224 if self.__wcng: |
|
1225 return self.__vcsRegisteredState_wcng(name) |
|
1226 else: |
|
1227 return self.__vcsRegisteredState_wc(name) |
|
1228 |
|
1229 def __vcsRegisteredState_wcng(self, name): |
|
1230 """ |
|
1231 Private method used to get the registered state of a file in the vcs. |
|
1232 |
|
1233 This is the variant for subversion installations using the new |
|
1234 working copy meta-data format. |
|
1235 |
|
1236 @param name filename to check (string) |
|
1237 @return a combination of canBeCommited and canBeAdded |
|
1238 """ |
|
1239 if name.endswith(os.sep): |
|
1240 name = name[:-1] |
|
1241 name = os.path.normcase(name) |
|
1242 dname, fname = self.splitPath(name) |
|
1243 |
|
1244 if fname == '.' and os.path.isdir(os.path.join(dname, self.adminDir)): |
|
1245 return self.canBeCommitted |
|
1246 |
|
1247 if name in self.statusCache: |
|
1248 return self.statusCache[name] |
|
1249 |
|
1250 name = os.path.normcase(name) |
|
1251 states = {name: 0} |
|
1252 states = self.vcsAllRegisteredStates(states, dname, False) |
|
1253 if states[name] == self.canBeCommitted: |
|
1254 return self.canBeCommitted |
|
1255 else: |
|
1256 return self.canBeAdded |
|
1257 |
|
1258 def __vcsRegisteredState_wc(self, name): |
|
1259 """ |
|
1260 Private method used to get the registered state of a file in the VCS. |
|
1261 |
|
1262 This is the variant for subversion installations using the old working |
|
1263 copy meta-data format. |
|
1264 |
|
1265 @param name filename to check (string) |
|
1266 @return a combination of canBeCommited and canBeAdded |
|
1267 """ |
|
1268 dname, fname = self.splitPath(name) |
|
1269 |
|
1270 if fname == '.': |
|
1271 if os.path.isdir(os.path.join(dname, self.adminDir)): |
|
1272 return self.canBeCommitted |
|
1273 else: |
|
1274 return self.canBeAdded |
|
1275 |
|
1276 name = os.path.normcase(name) |
|
1277 states = {name: 0} |
|
1278 states = self.vcsAllRegisteredStates(states, dname, False) |
|
1279 if states[name] == self.canBeCommitted: |
|
1280 return self.canBeCommitted |
|
1281 else: |
|
1282 return self.canBeAdded |
|
1283 |
|
1284 def vcsAllRegisteredStates(self, names, dname, shortcut=True): |
|
1285 """ |
|
1286 Public method used to get the registered states of a number of files |
|
1287 in the VCS. |
|
1288 |
|
1289 <b>Note:</b> If a shortcut is to be taken, the code will only check, |
|
1290 if the named directory has been scanned already. If so, it is assumed, |
|
1291 that the states for all files have been populated by the previous run. |
|
1292 |
|
1293 @param names dictionary with all filenames to be checked as keys |
|
1294 @param dname directory to check in (string) |
|
1295 @param shortcut flag indicating a shortcut should be taken (boolean) |
|
1296 @return the received dictionary completed with a combination of |
|
1297 canBeCommited and canBeAdded or None in order to signal an error |
|
1298 """ |
|
1299 if self.__wcng: |
|
1300 return self.__vcsAllRegisteredStates_wcng(names, dname, shortcut) |
|
1301 else: |
|
1302 return self.__vcsAllRegisteredStates_wc(names, dname, shortcut) |
|
1303 |
|
1304 def __vcsAllRegisteredStates_wcng(self, names, dname, shortcut=True): |
|
1305 """ |
|
1306 Private method used to get the registered states of a number of files |
|
1307 in the VCS. |
|
1308 |
|
1309 This is the variant for subversion installations using the new working |
|
1310 copy meta-data format. |
|
1311 |
|
1312 <b>Note:</b> If a shortcut is to be taken, the code will only check, |
|
1313 if the named directory has been scanned already. If so, it is assumed, |
|
1314 that the states for all files has been populated by the previous run. |
|
1315 |
|
1316 @param names dictionary with all filenames to be checked as keys |
|
1317 @param dname directory to check in (string) |
|
1318 @param shortcut flag indicating a shortcut should be taken (boolean) |
|
1319 @return the received dictionary completed with a combination of |
|
1320 canBeCommited and canBeAdded or None in order to signal an error |
|
1321 """ |
|
1322 if dname.endswith(os.sep): |
|
1323 dname = dname[:-1] |
|
1324 dname = os.path.normcase(dname) |
|
1325 |
|
1326 found = False |
|
1327 for name in self.statusCache: |
|
1328 if name in names: |
|
1329 found = True |
|
1330 names[name] = self.statusCache[name] |
|
1331 |
|
1332 if not found: |
|
1333 # find the root of the repo |
|
1334 repodir = dname |
|
1335 while not os.path.isdir(os.path.join(repodir, self.adminDir)): |
|
1336 repodir = os.path.dirname(repodir) |
|
1337 if os.path.splitdrive(repodir)[1] == os.sep: |
|
1338 return names |
|
1339 |
|
1340 ioEncoding = str(Preferences.getSystem("IOEncoding")) |
|
1341 process = QProcess() |
|
1342 args = [] |
|
1343 args.append('status') |
|
1344 args.append('--verbose') |
|
1345 args.append('--non-interactive') |
|
1346 args.append(dname) |
|
1347 process.start('svn', args) |
|
1348 procStarted = process.waitForStarted(5000) |
|
1349 if procStarted: |
|
1350 finished = process.waitForFinished(30000) |
|
1351 if finished and process.exitCode() == 0: |
|
1352 output = str(process.readAllStandardOutput(), ioEncoding, |
|
1353 'replace') |
|
1354 for line in output.splitlines(): |
|
1355 match = self.rx_status1.fullmatch(line) |
|
1356 if match is not None: |
|
1357 flags = match.group(1) |
|
1358 path = match.group(5).strip() |
|
1359 else: |
|
1360 match = self.rx_status2.fullmatch(line) |
|
1361 if match is not None: |
|
1362 flags = match.group(1) |
|
1363 path = match.group(2).strip() |
|
1364 else: |
|
1365 continue |
|
1366 name = os.path.normcase(path) |
|
1367 if flags[0] not in "?I": |
|
1368 if name in names: |
|
1369 names[name] = self.canBeCommitted |
|
1370 self.statusCache[name] = self.canBeCommitted |
|
1371 else: |
|
1372 self.statusCache[name] = self.canBeAdded |
|
1373 |
|
1374 return names |
|
1375 |
|
1376 def __vcsAllRegisteredStates_wc(self, names, dname, shortcut=True): |
|
1377 """ |
|
1378 Private method used to get the registered states of a number of files |
|
1379 in the VCS. |
|
1380 |
|
1381 This is the variant for subversion installations using the old working |
|
1382 copy meta-data format. |
|
1383 |
|
1384 <b>Note:</b> If a shortcut is to be taken, the code will only check, |
|
1385 if the named directory has been scanned already. If so, it is assumed, |
|
1386 that the states for all files has been populated by the previous run. |
|
1387 |
|
1388 @param names dictionary with all filenames to be checked as keys |
|
1389 @param dname directory to check in (string) |
|
1390 @param shortcut flag indicating a shortcut should be taken (boolean) |
|
1391 @return the received dictionary completed with a combination of |
|
1392 canBeCommited and canBeAdded or None in order to signal an error |
|
1393 """ |
|
1394 if not os.path.isdir(os.path.join(dname, self.adminDir)): |
|
1395 # not under version control -> do nothing |
|
1396 return names |
|
1397 |
|
1398 found = False |
|
1399 for name in list(self.statusCache.keys()): |
|
1400 if os.path.dirname(name) == dname: |
|
1401 if shortcut: |
|
1402 found = True |
|
1403 break |
|
1404 if name in names: |
|
1405 found = True |
|
1406 names[name] = self.statusCache[name] |
|
1407 |
|
1408 if not found: |
|
1409 ioEncoding = Preferences.getSystem("IOEncoding") |
|
1410 process = QProcess() |
|
1411 args = [] |
|
1412 args.append('status') |
|
1413 args.append('--verbose') |
|
1414 args.append('--non-interactive') |
|
1415 args.append(dname) |
|
1416 process.start('svn', args) |
|
1417 procStarted = process.waitForStarted(5000) |
|
1418 if procStarted: |
|
1419 finished = process.waitForFinished(30000) |
|
1420 if finished and process.exitCode() == 0: |
|
1421 output = str(process.readAllStandardOutput(), ioEncoding, |
|
1422 'replace') |
|
1423 for line in output.splitlines(): |
|
1424 match = self.rx_status1.fullmatch(line) |
|
1425 if match is not None: |
|
1426 flags = match.group(1) |
|
1427 path = match.group(5).strip() |
|
1428 else: |
|
1429 match = self.rx_status2.fullmatch(line) |
|
1430 if match is not None: |
|
1431 flags = match.group(1) |
|
1432 path = match.group(2).strip() |
|
1433 else: |
|
1434 continue |
|
1435 name = os.path.normcase(path) |
|
1436 if flags[0] not in "?I": |
|
1437 if name in names: |
|
1438 names[name] = self.canBeCommitted |
|
1439 self.statusCache[name] = self.canBeCommitted |
|
1440 else: |
|
1441 self.statusCache[name] = self.canBeAdded |
|
1442 |
|
1443 return names |
|
1444 |
|
1445 def clearStatusCache(self): |
|
1446 """ |
|
1447 Public method to clear the status cache. |
|
1448 """ |
|
1449 self.statusCache = {} |
|
1450 |
|
1451 def vcsInitConfig(self, project): |
|
1452 """ |
|
1453 Public method to initialize the VCS configuration. |
|
1454 |
|
1455 This method ensures, that an ignore file exists. |
|
1456 |
|
1457 @param project reference to the project (Project) |
|
1458 """ |
|
1459 configPath = getConfigPath() |
|
1460 if os.path.exists(configPath): |
|
1461 amendConfig() |
|
1462 else: |
|
1463 createDefaultConfig() |
|
1464 |
|
1465 def vcsName(self): |
|
1466 """ |
|
1467 Public method returning the name of the vcs. |
|
1468 |
|
1469 @return always 'Subversion' (string) |
|
1470 """ |
|
1471 return "Subversion" |
|
1472 |
|
1473 def vcsCleanup(self, name): |
|
1474 """ |
|
1475 Public method used to cleanup the working copy. |
|
1476 |
|
1477 @param name directory name to be cleaned up (string) |
|
1478 """ |
|
1479 args = [] |
|
1480 args.append('cleanup') |
|
1481 self.addArguments(args, self.options['global']) |
|
1482 args.append(name) |
|
1483 |
|
1484 dia = SvnDialog(self.tr('Cleaning up {0}') |
|
1485 .format(name)) |
|
1486 res = dia.startProcess(args) |
|
1487 if res: |
|
1488 dia.exec() |
|
1489 |
|
1490 def vcsCommandLine(self, name): |
|
1491 """ |
|
1492 Public method used to execute arbitrary subversion commands. |
|
1493 |
|
1494 @param name directory name of the working directory (string) |
|
1495 """ |
|
1496 from .SvnCommandDialog import SvnCommandDialog |
|
1497 dlg = SvnCommandDialog(self.commandHistory, self.wdHistory, name) |
|
1498 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
1499 command, wd = dlg.getData() |
|
1500 commandList = Utilities.parseOptionString(command) |
|
1501 |
|
1502 # This moves any previous occurrence of these arguments to the head |
|
1503 # of the list. |
|
1504 if command in self.commandHistory: |
|
1505 self.commandHistory.remove(command) |
|
1506 self.commandHistory.insert(0, command) |
|
1507 if wd in self.wdHistory: |
|
1508 self.wdHistory.remove(wd) |
|
1509 self.wdHistory.insert(0, wd) |
|
1510 |
|
1511 args = [] |
|
1512 self.addArguments(args, commandList) |
|
1513 |
|
1514 dia = SvnDialog(self.tr('Subversion command')) |
|
1515 res = dia.startProcess(args, wd) |
|
1516 if res: |
|
1517 dia.exec() |
|
1518 |
|
1519 def vcsOptionsDialog(self, project, archive, editable=False, parent=None): |
|
1520 """ |
|
1521 Public method to get a dialog to enter repository info. |
|
1522 |
|
1523 @param project reference to the project object |
|
1524 @param archive name of the project in the repository (string) |
|
1525 @param editable flag indicating that the project name is editable |
|
1526 (boolean) |
|
1527 @param parent parent widget (QWidget) |
|
1528 @return reference to the instantiated options dialog (SvnOptionsDialog) |
|
1529 """ |
|
1530 from .SvnOptionsDialog import SvnOptionsDialog |
|
1531 return SvnOptionsDialog(self, project, parent) |
|
1532 |
|
1533 def vcsNewProjectOptionsDialog(self, parent=None): |
|
1534 """ |
|
1535 Public method to get a dialog to enter repository info for getting |
|
1536 a new project. |
|
1537 |
|
1538 @param parent parent widget (QWidget) |
|
1539 @return reference to the instantiated options dialog |
|
1540 (SvnNewProjectOptionsDialog) |
|
1541 """ |
|
1542 from .SvnNewProjectOptionsDialog import SvnNewProjectOptionsDialog |
|
1543 return SvnNewProjectOptionsDialog(self, parent) |
|
1544 |
|
1545 def vcsRepositoryInfos(self, ppath): |
|
1546 """ |
|
1547 Public method to retrieve information about the repository. |
|
1548 |
|
1549 @param ppath local path to get the repository infos (string) |
|
1550 @return string with ready formated info for display (string) |
|
1551 """ |
|
1552 info = { |
|
1553 'committed-rev': '', |
|
1554 'committed-date': '', |
|
1555 'committed-time': '', |
|
1556 'url': '', |
|
1557 'last-author': '', |
|
1558 'revision': '' |
|
1559 } |
|
1560 |
|
1561 ioEncoding = Preferences.getSystem("IOEncoding") |
|
1562 |
|
1563 process = QProcess() |
|
1564 args = [] |
|
1565 args.append('info') |
|
1566 args.append('--non-interactive') |
|
1567 args.append('--xml') |
|
1568 args.append(ppath) |
|
1569 process.start('svn', args) |
|
1570 procStarted = process.waitForStarted(5000) |
|
1571 if procStarted: |
|
1572 finished = process.waitForFinished(30000) |
|
1573 if finished and process.exitCode() == 0: |
|
1574 output = str(process.readAllStandardOutput(), ioEncoding, |
|
1575 'replace') |
|
1576 entryFound = False |
|
1577 commitFound = False |
|
1578 for line in output.splitlines(): |
|
1579 line = line.strip() |
|
1580 if line.startswith('<entry'): |
|
1581 entryFound = True |
|
1582 elif line.startswith('<commit'): |
|
1583 commitFound = True |
|
1584 elif line.startswith('</commit>'): |
|
1585 commitFound = False |
|
1586 elif line.startswith("revision="): |
|
1587 rev = line[line.find('"') + 1:line.rfind('"')] |
|
1588 if entryFound: |
|
1589 info['revision'] = rev |
|
1590 entryFound = False |
|
1591 elif commitFound: |
|
1592 info['committed-rev'] = rev |
|
1593 elif line.startswith('<url>'): |
|
1594 info['url'] = ( |
|
1595 line.replace('<url>', '').replace('</url>', '') |
|
1596 ) |
|
1597 elif line.startswith('<author>'): |
|
1598 info['last-author'] = ( |
|
1599 line.replace('<author>', '') |
|
1600 .replace('</author>', '') |
|
1601 ) |
|
1602 elif line.startswith('<date>'): |
|
1603 value = ( |
|
1604 line.replace('<date>', '').replace('</date>', '') |
|
1605 ) |
|
1606 date, time = value.split('T') |
|
1607 info['committed-date'] = date |
|
1608 info['committed-time'] = "{0}{1}".format( |
|
1609 time.split('.')[0], time[-1]) |
|
1610 |
|
1611 return QCoreApplication.translate( |
|
1612 'subversion', |
|
1613 """<h3>Repository information</h3>""" |
|
1614 """<table>""" |
|
1615 """<tr><td><b>Subversion V.</b></td><td>{0}</td></tr>""" |
|
1616 """<tr><td><b>URL</b></td><td>{1}</td></tr>""" |
|
1617 """<tr><td><b>Current revision</b></td><td>{2}</td></tr>""" |
|
1618 """<tr><td><b>Committed revision</b></td><td>{3}</td></tr>""" |
|
1619 """<tr><td><b>Committed date</b></td><td>{4}</td></tr>""" |
|
1620 """<tr><td><b>Comitted time</b></td><td>{5}</td></tr>""" |
|
1621 """<tr><td><b>Last author</b></td><td>{6}</td></tr>""" |
|
1622 """</table>""" |
|
1623 ).format(self.versionStr, |
|
1624 info['url'], |
|
1625 info['revision'], |
|
1626 info['committed-rev'], |
|
1627 info['committed-date'], |
|
1628 info['committed-time'], |
|
1629 info['last-author']) |
|
1630 |
|
1631 ########################################################################### |
|
1632 ## Public Subversion specific methods are below. |
|
1633 ########################################################################### |
|
1634 |
|
1635 def svnGetReposName(self, path): |
|
1636 """ |
|
1637 Public method used to retrieve the URL of the subversion repository |
|
1638 path. |
|
1639 |
|
1640 @param path local path to get the svn repository path for (string) |
|
1641 @return string with the repository path URL |
|
1642 """ |
|
1643 ioEncoding = Preferences.getSystem("IOEncoding") |
|
1644 |
|
1645 process = QProcess() |
|
1646 args = [] |
|
1647 args.append('info') |
|
1648 args.append('--xml') |
|
1649 args.append('--non-interactive') |
|
1650 args.append(path) |
|
1651 process.start('svn', args) |
|
1652 procStarted = process.waitForStarted(5000) |
|
1653 if procStarted: |
|
1654 finished = process.waitForFinished(30000) |
|
1655 if finished and process.exitCode() == 0: |
|
1656 output = str(process.readAllStandardOutput(), ioEncoding, |
|
1657 'replace') |
|
1658 for line in output.splitlines(): |
|
1659 line = line.strip() |
|
1660 if line.startswith('<url>'): |
|
1661 reposURL = ( |
|
1662 line.replace('<url>', '').replace('</url>', '') |
|
1663 ) |
|
1664 return reposURL |
|
1665 |
|
1666 return "" |
|
1667 |
|
1668 def svnResolve(self, name): |
|
1669 """ |
|
1670 Public method used to resolve conflicts of a file/directory. |
|
1671 |
|
1672 @param name file/directory name to be resolved (string) |
|
1673 """ |
|
1674 args = [] |
|
1675 if self.version >= (1, 5, 0): |
|
1676 args.append('resolve') |
|
1677 args.append('--accept') |
|
1678 args.append('working') |
|
1679 else: |
|
1680 args.append('resolved') |
|
1681 self.addArguments(args, self.options['global']) |
|
1682 if isinstance(name, list): |
|
1683 self.addArguments(args, name) |
|
1684 else: |
|
1685 if os.path.isdir(name): |
|
1686 args.append('--recursive') |
|
1687 args.append(name) |
|
1688 |
|
1689 dia = SvnDialog(self.tr('Resolving conficts')) |
|
1690 res = dia.startProcess(args) |
|
1691 if res: |
|
1692 dia.exec() |
|
1693 self.checkVCSStatus() |
|
1694 |
|
1695 def svnCopy(self, name, project): |
|
1696 """ |
|
1697 Public method used to copy a file/directory. |
|
1698 |
|
1699 @param name file/directory name to be copied (string) |
|
1700 @param project reference to the project object |
|
1701 @return flag indicating successfull operation (boolean) |
|
1702 """ |
|
1703 from .SvnCopyDialog import SvnCopyDialog |
|
1704 rx_prot = re.compile('(file:|svn:|svn+ssh:|http:|https:).+') |
|
1705 dlg = SvnCopyDialog(name) |
|
1706 res = False |
|
1707 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
1708 target, force = dlg.getData() |
|
1709 |
|
1710 args = [] |
|
1711 args.append('copy') |
|
1712 self.addArguments(args, self.options['global']) |
|
1713 match = rx_prot.fullmatch(target) |
|
1714 if match is not None: |
|
1715 args.append('--message') |
|
1716 args.append('Copying {0} to {1}'.format(name, target)) |
|
1717 target = self.__svnURL(target) |
|
1718 args.append(name) |
|
1719 args.append(target) |
|
1720 |
|
1721 dia = SvnDialog(self.tr('Copying {0}') |
|
1722 .format(name)) |
|
1723 res = dia.startProcess(args) |
|
1724 if res: |
|
1725 dia.exec() |
|
1726 res = dia.normalExit() |
|
1727 if ( |
|
1728 res and |
|
1729 match is None and |
|
1730 target.startswith(project.getProjectPath()) |
|
1731 ): |
|
1732 if os.path.isdir(name): |
|
1733 project.copyDirectory(name, target) |
|
1734 else: |
|
1735 project.appendFile(target) |
|
1736 return res |
|
1737 |
|
1738 def svnListProps(self, name, recursive=False): |
|
1739 """ |
|
1740 Public method used to list the properties of a file/directory. |
|
1741 |
|
1742 @param name file/directory name (string or list of strings) |
|
1743 @param recursive flag indicating a recursive list is requested |
|
1744 """ |
|
1745 if self.propList is None: |
|
1746 from .SvnPropListDialog import SvnPropListDialog |
|
1747 self.propList = SvnPropListDialog(self) |
|
1748 self.propList.show() |
|
1749 self.propList.raise_() |
|
1750 self.propList.start(name, recursive) |
|
1751 |
|
1752 def svnSetProp(self, name, recursive=False): |
|
1753 """ |
|
1754 Public method used to add a property to a file/directory. |
|
1755 |
|
1756 @param name file/directory name (string or list of strings) |
|
1757 @param recursive flag indicating a recursive list is requested |
|
1758 """ |
|
1759 from .SvnPropSetDialog import SvnPropSetDialog |
|
1760 dlg = SvnPropSetDialog() |
|
1761 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
1762 propName, fileFlag, propValue = dlg.getData() |
|
1763 if not propName: |
|
1764 E5MessageBox.critical( |
|
1765 self.__ui, |
|
1766 self.tr("Subversion Set Property"), |
|
1767 self.tr("""You have to supply a property name.""" |
|
1768 """ Aborting.""")) |
|
1769 return |
|
1770 |
|
1771 args = [] |
|
1772 args.append('propset') |
|
1773 self.addArguments(args, self.options['global']) |
|
1774 if recursive: |
|
1775 args.append('--recursive') |
|
1776 args.append(propName) |
|
1777 if fileFlag: |
|
1778 args.append('--file') |
|
1779 args.append(propValue) |
|
1780 if isinstance(name, list): |
|
1781 dname, fnames = self.splitPathList(name) |
|
1782 self.addArguments(args, fnames) |
|
1783 else: |
|
1784 dname, fname = self.splitPath(name) |
|
1785 args.append(fname) |
|
1786 |
|
1787 dia = SvnDialog(self.tr('Subversion Set Property')) |
|
1788 res = dia.startProcess(args, dname) |
|
1789 if res: |
|
1790 dia.exec() |
|
1791 |
|
1792 def svnDelProp(self, name, recursive=False): |
|
1793 """ |
|
1794 Public method used to delete a property of a file/directory. |
|
1795 |
|
1796 @param name file/directory name (string or list of strings) |
|
1797 @param recursive flag indicating a recursive list is requested |
|
1798 """ |
|
1799 propName, ok = QInputDialog.getText( |
|
1800 None, |
|
1801 self.tr("Subversion Delete Property"), |
|
1802 self.tr("Enter property name"), |
|
1803 QLineEdit.EchoMode.Normal) |
|
1804 |
|
1805 if not ok: |
|
1806 return |
|
1807 |
|
1808 if not propName: |
|
1809 E5MessageBox.critical( |
|
1810 self.__ui, |
|
1811 self.tr("Subversion Delete Property"), |
|
1812 self.tr("""You have to supply a property name.""" |
|
1813 """ Aborting.""")) |
|
1814 return |
|
1815 |
|
1816 args = [] |
|
1817 args.append('propdel') |
|
1818 self.addArguments(args, self.options['global']) |
|
1819 if recursive: |
|
1820 args.append('--recursive') |
|
1821 args.append(propName) |
|
1822 if isinstance(name, list): |
|
1823 dname, fnames = self.splitPathList(name) |
|
1824 self.addArguments(args, fnames) |
|
1825 else: |
|
1826 dname, fname = self.splitPath(name) |
|
1827 args.append(fname) |
|
1828 |
|
1829 dia = SvnDialog(self.tr('Subversion Delete Property')) |
|
1830 res = dia.startProcess(args, dname) |
|
1831 if res: |
|
1832 dia.exec() |
|
1833 |
|
1834 def svnListTagBranch(self, path, tags=True): |
|
1835 """ |
|
1836 Public method used to list the available tags or branches. |
|
1837 |
|
1838 @param path directory name of the project (string) |
|
1839 @param tags flag indicating listing of branches or tags |
|
1840 (False = branches, True = tags) |
|
1841 """ |
|
1842 if self.tagbranchList is None: |
|
1843 from .SvnTagBranchListDialog import SvnTagBranchListDialog |
|
1844 self.tagbranchList = SvnTagBranchListDialog(self) |
|
1845 self.tagbranchList.show() |
|
1846 self.tagbranchList.raise_() |
|
1847 if tags: |
|
1848 if not self.showedTags: |
|
1849 self.showedTags = True |
|
1850 allTagsBranchesList = self.allTagsBranchesList |
|
1851 else: |
|
1852 self.tagsList = [] |
|
1853 allTagsBranchesList = None |
|
1854 self.tagbranchList.start(path, tags, |
|
1855 self.tagsList, allTagsBranchesList) |
|
1856 elif not tags: |
|
1857 if not self.showedBranches: |
|
1858 self.showedBranches = True |
|
1859 allTagsBranchesList = self.allTagsBranchesList |
|
1860 else: |
|
1861 self.branchesList = [] |
|
1862 allTagsBranchesList = None |
|
1863 self.tagbranchList.start( |
|
1864 path, tags, self.branchesList, self.allTagsBranchesList) |
|
1865 |
|
1866 def svnBlame(self, name): |
|
1867 """ |
|
1868 Public method to show the output of the svn blame command. |
|
1869 |
|
1870 @param name file name to show the blame for (string) |
|
1871 """ |
|
1872 if self.blame is None: |
|
1873 from .SvnBlameDialog import SvnBlameDialog |
|
1874 self.blame = SvnBlameDialog(self) |
|
1875 self.blame.show() |
|
1876 self.blame.raise_() |
|
1877 self.blame.start(name) |
|
1878 |
|
1879 def svnExtendedDiff(self, name): |
|
1880 """ |
|
1881 Public method used to view the difference of a file/directory to the |
|
1882 Subversion repository. |
|
1883 |
|
1884 If name is a directory and is the project directory, all project files |
|
1885 are saved first. If name is a file (or list of files), which is/are |
|
1886 being edited and has unsaved modification, they can be saved or the |
|
1887 operation may be aborted. |
|
1888 |
|
1889 This method gives the chance to enter the revisions to be compared. |
|
1890 |
|
1891 @param name file/directory name to be diffed (string) |
|
1892 """ |
|
1893 names = name[:] if isinstance(name, list) else [name] |
|
1894 for nam in names: |
|
1895 if os.path.isfile(nam): |
|
1896 editor = e5App().getObject("ViewManager").getOpenEditor(nam) |
|
1897 if editor and not editor.checkDirty(): |
|
1898 return |
|
1899 else: |
|
1900 project = e5App().getObject("Project") |
|
1901 if nam == project.ppath and not project.saveAllScripts(): |
|
1902 return |
|
1903 from .SvnRevisionSelectionDialog import SvnRevisionSelectionDialog |
|
1904 dlg = SvnRevisionSelectionDialog() |
|
1905 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
1906 revisions = dlg.getRevisions() |
|
1907 from .SvnDiffDialog import SvnDiffDialog |
|
1908 self.diff = SvnDiffDialog(self) |
|
1909 self.diff.show() |
|
1910 self.diff.start(name, revisions) |
|
1911 |
|
1912 def svnUrlDiff(self, name): |
|
1913 """ |
|
1914 Public method used to view the difference of a file/directory of two |
|
1915 repository URLs. |
|
1916 |
|
1917 If name is a directory and is the project directory, all project files |
|
1918 are saved first. If name is a file (or list of files), which is/are |
|
1919 being edited and has unsaved modification, they can be saved or the |
|
1920 operation may be aborted. |
|
1921 |
|
1922 This method gives the chance to enter the revisions to be compared. |
|
1923 |
|
1924 @param name file/directory name to be diffed (string) |
|
1925 """ |
|
1926 names = name[:] if isinstance(name, list) else [name] |
|
1927 for nam in names: |
|
1928 if os.path.isfile(nam): |
|
1929 editor = e5App().getObject("ViewManager").getOpenEditor(nam) |
|
1930 if editor and not editor.checkDirty(): |
|
1931 return |
|
1932 else: |
|
1933 project = e5App().getObject("Project") |
|
1934 if nam == project.ppath and not project.saveAllScripts(): |
|
1935 return |
|
1936 |
|
1937 dname = self.splitPath(names[0])[0] |
|
1938 |
|
1939 from .SvnUrlSelectionDialog import SvnUrlSelectionDialog |
|
1940 dlg = SvnUrlSelectionDialog(self, self.tagsList, self.branchesList, |
|
1941 dname) |
|
1942 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
1943 urls, summary = dlg.getURLs() |
|
1944 from .SvnDiffDialog import SvnDiffDialog |
|
1945 self.diff = SvnDiffDialog(self) |
|
1946 self.diff.show() |
|
1947 QApplication.processEvents() |
|
1948 self.diff.start(name, urls=urls, summary=summary) |
|
1949 |
|
1950 def __svnGetFileForRevision(self, name, rev=""): |
|
1951 """ |
|
1952 Private method to get a file for a specific revision from the |
|
1953 repository. |
|
1954 |
|
1955 @param name file name to get from the repository (string) |
|
1956 @param rev revision to retrieve (integer or string) |
|
1957 @return contents of the file (string) and an error message (string) |
|
1958 """ |
|
1959 args = [] |
|
1960 args.append("cat") |
|
1961 if rev: |
|
1962 args.append("--revision") |
|
1963 args.append(str(rev)) |
|
1964 args.append(name) |
|
1965 |
|
1966 output = "" |
|
1967 error = "" |
|
1968 |
|
1969 process = QProcess() |
|
1970 process.start('svn', args) |
|
1971 procStarted = process.waitForStarted(5000) |
|
1972 if procStarted: |
|
1973 finished = process.waitForFinished(30000) |
|
1974 if finished: |
|
1975 if process.exitCode() == 0: |
|
1976 output = str( |
|
1977 process.readAllStandardOutput(), |
|
1978 Preferences.getSystem("IOEncoding"), 'replace') |
|
1979 else: |
|
1980 error = str( |
|
1981 process.readAllStandardError(), |
|
1982 Preferences.getSystem("IOEncoding"), 'replace') |
|
1983 else: |
|
1984 error = self.tr( |
|
1985 "The svn process did not finish within 30s.") |
|
1986 else: |
|
1987 error = self.tr( |
|
1988 'The process {0} could not be started. ' |
|
1989 'Ensure, that it is in the search path.').format('svn') |
|
1990 |
|
1991 return output, error |
|
1992 |
|
1993 def svnSbsDiff(self, name, extended=False, revisions=None): |
|
1994 """ |
|
1995 Public method used to view the difference of a file to the Mercurial |
|
1996 repository side-by-side. |
|
1997 |
|
1998 @param name file name to be diffed (string) |
|
1999 @param extended flag indicating the extended variant (boolean) |
|
2000 @param revisions tuple of two revisions (tuple of strings) |
|
2001 @exception ValueError raised to indicate an illegal name parameter type |
|
2002 """ |
|
2003 if isinstance(name, list): |
|
2004 raise ValueError("Wrong parameter type") |
|
2005 |
|
2006 if extended: |
|
2007 from .SvnRevisionSelectionDialog import SvnRevisionSelectionDialog |
|
2008 dlg = SvnRevisionSelectionDialog() |
|
2009 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
2010 rev1, rev2 = dlg.getRevisions() |
|
2011 if rev1 == "WORKING": |
|
2012 rev1 = "" |
|
2013 if rev2 == "WORKING": |
|
2014 rev2 = "" |
|
2015 else: |
|
2016 return |
|
2017 elif revisions: |
|
2018 rev1, rev2 = revisions[0], revisions[1] |
|
2019 else: |
|
2020 rev1, rev2 = "", "" |
|
2021 |
|
2022 output1, error = self.__svnGetFileForRevision(name, rev=rev1) |
|
2023 if error: |
|
2024 E5MessageBox.critical( |
|
2025 self.__ui, |
|
2026 self.tr("Subversion Side-by-Side Difference"), |
|
2027 error) |
|
2028 return |
|
2029 name1 = "{0} (rev. {1})".format(name, rev1 and rev1 or ".") |
|
2030 |
|
2031 if rev2: |
|
2032 output2, error = self.__svnGetFileForRevision(name, rev=rev2) |
|
2033 if error: |
|
2034 E5MessageBox.critical( |
|
2035 self.__ui, |
|
2036 self.tr("Subversion Side-by-Side Difference"), |
|
2037 error) |
|
2038 return |
|
2039 name2 = "{0} (rev. {1})".format(name, rev2) |
|
2040 else: |
|
2041 try: |
|
2042 with open(name, "r", encoding="utf-8") as f1: |
|
2043 output2 = f1.read() |
|
2044 name2 = name |
|
2045 except OSError: |
|
2046 E5MessageBox.critical( |
|
2047 self.__ui, |
|
2048 self.tr("Subversion Side-by-Side Difference"), |
|
2049 self.tr( |
|
2050 """<p>The file <b>{0}</b> could not be read.</p>""") |
|
2051 .format(name)) |
|
2052 return |
|
2053 |
|
2054 if self.sbsDiff is None: |
|
2055 from UI.CompareDialog import CompareDialog |
|
2056 self.sbsDiff = CompareDialog() |
|
2057 self.sbsDiff.show() |
|
2058 self.sbsDiff.raise_() |
|
2059 self.sbsDiff.compare(output1, output2, name1, name2) |
|
2060 |
|
2061 def vcsLogBrowser(self, name, isFile=False): |
|
2062 """ |
|
2063 Public method used to browse the log of a file/directory from the |
|
2064 Subversion repository. |
|
2065 |
|
2066 @param name file/directory name to show the log of (string) |
|
2067 @param isFile flag indicating log for a file is to be shown (boolean) |
|
2068 """ |
|
2069 if self.logBrowser is None: |
|
2070 from .SvnLogBrowserDialog import SvnLogBrowserDialog |
|
2071 self.logBrowser = SvnLogBrowserDialog(self) |
|
2072 self.logBrowser.show() |
|
2073 self.logBrowser.raise_() |
|
2074 self.logBrowser.start(name, isFile=isFile) |
|
2075 |
|
2076 def svnLock(self, name, stealIt=False, parent=None): |
|
2077 """ |
|
2078 Public method used to lock a file in the Subversion repository. |
|
2079 |
|
2080 @param name file/directory name to be locked (string or list of |
|
2081 strings) |
|
2082 @param stealIt flag indicating a forced operation (boolean) |
|
2083 @param parent reference to the parent object of the subversion dialog |
|
2084 (QWidget) |
|
2085 """ |
|
2086 args = [] |
|
2087 args.append('lock') |
|
2088 self.addArguments(args, self.options['global']) |
|
2089 if stealIt: |
|
2090 args.append('--force') |
|
2091 if isinstance(name, list): |
|
2092 dname, fnames = self.splitPathList(name) |
|
2093 self.addArguments(args, fnames) |
|
2094 else: |
|
2095 dname, fname = self.splitPath(name) |
|
2096 args.append(fname) |
|
2097 |
|
2098 dia = SvnDialog( |
|
2099 self.tr('Locking in the Subversion repository'), parent) |
|
2100 res = dia.startProcess(args, dname) |
|
2101 if res: |
|
2102 dia.exec() |
|
2103 |
|
2104 def svnUnlock(self, name, breakIt=False, parent=None): |
|
2105 """ |
|
2106 Public method used to unlock a file in the Subversion repository. |
|
2107 |
|
2108 @param name file/directory name to be unlocked (string or list of |
|
2109 strings) |
|
2110 @param breakIt flag indicating a forced operation (boolean) |
|
2111 @param parent reference to the parent object of the subversion dialog |
|
2112 (QWidget) |
|
2113 """ |
|
2114 args = [] |
|
2115 args.append('unlock') |
|
2116 self.addArguments(args, self.options['global']) |
|
2117 if breakIt: |
|
2118 args.append('--force') |
|
2119 if isinstance(name, list): |
|
2120 dname, fnames = self.splitPathList(name) |
|
2121 self.addArguments(args, fnames) |
|
2122 else: |
|
2123 dname, fname = self.splitPath(name) |
|
2124 args.append(fname) |
|
2125 |
|
2126 dia = SvnDialog( |
|
2127 self.tr('Unlocking in the Subversion repository'), parent) |
|
2128 res = dia.startProcess(args, dname) |
|
2129 if res: |
|
2130 dia.exec() |
|
2131 |
|
2132 def svnRelocate(self, projectPath): |
|
2133 """ |
|
2134 Public method to relocate the working copy to a new repository URL. |
|
2135 |
|
2136 @param projectPath path name of the project (string) |
|
2137 """ |
|
2138 from .SvnRelocateDialog import SvnRelocateDialog |
|
2139 currUrl = self.svnGetReposName(projectPath) |
|
2140 dlg = SvnRelocateDialog(currUrl) |
|
2141 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
2142 newUrl, inside = dlg.getData() |
|
2143 args = [] |
|
2144 args.append('switch') |
|
2145 if not inside: |
|
2146 args.append('--relocate') |
|
2147 args.append(currUrl) |
|
2148 args.append(newUrl) |
|
2149 args.append(projectPath) |
|
2150 |
|
2151 dia = SvnDialog(self.tr('Relocating')) |
|
2152 res = dia.startProcess(args) |
|
2153 if res: |
|
2154 dia.exec() |
|
2155 |
|
2156 def svnRepoBrowser(self, projectPath=None): |
|
2157 """ |
|
2158 Public method to open the repository browser. |
|
2159 |
|
2160 @param projectPath path name of the project (string) |
|
2161 """ |
|
2162 url = self.svnGetReposName(projectPath) if projectPath else None |
|
2163 |
|
2164 if url is None: |
|
2165 url, ok = QInputDialog.getText( |
|
2166 None, |
|
2167 self.tr("Repository Browser"), |
|
2168 self.tr("Enter the repository URL."), |
|
2169 QLineEdit.EchoMode.Normal) |
|
2170 if not ok or not url: |
|
2171 return |
|
2172 |
|
2173 if self.repoBrowser is None: |
|
2174 from .SvnRepoBrowserDialog import SvnRepoBrowserDialog |
|
2175 self.repoBrowser = SvnRepoBrowserDialog(self) |
|
2176 self.repoBrowser.show() |
|
2177 self.repoBrowser.raise_() |
|
2178 self.repoBrowser.start(url) |
|
2179 |
|
2180 def svnRemoveFromChangelist(self, names): |
|
2181 """ |
|
2182 Public method to remove a file or directory from its changelist. |
|
2183 |
|
2184 Note: Directories will be removed recursively. |
|
2185 |
|
2186 @param names name or list of names of file or directory to remove |
|
2187 (string) |
|
2188 """ |
|
2189 args = [] |
|
2190 args.append('changelist') |
|
2191 self.addArguments(args, self.options['global']) |
|
2192 args.append('--remove') |
|
2193 args.append('--recursive') |
|
2194 if isinstance(names, list): |
|
2195 dname, fnames = self.splitPathList(names) |
|
2196 self.addArguments(args, fnames) |
|
2197 else: |
|
2198 dname, fname = self.splitPath(names) |
|
2199 args.append(fname) |
|
2200 |
|
2201 dia = SvnDialog(self.tr('Remove from changelist')) |
|
2202 res = dia.startProcess(args, dname) |
|
2203 if res: |
|
2204 dia.exec() |
|
2205 |
|
2206 def svnAddToChangelist(self, names): |
|
2207 """ |
|
2208 Public method to add a file or directory to a changelist. |
|
2209 |
|
2210 Note: Directories will be added recursively. |
|
2211 |
|
2212 @param names name or list of names of file or directory to add |
|
2213 (string) |
|
2214 """ |
|
2215 clname, ok = QInputDialog.getItem( |
|
2216 None, |
|
2217 self.tr("Add to changelist"), |
|
2218 self.tr("Enter name of the changelist:"), |
|
2219 sorted(self.svnGetChangelists()), |
|
2220 0, True) |
|
2221 if not ok or not clname: |
|
2222 return |
|
2223 |
|
2224 args = [] |
|
2225 args.append('changelist') |
|
2226 self.addArguments(args, self.options['global']) |
|
2227 args.append('--recursive') |
|
2228 args.append(clname) |
|
2229 if isinstance(names, list): |
|
2230 dname, fnames = self.splitPathList(names) |
|
2231 self.addArguments(args, fnames) |
|
2232 else: |
|
2233 dname, fname = self.splitPath(names) |
|
2234 args.append(fname) |
|
2235 |
|
2236 dia = SvnDialog(self.tr('Remove from changelist')) |
|
2237 res = dia.startProcess(args, dname) |
|
2238 if res: |
|
2239 dia.exec() |
|
2240 |
|
2241 def svnShowChangelists(self, path): |
|
2242 """ |
|
2243 Public method used to inspect the change lists defined for the project. |
|
2244 |
|
2245 @param path directory name to show change lists for (string) |
|
2246 """ |
|
2247 from .SvnChangeListsDialog import SvnChangeListsDialog |
|
2248 self.changeLists = SvnChangeListsDialog(self) |
|
2249 self.changeLists.show() |
|
2250 QApplication.processEvents() |
|
2251 self.changeLists.start(path) |
|
2252 |
|
2253 def svnGetChangelists(self): |
|
2254 """ |
|
2255 Public method to get a list of all defined change lists. |
|
2256 |
|
2257 @return list of defined change list names (list of strings) |
|
2258 """ |
|
2259 changelists = [] |
|
2260 rx_changelist = re.compile('--- \\S+ .([\\w\\s]+).:\\s*') |
|
2261 # three dashes, Changelist (translated), quote, |
|
2262 # changelist name, quote, : |
|
2263 |
|
2264 args = [] |
|
2265 args.append("status") |
|
2266 args.append("--non-interactive") |
|
2267 args.append(".") |
|
2268 |
|
2269 ppath = e5App().getObject("Project").getProjectPath() |
|
2270 process = QProcess() |
|
2271 process.setWorkingDirectory(ppath) |
|
2272 process.start('svn', args) |
|
2273 procStarted = process.waitForStarted(5000) |
|
2274 if procStarted: |
|
2275 finished = process.waitForFinished(30000) |
|
2276 if finished and process.exitCode() == 0: |
|
2277 output = str(process.readAllStandardOutput(), |
|
2278 Preferences.getSystem("IOEncoding"), |
|
2279 'replace') |
|
2280 if output: |
|
2281 for line in output.splitlines(): |
|
2282 match = rx_changelist.fullmatch(line) |
|
2283 if match is not None: |
|
2284 changelist = match.group(1) |
|
2285 if changelist not in changelists: |
|
2286 changelists.append(changelist) |
|
2287 |
|
2288 return changelists |
|
2289 |
|
2290 def svnUpgrade(self, path): |
|
2291 """ |
|
2292 Public method to upgrade the working copy format. |
|
2293 |
|
2294 @param path directory name to show change lists for (string) |
|
2295 """ |
|
2296 args = [] |
|
2297 args.append("upgrade") |
|
2298 args.append(".") |
|
2299 |
|
2300 dia = SvnDialog(self.tr('Upgrade')) |
|
2301 res = dia.startProcess(args, path) |
|
2302 if res: |
|
2303 dia.exec() |
|
2304 |
|
2305 ########################################################################### |
|
2306 ## Private Subversion specific methods are below. |
|
2307 ########################################################################### |
|
2308 |
|
2309 def __svnURL(self, url): |
|
2310 """ |
|
2311 Private method to format a url for subversion. |
|
2312 |
|
2313 @param url unformatted url string (string) |
|
2314 @return properly formated url for subversion (string) |
|
2315 """ |
|
2316 url = self.svnNormalizeURL(url) |
|
2317 url = url.split(':', 2) |
|
2318 if len(url) == 3: |
|
2319 scheme = url[0] |
|
2320 host = url[1] |
|
2321 port, path = url[2].split("/", 1) |
|
2322 return "{0}:{1}:{2}/{3}".format(scheme, host, port, quote(path)) |
|
2323 else: |
|
2324 scheme = url[0] |
|
2325 if scheme == "file": |
|
2326 return "{0}:{1}".format(scheme, quote(url[1])) |
|
2327 else: |
|
2328 try: |
|
2329 host, path = url[1][2:].split("/", 1) |
|
2330 except ValueError: |
|
2331 host = url[1][2:] |
|
2332 path = "" |
|
2333 return "{0}://{1}/{2}".format(scheme, host, quote(path)) |
|
2334 |
|
2335 def svnNormalizeURL(self, url): |
|
2336 """ |
|
2337 Public method to normalize a url for subversion. |
|
2338 |
|
2339 @param url url string (string) |
|
2340 @return properly normalized url for subversion (string) |
|
2341 """ |
|
2342 protocol, url = url.split("://", 1) |
|
2343 if url.startswith("\\\\"): |
|
2344 url = url[2:] |
|
2345 if protocol == "file": |
|
2346 url = os.path.normcase(url) |
|
2347 url = url.replace('\\', '/') |
|
2348 if url.endswith('/'): |
|
2349 url = url[:-1] |
|
2350 if not url.startswith("/") and url[1] in [":", "|"]: |
|
2351 url = "/{0}".format(url) |
|
2352 return "{0}://{1}".format(protocol, url) |
|
2353 |
|
2354 ########################################################################### |
|
2355 ## Methods to get the helper objects are below. |
|
2356 ########################################################################### |
|
2357 |
|
2358 def vcsGetProjectBrowserHelper(self, browser, project, |
|
2359 isTranslationsBrowser=False): |
|
2360 """ |
|
2361 Public method to instanciate a helper object for the different |
|
2362 project browsers. |
|
2363 |
|
2364 @param browser reference to the project browser object |
|
2365 @param project reference to the project object |
|
2366 @param isTranslationsBrowser flag indicating, the helper is requested |
|
2367 for the translations browser (this needs some special treatment) |
|
2368 @return the project browser helper object |
|
2369 """ |
|
2370 from .ProjectBrowserHelper import SvnProjectBrowserHelper |
|
2371 return SvnProjectBrowserHelper(self, browser, project, |
|
2372 isTranslationsBrowser) |
|
2373 |
|
2374 def vcsGetProjectHelper(self, project): |
|
2375 """ |
|
2376 Public method to instanciate a helper object for the project. |
|
2377 |
|
2378 @param project reference to the project object |
|
2379 @return the project helper object |
|
2380 """ |
|
2381 helper = self.__plugin.getProjectHelper() |
|
2382 helper.setObjects(self, project) |
|
2383 self.__wcng = ( |
|
2384 os.path.exists( |
|
2385 os.path.join(project.getProjectPath(), ".svn", "format")) or |
|
2386 os.path.exists( |
|
2387 os.path.join(project.getProjectPath(), "_svn", "format")) or |
|
2388 os.path.exists( |
|
2389 os.path.join(project.getProjectPath(), ".svn", "wc.db")) or |
|
2390 os.path.exists( |
|
2391 os.path.join(project.getProjectPath(), "_svn", "wc.db")) |
|
2392 ) |
|
2393 return helper |
|
2394 |
|
2395 ########################################################################### |
|
2396 ## Status Monitor Thread methods |
|
2397 ########################################################################### |
|
2398 |
|
2399 def _createStatusMonitorThread(self, interval, project): |
|
2400 """ |
|
2401 Protected method to create an instance of the VCS status monitor |
|
2402 thread. |
|
2403 |
|
2404 @param interval check interval for the monitor thread in seconds |
|
2405 (integer) |
|
2406 @param project reference to the project object |
|
2407 @return reference to the monitor thread (QThread) |
|
2408 """ |
|
2409 from .SvnStatusMonitorThread import SvnStatusMonitorThread |
|
2410 return SvnStatusMonitorThread(interval, project, self) |