4 from cStringIO
import StringIO
8 from cola
import gitcmds
10 from cola
import signals
11 from cola
import cmdfactory
12 from cola
import difftool
13 from cola
import version
14 from cola
.diffparse
import DiffParser
16 _notifier
= cola
.notifier()
17 _factory
= cmdfactory
.factory()
20 class Command(object):
21 """Base class for all commands; provides the command pattern."""
22 def __init__(self
, update
=False):
23 """Initialize the command and stash away values for use in do()"""
24 # These are commonly used so let's make it easier to write new commands.
26 self
.model
= cola
.model()
29 self
.old_diff_text
= self
.model
.diff_text
30 self
.old_filename
= self
.model
.filename
31 self
.old_mode
= self
.model
.mode
32 self
.old_head
= self
.model
.head
34 self
.new_diff_text
= self
.old_diff_text
35 self
.new_filename
= self
.old_filename
36 self
.new_head
= self
.old_head
37 self
.new_mode
= self
.old_mode
40 """Perform the operation."""
41 self
.model
.set_diff_text(self
.new_diff_text
)
42 self
.model
.set_filename(self
.new_filename
)
43 self
.model
.set_head(self
.new_head
)
44 self
.model
.set_mode(self
.new_mode
)
46 self
.model
.update_status()
48 def is_undoable(self
):
49 """Can this be undone?"""
53 """Undo the operation."""
54 self
.model
.set_diff_text(self
.old_diff_text
)
55 self
.model
.set_filename(self
.old_filename
)
56 self
.model
.set_head(self
.old_head
)
57 self
.model
.set_mode(self
.old_mode
)
59 self
.model
.update_status()
62 """Return this command's name."""
63 return self
.__class
__.__name
__
66 class AddSignoff(Command
):
67 """Add a signed-off-by to the commit message."""
69 Command
.__init
__(self
)
71 self
.old_commitmsg
= self
.model
.commitmsg
72 self
.new_commitmsg
= self
.old_commitmsg
73 signoff
= ('\nSigned-off-by: %s <%s>\n' %
74 (self
.model
.local_user_name
, self
.model
.local_user_email
))
75 if signoff
not in self
.new_commitmsg
:
76 self
.new_commitmsg
+= ('\n' + signoff
)
79 self
.model
.set_commitmsg(self
.new_commitmsg
)
82 self
.model
.set_commitmsg(self
.old_commitmsg
)
85 class AmendMode(Command
):
86 """Try to amend a commit."""
87 def __init__(self
, amend
):
88 Command
.__init
__(self
, update
=True)
92 self
.old_commitmsg
= self
.model
.commitmsg
95 self
.new_mode
= self
.model
.mode_amend
96 self
.new_head
= 'HEAD^'
97 self
.new_commitmsg
= self
.model
.prev_commitmsg()
99 # else, amend unchecked, regular commit
100 self
.new_mode
= self
.model
.mode_none
101 self
.new_head
= 'HEAD'
102 self
.new_commitmsg
= self
.model
.commitmsg
103 # If we're going back into new-commit-mode then search the
104 # undo stack for a previous amend-commit-mode and grab the
105 # commit message at that point in time.
106 if not _factory
.undostack
:
108 undo_count
= len(_factory
.undostack
)
109 for i
in xrange(undo_count
):
110 # Find the latest AmendMode command
111 idx
= undo_count
- i
- 1
112 cmdobj
= _factory
.undostack
[idx
]
113 if type(cmdobj
) is not AmendMode
:
116 self
.new_commitmsg
= cmdobj
.old_commitmsg
120 """Leave/enter amend mode."""
121 """Attempt to enter amend mode. Do not allow this when merging."""
123 if os
.path
.exists(self
.model
.git_repo_path('MERGE_HEAD')):
125 _notifier
.broadcast(signals
.amend
, False)
126 _notifier
.broadcast(signals
.information
,
128 'You are in the middle of a merge.\n'
129 'You cannot amend while merging.')
132 _notifier
.broadcast(signals
.amend
, self
.amending
)
133 self
.model
.set_commitmsg(self
.new_commitmsg
)
139 self
.model
.set_commitmsg(self
.old_commitmsg
)
143 class ApplyDiffSelection(Command
):
144 def __init__(self
, staged
, selected
, offset
, selection
, apply_to_worktree
):
145 Command
.__init
__(self
, update
=True)
147 self
.selected
= selected
149 self
.selection
= selection
150 self
.apply_to_worktree
= apply_to_worktree
153 if self
.model
.mode
== self
.model
.mode_branch
:
154 # We're applying changes from a different branch!
155 parser
= DiffParser(self
.model
,
156 filename
=self
.model
.filename
,
158 branch
=self
.model
.head
)
159 parser
.process_diff_selection(self
.selected
,
162 apply_to_worktree
=True)
164 # The normal worktree vs index scenario
165 parser
= DiffParser(self
.model
,
166 filename
=self
.model
.filename
,
168 reverse
=self
.apply_to_worktree
)
169 parser
.process_diff_selection(self
.selected
,
173 self
.apply_to_worktree
)
174 # Redo the diff to show changes
176 diffcmd
= DiffStaged([self
.model
.filename
])
178 diffcmd
= Diff([self
.model
.filename
])
180 self
.model
.update_status()
182 class ApplyPatches(Command
):
183 def __init__(self
, patches
):
184 Command
.__init
__(self
)
186 self
.patches
= patches
190 num_patches
= len(self
.patches
)
191 orig_head
= cola
.model().git
.rev_parse('HEAD')
193 for idx
, patch
in enumerate(self
.patches
):
194 status
, output
= cola
.model().git
.am(patch
,
197 # Log the git-am command
198 _notifier
.broadcast(signals
.log_cmd
, status
, output
)
201 diff
= cola
.model().git
.diff('HEAD^!', stat
=True)
202 diff_text
+= 'Patch %d/%d - ' % (idx
+1, num_patches
)
203 diff_text
+= '%s:\n%s\n\n' % (os
.path
.basename(patch
), diff
)
205 diff_text
+= 'Summary:\n'
206 diff_text
+= cola
.model().git
.diff(orig_head
, stat
=True)
209 self
.model
.set_diff_text(diff_text
)
211 _notifier
.broadcast(signals
.information
,
213 '%d patch(es) applied:\n\n%s' %
215 '\n'.join(map(os
.path
.basename
, self
.patches
))))
218 class HeadChangeCommand(Command
):
219 """Changes the model's current head."""
220 def __init__(self
, treeish
):
221 Command
.__init
__(self
, update
=True)
222 self
.new_head
= treeish
223 self
.new_diff_text
= ''
226 class BranchMode(HeadChangeCommand
):
227 """Enter into diff-branch mode."""
228 def __init__(self
, treeish
, filename
):
229 HeadChangeCommand
.__init
__(self
, treeish
)
230 self
.old_filename
= self
.model
.filename
231 self
.new_filename
= filename
232 self
.new_mode
= self
.model
.mode_branch
233 self
.new_diff_text
= gitcmds
.diff_helper(filename
=filename
,
237 class Checkout(Command
):
239 A command object for git-checkout.
241 'argv' is handed off directly to git.
244 def __init__(self
, argv
):
245 Command
.__init
__(self
)
249 status
, output
= self
.model
.git
.checkout(with_stderr
=True,
250 with_status
=True, *self
.argv
)
251 _notifier
.broadcast(signals
.log_cmd
, status
, output
)
252 self
.model
.set_diff_text('')
253 self
.model
.update_status()
256 class CheckoutBranch(Checkout
):
257 """Checkout a branch."""
258 def __init__(self
, branch
):
259 Checkout
.__init
__(self
, [branch
])
262 class CherryPick(Command
):
263 """Cherry pick commits into the current branch."""
264 def __init__(self
, commits
):
265 Command
.__init
__(self
)
266 self
.commits
= commits
269 self
.model
.cherry_pick_list(self
.commits
)
272 class ResetMode(Command
):
273 """Reset the mode and clear the model's diff text."""
275 Command
.__init
__(self
, update
=True)
276 self
.new_mode
= self
.model
.mode_none
277 self
.new_head
= 'HEAD'
278 self
.new_diff_text
= ''
281 class Commit(ResetMode
):
282 """Attempt to create a new commit."""
283 def __init__(self
, amend
, msg
):
284 ResetMode
.__init
__(self
)
286 self
.msg
= core
.encode(msg
)
287 self
.old_commitmsg
= self
.model
.commitmsg
288 self
.new_commitmsg
= ''
291 status
, output
= self
.model
.commit_with_msg(self
.msg
, amend
=self
.amend
)
294 self
.model
.set_commitmsg(self
.new_commitmsg
)
297 title
= 'Commit failed: '
298 _notifier
.broadcast(signals
.log_cmd
, status
, title
+output
)
301 class Delete(Command
):
302 """Simply delete files."""
303 def __init__(self
, filenames
):
304 Command
.__init
__(self
)
305 self
.filenames
= filenames
306 # We could git-hash-object stuff and provide undo-ability
310 for filename
in self
.filenames
:
316 _notifier
.broadcast(signals
.information
,
318 'Deleting "%s" failed.' % filename
)
320 self
.model
.update_status()
322 class DeleteBranch(Command
):
323 """Delete a git branch."""
324 def __init__(self
, branch
):
325 Command
.__init
__(self
)
329 status
, output
= self
.model
.delete_branch(self
.branch
)
331 if output
.startswith('error:'):
332 output
= 'E' + output
[1:]
335 _notifier
.broadcast(signals
.log_cmd
, status
, title
+ output
)
339 """Perform a diff and set the model's current text."""
340 def __init__(self
, filenames
, cached
=False):
341 Command
.__init
__(self
)
344 cached
= not self
.model
.read_only()
345 opts
= dict(ref
=self
.model
.head
)
347 self
.new_filename
= filenames
[0]
348 self
.old_filename
= self
.model
.filename
349 if not self
.model
.read_only():
350 if self
.model
.mode
!= self
.model
.mode_amend
:
351 self
.new_mode
= self
.model
.mode_worktree
352 self
.new_diff_text
= gitcmds
.diff_helper(filename
=self
.new_filename
,
353 cached
=cached
, **opts
)
356 class DiffMode(HeadChangeCommand
):
357 """Enter diff mode and clear the model's diff text."""
358 def __init__(self
, treeish
):
359 HeadChangeCommand
.__init
__(self
, treeish
)
360 self
.new_mode
= self
.model
.mode_diff
363 class DiffExprMode(HeadChangeCommand
):
364 """Enter diff-expr mode and clear the model's diff text."""
365 def __init__(self
, treeish
):
366 HeadChangeCommand
.__init
__(self
, treeish
)
367 self
.new_mode
= self
.model
.mode_diff_expr
370 class Diffstat(Command
):
371 """Perform a diffstat and set the model's diff text."""
373 Command
.__init
__(self
)
374 diff
= self
.model
.git
.diff(self
.model
.head
,
375 unified
=self
.model
.diff_context
,
379 self
.new_diff_text
= core
.decode(diff
)
380 self
.new_mode
= self
.model
.mode_worktree
383 class DiffStaged(Diff
):
384 """Perform a staged diff on a file."""
385 def __init__(self
, filenames
):
386 Diff
.__init
__(self
, filenames
, cached
=True)
387 if not self
.model
.read_only():
388 if self
.model
.mode
!= self
.model
.mode_amend
:
389 self
.new_mode
= self
.model
.mode_index
392 class DiffStagedSummary(Command
):
394 Command
.__init
__(self
)
395 cached
= not self
.model
.read_only()
396 diff
= self
.model
.git
.diff(self
.model
.head
,
399 patch_with_stat
=True,
401 self
.new_diff_text
= core
.decode(diff
)
402 if not self
.model
.read_only():
403 if self
.model
.mode
!= self
.model
.mode_amend
:
404 self
.new_mode
= self
.model
.mode_index
407 class Difftool(Command
):
408 """Run git-difftool limited by path."""
409 def __init__(self
, staged
, filenames
):
410 Command
.__init
__(self
)
412 self
.filenames
= filenames
415 if not self
.filenames
:
418 if self
.staged
and not self
.model
.read_only():
419 args
.append('--cached')
420 args
.extend([self
.model
.head
, '--'])
421 args
.extend(self
.filenames
)
422 difftool
.launch(args
)
426 """Edit a file using the configured gui.editor."""
427 def __init__(self
, filenames
, line_number
=None):
428 Command
.__init
__(self
)
429 self
.filenames
= filenames
430 self
.line_number
= line_number
433 filename
= self
.filenames
[0]
434 if not os
.path
.exists(filename
):
436 editor
= self
.model
.editor()
437 if 'vi' in editor
and self
.line_number
:
438 utils
.fork([editor
, filename
, '+'+self
.line_number
])
440 utils
.fork([editor
, filename
])
443 class FormatPatch(Command
):
444 """Output a patch series given all revisions and a selected subset."""
445 def __init__(self
, to_export
, revs
):
446 Command
.__init
__(self
)
447 self
.to_export
= to_export
451 status
, output
= gitcmds
.format_patchsets(self
.to_export
, self
.revs
)
452 _notifier
.broadcast(signals
.log_cmd
, status
, output
)
455 class GrepMode(Command
):
456 def __init__(self
, txt
):
457 """Perform a git-grep."""
458 Command
.__init
__(self
)
459 self
.new_mode
= self
.model
.mode_grep
460 self
.new_diff_text
= self
.model
.git
.grep(txt
, n
=True)
463 class LoadCommitMessage(Command
):
464 """Loads a commit message from a path."""
465 def __init__(self
, path
):
466 Command
.__init
__(self
)
469 self
.old_commitmsg
= self
.model
.commitmsg
470 self
.old_directory
= self
.model
.directory
473 self
.model
.set_directory(os
.path
.dirname(self
.path
))
474 fh
= open(self
.path
, 'r')
475 contents
= core
.decode(core
.read_nointr(fh
))
477 self
.model
.set_commitmsg(contents
)
480 self
.model
.set_commitmsg(self
.old_commitmsg
)
481 self
.model
.set_directory(self
.old_directory
)
484 class Mergetool(Command
):
485 """Launch git-mergetool on a list of paths."""
486 def __init__(self
, paths
):
487 Command
.__init
__(self
)
493 if version
.check('mergetool-no-prompt',
494 self
.model
.git
.version().split()[-1]):
495 utils
.fork(['git', 'mergetool', '--no-prompt', '--'] + self
.paths
)
497 utils
.fork(['xterm', '-e', 'git', 'mergetool', '--'] + self
.paths
)
500 class OpenRepo(Command
):
501 """Launches git-cola on a repo."""
502 def __init__(self
, dirname
):
503 Command
.__init
__(self
)
504 self
.new_directory
= utils
.quote_repopath(dirname
)
507 self
.model
.set_directory(self
.new_directory
)
508 utils
.fork(['python', sys
.argv
[0], '--repo', self
.new_directory
])
511 class Clone(Command
):
512 """Clones a repository and optionally spawns a new cola session."""
513 def __init__(self
, url
, destdir
, spawn
=True):
514 Command
.__init
__(self
)
516 self
.new_directory
= utils
.quote_repopath(destdir
)
520 self
.model
.git
.clone(self
.url
, self
.new_directory
,
521 with_stderr
=True, with_status
=True)
523 utils
.fork(['python', sys
.argv
[0], '--repo', self
.new_directory
])
526 class Rescan(Command
):
527 """Rescans for changes."""
529 Command
.__init
__(self
, update
=True)
532 class ReviewBranchMode(Command
):
533 """Enter into review-branch mode."""
534 def __init__(self
, branch
):
535 Command
.__init
__(self
, update
=True)
536 self
.new_mode
= self
.model
.mode_review
537 self
.new_head
= gitcmds
.merge_base_to(branch
)
538 self
.new_diff_text
= ''
541 class ShowUntracked(Command
):
542 """Show an untracked file."""
543 # We don't actually do anything other than set the mode right now.
544 # We could probably check the mimetype for the file and handle things
546 def __init__(self
, filenames
):
547 Command
.__init
__(self
)
548 self
.new_mode
= self
.model
.mode_worktree
549 # TODO new_diff_text = utils.file_preview(filenames[0])
552 class Stage(Command
):
553 """Stage a set of paths."""
554 def __init__(self
, paths
):
555 Command
.__init
__(self
)
559 msg
= 'Staging: %s' % (', '.join(self
.paths
))
560 _notifier
.broadcast(signals
.log_cmd
, 0, msg
)
561 self
.model
.stage_paths(self
.paths
)
564 class StageModified(Stage
):
565 """Stage all modified files."""
567 Stage
.__init
__(self
, None)
568 self
.paths
= self
.model
.modified
571 class StageUntracked(Stage
):
572 """Stage all untracked files."""
574 Stage
.__init
__(self
, None)
575 self
.paths
= self
.model
.untracked
578 """Create a tag object."""
579 def __init__(self
, name
, revision
, sign
=False, message
=''):
580 Command
.__init
__(self
)
582 self
._message
= core
.encode(message
)
583 self
._revision
= revision
587 log_msg
= 'Tagging: "%s" as "%s"' % (self
._revision
, self
._name
)
589 log_msg
+= ', GPG-signed'
590 path
= cola
.model().tmp_filename()
591 utils
.write(path
, self
._message
)
592 status
, output
= cola
.model().git
.tag(self
._name
,
600 status
, output
= cola
.model().git
.tag(self
._name
,
605 log_msg
+= '\nOutput:\n%s' % output
607 _notifier
.broadcast(signals
.log_cmd
, status
, log_msg
)
609 cola
.model().update_status()
612 class Unstage(Command
):
613 """Unstage a set of paths."""
614 def __init__(self
, paths
):
615 Command
.__init
__(self
)
619 msg
= 'Unstaging: %s' % (', '.join(self
.paths
))
620 _notifier
.broadcast(signals
.log_cmd
, 0, msg
)
621 gitcmds
.unstage_paths(self
.paths
)
622 self
.model
.update_status()
625 class UnstageAll(Command
):
626 """Unstage all files; resets the index."""
628 Command
.__init
__(self
, update
=True)
631 self
.model
.unstage_all()
634 class UnstageSelected(Unstage
):
635 """Unstage selected files."""
637 Unstage
.__init
__(self
, cola
.selection_model().staged
)
640 class UntrackedSummary(Command
):
641 """List possible .gitignore rules as the diff text."""
643 Command
.__init
__(self
)
644 untracked
= self
.model
.untracked
645 suffix
= len(untracked
) > 1 and 's' or ''
647 io
.write('# %s untracked file%s\n' % (len(untracked
), suffix
))
649 io
.write('# possible .gitignore rule%s:\n' % suffix
)
651 io
.write('/%s\n' % u
)
652 self
.new_diff_text
= io
.getvalue()
655 class VisualizeAll(Command
):
656 """Visualize all branches."""
658 browser
= self
.model
.history_browser()
659 utils
.fork([browser
, '--all'])
662 class VisualizeCurrent(Command
):
663 """Visualize all branches."""
665 browser
= self
.model
.history_browser()
666 utils
.fork([browser
, self
.model
.currentbranch
])
669 class VisualizePaths(Command
):
670 """Path-limited visualization."""
671 def __init__(self
, paths
):
672 Command
.__init
__(self
)
673 browser
= self
.model
.history_browser()
675 self
.argv
= [browser
] + paths
677 self
.argv
= [browser
]
680 utils
.fork(self
.argv
)
685 Register signal mappings with the factory.
687 These commands are automatically created and run when
688 their corresponding signal is broadcast by the notifier.
691 signal_to_command_map
= {
692 signals
.add_signoff
: AddSignoff
,
693 signals
.amend_mode
: AmendMode
,
694 signals
.apply_diff_selection
: ApplyDiffSelection
,
695 signals
.apply_patches
: ApplyPatches
,
696 signals
.branch_mode
: BranchMode
,
697 signals
.clone
: Clone
,
698 signals
.checkout
: Checkout
,
699 signals
.checkout_branch
: CheckoutBranch
,
700 signals
.cherry_pick
: CherryPick
,
701 signals
.commit
: Commit
,
702 signals
.delete
: Delete
,
703 signals
.delete_branch
: DeleteBranch
,
705 signals
.diff_mode
: DiffMode
,
706 signals
.diff_expr_mode
: DiffExprMode
,
707 signals
.diff_staged
: DiffStaged
,
708 signals
.diffstat
: Diffstat
,
709 signals
.difftool
: Difftool
,
711 signals
.format_patch
: FormatPatch
,
712 signals
.grep
: GrepMode
,
713 signals
.load_commit_message
: LoadCommitMessage
,
714 signals
.modified_summary
: Diffstat
,
715 signals
.mergetool
: Mergetool
,
716 signals
.open_repo
: OpenRepo
,
717 signals
.rescan
: Rescan
,
718 signals
.reset_mode
: ResetMode
,
719 signals
.review_branch_mode
: ReviewBranchMode
,
720 signals
.show_untracked
: ShowUntracked
,
721 signals
.stage
: Stage
,
722 signals
.stage_modified
: StageModified
,
723 signals
.stage_untracked
: StageUntracked
,
724 signals
.staged_summary
: DiffStagedSummary
,
726 signals
.unstage
: Unstage
,
727 signals
.unstage_all
: UnstageAll
,
728 signals
.unstage_selected
: UnstageSelected
,
729 signals
.untracked_summary
: UntrackedSummary
,
730 signals
.visualize_all
: VisualizeAll
,
731 signals
.visualize_current
: VisualizeCurrent
,
732 signals
.visualize_paths
: VisualizePaths
,
735 for signal
, cmd
in signal_to_command_map
.iteritems():
736 _factory
.add_command(signal
, cmd
)