cmd_unstage: work even when there are unstaged changes
[yap.git] / yap / yap.py
blob32cc9e4f12c64e64bbd62e9c798197b55f06f943
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 try:
343 run_safely("git read-tree -m HEAD")
344 except ShellError:
345 run_safely("git read-tree HEAD")
346 run_safely("git update-index -q --refresh")
347 self.cmd_status()
348 return
350 if not files:
351 raise TypeError
353 for f in files:
354 self._unstage_one(f)
355 self.cmd_status()
357 @short_help("show files with staged and unstaged changes")
358 @long_help("""
359 Show the files in the repository with changes since the last commit,
360 categorized based on whether the changes are staged or not. A file may
361 appear under each heading if the same file has both staged and unstaged
362 changes.
363 """)
364 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 -u -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 recorded. 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 "[-a | -d]"
422 self._check_rebasing()
423 self._check_commit(**flags)
424 if not self._get_staged_files():
425 raise YapError("No changes to commit")
426 msg = flags.get('-m', None)
427 self._do_commit(msg)
428 self.cmd_status()
430 @short_help("reverse the actions of the last commit")
431 @long_help("""
432 Reverse the effects of the last 'commit' operation. The changes that
433 were part of the previous commit will show as "staged changes" in the
434 output of 'status'. This means that if no files were changed since the
435 last commit was created, 'uncommit' followed by 'commit' is a lossless
436 operation.
437 """)
438 def cmd_uncommit(self):
440 self._do_uncommit()
441 self.cmd_status()
443 @short_help("report the current version of yap")
444 def cmd_version(self):
445 print "Yap version 0.1"
447 @short_help("show the changelog for particular versions or files")
448 @long_help("""
449 The arguments are the files with which to filter history. If none are
450 given, all changes are listed. Otherwise only commits that affected one
451 or more of the given files are listed. The -r option changes the
452 starting revision for traversing history. By default, history is listed
453 starting at HEAD.
454 """)
455 @takes_options("r:")
456 def cmd_log(self, *paths, **flags):
457 "[-r <rev>] <path>..."
458 rev = flags.get('-r', 'HEAD')
459 paths = ' '.join(paths)
460 os.system("git log --name-status '%s' -- %s" % (rev, paths))
462 @short_help("show staged, unstaged, or all uncommitted changes")
463 @long_help("""
464 Show staged, unstaged, or all uncommitted changes. By default, all
465 changes are shown. The '-u' flag causes only unstaged changes to be
466 shown. The '-d' flag causes only staged changes to be shown.
467 """)
468 @takes_options("ud")
469 def cmd_diff(self, **flags):
470 "[ -u | -d ]"
471 if '-u' in flags and '-d' in flags:
472 raise YapError("Conflicting flags: -u and -d")
474 pager = self._get_pager_cmd()
476 if '-u' in flags:
477 os.system("git diff-files -p | %s" % pager)
478 elif '-d' in flags:
479 os.system("git diff-index --cached -p HEAD | %s" % pager)
480 else:
481 os.system("git diff-index -p HEAD | %s" % pager)
483 @short_help("list, create, or delete branches")
484 @long_help("""
485 If no arguments are specified, a list of local branches is given. The
486 current branch is indicated by a "*" next to the name. If an argument
487 is given, it is taken as the name of a new branch to create. The branch
488 will start pointing at the current HEAD. See 'point' for details on
489 changing the revision of the new branch. Note that this command does
490 not switch the current working branch. See 'switch' for details on
491 changing the current working branch.
493 The '-d' flag can be used to delete local branches. If the delete
494 operation would remove the last branch reference to a given line of
495 history (colloquially referred to as "dangling commits"), yap will
496 report an error and abort. The '-f' flag can be used to force the delete
497 in spite of this.
498 """)
499 @takes_options("fd:")
500 def cmd_branch(self, branch=None, **flags):
501 "[ [-f] -d <branch> | <branch> ]"
502 force = '-f' in flags
503 if '-d' in flags:
504 self._delete_branch(flags['-d'], force)
505 self.cmd_branch()
506 return
508 if branch is not None:
509 ref = get_output("git rev-parse --verify HEAD")
510 if not ref:
511 raise YapError("No branch point yet. Make a commit")
512 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch, ref[0]))
514 current = get_output("git symbolic-ref HEAD")[0]
515 branches = get_output("git for-each-ref --format='%(refname)' 'refs/heads/*'")
516 for b in branches:
517 if b == current:
518 print "* ",
519 else:
520 print " ",
521 b = b.replace('refs/heads/', '')
522 print b
524 @short_help("change the current working branch")
525 @long_help("""
526 The argument is the name of the branch to make the current working
527 branch. This command will fail if there are uncommitted changes to any
528 files. Otherwise, the contents of the files in the working directory
529 are updated to reflect their state in the new branch. Additionally, any
530 future commits are added to the new branch instead of the previous line
531 of history.
532 """)
533 def cmd_switch(self, branch):
534 "<branch>"
535 ref = get_output("git rev-parse --verify 'refs/heads/%s'" % branch)
536 if not ref:
537 raise YapError("No such branch: %s" % branch)
539 # XXX: support merging like git-checkout
540 if self._get_unstaged_files() or self._get_staged_files():
541 raise YapError("You have uncommitted changes. Commit them first")
543 run_safely("git symbolic-ref HEAD refs/heads/'%s'" % branch)
544 run_safely("git read-tree -u -m HEAD")
545 run_safely("git checkout-index -u -f -a")
546 self.cmd_branch()
548 @short_help("move the current branch to a different revision")
549 @long_help("""
550 The argument is the hash of the commit to which the current branch
551 should point, or alternately a branch or tag (a.k.a, "committish"). If
552 moving the branch would create "dangling commits" (see 'branch'), yap
553 will report an error and abort. The '-f' flag can be used to force the
554 operation in spite of this.
555 """)
556 @takes_options("f")
557 def cmd_point(self, where, **flags):
558 "<where>"
559 head = get_output("git rev-parse --verify HEAD")
560 if not head:
561 raise YapError("No commit yet; nowhere to point")
563 ref = get_output("git rev-parse --verify '%s'" % where)
564 if not ref:
565 raise YapError("Not a valid ref: %s" % where)
567 if self._get_unstaged_files() or self._get_staged_files():
568 raise YapError("You have uncommitted changes. Commit them first")
570 type = get_output("git cat-file -t '%s'" % ref[0])
571 if type and type[0] == "tag":
572 tag = get_output("git cat-file tag '%s'" % ref[0])
573 ref[0] = tag[0].split(' ')[1]
575 run_safely("git update-ref HEAD '%s'" % ref[0])
577 if '-f' not in flags:
578 name = get_output("git name-rev --name-only '%s'" % head[0])[0]
579 if name == "undefined":
580 os.system("git update-ref HEAD '%s'" % head[0])
581 raise YapError("Pointing there will lose commits. Use -f to force")
583 run_safely("git read-tree -u -m HEAD")
584 run_safely("git checkout-index -u -f -a")
586 @short_help("alter history by dropping or amending commits")
587 @long_help("""
588 This command operates in two distinct modes, "amend" and "drop" mode.
589 In drop mode, the given commit is removed from the history of the
590 current branch, as though that commit never happened. By default the
591 commit used is HEAD.
593 In amend mode, the uncommitted changes present are merged into a
594 previous commit. This is useful for correcting typos or adding missed
595 files into past commits. By default the commit used is HEAD.
597 While rewriting history it is possible that conflicts will arise. If
598 this happens, the rewrite will pause and you will be prompted to resolve
599 the conflicts and stage them. Once that is done, you will run "yap
600 history continue." If instead you want the conflicting commit removed
601 from history (perhaps your changes supercede that commit) you can run
602 "yap history skip". Once the rewrite completes, your branch will be on
603 the same commit as when the rewrite started.
604 """)
605 def cmd_history(self, subcmd, *args):
606 "amend | drop <commit>"
608 if subcmd not in ("amend", "drop", "continue", "skip"):
609 raise TypeError
611 resolvemsg = """
612 When you have resolved the conflicts run \"yap history continue\".
613 To skip the problematic patch, run \"yap history skip\"."""
615 if subcmd == "continue":
616 os.system("git am -3 -r --resolvemsg='%s'" % resolvemsg)
617 return
618 if subcmd == "skip":
619 os.system("git reset --hard")
620 os.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg)
621 return
623 if subcmd == "amend":
624 flags, args = getopt.getopt(args, "ad")
625 flags = dict(flags)
627 if len(args) > 1:
628 raise TypeError
629 if args:
630 commit = args[0]
631 else:
632 commit = "HEAD"
634 if run_command("git rev-parse --verify '%s'" % commit):
635 raise YapError("Not a valid commit: %s" % commit)
637 self._check_rebasing()
639 if subcmd == "amend":
640 self._check_commit(**flags)
641 if self._get_unstaged_files():
642 # XXX: handle unstaged changes better
643 raise YapError("Commit away changes that you aren't amending")
645 try:
646 stash = get_output("git stash create")
647 run_command("git reset --hard")
648 if subcmd == "amend" and not stash:
649 raise YapError("Failed to stash; no changes?")
651 try:
652 fd, tmpfile = tempfile.mkstemp("yap")
653 os.close(fd)
654 os.system("git format-patch -k --stdout '%s' > %s" % (commit, tmpfile))
655 if subcmd == "amend":
656 self.cmd_point(commit, **{'-f': True})
657 finally:
658 if subcmd == "amend":
659 rc = os.system("git stash apply --index %s" % stash[0])
660 if rc:
661 raise YapError("Failed to apply stash")
663 try:
664 if subcmd == "amend":
665 self._do_uncommit()
666 self._do_commit()
667 else:
668 self.cmd_point("%s^" % commit, **{'-f': True})
670 stat = os.stat(tmpfile)
671 size = stat[6]
672 if size > 0:
673 rc = os.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg, tmpfile))
674 if (rc):
675 raise YapError("Failed to apply changes")
676 finally:
677 if stash:
678 run_command("git stash apply --index %s" % stash[0])
679 finally:
680 os.unlink(tmpfile)
681 self.cmd_status()
683 @short_help("show the changes introduced by a given commit")
684 @long_help("""
685 By default, the changes in the last commit are shown. To override this,
686 specify a hash, branch, or tag (committish). The hash of the commit,
687 the commit's author, log message, and a diff of the changes are shown.
688 """)
689 def cmd_show(self, commit="HEAD"):
690 "[commit]"
691 os.system("git show '%s'" % commit)
693 @short_help("apply the changes in a given commit to the current branch")
694 @long_help("""
695 The argument is the hash, branch, or tag (committish) of the commit to
696 be applied. In general, it only makes sense to apply commits that
697 happened on another branch. The '-r' flag can be used to have the
698 changes in the given commit reversed from the current branch. In
699 general, this only makes sense for commits that happened on the current
700 branch.
701 """)
702 @takes_options("r")
703 def cmd_cherry_pick(self, commit, **flags):
704 "[-r] <commit>"
705 if '-r' in flags:
706 os.system("git revert '%s'" % commit)
707 else:
708 os.system("git cherry-pick '%s'" % commit)
710 @short_help("list, add, or delete configured remote repositories")
711 @long_help("""
712 When invoked with no arguments, this command will show the list of
713 currently configured remote repositories, giving both the name and URL
714 of each. To add a new repository, give the desired name as the first
715 argument and the URL as the second. The '-d' flag can be used to remove
716 a previously added repository.
717 """)
718 @takes_options("d:")
719 def cmd_repo(self, name=None, url=None, **flags):
720 "[<name> <url> | -d <name>]"
721 if name is not None and url is None:
722 raise TypeError
724 if '-d' in flags:
725 if flags['-d'] not in self._list_remotes():
726 raise YapError("No such repository: %s" % flags['-d'])
727 os.system("git config --unset remote.%s.url" % flags['-d'])
728 os.system("git config --unset remote.%s.fetch" % flags['-d'])
730 if name:
731 if flags['-d'] in self._list_remotes():
732 raise YapError("Repository '%s' already exists" % flags['-d'])
733 os.system("git config remote.%s.url %s" % (name, url))
734 os.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name, url))
736 for remote, url in self._list_remotes():
737 print "%-20s %s" % (remote, url)
739 @takes_options("cd")
740 def cmd_push(self, repo, **flags):
741 "[-c | -d] <repo>"
743 if repo not in self._list_remotes():
744 raise YapError("No such repository: %s" % repo)
746 current = get_output("git symbolic-ref HEAD")[0]
747 ref = current
748 current = current.replace('refs/heads/', '')
749 remote = get_output("git config branch.%s.remote" % current)
750 if remote and remote[0] == repo:
751 merge = get_output("git config branch.%s.merge" % current)
752 if merge:
753 ref = merge[0]
755 if '-c' not in flags and '-d' not in flags:
756 if run_command("git rev-parse --verify refs/remotes/%s/%s"
757 % (remote, ref.replace('refs/heads/', ''))):
758 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
760 if '-d' in flags:
761 lhs = ""
762 else:
763 lhs = "refs/heads/%s" % current
764 rc = os.system("git push %s %s:%s" % (repo, lhs, ref))
765 if rc:
766 raise YapError("Push failed.")
769 def cmd_help(self, cmd=None):
770 if cmd is not None:
771 try:
772 attr = self.__getattribute__("cmd_"+cmd.replace('-', '_'))
773 except AttributeError:
774 raise YapError("No such command: %s" % cmd)
775 try:
776 help = attr.long_help
777 except AttributeError:
778 raise YapError("Sorry, no help for '%s'. Ask Steven." % cmd)
780 print >>sys.stderr, "The '%s' command" % cmd
781 print >>sys.stderr, "\tyap %s %s" % (cmd, attr.__doc__)
782 print >>sys.stderr, "%s" % help
783 return
785 print >> sys.stderr, "Yet Another (Git) Porcelein"
786 print >> sys.stderr
788 for name in dir(self):
789 if not name.startswith('cmd_'):
790 continue
791 attr = self.__getattribute__(name)
792 if not callable(attr):
793 continue
794 try:
795 short_msg = attr.short_help
796 except AttributeError:
797 continue
799 name = name.replace('cmd_', '')
800 name = name.replace('_', '-')
801 print >> sys.stderr, "%-16s%s" % (name, short_msg)
802 print >> sys.stderr
803 print >> sys.stderr, "(*) Indicates that the command is not readily reversible"
805 def cmd_usage(self):
806 print >> sys.stderr, "usage: %s <command>" % os.path.basename(sys.argv[0])
807 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"
809 def main(self, args):
810 if len(args) < 1:
811 self.cmd_usage()
812 sys.exit(2)
814 command = args[0]
815 args = args[1:]
817 debug = os.getenv('YAP_DEBUG')
819 try:
820 command = command.replace('-', '_')
822 meth = None
823 for p in self.plugins:
824 try:
825 meth = p.__getattribute__("cmd_"+command)
826 except AttributeError:
827 continue
829 try:
830 default_meth = self.__getattribute__("cmd_"+command)
831 except AttributeError:
832 default_meth = None
834 if meth is None:
835 meth = default_meth
836 if meth is None:
837 raise AttributeError
839 try:
840 if "options" in meth.__dict__:
841 options = meth.options
842 if default_meth and "options" in default_meth.__dict__:
843 options += default_meth.options
844 flags, args = getopt.getopt(args, options)
845 flags = dict(flags)
846 else:
847 flags = dict()
849 # invoke pre-hooks
850 for p in self.plugins:
851 try:
852 meth = p.__getattribute__("pre_"+command)
853 except AttributeError:
854 continue
855 meth(*args, **flags)
857 meth(*args, **flags)
859 # invoke post-hooks
860 for p in self.plugins:
861 try:
862 meth = p.__getattribute__("post_"+command)
863 except AttributeError:
864 continue
865 meth()
867 except (TypeError, getopt.GetoptError):
868 if debug:
869 raise
870 print "%s %s %s" % (sys.argv[0], command, meth.__doc__)
871 except YapError, e:
872 print >> sys.stderr, e
873 sys.exit(1)
874 except AttributeError:
875 if debug:
876 raise
877 self.cmd_usage()
878 sys.exit(2)