Plugins/VcsPlugins/vcsSubversion/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 from PyQt4.QtCore import *
13 from PyQt4.QtGui import *
14
15 from SvnDiffDialog import SvnDiffDialog
16
17 from Ui_SvnLogBrowserDialog import Ui_SvnLogBrowserDialog
18
19 import UI.PixmapCache
20
21 import Preferences
22
23 class SvnLogBrowserDialog(QDialog, Ui_SvnLogBrowserDialog):
24 """
25 Class implementing a dialog to browse the log history.
26 """
27 def __init__(self, vcs, parent = None):
28 """
29 Constructor
30
31 @param vcs reference to the vcs object
32 @param parent parent widget (QWidget)
33 """
34 QDialog.__init__(self, parent)
35 self.setupUi(self)
36
37 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
38 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
39
40 self.filesTree.headerItem().setText(self.filesTree.columnCount(), "")
41 self.filesTree.header().setSortIndicator(0, Qt.AscendingOrder)
42
43 self.vcs = vcs
44
45 self.__maxDate = QDate()
46 self.__minDate = QDate()
47 self.__filterLogsEnabled = True
48
49 self.fromDate.setDisplayFormat("yyyy-MM-dd")
50 self.toDate.setDisplayFormat("yyyy-MM-dd")
51 self.fromDate.setDate(QDate.currentDate())
52 self.toDate.setDate(QDate.currentDate())
53 self.fieldCombo.setCurrentIndex(self.fieldCombo.findText(self.trUtf8("Message")))
54 self.clearRxEditButton.setIcon(UI.PixmapCache.getIcon("clearLeft.png"))
55 self.limitSpinBox.setValue(self.vcs.getPlugin().getPreferences("LogLimit"))
56 self.stopCheckBox.setChecked(self.vcs.getPlugin().getPreferences("StopLogOnCopy"))
57
58 self.__messageRole = Qt.UserRole
59 self.__changesRole = Qt.UserRole + 1
60
61 self.process = QProcess()
62 self.connect(self.process, SIGNAL('finished(int, QProcess::ExitStatus)'),
63 self.__procFinished)
64 self.connect(self.process, SIGNAL('readyReadStandardOutput()'),
65 self.__readStdout)
66 self.connect(self.process, SIGNAL('readyReadStandardError()'),
67 self.__readStderr)
68
69 self.rx_sep1 = QRegExp('\\-+\\s*')
70 self.rx_sep2 = QRegExp('=+\\s*')
71 self.rx_rev1 = QRegExp('rev ([0-9]+): ([^|]*) \| ([^|]*) \| ([0-9]+) .*')
72 # "rev" followed by one or more decimals followed by a colon followed
73 # anything up to " | " (twice) followed by one or more decimals followed
74 # by anything
75 self.rx_rev2 = QRegExp('r([0-9]+) \| ([^|]*) \| ([^|]*) \| ([0-9]+) .*')
76 # "r" followed by one or more decimals followed by " | " followed
77 # anything up to " | " (twice) followed by one or more decimals followed
78 # by anything
79 self.rx_flags1 = QRegExp(r""" ([ADM])\s(.*)\s+\(\w+\s+(.*):([0-9]+)\)\s*""")
80 # three blanks followed by A or D or M followed by path followed by
81 # path copied from followed by copied from revision
82 self.rx_flags2 = QRegExp(' ([ADM]) (.*)\\s*')
83 # three blanks followed by A or D or M followed by path
84
85 self.flags = {
86 'A' : self.trUtf8('Added'),
87 'D' : self.trUtf8('Deleted'),
88 'M' : self.trUtf8('Modified'),
89 }
90
91 self.buf = [] # buffer for stdout
92 self.diff = None
93 self.__started = False
94 self.__lastRev = 0
95
96 def closeEvent(self, e):
97 """
98 Private slot implementing a close event handler.
99
100 @param e close event (QCloseEvent)
101 """
102 if self.process is not None and \
103 self.process.state() != QProcess.NotRunning:
104 self.process.terminate()
105 QTimer.singleShot(2000, self.process, SLOT('kill()'))
106 self.process.waitForFinished(3000)
107
108 e.accept()
109
110 def __resizeColumnsLog(self):
111 """
112 Private method to resize the log tree columns.
113 """
114 self.logTree.header().resizeSections(QHeaderView.ResizeToContents)
115 self.logTree.header().setStretchLastSection(True)
116
117 def __resortLog(self):
118 """
119 Private method to resort the log tree.
120 """
121 self.logTree.sortItems(self.logTree.sortColumn(),
122 self.logTree.header().sortIndicatorOrder())
123
124 def __resizeColumnsFiles(self):
125 """
126 Private method to resize the changed files tree columns.
127 """
128 self.filesTree.header().resizeSections(QHeaderView.ResizeToContents)
129 self.filesTree.header().setStretchLastSection(True)
130
131 def __resortFiles(self):
132 """
133 Private method to resort the changed files tree.
134 """
135 sortColumn = self.filesTree.sortColumn()
136 self.filesTree.sortItems(1,
137 self.filesTree.header().sortIndicatorOrder())
138 self.filesTree.sortItems(sortColumn,
139 self.filesTree.header().sortIndicatorOrder())
140
141 def __generateLogItem(self, author, date, message, revision, changedPaths):
142 """
143 Private method to generate a log tree entry.
144
145 @param author author info (string)
146 @param date date info (string)
147 @param message text of the log message (list of strings)
148 @param revision revision info (string)
149 @param changedPaths list of dictionary objects containing
150 info about the changed files/directories
151 @return reference to the generated item (QTreeWidgetItem)
152 """
153 msg = []
154 for line in message:
155 msg.append(line.strip())
156
157 itm = QTreeWidgetItem(self.logTree, [
158 "%7s" % revision,
159 author,
160 date,
161 " ".join(msg),
162 ])
163
164 itm.setData(0, self.__messageRole, QVariant(message))
165 # TODO: change this to simply store the list for QVariant v2
166 itm.setData(0, self.__changesRole, QVariant(repr(changedPaths)))
167
168 itm.setTextAlignment(0, Qt.AlignRight)
169 itm.setTextAlignment(1, Qt.AlignLeft)
170 itm.setTextAlignment(2, Qt.AlignLeft)
171 itm.setTextAlignment(3, Qt.AlignLeft)
172 itm.setTextAlignment(4, Qt.AlignLeft)
173
174 try:
175 self.__lastRev = int(revision)
176 except ValueError:
177 self.__lastRev = 0
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],
193 path,
194 copyFrom,
195 copyRev,
196 ])
197
198 itm.setTextAlignment(3, Qt.AlignRight)
199
200 return itm
201
202 def __getLogEntries(self, startRev = None):
203 """
204 Private method to retrieve log entries from the repository.
205
206 @param startRev revision number to start from (integer, string)
207 """
208 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
209 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
210 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
211 QApplication.processEvents()
212
213 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
214 QApplication.processEvents()
215
216 self.intercept = False
217 self.process.kill()
218
219 self.buf = []
220 self.cancelled = False
221 self.errors.clear()
222
223 args = []
224 args.append('log')
225 self.vcs.addArguments(args, self.vcs.options['global'])
226 self.vcs.addArguments(args, self.vcs.options['log'])
227 args.append('--verbose')
228 args.append('--limit')
229 args.append('%d' % self.limitSpinBox.value())
230 if startRev is not None:
231 args.append('--revision')
232 args.append('%s:0' % startRev)
233 if self.stopCheckBox.isChecked():
234 args.append('--stop-on-copy')
235 args.append(self.fname)
236
237 self.process.setWorkingDirectory(self.dname)
238
239 self.process.start('svn', args)
240 procStarted = self.process.waitForStarted()
241 if not procStarted:
242 self.inputGroup.setEnabled(False)
243 QMessageBox.critical(None,
244 self.trUtf8('Process Generation Error'),
245 self.trUtf8(
246 'The process {0} could not be started. '
247 'Ensure, that it is in the search path.'
248 ).format('svn'))
249
250 def start(self, fn):
251 """
252 Public slot to start the svn log command.
253
254 @param fn filename to show the log for (string)
255 """
256 self.filename = fn
257 self.dname, self.fname = self.vcs.splitPath(fn)
258
259 self.activateWindow()
260 self.raise_()
261
262 self.logTree.clear()
263 self.__started = True
264 self.__getLogEntries()
265
266 def __procFinished(self, exitCode, exitStatus):
267 """
268 Private slot connected to the finished signal.
269
270 @param exitCode exit code of the process (integer)
271 @param exitStatus exit status of the process (QProcess.ExitStatus)
272 """
273 self.__processBuffer()
274 self.__finish()
275
276 def __finish(self):
277 """
278 Private slot called when the process finished or the user pressed the button.
279 """
280 if self.process is not None and \
281 self.process.state() != QProcess.NotRunning:
282 self.process.terminate()
283 QTimer.singleShot(2000, self.process, SLOT('kill()'))
284 self.process.waitForFinished(3000)
285
286 QApplication.restoreOverrideCursor()
287
288 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
289 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
290 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
291
292 self.inputGroup.setEnabled(False)
293
294 def __processBuffer(self):
295 """
296 Private method to process the buffered output of the svn log command.
297 """
298 ioEncoding = Preferences.getSystem("IOEncoding")
299
300 noEntries = 0
301 log = {"message" : []}
302 changedPaths = []
303 for s in self.buf:
304 if self.rx_rev1.exactMatch(s):
305 log["revision"] = self.rx_rev.cap(1)
306 log["author"] = self.rx_rev.cap(2)
307 log["date"] = self.rx_rev.cap(3)
308 # number of lines is ignored
309 elif self.rx_rev2.exactMatch(s):
310 log["revision"] = self.rx_rev2.cap(1)
311 log["author"] = self.rx_rev2.cap(2)
312 log["date"] = self.rx_rev2.cap(3)
313 # number of lines is ignored
314 elif self.rx_flags1.exactMatch(s):
315 changedPaths.append({\
316 "action" :
317 unicode(self.rx_flags1.cap(1).strip(), ioEncoding, 'replace'),
318 "path" :
319 unicode(self.rx_flags1.cap(2).strip(), ioEncoding, 'replace'),
320 "copyfrom_path" :
321 unicode(self.rx_flags1.cap(3).strip(), ioEncoding, 'replace'),
322 "copyfrom_revision" :
323 unicode(self.rx_flags1.cap(4).strip(), ioEncoding, 'replace'),
324 })
325 elif self.rx_flags2.exactMatch(s):
326 changedPaths.append({\
327 "action" :
328 unicode(self.rx_flags2.cap(1).strip(), ioEncoding, 'replace'),
329 "path" :
330 unicode(self.rx_flags2.cap(2).strip(), ioEncoding, 'replace'),
331 "copyfrom_path" : "",
332 "copyfrom_revision" : "",
333 })
334 elif self.rx_sep1.exactMatch(s) or self.rx_sep2.exactMatch(s):
335 if len(log) > 1:
336 self.__generateLogItem(log["author"], log["date"],
337 log["message"], log["revision"], changedPaths)
338 dt = QDate.fromString(log["date"], Qt.ISODate)
339 if not self.__maxDate.isValid() and not self.__minDate.isValid():
340 self.__maxDate = dt
341 self.__minDate = dt
342 else:
343 if self.__maxDate < dt:
344 self.__maxDate = dt
345 if self.__minDate > dt:
346 self.__minDate = dt
347 noEntries += 1
348 log = {"message" : []}
349 changedPaths = []
350 else:
351 if s.strip().endswith(":") or not s.strip():
352 continue
353 else:
354 log["message"].append(s)
355
356 self.logTree.doItemsLayout()
357 self.__resizeColumnsLog()
358 self.__resortLog()
359
360 if self.__started:
361 self.logTree.setCurrentItem(self.logTree.topLevelItem(0))
362 self.__started = False
363
364 if noEntries < self.limitSpinBox.value() and not self.cancelled:
365 self.nextButton.setEnabled(False)
366 self.limitSpinBox.setEnabled(False)
367
368 self.__filterLogsEnabled = False
369 self.fromDate.setMinimumDate(self.__minDate)
370 self.fromDate.setMaximumDate(self.__maxDate)
371 self.fromDate.setDate(self.__minDate)
372 self.toDate.setMinimumDate(self.__minDate)
373 self.toDate.setMaximumDate(self.__maxDate)
374 self.toDate.setDate(self.__maxDate)
375 self.__filterLogsEnabled = True
376 self.__filterLogs()
377
378 def __readStdout(self):
379 """
380 Private slot to handle the readyReadStandardOutput signal.
381
382 It reads the output of the process and inserts it into a buffer.
383 """
384 self.process.setReadChannel(QProcess.StandardOutput)
385
386 while self.process.canReadLine():
387 line = unicode(self.process.readLine())
388 self.buf.append(line)
389
390 def __readStderr(self):
391 """
392 Private slot to handle the readyReadStandardError signal.
393
394 It reads the error output of the process and inserts it into the
395 error pane.
396 """
397 if self.process is not None:
398 s = unicode(self.process.readAllStandardError())
399 self.errors.insertPlainText(s)
400 self.errors.ensureCursorVisible()
401
402 def __diffRevisions(self, rev1, rev2):
403 """
404 Private method to do a diff of two revisions.
405
406 @param rev1 first revision number (integer)
407 @param rev2 second revision number (integer)
408 """
409 if self.diff:
410 self.diff.close()
411 del self.diff
412 self.diff = SvnDiffDialog(self.vcs)
413 self.diff.show()
414 self.diff.start(self.filename, [rev1, rev2])
415
416 def on_buttonBox_clicked(self, button):
417 """
418 Private slot called by a button of the button box clicked.
419
420 @param button button that was clicked (QAbstractButton)
421 """
422 if button == self.buttonBox.button(QDialogButtonBox.Close):
423 self.close()
424 elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
425 self.cancelled = True
426 self.__finish()
427
428 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
429 def on_logTree_currentItemChanged(self, current, previous):
430 """
431 Private slot called, when the current item of the log tree changes.
432
433 @param current reference to the new current item (QTreeWidgetItem)
434 @param previous reference to the old current item (QTreeWidgetItem)
435 """
436 self.messageEdit.clear()
437 for line in current.data(0, self.__messageRole).toStringList():
438 self.messageEdit.append(line.strip())
439
440 self.filesTree.clear()
441 # TODO: change this for QVariant v2 to not use eval
442 changes = eval(current.data(0, self.__changesRole).toString())
443 if len(changes) > 0:
444 for change in changes:
445 self.__generateFileItem(change["action"], change["path"],
446 change["copyfrom_path"], change["copyfrom_revision"])
447 self.__resizeColumnsFiles()
448 self.__resortFiles()
449
450 self.diffPreviousButton.setEnabled(\
451 current != self.logTree.topLevelItem(self.logTree.topLevelItemCount() - 1))
452
453 @pyqtSlot()
454 def on_logTree_itemSelectionChanged(self):
455 """
456 Private slot called, when the selection has changed.
457 """
458 self.diffRevisionsButton.setEnabled(len(self.logTree.selectedItems()) == 2)
459
460 @pyqtSlot()
461 def on_nextButton_clicked(self):
462 """
463 Private slot to handle the Next button.
464 """
465 if self.__lastRev > 1:
466 self.__getLogEntries(self.__lastRev - 1)
467
468 @pyqtSlot()
469 def on_diffPreviousButton_clicked(self):
470 """
471 Private slot to handle the Diff to Previous button.
472 """
473 itm = self.logTree.currentItem()
474 if itm is None:
475 self.diffPreviousButton.setEnabled(False)
476 return
477 rev2 = int(itm.text(0))
478
479 itm = self.logTree.topLevelItem(self.logTree.indexOfTopLevelItem(itm) + 1)
480 if itm is None:
481 self.diffPreviousButton.setEnabled(False)
482 return
483 rev1 = int(itm.text(0))
484
485 self.__diffRevisions(rev1, rev2)
486
487 @pyqtSlot()
488 def on_diffRevisionsButton_clicked(self):
489 """
490 Private slot to handle the Compare Revisions button.
491 """
492 items = self.logTree.selectedItems()
493 if len(items) != 2:
494 self.diffRevisionsButton.setEnabled(False)
495 return
496
497 rev2 = int(items[0].text(0))
498 rev1 = int(items[1].text(0))
499
500 self.__diffRevisions(min(rev1, rev2), max(rev1, rev2))
501
502 @pyqtSlot(QDate)
503 def on_fromDate_dateChanged(self, date):
504 """
505 Private slot called, when the from date changes.
506
507 @param date new date (QDate)
508 """
509 self.__filterLogs()
510
511 @pyqtSlot(QDate)
512 def on_toDate_dateChanged(self, date):
513 """
514 Private slot called, when the from date changes.
515
516 @param date new date (QDate)
517 """
518 self.__filterLogs()
519
520 @pyqtSlot(str)
521 def on_fieldCombo_activated(self, txt):
522 """
523 Private slot called, when a new filter field is selected.
524
525 @param txt text of the selected field (QString)
526 """
527 self.__filterLogs()
528
529 @pyqtSlot(str)
530 def on_rxEdit_textChanged(self, txt):
531 """
532 Private slot called, when a filter expression is entered.
533
534 @param txt filter expression (string)
535 """
536 self.__filterLogs()
537
538 def __filterLogs(self):
539 """
540 Private method to filter the log entries.
541 """
542 if self.__filterLogsEnabled:
543 from_ = self.fromDate.date().toString("yyyy-MM-dd")
544 to_ = self.toDate.date().addDays(1).toString("yyyy-MM-dd")
545 txt = self.fieldCombo.currentText()
546 if txt == self.trUtf8("Author"):
547 fieldIndex = 1
548 searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive)
549 elif txt == self.trUtf8("Revision"):
550 fieldIndex = 0
551 txt = self.rxEdit.text()
552 if txt.startswith("^"):
553 searchRx = QRegExp("^\s*%s" % txt[1:], Qt.CaseInsensitive)
554 else:
555 searchRx = QRegExp(txt, Qt.CaseInsensitive)
556 else:
557 fieldIndex = 3
558 searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive)
559
560 currentItem = self.logTree.currentItem()
561 for topIndex in range(self.logTree.topLevelItemCount()):
562 topItem = self.logTree.topLevelItem(topIndex)
563 if topItem.text(2) <= to_ and topItem.text(2) >= from_ and \
564 searchRx.indexIn(topItem.text(fieldIndex)) > -1:
565 topItem.setHidden(False)
566 if topItem is currentItem:
567 self.on_logTree_currentItemChanged(topItem, None)
568 else:
569 topItem.setHidden(True)
570 if topItem is currentItem:
571 self.messageEdit.clear()
572 self.filesTree.clear()
573
574 @pyqtSlot()
575 def on_clearRxEditButton_clicked(self):
576 """
577 Private slot called by a click of the clear RX edit button.
578 """
579 self.rxEdit.clear()
580
581 @pyqtSlot(bool)
582 def on_stopCheckBox_clicked(self, checked):
583 """
584 Private slot called, when the stop on copy/move checkbox is clicked
585 """
586 self.vcs.getPlugin().setPreferences("StopLogOnCopy",
587 int(self.stopCheckBox.isChecked()))
588 self.nextButton.setEnabled(True)
589 self.limitSpinBox.setEnabled(True)
590
591 def on_passwordCheckBox_toggled(self, isOn):
592 """
593 Private slot to handle the password checkbox toggled.
594
595 @param isOn flag indicating the status of the check box (boolean)
596 """
597 if isOn:
598 self.input.setEchoMode(QLineEdit.Password)
599 else:
600 self.input.setEchoMode(QLineEdit.Normal)
601
602 @pyqtSlot()
603 def on_sendButton_clicked(self):
604 """
605 Private slot to send the input to the subversion process.
606 """
607 input = self.input.text()
608 input += os.linesep
609
610 if self.passwordCheckBox.isChecked():
611 self.errors.insertPlainText(os.linesep)
612 self.errors.ensureCursorVisible()
613 else:
614 self.errors.insertPlainText(input)
615 self.errors.ensureCursorVisible()
616
617 self.process.write(input)
618
619 self.passwordCheckBox.setChecked(False)
620 self.input.clear()
621
622 def on_input_returnPressed(self):
623 """
624 Private slot to handle the press of the return key in the input field.
625 """
626 self.intercept = True
627 self.on_sendButton_clicked()
628
629 def keyPressEvent(self, evt):
630 """
631 Protected slot to handle a key press event.
632
633 @param evt the key press event (QKeyEvent)
634 """
635 if self.intercept:
636 self.intercept = False
637 evt.accept()
638 return
639 QWidget.keyPressEvent(self, evt)

eric ide

mercurial