1 """This view provides the main git-cola user interface.
5 from PyQt4
import QtGui
6 from PyQt4
import QtCore
7 from PyQt4
.QtCore
import Qt
8 from PyQt4
.QtCore
import SIGNAL
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."""
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
,
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
)
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."""
184 self
.rescan_button
.hide()
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:
205 merge_msg_hash
= utils
.checksum(merge_message_path
)
206 if merge_msg_hash
== self
.merge_msg_hash
:
208 self
.merge_msg_hash
= merge_msg_hash
209 cola
.notifier().broadcast(signals
.load_commit_message
,
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()
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()
226 self
.display_text
.setText(text
)
227 scrollbar
.setValue(scrollvalue
)
229 def action_cut(self
):
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())
257 and contents
[offset
-1]
258 and contents
[offset
-1] != '\n'):
260 data
= contents
[offset
:]
262 line
, rest
= data
.split('\n', 1)
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
)
276 display
= ('<span style="color: white; '
277 ' background-color: red;"'
278 '>%s</span>' % display
.replace(' ', ' '))
280 display
= ('<span style="color: black; '
281 ' background-color: orange;"'
282 '>%s</span>' % display
.replace(' ', ' '))
284 display
= ('<span style="color: black; '
285 ' background-color: yellow;"'
286 '>%s</span>' % display
.replace(' ', ' '))
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())
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() +
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',
319 self
.model
.all_branches() +
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() +
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',
339 self
.model
.all_branches() +
343 zfiles_str
= self
.model
.git
.diff(branch
, name_only
=True,
346 files
= zfiles_str
.split('\0')
347 filename
= choose_from_list('Select File', files
)
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...',
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')
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
:
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
:
382 elif self
.mode
== self
.model
.mode_index
:
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,
393 """Implement un/staging of selected lines or hunks."""
394 offset
, selection
= self
.diff_selection()
395 cola
.notifier().broadcast(signals
.apply_diff_selection
,
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'
411 self
.process_diff_selection(staged
=False, apply_to_worktree
=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'
423 self
.process_diff_selection(staged
=False, apply_to_worktree
=True,
424 reverse
=True, selected
=True)
427 """Stage selected files, or all files if no selection exists."""
428 paths
= cola
.selection_model().unstaged
430 cola
.notifier().broadcast(signals
.stage_modified
)
432 cola
.notifier().broadcast(signals
.stage
, paths
)
435 """Unstage selected files, or all files if no selection exists."""
436 paths
= cola
.selection_model().staged
438 cola
.notifier().broadcast(signals
.unstage_all
)
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
:
471 menu
.addAction(self
.tr('Stage &Hunk For Commit'),
473 menu
.addAction(self
.tr('Stage &Selected Lines'),
474 self
.stage_hunk_selection
)
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
)
491 menu
.addAction(self
.tr('Copy'), self
.copy_display
)
495 """Launch the 'fetch' remote dialog."""
496 remote_action(self
, 'fetch')
499 """Launch the 'push' remote dialog."""
500 remote_action(self
, 'push')
503 """Launch the 'pull' remote dialog."""
504 remote_action(self
, 'pull')
507 """Attempt to create a commit from the index and commit message."""
509 msg
= self
.model
.commitmsg
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',
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
,
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'
544 cola
.notifier().broadcast(signals
.commit
, amend
, msg
)
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')
552 cola
.notifier().broadcast(signals
.grep
, txt
)
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
)
562 """Spawn a new cola session."""
563 dirname
= qtutils
.opendir_dialog(self
,
564 'Open Git Repository...',
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
:
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]
587 # The URL is the current repo
588 default
= os
.path
.basename(os
.getcwd())
592 cola
.notifier().broadcast(signals
.information
,
594 'Could not parse: "%s"' % url
)
595 qtutils
.log(1, 'Oops, could not parse git url: "%s"' % url
)
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())
604 destdir
= os
.path
.join(dirname
, default
)
606 if os
.path
.exists(destdir
):
607 # An existing path can be specified
608 msg
= ('"%s" already exists, cola will create a new directory' %
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
)
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)
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
)
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',
653 self
.model
.all_branches())
656 # Launch the repobrowser
657 browse_git_branch(branch
)
659 def branch_create(self
):
660 """Launch the 'Create Branch' dialog."""
663 def branch_delete(self
):
664 """Launch the 'Delete Branch' dialog."""
665 branch
= choose_from_combo('Delete Branch',
666 self
.model
.local_branches
)
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
)
677 cola
.notifier().broadcast(signals
.checkout_branch
, branch
)
680 """Rebase onto a branch."""
681 branch
= choose_from_combo('Rebase Branch',
682 self
.model
.all_branches())
686 status
, output
= self
.model
.git
.rebase(branch
,
689 qtutils
.log(status
, output
)