1 """Widgets for Fetch, Push, and Pull"""
8 except (ImportError, ModuleNotFoundError
):
11 from qtpy
import QtGui
12 from qtpy
import QtWidgets
13 from qtpy
.QtCore
import Qt
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
24 from .. import gitcmds
26 from .. import resources
27 from .. import qtutils
31 from . import standard
35 """Fetch from remote repositories"""
36 return run(context
, Fetch
)
40 """Push to remote repositories"""
41 return run(context
, Push
)
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
)
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.
66 if isinstance(prev
, (tuple, list)):
68 raise AssertionError('combine() with length %d' % len(prev
))
70 max(prev
[0], result
[0]),
71 combine(prev
[1], result
[1]),
72 combine(prev
[2], result
[2]),
75 combined
= prev
+ '\n\n' + result
84 def uncheck(value
, *checkboxes
):
85 """Uncheck the specified checkboxes if value is True"""
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".
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
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"""
131 notification
= notifypy
.Notify()
133 total
= len(selected_remotes
)
134 count
= len(pushed_remotes
)
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')
147 notification
.icon
= error_icon
149 notification
.icon
= success_icon
151 if pushed_remotes
and unpushed_remotes
:
152 notification
.message
= unpushed_message
+ '\t\t' + pushed_message
154 notification
.message
= pushed_message
156 notification
.message
= unpushed_message
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
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()
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')
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')
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
)
249 'Allow non-fast-forward updates. Using "force" can '
250 'cause the remote repository to lose commits; '
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
)
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
,
288 self
.ff_only_checkbox
,
293 self
.rebase_checkbox
,
297 self
.remote_branches
,
298 self
.upstream_checkbox
,
299 self
.prompt_checkbox
,
300 self
.remote_messages_checkbox
,
302 self
.progress
= standard
.progress_bar(
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(
320 self
.remote_branch_label
,
324 self
.options_layout
= qtutils
.hbox(
328 self
.ff_only_checkbox
,
332 self
.rebase_checkbox
,
333 self
.upstream_checkbox
,
334 self
.prompt_checkbox
,
335 self
.close_on_completion_checkbox
,
336 self
.remote_messages_checkbox
,
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(
354 self
.remote_branch_layout
,
355 self
.remote_branches
,
360 self
.remote_input_layout
,
361 self
.local_branch_input_layout
,
362 self
.remote_branch_input_layout
,
364 else: # fetch and pull
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(
376 self
.command_display
,
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
)
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
)
421 self
, N_('Close'), self
.close
, QtGui
.QKeySequence
.Close
, 'Esc'
424 self
.prune_checkbox
.hide()
428 self
.upstream_checkbox
.hide()
429 self
.prompt_checkbox
.hide()
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()
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"
456 self
.set_local_branch('')
457 self
.set_remote_branch('')
458 if action
== PULL
: # Nothing to do when fetching.
460 # Select the current branch by default for push
462 branch
= self
.model
.currentbranch
464 idx
= self
.model
.local_branches
.index(branch
)
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"""
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
))
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
)
499 self
.local_branch
.selectAll()
501 def set_remote_branch(self
, branch
):
502 """Set the remote branch name"""
503 self
.remote_branch
.setText(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
)
522 item
.setSelected(True)
524 self
.remotes
.setCurrentItem(item
)
525 self
.set_remote_name(item
.text())
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
)
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.
551 for remote
in remotes
:
552 if remote
in self
.model
.remotes
:
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
)
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
)
565 item
.setSelected(True)
566 self
.local_branches
.setCurrentItem(item
)
567 self
.set_local_branch(item
.text())
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
)
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
)
587 def display_remotes(self
, widget
):
588 """Display the available remotes in a listwidget"""
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
)
602 self
.selected_remotes
= []
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)
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
:
624 if fnmatch
.fnmatch(branch
, pat
):
625 branches
.append(branch
)
628 self
.set_remote_branches(branches
)
630 self
.set_remote_branches(all_branches
)
632 if self
.action
== FETCH
:
633 self
.set_remote_branch('')
634 elif self
.action
in (PUSH
, PULL
):
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
643 idx
= self
.filtered_remote_branches
.index(remote_branch
)
646 self
.select_remote_branch(idx
)
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
]
662 branches
= self
.model
.local_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
)
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
)
684 branch
= utils
.strip_one(selection
)
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
)
709 'local_branch': local_branch
,
712 'remote_branch': remote_branch
,
713 'set_upstream': set_upstream
,
721 def push_to_all(self
, _remote
, *args
, **kwargs
):
722 """Push to all selected remotes"""
723 selected_remotes
= self
.selected_remotes
727 unpushed_remotes
= []
729 for remote
in selected_remotes
:
730 result
= self
.model
.push(remote
, *args
, **kwargs
)
733 pushed_remotes
.append(remote
)
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
)
744 def action_callback(self
):
745 """Perform the actual fetch/push/pull operation"""
747 remote_messages
= get(self
.remote_messages_checkbox
)
749 model_action
= self
.model
.fetch
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()
757 errmsg
= N_('No repository selected.')
758 Interaction
.log(errmsg
)
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
:
780 'Branch "%(branch)s" does not exist in "%(remote)s".\n'
781 'A new remote branch will be published.'
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()
792 if get(self
.force_checkbox
):
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')
799 title
= N_('Force Push?')
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
808 if not Interaction
.confirm(
809 title
, msg
, info_txt
, ok_text
, default
=False, icon
=icons
.discard()
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
)
820 result
= log
.show_remote_messages(self
, self
.context
)
825 progress
=self
.progress
,
826 finish
=self
.action_completed
,
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)):
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
850 log_message
+= '\n\n' + details
851 Interaction
.log(log_message
)
854 close_on_completion
= get(self
.close_on_completion_checkbox
)
855 if close_on_completion
:
859 if self
.action
== PUSH
:
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
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
, [])
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()
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
):
904 # Restore the original selection if nothing was selected.
906 for item
in current_selection
:
907 item
.setSelected(True)
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
)
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
)
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
)
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
)
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
)
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
)