6 from argparse
import ArgumentParser
8 from os
.path
import abspath
9 from os
.path
import dirname
12 def setup_environment():
13 prefix
= dirname(dirname(dirname(dirname(abspath(__file__
)))))
14 source_tree
= os
.path
.join(prefix
, 'cola', '__init__.py')
15 if os
.path
.exists(source_tree
):
18 modules
= os
.path
.join(prefix
, 'share', 'git-cola', 'lib')
19 sys
.path
.insert(1, modules
)
25 from cola
import difftool
26 from cola
import observable
28 from cola
import qtutils
29 from cola
import utils
30 from cola
.compat
import json
31 from cola
.i18n
import N_
32 # TODO move these next 3 somewhere more neutral
33 from cola
.dag
.model
import DAG
34 from cola
.dag
.model
import RepoReader
35 from cola
.prefs
.view
import diff_font
36 from cola
.widgets
import defs
37 from cola
.widgets
.diff
import DiffWidget
38 from cola
.widgets
.diff
import COMMITS_SELECTED
39 from cola
.widgets
.standard
import TreeWidget
41 from PyQt4
import QtCore
42 from PyQt4
import QtGui
43 from PyQt4
.QtCore
import Qt
44 from PyQt4
.QtCore
import SIGNAL
47 XBASE_MIMETYPE
= 'application/git-xbase-items'
53 COMMANDS
= (PICK
, REWORD
, EDIT
, FIXUP
, SQUASH
,)
54 COMMAND_TO_IDX
= dict([(cmd
, idx
) for idx
, cmd
in enumerate(COMMANDS
)])
59 app
.setup_environment()
60 new_app
= app
.new_application()
61 app
.new_model(new_app
, os
.getcwd())
63 desktop
= new_app
.desktop()
64 window
= new_window(args
.filename
)
65 window
.resize(desktop
.width(), desktop
.height())
73 parser
= ArgumentParser()
74 parser
.add_argument('filename', metavar
='<filename>',
75 help='git-rebase-todo file to edit')
76 return parser
.parse_args()
79 def new_window(filename
):
80 editor
= Editor(filename
)
81 window
= MainWindow(editor
)
85 class MainWindow(QtGui
.QMainWindow
):
87 def __init__(self
, editor
, parent
=None):
88 super(MainWindow
, self
).__init
__(parent
)
90 default_title
= '%s - git xbase' % core
.getcwd()
91 title
= core
.getenv('GIT_XBASE_TITLE', default_title
)
92 self
.setAttribute(Qt
.WA_MacMetalStyle
)
93 self
.setWindowTitle(title
)
94 self
.setCentralWidget(editor
)
95 self
.connect(editor
, SIGNAL('exit(int)'), self
.exit
)
98 self
.show_help_action
= qtutils
.add_action(self
,
99 N_('Show Help'), show_help
, Qt
.Key_Question
)
101 self
.menubar
= QtGui
.QMenuBar(self
)
102 self
.help_menu
= self
.menubar
.addMenu(N_('Help'))
103 self
.help_menu
.addAction(self
.show_help_action
)
104 self
.setMenuBar(self
.menubar
)
106 qtutils
.add_close_action(self
)
108 def exit(self
, status
):
113 class Editor(QtGui
.QWidget
):
115 def __init__(self
, filename
, parent
=None):
116 super(Editor
, self
).__init
__(parent
)
118 self
.widget_version
= 1
119 self
.filename
= filename
121 self
.notifier
= notifier
= observable
.Observable()
122 self
.diff
= DiffWidget(notifier
, self
)
123 self
.tree
= RebaseTreeWidget(notifier
, self
)
124 self
.setFocusProxy(self
.tree
)
126 self
.rebase_button
= qt
.create_button(
127 text
=core
.getenv('GIT_XBASE_ACTION', N_('Rebase')),
128 tooltip
=N_('Accept changes and rebase\n'
129 'Shortcut: Ctrl+Enter'),
130 icon
=qtutils
.apply_icon())
132 self
.external_diff_button
= qt
.create_button(
133 text
=N_('External Diff'),
134 tooltip
=N_('Launch external diff\n'
136 self
.external_diff_button
.setEnabled(False)
138 self
.help_button
= qt
.create_button(text
=N_('Help'),
139 tooltip
=N_('Show help\n'
141 icon
=qtutils
.help_icon())
143 self
.cancel_button
= qt
.create_button(text
=N_('Cancel'),
144 tooltip
=N_('Cancel rebase\n'
146 icon
=qtutils
.close_icon())
147 splitter
= QtGui
.QSplitter()
148 splitter
.setHandleWidth(defs
.handle_width
)
149 splitter
.setOrientation(Qt
.Vertical
)
150 splitter
.insertWidget(0, self
.tree
)
151 splitter
.insertWidget(1, self
.diff
)
152 splitter
.setStretchFactor(0, 1)
153 splitter
.setStretchFactor(1, 1)
155 controls_layout
= QtGui
.QHBoxLayout()
156 controls_layout
.setMargin(defs
.no_margin
)
157 controls_layout
.setSpacing(defs
.button_spacing
)
158 controls_layout
.addWidget(self
.rebase_button
)
159 controls_layout
.addWidget(self
.external_diff_button
)
160 controls_layout
.addWidget(self
.help_button
)
161 controls_layout
.addStretch()
162 controls_layout
.addWidget(self
.cancel_button
)
164 layout
= QtGui
.QVBoxLayout()
165 layout
.setMargin(defs
.no_margin
)
166 layout
.setSpacing(defs
.spacing
)
167 layout
.addWidget(splitter
)
168 layout
.addLayout(controls_layout
)
169 self
.setLayout(layout
)
171 self
.action_rebase
= qtutils
.add_action(self
,
172 N_('Rebase'), self
.rebase
, 'Ctrl+Return')
174 notifier
.add_observer(COMMITS_SELECTED
, self
.commits_selected
)
176 qtutils
.connect_button(self
.rebase_button
, self
.rebase
)
177 qtutils
.connect_button(self
.external_diff_button
, self
.external_diff
)
178 qtutils
.connect_button(self
.help_button
, show_help
)
179 qtutils
.connect_button(self
.cancel_button
, self
.cancel
)
180 self
.connect(self
.tree
, SIGNAL('external_diff()'), self
.external_diff
)
182 insns
= core
.read(filename
)
183 self
.parse_sequencer_instructions(insns
)
186 def commits_selected(self
, commits
):
187 self
.external_diff_button
.setEnabled(bool(commits
))
190 def emit_exit(self
, status
):
191 self
.emit(SIGNAL('exit(int)'), status
)
193 def parse_sequencer_instructions(self
, insns
):
195 rebase_command
= re
.compile(
196 r
'^(# )?(pick|fixup|squash) ([0-9a-f]{7,40}) (.+)$')
197 for line
in insns
.splitlines():
198 match
= rebase_command
.match(line
)
201 enabled
= match
.group(1) is None
202 command
= match
.group(2)
203 sha1hex
= match
.group(3)
204 summary
= match
.group(4)
205 self
.tree
.add_step(idx
, enabled
, command
, sha1hex
, summary
)
209 self
.tree
.select_first()
216 lines
= [item
.value() for item
in self
.tree
.items()]
217 sequencer_instructions
= '\n'.join(lines
) + '\n'
219 core
.write(self
.filename
, sequencer_instructions
)
221 except Exception as e
:
222 msg
, details
= utils
.format_exception(e
)
223 sys
.stderr
.write(msg
+ '\n\n' + details
)
226 def external_diff(self
):
227 items
= self
.tree
.selected_items()
231 difftool
.diff_expression(self
, item
.sha1hex
+ '^!',
235 class RebaseTreeWidget(TreeWidget
):
237 def __init__(self
, notifier
, parent
=None):
238 super(RebaseTreeWidget
, self
).__init
__()
239 self
.notifier
= notifier
241 self
.setHeaderLabels([N_('#'),
246 self
.header().setStretchLastSection(True)
247 self
.setColumnCount(5)
250 self
.clicked_item
= None
251 self
.clicked_idx
= -1
252 self
.dragged_items
= []
253 self
.inner_drag
= False
254 self
.setSelectionMode(self
.SingleSelection
)
255 self
.setDragEnabled(True)
256 self
.setAcceptDrops(True)
257 self
.setDropIndicatorShown(True)
258 self
.setDragDropMode(QtGui
.QAbstractItemView
.InternalMove
)
259 self
.setSortingEnabled(False)
262 self
.copy_sha1_action
= qtutils
.add_action(self
,
263 N_('Copy SHA-1'), self
.copy_sha1
, QtGui
.QKeySequence
.Copy
)
265 self
.external_diff_action
= qtutils
.add_action(self
,
266 N_('External Diff'), self
.external_diff
,
267 cmds
.LaunchDifftool
.SHORTCUT
)
269 self
.toggle_enabled_action
= qtutils
.add_action(self
,
270 N_('Toggle Enabled'), self
.toggle_enabled
,
273 self
.action_pick
= qtutils
.add_action(self
,
274 N_('Pick'), lambda: self
.set_selected_to(PICK
),
277 self
.action_reword
= qtutils
.add_action(self
,
278 N_('Reword'), lambda: self
.set_selected_to(REWORD
),
281 self
.action_edit
= qtutils
.add_action(self
,
282 N_('Edit'), lambda: self
.set_selected_to(EDIT
),
285 self
.action_fixup
= qtutils
.add_action(self
,
286 N_('Fixup'), lambda: self
.set_selected_to(FIXUP
),
289 self
.action_squash
= qtutils
.add_action(self
,
290 N_('Squash'), lambda: self
.set_selected_to(SQUASH
),
293 self
.action_shift_down
= qtutils
.add_action(self
,
294 N_('Shift Down'), self
.shift_down
, 'Shift+j')
296 self
.action_shift_up
= qtutils
.add_action(self
,
297 N_('Shift Up'), self
.shift_up
, 'Shift+k')
299 self
.connect(self
, SIGNAL('itemChanged(QTreeWidgetItem *, int)'),
302 self
.connect(self
, SIGNAL('itemSelectionChanged()'),
303 self
.selection_changed
)
305 self
.connect(self
, SIGNAL('move'), self
.move
)
307 def add_step(self
, idx
, enabled
, command
, sha1hex
, summary
):
308 item
= RebaseTreeWidgetItem(idx
, enabled
, command
,
310 self
.invisibleRootItem().addChild(item
)
313 for item
in self
.items():
314 self
.decorate_item(item
)
317 self
.resizeColumnToContents(0)
318 self
.resizeColumnToContents(1)
319 self
.resizeColumnToContents(2)
320 self
.resizeColumnToContents(3)
321 self
.resizeColumnToContents(4)
324 def item_changed(self
, item
, column
):
325 if column
== item
.ENABLED_COLUMN
:
329 for item
in self
.items():
330 if not item
.is_enabled():
332 if item
.command
!= PICK
:
334 self
.decorate_item(item
)
337 def decorate_item(self
, item
):
338 item
.combo
= combo
= QtGui
.QComboBox()
339 combo
.addItems(COMMANDS
)
340 combo
.setEditable(False)
341 combo
.setCurrentIndex(COMMAND_TO_IDX
[item
.command
])
342 combo
.connect(combo
, SIGNAL('currentIndexChanged(const QString &)'),
343 lambda s
: self
.set_command(item
, unicode(s
)))
344 self
.setItemWidget(item
, item
.COMMAND_COLUMN
, combo
)
346 def set_selected_to(self
, command
):
347 for i
in self
.selected_items():
349 i
.combo
.setCurrentIndex(COMMAND_TO_IDX
[command
])
352 def set_command(self
, item
, command
):
353 item
.command
= command
354 item
.combo
.setCurrentIndex(COMMAND_TO_IDX
[command
])
358 item
= self
.selected_item()
361 sha1hex
= item
.sha1hex
362 qtutils
.set_clipboard(sha1hex
)
364 def selection_changed(self
):
365 item
= self
.selected_item()
368 sha1hex
= item
.sha1hex
369 dag
= DAG(sha1hex
, 2)
370 repo
= RepoReader(dag
)
375 commits
= commits
[-1:]
376 self
.notifier
.notify_observers(COMMITS_SELECTED
, commits
)
378 def external_diff(self
):
379 self
.emit(SIGNAL('external_diff()'))
381 def toggle_enabled(self
):
382 item
= self
.selected_item()
385 item
.toggle_enabled()
387 def select_first(self
):
391 idx
= self
.model().index(0, 0)
393 self
.setCurrentIndex(idx
)
395 def shift_down(self
):
396 item
= self
.selected_item()
400 idx
= items
.index(item
)
401 if idx
< len(items
) - 1:
402 self
.emit(SIGNAL('move'), [idx
], idx
+ 1)
405 item
= self
.selected_item()
409 idx
= items
.index(item
)
411 self
.emit(SIGNAL('move'), [idx
], idx
- 1)
415 return [XBASE_MIMETYPE
]
417 def mousePressEvent(self
, event
):
418 self
.clicked_item
= self
.itemAt(event
.pos())
419 if self
.clicked_item
:
420 self
.clicked_idx
= self
.items().index(self
.clicked_item
)
422 self
.clicked_idx
= -1
423 self
.clearSelection()
424 super(RebaseTreeWidget
, self
).mousePressEvent(event
)
425 return QtGui
.QTreeWidget
.mousePressEvent(self
, event
)
427 def supportedDropActions(self
):
430 def dropMimeData(self
, parent
, index
, data
, action
):
433 def mimeData(self
, items
):
434 mime
= QtCore
.QMimeData()
435 data
= [i
.copy() for i
in self
.selected_items()]
436 mime
.setData(XBASE_MIMETYPE
, json
.dumps(data
))
437 self
.dragged_items
= self
.selected_items()
440 def dragEnterEvent(self
, event
):
441 """Detect drag enter events to flag internal moves."""
442 self
.inner_drag
= event
.source() == self
444 return super(RebaseTreeWidget
, self
).dragEnterEvent(event
)
446 def dragLeaveEvent(self
, event
):
447 """Detect when drags leave so that we can flag it."""
448 self
.inner_drag
= False
449 return super(RebaseTreeWidget
, self
).dragLeaveEvent(event
)
451 def dropEvent(self
, event
):
452 # Qt's default action is losing items so we handle the internal
453 # move ourselves. Ignore the event and reorder the tree.
456 if self
.clicked_item
is None:
459 drop_pos
= event
.pos()
460 dst_item
= self
.itemAt(drop_pos
)
462 dst_idx
= self
.items().index(dst_item
)
464 dst_idx
= len(self
.items())
466 src_idxs
= [self
.items().index(i
) for i
in self
.dragged_items
]
468 self
.emit(SIGNAL('move'), src_idxs
, dst_idx
)
470 self
.viewport().update()
471 self
.dragged_items
= []
473 def move(self
, src_idxs
, dst_idx
):
476 for idx
in reversed(sorted(src_idxs
)):
477 data
= items
[idx
].copy()
478 self
.invisibleRootItem().takeChild(idx
)
479 item
= RebaseTreeWidgetItem(data
['idx'], data
['enabled'],
480 data
['command'], data
['sha1hex'],
482 new_items
.insert(0, item
)
485 self
.invisibleRootItem().insertChildren(dst_idx
, new_items
)
486 self
.setCurrentItem(new_items
[0])
491 def contextMenuEvent(self
, event
):
492 menu
= QtGui
.QMenu(self
)
493 menu
.addAction(self
.action_pick
)
494 menu
.addAction(self
.action_reword
)
495 menu
.addAction(self
.action_edit
)
496 menu
.addAction(self
.action_fixup
)
497 menu
.addAction(self
.action_squash
)
499 menu
.addAction(self
.toggle_enabled_action
)
501 menu
.addAction(self
.copy_sha1_action
)
502 menu
.addAction(self
.external_diff_action
)
503 menu
.exec_(self
.mapToGlobal(event
.pos()))
506 class RebaseTreeWidgetItem(QtGui
.QTreeWidgetItem
):
511 def __init__(self
, idx
, enabled
, command
, sha1hex
, summary
, parent
=None):
512 QtGui
.QTreeWidgetItem
.__init
__(self
, parent
)
514 self
.command
= command
516 self
.sha1hex
= sha1hex
517 self
.summary
= summary
519 self
.setText(0, '%02d' % idx
)
520 self
.set_enabled(enabled
)
522 self
.setText(3, sha1hex
)
523 self
.setText(4, summary
)
525 flags
= self
.flags() | Qt
.ItemIsUserCheckable
526 flags
= flags | Qt
.ItemIsDragEnabled
527 flags
= flags
& ~Qt
.ItemIsDropEnabled
532 'command': self
.command
,
533 'enabled': self
.is_enabled(),
535 'sha1hex': self
.sha1hex
,
536 'summary': self
.summary
,
540 return '%s %s %s %s' % (
541 not self
.is_enabled() and '# ' or '',
542 self
.command
, self
.sha1hex
, self
.summary
)
544 def is_enabled(self
):
545 return self
.checkState(self
.ENABLED_COLUMN
) == Qt
.Checked
547 def set_enabled(self
, enabled
):
548 self
.setCheckState(self
.ENABLED_COLUMN
,
549 enabled
and Qt
.Checked
or Qt
.Unchecked
)
551 def toggle_enabled(self
):
552 self
.set_enabled(not self
.is_enabled())
560 reword = use commit, but edit the commit message
561 edit = use commit, but stop for amending
562 squash = use commit, but meld into previous commit
563 fixup = like "squash", but discard this commit's log message
565 These lines can be re-ordered; they are executed from top to bottom.
567 If you disable a line here THAT COMMIT WILL BE LOST.
569 However, if you disable everything, the rebase will be aborted.
584 spacebar = toggle enabled
586 ctrl+enter = accept changes and rebase
587 ctrl+q = cancel and abort the rebase
588 ctrl+d = launch external diff
591 parent
= qtutils
.active_window()
592 text
= QtGui
.QLabel(parent
)
593 text
.setFont(diff_font())
594 text
.setText(help_text
)
595 text
.setTextInteractionFlags(Qt
.NoTextInteraction
)
597 layout
= QtGui
.QHBoxLayout()
598 layout
.setMargin(defs
.margin
)
599 layout
.setSpacing(defs
.spacing
)
600 layout
.addWidget(text
)
602 widget
= QtGui
.QDialog(parent
)
603 widget
.setWindowModality(Qt
.WindowModal
)
604 widget
.setWindowTitle(N_('Help - git-xbase'))
605 widget
.setLayout(layout
)
607 qtutils
.add_action(widget
, N_('Close'), widget
.accept
,
615 if __name__
== '__main__':