9 output
= fd
.readlines()
11 return [x
.strip() for x
in output
]
14 rc
= os
.system("%s > /dev/null 2>&1" % cmd
)
18 class YapError(Exception):
19 def __init__(self
, msg
):
25 def takes_options(options
):
27 func
.options
= options
31 def short_help(help_msg
):
33 func
.short_help
= help_msg
37 def long_help(help_msg
):
39 func
.long_help
= help_msg
44 def _add_new_file(self
, file):
45 repo
= get_output('git rev-parse --git-dir')[0]
46 dir = os
.path
.join(repo
, 'yap')
51 files
= self
._get
_new
_files
()
53 path
= os
.path
.join(dir, 'new-files')
54 pickle
.dump(files
, open(path
, 'w'))
56 def _get_new_files(self
):
57 repo
= get_output('git rev-parse --git-dir')[0]
58 path
= os
.path
.join(repo
, 'yap', 'new-files')
60 files
= pickle
.load(file(path
))
67 if get_output("git ls-files --cached '%s'" % f
) != []:
72 def _remove_new_file(self
, file):
73 files
= self
._get
_new
_files
()
74 files
= filter(lambda x
: x
!= file, files
)
76 repo
= get_output('git rev-parse --git-dir')[0]
77 path
= os
.path
.join(repo
, 'yap', 'new-files')
78 pickle
.dump(files
, open(path
, 'w'))
80 def _clear_new_files(self
):
81 repo
= get_output('git rev-parse --git-dir')[0]
82 path
= os
.path
.join(repo
, 'yap', 'new-files')
85 def _assert_file_exists(self
, file):
86 if not os
.access(file, os
.R_OK
):
87 raise YapError("No such file: %s" % file)
89 def _get_staged_files(self
):
90 if run_command("git rev-parse HEAD"):
91 files
= get_output("git ls-files --cached")
93 files
= get_output("git diff-index --cached --name-only HEAD")
96 def _get_unstaged_files(self
):
97 files
= self
._get
_new
_files
()
98 files
+= get_output("git ls-files -m")
101 def _delete_branch(self
, branch
, force
):
102 current
= get_output("git symbolic-ref HEAD")[0]
103 current
= current
.replace('refs/heads/', '')
104 if branch
== current
:
105 raise YapError("Can't delete current branch")
107 ref
= get_output("git rev-parse 'refs/heads/%s'" % branch
)
109 raise YapError("No such branch: %s" % branch
)
110 os
.system("git update-ref -d 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
113 name
= get_output("git name-rev --name-only '%s'" % ref
[0])[0]
114 if name
== 'undefined':
115 os
.system("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
116 raise YapError("Refusing to delete leaf branch (use -f to force)")
117 def _get_pager_cmd(self
):
118 if 'YAP_PAGER' in os
.environ
:
119 return os
.environ
['YAP_PAGER']
120 elif 'GIT_PAGER' in os
.environ
:
121 return os
.environ
['GIT_PAGER']
122 elif 'PAGER' in os
.environ
:
123 return os
.environ
['PAGER']
127 def _add_one(self
, file):
128 self
._assert
_file
_exists
(file)
129 x
= get_output("git ls-files '%s'" % file)
131 raise YapError("File '%s' already in repository" % file)
132 self
._add
_new
_file
(file)
134 def _rm_one(self
, file):
135 self
._assert
_file
_exists
(file)
136 if get_output("git ls-files '%s'" % file) != []:
137 os
.system("git rm --cached '%s'" % file)
138 self
._remove
_new
_file
(file)
140 def _stage_one(self
, file):
141 self
._assert
_file
_exists
(file)
142 os
.system("git update-index --add '%s'" % file)
144 def _unstage_one(self
, file):
145 self
._assert
_file
_exists
(file)
146 if run_command("git rev-parse HEAD"):
147 os
.system("git update-index --force-remove '%s'" % file)
149 os
.system("git diff-index -p HEAD '%s' | git apply -R --cached" % file)
151 def _revert_one(self
, file):
152 self
._assert
_file
_exists
(file)
153 os
.system("git checkout-index -f '%s'" % file)
155 def _parse_commit(self
, commit
):
156 lines
= get_output("git cat-file commit '%s'" % commit
)
161 if mode
!= 'commit' and l
.strip() == "":
166 commit
['log'].append(l
)
173 commit
['log'] = '\n'.join(commit
['log'])
176 def _check_commit(self
, **flags
):
177 if '-a' in flags
and '-d' in flags
:
178 raise YapError("Conflicting flags: -a and -d")
180 if '-d' not in flags
and self
._get
_unstaged
_files
():
181 if '-a' not in flags
and self
._get
_staged
_files
():
182 raise YapError("Staged and unstaged changes present. Specify what to commit")
183 os
.system("git diff-files -p | git apply --cached 2>/dev/null")
184 for f
in self
._get
_new
_files
():
187 if not self
._get
_staged
_files
():
188 raise YapError("No changes to commit")
190 def _do_uncommit(self
):
191 commit
= self
._parse
_commit
("HEAD")
192 repo
= get_output('git rev-parse --git-dir')[0]
193 dir = os
.path
.join(repo
, 'yap')
198 msg_file
= os
.path
.join(dir, 'msg')
199 fd
= file(msg_file
, 'w')
200 print >>fd
, commit
['log']
203 tree
= get_output("git rev-parse HEAD^")
204 os
.system("git update-ref -m uncommit HEAD '%s'" % tree
[0])
206 def _do_commit(self
):
207 tree
= get_output("git write-tree")[0]
208 parent
= get_output("git rev-parse HEAD 2> /dev/null")[0]
210 if os
.environ
.has_key('YAP_EDITOR'):
211 editor
= os
.environ
['YAP_EDITOR']
212 elif os
.environ
.has_key('GIT_EDITOR'):
213 editor
= os
.environ
['GIT_EDITOR']
214 elif os
.environ
.has_key('EDITOR'):
215 editor
= os
.environ
['EDITOR']
219 fd
, tmpfile
= tempfile
.mkstemp("yap")
222 repo
= get_output('git rev-parse --git-dir')[0]
223 msg_file
= os
.path
.join(repo
, 'yap', 'msg')
224 if os
.access(msg_file
, os
.R_OK
):
226 fd2
= file(tmpfile
, 'w')
227 for l
in fd1
.xreadlines():
228 print >>fd2
, l
.strip()
232 if os
.system("%s '%s'" % (editor
, tmpfile
)) != 0:
233 raise YapError("Editing commit message failed")
235 commit
= get_output("git commit-tree '%s' -p '%s' < '%s'" % (tree
, parent
, tmpfile
))
237 commit
= get_output("git commit-tree '%s' < '%s'" % (tree
, tmpfile
))
239 raise YapError("Commit failed; no log message?")
241 os
.system("git update-ref HEAD '%s'" % commit
[0])
243 def _check_rebasing(self
):
244 repo
= get_output('git rev-parse --git-dir')[0]
245 dotest
= os
.path
.join(repo
, '.dotest')
246 if os
.access(dotest
, os
.R_OK
):
247 raise YapError("A git operation is in progress. Complete it first")
248 dotest
= os
.path
.join(repo
, '..', '.dotest')
249 if os
.access(dotest
, os
.R_OK
):
250 raise YapError("A git operation is in progress. Complete it first")
252 def _list_remotes(self
):
253 remotes
= get_output("git config --get-regexp 'remote.*.url'")
255 remote
, url
= x
.split(' ')
256 remote
= remote
.replace('remote.', '')
257 remote
= remote
.replace('.url', '')
260 @short_help("make a local copy of an existing repository")
261 def cmd_clone(self
, url
, directory
=""):
263 # XXX: implement in terms of init + remote add + fetch
264 os
.system("git clone '%s' %s" % (url
, directory
))
266 @short_help("turn a directory into a repository")
268 os
.system("git init")
270 @short_help("add a new file to the repository")
271 def cmd_add(self
, *files
):
280 @short_help("delete a file from the repository")
281 def cmd_rm(self
, *files
):
290 @short_help("stage changes in a file for commit")
291 def cmd_stage(self
, *files
):
300 @short_help("unstage changes in a file")
301 def cmd_unstage(self
, *files
):
310 @short_help("show files with staged and unstaged changes")
311 def cmd_status(self
):
312 branch
= get_output("git symbolic-ref HEAD")[0]
313 branch
= branch
.replace('refs/heads/', '')
314 print "Current branch: %s" % branch
316 print "Files with staged changes:"
317 files
= self
._get
_staged
_files
()
323 print "Files with unstaged changes:"
324 prefix
= get_output("git rev-parse --show-prefix")
325 files
= self
._get
_unstaged
_files
()
328 f
= os
.path
.join(prefix
[0], f
)
333 @short_help("remove uncommitted changes from a file (*)")
335 def cmd_revert(self
, *files
, **flags
):
338 os
.system("git checkout-index -f -a")
348 @short_help("record changes to files as a new commit")
350 def cmd_commit(self
, **flags
):
351 self
._check
_rebasing
()
352 self
._check
_commit
(**flags
)
356 @short_help("reverse the actions of the last commit")
357 def cmd_uncommit(self
):
361 def cmd_version(self
):
362 print "Yap version 0.1"
364 @short_help("show the changelog for particular versions or files")
366 def cmd_log(self
, *paths
, **flags
):
367 "[-r <rev>] <path>..."
368 rev
= flags
.get('-r', 'HEAD')
369 paths
= ' '.join(paths
)
370 os
.system("git log --name-status '%s' -- %s" % (rev
, paths
))
372 @short_help("show staged, unstaged, or all uncommitted changes")
374 def cmd_diff(self
, **flags
):
376 if '-u' in flags
and '-d' in flags
:
377 raise YapError("Conflicting flags: -u and -d")
379 pager
= self
._get
_pager
_cmd
()
381 os
.system("git update-index -q --refresh")
383 os
.system("git diff-files -p | %s" % pager
)
385 os
.system("git diff-index --cached -p HEAD | %s" % pager
)
387 os
.system("git diff-index -p HEAD | %s" % pager
)
389 @short_help("list, create, or delete branches")
390 @takes_options("fd:")
391 def cmd_branch(self
, branch
=None, **flags
):
392 "[ [-f] -d <branch> | <branch> ]"
393 force
= '-f' in flags
395 self
._delete
_branch
(flags
['-d'], force
)
399 if branch
is not None:
400 ref
= get_output("git rev-parse HEAD")
402 raise YapError("No branch point yet. Make a commit")
403 os
.system("git update-ref 'refs/heads/%s' '%s'" % (branch
, ref
[0]))
405 current
= get_output("git symbolic-ref HEAD")[0]
406 branches
= get_output("git for-each-ref --format='%(refname)' 'refs/heads/*'")
412 b
= b
.replace('refs/heads/', '')
415 @short_help("change the current working branch")
416 def cmd_switch(self
, branch
):
418 ref
= get_output("git rev-parse 'refs/heads/%s'" % branch
)
420 raise YapError("No such branch: %s" % branch
)
422 # XXX: support merging like git-checkout
423 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
424 raise YapError("You have uncommitted changes. Commit them first")
426 os
.system("git symbolic-ref HEAD refs/heads/'%s'" % branch
)
427 os
.system("git read-tree HEAD")
428 os
.system("git checkout-index -f -a")
431 @short_help("move the current branch to a different revision")
433 def cmd_point(self
, where
, **flags
):
435 head
= get_output("git rev-parse HEAD")
437 raise YapError("No commit yet; nowhere to point")
439 ref
= get_output("git rev-parse '%s'" % where
)
441 raise YapError("Not a valid ref: %s" % where
)
443 if self
._get
_unstaged
_files
() or self
._get
_staged
_files
():
444 raise YapError("You have uncommitted changes. Commit them first")
446 type = get_output("git cat-file -t '%s'" % ref
[0])
447 if type and type[0] == "tag":
448 tag
= get_output("git cat-file tag '%s'" % ref
[0])
449 ref
[0] = tag
[0].split(' ')[1]
451 os
.system("git update-ref HEAD '%s'" % ref
[0])
453 if '-f' not in flags
:
454 name
= get_output("git name-rev --name-only '%s'" % head
[0])[0]
455 if name
== "undefined":
456 os
.system("git update-ref HEAD '%s'" % head
[0])
457 raise YapError("Pointing there will lose commits. Use -f to force")
459 os
.system("git read-tree HEAD")
460 os
.system("git checkout-index -f -a")
461 os
.system("git update-index --refresh")
463 @short_help("alter history by dropping or amending commits")
464 def cmd_history(self
, subcmd
, *args
):
465 "amend | drop <commit>"
467 if subcmd
not in ("amend", "drop", "continue", "skip"):
471 When you have resolved the conflicts run \"yap history continue\".
472 To skip the problematic patch, run \"yap history skip\"."""
474 if subcmd
== "continue":
475 os
.system("git am -r --resolvemsg='%s'" % resolvemsg
)
478 os
.system("git reset --hard")
479 os
.system("git am --skip --resolvemsg='%s'" % resolvemsg
)
482 if subcmd
== "amend":
483 flags
, args
= getopt
.getopt(args
, "ad")
493 if run_command("git rev-parse --verify '%s'" % commit
):
494 raise YapError("Not a valid commit: %s" % commit
)
496 self
._check
_rebasing
()
498 if subcmd
== "amend":
499 self
._check
_commit
(**flags
)
501 stash
= get_output("git stash create")
502 run_command("git reset --hard")
504 if subcmd
== "amend" and not stash
:
505 raise YapError("Failed to stash; no changes?")
507 fd
, tmpfile
= tempfile
.mkstemp("yap")
510 os
.system("git format-patch -k --stdout '%s' > %s" % (commit
, tmpfile
))
511 if subcmd
== "amend":
512 self
.cmd_point(commit
, **{'-f': True})
513 run_command("git stash apply --index %s" % stash
[0])
516 stash
= get_output("git stash create")
517 run_command("git reset --hard")
519 self
.cmd_point("%s^" % commit
, **{'-f': True})
521 stat
= os
.stat(tmpfile
)
524 rc
= os
.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg
, tmpfile
))
526 raise YapError("Failed to apply changes")
529 run_command("git stash apply %s" % stash
[0])
534 @short_help("show the changes introduced by a given commit")
535 def cmd_show(self
, commit
="HEAD"):
537 os
.system("git show '%s'" % commit
)
539 @short_help("apply the changes in a given commit to the current branch")
541 def cmd_cherry_pick(self
, commit
, **flags
):
544 os
.system("git revert '%s'" % commit
)
546 os
.system("git cherry-pick '%s'" % commit
)
548 @short_help("list, add, or delete configured remote repositories")
550 def cmd_repo(self
, name
=None, url
=None, **flags
):
551 "[<name> <url> | -d <name>]"
552 if name
is not None and url
is None:
556 if flags
['-d'] not in self
._list
_remotes
():
557 raise YapError("No such repository: %s" % flags
['-d'])
558 os
.system("git config --unset remote.%s.url" % flags
['-d'])
559 os
.system("git config --unset remote.%s.fetch" % flags
['-d'])
562 if flags
['-d'] in self
._list
_remotes
():
563 raise YapError("Repository '%s' already exists" % flags
['-d'])
564 os
.system("git config remote.%s.url %s" % (name
, url
))
565 os
.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name
, url
))
567 for remote
, url
in self
._list
_remotes
():
568 print "%s:\t\t%s" % (remote
, url
)
571 print >> sys
.stderr
, "Yet Another (Git) Porcelein"
574 for name
in dir(self
):
575 if not name
.startswith('cmd_'):
577 attr
= self
.__getattribute
__(name
)
578 if not callable(attr
):
581 short_msg
= attr
.short_help
582 except AttributeError:
585 name
= name
.replace('cmd_', '')
586 name
= name
.replace('_', '-')
587 print >> sys
.stderr
, "%-16s%s" % (name
, short_msg
)
589 print >> sys
.stderr
, "(*) Indicates that the command is not readily reversible"
592 print >> sys
.stderr
, "usage: %s <command>" % sys
.argv
[0]
593 print >> sys
.stderr
, " valid commands: help init add rm stage unstage status revert commit uncommit log show diff branch switch point cherry-pick history version"
595 def main(self
, args
):
603 debug
= os
.getenv('YAP_DEBUG')
606 command
= command
.replace('-', '_')
607 meth
= self
.__getattribute
__("cmd_"+command
)
609 if "options" in meth
.__dict
__:
610 flags
, args
= getopt
.getopt(args
, meth
.options
)
616 except (TypeError, getopt
.GetoptError
):
619 print "%s %s %s" % (sys
.argv
[0], command
, meth
.__doc
__)
621 print >> sys
.stderr
, e
623 except AttributeError: