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
)
177 self
.patches
= patches
181 num_patches
= len(self
.patches
)
182 orig_head
= self
.model
.git
.rev_parse('HEAD')[STDOUT
]
184 for idx
, patch
in enumerate(self
.patches
):
185 status
, out
, err
= self
.model
.git
.am(patch
)
186 # Log the git-am command
187 Interaction
.log_status(status
, out
, err
)
190 diff
= self
.model
.git
.diff('HEAD^!', stat
=True)[STDOUT
]
191 diff_text
+= (N_('PATCH %(current)d/%(count)d') %
192 dict(current
=idx
+1, count
=num_patches
))
193 diff_text
+= ' - %s:\n%s\n\n' % (os
.path
.basename(patch
), diff
)
195 diff_text
+= N_('Summary:') + '\n'
196 diff_text
+= self
.model
.git
.diff(orig_head
, stat
=True)[STDOUT
]
199 self
.model
.set_diff_text(diff_text
)
200 self
.model
.update_file_status()
202 basenames
= '\n'.join([os
.path
.basename(p
) for p
in self
.patches
])
203 Interaction
.information(
204 N_('Patch(es) Applied'),
205 (N_('%d patch(es) applied.') + '\n\n%s') %
206 (len(self
.patches
), basenames
))
209 class Archive(BaseCommand
):
211 def __init__(self
, ref
, fmt
, prefix
, filename
):
212 BaseCommand
.__init
__(self
)
216 self
.filename
= filename
219 fp
= core
.xopen(self
.filename
, 'wb')
220 cmd
= ['git', 'archive', '--format='+self
.fmt
]
221 if self
.fmt
in ('tgz', 'tar.gz'):
224 cmd
.append('--prefix=' + self
.prefix
)
226 proc
= core
.start_command(cmd
, stdout
=fp
)
227 out
, err
= proc
.communicate()
229 status
= proc
.returncode
230 Interaction
.log_status(status
, out
or '', err
or '')
233 class Checkout(Command
):
235 A command object for git-checkout.
237 'argv' is handed off directly to git.
241 def __init__(self
, argv
, checkout_branch
=False):
242 Command
.__init
__(self
)
244 self
.checkout_branch
= checkout_branch
245 self
.new_diff_text
= ''
248 status
, out
, err
= self
.model
.git
.checkout(*self
.argv
)
249 Interaction
.log_status(status
, out
, err
)
250 if self
.checkout_branch
:
251 self
.model
.update_status()
253 self
.model
.update_file_status()
256 class CheckoutBranch(Checkout
):
257 """Checkout a branch."""
259 def __init__(self
, branch
):
261 Checkout
.__init
__(self
, args
, checkout_branch
=True)
264 class CherryPick(Command
):
265 """Cherry pick commits into the current branch."""
267 def __init__(self
, commits
):
268 Command
.__init
__(self
)
269 self
.commits
= commits
272 self
.model
.cherry_pick_list(self
.commits
)
273 self
.model
.update_file_status()
276 class ResetMode(Command
):
277 """Reset the mode and clear the model's diff text."""
280 Command
.__init
__(self
)
281 self
.new_mode
= self
.model
.mode_none
282 self
.new_diff_text
= ''
286 self
.model
.update_file_status()
289 class RevertUnstagedEdits(Command
):
294 if not self
.model
.undoable():
296 s
= selection
.selection()
298 items_to_undo
= s
.staged
300 items_to_undo
= s
.modified
302 if not Interaction
.confirm(N_('Revert Unstaged Changes?'),
303 N_('This operation drops unstaged changes.\n'
304 'These changes cannot be recovered.'),
305 N_('Revert the unstaged changes?'),
306 N_('Revert Unstaged Changes'),
308 icon
=resources
.icon('undo.svg')):
311 if not s
.staged
and self
.model
.amending():
312 args
.append(self
.model
.head
)
313 do(Checkout
, args
+ ['--'] + items_to_undo
)
315 msg
= N_('No files selected for checkout from HEAD.')
319 class Commit(ResetMode
):
320 """Attempt to create a new commit."""
322 SHORTCUT
= 'Ctrl+Return'
324 def __init__(self
, amend
, msg
):
325 ResetMode
.__init
__(self
)
328 self
.old_commitmsg
= self
.model
.commitmsg
329 self
.new_commitmsg
= ''
332 tmpfile
= utils
.tmp_filename('commit-message')
333 status
, out
, err
= self
.model
.commit_with_msg(self
.msg
, tmpfile
,
337 self
.model
.set_commitmsg(self
.new_commitmsg
)
338 msg
= N_('Created commit: %s') % out
340 msg
= N_('Commit failed: %s') % out
341 Interaction
.log_status(status
, msg
, err
)
343 return status
, out
, err
346 class Ignore(Command
):
347 """Add files to .gitignore"""
349 def __init__(self
, filenames
):
350 Command
.__init
__(self
)
351 self
.filenames
= filenames
355 for fname
in self
.filenames
:
356 new_additions
= new_additions
+ fname
+ '\n'
357 for_status
= new_additions
359 if core
.exists('.gitignore'):
360 current_list
= core
.read('.gitignore')
361 new_additions
= new_additions
+ current_list
362 core
.write('.gitignore', new_additions
)
363 Interaction
.log_status(
364 0, 'Added to .gitignore:\n%s' % for_status
, '')
365 self
.model
.update_file_status()
368 class Delete(Command
):
371 def __init__(self
, filenames
):
372 Command
.__init
__(self
)
373 self
.filenames
= filenames
374 # We could git-hash-object stuff and provide undo-ability
378 for filename
in self
.filenames
:
384 Interaction
.information(
386 N_('Deleting "%s" failed') % filename
)
388 self
.model
.update_file_status()
391 class DeleteBranch(Command
):
392 """Delete a git branch."""
394 def __init__(self
, branch
):
395 Command
.__init
__(self
)
399 status
, out
, err
= self
.model
.delete_branch(self
.branch
)
400 Interaction
.log_status(status
, out
, err
)
403 class DeleteRemoteBranch(Command
):
404 """Delete a remote git branch."""
406 def __init__(self
, remote
, branch
):
407 Command
.__init
__(self
)
412 status
, out
, err
= self
.model
.git
.push(self
.remote
, self
.branch
,
414 Interaction
.log_status(status
, out
, err
)
415 self
.model
.update_status()
418 Interaction
.information(
419 N_('Remote Branch Deleted'),
420 N_('"%(branch)s" has been deleted from "%(remote)s".')
421 % dict(branch
=self
.branch
, remote
=self
.remote
))
424 message
= (N_('"%(command)s" returned exit status %(status)d') %
425 dict(command
=command
, status
=status
))
427 Interaction
.critical(N_('Error Deleting Remote Branch'),
433 """Perform a diff and set the model's current text."""
435 def __init__(self
, filenames
, cached
=False):
436 Command
.__init
__(self
)
437 # Guard against the list of files being empty
442 opts
['ref'] = self
.model
.head
443 self
.new_filename
= filenames
[0]
444 self
.old_filename
= self
.model
.filename
445 self
.new_mode
= self
.model
.mode_worktree
446 self
.new_diff_text
= gitcmds
.diff_helper(filename
=self
.new_filename
,
447 cached
=cached
, **opts
)
450 class Diffstat(Command
):
451 """Perform a diffstat and set the model's diff text."""
454 Command
.__init
__(self
)
455 diff
= self
.model
.git
.diff(self
.model
.head
,
456 unified
=_config
.get('diff.context', 3),
461 self
.new_diff_text
= diff
462 self
.new_mode
= self
.model
.mode_worktree
465 class DiffStaged(Diff
):
466 """Perform a staged diff on a file."""
468 def __init__(self
, filenames
):
469 Diff
.__init
__(self
, filenames
, cached
=True)
470 self
.new_mode
= self
.model
.mode_index
473 class DiffStagedSummary(Command
):
476 Command
.__init
__(self
)
477 diff
= self
.model
.git
.diff(self
.model
.head
,
481 patch_with_stat
=True,
483 self
.new_diff_text
= diff
484 self
.new_mode
= self
.model
.mode_index
487 class Difftool(Command
):
488 """Run git-difftool limited by path."""
490 def __init__(self
, staged
, filenames
):
491 Command
.__init
__(self
)
493 self
.filenames
= filenames
496 difftool
.launch_with_head(self
.filenames
,
497 self
.staged
, self
.model
.head
)
501 """Edit a file using the configured gui.editor."""
508 def __init__(self
, filenames
, line_number
=None):
509 Command
.__init
__(self
)
510 self
.filenames
= filenames
511 self
.line_number
= line_number
514 if not self
.filenames
:
516 filename
= self
.filenames
[0]
517 if not core
.exists(filename
):
519 editor
= prefs
.editor()
522 if self
.line_number
is None:
523 opts
= self
.filenames
525 # Single-file w/ line-numbers (likely from grep)
527 '*vim*': ['+'+self
.line_number
, filename
],
528 '*emacs*': ['+'+self
.line_number
, filename
],
529 '*textpad*': ['%s(%s,0)' % (filename
, self
.line_number
)],
530 '*notepad++*': ['-n'+self
.line_number
, filename
],
533 opts
= self
.filenames
534 for pattern
, opt
in editor_opts
.items():
535 if fnmatch(editor
, pattern
):
540 core
.fork(utils
.shell_split(editor
) + opts
)
541 except Exception as e
:
542 message
= (N_('Cannot exec "%s": please configure your editor') %
544 Interaction
.critical(N_('Error Editing File'),
548 class FormatPatch(Command
):
549 """Output a patch series given all revisions and a selected subset."""
551 def __init__(self
, to_export
, revs
):
552 Command
.__init
__(self
)
553 self
.to_export
= to_export
557 status
, out
, err
= gitcmds
.format_patchsets(self
.to_export
, self
.revs
)
558 Interaction
.log_status(status
, out
, err
)
561 class LaunchDifftool(BaseCommand
):
567 return N_('Launch Diff Tool')
570 BaseCommand
.__init
__(self
)
573 s
= selection
.selection()
577 core
.fork(['git', 'mergetool', '--no-prompt', '--'] + paths
)
579 core
.fork(['xterm', '-e',
580 'git', 'mergetool', '--no-prompt', '--'] + paths
)
585 class LaunchTerminal(BaseCommand
):
591 return N_('Launch Terminal')
593 def __init__(self
, path
):
597 cmd
= _config
.get('cola.terminal', 'xterm -e $SHELL')
598 cmd
= os
.path
.expandvars(cmd
)
599 argv
= utils
.shell_split(cmd
)
600 core
.fork(argv
, cwd
=self
.path
)
603 class LaunchEditor(Edit
):
608 return N_('Launch Editor')
611 s
= selection
.selection()
612 allfiles
= s
.staged
+ s
.unmerged
+ s
.modified
+ s
.untracked
613 Edit
.__init
__(self
, allfiles
)
616 class LoadCommitMessageFromFile(Command
):
617 """Loads a commit message from a path."""
619 def __init__(self
, path
):
620 Command
.__init
__(self
)
623 self
.old_commitmsg
= self
.model
.commitmsg
624 self
.old_directory
= self
.model
.directory
628 if not path
or not core
.isfile(path
):
629 raise UsageError(N_('Error: Cannot find commit template'),
630 N_('%s: No such file or directory.') % path
)
631 self
.model
.set_directory(os
.path
.dirname(path
))
632 self
.model
.set_commitmsg(core
.read(path
))
635 self
.model
.set_commitmsg(self
.old_commitmsg
)
636 self
.model
.set_directory(self
.old_directory
)
639 class LoadCommitMessageFromTemplate(LoadCommitMessageFromFile
):
640 """Loads the commit message template specified by commit.template."""
643 template
= _config
.get('commit.template')
644 LoadCommitMessageFromFile
.__init
__(self
, template
)
647 if self
.path
is None:
649 N_('Error: Unconfigured commit template'),
650 N_('A commit template has not been configured.\n'
651 'Use "git config" to define "commit.template"\n'
652 'so that it points to a commit template.'))
653 return LoadCommitMessageFromFile
.do(self
)
657 class LoadCommitMessageFromSHA1(Command
):
658 """Load a previous commit message"""
660 def __init__(self
, sha1
, prefix
=''):
661 Command
.__init
__(self
)
663 self
.old_commitmsg
= self
.model
.commitmsg
664 self
.new_commitmsg
= prefix
+ self
.model
.prev_commitmsg(sha1
)
668 self
.model
.set_commitmsg(self
.new_commitmsg
)
671 self
.model
.set_commitmsg(self
.old_commitmsg
)
674 class LoadFixupMessage(LoadCommitMessageFromSHA1
):
675 """Load a fixup message"""
677 def __init__(self
, sha1
):
678 LoadCommitMessageFromSHA1
.__init__(self
, sha1
, prefix
='fixup! ')
681 class Merge(Command
):
682 def __init__(self
, revision
, no_commit
, squash
):
683 Command
.__init
__(self
)
684 self
.revision
= revision
685 self
.no_commit
= no_commit
690 revision
= self
.revision
691 no_commit
= self
.no_commit
692 msg
= gitcmds
.merge_message(revision
)
694 status
, out
, err
= self
.model
.git
.merge('-m', msg
,
699 Interaction
.log_status(status
, out
, err
)
700 self
.model
.update_status()
703 class OpenDefaultApp(BaseCommand
):
704 """Open a file using the OS default."""
709 return N_('Open Using Default Application')
711 def __init__(self
, filenames
):
712 BaseCommand
.__init
__(self
)
713 if utils
.is_darwin():
716 launcher
= 'xdg-open'
717 self
.launcher
= launcher
718 self
.filenames
= filenames
721 if not self
.filenames
:
723 core
.fork([self
.launcher
] + self
.filenames
)
726 class OpenParentDir(OpenDefaultApp
):
727 """Open parent directories using the OS default."""
728 SHORTCUT
= 'Shift+Space'
732 return N_('Open Parent Directory')
734 def __init__(self
, filenames
):
735 OpenDefaultApp
.__init
__(self
, filenames
)
738 if not self
.filenames
:
740 dirs
= set(map(os
.path
.dirname
, self
.filenames
))
741 core
.fork([self
.launcher
] + dirs
)
744 class OpenNewRepo(Command
):
745 """Launches git-cola on a repo."""
747 def __init__(self
, repo_path
):
748 Command
.__init
__(self
)
749 self
.repo_path
= repo_path
752 self
.model
.set_directory(self
.repo_path
)
753 core
.fork([sys
.executable
, sys
.argv
[0], '--repo', self
.repo_path
])
756 class OpenRepo(Command
):
757 def __init__(self
, repo_path
):
758 Command
.__init
__(self
)
759 self
.repo_path
= repo_path
763 old_worktree
= git
.worktree()
764 if not self
.model
.set_worktree(self
.repo_path
):
765 self
.model
.set_worktree(old_worktree
)
767 new_worktree
= git
.worktree()
768 core
.chdir(new_worktree
)
769 self
.model
.set_directory(self
.repo_path
)
771 self
.model
.update_status()
774 class Clone(Command
):
775 """Clones a repository and optionally spawns a new cola session."""
777 def __init__(self
, url
, new_directory
, spawn
=True):
778 Command
.__init
__(self
)
780 self
.new_directory
= new_directory
784 status
, out
, err
= self
.model
.git
.clone(self
.url
, self
.new_directory
)
786 Interaction
.information(
787 N_('Error: could not clone "%s"') % self
.url
,
788 (N_('git clone returned exit code %s') % status
) +
789 ((out
+err
) and ('\n\n' + out
+ err
) or ''))
792 core
.fork([sys
.executable
, sys
.argv
[0],
793 '--repo', self
.new_directory
])
797 class GitXBaseContext(object):
799 def __init__(self
, **kwargs
):
803 compat
.setenv('GIT_SEQUENCE_EDITOR',
804 resources
.share('bin', 'git-xbase'))
805 for var
, value
in self
.extras
.items():
806 compat
.setenv(var
, value
)
809 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
810 compat
.unsetenv('GIT_SEQUENCE_EDITOR')
811 for var
in self
.extras
:
815 class Rebase(Command
):
817 def __init__(self
, branch
, capture_output
=True):
818 Command
.__init
__(self
)
820 self
.capture_output
= capture_output
830 if self
.capture_output
:
831 extra
['_stderr'] = None
832 extra
['_stdout'] = None
833 with
GitXBaseContext(
834 GIT_EDITOR
=prefs
.editor(),
835 GIT_XBASE_TITLE
=N_('Rebase onto %s') % branch
,
836 GIT_XBASE_ACTION
=N_('Rebase')):
837 status
, out
, err
= self
.model
.git
.rebase(branch
,
841 Interaction
.log_status(status
, out
, err
)
842 self
.model
.update_status()
843 return status
, out
, err
846 class RebaseEditTodo(Command
):
849 with
GitXBaseContext(
850 GIT_XBASE_TITLE
=N_('Edit Rebase'),
851 GIT_XBASE_ACTION
=N_('Save')):
852 status
, out
, err
= self
.model
.git
.rebase(edit_todo
=True)
853 Interaction
.log_status(status
, out
, err
)
854 self
.model
.update_status()
857 class RebaseContinue(Command
):
860 status
, out
, err
= self
.model
.git
.rebase('--continue')
861 Interaction
.log_status(status
, out
, err
)
862 self
.model
.update_status()
865 class RebaseSkip(Command
):
868 status
, out
, err
= self
.model
.git
.rebase(skip
=True)
869 Interaction
.log_status(status
, out
, err
)
870 self
.model
.update_status()
873 class RebaseAbort(Command
):
876 status
, out
, err
= self
.model
.git
.rebase(abort
=True)
877 Interaction
.log_status(status
, out
, err
)
878 self
.model
.update_status()
881 class Rescan(Command
):
882 """Rescan for changes"""
885 self
.model
.update_status()
888 class Refresh(Command
):
889 """Update refs and refresh the index"""
898 self
.model
.update_status(update_index
=True)
901 class RunConfigAction(Command
):
902 """Run a user-configured action, typically from the "Tools" menu"""
904 def __init__(self
, action_name
):
905 Command
.__init
__(self
)
906 self
.action_name
= action_name
907 self
.model
= main
.model()
910 for env
in ('FILENAME', 'REVISION', 'ARGS'):
917 opts
= _config
.get_guitool_opts(self
.action_name
)
918 cmd
= opts
.get('cmd')
919 if 'title' not in opts
:
922 if 'prompt' not in opts
or opts
.get('prompt') is True:
923 prompt
= N_('Run "%s"?') % cmd
924 opts
['prompt'] = prompt
926 if opts
.get('needsfile'):
927 filename
= selection
.filename()
929 Interaction
.information(
930 N_('Please select a file'),
931 N_('"%s" requires a selected file.') % cmd
)
933 compat
.setenv('FILENAME', filename
)
935 if opts
.get('revprompt') or opts
.get('argprompt'):
937 ok
= Interaction
.confirm_config_action(cmd
, opts
)
940 rev
= opts
.get('revision')
941 args
= opts
.get('args')
942 if opts
.get('revprompt') and not rev
:
943 title
= N_('Invalid Revision')
944 msg
= N_('The revision expression cannot be empty.')
945 Interaction
.critical(title
, msg
)
949 elif opts
.get('confirm'):
950 title
= os
.path
.expandvars(opts
.get('title'))
951 prompt
= os
.path
.expandvars(opts
.get('prompt'))
952 if Interaction
.question(title
, prompt
):
955 compat
.setenv('REVISION', rev
)
957 compat
.setenv('ARGS', args
)
958 title
= os
.path
.expandvars(cmd
)
959 Interaction
.log(N_('Running command: %s') % title
)
960 cmd
= ['sh', '-c', cmd
]
962 if opts
.get('noconsole'):
963 status
, out
, err
= core
.run_command(cmd
)
965 status
, out
, err
= Interaction
.run_command(title
, cmd
)
967 Interaction
.log_status(status
,
968 out
and (N_('Output: %s') % out
) or '',
969 err
and (N_('Errors: %s') % err
) or '')
971 if not opts
.get('norescan'):
972 self
.model
.update_status()
976 class SetDiffText(Command
):
978 def __init__(self
, text
):
979 Command
.__init
__(self
)
981 self
.new_diff_text
= text
984 class ShowUntracked(Command
):
985 """Show an untracked file."""
987 def __init__(self
, filenames
):
988 Command
.__init
__(self
)
989 self
.filenames
= filenames
990 self
.new_mode
= self
.model
.mode_untracked
991 self
.new_diff_text
= ''
993 self
.new_diff_text
= self
.diff_text_for(filenames
[0])
995 def diff_text_for(self
, filename
):
996 size
= _config
.get('cola.readsize', 1024 * 2)
998 result
= core
.read(filename
, size
=size
)
1002 if len(result
) == size
:
1007 class SignOff(Command
):
1012 return N_('Sign Off')
1015 Command
.__init
__(self
)
1016 self
.undoable
= True
1017 self
.old_commitmsg
= self
.model
.commitmsg
1020 signoff
= self
.signoff()
1021 if signoff
in self
.model
.commitmsg
:
1023 self
.model
.set_commitmsg(self
.model
.commitmsg
+ '\n' + signoff
)
1026 self
.model
.set_commitmsg(self
.old_commitmsg
)
1031 user
= pwd
.getpwuid(os
.getuid()).pw_name
1033 user
= os
.getenv('USER', N_('unknown'))
1035 name
= _config
.get('user.name', user
)
1036 email
= _config
.get('user.email', '%s@%s' % (user
, core
.node()))
1037 return '\nSigned-off-by: %s <%s>' % (name
, email
)
1040 class Stage(Command
):
1041 """Stage a set of paths."""
1048 def __init__(self
, paths
):
1049 Command
.__init
__(self
)
1053 msg
= N_('Staging: %s') % (', '.join(self
.paths
))
1054 Interaction
.log(msg
)
1055 # Prevent external updates while we are staging files.
1056 # We update file stats at the end of this operation
1057 # so there's no harm in ignoring updates from other threads
1059 with
CommandDisabled(UpdateFileStatus
):
1060 self
.model
.stage_paths(self
.paths
)
1063 class StageModified(Stage
):
1064 """Stage all modified files."""
1070 return N_('Stage Modified')
1073 Stage
.__init
__(self
, None)
1074 self
.paths
= self
.model
.modified
1077 class StageUnmerged(Stage
):
1078 """Stage all modified files."""
1084 return N_('Stage Unmerged')
1087 Stage
.__init
__(self
, None)
1088 self
.paths
= self
.model
.unmerged
1091 class StageUntracked(Stage
):
1092 """Stage all untracked files."""
1098 return N_('Stage Untracked')
1101 Stage
.__init
__(self
, None)
1102 self
.paths
= self
.model
.untracked
1106 """Create a tag object."""
1108 def __init__(self
, name
, revision
, sign
=False, message
=''):
1109 Command
.__init
__(self
)
1111 self
._message
= message
1112 self
._revision
= revision
1116 log_msg
= (N_('Tagging "%(revision)s" as "%(name)s"') %
1117 dict(revision
=self
._revision
, name
=self
._name
))
1120 opts
['F'] = utils
.tmp_filename('tag-message')
1121 core
.write(opts
['F'], self
._message
)
1124 log_msg
+= ' (%s)' % N_('GPG-signed')
1126 status
, output
, err
= self
.model
.git
.tag(self
._name
,
1127 self
._revision
, **opts
)
1129 opts
['a'] = bool(self
._message
)
1130 status
, output
, err
= self
.model
.git
.tag(self
._name
,
1131 self
._revision
, **opts
)
1133 os
.unlink(opts
['F'])
1136 log_msg
+= '\n' + (N_('Output: %s') % output
)
1138 Interaction
.log_status(status
, log_msg
, err
)
1140 self
.model
.update_status()
1143 class Unstage(Command
):
1144 """Unstage a set of paths."""
1150 return N_('Unstage')
1152 def __init__(self
, paths
):
1153 Command
.__init
__(self
)
1157 msg
= N_('Unstaging: %s') % (', '.join(self
.paths
))
1158 Interaction
.log(msg
)
1159 with
CommandDisabled(UpdateFileStatus
):
1160 self
.model
.unstage_paths(self
.paths
)
1163 class UnstageAll(Command
):
1164 """Unstage all files; resets the index."""
1167 self
.model
.unstage_all()
1170 class UnstageSelected(Unstage
):
1171 """Unstage selected files."""
1174 Unstage
.__init
__(self
, selection
.selection_model().staged
)
1177 class Untrack(Command
):
1178 """Unstage a set of paths."""
1180 def __init__(self
, paths
):
1181 Command
.__init
__(self
)
1185 msg
= N_('Untracking: %s') % (', '.join(self
.paths
))
1186 Interaction
.log(msg
)
1187 with
CommandDisabled(UpdateFileStatus
):
1188 status
, out
, err
= self
.model
.untrack_paths(self
.paths
)
1189 Interaction
.log_status(status
, out
, err
)
1192 class UntrackedSummary(Command
):
1193 """List possible .gitignore rules as the diff text."""
1196 Command
.__init
__(self
)
1197 untracked
= self
.model
.untracked
1198 suffix
= len(untracked
) > 1 and 's' or ''
1200 io
.write('# %s untracked file%s\n' % (len(untracked
), suffix
))
1202 io
.write('# possible .gitignore rule%s:\n' % suffix
)
1204 io
.write('/'+core
.encode(u
)+'\n')
1205 self
.new_diff_text
= core
.decode(io
.getvalue())
1206 self
.new_mode
= self
.model
.mode_untracked
1209 class UpdateFileStatus(Command
):
1210 """Rescans for changes."""
1213 self
.model
.update_file_status()
1216 class VisualizeAll(Command
):
1217 """Visualize all branches."""
1220 browser
= utils
.shell_split(prefs
.history_browser())
1221 core
.fork(browser
+ ['--all'])
1224 class VisualizeCurrent(Command
):
1225 """Visualize all branches."""
1228 browser
= utils
.shell_split(prefs
.history_browser())
1229 core
.fork(browser
+ [self
.model
.currentbranch
])
1232 class VisualizePaths(Command
):
1233 """Path-limited visualization."""
1235 def __init__(self
, paths
):
1236 Command
.__init
__(self
)
1237 browser
= utils
.shell_split(prefs
.history_browser())
1239 self
.argv
= browser
+ paths
1244 core
.fork(self
.argv
)
1247 class VisualizeRevision(Command
):
1248 """Visualize a specific revision."""
1250 def __init__(self
, revision
, paths
=None):
1251 Command
.__init
__(self
)
1252 self
.revision
= revision
1256 argv
= utils
.shell_split(prefs
.history_browser())
1258 argv
.append(self
.revision
)
1261 argv
.extend(self
.paths
)
1265 except Exception as e
:
1266 _
, details
= utils
.format_exception(e
)
1267 title
= N_('Error Launching History Browser')
1268 msg
= (N_('Cannot exec "%s": please configure a history browser') %
1270 Interaction
.critical(title
, message
=msg
, details
=details
)
1273 def run(cls
, *args
, **opts
):
1275 Returns a callback that runs a command
1277 If the caller of run() provides args or opts then those are
1278 used instead of the ones provided by the invoker of the callback.
1281 def runner(*local_args
, **local_opts
):
1283 do(cls
, *args
, **opts
)
1285 do(cls
, *local_args
, **local_opts
)
1290 class CommandDisabled(object):
1292 """Context manager to temporarily disable a command from running"""
1293 def __init__(self
, cmdclass
):
1294 self
.cmdclass
= cmdclass
1296 def __enter__(self
):
1297 self
.cmdclass
.DISABLED
= True
1300 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
1301 self
.cmdclass
.DISABLED
= False
1304 def do(cls
, *args
, **opts
):
1305 """Run a command in-place"""
1306 return do_cmd(cls(*args
, **opts
))
1310 if hasattr(cmd
, 'DISABLED') and cmd
.DISABLED
:
1314 except StandardError, e
:
1315 msg
, details
= utils
.format_exception(e
)
1316 Interaction
.critical(N_('Error'), message
=msg
, details
=details
)