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")
133 current
= current
[0].replace('refs/heads/', '')
134 if branch
== current
:
135 raise YapError("Can't delete current branch")
137 ref
= get_output("git rev-parse --verify 'refs/heads/%s'" % branch
)
139 raise YapError("No such branch: %s" % branch
)
140 run_safely("git update-ref -d 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
143 name
= get_output("git name-rev --name-only '%s'" % ref
[0])[0]
144 if name
== 'undefined':
145 run_command("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
146 raise YapError("Refusing to delete leaf branch (use -f to force)")
147 def _get_pager_cmd(self
):
148 if 'YAP_PAGER' in os
.environ
:
149 return os
.environ
['YAP_PAGER']
150 elif 'GIT_PAGER' in os
.environ
:
151 return os
.environ
['GIT_PAGER']
152 elif 'PAGER' in os
.environ
:
153 return os
.environ
['PAGER']
157 def _add_one(self
, file):
158 self
._assert
_file
_exists
(file)
159 x
= get_output("git ls-files '%s'" % file)
161 raise YapError("File '%s' already in repository" % file)
162 self
._add
_new
_file
(file)
164 def _rm_one(self
, file):
165 self
._assert
_file
_exists
(file)
166 if get_output("git ls-files '%s'" % file) != []:
167 run_safely("git rm --cached '%s'" % file)
168 self
._remove
_new
_file
(file)
170 def _stage_one(self
, file, allow_unmerged
=False):
171 self
._assert
_file
_exists
(file)
172 prefix
= get_output("git rev-parse --show-prefix")
174 tmp
= os
.path
.normpath(os
.path
.join(prefix
[0], file))
177 if not allow_unmerged
and tmp
in self
._get
_unmerged
_files
():
178 raise YapError("Refusing to stage conflicted file: %s" % file)
179 run_safely("git update-index --add '%s'" % file)
181 def _unstage_one(self
, file):
182 self
._assert
_file
_exists
(file)
183 if run_command("git rev-parse HEAD"):
184 run_safely("git update-index --force-remove '%s'" % file)
186 run_safely("git diff-index -p HEAD '%s' | git apply -R --cached" % file)
188 def _revert_one(self
, file):
189 self
._assert
_file
_exists
(file)
190 self
._unstage
_one
(file)
191 run_safely("git checkout-index -u -f '%s'" % file)
193 def _parse_commit(self
, commit
):
194 lines
= get_output("git cat-file commit '%s'" % commit
)
199 if mode
!= 'commit' and l
.strip() == "":
204 commit
['log'].append(l
)
211 commit
['log'] = '\n'.join(commit
['log'])
214 def _check_commit(self
, **flags
):
215 if '-a' in flags
and '-d' in flags
:
216 raise YapError("Conflicting flags: -a and -d")
218 if '-d' not in flags
and self
._get
_unstaged
_files
():
219 if '-a' not in flags
and self
._get
_staged
_files
():
220 raise YapError("Staged and unstaged changes present. Specify what to commit")
221 os
.system("git diff-files -p | git apply --cached")
222 for f
in self
._get
_new
_files
():
225 def _do_uncommit(self
):
226 commit
= self
._parse
_commit
("HEAD")
227 repo
= get_output('git rev-parse --git-dir')[0]
228 dir = os
.path
.join(repo
, 'yap')
233 msg_file
= os
.path
.join(dir, 'msg')
234 fd
= file(msg_file
, 'w')
235 print >>fd
, commit
['log']
238 tree
= get_output("git rev-parse --verify HEAD^")
239 run_safely("git update-ref -m uncommit HEAD '%s'" % tree
[0])
241 def _do_commit(self
, msg
=None):
242 tree
= get_output("git write-tree")[0]
243 parent
= get_output("git rev-parse --verify HEAD 2> /dev/null")[0]
245 if os
.environ
.has_key('YAP_EDITOR'):
246 editor
= os
.environ
['YAP_EDITOR']
247 elif os
.environ
.has_key('GIT_EDITOR'):
248 editor
= os
.environ
['GIT_EDITOR']
249 elif os
.environ
.has_key('EDITOR'):
250 editor
= os
.environ
['EDITOR']
254 fd
, tmpfile
= tempfile
.mkstemp("yap")
259 repo
= get_output('git rev-parse --git-dir')[0]
260 msg_file
= os
.path
.join(repo
, 'yap', 'msg')
261 if os
.access(msg_file
, os
.R_OK
):
263 fd2
= file(tmpfile
, 'w')
264 for l
in fd1
.xreadlines():
265 print >>fd2
, l
.strip()
268 if os
.system("%s '%s'" % (editor
, tmpfile
)) != 0:
269 raise YapError("Editing commit message failed")
276 raise YapError("Refusing to use empty commit message")
278 (fd_w
, fd_r
) = os
.popen2("git stripspace > %s" % tmpfile
)
284 commit
= get_output("git commit-tree '%s' -p '%s' < '%s'" % (tree
, parent
, tmpfile
))
286 commit
= get_output("git commit-tree '%s' < '%s'" % (tree
, tmpfile
))
289 run_safely("git update-ref HEAD '%s'" % commit
[0])
291 def _check_rebasing(self
):
292 repo
= get_output('git rev-parse --git-dir')[0]
293 dotest
= os
.path
.join(repo
, '.dotest')
294 if os
.access(dotest
, os
.R_OK
):
295 raise YapError("A git operation is in progress. Complete it first")
296 dotest
= os
.path
.join(repo
, '..', '.dotest')
297 if os
.access(dotest
, os
.R_OK
):
298 raise YapError("A git operation is in progress. Complete it first")
300 def _list_remotes(self
):
301 remotes
= get_output("git config --get-regexp '^remote.*.url'")
303 remote
, url
= x
.split(' ')
304 remote
= remote
.replace('remote.', '')
305 remote
= remote
.replace('.url', '')
308 def _unstage_all(self
):
310 run_safely("git read-tree -m HEAD")
312 run_safely("git read-tree HEAD")
313 run_safely("git update-index -q --refresh")
315 def _get_tracking(self
, current
):
316 remote
= get_output("git config branch.%s.remote" % current
)
318 raise YapError("No tracking branch configured for '%s'" % current
)
320 merge
= get_output("git config branch.%s.merge" % current
)
322 raise YapError("No tracking branch configured for '%s'" % current
)
323 return remote
[0], merge
325 @short_help("make a local copy of an existing repository")
327 The first argument is a URL to the existing repository. This can be an
328 absolute path if the repository is local, or a URL with the git://,
329 ssh://, or http:// schemes. By default, the directory used is the last
330 component of the URL, sans '.git'. This can be overridden by providing
333 def cmd_clone(self
, url
, directory
=None):
336 if '://' not in url
and url
[0] != '/':
337 url
= os
.path
.join(os
.getcwd(), url
)
339 url
= url
.rstrip('/')
340 if directory
is None:
341 directory
= url
.rsplit('/')[-1]
342 directory
= directory
.replace('.git', '')
347 raise YapError("Directory exists: %s" % directory
)
350 self
.cmd_repo("origin", url
)
351 self
.cmd_fetch("origin")
354 if not run_command("git rev-parse --verify refs/remotes/origin/HEAD"):
355 hash = get_output("git rev-parse refs/remotes/origin/HEAD")[0]
356 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'"):
357 if get_output("git rev-parse %s" % b
)[0] == hash:
361 if not run_command("git rev-parse --verify refs/remotes/origin/master"):
362 branch
= "refs/remotes/origin/master"
364 branch
= get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'")
367 hash = get_output("git rev-parse %s" % branch
)
369 branch
= branch
.replace('refs/remotes/origin/', '')
370 run_safely("git update-ref refs/heads/%s %s" % (branch
, hash[0]))
371 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
372 self
.cmd_revert(**{'-a': 1})
374 @short_help("turn a directory into a repository")
376 Converts the current working directory into a repository. The primary
377 side-effect of this command is the creation of a '.git' subdirectory.
378 No files are added nor commits made.
381 os
.system("git init")
383 @short_help("add a new file to the repository")
385 The arguments are the files to be added to the repository. Once added,
386 the files will show as "unstaged changes" in the output of 'status'. To
387 reverse the effects of this command, see 'rm'.
389 def cmd_add(self
, *files
):
398 @short_help("delete a file from the repository")
400 The arguments are the files to be removed from the current revision of
401 the repository. The files will still exist in any past commits that the
402 files may have been a part of. The file is not actually deleted, it is
403 just no longer tracked as part of the repository.
405 def cmd_rm(self
, *files
):
414 @short_help("stage changes in a file for commit")
416 The arguments are the files to be staged. Staging changes is a way to
417 build up a commit when you do not want to commit all changes at once.
418 To commit only staged changes, use the '-d' flag to 'commit.' To
419 reverse the effects of this command, see 'unstage'. Once staged, the
420 files will show as "staged changes" in the output of 'status'.
422 def cmd_stage(self
, *files
):
431 @short_help("unstage changes in a file")
433 The arguments are the files to be unstaged. Once unstaged, the files
434 will show as "unstaged changes" in the output of 'status'. The '-a'
435 flag can be used to unstage all staged changes at once.
438 def cmd_unstage(self
, *files
, **flags
):
452 @short_help("show files with staged and unstaged changes")
454 Show the files in the repository with changes since the last commit,
455 categorized based on whether the changes are staged or not. A file may
456 appear under each heading if the same file has both staged and unstaged
459 def cmd_status(self
):
461 branch
= get_output("git symbolic-ref HEAD")
463 branch
= branch
[0].replace('refs/heads/', '')
466 print "Current branch: %s" % branch
468 print "Files with staged changes:"
469 files
= self
._get
_staged
_files
()
475 print "Files with unstaged changes:"
476 files
= self
._get
_unstaged
_files
()
482 files
= self
._get
_unmerged
_files
()
484 print "Files with conflicts:"
488 @short_help("remove uncommitted changes from a file (*)")
490 The arguments are the files whose changes will be reverted. If the '-a'
491 flag is given, then all files will have uncommitted changes removed.
492 Note that there is no way to reverse this command short of manually
493 editing each file again.
496 def cmd_revert(self
, *files
, **flags
):
500 run_safely("git checkout-index -u -f -a")
511 @short_help("record changes to files as a new commit")
513 Create a new commit recording changes since the last commit. If there
514 are only unstaged changes, those will be recorded. If there are only
515 staged changes, those will be recorded. Otherwise, you will have to
516 specify either the '-a' flag or the '-d' flag to commit all changes or
517 only staged changes, respectively. To reverse the effects of this
518 command, see 'uncommit'.
520 @takes_options("adm:")
521 def cmd_commit(self
, **flags
):
523 self
._check
_rebasing
()
524 self
._check
_commit
(**flags
)
525 if not self
._get
_staged
_files
():
526 raise YapError("No changes to commit")
527 msg
= flags
.get('-m', None)
531 @short_help("reverse the actions of the last commit")
533 Reverse the effects of the last 'commit' operation. The changes that
534 were part of the previous commit will show as "staged changes" in the
535 output of 'status'. This means that if no files were changed since the
536 last commit was created, 'uncommit' followed by 'commit' is a lossless
539 def cmd_uncommit(self
):
544 @short_help("report the current version of yap")
545 def cmd_version(self
):
546 print "Yap version 0.1"
548 @short_help("show the changelog for particular versions or files")
550 The arguments are the files with which to filter history. If none are
551 given, all changes are listed. Otherwise only commits that affected one
552 or more of the given files are listed. The -r option changes the
553 starting revision for traversing history. By default, history is listed
556 @takes_options("pr:")
557 def cmd_log(self
, *paths
, **flags
):
558 "[-p] [-r <rev>] <path>..."
559 rev
= flags
.get('-r', 'HEAD')
560 paths
= ' '.join(paths
)
563 os
.system("git log %s '%s' -- %s"
564 % (flags
.get('-p', '--name-status'), rev
, paths
))
566 @short_help("show staged, unstaged, or all uncommitted changes")
568 Show staged, unstaged, or all uncommitted changes. By default, all
569 changes are shown. The '-u' flag causes only unstaged changes to be
570 shown. The '-d' flag causes only staged changes to be shown.
573 def cmd_diff(self
, **flags
):
575 if '-u' in flags
and '-d' in flags
:
576 raise YapError("Conflicting flags: -u and -d")
578 pager
= self
._get
_pager
_cmd
()
581 os
.system("git diff-files -p | %s" % pager
)
583 os
.system("git diff-index --cached -p HEAD | %s" % pager
)
585 os
.system("git diff-index -p HEAD | %s" % pager
)
587 @short_help("list, create, or delete branches")
589 If no arguments are specified, a list of local branches is given. The
590 current branch is indicated by a "*" next to the name. If an argument
591 is given, it is taken as the name of a new branch to create. The branch
592 will start pointing at the current HEAD. See 'point' for details on
593 changing the revision of the new branch. Note that this command does
594 not switch the current working branch. See 'switch' for details on
595 changing the current working branch.
597 The '-d' flag can be used to delete local branches. If the delete
598 operation would remove the last branch reference to a given line of
599 history (colloquially referred to as "dangling commits"), yap will
600 report an error and abort. The '-f' flag can be used to force the delete
603 @takes_options("fd:")
604 def cmd_branch(self
, branch
=None, **flags
):
605 "[ [-f] -d <branch> | <branch> ]"
606 force
= '-f' in flags
608 self
._delete
_branch
(flags
['-d'], force
)
612 if branch
is not None:
613 ref
= get_output("git rev-parse --verify HEAD")
615 raise YapError("No branch point yet. Make a commit")
616 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
618 current
= get_output("git symbolic-ref HEAD")
619 branches
= get_output("git for-each-ref --format='%(refname)' 'refs/heads/*'")
621 if current
and b
== current
[0]:
625 b
= b
.replace('refs/heads/', '')
628 @short_help("change the current working branch")
630 The argument is the name of the branch to make the current working
631 branch. This command will fail if there are uncommitted changes to any
632 files. Otherwise, the contents of the files in the working directory
633 are updated to reflect their state in the new branch. Additionally, any
634 future commits are added to the new branch instead of the previous line
638 def cmd_switch(self
, branch
, **flags
):
640 ref
= get_output("git rev-parse --verify 'refs/heads/%s'" % branch
)
642 raise YapError("No such branch: %s" % branch
)
644 if '-f' not in flags
and (self
._get
_unstaged
_files
() or self
._get
_staged
_files
()):
645 raise YapError("You have uncommitted changes. Use -f to continue anyway")
647 if self
._get
_unstaged
_files
() and self
._get
_staged
_files
():
648 raise YapError("You have staged and unstaged changes. Perhaps unstage -a?")
650 staged
= bool(self
._get
_staged
_files
())
652 run_command("git diff-files -p | git apply --cached")
653 for f
in self
._get
_new
_files
():
656 idx
= get_output("git write-tree")
657 new
= get_output("git rev-parse refs/heads/%s" % branch
)
658 if os
.system("git read-tree --aggressive -u -m HEAD %s %s" % (idx
[0], new
[0])):
659 raise YapError("Failed to switch")
660 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
666 @short_help("move the current branch to a different revision")
668 The argument is the hash of the commit to which the current branch
669 should point, or alternately a branch or tag (a.k.a, "committish"). If
670 moving the branch would create "dangling commits" (see 'branch'), yap
671 will report an error and abort. The '-f' flag can be used to force the
672 operation in spite of this.
675 def cmd_point(self
, where
, **flags
):
677 head
= get_output("git rev-parse --verify HEAD")
679 raise YapError("No commit yet; nowhere to point")
681 ref
= get_output("git rev-parse --verify '%s'" % where
)
683 raise YapError("Not a valid ref: %s" % where
)
685 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
686 raise YapError("You have uncommitted changes. Commit them first")
688 type = get_output("git cat-file -t '%s'" % ref
[0])
689 if type and type[0] == "tag":
690 tag
= get_output("git cat-file tag '%s'" % ref
[0])
691 ref
[0] = tag
[0].split(' ')[1]
693 run_safely("git update-ref HEAD '%s'" % ref
[0])
695 if '-f' not in flags
:
696 name
= get_output("git name-rev --name-only '%s'" % head
[0])[0]
697 if name
== "undefined":
698 os
.system("git update-ref HEAD '%s'" % head
[0])
699 raise YapError("Pointing there will lose commits. Use -f to force")
702 run_safely("git read-tree -u -m HEAD")
704 run_safely("git read-tree HEAD")
705 run_safely("git checkout-index -u -f -a")
707 @short_help("alter history by dropping or amending commits")
709 This command operates in two distinct modes, "amend" and "drop" mode.
710 In drop mode, the given commit is removed from the history of the
711 current branch, as though that commit never happened. By default the
714 In amend mode, the uncommitted changes present are merged into a
715 previous commit. This is useful for correcting typos or adding missed
716 files into past commits. By default the commit used is HEAD.
718 While rewriting history it is possible that conflicts will arise. If
719 this happens, the rewrite will pause and you will be prompted to resolve
720 the conflicts and stage them. Once that is done, you will run "yap
721 history continue." If instead you want the conflicting commit removed
722 from history (perhaps your changes supercede that commit) you can run
723 "yap history skip". Once the rewrite completes, your branch will be on
724 the same commit as when the rewrite started.
726 def cmd_history(self
, subcmd
, *args
):
727 "amend | drop <commit>"
729 if subcmd
not in ("amend", "drop", "continue", "skip"):
733 When you have resolved the conflicts run \"yap history continue\".
734 To skip the problematic patch, run \"yap history skip\"."""
736 if subcmd
== "continue":
737 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
740 os
.system("git reset --hard")
741 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
744 if subcmd
== "amend":
745 flags
, args
= getopt
.getopt(args
, "ad")
755 if run_command("git rev-parse --verify '%s'" % commit
):
756 raise YapError("Not a valid commit: %s" % commit
)
758 self
._check
_rebasing
()
760 if subcmd
== "amend":
761 self
._check
_commit
(**flags
)
762 if self
._get
_unstaged
_files
():
763 # XXX: handle unstaged changes better
764 raise YapError("Commit away changes that you aren't amending")
768 start
= get_output("git rev-parse HEAD")
769 stash
= get_output("git stash create")
770 run_command("git reset --hard")
772 fd
, tmpfile
= tempfile
.mkstemp("yap")
776 os
.system("git format-patch -k --stdout '%s' > %s" % (commit
, tmpfile
))
777 if subcmd
== "amend":
778 self
.cmd_point(commit
, **{'-f': True})
780 if subcmd
== "amend":
782 rc
= os
.system("git stash apply %s" % stash
[0])
784 self
.cmd_point(start
[0], **{'-f': True})
785 os
.system("git stash apply %s" % stash
[0])
786 raise YapError("Failed to apply stash")
789 if subcmd
== "amend":
791 for f
in self
._get
_unstaged
_files
():
795 self
.cmd_point("%s^" % commit
, **{'-f': True})
797 stat
= os
.stat(tmpfile
)
800 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
802 raise YapError("Failed to apply changes")
807 run_command("git stash apply %s" % stash
[0])
810 @short_help("show the changes introduced by a given commit")
812 By default, the changes in the last commit are shown. To override this,
813 specify a hash, branch, or tag (committish). The hash of the commit,
814 the commit's author, log message, and a diff of the changes are shown.
816 def cmd_show(self
, commit
="HEAD"):
818 os
.system("git show '%s'" % commit
)
820 @short_help("apply the changes in a given commit to the current branch")
822 The argument is the hash, branch, or tag (committish) of the commit to
823 be applied. In general, it only makes sense to apply commits that
824 happened on another branch. The '-r' flag can be used to have the
825 changes in the given commit reversed from the current branch. In
826 general, this only makes sense for commits that happened on the current
830 def cmd_cherry_pick(self
, commit
, **flags
):
833 os
.system("git revert '%s'" % commit
)
835 os
.system("git cherry-pick '%s'" % commit
)
837 @short_help("list, add, or delete configured remote repositories")
839 When invoked with no arguments, this command will show the list of
840 currently configured remote repositories, giving both the name and URL
841 of each. To add a new repository, give the desired name as the first
842 argument and the URL as the second. The '-d' flag can be used to remove
843 a previously added repository.
846 def cmd_repo(self
, name
=None, url
=None, **flags
):
847 "[<name> <url> | -d <name>]"
848 if name
is not None and url
is None:
852 if flags
['-d'] not in [ x
[0] for x
in self
._list
_remotes
() ]:
853 raise YapError("No such repository: %s" % flags
['-d'])
854 os
.system("git config --unset remote.%s.url" % flags
['-d'])
855 os
.system("git config --unset remote.%s.fetch" % flags
['-d'])
858 if name
in [ x
[0] for x
in self
._list
_remotes
() ]:
859 raise YapError("Repository '%s' already exists" % flags
['-d'])
860 os
.system("git config remote.%s.url %s" % (name
, url
))
861 os
.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name
, name
))
863 for remote
, url
in self
._list
_remotes
():
864 print "%-20s %s" % (remote
, url
)
866 @short_help("send local commits to a remote repository")
868 When invoked with no arguments, the current branch is synchronized to
869 the tracking branch of the tracking remote. If no tracking remote is
870 specified, the repository will have to be specified on the command line.
871 In that case, the default is to push to a branch with the same name as
872 the current branch. This behavior can be overridden by giving a second
873 argument to specify the remote branch.
875 If the remote branch does not currently exist, the command will abort
876 unless the -c flag is provided. If the remote branch is not a direct
877 descendent of the local branch, the command will abort unless the -f
878 flag is provided. Forcing a push in this way can be problematic to
879 other users of the repository if they are not expecting it.
881 To delete a branch on the remote repository, use the -d flag.
883 @takes_options("cdf")
884 def cmd_push(self
, repo
=None, rhs
=None, **flags
):
887 if '-c' in flags
and '-d' in flags
:
890 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
891 raise YapError("No such repository: %s" % repo
)
893 current
= get_output("git symbolic-ref HEAD")
895 raise YapError("Not on a branch!")
897 current
= current
[0].replace('refs/heads/', '')
898 remote
= get_output("git config branch.%s.remote" % current
)
899 if repo
is None and remote
:
903 raise YapError("No tracking branch configured; specify destination repository")
905 if rhs
is None and remote
and remote
[0] == repo
:
906 merge
= get_output("git config branch.%s.merge" % current
)
911 rhs
= "refs/heads/%s" % current
913 if '-c' not in flags
and '-d' not in flags
:
914 if run_command("git rev-parse --verify refs/remotes/%s/%s"
915 % (repo
, rhs
.replace('refs/heads/', ''))):
916 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
917 if '-f' not in flags
:
918 hash = get_output("git rev-parse refs/remotes/%s/%s" % (repo
, rhs
.replace('refs/heads/', '')))
919 base
= get_output("git merge-base HEAD %s" % hash[0])
921 if base
[0] != hash[0]:
922 raise YapError("Branch not up-to-date with remote. Update or use -f")
924 print "About to push local branch '%s' to '%s' on '%s'" % (current
, rhs
, repo
)
925 print "Continue (y/n)? ",
927 ans
= sys
.stdin
.readline().strip()
929 if ans
.lower() != 'y' and ans
.lower() != 'yes':
930 raise YapError("Aborted.")
938 lhs
= "refs/heads/%s" % current
939 rc
= os
.system("git push %s %s %s:%s" % (flags
.get('-f', ''), repo
, lhs
, rhs
))
941 raise YapError("Push failed.")
943 @short_help("retrieve commits from a remote repository")
945 When run with no arguments, the command will retrieve new commits from
946 the remote tracking repository. Note that this does not in any way
947 alter the current branch. For that, see "update". If a remote other
948 than the tracking remote is desired, it can be specified as the first
951 def cmd_fetch(self
, repo
=None):
953 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
954 raise YapError("No such repository: %s" % repo
)
956 remote
= get_output("git config branch.%s.remote" % current
)
959 raise YapError("No tracking branch configured; specify a repository")
960 os
.system("git fetch %s" % repo
)
962 @short_help("update the current branch relative to its tracking branch")
964 Updates the current branch relative to its remote tracking branch. This
965 command requires that the current branch have a remote tracking branch
966 configured. If any conflicts occur while applying your changes to the
967 updated remote, the command will pause to allow you to fix them. Once
968 that is done, run "update" with the "continue" subcommand. Alternately,
969 the "skip" subcommand can be used to discard the conflicting changes.
971 def cmd_update(self
, subcmd
=None):
973 if subcmd
and subcmd
not in ["continue", "skip"]:
977 When you have resolved the conflicts run \"yap update continue\".
978 To skip the problematic patch, run \"yap update skip\"."""
980 if subcmd
== "continue":
981 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
984 os
.system("git reset --hard")
985 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
988 self
._check
_rebasing
()
989 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
990 raise YapError("You have uncommitted changes. Commit them first")
992 current
= get_output("git symbolic-ref HEAD")
994 raise YapError("Not on a branch!")
996 current
= current
[0].replace('refs/heads/', '')
997 remote
, merge
= self
._get
_tracking
(current
)
998 merge
= merge
[0].replace('refs/heads/', '')
1000 self
.cmd_fetch(remote
)
1001 base
= get_output("git merge-base HEAD refs/remotes/%s/%s" % (remote
, merge
))
1004 fd
, tmpfile
= tempfile
.mkstemp("yap")
1006 os
.system("git format-patch -k --stdout '%s' > %s" % (base
[0], tmpfile
))
1007 self
.cmd_point("refs/remotes/%s/%s" % (remote
, merge
), **{'-f': True})
1009 stat
= os
.stat(tmpfile
)
1012 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
1014 raise YapError("Failed to apply changes")
1018 @short_help("query and configure remote branch tracking")
1020 When invoked with no arguments, the command displays the tracking
1021 information for the current branch. To configure the tracking
1022 information, two arguments for the remote repository and remote branch
1023 are given. The tracking information is used to provide defaults for
1024 where to push local changes and from where to get updates to the branch.
1026 def cmd_track(self
, repo
=None, branch
=None):
1029 current
= get_output("git symbolic-ref HEAD")
1031 raise YapError("Not on a branch!")
1032 current
= current
[0].replace('refs/heads/', '')
1034 if repo
is None and branch
is None:
1035 repo
, merge
= self
._get
_tracking
(current
)
1036 merge
= merge
[0].replace('refs/heads/', '')
1037 print "Branch '%s' tracking refs/remotes/%s/%s" % (current
, repo
, merge
)
1040 if repo
is None or branch
is None:
1043 if repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1044 raise YapError("No such repository: %s" % repo
)
1046 if run_command("git rev-parse --verify refs/remotes/%s/%s" % (repo
, branch
)):
1047 raise YapError("No such branch '%s' on repository '%s'" % (repo
, branch
))
1049 os
.system("git config branch.%s.remote '%s'" % (current
, repo
))
1050 os
.system("git config branch.%s.merge 'refs/heads/%s'" % (current
, branch
))
1051 print "Branch '%s' now tracking refs/remotes/%s/%s" % (current
, repo
, branch
)
1053 @short_help("mark files with conflicts as resolved")
1055 The arguments are the files to be marked resolved. When a conflict
1056 occurs while merging changes to a file, that file is marked as
1057 "unmerged." Until the file(s) with conflicts are marked resolved,
1058 commits cannot be made.
1060 def cmd_resolved(self
, *args
):
1066 self
._stage
_one
(f
, True)
1069 def cmd_help(self
, cmd
=None):
1072 attr
= self
.__getattribute
__("cmd_"+cmd
.replace('-', '_'))
1073 except AttributeError:
1074 raise YapError("No such command: %s" % cmd
)
1076 help = attr
.long_help
1077 except AttributeError:
1078 raise YapError("Sorry, no help for '%s'. Ask Steven." % cmd
)
1080 print >>sys
.stderr
, "The '%s' command" % cmd
1081 print >>sys
.stderr
, "\tyap %s %s" % (cmd
, attr
.__doc
__)
1082 print >>sys
.stderr
, "%s" % help
1085 print >> sys
.stderr
, "Yet Another (Git) Porcelein"
1088 for name
in dir(self
):
1089 if not name
.startswith('cmd_'):
1091 attr
= self
.__getattribute
__(name
)
1092 if not callable(attr
):
1095 short_msg
= attr
.short_help
1096 except AttributeError:
1099 name
= name
.replace('cmd_', '')
1100 name
= name
.replace('_', '-')
1101 print >> sys
.stderr
, "%-16s%s" % (name
, short_msg
)
1103 print >> sys
.stderr
, "(*) Indicates that the command is not readily reversible"
1105 def cmd_usage(self
):
1106 print >> sys
.stderr
, "usage: %s <command>" % os
.path
.basename(sys
.argv
[0])
1107 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"
1109 def main(self
, args
):
1117 debug
= os
.getenv('YAP_DEBUG')
1120 command
= command
.replace('-', '_')
1123 for p
in self
.plugins
:
1125 meth
= p
.__getattribute
__("cmd_"+command
)
1126 except AttributeError:
1130 default_meth
= self
.__getattribute
__("cmd_"+command
)
1131 except AttributeError:
1137 raise AttributeError
1140 if "options" in meth
.__dict
__:
1141 options
= meth
.options
1142 if default_meth
and "options" in default_meth
.__dict
__:
1143 options
+= default_meth
.options
1144 flags
, args
= getopt
.getopt(args
, options
)
1150 for p
in self
.plugins
:
1152 meth
= p
.__getattribute
__("pre_"+command
)
1153 except AttributeError:
1155 meth(*args
, **flags
)
1157 meth(*args
, **flags
)
1160 for p
in self
.plugins
:
1162 meth
= p
.__getattribute
__("post_"+command
)
1163 except AttributeError:
1167 except (TypeError, getopt
.GetoptError
):
1170 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, meth
.__doc
__)
1172 print >> sys
.stderr
, e
1174 except AttributeError: