2 from __future__
import absolute_import
, division
, print_function
, unicode_literals
6 from fnmatch
import fnmatch
7 from io
import StringIO
10 from send2trash
import send2trash
18 from . import resources
19 from . import textwrap
22 from .cmd
import ContextCommand
23 from .diffparse
import DiffParser
24 from .git
import STDOUT
25 from .git
import EMPTY_TREE_OID
26 from .git
import MISSING_BLOB_OID
28 from .interaction
import Interaction
29 from .models
import main
30 from .models
import prefs
33 class UsageError(Exception):
34 """Exception class for usage errors."""
36 def __init__(self
, title
, message
):
37 Exception.__init
__(self
, message
)
42 class EditModel(ContextCommand
):
43 """Commands that mutate the main model diff data"""
47 def __init__(self
, context
):
48 """Common edit operations on the main model"""
49 super(EditModel
, self
).__init
__(context
)
51 self
.old_diff_text
= self
.model
.diff_text
52 self
.old_filename
= self
.model
.filename
53 self
.old_mode
= self
.model
.mode
54 self
.old_diff_type
= self
.model
.diff_type
55 self
.old_file_type
= self
.model
.file_type
57 self
.new_diff_text
= self
.old_diff_text
58 self
.new_filename
= self
.old_filename
59 self
.new_mode
= self
.old_mode
60 self
.new_diff_type
= self
.old_diff_type
61 self
.new_file_type
= self
.old_file_type
64 """Perform the operation."""
65 self
.model
.filename
= self
.new_filename
66 self
.model
.set_mode(self
.new_mode
)
67 self
.model
.set_diff_text(self
.new_diff_text
)
68 self
.model
.set_diff_type(self
.new_diff_type
)
69 self
.model
.set_file_type(self
.new_file_type
)
72 """Undo the operation."""
73 self
.model
.filename
= self
.old_filename
74 self
.model
.set_mode(self
.old_mode
)
75 self
.model
.set_diff_text(self
.old_diff_text
)
76 self
.model
.set_diff_type(self
.old_diff_type
)
77 self
.model
.set_file_type(self
.old_file_type
)
80 class ConfirmAction(ContextCommand
):
81 """Confirm an action before running it"""
83 # pylint: disable=no-self-use
85 """Return True when the command is ok to run"""
88 # pylint: disable=no-self-use
90 """Prompt for confirmation"""
93 # pylint: disable=no-self-use
95 """Run the command and return (status, out, err)"""
98 # pylint: disable=no-self-use
100 """Callback run on success"""
103 # pylint: disable=no-self-use
105 """Command name, for error messages"""
108 # pylint: disable=no-self-use
109 def error_message(self
):
110 """Command error message"""
114 """Prompt for confirmation before running a command"""
117 ok
= self
.ok_to_run() and self
.confirm()
119 status
, out
, err
= self
.action()
122 title
= self
.error_message()
124 Interaction
.command(title
, cmd
, status
, out
, err
)
126 return ok
, status
, out
, err
129 class AbortMerge(ConfirmAction
):
130 """Reset an in-progress merge back to HEAD"""
133 title
= N_('Abort Merge...')
134 question
= N_('Aborting the current merge?')
136 'Aborting the current merge will cause '
137 '*ALL* uncommitted changes to be lost.\n'
138 'Recovering uncommitted changes is not possible.'
140 ok_txt
= N_('Abort Merge')
141 return Interaction
.confirm(
142 title
, question
, info
, ok_txt
, default
=False, icon
=icons
.undo()
146 status
, out
, err
= gitcmds
.abort_merge(self
.context
)
147 self
.model
.update_file_status()
148 return status
, out
, err
151 self
.model
.set_commitmsg('')
153 def error_message(self
):
160 class AmendMode(EditModel
):
161 """Try to amend a commit."""
170 def __init__(self
, context
, amend
=True):
171 super(AmendMode
, self
).__init
__(context
)
173 self
.amending
= amend
174 self
.old_commitmsg
= self
.model
.commitmsg
175 self
.old_mode
= self
.model
.mode
178 self
.new_mode
= self
.model
.mode_amend
179 self
.new_commitmsg
= gitcmds
.prev_commitmsg(context
)
180 AmendMode
.LAST_MESSAGE
= self
.model
.commitmsg
182 # else, amend unchecked, regular commit
183 self
.new_mode
= self
.model
.mode_none
184 self
.new_diff_text
= ''
185 self
.new_commitmsg
= self
.model
.commitmsg
186 # If we're going back into new-commit-mode then search the
187 # undo stack for a previous amend-commit-mode and grab the
188 # commit message at that point in time.
189 if AmendMode
.LAST_MESSAGE
is not None:
190 self
.new_commitmsg
= AmendMode
.LAST_MESSAGE
191 AmendMode
.LAST_MESSAGE
= None
194 """Leave/enter amend mode."""
195 # Attempt to enter amend mode. Do not allow this when merging.
197 if self
.model
.is_merging
:
199 self
.model
.set_mode(self
.old_mode
)
200 Interaction
.information(
203 'You are in the middle of a merge.\n'
204 'Cannot amend while merging.'
209 super(AmendMode
, self
).do()
210 self
.model
.set_commitmsg(self
.new_commitmsg
)
211 self
.model
.update_file_status()
216 self
.model
.set_commitmsg(self
.old_commitmsg
)
217 super(AmendMode
, self
).undo()
218 self
.model
.update_file_status()
221 class AnnexAdd(ContextCommand
):
222 """Add to Git Annex"""
224 def __init__(self
, context
):
225 super(AnnexAdd
, self
).__init
__(context
)
226 self
.filename
= self
.selection
.filename()
229 status
, out
, err
= self
.git
.annex('add', self
.filename
)
230 Interaction
.command(N_('Error'), 'git annex add', status
, out
, err
)
231 self
.model
.update_status()
234 class AnnexInit(ContextCommand
):
235 """Initialize Git Annex"""
238 status
, out
, err
= self
.git
.annex('init')
239 Interaction
.command(N_('Error'), 'git annex init', status
, out
, err
)
240 self
.model
.cfg
.reset()
241 self
.model
.emit_updated()
244 class LFSTrack(ContextCommand
):
245 """Add a file to git lfs"""
247 def __init__(self
, context
):
248 super(LFSTrack
, self
).__init
__(context
)
249 self
.filename
= self
.selection
.filename()
250 self
.stage_cmd
= Stage(context
, [self
.filename
])
253 status
, out
, err
= self
.git
.lfs('track', self
.filename
)
254 Interaction
.command(N_('Error'), 'git lfs track', status
, out
, err
)
259 class LFSInstall(ContextCommand
):
260 """Initialize git lfs"""
263 status
, out
, err
= self
.git
.lfs('install')
264 Interaction
.command(N_('Error'), 'git lfs install', status
, out
, err
)
265 self
.model
.update_config(reset
=True, emit
=True)
268 class ApplyDiffSelection(ContextCommand
):
269 """Apply the selected diff to the worktree or index"""
280 super(ApplyDiffSelection
, self
).__init
__(context
)
281 self
.first_line_idx
= first_line_idx
282 self
.last_line_idx
= last_line_idx
283 self
.has_selection
= has_selection
284 self
.reverse
= reverse
285 self
.apply_to_worktree
= apply_to_worktree
288 context
= self
.context
289 cfg
= self
.context
.cfg
290 diff_text
= self
.model
.diff_text
292 parser
= DiffParser(self
.model
.filename
, diff_text
)
293 if self
.has_selection
:
294 patch
= parser
.generate_patch(
295 self
.first_line_idx
, self
.last_line_idx
, reverse
=self
.reverse
298 patch
= parser
.generate_hunk_patch(
299 self
.first_line_idx
, reverse
=self
.reverse
304 if isinstance(diff_text
, core
.UStr
):
305 # original encoding must prevail
306 encoding
= diff_text
.encoding
308 encoding
= cfg
.file_encoding(self
.model
.filename
)
310 tmp_file
= utils
.tmp_filename('patch')
312 core
.write(tmp_file
, patch
, encoding
=encoding
)
313 if self
.apply_to_worktree
:
314 status
, out
, err
= gitcmds
.apply_diff_to_worktree(context
, tmp_file
)
316 status
, out
, err
= gitcmds
.apply_diff(context
, tmp_file
)
318 core
.unlink(tmp_file
)
320 Interaction
.log_status(status
, out
, err
)
321 self
.model
.update_file_status(update_index
=True)
324 class ApplyPatches(ContextCommand
):
325 """Apply patches using the "git am" command"""
327 def __init__(self
, context
, patches
):
328 super(ApplyPatches
, self
).__init
__(context
)
329 self
.patches
= patches
332 status
, out
, err
= self
.git
.am('-3', *self
.patches
)
333 Interaction
.log_status(status
, out
, err
)
336 self
.model
.update_file_status()
338 patch_basenames
= [os
.path
.basename(p
) for p
in self
.patches
]
339 if len(patch_basenames
) > 25:
340 patch_basenames
= patch_basenames
[:25]
341 patch_basenames
.append('...')
343 basenames
= '\n'.join(patch_basenames
)
344 Interaction
.information(
345 N_('Patch(es) Applied'),
346 (N_('%d patch(es) applied.') + '\n\n%s') % (len(self
.patches
), basenames
),
350 class Archive(ContextCommand
):
351 """ "Export archives using the "git archive" command"""
353 def __init__(self
, context
, ref
, fmt
, prefix
, filename
):
354 super(Archive
, self
).__init
__(context
)
358 self
.filename
= filename
361 fp
= core
.xopen(self
.filename
, 'wb')
362 cmd
= ['git', 'archive', '--format=' + self
.fmt
]
363 if self
.fmt
in ('tgz', 'tar.gz'):
366 cmd
.append('--prefix=' + self
.prefix
)
368 proc
= core
.start_command(cmd
, stdout
=fp
)
369 out
, err
= proc
.communicate()
371 status
= proc
.returncode
372 Interaction
.log_status(status
, out
or '', err
or '')
375 class Checkout(EditModel
):
376 """A command object for git-checkout.
378 'argv' is handed off directly to git.
382 def __init__(self
, context
, argv
, checkout_branch
=False):
383 super(Checkout
, self
).__init
__(context
)
385 self
.checkout_branch
= checkout_branch
386 self
.new_diff_text
= ''
387 self
.new_diff_type
= main
.Types
.TEXT
388 self
.new_file_type
= main
.Types
.TEXT
391 super(Checkout
, self
).do()
392 status
, out
, err
= self
.git
.checkout(*self
.argv
)
393 if self
.checkout_branch
:
394 self
.model
.update_status()
396 self
.model
.update_file_status()
397 Interaction
.command(N_('Error'), 'git checkout', status
, out
, err
)
400 class BlamePaths(ContextCommand
):
401 """Blame view for paths."""
405 return N_('Blame...')
407 def __init__(self
, context
, paths
=None):
408 super(BlamePaths
, self
).__init
__(context
)
410 paths
= context
.selection
.union()
411 viewer
= utils
.shell_split(prefs
.blame_viewer(context
))
412 self
.argv
= viewer
+ list(paths
)
418 _
, details
= utils
.format_exception(e
)
419 title
= N_('Error Launching Blame Viewer')
420 msg
= N_('Cannot exec "%s": please configure a blame viewer') % ' '.join(
423 Interaction
.critical(title
, message
=msg
, details
=details
)
426 class CheckoutBranch(Checkout
):
427 """Checkout a branch."""
429 def __init__(self
, context
, branch
):
431 super(CheckoutBranch
, self
).__init
__(context
, args
, checkout_branch
=True)
434 class CherryPick(ContextCommand
):
435 """Cherry pick commits into the current branch."""
437 def __init__(self
, context
, commits
):
438 super(CherryPick
, self
).__init
__(context
)
439 self
.commits
= commits
442 self
.model
.cherry_pick_list(self
.commits
)
443 self
.model
.update_file_status()
446 class Revert(ContextCommand
):
447 """Cherry pick commits into the current branch."""
449 def __init__(self
, context
, oid
):
450 super(Revert
, self
).__init
__(context
)
454 self
.git
.revert(self
.oid
, no_edit
=True)
455 self
.model
.update_file_status()
458 class ResetMode(EditModel
):
459 """Reset the mode and clear the model's diff text."""
461 def __init__(self
, context
):
462 super(ResetMode
, self
).__init
__(context
)
463 self
.new_mode
= self
.model
.mode_none
464 self
.new_diff_text
= ''
465 self
.new_diff_type
= main
.Types
.TEXT
466 self
.new_file_type
= main
.Types
.TEXT
467 self
.new_filename
= ''
470 super(ResetMode
, self
).do()
471 self
.model
.update_file_status()
474 class ResetCommand(ConfirmAction
):
475 """Reset state using the "git reset" command"""
477 def __init__(self
, context
, ref
):
478 super(ResetCommand
, self
).__init
__(context
)
487 def error_message(self
):
491 self
.model
.update_file_status()
494 raise NotImplementedError('confirm() must be overridden')
497 raise NotImplementedError('reset() must be overridden')
500 class ResetMixed(ResetCommand
):
503 tooltip
= N_('The branch will be reset using "git reset --mixed %s"')
507 title
= N_('Reset Branch and Stage (Mixed)')
508 question
= N_('Point the current branch head to a new commit?')
509 info
= self
.tooltip(self
.ref
)
510 ok_text
= N_('Reset Branch')
511 return Interaction
.confirm(title
, question
, info
, ok_text
)
514 return self
.git
.reset(self
.ref
, '--', mixed
=True)
517 class ResetKeep(ResetCommand
):
520 tooltip
= N_('The repository will be reset using "git reset --keep %s"')
524 title
= N_('Restore Worktree and Reset All (Keep Unstaged Changes)')
525 question
= N_('Restore worktree, reset, and preserve unstaged edits?')
526 info
= self
.tooltip(self
.ref
)
527 ok_text
= N_('Reset and Restore')
528 return Interaction
.confirm(title
, question
, info
, ok_text
)
531 return self
.git
.reset(self
.ref
, '--', keep
=True)
534 class ResetMerge(ResetCommand
):
537 tooltip
= N_('The repository will be reset using "git reset --merge %s"')
541 title
= N_('Restore Worktree and Reset All (Merge)')
542 question
= N_('Reset Worktree and Reset All?')
543 info
= self
.tooltip(self
.ref
)
544 ok_text
= N_('Reset and Restore')
545 return Interaction
.confirm(title
, question
, info
, ok_text
)
548 return self
.git
.reset(self
.ref
, '--', merge
=True)
551 class ResetSoft(ResetCommand
):
554 tooltip
= N_('The branch will be reset using "git reset --soft %s"')
558 title
= N_('Reset Branch (Soft)')
559 question
= N_('Reset branch?')
560 info
= self
.tooltip(self
.ref
)
561 ok_text
= N_('Reset Branch')
562 return Interaction
.confirm(title
, question
, info
, ok_text
)
565 return self
.git
.reset(self
.ref
, '--', soft
=True)
568 class ResetHard(ResetCommand
):
571 tooltip
= N_('The repository will be reset using "git reset --hard %s"')
575 title
= N_('Restore Worktree and Reset All (Hard)')
576 question
= N_('Restore Worktree and Reset All?')
577 info
= self
.tooltip(self
.ref
)
578 ok_text
= N_('Reset and Restore')
579 return Interaction
.confirm(title
, question
, info
, ok_text
)
582 return self
.git
.reset(self
.ref
, '--', hard
=True)
585 class RestoreWorktree(ConfirmAction
):
586 """Reset the worktree using the "git read-tree" command"""
591 'The worktree will be restored using "git read-tree --reset -u %s"'
595 def __init__(self
, context
, ref
):
596 super(RestoreWorktree
, self
).__init
__(context
)
600 return self
.git
.read_tree(self
.ref
, reset
=True, u
=True)
603 return 'git read-tree --reset -u %s' % self
.ref
605 def error_message(self
):
609 self
.model
.update_file_status()
612 title
= N_('Restore Worktree')
613 question
= N_('Restore Worktree to %s?') % self
.ref
614 info
= self
.tooltip(self
.ref
)
615 ok_text
= N_('Restore Worktree')
616 return Interaction
.confirm(title
, question
, info
, ok_text
)
619 class UndoLastCommit(ResetCommand
):
620 """Undo the last commit"""
622 # NOTE: this is the similar to ResetSoft() with an additional check for
623 # published commits and different messages.
624 def __init__(self
, context
):
625 super(UndoLastCommit
, self
).__init
__(context
, 'HEAD^')
628 check_published
= prefs
.check_published_commits(self
.context
)
629 if check_published
and self
.model
.is_commit_published():
630 return Interaction
.confirm(
631 N_('Rewrite Published Commit?'),
633 'This commit has already been published.\n'
634 'This operation will rewrite published history.\n'
635 'You probably don\'t want to do this.'
637 N_('Undo the published commit?'),
638 N_('Undo Last Commit'),
643 title
= N_('Undo Last Commit')
644 question
= N_('Undo last commit?')
645 info
= N_('The branch will be reset using "git reset --soft %s"')
646 ok_text
= N_('Undo Last Commit')
647 info_text
= info
% self
.ref
648 return Interaction
.confirm(title
, question
, info_text
, ok_text
)
651 return self
.git
.reset('HEAD^', '--', soft
=True)
654 class Commit(ResetMode
):
655 """Attempt to create a new commit."""
657 def __init__(self
, context
, amend
, msg
, sign
, no_verify
=False):
658 super(Commit
, self
).__init
__(context
)
662 self
.no_verify
= no_verify
663 self
.old_commitmsg
= self
.model
.commitmsg
664 self
.new_commitmsg
= ''
667 # Create the commit message file
668 context
= self
.context
669 comment_char
= prefs
.comment_char(context
)
670 msg
= self
.strip_comments(self
.msg
, comment_char
=comment_char
)
671 tmp_file
= utils
.tmp_filename('commit-message')
673 core
.write(tmp_file
, msg
)
675 status
, out
, err
= self
.git
.commit(
680 no_verify
=self
.no_verify
,
683 core
.unlink(tmp_file
)
685 super(Commit
, self
).do()
686 if context
.cfg
.get(prefs
.AUTOTEMPLATE
):
687 template_loader
= LoadCommitMessageFromTemplate(context
)
690 self
.model
.set_commitmsg(self
.new_commitmsg
)
692 title
= N_('Commit failed')
693 Interaction
.command(title
, 'git commit', status
, out
, err
)
695 return status
, out
, err
698 def strip_comments(msg
, comment_char
='#'):
701 line
for line
in msg
.split('\n') if not line
.startswith(comment_char
)
703 msg
= '\n'.join(message_lines
)
704 if not msg
.endswith('\n'):
710 class CycleReferenceSort(ContextCommand
):
711 """Choose the next reference sort type"""
714 self
.model
.cycle_ref_sort()
717 class Ignore(ContextCommand
):
718 """Add files to an exclusion file"""
720 def __init__(self
, context
, filenames
, local
=False):
721 super(Ignore
, self
).__init
__(context
)
722 self
.filenames
= list(filenames
)
726 if not self
.filenames
:
728 new_additions
= '\n'.join(self
.filenames
) + '\n'
729 for_status
= new_additions
731 filename
= os
.path
.join('.git', 'info', 'exclude')
733 filename
= '.gitignore'
734 if core
.exists(filename
):
735 current_list
= core
.read(filename
)
736 new_additions
= current_list
.rstrip() + '\n' + new_additions
737 core
.write(filename
, new_additions
)
738 Interaction
.log_status(0, 'Added to %s:\n%s' % (filename
, for_status
), '')
739 self
.model
.update_file_status()
742 def file_summary(files
):
743 txt
= core
.list2cmdline(files
)
745 txt
= txt
[:768].rstrip() + '...'
746 wrap
= textwrap
.TextWrapper()
747 return '\n'.join(wrap
.wrap(txt
))
750 class RemoteCommand(ConfirmAction
):
751 def __init__(self
, context
, remote
):
752 super(RemoteCommand
, self
).__init
__(context
)
757 self
.model
.update_remotes()
760 class RemoteAdd(RemoteCommand
):
761 def __init__(self
, context
, remote
, url
):
762 super(RemoteAdd
, self
).__init
__(context
, remote
)
766 return self
.git
.remote('add', self
.remote
, self
.url
)
768 def error_message(self
):
769 return N_('Error creating remote "%s"') % self
.remote
772 return 'git remote add "%s" "%s"' % (self
.remote
, self
.url
)
775 class RemoteRemove(RemoteCommand
):
777 title
= N_('Delete Remote')
778 question
= N_('Delete remote?')
779 info
= N_('Delete remote "%s"') % self
.remote
780 ok_text
= N_('Delete')
781 return Interaction
.confirm(title
, question
, info
, ok_text
)
784 return self
.git
.remote('rm', self
.remote
)
786 def error_message(self
):
787 return N_('Error deleting remote "%s"') % self
.remote
790 return 'git remote rm "%s"' % self
.remote
793 class RemoteRename(RemoteCommand
):
794 def __init__(self
, context
, remote
, new_name
):
795 super(RemoteRename
, self
).__init
__(context
, remote
)
796 self
.new_name
= new_name
799 title
= N_('Rename Remote')
800 text
= N_('Rename remote "%(current)s" to "%(new)s"?') % dict(
801 current
=self
.remote
, new
=self
.new_name
805 return Interaction
.confirm(title
, text
, info_text
, ok_text
)
808 return self
.git
.remote('rename', self
.remote
, self
.new_name
)
810 def error_message(self
):
811 return N_('Error renaming "%(name)s" to "%(new_name)s"') % dict(
812 name
=self
.remote
, new_name
=self
.new_name
816 return 'git remote rename "%s" "%s"' % (self
.remote
, self
.new_name
)
819 class RemoteSetURL(RemoteCommand
):
820 def __init__(self
, context
, remote
, url
):
821 super(RemoteSetURL
, self
).__init
__(context
, remote
)
825 return self
.git
.remote('set-url', self
.remote
, self
.url
)
827 def error_message(self
):
828 return N_('Unable to set URL for "%(name)s" to "%(url)s"') % dict(
829 name
=self
.remote
, url
=self
.url
833 return 'git remote set-url "%s" "%s"' % (self
.remote
, self
.url
)
836 class RemoteEdit(ContextCommand
):
837 """Combine RemoteRename and RemoteSetURL"""
839 def __init__(self
, context
, old_name
, remote
, url
):
840 super(RemoteEdit
, self
).__init
__(context
)
841 self
.rename
= RemoteRename(context
, old_name
, remote
)
842 self
.set_url
= RemoteSetURL(context
, remote
, url
)
845 result
= self
.rename
.do()
849 result
= self
.set_url
.do()
851 return name_ok
, url_ok
854 class RemoveFromSettings(ConfirmAction
):
855 def __init__(self
, context
, repo
, entry
, icon
=None):
856 super(RemoveFromSettings
, self
).__init
__(context
)
857 self
.context
= context
863 self
.context
.settings
.save()
866 class RemoveBookmark(RemoveFromSettings
):
869 title
= msg
= N_('Delete Bookmark?')
870 info
= N_('%s will be removed from your bookmarks.') % entry
871 ok_text
= N_('Delete Bookmark')
872 return Interaction
.confirm(title
, msg
, info
, ok_text
, icon
=self
.icon
)
875 self
.context
.settings
.remove_bookmark(self
.repo
, self
.entry
)
879 class RemoveRecent(RemoveFromSettings
):
882 title
= msg
= N_('Remove %s from the recent list?') % repo
883 info
= N_('%s will be removed from your recent repositories.') % repo
884 ok_text
= N_('Remove')
885 return Interaction
.confirm(title
, msg
, info
, ok_text
, icon
=self
.icon
)
888 self
.context
.settings
.remove_recent(self
.repo
)
892 class RemoveFiles(ContextCommand
):
895 def __init__(self
, context
, remover
, filenames
):
896 super(RemoveFiles
, self
).__init
__(context
)
899 self
.remover
= remover
900 self
.filenames
= filenames
901 # We could git-hash-object stuff and provide undo-ability
905 files
= self
.filenames
911 remove
= self
.remover
912 for filename
in files
:
918 bad_filenames
.append(filename
)
921 Interaction
.information(
922 N_('Error'), N_('Deleting "%s" failed') % file_summary(bad_filenames
)
926 self
.model
.update_file_status()
929 class Delete(RemoveFiles
):
932 def __init__(self
, context
, filenames
):
933 super(Delete
, self
).__init
__(context
, os
.remove
, filenames
)
936 files
= self
.filenames
940 title
= N_('Delete Files?')
941 msg
= N_('The following files will be deleted:') + '\n\n'
942 msg
+= file_summary(files
)
943 info_txt
= N_('Delete %d file(s)?') % len(files
)
944 ok_txt
= N_('Delete Files')
946 if Interaction
.confirm(
947 title
, msg
, info_txt
, ok_txt
, default
=True, icon
=icons
.remove()
949 super(Delete
, self
).do()
952 class MoveToTrash(RemoveFiles
):
953 """Move files to the trash using send2trash"""
955 AVAILABLE
= send2trash
is not None
957 def __init__(self
, context
, filenames
):
958 super(MoveToTrash
, self
).__init
__(context
, send2trash
, filenames
)
961 class DeleteBranch(ConfirmAction
):
962 """Delete a git branch."""
964 def __init__(self
, context
, branch
):
965 super(DeleteBranch
, self
).__init
__(context
)
969 title
= N_('Delete Branch')
970 question
= N_('Delete branch "%s"?') % self
.branch
971 info
= N_('The branch will be no longer available.')
972 ok_txt
= N_('Delete Branch')
973 return Interaction
.confirm(
974 title
, question
, info
, ok_txt
, default
=True, icon
=icons
.discard()
978 return self
.model
.delete_branch(self
.branch
)
980 def error_message(self
):
981 return N_('Error deleting branch "%s"' % self
.branch
)
984 command
= 'git branch -D %s'
985 return command
% self
.branch
988 class Rename(ContextCommand
):
989 """Rename a set of paths."""
991 def __init__(self
, context
, paths
):
992 super(Rename
, self
).__init
__(context
)
996 msg
= N_('Untracking: %s') % (', '.join(self
.paths
))
999 for path
in self
.paths
:
1000 ok
= self
.rename(path
)
1004 self
.model
.update_status()
1006 def rename(self
, path
):
1008 title
= N_('Rename "%s"') % path
1010 if os
.path
.isdir(path
):
1011 base_path
= os
.path
.dirname(path
)
1014 new_path
= Interaction
.save_as(base_path
, title
)
1018 status
, out
, err
= git
.mv(path
, new_path
, force
=True, verbose
=True)
1019 Interaction
.command(N_('Error'), 'git mv', status
, out
, err
)
1023 class RenameBranch(ContextCommand
):
1024 """Rename a git branch."""
1026 def __init__(self
, context
, branch
, new_branch
):
1027 super(RenameBranch
, self
).__init
__(context
)
1028 self
.branch
= branch
1029 self
.new_branch
= new_branch
1032 branch
= self
.branch
1033 new_branch
= self
.new_branch
1034 status
, out
, err
= self
.model
.rename_branch(branch
, new_branch
)
1035 Interaction
.log_status(status
, out
, err
)
1038 class DeleteRemoteBranch(DeleteBranch
):
1039 """Delete a remote git branch."""
1041 def __init__(self
, context
, remote
, branch
):
1042 super(DeleteRemoteBranch
, self
).__init
__(context
, branch
)
1043 self
.remote
= remote
1046 return self
.git
.push(self
.remote
, self
.branch
, delete
=True)
1049 self
.model
.update_status()
1050 Interaction
.information(
1051 N_('Remote Branch Deleted'),
1052 N_('"%(branch)s" has been deleted from "%(remote)s".')
1053 % dict(branch
=self
.branch
, remote
=self
.remote
),
1056 def error_message(self
):
1057 return N_('Error Deleting Remote Branch')
1060 command
= 'git push --delete %s %s'
1061 return command
% (self
.remote
, self
.branch
)
1064 def get_mode(model
, staged
, modified
, unmerged
, untracked
):
1066 mode
= model
.mode_index
1067 elif modified
or unmerged
:
1068 mode
= model
.mode_worktree
1070 mode
= model
.mode_untracked
1076 class DiffText(EditModel
):
1077 """Set the diff type to text"""
1079 def __init__(self
, context
):
1080 super(DiffText
, self
).__init
__(context
)
1081 self
.new_file_type
= main
.Types
.TEXT
1082 self
.new_diff_type
= main
.Types
.TEXT
1085 class ToggleDiffType(ContextCommand
):
1086 """Toggle the diff type between image and text"""
1088 def __init__(self
, context
):
1089 super(ToggleDiffType
, self
).__init
__(context
)
1090 if self
.model
.diff_type
== main
.Types
.IMAGE
:
1091 self
.new_diff_type
= main
.Types
.TEXT
1092 self
.new_value
= False
1094 self
.new_diff_type
= main
.Types
.IMAGE
1095 self
.new_value
= True
1098 diff_type
= self
.new_diff_type
1099 value
= self
.new_value
1101 self
.model
.set_diff_type(diff_type
)
1103 filename
= self
.model
.filename
1104 _
, ext
= os
.path
.splitext(filename
)
1105 if ext
.startswith('.'):
1106 cfg
= 'cola.imagediff' + ext
1107 self
.cfg
.set_repo(cfg
, value
)
1110 class DiffImage(EditModel
):
1112 self
, context
, filename
, deleted
, staged
, modified
, unmerged
, untracked
1114 super(DiffImage
, self
).__init
__(context
)
1116 self
.new_filename
= filename
1117 self
.new_diff_type
= self
.get_diff_type(filename
)
1118 self
.new_file_type
= main
.Types
.IMAGE
1119 self
.new_mode
= get_mode(self
.model
, staged
, modified
, unmerged
, untracked
)
1120 self
.staged
= staged
1121 self
.modified
= modified
1122 self
.unmerged
= unmerged
1123 self
.untracked
= untracked
1124 self
.deleted
= deleted
1125 self
.annex
= self
.cfg
.is_annex()
1127 def get_diff_type(self
, filename
):
1128 """Query the diff type to use based on cola.imagediff.<extension>"""
1129 _
, ext
= os
.path
.splitext(filename
)
1130 if ext
.startswith('.'):
1131 # Check eg. "cola.imagediff.svg" to see if we should imagediff.
1132 cfg
= 'cola.imagediff' + ext
1133 if self
.cfg
.get(cfg
, True):
1134 result
= main
.Types
.IMAGE
1136 result
= main
.Types
.TEXT
1138 result
= main
.Types
.IMAGE
1142 filename
= self
.new_filename
1145 images
= self
.staged_images()
1147 images
= self
.modified_images()
1149 images
= self
.unmerged_images()
1150 elif self
.untracked
:
1151 images
= [(filename
, False)]
1155 self
.model
.set_images(images
)
1156 super(DiffImage
, self
).do()
1158 def staged_images(self
):
1159 context
= self
.context
1161 head
= self
.model
.head
1162 filename
= self
.new_filename
1166 index
= git
.diff_index(head
, '--', filename
, cached
=True)[STDOUT
]
1169 # :100644 100644 fabadb8... 4866510... M describe.c
1170 parts
= index
.split(' ')
1175 if old_oid
!= MISSING_BLOB_OID
:
1176 # First, check if we can get a pre-image from git-annex
1179 annex_image
= gitcmds
.annex_path(context
, head
, filename
)
1181 images
.append((annex_image
, False)) # git annex HEAD
1183 image
= gitcmds
.write_blob_path(context
, head
, old_oid
, filename
)
1185 images
.append((image
, True))
1187 if new_oid
!= MISSING_BLOB_OID
:
1188 found_in_annex
= False
1189 if annex
and core
.islink(filename
):
1190 status
, out
, _
= git
.annex('status', '--', filename
)
1192 details
= out
.split(' ')
1193 if details
and details
[0] == 'A': # newly added file
1194 images
.append((filename
, False))
1195 found_in_annex
= True
1197 if not found_in_annex
:
1198 image
= gitcmds
.write_blob(context
, new_oid
, filename
)
1200 images
.append((image
, True))
1204 def unmerged_images(self
):
1205 context
= self
.context
1207 head
= self
.model
.head
1208 filename
= self
.new_filename
1211 candidate_merge_heads
= ('HEAD', 'CHERRY_HEAD', 'MERGE_HEAD')
1214 for merge_head
in candidate_merge_heads
1215 if core
.exists(git
.git_path(merge_head
))
1218 if annex
: # Attempt to find files in git-annex
1220 for merge_head
in merge_heads
:
1221 image
= gitcmds
.annex_path(context
, merge_head
, filename
)
1223 annex_images
.append((image
, False))
1225 annex_images
.append((filename
, False))
1228 # DIFF FORMAT FOR MERGES
1229 # "git-diff-tree", "git-diff-files" and "git-diff --raw"
1230 # can take -c or --cc option to generate diff output also
1231 # for merge commits. The output differs from the format
1232 # described above in the following way:
1234 # 1. there is a colon for each parent
1235 # 2. there are more "src" modes and "src" sha1
1236 # 3. status is concatenated status characters for each parent
1237 # 4. no optional "score" number
1238 # 5. single path, only for "dst"
1240 # ::100644 100644 100644 fabadb8... cc95eb0... 4866510... \
1243 index
= git
.diff_index(head
, '--', filename
, cached
=True, cc
=True)[STDOUT
]
1245 parts
= index
.split(' ')
1247 first_mode
= parts
[0]
1248 num_parents
= first_mode
.count(':')
1249 # colon for each parent, but for the index, the "parents"
1250 # are really entries in stages 1,2,3 (head, base, remote)
1251 # remote, base, head
1252 for i
in range(num_parents
):
1253 offset
= num_parents
+ i
+ 1
1256 merge_head
= merge_heads
[i
]
1259 if oid
!= MISSING_BLOB_OID
:
1260 image
= gitcmds
.write_blob_path(
1261 context
, merge_head
, oid
, filename
1264 images
.append((image
, True))
1266 images
.append((filename
, False))
1269 def modified_images(self
):
1270 context
= self
.context
1272 head
= self
.model
.head
1273 filename
= self
.new_filename
1278 if annex
: # Check for a pre-image from git-annex
1279 annex_image
= gitcmds
.annex_path(context
, head
, filename
)
1281 images
.append((annex_image
, False)) # git annex HEAD
1283 worktree
= git
.diff_files('--', filename
)[STDOUT
]
1284 parts
= worktree
.split(' ')
1287 if oid
!= MISSING_BLOB_OID
:
1288 image
= gitcmds
.write_blob_path(context
, head
, oid
, filename
)
1290 images
.append((image
, True)) # HEAD
1292 images
.append((filename
, False)) # worktree
1296 class Diff(EditModel
):
1297 """Perform a diff and set the model's current text."""
1299 def __init__(self
, context
, filename
, cached
=False, deleted
=False):
1300 super(Diff
, self
).__init
__(context
)
1302 if cached
and gitcmds
.is_valid_ref(context
, self
.model
.head
):
1303 opts
['ref'] = self
.model
.head
1304 self
.new_filename
= filename
1305 self
.new_mode
= self
.model
.mode_worktree
1306 self
.new_diff_text
= gitcmds
.diff_helper(
1307 self
.context
, filename
=filename
, cached
=cached
, deleted
=deleted
, **opts
1311 class Diffstat(EditModel
):
1312 """Perform a diffstat and set the model's diff text."""
1314 def __init__(self
, context
):
1315 super(Diffstat
, self
).__init
__(context
)
1317 diff_context
= cfg
.get('diff.context', 3)
1318 diff
= self
.git
.diff(
1320 unified
=diff_context
,
1326 self
.new_diff_text
= diff
1327 self
.new_diff_type
= main
.Types
.TEXT
1328 self
.new_file_type
= main
.Types
.TEXT
1329 self
.new_mode
= self
.model
.mode_diffstat
1332 class DiffStaged(Diff
):
1333 """Perform a staged diff on a file."""
1335 def __init__(self
, context
, filename
, deleted
=None):
1336 super(DiffStaged
, self
).__init
__(
1337 context
, filename
, cached
=True, deleted
=deleted
1339 self
.new_mode
= self
.model
.mode_index
1342 class DiffStagedSummary(EditModel
):
1343 def __init__(self
, context
):
1344 super(DiffStagedSummary
, self
).__init
__(context
)
1345 diff
= self
.git
.diff(
1350 patch_with_stat
=True,
1353 self
.new_diff_text
= diff
1354 self
.new_diff_type
= main
.Types
.TEXT
1355 self
.new_file_type
= main
.Types
.TEXT
1356 self
.new_mode
= self
.model
.mode_index
1359 class Difftool(ContextCommand
):
1360 """Run git-difftool limited by path."""
1362 def __init__(self
, context
, staged
, filenames
):
1363 super(Difftool
, self
).__init
__(context
)
1364 self
.staged
= staged
1365 self
.filenames
= filenames
1368 difftool_launch_with_head(
1369 self
.context
, self
.filenames
, self
.staged
, self
.model
.head
1373 class Edit(ContextCommand
):
1374 """Edit a file using the configured gui.editor."""
1378 return N_('Launch Editor')
1380 def __init__(self
, context
, filenames
, line_number
=None, background_editor
=False):
1381 super(Edit
, self
).__init
__(context
)
1382 self
.filenames
= filenames
1383 self
.line_number
= line_number
1384 self
.background_editor
= background_editor
1387 context
= self
.context
1388 if not self
.filenames
:
1390 filename
= self
.filenames
[0]
1391 if not core
.exists(filename
):
1393 if self
.background_editor
:
1394 editor
= prefs
.background_editor(context
)
1396 editor
= prefs
.editor(context
)
1399 if self
.line_number
is None:
1400 opts
= self
.filenames
1402 # Single-file w/ line-numbers (likely from grep)
1404 '*vim*': [filename
, '+%s' % self
.line_number
],
1405 '*emacs*': ['+%s' % self
.line_number
, filename
],
1406 '*textpad*': ['%s(%s,0)' % (filename
, self
.line_number
)],
1407 '*notepad++*': ['-n%s' % self
.line_number
, filename
],
1408 '*subl*': ['%s:%s' % (filename
, self
.line_number
)],
1411 opts
= self
.filenames
1412 for pattern
, opt
in editor_opts
.items():
1413 if fnmatch(editor
, pattern
):
1418 core
.fork(utils
.shell_split(editor
) + opts
)
1419 except (OSError, ValueError) as e
:
1420 message
= N_('Cannot exec "%s": please configure your editor') % editor
1421 _
, details
= utils
.format_exception(e
)
1422 Interaction
.critical(N_('Error Editing File'), message
, details
)
1425 class FormatPatch(ContextCommand
):
1426 """Output a patch series given all revisions and a selected subset."""
1428 def __init__(self
, context
, to_export
, revs
, output
='patches'):
1429 super(FormatPatch
, self
).__init
__(context
)
1430 self
.to_export
= list(to_export
)
1431 self
.revs
= list(revs
)
1432 self
.output
= output
1435 context
= self
.context
1436 status
, out
, err
= gitcmds
.format_patchsets(
1437 context
, self
.to_export
, self
.revs
, self
.output
1439 Interaction
.log_status(status
, out
, err
)
1442 class LaunchDifftool(ContextCommand
):
1445 return N_('Launch Diff Tool')
1448 s
= self
.selection
.selection()
1451 if utils
.is_win32():
1452 core
.fork(['git', 'mergetool', '--no-prompt', '--'] + paths
)
1455 cmd
= cfg
.terminal()
1456 argv
= utils
.shell_split(cmd
)
1458 terminal
= os
.path
.basename(argv
[0])
1459 shellquote_terms
= set(['xfce4-terminal'])
1460 shellquote_default
= terminal
in shellquote_terms
1462 mergetool
= ['git', 'mergetool', '--no-prompt', '--']
1463 mergetool
.extend(paths
)
1464 needs_shellquote
= cfg
.get(
1465 'cola.terminalshellquote', shellquote_default
1468 if needs_shellquote
:
1469 argv
.append(core
.list2cmdline(mergetool
))
1471 argv
.extend(mergetool
)
1475 difftool_run(self
.context
)
1478 class LaunchTerminal(ContextCommand
):
1481 return N_('Launch Terminal')
1484 def is_available(context
):
1485 return context
.cfg
.terminal() is not None
1487 def __init__(self
, context
, path
):
1488 super(LaunchTerminal
, self
).__init
__(context
)
1492 cmd
= self
.context
.cfg
.terminal()
1495 if utils
.is_win32():
1496 argv
= ['start', '', cmd
, '--login']
1499 argv
= utils
.shell_split(cmd
)
1500 argv
.append(os
.getenv('SHELL', '/bin/sh'))
1502 core
.fork(argv
, cwd
=self
.path
, shell
=shell
)
1505 class LaunchEditor(Edit
):
1508 return N_('Launch Editor')
1510 def __init__(self
, context
):
1511 s
= context
.selection
.selection()
1512 filenames
= s
.staged
+ s
.unmerged
+ s
.modified
+ s
.untracked
1513 super(LaunchEditor
, self
).__init
__(context
, filenames
, background_editor
=True)
1516 class LaunchEditorAtLine(LaunchEditor
):
1517 """Launch an editor at the specified line"""
1519 def __init__(self
, context
):
1520 super(LaunchEditorAtLine
, self
).__init
__(context
)
1521 self
.line_number
= context
.selection
.line_number
1524 class LoadCommitMessageFromFile(ContextCommand
):
1525 """Loads a commit message from a path."""
1529 def __init__(self
, context
, path
):
1530 super(LoadCommitMessageFromFile
, self
).__init
__(context
)
1532 self
.old_commitmsg
= self
.model
.commitmsg
1533 self
.old_directory
= self
.model
.directory
1536 path
= os
.path
.expanduser(self
.path
)
1537 if not path
or not core
.isfile(path
):
1539 N_('Error: Cannot find commit template'),
1540 N_('%s: No such file or directory.') % path
,
1542 self
.model
.set_directory(os
.path
.dirname(path
))
1543 self
.model
.set_commitmsg(core
.read(path
))
1546 self
.model
.set_commitmsg(self
.old_commitmsg
)
1547 self
.model
.set_directory(self
.old_directory
)
1550 class LoadCommitMessageFromTemplate(LoadCommitMessageFromFile
):
1551 """Loads the commit message template specified by commit.template."""
1553 def __init__(self
, context
):
1555 template
= cfg
.get('commit.template')
1556 super(LoadCommitMessageFromTemplate
, self
).__init
__(context
, template
)
1559 if self
.path
is None:
1561 N_('Error: Unconfigured commit template'),
1563 'A commit template has not been configured.\n'
1564 'Use "git config" to define "commit.template"\n'
1565 'so that it points to a commit template.'
1568 return LoadCommitMessageFromFile
.do(self
)
1571 class LoadCommitMessageFromOID(ContextCommand
):
1572 """Load a previous commit message"""
1576 def __init__(self
, context
, oid
, prefix
=''):
1577 super(LoadCommitMessageFromOID
, self
).__init
__(context
)
1579 self
.old_commitmsg
= self
.model
.commitmsg
1580 self
.new_commitmsg
= prefix
+ gitcmds
.prev_commitmsg(context
, oid
)
1583 self
.model
.set_commitmsg(self
.new_commitmsg
)
1586 self
.model
.set_commitmsg(self
.old_commitmsg
)
1589 class PrepareCommitMessageHook(ContextCommand
):
1590 """Use the cola-prepare-commit-msg hook to prepare the commit message"""
1594 def __init__(self
, context
):
1595 super(PrepareCommitMessageHook
, self
).__init
__(context
)
1596 self
.old_commitmsg
= self
.model
.commitmsg
1598 def get_message(self
):
1600 title
= N_('Error running prepare-commitmsg hook')
1601 hook
= gitcmds
.prepare_commit_message_hook(self
.context
)
1603 if os
.path
.exists(hook
):
1604 filename
= self
.model
.save_commitmsg()
1605 status
, out
, err
= core
.run_command([hook
, filename
])
1608 result
= core
.read(filename
)
1610 result
= self
.old_commitmsg
1611 Interaction
.command_error(title
, hook
, status
, out
, err
)
1613 message
= N_('A hook must be provided at "%s"') % hook
1614 Interaction
.critical(title
, message
=message
)
1615 result
= self
.old_commitmsg
1620 msg
= self
.get_message()
1621 self
.model
.set_commitmsg(msg
)
1624 self
.model
.set_commitmsg(self
.old_commitmsg
)
1627 class LoadFixupMessage(LoadCommitMessageFromOID
):
1628 """Load a fixup message"""
1630 def __init__(self
, context
, oid
):
1631 super(LoadFixupMessage
, self
).__init
__(context
, oid
, prefix
='fixup! ')
1632 if self
.new_commitmsg
:
1633 self
.new_commitmsg
= self
.new_commitmsg
.splitlines()[0]
1636 class Merge(ContextCommand
):
1639 def __init__(self
, context
, revision
, no_commit
, squash
, no_ff
, sign
):
1640 super(Merge
, self
).__init
__(context
)
1641 self
.revision
= revision
1643 self
.no_commit
= no_commit
1644 self
.squash
= squash
1648 squash
= self
.squash
1649 revision
= self
.revision
1651 no_commit
= self
.no_commit
1654 status
, out
, err
= self
.git
.merge(
1655 revision
, gpg_sign
=sign
, no_ff
=no_ff
, no_commit
=no_commit
, squash
=squash
1657 self
.model
.update_status()
1658 title
= N_('Merge failed. Conflict resolution is required.')
1659 Interaction
.command(title
, 'git merge', status
, out
, err
)
1661 return status
, out
, err
1664 class OpenDefaultApp(ContextCommand
):
1665 """Open a file using the OS default."""
1669 return N_('Open Using Default Application')
1671 def __init__(self
, context
, filenames
):
1672 super(OpenDefaultApp
, self
).__init
__(context
)
1673 self
.filenames
= filenames
1676 if not self
.filenames
:
1678 utils
.launch_default_app(self
.filenames
)
1681 class OpenDir(OpenDefaultApp
):
1682 """Open directories using the OS default."""
1686 return N_('Open Directory')
1689 def _dirnames(self
):
1690 return self
.filenames
1693 dirnames
= self
._dirnames
1696 # An empty dirname defaults to CWD.
1697 dirs
= [(dirname
or core
.getcwd()) for dirname
in dirnames
]
1698 utils
.launch_default_app(dirs
)
1701 class OpenParentDir(OpenDir
):
1702 """Open parent directories using the OS default."""
1706 return N_('Open Parent Directory')
1709 def _dirnames(self
):
1710 dirnames
= list(set([os
.path
.dirname(x
) for x
in self
.filenames
]))
1714 class OpenWorktree(OpenDir
):
1715 """Open worktree directory using the OS default."""
1719 return N_('Open Worktree')
1721 # The _unused parameter is needed by worktree_dir_action() -> common.cmd_action().
1722 def __init__(self
, context
, _unused
=None):
1723 dirnames
= [context
.git
.worktree()]
1724 super(OpenWorktree
, self
).__init
__(context
, dirnames
)
1727 class OpenNewRepo(ContextCommand
):
1728 """Launches git-cola on a repo."""
1730 def __init__(self
, context
, repo_path
):
1731 super(OpenNewRepo
, self
).__init
__(context
)
1732 self
.repo_path
= repo_path
1735 self
.model
.set_directory(self
.repo_path
)
1736 core
.fork([sys
.executable
, sys
.argv
[0], '--repo', self
.repo_path
])
1739 class OpenRepo(EditModel
):
1740 def __init__(self
, context
, repo_path
):
1741 super(OpenRepo
, self
).__init
__(context
)
1742 self
.repo_path
= repo_path
1743 self
.new_mode
= self
.model
.mode_none
1744 self
.new_diff_text
= ''
1745 self
.new_diff_type
= main
.Types
.TEXT
1746 self
.new_file_type
= main
.Types
.TEXT
1747 self
.new_commitmsg
= ''
1748 self
.new_filename
= ''
1751 old_repo
= self
.git
.getcwd()
1752 if self
.model
.set_worktree(self
.repo_path
):
1753 self
.fsmonitor
.stop()
1754 self
.fsmonitor
.start()
1755 self
.model
.update_status(reset
=True)
1756 # Check if template should be loaded
1757 if self
.context
.cfg
.get(prefs
.AUTOTEMPLATE
):
1758 template_loader
= LoadCommitMessageFromTemplate(self
.context
)
1759 template_loader
.do()
1761 self
.model
.set_commitmsg(self
.new_commitmsg
)
1762 settings
= self
.context
.settings
1764 settings
.add_recent(self
.repo_path
, prefs
.maxrecent(self
.context
))
1766 super(OpenRepo
, self
).do()
1768 self
.model
.set_worktree(old_repo
)
1771 class OpenParentRepo(OpenRepo
):
1772 def __init__(self
, context
):
1774 if version
.check_git(context
, 'show-superproject-working-tree'):
1775 status
, out
, _
= context
.git
.rev_parse(show_superproject_working_tree
=True)
1779 path
= os
.path
.dirname(core
.getcwd())
1780 super(OpenParentRepo
, self
).__init
__(context
, path
)
1783 class Clone(ContextCommand
):
1784 """Clones a repository and optionally spawns a new cola session."""
1787 self
, context
, url
, new_directory
, submodules
=False, shallow
=False, spawn
=True
1789 super(Clone
, self
).__init
__(context
)
1791 self
.new_directory
= new_directory
1792 self
.submodules
= submodules
1793 self
.shallow
= shallow
1803 recurse_submodules
= self
.submodules
1804 shallow_submodules
= self
.submodules
and self
.shallow
1806 status
, out
, err
= self
.git
.clone(
1809 recurse_submodules
=recurse_submodules
,
1810 shallow_submodules
=shallow_submodules
,
1814 self
.status
= status
1817 if status
== 0 and self
.spawn
:
1818 executable
= sys
.executable
1819 core
.fork([executable
, sys
.argv
[0], '--repo', self
.new_directory
])
1823 class NewBareRepo(ContextCommand
):
1824 """Create a new shared bare repository"""
1826 def __init__(self
, context
, path
):
1827 super(NewBareRepo
, self
).__init
__(context
)
1832 status
, out
, err
= self
.git
.init(path
, bare
=True, shared
=True)
1833 Interaction
.command(
1834 N_('Error'), 'git init --bare --shared "%s"' % path
, status
, out
, err
1839 def unix_path(path
, is_win32
=utils
.is_win32
):
1840 """Git for Windows requires unix paths, so force them here"""
1842 path
= path
.replace('\\', '/')
1845 if second
== ':': # sanity check, this better be a Windows-style path
1846 path
= '/' + first
+ path
[2:]
1851 def sequence_editor():
1852 """Set GIT_SEQUENCE_EDITOR for running git-cola-sequence-editor"""
1853 xbase
= unix_path(resources
.command('git-cola-sequence-editor'))
1854 if utils
.is_win32():
1855 editor
= core
.list2cmdline([unix_path(sys
.executable
), xbase
])
1857 editor
= core
.list2cmdline([xbase
])
1861 class SequenceEditorEnvironment(object):
1862 """Set environment variables to enable git-cola-sequence-editor"""
1864 def __init__(self
, context
, **kwargs
):
1866 'GIT_EDITOR': prefs
.editor(context
),
1867 'GIT_SEQUENCE_EDITOR': sequence_editor(),
1868 'GIT_COLA_SEQ_EDITOR_CANCEL_ACTION': 'save',
1870 self
.env
.update(kwargs
)
1872 def __enter__(self
):
1873 for var
, value
in self
.env
.items():
1874 compat
.setenv(var
, value
)
1877 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
1878 for var
in self
.env
:
1879 compat
.unsetenv(var
)
1882 class Rebase(ContextCommand
):
1883 def __init__(self
, context
, upstream
=None, branch
=None, **kwargs
):
1884 """Start an interactive rebase session
1886 :param upstream: upstream branch
1887 :param branch: optional branch to checkout
1888 :param kwargs: forwarded directly to `git.rebase()`
1891 super(Rebase
, self
).__init
__(context
)
1893 self
.upstream
= upstream
1894 self
.branch
= branch
1895 self
.kwargs
= kwargs
1897 def prepare_arguments(self
, upstream
):
1901 # Rebase actions must be the only option specified
1902 for action
in ('continue', 'abort', 'skip', 'edit_todo'):
1903 if self
.kwargs
.get(action
, False):
1904 kwargs
[action
] = self
.kwargs
[action
]
1907 kwargs
['interactive'] = True
1908 kwargs
['autosquash'] = self
.kwargs
.get('autosquash', True)
1909 kwargs
.update(self
.kwargs
)
1912 args
.append(upstream
)
1914 args
.append(self
.branch
)
1919 (status
, out
, err
) = (1, '', '')
1920 context
= self
.context
1924 if not cfg
.get('rebase.autostash', False):
1925 if model
.staged
or model
.unmerged
or model
.modified
:
1926 Interaction
.information(
1927 N_('Unable to rebase'),
1928 N_('You cannot rebase with uncommitted changes.'),
1930 return status
, out
, err
1932 upstream
= self
.upstream
or Interaction
.choose_ref(
1934 N_('Select New Upstream'),
1935 N_('Interactive Rebase'),
1936 default
='@{upstream}',
1939 return status
, out
, err
1941 self
.model
.is_rebasing
= True
1942 self
.model
.emit_updated()
1944 args
, kwargs
= self
.prepare_arguments(upstream
)
1945 upstream_title
= upstream
or '@{upstream}'
1946 with
SequenceEditorEnvironment(
1948 GIT_COLA_SEQ_EDITOR_TITLE
=N_('Rebase onto %s') % upstream_title
,
1949 GIT_COLA_SEQ_EDITOR_ACTION
=N_('Rebase'),
1951 # TODO this blocks the user interface window for the duration
1952 # of git-cola-sequence-editor. We would need to implement
1953 # signals for QProcess and continue running the main thread.
1954 # Alternatively, we can hide the main window while rebasing.
1955 # That doesn't require as much effort.
1956 status
, out
, err
= self
.git
.rebase(
1957 *args
, _no_win32_startupinfo
=True, **kwargs
1959 self
.model
.update_status()
1960 if err
.strip() != 'Nothing to do':
1961 title
= N_('Rebase stopped')
1962 Interaction
.command(title
, 'git rebase', status
, out
, err
)
1963 return status
, out
, err
1966 class RebaseEditTodo(ContextCommand
):
1968 (status
, out
, err
) = (1, '', '')
1969 with
SequenceEditorEnvironment(
1971 GIT_COLA_SEQ_EDITOR_TITLE
=N_('Edit Rebase'),
1972 GIT_COLA_SEQ_EDITOR_ACTION
=N_('Save'),
1974 status
, out
, err
= self
.git
.rebase(edit_todo
=True)
1975 Interaction
.log_status(status
, out
, err
)
1976 self
.model
.update_status()
1977 return status
, out
, err
1980 class RebaseContinue(ContextCommand
):
1982 (status
, out
, err
) = (1, '', '')
1983 with
SequenceEditorEnvironment(
1985 GIT_COLA_SEQ_EDITOR_TITLE
=N_('Rebase'),
1986 GIT_COLA_SEQ_EDITOR_ACTION
=N_('Rebase'),
1988 status
, out
, err
= self
.git
.rebase('--continue')
1989 Interaction
.log_status(status
, out
, err
)
1990 self
.model
.update_status()
1991 return status
, out
, err
1994 class RebaseSkip(ContextCommand
):
1996 (status
, out
, err
) = (1, '', '')
1997 with
SequenceEditorEnvironment(
1999 GIT_COLA_SEQ_EDITOR_TITLE
=N_('Rebase'),
2000 GIT_COLA_SEQ_EDITOR_ACTION
=N_('Rebase'),
2002 status
, out
, err
= self
.git
.rebase(skip
=True)
2003 Interaction
.log_status(status
, out
, err
)
2004 self
.model
.update_status()
2005 return status
, out
, err
2008 class RebaseAbort(ContextCommand
):
2010 status
, out
, err
= self
.git
.rebase(abort
=True)
2011 Interaction
.log_status(status
, out
, err
)
2012 self
.model
.update_status()
2015 class Rescan(ContextCommand
):
2016 """Rescan for changes"""
2019 self
.model
.update_status()
2022 class Refresh(ContextCommand
):
2023 """Update refs, refresh the index, and update config"""
2027 return N_('Refresh')
2030 self
.model
.update_status(update_index
=True)
2032 self
.fsmonitor
.refresh()
2035 class RefreshConfig(ContextCommand
):
2036 """Refresh the git config cache"""
2042 class RevertEditsCommand(ConfirmAction
):
2043 def __init__(self
, context
):
2044 super(RevertEditsCommand
, self
).__init
__(context
)
2045 self
.icon
= icons
.undo()
2047 def ok_to_run(self
):
2048 return self
.model
.undoable()
2050 # pylint: disable=no-self-use
2051 def checkout_from_head(self
):
2054 def checkout_args(self
):
2056 s
= self
.selection
.selection()
2057 if self
.checkout_from_head():
2058 args
.append(self
.model
.head
)
2070 checkout_args
= self
.checkout_args()
2071 return self
.git
.checkout(*checkout_args
)
2074 self
.model
.set_diff_type(main
.Types
.TEXT
)
2075 self
.model
.update_file_status()
2078 class RevertUnstagedEdits(RevertEditsCommand
):
2081 return N_('Revert Unstaged Edits...')
2083 def checkout_from_head(self
):
2084 # Being in amend mode should not affect the behavior of this command.
2085 # The only sensible thing to do is to checkout from the index.
2089 title
= N_('Revert Unstaged Changes?')
2091 'This operation removes unstaged edits from selected files.\n'
2092 'These changes cannot be recovered.'
2094 info
= N_('Revert the unstaged changes?')
2095 ok_text
= N_('Revert Unstaged Changes')
2096 return Interaction
.confirm(
2097 title
, text
, info
, ok_text
, default
=True, icon
=self
.icon
2101 class RevertUncommittedEdits(RevertEditsCommand
):
2104 return N_('Revert Uncommitted Edits...')
2106 def checkout_from_head(self
):
2110 """Prompt for reverting changes"""
2111 title
= N_('Revert Uncommitted Changes?')
2113 'This operation removes uncommitted edits from selected files.\n'
2114 'These changes cannot be recovered.'
2116 info
= N_('Revert the uncommitted changes?')
2117 ok_text
= N_('Revert Uncommitted Changes')
2118 return Interaction
.confirm(
2119 title
, text
, info
, ok_text
, default
=True, icon
=self
.icon
2123 class RunConfigAction(ContextCommand
):
2124 """Run a user-configured action, typically from the "Tools" menu"""
2126 def __init__(self
, context
, action_name
):
2127 super(RunConfigAction
, self
).__init
__(context
)
2128 self
.action_name
= action_name
2131 """Run the user-configured action"""
2132 for env
in ('ARGS', 'DIRNAME', 'FILENAME', 'REVISION'):
2134 compat
.unsetenv(env
)
2139 context
= self
.context
2141 opts
= cfg
.get_guitool_opts(self
.action_name
)
2142 cmd
= opts
.get('cmd')
2143 if 'title' not in opts
:
2146 if 'prompt' not in opts
or opts
.get('prompt') is True:
2147 prompt
= N_('Run "%s"?') % cmd
2148 opts
['prompt'] = prompt
2150 if opts
.get('needsfile'):
2151 filename
= self
.selection
.filename()
2153 Interaction
.information(
2154 N_('Please select a file'),
2155 N_('"%s" requires a selected file.') % cmd
,
2158 dirname
= utils
.dirname(filename
, current_dir
='.')
2159 compat
.setenv('FILENAME', filename
)
2160 compat
.setenv('DIRNAME', dirname
)
2162 if opts
.get('revprompt') or opts
.get('argprompt'):
2164 ok
= Interaction
.confirm_config_action(context
, cmd
, opts
)
2167 rev
= opts
.get('revision')
2168 args
= opts
.get('args')
2169 if opts
.get('revprompt') and not rev
:
2170 title
= N_('Invalid Revision')
2171 msg
= N_('The revision expression cannot be empty.')
2172 Interaction
.critical(title
, msg
)
2176 elif opts
.get('confirm'):
2177 title
= os
.path
.expandvars(opts
.get('title'))
2178 prompt
= os
.path
.expandvars(opts
.get('prompt'))
2179 if not Interaction
.question(title
, prompt
):
2182 compat
.setenv('REVISION', rev
)
2184 compat
.setenv('ARGS', args
)
2185 title
= os
.path
.expandvars(cmd
)
2186 Interaction
.log(N_('Running command: %s') % title
)
2187 cmd
= ['sh', '-c', cmd
]
2189 if opts
.get('background'):
2191 status
, out
, err
= (0, '', '')
2192 elif opts
.get('noconsole'):
2193 status
, out
, err
= core
.run_command(cmd
)
2195 status
, out
, err
= Interaction
.run_command(title
, cmd
)
2197 if not opts
.get('background') and not opts
.get('norescan'):
2198 self
.model
.update_status()
2201 Interaction
.command(title
, cmd
, status
, out
, err
)
2206 class SetDefaultRepo(ContextCommand
):
2207 """Set the default repository"""
2209 def __init__(self
, context
, repo
):
2210 super(SetDefaultRepo
, self
).__init
__(context
)
2214 self
.cfg
.set_user('cola.defaultrepo', self
.repo
)
2217 class SetDiffText(EditModel
):
2218 """Set the diff text"""
2222 def __init__(self
, context
, text
):
2223 super(SetDiffText
, self
).__init
__(context
)
2224 self
.new_diff_text
= text
2225 self
.new_diff_type
= main
.Types
.TEXT
2226 self
.new_file_type
= main
.Types
.TEXT
2229 class SetUpstreamBranch(ContextCommand
):
2230 """Set the upstream branch"""
2232 def __init__(self
, context
, branch
, remote
, remote_branch
):
2233 super(SetUpstreamBranch
, self
).__init
__(context
)
2234 self
.branch
= branch
2235 self
.remote
= remote
2236 self
.remote_branch
= remote_branch
2240 remote
= self
.remote
2241 branch
= self
.branch
2242 remote_branch
= self
.remote_branch
2243 cfg
.set_repo('branch.%s.remote' % branch
, remote
)
2244 cfg
.set_repo('branch.%s.merge' % branch
, 'refs/heads/' + remote_branch
)
2247 def format_hex(data
):
2248 """Translate binary data into a hex dump"""
2249 hexdigits
= '0123456789ABCDEF'
2252 byte_offset_to_int
= compat
.byte_offset_to_int_converter()
2253 while offset
< len(data
):
2254 result
+= '%04u |' % offset
2256 for i
in range(0, 16):
2257 if i
> 0 and i
% 4 == 0:
2259 if offset
< len(data
):
2260 v
= byte_offset_to_int(data
[offset
])
2261 result
+= ' ' + hexdigits
[v
>> 4] + hexdigits
[v
& 0xF]
2262 textpart
+= chr(v
) if 32 <= v
< 127 else '.'
2267 result
+= ' | ' + textpart
+ ' |\n'
2272 class ShowUntracked(EditModel
):
2273 """Show an untracked file."""
2275 def __init__(self
, context
, filename
):
2276 super(ShowUntracked
, self
).__init
__(context
)
2277 self
.new_filename
= filename
2278 if gitcmds
.is_binary(context
, filename
):
2279 self
.new_mode
= self
.model
.mode_untracked
2280 self
.new_diff_text
= self
.read(filename
)
2282 self
.new_mode
= self
.model
.mode_untracked_diff
2283 self
.new_diff_text
= gitcmds
.diff_helper(
2284 self
.context
, filename
=filename
, cached
=False, untracked
=True
2286 self
.new_diff_type
= main
.Types
.TEXT
2287 self
.new_file_type
= main
.Types
.TEXT
2289 def read(self
, filename
):
2290 """Read file contents"""
2292 size
= cfg
.get('cola.readsize', 2048)
2294 result
= core
.read(filename
, size
=size
, encoding
='bytes')
2295 except (IOError, OSError):
2298 truncated
= len(result
) == size
2300 encoding
= cfg
.file_encoding(filename
) or core
.ENCODING
2302 text_result
= core
.decode_maybe(result
, encoding
)
2303 except UnicodeError:
2304 text_result
= format_hex(result
)
2307 text_result
+= '...'
2311 class SignOff(ContextCommand
):
2312 """Append a signoff to the commit message"""
2318 return N_('Sign Off')
2320 def __init__(self
, context
):
2321 super(SignOff
, self
).__init
__(context
)
2322 self
.old_commitmsg
= self
.model
.commitmsg
2325 """Add a signoff to the commit message"""
2326 signoff
= self
.signoff()
2327 if signoff
in self
.model
.commitmsg
:
2329 msg
= self
.model
.commitmsg
.rstrip()
2330 self
.model
.set_commitmsg(msg
+ '\n' + signoff
)
2333 """Restore the commit message"""
2334 self
.model
.set_commitmsg(self
.old_commitmsg
)
2337 """Generate the signoff string"""
2339 import pwd
# pylint: disable=all
2341 user
= pwd
.getpwuid(os
.getuid()).pw_name
2343 user
= os
.getenv('USER', N_('unknown'))
2346 name
= cfg
.get('user.name', user
)
2347 email
= cfg
.get('user.email', '%s@%s' % (user
, core
.node()))
2348 return '\nSigned-off-by: %s <%s>' % (name
, email
)
2351 def check_conflicts(context
, unmerged
):
2352 """Check paths for conflicts
2354 Conflicting files can be filtered out one-by-one.
2357 if prefs
.check_conflicts(context
):
2358 unmerged
= [path
for path
in unmerged
if is_conflict_free(path
)]
2362 def is_conflict_free(path
):
2363 """Return True if `path` contains no conflict markers"""
2364 rgx
= re
.compile(r
'^(<<<<<<<|\|\|\|\|\|\|\||>>>>>>>) ')
2366 with core
.xopen(path
, 'rb') as f
:
2368 line
= core
.decode(line
, errors
='ignore')
2370 return should_stage_conflicts(path
)
2372 # We can't read this file ~ we may be staging a removal
2377 def should_stage_conflicts(path
):
2378 """Inform the user that a file contains merge conflicts
2380 Return `True` if we should stage the path nonetheless.
2383 title
= msg
= N_('Stage conflicts?')
2386 '%s appears to contain merge conflicts.\n\n'
2387 'You should probably skip this file.\n'
2392 ok_text
= N_('Stage conflicts')
2393 cancel_text
= N_('Skip')
2394 return Interaction
.confirm(
2395 title
, msg
, info
, ok_text
, default
=False, cancel_text
=cancel_text
2399 class Stage(ContextCommand
):
2400 """Stage a set of paths."""
2406 def __init__(self
, context
, paths
):
2407 super(Stage
, self
).__init
__(context
)
2411 msg
= N_('Staging: %s') % (', '.join(self
.paths
))
2412 Interaction
.log(msg
)
2413 return self
.stage_paths()
2415 def stage_paths(self
):
2416 """Stages add/removals to git."""
2417 context
= self
.context
2420 if self
.model
.cfg
.get('cola.safemode', False):
2422 return self
.stage_all()
2427 for path
in set(paths
):
2428 if core
.exists(path
) or core
.islink(path
):
2429 if path
.endswith('/'):
2430 path
= path
.rstrip('/')
2435 self
.model
.emit_about_to_update()
2437 # `git add -u` doesn't work on untracked files
2439 status
, out
, err
= gitcmds
.add(context
, add
)
2440 Interaction
.command(N_('Error'), 'git add', status
, out
, err
)
2442 # If a path doesn't exist then that means it should be removed
2443 # from the index. We use `git add -u` for that.
2445 status
, out
, err
= gitcmds
.add(context
, remove
, u
=True)
2446 Interaction
.command(N_('Error'), 'git add -u', status
, out
, err
)
2448 self
.model
.update_files(emit
=True)
2449 return status
, out
, err
2451 def stage_all(self
):
2452 """Stage all files"""
2453 status
, out
, err
= self
.git
.add(v
=True, u
=True)
2454 Interaction
.command(N_('Error'), 'git add -u', status
, out
, err
)
2455 self
.model
.update_file_status()
2456 return (status
, out
, err
)
2459 class StageCarefully(Stage
):
2460 """Only stage when the path list is non-empty
2462 We use "git add -u -- <pathspec>" to stage, and it stages everything by
2463 default when no pathspec is specified, so this class ensures that paths
2464 are specified before calling git.
2466 When no paths are specified, the command does nothing.
2470 def __init__(self
, context
):
2471 super(StageCarefully
, self
).__init
__(context
, None)
2474 # pylint: disable=no-self-use
2475 def init_paths(self
):
2476 """Initialize path data"""
2479 def ok_to_run(self
):
2480 """Prevent catch-all "git add -u" from adding unmerged files"""
2481 return self
.paths
or not self
.model
.unmerged
2484 """Stage files when ok_to_run() return True"""
2485 if self
.ok_to_run():
2486 return super(StageCarefully
, self
).do()
2490 class StageModified(StageCarefully
):
2491 """Stage all modified files."""
2495 return N_('Stage Modified')
2497 def init_paths(self
):
2498 self
.paths
= self
.model
.modified
2501 class StageUnmerged(StageCarefully
):
2502 """Stage unmerged files."""
2506 return N_('Stage Unmerged')
2508 def init_paths(self
):
2509 self
.paths
= check_conflicts(self
.context
, self
.model
.unmerged
)
2512 class StageUntracked(StageCarefully
):
2513 """Stage all untracked files."""
2517 return N_('Stage Untracked')
2519 def init_paths(self
):
2520 self
.paths
= self
.model
.untracked
2523 class StageModifiedAndUntracked(StageCarefully
):
2524 """Stage all untracked files."""
2528 return N_('Stage Modified and Untracked')
2530 def init_paths(self
):
2531 self
.paths
= self
.model
.modified
+ self
.model
.untracked
2534 class StageOrUnstageAll(ContextCommand
):
2535 """If the selection is staged, unstage it, otherwise stage"""
2539 return N_('Stage / Unstage All')
2542 if self
.model
.staged
:
2543 do(Unstage
, self
.context
, self
.model
.staged
)
2545 if self
.cfg
.get('cola.safemode', False):
2546 unstaged
= self
.model
.modified
2548 unstaged
= self
.model
.modified
+ self
.model
.untracked
2549 do(Stage
, self
.context
, unstaged
)
2552 class StageOrUnstage(ContextCommand
):
2553 """If the selection is staged, unstage it, otherwise stage"""
2557 return N_('Stage / Unstage')
2560 s
= self
.selection
.selection()
2562 do(Unstage
, self
.context
, s
.staged
)
2565 unmerged
= check_conflicts(self
.context
, s
.unmerged
)
2567 unstaged
.extend(unmerged
)
2569 unstaged
.extend(s
.modified
)
2571 unstaged
.extend(s
.untracked
)
2573 do(Stage
, self
.context
, unstaged
)
2576 class Tag(ContextCommand
):
2577 """Create a tag object."""
2579 def __init__(self
, context
, name
, revision
, sign
=False, message
=''):
2580 super(Tag
, self
).__init
__(context
)
2582 self
._message
= message
2583 self
._revision
= revision
2589 revision
= self
._revision
2590 tag_name
= self
._name
2591 tag_message
= self
._message
2594 Interaction
.critical(
2595 N_('Missing Revision'), N_('Please specify a revision to tag.')
2600 Interaction
.critical(
2601 N_('Missing Name'), N_('Please specify a name for the new tag.')
2605 title
= N_('Missing Tag Message')
2606 message
= N_('Tag-signing was requested but the tag message is empty.')
2608 'An unsigned, lightweight tag will be created instead.\n'
2609 'Create an unsigned tag?'
2611 ok_text
= N_('Create Unsigned Tag')
2613 if sign
and not tag_message
:
2614 # We require a message in order to sign the tag, so if they
2615 # choose to create an unsigned tag we have to clear the sign flag.
2616 if not Interaction
.confirm(
2617 title
, message
, info
, ok_text
, default
=False, icon
=icons
.save()
2626 tmp_file
= utils
.tmp_filename('tag-message')
2627 opts
['file'] = tmp_file
2628 core
.write(tmp_file
, tag_message
)
2633 opts
['annotate'] = True
2634 status
, out
, err
= git
.tag(tag_name
, revision
, **opts
)
2637 core
.unlink(tmp_file
)
2639 title
= N_('Error: could not create tag "%s"') % tag_name
2640 Interaction
.command(title
, 'git tag', status
, out
, err
)
2644 self
.model
.update_status()
2645 Interaction
.information(
2647 N_('Created a new tag named "%s"') % tag_name
,
2648 details
=tag_message
or None,
2654 class Unstage(ContextCommand
):
2655 """Unstage a set of paths."""
2659 return N_('Unstage')
2661 def __init__(self
, context
, paths
):
2662 super(Unstage
, self
).__init
__(context
)
2667 context
= self
.context
2668 head
= self
.model
.head
2671 msg
= N_('Unstaging: %s') % (', '.join(paths
))
2672 Interaction
.log(msg
)
2674 return unstage_all(context
)
2675 status
, out
, err
= gitcmds
.unstage_paths(context
, paths
, head
=head
)
2676 Interaction
.command(N_('Error'), 'git reset', status
, out
, err
)
2677 self
.model
.update_file_status()
2678 return (status
, out
, err
)
2681 class UnstageAll(ContextCommand
):
2682 """Unstage all files; resets the index."""
2685 return unstage_all(self
.context
)
2688 def unstage_all(context
):
2689 """Unstage all files, even while amending"""
2690 model
= context
.model
2693 status
, out
, err
= git
.reset(head
, '--', '.')
2694 Interaction
.command(N_('Error'), 'git reset', status
, out
, err
)
2695 model
.update_file_status()
2696 return (status
, out
, err
)
2699 class StageSelected(ContextCommand
):
2700 """Stage selected files, or all files if no selection exists."""
2703 context
= self
.context
2704 paths
= self
.selection
.unstaged
2706 do(Stage
, context
, paths
)
2707 elif self
.cfg
.get('cola.safemode', False):
2708 do(StageModified
, context
)
2711 class UnstageSelected(Unstage
):
2712 """Unstage selected files."""
2714 def __init__(self
, context
):
2715 staged
= context
.selection
.staged
2716 super(UnstageSelected
, self
).__init
__(context
, staged
)
2719 class Untrack(ContextCommand
):
2720 """Unstage a set of paths."""
2722 def __init__(self
, context
, paths
):
2723 super(Untrack
, self
).__init
__(context
)
2727 msg
= N_('Untracking: %s') % (', '.join(self
.paths
))
2728 Interaction
.log(msg
)
2729 status
, out
, err
= self
.model
.untrack_paths(self
.paths
)
2730 Interaction
.log_status(status
, out
, err
)
2733 class UntrackedSummary(EditModel
):
2734 """List possible .gitignore rules as the diff text."""
2736 def __init__(self
, context
):
2737 super(UntrackedSummary
, self
).__init
__(context
)
2738 untracked
= self
.model
.untracked
2739 suffix
= 's' if untracked
else ''
2741 io
.write('# %s untracked file%s\n' % (len(untracked
), suffix
))
2743 io
.write('# possible .gitignore rule%s:\n' % suffix
)
2745 io
.write('/' + u
+ '\n')
2746 self
.new_diff_text
= io
.getvalue()
2747 self
.new_diff_type
= main
.Types
.TEXT
2748 self
.new_file_type
= main
.Types
.TEXT
2749 self
.new_mode
= self
.model
.mode_untracked
2752 class VisualizeAll(ContextCommand
):
2753 """Visualize all branches."""
2756 context
= self
.context
2757 browser
= utils
.shell_split(prefs
.history_browser(context
))
2758 launch_history_browser(browser
+ ['--all'])
2761 class VisualizeCurrent(ContextCommand
):
2762 """Visualize all branches."""
2765 context
= self
.context
2766 browser
= utils
.shell_split(prefs
.history_browser(context
))
2767 launch_history_browser(browser
+ [self
.model
.currentbranch
] + ['--'])
2770 class VisualizePaths(ContextCommand
):
2771 """Path-limited visualization."""
2773 def __init__(self
, context
, paths
):
2774 super(VisualizePaths
, self
).__init
__(context
)
2775 context
= self
.context
2776 browser
= utils
.shell_split(prefs
.history_browser(context
))
2778 self
.argv
= browser
+ ['--'] + list(paths
)
2783 launch_history_browser(self
.argv
)
2786 class VisualizeRevision(ContextCommand
):
2787 """Visualize a specific revision."""
2789 def __init__(self
, context
, revision
, paths
=None):
2790 super(VisualizeRevision
, self
).__init
__(context
)
2791 self
.revision
= revision
2795 context
= self
.context
2796 argv
= utils
.shell_split(prefs
.history_browser(context
))
2798 argv
.append(self
.revision
)
2801 argv
.extend(self
.paths
)
2802 launch_history_browser(argv
)
2805 class SubmoduleAdd(ConfirmAction
):
2806 """Add specified submodules"""
2808 def __init__(self
, context
, url
, path
, branch
, depth
, reference
):
2809 super(SubmoduleAdd
, self
).__init
__(context
)
2812 self
.branch
= branch
2814 self
.reference
= reference
2817 title
= N_('Add Submodule...')
2818 question
= N_('Add this submodule?')
2819 info
= N_('The submodule will be added using\n' '"%s"' % self
.command())
2820 ok_txt
= N_('Add Submodule')
2821 return Interaction
.confirm(title
, question
, info
, ok_txt
, icon
=icons
.ok())
2824 context
= self
.context
2825 args
= self
.get_args()
2826 return context
.git
.submodule('add', *args
)
2829 self
.model
.update_file_status()
2830 self
.model
.update_submodules_list()
2832 def error_message(self
):
2833 return N_('Error updating submodule %s' % self
.path
)
2836 cmd
= ['git', 'submodule', 'add']
2837 cmd
.extend(self
.get_args())
2838 return core
.list2cmdline(cmd
)
2843 args
.extend(['--branch', self
.branch
])
2845 args
.extend(['--reference', self
.reference
])
2847 args
.extend(['--depth', '%d' % self
.depth
])
2848 args
.extend(['--', self
.url
])
2850 args
.append(self
.path
)
2854 class SubmoduleUpdate(ConfirmAction
):
2855 """Update specified submodule"""
2857 def __init__(self
, context
, path
):
2858 super(SubmoduleUpdate
, self
).__init
__(context
)
2862 title
= N_('Update Submodule...')
2863 question
= N_('Update this submodule?')
2864 info
= N_('The submodule will be updated using\n' '"%s"' % self
.command())
2865 ok_txt
= N_('Update Submodule')
2866 return Interaction
.confirm(
2867 title
, question
, info
, ok_txt
, default
=False, icon
=icons
.pull()
2871 context
= self
.context
2872 args
= self
.get_args()
2873 return context
.git
.submodule(*args
)
2876 self
.model
.update_file_status()
2878 def error_message(self
):
2879 return N_('Error updating submodule %s' % self
.path
)
2882 cmd
= ['git', 'submodule']
2883 cmd
.extend(self
.get_args())
2884 return core
.list2cmdline(cmd
)
2888 if version
.check_git(self
.context
, 'submodule-update-recursive'):
2889 cmd
.append('--recursive')
2890 cmd
.extend(['--', self
.path
])
2894 class SubmodulesUpdate(ConfirmAction
):
2895 """Update all submodules"""
2898 title
= N_('Update submodules...')
2899 question
= N_('Update all submodules?')
2900 info
= N_('All submodules will be updated using\n' '"%s"' % self
.command())
2901 ok_txt
= N_('Update Submodules')
2902 return Interaction
.confirm(
2903 title
, question
, info
, ok_txt
, default
=False, icon
=icons
.pull()
2907 context
= self
.context
2908 args
= self
.get_args()
2909 return context
.git
.submodule(*args
)
2912 self
.model
.update_file_status()
2914 def error_message(self
):
2915 return N_('Error updating submodules')
2918 cmd
= ['git', 'submodule']
2919 cmd
.extend(self
.get_args())
2920 return core
.list2cmdline(cmd
)
2924 if version
.check_git(self
.context
, 'submodule-update-recursive'):
2925 cmd
.append('--recursive')
2929 def launch_history_browser(argv
):
2930 """Launch the configured history browser"""
2933 except OSError as e
:
2934 _
, details
= utils
.format_exception(e
)
2935 title
= N_('Error Launching History Browser')
2936 msg
= N_('Cannot exec "%s": please configure a history browser') % ' '.join(
2939 Interaction
.critical(title
, message
=msg
, details
=details
)
2942 def run(cls
, *args
, **opts
):
2944 Returns a callback that runs a command
2946 If the caller of run() provides args or opts then those are
2947 used instead of the ones provided by the invoker of the callback.
2951 def runner(*local_args
, **local_opts
):
2952 """Closure return by run() which runs the command"""
2954 do(cls
, *args
, **opts
)
2956 do(cls
, *local_args
, **local_opts
)
2961 def do(cls
, *args
, **opts
):
2962 """Run a command in-place"""
2964 cmd
= cls(*args
, **opts
)
2966 except Exception as e
: # pylint: disable=broad-except
2967 msg
, details
= utils
.format_exception(e
)
2968 if hasattr(cls
, '__name__'):
2969 msg
= '%s exception:\n%s' % (cls
.__name
__, msg
)
2970 Interaction
.critical(N_('Error'), message
=msg
, details
=details
)
2974 def difftool_run(context
):
2975 """Start a default difftool session"""
2976 selection
= context
.selection
2977 files
= selection
.group()
2980 s
= selection
.selection()
2981 head
= context
.model
.head
2982 difftool_launch_with_head(context
, files
, bool(s
.staged
), head
)
2985 def difftool_launch_with_head(context
, filenames
, staged
, head
):
2986 """Launch difftool against the provided head"""
2991 difftool_launch(context
, left
=left
, staged
=staged
, paths
=filenames
)
2994 def difftool_launch(
3001 left_take_magic
=False,
3002 left_take_parent
=False,
3004 """Launches 'git difftool' with given parameters
3006 :param left: first argument to difftool
3007 :param right: second argument to difftool_args
3008 :param paths: paths to diff
3009 :param staged: activate `git difftool --staged`
3010 :param dir_diff: activate `git difftool --dir-diff`
3011 :param left_take_magic: whether to append the magic ^! diff expression
3012 :param left_take_parent: whether to append the first-parent ~ for diffing
3016 difftool_args
= ['git', 'difftool', '--no-prompt']
3018 difftool_args
.append('--cached')
3020 difftool_args
.append('--dir-diff')
3023 if left_take_parent
or left_take_magic
:
3024 suffix
= '^!' if left_take_magic
else '~'
3025 # Check root commit (no parents and thus cannot execute '~')
3027 status
, out
, err
= git
.rev_list(left
, parents
=True, n
=1)
3028 Interaction
.log_status(status
, out
, err
)
3030 raise OSError('git rev-list command failed')
3032 if len(out
.split()) >= 2:
3033 # Commit has a parent, so we can take its child as requested
3036 # No parent, assume it's the root commit, so we have to diff
3037 # against the empty tree.
3038 left
= EMPTY_TREE_OID
3039 if not right
and left_take_magic
:
3041 difftool_args
.append(left
)
3044 difftool_args
.append(right
)
3047 difftool_args
.append('--')
3048 difftool_args
.extend(paths
)
3050 runtask
= context
.runtask
3052 Interaction
.async_command(N_('Difftool'), difftool_args
, runtask
)
3054 core
.fork(difftool_args
)