1 """Widgets for Fetch, Push, and Pull"""
2 from __future__
import division
, absolute_import
, unicode_literals
6 from qtpy
import QtWidgets
7 from qtpy
.QtCore
import Qt
10 from ..interaction
import Interaction
11 from ..models
import main
12 from ..qtutils
import connect_button
13 from ..qtutils
import get
14 from .. import gitcmds
16 from .. import qtutils
18 from .standard
import ProgressDialog
20 from . import standard
29 """Fetch from remote repositories"""
30 return run(context
, Fetch
)
34 """Push to remote repositories"""
35 return run(context
, Push
)
39 """Pull from remote repositories"""
40 return run(context
, Pull
)
43 def run(context
, RemoteDialog
):
44 """Launches fetch/push/pull dialogs."""
45 # Copy global stuff over to speedup startup
46 model
= main
.MainModel(context
)
47 global_model
= context
.model
48 model
.currentbranch
= global_model
.currentbranch
49 model
.local_branches
= global_model
.local_branches
50 model
.remote_branches
= global_model
.remote_branches
51 model
.tags
= global_model
.tags
52 model
.remotes
= global_model
.remotes
53 parent
= qtutils
.active_window()
54 view
= RemoteDialog(context
, model
, parent
=parent
)
59 def combine(result
, prev
):
60 """Combine multiple (status, out, err) tuples into a combined tuple
62 The current state is passed in via `prev`.
63 The status code is a max() over all the subprocess status codes.
64 Individual (out, err) strings are sequentially concatenated together.
67 if isinstance(prev
, (tuple, list)):
69 raise AssertionError('combine() with length %d' % len(prev
))
70 combined
= (max(prev
[0], result
[0]),
71 combine(prev
[1], result
[1]),
72 combine(prev
[2], result
[2]))
74 combined
= prev
+ '\n\n' + result
83 def uncheck(value
, *checkboxes
):
84 """Uncheck the specified checkboxes if value is True"""
86 for checkbox
in checkboxes
:
87 checkbox
.setChecked(False)
90 def strip_remotes(remote_branches
):
91 """Strip the <remote>/ prefixes from branches
93 e.g. "origin/master" becomes "master".
96 branches
= [utils
.strip_one(branch
) for branch
in remote_branches
]
97 return [branch
for branch
in branches
if branch
!= 'HEAD']
100 class ActionTask(qtutils
.Task
):
101 """Run actions asynchronously"""
103 def __init__(self
, parent
, model_action
, remote
, kwargs
):
104 qtutils
.Task
.__init
__(self
, parent
)
105 self
.model_action
= model_action
110 """Runs the model action and captures the result"""
111 return self
.model_action(self
.remote
, **self
.kwargs
)
114 class RemoteActionDialog(standard
.Dialog
):
115 """Interface for performing remote operations"""
117 def __init__(self
, context
, model
, action
, title
, parent
=None, icon
=None):
118 """Customize the dialog based on the remote action"""
119 standard
.Dialog
.__init
__(self
, parent
=parent
)
121 self
.context
= context
124 self
.filtered_remote_branches
= []
125 self
.selected_remotes
= []
127 self
.setWindowTitle(title
)
128 if parent
is not None:
129 self
.setWindowModality(Qt
.WindowModal
)
131 self
.runtask
= qtutils
.RunTask(parent
=self
)
132 self
.progress
= ProgressDialog(title
, N_('Updating'), self
)
134 self
.local_label
= QtWidgets
.QLabel()
135 self
.local_label
.setText(N_('Local Branch'))
137 self
.local_branch
= QtWidgets
.QLineEdit()
138 qtutils
.add_completer(self
.local_branch
, self
.model
.local_branches
)
140 self
.local_branches
= QtWidgets
.QListWidget()
141 self
.local_branches
.addItems(self
.model
.local_branches
)
143 self
.remote_label
= QtWidgets
.QLabel()
144 self
.remote_label
.setText(N_('Remote'))
146 self
.remote_name
= QtWidgets
.QLineEdit()
147 qtutils
.add_completer(self
.remote_name
, self
.model
.remotes
)
148 self
.remote_name
.editingFinished
.connect(self
.remote_name_edited
)
149 self
.remote_name
.textEdited
.connect(lambda x
: self
.remote_name_edited())
151 self
.remotes
= QtWidgets
.QListWidget()
153 mode
= QtWidgets
.QAbstractItemView
.ExtendedSelection
154 self
.remotes
.setSelectionMode(mode
)
155 self
.remotes
.addItems(self
.model
.remotes
)
157 self
.remote_branch_label
= QtWidgets
.QLabel()
158 self
.remote_branch_label
.setText(N_('Remote Branch'))
160 self
.remote_branch
= QtWidgets
.QLineEdit()
161 remote_branches
= strip_remotes(self
.model
.remote_branches
)
162 qtutils
.add_completer(self
.remote_branch
, remote_branches
)
164 self
.remote_branches
= QtWidgets
.QListWidget()
165 self
.remote_branches
.addItems(self
.model
.remote_branches
)
167 text
= N_('Prompt on creation')
168 tooltip
= N_('Prompt when pushing creates new remote branches')
169 self
.prompt_checkbox
= qtutils
.checkbox(checked
=True, text
=text
,
172 text
= N_('Fast-forward only')
173 tooltip
= N_('Refuse to merge unless the current HEAD is already up-'
174 'to-date or the merge can be resolved as a fast-forward')
175 self
.ff_only_checkbox
= qtutils
.checkbox(checked
=True,
176 text
=text
, tooltip
=tooltip
)
178 text
= N_('No fast-forward')
179 tooltip
= N_('Create a merge commit even when the merge resolves as a '
181 self
.no_ff_checkbox
= qtutils
.checkbox(checked
=False,
182 text
=text
, tooltip
=tooltip
)
184 tooltip
= N_('Allow non-fast-forward updates. Using "force" can '
185 'cause the remote repository to lose commits; '
187 self
.force_checkbox
= qtutils
.checkbox(checked
=False, text
=text
,
190 self
.tags_checkbox
= qtutils
.checkbox(text
=N_('Include tags '))
192 tooltip
= N_('Remove remote-tracking branches that no longer '
193 'exist on the remote')
194 self
.prune_checkbox
= qtutils
.checkbox(text
=N_('Prune '),
197 tooltip
= N_('Rebase the current branch instead of merging')
198 self
.rebase_checkbox
= qtutils
.checkbox(text
=N_('Rebase'),
201 text
= N_('Set upstream')
202 tooltip
= N_('Configure the remote branch as the the new upstream')
203 self
.upstream_checkbox
= qtutils
.checkbox(text
=text
, tooltip
=tooltip
)
205 self
.action_button
= qtutils
.ok_button(title
, icon
=icon
)
206 self
.close_button
= qtutils
.close_button()
208 self
.buttons
= utils
.Group(self
.action_button
, self
.close_button
)
210 self
.local_branch_layout
= qtutils
.hbox(
216 self
.remote_layout
= qtutils
.hbox(
222 self
.remote_branch_layout
= qtutils
.hbox(
225 self
.remote_branch_label
,
228 self
.options_layout
= qtutils
.hbox(
229 defs
.no_margin
, defs
.button_spacing
,
233 self
.ff_only_checkbox
,
237 self
.rebase_checkbox
,
238 self
.upstream_checkbox
,
239 self
.prompt_checkbox
,
242 self
.remote_input_layout
= qtutils
.vbox(
243 defs
.no_margin
, defs
.spacing
,
244 self
.remote_layout
, self
.remotes
)
246 self
.local_branch_input_layout
= qtutils
.vbox(
247 defs
.no_margin
, defs
.spacing
,
248 self
.local_branch_layout
, self
.local_branches
)
250 self
.remote_branch_input_layout
= qtutils
.vbox(
251 defs
.no_margin
, defs
.spacing
,
252 self
.remote_branch_layout
, self
.remote_branches
)
256 self
.remote_input_layout
,
257 self
.local_branch_input_layout
,
258 self
.remote_branch_input_layout
)
259 else: # fetch and pull
261 self
.remote_input_layout
,
262 self
.remote_branch_input_layout
,
263 self
.local_branch_input_layout
)
264 self
.top_layout
= qtutils
.hbox(defs
.no_margin
, defs
.spacing
, *widgets
)
266 self
.main_layout
= qtutils
.vbox(
267 defs
.margin
, defs
.spacing
,
268 self
.top_layout
, self
.options_layout
)
269 self
.setLayout(self
.main_layout
)
271 # Select the upstream remote if configured, or "origin"
272 default_remote
= gitcmds
.upstream_remote(context
) or 'origin'
273 if self
.select_remote_by_name(default_remote
):
274 self
.set_remote_name(default_remote
)
275 elif self
.select_first_remote():
276 self
.set_remote_name(model
.remotes
[0])
278 # Trim the remote list to just the default remote
279 self
.update_remotes()
280 self
.set_field_defaults()
282 # Setup signals and slots
283 self
.remotes
.itemSelectionChanged
.connect(self
.update_remotes
)
285 local
= self
.local_branches
286 local
.itemSelectionChanged
.connect(self
.update_local_branches
)
288 remote
= self
.remote_branches
289 remote
.itemSelectionChanged
.connect(self
.update_remote_branches
)
291 self
.no_ff_checkbox
.toggled
.connect(
292 lambda x
: uncheck(x
, self
.ff_only_checkbox
, self
.rebase_checkbox
))
294 self
.ff_only_checkbox
.toggled
.connect(
295 lambda x
: uncheck(x
, self
.no_ff_checkbox
, self
.rebase_checkbox
))
297 self
.rebase_checkbox
.toggled
.connect(
298 lambda x
: uncheck(x
, self
.no_ff_checkbox
, self
.ff_only_checkbox
))
300 connect_button(self
.action_button
, self
.action_callback
)
301 connect_button(self
.close_button
, self
.close
)
303 qtutils
.add_action(self
, N_('Close'), self
.close
,
304 QtGui
.QKeySequence
.Close
, 'Esc')
306 self
.prune_checkbox
.hide()
310 self
.upstream_checkbox
.hide()
311 self
.prompt_checkbox
.hide()
314 # Fetch and Push-only options
315 self
.force_checkbox
.hide()
316 self
.tags_checkbox
.hide()
317 self
.local_label
.hide()
318 self
.local_branch
.hide()
319 self
.local_branches
.hide()
322 self
.rebase_checkbox
.hide()
323 self
.no_ff_checkbox
.hide()
324 self
.ff_only_checkbox
.hide()
326 self
.init_size(parent
=parent
)
328 def set_rebase(self
, value
):
329 """Check the rebase checkbox"""
330 self
.rebase_checkbox
.setChecked(value
)
332 def set_field_defaults(self
):
333 """Set sensible initial defaults"""
334 # Default to "git fetch origin master"
336 if action
in (FETCH
, PULL
):
337 self
.local_branch
.setText('')
338 self
.remote_branch
.setText('')
341 # Select the current branch by default for push
343 branch
= self
.model
.currentbranch
345 idx
= self
.model
.local_branches
.index(branch
)
348 if self
.select_local_branch(idx
):
349 self
.set_local_branch(branch
)
350 self
.set_remote_branch('')
352 def set_remote_name(self
, remote_name
):
353 """Set the remote name"""
354 self
.remote_name
.setText(remote_name
)
356 def set_local_branch(self
, branch
):
357 """Set the local branch name"""
358 self
.local_branch
.setText(branch
)
360 self
.local_branch
.selectAll()
362 def set_remote_branch(self
, branch
):
363 """Set the remote branch name"""
364 self
.remote_branch
.setText(branch
)
366 self
.remote_branch
.selectAll()
368 def set_remote_branches(self
, branches
):
369 """Set the list of remote branches"""
370 self
.remote_branches
.clear()
371 self
.remote_branches
.addItems(branches
)
372 self
.filtered_remote_branches
= branches
373 qtutils
.add_completer(self
.remote_branch
, strip_remotes(branches
))
375 def select_first_remote(self
):
376 """Select the first remote in the list view"""
377 return self
.select_remote(0)
379 def select_remote(self
, idx
):
380 """Select a remote by index"""
381 item
= self
.remotes
.item(idx
)
383 item
.setSelected(True)
384 self
.remotes
.setCurrentItem(item
)
385 self
.set_remote_name(item
.text())
391 def select_remote_by_name(self
, remote
):
392 """Select a remote by name"""
393 remotes
= self
.model
.remotes
394 if remote
in remotes
:
395 idx
= remotes
.index(remote
)
396 result
= self
.select_remote(idx
)
401 def set_selected_remotes(self
, remotes
):
402 """Set the list of selected remotes
404 Return True if all remotes were found and selected.
407 # Invalid remote names are ignored.
408 # This handles a remote going away between sessions.
409 # The selection is unchanged when none of the specified remotes exist.
411 for remote
in remotes
:
412 if remote
in self
.model
.remotes
:
416 # Only clear the selection if the specified remotes exist
417 self
.remotes
.clearSelection()
418 found
= all([self
.select_remote_by_name(x
) for x
in remotes
])
421 def select_local_branch(self
, idx
):
422 """Selects a local branch by index in the list view"""
423 item
= self
.local_branches
.item(idx
)
425 item
.setSelected(True)
426 self
.local_branches
.setCurrentItem(item
)
427 self
.local_branch
.setText(item
.text())
433 def display_remotes(self
, widget
):
434 """Display the available remotes in a listwidget"""
436 for remote_name
in self
.model
.remotes
:
437 url
= self
.model
.remote_url(remote_name
, self
.action
)
438 display
= ('%s\t(%s)'
439 % (remote_name
, N_('URL: %s') % url
))
440 displayed
.append(display
)
441 qtutils
.set_items(widget
, displayed
)
443 def update_remotes(self
):
444 """Update the remote name when a remote from the list is selected"""
445 widget
= self
.remotes
446 remotes
= self
.model
.remotes
447 selection
= qtutils
.selected_item(widget
, remotes
)
449 self
.selected_remotes
= []
451 self
.set_remote_name(selection
)
452 self
.selected_remotes
= qtutils
.selected_items(self
.remotes
,
454 self
.set_remote_to(selection
, self
.selected_remotes
)
456 def set_remote_to(self
, _remote
, selected_remotes
):
457 context
= self
.context
458 all_branches
= gitcmds
.branch_list(context
, remote
=True)
461 for remote_name
in selected_remotes
:
462 patterns
.append(remote_name
+ '/*')
464 for branch
in all_branches
:
466 if fnmatch
.fnmatch(branch
, pat
):
467 branches
.append(branch
)
470 self
.set_remote_branches(branches
)
472 self
.set_remote_branches(all_branches
)
473 self
.set_remote_branch('')
475 def remote_name_edited(self
):
476 """Update the current remote when the remote name is typed manually"""
477 remote
= self
.remote_name
.text()
478 self
.set_remote_to(remote
, [remote
])
480 def update_local_branches(self
):
481 """Update the local/remote branch names when a branch is selected"""
482 branches
= self
.model
.local_branches
483 widget
= self
.local_branches
484 selection
= qtutils
.selected_item(widget
, branches
)
487 self
.set_local_branch(selection
)
488 self
.set_remote_branch(selection
)
490 def update_remote_branches(self
):
491 """Update the remote branch name when a branch is selected"""
492 widget
= self
.remote_branches
493 branches
= self
.filtered_remote_branches
494 selection
= qtutils
.selected_item(widget
, branches
)
497 branch
= utils
.strip_one(selection
)
500 self
.set_remote_branch(branch
)
502 def common_args(self
):
503 """Returns git arguments common to fetch/push/pull"""
504 remote_name
= self
.remote_name
.text()
505 local_branch
= self
.local_branch
.text()
506 remote_branch
= self
.remote_branch
.text()
508 ff_only
= get(self
.ff_only_checkbox
)
509 force
= get(self
.force_checkbox
)
510 no_ff
= get(self
.no_ff_checkbox
)
511 rebase
= get(self
.rebase_checkbox
)
512 set_upstream
= get(self
.upstream_checkbox
)
513 tags
= get(self
.tags_checkbox
)
514 prune
= get(self
.prune_checkbox
)
520 'local_branch': local_branch
,
523 'remote_branch': remote_branch
,
524 'set_upstream': set_upstream
,
531 def push_to_all(self
, _remote
, *args
, **kwargs
):
532 """Push to all selected remotes"""
533 selected_remotes
= self
.selected_remotes
535 for remote
in selected_remotes
:
536 result
= self
.model
.push(remote
, *args
, **kwargs
)
537 all_results
= combine(result
, all_results
)
540 def action_callback(self
):
541 """Perform the actual fetch/push/pull operation"""
544 model_action
= self
.model
.fetch
546 model_action
= self
.push_to_all
547 else: # if action == PULL:
548 model_action
= self
.model
.pull
550 remote_name
= self
.remote_name
.text()
552 errmsg
= N_('No repository selected.')
553 Interaction
.log(errmsg
)
555 remote
, kwargs
= self
.common_args()
556 self
.selected_remotes
= qtutils
.selected_items(self
.remotes
,
559 # Check if we're about to create a new branch and warn.
560 remote_branch
= self
.remote_branch
.text()
561 local_branch
= self
.local_branch
.text()
563 if action
== PUSH
and not remote_branch
:
564 branch
= local_branch
565 candidate
= '%s/%s' % (remote
, branch
)
566 prompt
= get(self
.prompt_checkbox
)
568 if prompt
and candidate
not in self
.model
.remote_branches
:
570 args
= dict(branch
=branch
, remote
=remote
)
571 msg
= N_('Branch "%(branch)s" does not exist in "%(remote)s".\n'
572 'A new remote branch will be published.') % args
573 info_txt
= N_('Create a new remote branch?')
574 ok_text
= N_('Create Remote Branch')
575 if not Interaction
.confirm(title
, msg
, info_txt
, ok_text
,
579 if get(self
.force_checkbox
):
581 title
= N_('Force Fetch?')
582 msg
= N_('Non-fast-forward fetch overwrites local history!')
583 info_txt
= N_('Force fetching from %s?') % remote
584 ok_text
= N_('Force Fetch')
586 title
= N_('Force Push?')
587 msg
= N_('Non-fast-forward push overwrites published '
588 'history!\n(Did you pull first?)')
589 info_txt
= N_('Force push to %s?') % remote
590 ok_text
= N_('Force Push')
591 else: # pull: shouldn't happen since the controls are hidden
593 if not Interaction
.confirm(title
, msg
, info_txt
, ok_text
,
594 default
=False, icon
=icons
.discard()):
597 # Disable the GUI by default
598 self
.buttons
.setEnabled(False)
600 # Use a thread to update in the background
601 task
= ActionTask(self
, model_action
, remote
, kwargs
)
602 self
.runtask
.start(task
,
603 progress
=self
.progress
,
604 finish
=self
.action_completed
)
606 def action_completed(self
, task
):
607 """Grab the results of the action and finish up"""
608 status
, out
, err
= task
.result
609 self
.buttons
.setEnabled(True)
611 command
= 'git %s' % self
.action
.lower()
612 message
= Interaction
.format_command_status(command
, status
)
613 details
= Interaction
.format_out_err(out
, err
)
615 log_message
= message
617 log_message
+= '\n\n' + details
618 Interaction
.log(log_message
)
624 if self
.action
== PUSH
:
626 message
+= N_('Have you rebased/pulled lately?')
628 Interaction
.critical(self
.windowTitle(),
629 message
=message
, details
=details
)
632 # Use distinct classes so that each saves its own set of preferences
633 class Fetch(RemoteActionDialog
):
634 """Fetch from remote repositories"""
636 def __init__(self
, context
, model
, parent
=None):
637 super(Fetch
, self
).__init
__(
638 context
, model
, FETCH
, N_('Fetch'),
639 parent
=parent
, icon
=icons
.repo())
641 def export_state(self
):
642 """Export persistent settings"""
643 state
= RemoteActionDialog
.export_state(self
)
644 state
['tags'] = get(self
.tags_checkbox
)
645 state
['prune'] = get(self
.prune_checkbox
)
648 def apply_state(self
, state
):
649 """Apply persistent settings"""
650 result
= RemoteActionDialog
.apply_state(self
, state
)
651 tags
= bool(state
.get('tags', False))
652 self
.tags_checkbox
.setChecked(tags
)
653 prune
= bool(state
.get('prune', False))
654 self
.prune_checkbox
.setChecked(prune
)
658 class Push(RemoteActionDialog
):
659 """Push to remote repositories"""
661 def __init__(self
, context
, model
, parent
=None):
662 super(Push
, self
).__init
__(
663 context
, model
, PUSH
, N_('Push'), parent
=parent
, icon
=icons
.push())
665 def export_state(self
):
666 """Export persistent settings"""
667 state
= RemoteActionDialog
.export_state(self
)
668 state
['prompt'] = get(self
.prompt_checkbox
)
669 state
['remote'] = get(self
.remote_name
)
670 state
['selection'] = self
.selected_remotes
671 state
['tags'] = get(self
.tags_checkbox
)
674 def apply_state(self
, state
):
675 """Apply persistent settings"""
676 result
= RemoteActionDialog
.apply_state(self
, state
)
678 # Restore the "prompt on creation" checkbox
679 prompt
= bool(state
.get('prompt', True))
680 self
.prompt_checkbox
.setChecked(prompt
)
682 # Restore the "remote" text
683 remote
= state
.get('remote', None)
684 if remote
is not None:
685 self
.set_remote_name(remote
)
687 # Restore selected remotes
688 selection
= state
.get('selection', [])
689 self
.set_selected_remotes(selection
)
691 # Restore the "tags" checkbox
692 tags
= bool(state
.get('tags', False))
693 self
.tags_checkbox
.setChecked(tags
)
698 class Pull(RemoteActionDialog
):
699 """Pull from remote repositories"""
701 def __init__(self
, context
, model
, parent
=None):
702 super(Pull
, self
).__init
__(
703 context
, model
, PULL
, N_('Pull'), parent
=parent
, icon
=icons
.pull())
705 def apply_state(self
, state
):
706 """Apply persistent settings"""
707 result
= RemoteActionDialog
.apply_state(self
, state
)
708 # Rebase has the highest priority
709 rebase
= bool(state
.get('rebase', False))
710 ff_only
= not rebase
and bool(state
.get('ff_only', False))
711 no_ff
= not rebase
and not ff_only
and bool(state
.get('no_ff', False))
713 self
.rebase_checkbox
.setChecked(rebase
)
714 self
.no_ff_checkbox
.setChecked(no_ff
)
716 # Allow users coming from older versions that have rebase=False to
717 # pickup the new ff_only=True default by only setting ff_only False
718 # when it either exists in the config or when rebase=True.
719 if 'ff_only' in state
or rebase
:
720 self
.ff_only_checkbox
.setChecked(ff_only
)
724 def export_state(self
):
725 """Export persistent settings"""
726 state
= RemoteActionDialog
.export_state(self
)
728 state
['ff_only'] = get(self
.ff_only_checkbox
)
729 state
['no_ff'] = get(self
.no_ff_checkbox
)
730 state
['rebase'] = get(self
.rebase_checkbox
)