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