Implement plugin loading
[yap.git] / yap / yap.py
blobaf798a64cec7ef6eebaf10474051ef53dd1c74c5
1 import sys
2 import os
3 import glob
4 import getopt
5 import pickle
6 import tempfile
8 from plugin import YapPlugin
10 def get_output(cmd):
11 fd = os.popen(cmd)
12 output = fd.readlines()
13 rc = fd.close()
14 return [x.strip() for x in output]
16 def run_command(cmd):
17 rc = os.system("%s > /dev/null 2>&1" % cmd)
18 rc >>= 8
19 return rc
21 class YapError(Exception):
22 def __init__(self, msg):
23 self.msg = msg
25 def __str__(self):
26 return self.msg
28 def takes_options(options):
29 def decorator(func):
30 func.options = options
31 return func
32 return decorator
34 def short_help(help_msg):
35 def decorator(func):
36 func.short_help = help_msg
37 return func
38 return decorator
40 def long_help(help_msg):
41 def decorator(func):
42 func.long_help = help_msg
43 return func
44 return decorator
46 class Yap(object):
47 def __init__(self):
48 self.plugins = set()
49 plugindir = os.path.expanduser("~/.yap/plugins")
50 for p in glob.glob(os.path.join(plugindir, "*.py")):
51 glbls = {}
52 execfile(p, glbls)
53 for cls in glbls.values():
54 if not type(cls) == type:
55 continue
56 if not issubclass(cls, YapPlugin):
57 continue
58 if cls is YapPlugin:
59 continue
60 x = cls(self)
61 # XXX: check for override overlap
62 self.plugins.add(x)
64 def _add_new_file(self, file):
65 repo = get_output('git rev-parse --git-dir')[0]
66 dir = os.path.join(repo, 'yap')
67 try:
68 os.mkdir(dir)
69 except OSError:
70 pass
71 files = self._get_new_files()
72 files.append(file)
73 path = os.path.join(dir, 'new-files')
74 pickle.dump(files, open(path, 'w'))
76 def _get_new_files(self):
77 repo = get_output('git rev-parse --git-dir')[0]
78 path = os.path.join(repo, 'yap', 'new-files')
79 try:
80 files = pickle.load(file(path))
81 except IOError:
82 files = []
84 x = []
85 for f in files:
86 # if f in the index
87 if get_output("git ls-files --cached '%s'" % f) != []:
88 continue
89 x.append(f)
90 return x
92 def _remove_new_file(self, file):
93 files = self._get_new_files()
94 files = filter(lambda x: x != file, files)
96 repo = get_output('git rev-parse --git-dir')[0]
97 path = os.path.join(repo, 'yap', 'new-files')
98 pickle.dump(files, open(path, 'w'))
100 def _clear_new_files(self):
101 repo = get_output('git rev-parse --git-dir')[0]
102 path = os.path.join(repo, 'yap', 'new-files')
103 os.unlink(path)
105 def _assert_file_exists(self, file):
106 if not os.access(file, os.R_OK):
107 raise YapError("No such file: %s" % file)
109 def _get_staged_files(self):
110 if run_command("git rev-parse HEAD"):
111 files = get_output("git ls-files --cached")
112 else:
113 files = get_output("git diff-index --cached --name-only HEAD")
114 return files
116 def _get_unstaged_files(self):
117 files = self._get_new_files()
118 files += get_output("git ls-files -m")
119 return files
121 def _delete_branch(self, branch, force):
122 current = get_output("git symbolic-ref HEAD")[0]
123 current = current.replace('refs/heads/', '')
124 if branch == current:
125 raise YapError("Can't delete current branch")
127 ref = get_output("git rev-parse 'refs/heads/%s'" % branch)
128 if not ref:
129 raise YapError("No such branch: %s" % branch)
130 os.system("git update-ref -d 'refs/heads/%s' '%s'" % (branch, ref[0]))
132 if not force:
133 name = get_output("git name-rev --name-only '%s'" % ref[0])[0]
134 if name == 'undefined':
135 os.system("git update-ref 'refs/heads/%s' '%s'" % (branch, ref[0]))
136 raise YapError("Refusing to delete leaf branch (use -f to force)")
137 def _get_pager_cmd(self):
138 if 'YAP_PAGER' in os.environ:
139 return os.environ['YAP_PAGER']
140 elif 'GIT_PAGER' in os.environ:
141 return os.environ['GIT_PAGER']
142 elif 'PAGER' in os.environ:
143 return os.environ['PAGER']
144 else:
145 return "more"
147 def _add_one(self, file):
148 self._assert_file_exists(file)
149 x = get_output("git ls-files '%s'" % file)
150 if x != []:
151 raise YapError("File '%s' already in repository" % file)
152 self._add_new_file(file)
154 def _rm_one(self, file):
155 self._assert_file_exists(file)
156 if get_output("git ls-files '%s'" % file) != []:
157 os.system("git rm --cached '%s'" % file)
158 self._remove_new_file(file)
160 def _stage_one(self, file):
161 self._assert_file_exists(file)
162 os.system("git update-index --add '%s'" % file)
164 def _unstage_one(self, file):
165 self._assert_file_exists(file)
166 if run_command("git rev-parse HEAD"):
167 os.system("git update-index --force-remove '%s'" % file)
168 else:
169 os.system("git diff-index -p HEAD '%s' | git apply -R --cached" % file)
171 def _revert_one(self, file):
172 self._assert_file_exists(file)
173 self._unstage_one(file)
174 os.system("git checkout-index -f '%s'" % file)
176 def _parse_commit(self, commit):
177 lines = get_output("git cat-file commit '%s'" % commit)
178 commit = {}
180 mode = None
181 for l in lines:
182 if mode != 'commit' and l.strip() == "":
183 mode = 'commit'
184 commit['log'] = []
185 continue
186 if mode == 'commit':
187 commit['log'].append(l)
188 continue
190 x = l.split(' ')
191 k = x[0]
192 v = ' '.join(x[1:])
193 commit[k] = v
194 commit['log'] = '\n'.join(commit['log'])
195 return commit
197 def _check_commit(self, **flags):
198 if '-a' in flags and '-d' in flags:
199 raise YapError("Conflicting flags: -a and -d")
201 if '-d' not in flags and self._get_unstaged_files():
202 if '-a' not in flags and self._get_staged_files():
203 raise YapError("Staged and unstaged changes present. Specify what to commit")
204 os.system("git diff-files -p | git apply --cached 2>/dev/null")
205 for f in self._get_new_files():
206 self._stage_one(f)
208 if not self._get_staged_files():
209 raise YapError("No changes to commit")
211 def _do_uncommit(self):
212 commit = self._parse_commit("HEAD")
213 repo = get_output('git rev-parse --git-dir')[0]
214 dir = os.path.join(repo, 'yap')
215 try:
216 os.mkdir(dir)
217 except OSError:
218 pass
219 msg_file = os.path.join(dir, 'msg')
220 fd = file(msg_file, 'w')
221 print >>fd, commit['log']
222 fd.close()
224 tree = get_output("git rev-parse HEAD^")
225 os.system("git update-ref -m uncommit HEAD '%s'" % tree[0])
227 def _do_commit(self):
228 tree = get_output("git write-tree")[0]
229 parent = get_output("git rev-parse HEAD 2> /dev/null")[0]
231 if os.environ.has_key('YAP_EDITOR'):
232 editor = os.environ['YAP_EDITOR']
233 elif os.environ.has_key('GIT_EDITOR'):
234 editor = os.environ['GIT_EDITOR']
235 elif os.environ.has_key('EDITOR'):
236 editor = os.environ['EDITOR']
237 else:
238 editor = "vi"
240 fd, tmpfile = tempfile.mkstemp("yap")
241 os.close(fd)
243 repo = get_output('git rev-parse --git-dir')[0]
244 msg_file = os.path.join(repo, 'yap', 'msg')
245 if os.access(msg_file, os.R_OK):
246 fd1 = file(msg_file)
247 fd2 = file(tmpfile, 'w')
248 for l in fd1.xreadlines():
249 print >>fd2, l.strip()
250 fd2.close()
251 os.unlink(msg_file)
253 if os.system("%s '%s'" % (editor, tmpfile)) != 0:
254 raise YapError("Editing commit message failed")
255 if parent != 'HEAD':
256 commit = get_output("git commit-tree '%s' -p '%s' < '%s'" % (tree, parent, tmpfile))
257 else:
258 commit = get_output("git commit-tree '%s' < '%s'" % (tree, tmpfile))
259 if not commit:
260 raise YapError("Commit failed; no log message?")
261 os.unlink(tmpfile)
262 os.system("git update-ref HEAD '%s'" % commit[0])
264 def _check_rebasing(self):
265 repo = get_output('git rev-parse --git-dir')[0]
266 dotest = os.path.join(repo, '.dotest')
267 if os.access(dotest, os.R_OK):
268 raise YapError("A git operation is in progress. Complete it first")
269 dotest = os.path.join(repo, '..', '.dotest')
270 if os.access(dotest, os.R_OK):
271 raise YapError("A git operation is in progress. Complete it first")
273 def _list_remotes(self):
274 remotes = get_output("git config --get-regexp 'remote.*.url'")
275 for x in remotes:
276 remote, url = x.split(' ')
277 remote = remote.replace('remote.', '')
278 remote = remote.replace('.url', '')
279 yield remote, url
281 @short_help("make a local copy of an existing repository")
282 @long_help("""
283 The first argument is a URL to the existing repository. This can be an
284 absolute path if the repository is local, or a URL with the git://,
285 ssh://, or http:// schemes. By default, the directory used is the last
286 component of the URL, sans '.git'. This can be overridden by providing
287 a second argument.
288 """)
289 def cmd_clone(self, url, directory=""):
290 "<url> [directory]"
291 # XXX: implement in terms of init + remote add + fetch
292 os.system("git clone '%s' %s" % (url, directory))
294 @short_help("turn a directory into a repository")
295 @long_help("""
296 Converts the current working directory into a repository. The primary
297 side-effect of this command is the creation of a '.git' subdirectory.
298 No files are added nor commits made.
299 """)
300 def cmd_init(self):
301 os.system("git init")
303 @short_help("add a new file to the repository")
304 @long_help("""
305 The arguments are the files to be added to the repository. Once added,
306 the files will show as "unstaged changes" in the output of 'status'. To
307 reverse the effects of this command, see 'rm'.
308 """)
309 def cmd_add(self, *files):
310 "<file>..."
311 if not files:
312 raise TypeError
314 for f in files:
315 self._add_one(f)
316 self.cmd_status()
318 @short_help("delete a file from the repository")
319 @long_help("""
320 The arguments are the files to be removed from the current revision of
321 the repository. The files will still exist in any past commits that the
322 file may have been a part of. The file is not actually deleted, it is
323 just no longer tracked as part of the repository.
324 """)
325 def cmd_rm(self, *files):
326 "<file>..."
327 if not files:
328 raise TypeError
330 for f in files:
331 self._rm_one(f)
332 self.cmd_status()
334 @short_help("stage changes in a file for commit")
335 @long_help("""
336 The arguments are the files to be staged. Staging changes is a way to
337 build up a commit when you do not want to commit all changes at once.
338 To commit only staged changes, use the '-d' flag to 'commit.' To
339 reverse the effects of this command, see 'unstage'. Once staged, the
340 files will show as "staged changes" in the output of 'status'.
341 """)
342 def cmd_stage(self, *files):
343 "<file>..."
344 if not files:
345 raise TypeError
347 for f in files:
348 self._stage_one(f)
349 self.cmd_status()
351 @short_help("unstage changes in a file")
352 @long_help("""
353 The arguments are the files to be unstaged. Once unstaged, the files
354 will show as "unstaged changes" in the output of 'status'. The '-a'
355 flag can be used to unstage all staged changes at once.
356 """)
357 @takes_options("a")
358 def cmd_unstage(self, *files, **flags):
359 "[-a] | <file>..."
360 if '-a' in flags:
361 os.system("git read-tree HEAD")
362 self.cmd_status()
363 return
365 if not files:
366 raise TypeError
368 for f in files:
369 self._unstage_one(f)
370 self.cmd_status()
372 @short_help("show files with staged and unstaged changes")
373 @long_help("""
374 Show the files in the repository with changes since the last commit,
375 categorized based on whether the changes are staged or not. A file may
376 appear under each heading if the same file has both staged and unstaged
377 changes.
378 """)
379 def cmd_status(self):
380 branch = get_output("git symbolic-ref HEAD")[0]
381 branch = branch.replace('refs/heads/', '')
382 print "Current branch: %s" % branch
384 print "Files with staged changes:"
385 files = self._get_staged_files()
386 for f in files:
387 print "\t%s" % f
388 if not files:
389 print "\t(none)"
391 print "Files with unstaged changes:"
392 prefix = get_output("git rev-parse --show-prefix")
393 files = self._get_unstaged_files()
394 for f in files:
395 if prefix:
396 f = os.path.join(prefix[0], f)
397 print "\t%s" % f
398 if not files:
399 print "\t(none)"
401 @short_help("remove uncommitted changes from a file (*)")
402 @long_help("""
403 The arguments are the files whose changes will be reverted. If the '-a'
404 flag is given, then all files will have uncommitted changes removed.
405 Note that there is no way to reverse this command short of manually
406 editing each file again.
407 """)
408 @takes_options("a")
409 def cmd_revert(self, *files, **flags):
410 "(-a | <file>)"
411 if '-a' in flags:
412 os.system("git read-tree HEAD")
413 os.system("git checkout-index -f -a")
414 return
416 if not files:
417 raise TypeError
419 for f in files:
420 self._revert_one(f)
421 self.cmd_status()
423 @short_help("record changes to files as a new commit")
424 @long_help("""
425 Create a new commit recording changes since the last commit. If there
426 are only unstaged changes, those will be recorded. If there are only
427 staged changes, those will be recorder. Otherwise, you will have to
428 specify either the '-a' flag or the '-d' flag to commit all changes or
429 only staged changes, respectively. To reverse the effects of this
430 command, see 'uncommit'.
431 """)
432 @takes_options("ad")
433 def cmd_commit(self, **flags):
434 self._check_rebasing()
435 self._check_commit(**flags)
436 self._do_commit()
437 self.cmd_status()
439 @short_help("reverse the actions of the last commit")
440 @long_help("""
441 Reverse the effects of the last 'commit' operation. The changes that
442 were part of the previous commit will show as "staged changes" in the
443 output of 'status'. This means that if no files were changed since the
444 last commit was created, 'uncommit' followed by 'commit' is a lossless
445 operation.
446 """)
447 def cmd_uncommit(self):
448 self._do_uncommit()
449 self.cmd_status()
451 @short_help("report the current version of yap")
452 def cmd_version(self):
453 print "Yap version 0.1"
455 @short_help("show the changelog for particular versions or files")
456 @long_help("""
457 The arguments are the files with which to filter history. If none are
458 given, all changes are listed. Otherwise only commits that affected one
459 or more of the given files are listed. The -r option changes the
460 starting revision for traversing history. By default, history is listed
461 starting at HEAD.
462 """)
463 @takes_options("r:")
464 def cmd_log(self, *paths, **flags):
465 "[-r <rev>] <path>..."
466 rev = flags.get('-r', 'HEAD')
467 paths = ' '.join(paths)
468 os.system("git log --name-status '%s' -- %s" % (rev, paths))
470 @short_help("show staged, unstaged, or all uncommitted changes")
471 @long_help("""
472 Show staged, unstaged, or all uncommitted changes. By default, all
473 changes are shown. The '-u' flag causes only unstaged changes to be
474 shown. The '-d' flag causes only staged changes to be shown.
475 """)
476 @takes_options("ud")
477 def cmd_diff(self, **flags):
478 "[ -u | -d ]"
479 if '-u' in flags and '-d' in flags:
480 raise YapError("Conflicting flags: -u and -d")
482 pager = self._get_pager_cmd()
484 os.system("git update-index -q --refresh")
485 if '-u' in flags:
486 os.system("git diff-files -p | %s" % pager)
487 elif '-d' in flags:
488 os.system("git diff-index --cached -p HEAD | %s" % pager)
489 else:
490 os.system("git diff-index -p HEAD | %s" % pager)
492 @short_help("list, create, or delete branches")
493 @long_help("""
494 If no arguments are given, a list of local branches is given. The
495 current branch is indicated by a "*" next to the name. If an argument
496 is given, it is taken as the name of a new branch to create. The branch
497 will start pointing at the current HEAD. See 'point' for details on
498 changing the revision of the new branch. Note that this command does
499 not switch the current working branch. See 'switch' for details on
500 changing the current working branch.
502 The '-d' flag can be used to delete local branches. If the delete
503 operation would remove the last branch reference to a given line of
504 history (colloquially referred to as "dangling commits"), yap will
505 report an error and abort. The '-f' flag can be used to force the delete
506 in spite of this.
507 """)
508 @takes_options("fd:")
509 def cmd_branch(self, branch=None, **flags):
510 "[ [-f] -d <branch> | <branch> ]"
511 force = '-f' in flags
512 if '-d' in flags:
513 self._delete_branch(flags['-d'], force)
514 self.cmd_branch()
515 return
517 if branch is not None:
518 ref = get_output("git rev-parse HEAD")
519 if not ref:
520 raise YapError("No branch point yet. Make a commit")
521 os.system("git update-ref 'refs/heads/%s' '%s'" % (branch, ref[0]))
523 current = get_output("git symbolic-ref HEAD")[0]
524 branches = get_output("git for-each-ref --format='%(refname)' 'refs/heads/*'")
525 for b in branches:
526 if b == current:
527 print "* ",
528 else:
529 print " ",
530 b = b.replace('refs/heads/', '')
531 print b
533 @short_help("change the current working branch")
534 @long_help("""
535 The argument is the name of the branch to make the current working
536 branch. This command will fail if there are uncommitted changes to any
537 files. Otherwise, the contents of the files in the working directory
538 are updated to reflect their state in the new branch. Additionally, any
539 future commits are added to the new branch instead of the previous line
540 of history.
541 """)
542 def cmd_switch(self, branch):
543 "<branch>"
544 ref = get_output("git rev-parse 'refs/heads/%s'" % branch)
545 if not ref:
546 raise YapError("No such branch: %s" % branch)
548 # XXX: support merging like git-checkout
549 if self._get_unstaged_files() or self._get_staged_files():
550 raise YapError("You have uncommitted changes. Commit them first")
552 os.system("git symbolic-ref HEAD refs/heads/'%s'" % branch)
553 os.system("git read-tree HEAD")
554 os.system("git checkout-index -f -a")
555 self.cmd_branch()
557 @short_help("move the current branch to a different revision")
558 @long_help("""
559 The argument is the hash of the commit to which the current branch
560 should point, or alternately a branch or tag (a.k.a, "committish"). If
561 moving the branch would create "dangling commits" (see 'branch'), yap
562 will report an error and abort. The '-f' flag can be used to force the
563 operation in spite of this.
564 """)
565 @takes_options("f")
566 def cmd_point(self, where, **flags):
567 "<where>"
568 head = get_output("git rev-parse HEAD")
569 if not head:
570 raise YapError("No commit yet; nowhere to point")
572 ref = get_output("git rev-parse '%s'" % where)
573 if not ref:
574 raise YapError("Not a valid ref: %s" % where)
576 if self._get_unstaged_files() or self._get_staged_files():
577 raise YapError("You have uncommitted changes. Commit them first")
579 type = get_output("git cat-file -t '%s'" % ref[0])
580 if type and type[0] == "tag":
581 tag = get_output("git cat-file tag '%s'" % ref[0])
582 ref[0] = tag[0].split(' ')[1]
584 os.system("git update-ref HEAD '%s'" % ref[0])
586 if '-f' not in flags:
587 name = get_output("git name-rev --name-only '%s'" % head[0])[0]
588 if name == "undefined":
589 os.system("git update-ref HEAD '%s'" % head[0])
590 raise YapError("Pointing there will lose commits. Use -f to force")
592 os.system("git read-tree HEAD")
593 os.system("git checkout-index -f -a")
594 os.system("git update-index --refresh")
596 @short_help("alter history by dropping or amending commits")
597 @long_help("""
598 This command operates in two distinct modes, "amend" and "drop" mode.
599 In drop mode, the given commit is removed from the history of the
600 current branch, as though that commit never happened. By default the
601 commit used is HEAD.
603 In amend mode, the uncommitted changes present are merged into a
604 previous commit. This is useful for correcting typos or adding missed
605 files into past commits. By default the commit used is HEAD.
607 While rewriting history it is possible that conflicts will arise. If
608 this happens, the rewrite will pause and you will be prompted to resolve
609 the conflicts and staged them. Once that is done, you will run "yap
610 history continue." If instead you want the conflicting commit removed
611 from history (perhaps your changes supercede that commit) you can run
612 "yap history skip". Once the rewrite completes, your branch will be on
613 the same commit as when the rewrite started.
614 """)
615 def cmd_history(self, subcmd, *args):
616 "amend | drop <commit>"
618 if subcmd not in ("amend", "drop", "continue", "skip"):
619 raise TypeError
621 resolvemsg = """
622 When you have resolved the conflicts run \"yap history continue\".
623 To skip the problematic patch, run \"yap history skip\"."""
625 if subcmd == "continue":
626 os.system("git am -3 -r --resolvemsg='%s'" % resolvemsg)
627 return
628 if subcmd == "skip":
629 os.system("git reset --hard")
630 os.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg)
631 return
633 if subcmd == "amend":
634 flags, args = getopt.getopt(args, "ad")
635 flags = dict(flags)
637 if len(args) > 1:
638 raise TypeError
639 if args:
640 commit = args[0]
641 else:
642 commit = "HEAD"
644 if run_command("git rev-parse --verify '%s'" % commit):
645 raise YapError("Not a valid commit: %s" % commit)
647 self._check_rebasing()
649 if subcmd == "amend":
650 self._check_commit(**flags)
652 stash = get_output("git stash create")
653 run_command("git reset --hard")
655 if subcmd == "amend" and not stash:
656 raise YapError("Failed to stash; no changes?")
658 fd, tmpfile = tempfile.mkstemp("yap")
659 os.close(fd)
660 try:
661 os.system("git format-patch -k --stdout '%s' > %s" % (commit, tmpfile))
662 if subcmd == "amend":
663 self.cmd_point(commit, **{'-f': True})
664 run_command("git stash apply --index %s" % stash[0])
665 self._do_uncommit()
666 self._do_commit()
667 stash = get_output("git stash create")
668 run_command("git reset --hard")
669 else:
670 self.cmd_point("%s^" % commit, **{'-f': True})
672 stat = os.stat(tmpfile)
673 size = stat[6]
674 if size > 0:
675 rc = os.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg, tmpfile))
676 if (rc):
677 raise YapError("Failed to apply changes")
679 if stash:
680 run_command("git stash apply %s" % stash[0])
681 finally:
682 os.unlink(tmpfile)
683 self.cmd_status()
685 @short_help("show the changes introduced by a given commit")
686 @long_help("""
687 By default, the changes in the last commit are shown. To override this,
688 specify a hash, branch, or tag (committish). The hash of the commit,
689 the commit's author, log message, and a diff of the changes are shown.
690 """)
691 def cmd_show(self, commit="HEAD"):
692 "[commit]"
693 os.system("git show '%s'" % commit)
695 @short_help("apply the changes in a given commit to the current branch")
696 @long_help("""
697 The argument is the hash, branch, or tag (committish) of the commit to
698 be applied. In general, it only makes sense to apply commits that
699 happened on another branch. The '-r' flag can be used to have the
700 changes in the given commit reversed from the current branch. In
701 general, this only makes sense for commits that happened on the current
702 branch.
703 """)
704 @takes_options("r")
705 def cmd_cherry_pick(self, commit, **flags):
706 "[-r] <commit>"
707 if '-r' in flags:
708 os.system("git revert '%s'" % commit)
709 else:
710 os.system("git cherry-pick '%s'" % commit)
712 @short_help("list, add, or delete configured remote repositories")
713 @long_help("""
714 When invoked with no arguments, this command will show the list of
715 currently configured remote repositories, giving both the name and URL
716 of each. To add a new repository, give the desired name as the first
717 argument and the URL as the second. The '-d' flag can be used to remove
718 a previously added repository.
719 """)
720 @takes_options("d:")
721 def cmd_repo(self, name=None, url=None, **flags):
722 "[<name> <url> | -d <name>]"
723 if name is not None and url is None:
724 raise TypeError
726 if '-d' in flags:
727 if flags['-d'] not in self._list_remotes():
728 raise YapError("No such repository: %s" % flags['-d'])
729 os.system("git config --unset remote.%s.url" % flags['-d'])
730 os.system("git config --unset remote.%s.fetch" % flags['-d'])
732 if name:
733 if flags['-d'] in self._list_remotes():
734 raise YapError("Repository '%s' already exists" % flags['-d'])
735 os.system("git config remote.%s.url %s" % (name, url))
736 os.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name, url))
738 for remote, url in self._list_remotes():
739 print "%s:\t\t%s" % (remote, url)
741 def cmd_help(self, cmd=None):
742 if cmd is not None:
743 try:
744 attr = self.__getattribute__("cmd_"+cmd)
745 except AttributeError:
746 raise YapError("No such command: %s" % cmd)
747 try:
748 help = attr.long_help
749 except AttributeError:
750 raise YapError("Sorry, no help for '%s'. Ask Steven." % cmd)
752 print >>sys.stderr, "The '%s' command" % cmd
753 print >>sys.stderr, "\tyap %s %s" % (cmd, attr.__doc__)
754 print >>sys.stderr, "%s" % help
755 return
757 print >> sys.stderr, "Yet Another (Git) Porcelein"
758 print >> sys.stderr
760 for name in dir(self):
761 if not name.startswith('cmd_'):
762 continue
763 attr = self.__getattribute__(name)
764 if not callable(attr):
765 continue
766 try:
767 short_msg = attr.short_help
768 except AttributeError:
769 continue
771 name = name.replace('cmd_', '')
772 name = name.replace('_', '-')
773 print >> sys.stderr, "%-16s%s" % (name, short_msg)
774 print >> sys.stderr
775 print >> sys.stderr, "(*) Indicates that the command is not readily reversible"
777 def cmd_usage(self):
778 print >> sys.stderr, "usage: %s <command>" % sys.argv[0]
779 print >> sys.stderr, " valid commands: help init add rm stage unstage status revert commit uncommit log show diff branch switch point cherry-pick history version"
781 def main(self, args):
782 if len(args) < 1:
783 self.cmd_usage()
784 sys.exit(2)
786 command = args[0]
787 args = args[1:]
789 debug = os.getenv('YAP_DEBUG')
791 try:
792 command = command.replace('-', '_')
793 meth = self.__getattribute__("cmd_"+command)
794 try:
795 if "options" in meth.__dict__:
796 flags, args = getopt.getopt(args, meth.options)
797 flags = dict(flags)
798 else:
799 flags = dict()
801 meth(*args, **flags)
802 except (TypeError, getopt.GetoptError):
803 if debug:
804 raise
805 print "%s %s %s" % (sys.argv[0], command, meth.__doc__)
806 except YapError, e:
807 print >> sys.stderr, e
808 sys.exit(1)
809 except AttributeError:
810 if debug:
811 raise
812 self.cmd_usage()
813 sys.exit(2)