pull: Remember the 'rebase' checkbox state between invocations
[git-cola.git] / cola / widgets / remote.py
blobfcd5b2a0707660366059f89f5adeddd9119f920b
1 import fnmatch
3 from PyQt4 import QtCore
4 from PyQt4 import QtGui
5 from PyQt4.QtCore import Qt
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.widgets import defs
14 from cola.widgets import standard
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 super(RemoteActionDialog, self).__init__(parent=parent)
69 self.model = model
70 self.action = action
71 self.tasks = []
73 self.setAttribute(Qt.WA_MacMetalStyle)
74 self.setWindowModality(Qt.WindowModal)
75 self.setWindowTitle(self.tr(action))
77 self.progress = QtGui.QProgressDialog(self)
78 self.progress.setRange(0, 0)
79 self.progress.setCancelButton(None)
80 self.progress.setWindowTitle(self.tr(action))
81 self.progress.setWindowModality(Qt.WindowModal)
83 self.local_label = QtGui.QLabel()
84 self.local_label.setText(self.tr('Local Branch'))
86 self.local_branch = QtGui.QLineEdit()
87 self.local_branches = QtGui.QListWidget()
88 self.local_branches.addItems(self.model.local_branches)
90 self.remote_label = QtGui.QLabel()
91 self.remote_label.setText(self.tr('Remote'))
93 self.remote_name = QtGui.QLineEdit()
94 self.remotes = QtGui.QListWidget()
95 self.remotes.addItems(self.model.remotes)
97 self.remote_branch_label = QtGui.QLabel()
98 self.remote_branch_label.setText(self.tr('Remote Branch'))
100 self.remote_branch = QtGui.QLineEdit()
101 self.remote_branches = QtGui.QListWidget()
102 self.remote_branches.addItems(self.model.remote_branches)
104 self.ffwd_only_checkbox = QtGui.QCheckBox()
105 self.ffwd_only_checkbox.setText(self.tr('Fast Forward Only '))
106 self.ffwd_only_checkbox.setChecked(True)
108 self.tags_checkbox = QtGui.QCheckBox()
109 self.tags_checkbox.setText(self.tr('Include tags '))
111 self.rebase_checkbox = QtGui.QCheckBox()
112 self.rebase_checkbox.setText(self.tr('Rebase '))
114 self.action_button = QtGui.QPushButton()
115 self.action_button.setText(self.tr(action))
116 self.action_button.setIcon(qtutils.ok_icon())
118 self.close_button = QtGui.QPushButton()
119 self.close_button.setText(self.tr('Close'))
120 self.close_button.setIcon(qtutils.close_icon())
122 self.local_branch_layout = QtGui.QHBoxLayout()
123 self.local_branch_layout.addWidget(self.local_label)
124 self.local_branch_layout.addWidget(self.local_branch)
126 self.remote_branch_layout = QtGui.QHBoxLayout()
127 self.remote_branch_layout.addWidget(self.remote_label)
128 self.remote_branch_layout.addWidget(self.remote_name)
130 self.remote_branches_layout = QtGui.QHBoxLayout()
131 self.remote_branches_layout.addWidget(self.remote_branch_label)
132 self.remote_branches_layout.addWidget(self.remote_branch)
134 self.options_layout = QtGui.QHBoxLayout()
135 self.options_layout.setSpacing(defs.button_spacing)
136 self.options_layout.addStretch()
137 self.options_layout.addWidget(self.ffwd_only_checkbox)
138 self.options_layout.addWidget(self.tags_checkbox)
139 self.options_layout.addWidget(self.rebase_checkbox)
140 self.options_layout.addWidget(self.action_button)
141 self.options_layout.addWidget(self.close_button)
143 self.main_layout = QtGui.QVBoxLayout()
144 self.main_layout.setMargin(defs.margin)
145 self.main_layout.setSpacing(defs.spacing)
146 self.main_layout.addLayout(self.remote_branch_layout)
147 self.main_layout.addWidget(self.remotes)
148 if action == PUSH:
149 self.main_layout.addLayout(self.local_branch_layout)
150 self.main_layout.addWidget(self.local_branches)
151 self.main_layout.addLayout(self.remote_branches_layout)
152 self.main_layout.addWidget(self.remote_branches)
153 else: # fetch and pull
154 self.main_layout.addLayout(self.remote_branches_layout)
155 self.main_layout.addWidget(self.remote_branches)
156 self.main_layout.addLayout(self.local_branch_layout)
157 self.main_layout.addWidget(self.local_branches)
158 self.main_layout.addLayout(self.options_layout)
159 self.setLayout(self.main_layout)
161 remotes = self.model.remotes
162 if 'origin' in remotes:
163 idx = remotes.index('origin')
164 if self.select_remote(idx):
165 self.remote_name.setText('origin')
166 else:
167 if self.select_first_remote():
168 self.remote_name.setText(remotes[0])
170 # Trim the remote list to just the default remote
171 self.update_remotes()
172 self.set_field_defaults()
174 # Setup signals and slots
175 self.connect(self.remotes, SIGNAL('itemSelectionChanged()'),
176 self.update_remotes)
178 self.connect(self.local_branches, SIGNAL('itemSelectionChanged()'),
179 self.update_local_branches)
181 self.connect(self.remote_branches, SIGNAL('itemSelectionChanged()'),
182 self.update_remote_branches)
184 connect_button(self.action_button, self.action_callback)
185 connect_button(self.close_button, self.reject)
187 self.connect(self, SIGNAL('action_completed'), self.action_completed)
189 if action == PULL:
190 self.tags_checkbox.hide()
191 self.ffwd_only_checkbox.hide()
192 self.local_label.hide()
193 self.local_branch.hide()
194 self.local_branches.hide()
195 self.remote_branch.setFocus()
196 else:
197 self.rebase_checkbox.hide()
199 if not qtutils.apply_state(self):
200 self.resize(666, 420)
202 self.remote_name.setFocus()
205 def set_field_defaults(self):
206 # Default to "git fetch origin master"
207 action = self.action
208 if action == FETCH or action == PULL:
209 self.local_branch.setText('')
210 self.remote_branch.setText('')
211 return
213 # Select the current branch by default for push
214 if action == PUSH:
215 branch = self.model.currentbranch
216 try:
217 idx = self.model.local_branches.index(branch)
218 except ValueError:
219 return
220 if self.select_local_branch(idx):
221 self.set_local_branch(branch)
222 self.set_remote_branch('')
224 def set_remote_name(self, remote_name):
225 self.remote_name.setText(remote_name)
226 if remote_name:
227 self.remote_name.selectAll()
229 def set_local_branch(self, branch):
230 self.local_branch.setText(branch)
231 if branch:
232 self.local_branch.selectAll()
234 def set_remote_branch(self, branch):
235 self.remote_branch.setText(branch)
236 if branch:
237 self.remote_branch.selectAll()
239 def set_remote_branches(self, branches):
240 self.remote_branches.clear()
241 self.remote_branches.addItems(branches)
243 def select_first_remote(self):
244 """Selects the first remote in the list view"""
245 return self.select_remote(0)
247 def select_remote(self, idx):
248 """Selects a remote by index"""
249 item = self.remotes.item(idx)
250 if item:
251 self.remotes.setItemSelected(item, True)
252 self.remotes.setCurrentItem(item)
253 self.set_remote_name(unicode(item.text()))
254 return True
255 else:
256 return False
258 def select_local_branch(self, idx):
259 """Selects a local branch by index in the list view"""
260 item = self.local_branches.item(idx)
261 if not item:
262 return False
263 self.local_branches.setItemSelected(item, True)
264 self.local_branches.setCurrentItem(item)
265 self.local_branch.setText(item.text())
266 return True
268 def display_remotes(self, widget):
269 """Display the available remotes in a listwidget"""
270 displayed = []
271 for remote_name in self.model.remotes:
272 url = self.model.remote_url(remote_name, self.action)
273 display = ('%s\t(%s %s)'
274 % (remote_name, unicode(self.tr('URL:')), url))
275 displayed.append(display)
276 qtutils.set_items(widget,displayed)
278 def update_remotes(self, *rest):
279 """Update the remote name when a remote from the list is selected"""
280 widget = self.remotes
281 remotes = self.model.remotes
282 selection = qtutils.selected_item(widget, remotes)
283 if not selection:
284 return
285 self.set_remote_name(selection)
287 all_branches = gitcmds.branch_list(remote=True)
288 branches = []
289 pat = selection + '/*'
290 for branch in all_branches:
291 if fnmatch.fnmatch(branch, pat):
292 branches.append(branch)
293 if branches:
294 self.set_remote_branches(branches)
295 else:
296 self.set_remote_branches(all_branches)
297 self.set_remote_branch('')
299 def update_local_branches(self,*rest):
300 """Update the local/remote branch names when a branch is selected"""
301 branches = self.model.local_branches
302 widget = self.local_branches
303 selection = qtutils.selected_item(widget, branches)
304 if not selection:
305 return
306 self.set_local_branch(selection)
307 self.set_remote_branch(selection)
309 def update_remote_branches(self,*rest):
310 """Update the remote branch name when a branch is selected"""
311 widget = self.remote_branches
312 branches = self.model.remote_branches
313 selection = qtutils.selected_item(widget,branches)
314 if not selection:
315 return
316 branch = utils.basename(selection)
317 if branch == 'HEAD':
318 return
319 self.set_remote_branch(branch)
321 def common_args(self):
322 """Returns git arguments common to fetch/push/pulll"""
323 remote_name = unicode(self.remote_name.text())
324 local_branch = unicode(self.local_branch.text())
325 remote_branch = unicode(self.remote_branch.text())
327 ffwd_only = self.ffwd_only_checkbox.isChecked()
328 rebase = self.rebase_checkbox.isChecked()
329 tags = self.tags_checkbox.isChecked()
331 return (remote_name,
333 'local_branch': local_branch,
334 'remote_branch': remote_branch,
335 'ffwd': ffwd_only,
336 'rebase': rebase,
337 'tags': tags,
340 #+-------------------------------------------------------------
341 #+ Actions
342 def action_callback(self):
343 action = self.action
344 if action == FETCH:
345 model_action = self.model.fetch
346 elif action == PUSH:
347 model_action = self.model.push
348 else: # if action == PULL:
349 model_action = self.model.pull
351 remote_name = unicode(self.remote_name.text())
352 if not remote_name:
353 errmsg = self.tr('No repository selected.')
354 qtutils.log(1, errmsg)
355 return
356 remote, kwargs = self.common_args()
358 # Check if we're about to create a new branch and warn.
359 remote_branch = unicode(self.remote_branch.text())
360 local_branch = unicode(self.local_branch.text())
362 if action == PUSH and not remote_branch:
363 branch = local_branch
364 candidate = '%s/%s' % (remote, branch)
365 if candidate not in self.model.remote_branches:
366 title = self.tr(PUSH)
367 msg = 'Branch "%s" does not exist in %s.' % (branch, remote)
368 msg += '\nA new remote branch will be published.'
369 info_txt= 'Create a new remote branch?'
370 ok_text = 'Create Remote Branch'
371 if not qtutils.confirm(title, msg, info_txt, ok_text,
372 default=False,
373 icon=qtutils.git_icon()):
374 return
376 if not self.ffwd_only_checkbox.isChecked():
377 title = 'Force %s?' % action.title()
378 ok_text = 'Force %s' % action.title()
380 if action == FETCH:
381 msg = 'Non-fast-forward fetch overwrites local history!'
382 info_txt = 'Force fetching from %s?' % remote
383 elif action == PUSH:
384 msg = ('Non-fast-forward push overwrites published '
385 'history!\n(Did you pull first?)')
386 info_txt = 'Force push to %s?' % remote
387 else: # pull: shouldn't happen since the controls are hidden
388 msg = "You probably don't want to do this.\n\tContinue?"
389 return
391 if not qtutils.confirm(title, msg, info_txt, ok_text,
392 default=False,
393 icon=qtutils.discard_icon()):
394 return
396 # Disable the GUI by default
397 self.setEnabled(False)
398 self.progress.setEnabled(True)
399 QtGui.QApplication.setOverrideCursor(Qt.WaitCursor)
401 # Show a nice progress bar
402 self.progress.setLabelText('Updating...')
403 self.progress.show()
405 # Use a thread to update in the background
406 task = ActionTask(self, model_action, remote, kwargs)
407 self.tasks.append(task)
408 QtCore.QThreadPool.globalInstance().start(task)
410 def action_completed(self, task, status, output):
411 # Grab the results of the action and finish up
412 if task in self.tasks:
413 self.tasks.remove(task)
415 if not output: # git fetch --tags --verbose doesn't print anything...
416 output = self.tr('Already up-to-date.')
417 # Force the status to 1 so that we always display the log
418 qtutils.log(1, output)
420 self.progress.close()
421 QtGui.QApplication.restoreOverrideCursor()
423 if status != 0 and self.action == PUSH:
424 remote_name = unicode(self.remote_name.text())
425 message = 'Error pushing to "%s".\n\nPull first?' % remote_name
426 qtutils.critical('Push Error',
427 message=message, details=output)
428 else:
429 title = self.windowTitle()
430 if status == 0:
431 result = 'succeeded'
432 else:
433 result = 'returned exit status %d' % status
435 message = '"git %s" %s' % (self.action.lower(), result)
436 qtutils.information(title,
437 message=message, details=output)
438 self.accept()
441 # Use distinct classes so that each saves its own set of preferences
442 class Fetch(RemoteActionDialog):
443 def __init__(self, model, parent):
444 super(Fetch, self).__init__(model, FETCH, parent)
447 class Push(RemoteActionDialog):
448 def __init__(self, model, parent):
449 super(Push, self).__init__(model, PUSH, parent)
452 class Pull(RemoteActionDialog):
453 def __init__(self, model, parent):
454 super(Pull, self).__init__(model, PULL, parent)
456 def apply_state(self, state):
457 super(Pull, self).apply_state(state)
458 try:
459 rebase = state['rebase']
460 except KeyError:
461 pass
462 else:
463 self.rebase_checkbox.setChecked(rebase)
465 def export_state(self):
466 state = super(Pull, self).export_state()
467 state['rebase'] = self.rebase_checkbox.isChecked()
468 return state
470 def done(self, exit_code):
471 qtutils.save_state(self)
472 return super(Pull, self).done(exit_code)