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