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 prefs
30 from .settings
import Settings
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"""
46 def __init__(self
, context
):
47 """Common edit operations on the main model"""
48 super(EditModel
, self
).__init
__(context
)
50 self
.old_diff_text
= self
.model
.diff_text
51 self
.old_filename
= self
.model
.filename
52 self
.old_mode
= self
.model
.mode
53 self
.old_diff_type
= self
.model
.diff_type
55 self
.new_diff_text
= self
.old_diff_text
56 self
.new_filename
= self
.old_filename
57 self
.new_mode
= self
.old_mode
58 self
.new_diff_type
= self
.old_diff_type
61 """Perform the operation."""
62 self
.model
.set_filename(self
.new_filename
)
63 self
.model
.set_mode(self
.new_mode
)
64 self
.model
.set_diff_text(self
.new_diff_text
)
65 self
.model
.set_diff_type(self
.new_diff_type
)
68 """Undo the operation."""
69 self
.model
.set_filename(self
.old_filename
)
70 self
.model
.set_mode(self
.old_mode
)
71 self
.model
.set_diff_text(self
.old_diff_text
)
72 self
.model
.set_diff_type(self
.old_diff_type
)
75 class ConfirmAction(ContextCommand
):
76 """Confirm an action before running it"""
78 # pylint: disable=no-self-use
80 """Return True when the command is ok to run"""
83 # pylint: disable=no-self-use
85 """Prompt for confirmation"""
88 # pylint: disable=no-self-use
90 """Run the command and return (status, out, err)"""
93 # pylint: disable=no-self-use
95 """Callback run on success"""
98 # pylint: disable=no-self-use
100 """Command name, for error messages"""
103 # pylint: disable=no-self-use
104 def error_message(self
):
105 """Command error message"""
109 """Prompt for confirmation before running a command"""
112 ok
= self
.ok_to_run() and self
.confirm()
114 status
, out
, err
= self
.action()
117 title
= self
.error_message()
119 Interaction
.command(title
, cmd
, status
, out
, err
)
121 return ok
, status
, out
, err
124 class AbortMerge(ConfirmAction
):
125 """Reset an in-progress merge back to HEAD"""
128 title
= N_('Abort Merge...')
129 question
= N_('Aborting the current merge?')
130 info
= N_('Aborting the current merge will cause '
131 '*ALL* uncommitted changes to be lost.\n'
132 'Recovering uncommitted changes is not possible.')
133 ok_txt
= N_('Abort Merge')
134 return Interaction
.confirm(title
, question
, info
, ok_txt
,
135 default
=False, icon
=icons
.undo())
138 status
, out
, err
= gitcmds
.abort_merge(self
.context
)
139 self
.model
.update_file_status()
140 return status
, out
, err
143 self
.model
.set_commitmsg('')
145 def error_message(self
):
152 class AmendMode(EditModel
):
153 """Try to amend a commit."""
161 def __init__(self
, context
, amend
=True):
162 super(AmendMode
, self
).__init
__(context
)
164 self
.amending
= amend
165 self
.old_commitmsg
= self
.model
.commitmsg
166 self
.old_mode
= self
.model
.mode
169 self
.new_mode
= self
.model
.mode_amend
170 self
.new_commitmsg
= gitcmds
.prev_commitmsg(context
)
171 AmendMode
.LAST_MESSAGE
= self
.model
.commitmsg
173 # else, amend unchecked, regular commit
174 self
.new_mode
= self
.model
.mode_none
175 self
.new_diff_text
= ''
176 self
.new_commitmsg
= self
.model
.commitmsg
177 # If we're going back into new-commit-mode then search the
178 # undo stack for a previous amend-commit-mode and grab the
179 # commit message at that point in time.
180 if AmendMode
.LAST_MESSAGE
is not None:
181 self
.new_commitmsg
= AmendMode
.LAST_MESSAGE
182 AmendMode
.LAST_MESSAGE
= None
185 """Leave/enter amend mode."""
186 # Attempt to enter amend mode. Do not allow this when merging.
188 if self
.model
.is_merging
:
190 self
.model
.set_mode(self
.old_mode
)
191 Interaction
.information(
193 N_('You are in the middle of a merge.\n'
194 'Cannot amend while merging.'))
197 super(AmendMode
, self
).do()
198 self
.model
.set_commitmsg(self
.new_commitmsg
)
199 self
.model
.update_file_status()
204 self
.model
.set_commitmsg(self
.old_commitmsg
)
205 super(AmendMode
, self
).undo()
206 self
.model
.update_file_status()
209 class AnnexAdd(ContextCommand
):
210 """Add to Git Annex"""
212 def __init__(self
, context
):
213 super(AnnexAdd
, self
).__init
__(context
)
214 self
.filename
= self
.selection
.filename()
217 status
, out
, err
= self
.git
.annex('add', self
.filename
)
218 Interaction
.command(N_('Error'), 'git annex add', status
, out
, err
)
219 self
.model
.update_status()
222 class AnnexInit(ContextCommand
):
223 """Initialize Git Annex"""
226 status
, out
, err
= self
.git
.annex('init')
227 Interaction
.command(N_('Error'), 'git annex init', status
, out
, err
)
228 self
.model
.cfg
.reset()
229 self
.model
.emit_updated()
232 class LFSTrack(ContextCommand
):
233 """Add a file to git lfs"""
235 def __init__(self
, context
):
236 super(LFSTrack
, self
).__init
__(context
)
237 self
.filename
= self
.selection
.filename()
238 self
.stage_cmd
= Stage(context
, [self
.filename
])
241 status
, out
, err
= self
.git
.lfs('track', self
.filename
)
243 N_('Error'), 'git lfs track', status
, out
, err
)
248 class LFSInstall(ContextCommand
):
249 """Initialize git lfs"""
252 status
, out
, err
= self
.git
.lfs('install')
254 N_('Error'), 'git lfs install', status
, out
, err
)
255 self
.model
.update_config(reset
=True, emit
=True)
258 class ApplyDiffSelection(ContextCommand
):
259 """Apply the selected diff to the worktree or index"""
261 def __init__(self
, context
, first_line_idx
, last_line_idx
, has_selection
,
262 reverse
, apply_to_worktree
):
263 super(ApplyDiffSelection
, self
).__init
__(context
)
264 self
.first_line_idx
= first_line_idx
265 self
.last_line_idx
= last_line_idx
266 self
.has_selection
= has_selection
267 self
.reverse
= reverse
268 self
.apply_to_worktree
= apply_to_worktree
271 context
= self
.context
272 cfg
= self
.context
.cfg
273 diff_text
= self
.model
.diff_text
275 parser
= DiffParser(self
.model
.filename
, diff_text
)
276 if self
.has_selection
:
277 patch
= parser
.generate_patch(
278 self
.first_line_idx
, self
.last_line_idx
, reverse
=self
.reverse
)
280 patch
= parser
.generate_hunk_patch(
281 self
.first_line_idx
, reverse
=self
.reverse
)
285 if isinstance(diff_text
, core
.UStr
):
286 # original encoding must prevail
287 encoding
= diff_text
.encoding
289 encoding
= cfg
.file_encoding(self
.model
.filename
)
291 tmp_file
= utils
.tmp_filename('patch')
293 core
.write(tmp_file
, patch
, encoding
=encoding
)
294 if self
.apply_to_worktree
:
295 status
, out
, err
= gitcmds
.apply_diff_to_worktree(
298 status
, out
, err
= gitcmds
.apply_diff(context
, tmp_file
)
300 core
.unlink(tmp_file
)
302 Interaction
.log_status(status
, out
, err
)
303 self
.model
.update_file_status(update_index
=True)
306 class ApplyPatches(ContextCommand
):
307 """Apply patches using the "git am" command"""
309 def __init__(self
, context
, patches
):
310 super(ApplyPatches
, self
).__init
__(context
)
311 self
.patches
= patches
314 status
, out
, err
= self
.git
.am('-3', *self
.patches
)
315 Interaction
.log_status(status
, out
, err
)
318 self
.model
.update_file_status()
320 patch_basenames
= [os
.path
.basename(p
) for p
in self
.patches
]
321 if len(patch_basenames
) > 25:
322 patch_basenames
= patch_basenames
[:25]
323 patch_basenames
.append('...')
325 basenames
= '\n'.join(patch_basenames
)
326 Interaction
.information(
327 N_('Patch(es) Applied'),
328 (N_('%d patch(es) applied.') + '\n\n%s')
329 % (len(self
.patches
), basenames
))
332 class Archive(ContextCommand
):
333 """"Export archives using the "git archive" command"""
335 def __init__(self
, context
, ref
, fmt
, prefix
, filename
):
336 super(Archive
, self
).__init
__(context
)
340 self
.filename
= filename
343 fp
= core
.xopen(self
.filename
, 'wb')
344 cmd
= ['git', 'archive', '--format='+self
.fmt
]
345 if self
.fmt
in ('tgz', 'tar.gz'):
348 cmd
.append('--prefix=' + self
.prefix
)
350 proc
= core
.start_command(cmd
, stdout
=fp
)
351 out
, err
= proc
.communicate()
353 status
= proc
.returncode
354 Interaction
.log_status(status
, out
or '', err
or '')
357 class Checkout(EditModel
):
358 """A command object for git-checkout.
360 'argv' is handed off directly to git.
363 def __init__(self
, context
, argv
, checkout_branch
=False):
364 super(Checkout
, self
).__init
__(context
)
366 self
.checkout_branch
= checkout_branch
367 self
.new_diff_text
= ''
368 self
.new_diff_type
= 'text'
371 super(Checkout
, self
).do()
372 status
, out
, err
= self
.git
.checkout(*self
.argv
)
373 if self
.checkout_branch
:
374 self
.model
.update_status()
376 self
.model
.update_file_status()
377 Interaction
.command(N_('Error'), 'git checkout', status
, out
, err
)
380 class BlamePaths(ContextCommand
):
381 """Blame view for paths."""
385 return N_('Blame...')
387 def __init__(self
, context
, paths
=None):
388 super(BlamePaths
, self
).__init
__(context
)
390 paths
= context
.selection
.union()
391 viewer
= utils
.shell_split(prefs
.blame_viewer(context
))
392 self
.argv
= viewer
+ list(paths
)
398 _
, details
= utils
.format_exception(e
)
399 title
= N_('Error Launching Blame Viewer')
400 msg
= (N_('Cannot exec "%s": please configure a blame viewer')
401 % ' '.join(self
.argv
))
402 Interaction
.critical(title
, message
=msg
, details
=details
)
405 class CheckoutBranch(Checkout
):
406 """Checkout a branch."""
408 def __init__(self
, context
, branch
):
410 super(CheckoutBranch
, self
).__init
__(
411 context
, args
, checkout_branch
=True)
414 class CherryPick(ContextCommand
):
415 """Cherry pick commits into the current branch."""
417 def __init__(self
, context
, commits
):
418 super(CherryPick
, self
).__init
__(context
)
419 self
.commits
= commits
422 self
.model
.cherry_pick_list(self
.commits
)
423 self
.model
.update_file_status()
426 class Revert(ContextCommand
):
427 """Cherry pick commits into the current branch."""
429 def __init__(self
, context
, oid
):
430 super(Revert
, self
).__init
__(context
)
434 self
.git
.revert(self
.oid
, no_edit
=True)
435 self
.model
.update_file_status()
438 class ResetMode(EditModel
):
439 """Reset the mode and clear the model's diff text."""
441 def __init__(self
, context
):
442 super(ResetMode
, self
).__init
__(context
)
443 self
.new_mode
= self
.model
.mode_none
444 self
.new_diff_text
= ''
445 self
.new_diff_type
= 'text'
446 self
.new_filename
= ''
449 super(ResetMode
, self
).do()
450 self
.model
.update_file_status()
453 class ResetCommand(ConfirmAction
):
454 """Reset state using the "git reset" command"""
456 def __init__(self
, context
, ref
):
457 super(ResetCommand
, self
).__init
__(context
)
466 def error_message(self
):
470 self
.model
.update_file_status()
473 raise NotImplementedError('confirm() must be overridden')
476 raise NotImplementedError('reset() must be overridden')
479 class ResetBranchHead(ResetCommand
):
482 title
= N_('Reset Branch')
483 question
= N_('Point the current branch head to a new commit?')
484 info
= N_('The branch will be reset using "git reset --mixed %s"')
485 ok_text
= N_('Reset Branch')
486 info
= info
% self
.ref
487 return Interaction
.confirm(title
, question
, info
, ok_text
)
490 return self
.git
.reset(self
.ref
, '--', mixed
=True)
493 class ResetWorktree(ResetCommand
):
496 title
= N_('Reset Worktree')
497 question
= N_('Reset worktree?')
498 info
= N_('The worktree will be reset using "git reset --keep %s"')
499 ok_text
= N_('Reset Worktree')
500 info
= info
% self
.ref
501 return Interaction
.confirm(title
, question
, info
, ok_text
)
504 return self
.git
.reset(self
.ref
, '--', keep
=True)
507 class ResetMerge(ResetCommand
):
510 title
= N_('Reset Merge')
511 question
= N_('Reset merge?')
512 info
= N_('The branch will be reset using "git reset --merge %s"')
513 ok_text
= N_('Reset Merge')
514 info
= info
% self
.ref
515 return Interaction
.confirm(title
, question
, info
, ok_text
)
518 return self
.git
.reset(self
.ref
, '--', merge
=True)
521 class ResetSoft(ResetCommand
):
524 title
= N_('Reset Soft')
525 question
= N_('Reset soft?')
526 info
= N_('The branch will be reset using "git reset --soft %s"')
527 ok_text
= N_('Reset Soft')
528 info
= info
% self
.ref
529 return Interaction
.confirm(title
, question
, info
, ok_text
)
532 return self
.git
.reset(self
.ref
, '--', soft
=True)
535 class ResetHard(ResetCommand
):
538 title
= N_('Reset Hard')
539 question
= N_('Reset hard?')
540 info
= N_('The branch will be reset using "git reset --hard %s"')
541 ok_text
= N_('Reset Hard')
542 info
= info
% self
.ref
543 return Interaction
.confirm(title
, question
, info
, ok_text
)
546 return self
.git
.reset(self
.ref
, '--', hard
=True)
549 class Commit(ResetMode
):
550 """Attempt to create a new commit."""
552 def __init__(self
, context
, amend
, msg
, sign
, no_verify
=False):
553 super(Commit
, self
).__init
__(context
)
557 self
.no_verify
= no_verify
558 self
.old_commitmsg
= self
.model
.commitmsg
559 self
.new_commitmsg
= ''
562 # Create the commit message file
563 context
= self
.context
564 comment_char
= prefs
.comment_char(context
)
565 msg
= self
.strip_comments(self
.msg
, comment_char
=comment_char
)
566 tmp_file
= utils
.tmp_filename('commit-message')
568 core
.write(tmp_file
, msg
)
570 status
, out
, err
= self
.git
.commit(
571 F
=tmp_file
, v
=True, gpg_sign
=self
.sign
,
572 amend
=self
.amend
, no_verify
=self
.no_verify
)
574 core
.unlink(tmp_file
)
576 super(Commit
, self
).do()
577 if context
.cfg
.get(prefs
.AUTOTEMPLATE
):
578 template_loader
= LoadCommitMessageFromTemplate(context
)
581 self
.model
.set_commitmsg(self
.new_commitmsg
)
583 title
= N_('Commit failed')
584 Interaction
.command(title
, 'git commit', status
, out
, err
)
586 return status
, out
, err
589 def strip_comments(msg
, comment_char
='#'):
591 message_lines
= [line
for line
in msg
.split('\n')
592 if not line
.startswith(comment_char
)]
593 msg
= '\n'.join(message_lines
)
594 if not msg
.endswith('\n'):
600 class CycleReferenceSort(ContextCommand
):
601 """Choose the next reference sort type"""
603 self
.model
.cycle_ref_sort()
606 class Ignore(ContextCommand
):
607 """Add files to an exclusion file"""
609 def __init__(self
, context
, filenames
, local
=False):
610 super(Ignore
, self
).__init
__(context
)
611 self
.filenames
= list(filenames
)
615 if not self
.filenames
:
617 new_additions
= '\n'.join(self
.filenames
) + '\n'
618 for_status
= new_additions
620 filename
= os
.path
.join('.git', 'info', 'exclude')
622 filename
= '.gitignore'
623 if core
.exists(filename
):
624 current_list
= core
.read(filename
)
625 new_additions
= current_list
.rstrip() + '\n' + new_additions
626 core
.write(filename
, new_additions
)
627 Interaction
.log_status(0, 'Added to %s:\n%s' % (filename
, for_status
),
629 self
.model
.update_file_status()
632 def file_summary(files
):
633 txt
= core
.list2cmdline(files
)
635 txt
= txt
[:768].rstrip() + '...'
636 wrap
= textwrap
.TextWrapper()
637 return '\n'.join(wrap
.wrap(txt
))
640 class RemoteCommand(ConfirmAction
):
642 def __init__(self
, context
, remote
):
643 super(RemoteCommand
, self
).__init
__(context
)
648 self
.model
.update_remotes()
651 class RemoteAdd(RemoteCommand
):
653 def __init__(self
, context
, remote
, url
):
654 super(RemoteAdd
, self
).__init
__(context
, remote
)
658 return self
.git
.remote('add', self
.remote
, self
.url
)
660 def error_message(self
):
661 return N_('Error creating remote "%s"') % self
.remote
664 return 'git remote add "%s" "%s"' % (self
.remote
, self
.url
)
667 class RemoteRemove(RemoteCommand
):
670 title
= N_('Delete Remote')
671 question
= N_('Delete remote?')
672 info
= N_('Delete remote "%s"') % self
.remote
673 ok_text
= N_('Delete')
674 return Interaction
.confirm(title
, question
, info
, ok_text
)
677 return self
.git
.remote('rm', self
.remote
)
679 def error_message(self
):
680 return N_('Error deleting remote "%s"') % self
.remote
683 return 'git remote rm "%s"' % self
.remote
686 class RemoteRename(RemoteCommand
):
688 def __init__(self
, context
, remote
, new_name
):
689 super(RemoteRename
, self
).__init
__(context
, remote
)
690 self
.new_name
= new_name
693 title
= N_('Rename Remote')
694 text
= (N_('Rename remote "%(current)s" to "%(new)s"?') %
695 dict(current
=self
.remote
, new
=self
.new_name
))
698 return Interaction
.confirm(title
, text
, info_text
, ok_text
)
701 return self
.git
.remote('rename', self
.remote
, self
.new_name
)
703 def error_message(self
):
704 return (N_('Error renaming "%(name)s" to "%(new_name)s"')
705 % dict(name
=self
.remote
, new_name
=self
.new_name
))
708 return 'git remote rename "%s" "%s"' % (self
.remote
, self
.new_name
)
711 class RemoteSetURL(RemoteCommand
):
713 def __init__(self
, context
, remote
, url
):
714 super(RemoteSetURL
, self
).__init
__(context
, remote
)
718 return self
.git
.remote('set-url', self
.remote
, self
.url
)
720 def error_message(self
):
721 return (N_('Unable to set URL for "%(name)s" to "%(url)s"')
722 % dict(name
=self
.remote
, url
=self
.url
))
725 return 'git remote set-url "%s" "%s"' % (self
.remote
, self
.url
)
728 class RemoteEdit(ContextCommand
):
729 """Combine RemoteRename and RemoteSetURL"""
731 def __init__(self
, context
, old_name
, remote
, url
):
732 super(RemoteEdit
, self
).__init
__(context
)
733 self
.rename
= RemoteRename(context
, old_name
, remote
)
734 self
.set_url
= RemoteSetURL(context
, remote
, url
)
737 result
= self
.rename
.do()
741 result
= self
.set_url
.do()
743 return name_ok
, url_ok
746 class RemoveFromSettings(ConfirmAction
):
748 def __init__(self
, context
, settings
, repo
, entry
, icon
=None):
749 super(RemoveFromSettings
, self
).__init
__(context
)
750 self
.settings
= settings
759 class RemoveBookmark(RemoveFromSettings
):
763 title
= msg
= N_('Delete Bookmark?')
764 info
= N_('%s will be removed from your bookmarks.') % entry
765 ok_text
= N_('Delete Bookmark')
766 return Interaction
.confirm(title
, msg
, info
, ok_text
, icon
=self
.icon
)
769 self
.settings
.remove_bookmark(self
.repo
, self
.entry
)
773 class RemoveRecent(RemoveFromSettings
):
777 title
= msg
= N_('Remove %s from the recent list?') % repo
778 info
= N_('%s will be removed from your recent repositories.') % repo
779 ok_text
= N_('Remove')
780 return Interaction
.confirm(title
, msg
, info
, ok_text
, icon
=self
.icon
)
783 self
.settings
.remove_recent(self
.repo
)
787 class RemoveFiles(ContextCommand
):
790 def __init__(self
, context
, remover
, filenames
):
791 super(RemoveFiles
, self
).__init
__(context
)
794 self
.remover
= remover
795 self
.filenames
= filenames
796 # We could git-hash-object stuff and provide undo-ability
800 files
= self
.filenames
806 remove
= self
.remover
807 for filename
in files
:
813 bad_filenames
.append(filename
)
816 Interaction
.information(
818 N_('Deleting "%s" failed') % file_summary(bad_filenames
))
821 self
.model
.update_file_status()
824 class Delete(RemoveFiles
):
827 def __init__(self
, context
, filenames
):
828 super(Delete
, self
).__init
__(context
, os
.remove
, filenames
)
831 files
= self
.filenames
835 title
= N_('Delete Files?')
836 msg
= N_('The following files will be deleted:') + '\n\n'
837 msg
+= file_summary(files
)
838 info_txt
= N_('Delete %d file(s)?') % len(files
)
839 ok_txt
= N_('Delete Files')
841 if Interaction
.confirm(title
, msg
, info_txt
, ok_txt
,
842 default
=True, icon
=icons
.remove()):
843 super(Delete
, self
).do()
846 class MoveToTrash(RemoveFiles
):
847 """Move files to the trash using send2trash"""
849 AVAILABLE
= send2trash
is not None
851 def __init__(self
, context
, filenames
):
852 super(MoveToTrash
, self
).__init
__(context
, send2trash
, filenames
)
855 class DeleteBranch(ConfirmAction
):
856 """Delete a git branch."""
858 def __init__(self
, context
, branch
):
859 super(DeleteBranch
, self
).__init
__(context
)
863 title
= N_('Delete Branch')
864 question
= N_('Delete branch "%s"?') % self
.branch
865 info
= N_('The branch will be no longer available.')
866 ok_txt
= N_('Delete Branch')
867 return Interaction
.confirm(title
, question
, info
, ok_txt
,
868 default
=True, icon
=icons
.discard())
871 return self
.model
.delete_branch(self
.branch
)
873 def error_message(self
):
874 return N_('Error deleting branch "%s"' % self
.branch
)
877 command
= 'git branch -D %s'
878 return command
% self
.branch
881 class Rename(ContextCommand
):
882 """Rename a set of paths."""
884 def __init__(self
, context
, paths
):
885 super(Rename
, self
).__init
__(context
)
889 msg
= N_('Untracking: %s') % (', '.join(self
.paths
))
892 for path
in self
.paths
:
893 ok
= self
.rename(path
)
897 self
.model
.update_status()
899 def rename(self
, path
):
901 title
= N_('Rename "%s"') % path
903 if os
.path
.isdir(path
):
904 base_path
= os
.path
.dirname(path
)
907 new_path
= Interaction
.save_as(base_path
, title
)
911 status
, out
, err
= git
.mv(path
, new_path
, force
=True, verbose
=True)
912 Interaction
.command(N_('Error'), 'git mv', status
, out
, err
)
916 class RenameBranch(ContextCommand
):
917 """Rename a git branch."""
919 def __init__(self
, context
, branch
, new_branch
):
920 super(RenameBranch
, self
).__init
__(context
)
922 self
.new_branch
= new_branch
926 new_branch
= self
.new_branch
927 status
, out
, err
= self
.model
.rename_branch(branch
, new_branch
)
928 Interaction
.log_status(status
, out
, err
)
931 class DeleteRemoteBranch(DeleteBranch
):
932 """Delete a remote git branch."""
934 def __init__(self
, context
, remote
, branch
):
935 super(DeleteRemoteBranch
, self
).__init
__(context
, branch
)
939 return self
.git
.push(self
.remote
, self
.branch
, delete
=True)
942 self
.model
.update_status()
943 Interaction
.information(
944 N_('Remote Branch Deleted'),
945 N_('"%(branch)s" has been deleted from "%(remote)s".')
946 % dict(branch
=self
.branch
, remote
=self
.remote
))
948 def error_message(self
):
949 return N_('Error Deleting Remote Branch')
952 command
= 'git push --delete %s %s'
953 return command
% (self
.remote
, self
.branch
)
956 def get_mode(model
, staged
, modified
, unmerged
, untracked
):
958 mode
= model
.mode_index
959 elif modified
or unmerged
:
960 mode
= model
.mode_worktree
962 mode
= model
.mode_untracked
968 class DiffImage(EditModel
):
970 def __init__(self
, context
, filename
,
971 deleted
, staged
, modified
, unmerged
, untracked
):
972 super(DiffImage
, self
).__init
__(context
)
974 self
.new_filename
= filename
975 self
.new_diff_text
= ''
976 self
.new_diff_type
= 'image'
977 self
.new_mode
= get_mode(
978 self
.model
, staged
, modified
, unmerged
, untracked
)
980 self
.modified
= modified
981 self
.unmerged
= unmerged
982 self
.untracked
= untracked
983 self
.deleted
= deleted
984 self
.annex
= self
.cfg
.is_annex()
987 filename
= self
.new_filename
990 images
= self
.staged_images()
992 images
= self
.modified_images()
994 images
= self
.unmerged_images()
996 images
= [(filename
, False)]
1000 self
.model
.set_images(images
)
1001 super(DiffImage
, self
).do()
1003 def staged_images(self
):
1004 context
= self
.context
1006 head
= self
.model
.head
1007 filename
= self
.new_filename
1011 index
= git
.diff_index(head
, '--', filename
, cached
=True)[STDOUT
]
1014 # :100644 100644 fabadb8... 4866510... M describe.c
1015 parts
= index
.split(' ')
1020 if old_oid
!= MISSING_BLOB_OID
:
1021 # First, check if we can get a pre-image from git-annex
1024 annex_image
= gitcmds
.annex_path(context
, head
, filename
)
1026 images
.append((annex_image
, False)) # git annex HEAD
1028 image
= gitcmds
.write_blob_path(
1029 context
, head
, old_oid
, filename
)
1031 images
.append((image
, True))
1033 if new_oid
!= MISSING_BLOB_OID
:
1034 found_in_annex
= False
1035 if annex
and core
.islink(filename
):
1036 status
, out
, _
= git
.annex('status', '--', filename
)
1038 details
= out
.split(' ')
1039 if details
and details
[0] == 'A': # newly added file
1040 images
.append((filename
, False))
1041 found_in_annex
= True
1043 if not found_in_annex
:
1044 image
= gitcmds
.write_blob(context
, new_oid
, filename
)
1046 images
.append((image
, True))
1050 def unmerged_images(self
):
1051 context
= self
.context
1053 head
= self
.model
.head
1054 filename
= self
.new_filename
1057 candidate_merge_heads
= ('HEAD', 'CHERRY_HEAD', 'MERGE_HEAD')
1059 merge_head
for merge_head
in candidate_merge_heads
1060 if core
.exists(git
.git_path(merge_head
))]
1062 if annex
: # Attempt to find files in git-annex
1064 for merge_head
in merge_heads
:
1065 image
= gitcmds
.annex_path(context
, merge_head
, filename
)
1067 annex_images
.append((image
, False))
1069 annex_images
.append((filename
, False))
1072 # DIFF FORMAT FOR MERGES
1073 # "git-diff-tree", "git-diff-files" and "git-diff --raw"
1074 # can take -c or --cc option to generate diff output also
1075 # for merge commits. The output differs from the format
1076 # described above in the following way:
1078 # 1. there is a colon for each parent
1079 # 2. there are more "src" modes and "src" sha1
1080 # 3. status is concatenated status characters for each parent
1081 # 4. no optional "score" number
1082 # 5. single path, only for "dst"
1084 # ::100644 100644 100644 fabadb8... cc95eb0... 4866510... \
1087 index
= git
.diff_index(head
, '--', filename
,
1088 cached
=True, cc
=True)[STDOUT
]
1090 parts
= index
.split(' ')
1092 first_mode
= parts
[0]
1093 num_parents
= first_mode
.count(':')
1094 # colon for each parent, but for the index, the "parents"
1095 # are really entries in stages 1,2,3 (head, base, remote)
1096 # remote, base, head
1097 for i
in range(num_parents
):
1098 offset
= num_parents
+ i
+ 1
1101 merge_head
= merge_heads
[i
]
1104 if oid
!= MISSING_BLOB_OID
:
1105 image
= gitcmds
.write_blob_path(
1106 context
, merge_head
, oid
, filename
)
1108 images
.append((image
, True))
1110 images
.append((filename
, False))
1113 def modified_images(self
):
1114 context
= self
.context
1116 head
= self
.model
.head
1117 filename
= self
.new_filename
1122 if annex
: # Check for a pre-image from git-annex
1123 annex_image
= gitcmds
.annex_path(context
, head
, filename
)
1125 images
.append((annex_image
, False)) # git annex HEAD
1127 worktree
= git
.diff_files('--', filename
)[STDOUT
]
1128 parts
= worktree
.split(' ')
1131 if oid
!= MISSING_BLOB_OID
:
1132 image
= gitcmds
.write_blob_path(
1133 context
, head
, oid
, filename
)
1135 images
.append((image
, True)) # HEAD
1137 images
.append((filename
, False)) # worktree
1141 class Diff(EditModel
):
1142 """Perform a diff and set the model's current text."""
1144 def __init__(self
, context
, filename
, cached
=False, deleted
=False):
1145 super(Diff
, self
).__init
__(context
)
1148 opts
['ref'] = self
.model
.head
1149 self
.new_filename
= filename
1150 self
.new_mode
= self
.model
.mode_worktree
1151 self
.new_diff_text
= gitcmds
.diff_helper(
1152 self
.context
, filename
=filename
, cached
=cached
,
1153 deleted
=deleted
, **opts
)
1154 self
.new_diff_type
= 'text'
1157 class Diffstat(EditModel
):
1158 """Perform a diffstat and set the model's diff text."""
1160 def __init__(self
, context
):
1161 super(Diffstat
, self
).__init
__(context
)
1163 diff_context
= cfg
.get('diff.context', 3)
1164 diff
= self
.git
.diff(
1165 self
.model
.head
, unified
=diff_context
, no_ext_diff
=True,
1166 no_color
=True, M
=True, stat
=True)[STDOUT
]
1167 self
.new_diff_text
= diff
1168 self
.new_diff_type
= 'text'
1169 self
.new_mode
= self
.model
.mode_diffstat
1172 class DiffStaged(Diff
):
1173 """Perform a staged diff on a file."""
1175 def __init__(self
, context
, filename
, deleted
=None):
1176 super(DiffStaged
, self
).__init
__(
1177 context
, filename
, cached
=True, deleted
=deleted
)
1178 self
.new_mode
= self
.model
.mode_index
1181 class DiffStagedSummary(EditModel
):
1183 def __init__(self
, context
):
1184 super(DiffStagedSummary
, self
).__init
__(context
)
1185 diff
= self
.git
.diff(
1186 self
.model
.head
, cached
=True, no_color
=True,
1187 no_ext_diff
=True, patch_with_stat
=True, M
=True)[STDOUT
]
1188 self
.new_diff_text
= diff
1189 self
.new_diff_type
= 'text'
1190 self
.new_mode
= self
.model
.mode_index
1193 class Difftool(ContextCommand
):
1194 """Run git-difftool limited by path."""
1196 def __init__(self
, context
, staged
, filenames
):
1197 super(Difftool
, self
).__init
__(context
)
1198 self
.staged
= staged
1199 self
.filenames
= filenames
1202 difftool_launch_with_head(
1203 self
.context
, self
.filenames
, self
.staged
, self
.model
.head
)
1206 class Edit(ContextCommand
):
1207 """Edit a file using the configured gui.editor."""
1211 return N_('Launch Editor')
1213 def __init__(self
, context
, filenames
,
1214 line_number
=None, background_editor
=False):
1215 super(Edit
, self
).__init
__(context
)
1216 self
.filenames
= filenames
1217 self
.line_number
= line_number
1218 self
.background_editor
= background_editor
1221 context
= self
.context
1222 if not self
.filenames
:
1224 filename
= self
.filenames
[0]
1225 if not core
.exists(filename
):
1227 if self
.background_editor
:
1228 editor
= prefs
.background_editor(context
)
1230 editor
= prefs
.editor(context
)
1233 if self
.line_number
is None:
1234 opts
= self
.filenames
1236 # Single-file w/ line-numbers (likely from grep)
1238 '*vim*': [filename
, '+%s' % self
.line_number
],
1239 '*emacs*': ['+%s' % self
.line_number
, filename
],
1240 '*textpad*': ['%s(%s,0)' % (filename
, self
.line_number
)],
1241 '*notepad++*': ['-n%s' % self
.line_number
, filename
],
1242 '*subl*': ['%s:%s' % (filename
, self
.line_number
)],
1245 opts
= self
.filenames
1246 for pattern
, opt
in editor_opts
.items():
1247 if fnmatch(editor
, pattern
):
1252 core
.fork(utils
.shell_split(editor
) + opts
)
1253 except (OSError, ValueError) as e
:
1254 message
= (N_('Cannot exec "%s": please configure your editor')
1256 _
, details
= utils
.format_exception(e
)
1257 Interaction
.critical(N_('Error Editing File'), message
, details
)
1260 class FormatPatch(ContextCommand
):
1261 """Output a patch series given all revisions and a selected subset."""
1263 def __init__(self
, context
, to_export
, revs
, output
='patches'):
1264 super(FormatPatch
, self
).__init
__(context
)
1265 self
.to_export
= list(to_export
)
1266 self
.revs
= list(revs
)
1267 self
.output
= output
1270 context
= self
.context
1271 status
, out
, err
= gitcmds
.format_patchsets(
1272 context
, self
.to_export
, self
.revs
, self
.output
)
1273 Interaction
.log_status(status
, out
, err
)
1276 class LaunchDifftool(ContextCommand
):
1280 return N_('Launch Diff Tool')
1283 s
= self
.selection
.selection()
1286 if utils
.is_win32():
1287 core
.fork(['git', 'mergetool', '--no-prompt', '--'] + paths
)
1290 cmd
= cfg
.terminal()
1291 argv
= utils
.shell_split(cmd
)
1293 terminal
= os
.path
.basename(argv
[0])
1294 shellquote_terms
= set(['xfce4-terminal'])
1295 shellquote_default
= terminal
in shellquote_terms
1297 mergetool
= ['git', 'mergetool', '--no-prompt', '--']
1298 mergetool
.extend(paths
)
1299 needs_shellquote
= cfg
.get(
1300 'cola.terminalshellquote', shellquote_default
)
1302 if needs_shellquote
:
1303 argv
.append(core
.list2cmdline(mergetool
))
1305 argv
.extend(mergetool
)
1309 difftool_run(self
.context
)
1312 class LaunchTerminal(ContextCommand
):
1316 return N_('Launch Terminal')
1319 def is_available(context
):
1320 return context
.cfg
.terminal() is not None
1322 def __init__(self
, context
, path
):
1323 super(LaunchTerminal
, self
).__init
__(context
)
1327 cmd
= self
.context
.cfg
.terminal()
1330 if utils
.is_win32():
1331 argv
= ['start', '', cmd
, '--login']
1334 argv
= utils
.shell_split(cmd
)
1335 argv
.append(os
.getenv('SHELL', '/bin/sh'))
1337 core
.fork(argv
, cwd
=self
.path
, shell
=shell
)
1340 class LaunchEditor(Edit
):
1344 return N_('Launch Editor')
1346 def __init__(self
, context
):
1347 s
= context
.selection
.selection()
1348 filenames
= s
.staged
+ s
.unmerged
+ s
.modified
+ s
.untracked
1349 super(LaunchEditor
, self
).__init
__(
1350 context
, filenames
, background_editor
=True)
1353 class LaunchEditorAtLine(LaunchEditor
):
1354 """Launch an editor at the specified line"""
1356 def __init__(self
, context
):
1357 super(LaunchEditorAtLine
, self
).__init
__(context
)
1358 self
.line_number
= context
.selection
.line_number
1361 class LoadCommitMessageFromFile(ContextCommand
):
1362 """Loads a commit message from a path."""
1365 def __init__(self
, context
, path
):
1366 super(LoadCommitMessageFromFile
, self
).__init
__(context
)
1368 self
.old_commitmsg
= self
.model
.commitmsg
1369 self
.old_directory
= self
.model
.directory
1372 path
= os
.path
.expanduser(self
.path
)
1373 if not path
or not core
.isfile(path
):
1374 raise UsageError(N_('Error: Cannot find commit template'),
1375 N_('%s: No such file or directory.') % path
)
1376 self
.model
.set_directory(os
.path
.dirname(path
))
1377 self
.model
.set_commitmsg(core
.read(path
))
1380 self
.model
.set_commitmsg(self
.old_commitmsg
)
1381 self
.model
.set_directory(self
.old_directory
)
1384 class LoadCommitMessageFromTemplate(LoadCommitMessageFromFile
):
1385 """Loads the commit message template specified by commit.template."""
1387 def __init__(self
, context
):
1389 template
= cfg
.get('commit.template')
1390 super(LoadCommitMessageFromTemplate
, self
).__init
__(context
, template
)
1393 if self
.path
is None:
1395 N_('Error: Unconfigured commit template'),
1396 N_('A commit template has not been configured.\n'
1397 'Use "git config" to define "commit.template"\n'
1398 'so that it points to a commit template.'))
1399 return LoadCommitMessageFromFile
.do(self
)
1402 class LoadCommitMessageFromOID(ContextCommand
):
1403 """Load a previous commit message"""
1406 def __init__(self
, context
, oid
, prefix
=''):
1407 super(LoadCommitMessageFromOID
, self
).__init
__(context
)
1409 self
.old_commitmsg
= self
.model
.commitmsg
1410 self
.new_commitmsg
= prefix
+ gitcmds
.prev_commitmsg(context
, oid
)
1413 self
.model
.set_commitmsg(self
.new_commitmsg
)
1416 self
.model
.set_commitmsg(self
.old_commitmsg
)
1419 class PrepareCommitMessageHook(ContextCommand
):
1420 """Use the cola-prepare-commit-msg hook to prepare the commit message
1424 def __init__(self
, context
):
1425 super(PrepareCommitMessageHook
, self
).__init
__(context
)
1426 self
.old_commitmsg
= self
.model
.commitmsg
1428 def get_message(self
):
1430 title
= N_('Error running prepare-commitmsg hook')
1431 hook
= gitcmds
.prepare_commit_message_hook(self
.context
)
1433 if os
.path
.exists(hook
):
1434 filename
= self
.model
.save_commitmsg()
1435 status
, out
, err
= core
.run_command([hook
, filename
])
1438 result
= core
.read(filename
)
1440 result
= self
.old_commitmsg
1441 Interaction
.command_error(title
, hook
, status
, out
, err
)
1443 message
= N_('A hook must be provided at "%s"') % hook
1444 Interaction
.critical(title
, message
=message
)
1445 result
= self
.old_commitmsg
1450 msg
= self
.get_message()
1451 self
.model
.set_commitmsg(msg
)
1454 self
.model
.set_commitmsg(self
.old_commitmsg
)
1457 class LoadFixupMessage(LoadCommitMessageFromOID
):
1458 """Load a fixup message"""
1460 def __init__(self
, context
, oid
):
1461 super(LoadFixupMessage
, self
).__init
__(context
, oid
, prefix
='fixup! ')
1462 if self
.new_commitmsg
:
1463 self
.new_commitmsg
= self
.new_commitmsg
.splitlines()[0]
1466 class Merge(ContextCommand
):
1469 def __init__(self
, context
, revision
, no_commit
, squash
, no_ff
, sign
):
1470 super(Merge
, self
).__init
__(context
)
1471 self
.revision
= revision
1473 self
.no_commit
= no_commit
1474 self
.squash
= squash
1478 squash
= self
.squash
1479 revision
= self
.revision
1481 no_commit
= self
.no_commit
1484 status
, out
, err
= self
.git
.merge(
1485 revision
, gpg_sign
=sign
, no_ff
=no_ff
,
1486 no_commit
=no_commit
, squash
=squash
)
1487 self
.model
.update_status()
1488 title
= N_('Merge failed. Conflict resolution is required.')
1489 Interaction
.command(title
, 'git merge', status
, out
, err
)
1491 return status
, out
, err
1494 class OpenDefaultApp(ContextCommand
):
1495 """Open a file using the OS default."""
1499 return N_('Open Using Default Application')
1501 def __init__(self
, context
, filenames
):
1502 super(OpenDefaultApp
, self
).__init
__(context
)
1503 if utils
.is_darwin():
1506 launcher
= 'xdg-open'
1507 self
.launcher
= launcher
1508 self
.filenames
= filenames
1511 if not self
.filenames
:
1513 core
.fork([self
.launcher
] + self
.filenames
)
1516 class OpenParentDir(OpenDefaultApp
):
1517 """Open parent directories using the OS default."""
1521 return N_('Open Parent Directory')
1523 def __init__(self
, context
, filenames
):
1524 OpenDefaultApp
.__init
__(self
, context
, filenames
)
1527 if not self
.filenames
:
1529 dirnames
= list(set([os
.path
.dirname(x
) for x
in self
.filenames
]))
1530 # os.path.dirname() can return an empty string so we fallback to
1531 # the current directory
1532 dirs
= [(dirname
or core
.getcwd()) for dirname
in dirnames
]
1533 core
.fork([self
.launcher
] + dirs
)
1536 class OpenNewRepo(ContextCommand
):
1537 """Launches git-cola on a repo."""
1539 def __init__(self
, context
, repo_path
):
1540 super(OpenNewRepo
, self
).__init
__(context
)
1541 self
.repo_path
= repo_path
1544 self
.model
.set_directory(self
.repo_path
)
1545 core
.fork([sys
.executable
, sys
.argv
[0], '--repo', self
.repo_path
])
1548 class OpenRepo(EditModel
):
1550 def __init__(self
, context
, repo_path
):
1551 super(OpenRepo
, self
).__init
__(context
)
1552 self
.repo_path
= repo_path
1553 self
.new_mode
= self
.model
.mode_none
1554 self
.new_diff_text
= ''
1555 self
.new_diff_type
= 'text'
1556 self
.new_commitmsg
= ''
1557 self
.new_filename
= ''
1560 old_repo
= self
.git
.getcwd()
1561 if self
.model
.set_worktree(self
.repo_path
):
1562 self
.fsmonitor
.stop()
1563 self
.fsmonitor
.start()
1564 self
.model
.update_status()
1565 # Check if template should be loaded
1566 if self
.context
.cfg
.get(prefs
.AUTOTEMPLATE
):
1567 template_loader
= LoadCommitMessageFromTemplate(self
.context
)
1568 template_loader
.do()
1570 self
.model
.set_commitmsg(self
.new_commitmsg
)
1571 settings
= Settings()
1573 settings
.add_recent(self
.repo_path
, prefs
.maxrecent(self
.context
))
1575 super(OpenRepo
, self
).do()
1577 self
.model
.set_worktree(old_repo
)
1580 class OpenParentRepo(OpenRepo
):
1582 def __init__(self
, context
):
1584 if version
.check_git(context
, 'show-superproject-working-tree'):
1585 status
, out
, _
= context
.git
.rev_parse(
1586 show_superproject_working_tree
=True)
1590 path
= os
.path
.dirname(core
.getcwd())
1591 super(OpenParentRepo
, self
).__init
__(context
, path
)
1594 class Clone(ContextCommand
):
1595 """Clones a repository and optionally spawns a new cola session."""
1597 def __init__(self
, context
, url
, new_directory
,
1598 submodules
=False, shallow
=False, spawn
=True):
1599 super(Clone
, self
).__init
__(context
)
1601 self
.new_directory
= new_directory
1602 self
.submodules
= submodules
1603 self
.shallow
= shallow
1613 recurse_submodules
= self
.submodules
1614 shallow_submodules
= self
.submodules
and self
.shallow
1616 status
, out
, err
= self
.git
.clone(
1617 self
.url
, self
.new_directory
,
1618 recurse_submodules
=recurse_submodules
,
1619 shallow_submodules
=shallow_submodules
,
1622 self
.status
= status
1625 if status
== 0 and self
.spawn
:
1626 executable
= sys
.executable
1627 core
.fork([executable
, sys
.argv
[0], '--repo', self
.new_directory
])
1631 class NewBareRepo(ContextCommand
):
1632 """Create a new shared bare repository"""
1634 def __init__(self
, context
, path
):
1635 super(NewBareRepo
, self
).__init
__(context
)
1640 status
, out
, err
= self
.git
.init(path
, bare
=True, shared
=True)
1641 Interaction
.command(
1642 N_('Error'), 'git init --bare --shared "%s"' % path
,
1647 def unix_path(path
, is_win32
=utils
.is_win32
):
1648 """Git for Windows requires unix paths, so force them here
1651 path
= path
.replace('\\', '/')
1654 if second
== ':': # sanity check, this better be a Windows-style path
1655 path
= '/' + first
+ path
[2:]
1660 def sequence_editor():
1661 """Return a GIT_SEQUENCE_EDITOR environment value that enables git-xbase"""
1662 xbase
= unix_path(resources
.share('bin', 'git-xbase'))
1663 editor
= core
.list2cmdline([unix_path(sys
.executable
), xbase
])
1667 class GitXBaseContext(object):
1669 def __init__(self
, context
, **kwargs
):
1671 'GIT_EDITOR': prefs
.editor(context
),
1672 'GIT_SEQUENCE_EDITOR': sequence_editor(),
1673 'GIT_XBASE_CANCEL_ACTION': 'save',
1675 self
.env
.update(kwargs
)
1677 def __enter__(self
):
1678 for var
, value
in self
.env
.items():
1679 compat
.setenv(var
, value
)
1682 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
1683 for var
in self
.env
:
1684 compat
.unsetenv(var
)
1687 class Rebase(ContextCommand
):
1689 def __init__(self
, context
, upstream
=None, branch
=None, **kwargs
):
1690 """Start an interactive rebase session
1692 :param upstream: upstream branch
1693 :param branch: optional branch to checkout
1694 :param kwargs: forwarded directly to `git.rebase()`
1697 super(Rebase
, self
).__init
__(context
)
1699 self
.upstream
= upstream
1700 self
.branch
= branch
1701 self
.kwargs
= kwargs
1703 def prepare_arguments(self
, upstream
):
1707 # Rebase actions must be the only option specified
1708 for action
in ('continue', 'abort', 'skip', 'edit_todo'):
1709 if self
.kwargs
.get(action
, False):
1710 kwargs
[action
] = self
.kwargs
[action
]
1713 kwargs
['interactive'] = True
1714 kwargs
['autosquash'] = self
.kwargs
.get('autosquash', True)
1715 kwargs
.update(self
.kwargs
)
1718 args
.append(upstream
)
1720 args
.append(self
.branch
)
1725 (status
, out
, err
) = (1, '', '')
1726 context
= self
.context
1730 if not cfg
.get('rebase.autostash', False):
1731 if model
.staged
or model
.unmerged
or model
.modified
:
1732 Interaction
.information(
1733 N_('Unable to rebase'),
1734 N_('You cannot rebase with uncommitted changes.'))
1735 return status
, out
, err
1737 upstream
= self
.upstream
or Interaction
.choose_ref(
1738 context
, N_('Select New Upstream'), N_('Interactive Rebase'),
1739 default
='@{upstream}')
1741 return status
, out
, err
1743 self
.model
.is_rebasing
= True
1744 self
.model
.emit_updated()
1746 args
, kwargs
= self
.prepare_arguments(upstream
)
1747 upstream_title
= upstream
or '@{upstream}'
1748 with
GitXBaseContext(
1750 GIT_XBASE_TITLE
=N_('Rebase onto %s') % upstream_title
,
1751 GIT_XBASE_ACTION
=N_('Rebase')
1753 # TODO this blocks the user interface window for the duration
1754 # of git-xbase's invocation. We would need to implement
1755 # signals for QProcess and continue running the main thread.
1756 # alternatively we could hide the main window while rebasing.
1757 # that doesn't require as much effort.
1758 status
, out
, err
= self
.git
.rebase(
1759 *args
, _no_win32_startupinfo
=True, **kwargs
)
1760 self
.model
.update_status()
1761 if err
.strip() != 'Nothing to do':
1762 title
= N_('Rebase stopped')
1763 Interaction
.command(title
, 'git rebase', status
, out
, err
)
1764 return status
, out
, err
1767 class RebaseEditTodo(ContextCommand
):
1770 (status
, out
, err
) = (1, '', '')
1771 with
GitXBaseContext(
1773 GIT_XBASE_TITLE
=N_('Edit Rebase'),
1774 GIT_XBASE_ACTION
=N_('Save')
1776 status
, out
, err
= self
.git
.rebase(edit_todo
=True)
1777 Interaction
.log_status(status
, out
, err
)
1778 self
.model
.update_status()
1779 return status
, out
, err
1782 class RebaseContinue(ContextCommand
):
1785 (status
, out
, err
) = (1, '', '')
1786 with
GitXBaseContext(
1788 GIT_XBASE_TITLE
=N_('Rebase'),
1789 GIT_XBASE_ACTION
=N_('Rebase')
1791 status
, out
, err
= self
.git
.rebase('--continue')
1792 Interaction
.log_status(status
, out
, err
)
1793 self
.model
.update_status()
1794 return status
, out
, err
1797 class RebaseSkip(ContextCommand
):
1800 (status
, out
, err
) = (1, '', '')
1801 with
GitXBaseContext(
1803 GIT_XBASE_TITLE
=N_('Rebase'),
1804 GIT_XBASE_ACTION
=N_('Rebase')
1806 status
, out
, err
= self
.git
.rebase(skip
=True)
1807 Interaction
.log_status(status
, out
, err
)
1808 self
.model
.update_status()
1809 return status
, out
, err
1812 class RebaseAbort(ContextCommand
):
1815 status
, out
, err
= self
.git
.rebase(abort
=True)
1816 Interaction
.log_status(status
, out
, err
)
1817 self
.model
.update_status()
1820 class Rescan(ContextCommand
):
1821 """Rescan for changes"""
1824 self
.model
.update_status()
1827 class Refresh(ContextCommand
):
1828 """Update refs, refresh the index, and update config"""
1832 return N_('Refresh')
1835 self
.model
.update_status(update_index
=True)
1837 self
.fsmonitor
.refresh()
1840 class RefreshConfig(ContextCommand
):
1841 """Refresh the git config cache"""
1847 class RevertEditsCommand(ConfirmAction
):
1849 def __init__(self
, context
):
1850 super(RevertEditsCommand
, self
).__init
__(context
)
1851 self
.icon
= icons
.undo()
1853 def ok_to_run(self
):
1854 return self
.model
.undoable()
1856 # pylint: disable=no-self-use
1857 def checkout_from_head(self
):
1860 def checkout_args(self
):
1862 s
= self
.selection
.selection()
1863 if self
.checkout_from_head():
1864 args
.append(self
.model
.head
)
1876 checkout_args
= self
.checkout_args()
1877 return self
.git
.checkout(*checkout_args
)
1880 self
.model
.update_file_status()
1883 class RevertUnstagedEdits(RevertEditsCommand
):
1887 return N_('Revert Unstaged Edits...')
1889 def checkout_from_head(self
):
1890 # Being in amend mode should not affect the behavior of this command.
1891 # The only sensible thing to do is to checkout from the index.
1895 title
= N_('Revert Unstaged Changes?')
1897 'This operation removes unstaged edits from selected files.\n'
1898 'These changes cannot be recovered.')
1899 info
= N_('Revert the unstaged changes?')
1900 ok_text
= N_('Revert Unstaged Changes')
1901 return Interaction
.confirm(title
, text
, info
, ok_text
,
1902 default
=True, icon
=self
.icon
)
1905 class RevertUncommittedEdits(RevertEditsCommand
):
1909 return N_('Revert Uncommitted Edits...')
1911 def checkout_from_head(self
):
1915 """Prompt for reverting changes"""
1916 title
= N_('Revert Uncommitted Changes?')
1918 'This operation removes uncommitted edits from selected files.\n'
1919 'These changes cannot be recovered.')
1920 info
= N_('Revert the uncommitted changes?')
1921 ok_text
= N_('Revert Uncommitted Changes')
1922 return Interaction
.confirm(title
, text
, info
, ok_text
,
1923 default
=True, icon
=self
.icon
)
1926 class RunConfigAction(ContextCommand
):
1927 """Run a user-configured action, typically from the "Tools" menu"""
1929 def __init__(self
, context
, action_name
):
1930 super(RunConfigAction
, self
).__init
__(context
)
1931 self
.action_name
= action_name
1934 """Run the user-configured action"""
1935 for env
in ('ARGS', 'DIRNAME', 'FILENAME', 'REVISION'):
1937 compat
.unsetenv(env
)
1942 context
= self
.context
1944 opts
= cfg
.get_guitool_opts(self
.action_name
)
1945 cmd
= opts
.get('cmd')
1946 if 'title' not in opts
:
1949 if 'prompt' not in opts
or opts
.get('prompt') is True:
1950 prompt
= N_('Run "%s"?') % cmd
1951 opts
['prompt'] = prompt
1953 if opts
.get('needsfile'):
1954 filename
= self
.selection
.filename()
1956 Interaction
.information(
1957 N_('Please select a file'),
1958 N_('"%s" requires a selected file.') % cmd
)
1960 dirname
= utils
.dirname(filename
, current_dir
='.')
1961 compat
.setenv('FILENAME', filename
)
1962 compat
.setenv('DIRNAME', dirname
)
1964 if opts
.get('revprompt') or opts
.get('argprompt'):
1966 ok
= Interaction
.confirm_config_action(context
, cmd
, opts
)
1969 rev
= opts
.get('revision')
1970 args
= opts
.get('args')
1971 if opts
.get('revprompt') and not rev
:
1972 title
= N_('Invalid Revision')
1973 msg
= N_('The revision expression cannot be empty.')
1974 Interaction
.critical(title
, msg
)
1978 elif opts
.get('confirm'):
1979 title
= os
.path
.expandvars(opts
.get('title'))
1980 prompt
= os
.path
.expandvars(opts
.get('prompt'))
1981 if not Interaction
.question(title
, prompt
):
1984 compat
.setenv('REVISION', rev
)
1986 compat
.setenv('ARGS', args
)
1987 title
= os
.path
.expandvars(cmd
)
1988 Interaction
.log(N_('Running command: %s') % title
)
1989 cmd
= ['sh', '-c', cmd
]
1991 if opts
.get('background'):
1993 status
, out
, err
= (0, '', '')
1994 elif opts
.get('noconsole'):
1995 status
, out
, err
= core
.run_command(cmd
)
1997 status
, out
, err
= Interaction
.run_command(title
, cmd
)
1999 if not opts
.get('background') and not opts
.get('norescan'):
2000 self
.model
.update_status()
2003 Interaction
.command(title
, cmd
, status
, out
, err
)
2008 class SetDefaultRepo(ContextCommand
):
2009 """Set the default repository"""
2011 def __init__(self
, context
, repo
):
2012 super(SetDefaultRepo
, self
).__init
__(context
)
2016 self
.cfg
.set_user('cola.defaultrepo', self
.repo
)
2019 class SetDiffText(EditModel
):
2020 """Set the diff text"""
2023 def __init__(self
, context
, text
):
2024 super(SetDiffText
, self
).__init
__(context
)
2025 self
.new_diff_text
= text
2026 self
.new_diff_type
= 'text'
2029 class SetUpstreamBranch(ContextCommand
):
2030 """Set the upstream branch"""
2032 def __init__(self
, context
, branch
, remote
, remote_branch
):
2033 super(SetUpstreamBranch
, self
).__init
__(context
)
2034 self
.branch
= branch
2035 self
.remote
= remote
2036 self
.remote_branch
= remote_branch
2040 remote
= self
.remote
2041 branch
= self
.branch
2042 remote_branch
= self
.remote_branch
2043 cfg
.set_repo('branch.%s.remote' % branch
, remote
)
2044 cfg
.set_repo('branch.%s.merge' % branch
, 'refs/heads/' + remote_branch
)
2047 class ShowUntracked(EditModel
):
2048 """Show an untracked file."""
2050 def __init__(self
, context
, filename
):
2051 super(ShowUntracked
, self
).__init
__(context
)
2052 self
.new_filename
= filename
2053 self
.new_mode
= self
.model
.mode_untracked
2054 self
.new_diff_text
= self
.read(filename
)
2055 self
.new_diff_type
= 'text'
2057 def read(self
, filename
):
2058 """Read file contents"""
2060 size
= cfg
.get('cola.readsize', 2048)
2062 result
= core
.read(filename
, size
=size
,
2063 encoding
=core
.ENCODING
, errors
='ignore')
2064 except (IOError, OSError):
2067 if len(result
) == size
:
2072 class SignOff(ContextCommand
):
2073 """Append a signoff to the commit message"""
2078 return N_('Sign Off')
2080 def __init__(self
, context
):
2081 super(SignOff
, self
).__init
__(context
)
2082 self
.old_commitmsg
= self
.model
.commitmsg
2085 """Add a signoff to the commit message"""
2086 signoff
= self
.signoff()
2087 if signoff
in self
.model
.commitmsg
:
2089 msg
= self
.model
.commitmsg
.rstrip()
2090 self
.model
.set_commitmsg(msg
+ '\n' + signoff
)
2093 """Restore the commit message"""
2094 self
.model
.set_commitmsg(self
.old_commitmsg
)
2097 """Generate the signoff string"""
2100 user
= pwd
.getpwuid(os
.getuid()).pw_name
2102 user
= os
.getenv('USER', N_('unknown'))
2105 name
= cfg
.get('user.name', user
)
2106 email
= cfg
.get('user.email', '%s@%s' % (user
, core
.node()))
2107 return '\nSigned-off-by: %s <%s>' % (name
, email
)
2110 def check_conflicts(context
, unmerged
):
2111 """Check paths for conflicts
2113 Conflicting files can be filtered out one-by-one.
2116 if prefs
.check_conflicts(context
):
2117 unmerged
= [path
for path
in unmerged
if is_conflict_free(path
)]
2121 def is_conflict_free(path
):
2122 """Return True if `path` contains no conflict markers
2124 rgx
= re
.compile(r
'^(<<<<<<<|\|\|\|\|\|\|\||>>>>>>>) ')
2126 with core
.xopen(path
, 'r') as f
:
2128 line
= core
.decode(line
, errors
='ignore')
2130 return should_stage_conflicts(path
)
2132 # We can't read this file ~ we may be staging a removal
2137 def should_stage_conflicts(path
):
2138 """Inform the user that a file contains merge conflicts
2140 Return `True` if we should stage the path nonetheless.
2143 title
= msg
= N_('Stage conflicts?')
2144 info
= N_('%s appears to contain merge conflicts.\n\n'
2145 'You should probably skip this file.\n'
2146 'Stage it anyways?') % path
2147 ok_text
= N_('Stage conflicts')
2148 cancel_text
= N_('Skip')
2149 return Interaction
.confirm(title
, msg
, info
, ok_text
,
2150 default
=False, cancel_text
=cancel_text
)
2153 class Stage(ContextCommand
):
2154 """Stage a set of paths."""
2160 def __init__(self
, context
, paths
):
2161 super(Stage
, self
).__init
__(context
)
2165 msg
= N_('Staging: %s') % (', '.join(self
.paths
))
2166 Interaction
.log(msg
)
2167 return self
.stage_paths()
2169 def stage_paths(self
):
2170 """Stages add/removals to git."""
2171 context
= self
.context
2174 if self
.model
.cfg
.get('cola.safemode', False):
2176 return self
.stage_all()
2181 for path
in set(paths
):
2182 if core
.exists(path
) or core
.islink(path
):
2183 if path
.endswith('/'):
2184 path
= path
.rstrip('/')
2189 self
.model
.emit_about_to_update()
2191 # `git add -u` doesn't work on untracked files
2193 status
, out
, err
= gitcmds
.add(context
, add
)
2194 Interaction
.command(N_('Error'), 'git add', status
, out
, err
)
2196 # If a path doesn't exist then that means it should be removed
2197 # from the index. We use `git add -u` for that.
2199 status
, out
, err
= gitcmds
.add(context
, remove
, u
=True)
2200 Interaction
.command(N_('Error'), 'git add -u', status
, out
, err
)
2202 self
.model
.update_files(emit
=True)
2203 return status
, out
, err
2205 def stage_all(self
):
2206 """Stage all files"""
2207 status
, out
, err
= self
.git
.add(v
=True, u
=True)
2208 Interaction
.command(N_('Error'), 'git add -u', status
, out
, err
)
2209 self
.model
.update_file_status()
2210 return (status
, out
, err
)
2213 class StageCarefully(Stage
):
2214 """Only stage when the path list is non-empty
2216 We use "git add -u -- <pathspec>" to stage, and it stages everything by
2217 default when no pathspec is specified, so this class ensures that paths
2218 are specified before calling git.
2220 When no paths are specified, the command does nothing.
2223 def __init__(self
, context
):
2224 super(StageCarefully
, self
).__init
__(context
, None)
2227 # pylint: disable=no-self-use
2228 def init_paths(self
):
2229 """Initialize path data"""
2232 def ok_to_run(self
):
2233 """Prevent catch-all "git add -u" from adding unmerged files"""
2234 return self
.paths
or not self
.model
.unmerged
2237 """Stage files when ok_to_run() return True"""
2238 if self
.ok_to_run():
2239 return super(StageCarefully
, self
).do()
2243 class StageModified(StageCarefully
):
2244 """Stage all modified files."""
2248 return N_('Stage Modified')
2250 def init_paths(self
):
2251 self
.paths
= self
.model
.modified
2254 class StageUnmerged(StageCarefully
):
2255 """Stage unmerged files."""
2259 return N_('Stage Unmerged')
2261 def init_paths(self
):
2262 self
.paths
= check_conflicts(self
.context
, self
.model
.unmerged
)
2265 class StageUntracked(StageCarefully
):
2266 """Stage all untracked files."""
2270 return N_('Stage Untracked')
2272 def init_paths(self
):
2273 self
.paths
= self
.model
.untracked
2276 class StageOrUnstage(ContextCommand
):
2277 """If the selection is staged, unstage it, otherwise stage"""
2281 return N_('Stage / Unstage')
2284 s
= self
.selection
.selection()
2286 do(Unstage
, self
.context
, s
.staged
)
2289 unmerged
= check_conflicts(self
.context
, s
.unmerged
)
2291 unstaged
.extend(unmerged
)
2293 unstaged
.extend(s
.modified
)
2295 unstaged
.extend(s
.untracked
)
2297 do(Stage
, self
.context
, unstaged
)
2300 class Tag(ContextCommand
):
2301 """Create a tag object."""
2303 def __init__(self
, context
, name
, revision
, sign
=False, message
=''):
2304 super(Tag
, self
).__init
__(context
)
2306 self
._message
= message
2307 self
._revision
= revision
2313 revision
= self
._revision
2314 tag_name
= self
._name
2315 tag_message
= self
._message
2318 Interaction
.critical(
2319 N_('Missing Revision'),
2320 N_('Please specify a revision to tag.'))
2324 Interaction
.critical(
2326 N_('Please specify a name for the new tag.'))
2329 title
= N_('Missing Tag Message')
2330 message
= N_('Tag-signing was requested but the tag message is empty.')
2331 info
= N_('An unsigned, lightweight tag will be created instead.\n'
2332 'Create an unsigned tag?')
2333 ok_text
= N_('Create Unsigned Tag')
2335 if sign
and not tag_message
:
2336 # We require a message in order to sign the tag, so if they
2337 # choose to create an unsigned tag we have to clear the sign flag.
2338 if not Interaction
.confirm(title
, message
, info
, ok_text
,
2339 default
=False, icon
=icons
.save()):
2347 tmp_file
= utils
.tmp_filename('tag-message')
2348 opts
['file'] = tmp_file
2349 core
.write(tmp_file
, tag_message
)
2354 opts
['annotate'] = True
2355 status
, out
, err
= git
.tag(tag_name
, revision
, **opts
)
2358 core
.unlink(tmp_file
)
2360 title
= N_('Error: could not create tag "%s"') % tag_name
2361 Interaction
.command(title
, 'git tag', status
, out
, err
)
2365 self
.model
.update_status()
2366 Interaction
.information(
2368 N_('Created a new tag named "%s"') % tag_name
,
2369 details
=tag_message
or None)
2374 class Unstage(ContextCommand
):
2375 """Unstage a set of paths."""
2379 return N_('Unstage')
2381 def __init__(self
, context
, paths
):
2382 super(Unstage
, self
).__init
__(context
)
2387 context
= self
.context
2388 head
= self
.model
.head
2391 msg
= N_('Unstaging: %s') % (', '.join(paths
))
2392 Interaction
.log(msg
)
2394 return unstage_all(context
)
2395 status
, out
, err
= gitcmds
.unstage_paths(context
, paths
, head
=head
)
2396 Interaction
.command(N_('Error'), 'git reset', status
, out
, err
)
2397 self
.model
.update_file_status()
2398 return (status
, out
, err
)
2401 class UnstageAll(ContextCommand
):
2402 """Unstage all files; resets the index."""
2405 return unstage_all(self
.context
)
2408 def unstage_all(context
):
2409 """Unstage all files, even while amending"""
2410 model
= context
.model
2413 status
, out
, err
= git
.reset(head
, '--', '.')
2414 Interaction
.command(N_('Error'), 'git reset', status
, out
, err
)
2415 model
.update_file_status()
2416 return (status
, out
, err
)
2419 class StageSelected(ContextCommand
):
2420 """Stage selected files, or all files if no selection exists."""
2423 context
= self
.context
2424 paths
= self
.selection
.unstaged
2426 do(Stage
, context
, paths
)
2427 elif self
.cfg
.get('cola.safemode', False):
2428 do(StageModified
, context
)
2431 class UnstageSelected(Unstage
):
2432 """Unstage selected files."""
2434 def __init__(self
, context
):
2435 staged
= self
.selection
.staged
2436 super(UnstageSelected
, self
).__init
__(context
, staged
)
2439 class Untrack(ContextCommand
):
2440 """Unstage a set of paths."""
2442 def __init__(self
, context
, paths
):
2443 super(Untrack
, self
).__init
__(context
)
2447 msg
= N_('Untracking: %s') % (', '.join(self
.paths
))
2448 Interaction
.log(msg
)
2449 status
, out
, err
= self
.model
.untrack_paths(self
.paths
)
2450 Interaction
.log_status(status
, out
, err
)
2453 class UntrackedSummary(EditModel
):
2454 """List possible .gitignore rules as the diff text."""
2456 def __init__(self
, context
):
2457 super(UntrackedSummary
, self
).__init
__(context
)
2458 untracked
= self
.model
.untracked
2459 suffix
= 's' if untracked
else ''
2461 io
.write('# %s untracked file%s\n' % (len(untracked
), suffix
))
2463 io
.write('# possible .gitignore rule%s:\n' % suffix
)
2465 io
.write('/'+u
+'\n')
2466 self
.new_diff_text
= io
.getvalue()
2467 self
.new_diff_type
= 'text'
2468 self
.new_mode
= self
.model
.mode_untracked
2471 class VisualizeAll(ContextCommand
):
2472 """Visualize all branches."""
2475 context
= self
.context
2476 browser
= utils
.shell_split(prefs
.history_browser(context
))
2477 launch_history_browser(browser
+ ['--all'])
2480 class VisualizeCurrent(ContextCommand
):
2481 """Visualize all branches."""
2484 context
= self
.context
2485 browser
= utils
.shell_split(prefs
.history_browser(context
))
2486 launch_history_browser(browser
+ [self
.model
.currentbranch
] + ['--'])
2489 class VisualizePaths(ContextCommand
):
2490 """Path-limited visualization."""
2492 def __init__(self
, context
, paths
):
2493 super(VisualizePaths
, self
).__init
__(context
)
2494 context
= self
.context
2495 browser
= utils
.shell_split(prefs
.history_browser(context
))
2497 self
.argv
= browser
+ ['--'] + list(paths
)
2502 launch_history_browser(self
.argv
)
2505 class VisualizeRevision(ContextCommand
):
2506 """Visualize a specific revision."""
2508 def __init__(self
, context
, revision
, paths
=None):
2509 super(VisualizeRevision
, self
).__init
__(context
)
2510 self
.revision
= revision
2514 context
= self
.context
2515 argv
= utils
.shell_split(prefs
.history_browser(context
))
2517 argv
.append(self
.revision
)
2520 argv
.extend(self
.paths
)
2521 launch_history_browser(argv
)
2524 class SubmoduleUpdate(ConfirmAction
):
2525 """Update specified submodule"""
2527 def __init__(self
, context
, path
):
2528 super(SubmoduleUpdate
, self
).__init
__(context
)
2532 title
= N_('Update Submodule...')
2533 question
= N_('Update this submodule?')
2534 info
= N_('The submodule will be updated using\n'
2535 '"%s"' % self
.command())
2536 ok_txt
= N_('Update Submodule')
2537 return Interaction
.confirm(title
, question
, info
, ok_txt
,
2538 default
=False, icon
=icons
.pull())
2541 context
= self
.context
2542 return context
.git
.submodule('update', '--', self
.path
)
2545 self
.model
.update_file_status()
2547 def error_message(self
):
2548 return N_('Error updating submodule %s' % self
.path
)
2551 command
= 'git submodule update -- %s'
2552 return command
% self
.path
2555 class SubmodulesUpdate(ConfirmAction
):
2556 """Update all submodules"""
2559 title
= N_('Update submodules...')
2560 question
= N_('Update all submodules?')
2561 info
= N_('All submodules will be updated using\n'
2562 '"%s"' % self
.command())
2563 ok_txt
= N_('Update Submodules')
2564 return Interaction
.confirm(title
, question
, info
, ok_txt
,
2565 default
=False, icon
=icons
.pull())
2568 context
= self
.context
2569 return context
.git
.submodule('update')
2572 self
.model
.update_file_status()
2574 def error_message(self
):
2575 return N_('Error updating submodules')
2578 return 'git submodule update'
2581 def launch_history_browser(argv
):
2582 """Launch the configured history browser"""
2585 except OSError as e
:
2586 _
, details
= utils
.format_exception(e
)
2587 title
= N_('Error Launching History Browser')
2588 msg
= (N_('Cannot exec "%s": please configure a history browser') %
2590 Interaction
.critical(title
, message
=msg
, details
=details
)
2593 def run(cls
, *args
, **opts
):
2595 Returns a callback that runs a command
2597 If the caller of run() provides args or opts then those are
2598 used instead of the ones provided by the invoker of the callback.
2601 def runner(*local_args
, **local_opts
):
2602 """Closure return by run() which runs the command"""
2604 do(cls
, *args
, **opts
)
2606 do(cls
, *local_args
, **local_opts
)
2611 def do(cls
, *args
, **opts
):
2612 """Run a command in-place"""
2614 cmd
= cls(*args
, **opts
)
2616 except Exception as e
: # pylint: disable=broad-except
2617 msg
, details
= utils
.format_exception(e
)
2618 if hasattr(cls
, '__name__'):
2619 msg
= ('%s exception:\n%s' % (cls
.__name
__, msg
))
2620 Interaction
.critical(N_('Error'), message
=msg
, details
=details
)
2624 def difftool_run(context
):
2625 """Start a default difftool session"""
2626 selection
= context
.selection
2627 files
= selection
.group()
2630 s
= selection
.selection()
2631 head
= context
.model
.head
2632 difftool_launch_with_head(context
, files
, bool(s
.staged
), head
)
2635 def difftool_launch_with_head(context
, filenames
, staged
, head
):
2636 """Launch difftool against the provided head"""
2641 difftool_launch(context
, left
=left
, staged
=staged
, paths
=filenames
)
2644 def difftool_launch(context
, left
=None, right
=None, paths
=None,
2645 staged
=False, dir_diff
=False,
2646 left_take_magic
=False, left_take_parent
=False):
2647 """Launches 'git difftool' with given parameters
2649 :param left: first argument to difftool
2650 :param right: second argument to difftool_args
2651 :param paths: paths to diff
2652 :param staged: activate `git difftool --staged`
2653 :param dir_diff: activate `git difftool --dir-diff`
2654 :param left_take_magic: whether to append the magic ^! diff expression
2655 :param left_take_parent: whether to append the first-parent ~ for diffing
2659 difftool_args
= ['git', 'difftool', '--no-prompt']
2661 difftool_args
.append('--cached')
2663 difftool_args
.append('--dir-diff')
2666 if left_take_parent
or left_take_magic
:
2667 suffix
= '^!' if left_take_magic
else '~'
2668 # Check root commit (no parents and thus cannot execute '~')
2670 status
, out
, err
= git
.rev_list(left
, parents
=True, n
=1)
2671 Interaction
.log_status(status
, out
, err
)
2673 raise OSError('git rev-list command failed')
2675 if len(out
.split()) >= 2:
2676 # Commit has a parent, so we can take its child as requested
2679 # No parent, assume it's the root commit, so we have to diff
2680 # against the empty tree.
2681 left
= EMPTY_TREE_OID
2682 if not right
and left_take_magic
:
2684 difftool_args
.append(left
)
2687 difftool_args
.append(right
)
2690 difftool_args
.append('--')
2691 difftool_args
.extend(paths
)
2693 runtask
= context
.runtask
2695 Interaction
.async_command(N_('Difftool'), difftool_args
, runtask
)
2697 core
.fork(difftool_args
)