bookmarks: avoid duplicates between the recent and bookmarked lists
[git-cola.git] / cola / widgets / remote.py
blob2eb1d9fa3441b40cbd845a5fe46fb54428731e33
1 import fnmatch
2 import time
4 from PyQt4 import QtCore
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 qtutils
11 from cola import utils
12 from cola.i18n import N_
13 from cola.interaction import Interaction
14 from cola.models import main
15 from cola.qtutils import connect_button
16 from cola.widgets import defs
17 from cola.widgets import standard
19 FETCH = 'Fetch'
20 PUSH = 'Push'
21 PULL = 'Pull'
24 def fetch():
25 return run(Fetch)
28 def push():
29 return run(Push)
32 def pull():
33 return run(Pull)
36 def run(RemoteDialog):
37 """Launches fetch/push/pull dialogs."""
38 # Copy global stuff over to speedup startup
39 model = main.MainModel()
40 global_model = main.model()
41 model.currentbranch = global_model.currentbranch
42 model.local_branches = global_model.local_branches
43 model.remote_branches = global_model.remote_branches
44 model.tags = global_model.tags
45 model.remotes = global_model.remotes
46 parent = qtutils.active_window()
47 view = RemoteDialog(model, parent=parent)
48 view.show()
49 return view
52 def combine(result, existing):
53 if existing is None:
54 return result
56 if type(existing) is tuple:
57 if len(existing) == 3:
58 return (max(existing[0], result[0]),
59 combine(existing[1], result[1]),
60 combine(existing[2], result[2]))
61 else:
62 raise AssertionError('combine() with length %d' % len(existing))
63 else:
64 if existing and result:
65 return existing + '\n\n' + result
66 elif existing:
67 return existing
68 else:
69 return result
72 class ActionTask(QtCore.QRunnable):
74 def __init__(self, sender, model_action, remote, kwargs):
75 QtCore.QRunnable.__init__(self)
76 self.sender = sender
77 self.model_action = model_action
78 self.remote = remote
79 self.kwargs = kwargs
81 def run(self):
82 """Runs the model action and captures the result"""
83 status, out, err = self.model_action(self.remote, **self.kwargs)
84 self.sender.emit(SIGNAL('action_completed'), self, status, out, err)
87 class ProgressAnimationThread(QtCore.QThread):
89 def __init__(self, txt, parent, timeout=0.25):
90 QtCore.QThread.__init__(self, parent)
91 self.running = False
92 self.txt = txt
93 self.timeout = timeout
94 self.symbols = [
95 '.. ',
96 '... ',
97 '.... ',
98 '.....',
99 '.... ',
100 '... '
102 self.idx = -1
104 def next(self):
105 self.idx = (self.idx + 1) % len(self.symbols)
106 return self.txt + self.symbols[self.idx]
108 def stop(self):
109 self.running = False
111 def run(self):
112 self.running = True
113 while self.running:
114 self.emit(SIGNAL('str'), self.next())
115 time.sleep(self.timeout)
118 class RemoteActionDialog(standard.Dialog):
120 def __init__(self, model, action, parent=None):
121 """Customizes the dialog based on the remote action
123 standard.Dialog.__init__(self, parent=parent)
124 self.model = model
125 self.action = action
126 self.tasks = []
127 self.filtered_remote_branches = []
128 self.selected_remotes = []
130 self.setAttribute(Qt.WA_MacMetalStyle)
131 self.setWindowTitle(N_(action))
132 if parent is not None:
133 self.setWindowModality(Qt.WindowModal)
135 self.progress = QtGui.QProgressDialog(self)
136 self.progress.setFont(qtutils.diff_font())
137 self.progress.setRange(0, 0)
138 self.progress.setCancelButton(None)
139 self.progress.setWindowTitle(action)
140 self.progress.setWindowModality(Qt.WindowModal)
141 self.progress.setLabelText(N_('Updating') + '.. ')
142 self.progress_thread = ProgressAnimationThread(N_('Updating'), self)
144 self.local_label = QtGui.QLabel()
145 self.local_label.setText(N_('Local Branch'))
147 self.local_branch = QtGui.QLineEdit()
148 self.local_branches = QtGui.QListWidget()
149 self.local_branches.addItems(self.model.local_branches)
151 self.remote_label = QtGui.QLabel()
152 self.remote_label.setText(N_('Remote'))
154 self.remote_name = QtGui.QLineEdit()
155 self.remotes = QtGui.QListWidget()
156 if action == PUSH:
157 self.remotes.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
158 self.remotes.addItems(self.model.remotes)
160 self.remote_branch_label = QtGui.QLabel()
161 self.remote_branch_label.setText(N_('Remote Branch'))
163 self.remote_branch = QtGui.QLineEdit()
164 self.remote_branches = QtGui.QListWidget()
165 self.remote_branches.addItems(self.model.remote_branches)
167 self.ffwd_only_checkbox = QtGui.QCheckBox()
168 self.ffwd_only_checkbox.setText(N_('Fast Forward Only '))
169 self.ffwd_only_checkbox.setChecked(True)
171 self.tags_checkbox = QtGui.QCheckBox()
172 self.tags_checkbox.setText(N_('Include tags '))
174 self.rebase_checkbox = QtGui.QCheckBox()
175 self.rebase_checkbox.setText(N_('Rebase '))
177 self.action_button = QtGui.QPushButton()
178 self.action_button.setText(N_(action))
179 self.action_button.setIcon(qtutils.ok_icon())
181 self.close_button = QtGui.QPushButton()
182 self.close_button.setText(N_('Close'))
183 self.close_button.setIcon(qtutils.close_icon())
185 self.local_branch_layout = QtGui.QHBoxLayout()
186 self.local_branch_layout.addWidget(self.local_label)
187 self.local_branch_layout.addWidget(self.local_branch)
189 self.remote_branch_layout = QtGui.QHBoxLayout()
190 self.remote_branch_layout.addWidget(self.remote_label)
191 self.remote_branch_layout.addWidget(self.remote_name)
193 self.remote_branches_layout = QtGui.QHBoxLayout()
194 self.remote_branches_layout.addWidget(self.remote_branch_label)
195 self.remote_branches_layout.addWidget(self.remote_branch)
197 self.options_layout = QtGui.QHBoxLayout()
198 self.options_layout.setSpacing(defs.button_spacing)
199 self.options_layout.addStretch()
200 self.options_layout.addWidget(self.ffwd_only_checkbox)
201 self.options_layout.addWidget(self.tags_checkbox)
202 self.options_layout.addWidget(self.rebase_checkbox)
203 self.options_layout.addWidget(self.action_button)
204 self.options_layout.addWidget(self.close_button)
206 self.main_layout = QtGui.QVBoxLayout()
207 self.main_layout.setMargin(defs.margin)
208 self.main_layout.setSpacing(defs.spacing)
209 self.main_layout.addLayout(self.remote_branch_layout)
210 self.main_layout.addWidget(self.remotes)
211 if action == PUSH:
212 self.main_layout.addLayout(self.local_branch_layout)
213 self.main_layout.addWidget(self.local_branches)
214 self.main_layout.addLayout(self.remote_branches_layout)
215 self.main_layout.addWidget(self.remote_branches)
216 else: # fetch and pull
217 self.main_layout.addLayout(self.remote_branches_layout)
218 self.main_layout.addWidget(self.remote_branches)
219 self.main_layout.addLayout(self.local_branch_layout)
220 self.main_layout.addWidget(self.local_branches)
221 self.main_layout.addLayout(self.options_layout)
222 self.setLayout(self.main_layout)
224 remotes = self.model.remotes
225 if 'origin' in remotes:
226 idx = remotes.index('origin')
227 if self.select_remote(idx):
228 self.remote_name.setText('origin')
229 else:
230 if self.select_first_remote():
231 self.remote_name.setText(remotes[0])
233 # Trim the remote list to just the default remote
234 self.update_remotes()
235 self.set_field_defaults()
237 # Setup signals and slots
238 self.connect(self.remotes, SIGNAL('itemSelectionChanged()'),
239 self.update_remotes)
241 self.connect(self.local_branches, SIGNAL('itemSelectionChanged()'),
242 self.update_local_branches)
244 self.connect(self.remote_branches, SIGNAL('itemSelectionChanged()'),
245 self.update_remote_branches)
247 connect_button(self.action_button, self.action_callback)
248 connect_button(self.close_button, self.close)
250 qtutils.add_action(self, N_('Close'),
251 self.close, QtGui.QKeySequence.Close, 'Esc')
253 self.connect(self, SIGNAL('action_completed'), self.action_completed)
254 self.connect(self.progress_thread, SIGNAL('str'), self.update_progress)
256 if action == PULL:
257 self.tags_checkbox.hide()
258 self.ffwd_only_checkbox.hide()
259 self.local_label.hide()
260 self.local_branch.hide()
261 self.local_branches.hide()
262 self.remote_branch.setFocus()
263 else:
264 self.rebase_checkbox.hide()
266 if not qtutils.apply_state(self):
267 self.resize(666, 420)
269 self.remote_name.setFocus()
271 def set_rebase(self, value):
272 self.rebase_checkbox.setChecked(value)
274 def set_field_defaults(self):
275 # Default to "git fetch origin master"
276 action = self.action
277 if action == FETCH or action == PULL:
278 self.local_branch.setText('')
279 self.remote_branch.setText('')
280 return
282 # Select the current branch by default for push
283 if action == PUSH:
284 branch = self.model.currentbranch
285 try:
286 idx = self.model.local_branches.index(branch)
287 except ValueError:
288 return
289 if self.select_local_branch(idx):
290 self.set_local_branch(branch)
291 self.set_remote_branch('')
293 def set_remote_name(self, remote_name):
294 self.remote_name.setText(remote_name)
295 if remote_name:
296 self.remote_name.selectAll()
298 def set_local_branch(self, branch):
299 self.local_branch.setText(branch)
300 if branch:
301 self.local_branch.selectAll()
303 def set_remote_branch(self, branch):
304 self.remote_branch.setText(branch)
305 if branch:
306 self.remote_branch.selectAll()
308 def set_remote_branches(self, branches):
309 self.remote_branches.clear()
310 self.remote_branches.addItems(branches)
311 self.filtered_remote_branches = branches
313 def select_first_remote(self):
314 """Selects the first remote in the list view"""
315 return self.select_remote(0)
317 def select_remote(self, idx):
318 """Selects a remote by index"""
319 item = self.remotes.item(idx)
320 if item:
321 self.remotes.setItemSelected(item, True)
322 self.remotes.setCurrentItem(item)
323 self.set_remote_name(unicode(item.text()))
324 return True
325 else:
326 return False
328 def select_local_branch(self, idx):
329 """Selects a local branch by index in the list view"""
330 item = self.local_branches.item(idx)
331 if not item:
332 return False
333 self.local_branches.setItemSelected(item, True)
334 self.local_branches.setCurrentItem(item)
335 self.local_branch.setText(item.text())
336 return True
338 def display_remotes(self, widget):
339 """Display the available remotes in a listwidget"""
340 displayed = []
341 for remote_name in self.model.remotes:
342 url = self.model.remote_url(remote_name, self.action)
343 display = ('%s\t(%s)'
344 % (remote_name, N_('URL: %s') % url))
345 displayed.append(display)
346 qtutils.set_items(widget,displayed)
348 def update_remotes(self, *rest):
349 """Update the remote name when a remote from the list is selected"""
350 widget = self.remotes
351 remotes = self.model.remotes
352 selection = qtutils.selected_item(widget, remotes)
353 if not selection:
354 self.selected_remotes = []
355 return
356 self.set_remote_name(selection)
357 self.selected_remotes = qtutils.selected_items(self.remotes,
358 self.model.remotes)
360 all_branches = gitcmds.branch_list(remote=True)
361 branches = []
362 patterns = []
363 for remote in self.selected_remotes:
364 pat = remote + '/*'
365 patterns.append(pat)
367 for branch in all_branches:
368 for pat in patterns:
369 if fnmatch.fnmatch(branch, pat):
370 branches.append(branch)
371 break
372 if branches:
373 self.set_remote_branches(branches)
374 else:
375 self.set_remote_branches(all_branches)
376 self.set_remote_branch('')
378 def update_local_branches(self,*rest):
379 """Update the local/remote branch names when a branch is selected"""
380 branches = self.model.local_branches
381 widget = self.local_branches
382 selection = qtutils.selected_item(widget, branches)
383 if not selection:
384 return
385 self.set_local_branch(selection)
386 self.set_remote_branch(selection)
388 def update_remote_branches(self,*rest):
389 """Update the remote branch name when a branch is selected"""
390 widget = self.remote_branches
391 branches = self.filtered_remote_branches
392 selection = qtutils.selected_item(widget, branches)
393 if not selection:
394 return
395 branch = utils.strip_one(selection)
396 if branch == 'HEAD':
397 return
398 self.set_remote_branch(branch)
400 def common_args(self):
401 """Returns git arguments common to fetch/push/pulll"""
402 remote_name = unicode(self.remote_name.text())
403 local_branch = unicode(self.local_branch.text())
404 remote_branch = unicode(self.remote_branch.text())
406 ffwd_only = self.ffwd_only_checkbox.isChecked()
407 rebase = self.rebase_checkbox.isChecked()
408 tags = self.tags_checkbox.isChecked()
410 return (remote_name,
412 'local_branch': local_branch,
413 'remote_branch': remote_branch,
414 'ffwd': ffwd_only,
415 'rebase': rebase,
416 'tags': tags,
419 # Actions
421 def action_callback(self):
422 action = self.action
423 if action == FETCH:
424 model_action = self.model.fetch
425 elif action == PUSH:
426 model_action = self.push_to_all
427 else: # if action == PULL:
428 model_action = self.model.pull
430 remote_name = unicode(self.remote_name.text())
431 if not remote_name:
432 errmsg = N_('No repository selected.')
433 Interaction.log(errmsg)
434 return
435 remote, kwargs = self.common_args()
436 self.selected_remotes = qtutils.selected_items(self.remotes,
437 self.model.remotes)
439 # Check if we're about to create a new branch and warn.
440 remote_branch = unicode(self.remote_branch.text())
441 local_branch = unicode(self.local_branch.text())
443 if action == PUSH and not remote_branch:
444 branch = local_branch
445 candidate = '%s/%s' % (remote, branch)
446 if candidate not in self.model.remote_branches:
447 title = N_('Push')
448 args = dict(branch=branch, remote=remote)
449 msg = N_('Branch "%(branch)s" does not exist in "%(remote)s".\n'
450 'A new remote branch will be published.') % args
451 info_txt= N_('Create a new remote branch?')
452 ok_text = N_('Create Remote Branch')
453 if not qtutils.confirm(title, msg, info_txt, ok_text,
454 default=False,
455 icon=qtutils.git_icon()):
456 return
458 if not self.ffwd_only_checkbox.isChecked():
459 if action == FETCH:
460 title = N_('Force Fetch?')
461 msg = N_('Non-fast-forward fetch overwrites local history!')
462 info_txt = N_('Force fetching from %s?') % remote
463 ok_text = N_('Force Fetch')
464 elif action == PUSH:
465 title = N_('Force Push?')
466 msg = N_('Non-fast-forward push overwrites published '
467 'history!\n(Did you pull first?)')
468 info_txt = N_('Force push to %s?') % remote
469 ok_text = N_('Force Push')
470 else: # pull: shouldn't happen since the controls are hidden
471 msg = "You probably don't want to do this.\n\tContinue?"
472 return
474 if not qtutils.confirm(title, msg, info_txt, ok_text,
475 default=False,
476 icon=qtutils.discard_icon()):
477 return
479 # Disable the GUI by default
480 self.action_button.setEnabled(False)
481 self.close_button.setEnabled(False)
482 QtGui.QApplication.setOverrideCursor(Qt.WaitCursor)
484 # Show a nice progress bar
485 self.progress.show()
486 self.progress_thread.start()
488 # Use a thread to update in the background
489 task = ActionTask(self, model_action, remote, kwargs)
490 self.tasks.append(task)
491 QtCore.QThreadPool.globalInstance().start(task)
493 def update_progress(self, txt):
494 self.progress.setLabelText(txt)
496 def push_to_all(self, dummy_remote, *args, **kwargs):
497 selected_remotes = self.selected_remotes
498 all_results = None
499 for remote in selected_remotes:
500 result = self.model.push(remote, *args, **kwargs)
501 all_results = combine(result, all_results)
502 return all_results
504 def action_completed(self, task, status, out, err):
505 # Grab the results of the action and finish up
506 self.action_button.setEnabled(True)
507 self.close_button.setEnabled(True)
508 QtGui.QApplication.restoreOverrideCursor()
510 self.progress_thread.stop()
511 self.progress_thread.wait()
512 self.progress.close()
513 if task in self.tasks:
514 self.tasks.remove(task)
516 already_up_to_date = N_('Already up-to-date.')
518 if not out: # git fetch --tags --verbose doesn't print anything...
519 out = already_up_to_date
521 command = 'git %s' % self.action.lower()
522 message = (N_('"%(command)s" returned exit status %(status)d') %
523 dict(command=command, status=status))
524 details = ''
525 if out:
526 details = out
527 if err:
528 details += '\n\n' + err
530 log_message = message
531 if details:
532 log_message += '\n\n' + details
533 Interaction.log(log_message)
535 if status == 0:
536 self.accept()
537 return
539 if self.action == PUSH:
540 message += '\n\n'
541 message += N_('Have you rebased/pulled lately?')
543 Interaction.critical(self.windowTitle(),
544 message=message, details=details)
547 # Use distinct classes so that each saves its own set of preferences
548 class Fetch(RemoteActionDialog):
549 def __init__(self, model, parent=None):
550 RemoteActionDialog.__init__(self, model, FETCH, parent=parent)
553 class Push(RemoteActionDialog):
554 def __init__(self, model, parent=None):
555 RemoteActionDialog.__init__(self, model, PUSH, parent=parent)
558 class Pull(RemoteActionDialog):
559 def __init__(self, model, parent=None):
560 RemoteActionDialog.__init__(self, model, PULL, parent=parent)
562 def apply_state(self, state):
563 RemoteActionDialog.apply_state(self, state)
564 try:
565 rebase = state['rebase']
566 except KeyError:
567 pass
568 else:
569 self.rebase_checkbox.setChecked(rebase)
571 def export_state(self):
572 state = RemoteActionDialog.export_state(self)
573 state['rebase'] = self.rebase_checkbox.isChecked()
574 return state
576 def done(self, exit_code):
577 qtutils.save_state(self)
578 return RemoteActionDialog.done(self, exit_code)