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
27 from cola
import qtutils
28 from cola
import utils
29 from cola
.compat
import set
30 from cola
.i18n
import N_
31 from cola
.models
.dag
import DAG
32 from cola
.models
.dag
import RepoReader
33 from cola
.qtutils
import diff_font
34 from cola
.widgets
import defs
35 from cola
.widgets
.diff
import DiffWidget
36 from cola
.widgets
.diff
import COMMITS_SELECTED
37 from cola
.widgets
.standard
import DraggableTreeWidget
39 from PyQt4
import QtGui
40 from PyQt4
.QtCore
import Qt
41 from PyQt4
.QtCore
import SIGNAL
49 COMMANDS
= (PICK
, REWORD
, EDIT
, FIXUP
, SQUASH
,)
50 COMMAND_TO_IDX
= dict([(cmd
, idx
) for idx
, cmd
in enumerate(COMMANDS
)])
55 app
.setup_environment()
56 new_app
= app
.new_application()
57 app
.new_model(new_app
, os
.getcwd())
59 desktop
= new_app
.desktop()
60 window
= new_window(args
.filename
)
61 window
.resize(desktop
.width(), desktop
.height())
69 parser
= ArgumentParser()
70 parser
.add_argument('filename', metavar
='<filename>',
71 help='git-rebase-todo file to edit')
72 return parser
.parse_args()
75 def new_window(filename
):
76 editor
= Editor(filename
)
77 window
= MainWindow(editor
)
81 class MainWindow(QtGui
.QMainWindow
):
83 def __init__(self
, editor
, parent
=None):
84 super(MainWindow
, self
).__init
__(parent
)
86 default_title
= '%s - git xbase' % core
.getcwd()
87 title
= core
.getenv('GIT_XBASE_TITLE', default_title
)
88 self
.setAttribute(Qt
.WA_MacMetalStyle
)
89 self
.setWindowTitle(title
)
90 self
.setCentralWidget(editor
)
91 self
.connect(editor
, SIGNAL('exit(int)'), self
.exit
)
94 self
.show_help_action
= qtutils
.add_action(self
,
95 N_('Show Help'), show_help
, Qt
.Key_Question
)
97 self
.menubar
= QtGui
.QMenuBar(self
)
98 self
.help_menu
= self
.menubar
.addMenu(N_('Help'))
99 self
.help_menu
.addAction(self
.show_help_action
)
100 self
.setMenuBar(self
.menubar
)
102 qtutils
.add_close_action(self
)
104 def exit(self
, status
):
109 class Editor(QtGui
.QWidget
):
111 def __init__(self
, filename
, parent
=None):
112 super(Editor
, self
).__init
__(parent
)
114 self
.widget_version
= 1
115 self
.filename
= filename
117 self
.notifier
= notifier
= observable
.Observable()
118 self
.diff
= DiffWidget(notifier
, self
)
119 self
.tree
= RebaseTreeWidget(notifier
, self
)
120 self
.setFocusProxy(self
.tree
)
122 self
.rebase_button
= qtutils
.create_button(
123 text
=core
.getenv('GIT_XBASE_ACTION', N_('Rebase')),
124 tooltip
=N_('Accept changes and rebase\n'
125 'Shortcut: Ctrl+Enter'),
126 icon
=qtutils
.apply_icon())
128 self
.external_diff_button
= qtutils
.create_button(
129 text
=N_('External Diff'),
130 tooltip
=N_('Launch external diff\n'
132 self
.external_diff_button
.setEnabled(False)
134 self
.help_button
= qtutils
.create_button(
136 tooltip
=N_('Show help\nShortcut: ?'),
137 icon
=qtutils
.help_icon())
139 self
.cancel_button
= qtutils
.create_button(
141 tooltip
=N_('Cancel rebase\nShortcut: Ctrl+Q'),
142 icon
=qtutils
.close_icon())
143 splitter
= QtGui
.QSplitter()
144 splitter
.setHandleWidth(defs
.handle_width
)
145 splitter
.setOrientation(Qt
.Vertical
)
146 splitter
.insertWidget(0, self
.tree
)
147 splitter
.insertWidget(1, self
.diff
)
148 splitter
.setStretchFactor(0, 1)
149 splitter
.setStretchFactor(1, 1)
151 controls_layout
= QtGui
.QHBoxLayout()
152 controls_layout
.setMargin(defs
.no_margin
)
153 controls_layout
.setSpacing(defs
.button_spacing
)
154 controls_layout
.addWidget(self
.rebase_button
)
155 controls_layout
.addWidget(self
.external_diff_button
)
156 controls_layout
.addWidget(self
.help_button
)
157 controls_layout
.addStretch()
158 controls_layout
.addWidget(self
.cancel_button
)
160 layout
= QtGui
.QVBoxLayout()
161 layout
.setMargin(defs
.no_margin
)
162 layout
.setSpacing(defs
.spacing
)
163 layout
.addWidget(splitter
)
164 layout
.addLayout(controls_layout
)
165 self
.setLayout(layout
)
167 self
.action_rebase
= qtutils
.add_action(self
,
168 N_('Rebase'), self
.rebase
, 'Ctrl+Return')
170 notifier
.add_observer(COMMITS_SELECTED
, self
.commits_selected
)
172 qtutils
.connect_button(self
.rebase_button
, self
.rebase
)
173 qtutils
.connect_button(self
.external_diff_button
, self
.external_diff
)
174 qtutils
.connect_button(self
.help_button
, show_help
)
175 qtutils
.connect_button(self
.cancel_button
, self
.cancel
)
176 self
.connect(self
.tree
, SIGNAL('external_diff()'), self
.external_diff
)
178 insns
= core
.read(filename
)
179 self
.parse_sequencer_instructions(insns
)
182 def commits_selected(self
, commits
):
183 self
.external_diff_button
.setEnabled(bool(commits
))
186 def emit_exit(self
, status
):
187 self
.emit(SIGNAL('exit(int)'), status
)
189 def parse_sequencer_instructions(self
, insns
):
191 rebase_command
= re
.compile(
192 r
'^(# )?(pick|fixup|squash) ([0-9a-f]{7,40}) (.+)$')
193 for line
in insns
.splitlines():
194 match
= rebase_command
.match(line
)
197 enabled
= match
.group(1) is None
198 command
= match
.group(2)
199 sha1hex
= match
.group(3)
200 summary
= match
.group(4)
201 self
.tree
.add_step(idx
, enabled
, command
, sha1hex
, summary
)
205 self
.tree
.select_first()
212 lines
= [item
.value() for item
in self
.tree
.items()]
213 sequencer_instructions
= '\n'.join(lines
) + '\n'
215 core
.write(self
.filename
, sequencer_instructions
)
217 except Exception as e
:
218 msg
, details
= utils
.format_exception(e
)
219 sys
.stderr
.write(msg
+ '\n\n' + details
)
222 def external_diff(self
):
223 items
= self
.tree
.selected_items()
227 difftool
.diff_expression(self
, item
.sha1hex
+ '^!',
231 class RebaseTreeWidget(DraggableTreeWidget
):
233 def __init__(self
, notifier
, parent
=None):
234 super(RebaseTreeWidget
, self
).__init
__(parent
=parent
)
235 self
.notifier
= notifier
237 self
.setHeaderLabels([N_('#'),
242 self
.header().setStretchLastSection(True)
243 self
.setColumnCount(5)
246 self
.copy_sha1_action
= qtutils
.add_action(self
,
247 N_('Copy SHA-1'), self
.copy_sha1
, QtGui
.QKeySequence
.Copy
)
249 self
.external_diff_action
= qtutils
.add_action(self
,
250 N_('External Diff'), self
.external_diff
,
251 cmds
.LaunchDifftool
.SHORTCUT
)
253 self
.toggle_enabled_action
= qtutils
.add_action(self
,
254 N_('Toggle Enabled'), self
.toggle_enabled
,
257 self
.action_pick
= qtutils
.add_action(self
,
258 N_('Pick'), lambda: self
.set_selected_to(PICK
),
261 self
.action_reword
= qtutils
.add_action(self
,
262 N_('Reword'), lambda: self
.set_selected_to(REWORD
),
265 self
.action_edit
= qtutils
.add_action(self
,
266 N_('Edit'), lambda: self
.set_selected_to(EDIT
),
269 self
.action_fixup
= qtutils
.add_action(self
,
270 N_('Fixup'), lambda: self
.set_selected_to(FIXUP
),
273 self
.action_squash
= qtutils
.add_action(self
,
274 N_('Squash'), lambda: self
.set_selected_to(SQUASH
),
277 self
.action_shift_down
= qtutils
.add_action(self
,
278 N_('Shift Down'), self
.shift_down
, 'Shift+j')
280 self
.action_shift_up
= qtutils
.add_action(self
,
281 N_('Shift Up'), self
.shift_up
, 'Shift+k')
283 self
.connect(self
, SIGNAL('itemChanged(QTreeWidgetItem *, int)'),
286 self
.connect(self
, SIGNAL('itemSelectionChanged()'),
287 self
.selection_changed
)
289 self
.connect(self
, SIGNAL('move'), self
.move
)
291 def add_step(self
, idx
, enabled
, command
, sha1hex
, summary
):
292 item
= RebaseTreeWidgetItem(idx
, enabled
, command
,
294 self
.invisibleRootItem().addChild(item
)
297 for item
in self
.items():
298 self
.decorate_item(item
)
301 self
.resizeColumnToContents(0)
302 self
.resizeColumnToContents(1)
303 self
.resizeColumnToContents(2)
304 self
.resizeColumnToContents(3)
305 self
.resizeColumnToContents(4)
308 def item_changed(self
, item
, column
):
309 if column
== item
.ENABLED_COLUMN
:
313 invalid_first_choice
= set([FIXUP
, SQUASH
])
314 for item
in self
.items():
315 if not item
.is_enabled():
317 if item
.command
in invalid_first_choice
:
319 self
.decorate_item(item
)
322 def decorate_item(self
, item
):
323 item
.combo
= combo
= QtGui
.QComboBox()
324 combo
.addItems(COMMANDS
)
325 combo
.setEditable(False)
326 combo
.setCurrentIndex(COMMAND_TO_IDX
[item
.command
])
327 combo
.connect(combo
, SIGNAL('currentIndexChanged(const QString &)'),
328 lambda s
: self
.set_command(item
, unicode(s
)))
329 self
.setItemWidget(item
, item
.COMMAND_COLUMN
, combo
)
331 def set_selected_to(self
, command
):
332 for i
in self
.selected_items():
334 i
.combo
.setCurrentIndex(COMMAND_TO_IDX
[command
])
337 def set_command(self
, item
, command
):
338 item
.command
= command
339 item
.combo
.setCurrentIndex(COMMAND_TO_IDX
[command
])
343 item
= self
.selected_item()
346 sha1hex
= item
.sha1hex
347 qtutils
.set_clipboard(sha1hex
)
349 def selection_changed(self
):
350 item
= self
.selected_item()
353 sha1hex
= item
.sha1hex
354 dag
= DAG(sha1hex
, 2)
355 repo
= RepoReader(dag
)
360 commits
= commits
[-1:]
361 self
.notifier
.notify_observers(COMMITS_SELECTED
, commits
)
363 def external_diff(self
):
364 self
.emit(SIGNAL('external_diff()'))
366 def toggle_enabled(self
):
367 item
= self
.selected_item()
370 item
.toggle_enabled()
372 def select_first(self
):
376 idx
= self
.model().index(0, 0)
378 self
.setCurrentIndex(idx
)
380 def shift_down(self
):
381 item
= self
.selected_item()
385 idx
= items
.index(item
)
386 if idx
< len(items
) - 1:
387 self
.emit(SIGNAL('move'), [idx
], idx
+ 1)
390 item
= self
.selected_item()
394 idx
= items
.index(item
)
396 self
.emit(SIGNAL('move'), [idx
], idx
- 1)
398 def move(self
, src_idxs
, dst_idx
):
401 for idx
in reversed(sorted(src_idxs
)):
402 data
= items
[idx
].copy()
403 self
.invisibleRootItem().takeChild(idx
)
404 item
= RebaseTreeWidgetItem(data
['idx'], data
['enabled'],
405 data
['command'], data
['sha1hex'],
407 new_items
.insert(0, item
)
410 self
.invisibleRootItem().insertChildren(dst_idx
, new_items
)
411 self
.setCurrentItem(new_items
[0])
418 def dropEvent(self
, event
):
419 super(RebaseTreeWidget
, self
).dropEvent(event
)
423 def contextMenuEvent(self
, event
):
424 menu
= QtGui
.QMenu(self
)
425 menu
.addAction(self
.action_pick
)
426 menu
.addAction(self
.action_reword
)
427 menu
.addAction(self
.action_edit
)
428 menu
.addAction(self
.action_fixup
)
429 menu
.addAction(self
.action_squash
)
431 menu
.addAction(self
.toggle_enabled_action
)
433 menu
.addAction(self
.copy_sha1_action
)
434 menu
.addAction(self
.external_diff_action
)
435 menu
.exec_(self
.mapToGlobal(event
.pos()))
438 class RebaseTreeWidgetItem(QtGui
.QTreeWidgetItem
):
443 def __init__(self
, idx
, enabled
, command
, sha1hex
, summary
, parent
=None):
444 QtGui
.QTreeWidgetItem
.__init
__(self
, parent
)
446 self
.command
= command
448 self
.sha1hex
= sha1hex
449 self
.summary
= summary
451 self
.setText(0, '%02d' % idx
)
452 self
.set_enabled(enabled
)
454 self
.setText(3, sha1hex
)
455 self
.setText(4, summary
)
457 flags
= self
.flags() | Qt
.ItemIsUserCheckable
458 flags
= flags | Qt
.ItemIsDragEnabled
459 flags
= flags
& ~Qt
.ItemIsDropEnabled
464 'command': self
.command
,
465 'enabled': self
.is_enabled(),
467 'sha1hex': self
.sha1hex
,
468 'summary': self
.summary
,
472 return '%s %s %s %s' % (
473 not self
.is_enabled() and '# ' or '',
474 self
.command
, self
.sha1hex
, self
.summary
)
476 def is_enabled(self
):
477 return self
.checkState(self
.ENABLED_COLUMN
) == Qt
.Checked
479 def set_enabled(self
, enabled
):
480 self
.setCheckState(self
.ENABLED_COLUMN
,
481 enabled
and Qt
.Checked
or Qt
.Unchecked
)
483 def toggle_enabled(self
):
484 self
.set_enabled(not self
.is_enabled())
492 reword = use commit, but edit the commit message
493 edit = use commit, but stop for amending
494 squash = use commit, but meld into previous commit
495 fixup = like "squash", but discard this commit's log message
497 These lines can be re-ordered; they are executed from top to bottom.
499 If you disable a line here THAT COMMIT WILL BE LOST.
501 However, if you disable everything, the rebase will be aborted.
516 spacebar = toggle enabled
518 ctrl+enter = accept changes and rebase
519 ctrl+q = cancel and abort the rebase
520 ctrl+d = launch external diff
523 parent
= qtutils
.active_window()
524 text
= QtGui
.QLabel(parent
)
525 text
.setFont(diff_font())
526 text
.setText(help_text
)
527 text
.setTextInteractionFlags(Qt
.NoTextInteraction
)
529 layout
= QtGui
.QHBoxLayout()
530 layout
.setMargin(defs
.margin
)
531 layout
.setSpacing(defs
.spacing
)
532 layout
.addWidget(text
)
534 widget
= QtGui
.QDialog(parent
)
535 widget
.setWindowModality(Qt
.WindowModal
)
536 widget
.setWindowTitle(N_('Help - git-xbase'))
537 widget
.setLayout(layout
)
539 qtutils
.add_action(widget
, N_('Close'), widget
.accept
,
547 if __name__
== '__main__':