2 from __future__
import division
, absolute_import
, 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
.set_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
.set_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 ResetBranchHead(ResetCommand
):
502 title
= N_('Reset Branch')
503 question
= N_('Point the current branch head to a new commit?')
504 info
= N_('The branch will be reset using "git reset --mixed %s"')
505 ok_text
= N_('Reset Branch')
506 info
= info
% self
.ref
507 return Interaction
.confirm(title
, question
, info
, ok_text
)
510 return self
.git
.reset(self
.ref
, '--', mixed
=True)
513 class ResetWorktree(ResetCommand
):
515 title
= N_('Reset Worktree')
516 question
= N_('Reset worktree?')
517 info
= N_('The worktree will be reset using "git reset --keep %s"')
518 ok_text
= N_('Reset Worktree')
519 info
= info
% self
.ref
520 return Interaction
.confirm(title
, question
, info
, ok_text
)
523 return self
.git
.reset(self
.ref
, '--', keep
=True)
526 class ResetMerge(ResetCommand
):
528 title
= N_('Reset Merge')
529 question
= N_('Reset merge?')
530 info
= N_('The branch will be reset using "git reset --merge %s"')
531 ok_text
= N_('Reset Merge')
532 info
= info
% self
.ref
533 return Interaction
.confirm(title
, question
, info
, ok_text
)
536 return self
.git
.reset(self
.ref
, '--', merge
=True)
539 class ResetSoft(ResetCommand
):
541 title
= N_('Reset Soft')
542 question
= N_('Reset soft?')
543 info
= N_('The branch will be reset using "git reset --soft %s"')
544 ok_text
= N_('Reset Soft')
545 info
= info
% self
.ref
546 return Interaction
.confirm(title
, question
, info
, ok_text
)
549 return self
.git
.reset(self
.ref
, '--', soft
=True)
552 class ResetHard(ResetCommand
):
554 title
= N_('Reset Hard')
555 question
= N_('Reset hard?')
556 info
= N_('The branch will be reset using "git reset --hard %s"')
557 ok_text
= N_('Reset Hard')
558 info
= info
% self
.ref
559 return Interaction
.confirm(title
, question
, info
, ok_text
)
562 return self
.git
.reset(self
.ref
, '--', hard
=True)
565 class UndoLastCommit(ResetCommand
):
566 """Undo the last commit"""
567 # NOTE: this is the similar to ResetSoft() with an additional check for
568 # published commits and different messages.
569 def __init__(self
, context
):
570 super(UndoLastCommit
, self
).__init
__(context
, 'HEAD^')
573 check_published
= prefs
.check_published_commits(self
.context
)
574 if check_published
and self
.model
.is_commit_published():
575 return Interaction
.confirm(
576 N_('Rewrite Published Commit?'),
578 'This commit has already been published.\n'
579 'This operation will rewrite published history.\n'
580 'You probably don\'t want to do this.'
582 N_('Undo the published commit?'),
583 N_('Undo Last Commit'),
588 title
= N_('Undo Last Commit')
589 question
= N_('Undo last commit?')
590 info
= N_('The branch will be reset using "git reset --soft %s"')
591 ok_text
= N_('Undo Last Commit')
592 info_text
= info
% self
.ref
593 return Interaction
.confirm(title
, question
, info_text
, ok_text
)
596 return self
.git
.reset('HEAD^', '--', soft
=True)
599 class Commit(ResetMode
):
600 """Attempt to create a new commit."""
602 def __init__(self
, context
, amend
, msg
, sign
, no_verify
=False):
603 super(Commit
, self
).__init
__(context
)
607 self
.no_verify
= no_verify
608 self
.old_commitmsg
= self
.model
.commitmsg
609 self
.new_commitmsg
= ''
612 # Create the commit message file
613 context
= self
.context
614 comment_char
= prefs
.comment_char(context
)
615 msg
= self
.strip_comments(self
.msg
, comment_char
=comment_char
)
616 tmp_file
= utils
.tmp_filename('commit-message')
618 core
.write(tmp_file
, msg
)
620 status
, out
, err
= self
.git
.commit(
625 no_verify
=self
.no_verify
,
628 core
.unlink(tmp_file
)
630 super(Commit
, self
).do()
631 if context
.cfg
.get(prefs
.AUTOTEMPLATE
):
632 template_loader
= LoadCommitMessageFromTemplate(context
)
635 self
.model
.set_commitmsg(self
.new_commitmsg
)
637 title
= N_('Commit failed')
638 Interaction
.command(title
, 'git commit', status
, out
, err
)
640 return status
, out
, err
643 def strip_comments(msg
, comment_char
='#'):
646 line
for line
in msg
.split('\n') if not line
.startswith(comment_char
)
648 msg
= '\n'.join(message_lines
)
649 if not msg
.endswith('\n'):
655 class CycleReferenceSort(ContextCommand
):
656 """Choose the next reference sort type"""
659 self
.model
.cycle_ref_sort()
662 class Ignore(ContextCommand
):
663 """Add files to an exclusion file"""
665 def __init__(self
, context
, filenames
, local
=False):
666 super(Ignore
, self
).__init
__(context
)
667 self
.filenames
= list(filenames
)
671 if not self
.filenames
:
673 new_additions
= '\n'.join(self
.filenames
) + '\n'
674 for_status
= new_additions
676 filename
= os
.path
.join('.git', 'info', 'exclude')
678 filename
= '.gitignore'
679 if core
.exists(filename
):
680 current_list
= core
.read(filename
)
681 new_additions
= current_list
.rstrip() + '\n' + new_additions
682 core
.write(filename
, new_additions
)
683 Interaction
.log_status(0, 'Added to %s:\n%s' % (filename
, for_status
), '')
684 self
.model
.update_file_status()
687 def file_summary(files
):
688 txt
= core
.list2cmdline(files
)
690 txt
= txt
[:768].rstrip() + '...'
691 wrap
= textwrap
.TextWrapper()
692 return '\n'.join(wrap
.wrap(txt
))
695 class RemoteCommand(ConfirmAction
):
696 def __init__(self
, context
, remote
):
697 super(RemoteCommand
, self
).__init
__(context
)
702 self
.model
.update_remotes()
705 class RemoteAdd(RemoteCommand
):
706 def __init__(self
, context
, remote
, url
):
707 super(RemoteAdd
, self
).__init
__(context
, remote
)
711 return self
.git
.remote('add', self
.remote
, self
.url
)
713 def error_message(self
):
714 return N_('Error creating remote "%s"') % self
.remote
717 return 'git remote add "%s" "%s"' % (self
.remote
, self
.url
)
720 class RemoteRemove(RemoteCommand
):
722 title
= N_('Delete Remote')
723 question
= N_('Delete remote?')
724 info
= N_('Delete remote "%s"') % self
.remote
725 ok_text
= N_('Delete')
726 return Interaction
.confirm(title
, question
, info
, ok_text
)
729 return self
.git
.remote('rm', self
.remote
)
731 def error_message(self
):
732 return N_('Error deleting remote "%s"') % self
.remote
735 return 'git remote rm "%s"' % self
.remote
738 class RemoteRename(RemoteCommand
):
739 def __init__(self
, context
, remote
, new_name
):
740 super(RemoteRename
, self
).__init
__(context
, remote
)
741 self
.new_name
= new_name
744 title
= N_('Rename Remote')
745 text
= N_('Rename remote "%(current)s" to "%(new)s"?') % dict(
746 current
=self
.remote
, new
=self
.new_name
750 return Interaction
.confirm(title
, text
, info_text
, ok_text
)
753 return self
.git
.remote('rename', self
.remote
, self
.new_name
)
755 def error_message(self
):
756 return N_('Error renaming "%(name)s" to "%(new_name)s"') % dict(
757 name
=self
.remote
, new_name
=self
.new_name
761 return 'git remote rename "%s" "%s"' % (self
.remote
, self
.new_name
)
764 class RemoteSetURL(RemoteCommand
):
765 def __init__(self
, context
, remote
, url
):
766 super(RemoteSetURL
, self
).__init
__(context
, remote
)
770 return self
.git
.remote('set-url', self
.remote
, self
.url
)
772 def error_message(self
):
773 return N_('Unable to set URL for "%(name)s" to "%(url)s"') % dict(
774 name
=self
.remote
, url
=self
.url
778 return 'git remote set-url "%s" "%s"' % (self
.remote
, self
.url
)
781 class RemoteEdit(ContextCommand
):
782 """Combine RemoteRename and RemoteSetURL"""
784 def __init__(self
, context
, old_name
, remote
, url
):
785 super(RemoteEdit
, self
).__init
__(context
)
786 self
.rename
= RemoteRename(context
, old_name
, remote
)
787 self
.set_url
= RemoteSetURL(context
, remote
, url
)
790 result
= self
.rename
.do()
794 result
= self
.set_url
.do()
796 return name_ok
, url_ok
799 class RemoveFromSettings(ConfirmAction
):
801 def __init__(self
, context
, repo
, entry
, icon
=None):
802 super(RemoveFromSettings
, self
).__init
__(context
)
803 self
.context
= context
809 self
.context
.settings
.save()
812 class RemoveBookmark(RemoveFromSettings
):
816 title
= msg
= N_('Delete Bookmark?')
817 info
= N_('%s will be removed from your bookmarks.') % entry
818 ok_text
= N_('Delete Bookmark')
819 return Interaction
.confirm(title
, msg
, info
, ok_text
, icon
=self
.icon
)
822 self
.context
.settings
.remove_bookmark(self
.repo
, self
.entry
)
826 class RemoveRecent(RemoveFromSettings
):
829 title
= msg
= N_('Remove %s from the recent list?') % repo
830 info
= N_('%s will be removed from your recent repositories.') % repo
831 ok_text
= N_('Remove')
832 return Interaction
.confirm(title
, msg
, info
, ok_text
, icon
=self
.icon
)
835 self
.context
.settings
.remove_recent(self
.repo
)
839 class RemoveFiles(ContextCommand
):
842 def __init__(self
, context
, remover
, filenames
):
843 super(RemoveFiles
, self
).__init
__(context
)
846 self
.remover
= remover
847 self
.filenames
= filenames
848 # We could git-hash-object stuff and provide undo-ability
852 files
= self
.filenames
858 remove
= self
.remover
859 for filename
in files
:
865 bad_filenames
.append(filename
)
868 Interaction
.information(
869 N_('Error'), N_('Deleting "%s" failed') % file_summary(bad_filenames
)
873 self
.model
.update_file_status()
876 class Delete(RemoveFiles
):
879 def __init__(self
, context
, filenames
):
880 super(Delete
, self
).__init
__(context
, os
.remove
, filenames
)
883 files
= self
.filenames
887 title
= N_('Delete Files?')
888 msg
= N_('The following files will be deleted:') + '\n\n'
889 msg
+= file_summary(files
)
890 info_txt
= N_('Delete %d file(s)?') % len(files
)
891 ok_txt
= N_('Delete Files')
893 if Interaction
.confirm(
894 title
, msg
, info_txt
, ok_txt
, default
=True, icon
=icons
.remove()
896 super(Delete
, self
).do()
899 class MoveToTrash(RemoveFiles
):
900 """Move files to the trash using send2trash"""
902 AVAILABLE
= send2trash
is not None
904 def __init__(self
, context
, filenames
):
905 super(MoveToTrash
, self
).__init
__(context
, send2trash
, filenames
)
908 class DeleteBranch(ConfirmAction
):
909 """Delete a git branch."""
911 def __init__(self
, context
, branch
):
912 super(DeleteBranch
, self
).__init
__(context
)
916 title
= N_('Delete Branch')
917 question
= N_('Delete branch "%s"?') % self
.branch
918 info
= N_('The branch will be no longer available.')
919 ok_txt
= N_('Delete Branch')
920 return Interaction
.confirm(
921 title
, question
, info
, ok_txt
, default
=True, icon
=icons
.discard()
925 return self
.model
.delete_branch(self
.branch
)
927 def error_message(self
):
928 return N_('Error deleting branch "%s"' % self
.branch
)
931 command
= 'git branch -D %s'
932 return command
% self
.branch
935 class Rename(ContextCommand
):
936 """Rename a set of paths."""
938 def __init__(self
, context
, paths
):
939 super(Rename
, self
).__init
__(context
)
943 msg
= N_('Untracking: %s') % (', '.join(self
.paths
))
946 for path
in self
.paths
:
947 ok
= self
.rename(path
)
951 self
.model
.update_status()
953 def rename(self
, path
):
955 title
= N_('Rename "%s"') % path
957 if os
.path
.isdir(path
):
958 base_path
= os
.path
.dirname(path
)
961 new_path
= Interaction
.save_as(base_path
, title
)
965 status
, out
, err
= git
.mv(path
, new_path
, force
=True, verbose
=True)
966 Interaction
.command(N_('Error'), 'git mv', status
, out
, err
)
970 class RenameBranch(ContextCommand
):
971 """Rename a git branch."""
973 def __init__(self
, context
, branch
, new_branch
):
974 super(RenameBranch
, self
).__init
__(context
)
976 self
.new_branch
= new_branch
980 new_branch
= self
.new_branch
981 status
, out
, err
= self
.model
.rename_branch(branch
, new_branch
)
982 Interaction
.log_status(status
, out
, err
)
985 class DeleteRemoteBranch(DeleteBranch
):
986 """Delete a remote git branch."""
988 def __init__(self
, context
, remote
, branch
):
989 super(DeleteRemoteBranch
, self
).__init
__(context
, branch
)
993 return self
.git
.push(self
.remote
, self
.branch
, delete
=True)
996 self
.model
.update_status()
997 Interaction
.information(
998 N_('Remote Branch Deleted'),
999 N_('"%(branch)s" has been deleted from "%(remote)s".')
1000 % dict(branch
=self
.branch
, remote
=self
.remote
),
1003 def error_message(self
):
1004 return N_('Error Deleting Remote Branch')
1007 command
= 'git push --delete %s %s'
1008 return command
% (self
.remote
, self
.branch
)
1011 def get_mode(model
, staged
, modified
, unmerged
, untracked
):
1013 mode
= model
.mode_index
1014 elif modified
or unmerged
:
1015 mode
= model
.mode_worktree
1017 mode
= model
.mode_untracked
1023 class DiffText(EditModel
):
1024 """Set the diff type to text"""
1026 def __init__(self
, context
):
1027 super(DiffText
, self
).__init
__(context
)
1028 self
.new_file_type
= main
.Types
.TEXT
1029 self
.new_diff_type
= main
.Types
.TEXT
1032 class ToggleDiffType(ContextCommand
):
1033 """Toggle the diff type between image and text"""
1035 def __init__(self
, context
):
1036 super(ToggleDiffType
, self
).__init
__(context
)
1037 if self
.model
.diff_type
== main
.Types
.IMAGE
:
1038 self
.new_diff_type
= main
.Types
.TEXT
1039 self
.new_value
= False
1041 self
.new_diff_type
= main
.Types
.IMAGE
1042 self
.new_value
= True
1045 diff_type
= self
.new_diff_type
1046 value
= self
.new_value
1048 self
.model
.set_diff_type(diff_type
)
1050 filename
= self
.model
.filename
1051 _
, ext
= os
.path
.splitext(filename
)
1052 if ext
.startswith('.'):
1053 cfg
= 'cola.imagediff' + ext
1054 self
.cfg
.set_repo(cfg
, value
)
1057 class DiffImage(EditModel
):
1059 self
, context
, filename
, deleted
, staged
, modified
, unmerged
, untracked
1061 super(DiffImage
, self
).__init
__(context
)
1063 self
.new_filename
= filename
1064 self
.new_diff_type
= self
.get_diff_type(filename
)
1065 self
.new_file_type
= main
.Types
.IMAGE
1066 self
.new_mode
= get_mode(self
.model
, staged
, modified
, unmerged
, untracked
)
1067 self
.staged
= staged
1068 self
.modified
= modified
1069 self
.unmerged
= unmerged
1070 self
.untracked
= untracked
1071 self
.deleted
= deleted
1072 self
.annex
= self
.cfg
.is_annex()
1074 def get_diff_type(self
, filename
):
1075 """Query the diff type to use based on cola.imagediff.<extension>"""
1076 _
, ext
= os
.path
.splitext(filename
)
1077 if ext
.startswith('.'):
1078 # Check eg. "cola.imagediff.svg" to see if we should imagediff.
1079 cfg
= 'cola.imagediff' + ext
1080 if self
.cfg
.get(cfg
, True):
1081 result
= main
.Types
.IMAGE
1083 result
= main
.Types
.TEXT
1085 result
= main
.Types
.IMAGE
1089 filename
= self
.new_filename
1092 images
= self
.staged_images()
1094 images
= self
.modified_images()
1096 images
= self
.unmerged_images()
1097 elif self
.untracked
:
1098 images
= [(filename
, False)]
1102 self
.model
.set_images(images
)
1103 super(DiffImage
, self
).do()
1105 def staged_images(self
):
1106 context
= self
.context
1108 head
= self
.model
.head
1109 filename
= self
.new_filename
1113 index
= git
.diff_index(head
, '--', filename
, cached
=True)[STDOUT
]
1116 # :100644 100644 fabadb8... 4866510... M describe.c
1117 parts
= index
.split(' ')
1122 if old_oid
!= MISSING_BLOB_OID
:
1123 # First, check if we can get a pre-image from git-annex
1126 annex_image
= gitcmds
.annex_path(context
, head
, filename
)
1128 images
.append((annex_image
, False)) # git annex HEAD
1130 image
= gitcmds
.write_blob_path(context
, head
, old_oid
, filename
)
1132 images
.append((image
, True))
1134 if new_oid
!= MISSING_BLOB_OID
:
1135 found_in_annex
= False
1136 if annex
and core
.islink(filename
):
1137 status
, out
, _
= git
.annex('status', '--', filename
)
1139 details
= out
.split(' ')
1140 if details
and details
[0] == 'A': # newly added file
1141 images
.append((filename
, False))
1142 found_in_annex
= True
1144 if not found_in_annex
:
1145 image
= gitcmds
.write_blob(context
, new_oid
, filename
)
1147 images
.append((image
, True))
1151 def unmerged_images(self
):
1152 context
= self
.context
1154 head
= self
.model
.head
1155 filename
= self
.new_filename
1158 candidate_merge_heads
= ('HEAD', 'CHERRY_HEAD', 'MERGE_HEAD')
1161 for merge_head
in candidate_merge_heads
1162 if core
.exists(git
.git_path(merge_head
))
1165 if annex
: # Attempt to find files in git-annex
1167 for merge_head
in merge_heads
:
1168 image
= gitcmds
.annex_path(context
, merge_head
, filename
)
1170 annex_images
.append((image
, False))
1172 annex_images
.append((filename
, False))
1175 # DIFF FORMAT FOR MERGES
1176 # "git-diff-tree", "git-diff-files" and "git-diff --raw"
1177 # can take -c or --cc option to generate diff output also
1178 # for merge commits. The output differs from the format
1179 # described above in the following way:
1181 # 1. there is a colon for each parent
1182 # 2. there are more "src" modes and "src" sha1
1183 # 3. status is concatenated status characters for each parent
1184 # 4. no optional "score" number
1185 # 5. single path, only for "dst"
1187 # ::100644 100644 100644 fabadb8... cc95eb0... 4866510... \
1190 index
= git
.diff_index(head
, '--', filename
, cached
=True, cc
=True)[STDOUT
]
1192 parts
= index
.split(' ')
1194 first_mode
= parts
[0]
1195 num_parents
= first_mode
.count(':')
1196 # colon for each parent, but for the index, the "parents"
1197 # are really entries in stages 1,2,3 (head, base, remote)
1198 # remote, base, head
1199 for i
in range(num_parents
):
1200 offset
= num_parents
+ i
+ 1
1203 merge_head
= merge_heads
[i
]
1206 if oid
!= MISSING_BLOB_OID
:
1207 image
= gitcmds
.write_blob_path(
1208 context
, merge_head
, oid
, filename
1211 images
.append((image
, True))
1213 images
.append((filename
, False))
1216 def modified_images(self
):
1217 context
= self
.context
1219 head
= self
.model
.head
1220 filename
= self
.new_filename
1225 if annex
: # Check for a pre-image from git-annex
1226 annex_image
= gitcmds
.annex_path(context
, head
, filename
)
1228 images
.append((annex_image
, False)) # git annex HEAD
1230 worktree
= git
.diff_files('--', filename
)[STDOUT
]
1231 parts
= worktree
.split(' ')
1234 if oid
!= MISSING_BLOB_OID
:
1235 image
= gitcmds
.write_blob_path(context
, head
, oid
, filename
)
1237 images
.append((image
, True)) # HEAD
1239 images
.append((filename
, False)) # worktree
1243 class Diff(EditModel
):
1244 """Perform a diff and set the model's current text."""
1246 def __init__(self
, context
, filename
, cached
=False, deleted
=False):
1247 super(Diff
, self
).__init
__(context
)
1250 opts
['ref'] = self
.model
.head
1251 self
.new_filename
= filename
1252 self
.new_mode
= self
.model
.mode_worktree
1253 self
.new_diff_text
= gitcmds
.diff_helper(
1254 self
.context
, filename
=filename
, cached
=cached
, deleted
=deleted
, **opts
1258 class Diffstat(EditModel
):
1259 """Perform a diffstat and set the model's diff text."""
1261 def __init__(self
, context
):
1262 super(Diffstat
, self
).__init
__(context
)
1264 diff_context
= cfg
.get('diff.context', 3)
1265 diff
= self
.git
.diff(
1267 unified
=diff_context
,
1273 self
.new_diff_text
= diff
1274 self
.new_diff_type
= main
.Types
.TEXT
1275 self
.new_file_type
= main
.Types
.TEXT
1276 self
.new_mode
= self
.model
.mode_diffstat
1279 class DiffStaged(Diff
):
1280 """Perform a staged diff on a file."""
1282 def __init__(self
, context
, filename
, deleted
=None):
1283 super(DiffStaged
, self
).__init
__(
1284 context
, filename
, cached
=True, deleted
=deleted
1286 self
.new_mode
= self
.model
.mode_index
1289 class DiffStagedSummary(EditModel
):
1290 def __init__(self
, context
):
1291 super(DiffStagedSummary
, self
).__init
__(context
)
1292 diff
= self
.git
.diff(
1297 patch_with_stat
=True,
1300 self
.new_diff_text
= diff
1301 self
.new_diff_type
= main
.Types
.TEXT
1302 self
.new_file_type
= main
.Types
.TEXT
1303 self
.new_mode
= self
.model
.mode_index
1306 class Difftool(ContextCommand
):
1307 """Run git-difftool limited by path."""
1309 def __init__(self
, context
, staged
, filenames
):
1310 super(Difftool
, self
).__init
__(context
)
1311 self
.staged
= staged
1312 self
.filenames
= filenames
1315 difftool_launch_with_head(
1316 self
.context
, self
.filenames
, self
.staged
, self
.model
.head
1320 class Edit(ContextCommand
):
1321 """Edit a file using the configured gui.editor."""
1325 return N_('Launch Editor')
1327 def __init__(self
, context
, filenames
, line_number
=None, background_editor
=False):
1328 super(Edit
, self
).__init
__(context
)
1329 self
.filenames
= filenames
1330 self
.line_number
= line_number
1331 self
.background_editor
= background_editor
1334 context
= self
.context
1335 if not self
.filenames
:
1337 filename
= self
.filenames
[0]
1338 if not core
.exists(filename
):
1340 if self
.background_editor
:
1341 editor
= prefs
.background_editor(context
)
1343 editor
= prefs
.editor(context
)
1346 if self
.line_number
is None:
1347 opts
= self
.filenames
1349 # Single-file w/ line-numbers (likely from grep)
1351 '*vim*': [filename
, '+%s' % self
.line_number
],
1352 '*emacs*': ['+%s' % self
.line_number
, filename
],
1353 '*textpad*': ['%s(%s,0)' % (filename
, self
.line_number
)],
1354 '*notepad++*': ['-n%s' % self
.line_number
, filename
],
1355 '*subl*': ['%s:%s' % (filename
, self
.line_number
)],
1358 opts
= self
.filenames
1359 for pattern
, opt
in editor_opts
.items():
1360 if fnmatch(editor
, pattern
):
1365 core
.fork(utils
.shell_split(editor
) + opts
)
1366 except (OSError, ValueError) as e
:
1367 message
= N_('Cannot exec "%s": please configure your editor') % editor
1368 _
, details
= utils
.format_exception(e
)
1369 Interaction
.critical(N_('Error Editing File'), message
, details
)
1372 class FormatPatch(ContextCommand
):
1373 """Output a patch series given all revisions and a selected subset."""
1375 def __init__(self
, context
, to_export
, revs
, output
='patches'):
1376 super(FormatPatch
, self
).__init
__(context
)
1377 self
.to_export
= list(to_export
)
1378 self
.revs
= list(revs
)
1379 self
.output
= output
1382 context
= self
.context
1383 status
, out
, err
= gitcmds
.format_patchsets(
1384 context
, self
.to_export
, self
.revs
, self
.output
1386 Interaction
.log_status(status
, out
, err
)
1389 class LaunchDifftool(ContextCommand
):
1392 return N_('Launch Diff Tool')
1395 s
= self
.selection
.selection()
1398 if utils
.is_win32():
1399 core
.fork(['git', 'mergetool', '--no-prompt', '--'] + paths
)
1402 cmd
= cfg
.terminal()
1403 argv
= utils
.shell_split(cmd
)
1405 terminal
= os
.path
.basename(argv
[0])
1406 shellquote_terms
= set(['xfce4-terminal'])
1407 shellquote_default
= terminal
in shellquote_terms
1409 mergetool
= ['git', 'mergetool', '--no-prompt', '--']
1410 mergetool
.extend(paths
)
1411 needs_shellquote
= cfg
.get(
1412 'cola.terminalshellquote', shellquote_default
1415 if needs_shellquote
:
1416 argv
.append(core
.list2cmdline(mergetool
))
1418 argv
.extend(mergetool
)
1422 difftool_run(self
.context
)
1425 class LaunchTerminal(ContextCommand
):
1428 return N_('Launch Terminal')
1431 def is_available(context
):
1432 return context
.cfg
.terminal() is not None
1434 def __init__(self
, context
, path
):
1435 super(LaunchTerminal
, self
).__init
__(context
)
1439 cmd
= self
.context
.cfg
.terminal()
1442 if utils
.is_win32():
1443 argv
= ['start', '', cmd
, '--login']
1446 argv
= utils
.shell_split(cmd
)
1447 argv
.append(os
.getenv('SHELL', '/bin/sh'))
1449 core
.fork(argv
, cwd
=self
.path
, shell
=shell
)
1452 class LaunchEditor(Edit
):
1455 return N_('Launch Editor')
1457 def __init__(self
, context
):
1458 s
= context
.selection
.selection()
1459 filenames
= s
.staged
+ s
.unmerged
+ s
.modified
+ s
.untracked
1460 super(LaunchEditor
, self
).__init
__(context
, filenames
, background_editor
=True)
1463 class LaunchEditorAtLine(LaunchEditor
):
1464 """Launch an editor at the specified line"""
1466 def __init__(self
, context
):
1467 super(LaunchEditorAtLine
, self
).__init
__(context
)
1468 self
.line_number
= context
.selection
.line_number
1471 class LoadCommitMessageFromFile(ContextCommand
):
1472 """Loads a commit message from a path."""
1476 def __init__(self
, context
, path
):
1477 super(LoadCommitMessageFromFile
, self
).__init
__(context
)
1479 self
.old_commitmsg
= self
.model
.commitmsg
1480 self
.old_directory
= self
.model
.directory
1483 path
= os
.path
.expanduser(self
.path
)
1484 if not path
or not core
.isfile(path
):
1486 N_('Error: Cannot find commit template'),
1487 N_('%s: No such file or directory.') % path
,
1489 self
.model
.set_directory(os
.path
.dirname(path
))
1490 self
.model
.set_commitmsg(core
.read(path
))
1493 self
.model
.set_commitmsg(self
.old_commitmsg
)
1494 self
.model
.set_directory(self
.old_directory
)
1497 class LoadCommitMessageFromTemplate(LoadCommitMessageFromFile
):
1498 """Loads the commit message template specified by commit.template."""
1500 def __init__(self
, context
):
1502 template
= cfg
.get('commit.template')
1503 super(LoadCommitMessageFromTemplate
, self
).__init
__(context
, template
)
1506 if self
.path
is None:
1508 N_('Error: Unconfigured commit template'),
1510 'A commit template has not been configured.\n'
1511 'Use "git config" to define "commit.template"\n'
1512 'so that it points to a commit template.'
1515 return LoadCommitMessageFromFile
.do(self
)
1518 class LoadCommitMessageFromOID(ContextCommand
):
1519 """Load a previous commit message"""
1523 def __init__(self
, context
, oid
, prefix
=''):
1524 super(LoadCommitMessageFromOID
, self
).__init
__(context
)
1526 self
.old_commitmsg
= self
.model
.commitmsg
1527 self
.new_commitmsg
= prefix
+ gitcmds
.prev_commitmsg(context
, oid
)
1530 self
.model
.set_commitmsg(self
.new_commitmsg
)
1533 self
.model
.set_commitmsg(self
.old_commitmsg
)
1536 class PrepareCommitMessageHook(ContextCommand
):
1537 """Use the cola-prepare-commit-msg hook to prepare the commit message
1542 def __init__(self
, context
):
1543 super(PrepareCommitMessageHook
, self
).__init
__(context
)
1544 self
.old_commitmsg
= self
.model
.commitmsg
1546 def get_message(self
):
1548 title
= N_('Error running prepare-commitmsg hook')
1549 hook
= gitcmds
.prepare_commit_message_hook(self
.context
)
1551 if os
.path
.exists(hook
):
1552 filename
= self
.model
.save_commitmsg()
1553 status
, out
, err
= core
.run_command([hook
, filename
])
1556 result
= core
.read(filename
)
1558 result
= self
.old_commitmsg
1559 Interaction
.command_error(title
, hook
, status
, out
, err
)
1561 message
= N_('A hook must be provided at "%s"') % hook
1562 Interaction
.critical(title
, message
=message
)
1563 result
= self
.old_commitmsg
1568 msg
= self
.get_message()
1569 self
.model
.set_commitmsg(msg
)
1572 self
.model
.set_commitmsg(self
.old_commitmsg
)
1575 class LoadFixupMessage(LoadCommitMessageFromOID
):
1576 """Load a fixup message"""
1578 def __init__(self
, context
, oid
):
1579 super(LoadFixupMessage
, self
).__init
__(context
, oid
, prefix
='fixup! ')
1580 if self
.new_commitmsg
:
1581 self
.new_commitmsg
= self
.new_commitmsg
.splitlines()[0]
1584 class Merge(ContextCommand
):
1587 def __init__(self
, context
, revision
, no_commit
, squash
, no_ff
, sign
):
1588 super(Merge
, self
).__init
__(context
)
1589 self
.revision
= revision
1591 self
.no_commit
= no_commit
1592 self
.squash
= squash
1596 squash
= self
.squash
1597 revision
= self
.revision
1599 no_commit
= self
.no_commit
1602 status
, out
, err
= self
.git
.merge(
1603 revision
, gpg_sign
=sign
, no_ff
=no_ff
, no_commit
=no_commit
, squash
=squash
1605 self
.model
.update_status()
1606 title
= N_('Merge failed. Conflict resolution is required.')
1607 Interaction
.command(title
, 'git merge', status
, out
, err
)
1609 return status
, out
, err
1612 class OpenDefaultApp(ContextCommand
):
1613 """Open a file using the OS default."""
1617 return N_('Open Using Default Application')
1619 def __init__(self
, context
, filenames
):
1620 super(OpenDefaultApp
, self
).__init
__(context
)
1621 if utils
.is_darwin():
1624 launcher
= 'xdg-open'
1625 self
.launcher
= launcher
1626 self
.filenames
= filenames
1629 if not self
.filenames
:
1631 core
.fork([self
.launcher
] + self
.filenames
)
1634 class OpenParentDir(OpenDefaultApp
):
1635 """Open parent directories using the OS default."""
1639 return N_('Open Parent Directory')
1641 def __init__(self
, context
, filenames
):
1642 OpenDefaultApp
.__init
__(self
, context
, filenames
)
1645 if not self
.filenames
:
1647 dirnames
= list(set([os
.path
.dirname(x
) for x
in self
.filenames
]))
1648 # os.path.dirname() can return an empty string so we fallback to
1649 # the current directory
1650 dirs
= [(dirname
or core
.getcwd()) for dirname
in dirnames
]
1651 core
.fork([self
.launcher
] + dirs
)
1654 class OpenNewRepo(ContextCommand
):
1655 """Launches git-cola on a repo."""
1657 def __init__(self
, context
, repo_path
):
1658 super(OpenNewRepo
, self
).__init
__(context
)
1659 self
.repo_path
= repo_path
1662 self
.model
.set_directory(self
.repo_path
)
1663 core
.fork([sys
.executable
, sys
.argv
[0], '--repo', self
.repo_path
])
1666 class OpenRepo(EditModel
):
1667 def __init__(self
, context
, repo_path
):
1668 super(OpenRepo
, self
).__init
__(context
)
1669 self
.repo_path
= repo_path
1670 self
.new_mode
= self
.model
.mode_none
1671 self
.new_diff_text
= ''
1672 self
.new_diff_type
= main
.Types
.TEXT
1673 self
.new_file_type
= main
.Types
.TEXT
1674 self
.new_commitmsg
= ''
1675 self
.new_filename
= ''
1678 old_repo
= self
.git
.getcwd()
1679 if self
.model
.set_worktree(self
.repo_path
):
1680 self
.fsmonitor
.stop()
1681 self
.fsmonitor
.start()
1682 self
.model
.update_status()
1683 # Check if template should be loaded
1684 if self
.context
.cfg
.get(prefs
.AUTOTEMPLATE
):
1685 template_loader
= LoadCommitMessageFromTemplate(self
.context
)
1686 template_loader
.do()
1688 self
.model
.set_commitmsg(self
.new_commitmsg
)
1689 settings
= self
.context
.settings
1691 settings
.add_recent(self
.repo_path
, prefs
.maxrecent(self
.context
))
1693 super(OpenRepo
, self
).do()
1695 self
.model
.set_worktree(old_repo
)
1698 class OpenParentRepo(OpenRepo
):
1699 def __init__(self
, context
):
1701 if version
.check_git(context
, 'show-superproject-working-tree'):
1702 status
, out
, _
= context
.git
.rev_parse(show_superproject_working_tree
=True)
1706 path
= os
.path
.dirname(core
.getcwd())
1707 super(OpenParentRepo
, self
).__init
__(context
, path
)
1710 class Clone(ContextCommand
):
1711 """Clones a repository and optionally spawns a new cola session."""
1714 self
, context
, url
, new_directory
, submodules
=False, shallow
=False, spawn
=True
1716 super(Clone
, self
).__init
__(context
)
1718 self
.new_directory
= new_directory
1719 self
.submodules
= submodules
1720 self
.shallow
= shallow
1730 recurse_submodules
= self
.submodules
1731 shallow_submodules
= self
.submodules
and self
.shallow
1733 status
, out
, err
= self
.git
.clone(
1736 recurse_submodules
=recurse_submodules
,
1737 shallow_submodules
=shallow_submodules
,
1741 self
.status
= status
1744 if status
== 0 and self
.spawn
:
1745 executable
= sys
.executable
1746 core
.fork([executable
, sys
.argv
[0], '--repo', self
.new_directory
])
1750 class NewBareRepo(ContextCommand
):
1751 """Create a new shared bare repository"""
1753 def __init__(self
, context
, path
):
1754 super(NewBareRepo
, self
).__init
__(context
)
1759 status
, out
, err
= self
.git
.init(path
, bare
=True, shared
=True)
1760 Interaction
.command(
1761 N_('Error'), 'git init --bare --shared "%s"' % path
, status
, out
, err
1766 def unix_path(path
, is_win32
=utils
.is_win32
):
1767 """Git for Windows requires unix paths, so force them here
1770 path
= path
.replace('\\', '/')
1773 if second
== ':': # sanity check, this better be a Windows-style path
1774 path
= '/' + first
+ path
[2:]
1779 def sequence_editor():
1780 """Set GIT_SEQUENCE_EDITOR for running git-cola-sequence-editor"""
1781 xbase
= unix_path(resources
.command('git-cola-sequence-editor'))
1782 editor
= core
.list2cmdline([unix_path(sys
.executable
), xbase
])
1786 class SequenceEditorEnvironment(object):
1787 """Set environment variables to enable git-cola-sequence-editor"""
1789 def __init__(self
, context
, **kwargs
):
1791 'GIT_EDITOR': prefs
.editor(context
),
1792 'GIT_SEQUENCE_EDITOR': sequence_editor(),
1793 'GIT_COLA_SEQ_EDITOR_CANCEL_ACTION': 'save',
1795 self
.env
.update(kwargs
)
1797 def __enter__(self
):
1798 for var
, value
in self
.env
.items():
1799 compat
.setenv(var
, value
)
1802 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
1803 for var
in self
.env
:
1804 compat
.unsetenv(var
)
1807 class Rebase(ContextCommand
):
1808 def __init__(self
, context
, upstream
=None, branch
=None, **kwargs
):
1809 """Start an interactive rebase session
1811 :param upstream: upstream branch
1812 :param branch: optional branch to checkout
1813 :param kwargs: forwarded directly to `git.rebase()`
1816 super(Rebase
, self
).__init
__(context
)
1818 self
.upstream
= upstream
1819 self
.branch
= branch
1820 self
.kwargs
= kwargs
1822 def prepare_arguments(self
, upstream
):
1826 # Rebase actions must be the only option specified
1827 for action
in ('continue', 'abort', 'skip', 'edit_todo'):
1828 if self
.kwargs
.get(action
, False):
1829 kwargs
[action
] = self
.kwargs
[action
]
1832 kwargs
['interactive'] = True
1833 kwargs
['autosquash'] = self
.kwargs
.get('autosquash', True)
1834 kwargs
.update(self
.kwargs
)
1837 args
.append(upstream
)
1839 args
.append(self
.branch
)
1844 (status
, out
, err
) = (1, '', '')
1845 context
= self
.context
1849 if not cfg
.get('rebase.autostash', False):
1850 if model
.staged
or model
.unmerged
or model
.modified
:
1851 Interaction
.information(
1852 N_('Unable to rebase'),
1853 N_('You cannot rebase with uncommitted changes.'),
1855 return status
, out
, err
1857 upstream
= self
.upstream
or Interaction
.choose_ref(
1859 N_('Select New Upstream'),
1860 N_('Interactive Rebase'),
1861 default
='@{upstream}',
1864 return status
, out
, err
1866 self
.model
.is_rebasing
= True
1867 self
.model
.emit_updated()
1869 args
, kwargs
= self
.prepare_arguments(upstream
)
1870 upstream_title
= upstream
or '@{upstream}'
1871 with
SequenceEditorEnvironment(
1873 GIT_COLA_SEQ_EDITOR_TITLE
=N_('Rebase onto %s') % upstream_title
,
1874 GIT_COLA_SEQ_EDITOR_ACTION
=N_('Rebase'),
1876 # TODO this blocks the user interface window for the duration
1877 # of git-cola-sequence-editor. We would need to implement
1878 # signals for QProcess and continue running the main thread.
1879 # Alternatively, we can hide the main window while rebasing.
1880 # That doesn't require as much effort.
1881 status
, out
, err
= self
.git
.rebase(
1882 *args
, _no_win32_startupinfo
=True, **kwargs
1884 self
.model
.update_status()
1885 if err
.strip() != 'Nothing to do':
1886 title
= N_('Rebase stopped')
1887 Interaction
.command(title
, 'git rebase', status
, out
, err
)
1888 return status
, out
, err
1891 class RebaseEditTodo(ContextCommand
):
1893 (status
, out
, err
) = (1, '', '')
1894 with
SequenceEditorEnvironment(
1896 GIT_COLA_SEQ_EDITOR_TITLE
=N_('Edit Rebase'),
1897 GIT_COLA_SEQ_EDITOR_ACTION
=N_('Save'),
1899 status
, out
, err
= self
.git
.rebase(edit_todo
=True)
1900 Interaction
.log_status(status
, out
, err
)
1901 self
.model
.update_status()
1902 return status
, out
, err
1905 class RebaseContinue(ContextCommand
):
1907 (status
, out
, err
) = (1, '', '')
1908 with
SequenceEditorEnvironment(
1910 GIT_COLA_SEQ_EDITOR_TITLE
=N_('Rebase'),
1911 GIT_COLA_SEQ_EDITOR_ACTION
=N_('Rebase'),
1913 status
, out
, err
= self
.git
.rebase('--continue')
1914 Interaction
.log_status(status
, out
, err
)
1915 self
.model
.update_status()
1916 return status
, out
, err
1919 class RebaseSkip(ContextCommand
):
1921 (status
, out
, err
) = (1, '', '')
1922 with
SequenceEditorEnvironment(
1924 GIT_COLA_SEQ_EDITOR_TITLE
=N_('Rebase'),
1925 GIT_COLA_SEQ_EDITOR_ACTION
=N_('Rebase'),
1927 status
, out
, err
= self
.git
.rebase(skip
=True)
1928 Interaction
.log_status(status
, out
, err
)
1929 self
.model
.update_status()
1930 return status
, out
, err
1933 class RebaseAbort(ContextCommand
):
1935 status
, out
, err
= self
.git
.rebase(abort
=True)
1936 Interaction
.log_status(status
, out
, err
)
1937 self
.model
.update_status()
1940 class Rescan(ContextCommand
):
1941 """Rescan for changes"""
1944 self
.model
.update_status()
1947 class Refresh(ContextCommand
):
1948 """Update refs, refresh the index, and update config"""
1952 return N_('Refresh')
1955 self
.model
.update_status(update_index
=True)
1957 self
.fsmonitor
.refresh()
1960 class RefreshConfig(ContextCommand
):
1961 """Refresh the git config cache"""
1967 class RevertEditsCommand(ConfirmAction
):
1968 def __init__(self
, context
):
1969 super(RevertEditsCommand
, self
).__init
__(context
)
1970 self
.icon
= icons
.undo()
1972 def ok_to_run(self
):
1973 return self
.model
.undoable()
1975 # pylint: disable=no-self-use
1976 def checkout_from_head(self
):
1979 def checkout_args(self
):
1981 s
= self
.selection
.selection()
1982 if self
.checkout_from_head():
1983 args
.append(self
.model
.head
)
1995 checkout_args
= self
.checkout_args()
1996 return self
.git
.checkout(*checkout_args
)
1999 self
.model
.set_diff_type(main
.Types
.TEXT
)
2000 self
.model
.update_file_status()
2003 class RevertUnstagedEdits(RevertEditsCommand
):
2006 return N_('Revert Unstaged Edits...')
2008 def checkout_from_head(self
):
2009 # Being in amend mode should not affect the behavior of this command.
2010 # The only sensible thing to do is to checkout from the index.
2014 title
= N_('Revert Unstaged Changes?')
2016 'This operation removes unstaged edits from selected files.\n'
2017 'These changes cannot be recovered.'
2019 info
= N_('Revert the unstaged changes?')
2020 ok_text
= N_('Revert Unstaged Changes')
2021 return Interaction
.confirm(
2022 title
, text
, info
, ok_text
, default
=True, icon
=self
.icon
2026 class RevertUncommittedEdits(RevertEditsCommand
):
2029 return N_('Revert Uncommitted Edits...')
2031 def checkout_from_head(self
):
2035 """Prompt for reverting changes"""
2036 title
= N_('Revert Uncommitted Changes?')
2038 'This operation removes uncommitted edits from selected files.\n'
2039 'These changes cannot be recovered.'
2041 info
= N_('Revert the uncommitted changes?')
2042 ok_text
= N_('Revert Uncommitted Changes')
2043 return Interaction
.confirm(
2044 title
, text
, info
, ok_text
, default
=True, icon
=self
.icon
2048 class RunConfigAction(ContextCommand
):
2049 """Run a user-configured action, typically from the "Tools" menu"""
2051 def __init__(self
, context
, action_name
):
2052 super(RunConfigAction
, self
).__init
__(context
)
2053 self
.action_name
= action_name
2056 """Run the user-configured action"""
2057 for env
in ('ARGS', 'DIRNAME', 'FILENAME', 'REVISION'):
2059 compat
.unsetenv(env
)
2064 context
= self
.context
2066 opts
= cfg
.get_guitool_opts(self
.action_name
)
2067 cmd
= opts
.get('cmd')
2068 if 'title' not in opts
:
2071 if 'prompt' not in opts
or opts
.get('prompt') is True:
2072 prompt
= N_('Run "%s"?') % cmd
2073 opts
['prompt'] = prompt
2075 if opts
.get('needsfile'):
2076 filename
= self
.selection
.filename()
2078 Interaction
.information(
2079 N_('Please select a file'),
2080 N_('"%s" requires a selected file.') % cmd
,
2083 dirname
= utils
.dirname(filename
, current_dir
='.')
2084 compat
.setenv('FILENAME', filename
)
2085 compat
.setenv('DIRNAME', dirname
)
2087 if opts
.get('revprompt') or opts
.get('argprompt'):
2089 ok
= Interaction
.confirm_config_action(context
, cmd
, opts
)
2092 rev
= opts
.get('revision')
2093 args
= opts
.get('args')
2094 if opts
.get('revprompt') and not rev
:
2095 title
= N_('Invalid Revision')
2096 msg
= N_('The revision expression cannot be empty.')
2097 Interaction
.critical(title
, msg
)
2101 elif opts
.get('confirm'):
2102 title
= os
.path
.expandvars(opts
.get('title'))
2103 prompt
= os
.path
.expandvars(opts
.get('prompt'))
2104 if not Interaction
.question(title
, prompt
):
2107 compat
.setenv('REVISION', rev
)
2109 compat
.setenv('ARGS', args
)
2110 title
= os
.path
.expandvars(cmd
)
2111 Interaction
.log(N_('Running command: %s') % title
)
2112 cmd
= ['sh', '-c', cmd
]
2114 if opts
.get('background'):
2116 status
, out
, err
= (0, '', '')
2117 elif opts
.get('noconsole'):
2118 status
, out
, err
= core
.run_command(cmd
)
2120 status
, out
, err
= Interaction
.run_command(title
, cmd
)
2122 if not opts
.get('background') and not opts
.get('norescan'):
2123 self
.model
.update_status()
2126 Interaction
.command(title
, cmd
, status
, out
, err
)
2131 class SetDefaultRepo(ContextCommand
):
2132 """Set the default repository"""
2134 def __init__(self
, context
, repo
):
2135 super(SetDefaultRepo
, self
).__init
__(context
)
2139 self
.cfg
.set_user('cola.defaultrepo', self
.repo
)
2142 class SetDiffText(EditModel
):
2143 """Set the diff text"""
2147 def __init__(self
, context
, text
):
2148 super(SetDiffText
, self
).__init
__(context
)
2149 self
.new_diff_text
= text
2150 self
.new_diff_type
= main
.Types
.TEXT
2151 self
.new_file_type
= main
.Types
.TEXT
2154 class SetUpstreamBranch(ContextCommand
):
2155 """Set the upstream branch"""
2157 def __init__(self
, context
, branch
, remote
, remote_branch
):
2158 super(SetUpstreamBranch
, self
).__init
__(context
)
2159 self
.branch
= branch
2160 self
.remote
= remote
2161 self
.remote_branch
= remote_branch
2165 remote
= self
.remote
2166 branch
= self
.branch
2167 remote_branch
= self
.remote_branch
2168 cfg
.set_repo('branch.%s.remote' % branch
, remote
)
2169 cfg
.set_repo('branch.%s.merge' % branch
, 'refs/heads/' + remote_branch
)
2172 def format_hex(data
):
2173 """Translate binary data into a hex dump"""
2174 hexdigits
= '0123456789ABCDEF'
2177 byte_offset_to_int
= compat
.byte_offset_to_int_converter()
2178 while offset
< len(data
):
2179 result
+= '%04u |' % offset
2181 for i
in range(0, 16):
2182 if i
> 0 and i
% 4 == 0:
2184 if offset
< len(data
):
2185 v
= byte_offset_to_int(data
[offset
])
2186 result
+= ' ' + hexdigits
[v
>> 4] + hexdigits
[v
& 0xF]
2187 textpart
+= chr(v
) if 32 <= v
< 127 else '.'
2192 result
+= ' | ' + textpart
+ ' |\n'
2197 class ShowUntracked(EditModel
):
2198 """Show an untracked file."""
2200 def __init__(self
, context
, filename
):
2201 super(ShowUntracked
, self
).__init
__(context
)
2202 self
.new_filename
= filename
2203 self
.new_mode
= self
.model
.mode_untracked
2204 self
.new_diff_text
= self
.read(filename
)
2205 self
.new_diff_type
= main
.Types
.TEXT
2206 self
.new_file_type
= main
.Types
.TEXT
2208 def read(self
, filename
):
2209 """Read file contents"""
2211 size
= cfg
.get('cola.readsize', 2048)
2213 result
= core
.read(filename
, size
=size
, encoding
='bytes')
2214 except (IOError, OSError):
2217 truncated
= len(result
) == size
2219 encoding
= cfg
.file_encoding(filename
) or core
.ENCODING
2221 text_result
= core
.decode_maybe(result
, encoding
)
2222 except UnicodeError:
2223 text_result
= format_hex(result
)
2226 text_result
+= '...'
2230 class SignOff(ContextCommand
):
2231 """Append a signoff to the commit message"""
2237 return N_('Sign Off')
2239 def __init__(self
, context
):
2240 super(SignOff
, self
).__init
__(context
)
2241 self
.old_commitmsg
= self
.model
.commitmsg
2244 """Add a signoff to the commit message"""
2245 signoff
= self
.signoff()
2246 if signoff
in self
.model
.commitmsg
:
2248 msg
= self
.model
.commitmsg
.rstrip()
2249 self
.model
.set_commitmsg(msg
+ '\n' + signoff
)
2252 """Restore the commit message"""
2253 self
.model
.set_commitmsg(self
.old_commitmsg
)
2256 """Generate the signoff string"""
2258 import pwd
# pylint: disable=all
2260 user
= pwd
.getpwuid(os
.getuid()).pw_name
2262 user
= os
.getenv('USER', N_('unknown'))
2265 name
= cfg
.get('user.name', user
)
2266 email
= cfg
.get('user.email', '%s@%s' % (user
, core
.node()))
2267 return '\nSigned-off-by: %s <%s>' % (name
, email
)
2270 def check_conflicts(context
, unmerged
):
2271 """Check paths for conflicts
2273 Conflicting files can be filtered out one-by-one.
2276 if prefs
.check_conflicts(context
):
2277 unmerged
= [path
for path
in unmerged
if is_conflict_free(path
)]
2281 def is_conflict_free(path
):
2282 """Return True if `path` contains no conflict markers
2284 rgx
= re
.compile(r
'^(<<<<<<<|\|\|\|\|\|\|\||>>>>>>>) ')
2286 with core
.xopen(path
, 'r') as f
:
2288 line
= core
.decode(line
, errors
='ignore')
2290 return should_stage_conflicts(path
)
2292 # We can't read this file ~ we may be staging a removal
2297 def should_stage_conflicts(path
):
2298 """Inform the user that a file contains merge conflicts
2300 Return `True` if we should stage the path nonetheless.
2303 title
= msg
= N_('Stage conflicts?')
2306 '%s appears to contain merge conflicts.\n\n'
2307 'You should probably skip this file.\n'
2312 ok_text
= N_('Stage conflicts')
2313 cancel_text
= N_('Skip')
2314 return Interaction
.confirm(
2315 title
, msg
, info
, ok_text
, default
=False, cancel_text
=cancel_text
2319 class Stage(ContextCommand
):
2320 """Stage a set of paths."""
2326 def __init__(self
, context
, paths
):
2327 super(Stage
, self
).__init
__(context
)
2331 msg
= N_('Staging: %s') % (', '.join(self
.paths
))
2332 Interaction
.log(msg
)
2333 return self
.stage_paths()
2335 def stage_paths(self
):
2336 """Stages add/removals to git."""
2337 context
= self
.context
2340 if self
.model
.cfg
.get('cola.safemode', False):
2342 return self
.stage_all()
2347 for path
in set(paths
):
2348 if core
.exists(path
) or core
.islink(path
):
2349 if path
.endswith('/'):
2350 path
= path
.rstrip('/')
2355 self
.model
.emit_about_to_update()
2357 # `git add -u` doesn't work on untracked files
2359 status
, out
, err
= gitcmds
.add(context
, add
)
2360 Interaction
.command(N_('Error'), 'git add', status
, out
, err
)
2362 # If a path doesn't exist then that means it should be removed
2363 # from the index. We use `git add -u` for that.
2365 status
, out
, err
= gitcmds
.add(context
, remove
, u
=True)
2366 Interaction
.command(N_('Error'), 'git add -u', status
, out
, err
)
2368 self
.model
.update_files(emit
=True)
2369 return status
, out
, err
2371 def stage_all(self
):
2372 """Stage all files"""
2373 status
, out
, err
= self
.git
.add(v
=True, u
=True)
2374 Interaction
.command(N_('Error'), 'git add -u', status
, out
, err
)
2375 self
.model
.update_file_status()
2376 return (status
, out
, err
)
2379 class StageCarefully(Stage
):
2380 """Only stage when the path list is non-empty
2382 We use "git add -u -- <pathspec>" to stage, and it stages everything by
2383 default when no pathspec is specified, so this class ensures that paths
2384 are specified before calling git.
2386 When no paths are specified, the command does nothing.
2390 def __init__(self
, context
):
2391 super(StageCarefully
, self
).__init
__(context
, None)
2394 # pylint: disable=no-self-use
2395 def init_paths(self
):
2396 """Initialize path data"""
2399 def ok_to_run(self
):
2400 """Prevent catch-all "git add -u" from adding unmerged files"""
2401 return self
.paths
or not self
.model
.unmerged
2404 """Stage files when ok_to_run() return True"""
2405 if self
.ok_to_run():
2406 return super(StageCarefully
, self
).do()
2410 class StageModified(StageCarefully
):
2411 """Stage all modified files."""
2415 return N_('Stage Modified')
2417 def init_paths(self
):
2418 self
.paths
= self
.model
.modified
2421 class StageUnmerged(StageCarefully
):
2422 """Stage unmerged files."""
2426 return N_('Stage Unmerged')
2428 def init_paths(self
):
2429 self
.paths
= check_conflicts(self
.context
, self
.model
.unmerged
)
2432 class StageUntracked(StageCarefully
):
2433 """Stage all untracked files."""
2437 return N_('Stage Untracked')
2439 def init_paths(self
):
2440 self
.paths
= self
.model
.untracked
2443 class StageOrUnstage(ContextCommand
):
2444 """If the selection is staged, unstage it, otherwise stage"""
2448 return N_('Stage / Unstage')
2451 s
= self
.selection
.selection()
2453 do(Unstage
, self
.context
, s
.staged
)
2456 unmerged
= check_conflicts(self
.context
, s
.unmerged
)
2458 unstaged
.extend(unmerged
)
2460 unstaged
.extend(s
.modified
)
2462 unstaged
.extend(s
.untracked
)
2464 do(Stage
, self
.context
, unstaged
)
2467 class Tag(ContextCommand
):
2468 """Create a tag object."""
2470 def __init__(self
, context
, name
, revision
, sign
=False, message
=''):
2471 super(Tag
, self
).__init
__(context
)
2473 self
._message
= message
2474 self
._revision
= revision
2480 revision
= self
._revision
2481 tag_name
= self
._name
2482 tag_message
= self
._message
2485 Interaction
.critical(
2486 N_('Missing Revision'), N_('Please specify a revision to tag.')
2491 Interaction
.critical(
2492 N_('Missing Name'), N_('Please specify a name for the new tag.')
2496 title
= N_('Missing Tag Message')
2497 message
= N_('Tag-signing was requested but the tag message is empty.')
2499 'An unsigned, lightweight tag will be created instead.\n'
2500 'Create an unsigned tag?'
2502 ok_text
= N_('Create Unsigned Tag')
2504 if sign
and not tag_message
:
2505 # We require a message in order to sign the tag, so if they
2506 # choose to create an unsigned tag we have to clear the sign flag.
2507 if not Interaction
.confirm(
2508 title
, message
, info
, ok_text
, default
=False, icon
=icons
.save()
2517 tmp_file
= utils
.tmp_filename('tag-message')
2518 opts
['file'] = tmp_file
2519 core
.write(tmp_file
, tag_message
)
2524 opts
['annotate'] = True
2525 status
, out
, err
= git
.tag(tag_name
, revision
, **opts
)
2528 core
.unlink(tmp_file
)
2530 title
= N_('Error: could not create tag "%s"') % tag_name
2531 Interaction
.command(title
, 'git tag', status
, out
, err
)
2535 self
.model
.update_status()
2536 Interaction
.information(
2538 N_('Created a new tag named "%s"') % tag_name
,
2539 details
=tag_message
or None,
2545 class Unstage(ContextCommand
):
2546 """Unstage a set of paths."""
2550 return N_('Unstage')
2552 def __init__(self
, context
, paths
):
2553 super(Unstage
, self
).__init
__(context
)
2558 context
= self
.context
2559 head
= self
.model
.head
2562 msg
= N_('Unstaging: %s') % (', '.join(paths
))
2563 Interaction
.log(msg
)
2565 return unstage_all(context
)
2566 status
, out
, err
= gitcmds
.unstage_paths(context
, paths
, head
=head
)
2567 Interaction
.command(N_('Error'), 'git reset', status
, out
, err
)
2568 self
.model
.update_file_status()
2569 return (status
, out
, err
)
2572 class UnstageAll(ContextCommand
):
2573 """Unstage all files; resets the index."""
2576 return unstage_all(self
.context
)
2579 def unstage_all(context
):
2580 """Unstage all files, even while amending"""
2581 model
= context
.model
2584 status
, out
, err
= git
.reset(head
, '--', '.')
2585 Interaction
.command(N_('Error'), 'git reset', status
, out
, err
)
2586 model
.update_file_status()
2587 return (status
, out
, err
)
2590 class StageSelected(ContextCommand
):
2591 """Stage selected files, or all files if no selection exists."""
2594 context
= self
.context
2595 paths
= self
.selection
.unstaged
2597 do(Stage
, context
, paths
)
2598 elif self
.cfg
.get('cola.safemode', False):
2599 do(StageModified
, context
)
2602 class UnstageSelected(Unstage
):
2603 """Unstage selected files."""
2605 def __init__(self
, context
):
2606 staged
= self
.selection
.staged
2607 super(UnstageSelected
, self
).__init
__(context
, staged
)
2610 class Untrack(ContextCommand
):
2611 """Unstage a set of paths."""
2613 def __init__(self
, context
, paths
):
2614 super(Untrack
, self
).__init
__(context
)
2618 msg
= N_('Untracking: %s') % (', '.join(self
.paths
))
2619 Interaction
.log(msg
)
2620 status
, out
, err
= self
.model
.untrack_paths(self
.paths
)
2621 Interaction
.log_status(status
, out
, err
)
2624 class UntrackedSummary(EditModel
):
2625 """List possible .gitignore rules as the diff text."""
2627 def __init__(self
, context
):
2628 super(UntrackedSummary
, self
).__init
__(context
)
2629 untracked
= self
.model
.untracked
2630 suffix
= 's' if untracked
else ''
2632 io
.write('# %s untracked file%s\n' % (len(untracked
), suffix
))
2634 io
.write('# possible .gitignore rule%s:\n' % suffix
)
2636 io
.write('/' + u
+ '\n')
2637 self
.new_diff_text
= io
.getvalue()
2638 self
.new_diff_type
= main
.Types
.TEXT
2639 self
.new_file_type
= main
.Types
.TEXT
2640 self
.new_mode
= self
.model
.mode_untracked
2643 class VisualizeAll(ContextCommand
):
2644 """Visualize all branches."""
2647 context
= self
.context
2648 browser
= utils
.shell_split(prefs
.history_browser(context
))
2649 launch_history_browser(browser
+ ['--all'])
2652 class VisualizeCurrent(ContextCommand
):
2653 """Visualize all branches."""
2656 context
= self
.context
2657 browser
= utils
.shell_split(prefs
.history_browser(context
))
2658 launch_history_browser(browser
+ [self
.model
.currentbranch
] + ['--'])
2661 class VisualizePaths(ContextCommand
):
2662 """Path-limited visualization."""
2664 def __init__(self
, context
, paths
):
2665 super(VisualizePaths
, self
).__init
__(context
)
2666 context
= self
.context
2667 browser
= utils
.shell_split(prefs
.history_browser(context
))
2669 self
.argv
= browser
+ ['--'] + list(paths
)
2674 launch_history_browser(self
.argv
)
2677 class VisualizeRevision(ContextCommand
):
2678 """Visualize a specific revision."""
2680 def __init__(self
, context
, revision
, paths
=None):
2681 super(VisualizeRevision
, self
).__init
__(context
)
2682 self
.revision
= revision
2686 context
= self
.context
2687 argv
= utils
.shell_split(prefs
.history_browser(context
))
2689 argv
.append(self
.revision
)
2692 argv
.extend(self
.paths
)
2693 launch_history_browser(argv
)
2696 class SubmoduleAdd(ConfirmAction
):
2697 """Add specified submodules"""
2699 def __init__(self
, context
, url
, path
, branch
, depth
, reference
):
2700 super(SubmoduleAdd
, self
).__init
__(context
)
2703 self
.branch
= branch
2705 self
.reference
= reference
2708 title
= N_('Add Submodule...')
2709 question
= N_('Add this submodule?')
2710 info
= N_('The submodule will be added using\n' '"%s"' % self
.command())
2711 ok_txt
= N_('Add Submodule')
2712 return Interaction
.confirm(title
, question
, info
, ok_txt
, icon
=icons
.ok())
2715 context
= self
.context
2716 args
= self
.get_args()
2717 return context
.git
.submodule('add', *args
)
2720 self
.model
.update_file_status()
2721 self
.model
.update_submodules_list()
2723 def error_message(self
):
2724 return N_('Error updating submodule %s' % self
.path
)
2727 cmd
= ['git', 'submodule', 'add']
2728 cmd
.extend(self
.get_args())
2729 return core
.list2cmdline(cmd
)
2734 args
.extend(['--branch', self
.branch
])
2736 args
.extend(['--reference', self
.reference
])
2738 args
.extend(['--depth', '%d' % self
.depth
])
2739 args
.extend(['--', self
.url
])
2741 args
.append(self
.path
)
2745 class SubmoduleUpdate(ConfirmAction
):
2746 """Update specified submodule"""
2748 def __init__(self
, context
, path
):
2749 super(SubmoduleUpdate
, self
).__init
__(context
)
2753 title
= N_('Update Submodule...')
2754 question
= N_('Update this submodule?')
2755 info
= N_('The submodule will be updated using\n' '"%s"' % self
.command())
2756 ok_txt
= N_('Update Submodule')
2757 return Interaction
.confirm(
2758 title
, question
, info
, ok_txt
, default
=False, icon
=icons
.pull()
2762 context
= self
.context
2763 args
= self
.get_args()
2764 return context
.git
.submodule(*args
)
2767 self
.model
.update_file_status()
2769 def error_message(self
):
2770 return N_('Error updating submodule %s' % self
.path
)
2773 cmd
= ['git', 'submodule']
2774 cmd
.extend(self
.get_args())
2775 return core
.list2cmdline(cmd
)
2779 if version
.check_git(self
.context
, 'submodule-update-recursive'):
2780 cmd
.append('--recursive')
2781 cmd
.extend(['--', self
.path
])
2785 class SubmodulesUpdate(ConfirmAction
):
2786 """Update all submodules"""
2789 title
= N_('Update submodules...')
2790 question
= N_('Update all submodules?')
2791 info
= N_('All submodules will be updated using\n' '"%s"' % self
.command())
2792 ok_txt
= N_('Update Submodules')
2793 return Interaction
.confirm(
2794 title
, question
, info
, ok_txt
, default
=False, icon
=icons
.pull()
2798 context
= self
.context
2799 args
= self
.get_args()
2800 return context
.git
.submodule(*args
)
2803 self
.model
.update_file_status()
2805 def error_message(self
):
2806 return N_('Error updating submodules')
2809 cmd
= ['git', 'submodule']
2810 cmd
.extend(self
.get_args())
2811 return core
.list2cmdline(cmd
)
2815 if version
.check_git(self
.context
, 'submodule-update-recursive'):
2816 cmd
.append('--recursive')
2820 def launch_history_browser(argv
):
2821 """Launch the configured history browser"""
2824 except OSError as e
:
2825 _
, details
= utils
.format_exception(e
)
2826 title
= N_('Error Launching History Browser')
2827 msg
= N_('Cannot exec "%s": please configure a history browser') % ' '.join(
2830 Interaction
.critical(title
, message
=msg
, details
=details
)
2833 def run(cls
, *args
, **opts
):
2835 Returns a callback that runs a command
2837 If the caller of run() provides args or opts then those are
2838 used instead of the ones provided by the invoker of the callback.
2842 def runner(*local_args
, **local_opts
):
2843 """Closure return by run() which runs the command"""
2845 do(cls
, *args
, **opts
)
2847 do(cls
, *local_args
, **local_opts
)
2852 def do(cls
, *args
, **opts
):
2853 """Run a command in-place"""
2855 cmd
= cls(*args
, **opts
)
2857 except Exception as e
: # pylint: disable=broad-except
2858 msg
, details
= utils
.format_exception(e
)
2859 if hasattr(cls
, '__name__'):
2860 msg
= '%s exception:\n%s' % (cls
.__name
__, msg
)
2861 Interaction
.critical(N_('Error'), message
=msg
, details
=details
)
2865 def difftool_run(context
):
2866 """Start a default difftool session"""
2867 selection
= context
.selection
2868 files
= selection
.group()
2871 s
= selection
.selection()
2872 head
= context
.model
.head
2873 difftool_launch_with_head(context
, files
, bool(s
.staged
), head
)
2876 def difftool_launch_with_head(context
, filenames
, staged
, head
):
2877 """Launch difftool against the provided head"""
2882 difftool_launch(context
, left
=left
, staged
=staged
, paths
=filenames
)
2885 def difftool_launch(
2892 left_take_magic
=False,
2893 left_take_parent
=False,
2895 """Launches 'git difftool' with given parameters
2897 :param left: first argument to difftool
2898 :param right: second argument to difftool_args
2899 :param paths: paths to diff
2900 :param staged: activate `git difftool --staged`
2901 :param dir_diff: activate `git difftool --dir-diff`
2902 :param left_take_magic: whether to append the magic ^! diff expression
2903 :param left_take_parent: whether to append the first-parent ~ for diffing
2907 difftool_args
= ['git', 'difftool', '--no-prompt']
2909 difftool_args
.append('--cached')
2911 difftool_args
.append('--dir-diff')
2914 if left_take_parent
or left_take_magic
:
2915 suffix
= '^!' if left_take_magic
else '~'
2916 # Check root commit (no parents and thus cannot execute '~')
2918 status
, out
, err
= git
.rev_list(left
, parents
=True, n
=1)
2919 Interaction
.log_status(status
, out
, err
)
2921 raise OSError('git rev-list command failed')
2923 if len(out
.split()) >= 2:
2924 # Commit has a parent, so we can take its child as requested
2927 # No parent, assume it's the root commit, so we have to diff
2928 # against the empty tree.
2929 left
= EMPTY_TREE_OID
2930 if not right
and left_take_magic
:
2932 difftool_args
.append(left
)
2935 difftool_args
.append(right
)
2938 difftool_args
.append('--')
2939 difftool_args
.extend(paths
)
2941 runtask
= context
.runtask
2943 Interaction
.async_command(N_('Difftool'), difftool_args
, runtask
)
2945 core
.fork(difftool_args
)