8 from plugin
import YapPlugin
11 class ShellError(Exception):
12 def __init__(self
, cmd
, rc
):
17 return "%s returned %d" % (self
.cmd
, self
.rc
)
19 class YapError(Exception):
20 def __init__(self
, msg
):
30 plugindir
= os
.path
.expanduser("~/.yap/plugins")
31 for p
in glob
.glob(os
.path
.join(plugindir
, "*.py")):
34 for k
, cls
in glbls
.items():
35 if not type(cls
) == type:
37 if not issubclass(cls
, YapPlugin
):
44 if not func
.startswith('cmd_'):
46 if func
in self
.overrides
:
47 print >>sys
.stderr
, "Plugin %s overrides already overridden function %s. Disabling" % (p
, func
)
51 def _add_new_file(self
, file):
52 repo
= get_output('git rev-parse --git-dir')[0]
53 dir = os
.path
.join(repo
, 'yap')
58 files
= self
._get
_new
_files
()
60 path
= os
.path
.join(dir, 'new-files')
61 pickle
.dump(files
, open(path
, 'w'))
63 def _get_new_files(self
):
64 repo
= get_output('git rev-parse --git-dir')[0]
65 path
= os
.path
.join(repo
, 'yap', 'new-files')
67 files
= pickle
.load(file(path
))
74 if get_output("git ls-files --cached '%s'" % f
) != []:
79 def _remove_new_file(self
, file):
80 files
= self
._get
_new
_files
()
81 files
= filter(lambda x
: x
!= file, files
)
83 repo
= get_output('git rev-parse --git-dir')[0]
84 path
= os
.path
.join(repo
, 'yap', 'new-files')
86 pickle
.dump(files
, open(path
, 'w'))
90 def _clear_new_files(self
):
91 repo
= get_output('git rev-parse --git-dir')[0]
92 path
= os
.path
.join(repo
, 'yap', 'new-files')
95 def _assert_file_exists(self
, file):
96 if not os
.access(file, os
.R_OK
):
97 raise YapError("No such file: %s" % file)
99 def _get_staged_files(self
):
100 if run_command("git rev-parse HEAD"):
101 files
= get_output("git ls-files --cached")
103 files
= get_output("git diff-index --cached --name-only HEAD")
104 unmerged
= self
._get
_unmerged
_files
()
106 unmerged
= set(unmerged
)
107 files
= set(files
).difference(unmerged
)
111 def _get_unstaged_files(self
):
112 files
= get_output("git ls-files -m")
113 prefix
= get_output("git rev-parse --show-prefix")
115 files
= [ os
.path
.join(prefix
[0], x
) for x
in files
]
116 files
+= self
._get
_new
_files
()
117 unmerged
= self
._get
_unmerged
_files
()
119 unmerged
= set(unmerged
)
120 files
= set(files
).difference(unmerged
)
124 def _get_unmerged_files(self
):
125 files
= get_output("git ls-files -u")
126 files
= [ x
.replace('\t', ' ').split(' ')[3] for x
in files
]
127 prefix
= get_output("git rev-parse --show-prefix")
129 files
= [ os
.path
.join(prefix
[0], x
) for x
in files
]
130 return list(set(files
))
132 def _delete_branch(self
, branch
, force
):
133 current
= get_output("git symbolic-ref HEAD")
135 current
= current
[0].replace('refs/heads/', '')
136 if branch
== current
:
137 raise YapError("Can't delete current branch")
139 ref
= get_output("git rev-parse --verify 'refs/heads/%s'" % branch
)
141 raise YapError("No such branch: %s" % branch
)
142 run_safely("git update-ref -d 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
145 name
= get_output("git name-rev --name-only '%s'" % ref
[0])[0]
146 if name
== 'undefined':
147 run_command("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
148 raise YapError("Refusing to delete leaf branch (use -f to force)")
149 def _get_pager_cmd(self
):
150 if 'YAP_PAGER' in os
.environ
:
151 return os
.environ
['YAP_PAGER']
152 elif 'GIT_PAGER' in os
.environ
:
153 return os
.environ
['GIT_PAGER']
154 elif 'PAGER' in os
.environ
:
155 return os
.environ
['PAGER']
159 def _add_one(self
, file):
160 self
._assert
_file
_exists
(file)
161 x
= get_output("git ls-files '%s'" % file)
163 raise YapError("File '%s' already in repository" % file)
164 self
._add
_new
_file
(file)
166 def _rm_one(self
, file):
167 self
._assert
_file
_exists
(file)
168 if get_output("git ls-files '%s'" % file) != []:
169 run_safely("git rm --cached '%s'" % file)
170 self
._remove
_new
_file
(file)
172 def _stage_one(self
, file, allow_unmerged
=False):
173 self
._assert
_file
_exists
(file)
174 prefix
= get_output("git rev-parse --show-prefix")
176 tmp
= os
.path
.normpath(os
.path
.join(prefix
[0], file))
179 if not allow_unmerged
and tmp
in self
._get
_unmerged
_files
():
180 raise YapError("Refusing to stage conflicted file: %s" % file)
181 run_safely("git update-index --add '%s'" % file)
183 def _unstage_one(self
, file):
184 self
._assert
_file
_exists
(file)
185 if run_command("git rev-parse HEAD"):
186 rc
= run_command("git update-index --force-remove '%s'" % file)
188 rc
= run_command("git diff-index -p HEAD '%s' | git apply -R --cached" % file)
190 raise YapError("Failed to unstage")
192 def _revert_one(self
, file):
193 self
._assert
_file
_exists
(file)
195 self
._unstage
_one
(file)
198 run_safely("git checkout-index -u -f '%s'" % file)
200 def _parse_commit(self
, commit
):
201 lines
= get_output("git cat-file commit '%s'" % commit
)
206 if mode
!= 'commit' and l
.strip() == "":
211 commit
['log'].append(l
)
218 commit
['log'] = '\n'.join(commit
['log'])
221 def _check_commit(self
, **flags
):
222 if '-a' in flags
and '-d' in flags
:
223 raise YapError("Conflicting flags: -a and -d")
225 if '-d' not in flags
and self
._get
_unstaged
_files
():
226 if '-a' not in flags
and self
._get
_staged
_files
():
227 raise YapError("Staged and unstaged changes present. Specify what to commit")
228 os
.system("git diff-files -p | git apply --cached")
229 for f
in self
._get
_new
_files
():
232 def _do_uncommit(self
):
233 commit
= self
._parse
_commit
("HEAD")
234 repo
= get_output('git rev-parse --git-dir')[0]
235 dir = os
.path
.join(repo
, 'yap')
240 msg_file
= os
.path
.join(dir, 'msg')
241 fd
= file(msg_file
, 'w')
242 print >>fd
, commit
['log']
245 tree
= get_output("git rev-parse --verify HEAD^")
246 run_safely("git update-ref -m uncommit HEAD '%s'" % tree
[0])
248 def _do_commit(self
, msg
=None):
249 tree
= get_output("git write-tree")[0]
251 repo
= get_output('git rev-parse --git-dir')[0]
252 head_file
= os
.path
.join(repo
, 'yap', 'merge')
254 parent
= pickle
.load(file(head_file
))
256 parent
= get_output("git rev-parse --verify HEAD 2> /dev/null")
258 if os
.environ
.has_key('YAP_EDITOR'):
259 editor
= os
.environ
['YAP_EDITOR']
260 elif os
.environ
.has_key('GIT_EDITOR'):
261 editor
= os
.environ
['GIT_EDITOR']
262 elif os
.environ
.has_key('EDITOR'):
263 editor
= os
.environ
['EDITOR']
267 fd
, tmpfile
= tempfile
.mkstemp("yap")
272 msg_file
= os
.path
.join(repo
, 'yap', 'msg')
273 if os
.access(msg_file
, os
.R_OK
):
275 fd2
= file(tmpfile
, 'w')
276 for l
in fd1
.xreadlines():
277 print >>fd2
, l
.strip()
280 if os
.system("%s '%s'" % (editor
, tmpfile
)) != 0:
281 raise YapError("Editing commit message failed")
288 raise YapError("Refusing to use empty commit message")
290 (fd_w
, fd_r
) = os
.popen2("git stripspace > %s" % tmpfile
)
296 parent
= ' -p '.join(parent
)
297 commit
= get_output("git commit-tree '%s' -p %s < '%s'" % (tree
, parent
, tmpfile
))
299 commit
= get_output("git commit-tree '%s' < '%s'" % (tree
, tmpfile
))
302 run_safely("git update-ref HEAD '%s'" % commit
[0])
305 def _check_rebasing(self
):
306 repo
= get_output('git rev-parse --git-dir')[0]
307 dotest
= os
.path
.join(repo
, '.dotest')
308 if os
.access(dotest
, os
.R_OK
):
309 raise YapError("A git operation is in progress. Complete it first")
310 dotest
= os
.path
.join(repo
, '..', '.dotest')
311 if os
.access(dotest
, os
.R_OK
):
312 raise YapError("A git operation is in progress. Complete it first")
314 def _check_git(self
):
315 if run_command("git rev-parse --git-dir"):
316 raise YapError("That command must be run from inside a git repository")
318 def _list_remotes(self
):
319 remotes
= get_output("git config --get-regexp '^remote.*.url'")
321 remote
, url
= x
.split(' ')
322 remote
= remote
.replace('remote.', '')
323 remote
= remote
.replace('.url', '')
326 def _unstage_all(self
):
328 run_safely("git read-tree -m HEAD")
330 run_safely("git read-tree HEAD")
331 run_safely("git update-index -q --refresh")
333 def _get_tracking(self
, current
):
334 remote
= get_output("git config branch.%s.remote" % current
)
336 raise YapError("No tracking branch configured for '%s'" % current
)
338 merge
= get_output("git config branch.%s.merge" % current
)
340 raise YapError("No tracking branch configured for '%s'" % current
)
341 return remote
[0], merge
[0]
343 def __getattribute__(self
, attr
):
344 if attr
.startswith("cmd_"):
346 for p
in self
.plugins
.values():
348 meth
= p
.__getattribute
__(attr
)
350 except AttributeError:
355 return super(Yap
, self
).__getattribute
__(attr
)
357 def _call_base(self
, method
, *args
, **flags
):
358 base_method
= super(Yap
, self
).__getattribute
__(method
)
359 return base_method(*args
, **flags
)
360 def _confirm_push(self
, current
, rhs
, repo
):
361 print "About to push local branch '%s' to '%s' on '%s'" % (current
, rhs
, repo
)
362 print "Continue (y/n)? ",
364 ans
= sys
.stdin
.readline().strip()
366 if ans
.lower() != 'y' and ans
.lower() != 'yes':
367 raise YapError("Aborted.")
369 def _clear_state(self
):
370 repo
= get_output('git rev-parse --git-dir')[0]
371 dir = os
.path
.join(repo
, 'yap')
373 os
.unlink(os
.path
.join(dir, 'new-files'))
374 os
.unlink(os
.path
.join(dir, 'merge'))
375 os
.unlink(os
.path
.join(dir, 'msg'))
379 @short_help("make a local copy of an existing repository")
381 The first argument is a URL to the existing repository. This can be an
382 absolute path if the repository is local, or a URL with the git://,
383 ssh://, or http:// schemes. By default, the directory used is the last
384 component of the URL, sans '.git'. This can be overridden by providing
387 def cmd_clone(self
, url
, directory
=None):
390 if '://' not in url
and url
[0] != '/':
391 url
= os
.path
.join(os
.getcwd(), url
)
393 url
= url
.rstrip('/')
394 if directory
is None:
395 directory
= url
.rsplit('/')[-1]
396 directory
= directory
.replace('.git', '')
401 raise YapError("Directory exists: %s" % directory
)
404 self
.cmd_repo("origin", url
)
405 self
.cmd_fetch("origin")
408 if not run_command("git rev-parse --verify refs/remotes/origin/HEAD"):
409 hash = get_output("git rev-parse refs/remotes/origin/HEAD")[0]
410 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'"):
411 if get_output("git rev-parse %s" % b
)[0] == hash:
415 if not run_command("git rev-parse --verify refs/remotes/origin/master"):
416 branch
= "refs/remotes/origin/master"
418 branch
= get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'")
421 hash = get_output("git rev-parse %s" % branch
)
423 branch
= branch
.replace('refs/remotes/origin/', '')
424 run_safely("git update-ref refs/heads/%s %s" % (branch
, hash[0]))
425 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
426 self
.cmd_revert(**{'-a': 1})
428 @short_help("turn a directory into a repository")
430 Converts the current working directory into a repository. The primary
431 side-effect of this command is the creation of a '.git' subdirectory.
432 No files are added nor commits made.
435 os
.system("git init")
437 @short_help("add a new file to the repository")
439 The arguments are the files to be added to the repository. Once added,
440 the files will show as "unstaged changes" in the output of 'status'. To
441 reverse the effects of this command, see 'rm'.
443 def cmd_add(self
, *files
):
454 @short_help("delete a file from the repository")
456 The arguments are the files to be removed from the current revision of
457 the repository. The files will still exist in any past commits that the
458 files may have been a part of. The file is not actually deleted, it is
459 just no longer tracked as part of the repository.
461 def cmd_rm(self
, *files
):
471 @short_help("stage changes in a file for commit")
473 The arguments are the files to be staged. Staging changes is a way to
474 build up a commit when you do not want to commit all changes at once.
475 To commit only staged changes, use the '-d' flag to 'commit.' To
476 reverse the effects of this command, see 'unstage'. Once staged, the
477 files will show as "staged changes" in the output of 'status'.
479 def cmd_stage(self
, *files
):
489 @short_help("unstage changes in a file")
491 The arguments are the files to be unstaged. Once unstaged, the files
492 will show as "unstaged changes" in the output of 'status'. The '-a'
493 flag can be used to unstage all staged changes at once.
496 def cmd_unstage(self
, *files
, **flags
):
511 @short_help("show files with staged and unstaged changes")
513 Show the files in the repository with changes since the last commit,
514 categorized based on whether the changes are staged or not. A file may
515 appear under each heading if the same file has both staged and unstaged
518 def cmd_status(self
):
521 branch
= get_output("git symbolic-ref HEAD")
523 branch
= branch
[0].replace('refs/heads/', '')
526 print "Current branch: %s" % branch
528 print "Files with staged changes:"
529 files
= self
._get
_staged
_files
()
535 print "Files with unstaged changes:"
536 files
= self
._get
_unstaged
_files
()
542 files
= self
._get
_unmerged
_files
()
544 print "Files with conflicts:"
548 @short_help("remove uncommitted changes from a file (*)")
550 The arguments are the files whose changes will be reverted. If the '-a'
551 flag is given, then all files will have uncommitted changes removed.
552 Note that there is no way to reverse this command short of manually
553 editing each file again.
556 def cmd_revert(self
, *files
, **flags
):
561 run_safely("git checkout-index -u -f -a")
573 @short_help("record changes to files as a new commit")
575 Create a new commit recording changes since the last commit. If there
576 are only unstaged changes, those will be recorded. If there are only
577 staged changes, those will be recorded. Otherwise, you will have to
578 specify either the '-a' flag or the '-d' flag to commit all changes or
579 only staged changes, respectively. To reverse the effects of this
580 command, see 'uncommit'.
582 @takes_options("adm:")
583 def cmd_commit(self
, **flags
):
584 "[-a | -d] [-m <msg>]"
586 self
._check
_rebasing
()
587 self
._check
_commit
(**flags
)
588 if not self
._get
_staged
_files
():
589 raise YapError("No changes to commit")
590 msg
= flags
.get('-m', None)
594 @short_help("reverse the actions of the last commit")
596 Reverse the effects of the last 'commit' operation. The changes that
597 were part of the previous commit will show as "staged changes" in the
598 output of 'status'. This means that if no files were changed since the
599 last commit was created, 'uncommit' followed by 'commit' is a lossless
602 def cmd_uncommit(self
):
608 @short_help("report the current version of yap")
609 def cmd_version(self
):
610 print "Yap version 0.1"
612 @short_help("show the changelog for particular versions or files")
614 The arguments are the files with which to filter history. If none are
615 given, all changes are listed. Otherwise only commits that affected one
616 or more of the given files are listed. The -r option changes the
617 starting revision for traversing history. By default, history is listed
620 @takes_options("pr:")
621 def cmd_log(self
, *paths
, **flags
):
622 "[-p] [-r <rev>] <path>..."
624 rev
= flags
.get('-r', 'HEAD')
633 paths
= ' '.join(paths
)
634 os
.system("git log -M -C %s %s '%s' -- %s"
635 % (follow
, flags
.get('-p', '--name-status'), rev
, paths
))
637 @short_help("show staged, unstaged, or all uncommitted changes")
639 Show staged, unstaged, or all uncommitted changes. By default, all
640 changes are shown. The '-u' flag causes only unstaged changes to be
641 shown. The '-d' flag causes only staged changes to be shown.
644 def cmd_diff(self
, **flags
):
647 if '-u' in flags
and '-d' in flags
:
648 raise YapError("Conflicting flags: -u and -d")
650 pager
= self
._get
_pager
_cmd
()
653 os
.system("git diff-files -p | %s" % pager
)
655 os
.system("git diff-index --cached -p HEAD | %s" % pager
)
657 os
.system("git diff-index -p HEAD | %s" % pager
)
659 @short_help("list, create, or delete branches")
661 If no arguments are specified, a list of local branches is given. The
662 current branch is indicated by a "*" next to the name. If an argument
663 is given, it is taken as the name of a new branch to create. The branch
664 will start pointing at the current HEAD. See 'point' for details on
665 changing the revision of the new branch. Note that this command does
666 not switch the current working branch. See 'switch' for details on
667 changing the current working branch.
669 The '-d' flag can be used to delete local branches. If the delete
670 operation would remove the last branch reference to a given line of
671 history (colloquially referred to as "dangling commits"), yap will
672 report an error and abort. The '-f' flag can be used to force the delete
675 @takes_options("fd:")
676 def cmd_branch(self
, branch
=None, **flags
):
677 "[ [-f] -d <branch> | <branch> ]"
679 force
= '-f' in flags
681 self
._delete
_branch
(flags
['-d'], force
)
685 if branch
is not None:
686 ref
= get_output("git rev-parse --verify HEAD")
688 raise YapError("No branch point yet. Make a commit")
689 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
691 current
= get_output("git symbolic-ref HEAD")
692 branches
= get_output("git for-each-ref --format='%(refname)' 'refs/heads/*'")
694 if current
and b
== current
[0]:
698 b
= b
.replace('refs/heads/', '')
701 @short_help("change the current working branch")
703 The argument is the name of the branch to make the current working
704 branch. This command will fail if there are uncommitted changes to any
705 files. Otherwise, the contents of the files in the working directory
706 are updated to reflect their state in the new branch. Additionally, any
707 future commits are added to the new branch instead of the previous line
711 def cmd_switch(self
, branch
, **flags
):
714 self
._check
_rebasing
()
715 ref
= get_output("git rev-parse --verify 'refs/heads/%s'" % branch
)
717 raise YapError("No such branch: %s" % branch
)
719 if '-f' not in flags
:
720 if (self
._get
_staged
_files
()
721 or (self
._get
_unstaged
_files
()
722 and run_command("git update-index --refresh"))):
723 raise YapError("You have uncommitted changes. Use -f to continue anyway")
725 if self
._get
_unstaged
_files
() and self
._get
_staged
_files
():
726 raise YapError("You have staged and unstaged changes. Perhaps unstage -a?")
728 staged
= bool(self
._get
_staged
_files
())
730 run_command("git diff-files -p | git apply --cached")
731 for f
in self
._get
_new
_files
():
734 idx
= get_output("git write-tree")
735 new
= get_output("git rev-parse refs/heads/%s" % branch
)
736 readtree
= "git read-tree --aggressive -u -m HEAD %s %s" % (idx
[0], new
[0])
737 if run_command(readtree
):
738 run_command("git update-index --refresh")
739 if os
.system(readtree
):
740 raise YapError("Failed to switch")
741 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
743 if '-f' not in flags
:
750 @short_help("move the current branch to a different revision")
752 The argument is the hash of the commit to which the current branch
753 should point, or alternately a branch or tag (a.k.a, "committish"). If
754 moving the branch would create "dangling commits" (see 'branch'), yap
755 will report an error and abort. The '-f' flag can be used to force the
756 operation in spite of this.
759 def cmd_point(self
, where
, **flags
):
762 self
._check
_rebasing
()
764 head
= get_output("git rev-parse --verify HEAD")
766 raise YapError("No commit yet; nowhere to point")
768 ref
= get_output("git rev-parse --verify '%s^{commit}'" % where
)
770 raise YapError("Not a valid ref: %s" % where
)
772 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
773 raise YapError("You have uncommitted changes. Commit them first")
775 run_safely("git update-ref HEAD '%s'" % ref
[0])
777 if '-f' not in flags
:
778 name
= get_output("git name-rev --name-only '%s'" % head
[0])[0]
779 if name
== "undefined":
780 os
.system("git update-ref HEAD '%s'" % head
[0])
781 raise YapError("Pointing there will lose commits. Use -f to force")
784 run_safely("git read-tree -u -m HEAD")
786 run_safely("git read-tree HEAD")
787 run_safely("git checkout-index -u -f -a")
790 @short_help("alter history by dropping or amending commits")
792 This command operates in two distinct modes, "amend" and "drop" mode.
793 In drop mode, the given commit is removed from the history of the
794 current branch, as though that commit never happened. By default the
797 In amend mode, the uncommitted changes present are merged into a
798 previous commit. This is useful for correcting typos or adding missed
799 files into past commits. By default the commit used is HEAD.
801 While rewriting history it is possible that conflicts will arise. If
802 this happens, the rewrite will pause and you will be prompted to resolve
803 the conflicts and stage them. Once that is done, you will run "yap
804 history continue." If instead you want the conflicting commit removed
805 from history (perhaps your changes supercede that commit) you can run
806 "yap history skip". Once the rewrite completes, your branch will be on
807 the same commit as when the rewrite started.
809 def cmd_history(self
, subcmd
, *args
):
810 "amend | drop <commit>"
813 if subcmd
not in ("amend", "drop", "continue", "skip"):
817 When you have resolved the conflicts run \"yap history continue\".
818 To skip the problematic patch, run \"yap history skip\"."""
820 if subcmd
== "continue":
821 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
824 os
.system("git reset --hard")
825 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
828 if subcmd
== "amend":
829 flags
, args
= getopt
.getopt(args
, "ad")
839 if run_command("git rev-parse --verify '%s'" % commit
):
840 raise YapError("Not a valid commit: %s" % commit
)
842 self
._check
_rebasing
()
844 if subcmd
== "amend":
845 self
._check
_commit
(**flags
)
846 if self
._get
_unstaged
_files
():
847 # XXX: handle unstaged changes better
848 raise YapError("Commit away changes that you aren't amending")
852 start
= get_output("git rev-parse HEAD")
853 stash
= get_output("git stash create")
854 run_command("git reset --hard")
856 fd
, tmpfile
= tempfile
.mkstemp("yap")
860 os
.system("git format-patch -k --stdout '%s' > %s" % (commit
, tmpfile
))
861 if subcmd
== "amend":
862 self
.cmd_point(commit
, **{'-f': True})
864 if subcmd
== "amend":
866 rc
= os
.system("git stash apply %s" % stash
[0])
868 self
.cmd_point(start
[0], **{'-f': True})
869 os
.system("git stash apply %s" % stash
[0])
870 raise YapError("Failed to apply stash")
873 if subcmd
== "amend":
875 self
._check
_commit
(**{'-a': True})
878 self
.cmd_point("%s^" % commit
, **{'-f': True})
880 stat
= os
.stat(tmpfile
)
883 run_safely("git update-index --refresh")
884 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
886 raise YapError("Failed to apply changes")
891 run_command("git stash apply %s" % stash
[0])
894 @short_help("show the changes introduced by a given commit")
896 By default, the changes in the last commit are shown. To override this,
897 specify a hash, branch, or tag (committish). The hash of the commit,
898 the commit's author, log message, and a diff of the changes are shown.
900 def cmd_show(self
, commit
="HEAD"):
903 os
.system("git show '%s'" % commit
)
905 @short_help("apply the changes in a given commit to the current branch")
907 The argument is the hash, branch, or tag (committish) of the commit to
908 be applied. In general, it only makes sense to apply commits that
909 happened on another branch. The '-r' flag can be used to have the
910 changes in the given commit reversed from the current branch. In
911 general, this only makes sense for commits that happened on the current
915 def cmd_cherry_pick(self
, commit
, **flags
):
919 os
.system("git revert '%s'" % commit
)
921 os
.system("git cherry-pick '%s'" % commit
)
923 @short_help("list, add, or delete configured remote repositories")
925 When invoked with no arguments, this command will show the list of
926 currently configured remote repositories, giving both the name and URL
927 of each. To add a new repository, give the desired name as the first
928 argument and the URL as the second. The '-d' flag can be used to remove
929 a previously added repository.
932 def cmd_repo(self
, name
=None, url
=None, **flags
):
933 "[<name> <url> | -d <name>]"
935 if name
is not None and url
is None:
939 if flags
['-d'] not in [ x
[0] for x
in self
._list
_remotes
() ]:
940 raise YapError("No such repository: %s" % flags
['-d'])
941 os
.system("git config --unset remote.%s.url" % flags
['-d'])
942 os
.system("git config --unset remote.%s.fetch" % flags
['-d'])
945 if name
in [ x
[0] for x
in self
._list
_remotes
() ]:
946 raise YapError("Repository '%s' already exists" % flags
['-d'])
947 os
.system("git config remote.%s.url %s" % (name
, url
))
948 os
.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name
, name
))
950 for remote
, url
in self
._list
_remotes
():
951 print "%-20s %s" % (remote
, url
)
953 @short_help("send local commits to a remote repository (*)")
955 When invoked with no arguments, the current branch is synchronized to
956 the tracking branch of the tracking remote. If no tracking remote is
957 specified, the repository will have to be specified on the command line.
958 In that case, the default is to push to a branch with the same name as
959 the current branch. This behavior can be overridden by giving a second
960 argument to specify the remote branch.
962 If the remote branch does not currently exist, the command will abort
963 unless the -c flag is provided. If the remote branch is not a direct
964 descendent of the local branch, the command will abort unless the -f
965 flag is provided. Forcing a push in this way can be problematic to
966 other users of the repository if they are not expecting it.
968 To delete a branch on the remote repository, use the -d flag.
970 @takes_options("cdf")
971 def cmd_push(self
, repo
=None, rhs
=None, **flags
):
974 if '-c' in flags
and '-d' in flags
:
977 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
978 raise YapError("No such repository: %s" % repo
)
980 current
= get_output("git symbolic-ref HEAD")
982 raise YapError("Not on a branch!")
984 self
._check
_rebasing
()
986 current
= current
[0].replace('refs/heads/', '')
987 remote
= get_output("git config branch.%s.remote" % current
)
988 if repo
is None and remote
:
992 raise YapError("No tracking branch configured; specify destination repository")
994 if rhs
is None and remote
and remote
[0] == repo
:
995 merge
= get_output("git config branch.%s.merge" % current
)
1000 rhs
= "refs/heads/%s" % current
1002 if '-c' not in flags
and '-d' not in flags
:
1003 if run_command("git rev-parse --verify refs/remotes/%s/%s"
1004 % (repo
, rhs
.replace('refs/heads/', ''))):
1005 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
1006 if '-f' not in flags
:
1007 hash = get_output("git rev-parse refs/remotes/%s/%s" % (repo
, rhs
.replace('refs/heads/', '')))
1008 base
= get_output("git merge-base HEAD %s" % hash[0])
1010 if base
[0] != hash[0]:
1011 raise YapError("Branch not up-to-date with remote. Update or use -f")
1013 self
._confirm
_push
(current
, rhs
, repo
)
1020 lhs
= "refs/heads/%s" % current
1021 rc
= os
.system("git push %s %s %s:%s" % (flags
.get('-f', ''), repo
, lhs
, rhs
))
1023 raise YapError("Push failed.")
1025 @short_help("retrieve commits from a remote repository")
1027 When run with no arguments, the command will retrieve new commits from
1028 the remote tracking repository. Note that this does not in any way
1029 alter the current branch. For that, see "update". If a remote other
1030 than the tracking remote is desired, it can be specified as the first
1033 def cmd_fetch(self
, repo
=None):
1036 current
= get_output("git symbolic-ref HEAD")
1038 raise YapError("Not on a branch!")
1040 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1041 raise YapError("No such repository: %s" % repo
)
1043 current
= current
[0].replace('refs/heads/', '')
1044 remote
= get_output("git config branch.%s.remote" % current
)
1048 raise YapError("No tracking branch configured; specify a repository")
1049 os
.system("git fetch %s" % repo
)
1051 @short_help("update the current branch relative to its tracking branch")
1053 Updates the current branch relative to its remote tracking branch. This
1054 command requires that the current branch have a remote tracking branch
1055 configured. If any conflicts occur while applying your changes to the
1056 updated remote, the command will pause to allow you to fix them. Once
1057 that is done, run "update" with the "continue" subcommand. Alternately,
1058 the "skip" subcommand can be used to discard the conflicting changes.
1060 def cmd_update(self
, subcmd
=None):
1063 if subcmd
and subcmd
not in ["continue", "skip"]:
1067 When you have resolved the conflicts run \"yap update continue\".
1068 To skip the problematic patch, run \"yap update skip\"."""
1070 if subcmd
== "continue":
1071 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
1073 if subcmd
== "skip":
1074 os
.system("git reset --hard")
1075 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
1078 self
._check
_rebasing
()
1079 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
1080 raise YapError("You have uncommitted changes. Commit them first")
1082 current
= get_output("git symbolic-ref HEAD")
1084 raise YapError("Not on a branch!")
1086 current
= current
[0].replace('refs/heads/', '')
1087 remote
, merge
= self
._get
_tracking
(current
)
1088 merge
= merge
.replace('refs/heads/', '')
1090 self
.cmd_fetch(remote
)
1091 base
= get_output("git merge-base HEAD refs/remotes/%s/%s" % (remote
, merge
))
1094 fd
, tmpfile
= tempfile
.mkstemp("yap")
1096 os
.system("git format-patch -k --stdout '%s' > %s" % (base
[0], tmpfile
))
1097 self
.cmd_point("refs/remotes/%s/%s" % (remote
, merge
), **{'-f': True})
1099 stat
= os
.stat(tmpfile
)
1102 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
1104 raise YapError("Failed to apply changes")
1108 @short_help("query and configure remote branch tracking")
1110 When invoked with no arguments, the command displays the tracking
1111 information for the current branch. To configure the tracking
1112 information, two arguments for the remote repository and remote branch
1113 are given. The tracking information is used to provide defaults for
1114 where to push local changes and from where to get updates to the branch.
1116 def cmd_track(self
, repo
=None, branch
=None):
1120 current
= get_output("git symbolic-ref HEAD")
1122 raise YapError("Not on a branch!")
1123 current
= current
[0].replace('refs/heads/', '')
1125 if repo
is None and branch
is None:
1126 repo
, merge
= self
._get
_tracking
(current
)
1127 merge
= merge
.replace('refs/heads/', '')
1128 print "Branch '%s' tracking refs/remotes/%s/%s" % (current
, repo
, merge
)
1131 if repo
is None or branch
is None:
1134 if repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1135 raise YapError("No such repository: %s" % repo
)
1137 if run_command("git rev-parse --verify refs/remotes/%s/%s" % (repo
, branch
)):
1138 raise YapError("No such branch '%s' on repository '%s'" % (branch
, repo
))
1140 os
.system("git config branch.%s.remote '%s'" % (current
, repo
))
1141 os
.system("git config branch.%s.merge 'refs/heads/%s'" % (current
, branch
))
1142 print "Branch '%s' now tracking refs/remotes/%s/%s" % (current
, repo
, branch
)
1144 @short_help("mark files with conflicts as resolved")
1146 The arguments are the files to be marked resolved. When a conflict
1147 occurs while merging changes to a file, that file is marked as
1148 "unmerged." Until the file(s) with conflicts are marked resolved,
1149 commits cannot be made.
1151 def cmd_resolved(self
, *files
):
1158 self
._stage
_one
(f
, True)
1161 @short_help("merge a branch into the current branch")
1162 def cmd_merge(self
, branch
):
1166 branch_name
= branch
1167 branch
= get_output("git rev-parse --verify %s" % branch
)
1169 raise YapError("No such branch: %s" % branch
)
1172 base
= get_output("git merge-base HEAD %s" % branch
)
1174 raise YapError("Branch '%s' is not a fork of the current branch"
1177 readtree
= ("git read-tree --aggressive -u -m %s HEAD %s"
1178 % (base
[0], branch
))
1179 if run_command(readtree
):
1180 run_command("git update-index --refresh")
1181 if os
.system(readtree
):
1182 raise YapError("Failed to merge")
1184 repo
= get_output('git rev-parse --git-dir')[0]
1185 dir = os
.path
.join(repo
, 'yap')
1190 msg_file
= os
.path
.join(dir, 'msg')
1191 msg
= file(msg_file
, 'w')
1192 print >>msg
, "Merge branch '%s'" % branch_name
1195 head
= get_output("git rev-parse --verify HEAD")
1197 heads
= [head
[0], branch
]
1198 head_file
= os
.path
.join(dir, 'merge')
1199 pickle
.dump(heads
, file(head_file
, 'w'))
1201 self
._merge
_index
(branch
, base
[0])
1202 if self
._get
_unmerged
_files
():
1204 raise YapError("Fix conflicts then commit")
1208 def _merge_index(self
, branch
, base
):
1209 for f
in self
._get
_unmerged
_files
():
1210 fd
, bfile
= tempfile
.mkstemp("yap")
1212 rc
= os
.system("git show %s:%s > %s" % (base
, f
, bfile
))
1215 fd
, ofile
= tempfile
.mkstemp("yap")
1217 rc
= os
.system("git show %s:%s > %s" % (branch
, f
, ofile
))
1220 command
= "git merge-file -L %(file)s -L %(file)s.base -L %(file)s.%(branch)s %(file)s %(base)s %(other)s " % dict(file=f
, branch
=branch
, base
=bfile
, other
=ofile
)
1221 rc
= os
.system(command
)
1227 self
._stage
_one
(f
, True)
1229 @short_help("show information about loaded plugins")
1230 def cmd_plugins(self
):
1232 if not self
.plugins
:
1233 print >>sys
.stderr
, "No plugins loaded."
1234 for k
, v
in self
.plugins
.items():
1237 doc
= "No description"
1238 print "%-20s%s" % (k
, doc
)
1241 if not func
.startswith('cmd_'):
1244 print "\tOverrides:"
1248 def cmd_help(self
, cmd
=None):
1250 cmd
= "cmd_" + cmd
.replace('-', '_')
1252 attr
= self
.__getattribute
__(cmd
)
1253 except AttributeError:
1254 raise YapError("No such command: %s" % cmd
)
1256 help = attr
.long_help
1257 except AttributeError:
1258 attr
= super(Yap
, self
).__getattribute
__(cmd
)
1260 help = attr
.long_help
1261 except AttributeError:
1262 raise YapError("Sorry, no help for '%s'. Ask Steven." % cmd
)
1264 print >>sys
.stderr
, "The '%s' command" % cmd
1265 print >>sys
.stderr
, "\tyap %s %s" % (cmd
, attr
.__doc
__)
1266 print >>sys
.stderr
, "%s" % help
1269 print >> sys
.stderr
, "Yet Another (Git) Porcelein"
1272 for name
in dir(self
):
1273 if not name
.startswith('cmd_'):
1275 attr
= self
.__getattribute
__(name
)
1276 if not callable(attr
):
1280 short_msg
= attr
.short_help
1281 except AttributeError:
1283 default_meth
= super(Yap
, self
).__getattribute
__(name
)
1284 short_msg
= default_meth
.short_help
1285 except AttributeError:
1288 name
= name
.replace('cmd_', '')
1289 name
= name
.replace('_', '-')
1290 print >> sys
.stderr
, "%-16s%s" % (name
, short_msg
)
1293 print >> sys
.stderr
, "Commands provided by plugins:"
1294 for k
, v
in self
.plugins
.items():
1296 if not name
.startswith('cmd_'):
1299 attr
= self
.__getattribute
__(name
)
1300 short_msg
= attr
.short_help
1301 except AttributeError:
1303 name
= name
.replace('cmd_', '')
1304 name
= name
.replace('_', '-')
1305 print >> sys
.stderr
, "%-8s(%s) %s" % (name
, k
, short_msg
)
1308 print >> sys
.stderr
, "(*) Indicates that the command is not readily reversible"
1310 def cmd_usage(self
):
1311 print >> sys
.stderr
, "usage: %s <command>" % os
.path
.basename(sys
.argv
[0])
1312 print >> sys
.stderr
, " valid commands: help init clone add rm stage unstage status revert commit uncommit log show diff branch switch point cherry-pick repo track push fetch update history resolved plugins version"
1314 def main(self
, args
):
1322 if run_command("git --version"):
1323 print >>sys
.stderr
, "Failed to run git; is it installed?"
1326 debug
= os
.getenv('YAP_DEBUG')
1329 command
= command
.replace('-', '_')
1331 meth
= self
.__getattribute
__("cmd_"+command
)
1333 default_meth
= super(Yap
, self
).__getattribute
__("cmd_"+command
)
1334 except AttributeError:
1337 if meth
.__doc
__ is not None:
1339 elif default_meth
is not None:
1340 doc
= default_meth
.__doc
__
1346 if "options" in meth
.__dict
__:
1347 options
= meth
.options
1348 if default_meth
and "options" in default_meth
.__dict
__:
1349 options
+= default_meth
.options
1351 flags
, args
= getopt
.getopt(args
, options
)
1356 # cast args to a mutable type. this lets the pre-hooks act as
1357 # filters on the arguments
1361 for p
in self
.plugins
.values():
1363 pre_meth
= p
.__getattribute
__("pre_"+command
)
1364 except AttributeError:
1366 pre_meth(args
, flags
)
1368 meth(*args
, **flags
)
1371 for p
in self
.plugins
.values():
1373 meth
= p
.__getattribute
__("post_"+command
)
1374 except AttributeError:
1378 except (TypeError, getopt
.GetoptError
):
1381 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1385 print >> sys
.stderr
, e
1387 except AttributeError: