10 class ShellError(Exception):
11 def __init__(self
, cmd
, rc
):
16 return "%s returned %d" % (self
.cmd
, self
.rc
)
18 class YapError(Exception):
19 def __init__(self
, msg
):
25 class YapCore(object):
26 def _add_new_file(self
, file):
27 repo
= get_output('git rev-parse --git-dir')[0]
28 dir = os
.path
.join(repo
, 'yap')
33 files
= self
._get
_new
_files
()
35 path
= os
.path
.join(dir, 'new-files')
36 pickle
.dump(files
, open(path
, 'w'))
38 def _get_new_files(self
):
39 repo
= get_output('git rev-parse --git-dir')[0]
40 path
= os
.path
.join(repo
, 'yap', 'new-files')
42 files
= pickle
.load(file(path
))
49 if get_output("git ls-files --cached '%s'" % f
) != []:
54 def _remove_new_file(self
, file):
55 files
= self
._get
_new
_files
()
56 files
= filter(lambda x
: x
!= file, files
)
58 repo
= get_output('git rev-parse --git-dir')[0]
59 path
= os
.path
.join(repo
, 'yap', 'new-files')
61 pickle
.dump(files
, open(path
, 'w'))
65 def _assert_file_exists(self
, file):
66 if not os
.access(file, os
.R_OK
):
67 raise YapError("No such file: %s" % file)
69 def _repo_path_to_rel(self
, path
):
70 prefix
= get_output("git rev-parse --show-prefix")
74 prefix
= [ prefix
[0] ]
76 head
, tail
= os
.path
.split(prefix
[0])
81 prefix
.insert(1, tail
)
85 head
, tail
= os
.path
.split(path
[0])
93 for a
, b
in zip(prefix
, path
):
99 cdup
= [".."] * (len(prefix
) - common
)
100 path
= cdup
+ list(path
)
101 path
= os
.path
.join(*path
)
104 def _get_staged_files(self
):
105 if run_command("git rev-parse HEAD"):
106 files
= get_output("git ls-files --cached")
108 files
= get_output("git diff-index --cached --name-only HEAD")
109 unmerged
= self
._get
_unmerged
_files
()
111 unmerged
= set(unmerged
)
112 files
= set(files
).difference(unmerged
)
116 def _get_unstaged_files(self
):
118 cdup
= self
._get
_cdup
()
120 files
= get_output("git ls-files -m")
123 new_files
= self
._get
_new
_files
()
125 staged
= self
._get
_staged
_files
()
128 new_files
= set(new_files
).difference(staged
)
129 new_files
= list(new_files
)
131 unmerged
= self
._get
_unmerged
_files
()
133 unmerged
= set(unmerged
)
134 files
= set(files
).difference(unmerged
)
138 def _get_unmerged_files(self
):
140 cdup
= self
._get
_cdup
()
142 files
= get_output("git ls-files -u")
144 files
= [ x
.replace('\t', ' ').split(' ')[3] for x
in files
]
145 return list(set(files
))
147 def _resolve_rev(self
, rev
):
148 ref
= get_output("git rev-parse --verify %s 2>/dev/null" % rev
)
150 raise YapError("No such revision: %s" % rev
)
153 def _delete_branch(self
, branch
, force
):
154 current
= get_output("git symbolic-ref HEAD")
156 current
= current
[0].replace('refs/heads/', '')
157 if branch
== current
:
158 raise YapError("Can't delete current branch")
160 ref
= self
._resolve
_rev
('refs/heads/'+branch
)
161 run_safely("git update-ref -d 'refs/heads/%s' '%s'" % (branch
, ref
))
164 name
= get_output("git name-rev --name-only '%s'" % ref
)[0]
165 if name
== 'undefined':
166 run_command("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
))
167 raise YapError("Refusing to delete leaf branch (use -f to force)")
168 def _get_pager_cmd(self
):
169 if 'YAP_PAGER' in os
.environ
:
170 return os
.environ
['YAP_PAGER']
171 elif 'GIT_PAGER' in os
.environ
:
172 return os
.environ
['GIT_PAGER']
173 elif 'PAGER' in os
.environ
:
174 return os
.environ
['PAGER']
178 def _add_one(self
, file):
179 self
._assert
_file
_exists
(file)
180 x
= get_output("git ls-files '%s'" % file)
181 if x
!= [] or file in self
._get
_new
_files
():
182 raise YapError("File '%s' already in repository" % file)
183 self
._add
_new
_file
(file)
185 def _rm_one(self
, file):
186 self
._assert
_file
_exists
(file)
187 if get_output("git ls-files '%s'" % file) != []:
188 run_safely("git rm --cached '%s'" % file)
189 self
._remove
_new
_file
(file)
191 def _stage_one(self
, file, allow_unmerged
=False):
192 self
._assert
_file
_exists
(file)
193 prefix
= get_output("git rev-parse --show-prefix")
195 tmp
= os
.path
.normpath(os
.path
.join(prefix
[0], file))
198 if not allow_unmerged
and tmp
in self
._get
_unmerged
_files
():
199 raise YapError("Refusing to stage conflicted file: %s" % file)
200 run_safely("git update-index --add '%s'" % file)
203 cdup
= get_output("git rev-parse --show-cdup")
211 def _unstage_one(self
, file):
212 self
._assert
_file
_exists
(file)
213 if run_command("git rev-parse HEAD"):
214 rc
= run_command("git update-index --force-remove '%s'" % file)
216 cdup
= self
._get
_cdup
()
217 rc
= run_command("git diff-index --cached -p HEAD '%s' | (cd %s; git apply -R --cached)" % (file, cdup
))
219 raise YapError("Failed to unstage")
221 def _revert_one(self
, file):
222 self
._assert
_file
_exists
(file)
224 self
._unstage
_one
(file)
227 run_safely("git checkout-index -u -f '%s'" % file)
229 def _parse_commit(self
, commit
):
230 lines
= get_output("git cat-file commit '%s'" % commit
)
235 if mode
!= 'commit' and l
.strip() == "":
240 commit
['log'].append(l
)
247 commit
['log'] = '\n'.join(commit
['log'])
250 def _check_commit(self
, **flags
):
251 if '-a' in flags
and '-d' in flags
:
252 raise YapError("Conflicting flags: -a and -d")
254 if '-d' not in flags
and self
._get
_unstaged
_files
():
255 if '-a' not in flags
and self
._get
_staged
_files
():
256 raise YapError("Staged and unstaged changes present. Specify what to commit")
257 cdup
= self
._get
_cdup
()
258 run_command("(cd %s; git add -u)" % cdup
)
259 for f
in self
._get
_new
_files
():
262 def _do_uncommit(self
):
263 commit
= self
._parse
_commit
("HEAD")
264 repo
= get_output('git rev-parse --git-dir')[0]
265 dir = os
.path
.join(repo
, 'yap')
270 msg_file
= os
.path
.join(dir, 'msg')
271 fd
= file(msg_file
, 'w')
272 print >>fd
, commit
['log']
275 tree
= get_output("git rev-parse --verify HEAD^")
276 run_safely("git update-ref -m uncommit HEAD '%s'" % tree
[0])
278 def _do_commit(self
, msg
=None):
279 tree
= get_output("git write-tree")[0]
281 repo
= get_output('git rev-parse --git-dir')[0]
282 head_file
= os
.path
.join(repo
, 'yap', 'merge')
284 parent
= pickle
.load(file(head_file
))
286 parent
= get_output("git rev-parse --verify HEAD 2> /dev/null")
288 if os
.environ
.has_key('YAP_EDITOR'):
289 editor
= os
.environ
['YAP_EDITOR']
290 elif os
.environ
.has_key('GIT_EDITOR'):
291 editor
= os
.environ
['GIT_EDITOR']
292 elif os
.environ
.has_key('EDITOR'):
293 editor
= os
.environ
['EDITOR']
297 fd
, tmpfile
= tempfile
.mkstemp("yap")
301 msg_file
= os
.path
.join(repo
, 'yap', 'msg')
302 if os
.access(msg_file
, os
.R_OK
):
304 fd2
= file(tmpfile
, 'w')
305 for l
in fd1
.xreadlines():
306 print >>fd2
, l
.strip()
309 if os
.system("%s '%s'" % (editor
, tmpfile
)) != 0:
310 raise YapError("Editing commit message failed")
317 raise YapError("Refusing to use empty commit message")
319 fd
= os
.popen("git stripspace > %s" % tmpfile
, 'w')
324 parent
= ' -p '.join(parent
)
325 commit
= get_output("git commit-tree '%s' -p %s < '%s'" % (tree
, parent
, tmpfile
))
327 commit
= get_output("git commit-tree '%s' < '%s'" % (tree
, tmpfile
))
330 run_safely("git update-ref HEAD '%s'" % commit
[0])
333 def _check_rebasing(self
):
334 repo
= get_output('git rev-parse --git-dir')[0]
335 dotest
= os
.path
.join(repo
, '.dotest')
336 if os
.access(dotest
, os
.R_OK
):
337 raise YapError("A git operation is in progress. Complete it first")
338 dotest
= os
.path
.join(repo
, '..', '.dotest')
339 if os
.access(dotest
, os
.R_OK
):
340 raise YapError("A git operation is in progress. Complete it first")
342 def _check_git(self
):
343 if run_command("git rev-parse --git-dir"):
344 raise YapError("That command must be run from inside a git repository")
346 def _list_remotes(self
):
347 remotes
= get_output("git config --get-regexp '^remote.*.url'")
349 remote
, url
= x
.split(' ')
350 remote
= remote
.replace('remote.', '')
351 remote
= remote
.replace('.url', '')
354 def _unstage_all(self
):
356 run_safely("git read-tree -m HEAD")
358 run_safely("git read-tree HEAD")
359 run_safely("git update-index -q --refresh")
361 def _get_tracking(self
, current
):
362 remote
= get_output("git config branch.%s.remote" % current
)
364 raise YapError("No tracking branch configured for '%s'" % current
)
366 merge
= get_output("git config branch.%s.merge" % current
)
368 raise YapError("No tracking branch configured for '%s'" % current
)
369 return remote
[0], merge
[0]
371 def _confirm_push(self
, current
, rhs
, repo
):
372 print "About to push local branch '%s' to '%s' on '%s'" % (current
, rhs
, repo
)
373 print "Continue (y/n)? ",
375 ans
= sys
.stdin
.readline().strip()
377 if ans
.lower() != 'y' and ans
.lower() != 'yes':
378 raise YapError("Aborted.")
380 def _clear_state(self
):
381 repo
= get_output('git rev-parse --git-dir')[0]
382 dir = os
.path
.join(repo
, 'yap')
383 for f
in "new-files", "merge", "msg":
385 os
.unlink(os
.path
.join(dir, f
))
389 def _get_attr(self
, name
, attr
):
391 for c
in self
.__class
__.__bases
__:
393 m2
= c
.__dict
__[name
]
397 val
= m2
.__getattribute__(attr
)
398 except AttributeError:
402 def _filter_log(self
, commit
):
405 def _check_rename(self
, rev
, path
):
406 renames
= get_output("git diff-tree -C -M --diff-filter=R %s %s^"
409 r
= r
.replace('\t', ' ')
410 fields
= r
.split(' ')
411 mode1
, mode2
, hash1
, hash2
, rename
, dst
, src
= fields
416 def _expand_directories(self
, files
):
418 for i
, f
in enumerate(files
[:]):
419 if not os
.path
.isdir(f
):
423 fd
= os
.popen("find %s -type f" % f
)
424 for x
in fd
.xreadlines():
426 if '.git' in x
.split(os
.path
.sep
):
432 @short_help("make a local copy of an existing repository")
434 The first argument is a URL to the existing repository. This can be an
435 absolute path if the repository is local, or a URL with the git://,
436 ssh://, or http:// schemes. By default, the directory used is the last
437 component of the URL, sans '.git'. This can be overridden by providing
440 def cmd_clone(self
, url
, directory
=None):
443 if '://' not in url
and url
[0] != '/':
444 url
= os
.path
.join(os
.getcwd(), url
)
446 url
= url
.rstrip('/')
447 if directory
is None:
448 directory
= url
.rsplit('/')[-1]
449 directory
= directory
.replace('.git', '')
454 raise YapError("Directory exists: %s" % directory
)
457 self
.cmd_repo("origin", url
)
458 self
.cmd_fetch("origin")
461 if not run_command("git rev-parse --verify refs/remotes/origin/HEAD"):
462 hash = get_output("git rev-parse refs/remotes/origin/HEAD")[0]
463 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin'"):
464 if get_output("git rev-parse %s" % b
)[0] == hash:
468 if not run_command("git rev-parse --verify refs/remotes/origin/master"):
469 branch
= "refs/remotes/origin/master"
471 branch
= get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin'")
474 hash = get_output("git rev-parse %s" % branch
)
476 branch
= branch
.replace('refs/remotes/origin/', '')
477 run_safely("git update-ref refs/heads/%s %s" % (branch
, hash[0]))
478 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
479 self
.cmd_revert(**{'-a': 1})
481 @short_help("turn a directory into a repository")
483 Converts the current working directory into a repository. The primary
484 side-effect of this command is the creation of a '.git' subdirectory.
485 No files are added nor commits made.
488 os
.system("git init")
490 @short_help("add a new file to the repository")
492 The arguments are the files to be added to the repository. Once added,
493 the files will show as "unstaged changes" in the output of 'status'. To
494 reverse the effects of this command, see 'rm'.
496 def cmd_add(self
, *files
):
503 files
= self
._expand
_directories
(files
)
508 @short_help("delete a file from the repository")
510 The arguments are the files to be removed from the current revision of
511 the repository. The files will still exist in any past commits that the
512 files may have been a part of. The file is not actually deleted, it is
513 just no longer tracked as part of the repository.
515 def cmd_rm(self
, *files
):
525 @short_help("stage changes in a file for commit")
527 The arguments are the files to be staged. Staging changes is a way to
528 build up a commit when you do not want to commit all changes at once.
529 To commit only staged changes, use the '-d' flag to 'commit.' To
530 reverse the effects of this command, see 'unstage'. Once staged, the
531 files will show as "staged changes" in the output of 'status'.
533 def cmd_stage(self
, *files
):
539 files
= self
._expand
_directories
(files
)
544 @short_help("unstage changes in a file")
546 The arguments are the files to be unstaged. Once unstaged, the files
547 will show as "unstaged changes" in the output of 'status'. The '-a'
548 flag can be used to unstage all staged changes at once.
551 def cmd_unstage(self
, *files
, **flags
):
555 files
= self
._get
_staged
_files
()
558 raise YapError("Nothing to do")
560 files
= [ self
._repo
_path
_to
_rel
(x
) for x
in files
]
565 @short_help("show files with staged and unstaged changes")
567 Show the files in the repository with changes since the last commit,
568 categorized based on whether the changes are staged or not. A file may
569 appear under each heading if the same file has both staged and unstaged
572 def cmd_status(self
):
575 branch
= get_output("git symbolic-ref HEAD")
577 branch
= branch
[0].replace('refs/heads/', '')
580 print "Current branch: %s" % branch
582 print "Files with staged changes:"
583 files
= self
._get
_staged
_files
()
585 print "\t%s" % self
._repo
_path
_to
_rel
(f
)
589 print "Files with unstaged changes:"
590 files
= self
._get
_unstaged
_files
()
592 print "\t%s" % self
._repo
_path
_to
_rel
(f
)
596 files
= self
._get
_unmerged
_files
()
598 print "Files with conflicts:"
600 print "\t%s" % self
._repo
_path
_to
_rel
(f
)
602 @short_help("remove uncommitted changes from a file (*)")
604 The arguments are the files whose changes will be reverted. If the '-a'
605 flag is given, then all files will have uncommitted changes removed.
606 Note that there is no way to reverse this command short of manually
607 editing each file again.
610 def cmd_revert(self
, *files
, **flags
):
614 cdup
= self
._get
_cdup
()
615 run_command("(cd %s; git add -u)" % cdup
)
616 os
.system("git read-tree -v --aggressive -u -m HEAD")
628 @short_help("record changes to files as a new commit")
630 Create a new commit recording changes since the last commit. If there
631 are only unstaged changes, those will be recorded. If there are only
632 staged changes, those will be recorded. Otherwise, you will have to
633 specify either the '-a' flag or the '-d' flag to commit all changes or
634 only staged changes, respectively. To reverse the effects of this
635 command, see 'uncommit'.
637 @takes_options("adm:")
638 def cmd_commit(self
, **flags
):
639 "[-a | -d] [-m <msg>]"
641 self
._check
_rebasing
()
642 self
._check
_commit
(**flags
)
643 if not self
._get
_staged
_files
():
644 raise YapError("No changes to commit")
645 msg
= flags
.get('-m', None)
649 @short_help("reverse the actions of the last commit")
651 Reverse the effects of the last 'commit' operation. The changes that
652 were part of the previous commit will show as "staged changes" in the
653 output of 'status'. This means that if no files were changed since the
654 last commit was created, 'uncommit' followed by 'commit' is a lossless
657 def cmd_uncommit(self
):
663 @short_help("report the current version of yap")
664 def cmd_version(self
):
665 print "Yap version %s" % self
.version
667 @short_help("show the changelog for particular versions or files")
669 The arguments are the files with which to filter history. If none are
670 given, all changes are listed. Otherwise only commits that affected one
671 or more of the given files are listed. The -r option changes the
672 starting revision for traversing history. By default, history is listed
675 @takes_options("pr:")
676 def cmd_log(self
, *paths
, **flags
):
677 "[-p] [-r <rev>] <path>..."
679 rev
= flags
.get('-r', 'HEAD')
680 rev
= self
._resolve
_rev
(rev
)
687 pager
= os
.popen(self
._get
_pager
_cmd
(), 'w')
690 for hash in yield_output("git rev-list '%s' -- %s"
691 % (rev
, ' '.join(paths
))):
692 commit
= get_output("git show -M -C %s %s"
693 % (flags
.get('-p', '--name-status'), hash),
695 commit
= self
._filter
_log
(commit
)
696 print >>pager
, ''.join(commit
)
700 src
= self
._check
_rename
(hash, paths
[0])
709 except (IOError, KeyboardInterrupt):
712 @short_help("show staged, unstaged, or all uncommitted changes")
714 Show staged, unstaged, or all uncommitted changes. By default, all
715 changes are shown. The '-u' flag causes only unstaged changes to be
716 shown. The '-d' flag causes only staged changes to be shown.
719 def cmd_diff(self
, **flags
):
722 if '-u' in flags
and '-d' in flags
:
723 raise YapError("Conflicting flags: -u and -d")
725 pager
= self
._get
_pager
_cmd
()
728 os
.system("git diff-files -p | %s" % pager
)
730 os
.system("git diff-index --cached -p HEAD | %s" % pager
)
732 os
.system("git diff-index -p HEAD | %s" % pager
)
734 @short_help("list, create, or delete branches")
736 If no arguments are specified, a list of local branches is given. The
737 current branch is indicated by a "*" next to the name. If an argument
738 is given, it is taken as the name of a new branch to create. The branch
739 will start pointing at the current HEAD. See 'point' for details on
740 changing the revision of the new branch. Note that this command does
741 not switch the current working branch. See 'switch' for details on
742 changing the current working branch.
744 The '-d' flag can be used to delete local branches. If the delete
745 operation would remove the last branch reference to a given line of
746 history (colloquially referred to as "dangling commits"), yap will
747 report an error and abort. The '-f' flag can be used to force the delete
750 @takes_options("fd:")
751 def cmd_branch(self
, branch
=None, **flags
):
752 "[ [-f] -d <branch> | <branch> ]"
754 force
= '-f' in flags
756 self
._delete
_branch
(flags
['-d'], force
)
760 if branch
is not None:
761 ref
= get_output("git rev-parse --verify HEAD")
763 raise YapError("No branch point yet. Make a commit")
764 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
766 current
= get_output("git symbolic-ref HEAD")
767 branches
= get_output("git for-each-ref --format='%(refname)' 'refs/heads'")
769 if current
and b
== current
[0]:
773 b
= b
.replace('refs/heads/', '')
776 @short_help("change the current working branch")
778 The argument is the name of the branch to make the current working
779 branch. This command will fail if there are uncommitted changes to any
780 files. Otherwise, the contents of the files in the working directory
781 are updated to reflect their state in the new branch. Additionally, any
782 future commits are added to the new branch instead of the previous line
786 def cmd_switch(self
, branch
, **flags
):
789 self
._check
_rebasing
()
790 ref
= self
._resolve
_rev
('refs/heads/'+branch
)
792 if '-f' not in flags
:
793 if (self
._get
_staged
_files
()
794 or (self
._get
_unstaged
_files
()
795 and run_command("git update-index --refresh"))):
796 raise YapError("You have uncommitted changes. Use -f to continue anyway")
798 if self
._get
_unstaged
_files
() and self
._get
_staged
_files
():
799 raise YapError("You have staged and unstaged changes. Perhaps unstage -a?")
801 staged
= bool(self
._get
_staged
_files
())
803 cdup
= self
._get
_cdup
()
804 run_command("(cd %s; git add -u)" % cdup
)
805 for f
in self
._get
_new
_files
():
808 idx
= get_output("git write-tree")
809 new
= self
._resolve
_rev
('refs/heads/'+branch
)
811 run_command("git update-index --refresh")
812 readtree
= "git read-tree -v --aggressive -u -m HEAD %s %s" % (idx
[0], new
)
813 if os
.system(readtree
):
814 raise YapError("Failed to switch")
815 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch
)
817 if '-f' not in flags
:
824 @short_help("move the current branch to a different revision")
826 The argument is the hash of the commit to which the current branch
827 should point, or alternately a branch or tag (a.k.a, "committish"). If
828 moving the branch would create "dangling commits" (see 'branch'), yap
829 will report an error and abort. The '-f' flag can be used to force the
830 operation in spite of this.
833 def cmd_point(self
, where
, **flags
):
836 self
._check
_rebasing
()
838 head
= get_output("git rev-parse --verify HEAD")
840 raise YapError("No commit yet; nowhere to point")
842 ref
= self
._resolve
_rev
(where
)
843 ref
= get_output("git rev-parse --verify '%s^{commit}'" % ref
)
845 raise YapError("Not a commit: %s" % where
)
847 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
848 raise YapError("You have uncommitted changes. Commit them first")
850 run_safely("git update-ref HEAD '%s'" % ref
[0])
852 if '-f' not in flags
:
853 name
= get_output("git name-rev --name-only '%s'" % head
[0])[0]
854 if name
== "undefined":
855 os
.system("git update-ref HEAD '%s'" % head
[0])
856 raise YapError("Pointing there will lose commits. Use -f to force")
858 run_command("git update-index --refresh")
859 rc
= os
.system("git read-tree -v --reset -u HEAD")
861 raise YapError("checkout-index failed")
864 @short_help("alter history by dropping or amending commits")
866 This command operates in two distinct modes, "amend" and "drop" mode.
867 In drop mode, the given commit is removed from the history of the
868 current branch, as though that commit never happened. By default the
871 In amend mode, the uncommitted changes present are merged into a
872 previous commit. This is useful for correcting typos or adding missed
873 files into past commits. By default the commit used is HEAD.
875 While rewriting history it is possible that conflicts will arise. If
876 this happens, the rewrite will pause and you will be prompted to resolve
877 the conflicts and stage them. Once that is done, you will run "yap
878 history continue." If instead you want the conflicting commit removed
879 from history (perhaps your changes supercede that commit) you can run
880 "yap history skip". Once the rewrite completes, your branch will be on
881 the same commit as when the rewrite started.
883 def cmd_history(self
, subcmd
, *args
):
884 "amend | drop <commit>"
887 if subcmd
not in ("amend", "drop", "continue", "skip"):
891 When you have resolved the conflicts run \"yap history continue\".
892 To skip the problematic patch, run \"yap history skip\"."""
894 if subcmd
== "continue":
895 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
898 os
.system("git reset --hard")
899 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
902 if subcmd
== "amend":
903 flags
, args
= getopt
.getopt(args
, "ad")
913 self
._resolve
_rev
(commit
)
914 self
._check
_rebasing
()
916 if subcmd
== "amend":
917 self
._check
_commit
(**flags
)
918 if self
._get
_unstaged
_files
():
919 # XXX: handle unstaged changes better
920 raise YapError("Commit away changes that you aren't amending")
924 start
= get_output("git rev-parse HEAD")
925 stash
= get_output("git stash create")
926 run_command("git reset --hard")
928 fd
, tmpfile
= tempfile
.mkstemp("yap")
932 os
.system("git format-patch -k --stdout '%s' > %s" % (commit
, tmpfile
))
933 if subcmd
== "amend":
934 self
.cmd_point(commit
, **{'-f': True})
936 if subcmd
== "amend":
938 rc
= os
.system("git stash apply %s" % stash
[0])
940 self
.cmd_point(start
[0], **{'-f': True})
941 os
.system("git stash apply %s" % stash
[0])
942 raise YapError("Failed to apply stash")
945 if subcmd
== "amend":
947 self
._check
_commit
(**{'-a': True})
950 self
.cmd_point("%s^" % commit
, **{'-f': True})
952 stat
= os
.stat(tmpfile
)
955 run_safely("git update-index --refresh")
956 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
958 raise YapError("Failed to apply changes")
963 run_command("git stash apply %s" % stash
[0])
966 @short_help("show the changes introduced by a given commit")
968 By default, the changes in the last commit are shown. To override this,
969 specify a hash, branch, or tag (committish). The hash of the commit,
970 the commit's author, log message, and a diff of the changes are shown.
972 def cmd_show(self
, commit
="HEAD"):
975 commit
= self
._resolve
_rev
(commit
)
976 os
.system("git show '%s'" % commit
)
978 @short_help("apply the changes in a given commit to the current branch")
980 The argument is the hash, branch, or tag (committish) of the commit to
981 be applied. In general, it only makes sense to apply commits that
982 happened on another branch. The '-r' flag can be used to have the
983 changes in the given commit reversed from the current branch. In
984 general, this only makes sense for commits that happened on the current
988 def cmd_cherry_pick(self
, commit
, **flags
):
991 commit
= self
._resolve
_rev
(commit
)
993 os
.system("git revert '%s'" % commit
)
995 os
.system("git cherry-pick '%s'" % commit
)
997 @short_help("list, add, or delete configured remote repositories")
999 When invoked with no arguments, this command will show the list of
1000 currently configured remote repositories, giving both the name and URL
1001 of each. To add a new repository, give the desired name as the first
1002 argument and the URL as the second. The '-d' flag can be used to remove
1003 a previously added repository.
1005 @takes_options("d:")
1006 def cmd_repo(self
, name
=None, url
=None, **flags
):
1007 "[<name> <url> | -d <name>]"
1009 if name
is not None and url
is None:
1013 if flags
['-d'] not in [ x
[0] for x
in self
._list
_remotes
() ]:
1014 raise YapError("No such repository: %s" % flags
['-d'])
1015 os
.system("git config --unset remote.%s.url" % flags
['-d'])
1016 os
.system("git config --unset remote.%s.fetch" % flags
['-d'])
1017 for b
in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin'"):
1018 hash = get_output("git rev-parse %s" % b
)
1020 run_safely("git update-ref -d %s %s" % (b
, hash[0]))
1023 if name
in [ x
[0] for x
in self
._list
_remotes
() ]:
1024 raise YapError("Repository '%s' already exists" % name
)
1025 os
.system("git config remote.%s.url %s" % (name
, url
))
1026 os
.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name
, name
))
1028 for remote
, url
in self
._list
_remotes
():
1030 print " URL: %s" % url
1032 for b
in get_output("git for-each-ref --format='%%(refname)' 'refs/remotes/%s'" % remote
):
1033 b
= b
.replace('refs/remotes/', '')
1035 branches
= "Branches: "
1038 print " %s%s" % (branches
, b
)
1041 @short_help("send local commits to a remote repository (*)")
1043 When invoked with no arguments, the current branch is synchronized to
1044 the tracking branch of the tracking remote. If no tracking remote is
1045 specified, the repository will have to be specified on the command line.
1046 In that case, the default is to push to a branch with the same name as
1047 the current branch. This behavior can be overridden by giving a second
1048 argument to specify the remote branch.
1050 If the remote branch does not currently exist, the command will abort
1051 unless the -c flag is provided. If the remote branch is not a direct
1052 descendent of the local branch, the command will abort unless the -f
1053 flag is provided. Forcing a push in this way can be problematic to
1054 other users of the repository if they are not expecting it.
1056 To delete a branch on the remote repository, use the -d flag.
1058 @takes_options("cdf")
1059 def cmd_push(self
, repo
=None, rhs
=None, **flags
):
1062 if '-c' in flags
and '-d' in flags
:
1065 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1066 raise YapError("No such repository: %s" % repo
)
1068 current
= get_output("git symbolic-ref HEAD")
1070 raise YapError("Not on a branch!")
1072 self
._check
_rebasing
()
1074 current
= current
[0].replace('refs/heads/', '')
1075 remote
= get_output("git config branch.%s.remote" % current
)
1076 if repo
is None and remote
:
1080 raise YapError("No tracking branch configured; specify destination repository")
1082 if rhs
is None and remote
and remote
[0] == repo
:
1083 merge
= get_output("git config branch.%s.merge" % current
)
1088 rhs
= "refs/heads/%s" % current
1090 if '-c' not in flags
and '-d' not in flags
:
1091 if run_command("git rev-parse --verify refs/remotes/%s/%s"
1092 % (repo
, rhs
.replace('refs/heads/', ''))):
1093 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
1094 if '-f' not in flags
:
1095 hash = get_output("git rev-parse refs/remotes/%s/%s" % (repo
, rhs
.replace('refs/heads/', '')))
1096 base
= get_output("git merge-base HEAD %s" % hash[0])
1098 if base
[0] != hash[0]:
1099 raise YapError("Branch not up-to-date with remote. Update or use -f")
1101 self
._confirm
_push
(current
, rhs
, repo
)
1108 lhs
= "refs/heads/%s" % current
1109 rc
= os
.system("git push %s %s %s:%s" % (flags
.get('-f', ''), repo
, lhs
, rhs
))
1111 raise YapError("Push failed.")
1113 @short_help("retrieve commits from a remote repository")
1115 When run with no arguments, the command will retrieve new commits from
1116 the remote tracking repository. Note that this does not in any way
1117 alter the current branch. For that, see "update". If a remote other
1118 than the tracking remote is desired, it can be specified as the first
1121 def cmd_fetch(self
, repo
=None):
1124 current
= get_output("git symbolic-ref HEAD")
1126 raise YapError("Not on a branch!")
1128 if repo
and repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1129 raise YapError("No such repository: %s" % repo
)
1131 current
= current
[0].replace('refs/heads/', '')
1132 remote
= get_output("git config branch.%s.remote" % current
)
1136 raise YapError("No tracking branch configured; specify a repository")
1137 os
.system("git fetch %s" % repo
)
1139 @short_help("update the current branch relative to its tracking branch")
1141 Updates the current branch relative to its remote tracking branch. This
1142 command requires that the current branch have a remote tracking branch
1143 configured. If any conflicts occur while applying your changes to the
1144 updated remote, the command will pause to allow you to fix them. Once
1145 that is done, run "update" with the "continue" subcommand. Alternately,
1146 the "skip" subcommand can be used to discard the conflicting changes.
1148 def cmd_update(self
, subcmd
=None):
1151 if subcmd
and subcmd
not in ["continue", "skip"]:
1155 When you have resolved the conflicts run \"yap update continue\".
1156 To skip the problematic patch, run \"yap update skip\"."""
1158 if subcmd
== "continue":
1159 os
.system("git am -3 -r --resolvemsg='%s'" % resolvemsg
)
1161 if subcmd
== "skip":
1162 os
.system("git reset --hard")
1163 os
.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg
)
1166 self
._check
_rebasing
()
1167 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
1168 raise YapError("You have uncommitted changes. Commit them first")
1170 current
= get_output("git symbolic-ref HEAD")
1172 raise YapError("Not on a branch!")
1174 current
= current
[0].replace('refs/heads/', '')
1175 remote
, merge
= self
._get
_tracking
(current
)
1176 merge
= merge
.replace('refs/heads/', '')
1178 self
.cmd_fetch(remote
)
1179 base
= get_output("git merge-base HEAD refs/remotes/%s/%s" % (remote
, merge
))
1182 fd
, tmpfile
= tempfile
.mkstemp("yap")
1184 os
.system("git format-patch -k --stdout '%s' > %s" % (base
[0], tmpfile
))
1185 self
.cmd_point("refs/remotes/%s/%s" % (remote
, merge
), **{'-f': True})
1187 stat
= os
.stat(tmpfile
)
1190 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
1192 raise YapError("Failed to apply changes")
1196 @short_help("query and configure remote branch tracking")
1198 When invoked with no arguments, the command displays the tracking
1199 information for the current branch. To configure the tracking
1200 information, two arguments for the remote repository and remote branch
1201 are given. The tracking information is used to provide defaults for
1202 where to push local changes and from where to get updates to the branch.
1204 def cmd_track(self
, repo
=None, branch
=None):
1208 current
= get_output("git symbolic-ref HEAD")
1210 raise YapError("Not on a branch!")
1211 current
= current
[0].replace('refs/heads/', '')
1213 if repo
is None and branch
is None:
1214 repo
, merge
= self
._get
_tracking
(current
)
1215 merge
= merge
.replace('refs/heads/', '')
1216 print "Branch '%s' tracking refs/remotes/%s/%s" % (current
, repo
, merge
)
1219 if repo
is None or branch
is None:
1222 if repo
not in [ x
[0] for x
in self
._list
_remotes
() ]:
1223 raise YapError("No such repository: %s" % repo
)
1225 if run_command("git rev-parse --verify refs/remotes/%s/%s" % (repo
, branch
)):
1226 raise YapError("No such branch '%s' on repository '%s'" % (branch
, repo
))
1228 os
.system("git config branch.%s.remote '%s'" % (current
, repo
))
1229 os
.system("git config branch.%s.merge 'refs/heads/%s'" % (current
, branch
))
1230 print "Branch '%s' now tracking refs/remotes/%s/%s" % (current
, repo
, branch
)
1232 @short_help("mark files with conflicts as resolved")
1234 The arguments are the files to be marked resolved. When a conflict
1235 occurs while merging changes to a file, that file is marked as
1236 "unmerged." Until the file(s) with conflicts are marked resolved,
1237 commits cannot be made.
1239 def cmd_resolved(self
, *files
):
1246 self
._stage
_one
(f
, True)
1249 @short_help("merge a branch into the current branch")
1250 def cmd_merge(self
, branch
):
1254 branch_name
= branch
1255 branch
= self
._resolve
_rev
(branch
)
1256 base
= get_output("git merge-base HEAD %s" % branch
)
1258 raise YapError("Branch '%s' is not a fork of the current branch"
1261 readtree
= ("git read-tree --aggressive -u -m %s HEAD %s"
1262 % (base
[0], branch
))
1263 if run_command(readtree
):
1264 run_command("git update-index --refresh")
1265 if os
.system(readtree
):
1266 raise YapError("Failed to merge")
1268 repo
= get_output('git rev-parse --git-dir')[0]
1269 dir = os
.path
.join(repo
, 'yap')
1274 msg_file
= os
.path
.join(dir, 'msg')
1275 msg
= file(msg_file
, 'w')
1276 print >>msg
, "Merge branch '%s'" % branch_name
1279 head
= get_output("git rev-parse --verify HEAD")
1281 heads
= [head
[0], branch
]
1282 head_file
= os
.path
.join(dir, 'merge')
1283 pickle
.dump(heads
, file(head_file
, 'w'))
1285 self
._merge
_index
(branch
, base
[0])
1286 if self
._get
_unmerged
_files
():
1288 raise YapError("Fix conflicts then commit")
1292 def _merge_index(self
, branch
, base
):
1293 for f
in self
._get
_unmerged
_files
():
1294 fd
, bfile
= tempfile
.mkstemp("yap")
1296 rc
= os
.system("git show %s:%s > %s" % (base
, f
, bfile
))
1299 fd
, ofile
= tempfile
.mkstemp("yap")
1301 rc
= os
.system("git show %s:%s > %s" % (branch
, f
, ofile
))
1304 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
)
1305 rc
= os
.system(command
)
1311 self
._stage
_one
(f
, True)
1313 def cmd_help(self
, cmd
=None):
1316 cmd
= "cmd_" + cmd
.replace('-', '_')
1318 attr
= self
.__getattribute
__(cmd
)
1319 except AttributeError:
1320 raise YapError("No such command: %s" % cmd
)
1322 help = self
._get
_attr
(cmd
, "long_help")
1324 raise YapError("Sorry, no help for '%s'. Ask Steven." % oldcmd
)
1326 print >>sys
.stderr
, "The '%s' command" % oldcmd
1327 doc
= self
._get
_attr
(cmd
, "__doc__")
1330 print >>sys
.stderr
, "\tyap %s %s" % (oldcmd
, doc
)
1331 print >>sys
.stderr
, "%s" % help
1334 print >> sys
.stderr
, "Yet Another (Git) Porcelein"
1337 for name
in dir(self
):
1338 if not name
.startswith('cmd_'):
1340 attr
= self
.__getattribute
__(name
)
1341 if not callable(attr
):
1344 short_msg
= self
._get
_attr
(name
, "short_help")
1345 if short_msg
is None:
1348 name
= name
.replace('cmd_', '')
1349 name
= name
.replace('_', '-')
1350 print >> sys
.stderr
, "%-16s%s" % (name
, short_msg
)
1353 print >> sys
.stderr
, "(*) Indicates that the command is not readily reversible"
1355 @short_help("show information about loaded plugins")
1356 def cmd_plugins(self
):
1358 print >> sys
.stderr
, "Loaded plugins:"
1359 plugins
= load_plugins()
1360 for name
, cls
in plugins
.items():
1361 print "\t%-16s: %s" % (name
, cls
.__doc
__)
1363 print "\t%-16s" % "None"
1365 def cmd_usage(self
):
1366 print >> sys
.stderr
, "usage: %s <command>" % os
.path
.basename(sys
.argv
[0])
1367 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"
1370 plugindir
= os
.path
.join("~", ".yap", "plugins")
1371 plugindir
= os
.path
.expanduser(plugindir
)
1372 plugindir
= os
.path
.join(plugindir
, "*.py")
1375 for p
in glob
.glob(os
.path
.expanduser(plugindir
)):
1376 plugin
= os
.path
.basename(p
).replace('.py', '')
1377 m
= __import__(plugin
)
1380 if not type(cls
) == type:
1382 if not issubclass(cls
, YapCore
):
1389 def yap_metaclass(name
, bases
, dct
):
1390 plugindir
= os
.path
.join("~", ".yap", "plugins")
1391 plugindir
= os
.path
.expanduser(plugindir
)
1392 sys
.path
.insert(0, plugindir
)
1394 plugins
= set(load_plugins().values())
1397 p2
-= set(cls
.__bases
__)
1399 bases
= list(plugins
) + list(bases
)
1400 return type(name
, tuple(bases
), dct
)
1403 __metaclass__
= yap_metaclass
1405 def main(self
, args
):
1413 if run_command("git --version"):
1414 print >>sys
.stderr
, "Failed to run git; is it installed?"
1417 debug
= os
.getenv('YAP_DEBUG')
1420 command
= command
.replace('-', '_')
1421 meth
= self
.__getattribute
__("cmd_"+command
)
1422 doc
= self
._get
_attr
("cmd_"+command
, "__doc__")
1426 for c
in self
.__class
__.__bases
__:
1428 t
= c
.__dict
__["cmd_"+command
]
1431 if "options" in t
.__dict
__:
1432 options
+= t
.options
1436 flags
, args
= getopt
.getopt(args
, options
)
1438 except getopt
.GetoptError
, e
:
1441 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1447 meth(*args
, **flags
)
1448 except (TypeError, getopt
.GetoptError
):
1451 print "Usage: %s %s %s" % (os
.path
.basename(sys
.argv
[0]), command
, doc
)
1455 print >> sys
.stderr
, e
1457 except AttributeError: