src/eric7/Plugins/VcsPlugins/vcsPySvn/SvnLogBrowserDialog.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 a dialog to browse the log history.
8 """
9
10 import re
11 import os
12
13 import pysvn
14
15 from PyQt6.QtCore import Qt, QDate, pyqtSlot, QPoint
16 from PyQt6.QtWidgets import (
17 QHeaderView, QWidget, QApplication, QDialogButtonBox, QTreeWidgetItem
18 )
19
20 from EricWidgets import EricMessageBox
21 from EricGui.EricOverrideCursor import EricOverrideCursor
22
23 from EricUtilities.EricMutexLocker import EricMutexLocker
24
25 from .SvnUtilities import formatTime, dateFromTime_t
26 from .SvnDialogMixin import SvnDialogMixin
27
28 from .Ui_SvnLogBrowserDialog import Ui_SvnLogBrowserDialog
29
30 import UI.PixmapCache
31
32
33 class SvnLogBrowserDialog(QWidget, SvnDialogMixin, Ui_SvnLogBrowserDialog):
34 """
35 Class implementing a dialog to browse the log history.
36 """
37 def __init__(self, vcs, parent=None):
38 """
39 Constructor
40
41 @param vcs reference to the vcs object
42 @param parent parent widget (QWidget)
43 """
44 super().__init__(parent)
45 self.setupUi(self)
46 SvnDialogMixin.__init__(self)
47
48 self.__position = QPoint()
49
50 self.buttonBox.button(
51 QDialogButtonBox.StandardButton.Close).setEnabled(False)
52 self.buttonBox.button(
53 QDialogButtonBox.StandardButton.Cancel).setDefault(True)
54
55 self.upButton.setIcon(UI.PixmapCache.getIcon("1uparrow"))
56 self.downButton.setIcon(UI.PixmapCache.getIcon("1downarrow"))
57
58 self.filesTree.headerItem().setText(self.filesTree.columnCount(), "")
59 self.filesTree.header().setSortIndicator(
60 0, Qt.SortOrder.AscendingOrder)
61
62 self.vcs = vcs
63
64 self.__initData()
65
66 self.fromDate.setDisplayFormat("yyyy-MM-dd")
67 self.toDate.setDisplayFormat("yyyy-MM-dd")
68 self.__resetUI()
69
70 self.__messageRole = Qt.ItemDataRole.UserRole
71 self.__changesRole = Qt.ItemDataRole.UserRole + 1
72
73 self.flags = {
74 'A': self.tr('Added'),
75 'D': self.tr('Deleted'),
76 'M': self.tr('Modified'),
77 'R': self.tr('Replaced'),
78 }
79
80 self.__logTreeNormalFont = self.logTree.font()
81 self.__logTreeNormalFont.setBold(False)
82 self.__logTreeBoldFont = self.logTree.font()
83 self.__logTreeBoldFont.setBold(True)
84
85 self.client = self.vcs.getClient()
86 self.client.callback_cancel = self._clientCancelCallback
87 self.client.callback_get_login = self._clientLoginCallback
88 self.client.callback_ssl_server_trust_prompt = (
89 self._clientSslServerTrustPromptCallback
90 )
91
92 def __initData(self):
93 """
94 Private method to (re-)initialize some data.
95 """
96 self.__maxDate = QDate()
97 self.__minDate = QDate()
98 self.__filterLogsEnabled = True
99
100 self.diff = None
101 self.__lastRev = 0
102
103 def closeEvent(self, e):
104 """
105 Protected slot implementing a close event handler.
106
107 @param e close event (QCloseEvent)
108 """
109 self.__position = self.pos()
110
111 e.accept()
112
113 def show(self):
114 """
115 Public slot to show the dialog.
116 """
117 if not self.__position.isNull():
118 self.move(self.__position)
119 self.__resetUI()
120
121 super().show()
122
123 def __resetUI(self):
124 """
125 Private method to reset the user interface.
126 """
127 self.fromDate.setDate(QDate.currentDate())
128 self.toDate.setDate(QDate.currentDate())
129 self.fieldCombo.setCurrentIndex(self.fieldCombo.findText(
130 self.tr("Message")))
131 self.limitSpinBox.setValue(self.vcs.getPlugin().getPreferences(
132 "LogLimit"))
133 self.stopCheckBox.setChecked(self.vcs.getPlugin().getPreferences(
134 "StopLogOnCopy"))
135
136 self.logTree.clear()
137
138 self.nextButton.setEnabled(True)
139 self.limitSpinBox.setEnabled(True)
140
141 def _reset(self):
142 """
143 Protected method to reset the internal state of the dialog.
144 """
145 SvnDialogMixin._reset(self)
146
147 self.cancelled = False
148
149 self.buttonBox.button(
150 QDialogButtonBox.StandardButton.Close).setEnabled(False)
151 self.buttonBox.button(
152 QDialogButtonBox.StandardButton.Cancel).setEnabled(True)
153 self.buttonBox.button(
154 QDialogButtonBox.StandardButton.Cancel).setDefault(True)
155 QApplication.processEvents()
156
157 def __resizeColumnsLog(self):
158 """
159 Private method to resize the log tree columns.
160 """
161 self.logTree.header().resizeSections(
162 QHeaderView.ResizeMode.ResizeToContents)
163 self.logTree.header().setStretchLastSection(True)
164
165 def __resortLog(self):
166 """
167 Private method to resort the log tree.
168 """
169 self.logTree.sortItems(
170 self.logTree.sortColumn(),
171 self.logTree.header().sortIndicatorOrder())
172
173 def __resizeColumnsFiles(self):
174 """
175 Private method to resize the changed files tree columns.
176 """
177 self.filesTree.header().resizeSections(
178 QHeaderView.ResizeMode.ResizeToContents)
179 self.filesTree.header().setStretchLastSection(True)
180
181 def __resortFiles(self):
182 """
183 Private method to resort the changed files tree.
184 """
185 sortColumn = self.filesTree.sortColumn()
186 self.filesTree.sortItems(
187 1, self.filesTree.header().sortIndicatorOrder())
188 self.filesTree.sortItems(
189 sortColumn, self.filesTree.header().sortIndicatorOrder())
190
191 def __generateLogItem(self, author, date, message, revision, changedPaths):
192 """
193 Private method to generate a log tree entry.
194
195 @param author author info (string)
196 @param date date info (integer)
197 @param message text of the log message (string)
198 @param revision revision info (string or pysvn.opt_revision_kind)
199 @param changedPaths list of pysvn dictionary like objects containing
200 info about the changed files/directories
201 @return reference to the generated item (QTreeWidgetItem)
202 """
203 if revision == "":
204 rev = ""
205 self.__lastRev = 0
206 else:
207 rev = revision.number
208 self.__lastRev = revision.number
209 dt = formatTime(date) if date else ""
210
211 itm = QTreeWidgetItem(self.logTree)
212 itm.setData(0, Qt.ItemDataRole.DisplayRole, rev)
213 itm.setData(1, Qt.ItemDataRole.DisplayRole, author)
214 itm.setData(2, Qt.ItemDataRole.DisplayRole, dt)
215 itm.setData(3, Qt.ItemDataRole.DisplayRole,
216 " ".join(message.splitlines()))
217
218 changes = []
219 for changedPath in changedPaths:
220 copyPath = (
221 ""
222 if changedPath["copyfrom_path"] is None else
223 changedPath["copyfrom_path"]
224 )
225 copyRev = (
226 ""
227 if changedPath["copyfrom_revision"] is None else
228 "{0:7d}".format(changedPath["copyfrom_revision"].number)
229 )
230 change = {
231 "action": changedPath["action"],
232 "path": changedPath["path"],
233 "copyfrom_path": copyPath,
234 "copyfrom_revision": copyRev,
235 }
236 changes.append(change)
237 itm.setData(0, self.__messageRole, message)
238 itm.setData(0, self.__changesRole, changes)
239
240 itm.setTextAlignment(0, Qt.AlignmentFlag.AlignRight)
241 itm.setTextAlignment(1, Qt.AlignmentFlag.AlignLeft)
242 itm.setTextAlignment(2, Qt.AlignmentFlag.AlignLeft)
243 itm.setTextAlignment(3, Qt.AlignmentFlag.AlignLeft)
244 itm.setTextAlignment(4, Qt.AlignmentFlag.AlignLeft)
245
246 return itm
247
248 def __generateFileItem(self, action, path, copyFrom, copyRev):
249 """
250 Private method to generate a changed files tree entry.
251
252 @param action indicator for the change action ("A", "D" or "M")
253 @param path path of the file in the repository (string)
254 @param copyFrom path the file was copied from (None, string)
255 @param copyRev revision the file was copied from (None, string)
256 @return reference to the generated item (QTreeWidgetItem)
257 """
258 itm = QTreeWidgetItem(
259 self.filesTree,
260 [self.flags[action], path, copyFrom, copyRev]
261 )
262
263 itm.setTextAlignment(3, Qt.AlignmentFlag.AlignRight)
264
265 return itm
266
267 def __getLogEntries(self, startRev=None):
268 """
269 Private method to retrieve log entries from the repository.
270
271 @param startRev revision number to start from (integer, string)
272 """
273 fetchLimit = 10
274 self._reset()
275
276 limit = self.limitSpinBox.value()
277 if startRev is None:
278 start = pysvn.Revision(pysvn.opt_revision_kind.head)
279 else:
280 try:
281 start = pysvn.Revision(pysvn.opt_revision_kind.number,
282 int(startRev))
283 except TypeError:
284 start = pysvn.Revision(pysvn.opt_revision_kind.head)
285
286 with EricOverrideCursor():
287 cwd = os.getcwd()
288 os.chdir(self.dname)
289 try:
290 nextRev = 0
291 fetched = 0
292 logs = []
293 with EricMutexLocker(self.vcs.vcsExecutionMutex):
294 while fetched < limit:
295 flimit = min(fetchLimit, limit - fetched)
296 revstart = (
297 start
298 if fetched == 0 else
299 pysvn.Revision(pysvn.opt_revision_kind.number,
300 nextRev)
301 )
302 allLogs = self.client.log(
303 self.fname, revision_start=revstart,
304 discover_changed_paths=True, limit=flimit + 1,
305 strict_node_history=self.stopCheckBox.isChecked())
306 if (
307 len(allLogs) <= flimit or
308 self._clientCancelCallback()
309 ):
310 logs.extend(allLogs)
311 break
312 else:
313 logs.extend(allLogs[:-1])
314 nextRev = allLogs[-1]["revision"].number
315 fetched += fetchLimit
316
317 for log in logs:
318 author = log["author"]
319 message = log["message"]
320 self.__generateLogItem(
321 author, log["date"], message,
322 log["revision"], log['changed_paths'])
323 dt = dateFromTime_t(log["date"])
324 if (
325 not self.__maxDate.isValid() and
326 not self.__minDate.isValid()
327 ):
328 self.__maxDate = dt
329 self.__minDate = dt
330 else:
331 if self.__maxDate < dt:
332 self.__maxDate = dt
333 if self.__minDate > dt:
334 self.__minDate = dt
335 if len(logs) < limit and not self.cancelled:
336 self.nextButton.setEnabled(False)
337 self.limitSpinBox.setEnabled(False)
338 self.__filterLogsEnabled = False
339 self.fromDate.setMinimumDate(self.__minDate)
340 self.fromDate.setMaximumDate(self.__maxDate)
341 self.fromDate.setDate(self.__minDate)
342 self.toDate.setMinimumDate(self.__minDate)
343 self.toDate.setMaximumDate(self.__maxDate)
344 self.toDate.setDate(self.__maxDate)
345 self.__filterLogsEnabled = True
346
347 self.__resizeColumnsLog()
348 self.__resortLog()
349 self.__filterLogs()
350 except pysvn.ClientError as e:
351 self.__showError(e.args[0])
352 os.chdir(cwd)
353 self.__finish()
354
355 def start(self, fn, isFile=False):
356 """
357 Public slot to start the svn log command.
358
359 @param fn filename to show the log for (string)
360 @param isFile flag indicating log for a file is to be shown
361 (boolean)
362 """
363 self.sbsCheckBox.setEnabled(isFile)
364 self.sbsCheckBox.setVisible(isFile)
365
366 self.__initData()
367
368 self.filename = fn
369 self.dname, self.fname = self.vcs.splitPath(fn)
370
371 self.activateWindow()
372 self.raise_()
373
374 self.logTree.clear()
375 self.__getLogEntries()
376 self.logTree.setCurrentItem(self.logTree.topLevelItem(0))
377
378 def __finish(self):
379 """
380 Private slot called when the user pressed the button.
381 """
382 self.buttonBox.button(
383 QDialogButtonBox.StandardButton.Close).setEnabled(True)
384 self.buttonBox.button(
385 QDialogButtonBox.StandardButton.Cancel).setEnabled(False)
386 self.buttonBox.button(
387 QDialogButtonBox.StandardButton.Close).setDefault(True)
388
389 self._cancel()
390
391 def __diffRevisions(self, rev1, rev2, peg_rev):
392 """
393 Private method to do a diff of two revisions.
394
395 @param rev1 first revision number (integer)
396 @param rev2 second revision number (integer)
397 @param peg_rev revision number to use as a reference (integer)
398 """
399 if self.sbsCheckBox.isEnabled() and self.sbsCheckBox.isChecked():
400 self.vcs.vcsSbsDiff(self.filename,
401 revisions=(str(rev1), str(rev2)))
402 else:
403 if self.diff is None:
404 from .SvnDiffDialog import SvnDiffDialog
405 self.diff = SvnDiffDialog(self.vcs)
406 self.diff.show()
407 self.diff.raise_()
408 QApplication.processEvents()
409 self.diff.start(self.filename, [rev1, rev2], pegRev=peg_rev)
410
411 def on_buttonBox_clicked(self, button):
412 """
413 Private slot called by a button of the button box clicked.
414
415 @param button button that was clicked (QAbstractButton)
416 """
417 if button == self.buttonBox.button(
418 QDialogButtonBox.StandardButton.Close
419 ):
420 self.close()
421 elif button == self.buttonBox.button(
422 QDialogButtonBox.StandardButton.Cancel
423 ):
424 self.cancelled = True
425 self.__finish()
426
427 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
428 def on_logTree_currentItemChanged(self, current, previous):
429 """
430 Private slot called, when the current item of the log tree changes.
431
432 @param current reference to the new current item (QTreeWidgetItem)
433 @param previous reference to the old current item (QTreeWidgetItem)
434 """
435 if current is not None:
436 self.messageEdit.setPlainText(current.data(0, self.__messageRole))
437
438 self.filesTree.clear()
439 changes = current.data(0, self.__changesRole)
440 if len(changes) > 0:
441 for change in changes:
442 self.__generateFileItem(
443 change["action"], change["path"],
444 change["copyfrom_path"], change["copyfrom_revision"])
445 self.__resizeColumnsFiles()
446 self.__resortFiles()
447
448 self.diffPreviousButton.setEnabled(
449 current != self.logTree.topLevelItem(
450 self.logTree.topLevelItemCount() - 1))
451
452 # Highlight the current entry using a bold font
453 for col in range(self.logTree.columnCount()):
454 current and current.setFont(col, self.__logTreeBoldFont)
455 previous and previous.setFont(col, self.__logTreeNormalFont)
456
457 # set the state of the up and down buttons
458 self.upButton.setEnabled(
459 current is not None and
460 self.logTree.indexOfTopLevelItem(current) > 0)
461 self.downButton.setEnabled(
462 current is not None and
463 int(current.text(0)) > 1)
464
465 @pyqtSlot()
466 def on_logTree_itemSelectionChanged(self):
467 """
468 Private slot called, when the selection has changed.
469 """
470 self.diffRevisionsButton.setEnabled(
471 len(self.logTree.selectedItems()) == 2)
472
473 @pyqtSlot()
474 def on_nextButton_clicked(self):
475 """
476 Private slot to handle the Next button.
477 """
478 if self.__lastRev > 1:
479 self.__getLogEntries(self.__lastRev - 1)
480
481 @pyqtSlot()
482 def on_diffPreviousButton_clicked(self):
483 """
484 Private slot to handle the Diff to Previous button.
485 """
486 itm = self.logTree.topLevelItem(0)
487 if itm is None:
488 self.diffPreviousButton.setEnabled(False)
489 return
490 peg_rev = int(itm.text(0))
491
492 itm = self.logTree.currentItem()
493 if itm is None:
494 self.diffPreviousButton.setEnabled(False)
495 return
496 rev2 = int(itm.text(0))
497
498 itm = self.logTree.topLevelItem(
499 self.logTree.indexOfTopLevelItem(itm) + 1)
500 if itm is None:
501 self.diffPreviousButton.setEnabled(False)
502 return
503 rev1 = int(itm.text(0))
504
505 self.__diffRevisions(rev1, rev2, peg_rev)
506
507 @pyqtSlot()
508 def on_diffRevisionsButton_clicked(self):
509 """
510 Private slot to handle the Compare Revisions button.
511 """
512 items = self.logTree.selectedItems()
513 if len(items) != 2:
514 self.diffRevisionsButton.setEnabled(False)
515 return
516
517 rev2 = int(items[0].text(0))
518 rev1 = int(items[1].text(0))
519
520 itm = self.logTree.topLevelItem(0)
521 if itm is None:
522 self.diffPreviousButton.setEnabled(False)
523 return
524 peg_rev = int(itm.text(0))
525
526 self.__diffRevisions(min(rev1, rev2), max(rev1, rev2), peg_rev)
527
528 def __showError(self, msg):
529 """
530 Private slot to show an error message.
531
532 @param msg error message to show (string)
533 """
534 EricMessageBox.critical(
535 self,
536 self.tr("Subversion Error"),
537 msg)
538
539 @pyqtSlot(QDate)
540 def on_fromDate_dateChanged(self, date):
541 """
542 Private slot called, when the from date changes.
543
544 @param date new date (QDate)
545 """
546 self.__filterLogs()
547
548 @pyqtSlot(QDate)
549 def on_toDate_dateChanged(self, date):
550 """
551 Private slot called, when the from date changes.
552
553 @param date new date (QDate)
554 """
555 self.__filterLogs()
556
557 @pyqtSlot(int)
558 def on_fieldCombo_activated(self, index):
559 """
560 Private slot called, when a new filter field is selected.
561
562 @param index index of the selected entry
563 @type int
564 """
565 self.__filterLogs()
566
567 @pyqtSlot(str)
568 def on_rxEdit_textChanged(self, txt):
569 """
570 Private slot called, when a filter expression is entered.
571
572 @param txt filter expression (string)
573 """
574 self.__filterLogs()
575
576 def __filterLogs(self):
577 """
578 Private method to filter the log entries.
579 """
580 if self.__filterLogsEnabled:
581 from_ = self.fromDate.date().toString("yyyy-MM-dd")
582 to_ = self.toDate.date().addDays(1).toString("yyyy-MM-dd")
583 txt = self.fieldCombo.currentText()
584 if txt == self.tr("Author"):
585 fieldIndex = 1
586 searchRx = re.compile(self.rxEdit.text(), re.IGNORECASE)
587 elif txt == self.tr("Revision"):
588 fieldIndex = 0
589 txt = self.rxEdit.text()
590 if txt.startswith("^"):
591 searchRx = re.compile(
592 r"^\s*{0}".format(txt[1:]), re.IGNORECASE)
593 else:
594 searchRx = re.compile(txt, re.IGNORECASE)
595 else:
596 fieldIndex = 3
597 searchRx = re.compile(self.rxEdit.text(), re.IGNORECASE)
598
599 currentItem = self.logTree.currentItem()
600 for topIndex in range(self.logTree.topLevelItemCount()):
601 topItem = self.logTree.topLevelItem(topIndex)
602 if (
603 topItem.text(2) <= to_ and
604 topItem.text(2) >= from_ and
605 searchRx.search(topItem.text(fieldIndex)) is not None
606 ):
607 topItem.setHidden(False)
608 if topItem is currentItem:
609 self.on_logTree_currentItemChanged(topItem, None)
610 else:
611 topItem.setHidden(True)
612 if topItem is currentItem:
613 self.messageEdit.clear()
614 self.filesTree.clear()
615
616 @pyqtSlot(bool)
617 def on_stopCheckBox_clicked(self, checked):
618 """
619 Private slot called, when the stop on copy/move checkbox is clicked.
620
621 @param checked flag indicating the check box state (boolean)
622 """
623 self.vcs.getPlugin().setPreferences("StopLogOnCopy",
624 int(self.stopCheckBox.isChecked()))
625 self.nextButton.setEnabled(True)
626 self.limitSpinBox.setEnabled(True)
627
628 @pyqtSlot()
629 def on_upButton_clicked(self):
630 """
631 Private slot to move the current item up one entry.
632 """
633 itm = self.logTree.itemAbove(self.logTree.currentItem())
634 if itm:
635 self.logTree.setCurrentItem(itm)
636
637 @pyqtSlot()
638 def on_downButton_clicked(self):
639 """
640 Private slot to move the current item down one entry.
641 """
642 itm = self.logTree.itemBelow(self.logTree.currentItem())
643 if itm:
644 self.logTree.setCurrentItem(itm)
645 else:
646 # load the next bunch and try again
647 self.on_nextButton_clicked()
648 self.on_downButton_clicked()

eric ide

mercurial