1 from __future__
import division
, absolute_import
, unicode_literals
5 from fnmatch
import fnmatch
6 from io
import StringIO
9 from send2trash
import send2trash
15 from . import fsmonitor
20 from . import resources
21 from .diffparse
import DiffParser
22 from .git
import STDOUT
24 from .interaction
import Interaction
25 from .models
import main
26 from .models
import prefs
27 from .models
import selection
30 class UsageError(Exception):
31 """Exception class for usage errors."""
32 def __init__(self
, title
, message
):
33 Exception.__init
__(self
, message
)
38 class BaseCommand(object):
39 """Base class for all commands; provides the command pattern"""
43 def __init__(self
, **kwargs
):
45 for k
, v
in kwargs
.items():
48 def is_undoable(self
):
49 """Can this be undone?"""
63 class ConfirmAction(BaseCommand
):
65 def __init__(self
, **kwargs
):
66 BaseCommand
.__init
__(self
, **kwargs
)
83 def fail(self
, status
, out
, err
):
84 title
= msg
= self
.error_message()
85 details
= self
.error_details() or out
+ err
86 Interaction
.critical(title
, message
=msg
, details
=details
)
88 def error_message(self
):
91 def error_details(self
):
97 ok
= self
.ok_to_run() and self
.confirm()
99 status
, out
, err
= self
.action()
103 self
.fail(status
, out
, err
)
105 return ok
, status
, out
, err
108 class ModelCommand(BaseCommand
):
109 """Commands that manipulate the main models"""
111 def __init__(self
, **kwargs
):
112 # Note: self.model is set before calling the base class constructor
113 # to allow being having the `model` value be overridden by passing
114 # `model=xxx` during construction.
115 self
.model
= main
.model()
116 BaseCommand
.__init
__(self
, **kwargs
)
119 class Command(ModelCommand
):
120 """Base class for commands that modify the main model"""
123 """Initialize the command and stash away values for use in do()"""
124 # These are commonly used so let's make it easier to write new commands.
125 ModelCommand
.__init
__(self
)
127 self
.old_diff_text
= self
.model
.diff_text
128 self
.old_filename
= self
.model
.filename
129 self
.old_mode
= self
.model
.mode
131 self
.new_diff_text
= self
.old_diff_text
132 self
.new_filename
= self
.old_filename
133 self
.new_mode
= self
.old_mode
136 """Perform the operation."""
137 self
.model
.set_filename(self
.new_filename
)
138 self
.model
.set_mode(self
.new_mode
)
139 self
.model
.set_diff_text(self
.new_diff_text
)
142 """Undo the operation."""
143 self
.model
.set_diff_text(self
.old_diff_text
)
144 self
.model
.set_filename(self
.old_filename
)
145 self
.model
.set_mode(self
.old_mode
)
148 class AmendMode(Command
):
149 """Try to amend a commit."""
157 def __init__(self
, amend
):
158 Command
.__init
__(self
)
161 self
.amending
= amend
162 self
.old_commitmsg
= self
.model
.commitmsg
163 self
.old_mode
= self
.model
.mode
166 self
.new_mode
= self
.model
.mode_amend
167 self
.new_commitmsg
= self
.model
.prev_commitmsg()
168 AmendMode
.LAST_MESSAGE
= self
.model
.commitmsg
170 # else, amend unchecked, regular commit
171 self
.new_mode
= self
.model
.mode_none
172 self
.new_diff_text
= ''
173 self
.new_commitmsg
= self
.model
.commitmsg
174 # If we're going back into new-commit-mode then search the
175 # undo stack for a previous amend-commit-mode and grab the
176 # commit message at that point in time.
177 if AmendMode
.LAST_MESSAGE
is not None:
178 self
.new_commitmsg
= AmendMode
.LAST_MESSAGE
179 AmendMode
.LAST_MESSAGE
= None
182 """Leave/enter amend mode."""
183 """Attempt to enter amend mode. Do not allow this when merging."""
185 if self
.model
.is_merging
:
187 self
.model
.set_mode(self
.old_mode
)
188 Interaction
.information(
190 N_('You are in the middle of a merge.\n'
191 'Cannot amend while merging.'))
195 self
.model
.set_commitmsg(self
.new_commitmsg
)
196 self
.model
.update_file_status()
201 self
.model
.set_commitmsg(self
.old_commitmsg
)
203 self
.model
.update_file_status()
206 class ApplyDiffSelection(Command
):
208 def __init__(self
, first_line_idx
, last_line_idx
, has_selection
,
209 reverse
, apply_to_worktree
):
210 Command
.__init
__(self
)
211 self
.first_line_idx
= first_line_idx
212 self
.last_line_idx
= last_line_idx
213 self
.has_selection
= has_selection
214 self
.reverse
= reverse
215 self
.apply_to_worktree
= apply_to_worktree
218 parser
= DiffParser(self
.model
.filename
, self
.model
.diff_text
)
219 if self
.has_selection
:
220 patch
= parser
.generate_patch(self
.first_line_idx
,
222 reverse
=self
.reverse
)
224 patch
= parser
.generate_hunk_patch(self
.first_line_idx
,
225 reverse
=self
.reverse
)
229 cfg
= gitcfg
.current()
230 encoding
= cfg
.file_encoding(self
.model
.filename
)
231 tmp_file
= utils
.tmp_filename('patch')
233 core
.write(tmp_file
, patch
, encoding
=encoding
)
234 if self
.apply_to_worktree
:
235 status
, out
, err
= self
.model
.apply_diff_to_worktree(tmp_file
)
237 status
, out
, err
= self
.model
.apply_diff(tmp_file
)
239 core
.unlink(tmp_file
)
241 Interaction
.log_status(status
, out
, err
)
242 self
.model
.update_file_status(update_index
=True)
245 class ApplyPatches(Command
):
247 def __init__(self
, patches
):
248 Command
.__init
__(self
)
249 self
.patches
= patches
253 num_patches
= len(self
.patches
)
254 orig_head
= self
.model
.git
.rev_parse('HEAD')[STDOUT
]
256 for idx
, patch
in enumerate(self
.patches
):
257 status
, out
, err
= self
.model
.git
.am(patch
)
258 # Log the git-am command
259 Interaction
.log_status(status
, out
, err
)
262 diff
= self
.model
.git
.diff('HEAD^!', stat
=True)[STDOUT
]
263 diff_text
+= (N_('PATCH %(current)d/%(count)d') %
264 dict(current
=idx
+1, count
=num_patches
))
265 diff_text
+= ' - %s:\n%s\n\n' % (os
.path
.basename(patch
), diff
)
267 diff_text
+= N_('Summary:') + '\n'
268 diff_text
+= self
.model
.git
.diff(orig_head
, stat
=True)[STDOUT
]
271 self
.model
.set_diff_text(diff_text
)
272 self
.model
.update_file_status()
274 basenames
= '\n'.join([os
.path
.basename(p
) for p
in self
.patches
])
275 Interaction
.information(
276 N_('Patch(es) Applied'),
277 (N_('%d patch(es) applied.') +
278 '\n\n%s') % (len(self
.patches
), basenames
))
281 class Archive(BaseCommand
):
283 def __init__(self
, ref
, fmt
, prefix
, filename
):
284 BaseCommand
.__init
__(self
)
288 self
.filename
= filename
291 fp
= core
.xopen(self
.filename
, 'wb')
292 cmd
= ['git', 'archive', '--format='+self
.fmt
]
293 if self
.fmt
in ('tgz', 'tar.gz'):
296 cmd
.append('--prefix=' + self
.prefix
)
298 proc
= core
.start_command(cmd
, stdout
=fp
)
299 out
, err
= proc
.communicate()
301 status
= proc
.returncode
302 Interaction
.log_status(status
, out
or '', err
or '')
305 class Checkout(Command
):
307 A command object for git-checkout.
309 'argv' is handed off directly to git.
313 def __init__(self
, argv
, checkout_branch
=False):
314 Command
.__init
__(self
)
316 self
.checkout_branch
= checkout_branch
317 self
.new_diff_text
= ''
320 status
, out
, err
= self
.model
.git
.checkout(*self
.argv
)
321 Interaction
.log_status(status
, out
, err
)
322 if self
.checkout_branch
:
323 self
.model
.update_status()
325 self
.model
.update_file_status()
328 class BlamePaths(Command
):
329 """Blame view for paths."""
331 def __init__(self
, paths
):
332 Command
.__init
__(self
)
333 viewer
= utils
.shell_split(prefs
.blame_viewer())
334 self
.argv
= viewer
+ list(paths
)
339 except Exception as e
:
340 _
, details
= utils
.format_exception(e
)
341 title
= N_('Error Launching Blame Viewer')
342 msg
= (N_('Cannot exec "%s": please configure a blame viewer') %
344 Interaction
.critical(title
, message
=msg
, details
=details
)
347 class CheckoutBranch(Checkout
):
348 """Checkout a branch."""
350 def __init__(self
, branch
):
352 Checkout
.__init
__(self
, args
, checkout_branch
=True)
355 class CherryPick(Command
):
356 """Cherry pick commits into the current branch."""
358 def __init__(self
, commits
):
359 Command
.__init
__(self
)
360 self
.commits
= commits
363 self
.model
.cherry_pick_list(self
.commits
)
364 self
.model
.update_file_status()
367 class ResetMode(Command
):
368 """Reset the mode and clear the model's diff text."""
371 Command
.__init
__(self
)
372 self
.new_mode
= self
.model
.mode_none
373 self
.new_diff_text
= ''
377 self
.model
.update_file_status()
380 class ResetCommand(ConfirmAction
):
382 def __init__(self
, ref
):
383 ConfirmAction
.__init
__(self
, model
=main
.model(), ref
=ref
)
386 status
, out
, err
= self
.reset()
387 Interaction
.log_status(status
, out
, err
)
388 return status
, out
, err
391 self
.model
.update_file_status()
394 raise NotImplemented('confirm() must be overridden')
397 raise NotImplemented('reset() must be overridden')
400 class ResetBranchHead(ResetCommand
):
403 title
= N_('Reset Branch')
404 question
= N_('Point the current branch head to a new commit?')
405 info
= N_('The branch will be reset using "git reset --mixed %s"')
406 ok_btn
= N_('Reset Branch')
407 info
= info
% self
.ref
408 return Interaction
.confirm(title
, question
, info
, ok_btn
)
412 return git
.reset(self
.ref
, '--', mixed
=True)
415 class ResetWorktree(ResetCommand
):
418 title
= N_('Reset Worktree')
419 question
= N_('Reset worktree?')
420 info
= N_('The worktree will be reset using "git reset --merge %s"')
421 ok_btn
= N_('Reset Worktree')
422 info
= info
% self
.ref
423 return Interaction
.confirm(title
, question
, info
, ok_btn
)
426 return self
.model
.git
.reset(self
.ref
, '--', merge
=True)
429 class Commit(ResetMode
):
430 """Attempt to create a new commit."""
432 def __init__(self
, amend
, msg
, sign
, no_verify
=False):
433 ResetMode
.__init
__(self
)
437 self
.no_verify
= no_verify
438 self
.old_commitmsg
= self
.model
.commitmsg
439 self
.new_commitmsg
= ''
442 # Create the commit message file
443 comment_char
= prefs
.comment_char()
444 msg
= self
.strip_comments(self
.msg
, comment_char
=comment_char
)
445 tmp_file
= utils
.tmp_filename('commit-message')
447 core
.write(tmp_file
, msg
)
450 status
, out
, err
= self
.model
.git
.commit(F
=tmp_file
,
454 no_verify
=self
.no_verify
)
456 core
.unlink(tmp_file
)
460 self
.model
.set_commitmsg(self
.new_commitmsg
)
461 msg
= N_('Created commit: %s') % out
463 msg
= N_('Commit failed: %s') % out
464 Interaction
.log_status(status
, msg
, err
)
466 return status
, out
, err
469 def strip_comments(msg
, comment_char
='#'):
471 message_lines
= [line
for line
in msg
.split('\n')
472 if not line
.startswith(comment_char
)]
473 msg
= '\n'.join(message_lines
)
474 if not msg
.endswith('\n'):
480 class Ignore(Command
):
481 """Add files to .gitignore"""
483 def __init__(self
, filenames
):
484 Command
.__init
__(self
)
485 self
.filenames
= list(filenames
)
488 if not self
.filenames
:
490 new_additions
= '\n'.join(self
.filenames
) + '\n'
491 for_status
= new_additions
492 if core
.exists('.gitignore'):
493 current_list
= core
.read('.gitignore')
494 new_additions
= current_list
.rstrip() + '\n' + new_additions
495 core
.write('.gitignore', new_additions
)
496 Interaction
.log_status(0, 'Added to .gitignore:\n%s' % for_status
, '')
497 self
.model
.update_file_status()
500 def file_summary(files
):
501 txt
= core
.list2cmdline(files
)
503 txt
= txt
[:768].rstrip() + '...'
507 class RemoteCommand(ConfirmAction
):
509 def __init__(self
, remote
):
510 ConfirmAction
.__init
__(self
)
511 self
.model
= main
.model()
515 self
.model
.update_remotes()
518 class RemoteAdd(RemoteCommand
):
520 def __init__(self
, remote
, url
):
521 RemoteCommand
.__init
__(self
, remote
)
526 return git
.remote('add', self
.remote
, self
.url
)
528 def error_message(self
):
529 return N_('Error creating remote "%s"') % self
.remote
532 class RemoteRemove(RemoteCommand
):
535 title
= N_('Delete Remote')
536 question
= N_('Delete remote?')
537 info
= N_('Delete remote "%s"') % self
.remote
538 ok_btn
= N_('Delete')
539 return Interaction
.confirm(title
, question
, info
, ok_btn
)
543 return git
.remote('rm', self
.remote
)
545 def error_message(self
):
546 return N_('Error deleting remote "%s"') % self
.remote
549 class RemoteRename(RemoteCommand
):
551 def __init__(self
, remote
, new_remote
):
552 RemoteCommand
.__init
__(self
, remote
)
553 self
.new_remote
= new_remote
556 title
= N_('Rename Remote')
557 question
= N_('Rename remote?')
558 info
= (N_('Rename remote "%(current)s" to "%(new)s"?') %
559 dict(current
=self
.remote
, new
=self
.new_remote
))
560 ok_btn
= N_('Rename')
561 return Interaction
.confirm(title
, question
, info
, ok_btn
)
565 return git
.remote('rename', self
.remote
, self
.new_remote
)
568 class RemoveFromSettings(ConfirmAction
):
570 def __init__(self
, settings
, repo
, name
, icon
=None):
571 ConfirmAction
.__init
__(self
)
572 self
.settings
= settings
581 class RemoveBookmark(RemoveFromSettings
):
585 title
= msg
= N_('Delete Bookmark?')
586 info
= N_('%s will be removed from your bookmarks.') % name
587 ok_text
= N_('Delete Bookmark')
588 return Interaction
.confirm(title
, msg
, info
, ok_text
, icon
=self
.icon
)
591 self
.settings
.remove_bookmark(self
.repo
, self
.name
)
595 class RemoveRecent(RemoveFromSettings
):
599 title
= msg
= N_('Remove %s from the recent list?') % repo
600 info
= N_('%s will be removed from your recent repositories.') % repo
601 ok_text
= N_('Remove')
602 return Interaction
.confirm(title
, msg
, info
, ok_text
, icon
=self
.icon
)
605 self
.settings
.remove_recent(self
.repo
)
609 class RemoveFiles(Command
):
612 def __init__(self
, remover
, filenames
):
613 Command
.__init
__(self
)
616 self
.remover
= remover
617 self
.filenames
= filenames
618 # We could git-hash-object stuff and provide undo-ability
622 files
= self
.filenames
628 remove
= self
.remover
629 for filename
in files
:
635 bad_filenames
.append(filename
)
638 Interaction
.information(
640 N_('Deleting "%s" failed') % file_summary(files
))
643 self
.model
.update_file_status()
646 class Delete(RemoveFiles
):
649 def __init__(self
, filenames
):
650 RemoveFiles
.__init
__(self
, os
.remove
, filenames
)
653 files
= self
.filenames
657 title
= N_('Delete Files?')
658 msg
= N_('The following files will be deleted:') + '\n\n'
659 msg
+= file_summary(files
)
660 info_txt
= N_('Delete %d file(s)?') % len(files
)
661 ok_txt
= N_('Delete Files')
663 if not Interaction
.confirm(title
, msg
, info_txt
, ok_txt
,
664 default
=True, icon
=icons
.remove()):
667 return RemoveFiles
.do(self
)
670 class MoveToTrash(RemoveFiles
):
671 """Move files to the trash using send2trash"""
673 AVAILABLE
= send2trash
is not None
675 def __init__(self
, filenames
):
676 RemoveFiles
.__init
__(self
, send2trash
, filenames
)
679 class DeleteBranch(Command
):
680 """Delete a git branch."""
682 def __init__(self
, branch
):
683 Command
.__init
__(self
)
687 status
, out
, err
= self
.model
.delete_branch(self
.branch
)
688 Interaction
.log_status(status
, out
, err
)
691 class RenameBranch(Command
):
692 """Rename a git branch."""
694 def __init__(self
, branch
, new_branch
):
695 Command
.__init
__(self
)
697 self
.new_branch
= new_branch
700 status
, out
, err
= self
.model
.rename_branch(self
.branch
,
702 Interaction
.log_status(status
, out
, err
)
705 class DeleteRemoteBranch(Command
):
706 """Delete a remote git branch."""
708 def __init__(self
, remote
, branch
):
709 Command
.__init
__(self
)
714 status
, out
, err
= self
.model
.git
.push(self
.remote
, self
.branch
,
716 Interaction
.log_status(status
, out
, err
)
717 self
.model
.update_status()
720 Interaction
.information(
721 N_('Remote Branch Deleted'),
722 N_('"%(branch)s" has been deleted from "%(remote)s".')
723 % dict(branch
=self
.branch
, remote
=self
.remote
))
726 message
= (N_('"%(command)s" returned exit status %(status)d') %
727 dict(command
=command
, status
=status
))
729 Interaction
.critical(N_('Error Deleting Remote Branch'),
734 """Perform a diff and set the model's current text."""
736 def __init__(self
, filename
, cached
=False, deleted
=False):
737 Command
.__init
__(self
)
740 opts
['ref'] = self
.model
.head
741 self
.new_filename
= filename
742 self
.new_mode
= self
.model
.mode_worktree
743 self
.new_diff_text
= gitcmds
.diff_helper(filename
=filename
,
749 class Diffstat(Command
):
750 """Perform a diffstat and set the model's diff text."""
753 Command
.__init
__(self
)
754 cfg
= gitcfg
.current()
755 diff_context
= cfg
.get('diff.context', 3)
756 diff
= self
.model
.git
.diff(self
.model
.head
,
757 unified
=diff_context
,
762 self
.new_diff_text
= diff
763 self
.new_mode
= self
.model
.mode_diffstat
766 class DiffStaged(Diff
):
767 """Perform a staged diff on a file."""
769 def __init__(self
, filename
, deleted
=None):
770 Diff
.__init
__(self
, filename
, cached
=True, deleted
=deleted
)
771 self
.new_mode
= self
.model
.mode_index
774 class DiffStagedSummary(Command
):
777 Command
.__init
__(self
)
778 diff
= self
.model
.git
.diff(self
.model
.head
,
782 patch_with_stat
=True,
784 self
.new_diff_text
= diff
785 self
.new_mode
= self
.model
.mode_index
788 class Difftool(Command
):
789 """Run git-difftool limited by path."""
791 def __init__(self
, staged
, filenames
):
792 Command
.__init
__(self
)
794 self
.filenames
= filenames
797 difftool_launch_with_head(self
.filenames
, self
.staged
, self
.model
.head
)
801 """Edit a file using the configured gui.editor."""
805 return N_('Launch Editor')
807 def __init__(self
, filenames
, line_number
=None):
808 Command
.__init
__(self
)
809 self
.filenames
= filenames
810 self
.line_number
= line_number
813 if not self
.filenames
:
815 filename
= self
.filenames
[0]
816 if not core
.exists(filename
):
818 editor
= prefs
.editor()
821 if self
.line_number
is None:
822 opts
= self
.filenames
824 # Single-file w/ line-numbers (likely from grep)
826 '*vim*': ['+'+self
.line_number
, filename
],
827 '*emacs*': ['+'+self
.line_number
, filename
],
828 '*textpad*': ['%s(%s,0)' % (filename
, self
.line_number
)],
829 '*notepad++*': ['-n'+self
.line_number
, filename
],
832 opts
= self
.filenames
833 for pattern
, opt
in editor_opts
.items():
834 if fnmatch(editor
, pattern
):
839 core
.fork(utils
.shell_split(editor
) + opts
)
840 except Exception as e
:
841 message
= (N_('Cannot exec "%s": please configure your editor')
843 details
= core
.decode(e
.strerror
)
844 Interaction
.critical(N_('Error Editing File'), message
, details
)
847 class FormatPatch(Command
):
848 """Output a patch series given all revisions and a selected subset."""
850 def __init__(self
, to_export
, revs
):
851 Command
.__init
__(self
)
852 self
.to_export
= list(to_export
)
853 self
.revs
= list(revs
)
856 status
, out
, err
= gitcmds
.format_patchsets(self
.to_export
, self
.revs
)
857 Interaction
.log_status(status
, out
, err
)
860 class LaunchDifftool(BaseCommand
):
864 return N_('Launch Diff Tool')
867 BaseCommand
.__init
__(self
)
870 s
= selection
.selection()
874 core
.fork(['git', 'mergetool', '--no-prompt', '--'] + paths
)
876 cfg
= gitcfg
.current()
878 argv
= utils
.shell_split(cmd
)
879 mergetool
= ['git', 'mergetool', '--no-prompt', '--']
880 mergetool
.extend(paths
)
881 needs_shellquote
= set(['gnome-terminal', 'xfce4-terminal'])
882 if os
.path
.basename(argv
[0]) in needs_shellquote
:
883 argv
.append(core
.list2cmdline(mergetool
))
885 argv
.extend(mergetool
)
891 class LaunchTerminal(BaseCommand
):
895 return N_('Launch Terminal')
897 def __init__(self
, path
):
898 BaseCommand
.__init
__(self
)
902 cfg
= gitcfg
.current()
904 argv
= utils
.shell_split(cmd
)
905 argv
.append(os
.getenv('SHELL', '/bin/sh'))
906 core
.fork(argv
, cwd
=self
.path
)
909 class LaunchEditor(Edit
):
913 return N_('Launch Editor')
916 s
= selection
.selection()
917 allfiles
= s
.staged
+ s
.unmerged
+ s
.modified
+ s
.untracked
918 Edit
.__init
__(self
, allfiles
)
921 class LoadCommitMessageFromFile(Command
):
922 """Loads a commit message from a path."""
924 def __init__(self
, path
):
925 Command
.__init
__(self
)
928 self
.old_commitmsg
= self
.model
.commitmsg
929 self
.old_directory
= self
.model
.directory
932 path
= os
.path
.expanduser(self
.path
)
933 if not path
or not core
.isfile(path
):
934 raise UsageError(N_('Error: Cannot find commit template'),
935 N_('%s: No such file or directory.') % path
)
936 self
.model
.set_directory(os
.path
.dirname(path
))
937 self
.model
.set_commitmsg(core
.read(path
))
940 self
.model
.set_commitmsg(self
.old_commitmsg
)
941 self
.model
.set_directory(self
.old_directory
)
944 class LoadCommitMessageFromTemplate(LoadCommitMessageFromFile
):
945 """Loads the commit message template specified by commit.template."""
948 cfg
= gitcfg
.current()
949 template
= cfg
.get('commit.template')
950 LoadCommitMessageFromFile
.__init
__(self
, template
)
953 if self
.path
is None:
955 N_('Error: Unconfigured commit template'),
956 N_('A commit template has not been configured.\n'
957 'Use "git config" to define "commit.template"\n'
958 'so that it points to a commit template.'))
959 return LoadCommitMessageFromFile
.do(self
)
962 class LoadCommitMessageFromOID(Command
):
963 """Load a previous commit message"""
965 def __init__(self
, oid
, prefix
=''):
966 Command
.__init
__(self
)
968 self
.old_commitmsg
= self
.model
.commitmsg
969 self
.new_commitmsg
= prefix
+ self
.model
.prev_commitmsg(oid
)
973 self
.model
.set_commitmsg(self
.new_commitmsg
)
976 self
.model
.set_commitmsg(self
.old_commitmsg
)
979 class PrepareCommitMessageHook(Command
):
980 """Use the cola-prepare-commit-msg hook to prepare the commit message
983 Command
.__init
__(self
)
984 self
.old_commitmsg
= self
.model
.commitmsg
987 def get_message(self
):
989 title
= N_('Error running prepare-commitmsg hook')
990 hook
= gitcmds
.prepare_commit_message_hook()
992 if os
.path
.exists(hook
):
993 filename
= self
.model
.save_commitmsg()
994 status
, out
, err
= core
.run_command([hook
, filename
])
997 result
= core
.read(filename
)
999 result
= self
.old_commitmsg
1002 if details
and not details
.endswith('\n'):
1005 message
= N_('"%s" returned exit status %d') % (hook
, status
)
1006 Interaction
.critical(title
, message
=message
, details
=details
)
1008 message
= N_('A hook must be provided at "%s"') % hook
1009 Interaction
.critical(title
, message
=message
)
1010 result
= self
.old_commitmsg
1015 msg
= self
.get_message()
1016 self
.model
.set_commitmsg(msg
)
1019 self
.model
.set_commitmsg(self
.old_commitmsg
)
1022 class LoadFixupMessage(LoadCommitMessageFromOID
):
1023 """Load a fixup message"""
1025 def __init__(self
, oid
):
1026 LoadCommitMessageFromOID
.__init
__(self
, oid
, prefix
='fixup! ')
1027 if self
.new_commitmsg
:
1028 self
.new_commitmsg
= self
.new_commitmsg
.splitlines()[0]
1031 class Merge(Command
):
1034 def __init__(self
, revision
, no_commit
, squash
, no_ff
, sign
):
1035 Command
.__init
__(self
)
1036 self
.revision
= revision
1038 self
.no_commit
= no_commit
1039 self
.squash
= squash
1043 squash
= self
.squash
1044 revision
= self
.revision
1046 no_commit
= self
.no_commit
1049 status
, out
, err
= self
.model
.git
.merge(revision
,
1052 no_commit
=no_commit
,
1054 Interaction
.log_status(status
, out
, err
)
1055 self
.model
.update_status()
1058 class OpenDefaultApp(BaseCommand
):
1059 """Open a file using the OS default."""
1063 return N_('Open Using Default Application')
1065 def __init__(self
, filenames
):
1066 BaseCommand
.__init
__(self
)
1067 if utils
.is_darwin():
1070 launcher
= 'xdg-open'
1071 self
.launcher
= launcher
1072 self
.filenames
= filenames
1075 if not self
.filenames
:
1077 core
.fork([self
.launcher
] + self
.filenames
)
1080 class OpenParentDir(OpenDefaultApp
):
1081 """Open parent directories using the OS default."""
1085 return N_('Open Parent Directory')
1087 def __init__(self
, filenames
):
1088 OpenDefaultApp
.__init
__(self
, filenames
)
1091 if not self
.filenames
:
1093 dirnames
= list(set([os
.path
.dirname(x
) for x
in self
.filenames
]))
1094 # os.path.dirname() can return an empty string so we fallback to
1095 # the current directory
1096 dirs
= [(dirname
or core
.getcwd()) for dirname
in dirnames
]
1097 core
.fork([self
.launcher
] + dirs
)
1100 class OpenNewRepo(Command
):
1101 """Launches git-cola on a repo."""
1103 def __init__(self
, repo_path
):
1104 Command
.__init
__(self
)
1105 self
.repo_path
= repo_path
1108 self
.model
.set_directory(self
.repo_path
)
1109 core
.fork([sys
.executable
, sys
.argv
[0], '--repo', self
.repo_path
])
1112 class OpenRepo(Command
):
1113 def __init__(self
, repo_path
):
1114 Command
.__init
__(self
)
1115 self
.repo_path
= repo_path
1118 git
= self
.model
.git
1119 old_repo
= git
.getcwd()
1120 if self
.model
.set_worktree(self
.repo_path
):
1121 fsmonitor
.current().stop()
1122 fsmonitor
.current().start()
1123 self
.model
.update_status()
1125 self
.model
.set_worktree(old_repo
)
1128 class Clone(Command
):
1129 """Clones a repository and optionally spawns a new cola session."""
1131 def __init__(self
, url
, new_directory
, spawn
=True):
1132 Command
.__init
__(self
)
1134 self
.new_directory
= new_directory
1138 self
.error_message
= ''
1139 self
.error_details
= ''
1142 status
, out
, err
= self
.model
.git
.clone(self
.url
, self
.new_directory
)
1143 self
.ok
= status
== 0
1147 core
.fork([sys
.executable
, sys
.argv
[0],
1148 '--repo', self
.new_directory
])
1150 self
.error_message
= N_('Error: could not clone "%s"') % self
.url
1151 self
.error_details
= (
1152 (N_('git clone returned exit code %s') % status
) +
1153 ((out
+err
) and ('\n\n' + out
+ err
) or ''))
1158 def unix_path(path
, is_win32
=utils
.is_win32
):
1159 """Git for Windows requires unix paths, so force them here
1165 if second
== ':': # sanity check, this better be a Windows-style path
1166 unix_path
= '/' + first
+ path
[2:].replace('\\', '/')
1171 class GitXBaseContext(object):
1173 def __init__(self
, **kwargs
):
1174 self
.env
= {'GIT_EDITOR': prefs
.editor()}
1175 self
.env
.update(kwargs
)
1177 def __enter__(self
):
1178 compat
.setenv('GIT_SEQUENCE_EDITOR',
1179 unix_path(resources
.share('bin', 'git-xbase')))
1180 for var
, value
in self
.env
.items():
1181 compat
.setenv(var
, value
)
1184 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
1185 compat
.unsetenv('GIT_SEQUENCE_EDITOR')
1186 for var
in self
.env
:
1187 compat
.unsetenv(var
)
1190 class Rebase(Command
):
1193 upstream
=None, branch
=None, capture_output
=True, **kwargs
):
1194 """Start an interactive rebase session
1196 :param upstream: upstream branch
1197 :param branch: optional branch to checkout
1198 :param capture_output: whether to capture stdout and stderr
1199 :param kwargs: forwarded directly to `git.rebase()`
1202 Command
.__init
__(self
)
1204 self
.upstream
= upstream
1205 self
.branch
= branch
1206 self
.capture_output
= capture_output
1207 self
.kwargs
= kwargs
1209 def prepare_arguments(self
):
1213 if self
.capture_output
:
1214 kwargs
['_stderr'] = None
1215 kwargs
['_stdout'] = None
1217 # Rebase actions must be the only option specified
1218 for action
in ('continue', 'abort', 'skip', 'edit_todo'):
1219 if self
.kwargs
.get(action
, False):
1220 kwargs
[action
] = self
.kwargs
[action
]
1223 kwargs
['interactive'] = True
1224 kwargs
['autosquash'] = self
.kwargs
.get('autosquash', True)
1225 kwargs
.update(self
.kwargs
)
1228 args
.append(self
.upstream
)
1230 args
.append(self
.branch
)
1235 (status
, out
, err
) = (1, '', '')
1236 args
, kwargs
= self
.prepare_arguments()
1237 upstream_title
= self
.upstream
or '@{upstream}'
1238 with
GitXBaseContext(
1239 GIT_XBASE_TITLE
=N_('Rebase onto %s') % upstream_title
,
1240 GIT_XBASE_ACTION
=N_('Rebase')):
1241 # XXX this blocks the user interface window for the duration of our
1242 # git-xbase's invocation. would need to implement signals for
1243 # QProcess and continue running the main thread. alternatively we
1244 # could hide the main window while rebasing. that doesn't require
1246 status
, out
, err
= self
.model
.git
.rebase(*args
, _no_win32_startupinfo
=True, **kwargs
)
1247 Interaction
.log_status(status
, out
, err
)
1248 self
.model
.update_status()
1249 return status
, out
, err
1252 class RebaseEditTodo(Command
):
1255 (status
, out
, err
) = (1, '', '')
1256 with
GitXBaseContext(
1257 GIT_XBASE_TITLE
=N_('Edit Rebase'),
1258 GIT_XBASE_ACTION
=N_('Save')):
1259 status
, out
, err
= self
.model
.git
.rebase(edit_todo
=True)
1260 Interaction
.log_status(status
, out
, err
)
1261 self
.model
.update_status()
1262 return status
, out
, err
1265 class RebaseContinue(Command
):
1268 (status
, out
, err
) = (1, '', '')
1269 with
GitXBaseContext(
1270 GIT_XBASE_TITLE
=N_('Rebase'),
1271 GIT_XBASE_ACTION
=N_('Rebase')):
1272 status
, out
, err
= self
.model
.git
.rebase('--continue')
1273 Interaction
.log_status(status
, out
, err
)
1274 self
.model
.update_status()
1275 return status
, out
, err
1278 class RebaseSkip(Command
):
1281 (status
, out
, err
) = (1, '', '')
1282 with
GitXBaseContext(
1283 GIT_XBASE_TITLE
=N_('Rebase'),
1284 GIT_XBASE_ACTION
=N_('Rebase')):
1285 status
, out
, err
= self
.model
.git
.rebase(skip
=True)
1286 Interaction
.log_status(status
, out
, err
)
1287 self
.model
.update_status()
1288 return status
, out
, err
1291 class RebaseAbort(Command
):
1294 status
, out
, err
= self
.model
.git
.rebase(abort
=True)
1295 Interaction
.log_status(status
, out
, err
)
1296 self
.model
.update_status()
1299 class Rescan(Command
):
1300 """Rescan for changes"""
1303 self
.model
.update_status()
1306 class Refresh(Command
):
1307 """Update refs and refresh the index"""
1311 return N_('Refresh')
1314 self
.model
.update_status(update_index
=True)
1315 fsmonitor
.current().refresh()
1318 class RevertEditsCommand(ConfirmAction
):
1321 ConfirmAction
.__init
__(self
)
1322 self
.model
= main
.model()
1323 self
.icon
= icons
.undo()
1325 def ok_to_run(self
):
1326 return self
.model
.undoable()
1328 def checkout_from_head(self
):
1331 def checkout_args(self
):
1333 s
= selection
.selection()
1334 if self
.checkout_from_head():
1335 args
.append(self
.model
.head
)
1347 git
= self
.model
.git
1348 checkout_args
= self
.checkout_args()
1349 return git
.checkout(*checkout_args
)
1352 self
.model
.update_file_status()
1355 class RevertUnstagedEdits(RevertEditsCommand
):
1359 return N_('Revert Unstaged Edits...')
1361 def checkout_from_head(self
):
1362 # If we are amending and a modified file is selected
1363 # then we should include "HEAD^" on the command-line.
1364 selected
= selection
.selection()
1365 return not selected
.staged
and self
.model
.amending()
1368 title
= N_('Revert Unstaged Changes?')
1369 text
= N_('This operation drops unstaged changes.\n'
1370 'These changes cannot be recovered.')
1371 info
= N_('Revert the unstaged changes?')
1372 ok_text
= N_('Revert Unstaged Changes')
1373 return Interaction
.confirm(title
, text
, info
, ok_text
,
1374 default
=True, icon
=self
.icon
)
1377 class RevertUncommittedEdits(RevertEditsCommand
):
1381 return N_('Revert Uncommitted Edits...')
1383 def checkout_from_head(self
):
1387 title
= N_('Revert Uncommitted Changes?')
1388 text
= N_('This operation drops uncommitted changes.\n'
1389 'These changes cannot be recovered.')
1390 info
= N_('Revert the uncommitted changes?')
1391 ok_text
= N_('Revert Uncommitted Changes')
1392 return Interaction
.confirm(title
, text
, info
, ok_text
,
1393 default
=True, icon
=self
.icon
)
1396 class RunConfigAction(Command
):
1397 """Run a user-configured action, typically from the "Tools" menu"""
1399 def __init__(self
, action_name
):
1400 Command
.__init
__(self
)
1401 self
.action_name
= action_name
1402 self
.model
= main
.model()
1405 for env
in ('ARGS', 'DIRNAME', 'FILENAME', 'REVISION'):
1407 compat
.unsetenv(env
)
1412 cfg
= gitcfg
.current()
1413 opts
= cfg
.get_guitool_opts(self
.action_name
)
1414 cmd
= opts
.get('cmd')
1415 if 'title' not in opts
:
1418 if 'prompt' not in opts
or opts
.get('prompt') is True:
1419 prompt
= N_('Run "%s"?') % cmd
1420 opts
['prompt'] = prompt
1422 if opts
.get('needsfile'):
1423 filename
= selection
.filename()
1425 Interaction
.information(
1426 N_('Please select a file'),
1427 N_('"%s" requires a selected file.') % cmd
)
1429 dirname
= utils
.dirname(filename
, current_dir
='.')
1430 compat
.setenv('FILENAME', filename
)
1431 compat
.setenv('DIRNAME', dirname
)
1433 if opts
.get('revprompt') or opts
.get('argprompt'):
1435 ok
= Interaction
.confirm_config_action(cmd
, opts
)
1438 rev
= opts
.get('revision')
1439 args
= opts
.get('args')
1440 if opts
.get('revprompt') and not rev
:
1441 title
= N_('Invalid Revision')
1442 msg
= N_('The revision expression cannot be empty.')
1443 Interaction
.critical(title
, msg
)
1447 elif opts
.get('confirm'):
1448 title
= os
.path
.expandvars(opts
.get('title'))
1449 prompt
= os
.path
.expandvars(opts
.get('prompt'))
1450 if not Interaction
.question(title
, prompt
):
1453 compat
.setenv('REVISION', rev
)
1455 compat
.setenv('ARGS', args
)
1456 title
= os
.path
.expandvars(cmd
)
1457 Interaction
.log(N_('Running command: %s') % title
)
1458 cmd
= ['sh', '-c', cmd
]
1460 if opts
.get('background'):
1462 status
, out
, err
= (0, '', '')
1463 elif opts
.get('noconsole'):
1464 status
, out
, err
= core
.run_command(cmd
)
1466 status
, out
, err
= Interaction
.run_command(title
, cmd
)
1468 Interaction
.log_status(status
,
1469 out
and (N_('Output: %s') % out
) or '',
1470 err
and (N_('Errors: %s') % err
) or '')
1472 if not opts
.get('background') and not opts
.get('norescan'):
1473 self
.model
.update_status()
1477 class SetDefaultRepo(Command
):
1479 def __init__(self
, repo
, name
):
1480 Command
.__init
__(self
)
1485 gitcfg
.current().set_user('cola.defaultrepo', self
.repo
)
1488 class SetDiffText(Command
):
1490 def __init__(self
, text
):
1491 Command
.__init
__(self
)
1492 self
.undoable
= True
1493 self
.new_diff_text
= text
1496 class ShowUntracked(Command
):
1497 """Show an untracked file."""
1499 def __init__(self
, filename
):
1500 Command
.__init
__(self
)
1501 self
.new_filename
= filename
1502 self
.new_mode
= self
.model
.mode_untracked
1503 self
.new_diff_text
= self
.diff_text_for(filename
)
1505 def diff_text_for(self
, filename
):
1506 cfg
= gitcfg
.current()
1507 size
= cfg
.get('cola.readsize', 1024 * 2)
1509 result
= core
.read(filename
, size
=size
,
1510 encoding
='utf-8', errors
='ignore')
1514 if len(result
) == size
:
1519 class SignOff(Command
):
1523 return N_('Sign Off')
1526 Command
.__init
__(self
)
1527 self
.undoable
= True
1528 self
.old_commitmsg
= self
.model
.commitmsg
1531 signoff
= self
.signoff()
1532 if signoff
in self
.model
.commitmsg
:
1534 self
.model
.set_commitmsg(self
.model
.commitmsg
+ '\n' + signoff
)
1537 self
.model
.set_commitmsg(self
.old_commitmsg
)
1542 user
= pwd
.getpwuid(os
.getuid()).pw_name
1544 user
= os
.getenv('USER', N_('unknown'))
1546 cfg
= gitcfg
.current()
1547 name
= cfg
.get('user.name', user
)
1548 email
= cfg
.get('user.email', '%s@%s' % (user
, core
.node()))
1549 return '\nSigned-off-by: %s <%s>' % (name
, email
)
1552 def check_conflicts(unmerged
):
1553 """Check paths for conflicts
1555 Conflicting files can be filtered out one-by-one.
1558 if prefs
.check_conflicts():
1559 unmerged
= [path
for path
in unmerged
if is_conflict_free(path
)]
1563 def is_conflict_free(path
):
1564 """Return True if `path` contains no conflict markers
1566 rgx
= re
.compile(r
'^(<<<<<<<|\|\|\|\|\|\|\||>>>>>>>) ')
1568 with core
.xopen(path
, 'r') as f
:
1570 line
= core
.decode(line
, errors
='ignore')
1572 if should_stage_conflicts(path
):
1577 # We can't read this file ~ we may be staging a removal
1582 def should_stage_conflicts(path
):
1583 """Inform the user that a file contains merge conflicts
1585 Return `True` if we should stage the path nonetheless.
1588 title
= msg
= N_('Stage conflicts?')
1589 info
= N_('%s appears to contain merge conflicts.\n\n'
1590 'You should probably skip this file.\n'
1591 'Stage it anyways?') % path
1592 ok_text
= N_('Stage conflicts')
1593 cancel_text
= N_('Skip')
1594 return Interaction
.confirm(title
, msg
, info
, ok_text
,
1595 default
=False, cancel_text
=cancel_text
)
1598 class Stage(Command
):
1599 """Stage a set of paths."""
1605 def __init__(self
, paths
):
1606 Command
.__init
__(self
)
1610 msg
= N_('Staging: %s') % (', '.join(self
.paths
))
1611 Interaction
.log(msg
)
1612 # Prevent external updates while we are staging files.
1613 # We update file stats at the end of this operation
1614 # so there's no harm in ignoring updates from other threads
1615 # (e.g. the file system change monitor).
1616 with
CommandDisabled(UpdateFileStatus
):
1617 self
.model
.stage_paths(self
.paths
)
1620 class StageCarefully(Stage
):
1621 """Only stage when the path list is non-empty
1623 We use "git add -u -- <pathspec>" to stage, and it stages everything by
1624 default when no pathspec is specified, so this class ensures that paths
1625 are specified before calling git.
1627 When no paths are specified, the command does nothing.
1631 Stage
.__init
__(self
, None)
1634 def init_paths(self
):
1637 def ok_to_run(self
):
1638 """Prevent catch-all "git add -u" from adding unmerged files"""
1639 return self
.paths
or not self
.model
.unmerged
1642 if self
.ok_to_run():
1646 class StageModified(StageCarefully
):
1647 """Stage all modified files."""
1651 return N_('Stage Modified')
1653 def init_paths(self
):
1654 self
.paths
= self
.model
.modified
1657 class StageUnmerged(StageCarefully
):
1658 """Stage unmerged files."""
1662 return N_('Stage Unmerged')
1664 def init_paths(self
):
1665 self
.paths
= check_conflicts(self
.model
.unmerged
)
1668 class StageUntracked(StageCarefully
):
1669 """Stage all untracked files."""
1673 return N_('Stage Untracked')
1675 def init_paths(self
):
1676 self
.paths
= self
.model
.untracked
1679 class StageOrUnstage(Command
):
1680 """If the selection is staged, unstage it, otherwise stage"""
1684 return N_('Stage / Unstage')
1687 s
= selection
.selection()
1689 do(Unstage
, s
.staged
)
1692 unmerged
= check_conflicts(s
.unmerged
)
1694 unstaged
.extend(unmerged
)
1696 unstaged
.extend(s
.modified
)
1698 unstaged
.extend(s
.untracked
)
1704 """Create a tag object."""
1706 def __init__(self
, name
, revision
, sign
=False, message
=''):
1707 Command
.__init
__(self
)
1709 self
._message
= message
1710 self
._revision
= revision
1714 log_msg
= (N_('Tagging "%(revision)s" as "%(name)s"') %
1715 dict(revision
=self
._revision
, name
=self
._name
))
1720 tmp_file
= utils
.tmp_filename('tag-message')
1721 opts
['F'] = tmp_file
1722 core
.write(tmp_file
, self
._message
)
1725 log_msg
+= ' (%s)' % N_('GPG-signed')
1728 opts
['a'] = bool(self
._message
)
1729 status
, output
, err
= self
.model
.git
.tag(self
._name
,
1730 self
._revision
, **opts
)
1733 core
.unlink(tmp_file
)
1736 log_msg
+= '\n' + (N_('Output: %s') % output
)
1738 Interaction
.log_status(status
, log_msg
, err
)
1740 self
.model
.update_status()
1741 return (status
, output
, err
)
1744 class Unstage(Command
):
1745 """Unstage a set of paths."""
1749 return N_('Unstage')
1751 def __init__(self
, paths
):
1752 Command
.__init
__(self
)
1756 msg
= N_('Unstaging: %s') % (', '.join(self
.paths
))
1757 Interaction
.log(msg
)
1758 with
CommandDisabled(UpdateFileStatus
):
1759 self
.model
.unstage_paths(self
.paths
)
1762 class UnstageAll(Command
):
1763 """Unstage all files; resets the index."""
1766 self
.model
.unstage_all()
1769 class UnstageSelected(Unstage
):
1770 """Unstage selected files."""
1773 Unstage
.__init
__(self
, selection
.selection_model().staged
)
1776 class Untrack(Command
):
1777 """Unstage a set of paths."""
1779 def __init__(self
, paths
):
1780 Command
.__init
__(self
)
1784 msg
= N_('Untracking: %s') % (', '.join(self
.paths
))
1785 Interaction
.log(msg
)
1786 with
CommandDisabled(UpdateFileStatus
):
1787 status
, out
, err
= self
.model
.untrack_paths(self
.paths
)
1788 Interaction
.log_status(status
, out
, err
)
1791 class UntrackedSummary(Command
):
1792 """List possible .gitignore rules as the diff text."""
1795 Command
.__init
__(self
)
1796 untracked
= self
.model
.untracked
1797 suffix
= len(untracked
) > 1 and 's' or ''
1799 io
.write('# %s untracked file%s\n' % (len(untracked
), suffix
))
1801 io
.write('# possible .gitignore rule%s:\n' % suffix
)
1803 io
.write('/'+u
+'\n')
1804 self
.new_diff_text
= io
.getvalue()
1805 self
.new_mode
= self
.model
.mode_untracked
1808 class UpdateFileStatus(Command
):
1809 """Rescans for changes."""
1812 self
.model
.update_file_status()
1815 class VisualizeAll(Command
):
1816 """Visualize all branches."""
1819 browser
= utils
.shell_split(prefs
.history_browser())
1820 launch_history_browser(browser
+ ['--all'])
1823 class VisualizeCurrent(Command
):
1824 """Visualize all branches."""
1827 browser
= utils
.shell_split(prefs
.history_browser())
1828 launch_history_browser(browser
+ [self
.model
.currentbranch
])
1831 class VisualizePaths(Command
):
1832 """Path-limited visualization."""
1834 def __init__(self
, paths
):
1835 Command
.__init
__(self
)
1836 browser
= utils
.shell_split(prefs
.history_browser())
1838 self
.argv
= browser
+ list(paths
)
1843 launch_history_browser(self
.argv
)
1846 class VisualizeRevision(Command
):
1847 """Visualize a specific revision."""
1849 def __init__(self
, revision
, paths
=None):
1850 Command
.__init
__(self
)
1851 self
.revision
= revision
1855 argv
= utils
.shell_split(prefs
.history_browser())
1857 argv
.append(self
.revision
)
1860 argv
.extend(self
.paths
)
1861 launch_history_browser(argv
)
1864 def launch_history_browser(argv
):
1867 except Exception as e
:
1868 _
, details
= utils
.format_exception(e
)
1869 title
= N_('Error Launching History Browser')
1870 msg
= (N_('Cannot exec "%s": please configure a history browser') %
1872 Interaction
.critical(title
, message
=msg
, details
=details
)
1875 def run(cls
, *args
, **opts
):
1877 Returns a callback that runs a command
1879 If the caller of run() provides args or opts then those are
1880 used instead of the ones provided by the invoker of the callback.
1883 def runner(*local_args
, **local_opts
):
1885 do(cls
, *args
, **opts
)
1887 do(cls
, *local_args
, **local_opts
)
1892 class CommandDisabled(object):
1894 """Context manager to temporarily disable a command from running"""
1895 def __init__(self
, cmdclass
):
1896 self
.cmdclass
= cmdclass
1898 def __enter__(self
):
1899 self
.cmdclass
.DISABLED
= True
1902 def __exit__(self
, exc_type
, exc_val
, exc_tb
):
1903 self
.cmdclass
.DISABLED
= False
1906 def do(cls
, *args
, **opts
):
1907 """Run a command in-place"""
1908 return do_cmd(cls(*args
, **opts
))
1912 if hasattr(cmd
, 'DISABLED') and cmd
.DISABLED
:
1916 except Exception as e
:
1917 msg
, details
= utils
.format_exception(e
)
1918 Interaction
.critical(N_('Error'), message
=msg
, details
=details
)
1923 """Start a default difftool session"""
1924 files
= selection
.selected_group()
1927 s
= selection
.selection()
1928 model
= main
.model()
1929 difftool_launch_with_head(files
, bool(s
.staged
), model
.head
)
1932 def difftool_launch_with_head(filenames
, staged
, head
):
1933 """Launch difftool against the provided head"""
1938 difftool_launch(left
=left
, staged
=staged
, paths
=filenames
)
1941 def difftool_launch(left
=None, right
=None, paths
=None,
1942 staged
=False, dir_diff
=False,
1943 left_take_magic
=False, left_take_parent
=False):
1944 """Launches 'git difftool' with given parameters
1946 :param left: first argument to difftool
1947 :param right: second argument to difftool_args
1948 :param paths: paths to diff
1949 :param staged: activate `git difftool --staged`
1950 :param dir_diff: activate `git difftool --dir-diff`
1951 :param left_take_magic: whether to append the magic ^! diff expression
1952 :param left_take_parent: whether to append the first-parent ~ for diffing
1956 difftool_args
= ['git', 'difftool', '--no-prompt']
1958 difftool_args
.append('--cached')
1960 difftool_args
.append('--dir-diff')
1963 if left_take_parent
or left_take_magic
:
1964 suffix
= left_take_magic
and '^!' or '~'
1965 # Check root commit (no parents and thus cannot execute '~')
1966 model
= main
.model()
1968 status
, out
, err
= git
.rev_list(left
, parents
=True, n
=1)
1969 Interaction
.log_status(status
, out
, err
)
1971 raise OSError('git rev-list command failed')
1973 if len(out
.split()) >= 2:
1974 # Commit has a parent, so we can take its child as requested
1977 # No parent, assume it's the root commit, so we have to diff
1978 # against the empty tree. Git's empty tree is a built-in
1979 # constant object name.
1980 left
= '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
1981 if not right
and left_take_magic
:
1983 difftool_args
.append(left
)
1986 difftool_args
.append(right
)
1989 difftool_args
.append('--')
1990 difftool_args
.extend(paths
)
1992 core
.fork(difftool_args
)