Remove expensive calls to update-index
[yap.git] / yap / yap.py
blob30121e4e27801257ed3fdab7972b1c4e5da65cf4
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 os.system("git checkout-index -f '%s'" % file)
155 def _parse_commit(self, commit):
156 lines = get_output("git cat-file commit '%s'" % commit)
157 commit = {}
159 mode = None
160 for l in lines:
161 if mode != 'commit' and l.strip() == "":
162 mode = 'commit'
163 commit['log'] = []
164 continue
165 if mode == 'commit':
166 commit['log'].append(l)
167 continue
169 x = l.split(' ')
170 k = x[0]
171 v = ' '.join(x[1:])
172 commit[k] = v
173 commit['log'] = '\n'.join(commit['log'])
174 return commit
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():
185 self._stage_one(f)
187 def _do_uncommit(self):
188 commit = self._parse_commit("HEAD")
189 repo = get_output('git rev-parse --git-dir')[0]
190 dir = os.path.join(repo, 'yap')
191 try:
192 os.mkdir(dir)
193 except OSError:
194 pass
195 msg_file = os.path.join(dir, 'msg')
196 fd = file(msg_file, 'w')
197 print >>fd, commit['log']
198 fd.close()
200 tree = get_output("git rev-parse HEAD^")
201 os.system("git update-ref -m uncommit HEAD '%s'" % tree[0])
203 def _do_commit(self):
204 tree = get_output("git write-tree")[0]
205 parent = get_output("git rev-parse HEAD 2> /dev/null")[0]
207 if os.environ.has_key('YAP_EDITOR'):
208 editor = os.environ['YAP_EDITOR']
209 elif os.environ.has_key('GIT_EDITOR'):
210 editor = os.environ['GIT_EDITOR']
211 elif os.environ.has_key('EDITOR'):
212 editor = os.environ['EDITOR']
213 else:
214 editor = "vi"
216 fd, tmpfile = tempfile.mkstemp("yap")
217 os.close(fd)
219 repo = get_output('git rev-parse --git-dir')[0]
220 msg_file = os.path.join(repo, 'yap', 'msg')
221 if os.access(msg_file, os.R_OK):
222 fd1 = file(msg_file)
223 fd2 = file(tmpfile, 'w')
224 for l in fd1.xreadlines():
225 print >>fd2, l.strip()
226 fd2.close()
227 os.unlink(msg_file)
229 if os.system("%s '%s'" % (editor, tmpfile)) != 0:
230 raise YapError("Editing commit message failed")
231 if parent != 'HEAD':
232 commit = get_output("git commit-tree '%s' -p '%s' < '%s'" % (tree, parent, tmpfile))
233 else:
234 commit = get_output("git commit-tree '%s' < '%s'" % (tree, tmpfile))
235 if not commit:
236 raise YapError("Commit failed; no log message?")
237 os.unlink(tmpfile)
238 os.system("git update-ref HEAD '%s'" % commit[0])
240 def _check_rebasing(self):
241 repo = get_output('git rev-parse --git-dir')[0]
242 dotest = os.path.join(repo, '.dotest')
243 if os.access(dotest, os.R_OK):
244 raise YapError("A git operation is in progress. Complete it first")
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")
249 def _list_remotes(self):
250 remotes = get_output("git config --get-regexp 'remote.*.url'")
251 for x in remotes:
252 remote, url = x.split(' ')
253 remote = remote.replace('remote.', '')
254 remote = remote.replace('.url', '')
255 yield remote, url
257 @short_help("make a local copy of an existing repository")
258 @long_help("""
259 The first argument is a URL to the existing repository. This can be an
260 absolute path if the repository is local, or a URL with the git://,
261 ssh://, or http:// schemes. By default, the directory used is the last
262 component of the URL, sans '.git'. This can be overridden by providing
263 a second argument.
264 """)
265 def cmd_clone(self, url, directory=""):
266 "<url> [directory]"
267 # XXX: implement in terms of init + remote add + fetch
268 os.system("git clone '%s' %s" % (url, directory))
270 @short_help("turn a directory into a repository")
271 @long_help("""
272 Converts the current working directory into a repository. The primary
273 side-effect of this command is the creation of a '.git' subdirectory.
274 No files are added nor commits made.
275 """)
276 def cmd_init(self):
277 os.system("git init")
279 @short_help("add a new file to the repository")
280 @long_help("""
281 The arguments are the files to be added to the repository. Once added,
282 the files will show as "unstaged changes" in the output of 'status'. To
283 reverse the effects of this command, see 'rm'.
284 """)
285 def cmd_add(self, *files):
286 "<file>..."
287 if not files:
288 raise TypeError
290 for f in files:
291 self._add_one(f)
292 self.cmd_status()
294 @short_help("delete a file from the repository")
295 @long_help("""
296 The arguments are the files to be removed from the current revision of
297 the repository. The files will still exist in any past commits that the
298 file may have been a part of. The file is not actually deleted, it is
299 just no longer tracked as part of the repository.
300 """)
301 def cmd_rm(self, *files):
302 "<file>..."
303 if not files:
304 raise TypeError
306 for f in files:
307 self._rm_one(f)
308 self.cmd_status()
310 @short_help("stage changes in a file for commit")
311 @long_help("""
312 The arguments are the files to be staged. Staging changes is a way to
313 build up a commit when you do not want to commit all changes at once.
314 To commit only staged changes, use the '-d' flag to 'commit.' To
315 reverse the effects of this command, see 'unstage'. Once staged, the
316 files will show as "staged changes" in the output of 'status'.
317 """)
318 def cmd_stage(self, *files):
319 "<file>..."
320 if not files:
321 raise TypeError
323 for f in files:
324 self._stage_one(f)
325 self.cmd_status()
327 @short_help("unstage changes in a file")
328 @long_help("""
329 The arguments are the files to be unstaged. Once unstaged, the files
330 will show as "unstaged changes" in the output of 'status'.
331 """)
332 def cmd_unstage(self, *files):
333 "<file>..."
334 if not files:
335 raise TypeError
337 for f in files:
338 self._unstage_one(f)
339 self.cmd_status()
341 @short_help("show files with staged and unstaged changes")
342 @long_help("""
343 Show the files in the repository with changes since the last commit,
344 categorized based on whether the changes are staged or not. A file may
345 appear under each heading if the same file has both staged and unstaged
346 changes.
347 """)
348 def cmd_status(self):
349 branch = get_output("git symbolic-ref HEAD")[0]
350 branch = branch.replace('refs/heads/', '')
351 print "Current branch: %s" % branch
353 print "Files with staged changes:"
354 files = self._get_staged_files()
355 for f in files:
356 print "\t%s" % f
357 if not files:
358 print "\t(none)"
360 print "Files with unstaged changes:"
361 prefix = get_output("git rev-parse --show-prefix")
362 files = self._get_unstaged_files()
363 for f in files:
364 if prefix:
365 f = os.path.join(prefix[0], f)
366 print "\t%s" % f
367 if not files:
368 print "\t(none)"
370 @short_help("remove uncommitted changes from a file (*)")
371 @long_help("""
372 The arguments are the files whose changes will be reverted. If the '-a'
373 flag is given, then all files will have uncommitted changes removed.
374 Note that there is no way to reverse this command short of manually
375 editing each file again.
376 """)
377 @takes_options("a")
378 def cmd_revert(self, *files, **flags):
379 "(-a | <file>)"
380 if '-a' in flags:
381 os.system("git checkout-index -f -a")
382 return
384 if not files:
385 raise TypeError
387 for f in files:
388 self._revert_one(f)
389 self.cmd_status()
391 @short_help("record changes to files as a new commit")
392 @long_help("""
393 Create a new commit recording changes since the last commit. If there
394 are only unstaged changes, those will be recorded. If there are only
395 staged changes, those will be recorder. Otherwise, you will have to
396 specify either the '-a' flag or the '-d' flag to commit all changes or
397 only staged changes, respectively. To reverse the effects of this
398 command, see 'uncommit'.
399 """)
400 @takes_options("ad")
401 def cmd_commit(self, **flags):
402 self._check_rebasing()
403 self._check_commit(**flags)
404 if not self._get_staged_files():
405 raise YapError("No changes to commit")
406 self._do_commit()
407 self.cmd_status()
409 @short_help("reverse the actions of the last commit")
410 @long_help("""
411 Reverse the effects of the last 'commit' operation. The changes that
412 were part of the previous commit will show as "staged changes" in the
413 output of 'status'. This means that if no files were changed since the
414 last commit was created, 'uncommit' followed by 'commit' is a lossless
415 operation.
416 """)
417 def cmd_uncommit(self):
418 self._do_uncommit()
419 self.cmd_status()
421 @short_help("report the current version of yap")
422 def cmd_version(self):
423 print "Yap version 0.1"
425 @short_help("show the changelog for particular versions or files")
426 @long_help("""
427 The arguments are the files with which to filter history. If none are
428 given, all changes are listed. Otherwise only commits that affected one
429 or more of the given files are listed. The -r option changes the
430 starting revision for traversing history. By default, history is listed
431 starting at HEAD.
432 """)
433 @takes_options("r:")
434 def cmd_log(self, *paths, **flags):
435 "[-r <rev>] <path>..."
436 rev = flags.get('-r', 'HEAD')
437 paths = ' '.join(paths)
438 os.system("git log --name-status '%s' -- %s" % (rev, paths))
440 @short_help("show staged, unstaged, or all uncommitted changes")
441 @long_help("""
442 Show staged, unstaged, or all uncommitted changes. By default, all
443 changes are shown. The '-u' flag causes only unstaged changes to be
444 shown. The '-d' flag causes only staged changes to be shown.
445 """)
446 @takes_options("ud")
447 def cmd_diff(self, **flags):
448 "[ -u | -d ]"
449 if '-u' in flags and '-d' in flags:
450 raise YapError("Conflicting flags: -u and -d")
452 pager = self._get_pager_cmd()
454 if '-u' in flags:
455 os.system("git diff-files -p | %s" % pager)
456 elif '-d' in flags:
457 os.system("git diff-index --cached -p HEAD | %s" % pager)
458 else:
459 os.system("git diff-index -p HEAD | %s" % pager)
461 @short_help("list, create, or delete branches")
462 @long_help("""
463 If no arguments are given, a list of local branches is given. The
464 current branch is indicated by a "*" next to the name. If an argument
465 is given, it is taken as the name of a new branch to create. The branch
466 will start pointing at the current HEAD. See 'point' for details on
467 changing the revision of the new branch. Note that this command does
468 not switch the current working branch. See 'switch' for details on
469 changing the current working branch.
471 The '-d' flag can be used to delete local branches. If the delete
472 operation would remove the last branch reference to a given line of
473 history (colloquially referred to as "dangling commits"), yap will
474 report an error and abort. The '-f' flag can be used to force the delete
475 in spite of this.
476 """)
477 @takes_options("fd:")
478 def cmd_branch(self, branch=None, **flags):
479 "[ [-f] -d <branch> | <branch> ]"
480 force = '-f' in flags
481 if '-d' in flags:
482 self._delete_branch(flags['-d'], force)
483 self.cmd_branch()
484 return
486 if branch is not None:
487 ref = get_output("git rev-parse HEAD")
488 if not ref:
489 raise YapError("No branch point yet. Make a commit")
490 os.system("git update-ref 'refs/heads/%s' '%s'" % (branch, ref[0]))
492 current = get_output("git symbolic-ref HEAD")[0]
493 branches = get_output("git for-each-ref --format='%(refname)' 'refs/heads/*'")
494 for b in branches:
495 if b == current:
496 print "* ",
497 else:
498 print " ",
499 b = b.replace('refs/heads/', '')
500 print b
502 @short_help("change the current working branch")
503 @long_help("""
504 The argument is the name of the branch to make the current working
505 branch. This command will fail if there are uncommitted changes to any
506 files. Otherwise, the contents of the files in the working directory
507 are updated to reflect their state in the new branch. Additionally, any
508 future commits are added to the new branch instead of the previous line
509 of history.
510 """)
511 def cmd_switch(self, branch):
512 "<branch>"
513 ref = get_output("git rev-parse 'refs/heads/%s'" % branch)
514 if not ref:
515 raise YapError("No such branch: %s" % branch)
517 # XXX: support merging like git-checkout
518 if self._get_unstaged_files() or self._get_staged_files():
519 raise YapError("You have uncommitted changes. Commit them first")
521 os.system("git symbolic-ref HEAD refs/heads/'%s'" % branch)
522 os.system("git read-tree HEAD")
523 os.system("git checkout-index -f -a")
524 self.cmd_branch()
526 @short_help("move the current branch to a different revision")
527 @long_help("""
528 The argument is the hash of the commit to which the current branch
529 should point, or alternately a branch or tag (a.k.a, "committish"). If
530 moving the branch would create "dangling commits" (see 'branch'), yap
531 will report an error and abort. The '-f' flag can be used to force the
532 operation in spite of this.
533 """)
534 @takes_options("f")
535 def cmd_point(self, where, **flags):
536 "<where>"
537 head = get_output("git rev-parse HEAD")
538 if not head:
539 raise YapError("No commit yet; nowhere to point")
541 ref = get_output("git rev-parse '%s'" % where)
542 if not ref:
543 raise YapError("Not a valid ref: %s" % where)
545 if self._get_unstaged_files() or self._get_staged_files():
546 raise YapError("You have uncommitted changes. Commit them first")
548 type = get_output("git cat-file -t '%s'" % ref[0])
549 if type and type[0] == "tag":
550 tag = get_output("git cat-file tag '%s'" % ref[0])
551 ref[0] = tag[0].split(' ')[1]
553 os.system("git update-ref HEAD '%s'" % ref[0])
555 if '-f' not in flags:
556 name = get_output("git name-rev --name-only '%s'" % head[0])[0]
557 if name == "undefined":
558 os.system("git update-ref HEAD '%s'" % head[0])
559 raise YapError("Pointing there will lose commits. Use -f to force")
561 os.system("git read-tree HEAD")
562 os.system("git checkout-index -u -f -a")
564 @short_help("alter history by dropping or amending commits")
565 @long_help("""
566 This command operates in two distinct modes, "amend" and "drop" mode.
567 In drop mode, the given commit is removed from the history of the
568 current branch, as though that commit never happened. By default the
569 commit used is HEAD.
571 In amend mode, the uncommitted changes present are merged into a
572 previous commit. This is useful for correcting typos or adding missed
573 files into past commits. By default the commit used is HEAD.
575 While rewriting history it is possible that conflicts will arise. If
576 this happens, the rewrite will pause and you will be prompted to resolve
577 the conflicts and staged them. Once that is done, you will run "yap
578 history continue." If instead you want the conflicting commit removed
579 from history (perhaps your changes supercede that commit) you can run
580 "yap history skip". Once the rewrite completes, your branch will be on
581 the same commit as when the rewrite started.
582 """)
583 def cmd_history(self, subcmd, *args):
584 "amend | drop <commit>"
586 if subcmd not in ("amend", "drop", "continue", "skip"):
587 raise TypeError
589 resolvemsg = """
590 When you have resolved the conflicts run \"yap history continue\".
591 To skip the problematic patch, run \"yap history skip\"."""
593 if subcmd == "continue":
594 os.system("git am -3 -r --resolvemsg='%s'" % resolvemsg)
595 return
596 if subcmd == "skip":
597 os.system("git reset --hard")
598 os.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg)
599 return
601 if subcmd == "amend":
602 flags, args = getopt.getopt(args, "ad")
603 flags = dict(flags)
605 if len(args) > 1:
606 raise TypeError
607 if args:
608 commit = args[0]
609 else:
610 commit = "HEAD"
612 if run_command("git rev-parse --verify '%s'" % commit):
613 raise YapError("Not a valid commit: %s" % commit)
615 self._check_rebasing()
617 if subcmd == "amend":
618 self._check_commit(**flags)
620 stash = get_output("git stash create")
621 run_command("git reset --hard")
623 fd, tmpfile = tempfile.mkstemp("yap")
624 os.close(fd)
625 try:
626 os.system("git format-patch -k --stdout '%s' > %s" % (commit, tmpfile))
627 if subcmd == "amend":
628 self.cmd_point(commit, **{'-f': True})
629 if stash:
630 run_command("git stash apply --index %s" % stash[0])
631 self._do_uncommit()
632 self._do_commit()
633 stash = get_output("git stash create")
634 run_command("git reset --hard")
635 else:
636 self.cmd_point("%s^" % commit, **{'-f': True})
638 stat = os.stat(tmpfile)
639 size = stat[6]
640 if size > 0:
641 rc = os.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg, tmpfile))
642 if (rc):
643 raise YapError("Failed to apply changes")
645 if stash:
646 run_command("git stash apply %s" % stash[0])
647 finally:
648 os.unlink(tmpfile)
649 self.cmd_status()
651 @short_help("show the changes introduced by a given commit")
652 @long_help("""
653 By default, the changes in the last commit are shown. To override this,
654 specify a hash, branch, or tag (committish). The hash of the commit,
655 the commit's author, log message, and a diff of the changes are shown.
656 """)
657 def cmd_show(self, commit="HEAD"):
658 "[commit]"
659 os.system("git show '%s'" % commit)
661 @short_help("apply the changes in a given commit to the current branch")
662 @long_help("""
663 The argument is the hash, branch, or tag (committish) of the commit to
664 be applied. In general, it only makes sense to apply commits that
665 happened on another branch. The '-r' flag can be used to have the
666 changes in the given commit reversed from the current branch. In
667 general, this only makes sense for commits that happened on the current
668 branch.
669 """)
670 @takes_options("r")
671 def cmd_cherry_pick(self, commit, **flags):
672 "[-r] <commit>"
673 if '-r' in flags:
674 os.system("git revert '%s'" % commit)
675 else:
676 os.system("git cherry-pick '%s'" % commit)
678 @short_help("list, add, or delete configured remote repositories")
679 @long_help("""
680 When invoked with no arguments, this command will show the list of
681 currently configured remote repositories, giving both the name and URL
682 of each. To add a new repository, give the desired name as the first
683 argument and the URL as the second. The '-d' flag can be used to remove
684 a previously added repository.
685 """)
686 @takes_options("d:")
687 def cmd_repo(self, name=None, url=None, **flags):
688 "[<name> <url> | -d <name>]"
689 if name is not None and url is None:
690 raise TypeError
692 if '-d' in flags:
693 if flags['-d'] not in self._list_remotes():
694 raise YapError("No such repository: %s" % flags['-d'])
695 os.system("git config --unset remote.%s.url" % flags['-d'])
696 os.system("git config --unset remote.%s.fetch" % flags['-d'])
698 if name:
699 if flags['-d'] in self._list_remotes():
700 raise YapError("Repository '%s' already exists" % flags['-d'])
701 os.system("git config remote.%s.url %s" % (name, url))
702 os.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name, url))
704 for remote, url in self._list_remotes():
705 print "%s:\t\t%s" % (remote, url)
707 def cmd_help(self, cmd=None):
708 if cmd is not None:
709 try:
710 attr = self.__getattribute__("cmd_"+cmd)
711 except AttributeError:
712 raise YapError("No such command: %s" % cmd)
713 try:
714 help = attr.long_help
715 except AttributeError:
716 raise YapError("Sorry, no help for '%s'. Ask Steven." % cmd)
718 print >>sys.stderr, "The '%s' command" % cmd
719 print >>sys.stderr, "\tyap %s %s" % (cmd, attr.__doc__)
720 print >>sys.stderr, "%s" % help
721 return
723 print >> sys.stderr, "Yet Another (Git) Porcelein"
724 print >> sys.stderr
726 for name in dir(self):
727 if not name.startswith('cmd_'):
728 continue
729 attr = self.__getattribute__(name)
730 if not callable(attr):
731 continue
732 try:
733 short_msg = attr.short_help
734 except AttributeError:
735 continue
737 name = name.replace('cmd_', '')
738 name = name.replace('_', '-')
739 print >> sys.stderr, "%-16s%s" % (name, short_msg)
740 print >> sys.stderr
741 print >> sys.stderr, "(*) Indicates that the command is not readily reversible"
743 def cmd_usage(self):
744 print >> sys.stderr, "usage: %s <command>" % sys.argv[0]
745 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"
747 def main(self, args):
748 if len(args) < 1:
749 self.cmd_usage()
750 sys.exit(2)
752 command = args[0]
753 args = args[1:]
755 debug = os.getenv('YAP_DEBUG')
757 try:
758 command = command.replace('-', '_')
759 meth = self.__getattribute__("cmd_"+command)
760 try:
761 if "options" in meth.__dict__:
762 flags, args = getopt.getopt(args, meth.options)
763 flags = dict(flags)
764 else:
765 flags = dict()
767 meth(*args, **flags)
768 except (TypeError, getopt.GetoptError):
769 if debug:
770 raise
771 print "%s %s %s" % (sys.argv[0], command, meth.__doc__)
772 except YapError, e:
773 print >> sys.stderr, e
774 sys.exit(1)
775 except AttributeError:
776 if debug:
777 raise
778 self.cmd_usage()
779 sys.exit(2)