10 class ShellError(Exception):
11 def __init__(self
, cmd
, rc
):
16 return "%s returned %d" % (self
.cmd
, self
.rc
)
18 class YapError(Exception):
19 def __init__(self
, msg
):
25 class YapCore(object):
26 def _add_new_file(self
, file):
27 repo
= get_output('git rev-parse --git-dir')[0]
28 dir = os
.path
.join(repo
, 'yap')
33 files
= self
._get
_new
_files
()
35 path
= os
.path
.join(dir, 'new-files')
36 pickle
.dump(files
, open(path
, 'w'))
38 def _get_new_files(self
):
39 repo
= get_output('git rev-parse --git-dir')[0]
40 path
= os
.path
.join(repo
, 'yap', 'new-files')
42 files
= pickle
.load(file(path
))
49 if get_output("git ls-files --cached '%s'" % f
) != []:
54 def _remove_new_file(self
, file):
55 files
= self
._get
_new
_files
()
56 files
= filter(lambda x
: x
!= file, files
)
58 repo
= get_output('git rev-parse --git-dir')[0]
59 path
= os
.path
.join(repo
, 'yap', 'new-files')
61 pickle
.dump(files
, open(path
, 'w'))
65 def _assert_file_exists(self
, file):
66 if not os
.access(file, os
.R_OK
):
67 raise YapError("No such file: %s" % file)
69 def _repo_path_to_rel(self
, path
):
70 prefix
= get_output("git rev-parse --show-prefix")
73 prefix
= os
.path
.split(prefix
[0])
75 # strip empty components from prefix
83 path
= os
.path
.split(path
)
85 for a
, b
in zip(prefix
, path
):
91 cdup
= [".."] * (len(prefix
) - common
)
92 path
= cdup
+ list(path
)
93 path
= os
.path
.join(*path
)
96 def _get_staged_files(self
):
97 if run_command("git rev-parse HEAD"):
98 files
= get_output("git ls-files --cached")
100 files
= get_output("git diff-index --cached --name-only HEAD")
101 unmerged
= self
._get
_unmerged
_files
()
103 unmerged
= set(unmerged
)
104 files
= set(files
).difference(unmerged
)
108 def _get_unstaged_files(self
):
110 cdup
= get_output("git rev-parse --show-cdup")
114 files
= get_output("git ls-files -m")
117 new_files
= self
._get
_new
_files
()
119 staged
= self
._get
_staged
_files
()
122 new_files
= set(new_files
).difference(staged
)
123 new_files
= list(new_files
)
125 unmerged
= self
._get
_unmerged
_files
()
127 unmerged
= set(unmerged
)
128 files
= set(files
).difference(unmerged
)
132 def _get_unmerged_files(self
):
134 cdup
= get_output("git rev-parse --show-cdup")
138 files
= get_output("git ls-files -u")
140 files
= [ x
.replace('\t', ' ').split(' ')[3] for x
in files
]
141 return list(set(files
))
143 def _resolve_rev(self
, rev
):
144 ref
= get_output("git rev-parse --verify %s 2>/dev/null" % rev
)
146 raise YapError("No such revision: %s" % rev
)
149 def _delete_branch(self
, branch
, force
):
150 current
= get_output("git symbolic-ref HEAD")
152 current
= current
[0].replace('refs/heads/', '')
153 if branch
== current
:
154 raise YapError("Can't delete current branch")
156 ref
= self
._resolve
_rev
('refs/heads/'+branch
)
157 run_safely("git update-ref -d 'refs/heads/%s' '%s'" % (branch
, ref
))
160 name
= get_output("git name-rev --name-only '%s'" % ref
)[0]
161 if name
== 'undefined':
162 run_command("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
))
163 raise YapError("Refusing to delete leaf branch (use -f to force)")
164 def _get_pager_cmd(self
):
165 if 'YAP_PAGER' in os
.environ
:
166 return os
.environ
['YAP_PAGER']
167 elif 'GIT_PAGER' in os
.environ
:
168 return os
.environ
['GIT_PAGER']
169 elif 'PAGER' in os
.environ
:
170 return os
.environ
['PAGER']
174 def _add_one(self
, file):
175 self
._assert
_file
_exists
(file)
176 x
= get_output("git ls-files '%s'" % file)
178 raise YapError("File '%s' already in repository" % file)
179 self
._add
_new
_file
(file)
181 def _rm_one(self
, file):
182 self
._assert
_file
_exists
(file)
183 if get_output("git ls-files '%s'" % file) != []:
184 run_safely("git rm --cached '%s'" % file)
185 self
._remove
_new
_file
(file)
187 def _stage_one(self
, file, allow_unmerged
=False):
188 self
._assert
_file
_exists
(file)
189 prefix
= get_output("git rev-parse --show-prefix")
191 tmp
= os
.path
.normpath(os
.path
.join(prefix
[0], file))
194 if not allow_unmerged
and tmp
in self
._get
_unmerged
_files
():
195 raise YapError("Refusing to stage conflicted file: %s" % file)
196 run_safely("git update-index --add '%s'" % file)
198 def _unstage_one(self
, file):
199 self
._assert
_file
_exists
(file)
200 if run_command("git rev-parse HEAD"):
201 rc
= run_command("git update-index --force-remove '%s'" % file)
203 cdup
= get_output("git rev-parse --show-cdup")
210 rc
= run_command("git diff-index --cached -p HEAD '%s' | (cd %s; git apply -R --cached)" % (file, cdup
))
212 raise YapError("Failed to unstage")
214 def _revert_one(self
, file):
215 self
._assert
_file
_exists
(file)
217 self
._unstage
_one
(file)
220 run_safely("git checkout-index -u -f '%s'" % file)
222 def _parse_commit(self
, commit
):
223 lines
= get_output("git cat-file commit '%s'" % commit
)
228 if mode
!= 'commit' and l
.strip() == "":
233 commit
['log'].append(l
)
240 commit
['log'] = '\n'.join(commit
['log'])
243 def _check_commit(self
, **flags
):
244 if '-a' in flags
and '-d' in flags
:
245 raise YapError("Conflicting flags: -a and -d")
247 if '-d' not in flags
and self
._get
_unstaged
_files
():
248 if '-a' not in flags
and self
._get
_staged
_files
():
249 raise YapError("Staged and unstaged changes present. Specify what to commit")
250 os
.system("git diff-files -p | git apply --cached")
251 for f
in self
._get
_new
_files
():
254 def _do_uncommit(self
):
255 commit
= self
._parse
_commit
("HEAD")
256 repo
= get_output('git rev-parse --git-dir')[0]
257 dir = os
.path
.join(repo
, 'yap')
262 msg_file
= os
.path
.join(dir, 'msg')
263 fd
= file(msg_file
, 'w')
264 print >>fd
, commit
['log']
267 tree
= get_output("git rev-parse --verify HEAD^")
268 run_safely("git update-ref -m uncommit HEAD '%s'" % tree
[0])
270 def _do_commit(self
, msg
=None):
271 tree
= get_output("git write-tree")[0]
273 repo
= get_output('git rev-parse --git-dir')[0]
274 head_file
= os
.path
.join(repo
, 'yap', 'merge')
276 parent
= pickle
.load(file(head_file
))
278 parent
= get_output("git rev-parse --verify HEAD 2> /dev/null")
280 if os
.environ
.has_key('YAP_EDITOR'):
281 editor
= os
.environ
['YAP_EDITOR']
282 elif os
.environ
.has_key('GIT_EDITOR'):
283 editor
= os
.environ
['GIT_EDITOR']
284 elif os
.environ
.has_key('EDITOR'):
285 editor
= os
.environ
['EDITOR']
289 fd
, tmpfile
= tempfile
.mkstemp("yap")
294 msg_file
= os
.path
.join(repo
, 'yap', 'msg')
295 if os
.access(msg_file
, os
.R_OK
):
297 fd2
= file(tmpfile
, 'w')
298 for l
in fd1
.xreadlines():
299 print >>fd2
, l
.strip()
302 if os
.system("%s '%s'" % (editor
, tmpfile
)) != 0:
303 raise YapError("Editing commit message failed")
310 raise YapError("Refusing to use empty commit message")
312 (fd_w
, fd_r
) = os
.popen2("git stripspace > %s" % tmpfile
)
318 parent
= ' -p '.join(parent
)
319 commit
= get_output("git commit-tree '%s' -p %s < '%s'" % (tree
, parent
, tmpfile
))
321 commit
= get_output("git commit-tree '%s' < '%s'" % (tree
, tmpfile
))
324 run_safely("git update-ref HEAD '%s'" % commit
[0])
327 def _check_rebasing(self
):
328 repo
= get_output('git rev-parse --git-dir')[0]
329 dotest
= os
.path
.join(repo
, '.dotest')
330 if os
.access(dotest
, os
.R_OK
):
331 raise YapError("A git operation is in progress. Complete it first")
332 dotest
= os
.path
.join(repo
, '..', '.dotest')
333 if os
.access(dotest
, os
.R_OK
):
334 raise YapError("A git operation is in progress. Complete it first")
336 def _check_git(self
):
337 if run_command("git rev-parse --git-dir"):
338 raise YapError("That command must be run from inside a git repository")
340 def _list_remotes(self
):
341 remotes
= get_output("git config --get-regexp '^remote.*.url'")
343 remote
, url
= x
.split(' ')
344 remote
= remote
.replace('remote.', '')
345 remote
= remote
.replace('.url', '')
348 def _unstage_all(self
):
350 run_safely("git read-tree -m HEAD")
352 run_safely("git read-tree HEAD")
353 run_safely("git update-index -q --refresh")
355 def _get_tracking(self
, current
):
356 remote
= get_output("git config branch.%s.remote" % current
)
358 raise YapError("No tracking branch configured for '%s'" % current
)
360 merge
= get_output("git config branch.%s.merge" % current
)
362 raise YapError("No tracking branch configured for '%s'" % current
)
363 return remote
[0], merge
[0]
365 def _confirm_push(self
, current
, rhs
, repo
):
366 print "About to push local branch '%s' to '%s' on '%s'" % (current
, rhs
, repo
)
367 print "Continue (y/n)? ",
369 ans
= sys
.stdin
.readline().strip()
371 if ans
.lower() != 'y' and ans
.lower() != 'yes':
372 raise YapError("Aborted.")
374 def _clear_state(self
):
375 repo
= get_output('git rev-parse --git-dir')[0]
376 dir = os
.path
.join(repo
, 'yap')
377 for f
in "new-files", "merge", "msg":
379 os
.unlink(os
.path
.join(dir, f
))
383 def _get_attr(self
, name
, attr
):
385 for c
in self
.__class
__.__bases
__:
387 m2
= c
.__dict
__[name
]
391 val
= m2
.__getattribute__(attr
)
392 except AttributeError:
396 @short_help("make a local copy of an existing repository")
398 The first argument is a URL to the existing repository. This can be an
399 absolute path if the repository is local, or a URL with the git://,
400 ssh://, or http:// schemes. By default, the directory used is the last
401 component of the URL, sans '.git'. This can be overridden by providing
404 def cmd_clone(self
, url
, directory
=None):
407 if '://' not in url
and url
[0] != '/':
408 url
= os
.path
.join(os
.getcwd(), url
)
410 url
= url
.rstrip('/')
411 if directory
is None:
412 directory
= url
.rsplit('/')[-1]
413 directory
= directory
.replace('.git', '')
418 raise YapError("Directory exists: %s" % directory
)
421 self
.cmd_repo("origin", url
)
422 self
.cmd_fetch("origin")
425 if not run_command("git rev-parse --verify refs/remotes/origin/HEAD"):
426 hash = get_output("git rev-parse refs/remotes/origin/HEAD")[0]
427 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'"):
428 if get_output("git rev-parse %s" % b
)[0] == hash:
432 if not run_command("git rev-parse --verify refs/remotes/origin/master"):
433 branch
= "refs/remotes/origin/master"
435 branch
= get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'")
438 hash = get_output("git rev-parse %s" % branch
)
440 branch
= branch
.replace('refs/remotes/origin/', '')
441 run_safely("git update-ref refs/heads/%s %s" % (branch
, hash[0]))
442 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
443 self
.cmd_revert(**{'-a': 1})
445 @short_help("turn a directory into a repository")
447 Converts the current working directory into a repository. The primary
448 side-effect of this command is the creation of a '.git' subdirectory.
449 No files are added nor commits made.
452 os
.system("git init")
454 @short_help("add a new file to the repository")
456 The arguments are the files to be added to the repository. Once added,
457 the files will show as "unstaged changes" in the output of 'status'. To
458 reverse the effects of this command, see 'rm'.
460 def cmd_add(self
, *files
):
471 @short_help("delete a file from the repository")
473 The arguments are the files to be removed from the current revision of
474 the repository. The files will still exist in any past commits that the
475 files may have been a part of. The file is not actually deleted, it is
476 just no longer tracked as part of the repository.
478 def cmd_rm(self
, *files
):
488 @short_help("stage changes in a file for commit")
490 The arguments are the files to be staged. Staging changes is a way to
491 build up a commit when you do not want to commit all changes at once.
492 To commit only staged changes, use the '-d' flag to 'commit.' To
493 reverse the effects of this command, see 'unstage'. Once staged, the
494 files will show as "staged changes" in the output of 'status'.
496 def cmd_stage(self
, *files
):
506 @short_help("unstage changes in a file")
508 The arguments are the files to be unstaged. Once unstaged, the files
509 will show as "unstaged changes" in the output of 'status'. The '-a'
510 flag can be used to unstage all staged changes at once.
513 def cmd_unstage(self
, *files
, **flags
):
517 files
= self
._get
_staged
_files
()
526 @short_help("show files with staged and unstaged changes")
528 Show the files in the repository with changes since the last commit,
529 categorized based on whether the changes are staged or not. A file may
530 appear under each heading if the same file has both staged and unstaged
533 def cmd_status(self
):
536 branch
= get_output("git symbolic-ref HEAD")
538 branch
= branch
[0].replace('refs/heads/', '')
541 print "Current branch: %s" % branch
543 print "Files with staged changes:"
544 files
= self
._get
_staged
_files
()
546 print "\t%s" % self
._repo
_path
_to
_rel
(f
)
550 print "Files with unstaged changes:"
551 files
= self
._get
_unstaged
_files
()
553 print "\t%s" % self
._repo
_path
_to
_rel
(f
)
557 files
= self
._get
_unmerged
_files
()
559 print "Files with conflicts:"
561 print "\t%s" % self
._repo
_path
_to
_rel
(f
)
563 @short_help("remove uncommitted changes from a file (*)")
565 The arguments are the files whose changes will be reverted. If the '-a'
566 flag is given, then all files will have uncommitted changes removed.
567 Note that there is no way to reverse this command short of manually
568 editing each file again.
571 def cmd_revert(self
, *files
, **flags
):
576 run_safely("git checkout-index -u -f -a")
588 @short_help("record changes to files as a new commit")
590 Create a new commit recording changes since the last commit. If there
591 are only unstaged changes, those will be recorded. If there are only
592 staged changes, those will be recorded. Otherwise, you will have to
593 specify either the '-a' flag or the '-d' flag to commit all changes or
594 only staged changes, respectively. To reverse the effects of this
595 command, see 'uncommit'.
597 @takes_options("adm:")
598 def cmd_commit(self
, **flags
):
599 "[-a | -d] [-m <msg>]"
601 self
._check
_rebasing
()
602 self
._check
_commit
(**flags
)
603 if not self
._get
_staged
_files
():
604 raise YapError("No changes to commit")
605 msg
= flags
.get('-m', None)
609 @short_help("reverse the actions of the last commit")
611 Reverse the effects of the last 'commit' operation. The changes that
612 were part of the previous commit will show as "staged changes" in the
613 output of 'status'. This means that if no files were changed since the
614 last commit was created, 'uncommit' followed by 'commit' is a lossless
617 def cmd_uncommit(self
):
623 @short_help("report the current version of yap")
624 def cmd_version(self
):
625 print "Yap version %s" % self
.version
627 @short_help("show the changelog for particular versions or files")
629 The arguments are the files with which to filter history. If none are
630 given, all changes are listed. Otherwise only commits that affected one
631 or more of the given files are listed. The -r option changes the
632 starting revision for traversing history. By default, history is listed
635 @takes_options("pr:")
636 def cmd_log(self
, *paths
, **flags
):
637 "[-p] [-r <rev>] <path>..."
639 rev
= flags
.get('-r', 'HEAD')
640 rev
= self
._resolve
_rev
(rev
)
649 paths
= ' '.join(paths
)
650 os
.system("git log -M -C %s %s '%s' -- %s"
651 % (follow
, flags
.get('-p', '--name-status'), rev
, paths
))
653 @short_help("show staged, unstaged, or all uncommitted changes")
655 Show staged, unstaged, or all uncommitted changes. By default, all
656 changes are shown. The '-u' flag causes only unstaged changes to be
657 shown. The '-d' flag causes only staged changes to be shown.
660 def cmd_diff(self
, **flags
):
663 if '-u' in flags
and '-d' in flags
:
664 raise YapError("Conflicting flags: -u and -d")
666 pager
= self
._get
_pager
_cmd
()
669 os
.system("git diff-files -p | %s" % pager
)
671 os
.system("git diff-index --cached -p HEAD | %s" % pager
)
673 os
.system("git diff-index -p HEAD | %s" % pager
)
675 @short_help("list, create, or delete branches")
677 If no arguments are specified, a list of local branches is given. The
678 current branch is indicated by a "*" next to the name. If an argument
679 is given, it is taken as the name of a new branch to create. The branch
680 will start pointing at the current HEAD. See 'point' for details on
681 changing the revision of the new branch. Note that this command does
682 not switch the current working branch. See 'switch' for details on
683 changing the current working branch.
685 The '-d' flag can be used to delete local branches. If the delete
686 operation would remove the last branch reference to a given line of
687 history (colloquially referred to as "dangling commits"), yap will
688 report an error and abort. The '-f' flag can be used to force the delete
691 @takes_options("fd:")
692 def cmd_branch(self
, branch
=None, **flags
):
693 "[ [-f] -d <branch> | <branch> ]"
695 force
= '-f' in flags
697 self
._delete
_branch
(flags
['-d'], force
)
701 if branch
is not None:
702 ref
= get_output("git rev-parse --verify HEAD")
704 raise YapError("No branch point yet. Make a commit")
705 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
707 current
= get_output("git symbolic-ref HEAD")
708 branches
= get_output("git for-each-ref --format='%(refname)' 'refs/heads/*'")
710 if current
and b
== current
[0]:
714 b
= b
.replace('refs/heads/', '')
717 @short_help("change the current working branch")
719 The argument is the name of the branch to make the current working
720 branch. This command will fail if there are uncommitted changes to any
721 files. Otherwise, the contents of the files in the working directory
722 are updated to reflect their state in the new branch. Additionally, any
723 future commits are added to the new branch instead of the previous line
727 def cmd_switch(self
, branch
, **flags
):
730 self
._check
_rebasing
()
731 ref
= self
._resolve
_rev
('refs/heads/'+branch
)
733 if '-f' not in flags
:
734 if (self
._get
_staged
_files
()
735 or (self
._get
_unstaged
_files
()
736 and run_command("git update-index --refresh"))):
737 raise YapError("You have uncommitted changes. Use -f to continue anyway")
739 if self
._get
_unstaged
_files
() and self
._get
_staged
_files
():
740 raise YapError("You have staged and unstaged changes. Perhaps unstage -a?")
742 staged
= bool(self
._get
_staged
_files
())
744 run_command("git diff-files -p | git apply --cached")
745 for f
in self
._get
_new
_files
():
748 idx
= get_output("git write-tree")
749 new
= self
._resolve
_rev
('refs/heads/'+branch
)
750 readtree
= "git read-tree --aggressive -u -m HEAD %s %s" % (idx
[0], new
)
751 if run_command(readtree
):
752 run_command("git update-index --refresh")
753 if os
.system(readtree
):
754 raise YapError("Failed to switch")
755 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
757 if '-f' not in flags
:
764 @short_help("move the current branch to a different revision")
766 The argument is the hash of the commit to which the current branch
767 should point, or alternately a branch or tag (a.k.a, "committish"). If
768 moving the branch would create "dangling commits" (see 'branch'), yap
769 will report an error and abort. The '-f' flag can be used to force the
770 operation in spite of this.
773 def cmd_point(self
, where
, **flags
):
776 self
._check
_rebasing
()
778 head
= get_output("git rev-parse --verify HEAD")
780 raise YapError("No commit yet; nowhere to point")
782 ref
= self
._resolve
_rev
(where
)
783 ref
= get_output("git rev-parse --verify '%s^{commit}'" % ref
)
785 raise YapError("Not a commit: %s" % where
)
787 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
788 raise YapError("You have uncommitted changes. Commit them first")
790 run_safely("git update-ref HEAD '%s'" % ref
[0])
792 if '-f' not in flags
:
793 name
= get_output("git name-rev --name-only '%s'" % head
[0])[0]
794 if name
== "undefined":
795 os
.system("git update-ref HEAD '%s'" % head
[0])
796 raise YapError("Pointing there will lose commits. Use -f to force")
799 run_safely("git read-tree -u -m HEAD")
801 run_safely("git read-tree HEAD")
802 run_safely("git checkout-index -u -f -a")
805 @short_help("alter history by dropping or amending commits")
807 This command operates in two distinct modes, "amend" and "drop" mode.
808 In drop mode, the given commit is removed from the history of the
809 current branch, as though that commit never happened. By default the
812 In amend mode, the uncommitted changes present are merged into a
813 previous commit. This is useful for correcting typos or adding missed
814 files into past commits. By default the commit used is HEAD.
816 While rewriting history it is possible that conflicts will arise. If
817 this happens, the rewrite will pause and you will be prompted to resolve
818 the conflicts and stage them. Once that is done, you will run "yap
819 history continue." If instead you want the conflicting commit removed
820 from history (perhaps your changes supercede that commit) you can run
821 "yap history skip". Once the rewrite completes, your branch will be on
822 the same commit as when the rewrite started.
824 def cmd_history(self
, subcmd
, *args
):
825 "amend | drop <commit>"
828 if subcmd
not in ("amend", "drop", "continue", "skip"):
832 When you have resolved the conflicts run \"yap history continue\".
833 To skip the problematic patch, run \"yap history skip\"."""
835 if subcmd
== "continue":
836 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
839 os
.system("git reset --hard")
840 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
843 if subcmd
== "amend":
844 flags
, args
= getopt
.getopt(args
, "ad")
854 self
._resolve
_rev
(commit
)
855 self
._check
_rebasing
()
857 if subcmd
== "amend":
858 self
._check
_commit
(**flags
)
859 if self
._get
_unstaged
_files
():
860 # XXX: handle unstaged changes better
861 raise YapError("Commit away changes that you aren't amending")
865 start
= get_output("git rev-parse HEAD")
866 stash
= get_output("git stash create")
867 run_command("git reset --hard")
869 fd
, tmpfile
= tempfile
.mkstemp("yap")
873 os
.system("git format-patch -k --stdout '%s' > %s" % (commit
, tmpfile
))
874 if subcmd
== "amend":
875 self
.cmd_point(commit
, **{'-f': True})
877 if subcmd
== "amend":
879 rc
= os
.system("git stash apply %s" % stash
[0])
881 self
.cmd_point(start
[0], **{'-f': True})
882 os
.system("git stash apply %s" % stash
[0])
883 raise YapError("Failed to apply stash")
886 if subcmd
== "amend":
888 self
._check
_commit
(**{'-a': True})
891 self
.cmd_point("%s^" % commit
, **{'-f': True})
893 stat
= os
.stat(tmpfile
)
896 run_safely("git update-index --refresh")
897 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
899 raise YapError("Failed to apply changes")
904 run_command("git stash apply %s" % stash
[0])
907 @short_help("show the changes introduced by a given commit")
909 By default, the changes in the last commit are shown. To override this,
910 specify a hash, branch, or tag (committish). The hash of the commit,
911 the commit's author, log message, and a diff of the changes are shown.
913 def cmd_show(self
, commit
="HEAD"):
916 commit
= self
._resolve
_rev
(commit
)
917 os
.system("git show '%s'" % commit
)
919 @short_help("apply the changes in a given commit to the current branch")
921 The argument is the hash, branch, or tag (committish) of the commit to
922 be applied. In general, it only makes sense to apply commits that
923 happened on another branch. The '-r' flag can be used to have the
924 changes in the given commit reversed from the current branch. In
925 general, this only makes sense for commits that happened on the current
929 def cmd_cherry_pick(self
, commit
, **flags
):
933 os
.system("git revert '%s'" % commit
)
935 os
.system("git cherry-pick '%s'" % commit
)
937 @short_help("list, add, or delete configured remote repositories")
939 When invoked with no arguments, this command will show the list of
940 currently configured remote repositories, giving both the name and URL
941 of each. To add a new repository, give the desired name as the first
942 argument and the URL as the second. The '-d' flag can be used to remove
943 a previously added repository.
946 def cmd_repo(self
, name
=None, url
=None, **flags
):
947 "[<name> <url> | -d <name>]"
949 if name
is not None and url
is None:
953 if flags
['-d'] not in [ x
[0] for x
in self
._list
_remotes
() ]:
954 raise YapError("No such repository: %s" % flags
['-d'])
955 os
.system("git config --unset remote.%s.url" % flags
['-d'])
956 os
.system("git config --unset remote.%s.fetch" % flags
['-d'])
957 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'"):
958 hash = get_output("git rev-parse %s" % b
)
960 run_safely("git update-ref -d %s %s" % (b
, hash[0]))
963 if name
in [ x
[0] for x
in self
._list
_remotes
() ]:
964 raise YapError("Repository '%s' already exists" % name
)
965 os
.system("git config remote.%s.url %s" % (name
, url
))
966 os
.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name
, name
))
968 for remote
, url
in self
._list
_remotes
():
969 print "%-20s %s" % (remote
, url
)
971 @short_help("send local commits to a remote repository (*)")
973 When invoked with no arguments, the current branch is synchronized to
974 the tracking branch of the tracking remote. If no tracking remote is
975 specified, the repository will have to be specified on the command line.
976 In that case, the default is to push to a branch with the same name as
977 the current branch. This behavior can be overridden by giving a second
978 argument to specify the remote branch.
980 If the remote branch does not currently exist, the command will abort
981 unless the -c flag is provided. If the remote branch is not a direct
982 descendent of the local branch, the command will abort unless the -f
983 flag is provided. Forcing a push in this way can be problematic to
984 other users of the repository if they are not expecting it.
986 To delete a branch on the remote repository, use the -d flag.
988 @takes_options("cdf")
989 def cmd_push(self
, repo
=None, rhs
=None, **flags
):
992 if '-c' in flags
and '-d' in flags
:
995 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
996 raise YapError("No such repository: %s" % repo
)
998 current
= get_output("git symbolic-ref HEAD")
1000 raise YapError("Not on a branch!")
1002 self
._check
_rebasing
()
1004 current
= current
[0].replace('refs/heads/', '')
1005 remote
= get_output("git config branch.%s.remote" % current
)
1006 if repo
is None and remote
:
1010 raise YapError("No tracking branch configured; specify destination repository")
1012 if rhs
is None and remote
and remote
[0] == repo
:
1013 merge
= get_output("git config branch.%s.merge" % current
)
1018 rhs
= "refs/heads/%s" % current
1020 if '-c' not in flags
and '-d' not in flags
:
1021 if run_command("git rev-parse --verify refs/remotes/%s/%s"
1022 % (repo
, rhs
.replace('refs/heads/', ''))):
1023 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
1024 if '-f' not in flags
:
1025 hash = get_output("git rev-parse refs/remotes/%s/%s" % (repo
, rhs
.replace('refs/heads/', '')))
1026 base
= get_output("git merge-base HEAD %s" % hash[0])
1028 if base
[0] != hash[0]:
1029 raise YapError("Branch not up-to-date with remote. Update or use -f")
1031 self
._confirm
_push
(current
, rhs
, repo
)
1038 lhs
= "refs/heads/%s" % current
1039 rc
= os
.system("git push %s %s %s:%s" % (flags
.get('-f', ''), repo
, lhs
, rhs
))
1041 raise YapError("Push failed.")
1043 @short_help("retrieve commits from a remote repository")
1045 When run with no arguments, the command will retrieve new commits from
1046 the remote tracking repository. Note that this does not in any way
1047 alter the current branch. For that, see "update". If a remote other
1048 than the tracking remote is desired, it can be specified as the first
1051 def cmd_fetch(self
, repo
=None):
1054 current
= get_output("git symbolic-ref HEAD")
1056 raise YapError("Not on a branch!")
1058 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1059 raise YapError("No such repository: %s" % repo
)
1061 current
= current
[0].replace('refs/heads/', '')
1062 remote
= get_output("git config branch.%s.remote" % current
)
1066 raise YapError("No tracking branch configured; specify a repository")
1067 os
.system("git fetch %s" % repo
)
1069 @short_help("update the current branch relative to its tracking branch")
1071 Updates the current branch relative to its remote tracking branch. This
1072 command requires that the current branch have a remote tracking branch
1073 configured. If any conflicts occur while applying your changes to the
1074 updated remote, the command will pause to allow you to fix them. Once
1075 that is done, run "update" with the "continue" subcommand. Alternately,
1076 the "skip" subcommand can be used to discard the conflicting changes.
1078 def cmd_update(self
, subcmd
=None):
1081 if subcmd
and subcmd
not in ["continue", "skip"]:
1085 When you have resolved the conflicts run \"yap update continue\".
1086 To skip the problematic patch, run \"yap update skip\"."""
1088 if subcmd
== "continue":
1089 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
1091 if subcmd
== "skip":
1092 os
.system("git reset --hard")
1093 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
1096 self
._check
_rebasing
()
1097 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
1098 raise YapError("You have uncommitted changes. Commit them first")
1100 current
= get_output("git symbolic-ref HEAD")
1102 raise YapError("Not on a branch!")
1104 current
= current
[0].replace('refs/heads/', '')
1105 remote
, merge
= self
._get
_tracking
(current
)
1106 merge
= merge
.replace('refs/heads/', '')
1108 self
.cmd_fetch(remote
)
1109 base
= get_output("git merge-base HEAD refs/remotes/%s/%s" % (remote
, merge
))
1112 fd
, tmpfile
= tempfile
.mkstemp("yap")
1114 os
.system("git format-patch -k --stdout '%s' > %s" % (base
[0], tmpfile
))
1115 self
.cmd_point("refs/remotes/%s/%s" % (remote
, merge
), **{'-f': True})
1117 stat
= os
.stat(tmpfile
)
1120 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
1122 raise YapError("Failed to apply changes")
1126 @short_help("query and configure remote branch tracking")
1128 When invoked with no arguments, the command displays the tracking
1129 information for the current branch. To configure the tracking
1130 information, two arguments for the remote repository and remote branch
1131 are given. The tracking information is used to provide defaults for
1132 where to push local changes and from where to get updates to the branch.
1134 def cmd_track(self
, repo
=None, branch
=None):
1138 current
= get_output("git symbolic-ref HEAD")
1140 raise YapError("Not on a branch!")
1141 current
= current
[0].replace('refs/heads/', '')
1143 if repo
is None and branch
is None:
1144 repo
, merge
= self
._get
_tracking
(current
)
1145 merge
= merge
.replace('refs/heads/', '')
1146 print "Branch '%s' tracking refs/remotes/%s/%s" % (current
, repo
, merge
)
1149 if repo
is None or branch
is None:
1152 if repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1153 raise YapError("No such repository: %s" % repo
)
1155 if run_command("git rev-parse --verify refs/remotes/%s/%s" % (repo
, branch
)):
1156 raise YapError("No such branch '%s' on repository '%s'" % (branch
, repo
))
1158 os
.system("git config branch.%s.remote '%s'" % (current
, repo
))
1159 os
.system("git config branch.%s.merge 'refs/heads/%s'" % (current
, branch
))
1160 print "Branch '%s' now tracking refs/remotes/%s/%s" % (current
, repo
, branch
)
1162 @short_help("mark files with conflicts as resolved")
1164 The arguments are the files to be marked resolved. When a conflict
1165 occurs while merging changes to a file, that file is marked as
1166 "unmerged." Until the file(s) with conflicts are marked resolved,
1167 commits cannot be made.
1169 def cmd_resolved(self
, *files
):
1176 self
._stage
_one
(f
, True)
1179 @short_help("merge a branch into the current branch")
1180 def cmd_merge(self
, branch
):
1184 branch_name
= branch
1185 branch
= self
._resolve
_rev
(branch
)
1186 base
= get_output("git merge-base HEAD %s" % branch
)
1188 raise YapError("Branch '%s' is not a fork of the current branch"
1191 readtree
= ("git read-tree --aggressive -u -m %s HEAD %s"
1192 % (base
[0], branch
))
1193 if run_command(readtree
):
1194 run_command("git update-index --refresh")
1195 if os
.system(readtree
):
1196 raise YapError("Failed to merge")
1198 repo
= get_output('git rev-parse --git-dir')[0]
1199 dir = os
.path
.join(repo
, 'yap')
1204 msg_file
= os
.path
.join(dir, 'msg')
1205 msg
= file(msg_file
, 'w')
1206 print >>msg
, "Merge branch '%s'" % branch_name
1209 head
= get_output("git rev-parse --verify HEAD")
1211 heads
= [head
[0], branch
]
1212 head_file
= os
.path
.join(dir, 'merge')
1213 pickle
.dump(heads
, file(head_file
, 'w'))
1215 self
._merge
_index
(branch
, base
[0])
1216 if self
._get
_unmerged
_files
():
1218 raise YapError("Fix conflicts then commit")
1222 def _merge_index(self
, branch
, base
):
1223 for f
in self
._get
_unmerged
_files
():
1224 fd
, bfile
= tempfile
.mkstemp("yap")
1226 rc
= os
.system("git show %s:%s > %s" % (base
, f
, bfile
))
1229 fd
, ofile
= tempfile
.mkstemp("yap")
1231 rc
= os
.system("git show %s:%s > %s" % (branch
, f
, ofile
))
1234 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
)
1235 rc
= os
.system(command
)
1241 self
._stage
_one
(f
, True)
1243 def cmd_help(self
, cmd
=None):
1246 cmd
= "cmd_" + cmd
.replace('-', '_')
1248 attr
= self
.__getattribute
__(cmd
)
1249 except AttributeError:
1250 raise YapError("No such command: %s" % cmd
)
1253 help = self
._get
_attr
(cmd
, "long_help")
1254 except AttributeError:
1256 raise YapError("Sorry, no help for '%s'. Ask Steven." % cmd
)
1258 print >>sys
.stderr
, "The '%s' command" % oldcmd
1259 print >>sys
.stderr
, "\tyap %s %s" % (oldcmd
, attr
.__doc
__)
1260 print >>sys
.stderr
, "%s" % help
1263 print >> sys
.stderr
, "Yet Another (Git) Porcelein"
1266 for name
in dir(self
):
1267 if not name
.startswith('cmd_'):
1269 attr
= self
.__getattribute
__(name
)
1270 if not callable(attr
):
1274 short_msg
= self
._get
_attr
(name
, "short_help")
1275 except AttributeError:
1278 name
= name
.replace('cmd_', '')
1279 name
= name
.replace('_', '-')
1280 print >> sys
.stderr
, "%-16s%s" % (name
, short_msg
)
1283 print >> sys
.stderr
, "(*) Indicates that the command is not readily reversible"
1285 @short_help("show information about loaded plugins")
1286 def cmd_plugins(self
):
1288 print >> sys
.stderr
, "Loaded plugins:"
1289 plugins
= load_plugins()
1290 for name
, cls
in plugins
.items():
1291 print "\t%-16s: %s" % (name
, cls
.__doc
__)
1293 print "\t%-16s" % "None"
1295 def cmd_usage(self
):
1296 print >> sys
.stderr
, "usage: %s <command>" % os
.path
.basename(sys
.argv
[0])
1297 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 version"
1300 plugindir
= os
.path
.join("~", ".yap", "plugins")
1301 plugindir
= os
.path
.expanduser(plugindir
)
1302 plugindir
= os
.path
.join(plugindir
, "*.py")
1305 for p
in glob
.glob(os
.path
.expanduser(plugindir
)):
1306 plugin
= os
.path
.basename(p
).replace('.py', '')
1307 m
= __import__(plugin
)
1310 if not type(cls
) == type:
1312 if not issubclass(cls
, YapCore
):
1319 def yap_metaclass(name
, bases
, dct
):
1320 plugindir
= os
.path
.join("~", ".yap", "plugins")
1321 plugindir
= os
.path
.expanduser(plugindir
)
1322 sys
.path
.insert(0, plugindir
)
1324 plugins
= set(load_plugins().values())
1327 p2
-= set(cls
.__bases
__)
1329 bases
= list(plugins
) + list(bases
)
1330 return type(name
, tuple(bases
), dct
)
1333 __metaclass__
= yap_metaclass
1335 def main(self
, args
):
1343 if run_command("git --version"):
1344 print >>sys
.stderr
, "Failed to run git; is it installed?"
1347 debug
= os
.getenv('YAP_DEBUG')
1350 command
= command
.replace('-', '_')
1351 meth
= self
.__getattribute
__("cmd_"+command
)
1352 doc
= self
._get
_attr
("cmd_"+command
, "__doc__")
1356 for c
in self
.__class
__.__bases
__:
1358 t
= c
.__dict
__["cmd_"+command
]
1361 if "options" in t
.__dict
__:
1362 options
+= t
.options
1366 flags
, args
= getopt
.getopt(args
, options
)
1368 except getopt
.GetoptError
, e
:
1371 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1377 meth(*args
, **flags
)
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: