Plugins/VcsPlugins/vcsPySvn/SvnLogBrowserDialog.py

changeset 0
de9c2efb9d02
child 7
c679fb30c8f3
equal deleted inserted replaced
-1:000000000000 0:de9c2efb9d02
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2007 - 2009 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog to browse the log history.
8 """
9
10 import os
11
12 import pysvn
13
14 from PyQt4.QtCore import *
15 from PyQt4.QtGui import *
16
17 from SvnUtilities import formatTime, dateFromTime_t
18 from SvnDialogMixin import SvnDialogMixin
19 from SvnDiffDialog import SvnDiffDialog
20
21 from Ui_SvnLogBrowserDialog import Ui_SvnLogBrowserDialog
22
23 import UI.PixmapCache
24
25 class SvnLogBrowserDialog(QDialog, SvnDialogMixin, Ui_SvnLogBrowserDialog):
26 """
27 Class implementing a dialog to browse the log history.
28 """
29 def __init__(self, vcs, parent = None):
30 """
31 Constructor
32
33 @param vcs reference to the vcs object
34 @param parent parent widget (QWidget)
35 """
36 QDialog.__init__(self, parent)
37 self.setupUi(self)
38 SvnDialogMixin.__init__(self)
39
40 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
41 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
42
43 self.filesTree.headerItem().setText(self.filesTree.columnCount(), "")
44 self.filesTree.header().setSortIndicator(0, Qt.AscendingOrder)
45
46 self.vcs = vcs
47
48 self.__maxDate = QDate()
49 self.__minDate = QDate()
50 self.__filterLogsEnabled = True
51
52 self.fromDate.setDisplayFormat("yyyy-MM-dd")
53 self.toDate.setDisplayFormat("yyyy-MM-dd")
54 self.fromDate.setDate(QDate.currentDate())
55 self.toDate.setDate(QDate.currentDate())
56 self.fieldCombo.setCurrentIndex(self.fieldCombo.findText(self.trUtf8("Message")))
57 self.clearRxEditButton.setIcon(UI.PixmapCache.getIcon("clearLeft.png"))
58 self.limitSpinBox.setValue(self.vcs.getPlugin().getPreferences("LogLimit"))
59 self.stopCheckBox.setChecked(self.vcs.getPlugin().getPreferences("StopLogOnCopy"))
60
61 self.__messageRole = Qt.UserRole
62 self.__changesRole = Qt.UserRole + 1
63
64 self.flags = {
65 'A' : self.trUtf8('Added'),
66 'D' : self.trUtf8('Deleted'),
67 'M' : self.trUtf8('Modified')
68 }
69
70 self.diff = None
71 self.__lastRev = 0
72
73 self.client = self.vcs.getClient()
74 self.client.callback_cancel = \
75 self._clientCancelCallback
76 self.client.callback_get_login = \
77 self._clientLoginCallback
78 self.client.callback_ssl_server_trust_prompt = \
79 self._clientSslServerTrustPromptCallback
80
81 def _reset(self):
82 """
83 Protected method to reset the internal state of the dialog.
84 """
85 SvnDialogMixin._reset(self)
86
87 self.cancelled = False
88
89 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
90 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
91 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
92 QApplication.processEvents()
93
94 def __resizeColumnsLog(self):
95 """
96 Private method to resize the log tree columns.
97 """
98 self.logTree.header().resizeSections(QHeaderView.ResizeToContents)
99 self.logTree.header().setStretchLastSection(True)
100
101 def __resortLog(self):
102 """
103 Private method to resort the log tree.
104 """
105 self.logTree.sortItems(self.logTree.sortColumn(),
106 self.logTree.header().sortIndicatorOrder())
107
108 def __resizeColumnsFiles(self):
109 """
110 Private method to resize the changed files tree columns.
111 """
112 self.filesTree.header().resizeSections(QHeaderView.ResizeToContents)
113 self.filesTree.header().setStretchLastSection(True)
114
115 def __resortFiles(self):
116 """
117 Private method to resort the changed files tree.
118 """
119 sortColumn = self.filesTree.sortColumn()
120 self.filesTree.sortItems(1,
121 self.filesTree.header().sortIndicatorOrder())
122 self.filesTree.sortItems(sortColumn,
123 self.filesTree.header().sortIndicatorOrder())
124
125 def __generateLogItem(self, author, date, message, revision, changedPaths):
126 """
127 Private method to generate a log tree entry.
128
129 @param author author info (string)
130 @param date date info (integer)
131 @param message text of the log message (string)
132 @param revision revision info (string or pysvn.opt_revision_kind)
133 @param changedPaths list of pysvn dictionary like objects containing
134 info about the changed files/directories
135 @return reference to the generated item (QTreeWidgetItem)
136 """
137 if revision == "":
138 rev = ""
139 self.__lastRev = 0
140 else:
141 rev = "%7d" % revision.number
142 self.__lastRev = revision.number
143 if date == "":
144 dt = ""
145 else:
146 dt = formatTime(date)
147
148 itm = QTreeWidgetItem(self.logTree,
149 [rev, author, dt," ".join(message.splitlines())]
150 )
151
152 changes = []
153 for changedPath in changedPaths:
154 if changedPath["copyfrom_path"] is None:
155 copyPath = ""
156 else:
157 copyPath = changedPath["copyfrom_path"]
158 if changedPath["copyfrom_revision"] is None:
159 copyRev = ""
160 else:
161 copyRev = "%7d" % changedPath["copyfrom_revision"].number
162 change = {
163 "action" : changedPath["action"],
164 "path" : changedPath["path"],
165 "copyfrom_path" : copyPath,
166 "copyfrom_revision" : copyRev,
167 }
168 changes.append(change)
169 itm.setData(0, self.__messageRole, QVariant(message))
170 # TODO: change this to simply store the list for QVariant v2
171 itm.setData(0, self.__changesRole, QVariant(unicode(changes)))
172
173 itm.setTextAlignment(0, Qt.AlignRight)
174 itm.setTextAlignment(1, Qt.AlignLeft)
175 itm.setTextAlignment(2, Qt.AlignLeft)
176 itm.setTextAlignment(3, Qt.AlignLeft)
177 itm.setTextAlignment(4, Qt.AlignLeft)
178
179 return itm
180
181 def __generateFileItem(self, action, path, copyFrom, copyRev):
182 """
183 Private method to generate a changed files tree entry.
184
185 @param action indicator for the change action ("A", "D" or "M")
186 @param path path of the file in the repository (string)
187 @param copyFrom path the file was copied from (None, string)
188 @param copyRev revision the file was copied from (None, string)
189 @return reference to the generated item (QTreeWidgetItem)
190 """
191 itm = QTreeWidgetItem(self.filesTree,
192 [self.flags[action], path, copyFrom, copyRev]
193 )
194
195 itm.setTextAlignment(3, Qt.AlignRight)
196
197 return itm
198
199 def __getLogEntries(self, startRev = None):
200 """
201 Private method to retrieve log entries from the repository.
202
203 @param startRev revision number to start from (integer, string)
204 """
205 fetchLimit = 10
206 self._reset()
207
208 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
209 QApplication.processEvents()
210
211 limit = self.limitSpinBox.value()
212 if startRev is None:
213 start = pysvn.Revision(pysvn.opt_revision_kind.head)
214 else:
215 try:
216 start = pysvn.Revision(pysvn.opt_revision_kind.number, int(startRev))
217 except TypeError:
218 start = pysvn.Revision(pysvn.opt_revision_kind.head)
219
220 locker = QMutexLocker(self.vcs.vcsExecutionMutex)
221 cwd = os.getcwd()
222 os.chdir(self.dname)
223 try:
224 fetched = 0
225 logs = []
226 while fetched < limit:
227 flimit = min(fetchLimit, limit - fetched)
228 if fetched == 0:
229 revstart = start
230 else:
231 revstart = pysvn.Revision(\
232 pysvn.opt_revision_kind.number, nextRev)
233 allLogs = self.client.log(self.fname,
234 revision_start = revstart,
235 discover_changed_paths = True,
236 limit = flimit + 1,
237 strict_node_history = self.stopCheckBox.isChecked())
238 if len(allLogs) <= flimit or self._clientCancelCallback():
239 logs.extend(allLogs)
240 break
241 else:
242 logs.extend(allLogs[:-1])
243 nextRev = allLogs[-1]["revision"].number
244 fetched += fetchLimit
245 locker.unlock()
246
247 for log in logs:
248 self.__generateLogItem(log["author"], log["date"],
249 log["message"], log["revision"], log['changed_paths'])
250 dt = dateFromTime_t(log["date"])
251 if not self.__maxDate.isValid() and not self.__minDate.isValid():
252 self.__maxDate = dt
253 self.__minDate = dt
254 else:
255 if self.__maxDate < dt:
256 self.__maxDate = dt
257 if self.__minDate > dt:
258 self.__minDate = dt
259 if len(logs) < limit and not self.cancelled:
260 self.nextButton.setEnabled(False)
261 self.limitSpinBox.setEnabled(False)
262 self.__filterLogsEnabled = False
263 self.fromDate.setMinimumDate(self.__minDate)
264 self.fromDate.setMaximumDate(self.__maxDate)
265 self.fromDate.setDate(self.__minDate)
266 self.toDate.setMinimumDate(self.__minDate)
267 self.toDate.setMaximumDate(self.__maxDate)
268 self.toDate.setDate(self.__maxDate)
269 self.__filterLogsEnabled = True
270
271 self.__resizeColumnsLog()
272 self.__resortLog()
273 self.__filterLogs()
274 except pysvn.ClientError, e:
275 locker.unlock()
276 self.__showError(e.args[0])
277 os.chdir(cwd)
278 self.__finish()
279
280 def start(self, fn):
281 """
282 Public slot to start the svn log command.
283
284 @param fn filename to show the log for (string)
285 """
286 self.filename = fn
287 self.dname, self.fname = self.vcs.splitPath(fn)
288
289 self.activateWindow()
290 self.raise_()
291
292 self.logTree.clear()
293 self.__getLogEntries()
294 self.logTree.setCurrentItem(self.logTree.topLevelItem(0))
295
296 def __finish(self):
297 """
298 Private slot called when the user pressed the button.
299 """
300 QApplication.restoreOverrideCursor()
301
302 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
303 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
304 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
305
306 self._cancel()
307
308 def __diffRevisions(self, rev1, rev2, peg_rev):
309 """
310 Private method to do a diff of two revisions.
311
312 @param rev1 first revision number (integer)
313 @param rev2 second revision number (integer)
314 @param peg_rev revision number to use as a reference (integer)
315 """
316 if self.diff is None:
317 self.diff = SvnDiffDialog(self.vcs)
318 self.diff.show()
319 QApplication.processEvents()
320 self.diff.start(self.filename, [rev1, rev2], pegRev = peg_rev)
321
322 def on_buttonBox_clicked(self, button):
323 """
324 Private slot called by a button of the button box clicked.
325
326 @param button button that was clicked (QAbstractButton)
327 """
328 if button == self.buttonBox.button(QDialogButtonBox.Close):
329 self.close()
330 elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
331 self.cancelled = True
332 self.__finish()
333
334 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
335 def on_logTree_currentItemChanged(self, current, previous):
336 """
337 Private slot called, when the current item of the log tree changes.
338
339 @param current reference to the new current item (QTreeWidgetItem)
340 @param previous reference to the old current item (QTreeWidgetItem)
341 """
342 self.messageEdit.setPlainText(current.data(0, self.__messageRole).toString())
343
344 self.filesTree.clear()
345 # TODO: change this for QVariant v2 to not use eval
346 changes = eval(unicode(current.data(0, self.__changesRole).toString()))
347 if len(changes) > 0:
348 for change in changes:
349 self.__generateFileItem(change["action"], change["path"],
350 change["copyfrom_path"], change["copyfrom_revision"])
351 self.__resizeColumnsFiles()
352 self.__resortFiles()
353
354 self.diffPreviousButton.setEnabled(\
355 current != self.logTree.topLevelItem(self.logTree.topLevelItemCount() - 1))
356
357 @pyqtSlot()
358 def on_logTree_itemSelectionChanged(self):
359 """
360 Private slot called, when the selection has changed.
361 """
362 self.diffRevisionsButton.setEnabled(len(self.logTree.selectedItems()) == 2)
363
364 @pyqtSlot()
365 def on_nextButton_clicked(self):
366 """
367 Private slot to handle the Next button.
368 """
369 if self.__lastRev > 1:
370 self.__getLogEntries(self.__lastRev - 1)
371
372 @pyqtSlot()
373 def on_diffPreviousButton_clicked(self):
374 """
375 Private slot to handle the Diff to Previous button.
376 """
377 itm = self.logTree.topLevelItem(0)
378 if itm is None:
379 self.diffPreviousButton.setEnabled(False)
380 return
381 peg_rev = int(itm.text(0))
382
383 itm = self.logTree.currentItem()
384 if itm is None:
385 self.diffPreviousButton.setEnabled(False)
386 return
387 rev2 = int(itm.text(0))
388
389 itm = self.logTree.topLevelItem(self.logTree.indexOfTopLevelItem(itm) + 1)
390 if itm is None:
391 self.diffPreviousButton.setEnabled(False)
392 return
393 rev1 = int(itm.text(0))
394
395 self.__diffRevisions(rev1, rev2, peg_rev)
396
397 @pyqtSlot()
398 def on_diffRevisionsButton_clicked(self):
399 """
400 Private slot to handle the Compare Revisions button.
401 """
402 items = self.logTree.selectedItems()
403 if len(items) != 2:
404 self.diffRevisionsButton.setEnabled(False)
405 return
406
407 rev2 = int(items[0].text(0))
408 rev1 = int(items[1].text(0))
409
410 itm = self.logTree.topLevelItem(0)
411 if itm is None:
412 self.diffPreviousButton.setEnabled(False)
413 return
414 peg_rev = int(itm.text(0))
415
416 self.__diffRevisions(min(rev1, rev2), max(rev1, rev2), peg_rev)
417
418 def __showError(self, msg):
419 """
420 Private slot to show an error message.
421
422 @param msg error message to show (string)
423 """
424 QMessageBox.critical(self,
425 self.trUtf8("Subversion Error"),
426 msg)
427
428 @pyqtSlot(QDate)
429 def on_fromDate_dateChanged(self, date):
430 """
431 Private slot called, when the from date changes.
432
433 @param date new date (QDate)
434 """
435 self.__filterLogs()
436
437 @pyqtSlot(QDate)
438 def on_toDate_dateChanged(self, date):
439 """
440 Private slot called, when the from date changes.
441
442 @param date new date (QDate)
443 """
444 self.__filterLogs()
445
446 @pyqtSlot(str)
447 def on_fieldCombo_activated(self, txt):
448 """
449 Private slot called, when a new filter field is selected.
450
451 @param txt text of the selected field (string)
452 """
453 self.__filterLogs()
454
455 @pyqtSlot(str)
456 def on_rxEdit_textChanged(self, txt):
457 """
458 Private slot called, when a filter expression is entered.
459
460 @param txt filter expression (string)
461 """
462 self.__filterLogs()
463
464 def __filterLogs(self):
465 """
466 Private method to filter the log entries.
467 """
468 if self.__filterLogsEnabled:
469 from_ = self.fromDate.date().toString("yyyy-MM-dd")
470 to_ = self.toDate.date().addDays(1).toString("yyyy-MM-dd")
471 txt = self.fieldCombo.currentText()
472 if txt == self.trUtf8("Author"):
473 fieldIndex = 1
474 searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive)
475 elif txt == self.trUtf8("Revision"):
476 fieldIndex = 0
477 txt = self.rxEdit.text()
478 if txt.startswith("^"):
479 searchRx = QRegExp("^\s*%s" % txt[1:], Qt.CaseInsensitive)
480 else:
481 searchRx = QRegExp(txt, Qt.CaseInsensitive)
482 else:
483 fieldIndex = 3
484 searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive)
485
486 currentItem = self.logTree.currentItem()
487 for topIndex in range(self.logTree.topLevelItemCount()):
488 topItem = self.logTree.topLevelItem(topIndex)
489 if topItem.text(2) <= to_ and topItem.text(2) >= from_ and \
490 searchRx.indexIn(topItem.text(fieldIndex)) > -1:
491 topItem.setHidden(False)
492 if topItem is currentItem:
493 self.on_logTree_currentItemChanged(topItem, None)
494 else:
495 topItem.setHidden(True)
496 if topItem is currentItem:
497 self.messageEdit.clear()
498 self.filesTree.clear()
499
500 @pyqtSlot()
501 def on_clearRxEditButton_clicked(self):
502 """
503 Private slot called by a click of the clear RX edit button.
504 """
505 self.rxEdit.clear()
506
507 @pyqtSlot(bool)
508 def on_stopCheckBox_clicked(self, checked):
509 """
510 Private slot called, when the stop on copy/move checkbox is clicked
511 """
512 self.vcs.getPlugin().setPreferences("StopLogOnCopy",
513 int(self.stopCheckBox.isChecked()))
514 self.nextButton.setEnabled(True)
515 self.limitSpinBox.setEnabled(True)

eric ide

mercurial