Remove a newline from the e-mail template
[stgit] / stgit / lib / git.py
CommitLineData
cbe4567e
KH
1import os, os.path, re
2from stgit import exception, run, utils
3
4class RepositoryException(exception.StgException):
5 pass
6
7class DetachedHeadException(RepositoryException):
8 def __init__(self):
9 RepositoryException.__init__(self, 'Not on any branch')
10
11class Repr(object):
12 def __repr__(self):
13 return str(self)
14
15class NoValue(object):
16 pass
17
18def make_defaults(defaults):
19 def d(val, attr):
20 if val != NoValue:
21 return val
22 elif defaults != NoValue:
23 return getattr(defaults, attr)
24 else:
25 return None
26 return d
27
28class Person(Repr):
29 """Immutable."""
30 def __init__(self, name = NoValue, email = NoValue,
31 date = NoValue, defaults = NoValue):
32 d = make_defaults(defaults)
33 self.__name = d(name, 'name')
34 self.__email = d(email, 'email')
35 self.__date = d(date, 'date')
36 name = property(lambda self: self.__name)
37 email = property(lambda self: self.__email)
38 date = property(lambda self: self.__date)
39 def set_name(self, name):
40 return type(self)(name = name, defaults = self)
41 def set_email(self, email):
42 return type(self)(email = email, defaults = self)
43 def set_date(self, date):
44 return type(self)(date = date, defaults = self)
45 def __str__(self):
46 return '%s <%s> %s' % (self.name, self.email, self.date)
47 @classmethod
48 def parse(cls, s):
49 m = re.match(r'^([^<]*)<([^>]*)>\s+(\d+\s+[+-]\d{4})$', s)
50 assert m
51 name = m.group(1).strip()
52 email = m.group(2)
53 date = m.group(3)
54 return cls(name, email, date)
55
56class Tree(Repr):
57 """Immutable."""
58 def __init__(self, sha1):
59 self.__sha1 = sha1
60 sha1 = property(lambda self: self.__sha1)
61 def __str__(self):
62 return 'Tree<%s>' % self.sha1
63
64class Commitdata(Repr):
65 """Immutable."""
66 def __init__(self, tree = NoValue, parents = NoValue, author = NoValue,
67 committer = NoValue, message = NoValue, defaults = NoValue):
68 d = make_defaults(defaults)
69 self.__tree = d(tree, 'tree')
70 self.__parents = d(parents, 'parents')
71 self.__author = d(author, 'author')
72 self.__committer = d(committer, 'committer')
73 self.__message = d(message, 'message')
74 tree = property(lambda self: self.__tree)
75 parents = property(lambda self: self.__parents)
76 @property
77 def parent(self):
78 assert len(self.__parents) == 1
79 return self.__parents[0]
80 author = property(lambda self: self.__author)
81 committer = property(lambda self: self.__committer)
82 message = property(lambda self: self.__message)
83 def set_tree(self, tree):
84 return type(self)(tree = tree, defaults = self)
85 def set_parents(self, parents):
86 return type(self)(parents = parents, defaults = self)
87 def add_parent(self, parent):
88 return type(self)(parents = list(self.parents or []) + [parent],
89 defaults = self)
90 def set_parent(self, parent):
91 return self.set_parents([parent])
92 def set_author(self, author):
93 return type(self)(author = author, defaults = self)
94 def set_committer(self, committer):
95 return type(self)(committer = committer, defaults = self)
96 def set_message(self, message):
97 return type(self)(message = message, defaults = self)
98 def __str__(self):
99 if self.tree == None:
100 tree = None
101 else:
102 tree = self.tree.sha1
103 if self.parents == None:
104 parents = None
105 else:
106 parents = [p.sha1 for p in self.parents]
107 return ('Commitdata<tree: %s, parents: %s, author: %s,'
108 ' committer: %s, message: "%s">'
109 ) % (tree, parents, self.author, self.committer, self.message)
110 @classmethod
111 def parse(cls, repository, s):
112 cd = cls()
113 lines = list(s.splitlines(True))
114 for i in xrange(len(lines)):
115 line = lines[i].strip()
116 if not line:
117 return cd.set_message(''.join(lines[i+1:]))
118 key, value = line.split(None, 1)
119 if key == 'tree':
120 cd = cd.set_tree(repository.get_tree(value))
121 elif key == 'parent':
122 cd = cd.add_parent(repository.get_commit(value))
123 elif key == 'author':
124 cd = cd.set_author(Person.parse(value))
125 elif key == 'committer':
126 cd = cd.set_committer(Person.parse(value))
127 else:
128 assert False
129 assert False
130
131class Commit(Repr):
132 """Immutable."""
133 def __init__(self, repository, sha1):
134 self.__sha1 = sha1
135 self.__repository = repository
136 self.__data = None
137 sha1 = property(lambda self: self.__sha1)
138 @property
139 def data(self):
140 if self.__data == None:
141 self.__data = Commitdata.parse(
142 self.__repository,
143 self.__repository.cat_object(self.sha1))
144 return self.__data
145 def __str__(self):
146 return 'Commit<sha1: %s, data: %s>' % (self.sha1, self.__data)
147
148class Refs(object):
149 def __init__(self, repository):
150 self.__repository = repository
151 self.__refs = None
152 def __cache_refs(self):
153 self.__refs = {}
154 for line in self.__repository.run(['git', 'show-ref']).output_lines():
155 m = re.match(r'^([0-9a-f]{40})\s+(\S+)$', line)
156 sha1, ref = m.groups()
157 self.__refs[ref] = sha1
158 def get(self, ref):
159 """Throws KeyError if ref doesn't exist."""
160 if self.__refs == None:
161 self.__cache_refs()
162 return self.__repository.get_commit(self.__refs[ref])
f5c820a8
KH
163 def exists(self, ref):
164 try:
165 self.get(ref)
166 except KeyError:
167 return False
168 else:
169 return True
cbe4567e
KH
170 def set(self, ref, commit, msg):
171 if self.__refs == None:
172 self.__cache_refs()
173 old_sha1 = self.__refs.get(ref, '0'*40)
174 new_sha1 = commit.sha1
175 if old_sha1 != new_sha1:
176 self.__repository.run(['git', 'update-ref', '-m', msg,
177 ref, new_sha1, old_sha1]).no_output()
178 self.__refs[ref] = new_sha1
179 def delete(self, ref):
180 if self.__refs == None:
181 self.__cache_refs()
182 self.__repository.run(['git', 'update-ref',
183 '-d', ref, self.__refs[ref]]).no_output()
184 del self.__refs[ref]
185
186class ObjectCache(object):
187 """Cache for Python objects, for making sure that we create only one
188 Python object per git object."""
189 def __init__(self, create):
190 self.__objects = {}
191 self.__create = create
192 def __getitem__(self, name):
193 if not name in self.__objects:
194 self.__objects[name] = self.__create(name)
195 return self.__objects[name]
196 def __contains__(self, name):
197 return name in self.__objects
198 def __setitem__(self, name, val):
199 assert not name in self.__objects
200 self.__objects[name] = val
201
202class RunWithEnv(object):
203 def run(self, args, env = {}):
204 return run.Run(*args).env(utils.add_dict(self.env, env))
205
206class Repository(RunWithEnv):
207 def __init__(self, directory):
208 self.__git_dir = directory
209 self.__refs = Refs(self)
210 self.__trees = ObjectCache(lambda sha1: Tree(sha1))
211 self.__commits = ObjectCache(lambda sha1: Commit(self, sha1))
212 env = property(lambda self: { 'GIT_DIR': self.__git_dir })
213 @classmethod
214 def default(cls):
215 """Return the default repository."""
216 try:
217 return cls(run.Run('git', 'rev-parse', '--git-dir'
218 ).output_one_line())
219 except run.RunException:
220 raise RepositoryException('Cannot find git repository')
221 directory = property(lambda self: self.__git_dir)
222 refs = property(lambda self: self.__refs)
223 def cat_object(self, sha1):
224 return self.run(['git', 'cat-file', '-p', sha1]).raw_output()
225 def rev_parse(self, rev):
226 try:
227 return self.get_commit(self.run(
228 ['git', 'rev-parse', '%s^{commit}' % rev]
229 ).output_one_line())
230 except run.RunException:
231 raise RepositoryException('%s: No such revision' % rev)
232 def get_tree(self, sha1):
233 return self.__trees[sha1]
234 def get_commit(self, sha1):
235 return self.__commits[sha1]
236 def commit(self, commitdata):
237 c = ['git', 'commit-tree', commitdata.tree.sha1]
238 for p in commitdata.parents:
239 c.append('-p')
240 c.append(p.sha1)
241 env = {}
242 for p, v1 in ((commitdata.author, 'AUTHOR'),
243 (commitdata.committer, 'COMMITTER')):
244 if p != None:
245 for attr, v2 in (('name', 'NAME'), ('email', 'EMAIL'),
246 ('date', 'DATE')):
247 if getattr(p, attr) != None:
248 env['GIT_%s_%s' % (v1, v2)] = getattr(p, attr)
249 sha1 = self.run(c, env = env).raw_input(commitdata.message
250 ).output_one_line()
251 return self.get_commit(sha1)
252 @property
253 def head(self):
254 try:
255 return self.run(['git', 'symbolic-ref', '-q', 'HEAD']
256 ).output_one_line()
257 except run.RunException:
258 raise DetachedHeadException()
259 def set_head(self, ref, msg):
260 self.run(['git', 'symbolic-ref', '-m', msg, 'HEAD', ref]).no_output()