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 update_options
21 from utilcontroller
import log_window
23 class Controller(QObserver
):
24 '''The controller is a mediator between the model and view.
25 It allows for a clean decoupling between view and model classes.'''
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 self
.__last
_inotify
_event
= time
.time()
35 # The diff-display context menu
37 self
.__staged
_diff
_in
_view
= True
39 # Diff display context menu
40 view
.display_text
.contextMenuEvent
= self
.diff_context_menu_event
42 # Binds a specific model attribute to a view widget,
44 self
.model_to_view('commitmsg', 'commit_text')
45 self
.model_to_view('staged', 'staged_list')
46 self
.model_to_view('all_unstaged', 'unstaged_list')
48 # When a model attribute changes, this runs a specific action
49 self
.add_actions('staged', self
.action_staged
)
50 self
.add_actions('all_unstaged', self
.action_all_unstaged
)
51 self
.add_actions('global.ugit.fontdiff', self
.update_diff_font
)
52 self
.add_actions('global.ugit.fontui', self
.update_ui_font
)
54 # Routes signals for multiple widgets to our callbacks
56 self
.add_signals('textChanged()', view
.commit_text
)
57 self
.add_signals('stateChanged(int)', view
.untracked_checkbox
)
59 self
.add_signals('released()',
65 self
.add_signals('triggered()',
68 view
.menu_create_branch
,
69 view
.menu_checkout_branch
,
70 view
.menu_rebase_branch
,
71 view
.menu_delete_branch
,
72 view
.menu_get_prev_commitmsg
,
74 view
.menu_stage_changed
,
75 view
.menu_stage_untracked
,
76 view
.menu_stage_selected
,
77 view
.menu_unstage_all
,
78 view
.menu_unstage_selected
,
79 view
.menu_show_diffstat
,
80 view
.menu_browse_branch
,
81 view
.menu_browse_other_branch
,
82 view
.menu_visualize_all
,
83 view
.menu_visualize_current
,
84 view
.menu_export_patches
,
85 view
.menu_cherry_pick
,
86 view
.menu_load_commitmsg
,
95 self
.add_signals('itemClicked(QListWidgetItem *)',
99 self
.add_signals('itemSelectionChanged()',
103 self
.add_signals('splitterMoved(int,int)',
104 view
.splitter_top
, view
.splitter_bottom
)
106 # Vanilla signal/slots
107 self
.connect(self
.view
.toolbar_show_log
,
108 'triggered()', self
.show_log
)
111 self
.connect(QtGui
.qApp
, 'lastWindowClosed()',
112 self
.last_window_closed
)
114 # These callbacks are called in response to the signals
115 # defined above. One property of the QObserver callback
116 # mechanism is that the model is passed in as the first
117 # argument to the callback. This allows for a single
118 # controller to manage multiple models, though this
119 # isn't used at the moment.
121 # Actions that delegate directly to the model
122 signoff_button
= model
.add_signoff
,
123 menu_get_prev_commitmsg
= model
.get_prev_commitmsg
,
124 menu_stage_changed
= self
.model
.stage_changed
,
125 menu_stage_untracked
= self
.model
.stage_untracked
,
126 menu_unstage_all
= self
.model
.unstage_all
,
128 # Actions that delegate direclty to the view
129 menu_cut
= view
.action_cut
,
130 menu_copy
= view
.action_copy
,
131 menu_paste
= view
.action_paste
,
132 menu_delete
= view
.action_delete
,
133 menu_select_all
= view
.action_select_all
,
134 menu_undo
= view
.action_undo
,
135 menu_redo
= view
.action_redo
,
138 stage_button
= self
.stage_selected
,
139 commit_button
= self
.commit
,
140 push_button
= self
.push
,
143 staged_list
= self
.diff_staged
,
144 unstaged_list
= self
.diff_unstaged
,
147 untracked_checkbox
= self
.rescan
,
150 menu_options
= self
.options
,
151 menu_rescan
= self
.rescan
,
152 menu_create_branch
= self
.branch_create
,
153 menu_delete_branch
= self
.branch_delete
,
154 menu_checkout_branch
= self
.checkout_branch
,
155 menu_rebase_branch
= self
.rebase
,
156 menu_commit
= self
.commit
,
157 menu_stage_selected
= self
.stage_selected
,
158 menu_unstage_selected
= self
.unstage_selected
,
159 menu_show_diffstat
= self
.show_diffstat
,
160 menu_browse_branch
= self
.browse_current
,
161 menu_browse_other_branch
= self
.browse_other
,
162 menu_visualize_current
= self
.viz_current
,
163 menu_visualize_all
= self
.viz_all
,
164 menu_export_patches
= self
.export_patches
,
165 menu_cherry_pick
= self
.cherry_pick
,
166 menu_load_commitmsg
= self
.load_commitmsg
,
169 splitter_top
= self
.splitter_top_event
,
170 splitter_bottom
= self
.splitter_bottom_event
,
173 # Handle double-clicks in the staged/unstaged lists.
174 # These are vanilla signal/slots since the qobserver
175 # signal routing is already handling these lists' signals.
176 self
.connect(view
.unstaged_list
,
177 'itemDoubleClicked(QListWidgetItem*)',
180 self
.connect(view
.staged_list
,
181 'itemDoubleClicked(QListWidgetItem*)',
182 self
.unstage_selected
)
184 # Delegate window move events here
185 view
.moveEvent
= self
.move_event
186 view
.resizeEvent
= self
.resize_event
189 self
.load_window_settings()
191 # Setup the inotify watchdog
192 self
.start_inotify_thread()
198 #####################################################################
199 # event() is called in response to messages from the inotify thread
201 def event(self
, msg
):
202 if msg
.type() == defaults
.INOTIFY_EVENT
:
208 #####################################################################
209 # Actions triggered during model updates
211 def action_staged(self
, widget
):
212 qtutils
.update_listwidget(widget
,
213 self
.model
.get_staged(), staged
=True)
215 def action_all_unstaged(self
, widget
):
216 qtutils
.update_listwidget(widget
,
217 self
.model
.get_unstaged(), staged
=False)
219 if self
.view
.untracked_checkbox
.isChecked():
220 qtutils
.update_listwidget(widget
,
221 self
.model
.get_untracked(),
226 #####################################################################
229 def show_log(self
, *rest
):
230 qtutils
.toggle_log_window()
233 update_options(self
.model
, self
.view
)
235 def branch_create(self
):
236 if create_new_branch(self
.model
, self
.view
):
239 def branch_delete(self
):
240 branch
= choose_branch('Delete Branch',
241 self
.view
, self
.model
.get_local_branches())
242 if not branch
: return
243 self
.log_output(self
.model
.delete_branch(branch
))
245 def browse_current(self
):
246 branch
= self
.model
.get_branch()
247 browse_git_branch(self
.model
, self
.view
, branch
)
249 def browse_other(self
):
250 # Prompt for a branch to browse
251 branch
= choose_branch('Browse Branch Files',
252 self
.view
, self
.model
.get_all_branches())
253 if not branch
: return
254 # Launch the repobrowser
255 browse_git_branch(self
.model
, self
.view
, branch
)
257 def checkout_branch(self
):
258 branch
= choose_branch('Checkout Branch',
259 self
.view
, self
.model
.get_local_branches())
260 if not branch
: return
261 self
.log_output(self
.model
.checkout(branch
))
263 def cherry_pick(self
):
264 commits
= self
.select_commits_gui(*self
.model
.log(all
=True))
265 if not commits
: return
266 self
.log_output(self
.model
.cherry_pick(commits
))
269 msg
= self
.model
.get_commitmsg()
271 error_msg
= self
.tr(""
272 + "Please supply a commit message.\n"
274 + "A good commit message has the following format:\n"
276 + "- First line: Describe in one sentence what you did.\n"
277 + "- Second line: Blank\n"
278 + "- Remaining lines: Describe why this change is good.\n")
279 qtutils
.show_output(error_msg
)
282 files
= self
.model
.get_staged()
284 error_msg
= self
.tr(""
285 + "No changes to commit.\n"
287 + "You must stage at least 1 file before you can commit.\n")
288 qtutils
.show_output(error_msg
)
292 output
= self
.model
.commit(
293 msg
, amend
=self
.view
.amend_radio
.isChecked())
296 self
.view
.new_commit_radio
.setChecked(True)
297 self
.view
.amend_radio
.setChecked(False)
298 self
.model
.set_commitmsg('')
299 self
.log_output(output
, alert
=True)
301 def view_diff(self
, staged
=True):
302 self
.__staged
_diff
_in
_view
= staged
303 if self
.__staged
_diff
_in
_view
:
304 widget
= self
.view
.staged_list
306 widget
= self
.view
.unstaged_list
307 row
, selected
= qtutils
.get_selected_row(widget
)
309 self
.view
.reset_display()
312 status
) = self
.model
.get_diff_and_status(row
, staged
=staged
)
314 self
.view
.set_display(diff
)
315 self
.view
.set_info(self
.tr(status
))
317 # use *rest to handle being called from different signals
318 def diff_staged(self
, *rest
):
319 self
.view_diff(staged
=True)
321 # use *rest to handle being called from different signals
322 def diff_unstaged(self
,*rest
):
323 self
.view_diff(staged
=False)
325 def export_patches(self
):
326 (revs
, summaries
) = self
.model
.log()
327 commits
= self
.select_commits_gui(revs
, summaries
)
328 if not commits
: return
329 qtutils
.show_output(self
.model
.format_patch(commits
))
331 def last_window_closed(self
):
332 '''Save config settings and cleanup any inotify threads.'''
334 self
.model
.save_window_geom()
336 if not self
.inotify_thread
: return
337 if not self
.inotify_thread
.isRunning(): return
339 self
.inotify_thread
.abort
= True
340 self
.inotify_thread
.terminate()
341 self
.inotify_thread
.wait()
343 def load_commitmsg(self
):
344 file = qtutils
.open_dialog(self
.view
,
345 'Load Commit Message...', defaults
.DIRECTORY
)
348 defaults
.DIRECTORY
= os
.path
.dirname(file)
349 slushy
= utils
.slurp(file)
350 if slushy
: self
.model
.set_commitmsg(slushy
)
353 branch
= choose_branch('Rebase Branch',
354 self
.view
, self
.model
.get_local_branches())
355 if not branch
: return
356 self
.log_output(self
.model
.rebase(branch
))
358 # use *rest to handle being called from the checkbox signal
359 def rescan(self
, *rest
):
360 '''Populates view widgets with results from "git status."'''
361 self
.model
.update_status()
362 self
.view
.setWindowTitle('%s [%s]' % (
363 self
.model
.get_project(),
364 self
.model
.get_branch()))
366 if not self
.model
.has_squash_msg(): return
368 if self
.model
.get_commitmsg():
369 answer
= qtutils
.question(self
.view
,
370 self
.tr('Import Commit Message?'),
371 self
.tr('A commit message from an in-progress'
372 + ' merge was found.\nImport it?'))
374 if not answer
: return
376 # Set the new commit message
377 self
.model
.set_squash_msg()
380 push_branches(self
.model
, self
.view
)
382 def show_diffstat(self
):
383 '''Show the diffstat from the latest commit.'''
384 qtutils
.show_output(self
.model
.diff_stat())
386 #####################################################################
388 def process_diff_selection(self
, items
, widget
,
389 cached
=True, selected
=False, reverse
=True, noop
=False):
391 filename
= qtutils
.get_selected_item(widget
, items
)
392 if not filename
: return
393 parser
= utils
.DiffParser(self
.model
, filename
=filename
,
395 offset
, selection
= self
.view
.diff_selection()
396 parser
.process_diff_selection(selected
, offset
, selection
)
398 def stage_hunk(self
):
399 self
.process_diff_selection(
400 self
.model
.get_unstaged(),
401 self
.view
.unstaged_list
,
403 def stage_hunk_selection(self
):
404 self
.process_diff_selection(
405 self
.model
.get_unstaged(),
406 self
.view
.unstaged_list
,
409 def unstage_hunk(self
, cached
=True):
410 self
.process_diff_selection(
411 self
.model
.get_staged(),
412 self
.view
.staged_list
,
414 def unstage_hunk_selection(self
):
415 self
.process_diff_selection(
416 self
.model
.get_staged(),
417 self
.view
.staged_list
,
421 # #######################################################################
424 # use *rest to handle being called from different signals
425 def stage_selected(self
,*rest
):
426 '''Use "git add" to add items to the git index.
427 This is a thin wrapper around apply_to_list.'''
428 command
= self
.model
.add_or_remove
429 widget
= self
.view
.unstaged_list
430 items
= self
.model
.get_all_unstaged()
431 self
.apply_to_list(command
,widget
,items
)
433 # use *rest to handle being called from different signals
434 def unstage_selected(self
, *rest
):
435 '''Use "git reset" to remove items from the git index.
436 This is a thin wrapper around apply_to_list.'''
437 command
= self
.model
.reset
438 widget
= self
.view
.staged_list
439 items
= self
.model
.get_staged()
440 self
.apply_to_list(command
, widget
, items
)
443 '''Visualizes the entire git history using gitk.'''
444 utils
.fork('gitk','--all')
446 def viz_current(self
):
447 '''Visualizes the current branch's history using gitk.'''
448 utils
.fork('gitk', self
.model
.get_branch())
450 # These actions monitor window resizes, splitter changes, etc.
451 def move_event(self
, event
):
452 defaults
.X
= event
.pos().x()
453 defaults
.Y
= event
.pos().y()
455 def resize_event(self
, event
):
456 defaults
.WIDTH
= event
.size().width()
457 defaults
.HEIGHT
= event
.size().height()
459 def splitter_top_event(self
,*rest
):
460 sizes
= self
.view
.splitter_top
.sizes()
461 defaults
.SPLITTER_TOP_0
= sizes
[0]
462 defaults
.SPLITTER_TOP_1
= sizes
[1]
464 def splitter_bottom_event(self
,*rest
):
465 sizes
= self
.view
.splitter_bottom
.sizes()
466 defaults
.SPLITTER_BOTTOM_0
= sizes
[0]
467 defaults
.SPLITTER_BOTTOM_1
= sizes
[1]
469 def load_window_settings(self
):
472 sb0
,sb1
) = self
.model
.get_window_geom()
473 self
.view
.resize(w
,h
)
475 self
.view
.splitter_top
.setSizes([st0
,st1
])
476 self
.view
.splitter_bottom
.setSizes([sb0
,sb1
])
478 def log_output(self
, output
, rescan
=True, alert
=False):
479 '''Logs output and optionally rescans for changes.'''
480 if rescan
: self
.rescan()
481 qtutils
.log_output(output
, alert
=alert
)
483 #####################################################################
486 def apply_to_list(self
, command
, widget
, items
):
487 '''This is a helper method that retrieves the current
488 selection list, applies a command to that list,
489 displays a dialog showing the output of that command,
490 and calls rescan to pickup changes.'''
491 apply_items
= qtutils
.get_selection_list(widget
, items
)
492 output
= command(apply_items
)
496 def diff_context_menu_about_to_show(self
):
497 unstaged_item
= qtutils
.get_selected_item(
498 self
.view
.unstaged_list
,
499 self
.model
.get_all_unstaged())
501 is_tracked
= unstaged_item
not in self
.model
.get_untracked()
505 and not self
.__staged
_diff
_in
_view
509 self
.__staged
_diff
_in
_view
510 and qtutils
.get_selected_item(
511 self
.view
.staged_list
,
512 self
.model
.get_staged()))
514 self
.__stage
_hunk
_action
.setEnabled(bool(enable_staged
))
515 self
.__stage
_hunk
_selection
_action
.setEnabled(bool(enable_staged
))
517 self
.__unstage
_hunk
_action
.setEnabled(bool(enable_unstaged
))
518 self
.__unstage
_hunk
_selection
_action
.setEnabled(bool(enable_unstaged
))
520 def diff_context_menu_event(self
, event
):
521 self
.diff_context_menu_setup()
522 textedit
= self
.view
.display_text
523 self
.__menu
.exec_(textedit
.mapToGlobal(event
.pos()))
525 def diff_context_menu_setup(self
):
526 if self
.__menu
: return
528 menu
= self
.__menu
= QMenu(self
.view
)
529 self
.__stage
_hunk
_action
= menu
.addAction(
530 self
.tr('Stage Hunk For Commit'), self
.stage_hunk
)
532 self
.__stage
_hunk
_selection
_action
= menu
.addAction(
533 self
.tr('Stage Selected Lines'),
534 self
.stage_hunk_selection
)
536 self
.__unstage
_hunk
_action
= menu
.addAction(
537 self
.tr('Unstage Hunk From Commit'),
540 self
.__unstage
_hunk
_selection
_action
= menu
.addAction(
541 self
.tr('Unstage Selected Lines'),
542 self
.unstage_hunk_selection
)
544 self
.__copy
_action
= menu
.addAction(
545 self
.tr('Copy'), self
.view
.copy_display
)
547 self
.connect(self
.__menu
, 'aboutToShow()',
548 self
.diff_context_menu_about_to_show
)
550 def select_commits_gui(self
, revs
, summaries
):
551 return select_commits(self
.model
, self
.view
, revs
, summaries
)
553 def update_diff_font(self
):
554 font
= self
.model
.get_param('global.ugit.fontdiff')
557 qfont
.fromString(font
)
558 self
.view
.display_text
.setFont(qfont
)
560 def update_ui_font(self
):
561 font
= self
.model
.get_param('global.ugit.fontui')
564 qfont
.fromString(font
)
565 QtGui
.qApp
.setFont(qfont
)
568 qtutils
.log_output(self
.model
.get_git_version()
570 + 'ugit version ' + defaults
.VERSION
572 + 'Current Branch: ' + self
.model
.get_branch())
574 def start_inotify_thread(self
):
575 # Do we have inotify? If not, return.
576 # Recommend installing inotify if we're on Linux.
577 self
.inotify_thread
= None
579 from inotify
import GitNotifier
582 if platform
.system() == 'Linux':
583 msg
=(self
.tr('Unable import pyinotify.\n'
584 + 'inotify support has been'
588 plat
= platform
.platform().lower()
589 if 'debian' in plat
or 'ubuntu' in plat
:
590 msg
+= (self
.tr('Hint:')
591 + 'sudo apt-get install'
592 + ' python-pyinotify')
594 qtutils
.information(self
.view
,
595 self
.tr('inotify disabled'), msg
)
597 # Start the notification thread
598 self
.inotify_thread
= GitNotifier(self
, os
.getcwd())
599 self
.inotify_thread
.start()