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
, 'Ctrl+Return')
177 notifier
.add_observer(diff
.COMMITS_SELECTED
, self
.commits_selected
)
178 self
.tree
.external_diff
.connect(self
.external_diff
)
180 qtutils
.connect_button(self
.rebase_button
, self
.rebase
)
181 qtutils
.connect_button(self
.extdiff_button
, self
.external_diff
)
182 qtutils
.connect_button(self
.help_button
, show_help
)
183 qtutils
.connect_button(self
.cancel_button
, self
.cancel
)
185 insns
= core
.read(filename
)
186 self
.parse_sequencer_instructions(insns
)
189 def commits_selected(self
, commits
):
190 self
.extdiff_button
.setEnabled(bool(commits
))
193 def parse_sequencer_instructions(self
, insns
):
195 re_comment_char
= re
.escape(self
.comment_char
)
196 exec_rgx
= re
.compile(r
'^\s*(%s)?\s*(x|exec)\s+(.+)$'
198 pick_rgx
= re
.compile((r
'^\s*(%s)?\s*'
199 r
'(p|pick|r|reword|e|edit|f|fixup|s|squash)'
200 r
'\s+([0-9a-f]{7,40})'
201 r
'\s+(.+)$') % re_comment_char
)
202 for line
in insns
.splitlines():
203 match
= pick_rgx
.match(line
)
205 enabled
= match
.group(1) is None
206 command
= unabbrev(match
.group(2))
207 sha1hex
= match
.group(3)
208 summary
= match
.group(4)
209 self
.tree
.add_item(idx
, enabled
, command
,
210 sha1hex
=sha1hex
, summary
=summary
)
213 match
= exec_rgx
.match(line
)
215 enabled
= match
.group(1) is None
216 command
= unabbrev(match
.group(2))
217 cmdexec
= match
.group(3)
218 self
.tree
.add_item(idx
, enabled
, command
, cmdexec
=cmdexec
)
222 self
.tree
.decorate(self
.tree
.items())
224 self
.tree
.select_first()
232 lines
= [item
.value() for item
in self
.tree
.items()]
233 sequencer_instructions
= '\n'.join(lines
) + '\n'
235 core
.write(self
.filename
, sequencer_instructions
)
238 except Exception as e
:
239 msg
, details
= utils
.format_exception(e
)
240 sys
.stderr
.write(msg
+ '\n\n' + details
)
244 def external_diff(self
):
245 items
= self
.tree
.selected_items()
249 difftool
.diff_expression(self
, item
.sha1hex
+ '^!',
253 class RebaseTreeWidget(standard
.DraggableTreeWidget
):
254 external_diff
= Signal()
255 move_rows
= Signal(object, object)
257 def __init__(self
, notifier
, comment_char
, parent
=None):
258 super(RebaseTreeWidget
, self
).__init
__(parent
=parent
)
259 self
.notifier
= notifier
260 self
.comment_char
= comment_char
262 self
.setHeaderLabels([N_('#'),
267 self
.header().setStretchLastSection(True)
268 self
.setColumnCount(5)
271 self
.copy_sha1_action
= qtutils
.add_action(
272 self
, N_('Copy SHA-1'), self
.copy_sha1
, QtGui
.QKeySequence
.Copy
)
274 self
.external_diff_action
= qtutils
.add_action(
275 self
, N_('External Diff'), self
.external_diff
.emit
,
278 self
.toggle_enabled_action
= qtutils
.add_action(
279 self
, N_('Toggle Enabled'), self
.toggle_enabled
,
280 hotkeys
.PRIMARY_ACTION
)
282 self
.action_pick
= qtutils
.add_action(
283 self
, N_('Pick'), lambda: self
.set_selected_to(PICK
),
284 *hotkeys
.REBASE_PICK
)
286 self
.action_reword
= qtutils
.add_action(
287 self
, N_('Reword'), lambda: self
.set_selected_to(REWORD
),
288 *hotkeys
.REBASE_REWORD
)
290 self
.action_edit
= qtutils
.add_action(
291 self
, N_('Edit'), lambda: self
.set_selected_to(EDIT
),
292 *hotkeys
.REBASE_EDIT
)
294 self
.action_fixup
= qtutils
.add_action(
295 self
, N_('Fixup'), lambda: self
.set_selected_to(FIXUP
),
296 *hotkeys
.REBASE_FIXUP
)
298 self
.action_squash
= qtutils
.add_action(
299 self
, N_('Squash'), lambda: self
.set_selected_to(SQUASH
),
300 *hotkeys
.REBASE_SQUASH
)
302 self
.action_shift_down
= qtutils
.add_action(
303 self
, N_('Shift Down'), self
.shift_down
,
304 hotkeys
.MOVE_DOWN_TERTIARY
)
306 self
.action_shift_up
= qtutils
.add_action(
307 self
, N_('Shift Up'), self
.shift_up
, hotkeys
.MOVE_UP_TERTIARY
)
309 self
.itemChanged
.connect(self
.item_changed
)
310 self
.itemSelectionChanged
.connect(self
.selection_changed
)
311 self
.move_rows
.connect(self
.move
)
312 self
.items_moved
.connect(self
.decorate
)
314 def add_item(self
, idx
, enabled
, command
,
315 sha1hex
='', summary
='', cmdexec
=''):
316 comment_char
= self
.comment_char
317 item
= RebaseTreeWidgetItem(idx
, enabled
, command
,
318 sha1hex
=sha1hex
, summary
=summary
,
319 cmdexec
=cmdexec
, comment_char
=comment_char
)
320 self
.invisibleRootItem().addChild(item
)
322 def decorate(self
, items
):
327 self
.resizeColumnToContents(0)
328 self
.resizeColumnToContents(1)
329 self
.resizeColumnToContents(2)
330 self
.resizeColumnToContents(3)
331 self
.resizeColumnToContents(4)
334 def item_changed(self
, item
, column
):
335 if column
== item
.ENABLED_COLUMN
:
339 invalid_first_choice
= set([FIXUP
, SQUASH
])
340 for item
in self
.items():
341 if item
.is_enabled() and item
.is_commit():
342 if item
.command
in invalid_first_choice
:
343 item
.reset_command(PICK
)
346 def set_selected_to(self
, command
):
347 for i
in self
.selected_items():
348 i
.reset_command(command
)
351 def set_command(self
, item
, command
):
352 item
.reset_command(command
)
356 item
= self
.selected_item()
359 clipboard
= item
.sha1hex
or item
.cmdexec
360 qtutils
.set_clipboard(clipboard
)
362 def selection_changed(self
):
363 item
= self
.selected_item()
364 if item
is None or not item
.is_commit():
366 sha1hex
= item
.sha1hex
367 ctx
= dag
.DAG(sha1hex
, 2)
368 repo
= dag
.RepoReader(ctx
)
373 commits
= commits
[-1:]
374 self
.notifier
.notify_observers(diff
.COMMITS_SELECTED
, commits
)
376 def toggle_enabled(self
):
377 item
= self
.selected_item()
380 item
.toggle_enabled()
382 def select_first(self
):
386 idx
= self
.model().index(0, 0)
388 self
.setCurrentIndex(idx
)
390 def shift_down(self
):
391 item
= self
.selected_item()
395 idx
= items
.index(item
)
396 if idx
< len(items
) - 1:
397 self
.move_rows
.emit([idx
], idx
+ 1)
400 item
= self
.selected_item()
404 idx
= items
.index(item
)
406 self
.move_rows
.emit([idx
], idx
- 1)
408 def move(self
, src_idxs
, dst_idx
):
411 for idx
in reversed(sorted(src_idxs
)):
412 item
= items
[idx
].copy()
413 self
.invisibleRootItem().takeChild(idx
)
414 new_items
.insert(0, item
)
417 self
.invisibleRootItem().insertChildren(dst_idx
, new_items
)
418 self
.setCurrentItem(new_items
[0])
419 # If we've moved to the top then we need to re-decorate all items.
420 # Otherwise, we can decorate just the new items.
422 self
.decorate(self
.items())
424 self
.decorate(new_items
)
429 def dropEvent(self
, event
):
430 super(RebaseTreeWidget
, self
).dropEvent(event
)
433 def contextMenuEvent(self
, event
):
434 menu
= qtutils
.create_menu(N_('Actions'), self
)
435 menu
.addAction(self
.action_pick
)
436 menu
.addAction(self
.action_reword
)
437 menu
.addAction(self
.action_edit
)
438 menu
.addAction(self
.action_fixup
)
439 menu
.addAction(self
.action_squash
)
441 menu
.addAction(self
.toggle_enabled_action
)
443 menu
.addAction(self
.copy_sha1_action
)
444 menu
.addAction(self
.external_diff_action
)
445 menu
.exec_(self
.mapToGlobal(event
.pos()))
448 class ComboBox(QtWidgets
.QComboBox
):
452 class RebaseTreeWidgetItem(QtWidgets
.QTreeWidgetItem
):
458 def __init__(self
, idx
, enabled
, command
,
459 sha1hex
='', summary
='', cmdexec
='', comment_char
='#',
461 QtWidgets
.QTreeWidgetItem
.__init
__(self
, parent
)
463 self
.command
= command
465 self
.sha1hex
= sha1hex
466 self
.summary
= summary
467 self
.cmdexec
= cmdexec
468 self
.comment_char
= comment_char
470 # if core.abbrev is set to a higher value then we will notice by
471 # simply tracking the longest sha1 we've seen
472 sha1len
= self
.__class
__.SHA1LEN
473 sha1len
= self
.__class
__.SHA1LEN
= max(len(sha1hex
), sha1len
)
475 self
.setText(0, '%02d' % idx
)
476 self
.set_enabled(enabled
)
481 self
.setText(4, cmdexec
)
483 self
.setText(3, sha1hex
)
484 self
.setText(4, summary
)
486 flags
= self
.flags() | Qt
.ItemIsUserCheckable
487 flags
= flags | Qt
.ItemIsDragEnabled
488 flags
= flags
& ~Qt
.ItemIsDropEnabled
492 return self
.__class
__(self
.idx
, self
.is_enabled(), self
.command
,
493 sha1hex
=self
.sha1hex
, summary
=self
.summary
,
494 cmdexec
=self
.cmdexec
)
496 def decorate(self
, parent
):
502 idx
= COMMAND_IDX
[self
.command
]
503 combo
= self
.combo
= ComboBox()
504 combo
.setEditable(False)
505 combo
.addItems(items
)
506 combo
.setCurrentIndex(idx
)
507 combo
.setEnabled(self
.is_commit())
509 signal
= combo
.currentIndexChanged
['QString']
510 signal
.connect(self
.set_command_and_validate
)
511 combo
.validate
.connect(parent
.validate
)
513 parent
.setItemWidget(self
, self
.COMMAND_COLUMN
, combo
)
516 return self
.command
== EXEC
519 return bool(self
.command
!= EXEC
and self
.sha1hex
and self
.summary
)
522 """Return the serialized representation of an item"""
523 if self
.is_enabled():
526 comment
= self
.comment_char
+ ' '
528 return ('%s%s %s' % (comment
, self
.command
, self
.cmdexec
))
529 return ('%s%s %s %s' %
530 (comment
, self
.command
, self
.sha1hex
, self
.summary
))
532 def is_enabled(self
):
533 return self
.checkState(self
.ENABLED_COLUMN
) == Qt
.Checked
535 def set_enabled(self
, enabled
):
536 self
.setCheckState(self
.ENABLED_COLUMN
,
537 enabled
and Qt
.Checked
or Qt
.Unchecked
)
539 def toggle_enabled(self
):
540 self
.set_enabled(not self
.is_enabled())
542 def set_command(self
, command
):
543 """Set the item to a different command, no-op for exec items"""
546 self
.command
= command
549 """Update the view to match the updated state"""
550 command
= self
.command
551 self
.combo
.setCurrentIndex(COMMAND_IDX
[command
])
553 def reset_command(self
, command
):
554 """Set and refresh the item in one shot"""
555 self
.set_command(command
)
558 def set_command_and_validate(self
, command
):
559 self
.set_command(command
)
560 self
.combo
.validate
.emit()
568 reword = use commit, but edit the commit message
569 edit = use commit, but stop for amending
570 squash = use commit, but meld into previous commit
571 fixup = like "squash", but discard this commit's log message
572 exec = run command (the rest of the line) using shell
574 These lines can be re-ordered; they are executed from top to bottom.
576 If you disable a line here THAT COMMIT WILL BE LOST.
578 However, if you disable everything, the rebase will be aborted.
593 spacebar = toggle enabled
595 ctrl+enter = accept changes and rebase
596 ctrl+q = cancel and abort the rebase
597 ctrl+d = launch external diff
599 title
= N_('Help - git-xbase')
600 return text
.text_dialog(help_text
, title
)
603 if __name__
== '__main__':