Implement commit
[yap.git] / yap / yap.py
blob041de83b7398eb2c052d374aa3495be34d8e8284
1 import sys
2 import os
3 import getopt
4 import pickle
5 import tempfile
7 def get_output(cmd):
8 fd = os.popen(cmd)
9 output = fd.readlines()
10 rc = fd.close()
11 return [x.strip() for x in output]
13 def run_command(cmd):
14 rc = os.system("%s > /dev/null 2>&1" % cmd)
15 rc >>= 8
16 return rc
18 class YapError(Exception):
19 def __init__(self, msg):
20 self.msg = msg
22 def __str__(self):
23 return self.msg
25 def takes_options(options):
26 def decorator(func):
27 func.options = options
28 return func
29 return decorator
31 class Yap(object):
32 def _add_new_file(self, file):
33 repo = get_output('git rev-parse --git-dir')[0]
34 dir = os.path.join(repo, 'yap')
35 try:
36 os.mkdir(dir)
37 except OSError:
38 pass
39 files = self._get_new_files()
40 files.append(file)
41 path = os.path.join(dir, 'new-files')
42 pickle.dump(files, open(path, 'w'))
44 def _get_new_files(self):
45 repo = get_output('git rev-parse --git-dir')[0]
46 path = os.path.join(repo, 'yap', 'new-files')
47 try:
48 files = pickle.load(file(path))
49 except IOError:
50 files = []
52 x = []
53 for f in files:
54 # if f in the index
55 if get_output("git ls-files --cached '%s'" % f) != []:
56 continue
57 x.append(f)
58 return x
60 def _remove_new_file(self, file):
61 files = self._get_new_files()
62 files = filter(lambda x: x != file, files)
64 repo = get_output('git rev-parse --git-dir')[0]
65 path = os.path.join(repo, 'yap', 'new-files')
66 pickle.dump(files, open(path, 'w'))
68 def _clear_new_files(self):
69 repo = get_output('git rev-parse --git-dir')[0]
70 path = os.path.join(repo, 'yap', 'new-files')
71 os.unlink(path)
73 def _assert_file_exists(self, file):
74 if not os.access(file, os.R_OK):
75 raise YapError("No such file: %s" % file)
77 def _get_staged_files(self):
78 if run_command("git rev-parse HEAD"):
79 files = get_output("git ls-files --cached")
80 else:
81 files = get_output("git diff-index --cached --name-only HEAD")
82 return files
84 def _get_unstaged_files(self):
85 files = self._get_new_files()
86 files += get_output("git ls-files -m")
87 return files
89 def cmd_clone(self, url, directory=""):
90 # XXX: implement in terms of init + remote add + fetch
91 os.system("git clone '%s' %s" % (url, directory))
93 def cmd_init(self):
94 os.system("git init")
96 def cmd_add(self, file):
97 self._assert_file_exists(file)
98 x = get_output("git ls-files '%s'" % file)
99 if x != []:
100 raise YapError("File '%s' already in repository" % file)
101 self._add_new_file(file)
102 self.cmd_status()
104 def cmd_rm(self, file):
105 self._assert_file_exists(file)
106 if get_output("git ls-files '%s'" % file) != []:
107 os.system("git rm --cached '%s'" % file)
108 self._remove_new_file(file)
109 self.cmd_status()
111 def cmd_stage(self, file, quiet=False):
112 self._assert_file_exists(file)
113 os.system("git update-index --add '%s'" % file)
114 if not quiet:
115 self.cmd_status()
117 def cmd_unstage(self, file):
118 self._assert_file_exists(file)
119 if run_command("git rev-parse HEAD"):
120 os.system("git update-index --force-remove '%s'" % file)
121 else:
122 os.system("git diff-index HEAD '%s' | git apply -R --cached" % file)
123 self.cmd_status()
125 def cmd_status(self):
126 branch = get_output("git symbolic-ref HEAD")[0]
127 branch = branch.replace('refs/heads/', '')
128 print "Current branch: %s" % branch
130 print "Files with staged changes:"
131 files = self._get_staged_files()
132 for f in files:
133 print "\t%s" % f
134 if not files:
135 print "\t(none)"
137 print "Files with unstaged changes:"
138 files = self._get_unstaged_files()
139 for f in files:
140 print "\t%s" % f
141 if not files:
142 print "\t(none)"
144 def cmd_unedit(self, file):
145 self._assert_file_exists(file)
146 os.system("git checkout-index -f '%s'" % file)
147 self.cmd_status()
149 def cmd_commit(self):
150 if self._get_unstaged_files():
151 if self._get_staged_files():
152 raise YapError("Staged and unstaged changes present. Specify what to commit")
153 os.system("git diff-files -p | git apply --cached 2>/dev/null")
154 for f in self._get_new_files():
155 self.cmd_stage(f, True)
157 if not self._get_staged_files():
158 raise YapError("No changes to commit")
160 tree = get_output("git write-tree")[0]
162 parent = get_output("git rev-parse HEAD 2> /dev/null")[0]
164 if os.environ.has_key('YAP_EDITOR'):
165 editor = os.environ['YAP_EDITOR']
166 elif os.environ.has_key('GIT_EDITOR'):
167 editor = os.environ['GIT_EDITOR']
168 elif os.environ.has_key('EDITOR'):
169 editor = os.environ['EDITOR']
170 else:
171 editor = "vi"
173 fd, tmpfile = tempfile.mkstemp("yap")
174 os.close(fd)
175 if os.system("%s '%s'" % (editor, tmpfile)) != 0:
176 raise YapError("Editing commit message failed")
177 if parent != 'HEAD':
178 commit = get_output("git commit-tree '%s' -p '%s' < '%s'" % (tree, parent, tmpfile))
179 else:
180 commit = get_output("git commit-tree '%s' < '%s'" % (tree, tmpfile))
181 if not commit:
182 raise YapError("Commit failed; no log message?")
183 os.unlink(tmpfile)
184 os.system("git update-ref HEAD '%s'" % commit[0])
186 def cmd_version(self):
187 print "Yap version 0.1"
189 def cmd_usage(self):
190 print >> sys.stderr, "usage: %s <command>" % sys.argv[0]
191 print >> sys.stderr, " valid commands: version"
193 def main(self, args):
194 if len(args) < 1:
195 self.cmd_usage()
196 sys.exit(2)
198 command = args[0]
199 args = args[1:]
201 debug = os.getenv('YAP_DEBUG')
203 try:
204 meth = self.__getattribute__("cmd_"+command)
205 try:
206 if "option" in meth.__dict__:
207 flags, args = getopt.getopt(args, meth.options)
208 flags = dict(flags)
209 else:
210 flags = dict()
212 meth(*args, **flags)
213 except (TypeError, getopt.GetoptError):
214 if debug:
215 raise
216 print "%s %s %s" % (sys.argv[0], command, meth.__doc__)
217 except YapError, e:
218 print >> sys.stderr, e
219 sys.exit(1)
220 except AttributeError:
221 if debug:
222 raise
223 self.cmd_usage()
224 sys.exit(2)