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.
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:
27 subprocess
= mysubprocess
31 qconnect
= qt
.QObject
.connect
33 applicationName
= 'Git commit tool'
40 class CommitError(Exception):
41 def __init__(self
, operation
, msg
):
42 self
.operation
= operation
45 class MainWidget(qt
.QMainWindow
):
46 def __init__(self
, parent
=None, name
=None):
47 qt
.QMainWindow
.__init
__(self
, parent
, name
)
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
)
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)
83 def about(self
, ignore
):
84 qt
.QMessageBox
.about(self
, "About " + applicationName
,
85 "<qt><center><h1>" + applicationName
+ "</h1></center>\n" +
86 "<center>Copyright © 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
):
93 self
.files
[0].patch
= self
.text
.text()
97 self
.text
.setUpdatesEnabled(False)
99 self
.text
.setText(self
.files
[index
].patch
)
100 self
.text
.setReadOnly(False)
102 self
.setColorPatch(self
.files
[index
].patch
)
103 self
.text
.setReadOnly(True)
105 self
.text
.setContentsPos(0, 0)
106 self
.text
.setUpdatesEnabled(True)
109 def setColorPatch(self
, patch
):
112 for l
in patch
.split('\n'):
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
)
124 def commit(self
, id):
125 if self
.prevCur
== 0:
126 self
.files
[0].patch
= self
.text
.text()
130 for x
in range(1, len(self
.files
)):
131 if self
.filesW
.isSelected(x
):
132 selFileNames
.append(self
.files
[x
].text
)
134 keepFiles
.append(self
.files
[x
])
136 commitMsg
= str(self
.files
[0].patch
)
139 qt
.QMessageBox
.information(self
, "Commit - " + applicationName
,
140 "No files selected for commit.", "&Ok")
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
),
153 doCommit(keepFiles
, commitMsg
)
154 except CommitError
, e
:
155 qt
.QMessageBox
.warning(self
, "Commit Failed - " + applicationName
,
156 "Commit failed during " + e
.operation
+ ": " + e
.msg
,
159 qt
.QMessageBox
.warning(self
, "Commit Failed - " + applicationName
,
160 "Commit failed: " + e
.strerror
,
165 def setFiles(self
, files
):
168 f
.text
= "Commit message"
172 self
.filesW
.insertStrList([x
.text
for x
in self
.files
])
173 self
.filesW
.item(0).setSelectable(False)
176 self
.text
.setText('')
178 self
.filesW
.setCurrentItem(0)
180 def refreshFiles(self
, ignored
=None):
186 cacheHeadDiff
= parseDiff('git-diff-cache -z --cached HEAD')
188 # The set of files that are different in the cache compared to HEAD
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
204 if file.change
== 'N':
205 runProgram(['git-update-cache', '--force-remove', file.srcName
])
207 runProgram(['git-update-cache', '--add', '--replace', '--cacheinfo',
208 file.srcMode
, file.srcSHA
, file.srcName
])
210 tree
= runProgram(['git-write-tree'])
212 commit
= runProgram(['git-commit-tree', tree
, '-p', 'HEAD'], msg
)
213 commit
= commit
.rstrip()
216 f
= open(os
.environ
['GIT_DIR'] + '/HEAD', 'w+')
220 raise CommitError('write to ' + os
.environ
['GIT_DIR'] + '/HEAD', e
.strerror
)
223 os
.unlink(os
.environ
['GIT_DIR'] + '/MERGE_HEAD')
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
):
238 msg
= re
.sub(commitMsgRE
, '\n', msg
)
243 return ' ' + msg
.replace('\n', ' \n')
248 parseDiffRE
= re
.compile(':([0-9]+) ([0-9]+) ([0-9a-f]{40}) ([0-9a-f]{40}) ([MCRNDUT])([0-9]*)')
250 inp
= runProgram(prog
)
253 recs
= inp
.split("\0")
254 recs
.pop() # remove last entry (which is '')
258 m
= parseDiffRE
.match(rec
)
261 print "Unknown output from " + str(prog
) + "!: " + rec
+ "\n"
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)
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
)
277 f
.patch
= getPatch(f
.srcName
)
281 f
.text
= 'Copy from ' + f
.srcName
+ ' to ' + f
.dstName
283 f
.text
= 'Rename from ' + f
.srcName
+ ' to ' + f
.dstName
285 f
.text
= 'New file: ' + f
.srcName
287 f
.text
= 'Deleted file: ' + f
.srcName
289 f
.text
= 'Type change: ' + f
.srcName
293 except StopIteration:
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.
301 return parseDiff('git-diff-cache -z -M --cached HEAD')
303 def getPatch(file, otherFile
= None):
305 f
= [file, otherFile
]
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')
314 class ProgramError(Exception):
315 def __init__(self
, program
, err
):
316 self
.program
= program
319 def runProgram(prog
, input=None):
320 debug('runProgram prog: ' + str(prog
) + " input: " + str(input))
321 if type(prog
) is str:
324 progStr
= ' '.join(prog
)
327 pop
= subprocess
.Popen(prog
,
328 shell
= type(prog
) is str,
329 stderr
=subprocess
.STDOUT
,
330 stdout
=subprocess
.PIPE
,
331 stdin
=subprocess
.PIPE
)
333 debug("strerror: " + e
.strerror
)
334 raise ProgramError(progStr
, e
.strerror
)
337 pop
.stdin
.write(input)
341 out
= pop
.stdout
.read()
343 debug("error output: " + out
)
344 raise ProgramError(progStr
, out
)
345 debug("output: " + out
.replace('\0', '\n'))
348 app
= qt
.QApplication(sys
.argv
)
350 app
.setMainWidget(mw
)
353 mw
.setGeometry(100, 100, 500, 600)
356 if not os
.environ
.has_key('GIT_DIR'):
357 os
.environ
['GIT_DIR'] = '.git'
359 sys
.exit(app
.exec_loop())