sequenceeditor: import "gitcmds" directly
[git-cola.git] / cola / sequenceeditor.py
bloba5561a5d1e1cc3c0748621b178746f4f119d65ad
1 import sys
2 import re
3 from argparse import ArgumentParser
4 from functools import partial, reduce
5 from threading import Thread
7 from cola import app # prints a message if Qt cannot be found
8 from qtpy import QtCore
9 from qtpy import QtGui
10 from qtpy import QtWidgets
11 from qtpy.QtCore import Qt
12 from qtpy.QtCore import Signal
14 # pylint: disable=ungrouped-imports
15 from cola import core
16 from cola import difftool
17 from cola import gitcmds
18 from cola import hotkeys
19 from cola import icons
20 from cola import qtutils
21 from cola import utils
22 from cola.i18n import N_
23 from cola.models import dag
24 from cola.models import prefs
25 from cola.widgets import defs
26 from cola.widgets import filelist
27 from cola.widgets import diff
28 from cola.widgets import standard
29 from cola.widgets import text
32 PICK = 'pick'
33 REWORD = 'reword'
34 EDIT = 'edit'
35 FIXUP = 'fixup'
36 SQUASH = 'squash'
37 UPDATE_REF = 'update-ref'
38 EXEC = 'exec'
39 COMMANDS = (
40 PICK,
41 REWORD,
42 EDIT,
43 FIXUP,
44 SQUASH,
46 COMMAND_IDX = {cmd_: idx_ for idx_, cmd_ in enumerate(COMMANDS)}
47 ABBREV = {
48 'p': PICK,
49 'r': REWORD,
50 'e': EDIT,
51 'f': FIXUP,
52 's': SQUASH,
53 'x': EXEC,
54 'u': UPDATE_REF,
58 def main():
59 """Start a git-cola-sequence-editor session"""
60 args = parse_args()
61 context = app.application_init(args)
62 view = new_window(context, args.filename)
63 app.application_run(context, view, start=view.start, stop=stop)
64 return view.status
67 def stop(_context, _view):
68 """All done, cleanup"""
69 QtCore.QThreadPool.globalInstance().waitForDone()
72 def parse_args():
73 parser = ArgumentParser()
74 parser.add_argument(
75 'filename', metavar='<filename>', help='git-rebase-todo file to edit'
77 app.add_common_arguments(parser)
78 return parser.parse_args()
81 def new_window(context, filename):
82 window = MainWindow(context)
83 editor = Editor(context, filename, parent=window)
84 window.set_editor(editor)
85 return window
88 def unabbrev(cmd):
89 """Expand shorthand commands into their full name"""
90 return ABBREV.get(cmd, cmd)
93 class MainWindow(standard.MainWindow):
94 """The main git-cola application window"""
96 def __init__(self, context, parent=None):
97 super().__init__(parent)
98 self.context = context
99 self.status = 1
101 # Final user decision at the window close moment.
102 # If user just closed the window, it's considered to be canceled.
103 self.cancelled = True
105 self.editor = None
106 default_title = '%s - git cola sequence editor' % core.getcwd()
107 title = core.getenv('GIT_COLA_SEQ_EDITOR_TITLE', default_title)
108 self.setWindowTitle(title)
110 self.show_help_action = qtutils.add_action(
111 self, N_('Show Help'), partial(show_help, context), hotkeys.QUESTION
114 self.menubar = QtWidgets.QMenuBar(self)
115 self.help_menu = self.menubar.addMenu(N_('Help'))
116 self.help_menu.addAction(self.show_help_action)
117 self.setMenuBar(self.menubar)
119 qtutils.add_close_action(self)
120 self.init_state(context.settings, self.init_window_size)
122 def init_window_size(self):
123 """Set the window size on the first initial view"""
124 if utils.is_darwin():
125 width, height = qtutils.desktop_size()
126 self.resize(width, height)
127 else:
128 self.showMaximized()
130 def set_editor(self, editor):
131 self.editor = editor
132 self.setCentralWidget(editor)
133 editor.cancel.connect(self.cancel)
134 editor.rebase.connect(self.rebase)
135 editor.setFocus()
137 def start(self, _context, _view):
138 self.editor.start()
140 def cancel(self):
141 self.cancelled = True
142 self.close()
144 def rebase(self):
145 self.cancelled = False
146 self.close()
148 def closeEvent(self, event):
149 self.editor.stopped()
151 if self.cancelled:
152 cancel_action = core.getenv('GIT_COLA_SEQ_EDITOR_CANCEL_ACTION', 'abort')
154 if cancel_action == 'save':
155 status = self.editor.save('')
156 else:
157 status = 1
158 else:
159 status = self.editor.save()
161 self.status = status
163 super().closeEvent(event)
166 class Editor(QtWidgets.QWidget):
167 cancel = Signal()
168 rebase = Signal()
170 def __init__(self, context, filename, parent=None):
171 super().__init__(parent)
173 self.widget_version = 1
174 self.context = context
175 self.filename = filename
176 self.comment_char = comment_char = prefs.comment_char(context)
178 self.diff = diff.DiffWidget(context, self)
179 self.tree = RebaseTreeWidget(context, comment_char, self)
180 self.filewidget = filelist.FileWidget(context, self, remarks=True)
181 self.setFocusProxy(self.tree)
183 self.rebase_button = qtutils.create_button(
184 text=core.getenv('GIT_COLA_SEQ_EDITOR_ACTION', N_('Rebase')),
185 tooltip=N_('Accept changes and rebase\nShortcut: Ctrl+Enter'),
186 icon=icons.ok(),
187 default=True,
190 self.extdiff_button = qtutils.create_button(
191 text=N_('Launch Diff Tool'),
192 tooltip=N_('Launch external diff tool\nShortcut: Ctrl+D'),
194 self.extdiff_button.setEnabled(False)
196 self.help_button = qtutils.create_button(
197 text=N_('Help'), tooltip=N_('Show help\nShortcut: ?'), icon=icons.question()
200 self.cancel_button = qtutils.create_button(
201 text=N_('Cancel'),
202 tooltip=N_('Cancel rebase\nShortcut: Ctrl+Q'),
203 icon=icons.close(),
206 top = qtutils.splitter(Qt.Horizontal, self.tree, self.filewidget)
207 top.setSizes([75, 25])
209 main_split = qtutils.splitter(Qt.Vertical, top, self.diff)
210 main_split.setSizes([25, 75])
212 controls_layout = qtutils.hbox(
213 defs.no_margin,
214 defs.button_spacing,
215 self.cancel_button,
216 qtutils.STRETCH,
217 self.help_button,
218 self.extdiff_button,
219 self.rebase_button,
221 layout = qtutils.vbox(defs.no_margin, defs.spacing, main_split, controls_layout)
222 self.setLayout(layout)
224 self.action_rebase = qtutils.add_action(
225 self,
226 N_('Rebase'),
227 self.rebase.emit,
228 hotkeys.CTRL_RETURN,
229 hotkeys.CTRL_ENTER,
232 self.tree.commits_selected.connect(self.commits_selected)
233 self.tree.commits_selected.connect(self.filewidget.commits_selected)
234 self.tree.commits_selected.connect(self.diff.commits_selected)
235 self.tree.external_diff.connect(self.external_diff)
237 self.filewidget.files_selected.connect(self.diff.files_selected)
238 self.filewidget.remark_toggled.connect(self.remark_toggled_for_files)
240 # `git` calls are too expensive.
241 # When user toggles a remark of all commits touching selected paths
242 # the GUI freezes for a while on a big enough sequence.
243 # So, a cache is used (commit ID to paths tuple) to avoid freezing
244 # during consequent work.
245 self.oid_to_paths = {}
246 # A thread fills this cache in background to reduce the first
247 # run freezing.
248 # This flag stops it.
249 self.working = True
251 qtutils.connect_button(self.rebase_button, self.rebase.emit)
252 qtutils.connect_button(self.extdiff_button, self.external_diff)
253 qtutils.connect_button(self.help_button, partial(show_help, context))
254 qtutils.connect_button(self.cancel_button, self.cancel.emit)
256 def start(self):
257 insns = core.read(self.filename)
258 self.parse_sequencer_instructions(insns)
260 # Assume that the tree is filled at this point.
261 Thread(target=self.poll_touched_paths_main).start()
263 def stopped(self):
264 self.working = False
266 # signal callbacks
267 def commits_selected(self, commits):
268 self.extdiff_button.setEnabled(bool(commits))
270 def remark_toggled_for_files(self, remark, filenames):
271 filenames = set(filenames)
273 items = self.tree.items()
274 touching_items = []
276 for item in items:
277 if not item.is_commit():
278 continue
279 oid = item.oid
280 paths = self.paths_touched_by_oid(oid)
281 if filenames.intersection(paths):
282 touching_items.append(item)
284 self.tree.toggle_remark_of_items(remark, touching_items)
286 def external_diff(self):
287 items = self.tree.selected_items()
288 if not items:
289 return
290 item = items[0]
291 difftool.diff_expression(self.context, self, item.oid + '^!', hide_expr=True)
293 # helpers
295 def paths_touched_by_oid(self, oid):
296 try:
297 return self.oid_to_paths[oid]
298 except KeyError:
299 pass
301 paths = gitcmds.changed_files(self.context, oid)
302 self.oid_to_paths[oid] = paths
304 return paths
306 def poll_touched_paths_main(self):
307 for item in self.tree.items():
308 if not self.working:
309 return
310 self.paths_touched_by_oid(item.oid)
312 def parse_sequencer_instructions(self, insns):
313 idx = 1
314 re_comment_char = re.escape(self.comment_char)
315 exec_rgx = re.compile(r'^\s*(%s)?\s*(x|exec)\s+(.+)$' % re_comment_char)
316 update_ref_rgx = re.compile(
317 r'^\s*(%s)?\s*(u|update-ref)\s+(.+)$' % re_comment_char
319 # The upper bound of 40 below must match git.OID_LENGTH.
320 # We'll have to update this to the new hash length when that happens.
321 pick_rgx = re.compile(
323 r'^\s*(%s)?\s*'
324 + r'(p|pick|r|reword|e|edit|f|fixup|s|squash)'
325 + r'\s+([0-9a-f]{7,40})'
326 + r'\s+(.+)$'
328 % re_comment_char
330 for line in insns.splitlines():
331 match = pick_rgx.match(line)
332 if match:
333 enabled = match.group(1) is None
334 command = unabbrev(match.group(2))
335 oid = match.group(3)
336 summary = match.group(4)
337 self.tree.add_item(idx, enabled, command, oid=oid, summary=summary)
338 idx += 1
339 continue
340 match = exec_rgx.match(line)
341 if match:
342 enabled = match.group(1) is None
343 command = unabbrev(match.group(2))
344 cmdexec = match.group(3)
345 self.tree.add_item(idx, enabled, command, cmdexec=cmdexec)
346 idx += 1
347 continue
348 match = update_ref_rgx.match(line)
349 if match:
350 enabled = match.group(1) is None
351 command = unabbrev(match.group(2))
352 branch = match.group(3)
353 self.tree.add_item(idx, enabled, command, branch=branch)
354 idx += 1
355 continue
357 self.tree.decorate(self.tree.items())
358 self.tree.refit()
359 self.tree.select_first()
361 def save(self, string=None):
362 """Save the instruction sheet"""
364 if string is None:
365 lines = [item.value() for item in self.tree.items()]
366 # sequencer instructions
367 string = '\n'.join(lines) + '\n'
369 try:
370 core.write(self.filename, string)
371 status = 0
372 except (OSError, ValueError) as exc:
373 msg, details = utils.format_exception(exc)
374 sys.stderr.write(msg + '\n\n' + details)
375 status = 128
376 return status
379 # pylint: disable=too-many-ancestors
380 class RebaseTreeWidget(standard.DraggableTreeWidget):
381 commits_selected = Signal(object)
382 external_diff = Signal()
383 move_rows = Signal(object, object)
385 def __init__(self, context, comment_char, parent):
386 super().__init__(parent=parent)
387 self.context = context
388 self.comment_char = comment_char
389 # header
390 self.setHeaderLabels([
391 N_('#'),
392 N_('Enabled'),
393 N_('Command'),
394 N_('SHA-1'),
395 N_('Remarks'),
396 N_('Summary'),
398 self.header().setStretchLastSection(True)
399 self.setColumnCount(6)
400 self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
402 # actions
403 self.copy_oid_action = qtutils.add_action(
404 self, N_('Copy SHA-1'), self.copy_oid, QtGui.QKeySequence.Copy
407 self.external_diff_action = qtutils.add_action(
408 self, N_('Launch Diff Tool'), self.external_diff.emit, hotkeys.DIFF
411 self.toggle_enabled_action = qtutils.add_action(
412 self, N_('Toggle Enabled'), self.toggle_enabled, hotkeys.PRIMARY_ACTION
415 self.action_pick = qtutils.add_action(
416 self, N_('Pick'), lambda: self.set_selected_to(PICK), *hotkeys.REBASE_PICK
419 self.action_reword = qtutils.add_action(
420 self,
421 N_('Reword'),
422 lambda: self.set_selected_to(REWORD),
423 *hotkeys.REBASE_REWORD,
426 self.action_edit = qtutils.add_action(
427 self, N_('Edit'), lambda: self.set_selected_to(EDIT), *hotkeys.REBASE_EDIT
430 self.action_fixup = qtutils.add_action(
431 self,
432 N_('Fixup'),
433 lambda: self.set_selected_to(FIXUP),
434 *hotkeys.REBASE_FIXUP,
437 self.action_squash = qtutils.add_action(
438 self,
439 N_('Squash'),
440 lambda: self.set_selected_to(SQUASH),
441 *hotkeys.REBASE_SQUASH,
444 self.action_shift_down = qtutils.add_action(
445 self, N_('Shift Down'), self.shift_down, hotkeys.MOVE_DOWN_TERTIARY
448 self.action_shift_up = qtutils.add_action(
449 self, N_('Shift Up'), self.shift_up, hotkeys.MOVE_UP_TERTIARY
452 self.toggle_remark_actions = tuple(
453 qtutils.add_action(
454 self,
456 lambda remark=r: self.toggle_remark(remark),
457 hotkeys.hotkey(Qt.CTRL | getattr(Qt, 'Key_' + r)),
459 for r in map(str, range(10))
462 # pylint: disable=no-member
463 self.itemChanged.connect(self.item_changed)
464 self.itemSelectionChanged.connect(self.selection_changed)
465 self.move_rows.connect(self.move)
466 self.items_moved.connect(self.decorate)
468 def add_item(
469 self, idx, enabled, command, oid='', summary='', cmdexec='', branch=''
471 comment_char = self.comment_char
472 item = RebaseTreeWidgetItem(
473 idx,
474 enabled,
475 command,
476 oid=oid,
477 summary=summary,
478 cmdexec=cmdexec,
479 branch=branch,
480 comment_char=comment_char,
482 self.invisibleRootItem().addChild(item)
484 def decorate(self, items):
485 for item in items:
486 item.decorate(self)
488 def refit(self):
489 self.resizeColumnToContents(0)
490 self.resizeColumnToContents(1)
491 self.resizeColumnToContents(2)
492 self.resizeColumnToContents(3)
493 self.resizeColumnToContents(4)
494 self.resizeColumnToContents(5)
496 # actions
497 def item_changed(self, item, column):
498 if column == item.ENABLED_COLUMN:
499 self.validate()
501 def validate(self):
502 invalid_first_choice = {FIXUP, SQUASH}
503 for item in self.items():
504 if item.is_enabled() and item.is_commit():
505 if item.command in invalid_first_choice:
506 item.reset_command(PICK)
507 break
509 def set_selected_to(self, command):
510 for i in self.selected_items():
511 i.reset_command(command)
512 self.validate()
514 def set_command(self, item, command):
515 item.reset_command(command)
516 self.validate()
518 def copy_oid(self):
519 item = self.selected_item()
520 if item is None:
521 return
522 clipboard = item.oid or item.cmdexec
523 qtutils.set_clipboard(clipboard)
525 def selection_changed(self):
526 item = self.selected_item()
527 if item is None or not item.is_commit():
528 return
529 context = self.context
530 oid = item.oid
531 params = dag.DAG(oid, 2)
532 repo = dag.RepoReader(context, params)
533 commits = []
534 for commit in repo.get():
535 commits.append(commit)
536 if commits:
537 commits = commits[-1:]
538 self.commits_selected.emit(commits)
540 def toggle_enabled(self):
541 items = self.selected_items()
542 logic_or = reduce(lambda res, item: res or item.is_enabled(), items, False)
543 for item in items:
544 item.set_enabled(not logic_or)
546 def select_first(self):
547 items = self.items()
548 if not items:
549 return
550 idx = self.model().index(0, 0)
551 if idx.isValid():
552 self.setCurrentIndex(idx)
554 def shift_down(self):
555 sel_items = self.selected_items()
556 all_items = self.items()
557 sel_idx = sorted([all_items.index(item) for item in sel_items])
558 if not sel_idx:
559 return
560 idx = sel_idx[0] + 1
561 if not (
562 idx > len(all_items) - len(sel_items)
563 or all_items[sel_idx[-1]] is all_items[-1]
565 self.move_rows.emit(sel_idx, idx)
567 def shift_up(self):
568 sel_items = self.selected_items()
569 all_items = self.items()
570 sel_idx = sorted([all_items.index(item) for item in sel_items])
571 if not sel_idx:
572 return
573 idx = sel_idx[0] - 1
574 if idx >= 0:
575 self.move_rows.emit(sel_idx, idx)
577 def toggle_remark(self, remark):
578 items = self.selected_items()
579 self.toggle_remark_of_items(remark, items)
581 def toggle_remark_of_items(self, remark, items):
582 logic_or = reduce(lambda res, item: res or remark in item.remarks, items, False)
583 if logic_or:
584 for item in items:
585 item.remove_remark(remark)
586 else:
587 for item in items:
588 item.add_remark(remark)
590 def move(self, src_idxs, dst_idx):
591 moved_items = []
592 src_base = sorted(src_idxs)[0]
593 for idx in reversed(sorted(src_idxs)):
594 item = self.invisibleRootItem().takeChild(idx)
595 moved_items.insert(0, [dst_idx + (idx - src_base), item])
597 for item in moved_items:
598 self.invisibleRootItem().insertChild(item[0], item[1])
599 self.setCurrentItem(item[1])
601 if moved_items:
602 moved_items = [item[1] for item in moved_items]
603 # If we've moved to the top then we need to re-decorate all items.
604 # Otherwise, we can decorate just the new items.
605 if dst_idx == 0:
606 self.decorate(self.items())
607 else:
608 self.decorate(moved_items)
610 for item in moved_items:
611 item.setSelected(True)
612 self.validate()
614 # Qt events
616 def dropEvent(self, event):
617 super().dropEvent(event)
618 self.validate()
620 def contextMenuEvent(self, event):
621 items = self.selected_items()
622 menu = qtutils.create_menu(N_('Actions'), self)
623 menu.addAction(self.action_pick)
624 menu.addAction(self.action_reword)
625 menu.addAction(self.action_edit)
626 menu.addAction(self.action_fixup)
627 menu.addAction(self.action_squash)
628 menu.addSeparator()
629 menu.addAction(self.toggle_enabled_action)
630 menu.addSeparator()
631 menu.addAction(self.copy_oid_action)
632 self.copy_oid_action.setDisabled(len(items) > 1)
633 menu.addAction(self.external_diff_action)
634 self.external_diff_action.setDisabled(len(items) > 1)
635 menu.addSeparator()
636 menu_toggle_remark = menu.addMenu(N_('Toggle remark'))
637 tuple(map(menu_toggle_remark.addAction, self.toggle_remark_actions))
638 menu.exec_(self.mapToGlobal(event.pos()))
641 class ComboBox(QtWidgets.QComboBox):
642 validate = Signal()
645 class RebaseTreeWidgetItem(QtWidgets.QTreeWidgetItem):
646 ENABLED_COLUMN = 1
647 COMMAND_COLUMN = 2
648 OID_LENGTH = 7
650 def __init__(
651 self,
652 idx,
653 enabled,
654 command,
655 oid='',
656 summary='',
657 cmdexec='',
658 branch='',
659 comment_char='#',
660 remarks=tuple(),
661 parent=None,
663 QtWidgets.QTreeWidgetItem.__init__(self, parent)
664 self.combo = None
665 self.command = command
666 self.idx = idx
667 self.oid = oid
668 self.summary = summary
669 self.cmdexec = cmdexec
670 self.branch = branch
671 self.comment_char = comment_char
673 # if core.abbrev is set to a higher value then we will notice by
674 # simply tracking the longest oid we've seen
675 oid_len = self.__class__.OID_LENGTH
676 self.__class__.OID_LENGTH = max(len(oid), oid_len)
678 self.setText(0, '%02d' % idx)
679 self.set_enabled(enabled)
680 # checkbox on 1
681 # combo box on 2
682 if self.is_exec():
683 self.setText(3, '')
684 self.setText(5, cmdexec)
685 elif self.is_update_ref():
686 self.setText(3, '')
687 self.setText(5, branch)
688 else:
689 self.setText(3, oid)
690 self.setText(5, summary)
692 self.set_remarks(remarks)
694 flags = self.flags() | Qt.ItemIsUserCheckable
695 flags = flags | Qt.ItemIsDragEnabled
696 flags = flags & ~Qt.ItemIsDropEnabled
697 self.setFlags(flags)
699 def __eq__(self, other):
700 return self is other
702 def __hash__(self):
703 return self.oid
705 def copy(self):
706 return self.__class__(
707 self.idx,
708 self.is_enabled(),
709 self.command,
710 oid=self.oid,
711 summary=self.summary,
712 cmdexec=self.cmdexec,
713 branch=self.branch,
714 comment_char=self.comment_char,
715 remarks=self.remarks,
718 def decorate(self, parent):
719 if self.is_exec():
720 items = [EXEC]
721 idx = 0
722 elif self.is_update_ref():
723 items = [UPDATE_REF]
724 idx = 0
725 else:
726 items = COMMANDS
727 idx = COMMAND_IDX[self.command]
728 combo = self.combo = ComboBox()
729 combo.setEditable(False)
730 combo.addItems(items)
731 combo.setCurrentIndex(idx)
732 combo.setEnabled(self.is_commit())
734 signal = combo.currentIndexChanged
735 # pylint: disable=no-member
736 signal.connect(lambda x: self.set_command_and_validate(combo))
737 combo.validate.connect(parent.validate)
739 parent.setItemWidget(self, self.COMMAND_COLUMN, combo)
741 def is_exec(self):
742 return self.command == EXEC
744 def is_update_ref(self):
745 return self.command == UPDATE_REF
747 def is_commit(self):
748 return bool(
749 not (self.is_exec() or self.is_update_ref()) and self.oid and self.summary
752 def value(self):
753 """Return the serialized representation of an item"""
754 if self.is_enabled():
755 comment = ''
756 else:
757 comment = self.comment_char + ' '
758 if self.is_exec():
759 return f'{comment}{self.command} {self.cmdexec}'
760 if self.is_update_ref():
761 return f'{comment}{self.command} {self.branch}'
762 return f'{comment}{self.command} {self.oid} {self.summary}'
764 def is_enabled(self):
765 return self.checkState(self.ENABLED_COLUMN) == Qt.Checked
767 def set_enabled(self, enabled):
768 self.setCheckState(self.ENABLED_COLUMN, enabled and Qt.Checked or Qt.Unchecked)
770 def toggle_enabled(self):
771 self.set_enabled(not self.is_enabled())
773 def add_remark(self, remark):
774 self.set_remarks(tuple(sorted(set(self.remarks + (remark,)))))
776 def remove_remark(self, remark):
777 self.set_remarks(tuple(r for r in self.remarks if r != remark))
779 def set_remarks(self, remarks):
780 self.remarks = remarks
781 self.setText(4, ''.join(remarks))
783 def set_command(self, command):
784 """Set the item to a different command, no-op for exec items"""
785 if self.is_exec():
786 return
787 self.command = command
789 def refresh(self):
790 """Update the view to match the updated state"""
791 if self.is_commit():
792 command = self.command
793 self.combo.setCurrentIndex(COMMAND_IDX[command])
795 def reset_command(self, command):
796 """Set and refresh the item in one shot"""
797 self.set_command(command)
798 self.refresh()
800 def set_command_and_validate(self, combo):
801 command = COMMANDS[combo.currentIndex()]
802 self.set_command(command)
803 self.combo.validate.emit()
806 def show_help(context):
807 help_text = N_(
809 Commands
810 --------
811 pick = use commit
812 reword = use commit, but edit the commit message
813 edit = use commit, but stop for amending
814 squash = use commit, but meld into previous commit
815 fixup = like "squash", but discard this commit's log message
816 exec = run command (the rest of the line) using shell
817 update-ref = update branches that point to commits
819 These lines can be re-ordered; they are executed from top to bottom.
821 If you disable a line here THAT COMMIT WILL BE LOST.
823 However, if you disable everything, the rebase will be aborted.
825 Keyboard Shortcuts
826 ------------------
827 ? = show help
828 j = move down
829 k = move up
830 J = shift row down
831 K = shift row up
833 1, p = pick
834 2, r = reword
835 3, e = edit
836 4, f = fixup
837 5, s = squash
838 spacebar = toggle enabled
840 ctrl+enter = accept changes and rebase
841 ctrl+q = cancel and abort the rebase
842 ctrl+d = launch difftool
845 title = N_('Help - git-cola-sequence-editor')
846 return text.text_dialog(context, help_text, title)