4 from PyQt4
import QtGui
5 from PyQt4
.QtGui
import QDialog
6 from PyQt4
.QtGui
import QMessageBox
7 from PyQt4
.QtGui
import QMenu
12 from qobserver
import QObserver
13 from repobrowsercontroller
import browse_git_branch
14 from createbranchcontroller
import create_new_branch
15 from pushcontroller
import push_branches
16 from utilcontroller
import choose_branch
17 from utilcontroller
import select_commits
18 from utilcontroller
import update_options
20 class Controller(QObserver
):
21 '''The controller is a mediator between the model and view.
22 It allows for a clean decoupling between view and model classes.'''
24 def __init__(self
, model
, view
):
25 QObserver
.__init
__(self
, model
, view
)
27 # The diff-display context menu
29 self
.__staged
_diff
_in
_view
= True
31 # Diff display context menu
32 view
.displayText
.controller
= self
33 view
.displayText
.contextMenuEvent
= self
.__menu
_event
35 # Default to creating a new commit(i.e. not an amend commit)
36 view
.newCommitRadio
.setChecked(True)
38 # Binds a specific model attribute to a view widget,
40 self
.model_to_view('commitmsg', 'commitText')
41 self
.model_to_view('staged', 'stagedList')
42 self
.model_to_view('all_unstaged', 'unstagedList')
44 # When a model attribute changes, this runs a specific action
45 self
.add_actions('staged', self
.action_staged
)
46 self
.add_actions('all_unstaged', self
.action_all_unstaged
)
48 # Routes signals for multiple widgets to our callbacks
50 self
.add_signals('textChanged()', view
.commitText
)
51 self
.add_signals('stateChanged(int)', view
.untrackedCheckBox
)
53 self
.add_signals('released()',
54 view
.stageButton
, view
.commitButton
,
55 view
.pushButton
, view
.signOffButton
,)
57 self
.add_signals('triggered()',
58 view
.rescan
, view
.options
,
59 view
.createBranch
, view
.checkoutBranch
,
60 view
.rebaseBranch
, view
.deleteBranch
,
61 view
.setCommitMessage
, view
.commit
,
62 view
.stageChanged
, view
.stageUntracked
,
63 view
.stageSelected
, view
.unstageAll
,
66 view
.browseBranch
, view
.browseOtherBranch
,
67 view
.visualizeAll
, view
.visualizeCurrent
,
68 view
.exportPatches
, view
.cherryPick
,
70 view
.cut
, view
.copy
, view
.paste
, view
.delete
,
71 view
.selectAll
, view
.undo
, view
.redo
,)
73 self
.add_signals('itemClicked(QListWidgetItem *)',
74 view
.stagedList
, view
.unstagedList
,)
76 self
.add_signals('itemSelectionChanged()',
77 view
.stagedList
, view
.unstagedList
,)
79 self
.add_signals('splitterMoved(int,int)',
80 view
.splitter_top
, view
.splitter_bottom
)
83 self
.connect(QtGui
.qApp
, 'lastWindowClosed()',
84 self
.last_window_closed
)
86 # These callbacks are called in response to the signals
87 # defined above. One property of the QObserver callback
88 # mechanism is that the model is passed in as the first
89 # argument to the callback. This allows for a single
90 # controller to manage multiple models, though this
91 # isn't used at the moment.
93 # Actions that delegate directly to the model
94 signOffButton
= model
.add_signoff
,
95 setCommitMessage
= model
.get_prev_commitmsg
,
96 stageChanged
= self
.model
.stage_changed
,
97 stageUntracked
= self
.model
.stage_untracked
,
98 unstageAll
= self
.model
.unstage_all
,
100 # Actions that delegate direclty to the view
101 cut
= view
.action_cut
,
102 copy
= view
.action_copy
,
103 paste
= view
.action_paste
,
104 delete
= view
.action_delete
,
105 selectAll
= view
.action_select_all
,
106 undo
= view
.action_undo
,
107 redo
= view
.action_redo
,
110 stageButton
= self
.stage_selected
,
111 commitButton
= self
.commit
,
112 pushButton
= self
.push
,
115 stagedList
= self
.diff_staged
,
116 unstagedList
= self
.diff_unstaged
,
119 untrackedCheckBox
= self
.rescan
,
122 options
= self
.options
,
123 rescan
= self
.rescan
,
124 createBranch
= self
.branch_create
,
125 deleteBranch
= self
.branch_delete
,
126 checkoutBranch
= self
.checkout_branch
,
127 rebaseBranch
= self
.rebase
,
128 commit
= self
.commit
,
129 stageSelected
= self
.stage_selected
,
130 unstageSelected
= self
.unstage_selected
,
131 showDiffstat
= self
.show_diffstat
,
132 browseBranch
= self
.browse_current
,
133 browseOtherBranch
= self
.browse_other
,
134 visualizeCurrent
= self
.viz_current
,
135 visualizeAll
= self
.viz_all
,
136 exportPatches
= self
.export_patches
,
137 cherryPick
= self
.cherry_pick
,
138 loadCommitMsg
= self
.load_commitmsg
,
141 splitter_top
= self
.splitter_top_event
,
142 splitter_bottom
= self
.splitter_bottom_event
,
145 # Handle double-clicks in the staged/unstaged lists.
146 # These are vanilla signal/slots since the qobserver
147 # signal routing is already handling these lists' signals.
148 self
.connect(view
.unstagedList
,
149 'itemDoubleClicked(QListWidgetItem*)',
152 self
.connect(view
.stagedList
,
153 'itemDoubleClicked(QListWidgetItem*)',
154 self
.unstage_selected
)
156 # Delegate window move events here
157 self
.view
.moveEvent
= self
.move_event
158 self
.view
.resizeEvent
= self
.resize_event
161 self
.load_window_settings()
164 # Setup the inotify watchdog
165 self
.__start
_inotify
_thread
()
167 #####################################################################
168 # Actions triggered during model updates
170 def action_staged(self
, widget
):
171 qtutils
.update_listwidget(widget
,
172 self
.model
.get_staged(), staged
=True)
174 def action_all_unstaged(self
, widget
):
175 qtutils
.update_listwidget(widget
,
176 self
.model
.get_unstaged(), staged
=False)
178 if self
.view
.untrackedCheckBox
.isChecked():
179 qtutils
.update_listwidget(widget
,
180 self
.model
.get_untracked(),
185 #####################################################################
189 update_options(self
.model
, self
.view
)
191 def branch_create(self
):
192 if create_new_branch(self
.model
, self
.view
):
195 def branch_delete(self
):
196 branch
= choose_branch('Delete Branch',
197 self
.view
, self
.model
.get_local_branches())
198 if not branch
: return
199 self
.show_output(self
.model
.delete_branch(branch
))
201 def browse_current(self
):
202 branch
= self
.model
.get_branch()
203 browse_git_branch(self
.model
, self
.view
, branch
)
205 def browse_other(self
):
206 # Prompt for a branch to browse
207 branch
= choose_branch('Browse Branch Files',
208 self
.view
, self
.model
.get_all_branches())
209 if not branch
: return
210 # Launch the repobrowser
211 browse_git_branch(self
.model
, self
.view
, branch
)
213 def checkout_branch(self
):
214 branch
= choose_branch('Checkout Branch',
215 self
.view
, self
.model
.get_local_branches())
216 if not branch
: return
217 self
.show_output(self
.model
.checkout(branch
))
219 def cherry_pick(self
):
220 commits
= self
.select_commits_gui(*self
.model
.log(all
=True))
221 if not commits
: return
222 self
.show_output(self
.model
.cherry_pick(commits
))
225 msg
= self
.model
.get_commitmsg()
227 error_msg
= self
.tr(""
228 + "Please supply a commit message.\n"
230 + "A good commit message has the following format:\n"
232 + "- First line: Describe in one sentence what you did.\n"
233 + "- Second line: Blank\n"
234 + "- Remaining lines: Describe why this change is good.\n")
236 self
.show_output(error_msg
)
239 files
= self
.model
.get_staged()
242 + "No changes to commit.\n"
244 + "You must stage at least 1 file before you can commit.\n")
245 self
.show_output(errmsg
)
249 output
= self
.model
.commit(msg
, amend
=self
.view
.amendRadio
.isChecked())
252 self
.view
.newCommitRadio
.setChecked(True)
253 self
.view
.amendRadio
.setChecked(False)
254 self
.model
.set_commitmsg('')
255 self
.show_output(output
)
257 def view_diff(self
, staged
=True):
258 self
.__staged
_diff
_in
_view
= staged
259 if self
.__staged
_diff
_in
_view
:
260 widget
= self
.view
.stagedList
262 widget
= self
.view
.unstagedList
263 row
, selected
= qtutils
.get_selected_row(widget
)
265 self
.view
.reset_display()
268 status
) = self
.model
.get_diff_and_status(row
, staged
=staged
)
270 self
.view
.set_display(diff
)
271 self
.view
.set_info(self
.tr(status
))
273 # use *rest to handle being called from different signals
274 def diff_staged(self
, *rest
):
275 self
.view_diff(staged
=True)
277 # use *rest to handle being called from different signals
278 def diff_unstaged(self
,*rest
):
279 self
.view_diff(staged
=False)
281 def export_patches(self
):
282 (revs
, summaries
) = self
.model
.log()
283 commits
= self
.select_commits_gui(revs
, summaries
)
284 if not commits
: return
285 self
.show_output(self
.model
.format_patch(commits
))
287 def last_window_closed(self
):
288 '''Save config settings and cleanup any inotify threads.'''
290 self
.model
.save_window_geom()
292 if not self
.inotify_thread
: return
293 if not self
.inotify_thread
.isRunning(): return
295 self
.inotify_thread
.abort
= True
296 self
.inotify_thread
.quit()
297 self
.inotify_thread
.wait()
299 def load_commitmsg(self
):
300 file = qtutils
.open_dialog(self
.view
,
301 'Load Commit Message...', defaults
.DIRECTORY
)
304 defaults
.DIRECTORY
= os
.path
.dirname(file)
305 slushy
= utils
.slurp(file)
306 if slushy
: self
.model
.set_commitmsg(slushy
)
309 branch
= choose_branch('Rebase Branch',
310 self
.view
, self
.model
.get_local_branches())
311 if not branch
: return
312 self
.show_output(self
.model
.rebase(branch
))
314 # use *rest to handle being called from the checkbox signal
315 def rescan(self
, *rest
):
316 '''Populates view widgets with results from "git status."'''
318 self
.view
.statusBar().showMessage(
319 self
.tr('Scanning for modified files ...'))
321 self
.model
.update_status()
323 branch
= self
.model
.get_branch()
324 status_text
= self
.tr('Current Branch:') + ' ' + branch
325 self
.view
.statusBar().showMessage(status_text
)
327 title
= '%s [%s]' % (self
.model
.get_project(), branch
)
328 self
.view
.setWindowTitle(title
)
330 if not self
.model
.has_squash_msg(): return
332 if self
.model
.get_commitmsg():
333 answer
= qtutils
.question(self
.view
,
334 self
.tr('Import Commit Message?'),
335 self
.tr('A commit message from an in-progress'
336 + ' merge was found.\nImport it?'))
338 if not answer
: return
340 # Set the new commit message
341 self
.model
.set_squash_msg()
344 push_branches(self
.model
, self
.view
)
346 def show_diffstat(self
):
347 '''Show the diffstat from the latest commit.'''
348 self
.show_output(self
.model
.diff_stat(), rescan
=False)
350 #####################################################################
353 def process_diff_selection(self
, items
, widget
,
354 cached
=True, selected
=False, reverse
=True, noop
=False):
356 filename
= qtutils
.get_selected_item(widget
, items
)
357 if not filename
: return
358 parser
= utils
.DiffParser(self
.model
, filename
=filename
,
361 offset
, selection
= self
.view
.diff_selection()
362 parser
.process_diff_selection(selected
, offset
, selection
)
365 def stage_hunk(self
):
366 self
.process_diff_selection(
367 self
.model
.get_unstaged(),
368 self
.view
.unstagedList
,
371 def stage_hunks(self
):
372 self
.process_diff_selection(
373 self
.model
.get_unstaged(),
374 self
.view
.unstagedList
,
378 def unstage_hunk(self
, cached
=True):
379 self
.process_diff_selection(
380 self
.model
.get_staged(),
381 self
.view
.stagedList
,
384 def unstage_hunks(self
):
385 self
.process_diff_selection(
386 self
.model
.get_staged(),
387 self
.view
.stagedList
,
391 # #######################################################################
394 # use *rest to handle being called from different signals
395 def stage_selected(self
,*rest
):
396 '''Use "git add" to add items to the git index.
397 This is a thin wrapper around __apply_to_list.'''
398 command
= self
.model
.add_or_remove
399 widget
= self
.view
.unstagedList
400 items
= self
.model
.get_all_unstaged()
401 self
.__apply
_to
_list
(command
,widget
,items
)
403 # use *rest to handle being called from different signals
404 def unstage_selected(self
, *rest
):
405 '''Use "git reset" to remove items from the git index.
406 This is a thin wrapper around __apply_to_list.'''
407 command
= self
.model
.reset
408 widget
= self
.view
.stagedList
409 items
= self
.model
.get_staged()
410 self
.__apply
_to
_list
(command
, widget
, items
)
413 '''Visualizes the entire git history using gitk.'''
414 utils
.fork('gitk','--all')
416 def viz_current(self
):
417 '''Visualizes the current branch's history using gitk.'''
418 utils
.fork('gitk', self
.model
.get_branch())
420 # These actions monitor window resizes, splitter changes, etc.
421 def move_event(self
, event
):
422 defaults
.X
= event
.pos().x()
423 defaults
.Y
= event
.pos().y()
425 def resize_event(self
, event
):
426 defaults
.WIDTH
= event
.size().width()
427 defaults
.HEIGHT
= event
.size().height()
429 def splitter_top_event(self
,*rest
):
430 sizes
= self
.view
.splitter_top
.sizes()
431 defaults
.SPLITTER_TOP_0
= sizes
[0]
432 defaults
.SPLITTER_TOP_1
= sizes
[1]
434 def splitter_bottom_event(self
,*rest
):
435 sizes
= self
.view
.splitter_bottom
.sizes()
436 defaults
.SPLITTER_BOTTOM_0
= sizes
[0]
437 defaults
.SPLITTER_BOTTOM_1
= sizes
[1]
439 def load_window_settings(self
):
442 sb0
,sb1
) = self
.model
.get_window_geom()
443 self
.view
.resize(w
,h
)
445 self
.view
.splitter_top
.setSizes([st0
,st1
])
446 self
.view
.splitter_bottom
.setSizes([sb0
,sb1
])
448 def show_output(self
, output
, rescan
=True):
449 '''Shows output and optionally rescans for changes.'''
450 qtutils
.show_output(self
.view
, output
)
453 #####################################################################
456 def __apply_to_list(self
, command
, widget
, items
):
457 '''This is a helper method that retrieves the current
458 selection list, applies a command to that list,
459 displays a dialog showing the output of that command,
460 and calls rescan to pickup changes.'''
461 apply_items
= qtutils
.get_selection_list(widget
, items
)
462 output
= command(apply_items
)
466 def __menu_about_to_show(self
):
468 unstaged_item
= qtutils
.get_selected_item(
469 self
.view
.unstagedList
,
470 self
.model
.get_all_unstaged())
472 is_tracked
= unstaged_item
not in self
.model
.get_untracked()
476 and not self
.__staged
_diff
_in
_view
480 self
.__staged
_diff
_in
_view
481 and qtutils
.get_selected_item(
482 self
.view
.stagedList
,
483 self
.model
.get_staged()))
485 self
.__stage
_hunk
_action
.setEnabled(bool(enable_staged
))
486 self
.__stage
_hunks
_action
.setEnabled(bool(enable_staged
))
488 self
.__unstage
_hunk
_action
.setEnabled(bool(enable_unstaged
))
489 self
.__unstage
_hunks
_action
.setEnabled(bool(enable_unstaged
))
491 def __menu_event(self
, event
):
493 textedit
= self
.view
.displayText
494 self
.__menu
.exec_(textedit
.mapToGlobal(event
.pos()))
496 def __menu_setup(self
):
497 if self
.__menu
: return
499 menu
= self
.__menu
= QMenu(self
.view
)
500 self
.__stage
_hunk
_action
= menu
.addAction(
501 self
.tr('Stage Hunk For Commit'),
504 self
.__stage
_hunks
_action
= menu
.addAction(
505 self
.tr('Stage Selected Lines'),
508 self
.__unstage
_hunk
_action
= menu
.addAction(
509 self
.tr('Unstage Hunk From Commit'),
512 self
.__unstage
_hunks
_action
= menu
.addAction(
513 self
.tr('Unstage Selected Lines'),
516 self
.__copy
_action
= menu
.addAction(
518 self
.view
.copy_display
)
520 self
.connect(self
.__menu
, 'aboutToShow()', self
.__menu
_about
_to
_show
)
522 def select_commits_gui(self
, revs
, summaries
):
523 return select_commits(self
.model
, self
.view
, revs
, summaries
)
525 def __start_inotify_thread(self
):
526 # Do we have inotify? If not, return.
527 # Recommend installing inotify if we're on Linux.
528 self
.inotify_thread
= None
530 from inotify
import GitNotifier
533 if platform
.system() == 'Linux':
534 msg
=(self
.tr('Unable import pyinotify.\n'
535 + 'inotify support has been'
539 plat
= platform
.platform().lower()
540 if 'debian' in plat
or 'ubuntu' in plat
:
541 msg
+= (self
.tr('Hint:')
542 + 'sudo apt-get install'
543 + ' python-pyinotify')
545 qtutils
.information(self
.view
,
546 self
.tr('inotify disabled'), msg
)
549 self
.inotify_thread
= GitNotifier(os
.getcwd())
550 self
.connect(self
.inotify_thread
,
551 'timeForRescan()', self
.rescan
)
553 # Start the notification thread
554 self
.inotify_thread
.start()