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
):
29 def _add_new_file(self
, file):
30 repo
= get_output('git rev-parse --git-dir')[0]
31 dir = os
.path
.join(repo
, 'yap')
36 files
= self
._get
_new
_files
()
38 path
= os
.path
.join(dir, 'new-files')
39 pickle
.dump(files
, open(path
, 'w'))
41 def _get_new_files(self
):
42 repo
= get_output('git rev-parse --git-dir')[0]
43 path
= os
.path
.join(repo
, 'yap', 'new-files')
45 files
= pickle
.load(file(path
))
52 if get_output("git ls-files --cached '%s'" % f
) != []:
57 def _remove_new_file(self
, file):
58 files
= self
._get
_new
_files
()
59 files
= filter(lambda x
: x
!= file, files
)
61 repo
= get_output('git rev-parse --git-dir')[0]
62 path
= os
.path
.join(repo
, 'yap', 'new-files')
64 pickle
.dump(files
, open(path
, 'w'))
68 def _clear_new_files(self
):
69 repo
= get_output('git rev-parse --git-dir')[0]
70 path
= os
.path
.join(repo
, 'yap', 'new-files')
73 def _assert_file_exists(self
, file):
74 if not os
.access(file, os
.R_OK
):
75 raise YapError("No such file: %s" % file)
77 def _get_staged_files(self
):
78 if run_command("git rev-parse HEAD"):
79 files
= get_output("git ls-files --cached")
81 files
= get_output("git diff-index --cached --name-only HEAD")
82 unmerged
= self
._get
_unmerged
_files
()
84 unmerged
= set(unmerged
)
85 files
= set(files
).difference(unmerged
)
89 def _get_unstaged_files(self
):
90 files
= get_output("git ls-files -m")
91 prefix
= get_output("git rev-parse --show-prefix")
93 files
= [ os
.path
.join(prefix
[0], x
) for x
in files
]
94 files
+= self
._get
_new
_files
()
95 unmerged
= self
._get
_unmerged
_files
()
97 unmerged
= set(unmerged
)
98 files
= set(files
).difference(unmerged
)
102 def _get_unmerged_files(self
):
103 files
= get_output("git ls-files -u")
104 files
= [ x
.replace('\t', ' ').split(' ')[3] for x
in files
]
105 prefix
= get_output("git rev-parse --show-prefix")
107 files
= [ os
.path
.join(prefix
[0], x
) for x
in files
]
108 return list(set(files
))
110 def _delete_branch(self
, branch
, force
):
111 current
= get_output("git symbolic-ref HEAD")
113 current
= current
[0].replace('refs/heads/', '')
114 if branch
== current
:
115 raise YapError("Can't delete current branch")
117 ref
= get_output("git rev-parse --verify 'refs/heads/%s'" % branch
)
119 raise YapError("No such branch: %s" % branch
)
120 run_safely("git update-ref -d 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
123 name
= get_output("git name-rev --name-only '%s'" % ref
[0])[0]
124 if name
== 'undefined':
125 run_command("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
126 raise YapError("Refusing to delete leaf branch (use -f to force)")
127 def _get_pager_cmd(self
):
128 if 'YAP_PAGER' in os
.environ
:
129 return os
.environ
['YAP_PAGER']
130 elif 'GIT_PAGER' in os
.environ
:
131 return os
.environ
['GIT_PAGER']
132 elif 'PAGER' in os
.environ
:
133 return os
.environ
['PAGER']
137 def _add_one(self
, file):
138 self
._assert
_file
_exists
(file)
139 x
= get_output("git ls-files '%s'" % file)
141 raise YapError("File '%s' already in repository" % file)
142 self
._add
_new
_file
(file)
144 def _rm_one(self
, file):
145 self
._assert
_file
_exists
(file)
146 if get_output("git ls-files '%s'" % file) != []:
147 run_safely("git rm --cached '%s'" % file)
148 self
._remove
_new
_file
(file)
150 def _stage_one(self
, file, allow_unmerged
=False):
151 self
._assert
_file
_exists
(file)
152 prefix
= get_output("git rev-parse --show-prefix")
154 tmp
= os
.path
.normpath(os
.path
.join(prefix
[0], file))
157 if not allow_unmerged
and tmp
in self
._get
_unmerged
_files
():
158 raise YapError("Refusing to stage conflicted file: %s" % file)
159 run_safely("git update-index --add '%s'" % file)
161 def _unstage_one(self
, file):
162 self
._assert
_file
_exists
(file)
163 if run_command("git rev-parse HEAD"):
164 rc
= run_command("git update-index --force-remove '%s'" % file)
166 rc
= run_command("git diff-index -p HEAD '%s' | git apply -R --cached" % file)
168 raise YapError("Failed to unstage")
170 def _revert_one(self
, file):
171 self
._assert
_file
_exists
(file)
173 self
._unstage
_one
(file)
176 run_safely("git checkout-index -u -f '%s'" % file)
178 def _parse_commit(self
, commit
):
179 lines
= get_output("git cat-file commit '%s'" % commit
)
184 if mode
!= 'commit' and l
.strip() == "":
189 commit
['log'].append(l
)
196 commit
['log'] = '\n'.join(commit
['log'])
199 def _check_commit(self
, **flags
):
200 if '-a' in flags
and '-d' in flags
:
201 raise YapError("Conflicting flags: -a and -d")
203 if '-d' not in flags
and self
._get
_unstaged
_files
():
204 if '-a' not in flags
and self
._get
_staged
_files
():
205 raise YapError("Staged and unstaged changes present. Specify what to commit")
206 os
.system("git diff-files -p | git apply --cached")
207 for f
in self
._get
_new
_files
():
210 def _do_uncommit(self
):
211 commit
= self
._parse
_commit
("HEAD")
212 repo
= get_output('git rev-parse --git-dir')[0]
213 dir = os
.path
.join(repo
, 'yap')
218 msg_file
= os
.path
.join(dir, 'msg')
219 fd
= file(msg_file
, 'w')
220 print >>fd
, commit
['log']
223 tree
= get_output("git rev-parse --verify HEAD^")
224 run_safely("git update-ref -m uncommit HEAD '%s'" % tree
[0])
226 def _do_commit(self
, msg
=None):
227 tree
= get_output("git write-tree")[0]
228 parent
= get_output("git rev-parse --verify HEAD 2> /dev/null")
230 if os
.environ
.has_key('YAP_EDITOR'):
231 editor
= os
.environ
['YAP_EDITOR']
232 elif os
.environ
.has_key('GIT_EDITOR'):
233 editor
= os
.environ
['GIT_EDITOR']
234 elif os
.environ
.has_key('EDITOR'):
235 editor
= os
.environ
['EDITOR']
239 fd
, tmpfile
= tempfile
.mkstemp("yap")
244 repo
= get_output('git rev-parse --git-dir')[0]
245 msg_file
= os
.path
.join(repo
, 'yap', 'msg')
246 if os
.access(msg_file
, os
.R_OK
):
248 fd2
= file(tmpfile
, 'w')
249 for l
in fd1
.xreadlines():
250 print >>fd2
, l
.strip()
253 if os
.system("%s '%s'" % (editor
, tmpfile
)) != 0:
254 raise YapError("Editing commit message failed")
261 raise YapError("Refusing to use empty commit message")
263 (fd_w
, fd_r
) = os
.popen2("git stripspace > %s" % tmpfile
)
269 commit
= get_output("git commit-tree '%s' -p '%s' < '%s'" % (tree
, parent
[0], tmpfile
))
271 commit
= get_output("git commit-tree '%s' < '%s'" % (tree
, tmpfile
))
274 run_safely("git update-ref HEAD '%s'" % commit
[0])
276 def _check_rebasing(self
):
277 repo
= get_output('git rev-parse --git-dir')[0]
278 dotest
= os
.path
.join(repo
, '.dotest')
279 if os
.access(dotest
, os
.R_OK
):
280 raise YapError("A git operation is in progress. Complete it first")
281 dotest
= os
.path
.join(repo
, '..', '.dotest')
282 if os
.access(dotest
, os
.R_OK
):
283 raise YapError("A git operation is in progress. Complete it first")
285 def _check_git(self
):
286 if run_command("git rev-parse --git-dir"):
287 raise YapError("That command must be run from inside a git repository")
289 def _list_remotes(self
):
290 remotes
= get_output("git config --get-regexp '^remote.*.url'")
292 remote
, url
= x
.split(' ')
293 remote
= remote
.replace('remote.', '')
294 remote
= remote
.replace('.url', '')
297 def _unstage_all(self
):
299 run_safely("git read-tree -m HEAD")
301 run_safely("git read-tree HEAD")
302 run_safely("git update-index -q --refresh")
304 def _get_tracking(self
, current
):
305 remote
= get_output("git config branch.%s.remote" % current
)
307 raise YapError("No tracking branch configured for '%s'" % current
)
309 merge
= get_output("git config branch.%s.merge" % current
)
311 raise YapError("No tracking branch configured for '%s'" % current
)
312 return remote
[0], merge
[0]
314 def _confirm_push(self
, current
, rhs
, repo
):
315 print "About to push local branch '%s' to '%s' on '%s'" % (current
, rhs
, repo
)
316 print "Continue (y/n)? ",
318 ans
= sys
.stdin
.readline().strip()
320 if ans
.lower() != 'y' and ans
.lower() != 'yes':
321 raise YapError("Aborted.")
323 @short_help("make a local copy of an existing repository")
325 The first argument is a URL to the existing repository. This can be an
326 absolute path if the repository is local, or a URL with the git://,
327 ssh://, or http:// schemes. By default, the directory used is the last
328 component of the URL, sans '.git'. This can be overridden by providing
331 def cmd_clone(self
, url
, directory
=None):
334 if '://' not in url
and url
[0] != '/':
335 url
= os
.path
.join(os
.getcwd(), url
)
337 url
= url
.rstrip('/')
338 if directory
is None:
339 directory
= url
.rsplit('/')[-1]
340 directory
= directory
.replace('.git', '')
345 raise YapError("Directory exists: %s" % directory
)
348 self
.cmd_repo("origin", url
)
349 self
.cmd_fetch("origin")
352 if not run_command("git rev-parse --verify refs/remotes/origin/HEAD"):
353 hash = get_output("git rev-parse refs/remotes/origin/HEAD")[0]
354 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'"):
355 if get_output("git rev-parse %s" % b
)[0] == hash:
359 if not run_command("git rev-parse --verify refs/remotes/origin/master"):
360 branch
= "refs/remotes/origin/master"
362 branch
= get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'")
365 hash = get_output("git rev-parse %s" % branch
)
367 branch
= branch
.replace('refs/remotes/origin/', '')
368 run_safely("git update-ref refs/heads/%s %s" % (branch
, hash[0]))
369 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
370 self
.cmd_revert(**{'-a': 1})
372 @short_help("turn a directory into a repository")
374 Converts the current working directory into a repository. The primary
375 side-effect of this command is the creation of a '.git' subdirectory.
376 No files are added nor commits made.
379 os
.system("git init")
381 @short_help("add a new file to the repository")
383 The arguments are the files to be added to the repository. Once added,
384 the files will show as "unstaged changes" in the output of 'status'. To
385 reverse the effects of this command, see 'rm'.
387 def cmd_add(self
, *files
):
398 @short_help("delete a file from the repository")
400 The arguments are the files to be removed from the current revision of
401 the repository. The files will still exist in any past commits that the
402 files may have been a part of. The file is not actually deleted, it is
403 just no longer tracked as part of the repository.
405 def cmd_rm(self
, *files
):
415 @short_help("stage changes in a file for commit")
417 The arguments are the files to be staged. Staging changes is a way to
418 build up a commit when you do not want to commit all changes at once.
419 To commit only staged changes, use the '-d' flag to 'commit.' To
420 reverse the effects of this command, see 'unstage'. Once staged, the
421 files will show as "staged changes" in the output of 'status'.
423 def cmd_stage(self
, *files
):
433 @short_help("unstage changes in a file")
435 The arguments are the files to be unstaged. Once unstaged, the files
436 will show as "unstaged changes" in the output of 'status'. The '-a'
437 flag can be used to unstage all staged changes at once.
440 def cmd_unstage(self
, *files
, **flags
):
455 @short_help("show files with staged and unstaged changes")
457 Show the files in the repository with changes since the last commit,
458 categorized based on whether the changes are staged or not. A file may
459 appear under each heading if the same file has both staged and unstaged
462 def cmd_status(self
):
465 branch
= get_output("git symbolic-ref HEAD")
467 branch
= branch
[0].replace('refs/heads/', '')
470 print "Current branch: %s" % branch
472 print "Files with staged changes:"
473 files
= self
._get
_staged
_files
()
479 print "Files with unstaged changes:"
480 files
= self
._get
_unstaged
_files
()
486 files
= self
._get
_unmerged
_files
()
488 print "Files with conflicts:"
492 @short_help("remove uncommitted changes from a file (*)")
494 The arguments are the files whose changes will be reverted. If the '-a'
495 flag is given, then all files will have uncommitted changes removed.
496 Note that there is no way to reverse this command short of manually
497 editing each file again.
500 def cmd_revert(self
, *files
, **flags
):
505 run_safely("git checkout-index -u -f -a")
516 @short_help("record changes to files as a new commit")
518 Create a new commit recording changes since the last commit. If there
519 are only unstaged changes, those will be recorded. If there are only
520 staged changes, those will be recorded. Otherwise, you will have to
521 specify either the '-a' flag or the '-d' flag to commit all changes or
522 only staged changes, respectively. To reverse the effects of this
523 command, see 'uncommit'.
525 @takes_options("adm:")
526 def cmd_commit(self
, **flags
):
527 "[-a | -d] [-m <msg>]"
529 self
._check
_rebasing
()
530 self
._check
_commit
(**flags
)
531 if not self
._get
_staged
_files
():
532 raise YapError("No changes to commit")
533 msg
= flags
.get('-m', None)
537 @short_help("reverse the actions of the last commit")
539 Reverse the effects of the last 'commit' operation. The changes that
540 were part of the previous commit will show as "staged changes" in the
541 output of 'status'. This means that if no files were changed since the
542 last commit was created, 'uncommit' followed by 'commit' is a lossless
545 def cmd_uncommit(self
):
551 @short_help("report the current version of yap")
552 def cmd_version(self
):
553 print "Yap version 0.1"
555 @short_help("show the changelog for particular versions or files")
557 The arguments are the files with which to filter history. If none are
558 given, all changes are listed. Otherwise only commits that affected one
559 or more of the given files are listed. The -r option changes the
560 starting revision for traversing history. By default, history is listed
563 @takes_options("pr:")
564 def cmd_log(self
, *paths
, **flags
):
565 "[-p] [-r <rev>] <path>..."
567 rev
= flags
.get('-r', 'HEAD')
576 paths
= ' '.join(paths
)
577 os
.system("git log -M -C %s %s '%s' -- %s"
578 % (follow
, flags
.get('-p', '--name-status'), rev
, paths
))
580 @short_help("show staged, unstaged, or all uncommitted changes")
582 Show staged, unstaged, or all uncommitted changes. By default, all
583 changes are shown. The '-u' flag causes only unstaged changes to be
584 shown. The '-d' flag causes only staged changes to be shown.
587 def cmd_diff(self
, **flags
):
590 if '-u' in flags
and '-d' in flags
:
591 raise YapError("Conflicting flags: -u and -d")
593 pager
= self
._get
_pager
_cmd
()
596 os
.system("git diff-files -p | %s" % pager
)
598 os
.system("git diff-index --cached -p HEAD | %s" % pager
)
600 os
.system("git diff-index -p HEAD | %s" % pager
)
602 @short_help("list, create, or delete branches")
604 If no arguments are specified, a list of local branches is given. The
605 current branch is indicated by a "*" next to the name. If an argument
606 is given, it is taken as the name of a new branch to create. The branch
607 will start pointing at the current HEAD. See 'point' for details on
608 changing the revision of the new branch. Note that this command does
609 not switch the current working branch. See 'switch' for details on
610 changing the current working branch.
612 The '-d' flag can be used to delete local branches. If the delete
613 operation would remove the last branch reference to a given line of
614 history (colloquially referred to as "dangling commits"), yap will
615 report an error and abort. The '-f' flag can be used to force the delete
618 @takes_options("fd:")
619 def cmd_branch(self
, branch
=None, **flags
):
620 "[ [-f] -d <branch> | <branch> ]"
622 force
= '-f' in flags
624 self
._delete
_branch
(flags
['-d'], force
)
628 if branch
is not None:
629 ref
= get_output("git rev-parse --verify HEAD")
631 raise YapError("No branch point yet. Make a commit")
632 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
634 current
= get_output("git symbolic-ref HEAD")
635 branches
= get_output("git for-each-ref --format='%(refname)' 'refs/heads/*'")
637 if current
and b
== current
[0]:
641 b
= b
.replace('refs/heads/', '')
644 @short_help("change the current working branch")
646 The argument is the name of the branch to make the current working
647 branch. This command will fail if there are uncommitted changes to any
648 files. Otherwise, the contents of the files in the working directory
649 are updated to reflect their state in the new branch. Additionally, any
650 future commits are added to the new branch instead of the previous line
654 def cmd_switch(self
, branch
, **flags
):
657 self
._check
_rebasing
()
658 ref
= get_output("git rev-parse --verify 'refs/heads/%s'" % branch
)
660 raise YapError("No such branch: %s" % branch
)
662 if '-f' not in flags
:
663 if (self
._get
_staged
_files
()
664 or (self
._get
_unstaged
_files
()
665 and run_command("git update-index --refresh"))):
666 raise YapError("You have uncommitted changes. Use -f to continue anyway")
668 if self
._get
_unstaged
_files
() and self
._get
_staged
_files
():
669 raise YapError("You have staged and unstaged changes. Perhaps unstage -a?")
671 staged
= bool(self
._get
_staged
_files
())
673 run_command("git diff-files -p | git apply --cached")
674 for f
in self
._get
_new
_files
():
677 idx
= get_output("git write-tree")
678 new
= get_output("git rev-parse refs/heads/%s" % branch
)
679 readtree
= "git read-tree --aggressive -u -m HEAD %s %s" % (idx
[0], new
[0])
680 if run_command(readtree
):
681 run_command("git update-index --refresh")
682 if os
.system(readtree
):
683 raise YapError("Failed to switch")
684 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
690 @short_help("move the current branch to a different revision")
692 The argument is the hash of the commit to which the current branch
693 should point, or alternately a branch or tag (a.k.a, "committish"). If
694 moving the branch would create "dangling commits" (see 'branch'), yap
695 will report an error and abort. The '-f' flag can be used to force the
696 operation in spite of this.
699 def cmd_point(self
, where
, **flags
):
702 self
._check
_rebasing
()
704 head
= get_output("git rev-parse --verify HEAD")
706 raise YapError("No commit yet; nowhere to point")
708 ref
= get_output("git rev-parse --verify '%s^{commit}'" % where
)
710 raise YapError("Not a valid ref: %s" % where
)
712 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
713 raise YapError("You have uncommitted changes. Commit them first")
715 run_safely("git update-ref HEAD '%s'" % ref
[0])
717 if '-f' not in flags
:
718 name
= get_output("git name-rev --name-only '%s'" % head
[0])[0]
719 if name
== "undefined":
720 os
.system("git update-ref HEAD '%s'" % head
[0])
721 raise YapError("Pointing there will lose commits. Use -f to force")
724 run_safely("git read-tree -u -m HEAD")
726 run_safely("git read-tree HEAD")
727 run_safely("git checkout-index -u -f -a")
729 @short_help("alter history by dropping or amending commits")
731 This command operates in two distinct modes, "amend" and "drop" mode.
732 In drop mode, the given commit is removed from the history of the
733 current branch, as though that commit never happened. By default the
736 In amend mode, the uncommitted changes present are merged into a
737 previous commit. This is useful for correcting typos or adding missed
738 files into past commits. By default the commit used is HEAD.
740 While rewriting history it is possible that conflicts will arise. If
741 this happens, the rewrite will pause and you will be prompted to resolve
742 the conflicts and stage them. Once that is done, you will run "yap
743 history continue." If instead you want the conflicting commit removed
744 from history (perhaps your changes supercede that commit) you can run
745 "yap history skip". Once the rewrite completes, your branch will be on
746 the same commit as when the rewrite started.
748 def cmd_history(self
, subcmd
, *args
):
749 "amend | drop <commit>"
752 if subcmd
not in ("amend", "drop", "continue", "skip"):
756 When you have resolved the conflicts run \"yap history continue\".
757 To skip the problematic patch, run \"yap history skip\"."""
759 if subcmd
== "continue":
760 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
763 os
.system("git reset --hard")
764 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
767 if subcmd
== "amend":
768 flags
, args
= getopt
.getopt(args
, "ad")
778 if run_command("git rev-parse --verify '%s'" % commit
):
779 raise YapError("Not a valid commit: %s" % commit
)
781 self
._check
_rebasing
()
783 if subcmd
== "amend":
784 self
._check
_commit
(**flags
)
785 if self
._get
_unstaged
_files
():
786 # XXX: handle unstaged changes better
787 raise YapError("Commit away changes that you aren't amending")
791 start
= get_output("git rev-parse HEAD")
792 stash
= get_output("git stash create")
793 run_command("git reset --hard")
795 fd
, tmpfile
= tempfile
.mkstemp("yap")
799 os
.system("git format-patch -k --stdout '%s' > %s" % (commit
, tmpfile
))
800 if subcmd
== "amend":
801 self
.cmd_point(commit
, **{'-f': True})
803 if subcmd
== "amend":
805 rc
= os
.system("git stash apply %s" % stash
[0])
807 self
.cmd_point(start
[0], **{'-f': True})
808 os
.system("git stash apply %s" % stash
[0])
809 raise YapError("Failed to apply stash")
812 if subcmd
== "amend":
814 self
._check
_commit
(**{'-a': True})
817 self
.cmd_point("%s^" % commit
, **{'-f': True})
819 stat
= os
.stat(tmpfile
)
822 run_safely("git update-index --refresh")
823 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
825 raise YapError("Failed to apply changes")
830 run_command("git stash apply %s" % stash
[0])
833 @short_help("show the changes introduced by a given commit")
835 By default, the changes in the last commit are shown. To override this,
836 specify a hash, branch, or tag (committish). The hash of the commit,
837 the commit's author, log message, and a diff of the changes are shown.
839 def cmd_show(self
, commit
="HEAD"):
842 os
.system("git show '%s'" % commit
)
844 @short_help("apply the changes in a given commit to the current branch")
846 The argument is the hash, branch, or tag (committish) of the commit to
847 be applied. In general, it only makes sense to apply commits that
848 happened on another branch. The '-r' flag can be used to have the
849 changes in the given commit reversed from the current branch. In
850 general, this only makes sense for commits that happened on the current
854 def cmd_cherry_pick(self
, commit
, **flags
):
858 os
.system("git revert '%s'" % commit
)
860 os
.system("git cherry-pick '%s'" % commit
)
862 @short_help("list, add, or delete configured remote repositories")
864 When invoked with no arguments, this command will show the list of
865 currently configured remote repositories, giving both the name and URL
866 of each. To add a new repository, give the desired name as the first
867 argument and the URL as the second. The '-d' flag can be used to remove
868 a previously added repository.
871 def cmd_repo(self
, name
=None, url
=None, **flags
):
872 "[<name> <url> | -d <name>]"
874 if name
is not None and url
is None:
878 if flags
['-d'] not in [ x
[0] for x
in self
._list
_remotes
() ]:
879 raise YapError("No such repository: %s" % flags
['-d'])
880 os
.system("git config --unset remote.%s.url" % flags
['-d'])
881 os
.system("git config --unset remote.%s.fetch" % flags
['-d'])
884 if name
in [ x
[0] for x
in self
._list
_remotes
() ]:
885 raise YapError("Repository '%s' already exists" % flags
['-d'])
886 os
.system("git config remote.%s.url %s" % (name
, url
))
887 os
.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name
, name
))
889 for remote
, url
in self
._list
_remotes
():
890 print "%-20s %s" % (remote
, url
)
892 @short_help("send local commits to a remote repository (*)")
894 When invoked with no arguments, the current branch is synchronized to
895 the tracking branch of the tracking remote. If no tracking remote is
896 specified, the repository will have to be specified on the command line.
897 In that case, the default is to push to a branch with the same name as
898 the current branch. This behavior can be overridden by giving a second
899 argument to specify the remote branch.
901 If the remote branch does not currently exist, the command will abort
902 unless the -c flag is provided. If the remote branch is not a direct
903 descendent of the local branch, the command will abort unless the -f
904 flag is provided. Forcing a push in this way can be problematic to
905 other users of the repository if they are not expecting it.
907 To delete a branch on the remote repository, use the -d flag.
909 @takes_options("cdf")
910 def cmd_push(self
, repo
=None, rhs
=None, **flags
):
913 if '-c' in flags
and '-d' in flags
:
916 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
917 raise YapError("No such repository: %s" % repo
)
919 current
= get_output("git symbolic-ref HEAD")
921 raise YapError("Not on a branch!")
923 self
._check
_rebasing
()
925 current
= current
[0].replace('refs/heads/', '')
926 remote
= get_output("git config branch.%s.remote" % current
)
927 if repo
is None and remote
:
931 raise YapError("No tracking branch configured; specify destination repository")
933 if rhs
is None and remote
and remote
[0] == repo
:
934 merge
= get_output("git config branch.%s.merge" % current
)
939 rhs
= "refs/heads/%s" % current
941 if '-c' not in flags
and '-d' not in flags
:
942 if run_command("git rev-parse --verify refs/remotes/%s/%s"
943 % (repo
, rhs
.replace('refs/heads/', ''))):
944 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
945 if '-f' not in flags
:
946 hash = get_output("git rev-parse refs/remotes/%s/%s" % (repo
, rhs
.replace('refs/heads/', '')))
947 base
= get_output("git merge-base HEAD %s" % hash[0])
949 if base
[0] != hash[0]:
950 raise YapError("Branch not up-to-date with remote. Update or use -f")
952 self
._confirm
_push
(current
, rhs
, repo
)
959 lhs
= "refs/heads/%s" % current
960 rc
= os
.system("git push %s %s %s:%s" % (flags
.get('-f', ''), repo
, lhs
, rhs
))
962 raise YapError("Push failed.")
964 @short_help("retrieve commits from a remote repository")
966 When run with no arguments, the command will retrieve new commits from
967 the remote tracking repository. Note that this does not in any way
968 alter the current branch. For that, see "update". If a remote other
969 than the tracking remote is desired, it can be specified as the first
972 def cmd_fetch(self
, repo
=None):
975 current
= get_output("git symbolic-ref HEAD")
977 raise YapError("Not on a branch!")
979 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
980 raise YapError("No such repository: %s" % repo
)
982 current
= current
[0].replace('refs/heads/', '')
983 remote
= get_output("git config branch.%s.remote" % current
)
987 raise YapError("No tracking branch configured; specify a repository")
988 os
.system("git fetch %s" % repo
)
990 @short_help("update the current branch relative to its tracking branch")
992 Updates the current branch relative to its remote tracking branch. This
993 command requires that the current branch have a remote tracking branch
994 configured. If any conflicts occur while applying your changes to the
995 updated remote, the command will pause to allow you to fix them. Once
996 that is done, run "update" with the "continue" subcommand. Alternately,
997 the "skip" subcommand can be used to discard the conflicting changes.
999 def cmd_update(self
, subcmd
=None):
1002 if subcmd
and subcmd
not in ["continue", "skip"]:
1006 When you have resolved the conflicts run \"yap update continue\".
1007 To skip the problematic patch, run \"yap update skip\"."""
1009 if subcmd
== "continue":
1010 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
1012 if subcmd
== "skip":
1013 os
.system("git reset --hard")
1014 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
1017 self
._check
_rebasing
()
1018 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
1019 raise YapError("You have uncommitted changes. Commit them first")
1021 current
= get_output("git symbolic-ref HEAD")
1023 raise YapError("Not on a branch!")
1025 current
= current
[0].replace('refs/heads/', '')
1026 remote
, merge
= self
._get
_tracking
(current
)
1027 merge
= merge
.replace('refs/heads/', '')
1029 self
.cmd_fetch(remote
)
1030 base
= get_output("git merge-base HEAD refs/remotes/%s/%s" % (remote
, merge
))
1033 fd
, tmpfile
= tempfile
.mkstemp("yap")
1035 os
.system("git format-patch -k --stdout '%s' > %s" % (base
[0], tmpfile
))
1036 self
.cmd_point("refs/remotes/%s/%s" % (remote
, merge
), **{'-f': True})
1038 stat
= os
.stat(tmpfile
)
1041 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
1043 raise YapError("Failed to apply changes")
1047 @short_help("query and configure remote branch tracking")
1049 When invoked with no arguments, the command displays the tracking
1050 information for the current branch. To configure the tracking
1051 information, two arguments for the remote repository and remote branch
1052 are given. The tracking information is used to provide defaults for
1053 where to push local changes and from where to get updates to the branch.
1055 def cmd_track(self
, repo
=None, branch
=None):
1059 current
= get_output("git symbolic-ref HEAD")
1061 raise YapError("Not on a branch!")
1062 current
= current
[0].replace('refs/heads/', '')
1064 if repo
is None and branch
is None:
1065 repo
, merge
= self
._get
_tracking
(current
)
1066 merge
= merge
.replace('refs/heads/', '')
1067 print "Branch '%s' tracking refs/remotes/%s/%s" % (current
, repo
, merge
)
1070 if repo
is None or branch
is None:
1073 if repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1074 raise YapError("No such repository: %s" % repo
)
1076 if run_command("git rev-parse --verify refs/remotes/%s/%s" % (repo
, branch
)):
1077 raise YapError("No such branch '%s' on repository '%s'" % (branch
, repo
))
1079 os
.system("git config branch.%s.remote '%s'" % (current
, repo
))
1080 os
.system("git config branch.%s.merge 'refs/heads/%s'" % (current
, branch
))
1081 print "Branch '%s' now tracking refs/remotes/%s/%s" % (current
, repo
, branch
)
1083 @short_help("mark files with conflicts as resolved")
1085 The arguments are the files to be marked resolved. When a conflict
1086 occurs while merging changes to a file, that file is marked as
1087 "unmerged." Until the file(s) with conflicts are marked resolved,
1088 commits cannot be made.
1090 def cmd_resolved(self
, *args
):
1097 self
._stage
_one
(f
, True)
1100 def cmd_help(self
, cmd
=None):
1102 cmd
= "cmd_" + cmd
.replace('-', '_')
1104 attr
= self
.__getattribute
__(cmd
)
1105 except AttributeError:
1106 raise YapError("No such command: %s" % cmd
)
1108 help = attr
.long_help
1109 except AttributeError:
1110 attr
= super(Yap
, self
).__getattribute
__(cmd
)
1112 help = attr
.long_help
1113 except AttributeError:
1114 raise YapError("Sorry, no help for '%s'. Ask Steven." % cmd
)
1116 print >>sys
.stderr
, "The '%s' command" % cmd
1117 print >>sys
.stderr
, "\tyap %s %s" % (cmd
, attr
.__doc
__)
1118 print >>sys
.stderr
, "%s" % help
1121 print >> sys
.stderr
, "Yet Another (Git) Porcelein"
1124 for name
in dir(self
):
1125 if not name
.startswith('cmd_'):
1127 attr
= self
.__getattribute
__(name
)
1128 if not callable(attr
):
1132 short_msg
= attr
.short_help
1133 except AttributeError:
1135 default_meth
= super(Yap
, self
).__getattribute
__(name
)
1136 short_msg
= default_meth
.short_help
1137 except AttributeError:
1140 name
= name
.replace('cmd_', '')
1141 name
= name
.replace('_', '-')
1142 print >> sys
.stderr
, "%-16s%s" % (name
, short_msg
)
1145 print >> sys
.stderr
, "(*) Indicates that the command is not readily reversible"
1147 def cmd_usage(self
):
1148 print >> sys
.stderr
, "usage: %s <command>" % os
.path
.basename(sys
.argv
[0])
1149 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"
1151 def main(self
, args
):
1159 if run_command("git --version"):
1160 print >>sys
.stderr
, "Failed to run git; is it installed?"
1163 debug
= os
.getenv('YAP_DEBUG')
1166 command
= command
.replace('-', '_')
1168 meth
= self
.__getattribute
__("cmd_"+command
)
1173 if "options" in meth
.__dict
__:
1174 options
= meth
.options
1176 flags
, args
= getopt
.getopt(args
, options
)
1181 meth(*args
, **flags
)
1182 except (TypeError, getopt
.GetoptError
):
1185 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1189 print >> sys
.stderr
, e
1191 except AttributeError: