3 from fnmatch
import fnmatch
5 from cStringIO
import StringIO
7 from cola
import compat
9 from cola
import gitcfg
10 from cola
import gitcmds
11 from cola
import utils
12 from cola
import difftool
13 from cola
import resources
14 from cola
.compat
import set
15 from cola
.diffparse
import DiffParser
16 from cola
.git
import STDOUT
17 from cola
.i18n
import N_
18 from cola
.interaction
import Interaction
19 from cola
.models
import main
20 from cola
.models
import prefs
21 from cola
.models
import selection
23 _config
= gitcfg
.instance()
26 class UsageError(StandardError):
27 """Exception class for usage errors."""
28 def __init__(self
, title
, message
):
29 StandardError.__init
__(self
, message
)
34 class BaseCommand(object):
35 """Base class for all commands; provides the command pattern"""
42 def is_undoable(self
):
43 """Can this be undone?"""
51 raise NotImplementedError('%s.do() is unimplemented' % self
.__class
__.__name
__)
54 raise NotImplementedError('%s.undo() is unimplemented' % self
.__class
__.__name
__)
57 class Command(BaseCommand
):
58 """Base class for commands that modify the main model"""
61 """Initialize the command and stash away values for use in do()"""
62 # These are commonly used so let's make it easier to write new commands.
63 BaseCommand
.__init
__(self
)
64 self
.model
= main
.model()
66 self
.old_diff_text
= self
.model
.diff_text
67 self
.old_filename
= self
.model
.filename
68 self
.old_mode
= self
.model
.mode
70 self
.new_diff_text
= self
.old_diff_text
71 self
.new_filename
= self
.old_filename
72 self
.new_mode
= self
.old_mode
75 """Perform the operation."""
76 self
.model
.set_filename(self
.new_filename
)
77 self
.model
.set_mode(self
.new_mode
)
78 self
.model
.set_diff_text(self
.new_diff_text
)
81 """Undo the operation."""
82 self
.model
.set_diff_text(self
.old_diff_text
)
83 self
.model
.set_filename(self
.old_filename
)
84 self
.model
.set_mode(self
.old_mode
)
87 class AmendMode(Command
):
88 """Try to amend a commit."""
98 def __init__(self
, amend
):
99 Command
.__init
__(self
)
102 self
.amending
= amend
103 self
.old_commitmsg
= self
.model
.commitmsg
104 self
.old_mode
= self
.model
.mode
107 self
.new_mode
= self
.model
.mode_amend
108 self
.new_commitmsg
= self
.model
.prev_commitmsg()
109 AmendMode
.LAST_MESSAGE
= self
.model
.commitmsg
111 # else, amend unchecked, regular commit
112 self
.new_mode
= self
.model
.mode_none
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 self
.model
.is_merging
:
128 self
.model
.set_mode(self
.old_mode
)
129 Interaction
.information(
131 N_('You are in the middle of a merge.\n'
132 'Cannot amend while merging.'))
136 self
.model
.set_commitmsg(self
.new_commitmsg
)
137 self
.model
.update_file_status()
142 self
.model
.set_commitmsg(self
.old_commitmsg
)
144 self
.model
.update_file_status()
147 class ApplyDiffSelection(Command
):
149 def __init__(self
, staged
, selected
, offset
, selection_text
,
151 Command
.__init
__(self
)
153 self
.selected
= selected
155 self
.selection_text
= selection_text
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_diff_text
= ''
287 self
.model
.update_file_status()
290 class Commit(ResetMode
):
291 """Attempt to create a new commit."""
293 SHORTCUT
= 'Ctrl+Return'
295 def __init__(self
, amend
, msg
):
296 ResetMode
.__init
__(self
)
299 self
.old_commitmsg
= self
.model
.commitmsg
300 self
.new_commitmsg
= ''
303 tmpfile
= utils
.tmp_filename('commit-message')
304 status
, out
, err
= self
.model
.commit_with_msg(self
.msg
, tmpfile
,
308 self
.model
.set_commitmsg(self
.new_commitmsg
)
309 msg
= N_('Created commit: %s') % out
311 msg
= N_('Commit failed: %s') % out
312 Interaction
.log_status(status
, msg
, err
)
314 return status
, out
, err
317 class Ignore(Command
):
318 """Add files to .gitignore"""
320 def __init__(self
, filenames
):
321 Command
.__init
__(self
)
322 self
.filenames
= filenames
326 for fname
in self
.filenames
:
327 new_additions
= new_additions
+ fname
+ '\n'
328 for_status
= new_additions
330 if core
.exists('.gitignore'):
331 current_list
= core
.read('.gitignore')
332 new_additions
= new_additions
+ current_list
333 core
.write('.gitignore', new_additions
)
334 Interaction
.log_status(
335 0, 'Added to .gitignore:\n%s' % for_status
, '')
336 self
.model
.update_file_status()
339 class Delete(Command
):
342 def __init__(self
, filenames
):
343 Command
.__init
__(self
)
344 self
.filenames
= filenames
345 # We could git-hash-object stuff and provide undo-ability
349 for filename
in self
.filenames
:
355 Interaction
.information(
357 N_('Deleting "%s" failed') % filename
)
359 self
.model
.update_file_status()
362 class DeleteBranch(Command
):
363 """Delete a git branch."""
365 def __init__(self
, branch
):
366 Command
.__init
__(self
)
370 status
, out
, err
= self
.model
.delete_branch(self
.branch
)
371 Interaction
.log_status(status
, out
, err
)
374 class DeleteRemoteBranch(Command
):
375 """Delete a remote git branch."""
377 def __init__(self
, remote
, branch
):
378 Command
.__init
__(self
)
383 status
, out
, err
= self
.model
.git
.push(self
.remote
, self
.branch
,
385 Interaction
.log_status(status
, out
, err
)
386 self
.model
.update_status()
389 Interaction
.information(
390 N_('Remote Branch Deleted'),
391 N_('"%(branch)s" has been deleted from "%(remote)s".')
392 % dict(branch
=self
.branch
, remote
=self
.remote
))
395 message
= (N_('"%(command)s" returned exit status %(status)d') %
396 dict(command
=command
, status
=status
))
398 Interaction
.critical(N_('Error Deleting Remote Branch'),
404 """Perform a diff and set the model's current text."""
406 def __init__(self
, filenames
, cached
=False):
407 Command
.__init
__(self
)
408 # Guard against the list of files being empty
413 opts
['ref'] = self
.model
.head
414 self
.new_filename
= filenames
[0]
415 self
.old_filename
= self
.model
.filename
416 self
.new_mode
= self
.model
.mode_worktree
417 self
.new_diff_text
= gitcmds
.diff_helper(filename
=self
.new_filename
,
418 cached
=cached
, **opts
)
421 class Diffstat(Command
):
422 """Perform a diffstat and set the model's diff text."""
425 Command
.__init
__(self
)
426 diff
= self
.model
.git
.diff(self
.model
.head
,
427 unified
=_config
.get('diff.context', 3),
432 self
.new_diff_text
= diff
433 self
.new_mode
= self
.model
.mode_worktree
436 class DiffStaged(Diff
):
437 """Perform a staged diff on a file."""
439 def __init__(self
, filenames
):
440 Diff
.__init
__(self
, filenames
, cached
=True)
441 self
.new_mode
= self
.model
.mode_index
444 class DiffStagedSummary(Command
):
447 Command
.__init
__(self
)
448 diff
= self
.model
.git
.diff(self
.model
.head
,
452 patch_with_stat
=True,
454 self
.new_diff_text
= diff
455 self
.new_mode
= self
.model
.mode_index
458 class Difftool(Command
):
459 """Run git-difftool limited by path."""
461 def __init__(self
, staged
, filenames
):
462 Command
.__init
__(self
)
464 self
.filenames
= filenames
467 difftool
.launch_with_head(self
.filenames
,
468 self
.staged
, self
.model
.head
)
472 """Edit a file using the configured gui.editor."""
479 def __init__(self
, filenames
, line_number
=None):
480 Command
.__init
__(self
)
481 self
.filenames
= filenames
482 self
.line_number
= line_number
485 if not self
.filenames
:
487 filename
= self
.filenames
[0]
488 if not core
.exists(filename
):
490 editor
= prefs
.editor()
493 if self
.line_number
is None:
494 opts
= self
.filenames
496 # Single-file w/ line-numbers (likely from grep)
498 '*vim*': ['+'+self
.line_number
, filename
],
499 '*emacs*': ['+'+self
.line_number
, filename
],
500 '*textpad*': ['%s(%s,0)' % (filename
, self
.line_number
)],
501 '*notepad++*': ['-n'+self
.line_number
, filename
],
504 opts
= self
.filenames
505 for pattern
, opt
in editor_opts
.items():
506 if fnmatch(editor
, pattern
):
511 core
.fork(utils
.shell_split(editor
) + opts
)
512 except Exception as e
:
513 message
= (N_('Cannot exec "%s": please configure your editor') %
515 Interaction
.critical(N_('Error Editing File'),
519 class FormatPatch(Command
):
520 """Output a patch series given all revisions and a selected subset."""
522 def __init__(self
, to_export
, revs
):
523 Command
.__init
__(self
)
524 self
.to_export
= to_export
528 status
, out
, err
= gitcmds
.format_patchsets(self
.to_export
, self
.revs
)
529 Interaction
.log_status(status
, out
, err
)
532 class LaunchDifftool(BaseCommand
):
538 return N_('Launch Diff Tool')
541 BaseCommand
.__init
__(self
)
544 s
= selection
.selection()
548 core
.fork(['git', 'mergetool', '--no-prompt', '--'] + paths
)
550 core
.fork(['xterm', '-e',
551 'git', 'mergetool', '--no-prompt', '--'] + paths
)
556 class LaunchEditor(Edit
):
561 return N_('Launch Editor')
564 s
= selection
.selection()
565 allfiles
= s
.staged
+ s
.unmerged
+ s
.modified
+ s
.untracked
566 Edit
.__init
__(self
, allfiles
)
569 class LoadCommitMessageFromFile(Command
):
570 """Loads a commit message from a path."""
572 def __init__(self
, path
):
573 Command
.__init
__(self
)
576 self
.old_commitmsg
= self
.model
.commitmsg
577 self
.old_directory
= self
.model
.directory
581 if not path
or not core
.isfile(path
):
582 raise UsageError(N_('Error: Cannot find commit template'),
583 N_('%s: No such file or directory.') % path
)
584 self
.model
.set_directory(os
.path
.dirname(path
))
585 self
.model
.set_commitmsg(core
.read(path
))
588 self
.model
.set_commitmsg(self
.old_commitmsg
)
589 self
.model
.set_directory(self
.old_directory
)
592 class LoadCommitMessageFromTemplate(LoadCommitMessageFromFile
):
593 """Loads the commit message template specified by commit.template."""
596 template
= _config
.get('commit.template')
597 LoadCommitMessageFromFile
.__init
__(self
, template
)
600 if self
.path
is None:
602 N_('Error: Unconfigured commit template'),
603 N_('A commit template has not been configured.\n'
604 'Use "git config" to define "commit.template"\n'
605 'so that it points to a commit template.'))
606 return LoadCommitMessageFromFile
.do(self
)
610 class LoadCommitMessageFromSHA1(Command
):
611 """Load a previous commit message"""
613 def __init__(self
, sha1
, prefix
=''):
614 Command
.__init
__(self
)
616 self
.old_commitmsg
= self
.model
.commitmsg
617 self
.new_commitmsg
= prefix
+ self
.model
.prev_commitmsg(sha1
)
621 self
.model
.set_commitmsg(self
.new_commitmsg
)
624 self
.model
.set_commitmsg(self
.old_commitmsg
)
627 class LoadFixupMessage(LoadCommitMessageFromSHA1
):
628 """Load a fixup message"""
630 def __init__(self
, sha1
):
631 LoadCommitMessageFromSHA1
.__init__(self
, sha1
, prefix
='fixup! ')
634 class Merge(Command
):
635 def __init__(self
, revision
, no_commit
, squash
):
636 Command
.__init
__(self
)
637 self
.revision
= revision
638 self
.no_commit
= no_commit
643 revision
= self
.revision
644 no_commit
= self
.no_commit
645 msg
= gitcmds
.merge_message(revision
)
647 status
, out
, err
= self
.model
.git
.merge('-m', msg
,
652 Interaction
.log_status(status
, out
, err
)
653 self
.model
.update_status()
656 class OpenDefaultApp(BaseCommand
):
657 """Open a file using the OS default."""
662 return N_('Open Using Default Application')
664 def __init__(self
, filenames
):
665 BaseCommand
.__init
__(self
)
666 if utils
.is_darwin():
669 launcher
= 'xdg-open'
670 self
.launcher
= launcher
671 self
.filenames
= filenames
674 if not self
.filenames
:
676 core
.fork([self
.launcher
] + self
.filenames
)
679 class OpenParentDir(OpenDefaultApp
):
680 """Open parent directories using the OS default."""
681 SHORTCUT
= 'Shift+Space'
685 return N_('Open Parent Directory')
687 def __init__(self
, filenames
):
688 OpenDefaultApp
.__init
__(self
, filenames
)
691 if not self
.filenames
:
693 dirs
= set(map(os
.path
.dirname
, self
.filenames
))
694 core
.fork([self
.launcher
] + dirs
)
697 class OpenRepo(Command
):
698 """Launches git-cola on a repo."""
700 def __init__(self
, repo_path
):
701 Command
.__init
__(self
)
702 self
.repo_path
= repo_path
705 self
.model
.set_directory(self
.repo_path
)
706 core
.fork([sys
.executable
, sys
.argv
[0], '--repo', self
.repo_path
])
709 class Clone(Command
):
710 """Clones a repository and optionally spawns a new cola session."""
712 def __init__(self
, url
, new_directory
, spawn
=True):
713 Command
.__init
__(self
)
715 self
.new_directory
= new_directory
719 status
, out
, err
= self
.model
.git
.clone(self
.url
, self
.new_directory
)
721 Interaction
.information(
722 N_('Error: could not clone "%s"') % self
.url
,
723 (N_('git clone returned exit code %s') % status
) +
724 ((out
+err
) and ('\n\n' + out
+ err
) or ''))
727 core
.fork([sys
.executable
, sys
.argv
[0],
728 '--repo', self
.new_directory
])
732 class GitXBaseContext(object):
734 def __init__(self
, **kwargs
):
738 compat
.setenv('GIT_SEQUENCE_EDITOR',
739 resources
.share('bin', 'git-xbase'))
740 for var
, value
in self
.extras
.items():
741 compat
.setenv(var
, value
)
744 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
745 compat
.unsetenv('GIT_SEQUENCE_EDITOR')
746 for var
in self
.extras
:
750 class Rebase(Command
):
752 def __init__(self
, branch
):
753 Command
.__init
__(self
)
763 with
GitXBaseContext(
764 GIT_EDITOR
=prefs
.editor(),
765 GIT_XBASE_TITLE
=N_('Rebase onto %s') % branch
,
766 GIT_XBASE_ACTION
=N_('Rebase')):
767 status
, out
, err
= self
.model
.git
.rebase(branch
,
770 Interaction
.log_status(status
, out
, err
)
771 self
.model
.update_status()
772 return status
, out
, err
775 class RebaseEditTodo(Command
):
778 with
GitXBaseContext(
779 GIT_XBASE_TITLE
=N_('Edit Rebase'),
780 GIT_XBASE_ACTION
=N_('Save')):
781 status
, out
, err
= self
.model
.git
.rebase(edit_todo
=True)
782 Interaction
.log_status(status
, out
, err
)
783 self
.model
.update_status()
786 class RebaseContinue(Command
):
789 status
, out
, err
= self
.model
.git
.rebase('--continue')
790 Interaction
.log_status(status
, out
, err
)
791 self
.model
.update_status()
794 class RebaseSkip(Command
):
797 status
, out
, err
= self
.model
.git
.rebase(skip
=True)
798 Interaction
.log_status(status
, out
, err
)
799 self
.model
.update_status()
802 class RebaseAbort(Command
):
805 status
, out
, err
= self
.model
.git
.rebase(abort
=True)
806 Interaction
.log_status(status
, out
, err
)
807 self
.model
.update_status()
810 class Rescan(Command
):
811 """Rescan for changes"""
814 self
.model
.update_status()
817 class Refresh(Command
):
818 """Update refs and refresh the index"""
827 self
.model
.update_status(update_index
=True)
830 class RunConfigAction(Command
):
831 """Run a user-configured action, typically from the "Tools" menu"""
833 def __init__(self
, action_name
):
834 Command
.__init
__(self
)
835 self
.action_name
= action_name
836 self
.model
= main
.model()
839 for env
in ('FILENAME', 'REVISION', 'ARGS'):
846 opts
= _config
.get_guitool_opts(self
.action_name
)
847 cmd
= opts
.get('cmd')
848 if 'title' not in opts
:
851 if 'prompt' not in opts
or opts
.get('prompt') is True:
852 prompt
= N_('Run "%s"?') % cmd
853 opts
['prompt'] = prompt
855 if opts
.get('needsfile'):
856 filename
= selection
.filename()
858 Interaction
.information(
859 N_('Please select a file'),
860 N_('"%s" requires a selected file.') % cmd
)
862 compat
.setenv('FILENAME', filename
)
864 if opts
.get('revprompt') or opts
.get('argprompt'):
866 ok
= Interaction
.confirm_config_action(cmd
, opts
)
869 rev
= opts
.get('revision')
870 args
= opts
.get('args')
871 if opts
.get('revprompt') and not rev
:
872 title
= N_('Invalid Revision')
873 msg
= N_('The revision expression cannot be empty.')
874 Interaction
.critical(title
, msg
)
878 elif opts
.get('confirm'):
879 title
= os
.path
.expandvars(opts
.get('title'))
880 prompt
= os
.path
.expandvars(opts
.get('prompt'))
881 if Interaction
.question(title
, prompt
):
884 compat
.setenv('REVISION', rev
)
886 compat
.setenv('ARGS', args
)
887 title
= os
.path
.expandvars(cmd
)
888 Interaction
.log(N_('Running command: %s') % title
)
889 cmd
= ['sh', '-c', cmd
]
891 if opts
.get('noconsole'):
892 status
, out
, err
= core
.run_command(cmd
)
894 status
, out
, err
= Interaction
.run_command(title
, cmd
)
896 Interaction
.log_status(status
,
897 out
and (N_('Output: %s') % out
) or '',
898 err
and (N_('Errors: %s') % err
) or '')
900 if not opts
.get('norescan'):
901 self
.model
.update_status()
905 class SetDiffText(Command
):
907 def __init__(self
, text
):
908 Command
.__init
__(self
)
910 self
.new_diff_text
= text
913 class ShowUntracked(Command
):
914 """Show an untracked file."""
916 def __init__(self
, filenames
):
917 Command
.__init
__(self
)
918 self
.filenames
= filenames
919 self
.new_mode
= self
.model
.mode_untracked
920 self
.new_diff_text
= ''
922 self
.new_diff_text
= self
.diff_text_for(filenames
[0])
924 def diff_text_for(self
, filename
):
925 size
= _config
.get('cola.readsize', 1024 * 2)
927 result
= core
.read(filename
, size
=size
)
931 if len(result
) == size
:
936 class SignOff(Command
):
941 return N_('Sign Off')
944 Command
.__init
__(self
)
946 self
.old_commitmsg
= self
.model
.commitmsg
949 signoff
= self
.signoff()
950 if signoff
in self
.model
.commitmsg
:
952 self
.model
.set_commitmsg(self
.model
.commitmsg
+ '\n' + signoff
)
955 self
.model
.set_commitmsg(self
.old_commitmsg
)
960 user
= pwd
.getpwuid(os
.getuid()).pw_name
962 user
= os
.getenv('USER', N_('unknown'))
964 name
= _config
.get('user.name', user
)
965 email
= _config
.get('user.email', '%s@%s' % (user
, core
.node()))
966 return '\nSigned-off-by: %s <%s>' % (name
, email
)
969 class Stage(Command
):
970 """Stage a set of paths."""
977 def __init__(self
, paths
):
978 Command
.__init
__(self
)
982 msg
= N_('Staging: %s') % (', '.join(self
.paths
))
984 # Prevent external updates while we are staging files.
985 # We update file stats at the end of this operation
986 # so there's no harm in ignoring updates from other threads
988 with
CommandDisabled(UpdateFileStatus
):
989 self
.model
.stage_paths(self
.paths
)
992 class StageModified(Stage
):
993 """Stage all modified files."""
999 return N_('Stage Modified')
1002 Stage
.__init
__(self
, None)
1003 self
.paths
= self
.model
.modified
1006 class StageUnmerged(Stage
):
1007 """Stage all modified files."""
1013 return N_('Stage Unmerged')
1016 Stage
.__init
__(self
, None)
1017 self
.paths
= self
.model
.unmerged
1020 class StageUntracked(Stage
):
1021 """Stage all untracked files."""
1027 return N_('Stage Untracked')
1030 Stage
.__init
__(self
, None)
1031 self
.paths
= self
.model
.untracked
1035 """Create a tag object."""
1037 def __init__(self
, name
, revision
, sign
=False, message
=''):
1038 Command
.__init
__(self
)
1040 self
._message
= message
1041 self
._revision
= revision
1045 log_msg
= (N_('Tagging "%(revision)s" as "%(name)s"') %
1046 dict(revision
=self
._revision
, name
=self
._name
))
1049 opts
['F'] = utils
.tmp_filename('tag-message')
1050 core
.write(opts
['F'], self
._message
)
1053 log_msg
+= ' (%s)' % N_('GPG-signed')
1055 status
, output
, err
= self
.model
.git
.tag(self
._name
,
1056 self
._revision
, **opts
)
1058 opts
['a'] = bool(self
._message
)
1059 status
, output
, err
= self
.model
.git
.tag(self
._name
,
1060 self
._revision
, **opts
)
1062 os
.unlink(opts
['F'])
1065 log_msg
+= '\n' + (N_('Output: %s') % output
)
1067 Interaction
.log_status(status
, log_msg
, err
)
1069 self
.model
.update_status()
1072 class Unstage(Command
):
1073 """Unstage a set of paths."""
1079 return N_('Unstage')
1081 def __init__(self
, paths
):
1082 Command
.__init
__(self
)
1086 msg
= N_('Unstaging: %s') % (', '.join(self
.paths
))
1087 Interaction
.log(msg
)
1088 with
CommandDisabled(UpdateFileStatus
):
1089 self
.model
.unstage_paths(self
.paths
)
1092 class UnstageAll(Command
):
1093 """Unstage all files; resets the index."""
1096 self
.model
.unstage_all()
1099 class UnstageSelected(Unstage
):
1100 """Unstage selected files."""
1103 Unstage
.__init
__(self
, selection
.selection_model().staged
)
1106 class Untrack(Command
):
1107 """Unstage a set of paths."""
1109 def __init__(self
, paths
):
1110 Command
.__init
__(self
)
1114 msg
= N_('Untracking: %s') % (', '.join(self
.paths
))
1115 Interaction
.log(msg
)
1116 with
CommandDisabled(UpdateFileStatus
):
1117 status
, out
, err
= self
.model
.untrack_paths(self
.paths
)
1118 Interaction
.log_status(status
, out
, err
)
1121 class UntrackedSummary(Command
):
1122 """List possible .gitignore rules as the diff text."""
1125 Command
.__init
__(self
)
1126 untracked
= self
.model
.untracked
1127 suffix
= len(untracked
) > 1 and 's' or ''
1129 io
.write('# %s untracked file%s\n' % (len(untracked
), suffix
))
1131 io
.write('# possible .gitignore rule%s:\n' % suffix
)
1133 io
.write('/'+core
.encode(u
)+'\n')
1134 self
.new_diff_text
= core
.decode(io
.getvalue())
1135 self
.new_mode
= self
.model
.mode_untracked
1138 class UpdateFileStatus(Command
):
1139 """Rescans for changes."""
1142 self
.model
.update_file_status()
1145 class VisualizeAll(Command
):
1146 """Visualize all branches."""
1149 browser
= utils
.shell_split(prefs
.history_browser())
1150 core
.fork(browser
+ ['--all'])
1153 class VisualizeCurrent(Command
):
1154 """Visualize all branches."""
1157 browser
= utils
.shell_split(prefs
.history_browser())
1158 core
.fork(browser
+ [self
.model
.currentbranch
])
1161 class VisualizePaths(Command
):
1162 """Path-limited visualization."""
1164 def __init__(self
, paths
):
1165 Command
.__init
__(self
)
1166 browser
= utils
.shell_split(prefs
.history_browser())
1168 self
.argv
= browser
+ paths
1173 core
.fork(self
.argv
)
1176 class VisualizeRevision(Command
):
1177 """Visualize a specific revision."""
1179 def __init__(self
, revision
, paths
=None):
1180 Command
.__init
__(self
)
1181 self
.revision
= revision
1185 argv
= utils
.shell_split(prefs
.history_browser())
1187 argv
.append(self
.revision
)
1190 argv
.extend(self
.paths
)
1194 except Exception as e
:
1195 _
, details
= utils
.format_exception(e
)
1196 title
= N_('Error Launching History Browser')
1197 msg
= (N_('Cannot exec "%s": please configure a history browser') %
1199 Interaction
.critical(title
, message
=msg
, details
=details
)
1202 def run(cls
, *args
, **opts
):
1204 Returns a callback that runs a command
1206 If the caller of run() provides args or opts then those are
1207 used instead of the ones provided by the invoker of the callback.
1210 def runner(*local_args
, **local_opts
):
1212 do(cls
, *args
, **opts
)
1214 do(cls
, *local_args
, **local_opts
)
1219 class CommandDisabled(object):
1221 """Context manager to temporarily disable a command from running"""
1222 def __init__(self
, cmdclass
):
1223 self
.cmdclass
= cmdclass
1225 def __enter__(self
):
1226 self
.cmdclass
.DISABLED
= True
1229 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
1230 self
.cmdclass
.DISABLED
= False
1233 def do(cls
, *args
, **opts
):
1234 """Run a command in-place"""
1235 return do_cmd(cls(*args
, **opts
))
1239 if hasattr(cmd
, 'DISABLED') and cmd
.DISABLED
:
1243 except StandardError, e
:
1244 msg
, details
= utils
.format_exception(e
)
1245 Interaction
.critical(N_('Error'), message
=msg
, details
=details
)