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