models.main: Add a serialilzer to run generate_remote_helpers()
[git-cola.git] / cola / commands.py
blobc6dfdf7a32f99465be8dc76525d6300c1869ab9c
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
568 title = opts.get('title')
570 if 'prompt' not in opts or opts.get('prompt') is True:
571 prompt = i18n.gettext('Are you sure you want to run %s?') % cmd
572 opts['prompt'] = prompt
574 if opts.get('needsfile'):
575 filename = selection.filename()
576 if not filename:
577 _factory.prompt_user(signals.information,
578 'Please select a file',
579 '"%s" requires a selected file' % cmd)
580 return
581 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.expadnvars(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 cmdexpand = os.path.expandvars(cmd)
607 status = os.system(cmdexpand)
608 _notifier.broadcast(signals.log_cmd, status, 'Running: ' + cmdexpand)
609 if not opts.get('norescan'):
610 self.model.update_status()
611 return status
614 class ShowUntracked(Command):
615 """Show an untracked file."""
616 # We don't actually do anything other than set the mode right now.
617 # TODO check the mimetype for the file and handle things
618 # generically.
619 def __init__(self, filenames):
620 Command.__init__(self)
621 self.new_mode = self.model.mode_worktree
622 # TODO new_diff_text = utils.file_preview(filenames[0])
625 class Stage(Command):
626 """Stage a set of paths."""
627 def __init__(self, paths):
628 Command.__init__(self)
629 self.paths = paths
631 def do(self):
632 msg = 'Staging: %s' % (', '.join(self.paths))
633 _notifier.broadcast(signals.log_cmd, 0, msg)
634 self.model.stage_paths(self.paths)
637 class StageModified(Stage):
638 """Stage all modified files."""
639 def __init__(self):
640 Stage.__init__(self, None)
641 self.paths = self.model.modified
644 class StageUntracked(Stage):
645 """Stage all untracked files."""
646 def __init__(self):
647 Stage.__init__(self, None)
648 self.paths = self.model.untracked
650 class Tag(Command):
651 """Create a tag object."""
652 def __init__(self, name, revision, sign=False, message=''):
653 Command.__init__(self)
654 self._name = name
655 self._message = core.encode(message)
656 self._revision = revision
657 self._sign = sign
659 def do(self):
660 log_msg = 'Tagging: "%s" as "%s"' % (self._revision, self._name)
661 if self._sign:
662 log_msg += ', GPG-signed'
663 path = self.model.tmp_filename()
664 utils.write(path, self._message)
665 status, output = self.model.git.tag(self._name,
666 self._revision,
667 s=True,
668 F=path,
669 with_status=True,
670 with_stderr=True)
671 os.unlink(path)
672 else:
673 status, output = self.model.git.tag(self._name,
674 self._revision,
675 with_status=True,
676 with_stderr=True)
677 if output:
678 log_msg += '\nOutput:\n%s' % output
680 _notifier.broadcast(signals.log_cmd, status, log_msg)
681 if status == 0:
682 self.model.update_status()
685 class Unstage(Command):
686 """Unstage a set of paths."""
687 def __init__(self, paths):
688 Command.__init__(self)
689 self.paths = paths
691 def do(self):
692 msg = 'Unstaging: %s' % (', '.join(self.paths))
693 _notifier.broadcast(signals.log_cmd, 0, msg)
694 gitcmds.unstage_paths(self.paths)
695 self.model.update_status()
698 class UnstageAll(Command):
699 """Unstage all files; resets the index."""
700 def __init__(self):
701 Command.__init__(self, update=True)
703 def do(self):
704 self.model.unstage_all()
707 class UnstageSelected(Unstage):
708 """Unstage selected files."""
709 def __init__(self):
710 Unstage.__init__(self, cola.selection_model().staged)
713 class UntrackedSummary(Command):
714 """List possible .gitignore rules as the diff text."""
715 def __init__(self):
716 Command.__init__(self)
717 untracked = self.model.untracked
718 suffix = len(untracked) > 1 and 's' or ''
719 io = StringIO()
720 io.write('# %s untracked file%s\n' % (len(untracked), suffix))
721 if untracked:
722 io.write('# possible .gitignore rule%s:\n' % suffix)
723 for u in untracked:
724 io.write('/%s\n' % u)
725 self.new_diff_text = io.getvalue()
728 class VisualizeAll(Command):
729 """Visualize all branches."""
730 def do(self):
731 browser = self.model.history_browser()
732 utils.fork([browser, '--all'])
735 class VisualizeCurrent(Command):
736 """Visualize all branches."""
737 def do(self):
738 browser = self.model.history_browser()
739 utils.fork([browser, self.model.currentbranch])
742 class VisualizePaths(Command):
743 """Path-limited visualization."""
744 def __init__(self, paths):
745 Command.__init__(self)
746 browser = self.model.history_browser()
747 if paths:
748 self.argv = [browser] + paths
749 else:
750 self.argv = [browser]
752 def do(self):
753 utils.fork(self.argv)
756 def register():
758 Register signal mappings with the factory.
760 These commands are automatically created and run when
761 their corresponding signal is broadcast by the notifier.
764 signal_to_command_map = {
765 signals.add_signoff: AddSignoff,
766 signals.amend_mode: AmendMode,
767 signals.apply_diff_selection: ApplyDiffSelection,
768 signals.apply_patches: ApplyPatches,
769 signals.branch_mode: BranchMode,
770 signals.clone: Clone,
771 signals.checkout: Checkout,
772 signals.checkout_branch: CheckoutBranch,
773 signals.cherry_pick: CherryPick,
774 signals.commit: Commit,
775 signals.delete: Delete,
776 signals.delete_branch: DeleteBranch,
777 signals.diff: Diff,
778 signals.diff_mode: DiffMode,
779 signals.diff_expr_mode: DiffExprMode,
780 signals.diff_staged: DiffStaged,
781 signals.diffstat: Diffstat,
782 signals.difftool: Difftool,
783 signals.edit: Edit,
784 signals.format_patch: FormatPatch,
785 signals.grep: GrepMode,
786 signals.load_commit_message: LoadCommitMessage,
787 signals.load_commit_template: LoadCommitTemplate,
788 signals.modified_summary: Diffstat,
789 signals.mergetool: Mergetool,
790 signals.open_repo: OpenRepo,
791 signals.rescan: Rescan,
792 signals.reset_mode: ResetMode,
793 signals.review_branch_mode: ReviewBranchMode,
794 signals.run_config_action: RunConfigAction,
795 signals.show_untracked: ShowUntracked,
796 signals.stage: Stage,
797 signals.stage_modified: StageModified,
798 signals.stage_untracked: StageUntracked,
799 signals.staged_summary: DiffStagedSummary,
800 signals.tag: Tag,
801 signals.unstage: Unstage,
802 signals.unstage_all: UnstageAll,
803 signals.unstage_selected: UnstageSelected,
804 signals.untracked_summary: UntrackedSummary,
805 signals.visualize_all: VisualizeAll,
806 signals.visualize_current: VisualizeCurrent,
807 signals.visualize_paths: VisualizePaths,
810 for signal, cmd in signal_to_command_map.iteritems():
811 _factory.add_command(signal, cmd)