guicmds: Replace repobrowser with the simpler browse dialog
[git-cola.git] / cola / cmds.py
blob8d1e3de3237eb7ac4bd3092a5d31b8ee109d1098
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 '.gitignore' in gitcmds.all_files():
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(update_index=True)
603 class ReviewBranchMode(Command):
604 """Enter into review-branch mode."""
605 def __init__(self, branch):
606 Command.__init__(self)
607 self.new_mode = self.model.mode_review
608 self.new_head = gitcmds.merge_base_parent(branch)
609 self.new_diff_text = ''
611 def do(self):
612 Command.do(self)
613 self.model.update_status()
616 class RunConfigAction(Command):
617 """Run a user-configured action, typically from the "Tools" menu"""
618 def __init__(self, name):
619 Command.__init__(self)
620 self.name = name
621 self.model = cola.model()
623 def do(self):
624 for env in ('FILENAME', 'REVISION', 'ARGS'):
625 try:
626 del os.environ[env]
627 except KeyError:
628 pass
629 rev = None
630 args = None
631 opts = _config.get_guitool_opts(self.name)
632 cmd = opts.get('cmd')
633 if 'title' not in opts:
634 opts['title'] = cmd
636 if 'prompt' not in opts or opts.get('prompt') is True:
637 prompt = i18n.gettext('Are you sure you want to run %s?') % cmd
638 opts['prompt'] = prompt
640 if opts.get('needsfile'):
641 filename = selection.filename()
642 if not filename:
643 _factory.prompt_user(signals.information,
644 'Please select a file',
645 '"%s" requires a selected file' % cmd)
646 return
647 os.environ['FILENAME'] = filename
649 if opts.get('revprompt') or opts.get('argprompt'):
650 while True:
651 ok = _factory.prompt_user(signals.run_config_action, cmd, opts)
652 if not ok:
653 return
654 rev = opts.get('revision')
655 args = opts.get('args')
656 if opts.get('revprompt') and not rev:
657 msg = ('Invalid revision:\n\n'
658 'Revision expression is empty')
659 title = 'Oops!'
660 _factory.prompt_user(signals.information, title, msg)
661 continue
662 break
664 elif opts.get('confirm'):
665 title = os.path.expandvars(opts.get('title'))
666 prompt = os.path.expandvars(opts.get('prompt'))
667 if not _factory.prompt_user(signals.question, title, prompt):
668 return
669 if rev:
670 os.environ['REVISION'] = rev
671 if args:
672 os.environ['ARGS'] = args
673 title = os.path.expandvars(cmd)
674 _notifier.broadcast(signals.log_cmd, 0, 'running: ' + title)
675 cmd = ['sh', '-c', cmd]
677 if opts.get('noconsole'):
678 status, out, err = utils.run_command(cmd, flag_error=False)
679 else:
680 status, out, err = _factory.prompt_user(signals.run_command,
681 title, cmd)
683 _notifier.broadcast(signals.log_cmd, status,
684 'stdout: %s\nstatus: %s\nstderr: %s' %
685 (out.rstrip(), status, err.rstrip()))
687 if not opts.get('norescan'):
688 self.model.update_status()
689 return status
692 class SetDiffText(Command):
693 def __init__(self, text):
694 Command.__init__(self)
695 self.undoable = True
696 self.new_diff_text = text
699 class ShowUntracked(Command):
700 """Show an untracked file."""
701 # We don't actually do anything other than set the mode right now.
702 # TODO check the mimetype for the file and handle things
703 # generically.
704 def __init__(self, filenames):
705 Command.__init__(self)
706 self.new_mode = self.model.mode_worktree
707 # TODO new_diff_text = utils.file_preview(filenames[0])
710 class SignOff(Command):
711 def __init__(self):
712 Command.__init__(self)
713 self.undoable = True
714 self.old_commitmsg = self.model.commitmsg
716 def do(self):
717 signoff = self.signoff()
718 if signoff in self.model.commitmsg:
719 return
720 self.model.set_commitmsg(self.model.commitmsg + '\n' + signoff)
722 def undo(self):
723 self.model.set_commitmsg(self.old_commitmsg)
725 def signoff(self):
726 try:
727 import pwd
728 user = pwd.getpwuid(os.getuid()).pw_name
729 except ImportError:
730 user = os.getenv('USER', 'unknown')
732 name = _config.get('user.name', user)
733 email = _config.get('user.email', '%s@%s' % (user, platform.node()))
734 return '\nSigned-off-by: %s <%s>' % (name, email)
737 class Stage(Command):
738 """Stage a set of paths."""
739 def __init__(self, paths):
740 Command.__init__(self)
741 self.paths = paths
743 def do(self):
744 msg = 'Staging: %s' % (', '.join(self.paths))
745 _notifier.broadcast(signals.log_cmd, 0, msg)
746 self.model.stage_paths(self.paths)
749 class StageModified(Stage):
750 """Stage all modified files."""
751 def __init__(self):
752 Stage.__init__(self, None)
753 self.paths = self.model.modified
756 class StageUntracked(Stage):
757 """Stage all untracked files."""
758 def __init__(self):
759 Stage.__init__(self, None)
760 self.paths = self.model.untracked
763 class Tag(Command):
764 """Create a tag object."""
765 def __init__(self, name, revision, sign=False, message=''):
766 Command.__init__(self)
767 self._name = name
768 self._message = core.encode(message)
769 self._revision = revision
770 self._sign = sign
772 def do(self):
773 log_msg = 'Tagging: "%s" as "%s"' % (self._revision, self._name)
774 opts = {}
775 if self._message:
776 opts['F'] = self.model.tmp_filename()
777 utils.write(opts['F'], self._message)
779 if self._sign:
780 log_msg += ', GPG-signed'
781 opts['s'] = True
782 status, output = self.model.git.tag(self._name,
783 self._revision,
784 with_status=True,
785 with_stderr=True,
786 **opts)
787 else:
788 opts['a'] = bool(self._message)
789 status, output = self.model.git.tag(self._name,
790 self._revision,
791 with_status=True,
792 with_stderr=True,
793 **opts)
794 if 'F' in opts:
795 os.unlink(opts['F'])
797 if output:
798 log_msg += '\nOutput:\n%s' % output
800 _notifier.broadcast(signals.log_cmd, status, log_msg)
801 if status == 0:
802 self.model.update_status()
805 class Unstage(Command):
806 """Unstage a set of paths."""
807 def __init__(self, paths):
808 Command.__init__(self)
809 self.paths = paths
811 def do(self):
812 msg = 'Unstaging: %s' % (', '.join(self.paths))
813 _notifier.broadcast(signals.log_cmd, 0, msg)
814 self.model.unstage_paths(self.paths)
817 class UnstageAll(Command):
818 """Unstage all files; resets the index."""
819 def do(self):
820 self.model.unstage_all()
823 class UnstageSelected(Unstage):
824 """Unstage selected files."""
825 def __init__(self):
826 Unstage.__init__(self, cola.selection_model().staged)
829 class UntrackedSummary(Command):
830 """List possible .gitignore rules as the diff text."""
831 def __init__(self):
832 Command.__init__(self)
833 untracked = self.model.untracked
834 suffix = len(untracked) > 1 and 's' or ''
835 io = StringIO()
836 io.write('# %s untracked file%s\n' % (len(untracked), suffix))
837 if untracked:
838 io.write('# possible .gitignore rule%s:\n' % suffix)
839 for u in untracked:
840 io.write('/'+core.encode(u))
841 self.new_diff_text = core.decode(io.getvalue())
844 class UpdateStatus(Command):
845 """Update the status of a list of files."""
846 def __init__(self, files):
847 Command.__init__(self)
848 self.files = files
850 def do(self):
851 self.model.update_status_of_files(self.files)
854 class VisualizeAll(Command):
855 """Visualize all branches."""
856 def do(self):
857 browser = self.model.history_browser()
858 utils.fork([browser, '--all'])
861 class VisualizeCurrent(Command):
862 """Visualize all branches."""
863 def do(self):
864 browser = self.model.history_browser()
865 utils.fork([browser, self.model.currentbranch])
868 class VisualizePaths(Command):
869 """Path-limited visualization."""
870 def __init__(self, paths):
871 Command.__init__(self)
872 browser = self.model.history_browser()
873 if paths:
874 self.argv = [browser] + paths
875 else:
876 self.argv = [browser]
878 def do(self):
879 utils.fork(self.argv)
882 visualize_revision = 'visualize_revision'
884 class VisualizeRevision(Command):
885 """Visualize a specific revision."""
886 def __init__(self, revision, paths=None):
887 Command.__init__(self)
888 self.revision = revision
889 self.paths = paths
891 def do(self):
892 argv = [self.model.history_browser()]
893 if self.revision:
894 argv.append(self.revision)
895 if self.paths:
896 argv.append('--')
897 argv.extend(self.paths)
898 utils.fork(argv)
901 def register():
903 Register signal mappings with the factory.
905 These commands are automatically created and run when
906 their corresponding signal is broadcast by the notifier.
909 signal_to_command_map = {
910 signals.amend_mode: AmendMode,
911 signals.apply_diff_selection: ApplyDiffSelection,
912 signals.apply_patches: ApplyPatches,
913 signals.branch_mode: BranchMode,
914 signals.clone: Clone,
915 signals.checkout: Checkout,
916 signals.checkout_branch: CheckoutBranch,
917 signals.cherry_pick: CherryPick,
918 signals.commit: Commit,
919 signals.delete: Delete,
920 signals.delete_branch: DeleteBranch,
921 signals.diff: Diff,
922 signals.diff_mode: DiffMode,
923 signals.diff_expr_mode: DiffExprMode,
924 signals.diff_staged: DiffStaged,
925 signals.diffstat: Diffstat,
926 signals.difftool: Difftool,
927 signals.edit: Edit,
928 signals.format_patch: FormatPatch,
929 signals.grep: GrepMode,
930 signals.ignore: Ignore,
931 signals.load_commit_message: LoadCommitMessage,
932 signals.load_commit_template: LoadCommitTemplate,
933 signals.load_previous_message: LoadPreviousMessage,
934 signals.modified_summary: Diffstat,
935 signals.mergetool: Mergetool,
936 signals.open_repo: OpenRepo,
937 signals.rescan: Rescan,
938 signals.reset_mode: ResetMode,
939 signals.review_branch_mode: ReviewBranchMode,
940 signals.run_config_action: RunConfigAction,
941 signals.set_diff_text: SetDiffText,
942 signals.show_untracked: ShowUntracked,
943 signals.signoff: SignOff,
944 signals.stage: Stage,
945 signals.stage_modified: StageModified,
946 signals.stage_untracked: StageUntracked,
947 signals.staged_summary: DiffStagedSummary,
948 signals.tag: Tag,
949 signals.unstage: Unstage,
950 signals.unstage_all: UnstageAll,
951 signals.unstage_selected: UnstageSelected,
952 signals.untracked_summary: UntrackedSummary,
953 signals.update_status: UpdateStatus,
954 signals.visualize_all: VisualizeAll,
955 signals.visualize_current: VisualizeCurrent,
956 signals.visualize_paths: VisualizePaths,
957 signals.visualize_revision: VisualizeRevision,
960 for signal, cmd in signal_to_command_map.iteritems():
961 _factory.add_global_command(signal, cmd)