eric6/Plugins/VcsPlugins/vcsSubversion/SvnRepoBrowserDialog.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7192
a22eee00b052
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2007 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the subversion repository browser dialog.
8 """
9
10 from __future__ import unicode_literals
11 try:
12 str = unicode
13 except NameError:
14 pass
15
16 import os
17
18 from PyQt5.QtGui import QCursor
19 from PyQt5.QtWidgets import QHeaderView, QLineEdit, QDialog, QApplication, \
20 QDialogButtonBox, QTreeWidgetItem
21 from PyQt5.QtCore import QTimer, QProcess, QRegExp, Qt, pyqtSlot
22
23 from E5Gui import E5MessageBox
24
25 from .Ui_SvnRepoBrowserDialog import Ui_SvnRepoBrowserDialog
26
27 import UI.PixmapCache
28
29 import Preferences
30 from Globals import strToQByteArray
31
32
33 class SvnRepoBrowserDialog(QDialog, Ui_SvnRepoBrowserDialog):
34 """
35 Class implementing the subversion repository browser dialog.
36 """
37 def __init__(self, vcs, mode="browse", parent=None):
38 """
39 Constructor
40
41 @param vcs reference to the vcs object
42 @param mode mode of the dialog (string, "browse" or "select")
43 @param parent parent widget (QWidget)
44 """
45 super(SvnRepoBrowserDialog, self).__init__(parent)
46 self.setupUi(self)
47 self.setWindowFlags(Qt.Window)
48
49 self.repoTree.headerItem().setText(self.repoTree.columnCount(), "")
50 self.repoTree.header().setSortIndicator(0, Qt.AscendingOrder)
51
52 self.vcs = vcs
53 self.mode = mode
54
55 self.process = QProcess()
56 self.process.finished.connect(self.__procFinished)
57 self.process.readyReadStandardOutput.connect(self.__readStdout)
58 self.process.readyReadStandardError.connect(self.__readStderr)
59
60 if self.mode == "select":
61 self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)
62 self.buttonBox.button(QDialogButtonBox.Close).hide()
63 else:
64 self.buttonBox.button(QDialogButtonBox.Ok).hide()
65 self.buttonBox.button(QDialogButtonBox.Cancel).hide()
66
67 self.__dirIcon = UI.PixmapCache.getIcon("dirClosed.png")
68 self.__fileIcon = UI.PixmapCache.getIcon("fileMisc.png")
69
70 self.__urlRole = Qt.UserRole
71 self.__ignoreExpand = False
72 self.intercept = False
73
74 self.__rx_dir = QRegExp(
75 r"""\s*([0-9]+)\s+(\w+)\s+"""
76 r"""((?:\w+\s+\d+|[0-9.]+\s+\w+)\s+[0-9:]+)\s+(.+)\s*""")
77 self.__rx_file = QRegExp(
78 r"""\s*([0-9]+)\s+(\w+)\s+([0-9]+)\s"""
79 r"""((?:\w+\s+\d+|[0-9.]+\s+\w+)\s+[0-9:]+)\s+(.+)\s*""")
80
81 def closeEvent(self, e):
82 """
83 Protected slot implementing a close event handler.
84
85 @param e close event (QCloseEvent)
86 """
87 if self.process is not None and \
88 self.process.state() != QProcess.NotRunning:
89 self.process.terminate()
90 QTimer.singleShot(2000, self.process.kill)
91 self.process.waitForFinished(3000)
92
93 e.accept()
94
95 def __resort(self):
96 """
97 Private method to resort the tree.
98 """
99 self.repoTree.sortItems(
100 self.repoTree.sortColumn(),
101 self.repoTree.header().sortIndicatorOrder())
102
103 def __resizeColumns(self):
104 """
105 Private method to resize the tree columns.
106 """
107 self.repoTree.header().resizeSections(QHeaderView.ResizeToContents)
108 self.repoTree.header().setStretchLastSection(True)
109
110 def __generateItem(self, repopath, revision, author, size, date,
111 nodekind, url):
112 """
113 Private method to generate a tree item in the repository tree.
114
115 @param repopath path of the item (string)
116 @param revision revision info (string)
117 @param author author info (string)
118 @param size size info (string)
119 @param date date info (string)
120 @param nodekind node kind info (string, "dir" or "file")
121 @param url url of the entry (string)
122 @return reference to the generated item (QTreeWidgetItem)
123 """
124 path = repopath
125
126 if revision == "":
127 rev = ""
128 else:
129 rev = int(revision)
130 if size == "":
131 sz = ""
132 else:
133 sz = int(size)
134
135 itm = QTreeWidgetItem(self.parentItem)
136 itm.setData(0, Qt.DisplayRole, path)
137 itm.setData(1, Qt.DisplayRole, rev)
138 itm.setData(2, Qt.DisplayRole, author)
139 itm.setData(3, Qt.DisplayRole, sz)
140 itm.setData(4, Qt.DisplayRole, date)
141
142 if nodekind == "dir":
143 itm.setIcon(0, self.__dirIcon)
144 itm.setChildIndicatorPolicy(QTreeWidgetItem.ShowIndicator)
145 elif nodekind == "file":
146 itm.setIcon(0, self.__fileIcon)
147
148 itm.setData(0, self.__urlRole, url)
149
150 itm.setTextAlignment(0, Qt.AlignLeft)
151 itm.setTextAlignment(1, Qt.AlignRight)
152 itm.setTextAlignment(2, Qt.AlignLeft)
153 itm.setTextAlignment(3, Qt.AlignRight)
154 itm.setTextAlignment(4, Qt.AlignLeft)
155
156 return itm
157
158 def __repoRoot(self, url):
159 """
160 Private method to get the repository root using the svn info command.
161
162 @param url the repository URL to browser (string)
163 @return repository root (string)
164 """
165 ioEncoding = Preferences.getSystem("IOEncoding")
166 repoRoot = None
167
168 process = QProcess()
169
170 args = []
171 args.append('info')
172 self.vcs.addArguments(args, self.vcs.options['global'])
173 args.append('--xml')
174 args.append(url)
175
176 process.start('svn', args)
177 procStarted = process.waitForStarted(5000)
178 if procStarted:
179 finished = process.waitForFinished(30000)
180 if finished:
181 if process.exitCode() == 0:
182 output = str(process.readAllStandardOutput(), ioEncoding,
183 'replace')
184 for line in output.splitlines():
185 line = line.strip()
186 if line.startswith('<root>'):
187 repoRoot = line.replace('<root>', '')\
188 .replace('</root>', '')
189 break
190 else:
191 error = str(process.readAllStandardError(),
192 Preferences.getSystem("IOEncoding"),
193 'replace')
194 self.errors.insertPlainText(error)
195 self.errors.ensureCursorVisible()
196 else:
197 QApplication.restoreOverrideCursor()
198 E5MessageBox.critical(
199 self,
200 self.tr('Process Generation Error'),
201 self.tr(
202 'The process {0} could not be started. '
203 'Ensure, that it is in the search path.'
204 ).format('svn'))
205 return repoRoot
206
207 def __listRepo(self, url, parent=None):
208 """
209 Private method to perform the svn list command.
210
211 @param url the repository URL to browse (string)
212 @param parent reference to the item, the data should be appended to
213 (QTreeWidget or QTreeWidgetItem)
214 """
215 self.errorGroup.hide()
216
217 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
218 QApplication.processEvents()
219
220 self.repoUrl = url
221
222 if parent is None:
223 self.parentItem = self.repoTree
224 else:
225 self.parentItem = parent
226
227 if self.parentItem == self.repoTree:
228 repoRoot = self.__repoRoot(url)
229 if repoRoot is None:
230 self.__finish()
231 return
232 self.__ignoreExpand = True
233 itm = self.__generateItem(
234 repoRoot, "", "", "", "", "dir", repoRoot)
235 itm.setExpanded(True)
236 self.parentItem = itm
237 urlPart = repoRoot
238 for element in url.replace(repoRoot, "").split("/"):
239 if element:
240 urlPart = "{0}/{1}".format(urlPart, element)
241 itm = self.__generateItem(
242 element, "", "", "", "", "dir", urlPart)
243 itm.setExpanded(True)
244 self.parentItem = itm
245 itm.setExpanded(False)
246 self.__ignoreExpand = False
247 self.__finish()
248 return
249
250 self.intercept = False
251
252 self.process.kill()
253
254 args = []
255 args.append('list')
256 self.vcs.addArguments(args, self.vcs.options['global'])
257 if '--verbose' not in self.vcs.options['global']:
258 args.append('--verbose')
259 args.append(url)
260
261 self.process.start('svn', args)
262 procStarted = self.process.waitForStarted(5000)
263 if not procStarted:
264 self.__finish()
265 self.inputGroup.setEnabled(False)
266 self.inputGroup.hide()
267 E5MessageBox.critical(
268 self,
269 self.tr('Process Generation Error'),
270 self.tr(
271 'The process {0} could not be started. '
272 'Ensure, that it is in the search path.'
273 ).format('svn'))
274 else:
275 self.inputGroup.setEnabled(True)
276 self.inputGroup.show()
277
278 def __normalizeUrl(self, url):
279 """
280 Private method to normalite the url.
281
282 @param url the url to normalize (string)
283 @return normalized URL (string)
284 """
285 if url.endswith("/"):
286 return url[:-1]
287 return url
288
289 def start(self, url):
290 """
291 Public slot to start the svn info command.
292
293 @param url the repository URL to browser (string)
294 """
295 self.repoTree.clear()
296
297 self.url = ""
298
299 url = self.__normalizeUrl(url)
300 if self.urlCombo.findText(url) == -1:
301 self.urlCombo.addItem(url)
302
303 @pyqtSlot(str)
304 def on_urlCombo_currentIndexChanged(self, text):
305 """
306 Private slot called, when a new repository URL is entered or selected.
307
308 @param text the text of the current item (string)
309 """
310 url = self.__normalizeUrl(text)
311 if url != self.url:
312 self.url = url
313 self.repoTree.clear()
314 self.__listRepo(url)
315
316 @pyqtSlot(QTreeWidgetItem)
317 def on_repoTree_itemExpanded(self, item):
318 """
319 Private slot called when an item is expanded.
320
321 @param item reference to the item to be expanded (QTreeWidgetItem)
322 """
323 if not self.__ignoreExpand:
324 url = item.data(0, self.__urlRole)
325 self.__listRepo(url, item)
326
327 @pyqtSlot(QTreeWidgetItem)
328 def on_repoTree_itemCollapsed(self, item):
329 """
330 Private slot called when an item is collapsed.
331
332 @param item reference to the item to be collapsed (QTreeWidgetItem)
333 """
334 for child in item.takeChildren():
335 del child
336
337 @pyqtSlot()
338 def on_repoTree_itemSelectionChanged(self):
339 """
340 Private slot called when the selection changes.
341 """
342 if self.mode == "select":
343 self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(True)
344
345 def accept(self):
346 """
347 Public slot called when the dialog is accepted.
348 """
349 if self.focusWidget() == self.urlCombo:
350 return
351
352 super(SvnRepoBrowserDialog, self).accept()
353
354 def getSelectedUrl(self):
355 """
356 Public method to retrieve the selected repository URL.
357
358 @return the selected repository URL (string)
359 """
360 items = self.repoTree.selectedItems()
361 if len(items) == 1:
362 return items[0].data(0, self.__urlRole)
363 else:
364 return ""
365
366 def __finish(self):
367 """
368 Private slot called when the process finished or the user pressed the
369 button.
370 """
371 if self.process is not None and \
372 self.process.state() != QProcess.NotRunning:
373 self.process.terminate()
374 QTimer.singleShot(2000, self.process.kill)
375 self.process.waitForFinished(3000)
376
377 self.inputGroup.setEnabled(False)
378 self.inputGroup.hide()
379
380 self.__resizeColumns()
381 self.__resort()
382 QApplication.restoreOverrideCursor()
383
384 def __procFinished(self, exitCode, exitStatus):
385 """
386 Private slot connected to the finished signal.
387
388 @param exitCode exit code of the process (integer)
389 @param exitStatus exit status of the process (QProcess.ExitStatus)
390 """
391 self.__finish()
392
393 def __readStdout(self):
394 """
395 Private slot to handle the readyReadStandardOutput signal.
396
397 It reads the output of the process, formats it and inserts it into
398 the contents pane.
399 """
400 if self.process is not None:
401 self.process.setReadChannel(QProcess.StandardOutput)
402
403 while self.process.canReadLine():
404 s = str(self.process.readLine(),
405 Preferences.getSystem("IOEncoding"),
406 'replace')
407 if self.__rx_dir.exactMatch(s):
408 revision = self.__rx_dir.cap(1)
409 author = self.__rx_dir.cap(2)
410 date = self.__rx_dir.cap(3)
411 name = self.__rx_dir.cap(4).strip()
412 if name.endswith("/"):
413 name = name[:-1]
414 size = ""
415 nodekind = "dir"
416 if name == ".":
417 continue
418 elif self.__rx_file.exactMatch(s):
419 revision = self.__rx_file.cap(1)
420 author = self.__rx_file.cap(2)
421 size = self.__rx_file.cap(3)
422 date = self.__rx_file.cap(4)
423 name = self.__rx_file.cap(5).strip()
424 nodekind = "file"
425 else:
426 continue
427 url = "{0}/{1}".format(self.repoUrl, name)
428 self.__generateItem(
429 name, revision, author, size, date, nodekind, url)
430
431 def __readStderr(self):
432 """
433 Private slot to handle the readyReadStandardError signal.
434
435 It reads the error output of the process and inserts it into the
436 error pane.
437 """
438 if self.process is not None:
439 s = str(self.process.readAllStandardError(),
440 Preferences.getSystem("IOEncoding"),
441 'replace')
442 self.errors.insertPlainText(s)
443 self.errors.ensureCursorVisible()
444 self.errorGroup.show()
445
446 def on_passwordCheckBox_toggled(self, isOn):
447 """
448 Private slot to handle the password checkbox toggled.
449
450 @param isOn flag indicating the status of the check box (boolean)
451 """
452 if isOn:
453 self.input.setEchoMode(QLineEdit.Password)
454 else:
455 self.input.setEchoMode(QLineEdit.Normal)
456
457 @pyqtSlot()
458 def on_sendButton_clicked(self):
459 """
460 Private slot to send the input to the subversion process.
461 """
462 inputTxt = self.input.text()
463 inputTxt += os.linesep
464
465 if self.passwordCheckBox.isChecked():
466 self.errors.insertPlainText(os.linesep)
467 self.errors.ensureCursorVisible()
468 else:
469 self.errors.insertPlainText(inputTxt)
470 self.errors.ensureCursorVisible()
471
472 self.process.write(strToQByteArray(inputTxt))
473
474 self.passwordCheckBox.setChecked(False)
475 self.input.clear()
476
477 def on_input_returnPressed(self):
478 """
479 Private slot to handle the press of the return key in the input field.
480 """
481 self.intercept = True
482 self.on_sendButton_clicked()
483
484 def keyPressEvent(self, evt):
485 """
486 Protected slot to handle a key press event.
487
488 @param evt the key press event (QKeyEvent)
489 """
490 if self.intercept:
491 self.intercept = False
492 evt.accept()
493 return
494 super(SvnRepoBrowserDialog, self).keyPressEvent(evt)

eric ide

mercurial