|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2013 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog to show some summary information of the working |
|
8 directory state. |
|
9 """ |
|
10 |
|
11 from __future__ import unicode_literals |
|
12 try: |
|
13 str = unicode |
|
14 except NameError: |
|
15 pass |
|
16 |
|
17 import os |
|
18 |
|
19 from PyQt5.QtCore import pyqtSlot, QProcess, QTimer |
|
20 from PyQt5.QtWidgets import QDialog, QDialogButtonBox |
|
21 |
|
22 from E5Gui import E5MessageBox |
|
23 |
|
24 from .HgUtilities import prepareProcess |
|
25 |
|
26 from .Ui_HgSummaryDialog import Ui_HgSummaryDialog |
|
27 |
|
28 |
|
29 class HgSummaryDialog(QDialog, Ui_HgSummaryDialog): |
|
30 """ |
|
31 Class implementing a dialog to show some summary information of the working |
|
32 directory state. |
|
33 """ |
|
34 def __init__(self, vcs, parent=None): |
|
35 """ |
|
36 Constructor |
|
37 |
|
38 @param vcs reference to the vcs object |
|
39 @param parent parent widget (QWidget) |
|
40 """ |
|
41 super(HgSummaryDialog, self).__init__(parent) |
|
42 self.setupUi(self) |
|
43 |
|
44 self.refreshButton = self.buttonBox.addButton( |
|
45 self.tr("Refresh"), QDialogButtonBox.ActionRole) |
|
46 self.refreshButton.setToolTip( |
|
47 self.tr("Press to refresh the summary display")) |
|
48 self.refreshButton.setEnabled(False) |
|
49 |
|
50 self.vcs = vcs |
|
51 self.vcs.committed.connect(self.__committed) |
|
52 |
|
53 self.process = QProcess() |
|
54 prepareProcess(self.process, language="C") |
|
55 self.process.finished.connect(self.__procFinished) |
|
56 self.process.readyReadStandardOutput.connect(self.__readStdout) |
|
57 self.process.readyReadStandardError.connect(self.__readStderr) |
|
58 |
|
59 def closeEvent(self, e): |
|
60 """ |
|
61 Protected slot implementing a close event handler. |
|
62 |
|
63 @param e close event (QCloseEvent) |
|
64 """ |
|
65 if self.process is not None and \ |
|
66 self.process.state() != QProcess.NotRunning: |
|
67 self.process.terminate() |
|
68 QTimer.singleShot(2000, self.process.kill) |
|
69 self.process.waitForFinished(3000) |
|
70 |
|
71 e.accept() |
|
72 |
|
73 def start(self, path, mq=False, largefiles=False): |
|
74 """ |
|
75 Public slot to start the hg summary command. |
|
76 |
|
77 @param path path name of the working directory (string) |
|
78 @param mq flag indicating to show the queue status as well (boolean) |
|
79 @param largefiles flag indicating to show the largefiles status as |
|
80 well (boolean) |
|
81 """ |
|
82 self.errorGroup.hide() |
|
83 self.refreshButton.setEnabled(False) |
|
84 self.summary.clear() |
|
85 |
|
86 self.__path = path |
|
87 self.__mq = mq |
|
88 self.__largefiles = largefiles |
|
89 |
|
90 args = self.vcs.initCommand("summary") |
|
91 if self.vcs.canPull(): |
|
92 args.append("--remote") |
|
93 if self.__mq: |
|
94 args.append("--mq") |
|
95 if self.__largefiles: |
|
96 args.append("--large") |
|
97 |
|
98 # find the root of the repo |
|
99 repodir = self.__path |
|
100 while not os.path.isdir(os.path.join(repodir, self.vcs.adminDir)): |
|
101 repodir = os.path.dirname(repodir) |
|
102 if os.path.splitdrive(repodir)[1] == os.sep: |
|
103 return |
|
104 |
|
105 if self.process: |
|
106 self.process.kill() |
|
107 |
|
108 self.process.setWorkingDirectory(repodir) |
|
109 |
|
110 self.__buffer = [] |
|
111 |
|
112 self.process.start('hg', args) |
|
113 procStarted = self.process.waitForStarted(5000) |
|
114 if not procStarted: |
|
115 E5MessageBox.critical( |
|
116 self, |
|
117 self.tr('Process Generation Error'), |
|
118 self.tr( |
|
119 'The process {0} could not be started. ' |
|
120 'Ensure, that it is in the search path.' |
|
121 ).format('hg')) |
|
122 |
|
123 def __finish(self): |
|
124 """ |
|
125 Private slot called when the process finished or the user pressed |
|
126 the button. |
|
127 """ |
|
128 if self.process is not None and \ |
|
129 self.process.state() != QProcess.NotRunning: |
|
130 self.process.terminate() |
|
131 QTimer.singleShot(2000, self.process.kill) |
|
132 self.process.waitForFinished(3000) |
|
133 |
|
134 self.refreshButton.setEnabled(True) |
|
135 |
|
136 def on_buttonBox_clicked(self, button): |
|
137 """ |
|
138 Private slot called by a button of the button box clicked. |
|
139 |
|
140 @param button button that was clicked (QAbstractButton) |
|
141 """ |
|
142 if button == self.buttonBox.button(QDialogButtonBox.Close): |
|
143 self.close() |
|
144 elif button == self.refreshButton: |
|
145 self.on_refreshButton_clicked() |
|
146 |
|
147 def __procFinished(self, exitCode, exitStatus): |
|
148 """ |
|
149 Private slot connected to the finished signal. |
|
150 |
|
151 @param exitCode exit code of the process (integer) |
|
152 @param exitStatus exit status of the process (QProcess.ExitStatus) |
|
153 """ |
|
154 self.__processOutput(self.__buffer) |
|
155 self.__finish() |
|
156 |
|
157 def __readStdout(self): |
|
158 """ |
|
159 Private slot to handle the readyReadStandardOutput signal. |
|
160 |
|
161 It reads the output of the process, formats it and inserts it into |
|
162 the contents pane. |
|
163 """ |
|
164 if self.process is not None: |
|
165 self.process.setReadChannel(QProcess.StandardOutput) |
|
166 |
|
167 while self.process.canReadLine(): |
|
168 line = str(self.process.readLine(), self.vcs.getEncoding(), |
|
169 'replace') |
|
170 self.__buffer.append(line) |
|
171 |
|
172 def __readStderr(self): |
|
173 """ |
|
174 Private slot to handle the readyReadStandardError signal. |
|
175 |
|
176 It reads the error output of the process and inserts it into the |
|
177 error pane. |
|
178 """ |
|
179 if self.process is not None: |
|
180 s = str(self.process.readAllStandardError(), |
|
181 self.vcs.getEncoding(), 'replace') |
|
182 self.__showError(s) |
|
183 |
|
184 def __showError(self, out): |
|
185 """ |
|
186 Private slot to show some error. |
|
187 |
|
188 @param out error to be shown (string) |
|
189 """ |
|
190 self.errorGroup.show() |
|
191 self.errors.insertPlainText(out) |
|
192 self.errors.ensureCursorVisible() |
|
193 |
|
194 @pyqtSlot() |
|
195 def on_refreshButton_clicked(self): |
|
196 """ |
|
197 Private slot to refresh the status display. |
|
198 """ |
|
199 self.start(self.__path, mq=self.__mq) |
|
200 |
|
201 def __committed(self): |
|
202 """ |
|
203 Private slot called after the commit has finished. |
|
204 """ |
|
205 if self.isVisible(): |
|
206 self.on_refreshButton_clicked() |
|
207 |
|
208 def __processOutput(self, output): |
|
209 """ |
|
210 Private method to process the output into nice readable text. |
|
211 |
|
212 @param output output from the summary command (string) |
|
213 """ |
|
214 infoDict = {} |
|
215 |
|
216 # step 1: parse the output |
|
217 while output: |
|
218 line = output.pop(0) |
|
219 if ':' not in line: |
|
220 continue |
|
221 name, value = line.split(": ", 1) |
|
222 value = value.strip() |
|
223 |
|
224 if name == "parent": |
|
225 if " " in value: |
|
226 parent, tags = value.split(" ", 1) |
|
227 else: |
|
228 parent = value |
|
229 tags = "" |
|
230 rev, node = parent.split(":") |
|
231 |
|
232 remarks = [] |
|
233 if tags: |
|
234 if " (empty repository)" in tags: |
|
235 remarks.append("@EMPTY@") |
|
236 tags = tags.replace(" (empty repository)", "") |
|
237 if " (no revision checked out)" in tags: |
|
238 remarks.append("@NO_REVISION@") |
|
239 tags = tags.replace(" (no revision checked out)", "") |
|
240 else: |
|
241 tags = None |
|
242 |
|
243 value = infoDict.get(name, []) |
|
244 |
|
245 if rev == "-1": |
|
246 value.append((int(rev), node, tags, None, remarks)) |
|
247 else: |
|
248 message = output.pop(0).strip() |
|
249 value.append((int(rev), node, tags, message, remarks)) |
|
250 elif name == "branch": |
|
251 pass |
|
252 elif name == "bookmarks": |
|
253 pass |
|
254 elif name == "commit": |
|
255 stateDict = {} |
|
256 if "(" in value: |
|
257 if value.startswith("("): |
|
258 states = "" |
|
259 remark = value[1:-1] |
|
260 else: |
|
261 states, remark = value.rsplit(" (", 1) |
|
262 remark = remark[:-1] |
|
263 else: |
|
264 states = value |
|
265 remark = "" |
|
266 states = states.split(", ") |
|
267 for state in states: |
|
268 if state: |
|
269 count, category = state.split(" ") |
|
270 stateDict[category] = count |
|
271 value = (stateDict, remark) |
|
272 elif name == "update": |
|
273 if value.endswith("(current)"): |
|
274 value = ("@CURRENT@", 0, 0) |
|
275 elif value.endswith("(update)"): |
|
276 value = ("@UPDATE@", int(value.split(" ", 1)[0]), 0) |
|
277 elif value.endswith("(merge)"): |
|
278 parts = value.split(", ") |
|
279 value = ("@MERGE@", int(parts[0].split(" ", 1)[0]), |
|
280 int(parts[1].split(" ", 1)[0])) |
|
281 else: |
|
282 value = ("@UNKNOWN@", 0, 0) |
|
283 elif name == "remote": |
|
284 if value == "(synced)": |
|
285 value = (0, 0, 0, 0) |
|
286 else: |
|
287 inc = incb = outg = outgb = 0 |
|
288 for val in value.split(", "): |
|
289 count, category = val.split(" ", 1) |
|
290 if category == "outgoing": |
|
291 outg = int(count) |
|
292 elif category.endswith("incoming"): |
|
293 inc = int(count) |
|
294 elif category == "incoming bookmarks": |
|
295 incb = int(count) |
|
296 elif category == "outgoing bookmarks": |
|
297 outgb = int(count) |
|
298 value = (inc, outg, incb, outgb) |
|
299 elif name == "mq": |
|
300 if value == "(empty queue)": |
|
301 value = (0, 0) |
|
302 else: |
|
303 applied = unapplied = 0 |
|
304 for val in value.split(", "): |
|
305 count, category = val.split(" ", 1) |
|
306 if category == "applied": |
|
307 applied = int(count) |
|
308 elif category == "unapplied": |
|
309 unapplied = int(count) |
|
310 value = (applied, unapplied) |
|
311 elif name == "largefiles": |
|
312 if not value[0].isdigit(): |
|
313 value = 0 |
|
314 else: |
|
315 value = int(value.split(None, 1)[0]) |
|
316 else: |
|
317 # ignore unknown entries |
|
318 continue |
|
319 |
|
320 infoDict[name] = value |
|
321 |
|
322 # step 2: build the output |
|
323 if infoDict: |
|
324 info = ["<table>"] |
|
325 pindex = 0 |
|
326 for rev, node, tags, message, remarks in infoDict["parent"]: |
|
327 pindex += 1 |
|
328 changeset = "{0}:{1}".format(rev, node) |
|
329 if len(infoDict["parent"]) > 1: |
|
330 info.append(self.tr( |
|
331 "<tr><td><b>Parent #{0}</b></td><td>{1}</td></tr>") |
|
332 .format(pindex, changeset)) |
|
333 else: |
|
334 info.append(self.tr( |
|
335 "<tr><td><b>Parent</b></td><td>{0}</td></tr>") |
|
336 .format(changeset)) |
|
337 if tags: |
|
338 info.append(self.tr( |
|
339 "<tr><td><b>Tags</b></td><td>{0}</td></tr>") |
|
340 .format('<br/>'.join(tags.split()))) |
|
341 if message: |
|
342 info.append(self.tr( |
|
343 "<tr><td><b>Commit Message</b></td><td>{0}</td></tr>") |
|
344 .format(message)) |
|
345 if remarks: |
|
346 rem = [] |
|
347 if "@EMPTY@" in remarks: |
|
348 rem.append(self.tr("empty repository")) |
|
349 if "@NO_REVISION@" in remarks: |
|
350 rem.append(self.tr("no revision checked out")) |
|
351 info.append(self.tr( |
|
352 "<tr><td><b>Remarks</b></td><td>{0}</td></tr>") |
|
353 .format(", ".join(rem))) |
|
354 if "branch" in infoDict: |
|
355 info.append(self.tr( |
|
356 "<tr><td><b>Branch</b></td><td>{0}</td></tr>") |
|
357 .format(infoDict["branch"])) |
|
358 if "bookmarks" in infoDict: |
|
359 bookmarks = infoDict["bookmarks"].split() |
|
360 for i in range(len(bookmarks)): |
|
361 if bookmarks[i].startswith("*"): |
|
362 bookmarks[i] = "<b>{0}</b>".format(bookmarks[i]) |
|
363 info.append(self.tr( |
|
364 "<tr><td><b>Bookmarks</b></td><td>{0}</td></tr>") |
|
365 .format('<br/>'.join(bookmarks))) |
|
366 if "commit" in infoDict: |
|
367 cinfo = [] |
|
368 for category, count in infoDict["commit"][0].items(): |
|
369 if category == "modified": |
|
370 cinfo.append(self.tr("{0} modified").format(count)) |
|
371 elif category == "added": |
|
372 cinfo.append(self.tr("{0} added").format(count)) |
|
373 elif category == "removed": |
|
374 cinfo.append(self.tr("{0} removed").format(count)) |
|
375 elif category == "renamed": |
|
376 cinfo.append(self.tr("{0} renamed").format(count)) |
|
377 elif category == "copied": |
|
378 cinfo.append(self.tr("{0} copied").format(count)) |
|
379 elif category == "deleted": |
|
380 cinfo.append(self.tr("{0} deleted").format(count)) |
|
381 elif category == "unknown": |
|
382 cinfo.append(self.tr("{0} unknown").format(count)) |
|
383 elif category == "ignored": |
|
384 cinfo.append(self.tr("{0} ignored").format(count)) |
|
385 elif category == "unresolved": |
|
386 cinfo.append( |
|
387 self.tr("{0} unresolved").format(count)) |
|
388 elif category == "subrepos": |
|
389 cinfo.append(self.tr("{0} subrepos").format(count)) |
|
390 remark = infoDict["commit"][1] |
|
391 if remark == "merge": |
|
392 cinfo.append(self.tr("Merge needed")) |
|
393 elif remark == "new branch": |
|
394 cinfo.append(self.tr("New Branch")) |
|
395 elif remark == "head closed": |
|
396 cinfo.append(self.tr("Head is closed")) |
|
397 elif remark == "clean": |
|
398 cinfo.append(self.tr("No commit required")) |
|
399 elif remark == "new branch head": |
|
400 cinfo.append(self.tr("New Branch Head")) |
|
401 info.append(self.tr( |
|
402 "<tr><td><b>Commit Status</b></td><td>{0}</td></tr>") |
|
403 .format("<br/>".join(cinfo))) |
|
404 if "update" in infoDict: |
|
405 if infoDict["update"][0] == "@CURRENT@": |
|
406 uinfo = self.tr("current") |
|
407 elif infoDict["update"][0] == "@UPDATE@": |
|
408 uinfo = self.tr( |
|
409 "%n new changeset(s)<br/>Update required", "", |
|
410 infoDict["update"][1]) |
|
411 elif infoDict["update"][0] == "@MERGE@": |
|
412 uinfo1 = self.tr( |
|
413 "%n new changeset(s)", "", infoDict["update"][1]) |
|
414 uinfo2 = self.tr( |
|
415 "%n branch head(s)", "", infoDict["update"][2]) |
|
416 uinfo = self.tr( |
|
417 "{0}<br/>{1}<br/>Merge required", |
|
418 "0 is changesets, 1 is branch heads")\ |
|
419 .format(uinfo1, uinfo2) |
|
420 else: |
|
421 uinfo = self.tr("unknown status") |
|
422 info.append(self.tr( |
|
423 "<tr><td><b>Update Status</b></td><td>{0}</td></tr>") |
|
424 .format(uinfo)) |
|
425 if "remote" in infoDict: |
|
426 if infoDict["remote"] == (0, 0, 0, 0): |
|
427 rinfo = self.tr("synched") |
|
428 else: |
|
429 li = [] |
|
430 if infoDict["remote"][0]: |
|
431 li.append(self.tr("1 or more incoming changesets")) |
|
432 if infoDict["remote"][1]: |
|
433 li.append(self.tr("%n outgoing changeset(s)", "", |
|
434 infoDict["remote"][1])) |
|
435 if infoDict["remote"][2]: |
|
436 li.append(self.tr("%n incoming bookmark(s)", "", |
|
437 infoDict["remote"][2])) |
|
438 if infoDict["remote"][3]: |
|
439 li.append(self.tr("%n outgoing bookmark(s)", "", |
|
440 infoDict["remote"][3])) |
|
441 rinfo = "<br/>".join(li) |
|
442 info.append(self.tr( |
|
443 "<tr><td><b>Remote Status</b></td><td>{0}</td></tr>") |
|
444 .format(rinfo)) |
|
445 if "mq" in infoDict: |
|
446 if infoDict["mq"] == (0, 0): |
|
447 qinfo = self.tr("empty queue") |
|
448 else: |
|
449 li = [] |
|
450 if infoDict["mq"][0]: |
|
451 li.append(self.tr("{0} applied") |
|
452 .format(infoDict["mq"][0])) |
|
453 if infoDict["mq"][1]: |
|
454 li.append(self.tr("{0} unapplied") |
|
455 .format(infoDict["mq"][1])) |
|
456 qinfo = "<br/>".join(li) |
|
457 info.append(self.tr( |
|
458 "<tr><td><b>Queues Status</b></td><td>{0}</td></tr>") |
|
459 .format(qinfo)) |
|
460 if "largefiles" in infoDict: |
|
461 if infoDict["largefiles"] == 0: |
|
462 lfInfo = self.tr("No files to upload") |
|
463 else: |
|
464 lfInfo = self.tr("%n file(s) to upload", "", |
|
465 infoDict["largefiles"]) |
|
466 info.append(self.tr( |
|
467 "<tr><td><b>Large Files</b></td><td>{0}</td></tr>") |
|
468 .format(lfInfo)) |
|
469 info.append("</table>") |
|
470 else: |
|
471 info = [self.tr("<p>No status information available.</p>")] |
|
472 |
|
473 self.summary.insertHtml("\n".join(info)) |