sequenceeditor: use qtutils.SimpleTask instead of a raw Thread
[git-cola.git] / cola / sequenceeditor.py
bloba6638edb9004374e0e27e8e990e3752f92f75751
1 import sys
2 import re
3 from argparse import ArgumentParser
4 from functools import partial
6 from cola import app # prints a message if Qt cannot be found
7 from qtpy import QtGui
8 from qtpy import QtWidgets
9 from qtpy.QtCore import Qt
10 from qtpy.QtCore import Signal
12 # pylint: disable=ungrouped-imports
13 from cola import core
14 from cola import difftool
15 from cola import gitcmds
16 from cola import hotkeys
17 from cola import icons
18 from cola import qtutils
19 from cola import utils
20 from cola.i18n import N_
21 from cola.models import dag
22 from cola.models import prefs
23 from cola.widgets import defs
24 from cola.widgets import filelist
25 from cola.widgets import diff
26 from cola.widgets import standard
27 from cola.widgets import text
30 PICK = 'pick'
31 REWORD = 'reword'
32 EDIT = 'edit'
33 FIXUP = 'fixup'
34 SQUASH = 'squash'
35 UPDATE_REF = 'update-ref'
36 EXEC = 'exec'
37 COMMANDS = (
38 PICK,
39 REWORD,
40 EDIT,
41 FIXUP,
42 SQUASH,
44 COMMAND_IDX = {cmd_: idx_ for idx_, cmd_ in enumerate(COMMANDS)}
45 ABBREV = {
46 'p': PICK,
47 'r': REWORD,
48 'e': EDIT,
49 'f': FIXUP,
50 's': SQUASH,
51 'x': EXEC,
52 'u': UPDATE_REF,
56 def main():
57 """Start a git-cola-sequence-editor session"""
58 args = parse_args()
59 context = app.application_init(args)
60 view = new_window(context, args.filename)
61 app.application_run(context, view, start=view.start, stop=stop)
62 return view.status
65 def stop(context, _view):
66 """All done, cleanup"""
67 context.runtask.wait()
70 def parse_args():
71 parser = ArgumentParser()
72 parser.add_argument(
73 'filename', metavar='<filename>', help='git-rebase-todo file to edit'
75 app.add_common_arguments(parser)
76 return parser.parse_args()
79 def new_window(context, filename):
80 window = MainWindow(context)
81 editor = Editor(context, filename, parent=window)
82 window.set_editor(editor)
83 return window
86 def unabbrev(cmd):
87 """Expand shorthand commands into their full name"""
88 return ABBREV.get(cmd, cmd)
91 class MainWindow(standard.MainWindow):
92 """The main git-cola application window"""
94 def __init__(self, context, parent=None):
95 super().__init__(parent)
96 self.context = context
97 self.status = 1
98 # If user closed the window without confirmation it's considered cancelled.
99 self.cancelled = False
100 self.editor = None
101 default_title = '%s - git cola sequence editor' % core.getcwd()
102 title = core.getenv('GIT_COLA_SEQ_EDITOR_TITLE', default_title)
103 self.setWindowTitle(title)
104 self.show_help_action = qtutils.add_action(
105 self, N_('Show Help'), partial(show_help, context), hotkeys.QUESTION
107 self.menubar = QtWidgets.QMenuBar(self)
108 self.help_menu = self.menubar.addMenu(N_('Help'))
109 self.help_menu.addAction(self.show_help_action)
110 self.setMenuBar(self.menubar)
112 qtutils.add_close_action(self)
113 self.init_state(context.settings, self.init_window_size)
115 def init_window_size(self):
116 """Set the window size on the first initial view"""
117 if utils.is_darwin():
118 width, height = qtutils.desktop_size()
119 self.resize(width, height)
120 else:
121 self.showMaximized()
123 def set_editor(self, editor):
124 self.editor = editor
125 self.setCentralWidget(editor)
126 editor.cancel.connect(self.cancel)
127 editor.rebase.connect(self.rebase)
128 editor.setFocus()
130 def start(self, _context, _view):
131 self.editor.start()
133 def cancel(self):
134 self.cancelled = True
135 self.close()
137 def rebase(self):
138 self.cancelled = False
139 self.close()
141 def closeEvent(self, event):
142 self.editor.stop()
143 if self.cancelled:
144 cancel_action = core.getenv('GIT_COLA_SEQ_EDITOR_CANCEL_ACTION', 'abort')
146 if cancel_action == 'save':
147 status = self.editor.save('')
148 else:
149 status = 1
150 else:
151 status = self.editor.save()
152 self.status = status
153 stop(self.context, self)
155 super().closeEvent(event)
158 class Editor(QtWidgets.QWidget):
159 cancel = Signal()
160 rebase = Signal()
162 def __init__(self, context, filename, parent=None):
163 super().__init__(parent)
165 self.widget_version = 1
166 self.context = context
167 self.filename = filename
168 self.comment_char = comment_char = prefs.comment_char(context)
170 self.diff = diff.DiffWidget(context, self)
171 self.tree = RebaseTreeWidget(context, comment_char, self)
172 self.filewidget = filelist.FileWidget(context, self, remarks=True)
173 self.setFocusProxy(self.tree)
175 self.rebase_button = qtutils.create_button(
176 text=core.getenv('GIT_COLA_SEQ_EDITOR_ACTION', N_('Rebase')),
177 tooltip=N_('Accept changes and rebase\nShortcut: Ctrl+Enter'),
178 icon=icons.ok(),
179 default=True,
182 self.extdiff_button = qtutils.create_button(
183 text=N_('Launch Diff Tool'),
184 tooltip=N_('Launch external diff tool\nShortcut: Ctrl+D'),
186 self.extdiff_button.setEnabled(False)
188 self.help_button = qtutils.create_button(
189 text=N_('Help'), tooltip=N_('Show help\nShortcut: ?'), icon=icons.question()
192 self.cancel_button = qtutils.create_button(
193 text=N_('Cancel'),
194 tooltip=N_('Cancel rebase\nShortcut: Ctrl+Q'),
195 icon=icons.close(),
198 top = qtutils.splitter(Qt.Horizontal, self.tree, self.filewidget)
199 top.setSizes([75, 25])
201 main_split = qtutils.splitter(Qt.Vertical, top, self.diff)
202 main_split.setSizes([25, 75])
204 controls_layout = qtutils.hbox(
205 defs.no_margin,
206 defs.button_spacing,
207 self.cancel_button,
208 qtutils.STRETCH,
209 self.help_button,
210 self.extdiff_button,
211 self.rebase_button,
213 layout = qtutils.vbox(defs.no_margin, defs.spacing, main_split, controls_layout)
214 self.setLayout(layout)
216 self.action_rebase = qtutils.add_action(
217 self,
218 N_('Rebase'),
219 self.rebase.emit,
220 hotkeys.CTRL_RETURN,
221 hotkeys.CTRL_ENTER,
224 self.tree.commits_selected.connect(self.commits_selected)
225 self.tree.commits_selected.connect(self.filewidget.commits_selected)
226 self.tree.commits_selected.connect(self.diff.commits_selected)
227 self.tree.external_diff.connect(self.external_diff)
229 self.filewidget.files_selected.connect(self.diff.files_selected)
230 self.filewidget.remark_toggled.connect(self.remark_toggled_for_files)
232 # `git` calls are expensive. When user toggles a remark of all commits touching
233 # selected paths the GUI freezes for a while on a big enough sequence. This
234 # cache is used (commit ID to paths tuple) to minimize calls to git.
235 self.oid_to_paths = {}
236 self.task = None # A task fills the cache in the background.
237 self.running = False # This flag stops it.
239 qtutils.connect_button(self.rebase_button, self.rebase.emit)
240 qtutils.connect_button(self.extdiff_button, self.external_diff)
241 qtutils.connect_button(self.help_button, partial(show_help, context))
242 qtutils.connect_button(self.cancel_button, self.cancel.emit)
244 def start(self):
245 insns = core.read(self.filename)
246 self.parse_sequencer_instructions(insns)
248 # Assume that the tree is filled at this point.
249 self.running = True
250 self.task = qtutils.SimpleTask(self.calculate_oid_to_paths)
251 self.context.runtask.start(self.task)
253 def stop(self):
254 self.running = False
256 # signal callbacks
257 def commits_selected(self, commits):
258 self.extdiff_button.setEnabled(bool(commits))
260 def remark_toggled_for_files(self, remark, filenames):
261 filenames = set(filenames)
263 items = self.tree.items()
264 touching_items = []
266 for item in items:
267 if not item.is_commit():
268 continue
269 oid = item.oid
270 paths = self.paths_touched_by_oid(oid)
271 if filenames.intersection(paths):
272 touching_items.append(item)
274 self.tree.toggle_remark_of_items(remark, touching_items)
276 def external_diff(self):
277 items = self.tree.selected_items()
278 if not items:
279 return
280 item = items[0]
281 difftool.diff_expression(self.context, self, item.oid + '^!', hide_expr=True)
283 # helpers
285 def paths_touched_by_oid(self, oid):
286 try:
287 return self.oid_to_paths[oid]
288 except KeyError:
289 pass
291 paths = gitcmds.changed_files(self.context, oid)
292 self.oid_to_paths[oid] = paths
294 return paths
296 def calculate_oid_to_paths(self):
297 """Fills the oid_to_paths cache in the background"""
298 for item in self.tree.items():
299 if not self.running:
300 return
301 self.paths_touched_by_oid(item.oid)
303 def parse_sequencer_instructions(self, insns):
304 idx = 1
305 re_comment_char = re.escape(self.comment_char)
306 exec_rgx = re.compile(r'^\s*(%s)?\s*(x|exec)\s+(.+)$' % re_comment_char)
307 update_ref_rgx = re.compile(
308 r'^\s*(%s)?\s*(u|update-ref)\s+(.+)$' % re_comment_char
310 # The upper bound of 40 below must match git.OID_LENGTH.
311 # We'll have to update this to the new hash length when that happens.
312 pick_rgx = re.compile(
314 r'^\s*(%s)?\s*'
315 + r'(p|pick|r|reword|e|edit|f|fixup|s|squash)'
316 + r'\s+([0-9a-f]{7,40})'
317 + r'\s+(.+)$'
319 % re_comment_char
321 for line in insns.splitlines():
322 match = pick_rgx.match(line)
323 if match:
324 enabled = match.group(1) is None
325 command = unabbrev(match.group(2))
326 oid = match.group(3)
327 summary = match.group(4)
328 self.tree.add_item(idx, enabled, command, oid=oid, summary=summary)
329 idx += 1
330 continue
331 match = exec_rgx.match(line)
332 if match:
333 enabled = match.group(1) is None
334 command = unabbrev(match.group(2))
335 cmdexec = match.group(3)
336 self.tree.add_item(idx, enabled, command, cmdexec=cmdexec)
337 idx += 1
338 continue
339 match = update_ref_rgx.match(line)
340 if match:
341 enabled = match.group(1) is None
342 command = unabbrev(match.group(2))
343 branch = match.group(3)
344 self.tree.add_item(idx, enabled, command, branch=branch)
345 idx += 1
346 continue
348 self.tree.decorate(self.tree.items())
349 self.tree.refit()
350 self.tree.select_first()
352 def save(self, string=None):
353 """Save the instruction sheet"""
355 if string is None:
356 lines = [item.value() for item in self.tree.items()]
357 # sequencer instructions
358 string = '\n'.join(lines) + '\n'
360 try:
361 core.write(self.filename, string)
362 status = 0
363 except (OSError, ValueError) as exc:
364 msg, details = utils.format_exception(exc)
365 sys.stderr.write(msg + '\n\n' + details)
366 status = 128
367 return status
370 # pylint: disable=too-many-ancestors
371 class RebaseTreeWidget(standard.DraggableTreeWidget):
372 commits_selected = Signal(object)
373 external_diff = Signal()
374 move_rows = Signal(object, object)
376 def __init__(self, context, comment_char, parent):
377 super().__init__(parent=parent)
378 self.context = context
379 self.comment_char = comment_char
380 # header
381 self.setHeaderLabels([
382 N_('#'),
383 N_('Enabled'),
384 N_('Command'),
385 N_('SHA-1'),
386 N_('Remarks'),
387 N_('Summary'),
389 self.header().setStretchLastSection(True)
390 self.setColumnCount(6)
391 self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
393 # actions
394 self.copy_oid_action = qtutils.add_action(
395 self, N_('Copy SHA-1'), self.copy_oid, QtGui.QKeySequence.Copy
398 self.external_diff_action = qtutils.add_action(
399 self, N_('Launch Diff Tool'), self.external_diff.emit, hotkeys.DIFF
402 self.toggle_enabled_action = qtutils.add_action(
403 self, N_('Toggle Enabled'), self.toggle_enabled, hotkeys.PRIMARY_ACTION
406 self.action_pick = qtutils.add_action(
407 self, N_('Pick'), lambda: self.set_selected_to(PICK), *hotkeys.REBASE_PICK
410 self.action_reword = qtutils.add_action(
411 self,
412 N_('Reword'),
413 lambda: self.set_selected_to(REWORD),
414 *hotkeys.REBASE_REWORD,
417 self.action_edit = qtutils.add_action(
418 self, N_('Edit'), lambda: self.set_selected_to(EDIT), *hotkeys.REBASE_EDIT
421 self.action_fixup = qtutils.add_action(
422 self,
423 N_('Fixup'),
424 lambda: self.set_selected_to(FIXUP),
425 *hotkeys.REBASE_FIXUP,
428 self.action_squash = qtutils.add_action(
429 self,
430 N_('Squash'),
431 lambda: self.set_selected_to(SQUASH),
432 *hotkeys.REBASE_SQUASH,
435 self.action_shift_down = qtutils.add_action(
436 self, N_('Shift Down'), self.shift_down, hotkeys.MOVE_DOWN_TERTIARY
439 self.action_shift_up = qtutils.add_action(
440 self, N_('Shift Up'), self.shift_up, hotkeys.MOVE_UP_TERTIARY
443 self.toggle_remark_actions = tuple(
444 qtutils.add_action(
445 self,
447 lambda remark=r: self.toggle_remark(remark),
448 hotkeys.hotkey(Qt.CTRL | getattr(Qt, 'Key_' + r)),
450 for r in map(str, range(10))
453 # pylint: disable=no-member
454 self.itemChanged.connect(self.item_changed)
455 self.itemSelectionChanged.connect(self.selection_changed)
456 self.move_rows.connect(self.move)
457 self.items_moved.connect(self.decorate)
459 def add_item(
460 self, idx, enabled, command, oid='', summary='', cmdexec='', branch=''
462 comment_char = self.comment_char
463 item = RebaseTreeWidgetItem(
464 idx,
465 enabled,
466 command,
467 oid=oid,
468 summary=summary,
469 cmdexec=cmdexec,
470 branch=branch,
471 comment_char=comment_char,
473 self.invisibleRootItem().addChild(item)
475 def decorate(self, items):
476 for item in items:
477 item.decorate(self)
479 def refit(self):
480 self.resizeColumnToContents(0)
481 self.resizeColumnToContents(1)
482 self.resizeColumnToContents(2)
483 self.resizeColumnToContents(3)
484 self.resizeColumnToContents(4)
485 self.resizeColumnToContents(5)
487 # actions
488 def item_changed(self, item, column):
489 if column == item.ENABLED_COLUMN:
490 self.validate()
492 def validate(self):
493 invalid_first_choice = {FIXUP, SQUASH}
494 for item in self.items():
495 if item.is_enabled() and item.is_commit():
496 if item.command in invalid_first_choice:
497 item.reset_command(PICK)
498 break
500 def set_selected_to(self, command):
501 for i in self.selected_items():
502 i.reset_command(command)
503 self.validate()
505 def set_command(self, item, command):
506 item.reset_command(command)
507 self.validate()
509 def copy_oid(self):
510 item = self.selected_item()
511 if item is None:
512 return
513 clipboard = item.oid or item.cmdexec
514 qtutils.set_clipboard(clipboard)
516 def selection_changed(self):
517 item = self.selected_item()
518 if item is None or not item.is_commit():
519 return
520 context = self.context
521 oid = item.oid
522 params = dag.DAG(oid, 2)
523 repo = dag.RepoReader(context, params)
524 commits = []
525 for commit in repo.get():
526 commits.append(commit)
527 if commits:
528 commits = commits[-1:]
529 self.commits_selected.emit(commits)
531 def toggle_enabled(self):
532 items = self.selected_items()
533 logic_or = reduce(lambda res, item: res or item.is_enabled(), items, False)
534 for item in items:
535 item.set_enabled(not logic_or)
537 def select_first(self):
538 items = self.items()
539 if not items:
540 return
541 idx = self.model().index(0, 0)
542 if idx.isValid():
543 self.setCurrentIndex(idx)
545 def shift_down(self):
546 sel_items = self.selected_items()
547 all_items = self.items()
548 sel_idx = sorted([all_items.index(item) for item in sel_items])
549 if not sel_idx:
550 return
551 idx = sel_idx[0] + 1
552 if not (
553 idx > len(all_items) - len(sel_items)
554 or all_items[sel_idx[-1]] is all_items[-1]
556 self.move_rows.emit(sel_idx, idx)
558 def shift_up(self):
559 sel_items = self.selected_items()
560 all_items = self.items()
561 sel_idx = sorted([all_items.index(item) for item in sel_items])
562 if not sel_idx:
563 return
564 idx = sel_idx[0] - 1
565 if idx >= 0:
566 self.move_rows.emit(sel_idx, idx)
568 def toggle_remark(self, remark):
569 items = self.selected_items()
570 self.toggle_remark_of_items(remark, items)
572 def toggle_remark_of_items(self, remark, items):
573 logic_or = reduce(lambda res, item: res or remark in item.remarks, items, False)
574 if logic_or:
575 for item in items:
576 item.remove_remark(remark)
577 else:
578 for item in items:
579 item.add_remark(remark)
581 def move(self, src_idxs, dst_idx):
582 moved_items = []
583 src_base = sorted(src_idxs)[0]
584 for idx in reversed(sorted(src_idxs)):
585 item = self.invisibleRootItem().takeChild(idx)
586 moved_items.insert(0, [dst_idx + (idx - src_base), item])
588 for item in moved_items:
589 self.invisibleRootItem().insertChild(item[0], item[1])
590 self.setCurrentItem(item[1])
592 if moved_items:
593 moved_items = [item[1] for item in moved_items]
594 # If we've moved to the top then we need to re-decorate all items.
595 # Otherwise, we can decorate just the new items.
596 if dst_idx == 0:
597 self.decorate(self.items())
598 else:
599 self.decorate(moved_items)
601 for item in moved_items:
602 item.setSelected(True)
603 self.validate()
605 # Qt events
607 def dropEvent(self, event):
608 super().dropEvent(event)
609 self.validate()
611 def contextMenuEvent(self, event):
612 items = self.selected_items()
613 menu = qtutils.create_menu(N_('Actions'), self)
614 menu.addAction(self.action_pick)
615 menu.addAction(self.action_reword)
616 menu.addAction(self.action_edit)
617 menu.addAction(self.action_fixup)
618 menu.addAction(self.action_squash)
619 menu.addSeparator()
620 menu.addAction(self.toggle_enabled_action)
621 menu.addSeparator()
622 menu.addAction(self.copy_oid_action)
623 self.copy_oid_action.setDisabled(len(items) > 1)
624 menu.addAction(self.external_diff_action)
625 self.external_diff_action.setDisabled(len(items) > 1)
626 menu.addSeparator()
627 menu_toggle_remark = menu.addMenu(N_('Toggle remark'))
628 tuple(map(menu_toggle_remark.addAction, self.toggle_remark_actions))
629 menu.exec_(self.mapToGlobal(event.pos()))
632 class ComboBox(QtWidgets.QComboBox):
633 validate = Signal()
636 class RebaseTreeWidgetItem(QtWidgets.QTreeWidgetItem):
637 ENABLED_COLUMN = 1
638 COMMAND_COLUMN = 2
639 OID_LENGTH = 7
641 def __init__(
642 self,
643 idx,
644 enabled,
645 command,
646 oid='',
647 summary='',
648 cmdexec='',
649 branch='',
650 comment_char='#',
651 remarks=tuple(),
652 parent=None,
654 QtWidgets.QTreeWidgetItem.__init__(self, parent)
655 self.combo = None
656 self.command = command
657 self.idx = idx
658 self.oid = oid
659 self.summary = summary
660 self.cmdexec = cmdexec
661 self.branch = branch
662 self.comment_char = comment_char
664 # if core.abbrev is set to a higher value then we will notice by
665 # simply tracking the longest oid we've seen
666 oid_len = self.__class__.OID_LENGTH
667 self.__class__.OID_LENGTH = max(len(oid), oid_len)
669 self.setText(0, '%02d' % idx)
670 self.set_enabled(enabled)
671 # checkbox on 1
672 # combo box on 2
673 if self.is_exec():
674 self.setText(3, '')
675 self.setText(5, cmdexec)
676 elif self.is_update_ref():
677 self.setText(3, '')
678 self.setText(5, branch)
679 else:
680 self.setText(3, oid)
681 self.setText(5, summary)
683 self.set_remarks(remarks)
685 flags = self.flags() | Qt.ItemIsUserCheckable
686 flags = flags | Qt.ItemIsDragEnabled
687 flags = flags & ~Qt.ItemIsDropEnabled
688 self.setFlags(flags)
690 def __eq__(self, other):
691 return self is other
693 def __hash__(self):
694 return self.oid
696 def copy(self):
697 return self.__class__(
698 self.idx,
699 self.is_enabled(),
700 self.command,
701 oid=self.oid,
702 summary=self.summary,
703 cmdexec=self.cmdexec,
704 branch=self.branch,
705 comment_char=self.comment_char,
706 remarks=self.remarks,
709 def decorate(self, parent):
710 if self.is_exec():
711 items = [EXEC]
712 idx = 0
713 elif self.is_update_ref():
714 items = [UPDATE_REF]
715 idx = 0
716 else:
717 items = COMMANDS
718 idx = COMMAND_IDX[self.command]
719 combo = self.combo = ComboBox()
720 combo.setEditable(False)
721 combo.addItems(items)
722 combo.setCurrentIndex(idx)
723 combo.setEnabled(self.is_commit())
725 signal = combo.currentIndexChanged
726 # pylint: disable=no-member
727 signal.connect(lambda x: self.set_command_and_validate(combo))
728 combo.validate.connect(parent.validate)
730 parent.setItemWidget(self, self.COMMAND_COLUMN, combo)
732 def is_exec(self):
733 return self.command == EXEC
735 def is_update_ref(self):
736 return self.command == UPDATE_REF
738 def is_commit(self):
739 return bool(
740 not (self.is_exec() or self.is_update_ref()) and self.oid and self.summary
743 def value(self):
744 """Return the serialized representation of an item"""
745 if self.is_enabled():
746 comment = ''
747 else:
748 comment = self.comment_char + ' '
749 if self.is_exec():
750 return f'{comment}{self.command} {self.cmdexec}'
751 if self.is_update_ref():
752 return f'{comment}{self.command} {self.branch}'
753 return f'{comment}{self.command} {self.oid} {self.summary}'
755 def is_enabled(self):
756 return self.checkState(self.ENABLED_COLUMN) == Qt.Checked
758 def set_enabled(self, enabled):
759 self.setCheckState(self.ENABLED_COLUMN, enabled and Qt.Checked or Qt.Unchecked)
761 def toggle_enabled(self):
762 self.set_enabled(not self.is_enabled())
764 def add_remark(self, remark):
765 self.set_remarks(tuple(sorted(set(self.remarks + (remark,)))))
767 def remove_remark(self, remark):
768 self.set_remarks(tuple(r for r in self.remarks if r != remark))
770 def set_remarks(self, remarks):
771 self.remarks = remarks
772 self.setText(4, ''.join(remarks))
774 def set_command(self, command):
775 """Set the item to a different command, no-op for exec items"""
776 if self.is_exec():
777 return
778 self.command = command
780 def refresh(self):
781 """Update the view to match the updated state"""
782 if self.is_commit():
783 command = self.command
784 self.combo.setCurrentIndex(COMMAND_IDX[command])
786 def reset_command(self, command):
787 """Set and refresh the item in one shot"""
788 self.set_command(command)
789 self.refresh()
791 def set_command_and_validate(self, combo):
792 command = COMMANDS[combo.currentIndex()]
793 self.set_command(command)
794 self.combo.validate.emit()
797 def show_help(context):
798 help_text = N_(
800 Commands
801 --------
802 pick = use commit
803 reword = use commit, but edit the commit message
804 edit = use commit, but stop for amending
805 squash = use commit, but meld into previous commit
806 fixup = like "squash", but discard this commit's log message
807 exec = run command (the rest of the line) using shell
808 update-ref = update branches that point to commits
810 These lines can be re-ordered; they are executed from top to bottom.
812 If you disable a line here THAT COMMIT WILL BE LOST.
814 However, if you disable everything, the rebase will be aborted.
816 Keyboard Shortcuts
817 ------------------
818 ? = show help
819 j = move down
820 k = move up
821 J = shift row down
822 K = shift row up
824 1, p = pick
825 2, r = reword
826 3, e = edit
827 4, f = fixup
828 5, s = squash
829 spacebar = toggle enabled
831 ctrl+enter = accept changes and rebase
832 ctrl+q = cancel and abort the rebase
833 ctrl+d = launch difftool
836 title = N_('Help - git-cola-sequence-editor')
837 return text.text_dialog(context, help_text, title)