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
= self
._get
_cdup
()
120 files
= get_output("git ls-files -m")
123 new_files
= self
._get
_new
_files
()
125 staged
= self
._get
_staged
_files
()
128 new_files
= set(new_files
).difference(staged
)
129 new_files
= list(new_files
)
131 unmerged
= self
._get
_unmerged
_files
()
133 unmerged
= set(unmerged
)
134 files
= set(files
).difference(unmerged
)
138 def _get_unmerged_files(self
):
140 cdup
= self
._get
_cdup
()
142 files
= get_output("git ls-files -u")
144 files
= [ x
.replace('\t', ' ').split(' ')[3] for x
in files
]
145 return list(set(files
))
147 def _resolve_rev(self
, rev
):
148 ref
= get_output("git rev-parse --verify %s 2>/dev/null" % rev
)
150 raise YapError("No such revision: %s" % rev
)
153 def _delete_branch(self
, branch
, force
):
154 current
= get_output("git symbolic-ref HEAD")
156 current
= current
[0].replace('refs/heads/', '')
157 if branch
== current
:
158 raise YapError("Can't delete current branch")
160 ref
= self
._resolve
_rev
('refs/heads/'+branch
)
161 run_safely("git update-ref -d 'refs/heads/%s' '%s'" % (branch
, ref
))
164 name
= get_output("git name-rev --name-only '%s'" % ref
)[0]
165 if name
== 'undefined':
166 run_command("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
))
167 raise YapError("Refusing to delete leaf branch (use -f to force)")
168 def _get_pager_cmd(self
):
169 if 'YAP_PAGER' in os
.environ
:
170 return os
.environ
['YAP_PAGER']
171 elif 'GIT_PAGER' in os
.environ
:
172 return os
.environ
['GIT_PAGER']
173 elif 'PAGER' in os
.environ
:
174 return os
.environ
['PAGER']
178 def _add_one(self
, file):
179 self
._assert
_file
_exists
(file)
180 x
= get_output("git ls-files '%s'" % file)
181 if x
!= [] or file in self
._get
_new
_files
():
182 raise YapError("File '%s' already in repository" % file)
183 self
._add
_new
_file
(file)
185 def _rm_one(self
, file):
186 self
._assert
_file
_exists
(file)
187 if get_output("git ls-files '%s'" % file) != []:
188 run_safely("git rm --cached '%s'" % file)
189 self
._remove
_new
_file
(file)
191 def _stage_one(self
, file, allow_unmerged
=False):
192 self
._assert
_file
_exists
(file)
193 prefix
= get_output("git rev-parse --show-prefix")
195 tmp
= os
.path
.normpath(os
.path
.join(prefix
[0], file))
198 if not allow_unmerged
and tmp
in self
._get
_unmerged
_files
():
199 raise YapError("Refusing to stage conflicted file: %s" % file)
200 run_safely("git update-index --add '%s'" % file)
203 cdup
= get_output("git rev-parse --show-cdup")
211 def _unstage_one(self
, file):
212 self
._assert
_file
_exists
(file)
213 if run_command("git rev-parse HEAD"):
214 rc
= run_command("git update-index --force-remove '%s'" % file)
216 cdup
= self
._get
_cdup
()
217 rc
= run_command("git diff-index --cached -p HEAD '%s' | (cd %s; git apply -R --cached)" % (file, cdup
))
219 raise YapError("Failed to unstage")
221 def _revert_one(self
, file):
222 self
._assert
_file
_exists
(file)
224 self
._unstage
_one
(file)
227 run_safely("git checkout-index -u -f '%s'" % file)
229 def _parse_commit(self
, commit
):
230 lines
= get_output("git cat-file commit '%s'" % commit
)
235 if mode
!= 'commit' and l
.strip() == "":
240 commit
['log'].append(l
)
247 commit
['log'] = '\n'.join(commit
['log'])
250 def _check_commit(self
, **flags
):
251 if '-a' in flags
and '-d' in flags
:
252 raise YapError("Conflicting flags: -a and -d")
254 if '-d' not in flags
and self
._get
_unstaged
_files
():
255 if '-a' not in flags
and self
._get
_staged
_files
():
256 raise YapError("Staged and unstaged changes present. Specify what to commit")
257 os
.system("git diff-files -p | git apply --cached")
258 for f
in self
._get
_new
_files
():
261 def _do_uncommit(self
):
262 commit
= self
._parse
_commit
("HEAD")
263 repo
= get_output('git rev-parse --git-dir')[0]
264 dir = os
.path
.join(repo
, 'yap')
269 msg_file
= os
.path
.join(dir, 'msg')
270 fd
= file(msg_file
, 'w')
271 print >>fd
, commit
['log']
274 tree
= get_output("git rev-parse --verify HEAD^")
275 run_safely("git update-ref -m uncommit HEAD '%s'" % tree
[0])
277 def _do_commit(self
, msg
=None):
278 tree
= get_output("git write-tree")[0]
280 repo
= get_output('git rev-parse --git-dir')[0]
281 head_file
= os
.path
.join(repo
, 'yap', 'merge')
283 parent
= pickle
.load(file(head_file
))
285 parent
= get_output("git rev-parse --verify HEAD 2> /dev/null")
287 if os
.environ
.has_key('YAP_EDITOR'):
288 editor
= os
.environ
['YAP_EDITOR']
289 elif os
.environ
.has_key('GIT_EDITOR'):
290 editor
= os
.environ
['GIT_EDITOR']
291 elif os
.environ
.has_key('EDITOR'):
292 editor
= os
.environ
['EDITOR']
296 fd
, tmpfile
= tempfile
.mkstemp("yap")
300 msg_file
= os
.path
.join(repo
, 'yap', 'msg')
301 if os
.access(msg_file
, os
.R_OK
):
303 fd2
= file(tmpfile
, 'w')
304 for l
in fd1
.xreadlines():
305 print >>fd2
, l
.strip()
308 if os
.system("%s '%s'" % (editor
, tmpfile
)) != 0:
309 raise YapError("Editing commit message failed")
316 raise YapError("Refusing to use empty commit message")
318 fd
= os
.popen("git stripspace > %s" % tmpfile
, 'w')
323 parent
= ' -p '.join(parent
)
324 commit
= get_output("git commit-tree '%s' -p %s < '%s'" % (tree
, parent
, tmpfile
))
326 commit
= get_output("git commit-tree '%s' < '%s'" % (tree
, tmpfile
))
329 run_safely("git update-ref HEAD '%s'" % commit
[0])
332 def _check_rebasing(self
):
333 repo
= get_output('git rev-parse --git-dir')[0]
334 dotest
= os
.path
.join(repo
, '.dotest')
335 if os
.access(dotest
, os
.R_OK
):
336 raise YapError("A git operation is in progress. Complete it first")
337 dotest
= os
.path
.join(repo
, '..', '.dotest')
338 if os
.access(dotest
, os
.R_OK
):
339 raise YapError("A git operation is in progress. Complete it first")
341 def _check_git(self
):
342 if run_command("git rev-parse --git-dir"):
343 raise YapError("That command must be run from inside a git repository")
345 def _list_remotes(self
):
346 remotes
= get_output("git config --get-regexp '^remote.*.url'")
348 remote
, url
= x
.split(' ')
349 remote
= remote
.replace('remote.', '')
350 remote
= remote
.replace('.url', '')
353 def _unstage_all(self
):
355 run_safely("git read-tree -m HEAD")
357 run_safely("git read-tree HEAD")
358 run_safely("git update-index -q --refresh")
360 def _get_tracking(self
, current
):
361 remote
= get_output("git config branch.%s.remote" % current
)
363 raise YapError("No tracking branch configured for '%s'" % current
)
365 merge
= get_output("git config branch.%s.merge" % current
)
367 raise YapError("No tracking branch configured for '%s'" % current
)
368 return remote
[0], merge
[0]
370 def _confirm_push(self
, current
, rhs
, repo
):
371 print "About to push local branch '%s' to '%s' on '%s'" % (current
, rhs
, repo
)
372 print "Continue (y/n)? ",
374 ans
= sys
.stdin
.readline().strip()
376 if ans
.lower() != 'y' and ans
.lower() != 'yes':
377 raise YapError("Aborted.")
379 def _clear_state(self
):
380 repo
= get_output('git rev-parse --git-dir')[0]
381 dir = os
.path
.join(repo
, 'yap')
382 for f
in "new-files", "merge", "msg":
384 os
.unlink(os
.path
.join(dir, f
))
388 def _get_attr(self
, name
, attr
):
390 for c
in self
.__class
__.__bases
__:
392 m2
= c
.__dict
__[name
]
396 val
= m2
.__getattribute__(attr
)
397 except AttributeError:
401 def _filter_log(self
, commit
):
404 def _check_rename(self
, rev
, path
):
405 renames
= get_output("git diff-tree -C -M --diff-filter=R %s %s^"
408 r
= r
.replace('\t', ' ')
409 fields
= r
.split(' ')
410 mode1
, mode2
, hash1
, hash2
, rename
, dst
, src
= fields
415 @short_help("make a local copy of an existing repository")
417 The first argument is a URL to the existing repository. This can be an
418 absolute path if the repository is local, or a URL with the git://,
419 ssh://, or http:// schemes. By default, the directory used is the last
420 component of the URL, sans '.git'. This can be overridden by providing
423 def cmd_clone(self
, url
, directory
=None):
426 if '://' not in url
and url
[0] != '/':
427 url
= os
.path
.join(os
.getcwd(), url
)
429 url
= url
.rstrip('/')
430 if directory
is None:
431 directory
= url
.rsplit('/')[-1]
432 directory
= directory
.replace('.git', '')
437 raise YapError("Directory exists: %s" % directory
)
440 self
.cmd_repo("origin", url
)
441 self
.cmd_fetch("origin")
444 if not run_command("git rev-parse --verify refs/remotes/origin/HEAD"):
445 hash = get_output("git rev-parse refs/remotes/origin/HEAD")[0]
446 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin'"):
447 if get_output("git rev-parse %s" % b
)[0] == hash:
451 if not run_command("git rev-parse --verify refs/remotes/origin/master"):
452 branch
= "refs/remotes/origin/master"
454 branch
= get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin'")
457 hash = get_output("git rev-parse %s" % branch
)
459 branch
= branch
.replace('refs/remotes/origin/', '')
460 run_safely("git update-ref refs/heads/%s %s" % (branch
, hash[0]))
461 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
462 self
.cmd_revert(**{'-a': 1})
464 @short_help("turn a directory into a repository")
466 Converts the current working directory into a repository. The primary
467 side-effect of this command is the creation of a '.git' subdirectory.
468 No files are added nor commits made.
471 os
.system("git init")
473 @short_help("add a new file to the repository")
475 The arguments are the files to be added to the repository. Once added,
476 the files will show as "unstaged changes" in the output of 'status'. To
477 reverse the effects of this command, see 'rm'.
479 def cmd_add(self
, *files
):
490 @short_help("delete a file from the repository")
492 The arguments are the files to be removed from the current revision of
493 the repository. The files will still exist in any past commits that the
494 files may have been a part of. The file is not actually deleted, it is
495 just no longer tracked as part of the repository.
497 def cmd_rm(self
, *files
):
507 @short_help("stage changes in a file for commit")
509 The arguments are the files to be staged. Staging changes is a way to
510 build up a commit when you do not want to commit all changes at once.
511 To commit only staged changes, use the '-d' flag to 'commit.' To
512 reverse the effects of this command, see 'unstage'. Once staged, the
513 files will show as "staged changes" in the output of 'status'.
515 def cmd_stage(self
, *files
):
525 @short_help("unstage changes in a file")
527 The arguments are the files to be unstaged. Once unstaged, the files
528 will show as "unstaged changes" in the output of 'status'. The '-a'
529 flag can be used to unstage all staged changes at once.
532 def cmd_unstage(self
, *files
, **flags
):
536 files
= self
._get
_staged
_files
()
539 raise YapError("Nothing to do")
545 @short_help("show files with staged and unstaged changes")
547 Show the files in the repository with changes since the last commit,
548 categorized based on whether the changes are staged or not. A file may
549 appear under each heading if the same file has both staged and unstaged
552 def cmd_status(self
):
555 branch
= get_output("git symbolic-ref HEAD")
557 branch
= branch
[0].replace('refs/heads/', '')
560 print "Current branch: %s" % branch
562 print "Files with staged changes:"
563 files
= self
._get
_staged
_files
()
565 print "\t%s" % self
._repo
_path
_to
_rel
(f
)
569 print "Files with unstaged changes:"
570 files
= self
._get
_unstaged
_files
()
572 print "\t%s" % self
._repo
_path
_to
_rel
(f
)
576 files
= self
._get
_unmerged
_files
()
578 print "Files with conflicts:"
580 print "\t%s" % self
._repo
_path
_to
_rel
(f
)
582 @short_help("remove uncommitted changes from a file (*)")
584 The arguments are the files whose changes will be reverted. If the '-a'
585 flag is given, then all files will have uncommitted changes removed.
586 Note that there is no way to reverse this command short of manually
587 editing each file again.
590 def cmd_revert(self
, *files
, **flags
):
595 run_safely("git checkout-index -u -f -a")
607 @short_help("record changes to files as a new commit")
609 Create a new commit recording changes since the last commit. If there
610 are only unstaged changes, those will be recorded. If there are only
611 staged changes, those will be recorded. Otherwise, you will have to
612 specify either the '-a' flag or the '-d' flag to commit all changes or
613 only staged changes, respectively. To reverse the effects of this
614 command, see 'uncommit'.
616 @takes_options("adm:")
617 def cmd_commit(self
, **flags
):
618 "[-a | -d] [-m <msg>]"
620 self
._check
_rebasing
()
621 self
._check
_commit
(**flags
)
622 if not self
._get
_staged
_files
():
623 raise YapError("No changes to commit")
624 msg
= flags
.get('-m', None)
628 @short_help("reverse the actions of the last commit")
630 Reverse the effects of the last 'commit' operation. The changes that
631 were part of the previous commit will show as "staged changes" in the
632 output of 'status'. This means that if no files were changed since the
633 last commit was created, 'uncommit' followed by 'commit' is a lossless
636 def cmd_uncommit(self
):
642 @short_help("report the current version of yap")
643 def cmd_version(self
):
644 print "Yap version %s" % self
.version
646 @short_help("show the changelog for particular versions or files")
648 The arguments are the files with which to filter history. If none are
649 given, all changes are listed. Otherwise only commits that affected one
650 or more of the given files are listed. The -r option changes the
651 starting revision for traversing history. By default, history is listed
654 @takes_options("pr:")
655 def cmd_log(self
, *paths
, **flags
):
656 "[-p] [-r <rev>] <path>..."
658 rev
= flags
.get('-r', 'HEAD')
659 rev
= self
._resolve
_rev
(rev
)
666 pager
= os
.popen(self
._get
_pager
_cmd
(), 'w')
669 for hash in yield_output("git rev-list '%s' -- %s"
670 % (rev
, ' '.join(paths
))):
671 commit
= get_output("git show -M -C %s %s"
672 % (flags
.get('-p', '--name-status'), hash),
674 commit
= self
._filter
_log
(commit
)
675 print >>pager
, ''.join(commit
)
679 src
= self
._check
_rename
(hash, paths
[0])
688 except (IOError, KeyboardInterrupt):
691 @short_help("show staged, unstaged, or all uncommitted changes")
693 Show staged, unstaged, or all uncommitted changes. By default, all
694 changes are shown. The '-u' flag causes only unstaged changes to be
695 shown. The '-d' flag causes only staged changes to be shown.
698 def cmd_diff(self
, **flags
):
701 if '-u' in flags
and '-d' in flags
:
702 raise YapError("Conflicting flags: -u and -d")
704 pager
= self
._get
_pager
_cmd
()
707 os
.system("git diff-files -p | %s" % pager
)
709 os
.system("git diff-index --cached -p HEAD | %s" % pager
)
711 os
.system("git diff-index -p HEAD | %s" % pager
)
713 @short_help("list, create, or delete branches")
715 If no arguments are specified, a list of local branches is given. The
716 current branch is indicated by a "*" next to the name. If an argument
717 is given, it is taken as the name of a new branch to create. The branch
718 will start pointing at the current HEAD. See 'point' for details on
719 changing the revision of the new branch. Note that this command does
720 not switch the current working branch. See 'switch' for details on
721 changing the current working branch.
723 The '-d' flag can be used to delete local branches. If the delete
724 operation would remove the last branch reference to a given line of
725 history (colloquially referred to as "dangling commits"), yap will
726 report an error and abort. The '-f' flag can be used to force the delete
729 @takes_options("fd:")
730 def cmd_branch(self
, branch
=None, **flags
):
731 "[ [-f] -d <branch> | <branch> ]"
733 force
= '-f' in flags
735 self
._delete
_branch
(flags
['-d'], force
)
739 if branch
is not None:
740 ref
= get_output("git rev-parse --verify HEAD")
742 raise YapError("No branch point yet. Make a commit")
743 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
745 current
= get_output("git symbolic-ref HEAD")
746 branches
= get_output("git for-each-ref --format='%(refname)' 'refs/heads'")
748 if current
and b
== current
[0]:
752 b
= b
.replace('refs/heads/', '')
755 @short_help("change the current working branch")
757 The argument is the name of the branch to make the current working
758 branch. This command will fail if there are uncommitted changes to any
759 files. Otherwise, the contents of the files in the working directory
760 are updated to reflect their state in the new branch. Additionally, any
761 future commits are added to the new branch instead of the previous line
765 def cmd_switch(self
, branch
, **flags
):
768 self
._check
_rebasing
()
769 ref
= self
._resolve
_rev
('refs/heads/'+branch
)
771 if '-f' not in flags
:
772 if (self
._get
_staged
_files
()
773 or (self
._get
_unstaged
_files
()
774 and run_command("git update-index --refresh"))):
775 raise YapError("You have uncommitted changes. Use -f to continue anyway")
777 if self
._get
_unstaged
_files
() and self
._get
_staged
_files
():
778 raise YapError("You have staged and unstaged changes. Perhaps unstage -a?")
780 staged
= bool(self
._get
_staged
_files
())
782 run_command("git diff-files -p | git apply --cached")
783 for f
in self
._get
_new
_files
():
786 idx
= get_output("git write-tree")
787 new
= self
._resolve
_rev
('refs/heads/'+branch
)
789 run_command("git update-index --refresh")
790 readtree
= "git read-tree -v --aggressive -u -m HEAD %s %s" % (idx
[0], new
)
791 if os
.system(readtree
):
792 raise YapError("Failed to switch")
793 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
795 if '-f' not in flags
:
802 @short_help("move the current branch to a different revision")
804 The argument is the hash of the commit to which the current branch
805 should point, or alternately a branch or tag (a.k.a, "committish"). If
806 moving the branch would create "dangling commits" (see 'branch'), yap
807 will report an error and abort. The '-f' flag can be used to force the
808 operation in spite of this.
811 def cmd_point(self
, where
, **flags
):
814 self
._check
_rebasing
()
816 head
= get_output("git rev-parse --verify HEAD")
818 raise YapError("No commit yet; nowhere to point")
820 ref
= self
._resolve
_rev
(where
)
821 ref
= get_output("git rev-parse --verify '%s^{commit}'" % ref
)
823 raise YapError("Not a commit: %s" % where
)
825 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
826 raise YapError("You have uncommitted changes. Commit them first")
828 run_safely("git update-ref HEAD '%s'" % ref
[0])
830 if '-f' not in flags
:
831 name
= get_output("git name-rev --name-only '%s'" % head
[0])[0]
832 if name
== "undefined":
833 os
.system("git update-ref HEAD '%s'" % head
[0])
834 raise YapError("Pointing there will lose commits. Use -f to force")
836 run_command("git update-index --refresh")
837 rc
= os
.system("git read-tree -v --reset -u HEAD")
839 raise YapError("checkout-index failed")
842 @short_help("alter history by dropping or amending commits")
844 This command operates in two distinct modes, "amend" and "drop" mode.
845 In drop mode, the given commit is removed from the history of the
846 current branch, as though that commit never happened. By default the
849 In amend mode, the uncommitted changes present are merged into a
850 previous commit. This is useful for correcting typos or adding missed
851 files into past commits. By default the commit used is HEAD.
853 While rewriting history it is possible that conflicts will arise. If
854 this happens, the rewrite will pause and you will be prompted to resolve
855 the conflicts and stage them. Once that is done, you will run "yap
856 history continue." If instead you want the conflicting commit removed
857 from history (perhaps your changes supercede that commit) you can run
858 "yap history skip". Once the rewrite completes, your branch will be on
859 the same commit as when the rewrite started.
861 def cmd_history(self
, subcmd
, *args
):
862 "amend | drop <commit>"
865 if subcmd
not in ("amend", "drop", "continue", "skip"):
869 When you have resolved the conflicts run \"yap history continue\".
870 To skip the problematic patch, run \"yap history skip\"."""
872 if subcmd
== "continue":
873 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
876 os
.system("git reset --hard")
877 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
880 if subcmd
== "amend":
881 flags
, args
= getopt
.getopt(args
, "ad")
891 self
._resolve
_rev
(commit
)
892 self
._check
_rebasing
()
894 if subcmd
== "amend":
895 self
._check
_commit
(**flags
)
896 if self
._get
_unstaged
_files
():
897 # XXX: handle unstaged changes better
898 raise YapError("Commit away changes that you aren't amending")
902 start
= get_output("git rev-parse HEAD")
903 stash
= get_output("git stash create")
904 run_command("git reset --hard")
906 fd
, tmpfile
= tempfile
.mkstemp("yap")
910 os
.system("git format-patch -k --stdout '%s' > %s" % (commit
, tmpfile
))
911 if subcmd
== "amend":
912 self
.cmd_point(commit
, **{'-f': True})
914 if subcmd
== "amend":
916 rc
= os
.system("git stash apply %s" % stash
[0])
918 self
.cmd_point(start
[0], **{'-f': True})
919 os
.system("git stash apply %s" % stash
[0])
920 raise YapError("Failed to apply stash")
923 if subcmd
== "amend":
925 self
._check
_commit
(**{'-a': True})
928 self
.cmd_point("%s^" % commit
, **{'-f': True})
930 stat
= os
.stat(tmpfile
)
933 run_safely("git update-index --refresh")
934 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
936 raise YapError("Failed to apply changes")
941 run_command("git stash apply %s" % stash
[0])
944 @short_help("show the changes introduced by a given commit")
946 By default, the changes in the last commit are shown. To override this,
947 specify a hash, branch, or tag (committish). The hash of the commit,
948 the commit's author, log message, and a diff of the changes are shown.
950 def cmd_show(self
, commit
="HEAD"):
953 commit
= self
._resolve
_rev
(commit
)
954 os
.system("git show '%s'" % commit
)
956 @short_help("apply the changes in a given commit to the current branch")
958 The argument is the hash, branch, or tag (committish) of the commit to
959 be applied. In general, it only makes sense to apply commits that
960 happened on another branch. The '-r' flag can be used to have the
961 changes in the given commit reversed from the current branch. In
962 general, this only makes sense for commits that happened on the current
966 def cmd_cherry_pick(self
, commit
, **flags
):
969 commit
= self
._resolve
_rev
(commit
)
971 os
.system("git revert '%s'" % commit
)
973 os
.system("git cherry-pick '%s'" % commit
)
975 @short_help("list, add, or delete configured remote repositories")
977 When invoked with no arguments, this command will show the list of
978 currently configured remote repositories, giving both the name and URL
979 of each. To add a new repository, give the desired name as the first
980 argument and the URL as the second. The '-d' flag can be used to remove
981 a previously added repository.
984 def cmd_repo(self
, name
=None, url
=None, **flags
):
985 "[<name> <url> | -d <name>]"
987 if name
is not None and url
is None:
991 if flags
['-d'] not in [ x
[0] for x
in self
._list
_remotes
() ]:
992 raise YapError("No such repository: %s" % flags
['-d'])
993 os
.system("git config --unset remote.%s.url" % flags
['-d'])
994 os
.system("git config --unset remote.%s.fetch" % flags
['-d'])
995 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin'"):
996 hash = get_output("git rev-parse %s" % b
)
998 run_safely("git update-ref -d %s %s" % (b
, hash[0]))
1001 if name
in [ x
[0] for x
in self
._list
_remotes
() ]:
1002 raise YapError("Repository '%s' already exists" % name
)
1003 os
.system("git config remote.%s.url %s" % (name
, url
))
1004 os
.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name
, name
))
1006 for remote
, url
in self
._list
_remotes
():
1008 print " URL: %s" % url
1010 for b
in get_output("git for-each-ref --format='%%(refname)' 'refs/remotes/%s'" % remote
):
1011 b
= b
.replace('refs/remotes/', '')
1013 branches
= "Branches: "
1016 print " %s%s" % (branches
, b
)
1019 @short_help("send local commits to a remote repository (*)")
1021 When invoked with no arguments, the current branch is synchronized to
1022 the tracking branch of the tracking remote. If no tracking remote is
1023 specified, the repository will have to be specified on the command line.
1024 In that case, the default is to push to a branch with the same name as
1025 the current branch. This behavior can be overridden by giving a second
1026 argument to specify the remote branch.
1028 If the remote branch does not currently exist, the command will abort
1029 unless the -c flag is provided. If the remote branch is not a direct
1030 descendent of the local branch, the command will abort unless the -f
1031 flag is provided. Forcing a push in this way can be problematic to
1032 other users of the repository if they are not expecting it.
1034 To delete a branch on the remote repository, use the -d flag.
1036 @takes_options("cdf")
1037 def cmd_push(self
, repo
=None, rhs
=None, **flags
):
1040 if '-c' in flags
and '-d' in flags
:
1043 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1044 raise YapError("No such repository: %s" % repo
)
1046 current
= get_output("git symbolic-ref HEAD")
1048 raise YapError("Not on a branch!")
1050 self
._check
_rebasing
()
1052 current
= current
[0].replace('refs/heads/', '')
1053 remote
= get_output("git config branch.%s.remote" % current
)
1054 if repo
is None and remote
:
1058 raise YapError("No tracking branch configured; specify destination repository")
1060 if rhs
is None and remote
and remote
[0] == repo
:
1061 merge
= get_output("git config branch.%s.merge" % current
)
1066 rhs
= "refs/heads/%s" % current
1068 if '-c' not in flags
and '-d' not in flags
:
1069 if run_command("git rev-parse --verify refs/remotes/%s/%s"
1070 % (repo
, rhs
.replace('refs/heads/', ''))):
1071 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
1072 if '-f' not in flags
:
1073 hash = get_output("git rev-parse refs/remotes/%s/%s" % (repo
, rhs
.replace('refs/heads/', '')))
1074 base
= get_output("git merge-base HEAD %s" % hash[0])
1076 if base
[0] != hash[0]:
1077 raise YapError("Branch not up-to-date with remote. Update or use -f")
1079 self
._confirm
_push
(current
, rhs
, repo
)
1086 lhs
= "refs/heads/%s" % current
1087 rc
= os
.system("git push %s %s %s:%s" % (flags
.get('-f', ''), repo
, lhs
, rhs
))
1089 raise YapError("Push failed.")
1091 @short_help("retrieve commits from a remote repository")
1093 When run with no arguments, the command will retrieve new commits from
1094 the remote tracking repository. Note that this does not in any way
1095 alter the current branch. For that, see "update". If a remote other
1096 than the tracking remote is desired, it can be specified as the first
1099 def cmd_fetch(self
, repo
=None):
1102 current
= get_output("git symbolic-ref HEAD")
1104 raise YapError("Not on a branch!")
1106 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1107 raise YapError("No such repository: %s" % repo
)
1109 current
= current
[0].replace('refs/heads/', '')
1110 remote
= get_output("git config branch.%s.remote" % current
)
1114 raise YapError("No tracking branch configured; specify a repository")
1115 os
.system("git fetch %s" % repo
)
1117 @short_help("update the current branch relative to its tracking branch")
1119 Updates the current branch relative to its remote tracking branch. This
1120 command requires that the current branch have a remote tracking branch
1121 configured. If any conflicts occur while applying your changes to the
1122 updated remote, the command will pause to allow you to fix them. Once
1123 that is done, run "update" with the "continue" subcommand. Alternately,
1124 the "skip" subcommand can be used to discard the conflicting changes.
1126 def cmd_update(self
, subcmd
=None):
1129 if subcmd
and subcmd
not in ["continue", "skip"]:
1133 When you have resolved the conflicts run \"yap update continue\".
1134 To skip the problematic patch, run \"yap update skip\"."""
1136 if subcmd
== "continue":
1137 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
1139 if subcmd
== "skip":
1140 os
.system("git reset --hard")
1141 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
1144 self
._check
_rebasing
()
1145 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
1146 raise YapError("You have uncommitted changes. Commit them first")
1148 current
= get_output("git symbolic-ref HEAD")
1150 raise YapError("Not on a branch!")
1152 current
= current
[0].replace('refs/heads/', '')
1153 remote
, merge
= self
._get
_tracking
(current
)
1154 merge
= merge
.replace('refs/heads/', '')
1156 self
.cmd_fetch(remote
)
1157 base
= get_output("git merge-base HEAD refs/remotes/%s/%s" % (remote
, merge
))
1160 fd
, tmpfile
= tempfile
.mkstemp("yap")
1162 os
.system("git format-patch -k --stdout '%s' > %s" % (base
[0], tmpfile
))
1163 self
.cmd_point("refs/remotes/%s/%s" % (remote
, merge
), **{'-f': True})
1165 stat
= os
.stat(tmpfile
)
1168 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
1170 raise YapError("Failed to apply changes")
1174 @short_help("query and configure remote branch tracking")
1176 When invoked with no arguments, the command displays the tracking
1177 information for the current branch. To configure the tracking
1178 information, two arguments for the remote repository and remote branch
1179 are given. The tracking information is used to provide defaults for
1180 where to push local changes and from where to get updates to the branch.
1182 def cmd_track(self
, repo
=None, branch
=None):
1186 current
= get_output("git symbolic-ref HEAD")
1188 raise YapError("Not on a branch!")
1189 current
= current
[0].replace('refs/heads/', '')
1191 if repo
is None and branch
is None:
1192 repo
, merge
= self
._get
_tracking
(current
)
1193 merge
= merge
.replace('refs/heads/', '')
1194 print "Branch '%s' tracking refs/remotes/%s/%s" % (current
, repo
, merge
)
1197 if repo
is None or branch
is None:
1200 if repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1201 raise YapError("No such repository: %s" % repo
)
1203 if run_command("git rev-parse --verify refs/remotes/%s/%s" % (repo
, branch
)):
1204 raise YapError("No such branch '%s' on repository '%s'" % (branch
, repo
))
1206 os
.system("git config branch.%s.remote '%s'" % (current
, repo
))
1207 os
.system("git config branch.%s.merge 'refs/heads/%s'" % (current
, branch
))
1208 print "Branch '%s' now tracking refs/remotes/%s/%s" % (current
, repo
, branch
)
1210 @short_help("mark files with conflicts as resolved")
1212 The arguments are the files to be marked resolved. When a conflict
1213 occurs while merging changes to a file, that file is marked as
1214 "unmerged." Until the file(s) with conflicts are marked resolved,
1215 commits cannot be made.
1217 def cmd_resolved(self
, *files
):
1224 self
._stage
_one
(f
, True)
1227 @short_help("merge a branch into the current branch")
1228 def cmd_merge(self
, branch
):
1232 branch_name
= branch
1233 branch
= self
._resolve
_rev
(branch
)
1234 base
= get_output("git merge-base HEAD %s" % branch
)
1236 raise YapError("Branch '%s' is not a fork of the current branch"
1239 readtree
= ("git read-tree --aggressive -u -m %s HEAD %s"
1240 % (base
[0], branch
))
1241 if run_command(readtree
):
1242 run_command("git update-index --refresh")
1243 if os
.system(readtree
):
1244 raise YapError("Failed to merge")
1246 repo
= get_output('git rev-parse --git-dir')[0]
1247 dir = os
.path
.join(repo
, 'yap')
1252 msg_file
= os
.path
.join(dir, 'msg')
1253 msg
= file(msg_file
, 'w')
1254 print >>msg
, "Merge branch '%s'" % branch_name
1257 head
= get_output("git rev-parse --verify HEAD")
1259 heads
= [head
[0], branch
]
1260 head_file
= os
.path
.join(dir, 'merge')
1261 pickle
.dump(heads
, file(head_file
, 'w'))
1263 self
._merge
_index
(branch
, base
[0])
1264 if self
._get
_unmerged
_files
():
1266 raise YapError("Fix conflicts then commit")
1270 def _merge_index(self
, branch
, base
):
1271 for f
in self
._get
_unmerged
_files
():
1272 fd
, bfile
= tempfile
.mkstemp("yap")
1274 rc
= os
.system("git show %s:%s > %s" % (base
, f
, bfile
))
1277 fd
, ofile
= tempfile
.mkstemp("yap")
1279 rc
= os
.system("git show %s:%s > %s" % (branch
, f
, ofile
))
1282 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
)
1283 rc
= os
.system(command
)
1289 self
._stage
_one
(f
, True)
1291 def cmd_help(self
, cmd
=None):
1294 cmd
= "cmd_" + cmd
.replace('-', '_')
1296 attr
= self
.__getattribute
__(cmd
)
1297 except AttributeError:
1298 raise YapError("No such command: %s" % cmd
)
1300 help = self
._get
_attr
(cmd
, "long_help")
1302 raise YapError("Sorry, no help for '%s'. Ask Steven." % oldcmd
)
1304 print >>sys
.stderr
, "The '%s' command" % oldcmd
1305 doc
= self
._get
_attr
(cmd
, "__doc__")
1308 print >>sys
.stderr
, "\tyap %s %s" % (oldcmd
, doc
)
1309 print >>sys
.stderr
, "%s" % help
1312 print >> sys
.stderr
, "Yet Another (Git) Porcelein"
1315 for name
in dir(self
):
1316 if not name
.startswith('cmd_'):
1318 attr
= self
.__getattribute
__(name
)
1319 if not callable(attr
):
1322 short_msg
= self
._get
_attr
(name
, "short_help")
1323 if short_msg
is None:
1326 name
= name
.replace('cmd_', '')
1327 name
= name
.replace('_', '-')
1328 print >> sys
.stderr
, "%-16s%s" % (name
, short_msg
)
1331 print >> sys
.stderr
, "(*) Indicates that the command is not readily reversible"
1333 @short_help("show information about loaded plugins")
1334 def cmd_plugins(self
):
1336 print >> sys
.stderr
, "Loaded plugins:"
1337 plugins
= load_plugins()
1338 for name
, cls
in plugins
.items():
1339 print "\t%-16s: %s" % (name
, cls
.__doc
__)
1341 print "\t%-16s" % "None"
1343 def cmd_usage(self
):
1344 print >> sys
.stderr
, "usage: %s <command>" % os
.path
.basename(sys
.argv
[0])
1345 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"
1348 plugindir
= os
.path
.join("~", ".yap", "plugins")
1349 plugindir
= os
.path
.expanduser(plugindir
)
1350 plugindir
= os
.path
.join(plugindir
, "*.py")
1353 for p
in glob
.glob(os
.path
.expanduser(plugindir
)):
1354 plugin
= os
.path
.basename(p
).replace('.py', '')
1355 m
= __import__(plugin
)
1358 if not type(cls
) == type:
1360 if not issubclass(cls
, YapCore
):
1367 def yap_metaclass(name
, bases
, dct
):
1368 plugindir
= os
.path
.join("~", ".yap", "plugins")
1369 plugindir
= os
.path
.expanduser(plugindir
)
1370 sys
.path
.insert(0, plugindir
)
1372 plugins
= set(load_plugins().values())
1375 p2
-= set(cls
.__bases
__)
1377 bases
= list(plugins
) + list(bases
)
1378 return type(name
, tuple(bases
), dct
)
1381 __metaclass__
= yap_metaclass
1383 def main(self
, args
):
1391 if run_command("git --version"):
1392 print >>sys
.stderr
, "Failed to run git; is it installed?"
1395 debug
= os
.getenv('YAP_DEBUG')
1398 command
= command
.replace('-', '_')
1399 meth
= self
.__getattribute
__("cmd_"+command
)
1400 doc
= self
._get
_attr
("cmd_"+command
, "__doc__")
1404 for c
in self
.__class
__.__bases
__:
1406 t
= c
.__dict
__["cmd_"+command
]
1409 if "options" in t
.__dict
__:
1410 options
+= t
.options
1414 flags
, args
= getopt
.getopt(args
, options
)
1416 except getopt
.GetoptError
, e
:
1419 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1425 meth(*args
, **flags
)
1426 except (TypeError, getopt
.GetoptError
):
1429 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1433 print >> sys
.stderr
, e
1435 except AttributeError: