5 from fnmatch
import fnmatch
7 from cStringIO
import StringIO
9 from PyQt4
import QtCore
10 from PyQt4
.QtCore
import SIGNAL
13 from cola
import compat
15 from cola
import errors
16 from cola
import gitcfg
17 from cola
import gitcmds
18 from cola
import utils
19 from cola
import difftool
20 from cola
.compat
import set
21 from cola
.diffparse
import DiffParser
22 from cola
.i18n
import N_
23 from cola
.interaction
import Interaction
24 from cola
.models
import selection
26 _notifier
= cola
.notifier()
27 _config
= gitcfg
.instance()
30 class BaseCommand(object):
31 """Base class for all commands; provides the command pattern"""
38 def is_undoable(self
):
39 """Can this be undone?"""
47 """Prepare to run the command.
49 This is performed in a separate thread before do()
56 raise NotImplementedError('%s.do() is unimplemented' % self
.__class
__.__name
__)
59 raise NotImplementedError('%s.undo() is unimplemented' % self
.__class
__.__name
__)
62 class Command(BaseCommand
):
63 """Base class for commands that modify the main model"""
66 """Initialize the command and stash away values for use in do()"""
67 # These are commonly used so let's make it easier to write new commands.
68 BaseCommand
.__init
__(self
)
69 self
.model
= cola
.model()
71 self
.old_diff_text
= self
.model
.diff_text
72 self
.old_filename
= self
.model
.filename
73 self
.old_mode
= self
.model
.mode
74 self
.old_head
= self
.model
.head
76 self
.new_diff_text
= self
.old_diff_text
77 self
.new_filename
= self
.old_filename
78 self
.new_head
= self
.old_head
79 self
.new_mode
= self
.old_mode
82 """Perform the operation."""
83 self
.model
.set_filename(self
.new_filename
)
84 self
.model
.set_head(self
.new_head
)
85 self
.model
.set_mode(self
.new_mode
)
86 self
.model
.set_diff_text(self
.new_diff_text
)
89 """Undo the operation."""
90 self
.model
.set_diff_text(self
.old_diff_text
)
91 self
.model
.set_filename(self
.old_filename
)
92 self
.model
.set_head(self
.old_head
)
93 self
.model
.set_mode(self
.old_mode
)
96 class AmendMode(Command
):
97 """Try to amend a commit."""
107 def __init__(self
, amend
):
108 Command
.__init
__(self
)
111 self
.amending
= amend
112 self
.old_commitmsg
= self
.model
.commitmsg
115 self
.new_mode
= self
.model
.mode_amend
116 self
.new_head
= 'HEAD^'
117 self
.new_commitmsg
= self
.model
.prev_commitmsg()
118 AmendMode
.LAST_MESSAGE
= self
.model
.commitmsg
120 # else, amend unchecked, regular commit
121 self
.new_mode
= self
.model
.mode_none
122 self
.new_head
= 'HEAD'
123 self
.new_diff_text
= ''
124 self
.new_commitmsg
= self
.model
.commitmsg
125 # If we're going back into new-commit-mode then search the
126 # undo stack for a previous amend-commit-mode and grab the
127 # commit message at that point in time.
128 if AmendMode
.LAST_MESSAGE
is not None:
129 self
.new_commitmsg
= AmendMode
.LAST_MESSAGE
130 AmendMode
.LAST_MESSAGE
= None
133 """Leave/enter amend mode."""
134 """Attempt to enter amend mode. Do not allow this when merging."""
136 if os
.path
.exists(self
.model
.git
.git_path('MERGE_HEAD')):
138 _notifier
.broadcast(_notifier
.AMEND
, False)
139 Interaction
.information(
141 N_('You are in the middle of a merge.\n'
142 'Cannot amend while merging.'))
145 _notifier
.broadcast(_notifier
.AMEND
, self
.amending
)
146 self
.model
.set_commitmsg(self
.new_commitmsg
)
148 self
.model
.update_file_status()
153 self
.model
.set_commitmsg(self
.old_commitmsg
)
155 self
.model
.update_file_status()
158 class ApplyDiffSelection(Command
):
160 def __init__(self
, staged
, selected
, offset
, selection
, apply_to_worktree
):
161 Command
.__init
__(self
)
163 self
.selected
= selected
165 self
.selection
= selection
166 self
.apply_to_worktree
= apply_to_worktree
169 # The normal worktree vs index scenario
170 parser
= DiffParser(self
.model
,
171 filename
=self
.model
.filename
,
173 reverse
=self
.apply_to_worktree
)
175 parser
.process_diff_selection(self
.selected
,
178 apply_to_worktree
=self
.apply_to_worktree
)
179 Interaction
.log_status(status
, output
, '')
180 self
.model
.update_file_status(update_index
=True)
183 class ApplyPatches(Command
):
185 def __init__(self
, patches
):
186 Command
.__init
__(self
)
188 self
.patches
= patches
192 num_patches
= len(self
.patches
)
193 orig_head
= self
.model
.git
.rev_parse('HEAD')
195 for idx
, patch
in enumerate(self
.patches
):
196 status
, output
= self
.model
.git
.am(patch
,
199 # Log the git-am command
200 Interaction
.log_status(status
, output
, '')
203 diff
= self
.model
.git
.diff('HEAD^!', stat
=True)
204 diff_text
+= (N_('PATCH %(current)d/%(count)d') %
205 dict(current
=idx
+1, count
=num_patches
))
206 diff_text
+= ' - %s:\n%s\n\n' % (os
.path
.basename(patch
), diff
)
208 diff_text
+= N_('Summary:') + '\n'
209 diff_text
+= self
.model
.git
.diff(orig_head
, stat
=True)
212 self
.model
.set_diff_text(diff_text
)
213 self
.model
.update_file_status()
215 basenames
= '\n'.join([os
.path
.basename(p
) for p
in self
.patches
])
216 Interaction
.information(
217 N_('Patch(es) Applied'),
218 (N_('%d patch(es) applied.') + '\n\n%s') %
219 (len(self
.patches
), basenames
))
222 class Archive(BaseCommand
):
224 def __init__(self
, ref
, fmt
, prefix
, filename
):
225 BaseCommand
.__init
__(self
)
229 self
.filename
= filename
232 fp
= open(core
.encode(self
.filename
), 'wb')
233 cmd
= ['git', 'archive', '--format='+self
.fmt
]
234 if self
.fmt
in ('tgz', 'tar.gz'):
237 cmd
.append('--prefix=' + self
.prefix
)
239 proc
= utils
.start_command(cmd
, stdout
=fp
)
240 out
, err
= proc
.communicate()
242 status
= proc
.returncode
243 Interaction
.log_status(status
, out
or '', err
or '')
246 class Checkout(Command
):
248 A command object for git-checkout.
250 'argv' is handed off directly to git.
254 def __init__(self
, argv
, checkout_branch
=False):
255 Command
.__init
__(self
)
257 self
.checkout_branch
= checkout_branch
258 self
.new_diff_text
= ''
261 status
, output
= self
.model
.git
.checkout(with_stderr
=True,
262 with_status
=True, *self
.argv
)
263 Interaction
.log_status(status
, output
, '')
264 if self
.checkout_branch
:
265 self
.model
.update_status()
267 self
.model
.update_file_status()
270 class CheckoutBranch(Checkout
):
271 """Checkout a branch."""
273 def __init__(self
, branch
):
275 Checkout
.__init
__(self
, args
, checkout_branch
=True)
278 class CherryPick(Command
):
279 """Cherry pick commits into the current branch."""
281 def __init__(self
, commits
):
282 Command
.__init
__(self
)
283 self
.commits
= commits
286 self
.model
.cherry_pick_list(self
.commits
)
287 self
.model
.update_file_status()
290 class ResetMode(Command
):
291 """Reset the mode and clear the model's diff text."""
294 Command
.__init
__(self
)
295 self
.new_mode
= self
.model
.mode_none
296 self
.new_head
= 'HEAD'
297 self
.new_diff_text
= ''
301 self
.model
.update_file_status()
304 class Commit(ResetMode
):
305 """Attempt to create a new commit."""
307 SHORTCUT
= 'Ctrl+Return'
309 def __init__(self
, amend
, msg
):
310 ResetMode
.__init
__(self
)
312 self
.msg
= core
.encode(msg
)
313 self
.old_commitmsg
= self
.model
.commitmsg
314 self
.new_commitmsg
= ''
317 tmpfile
= utils
.tmp_filename('commit-message')
318 status
, output
= self
.model
.commit_with_msg(self
.msg
, tmpfile
, amend
=self
.amend
)
321 self
.model
.set_commitmsg(self
.new_commitmsg
)
322 msg
= N_('Created commit: %s') % output
324 msg
= N_('Commit failed: %s') % output
325 Interaction
.log_status(status
, msg
, '')
327 return status
, output
330 class Ignore(Command
):
331 """Add files to .gitignore"""
333 def __init__(self
, filenames
):
334 Command
.__init
__(self
)
335 self
.filenames
= filenames
339 for fname
in self
.filenames
:
340 new_additions
= new_additions
+ fname
+ '\n'
341 for_status
= new_additions
343 if os
.path
.exists('.gitignore'):
344 current_list
= utils
.slurp('.gitignore')
345 new_additions
= new_additions
+ current_list
346 utils
.write('.gitignore', new_additions
)
347 Interaction
.log_status(
348 0, 'Added to .gitignore:\n%s' % for_status
, '')
349 self
.model
.update_file_status()
352 class Delete(Command
):
355 def __init__(self
, filenames
):
356 Command
.__init
__(self
)
357 self
.filenames
= filenames
358 # We could git-hash-object stuff and provide undo-ability
362 for filename
in self
.filenames
:
368 Interaction
.information(
370 N_('Deleting "%s" failed') % filename
)
372 self
.model
.update_file_status()
375 class DeleteBranch(Command
):
376 """Delete a git branch."""
378 def __init__(self
, branch
):
379 Command
.__init
__(self
)
383 status
, output
= self
.model
.delete_branch(self
.branch
)
384 Interaction
.log_status(status
, output
)
387 class DeleteRemoteBranch(Command
):
388 """Delete a remote git branch."""
390 def __init__(self
, remote
, branch
):
391 Command
.__init
__(self
)
396 status
, output
= self
.model
.git
.push(self
.remote
, self
.branch
,
400 self
.model
.update_status()
402 Interaction
.log_status(status
, output
)
405 Interaction
.information(
406 N_('Remote Branch Deleted'),
407 N_('"%(branch)s" has been deleted from "%(remote)s".')
408 % dict(branch
=self
.branch
, remote
=self
.remote
))
411 message
= (N_('"%(command)s" returned exit status %(status)d') %
412 dict(command
=command
, status
=status
))
414 Interaction
.critical(N_('Error Deleting Remote Branch'),
420 """Perform a diff and set the model's current text."""
422 def __init__(self
, filenames
, cached
=False):
423 Command
.__init
__(self
)
424 # Guard against the list of files being empty
429 opts
['ref'] = self
.model
.head
430 self
.new_filename
= filenames
[0]
431 self
.old_filename
= self
.model
.filename
432 self
.new_mode
= self
.model
.mode_worktree
433 self
.new_diff_text
= gitcmds
.diff_helper(filename
=self
.new_filename
,
434 cached
=cached
, **opts
)
437 class Diffstat(Command
):
438 """Perform a diffstat and set the model's diff text."""
441 Command
.__init
__(self
)
442 diff
= self
.model
.git
.diff(self
.model
.head
,
443 unified
=_config
.get('diff.context', 3),
448 self
.new_diff_text
= core
.decode(diff
)
449 self
.new_mode
= self
.model
.mode_worktree
452 class DiffStaged(Diff
):
453 """Perform a staged diff on a file."""
455 def __init__(self
, filenames
):
456 Diff
.__init
__(self
, filenames
, cached
=True)
457 self
.new_mode
= self
.model
.mode_index
460 class DiffStagedSummary(Command
):
463 Command
.__init
__(self
)
464 diff
= self
.model
.git
.diff(self
.model
.head
,
468 patch_with_stat
=True,
470 self
.new_diff_text
= core
.decode(diff
)
471 self
.new_mode
= self
.model
.mode_index
474 class Difftool(Command
):
475 """Run git-difftool limited by path."""
477 def __init__(self
, staged
, filenames
):
478 Command
.__init
__(self
)
480 self
.filenames
= filenames
483 difftool
.launch_with_head(self
.filenames
,
484 self
.staged
, self
.model
.head
)
488 """Edit a file using the configured gui.editor."""
495 def __init__(self
, filenames
, line_number
=None):
496 Command
.__init
__(self
)
497 self
.filenames
= filenames
498 self
.line_number
= line_number
501 if not self
.filenames
:
503 filename
= self
.filenames
[0]
504 if not os
.path
.exists(filename
):
506 editor
= self
.model
.editor()
509 if self
.line_number
is None:
510 opts
= self
.filenames
512 # Single-file w/ line-numbers (likely from grep)
514 '*vim*': ['+'+self
.line_number
, filename
],
515 '*emacs*': ['+'+self
.line_number
, filename
],
516 '*textpad*': ['%s(%s,0)' % (filename
, self
.line_number
)],
517 '*notepad++*': ['-n'+self
.line_number
, filename
],
520 opts
= self
.filenames
521 for pattern
, opt
in editor_opts
.items():
522 if fnmatch(editor
, pattern
):
527 utils
.fork(utils
.shell_split(editor
) + opts
)
528 except Exception as e
:
529 message
= (N_('Cannot exec "%s": please configure your editor') %
531 Interaction
.critical(N_('Error Editing File'),
535 class FormatPatch(Command
):
536 """Output a patch series given all revisions and a selected subset."""
538 def __init__(self
, to_export
, revs
):
539 Command
.__init
__(self
)
540 self
.to_export
= to_export
544 status
, output
= gitcmds
.format_patchsets(self
.to_export
, self
.revs
)
545 Interaction
.log_status(status
, output
, '')
548 class LaunchDifftool(BaseCommand
):
554 return N_('Launch Diff Tool')
557 BaseCommand
.__init
__(self
)
564 utils
.fork(['git', 'mergetool', '--no-prompt', '--'] + paths
)
566 utils
.fork(['xterm', '-e',
567 'git', 'mergetool', '--no-prompt', '--'] + paths
)
572 class LaunchEditor(Edit
):
577 return N_('Launch Editor')
581 allfiles
= s
.staged
+ s
.unmerged
+ s
.modified
+ s
.untracked
582 Edit
.__init
__(self
, allfiles
)
585 class LoadCommitMessage(Command
):
586 """Loads a commit message from a path."""
588 def __init__(self
, path
):
589 Command
.__init
__(self
)
592 self
.old_commitmsg
= self
.model
.commitmsg
593 self
.old_directory
= self
.model
.directory
597 if not path
or not os
.path
.isfile(path
):
598 raise errors
.UsageError(N_('Error: Cannot find commit template'),
599 N_('%s: No such file or directory.') % path
)
600 self
.model
.set_directory(os
.path
.dirname(path
))
601 self
.model
.set_commitmsg(utils
.slurp(path
))
604 self
.model
.set_commitmsg(self
.old_commitmsg
)
605 self
.model
.set_directory(self
.old_directory
)
608 class LoadCommitTemplate(LoadCommitMessage
):
609 """Loads the commit message template specified by commit.template."""
611 LoadCommitMessage
.__init
__(self
, _config
.get('commit.template'))
614 if self
.path
is None:
615 raise errors
.UsageError(
616 N_('Error: Unconfigured commit template'),
617 N_('A commit template has not been configured.\n'
618 'Use "git config" to define "commit.template"\n'
619 'so that it points to a commit template.'))
620 return LoadCommitMessage
.do(self
)
623 class LoadPreviousMessage(Command
):
624 """Try to amend a commit."""
625 def __init__(self
, sha1
):
626 Command
.__init
__(self
)
628 self
.old_commitmsg
= self
.model
.commitmsg
629 self
.new_commitmsg
= self
.model
.prev_commitmsg(sha1
)
633 self
.model
.set_commitmsg(self
.new_commitmsg
)
636 self
.model
.set_commitmsg(self
.old_commitmsg
)
639 class Merge(Command
):
640 def __init__(self
, revision
, no_commit
, squash
):
641 Command
.__init
__(self
)
642 self
.revision
= revision
643 self
.no_commit
= no_commit
648 revision
= self
.revision
649 no_commit
= self
.no_commit
650 msg
= gitcmds
.merge_message(revision
)
652 status
, output
= self
.model
.git
.merge('-m', msg
,
659 Interaction
.log_status(status
, output
, '')
660 self
.model
.update_status()
663 class OpenDefaultApp(BaseCommand
):
664 """Open a file using the OS default."""
669 return N_('Open Using Default Application')
671 def __init__(self
, filenames
):
672 BaseCommand
.__init
__(self
)
673 if utils
.is_darwin():
676 launcher
= 'xdg-open'
677 self
.launcher
= launcher
678 self
.filenames
= filenames
681 if not self
.filenames
:
683 utils
.fork([self
.launcher
] + self
.filenames
)
686 class OpenParentDir(OpenDefaultApp
):
687 """Open parent directories using the OS default."""
688 SHORTCUT
= 'Shift+Space'
692 return N_('Open Parent Directory')
694 def __init__(self
, filenames
):
695 OpenDefaultApp
.__init
__(self
, filenames
)
698 if not self
.filenames
:
700 dirs
= set(map(os
.path
.dirname
, self
.filenames
))
701 utils
.fork([self
.launcher
] + dirs
)
704 class OpenRepo(Command
):
705 """Launches git-cola on a repo."""
707 def __init__(self
, repo_path
):
708 Command
.__init
__(self
)
709 self
.repo_path
= repo_path
712 self
.model
.set_directory(self
.repo_path
)
713 utils
.fork([sys
.executable
, sys
.argv
[0], '--repo', self
.repo_path
])
716 class Clone(Command
):
717 """Clones a repository and optionally spawns a new cola session."""
719 def __init__(self
, url
, new_directory
, spawn
=True):
720 Command
.__init
__(self
)
722 self
.new_directory
= new_directory
726 status
, out
= self
.model
.git
.clone(self
.url
,
731 Interaction
.information(
732 N_('Error: could not clone "%s"') % self
.url
,
733 (N_('git clone returned exit code %s') % status
) +
734 (out
and ('\n' + out
) or ''))
737 utils
.fork([sys
.executable
, sys
.argv
[0],
738 '--repo', self
.new_directory
])
742 class Rescan(Command
):
743 """Rescan for changes"""
746 self
.model
.update_status()
749 class Refresh(Command
):
750 """Update refs and refresh the index"""
759 self
.model
.update_status(update_index
=True)
762 class RunConfigAction(Command
):
763 """Run a user-configured action, typically from the "Tools" menu"""
765 def __init__(self
, action_name
):
766 Command
.__init
__(self
)
767 self
.action_name
= action_name
768 self
.model
= cola
.model()
771 for env
in ('FILENAME', 'REVISION', 'ARGS'):
778 opts
= _config
.get_guitool_opts(self
.action_name
)
779 cmd
= opts
.get('cmd')
780 if 'title' not in opts
:
783 if 'prompt' not in opts
or opts
.get('prompt') is True:
784 prompt
= N_('Run "%s"?') % cmd
785 opts
['prompt'] = prompt
787 if opts
.get('needsfile'):
788 filename
= selection
.filename()
790 Interaction
.information(
791 N_('Please select a file'),
792 N_('"%s" requires a selected file.') % cmd
)
794 compat
.setenv('FILENAME', filename
)
796 if opts
.get('revprompt') or opts
.get('argprompt'):
798 ok
= Interaction
.confirm_config_action(cmd
, opts
)
801 rev
= opts
.get('revision')
802 args
= opts
.get('args')
803 if opts
.get('revprompt') and not rev
:
804 title
= N_('Invalid Revision')
805 msg
= N_('The revision expression cannot be empty.')
806 Interaction
.critical(title
, msg
)
810 elif opts
.get('confirm'):
811 title
= os
.path
.expandvars(opts
.get('title'))
812 prompt
= os
.path
.expandvars(opts
.get('prompt'))
813 if Interaction
.question(title
, prompt
):
816 compat
.setenv('REVISION', rev
)
818 compat
.setenv('ARGS', args
)
819 title
= os
.path
.expandvars(cmd
)
820 Interaction
.log(N_('Running command: %s') % title
)
821 cmd
= ['sh', '-c', cmd
]
823 if opts
.get('noconsole'):
824 status
, out
, err
= utils
.run_command(cmd
)
826 status
, out
, err
= Interaction
.run_command(title
, cmd
)
828 Interaction
.log_status(status
,
829 out
and (N_('Output: %s') % out
) or '',
830 err
and (N_('Errors: %s') % err
) or '')
832 if not opts
.get('norescan'):
833 self
.model
.update_status()
837 class SetDiffText(Command
):
839 def __init__(self
, text
):
840 Command
.__init
__(self
)
842 self
.new_diff_text
= text
845 class ShowUntracked(Command
):
846 """Show an untracked file."""
848 def __init__(self
, filenames
):
849 Command
.__init
__(self
)
850 self
.filenames
= filenames
851 self
.new_mode
= self
.model
.mode_untracked
852 self
.new_diff_text
= ''
855 filenames
= self
.filenames
857 self
.new_diff_text
= self
.diff_text_for(filenames
[0])
859 def diff_text_for(self
, filename
):
860 size
= _config
.get('cola.readsize', 1024 * 2)
862 result
= utils
.slurp(filename
, size
=size
)
866 if len(result
) == size
:
871 class SignOff(Command
):
876 return N_('Sign Off')
879 Command
.__init
__(self
)
881 self
.old_commitmsg
= self
.model
.commitmsg
884 signoff
= self
.signoff()
885 if signoff
in self
.model
.commitmsg
:
887 self
.model
.set_commitmsg(self
.model
.commitmsg
+ '\n' + signoff
)
890 self
.model
.set_commitmsg(self
.old_commitmsg
)
895 user
= pwd
.getpwuid(os
.getuid()).pw_name
897 user
= os
.getenv('USER', N_('unknown'))
899 name
= _config
.get('user.name', user
)
900 email
= _config
.get('user.email', '%s@%s' % (user
, platform
.node()))
901 return '\nSigned-off-by: %s <%s>' % (name
, email
)
904 class Stage(Command
):
905 """Stage a set of paths."""
912 def __init__(self
, paths
):
913 Command
.__init
__(self
)
917 msg
= N_('Staging: %s') % (', '.join(self
.paths
))
919 # Prevent external updates while we are staging files.
920 # We update file stats at the end of this operation
921 # so there's no harm in ignoring updates from other threads
923 with
CommandDisabled(UpdateFileStatus
):
924 self
.model
.stage_paths(self
.paths
)
927 class StageModified(Stage
):
928 """Stage all modified files."""
934 return N_('Stage Modified')
937 Stage
.__init
__(self
, None)
938 self
.paths
= self
.model
.modified
941 class StageUnmerged(Stage
):
942 """Stage all modified files."""
948 return N_('Stage Unmerged')
951 Stage
.__init
__(self
, None)
952 self
.paths
= self
.model
.unmerged
955 class StageUntracked(Stage
):
956 """Stage all untracked files."""
962 return N_('Stage Untracked')
965 Stage
.__init
__(self
, None)
966 self
.paths
= self
.model
.untracked
970 """Create a tag object."""
972 def __init__(self
, name
, revision
, sign
=False, message
=''):
973 Command
.__init
__(self
)
975 self
._message
= core
.encode(message
)
976 self
._revision
= revision
980 log_msg
= (N_('Tagging "%(revision)s" as "%(name)s"') %
981 dict(revision
=self
._revision
, name
=self
._name
))
984 opts
['F'] = utils
.tmp_filename('tag-message')
985 utils
.write(opts
['F'], self
._message
)
988 log_msg
+= ' (%s)' % N_('GPG-signed')
990 status
, output
= self
.model
.git
.tag(self
._name
,
996 opts
['a'] = bool(self
._message
)
997 status
, output
= self
.model
.git
.tag(self
._name
,
1003 os
.unlink(opts
['F'])
1006 log_msg
+= '\n' + N_('Output: %s') % output
1008 Interaction
.log_status(status
, log_msg
, '')
1010 self
.model
.update_status()
1013 class Unstage(Command
):
1014 """Unstage a set of paths."""
1020 return N_('Unstage')
1022 def __init__(self
, paths
):
1023 Command
.__init
__(self
)
1027 msg
= N_('Unstaging: %s') % (', '.join(self
.paths
))
1028 Interaction
.log(msg
)
1029 with
CommandDisabled(UpdateFileStatus
):
1030 self
.model
.unstage_paths(self
.paths
)
1033 class UnstageAll(Command
):
1034 """Unstage all files; resets the index."""
1037 self
.model
.unstage_all()
1040 class UnstageSelected(Unstage
):
1041 """Unstage selected files."""
1044 Unstage
.__init
__(self
, cola
.selection_model().staged
)
1047 class Untrack(Command
):
1048 """Unstage a set of paths."""
1050 def __init__(self
, paths
):
1051 Command
.__init
__(self
)
1055 msg
= N_('Untracking: %s') % (', '.join(self
.paths
))
1056 Interaction
.log(msg
)
1057 with
CommandDisabled(UpdateFileStatus
):
1058 status
, out
= self
.model
.untrack_paths(self
.paths
)
1059 Interaction
.log_status(status
, out
, '')
1062 class UntrackedSummary(Command
):
1063 """List possible .gitignore rules as the diff text."""
1066 Command
.__init
__(self
)
1067 untracked
= self
.model
.untracked
1068 suffix
= len(untracked
) > 1 and 's' or ''
1070 io
.write('# %s untracked file%s\n' % (len(untracked
), suffix
))
1072 io
.write('# possible .gitignore rule%s:\n' % suffix
)
1074 io
.write('/'+core
.encode(u
)+'\n')
1075 self
.new_diff_text
= core
.decode(io
.getvalue())
1076 self
.new_mode
= self
.model
.mode_untracked
1079 class UpdateFileStatus(Command
):
1080 """Rescans for changes."""
1083 self
.model
.update_file_status()
1086 class VisualizeAll(Command
):
1087 """Visualize all branches."""
1090 browser
= utils
.shell_split(self
.model
.history_browser())
1091 utils
.fork(browser
+ ['--all'])
1094 class VisualizeCurrent(Command
):
1095 """Visualize all branches."""
1098 browser
= utils
.shell_split(self
.model
.history_browser())
1099 utils
.fork(browser
+ [self
.model
.currentbranch
])
1102 class VisualizePaths(Command
):
1103 """Path-limited visualization."""
1105 def __init__(self
, paths
):
1106 Command
.__init
__(self
)
1107 browser
= utils
.shell_split(self
.model
.history_browser())
1109 self
.argv
= browser
+ paths
1114 utils
.fork(self
.argv
)
1117 class VisualizeRevision(Command
):
1118 """Visualize a specific revision."""
1120 def __init__(self
, revision
, paths
=None):
1121 Command
.__init
__(self
)
1122 self
.revision
= revision
1126 argv
= utils
.shell_split(self
.model
.history_browser())
1128 argv
.append(self
.revision
)
1131 argv
.extend(self
.paths
)
1135 except Exception as e
:
1136 _
, details
= utils
.format_exception(e
)
1137 title
= N_('Error Launching History Browser')
1138 msg
= (N_('Cannot exec "%s": please configure a history browser') %
1140 Interaction
.critical(title
, message
=msg
, details
=details
)
1143 def run(cls
, *args
, **opts
):
1145 Returns a callback that runs a command
1147 If the caller of run() provides args or opts then those are
1148 used instead of the ones provided by the invoker of the callback.
1151 def runner(*local_args
, **local_opts
):
1153 do(cls
, *args
, **opts
)
1155 do(cls
, *local_args
, **local_opts
)
1160 class CommandDisabled(object):
1162 """Context manager to temporarily disable a command from running"""
1163 def __init__(self
, cmdclass
):
1164 self
.cmdclass
= cmdclass
1166 def __enter__(self
):
1167 self
.cmdclass
.DISABLED
= True
1170 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
1171 self
.cmdclass
.DISABLED
= False
1174 def do(cls
, *args
, **opts
):
1175 """Run a command in-place"""
1176 return do_cmd(cls(*args
, **opts
))
1180 if hasattr(cmd
, 'DISABLED') and cmd
.DISABLED
:
1184 except StandardError, e
:
1185 msg
, details
= utils
.format_exception(e
)
1186 Interaction
.critical(N_('Error'), message
=msg
, details
=details
)
1190 def bg(parent
, cls
, *args
, **opts
):
1192 Returns a callback that runs a command
1194 If the caller of run() provides args or opts then those are
1195 used instead of the ones provided by the invoker of the callback.
1198 def runner(*local_args
, **local_opts
):
1200 background(parent
, cls
, *args
, **opts
)
1202 background(parent
, cls
, *local_args
, **local_opts
)
1207 def background(parent
, cls
, *args
, **opts
):
1208 cmd
= cls(*args
, **opts
)
1209 task
= AsyncCommand(parent
, cmd
)
1210 QtCore
.QThreadPool
.globalInstance().start(task
)
1213 class AsyncCommand(QtCore
.QRunnable
):
1215 # Holds a reference to background tasks to avoid PyQt4 segfaults
1218 def __init__(self
, parent
, cmd
):
1219 QtCore
.QRunnable
.__init
__(self
)
1220 self
.__class
__.INSTANCES
.add(self
)
1222 self
.qobj
= QtCore
.QObject(parent
)
1223 self
.qobj
.connect(self
.qobj
, SIGNAL('command_ready'), self
.do
)
1228 self
.qobj
.emit(SIGNAL('command_ready'))
1233 self
.__class
__.INSTANCES
.remove(self
)