5 from PyQt4
import QtGui
6 from PyQt4
.QtGui
import QDialog
7 from PyQt4
.QtGui
import QMessageBox
8 from PyQt4
.QtGui
import QMenu
13 from qobserver
import QObserver
14 from repobrowsercontroller
import browse_git_branch
15 from createbranchcontroller
import create_new_branch
16 from pushcontroller
import push_branches
17 from utilcontroller
import choose_branch
18 from utilcontroller
import select_commits
19 from utilcontroller
import update_options
21 class Controller(QObserver
):
22 '''The controller is a mediator between the model and view.
23 It allows for a clean decoupling between view and model classes.'''
25 def __init__(self
, model
, view
):
26 QObserver
.__init
__(self
, model
, view
)
28 self
.__last
_inotify
_event
= time
.time()
30 # The diff-display context menu
32 self
.__staged
_diff
_in
_view
= True
34 # Diff display context menu
35 view
.displayText
.controller
= self
36 view
.displayText
.contextMenuEvent
= self
.diff_context_menu_event
38 # Default to creating a new commit(i.e. not an amend commit)
39 view
.newCommitRadio
.setChecked(True)
41 # Binds a specific model attribute to a view widget,
43 self
.model_to_view('commitmsg', 'commitText')
44 self
.model_to_view('staged', 'stagedList')
45 self
.model_to_view('all_unstaged', 'unstagedList')
47 # When a model attribute changes, this runs a specific action
48 self
.add_actions('staged', self
.action_staged
)
49 self
.add_actions('all_unstaged', self
.action_all_unstaged
)
51 # Routes signals for multiple widgets to our callbacks
53 self
.add_signals('textChanged()', view
.commitText
)
54 self
.add_signals('stateChanged(int)', view
.untrackedCheckBox
)
56 self
.add_signals('released()',
57 view
.stageButton
, view
.commitButton
,
58 view
.pushButton
, view
.signOffButton
,)
60 self
.add_signals('triggered()',
61 view
.rescan
, view
.options
,
62 view
.createBranch
, view
.checkoutBranch
,
63 view
.rebaseBranch
, view
.deleteBranch
,
64 view
.setCommitMessage
, view
.commit
,
65 view
.stageChanged
, view
.stageUntracked
,
66 view
.stageSelected
, view
.unstageAll
,
69 view
.browseBranch
, view
.browseOtherBranch
,
70 view
.visualizeAll
, view
.visualizeCurrent
,
71 view
.exportPatches
, view
.cherryPick
,
73 view
.cut
, view
.copy
, view
.paste
, view
.delete
,
74 view
.selectAll
, view
.undo
, view
.redo
,)
76 self
.add_signals('itemClicked(QListWidgetItem *)',
77 view
.stagedList
, view
.unstagedList
,)
79 self
.add_signals('itemSelectionChanged()',
80 view
.stagedList
, view
.unstagedList
,)
82 self
.add_signals('splitterMoved(int,int)',
83 view
.splitter_top
, view
.splitter_bottom
)
86 self
.connect(QtGui
.qApp
, 'lastWindowClosed()',
87 self
.last_window_closed
)
89 # These callbacks are called in response to the signals
90 # defined above. One property of the QObserver callback
91 # mechanism is that the model is passed in as the first
92 # argument to the callback. This allows for a single
93 # controller to manage multiple models, though this
94 # isn't used at the moment.
96 # Actions that delegate directly to the model
97 signOffButton
= model
.add_signoff
,
98 setCommitMessage
= model
.get_prev_commitmsg
,
99 stageChanged
= self
.model
.stage_changed
,
100 stageUntracked
= self
.model
.stage_untracked
,
101 unstageAll
= self
.model
.unstage_all
,
103 # Actions that delegate direclty to the view
104 cut
= view
.action_cut
,
105 copy
= view
.action_copy
,
106 paste
= view
.action_paste
,
107 delete
= view
.action_delete
,
108 selectAll
= view
.action_select_all
,
109 undo
= view
.action_undo
,
110 redo
= view
.action_redo
,
113 stageButton
= self
.stage_selected
,
114 commitButton
= self
.commit
,
115 pushButton
= self
.push
,
118 stagedList
= self
.diff_staged
,
119 unstagedList
= self
.diff_unstaged
,
122 untrackedCheckBox
= self
.rescan
,
125 options
= self
.options
,
126 rescan
= self
.rescan
,
127 createBranch
= self
.branch_create
,
128 deleteBranch
= self
.branch_delete
,
129 checkoutBranch
= self
.checkout_branch
,
130 rebaseBranch
= self
.rebase
,
131 commit
= self
.commit
,
132 stageSelected
= self
.stage_selected
,
133 unstageSelected
= self
.unstage_selected
,
134 showDiffstat
= self
.show_diffstat
,
135 browseBranch
= self
.browse_current
,
136 browseOtherBranch
= self
.browse_other
,
137 visualizeCurrent
= self
.viz_current
,
138 visualizeAll
= self
.viz_all
,
139 exportPatches
= self
.export_patches
,
140 cherryPick
= self
.cherry_pick
,
141 loadCommitMsg
= self
.load_commitmsg
,
144 splitter_top
= self
.splitter_top_event
,
145 splitter_bottom
= self
.splitter_bottom_event
,
148 # Handle double-clicks in the staged/unstaged lists.
149 # These are vanilla signal/slots since the qobserver
150 # signal routing is already handling these lists' signals.
151 self
.connect(view
.unstagedList
,
152 'itemDoubleClicked(QListWidgetItem*)',
155 self
.connect(view
.stagedList
,
156 'itemDoubleClicked(QListWidgetItem*)',
157 self
.unstage_selected
)
159 # Delegate window move events here
160 self
.view
.moveEvent
= self
.move_event
161 self
.view
.resizeEvent
= self
.resize_event
164 self
.load_window_settings()
167 # Setup the inotify watchdog
168 self
.start_inotify_thread()
170 #####################################################################
171 # 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_all_unstaged(self
, widget
):
188 qtutils
.update_listwidget(widget
,
189 self
.model
.get_unstaged(), staged
=False)
191 if self
.view
.untrackedCheckBox
.isChecked():
192 qtutils
.update_listwidget(widget
,
193 self
.model
.get_untracked(),
198 #####################################################################
202 update_options(self
.model
, self
.view
)
204 def branch_create(self
):
205 if create_new_branch(self
.model
, self
.view
):
208 def branch_delete(self
):
209 branch
= choose_branch('Delete Branch',
210 self
.view
, self
.model
.get_local_branches())
211 if not branch
: return
212 self
.show_output(self
.model
.delete_branch(branch
))
214 def browse_current(self
):
215 branch
= self
.model
.get_branch()
216 browse_git_branch(self
.model
, self
.view
, branch
)
218 def browse_other(self
):
219 # Prompt for a branch to browse
220 branch
= choose_branch('Browse Branch Files',
221 self
.view
, self
.model
.get_all_branches())
222 if not branch
: return
223 # Launch the repobrowser
224 browse_git_branch(self
.model
, self
.view
, branch
)
226 def checkout_branch(self
):
227 branch
= choose_branch('Checkout Branch',
228 self
.view
, self
.model
.get_local_branches())
229 if not branch
: return
230 self
.show_output(self
.model
.checkout(branch
))
232 def cherry_pick(self
):
233 commits
= self
.select_commits_gui(*self
.model
.log(all
=True))
234 if not commits
: return
235 self
.show_output(self
.model
.cherry_pick(commits
))
238 msg
= self
.model
.get_commitmsg()
240 error_msg
= self
.tr(""
241 + "Please supply a commit message.\n"
243 + "A good commit message has the following format:\n"
245 + "- First line: Describe in one sentence what you did.\n"
246 + "- Second line: Blank\n"
247 + "- Remaining lines: Describe why this change is good.\n")
249 self
.show_output(error_msg
)
252 files
= self
.model
.get_staged()
255 + "No changes to commit.\n"
257 + "You must stage at least 1 file before you can commit.\n")
258 self
.show_output(errmsg
)
262 output
= self
.model
.commit(msg
, amend
=self
.view
.amendRadio
.isChecked())
265 self
.view
.newCommitRadio
.setChecked(True)
266 self
.view
.amendRadio
.setChecked(False)
267 self
.model
.set_commitmsg('')
268 self
.show_output(output
)
270 def view_diff(self
, staged
=True):
271 self
.__staged
_diff
_in
_view
= staged
272 if self
.__staged
_diff
_in
_view
:
273 widget
= self
.view
.stagedList
275 widget
= self
.view
.unstagedList
276 row
, selected
= qtutils
.get_selected_row(widget
)
278 self
.view
.reset_display()
281 status
) = self
.model
.get_diff_and_status(row
, staged
=staged
)
283 self
.view
.set_display(diff
)
284 self
.view
.set_info(self
.tr(status
))
286 # use *rest to handle being called from different signals
287 def diff_staged(self
, *rest
):
288 self
.view_diff(staged
=True)
290 # use *rest to handle being called from different signals
291 def diff_unstaged(self
,*rest
):
292 self
.view_diff(staged
=False)
294 def export_patches(self
):
295 (revs
, summaries
) = self
.model
.log()
296 commits
= self
.select_commits_gui(revs
, summaries
)
297 if not commits
: return
298 self
.show_output(self
.model
.format_patch(commits
))
300 def last_window_closed(self
):
301 '''Save config settings and cleanup any inotify threads.'''
303 self
.model
.save_window_geom()
305 if not self
.inotify_thread
: return
306 if not self
.inotify_thread
.isRunning(): return
308 self
.inotify_thread
.abort
= True
309 self
.inotify_thread
.terminate()
310 self
.inotify_thread
.wait()
312 def load_commitmsg(self
):
313 file = qtutils
.open_dialog(self
.view
,
314 'Load Commit Message...', defaults
.DIRECTORY
)
317 defaults
.DIRECTORY
= os
.path
.dirname(file)
318 slushy
= utils
.slurp(file)
319 if slushy
: self
.model
.set_commitmsg(slushy
)
322 branch
= choose_branch('Rebase Branch',
323 self
.view
, self
.model
.get_local_branches())
324 if not branch
: return
325 self
.show_output(self
.model
.rebase(branch
))
327 # use *rest to handle being called from the checkbox signal
328 def rescan(self
, *rest
):
329 '''Populates view widgets with results from "git status."'''
331 self
.view
.statusBar().showMessage(
332 self
.tr('Scanning for modified files ...'))
334 self
.model
.update_status()
336 branch
= self
.model
.get_branch()
337 status_text
= self
.tr('Current Branch:') + ' ' + branch
338 self
.view
.statusBar().showMessage(status_text
)
340 title
= '%s [%s]' % (self
.model
.get_project(), branch
)
341 self
.view
.setWindowTitle(title
)
343 if not self
.model
.has_squash_msg(): return
345 if self
.model
.get_commitmsg():
346 answer
= qtutils
.question(self
.view
,
347 self
.tr('Import Commit Message?'),
348 self
.tr('A commit message from an in-progress'
349 + ' merge was found.\nImport it?'))
351 if not answer
: return
353 # Set the new commit message
354 self
.model
.set_squash_msg()
357 push_branches(self
.model
, self
.view
)
359 def show_diffstat(self
):
360 '''Show the diffstat from the latest commit.'''
361 self
.show_output(self
.model
.diff_stat(), rescan
=False)
363 #####################################################################
366 def process_diff_selection(self
, items
, widget
,
367 cached
=True, selected
=False, reverse
=True, noop
=False):
369 filename
= qtutils
.get_selected_item(widget
, items
)
370 if not filename
: return
371 parser
= utils
.DiffParser(self
.model
, filename
=filename
,
374 offset
, selection
= self
.view
.diff_selection()
375 parser
.process_diff_selection(selected
, offset
, selection
)
378 def stage_hunk(self
):
379 self
.process_diff_selection(
380 self
.model
.get_unstaged(),
381 self
.view
.unstagedList
,
384 def stage_hunks(self
):
385 self
.process_diff_selection(
386 self
.model
.get_unstaged(),
387 self
.view
.unstagedList
,
391 def unstage_hunk(self
, cached
=True):
392 self
.process_diff_selection(
393 self
.model
.get_staged(),
394 self
.view
.stagedList
,
397 def unstage_hunks(self
):
398 self
.process_diff_selection(
399 self
.model
.get_staged(),
400 self
.view
.stagedList
,
404 # #######################################################################
407 # use *rest to handle being called from different signals
408 def stage_selected(self
,*rest
):
409 '''Use "git add" to add items to the git index.
410 This is a thin wrapper around apply_to_list.'''
411 command
= self
.model
.add_or_remove
412 widget
= self
.view
.unstagedList
413 items
= self
.model
.get_all_unstaged()
414 self
.apply_to_list(command
,widget
,items
)
416 # use *rest to handle being called from different signals
417 def unstage_selected(self
, *rest
):
418 '''Use "git reset" to remove items from the git index.
419 This is a thin wrapper around apply_to_list.'''
420 command
= self
.model
.reset
421 widget
= self
.view
.stagedList
422 items
= self
.model
.get_staged()
423 self
.apply_to_list(command
, widget
, items
)
426 '''Visualizes the entire git history using gitk.'''
427 utils
.fork('gitk','--all')
429 def viz_current(self
):
430 '''Visualizes the current branch's history using gitk.'''
431 utils
.fork('gitk', self
.model
.get_branch())
433 # These actions monitor window resizes, splitter changes, etc.
434 def move_event(self
, event
):
435 defaults
.X
= event
.pos().x()
436 defaults
.Y
= event
.pos().y()
438 def resize_event(self
, event
):
439 defaults
.WIDTH
= event
.size().width()
440 defaults
.HEIGHT
= event
.size().height()
442 def splitter_top_event(self
,*rest
):
443 sizes
= self
.view
.splitter_top
.sizes()
444 defaults
.SPLITTER_TOP_0
= sizes
[0]
445 defaults
.SPLITTER_TOP_1
= sizes
[1]
447 def splitter_bottom_event(self
,*rest
):
448 sizes
= self
.view
.splitter_bottom
.sizes()
449 defaults
.SPLITTER_BOTTOM_0
= sizes
[0]
450 defaults
.SPLITTER_BOTTOM_1
= sizes
[1]
452 def load_window_settings(self
):
455 sb0
,sb1
) = self
.model
.get_window_geom()
456 self
.view
.resize(w
,h
)
458 self
.view
.splitter_top
.setSizes([st0
,st1
])
459 self
.view
.splitter_bottom
.setSizes([sb0
,sb1
])
461 def show_output(self
, output
, rescan
=True):
462 '''Shows output and optionally rescans for changes.'''
463 qtutils
.show_output(self
.view
, output
)
466 #####################################################################
469 def apply_to_list(self
, command
, widget
, items
):
470 '''This is a helper method that retrieves the current
471 selection list, applies a command to that list,
472 displays a dialog showing the output of that command,
473 and calls rescan to pickup changes.'''
474 apply_items
= qtutils
.get_selection_list(widget
, items
)
475 output
= command(apply_items
)
479 def diff_context_menu_about_to_show(self
):
480 unstaged_item
= qtutils
.get_selected_item(
481 self
.view
.unstagedList
,
482 self
.model
.get_all_unstaged())
484 is_tracked
= unstaged_item
not in self
.model
.get_untracked()
488 and not self
.__staged
_diff
_in
_view
492 self
.__staged
_diff
_in
_view
493 and qtutils
.get_selected_item(
494 self
.view
.stagedList
,
495 self
.model
.get_staged()))
497 self
.__stage
_hunk
_action
.setEnabled(bool(enable_staged
))
498 self
.__stage
_hunks
_action
.setEnabled(bool(enable_staged
))
500 self
.__unstage
_hunk
_action
.setEnabled(bool(enable_unstaged
))
501 self
.__unstage
_hunks
_action
.setEnabled(bool(enable_unstaged
))
503 def diff_context_menu_event(self
, event
):
504 self
.diff_context_menu_setup()
505 textedit
= self
.view
.displayText
506 self
.__menu
.exec_(textedit
.mapToGlobal(event
.pos()))
508 def diff_context_menu_setup(self
):
509 if self
.__menu
: return
511 menu
= self
.__menu
= QMenu(self
.view
)
512 self
.__stage
_hunk
_action
= menu
.addAction(
513 self
.tr('Stage Hunk For Commit'), self
.stage_hunk
)
515 self
.__stage
_hunks
_action
= menu
.addAction(
516 self
.tr('Stage Selected Lines'), self
.stage_hunks
)
518 self
.__unstage
_hunk
_action
= menu
.addAction(
519 self
.tr('Unstage Hunk From Commit'), self
.unstage_hunk
)
521 self
.__unstage
_hunks
_action
= menu
.addAction(
522 self
.tr('Unstage Selected Lines'), self
.unstage_hunks
)
524 self
.__copy
_action
= menu
.addAction(
525 self
.tr('Copy'), self
.view
.copy_display
)
527 self
.connect(self
.__menu
, 'aboutToShow()',
528 self
.diff_context_menu_about_to_show
)
530 def select_commits_gui(self
, revs
, summaries
):
531 return select_commits(self
.model
, self
.view
, revs
, summaries
)
533 def start_inotify_thread(self
):
534 # Do we have inotify? If not, return.
535 # Recommend installing inotify if we're on Linux.
536 self
.inotify_thread
= None
538 from inotify
import GitNotifier
541 if platform
.system() == 'Linux':
542 msg
=(self
.tr('Unable import pyinotify.\n'
543 + 'inotify support has been'
547 plat
= platform
.platform().lower()
548 if 'debian' in plat
or 'ubuntu' in plat
:
549 msg
+= (self
.tr('Hint:')
550 + 'sudo apt-get install'
551 + ' python-pyinotify')
553 qtutils
.information(self
.view
,
554 self
.tr('inotify disabled'), msg
)
556 # Start the notification thread
557 self
.inotify_thread
= GitNotifier(self
, os
.getcwd())
558 self
.inotify_thread
.start()