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
:
569 if 'prompt' not in opts
:
570 prompt
= i18n
.gettext('Are you sure you want to run %s?') % cmd
571 opts
['prompt'] = prompt
573 if opts
.get('needsfile'):
574 filename
= selection
.filename()
576 _notifier
.broadcast(signals
.information
,
577 'Please select a file',
578 '"%s" requires a selected file' % cmd
)
580 os
.environ
['FILENAME'] = utils
.shell_quote(filename
)
582 if opts
.get('revprompt') or opts
.get('argprompt'):
583 ok
= _factory
.prompt_user(signals
.run_config_action
, cmd
, opts
)
586 rev
= opts
.get('revision')
587 args
= opts
.get('args')
588 if opts
.get('revprompt') and not rev
:
591 elif opts
.get('confirm'):
592 title
= opts
.get('title')
593 prompt
= opts
.get('prompt')
594 title
= os
.path
.expandvars(title
)
595 prompt
= os
.path
.expandvars(prompt
)
596 if not _factory
.prompt_user(signals
.question
, title
, prompt
):
599 os
.environ
['REVISION'] = rev
601 os
.environ
['ARGS'] = args
602 cmdexpand
= os
.path
.expandvars(cmd
)
603 status
= os
.system(cmdexpand
)
604 _notifier
.broadcast(signals
.log_cmd
, status
, 'Running: ' + cmdexpand
)
605 if not opts
.get('norescan'):
606 self
.model
.update_status()
610 class ShowUntracked(Command
):
611 """Show an untracked file."""
612 # We don't actually do anything other than set the mode right now.
613 # TODO check the mimetype for the file and handle things
615 def __init__(self
, filenames
):
616 Command
.__init
__(self
)
617 self
.new_mode
= self
.model
.mode_worktree
618 # TODO new_diff_text = utils.file_preview(filenames[0])
621 class Stage(Command
):
622 """Stage a set of paths."""
623 def __init__(self
, paths
):
624 Command
.__init
__(self
)
628 msg
= 'Staging: %s' % (', '.join(self
.paths
))
629 _notifier
.broadcast(signals
.log_cmd
, 0, msg
)
630 self
.model
.stage_paths(self
.paths
)
633 class StageModified(Stage
):
634 """Stage all modified files."""
636 Stage
.__init
__(self
, None)
637 self
.paths
= self
.model
.modified
640 class StageUntracked(Stage
):
641 """Stage all untracked files."""
643 Stage
.__init
__(self
, None)
644 self
.paths
= self
.model
.untracked
647 """Create a tag object."""
648 def __init__(self
, name
, revision
, sign
=False, message
=''):
649 Command
.__init
__(self
)
651 self
._message
= core
.encode(message
)
652 self
._revision
= revision
656 log_msg
= 'Tagging: "%s" as "%s"' % (self
._revision
, self
._name
)
658 log_msg
+= ', GPG-signed'
659 path
= self
.model
.tmp_filename()
660 utils
.write(path
, self
._message
)
661 status
, output
= self
.model
.git
.tag(self
._name
,
669 status
, output
= self
.model
.git
.tag(self
._name
,
674 log_msg
+= '\nOutput:\n%s' % output
676 _notifier
.broadcast(signals
.log_cmd
, status
, log_msg
)
678 self
.model
.update_status()
681 class Unstage(Command
):
682 """Unstage a set of paths."""
683 def __init__(self
, paths
):
684 Command
.__init
__(self
)
688 msg
= 'Unstaging: %s' % (', '.join(self
.paths
))
689 _notifier
.broadcast(signals
.log_cmd
, 0, msg
)
690 gitcmds
.unstage_paths(self
.paths
)
691 self
.model
.update_status()
694 class UnstageAll(Command
):
695 """Unstage all files; resets the index."""
697 Command
.__init
__(self
, update
=True)
700 self
.model
.unstage_all()
703 class UnstageSelected(Unstage
):
704 """Unstage selected files."""
706 Unstage
.__init
__(self
, cola
.selection_model().staged
)
709 class UntrackedSummary(Command
):
710 """List possible .gitignore rules as the diff text."""
712 Command
.__init
__(self
)
713 untracked
= self
.model
.untracked
714 suffix
= len(untracked
) > 1 and 's' or ''
716 io
.write('# %s untracked file%s\n' % (len(untracked
), suffix
))
718 io
.write('# possible .gitignore rule%s:\n' % suffix
)
720 io
.write('/%s\n' % u
)
721 self
.new_diff_text
= io
.getvalue()
724 class VisualizeAll(Command
):
725 """Visualize all branches."""
727 browser
= self
.model
.history_browser()
728 utils
.fork([browser
, '--all'])
731 class VisualizeCurrent(Command
):
732 """Visualize all branches."""
734 browser
= self
.model
.history_browser()
735 utils
.fork([browser
, self
.model
.currentbranch
])
738 class VisualizePaths(Command
):
739 """Path-limited visualization."""
740 def __init__(self
, paths
):
741 Command
.__init
__(self
)
742 browser
= self
.model
.history_browser()
744 self
.argv
= [browser
] + paths
746 self
.argv
= [browser
]
749 utils
.fork(self
.argv
)
754 Register signal mappings with the factory.
756 These commands are automatically created and run when
757 their corresponding signal is broadcast by the notifier.
760 signal_to_command_map
= {
761 signals
.add_signoff
: AddSignoff
,
762 signals
.amend_mode
: AmendMode
,
763 signals
.apply_diff_selection
: ApplyDiffSelection
,
764 signals
.apply_patches
: ApplyPatches
,
765 signals
.branch_mode
: BranchMode
,
766 signals
.clone
: Clone
,
767 signals
.checkout
: Checkout
,
768 signals
.checkout_branch
: CheckoutBranch
,
769 signals
.cherry_pick
: CherryPick
,
770 signals
.commit
: Commit
,
771 signals
.delete
: Delete
,
772 signals
.delete_branch
: DeleteBranch
,
774 signals
.diff_mode
: DiffMode
,
775 signals
.diff_expr_mode
: DiffExprMode
,
776 signals
.diff_staged
: DiffStaged
,
777 signals
.diffstat
: Diffstat
,
778 signals
.difftool
: Difftool
,
780 signals
.format_patch
: FormatPatch
,
781 signals
.grep
: GrepMode
,
782 signals
.load_commit_message
: LoadCommitMessage
,
783 signals
.load_commit_template
: LoadCommitTemplate
,
784 signals
.modified_summary
: Diffstat
,
785 signals
.mergetool
: Mergetool
,
786 signals
.open_repo
: OpenRepo
,
787 signals
.rescan
: Rescan
,
788 signals
.reset_mode
: ResetMode
,
789 signals
.review_branch_mode
: ReviewBranchMode
,
790 signals
.run_config_action
: RunConfigAction
,
791 signals
.show_untracked
: ShowUntracked
,
792 signals
.stage
: Stage
,
793 signals
.stage_modified
: StageModified
,
794 signals
.stage_untracked
: StageUntracked
,
795 signals
.staged_summary
: DiffStagedSummary
,
797 signals
.unstage
: Unstage
,
798 signals
.unstage_all
: UnstageAll
,
799 signals
.unstage_selected
: UnstageSelected
,
800 signals
.untracked_summary
: UntrackedSummary
,
801 signals
.visualize_all
: VisualizeAll
,
802 signals
.visualize_current
: VisualizeCurrent
,
803 signals
.visualize_paths
: VisualizePaths
,
806 for signal
, cmd
in signal_to_command_map
.iteritems():
807 _factory
.add_command(signal
, cmd
)