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 cls
in glbls
.values():
35 if not type(cls
) == type:
37 if not issubclass(cls
, YapPlugin
):
45 if not func
.startswith('cmd_'):
47 if func
in self
.overrides
:
48 print >>sys
.stderr
, "Plugin %s overrides already overridden function %s. Disabling" % (p
, func
)
49 self
.plugins
.remove(x
)
52 def _add_new_file(self
, file):
53 repo
= get_output('git rev-parse --git-dir')[0]
54 dir = os
.path
.join(repo
, 'yap')
59 files
= self
._get
_new
_files
()
61 path
= os
.path
.join(dir, 'new-files')
62 pickle
.dump(files
, open(path
, 'w'))
64 def _get_new_files(self
):
65 repo
= get_output('git rev-parse --git-dir')[0]
66 path
= os
.path
.join(repo
, 'yap', 'new-files')
68 files
= pickle
.load(file(path
))
75 if get_output("git ls-files --cached '%s'" % f
) != []:
80 def _remove_new_file(self
, file):
81 files
= self
._get
_new
_files
()
82 files
= filter(lambda x
: x
!= file, files
)
84 repo
= get_output('git rev-parse --git-dir')[0]
85 path
= os
.path
.join(repo
, 'yap', 'new-files')
86 pickle
.dump(files
, open(path
, 'w'))
88 def _clear_new_files(self
):
89 repo
= get_output('git rev-parse --git-dir')[0]
90 path
= os
.path
.join(repo
, 'yap', 'new-files')
93 def _assert_file_exists(self
, file):
94 if not os
.access(file, os
.R_OK
):
95 raise YapError("No such file: %s" % file)
97 def _get_staged_files(self
):
98 if run_command("git rev-parse HEAD"):
99 files
= get_output("git ls-files --cached")
101 files
= get_output("git diff-index --cached --name-only HEAD")
102 unmerged
= self
._get
_unmerged
_files
()
104 unmerged
= set(unmerged
)
105 files
= set(files
).difference(unmerged
)
109 def _get_unstaged_files(self
):
110 files
= get_output("git ls-files -m")
111 prefix
= get_output("git rev-parse --show-prefix")
113 files
= [ os
.path
.join(prefix
[0], x
) for x
in files
]
114 files
+= self
._get
_new
_files
()
115 unmerged
= self
._get
_unmerged
_files
()
117 unmerged
= set(unmerged
)
118 files
= set(files
).difference(unmerged
)
122 def _get_unmerged_files(self
):
123 files
= get_output("git ls-files -u")
124 files
= [ x
.replace('\t', ' ').split(' ')[3] for x
in files
]
125 prefix
= get_output("git rev-parse --show-prefix")
127 files
= [ os
.path
.join(prefix
[0], x
) for x
in files
]
128 return list(set(files
))
130 def _delete_branch(self
, branch
, force
):
131 current
= get_output("git symbolic-ref HEAD")[0]
132 current
= current
.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 _list_remotes(self
):
300 remotes
= get_output("git config --get-regexp '^remote.*.url'")
302 remote
, url
= x
.split(' ')
303 remote
= remote
.replace('remote.', '')
304 remote
= remote
.replace('.url', '')
307 def _unstage_all(self
):
309 run_safely("git read-tree -m HEAD")
311 run_safely("git read-tree HEAD")
312 run_safely("git update-index -q --refresh")
314 def _get_tracking(self
, current
):
315 remote
= get_output("git config branch.%s.remote" % current
)
317 raise YapError("No tracking branch configured for '%s'" % current
)
319 merge
= get_output("git config branch.%s.merge" % current
)
321 raise YapError("No tracking branch configured for '%s'" % current
)
322 return remote
[0], merge
324 @short_help("make a local copy of an existing repository")
326 The first argument is a URL to the existing repository. This can be an
327 absolute path if the repository is local, or a URL with the git://,
328 ssh://, or http:// schemes. By default, the directory used is the last
329 component of the URL, sans '.git'. This can be overridden by providing
332 def cmd_clone(self
, url
, directory
=None):
335 if '://' not in url
and url
[0] != '/':
336 url
= os
.path
.join(os
.getcwd(), url
)
338 url
= url
.rstrip('/')
339 if directory
is None:
340 directory
= url
.rsplit('/')[-1]
341 directory
= directory
.replace('.git', '')
346 raise YapError("Directory exists: %s" % directory
)
349 self
.cmd_repo("origin", url
)
350 self
.cmd_fetch("origin")
353 if not run_command("git rev-parse --verify refs/remotes/origin/HEAD"):
354 hash = get_output("git rev-parse refs/remotes/origin/HEAD")[0]
355 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'"):
356 if get_output("git rev-parse %s" % b
)[0] == hash:
360 if not run_command("git rev-parse --verify refs/remotes/origin/master"):
361 branch
= "refs/remotes/origin/master"
363 branch
= get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'")
366 hash = get_output("git rev-parse %s" % branch
)
368 branch
= branch
.replace('refs/remotes/origin/', '')
369 run_safely("git update-ref refs/heads/%s %s" % (branch
, hash[0]))
370 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
371 self
.cmd_revert(**{'-a': 1})
373 @short_help("turn a directory into a repository")
375 Converts the current working directory into a repository. The primary
376 side-effect of this command is the creation of a '.git' subdirectory.
377 No files are added nor commits made.
380 os
.system("git init")
382 @short_help("add a new file to the repository")
384 The arguments are the files to be added to the repository. Once added,
385 the files will show as "unstaged changes" in the output of 'status'. To
386 reverse the effects of this command, see 'rm'.
388 def cmd_add(self
, *files
):
397 @short_help("delete a file from the repository")
399 The arguments are the files to be removed from the current revision of
400 the repository. The files will still exist in any past commits that the
401 files may have been a part of. The file is not actually deleted, it is
402 just no longer tracked as part of the repository.
404 def cmd_rm(self
, *files
):
413 @short_help("stage changes in a file for commit")
415 The arguments are the files to be staged. Staging changes is a way to
416 build up a commit when you do not want to commit all changes at once.
417 To commit only staged changes, use the '-d' flag to 'commit.' To
418 reverse the effects of this command, see 'unstage'. Once staged, the
419 files will show as "staged changes" in the output of 'status'.
421 def cmd_stage(self
, *files
):
430 @short_help("unstage changes in a file")
432 The arguments are the files to be unstaged. Once unstaged, the files
433 will show as "unstaged changes" in the output of 'status'. The '-a'
434 flag can be used to unstage all staged changes at once.
437 def cmd_unstage(self
, *files
, **flags
):
451 @short_help("show files with staged and unstaged changes")
453 Show the files in the repository with changes since the last commit,
454 categorized based on whether the changes are staged or not. A file may
455 appear under each heading if the same file has both staged and unstaged
458 def cmd_status(self
):
460 branch
= get_output("git symbolic-ref HEAD")[0]
461 branch
= branch
.replace('refs/heads/', '')
462 print "Current branch: %s" % branch
464 print "Files with staged changes:"
465 files
= self
._get
_staged
_files
()
471 print "Files with unstaged changes:"
472 files
= self
._get
_unstaged
_files
()
478 files
= self
._get
_unmerged
_files
()
480 print "Files with conflicts:"
484 @short_help("remove uncommitted changes from a file (*)")
486 The arguments are the files whose changes will be reverted. If the '-a'
487 flag is given, then all files will have uncommitted changes removed.
488 Note that there is no way to reverse this command short of manually
489 editing each file again.
492 def cmd_revert(self
, *files
, **flags
):
496 run_safely("git checkout-index -u -f -a")
507 @short_help("record changes to files as a new commit")
509 Create a new commit recording changes since the last commit. If there
510 are only unstaged changes, those will be recorded. If there are only
511 staged changes, those will be recorded. Otherwise, you will have to
512 specify either the '-a' flag or the '-d' flag to commit all changes or
513 only staged changes, respectively. To reverse the effects of this
514 command, see 'uncommit'.
516 @takes_options("adm:")
517 def cmd_commit(self
, **flags
):
519 self
._check
_rebasing
()
520 self
._check
_commit
(**flags
)
521 if not self
._get
_staged
_files
():
522 raise YapError("No changes to commit")
523 msg
= flags
.get('-m', None)
527 @short_help("reverse the actions of the last commit")
529 Reverse the effects of the last 'commit' operation. The changes that
530 were part of the previous commit will show as "staged changes" in the
531 output of 'status'. This means that if no files were changed since the
532 last commit was created, 'uncommit' followed by 'commit' is a lossless
535 def cmd_uncommit(self
):
540 @short_help("report the current version of yap")
541 def cmd_version(self
):
542 print "Yap version 0.1"
544 @short_help("show the changelog for particular versions or files")
546 The arguments are the files with which to filter history. If none are
547 given, all changes are listed. Otherwise only commits that affected one
548 or more of the given files are listed. The -r option changes the
549 starting revision for traversing history. By default, history is listed
553 def cmd_log(self
, *paths
, **flags
):
554 "[-r <rev>] <path>..."
555 rev
= flags
.get('-r', 'HEAD')
556 paths
= ' '.join(paths
)
557 os
.system("git log --name-status '%s' -- %s" % (rev
, paths
))
559 @short_help("show staged, unstaged, or all uncommitted changes")
561 Show staged, unstaged, or all uncommitted changes. By default, all
562 changes are shown. The '-u' flag causes only unstaged changes to be
563 shown. The '-d' flag causes only staged changes to be shown.
566 def cmd_diff(self
, **flags
):
568 if '-u' in flags
and '-d' in flags
:
569 raise YapError("Conflicting flags: -u and -d")
571 pager
= self
._get
_pager
_cmd
()
574 os
.system("git diff-files -p | %s" % pager
)
576 os
.system("git diff-index --cached -p HEAD | %s" % pager
)
578 os
.system("git diff-index -p HEAD | %s" % pager
)
580 @short_help("list, create, or delete branches")
582 If no arguments are specified, a list of local branches is given. The
583 current branch is indicated by a "*" next to the name. If an argument
584 is given, it is taken as the name of a new branch to create. The branch
585 will start pointing at the current HEAD. See 'point' for details on
586 changing the revision of the new branch. Note that this command does
587 not switch the current working branch. See 'switch' for details on
588 changing the current working branch.
590 The '-d' flag can be used to delete local branches. If the delete
591 operation would remove the last branch reference to a given line of
592 history (colloquially referred to as "dangling commits"), yap will
593 report an error and abort. The '-f' flag can be used to force the delete
596 @takes_options("fd:")
597 def cmd_branch(self
, branch
=None, **flags
):
598 "[ [-f] -d <branch> | <branch> ]"
599 force
= '-f' in flags
601 self
._delete
_branch
(flags
['-d'], force
)
605 if branch
is not None:
606 ref
= get_output("git rev-parse --verify HEAD")
608 raise YapError("No branch point yet. Make a commit")
609 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
611 current
= get_output("git symbolic-ref HEAD")[0]
612 branches
= get_output("git for-each-ref --format='%(refname)' 'refs/heads/*'")
618 b
= b
.replace('refs/heads/', '')
621 @short_help("change the current working branch")
623 The argument is the name of the branch to make the current working
624 branch. This command will fail if there are uncommitted changes to any
625 files. Otherwise, the contents of the files in the working directory
626 are updated to reflect their state in the new branch. Additionally, any
627 future commits are added to the new branch instead of the previous line
631 def cmd_switch(self
, branch
, **flags
):
633 ref
= get_output("git rev-parse --verify 'refs/heads/%s'" % branch
)
635 raise YapError("No such branch: %s" % branch
)
637 if '-f' not in flags
and (self
._get
_unstaged
_files
() or self
._get
_staged
_files
()):
638 raise YapError("You have uncommitted changes. Use -f to continue anyway")
640 if self
._get
_unstaged
_files
() and self
._get
_staged
_files
():
641 raise YapError("You have staged and unstaged changes. Perhaps unstage -a?")
643 staged
= bool(self
._get
_staged
_files
())
645 run_command("git diff-files -p | git apply --cached")
646 for f
in self
._get
_new
_files
():
649 idx
= get_output("git write-tree")
650 new
= get_output("git rev-parse refs/heads/%s" % branch
)
651 run_safely("git read-tree --aggressive -u -m HEAD %s %s" % (idx
[0], new
[0]))
652 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
658 @short_help("move the current branch to a different revision")
660 The argument is the hash of the commit to which the current branch
661 should point, or alternately a branch or tag (a.k.a, "committish"). If
662 moving the branch would create "dangling commits" (see 'branch'), yap
663 will report an error and abort. The '-f' flag can be used to force the
664 operation in spite of this.
667 def cmd_point(self
, where
, **flags
):
669 head
= get_output("git rev-parse --verify HEAD")
671 raise YapError("No commit yet; nowhere to point")
673 ref
= get_output("git rev-parse --verify '%s'" % where
)
675 raise YapError("Not a valid ref: %s" % where
)
677 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
678 raise YapError("You have uncommitted changes. Commit them first")
680 type = get_output("git cat-file -t '%s'" % ref
[0])
681 if type and type[0] == "tag":
682 tag
= get_output("git cat-file tag '%s'" % ref
[0])
683 ref
[0] = tag
[0].split(' ')[1]
685 run_safely("git update-ref HEAD '%s'" % ref
[0])
687 if '-f' not in flags
:
688 name
= get_output("git name-rev --name-only '%s'" % head
[0])[0]
689 if name
== "undefined":
690 os
.system("git update-ref HEAD '%s'" % head
[0])
691 raise YapError("Pointing there will lose commits. Use -f to force")
694 run_safely("git read-tree -u -m HEAD")
696 run_safely("git read-tree HEAD")
697 run_safely("git checkout-index -u -f -a")
699 @short_help("alter history by dropping or amending commits")
701 This command operates in two distinct modes, "amend" and "drop" mode.
702 In drop mode, the given commit is removed from the history of the
703 current branch, as though that commit never happened. By default the
706 In amend mode, the uncommitted changes present are merged into a
707 previous commit. This is useful for correcting typos or adding missed
708 files into past commits. By default the commit used is HEAD.
710 While rewriting history it is possible that conflicts will arise. If
711 this happens, the rewrite will pause and you will be prompted to resolve
712 the conflicts and stage them. Once that is done, you will run "yap
713 history continue." If instead you want the conflicting commit removed
714 from history (perhaps your changes supercede that commit) you can run
715 "yap history skip". Once the rewrite completes, your branch will be on
716 the same commit as when the rewrite started.
718 def cmd_history(self
, subcmd
, *args
):
719 "amend | drop <commit>"
721 if subcmd
not in ("amend", "drop", "continue", "skip"):
725 When you have resolved the conflicts run \"yap history continue\".
726 To skip the problematic patch, run \"yap history skip\"."""
728 if subcmd
== "continue":
729 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
732 os
.system("git reset --hard")
733 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
736 if subcmd
== "amend":
737 flags
, args
= getopt
.getopt(args
, "ad")
747 if run_command("git rev-parse --verify '%s'" % commit
):
748 raise YapError("Not a valid commit: %s" % commit
)
750 self
._check
_rebasing
()
752 if subcmd
== "amend":
753 self
._check
_commit
(**flags
)
754 if self
._get
_unstaged
_files
():
755 # XXX: handle unstaged changes better
756 raise YapError("Commit away changes that you aren't amending")
760 start
= get_output("git rev-parse HEAD")
761 stash
= get_output("git stash create")
762 run_command("git reset --hard")
764 fd
, tmpfile
= tempfile
.mkstemp("yap")
768 os
.system("git format-patch -k --stdout '%s' > %s" % (commit
, tmpfile
))
769 if subcmd
== "amend":
770 self
.cmd_point(commit
, **{'-f': True})
772 if subcmd
== "amend":
774 rc
= os
.system("git stash apply %s" % stash
[0])
776 self
.cmd_point(start
[0], **{'-f': True})
777 os
.system("git stash apply %s" % stash
[0])
778 raise YapError("Failed to apply stash")
781 if subcmd
== "amend":
783 for f
in self
._get
_unstaged
_files
():
787 self
.cmd_point("%s^" % commit
, **{'-f': True})
789 stat
= os
.stat(tmpfile
)
792 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
794 raise YapError("Failed to apply changes")
799 run_command("git stash apply %s" % stash
[0])
802 @short_help("show the changes introduced by a given commit")
804 By default, the changes in the last commit are shown. To override this,
805 specify a hash, branch, or tag (committish). The hash of the commit,
806 the commit's author, log message, and a diff of the changes are shown.
808 def cmd_show(self
, commit
="HEAD"):
810 os
.system("git show '%s'" % commit
)
812 @short_help("apply the changes in a given commit to the current branch")
814 The argument is the hash, branch, or tag (committish) of the commit to
815 be applied. In general, it only makes sense to apply commits that
816 happened on another branch. The '-r' flag can be used to have the
817 changes in the given commit reversed from the current branch. In
818 general, this only makes sense for commits that happened on the current
822 def cmd_cherry_pick(self
, commit
, **flags
):
825 os
.system("git revert '%s'" % commit
)
827 os
.system("git cherry-pick '%s'" % commit
)
829 @short_help("list, add, or delete configured remote repositories")
831 When invoked with no arguments, this command will show the list of
832 currently configured remote repositories, giving both the name and URL
833 of each. To add a new repository, give the desired name as the first
834 argument and the URL as the second. The '-d' flag can be used to remove
835 a previously added repository.
838 def cmd_repo(self
, name
=None, url
=None, **flags
):
839 "[<name> <url> | -d <name>]"
840 if name
is not None and url
is None:
844 if flags
['-d'] not in [ x
[0] for x
in self
._list
_remotes
() ]:
845 raise YapError("No such repository: %s" % flags
['-d'])
846 os
.system("git config --unset remote.%s.url" % flags
['-d'])
847 os
.system("git config --unset remote.%s.fetch" % flags
['-d'])
850 if name
in [ x
[0] for x
in self
._list
_remotes
() ]:
851 raise YapError("Repository '%s' already exists" % flags
['-d'])
852 os
.system("git config remote.%s.url %s" % (name
, url
))
853 os
.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name
, name
))
855 for remote
, url
in self
._list
_remotes
():
856 print "%-20s %s" % (remote
, url
)
858 @takes_options("cdf")
859 @short_help("send local commits to a remote repository")
860 def cmd_push(self
, repo
=None, rhs
=None, **flags
):
863 if '-c' in flags
and '-d' in flags
:
866 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
867 raise YapError("No such repository: %s" % repo
)
869 current
= get_output("git symbolic-ref HEAD")
871 raise YapError("Not on a branch!")
873 current
= current
[0].replace('refs/heads/', '')
874 remote
= get_output("git config branch.%s.remote" % current
)
875 if repo
is None and remote
:
879 raise YapError("No tracking branch configured; specify destination repository")
881 if rhs
is None and remote
and remote
[0] == repo
:
882 merge
= get_output("git config branch.%s.merge" % current
)
887 rhs
= "refs/heads/%s" % current
889 if '-c' not in flags
and '-d' not in flags
:
890 if run_command("git rev-parse --verify refs/remotes/%s/%s"
891 % (repo
, rhs
.replace('refs/heads/', ''))):
892 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
893 if '-f' not in flags
:
894 hash = get_output("git rev-parse refs/remotes/%s/%s" % (repo
, rhs
.replace('refs/heads/', '')))
895 base
= get_output("git merge-base HEAD %s" % hash[0])
897 if base
[0] != hash[0]:
898 raise YapError("Branch not up-to-date with remote. Update or use -f")
900 print "About to push local branch '%s' to '%s' on '%s'" % (current
, rhs
, repo
)
901 print "Continue (y/n)? ",
903 ans
= sys
.stdin
.readline().strip()
905 if ans
.lower() != 'y' and ans
.lower() != 'yes':
906 raise YapError("Aborted.")
914 lhs
= "refs/heads/%s" % current
915 rc
= os
.system("git push %s %s %s:%s" % (flags
.get('-f', ''), repo
, lhs
, rhs
))
917 raise YapError("Push failed.")
919 @short_help("retrieve commits from a remote repository")
920 def cmd_fetch(self
, repo
=None):
922 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
923 raise YapError("No such repository: %s" % repo
)
925 remote
= get_output("git config branch.%s.remote" % current
)
928 raise YapError("No tracking branch configured; specify a repository")
929 os
.system("git fetch %s" % repo
)
931 @short_help("update the current branch relative to its tracking branch")
932 def cmd_update(self
, subcmd
=None):
934 if subcmd
and subcmd
not in ["continue", "skip"]:
938 When you have resolved the conflicts run \"yap update continue\".
939 To skip the problematic patch, run \"yap update skip\"."""
941 if subcmd
== "continue":
942 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
945 os
.system("git reset --hard")
946 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
949 self
._check
_rebasing
()
950 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
951 raise YapError("You have uncommitted changes. Commit them first")
953 current
= get_output("git symbolic-ref HEAD")
955 raise YapError("Not on a branch!")
957 current
= current
[0].replace('refs/heads/', '')
958 remote
, merge
= self
._get
_tracking
(current
)
959 merge
= merge
[0].replace('refs/heads/', '')
961 self
.cmd_fetch(remote
)
962 base
= get_output("git merge-base HEAD refs/remotes/%s/%s" % (remote
, merge
))
965 fd
, tmpfile
= tempfile
.mkstemp("yap")
967 os
.system("git format-patch -k --stdout '%s' > %s" % (base
[0], tmpfile
))
968 self
.cmd_point("refs/remotes/%s/%s" % (remote
, merge
), **{'-f': True})
970 stat
= os
.stat(tmpfile
)
973 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
975 raise YapError("Failed to apply changes")
979 @short_help("query and configure remote branch tracking")
980 def cmd_track(self
, repo
=None, branch
=None):
983 current
= get_output("git symbolic-ref HEAD")
985 raise YapError("Not on a branch!")
986 current
= current
[0].replace('refs/heads/', '')
988 if repo
is None and branch
is None:
989 repo
, merge
= self
._get
_tracking
(current
)
990 merge
= merge
[0].replace('refs/heads/', '')
991 print "Branch '%s' tracking refs/remotes/%s/%s" % (current
, repo
, merge
)
994 if repo
is None or branch
is None:
997 if repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
998 raise YapError("No such repository: %s" % repo
)
1000 if run_command("git rev-parse --verify refs/remotes/%s/%s" % (repo
, branch
)):
1001 raise YapError("No such branch '%s' on repository '%s'" % (repo
, branch
))
1003 os
.system("git config branch.%s.remote '%s'" % (current
, repo
))
1004 os
.system("git config branch.%s.merge 'refs/heads/%s'" % (current
, branch
))
1005 print "Branch '%s' now tracking refs/remotes/%s/%s" % (current
, repo
, branch
)
1007 @short_help("mark files with conflicts as resolved")
1008 def cmd_resolved(self
, *args
):
1014 self
._stage
_one
(f
, True)
1017 def cmd_help(self
, cmd
=None):
1020 attr
= self
.__getattribute
__("cmd_"+cmd
.replace('-', '_'))
1021 except AttributeError:
1022 raise YapError("No such command: %s" % cmd
)
1024 help = attr
.long_help
1025 except AttributeError:
1026 raise YapError("Sorry, no help for '%s'. Ask Steven." % cmd
)
1028 print >>sys
.stderr
, "The '%s' command" % cmd
1029 print >>sys
.stderr
, "\tyap %s %s" % (cmd
, attr
.__doc
__)
1030 print >>sys
.stderr
, "%s" % help
1033 print >> sys
.stderr
, "Yet Another (Git) Porcelein"
1036 for name
in dir(self
):
1037 if not name
.startswith('cmd_'):
1039 attr
= self
.__getattribute
__(name
)
1040 if not callable(attr
):
1043 short_msg
= attr
.short_help
1044 except AttributeError:
1047 name
= name
.replace('cmd_', '')
1048 name
= name
.replace('_', '-')
1049 print >> sys
.stderr
, "%-16s%s" % (name
, short_msg
)
1051 print >> sys
.stderr
, "(*) Indicates that the command is not readily reversible"
1053 def cmd_usage(self
):
1054 print >> sys
.stderr
, "usage: %s <command>" % os
.path
.basename(sys
.argv
[0])
1055 print >> sys
.stderr
, " valid commands: help init clone add rm stage unstage status revert commit uncommit log show diff branch switch point cherry-pick repo track push fetch update history resolved version"
1057 def main(self
, args
):
1065 debug
= os
.getenv('YAP_DEBUG')
1068 command
= command
.replace('-', '_')
1071 for p
in self
.plugins
:
1073 meth
= p
.__getattribute
__("cmd_"+command
)
1074 except AttributeError:
1078 default_meth
= self
.__getattribute
__("cmd_"+command
)
1079 except AttributeError:
1085 raise AttributeError
1088 if "options" in meth
.__dict
__:
1089 options
= meth
.options
1090 if default_meth
and "options" in default_meth
.__dict
__:
1091 options
+= default_meth
.options
1092 flags
, args
= getopt
.getopt(args
, options
)
1098 for p
in self
.plugins
:
1100 meth
= p
.__getattribute
__("pre_"+command
)
1101 except AttributeError:
1103 meth(*args
, **flags
)
1105 meth(*args
, **flags
)
1108 for p
in self
.plugins
:
1110 meth
= p
.__getattribute
__("post_"+command
)
1111 except AttributeError:
1115 except (TypeError, getopt
.GetoptError
):
1118 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, meth
.__doc
__)
1120 print >> sys
.stderr
, e
1122 except AttributeError: