|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2003 - 2009 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog to show the output of the svn diff command process. |
|
8 """ |
|
9 |
|
10 import os |
|
11 import sys |
|
12 import types |
|
13 |
|
14 import pysvn |
|
15 |
|
16 from PyQt4.QtCore import * |
|
17 from PyQt4.QtGui import * |
|
18 |
|
19 from E4Gui.E4Application import e4App |
|
20 |
|
21 from SvnDialogMixin import SvnDialogMixin |
|
22 from Ui_SvnDiffDialog import Ui_SvnDiffDialog |
|
23 |
|
24 import Utilities |
|
25 |
|
26 class SvnDiffDialog(QWidget, SvnDialogMixin, Ui_SvnDiffDialog): |
|
27 """ |
|
28 Class implementing a dialog to show the output of the svn diff command. |
|
29 """ |
|
30 def __init__(self, vcs, parent = None): |
|
31 """ |
|
32 Constructor |
|
33 |
|
34 @param vcs reference to the vcs object |
|
35 @param parent parent widget (QWidget) |
|
36 """ |
|
37 QWidget.__init__(self, parent) |
|
38 self.setupUi(self) |
|
39 SvnDialogMixin.__init__(self) |
|
40 |
|
41 self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False) |
|
42 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) |
|
43 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) |
|
44 |
|
45 self.vcs = vcs |
|
46 |
|
47 if Utilities.isWindowsPlatform(): |
|
48 self.contents.setFontFamily("Lucida Console") |
|
49 else: |
|
50 self.contents.setFontFamily("Monospace") |
|
51 |
|
52 self.cNormalFormat = self.contents.currentCharFormat() |
|
53 self.cAddedFormat = self.contents.currentCharFormat() |
|
54 self.cAddedFormat.setBackground(QBrush(QColor(190, 237, 190))) |
|
55 self.cRemovedFormat = self.contents.currentCharFormat() |
|
56 self.cRemovedFormat.setBackground(QBrush(QColor(237, 190, 190))) |
|
57 self.cLineNoFormat = self.contents.currentCharFormat() |
|
58 self.cLineNoFormat.setBackground(QBrush(QColor(255, 220, 168))) |
|
59 |
|
60 self.client = self.vcs.getClient() |
|
61 self.client.callback_cancel = \ |
|
62 self._clientCancelCallback |
|
63 self.client.callback_get_login = \ |
|
64 self._clientLoginCallback |
|
65 self.client.callback_ssl_server_trust_prompt = \ |
|
66 self._clientSslServerTrustPromptCallback |
|
67 |
|
68 def __getVersionArg(self, version): |
|
69 """ |
|
70 Private method to get a pysvn revision object for the given version number. |
|
71 |
|
72 @param version revision (integer or string) |
|
73 @return revision object (pysvn.Revision) |
|
74 """ |
|
75 if type(version) == type(1) or type(version) == type(1L): |
|
76 return pysvn.Revision(pysvn.opt_revision_kind.number, version) |
|
77 elif version.startswith("{"): |
|
78 dateStr = version[1:-1] |
|
79 secs = QDateTime.fromString(dateStr, Qt.ISODate).toTime_t() |
|
80 return pysvn.Revision(pysvn.opt_revision_kind.date, secs) |
|
81 elif version == "HEAD": |
|
82 return pysvn.Revision(pysvn.opt_revision_kind.head) |
|
83 elif version == "COMMITTED": |
|
84 return pysvn.Revision(pysvn.opt_revision_kind.committed) |
|
85 elif version == "BASE": |
|
86 return pysvn.Revision(pysvn.opt_revision_kind.base) |
|
87 elif version == "WORKING": |
|
88 return pysvn.Revision(pysvn.opt_revision_kind.working) |
|
89 elif version == "PREV": |
|
90 return pysvn.Revision(pysvn.opt_revision_kind.previous) |
|
91 else: |
|
92 return pysvn.Revision(pysvn.opt_revision_kind.unspecified) |
|
93 |
|
94 def __getDiffSummaryKind(self, summaryKind): |
|
95 """ |
|
96 Private method to get a string descripion of the diff summary. |
|
97 |
|
98 @param summaryKind (pysvn.diff_summarize.summarize_kind) |
|
99 @return one letter string indicating the change type (string) |
|
100 """ |
|
101 if summaryKind == pysvn.diff_summarize_kind.delete: |
|
102 return "D" |
|
103 elif summaryKind == pysvn.diff_summarize_kind.modified: |
|
104 return "M" |
|
105 elif summaryKind == pysvn.diff_summarize_kind.added: |
|
106 return "A" |
|
107 elif summaryKind == pysvn.diff_summarize_kind.normal: |
|
108 return "N" |
|
109 else: |
|
110 return " " |
|
111 |
|
112 def start(self, fn, versions = None, urls = None, summary = False, pegRev = None): |
|
113 """ |
|
114 Public slot to start the svn diff command. |
|
115 |
|
116 @param fn filename to be diffed (string) |
|
117 @param versions list of versions to be diffed (list of up to 2 integer or None) |
|
118 @keyparam urls list of repository URLs (list of 2 strings) |
|
119 @keyparam summary flag indicating a summarizing diff |
|
120 (only valid for URL diffs) (boolean) |
|
121 @keyparam pegRev revision number the filename is valid (integer) |
|
122 """ |
|
123 self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False) |
|
124 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) |
|
125 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) |
|
126 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) |
|
127 |
|
128 self._reset() |
|
129 self.errorGroup.hide() |
|
130 |
|
131 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) |
|
132 QApplication.processEvents() |
|
133 self.filename = fn |
|
134 |
|
135 self.contents.clear() |
|
136 self.paras = 0 |
|
137 |
|
138 if Utilities.hasEnvironmentEntry('TEMP'): |
|
139 tmpdir = Utilities.getEnvironmentEntry('TEMP') |
|
140 elif Utilities.hasEnvironmentEntry('TMPDIR'): |
|
141 tmpdir = Utilities.getEnvironmentEntry('TMPDIR') |
|
142 elif Utilities.hasEnvironmentEntry('TMP'): |
|
143 tmpdir = Utilities.getEnvironmentEntry('TMP') |
|
144 elif os.path.exists('/var/tmp'): |
|
145 tmpdir = '/var/tmp' |
|
146 elif os.path.exists('/usr/tmp'): |
|
147 tmpdir = '/usr/tmp' |
|
148 elif os.path.exists('/tmp'): |
|
149 tmpdir = '/tmp' |
|
150 else: |
|
151 QMessageBox.critical(self, |
|
152 self.trUtf8("Subversion Diff"), |
|
153 self.trUtf8("""There is no temporary directory available.""")) |
|
154 return |
|
155 |
|
156 tmpdir = os.path.join(tmpdir, 'svn_tmp') |
|
157 if not os.path.exists(tmpdir): |
|
158 os.mkdir(tmpdir) |
|
159 |
|
160 opts = self.vcs.options['global'] + self.vcs.options['diff'] |
|
161 recurse = "--non-recursive" not in opts |
|
162 |
|
163 if versions is not None: |
|
164 self.raise_() |
|
165 self.activateWindow() |
|
166 rev1 = self.__getVersionArg(versions[0]) |
|
167 if len(versions) == 1: |
|
168 rev2 = self.__getVersionArg("WORKING") |
|
169 else: |
|
170 rev2 = self.__getVersionArg(versions[1]) |
|
171 else: |
|
172 rev1 = self.__getVersionArg("BASE") |
|
173 rev2 = self.__getVersionArg("WORKING") |
|
174 |
|
175 if urls is not None: |
|
176 rev1 = self.__getVersionArg("HEAD") |
|
177 rev2 = self.__getVersionArg("HEAD") |
|
178 |
|
179 if type(fn) is types.ListType: |
|
180 dname, fnames = self.vcs.splitPathList(fn) |
|
181 else: |
|
182 dname, fname = self.vcs.splitPath(fn) |
|
183 fnames = [fname] |
|
184 |
|
185 locker = QMutexLocker(self.vcs.vcsExecutionMutex) |
|
186 cwd = os.getcwd() |
|
187 os.chdir(dname) |
|
188 try: |
|
189 ppath = e4App().getObject('Project').getProjectPath() |
|
190 dname = dname.replace(ppath, '') |
|
191 if dname: |
|
192 dname += "/" |
|
193 for name in fnames: |
|
194 self.__showError(self.trUtf8("Processing file '{0}'...\n").format(name)) |
|
195 if urls is not None: |
|
196 url1 = "%s/%s%s" % (urls[0], dname, name) |
|
197 url2 = "%s/%s%s" % (urls[1], dname, name) |
|
198 if summary: |
|
199 diff_summary = self.client.diff_summarize(\ |
|
200 url1, revision1 = rev1, |
|
201 url_or_path2 = url2, revision2 = rev2, |
|
202 recurse = recurse) |
|
203 diff_list = [] |
|
204 for diff_sum in diff_summary: |
|
205 diff_list.append("%s %s" % \ |
|
206 (self.__getDiffSummaryKind(diff_sum['summarize_kind']), |
|
207 diff_sum['path'])) |
|
208 diffText = os.linesep.join(diff_list) |
|
209 else: |
|
210 diffText = self.client.diff(tmpdir, |
|
211 url1, revision1 = rev1, |
|
212 url_or_path2 = url2, revision2 = rev2, |
|
213 recurse = recurse) |
|
214 else: |
|
215 if pegRev is not None: |
|
216 diffText = self.client.diff_peg(tmpdir, name, |
|
217 peg_revision = self.__getVersionArg(pegRev), |
|
218 revision_start = rev1, revision_end = rev2, recurse = recurse) |
|
219 else: |
|
220 diffText = self.client.diff(tmpdir, name, |
|
221 revision1 = rev1, revision2 = rev2, recurse = recurse) |
|
222 counter = 0 |
|
223 for line in diffText.splitlines(): |
|
224 self.__appendText("%s%s" % (line, os.linesep)) |
|
225 counter += 1 |
|
226 if counter == 30: |
|
227 # check for cancel every 30 lines |
|
228 counter = 0 |
|
229 if self._clientCancelCallback(): |
|
230 break |
|
231 if self._clientCancelCallback(): |
|
232 break |
|
233 except pysvn.ClientError, e: |
|
234 self.__showError(e.args[0]) |
|
235 locker.unlock() |
|
236 os.chdir(cwd) |
|
237 self.__finish() |
|
238 |
|
239 if self.paras == 0: |
|
240 self.contents.insertPlainText(\ |
|
241 self.trUtf8('There is no difference.')) |
|
242 return |
|
243 |
|
244 self.buttonBox.button(QDialogButtonBox.Save).setEnabled(True) |
|
245 |
|
246 def __appendText(self, line): |
|
247 """ |
|
248 Private method to append text to the end of the contents pane. |
|
249 |
|
250 @param line line of text to insert (string) |
|
251 """ |
|
252 if line.startswith('+') or line.startswith('>') or line.startswith('A '): |
|
253 format = self.cAddedFormat |
|
254 elif line.startswith('-') or line.startswith('<') or line.startswith('D '): |
|
255 format = self.cRemovedFormat |
|
256 elif line.startswith('@@'): |
|
257 format = self.cLineNoFormat |
|
258 else: |
|
259 format = self.cNormalFormat |
|
260 |
|
261 tc = self.contents.textCursor() |
|
262 tc.movePosition(QTextCursor.End) |
|
263 self.contents.setTextCursor(tc) |
|
264 self.contents.setCurrentCharFormat(format) |
|
265 self.contents.insertPlainText(line) |
|
266 self.paras += 1 |
|
267 |
|
268 def __finish(self): |
|
269 """ |
|
270 Private slot called when the user pressed the button. |
|
271 """ |
|
272 QApplication.restoreOverrideCursor() |
|
273 |
|
274 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) |
|
275 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) |
|
276 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) |
|
277 |
|
278 tc = self.contents.textCursor() |
|
279 tc.movePosition(QTextCursor.Start) |
|
280 self.contents.setTextCursor(tc) |
|
281 self.contents.ensureCursorVisible() |
|
282 |
|
283 self._cancel() |
|
284 |
|
285 def on_buttonBox_clicked(self, button): |
|
286 """ |
|
287 Private slot called by a button of the button box clicked. |
|
288 |
|
289 @param button button that was clicked (QAbstractButton) |
|
290 """ |
|
291 if button == self.buttonBox.button(QDialogButtonBox.Close): |
|
292 self.close() |
|
293 elif button == self.buttonBox.button(QDialogButtonBox.Cancel): |
|
294 self.__finish() |
|
295 elif button == self.buttonBox.button(QDialogButtonBox.Save): |
|
296 self.on_saveButton_clicked() |
|
297 |
|
298 @pyqtSlot() |
|
299 def on_saveButton_clicked(self): |
|
300 """ |
|
301 Private slot to handle the Save button press. |
|
302 |
|
303 It saves the diff shown in the dialog to a file in the local |
|
304 filesystem. |
|
305 """ |
|
306 if type(self.filename) is types.ListType: |
|
307 if len(self.filename) > 1: |
|
308 fname = self.vcs.splitPathList(self.filename)[0] |
|
309 else: |
|
310 dname, fname = self.vcs.splitPath(self.filename[0]) |
|
311 if fname != '.': |
|
312 fname = "%s.diff" % self.filename[0] |
|
313 else: |
|
314 fname = dname |
|
315 else: |
|
316 fname = self.vcs.splitPath(self.filename)[0] |
|
317 |
|
318 fname, selectedFilter = QFileDialog.getSaveFileNameAndFilter(\ |
|
319 self, |
|
320 self.trUtf8("Save Diff"), |
|
321 fname, |
|
322 self.trUtf8("Patch Files (*.diff)"), |
|
323 None, |
|
324 QFileDialog.Options(QFileDialog.DontConfirmOverwrite)) |
|
325 |
|
326 if not fname: |
|
327 return # user aborted |
|
328 |
|
329 ext = QFileInfo(fname).suffix() |
|
330 if not ext: |
|
331 ex = selectedFilter.split("(*")[1].split(")")[0] |
|
332 if ex: |
|
333 fname += ex |
|
334 if QFileInfo(fname).exists(): |
|
335 res = QMessageBox.warning(self, |
|
336 self.trUtf8("Save Diff"), |
|
337 self.trUtf8("<p>The patch file <b>{0}</b> already exists.</p>") |
|
338 .format(fname), |
|
339 QMessageBox.StandardButtons(\ |
|
340 QMessageBox.Abort | \ |
|
341 QMessageBox.Save), |
|
342 QMessageBox.Abort) |
|
343 if res != QMessageBox.Save: |
|
344 return |
|
345 fname = Utilities.toNativeSeparators(fname) |
|
346 |
|
347 try: |
|
348 f = open(fname, "wb") |
|
349 f.write(self.contents.toPlainText()) |
|
350 f.close() |
|
351 except IOError, why: |
|
352 QMessageBox.critical(self, self.trUtf8('Save Diff'), |
|
353 self.trUtf8('<p>The patch file <b>{0}</b> could not be saved.' |
|
354 '<br>Reason: {1}</p>') |
|
355 .format(fname, unicode(why))) |
|
356 |
|
357 def __showError(self, msg): |
|
358 """ |
|
359 Private slot to show an error message. |
|
360 |
|
361 @param msg error message to show (string) |
|
362 """ |
|
363 self.errorGroup.show() |
|
364 self.errors.insertPlainText(msg) |
|
365 self.errors.ensureCursorVisible() |