|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2003 - 2009 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 shutil |
|
12 import types |
|
13 import urllib |
|
14 |
|
15 from PyQt4.QtCore import * |
|
16 from PyQt4.QtGui import * |
|
17 |
|
18 from E4Gui.E4Application import e4App |
|
19 |
|
20 from VCS.VersionControl import VersionControl |
|
21 |
|
22 from SvnDialog import SvnDialog |
|
23 from SvnCommitDialog import SvnCommitDialog |
|
24 from SvnLogDialog import SvnLogDialog |
|
25 from SvnLogBrowserDialog import SvnLogBrowserDialog |
|
26 from SvnDiffDialog import SvnDiffDialog |
|
27 from SvnRevisionSelectionDialog import SvnRevisionSelectionDialog |
|
28 from SvnStatusDialog import SvnStatusDialog |
|
29 from SvnTagDialog import SvnTagDialog |
|
30 from SvnTagBranchListDialog import SvnTagBranchListDialog |
|
31 from SvnCopyDialog import SvnCopyDialog |
|
32 from SvnCommandDialog import SvnCommandDialog |
|
33 from SvnSwitchDialog import SvnSwitchDialog |
|
34 from SvnMergeDialog import SvnMergeDialog |
|
35 from SvnPropListDialog import SvnPropListDialog |
|
36 from SvnPropSetDialog import SvnPropSetDialog |
|
37 from SvnOptionsDialog import SvnOptionsDialog |
|
38 from SvnNewProjectOptionsDialog import SvnNewProjectOptionsDialog |
|
39 from SvnBlameDialog import SvnBlameDialog |
|
40 from SvnRelocateDialog import SvnRelocateDialog |
|
41 from SvnUrlSelectionDialog import SvnUrlSelectionDialog |
|
42 from SvnRepoBrowserDialog import SvnRepoBrowserDialog |
|
43 from SvnStatusMonitorThread import SvnStatusMonitorThread |
|
44 |
|
45 from ProjectBrowserHelper import SvnProjectBrowserHelper |
|
46 from ProjectHelper import SvnProjectHelper |
|
47 |
|
48 import Preferences |
|
49 import Utilities |
|
50 |
|
51 class Subversion(VersionControl): |
|
52 """ |
|
53 Class implementing the version control systems interface to Subversion. |
|
54 |
|
55 @signal committed() emitted after the commit action has completed |
|
56 """ |
|
57 def __init__(self, plugin, parent=None, name=None): |
|
58 """ |
|
59 Constructor |
|
60 |
|
61 @param plugin reference to the plugin object |
|
62 @param parent parent widget (QWidget) |
|
63 @param name name of this object (string) |
|
64 """ |
|
65 VersionControl.__init__(self, parent, name) |
|
66 self.defaultOptions = { |
|
67 'global' : [''], |
|
68 'commit' : [''], |
|
69 'checkout' : [''], |
|
70 'update' : [''], |
|
71 'add' : [''], |
|
72 'remove' : [''], |
|
73 'diff' : [''], |
|
74 'log' : [''], |
|
75 'history' : [''], |
|
76 'status' : [''], |
|
77 'tag' : [''], |
|
78 'export' : [''] |
|
79 } |
|
80 self.interestingDataKeys = [ |
|
81 "standardLayout", |
|
82 ] |
|
83 |
|
84 self.__plugin = plugin |
|
85 self.__ui = parent |
|
86 |
|
87 self.options = self.defaultOptions |
|
88 self.otherData["standardLayout"] = True |
|
89 self.tagsList = [] |
|
90 self.branchesList = [] |
|
91 self.allTagsBranchesList = [] |
|
92 self.mergeList = [[], [], []] |
|
93 self.showedTags = False |
|
94 self.showedBranches = False |
|
95 |
|
96 self.tagTypeList = [ |
|
97 'tags', |
|
98 'branches', |
|
99 ] |
|
100 |
|
101 self.commandHistory = [] |
|
102 self.wdHistory = [] |
|
103 |
|
104 if os.environ.has_key("SVN_ASP_DOT_NET_HACK"): |
|
105 self.adminDir = '_svn' |
|
106 else: |
|
107 self.adminDir = '.svn' |
|
108 |
|
109 self.log = None |
|
110 self.diff = None |
|
111 self.status = None |
|
112 self.propList = None |
|
113 self.tagbranchList = None |
|
114 self.blame = None |
|
115 self.repoBrowser = None |
|
116 |
|
117 # regular expression object for evaluation of the status output |
|
118 self.rx_status1 = QRegExp('(.{8})\\s+([0-9-]+)\\s+([0-9?]+)\\s+([\\w?]+)\\s+(.+)') |
|
119 self.rx_status2 = QRegExp('(.{8})\\s+(.+)\\s*') |
|
120 self.statusCache = {} |
|
121 |
|
122 self.__commitData = {} |
|
123 self.__commitDialog = None |
|
124 |
|
125 def getPlugin(self): |
|
126 """ |
|
127 Public method to get a reference to the plugin object. |
|
128 |
|
129 @return reference to the plugin object (VcsSubversionPlugin) |
|
130 """ |
|
131 return self.__plugin |
|
132 |
|
133 def vcsShutdown(self): |
|
134 """ |
|
135 Public method used to shutdown the Subversion interface. |
|
136 """ |
|
137 if self.log is not None: |
|
138 self.log.close() |
|
139 if self.diff is not None: |
|
140 self.diff.close() |
|
141 if self.status is not None: |
|
142 self.status.close() |
|
143 if self.propList is not None: |
|
144 self.propList.close() |
|
145 if self.tagbranchList is not None: |
|
146 self.tagbranchList.close() |
|
147 if self.blame is not None: |
|
148 self.blame.close() |
|
149 if self.repoBrowser is not None: |
|
150 self.repoBrowser.close() |
|
151 |
|
152 def vcsExists(self): |
|
153 """ |
|
154 Public method used to test for the presence of the svn executable. |
|
155 |
|
156 @return flag indicating the existance (boolean) and an error message (string) |
|
157 """ |
|
158 self.versionStr = '' |
|
159 errMsg = "" |
|
160 ioEncoding = Preferences.getSystem("IOEncoding") |
|
161 |
|
162 process = QProcess() |
|
163 process.start('svn', ['--version']) |
|
164 procStarted = process.waitForStarted() |
|
165 if procStarted: |
|
166 finished = process.waitForFinished(30000) |
|
167 if finished and process.exitCode() == 0: |
|
168 output = \ |
|
169 unicode(process.readAllStandardOutput(), ioEncoding, 'replace') |
|
170 self.versionStr = output.split()[2] |
|
171 return True, errMsg |
|
172 else: |
|
173 if finished: |
|
174 errMsg = \ |
|
175 self.trUtf8("The svn process finished with the exit code {0}")\ |
|
176 .format(process.exitCode()) |
|
177 else: |
|
178 errMsg = self.trUtf8("The svn process did not finish within 30s.") |
|
179 else: |
|
180 errMsg = self.trUtf8("Could not start the svn executable.") |
|
181 |
|
182 return False, errMsg |
|
183 |
|
184 def vcsInit(self, vcsDir, noDialog = False): |
|
185 """ |
|
186 Public method used to initialize the subversion repository. |
|
187 |
|
188 The subversion repository has to be initialized from outside eric4 |
|
189 because the respective command always works locally. Therefore we |
|
190 always return TRUE without doing anything. |
|
191 |
|
192 @param vcsDir name of the VCS directory (string) |
|
193 @param noDialog flag indicating quiet operations (boolean) |
|
194 @return always TRUE |
|
195 """ |
|
196 return True |
|
197 |
|
198 def vcsConvertProject(self, vcsDataDict, project): |
|
199 """ |
|
200 Public method to convert an uncontrolled project to a version controlled project. |
|
201 |
|
202 @param vcsDataDict dictionary of data required for the conversion |
|
203 @param project reference to the project object |
|
204 """ |
|
205 success = self.vcsImport(vcsDataDict, project.ppath)[0] |
|
206 if not success: |
|
207 QMessageBox.critical(None, |
|
208 self.trUtf8("Create project in repository"), |
|
209 self.trUtf8("""The project could not be created in the repository.""" |
|
210 """ Maybe the given repository doesn't exist or the""" |
|
211 """ repository server is down.""")) |
|
212 else: |
|
213 cwdIsPpath = False |
|
214 if os.getcwd() == project.ppath: |
|
215 os.chdir(os.path.dirname(project.ppath)) |
|
216 cwdIsPpath = True |
|
217 tmpProjectDir = "%s_tmp" % project.ppath |
|
218 shutil.rmtree(tmpProjectDir, True) |
|
219 os.rename(project.ppath, tmpProjectDir) |
|
220 os.makedirs(project.ppath) |
|
221 self.vcsCheckout(vcsDataDict, project.ppath) |
|
222 if cwdIsPpath: |
|
223 os.chdir(project.ppath) |
|
224 self.vcsCommit(project.ppath, vcsDataDict["message"], True) |
|
225 pfn = project.pfile |
|
226 if not os.path.isfile(pfn): |
|
227 pfn += "z" |
|
228 if not os.path.isfile(pfn): |
|
229 QMessageBox.critical(None, |
|
230 self.trUtf8("New project"), |
|
231 self.trUtf8("""The project could not be checked out of the""" |
|
232 """ repository.<br />""" |
|
233 """Restoring the original contents.""")) |
|
234 if os.getcwd() == project.ppath: |
|
235 os.chdir(os.path.dirname(project.ppath)) |
|
236 cwdIsPpath = True |
|
237 else: |
|
238 cwdIsPpath = False |
|
239 shutil.rmtree(project.ppath, True) |
|
240 os.rename(tmpProjectDir, project.ppath) |
|
241 project.pdata["VCS"] = ['None'] |
|
242 project.vcs = None |
|
243 project.setDirty(True) |
|
244 project.saveProject() |
|
245 project.closeProject() |
|
246 return |
|
247 shutil.rmtree(tmpProjectDir, True) |
|
248 project.openProject(pfn) |
|
249 |
|
250 def vcsImport(self, vcsDataDict, projectDir, noDialog = False): |
|
251 """ |
|
252 Public method used to import the project into the Subversion repository. |
|
253 |
|
254 @param vcsDataDict dictionary of data required for the import |
|
255 @param projectDir project directory (string) |
|
256 @param noDialog flag indicating quiet operations |
|
257 @return flag indicating an execution without errors (boolean) |
|
258 and a flag indicating the version controll status (boolean) |
|
259 """ |
|
260 noDialog = False |
|
261 msg = vcsDataDict["message"] |
|
262 if not msg: |
|
263 msg = '***' |
|
264 |
|
265 vcsDir = self.svnNormalizeURL(vcsDataDict["url"]) |
|
266 if vcsDir.startswith('/'): |
|
267 vcsDir = 'file://%s' % vcsDir |
|
268 elif vcsDir[1] in ['|', ':']: |
|
269 vcsDir = 'file:///%s' % vcsDir |
|
270 |
|
271 project = vcsDir[vcsDir.rfind('/')+1:] |
|
272 |
|
273 # create the dir structure to be imported into the repository |
|
274 tmpDir = '%s_tmp' % projectDir |
|
275 try: |
|
276 os.makedirs(tmpDir) |
|
277 if self.otherData["standardLayout"]: |
|
278 os.mkdir(os.path.join(tmpDir, project)) |
|
279 os.mkdir(os.path.join(tmpDir, project, 'branches')) |
|
280 os.mkdir(os.path.join(tmpDir, project, 'tags')) |
|
281 shutil.copytree(projectDir, os.path.join(tmpDir, project, 'trunk')) |
|
282 else: |
|
283 shutil.copytree(projectDir, os.path.join(tmpDir, project)) |
|
284 except OSError, e: |
|
285 if os.path.isdir(tmpDir): |
|
286 shutil.rmtree(tmpDir, True) |
|
287 return False, False |
|
288 |
|
289 args = [] |
|
290 args.append('import') |
|
291 self.addArguments(args, self.options['global']) |
|
292 args.append('-m') |
|
293 args.append(msg) |
|
294 args.append(self.__svnURL(vcsDir)) |
|
295 |
|
296 if noDialog: |
|
297 status = self.startSynchronizedProcess(QProcess(), "svn", args, |
|
298 os.path.join(tmpDir, project)) |
|
299 else: |
|
300 dia = SvnDialog(self.trUtf8('Importing project into Subversion repository')) |
|
301 res = dia.startProcess(args, os.path.join(tmpDir, project)) |
|
302 if res: |
|
303 dia.exec_() |
|
304 status = dia.normalExit() |
|
305 |
|
306 shutil.rmtree(tmpDir, True) |
|
307 return status, False |
|
308 |
|
309 def vcsCheckout(self, vcsDataDict, projectDir, noDialog = False): |
|
310 """ |
|
311 Public method used to check the project out of the Subversion repository. |
|
312 |
|
313 @param vcsDataDict dictionary of data required for the checkout |
|
314 @param projectDir project directory to create (string) |
|
315 @param noDialog flag indicating quiet operations |
|
316 @return flag indicating an execution without errors (boolean) |
|
317 """ |
|
318 noDialog = False |
|
319 try: |
|
320 tag = vcsDataDict["tag"] |
|
321 except KeyError: |
|
322 tag = None |
|
323 vcsDir = self.svnNormalizeURL(vcsDataDict["url"]) |
|
324 if vcsDir.startswith('/'): |
|
325 vcsDir = 'file://%s' % vcsDir |
|
326 elif vcsDir[1] in ['|', ':']: |
|
327 vcsDir = 'file:///%s' % vcsDir |
|
328 |
|
329 if self.otherData["standardLayout"]: |
|
330 if tag is None or tag == '': |
|
331 svnUrl = '%s/trunk' % vcsDir |
|
332 else: |
|
333 if not tag.startswith('tags') and not tag.startswith('branches'): |
|
334 type, ok = QInputDialog.getItem(\ |
|
335 None, |
|
336 self.trUtf8("Subversion Checkout"), |
|
337 self.trUtf8("The tag must be a normal tag (tags) or" |
|
338 " a branch tag (branches)." |
|
339 " Please select from the list."), |
|
340 self.tagTypeList, |
|
341 0, False) |
|
342 if not ok: |
|
343 return False |
|
344 tag = '%s/%s' % (type, tag) |
|
345 svnUrl = '%s/%s' % (vcsDir, tag) |
|
346 else: |
|
347 svnUrl = vcsDir |
|
348 |
|
349 args = [] |
|
350 args.append('checkout') |
|
351 self.addArguments(args, self.options['global']) |
|
352 self.addArguments(args, self.options['checkout']) |
|
353 args.append(self.__svnURL(svnUrl)) |
|
354 args.append(projectDir) |
|
355 |
|
356 if noDialog: |
|
357 return self.startSynchronizedProcess(QProcess(), 'svn', args) |
|
358 else: |
|
359 dia = SvnDialog(self.trUtf8('Checking project out of Subversion repository')) |
|
360 res = dia.startProcess(args) |
|
361 if res: |
|
362 dia.exec_() |
|
363 return dia.normalExit() |
|
364 |
|
365 def vcsExport(self, vcsDataDict, projectDir): |
|
366 """ |
|
367 Public method used to export a directory from the Subversion repository. |
|
368 |
|
369 @param vcsDataDict dictionary of data required for the checkout |
|
370 @param projectDir project directory to create (string) |
|
371 @return flag indicating an execution without errors (boolean) |
|
372 """ |
|
373 try: |
|
374 tag = vcsDataDict["tag"] |
|
375 except KeyError: |
|
376 tag = None |
|
377 vcsDir = self.svnNormalizeURL(vcsDataDict["url"]) |
|
378 if vcsDir.startswith('/') or vcsDir[1] == '|': |
|
379 vcsDir = 'file://%s' % vcsDir |
|
380 |
|
381 if self.otherData["standardLayout"]: |
|
382 if tag is None or tag == '': |
|
383 svnUrl = '%s/trunk' % vcsDir |
|
384 else: |
|
385 if not tag.startswith('tags') and not tag.startswith('branches'): |
|
386 type, ok = QInputDialog.getItem(\ |
|
387 None, |
|
388 self.trUtf8("Subversion Export"), |
|
389 self.trUtf8("The tag must be a normal tag (tags) or" |
|
390 " a branch tag (branches)." |
|
391 " Please select from the list."), |
|
392 self.tagTypeList, |
|
393 0, False) |
|
394 if not ok: |
|
395 return False |
|
396 tag = '%s/%s' % (unicode(type), tag) |
|
397 svnUrl = '%s/%s' % (vcsDir, tag) |
|
398 else: |
|
399 svnUrl = vcsDir |
|
400 |
|
401 args = [] |
|
402 args.append('export') |
|
403 self.addArguments(args, self.options['global']) |
|
404 args.append("--force") |
|
405 args.append(self.__svnURL(svnUrl)) |
|
406 args.append(projectDir) |
|
407 |
|
408 dia = SvnDialog(self.trUtf8('Exporting project from Subversion repository')) |
|
409 res = dia.startProcess(args) |
|
410 if res: |
|
411 dia.exec_() |
|
412 return dia.normalExit() |
|
413 |
|
414 def vcsCommit(self, name, message, noDialog = False): |
|
415 """ |
|
416 Public method used to make the change of a file/directory permanent in the |
|
417 Subversion repository. |
|
418 |
|
419 @param name file/directory name to be committed (string or list of strings) |
|
420 @param message message for this operation (string) |
|
421 @param noDialog flag indicating quiet operations |
|
422 """ |
|
423 msg = message |
|
424 |
|
425 if not noDialog and not msg: |
|
426 # call CommitDialog and get message from there |
|
427 if self.__commitDialog is None: |
|
428 self.__commitDialog = SvnCommitDialog(self, self.__ui) |
|
429 self.connect(self.__commitDialog, SIGNAL("accepted()"), |
|
430 self.__vcsCommit_Step2) |
|
431 self.__commitDialog.show() |
|
432 self.__commitDialog.raise_() |
|
433 self.__commitDialog.activateWindow() |
|
434 |
|
435 self.__commitData["name"] = name |
|
436 self.__commitData["msg"] = msg |
|
437 self.__commitData["noDialog"] = noDialog |
|
438 |
|
439 if noDialog: |
|
440 self.__vcsCommit_Step2() |
|
441 |
|
442 def __vcsCommit_Step2(self): |
|
443 """ |
|
444 Private slot performing the second step of the commit action. |
|
445 """ |
|
446 name = self.__commitData["name"] |
|
447 msg = self.__commitData["msg"] |
|
448 noDialog = self.__commitData["noDialog"] |
|
449 |
|
450 if self.__commitDialog is not None: |
|
451 msg = self.__commitDialog.logMessage() |
|
452 if self.__commitDialog.hasChangelists(): |
|
453 changelists, keepChangelists = self.__commitDialog.changelistsData() |
|
454 else: |
|
455 changelists, keepChangelists = [], False |
|
456 self.disconnect(self.__commitDialog, SIGNAL("accepted()"), |
|
457 self.__vcsCommit_Step2) |
|
458 self.__commitDialog = None |
|
459 else: |
|
460 changelists, keepChangelists = [], False |
|
461 |
|
462 if not msg: |
|
463 msg = '***' |
|
464 |
|
465 args = [] |
|
466 args.append('commit') |
|
467 self.addArguments(args, self.options['global']) |
|
468 self.addArguments(args, self.options['commit']) |
|
469 if keepChangelists: |
|
470 args.append("--keep-changelists") |
|
471 for changelist in changelists: |
|
472 args.append("--changelist") |
|
473 args.append(changelist) |
|
474 args.append("-m") |
|
475 args.append(msg) |
|
476 if type(name) is types.ListType: |
|
477 dname, fnames = self.splitPathList(name) |
|
478 self.addArguments(args, fnames) |
|
479 else: |
|
480 dname, fname = self.splitPath(name) |
|
481 args.append(fname) |
|
482 |
|
483 if self.svnGetReposName(dname).startswith('http') or \ |
|
484 self.svnGetReposName(dname).startswith('svn'): |
|
485 noDialog = False |
|
486 |
|
487 if noDialog: |
|
488 self.startSynchronizedProcess(QProcess(), "svn", args, dname) |
|
489 else: |
|
490 dia = SvnDialog(self.trUtf8('Commiting changes to Subversion repository')) |
|
491 res = dia.startProcess(args, dname) |
|
492 if res: |
|
493 dia.exec_() |
|
494 self.emit(SIGNAL("committed()")) |
|
495 self.checkVCSStatus() |
|
496 |
|
497 def vcsUpdate(self, name, noDialog = False): |
|
498 """ |
|
499 Public method used to update a file/directory with the Subversion repository. |
|
500 |
|
501 @param name file/directory name to be updated (string or list of strings) |
|
502 @param noDialog flag indicating quiet operations (boolean) |
|
503 @return flag indicating, that the update contained an add |
|
504 or delete (boolean) |
|
505 """ |
|
506 args = [] |
|
507 args.append('update') |
|
508 self.addArguments(args, self.options['global']) |
|
509 self.addArguments(args, self.options['update']) |
|
510 if self.versionStr >= '1.5.0': |
|
511 args.append('--accept') |
|
512 args.append('postpone') |
|
513 if type(name) is types.ListType: |
|
514 dname, fnames = self.splitPathList(name) |
|
515 self.addArguments(args, fnames) |
|
516 else: |
|
517 dname, fname = self.splitPath(name) |
|
518 args.append(fname) |
|
519 |
|
520 if noDialog: |
|
521 self.startSynchronizedProcess(QProcess(), "svn", args, dname) |
|
522 res = False |
|
523 else: |
|
524 dia = SvnDialog(self.trUtf8('Synchronizing with the Subversion repository')) |
|
525 res = dia.startProcess(args, dname) |
|
526 if res: |
|
527 dia.exec_() |
|
528 res = dia.hasAddOrDelete() |
|
529 self.checkVCSStatus() |
|
530 return res |
|
531 |
|
532 def vcsAdd(self, name, isDir = False, noDialog = False): |
|
533 """ |
|
534 Public method used to add a file/directory to the Subversion repository. |
|
535 |
|
536 @param name file/directory name to be added (string) |
|
537 @param isDir flag indicating name is a directory (boolean) |
|
538 @param noDialog flag indicating quiet operations |
|
539 """ |
|
540 args = [] |
|
541 args.append('add') |
|
542 self.addArguments(args, self.options['global']) |
|
543 self.addArguments(args, self.options['add']) |
|
544 args.append('--non-recursive') |
|
545 if noDialog and '--force' not in args: |
|
546 args.append('--force') |
|
547 |
|
548 if type(name) is types.ListType: |
|
549 if isDir: |
|
550 dname, fname = os.path.split(name[0]) |
|
551 else: |
|
552 dname, fnames = self.splitPathList(name) |
|
553 else: |
|
554 if isDir: |
|
555 dname, fname = os.path.split(name) |
|
556 else: |
|
557 dname, fname = self.splitPath(name) |
|
558 tree = [] |
|
559 wdir = dname |
|
560 while not os.path.exists(os.path.join(dname, self.adminDir)): |
|
561 # add directories recursively, if they aren't in the repository already |
|
562 tree.insert(-1, dname) |
|
563 dname = os.path.split(dname)[0] |
|
564 wdir = dname |
|
565 self.addArguments(args, tree) |
|
566 |
|
567 if type(name) is types.ListType: |
|
568 tree2 = [] |
|
569 for n in name: |
|
570 d = os.path.split(n)[0] |
|
571 while not os.path.exists(os.path.join(d, self.adminDir)): |
|
572 if d in tree2 + tree: |
|
573 break |
|
574 tree2.append(d) |
|
575 d = os.path.split(d)[0] |
|
576 tree2.reverse() |
|
577 self.addArguments(args, tree2) |
|
578 self.addArguments(args, name) |
|
579 else: |
|
580 args.append(name) |
|
581 |
|
582 if noDialog: |
|
583 self.startSynchronizedProcess(QProcess(), "svn", args, wdir) |
|
584 else: |
|
585 dia = SvnDialog(\ |
|
586 self.trUtf8('Adding files/directories to the Subversion repository')) |
|
587 res = dia.startProcess(args, wdir) |
|
588 if res: |
|
589 dia.exec_() |
|
590 |
|
591 def vcsAddBinary(self, name, isDir = False): |
|
592 """ |
|
593 Public method used to add a file/directory in binary mode to the |
|
594 Subversion repository. |
|
595 |
|
596 @param name file/directory name to be added (string) |
|
597 @param isDir flag indicating name is a directory (boolean) |
|
598 """ |
|
599 self.vcsAdd(name, isDir) |
|
600 |
|
601 def vcsAddTree(self, path): |
|
602 """ |
|
603 Public method to add a directory tree rooted at path to the Subversion repository. |
|
604 |
|
605 @param path root directory of the tree to be added (string or list of strings)) |
|
606 """ |
|
607 args = [] |
|
608 args.append('add') |
|
609 self.addArguments(args, self.options['global']) |
|
610 self.addArguments(args, self.options['add']) |
|
611 |
|
612 tree = [] |
|
613 if type(path) is types.ListType: |
|
614 dname, fnames = self.splitPathList(path) |
|
615 for n in path: |
|
616 d = os.path.split(n)[0] |
|
617 while not os.path.exists(os.path.join(d, self.adminDir)): |
|
618 # add directories recursively, |
|
619 # if they aren't in the repository already |
|
620 if d in tree: |
|
621 break |
|
622 tree.append(d) |
|
623 d = os.path.split(d)[0] |
|
624 tree.reverse() |
|
625 else: |
|
626 dname, fname = os.path.split(path) |
|
627 while not os.path.exists(os.path.join(dname, self.adminDir)): |
|
628 # add directories recursively, |
|
629 # if they aren't in the repository already |
|
630 tree.insert(-1, dname) |
|
631 dname = os.path.split(dname)[0] |
|
632 if tree: |
|
633 self.vcsAdd(tree, True) |
|
634 |
|
635 if type(path) is types.ListType: |
|
636 self.addArguments(args, path) |
|
637 else: |
|
638 args.append(path) |
|
639 |
|
640 dia = SvnDialog(\ |
|
641 self.trUtf8('Adding directory trees to the Subversion repository')) |
|
642 res = dia.startProcess(args, dname) |
|
643 if res: |
|
644 dia.exec_() |
|
645 |
|
646 def vcsRemove(self, name, project = False, noDialog = False): |
|
647 """ |
|
648 Public method used to remove a file/directory from the Subversion repository. |
|
649 |
|
650 The default operation is to remove the local copy as well. |
|
651 |
|
652 @param name file/directory name to be removed (string or list of strings)) |
|
653 @param project flag indicating deletion of a project tree (boolean) (not needed) |
|
654 @param noDialog flag indicating quiet operations |
|
655 @return flag indicating successfull operation (boolean) |
|
656 """ |
|
657 args = [] |
|
658 args.append('delete') |
|
659 self.addArguments(args, self.options['global']) |
|
660 self.addArguments(args, self.options['remove']) |
|
661 if noDialog and '--force' not in args: |
|
662 args.append('--force') |
|
663 |
|
664 if type(name) is types.ListType: |
|
665 self.addArguments(args, name) |
|
666 else: |
|
667 args.append(name) |
|
668 |
|
669 if noDialog: |
|
670 res = self.startSynchronizedProcess(QProcess(), "svn", args) |
|
671 else: |
|
672 dia = SvnDialog(\ |
|
673 self.trUtf8('Removing files/directories from the Subversion repository')) |
|
674 res = dia.startProcess(args) |
|
675 if res: |
|
676 dia.exec_() |
|
677 res = dia.normalExit() |
|
678 |
|
679 return res |
|
680 |
|
681 def vcsMove(self, name, project, target = None, noDialog = False): |
|
682 """ |
|
683 Public method used to move a file/directory. |
|
684 |
|
685 @param name file/directory name to be moved (string) |
|
686 @param project reference to the project object |
|
687 @param target new name of the file/directory (string) |
|
688 @param noDialog flag indicating quiet operations |
|
689 @return flag indicating successfull operation (boolean) |
|
690 """ |
|
691 rx_prot = QRegExp('(file:|svn:|svn+ssh:|http:|https:).+') |
|
692 opts = self.options['global'][:] |
|
693 force = '--force' in opts |
|
694 if force: |
|
695 del opts[opts.index('--force')] |
|
696 |
|
697 res = False |
|
698 if noDialog: |
|
699 if target is None: |
|
700 return False |
|
701 force = True |
|
702 accepted = True |
|
703 else: |
|
704 dlg = SvnCopyDialog(name, None, True, force) |
|
705 accepted = (dlg.exec_() == QDialog.Accepted) |
|
706 if accepted: |
|
707 target, force = dlg.getData() |
|
708 |
|
709 if accepted: |
|
710 isdir = os.path.isdir(name) |
|
711 args = [] |
|
712 args.append('move') |
|
713 self.addArguments(args, opts) |
|
714 if force: |
|
715 args.append('--force') |
|
716 if rx_prot.exactMatch(target): |
|
717 args.append('--message') |
|
718 args.append('Moving {0} to {1}'.format(name, target)) |
|
719 target = self.__svnURL(target) |
|
720 else: |
|
721 target = unicode(target) |
|
722 args.append(name) |
|
723 args.append(target) |
|
724 |
|
725 if noDialog: |
|
726 res = self.startSynchronizedProcess(QProcess(), "svn", args) |
|
727 else: |
|
728 dia = SvnDialog(self.trUtf8('Moving {0}') |
|
729 .format(name)) |
|
730 res = dia.startProcess(args) |
|
731 if res: |
|
732 dia.exec_() |
|
733 res = dia.normalExit() |
|
734 if res and not rx_prot.exactMatch(target): |
|
735 if target.startswith(project.getProjectPath()): |
|
736 if os.path.isdir(name): |
|
737 project.moveDirectory(name, target) |
|
738 else: |
|
739 project.renameFileInPdata(name, target) |
|
740 else: |
|
741 if os.path.isdir(name): |
|
742 project.removeDirectory(name) |
|
743 else: |
|
744 project.removeFile(name) |
|
745 return res |
|
746 |
|
747 def vcsLog(self, name): |
|
748 """ |
|
749 Public method used to view the log of a file/directory from the |
|
750 Subversion repository. |
|
751 |
|
752 @param name file/directory name to show the log of (string) |
|
753 """ |
|
754 self.log = SvnLogDialog(self) |
|
755 self.log.show() |
|
756 self.log.start(name) |
|
757 |
|
758 def vcsDiff(self, name): |
|
759 """ |
|
760 Public method used to view the difference of a file/directory to the |
|
761 Subversion repository. |
|
762 |
|
763 If name is a directory and is the project directory, all project files |
|
764 are saved first. If name is a file (or list of files), which is/are being edited |
|
765 and has unsaved modification, they can be saved or the operation may be aborted. |
|
766 |
|
767 @param name file/directory name to be diffed (string) |
|
768 """ |
|
769 if type(name) is types.ListType: |
|
770 names = name[:] |
|
771 else: |
|
772 names = [name] |
|
773 for nam in names: |
|
774 if os.path.isfile(nam): |
|
775 editor = e4App().getObject("ViewManager").getOpenEditor(nam) |
|
776 if editor and not editor.checkDirty() : |
|
777 return |
|
778 else: |
|
779 project = e4App().getObject("Project") |
|
780 if nam == project.ppath and not project.saveAllScripts(): |
|
781 return |
|
782 self.diff = SvnDiffDialog(self) |
|
783 self.diff.show() |
|
784 QApplication.processEvents() |
|
785 self.diff.start(name) |
|
786 |
|
787 def vcsStatus(self, name): |
|
788 """ |
|
789 Public method used to view the status of files/directories in the |
|
790 Subversion repository. |
|
791 |
|
792 @param name file/directory name(s) to show the status of |
|
793 (string or list of strings) |
|
794 """ |
|
795 self.status = SvnStatusDialog(self) |
|
796 self.status.show() |
|
797 self.status.start(name) |
|
798 |
|
799 def vcsTag(self, name): |
|
800 """ |
|
801 Public method used to set the tag of a file/directory in the |
|
802 Subversion repository. |
|
803 |
|
804 @param name file/directory name to be tagged (string) |
|
805 """ |
|
806 dname, fname = self.splitPath(name) |
|
807 |
|
808 reposURL = self.svnGetReposName(dname) |
|
809 if reposURL is None: |
|
810 QMessageBox.critical(None, |
|
811 self.trUtf8("Subversion Error"), |
|
812 self.trUtf8("""The URL of the project repository could not be""" |
|
813 """ retrieved from the working copy. The tag operation will""" |
|
814 """ be aborted""")) |
|
815 return |
|
816 |
|
817 if self.otherData["standardLayout"]: |
|
818 url = None |
|
819 else: |
|
820 url = self.svnNormalizeURL(reposURL) |
|
821 dlg = SvnTagDialog(self.allTagsBranchesList, url, |
|
822 self.otherData["standardLayout"]) |
|
823 if dlg.exec_() == QDialog.Accepted: |
|
824 tag, tagOp = dlg.getParameters() |
|
825 if tag in self.allTagsBranchesList: |
|
826 self.allTagsBranchesList.remove(tag) |
|
827 self.allTagsBranchesList.insert(0, tag) |
|
828 else: |
|
829 return |
|
830 |
|
831 if self.otherData["standardLayout"]: |
|
832 rx_base = QRegExp('(.+)/(trunk|tags|branches).*') |
|
833 if not rx_base.exactMatch(reposURL): |
|
834 QMessageBox.critical(None, |
|
835 self.trUtf8("Subversion Error"), |
|
836 self.trUtf8("""The URL of the project repository has an""" |
|
837 """ invalid format. The tag operation will""" |
|
838 """ be aborted""")) |
|
839 return |
|
840 |
|
841 reposRoot = rx_base.cap(1) |
|
842 if tagOp in [1, 4]: |
|
843 url = '%s/tags/%s' % (reposRoot, urllib.quote(tag)) |
|
844 elif tagOp in [2, 8]: |
|
845 url = '%s/branches/%s' % (reposRoot, urllib.quote(tag)) |
|
846 else: |
|
847 url = self.__svnURL(tag) |
|
848 |
|
849 args = [] |
|
850 if tagOp in [1, 2]: |
|
851 args.append('copy') |
|
852 self.addArguments(args, self.options['global']) |
|
853 self.addArguments(args, self.options['tag']) |
|
854 args.append('--message') |
|
855 args.append('Created tag <%s>' % tag) |
|
856 args.append(reposURL) |
|
857 args.append(url) |
|
858 else: |
|
859 args.append('delete') |
|
860 self.addArguments(args, self.options['global']) |
|
861 self.addArguments(args, self.options['tag']) |
|
862 args.append('--message') |
|
863 args.append('Deleted tag <%s>' % tag) |
|
864 args.append(url) |
|
865 |
|
866 dia = SvnDialog(self.trUtf8('Tagging {0} in the Subversion repository') |
|
867 .format(name)) |
|
868 res = dia.startProcess(args) |
|
869 if res: |
|
870 dia.exec_() |
|
871 |
|
872 def vcsRevert(self, name): |
|
873 """ |
|
874 Public method used to revert changes made to a file/directory. |
|
875 |
|
876 @param name file/directory name to be reverted (string) |
|
877 """ |
|
878 args = [] |
|
879 args.append('revert') |
|
880 self.addArguments(args, self.options['global']) |
|
881 if type(name) is types.ListType: |
|
882 self.addArguments(args, name) |
|
883 else: |
|
884 if os.path.isdir(name): |
|
885 args.append('--recursive') |
|
886 args.append(name) |
|
887 |
|
888 dia = SvnDialog(self.trUtf8('Reverting changes')) |
|
889 res = dia.startProcess(args) |
|
890 if res: |
|
891 dia.exec_() |
|
892 self.checkVCSStatus() |
|
893 |
|
894 def vcsSwitch(self, name): |
|
895 """ |
|
896 Public method used to switch a directory to a different tag/branch. |
|
897 |
|
898 @param name directory name to be switched (string) |
|
899 """ |
|
900 dname, fname = self.splitPath(name) |
|
901 |
|
902 reposURL = self.svnGetReposName(dname) |
|
903 if reposURL is None: |
|
904 QMessageBox.critical(None, |
|
905 self.trUtf8("Subversion Error"), |
|
906 self.trUtf8("""The URL of the project repository could not be""" |
|
907 """ retrieved from the working copy. The switch operation will""" |
|
908 """ be aborted""")) |
|
909 return |
|
910 |
|
911 if self.otherData["standardLayout"]: |
|
912 url = None |
|
913 else: |
|
914 url = self.svnNormalizeURL(reposURL) |
|
915 dlg = SvnSwitchDialog(self.allTagsBranchesList, url, |
|
916 self.otherData["standardLayout"]) |
|
917 if dlg.exec_() == QDialog.Accepted: |
|
918 tag, tagType = dlg.getParameters() |
|
919 if tag in self.allTagsBranchesList: |
|
920 self.allTagsBranchesList.remove(tag) |
|
921 self.allTagsBranchesList.insert(0, tag) |
|
922 else: |
|
923 return |
|
924 |
|
925 if self.otherData["standardLayout"]: |
|
926 rx_base = QRegExp('(.+)/(trunk|tags|branches).*') |
|
927 if not rx_base.exactMatch(reposURL): |
|
928 QMessageBox.critical(None, |
|
929 self.trUtf8("Subversion Error"), |
|
930 self.trUtf8("""The URL of the project repository has an""" |
|
931 """ invalid format. The switch operation will""" |
|
932 """ be aborted""")) |
|
933 return |
|
934 |
|
935 reposRoot = rx_base.cap(1) |
|
936 tn = tag |
|
937 if tagType == 1: |
|
938 url = '%s/tags/%s' % (reposRoot, urllib.quote(tag)) |
|
939 elif tagType == 2: |
|
940 url = '%s/branches/%s' % (reposRoot, urllib.quote(tag)) |
|
941 elif tagType == 4: |
|
942 url = '%s/trunk' % (reposRoot) |
|
943 tn = 'HEAD' |
|
944 else: |
|
945 url = self.__svnURL(tag) |
|
946 tn = url |
|
947 |
|
948 args = [] |
|
949 args.append('switch') |
|
950 if self.versionStr >= '1.5.0': |
|
951 args.append('--accept') |
|
952 args.append('postpone') |
|
953 args.append(url) |
|
954 args.append(name) |
|
955 |
|
956 dia = SvnDialog(self.trUtf8('Switching to {0}') |
|
957 .format(tn)) |
|
958 res = dia.startProcess(args) |
|
959 if res: |
|
960 dia.exec_() |
|
961 |
|
962 def vcsMerge(self, name): |
|
963 """ |
|
964 Public method used to merge a URL/revision into the local project. |
|
965 |
|
966 @param name file/directory name to be merged (string) |
|
967 """ |
|
968 dname, fname = self.splitPath(name) |
|
969 |
|
970 opts = self.options['global'][:] |
|
971 force = '--force' in opts |
|
972 if force: |
|
973 del opts[opts.index('--force')] |
|
974 |
|
975 dlg = SvnMergeDialog(self.mergeList[0], self.mergeList[1], self.mergeList[2], |
|
976 force) |
|
977 if dlg.exec_() == QDialog.Accepted: |
|
978 urlrev1, urlrev2, target, force = dlg.getParameters() |
|
979 else: |
|
980 return |
|
981 |
|
982 # remember URL or revision |
|
983 if urlrev1 in self.mergeList[0]: |
|
984 self.mergeList[0].remove(urlrev1) |
|
985 self.mergeList[0].insert(0, urlrev1) |
|
986 if urlrev2 in self.mergeList[1]: |
|
987 self.mergeList[1].remove(urlrev2) |
|
988 self.mergeList[1].insert(0, urlrev2) |
|
989 |
|
990 rx_rev = QRegExp('\\d+|HEAD') |
|
991 |
|
992 args = [] |
|
993 args.append('merge') |
|
994 self.addArguments(args, opts) |
|
995 if self.versionStr >= '1.5.0': |
|
996 args.append('--accept') |
|
997 args.append('postpone') |
|
998 if force: |
|
999 args.append('--force') |
|
1000 if rx_rev.exactMatch(urlrev1): |
|
1001 args.append('-r') |
|
1002 args.append('{0}:{1}'.format(urlrev1, urlrev2)) |
|
1003 if not target: |
|
1004 args.append(name) |
|
1005 else: |
|
1006 args.append(target) |
|
1007 |
|
1008 # remember target |
|
1009 if target in self.mergeList[2]: |
|
1010 self.mergeList[2].remove(target) |
|
1011 self.mergeList[2].insert(0, target) |
|
1012 else: |
|
1013 args.append(self.__svnURL(urlrev1)) |
|
1014 args.append(self.__svnURL(urlrev2)) |
|
1015 args.append(fname) |
|
1016 |
|
1017 dia = SvnDialog(self.trUtf8('Merging {0}').format(name)) |
|
1018 res = dia.startProcess(args, dname) |
|
1019 if res: |
|
1020 dia.exec_() |
|
1021 |
|
1022 def vcsRegisteredState(self, name): |
|
1023 """ |
|
1024 Public method used to get the registered state of a file in the vcs. |
|
1025 |
|
1026 @param name filename to check (string) |
|
1027 @return a combination of canBeCommited and canBeAdded |
|
1028 """ |
|
1029 dname, fname = self.splitPath(name) |
|
1030 |
|
1031 if fname == '.': |
|
1032 if os.path.isdir(os.path.join(dname, self.adminDir)): |
|
1033 return self.canBeCommitted |
|
1034 else: |
|
1035 return self.canBeAdded |
|
1036 |
|
1037 name = os.path.normcase(name) |
|
1038 states = { name : 0 } |
|
1039 states = self.vcsAllRegisteredStates(states, dname, False) |
|
1040 if states[name] == self.canBeCommitted: |
|
1041 return self.canBeCommitted |
|
1042 else: |
|
1043 return self.canBeAdded |
|
1044 |
|
1045 def vcsAllRegisteredStates(self, names, dname, shortcut = True): |
|
1046 """ |
|
1047 Public method used to get the registered states of a number of files in the vcs. |
|
1048 |
|
1049 <b>Note:</b> If a shortcut is to be taken, the code will only check, if the named |
|
1050 directory has been scanned already. If so, it is assumed, that the states for |
|
1051 all files has been populated by the previous run. |
|
1052 |
|
1053 @param names dictionary with all filenames to be checked as keys |
|
1054 @param dname directory to check in (string) |
|
1055 @param shortcut flag indicating a shortcut should be taken (boolean) |
|
1056 @return the received dictionary completed with a combination of |
|
1057 canBeCommited and canBeAdded or None in order to signal an error |
|
1058 """ |
|
1059 if not os.path.isdir(os.path.join(dname, self.adminDir)): |
|
1060 # not under version control -> do nothing |
|
1061 return names |
|
1062 |
|
1063 found = False |
|
1064 for name in self.statusCache.keys(): |
|
1065 if os.path.dirname(name) == dname: |
|
1066 if shortcut: |
|
1067 found = True |
|
1068 break |
|
1069 if name in names: |
|
1070 found = True |
|
1071 names[name] = self.statusCache[name] |
|
1072 |
|
1073 if not found: |
|
1074 ioEncoding = Preferences.getSystem("IOEncoding") |
|
1075 process = QProcess() |
|
1076 args = [] |
|
1077 args.append('status') |
|
1078 args.append('--verbose') |
|
1079 args.append('--non-interactive') |
|
1080 args.append(dname) |
|
1081 process.start('svn', args) |
|
1082 procStarted = process.waitForStarted() |
|
1083 if procStarted: |
|
1084 finished = process.waitForFinished(30000) |
|
1085 if finished and process.exitCode() == 0: |
|
1086 output = \ |
|
1087 unicode(process.readAllStandardOutput(), ioEncoding, 'replace') |
|
1088 for line in output.splitlines(): |
|
1089 if self.rx_status1.exactMatch(line): |
|
1090 flags = str(self.rx_status1.cap(1)) |
|
1091 path = self.rx_status1.cap(5).strip() |
|
1092 elif self.rx_status2.exactMatch(line): |
|
1093 flags = str(self.rx_status2.cap(1)) |
|
1094 path = self.rx_status2.cap(2).strip() |
|
1095 else: |
|
1096 continue |
|
1097 name = os.path.normcase(path) |
|
1098 if flags[0] not in "?I": |
|
1099 if name in names: |
|
1100 names[name] = self.canBeCommitted |
|
1101 self.statusCache[name] = self.canBeCommitted |
|
1102 else: |
|
1103 self.statusCache[name] = self.canBeAdded |
|
1104 |
|
1105 return names |
|
1106 |
|
1107 def clearStatusCache(self): |
|
1108 """ |
|
1109 Public method to clear the status cache. |
|
1110 """ |
|
1111 self.statusCache = {} |
|
1112 |
|
1113 def vcsName(self): |
|
1114 """ |
|
1115 Public method returning the name of the vcs. |
|
1116 |
|
1117 @return always 'Subversion' (string) |
|
1118 """ |
|
1119 return "Subversion" |
|
1120 |
|
1121 def vcsCleanup(self, name): |
|
1122 """ |
|
1123 Public method used to cleanup the working copy. |
|
1124 |
|
1125 @param name directory name to be cleaned up (string) |
|
1126 """ |
|
1127 args = [] |
|
1128 args.append('cleanup') |
|
1129 self.addArguments(args, self.options['global']) |
|
1130 args.append(name) |
|
1131 |
|
1132 dia = SvnDialog(self.trUtf8('Cleaning up {0}') |
|
1133 .format(name)) |
|
1134 res = dia.startProcess(args) |
|
1135 if res: |
|
1136 dia.exec_() |
|
1137 |
|
1138 def vcsCommandLine(self, name): |
|
1139 """ |
|
1140 Public method used to execute arbitrary subversion commands. |
|
1141 |
|
1142 @param name directory name of the working directory (string) |
|
1143 """ |
|
1144 dlg = SvnCommandDialog(self.commandHistory, self.wdHistory, name) |
|
1145 if dlg.exec_() == QDialog.Accepted: |
|
1146 command, wd = dlg.getData() |
|
1147 commandList = Utilities.parseOptionString(command) |
|
1148 |
|
1149 # This moves any previous occurrence of these arguments to the head |
|
1150 # of the list. |
|
1151 if command in self.commandHistory: |
|
1152 self.commandHistory.remove(command) |
|
1153 self.commandHistory.insert(0, command) |
|
1154 if wd in self.wdHistory: |
|
1155 self.wdHistory.remove(wd) |
|
1156 self.wdHistory.insert(0, wd) |
|
1157 |
|
1158 args = [] |
|
1159 self.addArguments(args, commandList) |
|
1160 |
|
1161 dia = SvnDialog(self.trUtf8('Subversion command')) |
|
1162 res = dia.startProcess(args, wd) |
|
1163 if res: |
|
1164 dia.exec_() |
|
1165 |
|
1166 def vcsOptionsDialog(self, project, archive, editable = False, parent = None): |
|
1167 """ |
|
1168 Public method to get a dialog to enter repository info. |
|
1169 |
|
1170 @param project reference to the project object |
|
1171 @param archive name of the project in the repository (string) |
|
1172 @param editable flag indicating that the project name is editable (boolean) |
|
1173 @param parent parent widget (QWidget) |
|
1174 """ |
|
1175 return SvnOptionsDialog(self, project, parent) |
|
1176 |
|
1177 def vcsNewProjectOptionsDialog(self, parent = None): |
|
1178 """ |
|
1179 Public method to get a dialog to enter repository info for getting a new project. |
|
1180 |
|
1181 @param parent parent widget (QWidget) |
|
1182 """ |
|
1183 return SvnNewProjectOptionsDialog(self, parent) |
|
1184 |
|
1185 def vcsRepositoryInfos(self, ppath): |
|
1186 """ |
|
1187 Public method to retrieve information about the repository. |
|
1188 |
|
1189 @param ppath local path to get the repository infos (string) |
|
1190 @return string with ready formated info for display (string) |
|
1191 """ |
|
1192 info = {\ |
|
1193 'committed-rev' : '', |
|
1194 'committed-date' : '', |
|
1195 'committed-time' : '', |
|
1196 'url' : '', |
|
1197 'last-author' : '', |
|
1198 'revision' : '' |
|
1199 } |
|
1200 |
|
1201 ioEncoding = Preferences.getSystem("IOEncoding") |
|
1202 |
|
1203 process = QProcess() |
|
1204 args = [] |
|
1205 args.append('info') |
|
1206 args.append('--non-interactive') |
|
1207 args.append('--xml') |
|
1208 args.append(ppath) |
|
1209 process.start('svn', args) |
|
1210 procStarted = process.waitForStarted() |
|
1211 if procStarted: |
|
1212 finished = process.waitForFinished(30000) |
|
1213 if finished and process.exitCode() == 0: |
|
1214 output = unicode(process.readAllStandardOutput(), ioEncoding, 'replace') |
|
1215 entryFound = False |
|
1216 commitFound = False |
|
1217 for line in output.splitlines(): |
|
1218 line = line.strip() |
|
1219 if line.startswith('<entry'): |
|
1220 entryFound = True |
|
1221 elif line.startswith('<commit'): |
|
1222 commitFound = True |
|
1223 elif line.startswith('</commit>'): |
|
1224 commitFound = False |
|
1225 elif line.startswith("revision="): |
|
1226 rev = line[line.find('"')+1:line.rfind('"')] |
|
1227 if entryFound: |
|
1228 info['revision'] = rev |
|
1229 entryFound = False |
|
1230 elif commitFound: |
|
1231 info['committed-rev'] = rev |
|
1232 elif line.startswith('<url>'): |
|
1233 info['url'] = \ |
|
1234 line.replace('<url>', '').replace('</url>', '') |
|
1235 elif line.startswith('<author>'): |
|
1236 info['last-author'] = \ |
|
1237 line.replace('<author>', '').replace('</author>', '') |
|
1238 elif line.startswith('<date>'): |
|
1239 value = line.replace('<date>', '').replace('</date>', '') |
|
1240 date, time = value.split('T') |
|
1241 info['committed-date'] = date |
|
1242 info['committed-time'] = "%s%s" % (time.split('.')[0], time[-1]) |
|
1243 |
|
1244 return QApplication.translate('subversion', |
|
1245 """<h3>Repository information</h3>""" |
|
1246 """<table>""" |
|
1247 """<tr><td><b>Subversion V.</b></td><td>{0}</td></tr>""" |
|
1248 """<tr><td><b>URL</b></td><td>{1}</td></tr>""" |
|
1249 """<tr><td><b>Current revision</b></td><td>{2}</td></tr>""" |
|
1250 """<tr><td><b>Committed revision</b></td><td>{3}</td></tr>""" |
|
1251 """<tr><td><b>Committed date</b></td><td>{4}</td></tr>""" |
|
1252 """<tr><td><b>Comitted time</b></td><td>{5}</td></tr>""" |
|
1253 """<tr><td><b>Last author</b></td><td>{6}</td></tr>""" |
|
1254 """</table>""" |
|
1255 )\ |
|
1256 .format(self.versionStr, |
|
1257 info['url'], |
|
1258 info['revision'], |
|
1259 info['committed-rev'], |
|
1260 info['committed-date'], |
|
1261 info['committed-time'], |
|
1262 info['last-author']) |
|
1263 |
|
1264 ############################################################################ |
|
1265 ## Public Subversion specific methods are below. |
|
1266 ############################################################################ |
|
1267 |
|
1268 def svnGetReposName(self, path): |
|
1269 """ |
|
1270 Public method used to retrieve the URL of the subversion repository path. |
|
1271 |
|
1272 @param path local path to get the svn repository path for (string) |
|
1273 @return string with the repository path URL |
|
1274 """ |
|
1275 ioEncoding = Preferences.getSystem("IOEncoding") |
|
1276 |
|
1277 process = QProcess() |
|
1278 args = [] |
|
1279 args.append('info') |
|
1280 args.append('--xml') |
|
1281 args.append('--non-interactive') |
|
1282 args.append(path) |
|
1283 process.start('svn', args) |
|
1284 procStarted = process.waitForStarted() |
|
1285 if procStarted: |
|
1286 finished = process.waitForFinished(30000) |
|
1287 if finished and process.exitCode() == 0: |
|
1288 output = unicode(process.readAllStandardOutput(), ioEncoding, 'replace') |
|
1289 for line in output.splitlines(): |
|
1290 line = line.strip() |
|
1291 if line.startswith('<url>'): |
|
1292 reposURL = line.replace('<url>', '').replace('</url>', '') |
|
1293 return reposURL |
|
1294 |
|
1295 return None |
|
1296 |
|
1297 def svnResolve(self, name): |
|
1298 """ |
|
1299 Public method used to resolve conflicts of a file/directory. |
|
1300 |
|
1301 @param name file/directory name to be resolved (string) |
|
1302 """ |
|
1303 args = [] |
|
1304 if self.versionStr >= '1.5.0': |
|
1305 args.append('resolve') |
|
1306 args.append('--accept') |
|
1307 args.append('working') |
|
1308 else: |
|
1309 args.append('resolved') |
|
1310 self.addArguments(args, self.options['global']) |
|
1311 if type(name) is types.ListType: |
|
1312 self.addArguments(args, name) |
|
1313 else: |
|
1314 if os.path.isdir(name): |
|
1315 args.append('--recursive') |
|
1316 args.append(name) |
|
1317 |
|
1318 dia = SvnDialog(self.trUtf8('Resolving conficts')) |
|
1319 res = dia.startProcess(args) |
|
1320 if res: |
|
1321 dia.exec_() |
|
1322 self.checkVCSStatus() |
|
1323 |
|
1324 def svnCopy(self, name, project): |
|
1325 """ |
|
1326 Public method used to copy a file/directory. |
|
1327 |
|
1328 @param name file/directory name to be copied (string) |
|
1329 @param project reference to the project object |
|
1330 @return flag indicating successfull operation (boolean) |
|
1331 """ |
|
1332 rx_prot = QRegExp('(file:|svn:|svn+ssh:|http:|https:).+') |
|
1333 dlg = SvnCopyDialog(name) |
|
1334 res = False |
|
1335 if dlg.exec_() == QDialog.Accepted: |
|
1336 target, force = dlg.getData() |
|
1337 |
|
1338 args = [] |
|
1339 args.append('copy') |
|
1340 self.addArguments(args, self.options['global']) |
|
1341 if rx_prot.exactMatch(target): |
|
1342 args.append('--message') |
|
1343 args.append('Copying {0} to {1}'.format(name, target)) |
|
1344 target = self.__svnURL(target) |
|
1345 args.append(name) |
|
1346 args.append(target) |
|
1347 |
|
1348 dia = SvnDialog(self.trUtf8('Copying {0}') |
|
1349 .format(name)) |
|
1350 res = dia.startProcess(args) |
|
1351 if res: |
|
1352 dia.exec_() |
|
1353 res = dia.normalExit() |
|
1354 if res and \ |
|
1355 not rx_prot.exactMatch(target) and \ |
|
1356 target.startswith(project.getProjectPath()): |
|
1357 if os.path.isdir(name): |
|
1358 project.copyDirectory(name, target) |
|
1359 else: |
|
1360 project.appendFile(target) |
|
1361 return res |
|
1362 |
|
1363 def svnListProps(self, name, recursive = False): |
|
1364 """ |
|
1365 Public method used to list the properties of a file/directory. |
|
1366 |
|
1367 @param name file/directory name (string or list of strings) |
|
1368 @param recursive flag indicating a recursive list is requested |
|
1369 """ |
|
1370 self.propList = SvnPropListDialog(self) |
|
1371 self.propList.show() |
|
1372 self.propList.start(name, recursive) |
|
1373 |
|
1374 def svnSetProp(self, name, recursive = False): |
|
1375 """ |
|
1376 Public method used to add a property to a file/directory. |
|
1377 |
|
1378 @param name file/directory name (string or list of strings) |
|
1379 @param recursive flag indicating a recursive list is requested |
|
1380 """ |
|
1381 dlg = SvnPropSetDialog() |
|
1382 if dlg.exec_() == QDialog.Accepted: |
|
1383 propName, fileFlag, propValue = dlg.getData() |
|
1384 if not propName: |
|
1385 QMessageBox.critical(None, |
|
1386 self.trUtf8("Subversion Set Property"), |
|
1387 self.trUtf8("""You have to supply a property name. Aborting.""")) |
|
1388 return |
|
1389 |
|
1390 args = [] |
|
1391 args.append('propset') |
|
1392 self.addArguments(args, self.options['global']) |
|
1393 if recursive: |
|
1394 args.append('--recursive') |
|
1395 args.append(propName) |
|
1396 if fileFlag: |
|
1397 args.append('--file') |
|
1398 args.append(propValue) |
|
1399 if type(name) is types.ListType: |
|
1400 dname, fnames = self.splitPathList(name) |
|
1401 self.addArguments(args, fnames) |
|
1402 else: |
|
1403 dname, fname = self.splitPath(name) |
|
1404 args.append(fname) |
|
1405 |
|
1406 dia = SvnDialog(self.trUtf8('Subversion Set Property')) |
|
1407 res = dia.startProcess(args, dname) |
|
1408 if res: |
|
1409 dia.exec_() |
|
1410 |
|
1411 def svnDelProp(self, name, recursive = False): |
|
1412 """ |
|
1413 Public method used to delete a property of a file/directory. |
|
1414 |
|
1415 @param name file/directory name (string or list of strings) |
|
1416 @param recursive flag indicating a recursive list is requested |
|
1417 """ |
|
1418 propName, ok = QInputDialog.getText(\ |
|
1419 None, |
|
1420 self.trUtf8("Subversion Delete Property"), |
|
1421 self.trUtf8("Enter property name"), |
|
1422 QLineEdit.Normal) |
|
1423 |
|
1424 if not ok: |
|
1425 return |
|
1426 |
|
1427 if not propName: |
|
1428 QMessageBox.critical(None, |
|
1429 self.trUtf8("Subversion Delete Property"), |
|
1430 self.trUtf8("""You have to supply a property name. Aborting.""")) |
|
1431 return |
|
1432 |
|
1433 args = [] |
|
1434 args.append('propdel') |
|
1435 self.addArguments(args, self.options['global']) |
|
1436 if recursive: |
|
1437 args.append('--recursive') |
|
1438 args.append(propName) |
|
1439 if type(name) is types.ListType: |
|
1440 dname, fnames = self.splitPathList(name) |
|
1441 self.addArguments(args, fnames) |
|
1442 else: |
|
1443 dname, fname = self.splitPath(name) |
|
1444 args.append(fname) |
|
1445 |
|
1446 dia = SvnDialog(self.trUtf8('Subversion Delete Property')) |
|
1447 res = dia.startProcess(args, dname) |
|
1448 if res: |
|
1449 dia.exec_() |
|
1450 |
|
1451 def svnListTagBranch(self, path, tags = True): |
|
1452 """ |
|
1453 Public method used to list the available tags or branches. |
|
1454 |
|
1455 @param path directory name of the project (string) |
|
1456 @param tags flag indicating listing of branches or tags |
|
1457 (False = branches, True = tags) |
|
1458 """ |
|
1459 self.tagbranchList = SvnTagBranchListDialog(self) |
|
1460 self.tagbranchList.show() |
|
1461 if tags: |
|
1462 if not self.showedTags: |
|
1463 self.showedTags = True |
|
1464 allTagsBranchesList = self.allTagsBranchesList |
|
1465 else: |
|
1466 self.tagsList = [] |
|
1467 allTagsBranchesList = None |
|
1468 self.tagbranchList.start(path, tags, |
|
1469 self.tagsList, allTagsBranchesList) |
|
1470 elif not tags: |
|
1471 if not self.showedBranches: |
|
1472 self.showedBranches = True |
|
1473 allTagsBranchesList = self.allTagsBranchesList |
|
1474 else: |
|
1475 self.branchesList = [] |
|
1476 allTagsBranchesList = None |
|
1477 self.tagbranchList.start(path, tags, |
|
1478 self.branchesList, self.allTagsBranchesList) |
|
1479 |
|
1480 def svnBlame(self, name): |
|
1481 """ |
|
1482 Public method to show the output of the svn blame command. |
|
1483 |
|
1484 @param name file name to show the blame for (string) |
|
1485 """ |
|
1486 self.blame = SvnBlameDialog(self) |
|
1487 self.blame.show() |
|
1488 self.blame.start(name) |
|
1489 |
|
1490 def svnExtendedDiff(self, name): |
|
1491 """ |
|
1492 Public method used to view the difference of a file/directory to the |
|
1493 Subversion repository. |
|
1494 |
|
1495 If name is a directory and is the project directory, all project files |
|
1496 are saved first. If name is a file (or list of files), which is/are being edited |
|
1497 and has unsaved modification, they can be saved or the operation may be aborted. |
|
1498 |
|
1499 This method gives the chance to enter the revisions to be compared. |
|
1500 |
|
1501 @param name file/directory name to be diffed (string) |
|
1502 """ |
|
1503 if type(name) is types.ListType: |
|
1504 names = name[:] |
|
1505 else: |
|
1506 names = [name] |
|
1507 for nam in names: |
|
1508 if os.path.isfile(nam): |
|
1509 editor = e4App().getObject("ViewManager").getOpenEditor(nam) |
|
1510 if editor and not editor.checkDirty() : |
|
1511 return |
|
1512 else: |
|
1513 project = e4App().getObject("Project") |
|
1514 if nam == project.ppath and not project.saveAllScripts(): |
|
1515 return |
|
1516 dlg = SvnRevisionSelectionDialog() |
|
1517 if dlg.exec_() == QDialog.Accepted: |
|
1518 revisions = dlg.getRevisions() |
|
1519 self.diff = SvnDiffDialog(self) |
|
1520 self.diff.show() |
|
1521 self.diff.start(name, revisions) |
|
1522 |
|
1523 def svnUrlDiff(self, name): |
|
1524 """ |
|
1525 Public method used to view the difference of a file/directory of two |
|
1526 repository URLs. |
|
1527 |
|
1528 If name is a directory and is the project directory, all project files |
|
1529 are saved first. If name is a file (or list of files), which is/are being edited |
|
1530 and has unsaved modification, they can be saved or the operation may be aborted. |
|
1531 |
|
1532 This method gives the chance to enter the revisions to be compared. |
|
1533 |
|
1534 @param name file/directory name to be diffed (string) |
|
1535 """ |
|
1536 if type(name) is types.ListType: |
|
1537 names = name[:] |
|
1538 else: |
|
1539 names = [name] |
|
1540 for nam in names: |
|
1541 if os.path.isfile(nam): |
|
1542 editor = e4App().getObject("ViewManager").getOpenEditor(nam) |
|
1543 if editor and not editor.checkDirty() : |
|
1544 return |
|
1545 else: |
|
1546 project = e4App().getObject("Project") |
|
1547 if nam == project.ppath and not project.saveAllScripts(): |
|
1548 return |
|
1549 |
|
1550 dname = self.splitPath(names[0])[0] |
|
1551 |
|
1552 dlg = SvnUrlSelectionDialog(self, self.tagsList, self.branchesList, dname) |
|
1553 if dlg.exec_() == QDialog.Accepted: |
|
1554 urls, summary = dlg.getURLs() |
|
1555 self.diff = SvnDiffDialog(self) |
|
1556 self.diff.show() |
|
1557 QApplication.processEvents() |
|
1558 self.diff.start(name, urls = urls, summary = summary) |
|
1559 |
|
1560 def svnLogLimited(self, name): |
|
1561 """ |
|
1562 Public method used to view the (limited) log of a file/directory from the |
|
1563 Subversion repository. |
|
1564 |
|
1565 @param name file/directory name to show the log of (string) |
|
1566 """ |
|
1567 noEntries, ok = QInputDialog.getInteger(\ |
|
1568 None, |
|
1569 self.trUtf8("Subversion Log"), |
|
1570 self.trUtf8("Select number of entries to show."), |
|
1571 self.getPlugin().getPreferences("LogLimit"), 1, 999999, 1) |
|
1572 if ok: |
|
1573 self.log = SvnLogDialog(self) |
|
1574 self.log.show() |
|
1575 self.log.start(name, noEntries) |
|
1576 |
|
1577 def svnLogBrowser(self, path): |
|
1578 """ |
|
1579 Public method used to browse the log of a file/directory from the |
|
1580 Subversion repository. |
|
1581 |
|
1582 @param path file/directory name to show the log of (string) |
|
1583 """ |
|
1584 self.logBrowser = SvnLogBrowserDialog(self) |
|
1585 self.logBrowser.show() |
|
1586 self.logBrowser.start(path) |
|
1587 |
|
1588 def svnLock(self, name, stealIt=False, parent=None): |
|
1589 """ |
|
1590 Public method used to lock a file in the Subversion repository. |
|
1591 |
|
1592 @param name file/directory name to be locked (string or list of strings) |
|
1593 @param stealIt flag indicating a forced operation (boolean) |
|
1594 @param parent reference to the parent object of the subversion dialog (QWidget) |
|
1595 """ |
|
1596 args = [] |
|
1597 args.append('lock') |
|
1598 self.addArguments(args, self.options['global']) |
|
1599 if stealIt: |
|
1600 args.append('--force') |
|
1601 if type(name) is types.ListType: |
|
1602 dname, fnames = self.splitPathList(name) |
|
1603 self.addArguments(args, fnames) |
|
1604 else: |
|
1605 dname, fname = self.splitPath(name) |
|
1606 args.append(fname) |
|
1607 |
|
1608 dia = SvnDialog(self.trUtf8('Locking in the Subversion repository'), parent) |
|
1609 res = dia.startProcess(args, dname) |
|
1610 if res: |
|
1611 dia.exec_() |
|
1612 |
|
1613 def svnUnlock(self, name, breakIt=False, parent=None): |
|
1614 """ |
|
1615 Public method used to unlock a file in the Subversion repository. |
|
1616 |
|
1617 @param name file/directory name to be unlocked (string or list of strings) |
|
1618 @param breakIt flag indicating a forced operation (boolean) |
|
1619 @param parent reference to the parent object of the subversion dialog (QWidget) |
|
1620 """ |
|
1621 args = [] |
|
1622 args.append('unlock') |
|
1623 self.addArguments(args, self.options['global']) |
|
1624 if breakIt: |
|
1625 args.append('--force') |
|
1626 if type(name) is types.ListType: |
|
1627 dname, fnames = self.splitPathList(name) |
|
1628 self.addArguments(args, fnames) |
|
1629 else: |
|
1630 dname, fname = self.splitPath(name) |
|
1631 args.append(fname) |
|
1632 |
|
1633 dia = SvnDialog(self.trUtf8('Unlocking in the Subversion repository'), parent) |
|
1634 res = dia.startProcess(args, dname) |
|
1635 if res: |
|
1636 dia.exec_() |
|
1637 |
|
1638 def svnRelocate(self, projectPath): |
|
1639 """ |
|
1640 Public method to relocate the working copy to a new repository URL. |
|
1641 |
|
1642 @param projectPath path name of the project (string) |
|
1643 """ |
|
1644 currUrl = self.svnGetReposName(projectPath) |
|
1645 dlg = SvnRelocateDialog(currUrl) |
|
1646 if dlg.exec_() == QDialog.Accepted: |
|
1647 newUrl, inside = dlg.getData() |
|
1648 args = [] |
|
1649 args.append('switch') |
|
1650 if not inside: |
|
1651 args.append('--relocate') |
|
1652 args.append(currUrl) |
|
1653 args.append(newUrl) |
|
1654 args.append(projectPath) |
|
1655 |
|
1656 dia = SvnDialog(self.trUtf8('Relocating')) |
|
1657 res = dia.startProcess(args) |
|
1658 if res: |
|
1659 dia.exec_() |
|
1660 |
|
1661 def svnRepoBrowser(self, projectPath = None): |
|
1662 """ |
|
1663 Public method to open the repository browser. |
|
1664 |
|
1665 @param projectPath path name of the project (string) |
|
1666 """ |
|
1667 if projectPath: |
|
1668 url = self.svnGetReposName(projectPath) |
|
1669 else: |
|
1670 url = None |
|
1671 |
|
1672 if url is None: |
|
1673 url, ok = QInputDialog.getText(\ |
|
1674 None, |
|
1675 self.trUtf8("Repository Browser"), |
|
1676 self.trUtf8("Enter the repository URL."), |
|
1677 QLineEdit.Normal) |
|
1678 if not ok or not url: |
|
1679 return |
|
1680 |
|
1681 self.repoBrowser = SvnRepoBrowserDialog(self) |
|
1682 self.repoBrowser.show() |
|
1683 self.repoBrowser.start(url) |
|
1684 |
|
1685 def svnRemoveFromChangelist(self, names): |
|
1686 """ |
|
1687 Public method to remove a file or directory from it's changelist. |
|
1688 |
|
1689 Note: Directories will be removed recursively. |
|
1690 |
|
1691 @param names name or list of names of file or directory to remove |
|
1692 (string) |
|
1693 """ |
|
1694 args = [] |
|
1695 args.append('changelist') |
|
1696 self.addArguments(args, self.options['global']) |
|
1697 args.append('--remove') |
|
1698 args.append('--recursive') |
|
1699 if type(names) is types.ListType: |
|
1700 dname, fnames = self.splitPathList(names) |
|
1701 self.addArguments(args, fnames) |
|
1702 else: |
|
1703 dname, fname = self.splitPath(names) |
|
1704 args.append(fname) |
|
1705 |
|
1706 dia = SvnDialog(self.trUtf8('Remove from changelist')) |
|
1707 res = dia.startProcess(args, dname) |
|
1708 if res: |
|
1709 dia.exec_() |
|
1710 |
|
1711 def svnAddToChangelist(self, names): |
|
1712 """ |
|
1713 Public method to add a file or directory to a changelist. |
|
1714 |
|
1715 Note: Directories will be added recursively. |
|
1716 |
|
1717 @param names name or list of names of file or directory to add |
|
1718 (string) |
|
1719 """ |
|
1720 clname, ok = QInputDialog.getText(\ |
|
1721 None, |
|
1722 self.trUtf8("Add to changelist"), |
|
1723 self.trUtf8("Enter name of the changelist:"), |
|
1724 QLineEdit.Normal) |
|
1725 if not ok or not clname: |
|
1726 return |
|
1727 |
|
1728 args = [] |
|
1729 args.append('changelist') |
|
1730 self.addArguments(args, self.options['global']) |
|
1731 args.append('--recursive') |
|
1732 args.append(clname) |
|
1733 if type(names) is types.ListType: |
|
1734 dname, fnames = self.splitPathList(names) |
|
1735 self.addArguments(args, fnames) |
|
1736 else: |
|
1737 dname, fname = self.splitPath(names) |
|
1738 args.append(fname) |
|
1739 |
|
1740 dia = SvnDialog(self.trUtf8('Remove from changelist')) |
|
1741 res = dia.startProcess(args, dname) |
|
1742 if res: |
|
1743 dia.exec_() |
|
1744 |
|
1745 ############################################################################ |
|
1746 ## Private Subversion specific methods are below. |
|
1747 ############################################################################ |
|
1748 |
|
1749 def __svnURL(self, url): |
|
1750 """ |
|
1751 Private method to format a url for subversion. |
|
1752 |
|
1753 @param url unformatted url string (string) |
|
1754 @return properly formated url for subversion (string) |
|
1755 """ |
|
1756 url = self.svnNormalizeURL(url) |
|
1757 url = url.split(':', 2) |
|
1758 if len(url) == 3: |
|
1759 scheme = url[0] |
|
1760 host = url[1] |
|
1761 port, path = url[2].split("/",1) |
|
1762 return "%s:%s:%s/%s" % (scheme, host, port, urllib.quote(path)) |
|
1763 else: |
|
1764 scheme = url[0] |
|
1765 if scheme == "file": |
|
1766 return "%s:%s" % (scheme, urllib.quote(url[1])) |
|
1767 else: |
|
1768 host, path = url[1][2:].split("/",1) |
|
1769 return "%s://%s/%s" % (scheme, host, urllib.quote(path)) |
|
1770 |
|
1771 def svnNormalizeURL(self, url): |
|
1772 """ |
|
1773 Public method to normalize a url for subversion. |
|
1774 |
|
1775 @param url url string (string) |
|
1776 @return properly normalized url for subversion (string) |
|
1777 """ |
|
1778 url = url.replace('\\', '/') |
|
1779 if url.endswith('/'): |
|
1780 url = url[:-1] |
|
1781 urll = url.split('//') |
|
1782 return "%s//%s" % (urll[0], '/'.join(urll[1:])) |
|
1783 |
|
1784 ############################################################################ |
|
1785 ## Methods to get the helper objects are below. |
|
1786 ############################################################################ |
|
1787 |
|
1788 def vcsGetProjectBrowserHelper(self, browser, project, isTranslationsBrowser = False): |
|
1789 """ |
|
1790 Public method to instanciate a helper object for the different project browsers. |
|
1791 |
|
1792 @param browser reference to the project browser object |
|
1793 @param project reference to the project object |
|
1794 @param isTranslationsBrowser flag indicating, the helper is requested for the |
|
1795 translations browser (this needs some special treatment) |
|
1796 @return the project browser helper object |
|
1797 """ |
|
1798 return SvnProjectBrowserHelper(self, browser, project, isTranslationsBrowser) |
|
1799 |
|
1800 def vcsGetProjectHelper(self, project): |
|
1801 """ |
|
1802 Public method to instanciate a helper object for the project. |
|
1803 |
|
1804 @param project reference to the project object |
|
1805 @return the project helper object |
|
1806 """ |
|
1807 helper = self.__plugin.getProjectHelper() |
|
1808 helper.setObjects(self, project) |
|
1809 return helper |
|
1810 |
|
1811 ############################################################################ |
|
1812 ## Status Monitor Thread methods |
|
1813 ############################################################################ |
|
1814 |
|
1815 def _createStatusMonitorThread(self, interval, project): |
|
1816 """ |
|
1817 Protected method to create an instance of the VCS status monitor thread. |
|
1818 |
|
1819 @param project reference to the project object |
|
1820 @param interval check interval for the monitor thread in seconds (integer) |
|
1821 @return reference to the monitor thread (QThread) |
|
1822 """ |
|
1823 return SvnStatusMonitorThread(interval, project.ppath, self) |