main.model: Do not depend on ObservableModel
[git-cola.git] / cola / widgets / remote.py
blob06a885f8f5212a697e70425739181caaa49c65d1
1 import fnmatch
3 from PyQt4.QtCore import Qt
4 from PyQt4 import QtCore
5 from PyQt4 import QtGui
6 from PyQt4.QtCore import SIGNAL
8 import cola
9 from cola import gitcmds
10 from cola import qtutils
11 from cola import utils
12 from cola.qtutils import connect_button
13 from cola.views import standard
14 from cola.widgets import defs
15 from cola.main.model import MainModel
17 FETCH = 'Fetch'
18 PUSH = 'Push'
19 PULL = 'Pull'
22 def fetch():
23 return run(Fetch)
26 def push():
27 return run(Push)
30 def pull():
31 return run(Pull)
34 def run(RemoteDialog):
35 """Launches fetch/push/pull dialogs."""
36 # Copy global stuff over to speedup startup
37 model = MainModel()
38 global_model = cola.model()
39 model.currentbranch = global_model.currentbranch
40 model.local_branches = global_model.local_branches
41 model.remote_branches = global_model.remote_branches
42 model.tags = global_model.tags
43 model.remotes = global_model.remotes
44 parent = qtutils.active_window()
45 view = RemoteDialog(model, parent)
46 view.show()
47 return view
50 class ActionTask(QtCore.QRunnable):
51 def __init__(self, sender, model_action, remote, kwargs):
52 QtCore.QRunnable.__init__(self)
53 self.sender = sender
54 self.model_action = model_action
55 self.remote = remote
56 self.kwargs = kwargs
58 def run(self):
59 """Runs the model action and captures the result"""
60 status, output = self.model_action(self.remote, **self.kwargs)
61 self.sender.emit(SIGNAL('action_completed'), self, status, output)
64 class RemoteActionDialog(standard.Dialog):
65 def __init__(self, model, action, parent):
66 """Customizes the dialog based on the remote action
67 """
68 standard.Dialog.__init__(self, parent=parent)
69 self.model = model
70 self.action = action
71 self.tasks = []
73 self.setWindowTitle(self.tr(action))
74 self.setWindowModality(QtCore.Qt.WindowModal)
76 self.progress = QtGui.QProgressDialog(self)
77 self.progress.setRange(0, 0)
78 self.progress.setCancelButton(None)
79 self.progress.setWindowTitle(self.tr(action))
80 self.progress.setWindowModality(Qt.WindowModal)
82 self.local_label = QtGui.QLabel()
83 self.local_label.setText(self.tr('Local Branches'))
85 self.local_branch = QtGui.QLineEdit()
86 self.local_branches = QtGui.QListWidget()
87 self.local_branches.addItems(self.model.local_branches)
89 self.remote_label = QtGui.QLabel()
90 self.remote_label.setText(self.tr('Remote'))
92 self.remote_name = QtGui.QLineEdit()
93 self.remotes = QtGui.QListWidget()
94 self.remotes.addItems(self.model.remotes)
96 self.remote_label = QtGui.QLabel()
97 self.remote_label.setText(self.tr('Remote Branch'))
99 self.remote_branch = QtGui.QLineEdit()
100 self.remote_branches = QtGui.QListWidget()
101 self.remote_branches.addItems(self.model.remote_branches)
103 self.ffwd_only_checkbox = QtGui.QCheckBox()
104 self.ffwd_only_checkbox.setText(self.tr('Fast Forward Only '))
105 self.ffwd_only_checkbox.setChecked(True)
107 self.tags_checkbox = QtGui.QCheckBox()
108 self.tags_checkbox.setText(self.tr('Include tags '))
110 self.rebase_checkbox = QtGui.QCheckBox()
111 self.rebase_checkbox.setText(self.tr('Rebase '))
113 self.action_button = QtGui.QPushButton()
114 self.action_button.setText(self.tr(action))
115 self.action_button.setIcon(qtutils.ok_icon())
117 self.close_button = QtGui.QPushButton()
118 self.close_button.setText(self.tr('Close'))
119 self.close_button.setIcon(qtutils.close_icon())
121 self.local_branch_layout = QtGui.QHBoxLayout()
122 self.local_branch_layout.addWidget(self.local_label)
123 self.local_branch_layout.addWidget(self.local_branch)
125 self.remote_branch_layout = QtGui.QHBoxLayout()
126 self.remote_branch_layout.addWidget(self.remote_label)
127 self.remote_branch_layout.addWidget(self.remote_name)
129 self.remote_branches_layout = QtGui.QHBoxLayout()
130 self.remote_branches_layout.addWidget(self.remote_label)
131 self.remote_branches_layout.addWidget(self.remote_branch)
133 self.options_layout = QtGui.QHBoxLayout()
134 self.options_layout.setSpacing(defs.button_spacing)
135 self.options_layout.addStretch()
136 self.options_layout.addWidget(self.ffwd_only_checkbox)
137 self.options_layout.addWidget(self.tags_checkbox)
138 self.options_layout.addWidget(self.rebase_checkbox)
139 self.options_layout.addWidget(self.action_button)
140 self.options_layout.addWidget(self.close_button)
142 self.main_layout = QtGui.QVBoxLayout()
143 self.main_layout.setMargin(defs.margin)
144 self.main_layout.setSpacing(defs.spacing)
145 self.main_layout.addLayout(self.remote_branch_layout)
146 self.main_layout.addWidget(self.remotes)
147 if action == PUSH:
148 self.main_layout.addLayout(self.local_branch_layout)
149 self.main_layout.addWidget(self.local_branches)
150 self.main_layout.addLayout(self.remote_branches_layout)
151 self.main_layout.addWidget(self.remote_branches)
152 else: # fetch and pull
153 self.main_layout.addLayout(self.remote_branches_layout)
154 self.main_layout.addWidget(self.remote_branches)
155 self.main_layout.addLayout(self.local_branch_layout)
156 self.main_layout.addWidget(self.local_branches)
157 self.main_layout.addLayout(self.options_layout)
158 self.setLayout(self.main_layout)
160 remotes = self.model.remotes
161 if 'origin' in remotes:
162 idx = remotes.index('origin')
163 if self.select_remote(idx):
164 self.remote_name.setText('origin')
165 else:
166 if self.select_first_remote():
167 self.remote_name.setText(remotes[0])
169 # Trim the remote list to just the default remote
170 self.update_remotes()
171 self.set_field_defaults()
173 # Setup signals and slots
174 self.connect(self.remotes, SIGNAL('itemSelectionChanged()'),
175 self.update_remotes)
177 self.connect(self.local_branches, SIGNAL('itemSelectionChanged()'),
178 self.update_local_branches)
180 self.connect(self.remote_branches, SIGNAL('itemSelectionChanged()'),
181 self.update_remote_branches)
183 connect_button(self.action_button, self.action_callback)
184 connect_button(self.close_button, self.reject)
186 self.connect(self, SIGNAL('action_completed'), self.action_completed)
188 if action == PULL:
189 self.tags_checkbox.hide()
190 self.ffwd_only_checkbox.hide()
191 self.local_label.hide()
192 self.local_branch.hide()
193 self.local_branches.hide()
194 self.remote_branch.setFocus()
195 else:
196 self.rebase_checkbox.hide()
197 self.remote_name.setFocus()
199 self.resize(666, 420)
201 def set_field_defaults(self):
202 # Default to "git fetch origin master"
203 action = self.action
204 if action == FETCH or action == PULL:
205 self.local_branch.setText('')
206 self.remote_branch.setText('')
207 return
209 # Select the current branch by default for push
210 if action == PUSH:
211 branch = self.model.currentbranch
212 try:
213 idx = self.model.local_branches.index(branch)
214 except ValueError:
215 return
216 if self.select_local_branch(idx):
217 self.set_local_branch(branch)
218 self.set_remote_branch('')
220 def set_remote_name(self, remote_name):
221 self.remote_name.setText(remote_name)
222 if remote_name:
223 self.remote_name.selectAll()
225 def set_local_branch(self, branch):
226 self.local_branch.setText(branch)
227 if branch:
228 self.local_branch.selectAll()
230 def set_remote_branch(self, branch):
231 self.remote_branch.setText(branch)
232 if branch:
233 self.remote_branch.selectAll()
235 def set_remote_branches(self, branches):
236 self.remote_branches.clear()
237 self.remote_branches.addItems(branches)
239 def select_first_remote(self):
240 """Selects the first remote in the list view"""
241 return self.select_remote(0)
243 def select_remote(self, idx):
244 """Selects a remote by index"""
245 item = self.remotes.item(idx)
246 if item:
247 self.remotes.setItemSelected(item, True)
248 self.remotes.setCurrentItem(item)
249 self.set_remote_name(unicode(item.text()))
250 return True
251 else:
252 return False
254 def select_local_branch(self, idx):
255 """Selects a local branch by index in the list view"""
256 item = self.local_branches.item(idx)
257 if not item:
258 return False
259 self.local_branches.setItemSelected(item, True)
260 self.local_branches.setCurrentItem(item)
261 self.local_branch.setText(item.text())
262 return True
264 def display_remotes(self, widget):
265 """Display the available remotes in a listwidget"""
266 displayed = []
267 for remote_name in self.model.remotes:
268 url = self.model.remote_url(remote_name, self.action)
269 display = ('%s\t(%s %s)'
270 % (remote_name, unicode(self.tr('URL:')), url))
271 displayed.append(display)
272 qtutils.set_items(widget,displayed)
274 def update_remotes(self, *rest):
275 """Update the remote name when a remote from the list is selected"""
276 widget = self.remotes
277 remotes = self.model.remotes
278 selection = qtutils.selected_item(widget, remotes)
279 if not selection:
280 return
281 self.set_remote_name(selection)
283 all_branches = gitcmds.branch_list(remote=True)
284 branches = []
285 pat = selection + '/*'
286 for branch in all_branches:
287 if fnmatch.fnmatch(branch, pat):
288 branches.append(branch)
289 if branches:
290 self.set_remote_branches(branches)
291 else:
292 self.set_remote_branches(all_branches)
293 self.set_remote_branch('')
295 def update_local_branches(self,*rest):
296 """Update the local/remote branch names when a branch is selected"""
297 branches = self.model.local_branches
298 widget = self.local_branches
299 selection = qtutils.selected_item(widget, branches)
300 if not selection:
301 return
302 self.set_local_branch(selection)
303 self.set_remote_branch(selection)
305 def update_remote_branches(self,*rest):
306 """Update the remote branch name when a branch is selected"""
307 widget = self.remote_branches
308 branches = self.model.remote_branches
309 selection = qtutils.selected_item(widget,branches)
310 if not selection:
311 return
312 branch = utils.basename(selection)
313 if branch == 'HEAD':
314 return
315 self.set_remote_branch(branch)
317 def common_args(self):
318 """Returns git arguments common to fetch/push/pulll"""
319 remote_name = unicode(self.remote_name.text())
320 local_branch = unicode(self.local_branch.text())
321 remote_branch = unicode(self.remote_branch.text())
323 ffwd_only = self.ffwd_only_checkbox.isChecked()
324 rebase = self.rebase_checkbox.isChecked()
325 tags = self.tags_checkbox.isChecked()
327 return (remote_name,
329 'local_branch': local_branch,
330 'remote_branch': remote_branch,
331 'ffwd': ffwd_only,
332 'rebase': rebase,
333 'tags': tags,
336 #+-------------------------------------------------------------
337 #+ Actions
338 def action_callback(self):
339 action = self.action
340 if action == FETCH:
341 model_action = self.model.fetch
342 elif action == PUSH:
343 model_action = self.model.push
344 else: # if action == PULL:
345 model_action = self.model.pull
347 remote_name = unicode(self.remote_name.text())
348 if not remote_name:
349 errmsg = self.tr('No repository selected.')
350 qtutils.log(1, errmsg)
351 return
352 remote, kwargs = self.common_args()
353 action = self.action
355 # Check if we're about to create a new branch and warn.
356 remote_branch = unicode(self.remote_branch.text())
357 local_branch = unicode(self.local_branch.text())
359 if action == PUSH and not remote_branch:
360 branch = local_branch
361 candidate = '%s/%s' % (remote, branch)
362 if candidate not in self.model.remote_branches:
363 title = self.tr(PUSH)
364 msg = 'Branch "%s" does not exist in %s.' % (branch, remote)
365 msg += '\nA new remote branch will be published.'
366 info_txt= 'Create a new remote branch?'
367 ok_text = 'Create Remote Branch'
368 if not qtutils.confirm(title, msg, info_txt, ok_text,
369 default=False,
370 icon=qtutils.git_icon()):
371 return
373 if not self.ffwd_only_checkbox.isChecked():
374 title = 'Force %s?' % action.title()
375 ok_text = 'Force %s' % action.title()
377 if action == FETCH:
378 msg = 'Non-fast-forward fetch overwrites local history!'
379 info_txt = 'Force fetching from %s?' % remote
380 elif action == PUSH:
381 msg = ('Non-fast-forward push overwrites published '
382 'history!\n(Did you pull first?)')
383 info_txt = 'Force push to %s?' % remote
384 else: # pull: shouldn't happen since the controls are hidden
385 msg = "You probably don't want to do this.\n\tContinue?"
386 return
388 if not qtutils.confirm(title, msg, info_txt, ok_text,
389 default=False,
390 icon=qtutils.discard_icon()):
391 return
393 # Disable the GUI by default
394 self.setEnabled(False)
395 self.progress.setEnabled(True)
396 QtGui.QApplication.setOverrideCursor(Qt.WaitCursor)
398 # Show a nice progress bar
399 self.progress.setLabelText('Updating...')
400 self.progress.show()
402 # Use a thread to update in the background
403 task = ActionTask(self, model_action, remote, kwargs)
404 self.tasks.append(task)
405 QtCore.QThreadPool.globalInstance().start(task)
407 def action_completed(self, task, status, output):
408 # Grab the results of the action and finish up
409 if task in self.tasks:
410 self.tasks.remove(task)
412 if not output: # git fetch --tags --verbose doesn't print anything...
413 output = self.tr('Already up-to-date.')
414 # Force the status to 1 so that we always display the log
415 qtutils.log(1, output)
417 self.progress.close()
418 QtGui.QApplication.restoreOverrideCursor()
420 if status != 0 and self.action == PUSH:
421 remote_name = unicode(self.remote_name.text())
422 message = 'Error pushing to "%s".\n\nPull first?' % remote_name
423 qtutils.critical('Push Error',
424 message=message, details=output)
425 else:
426 title = self.windowTitle()
427 if status == 0:
428 result = 'succeeded'
429 else:
430 result = 'returned exit status %d' % status
432 message = '"git %s" %s' % (self.action.lower(), result)
433 qtutils.information(title,
434 message=message, details=output)
435 self.accept()
438 # Use distinct classes so that each saves its own set of preferences
439 class Fetch(RemoteActionDialog):
440 def __init__(self, model, parent):
441 super(Fetch, self).__init__(model, FETCH, parent)
444 class Push(RemoteActionDialog):
445 def __init__(self, model, parent):
446 super(Push, self).__init__(model, PUSH, parent)
449 class Pull(RemoteActionDialog):
450 def __init__(self, model, parent):
451 super(Pull, self).__init__(model, PULL, parent)