Revert "cmd_unstage: work correctly from a subdirectory"
[yap.git] / yap / yap.py
blob375c0c96ba66730e4625d4e392f2ec5589af5be4
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 for f in files:
562 self._unstage_one(f)
563 self.cmd_status()
565 @short_help("show files with staged and unstaged changes")
566 @long_help("""
567 Show the files in the repository with changes since the last commit,
568 categorized based on whether the changes are staged or not. A file may
569 appear under each heading if the same file has both staged and unstaged
570 changes.
571 """)
572 def cmd_status(self):
574 self._check_git()
575 branch = get_output("git symbolic-ref HEAD")
576 if branch:
577 branch = branch[0].replace('refs/heads/', '')
578 else:
579 branch = "DETACHED"
580 print "Current branch: %s" % branch
582 print "Files with staged changes:"
583 files = self._get_staged_files()
584 for f in files:
585 print "\t%s" % self._repo_path_to_rel(f)
586 if not files:
587 print "\t(none)"
589 print "Files with unstaged changes:"
590 files = self._get_unstaged_files()
591 for f in files:
592 print "\t%s" % self._repo_path_to_rel(f)
593 if not files:
594 print "\t(none)"
596 files = self._get_unmerged_files()
597 if files:
598 print "Files with conflicts:"
599 for f in files:
600 print "\t%s" % self._repo_path_to_rel(f)
602 @short_help("remove uncommitted changes from a file (*)")
603 @long_help("""
604 The arguments are the files whose changes will be reverted. If the '-a'
605 flag is given, then all files will have uncommitted changes removed.
606 Note that there is no way to reverse this command short of manually
607 editing each file again.
608 """)
609 @takes_options("a")
610 def cmd_revert(self, *files, **flags):
611 "(-a | <file>)"
612 self._check_git()
613 if '-a' in flags:
614 cdup = self._get_cdup()
615 run_command("(cd %s; git add -u)" % cdup)
616 os.system("git read-tree -v --aggressive -u -m HEAD")
617 self._clear_state()
618 self.cmd_status()
619 return
621 if not files:
622 raise TypeError
624 for f in files:
625 self._revert_one(f)
626 self.cmd_status()
628 @short_help("record changes to files as a new commit")
629 @long_help("""
630 Create a new commit recording changes since the last commit. If there
631 are only unstaged changes, those will be recorded. If there are only
632 staged changes, those will be recorded. Otherwise, you will have to
633 specify either the '-a' flag or the '-d' flag to commit all changes or
634 only staged changes, respectively. To reverse the effects of this
635 command, see 'uncommit'.
636 """)
637 @takes_options("adm:")
638 def cmd_commit(self, **flags):
639 "[-a | -d] [-m <msg>]"
640 self._check_git()
641 self._check_rebasing()
642 self._check_commit(**flags)
643 if not self._get_staged_files():
644 raise YapError("No changes to commit")
645 msg = flags.get('-m', None)
646 self._do_commit(msg)
647 self.cmd_status()
649 @short_help("reverse the actions of the last commit")
650 @long_help("""
651 Reverse the effects of the last 'commit' operation. The changes that
652 were part of the previous commit will show as "staged changes" in the
653 output of 'status'. This means that if no files were changed since the
654 last commit was created, 'uncommit' followed by 'commit' is a lossless
655 operation.
656 """)
657 def cmd_uncommit(self):
659 self._check_git()
660 self._do_uncommit()
661 self.cmd_status()
663 @short_help("report the current version of yap")
664 def cmd_version(self):
665 print "Yap version %s" % self.version
667 @short_help("show the changelog for particular versions or files")
668 @long_help("""
669 The arguments are the files with which to filter history. If none are
670 given, all changes are listed. Otherwise only commits that affected one
671 or more of the given files are listed. The -r option changes the
672 starting revision for traversing history. By default, history is listed
673 starting at HEAD.
674 """)
675 @takes_options("pr:")
676 def cmd_log(self, *paths, **flags):
677 "[-p] [-r <rev>] <path>..."
678 self._check_git()
679 rev = flags.get('-r', 'HEAD')
680 rev = self._resolve_rev(rev)
681 paths = list(paths)
683 if '-p' in flags:
684 flags['-p'] = '-p'
686 try:
687 pager = os.popen(self._get_pager_cmd(), 'w')
688 rename = False
689 while True:
690 for hash in yield_output("git rev-list '%s' -- %s"
691 % (rev, ' '.join(paths))):
692 commit = get_output("git show --date=local -M -C %s %s"
693 % (flags.get('-p', '--name-status'), hash),
694 strip=False)
695 commit = self._filter_log(commit)
696 print >>pager, ''.join(commit)
698 # Check for renames
699 if len(paths) == 1:
700 src = self._check_rename(hash, paths[0])
701 if src is not None:
702 paths[0] = src
703 rename = True
704 rev = hash+"^"
705 break
706 if not rename:
707 break
708 rename = False
709 except (IOError, KeyboardInterrupt):
710 pass
712 @short_help("show staged, unstaged, or all uncommitted changes")
713 @long_help("""
714 Show staged, unstaged, or all uncommitted changes. By default, all
715 changes are shown. The '-u' flag causes only unstaged changes to be
716 shown. The '-d' flag causes only staged changes to be shown.
717 """)
718 @takes_options("ud")
719 def cmd_diff(self, **flags):
720 "[ -u | -d ]"
721 self._check_git()
722 if '-u' in flags and '-d' in flags:
723 raise YapError("Conflicting flags: -u and -d")
725 pager = self._get_pager_cmd()
727 if '-u' in flags:
728 os.system("git diff-files -p | %s" % pager)
729 elif '-d' in flags:
730 os.system("git diff-index --cached -p HEAD | %s" % pager)
731 else:
732 os.system("git diff-index -p HEAD | %s" % pager)
734 @short_help("list, create, or delete branches")
735 @long_help("""
736 If no arguments are specified, a list of local branches is given. The
737 current branch is indicated by a "*" next to the name. If an argument
738 is given, it is taken as the name of a new branch to create. The branch
739 will start pointing at the current HEAD. See 'point' for details on
740 changing the revision of the new branch. Note that this command does
741 not switch the current working branch. See 'switch' for details on
742 changing the current working branch.
744 The '-d' flag can be used to delete local branches. If the delete
745 operation would remove the last branch reference to a given line of
746 history (colloquially referred to as "dangling commits"), yap will
747 report an error and abort. The '-f' flag can be used to force the delete
748 in spite of this.
749 """)
750 @takes_options("fd:")
751 def cmd_branch(self, branch=None, **flags):
752 "[ [-f] -d <branch> | <branch> ]"
753 self._check_git()
754 force = '-f' in flags
755 if '-d' in flags:
756 self._delete_branch(flags['-d'], force)
757 self.cmd_branch()
758 return
760 if branch is not None:
761 ref = get_output("git rev-parse --verify HEAD")
762 if not ref:
763 raise YapError("No branch point yet. Make a commit")
764 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch, ref[0]))
766 current = get_output("git symbolic-ref HEAD")
767 branches = get_output("git for-each-ref --format='%(refname)' 'refs/heads'")
768 for b in branches:
769 if current and b == current[0]:
770 print "* ",
771 else:
772 print " ",
773 b = b.replace('refs/heads/', '')
774 print b
776 @short_help("change the current working branch")
777 @long_help("""
778 The argument is the name of the branch to make the current working
779 branch. This command will fail if there are uncommitted changes to any
780 files. Otherwise, the contents of the files in the working directory
781 are updated to reflect their state in the new branch. Additionally, any
782 future commits are added to the new branch instead of the previous line
783 of history.
784 """)
785 @takes_options("f")
786 def cmd_switch(self, branch, **flags):
787 "[-f] <branch>"
788 self._check_git()
789 self._check_rebasing()
790 ref = self._resolve_rev('refs/heads/'+branch)
792 if '-f' not in flags:
793 if (self._get_staged_files()
794 or (self._get_unstaged_files()
795 and run_command("git update-index --refresh"))):
796 raise YapError("You have uncommitted changes. Use -f to continue anyway")
798 if self._get_unstaged_files() and self._get_staged_files():
799 raise YapError("You have staged and unstaged changes. Perhaps unstage -a?")
801 staged = bool(self._get_staged_files())
803 cdup = self._get_cdup()
804 run_command("(cd %s; git add -u)" % cdup)
805 for f in self._get_new_files():
806 self._stage_one(f)
808 idx = get_output("git write-tree")
809 new = self._resolve_rev('refs/heads/'+branch)
811 run_command("git update-index --refresh")
812 readtree = "git read-tree -v --aggressive -u -m HEAD %s %s" % (idx[0], new)
813 if os.system(readtree):
814 raise YapError("Failed to switch")
815 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch)
817 if '-f' not in flags:
818 self._clear_state()
820 if not staged:
821 self._unstage_all()
822 self.cmd_status()
824 @short_help("move the current branch to a different revision")
825 @long_help("""
826 The argument is the hash of the commit to which the current branch
827 should point, or alternately a branch or tag (a.k.a, "committish"). If
828 moving the branch would create "dangling commits" (see 'branch'), yap
829 will report an error and abort. The '-f' flag can be used to force the
830 operation in spite of this.
831 """)
832 @takes_options("f")
833 def cmd_point(self, where, **flags):
834 "[-f] <where>"
835 self._check_git()
836 self._check_rebasing()
838 head = get_output("git rev-parse --verify HEAD")
839 if not head:
840 raise YapError("No commit yet; nowhere to point")
842 ref = self._resolve_rev(where)
843 ref = get_output("git rev-parse --verify '%s^{commit}'" % ref)
844 if not ref:
845 raise YapError("Not a commit: %s" % where)
847 if self._get_unstaged_files() or self._get_staged_files():
848 raise YapError("You have uncommitted changes. Commit them first")
850 run_safely("git update-ref HEAD '%s'" % ref[0])
852 if '-f' not in flags:
853 name = get_output("git name-rev --name-only '%s'" % head[0])[0]
854 if name == "undefined":
855 os.system("git update-ref HEAD '%s'" % head[0])
856 raise YapError("Pointing there will lose commits. Use -f to force")
858 run_command("git update-index --refresh")
859 rc = os.system("git read-tree -v --reset -u HEAD")
860 if rc:
861 raise YapError("checkout-index failed")
862 self._clear_state()
864 @short_help("alter history by dropping or amending commits")
865 @long_help("""
866 This command operates in two distinct modes, "amend" and "drop" mode.
867 In drop mode, the given commit is removed from the history of the
868 current branch, as though that commit never happened. By default the
869 commit used is HEAD.
871 In amend mode, the uncommitted changes present are merged into a
872 previous commit. This is useful for correcting typos or adding missed
873 files into past commits. By default the commit used is HEAD.
875 While rewriting history it is possible that conflicts will arise. If
876 this happens, the rewrite will pause and you will be prompted to resolve
877 the conflicts and stage them. Once that is done, you will run "yap
878 history continue." If instead you want the conflicting commit removed
879 from history (perhaps your changes supercede that commit) you can run
880 "yap history skip". Once the rewrite completes, your branch will be on
881 the same commit as when the rewrite started.
882 """)
883 def cmd_history(self, subcmd, *args):
884 "amend | drop <commit>"
885 self._check_git()
887 if subcmd not in ("amend", "drop", "continue", "skip"):
888 raise TypeError
890 resolvemsg = """
891 When you have resolved the conflicts run \"yap history continue\".
892 To skip the problematic patch, run \"yap history skip\"."""
894 if subcmd == "continue":
895 os.system("git am -3 -r --resolvemsg='%s'" % resolvemsg)
896 return
897 if subcmd == "skip":
898 os.system("git reset --hard")
899 os.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg)
900 return
902 if subcmd == "amend":
903 flags, args = getopt.getopt(args, "ad")
904 flags = dict(flags)
906 if len(args) > 1:
907 raise TypeError
908 if args:
909 commit = args[0]
910 else:
911 commit = "HEAD"
913 self._resolve_rev(commit)
914 self._check_rebasing()
916 if subcmd == "amend":
917 self._check_commit(**flags)
918 if self._get_unstaged_files():
919 # XXX: handle unstaged changes better
920 raise YapError("Commit away changes that you aren't amending")
922 self._unstage_all()
924 start = get_output("git rev-parse HEAD")
925 stash = get_output("git stash create")
926 run_command("git reset --hard")
927 try:
928 fd, tmpfile = tempfile.mkstemp("yap")
929 try:
930 try:
931 os.close(fd)
932 os.system("git format-patch -k --stdout '%s' > %s" % (commit, tmpfile))
933 if subcmd == "amend":
934 self.cmd_point(commit, **{'-f': True})
935 finally:
936 if subcmd == "amend":
937 if stash:
938 rc = os.system("git stash apply %s" % stash[0])
939 if rc:
940 self.cmd_point(start[0], **{'-f': True})
941 os.system("git stash apply %s" % stash[0])
942 raise YapError("Failed to apply stash")
943 stash = None
945 if subcmd == "amend":
946 self._do_uncommit()
947 self._check_commit(**{'-a': True})
948 self._do_commit()
949 else:
950 self.cmd_point("%s^" % commit, **{'-f': True})
952 stat = os.stat(tmpfile)
953 size = stat[6]
954 if size > 0:
955 run_safely("git update-index --refresh")
956 rc = os.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg, tmpfile))
957 if (rc):
958 raise YapError("Failed to apply changes")
959 finally:
960 os.unlink(tmpfile)
961 finally:
962 if stash:
963 run_command("git stash apply %s" % stash[0])
964 self.cmd_status()
966 @short_help("show the changes introduced by a given commit")
967 @long_help("""
968 By default, the changes in the last commit are shown. To override this,
969 specify a hash, branch, or tag (committish). The hash of the commit,
970 the commit's author, log message, and a diff of the changes are shown.
971 """)
972 def cmd_show(self, commit="HEAD"):
973 "[commit]"
974 self._check_git()
975 commit = self._resolve_rev(commit)
976 os.system("git show '%s'" % commit)
978 @short_help("apply the changes in a given commit to the current branch")
979 @long_help("""
980 The argument is the hash, branch, or tag (committish) of the commit to
981 be applied. In general, it only makes sense to apply commits that
982 happened on another branch. The '-r' flag can be used to have the
983 changes in the given commit reversed from the current branch. In
984 general, this only makes sense for commits that happened on the current
985 branch.
986 """)
987 @takes_options("r")
988 def cmd_cherry_pick(self, commit, **flags):
989 "[-r] <commit>"
990 self._check_git()
991 commit = self._resolve_rev(commit)
992 if '-r' in flags:
993 os.system("git revert '%s'" % commit)
994 else:
995 os.system("git cherry-pick '%s'" % commit)
997 @short_help("list, add, or delete configured remote repositories")
998 @long_help("""
999 When invoked with no arguments, this command will show the list of
1000 currently configured remote repositories, giving both the name and URL
1001 of each. To add a new repository, give the desired name as the first
1002 argument and the URL as the second. The '-d' flag can be used to remove
1003 a previously added repository.
1004 """)
1005 @takes_options("d:")
1006 def cmd_repo(self, name=None, url=None, **flags):
1007 "[<name> <url> | -d <name>]"
1008 self._check_git()
1009 if name is not None and url is None:
1010 raise TypeError
1012 if '-d' in flags:
1013 if flags['-d'] not in [ x[0] for x in self._list_remotes() ]:
1014 raise YapError("No such repository: %s" % flags['-d'])
1015 os.system("git config --unset remote.%s.url" % flags['-d'])
1016 os.system("git config --unset remote.%s.fetch" % flags['-d'])
1017 for b in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/%s'" % flags['-d']):
1018 hash = get_output("git rev-parse %s" % b)
1019 assert hash
1020 run_safely("git update-ref -d %s %s" % (b, hash[0]))
1022 if name:
1023 if name in [ x[0] for x in self._list_remotes() ]:
1024 raise YapError("Repository '%s' already exists" % name)
1025 os.system("git config remote.%s.url %s" % (name, url))
1026 os.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name, name))
1028 for remote, url in self._list_remotes():
1029 print "%s" % remote
1030 print " URL: %s" % url
1031 first = True
1032 for b in get_output("git for-each-ref --format='%%(refname)' 'refs/remotes/%s'" % remote):
1033 b = b.replace('refs/remotes/', '')
1034 if first:
1035 branches = "Branches: "
1036 else:
1037 branches = " "
1038 print " %s%s" % (branches, b)
1039 first = False
1041 @short_help("send local commits to a remote repository (*)")
1042 @long_help("""
1043 When invoked with no arguments, the current branch is synchronized to
1044 the tracking branch of the tracking remote. If no tracking remote is
1045 specified, the repository will have to be specified on the command line.
1046 In that case, the default is to push to a branch with the same name as
1047 the current branch. This behavior can be overridden by giving a second
1048 argument to specify the remote branch.
1050 If the remote branch does not currently exist, the command will abort
1051 unless the -c flag is provided. If the remote branch is not a direct
1052 descendent of the local branch, the command will abort unless the -f
1053 flag is provided. Forcing a push in this way can be problematic to
1054 other users of the repository if they are not expecting it.
1056 To delete a branch on the remote repository, use the -d flag.
1057 """)
1058 @takes_options("cdf")
1059 def cmd_push(self, repo=None, rhs=None, **flags):
1060 "[-c | -d] <repo>"
1061 self._check_git()
1062 if '-c' in flags and '-d' in flags:
1063 raise TypeError
1065 if repo and repo not in [ x[0] for x in self._list_remotes() ]:
1066 raise YapError("No such repository: %s" % repo)
1068 current = get_output("git symbolic-ref HEAD")
1069 if not current:
1070 raise YapError("Not on a branch!")
1072 self._check_rebasing()
1074 current = current[0].replace('refs/heads/', '')
1075 remote = get_output("git config branch.%s.remote" % current)
1076 if repo is None and remote:
1077 repo = remote[0]
1079 if repo is None:
1080 raise YapError("No tracking branch configured; specify destination repository")
1082 if rhs is None and remote and remote[0] == repo:
1083 merge = get_output("git config branch.%s.merge" % current)
1084 if merge:
1085 rhs = merge[0]
1087 if rhs is None:
1088 rhs = "refs/heads/%s" % current
1090 if '-c' not in flags and '-d' not in flags:
1091 if run_command("git rev-parse --verify refs/remotes/%s/%s"
1092 % (repo, rhs.replace('refs/heads/', ''))):
1093 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
1094 if '-f' not in flags:
1095 hash = get_output("git rev-parse refs/remotes/%s/%s" % (repo, rhs.replace('refs/heads/', '')))
1096 base = get_output("git merge-base HEAD %s" % hash[0])
1097 assert base
1098 if base[0] != hash[0]:
1099 raise YapError("Branch not up-to-date with remote. Update or use -f")
1101 self._confirm_push(current, rhs, repo)
1102 if '-f' in flags:
1103 flags['-f'] = '-f'
1105 if '-d' in flags:
1106 lhs = ""
1107 else:
1108 lhs = "refs/heads/%s" % current
1109 rc = os.system("git push %s %s %s:%s" % (flags.get('-f', ''), repo, lhs, rhs))
1110 if rc:
1111 raise YapError("Push failed.")
1113 @short_help("retrieve commits from a remote repository")
1114 @long_help("""
1115 When run with no arguments, the command will retrieve new commits from
1116 the remote tracking repository. Note that this does not in any way
1117 alter the current branch. For that, see "update". If a remote other
1118 than the tracking remote is desired, it can be specified as the first
1119 argument.
1120 """)
1121 def cmd_fetch(self, repo=None):
1122 "<repo>"
1123 self._check_git()
1124 current = get_output("git symbolic-ref HEAD")
1125 if not current:
1126 raise YapError("Not on a branch!")
1128 if repo and repo not in [ x[0] for x in self._list_remotes() ]:
1129 raise YapError("No such repository: %s" % repo)
1130 if repo is None:
1131 current = current[0].replace('refs/heads/', '')
1132 remote = get_output("git config branch.%s.remote" % current)
1133 if remote:
1134 repo = remote[0]
1135 if repo is None:
1136 raise YapError("No tracking branch configured; specify a repository")
1137 os.system("git fetch %s" % repo)
1139 @short_help("update the current branch relative to its tracking branch")
1140 @long_help("""
1141 Updates the current branch relative to its remote tracking branch. This
1142 command requires that the current branch have a remote tracking branch
1143 configured. If any conflicts occur while applying your changes to the
1144 updated remote, the command will pause to allow you to fix them. Once
1145 that is done, run "update" with the "continue" subcommand. Alternately,
1146 the "skip" subcommand can be used to discard the conflicting changes.
1147 """)
1148 def cmd_update(self, subcmd=None):
1149 "[continue | skip]"
1150 self._check_git()
1151 if subcmd and subcmd not in ["continue", "skip"]:
1152 raise TypeError
1154 resolvemsg = """
1155 When you have resolved the conflicts run \"yap update continue\".
1156 To skip the problematic patch, run \"yap update skip\"."""
1158 if subcmd == "continue":
1159 os.system("git am -3 -r --resolvemsg='%s'" % resolvemsg)
1160 return
1161 if subcmd == "skip":
1162 os.system("git reset --hard")
1163 os.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg)
1164 return
1166 self._check_rebasing()
1167 if self._get_unstaged_files() or self._get_staged_files():
1168 raise YapError("You have uncommitted changes. Commit them first")
1170 current = get_output("git symbolic-ref HEAD")
1171 if not current:
1172 raise YapError("Not on a branch!")
1174 current = current[0].replace('refs/heads/', '')
1175 remote, merge = self._get_tracking(current)
1176 merge = merge.replace('refs/heads/', '')
1178 self.cmd_fetch(remote)
1179 base = get_output("git merge-base HEAD refs/remotes/%s/%s" % (remote, merge))
1181 try:
1182 fd, tmpfile = tempfile.mkstemp("yap")
1183 os.close(fd)
1184 os.system("git format-patch -k --stdout '%s' > %s" % (base[0], tmpfile))
1185 self.cmd_point("refs/remotes/%s/%s" % (remote, merge), **{'-f': True})
1187 stat = os.stat(tmpfile)
1188 size = stat[6]
1189 if size > 0:
1190 rc = os.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg, tmpfile))
1191 if (rc):
1192 raise YapError("Failed to apply changes")
1193 finally:
1194 os.unlink(tmpfile)
1196 @short_help("query and configure remote branch tracking")
1197 @long_help("""
1198 When invoked with no arguments, the command displays the tracking
1199 information for the current branch. To configure the tracking
1200 information, two arguments for the remote repository and remote branch
1201 are given. The tracking information is used to provide defaults for
1202 where to push local changes and from where to get updates to the branch.
1203 """)
1204 def cmd_track(self, repo=None, branch=None):
1205 "[<repo> <branch>]"
1206 self._check_git()
1208 current = get_output("git symbolic-ref HEAD")
1209 if not current:
1210 raise YapError("Not on a branch!")
1211 current = current[0].replace('refs/heads/', '')
1213 if repo is None and branch is None:
1214 repo, merge = self._get_tracking(current)
1215 merge = merge.replace('refs/heads/', '')
1216 print "Branch '%s' tracking refs/remotes/%s/%s" % (current, repo, merge)
1217 return
1219 if repo is None or branch is None:
1220 raise TypeError
1222 if repo not in [ x[0] for x in self._list_remotes() ]:
1223 raise YapError("No such repository: %s" % repo)
1225 if run_command("git rev-parse --verify refs/remotes/%s/%s" % (repo, branch)):
1226 raise YapError("No such branch '%s' on repository '%s'" % (branch, repo))
1228 os.system("git config branch.%s.remote '%s'" % (current, repo))
1229 os.system("git config branch.%s.merge 'refs/heads/%s'" % (current, branch))
1230 print "Branch '%s' now tracking refs/remotes/%s/%s" % (current, repo, branch)
1232 @short_help("mark files with conflicts as resolved")
1233 @long_help("""
1234 The arguments are the files to be marked resolved. When a conflict
1235 occurs while merging changes to a file, that file is marked as
1236 "unmerged." Until the file(s) with conflicts are marked resolved,
1237 commits cannot be made.
1238 """)
1239 def cmd_resolved(self, *files):
1240 "<file>..."
1241 self._check_git()
1242 if not files:
1243 raise TypeError
1245 for f in files:
1246 self._stage_one(f, True)
1247 self.cmd_status()
1249 @short_help("merge a branch into the current branch")
1250 def cmd_merge(self, branch):
1251 "<branch>"
1252 self._check_git()
1254 branch_name = branch
1255 branch = self._resolve_rev(branch)
1256 base = get_output("git merge-base HEAD %s" % branch)
1257 if not base:
1258 raise YapError("Branch '%s' is not a fork of the current branch"
1259 % branch)
1261 readtree = ("git read-tree --aggressive -u -m %s HEAD %s"
1262 % (base[0], branch))
1263 if run_command(readtree):
1264 run_command("git update-index --refresh")
1265 if os.system(readtree):
1266 raise YapError("Failed to merge")
1268 repo = get_output('git rev-parse --git-dir')[0]
1269 dir = os.path.join(repo, 'yap')
1270 try:
1271 os.mkdir(dir)
1272 except OSError:
1273 pass
1274 msg_file = os.path.join(dir, 'msg')
1275 msg = file(msg_file, 'w')
1276 print >>msg, "Merge branch '%s'" % branch_name
1277 msg.close()
1279 head = get_output("git rev-parse --verify HEAD")
1280 assert head
1281 heads = [head[0], branch]
1282 head_file = os.path.join(dir, 'merge')
1283 pickle.dump(heads, file(head_file, 'w'))
1285 self._merge_index(branch, base[0])
1286 if self._get_unmerged_files():
1287 self.cmd_status()
1288 raise YapError("Fix conflicts then commit")
1290 self._do_commit()
1292 def _merge_index(self, branch, base):
1293 for f in self._get_unmerged_files():
1294 fd, bfile = tempfile.mkstemp("yap")
1295 os.close(fd)
1296 rc = os.system("git show %s:%s > %s" % (base, f, bfile))
1297 assert rc == 0
1299 fd, ofile = tempfile.mkstemp("yap")
1300 os.close(fd)
1301 rc = os.system("git show %s:%s > %s" % (branch, f, ofile))
1302 assert rc == 0
1304 command = "git merge-file -L %(file)s -L %(file)s.base -L %(file)s.%(branch)s %(file)s %(base)s %(other)s " % dict(file=f, branch=branch, base=bfile, other=ofile)
1305 rc = os.system(command)
1306 os.unlink(ofile)
1307 os.unlink(bfile)
1309 assert rc >= 0
1310 if rc == 0:
1311 self._stage_one(f, True)
1313 def cmd_help(self, cmd=None):
1314 if cmd is not None:
1315 oldcmd = cmd
1316 cmd = "cmd_" + cmd.replace('-', '_')
1317 try:
1318 attr = self.__getattribute__(cmd)
1319 except AttributeError:
1320 raise YapError("No such command: %s" % cmd)
1322 help = self._get_attr(cmd, "long_help")
1323 if help is None:
1324 raise YapError("Sorry, no help for '%s'. Ask Steven." % oldcmd)
1326 print >>sys.stderr, "The '%s' command" % oldcmd
1327 doc = self._get_attr(cmd, "__doc__")
1328 if doc is None:
1329 doc = ""
1330 print >>sys.stderr, "\tyap %s %s" % (oldcmd, doc)
1331 print >>sys.stderr, "%s" % help
1332 return
1334 print >> sys.stderr, "Yet Another (Git) Porcelein"
1335 print >> sys.stderr
1337 for name in dir(self):
1338 if not name.startswith('cmd_'):
1339 continue
1340 attr = self.__getattribute__(name)
1341 if not callable(attr):
1342 continue
1344 short_msg = self._get_attr(name, "short_help")
1345 if short_msg is None:
1346 continue
1348 name = name.replace('cmd_', '')
1349 name = name.replace('_', '-')
1350 print >> sys.stderr, "%-16s%s" % (name, short_msg)
1352 print >> sys.stderr
1353 print >> sys.stderr, "(*) Indicates that the command is not readily reversible"
1355 @short_help("show information about loaded plugins")
1356 def cmd_plugins(self):
1358 print >> sys.stderr, "Loaded plugins:"
1359 plugins = load_plugins()
1360 for name, cls in plugins.items():
1361 print "\t%-16s: %s" % (name, cls.__doc__)
1362 if not plugins:
1363 print "\t%-16s" % "None"
1365 def cmd_usage(self):
1366 print >> sys.stderr, "usage: %s <command>" % os.path.basename(sys.argv[0])
1367 print >> sys.stderr, " valid commands: help init clone add rm stage unstage status revert commit uncommit log show diff branch switch point cherry-pick repo track push fetch update history resolved version"
1369 def load_plugins():
1370 plugindir = os.path.join("~", ".yap", "plugins")
1371 plugindir = os.path.expanduser(plugindir)
1372 plugindir = os.path.join(plugindir, "*.py")
1374 plugins = dict()
1375 for p in glob.glob(os.path.expanduser(plugindir)):
1376 plugin = os.path.basename(p).replace('.py', '')
1377 m = __import__(plugin)
1378 for k in dir(m):
1379 cls = m.__dict__[k]
1380 if not type(cls) == type:
1381 continue
1382 if not issubclass(cls, YapCore):
1383 continue
1384 if cls is YapCore:
1385 continue
1386 plugins[k] = cls
1387 return plugins
1389 def yap_metaclass(name, bases, dct):
1390 plugindir = os.path.join("~", ".yap", "plugins")
1391 plugindir = os.path.expanduser(plugindir)
1392 sys.path.insert(0, plugindir)
1394 plugins = set(load_plugins().values())
1395 p2 = plugins.copy()
1396 for cls in plugins:
1397 p2 -= set(cls.__bases__)
1398 plugins = p2
1399 bases = list(plugins) + list(bases)
1400 return type(name, tuple(bases), dct)
1402 class Yap(YapCore):
1403 __metaclass__ = yap_metaclass
1405 def main(self, args):
1406 if len(args) < 1:
1407 self.cmd_usage()
1408 sys.exit(2)
1410 command = args[0]
1411 args = args[1:]
1413 if run_command("git --version"):
1414 print >>sys.stderr, "Failed to run git; is it installed?"
1415 sys.exit(1)
1417 debug = os.getenv('YAP_DEBUG')
1419 try:
1420 command = command.replace('-', '_')
1421 meth = self.__getattribute__("cmd_"+command)
1422 doc = self._get_attr("cmd_"+command, "__doc__")
1424 try:
1425 options = ""
1426 for c in self.__class__.__bases__:
1427 try:
1428 t = c.__dict__["cmd_"+command]
1429 except KeyError:
1430 continue
1431 if "options" in t.__dict__:
1432 options += t.options
1434 if options:
1435 try:
1436 flags, args = getopt.getopt(args, options)
1437 flags = dict(flags)
1438 except getopt.GetoptError, e:
1439 if debug:
1440 raise
1441 print "Usage: %s %s %s" % (os.path.basename(sys.argv[0]), command, doc)
1442 print e
1443 sys.exit(2)
1444 else:
1445 flags = dict()
1447 meth(*args, **flags)
1448 except (TypeError, getopt.GetoptError):
1449 if debug:
1450 raise
1451 print "Usage: %s %s %s" % (os.path.basename(sys.argv[0]), command, doc)
1452 except YapError, e:
1453 if debug:
1454 raise
1455 print >> sys.stderr, e
1456 sys.exit(1)
1457 except AttributeError:
1458 if debug:
1459 raise
1460 self.cmd_usage()
1461 sys.exit(2)