Merge pull request #1405 from github/pre-commit-ci-update-config
[git-cola.git] / cola / widgets / remote.py
blob718911758296035b3a6b0f7e96def4f340fed1f5
1 """Widgets for Fetch, Push, and Pull"""
2 import fnmatch
3 import time
4 import os
6 try:
7 import notifypy
8 except (ImportError, ModuleNotFoundError):
9 notifypy = None
11 from qtpy import QtGui
12 from qtpy import QtWidgets
13 from qtpy.QtCore import Qt
15 from ..i18n import N_
16 from ..interaction import Interaction
17 from ..models import main
18 from ..models import prefs
19 from ..models.main import FETCH, FETCH_HEAD, PULL, PUSH
20 from ..qtutils import connect_button
21 from ..qtutils import get
22 from .. import core
23 from .. import git
24 from .. import gitcmds
25 from .. import icons
26 from .. import resources
27 from .. import qtutils
28 from .. import utils
29 from . import defs
30 from . import log
31 from . import standard
34 def fetch(context):
35 """Fetch from remote repositories"""
36 return run(context, Fetch)
39 def push(context):
40 """Push to remote repositories"""
41 return run(context, Push)
44 def pull(context):
45 """Pull from remote repositories"""
46 return run(context, Pull)
49 def run(context, RemoteDialog):
50 """Launches fetch/push/pull dialogs."""
51 # Copy global stuff over to speedup startup
52 parent = qtutils.active_window()
53 view = RemoteDialog(context, parent=parent)
54 view.show()
55 return view
58 def combine(result, prev):
59 """Combine multiple (status, out, err) tuples into a combined tuple
61 The current state is passed in via `prev`.
62 The status code is a max() over all the subprocess status codes.
63 Individual (out, err) strings are sequentially concatenated together.
65 """
66 if isinstance(prev, (tuple, list)):
67 if len(prev) != 3:
68 raise AssertionError('combine() with length %d' % len(prev))
69 combined = (
70 max(prev[0], result[0]),
71 combine(prev[1], result[1]),
72 combine(prev[2], result[2]),
74 elif prev and result:
75 combined = prev + '\n\n' + result
76 elif prev:
77 combined = prev
78 else:
79 combined = result
81 return combined
84 def uncheck(value, *checkboxes):
85 """Uncheck the specified checkboxes if value is True"""
86 if value:
87 for checkbox in checkboxes:
88 checkbox.setChecked(False)
91 def strip_remotes(remote_branches):
92 """Strip the <remote>/ prefixes from branches
94 e.g. "origin/main" becomes "main".
96 """
97 branches = [utils.strip_one(branch) for branch in remote_branches]
98 return [branch for branch in branches if branch != 'HEAD']
101 def get_default_remote(context):
102 """Get the name of the default remote to use for pushing.
104 This will be the remote the branch is set to track, if it is set. If it
105 is not, remote.pushDefault will be used (or origin if not set)
108 upstream_remote = gitcmds.upstream_remote(context)
109 return upstream_remote or context.cfg.get('remote.pushDefault', default='origin')
112 class ActionTask(qtutils.Task):
113 """Run actions asynchronously"""
115 def __init__(self, model_action, remote, kwargs):
116 qtutils.Task.__init__(self)
117 self.model_action = model_action
118 self.remote = remote
119 self.kwargs = kwargs
121 def task(self):
122 """Runs the model action and captures the result"""
123 return self.model_action(self.remote, **self.kwargs)
126 def _emit_push_notification(selected_remotes, pushed_remotes, unpushed_remotes):
127 """Emit desktop notification when pushing remotes"""
128 if notifypy is None:
129 return
131 notification = notifypy.Notify()
133 total = len(selected_remotes)
134 count = len(pushed_remotes)
135 scope = {
136 'total': total,
137 'count': count,
139 notification.title = N_('Pushed %(count)s / %(total)s remotes - Git Cola') % scope
141 pushed_message = N_('Pushed: %s') % ', '.join(pushed_remotes)
142 unpushed_message = N_('Not pushed: %s') % ', '.join(unpushed_remotes)
143 success_icon = resources.icon_path('git-cola-ok.svg')
144 error_icon = resources.icon_path('git-cola-error.svg')
146 if unpushed_remotes:
147 notification.icon = error_icon
148 else:
149 notification.icon = success_icon
151 if pushed_remotes and unpushed_remotes:
152 notification.message = unpushed_message + '\t\t' + pushed_message
153 elif pushed_remotes:
154 notification.message = pushed_message
155 else:
156 notification.message = unpushed_message
158 notification.send()
161 class RemoteActionDialog(standard.Dialog):
162 """Interface for performing remote operations"""
164 def __init__(self, context, action, title, parent=None, icon=None):
165 """Customize the dialog based on the remote action"""
166 standard.Dialog.__init__(self, parent=parent)
167 self.setWindowTitle(title)
168 if parent is not None:
169 self.setWindowModality(Qt.WindowModal)
171 self.context = context
172 self.model = model = context.model
173 self.action = action
174 self.filtered_remote_branches = []
175 self.selected_remotes = []
176 self.selected_remotes_by_worktree = {}
177 self.last_updated = 0.0
179 self.runtask = qtutils.RunTask(parent=self)
180 self.local_label = QtWidgets.QLabel()
181 self.local_label.setText(N_('Local Branch'))
183 self.local_branch = QtWidgets.QLineEdit()
184 self.local_branch.textChanged.connect(self.local_branch_text_changed)
185 local_branches = self.get_local_branches()
186 qtutils.add_completer(self.local_branch, local_branches)
188 self.local_branches = QtWidgets.QListWidget()
189 self.local_branches.addItems(local_branches)
191 self.remote_label = QtWidgets.QLabel()
192 self.remote_label.setText(N_('Remote'))
194 self.remote_name = QtWidgets.QLineEdit()
195 qtutils.add_completer(self.remote_name, model.remotes)
197 self.remote_name.editingFinished.connect(self.remote_name_edited)
198 self.remote_name.textEdited.connect(lambda _: self.remote_name_edited())
200 self.remotes = QtWidgets.QListWidget()
201 if action == PUSH:
202 mode = QtWidgets.QAbstractItemView.ExtendedSelection
203 self.remotes.setSelectionMode(mode)
204 self.remotes.addItems(model.remotes)
206 self.remote_branch_label = QtWidgets.QLabel()
207 self.remote_branch_label.setText(N_('Remote Branch'))
209 self.remote_branch = QtWidgets.QLineEdit()
210 self.remote_branch.textChanged.connect(lambda _: self.update_command_display())
211 remote_branches = strip_remotes(model.remote_branches)
212 qtutils.add_completer(self.remote_branch, remote_branches)
214 self.remote_branches = QtWidgets.QListWidget()
215 self.remote_branches.addItems(model.remote_branches)
217 text = N_('Prompt on creation')
218 tooltip = N_('Prompt when pushing creates new remote branches')
219 self.prompt_checkbox = qtutils.checkbox(
220 checked=True, text=text, tooltip=tooltip
223 text = N_('Show remote messages')
224 tooltip = N_('Display remote messages in a separate dialog')
225 self.remote_messages_checkbox = qtutils.checkbox(
226 checked=False, text=text, tooltip=tooltip
229 text = N_('Fast-forward only')
230 tooltip = N_(
231 'Refuse to merge unless the current HEAD is already up-'
232 'to-date or the merge can be resolved as a fast-forward'
234 self.ff_only_checkbox = qtutils.checkbox(
235 checked=True, text=text, tooltip=tooltip
237 self.ff_only_checkbox.toggled.connect(self.update_command_display)
239 text = N_('No fast-forward')
240 tooltip = N_(
241 'Create a merge commit even when the merge resolves as a fast-forward'
243 self.no_ff_checkbox = qtutils.checkbox(
244 checked=False, text=text, tooltip=tooltip
246 self.no_ff_checkbox.toggled.connect(self.update_command_display)
247 text = N_('Force')
248 tooltip = N_(
249 'Allow non-fast-forward updates. Using "force" can '
250 'cause the remote repository to lose commits; '
251 'use it with care'
253 self.force_checkbox = qtutils.checkbox(
254 checked=False, text=text, tooltip=tooltip
256 self.force_checkbox.toggled.connect(self.update_command_display)
258 self.tags_checkbox = qtutils.checkbox(text=N_('Include tags '))
259 self.tags_checkbox.toggled.connect(self.update_command_display)
261 tooltip = N_(
262 'Remove remote-tracking branches that no longer exist on the remote'
264 self.prune_checkbox = qtutils.checkbox(text=N_('Prune '), tooltip=tooltip)
265 self.prune_checkbox.toggled.connect(self.update_command_display)
267 tooltip = N_('Rebase the current branch instead of merging')
268 self.rebase_checkbox = qtutils.checkbox(text=N_('Rebase'), tooltip=tooltip)
269 self.rebase_checkbox.toggled.connect(self.update_command_display)
271 text = N_('Set upstream')
272 tooltip = N_('Configure the remote branch as the the new upstream')
273 self.upstream_checkbox = qtutils.checkbox(text=text, tooltip=tooltip)
274 self.upstream_checkbox.toggled.connect(self.update_command_display)
276 text = N_('Close on completion')
277 tooltip = N_('Close dialog when completed')
278 self.close_on_completion_checkbox = qtutils.checkbox(
279 checked=True, text=text, tooltip=tooltip
282 self.action_button = qtutils.ok_button(title, icon=icon)
283 self.close_button = qtutils.close_button()
284 self.buttons_group = utils.Group(self.close_button, self.action_button)
285 self.inputs_group = utils.Group(
286 self.close_on_completion_checkbox,
287 self.force_checkbox,
288 self.ff_only_checkbox,
289 self.local_branch,
290 self.local_branches,
291 self.tags_checkbox,
292 self.prune_checkbox,
293 self.rebase_checkbox,
294 self.remote_name,
295 self.remotes,
296 self.remote_branch,
297 self.remote_branches,
298 self.upstream_checkbox,
299 self.prompt_checkbox,
300 self.remote_messages_checkbox,
302 self.progress = standard.progress_bar(
303 self,
304 disable=(self.buttons_group, self.inputs_group),
307 self.command_display = log.LogWidget(self.context, display_usage=False)
309 self.local_branch_layout = qtutils.hbox(
310 defs.small_margin, defs.spacing, self.local_label, self.local_branch
313 self.remote_layout = qtutils.hbox(
314 defs.small_margin, defs.spacing, self.remote_label, self.remote_name
317 self.remote_branch_layout = qtutils.hbox(
318 defs.small_margin,
319 defs.spacing,
320 self.remote_branch_label,
321 self.remote_branch,
324 self.options_layout = qtutils.hbox(
325 defs.no_margin,
326 defs.button_spacing,
327 self.force_checkbox,
328 self.ff_only_checkbox,
329 self.no_ff_checkbox,
330 self.tags_checkbox,
331 self.prune_checkbox,
332 self.rebase_checkbox,
333 self.upstream_checkbox,
334 self.prompt_checkbox,
335 self.close_on_completion_checkbox,
336 self.remote_messages_checkbox,
337 qtutils.STRETCH,
338 self.progress,
339 self.close_button,
340 self.action_button,
343 self.remote_input_layout = qtutils.vbox(
344 defs.no_margin, defs.spacing, self.remote_layout, self.remotes
347 self.local_branch_input_layout = qtutils.vbox(
348 defs.no_margin, defs.spacing, self.local_branch_layout, self.local_branches
351 self.remote_branch_input_layout = qtutils.vbox(
352 defs.no_margin,
353 defs.spacing,
354 self.remote_branch_layout,
355 self.remote_branches,
358 if action == PUSH:
359 widgets = (
360 self.remote_input_layout,
361 self.local_branch_input_layout,
362 self.remote_branch_input_layout,
364 else: # fetch and pull
365 widgets = (
366 self.remote_input_layout,
367 self.remote_branch_input_layout,
368 self.local_branch_input_layout,
370 self.top_layout = qtutils.hbox(defs.no_margin, defs.spacing, *widgets)
372 self.main_layout = qtutils.vbox(
373 defs.margin,
374 defs.spacing,
375 self.top_layout,
376 self.command_display,
377 self.options_layout,
379 self.main_layout.setStretchFactor(self.top_layout, 2)
380 self.setLayout(self.main_layout)
382 default_remote = get_default_remote(context)
384 remotes = model.remotes
385 if default_remote in remotes:
386 idx = remotes.index(default_remote)
387 if self.select_remote(idx):
388 self.set_remote_name(default_remote)
389 else:
390 if self.select_first_remote():
391 self.set_remote_name(remotes[0])
393 # Trim the remote list to just the default remote
394 self.update_remotes(update_command_display=False)
396 # Setup signals and slots
397 self.remotes.itemSelectionChanged.connect(self.update_remotes)
399 local = self.local_branches
400 local.itemSelectionChanged.connect(self.update_local_branches)
402 remote = self.remote_branches
403 remote.itemSelectionChanged.connect(self.update_remote_branches)
405 self.no_ff_checkbox.toggled.connect(
406 lambda x: uncheck(x, self.ff_only_checkbox, self.rebase_checkbox)
409 self.ff_only_checkbox.toggled.connect(
410 lambda x: uncheck(x, self.no_ff_checkbox, self.rebase_checkbox)
413 self.rebase_checkbox.toggled.connect(
414 lambda x: uncheck(x, self.no_ff_checkbox, self.ff_only_checkbox)
417 connect_button(self.action_button, self.action_callback)
418 connect_button(self.close_button, self.close)
420 qtutils.add_action(
421 self, N_('Close'), self.close, QtGui.QKeySequence.Close, 'Esc'
423 if action != FETCH:
424 self.prune_checkbox.hide()
426 if action != PUSH:
427 # Push-only options
428 self.upstream_checkbox.hide()
429 self.prompt_checkbox.hide()
431 if action == PULL:
432 # Fetch and Push-only options
433 self.force_checkbox.hide()
434 self.tags_checkbox.hide()
435 self.local_label.hide()
436 self.local_branch.hide()
437 self.local_branches.hide()
438 else:
439 # Pull-only options
440 self.rebase_checkbox.hide()
441 self.no_ff_checkbox.hide()
442 self.ff_only_checkbox.hide()
444 self.init_size(parent=parent)
445 self.set_field_defaults()
447 def set_rebase(self, value):
448 """Check the rebase checkbox"""
449 self.rebase_checkbox.setChecked(value)
451 def set_field_defaults(self):
452 """Set sensible initial defaults"""
453 # Default to "git fetch origin main"
454 action = self.action
455 if action == FETCH:
456 self.set_local_branch('')
457 self.set_remote_branch('')
458 if action == PULL: # Nothing to do when fetching.
459 pass
460 # Select the current branch by default for push
461 if action == PUSH:
462 branch = self.model.currentbranch
463 try:
464 idx = self.model.local_branches.index(branch)
465 except ValueError:
466 return
467 self.select_local_branch(idx)
468 self.set_remote_branch(branch)
470 self.update_command_display()
472 def update_command_display(self):
473 """Display the git commands that will be run"""
474 commands = ['']
475 for remote in self.selected_remotes:
476 cmd = ['git', self.action]
477 _, kwargs = self.common_args()
478 args, kwargs = main.remote_args(self.context, remote, self.action, **kwargs)
479 cmd.extend(git.transform_kwargs(**kwargs))
480 cmd.extend(args)
481 commands.append(core.list2cmdline(cmd))
482 self.command_display.set_output('\n'.join(commands))
484 def local_branch_text_changed(self, value):
485 """Update the remote branch field in response to local branch text edits"""
486 if self.action == PUSH:
487 self.remote_branches.clearSelection()
488 self.set_remote_branch(value)
489 self.update_command_display()
491 def set_remote_name(self, remote_name):
492 """Set the remote name"""
493 self.remote_name.setText(remote_name)
495 def set_local_branch(self, branch):
496 """Set the local branch name"""
497 self.local_branch.setText(branch)
498 if branch:
499 self.local_branch.selectAll()
501 def set_remote_branch(self, branch):
502 """Set the remote branch name"""
503 self.remote_branch.setText(branch)
504 if branch:
505 self.remote_branch.selectAll()
507 def set_remote_branches(self, branches):
508 """Set the list of remote branches"""
509 self.remote_branches.clear()
510 self.remote_branches.addItems(branches)
511 self.filtered_remote_branches = branches
512 qtutils.add_completer(self.remote_branch, strip_remotes(branches))
514 def select_first_remote(self):
515 """Select the first remote in the list view"""
516 return self.select_remote(0)
518 def select_remote(self, idx, make_current=True):
519 """Select a remote by index"""
520 item = self.remotes.item(idx)
521 if item:
522 item.setSelected(True)
523 if make_current:
524 self.remotes.setCurrentItem(item)
525 self.set_remote_name(item.text())
526 result = True
527 else:
528 result = False
529 return result
531 def select_remote_by_name(self, remote, make_current=True):
532 """Select a remote by name"""
533 remotes = self.model.remotes
534 if remote in remotes:
535 idx = remotes.index(remote)
536 result = self.select_remote(idx, make_current=make_current)
537 else:
538 result = False
539 return result
541 def set_selected_remotes(self, remotes):
542 """Set the list of selected remotes
544 Return True if all remotes were found and selected.
547 # Invalid remote names are ignored.
548 # This handles a remote going away between sessions.
549 # The selection is unchanged when none of the specified remotes exist.
550 found = False
551 for remote in remotes:
552 if remote in self.model.remotes:
553 found = True
554 break
555 if found:
556 # Only clear the selection if the specified remotes exist
557 self.remotes.clearSelection()
558 found = all(self.select_remote_by_name(x) for x in remotes)
559 return found
561 def select_local_branch(self, idx):
562 """Selects a local branch by index in the list view"""
563 item = self.local_branches.item(idx)
564 if item:
565 item.setSelected(True)
566 self.local_branches.setCurrentItem(item)
567 self.set_local_branch(item.text())
568 result = True
569 else:
570 result = False
571 return result
573 def select_remote_branch(self, idx):
574 """Selects a remote branch by index in the list view"""
575 item = self.remote_branches.item(idx)
576 if item:
577 item.setSelected(True)
578 self.remote_branches.setCurrentItem(item)
579 remote_branch = item.text()
580 branch = remote_branch.split('/', 1)[-1]
581 self.set_remote_branch(branch)
582 result = True
583 else:
584 result = False
585 return result
587 def display_remotes(self, widget):
588 """Display the available remotes in a listwidget"""
589 displayed = []
590 for remote_name in self.model.remotes:
591 url = self.model.remote_url(remote_name, self.action)
592 display = '{}\t({})'.format(remote_name, N_('URL: %s') % url)
593 displayed.append(display)
594 qtutils.set_items(widget, displayed)
596 def update_remotes(self, update_command_display=True):
597 """Update the remote name when a remote from the list is selected"""
598 widget = self.remotes
599 remotes = self.model.remotes
600 selection = qtutils.selected_item(widget, remotes)
601 if not selection:
602 self.selected_remotes = []
603 return
604 self.set_remote_name(selection)
605 self.selected_remotes = qtutils.selected_items(self.remotes, self.model.remotes)
606 self.set_remote_to(selection, self.selected_remotes)
607 worktree = self.context.git.worktree()
608 self.selected_remotes_by_worktree[worktree] = self.selected_remotes
609 if update_command_display:
610 self.update_command_display()
612 def set_remote_to(self, _remote, selected_remotes):
613 context = self.context
614 all_branches = gitcmds.branch_list(context, remote=True)
615 branches = []
616 patterns = []
617 remote = ''
618 for remote_name in selected_remotes:
619 remote = remote or remote_name # Use the first remote when prepopulating.
620 patterns.append(remote_name + '/*')
622 for branch in all_branches:
623 for pat in patterns:
624 if fnmatch.fnmatch(branch, pat):
625 branches.append(branch)
626 break
627 if branches:
628 self.set_remote_branches(branches)
629 else:
630 self.set_remote_branches(all_branches)
632 if self.action == FETCH:
633 self.set_remote_branch('')
634 elif self.action in (PUSH, PULL):
635 branch = ''
636 current_branch = (
637 self.local_branch.text() or self.context.model.currentbranch
639 remote_branch = f'{remote}/{current_branch}'
640 if branches and remote_branch in branches:
641 branch = current_branch
642 try:
643 idx = self.filtered_remote_branches.index(remote_branch)
644 except ValueError:
645 pass
646 self.select_remote_branch(idx)
647 return
648 self.set_remote_branch(branch)
650 def remote_name_edited(self):
651 """Update the current remote when the remote name is typed manually"""
652 remote = self.remote_name.text()
653 self.update_selected_remotes(remote)
654 self.set_remote_to(remote, self.selected_remotes)
655 self.update_command_display()
657 def get_local_branches(self):
658 """Calculate the list of local branches"""
659 if self.action == FETCH:
660 branches = self.model.local_branches + [FETCH_HEAD]
661 else:
662 branches = self.model.local_branches
663 return branches
665 def update_local_branches(self):
666 """Update the local/remote branch names when a branch is selected"""
667 branches = self.get_local_branches()
668 widget = self.local_branches
669 selection = qtutils.selected_item(widget, branches)
670 if not selection:
671 return
672 self.set_local_branch(selection)
673 if self.action == FETCH and selection != FETCH_HEAD:
674 self.set_remote_branch(selection)
675 self.update_command_display()
677 def update_remote_branches(self):
678 """Update the remote branch name when a branch is selected"""
679 widget = self.remote_branches
680 branches = self.filtered_remote_branches
681 selection = qtutils.selected_item(widget, branches)
682 if not selection:
683 return
684 branch = utils.strip_one(selection)
685 if branch == 'HEAD':
686 return
687 self.set_remote_branch(branch)
688 self.update_command_display()
690 def common_args(self):
691 """Returns git arguments common to fetch/push/pull"""
692 remote_name = self.remote_name.text()
693 local_branch = self.local_branch.text()
694 remote_branch = self.remote_branch.text()
696 ff_only = get(self.ff_only_checkbox)
697 force = get(self.force_checkbox)
698 no_ff = get(self.no_ff_checkbox)
699 rebase = get(self.rebase_checkbox)
700 set_upstream = get(self.upstream_checkbox)
701 tags = get(self.tags_checkbox)
702 prune = get(self.prune_checkbox)
704 return (
705 remote_name,
707 'ff_only': ff_only,
708 'force': force,
709 'local_branch': local_branch,
710 'no_ff': no_ff,
711 'rebase': rebase,
712 'remote_branch': remote_branch,
713 'set_upstream': set_upstream,
714 'tags': tags,
715 'prune': prune,
719 # Actions
721 def push_to_all(self, _remote, *args, **kwargs):
722 """Push to all selected remotes"""
723 selected_remotes = self.selected_remotes
724 all_results = None
726 pushed_remotes = []
727 unpushed_remotes = []
729 for remote in selected_remotes:
730 result = self.model.push(remote, *args, **kwargs)
732 if result[0] == 0:
733 pushed_remotes.append(remote)
734 else:
735 unpushed_remotes.append(remote)
737 all_results = combine(result, all_results)
739 if prefs.notify_on_push(self.context):
740 _emit_push_notification(selected_remotes, pushed_remotes, unpushed_remotes)
742 return all_results
744 def action_callback(self):
745 """Perform the actual fetch/push/pull operation"""
746 action = self.action
747 remote_messages = get(self.remote_messages_checkbox)
748 if action == FETCH:
749 model_action = self.model.fetch
750 elif action == PUSH:
751 model_action = self.push_to_all
752 else: # if action == PULL:
753 model_action = self.model.pull
755 remote_name = self.remote_name.text()
756 if not remote_name:
757 errmsg = N_('No repository selected.')
758 Interaction.log(errmsg)
759 return
760 remote, kwargs = self.common_args()
761 self.update_selected_remotes(remote)
763 # Check if we're about to create a new branch and warn.
764 remote_branch = self.remote_branch.text()
765 local_branch = self.local_branch.text()
767 if action == PUSH and not remote_branch:
768 branch = local_branch
769 candidate = f'{remote}/{branch}'
770 prompt = get(self.prompt_checkbox)
772 if prompt and candidate not in self.model.remote_branches:
773 title = N_('Push')
774 args = {
775 'branch': branch,
776 'remote': remote,
778 msg = (
780 'Branch "%(branch)s" does not exist in "%(remote)s".\n'
781 'A new remote branch will be published.'
783 % args
785 info_txt = N_('Create a new remote branch?')
786 ok_text = N_('Create Remote Branch')
787 if not Interaction.confirm(
788 title, msg, info_txt, ok_text, icon=icons.cola()
790 return
792 if get(self.force_checkbox):
793 if action == FETCH:
794 title = N_('Force Fetch?')
795 msg = N_('Non-fast-forward fetch overwrites local history!')
796 info_txt = N_('Force fetching from %s?') % remote
797 ok_text = N_('Force Fetch')
798 elif action == PUSH:
799 title = N_('Force Push?')
800 msg = N_(
801 'Non-fast-forward push overwrites published '
802 'history!\n(Did you pull first?)'
804 info_txt = N_('Force push to %s?') % remote
805 ok_text = N_('Force Push')
806 else: # pull: shouldn't happen since the controls are hidden
807 return
808 if not Interaction.confirm(
809 title, msg, info_txt, ok_text, default=False, icon=icons.discard()
811 return
813 self.progress.setMaximumHeight(
814 self.action_button.height() - defs.small_margin * 2
817 # Use a thread to update in the background
818 task = ActionTask(model_action, remote, kwargs)
819 if remote_messages:
820 result = log.show_remote_messages(self, self.context)
821 else:
822 result = None
823 self.runtask.start(
824 task,
825 progress=self.progress,
826 finish=self.action_completed,
827 result=result,
830 def update_selected_remotes(self, remote):
831 """Update the selected remotes when an ad-hoc remote is typed in"""
832 self.selected_remotes = qtutils.selected_items(self.remotes, self.model.remotes)
833 if remote not in self.selected_remotes:
834 self.selected_remotes = [remote]
835 worktree = self.context.git.worktree()
836 self.selected_remotes_by_worktree[worktree] = self.selected_remotes
838 def action_completed(self, task):
839 """Grab the results of the action and finish up"""
840 if not task.result or not isinstance(task.result, (list, tuple)):
841 return
843 status, out, err = task.result
844 command = 'git %s' % self.action
845 message = Interaction.format_command_status(command, status)
846 details = Interaction.format_out_err(out, err)
848 log_message = message
849 if details:
850 log_message += '\n\n' + details
851 Interaction.log(log_message)
853 if status == 0:
854 close_on_completion = get(self.close_on_completion_checkbox)
855 if close_on_completion:
856 self.accept()
857 return
859 if self.action == PUSH:
860 message += '\n\n'
861 message += N_('Have you rebased/pulled lately?')
863 Interaction.critical(self.windowTitle(), message=message, details=details)
865 def export_state(self):
866 """Export persistent settings"""
867 state = standard.Dialog.export_state(self)
868 state['close_on_completion'] = get(self.close_on_completion_checkbox)
869 state['remote_messages'] = get(self.remote_messages_checkbox)
870 state['selected_remotes'] = self.selected_remotes_by_worktree
871 state['last_updated'] = self.last_updated
872 return state
874 def apply_state(self, state):
875 """Apply persistent settings"""
876 result = standard.Dialog.apply_state(self, state)
877 # Restore the "close on completion" checkbox
878 close_on_completion = bool(state.get('close_on_completion', True))
879 self.close_on_completion_checkbox.setChecked(close_on_completion)
880 # Restore the "show remote messages" checkbox
881 remote_messages = bool(state.get('remote_messages', False))
882 self.remote_messages_checkbox.setChecked(remote_messages)
883 # Restore the selected remotes.
884 self.selected_remotes_by_worktree = state.get('selected_remotes', {})
885 self.last_updated = state.get('last_updated', 0.0)
886 current_time = time.time()
887 one_month = 60.0 * 60.0 * 24.0 * 31.0 # one month is ~31 days.
888 if (current_time - self.last_updated) > one_month:
889 self._prune_selected_remotes()
890 self.last_updated = current_time
891 # Selected remotes are stored per-worktree.
892 worktree = self.context.git.worktree()
893 selected_remotes = self.selected_remotes_by_worktree.get(worktree, [])
894 if selected_remotes:
895 # Restore the stored selection. We stash away the current selection so that
896 # we can restore it in case we are unable to apply the stored selection.
897 current_selection = self.remotes.selectedItems()
898 self.remotes.clearSelection()
899 selected = False
900 for idx, remote in enumerate(selected_remotes):
901 make_current = idx == 0 or not selected
902 if self.select_remote_by_name(remote, make_current=make_current):
903 selected = True
904 # Restore the original selection if nothing was selected.
905 if not selected:
906 for item in current_selection:
907 item.setSelected(True)
908 return result
910 def _prune_selected_remotes(self):
911 """Prune stale worktrees from the persistent selected_remotes_by_worktree"""
912 worktrees = list(self.selected_remotes_by_worktree.keys())
913 for worktree in worktrees:
914 if not os.path.exists(worktree):
915 self.selected_remotes_by_worktree.pop(worktree, None)
918 # Use distinct classes so that each saves its own set of preferences
919 class Fetch(RemoteActionDialog):
920 """Fetch from remote repositories"""
922 def __init__(self, context, parent=None):
923 super().__init__(context, FETCH, N_('Fetch'), parent=parent, icon=icons.repo())
925 def export_state(self):
926 """Export persistent settings"""
927 state = RemoteActionDialog.export_state(self)
928 state['tags'] = get(self.tags_checkbox)
929 state['prune'] = get(self.prune_checkbox)
930 return state
932 def apply_state(self, state):
933 """Apply persistent settings"""
934 result = RemoteActionDialog.apply_state(self, state)
935 tags = bool(state.get('tags', False))
936 self.tags_checkbox.setChecked(tags)
937 prune = bool(state.get('prune', False))
938 self.prune_checkbox.setChecked(prune)
939 return result
942 class Push(RemoteActionDialog):
943 """Push to remote repositories"""
945 def __init__(self, context, parent=None):
946 super().__init__(context, PUSH, N_('Push'), parent=parent, icon=icons.push())
948 def export_state(self):
949 """Export persistent settings"""
950 state = RemoteActionDialog.export_state(self)
951 state['prompt'] = get(self.prompt_checkbox)
952 state['tags'] = get(self.tags_checkbox)
953 return state
955 def apply_state(self, state):
956 """Apply persistent settings"""
957 result = RemoteActionDialog.apply_state(self, state)
958 # Restore the "prompt on creation" checkbox
959 prompt = bool(state.get('prompt', True))
960 self.prompt_checkbox.setChecked(prompt)
961 # Restore the "tags" checkbox
962 tags = bool(state.get('tags', False))
963 self.tags_checkbox.setChecked(tags)
964 return result
967 class Pull(RemoteActionDialog):
968 """Pull from remote repositories"""
970 def __init__(self, context, parent=None):
971 super().__init__(context, PULL, N_('Pull'), parent=parent, icon=icons.pull())
973 def apply_state(self, state):
974 """Apply persistent settings"""
975 result = RemoteActionDialog.apply_state(self, state)
976 # Rebase has the highest priority
977 rebase = bool(state.get('rebase', False))
978 self.rebase_checkbox.setChecked(rebase)
980 ff_only = not rebase and bool(state.get('ff_only', False))
981 no_ff = not rebase and not ff_only and bool(state.get('no_ff', False))
982 self.no_ff_checkbox.setChecked(no_ff)
983 # Allow users coming from older versions that have rebase=False to
984 # pickup the new ff_only=True default by only setting ff_only False
985 # when it either exists in the config or when rebase=True.
986 if 'ff_only' in state or rebase:
987 self.ff_only_checkbox.setChecked(ff_only)
988 return result
990 def export_state(self):
991 """Export persistent settings"""
992 state = RemoteActionDialog.export_state(self)
993 state['ff_only'] = get(self.ff_only_checkbox)
994 state['no_ff'] = get(self.no_ff_checkbox)
995 state['rebase'] = get(self.rebase_checkbox)
996 return state