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')
86 pickle
.dump(files
, open(path
, 'w'))
90 def _clear_new_files(self
):
91 repo
= get_output('git rev-parse --git-dir')[0]
92 path
= os
.path
.join(repo
, 'yap', 'new-files')
95 def _assert_file_exists(self
, file):
96 if not os
.access(file, os
.R_OK
):
97 raise YapError("No such file: %s" % file)
99 def _get_staged_files(self
):
100 if run_command("git rev-parse HEAD"):
101 files
= get_output("git ls-files --cached")
103 files
= get_output("git diff-index --cached --name-only HEAD")
104 unmerged
= self
._get
_unmerged
_files
()
106 unmerged
= set(unmerged
)
107 files
= set(files
).difference(unmerged
)
111 def _get_unstaged_files(self
):
112 files
= get_output("git ls-files -m")
113 prefix
= get_output("git rev-parse --show-prefix")
115 files
= [ os
.path
.join(prefix
[0], x
) for x
in files
]
116 files
+= self
._get
_new
_files
()
117 unmerged
= self
._get
_unmerged
_files
()
119 unmerged
= set(unmerged
)
120 files
= set(files
).difference(unmerged
)
124 def _get_unmerged_files(self
):
125 files
= get_output("git ls-files -u")
126 files
= [ x
.replace('\t', ' ').split(' ')[3] for x
in files
]
127 prefix
= get_output("git rev-parse --show-prefix")
129 files
= [ os
.path
.join(prefix
[0], x
) for x
in files
]
130 return list(set(files
))
132 def _delete_branch(self
, branch
, force
):
133 current
= get_output("git symbolic-ref HEAD")
135 current
= current
[0].replace('refs/heads/', '')
136 if branch
== current
:
137 raise YapError("Can't delete current branch")
139 ref
= get_output("git rev-parse --verify 'refs/heads/%s'" % branch
)
141 raise YapError("No such branch: %s" % branch
)
142 run_safely("git update-ref -d 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
145 name
= get_output("git name-rev --name-only '%s'" % ref
[0])[0]
146 if name
== 'undefined':
147 run_command("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
148 raise YapError("Refusing to delete leaf branch (use -f to force)")
149 def _get_pager_cmd(self
):
150 if 'YAP_PAGER' in os
.environ
:
151 return os
.environ
['YAP_PAGER']
152 elif 'GIT_PAGER' in os
.environ
:
153 return os
.environ
['GIT_PAGER']
154 elif 'PAGER' in os
.environ
:
155 return os
.environ
['PAGER']
159 def _add_one(self
, file):
160 self
._assert
_file
_exists
(file)
161 x
= get_output("git ls-files '%s'" % file)
163 raise YapError("File '%s' already in repository" % file)
164 self
._add
_new
_file
(file)
166 def _rm_one(self
, file):
167 self
._assert
_file
_exists
(file)
168 if get_output("git ls-files '%s'" % file) != []:
169 run_safely("git rm --cached '%s'" % file)
170 self
._remove
_new
_file
(file)
172 def _stage_one(self
, file, allow_unmerged
=False):
173 self
._assert
_file
_exists
(file)
174 prefix
= get_output("git rev-parse --show-prefix")
176 tmp
= os
.path
.normpath(os
.path
.join(prefix
[0], file))
179 if not allow_unmerged
and tmp
in self
._get
_unmerged
_files
():
180 raise YapError("Refusing to stage conflicted file: %s" % file)
181 run_safely("git update-index --add '%s'" % file)
183 def _unstage_one(self
, file):
184 self
._assert
_file
_exists
(file)
185 if run_command("git rev-parse HEAD"):
186 rc
= run_command("git update-index --force-remove '%s'" % file)
188 rc
= run_command("git diff-index -p HEAD '%s' | git apply -R --cached" % file)
190 raise YapError("Failed to unstage")
192 def _revert_one(self
, file):
193 self
._assert
_file
_exists
(file)
195 self
._unstage
_one
(file)
198 run_safely("git checkout-index -u -f '%s'" % file)
200 def _parse_commit(self
, commit
):
201 lines
= get_output("git cat-file commit '%s'" % commit
)
206 if mode
!= 'commit' and l
.strip() == "":
211 commit
['log'].append(l
)
218 commit
['log'] = '\n'.join(commit
['log'])
221 def _check_commit(self
, **flags
):
222 if '-a' in flags
and '-d' in flags
:
223 raise YapError("Conflicting flags: -a and -d")
225 if '-d' not in flags
and self
._get
_unstaged
_files
():
226 if '-a' not in flags
and self
._get
_staged
_files
():
227 raise YapError("Staged and unstaged changes present. Specify what to commit")
228 os
.system("git diff-files -p | git apply --cached")
229 for f
in self
._get
_new
_files
():
232 def _do_uncommit(self
):
233 commit
= self
._parse
_commit
("HEAD")
234 repo
= get_output('git rev-parse --git-dir')[0]
235 dir = os
.path
.join(repo
, 'yap')
240 msg_file
= os
.path
.join(dir, 'msg')
241 fd
= file(msg_file
, 'w')
242 print >>fd
, commit
['log']
245 tree
= get_output("git rev-parse --verify HEAD^")
246 run_safely("git update-ref -m uncommit HEAD '%s'" % tree
[0])
248 def _do_commit(self
, msg
=None):
249 tree
= get_output("git write-tree")[0]
250 parent
= get_output("git rev-parse --verify HEAD 2> /dev/null")
252 if os
.environ
.has_key('YAP_EDITOR'):
253 editor
= os
.environ
['YAP_EDITOR']
254 elif os
.environ
.has_key('GIT_EDITOR'):
255 editor
= os
.environ
['GIT_EDITOR']
256 elif os
.environ
.has_key('EDITOR'):
257 editor
= os
.environ
['EDITOR']
261 fd
, tmpfile
= tempfile
.mkstemp("yap")
266 repo
= get_output('git rev-parse --git-dir')[0]
267 msg_file
= os
.path
.join(repo
, 'yap', 'msg')
268 if os
.access(msg_file
, os
.R_OK
):
270 fd2
= file(tmpfile
, 'w')
271 for l
in fd1
.xreadlines():
272 print >>fd2
, l
.strip()
275 if os
.system("%s '%s'" % (editor
, tmpfile
)) != 0:
276 raise YapError("Editing commit message failed")
283 raise YapError("Refusing to use empty commit message")
285 (fd_w
, fd_r
) = os
.popen2("git stripspace > %s" % tmpfile
)
291 commit
= get_output("git commit-tree '%s' -p '%s' < '%s'" % (tree
, parent
[0], tmpfile
))
293 commit
= get_output("git commit-tree '%s' < '%s'" % (tree
, tmpfile
))
296 run_safely("git update-ref HEAD '%s'" % commit
[0])
298 def _check_rebasing(self
):
299 repo
= get_output('git rev-parse --git-dir')[0]
300 dotest
= os
.path
.join(repo
, '.dotest')
301 if os
.access(dotest
, os
.R_OK
):
302 raise YapError("A git operation is in progress. Complete it first")
303 dotest
= os
.path
.join(repo
, '..', '.dotest')
304 if os
.access(dotest
, os
.R_OK
):
305 raise YapError("A git operation is in progress. Complete it first")
307 def _check_git(self
):
308 if run_command("git rev-parse --git-dir"):
309 raise YapError("That command must be run from inside a git repository")
311 def _list_remotes(self
):
312 remotes
= get_output("git config --get-regexp '^remote.*.url'")
314 remote
, url
= x
.split(' ')
315 remote
= remote
.replace('remote.', '')
316 remote
= remote
.replace('.url', '')
319 def _unstage_all(self
):
321 run_safely("git read-tree -m HEAD")
323 run_safely("git read-tree HEAD")
324 run_safely("git update-index -q --refresh")
326 def _get_tracking(self
, current
):
327 remote
= get_output("git config branch.%s.remote" % current
)
329 raise YapError("No tracking branch configured for '%s'" % current
)
331 merge
= get_output("git config branch.%s.merge" % current
)
333 raise YapError("No tracking branch configured for '%s'" % current
)
334 return remote
[0], merge
[0]
336 def __getattribute__(self
, attr
):
337 if attr
.startswith("cmd_"):
339 for p
in self
.plugins
.values():
341 meth
= p
.__getattribute
__(attr
)
343 except AttributeError:
348 return super(Yap
, self
).__getattribute
__(attr
)
350 def _call_base(self
, method
, *args
, **flags
):
351 base_method
= super(Yap
, self
).__getattribute
__(method
)
352 return base_method(*args
, **flags
)
353 def _confirm_push(self
, current
, rhs
, repo
):
354 print "About to push local branch '%s' to '%s' on '%s'" % (current
, rhs
, repo
)
355 print "Continue (y/n)? ",
357 ans
= sys
.stdin
.readline().strip()
359 if ans
.lower() != 'y' and ans
.lower() != 'yes':
360 raise YapError("Aborted.")
362 @short_help("make a local copy of an existing repository")
364 The first argument is a URL to the existing repository. This can be an
365 absolute path if the repository is local, or a URL with the git://,
366 ssh://, or http:// schemes. By default, the directory used is the last
367 component of the URL, sans '.git'. This can be overridden by providing
370 def cmd_clone(self
, url
, directory
=None):
373 if '://' not in url
and url
[0] != '/':
374 url
= os
.path
.join(os
.getcwd(), url
)
376 url
= url
.rstrip('/')
377 if directory
is None:
378 directory
= url
.rsplit('/')[-1]
379 directory
= directory
.replace('.git', '')
384 raise YapError("Directory exists: %s" % directory
)
387 self
.cmd_repo("origin", url
)
388 self
.cmd_fetch("origin")
391 if not run_command("git rev-parse --verify refs/remotes/origin/HEAD"):
392 hash = get_output("git rev-parse refs/remotes/origin/HEAD")[0]
393 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'"):
394 if get_output("git rev-parse %s" % b
)[0] == hash:
398 if not run_command("git rev-parse --verify refs/remotes/origin/master"):
399 branch
= "refs/remotes/origin/master"
401 branch
= get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'")
404 hash = get_output("git rev-parse %s" % branch
)
406 branch
= branch
.replace('refs/remotes/origin/', '')
407 run_safely("git update-ref refs/heads/%s %s" % (branch
, hash[0]))
408 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
409 self
.cmd_revert(**{'-a': 1})
411 @short_help("turn a directory into a repository")
413 Converts the current working directory into a repository. The primary
414 side-effect of this command is the creation of a '.git' subdirectory.
415 No files are added nor commits made.
418 os
.system("git init")
420 @short_help("add a new file to the repository")
422 The arguments are the files to be added to the repository. Once added,
423 the files will show as "unstaged changes" in the output of 'status'. To
424 reverse the effects of this command, see 'rm'.
426 def cmd_add(self
, *files
):
437 @short_help("delete a file from the repository")
439 The arguments are the files to be removed from the current revision of
440 the repository. The files will still exist in any past commits that the
441 files may have been a part of. The file is not actually deleted, it is
442 just no longer tracked as part of the repository.
444 def cmd_rm(self
, *files
):
454 @short_help("stage changes in a file for commit")
456 The arguments are the files to be staged. Staging changes is a way to
457 build up a commit when you do not want to commit all changes at once.
458 To commit only staged changes, use the '-d' flag to 'commit.' To
459 reverse the effects of this command, see 'unstage'. Once staged, the
460 files will show as "staged changes" in the output of 'status'.
462 def cmd_stage(self
, *files
):
472 @short_help("unstage changes in a file")
474 The arguments are the files to be unstaged. Once unstaged, the files
475 will show as "unstaged changes" in the output of 'status'. The '-a'
476 flag can be used to unstage all staged changes at once.
479 def cmd_unstage(self
, *files
, **flags
):
494 @short_help("show files with staged and unstaged changes")
496 Show the files in the repository with changes since the last commit,
497 categorized based on whether the changes are staged or not. A file may
498 appear under each heading if the same file has both staged and unstaged
501 def cmd_status(self
):
504 branch
= get_output("git symbolic-ref HEAD")
506 branch
= branch
[0].replace('refs/heads/', '')
509 print "Current branch: %s" % branch
511 print "Files with staged changes:"
512 files
= self
._get
_staged
_files
()
518 print "Files with unstaged changes:"
519 files
= self
._get
_unstaged
_files
()
525 files
= self
._get
_unmerged
_files
()
527 print "Files with conflicts:"
531 @short_help("remove uncommitted changes from a file (*)")
533 The arguments are the files whose changes will be reverted. If the '-a'
534 flag is given, then all files will have uncommitted changes removed.
535 Note that there is no way to reverse this command short of manually
536 editing each file again.
539 def cmd_revert(self
, *files
, **flags
):
544 run_safely("git checkout-index -u -f -a")
555 @short_help("record changes to files as a new commit")
557 Create a new commit recording changes since the last commit. If there
558 are only unstaged changes, those will be recorded. If there are only
559 staged changes, those will be recorded. Otherwise, you will have to
560 specify either the '-a' flag or the '-d' flag to commit all changes or
561 only staged changes, respectively. To reverse the effects of this
562 command, see 'uncommit'.
564 @takes_options("adm:")
565 def cmd_commit(self
, **flags
):
566 "[-a | -d] [-m <msg>]"
568 self
._check
_rebasing
()
569 self
._check
_commit
(**flags
)
570 if not self
._get
_staged
_files
():
571 raise YapError("No changes to commit")
572 msg
= flags
.get('-m', None)
576 @short_help("reverse the actions of the last commit")
578 Reverse the effects of the last 'commit' operation. The changes that
579 were part of the previous commit will show as "staged changes" in the
580 output of 'status'. This means that if no files were changed since the
581 last commit was created, 'uncommit' followed by 'commit' is a lossless
584 def cmd_uncommit(self
):
590 @short_help("report the current version of yap")
591 def cmd_version(self
):
592 print "Yap version 0.1"
594 @short_help("show the changelog for particular versions or files")
596 The arguments are the files with which to filter history. If none are
597 given, all changes are listed. Otherwise only commits that affected one
598 or more of the given files are listed. The -r option changes the
599 starting revision for traversing history. By default, history is listed
602 @takes_options("pr:")
603 def cmd_log(self
, *paths
, **flags
):
604 "[-p] [-r <rev>] <path>..."
606 rev
= flags
.get('-r', 'HEAD')
615 paths
= ' '.join(paths
)
616 os
.system("git log -M -C %s %s '%s' -- %s"
617 % (follow
, flags
.get('-p', '--name-status'), rev
, paths
))
619 @short_help("show staged, unstaged, or all uncommitted changes")
621 Show staged, unstaged, or all uncommitted changes. By default, all
622 changes are shown. The '-u' flag causes only unstaged changes to be
623 shown. The '-d' flag causes only staged changes to be shown.
626 def cmd_diff(self
, **flags
):
629 if '-u' in flags
and '-d' in flags
:
630 raise YapError("Conflicting flags: -u and -d")
632 pager
= self
._get
_pager
_cmd
()
635 os
.system("git diff-files -p | %s" % pager
)
637 os
.system("git diff-index --cached -p HEAD | %s" % pager
)
639 os
.system("git diff-index -p HEAD | %s" % pager
)
641 @short_help("list, create, or delete branches")
643 If no arguments are specified, a list of local branches is given. The
644 current branch is indicated by a "*" next to the name. If an argument
645 is given, it is taken as the name of a new branch to create. The branch
646 will start pointing at the current HEAD. See 'point' for details on
647 changing the revision of the new branch. Note that this command does
648 not switch the current working branch. See 'switch' for details on
649 changing the current working branch.
651 The '-d' flag can be used to delete local branches. If the delete
652 operation would remove the last branch reference to a given line of
653 history (colloquially referred to as "dangling commits"), yap will
654 report an error and abort. The '-f' flag can be used to force the delete
657 @takes_options("fd:")
658 def cmd_branch(self
, branch
=None, **flags
):
659 "[ [-f] -d <branch> | <branch> ]"
661 force
= '-f' in flags
663 self
._delete
_branch
(flags
['-d'], force
)
667 if branch
is not None:
668 ref
= get_output("git rev-parse --verify HEAD")
670 raise YapError("No branch point yet. Make a commit")
671 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
673 current
= get_output("git symbolic-ref HEAD")
674 branches
= get_output("git for-each-ref --format='%(refname)' 'refs/heads/*'")
676 if current
and b
== current
[0]:
680 b
= b
.replace('refs/heads/', '')
683 @short_help("change the current working branch")
685 The argument is the name of the branch to make the current working
686 branch. This command will fail if there are uncommitted changes to any
687 files. Otherwise, the contents of the files in the working directory
688 are updated to reflect their state in the new branch. Additionally, any
689 future commits are added to the new branch instead of the previous line
693 def cmd_switch(self
, branch
, **flags
):
696 self
._check
_rebasing
()
697 ref
= get_output("git rev-parse --verify 'refs/heads/%s'" % branch
)
699 raise YapError("No such branch: %s" % branch
)
701 if '-f' not in flags
:
702 if (self
._get
_staged
_files
()
703 or (self
._get
_unstaged
_files
()
704 and run_command("git update-index --refresh"))):
705 raise YapError("You have uncommitted changes. Use -f to continue anyway")
707 if self
._get
_unstaged
_files
() and self
._get
_staged
_files
():
708 raise YapError("You have staged and unstaged changes. Perhaps unstage -a?")
710 staged
= bool(self
._get
_staged
_files
())
712 run_command("git diff-files -p | git apply --cached")
713 for f
in self
._get
_new
_files
():
716 idx
= get_output("git write-tree")
717 new
= get_output("git rev-parse refs/heads/%s" % branch
)
718 readtree
= "git read-tree --aggressive -u -m HEAD %s %s" % (idx
[0], new
[0])
719 if run_command(readtree
):
720 run_command("git update-index --refresh")
721 if os
.system(readtree
):
722 raise YapError("Failed to switch")
723 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
729 @short_help("move the current branch to a different revision")
731 The argument is the hash of the commit to which the current branch
732 should point, or alternately a branch or tag (a.k.a, "committish"). If
733 moving the branch would create "dangling commits" (see 'branch'), yap
734 will report an error and abort. The '-f' flag can be used to force the
735 operation in spite of this.
738 def cmd_point(self
, where
, **flags
):
741 self
._check
_rebasing
()
743 head
= get_output("git rev-parse --verify HEAD")
745 raise YapError("No commit yet; nowhere to point")
747 ref
= get_output("git rev-parse --verify '%s^{commit}'" % where
)
749 raise YapError("Not a valid ref: %s" % where
)
751 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
752 raise YapError("You have uncommitted changes. Commit them first")
754 run_safely("git update-ref HEAD '%s'" % ref
[0])
756 if '-f' not in flags
:
757 name
= get_output("git name-rev --name-only '%s'" % head
[0])[0]
758 if name
== "undefined":
759 os
.system("git update-ref HEAD '%s'" % head
[0])
760 raise YapError("Pointing there will lose commits. Use -f to force")
763 run_safely("git read-tree -u -m HEAD")
765 run_safely("git read-tree HEAD")
766 run_safely("git checkout-index -u -f -a")
768 @short_help("alter history by dropping or amending commits")
770 This command operates in two distinct modes, "amend" and "drop" mode.
771 In drop mode, the given commit is removed from the history of the
772 current branch, as though that commit never happened. By default the
775 In amend mode, the uncommitted changes present are merged into a
776 previous commit. This is useful for correcting typos or adding missed
777 files into past commits. By default the commit used is HEAD.
779 While rewriting history it is possible that conflicts will arise. If
780 this happens, the rewrite will pause and you will be prompted to resolve
781 the conflicts and stage them. Once that is done, you will run "yap
782 history continue." If instead you want the conflicting commit removed
783 from history (perhaps your changes supercede that commit) you can run
784 "yap history skip". Once the rewrite completes, your branch will be on
785 the same commit as when the rewrite started.
787 def cmd_history(self
, subcmd
, *args
):
788 "amend | drop <commit>"
791 if subcmd
not in ("amend", "drop", "continue", "skip"):
795 When you have resolved the conflicts run \"yap history continue\".
796 To skip the problematic patch, run \"yap history skip\"."""
798 if subcmd
== "continue":
799 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
802 os
.system("git reset --hard")
803 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
806 if subcmd
== "amend":
807 flags
, args
= getopt
.getopt(args
, "ad")
817 if run_command("git rev-parse --verify '%s'" % commit
):
818 raise YapError("Not a valid commit: %s" % commit
)
820 self
._check
_rebasing
()
822 if subcmd
== "amend":
823 self
._check
_commit
(**flags
)
824 if self
._get
_unstaged
_files
():
825 # XXX: handle unstaged changes better
826 raise YapError("Commit away changes that you aren't amending")
830 start
= get_output("git rev-parse HEAD")
831 stash
= get_output("git stash create")
832 run_command("git reset --hard")
834 fd
, tmpfile
= tempfile
.mkstemp("yap")
838 os
.system("git format-patch -k --stdout '%s' > %s" % (commit
, tmpfile
))
839 if subcmd
== "amend":
840 self
.cmd_point(commit
, **{'-f': True})
842 if subcmd
== "amend":
844 rc
= os
.system("git stash apply %s" % stash
[0])
846 self
.cmd_point(start
[0], **{'-f': True})
847 os
.system("git stash apply %s" % stash
[0])
848 raise YapError("Failed to apply stash")
851 if subcmd
== "amend":
853 self
._check
_commit
(**{'-a': True})
856 self
.cmd_point("%s^" % commit
, **{'-f': True})
858 stat
= os
.stat(tmpfile
)
861 run_safely("git update-index --refresh")
862 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
864 raise YapError("Failed to apply changes")
869 run_command("git stash apply %s" % stash
[0])
872 @short_help("show the changes introduced by a given commit")
874 By default, the changes in the last commit are shown. To override this,
875 specify a hash, branch, or tag (committish). The hash of the commit,
876 the commit's author, log message, and a diff of the changes are shown.
878 def cmd_show(self
, commit
="HEAD"):
881 os
.system("git show '%s'" % commit
)
883 @short_help("apply the changes in a given commit to the current branch")
885 The argument is the hash, branch, or tag (committish) of the commit to
886 be applied. In general, it only makes sense to apply commits that
887 happened on another branch. The '-r' flag can be used to have the
888 changes in the given commit reversed from the current branch. In
889 general, this only makes sense for commits that happened on the current
893 def cmd_cherry_pick(self
, commit
, **flags
):
897 os
.system("git revert '%s'" % commit
)
899 os
.system("git cherry-pick '%s'" % commit
)
901 @short_help("list, add, or delete configured remote repositories")
903 When invoked with no arguments, this command will show the list of
904 currently configured remote repositories, giving both the name and URL
905 of each. To add a new repository, give the desired name as the first
906 argument and the URL as the second. The '-d' flag can be used to remove
907 a previously added repository.
910 def cmd_repo(self
, name
=None, url
=None, **flags
):
911 "[<name> <url> | -d <name>]"
913 if name
is not None and url
is None:
917 if flags
['-d'] not in [ x
[0] for x
in self
._list
_remotes
() ]:
918 raise YapError("No such repository: %s" % flags
['-d'])
919 os
.system("git config --unset remote.%s.url" % flags
['-d'])
920 os
.system("git config --unset remote.%s.fetch" % flags
['-d'])
923 if name
in [ x
[0] for x
in self
._list
_remotes
() ]:
924 raise YapError("Repository '%s' already exists" % flags
['-d'])
925 os
.system("git config remote.%s.url %s" % (name
, url
))
926 os
.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name
, name
))
928 for remote
, url
in self
._list
_remotes
():
929 print "%-20s %s" % (remote
, url
)
931 @short_help("send local commits to a remote repository (*)")
933 When invoked with no arguments, the current branch is synchronized to
934 the tracking branch of the tracking remote. If no tracking remote is
935 specified, the repository will have to be specified on the command line.
936 In that case, the default is to push to a branch with the same name as
937 the current branch. This behavior can be overridden by giving a second
938 argument to specify the remote branch.
940 If the remote branch does not currently exist, the command will abort
941 unless the -c flag is provided. If the remote branch is not a direct
942 descendent of the local branch, the command will abort unless the -f
943 flag is provided. Forcing a push in this way can be problematic to
944 other users of the repository if they are not expecting it.
946 To delete a branch on the remote repository, use the -d flag.
948 @takes_options("cdf")
949 def cmd_push(self
, repo
=None, rhs
=None, **flags
):
952 if '-c' in flags
and '-d' in flags
:
955 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
956 raise YapError("No such repository: %s" % repo
)
958 current
= get_output("git symbolic-ref HEAD")
960 raise YapError("Not on a branch!")
962 self
._check
_rebasing
()
964 current
= current
[0].replace('refs/heads/', '')
965 remote
= get_output("git config branch.%s.remote" % current
)
966 if repo
is None and remote
:
970 raise YapError("No tracking branch configured; specify destination repository")
972 if rhs
is None and remote
and remote
[0] == repo
:
973 merge
= get_output("git config branch.%s.merge" % current
)
978 rhs
= "refs/heads/%s" % current
980 if '-c' not in flags
and '-d' not in flags
:
981 if run_command("git rev-parse --verify refs/remotes/%s/%s"
982 % (repo
, rhs
.replace('refs/heads/', ''))):
983 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
984 if '-f' not in flags
:
985 hash = get_output("git rev-parse refs/remotes/%s/%s" % (repo
, rhs
.replace('refs/heads/', '')))
986 base
= get_output("git merge-base HEAD %s" % hash[0])
988 if base
[0] != hash[0]:
989 raise YapError("Branch not up-to-date with remote. Update or use -f")
991 self
._confirm
_push
(current
, rhs
, repo
)
998 lhs
= "refs/heads/%s" % current
999 rc
= os
.system("git push %s %s %s:%s" % (flags
.get('-f', ''), repo
, lhs
, rhs
))
1001 raise YapError("Push failed.")
1003 @short_help("retrieve commits from a remote repository")
1005 When run with no arguments, the command will retrieve new commits from
1006 the remote tracking repository. Note that this does not in any way
1007 alter the current branch. For that, see "update". If a remote other
1008 than the tracking remote is desired, it can be specified as the first
1011 def cmd_fetch(self
, repo
=None):
1014 current
= get_output("git symbolic-ref HEAD")
1016 raise YapError("Not on a branch!")
1018 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1019 raise YapError("No such repository: %s" % repo
)
1021 current
= current
[0].replace('refs/heads/', '')
1022 remote
= get_output("git config branch.%s.remote" % current
)
1026 raise YapError("No tracking branch configured; specify a repository")
1027 os
.system("git fetch %s" % repo
)
1029 @short_help("update the current branch relative to its tracking branch")
1031 Updates the current branch relative to its remote tracking branch. This
1032 command requires that the current branch have a remote tracking branch
1033 configured. If any conflicts occur while applying your changes to the
1034 updated remote, the command will pause to allow you to fix them. Once
1035 that is done, run "update" with the "continue" subcommand. Alternately,
1036 the "skip" subcommand can be used to discard the conflicting changes.
1038 def cmd_update(self
, subcmd
=None):
1041 if subcmd
and subcmd
not in ["continue", "skip"]:
1045 When you have resolved the conflicts run \"yap update continue\".
1046 To skip the problematic patch, run \"yap update skip\"."""
1048 if subcmd
== "continue":
1049 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
1051 if subcmd
== "skip":
1052 os
.system("git reset --hard")
1053 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
1056 self
._check
_rebasing
()
1057 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
1058 raise YapError("You have uncommitted changes. Commit them first")
1060 current
= get_output("git symbolic-ref HEAD")
1062 raise YapError("Not on a branch!")
1064 current
= current
[0].replace('refs/heads/', '')
1065 remote
, merge
= self
._get
_tracking
(current
)
1066 merge
= merge
.replace('refs/heads/', '')
1068 self
.cmd_fetch(remote
)
1069 base
= get_output("git merge-base HEAD refs/remotes/%s/%s" % (remote
, merge
))
1072 fd
, tmpfile
= tempfile
.mkstemp("yap")
1074 os
.system("git format-patch -k --stdout '%s' > %s" % (base
[0], tmpfile
))
1075 self
.cmd_point("refs/remotes/%s/%s" % (remote
, merge
), **{'-f': True})
1077 stat
= os
.stat(tmpfile
)
1080 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
1082 raise YapError("Failed to apply changes")
1086 @short_help("query and configure remote branch tracking")
1088 When invoked with no arguments, the command displays the tracking
1089 information for the current branch. To configure the tracking
1090 information, two arguments for the remote repository and remote branch
1091 are given. The tracking information is used to provide defaults for
1092 where to push local changes and from where to get updates to the branch.
1094 def cmd_track(self
, repo
=None, branch
=None):
1098 current
= get_output("git symbolic-ref HEAD")
1100 raise YapError("Not on a branch!")
1101 current
= current
[0].replace('refs/heads/', '')
1103 if repo
is None and branch
is None:
1104 repo
, merge
= self
._get
_tracking
(current
)
1105 merge
= merge
.replace('refs/heads/', '')
1106 print "Branch '%s' tracking refs/remotes/%s/%s" % (current
, repo
, merge
)
1109 if repo
is None or branch
is None:
1112 if repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1113 raise YapError("No such repository: %s" % repo
)
1115 if run_command("git rev-parse --verify refs/remotes/%s/%s" % (repo
, branch
)):
1116 raise YapError("No such branch '%s' on repository '%s'" % (branch
, repo
))
1118 os
.system("git config branch.%s.remote '%s'" % (current
, repo
))
1119 os
.system("git config branch.%s.merge 'refs/heads/%s'" % (current
, branch
))
1120 print "Branch '%s' now tracking refs/remotes/%s/%s" % (current
, repo
, branch
)
1122 @short_help("mark files with conflicts as resolved")
1124 The arguments are the files to be marked resolved. When a conflict
1125 occurs while merging changes to a file, that file is marked as
1126 "unmerged." Until the file(s) with conflicts are marked resolved,
1127 commits cannot be made.
1129 def cmd_resolved(self
, *args
):
1136 self
._stage
_one
(f
, True)
1139 @short_help("show information about loaded plugins")
1140 def cmd_plugins(self
):
1142 if not self
.plugins
:
1143 print >>sys
.stderr
, "No plugins loaded."
1144 for k
, v
in self
.plugins
.items():
1147 doc
= "No description"
1148 print "%-20s%s" % (k
, doc
)
1151 if not func
.startswith('cmd_'):
1154 print "\tOverrides:"
1158 def cmd_help(self
, cmd
=None):
1160 cmd
= "cmd_" + cmd
.replace('-', '_')
1162 attr
= self
.__getattribute
__(cmd
)
1163 except AttributeError:
1164 raise YapError("No such command: %s" % cmd
)
1166 help = attr
.long_help
1167 except AttributeError:
1168 attr
= super(Yap
, self
).__getattribute
__(cmd
)
1170 help = attr
.long_help
1171 except AttributeError:
1172 raise YapError("Sorry, no help for '%s'. Ask Steven." % cmd
)
1174 print >>sys
.stderr
, "The '%s' command" % cmd
1175 print >>sys
.stderr
, "\tyap %s %s" % (cmd
, attr
.__doc
__)
1176 print >>sys
.stderr
, "%s" % help
1179 print >> sys
.stderr
, "Yet Another (Git) Porcelein"
1182 for name
in dir(self
):
1183 if not name
.startswith('cmd_'):
1185 attr
= self
.__getattribute
__(name
)
1186 if not callable(attr
):
1190 short_msg
= attr
.short_help
1191 except AttributeError:
1193 default_meth
= super(Yap
, self
).__getattribute
__(name
)
1194 short_msg
= default_meth
.short_help
1195 except AttributeError:
1198 name
= name
.replace('cmd_', '')
1199 name
= name
.replace('_', '-')
1200 print >> sys
.stderr
, "%-16s%s" % (name
, short_msg
)
1203 print >> sys
.stderr
, "Commands provided by plugins:"
1204 for k
, v
in self
.plugins
.items():
1206 if not name
.startswith('cmd_'):
1209 attr
= self
.__getattribute
__(name
)
1210 short_msg
= attr
.short_help
1211 except AttributeError:
1213 name
= name
.replace('cmd_', '')
1214 name
= name
.replace('_', '-')
1215 print >> sys
.stderr
, "%-8s(%s) %s" % (name
, k
, short_msg
)
1218 print >> sys
.stderr
, "(*) Indicates that the command is not readily reversible"
1220 def cmd_usage(self
):
1221 print >> sys
.stderr
, "usage: %s <command>" % os
.path
.basename(sys
.argv
[0])
1222 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"
1224 def main(self
, args
):
1232 if run_command("git --version"):
1233 print >>sys
.stderr
, "Failed to run git; is it installed?"
1236 debug
= os
.getenv('YAP_DEBUG')
1239 command
= command
.replace('-', '_')
1241 meth
= self
.__getattribute
__("cmd_"+command
)
1243 default_meth
= super(Yap
, self
).__getattribute
__("cmd_"+command
)
1244 except AttributeError:
1247 if meth
.__doc
__ is not None:
1249 elif default_meth
is not None:
1250 doc
= default_meth
.__doc
__
1256 if "options" in meth
.__dict
__:
1257 options
= meth
.options
1258 if default_meth
and "options" in default_meth
.__dict
__:
1259 options
+= default_meth
.options
1261 flags
, args
= getopt
.getopt(args
, options
)
1266 # cast args to a mutable type. this lets the pre-hooks act as
1267 # filters on the arguments
1271 for p
in self
.plugins
.values():
1273 pre_meth
= p
.__getattribute
__("pre_"+command
)
1274 except AttributeError:
1276 pre_meth(args
, flags
)
1278 meth(*args
, **flags
)
1281 for p
in self
.plugins
.values():
1283 meth
= p
.__getattribute
__("post_"+command
)
1284 except AttributeError:
1288 except (TypeError, getopt
.GetoptError
):
1291 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1295 print >> sys
.stderr
, e
1297 except AttributeError: