prefs: apply flake8 suggestions
[git-cola.git] / cola / widgets / remote.py
blob73a2e03a1b0288153a16d7629d60822bf416849a
1 from __future__ import division, absolute_import, unicode_literals
3 import fnmatch
5 from PyQt4 import QtGui
6 from PyQt4.QtCore import Qt
7 from PyQt4.QtCore import SIGNAL
9 from cola import gitcmds
10 from cola import icons
11 from cola import qtutils
12 from cola import utils
13 from cola.i18n import N_
14 from cola.interaction import Interaction
15 from cola.models import main
16 from cola.qtutils import connect_button
17 from cola.widgets import defs
18 from cola.widgets import standard
19 from cola.widgets.standard import ProgressDialog
22 FETCH = 'FETCH'
23 PUSH = 'PUSH'
24 PULL = 'PULL'
27 def fetch():
28 return run(Fetch)
31 def push():
32 return run(Push)
35 def pull():
36 return run(Pull)
39 def run(RemoteDialog):
40 """Launches fetch/push/pull dialogs."""
41 # Copy global stuff over to speedup startup
42 model = main.MainModel()
43 global_model = main.model()
44 model.currentbranch = global_model.currentbranch
45 model.local_branches = global_model.local_branches
46 model.remote_branches = global_model.remote_branches
47 model.tags = global_model.tags
48 model.remotes = global_model.remotes
49 parent = qtutils.active_window()
50 view = RemoteDialog(model, parent=parent)
51 view.show()
52 return view
55 def combine(result, existing):
56 if existing is None:
57 return result
59 if type(existing) is tuple:
60 if len(existing) == 3:
61 return (max(existing[0], result[0]),
62 combine(existing[1], result[1]),
63 combine(existing[2], result[2]))
64 else:
65 raise AssertionError('combine() with length %d' % len(existing))
66 else:
67 if existing and result:
68 return existing + '\n\n' + result
69 elif existing:
70 return existing
71 else:
72 return result
75 class ActionTask(qtutils.Task):
77 def __init__(self, parent, model_action, remote, kwargs):
78 qtutils.Task.__init__(self, parent)
79 self.model_action = model_action
80 self.remote = remote
81 self.kwargs = kwargs
83 def task(self):
84 """Runs the model action and captures the result"""
85 return self.model_action(self.remote, **self.kwargs)
88 class RemoteActionDialog(standard.Dialog):
90 def __init__(self, model, action, title, parent=None, icon=None):
91 """Customizes the dialog based on the remote action
92 """
93 standard.Dialog.__init__(self, parent=parent)
94 self.model = model
95 self.action = action
96 self.filtered_remote_branches = []
97 self.selected_remotes = []
99 self.setAttribute(Qt.WA_MacMetalStyle)
100 self.setWindowTitle(title)
101 if parent is not None:
102 self.setWindowModality(Qt.WindowModal)
104 self.runtask = qtutils.RunTask(parent=self)
105 self.progress = ProgressDialog(title, N_('Updating'), self)
107 self.local_label = QtGui.QLabel()
108 self.local_label.setText(N_('Local Branch'))
110 self.local_branch = QtGui.QLineEdit()
111 self.local_branches = QtGui.QListWidget()
112 self.local_branches.addItems(self.model.local_branches)
114 self.remote_label = QtGui.QLabel()
115 self.remote_label.setText(N_('Remote'))
117 self.remote_name = QtGui.QLineEdit()
118 self.remotes = QtGui.QListWidget()
119 if action == PUSH:
120 self.remotes.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
121 self.remotes.addItems(self.model.remotes)
123 self.remote_branch_label = QtGui.QLabel()
124 self.remote_branch_label.setText(N_('Remote Branch'))
126 self.remote_branch = QtGui.QLineEdit()
127 self.remote_branches = QtGui.QListWidget()
128 self.remote_branches.addItems(self.model.remote_branches)
130 text = N_('Fast Forward Only ')
131 self.ffwd_only_checkbox = qtutils.checkbox(text=text, checked=True)
132 self.tags_checkbox = qtutils.checkbox(text=N_('Include tags '))
133 self.rebase_checkbox = qtutils.checkbox(text=N_('Rebase '))
135 if icon is None:
136 icon = icons.ok()
137 self.action_button = qtutils.create_button(text=title, icon=icon)
138 self.close_button = qtutils.close_button()
140 self.buttons = utils.Group(self.action_button, self.close_button)
142 self.local_branch_layout = qtutils.hbox(defs.small_margin, defs.spacing,
143 self.local_label,
144 self.local_branch)
146 self.remote_branch_layout = qtutils.hbox(defs.small_margin, defs.spacing,
147 self.remote_label,
148 self.remote_name)
150 self.remote_branches_layout = qtutils.hbox(defs.small_margin, defs.spacing,
151 self.remote_branch_label,
152 self.remote_branch)
154 self.options_layout = qtutils.hbox(defs.no_margin, defs.button_spacing,
155 qtutils.STRETCH,
156 self.ffwd_only_checkbox,
157 self.tags_checkbox,
158 self.rebase_checkbox,
159 self.action_button,
160 self.close_button)
161 if action == PUSH:
162 widgets = (
163 self.remote_branch_layout, self.remotes,
164 self.local_branch_layout, self.local_branches,
165 self.remote_branches_layout, self.remote_branches,
166 self.options_layout,
168 else: # fetch and pull
169 widgets = (
170 self.remote_branch_layout, self.remotes,
171 self.remote_branches_layout, self.remote_branches,
172 self.local_branch_layout, self.local_branches,
173 self.options_layout,
175 self.main_layout = qtutils.vbox(defs.no_margin, defs.spacing, *widgets)
176 self.setLayout(self.main_layout)
178 default_remote = gitcmds.default_remote() or 'origin'
180 remotes = self.model.remotes
181 if default_remote in remotes:
182 idx = remotes.index(default_remote)
183 if self.select_remote(idx):
184 self.remote_name.setText(default_remote)
185 else:
186 if self.select_first_remote():
187 self.remote_name.setText(remotes[0])
189 # Trim the remote list to just the default remote
190 self.update_remotes()
191 self.set_field_defaults()
193 # Setup signals and slots
194 self.connect(self.remotes, SIGNAL('itemSelectionChanged()'),
195 self.update_remotes)
197 self.connect(self.local_branches, SIGNAL('itemSelectionChanged()'),
198 self.update_local_branches)
200 self.connect(self.remote_branches, SIGNAL('itemSelectionChanged()'),
201 self.update_remote_branches)
203 connect_button(self.action_button, self.action_callback)
204 connect_button(self.close_button, self.close)
206 qtutils.add_action(self, N_('Close'), self.close,
207 QtGui.QKeySequence.Close, 'Esc')
209 if action == PULL:
210 self.tags_checkbox.hide()
211 self.ffwd_only_checkbox.hide()
212 self.local_label.hide()
213 self.local_branch.hide()
214 self.local_branches.hide()
215 self.remote_branch.setFocus()
216 else:
217 self.rebase_checkbox.hide()
219 if not self.restore_state():
220 self.resize(666, 420)
222 self.remote_name.setFocus()
224 def set_rebase(self, value):
225 self.rebase_checkbox.setChecked(value)
227 def set_field_defaults(self):
228 # Default to "git fetch origin master"
229 action = self.action
230 if action == FETCH or action == PULL:
231 self.local_branch.setText('')
232 self.remote_branch.setText('')
233 return
235 # Select the current branch by default for push
236 if action == PUSH:
237 branch = self.model.currentbranch
238 try:
239 idx = self.model.local_branches.index(branch)
240 except ValueError:
241 return
242 if self.select_local_branch(idx):
243 self.set_local_branch(branch)
244 self.set_remote_branch('')
246 def set_remote_name(self, remote_name):
247 self.remote_name.setText(remote_name)
248 if remote_name:
249 self.remote_name.selectAll()
251 def set_local_branch(self, branch):
252 self.local_branch.setText(branch)
253 if branch:
254 self.local_branch.selectAll()
256 def set_remote_branch(self, branch):
257 self.remote_branch.setText(branch)
258 if branch:
259 self.remote_branch.selectAll()
261 def set_remote_branches(self, branches):
262 self.remote_branches.clear()
263 self.remote_branches.addItems(branches)
264 self.filtered_remote_branches = branches
266 def select_first_remote(self):
267 """Selects the first remote in the list view"""
268 return self.select_remote(0)
270 def select_remote(self, idx):
271 """Selects a remote by index"""
272 item = self.remotes.item(idx)
273 if item:
274 self.remotes.setItemSelected(item, True)
275 self.remotes.setCurrentItem(item)
276 self.set_remote_name(item.text())
277 return True
278 else:
279 return False
281 def select_local_branch(self, idx):
282 """Selects a local branch by index in the list view"""
283 item = self.local_branches.item(idx)
284 if not item:
285 return False
286 self.local_branches.setItemSelected(item, True)
287 self.local_branches.setCurrentItem(item)
288 self.local_branch.setText(item.text())
289 return True
291 def display_remotes(self, widget):
292 """Display the available remotes in a listwidget"""
293 displayed = []
294 for remote_name in self.model.remotes:
295 url = self.model.remote_url(remote_name, self.action)
296 display = ('%s\t(%s)'
297 % (remote_name, N_('URL: %s') % url))
298 displayed.append(display)
299 qtutils.set_items(widget,displayed)
301 def update_remotes(self, *rest):
302 """Update the remote name when a remote from the list is selected"""
303 widget = self.remotes
304 remotes = self.model.remotes
305 selection = qtutils.selected_item(widget, remotes)
306 if not selection:
307 self.selected_remotes = []
308 return
309 self.set_remote_name(selection)
310 self.selected_remotes = qtutils.selected_items(self.remotes,
311 self.model.remotes)
313 all_branches = gitcmds.branch_list(remote=True)
314 branches = []
315 patterns = []
316 for remote in self.selected_remotes:
317 pat = remote + '/*'
318 patterns.append(pat)
320 for branch in all_branches:
321 for pat in patterns:
322 if fnmatch.fnmatch(branch, pat):
323 branches.append(branch)
324 break
325 if branches:
326 self.set_remote_branches(branches)
327 else:
328 self.set_remote_branches(all_branches)
329 self.set_remote_branch('')
331 def update_local_branches(self,*rest):
332 """Update the local/remote branch names when a branch is selected"""
333 branches = self.model.local_branches
334 widget = self.local_branches
335 selection = qtutils.selected_item(widget, branches)
336 if not selection:
337 return
338 self.set_local_branch(selection)
339 self.set_remote_branch(selection)
341 def update_remote_branches(self,*rest):
342 """Update the remote branch name when a branch is selected"""
343 widget = self.remote_branches
344 branches = self.filtered_remote_branches
345 selection = qtutils.selected_item(widget, branches)
346 if not selection:
347 return
348 branch = utils.strip_one(selection)
349 if branch == 'HEAD':
350 return
351 self.set_remote_branch(branch)
353 def common_args(self):
354 """Returns git arguments common to fetch/push/pulll"""
355 remote_name = self.remote_name.text()
356 local_branch = self.local_branch.text()
357 remote_branch = self.remote_branch.text()
359 ffwd_only = self.ffwd_only_checkbox.isChecked()
360 rebase = self.rebase_checkbox.isChecked()
361 tags = self.tags_checkbox.isChecked()
363 return (remote_name,
365 'local_branch': local_branch,
366 'remote_branch': remote_branch,
367 'ffwd': ffwd_only,
368 'rebase': rebase,
369 'tags': tags,
372 # Actions
374 def push_to_all(self, dummy_remote, *args, **kwargs):
375 selected_remotes = self.selected_remotes
376 all_results = None
377 for remote in selected_remotes:
378 result = self.model.push(remote, *args, **kwargs)
379 all_results = combine(result, all_results)
380 return all_results
382 def action_callback(self):
383 action = self.action
384 if action == FETCH:
385 model_action = self.model.fetch
386 elif action == PUSH:
387 model_action = self.push_to_all
388 else: # if action == PULL:
389 model_action = self.model.pull
391 remote_name = self.remote_name.text()
392 if not remote_name:
393 errmsg = N_('No repository selected.')
394 Interaction.log(errmsg)
395 return
396 remote, kwargs = self.common_args()
397 self.selected_remotes = qtutils.selected_items(self.remotes,
398 self.model.remotes)
400 # Check if we're about to create a new branch and warn.
401 remote_branch = self.remote_branch.text()
402 local_branch = self.local_branch.text()
404 if action == PUSH and not remote_branch:
405 branch = local_branch
406 candidate = '%s/%s' % (remote, branch)
407 if candidate not in self.model.remote_branches:
408 title = N_('Push')
409 args = dict(branch=branch, remote=remote)
410 msg = N_('Branch "%(branch)s" does not exist in "%(remote)s".\n'
411 'A new remote branch will be published.') % args
412 info_txt= N_('Create a new remote branch?')
413 ok_text = N_('Create Remote Branch')
414 if not qtutils.confirm(title, msg, info_txt, ok_text,
415 icon=icons.cola()):
416 return
418 if not self.ffwd_only_checkbox.isChecked():
419 if action == FETCH:
420 title = N_('Force Fetch?')
421 msg = N_('Non-fast-forward fetch overwrites local history!')
422 info_txt = N_('Force fetching from %s?') % remote
423 ok_text = N_('Force Fetch')
424 elif action == PUSH:
425 title = N_('Force Push?')
426 msg = N_('Non-fast-forward push overwrites published '
427 'history!\n(Did you pull first?)')
428 info_txt = N_('Force push to %s?') % remote
429 ok_text = N_('Force Push')
430 else: # pull: shouldn't happen since the controls are hidden
431 msg = "You probably don't want to do this.\n\tContinue?"
432 return
434 if not qtutils.confirm(title, msg, info_txt, ok_text,
435 default=False, icon=icons.discard()):
436 return
438 # Disable the GUI by default
439 self.buttons.setEnabled(False)
441 # Use a thread to update in the background
442 task = ActionTask(self, model_action, remote, kwargs)
443 self.runtask.start(task,
444 progress=self.progress,
445 finish=self.action_completed)
447 def action_completed(self, task):
448 # Grab the results of the action and finish up
449 status, out, err = task.result
450 self.buttons.setEnabled(True)
452 already_up_to_date = N_('Already up-to-date.')
454 if not out: # git fetch --tags --verbose doesn't print anything...
455 out = already_up_to_date
457 command = 'git %s' % self.action.lower()
458 message = (N_('"%(command)s" returned exit status %(status)d') %
459 dict(command=command, status=status))
460 details = ''
461 if out:
462 details = out
463 if err:
464 details += '\n\n' + err
466 log_message = message
467 if details:
468 log_message += '\n\n' + details
469 Interaction.log(log_message)
471 if status == 0:
472 self.accept()
473 return
475 if self.action == PUSH:
476 message += '\n\n'
477 message += N_('Have you rebased/pulled lately?')
479 Interaction.critical(self.windowTitle(),
480 message=message, details=details)
483 # Use distinct classes so that each saves its own set of preferences
484 class Fetch(RemoteActionDialog):
486 def __init__(self, model, parent=None):
487 RemoteActionDialog.__init__(self, model, FETCH, N_('Fetch'),
488 parent=parent, icon=icons.repo())
491 class Push(RemoteActionDialog):
493 def __init__(self, model, parent=None):
494 RemoteActionDialog.__init__(self, model, PUSH, N_('Push'),
495 parent=parent, icon=icons.push())
498 class Pull(RemoteActionDialog):
500 def __init__(self, model, parent=None):
501 RemoteActionDialog.__init__(self, model, PULL, N_('Pull'),
502 parent=parent, icon=icons.pull())
504 def apply_state(self, state):
505 result = RemoteActionDialog.apply_state(self, state)
506 try:
507 rebase = state['rebase']
508 except KeyError:
509 result = False
510 else:
511 self.rebase_checkbox.setChecked(rebase)
512 return result
514 def export_state(self):
515 state = RemoteActionDialog.export_state(self)
516 state['rebase'] = self.rebase_checkbox.isChecked()
517 return state
519 def done(self, exit_code):
520 self.save_state()
521 return RemoteActionDialog.done(self, exit_code)