cmds: Add a separate RescanAndRefresh command for updating the index
[git-cola.git] / cola / cmds.py
blobdd0e00d26fbe39ea0086121133fe8ede79075417
1 import os
2 import sys
3 import platform
5 from cStringIO import StringIO
7 import cola
8 from cola import i18n
9 from cola import core
10 from cola import errors
11 from cola import gitcfg
12 from cola import gitcmds
13 from cola import utils
14 from cola import signals
15 from cola import cmdfactory
16 from cola import difftool
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 BaseCommand(object):
26 """Base class for all commands; provides the command pattern"""
27 def __init__(self):
28 self.undoable = False
30 def is_undoable(self):
31 """Can this be undone?"""
32 return self.undoable
34 def name(self):
35 """Return this command's name."""
36 return self.__class__.__name__
38 def do(self):
39 raise NotImplementedError('%s.do() is unimplemented' % self.name())
41 def undo(self):
42 raise NotImplementedError('%s.undo() is unimplemented' % self.name())
45 class Command(BaseCommand):
46 """Base class for commands that modify the main model"""
47 def __init__(self):
48 """Initialize the command and stash away values for use in do()"""
49 # These are commonly used so let's make it easier to write new commands.
50 BaseCommand.__init__(self)
51 self.model = cola.model()
53 self.old_diff_text = self.model.diff_text
54 self.old_filename = self.model.filename
55 self.old_mode = self.model.mode
56 self.old_head = self.model.head
58 self.new_diff_text = self.old_diff_text
59 self.new_filename = self.old_filename
60 self.new_head = self.old_head
61 self.new_mode = self.old_mode
63 def do(self):
64 """Perform the operation."""
65 self.model.set_diff_text(self.new_diff_text)
66 self.model.set_filename(self.new_filename)
67 self.model.set_head(self.new_head)
68 self.model.set_mode(self.new_mode)
70 def undo(self):
71 """Undo the operation."""
72 self.model.set_diff_text(self.old_diff_text)
73 self.model.set_filename(self.old_filename)
74 self.model.set_head(self.old_head)
75 self.model.set_mode(self.old_mode)
78 class AmendMode(Command):
79 """Try to amend a commit."""
80 def __init__(self, amend):
81 Command.__init__(self)
82 self.undoable = True
83 self.skip = False
84 self.amending = amend
85 self.old_commitmsg = self.model.commitmsg
87 if self.amending:
88 self.new_mode = self.model.mode_amend
89 self.new_head = 'HEAD^'
90 self.new_commitmsg = self.model.prev_commitmsg()
91 return
92 # else, amend unchecked, regular commit
93 self.new_mode = self.model.mode_none
94 self.new_head = 'HEAD'
95 self.new_diff_text = ''
96 self.new_commitmsg = self.model.commitmsg
97 # If we're going back into new-commit-mode then search the
98 # undo stack for a previous amend-commit-mode and grab the
99 # commit message at that point in time.
100 if not _factory.undostack:
101 return
102 undo_count = len(_factory.undostack)
103 for i in xrange(undo_count):
104 # Find the latest AmendMode command
105 idx = undo_count - i - 1
106 cmdobj = _factory.undostack[idx]
107 if type(cmdobj) is not AmendMode:
108 continue
109 if cmdobj.amending:
110 self.new_commitmsg = cmdobj.old_commitmsg
111 break
113 def do(self):
114 """Leave/enter amend mode."""
115 """Attempt to enter amend mode. Do not allow this when merging."""
116 if self.amending:
117 if os.path.exists(self.model.git.git_path('MERGE_HEAD')):
118 self.skip = True
119 _notifier.broadcast(signals.amend, False)
120 _factory.prompt_user(signals.information,
121 'Oops! Unmerged',
122 'You are in the middle of a merge.\n'
123 'You cannot amend while merging.')
124 return
125 self.skip = False
126 _notifier.broadcast(signals.amend, self.amending)
127 self.model.set_commitmsg(self.new_commitmsg)
128 Command.do(self)
129 self.model.update_file_status()
131 def undo(self):
132 if self.skip:
133 return
134 self.model.set_commitmsg(self.old_commitmsg)
135 Command.undo(self)
136 self.model.update_file_status()
139 class ApplyDiffSelection(Command):
140 def __init__(self, staged, selected, offset, selection, apply_to_worktree):
141 Command.__init__(self)
142 self.staged = staged
143 self.selected = selected
144 self.offset = offset
145 self.selection = selection
146 self.apply_to_worktree = apply_to_worktree
148 def do(self):
149 if self.model.mode == self.model.mode_branch:
150 # We're applying changes from a different branch!
151 parser = DiffParser(self.model,
152 filename=self.model.filename,
153 cached=False,
154 branch=self.model.head)
155 status, output = \
156 parser.process_diff_selection(self.selected,
157 self.offset,
158 self.selection,
159 apply_to_worktree=True)
160 else:
161 # The normal worktree vs index scenario
162 parser = DiffParser(self.model,
163 filename=self.model.filename,
164 cached=self.staged,
165 reverse=self.apply_to_worktree)
166 status, output = \
167 parser.process_diff_selection(self.selected,
168 self.offset,
169 self.selection,
170 apply_to_worktree=
171 self.apply_to_worktree)
172 _notifier.broadcast(signals.log_cmd, status, output)
173 # Redo the diff to show changes
174 if self.staged:
175 diffcmd = DiffStaged([self.model.filename])
176 else:
177 diffcmd = Diff([self.model.filename])
178 diffcmd.do()
179 self.model.update_file_status()
182 class ApplyPatches(Command):
183 def __init__(self, patches):
184 Command.__init__(self)
185 patches.sort()
186 self.patches = patches
188 def do(self):
189 diff_text = ''
190 num_patches = len(self.patches)
191 orig_head = self.model.git.rev_parse('HEAD')
193 for idx, patch in enumerate(self.patches):
194 status, output = self.model.git.am(patch,
195 with_status=True,
196 with_stderr=True)
197 # Log the git-am command
198 _notifier.broadcast(signals.log_cmd, status, output)
200 if num_patches > 1:
201 diff = self.model.git.diff('HEAD^!', stat=True)
202 diff_text += 'Patch %d/%d - ' % (idx+1, num_patches)
203 diff_text += '%s:\n%s\n\n' % (os.path.basename(patch), diff)
205 diff_text += 'Summary:\n'
206 diff_text += self.model.git.diff(orig_head, stat=True)
208 # Display a diffstat
209 self.model.set_diff_text(diff_text)
211 self.model.update_file_status()
213 _factory.prompt_user(signals.information,
214 'Patch(es) Applied',
215 '%d patch(es) applied:\n\n%s' %
216 (len(self.patches),
217 '\n'.join(map(os.path.basename, self.patches))))
220 class HeadChangeCommand(Command):
221 """Changes the model's current head."""
222 def __init__(self, treeish):
223 Command.__init__(self)
224 self.new_head = treeish
225 self.new_diff_text = ''
227 def do(self):
228 Command.do(self)
229 self.model.update_file_status()
232 class BranchMode(HeadChangeCommand):
233 """Enter into diff-branch mode."""
234 def __init__(self, treeish, filename):
235 HeadChangeCommand.__init__(self, treeish)
236 self.old_filename = self.model.filename
237 self.new_filename = filename
238 self.new_mode = self.model.mode_branch
239 self.new_diff_text = gitcmds.diff_helper(filename=filename,
240 cached=False,
241 reverse=True,
242 branch=treeish)
243 class Checkout(Command):
245 A command object for git-checkout.
247 'argv' is handed off directly to git.
250 def __init__(self, argv, checkout_branch=False):
251 Command.__init__(self)
252 self.argv = argv
253 self.checkout_branch = checkout_branch
254 self.new_diff_text = ''
256 def do(self):
257 status, output = self.model.git.checkout(with_stderr=True,
258 with_status=True, *self.argv)
259 _notifier.broadcast(signals.log_cmd, status, output)
260 if self.checkout_branch:
261 self.model.update_status()
262 else:
263 self.model.update_file_status()
266 class CheckoutBranch(Checkout):
267 """Checkout a branch."""
268 def __init__(self, branch, checkout_branch=True):
269 Checkout.__init__(self, [branch])
272 class CherryPick(Command):
273 """Cherry pick commits into the current branch."""
274 def __init__(self, commits):
275 Command.__init__(self)
276 self.commits = commits
278 def do(self):
279 self.model.cherry_pick_list(self.commits)
280 self.model.update_file_status()
283 class ResetMode(Command):
284 """Reset the mode and clear the model's diff text."""
285 def __init__(self):
286 Command.__init__(self)
287 self.new_mode = self.model.mode_none
288 self.new_head = 'HEAD'
289 self.new_diff_text = ''
291 def do(self):
292 Command.do(self)
293 self.model.update_file_status()
296 class Commit(ResetMode):
297 """Attempt to create a new commit."""
298 def __init__(self, amend, msg):
299 ResetMode.__init__(self)
300 self.amend = amend
301 self.msg = core.encode(msg)
302 self.old_commitmsg = self.model.commitmsg
303 self.new_commitmsg = ''
305 def do(self):
306 status, output = self.model.commit_with_msg(self.msg, amend=self.amend)
307 if status == 0:
308 ResetMode.do(self)
309 self.model.set_commitmsg(self.new_commitmsg)
310 title = 'Commit: '
311 else:
312 title = 'Commit failed: '
313 _notifier.broadcast(signals.log_cmd, status, title+output)
316 class Ignore(Command):
317 """Add files to .gitignore"""
318 def __init__(self, filenames):
319 Command.__init__(self)
320 self.filenames = filenames
322 def do(self):
323 new_additions = ''
324 for fname in self.filenames:
325 new_additions = new_additions + fname + '\n'
326 for_status = new_additions
327 if new_additions:
328 if os.path.exists('.gitignore'):
329 current_list = utils.slurp('.gitignore')
330 new_additions = new_additions + current_list
331 utils.write('.gitignore', new_additions)
332 _notifier.broadcast(signals.log_cmd,
334 'Added to .gitignore:\n%s' % for_status)
335 self.model.update_file_status()
338 class Delete(Command):
339 """Simply delete files."""
340 def __init__(self, filenames):
341 Command.__init__(self)
342 self.filenames = filenames
343 # We could git-hash-object stuff and provide undo-ability
344 # as an option. Heh.
345 def do(self):
346 rescan = False
347 for filename in self.filenames:
348 if filename:
349 try:
350 os.remove(filename)
351 rescan=True
352 except:
353 _factory.prompt_user(signals.information,
354 'Error'
355 'Deleting "%s" failed.' % filename)
356 if rescan:
357 self.model.update_file_status()
359 class DeleteBranch(Command):
360 """Delete a git branch."""
361 def __init__(self, branch):
362 Command.__init__(self)
363 self.branch = branch
365 def do(self):
366 status, output = self.model.delete_branch(self.branch)
367 title = ''
368 if output.startswith('error:'):
369 output = 'E' + output[1:]
370 else:
371 title = 'Info: '
372 _notifier.broadcast(signals.log_cmd, status, title + output)
375 class Diff(Command):
376 """Perform a diff and set the model's current text."""
377 def __init__(self, filenames, cached=False):
378 Command.__init__(self)
379 # Guard against the list of files being empty
380 if not filenames:
381 return
382 opts = {}
383 if cached:
384 cached = not self.model.read_only()
385 opts = dict(ref=self.model.head)
387 self.new_filename = filenames[0]
388 self.old_filename = self.model.filename
389 if not self.model.read_only():
390 if self.model.mode != self.model.mode_amend:
391 self.new_mode = self.model.mode_worktree
392 self.new_diff_text = gitcmds.diff_helper(filename=self.new_filename,
393 cached=cached, **opts)
396 class DiffMode(HeadChangeCommand):
397 """Enter diff mode and clear the model's diff text."""
398 def __init__(self, treeish):
399 HeadChangeCommand.__init__(self, treeish)
400 self.new_mode = self.model.mode_diff
403 class DiffExprMode(HeadChangeCommand):
404 """Enter diff-expr mode and clear the model's diff text."""
405 def __init__(self, treeish):
406 HeadChangeCommand.__init__(self, treeish)
407 self.new_mode = self.model.mode_diff_expr
410 class Diffstat(Command):
411 """Perform a diffstat and set the model's diff text."""
412 def __init__(self):
413 Command.__init__(self)
414 diff = self.model.git.diff(self.model.head,
415 unified=_config.get('diff.context', 3),
416 no_color=True,
417 M=True,
418 stat=True)
419 self.new_diff_text = core.decode(diff)
420 self.new_mode = self.model.mode_worktree
423 class DiffStaged(Diff):
424 """Perform a staged diff on a file."""
425 def __init__(self, filenames):
426 Diff.__init__(self, filenames, cached=True)
427 if not self.model.read_only():
428 if self.model.mode != self.model.mode_amend:
429 self.new_mode = self.model.mode_index
432 class DiffStagedSummary(Command):
433 def __init__(self):
434 Command.__init__(self)
435 cached = not self.model.read_only()
436 diff = self.model.git.diff(self.model.head,
437 cached=cached,
438 no_color=True,
439 patch_with_stat=True,
440 M=True)
441 self.new_diff_text = core.decode(diff)
442 if not self.model.read_only():
443 if self.model.mode != self.model.mode_amend:
444 self.new_mode = self.model.mode_index
447 class Difftool(Command):
448 """Run git-difftool limited by path."""
449 def __init__(self, staged, filenames):
450 Command.__init__(self)
451 self.staged = staged
452 self.filenames = filenames
454 def do(self):
455 if not self.filenames:
456 return
457 args = []
458 if self.staged and not self.model.read_only():
459 args.append('--cached')
460 if self.model.head != 'HEAD':
461 args.append(self.model.head)
462 args.append('--')
463 args.extend(self.filenames)
464 difftool.launch(args)
467 class Edit(Command):
468 """Edit a file using the configured gui.editor."""
469 def __init__(self, filenames, line_number=None):
470 Command.__init__(self)
471 self.filenames = filenames
472 self.line_number = line_number
474 def do(self):
475 filename = self.filenames[0]
476 if not os.path.exists(filename):
477 return
478 editor = self.model.editor()
479 if 'vi' in editor and self.line_number:
480 utils.fork([editor, filename, '+'+self.line_number])
481 else:
482 utils.fork([editor, filename])
485 class FormatPatch(Command):
486 """Output a patch series given all revisions and a selected subset."""
487 def __init__(self, to_export, revs):
488 Command.__init__(self)
489 self.to_export = to_export
490 self.revs = revs
492 def do(self):
493 status, output = gitcmds.format_patchsets(self.to_export, self.revs)
494 _notifier.broadcast(signals.log_cmd, status, output)
497 class GrepMode(Command):
498 def __init__(self, txt):
499 """Perform a git-grep."""
500 Command.__init__(self)
501 self.new_mode = self.model.mode_grep
502 self.new_diff_text = core.decode(self.model.git.grep(txt, n=True))
505 class LoadCommitMessage(Command):
506 """Loads a commit message from a path."""
507 def __init__(self, path):
508 Command.__init__(self)
509 self.undoable = True
510 self.path = path
511 self.old_commitmsg = self.model.commitmsg
512 self.old_directory = self.model.directory
514 def do(self):
515 path = self.path
516 if not path or not os.path.isfile(path):
517 raise errors.UsageError('Error: cannot find commit template',
518 '%s: No such file or directory.' % path)
519 self.model.set_directory(os.path.dirname(path))
520 self.model.set_commitmsg(utils.slurp(path))
522 def undo(self):
523 self.model.set_commitmsg(self.old_commitmsg)
524 self.model.set_directory(self.old_directory)
527 class LoadCommitTemplate(LoadCommitMessage):
528 """Loads the commit message template specified by commit.template."""
529 def __init__(self):
530 LoadCommitMessage.__init__(self, _config.get('commit.template'))
532 def do(self):
533 if self.path is None:
534 raise errors.UsageError('Error: unconfigured commit template',
535 'A commit template has not been configured.\n'
536 'Use "git config" to define "commit.template"\n'
537 'so that it points to a commit template.')
538 return LoadCommitMessage.do(self)
541 class LoadPreviousMessage(Command):
542 """Try to amend a commit."""
543 def __init__(self, sha1):
544 Command.__init__(self)
545 self.sha1 = sha1
546 self.old_commitmsg = self.model.commitmsg
547 self.new_commitmsg = self.model.prev_commitmsg(sha1)
548 self.undoable = True
550 def do(self):
551 self.model.set_commitmsg(self.new_commitmsg)
553 def undo(self):
554 self.model.set_commitmsg(self.old_commitmsg)
557 class Mergetool(Command):
558 """Launch git-mergetool on a list of paths."""
559 def __init__(self, paths):
560 Command.__init__(self)
561 self.paths = paths
563 def do(self):
564 if not self.paths:
565 return
566 utils.fork(['git', 'mergetool', '--no-prompt', '--'] + self.paths)
569 class OpenRepo(Command):
570 """Launches git-cola on a repo."""
571 def __init__(self, dirname):
572 Command.__init__(self)
573 self.new_directory = dirname
575 def do(self):
576 self.model.set_directory(self.new_directory)
577 utils.fork([sys.executable, sys.argv[0], '--repo', self.new_directory])
580 class Clone(Command):
581 """Clones a repository and optionally spawns a new cola session."""
582 def __init__(self, url, destdir, spawn=True):
583 Command.__init__(self)
584 self.url = url
585 self.new_directory = destdir
586 self.spawn = spawn
588 def do(self):
589 self.model.git.clone(self.url, self.new_directory,
590 with_stderr=True, with_status=True)
591 if self.spawn:
592 utils.fork(['python', sys.argv[0], '--repo', self.new_directory])
595 rescan = 'rescan'
597 class Rescan(Command):
598 """Rescans for changes."""
599 def do(self):
600 self.model.update_status()
603 rescan_and_refresh = 'rescan_and_refresh'
605 class RescanAndRefresh(Command):
606 """Rescans for changes."""
607 def do(self):
608 self.model.update_status(update_index=True)
611 class ReviewBranchMode(Command):
612 """Enter into review-branch mode."""
613 def __init__(self, branch):
614 Command.__init__(self)
615 self.new_mode = self.model.mode_review
616 self.new_head = gitcmds.merge_base_parent(branch)
617 self.new_diff_text = ''
619 def do(self):
620 Command.do(self)
621 self.model.update_status()
624 class RunConfigAction(Command):
625 """Run a user-configured action, typically from the "Tools" menu"""
626 def __init__(self, name):
627 Command.__init__(self)
628 self.name = name
629 self.model = cola.model()
631 def do(self):
632 for env in ('FILENAME', 'REVISION', 'ARGS'):
633 try:
634 del os.environ[env]
635 except KeyError:
636 pass
637 rev = None
638 args = None
639 opts = _config.get_guitool_opts(self.name)
640 cmd = opts.get('cmd')
641 if 'title' not in opts:
642 opts['title'] = cmd
644 if 'prompt' not in opts or opts.get('prompt') is True:
645 prompt = i18n.gettext('Are you sure you want to run %s?') % cmd
646 opts['prompt'] = prompt
648 if opts.get('needsfile'):
649 filename = selection.filename()
650 if not filename:
651 _factory.prompt_user(signals.information,
652 'Please select a file',
653 '"%s" requires a selected file' % cmd)
654 return
655 os.environ['FILENAME'] = filename
657 if opts.get('revprompt') or opts.get('argprompt'):
658 while True:
659 ok = _factory.prompt_user(signals.run_config_action, cmd, opts)
660 if not ok:
661 return
662 rev = opts.get('revision')
663 args = opts.get('args')
664 if opts.get('revprompt') and not rev:
665 title = 'Invalid Revision'
666 msg = 'The revision expression cannot be empty.'
667 _factory.prompt_user(signals.critical, title, msg)
668 continue
669 break
671 elif opts.get('confirm'):
672 title = os.path.expandvars(opts.get('title'))
673 prompt = os.path.expandvars(opts.get('prompt'))
674 if not _factory.prompt_user(signals.question, title, prompt):
675 return
676 if rev:
677 os.environ['REVISION'] = rev
678 if args:
679 os.environ['ARGS'] = args
680 title = os.path.expandvars(cmd)
681 _notifier.broadcast(signals.log_cmd, 0, 'running: ' + title)
682 cmd = ['sh', '-c', cmd]
684 if opts.get('noconsole'):
685 status, out, err = utils.run_command(cmd, flag_error=False)
686 else:
687 status, out, err = _factory.prompt_user(signals.run_command,
688 title, cmd)
690 _notifier.broadcast(signals.log_cmd, status,
691 'stdout: %s\nstatus: %s\nstderr: %s' %
692 (out.rstrip(), status, err.rstrip()))
694 if not opts.get('norescan'):
695 self.model.update_status()
696 return status
699 class SetDiffText(Command):
700 def __init__(self, text):
701 Command.__init__(self)
702 self.undoable = True
703 self.new_diff_text = text
706 class ShowUntracked(Command):
707 """Show an untracked file."""
708 # We don't actually do anything other than set the mode right now.
709 # TODO check the mimetype for the file and handle things
710 # generically.
711 def __init__(self, filenames):
712 Command.__init__(self)
713 self.new_mode = self.model.mode_worktree
714 # TODO new_diff_text = utils.file_preview(filenames[0])
717 class SignOff(Command):
718 def __init__(self):
719 Command.__init__(self)
720 self.undoable = True
721 self.old_commitmsg = self.model.commitmsg
723 def do(self):
724 signoff = self.signoff()
725 if signoff in self.model.commitmsg:
726 return
727 self.model.set_commitmsg(self.model.commitmsg + '\n' + signoff)
729 def undo(self):
730 self.model.set_commitmsg(self.old_commitmsg)
732 def signoff(self):
733 try:
734 import pwd
735 user = pwd.getpwuid(os.getuid()).pw_name
736 except ImportError:
737 user = os.getenv('USER', 'unknown')
739 name = _config.get('user.name', user)
740 email = _config.get('user.email', '%s@%s' % (user, platform.node()))
741 return '\nSigned-off-by: %s <%s>' % (name, email)
744 class Stage(Command):
745 """Stage a set of paths."""
746 def __init__(self, paths):
747 Command.__init__(self)
748 self.paths = paths
750 def do(self):
751 msg = 'Staging: %s' % (', '.join(self.paths))
752 _notifier.broadcast(signals.log_cmd, 0, msg)
753 self.model.stage_paths(self.paths)
756 class StageModified(Stage):
757 """Stage all modified files."""
758 def __init__(self):
759 Stage.__init__(self, None)
760 self.paths = self.model.modified
763 class StageUntracked(Stage):
764 """Stage all untracked files."""
765 def __init__(self):
766 Stage.__init__(self, None)
767 self.paths = self.model.untracked
770 class Tag(Command):
771 """Create a tag object."""
772 def __init__(self, name, revision, sign=False, message=''):
773 Command.__init__(self)
774 self._name = name
775 self._message = core.encode(message)
776 self._revision = revision
777 self._sign = sign
779 def do(self):
780 log_msg = 'Tagging: "%s" as "%s"' % (self._revision, self._name)
781 opts = {}
782 if self._message:
783 opts['F'] = self.model.tmp_filename()
784 utils.write(opts['F'], self._message)
786 if self._sign:
787 log_msg += ', GPG-signed'
788 opts['s'] = True
789 status, output = self.model.git.tag(self._name,
790 self._revision,
791 with_status=True,
792 with_stderr=True,
793 **opts)
794 else:
795 opts['a'] = bool(self._message)
796 status, output = self.model.git.tag(self._name,
797 self._revision,
798 with_status=True,
799 with_stderr=True,
800 **opts)
801 if 'F' in opts:
802 os.unlink(opts['F'])
804 if output:
805 log_msg += '\nOutput:\n%s' % output
807 _notifier.broadcast(signals.log_cmd, status, log_msg)
808 if status == 0:
809 self.model.update_status()
812 class Unstage(Command):
813 """Unstage a set of paths."""
814 def __init__(self, paths):
815 Command.__init__(self)
816 self.paths = paths
818 def do(self):
819 msg = 'Unstaging: %s' % (', '.join(self.paths))
820 _notifier.broadcast(signals.log_cmd, 0, msg)
821 self.model.unstage_paths(self.paths)
824 class UnstageAll(Command):
825 """Unstage all files; resets the index."""
826 def do(self):
827 self.model.unstage_all()
830 class UnstageSelected(Unstage):
831 """Unstage selected files."""
832 def __init__(self):
833 Unstage.__init__(self, cola.selection_model().staged)
836 class UntrackedSummary(Command):
837 """List possible .gitignore rules as the diff text."""
838 def __init__(self):
839 Command.__init__(self)
840 untracked = self.model.untracked
841 suffix = len(untracked) > 1 and 's' or ''
842 io = StringIO()
843 io.write('# %s untracked file%s\n' % (len(untracked), suffix))
844 if untracked:
845 io.write('# possible .gitignore rule%s:\n' % suffix)
846 for u in untracked:
847 io.write('/'+core.encode(u))
848 self.new_diff_text = core.decode(io.getvalue())
851 class UpdateStatus(Command):
852 """Update the status of a list of files."""
853 def __init__(self, files):
854 Command.__init__(self)
855 self.files = files
857 def do(self):
858 self.model.update_status_of_files(self.files)
861 class VisualizeAll(Command):
862 """Visualize all branches."""
863 def do(self):
864 browser = self.model.history_browser()
865 utils.fork([browser, '--all'])
868 class VisualizeCurrent(Command):
869 """Visualize all branches."""
870 def do(self):
871 browser = self.model.history_browser()
872 utils.fork([browser, self.model.currentbranch])
875 class VisualizePaths(Command):
876 """Path-limited visualization."""
877 def __init__(self, paths):
878 Command.__init__(self)
879 browser = self.model.history_browser()
880 if paths:
881 self.argv = [browser] + paths
882 else:
883 self.argv = [browser]
885 def do(self):
886 utils.fork(self.argv)
889 visualize_revision = 'visualize_revision'
891 class VisualizeRevision(Command):
892 """Visualize a specific revision."""
893 def __init__(self, revision, paths=None):
894 Command.__init__(self)
895 self.revision = revision
896 self.paths = paths
898 def do(self):
899 argv = [self.model.history_browser()]
900 if self.revision:
901 argv.append(self.revision)
902 if self.paths:
903 argv.append('--')
904 argv.extend(self.paths)
905 utils.fork(argv)
908 def register():
910 Register signal mappings with the factory.
912 These commands are automatically created and run when
913 their corresponding signal is broadcast by the notifier.
916 signal_to_command_map = {
917 signals.amend_mode: AmendMode,
918 signals.apply_diff_selection: ApplyDiffSelection,
919 signals.apply_patches: ApplyPatches,
920 signals.branch_mode: BranchMode,
921 signals.clone: Clone,
922 signals.checkout: Checkout,
923 signals.checkout_branch: CheckoutBranch,
924 signals.cherry_pick: CherryPick,
925 signals.commit: Commit,
926 signals.delete: Delete,
927 signals.delete_branch: DeleteBranch,
928 signals.diff: Diff,
929 signals.diff_mode: DiffMode,
930 signals.diff_expr_mode: DiffExprMode,
931 signals.diff_staged: DiffStaged,
932 signals.diffstat: Diffstat,
933 signals.difftool: Difftool,
934 signals.edit: Edit,
935 signals.format_patch: FormatPatch,
936 signals.grep: GrepMode,
937 signals.ignore: Ignore,
938 signals.load_commit_message: LoadCommitMessage,
939 signals.load_commit_template: LoadCommitTemplate,
940 signals.load_previous_message: LoadPreviousMessage,
941 signals.modified_summary: Diffstat,
942 signals.mergetool: Mergetool,
943 signals.open_repo: OpenRepo,
944 signals.rescan: Rescan,
945 signals.rescan_and_refresh: RescanAndRefresh,
946 signals.reset_mode: ResetMode,
947 signals.review_branch_mode: ReviewBranchMode,
948 signals.run_config_action: RunConfigAction,
949 signals.set_diff_text: SetDiffText,
950 signals.show_untracked: ShowUntracked,
951 signals.signoff: SignOff,
952 signals.stage: Stage,
953 signals.stage_modified: StageModified,
954 signals.stage_untracked: StageUntracked,
955 signals.staged_summary: DiffStagedSummary,
956 signals.tag: Tag,
957 signals.unstage: Unstage,
958 signals.unstage_all: UnstageAll,
959 signals.unstage_selected: UnstageSelected,
960 signals.untracked_summary: UntrackedSummary,
961 signals.update_status: UpdateStatus,
962 signals.visualize_all: VisualizeAll,
963 signals.visualize_current: VisualizeCurrent,
964 signals.visualize_paths: VisualizePaths,
965 signals.visualize_revision: VisualizeRevision,
968 for signal, cmd in signal_to_command_map.iteritems():
969 _factory.add_global_command(signal, cmd)