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 if self
.model
.head
!= 'HEAD':
425 args
.append(self
.model
.head
)
427 args
.extend(self
.filenames
)
428 difftool
.launch(args
)
432 """Edit a file using the configured gui.editor."""
433 def __init__(self
, filenames
, line_number
=None):
434 Command
.__init
__(self
)
435 self
.filenames
= filenames
436 self
.line_number
= line_number
439 filename
= self
.filenames
[0]
440 if not os
.path
.exists(filename
):
442 editor
= self
.model
.editor()
443 if 'vi' in editor
and self
.line_number
:
444 utils
.fork([editor
, filename
, '+'+self
.line_number
])
446 utils
.fork([editor
, filename
])
449 class FormatPatch(Command
):
450 """Output a patch series given all revisions and a selected subset."""
451 def __init__(self
, to_export
, revs
):
452 Command
.__init
__(self
)
453 self
.to_export
= to_export
457 status
, output
= gitcmds
.format_patchsets(self
.to_export
, self
.revs
)
458 _notifier
.broadcast(signals
.log_cmd
, status
, output
)
461 class GrepMode(Command
):
462 def __init__(self
, txt
):
463 """Perform a git-grep."""
464 Command
.__init
__(self
)
465 self
.new_mode
= self
.model
.mode_grep
466 self
.new_diff_text
= self
.model
.git
.grep(txt
, n
=True)
468 class LoadCommitMessage(Command
):
469 """Loads a commit message from a path."""
470 def __init__(self
, path
):
471 Command
.__init
__(self
)
472 if not path
or not os
.path
.exists(path
):
473 raise OSError('error: "%s" does not exist' % path
)
476 self
.old_commitmsg
= self
.model
.commitmsg
477 self
.old_directory
= self
.model
.directory
480 self
.model
.set_directory(os
.path
.dirname(self
.path
))
481 self
.model
.set_commitmsg(utils
.slurp(self
.path
))
484 self
.model
.set_commitmsg(self
.old_commitmsg
)
485 self
.model
.set_directory(self
.old_directory
)
488 class LoadCommitTemplate(LoadCommitMessage
):
489 """Loads the commit message template specified by commit.template."""
491 LoadCommitMessage
.__init
__(self
, _config
.get('commit.template'))
494 class Mergetool(Command
):
495 """Launch git-mergetool on a list of paths."""
496 def __init__(self
, paths
):
497 Command
.__init
__(self
)
503 if version
.check('mergetool-no-prompt',
504 self
.model
.git
.version().split()[-1]):
505 utils
.fork(['git', 'mergetool', '--no-prompt', '--'] + self
.paths
)
507 utils
.fork(['xterm', '-e', 'git', 'mergetool', '--'] + self
.paths
)
510 class OpenRepo(Command
):
511 """Launches git-cola on a repo."""
512 def __init__(self
, dirname
):
513 Command
.__init
__(self
)
514 self
.new_directory
= utils
.quote_repopath(dirname
)
517 self
.model
.set_directory(self
.new_directory
)
518 utils
.fork(['python', sys
.argv
[0], '--repo', self
.new_directory
])
521 class Clone(Command
):
522 """Clones a repository and optionally spawns a new cola session."""
523 def __init__(self
, url
, destdir
, spawn
=True):
524 Command
.__init
__(self
)
526 self
.new_directory
= utils
.quote_repopath(destdir
)
530 self
.model
.git
.clone(self
.url
, self
.new_directory
,
531 with_stderr
=True, with_status
=True)
533 utils
.fork(['python', sys
.argv
[0], '--repo', self
.new_directory
])
536 class Rescan(Command
):
537 """Rescans for changes."""
539 Command
.__init
__(self
, update
=True)
542 class ReviewBranchMode(Command
):
543 """Enter into review-branch mode."""
544 def __init__(self
, branch
):
545 Command
.__init
__(self
, update
=True)
546 self
.new_mode
= self
.model
.mode_review
547 self
.new_head
= gitcmds
.merge_base_parent(branch
)
548 self
.new_diff_text
= ''
551 class RunConfigAction(Command
):
552 """Run a user-configured action, typically from the "Tools" menu"""
553 def __init__(self
, name
):
554 Command
.__init
__(self
)
556 self
.model
= cola
.model()
559 for env
in ('FILENAME', 'REVISION', 'ARGS'):
566 opts
= _config
.get_guitool_opts(self
.name
)
567 cmd
= opts
.get('cmd')
568 if 'title' not in opts
:
571 if 'prompt' not in opts
or opts
.get('prompt') is True:
572 prompt
= i18n
.gettext('Are you sure you want to run %s?') % cmd
573 opts
['prompt'] = prompt
575 if opts
.get('needsfile'):
576 filename
= selection
.filename()
578 _factory
.prompt_user(signals
.information
,
579 'Please select a file',
580 '"%s" requires a selected file' % cmd
)
582 os
.environ
['FILENAME'] = utils
.shell_quote(filename
)
585 if opts
.get('revprompt') or opts
.get('argprompt'):
587 ok
= _factory
.prompt_user(signals
.run_config_action
, cmd
, opts
)
590 rev
= opts
.get('revision')
591 args
= opts
.get('args')
592 if opts
.get('revprompt') and not rev
:
593 msg
= ('Invalid revision:\n\n'
594 'Revision expression is empty')
596 _factory
.prompt_user(signals
.information
, title
, msg
)
600 elif opts
.get('confirm'):
601 title
= os
.path
.expandvars(opts
.get('title'))
602 prompt
= os
.path
.expandvars(opts
.get('prompt'))
603 if not _factory
.prompt_user(signals
.question
, title
, prompt
):
606 os
.environ
['REVISION'] = rev
608 os
.environ
['ARGS'] = args
609 title
= os
.path
.expandvars(cmd
)
610 _notifier
.broadcast(signals
.log_cmd
, 0, 'running: ' + title
)
611 cmd
= ['sh', '-c', cmd
]
613 if opts
.get('noconsole'):
614 status
, out
, err
= utils
.run_command(cmd
, flag_error
=False)
616 status
, out
, err
= _factory
.prompt_user(signals
.run_command
,
619 _notifier
.broadcast(signals
.log_cmd
, status
,
620 'stdout: %s\nstatus: %s\nstderr: %s' %
621 (out
.rstrip(), status
, err
.rstrip()))
623 if not opts
.get('norescan'):
624 self
.model
.update_status()
628 class ShowUntracked(Command
):
629 """Show an untracked file."""
630 # We don't actually do anything other than set the mode right now.
631 # TODO check the mimetype for the file and handle things
633 def __init__(self
, filenames
):
634 Command
.__init
__(self
)
635 self
.new_mode
= self
.model
.mode_worktree
636 # TODO new_diff_text = utils.file_preview(filenames[0])
639 class Stage(Command
):
640 """Stage a set of paths."""
641 def __init__(self
, paths
):
642 Command
.__init
__(self
)
646 msg
= 'Staging: %s' % (', '.join(self
.paths
))
647 _notifier
.broadcast(signals
.log_cmd
, 0, msg
)
648 self
.model
.stage_paths(self
.paths
)
651 class StageModified(Stage
):
652 """Stage all modified files."""
654 Stage
.__init
__(self
, None)
655 self
.paths
= self
.model
.modified
658 class StageUntracked(Stage
):
659 """Stage all untracked files."""
661 Stage
.__init
__(self
, None)
662 self
.paths
= self
.model
.untracked
665 """Create a tag object."""
666 def __init__(self
, name
, revision
, sign
=False, message
=''):
667 Command
.__init
__(self
)
669 self
._message
= core
.encode(message
)
670 self
._revision
= revision
674 log_msg
= 'Tagging: "%s" as "%s"' % (self
._revision
, self
._name
)
676 log_msg
+= ', GPG-signed'
677 path
= self
.model
.tmp_filename()
678 utils
.write(path
, self
._message
)
679 status
, output
= self
.model
.git
.tag(self
._name
,
687 status
, output
= self
.model
.git
.tag(self
._name
,
692 log_msg
+= '\nOutput:\n%s' % output
694 _notifier
.broadcast(signals
.log_cmd
, status
, log_msg
)
696 self
.model
.update_status()
699 class Unstage(Command
):
700 """Unstage a set of paths."""
701 def __init__(self
, paths
):
702 Command
.__init
__(self
)
706 msg
= 'Unstaging: %s' % (', '.join(self
.paths
))
707 _notifier
.broadcast(signals
.log_cmd
, 0, msg
)
708 self
.model
.unstage_paths(self
.paths
)
711 class UnstageAll(Command
):
712 """Unstage all files; resets the index."""
714 Command
.__init
__(self
, update
=True)
717 self
.model
.unstage_all()
720 class UnstageSelected(Unstage
):
721 """Unstage selected files."""
723 Unstage
.__init
__(self
, cola
.selection_model().staged
)
726 class UntrackedSummary(Command
):
727 """List possible .gitignore rules as the diff text."""
729 Command
.__init
__(self
)
730 untracked
= self
.model
.untracked
731 suffix
= len(untracked
) > 1 and 's' or ''
733 io
.write('# %s untracked file%s\n' % (len(untracked
), suffix
))
735 io
.write('# possible .gitignore rule%s:\n' % suffix
)
737 io
.write('/%s\n' % u
)
738 self
.new_diff_text
= io
.getvalue()
741 class VisualizeAll(Command
):
742 """Visualize all branches."""
744 browser
= self
.model
.history_browser()
745 utils
.fork([browser
, '--all'])
748 class VisualizeCurrent(Command
):
749 """Visualize all branches."""
751 browser
= self
.model
.history_browser()
752 utils
.fork([browser
, self
.model
.currentbranch
])
755 class VisualizePaths(Command
):
756 """Path-limited visualization."""
757 def __init__(self
, paths
):
758 Command
.__init
__(self
)
759 browser
= self
.model
.history_browser()
761 self
.argv
= [browser
] + paths
763 self
.argv
= [browser
]
766 utils
.fork(self
.argv
)
771 Register signal mappings with the factory.
773 These commands are automatically created and run when
774 their corresponding signal is broadcast by the notifier.
777 signal_to_command_map
= {
778 signals
.add_signoff
: AddSignoff
,
779 signals
.amend_mode
: AmendMode
,
780 signals
.apply_diff_selection
: ApplyDiffSelection
,
781 signals
.apply_patches
: ApplyPatches
,
782 signals
.branch_mode
: BranchMode
,
783 signals
.clone
: Clone
,
784 signals
.checkout
: Checkout
,
785 signals
.checkout_branch
: CheckoutBranch
,
786 signals
.cherry_pick
: CherryPick
,
787 signals
.commit
: Commit
,
788 signals
.delete
: Delete
,
789 signals
.delete_branch
: DeleteBranch
,
791 signals
.diff_mode
: DiffMode
,
792 signals
.diff_expr_mode
: DiffExprMode
,
793 signals
.diff_staged
: DiffStaged
,
794 signals
.diffstat
: Diffstat
,
795 signals
.difftool
: Difftool
,
797 signals
.format_patch
: FormatPatch
,
798 signals
.grep
: GrepMode
,
799 signals
.load_commit_message
: LoadCommitMessage
,
800 signals
.load_commit_template
: LoadCommitTemplate
,
801 signals
.modified_summary
: Diffstat
,
802 signals
.mergetool
: Mergetool
,
803 signals
.open_repo
: OpenRepo
,
804 signals
.rescan
: Rescan
,
805 signals
.reset_mode
: ResetMode
,
806 signals
.review_branch_mode
: ReviewBranchMode
,
807 signals
.run_config_action
: RunConfigAction
,
808 signals
.show_untracked
: ShowUntracked
,
809 signals
.stage
: Stage
,
810 signals
.stage_modified
: StageModified
,
811 signals
.stage_untracked
: StageUntracked
,
812 signals
.staged_summary
: DiffStagedSummary
,
814 signals
.unstage
: Unstage
,
815 signals
.unstage_all
: UnstageAll
,
816 signals
.unstage_selected
: UnstageSelected
,
817 signals
.untracked_summary
: UntrackedSummary
,
818 signals
.visualize_all
: VisualizeAll
,
819 signals
.visualize_current
: VisualizeCurrent
,
820 signals
.visualize_paths
: VisualizePaths
,
823 for signal
, cmd
in signal_to_command_map
.iteritems():
824 _factory
.add_command(signal
, cmd
)