models.gitrepo: Use a custom event type for GitRepoInfoEvents
[git-cola.git] / cola / views / main.py
blob9d3056c2f6e02fb5412db3565be3a118363b9729
1 """This view provides the main git-cola user interface.
2 """
3 import os
5 from PyQt4 import QtGui
6 from PyQt4 import QtCore
7 from PyQt4.QtCore import Qt
8 from PyQt4.QtCore import SIGNAL
10 import cola
11 from cola import core
12 from cola import utils
13 from cola import qtutils
14 from cola import settings
15 from cola import signals
16 from cola import resources
17 from cola.qtutils import SLOT
18 from cola.views import about
19 from cola.views.syntax import DiffSyntaxHighlighter
20 from cola.views.mainwindow import MainWindow
21 from cola.controllers import compare
22 from cola.controllers import search as smod
23 from cola.controllers import createtag
24 from cola.controllers.bookmark import manage_bookmarks
25 from cola.controllers.bookmark import save_bookmark
26 from cola.controllers.createbranch import create_new_branch
27 from cola.controllers.merge import local_merge
28 from cola.controllers.merge import abort_merge
29 from cola.controllers.options import update_options
30 from cola.controllers.util import choose_from_combo
31 from cola.controllers.util import choose_from_list
32 from cola.controllers.remote import remote_action
33 from cola.controllers.repobrowser import browse_git_branch
34 from cola.controllers.stash import stash
35 from cola.controllers.selectcommits import select_commits
37 class MainView(MainWindow):
38 """The main cola interface."""
39 idx_header = -1
40 idx_staged = 0
41 idx_modified = 1
42 idx_unmerged = 2
43 idx_untracked = 3
44 idx_end = 4
46 # Read-only mode property
47 mode = property(lambda self: self.model.mode)
49 def __init__(self, parent=None):
50 MainWindow.__init__(self, parent)
51 self.amend_is_checked = self.amend_checkbox.isChecked
53 # Qt does not support noun/verbs
54 self.commit_button.setText(qtutils.tr('Commit@@verb'))
55 self.commit_menu.setTitle(qtutils.tr('Commit@@verb'))
57 # Diff/patch syntax highlighter
58 self.syntax = DiffSyntaxHighlighter(self.display_text.document())
60 # Display the current column
61 self.connect(self.commitmsg,
62 SIGNAL('cursorPositionChanged()'),
63 self.show_cursor_position)
65 # Keeps track of merge messages we've seen
66 self.merge_message_hash = ''
68 # Initialize the seen tree widget indexes
69 self._seen_indexes = set()
71 # Initialize the GUI to show 'Column: 00'
72 self.show_cursor_position()
74 # Internal field used by import/export_state().
75 # Change this whenever dockwidgets are removed.
76 self._widget_version = 1
78 self.model = cola.model()
79 self.model.add_message_observer(self.model.message_updated,
80 self._update_view)
82 # Listen for text and amend messages
83 cola.notifier().listen(signals.diff_text, self.set_display)
84 cola.notifier().listen(signals.mode, self._mode_changed)
85 cola.notifier().listen(signals.inotify, self._inotify_enabled)
86 cola.notifier().listen(signals.amend, self.amend_checkbox.setChecked)
88 # Broadcast the amend mode
89 self.connect(self.amend_checkbox, SIGNAL('toggled(bool)'),
90 SLOT(signals.amend_mode))
92 # Add button callbacks
93 self._relay_button(self.alt_button, signals.reset_mode)
94 self._relay_button(self.rescan_button, signals.rescan)
95 self._relay_button(self.signoff_button, signals.add_signoff)
97 self._connect_button(self.stage_button, self.stage)
98 self._connect_button(self.unstage_button, self.unstage)
99 self._connect_button(self.commit_button, self.commit)
100 self._connect_button(self.fetch_button, self.fetch)
101 self._connect_button(self.push_button, self.push)
102 self._connect_button(self.pull_button, self.pull)
103 self._connect_button(self.stash_button, stash)
105 # Menu actions
106 actions = (
107 (self.menu_quit, self.close),
108 (self.menu_branch_compare, compare.branch_compare),
109 (self.menu_branch_diff, self.branch_diff),
110 (self.menu_branch_review, self.review_branch),
111 (self.menu_browse_branch, self.browse_current),
112 (self.menu_browse_other_branch, self.browse_other),
113 (self.menu_browse_commits, self.browse_commits),
114 (self.menu_create_tag, createtag.create_tag),
115 (self.menu_create_branch, create_new_branch),
116 (self.menu_checkout_branch, self.checkout_branch),
117 (self.menu_delete_branch, self.branch_delete),
118 (self.menu_rebase_branch, self.rebase),
119 (self.menu_clone_repo, self.clone_repo),
120 (self.menu_commit_compare, compare.compare),
121 (self.menu_commit_compare_file, compare.compare_file),
122 (self.menu_cherry_pick, self.cherry_pick),
123 (self.menu_diff_expression, self.diff_expression),
124 (self.menu_diff_branch, self.diff_branch),
125 (self.menu_export_patches, self.export_patches),
126 (self.menu_help_about, about.launch_about_dialog),
127 (self.menu_help_docs,
128 lambda: self.model.git.web__browse(resources.html_docs())),
129 (self.menu_load_commitmsg, self.load_commitmsg),
130 (self.menu_load_commitmsg_template, self.load_template),
131 (self.menu_manage_bookmarks, manage_bookmarks),
132 (self.menu_save_bookmark, save_bookmark),
133 (self.menu_merge_local, local_merge),
134 (self.menu_merge_abort, abort_merge),
135 (self.menu_open_repo, self.open_repo),
136 (self.menu_options, update_options),
137 (self.menu_rescan, SLOT(signals.rescan)),
138 (self.menu_search_grep, self.grep),
139 (self.menu_search_revision, smod.search(smod.REVISION_ID)),
140 (self.menu_search_revision_range, smod.search(smod.REVISION_RANGE)),
141 (self.menu_search_message, smod.search(smod.MESSAGE)),
142 (self.menu_search_path, smod.search(smod.PATH, True)),
143 (self.menu_search_date_range, smod.search(smod.DATE_RANGE)),
144 (self.menu_search_diff, smod.search(smod.DIFF)),
145 (self.menu_search_author, smod.search(smod.AUTHOR)),
146 (self.menu_search_committer, smod.search(smod.COMMITTER)),
147 (self.menu_show_diffstat, SLOT(signals.diffstat)),
148 (self.menu_stash, stash),
149 (self.menu_stage_modified, SLOT(signals.stage_modified)),
150 (self.menu_stage_untracked, SLOT(signals.stage_untracked)),
151 (self.menu_unstage_selected, SLOT(signals.unstage_selected)),
152 (self.menu_unstage_all, SLOT(signals.unstage_all)),
153 (self.menu_visualize_all, SLOT(signals.visualize_all)),
154 (self.menu_visualize_current, SLOT(signals.visualize_current)),
155 # TODO This edit menu stuff should/could be command objects
156 (self.menu_cut, self.action_cut),
157 (self.menu_copy, self.action_copy),
158 (self.menu_paste, self.commitmsg.paste),
159 (self.menu_delete, self.action_delete),
160 (self.menu_select_all, self.commitmsg.selectAll),
161 (self.menu_undo, self.commitmsg.undo),
162 (self.menu_redo, self.commitmsg.redo),
164 for menu, callback in actions:
165 self.connect(menu, SIGNAL('triggered()'), callback)
167 # Install diff shortcut keys for stage/unstage
168 self.display_text.keyPressEvent = self.diff_key_press_event
169 self.display_text.contextMenuEvent = self.diff_context_menu_event
171 # Restore saved settings
172 self._load_gui_state()
174 def _relay_button(self, button, signal):
175 callback = SLOT(signal)
176 self._connect_button(button, callback)
178 def _connect_button(self, button, callback):
179 self.connect(button, SIGNAL('clicked()'), callback)
181 def _inotify_enabled(self, enabled):
182 """Hide the rescan button when inotify is enabled."""
183 if enabled:
184 self.rescan_button.hide()
185 else:
186 self.rescan_button.show()
188 def _update_view(self):
189 """Update the title with the current branch and directory name."""
190 title = '%s [%s]' % (self.model.project,
191 self.model.currentbranch)
192 if self.mode in (self.model.mode_diff, self.model.mode_diff_expr):
193 title += ' *** diff mode***'
194 elif self.mode == self.model.mode_review:
195 title += ' *** review mode***'
196 elif self.mode == self.model.mode_amend:
197 title += ' *** amending ***'
198 self.setWindowTitle(title)
200 if not self.model.read_only() and self.mode != self.model.mode_amend:
201 # Check if there's a message file in .git/
202 merge_msg_path = self.model.merge_message_path()
203 if merge_msg_path is None:
204 return
205 merge_msg_hash = utils.checksum(merge_message_path)
206 if merge_msg_hash == self.merge_msg_hash:
207 return
208 self.merge_msg_hash = merge_msg_hash
209 cola.notifier().broadcast(signals.load_commit_message,
210 merge_msg_path)
212 def _mode_changed(self, mode):
213 """React to mode changes; hide/show the "Exit Diff Mode" button."""
214 if mode in (self.model.mode_review, self.model.mode_diff):
215 self.alt_button.setMinimumHeight(40)
216 self.alt_button.show()
217 else:
218 self.alt_button.setMinimumHeight(1)
219 self.alt_button.hide()
221 def set_display(self, text):
222 """Set the diff text display."""
223 scrollbar = self.display_text.verticalScrollBar()
224 scrollvalue = scrollbar.value()
225 if text is not None:
226 self.display_text.setText(text)
227 scrollbar.setValue(scrollvalue)
229 def action_cut(self):
230 self.action_copy()
231 self.action_delete()
233 def action_copy(self):
234 cursor = self.commitmsg.textCursor()
235 selection = cursor.selection().toPlainText()
236 qtutils.set_clipboard(selection)
238 def action_delete(self):
239 self.commitmsg.textCursor().removeSelectedText()
241 def copy_display(self):
242 cursor = self.display_text.textCursor()
243 selection = cursor.selection().toPlainText()
244 qtutils.set_clipboard(selection)
246 def diff_selection(self):
247 cursor = self.display_text.textCursor()
248 offset = cursor.position()
249 selection = unicode(cursor.selection().toPlainText())
250 return offset, selection
252 def selected_line(self):
253 cursor = self.display_text.textCursor()
254 offset = cursor.position()
255 contents = unicode(self.display_text.toPlainText())
256 while (offset >= 1
257 and contents[offset-1]
258 and contents[offset-1] != '\n'):
259 offset -= 1
260 data = contents[offset:]
261 if '\n' in data:
262 line, rest = data.split('\n', 1)
263 else:
264 line = data
265 return line
267 def show_cursor_position(self):
268 """Update the UI with the current row and column."""
269 cursor = self.commitmsg.textCursor()
270 position = cursor.position()
271 txt = unicode(self.commitmsg.toPlainText())
272 rows = txt[:position].count('\n') + 1
273 cols = cursor.columnNumber()
274 display = ' %d,%d ' % (rows, cols)
275 if cols > 78:
276 display = ('<span style="color: white; '
277 ' background-color: red;"'
278 '>%s</span>' % display.replace(' ', '&nbsp;'))
279 elif cols > 72:
280 display = ('<span style="color: black; '
281 ' background-color: orange;"'
282 '>%s</span>' % display.replace(' ', '&nbsp;'))
283 elif cols > 64:
284 display = ('<span style="color: black; '
285 ' background-color: yellow;"'
286 '>%s</span>' % display.replace(' ', '&nbsp;'))
287 self.position_label.setText(display)
289 def import_state(self, state):
290 """Imports data for save/restore"""
291 MainWindow.import_state(self, state)
292 # Restore the dockwidget, etc. window state
293 if 'windowstate' in state:
294 windowstate = state['windowstate']
295 self.restoreState(QtCore.QByteArray.fromBase64(str(windowstate)),
296 self._widget_version)
298 def export_state(self):
299 """Exports data for save/restore"""
300 state = MainWindow.export_state(self)
301 # Save the window state
302 windowstate = self.saveState(self._widget_version)
303 state['windowstate'] = unicode(windowstate.toBase64().data())
304 return state
306 def review_branch(self):
307 """Diff against an arbitrary revision, branch, tag, etc."""
308 branch = choose_from_combo('Select Branch, Tag, or Commit-ish',
309 self.model.all_branches() +
310 self.model.tags)
311 if not branch:
312 return
313 cola.notifier().broadcast(signals.review_branch_mode, branch)
315 def branch_diff(self):
316 """Diff against an arbitrary revision, branch, tag, etc."""
317 branch = choose_from_combo('Select Branch, Tag, or Commit-ish',
318 ['HEAD^'] +
319 self.model.all_branches() +
320 self.model.tags)
321 if not branch:
322 return
323 cola.notifier().broadcast(signals.diff_mode, branch)
325 def diff_expression(self):
326 """Diff using an arbitrary expression."""
327 expr = choose_from_combo('Enter Diff Expression',
328 self.model.all_branches() +
329 self.model.tags)
330 if not expr:
331 return
332 cola.notifier().broadcast(signals.diff_expr_mode, expr)
335 def diff_branch(self):
336 """Launches a diff against a branch."""
337 branch = choose_from_combo('Select Branch, Tag, or Commit-ish',
338 ['HEAD^'] +
339 self.model.all_branches() +
340 self.model.tags)
341 if not branch:
342 return
343 zfiles_str = self.model.git.diff(branch, name_only=True,
344 no_color=True,
345 z=True).rstrip('\0')
346 files = zfiles_str.split('\0')
347 filename = choose_from_list('Select File', files)
348 if not filename:
349 return
350 cola.notifier().broadcast(signals.branch_mode, branch, filename)
352 def _load_gui_state(self):
353 """Restores the gui from the preferences file."""
354 state = settings.SettingsManager.gui_state(self)
355 self.import_state(state)
357 def load_commitmsg(self):
358 """Load a commit message from a file."""
359 filename = qtutils.open_dialog(self,
360 'Load Commit Message...',
361 self.model.getcwd())
362 if filename:
363 cola.notifier().broadcast(signals.load_commit_message, filename)
366 def load_template(self):
367 """Load the configured commit message template."""
368 template = self.model.global_config('commit.template')
369 if template:
370 cola.notifier().broadcast(signals.load_commit_message, template)
373 def diff_key_press_event(self, event):
374 """Handle shortcut keys in the diff view."""
375 if event.key() != QtCore.Qt.Key_H and event.key() != QtCore.Qt.Key_S:
376 event.ignore()
377 return
378 staged, modified, unmerged, untracked = cola.single_selection()
379 if event.key() == QtCore.Qt.Key_H:
380 if self.mode == self.model.mode_worktree and modified:
381 self.stage_hunk()
382 elif self.mode == self.model.mode_index:
383 self.unstage_hunk()
384 elif event.key() == QtCore.Qt.Key_S:
385 if self.mode == self.model.mode_worktree and modified:
386 self.stage_hunk_selection()
387 elif self.mode == self.model.mode_index:
388 self.unstage_hunk_selection()
390 def process_diff_selection(self, selected=False,
391 staged=True, apply_to_worktree=False,
392 reverse=False):
393 """Implement un/staging of selected lines or hunks."""
394 offset, selection = self.diff_selection()
395 cola.notifier().broadcast(signals.apply_diff_selection,
396 staged,
397 selected,
398 offset,
399 selection,
400 apply_to_worktree)
402 def undo_hunk(self):
403 """Destructively remove a hunk from a worktree file."""
404 if not qtutils.question(self,
405 'Destroy Local Changes?',
406 'This operation will drop '
407 'uncommitted changes.\n'
408 'Continue?',
409 default=False):
410 return
411 self.process_diff_selection(staged=False, apply_to_worktree=True,
412 reverse=True)
414 def undo_selection(self):
415 """Destructively check out content for the selected file from $head."""
416 if not qtutils.question(self,
417 'Destroy Local Changes?',
418 'This operation will drop '
419 'uncommitted changes.\n'
420 'Continue?',
421 default=False):
422 return
423 self.process_diff_selection(staged=False, apply_to_worktree=True,
424 reverse=True, selected=True)
426 def stage(self):
427 """Stage selected files, or all files if no selection exists."""
428 paths = cola.selection_model().unstaged
429 if not paths:
430 cola.notifier().broadcast(signals.stage_modified)
431 else:
432 cola.notifier().broadcast(signals.stage, paths)
434 def unstage(self):
435 """Unstage selected files, or all files if no selection exists."""
436 paths = cola.selection_model().staged
437 if not paths:
438 cola.notifier().broadcast(signals.unstage_all)
439 else:
440 cola.notifier().broadcast(signals.unstage, paths)
442 def stage_hunk(self):
443 """Stage a specific hunk."""
444 self.process_diff_selection(staged=False)
446 def stage_hunk_selection(self):
447 """Stage selected lines."""
448 self.process_diff_selection(staged=False, selected=True)
450 def unstage_hunk(self, cached=True):
451 """Unstage a hunk."""
452 self.process_diff_selection(staged=True)
454 def unstage_hunk_selection(self):
455 """Unstage selected lines."""
456 self.process_diff_selection(staged=True, selected=True)
458 def diff_context_menu_event(self, event):
459 """Create the context menu for the diff display."""
460 menu = self.diff_context_menu_setup()
461 textedit = self.display_text
462 menu.exec_(textedit.mapToGlobal(event.pos()))
464 def diff_context_menu_setup(self):
465 """Set up the context menu for the diff display."""
466 menu = QtGui.QMenu(self)
467 staged, modified, unmerged, untracked = cola.selection()
469 if self.mode == self.model.mode_worktree:
470 if modified:
471 menu.addAction(self.tr('Stage &Hunk For Commit'),
472 self.stage_hunk)
473 menu.addAction(self.tr('Stage &Selected Lines'),
474 self.stage_hunk_selection)
475 menu.addSeparator()
476 menu.addAction(self.tr('Undo Hunk'), self.undo_hunk)
477 menu.addAction(self.tr('Undo Selection'), self.undo_selection)
479 elif self.mode == self.model.mode_index:
480 menu.addAction(self.tr('Unstage &Hunk From Commit'), self.unstage_hunk)
481 menu.addAction(self.tr('Unstage &Selected Lines'), self.unstage_hunk_selection)
483 elif self.mode == self.model.mode_branch:
484 menu.addAction(self.tr('Apply Diff to Work Tree'), self.stage_hunk)
485 menu.addAction(self.tr('Apply Diff Selection to Work Tree'), self.stage_hunk_selection)
487 elif self.mode == self.model.mode_grep:
488 menu.addAction(self.tr('Go Here'), self.goto_grep)
490 menu.addSeparator()
491 menu.addAction(self.tr('Copy'), self.copy_display)
492 return menu
494 def fetch(self):
495 """Launch the 'fetch' remote dialog."""
496 remote_action(self, 'fetch')
498 def push(self):
499 """Launch the 'push' remote dialog."""
500 remote_action(self, 'push')
502 def pull(self):
503 """Launch the 'pull' remote dialog."""
504 remote_action(self, 'pull')
506 def commit(self):
507 """Attempt to create a commit from the index and commit message."""
508 #self.reset_mode()
509 msg = self.model.commitmsg
510 if not msg:
511 # Describe a good commit message
512 error_msg = self.tr(''
513 'Please supply a commit message.\n\n'
514 'A good commit message has the following format:\n\n'
515 '- First line: Describe in one sentence what you did.\n'
516 '- Second line: Blank\n'
517 '- Remaining lines: Describe why this change is good.\n')
518 qtutils.log(1, error_msg)
519 cola.notifier().broadcast(signals.information,
520 'Missing Commit Message',
521 error_msg)
522 return
523 if not self.model.staged:
524 error_msg = self.tr(''
525 'No changes to commit.\n\n'
526 'You must stage at least 1 file before you can commit.\n')
527 qtutils.log(1, error_msg)
528 cola.notifier().broadcast(signals.information,
529 'No Staged Changes',
530 error_msg)
531 return
532 # Warn that amending published commits is generally bad
533 amend = self.amend_is_checked()
534 if (amend and self.model.is_commit_published() and
535 not qtutils.question(self,
536 'Rewrite Published Commit?',
537 'This commit has already been published.\n'
538 'You are rewriting published history.\n'
539 'You probably don\'t want to do this.\n\n'
540 'Continue?',
541 default=False)):
542 return
543 # Perform the commit
544 cola.notifier().broadcast(signals.commit, amend, msg)
546 def grep(self):
547 """Prompt and use 'git grep' to find the content."""
548 # This should be a command in cola.commands.
549 txt, ok = qtutils.prompt('grep')
550 if not ok:
551 return
552 cola.notifier().broadcast(signals.grep, txt)
554 def goto_grep(self):
555 """Called when Search -> Grep's right-click 'goto' action."""
556 line = self.selected_line()
557 filename, line_number, contents = line.split(':', 2)
558 filename = core.encode(filename)
559 cola.notifier().broadcast(signals.edit, [filename], line_number=line_number)
561 def open_repo(self):
562 """Spawn a new cola session."""
563 dirname = qtutils.opendir_dialog(self,
564 'Open Git Repository...',
565 self.model.getcwd())
566 if not dirname:
567 return
568 cola.notifier().broadcast(signals.open_repo, dirname)
570 def clone_repo(self):
571 """Clone a git repository."""
572 url, ok = qtutils.prompt('Path or URL to clone (Env. $VARS okay)')
573 url = os.path.expandvars(url)
574 if not ok or not url:
575 return
576 try:
577 # Pick a suitable basename by parsing the URL
578 newurl = url.replace('\\', '/')
579 default = newurl.rsplit('/', 1)[-1]
580 if default == '.git':
581 # The end of the URL is /.git, so assume it's a file path
582 default = os.path.basename(os.path.dirname(newurl))
583 if default.endswith('.git'):
584 # The URL points to a bare repo
585 default = default[:-4]
586 if url == '.':
587 # The URL is the current repo
588 default = os.path.basename(os.getcwd())
589 if not default:
590 raise
591 except:
592 cola.notifier().broadcast(signals.information,
593 'Error Cloning',
594 'Could not parse: "%s"' % url)
595 qtutils.log(1, 'Oops, could not parse git url: "%s"' % url)
596 return
598 # Prompt the user for a directory to use as the parent directory
599 msg = 'Select a parent directory for the new clone'
600 dirname = qtutils.opendir_dialog(self, msg, self.model.getcwd())
601 if not dirname:
602 return
603 count = 1
604 destdir = os.path.join(dirname, default)
605 olddestdir = destdir
606 if os.path.exists(destdir):
607 # An existing path can be specified
608 msg = ('"%s" already exists, cola will create a new directory' %
609 destdir)
610 cola.notifier().broadcast(signals.information,
611 'Directory Exists', msg)
613 # Make sure the new destdir doesn't exist
614 while os.path.exists(destdir):
615 destdir = olddestdir + str(count)
616 count += 1
617 cola.notifier().broadcast(signals.clone, url, destdir)
619 def cherry_pick(self):
620 """Launch the 'Cherry-Pick' dialog."""
621 revs, summaries = self.model.log_helper(all=True)
622 commits = select_commits('Cherry-Pick Commit',
623 revs, summaries, multiselect=False)
624 if not commits:
625 return
626 cola.notifier().broadcast(signals.cherry_pick, commits)
628 def browse_commits(self):
629 """Launch the 'Browse Commits' dialog."""
630 revs, summaries = self.model.log_helper(all=True)
631 select_commits('Browse Commits', revs, summaries)
633 def export_patches(self):
634 """Run 'git format-patch' on a list of commits."""
635 revs, summaries = self.model.log_helper()
636 to_export = select_commits('Export Patches', revs, summaries)
637 if not to_export:
638 return
639 to_export.reverse()
640 revs.reverse()
641 cola.notifier().broadcast(signals.format_patch, to_export, revs)
643 def browse_current(self):
644 """Launch the 'Browse Current Branch' dialog."""
645 branch = self.model.currentbranch
646 browse_git_branch(branch)
648 def browse_other(self):
649 """Prompt for a branch and inspect content at that point in time."""
650 # Prompt for a branch to browse
651 branch = choose_from_combo('Browse Branch Files',
652 self.view,
653 self.model.all_branches())
654 if not branch:
655 return
656 # Launch the repobrowser
657 browse_git_branch(branch)
659 def branch_create(self):
660 """Launch the 'Create Branch' dialog."""
661 create_new_branch()
663 def branch_delete(self):
664 """Launch the 'Delete Branch' dialog."""
665 branch = choose_from_combo('Delete Branch',
666 self.model.local_branches)
667 if not branch:
668 return
669 cola.notifier().broadcast(signals.delete_branch, branch)
671 def checkout_branch(self):
672 """Launch the 'Checkout Branch' dialog."""
673 branch = choose_from_combo('Checkout Branch',
674 self.model.local_branches)
675 if not branch:
676 return
677 cola.notifier().broadcast(signals.checkout_branch, branch)
679 def rebase(self):
680 """Rebase onto a branch."""
681 branch = choose_from_combo('Rebase Branch',
682 self.model.all_branches())
683 if not branch:
684 return
685 #TODO cmd
686 status, output = self.model.git.rebase(branch,
687 with_stderr=True,
688 with_status=True)
689 qtutils.log(status, output)