2 from __future__
import absolute_import
, division
, unicode_literals
6 from argparse
import ArgumentParser
9 def setup_environment():
10 abspath
= os
.path
.abspath
11 dirname
= os
.path
.dirname
12 prefix
= dirname(dirname(dirname(dirname(abspath(__file__
)))))
13 source_tree
= os
.path
.join(prefix
, 'cola', '__init__.py')
14 if os
.path
.exists(source_tree
):
17 modules
= os
.path
.join(prefix
, 'share', 'git-cola', 'lib')
18 sys
.path
.insert(1, modules
)
24 from qtpy
import QtGui
25 from qtpy
import QtWidgets
26 from qtpy
.QtCore
import Qt
27 from qtpy
.QtCore
import Signal
30 from cola
import difftool
31 from cola
import hotkeys
32 from cola
import icons
33 from cola
import observable
34 from cola
import qtutils
35 from cola
import utils
36 from cola
.i18n
import N_
37 from cola
.models
import dag
38 from cola
.models
import prefs
39 from cola
.widgets
import defs
40 from cola
.widgets
import diff
41 from cola
.widgets
import standard
42 from cola
.widgets
import text
51 COMMANDS
= (PICK
, REWORD
, EDIT
, FIXUP
, SQUASH
,)
52 COMMAND_IDX
= dict([(cmd
, idx
) for idx
, cmd
in enumerate(COMMANDS
)])
65 context
= app
.application_init(args
)
67 desktop
= context
.app
.desktop()
68 window
= new_window(args
.filename
)
69 window
.resize(desktop
.width(), desktop
.height())
77 parser
= ArgumentParser()
78 parser
.add_argument('filename', metavar
='<filename>',
79 help='git-rebase-todo file to edit')
80 app
.add_common_arguments(parser
)
81 return parser
.parse_args()
84 def new_window(filename
):
86 editor
= Editor(filename
, parent
=window
)
87 window
.set_editor(editor
)
92 """Expand shorthand commands into their full name"""
93 return ABBREV
.get(cmd
, cmd
)
96 class MainWindow(QtWidgets
.QMainWindow
):
98 def __init__(self
, parent
=None):
99 super(MainWindow
, self
).__init
__(parent
)
102 default_title
= '%s - git xbase' % core
.getcwd()
103 title
= core
.getenv('GIT_XBASE_TITLE', default_title
)
104 self
.setAttribute(Qt
.WA_MacMetalStyle
)
105 self
.setWindowTitle(title
)
107 self
.show_help_action
= qtutils
.add_action(
108 self
, N_('Show Help'), show_help
, hotkeys
.QUESTION
)
110 self
.menubar
= QtWidgets
.QMenuBar(self
)
111 self
.help_menu
= self
.menubar
.addMenu(N_('Help'))
112 self
.help_menu
.addAction(self
.show_help_action
)
113 self
.setMenuBar(self
.menubar
)
115 qtutils
.add_close_action(self
)
117 def set_editor(self
, editor
):
119 self
.setCentralWidget(editor
)
120 editor
.exit
.connect(self
.exit
)
123 def exit(self
, status
):
128 class Editor(QtWidgets
.QWidget
):
131 def __init__(self
, filename
, parent
=None):
132 super(Editor
, self
).__init
__(parent
)
134 self
.widget_version
= 1
136 self
.filename
= filename
137 self
.comment_char
= comment_char
= prefs
.comment_char()
139 self
.notifier
= notifier
= observable
.Observable()
140 self
.diff
= diff
.DiffWidget(notifier
, self
)
141 self
.tree
= RebaseTreeWidget(notifier
, comment_char
, self
)
142 self
.setFocusProxy(self
.tree
)
144 self
.rebase_button
= qtutils
.create_button(
145 text
=core
.getenv('GIT_XBASE_ACTION', N_('Rebase')),
146 tooltip
=N_('Accept changes and rebase\n'
147 'Shortcut: Ctrl+Enter'),
150 self
.extdiff_button
= qtutils
.create_button(
151 text
=N_('External Diff'),
152 tooltip
=N_('Launch external diff\n'
154 self
.extdiff_button
.setEnabled(False)
156 self
.help_button
= qtutils
.create_button(
157 text
=N_('Help'), tooltip
=N_('Show help\nShortcut: ?'),
158 icon
=icons
.question())
160 self
.cancel_button
= qtutils
.create_button(
161 text
=N_('Cancel'), tooltip
=N_('Cancel rebase\nShortcut: Ctrl+Q'),
164 splitter
= qtutils
.splitter(Qt
.Vertical
, self
.tree
, self
.diff
)
166 controls_layout
= qtutils
.hbox(defs
.no_margin
, defs
.button_spacing
,
167 self
.rebase_button
, self
.extdiff_button
,
168 self
.help_button
, qtutils
.STRETCH
,
170 layout
= qtutils
.vbox(defs
.no_margin
, defs
.spacing
,
171 splitter
, controls_layout
)
172 self
.setLayout(layout
)
174 self
.action_rebase
= qtutils
.add_action(
175 self
, N_('Rebase'), self
.rebase
,
176 hotkeys
.CTRL_RETURN
, hotkeys
.CTRL_ENTER
)
178 notifier
.add_observer(diff
.COMMITS_SELECTED
, self
.commits_selected
)
179 self
.tree
.external_diff
.connect(self
.external_diff
)
181 qtutils
.connect_button(self
.rebase_button
, self
.rebase
)
182 qtutils
.connect_button(self
.extdiff_button
, self
.external_diff
)
183 qtutils
.connect_button(self
.help_button
, show_help
)
184 qtutils
.connect_button(self
.cancel_button
, self
.cancel
)
186 insns
= core
.read(filename
)
187 self
.parse_sequencer_instructions(insns
)
190 def commits_selected(self
, commits
):
191 self
.extdiff_button
.setEnabled(bool(commits
))
194 def parse_sequencer_instructions(self
, insns
):
196 re_comment_char
= re
.escape(self
.comment_char
)
197 exec_rgx
= re
.compile(r
'^\s*(%s)?\s*(x|exec)\s+(.+)$'
199 pick_rgx
= re
.compile((r
'^\s*(%s)?\s*'
200 r
'(p|pick|r|reword|e|edit|f|fixup|s|squash)'
201 r
'\s+([0-9a-f]{7,40})'
202 r
'\s+(.+)$') % re_comment_char
)
203 for line
in insns
.splitlines():
204 match
= pick_rgx
.match(line
)
206 enabled
= match
.group(1) is None
207 command
= unabbrev(match
.group(2))
208 sha1hex
= match
.group(3)
209 summary
= match
.group(4)
210 self
.tree
.add_item(idx
, enabled
, command
,
211 sha1hex
=sha1hex
, summary
=summary
)
214 match
= exec_rgx
.match(line
)
216 enabled
= match
.group(1) is None
217 command
= unabbrev(match
.group(2))
218 cmdexec
= match
.group(3)
219 self
.tree
.add_item(idx
, enabled
, command
, cmdexec
=cmdexec
)
223 self
.tree
.decorate(self
.tree
.items())
225 self
.tree
.select_first()
233 lines
= [item
.value() for item
in self
.tree
.items()]
234 sequencer_instructions
= '\n'.join(lines
) + '\n'
236 core
.write(self
.filename
, sequencer_instructions
)
239 except Exception as e
:
240 msg
, details
= utils
.format_exception(e
)
241 sys
.stderr
.write(msg
+ '\n\n' + details
)
245 def external_diff(self
):
246 items
= self
.tree
.selected_items()
250 difftool
.diff_expression(self
, item
.sha1hex
+ '^!',
254 class RebaseTreeWidget(standard
.DraggableTreeWidget
):
255 external_diff
= Signal()
256 move_rows
= Signal(object, object)
258 def __init__(self
, notifier
, comment_char
, parent
=None):
259 super(RebaseTreeWidget
, self
).__init
__(parent
=parent
)
260 self
.notifier
= notifier
261 self
.comment_char
= comment_char
263 self
.setHeaderLabels([N_('#'),
268 self
.header().setStretchLastSection(True)
269 self
.setColumnCount(5)
272 self
.copy_sha1_action
= qtutils
.add_action(
273 self
, N_('Copy SHA-1'), self
.copy_sha1
, QtGui
.QKeySequence
.Copy
)
275 self
.external_diff_action
= qtutils
.add_action(
276 self
, N_('External Diff'), self
.external_diff
.emit
,
279 self
.toggle_enabled_action
= qtutils
.add_action(
280 self
, N_('Toggle Enabled'), self
.toggle_enabled
,
281 hotkeys
.PRIMARY_ACTION
)
283 self
.action_pick
= qtutils
.add_action(
284 self
, N_('Pick'), lambda: self
.set_selected_to(PICK
),
285 *hotkeys
.REBASE_PICK
)
287 self
.action_reword
= qtutils
.add_action(
288 self
, N_('Reword'), lambda: self
.set_selected_to(REWORD
),
289 *hotkeys
.REBASE_REWORD
)
291 self
.action_edit
= qtutils
.add_action(
292 self
, N_('Edit'), lambda: self
.set_selected_to(EDIT
),
293 *hotkeys
.REBASE_EDIT
)
295 self
.action_fixup
= qtutils
.add_action(
296 self
, N_('Fixup'), lambda: self
.set_selected_to(FIXUP
),
297 *hotkeys
.REBASE_FIXUP
)
299 self
.action_squash
= qtutils
.add_action(
300 self
, N_('Squash'), lambda: self
.set_selected_to(SQUASH
),
301 *hotkeys
.REBASE_SQUASH
)
303 self
.action_shift_down
= qtutils
.add_action(
304 self
, N_('Shift Down'), self
.shift_down
,
305 hotkeys
.MOVE_DOWN_TERTIARY
)
307 self
.action_shift_up
= qtutils
.add_action(
308 self
, N_('Shift Up'), self
.shift_up
, hotkeys
.MOVE_UP_TERTIARY
)
310 self
.itemChanged
.connect(self
.item_changed
)
311 self
.itemSelectionChanged
.connect(self
.selection_changed
)
312 self
.move_rows
.connect(self
.move
)
313 self
.items_moved
.connect(self
.decorate
)
315 def add_item(self
, idx
, enabled
, command
,
316 sha1hex
='', summary
='', cmdexec
=''):
317 comment_char
= self
.comment_char
318 item
= RebaseTreeWidgetItem(idx
, enabled
, command
,
319 sha1hex
=sha1hex
, summary
=summary
,
320 cmdexec
=cmdexec
, comment_char
=comment_char
)
321 self
.invisibleRootItem().addChild(item
)
323 def decorate(self
, items
):
328 self
.resizeColumnToContents(0)
329 self
.resizeColumnToContents(1)
330 self
.resizeColumnToContents(2)
331 self
.resizeColumnToContents(3)
332 self
.resizeColumnToContents(4)
335 def item_changed(self
, item
, column
):
336 if column
== item
.ENABLED_COLUMN
:
340 invalid_first_choice
= set([FIXUP
, SQUASH
])
341 for item
in self
.items():
342 if item
.is_enabled() and item
.is_commit():
343 if item
.command
in invalid_first_choice
:
344 item
.reset_command(PICK
)
347 def set_selected_to(self
, command
):
348 for i
in self
.selected_items():
349 i
.reset_command(command
)
352 def set_command(self
, item
, command
):
353 item
.reset_command(command
)
357 item
= self
.selected_item()
360 clipboard
= item
.sha1hex
or item
.cmdexec
361 qtutils
.set_clipboard(clipboard
)
363 def selection_changed(self
):
364 item
= self
.selected_item()
365 if item
is None or not item
.is_commit():
367 sha1hex
= item
.sha1hex
368 ctx
= dag
.DAG(sha1hex
, 2)
369 repo
= dag
.RepoReader(ctx
)
374 commits
= commits
[-1:]
375 self
.notifier
.notify_observers(diff
.COMMITS_SELECTED
, commits
)
377 def toggle_enabled(self
):
378 item
= self
.selected_item()
381 item
.toggle_enabled()
383 def select_first(self
):
387 idx
= self
.model().index(0, 0)
389 self
.setCurrentIndex(idx
)
391 def shift_down(self
):
392 item
= self
.selected_item()
396 idx
= items
.index(item
)
397 if idx
< len(items
) - 1:
398 self
.move_rows
.emit([idx
], idx
+ 1)
401 item
= self
.selected_item()
405 idx
= items
.index(item
)
407 self
.move_rows
.emit([idx
], idx
- 1)
409 def move(self
, src_idxs
, dst_idx
):
412 for idx
in reversed(sorted(src_idxs
)):
413 item
= items
[idx
].copy()
414 self
.invisibleRootItem().takeChild(idx
)
415 new_items
.insert(0, item
)
418 self
.invisibleRootItem().insertChildren(dst_idx
, new_items
)
419 self
.setCurrentItem(new_items
[0])
420 # If we've moved to the top then we need to re-decorate all items.
421 # Otherwise, we can decorate just the new items.
423 self
.decorate(self
.items())
425 self
.decorate(new_items
)
430 def dropEvent(self
, event
):
431 super(RebaseTreeWidget
, self
).dropEvent(event
)
434 def contextMenuEvent(self
, event
):
435 menu
= qtutils
.create_menu(N_('Actions'), self
)
436 menu
.addAction(self
.action_pick
)
437 menu
.addAction(self
.action_reword
)
438 menu
.addAction(self
.action_edit
)
439 menu
.addAction(self
.action_fixup
)
440 menu
.addAction(self
.action_squash
)
442 menu
.addAction(self
.toggle_enabled_action
)
444 menu
.addAction(self
.copy_sha1_action
)
445 menu
.addAction(self
.external_diff_action
)
446 menu
.exec_(self
.mapToGlobal(event
.pos()))
449 class ComboBox(QtWidgets
.QComboBox
):
453 class RebaseTreeWidgetItem(QtWidgets
.QTreeWidgetItem
):
459 def __init__(self
, idx
, enabled
, command
,
460 sha1hex
='', summary
='', cmdexec
='', comment_char
='#',
462 QtWidgets
.QTreeWidgetItem
.__init
__(self
, parent
)
464 self
.command
= command
466 self
.sha1hex
= sha1hex
467 self
.summary
= summary
468 self
.cmdexec
= cmdexec
469 self
.comment_char
= comment_char
471 # if core.abbrev is set to a higher value then we will notice by
472 # simply tracking the longest sha1 we've seen
473 sha1len
= self
.__class
__.SHA1LEN
474 sha1len
= self
.__class
__.SHA1LEN
= max(len(sha1hex
), sha1len
)
476 self
.setText(0, '%02d' % idx
)
477 self
.set_enabled(enabled
)
482 self
.setText(4, cmdexec
)
484 self
.setText(3, sha1hex
)
485 self
.setText(4, summary
)
487 flags
= self
.flags() | Qt
.ItemIsUserCheckable
488 flags
= flags | Qt
.ItemIsDragEnabled
489 flags
= flags
& ~Qt
.ItemIsDropEnabled
492 def __eq__(self
, other
):
496 return self
.__class
__(self
.idx
, self
.is_enabled(), self
.command
,
497 sha1hex
=self
.sha1hex
, summary
=self
.summary
,
498 cmdexec
=self
.cmdexec
)
500 def decorate(self
, parent
):
506 idx
= COMMAND_IDX
[self
.command
]
507 combo
= self
.combo
= ComboBox()
508 combo
.setEditable(False)
509 combo
.addItems(items
)
510 combo
.setCurrentIndex(idx
)
511 combo
.setEnabled(self
.is_commit())
513 signal
= combo
.currentIndexChanged
514 signal
.connect(lambda x
: self
.set_command_and_validate(combo
))
515 combo
.validate
.connect(parent
.validate
)
517 parent
.setItemWidget(self
, self
.COMMAND_COLUMN
, combo
)
520 return self
.command
== EXEC
523 return bool(self
.command
!= EXEC
and self
.sha1hex
and self
.summary
)
526 """Return the serialized representation of an item"""
527 if self
.is_enabled():
530 comment
= self
.comment_char
+ ' '
532 return ('%s%s %s' % (comment
, self
.command
, self
.cmdexec
))
533 return ('%s%s %s %s' %
534 (comment
, self
.command
, self
.sha1hex
, self
.summary
))
536 def is_enabled(self
):
537 return self
.checkState(self
.ENABLED_COLUMN
) == Qt
.Checked
539 def set_enabled(self
, enabled
):
540 self
.setCheckState(self
.ENABLED_COLUMN
,
541 enabled
and Qt
.Checked
or Qt
.Unchecked
)
543 def toggle_enabled(self
):
544 self
.set_enabled(not self
.is_enabled())
546 def set_command(self
, command
):
547 """Set the item to a different command, no-op for exec items"""
550 self
.command
= command
553 """Update the view to match the updated state"""
555 command
= self
.command
556 self
.combo
.setCurrentIndex(COMMAND_IDX
[command
])
558 def reset_command(self
, command
):
559 """Set and refresh the item in one shot"""
560 self
.set_command(command
)
563 def set_command_and_validate(self
, combo
):
564 command
= COMMANDS
[combo
.currentIndex()]
565 self
.set_command(command
)
566 self
.combo
.validate
.emit()
574 reword = use commit, but edit the commit message
575 edit = use commit, but stop for amending
576 squash = use commit, but meld into previous commit
577 fixup = like "squash", but discard this commit's log message
578 exec = run command (the rest of the line) using shell
580 These lines can be re-ordered; they are executed from top to bottom.
582 If you disable a line here THAT COMMIT WILL BE LOST.
584 However, if you disable everything, the rebase will be aborted.
599 spacebar = toggle enabled
601 ctrl+enter = accept changes and rebase
602 ctrl+q = cancel and abort the rebase
603 ctrl+d = launch external diff
605 title
= N_('Help - git-xbase')
606 return text
.text_dialog(help_text
, title
)
609 if __name__
== '__main__':