cmd_revert: show status after a revert all
[yap.git] / yap / yap.py
blob01998ecdd0fecd34d63c5ef0230a74d7b13838cc
1 import sys
2 import os
3 import getopt
4 import pickle
5 import tempfile
7 def get_output(cmd):
8 fd = os.popen(cmd)
9 output = fd.readlines()
10 rc = fd.close()
11 return [x.strip() for x in output]
13 def run_command(cmd):
14 rc = os.system("%s > /dev/null 2>&1" % cmd)
15 rc >>= 8
16 return rc
18 class YapError(Exception):
19 def __init__(self, msg):
20 self.msg = msg
22 def __str__(self):
23 return self.msg
25 def takes_options(options):
26 def decorator(func):
27 func.options = options
28 return func
29 return decorator
31 def short_help(help_msg):
32 def decorator(func):
33 func.short_help = help_msg
34 return func
35 return decorator
37 def long_help(help_msg):
38 def decorator(func):
39 func.long_help = help_msg
40 return func
41 return decorator
43 class Yap(object):
44 def _add_new_file(self, file):
45 repo = get_output('git rev-parse --git-dir')[0]
46 dir = os.path.join(repo, 'yap')
47 try:
48 os.mkdir(dir)
49 except OSError:
50 pass
51 files = self._get_new_files()
52 files.append(file)
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')
59 try:
60 files = pickle.load(file(path))
61 except IOError:
62 files = []
64 x = []
65 for f in files:
66 # if f in the index
67 if get_output("git ls-files --cached '%s'" % f) != []:
68 continue
69 x.append(f)
70 return x
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')
83 os.unlink(path)
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")
92 else:
93 files = get_output("git diff-index --cached --name-only HEAD")
94 return files
96 def _get_unstaged_files(self):
97 files = self._get_new_files()
98 files += get_output("git ls-files -m")
99 return files
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)
108 if not ref:
109 raise YapError("No such branch: %s" % branch)
110 os.system("git update-ref -d 'refs/heads/%s' '%s'" % (branch, ref[0]))
112 if not force:
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']
124 else:
125 return "more"
127 def _add_one(self, file):
128 self._assert_file_exists(file)
129 x = get_output("git ls-files '%s'" % file)
130 if x != []:
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)
148 else:
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 self._unstage_one(file)
154 os.system("git checkout-index -u -f '%s'" % file)
156 def _parse_commit(self, commit):
157 lines = get_output("git cat-file commit '%s'" % commit)
158 commit = {}
160 mode = None
161 for l in lines:
162 if mode != 'commit' and l.strip() == "":
163 mode = 'commit'
164 commit['log'] = []
165 continue
166 if mode == 'commit':
167 commit['log'].append(l)
168 continue
170 x = l.split(' ')
171 k = x[0]
172 v = ' '.join(x[1:])
173 commit[k] = v
174 commit['log'] = '\n'.join(commit['log'])
175 return commit
177 def _check_commit(self, **flags):
178 if '-a' in flags and '-d' in flags:
179 raise YapError("Conflicting flags: -a and -d")
181 if '-d' not in flags and self._get_unstaged_files():
182 if '-a' not in flags and self._get_staged_files():
183 raise YapError("Staged and unstaged changes present. Specify what to commit")
184 os.system("git diff-files -p | git apply --cached 2>/dev/null")
185 for f in self._get_new_files():
186 self._stage_one(f)
188 def _do_uncommit(self):
189 commit = self._parse_commit("HEAD")
190 repo = get_output('git rev-parse --git-dir')[0]
191 dir = os.path.join(repo, 'yap')
192 try:
193 os.mkdir(dir)
194 except OSError:
195 pass
196 msg_file = os.path.join(dir, 'msg')
197 fd = file(msg_file, 'w')
198 print >>fd, commit['log']
199 fd.close()
201 tree = get_output("git rev-parse HEAD^")
202 os.system("git update-ref -m uncommit HEAD '%s'" % tree[0])
204 def _do_commit(self):
205 tree = get_output("git write-tree")[0]
206 parent = get_output("git rev-parse HEAD 2> /dev/null")[0]
208 if os.environ.has_key('YAP_EDITOR'):
209 editor = os.environ['YAP_EDITOR']
210 elif os.environ.has_key('GIT_EDITOR'):
211 editor = os.environ['GIT_EDITOR']
212 elif os.environ.has_key('EDITOR'):
213 editor = os.environ['EDITOR']
214 else:
215 editor = "vi"
217 fd, tmpfile = tempfile.mkstemp("yap")
218 os.close(fd)
220 repo = get_output('git rev-parse --git-dir')[0]
221 msg_file = os.path.join(repo, 'yap', 'msg')
222 if os.access(msg_file, os.R_OK):
223 fd1 = file(msg_file)
224 fd2 = file(tmpfile, 'w')
225 for l in fd1.xreadlines():
226 print >>fd2, l.strip()
227 fd2.close()
228 os.unlink(msg_file)
230 if os.system("%s '%s'" % (editor, tmpfile)) != 0:
231 raise YapError("Editing commit message failed")
232 if parent != 'HEAD':
233 commit = get_output("git commit-tree '%s' -p '%s' < '%s'" % (tree, parent, tmpfile))
234 else:
235 commit = get_output("git commit-tree '%s' < '%s'" % (tree, tmpfile))
236 if not commit:
237 raise YapError("Commit failed; no log message?")
238 os.unlink(tmpfile)
239 os.system("git update-ref HEAD '%s'" % commit[0])
241 def _check_rebasing(self):
242 repo = get_output('git rev-parse --git-dir')[0]
243 dotest = os.path.join(repo, '.dotest')
244 if os.access(dotest, os.R_OK):
245 raise YapError("A git operation is in progress. Complete it first")
246 dotest = os.path.join(repo, '..', '.dotest')
247 if os.access(dotest, os.R_OK):
248 raise YapError("A git operation is in progress. Complete it first")
250 def _list_remotes(self):
251 remotes = get_output("git config --get-regexp 'remote.*.url'")
252 for x in remotes:
253 remote, url = x.split(' ')
254 remote = remote.replace('remote.', '')
255 remote = remote.replace('.url', '')
256 yield remote, url
258 @short_help("make a local copy of an existing repository")
259 @long_help("""
260 The first argument is a URL to the existing repository. This can be an
261 absolute path if the repository is local, or a URL with the git://,
262 ssh://, or http:// schemes. By default, the directory used is the last
263 component of the URL, sans '.git'. This can be overridden by providing
264 a second argument.
265 """)
266 def cmd_clone(self, url, directory=""):
267 "<url> [directory]"
268 # XXX: implement in terms of init + remote add + fetch
269 os.system("git clone '%s' %s" % (url, directory))
271 @short_help("turn a directory into a repository")
272 @long_help("""
273 Converts the current working directory into a repository. The primary
274 side-effect of this command is the creation of a '.git' subdirectory.
275 No files are added nor commits made.
276 """)
277 def cmd_init(self):
278 os.system("git init")
280 @short_help("add a new file to the repository")
281 @long_help("""
282 The arguments are the files to be added to the repository. Once added,
283 the files will show as "unstaged changes" in the output of 'status'. To
284 reverse the effects of this command, see 'rm'.
285 """)
286 def cmd_add(self, *files):
287 "<file>..."
288 if not files:
289 raise TypeError
291 for f in files:
292 self._add_one(f)
293 self.cmd_status()
295 @short_help("delete a file from the repository")
296 @long_help("""
297 The arguments are the files to be removed from the current revision of
298 the repository. The files will still exist in any past commits that the
299 file may have been a part of. The file is not actually deleted, it is
300 just no longer tracked as part of the repository.
301 """)
302 def cmd_rm(self, *files):
303 "<file>..."
304 if not files:
305 raise TypeError
307 for f in files:
308 self._rm_one(f)
309 self.cmd_status()
311 @short_help("stage changes in a file for commit")
312 @long_help("""
313 The arguments are the files to be staged. Staging changes is a way to
314 build up a commit when you do not want to commit all changes at once.
315 To commit only staged changes, use the '-d' flag to 'commit.' To
316 reverse the effects of this command, see 'unstage'. Once staged, the
317 files will show as "staged changes" in the output of 'status'.
318 """)
319 def cmd_stage(self, *files):
320 "<file>..."
321 if not files:
322 raise TypeError
324 for f in files:
325 self._stage_one(f)
326 self.cmd_status()
328 @short_help("unstage changes in a file")
329 @long_help("""
330 The arguments are the files to be unstaged. Once unstaged, the files
331 will show as "unstaged changes" in the output of 'status'. The '-a'
332 flag can be used to unstage all staged changes at once.
333 """)
334 @takes_options("a")
335 def cmd_unstage(self, *files, **flags):
336 "[-a] | <file>..."
337 if '-a' in flags:
338 os.system("git read-tree -m HEAD")
339 self.cmd_status()
340 return
342 if not files:
343 raise TypeError
345 for f in files:
346 self._unstage_one(f)
347 self.cmd_status()
349 @short_help("show files with staged and unstaged changes")
350 @long_help("""
351 Show the files in the repository with changes since the last commit,
352 categorized based on whether the changes are staged or not. A file may
353 appear under each heading if the same file has both staged and unstaged
354 changes.
355 """)
356 def cmd_status(self):
357 branch = get_output("git symbolic-ref HEAD")[0]
358 branch = branch.replace('refs/heads/', '')
359 print "Current branch: %s" % branch
361 print "Files with staged changes:"
362 files = self._get_staged_files()
363 for f in files:
364 print "\t%s" % f
365 if not files:
366 print "\t(none)"
368 print "Files with unstaged changes:"
369 prefix = get_output("git rev-parse --show-prefix")
370 files = self._get_unstaged_files()
371 for f in files:
372 if prefix:
373 f = os.path.join(prefix[0], f)
374 print "\t%s" % f
375 if not files:
376 print "\t(none)"
378 @short_help("remove uncommitted changes from a file (*)")
379 @long_help("""
380 The arguments are the files whose changes will be reverted. If the '-a'
381 flag is given, then all files will have uncommitted changes removed.
382 Note that there is no way to reverse this command short of manually
383 editing each file again.
384 """)
385 @takes_options("a")
386 def cmd_revert(self, *files, **flags):
387 "(-a | <file>)"
388 if '-a' in flags:
389 os.system("git read-tree -m HEAD")
390 os.system("git checkout-index -u -f -a")
391 self.cmd_status()
392 return
394 if not files:
395 raise TypeError
397 for f in files:
398 self._revert_one(f)
399 self.cmd_status()
401 @short_help("record changes to files as a new commit")
402 @long_help("""
403 Create a new commit recording changes since the last commit. If there
404 are only unstaged changes, those will be recorded. If there are only
405 staged changes, those will be recorder. Otherwise, you will have to
406 specify either the '-a' flag or the '-d' flag to commit all changes or
407 only staged changes, respectively. To reverse the effects of this
408 command, see 'uncommit'.
409 """)
410 @takes_options("ad")
411 def cmd_commit(self, **flags):
412 self._check_rebasing()
413 self._check_commit(**flags)
414 if not self._get_staged_files():
415 raise YapError("No changes to commit")
416 self._do_commit()
417 self.cmd_status()
419 @short_help("reverse the actions of the last commit")
420 @long_help("""
421 Reverse the effects of the last 'commit' operation. The changes that
422 were part of the previous commit will show as "staged changes" in the
423 output of 'status'. This means that if no files were changed since the
424 last commit was created, 'uncommit' followed by 'commit' is a lossless
425 operation.
426 """)
427 def cmd_uncommit(self):
428 self._do_uncommit()
429 self.cmd_status()
431 @short_help("report the current version of yap")
432 def cmd_version(self):
433 print "Yap version 0.1"
435 @short_help("show the changelog for particular versions or files")
436 @long_help("""
437 The arguments are the files with which to filter history. If none are
438 given, all changes are listed. Otherwise only commits that affected one
439 or more of the given files are listed. The -r option changes the
440 starting revision for traversing history. By default, history is listed
441 starting at HEAD.
442 """)
443 @takes_options("r:")
444 def cmd_log(self, *paths, **flags):
445 "[-r <rev>] <path>..."
446 rev = flags.get('-r', 'HEAD')
447 paths = ' '.join(paths)
448 os.system("git log --name-status '%s' -- %s" % (rev, paths))
450 @short_help("show staged, unstaged, or all uncommitted changes")
451 @long_help("""
452 Show staged, unstaged, or all uncommitted changes. By default, all
453 changes are shown. The '-u' flag causes only unstaged changes to be
454 shown. The '-d' flag causes only staged changes to be shown.
455 """)
456 @takes_options("ud")
457 def cmd_diff(self, **flags):
458 "[ -u | -d ]"
459 if '-u' in flags and '-d' in flags:
460 raise YapError("Conflicting flags: -u and -d")
462 pager = self._get_pager_cmd()
464 if '-u' in flags:
465 os.system("git diff-files -p | %s" % pager)
466 elif '-d' in flags:
467 os.system("git diff-index --cached -p HEAD | %s" % pager)
468 else:
469 os.system("git diff-index -p HEAD | %s" % pager)
471 @short_help("list, create, or delete branches")
472 @long_help("""
473 If no arguments are given, a list of local branches is given. The
474 current branch is indicated by a "*" next to the name. If an argument
475 is given, it is taken as the name of a new branch to create. The branch
476 will start pointing at the current HEAD. See 'point' for details on
477 changing the revision of the new branch. Note that this command does
478 not switch the current working branch. See 'switch' for details on
479 changing the current working branch.
481 The '-d' flag can be used to delete local branches. If the delete
482 operation would remove the last branch reference to a given line of
483 history (colloquially referred to as "dangling commits"), yap will
484 report an error and abort. The '-f' flag can be used to force the delete
485 in spite of this.
486 """)
487 @takes_options("fd:")
488 def cmd_branch(self, branch=None, **flags):
489 "[ [-f] -d <branch> | <branch> ]"
490 force = '-f' in flags
491 if '-d' in flags:
492 self._delete_branch(flags['-d'], force)
493 self.cmd_branch()
494 return
496 if branch is not None:
497 ref = get_output("git rev-parse HEAD")
498 if not ref:
499 raise YapError("No branch point yet. Make a commit")
500 os.system("git update-ref 'refs/heads/%s' '%s'" % (branch, ref[0]))
502 current = get_output("git symbolic-ref HEAD")[0]
503 branches = get_output("git for-each-ref --format='%(refname)' 'refs/heads/*'")
504 for b in branches:
505 if b == current:
506 print "* ",
507 else:
508 print " ",
509 b = b.replace('refs/heads/', '')
510 print b
512 @short_help("change the current working branch")
513 @long_help("""
514 The argument is the name of the branch to make the current working
515 branch. This command will fail if there are uncommitted changes to any
516 files. Otherwise, the contents of the files in the working directory
517 are updated to reflect their state in the new branch. Additionally, any
518 future commits are added to the new branch instead of the previous line
519 of history.
520 """)
521 def cmd_switch(self, branch):
522 "<branch>"
523 ref = get_output("git rev-parse 'refs/heads/%s'" % branch)
524 if not ref:
525 raise YapError("No such branch: %s" % branch)
527 # XXX: support merging like git-checkout
528 if self._get_unstaged_files() or self._get_staged_files():
529 raise YapError("You have uncommitted changes. Commit them first")
531 os.system("git symbolic-ref HEAD refs/heads/'%s'" % branch)
532 os.system("git read-tree -m HEAD")
533 os.system("git checkout-index -u -f -a")
534 self.cmd_branch()
536 @short_help("move the current branch to a different revision")
537 @long_help("""
538 The argument is the hash of the commit to which the current branch
539 should point, or alternately a branch or tag (a.k.a, "committish"). If
540 moving the branch would create "dangling commits" (see 'branch'), yap
541 will report an error and abort. The '-f' flag can be used to force the
542 operation in spite of this.
543 """)
544 @takes_options("f")
545 def cmd_point(self, where, **flags):
546 "<where>"
547 head = get_output("git rev-parse HEAD")
548 if not head:
549 raise YapError("No commit yet; nowhere to point")
551 ref = get_output("git rev-parse '%s'" % where)
552 if not ref:
553 raise YapError("Not a valid ref: %s" % where)
555 if self._get_unstaged_files() or self._get_staged_files():
556 raise YapError("You have uncommitted changes. Commit them first")
558 type = get_output("git cat-file -t '%s'" % ref[0])
559 if type and type[0] == "tag":
560 tag = get_output("git cat-file tag '%s'" % ref[0])
561 ref[0] = tag[0].split(' ')[1]
563 os.system("git update-ref HEAD '%s'" % ref[0])
565 if '-f' not in flags:
566 name = get_output("git name-rev --name-only '%s'" % head[0])[0]
567 if name == "undefined":
568 os.system("git update-ref HEAD '%s'" % head[0])
569 raise YapError("Pointing there will lose commits. Use -f to force")
571 os.system("git read-tree -m HEAD")
572 os.system("git checkout-index -u -f -a")
574 @short_help("alter history by dropping or amending commits")
575 @long_help("""
576 This command operates in two distinct modes, "amend" and "drop" mode.
577 In drop mode, the given commit is removed from the history of the
578 current branch, as though that commit never happened. By default the
579 commit used is HEAD.
581 In amend mode, the uncommitted changes present are merged into a
582 previous commit. This is useful for correcting typos or adding missed
583 files into past commits. By default the commit used is HEAD.
585 While rewriting history it is possible that conflicts will arise. If
586 this happens, the rewrite will pause and you will be prompted to resolve
587 the conflicts and staged them. Once that is done, you will run "yap
588 history continue." If instead you want the conflicting commit removed
589 from history (perhaps your changes supercede that commit) you can run
590 "yap history skip". Once the rewrite completes, your branch will be on
591 the same commit as when the rewrite started.
592 """)
593 def cmd_history(self, subcmd, *args):
594 "amend | drop <commit>"
596 if subcmd not in ("amend", "drop", "continue", "skip"):
597 raise TypeError
599 resolvemsg = """
600 When you have resolved the conflicts run \"yap history continue\".
601 To skip the problematic patch, run \"yap history skip\"."""
603 if subcmd == "continue":
604 os.system("git am -3 -r --resolvemsg='%s'" % resolvemsg)
605 return
606 if subcmd == "skip":
607 os.system("git reset --hard")
608 os.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg)
609 return
611 if subcmd == "amend":
612 flags, args = getopt.getopt(args, "ad")
613 flags = dict(flags)
615 if len(args) > 1:
616 raise TypeError
617 if args:
618 commit = args[0]
619 else:
620 commit = "HEAD"
622 if run_command("git rev-parse --verify '%s'" % commit):
623 raise YapError("Not a valid commit: %s" % commit)
625 self._check_rebasing()
627 if subcmd == "amend":
628 self._check_commit(**flags)
630 stash = get_output("git stash create")
631 run_command("git reset --hard")
633 fd, tmpfile = tempfile.mkstemp("yap")
634 os.close(fd)
635 try:
636 os.system("git format-patch -k --stdout '%s' > %s" % (commit, tmpfile))
637 if subcmd == "amend":
638 self.cmd_point(commit, **{'-f': True})
639 if stash:
640 run_command("git stash apply --index %s" % stash[0])
641 self._do_uncommit()
642 self._do_commit()
643 stash = get_output("git stash create")
644 run_command("git reset --hard")
645 else:
646 self.cmd_point("%s^" % commit, **{'-f': True})
648 stat = os.stat(tmpfile)
649 size = stat[6]
650 if size > 0:
651 rc = os.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg, tmpfile))
652 if (rc):
653 raise YapError("Failed to apply changes")
655 if stash:
656 run_command("git stash apply %s" % stash[0])
657 finally:
658 os.unlink(tmpfile)
659 self.cmd_status()
661 @short_help("show the changes introduced by a given commit")
662 @long_help("""
663 By default, the changes in the last commit are shown. To override this,
664 specify a hash, branch, or tag (committish). The hash of the commit,
665 the commit's author, log message, and a diff of the changes are shown.
666 """)
667 def cmd_show(self, commit="HEAD"):
668 "[commit]"
669 os.system("git show '%s'" % commit)
671 @short_help("apply the changes in a given commit to the current branch")
672 @long_help("""
673 The argument is the hash, branch, or tag (committish) of the commit to
674 be applied. In general, it only makes sense to apply commits that
675 happened on another branch. The '-r' flag can be used to have the
676 changes in the given commit reversed from the current branch. In
677 general, this only makes sense for commits that happened on the current
678 branch.
679 """)
680 @takes_options("r")
681 def cmd_cherry_pick(self, commit, **flags):
682 "[-r] <commit>"
683 if '-r' in flags:
684 os.system("git revert '%s'" % commit)
685 else:
686 os.system("git cherry-pick '%s'" % commit)
688 @short_help("list, add, or delete configured remote repositories")
689 @long_help("""
690 When invoked with no arguments, this command will show the list of
691 currently configured remote repositories, giving both the name and URL
692 of each. To add a new repository, give the desired name as the first
693 argument and the URL as the second. The '-d' flag can be used to remove
694 a previously added repository.
695 """)
696 @takes_options("d:")
697 def cmd_repo(self, name=None, url=None, **flags):
698 "[<name> <url> | -d <name>]"
699 if name is not None and url is None:
700 raise TypeError
702 if '-d' in flags:
703 if flags['-d'] not in self._list_remotes():
704 raise YapError("No such repository: %s" % flags['-d'])
705 os.system("git config --unset remote.%s.url" % flags['-d'])
706 os.system("git config --unset remote.%s.fetch" % flags['-d'])
708 if name:
709 if flags['-d'] in self._list_remotes():
710 raise YapError("Repository '%s' already exists" % flags['-d'])
711 os.system("git config remote.%s.url %s" % (name, url))
712 os.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name, url))
714 for remote, url in self._list_remotes():
715 print "%s:\t\t%s" % (remote, url)
717 def cmd_help(self, cmd=None):
718 if cmd is not None:
719 try:
720 attr = self.__getattribute__("cmd_"+cmd)
721 except AttributeError:
722 raise YapError("No such command: %s" % cmd)
723 try:
724 help = attr.long_help
725 except AttributeError:
726 raise YapError("Sorry, no help for '%s'. Ask Steven." % cmd)
728 print >>sys.stderr, "The '%s' command" % cmd
729 print >>sys.stderr, "\tyap %s %s" % (cmd, attr.__doc__)
730 print >>sys.stderr, "%s" % help
731 return
733 print >> sys.stderr, "Yet Another (Git) Porcelein"
734 print >> sys.stderr
736 for name in dir(self):
737 if not name.startswith('cmd_'):
738 continue
739 attr = self.__getattribute__(name)
740 if not callable(attr):
741 continue
742 try:
743 short_msg = attr.short_help
744 except AttributeError:
745 continue
747 name = name.replace('cmd_', '')
748 name = name.replace('_', '-')
749 print >> sys.stderr, "%-16s%s" % (name, short_msg)
750 print >> sys.stderr
751 print >> sys.stderr, "(*) Indicates that the command is not readily reversible"
753 def cmd_usage(self):
754 print >> sys.stderr, "usage: %s <command>" % sys.argv[0]
755 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"
757 def main(self, args):
758 if len(args) < 1:
759 self.cmd_usage()
760 sys.exit(2)
762 command = args[0]
763 args = args[1:]
765 debug = os.getenv('YAP_DEBUG')
767 try:
768 command = command.replace('-', '_')
769 meth = self.__getattribute__("cmd_"+command)
770 try:
771 if "options" in meth.__dict__:
772 flags, args = getopt.getopt(args, meth.options)
773 flags = dict(flags)
774 else:
775 flags = dict()
777 meth(*args, **flags)
778 except (TypeError, getopt.GetoptError):
779 if debug:
780 raise
781 print "%s %s %s" % (sys.argv[0], command, meth.__doc__)
782 except YapError, e:
783 print >> sys.stderr, e
784 sys.exit(1)
785 except AttributeError:
786 if debug:
787 raise
788 self.cmd_usage()
789 sys.exit(2)