4 from cStringIO
import StringIO
10 from cola
import gitcfg
11 from cola
import gitcmds
12 from cola
import utils
13 from cola
import signals
14 from cola
import cmdfactory
15 from cola
import difftool
16 from cola
import version
17 from cola
.diffparse
import DiffParser
18 from cola
.models
import selection
20 _notifier
= cola
.notifier()
21 _factory
= cmdfactory
.factory()
22 _config
= gitcfg
.instance()
25 class Command(object):
26 """Base class for all commands; provides the command pattern."""
27 def __init__(self
, update
=False):
28 """Initialize the command and stash away values for use in do()"""
29 # These are commonly used so let's make it easier to write new commands.
31 self
.model
= cola
.model()
34 self
.old_diff_text
= self
.model
.diff_text
35 self
.old_filename
= self
.model
.filename
36 self
.old_mode
= self
.model
.mode
37 self
.old_head
= self
.model
.head
39 self
.new_diff_text
= self
.old_diff_text
40 self
.new_filename
= self
.old_filename
41 self
.new_head
= self
.old_head
42 self
.new_mode
= self
.old_mode
45 """Perform the operation."""
46 self
.model
.set_diff_text(self
.new_diff_text
)
47 self
.model
.set_filename(self
.new_filename
)
48 self
.model
.set_head(self
.new_head
)
49 self
.model
.set_mode(self
.new_mode
)
51 self
.model
.update_status()
53 def is_undoable(self
):
54 """Can this be undone?"""
58 """Undo the operation."""
59 self
.model
.set_diff_text(self
.old_diff_text
)
60 self
.model
.set_filename(self
.old_filename
)
61 self
.model
.set_head(self
.old_head
)
62 self
.model
.set_mode(self
.old_mode
)
64 self
.model
.update_status()
67 """Return this command's name."""
68 return self
.__class
__.__name
__
71 class AddSignoff(Command
):
72 """Add a signed-off-by to the commit message."""
74 Command
.__init
__(self
)
76 self
.old_commitmsg
= self
.model
.commitmsg
77 self
.new_commitmsg
= self
.old_commitmsg
78 signoff
= ('\nSigned-off-by: %s <%s>\n' %
79 (self
.model
.local_user_name
, self
.model
.local_user_email
))
80 if signoff
not in self
.new_commitmsg
:
81 self
.new_commitmsg
+= ('\n' + signoff
)
84 self
.model
.set_commitmsg(self
.new_commitmsg
)
87 self
.model
.set_commitmsg(self
.old_commitmsg
)
90 class AmendMode(Command
):
91 """Try to amend a commit."""
92 def __init__(self
, amend
):
93 Command
.__init
__(self
, update
=True)
97 self
.old_commitmsg
= self
.model
.commitmsg
100 self
.new_mode
= self
.model
.mode_amend
101 self
.new_head
= 'HEAD^'
102 self
.new_commitmsg
= self
.model
.prev_commitmsg()
104 # else, amend unchecked, regular commit
105 self
.new_mode
= self
.model
.mode_none
106 self
.new_head
= 'HEAD'
107 self
.new_commitmsg
= self
.model
.commitmsg
108 # If we're going back into new-commit-mode then search the
109 # undo stack for a previous amend-commit-mode and grab the
110 # commit message at that point in time.
111 if not _factory
.undostack
:
113 undo_count
= len(_factory
.undostack
)
114 for i
in xrange(undo_count
):
115 # Find the latest AmendMode command
116 idx
= undo_count
- i
- 1
117 cmdobj
= _factory
.undostack
[idx
]
118 if type(cmdobj
) is not AmendMode
:
121 self
.new_commitmsg
= cmdobj
.old_commitmsg
125 """Leave/enter amend mode."""
126 """Attempt to enter amend mode. Do not allow this when merging."""
128 if os
.path
.exists(self
.model
.git
.git_path('MERGE_HEAD')):
130 _notifier
.broadcast(signals
.amend
, False)
131 _factory
.prompt_user(signals
.information
,
133 'You are in the middle of a merge.\n'
134 'You cannot amend while merging.')
137 _notifier
.broadcast(signals
.amend
, self
.amending
)
138 self
.model
.set_commitmsg(self
.new_commitmsg
)
144 self
.model
.set_commitmsg(self
.old_commitmsg
)
148 class ApplyDiffSelection(Command
):
149 def __init__(self
, staged
, selected
, offset
, selection
, apply_to_worktree
):
150 Command
.__init
__(self
, update
=True)
152 self
.selected
= selected
154 self
.selection
= selection
155 self
.apply_to_worktree
= apply_to_worktree
158 if self
.model
.mode
== self
.model
.mode_branch
:
159 # We're applying changes from a different branch!
160 parser
= DiffParser(self
.model
,
161 filename
=self
.model
.filename
,
163 branch
=self
.model
.head
)
164 parser
.process_diff_selection(self
.selected
,
167 apply_to_worktree
=True)
169 # The normal worktree vs index scenario
170 parser
= DiffParser(self
.model
,
171 filename
=self
.model
.filename
,
173 reverse
=self
.apply_to_worktree
)
174 parser
.process_diff_selection(self
.selected
,
178 self
.apply_to_worktree
)
179 # Redo the diff to show changes
181 diffcmd
= DiffStaged([self
.model
.filename
])
183 diffcmd
= Diff([self
.model
.filename
])
185 self
.model
.update_status()
187 class ApplyPatches(Command
):
188 def __init__(self
, patches
):
189 Command
.__init
__(self
)
191 self
.patches
= patches
195 num_patches
= len(self
.patches
)
196 orig_head
= self
.model
.git
.rev_parse('HEAD')
198 for idx
, patch
in enumerate(self
.patches
):
199 status
, output
= self
.model
.git
.am(patch
,
202 # Log the git-am command
203 _notifier
.broadcast(signals
.log_cmd
, status
, output
)
206 diff
= self
.model
.git
.diff('HEAD^!', stat
=True)
207 diff_text
+= 'Patch %d/%d - ' % (idx
+1, num_patches
)
208 diff_text
+= '%s:\n%s\n\n' % (os
.path
.basename(patch
), diff
)
210 diff_text
+= 'Summary:\n'
211 diff_text
+= self
.model
.git
.diff(orig_head
, stat
=True)
214 self
.model
.set_diff_text(diff_text
)
216 _factory
.prompt_user(signals
.information
,
218 '%d patch(es) applied:\n\n%s' %
220 '\n'.join(map(os
.path
.basename
, self
.patches
))))
223 class HeadChangeCommand(Command
):
224 """Changes the model's current head."""
225 def __init__(self
, treeish
):
226 Command
.__init
__(self
, update
=True)
227 self
.new_head
= treeish
228 self
.new_diff_text
= ''
231 class BranchMode(HeadChangeCommand
):
232 """Enter into diff-branch mode."""
233 def __init__(self
, treeish
, filename
):
234 HeadChangeCommand
.__init
__(self
, treeish
)
235 self
.old_filename
= self
.model
.filename
236 self
.new_filename
= filename
237 self
.new_mode
= self
.model
.mode_branch
238 self
.new_diff_text
= gitcmds
.diff_helper(filename
=filename
,
242 class Checkout(Command
):
244 A command object for git-checkout.
246 'argv' is handed off directly to git.
249 def __init__(self
, argv
):
250 Command
.__init
__(self
)
254 status
, output
= self
.model
.git
.checkout(with_stderr
=True,
255 with_status
=True, *self
.argv
)
256 _notifier
.broadcast(signals
.log_cmd
, status
, output
)
257 self
.model
.set_diff_text('')
258 self
.model
.update_status()
261 class CheckoutBranch(Checkout
):
262 """Checkout a branch."""
263 def __init__(self
, branch
):
264 Checkout
.__init
__(self
, [branch
])
267 class CherryPick(Command
):
268 """Cherry pick commits into the current branch."""
269 def __init__(self
, commits
):
270 Command
.__init
__(self
)
271 self
.commits
= commits
274 self
.model
.cherry_pick_list(self
.commits
)
277 class ResetMode(Command
):
278 """Reset the mode and clear the model's diff text."""
280 Command
.__init
__(self
, update
=True)
281 self
.new_mode
= self
.model
.mode_none
282 self
.new_head
= 'HEAD'
283 self
.new_diff_text
= ''
286 class Commit(ResetMode
):
287 """Attempt to create a new commit."""
288 def __init__(self
, amend
, msg
):
289 ResetMode
.__init
__(self
)
291 self
.msg
= core
.encode(msg
)
292 self
.old_commitmsg
= self
.model
.commitmsg
293 self
.new_commitmsg
= ''
296 status
, output
= self
.model
.commit_with_msg(self
.msg
, amend
=self
.amend
)
299 self
.model
.set_commitmsg(self
.new_commitmsg
)
302 title
= 'Commit failed: '
303 _notifier
.broadcast(signals
.log_cmd
, status
, title
+output
)
306 class Delete(Command
):
307 """Simply delete files."""
308 def __init__(self
, filenames
):
309 Command
.__init
__(self
)
310 self
.filenames
= filenames
311 # We could git-hash-object stuff and provide undo-ability
315 for filename
in self
.filenames
:
321 _factory
.prompt_user(signals
.information
,
323 'Deleting "%s" failed.' % filename
)
325 self
.model
.update_status()
327 class DeleteBranch(Command
):
328 """Delete a git branch."""
329 def __init__(self
, branch
):
330 Command
.__init
__(self
)
334 status
, output
= self
.model
.delete_branch(self
.branch
)
336 if output
.startswith('error:'):
337 output
= 'E' + output
[1:]
340 _notifier
.broadcast(signals
.log_cmd
, status
, title
+ output
)
344 """Perform a diff and set the model's current text."""
345 def __init__(self
, filenames
, cached
=False):
346 Command
.__init
__(self
)
349 cached
= not self
.model
.read_only()
350 opts
= dict(ref
=self
.model
.head
)
352 self
.new_filename
= filenames
[0]
353 self
.old_filename
= self
.model
.filename
354 if not self
.model
.read_only():
355 if self
.model
.mode
!= self
.model
.mode_amend
:
356 self
.new_mode
= self
.model
.mode_worktree
357 self
.new_diff_text
= gitcmds
.diff_helper(filename
=self
.new_filename
,
358 cached
=cached
, **opts
)
361 class DiffMode(HeadChangeCommand
):
362 """Enter diff mode and clear the model's diff text."""
363 def __init__(self
, treeish
):
364 HeadChangeCommand
.__init
__(self
, treeish
)
365 self
.new_mode
= self
.model
.mode_diff
368 class DiffExprMode(HeadChangeCommand
):
369 """Enter diff-expr mode and clear the model's diff text."""
370 def __init__(self
, treeish
):
371 HeadChangeCommand
.__init
__(self
, treeish
)
372 self
.new_mode
= self
.model
.mode_diff_expr
375 class Diffstat(Command
):
376 """Perform a diffstat and set the model's diff text."""
378 Command
.__init
__(self
)
379 diff
= self
.model
.git
.diff(self
.model
.head
,
380 unified
=_config
.get('diff.context', 3),
384 self
.new_diff_text
= core
.decode(diff
)
385 self
.new_mode
= self
.model
.mode_worktree
388 class DiffStaged(Diff
):
389 """Perform a staged diff on a file."""
390 def __init__(self
, filenames
):
391 Diff
.__init
__(self
, filenames
, cached
=True)
392 if not self
.model
.read_only():
393 if self
.model
.mode
!= self
.model
.mode_amend
:
394 self
.new_mode
= self
.model
.mode_index
397 class DiffStagedSummary(Command
):
399 Command
.__init
__(self
)
400 cached
= not self
.model
.read_only()
401 diff
= self
.model
.git
.diff(self
.model
.head
,
404 patch_with_stat
=True,
406 self
.new_diff_text
= core
.decode(diff
)
407 if not self
.model
.read_only():
408 if self
.model
.mode
!= self
.model
.mode_amend
:
409 self
.new_mode
= self
.model
.mode_index
412 class Difftool(Command
):
413 """Run git-difftool limited by path."""
414 def __init__(self
, staged
, filenames
):
415 Command
.__init
__(self
)
417 self
.filenames
= filenames
420 if not self
.filenames
:
423 if self
.staged
and not self
.model
.read_only():
424 args
.append('--cached')
425 if self
.model
.head
!= 'HEAD':
426 args
.append(self
.model
.head
)
428 args
.extend(self
.filenames
)
429 difftool
.launch(args
)
433 """Edit a file using the configured gui.editor."""
434 def __init__(self
, filenames
, line_number
=None):
435 Command
.__init
__(self
)
436 self
.filenames
= filenames
437 self
.line_number
= line_number
440 filename
= self
.filenames
[0]
441 if not os
.path
.exists(filename
):
443 editor
= self
.model
.editor()
444 if 'vi' in editor
and self
.line_number
:
445 utils
.fork([editor
, filename
, '+'+self
.line_number
])
447 utils
.fork([editor
, filename
])
450 class FormatPatch(Command
):
451 """Output a patch series given all revisions and a selected subset."""
452 def __init__(self
, to_export
, revs
):
453 Command
.__init
__(self
)
454 self
.to_export
= to_export
458 status
, output
= gitcmds
.format_patchsets(self
.to_export
, self
.revs
)
459 _notifier
.broadcast(signals
.log_cmd
, status
, output
)
462 class GrepMode(Command
):
463 def __init__(self
, txt
):
464 """Perform a git-grep."""
465 Command
.__init
__(self
)
466 self
.new_mode
= self
.model
.mode_grep
467 self
.new_diff_text
= self
.model
.git
.grep(txt
, n
=True)
469 class LoadCommitMessage(Command
):
470 """Loads a commit message from a path."""
471 def __init__(self
, path
):
472 Command
.__init
__(self
)
473 if not path
or not os
.path
.exists(path
):
474 raise OSError('error: "%s" does not exist' % path
)
477 self
.old_commitmsg
= self
.model
.commitmsg
478 self
.old_directory
= self
.model
.directory
481 self
.model
.set_directory(os
.path
.dirname(self
.path
))
482 self
.model
.set_commitmsg(utils
.slurp(self
.path
))
485 self
.model
.set_commitmsg(self
.old_commitmsg
)
486 self
.model
.set_directory(self
.old_directory
)
489 class LoadCommitTemplate(LoadCommitMessage
):
490 """Loads the commit message template specified by commit.template."""
492 LoadCommitMessage
.__init
__(self
, _config
.get('commit.template'))
495 class Mergetool(Command
):
496 """Launch git-mergetool on a list of paths."""
497 def __init__(self
, paths
):
498 Command
.__init
__(self
)
504 if version
.check('mergetool-no-prompt',
505 self
.model
.git
.version().split()[-1]):
506 utils
.fork(['git', 'mergetool', '--no-prompt', '--'] + self
.paths
)
508 utils
.fork(['xterm', '-e', 'git', 'mergetool', '--'] + self
.paths
)
511 class OpenRepo(Command
):
512 """Launches git-cola on a repo."""
513 def __init__(self
, dirname
):
514 Command
.__init
__(self
)
515 self
.new_directory
= utils
.quote_repopath(dirname
)
518 self
.model
.set_directory(self
.new_directory
)
519 utils
.fork(['python', sys
.argv
[0], '--repo', self
.new_directory
])
522 class Clone(Command
):
523 """Clones a repository and optionally spawns a new cola session."""
524 def __init__(self
, url
, destdir
, spawn
=True):
525 Command
.__init
__(self
)
527 self
.new_directory
= utils
.quote_repopath(destdir
)
531 self
.model
.git
.clone(self
.url
, self
.new_directory
,
532 with_stderr
=True, with_status
=True)
534 utils
.fork(['python', sys
.argv
[0], '--repo', self
.new_directory
])
537 class Rescan(Command
):
538 """Rescans for changes."""
540 Command
.__init
__(self
, update
=True)
543 class ReviewBranchMode(Command
):
544 """Enter into review-branch mode."""
545 def __init__(self
, branch
):
546 Command
.__init
__(self
, update
=True)
547 self
.new_mode
= self
.model
.mode_review
548 self
.new_head
= gitcmds
.merge_base_parent(branch
)
549 self
.new_diff_text
= ''
552 class RunConfigAction(Command
):
553 """Run a user-configured action, typically from the "Tools" menu"""
554 def __init__(self
, name
):
555 Command
.__init
__(self
)
557 self
.model
= cola
.model()
560 for env
in ('FILENAME', 'REVISION', 'ARGS'):
567 opts
= _config
.get_guitool_opts(self
.name
)
568 cmd
= opts
.get('cmd')
569 if 'title' not in opts
:
572 if 'prompt' not in opts
or opts
.get('prompt') is True:
573 prompt
= i18n
.gettext('Are you sure you want to run %s?') % cmd
574 opts
['prompt'] = prompt
576 if opts
.get('needsfile'):
577 filename
= selection
.filename()
579 _factory
.prompt_user(signals
.information
,
580 'Please select a file',
581 '"%s" requires a selected file' % cmd
)
583 os
.environ
['FILENAME'] = commands
.mkarg(filename
)
586 if opts
.get('revprompt') or opts
.get('argprompt'):
588 ok
= _factory
.prompt_user(signals
.run_config_action
, cmd
, opts
)
591 rev
= opts
.get('revision')
592 args
= opts
.get('args')
593 if opts
.get('revprompt') and not rev
:
594 msg
= ('Invalid revision:\n\n'
595 'Revision expression is empty')
597 _factory
.prompt_user(signals
.information
, title
, msg
)
601 elif opts
.get('confirm'):
602 title
= os
.path
.expandvars(opts
.get('title'))
603 prompt
= os
.path
.expandvars(opts
.get('prompt'))
604 if not _factory
.prompt_user(signals
.question
, title
, prompt
):
607 os
.environ
['REVISION'] = rev
609 os
.environ
['ARGS'] = args
610 title
= os
.path
.expandvars(cmd
)
611 _notifier
.broadcast(signals
.log_cmd
, 0, 'running: ' + title
)
612 cmd
= ['sh', '-c', cmd
]
614 if opts
.get('noconsole'):
615 status
, out
, err
= utils
.run_command(cmd
, flag_error
=False)
617 status
, out
, err
= _factory
.prompt_user(signals
.run_command
,
620 _notifier
.broadcast(signals
.log_cmd
, status
,
621 'stdout: %s\nstatus: %s\nstderr: %s' %
622 (out
.rstrip(), status
, err
.rstrip()))
624 if not opts
.get('norescan'):
625 self
.model
.update_status()
629 class ShowUntracked(Command
):
630 """Show an untracked file."""
631 # We don't actually do anything other than set the mode right now.
632 # TODO check the mimetype for the file and handle things
634 def __init__(self
, filenames
):
635 Command
.__init
__(self
)
636 self
.new_mode
= self
.model
.mode_worktree
637 # TODO new_diff_text = utils.file_preview(filenames[0])
640 class Stage(Command
):
641 """Stage a set of paths."""
642 def __init__(self
, paths
):
643 Command
.__init
__(self
)
647 msg
= 'Staging: %s' % (', '.join(self
.paths
))
648 _notifier
.broadcast(signals
.log_cmd
, 0, msg
)
649 self
.model
.stage_paths(self
.paths
)
652 class StageModified(Stage
):
653 """Stage all modified files."""
655 Stage
.__init
__(self
, None)
656 self
.paths
= self
.model
.modified
659 class StageUntracked(Stage
):
660 """Stage all untracked files."""
662 Stage
.__init
__(self
, None)
663 self
.paths
= self
.model
.untracked
666 """Create a tag object."""
667 def __init__(self
, name
, revision
, sign
=False, message
=''):
668 Command
.__init
__(self
)
670 self
._message
= core
.encode(message
)
671 self
._revision
= revision
675 log_msg
= 'Tagging: "%s" as "%s"' % (self
._revision
, self
._name
)
678 opts
['F'] = self
.model
.tmp_filename()
679 utils
.write(opts
['F'], self
._message
)
682 log_msg
+= ', GPG-signed'
684 status
, output
= self
.model
.git
.tag(self
._name
,
690 opts
['a'] = bool(self
._message
)
691 status
, output
= self
.model
.git
.tag(self
._name
,
700 log_msg
+= '\nOutput:\n%s' % output
702 _notifier
.broadcast(signals
.log_cmd
, status
, log_msg
)
704 self
.model
.update_status()
707 class Unstage(Command
):
708 """Unstage a set of paths."""
709 def __init__(self
, paths
):
710 Command
.__init
__(self
)
714 msg
= 'Unstaging: %s' % (', '.join(self
.paths
))
715 _notifier
.broadcast(signals
.log_cmd
, 0, msg
)
716 self
.model
.unstage_paths(self
.paths
)
719 class UnstageAll(Command
):
720 """Unstage all files; resets the index."""
722 Command
.__init
__(self
, update
=True)
725 self
.model
.unstage_all()
728 class UnstageSelected(Unstage
):
729 """Unstage selected files."""
731 Unstage
.__init
__(self
, cola
.selection_model().staged
)
734 class UntrackedSummary(Command
):
735 """List possible .gitignore rules as the diff text."""
737 Command
.__init
__(self
)
738 untracked
= self
.model
.untracked
739 suffix
= len(untracked
) > 1 and 's' or ''
741 io
.write('# %s untracked file%s\n' % (len(untracked
), suffix
))
743 io
.write('# possible .gitignore rule%s:\n' % suffix
)
745 io
.write('/%s\n' % u
)
746 self
.new_diff_text
= io
.getvalue()
749 class VisualizeAll(Command
):
750 """Visualize all branches."""
752 browser
= self
.model
.history_browser()
753 utils
.fork([browser
, '--all'])
756 class VisualizeCurrent(Command
):
757 """Visualize all branches."""
759 browser
= self
.model
.history_browser()
760 utils
.fork([browser
, self
.model
.currentbranch
])
763 class VisualizePaths(Command
):
764 """Path-limited visualization."""
765 def __init__(self
, paths
):
766 Command
.__init
__(self
)
767 browser
= self
.model
.history_browser()
769 self
.argv
= [browser
] + paths
771 self
.argv
= [browser
]
774 utils
.fork(self
.argv
)
779 Register signal mappings with the factory.
781 These commands are automatically created and run when
782 their corresponding signal is broadcast by the notifier.
785 signal_to_command_map
= {
786 signals
.add_signoff
: AddSignoff
,
787 signals
.amend_mode
: AmendMode
,
788 signals
.apply_diff_selection
: ApplyDiffSelection
,
789 signals
.apply_patches
: ApplyPatches
,
790 signals
.branch_mode
: BranchMode
,
791 signals
.clone
: Clone
,
792 signals
.checkout
: Checkout
,
793 signals
.checkout_branch
: CheckoutBranch
,
794 signals
.cherry_pick
: CherryPick
,
795 signals
.commit
: Commit
,
796 signals
.delete
: Delete
,
797 signals
.delete_branch
: DeleteBranch
,
799 signals
.diff_mode
: DiffMode
,
800 signals
.diff_expr_mode
: DiffExprMode
,
801 signals
.diff_staged
: DiffStaged
,
802 signals
.diffstat
: Diffstat
,
803 signals
.difftool
: Difftool
,
805 signals
.format_patch
: FormatPatch
,
806 signals
.grep
: GrepMode
,
807 signals
.load_commit_message
: LoadCommitMessage
,
808 signals
.load_commit_template
: LoadCommitTemplate
,
809 signals
.modified_summary
: Diffstat
,
810 signals
.mergetool
: Mergetool
,
811 signals
.open_repo
: OpenRepo
,
812 signals
.rescan
: Rescan
,
813 signals
.reset_mode
: ResetMode
,
814 signals
.review_branch_mode
: ReviewBranchMode
,
815 signals
.run_config_action
: RunConfigAction
,
816 signals
.show_untracked
: ShowUntracked
,
817 signals
.stage
: Stage
,
818 signals
.stage_modified
: StageModified
,
819 signals
.stage_untracked
: StageUntracked
,
820 signals
.staged_summary
: DiffStagedSummary
,
822 signals
.unstage
: Unstage
,
823 signals
.unstage_all
: UnstageAll
,
824 signals
.unstage_selected
: UnstageSelected
,
825 signals
.untracked_summary
: UntrackedSummary
,
826 signals
.visualize_all
: VisualizeAll
,
827 signals
.visualize_current
: VisualizeCurrent
,
828 signals
.visualize_paths
: VisualizePaths
,
831 for signal
, cmd
in signal_to_command_map
.iteritems():
832 _factory
.add_command(signal
, cmd
)