4 from fnmatch
import fnmatch
6 from cStringIO
import StringIO
9 from cola
import compat
11 from cola
import errors
12 from cola
import gitcfg
13 from cola
import gitcmds
14 from cola
import utils
15 from cola
import difftool
16 from cola
import resources
17 from cola
.compat
import set
18 from cola
.diffparse
import DiffParser
19 from cola
.git
import STDOUT
20 from cola
.i18n
import N_
21 from cola
.interaction
import Interaction
22 from cola
.models
import prefs
23 from cola
.models
import selection
25 _notifier
= cola
.notifier()
26 _config
= gitcfg
.instance()
29 class BaseCommand(object):
30 """Base class for all commands; provides the command pattern"""
37 def is_undoable(self
):
38 """Can this be undone?"""
46 raise NotImplementedError('%s.do() is unimplemented' % self
.__class
__.__name
__)
49 raise NotImplementedError('%s.undo() is unimplemented' % self
.__class
__.__name
__)
52 class Command(BaseCommand
):
53 """Base class for commands that modify the main model"""
56 """Initialize the command and stash away values for use in do()"""
57 # These are commonly used so let's make it easier to write new commands.
58 BaseCommand
.__init
__(self
)
59 self
.model
= cola
.model()
61 self
.old_diff_text
= self
.model
.diff_text
62 self
.old_filename
= self
.model
.filename
63 self
.old_mode
= self
.model
.mode
64 self
.old_head
= self
.model
.head
66 self
.new_diff_text
= self
.old_diff_text
67 self
.new_filename
= self
.old_filename
68 self
.new_head
= self
.old_head
69 self
.new_mode
= self
.old_mode
72 """Perform the operation."""
73 self
.model
.set_filename(self
.new_filename
)
74 self
.model
.set_head(self
.new_head
)
75 self
.model
.set_mode(self
.new_mode
)
76 self
.model
.set_diff_text(self
.new_diff_text
)
79 """Undo the operation."""
80 self
.model
.set_diff_text(self
.old_diff_text
)
81 self
.model
.set_filename(self
.old_filename
)
82 self
.model
.set_head(self
.old_head
)
83 self
.model
.set_mode(self
.old_mode
)
86 class AmendMode(Command
):
87 """Try to amend a commit."""
97 def __init__(self
, amend
):
98 Command
.__init
__(self
)
101 self
.amending
= amend
102 self
.old_commitmsg
= self
.model
.commitmsg
105 self
.new_mode
= self
.model
.mode_amend
106 self
.new_head
= 'HEAD^'
107 self
.new_commitmsg
= self
.model
.prev_commitmsg()
108 AmendMode
.LAST_MESSAGE
= self
.model
.commitmsg
110 # else, amend unchecked, regular commit
111 self
.new_mode
= self
.model
.mode_none
112 self
.new_head
= 'HEAD'
113 self
.new_diff_text
= ''
114 self
.new_commitmsg
= self
.model
.commitmsg
115 # If we're going back into new-commit-mode then search the
116 # undo stack for a previous amend-commit-mode and grab the
117 # commit message at that point in time.
118 if AmendMode
.LAST_MESSAGE
is not None:
119 self
.new_commitmsg
= AmendMode
.LAST_MESSAGE
120 AmendMode
.LAST_MESSAGE
= None
123 """Leave/enter amend mode."""
124 """Attempt to enter amend mode. Do not allow this when merging."""
126 if core
.exists(self
.model
.git
.git_path('MERGE_HEAD')):
128 _notifier
.broadcast(_notifier
.AMEND
, False)
129 Interaction
.information(
131 N_('You are in the middle of a merge.\n'
132 'Cannot amend while merging.'))
135 _notifier
.broadcast(_notifier
.AMEND
, self
.amending
)
136 self
.model
.set_commitmsg(self
.new_commitmsg
)
138 self
.model
.update_file_status()
143 self
.model
.set_commitmsg(self
.old_commitmsg
)
145 self
.model
.update_file_status()
148 class ApplyDiffSelection(Command
):
150 def __init__(self
, staged
, selected
, offset
, selection
, apply_to_worktree
):
151 Command
.__init
__(self
)
153 self
.selected
= selected
155 self
.selection
= selection
156 self
.apply_to_worktree
= apply_to_worktree
159 # The normal worktree vs index scenario
160 parser
= DiffParser(self
.model
,
161 filename
=self
.model
.filename
,
163 reverse
=self
.apply_to_worktree
)
165 parser
.process_diff_selection(self
.selected
,
168 apply_to_worktree
=self
.apply_to_worktree
)
169 Interaction
.log_status(status
, out
, err
)
170 self
.model
.update_file_status(update_index
=True)
173 class ApplyPatches(Command
):
175 def __init__(self
, patches
):
176 Command
.__init
__(self
)
178 self
.patches
= patches
182 num_patches
= len(self
.patches
)
183 orig_head
= self
.model
.git
.rev_parse('HEAD')[STDOUT
]
185 for idx
, patch
in enumerate(self
.patches
):
186 status
, out
, err
= self
.model
.git
.am(patch
)
187 # Log the git-am command
188 Interaction
.log_status(status
, out
, err
)
191 diff
= self
.model
.git
.diff('HEAD^!', stat
=True)[STDOUT
]
192 diff_text
+= (N_('PATCH %(current)d/%(count)d') %
193 dict(current
=idx
+1, count
=num_patches
))
194 diff_text
+= ' - %s:\n%s\n\n' % (os
.path
.basename(patch
), diff
)
196 diff_text
+= N_('Summary:') + '\n'
197 diff_text
+= self
.model
.git
.diff(orig_head
, stat
=True)[STDOUT
]
200 self
.model
.set_diff_text(diff_text
)
201 self
.model
.update_file_status()
203 basenames
= '\n'.join([os
.path
.basename(p
) for p
in self
.patches
])
204 Interaction
.information(
205 N_('Patch(es) Applied'),
206 (N_('%d patch(es) applied.') + '\n\n%s') %
207 (len(self
.patches
), basenames
))
210 class Archive(BaseCommand
):
212 def __init__(self
, ref
, fmt
, prefix
, filename
):
213 BaseCommand
.__init
__(self
)
217 self
.filename
= filename
220 fp
= core
.xopen(self
.filename
, 'wb')
221 cmd
= ['git', 'archive', '--format='+self
.fmt
]
222 if self
.fmt
in ('tgz', 'tar.gz'):
225 cmd
.append('--prefix=' + self
.prefix
)
227 proc
= core
.start_command(cmd
, stdout
=fp
)
228 out
, err
= proc
.communicate()
230 status
= proc
.returncode
231 Interaction
.log_status(status
, out
or '', err
or '')
234 class Checkout(Command
):
236 A command object for git-checkout.
238 'argv' is handed off directly to git.
242 def __init__(self
, argv
, checkout_branch
=False):
243 Command
.__init
__(self
)
245 self
.checkout_branch
= checkout_branch
246 self
.new_diff_text
= ''
249 status
, out
, err
= self
.model
.git
.checkout(*self
.argv
)
250 Interaction
.log_status(status
, out
, err
)
251 if self
.checkout_branch
:
252 self
.model
.update_status()
254 self
.model
.update_file_status()
257 class CheckoutBranch(Checkout
):
258 """Checkout a branch."""
260 def __init__(self
, branch
):
262 Checkout
.__init
__(self
, args
, checkout_branch
=True)
265 class CherryPick(Command
):
266 """Cherry pick commits into the current branch."""
268 def __init__(self
, commits
):
269 Command
.__init
__(self
)
270 self
.commits
= commits
273 self
.model
.cherry_pick_list(self
.commits
)
274 self
.model
.update_file_status()
277 class ResetMode(Command
):
278 """Reset the mode and clear the model's diff text."""
281 Command
.__init
__(self
)
282 self
.new_mode
= self
.model
.mode_none
283 self
.new_head
= 'HEAD'
284 self
.new_diff_text
= ''
288 self
.model
.update_file_status()
291 class Commit(ResetMode
):
292 """Attempt to create a new commit."""
294 SHORTCUT
= 'Ctrl+Return'
296 def __init__(self
, amend
, msg
):
297 ResetMode
.__init
__(self
)
300 self
.old_commitmsg
= self
.model
.commitmsg
301 self
.new_commitmsg
= ''
304 tmpfile
= utils
.tmp_filename('commit-message')
305 status
, out
, err
= self
.model
.commit_with_msg(self
.msg
, tmpfile
,
309 self
.model
.set_commitmsg(self
.new_commitmsg
)
310 msg
= N_('Created commit: %s') % out
312 msg
= N_('Commit failed: %s') % out
313 Interaction
.log_status(status
, msg
, err
)
315 return status
, out
, err
318 class Ignore(Command
):
319 """Add files to .gitignore"""
321 def __init__(self
, filenames
):
322 Command
.__init
__(self
)
323 self
.filenames
= filenames
327 for fname
in self
.filenames
:
328 new_additions
= new_additions
+ fname
+ '\n'
329 for_status
= new_additions
331 if core
.exists('.gitignore'):
332 current_list
= core
.read('.gitignore')
333 new_additions
= new_additions
+ current_list
334 core
.write('.gitignore', new_additions
)
335 Interaction
.log_status(
336 0, 'Added to .gitignore:\n%s' % for_status
, '')
337 self
.model
.update_file_status()
340 class Delete(Command
):
343 def __init__(self
, filenames
):
344 Command
.__init
__(self
)
345 self
.filenames
= filenames
346 # We could git-hash-object stuff and provide undo-ability
350 for filename
in self
.filenames
:
356 Interaction
.information(
358 N_('Deleting "%s" failed') % filename
)
360 self
.model
.update_file_status()
363 class DeleteBranch(Command
):
364 """Delete a git branch."""
366 def __init__(self
, branch
):
367 Command
.__init
__(self
)
371 status
, out
, err
= self
.model
.delete_branch(self
.branch
)
372 Interaction
.log_status(status
, out
, err
)
375 class DeleteRemoteBranch(Command
):
376 """Delete a remote git branch."""
378 def __init__(self
, remote
, branch
):
379 Command
.__init
__(self
)
384 status
, out
, err
= self
.model
.git
.push(self
.remote
, self
.branch
,
386 Interaction
.log_status(status
, out
, err
)
387 self
.model
.update_status()
390 Interaction
.information(
391 N_('Remote Branch Deleted'),
392 N_('"%(branch)s" has been deleted from "%(remote)s".')
393 % dict(branch
=self
.branch
, remote
=self
.remote
))
396 message
= (N_('"%(command)s" returned exit status %(status)d') %
397 dict(command
=command
, status
=status
))
399 Interaction
.critical(N_('Error Deleting Remote Branch'),
405 """Perform a diff and set the model's current text."""
407 def __init__(self
, filenames
, cached
=False):
408 Command
.__init
__(self
)
409 # Guard against the list of files being empty
414 opts
['ref'] = self
.model
.head
415 self
.new_filename
= filenames
[0]
416 self
.old_filename
= self
.model
.filename
417 self
.new_mode
= self
.model
.mode_worktree
418 self
.new_diff_text
= gitcmds
.diff_helper(filename
=self
.new_filename
,
419 cached
=cached
, **opts
)
422 class Diffstat(Command
):
423 """Perform a diffstat and set the model's diff text."""
426 Command
.__init
__(self
)
427 diff
= self
.model
.git
.diff(self
.model
.head
,
428 unified
=_config
.get('diff.context', 3),
433 self
.new_diff_text
= diff
434 self
.new_mode
= self
.model
.mode_worktree
437 class DiffStaged(Diff
):
438 """Perform a staged diff on a file."""
440 def __init__(self
, filenames
):
441 Diff
.__init
__(self
, filenames
, cached
=True)
442 self
.new_mode
= self
.model
.mode_index
445 class DiffStagedSummary(Command
):
448 Command
.__init
__(self
)
449 diff
= self
.model
.git
.diff(self
.model
.head
,
453 patch_with_stat
=True,
455 self
.new_diff_text
= diff
456 self
.new_mode
= self
.model
.mode_index
459 class Difftool(Command
):
460 """Run git-difftool limited by path."""
462 def __init__(self
, staged
, filenames
):
463 Command
.__init
__(self
)
465 self
.filenames
= filenames
468 difftool
.launch_with_head(self
.filenames
,
469 self
.staged
, self
.model
.head
)
473 """Edit a file using the configured gui.editor."""
480 def __init__(self
, filenames
, line_number
=None):
481 Command
.__init
__(self
)
482 self
.filenames
= filenames
483 self
.line_number
= line_number
486 if not self
.filenames
:
488 filename
= self
.filenames
[0]
489 if not core
.exists(filename
):
491 editor
= prefs
.editor()
494 if self
.line_number
is None:
495 opts
= self
.filenames
497 # Single-file w/ line-numbers (likely from grep)
499 '*vim*': ['+'+self
.line_number
, filename
],
500 '*emacs*': ['+'+self
.line_number
, filename
],
501 '*textpad*': ['%s(%s,0)' % (filename
, self
.line_number
)],
502 '*notepad++*': ['-n'+self
.line_number
, filename
],
505 opts
= self
.filenames
506 for pattern
, opt
in editor_opts
.items():
507 if fnmatch(editor
, pattern
):
512 core
.fork(utils
.shell_split(editor
) + opts
)
513 except Exception as e
:
514 message
= (N_('Cannot exec "%s": please configure your editor') %
516 Interaction
.critical(N_('Error Editing File'),
520 class FormatPatch(Command
):
521 """Output a patch series given all revisions and a selected subset."""
523 def __init__(self
, to_export
, revs
):
524 Command
.__init
__(self
)
525 self
.to_export
= to_export
529 status
, out
, err
= gitcmds
.format_patchsets(self
.to_export
, self
.revs
)
530 Interaction
.log_status(status
, out
, err
)
533 class LaunchDifftool(BaseCommand
):
539 return N_('Launch Diff Tool')
542 BaseCommand
.__init
__(self
)
549 core
.fork(['git', 'mergetool', '--no-prompt', '--'] + paths
)
551 core
.fork(['xterm', '-e',
552 'git', 'mergetool', '--no-prompt', '--'] + paths
)
557 class LaunchEditor(Edit
):
562 return N_('Launch Editor')
566 allfiles
= s
.staged
+ s
.unmerged
+ s
.modified
+ s
.untracked
567 Edit
.__init
__(self
, allfiles
)
570 class LoadCommitMessageFromFile(Command
):
571 """Loads a commit message from a path."""
573 def __init__(self
, path
):
574 Command
.__init
__(self
)
577 self
.old_commitmsg
= self
.model
.commitmsg
578 self
.old_directory
= self
.model
.directory
582 if not path
or not core
.isfile(path
):
583 raise errors
.UsageError(N_('Error: Cannot find commit template'),
584 N_('%s: No such file or directory.') % path
)
585 self
.model
.set_directory(os
.path
.dirname(path
))
586 self
.model
.set_commitmsg(core
.read(path
))
589 self
.model
.set_commitmsg(self
.old_commitmsg
)
590 self
.model
.set_directory(self
.old_directory
)
593 class LoadCommitMessageFromTemplate(LoadCommitMessageFromFile
):
594 """Loads the commit message template specified by commit.template."""
597 template
= _config
.get('commit.template')
598 LoadCommitMessageFromFile
.__init
__(self
, template
)
601 if self
.path
is None:
602 raise errors
.UsageError(
603 N_('Error: Unconfigured commit template'),
604 N_('A commit template has not been configured.\n'
605 'Use "git config" to define "commit.template"\n'
606 'so that it points to a commit template.'))
607 return LoadCommitMessageFromFile
.do(self
)
611 class LoadCommitMessageFromSHA1(Command
):
612 """Load a previous commit message"""
614 def __init__(self
, sha1
, prefix
=''):
615 Command
.__init
__(self
)
617 self
.old_commitmsg
= self
.model
.commitmsg
618 self
.new_commitmsg
= prefix
+ self
.model
.prev_commitmsg(sha1
)
622 self
.model
.set_commitmsg(self
.new_commitmsg
)
625 self
.model
.set_commitmsg(self
.old_commitmsg
)
628 class LoadFixupMessage(LoadCommitMessageFromSHA1
):
629 """Load a fixup message"""
631 def __init__(self
, sha1
):
632 LoadCommitMessageFromSHA1
.__init__(self
, sha1
, prefix
='fixup! ')
635 class Merge(Command
):
636 def __init__(self
, revision
, no_commit
, squash
):
637 Command
.__init
__(self
)
638 self
.revision
= revision
639 self
.no_commit
= no_commit
644 revision
= self
.revision
645 no_commit
= self
.no_commit
646 msg
= gitcmds
.merge_message(revision
)
648 status
, out
, err
= self
.model
.git
.merge('-m', msg
,
653 Interaction
.log_status(status
, out
, err
)
654 self
.model
.update_status()
657 class OpenDefaultApp(BaseCommand
):
658 """Open a file using the OS default."""
663 return N_('Open Using Default Application')
665 def __init__(self
, filenames
):
666 BaseCommand
.__init
__(self
)
667 if utils
.is_darwin():
670 launcher
= 'xdg-open'
671 self
.launcher
= launcher
672 self
.filenames
= filenames
675 if not self
.filenames
:
677 core
.fork([self
.launcher
] + self
.filenames
)
680 class OpenParentDir(OpenDefaultApp
):
681 """Open parent directories using the OS default."""
682 SHORTCUT
= 'Shift+Space'
686 return N_('Open Parent Directory')
688 def __init__(self
, filenames
):
689 OpenDefaultApp
.__init
__(self
, filenames
)
692 if not self
.filenames
:
694 dirs
= set(map(os
.path
.dirname
, self
.filenames
))
695 core
.fork([self
.launcher
] + dirs
)
698 class OpenRepo(Command
):
699 """Launches git-cola on a repo."""
701 def __init__(self
, repo_path
):
702 Command
.__init
__(self
)
703 self
.repo_path
= repo_path
706 self
.model
.set_directory(self
.repo_path
)
707 core
.fork([sys
.executable
, sys
.argv
[0], '--repo', self
.repo_path
])
710 class Clone(Command
):
711 """Clones a repository and optionally spawns a new cola session."""
713 def __init__(self
, url
, new_directory
, spawn
=True):
714 Command
.__init
__(self
)
716 self
.new_directory
= new_directory
720 status
, out
, err
= self
.model
.git
.clone(self
.url
, self
.new_directory
)
722 Interaction
.information(
723 N_('Error: could not clone "%s"') % self
.url
,
724 (N_('git clone returned exit code %s') % status
) +
725 ((out
+err
) and ('\n\n' + out
+ err
) or ''))
728 core
.fork([sys
.executable
, sys
.argv
[0],
729 '--repo', self
.new_directory
])
733 class GitXBaseContext(object):
735 def __init__(self
, **kwargs
):
739 compat
.setenv('GIT_SEQUENCE_EDITOR',
740 resources
.share('bin', 'git-xbase'))
741 for var
, value
in self
.extras
.items():
742 compat
.setenv(var
, value
)
745 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
746 compat
.unsetenv('GIT_SEQUENCE_EDITOR')
747 for var
in self
.extras
:
751 class Rebase(Command
):
753 def __init__(self
, branch
):
754 Command
.__init
__(self
)
764 with
GitXBaseContext(
765 GIT_EDITOR
=prefs
.editor(),
766 GIT_XBASE_TITLE
=N_('Rebase onto %s') % branch
,
767 GIT_XBASE_ACTION
=N_('Rebase')):
768 status
, out
, err
= self
.model
.git
.rebase(branch
,
771 Interaction
.log_status(status
, out
, err
)
772 self
.model
.update_status()
773 return status
, out
, err
776 class RebaseEditTodo(Command
):
779 with
GitXBaseContext(
780 GIT_XBASE_TITLE
=N_('Edit Rebase'),
781 GIT_XBASE_ACTION
=N_('Save')):
782 status
, out
, err
= self
.model
.git
.rebase(edit_todo
=True)
783 Interaction
.log_status(status
, out
, err
)
784 self
.model
.update_status()
787 class RebaseContinue(Command
):
790 status
, out
, err
= self
.model
.git
.rebase('--continue')
791 Interaction
.log_status(status
, out
, err
)
792 self
.model
.update_status()
795 class RebaseSkip(Command
):
798 status
, out
, err
= self
.model
.git
.rebase(skip
=True)
799 Interaction
.log_status(status
, out
, err
)
800 self
.model
.update_status()
803 class RebaseAbort(Command
):
806 status
, out
, err
= self
.model
.git
.rebase(abort
=True)
807 Interaction
.log_status(status
, out
, err
)
808 self
.model
.update_status()
811 class Rescan(Command
):
812 """Rescan for changes"""
815 self
.model
.update_status()
818 class Refresh(Command
):
819 """Update refs and refresh the index"""
828 self
.model
.update_status(update_index
=True)
831 class RunConfigAction(Command
):
832 """Run a user-configured action, typically from the "Tools" menu"""
834 def __init__(self
, action_name
):
835 Command
.__init
__(self
)
836 self
.action_name
= action_name
837 self
.model
= cola
.model()
840 for env
in ('FILENAME', 'REVISION', 'ARGS'):
847 opts
= _config
.get_guitool_opts(self
.action_name
)
848 cmd
= opts
.get('cmd')
849 if 'title' not in opts
:
852 if 'prompt' not in opts
or opts
.get('prompt') is True:
853 prompt
= N_('Run "%s"?') % cmd
854 opts
['prompt'] = prompt
856 if opts
.get('needsfile'):
857 filename
= selection
.filename()
859 Interaction
.information(
860 N_('Please select a file'),
861 N_('"%s" requires a selected file.') % cmd
)
863 compat
.setenv('FILENAME', filename
)
865 if opts
.get('revprompt') or opts
.get('argprompt'):
867 ok
= Interaction
.confirm_config_action(cmd
, opts
)
870 rev
= opts
.get('revision')
871 args
= opts
.get('args')
872 if opts
.get('revprompt') and not rev
:
873 title
= N_('Invalid Revision')
874 msg
= N_('The revision expression cannot be empty.')
875 Interaction
.critical(title
, msg
)
879 elif opts
.get('confirm'):
880 title
= os
.path
.expandvars(opts
.get('title'))
881 prompt
= os
.path
.expandvars(opts
.get('prompt'))
882 if Interaction
.question(title
, prompt
):
885 compat
.setenv('REVISION', rev
)
887 compat
.setenv('ARGS', args
)
888 title
= os
.path
.expandvars(cmd
)
889 Interaction
.log(N_('Running command: %s') % title
)
890 cmd
= ['sh', '-c', cmd
]
892 if opts
.get('noconsole'):
893 status
, out
, err
= core
.run_command(cmd
)
895 status
, out
, err
= Interaction
.run_command(title
, cmd
)
897 Interaction
.log_status(status
,
898 out
and (N_('Output: %s') % out
) or '',
899 err
and (N_('Errors: %s') % err
) or '')
901 if not opts
.get('norescan'):
902 self
.model
.update_status()
906 class SetDiffText(Command
):
908 def __init__(self
, text
):
909 Command
.__init
__(self
)
911 self
.new_diff_text
= text
914 class ShowUntracked(Command
):
915 """Show an untracked file."""
917 def __init__(self
, filenames
):
918 Command
.__init
__(self
)
919 self
.filenames
= filenames
920 self
.new_mode
= self
.model
.mode_untracked
921 self
.new_diff_text
= ''
923 self
.new_diff_text
= self
.diff_text_for(filenames
[0])
925 def diff_text_for(self
, filename
):
926 size
= _config
.get('cola.readsize', 1024 * 2)
928 result
= core
.read(filename
, size
=size
)
932 if len(result
) == size
:
937 class SignOff(Command
):
942 return N_('Sign Off')
945 Command
.__init
__(self
)
947 self
.old_commitmsg
= self
.model
.commitmsg
950 signoff
= self
.signoff()
951 if signoff
in self
.model
.commitmsg
:
953 self
.model
.set_commitmsg(self
.model
.commitmsg
+ '\n' + signoff
)
956 self
.model
.set_commitmsg(self
.old_commitmsg
)
961 user
= pwd
.getpwuid(os
.getuid()).pw_name
963 user
= os
.getenv('USER', N_('unknown'))
965 name
= _config
.get('user.name', user
)
966 email
= _config
.get('user.email', '%s@%s' % (user
, platform
.node()))
967 return '\nSigned-off-by: %s <%s>' % (name
, email
)
970 class Stage(Command
):
971 """Stage a set of paths."""
978 def __init__(self
, paths
):
979 Command
.__init
__(self
)
983 msg
= N_('Staging: %s') % (', '.join(self
.paths
))
985 # Prevent external updates while we are staging files.
986 # We update file stats at the end of this operation
987 # so there's no harm in ignoring updates from other threads
989 with
CommandDisabled(UpdateFileStatus
):
990 self
.model
.stage_paths(self
.paths
)
993 class StageModified(Stage
):
994 """Stage all modified files."""
1000 return N_('Stage Modified')
1003 Stage
.__init
__(self
, None)
1004 self
.paths
= self
.model
.modified
1007 class StageUnmerged(Stage
):
1008 """Stage all modified files."""
1014 return N_('Stage Unmerged')
1017 Stage
.__init
__(self
, None)
1018 self
.paths
= self
.model
.unmerged
1021 class StageUntracked(Stage
):
1022 """Stage all untracked files."""
1028 return N_('Stage Untracked')
1031 Stage
.__init
__(self
, None)
1032 self
.paths
= self
.model
.untracked
1036 """Create a tag object."""
1038 def __init__(self
, name
, revision
, sign
=False, message
=''):
1039 Command
.__init
__(self
)
1041 self
._message
= message
1042 self
._revision
= revision
1046 log_msg
= (N_('Tagging "%(revision)s" as "%(name)s"') %
1047 dict(revision
=self
._revision
, name
=self
._name
))
1050 opts
['F'] = utils
.tmp_filename('tag-message')
1051 core
.write(opts
['F'], self
._message
)
1054 log_msg
+= ' (%s)' % N_('GPG-signed')
1056 status
, output
, err
= self
.model
.git
.tag(self
._name
,
1057 self
._revision
, **opts
)
1059 opts
['a'] = bool(self
._message
)
1060 status
, output
, err
= self
.model
.git
.tag(self
._name
,
1061 self
._revision
, **opts
)
1063 os
.unlink(opts
['F'])
1066 log_msg
+= '\n' + (N_('Output: %s') % output
)
1068 Interaction
.log_status(status
, log_msg
, err
)
1070 self
.model
.update_status()
1073 class Unstage(Command
):
1074 """Unstage a set of paths."""
1080 return N_('Unstage')
1082 def __init__(self
, paths
):
1083 Command
.__init
__(self
)
1087 msg
= N_('Unstaging: %s') % (', '.join(self
.paths
))
1088 Interaction
.log(msg
)
1089 with
CommandDisabled(UpdateFileStatus
):
1090 self
.model
.unstage_paths(self
.paths
)
1093 class UnstageAll(Command
):
1094 """Unstage all files; resets the index."""
1097 self
.model
.unstage_all()
1100 class UnstageSelected(Unstage
):
1101 """Unstage selected files."""
1104 Unstage
.__init
__(self
, cola
.selection_model().staged
)
1107 class Untrack(Command
):
1108 """Unstage a set of paths."""
1110 def __init__(self
, paths
):
1111 Command
.__init
__(self
)
1115 msg
= N_('Untracking: %s') % (', '.join(self
.paths
))
1116 Interaction
.log(msg
)
1117 with
CommandDisabled(UpdateFileStatus
):
1118 status
, out
, err
= self
.model
.untrack_paths(self
.paths
)
1119 Interaction
.log_status(status
, out
, err
)
1122 class UntrackedSummary(Command
):
1123 """List possible .gitignore rules as the diff text."""
1126 Command
.__init
__(self
)
1127 untracked
= self
.model
.untracked
1128 suffix
= len(untracked
) > 1 and 's' or ''
1130 io
.write('# %s untracked file%s\n' % (len(untracked
), suffix
))
1132 io
.write('# possible .gitignore rule%s:\n' % suffix
)
1134 io
.write('/'+core
.encode(u
)+'\n')
1135 self
.new_diff_text
= core
.decode(io
.getvalue())
1136 self
.new_mode
= self
.model
.mode_untracked
1139 class UpdateFileStatus(Command
):
1140 """Rescans for changes."""
1143 self
.model
.update_file_status()
1146 class VisualizeAll(Command
):
1147 """Visualize all branches."""
1150 browser
= utils
.shell_split(prefs
.history_browser())
1151 core
.fork(browser
+ ['--all'])
1154 class VisualizeCurrent(Command
):
1155 """Visualize all branches."""
1158 browser
= utils
.shell_split(prefs
.history_browser())
1159 core
.fork(browser
+ [self
.model
.currentbranch
])
1162 class VisualizePaths(Command
):
1163 """Path-limited visualization."""
1165 def __init__(self
, paths
):
1166 Command
.__init
__(self
)
1167 browser
= utils
.shell_split(prefs
.history_browser())
1169 self
.argv
= browser
+ paths
1174 core
.fork(self
.argv
)
1177 class VisualizeRevision(Command
):
1178 """Visualize a specific revision."""
1180 def __init__(self
, revision
, paths
=None):
1181 Command
.__init
__(self
)
1182 self
.revision
= revision
1186 argv
= utils
.shell_split(prefs
.history_browser())
1188 argv
.append(self
.revision
)
1191 argv
.extend(self
.paths
)
1195 except Exception as e
:
1196 _
, details
= utils
.format_exception(e
)
1197 title
= N_('Error Launching History Browser')
1198 msg
= (N_('Cannot exec "%s": please configure a history browser') %
1200 Interaction
.critical(title
, message
=msg
, details
=details
)
1203 def run(cls
, *args
, **opts
):
1205 Returns a callback that runs a command
1207 If the caller of run() provides args or opts then those are
1208 used instead of the ones provided by the invoker of the callback.
1211 def runner(*local_args
, **local_opts
):
1213 do(cls
, *args
, **opts
)
1215 do(cls
, *local_args
, **local_opts
)
1220 class CommandDisabled(object):
1222 """Context manager to temporarily disable a command from running"""
1223 def __init__(self
, cmdclass
):
1224 self
.cmdclass
= cmdclass
1226 def __enter__(self
):
1227 self
.cmdclass
.DISABLED
= True
1230 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
1231 self
.cmdclass
.DISABLED
= False
1234 def do(cls
, *args
, **opts
):
1235 """Run a command in-place"""
1236 return do_cmd(cls(*args
, **opts
))
1240 if hasattr(cmd
, 'DISABLED') and cmd
.DISABLED
:
1244 except StandardError, e
:
1245 msg
, details
= utils
.format_exception(e
)
1246 Interaction
.critical(N_('Error'), message
=msg
, details
=details
)