5 from PyQt4
import QtCore
6 from PyQt4
import QtGui
7 from PyQt4
.QtGui
import QDialog
8 from PyQt4
.QtGui
import QMessageBox
9 from PyQt4
.QtGui
import QMenu
10 from PyQt4
.QtGui
import QFont
15 from qobserver
import QObserver
16 from repobrowsercontroller
import browse_git_branch
17 from createbranchcontroller
import create_new_branch
18 from pushcontroller
import push_branches
19 from utilcontroller
import choose_branch
20 from utilcontroller
import select_commits
21 from utilcontroller
import find_revisions
22 from utilcontroller
import update_options
23 from utilcontroller
import log_window
25 class Controller(QObserver
):
26 """Controller manages the interaction between the model and views."""
28 def __init__(self
, model
, view
):
29 QObserver
.__init
__(self
, model
, view
)
31 # parent-less log window
32 qtutils
.LOGGER
= log_window(QtGui
.qApp
.activeWindow())
34 # Avoids inotify floods from e.g. make
35 self
.__last
_inotify
_event
= time
.time()
37 # The unstaged list context menu
38 self
.__unstaged
_menu
= None
40 # The diff-display context menu
41 self
.__diff
_menu
= None
42 self
.__staged
_diff
_in
_view
= True
43 self
.__diffgui
_enabled
= True
45 # Unstaged changes context menu
46 view
.unstaged
.contextMenuEvent
= self
.unstaged_context_menu_event
48 # Diff display context menu
49 view
.display_text
.contextMenuEvent
= self
.diff_context_menu_event
51 # Binds model params to their equivalent view widget
52 self
.add_observables('commitmsg', 'staged', 'unstaged')
54 # When a model attribute changes, this runs a specific action
55 self
.add_actions('staged', self
.action_staged
)
56 self
.add_actions('unstaged', self
.action_unstaged
)
57 self
.add_actions('global_ugit_fontdiff', self
.update_diff_font
)
58 self
.add_actions('global_ugit_fontui', self
.update_ui_font
)
61 # Actions that delegate directly to the model
62 signoff_button
= model
.add_signoff
,
63 menu_get_prev_commitmsg
= model
.get_prev_commitmsg
,
65 lambda: self
.log(self
.model
.stage_modified()),
66 menu_stage_untracked
=
67 lambda: self
.log(self
.model
.stage_untracked()),
69 lambda: self
.log(self
.model
.unstage_all()),
71 # Actions that delegate direclty to the view
72 menu_cut
= view
.action_cut
,
73 menu_copy
= view
.action_copy
,
74 menu_paste
= view
.action_paste
,
75 menu_delete
= view
.action_delete
,
76 menu_select_all
= view
.action_select_all
,
77 menu_undo
= view
.action_undo
,
78 menu_redo
= view
.action_redo
,
81 stage_button
= self
.stage_selected
,
82 commit_button
= self
.commit
,
83 push_button
= self
.push
,
86 staged
= self
.diff_staged
,
87 unstaged
= self
.diff_unstaged
,
90 untracked_checkbox
= self
.rescan
,
93 menu_quit
= self
.quit_app
,
94 # menu_load_bookmark = self.load_bookmark,
95 # menu_save_bookmark = self.save_bookmark,
96 # menu_manage_bookmarks = self.manage_bookmarks,
99 menu_search_revision
= self
.search_revision
,
100 # menu_search_revision_range = self.search_revision_range,
101 # menu_search_messages = self.search_messages,
102 # menu_search_date = self.search_date,
103 # menu_search_date_range = self.search_date_range,
104 # menu_search_diffs = self.search_diffs,
107 menu_visualize_current
= self
.viz_current
,
108 menu_visualize_all
= self
.viz_all
,
109 menu_browse_commits
= self
.browse_commits
,
110 menu_browse_branch
= self
.browse_current
,
111 menu_browse_other_branch
= self
.browse_other
,
114 menu_rescan
= self
.rescan
,
115 menu_create_branch
= self
.branch_create
,
116 menu_delete_branch
= self
.branch_delete
,
117 menu_checkout_branch
= self
.checkout_branch
,
118 menu_rebase_branch
= self
.rebase
,
119 menu_commit
= self
.commit
,
120 menu_stage_selected
= self
.stage_selected
,
121 menu_unstage_selected
= self
.unstage_selected
,
122 menu_show_diffstat
= self
.show_diffstat
,
123 menu_show_index
= self
.show_index
,
124 menu_export_patches
= self
.export_patches
,
125 menu_load_commitmsg
= self
.load_commitmsg
,
126 menu_cherry_pick
= self
.cherry_pick
,
129 menu_options
= self
.options
,
132 # Delegate window events here
133 view
.moveEvent
= self
.move_event
134 view
.resizeEvent
= self
.resize_event
135 view
.closeEvent
= self
.quit_app
136 view
.staged
.mousePressEvent
= self
.click_staged
137 view
.unstaged
.mousePressEvent
= self
.click_unstaged
139 # These are vanilla signal/slots since QObserver
140 # is already handling these signals.
141 self
.connect(view
.unstaged
,
142 'itemDoubleClicked(QListWidgetItem*)',
144 self
.connect(view
.staged
,
145 'itemDoubleClicked(QListWidgetItem*)',
146 self
.unstage_selected
)
149 self
.connect(self
.view
.toolbar_show_log
,
150 'triggered()', self
.show_log
)
152 self
.connect(view
.diff_dock
,
153 'topLevelChanged(bool)',
154 lambda(b
): self
.setwindow(view
.diff_dock
, b
))
156 self
.connect(view
.editor_dock
,
157 'topLevelChanged(bool)',
158 lambda(b
): self
.setwindow(view
.editor_dock
, b
))
160 self
.connect(view
.status_dock
,
161 'topLevelChanged(bool)',
162 lambda(b
): self
.setwindow(view
.status_dock
, b
))
164 self
.init_log_window()
165 self
.load_gui_settings()
168 'global_ugit_fontdiff',
169 'global_ugit_fontui',
171 self
.start_inotify_thread()
173 def setwindow(self
, dock
, isfloating
):
175 flags
= ( QtCore
.Qt
.Window
176 | QtCore
.Qt
.FramelessWindowHint
)
177 dock
.setWindowFlags( flags
)
180 #####################################################################
181 # handle when the listitem icons are clicked
182 def click_event(self
, widget
, action_callback
, event
):
183 result
= QtGui
.QListWidget
.mousePressEvent(widget
, event
)
184 xpos
= event
.pos().x()
185 if xpos
> 5 and xpos
< 20:
189 def click_staged(self
, event
):
190 return self
.click_event(
192 self
.unstage_selected
,
195 def click_unstaged(self
, event
):
196 return self
.click_event(
202 #####################################################################
203 # event() is called in response to messages from the inotify thread
204 def event(self
, msg
):
205 if msg
.type() == defaults
.INOTIFY_EVENT
:
211 #####################################################################
212 # Actions triggered during model updates
214 def action_staged(self
, widget
):
215 qtutils
.update_listwidget(widget
,
216 self
.model
.get_staged(), staged
=True)
218 def action_unstaged(self
, widget
):
219 qtutils
.update_listwidget(widget
,
220 self
.model
.get_modified(), staged
=False)
222 if self
.view
.untracked_checkbox
.isChecked():
223 qtutils
.update_listwidget(widget
,
224 self
.model
.get_untracked(),
229 #####################################################################
232 def show_log(self
, *rest
):
233 qtutils
.toggle_log_window()
236 update_options(self
.model
, self
.view
)
238 def branch_create(self
):
239 if create_new_branch(self
.model
, self
.view
):
242 def branch_delete(self
):
243 branch
= choose_branch('Delete Branch',
244 self
.view
, self
.model
.get_local_branches())
245 if not branch
: return
246 self
.log(self
.model
.delete_branch(branch
))
248 def browse_current(self
):
249 branch
= self
.model
.get_branch()
250 browse_git_branch(self
.model
, self
.view
, branch
)
252 def browse_other(self
):
253 # Prompt for a branch to browse
254 branch
= choose_branch('Browse Branch Files',
255 self
.view
, self
.model
.get_all_branches())
256 if not branch
: return
257 # Launch the repobrowser
258 browse_git_branch(self
.model
, self
.view
, branch
)
260 def checkout_branch(self
):
261 branch
= choose_branch('Checkout Branch',
262 self
.view
, self
.model
.get_local_branches())
263 if not branch
: return
264 self
.log(self
.model
.checkout(branch
))
266 def browse_commits(self
):
267 self
.select_commits_gui(self
.tr('Browse Commits'),
268 *self
.model
.log_helper(all
=True))
270 def search_revision(self
):
271 find_revisions(self
.model
, self
.view
)
273 def cherry_pick(self
):
274 commits
= self
.select_commits_gui(self
.tr('Cherry-Pick Commits'),
275 *self
.model
.log_helper(all
=True))
276 if not commits
: return
277 self
.log(self
.model
.cherry_pick_list(commits
))
280 msg
= self
.model
.get_commitmsg()
282 error_msg
= self
.tr(""
283 + "Please supply a commit message.\n"
285 + "A good commit message has the following format:\n"
287 + "- First line: Describe in one sentence what you did.\n"
288 + "- Second line: Blank\n"
289 + "- Remaining lines: Describe why this change is good.\n")
293 files
= self
.model
.get_staged()
295 error_msg
= self
.tr(""
296 + "No changes to commit.\n"
298 + "You must stage at least 1 file before you can commit.\n")
303 output
= self
.model
.commit_with_msg(
304 msg
, amend
=self
.view
.amend_radio
.isChecked())
307 self
.view
.new_commit_radio
.setChecked(True)
308 self
.view
.amend_radio
.setChecked(False)
309 self
.model
.set_commitmsg('')
312 def view_diff(self
, staged
=True):
313 self
.__staged
_diff
_in
_view
= staged
314 if self
.__staged
_diff
_in
_view
:
315 widget
= self
.view
.staged
317 widget
= self
.view
.unstaged
318 row
, selected
= qtutils
.get_selected_row(widget
)
320 self
.view
.reset_display()
321 self
.__diffgui
_enabled
= False
324 status
) = self
.model
.get_diff_and_status(row
, staged
=staged
)
326 self
.view
.set_display(diff
)
327 self
.view
.set_info(self
.tr(status
))
328 self
.view
.diff_dock
.raise_()
329 self
.__diffgui
_enabled
= True
331 # use *rest to handle being called from different signals
332 def diff_staged(self
, *rest
):
333 self
.view_diff(staged
=True)
335 # use *rest to handle being called from different signals
336 def diff_unstaged(self
, *rest
):
337 self
.view_diff(staged
=False)
339 def export_patches(self
):
340 (revs
, summaries
) = self
.model
.log_helper()
341 commits
= self
.select_commits_gui(self
.tr('Export Patches'),
343 if not commits
: return
344 self
.log(self
.model
.format_patch_helper(*commits
))
346 def quit_app(self
,*rest
):
347 """Save config settings and cleanup any inotify threads."""
349 if self
.model
.save_at_exit():
350 self
.model
.save_gui_settings()
351 qtutils
.close_log_window()
354 if not self
.inotify_thread
: return
355 if not self
.inotify_thread
.isRunning(): return
357 self
.inotify_thread
.abort
= True
358 self
.inotify_thread
.terminate()
359 self
.inotify_thread
.wait()
361 def load_commitmsg(self
):
362 file = qtutils
.open_dialog(self
.view
,
363 'Load Commit Message...', defaults
.DIRECTORY
)
366 defaults
.DIRECTORY
= os
.path
.dirname(file)
367 slushy
= utils
.slurp(file)
368 if slushy
: self
.model
.set_commitmsg(slushy
)
371 branch
= choose_branch('Rebase Branch',
372 self
.view
, self
.model
.get_local_branches())
373 if not branch
: return
374 self
.log(self
.model
.rebase(branch
))
376 # use *rest to handle being called from the checkbox signal
377 def rescan(self
, *rest
):
378 '''Populates view widgets with results from "git status."'''
380 # save entire selection
381 unstaged
= qtutils
.get_selection_list(
383 self
.model
.get_unstaged())
384 staged
= qtutils
.get_selection_list(
386 self
.model
.get_staged())
388 scrollbar
= self
.view
.display_text
.verticalScrollBar()
389 scrollvalue
= scrollbar
.value()
392 unstageditem
= qtutils
.get_selected_item(
394 self
.model
.get_unstaged())
396 stageditem
= qtutils
.get_selected_item(
398 self
.model
.get_staged())
401 self
.model
.update_status()
404 update_staged
= False
405 update_unstaged
= False
406 updated_unstaged
= self
.model
.get_unstaged()
407 updated_staged
= self
.model
.get_staged()
409 for item
in unstaged
:
410 if item
in updated_unstaged
:
411 idx
= updated_unstaged
.index(item
)
412 listitem
= self
.view
.unstaged
.item(idx
)
414 listitem
.setSelected(True)
416 .setItemSelected(listitem
, True)
417 update_unstaged
= True
418 self
.view
.unstaged
.update()
420 if item
in updated_staged
:
421 idx
= updated_staged
.index(item
)
422 listitem
= self
.view
.staged
.item(idx
)
424 listitem
.setSelected(True)
426 .setItemSelected(listitem
, True)
429 # restore selected item
430 if update_staged
and stageditem
:
431 idx
= updated_staged
.index(stageditem
)
432 item
= self
.view
.staged
.item(idx
)
433 self
.view
.staged
.setCurrentItem(item
)
435 scrollbar
.setValue(scrollvalue
)
437 elif update_unstaged
and unstageditem
:
438 idx
= updated_unstaged
.index(unstageditem
)
439 item
= self
.view
.unstaged
.item(idx
)
440 self
.view
.unstaged
.setCurrentItem(item
)
441 self
.view_diff(False)
442 scrollbar
.setValue(scrollvalue
)
444 self
.view
.setWindowTitle('%s [%s]' % (
445 self
.model
.get_project(),
446 self
.model
.get_branch()))
448 if self
.model
.has_squash_msg():
449 if self
.model
.get_commitmsg():
450 answer
= qtutils
.question(self
.view
,
451 self
.tr('Import Commit Message?'),
452 self
.tr('A commit message from an in-progress'
453 + ' merge was found.\nImport it?'))
456 self
.model
.set_squash_msg()
458 # Set the new commit message
459 self
.model
.set_squash_msg()
462 push_branches(self
.model
, self
.view
)
464 def show_diffstat(self
):
465 """Show the diffstat from the latest commit."""
466 self
.__diffgui
_enabled
= False
467 self
.view
.set_info(self
.tr('Diffstat'))
468 self
.view
.set_display(self
.model
.diffstat())
470 def show_index(self
):
471 self
.__diffgui
_enabled
= False
472 self
.view
.set_info(self
.tr('Index'))
473 self
.view
.set_display(self
.model
.diffindex())
475 #####################################################################
477 def process_diff_selection(self
, items
, widget
,
478 cached
=True, selected
=False, reverse
=True, noop
=False):
480 filename
= qtutils
.get_selected_item(widget
, items
)
481 if not filename
: return
482 parser
= utils
.DiffParser(self
.model
, filename
=filename
,
484 offset
, selection
= self
.view
.diff_selection()
485 parser
.process_diff_selection(selected
, offset
, selection
)
488 def stage_hunk(self
):
489 self
.process_diff_selection(
490 self
.model
.get_unstaged(),
494 def stage_hunk_selection(self
):
495 self
.process_diff_selection(
496 self
.model
.get_unstaged(),
501 def unstage_hunk(self
, cached
=True):
502 self
.process_diff_selection(
503 self
.model
.get_staged(),
507 def unstage_hunk_selection(self
):
508 self
.process_diff_selection(
509 self
.model
.get_staged(),
514 # #######################################################################
517 # *rest handles being called from different signals
518 def stage_selected(self
,*rest
):
519 """Use "git add" to add items to the git index.
520 This is a thin wrapper around map_to_listwidget."""
521 command
= self
.model
.add_or_remove
522 widget
= self
.view
.unstaged
523 items
= self
.model
.get_unstaged()
524 self
.map_to_listwidget(command
, widget
, items
)
526 # *rest handles being called from different signals
527 def unstage_selected(self
, *rest
):
528 """Use "git reset" to remove items from the git index.
529 This is a thin wrapper around map_to_listwidget."""
530 command
= self
.model
.reset
531 widget
= self
.view
.staged
532 items
= self
.model
.get_staged()
533 self
.map_to_listwidget(command
, widget
, items
)
535 def undo_changes(self
):
536 """Reverts local changes back to whatever's in HEAD."""
537 widget
= self
.view
.unstaged
538 items
= self
.model
.get_unstaged()
539 potential_items
= qtutils
.get_selection_list(widget
, items
)
541 untracked
= self
.model
.get_untracked()
542 for item
in potential_items
:
543 if item
not in untracked
:
544 items_to_undo
.append(item
)
546 answer
= qtutils
.question(self
.view
,
547 self
.tr('Destroy Local Changes?'),
548 self
.tr('This operation will drop all '
549 + ' uncommitted changes. Continue?'),
552 if not answer
: return
554 output
= self
.model
.checkout('HEAD', '--',
556 self
.log('git checkout HEAD -- '
557 + ' '.join(items_to_undo
)
560 msg
= 'No files selected for checkout from HEAD.'
561 self
.log(self
.tr(msg
))
564 """Visualizes the entire git history using gitk."""
565 browser
= self
.model
.get_global_ugit_historybrowser()
566 utils
.fork(browser
,'--all')
568 def viz_current(self
):
569 """Visualizes the current branch's history using gitk."""
570 browser
= self
.model
.get_global_ugit_historybrowser()
571 utils
.fork(browser
, self
.model
.get_branch())
573 def move_event(self
, event
):
574 defaults
.X
= event
.pos().x()
575 defaults
.Y
= event
.pos().y()
577 def resize_event(self
, event
):
578 defaults
.WIDTH
= event
.size().width()
579 defaults
.HEIGHT
= event
.size().height()
581 def load_gui_settings(self
):
582 if not self
.model
.remember_gui_settings():
586 sb0
,sb1
) = self
.model
.get_window_geom()
587 self
.view
.resize(w
,h
)
590 def log(self
, output
, rescan
=True, quiet
=False):
591 """Logs output and optionally rescans for changes."""
592 qtutils
.log(output
, quiet
=quiet
, doraise
=False)
593 if rescan
: self
.rescan()
595 def map_to_listwidget(self
, command
, widget
, items
):
596 """This is a helper method that retrieves the current
597 selection list, applies a command to that list,
598 displays a dialog showing the output of that command,
599 and calls rescan to pickup changes."""
600 apply_items
= qtutils
.get_selection_list(widget
, items
)
601 output
= command(*apply_items
)
602 self
.log(output
, quiet
=True)
604 def unstaged_context_menu_event(self
, event
):
605 self
.unstaged_context_menu_setup()
606 unstaged
= self
.view
.unstaged
607 self
.__unstaged
_menu
.exec_(unstaged
.mapToGlobal(event
.pos()))
609 def unstaged_context_menu_setup(self
):
610 if self
.__unstaged
_menu
: return
612 menu
= self
.__unstaged
_menu
= QMenu(self
.view
)
613 self
.__stage
_selected
_action
= menu
.addAction(
614 self
.tr('Stage Selected'), self
.stage_selected
)
615 self
.__undo
_changes
_action
= menu
.addAction(
616 self
.tr('Undo Local Changes'), self
.undo_changes
)
617 self
.connect(self
.__unstaged
_menu
, 'aboutToShow()',
618 self
.unstaged_context_menu_about_to_show
)
620 def unstaged_context_menu_about_to_show(self
):
621 unstaged_item
= qtutils
.get_selected_item(
623 self
.model
.get_unstaged())
625 is_tracked
= unstaged_item
not in self
.model
.get_untracked()
627 enable_staging
= bool(self
.__diffgui
_enabled
629 enable_undo
= enable_staging
and is_tracked
631 self
.__stage
_selected
_action
.setEnabled(enable_staging
)
632 self
.__undo
_changes
_action
.setEnabled(enable_undo
)
634 def diff_context_menu_about_to_show(self
):
635 unstaged_item
= qtutils
.get_selected_item(
637 self
.model
.get_unstaged())
639 is_tracked
= unstaged_item
not in self
.model
.get_untracked()
642 self
.__diffgui
_enabled
644 and not self
.__staged
_diff
_in
_view
648 self
.__diffgui
_enabled
649 and self
.__staged
_diff
_in
_view
650 and qtutils
.get_selected_item(
652 self
.model
.get_staged()))
654 self
.__stage
_hunk
_action
.setEnabled(bool(enable_staged
))
655 self
.__stage
_hunk
_selection
_action
.setEnabled(bool(enable_staged
))
657 self
.__unstage
_hunk
_action
.setEnabled(bool(enable_unstaged
))
658 self
.__unstage
_hunk
_selection
_action
.setEnabled(bool(enable_unstaged
))
660 def diff_context_menu_event(self
, event
):
661 self
.diff_context_menu_setup()
662 textedit
= self
.view
.display_text
663 self
.__diff
_menu
.exec_(textedit
.mapToGlobal(event
.pos()))
665 def diff_context_menu_setup(self
):
666 if self
.__diff
_menu
: return
668 menu
= self
.__diff
_menu
= QMenu(self
.view
)
669 self
.__stage
_hunk
_action
= menu
.addAction(
670 self
.tr('Stage Hunk For Commit'), self
.stage_hunk
)
672 self
.__stage
_hunk
_selection
_action
= menu
.addAction(
673 self
.tr('Stage Selected Lines'),
674 self
.stage_hunk_selection
)
676 self
.__unstage
_hunk
_action
= menu
.addAction(
677 self
.tr('Unstage Hunk From Commit'),
680 self
.__unstage
_hunk
_selection
_action
= menu
.addAction(
681 self
.tr('Unstage Selected Lines'),
682 self
.unstage_hunk_selection
)
684 self
.__copy
_action
= menu
.addAction(
685 self
.tr('Copy'), self
.view
.copy_display
)
687 self
.connect(self
.__diff
_menu
, 'aboutToShow()',
688 self
.diff_context_menu_about_to_show
)
690 def select_commits_gui(self
, title
, revs
, summaries
):
691 return select_commits(self
.model
, self
.view
, title
, revs
, summaries
)
693 def update_diff_font(self
):
694 font
= self
.model
.get_global_ugit_fontdiff()
697 qfont
.fromString(font
)
698 self
.view
.display_text
.setFont(qfont
)
699 self
.view
.commitmsg
.setFont(qfont
)
701 def update_ui_font(self
):
702 font
= self
.model
.get_global_ugit_fontui()
705 qfont
.fromString(font
)
706 QtGui
.qApp
.setFont(qfont
)
708 def init_log_window(self
):
709 branch
, version
= self
.model
.get_branch(), defaults
.VERSION
710 qtutils
.log(self
.model
.get_git_version()
711 + '\nugit version '+ version
712 + '\nCurrent Branch: '+ branch
)
714 def start_inotify_thread(self
):
715 # Do we have inotify? If not, return.
716 # Recommend installing inotify if we're on Linux.
717 self
.inotify_thread
= None
719 from inotify
import GitNotifier
720 qtutils
.log(self
.tr('inotify support: enabled'))
723 if platform
.system() == 'Linux':
726 'inotify: disabled\n'
727 'Note: To enable inotify, '
728 'install python-pyinotify.\n')
730 plat
= platform
.platform().lower()
731 if 'debian' in plat
or 'ubuntu' in plat
:
733 'On Debian or Ubuntu systems, '
734 'try: sudo apt-get install '
740 # Start the notification thread
741 self
.inotify_thread
= GitNotifier(self
, os
.getcwd())
742 self
.inotify_thread
.start()