src/eric7/Plugins/VcsPlugins/vcsSubversion/SvnRepoBrowserDialog.py

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

eric ide

mercurial