4 from cStringIO
import StringIO
9 from cola
import gitcfg
10 from cola
import gitcmds
11 from cola
import utils
12 from cola
import signals
13 from cola
import cmdfactory
14 from cola
import difftool
15 from cola
import version
16 from cola
.diffparse
import DiffParser
17 from cola
.models
import selection
19 _notifier
= cola
.notifier()
20 _factory
= cmdfactory
.factory()
21 _config
= gitcfg
.instance()
24 class Command(object):
25 """Base class for all commands; provides the command pattern."""
26 def __init__(self
, update
=False):
27 """Initialize the command and stash away values for use in do()"""
28 # These are commonly used so let's make it easier to write new commands.
30 self
.model
= cola
.model()
33 self
.old_diff_text
= self
.model
.diff_text
34 self
.old_filename
= self
.model
.filename
35 self
.old_mode
= self
.model
.mode
36 self
.old_head
= self
.model
.head
38 self
.new_diff_text
= self
.old_diff_text
39 self
.new_filename
= self
.old_filename
40 self
.new_head
= self
.old_head
41 self
.new_mode
= self
.old_mode
44 """Perform the operation."""
45 self
.model
.set_diff_text(self
.new_diff_text
)
46 self
.model
.set_filename(self
.new_filename
)
47 self
.model
.set_head(self
.new_head
)
48 self
.model
.set_mode(self
.new_mode
)
50 self
.model
.update_status()
52 def is_undoable(self
):
53 """Can this be undone?"""
57 """Undo the operation."""
58 self
.model
.set_diff_text(self
.old_diff_text
)
59 self
.model
.set_filename(self
.old_filename
)
60 self
.model
.set_head(self
.old_head
)
61 self
.model
.set_mode(self
.old_mode
)
63 self
.model
.update_status()
66 """Return this command's name."""
67 return self
.__class
__.__name
__
70 class AddSignoff(Command
):
71 """Add a signed-off-by to the commit message."""
73 Command
.__init
__(self
)
75 self
.old_commitmsg
= self
.model
.commitmsg
76 self
.new_commitmsg
= self
.old_commitmsg
77 signoff
= ('\nSigned-off-by: %s <%s>\n' %
78 (self
.model
.local_user_name
, self
.model
.local_user_email
))
79 if signoff
not in self
.new_commitmsg
:
80 self
.new_commitmsg
+= ('\n' + signoff
)
83 self
.model
.set_commitmsg(self
.new_commitmsg
)
86 self
.model
.set_commitmsg(self
.old_commitmsg
)
89 class AmendMode(Command
):
90 """Try to amend a commit."""
91 def __init__(self
, amend
):
92 Command
.__init
__(self
, update
=True)
96 self
.old_commitmsg
= self
.model
.commitmsg
99 self
.new_mode
= self
.model
.mode_amend
100 self
.new_head
= 'HEAD^'
101 self
.new_commitmsg
= self
.model
.prev_commitmsg()
103 # else, amend unchecked, regular commit
104 self
.new_mode
= self
.model
.mode_none
105 self
.new_head
= 'HEAD'
106 self
.new_commitmsg
= self
.model
.commitmsg
107 # If we're going back into new-commit-mode then search the
108 # undo stack for a previous amend-commit-mode and grab the
109 # commit message at that point in time.
110 if not _factory
.undostack
:
112 undo_count
= len(_factory
.undostack
)
113 for i
in xrange(undo_count
):
114 # Find the latest AmendMode command
115 idx
= undo_count
- i
- 1
116 cmdobj
= _factory
.undostack
[idx
]
117 if type(cmdobj
) is not AmendMode
:
120 self
.new_commitmsg
= cmdobj
.old_commitmsg
124 """Leave/enter amend mode."""
125 """Attempt to enter amend mode. Do not allow this when merging."""
127 if os
.path
.exists(self
.model
.git
.git_path('MERGE_HEAD')):
129 _notifier
.broadcast(signals
.amend
, False)
130 _factory
.prompt_user(signals
.information
,
132 'You are in the middle of a merge.\n'
133 'You cannot amend while merging.')
136 _notifier
.broadcast(signals
.amend
, self
.amending
)
137 self
.model
.set_commitmsg(self
.new_commitmsg
)
143 self
.model
.set_commitmsg(self
.old_commitmsg
)
147 class ApplyDiffSelection(Command
):
148 def __init__(self
, staged
, selected
, offset
, selection
, apply_to_worktree
):
149 Command
.__init
__(self
, update
=True)
151 self
.selected
= selected
153 self
.selection
= selection
154 self
.apply_to_worktree
= apply_to_worktree
157 if self
.model
.mode
== self
.model
.mode_branch
:
158 # We're applying changes from a different branch!
159 parser
= DiffParser(self
.model
,
160 filename
=self
.model
.filename
,
162 branch
=self
.model
.head
)
163 parser
.process_diff_selection(self
.selected
,
166 apply_to_worktree
=True)
168 # The normal worktree vs index scenario
169 parser
= DiffParser(self
.model
,
170 filename
=self
.model
.filename
,
172 reverse
=self
.apply_to_worktree
)
173 parser
.process_diff_selection(self
.selected
,
177 self
.apply_to_worktree
)
178 # Redo the diff to show changes
180 diffcmd
= DiffStaged([self
.model
.filename
])
182 diffcmd
= Diff([self
.model
.filename
])
184 self
.model
.update_status()
186 class ApplyPatches(Command
):
187 def __init__(self
, patches
):
188 Command
.__init
__(self
)
190 self
.patches
= patches
194 num_patches
= len(self
.patches
)
195 orig_head
= self
.model
.git
.rev_parse('HEAD')
197 for idx
, patch
in enumerate(self
.patches
):
198 status
, output
= self
.model
.git
.am(patch
,
201 # Log the git-am command
202 _notifier
.broadcast(signals
.log_cmd
, status
, output
)
205 diff
= self
.model
.git
.diff('HEAD^!', stat
=True)
206 diff_text
+= 'Patch %d/%d - ' % (idx
+1, num_patches
)
207 diff_text
+= '%s:\n%s\n\n' % (os
.path
.basename(patch
), diff
)
209 diff_text
+= 'Summary:\n'
210 diff_text
+= self
.model
.git
.diff(orig_head
, stat
=True)
213 self
.model
.set_diff_text(diff_text
)
215 _factory
.prompt_user(signals
.information
,
217 '%d patch(es) applied:\n\n%s' %
219 '\n'.join(map(os
.path
.basename
, self
.patches
))))
222 class HeadChangeCommand(Command
):
223 """Changes the model's current head."""
224 def __init__(self
, treeish
):
225 Command
.__init
__(self
, update
=True)
226 self
.new_head
= treeish
227 self
.new_diff_text
= ''
230 class BranchMode(HeadChangeCommand
):
231 """Enter into diff-branch mode."""
232 def __init__(self
, treeish
, filename
):
233 HeadChangeCommand
.__init
__(self
, treeish
)
234 self
.old_filename
= self
.model
.filename
235 self
.new_filename
= filename
236 self
.new_mode
= self
.model
.mode_branch
237 self
.new_diff_text
= gitcmds
.diff_helper(filename
=filename
,
241 class Checkout(Command
):
243 A command object for git-checkout.
245 'argv' is handed off directly to git.
248 def __init__(self
, argv
):
249 Command
.__init
__(self
)
253 status
, output
= self
.model
.git
.checkout(with_stderr
=True,
254 with_status
=True, *self
.argv
)
255 _notifier
.broadcast(signals
.log_cmd
, status
, output
)
256 self
.model
.set_diff_text('')
257 self
.model
.update_status()
260 class CheckoutBranch(Checkout
):
261 """Checkout a branch."""
262 def __init__(self
, branch
):
263 Checkout
.__init
__(self
, [branch
])
266 class CherryPick(Command
):
267 """Cherry pick commits into the current branch."""
268 def __init__(self
, commits
):
269 Command
.__init
__(self
)
270 self
.commits
= commits
273 self
.model
.cherry_pick_list(self
.commits
)
276 class ResetMode(Command
):
277 """Reset the mode and clear the model's diff text."""
279 Command
.__init
__(self
, update
=True)
280 self
.new_mode
= self
.model
.mode_none
281 self
.new_head
= 'HEAD'
282 self
.new_diff_text
= ''
285 class Commit(ResetMode
):
286 """Attempt to create a new commit."""
287 def __init__(self
, amend
, msg
):
288 ResetMode
.__init
__(self
)
290 self
.msg
= core
.encode(msg
)
291 self
.old_commitmsg
= self
.model
.commitmsg
292 self
.new_commitmsg
= ''
295 status
, output
= self
.model
.commit_with_msg(self
.msg
, amend
=self
.amend
)
298 self
.model
.set_commitmsg(self
.new_commitmsg
)
301 title
= 'Commit failed: '
302 _notifier
.broadcast(signals
.log_cmd
, status
, title
+output
)
305 class Delete(Command
):
306 """Simply delete files."""
307 def __init__(self
, filenames
):
308 Command
.__init
__(self
)
309 self
.filenames
= filenames
310 # We could git-hash-object stuff and provide undo-ability
314 for filename
in self
.filenames
:
320 _factory
.prompt_user(signals
.information
,
322 'Deleting "%s" failed.' % filename
)
324 self
.model
.update_status()
326 class DeleteBranch(Command
):
327 """Delete a git branch."""
328 def __init__(self
, branch
):
329 Command
.__init
__(self
)
333 status
, output
= self
.model
.delete_branch(self
.branch
)
335 if output
.startswith('error:'):
336 output
= 'E' + output
[1:]
339 _notifier
.broadcast(signals
.log_cmd
, status
, title
+ output
)
343 """Perform a diff and set the model's current text."""
344 def __init__(self
, filenames
, cached
=False):
345 Command
.__init
__(self
)
348 cached
= not self
.model
.read_only()
349 opts
= dict(ref
=self
.model
.head
)
351 self
.new_filename
= filenames
[0]
352 self
.old_filename
= self
.model
.filename
353 if not self
.model
.read_only():
354 if self
.model
.mode
!= self
.model
.mode_amend
:
355 self
.new_mode
= self
.model
.mode_worktree
356 self
.new_diff_text
= gitcmds
.diff_helper(filename
=self
.new_filename
,
357 cached
=cached
, **opts
)
360 class DiffMode(HeadChangeCommand
):
361 """Enter diff mode and clear the model's diff text."""
362 def __init__(self
, treeish
):
363 HeadChangeCommand
.__init
__(self
, treeish
)
364 self
.new_mode
= self
.model
.mode_diff
367 class DiffExprMode(HeadChangeCommand
):
368 """Enter diff-expr mode and clear the model's diff text."""
369 def __init__(self
, treeish
):
370 HeadChangeCommand
.__init
__(self
, treeish
)
371 self
.new_mode
= self
.model
.mode_diff_expr
374 class Diffstat(Command
):
375 """Perform a diffstat and set the model's diff text."""
377 Command
.__init
__(self
)
378 diff
= self
.model
.git
.diff(self
.model
.head
,
379 unified
=_config
.get('diff.context', 3),
383 self
.new_diff_text
= core
.decode(diff
)
384 self
.new_mode
= self
.model
.mode_worktree
387 class DiffStaged(Diff
):
388 """Perform a staged diff on a file."""
389 def __init__(self
, filenames
):
390 Diff
.__init
__(self
, filenames
, cached
=True)
391 if not self
.model
.read_only():
392 if self
.model
.mode
!= self
.model
.mode_amend
:
393 self
.new_mode
= self
.model
.mode_index
396 class DiffStagedSummary(Command
):
398 Command
.__init
__(self
)
399 cached
= not self
.model
.read_only()
400 diff
= self
.model
.git
.diff(self
.model
.head
,
403 patch_with_stat
=True,
405 self
.new_diff_text
= core
.decode(diff
)
406 if not self
.model
.read_only():
407 if self
.model
.mode
!= self
.model
.mode_amend
:
408 self
.new_mode
= self
.model
.mode_index
411 class Difftool(Command
):
412 """Run git-difftool limited by path."""
413 def __init__(self
, staged
, filenames
):
414 Command
.__init
__(self
)
416 self
.filenames
= filenames
419 if not self
.filenames
:
422 if self
.staged
and not self
.model
.read_only():
423 args
.append('--cached')
424 args
.extend([self
.model
.head
, '--'])
425 args
.extend(self
.filenames
)
426 difftool
.launch(args
)
430 """Edit a file using the configured gui.editor."""
431 def __init__(self
, filenames
, line_number
=None):
432 Command
.__init
__(self
)
433 self
.filenames
= filenames
434 self
.line_number
= line_number
437 filename
= self
.filenames
[0]
438 if not os
.path
.exists(filename
):
440 editor
= self
.model
.editor()
441 if 'vi' in editor
and self
.line_number
:
442 utils
.fork([editor
, filename
, '+'+self
.line_number
])
444 utils
.fork([editor
, filename
])
447 class FormatPatch(Command
):
448 """Output a patch series given all revisions and a selected subset."""
449 def __init__(self
, to_export
, revs
):
450 Command
.__init
__(self
)
451 self
.to_export
= to_export
455 status
, output
= gitcmds
.format_patchsets(self
.to_export
, self
.revs
)
456 _notifier
.broadcast(signals
.log_cmd
, status
, output
)
459 class GrepMode(Command
):
460 def __init__(self
, txt
):
461 """Perform a git-grep."""
462 Command
.__init
__(self
)
463 self
.new_mode
= self
.model
.mode_grep
464 self
.new_diff_text
= self
.model
.git
.grep(txt
, n
=True)
466 class LoadCommitMessage(Command
):
467 """Loads a commit message from a path."""
468 def __init__(self
, path
):
469 Command
.__init
__(self
)
470 if not path
or not os
.path
.exists(path
):
471 raise OSError('error: "%s" does not exist' % path
)
474 self
.old_commitmsg
= self
.model
.commitmsg
475 self
.old_directory
= self
.model
.directory
478 self
.model
.set_directory(os
.path
.dirname(self
.path
))
479 self
.model
.set_commitmsg(utils
.slurp(self
.path
))
482 self
.model
.set_commitmsg(self
.old_commitmsg
)
483 self
.model
.set_directory(self
.old_directory
)
486 class LoadCommitTemplate(LoadCommitMessage
):
487 """Loads the commit message template specified by commit.template."""
489 LoadCommitMessage
.__init
__(self
, _config
.get('commit.template'))
492 class Mergetool(Command
):
493 """Launch git-mergetool on a list of paths."""
494 def __init__(self
, paths
):
495 Command
.__init
__(self
)
501 if version
.check('mergetool-no-prompt',
502 self
.model
.git
.version().split()[-1]):
503 utils
.fork(['git', 'mergetool', '--no-prompt', '--'] + self
.paths
)
505 utils
.fork(['xterm', '-e', 'git', 'mergetool', '--'] + self
.paths
)
508 class OpenRepo(Command
):
509 """Launches git-cola on a repo."""
510 def __init__(self
, dirname
):
511 Command
.__init
__(self
)
512 self
.new_directory
= utils
.quote_repopath(dirname
)
515 self
.model
.set_directory(self
.new_directory
)
516 utils
.fork(['python', sys
.argv
[0], '--repo', self
.new_directory
])
519 class Clone(Command
):
520 """Clones a repository and optionally spawns a new cola session."""
521 def __init__(self
, url
, destdir
, spawn
=True):
522 Command
.__init
__(self
)
524 self
.new_directory
= utils
.quote_repopath(destdir
)
528 self
.model
.git
.clone(self
.url
, self
.new_directory
,
529 with_stderr
=True, with_status
=True)
531 utils
.fork(['python', sys
.argv
[0], '--repo', self
.new_directory
])
534 class Rescan(Command
):
535 """Rescans for changes."""
537 Command
.__init
__(self
, update
=True)
540 class ReviewBranchMode(Command
):
541 """Enter into review-branch mode."""
542 def __init__(self
, branch
):
543 Command
.__init
__(self
, update
=True)
544 self
.new_mode
= self
.model
.mode_review
545 self
.new_head
= gitcmds
.merge_base_parent(branch
)
546 self
.new_diff_text
= ''
549 class RunConfigAction(Command
):
550 """Run a user-configured action, typically from the "Tools" menu"""
551 def __init__(self
, name
):
552 Command
.__init
__(self
)
554 self
.model
= cola
.model()
557 for env
in ('FILENAME', 'REVISION', 'ARGS'):
564 opts
= _config
.get_guitool_opts(self
.name
)
565 cmd
= opts
.get('cmd')
566 if 'title' not in opts
:
568 title
= opts
.get('title')
570 if 'prompt' not in opts
or opts
.get('prompt') is True:
571 prompt
= i18n
.gettext('Are you sure you want to run %s?') % cmd
572 opts
['prompt'] = prompt
574 if opts
.get('needsfile'):
575 filename
= selection
.filename()
577 _factory
.prompt_user(signals
.information
,
578 'Please select a file',
579 '"%s" requires a selected file' % cmd
)
581 os
.environ
['FILENAME'] = utils
.shell_quote(filename
)
583 if opts
.get('revprompt') or opts
.get('argprompt'):
585 ok
= _factory
.prompt_user(signals
.run_config_action
, cmd
, opts
)
588 rev
= opts
.get('revision')
589 args
= opts
.get('args')
590 if opts
.get('revprompt') and not rev
:
591 msg
= ('Invalid revision:\n\n'
592 'Revision expression is empty')
593 _factory
.prompt_user(signals
.information
, title
, msg
)
597 elif opts
.get('confirm'):
598 title
= os
.path
.expandvars(opts
.get('title'))
599 prompt
= os
.path
.expadnvars(opts
.get('prompt'))
600 if not _factory
.prompt_user(signals
.question
, title
, prompt
):
603 os
.environ
['REVISION'] = rev
605 os
.environ
['ARGS'] = args
606 cmdexpand
= os
.path
.expandvars(cmd
)
607 status
= os
.system(cmdexpand
)
608 _notifier
.broadcast(signals
.log_cmd
, status
, 'Running: ' + cmdexpand
)
609 if not opts
.get('norescan'):
610 self
.model
.update_status()
614 class ShowUntracked(Command
):
615 """Show an untracked file."""
616 # We don't actually do anything other than set the mode right now.
617 # TODO check the mimetype for the file and handle things
619 def __init__(self
, filenames
):
620 Command
.__init
__(self
)
621 self
.new_mode
= self
.model
.mode_worktree
622 # TODO new_diff_text = utils.file_preview(filenames[0])
625 class Stage(Command
):
626 """Stage a set of paths."""
627 def __init__(self
, paths
):
628 Command
.__init
__(self
)
632 msg
= 'Staging: %s' % (', '.join(self
.paths
))
633 _notifier
.broadcast(signals
.log_cmd
, 0, msg
)
634 self
.model
.stage_paths(self
.paths
)
637 class StageModified(Stage
):
638 """Stage all modified files."""
640 Stage
.__init
__(self
, None)
641 self
.paths
= self
.model
.modified
644 class StageUntracked(Stage
):
645 """Stage all untracked files."""
647 Stage
.__init
__(self
, None)
648 self
.paths
= self
.model
.untracked
651 """Create a tag object."""
652 def __init__(self
, name
, revision
, sign
=False, message
=''):
653 Command
.__init
__(self
)
655 self
._message
= core
.encode(message
)
656 self
._revision
= revision
660 log_msg
= 'Tagging: "%s" as "%s"' % (self
._revision
, self
._name
)
662 log_msg
+= ', GPG-signed'
663 path
= self
.model
.tmp_filename()
664 utils
.write(path
, self
._message
)
665 status
, output
= self
.model
.git
.tag(self
._name
,
673 status
, output
= self
.model
.git
.tag(self
._name
,
678 log_msg
+= '\nOutput:\n%s' % output
680 _notifier
.broadcast(signals
.log_cmd
, status
, log_msg
)
682 self
.model
.update_status()
685 class Unstage(Command
):
686 """Unstage a set of paths."""
687 def __init__(self
, paths
):
688 Command
.__init
__(self
)
692 msg
= 'Unstaging: %s' % (', '.join(self
.paths
))
693 _notifier
.broadcast(signals
.log_cmd
, 0, msg
)
694 gitcmds
.unstage_paths(self
.paths
)
695 self
.model
.update_status()
698 class UnstageAll(Command
):
699 """Unstage all files; resets the index."""
701 Command
.__init
__(self
, update
=True)
704 self
.model
.unstage_all()
707 class UnstageSelected(Unstage
):
708 """Unstage selected files."""
710 Unstage
.__init
__(self
, cola
.selection_model().staged
)
713 class UntrackedSummary(Command
):
714 """List possible .gitignore rules as the diff text."""
716 Command
.__init
__(self
)
717 untracked
= self
.model
.untracked
718 suffix
= len(untracked
) > 1 and 's' or ''
720 io
.write('# %s untracked file%s\n' % (len(untracked
), suffix
))
722 io
.write('# possible .gitignore rule%s:\n' % suffix
)
724 io
.write('/%s\n' % u
)
725 self
.new_diff_text
= io
.getvalue()
728 class VisualizeAll(Command
):
729 """Visualize all branches."""
731 browser
= self
.model
.history_browser()
732 utils
.fork([browser
, '--all'])
735 class VisualizeCurrent(Command
):
736 """Visualize all branches."""
738 browser
= self
.model
.history_browser()
739 utils
.fork([browser
, self
.model
.currentbranch
])
742 class VisualizePaths(Command
):
743 """Path-limited visualization."""
744 def __init__(self
, paths
):
745 Command
.__init
__(self
)
746 browser
= self
.model
.history_browser()
748 self
.argv
= [browser
] + paths
750 self
.argv
= [browser
]
753 utils
.fork(self
.argv
)
758 Register signal mappings with the factory.
760 These commands are automatically created and run when
761 their corresponding signal is broadcast by the notifier.
764 signal_to_command_map
= {
765 signals
.add_signoff
: AddSignoff
,
766 signals
.amend_mode
: AmendMode
,
767 signals
.apply_diff_selection
: ApplyDiffSelection
,
768 signals
.apply_patches
: ApplyPatches
,
769 signals
.branch_mode
: BranchMode
,
770 signals
.clone
: Clone
,
771 signals
.checkout
: Checkout
,
772 signals
.checkout_branch
: CheckoutBranch
,
773 signals
.cherry_pick
: CherryPick
,
774 signals
.commit
: Commit
,
775 signals
.delete
: Delete
,
776 signals
.delete_branch
: DeleteBranch
,
778 signals
.diff_mode
: DiffMode
,
779 signals
.diff_expr_mode
: DiffExprMode
,
780 signals
.diff_staged
: DiffStaged
,
781 signals
.diffstat
: Diffstat
,
782 signals
.difftool
: Difftool
,
784 signals
.format_patch
: FormatPatch
,
785 signals
.grep
: GrepMode
,
786 signals
.load_commit_message
: LoadCommitMessage
,
787 signals
.load_commit_template
: LoadCommitTemplate
,
788 signals
.modified_summary
: Diffstat
,
789 signals
.mergetool
: Mergetool
,
790 signals
.open_repo
: OpenRepo
,
791 signals
.rescan
: Rescan
,
792 signals
.reset_mode
: ResetMode
,
793 signals
.review_branch_mode
: ReviewBranchMode
,
794 signals
.run_config_action
: RunConfigAction
,
795 signals
.show_untracked
: ShowUntracked
,
796 signals
.stage
: Stage
,
797 signals
.stage_modified
: StageModified
,
798 signals
.stage_untracked
: StageUntracked
,
799 signals
.staged_summary
: DiffStagedSummary
,
801 signals
.unstage
: Unstage
,
802 signals
.unstage_all
: UnstageAll
,
803 signals
.unstage_selected
: UnstageSelected
,
804 signals
.untracked_summary
: UntrackedSummary
,
805 signals
.visualize_all
: VisualizeAll
,
806 signals
.visualize_current
: VisualizeCurrent
,
807 signals
.visualize_paths
: VisualizePaths
,
810 for signal
, cmd
in signal_to_command_map
.iteritems():
811 _factory
.add_command(signal
, cmd
)