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")
74 prefix
= [ prefix
[0] ]
76 head
, tail
= os
.path
.split(prefix
[0])
81 prefix
.insert(1, tail
)
85 head
, tail
= os
.path
.split(path
[0])
93 for a
, b
in zip(prefix
, path
):
99 cdup
= [".."] * (len(prefix
) - common
)
100 path
= cdup
+ list(path
)
101 path
= os
.path
.join(*path
)
104 def _get_staged_files(self
):
105 if run_command("git rev-parse HEAD"):
106 files
= get_output("git ls-files --cached")
108 files
= get_output("git diff-index --cached --name-only HEAD")
109 unmerged
= self
._get
_unmerged
_files
()
111 unmerged
= set(unmerged
)
112 files
= set(files
).difference(unmerged
)
116 def _get_unstaged_files(self
):
118 cdup
= get_output("git rev-parse --show-cdup")
122 files
= get_output("git ls-files -m")
125 new_files
= self
._get
_new
_files
()
127 staged
= self
._get
_staged
_files
()
130 new_files
= set(new_files
).difference(staged
)
131 new_files
= list(new_files
)
133 unmerged
= self
._get
_unmerged
_files
()
135 unmerged
= set(unmerged
)
136 files
= set(files
).difference(unmerged
)
140 def _get_unmerged_files(self
):
142 cdup
= get_output("git rev-parse --show-cdup")
146 files
= get_output("git ls-files -u")
148 files
= [ x
.replace('\t', ' ').split(' ')[3] for x
in files
]
149 return list(set(files
))
151 def _resolve_rev(self
, rev
):
152 ref
= get_output("git rev-parse --verify %s 2>/dev/null" % rev
)
154 raise YapError("No such revision: %s" % rev
)
157 def _delete_branch(self
, branch
, force
):
158 current
= get_output("git symbolic-ref HEAD")
160 current
= current
[0].replace('refs/heads/', '')
161 if branch
== current
:
162 raise YapError("Can't delete current branch")
164 ref
= self
._resolve
_rev
('refs/heads/'+branch
)
165 run_safely("git update-ref -d 'refs/heads/%s' '%s'" % (branch
, ref
))
168 name
= get_output("git name-rev --name-only '%s'" % ref
)[0]
169 if name
== 'undefined':
170 run_command("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
))
171 raise YapError("Refusing to delete leaf branch (use -f to force)")
172 def _get_pager_cmd(self
):
173 if 'YAP_PAGER' in os
.environ
:
174 return os
.environ
['YAP_PAGER']
175 elif 'GIT_PAGER' in os
.environ
:
176 return os
.environ
['GIT_PAGER']
177 elif 'PAGER' in os
.environ
:
178 return os
.environ
['PAGER']
182 def _add_one(self
, file):
183 self
._assert
_file
_exists
(file)
184 x
= get_output("git ls-files '%s'" % file)
186 raise YapError("File '%s' already in repository" % file)
187 self
._add
_new
_file
(file)
189 def _rm_one(self
, file):
190 self
._assert
_file
_exists
(file)
191 if get_output("git ls-files '%s'" % file) != []:
192 run_safely("git rm --cached '%s'" % file)
193 self
._remove
_new
_file
(file)
195 def _stage_one(self
, file, allow_unmerged
=False):
196 self
._assert
_file
_exists
(file)
197 prefix
= get_output("git rev-parse --show-prefix")
199 tmp
= os
.path
.normpath(os
.path
.join(prefix
[0], file))
202 if not allow_unmerged
and tmp
in self
._get
_unmerged
_files
():
203 raise YapError("Refusing to stage conflicted file: %s" % file)
204 run_safely("git update-index --add '%s'" % file)
206 def _unstage_one(self
, file):
207 self
._assert
_file
_exists
(file)
208 if run_command("git rev-parse HEAD"):
209 rc
= run_command("git update-index --force-remove '%s'" % file)
211 cdup
= get_output("git rev-parse --show-cdup")
218 rc
= run_command("git diff-index --cached -p HEAD '%s' | (cd %s; git apply -R --cached)" % (file, cdup
))
220 raise YapError("Failed to unstage")
222 def _revert_one(self
, file):
223 self
._assert
_file
_exists
(file)
225 self
._unstage
_one
(file)
228 run_safely("git checkout-index -u -f '%s'" % file)
230 def _parse_commit(self
, commit
):
231 lines
= get_output("git cat-file commit '%s'" % commit
)
236 if mode
!= 'commit' and l
.strip() == "":
241 commit
['log'].append(l
)
248 commit
['log'] = '\n'.join(commit
['log'])
251 def _check_commit(self
, **flags
):
252 if '-a' in flags
and '-d' in flags
:
253 raise YapError("Conflicting flags: -a and -d")
255 if '-d' not in flags
and self
._get
_unstaged
_files
():
256 if '-a' not in flags
and self
._get
_staged
_files
():
257 raise YapError("Staged and unstaged changes present. Specify what to commit")
258 os
.system("git diff-files -p | git apply --cached")
259 for f
in self
._get
_new
_files
():
262 def _do_uncommit(self
):
263 commit
= self
._parse
_commit
("HEAD")
264 repo
= get_output('git rev-parse --git-dir')[0]
265 dir = os
.path
.join(repo
, 'yap')
270 msg_file
= os
.path
.join(dir, 'msg')
271 fd
= file(msg_file
, 'w')
272 print >>fd
, commit
['log']
275 tree
= get_output("git rev-parse --verify HEAD^")
276 run_safely("git update-ref -m uncommit HEAD '%s'" % tree
[0])
278 def _do_commit(self
, msg
=None):
279 tree
= get_output("git write-tree")[0]
281 repo
= get_output('git rev-parse --git-dir')[0]
282 head_file
= os
.path
.join(repo
, 'yap', 'merge')
284 parent
= pickle
.load(file(head_file
))
286 parent
= get_output("git rev-parse --verify HEAD 2> /dev/null")
288 if os
.environ
.has_key('YAP_EDITOR'):
289 editor
= os
.environ
['YAP_EDITOR']
290 elif os
.environ
.has_key('GIT_EDITOR'):
291 editor
= os
.environ
['GIT_EDITOR']
292 elif os
.environ
.has_key('EDITOR'):
293 editor
= os
.environ
['EDITOR']
297 fd
, tmpfile
= tempfile
.mkstemp("yap")
301 msg_file
= os
.path
.join(repo
, 'yap', 'msg')
302 if os
.access(msg_file
, os
.R_OK
):
304 fd2
= file(tmpfile
, 'w')
305 for l
in fd1
.xreadlines():
306 print >>fd2
, l
.strip()
309 if os
.system("%s '%s'" % (editor
, tmpfile
)) != 0:
310 raise YapError("Editing commit message failed")
317 raise YapError("Refusing to use empty commit message")
319 fd
= os
.popen("git stripspace > %s" % tmpfile
, 'w')
324 parent
= ' -p '.join(parent
)
325 commit
= get_output("git commit-tree '%s' -p %s < '%s'" % (tree
, parent
, tmpfile
))
327 commit
= get_output("git commit-tree '%s' < '%s'" % (tree
, tmpfile
))
330 run_safely("git update-ref HEAD '%s'" % commit
[0])
333 def _check_rebasing(self
):
334 repo
= get_output('git rev-parse --git-dir')[0]
335 dotest
= os
.path
.join(repo
, '.dotest')
336 if os
.access(dotest
, os
.R_OK
):
337 raise YapError("A git operation is in progress. Complete it first")
338 dotest
= os
.path
.join(repo
, '..', '.dotest')
339 if os
.access(dotest
, os
.R_OK
):
340 raise YapError("A git operation is in progress. Complete it first")
342 def _check_git(self
):
343 if run_command("git rev-parse --git-dir"):
344 raise YapError("That command must be run from inside a git repository")
346 def _list_remotes(self
):
347 remotes
= get_output("git config --get-regexp '^remote.*.url'")
349 remote
, url
= x
.split(' ')
350 remote
= remote
.replace('remote.', '')
351 remote
= remote
.replace('.url', '')
354 def _unstage_all(self
):
356 run_safely("git read-tree -m HEAD")
358 run_safely("git read-tree HEAD")
359 run_safely("git update-index -q --refresh")
361 def _get_tracking(self
, current
):
362 remote
= get_output("git config branch.%s.remote" % current
)
364 raise YapError("No tracking branch configured for '%s'" % current
)
366 merge
= get_output("git config branch.%s.merge" % current
)
368 raise YapError("No tracking branch configured for '%s'" % current
)
369 return remote
[0], merge
[0]
371 def _confirm_push(self
, current
, rhs
, repo
):
372 print "About to push local branch '%s' to '%s' on '%s'" % (current
, rhs
, repo
)
373 print "Continue (y/n)? ",
375 ans
= sys
.stdin
.readline().strip()
377 if ans
.lower() != 'y' and ans
.lower() != 'yes':
378 raise YapError("Aborted.")
380 def _clear_state(self
):
381 repo
= get_output('git rev-parse --git-dir')[0]
382 dir = os
.path
.join(repo
, 'yap')
383 for f
in "new-files", "merge", "msg":
385 os
.unlink(os
.path
.join(dir, f
))
389 def _get_attr(self
, name
, attr
):
391 for c
in self
.__class
__.__bases
__:
393 m2
= c
.__dict
__[name
]
397 val
= m2
.__getattribute__(attr
)
398 except AttributeError:
402 def _filter_log(self
, commit
):
405 def _check_rename(self
, rev
, path
):
406 renames
= get_output("git diff-tree -C -M --diff-filter=R %s %s^"
409 r
= r
.replace('\t', ' ')
410 fields
= r
.split(' ')
411 mode1
, mode2
, hash1
, hash2
, rename
, dst
, src
= fields
416 @short_help("make a local copy of an existing repository")
418 The first argument is a URL to the existing repository. This can be an
419 absolute path if the repository is local, or a URL with the git://,
420 ssh://, or http:// schemes. By default, the directory used is the last
421 component of the URL, sans '.git'. This can be overridden by providing
424 def cmd_clone(self
, url
, directory
=None):
427 if '://' not in url
and url
[0] != '/':
428 url
= os
.path
.join(os
.getcwd(), url
)
430 url
= url
.rstrip('/')
431 if directory
is None:
432 directory
= url
.rsplit('/')[-1]
433 directory
= directory
.replace('.git', '')
438 raise YapError("Directory exists: %s" % directory
)
441 self
.cmd_repo("origin", url
)
442 self
.cmd_fetch("origin")
445 if not run_command("git rev-parse --verify refs/remotes/origin/HEAD"):
446 hash = get_output("git rev-parse refs/remotes/origin/HEAD")[0]
447 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin'"):
448 if get_output("git rev-parse %s" % b
)[0] == hash:
452 if not run_command("git rev-parse --verify refs/remotes/origin/master"):
453 branch
= "refs/remotes/origin/master"
455 branch
= get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin'")
458 hash = get_output("git rev-parse %s" % branch
)
460 branch
= branch
.replace('refs/remotes/origin/', '')
461 run_safely("git update-ref refs/heads/%s %s" % (branch
, hash[0]))
462 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
463 self
.cmd_revert(**{'-a': 1})
465 @short_help("turn a directory into a repository")
467 Converts the current working directory into a repository. The primary
468 side-effect of this command is the creation of a '.git' subdirectory.
469 No files are added nor commits made.
472 os
.system("git init")
474 @short_help("add a new file to the repository")
476 The arguments are the files to be added to the repository. Once added,
477 the files will show as "unstaged changes" in the output of 'status'. To
478 reverse the effects of this command, see 'rm'.
480 def cmd_add(self
, *files
):
491 @short_help("delete a file from the repository")
493 The arguments are the files to be removed from the current revision of
494 the repository. The files will still exist in any past commits that the
495 files may have been a part of. The file is not actually deleted, it is
496 just no longer tracked as part of the repository.
498 def cmd_rm(self
, *files
):
508 @short_help("stage changes in a file for commit")
510 The arguments are the files to be staged. Staging changes is a way to
511 build up a commit when you do not want to commit all changes at once.
512 To commit only staged changes, use the '-d' flag to 'commit.' To
513 reverse the effects of this command, see 'unstage'. Once staged, the
514 files will show as "staged changes" in the output of 'status'.
516 def cmd_stage(self
, *files
):
526 @short_help("unstage changes in a file")
528 The arguments are the files to be unstaged. Once unstaged, the files
529 will show as "unstaged changes" in the output of 'status'. The '-a'
530 flag can be used to unstage all staged changes at once.
533 def cmd_unstage(self
, *files
, **flags
):
537 files
= self
._get
_staged
_files
()
540 raise YapError("Nothing to do")
546 @short_help("show files with staged and unstaged changes")
548 Show the files in the repository with changes since the last commit,
549 categorized based on whether the changes are staged or not. A file may
550 appear under each heading if the same file has both staged and unstaged
553 def cmd_status(self
):
556 branch
= get_output("git symbolic-ref HEAD")
558 branch
= branch
[0].replace('refs/heads/', '')
561 print "Current branch: %s" % branch
563 print "Files with staged changes:"
564 files
= self
._get
_staged
_files
()
566 print "\t%s" % self
._repo
_path
_to
_rel
(f
)
570 print "Files with unstaged changes:"
571 files
= self
._get
_unstaged
_files
()
573 print "\t%s" % self
._repo
_path
_to
_rel
(f
)
577 files
= self
._get
_unmerged
_files
()
579 print "Files with conflicts:"
581 print "\t%s" % self
._repo
_path
_to
_rel
(f
)
583 @short_help("remove uncommitted changes from a file (*)")
585 The arguments are the files whose changes will be reverted. If the '-a'
586 flag is given, then all files will have uncommitted changes removed.
587 Note that there is no way to reverse this command short of manually
588 editing each file again.
591 def cmd_revert(self
, *files
, **flags
):
596 run_safely("git checkout-index -u -f -a")
608 @short_help("record changes to files as a new commit")
610 Create a new commit recording changes since the last commit. If there
611 are only unstaged changes, those will be recorded. If there are only
612 staged changes, those will be recorded. Otherwise, you will have to
613 specify either the '-a' flag or the '-d' flag to commit all changes or
614 only staged changes, respectively. To reverse the effects of this
615 command, see 'uncommit'.
617 @takes_options("adm:")
618 def cmd_commit(self
, **flags
):
619 "[-a | -d] [-m <msg>]"
621 self
._check
_rebasing
()
622 self
._check
_commit
(**flags
)
623 if not self
._get
_staged
_files
():
624 raise YapError("No changes to commit")
625 msg
= flags
.get('-m', None)
629 @short_help("reverse the actions of the last commit")
631 Reverse the effects of the last 'commit' operation. The changes that
632 were part of the previous commit will show as "staged changes" in the
633 output of 'status'. This means that if no files were changed since the
634 last commit was created, 'uncommit' followed by 'commit' is a lossless
637 def cmd_uncommit(self
):
643 @short_help("report the current version of yap")
644 def cmd_version(self
):
645 print "Yap version %s" % self
.version
647 @short_help("show the changelog for particular versions or files")
649 The arguments are the files with which to filter history. If none are
650 given, all changes are listed. Otherwise only commits that affected one
651 or more of the given files are listed. The -r option changes the
652 starting revision for traversing history. By default, history is listed
655 @takes_options("pr:")
656 def cmd_log(self
, *paths
, **flags
):
657 "[-p] [-r <rev>] <path>..."
659 rev
= flags
.get('-r', 'HEAD')
660 rev
= self
._resolve
_rev
(rev
)
667 pager
= os
.popen(self
._get
_pager
_cmd
(), 'w')
670 for hash in yield_output("git rev-list '%s' -- %s"
671 % (rev
, ' '.join(paths
))):
672 commit
= get_output("git show -M -C %s %s"
673 % (flags
.get('-p', '--name-status'), hash),
675 commit
= self
._filter
_log
(commit
)
676 print >>pager
, ''.join(commit
)
680 src
= self
._check
_rename
(hash, paths
[0])
689 except (IOError, KeyboardInterrupt):
692 @short_help("show staged, unstaged, or all uncommitted changes")
694 Show staged, unstaged, or all uncommitted changes. By default, all
695 changes are shown. The '-u' flag causes only unstaged changes to be
696 shown. The '-d' flag causes only staged changes to be shown.
699 def cmd_diff(self
, **flags
):
702 if '-u' in flags
and '-d' in flags
:
703 raise YapError("Conflicting flags: -u and -d")
705 pager
= self
._get
_pager
_cmd
()
708 os
.system("git diff-files -p | %s" % pager
)
710 os
.system("git diff-index --cached -p HEAD | %s" % pager
)
712 os
.system("git diff-index -p HEAD | %s" % pager
)
714 @short_help("list, create, or delete branches")
716 If no arguments are specified, a list of local branches is given. The
717 current branch is indicated by a "*" next to the name. If an argument
718 is given, it is taken as the name of a new branch to create. The branch
719 will start pointing at the current HEAD. See 'point' for details on
720 changing the revision of the new branch. Note that this command does
721 not switch the current working branch. See 'switch' for details on
722 changing the current working branch.
724 The '-d' flag can be used to delete local branches. If the delete
725 operation would remove the last branch reference to a given line of
726 history (colloquially referred to as "dangling commits"), yap will
727 report an error and abort. The '-f' flag can be used to force the delete
730 @takes_options("fd:")
731 def cmd_branch(self
, branch
=None, **flags
):
732 "[ [-f] -d <branch> | <branch> ]"
734 force
= '-f' in flags
736 self
._delete
_branch
(flags
['-d'], force
)
740 if branch
is not None:
741 ref
= get_output("git rev-parse --verify HEAD")
743 raise YapError("No branch point yet. Make a commit")
744 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
746 current
= get_output("git symbolic-ref HEAD")
747 branches
= get_output("git for-each-ref --format='%(refname)' 'refs/heads'")
749 if current
and b
== current
[0]:
753 b
= b
.replace('refs/heads/', '')
756 @short_help("change the current working branch")
758 The argument is the name of the branch to make the current working
759 branch. This command will fail if there are uncommitted changes to any
760 files. Otherwise, the contents of the files in the working directory
761 are updated to reflect their state in the new branch. Additionally, any
762 future commits are added to the new branch instead of the previous line
766 def cmd_switch(self
, branch
, **flags
):
769 self
._check
_rebasing
()
770 ref
= self
._resolve
_rev
('refs/heads/'+branch
)
772 if '-f' not in flags
:
773 if (self
._get
_staged
_files
()
774 or (self
._get
_unstaged
_files
()
775 and run_command("git update-index --refresh"))):
776 raise YapError("You have uncommitted changes. Use -f to continue anyway")
778 if self
._get
_unstaged
_files
() and self
._get
_staged
_files
():
779 raise YapError("You have staged and unstaged changes. Perhaps unstage -a?")
781 staged
= bool(self
._get
_staged
_files
())
783 run_command("git diff-files -p | git apply --cached")
784 for f
in self
._get
_new
_files
():
787 idx
= get_output("git write-tree")
788 new
= self
._resolve
_rev
('refs/heads/'+branch
)
790 run_command("git update-index --refresh")
791 readtree
= "git read-tree -v --aggressive -u -m HEAD %s %s" % (idx
[0], new
)
792 if os
.system(readtree
):
793 raise YapError("Failed to switch")
794 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
796 if '-f' not in flags
:
803 @short_help("move the current branch to a different revision")
805 The argument is the hash of the commit to which the current branch
806 should point, or alternately a branch or tag (a.k.a, "committish"). If
807 moving the branch would create "dangling commits" (see 'branch'), yap
808 will report an error and abort. The '-f' flag can be used to force the
809 operation in spite of this.
812 def cmd_point(self
, where
, **flags
):
815 self
._check
_rebasing
()
817 head
= get_output("git rev-parse --verify HEAD")
819 raise YapError("No commit yet; nowhere to point")
821 ref
= self
._resolve
_rev
(where
)
822 ref
= get_output("git rev-parse --verify '%s^{commit}'" % ref
)
824 raise YapError("Not a commit: %s" % where
)
826 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
827 raise YapError("You have uncommitted changes. Commit them first")
829 run_safely("git update-ref HEAD '%s'" % ref
[0])
831 if '-f' not in flags
:
832 name
= get_output("git name-rev --name-only '%s'" % head
[0])[0]
833 if name
== "undefined":
834 os
.system("git update-ref HEAD '%s'" % head
[0])
835 raise YapError("Pointing there will lose commits. Use -f to force")
837 run_command("git update-index --refresh")
838 rc
= os
.system("git read-tree -v --reset -u HEAD")
840 raise YapError("checkout-index failed")
843 @short_help("alter history by dropping or amending commits")
845 This command operates in two distinct modes, "amend" and "drop" mode.
846 In drop mode, the given commit is removed from the history of the
847 current branch, as though that commit never happened. By default the
850 In amend mode, the uncommitted changes present are merged into a
851 previous commit. This is useful for correcting typos or adding missed
852 files into past commits. By default the commit used is HEAD.
854 While rewriting history it is possible that conflicts will arise. If
855 this happens, the rewrite will pause and you will be prompted to resolve
856 the conflicts and stage them. Once that is done, you will run "yap
857 history continue." If instead you want the conflicting commit removed
858 from history (perhaps your changes supercede that commit) you can run
859 "yap history skip". Once the rewrite completes, your branch will be on
860 the same commit as when the rewrite started.
862 def cmd_history(self
, subcmd
, *args
):
863 "amend | drop <commit>"
866 if subcmd
not in ("amend", "drop", "continue", "skip"):
870 When you have resolved the conflicts run \"yap history continue\".
871 To skip the problematic patch, run \"yap history skip\"."""
873 if subcmd
== "continue":
874 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
877 os
.system("git reset --hard")
878 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
881 if subcmd
== "amend":
882 flags
, args
= getopt
.getopt(args
, "ad")
892 self
._resolve
_rev
(commit
)
893 self
._check
_rebasing
()
895 if subcmd
== "amend":
896 self
._check
_commit
(**flags
)
897 if self
._get
_unstaged
_files
():
898 # XXX: handle unstaged changes better
899 raise YapError("Commit away changes that you aren't amending")
903 start
= get_output("git rev-parse HEAD")
904 stash
= get_output("git stash create")
905 run_command("git reset --hard")
907 fd
, tmpfile
= tempfile
.mkstemp("yap")
911 os
.system("git format-patch -k --stdout '%s' > %s" % (commit
, tmpfile
))
912 if subcmd
== "amend":
913 self
.cmd_point(commit
, **{'-f': True})
915 if subcmd
== "amend":
917 rc
= os
.system("git stash apply %s" % stash
[0])
919 self
.cmd_point(start
[0], **{'-f': True})
920 os
.system("git stash apply %s" % stash
[0])
921 raise YapError("Failed to apply stash")
924 if subcmd
== "amend":
926 self
._check
_commit
(**{'-a': True})
929 self
.cmd_point("%s^" % commit
, **{'-f': True})
931 stat
= os
.stat(tmpfile
)
934 run_safely("git update-index --refresh")
935 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
937 raise YapError("Failed to apply changes")
942 run_command("git stash apply %s" % stash
[0])
945 @short_help("show the changes introduced by a given commit")
947 By default, the changes in the last commit are shown. To override this,
948 specify a hash, branch, or tag (committish). The hash of the commit,
949 the commit's author, log message, and a diff of the changes are shown.
951 def cmd_show(self
, commit
="HEAD"):
954 commit
= self
._resolve
_rev
(commit
)
955 os
.system("git show '%s'" % commit
)
957 @short_help("apply the changes in a given commit to the current branch")
959 The argument is the hash, branch, or tag (committish) of the commit to
960 be applied. In general, it only makes sense to apply commits that
961 happened on another branch. The '-r' flag can be used to have the
962 changes in the given commit reversed from the current branch. In
963 general, this only makes sense for commits that happened on the current
967 def cmd_cherry_pick(self
, commit
, **flags
):
970 commit
= self
._resolve
_rev
(commit
)
972 os
.system("git revert '%s'" % commit
)
974 os
.system("git cherry-pick '%s'" % commit
)
976 @short_help("list, add, or delete configured remote repositories")
978 When invoked with no arguments, this command will show the list of
979 currently configured remote repositories, giving both the name and URL
980 of each. To add a new repository, give the desired name as the first
981 argument and the URL as the second. The '-d' flag can be used to remove
982 a previously added repository.
985 def cmd_repo(self
, name
=None, url
=None, **flags
):
986 "[<name> <url> | -d <name>]"
988 if name
is not None and url
is None:
992 if flags
['-d'] not in [ x
[0] for x
in self
._list
_remotes
() ]:
993 raise YapError("No such repository: %s" % flags
['-d'])
994 os
.system("git config --unset remote.%s.url" % flags
['-d'])
995 os
.system("git config --unset remote.%s.fetch" % flags
['-d'])
996 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin'"):
997 hash = get_output("git rev-parse %s" % b
)
999 run_safely("git update-ref -d %s %s" % (b
, hash[0]))
1002 if name
in [ x
[0] for x
in self
._list
_remotes
() ]:
1003 raise YapError("Repository '%s' already exists" % name
)
1004 os
.system("git config remote.%s.url %s" % (name
, url
))
1005 os
.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name
, name
))
1007 for remote
, url
in self
._list
_remotes
():
1009 print " URL: %s" % url
1011 for b
in get_output("git for-each-ref --format='%%(refname)' 'refs/remotes/%s'" % remote
):
1012 b
= b
.replace('refs/remotes/', '')
1014 branches
= "Branches: "
1017 print " %s%s" % (branches
, b
)
1020 @short_help("send local commits to a remote repository (*)")
1022 When invoked with no arguments, the current branch is synchronized to
1023 the tracking branch of the tracking remote. If no tracking remote is
1024 specified, the repository will have to be specified on the command line.
1025 In that case, the default is to push to a branch with the same name as
1026 the current branch. This behavior can be overridden by giving a second
1027 argument to specify the remote branch.
1029 If the remote branch does not currently exist, the command will abort
1030 unless the -c flag is provided. If the remote branch is not a direct
1031 descendent of the local branch, the command will abort unless the -f
1032 flag is provided. Forcing a push in this way can be problematic to
1033 other users of the repository if they are not expecting it.
1035 To delete a branch on the remote repository, use the -d flag.
1037 @takes_options("cdf")
1038 def cmd_push(self
, repo
=None, rhs
=None, **flags
):
1041 if '-c' in flags
and '-d' in flags
:
1044 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1045 raise YapError("No such repository: %s" % repo
)
1047 current
= get_output("git symbolic-ref HEAD")
1049 raise YapError("Not on a branch!")
1051 self
._check
_rebasing
()
1053 current
= current
[0].replace('refs/heads/', '')
1054 remote
= get_output("git config branch.%s.remote" % current
)
1055 if repo
is None and remote
:
1059 raise YapError("No tracking branch configured; specify destination repository")
1061 if rhs
is None and remote
and remote
[0] == repo
:
1062 merge
= get_output("git config branch.%s.merge" % current
)
1067 rhs
= "refs/heads/%s" % current
1069 if '-c' not in flags
and '-d' not in flags
:
1070 if run_command("git rev-parse --verify refs/remotes/%s/%s"
1071 % (repo
, rhs
.replace('refs/heads/', ''))):
1072 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
1073 if '-f' not in flags
:
1074 hash = get_output("git rev-parse refs/remotes/%s/%s" % (repo
, rhs
.replace('refs/heads/', '')))
1075 base
= get_output("git merge-base HEAD %s" % hash[0])
1077 if base
[0] != hash[0]:
1078 raise YapError("Branch not up-to-date with remote. Update or use -f")
1080 self
._confirm
_push
(current
, rhs
, repo
)
1087 lhs
= "refs/heads/%s" % current
1088 rc
= os
.system("git push %s %s %s:%s" % (flags
.get('-f', ''), repo
, lhs
, rhs
))
1090 raise YapError("Push failed.")
1092 @short_help("retrieve commits from a remote repository")
1094 When run with no arguments, the command will retrieve new commits from
1095 the remote tracking repository. Note that this does not in any way
1096 alter the current branch. For that, see "update". If a remote other
1097 than the tracking remote is desired, it can be specified as the first
1100 def cmd_fetch(self
, repo
=None):
1103 current
= get_output("git symbolic-ref HEAD")
1105 raise YapError("Not on a branch!")
1107 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1108 raise YapError("No such repository: %s" % repo
)
1110 current
= current
[0].replace('refs/heads/', '')
1111 remote
= get_output("git config branch.%s.remote" % current
)
1115 raise YapError("No tracking branch configured; specify a repository")
1116 os
.system("git fetch %s" % repo
)
1118 @short_help("update the current branch relative to its tracking branch")
1120 Updates the current branch relative to its remote tracking branch. This
1121 command requires that the current branch have a remote tracking branch
1122 configured. If any conflicts occur while applying your changes to the
1123 updated remote, the command will pause to allow you to fix them. Once
1124 that is done, run "update" with the "continue" subcommand. Alternately,
1125 the "skip" subcommand can be used to discard the conflicting changes.
1127 def cmd_update(self
, subcmd
=None):
1130 if subcmd
and subcmd
not in ["continue", "skip"]:
1134 When you have resolved the conflicts run \"yap update continue\".
1135 To skip the problematic patch, run \"yap update skip\"."""
1137 if subcmd
== "continue":
1138 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
1140 if subcmd
== "skip":
1141 os
.system("git reset --hard")
1142 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
1145 self
._check
_rebasing
()
1146 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
1147 raise YapError("You have uncommitted changes. Commit them first")
1149 current
= get_output("git symbolic-ref HEAD")
1151 raise YapError("Not on a branch!")
1153 current
= current
[0].replace('refs/heads/', '')
1154 remote
, merge
= self
._get
_tracking
(current
)
1155 merge
= merge
.replace('refs/heads/', '')
1157 self
.cmd_fetch(remote
)
1158 base
= get_output("git merge-base HEAD refs/remotes/%s/%s" % (remote
, merge
))
1161 fd
, tmpfile
= tempfile
.mkstemp("yap")
1163 os
.system("git format-patch -k --stdout '%s' > %s" % (base
[0], tmpfile
))
1164 self
.cmd_point("refs/remotes/%s/%s" % (remote
, merge
), **{'-f': True})
1166 stat
= os
.stat(tmpfile
)
1169 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
1171 raise YapError("Failed to apply changes")
1175 @short_help("query and configure remote branch tracking")
1177 When invoked with no arguments, the command displays the tracking
1178 information for the current branch. To configure the tracking
1179 information, two arguments for the remote repository and remote branch
1180 are given. The tracking information is used to provide defaults for
1181 where to push local changes and from where to get updates to the branch.
1183 def cmd_track(self
, repo
=None, branch
=None):
1187 current
= get_output("git symbolic-ref HEAD")
1189 raise YapError("Not on a branch!")
1190 current
= current
[0].replace('refs/heads/', '')
1192 if repo
is None and branch
is None:
1193 repo
, merge
= self
._get
_tracking
(current
)
1194 merge
= merge
.replace('refs/heads/', '')
1195 print "Branch '%s' tracking refs/remotes/%s/%s" % (current
, repo
, merge
)
1198 if repo
is None or branch
is None:
1201 if repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1202 raise YapError("No such repository: %s" % repo
)
1204 if run_command("git rev-parse --verify refs/remotes/%s/%s" % (repo
, branch
)):
1205 raise YapError("No such branch '%s' on repository '%s'" % (branch
, repo
))
1207 os
.system("git config branch.%s.remote '%s'" % (current
, repo
))
1208 os
.system("git config branch.%s.merge 'refs/heads/%s'" % (current
, branch
))
1209 print "Branch '%s' now tracking refs/remotes/%s/%s" % (current
, repo
, branch
)
1211 @short_help("mark files with conflicts as resolved")
1213 The arguments are the files to be marked resolved. When a conflict
1214 occurs while merging changes to a file, that file is marked as
1215 "unmerged." Until the file(s) with conflicts are marked resolved,
1216 commits cannot be made.
1218 def cmd_resolved(self
, *files
):
1225 self
._stage
_one
(f
, True)
1228 @short_help("merge a branch into the current branch")
1229 def cmd_merge(self
, branch
):
1233 branch_name
= branch
1234 branch
= self
._resolve
_rev
(branch
)
1235 base
= get_output("git merge-base HEAD %s" % branch
)
1237 raise YapError("Branch '%s' is not a fork of the current branch"
1240 readtree
= ("git read-tree --aggressive -u -m %s HEAD %s"
1241 % (base
[0], branch
))
1242 if run_command(readtree
):
1243 run_command("git update-index --refresh")
1244 if os
.system(readtree
):
1245 raise YapError("Failed to merge")
1247 repo
= get_output('git rev-parse --git-dir')[0]
1248 dir = os
.path
.join(repo
, 'yap')
1253 msg_file
= os
.path
.join(dir, 'msg')
1254 msg
= file(msg_file
, 'w')
1255 print >>msg
, "Merge branch '%s'" % branch_name
1258 head
= get_output("git rev-parse --verify HEAD")
1260 heads
= [head
[0], branch
]
1261 head_file
= os
.path
.join(dir, 'merge')
1262 pickle
.dump(heads
, file(head_file
, 'w'))
1264 self
._merge
_index
(branch
, base
[0])
1265 if self
._get
_unmerged
_files
():
1267 raise YapError("Fix conflicts then commit")
1271 def _merge_index(self
, branch
, base
):
1272 for f
in self
._get
_unmerged
_files
():
1273 fd
, bfile
= tempfile
.mkstemp("yap")
1275 rc
= os
.system("git show %s:%s > %s" % (base
, f
, bfile
))
1278 fd
, ofile
= tempfile
.mkstemp("yap")
1280 rc
= os
.system("git show %s:%s > %s" % (branch
, f
, ofile
))
1283 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
)
1284 rc
= os
.system(command
)
1290 self
._stage
_one
(f
, True)
1292 def cmd_help(self
, cmd
=None):
1295 cmd
= "cmd_" + cmd
.replace('-', '_')
1297 attr
= self
.__getattribute
__(cmd
)
1298 except AttributeError:
1299 raise YapError("No such command: %s" % cmd
)
1301 help = self
._get
_attr
(cmd
, "long_help")
1303 raise YapError("Sorry, no help for '%s'. Ask Steven." % oldcmd
)
1305 print >>sys
.stderr
, "The '%s' command" % oldcmd
1306 doc
= self
._get
_attr
(cmd
, "__doc__")
1309 print >>sys
.stderr
, "\tyap %s %s" % (oldcmd
, doc
)
1310 print >>sys
.stderr
, "%s" % help
1313 print >> sys
.stderr
, "Yet Another (Git) Porcelein"
1316 for name
in dir(self
):
1317 if not name
.startswith('cmd_'):
1319 attr
= self
.__getattribute
__(name
)
1320 if not callable(attr
):
1323 short_msg
= self
._get
_attr
(name
, "short_help")
1324 if short_msg
is None:
1327 name
= name
.replace('cmd_', '')
1328 name
= name
.replace('_', '-')
1329 print >> sys
.stderr
, "%-16s%s" % (name
, short_msg
)
1332 print >> sys
.stderr
, "(*) Indicates that the command is not readily reversible"
1334 @short_help("show information about loaded plugins")
1335 def cmd_plugins(self
):
1337 print >> sys
.stderr
, "Loaded plugins:"
1338 plugins
= load_plugins()
1339 for name
, cls
in plugins
.items():
1340 print "\t%-16s: %s" % (name
, cls
.__doc
__)
1342 print "\t%-16s" % "None"
1344 def cmd_usage(self
):
1345 print >> sys
.stderr
, "usage: %s <command>" % os
.path
.basename(sys
.argv
[0])
1346 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"
1349 plugindir
= os
.path
.join("~", ".yap", "plugins")
1350 plugindir
= os
.path
.expanduser(plugindir
)
1351 plugindir
= os
.path
.join(plugindir
, "*.py")
1354 for p
in glob
.glob(os
.path
.expanduser(plugindir
)):
1355 plugin
= os
.path
.basename(p
).replace('.py', '')
1356 m
= __import__(plugin
)
1359 if not type(cls
) == type:
1361 if not issubclass(cls
, YapCore
):
1368 def yap_metaclass(name
, bases
, dct
):
1369 plugindir
= os
.path
.join("~", ".yap", "plugins")
1370 plugindir
= os
.path
.expanduser(plugindir
)
1371 sys
.path
.insert(0, plugindir
)
1373 plugins
= set(load_plugins().values())
1376 p2
-= set(cls
.__bases
__)
1378 bases
= list(plugins
) + list(bases
)
1379 return type(name
, tuple(bases
), dct
)
1382 __metaclass__
= yap_metaclass
1384 def main(self
, args
):
1392 if run_command("git --version"):
1393 print >>sys
.stderr
, "Failed to run git; is it installed?"
1396 debug
= os
.getenv('YAP_DEBUG')
1399 command
= command
.replace('-', '_')
1400 meth
= self
.__getattribute
__("cmd_"+command
)
1401 doc
= self
._get
_attr
("cmd_"+command
, "__doc__")
1405 for c
in self
.__class
__.__bases
__:
1407 t
= c
.__dict
__["cmd_"+command
]
1410 if "options" in t
.__dict
__:
1411 options
+= t
.options
1415 flags
, args
= getopt
.getopt(args
, options
)
1417 except getopt
.GetoptError
, e
:
1420 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1426 meth(*args
, **flags
)
1427 except (TypeError, getopt
.GetoptError
):
1430 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1434 print >> sys
.stderr
, e
1436 except AttributeError: