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
, signal
, sets
22 # Determine semantics according to executable name. Default to git.
23 if os
.path
.basename(sys
.argv
[0]) == 'hgct':
24 print "defaulting to import hg because arg = %s" % sys
.argv
[0]
29 qconnect
= qt
.QObject
.connect
31 applicationName
= 'Commit Tool'
36 class CommitError(Exception):
37 def __init__(self
, operation
, msg
):
38 self
.operation
= operation
44 class MyListItem(qt
.QCheckListItem
):
45 def __init__(self
, parent
, file, commitMsg
= False):
46 qt
.QCheckListItem
.__init
__(self
, parent
, file.text
, qt
.QCheckListItem
.CheckBox
)
48 self
.commitMsg
= commitMsg
50 def compare(self
, item
, col
, asc
):
62 return cmp(self
.key(col
, asc
), item
.key(col
, asc
))
64 def paintCell(self
, p
, cg
, col
, w
, a
):
66 qt
.QListViewItem
.paintCell(self
, p
, cg
, col
, w
, a
)
68 qt
.QCheckListItem
.paintCell(self
, p
, cg
, col
, w
, a
)
71 return self
.state() == qt
.QCheckListItem
.On
73 def setSelected(self
, s
):
75 self
.setState(qt
.QCheckListItem
.On
)
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
)
84 return ListViewIterator(self
)
86 class ListViewIterator
:
87 def __init__(self
, listview
):
88 self
.it
= qt
.QListViewItemIterator(listview
)
91 cur
= self
.it
.current()
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
)
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
)
126 ops
.insertItem("Select All", self
.selectAll
, Qt
.CTRL
+Qt
.Key_A
)
127 ops
.insertItem("Unselect All", self
.unselectAll
, Qt
.CTRL
+Qt
.Key_U
)
130 m
.insertItem("&Operations", ops
)
132 h
= qt
.QPopupMenu(self
)
133 h
.insertItem("&About", self
.about
)
134 m
.insertItem("&Help", h
)
136 qconnect(fW
, qt
.SIGNAL("contextMenuRequested(QListViewItem*, const QPoint&, int)"),
137 self
.contextMenuRequestedSlot
)
138 self
.fileOps
= qt
.QPopupMenu(self
)
139 self
.fileOps
.insertItem("Toggle selection", self
.toggleFile
)
140 self
.fileOps
.insertItem("Edit", self
.editFile
, Qt
.CTRL
+Qt
.Key_E
)
141 self
.fileOps
.insertItem("Discard changes", self
.discardFile
)
143 # The following attribute is set by contextMenuRequestedSlot
144 # and currentChange and used by the fileOps
145 self
.currentContextItem
= None
147 self
.patchColors
= {'std': 'black', 'new': '#009600', 'remove': '#C80000', 'head': '#C800C8'}
151 self
.splitter
= splitter
155 f
.text
= "Commit message"
156 f
.textW
= self
.newTextEdit()
157 f
.textW
.setTextFormat(Qt
.PlainText
)
158 f
.textW
.setReadOnly(False)
160 self
.createCmitItem()
161 self
.splitter
.setSizes(eval(str(settings
.readEntry('splitter', '[400, 200]')[0])))
163 self
.editorProcesses
= sets
.Set()
165 def closeEvent(self
, e
):
167 settings
.writeEntry('x', p
.x()),
168 settings
.writeEntry('y', p
.y()),
171 settings
.writeEntry('width', s
.width()),
172 settings
.writeEntry('height', s
.height())
174 settings
.writeEntry('splitter', str(self
.splitter
.sizes()))
177 def createCmitItem(self
):
178 self
.cmitItem
= MyListItem(self
.filesW
, self
.cmitFile
, True)
179 self
.cmitItem
.setSelectable(False)
180 self
.filesW
.insertItem(self
.cmitItem
)
182 def about(self
, ignore
):
183 qt
.QMessageBox
.about(self
, "About " + applicationName
,
184 "<qt><center><h1>" + applicationName
+ " " + version
+ """</h1></center>\n
185 <center>Copyright © 2005 Fredrik Kuivinen <freku045@student.liu.se>
186 </center>\n<p>This program is free software; you can redistribute it and/or
187 modify it under the terms of the GNU General Public License version 2 as
188 published by the Free Software Foundation.</p></qt>""")
190 def contextMenuRequestedSlot(self
, item
, pos
, col
):
191 if item
and not item
.commitMsg
:
192 self
.currentContextItem
= item
193 self
.fileOps
.exec_loop(qt
.QCursor
.pos())
195 self
.currentContextItem
= None
197 def toggleFile(self
, ignored
):
198 it
= self
.currentContextItem
203 it
.setSelected(False)
207 def editFile(self
, ignored
):
208 it
= self
.currentContextItem
214 qt
.QMessageBox
.warning(self
, 'No editor found',
215 '''No editor found. Gct looks for an editor to execute in the environment
216 variable GCT_EDITOR, if that variable is not set it will use the variable
220 # This piece of code is not entirely satisfactory. If the user
221 # has EDITOR set to 'vi', or some other non-X application, the
222 # editor will be started in the terminal which (h)gct was
223 # started in. A better approach would be to close stdin and
224 # stdout after the fork but before the exec, but this doesn't
225 # seem to be possible with QProcess.
227 p
.addArgument(it
.file.dstName
)
228 p
.setCommunication(0)
229 qconnect(p
, qt
.SIGNAL('processExited()'), self
.editorExited
)
230 if not p
.launch(qt
.QByteArray()):
231 qt
.QMessageBox
.warning(self
, 'Failed to launch editor',
232 shortName
+ ' failed to launch the ' + \
233 'editor. The command used was: ' + \
234 ed
+ ' ' + it
.file.dstName
)
236 self
.editorProcesses
.add(p
)
238 def editorExited(self
):
240 status
= p
.exitStatus()
241 file = str(p
.arguments()[1])
242 editor
= str(p
.arguments()[0]) + ' ' + file
243 if not p
.normalExit():
244 qt
.QMessageBox
.warning(self
, 'Editor failure',
245 'The editor, ' + editor
+ ', exited abnormally.')
247 qt
.QMessageBox
.warning(self
, 'Editor failure',
248 'The editor, ' + editor
+ ', exited with exit code ' + str(status
))
250 self
.editorProcesses
.remove(p
)
251 scm
.doUpdateCache(file)
254 def discardFile(self
, ignored
):
255 it
= self
.currentContextItem
259 scm
.discardFile(it
.file)
262 def currentChange(self
, item
):
263 self
.text
.raiseWidget(item
.file.textW
)
265 self
.currentContextItem
= item
267 def selectedItems(self
):
269 for item
in self
.filesW
:
270 if item
.isSelected():
274 def commit(self
, id):
279 for item
in self
.filesW
:
280 debug("file: " + item
.file.text
)
281 if item
.isSelected():
282 selFileNames
.append(item
.file.text
)
283 commitFiles
.append(item
.file.dstName
)
285 keepFiles
.append(item
.file)
287 commitMsg
= str(self
.cmitItem
.file.textW
.text())
290 qt
.QMessageBox
.information(self
, "Commit - " + applicationName
,
291 "No files selected for commit.", "&Ok")
294 commitMsg
= fixCommitMsgWhiteSpace(commitMsg
)
295 if(qt
.QMessageBox
.question(self
, "Confirm Commit - " + applicationName
,
296 '''<qt><p>Do you want to commit the following file(s):</p><blockquote>''' +
297 '<br>'.join(selFileNames
) +
298 '''</blockquote><p>with the commit message:</p><blockquote><pre>''' +
299 str(qt
.QStyleSheet
.escape(commitMsg
)) + '</pre></blockquote></qt>',
304 scm
.doCommit(keepFiles
, commitFiles
, commitMsg
)
305 except CommitError
, e
:
306 qt
.QMessageBox
.warning(self
, "Commit Failed - " + applicationName
,
307 "Commit failed during " + e
.operation
+ ": " + e
.msg
,
310 qt
.QMessageBox
.warning(self
, "Commit Failed - " + applicationName
,
311 "Commit failed: " + e
.strerror
,
314 self
.cmitItem
.file.textW
.setText('')
317 def getFileState(self
):
319 cur
= self
.filesW
.currentItem()
320 if cur
and cur
!= self
.cmitItem
:
321 ret
.current
= self
.filesW
.currentItem().file.srcName
326 for x
in self
.filesW
:
328 ret
.selected
[x
.file.srcName
] = True
331 def restoreFileState(self
, state
):
332 for x
in self
.filesW
:
333 if state
.selected
.has_key(x
.file.srcName
):
335 if x
.file.srcName
== state
.current
:
336 self
.filesW
.setCurrentItem(x
)
338 def newTextEdit(self
):
340 self
.text
.addWidget(ret
)
343 def setFiles(self
, files
):
344 state
= self
.getFileState()
346 self
.createCmitItem()
348 self
.text
.removeWidget(f
.textW
)
352 f
.textW
= self
.newTextEdit()
353 f
.textW
.setReadOnly(False)
354 f
.textW
.setTextFormat(Qt
.RichText
)
355 f
.textW
.setText(formatPatchRichText(f
.patch
, self
.patchColors
))
357 self
.filesW
.insertItem(MyListItem(self
.filesW
, f
))
359 self
.filesW
.setCurrentItem(self
.cmitItem
)
361 # For some reason the currentChanged signal isn't emitted
362 # here. We call currentChange ourselves instead.
363 self
.currentChange(self
.cmitItem
)
365 self
.restoreFileState(state
)
367 def refreshFiles(self
, ignored
=None):
368 self
.setFiles(scm
.getFiles())
371 for x
in self
.filesW
:
374 def unselectAll(self
):
375 for x
in self
.filesW
:
378 commitMsgRE
= re
.compile('[ \t\r\f\v]*\n\\s*\n')
379 def fixCommitMsgWhiteSpace(msg
):
382 msg
= re
.sub(commitMsgRE
, '\n\n', msg
)
386 def formatPatchRichText(patch
, colors
):
387 ret
= ['<qt><pre><font color="', colors
['std'], '">']
389 for l
in patch
.split('\n'):
396 if c
== '+': style
= 'new'
397 elif c
== '-': style
= 'remove'
398 elif c
== '@': style
= 'head'
400 ret
.extend(['</font><font color="', colors
[style
], '">'])
402 line
= qt
.QStyleSheet
.escape(l
).ascii()
407 ret
.extend([line
, '\n'])
408 ret
.append('</pre></qt>')
412 if os
.environ
.has_key('GCT_EDITOR'):
413 return os
.environ
['GCT_EDITOR']
414 elif os
.environ
.has_key('EDITOR'):
415 return os
.environ
['EDITOR']
421 app
= qt
.QApplication(sys
.argv
)
422 settings
= qt
.QSettings()
423 settings
.beginGroup('/' + shortName
)
424 settings
.beginGroup('/geometry/')
429 mw
.resize(settings
.readNumEntry('width', 500)[0],
430 settings
.readNumEntry('height', 600)[0])
433 # The following code doesn't work correctly in some (at least
435 # managers. http://doc.trolltech.com/3.3/geometry.html contains some
436 # information about this issue.
437 # mw.move(settings.readNumEntry('x', 100)[0],
438 # settings.readNumEntry('y', 100)[0])
441 app
.setMainWidget(mw
)
444 # Handle CTRL-C appropriately
445 signal
.signal(signal
.SIGINT
, lambda s
, f
: app
.quit())
447 ret
= app
.exec_loop()