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
354 if not self
.filenames
:
356 new_additions
= '\n'.join(self
.filenames
) + '\n'
357 for_status
= new_additions
358 if core
.exists('.gitignore'):
359 current_list
= core
.read('.gitignore')
360 new_additions
= current_list
.rstrip() + '\n' + new_additions
361 core
.write('.gitignore', new_additions
)
362 Interaction
.log_status(0, 'Added to .gitignore:\n%s' % for_status
, '')
363 self
.model
.update_file_status()
366 class Delete(Command
):
369 def __init__(self
, filenames
):
370 Command
.__init
__(self
)
371 self
.filenames
= filenames
372 # We could git-hash-object stuff and provide undo-ability
376 for filename
in self
.filenames
:
382 Interaction
.information(
384 N_('Deleting "%s" failed') % filename
)
386 self
.model
.update_file_status()
389 class DeleteBranch(Command
):
390 """Delete a git branch."""
392 def __init__(self
, branch
):
393 Command
.__init
__(self
)
397 status
, out
, err
= self
.model
.delete_branch(self
.branch
)
398 Interaction
.log_status(status
, out
, err
)
401 class DeleteRemoteBranch(Command
):
402 """Delete a remote git branch."""
404 def __init__(self
, remote
, branch
):
405 Command
.__init
__(self
)
410 status
, out
, err
= self
.model
.git
.push(self
.remote
, self
.branch
,
412 Interaction
.log_status(status
, out
, err
)
413 self
.model
.update_status()
416 Interaction
.information(
417 N_('Remote Branch Deleted'),
418 N_('"%(branch)s" has been deleted from "%(remote)s".')
419 % dict(branch
=self
.branch
, remote
=self
.remote
))
422 message
= (N_('"%(command)s" returned exit status %(status)d') %
423 dict(command
=command
, status
=status
))
425 Interaction
.critical(N_('Error Deleting Remote Branch'),
431 """Perform a diff and set the model's current text."""
433 def __init__(self
, filenames
, cached
=False):
434 Command
.__init
__(self
)
435 # Guard against the list of files being empty
440 opts
['ref'] = self
.model
.head
441 self
.new_filename
= filenames
[0]
442 self
.old_filename
= self
.model
.filename
443 self
.new_mode
= self
.model
.mode_worktree
444 self
.new_diff_text
= gitcmds
.diff_helper(filename
=self
.new_filename
,
445 cached
=cached
, **opts
)
448 class Diffstat(Command
):
449 """Perform a diffstat and set the model's diff text."""
452 Command
.__init
__(self
)
453 diff
= self
.model
.git
.diff(self
.model
.head
,
454 unified
=_config
.get('diff.context', 3),
459 self
.new_diff_text
= diff
460 self
.new_mode
= self
.model
.mode_worktree
463 class DiffStaged(Diff
):
464 """Perform a staged diff on a file."""
466 def __init__(self
, filenames
):
467 Diff
.__init
__(self
, filenames
, cached
=True)
468 self
.new_mode
= self
.model
.mode_index
471 class DiffStagedSummary(Command
):
474 Command
.__init
__(self
)
475 diff
= self
.model
.git
.diff(self
.model
.head
,
479 patch_with_stat
=True,
481 self
.new_diff_text
= diff
482 self
.new_mode
= self
.model
.mode_index
485 class Difftool(Command
):
486 """Run git-difftool limited by path."""
488 def __init__(self
, staged
, filenames
):
489 Command
.__init
__(self
)
491 self
.filenames
= filenames
494 difftool
.launch_with_head(self
.filenames
,
495 self
.staged
, self
.model
.head
)
499 """Edit a file using the configured gui.editor."""
506 def __init__(self
, filenames
, line_number
=None):
507 Command
.__init
__(self
)
508 self
.filenames
= filenames
509 self
.line_number
= line_number
512 if not self
.filenames
:
514 filename
= self
.filenames
[0]
515 if not core
.exists(filename
):
517 editor
= prefs
.editor()
520 if self
.line_number
is None:
521 opts
= self
.filenames
523 # Single-file w/ line-numbers (likely from grep)
525 '*vim*': ['+'+self
.line_number
, filename
],
526 '*emacs*': ['+'+self
.line_number
, filename
],
527 '*textpad*': ['%s(%s,0)' % (filename
, self
.line_number
)],
528 '*notepad++*': ['-n'+self
.line_number
, filename
],
531 opts
= self
.filenames
532 for pattern
, opt
in editor_opts
.items():
533 if fnmatch(editor
, pattern
):
538 core
.fork(utils
.shell_split(editor
) + opts
)
539 except Exception as e
:
540 message
= (N_('Cannot exec "%s": please configure your editor') %
542 Interaction
.critical(N_('Error Editing File'),
546 class FormatPatch(Command
):
547 """Output a patch series given all revisions and a selected subset."""
549 def __init__(self
, to_export
, revs
):
550 Command
.__init
__(self
)
551 self
.to_export
= to_export
555 status
, out
, err
= gitcmds
.format_patchsets(self
.to_export
, self
.revs
)
556 Interaction
.log_status(status
, out
, err
)
559 class LaunchDifftool(BaseCommand
):
565 return N_('Launch Diff Tool')
568 BaseCommand
.__init
__(self
)
571 s
= selection
.selection()
575 core
.fork(['git', 'mergetool', '--no-prompt', '--'] + paths
)
577 core
.fork(['xterm', '-e',
578 'git', 'mergetool', '--no-prompt', '--'] + paths
)
583 class LaunchTerminal(BaseCommand
):
589 return N_('Launch Terminal')
591 def __init__(self
, path
):
595 cmd
= _config
.get('cola.terminal', 'xterm -e $SHELL')
596 cmd
= os
.path
.expandvars(cmd
)
597 argv
= utils
.shell_split(cmd
)
598 core
.fork(argv
, cwd
=self
.path
)
601 class LaunchEditor(Edit
):
606 return N_('Launch Editor')
609 s
= selection
.selection()
610 allfiles
= s
.staged
+ s
.unmerged
+ s
.modified
+ s
.untracked
611 Edit
.__init
__(self
, allfiles
)
614 class LoadCommitMessageFromFile(Command
):
615 """Loads a commit message from a path."""
617 def __init__(self
, path
):
618 Command
.__init
__(self
)
621 self
.old_commitmsg
= self
.model
.commitmsg
622 self
.old_directory
= self
.model
.directory
626 if not path
or not core
.isfile(path
):
627 raise UsageError(N_('Error: Cannot find commit template'),
628 N_('%s: No such file or directory.') % path
)
629 self
.model
.set_directory(os
.path
.dirname(path
))
630 self
.model
.set_commitmsg(core
.read(path
))
633 self
.model
.set_commitmsg(self
.old_commitmsg
)
634 self
.model
.set_directory(self
.old_directory
)
637 class LoadCommitMessageFromTemplate(LoadCommitMessageFromFile
):
638 """Loads the commit message template specified by commit.template."""
641 template
= _config
.get('commit.template')
642 LoadCommitMessageFromFile
.__init
__(self
, template
)
645 if self
.path
is None:
647 N_('Error: Unconfigured commit template'),
648 N_('A commit template has not been configured.\n'
649 'Use "git config" to define "commit.template"\n'
650 'so that it points to a commit template.'))
651 return LoadCommitMessageFromFile
.do(self
)
655 class LoadCommitMessageFromSHA1(Command
):
656 """Load a previous commit message"""
658 def __init__(self
, sha1
, prefix
=''):
659 Command
.__init
__(self
)
661 self
.old_commitmsg
= self
.model
.commitmsg
662 self
.new_commitmsg
= prefix
+ self
.model
.prev_commitmsg(sha1
)
666 self
.model
.set_commitmsg(self
.new_commitmsg
)
669 self
.model
.set_commitmsg(self
.old_commitmsg
)
672 class LoadFixupMessage(LoadCommitMessageFromSHA1
):
673 """Load a fixup message"""
675 def __init__(self
, sha1
):
676 LoadCommitMessageFromSHA1
.__init__(self
, sha1
, prefix
='fixup! ')
679 class Merge(Command
):
680 def __init__(self
, revision
, no_commit
, squash
):
681 Command
.__init
__(self
)
682 self
.revision
= revision
683 self
.no_commit
= no_commit
688 revision
= self
.revision
689 no_commit
= self
.no_commit
690 msg
= gitcmds
.merge_message(revision
)
692 status
, out
, err
= self
.model
.git
.merge('-m', msg
,
697 Interaction
.log_status(status
, out
, err
)
698 self
.model
.update_status()
701 class OpenDefaultApp(BaseCommand
):
702 """Open a file using the OS default."""
707 return N_('Open Using Default Application')
709 def __init__(self
, filenames
):
710 BaseCommand
.__init
__(self
)
711 if utils
.is_darwin():
714 launcher
= 'xdg-open'
715 self
.launcher
= launcher
716 self
.filenames
= filenames
719 if not self
.filenames
:
721 core
.fork([self
.launcher
] + self
.filenames
)
724 class OpenParentDir(OpenDefaultApp
):
725 """Open parent directories using the OS default."""
726 SHORTCUT
= 'Shift+Space'
730 return N_('Open Parent Directory')
732 def __init__(self
, filenames
):
733 OpenDefaultApp
.__init
__(self
, filenames
)
736 if not self
.filenames
:
738 dirs
= list(set(map(os
.path
.dirname
, self
.filenames
)))
739 core
.fork([self
.launcher
] + dirs
)
742 class OpenNewRepo(Command
):
743 """Launches git-cola on a repo."""
745 def __init__(self
, repo_path
):
746 Command
.__init
__(self
)
747 self
.repo_path
= repo_path
750 self
.model
.set_directory(self
.repo_path
)
751 core
.fork([sys
.executable
, sys
.argv
[0], '--repo', self
.repo_path
])
754 class OpenRepo(Command
):
755 def __init__(self
, repo_path
):
756 Command
.__init
__(self
)
757 self
.repo_path
= repo_path
761 old_worktree
= git
.worktree()
762 if not self
.model
.set_worktree(self
.repo_path
):
763 self
.model
.set_worktree(old_worktree
)
765 new_worktree
= git
.worktree()
766 core
.chdir(new_worktree
)
767 self
.model
.set_directory(self
.repo_path
)
769 self
.model
.update_status()
772 class Clone(Command
):
773 """Clones a repository and optionally spawns a new cola session."""
775 def __init__(self
, url
, new_directory
, spawn
=True):
776 Command
.__init
__(self
)
778 self
.new_directory
= new_directory
782 status
, out
, err
= self
.model
.git
.clone(self
.url
, self
.new_directory
)
784 Interaction
.information(
785 N_('Error: could not clone "%s"') % self
.url
,
786 (N_('git clone returned exit code %s') % status
) +
787 ((out
+err
) and ('\n\n' + out
+ err
) or ''))
790 core
.fork([sys
.executable
, sys
.argv
[0],
791 '--repo', self
.new_directory
])
795 class GitXBaseContext(object):
797 def __init__(self
, **kwargs
):
801 compat
.setenv('GIT_SEQUENCE_EDITOR',
802 resources
.share('bin', 'git-xbase'))
803 for var
, value
in self
.extras
.items():
804 compat
.setenv(var
, value
)
807 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
808 compat
.unsetenv('GIT_SEQUENCE_EDITOR')
809 for var
in self
.extras
:
813 class Rebase(Command
):
815 def __init__(self
, branch
, capture_output
=True):
816 Command
.__init
__(self
)
818 self
.capture_output
= capture_output
828 if self
.capture_output
:
829 extra
['_stderr'] = None
830 extra
['_stdout'] = None
831 with
GitXBaseContext(
832 GIT_EDITOR
=prefs
.editor(),
833 GIT_XBASE_TITLE
=N_('Rebase onto %s') % branch
,
834 GIT_XBASE_ACTION
=N_('Rebase')):
835 status
, out
, err
= self
.model
.git
.rebase(branch
,
839 Interaction
.log_status(status
, out
, err
)
840 self
.model
.update_status()
841 return status
, out
, err
844 class RebaseEditTodo(Command
):
847 with
GitXBaseContext(
848 GIT_XBASE_TITLE
=N_('Edit Rebase'),
849 GIT_XBASE_ACTION
=N_('Save')):
850 status
, out
, err
= self
.model
.git
.rebase(edit_todo
=True)
851 Interaction
.log_status(status
, out
, err
)
852 self
.model
.update_status()
855 class RebaseContinue(Command
):
858 status
, out
, err
= self
.model
.git
.rebase('--continue')
859 Interaction
.log_status(status
, out
, err
)
860 self
.model
.update_status()
863 class RebaseSkip(Command
):
866 status
, out
, err
= self
.model
.git
.rebase(skip
=True)
867 Interaction
.log_status(status
, out
, err
)
868 self
.model
.update_status()
871 class RebaseAbort(Command
):
874 status
, out
, err
= self
.model
.git
.rebase(abort
=True)
875 Interaction
.log_status(status
, out
, err
)
876 self
.model
.update_status()
879 class Rescan(Command
):
880 """Rescan for changes"""
883 self
.model
.update_status()
886 class Refresh(Command
):
887 """Update refs and refresh the index"""
896 self
.model
.update_status(update_index
=True)
899 class RunConfigAction(Command
):
900 """Run a user-configured action, typically from the "Tools" menu"""
902 def __init__(self
, action_name
):
903 Command
.__init
__(self
)
904 self
.action_name
= action_name
905 self
.model
= main
.model()
908 for env
in ('FILENAME', 'REVISION', 'ARGS'):
915 opts
= _config
.get_guitool_opts(self
.action_name
)
916 cmd
= opts
.get('cmd')
917 if 'title' not in opts
:
920 if 'prompt' not in opts
or opts
.get('prompt') is True:
921 prompt
= N_('Run "%s"?') % cmd
922 opts
['prompt'] = prompt
924 if opts
.get('needsfile'):
925 filename
= selection
.filename()
927 Interaction
.information(
928 N_('Please select a file'),
929 N_('"%s" requires a selected file.') % cmd
)
931 compat
.setenv('FILENAME', filename
)
933 if opts
.get('revprompt') or opts
.get('argprompt'):
935 ok
= Interaction
.confirm_config_action(cmd
, opts
)
938 rev
= opts
.get('revision')
939 args
= opts
.get('args')
940 if opts
.get('revprompt') and not rev
:
941 title
= N_('Invalid Revision')
942 msg
= N_('The revision expression cannot be empty.')
943 Interaction
.critical(title
, msg
)
947 elif opts
.get('confirm'):
948 title
= os
.path
.expandvars(opts
.get('title'))
949 prompt
= os
.path
.expandvars(opts
.get('prompt'))
950 if Interaction
.question(title
, prompt
):
953 compat
.setenv('REVISION', rev
)
955 compat
.setenv('ARGS', args
)
956 title
= os
.path
.expandvars(cmd
)
957 Interaction
.log(N_('Running command: %s') % title
)
958 cmd
= ['sh', '-c', cmd
]
960 if opts
.get('noconsole'):
961 status
, out
, err
= core
.run_command(cmd
)
963 status
, out
, err
= Interaction
.run_command(title
, cmd
)
965 Interaction
.log_status(status
,
966 out
and (N_('Output: %s') % out
) or '',
967 err
and (N_('Errors: %s') % err
) or '')
969 if not opts
.get('norescan'):
970 self
.model
.update_status()
974 class SetDiffText(Command
):
976 def __init__(self
, text
):
977 Command
.__init
__(self
)
979 self
.new_diff_text
= text
982 class ShowUntracked(Command
):
983 """Show an untracked file."""
985 def __init__(self
, filenames
):
986 Command
.__init
__(self
)
987 self
.filenames
= filenames
988 self
.new_mode
= self
.model
.mode_untracked
989 self
.new_diff_text
= ''
991 self
.new_diff_text
= self
.diff_text_for(filenames
[0])
993 def diff_text_for(self
, filename
):
994 size
= _config
.get('cola.readsize', 1024 * 2)
996 result
= core
.read(filename
, size
=size
)
1000 if len(result
) == size
:
1005 class SignOff(Command
):
1010 return N_('Sign Off')
1013 Command
.__init
__(self
)
1014 self
.undoable
= True
1015 self
.old_commitmsg
= self
.model
.commitmsg
1018 signoff
= self
.signoff()
1019 if signoff
in self
.model
.commitmsg
:
1021 self
.model
.set_commitmsg(self
.model
.commitmsg
+ '\n' + signoff
)
1024 self
.model
.set_commitmsg(self
.old_commitmsg
)
1029 user
= pwd
.getpwuid(os
.getuid()).pw_name
1031 user
= os
.getenv('USER', N_('unknown'))
1033 name
= _config
.get('user.name', user
)
1034 email
= _config
.get('user.email', '%s@%s' % (user
, core
.node()))
1035 return '\nSigned-off-by: %s <%s>' % (name
, email
)
1038 class Stage(Command
):
1039 """Stage a set of paths."""
1046 def __init__(self
, paths
):
1047 Command
.__init
__(self
)
1051 msg
= N_('Staging: %s') % (', '.join(self
.paths
))
1052 Interaction
.log(msg
)
1053 # Prevent external updates while we are staging files.
1054 # We update file stats at the end of this operation
1055 # so there's no harm in ignoring updates from other threads
1057 with
CommandDisabled(UpdateFileStatus
):
1058 self
.model
.stage_paths(self
.paths
)
1061 class StageModified(Stage
):
1062 """Stage all modified files."""
1068 return N_('Stage Modified')
1071 Stage
.__init
__(self
, None)
1072 self
.paths
= self
.model
.modified
1075 class StageUnmerged(Stage
):
1076 """Stage all modified files."""
1082 return N_('Stage Unmerged')
1085 Stage
.__init
__(self
, None)
1086 self
.paths
= self
.model
.unmerged
1089 class StageUntracked(Stage
):
1090 """Stage all untracked files."""
1096 return N_('Stage Untracked')
1099 Stage
.__init
__(self
, None)
1100 self
.paths
= self
.model
.untracked
1104 """Create a tag object."""
1106 def __init__(self
, name
, revision
, sign
=False, message
=''):
1107 Command
.__init
__(self
)
1109 self
._message
= message
1110 self
._revision
= revision
1114 log_msg
= (N_('Tagging "%(revision)s" as "%(name)s"') %
1115 dict(revision
=self
._revision
, name
=self
._name
))
1118 opts
['F'] = utils
.tmp_filename('tag-message')
1119 core
.write(opts
['F'], self
._message
)
1122 log_msg
+= ' (%s)' % N_('GPG-signed')
1124 status
, output
, err
= self
.model
.git
.tag(self
._name
,
1125 self
._revision
, **opts
)
1127 opts
['a'] = bool(self
._message
)
1128 status
, output
, err
= self
.model
.git
.tag(self
._name
,
1129 self
._revision
, **opts
)
1131 os
.unlink(opts
['F'])
1134 log_msg
+= '\n' + (N_('Output: %s') % output
)
1136 Interaction
.log_status(status
, log_msg
, err
)
1138 self
.model
.update_status()
1141 class Unstage(Command
):
1142 """Unstage a set of paths."""
1148 return N_('Unstage')
1150 def __init__(self
, paths
):
1151 Command
.__init
__(self
)
1155 msg
= N_('Unstaging: %s') % (', '.join(self
.paths
))
1156 Interaction
.log(msg
)
1157 with
CommandDisabled(UpdateFileStatus
):
1158 self
.model
.unstage_paths(self
.paths
)
1161 class UnstageAll(Command
):
1162 """Unstage all files; resets the index."""
1165 self
.model
.unstage_all()
1168 class UnstageSelected(Unstage
):
1169 """Unstage selected files."""
1172 Unstage
.__init
__(self
, selection
.selection_model().staged
)
1175 class Untrack(Command
):
1176 """Unstage a set of paths."""
1178 def __init__(self
, paths
):
1179 Command
.__init
__(self
)
1183 msg
= N_('Untracking: %s') % (', '.join(self
.paths
))
1184 Interaction
.log(msg
)
1185 with
CommandDisabled(UpdateFileStatus
):
1186 status
, out
, err
= self
.model
.untrack_paths(self
.paths
)
1187 Interaction
.log_status(status
, out
, err
)
1190 class UntrackedSummary(Command
):
1191 """List possible .gitignore rules as the diff text."""
1194 Command
.__init
__(self
)
1195 untracked
= self
.model
.untracked
1196 suffix
= len(untracked
) > 1 and 's' or ''
1198 io
.write('# %s untracked file%s\n' % (len(untracked
), suffix
))
1200 io
.write('# possible .gitignore rule%s:\n' % suffix
)
1202 io
.write('/'+core
.encode(u
)+'\n')
1203 self
.new_diff_text
= core
.decode(io
.getvalue())
1204 self
.new_mode
= self
.model
.mode_untracked
1207 class UpdateFileStatus(Command
):
1208 """Rescans for changes."""
1211 self
.model
.update_file_status()
1214 class VisualizeAll(Command
):
1215 """Visualize all branches."""
1218 browser
= utils
.shell_split(prefs
.history_browser())
1219 launch_history_browser(browser
+ ['--all'])
1222 class VisualizeCurrent(Command
):
1223 """Visualize all branches."""
1226 browser
= utils
.shell_split(prefs
.history_browser())
1227 launch_history_browser(browser
+ [self
.model
.currentbranch
])
1230 class VisualizePaths(Command
):
1231 """Path-limited visualization."""
1233 def __init__(self
, paths
):
1234 Command
.__init
__(self
)
1235 browser
= utils
.shell_split(prefs
.history_browser())
1237 self
.argv
= browser
+ paths
1242 launch_history_browser(self
.argv
)
1245 class VisualizeRevision(Command
):
1246 """Visualize a specific revision."""
1248 def __init__(self
, revision
, paths
=None):
1249 Command
.__init
__(self
)
1250 self
.revision
= revision
1254 argv
= utils
.shell_split(prefs
.history_browser())
1256 argv
.append(self
.revision
)
1259 argv
.extend(self
.paths
)
1260 launch_history_browser(argv
)
1263 def launch_history_browser(argv
):
1266 except Exception as e
:
1267 _
, details
= utils
.format_exception(e
)
1268 title
= N_('Error Launching History Browser')
1269 msg
= (N_('Cannot exec "%s": please configure a history browser') %
1271 Interaction
.critical(title
, message
=msg
, details
=details
)
1274 def run(cls
, *args
, **opts
):
1276 Returns a callback that runs a command
1278 If the caller of run() provides args or opts then those are
1279 used instead of the ones provided by the invoker of the callback.
1282 def runner(*local_args
, **local_opts
):
1284 do(cls
, *args
, **opts
)
1286 do(cls
, *local_args
, **local_opts
)
1291 class CommandDisabled(object):
1293 """Context manager to temporarily disable a command from running"""
1294 def __init__(self
, cmdclass
):
1295 self
.cmdclass
= cmdclass
1297 def __enter__(self
):
1298 self
.cmdclass
.DISABLED
= True
1301 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
1302 self
.cmdclass
.DISABLED
= False
1305 def do(cls
, *args
, **opts
):
1306 """Run a command in-place"""
1307 return do_cmd(cls(*args
, **opts
))
1311 if hasattr(cmd
, 'DISABLED') and cmd
.DISABLED
:
1315 except StandardError, e
:
1316 msg
, details
= utils
.format_exception(e
)
1317 Interaction
.critical(N_('Error'), message
=msg
, details
=details
)