4 from cStringIO
import StringIO
8 from cola
import gitcfg
9 from cola
import gitcmds
10 from cola
import utils
11 from cola
import signals
12 from cola
import cmdfactory
13 from cola
import difftool
14 from cola
import version
15 from cola
.diffparse
import DiffParser
17 _notifier
= cola
.notifier()
18 _factory
= cmdfactory
.factory()
19 _config
= gitcfg
.instance()
22 class Command(object):
23 """Base class for all commands; provides the command pattern."""
24 def __init__(self
, update
=False):
25 """Initialize the command and stash away values for use in do()"""
26 # These are commonly used so let's make it easier to write new commands.
28 self
.model
= cola
.model()
31 self
.old_diff_text
= self
.model
.diff_text
32 self
.old_filename
= self
.model
.filename
33 self
.old_mode
= self
.model
.mode
34 self
.old_head
= self
.model
.head
36 self
.new_diff_text
= self
.old_diff_text
37 self
.new_filename
= self
.old_filename
38 self
.new_head
= self
.old_head
39 self
.new_mode
= self
.old_mode
42 """Perform the operation."""
43 self
.model
.set_diff_text(self
.new_diff_text
)
44 self
.model
.set_filename(self
.new_filename
)
45 self
.model
.set_head(self
.new_head
)
46 self
.model
.set_mode(self
.new_mode
)
48 self
.model
.update_status()
50 def is_undoable(self
):
51 """Can this be undone?"""
55 """Undo the operation."""
56 self
.model
.set_diff_text(self
.old_diff_text
)
57 self
.model
.set_filename(self
.old_filename
)
58 self
.model
.set_head(self
.old_head
)
59 self
.model
.set_mode(self
.old_mode
)
61 self
.model
.update_status()
64 """Return this command's name."""
65 return self
.__class
__.__name
__
68 class AddSignoff(Command
):
69 """Add a signed-off-by to the commit message."""
71 Command
.__init
__(self
)
73 self
.old_commitmsg
= self
.model
.commitmsg
74 self
.new_commitmsg
= self
.old_commitmsg
75 signoff
= ('\nSigned-off-by: %s <%s>\n' %
76 (self
.model
.local_user_name
, self
.model
.local_user_email
))
77 if signoff
not in self
.new_commitmsg
:
78 self
.new_commitmsg
+= ('\n' + signoff
)
81 self
.model
.set_commitmsg(self
.new_commitmsg
)
84 self
.model
.set_commitmsg(self
.old_commitmsg
)
87 class AmendMode(Command
):
88 """Try to amend a commit."""
89 def __init__(self
, amend
):
90 Command
.__init
__(self
, update
=True)
94 self
.old_commitmsg
= self
.model
.commitmsg
97 self
.new_mode
= self
.model
.mode_amend
98 self
.new_head
= 'HEAD^'
99 self
.new_commitmsg
= self
.model
.prev_commitmsg()
101 # else, amend unchecked, regular commit
102 self
.new_mode
= self
.model
.mode_none
103 self
.new_head
= 'HEAD'
104 self
.new_commitmsg
= self
.model
.commitmsg
105 # If we're going back into new-commit-mode then search the
106 # undo stack for a previous amend-commit-mode and grab the
107 # commit message at that point in time.
108 if not _factory
.undostack
:
110 undo_count
= len(_factory
.undostack
)
111 for i
in xrange(undo_count
):
112 # Find the latest AmendMode command
113 idx
= undo_count
- i
- 1
114 cmdobj
= _factory
.undostack
[idx
]
115 if type(cmdobj
) is not AmendMode
:
118 self
.new_commitmsg
= cmdobj
.old_commitmsg
122 """Leave/enter amend mode."""
123 """Attempt to enter amend mode. Do not allow this when merging."""
125 if os
.path
.exists(self
.model
.git_repo_path('MERGE_HEAD')):
127 _notifier
.broadcast(signals
.amend
, False)
128 _notifier
.broadcast(signals
.information
,
130 'You are in the middle of a merge.\n'
131 'You cannot amend while merging.')
134 _notifier
.broadcast(signals
.amend
, self
.amending
)
135 self
.model
.set_commitmsg(self
.new_commitmsg
)
141 self
.model
.set_commitmsg(self
.old_commitmsg
)
145 class ApplyDiffSelection(Command
):
146 def __init__(self
, staged
, selected
, offset
, selection
, apply_to_worktree
):
147 Command
.__init
__(self
, update
=True)
149 self
.selected
= selected
151 self
.selection
= selection
152 self
.apply_to_worktree
= apply_to_worktree
155 if self
.model
.mode
== self
.model
.mode_branch
:
156 # We're applying changes from a different branch!
157 parser
= DiffParser(self
.model
,
158 filename
=self
.model
.filename
,
160 branch
=self
.model
.head
)
161 parser
.process_diff_selection(self
.selected
,
164 apply_to_worktree
=True)
166 # The normal worktree vs index scenario
167 parser
= DiffParser(self
.model
,
168 filename
=self
.model
.filename
,
170 reverse
=self
.apply_to_worktree
)
171 parser
.process_diff_selection(self
.selected
,
175 self
.apply_to_worktree
)
176 # Redo the diff to show changes
178 diffcmd
= DiffStaged([self
.model
.filename
])
180 diffcmd
= Diff([self
.model
.filename
])
182 self
.model
.update_status()
184 class ApplyPatches(Command
):
185 def __init__(self
, patches
):
186 Command
.__init
__(self
)
188 self
.patches
= patches
192 num_patches
= len(self
.patches
)
193 orig_head
= cola
.model().git
.rev_parse('HEAD')
195 for idx
, patch
in enumerate(self
.patches
):
196 status
, output
= cola
.model().git
.am(patch
,
199 # Log the git-am command
200 _notifier
.broadcast(signals
.log_cmd
, status
, output
)
203 diff
= cola
.model().git
.diff('HEAD^!', stat
=True)
204 diff_text
+= 'Patch %d/%d - ' % (idx
+1, num_patches
)
205 diff_text
+= '%s:\n%s\n\n' % (os
.path
.basename(patch
), diff
)
207 diff_text
+= 'Summary:\n'
208 diff_text
+= cola
.model().git
.diff(orig_head
, stat
=True)
211 self
.model
.set_diff_text(diff_text
)
213 _notifier
.broadcast(signals
.information
,
215 '%d patch(es) applied:\n\n%s' %
217 '\n'.join(map(os
.path
.basename
, self
.patches
))))
220 class HeadChangeCommand(Command
):
221 """Changes the model's current head."""
222 def __init__(self
, treeish
):
223 Command
.__init
__(self
, update
=True)
224 self
.new_head
= treeish
225 self
.new_diff_text
= ''
228 class BranchMode(HeadChangeCommand
):
229 """Enter into diff-branch mode."""
230 def __init__(self
, treeish
, filename
):
231 HeadChangeCommand
.__init
__(self
, treeish
)
232 self
.old_filename
= self
.model
.filename
233 self
.new_filename
= filename
234 self
.new_mode
= self
.model
.mode_branch
235 self
.new_diff_text
= gitcmds
.diff_helper(filename
=filename
,
239 class Checkout(Command
):
241 A command object for git-checkout.
243 'argv' is handed off directly to git.
246 def __init__(self
, argv
):
247 Command
.__init
__(self
)
251 status
, output
= self
.model
.git
.checkout(with_stderr
=True,
252 with_status
=True, *self
.argv
)
253 _notifier
.broadcast(signals
.log_cmd
, status
, output
)
254 self
.model
.set_diff_text('')
255 self
.model
.update_status()
258 class CheckoutBranch(Checkout
):
259 """Checkout a branch."""
260 def __init__(self
, branch
):
261 Checkout
.__init
__(self
, [branch
])
264 class CherryPick(Command
):
265 """Cherry pick commits into the current branch."""
266 def __init__(self
, commits
):
267 Command
.__init
__(self
)
268 self
.commits
= commits
271 self
.model
.cherry_pick_list(self
.commits
)
274 class ResetMode(Command
):
275 """Reset the mode and clear the model's diff text."""
277 Command
.__init
__(self
, update
=True)
278 self
.new_mode
= self
.model
.mode_none
279 self
.new_head
= 'HEAD'
280 self
.new_diff_text
= ''
283 class Commit(ResetMode
):
284 """Attempt to create a new commit."""
285 def __init__(self
, amend
, msg
):
286 ResetMode
.__init
__(self
)
288 self
.msg
= core
.encode(msg
)
289 self
.old_commitmsg
= self
.model
.commitmsg
290 self
.new_commitmsg
= ''
293 status
, output
= self
.model
.commit_with_msg(self
.msg
, amend
=self
.amend
)
296 self
.model
.set_commitmsg(self
.new_commitmsg
)
299 title
= 'Commit failed: '
300 _notifier
.broadcast(signals
.log_cmd
, status
, title
+output
)
303 class Delete(Command
):
304 """Simply delete files."""
305 def __init__(self
, filenames
):
306 Command
.__init
__(self
)
307 self
.filenames
= filenames
308 # We could git-hash-object stuff and provide undo-ability
312 for filename
in self
.filenames
:
318 _notifier
.broadcast(signals
.information
,
320 'Deleting "%s" failed.' % filename
)
322 self
.model
.update_status()
324 class DeleteBranch(Command
):
325 """Delete a git branch."""
326 def __init__(self
, branch
):
327 Command
.__init
__(self
)
331 status
, output
= self
.model
.delete_branch(self
.branch
)
333 if output
.startswith('error:'):
334 output
= 'E' + output
[1:]
337 _notifier
.broadcast(signals
.log_cmd
, status
, title
+ output
)
341 """Perform a diff and set the model's current text."""
342 def __init__(self
, filenames
, cached
=False):
343 Command
.__init
__(self
)
346 cached
= not self
.model
.read_only()
347 opts
= dict(ref
=self
.model
.head
)
349 self
.new_filename
= filenames
[0]
350 self
.old_filename
= self
.model
.filename
351 if not self
.model
.read_only():
352 if self
.model
.mode
!= self
.model
.mode_amend
:
353 self
.new_mode
= self
.model
.mode_worktree
354 self
.new_diff_text
= gitcmds
.diff_helper(filename
=self
.new_filename
,
355 cached
=cached
, **opts
)
358 class DiffMode(HeadChangeCommand
):
359 """Enter diff mode and clear the model's diff text."""
360 def __init__(self
, treeish
):
361 HeadChangeCommand
.__init
__(self
, treeish
)
362 self
.new_mode
= self
.model
.mode_diff
365 class DiffExprMode(HeadChangeCommand
):
366 """Enter diff-expr mode and clear the model's diff text."""
367 def __init__(self
, treeish
):
368 HeadChangeCommand
.__init
__(self
, treeish
)
369 self
.new_mode
= self
.model
.mode_diff_expr
372 class Diffstat(Command
):
373 """Perform a diffstat and set the model's diff text."""
375 Command
.__init
__(self
)
376 diff
= self
.model
.git
.diff(self
.model
.head
,
377 unified
=_config
.get('diff.context', 3),
381 self
.new_diff_text
= core
.decode(diff
)
382 self
.new_mode
= self
.model
.mode_worktree
385 class DiffStaged(Diff
):
386 """Perform a staged diff on a file."""
387 def __init__(self
, filenames
):
388 Diff
.__init
__(self
, filenames
, cached
=True)
389 if not self
.model
.read_only():
390 if self
.model
.mode
!= self
.model
.mode_amend
:
391 self
.new_mode
= self
.model
.mode_index
394 class DiffStagedSummary(Command
):
396 Command
.__init
__(self
)
397 cached
= not self
.model
.read_only()
398 diff
= self
.model
.git
.diff(self
.model
.head
,
401 patch_with_stat
=True,
403 self
.new_diff_text
= core
.decode(diff
)
404 if not self
.model
.read_only():
405 if self
.model
.mode
!= self
.model
.mode_amend
:
406 self
.new_mode
= self
.model
.mode_index
409 class Difftool(Command
):
410 """Run git-difftool limited by path."""
411 def __init__(self
, staged
, filenames
):
412 Command
.__init
__(self
)
414 self
.filenames
= filenames
417 if not self
.filenames
:
420 if self
.staged
and not self
.model
.read_only():
421 args
.append('--cached')
422 args
.extend([self
.model
.head
, '--'])
423 args
.extend(self
.filenames
)
424 difftool
.launch(args
)
428 """Edit a file using the configured gui.editor."""
429 def __init__(self
, filenames
, line_number
=None):
430 Command
.__init
__(self
)
431 self
.filenames
= filenames
432 self
.line_number
= line_number
435 filename
= self
.filenames
[0]
436 if not os
.path
.exists(filename
):
438 editor
= self
.model
.editor()
439 if 'vi' in editor
and self
.line_number
:
440 utils
.fork([editor
, filename
, '+'+self
.line_number
])
442 utils
.fork([editor
, filename
])
445 class FormatPatch(Command
):
446 """Output a patch series given all revisions and a selected subset."""
447 def __init__(self
, to_export
, revs
):
448 Command
.__init
__(self
)
449 self
.to_export
= to_export
453 status
, output
= gitcmds
.format_patchsets(self
.to_export
, self
.revs
)
454 _notifier
.broadcast(signals
.log_cmd
, status
, output
)
457 class GrepMode(Command
):
458 def __init__(self
, txt
):
459 """Perform a git-grep."""
460 Command
.__init
__(self
)
461 self
.new_mode
= self
.model
.mode_grep
462 self
.new_diff_text
= self
.model
.git
.grep(txt
, n
=True)
465 class LoadCommitMessage(Command
):
466 """Loads a commit message from a path."""
467 def __init__(self
, path
):
468 Command
.__init
__(self
)
471 self
.old_commitmsg
= self
.model
.commitmsg
472 self
.old_directory
= self
.model
.directory
475 self
.model
.set_directory(os
.path
.dirname(self
.path
))
476 fh
= open(self
.path
, 'r')
477 contents
= core
.decode(core
.read_nointr(fh
))
479 self
.model
.set_commitmsg(contents
)
482 self
.model
.set_commitmsg(self
.old_commitmsg
)
483 self
.model
.set_directory(self
.old_directory
)
486 class Mergetool(Command
):
487 """Launch git-mergetool on a list of paths."""
488 def __init__(self
, paths
):
489 Command
.__init
__(self
)
495 if version
.check('mergetool-no-prompt',
496 self
.model
.git
.version().split()[-1]):
497 utils
.fork(['git', 'mergetool', '--no-prompt', '--'] + self
.paths
)
499 utils
.fork(['xterm', '-e', 'git', 'mergetool', '--'] + self
.paths
)
502 class OpenRepo(Command
):
503 """Launches git-cola on a repo."""
504 def __init__(self
, dirname
):
505 Command
.__init
__(self
)
506 self
.new_directory
= utils
.quote_repopath(dirname
)
509 self
.model
.set_directory(self
.new_directory
)
510 utils
.fork(['python', sys
.argv
[0], '--repo', self
.new_directory
])
513 class Clone(Command
):
514 """Clones a repository and optionally spawns a new cola session."""
515 def __init__(self
, url
, destdir
, spawn
=True):
516 Command
.__init
__(self
)
518 self
.new_directory
= utils
.quote_repopath(destdir
)
522 self
.model
.git
.clone(self
.url
, self
.new_directory
,
523 with_stderr
=True, with_status
=True)
525 utils
.fork(['python', sys
.argv
[0], '--repo', self
.new_directory
])
528 class Rescan(Command
):
529 """Rescans for changes."""
531 Command
.__init
__(self
, update
=True)
534 class ReviewBranchMode(Command
):
535 """Enter into review-branch mode."""
536 def __init__(self
, branch
):
537 Command
.__init
__(self
, update
=True)
538 self
.new_mode
= self
.model
.mode_review
539 self
.new_head
= gitcmds
.merge_base_to(branch
)
540 self
.new_diff_text
= ''
543 class ShowUntracked(Command
):
544 """Show an untracked file."""
545 # We don't actually do anything other than set the mode right now.
546 # We could probably check the mimetype for the file and handle things
548 def __init__(self
, filenames
):
549 Command
.__init
__(self
)
550 self
.new_mode
= self
.model
.mode_worktree
551 # TODO new_diff_text = utils.file_preview(filenames[0])
554 class Stage(Command
):
555 """Stage a set of paths."""
556 def __init__(self
, paths
):
557 Command
.__init
__(self
)
561 msg
= 'Staging: %s' % (', '.join(self
.paths
))
562 _notifier
.broadcast(signals
.log_cmd
, 0, msg
)
563 self
.model
.stage_paths(self
.paths
)
566 class StageModified(Stage
):
567 """Stage all modified files."""
569 Stage
.__init
__(self
, None)
570 self
.paths
= self
.model
.modified
573 class StageUntracked(Stage
):
574 """Stage all untracked files."""
576 Stage
.__init
__(self
, None)
577 self
.paths
= self
.model
.untracked
580 """Create a tag object."""
581 def __init__(self
, name
, revision
, sign
=False, message
=''):
582 Command
.__init
__(self
)
584 self
._message
= core
.encode(message
)
585 self
._revision
= revision
589 log_msg
= 'Tagging: "%s" as "%s"' % (self
._revision
, self
._name
)
591 log_msg
+= ', GPG-signed'
592 path
= cola
.model().tmp_filename()
593 utils
.write(path
, self
._message
)
594 status
, output
= cola
.model().git
.tag(self
._name
,
602 status
, output
= cola
.model().git
.tag(self
._name
,
607 log_msg
+= '\nOutput:\n%s' % output
609 _notifier
.broadcast(signals
.log_cmd
, status
, log_msg
)
611 cola
.model().update_status()
614 class Unstage(Command
):
615 """Unstage a set of paths."""
616 def __init__(self
, paths
):
617 Command
.__init
__(self
)
621 msg
= 'Unstaging: %s' % (', '.join(self
.paths
))
622 _notifier
.broadcast(signals
.log_cmd
, 0, msg
)
623 gitcmds
.unstage_paths(self
.paths
)
624 self
.model
.update_status()
627 class UnstageAll(Command
):
628 """Unstage all files; resets the index."""
630 Command
.__init
__(self
, update
=True)
633 self
.model
.unstage_all()
636 class UnstageSelected(Unstage
):
637 """Unstage selected files."""
639 Unstage
.__init
__(self
, cola
.selection_model().staged
)
642 class UntrackedSummary(Command
):
643 """List possible .gitignore rules as the diff text."""
645 Command
.__init
__(self
)
646 untracked
= self
.model
.untracked
647 suffix
= len(untracked
) > 1 and 's' or ''
649 io
.write('# %s untracked file%s\n' % (len(untracked
), suffix
))
651 io
.write('# possible .gitignore rule%s:\n' % suffix
)
653 io
.write('/%s\n' % u
)
654 self
.new_diff_text
= io
.getvalue()
657 class VisualizeAll(Command
):
658 """Visualize all branches."""
660 browser
= self
.model
.history_browser()
661 utils
.fork([browser
, '--all'])
664 class VisualizeCurrent(Command
):
665 """Visualize all branches."""
667 browser
= self
.model
.history_browser()
668 utils
.fork([browser
, self
.model
.currentbranch
])
671 class VisualizePaths(Command
):
672 """Path-limited visualization."""
673 def __init__(self
, paths
):
674 Command
.__init
__(self
)
675 browser
= self
.model
.history_browser()
677 self
.argv
= [browser
] + paths
679 self
.argv
= [browser
]
682 utils
.fork(self
.argv
)
687 Register signal mappings with the factory.
689 These commands are automatically created and run when
690 their corresponding signal is broadcast by the notifier.
693 signal_to_command_map
= {
694 signals
.add_signoff
: AddSignoff
,
695 signals
.amend_mode
: AmendMode
,
696 signals
.apply_diff_selection
: ApplyDiffSelection
,
697 signals
.apply_patches
: ApplyPatches
,
698 signals
.branch_mode
: BranchMode
,
699 signals
.clone
: Clone
,
700 signals
.checkout
: Checkout
,
701 signals
.checkout_branch
: CheckoutBranch
,
702 signals
.cherry_pick
: CherryPick
,
703 signals
.commit
: Commit
,
704 signals
.delete
: Delete
,
705 signals
.delete_branch
: DeleteBranch
,
707 signals
.diff_mode
: DiffMode
,
708 signals
.diff_expr_mode
: DiffExprMode
,
709 signals
.diff_staged
: DiffStaged
,
710 signals
.diffstat
: Diffstat
,
711 signals
.difftool
: Difftool
,
713 signals
.format_patch
: FormatPatch
,
714 signals
.grep
: GrepMode
,
715 signals
.load_commit_message
: LoadCommitMessage
,
716 signals
.modified_summary
: Diffstat
,
717 signals
.mergetool
: Mergetool
,
718 signals
.open_repo
: OpenRepo
,
719 signals
.rescan
: Rescan
,
720 signals
.reset_mode
: ResetMode
,
721 signals
.review_branch_mode
: ReviewBranchMode
,
722 signals
.show_untracked
: ShowUntracked
,
723 signals
.stage
: Stage
,
724 signals
.stage_modified
: StageModified
,
725 signals
.stage_untracked
: StageUntracked
,
726 signals
.staged_summary
: DiffStagedSummary
,
728 signals
.unstage
: Unstage
,
729 signals
.unstage_all
: UnstageAll
,
730 signals
.unstage_selected
: UnstageSelected
,
731 signals
.untracked_summary
: UntrackedSummary
,
732 signals
.visualize_all
: VisualizeAll
,
733 signals
.visualize_current
: VisualizeCurrent
,
734 signals
.visualize_paths
: VisualizePaths
,
737 for signal
, cmd
in signal_to_command_map
.iteritems():
738 _factory
.add_command(signal
, cmd
)