views.dag: Pass the parent widget to the dag view
[git-cola.git] / cola / views / main.py
blob0faa3561fe482bfcd0a514f65192d913abf7738d
1 """This view provides the main git-cola user interface.
2 """
3 import os
5 from PyQt4 import QtGui
6 from PyQt4 import QtCore
7 from PyQt4.QtCore import Qt
8 from PyQt4.QtCore import SIGNAL
10 import cola
11 from cola import core
12 from cola import gitcmds
13 from cola import guicmds
14 from cola import utils
15 from cola import qtutils
16 from cola import settings
17 from cola import signals
18 from cola import resources
19 from cola.compat import set
20 from cola.qtutils import SLOT
21 from cola.views import dag
22 from cola.views import about
23 from cola.views.syntax import DiffSyntaxHighlighter
24 from cola.views.mainwindow import MainWindow
25 from cola.controllers import classic
26 from cola.controllers import compare
27 from cola.controllers import search as smod
28 from cola.controllers import createtag
29 from cola.controllers.bookmark import manage_bookmarks
30 from cola.controllers.bookmark import save_bookmark
31 from cola.controllers.createbranch import create_new_branch
32 from cola.controllers.merge import local_merge
33 from cola.controllers.merge import abort_merge
34 from cola.controllers.options import update_options
35 from cola.controllers.util import choose_from_combo
36 from cola.controllers.util import choose_from_list
37 from cola.controllers.remote import remote_action
38 from cola.controllers.repobrowser import browse_git_branch
39 from cola.controllers.stash import stash
40 from cola.controllers.selectcommits import select_commits
42 class MainView(MainWindow):
43 """The main cola interface."""
45 # Read-only mode property
46 mode = property(lambda self: self.model.mode)
48 def __init__(self, parent=None):
49 MainWindow.__init__(self, parent)
50 self.setAcceptDrops(True)
52 # Qt does not support noun/verbs
53 self.commit_button.setText(qtutils.tr('Commit@@verb'))
54 self.commit_menu.setTitle(qtutils.tr('Commit@@verb'))
56 # Diff/patch syntax highlighter
57 self.syntax = DiffSyntaxHighlighter(self.display_text.document())
59 # Display the current column
60 self.connect(self.commitmsg,
61 SIGNAL('cursorPositionChanged()'),
62 self.show_cursor_position)
64 # Keeps track of merge messages we've seen
65 self.merge_message_hash = ''
67 # Initialize the seen tree widget indexes
68 self._seen_indexes = set()
70 # Initialize the GUI to show 'Column: 00'
71 self.show_cursor_position()
73 # Internal field used by import/export_state().
74 # Change this whenever dockwidgets are removed.
75 self._widget_version = 1
77 self.model = cola.model()
78 self.model.add_message_observer(self.model.message_updated,
79 self._update_view)
81 # Listen for text and amend messages
82 cola.notifier().listen(signals.diff_text, self.set_display)
83 cola.notifier().listen(signals.mode, self._mode_changed)
84 cola.notifier().listen(signals.inotify, self._inotify_enabled)
85 cola.notifier().listen(signals.amend, self.amend_checkbox.setChecked)
87 # Broadcast the amend mode
88 self.connect(self.amend_checkbox, SIGNAL('toggled(bool)'),
89 SLOT(signals.amend_mode))
91 # Add button callbacks
92 self._relay_button(self.alt_button, signals.reset_mode)
93 self._relay_button(self.rescan_button, signals.rescan)
94 self._relay_button(self.signoff_button, signals.add_signoff)
96 self._connect_button(self.stage_button, self.stage)
97 self._connect_button(self.unstage_button, self.unstage)
98 self._connect_button(self.commit_button, self.commit)
99 self._connect_button(self.fetch_button, self.fetch)
100 self._connect_button(self.push_button, self.push)
101 self._connect_button(self.pull_button, self.pull)
102 self._connect_button(self.stash_button, stash)
104 # Menu actions
105 actions = (
106 (self.menu_quit, self.close),
107 (self.menu_branch_compare, compare.branch_compare),
108 (self.menu_branch_diff, self.branch_diff),
109 (self.menu_branch_review, self.review_branch),
110 (self.menu_browse_branch, self.browse_current),
111 (self.menu_browse_other_branch, self.browse_other),
112 (self.menu_browse_commits, self.browse_commits),
113 (self.menu_create_tag, createtag.create_tag),
114 (self.menu_create_branch, create_new_branch),
115 (self.menu_checkout_branch, self.checkout_branch),
116 (self.menu_delete_branch, self.branch_delete),
117 (self.menu_rebase_branch, self.rebase),
118 (self.menu_clone_repo, guicmds.clone_repo),
119 (self.menu_commit_compare, compare.compare),
120 (self.menu_commit_compare_file, compare.compare_file),
121 (self.menu_cherry_pick, self.cherry_pick),
122 (self.menu_diff_expression, self.diff_expression),
123 (self.menu_diff_branch, self.diff_branch),
124 (self.menu_export_patches, self.export_patches),
125 (self.menu_help_about, about.launch_about_dialog),
126 (self.menu_help_docs,
127 lambda: self.model.git.web__browse(resources.html_docs())),
128 (self.menu_load_commitmsg, self.load_commitmsg),
129 (self.menu_load_commitmsg_template,
130 SLOT(signals.load_commit_template)),
131 (self.menu_manage_bookmarks, manage_bookmarks),
132 (self.menu_save_bookmark, save_bookmark),
133 (self.menu_merge_local, local_merge),
134 (self.menu_merge_abort, abort_merge),
135 (self.menu_open_repo, self.open_repo),
136 (self.menu_options, update_options),
137 (self.menu_rescan, SLOT(signals.rescan)),
138 (self.menu_search_grep, self.grep),
139 (self.menu_search_revision, smod.search(smod.REVISION_ID)),
140 (self.menu_search_revision_range, smod.search(smod.REVISION_RANGE)),
141 (self.menu_search_message, smod.search(smod.MESSAGE)),
142 (self.menu_search_path, smod.search(smod.PATH, True)),
143 (self.menu_search_date_range, smod.search(smod.DATE_RANGE)),
144 (self.menu_search_diff, smod.search(smod.DIFF)),
145 (self.menu_search_author, smod.search(smod.AUTHOR)),
146 (self.menu_search_committer, smod.search(smod.COMMITTER)),
147 (self.menu_show_diffstat, SLOT(signals.diffstat)),
148 (self.menu_stash, stash),
149 (self.menu_stage_modified, SLOT(signals.stage_modified)),
150 (self.menu_stage_untracked, SLOT(signals.stage_untracked)),
151 (self.menu_unstage_selected, SLOT(signals.unstage_selected)),
152 (self.menu_unstage_all, SLOT(signals.unstage_all)),
153 (self.menu_visualize_all, SLOT(signals.visualize_all)),
154 (self.menu_visualize_current, SLOT(signals.visualize_current)),
155 # TODO This edit menu stuff should/could be command objects
156 (self.menu_cut, self.action_cut),
157 (self.menu_copy, self.action_copy),
158 (self.menu_paste, self.commitmsg.paste),
159 (self.menu_delete, self.action_delete),
160 (self.menu_select_all, self.commitmsg.selectAll),
161 (self.menu_undo, self.commitmsg.undo),
162 (self.menu_redo, self.commitmsg.redo),
163 (self.menu_classic, lambda: classic.cola_classic(self)),
164 (self.menu_dag, lambda: dag.git_dag(parent=self)),
166 for menu, callback in actions:
167 self.connect(menu, SIGNAL('triggered()'), callback)
169 # Install diff shortcut keys for stage/unstage
170 self.display_text.keyPressEvent = self.diff_key_press_event
171 self.display_text.contextMenuEvent = self.diff_context_menu_event
173 # Restore saved settings
174 self._load_gui_state()
176 def _relay_button(self, button, signal):
177 callback = SLOT(signal)
178 self._connect_button(button, callback)
180 def _connect_button(self, button, callback):
181 self.connect(button, SIGNAL('clicked()'), callback)
183 def _inotify_enabled(self, enabled):
184 """Hide the rescan button when inotify is enabled."""
185 if enabled:
186 self.rescan_button.hide()
187 else:
188 self.rescan_button.show()
190 def _update_view(self):
191 """Update the title with the current branch and directory name."""
192 branch = self.model.currentbranch
193 curdir = os.getcwd()
194 msg = 'Repository: %s\nBranch: %s' % (curdir, branch)
196 self.setToolTip(msg)
198 title = '%s [%s]' % (self.model.project, branch)
199 if self.mode in (self.model.mode_diff, self.model.mode_diff_expr):
200 title += ' *** diff mode***'
201 elif self.mode == self.model.mode_review:
202 title += ' *** review mode***'
203 elif self.mode == self.model.mode_amend:
204 title += ' *** amending ***'
205 self.setWindowTitle(title)
207 if self.mode != self.model.mode_amend:
208 self.amend_checkbox.blockSignals(True)
209 self.amend_checkbox.setChecked(False)
210 self.amend_checkbox.blockSignals(False)
212 if not self.model.read_only() and self.mode != self.model.mode_amend:
213 # Check if there's a message file in .git/
214 merge_msg_path = gitcmds.merge_message_path()
215 if merge_msg_path is None:
216 return
217 merge_msg_hash = utils.checksum(merge_msg_path)
218 if merge_msg_hash == self.merge_message_hash:
219 return
220 self.merge_message_hash = merge_msg_hash
221 cola.notifier().broadcast(signals.load_commit_message,
222 merge_msg_path)
224 def _mode_changed(self, mode):
225 """React to mode changes; hide/show the "Exit Diff Mode" button."""
226 if mode in (self.model.mode_review,
227 self.model.mode_diff,
228 self.model.mode_diff_expr):
229 self.alt_button.setMinimumHeight(40)
230 self.alt_button.show()
231 else:
232 self.alt_button.setMinimumHeight(1)
233 self.alt_button.hide()
235 def set_display(self, text):
236 """Set the diff text display."""
237 scrollbar = self.display_text.verticalScrollBar()
238 scrollvalue = scrollbar.value()
239 if text is not None:
240 self.display_text.setPlainText(text)
241 scrollbar.setValue(scrollvalue)
243 def action_cut(self):
244 self.action_copy()
245 self.action_delete()
247 def action_copy(self):
248 cursor = self.commitmsg.textCursor()
249 selection = cursor.selection().toPlainText()
250 qtutils.set_clipboard(selection)
252 def action_delete(self):
253 self.commitmsg.textCursor().removeSelectedText()
255 def copy_display(self):
256 cursor = self.display_text.textCursor()
257 selection = cursor.selection().toPlainText()
258 qtutils.set_clipboard(selection)
260 def diff_selection(self):
261 cursor = self.display_text.textCursor()
262 offset = cursor.position()
263 selection = unicode(cursor.selection().toPlainText())
264 return offset, selection
266 def selected_line(self):
267 cursor = self.display_text.textCursor()
268 offset = cursor.position()
269 contents = unicode(self.display_text.toPlainText())
270 while (offset >= 1
271 and contents[offset-1]
272 and contents[offset-1] != '\n'):
273 offset -= 1
274 data = contents[offset:]
275 if '\n' in data:
276 line, rest = data.split('\n', 1)
277 else:
278 line = data
279 return line
281 def show_cursor_position(self):
282 """Update the UI with the current row and column."""
283 cursor = self.commitmsg.textCursor()
284 position = cursor.position()
285 txt = unicode(self.commitmsg.toPlainText())
286 rows = txt[:position].count('\n') + 1
287 cols = cursor.columnNumber()
288 display = ' %d,%d ' % (rows, cols)
289 if cols > 78:
290 display = ('<span style="color: white; '
291 ' background-color: red;"'
292 '>%s</span>' % display.replace(' ', '&nbsp;'))
293 elif cols > 72:
294 display = ('<span style="color: black; '
295 ' background-color: orange;"'
296 '>%s</span>' % display.replace(' ', '&nbsp;'))
297 elif cols > 64:
298 display = ('<span style="color: black; '
299 ' background-color: yellow;"'
300 '>%s</span>' % display.replace(' ', '&nbsp;'))
301 self.position_label.setText(display)
303 def import_state(self, state):
304 """Imports data for save/restore"""
305 MainWindow.import_state(self, state)
306 # Restore the dockwidget, etc. window state
307 if 'windowstate' in state:
308 windowstate = state['windowstate']
309 self.restoreState(QtCore.QByteArray.fromBase64(str(windowstate)),
310 self._widget_version)
312 def export_state(self):
313 """Exports data for save/restore"""
314 state = MainWindow.export_state(self)
315 # Save the window state
316 windowstate = self.saveState(self._widget_version)
317 state['windowstate'] = unicode(windowstate.toBase64().data())
318 return state
320 def review_branch(self):
321 """Diff against an arbitrary revision, branch, tag, etc."""
322 branch = choose_from_combo('Select Branch, Tag, or Commit-ish',
323 self.model.all_branches() +
324 self.model.tags)
325 if not branch:
326 return
327 cola.notifier().broadcast(signals.review_branch_mode, branch)
329 def branch_diff(self):
330 """Diff against an arbitrary revision, branch, tag, etc."""
331 branch = choose_from_combo('Select Branch, Tag, or Commit-ish',
332 ['HEAD^'] +
333 self.model.all_branches() +
334 self.model.tags)
335 if not branch:
336 return
337 cola.notifier().broadcast(signals.diff_mode, branch)
339 def diff_expression(self):
340 """Diff using an arbitrary expression."""
341 expr = choose_from_combo('Enter Diff Expression',
342 self.model.all_branches() +
343 self.model.tags)
344 if not expr:
345 return
346 cola.notifier().broadcast(signals.diff_expr_mode, expr)
349 def diff_branch(self):
350 """Launches a diff against a branch."""
351 branch = choose_from_combo('Select Branch, Tag, or Commit-ish',
352 ['HEAD^'] +
353 self.model.all_branches() +
354 self.model.tags)
355 if not branch:
356 return
357 zfiles_str = self.model.git.diff(branch, name_only=True,
358 no_color=True,
359 z=True).rstrip('\0')
360 files = zfiles_str.split('\0')
361 filename = choose_from_list('Select File', files)
362 if not filename:
363 return
364 cola.notifier().broadcast(signals.branch_mode, branch, filename)
366 def _load_gui_state(self):
367 """Restores the gui from the preferences file."""
368 state = settings.SettingsManager.gui_state(self)
369 self.import_state(state)
371 def load_commitmsg(self):
372 """Load a commit message from a file."""
373 filename = qtutils.open_dialog(self,
374 'Load Commit Message...',
375 self.model.getcwd())
376 if filename:
377 cola.notifier().broadcast(signals.load_commit_message, filename)
380 def diff_key_press_event(self, event):
381 """Handle shortcut keys in the diff view."""
382 if event.key() != QtCore.Qt.Key_H and event.key() != QtCore.Qt.Key_S:
383 event.ignore()
384 return
385 staged, modified, unmerged, untracked = cola.single_selection()
386 if event.key() == QtCore.Qt.Key_H:
387 if self.mode == self.model.mode_worktree and modified:
388 self.stage_hunk()
389 elif self.mode == self.model.mode_index:
390 self.unstage_hunk()
391 elif event.key() == QtCore.Qt.Key_S:
392 if self.mode == self.model.mode_worktree and modified:
393 self.stage_hunk_selection()
394 elif self.mode == self.model.mode_index:
395 self.unstage_hunk_selection()
397 def process_diff_selection(self, selected=False,
398 staged=True, apply_to_worktree=False,
399 reverse=False):
400 """Implement un/staging of selected lines or hunks."""
401 offset, selection = self.diff_selection()
402 cola.notifier().broadcast(signals.apply_diff_selection,
403 staged,
404 selected,
405 offset,
406 selection,
407 apply_to_worktree)
409 def undo_hunk(self):
410 """Destructively remove a hunk from a worktree file."""
411 if not qtutils.question(self,
412 'Destroy Local Changes?',
413 'This operation will drop '
414 'uncommitted changes.\n'
415 'Continue?',
416 default=False):
417 return
418 self.process_diff_selection(staged=False, apply_to_worktree=True,
419 reverse=True)
421 def undo_selection(self):
422 """Destructively check out content for the selected file from $head."""
423 if not qtutils.question(self,
424 'Destroy Local Changes?',
425 'This operation will drop '
426 'uncommitted changes.\n'
427 'Continue?',
428 default=False):
429 return
430 self.process_diff_selection(staged=False, apply_to_worktree=True,
431 reverse=True, selected=True)
433 def stage(self):
434 """Stage selected files, or all files if no selection exists."""
435 paths = cola.selection_model().unstaged
436 if not paths:
437 cola.notifier().broadcast(signals.stage_modified)
438 else:
439 cola.notifier().broadcast(signals.stage, paths)
441 def unstage(self):
442 """Unstage selected files, or all files if no selection exists."""
443 paths = cola.selection_model().staged
444 if not paths:
445 cola.notifier().broadcast(signals.unstage_all)
446 else:
447 cola.notifier().broadcast(signals.unstage, paths)
449 def stage_hunk(self):
450 """Stage a specific hunk."""
451 self.process_diff_selection(staged=False)
453 def stage_hunk_selection(self):
454 """Stage selected lines."""
455 self.process_diff_selection(staged=False, selected=True)
457 def unstage_hunk(self, cached=True):
458 """Unstage a hunk."""
459 self.process_diff_selection(staged=True)
461 def unstage_hunk_selection(self):
462 """Unstage selected lines."""
463 self.process_diff_selection(staged=True, selected=True)
465 def diff_context_menu_event(self, event):
466 """Create the context menu for the diff display."""
467 menu = self.diff_context_menu_setup()
468 textedit = self.display_text
469 menu.exec_(textedit.mapToGlobal(event.pos()))
471 def diff_context_menu_setup(self):
472 """Set up the context menu for the diff display."""
473 menu = QtGui.QMenu(self)
474 staged, modified, unmerged, untracked = cola.selection()
476 if self.mode == self.model.mode_worktree:
477 if modified and modified[0] in cola.model().submodules:
478 menu.addAction(self.tr('Stage'),
479 SLOT(signals.stage, modified))
480 menu.addAction(self.tr('Launch git-cola'),
481 SLOT(signals.open_repo,
482 os.path.abspath(modified[0])))
483 elif modified:
484 menu.addAction(self.tr('Stage &Hunk For Commit'),
485 self.stage_hunk)
486 menu.addAction(self.tr('Stage &Selected Lines'),
487 self.stage_hunk_selection)
488 menu.addSeparator()
489 menu.addAction(self.tr('Undo Hunk'),
490 self.undo_hunk)
491 menu.addAction(self.tr('Undo Selected Lines'),
492 self.undo_selection)
494 elif self.mode == self.model.mode_index:
495 if staged and staged[0] in cola.model().submodules:
496 menu.addAction(self.tr('Unstage'),
497 SLOT(signals.unstage, staged))
498 menu.addAction(self.tr('Launch git-cola'),
499 SLOT(signals.open_repo,
500 os.path.abspath(staged[0])))
501 else:
502 menu.addAction(self.tr('Unstage &Hunk From Commit'),
503 self.unstage_hunk)
504 menu.addAction(self.tr('Unstage &Selected Lines'),
505 self.unstage_hunk_selection)
507 elif self.mode == self.model.mode_branch:
508 menu.addAction(self.tr('Apply Diff to Work Tree'), self.stage_hunk)
509 menu.addAction(self.tr('Apply Diff Selection to Work Tree'), self.stage_hunk_selection)
511 elif self.mode == self.model.mode_grep:
512 menu.addAction(self.tr('Go Here'), self.goto_grep)
514 menu.addSeparator()
515 menu.addAction(self.tr('Copy'), self.copy_display)
516 return menu
518 def fetch(self):
519 """Launch the 'fetch' remote dialog."""
520 remote_action(self, 'fetch')
522 def push(self):
523 """Launch the 'push' remote dialog."""
524 remote_action(self, 'push')
526 def pull(self):
527 """Launch the 'pull' remote dialog."""
528 remote_action(self, 'pull')
530 def commit(self):
531 """Attempt to create a commit from the index and commit message."""
532 #self.reset_mode()
533 msg = self.model.commitmsg
534 if not msg:
535 # Describe a good commit message
536 error_msg = self.tr(''
537 'Please supply a commit message.\n\n'
538 'A good commit message has the following format:\n\n'
539 '- First line: Describe in one sentence what you did.\n'
540 '- Second line: Blank\n'
541 '- Remaining lines: Describe why this change is good.\n')
542 qtutils.log(1, error_msg)
543 cola.notifier().broadcast(signals.information,
544 'Missing Commit Message',
545 error_msg)
546 return
547 if not self.model.staged:
548 error_msg = self.tr(''
549 'No changes to commit.\n\n'
550 'You must stage at least 1 file before you can commit.\n')
551 qtutils.log(1, error_msg)
552 cola.notifier().broadcast(signals.information,
553 'No Staged Changes',
554 error_msg)
555 return
556 # Warn that amending published commits is generally bad
557 amend = self.amend_checkbox.isChecked()
558 if (amend and self.model.is_commit_published() and
559 not qtutils.question(self,
560 'Rewrite Published Commit?',
561 'This commit has already been published.\n'
562 'You are rewriting published history.\n'
563 'You probably don\'t want to do this.\n\n'
564 'Continue?',
565 default=False)):
566 return
567 # Perform the commit
568 cola.notifier().broadcast(signals.commit, amend, msg)
570 def grep(self):
571 """Prompt and use 'git grep' to find the content."""
572 # This should be a command in cola.commands.
573 txt, ok = qtutils.prompt('grep')
574 if not ok:
575 return
576 cola.notifier().broadcast(signals.grep, txt)
578 def goto_grep(self):
579 """Called when Search -> Grep's right-click 'goto' action."""
580 line = self.selected_line()
581 filename, line_number, contents = line.split(':', 2)
582 filename = core.encode(filename)
583 cola.notifier().broadcast(signals.edit, [filename], line_number=line_number)
585 def open_repo(self):
586 """Spawn a new cola session."""
587 dirname = qtutils.opendir_dialog(self,
588 'Open Git Repository...',
589 self.model.getcwd())
590 if not dirname:
591 return
592 cola.notifier().broadcast(signals.open_repo, dirname)
594 def cherry_pick(self):
595 """Launch the 'Cherry-Pick' dialog."""
596 revs, summaries = gitcmds.log_helper(all=True)
597 commits = select_commits('Cherry-Pick Commit',
598 revs, summaries, multiselect=False)
599 if not commits:
600 return
601 cola.notifier().broadcast(signals.cherry_pick, commits)
603 def browse_commits(self):
604 """Launch the 'Browse Commits' dialog."""
605 revs, summaries = gitcmds.log_helper(all=True)
606 select_commits('Browse Commits', revs, summaries)
608 def export_patches(self):
609 """Run 'git format-patch' on a list of commits."""
610 revs, summaries = gitcmds.log_helper()
611 to_export = select_commits('Export Patches', revs, summaries)
612 if not to_export:
613 return
614 to_export.reverse()
615 revs.reverse()
616 cola.notifier().broadcast(signals.format_patch, to_export, revs)
618 def browse_current(self):
619 """Launch the 'Browse Current Branch' dialog."""
620 browse_git_branch(gitcmds.current_branch())
622 def browse_other(self):
623 """Prompt for a branch and inspect content at that point in time."""
624 # Prompt for a branch to browse
625 branch = choose_from_combo('Browse Revision...', gitcmds.all_refs())
626 if not branch:
627 return
628 # Launch the repobrowser
629 browse_git_branch(branch)
631 def branch_create(self):
632 """Launch the 'Create Branch' dialog."""
633 create_new_branch()
635 def branch_delete(self):
636 """Launch the 'Delete Branch' dialog."""
637 branch = choose_from_combo('Delete Branch',
638 self.model.local_branches)
639 if not branch:
640 return
641 cola.notifier().broadcast(signals.delete_branch, branch)
643 def checkout_branch(self):
644 """Launch the 'Checkout Branch' dialog."""
645 branch = choose_from_combo('Checkout Branch',
646 self.model.local_branches)
647 if not branch:
648 return
649 cola.notifier().broadcast(signals.checkout_branch, branch)
651 def rebase(self):
652 """Rebase onto a branch."""
653 branch = choose_from_combo('Rebase Branch',
654 self.model.all_branches())
655 if not branch:
656 return
657 #TODO cmd
658 status, output = self.model.git.rebase(branch,
659 with_stderr=True,
660 with_status=True)
661 qtutils.log(status, output)
663 def dragEnterEvent(self, event):
664 """Accepts drops"""
665 MainWindow.dragEnterEvent(self, event)
666 event.acceptProposedAction()
668 def dropEvent(self, event):
669 """Apply dropped patches with git-am"""
670 event.accept()
671 urls = event.mimeData().urls()
672 if not urls:
673 return
674 paths = map(lambda x: unicode(x.path()), urls)
675 patches = [p for p in paths if p.endswith('.patch')]
676 dirs = [p for p in paths if os.path.isdir(p)]
677 dirs.sort()
678 for d in dirs:
679 patches.extend(self._gather_patches(d))
680 # Broadcast the patches to apply
681 cola.notifier().broadcast(signals.apply_patches, patches)
683 def _gather_patches(self, path):
684 """Find patches in a subdirectory"""
685 patches = []
686 for root, subdirs, files in os.walk(path):
687 for name in [f for f in files if f.endswith('.patch')]:
688 patches.append(os.path.join(root, name))
689 return patches