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