Implement log, diff, branch
[yap.git] / yap / yap.py
blobfed5798e7061795d385ae4d8361ca4a5c951985d
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 _delete_branch(self, branch, force):
90 current = get_output("git symbolic-ref HEAD")[0]
91 current = current.replace('refs/heads/', '')
92 if branch == current:
93 raise YapError("Can't delete current branch")
95 ref = get_output("git rev-parse 'refs/heads/%s'" % branch)
96 if not ref:
97 raise YapError("No such branch: %s" % branch)
98 os.system("git update-ref -d 'refs/heads/%s' '%s'" % (branch, ref[0]))
100 if not force:
101 name = get_output("git name-rev --name-only '%s'" % ref[0])[0]
102 if name == 'undefined':
103 os.system("git update-ref 'refs/heads/%s' '%s'" % (branch, ref[0]))
104 raise YapError("Refusing to delete leaf branch (use -f to force)")
106 def cmd_clone(self, url, directory=""):
107 "<url> [directory]"
108 # XXX: implement in terms of init + remote add + fetch
109 os.system("git clone '%s' %s" % (url, directory))
111 def cmd_init(self):
112 os.system("git init")
114 def cmd_add(self, file):
115 "<file>"
116 self._assert_file_exists(file)
117 x = get_output("git ls-files '%s'" % file)
118 if x != []:
119 raise YapError("File '%s' already in repository" % file)
120 self._add_new_file(file)
121 self.cmd_status()
123 def cmd_rm(self, file):
124 "<file>"
125 self._assert_file_exists(file)
126 if get_output("git ls-files '%s'" % file) != []:
127 os.system("git rm --cached '%s'" % file)
128 self._remove_new_file(file)
129 self.cmd_status()
131 def cmd_stage(self, file, quiet=False):
132 "<file>"
133 self._assert_file_exists(file)
134 os.system("git update-index --add '%s'" % file)
135 if not quiet:
136 self.cmd_status()
138 def cmd_unstage(self, file):
139 "<file>"
140 self._assert_file_exists(file)
141 if run_command("git rev-parse HEAD"):
142 os.system("git update-index --force-remove '%s'" % file)
143 else:
144 os.system("git diff-index -p HEAD '%s' | git apply -R --cached" % file)
145 self.cmd_status()
147 def cmd_status(self):
148 branch = get_output("git symbolic-ref HEAD")[0]
149 branch = branch.replace('refs/heads/', '')
150 print "Current branch: %s" % branch
152 print "Files with staged changes:"
153 files = self._get_staged_files()
154 for f in files:
155 print "\t%s" % f
156 if not files:
157 print "\t(none)"
159 print "Files with unstaged changes:"
160 files = self._get_unstaged_files()
161 for f in files:
162 print "\t%s" % f
163 if not files:
164 print "\t(none)"
166 def cmd_unedit(self, file):
167 "<file>"
168 self._assert_file_exists(file)
169 os.system("git checkout-index -f '%s'" % file)
170 self.cmd_status()
172 @takes_options("ad")
173 def cmd_commit(self, **flags):
174 if '-a' in flags and '-d' in flags:
175 raise YapError("Conflicting flags: -a and -d")
177 if '-d' not in flags and self._get_unstaged_files():
178 if '-a' not in flags and self._get_staged_files():
179 raise YapError("Staged and unstaged changes present. Specify what to commit")
180 os.system("git diff-files -p | git apply --cached 2>/dev/null")
181 for f in self._get_new_files():
182 self.cmd_stage(f, True)
184 if not self._get_staged_files():
185 raise YapError("No changes to commit")
187 tree = get_output("git write-tree")[0]
189 parent = get_output("git rev-parse HEAD 2> /dev/null")[0]
191 if os.environ.has_key('YAP_EDITOR'):
192 editor = os.environ['YAP_EDITOR']
193 elif os.environ.has_key('GIT_EDITOR'):
194 editor = os.environ['GIT_EDITOR']
195 elif os.environ.has_key('EDITOR'):
196 editor = os.environ['EDITOR']
197 else:
198 editor = "vi"
200 fd, tmpfile = tempfile.mkstemp("yap")
201 os.close(fd)
202 if os.system("%s '%s'" % (editor, tmpfile)) != 0:
203 raise YapError("Editing commit message failed")
204 if parent != 'HEAD':
205 commit = get_output("git commit-tree '%s' -p '%s' < '%s'" % (tree, parent, tmpfile))
206 else:
207 commit = get_output("git commit-tree '%s' < '%s'" % (tree, tmpfile))
208 if not commit:
209 raise YapError("Commit failed; no log message?")
210 os.unlink(tmpfile)
211 os.system("git update-ref HEAD '%s'" % commit[0])
212 self.cmd_status()
214 def cmd_uncommit(self):
215 tree = get_output("git rev-parse HEAD^")
216 os.system("git read-tree '%s'" % tree[0])
217 self.cmd_status()
219 def cmd_version(self):
220 print "Yap version 0.1"
222 @takes_options("r:")
223 def cmd_log(self, *paths, **flags):
224 rev = flags.get('-r', 'HEAD')
225 paths = ' '.join(paths)
226 os.system("git log --name-status '%s' -- %s" % (rev, paths))
228 @takes_options("ud")
229 def cmd_diff(self, **flags):
230 if '-u' in flags and '-d' in flags:
231 raise YapError("Conflicting flags: -u and -d")
233 os.system("git update-index -q --refresh")
234 if '-u' in flags:
235 os.system("git diff-files -p")
236 elif '-d' in flags:
237 os.system("git diff-index --cached -p HEAD")
238 else:
239 os.system("git diff-index -p HEAD")
241 @takes_options("fd:")
242 def cmd_branch(self, branch=None, **flags):
243 force = '-f' in flags
244 if '-d' in flags:
245 self._delete_branch(flags['-d'], force)
246 self.cmd_branch()
247 return
249 if branch is not None:
250 ref = get_output("git rev-parse HEAD")
251 if not ref:
252 raise YapError("No branch point yet. Make a commit")
253 os.system("git update-ref 'refs/heads/%s' '%s'" % (branch, ref[0]))
255 current = get_output("git symbolic-ref HEAD")[0]
256 branches = get_output("git for-each-ref --format='%(refname)' 'refs/heads/*'")
257 for b in branches:
258 if b == current:
259 print "* ",
260 else:
261 print " ",
262 b = b.replace('refs/heads/', '')
263 print b
265 def cmd_usage(self):
266 print >> sys.stderr, "usage: %s <command>" % sys.argv[0]
267 print >> sys.stderr, " valid commands: version"
269 def main(self, args):
270 if len(args) < 1:
271 self.cmd_usage()
272 sys.exit(2)
274 command = args[0]
275 args = args[1:]
277 debug = os.getenv('YAP_DEBUG')
279 try:
280 meth = self.__getattribute__("cmd_"+command)
281 try:
282 if "options" in meth.__dict__:
283 flags, args = getopt.getopt(args, meth.options)
284 flags = dict(flags)
285 else:
286 flags = dict()
288 meth(*args, **flags)
289 except (TypeError, getopt.GetoptError):
290 if debug:
291 raise
292 print "%s %s %s" % (sys.argv[0], command, meth.__doc__)
293 except YapError, e:
294 print >> sys.stderr, e
295 sys.exit(1)
296 except AttributeError:
297 if debug:
298 raise
299 self.cmd_usage()
300 sys.exit(2)