views.dag: Micro-optimize Node type(), shape(), boundingRect(), and glyph()
[git-cola.git] / cola / commands.py
blob8cf1a09597a96de1467e36f23237ad7e4f13d480
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 if self.model.head != 'HEAD':
425 args.append(self.model.head)
426 args.append('--')
427 args.extend(self.filenames)
428 difftool.launch(args)
431 class Edit(Command):
432 """Edit a file using the configured gui.editor."""
433 def __init__(self, filenames, line_number=None):
434 Command.__init__(self)
435 self.filenames = filenames
436 self.line_number = line_number
438 def do(self):
439 filename = self.filenames[0]
440 if not os.path.exists(filename):
441 return
442 editor = self.model.editor()
443 if 'vi' in editor and self.line_number:
444 utils.fork([editor, filename, '+'+self.line_number])
445 else:
446 utils.fork([editor, filename])
449 class FormatPatch(Command):
450 """Output a patch series given all revisions and a selected subset."""
451 def __init__(self, to_export, revs):
452 Command.__init__(self)
453 self.to_export = to_export
454 self.revs = revs
456 def do(self):
457 status, output = gitcmds.format_patchsets(self.to_export, self.revs)
458 _notifier.broadcast(signals.log_cmd, status, output)
461 class GrepMode(Command):
462 def __init__(self, txt):
463 """Perform a git-grep."""
464 Command.__init__(self)
465 self.new_mode = self.model.mode_grep
466 self.new_diff_text = self.model.git.grep(txt, n=True)
468 class LoadCommitMessage(Command):
469 """Loads a commit message from a path."""
470 def __init__(self, path):
471 Command.__init__(self)
472 if not path or not os.path.exists(path):
473 raise OSError('error: "%s" does not exist' % path)
474 self.undoable = True
475 self.path = path
476 self.old_commitmsg = self.model.commitmsg
477 self.old_directory = self.model.directory
479 def do(self):
480 self.model.set_directory(os.path.dirname(self.path))
481 self.model.set_commitmsg(utils.slurp(self.path))
483 def undo(self):
484 self.model.set_commitmsg(self.old_commitmsg)
485 self.model.set_directory(self.old_directory)
488 class LoadCommitTemplate(LoadCommitMessage):
489 """Loads the commit message template specified by commit.template."""
490 def __init__(self):
491 LoadCommitMessage.__init__(self, _config.get('commit.template'))
494 class Mergetool(Command):
495 """Launch git-mergetool on a list of paths."""
496 def __init__(self, paths):
497 Command.__init__(self)
498 self.paths = paths
500 def do(self):
501 if not self.paths:
502 return
503 if version.check('mergetool-no-prompt',
504 self.model.git.version().split()[-1]):
505 utils.fork(['git', 'mergetool', '--no-prompt', '--'] + self.paths)
506 else:
507 utils.fork(['xterm', '-e', 'git', 'mergetool', '--'] + self.paths)
510 class OpenRepo(Command):
511 """Launches git-cola on a repo."""
512 def __init__(self, dirname):
513 Command.__init__(self)
514 self.new_directory = utils.quote_repopath(dirname)
516 def do(self):
517 self.model.set_directory(self.new_directory)
518 utils.fork(['python', sys.argv[0], '--repo', self.new_directory])
521 class Clone(Command):
522 """Clones a repository and optionally spawns a new cola session."""
523 def __init__(self, url, destdir, spawn=True):
524 Command.__init__(self)
525 self.url = url
526 self.new_directory = utils.quote_repopath(destdir)
527 self.spawn = spawn
529 def do(self):
530 self.model.git.clone(self.url, self.new_directory,
531 with_stderr=True, with_status=True)
532 if self.spawn:
533 utils.fork(['python', sys.argv[0], '--repo', self.new_directory])
536 class Rescan(Command):
537 """Rescans for changes."""
538 def __init__(self):
539 Command.__init__(self, update=True)
542 class ReviewBranchMode(Command):
543 """Enter into review-branch mode."""
544 def __init__(self, branch):
545 Command.__init__(self, update=True)
546 self.new_mode = self.model.mode_review
547 self.new_head = gitcmds.merge_base_parent(branch)
548 self.new_diff_text = ''
551 class RunConfigAction(Command):
552 """Run a user-configured action, typically from the "Tools" menu"""
553 def __init__(self, name):
554 Command.__init__(self)
555 self.name = name
556 self.model = cola.model()
558 def do(self):
559 for env in ('FILENAME', 'REVISION', 'ARGS'):
560 try:
561 del os.environ[env]
562 except KeyError:
563 pass
564 rev = None
565 args = None
566 opts = _config.get_guitool_opts(self.name)
567 cmd = opts.get('cmd')
568 if 'title' not in opts:
569 opts['title'] = cmd
571 if 'prompt' not in opts or opts.get('prompt') is True:
572 prompt = i18n.gettext('Are you sure you want to run %s?') % cmd
573 opts['prompt'] = prompt
575 if opts.get('needsfile'):
576 filename = selection.filename()
577 if not filename:
578 _factory.prompt_user(signals.information,
579 'Please select a file',
580 '"%s" requires a selected file' % cmd)
581 return
582 os.environ['FILENAME'] = utils.shell_quote(filename)
585 if opts.get('revprompt') or opts.get('argprompt'):
586 while True:
587 ok = _factory.prompt_user(signals.run_config_action, cmd, opts)
588 if not ok:
589 return
590 rev = opts.get('revision')
591 args = opts.get('args')
592 if opts.get('revprompt') and not rev:
593 msg = ('Invalid revision:\n\n'
594 'Revision expression is empty')
595 title = 'Oops!'
596 _factory.prompt_user(signals.information, title, msg)
597 continue
598 break
600 elif opts.get('confirm'):
601 title = os.path.expandvars(opts.get('title'))
602 prompt = os.path.expandvars(opts.get('prompt'))
603 if not _factory.prompt_user(signals.question, title, prompt):
604 return
605 if rev:
606 os.environ['REVISION'] = rev
607 if args:
608 os.environ['ARGS'] = args
609 title = os.path.expandvars(cmd)
610 _notifier.broadcast(signals.log_cmd, 0, 'running: ' + title)
611 cmd = ['sh', '-c', cmd]
613 if opts.get('noconsole'):
614 status, out, err = utils.run_command(cmd, flag_error=False)
615 else:
616 status, out, err = _factory.prompt_user(signals.run_command,
617 title, cmd)
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 self.model.unstage_paths(self.paths)
711 class UnstageAll(Command):
712 """Unstage all files; resets the index."""
713 def __init__(self):
714 Command.__init__(self, update=True)
716 def do(self):
717 self.model.unstage_all()
720 class UnstageSelected(Unstage):
721 """Unstage selected files."""
722 def __init__(self):
723 Unstage.__init__(self, cola.selection_model().staged)
726 class UntrackedSummary(Command):
727 """List possible .gitignore rules as the diff text."""
728 def __init__(self):
729 Command.__init__(self)
730 untracked = self.model.untracked
731 suffix = len(untracked) > 1 and 's' or ''
732 io = StringIO()
733 io.write('# %s untracked file%s\n' % (len(untracked), suffix))
734 if untracked:
735 io.write('# possible .gitignore rule%s:\n' % suffix)
736 for u in untracked:
737 io.write('/%s\n' % u)
738 self.new_diff_text = io.getvalue()
741 class VisualizeAll(Command):
742 """Visualize all branches."""
743 def do(self):
744 browser = self.model.history_browser()
745 utils.fork([browser, '--all'])
748 class VisualizeCurrent(Command):
749 """Visualize all branches."""
750 def do(self):
751 browser = self.model.history_browser()
752 utils.fork([browser, self.model.currentbranch])
755 class VisualizePaths(Command):
756 """Path-limited visualization."""
757 def __init__(self, paths):
758 Command.__init__(self)
759 browser = self.model.history_browser()
760 if paths:
761 self.argv = [browser] + paths
762 else:
763 self.argv = [browser]
765 def do(self):
766 utils.fork(self.argv)
769 def register():
771 Register signal mappings with the factory.
773 These commands are automatically created and run when
774 their corresponding signal is broadcast by the notifier.
777 signal_to_command_map = {
778 signals.add_signoff: AddSignoff,
779 signals.amend_mode: AmendMode,
780 signals.apply_diff_selection: ApplyDiffSelection,
781 signals.apply_patches: ApplyPatches,
782 signals.branch_mode: BranchMode,
783 signals.clone: Clone,
784 signals.checkout: Checkout,
785 signals.checkout_branch: CheckoutBranch,
786 signals.cherry_pick: CherryPick,
787 signals.commit: Commit,
788 signals.delete: Delete,
789 signals.delete_branch: DeleteBranch,
790 signals.diff: Diff,
791 signals.diff_mode: DiffMode,
792 signals.diff_expr_mode: DiffExprMode,
793 signals.diff_staged: DiffStaged,
794 signals.diffstat: Diffstat,
795 signals.difftool: Difftool,
796 signals.edit: Edit,
797 signals.format_patch: FormatPatch,
798 signals.grep: GrepMode,
799 signals.load_commit_message: LoadCommitMessage,
800 signals.load_commit_template: LoadCommitTemplate,
801 signals.modified_summary: Diffstat,
802 signals.mergetool: Mergetool,
803 signals.open_repo: OpenRepo,
804 signals.rescan: Rescan,
805 signals.reset_mode: ResetMode,
806 signals.review_branch_mode: ReviewBranchMode,
807 signals.run_config_action: RunConfigAction,
808 signals.show_untracked: ShowUntracked,
809 signals.stage: Stage,
810 signals.stage_modified: StageModified,
811 signals.stage_untracked: StageUntracked,
812 signals.staged_summary: DiffStagedSummary,
813 signals.tag: Tag,
814 signals.unstage: Unstage,
815 signals.unstage_all: UnstageAll,
816 signals.unstage_selected: UnstageSelected,
817 signals.untracked_summary: UntrackedSummary,
818 signals.visualize_all: VisualizeAll,
819 signals.visualize_current: VisualizeCurrent,
820 signals.visualize_paths: VisualizePaths,
823 for signal, cmd in signal_to_command_map.iteritems():
824 _factory.add_command(signal, cmd)