Fix the about box...
[hgct.git] / citool.py
blob5671502571d4f8594128556b4a52c3a908e0fc01
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
20 # PyQt3 and python 2.4 isn't currently available together on
21 # Debian/testing we do therefore use the following quite ugly work
22 # around. The module 'mysubprocess' is just a copy of the 'subprocess'
23 # module from the Python 2.4 distribution.
24 ver = sys.version_info
25 if ver[0] <= 2 and ver[1] <= 3:
26 import mysubprocess
27 subprocess = mysubprocess
28 else:
29 import subprocess
31 qconnect = qt.QObject.connect
32 Qt = qt.Qt
33 applicationName = 'Git commit tool'
34 DEBUG = 1
36 def debug(str):
37 if DEBUG:
38 print str
40 class CommitError(Exception):
41 def __init__(self, operation, msg):
42 self.operation = operation
43 self.msg = msg
45 class MainWidget(qt.QMainWindow):
46 def __init__(self, parent=None, name=None):
47 qt.QMainWindow.__init__(self, parent, name)
48 l = qt.QVBox(self)
50 fW = qt.QListBox(l)
51 fW.setFocus()
52 fW.setSelectionMode(qt.QListBox.Multi)
54 text = qt.QTextEdit(l)
55 text.setTextFormat(qt.QTextEdit.PlainText)
57 self.setCentralWidget(l)
58 self.setCaption(applicationName)
60 self.newSelLambda = lambda i: self.newSelection(i)
61 qconnect(fW, qt.SIGNAL("highlighted(int)"), self.newSelLambda)
63 ops = qt.QPopupMenu(self)
64 ops.insertItem("Commit", self.commit, Qt.CTRL+Qt.Key_T)
65 ops.insertItem("Refresh", self.refreshFiles, Qt.CTRL+Qt.Key_R)
67 m = self.menuBar()
68 m.insertItem("&Operations", ops)
70 h = qt.QPopupMenu(self)
71 h.insertItem("&About", self.about)
72 m.insertItem("&Help", h)
74 self.diffNew = qt.QColor(0, 150, 0)
75 self.diffRemove = qt.QColor(200, 0, 0)
76 self.diffHead = qt.QColor(200, 0, 200)
77 self.diffStd = qt.QColor(0, 0, 0)
79 self.filesW = fW
80 self.layout = l
81 self.text = text
83 def about(self, ignore):
84 qt.QMessageBox.about(self, "About " + applicationName,
85 "<qt><center><h1>" + applicationName + "</h1></center>\n" +
86 "<center>Copyright &copy; 2005 Fredrik Kuivinen</center>\n" +
87 "<p>This program is free software; you can redistribute it and/or modify " +
88 "it under the terms of the GNU General Public License version 2 as " +
89 "published by the Free Software Foundation.</p></qt>")
91 def newSelection(self, index):
92 if self.prevCur == 0:
93 self.files[0].patch = self.text.text()
95 self.prevCur = index
97 self.text.setUpdatesEnabled(False)
98 if index == 0:
99 self.text.setText(self.files[index].patch)
100 self.text.setReadOnly(False)
101 else:
102 self.setColorPatch(self.files[index].patch)
103 self.text.setReadOnly(True)
105 self.text.setContentsPos(0, 0)
106 self.text.setUpdatesEnabled(True)
107 self.text.update()
109 def setColorPatch(self, patch):
110 t = self.text
111 t.setText('')
112 for l in patch.split('\n'):
113 if len(l) > 0:
114 c = l[0]
115 else:
116 c = ''
118 if c == '+': t.setColor(self.diffNew)
119 elif c == '-': t.setColor(self.diffRemove)
120 elif c == '@': t.setColor(self.diffHead)
121 else: t.setColor(self.diffStd)
122 t.append(l + '\n')
124 def commit(self, id):
125 if self.prevCur == 0:
126 self.files[0].patch = self.text.text()
128 selFileNames = []
129 keepFiles = []
130 for x in range(1, len(self.files)):
131 if self.filesW.isSelected(x):
132 selFileNames.append(self.files[x].text)
133 else:
134 keepFiles.append(self.files[x])
136 commitMsg = str(self.files[0].patch)
138 if not selFileNames:
139 qt.QMessageBox.information(self, "Commit - " + applicationName,
140 "No files selected for commit.", "&Ok")
141 return
143 commitMsg = fixCommitMsgWhiteSpace(commitMsg)
144 if(qt.QMessageBox.question(self, "Commit - " + applicationName,
145 "Do you want to commit the following file(s):\n\n" +
146 indentMsg('\n'.join(selFileNames)) + '\n\n' +
147 'with the commit message:\n\n' +
148 indentMsg(commitMsg),
149 '&Yes', '&No')):
150 return
151 else:
152 try:
153 doCommit(keepFiles, commitMsg)
154 except CommitError, e:
155 qt.QMessageBox.warning(self, "Commit Failed - " + applicationName,
156 "Commit failed during " + e.operation + ": " + e.msg,
157 '&Ok')
158 except OSError, e:
159 qt.QMessageBox.warning(self, "Commit Failed - " + applicationName,
160 "Commit failed: " + e.strerror,
161 '&Ok')
162 else:
163 self.refreshFiles()
165 def setFiles(self, files):
166 self.filesW.clear()
167 f = File()
168 f.text = "Commit message"
169 self.files = [f]
170 for x in files:
171 self.files.append(x)
172 self.filesW.insertStrList([x.text for x in self.files])
173 self.filesW.item(0).setSelectable(False)
175 f.patch = ''
176 self.text.setText('')
177 self.prevCur = 0
178 self.filesW.setCurrentItem(0)
180 def refreshFiles(self, ignored=None):
181 updateCache()
182 files = getFiles()
183 self.setFiles(files)
185 def updateCache():
186 cacheHeadDiff = parseDiff('git-diff-cache -z --cached HEAD')
188 # The set of files that are different in the cache compared to HEAD
189 cacheHeadChange = {}
190 for f in cacheHeadDiff:
191 cacheHeadChange[f.srcName] = True
193 noncacheHeadDiff = parseDiff('git-diff-cache -z HEAD')
194 for f in noncacheHeadDiff:
195 if (f.srcSHA == '0'*40 or f.dstSHA == '0'*40) and not cacheHeadChange.has_key(f.srcName):
196 runProgram(['git-update-cache', '--remove', f.srcName])
198 def doCommit(filesToKeep, msg):
199 for file in filesToKeep:
200 # If we have a new file in the cache which we do not want to
201 # commit we have to remove it from the cache. We will add this
202 # cache entry back in to the cache at the end of this
203 # function.
204 if file.change == 'N':
205 runProgram(['git-update-cache', '--force-remove', file.srcName])
206 else:
207 runProgram(['git-update-cache', '--add', '--replace', '--cacheinfo',
208 file.srcMode, file.srcSHA, file.srcName])
210 tree = runProgram(['git-write-tree'])
211 tree = tree.rstrip()
212 commit = runProgram(['git-commit-tree', tree, '-p', 'HEAD'], msg)
213 commit = commit.rstrip()
215 try:
216 f = open(os.environ['GIT_DIR'] + '/HEAD', 'w+')
217 f.write(commit)
218 f.close()
219 except OSError, e:
220 raise CommitError('write to ' + os.environ['GIT_DIR'] + '/HEAD', e.strerror)
222 try:
223 os.unlink(os.environ['GIT_DIR'] + '/MERGE_HEAD')
224 except OSError:
225 pass
227 for file in filesToKeep:
228 # Don't add files that are going to be deleted back to the cache
229 if file.change != 'D':
230 runProgram(['git-update-cache', '--add', '--replace', '--cacheinfo',
231 file.dstMode, file.dstSHA, file.dstName])
234 commitMsgRE = re.compile('[ \t\r\f\v]*\n\\s*\n')
235 def fixCommitMsgWhiteSpace(msg):
236 msg = msg.lstrip()
237 msg = msg.rstrip()
238 msg = re.sub(commitMsgRE, '\n', msg)
239 msg += '\n'
240 return msg
242 def indentMsg(msg):
243 return ' ' + msg.replace('\n', ' \n')
245 class File:
246 pass
248 parseDiffRE = re.compile(':([0-9]+) ([0-9]+) ([0-9a-f]{40}) ([0-9a-f]{40}) ([MCRNDUT])([0-9]*)')
249 def parseDiff(prog):
250 inp = runProgram(prog)
251 ret = []
252 try:
253 recs = inp.split("\0")
254 recs.pop() # remove last entry (which is '')
255 it = recs.__iter__()
256 while True:
257 rec = it.next()
258 m = parseDiffRE.match(rec)
260 if not m:
261 print "Unknown output from " + str(prog) + "!: " + rec + "\n"
262 continue
264 f = File()
265 f.srcMode = m.group(1)
266 f.dstMode = m.group(2)
267 f.srcSHA = m.group(3)
268 f.dstSHA = m.group(4)
269 f.change = m.group(5)
270 f.score = m.group(6)
271 f.srcName = f.dstName = it.next()
273 if f.change == 'C' or f.change == 'R':
274 f.dstName = it.next()
275 f.patch = getPatch(f.srcName, f.dstName)
276 else:
277 f.patch = getPatch(f.srcName)
279 c = f.change
280 if c == 'C':
281 f.text = 'Copy from ' + f.srcName + ' to ' + f.dstName
282 elif c == 'R':
283 f.text = 'Rename from ' + f.srcName + ' to ' + f.dstName
284 elif c == 'N':
285 f.text = 'New file: ' + f.srcName
286 elif c == 'D':
287 f.text = 'Deleted file: ' + f.srcName
288 elif c == 'T':
289 f.text = 'Type change: ' + f.srcName
290 else:
291 f.text = f.srcName
292 ret.append(f)
293 except StopIteration:
294 pass
295 return ret
298 # HEAD is src in the returned File objects. That is, srcName is the
299 # name in HEAD and dstName is the name in the cache.
300 def getFiles():
301 return parseDiff('git-diff-cache -z -M --cached HEAD')
303 def getPatch(file, otherFile = None):
304 if otherFile:
305 f = [file, otherFile]
306 else:
307 f = [file]
308 # (ignored, fin) = os.popen2(['git-diff-cache', '-p', '-M', '--cached', 'HEAD'] + f, 'r')
309 (ignored, fin) = os.popen2(['git-diff-cache', '-p', '-M', 'HEAD'] + f, 'r')
310 ret = fin.read()
311 fin.close()
312 return ret
314 class ProgramError(Exception):
315 def __init__(self, program, err):
316 self.program = program
317 self.error = err
319 def runProgram(prog, input=None):
320 debug('runProgram prog: ' + str(prog) + " input: " + str(input))
321 if type(prog) is str:
322 progStr = prog
323 else:
324 progStr = ' '.join(prog)
326 try:
327 pop = subprocess.Popen(prog,
328 shell = type(prog) is str,
329 stderr=subprocess.STDOUT,
330 stdout=subprocess.PIPE,
331 stdin=subprocess.PIPE)
332 except OSError, e:
333 debug("strerror: " + e.strerror)
334 raise ProgramError(progStr, e.strerror)
336 if input != None:
337 pop.stdin.write(input)
338 pop.stdin.close()
340 code = pop.wait()
341 out = pop.stdout.read()
342 if code != 0:
343 debug("error output: " + out)
344 raise ProgramError(progStr, out)
345 debug("output: " + out.replace('\0', '\n'))
346 return out
348 app = qt.QApplication(sys.argv)
349 mw = MainWidget()
350 app.setMainWidget(mw)
352 mw.refreshFiles()
353 mw.setGeometry(100, 100, 500, 600)
354 mw.show()
356 if not os.environ.has_key('GIT_DIR'):
357 os.environ['GIT_DIR'] = '.git'
359 sys.exit(app.exec_loop())