8 from plugin
import YapPlugin
11 class ShellError(Exception):
12 def __init__(self
, cmd
, rc
):
17 return "%s returned %d" % (self
.cmd
, self
.rc
)
19 class YapError(Exception):
20 def __init__(self
, msg
):
30 plugindir
= os
.path
.expanduser("~/.yap/plugins")
31 for p
in glob
.glob(os
.path
.join(plugindir
, "*.py")):
34 for k
, cls
in glbls
.items():
35 if not type(cls
) == type:
37 if not issubclass(cls
, YapPlugin
):
44 if not func
.startswith('cmd_'):
46 if func
in self
.overrides
:
47 print >>sys
.stderr
, "Plugin %s overrides already overridden function %s. Disabling" % (p
, func
)
51 def _add_new_file(self
, file):
52 repo
= get_output('git rev-parse --git-dir')[0]
53 dir = os
.path
.join(repo
, 'yap')
58 files
= self
._get
_new
_files
()
60 path
= os
.path
.join(dir, 'new-files')
61 pickle
.dump(files
, open(path
, 'w'))
63 def _get_new_files(self
):
64 repo
= get_output('git rev-parse --git-dir')[0]
65 path
= os
.path
.join(repo
, 'yap', 'new-files')
67 files
= pickle
.load(file(path
))
74 if get_output("git ls-files --cached '%s'" % f
) != []:
79 def _remove_new_file(self
, file):
80 files
= self
._get
_new
_files
()
81 files
= filter(lambda x
: x
!= file, files
)
83 repo
= get_output('git rev-parse --git-dir')[0]
84 path
= os
.path
.join(repo
, 'yap', 'new-files')
85 pickle
.dump(files
, open(path
, 'w'))
87 def _clear_new_files(self
):
88 repo
= get_output('git rev-parse --git-dir')[0]
89 path
= os
.path
.join(repo
, 'yap', 'new-files')
92 def _assert_file_exists(self
, file):
93 if not os
.access(file, os
.R_OK
):
94 raise YapError("No such file: %s" % file)
96 def _get_staged_files(self
):
97 if run_command("git rev-parse HEAD"):
98 files
= get_output("git ls-files --cached")
100 files
= get_output("git diff-index --cached --name-only HEAD")
101 unmerged
= self
._get
_unmerged
_files
()
103 unmerged
= set(unmerged
)
104 files
= set(files
).difference(unmerged
)
108 def _get_unstaged_files(self
):
109 files
= get_output("git ls-files -m")
110 prefix
= get_output("git rev-parse --show-prefix")
112 files
= [ os
.path
.join(prefix
[0], x
) for x
in files
]
113 files
+= self
._get
_new
_files
()
114 unmerged
= self
._get
_unmerged
_files
()
116 unmerged
= set(unmerged
)
117 files
= set(files
).difference(unmerged
)
121 def _get_unmerged_files(self
):
122 files
= get_output("git ls-files -u")
123 files
= [ x
.replace('\t', ' ').split(' ')[3] for x
in files
]
124 prefix
= get_output("git rev-parse --show-prefix")
126 files
= [ os
.path
.join(prefix
[0], x
) for x
in files
]
127 return list(set(files
))
129 def _delete_branch(self
, branch
, force
):
130 current
= get_output("git symbolic-ref HEAD")
132 current
= current
[0].replace('refs/heads/', '')
133 if branch
== current
:
134 raise YapError("Can't delete current branch")
136 ref
= get_output("git rev-parse --verify 'refs/heads/%s'" % branch
)
138 raise YapError("No such branch: %s" % branch
)
139 run_safely("git update-ref -d 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
142 name
= get_output("git name-rev --name-only '%s'" % ref
[0])[0]
143 if name
== 'undefined':
144 run_command("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
145 raise YapError("Refusing to delete leaf branch (use -f to force)")
146 def _get_pager_cmd(self
):
147 if 'YAP_PAGER' in os
.environ
:
148 return os
.environ
['YAP_PAGER']
149 elif 'GIT_PAGER' in os
.environ
:
150 return os
.environ
['GIT_PAGER']
151 elif 'PAGER' in os
.environ
:
152 return os
.environ
['PAGER']
156 def _add_one(self
, file):
157 self
._assert
_file
_exists
(file)
158 x
= get_output("git ls-files '%s'" % file)
160 raise YapError("File '%s' already in repository" % file)
161 self
._add
_new
_file
(file)
163 def _rm_one(self
, file):
164 self
._assert
_file
_exists
(file)
165 if get_output("git ls-files '%s'" % file) != []:
166 run_safely("git rm --cached '%s'" % file)
167 self
._remove
_new
_file
(file)
169 def _stage_one(self
, file, allow_unmerged
=False):
170 self
._assert
_file
_exists
(file)
171 prefix
= get_output("git rev-parse --show-prefix")
173 tmp
= os
.path
.normpath(os
.path
.join(prefix
[0], file))
176 if not allow_unmerged
and tmp
in self
._get
_unmerged
_files
():
177 raise YapError("Refusing to stage conflicted file: %s" % file)
178 run_safely("git update-index --add '%s'" % file)
180 def _unstage_one(self
, file):
181 self
._assert
_file
_exists
(file)
182 if run_command("git rev-parse HEAD"):
183 run_safely("git update-index --force-remove '%s'" % file)
185 run_safely("git diff-index -p HEAD '%s' | git apply -R --cached" % file)
187 def _revert_one(self
, file):
188 self
._assert
_file
_exists
(file)
189 self
._unstage
_one
(file)
190 run_safely("git checkout-index -u -f '%s'" % file)
192 def _parse_commit(self
, commit
):
193 lines
= get_output("git cat-file commit '%s'" % commit
)
198 if mode
!= 'commit' and l
.strip() == "":
203 commit
['log'].append(l
)
210 commit
['log'] = '\n'.join(commit
['log'])
213 def _check_commit(self
, **flags
):
214 if '-a' in flags
and '-d' in flags
:
215 raise YapError("Conflicting flags: -a and -d")
217 if '-d' not in flags
and self
._get
_unstaged
_files
():
218 if '-a' not in flags
and self
._get
_staged
_files
():
219 raise YapError("Staged and unstaged changes present. Specify what to commit")
220 os
.system("git diff-files -p | git apply --cached")
221 for f
in self
._get
_new
_files
():
224 def _do_uncommit(self
):
225 commit
= self
._parse
_commit
("HEAD")
226 repo
= get_output('git rev-parse --git-dir')[0]
227 dir = os
.path
.join(repo
, 'yap')
232 msg_file
= os
.path
.join(dir, 'msg')
233 fd
= file(msg_file
, 'w')
234 print >>fd
, commit
['log']
237 tree
= get_output("git rev-parse --verify HEAD^")
238 run_safely("git update-ref -m uncommit HEAD '%s'" % tree
[0])
240 def _do_commit(self
, msg
=None):
241 tree
= get_output("git write-tree")[0]
242 parent
= get_output("git rev-parse --verify HEAD 2> /dev/null")[0]
244 if os
.environ
.has_key('YAP_EDITOR'):
245 editor
= os
.environ
['YAP_EDITOR']
246 elif os
.environ
.has_key('GIT_EDITOR'):
247 editor
= os
.environ
['GIT_EDITOR']
248 elif os
.environ
.has_key('EDITOR'):
249 editor
= os
.environ
['EDITOR']
253 fd
, tmpfile
= tempfile
.mkstemp("yap")
258 repo
= get_output('git rev-parse --git-dir')[0]
259 msg_file
= os
.path
.join(repo
, 'yap', 'msg')
260 if os
.access(msg_file
, os
.R_OK
):
262 fd2
= file(tmpfile
, 'w')
263 for l
in fd1
.xreadlines():
264 print >>fd2
, l
.strip()
267 if os
.system("%s '%s'" % (editor
, tmpfile
)) != 0:
268 raise YapError("Editing commit message failed")
275 raise YapError("Refusing to use empty commit message")
277 (fd_w
, fd_r
) = os
.popen2("git stripspace > %s" % tmpfile
)
283 commit
= get_output("git commit-tree '%s' -p '%s' < '%s'" % (tree
, parent
, tmpfile
))
285 commit
= get_output("git commit-tree '%s' < '%s'" % (tree
, tmpfile
))
288 run_safely("git update-ref HEAD '%s'" % commit
[0])
290 def _check_rebasing(self
):
291 repo
= get_output('git rev-parse --git-dir')[0]
292 dotest
= os
.path
.join(repo
, '.dotest')
293 if os
.access(dotest
, os
.R_OK
):
294 raise YapError("A git operation is in progress. Complete it first")
295 dotest
= os
.path
.join(repo
, '..', '.dotest')
296 if os
.access(dotest
, os
.R_OK
):
297 raise YapError("A git operation is in progress. Complete it first")
299 def _check_git(self
):
300 if run_command("git rev-parse --git-dir"):
301 raise YapError("That command must be run from inside a git repository")
303 def _list_remotes(self
):
304 remotes
= get_output("git config --get-regexp '^remote.*.url'")
306 remote
, url
= x
.split(' ')
307 remote
= remote
.replace('remote.', '')
308 remote
= remote
.replace('.url', '')
311 def _unstage_all(self
):
313 run_safely("git read-tree -m HEAD")
315 run_safely("git read-tree HEAD")
316 run_safely("git update-index -q --refresh")
318 def _get_tracking(self
, current
):
319 remote
= get_output("git config branch.%s.remote" % current
)
321 raise YapError("No tracking branch configured for '%s'" % current
)
323 merge
= get_output("git config branch.%s.merge" % current
)
325 raise YapError("No tracking branch configured for '%s'" % current
)
326 return remote
[0], merge
328 @short_help("make a local copy of an existing repository")
330 The first argument is a URL to the existing repository. This can be an
331 absolute path if the repository is local, or a URL with the git://,
332 ssh://, or http:// schemes. By default, the directory used is the last
333 component of the URL, sans '.git'. This can be overridden by providing
336 def cmd_clone(self
, url
, directory
=None):
339 if '://' not in url
and url
[0] != '/':
340 url
= os
.path
.join(os
.getcwd(), url
)
342 url
= url
.rstrip('/')
343 if directory
is None:
344 directory
= url
.rsplit('/')[-1]
345 directory
= directory
.replace('.git', '')
350 raise YapError("Directory exists: %s" % directory
)
353 self
.cmd_repo("origin", url
)
354 self
.cmd_fetch("origin")
357 if not run_command("git rev-parse --verify refs/remotes/origin/HEAD"):
358 hash = get_output("git rev-parse refs/remotes/origin/HEAD")[0]
359 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'"):
360 if get_output("git rev-parse %s" % b
)[0] == hash:
364 if not run_command("git rev-parse --verify refs/remotes/origin/master"):
365 branch
= "refs/remotes/origin/master"
367 branch
= get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'")
370 hash = get_output("git rev-parse %s" % branch
)
372 branch
= branch
.replace('refs/remotes/origin/', '')
373 run_safely("git update-ref refs/heads/%s %s" % (branch
, hash[0]))
374 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
375 self
.cmd_revert(**{'-a': 1})
377 @short_help("turn a directory into a repository")
379 Converts the current working directory into a repository. The primary
380 side-effect of this command is the creation of a '.git' subdirectory.
381 No files are added nor commits made.
384 os
.system("git init")
386 @short_help("add a new file to the repository")
388 The arguments are the files to be added to the repository. Once added,
389 the files will show as "unstaged changes" in the output of 'status'. To
390 reverse the effects of this command, see 'rm'.
392 def cmd_add(self
, *files
):
403 @short_help("delete a file from the repository")
405 The arguments are the files to be removed from the current revision of
406 the repository. The files will still exist in any past commits that the
407 files may have been a part of. The file is not actually deleted, it is
408 just no longer tracked as part of the repository.
410 def cmd_rm(self
, *files
):
420 @short_help("stage changes in a file for commit")
422 The arguments are the files to be staged. Staging changes is a way to
423 build up a commit when you do not want to commit all changes at once.
424 To commit only staged changes, use the '-d' flag to 'commit.' To
425 reverse the effects of this command, see 'unstage'. Once staged, the
426 files will show as "staged changes" in the output of 'status'.
428 def cmd_stage(self
, *files
):
438 @short_help("unstage changes in a file")
440 The arguments are the files to be unstaged. Once unstaged, the files
441 will show as "unstaged changes" in the output of 'status'. The '-a'
442 flag can be used to unstage all staged changes at once.
445 def cmd_unstage(self
, *files
, **flags
):
460 @short_help("show files with staged and unstaged changes")
462 Show the files in the repository with changes since the last commit,
463 categorized based on whether the changes are staged or not. A file may
464 appear under each heading if the same file has both staged and unstaged
467 def cmd_status(self
):
470 branch
= get_output("git symbolic-ref HEAD")
472 branch
= branch
[0].replace('refs/heads/', '')
475 print "Current branch: %s" % branch
477 print "Files with staged changes:"
478 files
= self
._get
_staged
_files
()
484 print "Files with unstaged changes:"
485 files
= self
._get
_unstaged
_files
()
491 files
= self
._get
_unmerged
_files
()
493 print "Files with conflicts:"
497 @short_help("remove uncommitted changes from a file (*)")
499 The arguments are the files whose changes will be reverted. If the '-a'
500 flag is given, then all files will have uncommitted changes removed.
501 Note that there is no way to reverse this command short of manually
502 editing each file again.
505 def cmd_revert(self
, *files
, **flags
):
510 run_safely("git checkout-index -u -f -a")
521 @short_help("record changes to files as a new commit")
523 Create a new commit recording changes since the last commit. If there
524 are only unstaged changes, those will be recorded. If there are only
525 staged changes, those will be recorded. Otherwise, you will have to
526 specify either the '-a' flag or the '-d' flag to commit all changes or
527 only staged changes, respectively. To reverse the effects of this
528 command, see 'uncommit'.
530 @takes_options("adm:")
531 def cmd_commit(self
, **flags
):
532 "[-a | -d] [-m <msg>]"
534 self
._check
_rebasing
()
535 self
._check
_commit
(**flags
)
536 if not self
._get
_staged
_files
():
537 raise YapError("No changes to commit")
538 msg
= flags
.get('-m', None)
542 @short_help("reverse the actions of the last commit")
544 Reverse the effects of the last 'commit' operation. The changes that
545 were part of the previous commit will show as "staged changes" in the
546 output of 'status'. This means that if no files were changed since the
547 last commit was created, 'uncommit' followed by 'commit' is a lossless
550 def cmd_uncommit(self
):
556 @short_help("report the current version of yap")
557 def cmd_version(self
):
558 print "Yap version 0.1"
560 @short_help("show the changelog for particular versions or files")
562 The arguments are the files with which to filter history. If none are
563 given, all changes are listed. Otherwise only commits that affected one
564 or more of the given files are listed. The -r option changes the
565 starting revision for traversing history. By default, history is listed
568 @takes_options("pr:")
569 def cmd_log(self
, *paths
, **flags
):
570 "[-p] [-r <rev>] <path>..."
572 rev
= flags
.get('-r', 'HEAD')
581 paths
= ' '.join(paths
)
582 os
.system("git log -M -C %s %s '%s' -- %s"
583 % (follow
, flags
.get('-p', '--name-status'), rev
, paths
))
585 @short_help("show staged, unstaged, or all uncommitted changes")
587 Show staged, unstaged, or all uncommitted changes. By default, all
588 changes are shown. The '-u' flag causes only unstaged changes to be
589 shown. The '-d' flag causes only staged changes to be shown.
592 def cmd_diff(self
, **flags
):
595 if '-u' in flags
and '-d' in flags
:
596 raise YapError("Conflicting flags: -u and -d")
598 pager
= self
._get
_pager
_cmd
()
601 os
.system("git diff-files -p | %s" % pager
)
603 os
.system("git diff-index --cached -p HEAD | %s" % pager
)
605 os
.system("git diff-index -p HEAD | %s" % pager
)
607 @short_help("list, create, or delete branches")
609 If no arguments are specified, a list of local branches is given. The
610 current branch is indicated by a "*" next to the name. If an argument
611 is given, it is taken as the name of a new branch to create. The branch
612 will start pointing at the current HEAD. See 'point' for details on
613 changing the revision of the new branch. Note that this command does
614 not switch the current working branch. See 'switch' for details on
615 changing the current working branch.
617 The '-d' flag can be used to delete local branches. If the delete
618 operation would remove the last branch reference to a given line of
619 history (colloquially referred to as "dangling commits"), yap will
620 report an error and abort. The '-f' flag can be used to force the delete
623 @takes_options("fd:")
624 def cmd_branch(self
, branch
=None, **flags
):
625 "[ [-f] -d <branch> | <branch> ]"
627 force
= '-f' in flags
629 self
._delete
_branch
(flags
['-d'], force
)
633 if branch
is not None:
634 ref
= get_output("git rev-parse --verify HEAD")
636 raise YapError("No branch point yet. Make a commit")
637 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
639 current
= get_output("git symbolic-ref HEAD")
640 branches
= get_output("git for-each-ref --format='%(refname)' 'refs/heads/*'")
642 if current
and b
== current
[0]:
646 b
= b
.replace('refs/heads/', '')
649 @short_help("change the current working branch")
651 The argument is the name of the branch to make the current working
652 branch. This command will fail if there are uncommitted changes to any
653 files. Otherwise, the contents of the files in the working directory
654 are updated to reflect their state in the new branch. Additionally, any
655 future commits are added to the new branch instead of the previous line
659 def cmd_switch(self
, branch
, **flags
):
662 self
._check
_rebasing
()
663 ref
= get_output("git rev-parse --verify 'refs/heads/%s'" % branch
)
665 raise YapError("No such branch: %s" % branch
)
667 if '-f' not in flags
and (self
._get
_unstaged
_files
() or self
._get
_staged
_files
()):
668 raise YapError("You have uncommitted changes. Use -f to continue anyway")
670 if self
._get
_unstaged
_files
() and self
._get
_staged
_files
():
671 raise YapError("You have staged and unstaged changes. Perhaps unstage -a?")
673 staged
= bool(self
._get
_staged
_files
())
675 run_command("git diff-files -p | git apply --cached")
676 for f
in self
._get
_new
_files
():
679 idx
= get_output("git write-tree")
680 new
= get_output("git rev-parse refs/heads/%s" % branch
)
681 if os
.system("git read-tree --aggressive -u -m HEAD %s %s" % (idx
[0], new
[0])):
682 raise YapError("Failed to switch")
683 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
689 @short_help("move the current branch to a different revision")
691 The argument is the hash of the commit to which the current branch
692 should point, or alternately a branch or tag (a.k.a, "committish"). If
693 moving the branch would create "dangling commits" (see 'branch'), yap
694 will report an error and abort. The '-f' flag can be used to force the
695 operation in spite of this.
698 def cmd_point(self
, where
, **flags
):
701 self
._check
_rebasing
()
703 head
= get_output("git rev-parse --verify HEAD")
705 raise YapError("No commit yet; nowhere to point")
707 ref
= get_output("git rev-parse --verify '%s'" % where
)
709 raise YapError("Not a valid ref: %s" % where
)
711 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
712 raise YapError("You have uncommitted changes. Commit them first")
714 type = get_output("git cat-file -t '%s'" % ref
[0])
715 if type and type[0] == "tag":
716 tag
= get_output("git cat-file tag '%s'" % ref
[0])
717 ref
[0] = tag
[0].split(' ')[1]
719 run_safely("git update-ref HEAD '%s'" % ref
[0])
721 if '-f' not in flags
:
722 name
= get_output("git name-rev --name-only '%s'" % head
[0])[0]
723 if name
== "undefined":
724 os
.system("git update-ref HEAD '%s'" % head
[0])
725 raise YapError("Pointing there will lose commits. Use -f to force")
728 run_safely("git read-tree -u -m HEAD")
730 run_safely("git read-tree HEAD")
731 run_safely("git checkout-index -u -f -a")
733 @short_help("alter history by dropping or amending commits")
735 This command operates in two distinct modes, "amend" and "drop" mode.
736 In drop mode, the given commit is removed from the history of the
737 current branch, as though that commit never happened. By default the
740 In amend mode, the uncommitted changes present are merged into a
741 previous commit. This is useful for correcting typos or adding missed
742 files into past commits. By default the commit used is HEAD.
744 While rewriting history it is possible that conflicts will arise. If
745 this happens, the rewrite will pause and you will be prompted to resolve
746 the conflicts and stage them. Once that is done, you will run "yap
747 history continue." If instead you want the conflicting commit removed
748 from history (perhaps your changes supercede that commit) you can run
749 "yap history skip". Once the rewrite completes, your branch will be on
750 the same commit as when the rewrite started.
752 def cmd_history(self
, subcmd
, *args
):
753 "amend | drop <commit>"
756 if subcmd
not in ("amend", "drop", "continue", "skip"):
760 When you have resolved the conflicts run \"yap history continue\".
761 To skip the problematic patch, run \"yap history skip\"."""
763 if subcmd
== "continue":
764 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
767 os
.system("git reset --hard")
768 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
771 if subcmd
== "amend":
772 flags
, args
= getopt
.getopt(args
, "ad")
782 if run_command("git rev-parse --verify '%s'" % commit
):
783 raise YapError("Not a valid commit: %s" % commit
)
785 self
._check
_rebasing
()
787 if subcmd
== "amend":
788 self
._check
_commit
(**flags
)
789 if self
._get
_unstaged
_files
():
790 # XXX: handle unstaged changes better
791 raise YapError("Commit away changes that you aren't amending")
795 start
= get_output("git rev-parse HEAD")
796 stash
= get_output("git stash create")
797 run_command("git reset --hard")
799 fd
, tmpfile
= tempfile
.mkstemp("yap")
803 os
.system("git format-patch -k --stdout '%s' > %s" % (commit
, tmpfile
))
804 if subcmd
== "amend":
805 self
.cmd_point(commit
, **{'-f': True})
807 if subcmd
== "amend":
809 rc
= os
.system("git stash apply %s" % stash
[0])
811 self
.cmd_point(start
[0], **{'-f': True})
812 os
.system("git stash apply %s" % stash
[0])
813 raise YapError("Failed to apply stash")
816 if subcmd
== "amend":
818 for f
in self
._get
_unstaged
_files
():
822 self
.cmd_point("%s^" % commit
, **{'-f': True})
824 stat
= os
.stat(tmpfile
)
827 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
829 raise YapError("Failed to apply changes")
834 run_command("git stash apply %s" % stash
[0])
837 @short_help("show the changes introduced by a given commit")
839 By default, the changes in the last commit are shown. To override this,
840 specify a hash, branch, or tag (committish). The hash of the commit,
841 the commit's author, log message, and a diff of the changes are shown.
843 def cmd_show(self
, commit
="HEAD"):
846 os
.system("git show '%s'" % commit
)
848 @short_help("apply the changes in a given commit to the current branch")
850 The argument is the hash, branch, or tag (committish) of the commit to
851 be applied. In general, it only makes sense to apply commits that
852 happened on another branch. The '-r' flag can be used to have the
853 changes in the given commit reversed from the current branch. In
854 general, this only makes sense for commits that happened on the current
858 def cmd_cherry_pick(self
, commit
, **flags
):
862 os
.system("git revert '%s'" % commit
)
864 os
.system("git cherry-pick '%s'" % commit
)
866 @short_help("list, add, or delete configured remote repositories")
868 When invoked with no arguments, this command will show the list of
869 currently configured remote repositories, giving both the name and URL
870 of each. To add a new repository, give the desired name as the first
871 argument and the URL as the second. The '-d' flag can be used to remove
872 a previously added repository.
875 def cmd_repo(self
, name
=None, url
=None, **flags
):
876 "[<name> <url> | -d <name>]"
878 if name
is not None and url
is None:
882 if flags
['-d'] not in [ x
[0] for x
in self
._list
_remotes
() ]:
883 raise YapError("No such repository: %s" % flags
['-d'])
884 os
.system("git config --unset remote.%s.url" % flags
['-d'])
885 os
.system("git config --unset remote.%s.fetch" % flags
['-d'])
888 if name
in [ x
[0] for x
in self
._list
_remotes
() ]:
889 raise YapError("Repository '%s' already exists" % flags
['-d'])
890 os
.system("git config remote.%s.url %s" % (name
, url
))
891 os
.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name
, name
))
893 for remote
, url
in self
._list
_remotes
():
894 print "%-20s %s" % (remote
, url
)
896 @short_help("send local commits to a remote repository")
898 When invoked with no arguments, the current branch is synchronized to
899 the tracking branch of the tracking remote. If no tracking remote is
900 specified, the repository will have to be specified on the command line.
901 In that case, the default is to push to a branch with the same name as
902 the current branch. This behavior can be overridden by giving a second
903 argument to specify the remote branch.
905 If the remote branch does not currently exist, the command will abort
906 unless the -c flag is provided. If the remote branch is not a direct
907 descendent of the local branch, the command will abort unless the -f
908 flag is provided. Forcing a push in this way can be problematic to
909 other users of the repository if they are not expecting it.
911 To delete a branch on the remote repository, use the -d flag.
913 @takes_options("cdf")
914 def cmd_push(self
, repo
=None, rhs
=None, **flags
):
917 if '-c' in flags
and '-d' in flags
:
920 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
921 raise YapError("No such repository: %s" % repo
)
923 current
= get_output("git symbolic-ref HEAD")
925 raise YapError("Not on a branch!")
927 self
._check
_rebasing
()
929 current
= current
[0].replace('refs/heads/', '')
930 remote
= get_output("git config branch.%s.remote" % current
)
931 if repo
is None and remote
:
935 raise YapError("No tracking branch configured; specify destination repository")
937 if rhs
is None and remote
and remote
[0] == repo
:
938 merge
= get_output("git config branch.%s.merge" % current
)
943 rhs
= "refs/heads/%s" % current
945 if '-c' not in flags
and '-d' not in flags
:
946 if run_command("git rev-parse --verify refs/remotes/%s/%s"
947 % (repo
, rhs
.replace('refs/heads/', ''))):
948 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
949 if '-f' not in flags
:
950 hash = get_output("git rev-parse refs/remotes/%s/%s" % (repo
, rhs
.replace('refs/heads/', '')))
951 base
= get_output("git merge-base HEAD %s" % hash[0])
953 if base
[0] != hash[0]:
954 raise YapError("Branch not up-to-date with remote. Update or use -f")
956 print "About to push local branch '%s' to '%s' on '%s'" % (current
, rhs
, repo
)
957 print "Continue (y/n)? ",
959 ans
= sys
.stdin
.readline().strip()
961 if ans
.lower() != 'y' and ans
.lower() != 'yes':
962 raise YapError("Aborted.")
970 lhs
= "refs/heads/%s" % current
971 rc
= os
.system("git push %s %s %s:%s" % (flags
.get('-f', ''), repo
, lhs
, rhs
))
973 raise YapError("Push failed.")
975 @short_help("retrieve commits from a remote repository")
977 When run with no arguments, the command will retrieve new commits from
978 the remote tracking repository. Note that this does not in any way
979 alter the current branch. For that, see "update". If a remote other
980 than the tracking remote is desired, it can be specified as the first
983 def cmd_fetch(self
, repo
=None):
986 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
987 raise YapError("No such repository: %s" % repo
)
989 remote
= get_output("git config branch.%s.remote" % current
)
992 raise YapError("No tracking branch configured; specify a repository")
993 os
.system("git fetch %s" % repo
)
995 @short_help("update the current branch relative to its tracking branch")
997 Updates the current branch relative to its remote tracking branch. This
998 command requires that the current branch have a remote tracking branch
999 configured. If any conflicts occur while applying your changes to the
1000 updated remote, the command will pause to allow you to fix them. Once
1001 that is done, run "update" with the "continue" subcommand. Alternately,
1002 the "skip" subcommand can be used to discard the conflicting changes.
1004 def cmd_update(self
, subcmd
=None):
1007 if subcmd
and subcmd
not in ["continue", "skip"]:
1011 When you have resolved the conflicts run \"yap update continue\".
1012 To skip the problematic patch, run \"yap update skip\"."""
1014 if subcmd
== "continue":
1015 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
1017 if subcmd
== "skip":
1018 os
.system("git reset --hard")
1019 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
1022 self
._check
_rebasing
()
1023 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
1024 raise YapError("You have uncommitted changes. Commit them first")
1026 current
= get_output("git symbolic-ref HEAD")
1028 raise YapError("Not on a branch!")
1030 current
= current
[0].replace('refs/heads/', '')
1031 remote
, merge
= self
._get
_tracking
(current
)
1032 merge
= merge
[0].replace('refs/heads/', '')
1034 self
.cmd_fetch(remote
)
1035 base
= get_output("git merge-base HEAD refs/remotes/%s/%s" % (remote
, merge
))
1038 fd
, tmpfile
= tempfile
.mkstemp("yap")
1040 os
.system("git format-patch -k --stdout '%s' > %s" % (base
[0], tmpfile
))
1041 self
.cmd_point("refs/remotes/%s/%s" % (remote
, merge
), **{'-f': True})
1043 stat
= os
.stat(tmpfile
)
1046 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
1048 raise YapError("Failed to apply changes")
1052 @short_help("query and configure remote branch tracking")
1054 When invoked with no arguments, the command displays the tracking
1055 information for the current branch. To configure the tracking
1056 information, two arguments for the remote repository and remote branch
1057 are given. The tracking information is used to provide defaults for
1058 where to push local changes and from where to get updates to the branch.
1060 def cmd_track(self
, repo
=None, branch
=None):
1064 current
= get_output("git symbolic-ref HEAD")
1066 raise YapError("Not on a branch!")
1067 current
= current
[0].replace('refs/heads/', '')
1069 if repo
is None and branch
is None:
1070 repo
, merge
= self
._get
_tracking
(current
)
1071 merge
= merge
[0].replace('refs/heads/', '')
1072 print "Branch '%s' tracking refs/remotes/%s/%s" % (current
, repo
, merge
)
1075 if repo
is None or branch
is None:
1078 if repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1079 raise YapError("No such repository: %s" % repo
)
1081 if run_command("git rev-parse --verify refs/remotes/%s/%s" % (repo
, branch
)):
1082 raise YapError("No such branch '%s' on repository '%s'" % (repo
, branch
))
1084 os
.system("git config branch.%s.remote '%s'" % (current
, repo
))
1085 os
.system("git config branch.%s.merge 'refs/heads/%s'" % (current
, branch
))
1086 print "Branch '%s' now tracking refs/remotes/%s/%s" % (current
, repo
, branch
)
1088 @short_help("mark files with conflicts as resolved")
1090 The arguments are the files to be marked resolved. When a conflict
1091 occurs while merging changes to a file, that file is marked as
1092 "unmerged." Until the file(s) with conflicts are marked resolved,
1093 commits cannot be made.
1095 def cmd_resolved(self
, *args
):
1102 self
._stage
_one
(f
, True)
1105 @short_help("show information about loaded plugins")
1106 def cmd_plugins(self
):
1108 if not self
.plugins
:
1109 print >>sys
.stderr
, "No plugins loaded."
1110 for k
, v
in self
.plugins
.items():
1113 doc
= "No description"
1114 print "%-20s%s" % (k
, doc
)
1117 if not func
.startswith('cmd_'):
1120 print "\tOverrides:"
1124 def cmd_help(self
, cmd
=None):
1127 attr
= self
.__getattribute
__("cmd_"+cmd
.replace('-', '_'))
1128 except AttributeError:
1129 raise YapError("No such command: %s" % cmd
)
1131 help = attr
.long_help
1132 except AttributeError:
1133 raise YapError("Sorry, no help for '%s'. Ask Steven." % cmd
)
1135 print >>sys
.stderr
, "The '%s' command" % cmd
1136 print >>sys
.stderr
, "\tyap %s %s" % (cmd
, attr
.__doc
__)
1137 print >>sys
.stderr
, "%s" % help
1140 print >> sys
.stderr
, "Yet Another (Git) Porcelein"
1143 for name
in dir(self
):
1144 if not name
.startswith('cmd_'):
1146 attr
= self
.__getattribute
__(name
)
1147 if not callable(attr
):
1150 short_msg
= attr
.short_help
1151 except AttributeError:
1154 name
= name
.replace('cmd_', '')
1155 name
= name
.replace('_', '-')
1156 print >> sys
.stderr
, "%-16s%s" % (name
, short_msg
)
1158 print >> sys
.stderr
, "(*) Indicates that the command is not readily reversible"
1160 def cmd_usage(self
):
1161 print >> sys
.stderr
, "usage: %s <command>" % os
.path
.basename(sys
.argv
[0])
1162 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 plugins version"
1164 def main(self
, args
):
1172 if run_command("git --version"):
1173 print >>sys
.stderr
, "Failed to run git; is it installed?"
1176 debug
= os
.getenv('YAP_DEBUG')
1179 command
= command
.replace('-', '_')
1182 for p
in self
.plugins
.values():
1184 meth
= p
.__getattribute
__("cmd_"+command
)
1185 except AttributeError:
1189 default_meth
= self
.__getattribute
__("cmd_"+command
)
1190 except AttributeError:
1196 raise AttributeError
1198 if meth
.__doc
__ is None:
1199 doc
= default_meth
.__doc
__
1204 if "options" in meth
.__dict
__:
1205 options
= meth
.options
1206 if default_meth
and "options" in default_meth
.__dict
__:
1207 options
+= default_meth
.options
1208 flags
, args
= getopt
.getopt(args
, options
)
1214 for p
in self
.plugins
.values():
1216 meth
= p
.__getattribute
__("pre_"+command
)
1217 except AttributeError:
1219 meth(*args
, **flags
)
1221 meth(*args
, **flags
)
1224 for p
in self
.plugins
.values():
1226 meth
= p
.__getattribute
__("post_"+command
)
1227 except AttributeError:
1231 except (TypeError, getopt
.GetoptError
):
1234 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1236 print >> sys
.stderr
, e
1238 except AttributeError: