Changed the create_new_branch method's signature to be consistent
[ugit.git] / ugitlibs / controllers.py
blob99e52005605e70d746ba54d2a084685f85b2473e
1 #!/usr/bin/env python
2 import os
4 from PyQt4 import QtGui
5 from PyQt4.QtGui import QDialog
6 from PyQt4.QtGui import QMessageBox
7 from PyQt4.QtGui import QMenu
9 import utils
10 import qtutils
11 import defaults
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
28 self.__menu = None
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,
39 # and vice versa.
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
49 # defined below.
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,
64 view.unstageSelected,
65 view.showDiffstat,
66 view.browseBranch, view.browseOtherBranch,
67 view.visualizeAll, view.visualizeCurrent,
68 view.exportPatches, view.cherryPick,
69 view.loadCommitMsg,
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)
82 # App cleanup
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.
92 self.add_callbacks({
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,
99 # Actions that delegate direclty to the view
100 'cut': view.action_cut,
101 'copy': view.action_copy,
102 'paste': view.action_paste,
103 'delete': view.action_delete,
104 'selectAll': view.action_select_all,
105 'undo': view.action_undo,
106 'redo': view.action_redo,
107 # Push Buttons
108 'stageButton': self.stage_selected,
109 'commitButton': self.commit,
110 'pushButton': self.push,
111 # List Widgets
112 'stagedList': self.diff_staged,
113 'unstagedList': self.diff_unstaged,
114 # Checkboxes
115 'untrackedCheckBox': self.rescan,
116 # Menu Actions
117 'options': self.options,
118 'rescan': self.rescan,
119 'createBranch': self.branch_create,
120 'deleteBranch': self.branch_delete,
121 'checkoutBranch': self.checkout_branch,
122 'rebaseBranch': self.rebase,
123 'commit': self.commit,
124 'stageSelected': self.stage_selected,
125 'unstageSelected': self.unstage_selected,
126 'showDiffstat': self.show_diffstat,
127 'browseBranch': self.browse_current,
128 'browseOtherBranch': self.browse_other,
129 'visualizeCurrent': self.viz_current,
130 'visualizeAll': self.viz_all,
131 'exportPatches': self.export_patches,
132 'cherryPick': self.cherry_pick,
133 'loadCommitMsg': self.load_commitmsg,
134 # Splitters
135 'splitter_top': self.splitter_top_event,
136 'splitter_bottom': self.splitter_bottom_event,
139 # Handle double-clicks in the staged/unstaged lists.
140 # These are vanilla signal/slots since the qobserver
141 # signal routing is already handling these lists' signals.
142 self.connect(view.unstagedList,
143 'itemDoubleClicked(QListWidgetItem*)',
144 self.stage_selected)
146 self.connect(view.stagedList,
147 'itemDoubleClicked(QListWidgetItem*)',
148 self.unstage_selected )
150 # Delegate window move events here
151 self.view.moveEvent = self.move_event
152 self.view.resizeEvent = self.resize_event
154 # Initialize the GUI
155 self.load_window_settings()
156 self.rescan()
158 # Setup the inotify watchdog
159 self.__start_inotify_thread()
161 #####################################################################
162 # Actions triggered during model updates
164 def action_staged(self, widget):
165 qtutils.update_listwidget(widget,
166 self.model.get_staged(), staged=True)
168 def action_all_unstaged(self, widget):
169 qtutils.update_listwidget(widget,
170 self.model.get_unstaged(), staged=False)
172 if self.view.untrackedCheckBox.isChecked():
173 qtutils.update_listwidget(widget,
174 self.model.get_untracked(),
175 staged=False,
176 append=True,
177 untracked=True)
179 #####################################################################
180 # Qt callbacks
182 def options(self):
183 update_options(self.model, self.view)
185 def branch_create(self):
186 if create_new_branch(self.model, self.view):
187 self.rescan()
189 def branch_delete(self):
190 branch = choose_branch('Delete Branch',
191 self.view, self.model.get_local_branches())
192 if not branch: return
193 self.show_output(self.model.delete_branch(branch))
195 def browse_current(self):
196 branch = self.model.get_branch()
197 browse_git_branch(self.model, self.view, branch)
199 def browse_other(self):
200 # Prompt for a branch to browse
201 branch = choose_branch('Browse Branch Files',
202 self.view, self.model.get_all_branches())
203 if not branch: return
204 # Launch the repobrowser
205 browse_git_branch(self.model, self.view, branch)
207 def checkout_branch(self):
208 branch = choose_branch('Checkout Branch',
209 self.view, self.model.get_local_branches())
210 if not branch: return
211 self.show_output(self.model.checkout(branch))
213 def cherry_pick(self):
214 commits = self.select_commits_gui(*self.model.log(all=True))
215 if not commits: return
216 self.show_output(self.model.cherry_pick(commits))
218 def commit(self):
219 msg = self.model.get_commitmsg()
220 if not msg:
221 error_msg = self.tr(""
222 + "Please supply a commit message.\n"
223 + "\n"
224 + "A good commit message has the following format:\n"
225 + "\n"
226 + "- First line: Describe in one sentence what you did.\n"
227 + "- Second line: Blank\n"
228 + "- Remaining lines: Describe why this change is good.\n")
230 self.show_output(error_msg)
231 return
233 files = self.model.get_staged()
234 if not files:
235 errmsg = self.tr(""
236 + "No changes to commit.\n"
237 + "\n"
238 + "You must stage at least 1 file before you can commit.\n")
239 self.show_output(errmsg)
240 return
242 # Perform the commit
243 output = self.model.commit(msg, amend=self.view.amendRadio.isChecked())
245 # Reset state
246 self.view.newCommitRadio.setChecked(True)
247 self.view.amendRadio.setChecked(False)
248 self.model.set_commitmsg('')
249 self.show_output(output)
251 def view_diff(self, staged=True):
252 self.__staged_diff_in_view = staged
253 if self.__staged_diff_in_view:
254 widget = self.view.stagedList
255 else:
256 widget = self.view.unstagedList
257 row, selected = qtutils.get_selected_row(widget)
258 if not selected:
259 self.view.reset_display()
260 return
261 (diff,
262 status) = self.model.get_diff_and_status(row, staged=staged)
264 self.view.set_display(diff)
265 self.view.set_info(self.tr(status))
267 # use *rest to handle being called from different signals
268 def diff_staged(self, *rest):
269 self.view_diff(staged=True)
271 # use *rest to handle being called from different signals
272 def diff_unstaged(self,*rest):
273 self.view_diff(staged=False)
275 def export_patches(self):
276 (revs, summaries) = self.model.log()
277 commits = self.select_commits_gui(revs, summaries)
278 if not commits: return
279 self.show_output(self.model.format_patch(commits))
281 def last_window_closed(self):
282 '''Save config settings and cleanup any inotify threads.'''
284 self.model.save_window_geom()
286 if not self.inotify_thread: return
287 if not self.inotify_thread.isRunning(): return
289 self.inotify_thread.abort = True
290 self.inotify_thread.quit()
291 self.inotify_thread.wait()
293 def load_commitmsg(self):
294 file = qtutils.open_dialog(self.view,
295 'Load Commit Message...', defaults.DIRECTORY)
297 if file:
298 defaults.DIRECTORY = os.path.dirname(file)
299 slushy = utils.slurp(file)
300 if slushy: self.model.set_commitmsg(slushy)
302 def rebase(self):
303 branch = choose_branch('Rebase Branch',
304 self.view, self.model.get_local_branches())
305 if not branch: return
306 self.show_output(self.model.rebase(branch))
308 # use *rest to handle being called from the checkbox signal
309 def rescan(self, *rest):
310 '''Populates view widgets with results from "git status."'''
312 self.view.statusBar().showMessage(
313 self.tr('Scanning for modified files ...'))
315 self.model.update_status()
317 branch = self.model.get_branch()
318 status_text = self.tr('Current Branch:') + ' ' + branch
319 self.view.statusBar().showMessage(status_text)
321 title = '%s [%s]' % (self.model.get_project(), branch)
322 self.view.setWindowTitle(title)
324 if not self.model.has_squash_msg(): return
326 if self.model.get_commitmsg():
327 answer = qtutils.question(self.view,
328 self.tr('Import Commit Message?'),
329 self.tr('A commit message from an in-progress'
330 + ' merge was found.\nImport it?'))
332 if not answer: return
334 # Set the new commit message
335 self.model.set_squash_msg()
337 def push(self):
338 push_branches(self.model, self.view)
340 def show_diffstat(self):
341 '''Show the diffstat from the latest commit.'''
342 self.show_output(self.model.diff_stat(), rescan=False)
344 #####################################################################
345 # diff gui
347 def process_diff_selection(self, items, widget,
348 cached=True, selected=False, reverse=True, noop=False):
350 filename = qtutils.get_selected_item(widget, items)
351 if not filename: return
352 parser = utils.DiffParser(self.model, filename=filename,
353 cached=cached)
355 offset, selection = self.view.diff_selection()
356 parser.process_diff_selection(selected, offset, selection)
357 self.rescan()
359 def stage_hunk(self):
360 self.process_diff_selection(
361 self.model.get_unstaged(),
362 self.view.unstagedList,
363 cached=False)
365 def stage_hunks(self):
366 self.process_diff_selection(
367 self.model.get_unstaged(),
368 self.view.unstagedList,
369 cached=False,
370 selected=True)
372 def unstage_hunk(self, cached=True):
373 self.process_diff_selection(
374 self.model.get_staged(),
375 self.view.stagedList,
376 cached=True)
378 def unstage_hunks(self):
379 self.process_diff_selection(
380 self.model.get_staged(),
381 self.view.stagedList,
382 cached=True,
383 selected=True)
385 # #######################################################################
386 # end diff gui
388 # use *rest to handle being called from different signals
389 def stage_selected(self,*rest):
390 '''Use "git add" to add items to the git index.
391 This is a thin wrapper around __apply_to_list.'''
392 command = self.model.add_or_remove
393 widget = self.view.unstagedList
394 items = self.model.get_all_unstaged()
395 self.__apply_to_list(command,widget,items)
397 # use *rest to handle being called from different signals
398 def unstage_selected(self, *rest):
399 '''Use "git reset" to remove items from the git index.
400 This is a thin wrapper around __apply_to_list.'''
401 command = self.model.reset
402 widget = self.view.stagedList
403 items = self.model.get_staged()
404 self.__apply_to_list(command, widget, items)
406 def viz_all(self):
407 '''Visualizes the entire git history using gitk.'''
408 utils.fork('gitk','--all')
410 def viz_current(self):
411 '''Visualizes the current branch's history using gitk.'''
412 utils.fork('gitk', self.model.get_branch())
414 # These actions monitor window resizes, splitter changes, etc.
415 def move_event(self, event):
416 defaults.X = event.pos().x()
417 defaults.Y = event.pos().y()
419 def resize_event(self, event):
420 defaults.WIDTH = event.size().width()
421 defaults.HEIGHT = event.size().height()
423 def splitter_top_event(self,*rest):
424 sizes = self.view.splitter_top.sizes()
425 defaults.SPLITTER_TOP_0 = sizes[0]
426 defaults.SPLITTER_TOP_1 = sizes[1]
428 def splitter_bottom_event(self,*rest):
429 sizes = self.view.splitter_bottom.sizes()
430 defaults.SPLITTER_BOTTOM_0 = sizes[0]
431 defaults.SPLITTER_BOTTOM_1 = sizes[1]
433 def load_window_settings(self):
434 (w,h,x,y,
435 st0,st1,
436 sb0,sb1) = self.model.get_window_geom()
437 self.view.resize(w,h)
438 self.view.move(x,y)
439 self.view.splitter_top.setSizes([st0,st1])
440 self.view.splitter_bottom.setSizes([sb0,sb1])
442 def show_output(self, output, rescan=True):
443 '''Shows output and optionally rescans for changes.'''
444 qtutils.show_output(self.view, output)
445 self.rescan()
447 #####################################################################
450 def __apply_to_list(self, command, widget, items):
451 '''This is a helper method that retrieves the current
452 selection list, applies a command to that list,
453 displays a dialog showing the output of that command,
454 and calls rescan to pickup changes.'''
455 apply_items = qtutils.get_selection_list(widget, items)
456 output = command(apply_items)
457 self.rescan()
458 return output
460 def __menu_about_to_show(self):
462 unstaged_item = qtutils.get_selected_item(
463 self.view.unstagedList,
464 self.model.get_all_unstaged())
466 is_tracked= unstaged_item not in self.model.get_untracked()
468 enable_staged= (
469 unstaged_item
470 and not self.__staged_diff_in_view
471 and is_tracked)
473 enable_unstaged= (
474 self.__staged_diff_in_view
475 and qtutils.get_selected_item(
476 self.view.stagedList,
477 self.model.get_staged()))
479 self.__stage_hunk_action.setEnabled(bool(enable_staged))
480 self.__stage_hunks_action.setEnabled(bool(enable_staged))
482 self.__unstage_hunk_action.setEnabled(bool(enable_unstaged))
483 self.__unstage_hunks_action.setEnabled(bool(enable_unstaged))
485 def __menu_event(self, event):
486 self.__menu_setup()
487 textedit = self.view.displayText
488 self.__menu.exec_(textedit.mapToGlobal(event.pos()))
490 def __menu_setup(self):
491 if self.__menu: return
493 menu = self.__menu = QMenu(self.view)
494 self.__stage_hunk_action = menu.addAction(
495 self.tr('Stage Hunk For Commit'),
496 self.stage_hunk)
498 self.__stage_hunks_action = menu.addAction(
499 self.tr('Stage Selected Lines'),
500 self.stage_hunks)
502 self.__unstage_hunk_action = menu.addAction(
503 self.tr('Unstage Hunk From Commit'),
504 self.unstage_hunk)
506 self.__unstage_hunks_action = menu.addAction(
507 self.tr('Unstage Selected Lines'),
508 self.unstage_hunks)
510 self.__copy_action = menu.addAction(
511 self.tr('Copy'),
512 self.view.copy_display)
514 self.connect(self.__menu, 'aboutToShow()', self.__menu_about_to_show)
516 def select_commits_gui(self, revs, summaries):
517 return select_commits(self.model, self.view, revs, summaries)
519 def __start_inotify_thread(self):
520 # Do we have inotify? If not, return.
521 # Recommend installing inotify if we're on Linux.
522 self.inotify_thread = None
523 try:
524 from inotify import GitNotifier
525 except ImportError:
526 import platform
527 if platform.system() == 'Linux':
528 msg =(self.tr('Unable import pyinotify.\n'
529 + 'inotify support has been'
530 + 'disabled.')
531 + '\n\n')
533 plat = platform.platform().lower()
534 if 'debian' in plat or 'ubuntu' in plat:
535 msg += (self.tr('Hint:')
536 + 'sudo apt-get install'
537 + ' python-pyinotify')
539 qtutils.information(self.view,
540 self.tr('inotify disabled'), msg)
541 return
543 self.inotify_thread = GitNotifier(os.getcwd())
544 self.connect(self.inotify_thread,
545 'timeForRescan()', self.rescan)
547 # Start the notification thread
548 self.inotify_thread.start()