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 _clear_new_files(self
):
66 repo
= get_output('git rev-parse --git-dir')[0]
67 path
= os
.path
.join(repo
, 'yap', 'new-files')
70 def _assert_file_exists(self
, file):
71 if not os
.access(file, os
.R_OK
):
72 raise YapError("No such file: %s" % file)
74 def _get_staged_files(self
):
75 if run_command("git rev-parse HEAD"):
76 files
= get_output("git ls-files --cached")
78 files
= get_output("git diff-index --cached --name-only HEAD")
79 unmerged
= self
._get
_unmerged
_files
()
81 unmerged
= set(unmerged
)
82 files
= set(files
).difference(unmerged
)
86 def _get_unstaged_files(self
):
87 files
= get_output("git ls-files -m")
88 prefix
= get_output("git rev-parse --show-prefix")
90 files
= [ os
.path
.join(prefix
[0], x
) for x
in files
]
91 files
+= self
._get
_new
_files
()
92 unmerged
= self
._get
_unmerged
_files
()
94 unmerged
= set(unmerged
)
95 files
= set(files
).difference(unmerged
)
99 def _get_unmerged_files(self
):
100 files
= get_output("git ls-files -u")
101 files
= [ x
.replace('\t', ' ').split(' ')[3] for x
in files
]
102 prefix
= get_output("git rev-parse --show-prefix")
104 files
= [ os
.path
.join(prefix
[0], x
) for x
in files
]
105 return list(set(files
))
107 def _delete_branch(self
, branch
, force
):
108 current
= get_output("git symbolic-ref HEAD")
110 current
= current
[0].replace('refs/heads/', '')
111 if branch
== current
:
112 raise YapError("Can't delete current branch")
114 ref
= get_output("git rev-parse --verify 'refs/heads/%s'" % branch
)
116 raise YapError("No such branch: %s" % branch
)
117 run_safely("git update-ref -d 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
120 name
= get_output("git name-rev --name-only '%s'" % ref
[0])[0]
121 if name
== 'undefined':
122 run_command("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
123 raise YapError("Refusing to delete leaf branch (use -f to force)")
124 def _get_pager_cmd(self
):
125 if 'YAP_PAGER' in os
.environ
:
126 return os
.environ
['YAP_PAGER']
127 elif 'GIT_PAGER' in os
.environ
:
128 return os
.environ
['GIT_PAGER']
129 elif 'PAGER' in os
.environ
:
130 return os
.environ
['PAGER']
134 def _add_one(self
, file):
135 self
._assert
_file
_exists
(file)
136 x
= get_output("git ls-files '%s'" % file)
138 raise YapError("File '%s' already in repository" % file)
139 self
._add
_new
_file
(file)
141 def _rm_one(self
, file):
142 self
._assert
_file
_exists
(file)
143 if get_output("git ls-files '%s'" % file) != []:
144 run_safely("git rm --cached '%s'" % file)
145 self
._remove
_new
_file
(file)
147 def _stage_one(self
, file, allow_unmerged
=False):
148 self
._assert
_file
_exists
(file)
149 prefix
= get_output("git rev-parse --show-prefix")
151 tmp
= os
.path
.normpath(os
.path
.join(prefix
[0], file))
154 if not allow_unmerged
and tmp
in self
._get
_unmerged
_files
():
155 raise YapError("Refusing to stage conflicted file: %s" % file)
156 run_safely("git update-index --add '%s'" % file)
158 def _unstage_one(self
, file):
159 self
._assert
_file
_exists
(file)
160 if run_command("git rev-parse HEAD"):
161 rc
= run_command("git update-index --force-remove '%s'" % file)
163 rc
= run_command("git diff-index -p HEAD '%s' | git apply -R --cached" % file)
165 raise YapError("Failed to unstage")
167 def _revert_one(self
, file):
168 self
._assert
_file
_exists
(file)
170 self
._unstage
_one
(file)
173 run_safely("git checkout-index -u -f '%s'" % file)
175 def _parse_commit(self
, commit
):
176 lines
= get_output("git cat-file commit '%s'" % commit
)
181 if mode
!= 'commit' and l
.strip() == "":
186 commit
['log'].append(l
)
193 commit
['log'] = '\n'.join(commit
['log'])
196 def _check_commit(self
, **flags
):
197 if '-a' in flags
and '-d' in flags
:
198 raise YapError("Conflicting flags: -a and -d")
200 if '-d' not in flags
and self
._get
_unstaged
_files
():
201 if '-a' not in flags
and self
._get
_staged
_files
():
202 raise YapError("Staged and unstaged changes present. Specify what to commit")
203 os
.system("git diff-files -p | git apply --cached")
204 for f
in self
._get
_new
_files
():
207 def _do_uncommit(self
):
208 commit
= self
._parse
_commit
("HEAD")
209 repo
= get_output('git rev-parse --git-dir')[0]
210 dir = os
.path
.join(repo
, 'yap')
215 msg_file
= os
.path
.join(dir, 'msg')
216 fd
= file(msg_file
, 'w')
217 print >>fd
, commit
['log']
220 tree
= get_output("git rev-parse --verify HEAD^")
221 run_safely("git update-ref -m uncommit HEAD '%s'" % tree
[0])
223 def _do_commit(self
, msg
=None):
224 tree
= get_output("git write-tree")[0]
225 parent
= get_output("git rev-parse --verify HEAD 2> /dev/null")
227 if os
.environ
.has_key('YAP_EDITOR'):
228 editor
= os
.environ
['YAP_EDITOR']
229 elif os
.environ
.has_key('GIT_EDITOR'):
230 editor
= os
.environ
['GIT_EDITOR']
231 elif os
.environ
.has_key('EDITOR'):
232 editor
= os
.environ
['EDITOR']
236 fd
, tmpfile
= tempfile
.mkstemp("yap")
241 repo
= get_output('git rev-parse --git-dir')[0]
242 msg_file
= os
.path
.join(repo
, 'yap', 'msg')
243 if os
.access(msg_file
, os
.R_OK
):
245 fd2
= file(tmpfile
, 'w')
246 for l
in fd1
.xreadlines():
247 print >>fd2
, l
.strip()
250 if os
.system("%s '%s'" % (editor
, tmpfile
)) != 0:
251 raise YapError("Editing commit message failed")
258 raise YapError("Refusing to use empty commit message")
260 (fd_w
, fd_r
) = os
.popen2("git stripspace > %s" % tmpfile
)
266 commit
= get_output("git commit-tree '%s' -p '%s' < '%s'" % (tree
, parent
[0], tmpfile
))
268 commit
= get_output("git commit-tree '%s' < '%s'" % (tree
, tmpfile
))
271 run_safely("git update-ref HEAD '%s'" % commit
[0])
273 def _check_rebasing(self
):
274 repo
= get_output('git rev-parse --git-dir')[0]
275 dotest
= os
.path
.join(repo
, '.dotest')
276 if os
.access(dotest
, os
.R_OK
):
277 raise YapError("A git operation is in progress. Complete it first")
278 dotest
= os
.path
.join(repo
, '..', '.dotest')
279 if os
.access(dotest
, os
.R_OK
):
280 raise YapError("A git operation is in progress. Complete it first")
282 def _check_git(self
):
283 if run_command("git rev-parse --git-dir"):
284 raise YapError("That command must be run from inside a git repository")
286 def _list_remotes(self
):
287 remotes
= get_output("git config --get-regexp '^remote.*.url'")
289 remote
, url
= x
.split(' ')
290 remote
= remote
.replace('remote.', '')
291 remote
= remote
.replace('.url', '')
294 def _unstage_all(self
):
296 run_safely("git read-tree -m HEAD")
298 run_safely("git read-tree HEAD")
299 run_safely("git update-index -q --refresh")
301 def _get_tracking(self
, current
):
302 remote
= get_output("git config branch.%s.remote" % current
)
304 raise YapError("No tracking branch configured for '%s'" % current
)
306 merge
= get_output("git config branch.%s.merge" % current
)
308 raise YapError("No tracking branch configured for '%s'" % current
)
309 return remote
[0], merge
[0]
311 def _confirm_push(self
, current
, rhs
, repo
):
312 print "About to push local branch '%s' to '%s' on '%s'" % (current
, rhs
, repo
)
313 print "Continue (y/n)? ",
315 ans
= sys
.stdin
.readline().strip()
317 if ans
.lower() != 'y' and ans
.lower() != 'yes':
318 raise YapError("Aborted.")
320 def _get_attr(self
, name
, attr
):
322 for c
in self
.__class
__.__bases
__:
324 m2
= c
.__dict
__[name
]
328 val
= m2
.__getattribute__(attr
)
329 except AttributeError:
333 @short_help("make a local copy of an existing repository")
335 The first argument is a URL to the existing repository. This can be an
336 absolute path if the repository is local, or a URL with the git://,
337 ssh://, or http:// schemes. By default, the directory used is the last
338 component of the URL, sans '.git'. This can be overridden by providing
341 def cmd_clone(self
, url
, directory
=None):
344 if '://' not in url
and url
[0] != '/':
345 url
= os
.path
.join(os
.getcwd(), url
)
347 url
= url
.rstrip('/')
348 if directory
is None:
349 directory
= url
.rsplit('/')[-1]
350 directory
= directory
.replace('.git', '')
355 raise YapError("Directory exists: %s" % directory
)
358 self
.cmd_repo("origin", url
)
359 self
.cmd_fetch("origin")
362 if not run_command("git rev-parse --verify refs/remotes/origin/HEAD"):
363 hash = get_output("git rev-parse refs/remotes/origin/HEAD")[0]
364 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'"):
365 if get_output("git rev-parse %s" % b
)[0] == hash:
369 if not run_command("git rev-parse --verify refs/remotes/origin/master"):
370 branch
= "refs/remotes/origin/master"
372 branch
= get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'")
375 hash = get_output("git rev-parse %s" % branch
)
377 branch
= branch
.replace('refs/remotes/origin/', '')
378 run_safely("git update-ref refs/heads/%s %s" % (branch
, hash[0]))
379 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
380 self
.cmd_revert(**{'-a': 1})
382 @short_help("turn a directory into a repository")
384 Converts the current working directory into a repository. The primary
385 side-effect of this command is the creation of a '.git' subdirectory.
386 No files are added nor commits made.
389 os
.system("git init")
391 @short_help("add a new file to the repository")
393 The arguments are the files to be added to the repository. Once added,
394 the files will show as "unstaged changes" in the output of 'status'. To
395 reverse the effects of this command, see 'rm'.
397 def cmd_add(self
, *files
):
408 @short_help("delete a file from the repository")
410 The arguments are the files to be removed from the current revision of
411 the repository. The files will still exist in any past commits that the
412 files may have been a part of. The file is not actually deleted, it is
413 just no longer tracked as part of the repository.
415 def cmd_rm(self
, *files
):
425 @short_help("stage changes in a file for commit")
427 The arguments are the files to be staged. Staging changes is a way to
428 build up a commit when you do not want to commit all changes at once.
429 To commit only staged changes, use the '-d' flag to 'commit.' To
430 reverse the effects of this command, see 'unstage'. Once staged, the
431 files will show as "staged changes" in the output of 'status'.
433 def cmd_stage(self
, *files
):
443 @short_help("unstage changes in a file")
445 The arguments are the files to be unstaged. Once unstaged, the files
446 will show as "unstaged changes" in the output of 'status'. The '-a'
447 flag can be used to unstage all staged changes at once.
450 def cmd_unstage(self
, *files
, **flags
):
465 @short_help("show files with staged and unstaged changes")
467 Show the files in the repository with changes since the last commit,
468 categorized based on whether the changes are staged or not. A file may
469 appear under each heading if the same file has both staged and unstaged
472 def cmd_status(self
):
475 branch
= get_output("git symbolic-ref HEAD")
477 branch
= branch
[0].replace('refs/heads/', '')
480 print "Current branch: %s" % branch
482 print "Files with staged changes:"
483 files
= self
._get
_staged
_files
()
489 print "Files with unstaged changes:"
490 files
= self
._get
_unstaged
_files
()
496 files
= self
._get
_unmerged
_files
()
498 print "Files with conflicts:"
502 @short_help("remove uncommitted changes from a file (*)")
504 The arguments are the files whose changes will be reverted. If the '-a'
505 flag is given, then all files will have uncommitted changes removed.
506 Note that there is no way to reverse this command short of manually
507 editing each file again.
510 def cmd_revert(self
, *files
, **flags
):
515 run_safely("git checkout-index -u -f -a")
526 @short_help("record changes to files as a new commit")
528 Create a new commit recording changes since the last commit. If there
529 are only unstaged changes, those will be recorded. If there are only
530 staged changes, those will be recorded. Otherwise, you will have to
531 specify either the '-a' flag or the '-d' flag to commit all changes or
532 only staged changes, respectively. To reverse the effects of this
533 command, see 'uncommit'.
535 @takes_options("adm:")
536 def cmd_commit(self
, **flags
):
537 "[-a | -d] [-m <msg>]"
539 self
._check
_rebasing
()
540 self
._check
_commit
(**flags
)
541 if not self
._get
_staged
_files
():
542 raise YapError("No changes to commit")
543 msg
= flags
.get('-m', None)
547 @short_help("reverse the actions of the last commit")
549 Reverse the effects of the last 'commit' operation. The changes that
550 were part of the previous commit will show as "staged changes" in the
551 output of 'status'. This means that if no files were changed since the
552 last commit was created, 'uncommit' followed by 'commit' is a lossless
555 def cmd_uncommit(self
):
561 @short_help("report the current version of yap")
562 def cmd_version(self
):
563 print "Yap version 0.1"
565 @short_help("show the changelog for particular versions or files")
567 The arguments are the files with which to filter history. If none are
568 given, all changes are listed. Otherwise only commits that affected one
569 or more of the given files are listed. The -r option changes the
570 starting revision for traversing history. By default, history is listed
573 @takes_options("pr:")
574 def cmd_log(self
, *paths
, **flags
):
575 "[-p] [-r <rev>] <path>..."
577 rev
= flags
.get('-r', 'HEAD')
586 paths
= ' '.join(paths
)
587 os
.system("git log -M -C %s %s '%s' -- %s"
588 % (follow
, flags
.get('-p', '--name-status'), rev
, paths
))
590 @short_help("show staged, unstaged, or all uncommitted changes")
592 Show staged, unstaged, or all uncommitted changes. By default, all
593 changes are shown. The '-u' flag causes only unstaged changes to be
594 shown. The '-d' flag causes only staged changes to be shown.
597 def cmd_diff(self
, **flags
):
600 if '-u' in flags
and '-d' in flags
:
601 raise YapError("Conflicting flags: -u and -d")
603 pager
= self
._get
_pager
_cmd
()
606 os
.system("git diff-files -p | %s" % pager
)
608 os
.system("git diff-index --cached -p HEAD | %s" % pager
)
610 os
.system("git diff-index -p HEAD | %s" % pager
)
612 @short_help("list, create, or delete branches")
614 If no arguments are specified, a list of local branches is given. The
615 current branch is indicated by a "*" next to the name. If an argument
616 is given, it is taken as the name of a new branch to create. The branch
617 will start pointing at the current HEAD. See 'point' for details on
618 changing the revision of the new branch. Note that this command does
619 not switch the current working branch. See 'switch' for details on
620 changing the current working branch.
622 The '-d' flag can be used to delete local branches. If the delete
623 operation would remove the last branch reference to a given line of
624 history (colloquially referred to as "dangling commits"), yap will
625 report an error and abort. The '-f' flag can be used to force the delete
628 @takes_options("fd:")
629 def cmd_branch(self
, branch
=None, **flags
):
630 "[ [-f] -d <branch> | <branch> ]"
632 force
= '-f' in flags
634 self
._delete
_branch
(flags
['-d'], force
)
638 if branch
is not None:
639 ref
= get_output("git rev-parse --verify HEAD")
641 raise YapError("No branch point yet. Make a commit")
642 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
644 current
= get_output("git symbolic-ref HEAD")
645 branches
= get_output("git for-each-ref --format='%(refname)' 'refs/heads/*'")
647 if current
and b
== current
[0]:
651 b
= b
.replace('refs/heads/', '')
654 @short_help("change the current working branch")
656 The argument is the name of the branch to make the current working
657 branch. This command will fail if there are uncommitted changes to any
658 files. Otherwise, the contents of the files in the working directory
659 are updated to reflect their state in the new branch. Additionally, any
660 future commits are added to the new branch instead of the previous line
664 def cmd_switch(self
, branch
, **flags
):
667 self
._check
_rebasing
()
668 ref
= get_output("git rev-parse --verify 'refs/heads/%s'" % branch
)
670 raise YapError("No such branch: %s" % branch
)
672 if '-f' not in flags
:
673 if (self
._get
_staged
_files
()
674 or (self
._get
_unstaged
_files
()
675 and run_command("git update-index --refresh"))):
676 raise YapError("You have uncommitted changes. Use -f to continue anyway")
678 if self
._get
_unstaged
_files
() and self
._get
_staged
_files
():
679 raise YapError("You have staged and unstaged changes. Perhaps unstage -a?")
681 staged
= bool(self
._get
_staged
_files
())
683 run_command("git diff-files -p | git apply --cached")
684 for f
in self
._get
_new
_files
():
687 idx
= get_output("git write-tree")
688 new
= get_output("git rev-parse refs/heads/%s" % branch
)
689 readtree
= "git read-tree --aggressive -u -m HEAD %s %s" % (idx
[0], new
[0])
690 if run_command(readtree
):
691 run_command("git update-index --refresh")
692 if os
.system(readtree
):
693 raise YapError("Failed to switch")
694 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
700 @short_help("move the current branch to a different revision")
702 The argument is the hash of the commit to which the current branch
703 should point, or alternately a branch or tag (a.k.a, "committish"). If
704 moving the branch would create "dangling commits" (see 'branch'), yap
705 will report an error and abort. The '-f' flag can be used to force the
706 operation in spite of this.
709 def cmd_point(self
, where
, **flags
):
712 self
._check
_rebasing
()
714 head
= get_output("git rev-parse --verify HEAD")
716 raise YapError("No commit yet; nowhere to point")
718 ref
= get_output("git rev-parse --verify '%s^{commit}'" % where
)
720 raise YapError("Not a valid ref: %s" % where
)
722 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
723 raise YapError("You have uncommitted changes. Commit them first")
725 run_safely("git update-ref HEAD '%s'" % ref
[0])
727 if '-f' not in flags
:
728 name
= get_output("git name-rev --name-only '%s'" % head
[0])[0]
729 if name
== "undefined":
730 os
.system("git update-ref HEAD '%s'" % head
[0])
731 raise YapError("Pointing there will lose commits. Use -f to force")
734 run_safely("git read-tree -u -m HEAD")
736 run_safely("git read-tree HEAD")
737 run_safely("git checkout-index -u -f -a")
739 @short_help("alter history by dropping or amending commits")
741 This command operates in two distinct modes, "amend" and "drop" mode.
742 In drop mode, the given commit is removed from the history of the
743 current branch, as though that commit never happened. By default the
746 In amend mode, the uncommitted changes present are merged into a
747 previous commit. This is useful for correcting typos or adding missed
748 files into past commits. By default the commit used is HEAD.
750 While rewriting history it is possible that conflicts will arise. If
751 this happens, the rewrite will pause and you will be prompted to resolve
752 the conflicts and stage them. Once that is done, you will run "yap
753 history continue." If instead you want the conflicting commit removed
754 from history (perhaps your changes supercede that commit) you can run
755 "yap history skip". Once the rewrite completes, your branch will be on
756 the same commit as when the rewrite started.
758 def cmd_history(self
, subcmd
, *args
):
759 "amend | drop <commit>"
762 if subcmd
not in ("amend", "drop", "continue", "skip"):
766 When you have resolved the conflicts run \"yap history continue\".
767 To skip the problematic patch, run \"yap history skip\"."""
769 if subcmd
== "continue":
770 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
773 os
.system("git reset --hard")
774 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
777 if subcmd
== "amend":
778 flags
, args
= getopt
.getopt(args
, "ad")
788 if run_command("git rev-parse --verify '%s'" % commit
):
789 raise YapError("Not a valid commit: %s" % commit
)
791 self
._check
_rebasing
()
793 if subcmd
== "amend":
794 self
._check
_commit
(**flags
)
795 if self
._get
_unstaged
_files
():
796 # XXX: handle unstaged changes better
797 raise YapError("Commit away changes that you aren't amending")
801 start
= get_output("git rev-parse HEAD")
802 stash
= get_output("git stash create")
803 run_command("git reset --hard")
805 fd
, tmpfile
= tempfile
.mkstemp("yap")
809 os
.system("git format-patch -k --stdout '%s' > %s" % (commit
, tmpfile
))
810 if subcmd
== "amend":
811 self
.cmd_point(commit
, **{'-f': True})
813 if subcmd
== "amend":
815 rc
= os
.system("git stash apply %s" % stash
[0])
817 self
.cmd_point(start
[0], **{'-f': True})
818 os
.system("git stash apply %s" % stash
[0])
819 raise YapError("Failed to apply stash")
822 if subcmd
== "amend":
824 self
._check
_commit
(**{'-a': True})
827 self
.cmd_point("%s^" % commit
, **{'-f': True})
829 stat
= os
.stat(tmpfile
)
832 run_safely("git update-index --refresh")
833 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
835 raise YapError("Failed to apply changes")
840 run_command("git stash apply %s" % stash
[0])
843 @short_help("show the changes introduced by a given commit")
845 By default, the changes in the last commit are shown. To override this,
846 specify a hash, branch, or tag (committish). The hash of the commit,
847 the commit's author, log message, and a diff of the changes are shown.
849 def cmd_show(self
, commit
="HEAD"):
852 os
.system("git show '%s'" % commit
)
854 @short_help("apply the changes in a given commit to the current branch")
856 The argument is the hash, branch, or tag (committish) of the commit to
857 be applied. In general, it only makes sense to apply commits that
858 happened on another branch. The '-r' flag can be used to have the
859 changes in the given commit reversed from the current branch. In
860 general, this only makes sense for commits that happened on the current
864 def cmd_cherry_pick(self
, commit
, **flags
):
868 os
.system("git revert '%s'" % commit
)
870 os
.system("git cherry-pick '%s'" % commit
)
872 @short_help("list, add, or delete configured remote repositories")
874 When invoked with no arguments, this command will show the list of
875 currently configured remote repositories, giving both the name and URL
876 of each. To add a new repository, give the desired name as the first
877 argument and the URL as the second. The '-d' flag can be used to remove
878 a previously added repository.
881 def cmd_repo(self
, name
=None, url
=None, **flags
):
882 "[<name> <url> | -d <name>]"
884 if name
is not None and url
is None:
888 if flags
['-d'] not in [ x
[0] for x
in self
._list
_remotes
() ]:
889 raise YapError("No such repository: %s" % flags
['-d'])
890 os
.system("git config --unset remote.%s.url" % flags
['-d'])
891 os
.system("git config --unset remote.%s.fetch" % flags
['-d'])
894 if name
in [ x
[0] for x
in self
._list
_remotes
() ]:
895 raise YapError("Repository '%s' already exists" % flags
['-d'])
896 os
.system("git config remote.%s.url %s" % (name
, url
))
897 os
.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name
, name
))
899 for remote
, url
in self
._list
_remotes
():
900 print "%-20s %s" % (remote
, url
)
902 @short_help("send local commits to a remote repository (*)")
904 When invoked with no arguments, the current branch is synchronized to
905 the tracking branch of the tracking remote. If no tracking remote is
906 specified, the repository will have to be specified on the command line.
907 In that case, the default is to push to a branch with the same name as
908 the current branch. This behavior can be overridden by giving a second
909 argument to specify the remote branch.
911 If the remote branch does not currently exist, the command will abort
912 unless the -c flag is provided. If the remote branch is not a direct
913 descendent of the local branch, the command will abort unless the -f
914 flag is provided. Forcing a push in this way can be problematic to
915 other users of the repository if they are not expecting it.
917 To delete a branch on the remote repository, use the -d flag.
919 @takes_options("cdf")
920 def cmd_push(self
, repo
=None, rhs
=None, **flags
):
923 if '-c' in flags
and '-d' in flags
:
926 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
927 raise YapError("No such repository: %s" % repo
)
929 current
= get_output("git symbolic-ref HEAD")
931 raise YapError("Not on a branch!")
933 self
._check
_rebasing
()
935 current
= current
[0].replace('refs/heads/', '')
936 remote
= get_output("git config branch.%s.remote" % current
)
937 if repo
is None and remote
:
941 raise YapError("No tracking branch configured; specify destination repository")
943 if rhs
is None and remote
and remote
[0] == repo
:
944 merge
= get_output("git config branch.%s.merge" % current
)
949 rhs
= "refs/heads/%s" % current
951 if '-c' not in flags
and '-d' not in flags
:
952 if run_command("git rev-parse --verify refs/remotes/%s/%s"
953 % (repo
, rhs
.replace('refs/heads/', ''))):
954 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
955 if '-f' not in flags
:
956 hash = get_output("git rev-parse refs/remotes/%s/%s" % (repo
, rhs
.replace('refs/heads/', '')))
957 base
= get_output("git merge-base HEAD %s" % hash[0])
959 if base
[0] != hash[0]:
960 raise YapError("Branch not up-to-date with remote. Update or use -f")
962 self
._confirm
_push
(current
, rhs
, repo
)
969 lhs
= "refs/heads/%s" % current
970 rc
= os
.system("git push %s %s %s:%s" % (flags
.get('-f', ''), repo
, lhs
, rhs
))
972 raise YapError("Push failed.")
974 @short_help("retrieve commits from a remote repository")
976 When run with no arguments, the command will retrieve new commits from
977 the remote tracking repository. Note that this does not in any way
978 alter the current branch. For that, see "update". If a remote other
979 than the tracking remote is desired, it can be specified as the first
982 def cmd_fetch(self
, repo
=None):
985 current
= get_output("git symbolic-ref HEAD")
987 raise YapError("Not on a branch!")
989 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
990 raise YapError("No such repository: %s" % repo
)
992 current
= current
[0].replace('refs/heads/', '')
993 remote
= get_output("git config branch.%s.remote" % current
)
997 raise YapError("No tracking branch configured; specify a repository")
998 os
.system("git fetch %s" % repo
)
1000 @short_help("update the current branch relative to its tracking branch")
1002 Updates the current branch relative to its remote tracking branch. This
1003 command requires that the current branch have a remote tracking branch
1004 configured. If any conflicts occur while applying your changes to the
1005 updated remote, the command will pause to allow you to fix them. Once
1006 that is done, run "update" with the "continue" subcommand. Alternately,
1007 the "skip" subcommand can be used to discard the conflicting changes.
1009 def cmd_update(self
, subcmd
=None):
1012 if subcmd
and subcmd
not in ["continue", "skip"]:
1016 When you have resolved the conflicts run \"yap update continue\".
1017 To skip the problematic patch, run \"yap update skip\"."""
1019 if subcmd
== "continue":
1020 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
1022 if subcmd
== "skip":
1023 os
.system("git reset --hard")
1024 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
1027 self
._check
_rebasing
()
1028 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
1029 raise YapError("You have uncommitted changes. Commit them first")
1031 current
= get_output("git symbolic-ref HEAD")
1033 raise YapError("Not on a branch!")
1035 current
= current
[0].replace('refs/heads/', '')
1036 remote
, merge
= self
._get
_tracking
(current
)
1037 merge
= merge
.replace('refs/heads/', '')
1039 self
.cmd_fetch(remote
)
1040 base
= get_output("git merge-base HEAD refs/remotes/%s/%s" % (remote
, merge
))
1043 fd
, tmpfile
= tempfile
.mkstemp("yap")
1045 os
.system("git format-patch -k --stdout '%s' > %s" % (base
[0], tmpfile
))
1046 self
.cmd_point("refs/remotes/%s/%s" % (remote
, merge
), **{'-f': True})
1048 stat
= os
.stat(tmpfile
)
1051 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
1053 raise YapError("Failed to apply changes")
1057 @short_help("query and configure remote branch tracking")
1059 When invoked with no arguments, the command displays the tracking
1060 information for the current branch. To configure the tracking
1061 information, two arguments for the remote repository and remote branch
1062 are given. The tracking information is used to provide defaults for
1063 where to push local changes and from where to get updates to the branch.
1065 def cmd_track(self
, repo
=None, branch
=None):
1069 current
= get_output("git symbolic-ref HEAD")
1071 raise YapError("Not on a branch!")
1072 current
= current
[0].replace('refs/heads/', '')
1074 if repo
is None and branch
is None:
1075 repo
, merge
= self
._get
_tracking
(current
)
1076 merge
= merge
.replace('refs/heads/', '')
1077 print "Branch '%s' tracking refs/remotes/%s/%s" % (current
, repo
, merge
)
1080 if repo
is None or branch
is None:
1083 if repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1084 raise YapError("No such repository: %s" % repo
)
1086 if run_command("git rev-parse --verify refs/remotes/%s/%s" % (repo
, branch
)):
1087 raise YapError("No such branch '%s' on repository '%s'" % (branch
, repo
))
1089 os
.system("git config branch.%s.remote '%s'" % (current
, repo
))
1090 os
.system("git config branch.%s.merge 'refs/heads/%s'" % (current
, branch
))
1091 print "Branch '%s' now tracking refs/remotes/%s/%s" % (current
, repo
, branch
)
1093 @short_help("mark files with conflicts as resolved")
1095 The arguments are the files to be marked resolved. When a conflict
1096 occurs while merging changes to a file, that file is marked as
1097 "unmerged." Until the file(s) with conflicts are marked resolved,
1098 commits cannot be made.
1100 def cmd_resolved(self
, *args
):
1107 self
._stage
_one
(f
, True)
1110 def cmd_help(self
, cmd
=None):
1113 cmd
= "cmd_" + cmd
.replace('-', '_')
1115 attr
= self
.__getattribute
__(cmd
)
1116 except AttributeError:
1117 raise YapError("No such command: %s" % cmd
)
1120 help = self
._get
_attr
(cmd
, "long_help")
1121 except AttributeError:
1123 raise YapError("Sorry, no help for '%s'. Ask Steven." % cmd
)
1125 print >>sys
.stderr
, "The '%s' command" % oldcmd
1126 print >>sys
.stderr
, "\tyap %s %s" % (oldcmd
, attr
.__doc
__)
1127 print >>sys
.stderr
, "%s" % help
1130 print >> sys
.stderr
, "Yet Another (Git) Porcelein"
1133 for name
in dir(self
):
1134 if not name
.startswith('cmd_'):
1136 attr
= self
.__getattribute
__(name
)
1137 if not callable(attr
):
1141 short_msg
= self
._get
_attr
(name
, "short_help")
1142 except AttributeError:
1145 name
= name
.replace('cmd_', '')
1146 name
= name
.replace('_', '-')
1147 print >> sys
.stderr
, "%-16s%s" % (name
, short_msg
)
1150 print >> sys
.stderr
, "(*) Indicates that the command is not readily reversible"
1152 def cmd_usage(self
):
1153 print >> sys
.stderr
, "usage: %s <command>" % os
.path
.basename(sys
.argv
[0])
1154 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"
1157 plugindir
= os
.path
.join("~", ".yap", "plugins")
1158 plugindir
= os
.path
.expanduser(plugindir
)
1159 plugindir
= os
.path
.join(plugindir
, "*.py")
1162 for p
in glob
.glob(os
.path
.expanduser(plugindir
)):
1163 plugin
= os
.path
.basename(p
).replace('.py', '')
1164 m
= __import__(plugin
)
1167 if not type(cls
) == type:
1169 if not issubclass(cls
, YapCore
):
1176 def yap_metaclass(name
, bases
, dct
):
1177 plugindir
= os
.path
.join("~", ".yap", "plugins")
1178 plugindir
= os
.path
.expanduser(plugindir
)
1179 sys
.path
.insert(0, plugindir
)
1181 plugins
= set(load_plugins().values())
1184 p2
-= set(cls
.__bases
__)
1186 bases
= list(plugins
) + list(bases
)
1187 return type(name
, tuple(bases
), dct
)
1190 __metaclass__
= yap_metaclass
1192 def main(self
, args
):
1200 if run_command("git --version"):
1201 print >>sys
.stderr
, "Failed to run git; is it installed?"
1204 debug
= os
.getenv('YAP_DEBUG')
1207 command
= command
.replace('-', '_')
1208 meth
= self
.__getattribute
__("cmd_"+command
)
1209 doc
= self
._get
_attr
("cmd_"+command
, "__doc__")
1213 for c
in self
.__class
__.__bases
__:
1215 t
= c
.__dict
__["cmd_"+command
]
1218 if "options" in t
.__dict
__:
1219 options
+= t
.options
1223 flags
, args
= getopt
.getopt(args
, options
)
1225 except getopt
.GetoptError
, e
:
1228 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1234 meth(*args
, **flags
)
1235 except (TypeError, getopt
.GetoptError
):
1238 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1242 print >> sys
.stderr
, e
1244 except AttributeError: