8 from plugin
import YapPlugin
11 class ShellError(Exception):
12 def __init__(self
, cmd
, rc
):
17 return "%s returned %d" % (self
.cmd
, self
.rc
)
19 class YapError(Exception):
20 def __init__(self
, msg
):
30 plugindir
= os
.path
.expanduser("~/.yap/plugins")
31 for p
in glob
.glob(os
.path
.join(plugindir
, "*.py")):
34 for k
, cls
in glbls
.items():
35 if not type(cls
) == type:
37 if not issubclass(cls
, YapPlugin
):
44 if not func
.startswith('cmd_'):
46 if func
in self
.overrides
:
47 print >>sys
.stderr
, "Plugin %s overrides already overridden function %s. Disabling" % (p
, func
)
51 def _add_new_file(self
, file):
52 repo
= get_output('git rev-parse --git-dir')[0]
53 dir = os
.path
.join(repo
, 'yap')
58 files
= self
._get
_new
_files
()
60 path
= os
.path
.join(dir, 'new-files')
61 pickle
.dump(files
, open(path
, 'w'))
63 def _get_new_files(self
):
64 repo
= get_output('git rev-parse --git-dir')[0]
65 path
= os
.path
.join(repo
, 'yap', 'new-files')
67 files
= pickle
.load(file(path
))
74 if get_output("git ls-files --cached '%s'" % f
) != []:
79 def _remove_new_file(self
, file):
80 files
= self
._get
_new
_files
()
81 files
= filter(lambda x
: x
!= file, files
)
83 repo
= get_output('git rev-parse --git-dir')[0]
84 path
= os
.path
.join(repo
, 'yap', 'new-files')
86 pickle
.dump(files
, open(path
, 'w'))
90 def _clear_new_files(self
):
91 repo
= get_output('git rev-parse --git-dir')[0]
92 path
= os
.path
.join(repo
, 'yap', 'new-files')
95 def _assert_file_exists(self
, file):
96 if not os
.access(file, os
.R_OK
):
97 raise YapError("No such file: %s" % file)
99 def _get_staged_files(self
):
100 if run_command("git rev-parse HEAD"):
101 files
= get_output("git ls-files --cached")
103 files
= get_output("git diff-index --cached --name-only HEAD")
104 unmerged
= self
._get
_unmerged
_files
()
106 unmerged
= set(unmerged
)
107 files
= set(files
).difference(unmerged
)
111 def _get_unstaged_files(self
):
112 files
= get_output("git ls-files -m")
113 prefix
= get_output("git rev-parse --show-prefix")
115 files
= [ os
.path
.join(prefix
[0], x
) for x
in files
]
116 files
+= self
._get
_new
_files
()
117 unmerged
= self
._get
_unmerged
_files
()
119 unmerged
= set(unmerged
)
120 files
= set(files
).difference(unmerged
)
124 def _get_unmerged_files(self
):
125 files
= get_output("git ls-files -u")
126 files
= [ x
.replace('\t', ' ').split(' ')[3] for x
in files
]
127 prefix
= get_output("git rev-parse --show-prefix")
129 files
= [ os
.path
.join(prefix
[0], x
) for x
in files
]
130 return list(set(files
))
132 def _delete_branch(self
, branch
, force
):
133 current
= get_output("git symbolic-ref HEAD")
135 current
= current
[0].replace('refs/heads/', '')
136 if branch
== current
:
137 raise YapError("Can't delete current branch")
139 ref
= get_output("git rev-parse --verify 'refs/heads/%s'" % branch
)
141 raise YapError("No such branch: %s" % branch
)
142 run_safely("git update-ref -d 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
145 name
= get_output("git name-rev --name-only '%s'" % ref
[0])[0]
146 if name
== 'undefined':
147 run_command("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
148 raise YapError("Refusing to delete leaf branch (use -f to force)")
149 def _get_pager_cmd(self
):
150 if 'YAP_PAGER' in os
.environ
:
151 return os
.environ
['YAP_PAGER']
152 elif 'GIT_PAGER' in os
.environ
:
153 return os
.environ
['GIT_PAGER']
154 elif 'PAGER' in os
.environ
:
155 return os
.environ
['PAGER']
159 def _add_one(self
, file):
160 self
._assert
_file
_exists
(file)
161 x
= get_output("git ls-files '%s'" % file)
163 raise YapError("File '%s' already in repository" % file)
164 self
._add
_new
_file
(file)
166 def _rm_one(self
, file):
167 self
._assert
_file
_exists
(file)
168 if get_output("git ls-files '%s'" % file) != []:
169 run_safely("git rm --cached '%s'" % file)
170 self
._remove
_new
_file
(file)
172 def _stage_one(self
, file, allow_unmerged
=False):
173 self
._assert
_file
_exists
(file)
174 prefix
= get_output("git rev-parse --show-prefix")
176 tmp
= os
.path
.normpath(os
.path
.join(prefix
[0], file))
179 if not allow_unmerged
and tmp
in self
._get
_unmerged
_files
():
180 raise YapError("Refusing to stage conflicted file: %s" % file)
181 run_safely("git update-index --add '%s'" % file)
183 def _unstage_one(self
, file):
184 self
._assert
_file
_exists
(file)
185 if run_command("git rev-parse HEAD"):
186 rc
= run_command("git update-index --force-remove '%s'" % file)
188 rc
= run_command("git diff-index -p HEAD '%s' | git apply -R --cached" % file)
190 raise YapError("Failed to unstage")
192 def _revert_one(self
, file):
193 self
._assert
_file
_exists
(file)
195 self
._unstage
_one
(file)
198 run_safely("git checkout-index -u -f '%s'" % file)
200 def _parse_commit(self
, commit
):
201 lines
= get_output("git cat-file commit '%s'" % commit
)
206 if mode
!= 'commit' and l
.strip() == "":
211 commit
['log'].append(l
)
218 commit
['log'] = '\n'.join(commit
['log'])
221 def _check_commit(self
, **flags
):
222 if '-a' in flags
and '-d' in flags
:
223 raise YapError("Conflicting flags: -a and -d")
225 if '-d' not in flags
and self
._get
_unstaged
_files
():
226 if '-a' not in flags
and self
._get
_staged
_files
():
227 raise YapError("Staged and unstaged changes present. Specify what to commit")
228 os
.system("git diff-files -p | git apply --cached")
229 for f
in self
._get
_new
_files
():
232 def _do_uncommit(self
):
233 commit
= self
._parse
_commit
("HEAD")
234 repo
= get_output('git rev-parse --git-dir')[0]
235 dir = os
.path
.join(repo
, 'yap')
240 msg_file
= os
.path
.join(dir, 'msg')
241 fd
= file(msg_file
, 'w')
242 print >>fd
, commit
['log']
245 tree
= get_output("git rev-parse --verify HEAD^")
246 run_safely("git update-ref -m uncommit HEAD '%s'" % tree
[0])
248 def _do_commit(self
, msg
=None):
249 tree
= get_output("git write-tree")[0]
251 repo
= get_output('git rev-parse --git-dir')[0]
252 head_file
= os
.path
.join(repo
, 'yap', 'merge')
254 parent
= pickle
.load(file(head_file
))
256 parent
= get_output("git rev-parse --verify HEAD 2> /dev/null")
258 if os
.environ
.has_key('YAP_EDITOR'):
259 editor
= os
.environ
['YAP_EDITOR']
260 elif os
.environ
.has_key('GIT_EDITOR'):
261 editor
= os
.environ
['GIT_EDITOR']
262 elif os
.environ
.has_key('EDITOR'):
263 editor
= os
.environ
['EDITOR']
267 fd
, tmpfile
= tempfile
.mkstemp("yap")
272 msg_file
= os
.path
.join(repo
, 'yap', 'msg')
273 if os
.access(msg_file
, os
.R_OK
):
275 fd2
= file(tmpfile
, 'w')
276 for l
in fd1
.xreadlines():
277 print >>fd2
, l
.strip()
280 if os
.system("%s '%s'" % (editor
, tmpfile
)) != 0:
281 raise YapError("Editing commit message failed")
288 raise YapError("Refusing to use empty commit message")
290 (fd_w
, fd_r
) = os
.popen2("git stripspace > %s" % tmpfile
)
296 parent
= ' -p '.join(parent
)
297 commit
= get_output("git commit-tree '%s' -p %s < '%s'" % (tree
, parent
, tmpfile
))
299 commit
= get_output("git commit-tree '%s' < '%s'" % (tree
, tmpfile
))
302 run_safely("git update-ref HEAD '%s'" % commit
[0])
305 def _check_rebasing(self
):
306 repo
= get_output('git rev-parse --git-dir')[0]
307 dotest
= os
.path
.join(repo
, '.dotest')
308 if os
.access(dotest
, os
.R_OK
):
309 raise YapError("A git operation is in progress. Complete it first")
310 dotest
= os
.path
.join(repo
, '..', '.dotest')
311 if os
.access(dotest
, os
.R_OK
):
312 raise YapError("A git operation is in progress. Complete it first")
314 def _check_git(self
):
315 if run_command("git rev-parse --git-dir"):
316 raise YapError("That command must be run from inside a git repository")
318 def _list_remotes(self
):
319 remotes
= get_output("git config --get-regexp '^remote.*.url'")
321 remote
, url
= x
.split(' ')
322 remote
= remote
.replace('remote.', '')
323 remote
= remote
.replace('.url', '')
326 def _unstage_all(self
):
328 run_safely("git read-tree -m HEAD")
330 run_safely("git read-tree HEAD")
331 run_safely("git update-index -q --refresh")
333 def _get_tracking(self
, current
):
334 remote
= get_output("git config branch.%s.remote" % current
)
336 raise YapError("No tracking branch configured for '%s'" % current
)
338 merge
= get_output("git config branch.%s.merge" % current
)
340 raise YapError("No tracking branch configured for '%s'" % current
)
341 return remote
[0], merge
[0]
343 def __getattribute__(self
, attr
):
344 if attr
.startswith("cmd_"):
346 for p
in self
.plugins
.values():
348 meth
= p
.__getattribute
__(attr
)
350 except AttributeError:
355 return super(Yap
, self
).__getattribute
__(attr
)
357 def _call_base(self
, method
, *args
, **flags
):
358 base_method
= super(Yap
, self
).__getattribute
__(method
)
359 return base_method(*args
, **flags
)
360 def _confirm_push(self
, current
, rhs
, repo
):
361 print "About to push local branch '%s' to '%s' on '%s'" % (current
, rhs
, repo
)
362 print "Continue (y/n)? ",
364 ans
= sys
.stdin
.readline().strip()
366 if ans
.lower() != 'y' and ans
.lower() != 'yes':
367 raise YapError("Aborted.")
369 def _clear_state(self
):
370 repo
= get_output('git rev-parse --git-dir')[0]
371 dir = os
.path
.join(repo
, 'yap')
373 os
.unlink(os
.path
.join(dir, 'new-files'))
374 os
.unlink(os
.path
.join(dir, 'merge'))
375 os
.unlink(os
.path
.join(dir, 'msg'))
379 @short_help("make a local copy of an existing repository")
381 The first argument is a URL to the existing repository. This can be an
382 absolute path if the repository is local, or a URL with the git://,
383 ssh://, or http:// schemes. By default, the directory used is the last
384 component of the URL, sans '.git'. This can be overridden by providing
387 def cmd_clone(self
, url
, directory
=None):
390 if '://' not in url
and url
[0] != '/':
391 url
= os
.path
.join(os
.getcwd(), url
)
393 url
= url
.rstrip('/')
394 if directory
is None:
395 directory
= url
.rsplit('/')[-1]
396 directory
= directory
.replace('.git', '')
401 raise YapError("Directory exists: %s" % directory
)
404 self
.cmd_repo("origin", url
)
405 self
.cmd_fetch("origin")
408 if not run_command("git rev-parse --verify refs/remotes/origin/HEAD"):
409 hash = get_output("git rev-parse refs/remotes/origin/HEAD")[0]
410 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'"):
411 if get_output("git rev-parse %s" % b
)[0] == hash:
415 if not run_command("git rev-parse --verify refs/remotes/origin/master"):
416 branch
= "refs/remotes/origin/master"
418 branch
= get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin/*'")
421 hash = get_output("git rev-parse %s" % branch
)
423 branch
= branch
.replace('refs/remotes/origin/', '')
424 run_safely("git update-ref refs/heads/%s %s" % (branch
, hash[0]))
425 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
426 self
.cmd_revert(**{'-a': 1})
428 @short_help("turn a directory into a repository")
430 Converts the current working directory into a repository. The primary
431 side-effect of this command is the creation of a '.git' subdirectory.
432 No files are added nor commits made.
435 os
.system("git init")
437 @short_help("add a new file to the repository")
439 The arguments are the files to be added to the repository. Once added,
440 the files will show as "unstaged changes" in the output of 'status'. To
441 reverse the effects of this command, see 'rm'.
443 def cmd_add(self
, *files
):
454 @short_help("delete a file from the repository")
456 The arguments are the files to be removed from the current revision of
457 the repository. The files will still exist in any past commits that the
458 files may have been a part of. The file is not actually deleted, it is
459 just no longer tracked as part of the repository.
461 def cmd_rm(self
, *files
):
471 @short_help("stage changes in a file for commit")
473 The arguments are the files to be staged. Staging changes is a way to
474 build up a commit when you do not want to commit all changes at once.
475 To commit only staged changes, use the '-d' flag to 'commit.' To
476 reverse the effects of this command, see 'unstage'. Once staged, the
477 files will show as "staged changes" in the output of 'status'.
479 def cmd_stage(self
, *files
):
489 @short_help("unstage changes in a file")
491 The arguments are the files to be unstaged. Once unstaged, the files
492 will show as "unstaged changes" in the output of 'status'. The '-a'
493 flag can be used to unstage all staged changes at once.
496 def cmd_unstage(self
, *files
, **flags
):
500 files
= self
._get
_staged
_files
()
509 @short_help("show files with staged and unstaged changes")
511 Show the files in the repository with changes since the last commit,
512 categorized based on whether the changes are staged or not. A file may
513 appear under each heading if the same file has both staged and unstaged
516 def cmd_status(self
):
519 branch
= get_output("git symbolic-ref HEAD")
521 branch
= branch
[0].replace('refs/heads/', '')
524 print "Current branch: %s" % branch
526 print "Files with staged changes:"
527 files
= self
._get
_staged
_files
()
533 print "Files with unstaged changes:"
534 files
= self
._get
_unstaged
_files
()
540 files
= self
._get
_unmerged
_files
()
542 print "Files with conflicts:"
546 @short_help("remove uncommitted changes from a file (*)")
548 The arguments are the files whose changes will be reverted. If the '-a'
549 flag is given, then all files will have uncommitted changes removed.
550 Note that there is no way to reverse this command short of manually
551 editing each file again.
554 def cmd_revert(self
, *files
, **flags
):
559 run_safely("git checkout-index -u -f -a")
571 @short_help("record changes to files as a new commit")
573 Create a new commit recording changes since the last commit. If there
574 are only unstaged changes, those will be recorded. If there are only
575 staged changes, those will be recorded. Otherwise, you will have to
576 specify either the '-a' flag or the '-d' flag to commit all changes or
577 only staged changes, respectively. To reverse the effects of this
578 command, see 'uncommit'.
580 @takes_options("adm:")
581 def cmd_commit(self
, **flags
):
582 "[-a | -d] [-m <msg>]"
584 self
._check
_rebasing
()
585 self
._check
_commit
(**flags
)
586 if not self
._get
_staged
_files
():
587 raise YapError("No changes to commit")
588 msg
= flags
.get('-m', None)
592 @short_help("reverse the actions of the last commit")
594 Reverse the effects of the last 'commit' operation. The changes that
595 were part of the previous commit will show as "staged changes" in the
596 output of 'status'. This means that if no files were changed since the
597 last commit was created, 'uncommit' followed by 'commit' is a lossless
600 def cmd_uncommit(self
):
606 @short_help("report the current version of yap")
607 def cmd_version(self
):
608 print "Yap version 0.1"
610 @short_help("show the changelog for particular versions or files")
612 The arguments are the files with which to filter history. If none are
613 given, all changes are listed. Otherwise only commits that affected one
614 or more of the given files are listed. The -r option changes the
615 starting revision for traversing history. By default, history is listed
618 @takes_options("pr:")
619 def cmd_log(self
, *paths
, **flags
):
620 "[-p] [-r <rev>] <path>..."
622 rev
= flags
.get('-r', 'HEAD')
631 paths
= ' '.join(paths
)
632 os
.system("git log -M -C %s %s '%s' -- %s"
633 % (follow
, flags
.get('-p', '--name-status'), rev
, paths
))
635 @short_help("show staged, unstaged, or all uncommitted changes")
637 Show staged, unstaged, or all uncommitted changes. By default, all
638 changes are shown. The '-u' flag causes only unstaged changes to be
639 shown. The '-d' flag causes only staged changes to be shown.
642 def cmd_diff(self
, **flags
):
645 if '-u' in flags
and '-d' in flags
:
646 raise YapError("Conflicting flags: -u and -d")
648 pager
= self
._get
_pager
_cmd
()
651 os
.system("git diff-files -p | %s" % pager
)
653 os
.system("git diff-index --cached -p HEAD | %s" % pager
)
655 os
.system("git diff-index -p HEAD | %s" % pager
)
657 @short_help("list, create, or delete branches")
659 If no arguments are specified, a list of local branches is given. The
660 current branch is indicated by a "*" next to the name. If an argument
661 is given, it is taken as the name of a new branch to create. The branch
662 will start pointing at the current HEAD. See 'point' for details on
663 changing the revision of the new branch. Note that this command does
664 not switch the current working branch. See 'switch' for details on
665 changing the current working branch.
667 The '-d' flag can be used to delete local branches. If the delete
668 operation would remove the last branch reference to a given line of
669 history (colloquially referred to as "dangling commits"), yap will
670 report an error and abort. The '-f' flag can be used to force the delete
673 @takes_options("fd:")
674 def cmd_branch(self
, branch
=None, **flags
):
675 "[ [-f] -d <branch> | <branch> ]"
677 force
= '-f' in flags
679 self
._delete
_branch
(flags
['-d'], force
)
683 if branch
is not None:
684 ref
= get_output("git rev-parse --verify HEAD")
686 raise YapError("No branch point yet. Make a commit")
687 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
689 current
= get_output("git symbolic-ref HEAD")
690 branches
= get_output("git for-each-ref --format='%(refname)' 'refs/heads/*'")
692 if current
and b
== current
[0]:
696 b
= b
.replace('refs/heads/', '')
699 @short_help("change the current working branch")
701 The argument is the name of the branch to make the current working
702 branch. This command will fail if there are uncommitted changes to any
703 files. Otherwise, the contents of the files in the working directory
704 are updated to reflect their state in the new branch. Additionally, any
705 future commits are added to the new branch instead of the previous line
709 def cmd_switch(self
, branch
, **flags
):
712 self
._check
_rebasing
()
713 ref
= get_output("git rev-parse --verify 'refs/heads/%s'" % branch
)
715 raise YapError("No such branch: %s" % branch
)
717 if '-f' not in flags
:
718 if (self
._get
_staged
_files
()
719 or (self
._get
_unstaged
_files
()
720 and run_command("git update-index --refresh"))):
721 raise YapError("You have uncommitted changes. Use -f to continue anyway")
723 if self
._get
_unstaged
_files
() and self
._get
_staged
_files
():
724 raise YapError("You have staged and unstaged changes. Perhaps unstage -a?")
726 staged
= bool(self
._get
_staged
_files
())
728 run_command("git diff-files -p | git apply --cached")
729 for f
in self
._get
_new
_files
():
732 idx
= get_output("git write-tree")
733 new
= get_output("git rev-parse refs/heads/%s" % branch
)
734 readtree
= "git read-tree --aggressive -u -m HEAD %s %s" % (idx
[0], new
[0])
735 if run_command(readtree
):
736 run_command("git update-index --refresh")
737 if os
.system(readtree
):
738 raise YapError("Failed to switch")
739 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
741 if '-f' not in flags
:
748 @short_help("move the current branch to a different revision")
750 The argument is the hash of the commit to which the current branch
751 should point, or alternately a branch or tag (a.k.a, "committish"). If
752 moving the branch would create "dangling commits" (see 'branch'), yap
753 will report an error and abort. The '-f' flag can be used to force the
754 operation in spite of this.
757 def cmd_point(self
, where
, **flags
):
760 self
._check
_rebasing
()
762 head
= get_output("git rev-parse --verify HEAD")
764 raise YapError("No commit yet; nowhere to point")
766 ref
= get_output("git rev-parse --verify '%s^{commit}'" % where
)
768 raise YapError("Not a valid ref: %s" % where
)
770 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
771 raise YapError("You have uncommitted changes. Commit them first")
773 run_safely("git update-ref HEAD '%s'" % ref
[0])
775 if '-f' not in flags
:
776 name
= get_output("git name-rev --name-only '%s'" % head
[0])[0]
777 if name
== "undefined":
778 os
.system("git update-ref HEAD '%s'" % head
[0])
779 raise YapError("Pointing there will lose commits. Use -f to force")
782 run_safely("git read-tree -u -m HEAD")
784 run_safely("git read-tree HEAD")
785 run_safely("git checkout-index -u -f -a")
788 @short_help("alter history by dropping or amending commits")
790 This command operates in two distinct modes, "amend" and "drop" mode.
791 In drop mode, the given commit is removed from the history of the
792 current branch, as though that commit never happened. By default the
795 In amend mode, the uncommitted changes present are merged into a
796 previous commit. This is useful for correcting typos or adding missed
797 files into past commits. By default the commit used is HEAD.
799 While rewriting history it is possible that conflicts will arise. If
800 this happens, the rewrite will pause and you will be prompted to resolve
801 the conflicts and stage them. Once that is done, you will run "yap
802 history continue." If instead you want the conflicting commit removed
803 from history (perhaps your changes supercede that commit) you can run
804 "yap history skip". Once the rewrite completes, your branch will be on
805 the same commit as when the rewrite started.
807 def cmd_history(self
, subcmd
, *args
):
808 "amend | drop <commit>"
811 if subcmd
not in ("amend", "drop", "continue", "skip"):
815 When you have resolved the conflicts run \"yap history continue\".
816 To skip the problematic patch, run \"yap history skip\"."""
818 if subcmd
== "continue":
819 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
822 os
.system("git reset --hard")
823 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
826 if subcmd
== "amend":
827 flags
, args
= getopt
.getopt(args
, "ad")
837 if run_command("git rev-parse --verify '%s'" % commit
):
838 raise YapError("Not a valid commit: %s" % commit
)
840 self
._check
_rebasing
()
842 if subcmd
== "amend":
843 self
._check
_commit
(**flags
)
844 if self
._get
_unstaged
_files
():
845 # XXX: handle unstaged changes better
846 raise YapError("Commit away changes that you aren't amending")
850 start
= get_output("git rev-parse HEAD")
851 stash
= get_output("git stash create")
852 run_command("git reset --hard")
854 fd
, tmpfile
= tempfile
.mkstemp("yap")
858 os
.system("git format-patch -k --stdout '%s' > %s" % (commit
, tmpfile
))
859 if subcmd
== "amend":
860 self
.cmd_point(commit
, **{'-f': True})
862 if subcmd
== "amend":
864 rc
= os
.system("git stash apply %s" % stash
[0])
866 self
.cmd_point(start
[0], **{'-f': True})
867 os
.system("git stash apply %s" % stash
[0])
868 raise YapError("Failed to apply stash")
871 if subcmd
== "amend":
873 self
._check
_commit
(**{'-a': True})
876 self
.cmd_point("%s^" % commit
, **{'-f': True})
878 stat
= os
.stat(tmpfile
)
881 run_safely("git update-index --refresh")
882 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
884 raise YapError("Failed to apply changes")
889 run_command("git stash apply %s" % stash
[0])
892 @short_help("show the changes introduced by a given commit")
894 By default, the changes in the last commit are shown. To override this,
895 specify a hash, branch, or tag (committish). The hash of the commit,
896 the commit's author, log message, and a diff of the changes are shown.
898 def cmd_show(self
, commit
="HEAD"):
901 os
.system("git show '%s'" % commit
)
903 @short_help("apply the changes in a given commit to the current branch")
905 The argument is the hash, branch, or tag (committish) of the commit to
906 be applied. In general, it only makes sense to apply commits that
907 happened on another branch. The '-r' flag can be used to have the
908 changes in the given commit reversed from the current branch. In
909 general, this only makes sense for commits that happened on the current
913 def cmd_cherry_pick(self
, commit
, **flags
):
917 os
.system("git revert '%s'" % commit
)
919 os
.system("git cherry-pick '%s'" % commit
)
921 @short_help("list, add, or delete configured remote repositories")
923 When invoked with no arguments, this command will show the list of
924 currently configured remote repositories, giving both the name and URL
925 of each. To add a new repository, give the desired name as the first
926 argument and the URL as the second. The '-d' flag can be used to remove
927 a previously added repository.
930 def cmd_repo(self
, name
=None, url
=None, **flags
):
931 "[<name> <url> | -d <name>]"
933 if name
is not None and url
is None:
937 if flags
['-d'] not in [ x
[0] for x
in self
._list
_remotes
() ]:
938 raise YapError("No such repository: %s" % flags
['-d'])
939 os
.system("git config --unset remote.%s.url" % flags
['-d'])
940 os
.system("git config --unset remote.%s.fetch" % flags
['-d'])
943 if name
in [ x
[0] for x
in self
._list
_remotes
() ]:
944 raise YapError("Repository '%s' already exists" % flags
['-d'])
945 os
.system("git config remote.%s.url %s" % (name
, url
))
946 os
.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name
, name
))
948 for remote
, url
in self
._list
_remotes
():
949 print "%-20s %s" % (remote
, url
)
951 @short_help("send local commits to a remote repository (*)")
953 When invoked with no arguments, the current branch is synchronized to
954 the tracking branch of the tracking remote. If no tracking remote is
955 specified, the repository will have to be specified on the command line.
956 In that case, the default is to push to a branch with the same name as
957 the current branch. This behavior can be overridden by giving a second
958 argument to specify the remote branch.
960 If the remote branch does not currently exist, the command will abort
961 unless the -c flag is provided. If the remote branch is not a direct
962 descendent of the local branch, the command will abort unless the -f
963 flag is provided. Forcing a push in this way can be problematic to
964 other users of the repository if they are not expecting it.
966 To delete a branch on the remote repository, use the -d flag.
968 @takes_options("cdf")
969 def cmd_push(self
, repo
=None, rhs
=None, **flags
):
972 if '-c' in flags
and '-d' in flags
:
975 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
976 raise YapError("No such repository: %s" % repo
)
978 current
= get_output("git symbolic-ref HEAD")
980 raise YapError("Not on a branch!")
982 self
._check
_rebasing
()
984 current
= current
[0].replace('refs/heads/', '')
985 remote
= get_output("git config branch.%s.remote" % current
)
986 if repo
is None and remote
:
990 raise YapError("No tracking branch configured; specify destination repository")
992 if rhs
is None and remote
and remote
[0] == repo
:
993 merge
= get_output("git config branch.%s.merge" % current
)
998 rhs
= "refs/heads/%s" % current
1000 if '-c' not in flags
and '-d' not in flags
:
1001 if run_command("git rev-parse --verify refs/remotes/%s/%s"
1002 % (repo
, rhs
.replace('refs/heads/', ''))):
1003 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
1004 if '-f' not in flags
:
1005 hash = get_output("git rev-parse refs/remotes/%s/%s" % (repo
, rhs
.replace('refs/heads/', '')))
1006 base
= get_output("git merge-base HEAD %s" % hash[0])
1008 if base
[0] != hash[0]:
1009 raise YapError("Branch not up-to-date with remote. Update or use -f")
1011 self
._confirm
_push
(current
, rhs
, repo
)
1018 lhs
= "refs/heads/%s" % current
1019 rc
= os
.system("git push %s %s %s:%s" % (flags
.get('-f', ''), repo
, lhs
, rhs
))
1021 raise YapError("Push failed.")
1023 @short_help("retrieve commits from a remote repository")
1025 When run with no arguments, the command will retrieve new commits from
1026 the remote tracking repository. Note that this does not in any way
1027 alter the current branch. For that, see "update". If a remote other
1028 than the tracking remote is desired, it can be specified as the first
1031 def cmd_fetch(self
, repo
=None):
1034 current
= get_output("git symbolic-ref HEAD")
1036 raise YapError("Not on a branch!")
1038 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1039 raise YapError("No such repository: %s" % repo
)
1041 current
= current
[0].replace('refs/heads/', '')
1042 remote
= get_output("git config branch.%s.remote" % current
)
1046 raise YapError("No tracking branch configured; specify a repository")
1047 os
.system("git fetch %s" % repo
)
1049 @short_help("update the current branch relative to its tracking branch")
1051 Updates the current branch relative to its remote tracking branch. This
1052 command requires that the current branch have a remote tracking branch
1053 configured. If any conflicts occur while applying your changes to the
1054 updated remote, the command will pause to allow you to fix them. Once
1055 that is done, run "update" with the "continue" subcommand. Alternately,
1056 the "skip" subcommand can be used to discard the conflicting changes.
1058 def cmd_update(self
, subcmd
=None):
1061 if subcmd
and subcmd
not in ["continue", "skip"]:
1065 When you have resolved the conflicts run \"yap update continue\".
1066 To skip the problematic patch, run \"yap update skip\"."""
1068 if subcmd
== "continue":
1069 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
1071 if subcmd
== "skip":
1072 os
.system("git reset --hard")
1073 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
1076 self
._check
_rebasing
()
1077 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
1078 raise YapError("You have uncommitted changes. Commit them first")
1080 current
= get_output("git symbolic-ref HEAD")
1082 raise YapError("Not on a branch!")
1084 current
= current
[0].replace('refs/heads/', '')
1085 remote
, merge
= self
._get
_tracking
(current
)
1086 merge
= merge
.replace('refs/heads/', '')
1088 self
.cmd_fetch(remote
)
1089 base
= get_output("git merge-base HEAD refs/remotes/%s/%s" % (remote
, merge
))
1092 fd
, tmpfile
= tempfile
.mkstemp("yap")
1094 os
.system("git format-patch -k --stdout '%s' > %s" % (base
[0], tmpfile
))
1095 self
.cmd_point("refs/remotes/%s/%s" % (remote
, merge
), **{'-f': True})
1097 stat
= os
.stat(tmpfile
)
1100 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
1102 raise YapError("Failed to apply changes")
1106 @short_help("query and configure remote branch tracking")
1108 When invoked with no arguments, the command displays the tracking
1109 information for the current branch. To configure the tracking
1110 information, two arguments for the remote repository and remote branch
1111 are given. The tracking information is used to provide defaults for
1112 where to push local changes and from where to get updates to the branch.
1114 def cmd_track(self
, repo
=None, branch
=None):
1118 current
= get_output("git symbolic-ref HEAD")
1120 raise YapError("Not on a branch!")
1121 current
= current
[0].replace('refs/heads/', '')
1123 if repo
is None and branch
is None:
1124 repo
, merge
= self
._get
_tracking
(current
)
1125 merge
= merge
.replace('refs/heads/', '')
1126 print "Branch '%s' tracking refs/remotes/%s/%s" % (current
, repo
, merge
)
1129 if repo
is None or branch
is None:
1132 if repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1133 raise YapError("No such repository: %s" % repo
)
1135 if run_command("git rev-parse --verify refs/remotes/%s/%s" % (repo
, branch
)):
1136 raise YapError("No such branch '%s' on repository '%s'" % (branch
, repo
))
1138 os
.system("git config branch.%s.remote '%s'" % (current
, repo
))
1139 os
.system("git config branch.%s.merge 'refs/heads/%s'" % (current
, branch
))
1140 print "Branch '%s' now tracking refs/remotes/%s/%s" % (current
, repo
, branch
)
1142 @short_help("mark files with conflicts as resolved")
1144 The arguments are the files to be marked resolved. When a conflict
1145 occurs while merging changes to a file, that file is marked as
1146 "unmerged." Until the file(s) with conflicts are marked resolved,
1147 commits cannot be made.
1149 def cmd_resolved(self
, *files
):
1156 self
._stage
_one
(f
, True)
1159 @short_help("merge a branch into the current branch")
1160 def cmd_merge(self
, branch
):
1164 branch_name
= branch
1165 branch
= get_output("git rev-parse --verify %s" % branch
)
1167 raise YapError("No such branch: %s" % branch
)
1170 base
= get_output("git merge-base HEAD %s" % branch
)
1172 raise YapError("Branch '%s' is not a fork of the current branch"
1175 readtree
= ("git read-tree --aggressive -u -m %s HEAD %s"
1176 % (base
[0], branch
))
1177 if run_command(readtree
):
1178 run_command("git update-index --refresh")
1179 if os
.system(readtree
):
1180 raise YapError("Failed to merge")
1182 repo
= get_output('git rev-parse --git-dir')[0]
1183 dir = os
.path
.join(repo
, 'yap')
1188 msg_file
= os
.path
.join(dir, 'msg')
1189 msg
= file(msg_file
, 'w')
1190 print >>msg
, "Merge branch '%s'" % branch_name
1193 head
= get_output("git rev-parse --verify HEAD")
1195 heads
= [head
[0], branch
]
1196 head_file
= os
.path
.join(dir, 'merge')
1197 pickle
.dump(heads
, file(head_file
, 'w'))
1199 self
._merge
_index
(branch
, base
[0])
1200 if self
._get
_unmerged
_files
():
1202 raise YapError("Fix conflicts then commit")
1206 def _merge_index(self
, branch
, base
):
1207 for f
in self
._get
_unmerged
_files
():
1208 fd
, bfile
= tempfile
.mkstemp("yap")
1210 rc
= os
.system("git show %s:%s > %s" % (base
, f
, bfile
))
1213 fd
, ofile
= tempfile
.mkstemp("yap")
1215 rc
= os
.system("git show %s:%s > %s" % (branch
, f
, ofile
))
1218 command
= "git merge-file -L %(file)s -L %(file)s.base -L %(file)s.%(branch)s %(file)s %(base)s %(other)s " % dict(file=f
, branch
=branch
, base
=bfile
, other
=ofile
)
1219 rc
= os
.system(command
)
1225 self
._stage
_one
(f
, True)
1227 @short_help("show information about loaded plugins")
1228 def cmd_plugins(self
):
1230 if not self
.plugins
:
1231 print >>sys
.stderr
, "No plugins loaded."
1232 for k
, v
in self
.plugins
.items():
1235 doc
= "No description"
1236 print "%-20s%s" % (k
, doc
)
1239 if not func
.startswith('cmd_'):
1242 print "\tOverrides:"
1246 def cmd_help(self
, cmd
=None):
1248 cmd
= "cmd_" + cmd
.replace('-', '_')
1250 attr
= self
.__getattribute
__(cmd
)
1251 except AttributeError:
1252 raise YapError("No such command: %s" % cmd
)
1254 help = attr
.long_help
1255 except AttributeError:
1256 attr
= super(Yap
, self
).__getattribute
__(cmd
)
1258 help = attr
.long_help
1259 except AttributeError:
1260 raise YapError("Sorry, no help for '%s'. Ask Steven." % cmd
)
1262 print >>sys
.stderr
, "The '%s' command" % cmd
1263 print >>sys
.stderr
, "\tyap %s %s" % (cmd
, attr
.__doc
__)
1264 print >>sys
.stderr
, "%s" % help
1267 print >> sys
.stderr
, "Yet Another (Git) Porcelein"
1270 for name
in dir(self
):
1271 if not name
.startswith('cmd_'):
1273 attr
= self
.__getattribute
__(name
)
1274 if not callable(attr
):
1278 short_msg
= attr
.short_help
1279 except AttributeError:
1281 default_meth
= super(Yap
, self
).__getattribute
__(name
)
1282 short_msg
= default_meth
.short_help
1283 except AttributeError:
1286 name
= name
.replace('cmd_', '')
1287 name
= name
.replace('_', '-')
1288 print >> sys
.stderr
, "%-16s%s" % (name
, short_msg
)
1291 print >> sys
.stderr
, "Commands provided by plugins:"
1292 for k
, v
in self
.plugins
.items():
1294 if not name
.startswith('cmd_'):
1297 attr
= self
.__getattribute
__(name
)
1298 short_msg
= attr
.short_help
1299 except AttributeError:
1301 name
= name
.replace('cmd_', '')
1302 name
= name
.replace('_', '-')
1303 print >> sys
.stderr
, "%-8s(%s) %s" % (name
, k
, short_msg
)
1306 print >> sys
.stderr
, "(*) Indicates that the command is not readily reversible"
1308 def cmd_usage(self
):
1309 print >> sys
.stderr
, "usage: %s <command>" % os
.path
.basename(sys
.argv
[0])
1310 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"
1312 def main(self
, args
):
1320 if run_command("git --version"):
1321 print >>sys
.stderr
, "Failed to run git; is it installed?"
1324 debug
= os
.getenv('YAP_DEBUG')
1327 command
= command
.replace('-', '_')
1329 meth
= self
.__getattribute
__("cmd_"+command
)
1331 default_meth
= super(Yap
, self
).__getattribute
__("cmd_"+command
)
1332 except AttributeError:
1335 if meth
.__doc
__ is not None:
1337 elif default_meth
is not None:
1338 doc
= default_meth
.__doc
__
1344 if "options" in meth
.__dict
__:
1345 options
= meth
.options
1346 if default_meth
and "options" in default_meth
.__dict
__:
1347 options
+= default_meth
.options
1349 flags
, args
= getopt
.getopt(args
, options
)
1354 # cast args to a mutable type. this lets the pre-hooks act as
1355 # filters on the arguments
1359 for p
in self
.plugins
.values():
1361 pre_meth
= p
.__getattribute
__("pre_"+command
)
1362 except AttributeError:
1364 pre_meth(args
, flags
)
1366 meth(*args
, **flags
)
1369 for p
in self
.plugins
.values():
1371 meth
= p
.__getattribute
__("post_"+command
)
1372 except AttributeError:
1376 except (TypeError, getopt
.GetoptError
):
1379 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1383 print >> sys
.stderr
, e
1385 except AttributeError: