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 _resolve_rev(self
, rev
):
108 ref
= get_output("git rev-parse --verify %s 2>/dev/null" % rev
)
110 raise YapError("No such revision: %s" % rev
)
113 def _delete_branch(self
, branch
, force
):
114 current
= get_output("git symbolic-ref HEAD")
116 current
= current
[0].replace('refs/heads/', '')
117 if branch
== current
:
118 raise YapError("Can't delete current branch")
120 ref
= self
._resolve
_rev
('refs/heads/'+branch
)
121 run_safely("git update-ref -d 'refs/heads/%s' '%s'" % (branch
, ref
))
124 name
= get_output("git name-rev --name-only '%s'" % ref
)[0]
125 if name
== 'undefined':
126 run_command("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
))
127 raise YapError("Refusing to delete leaf branch (use -f to force)")
128 def _get_pager_cmd(self
):
129 if 'YAP_PAGER' in os
.environ
:
130 return os
.environ
['YAP_PAGER']
131 elif 'GIT_PAGER' in os
.environ
:
132 return os
.environ
['GIT_PAGER']
133 elif 'PAGER' in os
.environ
:
134 return os
.environ
['PAGER']
138 def _add_one(self
, file):
139 self
._assert
_file
_exists
(file)
140 x
= get_output("git ls-files '%s'" % file)
142 raise YapError("File '%s' already in repository" % file)
143 self
._add
_new
_file
(file)
145 def _rm_one(self
, file):
146 self
._assert
_file
_exists
(file)
147 if get_output("git ls-files '%s'" % file) != []:
148 run_safely("git rm --cached '%s'" % file)
149 self
._remove
_new
_file
(file)
151 def _stage_one(self
, file, allow_unmerged
=False):
152 self
._assert
_file
_exists
(file)
153 prefix
= get_output("git rev-parse --show-prefix")
155 tmp
= os
.path
.normpath(os
.path
.join(prefix
[0], file))
158 if not allow_unmerged
and tmp
in self
._get
_unmerged
_files
():
159 raise YapError("Refusing to stage conflicted file: %s" % file)
160 run_safely("git update-index --add '%s'" % file)
162 def _unstage_one(self
, file):
163 self
._assert
_file
_exists
(file)
164 if run_command("git rev-parse HEAD"):
165 rc
= run_command("git update-index --force-remove '%s'" % file)
167 rc
= run_command("git diff-index --cached -p HEAD '%s' | git apply -R --cached" % file)
169 raise YapError("Failed to unstage")
171 def _revert_one(self
, file):
172 self
._assert
_file
_exists
(file)
174 self
._unstage
_one
(file)
177 run_safely("git checkout-index -u -f '%s'" % file)
179 def _parse_commit(self
, commit
):
180 lines
= get_output("git cat-file commit '%s'" % commit
)
185 if mode
!= 'commit' and l
.strip() == "":
190 commit
['log'].append(l
)
197 commit
['log'] = '\n'.join(commit
['log'])
200 def _check_commit(self
, **flags
):
201 if '-a' in flags
and '-d' in flags
:
202 raise YapError("Conflicting flags: -a and -d")
204 if '-d' not in flags
and self
._get
_unstaged
_files
():
205 if '-a' not in flags
and self
._get
_staged
_files
():
206 raise YapError("Staged and unstaged changes present. Specify what to commit")
207 os
.system("git diff-files -p | git apply --cached")
208 for f
in self
._get
_new
_files
():
211 def _do_uncommit(self
):
212 commit
= self
._parse
_commit
("HEAD")
213 repo
= get_output('git rev-parse --git-dir')[0]
214 dir = os
.path
.join(repo
, 'yap')
219 msg_file
= os
.path
.join(dir, 'msg')
220 fd
= file(msg_file
, 'w')
221 print >>fd
, commit
['log']
224 tree
= get_output("git rev-parse --verify HEAD^")
225 run_safely("git update-ref -m uncommit HEAD '%s'" % tree
[0])
227 def _do_commit(self
, msg
=None):
228 tree
= get_output("git write-tree")[0]
230 repo
= get_output('git rev-parse --git-dir')[0]
231 head_file
= os
.path
.join(repo
, 'yap', 'merge')
233 parent
= pickle
.load(file(head_file
))
235 parent
= get_output("git rev-parse --verify HEAD 2> /dev/null")
237 if os
.environ
.has_key('YAP_EDITOR'):
238 editor
= os
.environ
['YAP_EDITOR']
239 elif os
.environ
.has_key('GIT_EDITOR'):
240 editor
= os
.environ
['GIT_EDITOR']
241 elif os
.environ
.has_key('EDITOR'):
242 editor
= os
.environ
['EDITOR']
246 fd
, tmpfile
= tempfile
.mkstemp("yap")
251 msg_file
= os
.path
.join(repo
, 'yap', 'msg')
252 if os
.access(msg_file
, os
.R_OK
):
254 fd2
= file(tmpfile
, 'w')
255 for l
in fd1
.xreadlines():
256 print >>fd2
, l
.strip()
259 if os
.system("%s '%s'" % (editor
, tmpfile
)) != 0:
260 raise YapError("Editing commit message failed")
267 raise YapError("Refusing to use empty commit message")
269 (fd_w
, fd_r
) = os
.popen2("git stripspace > %s" % tmpfile
)
275 parent
= ' -p '.join(parent
)
276 commit
= get_output("git commit-tree '%s' -p %s < '%s'" % (tree
, parent
, tmpfile
))
278 commit
= get_output("git commit-tree '%s' < '%s'" % (tree
, tmpfile
))
281 run_safely("git update-ref HEAD '%s'" % commit
[0])
284 def _check_rebasing(self
):
285 repo
= get_output('git rev-parse --git-dir')[0]
286 dotest
= os
.path
.join(repo
, '.dotest')
287 if os
.access(dotest
, os
.R_OK
):
288 raise YapError("A git operation is in progress. Complete it first")
289 dotest
= os
.path
.join(repo
, '..', '.dotest')
290 if os
.access(dotest
, os
.R_OK
):
291 raise YapError("A git operation is in progress. Complete it first")
293 def _check_git(self
):
294 if run_command("git rev-parse --git-dir"):
295 raise YapError("That command must be run from inside a git repository")
297 def _list_remotes(self
):
298 remotes
= get_output("git config --get-regexp '^remote.*.url'")
300 remote
, url
= x
.split(' ')
301 remote
= remote
.replace('remote.', '')
302 remote
= remote
.replace('.url', '')
305 def _unstage_all(self
):
307 run_safely("git read-tree -m HEAD")
309 run_safely("git read-tree HEAD")
310 run_safely("git update-index -q --refresh")
312 def _get_tracking(self
, current
):
313 remote
= get_output("git config branch.%s.remote" % current
)
315 raise YapError("No tracking branch configured for '%s'" % current
)
317 merge
= get_output("git config branch.%s.merge" % current
)
319 raise YapError("No tracking branch configured for '%s'" % current
)
320 return remote
[0], merge
[0]
322 def _confirm_push(self
, current
, rhs
, repo
):
323 print "About to push local branch '%s' to '%s' on '%s'" % (current
, rhs
, repo
)
324 print "Continue (y/n)? ",
326 ans
= sys
.stdin
.readline().strip()
328 if ans
.lower() != 'y' and ans
.lower() != 'yes':
329 raise YapError("Aborted.")
331 def _clear_state(self
):
332 repo
= get_output('git rev-parse --git-dir')[0]
333 dir = os
.path
.join(repo
, 'yap')
334 for f
in "new-files", "merge", "msg":
336 os
.unlink(os
.path
.join(dir, f
))
340 def _get_attr(self
, name
, attr
):
342 for c
in self
.__class
__.__bases
__:
344 m2
= c
.__dict
__[name
]
348 val
= m2
.__getattribute__(attr
)
349 except AttributeError:
353 @short_help("make a local copy of an existing repository")
355 The first argument is a URL to the existing repository. This can be an
356 absolute path if the repository is local, or a URL with the git://,
357 ssh://, or http:// schemes. By default, the directory used is the last
358 component of the URL, sans '.git'. This can be overridden by providing
361 def cmd_clone(self
, url
, directory
=None):
364 if '://' not in url
and url
[0] != '/':
365 url
= os
.path
.join(os
.getcwd(), url
)
367 url
= url
.rstrip('/')
368 if directory
is None:
369 directory
= url
.rsplit('/')[-1]
370 directory
= directory
.replace('.git', '')
375 raise YapError("Directory exists: %s" % directory
)
378 self
.cmd_repo("origin", url
)
379 self
.cmd_fetch("origin")
382 if not run_command("git rev-parse --verify refs/remotes/origin/HEAD"):
383 hash = get_output("git rev-parse refs/remotes/origin/HEAD")[0]
384 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'"):
385 if get_output("git rev-parse %s" % b
)[0] == hash:
389 if not run_command("git rev-parse --verify refs/remotes/origin/master"):
390 branch
= "refs/remotes/origin/master"
392 branch
= get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'")
395 hash = get_output("git rev-parse %s" % branch
)
397 branch
= branch
.replace('refs/remotes/origin/', '')
398 run_safely("git update-ref refs/heads/%s %s" % (branch
, hash[0]))
399 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
400 self
.cmd_revert(**{'-a': 1})
402 @short_help("turn a directory into a repository")
404 Converts the current working directory into a repository. The primary
405 side-effect of this command is the creation of a '.git' subdirectory.
406 No files are added nor commits made.
409 os
.system("git init")
411 @short_help("add a new file to the repository")
413 The arguments are the files to be added to the repository. Once added,
414 the files will show as "unstaged changes" in the output of 'status'. To
415 reverse the effects of this command, see 'rm'.
417 def cmd_add(self
, *files
):
428 @short_help("delete a file from the repository")
430 The arguments are the files to be removed from the current revision of
431 the repository. The files will still exist in any past commits that the
432 files may have been a part of. The file is not actually deleted, it is
433 just no longer tracked as part of the repository.
435 def cmd_rm(self
, *files
):
445 @short_help("stage changes in a file for commit")
447 The arguments are the files to be staged. Staging changes is a way to
448 build up a commit when you do not want to commit all changes at once.
449 To commit only staged changes, use the '-d' flag to 'commit.' To
450 reverse the effects of this command, see 'unstage'. Once staged, the
451 files will show as "staged changes" in the output of 'status'.
453 def cmd_stage(self
, *files
):
463 @short_help("unstage changes in a file")
465 The arguments are the files to be unstaged. Once unstaged, the files
466 will show as "unstaged changes" in the output of 'status'. The '-a'
467 flag can be used to unstage all staged changes at once.
470 def cmd_unstage(self
, *files
, **flags
):
474 files
= self
._get
_staged
_files
()
483 @short_help("show files with staged and unstaged changes")
485 Show the files in the repository with changes since the last commit,
486 categorized based on whether the changes are staged or not. A file may
487 appear under each heading if the same file has both staged and unstaged
490 def cmd_status(self
):
493 branch
= get_output("git symbolic-ref HEAD")
495 branch
= branch
[0].replace('refs/heads/', '')
498 print "Current branch: %s" % branch
500 print "Files with staged changes:"
501 files
= self
._get
_staged
_files
()
507 print "Files with unstaged changes:"
508 files
= self
._get
_unstaged
_files
()
514 files
= self
._get
_unmerged
_files
()
516 print "Files with conflicts:"
520 @short_help("remove uncommitted changes from a file (*)")
522 The arguments are the files whose changes will be reverted. If the '-a'
523 flag is given, then all files will have uncommitted changes removed.
524 Note that there is no way to reverse this command short of manually
525 editing each file again.
528 def cmd_revert(self
, *files
, **flags
):
533 run_safely("git checkout-index -u -f -a")
545 @short_help("record changes to files as a new commit")
547 Create a new commit recording changes since the last commit. If there
548 are only unstaged changes, those will be recorded. If there are only
549 staged changes, those will be recorded. Otherwise, you will have to
550 specify either the '-a' flag or the '-d' flag to commit all changes or
551 only staged changes, respectively. To reverse the effects of this
552 command, see 'uncommit'.
554 @takes_options("adm:")
555 def cmd_commit(self
, **flags
):
556 "[-a | -d] [-m <msg>]"
558 self
._check
_rebasing
()
559 self
._check
_commit
(**flags
)
560 if not self
._get
_staged
_files
():
561 raise YapError("No changes to commit")
562 msg
= flags
.get('-m', None)
566 @short_help("reverse the actions of the last commit")
568 Reverse the effects of the last 'commit' operation. The changes that
569 were part of the previous commit will show as "staged changes" in the
570 output of 'status'. This means that if no files were changed since the
571 last commit was created, 'uncommit' followed by 'commit' is a lossless
574 def cmd_uncommit(self
):
580 @short_help("report the current version of yap")
581 def cmd_version(self
):
582 print "Yap version 0.1"
584 @short_help("show the changelog for particular versions or files")
586 The arguments are the files with which to filter history. If none are
587 given, all changes are listed. Otherwise only commits that affected one
588 or more of the given files are listed. The -r option changes the
589 starting revision for traversing history. By default, history is listed
592 @takes_options("pr:")
593 def cmd_log(self
, *paths
, **flags
):
594 "[-p] [-r <rev>] <path>..."
596 rev
= flags
.get('-r', 'HEAD')
605 paths
= ' '.join(paths
)
606 os
.system("git log -M -C %s %s '%s' -- %s"
607 % (follow
, flags
.get('-p', '--name-status'), rev
, paths
))
609 @short_help("show staged, unstaged, or all uncommitted changes")
611 Show staged, unstaged, or all uncommitted changes. By default, all
612 changes are shown. The '-u' flag causes only unstaged changes to be
613 shown. The '-d' flag causes only staged changes to be shown.
616 def cmd_diff(self
, **flags
):
619 if '-u' in flags
and '-d' in flags
:
620 raise YapError("Conflicting flags: -u and -d")
622 pager
= self
._get
_pager
_cmd
()
625 os
.system("git diff-files -p | %s" % pager
)
627 os
.system("git diff-index --cached -p HEAD | %s" % pager
)
629 os
.system("git diff-index -p HEAD | %s" % pager
)
631 @short_help("list, create, or delete branches")
633 If no arguments are specified, a list of local branches is given. The
634 current branch is indicated by a "*" next to the name. If an argument
635 is given, it is taken as the name of a new branch to create. The branch
636 will start pointing at the current HEAD. See 'point' for details on
637 changing the revision of the new branch. Note that this command does
638 not switch the current working branch. See 'switch' for details on
639 changing the current working branch.
641 The '-d' flag can be used to delete local branches. If the delete
642 operation would remove the last branch reference to a given line of
643 history (colloquially referred to as "dangling commits"), yap will
644 report an error and abort. The '-f' flag can be used to force the delete
647 @takes_options("fd:")
648 def cmd_branch(self
, branch
=None, **flags
):
649 "[ [-f] -d <branch> | <branch> ]"
651 force
= '-f' in flags
653 self
._delete
_branch
(flags
['-d'], force
)
657 if branch
is not None:
658 ref
= get_output("git rev-parse --verify HEAD")
660 raise YapError("No branch point yet. Make a commit")
661 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
663 current
= get_output("git symbolic-ref HEAD")
664 branches
= get_output("git for-each-ref --format='%(refname)' 'refs/heads/*'")
666 if current
and b
== current
[0]:
670 b
= b
.replace('refs/heads/', '')
673 @short_help("change the current working branch")
675 The argument is the name of the branch to make the current working
676 branch. This command will fail if there are uncommitted changes to any
677 files. Otherwise, the contents of the files in the working directory
678 are updated to reflect their state in the new branch. Additionally, any
679 future commits are added to the new branch instead of the previous line
683 def cmd_switch(self
, branch
, **flags
):
686 self
._check
_rebasing
()
687 ref
= self
._resolve
_rev
('refs/heads/'+branch
)
689 if '-f' not in flags
:
690 if (self
._get
_staged
_files
()
691 or (self
._get
_unstaged
_files
()
692 and run_command("git update-index --refresh"))):
693 raise YapError("You have uncommitted changes. Use -f to continue anyway")
695 if self
._get
_unstaged
_files
() and self
._get
_staged
_files
():
696 raise YapError("You have staged and unstaged changes. Perhaps unstage -a?")
698 staged
= bool(self
._get
_staged
_files
())
700 run_command("git diff-files -p | git apply --cached")
701 for f
in self
._get
_new
_files
():
704 idx
= get_output("git write-tree")
705 new
= self
._resolve
_rev
('refs/heads/'+branch
)
706 readtree
= "git read-tree --aggressive -u -m HEAD %s %s" % (idx
[0], new
)
707 if run_command(readtree
):
708 run_command("git update-index --refresh")
709 if os
.system(readtree
):
710 raise YapError("Failed to switch")
711 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
713 if '-f' not in flags
:
720 @short_help("move the current branch to a different revision")
722 The argument is the hash of the commit to which the current branch
723 should point, or alternately a branch or tag (a.k.a, "committish"). If
724 moving the branch would create "dangling commits" (see 'branch'), yap
725 will report an error and abort. The '-f' flag can be used to force the
726 operation in spite of this.
729 def cmd_point(self
, where
, **flags
):
732 self
._check
_rebasing
()
734 head
= get_output("git rev-parse --verify HEAD")
736 raise YapError("No commit yet; nowhere to point")
738 ref
= self
._resolve
_rev
(where
)
739 ref
= get_output("git rev-parse --verify '%s^{commit}'" % ref
)
741 raise YapError("Not a commit: %s" % where
)
743 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
744 raise YapError("You have uncommitted changes. Commit them first")
746 run_safely("git update-ref HEAD '%s'" % ref
[0])
748 if '-f' not in flags
:
749 name
= get_output("git name-rev --name-only '%s'" % head
[0])[0]
750 if name
== "undefined":
751 os
.system("git update-ref HEAD '%s'" % head
[0])
752 raise YapError("Pointing there will lose commits. Use -f to force")
755 run_safely("git read-tree -u -m HEAD")
757 run_safely("git read-tree HEAD")
758 run_safely("git checkout-index -u -f -a")
761 @short_help("alter history by dropping or amending commits")
763 This command operates in two distinct modes, "amend" and "drop" mode.
764 In drop mode, the given commit is removed from the history of the
765 current branch, as though that commit never happened. By default the
768 In amend mode, the uncommitted changes present are merged into a
769 previous commit. This is useful for correcting typos or adding missed
770 files into past commits. By default the commit used is HEAD.
772 While rewriting history it is possible that conflicts will arise. If
773 this happens, the rewrite will pause and you will be prompted to resolve
774 the conflicts and stage them. Once that is done, you will run "yap
775 history continue." If instead you want the conflicting commit removed
776 from history (perhaps your changes supercede that commit) you can run
777 "yap history skip". Once the rewrite completes, your branch will be on
778 the same commit as when the rewrite started.
780 def cmd_history(self
, subcmd
, *args
):
781 "amend | drop <commit>"
784 if subcmd
not in ("amend", "drop", "continue", "skip"):
788 When you have resolved the conflicts run \"yap history continue\".
789 To skip the problematic patch, run \"yap history skip\"."""
791 if subcmd
== "continue":
792 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
795 os
.system("git reset --hard")
796 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
799 if subcmd
== "amend":
800 flags
, args
= getopt
.getopt(args
, "ad")
810 self
._resolve
_rev
(commit
)
811 self
._check
_rebasing
()
813 if subcmd
== "amend":
814 self
._check
_commit
(**flags
)
815 if self
._get
_unstaged
_files
():
816 # XXX: handle unstaged changes better
817 raise YapError("Commit away changes that you aren't amending")
821 start
= get_output("git rev-parse HEAD")
822 stash
= get_output("git stash create")
823 run_command("git reset --hard")
825 fd
, tmpfile
= tempfile
.mkstemp("yap")
829 os
.system("git format-patch -k --stdout '%s' > %s" % (commit
, tmpfile
))
830 if subcmd
== "amend":
831 self
.cmd_point(commit
, **{'-f': True})
833 if subcmd
== "amend":
835 rc
= os
.system("git stash apply %s" % stash
[0])
837 self
.cmd_point(start
[0], **{'-f': True})
838 os
.system("git stash apply %s" % stash
[0])
839 raise YapError("Failed to apply stash")
842 if subcmd
== "amend":
844 self
._check
_commit
(**{'-a': True})
847 self
.cmd_point("%s^" % commit
, **{'-f': True})
849 stat
= os
.stat(tmpfile
)
852 run_safely("git update-index --refresh")
853 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
855 raise YapError("Failed to apply changes")
860 run_command("git stash apply %s" % stash
[0])
863 @short_help("show the changes introduced by a given commit")
865 By default, the changes in the last commit are shown. To override this,
866 specify a hash, branch, or tag (committish). The hash of the commit,
867 the commit's author, log message, and a diff of the changes are shown.
869 def cmd_show(self
, commit
="HEAD"):
872 os
.system("git show '%s'" % commit
)
874 @short_help("apply the changes in a given commit to the current branch")
876 The argument is the hash, branch, or tag (committish) of the commit to
877 be applied. In general, it only makes sense to apply commits that
878 happened on another branch. The '-r' flag can be used to have the
879 changes in the given commit reversed from the current branch. In
880 general, this only makes sense for commits that happened on the current
884 def cmd_cherry_pick(self
, commit
, **flags
):
888 os
.system("git revert '%s'" % commit
)
890 os
.system("git cherry-pick '%s'" % commit
)
892 @short_help("list, add, or delete configured remote repositories")
894 When invoked with no arguments, this command will show the list of
895 currently configured remote repositories, giving both the name and URL
896 of each. To add a new repository, give the desired name as the first
897 argument and the URL as the second. The '-d' flag can be used to remove
898 a previously added repository.
901 def cmd_repo(self
, name
=None, url
=None, **flags
):
902 "[<name> <url> | -d <name>]"
904 if name
is not None and url
is None:
908 if flags
['-d'] not in [ x
[0] for x
in self
._list
_remotes
() ]:
909 raise YapError("No such repository: %s" % flags
['-d'])
910 os
.system("git config --unset remote.%s.url" % flags
['-d'])
911 os
.system("git config --unset remote.%s.fetch" % flags
['-d'])
912 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'"):
913 hash = get_output("git rev-parse %s" % b
)
915 run_safely("git update-ref -d %s %s" % (b
, hash[0]))
918 if name
in [ x
[0] for x
in self
._list
_remotes
() ]:
919 raise YapError("Repository '%s' already exists" % name
)
920 os
.system("git config remote.%s.url %s" % (name
, url
))
921 os
.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name
, name
))
923 for remote
, url
in self
._list
_remotes
():
924 print "%-20s %s" % (remote
, url
)
926 @short_help("send local commits to a remote repository (*)")
928 When invoked with no arguments, the current branch is synchronized to
929 the tracking branch of the tracking remote. If no tracking remote is
930 specified, the repository will have to be specified on the command line.
931 In that case, the default is to push to a branch with the same name as
932 the current branch. This behavior can be overridden by giving a second
933 argument to specify the remote branch.
935 If the remote branch does not currently exist, the command will abort
936 unless the -c flag is provided. If the remote branch is not a direct
937 descendent of the local branch, the command will abort unless the -f
938 flag is provided. Forcing a push in this way can be problematic to
939 other users of the repository if they are not expecting it.
941 To delete a branch on the remote repository, use the -d flag.
943 @takes_options("cdf")
944 def cmd_push(self
, repo
=None, rhs
=None, **flags
):
947 if '-c' in flags
and '-d' in flags
:
950 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
951 raise YapError("No such repository: %s" % repo
)
953 current
= get_output("git symbolic-ref HEAD")
955 raise YapError("Not on a branch!")
957 self
._check
_rebasing
()
959 current
= current
[0].replace('refs/heads/', '')
960 remote
= get_output("git config branch.%s.remote" % current
)
961 if repo
is None and remote
:
965 raise YapError("No tracking branch configured; specify destination repository")
967 if rhs
is None and remote
and remote
[0] == repo
:
968 merge
= get_output("git config branch.%s.merge" % current
)
973 rhs
= "refs/heads/%s" % current
975 if '-c' not in flags
and '-d' not in flags
:
976 if run_command("git rev-parse --verify refs/remotes/%s/%s"
977 % (repo
, rhs
.replace('refs/heads/', ''))):
978 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
979 if '-f' not in flags
:
980 hash = get_output("git rev-parse refs/remotes/%s/%s" % (repo
, rhs
.replace('refs/heads/', '')))
981 base
= get_output("git merge-base HEAD %s" % hash[0])
983 if base
[0] != hash[0]:
984 raise YapError("Branch not up-to-date with remote. Update or use -f")
986 self
._confirm
_push
(current
, rhs
, repo
)
993 lhs
= "refs/heads/%s" % current
994 rc
= os
.system("git push %s %s %s:%s" % (flags
.get('-f', ''), repo
, lhs
, rhs
))
996 raise YapError("Push failed.")
998 @short_help("retrieve commits from a remote repository")
1000 When run with no arguments, the command will retrieve new commits from
1001 the remote tracking repository. Note that this does not in any way
1002 alter the current branch. For that, see "update". If a remote other
1003 than the tracking remote is desired, it can be specified as the first
1006 def cmd_fetch(self
, repo
=None):
1009 current
= get_output("git symbolic-ref HEAD")
1011 raise YapError("Not on a branch!")
1013 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1014 raise YapError("No such repository: %s" % repo
)
1016 current
= current
[0].replace('refs/heads/', '')
1017 remote
= get_output("git config branch.%s.remote" % current
)
1021 raise YapError("No tracking branch configured; specify a repository")
1022 os
.system("git fetch %s" % repo
)
1024 @short_help("update the current branch relative to its tracking branch")
1026 Updates the current branch relative to its remote tracking branch. This
1027 command requires that the current branch have a remote tracking branch
1028 configured. If any conflicts occur while applying your changes to the
1029 updated remote, the command will pause to allow you to fix them. Once
1030 that is done, run "update" with the "continue" subcommand. Alternately,
1031 the "skip" subcommand can be used to discard the conflicting changes.
1033 def cmd_update(self
, subcmd
=None):
1036 if subcmd
and subcmd
not in ["continue", "skip"]:
1040 When you have resolved the conflicts run \"yap update continue\".
1041 To skip the problematic patch, run \"yap update skip\"."""
1043 if subcmd
== "continue":
1044 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
1046 if subcmd
== "skip":
1047 os
.system("git reset --hard")
1048 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
1051 self
._check
_rebasing
()
1052 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
1053 raise YapError("You have uncommitted changes. Commit them first")
1055 current
= get_output("git symbolic-ref HEAD")
1057 raise YapError("Not on a branch!")
1059 current
= current
[0].replace('refs/heads/', '')
1060 remote
, merge
= self
._get
_tracking
(current
)
1061 merge
= merge
.replace('refs/heads/', '')
1063 self
.cmd_fetch(remote
)
1064 base
= get_output("git merge-base HEAD refs/remotes/%s/%s" % (remote
, merge
))
1067 fd
, tmpfile
= tempfile
.mkstemp("yap")
1069 os
.system("git format-patch -k --stdout '%s' > %s" % (base
[0], tmpfile
))
1070 self
.cmd_point("refs/remotes/%s/%s" % (remote
, merge
), **{'-f': True})
1072 stat
= os
.stat(tmpfile
)
1075 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
1077 raise YapError("Failed to apply changes")
1081 @short_help("query and configure remote branch tracking")
1083 When invoked with no arguments, the command displays the tracking
1084 information for the current branch. To configure the tracking
1085 information, two arguments for the remote repository and remote branch
1086 are given. The tracking information is used to provide defaults for
1087 where to push local changes and from where to get updates to the branch.
1089 def cmd_track(self
, repo
=None, branch
=None):
1093 current
= get_output("git symbolic-ref HEAD")
1095 raise YapError("Not on a branch!")
1096 current
= current
[0].replace('refs/heads/', '')
1098 if repo
is None and branch
is None:
1099 repo
, merge
= self
._get
_tracking
(current
)
1100 merge
= merge
.replace('refs/heads/', '')
1101 print "Branch '%s' tracking refs/remotes/%s/%s" % (current
, repo
, merge
)
1104 if repo
is None or branch
is None:
1107 if repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1108 raise YapError("No such repository: %s" % repo
)
1110 if run_command("git rev-parse --verify refs/remotes/%s/%s" % (repo
, branch
)):
1111 raise YapError("No such branch '%s' on repository '%s'" % (branch
, repo
))
1113 os
.system("git config branch.%s.remote '%s'" % (current
, repo
))
1114 os
.system("git config branch.%s.merge 'refs/heads/%s'" % (current
, branch
))
1115 print "Branch '%s' now tracking refs/remotes/%s/%s" % (current
, repo
, branch
)
1117 @short_help("mark files with conflicts as resolved")
1119 The arguments are the files to be marked resolved. When a conflict
1120 occurs while merging changes to a file, that file is marked as
1121 "unmerged." Until the file(s) with conflicts are marked resolved,
1122 commits cannot be made.
1124 def cmd_resolved(self
, *files
):
1131 self
._stage
_one
(f
, True)
1134 @short_help("merge a branch into the current branch")
1135 def cmd_merge(self
, branch
):
1139 branch_name
= branch
1140 branch
= self
._resolve
_rev
(branch
)
1141 base
= get_output("git merge-base HEAD %s" % branch
)
1143 raise YapError("Branch '%s' is not a fork of the current branch"
1146 readtree
= ("git read-tree --aggressive -u -m %s HEAD %s"
1147 % (base
[0], branch
))
1148 if run_command(readtree
):
1149 run_command("git update-index --refresh")
1150 if os
.system(readtree
):
1151 raise YapError("Failed to merge")
1153 repo
= get_output('git rev-parse --git-dir')[0]
1154 dir = os
.path
.join(repo
, 'yap')
1159 msg_file
= os
.path
.join(dir, 'msg')
1160 msg
= file(msg_file
, 'w')
1161 print >>msg
, "Merge branch '%s'" % branch_name
1164 head
= get_output("git rev-parse --verify HEAD")
1166 heads
= [head
[0], branch
]
1167 head_file
= os
.path
.join(dir, 'merge')
1168 pickle
.dump(heads
, file(head_file
, 'w'))
1170 self
._merge
_index
(branch
, base
[0])
1171 if self
._get
_unmerged
_files
():
1173 raise YapError("Fix conflicts then commit")
1177 def _merge_index(self
, branch
, base
):
1178 for f
in self
._get
_unmerged
_files
():
1179 fd
, bfile
= tempfile
.mkstemp("yap")
1181 rc
= os
.system("git show %s:%s > %s" % (base
, f
, bfile
))
1184 fd
, ofile
= tempfile
.mkstemp("yap")
1186 rc
= os
.system("git show %s:%s > %s" % (branch
, f
, ofile
))
1189 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
)
1190 rc
= os
.system(command
)
1196 self
._stage
_one
(f
, True)
1198 def cmd_help(self
, cmd
=None):
1201 cmd
= "cmd_" + cmd
.replace('-', '_')
1203 attr
= self
.__getattribute
__(cmd
)
1204 except AttributeError:
1205 raise YapError("No such command: %s" % cmd
)
1208 help = self
._get
_attr
(cmd
, "long_help")
1209 except AttributeError:
1211 raise YapError("Sorry, no help for '%s'. Ask Steven." % cmd
)
1213 print >>sys
.stderr
, "The '%s' command" % oldcmd
1214 print >>sys
.stderr
, "\tyap %s %s" % (oldcmd
, attr
.__doc
__)
1215 print >>sys
.stderr
, "%s" % help
1218 print >> sys
.stderr
, "Yet Another (Git) Porcelein"
1221 for name
in dir(self
):
1222 if not name
.startswith('cmd_'):
1224 attr
= self
.__getattribute
__(name
)
1225 if not callable(attr
):
1229 short_msg
= self
._get
_attr
(name
, "short_help")
1230 except AttributeError:
1233 name
= name
.replace('cmd_', '')
1234 name
= name
.replace('_', '-')
1235 print >> sys
.stderr
, "%-16s%s" % (name
, short_msg
)
1238 print >> sys
.stderr
, "(*) Indicates that the command is not readily reversible"
1240 @short_help("show information about loaded plugins")
1241 def cmd_plugins(self
):
1243 print >> sys
.stderr
, "Loaded plugins:"
1244 plugins
= load_plugins()
1245 for name
, cls
in plugins
.items():
1246 print "\t%-16s: %s" % (name
, cls
.__doc
__)
1248 print "\t%-16s" % "None"
1250 def cmd_usage(self
):
1251 print >> sys
.stderr
, "usage: %s <command>" % os
.path
.basename(sys
.argv
[0])
1252 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"
1255 plugindir
= os
.path
.join("~", ".yap", "plugins")
1256 plugindir
= os
.path
.expanduser(plugindir
)
1257 plugindir
= os
.path
.join(plugindir
, "*.py")
1260 for p
in glob
.glob(os
.path
.expanduser(plugindir
)):
1261 plugin
= os
.path
.basename(p
).replace('.py', '')
1262 m
= __import__(plugin
)
1265 if not type(cls
) == type:
1267 if not issubclass(cls
, YapCore
):
1274 def yap_metaclass(name
, bases
, dct
):
1275 plugindir
= os
.path
.join("~", ".yap", "plugins")
1276 plugindir
= os
.path
.expanduser(plugindir
)
1277 sys
.path
.insert(0, plugindir
)
1279 plugins
= set(load_plugins().values())
1282 p2
-= set(cls
.__bases
__)
1284 bases
= list(plugins
) + list(bases
)
1285 return type(name
, tuple(bases
), dct
)
1288 __metaclass__
= yap_metaclass
1290 def main(self
, args
):
1298 if run_command("git --version"):
1299 print >>sys
.stderr
, "Failed to run git; is it installed?"
1302 debug
= os
.getenv('YAP_DEBUG')
1305 command
= command
.replace('-', '_')
1306 meth
= self
.__getattribute
__("cmd_"+command
)
1307 doc
= self
._get
_attr
("cmd_"+command
, "__doc__")
1311 for c
in self
.__class
__.__bases
__:
1313 t
= c
.__dict
__["cmd_"+command
]
1316 if "options" in t
.__dict
__:
1317 options
+= t
.options
1321 flags
, args
= getopt
.getopt(args
, options
)
1323 except getopt
.GetoptError
, e
:
1326 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1332 meth(*args
, **flags
)
1333 except (TypeError, getopt
.GetoptError
):
1336 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1340 print >> sys
.stderr
, e
1342 except AttributeError: