views.actions: Add a command wrapper for run_command and use it
[git-cola.git] / cola / commands.py
blob6660d8e4af058f3f44eb2cf2ba09faedb7bcbd7e
1 import os
2 import sys
4 from cStringIO import StringIO
6 import cola
7 from cola import i18n
8 from cola import core
9 from cola import gitcfg
10 from cola import gitcmds
11 from cola import utils
12 from cola import signals
13 from cola import cmdfactory
14 from cola import difftool
15 from cola import version
16 from cola.diffparse import DiffParser
17 from cola.models import selection
19 _notifier = cola.notifier()
20 _factory = cmdfactory.factory()
21 _config = gitcfg.instance()
24 class Command(object):
25 """Base class for all commands; provides the command pattern."""
26 def __init__(self, update=False):
27 """Initialize the command and stash away values for use in do()"""
28 # These are commonly used so let's make it easier to write new commands.
29 self.undoable = False
30 self.model = cola.model()
31 self.update = update
33 self.old_diff_text = self.model.diff_text
34 self.old_filename = self.model.filename
35 self.old_mode = self.model.mode
36 self.old_head = self.model.head
38 self.new_diff_text = self.old_diff_text
39 self.new_filename = self.old_filename
40 self.new_head = self.old_head
41 self.new_mode = self.old_mode
43 def do(self):
44 """Perform the operation."""
45 self.model.set_diff_text(self.new_diff_text)
46 self.model.set_filename(self.new_filename)
47 self.model.set_head(self.new_head)
48 self.model.set_mode(self.new_mode)
49 if self.update:
50 self.model.update_status()
52 def is_undoable(self):
53 """Can this be undone?"""
54 return self.undoable
56 def undo(self):
57 """Undo the operation."""
58 self.model.set_diff_text(self.old_diff_text)
59 self.model.set_filename(self.old_filename)
60 self.model.set_head(self.old_head)
61 self.model.set_mode(self.old_mode)
62 if self.update:
63 self.model.update_status()
65 def name(self):
66 """Return this command's name."""
67 return self.__class__.__name__
70 class AddSignoff(Command):
71 """Add a signed-off-by to the commit message."""
72 def __init__(self):
73 Command.__init__(self)
74 self.undoable = True
75 self.old_commitmsg = self.model.commitmsg
76 self.new_commitmsg = self.old_commitmsg
77 signoff = ('\nSigned-off-by: %s <%s>\n' %
78 (self.model.local_user_name, self.model.local_user_email))
79 if signoff not in self.new_commitmsg:
80 self.new_commitmsg += ('\n' + signoff)
82 def do(self):
83 self.model.set_commitmsg(self.new_commitmsg)
85 def undo(self):
86 self.model.set_commitmsg(self.old_commitmsg)
89 class AmendMode(Command):
90 """Try to amend a commit."""
91 def __init__(self, amend):
92 Command.__init__(self, update=True)
93 self.undoable = True
94 self.skip = False
95 self.amending = amend
96 self.old_commitmsg = self.model.commitmsg
98 if self.amending:
99 self.new_mode = self.model.mode_amend
100 self.new_head = 'HEAD^'
101 self.new_commitmsg = self.model.prev_commitmsg()
102 return
103 # else, amend unchecked, regular commit
104 self.new_mode = self.model.mode_none
105 self.new_head = 'HEAD'
106 self.new_commitmsg = self.model.commitmsg
107 # If we're going back into new-commit-mode then search the
108 # undo stack for a previous amend-commit-mode and grab the
109 # commit message at that point in time.
110 if not _factory.undostack:
111 return
112 undo_count = len(_factory.undostack)
113 for i in xrange(undo_count):
114 # Find the latest AmendMode command
115 idx = undo_count - i - 1
116 cmdobj = _factory.undostack[idx]
117 if type(cmdobj) is not AmendMode:
118 continue
119 if cmdobj.amending:
120 self.new_commitmsg = cmdobj.old_commitmsg
121 break
123 def do(self):
124 """Leave/enter amend mode."""
125 """Attempt to enter amend mode. Do not allow this when merging."""
126 if self.amending:
127 if os.path.exists(self.model.git.git_path('MERGE_HEAD')):
128 self.skip = True
129 _notifier.broadcast(signals.amend, False)
130 _factory.prompt_user(signals.information,
131 'Oops! Unmerged',
132 'You are in the middle of a merge.\n'
133 'You cannot amend while merging.')
134 return
135 self.skip = False
136 _notifier.broadcast(signals.amend, self.amending)
137 self.model.set_commitmsg(self.new_commitmsg)
138 Command.do(self)
140 def undo(self):
141 if self.skip:
142 return
143 self.model.set_commitmsg(self.old_commitmsg)
144 Command.undo(self)
147 class ApplyDiffSelection(Command):
148 def __init__(self, staged, selected, offset, selection, apply_to_worktree):
149 Command.__init__(self, update=True)
150 self.staged = staged
151 self.selected = selected
152 self.offset = offset
153 self.selection = selection
154 self.apply_to_worktree = apply_to_worktree
156 def do(self):
157 if self.model.mode == self.model.mode_branch:
158 # We're applying changes from a different branch!
159 parser = DiffParser(self.model,
160 filename=self.model.filename,
161 cached=False,
162 branch=self.model.head)
163 parser.process_diff_selection(self.selected,
164 self.offset,
165 self.selection,
166 apply_to_worktree=True)
167 else:
168 # The normal worktree vs index scenario
169 parser = DiffParser(self.model,
170 filename=self.model.filename,
171 cached=self.staged,
172 reverse=self.apply_to_worktree)
173 parser.process_diff_selection(self.selected,
174 self.offset,
175 self.selection,
176 apply_to_worktree=
177 self.apply_to_worktree)
178 # Redo the diff to show changes
179 if self.staged:
180 diffcmd = DiffStaged([self.model.filename])
181 else:
182 diffcmd = Diff([self.model.filename])
183 diffcmd.do()
184 self.model.update_status()
186 class ApplyPatches(Command):
187 def __init__(self, patches):
188 Command.__init__(self)
189 patches.sort()
190 self.patches = patches
192 def do(self):
193 diff_text = ''
194 num_patches = len(self.patches)
195 orig_head = self.model.git.rev_parse('HEAD')
197 for idx, patch in enumerate(self.patches):
198 status, output = self.model.git.am(patch,
199 with_status=True,
200 with_stderr=True)
201 # Log the git-am command
202 _notifier.broadcast(signals.log_cmd, status, output)
204 if num_patches > 1:
205 diff = self.model.git.diff('HEAD^!', stat=True)
206 diff_text += 'Patch %d/%d - ' % (idx+1, num_patches)
207 diff_text += '%s:\n%s\n\n' % (os.path.basename(patch), diff)
209 diff_text += 'Summary:\n'
210 diff_text += self.model.git.diff(orig_head, stat=True)
212 # Display a diffstat
213 self.model.set_diff_text(diff_text)
215 _factory.prompt_user(signals.information,
216 'Patch(es) Applied',
217 '%d patch(es) applied:\n\n%s' %
218 (len(self.patches),
219 '\n'.join(map(os.path.basename, self.patches))))
222 class HeadChangeCommand(Command):
223 """Changes the model's current head."""
224 def __init__(self, treeish):
225 Command.__init__(self, update=True)
226 self.new_head = treeish
227 self.new_diff_text = ''
230 class BranchMode(HeadChangeCommand):
231 """Enter into diff-branch mode."""
232 def __init__(self, treeish, filename):
233 HeadChangeCommand.__init__(self, treeish)
234 self.old_filename = self.model.filename
235 self.new_filename = filename
236 self.new_mode = self.model.mode_branch
237 self.new_diff_text = gitcmds.diff_helper(filename=filename,
238 cached=False,
239 reverse=True,
240 branch=treeish)
241 class Checkout(Command):
243 A command object for git-checkout.
245 'argv' is handed off directly to git.
248 def __init__(self, argv):
249 Command.__init__(self)
250 self.argv = argv
252 def do(self):
253 status, output = self.model.git.checkout(with_stderr=True,
254 with_status=True, *self.argv)
255 _notifier.broadcast(signals.log_cmd, status, output)
256 self.model.set_diff_text('')
257 self.model.update_status()
260 class CheckoutBranch(Checkout):
261 """Checkout a branch."""
262 def __init__(self, branch):
263 Checkout.__init__(self, [branch])
266 class CherryPick(Command):
267 """Cherry pick commits into the current branch."""
268 def __init__(self, commits):
269 Command.__init__(self)
270 self.commits = commits
272 def do(self):
273 self.model.cherry_pick_list(self.commits)
276 class ResetMode(Command):
277 """Reset the mode and clear the model's diff text."""
278 def __init__(self):
279 Command.__init__(self, update=True)
280 self.new_mode = self.model.mode_none
281 self.new_head = 'HEAD'
282 self.new_diff_text = ''
285 class Commit(ResetMode):
286 """Attempt to create a new commit."""
287 def __init__(self, amend, msg):
288 ResetMode.__init__(self)
289 self.amend = amend
290 self.msg = core.encode(msg)
291 self.old_commitmsg = self.model.commitmsg
292 self.new_commitmsg = ''
294 def do(self):
295 status, output = self.model.commit_with_msg(self.msg, amend=self.amend)
296 if status == 0:
297 ResetMode.do(self)
298 self.model.set_commitmsg(self.new_commitmsg)
299 title = 'Commit: '
300 else:
301 title = 'Commit failed: '
302 _notifier.broadcast(signals.log_cmd, status, title+output)
305 class Delete(Command):
306 """Simply delete files."""
307 def __init__(self, filenames):
308 Command.__init__(self)
309 self.filenames = filenames
310 # We could git-hash-object stuff and provide undo-ability
311 # as an option. Heh.
312 def do(self):
313 rescan = False
314 for filename in self.filenames:
315 if filename:
316 try:
317 os.remove(filename)
318 rescan=True
319 except:
320 _factory.prompt_user(signals.information,
321 'Error'
322 'Deleting "%s" failed.' % filename)
323 if rescan:
324 self.model.update_status()
326 class DeleteBranch(Command):
327 """Delete a git branch."""
328 def __init__(self, branch):
329 Command.__init__(self)
330 self.branch = branch
332 def do(self):
333 status, output = self.model.delete_branch(self.branch)
334 title = ''
335 if output.startswith('error:'):
336 output = 'E' + output[1:]
337 else:
338 title = 'Info: '
339 _notifier.broadcast(signals.log_cmd, status, title + output)
342 class Diff(Command):
343 """Perform a diff and set the model's current text."""
344 def __init__(self, filenames, cached=False):
345 Command.__init__(self)
346 opts = {}
347 if cached:
348 cached = not self.model.read_only()
349 opts = dict(ref=self.model.head)
351 self.new_filename = filenames[0]
352 self.old_filename = self.model.filename
353 if not self.model.read_only():
354 if self.model.mode != self.model.mode_amend:
355 self.new_mode = self.model.mode_worktree
356 self.new_diff_text = gitcmds.diff_helper(filename=self.new_filename,
357 cached=cached, **opts)
360 class DiffMode(HeadChangeCommand):
361 """Enter diff mode and clear the model's diff text."""
362 def __init__(self, treeish):
363 HeadChangeCommand.__init__(self, treeish)
364 self.new_mode = self.model.mode_diff
367 class DiffExprMode(HeadChangeCommand):
368 """Enter diff-expr mode and clear the model's diff text."""
369 def __init__(self, treeish):
370 HeadChangeCommand.__init__(self, treeish)
371 self.new_mode = self.model.mode_diff_expr
374 class Diffstat(Command):
375 """Perform a diffstat and set the model's diff text."""
376 def __init__(self):
377 Command.__init__(self)
378 diff = self.model.git.diff(self.model.head,
379 unified=_config.get('diff.context', 3),
380 no_color=True,
381 M=True,
382 stat=True)
383 self.new_diff_text = core.decode(diff)
384 self.new_mode = self.model.mode_worktree
387 class DiffStaged(Diff):
388 """Perform a staged diff on a file."""
389 def __init__(self, filenames):
390 Diff.__init__(self, filenames, cached=True)
391 if not self.model.read_only():
392 if self.model.mode != self.model.mode_amend:
393 self.new_mode = self.model.mode_index
396 class DiffStagedSummary(Command):
397 def __init__(self):
398 Command.__init__(self)
399 cached = not self.model.read_only()
400 diff = self.model.git.diff(self.model.head,
401 cached=cached,
402 no_color=True,
403 patch_with_stat=True,
404 M=True)
405 self.new_diff_text = core.decode(diff)
406 if not self.model.read_only():
407 if self.model.mode != self.model.mode_amend:
408 self.new_mode = self.model.mode_index
411 class Difftool(Command):
412 """Run git-difftool limited by path."""
413 def __init__(self, staged, filenames):
414 Command.__init__(self)
415 self.staged = staged
416 self.filenames = filenames
418 def do(self):
419 if not self.filenames:
420 return
421 args = []
422 if self.staged and not self.model.read_only():
423 args.append('--cached')
424 args.extend([self.model.head, '--'])
425 args.extend(self.filenames)
426 difftool.launch(args)
429 class Edit(Command):
430 """Edit a file using the configured gui.editor."""
431 def __init__(self, filenames, line_number=None):
432 Command.__init__(self)
433 self.filenames = filenames
434 self.line_number = line_number
436 def do(self):
437 filename = self.filenames[0]
438 if not os.path.exists(filename):
439 return
440 editor = self.model.editor()
441 if 'vi' in editor and self.line_number:
442 utils.fork([editor, filename, '+'+self.line_number])
443 else:
444 utils.fork([editor, filename])
447 class FormatPatch(Command):
448 """Output a patch series given all revisions and a selected subset."""
449 def __init__(self, to_export, revs):
450 Command.__init__(self)
451 self.to_export = to_export
452 self.revs = revs
454 def do(self):
455 status, output = gitcmds.format_patchsets(self.to_export, self.revs)
456 _notifier.broadcast(signals.log_cmd, status, output)
459 class GrepMode(Command):
460 def __init__(self, txt):
461 """Perform a git-grep."""
462 Command.__init__(self)
463 self.new_mode = self.model.mode_grep
464 self.new_diff_text = self.model.git.grep(txt, n=True)
466 class LoadCommitMessage(Command):
467 """Loads a commit message from a path."""
468 def __init__(self, path):
469 Command.__init__(self)
470 if not path or not os.path.exists(path):
471 raise OSError('error: "%s" does not exist' % path)
472 self.undoable = True
473 self.path = path
474 self.old_commitmsg = self.model.commitmsg
475 self.old_directory = self.model.directory
477 def do(self):
478 self.model.set_directory(os.path.dirname(self.path))
479 self.model.set_commitmsg(utils.slurp(self.path))
481 def undo(self):
482 self.model.set_commitmsg(self.old_commitmsg)
483 self.model.set_directory(self.old_directory)
486 class LoadCommitTemplate(LoadCommitMessage):
487 """Loads the commit message template specified by commit.template."""
488 def __init__(self):
489 LoadCommitMessage.__init__(self, _config.get('commit.template'))
492 class Mergetool(Command):
493 """Launch git-mergetool on a list of paths."""
494 def __init__(self, paths):
495 Command.__init__(self)
496 self.paths = paths
498 def do(self):
499 if not self.paths:
500 return
501 if version.check('mergetool-no-prompt',
502 self.model.git.version().split()[-1]):
503 utils.fork(['git', 'mergetool', '--no-prompt', '--'] + self.paths)
504 else:
505 utils.fork(['xterm', '-e', 'git', 'mergetool', '--'] + self.paths)
508 class OpenRepo(Command):
509 """Launches git-cola on a repo."""
510 def __init__(self, dirname):
511 Command.__init__(self)
512 self.new_directory = utils.quote_repopath(dirname)
514 def do(self):
515 self.model.set_directory(self.new_directory)
516 utils.fork(['python', sys.argv[0], '--repo', self.new_directory])
519 class Clone(Command):
520 """Clones a repository and optionally spawns a new cola session."""
521 def __init__(self, url, destdir, spawn=True):
522 Command.__init__(self)
523 self.url = url
524 self.new_directory = utils.quote_repopath(destdir)
525 self.spawn = spawn
527 def do(self):
528 self.model.git.clone(self.url, self.new_directory,
529 with_stderr=True, with_status=True)
530 if self.spawn:
531 utils.fork(['python', sys.argv[0], '--repo', self.new_directory])
534 class Rescan(Command):
535 """Rescans for changes."""
536 def __init__(self):
537 Command.__init__(self, update=True)
540 class ReviewBranchMode(Command):
541 """Enter into review-branch mode."""
542 def __init__(self, branch):
543 Command.__init__(self, update=True)
544 self.new_mode = self.model.mode_review
545 self.new_head = gitcmds.merge_base_parent(branch)
546 self.new_diff_text = ''
549 class RunConfigAction(Command):
550 """Run a user-configured action, typically from the "Tools" menu"""
551 def __init__(self, name):
552 Command.__init__(self)
553 self.name = name
554 self.model = cola.model()
556 def do(self):
557 for env in ('FILENAME', 'REVISION', 'ARGS'):
558 try:
559 del os.environ[env]
560 except KeyError:
561 pass
562 rev = None
563 args = None
564 opts = _config.get_guitool_opts(self.name)
565 cmd = opts.get('cmd')
566 if 'title' not in opts:
567 opts['title'] = cmd
569 if 'prompt' not in opts or opts.get('prompt') is True:
570 prompt = i18n.gettext('Are you sure you want to run %s?') % cmd
571 opts['prompt'] = prompt
573 if opts.get('needsfile'):
574 filename = selection.filename()
575 if not filename:
576 _factory.prompt_user(signals.information,
577 'Please select a file',
578 '"%s" requires a selected file' % cmd)
579 return
580 os.environ['FILENAME'] = utils.shell_quote(filename)
583 if opts.get('revprompt') or opts.get('argprompt'):
584 while True:
585 ok = _factory.prompt_user(signals.run_config_action, cmd, opts)
586 if not ok:
587 return
588 rev = opts.get('revision')
589 args = opts.get('args')
590 if opts.get('revprompt') and not rev:
591 msg = ('Invalid revision:\n\n'
592 'Revision expression is empty')
593 _factory.prompt_user(signals.information, title, msg)
594 continue
595 break
597 elif opts.get('confirm'):
598 title = os.path.expandvars(opts.get('title'))
599 prompt = os.path.expandvars(opts.get('prompt'))
600 if not _factory.prompt_user(signals.question, title, prompt):
601 return
602 if rev:
603 os.environ['REVISION'] = rev
604 if args:
605 os.environ['ARGS'] = args
606 title = os.path.expandvars(cmd)
607 cmdexpand = os.path.expandvars(cmd)
608 _notifier.broadcast(signals.log_cmd, 0, 'running: ' + cmdexpand)
610 if opts.get('noconsole'):
611 status, out, err = utils.run_command(cmdexpand,
612 flag_error=False,
613 shell=True)
614 else:
615 status, out, err = _factory.prompt_user(signals.run_command,
616 title,
617 'sh', ['-c', cmdexpand])
619 _notifier.broadcast(signals.log_cmd, status,
620 'stdout: %s\nstatus: %s\nstderr: %s' %
621 (out.rstrip(), status, err.rstrip()))
623 if not opts.get('norescan'):
624 self.model.update_status()
625 return status
628 class ShowUntracked(Command):
629 """Show an untracked file."""
630 # We don't actually do anything other than set the mode right now.
631 # TODO check the mimetype for the file and handle things
632 # generically.
633 def __init__(self, filenames):
634 Command.__init__(self)
635 self.new_mode = self.model.mode_worktree
636 # TODO new_diff_text = utils.file_preview(filenames[0])
639 class Stage(Command):
640 """Stage a set of paths."""
641 def __init__(self, paths):
642 Command.__init__(self)
643 self.paths = paths
645 def do(self):
646 msg = 'Staging: %s' % (', '.join(self.paths))
647 _notifier.broadcast(signals.log_cmd, 0, msg)
648 self.model.stage_paths(self.paths)
651 class StageModified(Stage):
652 """Stage all modified files."""
653 def __init__(self):
654 Stage.__init__(self, None)
655 self.paths = self.model.modified
658 class StageUntracked(Stage):
659 """Stage all untracked files."""
660 def __init__(self):
661 Stage.__init__(self, None)
662 self.paths = self.model.untracked
664 class Tag(Command):
665 """Create a tag object."""
666 def __init__(self, name, revision, sign=False, message=''):
667 Command.__init__(self)
668 self._name = name
669 self._message = core.encode(message)
670 self._revision = revision
671 self._sign = sign
673 def do(self):
674 log_msg = 'Tagging: "%s" as "%s"' % (self._revision, self._name)
675 if self._sign:
676 log_msg += ', GPG-signed'
677 path = self.model.tmp_filename()
678 utils.write(path, self._message)
679 status, output = self.model.git.tag(self._name,
680 self._revision,
681 s=True,
682 F=path,
683 with_status=True,
684 with_stderr=True)
685 os.unlink(path)
686 else:
687 status, output = self.model.git.tag(self._name,
688 self._revision,
689 with_status=True,
690 with_stderr=True)
691 if output:
692 log_msg += '\nOutput:\n%s' % output
694 _notifier.broadcast(signals.log_cmd, status, log_msg)
695 if status == 0:
696 self.model.update_status()
699 class Unstage(Command):
700 """Unstage a set of paths."""
701 def __init__(self, paths):
702 Command.__init__(self)
703 self.paths = paths
705 def do(self):
706 msg = 'Unstaging: %s' % (', '.join(self.paths))
707 _notifier.broadcast(signals.log_cmd, 0, msg)
708 gitcmds.unstage_paths(self.paths)
709 self.model.update_status()
712 class UnstageAll(Command):
713 """Unstage all files; resets the index."""
714 def __init__(self):
715 Command.__init__(self, update=True)
717 def do(self):
718 self.model.unstage_all()
721 class UnstageSelected(Unstage):
722 """Unstage selected files."""
723 def __init__(self):
724 Unstage.__init__(self, cola.selection_model().staged)
727 class UntrackedSummary(Command):
728 """List possible .gitignore rules as the diff text."""
729 def __init__(self):
730 Command.__init__(self)
731 untracked = self.model.untracked
732 suffix = len(untracked) > 1 and 's' or ''
733 io = StringIO()
734 io.write('# %s untracked file%s\n' % (len(untracked), suffix))
735 if untracked:
736 io.write('# possible .gitignore rule%s:\n' % suffix)
737 for u in untracked:
738 io.write('/%s\n' % u)
739 self.new_diff_text = io.getvalue()
742 class VisualizeAll(Command):
743 """Visualize all branches."""
744 def do(self):
745 browser = self.model.history_browser()
746 utils.fork([browser, '--all'])
749 class VisualizeCurrent(Command):
750 """Visualize all branches."""
751 def do(self):
752 browser = self.model.history_browser()
753 utils.fork([browser, self.model.currentbranch])
756 class VisualizePaths(Command):
757 """Path-limited visualization."""
758 def __init__(self, paths):
759 Command.__init__(self)
760 browser = self.model.history_browser()
761 if paths:
762 self.argv = [browser] + paths
763 else:
764 self.argv = [browser]
766 def do(self):
767 utils.fork(self.argv)
770 def register():
772 Register signal mappings with the factory.
774 These commands are automatically created and run when
775 their corresponding signal is broadcast by the notifier.
778 signal_to_command_map = {
779 signals.add_signoff: AddSignoff,
780 signals.amend_mode: AmendMode,
781 signals.apply_diff_selection: ApplyDiffSelection,
782 signals.apply_patches: ApplyPatches,
783 signals.branch_mode: BranchMode,
784 signals.clone: Clone,
785 signals.checkout: Checkout,
786 signals.checkout_branch: CheckoutBranch,
787 signals.cherry_pick: CherryPick,
788 signals.commit: Commit,
789 signals.delete: Delete,
790 signals.delete_branch: DeleteBranch,
791 signals.diff: Diff,
792 signals.diff_mode: DiffMode,
793 signals.diff_expr_mode: DiffExprMode,
794 signals.diff_staged: DiffStaged,
795 signals.diffstat: Diffstat,
796 signals.difftool: Difftool,
797 signals.edit: Edit,
798 signals.format_patch: FormatPatch,
799 signals.grep: GrepMode,
800 signals.load_commit_message: LoadCommitMessage,
801 signals.load_commit_template: LoadCommitTemplate,
802 signals.modified_summary: Diffstat,
803 signals.mergetool: Mergetool,
804 signals.open_repo: OpenRepo,
805 signals.rescan: Rescan,
806 signals.reset_mode: ResetMode,
807 signals.review_branch_mode: ReviewBranchMode,
808 signals.run_config_action: RunConfigAction,
809 signals.show_untracked: ShowUntracked,
810 signals.stage: Stage,
811 signals.stage_modified: StageModified,
812 signals.stage_untracked: StageUntracked,
813 signals.staged_summary: DiffStagedSummary,
814 signals.tag: Tag,
815 signals.unstage: Unstage,
816 signals.unstage_all: UnstageAll,
817 signals.unstage_selected: UnstageSelected,
818 signals.untracked_summary: UntrackedSummary,
819 signals.visualize_all: VisualizeAll,
820 signals.visualize_current: VisualizeCurrent,
821 signals.visualize_paths: VisualizePaths,
824 for signal, cmd in signal_to_command_map.iteritems():
825 _factory.add_command(signal, cmd)