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

eric ide

mercurial