5 from PyQt4
import QtGui
6 from PyQt4
.QtGui
import QDialog
7 from PyQt4
.QtGui
import QMessageBox
8 from PyQt4
.QtGui
import QMenu
9 from PyQt4
.QtGui
import QFont
14 from qobserver
import QObserver
15 from repobrowsercontroller
import browse_git_branch
16 from createbranchcontroller
import create_new_branch
17 from pushcontroller
import push_branches
18 from utilcontroller
import choose_branch
19 from utilcontroller
import select_commits
20 from utilcontroller
import find_revisions
21 from utilcontroller
import update_options
22 from utilcontroller
import log_window
24 class Controller(QObserver
):
25 '''Controller manages the interaction between the model and views.'''
27 def __init__(self
, model
, view
):
28 QObserver
.__init
__(self
, model
, view
)
30 # parent-less log window
31 qtutils
.LOGGER
= log_window(model
, QtGui
.qApp
.activeWindow())
33 # Avoids inotify floods from e.g. make
34 self
.__last
_inotify
_event
= time
.time()
36 # The unstaged list context menu
37 self
.__unstaged
_menu
= None
39 # The diff-display context menu
40 self
.__diff
_menu
= None
41 self
.__staged
_diff
_in
_view
= True
42 self
.__diffgui
_enabled
= True
44 # Unstaged changes context menu
45 view
.unstaged
.contextMenuEvent
= self
.unstaged_context_menu_event
47 # Diff display context menu
48 view
.display_text
.contextMenuEvent
= self
.diff_context_menu_event
50 # Binds model params to their equivalent view widget
51 self
.add_observables('commitmsg', 'staged', 'unstaged')
53 # When a model attribute changes, this runs a specific action
54 self
.add_actions('staged', self
.action_staged
)
55 self
.add_actions('unstaged', self
.action_unstaged
)
56 self
.add_actions('global_ugit_fontdiff', self
.update_diff_font
)
57 self
.add_actions('global_ugit_fontui', self
.update_ui_font
)
60 # Actions that delegate directly to the model
61 signoff_button
= model
.add_signoff
,
62 menu_get_prev_commitmsg
= model
.get_prev_commitmsg
,
64 lambda: self
.log(self
.model
.stage_modified()),
65 menu_stage_untracked
=
66 lambda: self
.log(self
.model
.stage_untracked()),
68 lambda: self
.log(self
.model
.unstage_all()),
70 # Actions that delegate direclty to the view
71 menu_cut
= view
.action_cut
,
72 menu_copy
= view
.action_copy
,
73 menu_paste
= view
.action_paste
,
74 menu_delete
= view
.action_delete
,
75 menu_select_all
= view
.action_select_all
,
76 menu_undo
= view
.action_undo
,
77 menu_redo
= view
.action_redo
,
80 stage_button
= self
.stage_selected
,
81 commit_button
= self
.commit
,
82 push_button
= self
.push
,
85 staged
= self
.diff_staged
,
86 unstaged
= self
.diff_unstaged
,
89 untracked_checkbox
= self
.rescan
,
92 menu_load_commitmsg
= self
.load_commitmsg
,
93 menu_quit
= self
.quit_app
,
96 menu_visualize_current
= self
.viz_current
,
97 menu_visualize_all
= self
.viz_all
,
98 menu_show_revision
= self
.show_revision
,
99 menu_browse_commits
= self
.browse_commits
,
100 menu_browse_branch
= self
.browse_current
,
101 menu_browse_other_branch
= self
.browse_other
,
104 menu_rescan
= self
.rescan
,
105 menu_create_branch
= self
.branch_create
,
106 menu_delete_branch
= self
.branch_delete
,
107 menu_checkout_branch
= self
.checkout_branch
,
108 menu_rebase_branch
= self
.rebase
,
109 menu_commit
= self
.commit
,
110 menu_stage_selected
= self
.stage_selected
,
111 menu_unstage_selected
= self
.unstage_selected
,
112 menu_show_diffstat
= self
.show_diffstat
,
113 menu_show_index
= self
.show_index
,
114 menu_export_patches
= self
.export_patches
,
115 menu_cherry_pick
= self
.cherry_pick
,
117 menu_options
= self
.options
,
120 splitter_top
= self
.splitter_top_event
,
121 splitter_bottom
= self
.splitter_bottom_event
,
124 # These are vanilla signal/slots since QObserver
125 # is already handling these signals.
126 self
.connect(view
.unstaged
,
127 'itemDoubleClicked(QListWidgetItem*)',
129 self
.connect(view
.staged
,
130 'itemDoubleClicked(QListWidgetItem*)',
131 self
.unstage_selected
)
134 self
.connect(self
.view
.toolbar_show_log
,
135 'triggered()', self
.show_log
)
136 # Delegate window events here
137 view
.moveEvent
= self
.move_event
138 view
.resizeEvent
= self
.resize_event
139 view
.closeEvent
= self
.quit_app
140 view
.staged
.mousePressEvent
= self
.click_staged
141 view
.unstaged
.mousePressEvent
= self
.click_unstaged
143 self
.load_window_geom()
144 self
.init_log_window()
145 self
.start_inotify_thread()
149 #####################################################################
150 # handle when list item icons are clicked
151 def click_event(self
, widget
, action_callback
, event
):
152 result
= QtGui
.QListWidget
.mousePressEvent(widget
, event
)
153 xpos
= event
.pos().x()
154 if xpos
> 5 and xpos
< 20:
158 def click_staged(self
, event
):
159 return self
.click_event(
161 self
.unstage_selected
,
164 def click_unstaged(self
, event
):
165 return self
.click_event(
171 #####################################################################
172 # event() is called in response to messages from the inotify thread
173 def event(self
, msg
):
174 if msg
.type() == defaults
.INOTIFY_EVENT
:
180 #####################################################################
181 # Actions triggered during model updates
183 def action_staged(self
, widget
):
184 qtutils
.update_listwidget(widget
,
185 self
.model
.get_staged(), staged
=True)
187 def action_unstaged(self
, widget
):
188 qtutils
.update_listwidget(widget
,
189 self
.model
.get_modified(), staged
=False)
191 if self
.view
.untracked_checkbox
.isChecked():
192 qtutils
.update_listwidget(widget
,
193 self
.model
.get_untracked(),
198 #####################################################################
201 def show_log(self
, *rest
):
202 qtutils
.toggle_log_window()
205 update_options(self
.model
, self
.view
)
207 def branch_create(self
):
208 if create_new_branch(self
.model
, self
.view
):
211 def branch_delete(self
):
212 branch
= choose_branch('Delete Branch',
213 self
.view
, self
.model
.get_local_branches())
214 if not branch
: return
215 self
.log(self
.model
.delete_branch(branch
))
217 def browse_current(self
):
218 branch
= self
.model
.get_branch()
219 browse_git_branch(self
.model
, self
.view
, branch
)
221 def browse_other(self
):
222 # Prompt for a branch to browse
223 branch
= choose_branch('Browse Branch Files',
224 self
.view
, self
.model
.get_all_branches())
225 if not branch
: return
226 # Launch the repobrowser
227 browse_git_branch(self
.model
, self
.view
, branch
)
229 def checkout_branch(self
):
230 branch
= choose_branch('Checkout Branch',
231 self
.view
, self
.model
.get_local_branches())
232 if not branch
: return
233 self
.log(self
.model
.checkout(branch
))
235 def browse_commits(self
):
236 self
.select_commits_gui(self
.tr('Browse Commits'),
237 *self
.model
.log(all
=True))
239 def show_revision(self
):
240 find_revisions(self
.model
, self
.view
)
242 def cherry_pick(self
):
243 commits
= self
.select_commits_gui(self
.tr('Cherry-Pick Commits'),
244 *self
.model
.log(all
=True))
245 if not commits
: return
246 self
.log(self
.model
.cherry_pick(commits
))
249 msg
= self
.model
.get_commitmsg()
251 error_msg
= self
.tr(""
252 + "Please supply a commit message.\n"
254 + "A good commit message has the following format:\n"
256 + "- First line: Describe in one sentence what you did.\n"
257 + "- Second line: Blank\n"
258 + "- Remaining lines: Describe why this change is good.\n")
262 files
= self
.model
.get_staged()
264 error_msg
= self
.tr(""
265 + "No changes to commit.\n"
267 + "You must stage at least 1 file before you can commit.\n")
272 output
= self
.model
.commit(
273 msg
, amend
=self
.view
.amend_radio
.isChecked())
276 self
.view
.new_commit_radio
.setChecked(True)
277 self
.view
.amend_radio
.setChecked(False)
278 self
.model
.set_commitmsg('')
281 def view_diff(self
, staged
=True):
282 self
.__staged
_diff
_in
_view
= staged
283 if self
.__staged
_diff
_in
_view
:
284 widget
= self
.view
.staged
286 widget
= self
.view
.unstaged
287 row
, selected
= qtutils
.get_selected_row(widget
)
289 self
.view
.reset_display()
290 self
.__diffgui
_enabled
= False
293 status
) = self
.model
.get_diff_and_status(row
, staged
=staged
)
295 self
.view
.set_display(diff
)
296 self
.view
.set_info(self
.tr(status
))
297 self
.__diffgui
_enabled
= True
299 # use *rest to handle being called from different signals
300 def diff_staged(self
, *rest
):
301 self
.view_diff(staged
=True)
303 # use *rest to handle being called from different signals
304 def diff_unstaged(self
, *rest
):
305 self
.view_diff(staged
=False)
307 def export_patches(self
):
308 (revs
, summaries
) = self
.model
.log()
309 commits
= self
.select_commits_gui(self
.tr('Export Patches'),
311 if not commits
: return
312 self
.log(self
.model
.format_patch(commits
))
314 def quit_app(self
,*rest
):
315 '''Save config settings and cleanup any inotify threads.'''
317 self
.model
.save_window_geom()
318 qtutils
.close_log_window()
321 if not self
.inotify_thread
: return
322 if not self
.inotify_thread
.isRunning(): return
324 self
.inotify_thread
.abort
= True
325 self
.inotify_thread
.terminate()
326 self
.inotify_thread
.wait()
328 def load_commitmsg(self
):
329 file = qtutils
.open_dialog(self
.view
,
330 'Load Commit Message...', defaults
.DIRECTORY
)
333 defaults
.DIRECTORY
= os
.path
.dirname(file)
334 slushy
= utils
.slurp(file)
335 if slushy
: self
.model
.set_commitmsg(slushy
)
338 branch
= choose_branch('Rebase Branch',
339 self
.view
, self
.model
.get_local_branches())
340 if not branch
: return
341 self
.log(self
.model
.rebase(branch
))
343 # use *rest to handle being called from the checkbox signal
344 def rescan(self
, *rest
):
345 '''Populates view widgets with results from "git status."'''
347 # save entire selection
348 unstaged
= qtutils
.get_selection_list(
350 self
.model
.get_unstaged())
351 staged
= qtutils
.get_selection_list(
353 self
.model
.get_staged())
355 scrollbar
= self
.view
.display_text
.verticalScrollBar()
356 scrollvalue
= scrollbar
.value()
359 unstageditem
= qtutils
.get_selected_item(
361 self
.model
.get_unstaged())
363 stageditem
= qtutils
.get_selected_item(
365 self
.model
.get_staged())
368 self
.model
.update_status()
371 update_staged
= False
372 update_unstaged
= False
373 updated_unstaged
= self
.model
.get_unstaged()
374 updated_staged
= self
.model
.get_staged()
376 for item
in unstaged
:
377 if item
in updated_unstaged
:
378 idx
= updated_unstaged
.index(item
)
379 listitem
= self
.view
.unstaged
.item(idx
)
381 listitem
.setSelected(True)
383 .setItemSelected(listitem
, True)
384 update_unstaged
= True
385 self
.view
.unstaged
.update()
387 if item
in updated_staged
:
388 idx
= updated_staged
.index(item
)
389 listitem
= self
.view
.staged
.item(idx
)
391 listitem
.setSelected(True)
393 .setItemSelected(listitem
, True)
396 # restore selected item
397 if update_staged
and stageditem
:
398 idx
= updated_staged
.index(stageditem
)
399 item
= self
.view
.staged
.item(idx
)
400 self
.view
.staged
.setCurrentItem(item
)
402 scrollbar
.setValue(scrollvalue
)
404 elif update_unstaged
and unstageditem
:
405 idx
= updated_unstaged
.index(unstageditem
)
406 item
= self
.view
.unstaged
.item(idx
)
407 self
.view
.unstaged
.setCurrentItem(item
)
408 self
.view_diff(False)
409 scrollbar
.setValue(scrollvalue
)
411 self
.view
.setWindowTitle('%s [%s]' % (
412 self
.model
.get_project(),
413 self
.model
.get_branch()))
415 if self
.model
.has_squash_msg():
416 if self
.model
.get_commitmsg():
417 answer
= qtutils
.question(self
.view
,
418 self
.tr('Import Commit Message?'),
419 self
.tr('A commit message from an in-progress'
420 + ' merge was found.\nImport it?'))
423 self
.model
.set_squash_msg()
425 # Set the new commit message
426 self
.model
.set_squash_msg()
429 push_branches(self
.model
, self
.view
)
431 def show_diffstat(self
):
432 '''Show the diffstat from the latest commit.'''
433 self
.__diffgui
_enabled
= False
434 self
.view
.set_info(self
.tr('Diffstat'))
435 self
.view
.set_display(self
.model
.diffstat())
437 def show_index(self
):
438 self
.__diffgui
_enabled
= False
439 self
.view
.set_info(self
.tr('Index'))
440 self
.view
.set_display(self
.model
.diffindex())
442 #####################################################################
444 def process_diff_selection(self
, items
, widget
,
445 cached
=True, selected
=False, reverse
=True, noop
=False):
447 filename
= qtutils
.get_selected_item(widget
, items
)
448 if not filename
: return
449 parser
= utils
.DiffParser(self
.model
, filename
=filename
,
451 offset
, selection
= self
.view
.diff_selection()
452 parser
.process_diff_selection(selected
, offset
, selection
)
455 def stage_hunk(self
):
456 self
.process_diff_selection(
457 self
.model
.get_unstaged(),
461 def stage_hunk_selection(self
):
462 self
.process_diff_selection(
463 self
.model
.get_unstaged(),
468 def unstage_hunk(self
, cached
=True):
469 self
.process_diff_selection(
470 self
.model
.get_staged(),
474 def unstage_hunk_selection(self
):
475 self
.process_diff_selection(
476 self
.model
.get_staged(),
481 # #######################################################################
484 # use *rest to handle being called from different signals
485 def stage_selected(self
,*rest
):
486 '''Use "git add" to add items to the git index.
487 This is a thin wrapper around apply_to_list.'''
488 command
= self
.model
.add_or_remove
489 widget
= self
.view
.unstaged
490 items
= self
.model
.get_unstaged()
491 self
.apply_to_list(command
,widget
,items
)
493 # use *rest to handle being called from different signals
494 def unstage_selected(self
, *rest
):
495 '''Use "git reset" to remove items from the git index.
496 This is a thin wrapper around apply_to_list.'''
497 command
= self
.model
.reset
498 widget
= self
.view
.staged
499 items
= self
.model
.get_staged()
500 self
.apply_to_list(command
, widget
, items
)
502 def undo_changes(self
):
503 """Reverts local changes back to whatever's in HEAD."""
504 widget
= self
.view
.unstaged
505 items
= self
.model
.get_unstaged()
506 potential_items
= qtutils
.get_selection_list(widget
, items
)
508 untracked
= self
.model
.get_untracked()
509 for item
in potential_items
:
510 if item
not in untracked
:
511 items_to_undo
.append(item
)
513 answer
= qtutils
.question(self
.view
,
514 self
.tr('Destroy Local Changes?'),
515 self
.tr('This operation will drop all '
516 + ' uncommitted changes. Continue?'),
519 if not answer
: return
521 output
= self
.model
.checkout('HEAD', '--',
523 self
.log('git checkout HEAD -- '
524 + ' '.join(items_to_undo
)
527 msg
= 'No files selected for checkout from HEAD.'
528 self
.log(self
.tr(msg
))
531 '''Visualizes the entire git history using gitk.'''
532 browser
= self
.model
.get_global_ugit_historybrowser()
533 utils
.fork(browser
,'--all')
535 def viz_current(self
):
536 '''Visualizes the current branch's history using gitk.'''
537 browser
= self
.model
.get_global_ugit_historybrowser()
538 utils
.fork(browser
, self
.model
.get_branch())
540 # These actions monitor window resizes, splitter changes, etc.
541 def move_event(self
, event
):
542 defaults
.X
= event
.pos().x()
543 defaults
.Y
= event
.pos().y()
545 def resize_event(self
, event
):
546 defaults
.WIDTH
= event
.size().width()
547 defaults
.HEIGHT
= event
.size().height()
549 def splitter_top_event(self
,*rest
):
550 sizes
= self
.view
.splitter_top
.sizes()
551 defaults
.SPLITTER_TOP_0
= sizes
[0]
552 defaults
.SPLITTER_TOP_1
= sizes
[1]
554 def splitter_bottom_event(self
,*rest
):
555 sizes
= self
.view
.splitter_bottom
.sizes()
556 defaults
.SPLITTER_BOTTOM_0
= sizes
[0]
557 defaults
.SPLITTER_BOTTOM_1
= sizes
[1]
559 def load_window_geom(self
):
562 sb0
,sb1
) = self
.model
.get_window_geom()
563 self
.view
.resize(w
,h
)
565 self
.view
.splitter_top
.setSizes([st0
,st1
])
566 self
.view
.splitter_bottom
.setSizes([sb0
,sb1
])
568 def log(self
, output
, rescan
=True, quiet
=False):
569 '''Logs output and optionally rescans for changes.'''
570 qtutils
.log(output
, quiet
=quiet
, doraise
=False)
571 if rescan
: self
.rescan()
573 #####################################################################
576 def apply_to_list(self
, command
, widget
, items
):
577 '''This is a helper method that retrieves the current
578 selection list, applies a command to that list,
579 displays a dialog showing the output of that command,
580 and calls rescan to pickup changes.'''
581 apply_items
= qtutils
.get_selection_list(widget
, items
)
582 output
= command(apply_items
)
583 self
.log(output
, quiet
=True)
585 def unstaged_context_menu_event(self
, event
):
586 self
.unstaged_context_menu_setup()
587 unstaged
= self
.view
.unstaged
588 self
.__unstaged
_menu
.exec_(unstaged
.mapToGlobal(event
.pos()))
590 def unstaged_context_menu_setup(self
):
591 if self
.__unstaged
_menu
: return
593 menu
= self
.__unstaged
_menu
= QMenu(self
.view
)
594 self
.__stage
_selected
_action
= menu
.addAction(
595 self
.tr('Stage Selected'), self
.stage_selected
)
596 self
.__undo
_changes
_action
= menu
.addAction(
597 self
.tr('Undo Local Changes'), self
.undo_changes
)
598 self
.connect(self
.__unstaged
_menu
, 'aboutToShow()',
599 self
.unstaged_context_menu_about_to_show
)
601 def unstaged_context_menu_about_to_show(self
):
602 unstaged_item
= qtutils
.get_selected_item(
604 self
.model
.get_unstaged())
606 is_tracked
= unstaged_item
not in self
.model
.get_untracked()
608 enable_staging
= bool(self
.__diffgui
_enabled
610 enable_undo
= enable_staging
and is_tracked
612 self
.__stage
_selected
_action
.setEnabled(enable_staging
)
613 self
.__undo
_changes
_action
.setEnabled(enable_undo
)
615 def diff_context_menu_about_to_show(self
):
616 unstaged_item
= qtutils
.get_selected_item(
618 self
.model
.get_unstaged())
620 is_tracked
= unstaged_item
not in self
.model
.get_untracked()
623 self
.__diffgui
_enabled
625 and not self
.__staged
_diff
_in
_view
629 self
.__diffgui
_enabled
630 and self
.__staged
_diff
_in
_view
631 and qtutils
.get_selected_item(
633 self
.model
.get_staged()))
635 self
.__stage
_hunk
_action
.setEnabled(bool(enable_staged
))
636 self
.__stage
_hunk
_selection
_action
.setEnabled(bool(enable_staged
))
638 self
.__unstage
_hunk
_action
.setEnabled(bool(enable_unstaged
))
639 self
.__unstage
_hunk
_selection
_action
.setEnabled(bool(enable_unstaged
))
641 def diff_context_menu_event(self
, event
):
642 self
.diff_context_menu_setup()
643 textedit
= self
.view
.display_text
644 self
.__diff
_menu
.exec_(textedit
.mapToGlobal(event
.pos()))
646 def diff_context_menu_setup(self
):
647 if self
.__diff
_menu
: return
649 menu
= self
.__diff
_menu
= QMenu(self
.view
)
650 self
.__stage
_hunk
_action
= menu
.addAction(
651 self
.tr('Stage Hunk For Commit'), self
.stage_hunk
)
653 self
.__stage
_hunk
_selection
_action
= menu
.addAction(
654 self
.tr('Stage Selected Lines'),
655 self
.stage_hunk_selection
)
657 self
.__unstage
_hunk
_action
= menu
.addAction(
658 self
.tr('Unstage Hunk From Commit'),
661 self
.__unstage
_hunk
_selection
_action
= menu
.addAction(
662 self
.tr('Unstage Selected Lines'),
663 self
.unstage_hunk_selection
)
665 self
.__copy
_action
= menu
.addAction(
666 self
.tr('Copy'), self
.view
.copy_display
)
668 self
.connect(self
.__diff
_menu
, 'aboutToShow()',
669 self
.diff_context_menu_about_to_show
)
671 def select_commits_gui(self
, title
, revs
, summaries
):
672 return select_commits(self
.model
, self
.view
, title
, revs
, summaries
)
674 def update_diff_font(self
):
675 font
= self
.model
.get_global_ugit_fontdiff()
678 qfont
.fromString(font
)
679 self
.view
.display_text
.setFont(qfont
)
680 self
.view
.commitmsg
.setFont(qfont
)
682 def update_ui_font(self
):
683 font
= self
.model
.get_global_ugit_fontui()
686 qfont
.fromString(font
)
687 QtGui
.qApp
.setFont(qfont
)
689 def init_log_window(self
):
690 branch
, version
= self
.model
.get_branch(), defaults
.VERSION
691 qtutils
.log(self
.model
.get_git_version()
692 + '\nugit version '+ version
693 + '\nCurrent Branch: '+ branch
)
695 def start_inotify_thread(self
):
696 # Do we have inotify? If not, return.
697 # Recommend installing inotify if we're on Linux.
698 self
.inotify_thread
= None
700 from inotify
import GitNotifier
701 qtutils
.log(self
.tr('inotify support: enabled'))
704 if platform
.system() == 'Linux':
707 'inotify: disabled\n'
708 'Note: To enable inotify, '
709 'install python-pyinotify.\n')
711 plat
= platform
.platform().lower()
712 if 'debian' in plat
or 'ubuntu' in plat
:
714 'On Debian or Ubuntu systems, '
715 'try: sudo apt-get install '
721 # Start the notification thread
722 self
.inotify_thread
= GitNotifier(self
, os
.getcwd())
723 self
.inotify_thread
.start()