Remove the non-synchronized cache mess.
[hgct.git] / main.py
blob430afe6efdd2b038013bc18cef4fa4e0cc53a14e
1 #!/usr/bin/env python
3 # Copyright (c) 2005 Fredrik Kuivinen <freku045@student.liu.se>
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License version 2 as
7 # published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 import sys, math, random, qt, os, re, signal, sets
20 from ctcore import *
22 # Determine semantics according to executable name. Default to git.
23 if re.split('/', sys.argv[0]).pop() == 'hgct':
24 import hg as scm
25 else:
26 print "defaulting to import git because arg = %s" % sys.argv[0]
27 import git as scm
29 qconnect = qt.QObject.connect
30 Qt = qt.Qt
31 applicationName = 'Commit Tool'
32 shortName = 'ct'
33 version = 'v0.1'
34 #DEBUG = 1
36 class CommitError(Exception):
37 def __init__(self, operation, msg):
38 self.operation = operation
39 self.msg = msg
41 class FileState:
42 pass
44 class MyListItem(qt.QCheckListItem):
45 def __init__(self, parent, file, commitMsg = False):
46 qt.QListViewItem.__init__(self, parent, file.text, qt.QCheckListItem.CheckBox)
47 self.file = file
48 self.commitMsg = commitMsg
50 def compare(self, item, col, asc):
51 if self.commitMsg:
52 if asc:
53 return -1
54 else:
55 return 1
56 elif item.commitMsg:
57 if asc:
58 return 1
59 else:
60 return -1
61 else:
62 return cmp(self.key(col, asc), item.key(col, asc))
64 def paintCell(self, p, cg, col, w, a):
65 if self.commitMsg:
66 qt.QListViewItem.paintCell(self, p, cg, col, w, a)
67 else:
68 qt.QCheckListItem.paintCell(self, p, cg, col, w, a)
70 def isSelected(self):
71 return self.state() == qt.QCheckListItem.On
73 def setSelected(self, s):
74 if s:
75 self.setState(qt.QCheckListItem.On)
76 else:
77 self.setState(qt.QCheckListItem.Off)
79 class MyListView(qt.QListView):
80 def __init__(self, parent=None, name=None):
81 qt.QListView.__init__(self, parent, name)
83 def __iter__(self):
84 return ListViewIterator(self)
86 class ListViewIterator:
87 def __init__(self, listview):
88 self.it = qt.QListViewItemIterator(listview)
90 def next(self):
91 cur = self.it.current()
92 if cur:
93 self.it += 1
94 if cur.commitMsg:
95 return self.next()
96 else:
97 return cur
98 else:
99 raise StopIteration()
101 def __iter__(self):
102 return self
104 class MainWidget(qt.QMainWindow):
105 def __init__(self, parent=None, name=None):
106 qt.QMainWindow.__init__(self, parent, name)
107 splitter = qt.QSplitter(Qt.Vertical, self)
109 fW = MyListView(splitter)
110 fW.setFocus()
111 fW.setSelectionMode(qt.QListView.NoSelection)
112 fW.addColumn('Description')
113 fW.setResizeMode(qt.QListView.AllColumns)
115 text = qt.QWidgetStack(splitter)
117 self.setCentralWidget(splitter)
118 self.setCaption(applicationName)
120 self.newCurLambda = lambda i: self.currentChange(i)
121 qconnect(fW, qt.SIGNAL("currentChanged(QListViewItem*)"), self.newCurLambda)
123 ops = qt.QPopupMenu(self)
124 ops.insertItem("Commit Selected Files", self.commit, Qt.CTRL+Qt.Key_T)
125 ops.insertItem("Refresh", self.refreshFiles, Qt.CTRL+Qt.Key_R)
127 m = self.menuBar()
128 m.insertItem("&Operations", ops)
130 h = qt.QPopupMenu(self)
131 h.insertItem("&About", self.about)
132 m.insertItem("&Help", h)
134 qconnect(fW, qt.SIGNAL("contextMenuRequested(QListViewItem*, const QPoint&, int)"),
135 self.contextMenuRequestedSlot)
136 self.fileOps = qt.QPopupMenu(self)
137 self.fileOps.insertItem("Toggle selection", self.toggleFile)
138 self.fileOps.insertItem("Edit", self.editFile, Qt.CTRL+Qt.Key_E)
140 # The following attribute is set by contextMenuRequestedSlot
141 # and currentChange and used by the fileOps
142 self.currentContextItem = None
144 self.patchColors = {'std': 'black', 'new': '#009600', 'remove': '#C80000', 'head': '#C800C8'}
146 self.filesW = fW
147 self.files = []
148 self.splitter = splitter
149 self.text = text
151 f = File()
152 f.text = "Commit message"
153 f.textW = self.newTextEdit()
154 f.textW.setTextFormat(Qt.PlainText)
155 f.textW.setReadOnly(False)
156 self.cmitFile = f
157 self.createCmitItem()
158 self.splitter.setSizes(eval(str(settings.readEntry('splitter', '[400, 200]')[0])))
160 self.editorProcesses = sets.Set()
162 def closeEvent(self, e):
163 p = self.pos()
164 settings.writeEntry('x', p.x()),
165 settings.writeEntry('y', p.y()),
167 s = self.size()
168 settings.writeEntry('width', s.width()),
169 settings.writeEntry('height', s.height())
171 settings.writeEntry('splitter', str(self.splitter.sizes()))
172 e.accept()
174 def createCmitItem(self):
175 self.cmitItem = MyListItem(self.filesW, self.cmitFile, True)
176 self.cmitItem.setSelectable(False)
177 self.filesW.insertItem(self.cmitItem)
179 def about(self, ignore):
180 qt.QMessageBox.about(self, "About " + applicationName,
181 "<qt><center><h1>" + applicationName + " " + version + """</h1></center>\n
182 <center>Copyright &copy; 2005 Fredrik Kuivinen &lt;freku045@student.liu.se&gt;
183 </center>\n<p>This program is free software; you can redistribute it and/or
184 modify it under the terms of the GNU General Public License version 2 as
185 published by the Free Software Foundation.</p></qt>""")
187 def contextMenuRequestedSlot(self, item, pos, col):
188 if item and not item.commitMsg:
189 self.currentContextItem = item
190 self.fileOps.exec_loop(qt.QCursor.pos())
191 else:
192 self.currentContextItem = None
194 def toggleFile(self, ignored):
195 it = self.currentContextItem
196 if not it:
197 return
199 if it.isSelected():
200 it.setSelected(False)
201 else:
202 it.setSelected(True)
204 def editFile(self, ignored):
205 it = self.currentContextItem
206 if not it:
207 return
209 ed = getEditor()
210 if not ed:
211 qt.QMessageBox.warning(self, 'No editor found',
212 '''No editor found. Gct looks for an editor to execute in the environment
213 variable GCT_EDITOR, if that variable is not set it will use the variable
214 EDITOR.''')
215 return
217 # This piece of code is not entirely satisfactory. If the user
218 # has EDITOR set to 'vi', or some other non-X application, the
219 # editor will be started in the terminal which (h)gct was
220 # started in. A better approach would be to close stdin and
221 # stdout after the fork but before the exec, but this doesn't
222 # seem to be possible with QProcess.
223 p = qt.QProcess(ed)
224 p.addArgument(it.file.dstName)
225 p.setCommunication(0)
226 qconnect(p, qt.SIGNAL('processExited()'), self.editorExited)
227 if not p.launch(qt.QByteArray()):
228 qt.QMessageBox.warning(self, 'Failed to launch editor',
229 shortName + ' failed to launch the ' + \
230 'editor. The command used was: ' + \
231 ed + ' ' + it.file.dstName)
232 else:
233 self.editorProcesses.add(p)
235 def editorExited(self):
236 p = self.sender()
237 status = p.exitStatus()
238 file = str(p.arguments()[1])
239 editor = str(p.arguments()[0]) + ' ' + file
240 if not p.normalExit():
241 qt.QMessageBox.warning(self, 'Editor failure',
242 'The editor, ' + editor + ', exited abnormally.')
243 elif status != 0:
244 qt.QMessageBox.warning(self, 'Editor failure',
245 'The editor, ' + editor + ', exited with exit code ' + str(status))
247 self.editorProcesses.remove(p)
248 scm.doUpdateCache(file)
249 self.refreshFiles()
251 def currentChange(self, item):
252 self.text.raiseWidget(item.file.textW)
253 self.text.update()
254 self.currentContextItem = item
256 def selectedItems(self):
257 ret = []
258 for item in self.filesW:
259 if item.isSelected():
260 ret.append(item)
261 return ret
263 def commit(self, id):
264 selFileNames = []
265 keepFiles = []
266 commitFiles = []
268 for item in self.filesW:
269 debug("file: " + item.file.text)
270 if item.isSelected():
271 selFileNames.append(item.file.text)
272 commitFiles.append(item.file.dstName)
273 else:
274 keepFiles.append(item.file)
276 commitMsg = str(self.cmitItem.file.textW.text())
278 if not selFileNames:
279 qt.QMessageBox.information(self, "Commit - " + applicationName,
280 "No files selected for commit.", "&Ok")
281 return
283 commitMsg = fixCommitMsgWhiteSpace(commitMsg)
284 if(qt.QMessageBox.question(self, "Confirm Commit - " + applicationName,
285 '''<qt><p>Do you want to commit the following file(s):</p><blockquote>''' +
286 '<br>'.join(selFileNames) +
287 '''</blockquote><p>with the commit message:</p><blockquote><pre>''' +
288 str(qt.QStyleSheet.escape(commitMsg)) + '</pre></blockquote></qt>',
289 '&Yes', '&No')):
290 return
291 else:
292 try:
293 scm.doCommit(keepFiles, commitFiles, commitMsg)
294 except CommitError, e:
295 qt.QMessageBox.warning(self, "Commit Failed - " + applicationName,
296 "Commit failed during " + e.operation + ": " + e.msg,
297 '&Ok')
298 except OSError, e:
299 qt.QMessageBox.warning(self, "Commit Failed - " + applicationName,
300 "Commit failed: " + e.strerror,
301 '&Ok')
302 else:
303 self.cmitItem.file.textW.setText('')
304 self.refreshFiles()
306 def getFileState(self):
307 ret = FileState()
308 cur = self.filesW.currentItem()
309 if cur and cur != self.cmitItem:
310 ret.current = self.filesW.currentItem().file.srcName
311 else:
312 ret.current = None
313 ret.selected = {}
315 for x in self.filesW:
316 if x.isSelected():
317 ret.selected[x.file.srcName] = True
318 return ret
320 def restoreFileState(self, state):
321 for x in self.filesW:
322 if state.selected.has_key(x.file.srcName):
323 x.setSelected(True)
324 if x.file.srcName == state.current:
325 self.filesW.setCurrentItem(x)
327 def newTextEdit(self):
328 ret = qt.QTextEdit()
329 self.text.addWidget(ret)
330 return ret
332 def setFiles(self, files):
333 state = self.getFileState()
334 self.filesW.clear()
335 self.createCmitItem()
336 for f in self.files:
337 self.text.removeWidget(f.textW)
339 self.files = []
340 for f in files:
341 f.textW = self.newTextEdit()
342 f.textW.setReadOnly(False)
343 f.textW.setTextFormat(Qt.RichText)
344 f.textW.setText(formatPatchRichText(f.patch, self.patchColors))
345 self.files.append(f)
346 self.filesW.insertItem(MyListItem(self.filesW, f))
348 self.filesW.setCurrentItem(self.cmitItem)
350 # For some reason the currentChanged signal isn't emitted
351 # here. We call currentChange ourselves instead.
352 self.currentChange(self.cmitItem)
354 self.restoreFileState(state)
356 def refreshFiles(self, ignored=None):
357 self.setFiles(scm.getFiles())
359 commitMsgRE = re.compile('[ \t\r\f\v]*\n\\s*\n')
360 def fixCommitMsgWhiteSpace(msg):
361 msg = msg.lstrip()
362 msg = msg.rstrip()
363 msg = re.sub(commitMsgRE, '\n\n', msg)
364 msg += '\n'
365 return msg
367 def formatPatchRichText(patch, colors):
368 ret = ['<qt><pre><font color="', colors['std'], '">']
369 prev = ' '
370 for l in patch.split('\n'):
371 if len(l) > 0:
372 c = l[0]
373 else:
374 c = ' '
376 if c != prev:
377 if c == '+': style = 'new'
378 elif c == '-': style = 'remove'
379 elif c == '@': style = 'head'
380 else: style = 'std'
381 ret.extend(['</font><font color="', colors[style], '">'])
382 prev = c
383 ret.extend([str(qt.QStyleSheet.escape(l)), '\n'])
384 ret.append('</pre></qt>')
385 return ''.join(ret)
387 class ProgramError(Exception):
388 def __init__(self, program, err):
389 self.program = program
390 self.error = err
392 def getEditor():
393 if os.environ.has_key('GCT_EDITOR'):
394 return os.environ['GCT_EDITOR']
395 elif os.environ.has_key('EDITOR'):
396 return os.environ['EDITOR']
397 else:
398 return None
400 scm.repoValid()
402 app = qt.QApplication(sys.argv)
403 settings = qt.QSettings()
404 settings.beginGroup('/' + shortName)
405 settings.beginGroup('/geometry/')
407 mw = MainWidget()
408 mw.refreshFiles()
410 mw.resize(settings.readNumEntry('width', 500)[0],
411 settings.readNumEntry('height', 600)[0])
414 # The following code doesn't work correctly in some (at least
415 # Metacity) window
416 # managers. http://doc.trolltech.com/3.3/geometry.html contains some
417 # information about this issue.
418 # mw.move(settings.readNumEntry('x', 100)[0],
419 # settings.readNumEntry('y', 100)[0])
421 mw.show()
422 app.setMainWidget(mw)
425 # Handle CTRL-C appropriately
426 signal.signal(signal.SIGINT, lambda s, f: app.quit())
428 ret = app.exec_loop()
429 del settings
430 sys.exit(ret)