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
.basecmd
import BaseCommand
18 from cola
.compat
import set
19 from cola
.diffparse
import DiffParser
20 from cola
.git
import STDOUT
21 from cola
.i18n
import N_
22 from cola
.interaction
import Interaction
23 from cola
.models
import prefs
24 from cola
.models
import selection
26 _notifier
= cola
.notifier()
27 _config
= gitcfg
.instance()
30 class Command(BaseCommand
):
31 """Base class for commands that modify the main model"""
34 """Initialize the command and stash away values for use in do()"""
35 # These are commonly used so let's make it easier to write new commands.
36 BaseCommand
.__init
__(self
)
37 self
.model
= cola
.model()
39 self
.old_diff_text
= self
.model
.diff_text
40 self
.old_filename
= self
.model
.filename
41 self
.old_mode
= self
.model
.mode
42 self
.old_head
= self
.model
.head
44 self
.new_diff_text
= self
.old_diff_text
45 self
.new_filename
= self
.old_filename
46 self
.new_head
= self
.old_head
47 self
.new_mode
= self
.old_mode
50 """Perform the operation."""
51 self
.model
.set_filename(self
.new_filename
)
52 self
.model
.set_head(self
.new_head
)
53 self
.model
.set_mode(self
.new_mode
)
54 self
.model
.set_diff_text(self
.new_diff_text
)
57 """Undo the operation."""
58 self
.model
.set_diff_text(self
.old_diff_text
)
59 self
.model
.set_filename(self
.old_filename
)
60 self
.model
.set_head(self
.old_head
)
61 self
.model
.set_mode(self
.old_mode
)
64 class AmendMode(Command
):
65 """Try to amend a commit."""
75 def __init__(self
, amend
):
76 Command
.__init
__(self
)
80 self
.old_commitmsg
= self
.model
.commitmsg
83 self
.new_mode
= self
.model
.mode_amend
84 self
.new_head
= 'HEAD^'
85 self
.new_commitmsg
= self
.model
.prev_commitmsg()
86 AmendMode
.LAST_MESSAGE
= self
.model
.commitmsg
88 # else, amend unchecked, regular commit
89 self
.new_mode
= self
.model
.mode_none
90 self
.new_head
= 'HEAD'
91 self
.new_diff_text
= ''
92 self
.new_commitmsg
= self
.model
.commitmsg
93 # If we're going back into new-commit-mode then search the
94 # undo stack for a previous amend-commit-mode and grab the
95 # commit message at that point in time.
96 if AmendMode
.LAST_MESSAGE
is not None:
97 self
.new_commitmsg
= AmendMode
.LAST_MESSAGE
98 AmendMode
.LAST_MESSAGE
= None
101 """Leave/enter amend mode."""
102 """Attempt to enter amend mode. Do not allow this when merging."""
104 if core
.exists(self
.model
.git
.git_path('MERGE_HEAD')):
106 _notifier
.broadcast(_notifier
.AMEND
, False)
107 Interaction
.information(
109 N_('You are in the middle of a merge.\n'
110 'Cannot amend while merging.'))
113 _notifier
.broadcast(_notifier
.AMEND
, self
.amending
)
114 self
.model
.set_commitmsg(self
.new_commitmsg
)
116 self
.model
.update_file_status()
121 self
.model
.set_commitmsg(self
.old_commitmsg
)
123 self
.model
.update_file_status()
126 class ApplyDiffSelection(Command
):
128 def __init__(self
, staged
, selected
, offset
, selection
, apply_to_worktree
):
129 Command
.__init
__(self
)
131 self
.selected
= selected
133 self
.selection
= selection
134 self
.apply_to_worktree
= apply_to_worktree
137 # The normal worktree vs index scenario
138 parser
= DiffParser(self
.model
,
139 filename
=self
.model
.filename
,
141 reverse
=self
.apply_to_worktree
)
143 parser
.process_diff_selection(self
.selected
,
146 apply_to_worktree
=self
.apply_to_worktree
)
147 Interaction
.log_status(status
, out
, err
)
148 self
.model
.update_file_status(update_index
=True)
151 class ApplyPatches(Command
):
153 def __init__(self
, patches
):
154 Command
.__init
__(self
)
156 self
.patches
= patches
160 num_patches
= len(self
.patches
)
161 orig_head
= self
.model
.git
.rev_parse('HEAD')[STDOUT
]
163 for idx
, patch
in enumerate(self
.patches
):
164 status
, out
, err
= self
.model
.git
.am(patch
)
165 # Log the git-am command
166 Interaction
.log_status(status
, out
, err
)
169 diff
= self
.model
.git
.diff('HEAD^!', stat
=True)[STDOUT
]
170 diff_text
+= (N_('PATCH %(current)d/%(count)d') %
171 dict(current
=idx
+1, count
=num_patches
))
172 diff_text
+= ' - %s:\n%s\n\n' % (os
.path
.basename(patch
), diff
)
174 diff_text
+= N_('Summary:') + '\n'
175 diff_text
+= self
.model
.git
.diff(orig_head
, stat
=True)[STDOUT
]
178 self
.model
.set_diff_text(diff_text
)
179 self
.model
.update_file_status()
181 basenames
= '\n'.join([os
.path
.basename(p
) for p
in self
.patches
])
182 Interaction
.information(
183 N_('Patch(es) Applied'),
184 (N_('%d patch(es) applied.') + '\n\n%s') %
185 (len(self
.patches
), basenames
))
188 class Archive(BaseCommand
):
190 def __init__(self
, ref
, fmt
, prefix
, filename
):
191 BaseCommand
.__init
__(self
)
195 self
.filename
= filename
198 fp
= core
.xopen(self
.filename
, 'wb')
199 cmd
= ['git', 'archive', '--format='+self
.fmt
]
200 if self
.fmt
in ('tgz', 'tar.gz'):
203 cmd
.append('--prefix=' + self
.prefix
)
205 proc
= utils
.start_command(cmd
, stdout
=fp
)
206 out
, err
= proc
.communicate()
208 status
= proc
.returncode
209 Interaction
.log_status(status
, out
or '', err
or '')
212 class Checkout(Command
):
214 A command object for git-checkout.
216 'argv' is handed off directly to git.
220 def __init__(self
, argv
, checkout_branch
=False):
221 Command
.__init
__(self
)
223 self
.checkout_branch
= checkout_branch
224 self
.new_diff_text
= ''
227 status
, out
, err
= self
.model
.git
.checkout(*self
.argv
)
228 Interaction
.log_status(status
, out
, err
)
229 if self
.checkout_branch
:
230 self
.model
.update_status()
232 self
.model
.update_file_status()
235 class CheckoutBranch(Checkout
):
236 """Checkout a branch."""
238 def __init__(self
, branch
):
240 Checkout
.__init
__(self
, args
, checkout_branch
=True)
243 class CherryPick(Command
):
244 """Cherry pick commits into the current branch."""
246 def __init__(self
, commits
):
247 Command
.__init
__(self
)
248 self
.commits
= commits
251 self
.model
.cherry_pick_list(self
.commits
)
252 self
.model
.update_file_status()
255 class ResetMode(Command
):
256 """Reset the mode and clear the model's diff text."""
259 Command
.__init
__(self
)
260 self
.new_mode
= self
.model
.mode_none
261 self
.new_head
= 'HEAD'
262 self
.new_diff_text
= ''
266 self
.model
.update_file_status()
269 class Commit(ResetMode
):
270 """Attempt to create a new commit."""
272 SHORTCUT
= 'Ctrl+Return'
274 def __init__(self
, amend
, msg
):
275 ResetMode
.__init
__(self
)
278 self
.old_commitmsg
= self
.model
.commitmsg
279 self
.new_commitmsg
= ''
282 tmpfile
= utils
.tmp_filename('commit-message')
283 status
, out
, err
= self
.model
.commit_with_msg(self
.msg
, tmpfile
,
287 self
.model
.set_commitmsg(self
.new_commitmsg
)
288 msg
= N_('Created commit: %s') % out
290 msg
= N_('Commit failed: %s') % out
291 Interaction
.log_status(status
, msg
, err
)
293 return status
, out
, err
296 class Ignore(Command
):
297 """Add files to .gitignore"""
299 def __init__(self
, filenames
):
300 Command
.__init
__(self
)
301 self
.filenames
= filenames
305 for fname
in self
.filenames
:
306 new_additions
= new_additions
+ fname
+ '\n'
307 for_status
= new_additions
309 if core
.exists('.gitignore'):
310 current_list
= core
.read('.gitignore')
311 new_additions
= new_additions
+ current_list
312 core
.write('.gitignore', new_additions
)
313 Interaction
.log_status(
314 0, 'Added to .gitignore:\n%s' % for_status
, '')
315 self
.model
.update_file_status()
318 class Delete(Command
):
321 def __init__(self
, filenames
):
322 Command
.__init
__(self
)
323 self
.filenames
= filenames
324 # We could git-hash-object stuff and provide undo-ability
328 for filename
in self
.filenames
:
334 Interaction
.information(
336 N_('Deleting "%s" failed') % filename
)
338 self
.model
.update_file_status()
341 class DeleteBranch(Command
):
342 """Delete a git branch."""
344 def __init__(self
, branch
):
345 Command
.__init
__(self
)
349 status
, out
, err
= self
.model
.delete_branch(self
.branch
)
350 Interaction
.log_status(status
, out
, err
)
353 class DeleteRemoteBranch(Command
):
354 """Delete a remote git branch."""
356 def __init__(self
, remote
, branch
):
357 Command
.__init
__(self
)
362 status
, out
, err
= self
.model
.git
.push(self
.remote
, self
.branch
,
364 Interaction
.log_status(status
, out
, err
)
365 self
.model
.update_status()
368 Interaction
.information(
369 N_('Remote Branch Deleted'),
370 N_('"%(branch)s" has been deleted from "%(remote)s".')
371 % dict(branch
=self
.branch
, remote
=self
.remote
))
374 message
= (N_('"%(command)s" returned exit status %(status)d') %
375 dict(command
=command
, status
=status
))
377 Interaction
.critical(N_('Error Deleting Remote Branch'),
383 """Perform a diff and set the model's current text."""
385 def __init__(self
, filenames
, cached
=False):
386 Command
.__init
__(self
)
387 # Guard against the list of files being empty
392 opts
['ref'] = self
.model
.head
393 self
.new_filename
= filenames
[0]
394 self
.old_filename
= self
.model
.filename
395 self
.new_mode
= self
.model
.mode_worktree
396 self
.new_diff_text
= gitcmds
.diff_helper(filename
=self
.new_filename
,
397 cached
=cached
, **opts
)
400 class Diffstat(Command
):
401 """Perform a diffstat and set the model's diff text."""
404 Command
.__init
__(self
)
405 diff
= self
.model
.git
.diff(self
.model
.head
,
406 unified
=_config
.get('diff.context', 3),
411 self
.new_diff_text
= diff
412 self
.new_mode
= self
.model
.mode_worktree
415 class DiffStaged(Diff
):
416 """Perform a staged diff on a file."""
418 def __init__(self
, filenames
):
419 Diff
.__init
__(self
, filenames
, cached
=True)
420 self
.new_mode
= self
.model
.mode_index
423 class DiffStagedSummary(Command
):
426 Command
.__init
__(self
)
427 diff
= self
.model
.git
.diff(self
.model
.head
,
431 patch_with_stat
=True,
433 self
.new_diff_text
= diff
434 self
.new_mode
= self
.model
.mode_index
437 class Difftool(Command
):
438 """Run git-difftool limited by path."""
440 def __init__(self
, staged
, filenames
):
441 Command
.__init
__(self
)
443 self
.filenames
= filenames
446 difftool
.launch_with_head(self
.filenames
,
447 self
.staged
, self
.model
.head
)
451 """Edit a file using the configured gui.editor."""
458 def __init__(self
, filenames
, line_number
=None):
459 Command
.__init
__(self
)
460 self
.filenames
= filenames
461 self
.line_number
= line_number
464 if not self
.filenames
:
466 filename
= self
.filenames
[0]
467 if not core
.exists(filename
):
469 editor
= prefs
.editor()
472 if self
.line_number
is None:
473 opts
= self
.filenames
475 # Single-file w/ line-numbers (likely from grep)
477 '*vim*': ['+'+self
.line_number
, filename
],
478 '*emacs*': ['+'+self
.line_number
, filename
],
479 '*textpad*': ['%s(%s,0)' % (filename
, self
.line_number
)],
480 '*notepad++*': ['-n'+self
.line_number
, filename
],
483 opts
= self
.filenames
484 for pattern
, opt
in editor_opts
.items():
485 if fnmatch(editor
, pattern
):
490 utils
.fork(utils
.shell_split(editor
) + opts
)
491 except Exception as e
:
492 message
= (N_('Cannot exec "%s": please configure your editor') %
494 Interaction
.critical(N_('Error Editing File'),
498 class FormatPatch(Command
):
499 """Output a patch series given all revisions and a selected subset."""
501 def __init__(self
, to_export
, revs
):
502 Command
.__init
__(self
)
503 self
.to_export
= to_export
507 status
, out
, err
= gitcmds
.format_patchsets(self
.to_export
, self
.revs
)
508 Interaction
.log_status(status
, out
, err
)
511 class LaunchDifftool(BaseCommand
):
517 return N_('Launch Diff Tool')
520 BaseCommand
.__init
__(self
)
527 utils
.fork(['git', 'mergetool', '--no-prompt', '--'] + paths
)
529 utils
.fork(['xterm', '-e',
530 'git', 'mergetool', '--no-prompt', '--'] + paths
)
535 class LaunchEditor(Edit
):
540 return N_('Launch Editor')
544 allfiles
= s
.staged
+ s
.unmerged
+ s
.modified
+ s
.untracked
545 Edit
.__init
__(self
, allfiles
)
548 class LoadCommitMessageFromFile(Command
):
549 """Loads a commit message from a path."""
551 def __init__(self
, path
):
552 Command
.__init
__(self
)
555 self
.old_commitmsg
= self
.model
.commitmsg
556 self
.old_directory
= self
.model
.directory
560 if not path
or not core
.isfile(path
):
561 raise errors
.UsageError(N_('Error: Cannot find commit template'),
562 N_('%s: No such file or directory.') % path
)
563 self
.model
.set_directory(os
.path
.dirname(path
))
564 self
.model
.set_commitmsg(core
.read(path
))
567 self
.model
.set_commitmsg(self
.old_commitmsg
)
568 self
.model
.set_directory(self
.old_directory
)
571 class LoadCommitMessageFromTemplate(LoadCommitMessageFromFile
):
572 """Loads the commit message template specified by commit.template."""
575 template
= _config
.get('commit.template')
576 LoadCommitMessageFromFile
.__init
__(self
, template
)
579 if self
.path
is None:
580 raise errors
.UsageError(
581 N_('Error: Unconfigured commit template'),
582 N_('A commit template has not been configured.\n'
583 'Use "git config" to define "commit.template"\n'
584 'so that it points to a commit template.'))
585 return LoadCommitMessageFromFile
.do(self
)
589 class LoadCommitMessageFromSHA1(Command
):
590 """Load a previous commit message"""
592 def __init__(self
, sha1
, prefix
=''):
593 Command
.__init
__(self
)
595 self
.old_commitmsg
= self
.model
.commitmsg
596 self
.new_commitmsg
= prefix
+ self
.model
.prev_commitmsg(sha1
)
600 self
.model
.set_commitmsg(self
.new_commitmsg
)
603 self
.model
.set_commitmsg(self
.old_commitmsg
)
606 class LoadFixupMessage(LoadCommitMessageFromSHA1
):
607 """Load a fixup message"""
609 def __init__(self
, sha1
):
610 LoadCommitMessageFromSHA1
.__init__(self
, sha1
, prefix
='fixup! ')
613 class Merge(Command
):
614 def __init__(self
, revision
, no_commit
, squash
):
615 Command
.__init
__(self
)
616 self
.revision
= revision
617 self
.no_commit
= no_commit
622 revision
= self
.revision
623 no_commit
= self
.no_commit
624 msg
= gitcmds
.merge_message(revision
)
626 status
, out
, err
= self
.model
.git
.merge('-m', msg
,
631 Interaction
.log_status(status
, out
, err
)
632 self
.model
.update_status()
635 class OpenDefaultApp(BaseCommand
):
636 """Open a file using the OS default."""
641 return N_('Open Using Default Application')
643 def __init__(self
, filenames
):
644 BaseCommand
.__init
__(self
)
645 if utils
.is_darwin():
648 launcher
= 'xdg-open'
649 self
.launcher
= launcher
650 self
.filenames
= filenames
653 if not self
.filenames
:
655 utils
.fork([self
.launcher
] + self
.filenames
)
658 class OpenParentDir(OpenDefaultApp
):
659 """Open parent directories using the OS default."""
660 SHORTCUT
= 'Shift+Space'
664 return N_('Open Parent Directory')
666 def __init__(self
, filenames
):
667 OpenDefaultApp
.__init
__(self
, filenames
)
670 if not self
.filenames
:
672 dirs
= set(map(os
.path
.dirname
, self
.filenames
))
673 utils
.fork([self
.launcher
] + dirs
)
676 class OpenRepo(Command
):
677 """Launches git-cola on a repo."""
679 def __init__(self
, repo_path
):
680 Command
.__init
__(self
)
681 self
.repo_path
= repo_path
684 self
.model
.set_directory(self
.repo_path
)
685 utils
.fork([sys
.executable
, sys
.argv
[0], '--repo', self
.repo_path
])
688 class Clone(Command
):
689 """Clones a repository and optionally spawns a new cola session."""
691 def __init__(self
, url
, new_directory
, spawn
=True):
692 Command
.__init
__(self
)
694 self
.new_directory
= new_directory
698 status
, out
, err
= self
.model
.git
.clone(self
.url
, self
.new_directory
)
700 Interaction
.information(
701 N_('Error: could not clone "%s"') % self
.url
,
702 (N_('git clone returned exit code %s') % status
) +
703 ((out
+err
) and ('\n\n' + out
+ err
) or ''))
706 utils
.fork([sys
.executable
, sys
.argv
[0],
707 '--repo', self
.new_directory
])
711 class GitXBaseContext(object):
713 def __init__(self
, **kwargs
):
717 compat
.setenv('GIT_SEQUENCE_EDITOR',
718 resources
.share('bin', 'git-xbase'))
719 for var
, value
in self
.extras
.items():
720 compat
.setenv(var
, value
)
723 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
724 compat
.unsetenv('GIT_SEQUENCE_EDITOR')
725 for var
in self
.extras
:
729 class Rebase(Command
):
731 def __init__(self
, branch
):
732 Command
.__init
__(self
)
742 with
GitXBaseContext(
743 GIT_EDITOR
=prefs
.editor(),
744 GIT_XBASE_TITLE
=N_('Rebase onto %s') % branch
,
745 GIT_XBASE_ACTION
=N_('Rebase')):
746 status
, out
, err
= self
.model
.git
.rebase(branch
,
749 Interaction
.log_status(status
, out
, err
)
750 self
.model
.update_status()
751 return status
, out
, err
754 class RebaseEditTodo(Command
):
757 with
GitXBaseContext(
758 GIT_XBASE_TITLE
=N_('Edit Rebase'),
759 GIT_XBASE_ACTION
=N_('Save')):
760 status
, out
, err
= self
.model
.git
.rebase(edit_todo
=True)
761 Interaction
.log_status(status
, out
, err
)
762 self
.model
.update_status()
765 class RebaseContinue(Command
):
768 status
, out
, err
= self
.model
.git
.rebase('--continue')
769 Interaction
.log_status(status
, out
, err
)
770 self
.model
.update_status()
773 class RebaseSkip(Command
):
776 status
, out
, err
= self
.model
.git
.rebase(skip
=True)
777 Interaction
.log_status(status
, out
, err
)
778 self
.model
.update_status()
781 class RebaseAbort(Command
):
784 status
, out
, err
= self
.model
.git
.rebase(abort
=True)
785 Interaction
.log_status(status
, out
, err
)
786 self
.model
.update_status()
789 class Rescan(Command
):
790 """Rescan for changes"""
793 self
.model
.update_status()
796 class Refresh(Command
):
797 """Update refs and refresh the index"""
806 self
.model
.update_status(update_index
=True)
809 class RunConfigAction(Command
):
810 """Run a user-configured action, typically from the "Tools" menu"""
812 def __init__(self
, action_name
):
813 Command
.__init
__(self
)
814 self
.action_name
= action_name
815 self
.model
= cola
.model()
818 for env
in ('FILENAME', 'REVISION', 'ARGS'):
825 opts
= _config
.get_guitool_opts(self
.action_name
)
826 cmd
= opts
.get('cmd')
827 if 'title' not in opts
:
830 if 'prompt' not in opts
or opts
.get('prompt') is True:
831 prompt
= N_('Run "%s"?') % cmd
832 opts
['prompt'] = prompt
834 if opts
.get('needsfile'):
835 filename
= selection
.filename()
837 Interaction
.information(
838 N_('Please select a file'),
839 N_('"%s" requires a selected file.') % cmd
)
841 compat
.setenv('FILENAME', filename
)
843 if opts
.get('revprompt') or opts
.get('argprompt'):
845 ok
= Interaction
.confirm_config_action(cmd
, opts
)
848 rev
= opts
.get('revision')
849 args
= opts
.get('args')
850 if opts
.get('revprompt') and not rev
:
851 title
= N_('Invalid Revision')
852 msg
= N_('The revision expression cannot be empty.')
853 Interaction
.critical(title
, msg
)
857 elif opts
.get('confirm'):
858 title
= os
.path
.expandvars(opts
.get('title'))
859 prompt
= os
.path
.expandvars(opts
.get('prompt'))
860 if Interaction
.question(title
, prompt
):
863 compat
.setenv('REVISION', rev
)
865 compat
.setenv('ARGS', args
)
866 title
= os
.path
.expandvars(cmd
)
867 Interaction
.log(N_('Running command: %s') % title
)
868 cmd
= ['sh', '-c', cmd
]
870 if opts
.get('noconsole'):
871 status
, out
, err
= utils
.run_command(cmd
)
873 status
, out
, err
= Interaction
.run_command(title
, cmd
)
875 Interaction
.log_status(status
,
876 out
and (N_('Output: %s') % out
) or '',
877 err
and (N_('Errors: %s') % err
) or '')
879 if not opts
.get('norescan'):
880 self
.model
.update_status()
884 class SetDiffText(Command
):
886 def __init__(self
, text
):
887 Command
.__init
__(self
)
889 self
.new_diff_text
= text
892 class ShowUntracked(Command
):
893 """Show an untracked file."""
895 def __init__(self
, filenames
):
896 Command
.__init
__(self
)
897 self
.filenames
= filenames
898 self
.new_mode
= self
.model
.mode_untracked
899 self
.new_diff_text
= ''
901 self
.new_diff_text
= self
.diff_text_for(filenames
[0])
903 def diff_text_for(self
, filename
):
904 size
= _config
.get('cola.readsize', 1024 * 2)
906 result
= core
.read(filename
, size
=size
)
910 if len(result
) == size
:
915 class SignOff(Command
):
920 return N_('Sign Off')
923 Command
.__init
__(self
)
925 self
.old_commitmsg
= self
.model
.commitmsg
928 signoff
= self
.signoff()
929 if signoff
in self
.model
.commitmsg
:
931 self
.model
.set_commitmsg(self
.model
.commitmsg
+ '\n' + signoff
)
934 self
.model
.set_commitmsg(self
.old_commitmsg
)
939 user
= pwd
.getpwuid(os
.getuid()).pw_name
941 user
= os
.getenv('USER', N_('unknown'))
943 name
= _config
.get('user.name', user
)
944 email
= _config
.get('user.email', '%s@%s' % (user
, platform
.node()))
945 return '\nSigned-off-by: %s <%s>' % (name
, email
)
948 class Stage(Command
):
949 """Stage a set of paths."""
956 def __init__(self
, paths
):
957 Command
.__init
__(self
)
961 msg
= N_('Staging: %s') % (', '.join(self
.paths
))
963 # Prevent external updates while we are staging files.
964 # We update file stats at the end of this operation
965 # so there's no harm in ignoring updates from other threads
967 with
CommandDisabled(UpdateFileStatus
):
968 self
.model
.stage_paths(self
.paths
)
971 class StageModified(Stage
):
972 """Stage all modified files."""
978 return N_('Stage Modified')
981 Stage
.__init
__(self
, None)
982 self
.paths
= self
.model
.modified
985 class StageUnmerged(Stage
):
986 """Stage all modified files."""
992 return N_('Stage Unmerged')
995 Stage
.__init
__(self
, None)
996 self
.paths
= self
.model
.unmerged
999 class StageUntracked(Stage
):
1000 """Stage all untracked files."""
1006 return N_('Stage Untracked')
1009 Stage
.__init
__(self
, None)
1010 self
.paths
= self
.model
.untracked
1014 """Create a tag object."""
1016 def __init__(self
, name
, revision
, sign
=False, message
=''):
1017 Command
.__init
__(self
)
1019 self
._message
= message
1020 self
._revision
= revision
1024 log_msg
= (N_('Tagging "%(revision)s" as "%(name)s"') %
1025 dict(revision
=self
._revision
, name
=self
._name
))
1028 opts
['F'] = utils
.tmp_filename('tag-message')
1029 core
.write(opts
['F'], self
._message
)
1032 log_msg
+= ' (%s)' % N_('GPG-signed')
1034 status
, output
, err
= self
.model
.git
.tag(self
._name
,
1035 self
._revision
, **opts
)
1037 opts
['a'] = bool(self
._message
)
1038 status
, output
, err
= self
.model
.git
.tag(self
._name
,
1039 self
._revision
, **opts
)
1041 os
.unlink(opts
['F'])
1044 log_msg
+= '\n' + (N_('Output: %s') % output
)
1046 Interaction
.log_status(status
, log_msg
, err
)
1048 self
.model
.update_status()
1051 class Unstage(Command
):
1052 """Unstage a set of paths."""
1058 return N_('Unstage')
1060 def __init__(self
, paths
):
1061 Command
.__init
__(self
)
1065 msg
= N_('Unstaging: %s') % (', '.join(self
.paths
))
1066 Interaction
.log(msg
)
1067 with
CommandDisabled(UpdateFileStatus
):
1068 self
.model
.unstage_paths(self
.paths
)
1071 class UnstageAll(Command
):
1072 """Unstage all files; resets the index."""
1075 self
.model
.unstage_all()
1078 class UnstageSelected(Unstage
):
1079 """Unstage selected files."""
1082 Unstage
.__init
__(self
, cola
.selection_model().staged
)
1085 class Untrack(Command
):
1086 """Unstage a set of paths."""
1088 def __init__(self
, paths
):
1089 Command
.__init
__(self
)
1093 msg
= N_('Untracking: %s') % (', '.join(self
.paths
))
1094 Interaction
.log(msg
)
1095 with
CommandDisabled(UpdateFileStatus
):
1096 status
, out
, err
= self
.model
.untrack_paths(self
.paths
)
1097 Interaction
.log_status(status
, out
, err
)
1100 class UntrackedSummary(Command
):
1101 """List possible .gitignore rules as the diff text."""
1104 Command
.__init
__(self
)
1105 untracked
= self
.model
.untracked
1106 suffix
= len(untracked
) > 1 and 's' or ''
1108 io
.write('# %s untracked file%s\n' % (len(untracked
), suffix
))
1110 io
.write('# possible .gitignore rule%s:\n' % suffix
)
1112 io
.write('/'+core
.encode(u
)+'\n')
1113 self
.new_diff_text
= core
.decode(io
.getvalue())
1114 self
.new_mode
= self
.model
.mode_untracked
1117 class UpdateFileStatus(Command
):
1118 """Rescans for changes."""
1121 self
.model
.update_file_status()
1124 class VisualizeAll(Command
):
1125 """Visualize all branches."""
1128 browser
= utils
.shell_split(prefs
.history_browser())
1129 utils
.fork(browser
+ ['--all'])
1132 class VisualizeCurrent(Command
):
1133 """Visualize all branches."""
1136 browser
= utils
.shell_split(prefs
.history_browser())
1137 utils
.fork(browser
+ [self
.model
.currentbranch
])
1140 class VisualizePaths(Command
):
1141 """Path-limited visualization."""
1143 def __init__(self
, paths
):
1144 Command
.__init
__(self
)
1145 browser
= utils
.shell_split(prefs
.history_browser())
1147 self
.argv
= browser
+ paths
1152 utils
.fork(self
.argv
)
1155 class VisualizeRevision(Command
):
1156 """Visualize a specific revision."""
1158 def __init__(self
, revision
, paths
=None):
1159 Command
.__init
__(self
)
1160 self
.revision
= revision
1164 argv
= utils
.shell_split(prefs
.history_browser())
1166 argv
.append(self
.revision
)
1169 argv
.extend(self
.paths
)
1173 except Exception as e
:
1174 _
, details
= utils
.format_exception(e
)
1175 title
= N_('Error Launching History Browser')
1176 msg
= (N_('Cannot exec "%s": please configure a history browser') %
1178 Interaction
.critical(title
, message
=msg
, details
=details
)
1181 def run(cls
, *args
, **opts
):
1183 Returns a callback that runs a command
1185 If the caller of run() provides args or opts then those are
1186 used instead of the ones provided by the invoker of the callback.
1189 def runner(*local_args
, **local_opts
):
1191 do(cls
, *args
, **opts
)
1193 do(cls
, *local_args
, **local_opts
)
1198 class CommandDisabled(object):
1200 """Context manager to temporarily disable a command from running"""
1201 def __init__(self
, cmdclass
):
1202 self
.cmdclass
= cmdclass
1204 def __enter__(self
):
1205 self
.cmdclass
.DISABLED
= True
1208 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
1209 self
.cmdclass
.DISABLED
= False
1212 def do(cls
, *args
, **opts
):
1213 """Run a command in-place"""
1214 return do_cmd(cls(*args
, **opts
))
1218 if hasattr(cmd
, 'DISABLED') and cmd
.DISABLED
:
1222 except StandardError, e
:
1223 msg
, details
= utils
.format_exception(e
)
1224 Interaction
.critical(N_('Error'), message
=msg
, details
=details
)