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
.no_margin
, defs
.spacing
,
268 self
.top_layout
, self
.options_layout
)
269 self
.setLayout(self
.main_layout
)
271 default_remote
= gitcmds
.upstream_remote(context
) or 'origin'
273 remotes
= self
.model
.remotes
274 if default_remote
in remotes
:
275 idx
= remotes
.index(default_remote
)
276 if self
.select_remote(idx
):
277 self
.set_remote_name(default_remote
)
279 if self
.select_first_remote():
280 self
.set_remote_name(remotes
[0])
282 # Trim the remote list to just the default remote
283 self
.update_remotes()
284 self
.set_field_defaults()
286 # Setup signals and slots
287 self
.remotes
.itemSelectionChanged
.connect(self
.update_remotes
)
289 local
= self
.local_branches
290 local
.itemSelectionChanged
.connect(self
.update_local_branches
)
292 remote
= self
.remote_branches
293 remote
.itemSelectionChanged
.connect(self
.update_remote_branches
)
295 self
.no_ff_checkbox
.toggled
.connect(
296 lambda x
: uncheck(x
, self
.ff_only_checkbox
, self
.rebase_checkbox
))
298 self
.ff_only_checkbox
.toggled
.connect(
299 lambda x
: uncheck(x
, self
.no_ff_checkbox
, self
.rebase_checkbox
))
301 self
.rebase_checkbox
.toggled
.connect(
302 lambda x
: uncheck(x
, self
.no_ff_checkbox
, self
.ff_only_checkbox
))
304 connect_button(self
.action_button
, self
.action_callback
)
305 connect_button(self
.close_button
, self
.close
)
307 qtutils
.add_action(self
, N_('Close'), self
.close
,
308 QtGui
.QKeySequence
.Close
, 'Esc')
310 self
.prune_checkbox
.hide()
314 self
.upstream_checkbox
.hide()
315 self
.prompt_checkbox
.hide()
318 # Fetch and Push-only options
319 self
.force_checkbox
.hide()
320 self
.tags_checkbox
.hide()
321 self
.local_label
.hide()
322 self
.local_branch
.hide()
323 self
.local_branches
.hide()
326 self
.rebase_checkbox
.hide()
327 self
.no_ff_checkbox
.hide()
328 self
.ff_only_checkbox
.hide()
330 self
.init_size(parent
=parent
)
332 def set_rebase(self
, value
):
333 """Check the rebase checkbox"""
334 self
.rebase_checkbox
.setChecked(value
)
336 def set_field_defaults(self
):
337 """Set sensible initial defaults"""
338 # Default to "git fetch origin master"
340 if action
in (FETCH
, PULL
):
341 self
.local_branch
.setText('')
342 self
.remote_branch
.setText('')
345 # Select the current branch by default for push
347 branch
= self
.model
.currentbranch
349 idx
= self
.model
.local_branches
.index(branch
)
352 if self
.select_local_branch(idx
):
353 self
.set_local_branch(branch
)
354 self
.set_remote_branch('')
356 def set_remote_name(self
, remote_name
):
357 """Set the remote name"""
358 self
.remote_name
.setText(remote_name
)
360 def set_local_branch(self
, branch
):
361 """Set the local branch name"""
362 self
.local_branch
.setText(branch
)
364 self
.local_branch
.selectAll()
366 def set_remote_branch(self
, branch
):
367 """Set the remote branch name"""
368 self
.remote_branch
.setText(branch
)
370 self
.remote_branch
.selectAll()
372 def set_remote_branches(self
, branches
):
373 """Set the list of remote branches"""
374 self
.remote_branches
.clear()
375 self
.remote_branches
.addItems(branches
)
376 self
.filtered_remote_branches
= branches
377 qtutils
.add_completer(self
.remote_branch
, strip_remotes(branches
))
379 def select_first_remote(self
):
380 """Select the first remote in the list view"""
381 return self
.select_remote(0)
383 def select_remote(self
, idx
):
384 """Select a remote by index"""
385 item
= self
.remotes
.item(idx
)
387 item
.setSelected(True)
388 self
.remotes
.setCurrentItem(item
)
389 self
.set_remote_name(item
.text())
395 def select_local_branch(self
, idx
):
396 """Selects a local branch by index in the list view"""
397 item
= self
.local_branches
.item(idx
)
399 item
.setSelected(True)
400 self
.local_branches
.setCurrentItem(item
)
401 self
.local_branch
.setText(item
.text())
407 def display_remotes(self
, widget
):
408 """Display the available remotes in a listwidget"""
410 for remote_name
in self
.model
.remotes
:
411 url
= self
.model
.remote_url(remote_name
, self
.action
)
412 display
= ('%s\t(%s)'
413 % (remote_name
, N_('URL: %s') % url
))
414 displayed
.append(display
)
415 qtutils
.set_items(widget
, displayed
)
417 def update_remotes(self
):
418 """Update the remote name when a remote from the list is selected"""
419 widget
= self
.remotes
420 remotes
= self
.model
.remotes
421 selection
= qtutils
.selected_item(widget
, remotes
)
423 self
.selected_remotes
= []
425 self
.set_remote_name(selection
)
426 self
.selected_remotes
= qtutils
.selected_items(self
.remotes
,
428 self
.set_remote_to(selection
, self
.selected_remotes
)
430 def set_remote_to(self
, _remote
, selected_remotes
):
431 context
= self
.context
432 all_branches
= gitcmds
.branch_list(context
, remote
=True)
435 for remote_name
in selected_remotes
:
436 patterns
.append(remote_name
+ '/*')
438 for branch
in all_branches
:
440 if fnmatch
.fnmatch(branch
, pat
):
441 branches
.append(branch
)
444 self
.set_remote_branches(branches
)
446 self
.set_remote_branches(all_branches
)
447 self
.set_remote_branch('')
449 def remote_name_edited(self
):
450 """Update the current remote when the remote name is typed manually"""
451 remote
= self
.remote_name
.text()
452 self
.set_remote_to(remote
, [remote
])
454 def update_local_branches(self
):
455 """Update the local/remote branch names when a branch is selected"""
456 branches
= self
.model
.local_branches
457 widget
= self
.local_branches
458 selection
= qtutils
.selected_item(widget
, branches
)
461 self
.set_local_branch(selection
)
462 self
.set_remote_branch(selection
)
464 def update_remote_branches(self
):
465 """Update the remote branch name when a branch is selected"""
466 widget
= self
.remote_branches
467 branches
= self
.filtered_remote_branches
468 selection
= qtutils
.selected_item(widget
, branches
)
471 branch
= utils
.strip_one(selection
)
474 self
.set_remote_branch(branch
)
476 def common_args(self
):
477 """Returns git arguments common to fetch/push/pull"""
478 remote_name
= self
.remote_name
.text()
479 local_branch
= self
.local_branch
.text()
480 remote_branch
= self
.remote_branch
.text()
482 ff_only
= get(self
.ff_only_checkbox
)
483 force
= get(self
.force_checkbox
)
484 no_ff
= get(self
.no_ff_checkbox
)
485 rebase
= get(self
.rebase_checkbox
)
486 set_upstream
= get(self
.upstream_checkbox
)
487 tags
= get(self
.tags_checkbox
)
488 prune
= get(self
.prune_checkbox
)
494 'local_branch': local_branch
,
497 'remote_branch': remote_branch
,
498 'set_upstream': set_upstream
,
505 def push_to_all(self
, _remote
, *args
, **kwargs
):
506 """Push to all selected remotes"""
507 selected_remotes
= self
.selected_remotes
509 for remote
in selected_remotes
:
510 result
= self
.model
.push(remote
, *args
, **kwargs
)
511 all_results
= combine(result
, all_results
)
514 def action_callback(self
):
515 """Perform the actual fetch/push/pull operation"""
518 model_action
= self
.model
.fetch
520 model_action
= self
.push_to_all
521 else: # if action == PULL:
522 model_action
= self
.model
.pull
524 remote_name
= self
.remote_name
.text()
526 errmsg
= N_('No repository selected.')
527 Interaction
.log(errmsg
)
529 remote
, kwargs
= self
.common_args()
530 self
.selected_remotes
= qtutils
.selected_items(self
.remotes
,
533 # Check if we're about to create a new branch and warn.
534 remote_branch
= self
.remote_branch
.text()
535 local_branch
= self
.local_branch
.text()
537 if action
== PUSH
and not remote_branch
:
538 branch
= local_branch
539 candidate
= '%s/%s' % (remote
, branch
)
540 prompt
= get(self
.prompt_checkbox
)
542 if prompt
and candidate
not in self
.model
.remote_branches
:
544 args
= dict(branch
=branch
, remote
=remote
)
545 msg
= N_('Branch "%(branch)s" does not exist in "%(remote)s".\n'
546 'A new remote branch will be published.') % args
547 info_txt
= N_('Create a new remote branch?')
548 ok_text
= N_('Create Remote Branch')
549 if not Interaction
.confirm(title
, msg
, info_txt
, ok_text
,
553 if get(self
.force_checkbox
):
555 title
= N_('Force Fetch?')
556 msg
= N_('Non-fast-forward fetch overwrites local history!')
557 info_txt
= N_('Force fetching from %s?') % remote
558 ok_text
= N_('Force Fetch')
560 title
= N_('Force Push?')
561 msg
= N_('Non-fast-forward push overwrites published '
562 'history!\n(Did you pull first?)')
563 info_txt
= N_('Force push to %s?') % remote
564 ok_text
= N_('Force Push')
565 else: # pull: shouldn't happen since the controls are hidden
567 if not Interaction
.confirm(title
, msg
, info_txt
, ok_text
,
568 default
=False, icon
=icons
.discard()):
571 # Disable the GUI by default
572 self
.buttons
.setEnabled(False)
574 # Use a thread to update in the background
575 task
= ActionTask(self
, model_action
, remote
, kwargs
)
576 self
.runtask
.start(task
,
577 progress
=self
.progress
,
578 finish
=self
.action_completed
)
580 def action_completed(self
, task
):
581 """Grab the results of the action and finish up"""
582 status
, out
, err
= task
.result
583 self
.buttons
.setEnabled(True)
585 command
= 'git %s' % self
.action
.lower()
586 message
= Interaction
.format_command_status(command
, status
)
587 details
= Interaction
.format_out_err(out
, err
)
589 log_message
= message
591 log_message
+= '\n\n' + details
592 Interaction
.log(log_message
)
598 if self
.action
== PUSH
:
600 message
+= N_('Have you rebased/pulled lately?')
602 Interaction
.critical(self
.windowTitle(),
603 message
=message
, details
=details
)
606 # Use distinct classes so that each saves its own set of preferences
607 class Fetch(RemoteActionDialog
):
608 """Fetch from remote repositories"""
610 def __init__(self
, context
, model
, parent
=None):
611 super(Fetch
, self
).__init
__(
612 context
, model
, FETCH
, N_('Fetch'),
613 parent
=parent
, icon
=icons
.repo())
615 def export_state(self
):
616 """Export persistent settings"""
617 state
= RemoteActionDialog
.export_state(self
)
618 state
['tags'] = get(self
.tags_checkbox
)
619 state
['prune'] = get(self
.prune_checkbox
)
622 def apply_state(self
, state
):
623 """Apply persistent settings"""
624 result
= RemoteActionDialog
.apply_state(self
, state
)
625 tags
= bool(state
.get('tags', False))
626 self
.tags_checkbox
.setChecked(tags
)
627 prune
= bool(state
.get('prune', False))
628 self
.prune_checkbox
.setChecked(prune
)
632 class Push(RemoteActionDialog
):
633 """Push to remote repositories"""
635 def __init__(self
, context
, model
, parent
=None):
636 super(Push
, self
).__init
__(
637 context
, model
, PUSH
, N_('Push'), parent
=parent
, icon
=icons
.push())
639 def export_state(self
):
640 """Export persistent settings"""
641 state
= RemoteActionDialog
.export_state(self
)
642 state
['tags'] = get(self
.tags_checkbox
)
643 state
['prompt'] = get(self
.prompt_checkbox
)
646 def apply_state(self
, state
):
647 """Apply persistent settings"""
648 result
= RemoteActionDialog
.apply_state(self
, state
)
650 tags
= bool(state
.get('tags', False))
651 self
.tags_checkbox
.setChecked(tags
)
653 prompt
= bool(state
.get('prompt', True))
654 self
.prompt_checkbox
.setChecked(prompt
)
658 class Pull(RemoteActionDialog
):
659 """Pull from remote repositories"""
661 def __init__(self
, context
, model
, parent
=None):
662 super(Pull
, self
).__init
__(
663 context
, model
, PULL
, N_('Pull'), parent
=parent
, icon
=icons
.pull())
665 def apply_state(self
, state
):
666 """Apply persistent settings"""
667 result
= RemoteActionDialog
.apply_state(self
, state
)
668 # Rebase has the highest priority
669 rebase
= bool(state
.get('rebase', False))
670 ff_only
= not rebase
and bool(state
.get('ff_only', False))
671 no_ff
= not rebase
and not ff_only
and bool(state
.get('no_ff', False))
673 self
.rebase_checkbox
.setChecked(rebase
)
674 self
.no_ff_checkbox
.setChecked(no_ff
)
676 # Allow users coming from older versions that have rebase=False to
677 # pickup the new ff_only=True default by only setting ff_only False
678 # when it either exists in the config or when rebase=True.
679 if 'ff_only' in state
or rebase
:
680 self
.ff_only_checkbox
.setChecked(ff_only
)
684 def export_state(self
):
685 """Export persistent settings"""
686 state
= RemoteActionDialog
.export_state(self
)
688 state
['ff_only'] = get(self
.ff_only_checkbox
)
689 state
['no_ff'] = get(self
.no_ff_checkbox
)
690 state
['rebase'] = get(self
.rebase_checkbox
)