push: prefer the local branch when selecting a corresponding remote branch
[git-cola.git] / cola / widgets / remote.py
blob5a2151d4cdd0dd1485eb1cb0dfd1770650d172dd
1 """Widgets for Fetch, Push, and Pull"""
2 import fnmatch
3 import time
4 import os
6 from qtpy import QtGui
7 from qtpy import QtWidgets
8 from qtpy.QtCore import Qt
10 from ..i18n import N_
11 from ..interaction import Interaction
12 from ..models import main
13 from ..models.main import FETCH, FETCH_HEAD, PULL, PUSH
14 from ..qtutils import connect_button
15 from ..qtutils import get
16 from .. import core
17 from .. import git
18 from .. import gitcmds
19 from .. import icons
20 from .. import qtutils
21 from .. import utils
22 from . import defs
23 from . import log
24 from . import standard
27 def fetch(context):
28 """Fetch from remote repositories"""
29 return run(context, Fetch)
32 def push(context):
33 """Push to remote repositories"""
34 return run(context, Push)
37 def pull(context):
38 """Pull from remote repositories"""
39 return run(context, Pull)
42 def run(context, RemoteDialog):
43 """Launches fetch/push/pull dialogs."""
44 # Copy global stuff over to speedup startup
45 parent = qtutils.active_window()
46 view = RemoteDialog(context, parent=parent)
47 view.show()
48 return view
51 def combine(result, prev):
52 """Combine multiple (status, out, err) tuples into a combined tuple
54 The current state is passed in via `prev`.
55 The status code is a max() over all the subprocess status codes.
56 Individual (out, err) strings are sequentially concatenated together.
58 """
59 if isinstance(prev, (tuple, list)):
60 if len(prev) != 3:
61 raise AssertionError('combine() with length %d' % len(prev))
62 combined = (
63 max(prev[0], result[0]),
64 combine(prev[1], result[1]),
65 combine(prev[2], result[2]),
67 elif prev and result:
68 combined = prev + '\n\n' + result
69 elif prev:
70 combined = prev
71 else:
72 combined = result
74 return combined
77 def uncheck(value, *checkboxes):
78 """Uncheck the specified checkboxes if value is True"""
79 if value:
80 for checkbox in checkboxes:
81 checkbox.setChecked(False)
84 def strip_remotes(remote_branches):
85 """Strip the <remote>/ prefixes from branches
87 e.g. "origin/main" becomes "main".
89 """
90 branches = [utils.strip_one(branch) for branch in remote_branches]
91 return [branch for branch in branches if branch != 'HEAD']
94 def get_default_remote(context):
95 """Get the name of the default remote to use for pushing.
97 This will be the remote the branch is set to track, if it is set. If it
98 is not, remote.pushDefault will be used (or origin if not set)
101 upstream_remote = gitcmds.upstream_remote(context)
102 return upstream_remote or context.cfg.get('remote.pushDefault', default='origin')
105 class ActionTask(qtutils.Task):
106 """Run actions asynchronously"""
108 def __init__(self, model_action, remote, kwargs):
109 qtutils.Task.__init__(self)
110 self.model_action = model_action
111 self.remote = remote
112 self.kwargs = kwargs
114 def task(self):
115 """Runs the model action and captures the result"""
116 return self.model_action(self.remote, **self.kwargs)
119 class RemoteActionDialog(standard.Dialog):
120 """Interface for performing remote operations"""
122 def __init__(self, context, action, title, parent=None, icon=None):
123 """Customize the dialog based on the remote action"""
124 standard.Dialog.__init__(self, parent=parent)
125 self.setWindowTitle(title)
126 if parent is not None:
127 self.setWindowModality(Qt.WindowModal)
129 self.context = context
130 self.model = model = context.model
131 self.action = action
132 self.filtered_remote_branches = []
133 self.selected_remotes = []
134 self.selected_remotes_by_worktree = {}
135 self.last_updated = 0.0
137 self.runtask = qtutils.RunTask(parent=self)
138 self.local_label = QtWidgets.QLabel()
139 self.local_label.setText(N_('Local Branch'))
141 self.local_branch = QtWidgets.QLineEdit()
142 self.local_branch.textChanged.connect(self.local_branch_text_changed)
143 local_branches = self.get_local_branches()
144 qtutils.add_completer(self.local_branch, local_branches)
146 self.local_branches = QtWidgets.QListWidget()
147 self.local_branches.addItems(local_branches)
149 self.remote_label = QtWidgets.QLabel()
150 self.remote_label.setText(N_('Remote'))
152 self.remote_name = QtWidgets.QLineEdit()
153 qtutils.add_completer(self.remote_name, model.remotes)
155 self.remote_name.editingFinished.connect(self.remote_name_edited)
156 self.remote_name.textEdited.connect(lambda _: self.remote_name_edited())
158 self.remotes = QtWidgets.QListWidget()
159 if action == PUSH:
160 mode = QtWidgets.QAbstractItemView.ExtendedSelection
161 self.remotes.setSelectionMode(mode)
162 self.remotes.addItems(model.remotes)
164 self.remote_branch_label = QtWidgets.QLabel()
165 self.remote_branch_label.setText(N_('Remote Branch'))
167 self.remote_branch = QtWidgets.QLineEdit()
168 self.remote_branch.textChanged.connect(lambda _: self.update_command_display())
169 remote_branches = strip_remotes(model.remote_branches)
170 qtutils.add_completer(self.remote_branch, remote_branches)
172 self.remote_branches = QtWidgets.QListWidget()
173 self.remote_branches.addItems(model.remote_branches)
175 text = N_('Prompt on creation')
176 tooltip = N_('Prompt when pushing creates new remote branches')
177 self.prompt_checkbox = qtutils.checkbox(
178 checked=True, text=text, tooltip=tooltip
181 text = N_('Show remote messages')
182 tooltip = N_('Display remote messages in a separate dialog')
183 self.remote_messages_checkbox = qtutils.checkbox(
184 checked=False, text=text, tooltip=tooltip
187 text = N_('Fast-forward only')
188 tooltip = N_(
189 'Refuse to merge unless the current HEAD is already up-'
190 'to-date or the merge can be resolved as a fast-forward'
192 self.ff_only_checkbox = qtutils.checkbox(
193 checked=True, text=text, tooltip=tooltip
195 self.ff_only_checkbox.toggled.connect(self.update_command_display)
197 text = N_('No fast-forward')
198 tooltip = N_(
199 'Create a merge commit even when the merge resolves as a fast-forward'
201 self.no_ff_checkbox = qtutils.checkbox(
202 checked=False, text=text, tooltip=tooltip
204 self.no_ff_checkbox.toggled.connect(self.update_command_display)
205 text = N_('Force')
206 tooltip = N_(
207 'Allow non-fast-forward updates. Using "force" can '
208 'cause the remote repository to lose commits; '
209 'use it with care'
211 self.force_checkbox = qtutils.checkbox(
212 checked=False, text=text, tooltip=tooltip
214 self.force_checkbox.toggled.connect(self.update_command_display)
216 self.tags_checkbox = qtutils.checkbox(text=N_('Include tags '))
217 self.tags_checkbox.toggled.connect(self.update_command_display)
219 tooltip = N_(
220 'Remove remote-tracking branches that no longer exist on the remote'
222 self.prune_checkbox = qtutils.checkbox(text=N_('Prune '), tooltip=tooltip)
223 self.prune_checkbox.toggled.connect(self.update_command_display)
225 tooltip = N_('Rebase the current branch instead of merging')
226 self.rebase_checkbox = qtutils.checkbox(text=N_('Rebase'), tooltip=tooltip)
227 self.rebase_checkbox.toggled.connect(self.update_command_display)
229 text = N_('Set upstream')
230 tooltip = N_('Configure the remote branch as the the new upstream')
231 self.upstream_checkbox = qtutils.checkbox(text=text, tooltip=tooltip)
232 self.upstream_checkbox.toggled.connect(self.update_command_display)
234 text = N_('Close on completion')
235 tooltip = N_('Close dialog when completed')
236 self.close_on_completion_checkbox = qtutils.checkbox(
237 checked=True, text=text, tooltip=tooltip
240 self.action_button = qtutils.ok_button(title, icon=icon)
241 self.close_button = qtutils.close_button()
242 self.buttons_group = utils.Group(self.close_button, self.action_button)
243 self.inputs_group = utils.Group(
244 self.close_on_completion_checkbox,
245 self.force_checkbox,
246 self.ff_only_checkbox,
247 self.local_branch,
248 self.local_branches,
249 self.tags_checkbox,
250 self.prune_checkbox,
251 self.rebase_checkbox,
252 self.remote_name,
253 self.remotes,
254 self.remote_branch,
255 self.remote_branches,
256 self.upstream_checkbox,
257 self.prompt_checkbox,
258 self.remote_messages_checkbox,
260 self.progress = standard.progress_bar(
261 self,
262 disable=(self.buttons_group, self.inputs_group),
265 self.command_display = log.LogWidget(self.context, display_usage=False)
267 self.local_branch_layout = qtutils.hbox(
268 defs.small_margin, defs.spacing, self.local_label, self.local_branch
271 self.remote_layout = qtutils.hbox(
272 defs.small_margin, defs.spacing, self.remote_label, self.remote_name
275 self.remote_branch_layout = qtutils.hbox(
276 defs.small_margin,
277 defs.spacing,
278 self.remote_branch_label,
279 self.remote_branch,
282 self.options_layout = qtutils.hbox(
283 defs.no_margin,
284 defs.button_spacing,
285 self.force_checkbox,
286 self.ff_only_checkbox,
287 self.no_ff_checkbox,
288 self.tags_checkbox,
289 self.prune_checkbox,
290 self.rebase_checkbox,
291 self.upstream_checkbox,
292 self.prompt_checkbox,
293 self.close_on_completion_checkbox,
294 self.remote_messages_checkbox,
295 qtutils.STRETCH,
296 self.progress,
297 self.close_button,
298 self.action_button,
301 self.remote_input_layout = qtutils.vbox(
302 defs.no_margin, defs.spacing, self.remote_layout, self.remotes
305 self.local_branch_input_layout = qtutils.vbox(
306 defs.no_margin, defs.spacing, self.local_branch_layout, self.local_branches
309 self.remote_branch_input_layout = qtutils.vbox(
310 defs.no_margin,
311 defs.spacing,
312 self.remote_branch_layout,
313 self.remote_branches,
316 if action == PUSH:
317 widgets = (
318 self.remote_input_layout,
319 self.local_branch_input_layout,
320 self.remote_branch_input_layout,
322 else: # fetch and pull
323 widgets = (
324 self.remote_input_layout,
325 self.remote_branch_input_layout,
326 self.local_branch_input_layout,
328 self.top_layout = qtutils.hbox(defs.no_margin, defs.spacing, *widgets)
330 self.main_layout = qtutils.vbox(
331 defs.margin,
332 defs.spacing,
333 self.top_layout,
334 self.command_display,
335 self.options_layout,
337 self.main_layout.setStretchFactor(self.top_layout, 2)
338 self.setLayout(self.main_layout)
340 default_remote = get_default_remote(context)
342 remotes = model.remotes
343 if default_remote in remotes:
344 idx = remotes.index(default_remote)
345 if self.select_remote(idx):
346 self.set_remote_name(default_remote)
347 else:
348 if self.select_first_remote():
349 self.set_remote_name(remotes[0])
351 # Trim the remote list to just the default remote
352 self.update_remotes(update_command_display=False)
354 # Setup signals and slots
355 self.remotes.itemSelectionChanged.connect(self.update_remotes)
357 local = self.local_branches
358 local.itemSelectionChanged.connect(self.update_local_branches)
360 remote = self.remote_branches
361 remote.itemSelectionChanged.connect(self.update_remote_branches)
363 self.no_ff_checkbox.toggled.connect(
364 lambda x: uncheck(x, self.ff_only_checkbox, self.rebase_checkbox)
367 self.ff_only_checkbox.toggled.connect(
368 lambda x: uncheck(x, self.no_ff_checkbox, self.rebase_checkbox)
371 self.rebase_checkbox.toggled.connect(
372 lambda x: uncheck(x, self.no_ff_checkbox, self.ff_only_checkbox)
375 connect_button(self.action_button, self.action_callback)
376 connect_button(self.close_button, self.close)
378 qtutils.add_action(
379 self, N_('Close'), self.close, QtGui.QKeySequence.Close, 'Esc'
381 if action != FETCH:
382 self.prune_checkbox.hide()
384 if action != PUSH:
385 # Push-only options
386 self.upstream_checkbox.hide()
387 self.prompt_checkbox.hide()
389 if action == PULL:
390 # Fetch and Push-only options
391 self.force_checkbox.hide()
392 self.tags_checkbox.hide()
393 self.local_label.hide()
394 self.local_branch.hide()
395 self.local_branches.hide()
396 else:
397 # Pull-only options
398 self.rebase_checkbox.hide()
399 self.no_ff_checkbox.hide()
400 self.ff_only_checkbox.hide()
402 self.init_size(parent=parent)
403 self.set_field_defaults()
405 def set_rebase(self, value):
406 """Check the rebase checkbox"""
407 self.rebase_checkbox.setChecked(value)
409 def set_field_defaults(self):
410 """Set sensible initial defaults"""
411 # Default to "git fetch origin main"
412 action = self.action
413 if action == FETCH:
414 self.set_local_branch('')
415 self.set_remote_branch('')
416 if action == PULL: # Nothing to do when fetching.
417 pass
418 # Select the current branch by default for push
419 if action == PUSH:
420 branch = self.model.currentbranch
421 try:
422 idx = self.model.local_branches.index(branch)
423 except ValueError:
424 return
425 self.select_local_branch(idx)
426 self.set_remote_branch(branch)
428 self.update_command_display()
430 def update_command_display(self):
431 """Display the git commands that will be run"""
432 commands = ['']
433 for remote in self.selected_remotes:
434 cmd = ['git', self.action]
435 _, kwargs = self.common_args()
436 args, kwargs = main.remote_args(self.context, remote, self.action, **kwargs)
437 cmd.extend(git.transform_kwargs(**kwargs))
438 cmd.extend(args)
439 commands.append(core.list2cmdline(cmd))
440 self.command_display.set_output('\n'.join(commands))
442 def local_branch_text_changed(self, value):
443 """Update the remote branch field in response to local branch text edits"""
444 if self.action == PUSH:
445 self.remote_branches.clearSelection()
446 self.set_remote_branch(value)
447 self.update_command_display()
449 def set_remote_name(self, remote_name):
450 """Set the remote name"""
451 self.remote_name.setText(remote_name)
453 def set_local_branch(self, branch):
454 """Set the local branch name"""
455 self.local_branch.setText(branch)
456 if branch:
457 self.local_branch.selectAll()
459 def set_remote_branch(self, branch):
460 """Set the remote branch name"""
461 self.remote_branch.setText(branch)
462 if branch:
463 self.remote_branch.selectAll()
465 def set_remote_branches(self, branches):
466 """Set the list of remote branches"""
467 self.remote_branches.clear()
468 self.remote_branches.addItems(branches)
469 self.filtered_remote_branches = branches
470 qtutils.add_completer(self.remote_branch, strip_remotes(branches))
472 def select_first_remote(self):
473 """Select the first remote in the list view"""
474 return self.select_remote(0)
476 def select_remote(self, idx, make_current=True):
477 """Select a remote by index"""
478 item = self.remotes.item(idx)
479 if item:
480 item.setSelected(True)
481 if make_current:
482 self.remotes.setCurrentItem(item)
483 self.set_remote_name(item.text())
484 result = True
485 else:
486 result = False
487 return result
489 def select_remote_by_name(self, remote, make_current=True):
490 """Select a remote by name"""
491 remotes = self.model.remotes
492 if remote in remotes:
493 idx = remotes.index(remote)
494 result = self.select_remote(idx, make_current=make_current)
495 else:
496 result = False
497 return result
499 def set_selected_remotes(self, remotes):
500 """Set the list of selected remotes
502 Return True if all remotes were found and selected.
505 # Invalid remote names are ignored.
506 # This handles a remote going away between sessions.
507 # The selection is unchanged when none of the specified remotes exist.
508 found = False
509 for remote in remotes:
510 if remote in self.model.remotes:
511 found = True
512 break
513 if found:
514 # Only clear the selection if the specified remotes exist
515 self.remotes.clearSelection()
516 found = all(self.select_remote_by_name(x) for x in remotes)
517 return found
519 def select_local_branch(self, idx):
520 """Selects a local branch by index in the list view"""
521 item = self.local_branches.item(idx)
522 if item:
523 item.setSelected(True)
524 self.local_branches.setCurrentItem(item)
525 self.set_local_branch(item.text())
526 result = True
527 else:
528 result = False
529 return result
531 def select_remote_branch(self, idx):
532 """Selects a remote branch by index in the list view"""
533 item = self.remote_branches.item(idx)
534 if item:
535 item.setSelected(True)
536 self.remote_branches.setCurrentItem(item)
537 remote_branch = item.text()
538 branch = remote_branch.split('/', 1)[-1]
539 self.set_remote_branch(branch)
540 result = True
541 else:
542 result = False
543 return result
545 def display_remotes(self, widget):
546 """Display the available remotes in a listwidget"""
547 displayed = []
548 for remote_name in self.model.remotes:
549 url = self.model.remote_url(remote_name, self.action)
550 display = '{}\t({})'.format(remote_name, N_('URL: %s') % url)
551 displayed.append(display)
552 qtutils.set_items(widget, displayed)
554 def update_remotes(self, update_command_display=True):
555 """Update the remote name when a remote from the list is selected"""
556 widget = self.remotes
557 remotes = self.model.remotes
558 selection = qtutils.selected_item(widget, remotes)
559 if not selection:
560 self.selected_remotes = []
561 return
562 self.set_remote_name(selection)
563 self.selected_remotes = qtutils.selected_items(self.remotes, self.model.remotes)
564 self.set_remote_to(selection, self.selected_remotes)
565 worktree = self.context.git.worktree()
566 self.selected_remotes_by_worktree[worktree] = self.selected_remotes
567 if update_command_display:
568 self.update_command_display()
570 def set_remote_to(self, _remote, selected_remotes):
571 context = self.context
572 all_branches = gitcmds.branch_list(context, remote=True)
573 branches = []
574 patterns = []
575 remote = ''
576 for remote_name in selected_remotes:
577 remote = remote or remote_name # Use the first remote when prepopulating.
578 patterns.append(remote_name + '/*')
580 for branch in all_branches:
581 for pat in patterns:
582 if fnmatch.fnmatch(branch, pat):
583 branches.append(branch)
584 break
585 if branches:
586 self.set_remote_branches(branches)
587 else:
588 self.set_remote_branches(all_branches)
590 if self.action == FETCH:
591 self.set_remote_branch('')
592 elif self.action in (PUSH, PULL):
593 branch = ''
594 current_branch = (
595 self.local_branch.text() or self.context.model.currentbranch
597 remote_branch = f'{remote}/{current_branch}'
598 if branches and remote_branch in branches:
599 branch = current_branch
600 try:
601 idx = self.filtered_remote_branches.index(remote_branch)
602 except ValueError:
603 pass
604 self.select_remote_branch(idx)
605 return
606 self.set_remote_branch(branch)
608 def remote_name_edited(self):
609 """Update the current remote when the remote name is typed manually"""
610 remote = self.remote_name.text()
611 self.update_selected_remotes(remote)
612 self.set_remote_to(remote, self.selected_remotes)
613 self.update_command_display()
615 def get_local_branches(self):
616 """Calculate the list of local branches"""
617 if self.action == FETCH:
618 branches = self.model.local_branches + [FETCH_HEAD]
619 else:
620 branches = self.model.local_branches
621 return branches
623 def update_local_branches(self):
624 """Update the local/remote branch names when a branch is selected"""
625 branches = self.get_local_branches()
626 widget = self.local_branches
627 selection = qtutils.selected_item(widget, branches)
628 if not selection:
629 return
630 self.set_local_branch(selection)
631 if self.action == FETCH and selection != FETCH_HEAD:
632 self.set_remote_branch(selection)
633 self.update_command_display()
635 def update_remote_branches(self):
636 """Update the remote branch name when a branch is selected"""
637 widget = self.remote_branches
638 branches = self.filtered_remote_branches
639 selection = qtutils.selected_item(widget, branches)
640 if not selection:
641 return
642 branch = utils.strip_one(selection)
643 if branch == 'HEAD':
644 return
645 self.set_remote_branch(branch)
646 self.update_command_display()
648 def common_args(self):
649 """Returns git arguments common to fetch/push/pull"""
650 remote_name = self.remote_name.text()
651 local_branch = self.local_branch.text()
652 remote_branch = self.remote_branch.text()
654 ff_only = get(self.ff_only_checkbox)
655 force = get(self.force_checkbox)
656 no_ff = get(self.no_ff_checkbox)
657 rebase = get(self.rebase_checkbox)
658 set_upstream = get(self.upstream_checkbox)
659 tags = get(self.tags_checkbox)
660 prune = get(self.prune_checkbox)
662 return (
663 remote_name,
665 'ff_only': ff_only,
666 'force': force,
667 'local_branch': local_branch,
668 'no_ff': no_ff,
669 'rebase': rebase,
670 'remote_branch': remote_branch,
671 'set_upstream': set_upstream,
672 'tags': tags,
673 'prune': prune,
677 # Actions
679 def push_to_all(self, _remote, *args, **kwargs):
680 """Push to all selected remotes"""
681 selected_remotes = self.selected_remotes
682 all_results = None
683 for remote in selected_remotes:
684 result = self.model.push(remote, *args, **kwargs)
685 all_results = combine(result, all_results)
686 return all_results
688 def action_callback(self):
689 """Perform the actual fetch/push/pull operation"""
690 action = self.action
691 remote_messages = get(self.remote_messages_checkbox)
692 if action == FETCH:
693 model_action = self.model.fetch
694 elif action == PUSH:
695 model_action = self.push_to_all
696 else: # if action == PULL:
697 model_action = self.model.pull
699 remote_name = self.remote_name.text()
700 if not remote_name:
701 errmsg = N_('No repository selected.')
702 Interaction.log(errmsg)
703 return
704 remote, kwargs = self.common_args()
705 self.update_selected_remotes(remote)
707 # Check if we're about to create a new branch and warn.
708 remote_branch = self.remote_branch.text()
709 local_branch = self.local_branch.text()
711 if action == PUSH and not remote_branch:
712 branch = local_branch
713 candidate = f'{remote}/{branch}'
714 prompt = get(self.prompt_checkbox)
716 if prompt and candidate not in self.model.remote_branches:
717 title = N_('Push')
718 args = {
719 'branch': branch,
720 'remote': remote,
722 msg = (
724 'Branch "%(branch)s" does not exist in "%(remote)s".\n'
725 'A new remote branch will be published.'
727 % args
729 info_txt = N_('Create a new remote branch?')
730 ok_text = N_('Create Remote Branch')
731 if not Interaction.confirm(
732 title, msg, info_txt, ok_text, icon=icons.cola()
734 return
736 if get(self.force_checkbox):
737 if action == FETCH:
738 title = N_('Force Fetch?')
739 msg = N_('Non-fast-forward fetch overwrites local history!')
740 info_txt = N_('Force fetching from %s?') % remote
741 ok_text = N_('Force Fetch')
742 elif action == PUSH:
743 title = N_('Force Push?')
744 msg = N_(
745 'Non-fast-forward push overwrites published '
746 'history!\n(Did you pull first?)'
748 info_txt = N_('Force push to %s?') % remote
749 ok_text = N_('Force Push')
750 else: # pull: shouldn't happen since the controls are hidden
751 return
752 if not Interaction.confirm(
753 title, msg, info_txt, ok_text, default=False, icon=icons.discard()
755 return
757 self.progress.setMaximumHeight(
758 self.action_button.height() - defs.small_margin * 2
761 # Use a thread to update in the background
762 task = ActionTask(model_action, remote, kwargs)
763 if remote_messages:
764 result = log.show_remote_messages(self, self.context)
765 else:
766 result = None
767 self.runtask.start(
768 task,
769 progress=self.progress,
770 finish=self.action_completed,
771 result=result,
774 def update_selected_remotes(self, remote):
775 """Update the selected remotes when an ad-hoc remote is typed in"""
776 self.selected_remotes = qtutils.selected_items(self.remotes, self.model.remotes)
777 if remote not in self.selected_remotes:
778 self.selected_remotes = [remote]
779 worktree = self.context.git.worktree()
780 self.selected_remotes_by_worktree[worktree] = self.selected_remotes
782 def action_completed(self, task):
783 """Grab the results of the action and finish up"""
784 if not task.result or not isinstance(task.result, (list, tuple)):
785 return
787 status, out, err = task.result
788 command = 'git %s' % self.action
789 message = Interaction.format_command_status(command, status)
790 details = Interaction.format_out_err(out, err)
792 log_message = message
793 if details:
794 log_message += '\n\n' + details
795 Interaction.log(log_message)
797 if status == 0:
798 close_on_completion = get(self.close_on_completion_checkbox)
799 if close_on_completion:
800 self.accept()
801 return
803 if self.action == PUSH:
804 message += '\n\n'
805 message += N_('Have you rebased/pulled lately?')
807 Interaction.critical(self.windowTitle(), message=message, details=details)
809 def export_state(self):
810 """Export persistent settings"""
811 state = standard.Dialog.export_state(self)
812 state['close_on_completion'] = get(self.close_on_completion_checkbox)
813 state['remote_messages'] = get(self.remote_messages_checkbox)
814 state['selected_remotes'] = self.selected_remotes_by_worktree
815 state['last_updated'] = self.last_updated
816 return state
818 def apply_state(self, state):
819 """Apply persistent settings"""
820 result = standard.Dialog.apply_state(self, state)
821 # Restore the "close on completion" checkbox
822 close_on_completion = bool(state.get('close_on_completion', True))
823 self.close_on_completion_checkbox.setChecked(close_on_completion)
824 # Restore the "show remote messages" checkbox
825 remote_messages = bool(state.get('remote_messages', False))
826 self.remote_messages_checkbox.setChecked(remote_messages)
827 # Restore the selected remotes.
828 self.selected_remotes_by_worktree = state.get('selected_remotes', {})
829 self.last_updated = state.get('last_updated', 0.0)
830 current_time = time.time()
831 one_month = 60.0 * 60.0 * 24.0 * 31.0 # one month is ~31 days.
832 if (current_time - self.last_updated) > one_month:
833 self._prune_selected_remotes()
834 self.last_updated = current_time
835 # Selected remotes are stored per-worktree.
836 worktree = self.context.git.worktree()
837 selected_remotes = self.selected_remotes_by_worktree.get(worktree, [])
838 if selected_remotes:
839 # Restore the stored selection. We stash away the current selection so that
840 # we can restore it in case we are unable to apply the stored selection.
841 current_selection = self.remotes.selectedItems()
842 self.remotes.clearSelection()
843 selected = False
844 for idx, remote in enumerate(selected_remotes):
845 make_current = idx == 0 or not selected
846 if self.select_remote_by_name(remote, make_current=make_current):
847 selected = True
848 # Restore the original selection if nothing was selected.
849 if not selected:
850 for item in current_selection:
851 item.setSelected(True)
852 return result
854 def _prune_selected_remotes(self):
855 """Prune stale worktrees from the persistent selected_remotes_by_worktree"""
856 worktrees = list(self.selected_remotes_by_worktree.keys())
857 for worktree in worktrees:
858 if not os.path.exists(worktree):
859 self.selected_remotes_by_worktree.pop(worktree, None)
862 # Use distinct classes so that each saves its own set of preferences
863 class Fetch(RemoteActionDialog):
864 """Fetch from remote repositories"""
866 def __init__(self, context, parent=None):
867 super().__init__(context, FETCH, N_('Fetch'), parent=parent, icon=icons.repo())
869 def export_state(self):
870 """Export persistent settings"""
871 state = RemoteActionDialog.export_state(self)
872 state['tags'] = get(self.tags_checkbox)
873 state['prune'] = get(self.prune_checkbox)
874 return state
876 def apply_state(self, state):
877 """Apply persistent settings"""
878 result = RemoteActionDialog.apply_state(self, state)
879 tags = bool(state.get('tags', False))
880 self.tags_checkbox.setChecked(tags)
881 prune = bool(state.get('prune', False))
882 self.prune_checkbox.setChecked(prune)
883 return result
886 class Push(RemoteActionDialog):
887 """Push to remote repositories"""
889 def __init__(self, context, parent=None):
890 super().__init__(context, PUSH, N_('Push'), parent=parent, icon=icons.push())
892 def export_state(self):
893 """Export persistent settings"""
894 state = RemoteActionDialog.export_state(self)
895 state['prompt'] = get(self.prompt_checkbox)
896 state['tags'] = get(self.tags_checkbox)
897 return state
899 def apply_state(self, state):
900 """Apply persistent settings"""
901 result = RemoteActionDialog.apply_state(self, state)
902 # Restore the "prompt on creation" checkbox
903 prompt = bool(state.get('prompt', True))
904 self.prompt_checkbox.setChecked(prompt)
905 # Restore the "tags" checkbox
906 tags = bool(state.get('tags', False))
907 self.tags_checkbox.setChecked(tags)
908 return result
911 class Pull(RemoteActionDialog):
912 """Pull from remote repositories"""
914 def __init__(self, context, parent=None):
915 super().__init__(context, PULL, N_('Pull'), parent=parent, icon=icons.pull())
917 def apply_state(self, state):
918 """Apply persistent settings"""
919 result = RemoteActionDialog.apply_state(self, state)
920 # Rebase has the highest priority
921 rebase = bool(state.get('rebase', False))
922 self.rebase_checkbox.setChecked(rebase)
924 ff_only = not rebase and bool(state.get('ff_only', False))
925 no_ff = not rebase and not ff_only and bool(state.get('no_ff', False))
926 self.no_ff_checkbox.setChecked(no_ff)
927 # Allow users coming from older versions that have rebase=False to
928 # pickup the new ff_only=True default by only setting ff_only False
929 # when it either exists in the config or when rebase=True.
930 if 'ff_only' in state or rebase:
931 self.ff_only_checkbox.setChecked(ff_only)
932 return result
934 def export_state(self):
935 """Export persistent settings"""
936 state = RemoteActionDialog.export_state(self)
937 state['ff_only'] = get(self.ff_only_checkbox)
938 state['no_ff'] = get(self.no_ff_checkbox)
939 state['rebase'] = get(self.rebase_checkbox)
940 return state