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")
293 msg_file
= os
.path
.join(repo
, 'yap', 'msg')
294 if os
.access(msg_file
, os
.R_OK
):
296 fd2
= file(tmpfile
, 'w')
297 for l
in fd1
.xreadlines():
298 print >>fd2
, l
.strip()
301 if os
.system("%s '%s'" % (editor
, tmpfile
)) != 0:
302 raise YapError("Editing commit message failed")
309 raise YapError("Refusing to use empty commit message")
311 fd
= os
.popen("git stripspace > %s" % tmpfile
, 'w')
316 parent
= ' -p '.join(parent
)
317 commit
= get_output("git commit-tree '%s' -p %s < '%s'" % (tree
, parent
, tmpfile
))
319 commit
= get_output("git commit-tree '%s' < '%s'" % (tree
, tmpfile
))
322 run_safely("git update-ref HEAD '%s'" % commit
[0])
325 def _check_rebasing(self
):
326 repo
= get_output('git rev-parse --git-dir')[0]
327 dotest
= os
.path
.join(repo
, '.dotest')
328 if os
.access(dotest
, os
.R_OK
):
329 raise YapError("A git operation is in progress. Complete it first")
330 dotest
= os
.path
.join(repo
, '..', '.dotest')
331 if os
.access(dotest
, os
.R_OK
):
332 raise YapError("A git operation is in progress. Complete it first")
334 def _check_git(self
):
335 if run_command("git rev-parse --git-dir"):
336 raise YapError("That command must be run from inside a git repository")
338 def _list_remotes(self
):
339 remotes
= get_output("git config --get-regexp '^remote.*.url'")
341 remote
, url
= x
.split(' ')
342 remote
= remote
.replace('remote.', '')
343 remote
= remote
.replace('.url', '')
346 def _unstage_all(self
):
348 run_safely("git read-tree -m HEAD")
350 run_safely("git read-tree HEAD")
351 run_safely("git update-index -q --refresh")
353 def _get_tracking(self
, current
):
354 remote
= get_output("git config branch.%s.remote" % current
)
356 raise YapError("No tracking branch configured for '%s'" % current
)
358 merge
= get_output("git config branch.%s.merge" % current
)
360 raise YapError("No tracking branch configured for '%s'" % current
)
361 return remote
[0], merge
[0]
363 def _confirm_push(self
, current
, rhs
, repo
):
364 print "About to push local branch '%s' to '%s' on '%s'" % (current
, rhs
, repo
)
365 print "Continue (y/n)? ",
367 ans
= sys
.stdin
.readline().strip()
369 if ans
.lower() != 'y' and ans
.lower() != 'yes':
370 raise YapError("Aborted.")
372 def _clear_state(self
):
373 repo
= get_output('git rev-parse --git-dir')[0]
374 dir = os
.path
.join(repo
, 'yap')
375 for f
in "new-files", "merge", "msg":
377 os
.unlink(os
.path
.join(dir, f
))
381 def _get_attr(self
, name
, attr
):
383 for c
in self
.__class
__.__bases
__:
385 m2
= c
.__dict
__[name
]
389 val
= m2
.__getattribute__(attr
)
390 except AttributeError:
394 @short_help("make a local copy of an existing repository")
396 The first argument is a URL to the existing repository. This can be an
397 absolute path if the repository is local, or a URL with the git://,
398 ssh://, or http:// schemes. By default, the directory used is the last
399 component of the URL, sans '.git'. This can be overridden by providing
402 def cmd_clone(self
, url
, directory
=None):
405 if '://' not in url
and url
[0] != '/':
406 url
= os
.path
.join(os
.getcwd(), url
)
408 url
= url
.rstrip('/')
409 if directory
is None:
410 directory
= url
.rsplit('/')[-1]
411 directory
= directory
.replace('.git', '')
416 raise YapError("Directory exists: %s" % directory
)
419 self
.cmd_repo("origin", url
)
420 self
.cmd_fetch("origin")
423 if not run_command("git rev-parse --verify refs/remotes/origin/HEAD"):
424 hash = get_output("git rev-parse refs/remotes/origin/HEAD")[0]
425 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'"):
426 if get_output("git rev-parse %s" % b
)[0] == hash:
430 if not run_command("git rev-parse --verify refs/remotes/origin/master"):
431 branch
= "refs/remotes/origin/master"
433 branch
= get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'")
436 hash = get_output("git rev-parse %s" % branch
)
438 branch
= branch
.replace('refs/remotes/origin/', '')
439 run_safely("git update-ref refs/heads/%s %s" % (branch
, hash[0]))
440 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
441 self
.cmd_revert(**{'-a': 1})
443 @short_help("turn a directory into a repository")
445 Converts the current working directory into a repository. The primary
446 side-effect of this command is the creation of a '.git' subdirectory.
447 No files are added nor commits made.
450 os
.system("git init")
452 @short_help("add a new file to the repository")
454 The arguments are the files to be added to the repository. Once added,
455 the files will show as "unstaged changes" in the output of 'status'. To
456 reverse the effects of this command, see 'rm'.
458 def cmd_add(self
, *files
):
469 @short_help("delete a file from the repository")
471 The arguments are the files to be removed from the current revision of
472 the repository. The files will still exist in any past commits that the
473 files may have been a part of. The file is not actually deleted, it is
474 just no longer tracked as part of the repository.
476 def cmd_rm(self
, *files
):
486 @short_help("stage changes in a file for commit")
488 The arguments are the files to be staged. Staging changes is a way to
489 build up a commit when you do not want to commit all changes at once.
490 To commit only staged changes, use the '-d' flag to 'commit.' To
491 reverse the effects of this command, see 'unstage'. Once staged, the
492 files will show as "staged changes" in the output of 'status'.
494 def cmd_stage(self
, *files
):
504 @short_help("unstage changes in a file")
506 The arguments are the files to be unstaged. Once unstaged, the files
507 will show as "unstaged changes" in the output of 'status'. The '-a'
508 flag can be used to unstage all staged changes at once.
511 def cmd_unstage(self
, *files
, **flags
):
515 files
= self
._get
_staged
_files
()
524 @short_help("show files with staged and unstaged changes")
526 Show the files in the repository with changes since the last commit,
527 categorized based on whether the changes are staged or not. A file may
528 appear under each heading if the same file has both staged and unstaged
531 def cmd_status(self
):
534 branch
= get_output("git symbolic-ref HEAD")
536 branch
= branch
[0].replace('refs/heads/', '')
539 print "Current branch: %s" % branch
541 print "Files with staged changes:"
542 files
= self
._get
_staged
_files
()
544 print "\t%s" % self
._repo
_path
_to
_rel
(f
)
548 print "Files with unstaged changes:"
549 files
= self
._get
_unstaged
_files
()
551 print "\t%s" % self
._repo
_path
_to
_rel
(f
)
555 files
= self
._get
_unmerged
_files
()
557 print "Files with conflicts:"
559 print "\t%s" % self
._repo
_path
_to
_rel
(f
)
561 @short_help("remove uncommitted changes from a file (*)")
563 The arguments are the files whose changes will be reverted. If the '-a'
564 flag is given, then all files will have uncommitted changes removed.
565 Note that there is no way to reverse this command short of manually
566 editing each file again.
569 def cmd_revert(self
, *files
, **flags
):
574 run_safely("git checkout-index -u -f -a")
586 @short_help("record changes to files as a new commit")
588 Create a new commit recording changes since the last commit. If there
589 are only unstaged changes, those will be recorded. If there are only
590 staged changes, those will be recorded. Otherwise, you will have to
591 specify either the '-a' flag or the '-d' flag to commit all changes or
592 only staged changes, respectively. To reverse the effects of this
593 command, see 'uncommit'.
595 @takes_options("adm:")
596 def cmd_commit(self
, **flags
):
597 "[-a | -d] [-m <msg>]"
599 self
._check
_rebasing
()
600 self
._check
_commit
(**flags
)
601 if not self
._get
_staged
_files
():
602 raise YapError("No changes to commit")
603 msg
= flags
.get('-m', None)
607 @short_help("reverse the actions of the last commit")
609 Reverse the effects of the last 'commit' operation. The changes that
610 were part of the previous commit will show as "staged changes" in the
611 output of 'status'. This means that if no files were changed since the
612 last commit was created, 'uncommit' followed by 'commit' is a lossless
615 def cmd_uncommit(self
):
621 @short_help("report the current version of yap")
622 def cmd_version(self
):
623 print "Yap version %s" % self
.version
625 @short_help("show the changelog for particular versions or files")
627 The arguments are the files with which to filter history. If none are
628 given, all changes are listed. Otherwise only commits that affected one
629 or more of the given files are listed. The -r option changes the
630 starting revision for traversing history. By default, history is listed
633 @takes_options("pr:")
634 def cmd_log(self
, *paths
, **flags
):
635 "[-p] [-r <rev>] <path>..."
637 rev
= flags
.get('-r', 'HEAD')
638 rev
= self
._resolve
_rev
(rev
)
647 paths
= ' '.join(paths
)
648 os
.system("git log -M -C %s %s '%s' -- %s"
649 % (follow
, flags
.get('-p', '--name-status'), rev
, paths
))
651 @short_help("show staged, unstaged, or all uncommitted changes")
653 Show staged, unstaged, or all uncommitted changes. By default, all
654 changes are shown. The '-u' flag causes only unstaged changes to be
655 shown. The '-d' flag causes only staged changes to be shown.
658 def cmd_diff(self
, **flags
):
661 if '-u' in flags
and '-d' in flags
:
662 raise YapError("Conflicting flags: -u and -d")
664 pager
= self
._get
_pager
_cmd
()
667 os
.system("git diff-files -p | %s" % pager
)
669 os
.system("git diff-index --cached -p HEAD | %s" % pager
)
671 os
.system("git diff-index -p HEAD | %s" % pager
)
673 @short_help("list, create, or delete branches")
675 If no arguments are specified, a list of local branches is given. The
676 current branch is indicated by a "*" next to the name. If an argument
677 is given, it is taken as the name of a new branch to create. The branch
678 will start pointing at the current HEAD. See 'point' for details on
679 changing the revision of the new branch. Note that this command does
680 not switch the current working branch. See 'switch' for details on
681 changing the current working branch.
683 The '-d' flag can be used to delete local branches. If the delete
684 operation would remove the last branch reference to a given line of
685 history (colloquially referred to as "dangling commits"), yap will
686 report an error and abort. The '-f' flag can be used to force the delete
689 @takes_options("fd:")
690 def cmd_branch(self
, branch
=None, **flags
):
691 "[ [-f] -d <branch> | <branch> ]"
693 force
= '-f' in flags
695 self
._delete
_branch
(flags
['-d'], force
)
699 if branch
is not None:
700 ref
= get_output("git rev-parse --verify HEAD")
702 raise YapError("No branch point yet. Make a commit")
703 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
705 current
= get_output("git symbolic-ref HEAD")
706 branches
= get_output("git for-each-ref --format='%(refname)' 'refs/heads/*'")
708 if current
and b
== current
[0]:
712 b
= b
.replace('refs/heads/', '')
715 @short_help("change the current working branch")
717 The argument is the name of the branch to make the current working
718 branch. This command will fail if there are uncommitted changes to any
719 files. Otherwise, the contents of the files in the working directory
720 are updated to reflect their state in the new branch. Additionally, any
721 future commits are added to the new branch instead of the previous line
725 def cmd_switch(self
, branch
, **flags
):
728 self
._check
_rebasing
()
729 ref
= self
._resolve
_rev
('refs/heads/'+branch
)
731 if '-f' not in flags
:
732 if (self
._get
_staged
_files
()
733 or (self
._get
_unstaged
_files
()
734 and run_command("git update-index --refresh"))):
735 raise YapError("You have uncommitted changes. Use -f to continue anyway")
737 if self
._get
_unstaged
_files
() and self
._get
_staged
_files
():
738 raise YapError("You have staged and unstaged changes. Perhaps unstage -a?")
740 staged
= bool(self
._get
_staged
_files
())
742 run_command("git diff-files -p | git apply --cached")
743 for f
in self
._get
_new
_files
():
746 idx
= get_output("git write-tree")
747 new
= self
._resolve
_rev
('refs/heads/'+branch
)
748 readtree
= "git read-tree --aggressive -u -m HEAD %s %s" % (idx
[0], new
)
749 if run_command(readtree
):
750 run_command("git update-index --refresh")
751 if os
.system(readtree
):
752 raise YapError("Failed to switch")
753 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
755 if '-f' not in flags
:
762 @short_help("move the current branch to a different revision")
764 The argument is the hash of the commit to which the current branch
765 should point, or alternately a branch or tag (a.k.a, "committish"). If
766 moving the branch would create "dangling commits" (see 'branch'), yap
767 will report an error and abort. The '-f' flag can be used to force the
768 operation in spite of this.
771 def cmd_point(self
, where
, **flags
):
774 self
._check
_rebasing
()
776 head
= get_output("git rev-parse --verify HEAD")
778 raise YapError("No commit yet; nowhere to point")
780 ref
= self
._resolve
_rev
(where
)
781 ref
= get_output("git rev-parse --verify '%s^{commit}'" % ref
)
783 raise YapError("Not a commit: %s" % where
)
785 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
786 raise YapError("You have uncommitted changes. Commit them first")
788 run_safely("git update-ref HEAD '%s'" % ref
[0])
790 if '-f' not in flags
:
791 name
= get_output("git name-rev --name-only '%s'" % head
[0])[0]
792 if name
== "undefined":
793 os
.system("git update-ref HEAD '%s'" % head
[0])
794 raise YapError("Pointing there will lose commits. Use -f to force")
796 run_command("git update-index --refresh")
797 rc
= os
.system("git read-tree -v --reset -u HEAD")
799 raise YapError("checkout-index failed")
802 @short_help("alter history by dropping or amending commits")
804 This command operates in two distinct modes, "amend" and "drop" mode.
805 In drop mode, the given commit is removed from the history of the
806 current branch, as though that commit never happened. By default the
809 In amend mode, the uncommitted changes present are merged into a
810 previous commit. This is useful for correcting typos or adding missed
811 files into past commits. By default the commit used is HEAD.
813 While rewriting history it is possible that conflicts will arise. If
814 this happens, the rewrite will pause and you will be prompted to resolve
815 the conflicts and stage them. Once that is done, you will run "yap
816 history continue." If instead you want the conflicting commit removed
817 from history (perhaps your changes supercede that commit) you can run
818 "yap history skip". Once the rewrite completes, your branch will be on
819 the same commit as when the rewrite started.
821 def cmd_history(self
, subcmd
, *args
):
822 "amend | drop <commit>"
825 if subcmd
not in ("amend", "drop", "continue", "skip"):
829 When you have resolved the conflicts run \"yap history continue\".
830 To skip the problematic patch, run \"yap history skip\"."""
832 if subcmd
== "continue":
833 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
836 os
.system("git reset --hard")
837 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
840 if subcmd
== "amend":
841 flags
, args
= getopt
.getopt(args
, "ad")
851 self
._resolve
_rev
(commit
)
852 self
._check
_rebasing
()
854 if subcmd
== "amend":
855 self
._check
_commit
(**flags
)
856 if self
._get
_unstaged
_files
():
857 # XXX: handle unstaged changes better
858 raise YapError("Commit away changes that you aren't amending")
862 start
= get_output("git rev-parse HEAD")
863 stash
= get_output("git stash create")
864 run_command("git reset --hard")
866 fd
, tmpfile
= tempfile
.mkstemp("yap")
870 os
.system("git format-patch -k --stdout '%s' > %s" % (commit
, tmpfile
))
871 if subcmd
== "amend":
872 self
.cmd_point(commit
, **{'-f': True})
874 if subcmd
== "amend":
876 rc
= os
.system("git stash apply %s" % stash
[0])
878 self
.cmd_point(start
[0], **{'-f': True})
879 os
.system("git stash apply %s" % stash
[0])
880 raise YapError("Failed to apply stash")
883 if subcmd
== "amend":
885 self
._check
_commit
(**{'-a': True})
888 self
.cmd_point("%s^" % commit
, **{'-f': True})
890 stat
= os
.stat(tmpfile
)
893 run_safely("git update-index --refresh")
894 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
896 raise YapError("Failed to apply changes")
901 run_command("git stash apply %s" % stash
[0])
904 @short_help("show the changes introduced by a given commit")
906 By default, the changes in the last commit are shown. To override this,
907 specify a hash, branch, or tag (committish). The hash of the commit,
908 the commit's author, log message, and a diff of the changes are shown.
910 def cmd_show(self
, commit
="HEAD"):
913 commit
= self
._resolve
_rev
(commit
)
914 os
.system("git show '%s'" % commit
)
916 @short_help("apply the changes in a given commit to the current branch")
918 The argument is the hash, branch, or tag (committish) of the commit to
919 be applied. In general, it only makes sense to apply commits that
920 happened on another branch. The '-r' flag can be used to have the
921 changes in the given commit reversed from the current branch. In
922 general, this only makes sense for commits that happened on the current
926 def cmd_cherry_pick(self
, commit
, **flags
):
929 commit
= self
._resolve
_rev
(commit
)
931 os
.system("git revert '%s'" % commit
)
933 os
.system("git cherry-pick '%s'" % commit
)
935 @short_help("list, add, or delete configured remote repositories")
937 When invoked with no arguments, this command will show the list of
938 currently configured remote repositories, giving both the name and URL
939 of each. To add a new repository, give the desired name as the first
940 argument and the URL as the second. The '-d' flag can be used to remove
941 a previously added repository.
944 def cmd_repo(self
, name
=None, url
=None, **flags
):
945 "[<name> <url> | -d <name>]"
947 if name
is not None and url
is None:
951 if flags
['-d'] not in [ x
[0] for x
in self
._list
_remotes
() ]:
952 raise YapError("No such repository: %s" % flags
['-d'])
953 os
.system("git config --unset remote.%s.url" % flags
['-d'])
954 os
.system("git config --unset remote.%s.fetch" % flags
['-d'])
955 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'"):
956 hash = get_output("git rev-parse %s" % b
)
958 run_safely("git update-ref -d %s %s" % (b
, hash[0]))
961 if name
in [ x
[0] for x
in self
._list
_remotes
() ]:
962 raise YapError("Repository '%s' already exists" % name
)
963 os
.system("git config remote.%s.url %s" % (name
, url
))
964 os
.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name
, name
))
966 for remote
, url
in self
._list
_remotes
():
968 print " URL: %s" % url
970 for b
in get_output("git for-each-ref --format='%%(refname)' 'refs/remotes/%s/*'" % remote
):
971 b
= b
.replace('refs/remotes/', '')
973 branches
= "Branches: "
976 print " %s%s" % (branches
, b
)
979 @short_help("send local commits to a remote repository (*)")
981 When invoked with no arguments, the current branch is synchronized to
982 the tracking branch of the tracking remote. If no tracking remote is
983 specified, the repository will have to be specified on the command line.
984 In that case, the default is to push to a branch with the same name as
985 the current branch. This behavior can be overridden by giving a second
986 argument to specify the remote branch.
988 If the remote branch does not currently exist, the command will abort
989 unless the -c flag is provided. If the remote branch is not a direct
990 descendent of the local branch, the command will abort unless the -f
991 flag is provided. Forcing a push in this way can be problematic to
992 other users of the repository if they are not expecting it.
994 To delete a branch on the remote repository, use the -d flag.
996 @takes_options("cdf")
997 def cmd_push(self
, repo
=None, rhs
=None, **flags
):
1000 if '-c' in flags
and '-d' in flags
:
1003 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1004 raise YapError("No such repository: %s" % repo
)
1006 current
= get_output("git symbolic-ref HEAD")
1008 raise YapError("Not on a branch!")
1010 self
._check
_rebasing
()
1012 current
= current
[0].replace('refs/heads/', '')
1013 remote
= get_output("git config branch.%s.remote" % current
)
1014 if repo
is None and remote
:
1018 raise YapError("No tracking branch configured; specify destination repository")
1020 if rhs
is None and remote
and remote
[0] == repo
:
1021 merge
= get_output("git config branch.%s.merge" % current
)
1026 rhs
= "refs/heads/%s" % current
1028 if '-c' not in flags
and '-d' not in flags
:
1029 if run_command("git rev-parse --verify refs/remotes/%s/%s"
1030 % (repo
, rhs
.replace('refs/heads/', ''))):
1031 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
1032 if '-f' not in flags
:
1033 hash = get_output("git rev-parse refs/remotes/%s/%s" % (repo
, rhs
.replace('refs/heads/', '')))
1034 base
= get_output("git merge-base HEAD %s" % hash[0])
1036 if base
[0] != hash[0]:
1037 raise YapError("Branch not up-to-date with remote. Update or use -f")
1039 self
._confirm
_push
(current
, rhs
, repo
)
1046 lhs
= "refs/heads/%s" % current
1047 rc
= os
.system("git push %s %s %s:%s" % (flags
.get('-f', ''), repo
, lhs
, rhs
))
1049 raise YapError("Push failed.")
1051 @short_help("retrieve commits from a remote repository")
1053 When run with no arguments, the command will retrieve new commits from
1054 the remote tracking repository. Note that this does not in any way
1055 alter the current branch. For that, see "update". If a remote other
1056 than the tracking remote is desired, it can be specified as the first
1059 def cmd_fetch(self
, repo
=None):
1062 current
= get_output("git symbolic-ref HEAD")
1064 raise YapError("Not on a branch!")
1066 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1067 raise YapError("No such repository: %s" % repo
)
1069 current
= current
[0].replace('refs/heads/', '')
1070 remote
= get_output("git config branch.%s.remote" % current
)
1074 raise YapError("No tracking branch configured; specify a repository")
1075 os
.system("git fetch %s" % repo
)
1077 @short_help("update the current branch relative to its tracking branch")
1079 Updates the current branch relative to its remote tracking branch. This
1080 command requires that the current branch have a remote tracking branch
1081 configured. If any conflicts occur while applying your changes to the
1082 updated remote, the command will pause to allow you to fix them. Once
1083 that is done, run "update" with the "continue" subcommand. Alternately,
1084 the "skip" subcommand can be used to discard the conflicting changes.
1086 def cmd_update(self
, subcmd
=None):
1089 if subcmd
and subcmd
not in ["continue", "skip"]:
1093 When you have resolved the conflicts run \"yap update continue\".
1094 To skip the problematic patch, run \"yap update skip\"."""
1096 if subcmd
== "continue":
1097 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
1099 if subcmd
== "skip":
1100 os
.system("git reset --hard")
1101 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
1104 self
._check
_rebasing
()
1105 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
1106 raise YapError("You have uncommitted changes. Commit them first")
1108 current
= get_output("git symbolic-ref HEAD")
1110 raise YapError("Not on a branch!")
1112 current
= current
[0].replace('refs/heads/', '')
1113 remote
, merge
= self
._get
_tracking
(current
)
1114 merge
= merge
.replace('refs/heads/', '')
1116 self
.cmd_fetch(remote
)
1117 base
= get_output("git merge-base HEAD refs/remotes/%s/%s" % (remote
, merge
))
1120 fd
, tmpfile
= tempfile
.mkstemp("yap")
1122 os
.system("git format-patch -k --stdout '%s' > %s" % (base
[0], tmpfile
))
1123 self
.cmd_point("refs/remotes/%s/%s" % (remote
, merge
), **{'-f': True})
1125 stat
= os
.stat(tmpfile
)
1128 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
1130 raise YapError("Failed to apply changes")
1134 @short_help("query and configure remote branch tracking")
1136 When invoked with no arguments, the command displays the tracking
1137 information for the current branch. To configure the tracking
1138 information, two arguments for the remote repository and remote branch
1139 are given. The tracking information is used to provide defaults for
1140 where to push local changes and from where to get updates to the branch.
1142 def cmd_track(self
, repo
=None, branch
=None):
1146 current
= get_output("git symbolic-ref HEAD")
1148 raise YapError("Not on a branch!")
1149 current
= current
[0].replace('refs/heads/', '')
1151 if repo
is None and branch
is None:
1152 repo
, merge
= self
._get
_tracking
(current
)
1153 merge
= merge
.replace('refs/heads/', '')
1154 print "Branch '%s' tracking refs/remotes/%s/%s" % (current
, repo
, merge
)
1157 if repo
is None or branch
is None:
1160 if repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1161 raise YapError("No such repository: %s" % repo
)
1163 if run_command("git rev-parse --verify refs/remotes/%s/%s" % (repo
, branch
)):
1164 raise YapError("No such branch '%s' on repository '%s'" % (branch
, repo
))
1166 os
.system("git config branch.%s.remote '%s'" % (current
, repo
))
1167 os
.system("git config branch.%s.merge 'refs/heads/%s'" % (current
, branch
))
1168 print "Branch '%s' now tracking refs/remotes/%s/%s" % (current
, repo
, branch
)
1170 @short_help("mark files with conflicts as resolved")
1172 The arguments are the files to be marked resolved. When a conflict
1173 occurs while merging changes to a file, that file is marked as
1174 "unmerged." Until the file(s) with conflicts are marked resolved,
1175 commits cannot be made.
1177 def cmd_resolved(self
, *files
):
1184 self
._stage
_one
(f
, True)
1187 @short_help("merge a branch into the current branch")
1188 def cmd_merge(self
, branch
):
1192 branch_name
= branch
1193 branch
= self
._resolve
_rev
(branch
)
1194 base
= get_output("git merge-base HEAD %s" % branch
)
1196 raise YapError("Branch '%s' is not a fork of the current branch"
1199 readtree
= ("git read-tree --aggressive -u -m %s HEAD %s"
1200 % (base
[0], branch
))
1201 if run_command(readtree
):
1202 run_command("git update-index --refresh")
1203 if os
.system(readtree
):
1204 raise YapError("Failed to merge")
1206 repo
= get_output('git rev-parse --git-dir')[0]
1207 dir = os
.path
.join(repo
, 'yap')
1212 msg_file
= os
.path
.join(dir, 'msg')
1213 msg
= file(msg_file
, 'w')
1214 print >>msg
, "Merge branch '%s'" % branch_name
1217 head
= get_output("git rev-parse --verify HEAD")
1219 heads
= [head
[0], branch
]
1220 head_file
= os
.path
.join(dir, 'merge')
1221 pickle
.dump(heads
, file(head_file
, 'w'))
1223 self
._merge
_index
(branch
, base
[0])
1224 if self
._get
_unmerged
_files
():
1226 raise YapError("Fix conflicts then commit")
1230 def _merge_index(self
, branch
, base
):
1231 for f
in self
._get
_unmerged
_files
():
1232 fd
, bfile
= tempfile
.mkstemp("yap")
1234 rc
= os
.system("git show %s:%s > %s" % (base
, f
, bfile
))
1237 fd
, ofile
= tempfile
.mkstemp("yap")
1239 rc
= os
.system("git show %s:%s > %s" % (branch
, f
, ofile
))
1242 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
)
1243 rc
= os
.system(command
)
1249 self
._stage
_one
(f
, True)
1251 def cmd_help(self
, cmd
=None):
1254 cmd
= "cmd_" + cmd
.replace('-', '_')
1256 attr
= self
.__getattribute
__(cmd
)
1257 except AttributeError:
1258 raise YapError("No such command: %s" % cmd
)
1261 help = self
._get
_attr
(cmd
, "long_help")
1262 except AttributeError:
1264 raise YapError("Sorry, no help for '%s'. Ask Steven." % cmd
)
1266 print >>sys
.stderr
, "The '%s' command" % oldcmd
1267 print >>sys
.stderr
, "\tyap %s %s" % (oldcmd
, attr
.__doc
__)
1268 print >>sys
.stderr
, "%s" % help
1271 print >> sys
.stderr
, "Yet Another (Git) Porcelein"
1274 for name
in dir(self
):
1275 if not name
.startswith('cmd_'):
1277 attr
= self
.__getattribute
__(name
)
1278 if not callable(attr
):
1282 short_msg
= self
._get
_attr
(name
, "short_help")
1283 except AttributeError:
1286 name
= name
.replace('cmd_', '')
1287 name
= name
.replace('_', '-')
1288 print >> sys
.stderr
, "%-16s%s" % (name
, short_msg
)
1291 print >> sys
.stderr
, "(*) Indicates that the command is not readily reversible"
1293 @short_help("show information about loaded plugins")
1294 def cmd_plugins(self
):
1296 print >> sys
.stderr
, "Loaded plugins:"
1297 plugins
= load_plugins()
1298 for name
, cls
in plugins
.items():
1299 print "\t%-16s: %s" % (name
, cls
.__doc
__)
1301 print "\t%-16s" % "None"
1303 def cmd_usage(self
):
1304 print >> sys
.stderr
, "usage: %s <command>" % os
.path
.basename(sys
.argv
[0])
1305 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"
1308 plugindir
= os
.path
.join("~", ".yap", "plugins")
1309 plugindir
= os
.path
.expanduser(plugindir
)
1310 plugindir
= os
.path
.join(plugindir
, "*.py")
1313 for p
in glob
.glob(os
.path
.expanduser(plugindir
)):
1314 plugin
= os
.path
.basename(p
).replace('.py', '')
1315 m
= __import__(plugin
)
1318 if not type(cls
) == type:
1320 if not issubclass(cls
, YapCore
):
1327 def yap_metaclass(name
, bases
, dct
):
1328 plugindir
= os
.path
.join("~", ".yap", "plugins")
1329 plugindir
= os
.path
.expanduser(plugindir
)
1330 sys
.path
.insert(0, plugindir
)
1332 plugins
= set(load_plugins().values())
1335 p2
-= set(cls
.__bases
__)
1337 bases
= list(plugins
) + list(bases
)
1338 return type(name
, tuple(bases
), dct
)
1341 __metaclass__
= yap_metaclass
1343 def main(self
, args
):
1351 if run_command("git --version"):
1352 print >>sys
.stderr
, "Failed to run git; is it installed?"
1355 debug
= os
.getenv('YAP_DEBUG')
1358 command
= command
.replace('-', '_')
1359 meth
= self
.__getattribute
__("cmd_"+command
)
1360 doc
= self
._get
_attr
("cmd_"+command
, "__doc__")
1364 for c
in self
.__class
__.__bases
__:
1366 t
= c
.__dict
__["cmd_"+command
]
1369 if "options" in t
.__dict
__:
1370 options
+= t
.options
1374 flags
, args
= getopt
.getopt(args
, options
)
1376 except getopt
.GetoptError
, e
:
1379 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1385 meth(*args
, **flags
)
1386 except (TypeError, getopt
.GetoptError
):
1389 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1393 print >> sys
.stderr
, e
1395 except AttributeError: