cmd_init: create the .git/yap directory
[yap.git] / yap / yap.py
blob71f0d4bfac17dfec377e7a08dee4bd725abde633
1 import sys
2 import os
3 import glob
4 import getopt
5 import pickle
6 import tempfile
8 from util import *
10 class ShellError(Exception):
11 def __init__(self, cmd, rc):
12 self.cmd = cmd
13 self.rc = rc
15 def __str__(self):
16 return "%s returned %d" % (self.cmd, self.rc)
18 class YapError(Exception):
19 def __init__(self, msg):
20 self.msg = msg
22 def __str__(self):
23 return 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')
29 try:
30 os.mkdir(dir)
31 except OSError:
32 pass
33 files = self._get_new_files()
34 files.append(file)
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')
41 try:
42 files = pickle.load(file(path))
43 except IOError:
44 files = []
46 x = []
47 for f in files:
48 # if f in the index
49 if get_output("git ls-files --cached '%s'" % f) != []:
50 continue
51 x.append(f)
52 return x
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')
60 try:
61 pickle.dump(files, open(path, 'w'))
62 except IOError:
63 pass
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")
71 if not prefix:
72 return path
74 prefix = [ prefix[0] ]
75 while True:
76 head, tail = os.path.split(prefix[0])
77 if not head:
78 break
79 prefix[0] = head
80 if tail:
81 prefix.insert(1, tail)
83 path = [ path ]
84 while True:
85 head, tail = os.path.split(path[0])
86 if not head:
87 break
88 path[0] = head
89 if tail:
90 path.insert(1, tail)
92 common = 0
93 for a, b in zip(prefix, path):
94 if a != b:
95 break
96 common += 1
98 path = path[common:]
99 cdup = [".."] * (len(prefix) - common)
100 path = cdup + list(path)
101 path = os.path.join(*path)
102 return path
104 def _get_staged_files(self):
105 if run_command("git rev-parse HEAD"):
106 files = get_output("git ls-files --cached")
107 else:
108 files = get_output("git diff-index --cached --name-only HEAD")
109 unmerged = self._get_unmerged_files()
110 if unmerged:
111 unmerged = set(unmerged)
112 files = set(files).difference(unmerged)
113 files = list(files)
114 return files
116 def _get_unstaged_files(self):
117 cwd = os.getcwd()
118 cdup = self._get_cdup()
119 os.chdir(cdup)
120 files = get_output("git ls-files -m")
121 os.chdir(cwd)
123 new_files = self._get_new_files()
124 if new_files:
125 staged = self._get_staged_files()
126 if staged:
127 staged = set(staged)
128 new_files = set(new_files).difference(staged)
129 new_files = list(new_files)
130 files += new_files
131 unmerged = self._get_unmerged_files()
132 if unmerged:
133 unmerged = set(unmerged)
134 files = set(files).difference(unmerged)
135 files = list(files)
136 return files
138 def _get_unmerged_files(self):
139 cwd = os.getcwd()
140 cdup = self._get_cdup()
141 os.chdir(cdup)
142 files = get_output("git ls-files -u")
143 os.chdir(cwd)
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)
149 if not ref:
150 raise YapError("No such revision: %s" % rev)
151 return ref[0]
153 def _delete_branch(self, branch, force):
154 current = get_output("git symbolic-ref HEAD")
155 if current:
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))
163 if not force:
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']
175 else:
176 return "less"
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")
194 if prefix:
195 tmp = os.path.normpath(os.path.join(prefix[0], file))
196 else:
197 tmp = 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)
202 def _get_cdup(self):
203 cdup = get_output("git rev-parse --show-cdup")
204 assert cdup
205 if cdup[0]:
206 cdup = cdup[0]
207 else:
208 cdup = '.'
209 return 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)
215 else:
216 cdup = self._get_cdup()
217 rc = run_command("git diff-index --cached -p HEAD '%s' | (cd %s; git apply -R --cached)" % (file, cdup))
218 if rc:
219 raise YapError("Failed to unstage")
221 def _revert_one(self, file):
222 self._assert_file_exists(file)
223 try:
224 self._unstage_one(file)
225 except YapError:
226 pass
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)
231 commit = {}
233 mode = None
234 for l in lines:
235 if mode != 'commit' and l.strip() == "":
236 mode = 'commit'
237 commit['log'] = []
238 continue
239 if mode == 'commit':
240 commit['log'].append(l)
241 continue
243 x = l.split(' ')
244 k = x[0]
245 v = ' '.join(x[1:])
246 commit[k] = v
247 commit['log'] = '\n'.join(commit['log'])
248 return commit
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():
260 self._stage_one(f)
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')
266 try:
267 os.mkdir(dir)
268 except OSError:
269 pass
270 msg_file = os.path.join(dir, 'msg')
271 fd = file(msg_file, 'w')
272 print >>fd, commit['log']
273 fd.close()
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')
283 try:
284 parent = pickle.load(file(head_file))
285 except IOError:
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']
294 else:
295 editor = "vi"
297 fd, tmpfile = tempfile.mkstemp("yap")
298 os.close(fd)
300 if msg is None:
301 msg_file = os.path.join(repo, 'yap', 'msg')
302 if os.access(msg_file, os.R_OK):
303 fd1 = file(msg_file)
304 fd2 = file(tmpfile, 'w')
305 for l in fd1.xreadlines():
306 print >>fd2, l.strip()
307 fd2.close()
308 os.unlink(msg_file)
309 if os.system("%s '%s'" % (editor, tmpfile)) != 0:
310 raise YapError("Editing commit message failed")
311 fd = file(tmpfile)
312 msg = fd.readlines()
313 msg = ''.join(msg)
315 msg = msg.strip()
316 if not msg:
317 raise YapError("Refusing to use empty commit message")
319 fd = os.popen("git stripspace > %s" % tmpfile, 'w')
320 print >>fd, msg,
321 fd.close()
323 if parent:
324 parent = ' -p '.join(parent)
325 commit = get_output("git commit-tree '%s' -p %s < '%s'" % (tree, parent, tmpfile))
326 else:
327 commit = get_output("git commit-tree '%s' < '%s'" % (tree, tmpfile))
329 os.unlink(tmpfile)
330 run_safely("git update-ref HEAD '%s'" % commit[0])
331 self._clear_state()
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'")
348 for x in remotes:
349 remote, url = x.split(' ')
350 remote = remote.replace('remote.', '')
351 remote = remote.replace('.url', '')
352 yield remote, url
354 def _unstage_all(self):
355 try:
356 run_safely("git read-tree -m HEAD")
357 except ShellError:
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)
363 if not remote:
364 raise YapError("No tracking branch configured for '%s'" % current)
366 merge = get_output("git config branch.%s.merge" % current)
367 if not merge:
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)? ",
374 sys.stdout.flush()
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":
384 try:
385 os.unlink(os.path.join(dir, f))
386 except OSError:
387 pass
389 def _get_attr(self, name, attr):
390 val = None
391 for c in self.__class__.__bases__:
392 try:
393 m2 = c.__dict__[name]
394 except KeyError:
395 continue
396 try:
397 val = m2.__getattribute__(attr)
398 except AttributeError:
399 continue
400 return val
402 def _filter_log(self, commit):
403 return commit
405 def _check_rename(self, rev, path):
406 renames = get_output("git diff-tree -C -M --diff-filter=R %s %s^"
407 % (rev, rev))
408 for r in renames:
409 r = r.replace('\t', ' ')
410 fields = r.split(' ')
411 mode1, mode2, hash1, hash2, rename, dst, src = fields
412 if dst == path:
413 return src
414 return None
416 def _expand_directories(self, files):
417 files = list(files)
418 for i, f in enumerate(files[:]):
419 if not os.path.isdir(f):
420 continue
422 del files[i]
423 fd = os.popen("find %s -type f" % f)
424 for x in fd.xreadlines():
425 x = x.strip()
426 if '.git' in x.split(os.path.sep):
427 continue
428 files.append(x)
429 return files
432 @short_help("make a local copy of an existing repository")
433 @long_help("""
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
438 a second argument.
439 """)
440 def cmd_clone(self, url, directory=None):
441 "<url> [directory]"
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', '')
451 try:
452 os.mkdir(directory)
453 except OSError:
454 raise YapError("Directory exists: %s" % directory)
455 os.chdir(directory)
456 self.cmd_init()
457 self.cmd_repo("origin", url)
458 self.cmd_fetch("origin")
460 branch = None
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:
465 branch = b
466 break
467 if branch is None:
468 if not run_command("git rev-parse --verify refs/remotes/origin/master"):
469 branch = "refs/remotes/origin/master"
470 if branch is None:
471 branch = get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin'")
472 branch = branch[0]
474 hash = get_output("git rev-parse %s" % branch)
475 assert hash
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")
482 @long_help("""
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.
486 """)
487 def cmd_init(self):
488 os.system("git init")
489 os.mkdir(os.path.join(".git", "yap"))
491 @short_help("add a new file to the repository")
492 @long_help("""
493 The arguments are the files to be added to the repository. Once added,
494 the files will show as "unstaged changes" in the output of 'status'. To
495 reverse the effects of this command, see 'rm'.
496 """)
497 def cmd_add(self, *files):
498 "<file>..."
499 self._check_git()
501 if not files:
502 raise TypeError
504 files = self._expand_directories(files)
505 for f in files:
506 self._add_one(f)
507 self.cmd_status()
509 @short_help("delete a file from the repository")
510 @long_help("""
511 The arguments are the files to be removed from the current revision of
512 the repository. The files will still exist in any past commits that the
513 files may have been a part of. The file is not actually deleted, it is
514 just no longer tracked as part of the repository.
515 """)
516 def cmd_rm(self, *files):
517 "<file>..."
518 self._check_git()
519 if not files:
520 raise TypeError
522 for f in files:
523 self._rm_one(f)
524 self.cmd_status()
526 @short_help("stage changes in a file for commit")
527 @long_help("""
528 The arguments are the files to be staged. Staging changes is a way to
529 build up a commit when you do not want to commit all changes at once.
530 To commit only staged changes, use the '-d' flag to 'commit.' To
531 reverse the effects of this command, see 'unstage'. Once staged, the
532 files will show as "staged changes" in the output of 'status'.
533 """)
534 def cmd_stage(self, *files):
535 "<file>..."
536 self._check_git()
537 if not files:
538 raise TypeError
540 files = self._expand_directories(files)
541 for f in files:
542 self._stage_one(f)
543 self.cmd_status()
545 @short_help("unstage changes in a file")
546 @long_help("""
547 The arguments are the files to be unstaged. Once unstaged, the files
548 will show as "unstaged changes" in the output of 'status'. The '-a'
549 flag can be used to unstage all staged changes at once.
550 """)
551 @takes_options("a")
552 def cmd_unstage(self, *files, **flags):
553 "[-a] | <file>..."
554 self._check_git()
555 if '-a' in flags:
556 files = self._get_staged_files()
558 if not files:
559 raise YapError("Nothing to do")
561 files = [ self._repo_path_to_rel(x) for x in files ]
562 for f in files:
563 self._unstage_one(f)
564 self.cmd_status()
566 @short_help("show files with staged and unstaged changes")
567 @long_help("""
568 Show the files in the repository with changes since the last commit,
569 categorized based on whether the changes are staged or not. A file may
570 appear under each heading if the same file has both staged and unstaged
571 changes.
572 """)
573 def cmd_status(self):
575 self._check_git()
576 branch = get_output("git symbolic-ref HEAD")
577 if branch:
578 branch = branch[0].replace('refs/heads/', '')
579 else:
580 branch = "DETACHED"
581 print "Current branch: %s" % branch
583 print "Files with staged changes:"
584 files = self._get_staged_files()
585 for f in files:
586 print "\t%s" % self._repo_path_to_rel(f)
587 if not files:
588 print "\t(none)"
590 print "Files with unstaged changes:"
591 files = self._get_unstaged_files()
592 for f in files:
593 print "\t%s" % self._repo_path_to_rel(f)
594 if not files:
595 print "\t(none)"
597 files = self._get_unmerged_files()
598 if files:
599 print "Files with conflicts:"
600 for f in files:
601 print "\t%s" % self._repo_path_to_rel(f)
603 @short_help("remove uncommitted changes from a file (*)")
604 @long_help("""
605 The arguments are the files whose changes will be reverted. If the '-a'
606 flag is given, then all files will have uncommitted changes removed.
607 Note that there is no way to reverse this command short of manually
608 editing each file again.
609 """)
610 @takes_options("a")
611 def cmd_revert(self, *files, **flags):
612 "(-a | <file>)"
613 self._check_git()
614 if '-a' in flags:
615 cdup = self._get_cdup()
616 run_command("(cd %s; git add -u)" % cdup)
617 os.system("git read-tree -v --aggressive -u -m HEAD")
618 self._clear_state()
619 self.cmd_status()
620 return
622 if not files:
623 raise TypeError
625 for f in files:
626 self._revert_one(f)
627 self.cmd_status()
629 @short_help("record changes to files as a new commit")
630 @long_help("""
631 Create a new commit recording changes since the last commit. If there
632 are only unstaged changes, those will be recorded. If there are only
633 staged changes, those will be recorded. Otherwise, you will have to
634 specify either the '-a' flag or the '-d' flag to commit all changes or
635 only staged changes, respectively. To reverse the effects of this
636 command, see 'uncommit'.
637 """)
638 @takes_options("adm:")
639 def cmd_commit(self, **flags):
640 "[-a | -d] [-m <msg>]"
641 self._check_git()
642 self._check_rebasing()
643 self._check_commit(**flags)
644 if not self._get_staged_files():
645 raise YapError("No changes to commit")
646 msg = flags.get('-m', None)
647 self._do_commit(msg)
648 self.cmd_status()
650 @short_help("reverse the actions of the last commit")
651 @long_help("""
652 Reverse the effects of the last 'commit' operation. The changes that
653 were part of the previous commit will show as "staged changes" in the
654 output of 'status'. This means that if no files were changed since the
655 last commit was created, 'uncommit' followed by 'commit' is a lossless
656 operation.
657 """)
658 def cmd_uncommit(self):
660 self._check_git()
661 self._do_uncommit()
662 self.cmd_status()
664 @short_help("report the current version of yap")
665 def cmd_version(self):
666 print "Yap version %s" % self.version
668 @short_help("show the changelog for particular versions or files")
669 @long_help("""
670 The arguments are the files with which to filter history. If none are
671 given, all changes are listed. Otherwise only commits that affected one
672 or more of the given files are listed. The -r option changes the
673 starting revision for traversing history. By default, history is listed
674 starting at HEAD.
675 """)
676 @takes_options("pr:")
677 def cmd_log(self, *paths, **flags):
678 "[-p] [-r <rev>] <path>..."
679 self._check_git()
680 rev = flags.get('-r', 'HEAD')
681 rev = self._resolve_rev(rev)
682 paths = list(paths)
684 if '-p' in flags:
685 flags['-p'] = '-p'
687 try:
688 pager = os.popen(self._get_pager_cmd(), 'w')
689 rename = False
690 while True:
691 for hash in yield_output("git rev-list '%s' -- %s"
692 % (rev, ' '.join(paths))):
693 commit = get_output("git show -M -C %s %s"
694 % (flags.get('-p', '--name-status'), hash),
695 strip=False)
696 commit = self._filter_log(commit)
697 print >>pager, ''.join(commit)
699 # Check for renames
700 if len(paths) == 1:
701 src = self._check_rename(hash, paths[0])
702 if src is not None:
703 paths[0] = src
704 rename = True
705 rev = hash+"^"
706 break
707 if not rename:
708 break
709 rename = False
710 except (IOError, KeyboardInterrupt):
711 pass
713 @short_help("show staged, unstaged, or all uncommitted changes")
714 @long_help("""
715 Show staged, unstaged, or all uncommitted changes. By default, all
716 changes are shown. The '-u' flag causes only unstaged changes to be
717 shown. The '-d' flag causes only staged changes to be shown.
718 """)
719 @takes_options("ud")
720 def cmd_diff(self, **flags):
721 "[ -u | -d ]"
722 self._check_git()
723 if '-u' in flags and '-d' in flags:
724 raise YapError("Conflicting flags: -u and -d")
726 pager = self._get_pager_cmd()
728 if '-u' in flags:
729 os.system("git diff-files -p | %s" % pager)
730 elif '-d' in flags:
731 os.system("git diff-index --cached -p HEAD | %s" % pager)
732 else:
733 os.system("git diff-index -p HEAD | %s" % pager)
735 @short_help("list, create, or delete branches")
736 @long_help("""
737 If no arguments are specified, a list of local branches is given. The
738 current branch is indicated by a "*" next to the name. If an argument
739 is given, it is taken as the name of a new branch to create. The branch
740 will start pointing at the current HEAD. See 'point' for details on
741 changing the revision of the new branch. Note that this command does
742 not switch the current working branch. See 'switch' for details on
743 changing the current working branch.
745 The '-d' flag can be used to delete local branches. If the delete
746 operation would remove the last branch reference to a given line of
747 history (colloquially referred to as "dangling commits"), yap will
748 report an error and abort. The '-f' flag can be used to force the delete
749 in spite of this.
750 """)
751 @takes_options("fd:")
752 def cmd_branch(self, branch=None, **flags):
753 "[ [-f] -d <branch> | <branch> ]"
754 self._check_git()
755 force = '-f' in flags
756 if '-d' in flags:
757 self._delete_branch(flags['-d'], force)
758 self.cmd_branch()
759 return
761 if branch is not None:
762 ref = get_output("git rev-parse --verify HEAD")
763 if not ref:
764 raise YapError("No branch point yet. Make a commit")
765 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch, ref[0]))
767 current = get_output("git symbolic-ref HEAD")
768 branches = get_output("git for-each-ref --format='%(refname)' 'refs/heads'")
769 for b in branches:
770 if current and b == current[0]:
771 print "* ",
772 else:
773 print " ",
774 b = b.replace('refs/heads/', '')
775 print b
777 @short_help("change the current working branch")
778 @long_help("""
779 The argument is the name of the branch to make the current working
780 branch. This command will fail if there are uncommitted changes to any
781 files. Otherwise, the contents of the files in the working directory
782 are updated to reflect their state in the new branch. Additionally, any
783 future commits are added to the new branch instead of the previous line
784 of history.
785 """)
786 @takes_options("f")
787 def cmd_switch(self, branch, **flags):
788 "[-f] <branch>"
789 self._check_git()
790 self._check_rebasing()
791 ref = self._resolve_rev('refs/heads/'+branch)
793 if '-f' not in flags:
794 if (self._get_staged_files()
795 or (self._get_unstaged_files()
796 and run_command("git update-index --refresh"))):
797 raise YapError("You have uncommitted changes. Use -f to continue anyway")
799 if self._get_unstaged_files() and self._get_staged_files():
800 raise YapError("You have staged and unstaged changes. Perhaps unstage -a?")
802 staged = bool(self._get_staged_files())
804 cdup = self._get_cdup()
805 run_command("(cd %s; git add -u)" % cdup)
806 for f in self._get_new_files():
807 self._stage_one(f)
809 idx = get_output("git write-tree")
810 new = self._resolve_rev('refs/heads/'+branch)
812 run_command("git update-index --refresh")
813 readtree = "git read-tree -v --aggressive -u -m HEAD %s %s" % (idx[0], new)
814 if os.system(readtree):
815 raise YapError("Failed to switch")
816 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch)
818 if '-f' not in flags:
819 self._clear_state()
821 if not staged:
822 self._unstage_all()
823 self.cmd_status()
825 @short_help("move the current branch to a different revision")
826 @long_help("""
827 The argument is the hash of the commit to which the current branch
828 should point, or alternately a branch or tag (a.k.a, "committish"). If
829 moving the branch would create "dangling commits" (see 'branch'), yap
830 will report an error and abort. The '-f' flag can be used to force the
831 operation in spite of this.
832 """)
833 @takes_options("f")
834 def cmd_point(self, where, **flags):
835 "[-f] <where>"
836 self._check_git()
837 self._check_rebasing()
839 head = get_output("git rev-parse --verify HEAD")
840 if not head:
841 raise YapError("No commit yet; nowhere to point")
843 ref = self._resolve_rev(where)
844 ref = get_output("git rev-parse --verify '%s^{commit}'" % ref)
845 if not ref:
846 raise YapError("Not a commit: %s" % where)
848 if self._get_unstaged_files() or self._get_staged_files():
849 raise YapError("You have uncommitted changes. Commit them first")
851 run_safely("git update-ref HEAD '%s'" % ref[0])
853 if '-f' not in flags:
854 name = get_output("git name-rev --name-only '%s'" % head[0])[0]
855 if name == "undefined":
856 os.system("git update-ref HEAD '%s'" % head[0])
857 raise YapError("Pointing there will lose commits. Use -f to force")
859 run_command("git update-index --refresh")
860 rc = os.system("git read-tree -v --reset -u HEAD")
861 if rc:
862 raise YapError("checkout-index failed")
863 self._clear_state()
865 @short_help("alter history by dropping or amending commits")
866 @long_help("""
867 This command operates in two distinct modes, "amend" and "drop" mode.
868 In drop mode, the given commit is removed from the history of the
869 current branch, as though that commit never happened. By default the
870 commit used is HEAD.
872 In amend mode, the uncommitted changes present are merged into a
873 previous commit. This is useful for correcting typos or adding missed
874 files into past commits. By default the commit used is HEAD.
876 While rewriting history it is possible that conflicts will arise. If
877 this happens, the rewrite will pause and you will be prompted to resolve
878 the conflicts and stage them. Once that is done, you will run "yap
879 history continue." If instead you want the conflicting commit removed
880 from history (perhaps your changes supercede that commit) you can run
881 "yap history skip". Once the rewrite completes, your branch will be on
882 the same commit as when the rewrite started.
883 """)
884 def cmd_history(self, subcmd, *args):
885 "amend | drop <commit>"
886 self._check_git()
888 if subcmd not in ("amend", "drop", "continue", "skip"):
889 raise TypeError
891 resolvemsg = """
892 When you have resolved the conflicts run \"yap history continue\".
893 To skip the problematic patch, run \"yap history skip\"."""
895 if subcmd == "continue":
896 os.system("git am -3 -r --resolvemsg='%s'" % resolvemsg)
897 return
898 if subcmd == "skip":
899 os.system("git reset --hard")
900 os.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg)
901 return
903 if subcmd == "amend":
904 flags, args = getopt.getopt(args, "ad")
905 flags = dict(flags)
907 if len(args) > 1:
908 raise TypeError
909 if args:
910 commit = args[0]
911 else:
912 commit = "HEAD"
914 self._resolve_rev(commit)
915 self._check_rebasing()
917 if subcmd == "amend":
918 self._check_commit(**flags)
919 if self._get_unstaged_files():
920 # XXX: handle unstaged changes better
921 raise YapError("Commit away changes that you aren't amending")
923 self._unstage_all()
925 start = get_output("git rev-parse HEAD")
926 stash = get_output("git stash create")
927 run_command("git reset --hard")
928 try:
929 fd, tmpfile = tempfile.mkstemp("yap")
930 try:
931 try:
932 os.close(fd)
933 os.system("git format-patch -k --stdout '%s' > %s" % (commit, tmpfile))
934 if subcmd == "amend":
935 self.cmd_point(commit, **{'-f': True})
936 finally:
937 if subcmd == "amend":
938 if stash:
939 rc = os.system("git stash apply %s" % stash[0])
940 if rc:
941 self.cmd_point(start[0], **{'-f': True})
942 os.system("git stash apply %s" % stash[0])
943 raise YapError("Failed to apply stash")
944 stash = None
946 if subcmd == "amend":
947 self._do_uncommit()
948 self._check_commit(**{'-a': True})
949 self._do_commit()
950 else:
951 self.cmd_point("%s^" % commit, **{'-f': True})
953 stat = os.stat(tmpfile)
954 size = stat[6]
955 if size > 0:
956 run_safely("git update-index --refresh")
957 rc = os.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg, tmpfile))
958 if (rc):
959 raise YapError("Failed to apply changes")
960 finally:
961 os.unlink(tmpfile)
962 finally:
963 if stash:
964 run_command("git stash apply %s" % stash[0])
965 self.cmd_status()
967 @short_help("show the changes introduced by a given commit")
968 @long_help("""
969 By default, the changes in the last commit are shown. To override this,
970 specify a hash, branch, or tag (committish). The hash of the commit,
971 the commit's author, log message, and a diff of the changes are shown.
972 """)
973 def cmd_show(self, commit="HEAD"):
974 "[commit]"
975 self._check_git()
976 commit = self._resolve_rev(commit)
977 os.system("git show '%s'" % commit)
979 @short_help("apply the changes in a given commit to the current branch")
980 @long_help("""
981 The argument is the hash, branch, or tag (committish) of the commit to
982 be applied. In general, it only makes sense to apply commits that
983 happened on another branch. The '-r' flag can be used to have the
984 changes in the given commit reversed from the current branch. In
985 general, this only makes sense for commits that happened on the current
986 branch.
987 """)
988 @takes_options("r")
989 def cmd_cherry_pick(self, commit, **flags):
990 "[-r] <commit>"
991 self._check_git()
992 commit = self._resolve_rev(commit)
993 if '-r' in flags:
994 os.system("git revert '%s'" % commit)
995 else:
996 os.system("git cherry-pick '%s'" % commit)
998 @short_help("list, add, or delete configured remote repositories")
999 @long_help("""
1000 When invoked with no arguments, this command will show the list of
1001 currently configured remote repositories, giving both the name and URL
1002 of each. To add a new repository, give the desired name as the first
1003 argument and the URL as the second. The '-d' flag can be used to remove
1004 a previously added repository.
1005 """)
1006 @takes_options("d:")
1007 def cmd_repo(self, name=None, url=None, **flags):
1008 "[<name> <url> | -d <name>]"
1009 self._check_git()
1010 if name is not None and url is None:
1011 raise TypeError
1013 if '-d' in flags:
1014 if flags['-d'] not in [ x[0] for x in self._list_remotes() ]:
1015 raise YapError("No such repository: %s" % flags['-d'])
1016 os.system("git config --unset remote.%s.url" % flags['-d'])
1017 os.system("git config --unset remote.%s.fetch" % flags['-d'])
1018 for b in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/%s'" % flags['-d']):
1019 hash = get_output("git rev-parse %s" % b)
1020 assert hash
1021 run_safely("git update-ref -d %s %s" % (b, hash[0]))
1023 if name:
1024 if name in [ x[0] for x in self._list_remotes() ]:
1025 raise YapError("Repository '%s' already exists" % name)
1026 os.system("git config remote.%s.url %s" % (name, url))
1027 os.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name, name))
1029 for remote, url in self._list_remotes():
1030 print "%s" % remote
1031 print " URL: %s" % url
1032 first = True
1033 for b in get_output("git for-each-ref --format='%%(refname)' 'refs/remotes/%s'" % remote):
1034 b = b.replace('refs/remotes/', '')
1035 if first:
1036 branches = "Branches: "
1037 else:
1038 branches = " "
1039 print " %s%s" % (branches, b)
1040 first = False
1042 @short_help("send local commits to a remote repository (*)")
1043 @long_help("""
1044 When invoked with no arguments, the current branch is synchronized to
1045 the tracking branch of the tracking remote. If no tracking remote is
1046 specified, the repository will have to be specified on the command line.
1047 In that case, the default is to push to a branch with the same name as
1048 the current branch. This behavior can be overridden by giving a second
1049 argument to specify the remote branch.
1051 If the remote branch does not currently exist, the command will abort
1052 unless the -c flag is provided. If the remote branch is not a direct
1053 descendent of the local branch, the command will abort unless the -f
1054 flag is provided. Forcing a push in this way can be problematic to
1055 other users of the repository if they are not expecting it.
1057 To delete a branch on the remote repository, use the -d flag.
1058 """)
1059 @takes_options("cdf")
1060 def cmd_push(self, repo=None, rhs=None, **flags):
1061 "[-c | -d] <repo>"
1062 self._check_git()
1063 if '-c' in flags and '-d' in flags:
1064 raise TypeError
1066 if repo and repo not in [ x[0] for x in self._list_remotes() ]:
1067 raise YapError("No such repository: %s" % repo)
1069 current = get_output("git symbolic-ref HEAD")
1070 if not current:
1071 raise YapError("Not on a branch!")
1073 self._check_rebasing()
1075 current = current[0].replace('refs/heads/', '')
1076 remote = get_output("git config branch.%s.remote" % current)
1077 if repo is None and remote:
1078 repo = remote[0]
1080 if repo is None:
1081 raise YapError("No tracking branch configured; specify destination repository")
1083 if rhs is None and remote and remote[0] == repo:
1084 merge = get_output("git config branch.%s.merge" % current)
1085 if merge:
1086 rhs = merge[0]
1088 if rhs is None:
1089 rhs = "refs/heads/%s" % current
1091 if '-c' not in flags and '-d' not in flags:
1092 if run_command("git rev-parse --verify refs/remotes/%s/%s"
1093 % (repo, rhs.replace('refs/heads/', ''))):
1094 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
1095 if '-f' not in flags:
1096 hash = get_output("git rev-parse refs/remotes/%s/%s" % (repo, rhs.replace('refs/heads/', '')))
1097 base = get_output("git merge-base HEAD %s" % hash[0])
1098 assert base
1099 if base[0] != hash[0]:
1100 raise YapError("Branch not up-to-date with remote. Update or use -f")
1102 self._confirm_push(current, rhs, repo)
1103 if '-f' in flags:
1104 flags['-f'] = '-f'
1106 if '-d' in flags:
1107 lhs = ""
1108 else:
1109 lhs = "refs/heads/%s" % current
1110 rc = os.system("git push %s %s %s:%s" % (flags.get('-f', ''), repo, lhs, rhs))
1111 if rc:
1112 raise YapError("Push failed.")
1114 @short_help("retrieve commits from a remote repository")
1115 @long_help("""
1116 When run with no arguments, the command will retrieve new commits from
1117 the remote tracking repository. Note that this does not in any way
1118 alter the current branch. For that, see "update". If a remote other
1119 than the tracking remote is desired, it can be specified as the first
1120 argument.
1121 """)
1122 def cmd_fetch(self, repo=None):
1123 "<repo>"
1124 self._check_git()
1125 current = get_output("git symbolic-ref HEAD")
1126 if not current:
1127 raise YapError("Not on a branch!")
1129 if repo and repo not in [ x[0] for x in self._list_remotes() ]:
1130 raise YapError("No such repository: %s" % repo)
1131 if repo is None:
1132 current = current[0].replace('refs/heads/', '')
1133 remote = get_output("git config branch.%s.remote" % current)
1134 if remote:
1135 repo = remote[0]
1136 if repo is None:
1137 raise YapError("No tracking branch configured; specify a repository")
1138 os.system("git fetch %s" % repo)
1140 @short_help("update the current branch relative to its tracking branch")
1141 @long_help("""
1142 Updates the current branch relative to its remote tracking branch. This
1143 command requires that the current branch have a remote tracking branch
1144 configured. If any conflicts occur while applying your changes to the
1145 updated remote, the command will pause to allow you to fix them. Once
1146 that is done, run "update" with the "continue" subcommand. Alternately,
1147 the "skip" subcommand can be used to discard the conflicting changes.
1148 """)
1149 def cmd_update(self, subcmd=None):
1150 "[continue | skip]"
1151 self._check_git()
1152 if subcmd and subcmd not in ["continue", "skip"]:
1153 raise TypeError
1155 resolvemsg = """
1156 When you have resolved the conflicts run \"yap update continue\".
1157 To skip the problematic patch, run \"yap update skip\"."""
1159 if subcmd == "continue":
1160 os.system("git am -3 -r --resolvemsg='%s'" % resolvemsg)
1161 return
1162 if subcmd == "skip":
1163 os.system("git reset --hard")
1164 os.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg)
1165 return
1167 self._check_rebasing()
1168 if self._get_unstaged_files() or self._get_staged_files():
1169 raise YapError("You have uncommitted changes. Commit them first")
1171 current = get_output("git symbolic-ref HEAD")
1172 if not current:
1173 raise YapError("Not on a branch!")
1175 current = current[0].replace('refs/heads/', '')
1176 remote, merge = self._get_tracking(current)
1177 merge = merge.replace('refs/heads/', '')
1179 self.cmd_fetch(remote)
1180 base = get_output("git merge-base HEAD refs/remotes/%s/%s" % (remote, merge))
1182 try:
1183 fd, tmpfile = tempfile.mkstemp("yap")
1184 os.close(fd)
1185 os.system("git format-patch -k --stdout '%s' > %s" % (base[0], tmpfile))
1186 self.cmd_point("refs/remotes/%s/%s" % (remote, merge), **{'-f': True})
1188 stat = os.stat(tmpfile)
1189 size = stat[6]
1190 if size > 0:
1191 rc = os.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg, tmpfile))
1192 if (rc):
1193 raise YapError("Failed to apply changes")
1194 finally:
1195 os.unlink(tmpfile)
1197 @short_help("query and configure remote branch tracking")
1198 @long_help("""
1199 When invoked with no arguments, the command displays the tracking
1200 information for the current branch. To configure the tracking
1201 information, two arguments for the remote repository and remote branch
1202 are given. The tracking information is used to provide defaults for
1203 where to push local changes and from where to get updates to the branch.
1204 """)
1205 def cmd_track(self, repo=None, branch=None):
1206 "[<repo> <branch>]"
1207 self._check_git()
1209 current = get_output("git symbolic-ref HEAD")
1210 if not current:
1211 raise YapError("Not on a branch!")
1212 current = current[0].replace('refs/heads/', '')
1214 if repo is None and branch is None:
1215 repo, merge = self._get_tracking(current)
1216 merge = merge.replace('refs/heads/', '')
1217 print "Branch '%s' tracking refs/remotes/%s/%s" % (current, repo, merge)
1218 return
1220 if repo is None or branch is None:
1221 raise TypeError
1223 if repo not in [ x[0] for x in self._list_remotes() ]:
1224 raise YapError("No such repository: %s" % repo)
1226 if run_command("git rev-parse --verify refs/remotes/%s/%s" % (repo, branch)):
1227 raise YapError("No such branch '%s' on repository '%s'" % (branch, repo))
1229 os.system("git config branch.%s.remote '%s'" % (current, repo))
1230 os.system("git config branch.%s.merge 'refs/heads/%s'" % (current, branch))
1231 print "Branch '%s' now tracking refs/remotes/%s/%s" % (current, repo, branch)
1233 @short_help("mark files with conflicts as resolved")
1234 @long_help("""
1235 The arguments are the files to be marked resolved. When a conflict
1236 occurs while merging changes to a file, that file is marked as
1237 "unmerged." Until the file(s) with conflicts are marked resolved,
1238 commits cannot be made.
1239 """)
1240 def cmd_resolved(self, *files):
1241 "<file>..."
1242 self._check_git()
1243 if not files:
1244 raise TypeError
1246 for f in files:
1247 self._stage_one(f, True)
1248 self.cmd_status()
1250 @short_help("merge a branch into the current branch")
1251 def cmd_merge(self, branch):
1252 "<branch>"
1253 self._check_git()
1255 branch_name = branch
1256 branch = self._resolve_rev(branch)
1257 base = get_output("git merge-base HEAD %s" % branch)
1258 if not base:
1259 raise YapError("Branch '%s' is not a fork of the current branch"
1260 % branch)
1262 readtree = ("git read-tree --aggressive -u -m %s HEAD %s"
1263 % (base[0], branch))
1264 if run_command(readtree):
1265 run_command("git update-index --refresh")
1266 if os.system(readtree):
1267 raise YapError("Failed to merge")
1269 repo = get_output('git rev-parse --git-dir')[0]
1270 dir = os.path.join(repo, 'yap')
1271 try:
1272 os.mkdir(dir)
1273 except OSError:
1274 pass
1275 msg_file = os.path.join(dir, 'msg')
1276 msg = file(msg_file, 'w')
1277 print >>msg, "Merge branch '%s'" % branch_name
1278 msg.close()
1280 head = get_output("git rev-parse --verify HEAD")
1281 assert head
1282 heads = [head[0], branch]
1283 head_file = os.path.join(dir, 'merge')
1284 pickle.dump(heads, file(head_file, 'w'))
1286 self._merge_index(branch, base[0])
1287 if self._get_unmerged_files():
1288 self.cmd_status()
1289 raise YapError("Fix conflicts then commit")
1291 self._do_commit()
1293 def _merge_index(self, branch, base):
1294 for f in self._get_unmerged_files():
1295 fd, bfile = tempfile.mkstemp("yap")
1296 os.close(fd)
1297 rc = os.system("git show %s:%s > %s" % (base, f, bfile))
1298 assert rc == 0
1300 fd, ofile = tempfile.mkstemp("yap")
1301 os.close(fd)
1302 rc = os.system("git show %s:%s > %s" % (branch, f, ofile))
1303 assert rc == 0
1305 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)
1306 rc = os.system(command)
1307 os.unlink(ofile)
1308 os.unlink(bfile)
1310 assert rc >= 0
1311 if rc == 0:
1312 self._stage_one(f, True)
1314 def cmd_help(self, cmd=None):
1315 if cmd is not None:
1316 oldcmd = cmd
1317 cmd = "cmd_" + cmd.replace('-', '_')
1318 try:
1319 attr = self.__getattribute__(cmd)
1320 except AttributeError:
1321 raise YapError("No such command: %s" % cmd)
1323 help = self._get_attr(cmd, "long_help")
1324 if help is None:
1325 raise YapError("Sorry, no help for '%s'. Ask Steven." % oldcmd)
1327 print >>sys.stderr, "The '%s' command" % oldcmd
1328 doc = self._get_attr(cmd, "__doc__")
1329 if doc is None:
1330 doc = ""
1331 print >>sys.stderr, "\tyap %s %s" % (oldcmd, doc)
1332 print >>sys.stderr, "%s" % help
1333 return
1335 print >> sys.stderr, "Yet Another (Git) Porcelein"
1336 print >> sys.stderr
1338 for name in dir(self):
1339 if not name.startswith('cmd_'):
1340 continue
1341 attr = self.__getattribute__(name)
1342 if not callable(attr):
1343 continue
1345 short_msg = self._get_attr(name, "short_help")
1346 if short_msg is None:
1347 continue
1349 name = name.replace('cmd_', '')
1350 name = name.replace('_', '-')
1351 print >> sys.stderr, "%-16s%s" % (name, short_msg)
1353 print >> sys.stderr
1354 print >> sys.stderr, "(*) Indicates that the command is not readily reversible"
1356 @short_help("show information about loaded plugins")
1357 def cmd_plugins(self):
1359 print >> sys.stderr, "Loaded plugins:"
1360 plugins = load_plugins()
1361 for name, cls in plugins.items():
1362 print "\t%-16s: %s" % (name, cls.__doc__)
1363 if not plugins:
1364 print "\t%-16s" % "None"
1366 def cmd_usage(self):
1367 print >> sys.stderr, "usage: %s <command>" % os.path.basename(sys.argv[0])
1368 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 def load_plugins():
1371 plugindir = os.path.join("~", ".yap", "plugins")
1372 plugindir = os.path.expanduser(plugindir)
1373 plugindir = os.path.join(plugindir, "*.py")
1375 plugins = dict()
1376 for p in glob.glob(os.path.expanduser(plugindir)):
1377 plugin = os.path.basename(p).replace('.py', '')
1378 m = __import__(plugin)
1379 for k in dir(m):
1380 cls = m.__dict__[k]
1381 if not type(cls) == type:
1382 continue
1383 if not issubclass(cls, YapCore):
1384 continue
1385 if cls is YapCore:
1386 continue
1387 plugins[k] = cls
1388 return plugins
1390 def yap_metaclass(name, bases, dct):
1391 plugindir = os.path.join("~", ".yap", "plugins")
1392 plugindir = os.path.expanduser(plugindir)
1393 sys.path.insert(0, plugindir)
1395 plugins = set(load_plugins().values())
1396 p2 = plugins.copy()
1397 for cls in plugins:
1398 p2 -= set(cls.__bases__)
1399 plugins = p2
1400 bases = list(plugins) + list(bases)
1401 return type(name, tuple(bases), dct)
1403 class Yap(YapCore):
1404 __metaclass__ = yap_metaclass
1406 def main(self, args):
1407 if len(args) < 1:
1408 self.cmd_usage()
1409 sys.exit(2)
1411 command = args[0]
1412 args = args[1:]
1414 if run_command("git --version"):
1415 print >>sys.stderr, "Failed to run git; is it installed?"
1416 sys.exit(1)
1418 debug = os.getenv('YAP_DEBUG')
1420 try:
1421 command = command.replace('-', '_')
1422 meth = self.__getattribute__("cmd_"+command)
1423 doc = self._get_attr("cmd_"+command, "__doc__")
1425 try:
1426 options = ""
1427 for c in self.__class__.__bases__:
1428 try:
1429 t = c.__dict__["cmd_"+command]
1430 except KeyError:
1431 continue
1432 if "options" in t.__dict__:
1433 options += t.options
1435 if options:
1436 try:
1437 flags, args = getopt.getopt(args, options)
1438 flags = dict(flags)
1439 except getopt.GetoptError, e:
1440 if debug:
1441 raise
1442 print "Usage: %s %s %s" % (os.path.basename(sys.argv[0]), command, doc)
1443 print e
1444 sys.exit(2)
1445 else:
1446 flags = dict()
1448 meth(*args, **flags)
1449 except (TypeError, getopt.GetoptError):
1450 if debug:
1451 raise
1452 print "Usage: %s %s %s" % (os.path.basename(sys.argv[0]), command, doc)
1453 except YapError, e:
1454 if debug:
1455 raise
1456 print >> sys.stderr, e
1457 sys.exit(1)
1458 except AttributeError:
1459 if debug:
1460 raise
1461 self.cmd_usage()
1462 sys.exit(2)