Plugins/VcsPlugins/vcsMercurial/HgLogDialog.py

branch
maintenance
changeset 5468
c307358a2ecc
parent 5447
852016bbdedb
parent 5467
44ab42f1e8b1
child 5469
b46f68bbd6b4
equal deleted inserted replaced
5447:852016bbdedb 5468:c307358a2ecc
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2010 - 2017 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog to show the output of the hg log command process.
8 """
9
10 from __future__ import unicode_literals
11 try:
12 str = unicode
13 except NameError:
14 pass
15
16 import os
17
18 from PyQt5.QtCore import pyqtSlot, QProcess, QTimer, QUrl, QByteArray, \
19 qVersion
20 from PyQt5.QtGui import QTextCursor
21 from PyQt5.QtWidgets import QWidget, QDialogButtonBox, QApplication, QLineEdit
22
23 from E5Gui.E5Application import e5App
24 from E5Gui import E5MessageBox
25
26 from .Ui_HgLogDialog import Ui_HgLogDialog
27
28 import Utilities
29
30
31 class HgLogDialog(QWidget, Ui_HgLogDialog):
32 """
33 Class implementing a dialog to show the output of the hg log command
34 process.
35
36 The dialog is nonmodal. Clicking a link in the upper text pane shows
37 a diff of the revisions.
38 """
39 def __init__(self, vcs, mode="log", bundle=None, isFile=False,
40 parent=None):
41 """
42 Constructor
43
44 @param vcs reference to the vcs object
45 @param mode mode of the dialog (string; one of log, incoming, outgoing)
46 @param bundle name of a bundle file (string)
47 @param isFile flag indicating log for a file is to be shown (boolean)
48 @param parent parent widget (QWidget)
49 """
50 super(HgLogDialog, self).__init__(parent)
51 self.setupUi(self)
52
53 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
54
55 self.process = QProcess()
56 self.vcs = vcs
57 if mode in ("log", "incoming", "outgoing"):
58 self.mode = mode
59 else:
60 self.mode = "log"
61 self.bundle = bundle
62 self.__hgClient = self.vcs.getClient()
63
64 self.contents.setHtml(
65 self.tr('<b>Processing your request, please wait...</b>'))
66
67 self.process.finished.connect(self.__procFinished)
68 self.process.readyReadStandardOutput.connect(self.__readStdout)
69 self.process.readyReadStandardError.connect(self.__readStderr)
70
71 self.contents.anchorClicked.connect(self.__sourceChanged)
72
73 self.revisions = [] # stack of remembered revisions
74 self.revString = self.tr('Revision')
75 self.projectMode = False
76
77 self.logEntries = [] # list of log entries
78 self.lastLogEntry = {}
79 self.fileCopies = {}
80 self.endInitialText = False
81 self.initialText = []
82
83 self.diff = None
84
85 self.sbsCheckBox.setEnabled(isFile)
86 self.sbsCheckBox.setVisible(isFile)
87
88 def closeEvent(self, e):
89 """
90 Protected slot implementing a close event handler.
91
92 @param e close event (QCloseEvent)
93 """
94 if self.__hgClient:
95 if self.__hgClient.isExecuting():
96 self.__hgClient.cancel()
97 else:
98 if self.process is not None and \
99 self.process.state() != QProcess.NotRunning:
100 self.process.terminate()
101 QTimer.singleShot(2000, self.process.kill)
102 self.process.waitForFinished(3000)
103
104 e.accept()
105
106 def start(self, fn, noEntries=0, revisions=None):
107 """
108 Public slot to start the hg log command.
109
110 @param fn filename to show the log for (string)
111 @param noEntries number of entries to show (integer)
112 @param revisions revisions to show log for (list of strings)
113 """
114 self.errorGroup.hide()
115 QApplication.processEvents()
116
117 self.intercept = False
118 self.filename = fn
119 self.dname, self.fname = self.vcs.splitPath(fn)
120
121 # find the root of the repo
122 self.repodir = self.dname
123 while not os.path.isdir(os.path.join(self.repodir, self.vcs.adminDir)):
124 self.repodir = os.path.dirname(self.repodir)
125 if os.path.splitdrive(self.repodir)[1] == os.sep:
126 return
127
128 self.projectMode = (self.fname == "." and self.dname == self.repodir)
129
130 self.activateWindow()
131 self.raise_()
132
133 preargs = []
134 args = self.vcs.initCommand(self.mode)
135 if noEntries and self.mode == "log":
136 args.append('--limit')
137 args.append(str(noEntries))
138 if self.mode in ("incoming", "outgoing"):
139 args.append("--newest-first")
140 if self.vcs.hasSubrepositories():
141 args.append("--subrepos")
142 if self.mode == "log":
143 args.append('--copies')
144 args.append('--template')
145 args.append(os.path.join(os.path.dirname(__file__),
146 "templates",
147 "logDialogBookmarkPhase.tmpl"))
148 if self.mode == "incoming":
149 if self.bundle:
150 args.append(self.bundle)
151 elif not self.vcs.hasSubrepositories():
152 project = e5App().getObject("Project")
153 self.vcs.bundleFile = os.path.join(
154 project.getProjectManagementDir(), "hg-bundle.hg")
155 if os.path.exists(self.vcs.bundleFile):
156 os.remove(self.vcs.bundleFile)
157 preargs = args[:]
158 preargs.append("--quiet")
159 preargs.append('--bundle')
160 preargs.append(self.vcs.bundleFile)
161 args.append(self.vcs.bundleFile)
162 if revisions:
163 for rev in revisions:
164 args.append("--rev")
165 args.append(rev)
166 if not self.projectMode:
167 args.append(self.filename)
168
169 if self.__hgClient:
170 self.inputGroup.setEnabled(False)
171 self.inputGroup.hide()
172
173 if preargs:
174 out, err = self.__hgClient.runcommand(preargs)
175 else:
176 err = ""
177 if err:
178 self.__showError(err)
179 elif self.mode != "incoming" or \
180 (self.vcs.bundleFile and
181 os.path.exists(self.vcs.bundleFile)) or \
182 self.bundle:
183 out, err = self.__hgClient.runcommand(args)
184 if err:
185 self.__showError(err)
186 if out and self.isVisible():
187 for line in out.splitlines(True):
188 self.__processOutputLine(line)
189 if self.__hgClient.wasCanceled():
190 break
191 self.__finish()
192 else:
193 self.process.kill()
194
195 self.process.setWorkingDirectory(self.repodir)
196
197 if preargs:
198 process = QProcess()
199 process.setWorkingDirectory(self.repodir)
200 process.start('hg', args)
201 procStarted = process.waitForStarted(5000)
202 if procStarted:
203 process.waitForFinished(30000)
204
205 if self.mode != "incoming" or \
206 (self.vcs.bundleFile and
207 os.path.exists(self.vcs.bundleFile)) or \
208 self.bundle:
209 self.process.start('hg', args)
210 procStarted = self.process.waitForStarted(5000)
211 if not procStarted:
212 self.inputGroup.setEnabled(False)
213 self.inputGroup.hide()
214 E5MessageBox.critical(
215 self,
216 self.tr('Process Generation Error'),
217 self.tr(
218 'The process {0} could not be started. '
219 'Ensure, that it is in the search path.'
220 ).format('hg'))
221 else:
222 self.__finish()
223
224 def __getParents(self, rev):
225 """
226 Private method to get the parents of the currently viewed
227 file/directory.
228
229 @param rev revision number to get parents for (string)
230 @return list of parent revisions (list of strings)
231 """
232 errMsg = ""
233 parents = []
234
235 if int(rev) > 0:
236 args = self.vcs.initCommand("parents")
237 if self.mode == "incoming":
238 if self.bundle:
239 args.append("--repository")
240 args.append(self.bundle)
241 elif self.vcs.bundleFile and \
242 os.path.exists(self.vcs.bundleFile):
243 args.append("--repository")
244 args.append(self.vcs.bundleFile)
245 args.append("--template")
246 args.append("{rev}:{node|short}\n")
247 args.append("-r")
248 args.append(rev)
249 if not self.projectMode:
250 args.append(self.filename)
251
252 output = ""
253 if self.__hgClient:
254 output, errMsg = self.__hgClient.runcommand(args)
255 else:
256 process = QProcess()
257 process.setWorkingDirectory(self.repodir)
258 process.start('hg', args)
259 procStarted = process.waitForStarted(5000)
260 if procStarted:
261 finished = process.waitForFinished(30000)
262 if finished and process.exitCode() == 0:
263 output = str(process.readAllStandardOutput(),
264 self.vcs.getEncoding(), 'replace')
265 else:
266 if not finished:
267 errMsg = self.tr(
268 "The hg process did not finish within 30s.")
269 else:
270 errMsg = self.tr("Could not start the hg executable.")
271
272 if errMsg:
273 E5MessageBox.critical(
274 self,
275 self.tr("Mercurial Error"),
276 errMsg)
277
278 if output:
279 parents = [p for p in output.strip().splitlines()]
280
281 return parents
282
283 def __procFinished(self, exitCode, exitStatus):
284 """
285 Private slot connected to the finished signal.
286
287 @param exitCode exit code of the process (integer)
288 @param exitStatus exit status of the process (QProcess.ExitStatus)
289 """
290 self.__finish()
291
292 def __finish(self):
293 """
294 Private slot called when the process finished or the user pressed
295 the button.
296 """
297 self.inputGroup.setEnabled(False)
298 self.inputGroup.hide()
299
300 self.contents.clear()
301
302 if not self.logEntries:
303 self.errors.append(self.tr("No log available for '{0}'")
304 .format(self.filename))
305 self.errorGroup.show()
306 return
307
308 html = ""
309
310 if self.initialText:
311 for line in self.initialText:
312 html += Utilities.html_encode(line.strip())
313 html += '<br />\n'
314 html += '{0}<br/>\n'.format(80 * "=")
315
316 for entry in self.logEntries:
317 fileCopies = {}
318 if entry["file_copies"]:
319 for fentry in entry["file_copies"].split(", "):
320 newName, oldName = fentry[:-1].split(" (")
321 fileCopies[newName] = oldName
322
323 rev, hexRev = entry["change"].split(":")
324 dstr = '<p><b>{0} {1}</b>'.format(self.revString, entry["change"])
325 if entry["parents"]:
326 parents = entry["parents"].split()
327 else:
328 parents = self.__getParents(rev)
329 for parent in parents:
330 url = QUrl()
331 url.setScheme("file")
332 url.setPath(self.filename)
333 if qVersion() >= "5.0.0":
334 query = parent.split(":")[0] + '_' + rev
335 url.setQuery(query)
336 else:
337 query = QByteArray()
338 query.append(parent.split(":")[0]).append('_').append(rev)
339 url.setEncodedQuery(query)
340 dstr += ' [<a href="{0}" name="{1}" id="{1}">{2}</a>]'.format(
341 url.toString(), query,
342 self.tr('diff to {0}').format(parent),
343 )
344 dstr += '<br />\n'
345 html += dstr
346
347 if "phase" in entry:
348 html += self.tr("Phase: {0}<br />\n")\
349 .format(entry["phase"])
350
351 html += self.tr("Branch: {0}<br />\n")\
352 .format(entry["branches"])
353
354 html += self.tr("Tags: {0}<br />\n").format(entry["tags"])
355
356 if "bookmarks" in entry:
357 html += self.tr("Bookmarks: {0}<br />\n")\
358 .format(entry["bookmarks"])
359
360 html += self.tr("Parents: {0}<br />\n")\
361 .format(entry["parents"])
362
363 html += self.tr('<i>Author: {0}</i><br />\n')\
364 .format(Utilities.html_encode(entry["user"]))
365
366 date, time = entry["date"].split()[:2]
367 html += self.tr('<i>Date: {0}, {1}</i><br />\n')\
368 .format(date, time)
369
370 for line in entry["description"]:
371 html += Utilities.html_encode(line.strip())
372 html += '<br />\n'
373
374 if entry["file_adds"]:
375 html += '<br />\n'
376 for f in entry["file_adds"].strip().split(", "):
377 if f in fileCopies:
378 html += self.tr(
379 'Added {0} (copied from {1})<br />\n')\
380 .format(Utilities.html_encode(f),
381 Utilities.html_encode(fileCopies[f]))
382 else:
383 html += self.tr('Added {0}<br />\n')\
384 .format(Utilities.html_encode(f))
385
386 if entry["files_mods"]:
387 html += '<br />\n'
388 for f in entry["files_mods"].strip().split(", "):
389 html += self.tr('Modified {0}<br />\n')\
390 .format(Utilities.html_encode(f))
391
392 if entry["file_dels"]:
393 html += '<br />\n'
394 for f in entry["file_dels"].strip().split(", "):
395 html += self.tr('Deleted {0}<br />\n')\
396 .format(Utilities.html_encode(f))
397
398 html += '</p>{0}<br/>\n'.format(60 * "=")
399
400 self.contents.setHtml(html)
401 tc = self.contents.textCursor()
402 tc.movePosition(QTextCursor.Start)
403 self.contents.setTextCursor(tc)
404 self.contents.ensureCursorVisible()
405
406 def __readStdout(self):
407 """
408 Private slot to handle the readyReadStandardOutput signal.
409
410 It reads the output of the process and inserts it into a buffer.
411 """
412 self.process.setReadChannel(QProcess.StandardOutput)
413
414 while self.process.canReadLine():
415 s = str(self.process.readLine(), self.vcs.getEncoding(), 'replace')
416 self.__processOutputLine(s)
417
418 def __processOutputLine(self, line):
419 """
420 Private method to process the lines of output.
421
422 @param line output line to be processed (string)
423 """
424 if line == "@@@\n":
425 self.logEntries.append(self.lastLogEntry)
426 self.lastLogEntry = {}
427 self.fileCopies = {}
428 else:
429 try:
430 key, value = line.split("|", 1)
431 except ValueError:
432 key = ""
433 value = line
434 if key == "change":
435 self.endInitialText = True
436 if key in ("change", "tags", "parents", "user", "date",
437 "file_copies", "file_adds", "files_mods", "file_dels",
438 "bookmarks", "phase"):
439 self.lastLogEntry[key] = value.strip()
440 elif key == "branches":
441 if value.strip():
442 self.lastLogEntry[key] = value.strip()
443 else:
444 self.lastLogEntry[key] = "default"
445 elif key == "description":
446 self.lastLogEntry[key] = [value.strip()]
447 else:
448 if self.endInitialText:
449 self.lastLogEntry["description"].append(value.strip())
450 else:
451 self.initialText.append(value)
452
453 def __readStderr(self):
454 """
455 Private slot to handle the readyReadStandardError signal.
456
457 It reads the error output of the process and inserts it into the
458 error pane.
459 """
460 if self.process is not None:
461 s = str(self.process.readAllStandardError(),
462 self.vcs.getEncoding(), 'replace')
463 self.__showError(s)
464
465 def __showError(self, out):
466 """
467 Private slot to show some error.
468
469 @param out error to be shown (string)
470 """
471 self.errorGroup.show()
472 self.errors.insertPlainText(out)
473 self.errors.ensureCursorVisible()
474
475 def __sourceChanged(self, url):
476 """
477 Private slot to handle the sourceChanged signal of the contents pane.
478
479 @param url the url that was clicked (QUrl)
480 """
481 filename = url.path()
482 if Utilities.isWindowsPlatform():
483 if filename.startswith("/"):
484 filename = filename[1:]
485 if qVersion() >= "5.0.0":
486 ver = url.query()
487 else:
488 ver = bytes(url.encodedQuery()).decode()
489 v1, v2 = ver.split('_')
490 if v1 == "" or v2 == "":
491 return
492 self.contents.scrollToAnchor(ver)
493
494 if self.sbsCheckBox.isEnabled() and self.sbsCheckBox.isChecked():
495 self.vcs.hgSbsDiff(filename, revisions=(v1, v2))
496 else:
497 if self.diff is None:
498 from .HgDiffDialog import HgDiffDialog
499 self.diff = HgDiffDialog(self.vcs)
500 self.diff.show()
501 self.diff.start(filename, [v1, v2], self.bundle)
502
503 def on_passwordCheckBox_toggled(self, isOn):
504 """
505 Private slot to handle the password checkbox toggled.
506
507 @param isOn flag indicating the status of the check box (boolean)
508 """
509 if isOn:
510 self.input.setEchoMode(QLineEdit.Password)
511 else:
512 self.input.setEchoMode(QLineEdit.Normal)
513
514 @pyqtSlot()
515 def on_sendButton_clicked(self):
516 """
517 Private slot to send the input to the hg process.
518 """
519 input = self.input.text()
520 input += os.linesep
521
522 if self.passwordCheckBox.isChecked():
523 self.errors.insertPlainText(os.linesep)
524 self.errors.ensureCursorVisible()
525 else:
526 self.errors.insertPlainText(input)
527 self.errors.ensureCursorVisible()
528
529 self.process.write(input)
530
531 self.passwordCheckBox.setChecked(False)
532 self.input.clear()
533
534 def on_input_returnPressed(self):
535 """
536 Private slot to handle the press of the return key in the input field.
537 """
538 self.intercept = True
539 self.on_sendButton_clicked()
540
541 def keyPressEvent(self, evt):
542 """
543 Protected slot to handle a key press event.
544
545 @param evt the key press event (QKeyEvent)
546 """
547 if self.intercept:
548 self.intercept = False
549 evt.accept()
550 return
551 super(HgLogDialog, self).keyPressEvent(evt)

eric ide

mercurial