tree-wide: remove pylint cruft
[git-cola.git] / cola / widgets / remote.py
blob55527f0bf06f572be2d24a761bf8fb3210f4480c
1 """Widgets for Fetch, Push, and Pull"""
2 import fnmatch
4 from qtpy import QtGui
5 from qtpy import QtWidgets
6 from qtpy.QtCore import Qt
8 from ..i18n import N_
9 from ..interaction import Interaction
10 from ..qtutils import connect_button
11 from ..qtutils import get
12 from .. import gitcmds
13 from .. import icons
14 from .. import qtutils
15 from .. import utils
16 from . import defs
17 from . import log
18 from . import standard
21 FETCH = 'FETCH'
22 PUSH = 'PUSH'
23 PULL = 'PULL'
26 def fetch(context):
27 """Fetch from remote repositories"""
28 return run(context, Fetch)
31 def push(context):
32 """Push to remote repositories"""
33 return run(context, Push)
36 def pull(context):
37 """Pull from remote repositories"""
38 return run(context, Pull)
41 def run(context, RemoteDialog):
42 """Launches fetch/push/pull dialogs."""
43 # Copy global stuff over to speedup startup
44 parent = qtutils.active_window()
45 view = RemoteDialog(context, parent=parent)
46 view.show()
47 return view
50 def combine(result, prev):
51 """Combine multiple (status, out, err) tuples into a combined tuple
53 The current state is passed in via `prev`.
54 The status code is a max() over all the subprocess status codes.
55 Individual (out, err) strings are sequentially concatenated together.
57 """
58 if isinstance(prev, (tuple, list)):
59 if len(prev) != 3:
60 raise AssertionError('combine() with length %d' % len(prev))
61 combined = (
62 max(prev[0], result[0]),
63 combine(prev[1], result[1]),
64 combine(prev[2], result[2]),
66 elif prev and result:
67 combined = prev + '\n\n' + result
68 elif prev:
69 combined = prev
70 else:
71 combined = result
73 return combined
76 def uncheck(value, *checkboxes):
77 """Uncheck the specified checkboxes if value is True"""
78 if value:
79 for checkbox in checkboxes:
80 checkbox.setChecked(False)
83 def strip_remotes(remote_branches):
84 """Strip the <remote>/ prefixes from branches
86 e.g. "origin/main" becomes "main".
88 """
89 branches = [utils.strip_one(branch) for branch in remote_branches]
90 return [branch for branch in branches if branch != 'HEAD']
93 def get_default_remote(context):
94 """Get the name of the default remote to use for pushing.
96 This will be the remote the branch is set to track, if it is set. If it
97 is not, remote.pushDefault will be used (or origin if not set)
99 """
100 upstream_remote = gitcmds.upstream_remote(context)
101 return upstream_remote or context.cfg.get('remote.pushDefault', default='origin')
104 class ActionTask(qtutils.Task):
105 """Run actions asynchronously"""
107 def __init__(self, model_action, remote, kwargs):
108 qtutils.Task.__init__(self)
109 self.model_action = model_action
110 self.remote = remote
111 self.kwargs = kwargs
113 def task(self):
114 """Runs the model action and captures the result"""
115 return self.model_action(self.remote, **self.kwargs)
118 class RemoteActionDialog(standard.Dialog):
119 """Interface for performing remote operations"""
121 def __init__(self, context, action, title, parent=None, icon=None):
122 """Customize the dialog based on the remote action"""
123 standard.Dialog.__init__(self, parent=parent)
124 self.setWindowTitle(title)
125 if parent is not None:
126 self.setWindowModality(Qt.WindowModal)
128 self.context = context
129 self.model = model = context.model
130 self.action = action
131 self.filtered_remote_branches = []
132 self.selected_remotes = []
134 self.runtask = qtutils.RunTask(parent=self)
135 self.local_label = QtWidgets.QLabel()
136 self.local_label.setText(N_('Local Branch'))
138 self.local_branch = QtWidgets.QLineEdit()
139 qtutils.add_completer(self.local_branch, model.local_branches)
141 self.local_branches = QtWidgets.QListWidget()
142 self.local_branches.addItems(model.local_branches)
144 self.remote_label = QtWidgets.QLabel()
145 self.remote_label.setText(N_('Remote'))
147 self.remote_name = QtWidgets.QLineEdit()
148 qtutils.add_completer(self.remote_name, model.remotes)
150 self.remote_name.editingFinished.connect(self.remote_name_edited)
151 self.remote_name.textEdited.connect(lambda x: self.remote_name_edited())
153 self.remotes = QtWidgets.QListWidget()
154 if action == PUSH:
155 mode = QtWidgets.QAbstractItemView.ExtendedSelection
156 self.remotes.setSelectionMode(mode)
157 self.remotes.addItems(model.remotes)
159 self.remote_branch_label = QtWidgets.QLabel()
160 self.remote_branch_label.setText(N_('Remote Branch'))
162 self.remote_branch = QtWidgets.QLineEdit()
163 remote_branches = strip_remotes(model.remote_branches)
164 qtutils.add_completer(self.remote_branch, remote_branches)
166 self.remote_branches = QtWidgets.QListWidget()
167 self.remote_branches.addItems(model.remote_branches)
169 text = N_('Prompt on creation')
170 tooltip = N_('Prompt when pushing creates new remote branches')
171 self.prompt_checkbox = qtutils.checkbox(
172 checked=True, text=text, tooltip=tooltip
175 text = N_('Show remote messages')
176 tooltip = N_('Display remote messages in a separate dialog')
177 self.remote_messages_checkbox = qtutils.checkbox(
178 checked=False, text=text, tooltip=tooltip
181 text = N_('Fast-forward only')
182 tooltip = N_(
183 'Refuse to merge unless the current HEAD is already up-'
184 'to-date or the merge can be resolved as a fast-forward'
186 self.ff_only_checkbox = qtutils.checkbox(
187 checked=True, text=text, tooltip=tooltip
190 text = N_('No fast-forward')
191 tooltip = N_(
192 'Create a merge commit even when the merge resolves as a fast-forward'
194 self.no_ff_checkbox = qtutils.checkbox(
195 checked=False, text=text, tooltip=tooltip
197 text = N_('Force')
198 tooltip = N_(
199 'Allow non-fast-forward updates. Using "force" can '
200 'cause the remote repository to lose commits; '
201 'use it with care'
203 self.force_checkbox = qtutils.checkbox(
204 checked=False, text=text, tooltip=tooltip
207 self.tags_checkbox = qtutils.checkbox(text=N_('Include tags '))
209 tooltip = N_(
210 'Remove remote-tracking branches that no longer exist on the remote'
212 self.prune_checkbox = qtutils.checkbox(text=N_('Prune '), tooltip=tooltip)
214 tooltip = N_('Rebase the current branch instead of merging')
215 self.rebase_checkbox = qtutils.checkbox(text=N_('Rebase'), tooltip=tooltip)
217 text = N_('Set upstream')
218 tooltip = N_('Configure the remote branch as the the new upstream')
219 self.upstream_checkbox = qtutils.checkbox(text=text, tooltip=tooltip)
221 text = N_('Close on completion')
222 tooltip = N_('Close dialog when completed')
223 self.close_on_completion_checkbox = qtutils.checkbox(
224 checked=True, text=text, tooltip=tooltip
227 self.action_button = qtutils.ok_button(title, icon=icon)
228 self.close_button = qtutils.close_button()
229 self.buttons_group = utils.Group(self.close_button, self.action_button)
230 self.inputs_group = utils.Group(
231 self.close_on_completion_checkbox,
232 self.force_checkbox,
233 self.ff_only_checkbox,
234 self.local_branch,
235 self.local_branches,
236 self.tags_checkbox,
237 self.prune_checkbox,
238 self.rebase_checkbox,
239 self.remote_name,
240 self.remotes,
241 self.remote_branch,
242 self.remote_branches,
243 self.upstream_checkbox,
244 self.prompt_checkbox,
245 self.remote_messages_checkbox,
247 self.progress = standard.progress_bar(
248 self,
249 disable=(self.buttons_group, self.inputs_group),
252 self.local_branch_layout = qtutils.hbox(
253 defs.small_margin, defs.spacing, self.local_label, self.local_branch
256 self.remote_layout = qtutils.hbox(
257 defs.small_margin, defs.spacing, self.remote_label, self.remote_name
260 self.remote_branch_layout = qtutils.hbox(
261 defs.small_margin,
262 defs.spacing,
263 self.remote_branch_label,
264 self.remote_branch,
267 self.options_layout = qtutils.hbox(
268 defs.no_margin,
269 defs.button_spacing,
270 self.force_checkbox,
271 self.ff_only_checkbox,
272 self.no_ff_checkbox,
273 self.tags_checkbox,
274 self.prune_checkbox,
275 self.rebase_checkbox,
276 self.upstream_checkbox,
277 self.prompt_checkbox,
278 self.close_on_completion_checkbox,
279 self.remote_messages_checkbox,
280 qtutils.STRETCH,
281 self.progress,
282 self.close_button,
283 self.action_button,
286 self.remote_input_layout = qtutils.vbox(
287 defs.no_margin, defs.spacing, self.remote_layout, self.remotes
290 self.local_branch_input_layout = qtutils.vbox(
291 defs.no_margin, defs.spacing, self.local_branch_layout, self.local_branches
294 self.remote_branch_input_layout = qtutils.vbox(
295 defs.no_margin,
296 defs.spacing,
297 self.remote_branch_layout,
298 self.remote_branches,
301 if action == PUSH:
302 widgets = (
303 self.remote_input_layout,
304 self.local_branch_input_layout,
305 self.remote_branch_input_layout,
307 else: # fetch and pull
308 widgets = (
309 self.remote_input_layout,
310 self.remote_branch_input_layout,
311 self.local_branch_input_layout,
313 self.top_layout = qtutils.hbox(defs.no_margin, defs.spacing, *widgets)
315 self.main_layout = qtutils.vbox(
316 defs.margin, defs.spacing, self.top_layout, self.options_layout
318 self.setLayout(self.main_layout)
320 default_remote = get_default_remote(context)
322 remotes = model.remotes
323 if default_remote in remotes:
324 idx = remotes.index(default_remote)
325 if self.select_remote(idx):
326 self.set_remote_name(default_remote)
327 else:
328 if self.select_first_remote():
329 self.set_remote_name(remotes[0])
331 # Trim the remote list to just the default remote
332 self.update_remotes()
333 self.set_field_defaults()
335 # Setup signals and slots
336 self.remotes.itemSelectionChanged.connect(self.update_remotes)
338 local = self.local_branches
339 local.itemSelectionChanged.connect(self.update_local_branches)
341 remote = self.remote_branches
342 remote.itemSelectionChanged.connect(self.update_remote_branches)
344 self.no_ff_checkbox.toggled.connect(
345 lambda x: uncheck(x, self.ff_only_checkbox, self.rebase_checkbox)
348 self.ff_only_checkbox.toggled.connect(
349 lambda x: uncheck(x, self.no_ff_checkbox, self.rebase_checkbox)
352 self.rebase_checkbox.toggled.connect(
353 lambda x: uncheck(x, self.no_ff_checkbox, self.ff_only_checkbox)
356 connect_button(self.action_button, self.action_callback)
357 connect_button(self.close_button, self.close)
359 qtutils.add_action(
360 self, N_('Close'), self.close, QtGui.QKeySequence.Close, 'Esc'
362 if action != FETCH:
363 self.prune_checkbox.hide()
365 if action != PUSH:
366 # Push-only options
367 self.upstream_checkbox.hide()
368 self.prompt_checkbox.hide()
370 if action == PULL:
371 # Fetch and Push-only options
372 self.force_checkbox.hide()
373 self.tags_checkbox.hide()
374 self.local_label.hide()
375 self.local_branch.hide()
376 self.local_branches.hide()
377 else:
378 # Pull-only options
379 self.rebase_checkbox.hide()
380 self.no_ff_checkbox.hide()
381 self.ff_only_checkbox.hide()
383 self.init_size(parent=parent)
385 def set_rebase(self, value):
386 """Check the rebase checkbox"""
387 self.rebase_checkbox.setChecked(value)
389 def set_field_defaults(self):
390 """Set sensible initial defaults"""
391 # Default to "git fetch origin main"
392 action = self.action
393 if action in (FETCH, PULL):
394 self.local_branch.setText('')
395 self.remote_branch.setText('')
396 return
398 # Select the current branch by default for push
399 if action == PUSH:
400 branch = self.model.currentbranch
401 try:
402 idx = self.model.local_branches.index(branch)
403 except ValueError:
404 return
405 if self.select_local_branch(idx):
406 self.set_local_branch(branch)
407 self.set_remote_branch('')
409 def set_remote_name(self, remote_name):
410 """Set the remote name"""
411 self.remote_name.setText(remote_name)
413 def set_local_branch(self, branch):
414 """Set the local branch name"""
415 self.local_branch.setText(branch)
416 if branch:
417 self.local_branch.selectAll()
419 def set_remote_branch(self, branch):
420 """Set the remote branch name"""
421 self.remote_branch.setText(branch)
422 if branch:
423 self.remote_branch.selectAll()
425 def set_remote_branches(self, branches):
426 """Set the list of remote branches"""
427 self.remote_branches.clear()
428 self.remote_branches.addItems(branches)
429 self.filtered_remote_branches = branches
430 qtutils.add_completer(self.remote_branch, strip_remotes(branches))
432 def select_first_remote(self):
433 """Select the first remote in the list view"""
434 return self.select_remote(0)
436 def select_remote(self, idx):
437 """Select a remote by index"""
438 item = self.remotes.item(idx)
439 if item:
440 item.setSelected(True)
441 self.remotes.setCurrentItem(item)
442 self.set_remote_name(item.text())
443 result = True
444 else:
445 result = False
446 return result
448 def select_remote_by_name(self, remote):
449 """Select a remote by name"""
450 remotes = self.model.remotes
451 if remote in remotes:
452 idx = remotes.index(remote)
453 result = self.select_remote(idx)
454 else:
455 result = False
456 return result
458 def set_selected_remotes(self, remotes):
459 """Set the list of selected remotes
461 Return True if all remotes were found and selected.
464 # Invalid remote names are ignored.
465 # This handles a remote going away between sessions.
466 # The selection is unchanged when none of the specified remotes exist.
467 found = False
468 for remote in remotes:
469 if remote in self.model.remotes:
470 found = True
471 break
472 if found:
473 # Only clear the selection if the specified remotes exist
474 self.remotes.clearSelection()
475 found = all(self.select_remote_by_name(x) for x in remotes)
476 return found
478 def select_local_branch(self, idx):
479 """Selects a local branch by index in the list view"""
480 item = self.local_branches.item(idx)
481 if item:
482 item.setSelected(True)
483 self.local_branches.setCurrentItem(item)
484 self.local_branch.setText(item.text())
485 result = True
486 else:
487 result = False
488 return result
490 def display_remotes(self, widget):
491 """Display the available remotes in a listwidget"""
492 displayed = []
493 for remote_name in self.model.remotes:
494 url = self.model.remote_url(remote_name, self.action)
495 display = '{}\t({})'.format(remote_name, N_('URL: %s') % url)
496 displayed.append(display)
497 qtutils.set_items(widget, displayed)
499 def update_remotes(self):
500 """Update the remote name when a remote from the list is selected"""
501 widget = self.remotes
502 remotes = self.model.remotes
503 selection = qtutils.selected_item(widget, remotes)
504 if not selection:
505 self.selected_remotes = []
506 return
507 self.set_remote_name(selection)
508 self.selected_remotes = qtutils.selected_items(self.remotes, self.model.remotes)
509 self.set_remote_to(selection, self.selected_remotes)
511 def set_remote_to(self, _remote, selected_remotes):
512 context = self.context
513 all_branches = gitcmds.branch_list(context, remote=True)
514 branches = []
515 patterns = []
516 for remote_name in selected_remotes:
517 patterns.append(remote_name + '/*')
519 for branch in all_branches:
520 for pat in patterns:
521 if fnmatch.fnmatch(branch, pat):
522 branches.append(branch)
523 break
524 if branches:
525 self.set_remote_branches(branches)
526 else:
527 self.set_remote_branches(all_branches)
528 self.set_remote_branch('')
530 def remote_name_edited(self):
531 """Update the current remote when the remote name is typed manually"""
532 remote = self.remote_name.text()
533 self.set_remote_to(remote, [remote])
535 def update_local_branches(self):
536 """Update the local/remote branch names when a branch is selected"""
537 branches = self.model.local_branches
538 widget = self.local_branches
539 selection = qtutils.selected_item(widget, branches)
540 if not selection:
541 return
542 self.set_local_branch(selection)
543 self.set_remote_branch(selection)
545 def update_remote_branches(self):
546 """Update the remote branch name when a branch is selected"""
547 widget = self.remote_branches
548 branches = self.filtered_remote_branches
549 selection = qtutils.selected_item(widget, branches)
550 if not selection:
551 return
552 branch = utils.strip_one(selection)
553 if branch == 'HEAD':
554 return
555 self.set_remote_branch(branch)
557 def common_args(self):
558 """Returns git arguments common to fetch/push/pull"""
559 remote_name = self.remote_name.text()
560 local_branch = self.local_branch.text()
561 remote_branch = self.remote_branch.text()
563 ff_only = get(self.ff_only_checkbox)
564 force = get(self.force_checkbox)
565 no_ff = get(self.no_ff_checkbox)
566 rebase = get(self.rebase_checkbox)
567 set_upstream = get(self.upstream_checkbox)
568 tags = get(self.tags_checkbox)
569 prune = get(self.prune_checkbox)
571 return (
572 remote_name,
574 'ff_only': ff_only,
575 'force': force,
576 'local_branch': local_branch,
577 'no_ff': no_ff,
578 'rebase': rebase,
579 'remote_branch': remote_branch,
580 'set_upstream': set_upstream,
581 'tags': tags,
582 'prune': prune,
586 # Actions
588 def push_to_all(self, _remote, *args, **kwargs):
589 """Push to all selected remotes"""
590 selected_remotes = self.selected_remotes
591 all_results = None
592 for remote in selected_remotes:
593 result = self.model.push(remote, *args, **kwargs)
594 all_results = combine(result, all_results)
595 return all_results
597 def action_callback(self):
598 """Perform the actual fetch/push/pull operation"""
599 action = self.action
600 remote_messages = get(self.remote_messages_checkbox)
601 if action == FETCH:
602 model_action = self.model.fetch
603 elif action == PUSH:
604 model_action = self.push_to_all
605 else: # if action == PULL:
606 model_action = self.model.pull
608 remote_name = self.remote_name.text()
609 if not remote_name:
610 errmsg = N_('No repository selected.')
611 Interaction.log(errmsg)
612 return
613 remote, kwargs = self.common_args()
614 self.selected_remotes = qtutils.selected_items(self.remotes, self.model.remotes)
616 # Check if we're about to create a new branch and warn.
617 remote_branch = self.remote_branch.text()
618 local_branch = self.local_branch.text()
620 if action == PUSH and not remote_branch:
621 branch = local_branch
622 candidate = f'{remote}/{branch}'
623 prompt = get(self.prompt_checkbox)
625 if prompt and candidate not in self.model.remote_branches:
626 title = N_('Push')
627 args = {
628 'branch': branch,
629 'remote': remote,
631 msg = (
633 'Branch "%(branch)s" does not exist in "%(remote)s".\n'
634 'A new remote branch will be published.'
636 % args
638 info_txt = N_('Create a new remote branch?')
639 ok_text = N_('Create Remote Branch')
640 if not Interaction.confirm(
641 title, msg, info_txt, ok_text, icon=icons.cola()
643 return
645 if get(self.force_checkbox):
646 if action == FETCH:
647 title = N_('Force Fetch?')
648 msg = N_('Non-fast-forward fetch overwrites local history!')
649 info_txt = N_('Force fetching from %s?') % remote
650 ok_text = N_('Force Fetch')
651 elif action == PUSH:
652 title = N_('Force Push?')
653 msg = N_(
654 'Non-fast-forward push overwrites published '
655 'history!\n(Did you pull first?)'
657 info_txt = N_('Force push to %s?') % remote
658 ok_text = N_('Force Push')
659 else: # pull: shouldn't happen since the controls are hidden
660 return
661 if not Interaction.confirm(
662 title, msg, info_txt, ok_text, default=False, icon=icons.discard()
664 return
666 self.progress.setMaximumHeight(
667 self.action_button.height() - defs.small_margin * 2
670 # Use a thread to update in the background
671 task = ActionTask(model_action, remote, kwargs)
672 if remote_messages:
673 result = log.show_remote_messages(self, self.context)
674 else:
675 result = None
676 self.runtask.start(
677 task,
678 progress=self.progress,
679 finish=self.action_completed,
680 result=result,
683 def action_completed(self, task):
684 """Grab the results of the action and finish up"""
685 if not task.result or not isinstance(task.result, (list, tuple)):
686 return
688 status, out, err = task.result
689 command = 'git %s' % self.action.lower()
690 message = Interaction.format_command_status(command, status)
691 details = Interaction.format_out_err(out, err)
693 log_message = message
694 if details:
695 log_message += '\n\n' + details
696 Interaction.log(log_message)
698 if status == 0:
699 close_on_completion = get(self.close_on_completion_checkbox)
700 if close_on_completion:
701 self.accept()
702 return
704 if self.action == PUSH:
705 message += '\n\n'
706 message += N_('Have you rebased/pulled lately?')
708 Interaction.critical(self.windowTitle(), message=message, details=details)
710 def export_state(self):
711 """Export persistent settings"""
712 state = standard.Dialog.export_state(self)
713 state['close_on_completion'] = get(self.close_on_completion_checkbox)
714 state['remote_messages'] = get(self.remote_messages_checkbox)
715 return state
717 def apply_state(self, state):
718 """Apply persistent settings"""
719 result = standard.Dialog.apply_state(self, state)
720 # Restore the "close on completion" checkbox
721 close_on_completion = bool(state.get('close_on_completion', True))
722 self.close_on_completion_checkbox.setChecked(close_on_completion)
723 # Restore the "show remote messages" checkbox
724 remote_messages = bool(state.get('remote_messages', False))
725 self.remote_messages_checkbox.setChecked(remote_messages)
726 return result
729 # Use distinct classes so that each saves its own set of preferences
730 class Fetch(RemoteActionDialog):
731 """Fetch from remote repositories"""
733 def __init__(self, context, parent=None):
734 super().__init__(context, FETCH, N_('Fetch'), parent=parent, icon=icons.repo())
736 def export_state(self):
737 """Export persistent settings"""
738 state = RemoteActionDialog.export_state(self)
739 state['tags'] = get(self.tags_checkbox)
740 state['prune'] = get(self.prune_checkbox)
741 return state
743 def apply_state(self, state):
744 """Apply persistent settings"""
745 result = RemoteActionDialog.apply_state(self, state)
746 tags = bool(state.get('tags', False))
747 self.tags_checkbox.setChecked(tags)
748 prune = bool(state.get('prune', False))
749 self.prune_checkbox.setChecked(prune)
750 return result
753 class Push(RemoteActionDialog):
754 """Push to remote repositories"""
756 def __init__(self, context, parent=None):
757 super().__init__(context, PUSH, N_('Push'), parent=parent, icon=icons.push())
759 def export_state(self):
760 """Export persistent settings"""
761 state = RemoteActionDialog.export_state(self)
762 state['prompt'] = get(self.prompt_checkbox)
763 state['tags'] = get(self.tags_checkbox)
764 return state
766 def apply_state(self, state):
767 """Apply persistent settings"""
768 result = RemoteActionDialog.apply_state(self, state)
769 # Restore the "prompt on creation" checkbox
770 prompt = bool(state.get('prompt', True))
771 self.prompt_checkbox.setChecked(prompt)
772 # Restore the "tags" checkbox
773 tags = bool(state.get('tags', False))
774 self.tags_checkbox.setChecked(tags)
775 return result
778 class Pull(RemoteActionDialog):
779 """Pull from remote repositories"""
781 def __init__(self, context, parent=None):
782 super().__init__(context, PULL, N_('Pull'), parent=parent, icon=icons.pull())
784 def apply_state(self, state):
785 """Apply persistent settings"""
786 result = RemoteActionDialog.apply_state(self, state)
787 # Rebase has the highest priority
788 rebase = bool(state.get('rebase', False))
789 self.rebase_checkbox.setChecked(rebase)
791 ff_only = not rebase and bool(state.get('ff_only', False))
792 no_ff = not rebase and not ff_only and bool(state.get('no_ff', False))
793 self.no_ff_checkbox.setChecked(no_ff)
794 # Allow users coming from older versions that have rebase=False to
795 # pickup the new ff_only=True default by only setting ff_only False
796 # when it either exists in the config or when rebase=True.
797 if 'ff_only' in state or rebase:
798 self.ff_only_checkbox.setChecked(ff_only)
799 return result
801 def export_state(self):
802 """Export persistent settings"""
803 state = RemoteActionDialog.export_state(self)
804 state['ff_only'] = get(self.ff_only_checkbox)
805 state['no_ff'] = get(self.no_ff_checkbox)
806 state['rebase'] = get(self.rebase_checkbox)
807 return state