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 def __getattribute__(self
, attr
):
329 if attr
.startswith("cmd_"):
331 for p
in self
.plugins
.values():
333 meth
= p
.__getattribute
__(attr
)
335 except AttributeError:
340 return super(Yap
, self
).__getattribute
__(attr
)
342 def _call_base(self
, method
, *args
, **flags
):
343 base_method
= super(Yap
, self
).__getattribute
__(method
)
344 return base_method(*args
, **flags
)
345 def _confirm_push(self
, current
, rhs
, repo
):
346 print "About to push local branch '%s' to '%s' on '%s'" % (current
, rhs
, repo
)
347 print "Continue (y/n)? ",
349 ans
= sys
.stdin
.readline().strip()
351 if ans
.lower() != 'y' and ans
.lower() != 'yes':
352 raise YapError("Aborted.")
354 @short_help("make a local copy of an existing repository")
356 The first argument is a URL to the existing repository. This can be an
357 absolute path if the repository is local, or a URL with the git://,
358 ssh://, or http:// schemes. By default, the directory used is the last
359 component of the URL, sans '.git'. This can be overridden by providing
362 def cmd_clone(self
, url
, directory
=None):
365 if '://' not in url
and url
[0] != '/':
366 url
= os
.path
.join(os
.getcwd(), url
)
368 url
= url
.rstrip('/')
369 if directory
is None:
370 directory
= url
.rsplit('/')[-1]
371 directory
= directory
.replace('.git', '')
376 raise YapError("Directory exists: %s" % directory
)
379 self
.cmd_repo("origin", url
)
380 self
.cmd_fetch("origin")
383 if not run_command("git rev-parse --verify refs/remotes/origin/HEAD"):
384 hash = get_output("git rev-parse refs/remotes/origin/HEAD")[0]
385 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'"):
386 if get_output("git rev-parse %s" % b
)[0] == hash:
390 if not run_command("git rev-parse --verify refs/remotes/origin/master"):
391 branch
= "refs/remotes/origin/master"
393 branch
= get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'")
396 hash = get_output("git rev-parse %s" % branch
)
398 branch
= branch
.replace('refs/remotes/origin/', '')
399 run_safely("git update-ref refs/heads/%s %s" % (branch
, hash[0]))
400 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
401 self
.cmd_revert(**{'-a': 1})
403 @short_help("turn a directory into a repository")
405 Converts the current working directory into a repository. The primary
406 side-effect of this command is the creation of a '.git' subdirectory.
407 No files are added nor commits made.
410 os
.system("git init")
412 @short_help("add a new file to the repository")
414 The arguments are the files to be added to the repository. Once added,
415 the files will show as "unstaged changes" in the output of 'status'. To
416 reverse the effects of this command, see 'rm'.
418 def cmd_add(self
, *files
):
429 @short_help("delete a file from the repository")
431 The arguments are the files to be removed from the current revision of
432 the repository. The files will still exist in any past commits that the
433 files may have been a part of. The file is not actually deleted, it is
434 just no longer tracked as part of the repository.
436 def cmd_rm(self
, *files
):
446 @short_help("stage changes in a file for commit")
448 The arguments are the files to be staged. Staging changes is a way to
449 build up a commit when you do not want to commit all changes at once.
450 To commit only staged changes, use the '-d' flag to 'commit.' To
451 reverse the effects of this command, see 'unstage'. Once staged, the
452 files will show as "staged changes" in the output of 'status'.
454 def cmd_stage(self
, *files
):
464 @short_help("unstage changes in a file")
466 The arguments are the files to be unstaged. Once unstaged, the files
467 will show as "unstaged changes" in the output of 'status'. The '-a'
468 flag can be used to unstage all staged changes at once.
471 def cmd_unstage(self
, *files
, **flags
):
486 @short_help("show files with staged and unstaged changes")
488 Show the files in the repository with changes since the last commit,
489 categorized based on whether the changes are staged or not. A file may
490 appear under each heading if the same file has both staged and unstaged
493 def cmd_status(self
):
496 branch
= get_output("git symbolic-ref HEAD")
498 branch
= branch
[0].replace('refs/heads/', '')
501 print "Current branch: %s" % branch
503 print "Files with staged changes:"
504 files
= self
._get
_staged
_files
()
510 print "Files with unstaged changes:"
511 files
= self
._get
_unstaged
_files
()
517 files
= self
._get
_unmerged
_files
()
519 print "Files with conflicts:"
523 @short_help("remove uncommitted changes from a file (*)")
525 The arguments are the files whose changes will be reverted. If the '-a'
526 flag is given, then all files will have uncommitted changes removed.
527 Note that there is no way to reverse this command short of manually
528 editing each file again.
531 def cmd_revert(self
, *files
, **flags
):
536 run_safely("git checkout-index -u -f -a")
547 @short_help("record changes to files as a new commit")
549 Create a new commit recording changes since the last commit. If there
550 are only unstaged changes, those will be recorded. If there are only
551 staged changes, those will be recorded. Otherwise, you will have to
552 specify either the '-a' flag or the '-d' flag to commit all changes or
553 only staged changes, respectively. To reverse the effects of this
554 command, see 'uncommit'.
556 @takes_options("adm:")
557 def cmd_commit(self
, **flags
):
558 "[-a | -d] [-m <msg>]"
560 self
._check
_rebasing
()
561 self
._check
_commit
(**flags
)
562 if not self
._get
_staged
_files
():
563 raise YapError("No changes to commit")
564 msg
= flags
.get('-m', None)
568 @short_help("reverse the actions of the last commit")
570 Reverse the effects of the last 'commit' operation. The changes that
571 were part of the previous commit will show as "staged changes" in the
572 output of 'status'. This means that if no files were changed since the
573 last commit was created, 'uncommit' followed by 'commit' is a lossless
576 def cmd_uncommit(self
):
582 @short_help("report the current version of yap")
583 def cmd_version(self
):
584 print "Yap version 0.1"
586 @short_help("show the changelog for particular versions or files")
588 The arguments are the files with which to filter history. If none are
589 given, all changes are listed. Otherwise only commits that affected one
590 or more of the given files are listed. The -r option changes the
591 starting revision for traversing history. By default, history is listed
594 @takes_options("pr:")
595 def cmd_log(self
, *paths
, **flags
):
596 "[-p] [-r <rev>] <path>..."
598 rev
= flags
.get('-r', 'HEAD')
607 paths
= ' '.join(paths
)
608 os
.system("git log -M -C %s %s '%s' -- %s"
609 % (follow
, flags
.get('-p', '--name-status'), rev
, paths
))
611 @short_help("show staged, unstaged, or all uncommitted changes")
613 Show staged, unstaged, or all uncommitted changes. By default, all
614 changes are shown. The '-u' flag causes only unstaged changes to be
615 shown. The '-d' flag causes only staged changes to be shown.
618 def cmd_diff(self
, **flags
):
621 if '-u' in flags
and '-d' in flags
:
622 raise YapError("Conflicting flags: -u and -d")
624 pager
= self
._get
_pager
_cmd
()
627 os
.system("git diff-files -p | %s" % pager
)
629 os
.system("git diff-index --cached -p HEAD | %s" % pager
)
631 os
.system("git diff-index -p HEAD | %s" % pager
)
633 @short_help("list, create, or delete branches")
635 If no arguments are specified, a list of local branches is given. The
636 current branch is indicated by a "*" next to the name. If an argument
637 is given, it is taken as the name of a new branch to create. The branch
638 will start pointing at the current HEAD. See 'point' for details on
639 changing the revision of the new branch. Note that this command does
640 not switch the current working branch. See 'switch' for details on
641 changing the current working branch.
643 The '-d' flag can be used to delete local branches. If the delete
644 operation would remove the last branch reference to a given line of
645 history (colloquially referred to as "dangling commits"), yap will
646 report an error and abort. The '-f' flag can be used to force the delete
649 @takes_options("fd:")
650 def cmd_branch(self
, branch
=None, **flags
):
651 "[ [-f] -d <branch> | <branch> ]"
653 force
= '-f' in flags
655 self
._delete
_branch
(flags
['-d'], force
)
659 if branch
is not None:
660 ref
= get_output("git rev-parse --verify HEAD")
662 raise YapError("No branch point yet. Make a commit")
663 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
665 current
= get_output("git symbolic-ref HEAD")
666 branches
= get_output("git for-each-ref --format='%(refname)' 'refs/heads/*'")
668 if current
and b
== current
[0]:
672 b
= b
.replace('refs/heads/', '')
675 @short_help("change the current working branch")
677 The argument is the name of the branch to make the current working
678 branch. This command will fail if there are uncommitted changes to any
679 files. Otherwise, the contents of the files in the working directory
680 are updated to reflect their state in the new branch. Additionally, any
681 future commits are added to the new branch instead of the previous line
685 def cmd_switch(self
, branch
, **flags
):
688 self
._check
_rebasing
()
689 ref
= get_output("git rev-parse --verify 'refs/heads/%s'" % branch
)
691 raise YapError("No such branch: %s" % branch
)
693 if '-f' not in flags
and (self
._get
_unstaged
_files
() or self
._get
_staged
_files
()):
694 raise YapError("You have uncommitted changes. Use -f to continue anyway")
696 if self
._get
_unstaged
_files
() and self
._get
_staged
_files
():
697 raise YapError("You have staged and unstaged changes. Perhaps unstage -a?")
699 staged
= bool(self
._get
_staged
_files
())
701 run_command("git diff-files -p | git apply --cached")
702 for f
in self
._get
_new
_files
():
705 idx
= get_output("git write-tree")
706 new
= get_output("git rev-parse refs/heads/%s" % branch
)
707 readtree
= "git read-tree --aggressive -u -m HEAD %s %s" % (idx
[0], new
[0])
708 if run_command(readtree
):
709 run_command("git update-index --refresh")
710 if os
.system(readtree
):
711 raise YapError("Failed to switch")
712 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
718 @short_help("move the current branch to a different revision")
720 The argument is the hash of the commit to which the current branch
721 should point, or alternately a branch or tag (a.k.a, "committish"). If
722 moving the branch would create "dangling commits" (see 'branch'), yap
723 will report an error and abort. The '-f' flag can be used to force the
724 operation in spite of this.
727 def cmd_point(self
, where
, **flags
):
730 self
._check
_rebasing
()
732 head
= get_output("git rev-parse --verify HEAD")
734 raise YapError("No commit yet; nowhere to point")
736 ref
= get_output("git rev-parse --verify '%s'" % where
)
738 raise YapError("Not a valid ref: %s" % where
)
740 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
741 raise YapError("You have uncommitted changes. Commit them first")
743 type = get_output("git cat-file -t '%s'" % ref
[0])
744 if type and type[0] == "tag":
745 tag
= get_output("git cat-file tag '%s'" % ref
[0])
746 ref
[0] = tag
[0].split(' ')[1]
748 run_safely("git update-ref HEAD '%s'" % ref
[0])
750 if '-f' not in flags
:
751 name
= get_output("git name-rev --name-only '%s'" % head
[0])[0]
752 if name
== "undefined":
753 os
.system("git update-ref HEAD '%s'" % head
[0])
754 raise YapError("Pointing there will lose commits. Use -f to force")
757 run_safely("git read-tree -u -m HEAD")
759 run_safely("git read-tree HEAD")
760 run_safely("git checkout-index -u -f -a")
762 @short_help("alter history by dropping or amending commits")
764 This command operates in two distinct modes, "amend" and "drop" mode.
765 In drop mode, the given commit is removed from the history of the
766 current branch, as though that commit never happened. By default the
769 In amend mode, the uncommitted changes present are merged into a
770 previous commit. This is useful for correcting typos or adding missed
771 files into past commits. By default the commit used is HEAD.
773 While rewriting history it is possible that conflicts will arise. If
774 this happens, the rewrite will pause and you will be prompted to resolve
775 the conflicts and stage them. Once that is done, you will run "yap
776 history continue." If instead you want the conflicting commit removed
777 from history (perhaps your changes supercede that commit) you can run
778 "yap history skip". Once the rewrite completes, your branch will be on
779 the same commit as when the rewrite started.
781 def cmd_history(self
, subcmd
, *args
):
782 "amend | drop <commit>"
785 if subcmd
not in ("amend", "drop", "continue", "skip"):
789 When you have resolved the conflicts run \"yap history continue\".
790 To skip the problematic patch, run \"yap history skip\"."""
792 if subcmd
== "continue":
793 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
796 os
.system("git reset --hard")
797 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
800 if subcmd
== "amend":
801 flags
, args
= getopt
.getopt(args
, "ad")
811 if run_command("git rev-parse --verify '%s'" % commit
):
812 raise YapError("Not a valid commit: %s" % commit
)
814 self
._check
_rebasing
()
816 if subcmd
== "amend":
817 self
._check
_commit
(**flags
)
818 if self
._get
_unstaged
_files
():
819 # XXX: handle unstaged changes better
820 raise YapError("Commit away changes that you aren't amending")
824 start
= get_output("git rev-parse HEAD")
825 stash
= get_output("git stash create")
826 run_command("git reset --hard")
828 fd
, tmpfile
= tempfile
.mkstemp("yap")
832 os
.system("git format-patch -k --stdout '%s' > %s" % (commit
, tmpfile
))
833 if subcmd
== "amend":
834 self
.cmd_point(commit
, **{'-f': True})
836 if subcmd
== "amend":
838 rc
= os
.system("git stash apply %s" % stash
[0])
840 self
.cmd_point(start
[0], **{'-f': True})
841 os
.system("git stash apply %s" % stash
[0])
842 raise YapError("Failed to apply stash")
845 if subcmd
== "amend":
847 for f
in self
._get
_unstaged
_files
():
851 self
.cmd_point("%s^" % commit
, **{'-f': True})
853 stat
= os
.stat(tmpfile
)
856 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
858 raise YapError("Failed to apply changes")
863 run_command("git stash apply %s" % stash
[0])
866 @short_help("show the changes introduced by a given commit")
868 By default, the changes in the last commit are shown. To override this,
869 specify a hash, branch, or tag (committish). The hash of the commit,
870 the commit's author, log message, and a diff of the changes are shown.
872 def cmd_show(self
, commit
="HEAD"):
875 os
.system("git show '%s'" % commit
)
877 @short_help("apply the changes in a given commit to the current branch")
879 The argument is the hash, branch, or tag (committish) of the commit to
880 be applied. In general, it only makes sense to apply commits that
881 happened on another branch. The '-r' flag can be used to have the
882 changes in the given commit reversed from the current branch. In
883 general, this only makes sense for commits that happened on the current
887 def cmd_cherry_pick(self
, commit
, **flags
):
891 os
.system("git revert '%s'" % commit
)
893 os
.system("git cherry-pick '%s'" % commit
)
895 @short_help("list, add, or delete configured remote repositories")
897 When invoked with no arguments, this command will show the list of
898 currently configured remote repositories, giving both the name and URL
899 of each. To add a new repository, give the desired name as the first
900 argument and the URL as the second. The '-d' flag can be used to remove
901 a previously added repository.
904 def cmd_repo(self
, name
=None, url
=None, **flags
):
905 "[<name> <url> | -d <name>]"
907 if name
is not None and url
is None:
911 if flags
['-d'] not in [ x
[0] for x
in self
._list
_remotes
() ]:
912 raise YapError("No such repository: %s" % flags
['-d'])
913 os
.system("git config --unset remote.%s.url" % flags
['-d'])
914 os
.system("git config --unset remote.%s.fetch" % flags
['-d'])
917 if name
in [ x
[0] for x
in self
._list
_remotes
() ]:
918 raise YapError("Repository '%s' already exists" % flags
['-d'])
919 os
.system("git config remote.%s.url %s" % (name
, url
))
920 os
.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name
, name
))
922 for remote
, url
in self
._list
_remotes
():
923 print "%-20s %s" % (remote
, url
)
925 @short_help("send local commits to a remote repository (*)")
927 When invoked with no arguments, the current branch is synchronized to
928 the tracking branch of the tracking remote. If no tracking remote is
929 specified, the repository will have to be specified on the command line.
930 In that case, the default is to push to a branch with the same name as
931 the current branch. This behavior can be overridden by giving a second
932 argument to specify the remote branch.
934 If the remote branch does not currently exist, the command will abort
935 unless the -c flag is provided. If the remote branch is not a direct
936 descendent of the local branch, the command will abort unless the -f
937 flag is provided. Forcing a push in this way can be problematic to
938 other users of the repository if they are not expecting it.
940 To delete a branch on the remote repository, use the -d flag.
942 @takes_options("cdf")
943 def cmd_push(self
, repo
=None, rhs
=None, **flags
):
946 if '-c' in flags
and '-d' in flags
:
949 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
950 raise YapError("No such repository: %s" % repo
)
952 current
= get_output("git symbolic-ref HEAD")
954 raise YapError("Not on a branch!")
956 self
._check
_rebasing
()
958 current
= current
[0].replace('refs/heads/', '')
959 remote
= get_output("git config branch.%s.remote" % current
)
960 if repo
is None and remote
:
964 raise YapError("No tracking branch configured; specify destination repository")
966 if rhs
is None and remote
and remote
[0] == repo
:
967 merge
= get_output("git config branch.%s.merge" % current
)
972 rhs
= "refs/heads/%s" % current
974 if '-c' not in flags
and '-d' not in flags
:
975 if run_command("git rev-parse --verify refs/remotes/%s/%s"
976 % (repo
, rhs
.replace('refs/heads/', ''))):
977 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
978 if '-f' not in flags
:
979 hash = get_output("git rev-parse refs/remotes/%s/%s" % (repo
, rhs
.replace('refs/heads/', '')))
980 base
= get_output("git merge-base HEAD %s" % hash[0])
982 if base
[0] != hash[0]:
983 raise YapError("Branch not up-to-date with remote. Update or use -f")
985 self
._confirm
_push
(self
, current
, rhs
, repo
)
992 lhs
= "refs/heads/%s" % current
993 rc
= os
.system("git push %s %s %s:%s" % (flags
.get('-f', ''), repo
, lhs
, rhs
))
995 raise YapError("Push failed.")
997 @short_help("retrieve commits from a remote repository")
999 When run with no arguments, the command will retrieve new commits from
1000 the remote tracking repository. Note that this does not in any way
1001 alter the current branch. For that, see "update". If a remote other
1002 than the tracking remote is desired, it can be specified as the first
1005 def cmd_fetch(self
, repo
=None):
1008 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1009 raise YapError("No such repository: %s" % repo
)
1011 remote
= get_output("git config branch.%s.remote" % current
)
1014 raise YapError("No tracking branch configured; specify a repository")
1015 os
.system("git fetch %s" % repo
)
1017 @short_help("update the current branch relative to its tracking branch")
1019 Updates the current branch relative to its remote tracking branch. This
1020 command requires that the current branch have a remote tracking branch
1021 configured. If any conflicts occur while applying your changes to the
1022 updated remote, the command will pause to allow you to fix them. Once
1023 that is done, run "update" with the "continue" subcommand. Alternately,
1024 the "skip" subcommand can be used to discard the conflicting changes.
1026 def cmd_update(self
, subcmd
=None):
1029 if subcmd
and subcmd
not in ["continue", "skip"]:
1033 When you have resolved the conflicts run \"yap update continue\".
1034 To skip the problematic patch, run \"yap update skip\"."""
1036 if subcmd
== "continue":
1037 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
1039 if subcmd
== "skip":
1040 os
.system("git reset --hard")
1041 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
1044 self
._check
_rebasing
()
1045 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
1046 raise YapError("You have uncommitted changes. Commit them first")
1048 current
= get_output("git symbolic-ref HEAD")
1050 raise YapError("Not on a branch!")
1052 current
= current
[0].replace('refs/heads/', '')
1053 remote
, merge
= self
._get
_tracking
(current
)
1054 merge
= merge
[0].replace('refs/heads/', '')
1056 self
.cmd_fetch(remote
)
1057 base
= get_output("git merge-base HEAD refs/remotes/%s/%s" % (remote
, merge
))
1060 fd
, tmpfile
= tempfile
.mkstemp("yap")
1062 os
.system("git format-patch -k --stdout '%s' > %s" % (base
[0], tmpfile
))
1063 self
.cmd_point("refs/remotes/%s/%s" % (remote
, merge
), **{'-f': True})
1065 stat
= os
.stat(tmpfile
)
1068 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
1070 raise YapError("Failed to apply changes")
1074 @short_help("query and configure remote branch tracking")
1076 When invoked with no arguments, the command displays the tracking
1077 information for the current branch. To configure the tracking
1078 information, two arguments for the remote repository and remote branch
1079 are given. The tracking information is used to provide defaults for
1080 where to push local changes and from where to get updates to the branch.
1082 def cmd_track(self
, repo
=None, branch
=None):
1086 current
= get_output("git symbolic-ref HEAD")
1088 raise YapError("Not on a branch!")
1089 current
= current
[0].replace('refs/heads/', '')
1091 if repo
is None and branch
is None:
1092 repo
, merge
= self
._get
_tracking
(current
)
1093 merge
= merge
[0].replace('refs/heads/', '')
1094 print "Branch '%s' tracking refs/remotes/%s/%s" % (current
, repo
, merge
)
1097 if repo
is None or branch
is None:
1100 if repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1101 raise YapError("No such repository: %s" % repo
)
1103 if run_command("git rev-parse --verify refs/remotes/%s/%s" % (repo
, branch
)):
1104 raise YapError("No such branch '%s' on repository '%s'" % (repo
, branch
))
1106 os
.system("git config branch.%s.remote '%s'" % (current
, repo
))
1107 os
.system("git config branch.%s.merge 'refs/heads/%s'" % (current
, branch
))
1108 print "Branch '%s' now tracking refs/remotes/%s/%s" % (current
, repo
, branch
)
1110 @short_help("mark files with conflicts as resolved")
1112 The arguments are the files to be marked resolved. When a conflict
1113 occurs while merging changes to a file, that file is marked as
1114 "unmerged." Until the file(s) with conflicts are marked resolved,
1115 commits cannot be made.
1117 def cmd_resolved(self
, *args
):
1124 self
._stage
_one
(f
, True)
1127 @short_help("show information about loaded plugins")
1128 def cmd_plugins(self
):
1130 if not self
.plugins
:
1131 print >>sys
.stderr
, "No plugins loaded."
1132 for k
, v
in self
.plugins
.items():
1135 doc
= "No description"
1136 print "%-20s%s" % (k
, doc
)
1139 if not func
.startswith('cmd_'):
1142 print "\tOverrides:"
1146 def cmd_help(self
, cmd
=None):
1149 attr
= self
.__getattribute
__("cmd_"+cmd
.replace('-', '_'))
1150 except AttributeError:
1151 raise YapError("No such command: %s" % cmd
)
1153 help = attr
.long_help
1154 except AttributeError:
1155 raise YapError("Sorry, no help for '%s'. Ask Steven." % cmd
)
1157 print >>sys
.stderr
, "The '%s' command" % cmd
1158 print >>sys
.stderr
, "\tyap %s %s" % (cmd
, attr
.__doc
__)
1159 print >>sys
.stderr
, "%s" % help
1162 print >> sys
.stderr
, "Yet Another (Git) Porcelein"
1165 for name
in dir(self
):
1166 if not name
.startswith('cmd_'):
1168 attr
= self
.__getattribute
__(name
)
1169 if not callable(attr
):
1172 short_msg
= attr
.short_help
1173 except AttributeError:
1176 name
= name
.replace('cmd_', '')
1177 name
= name
.replace('_', '-')
1178 print >> sys
.stderr
, "%-16s%s" % (name
, short_msg
)
1180 print >> sys
.stderr
, "(*) Indicates that the command is not readily reversible"
1182 def cmd_usage(self
):
1183 print >> sys
.stderr
, "usage: %s <command>" % os
.path
.basename(sys
.argv
[0])
1184 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"
1186 def main(self
, args
):
1194 if run_command("git --version"):
1195 print >>sys
.stderr
, "Failed to run git; is it installed?"
1198 debug
= os
.getenv('YAP_DEBUG')
1201 command
= command
.replace('-', '_')
1203 meth
= self
.__getattribute
__("cmd_"+command
)
1205 default_meth
= super(Yap
, self
).__getattribute
__("cmd_"+command
)
1206 except AttributeError:
1209 if meth
.__doc
__ is not None:
1211 elif default_meth
is not None:
1212 doc
= default_meth
.__doc
__
1217 if "options" in meth
.__dict
__:
1218 options
= meth
.options
1219 if default_meth
and "options" in default_meth
.__dict
__:
1220 options
+= default_meth
.options
1221 flags
, args
= getopt
.getopt(args
, options
)
1227 for p
in self
.plugins
.values():
1229 pre_meth
= p
.__getattribute
__("pre_"+command
)
1230 except AttributeError:
1232 pre_meth(*args
, **flags
)
1234 meth(*args
, **flags
)
1237 for p
in self
.plugins
.values():
1239 meth
= p
.__getattribute
__("post_"+command
)
1240 except AttributeError:
1244 except (TypeError, getopt
.GetoptError
):
1247 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1249 print >> sys
.stderr
, e
1251 except AttributeError: