##// END OF EJS Templates
context: improve arg-checking assert.
Greg Ward -
r9024:10532b29 default
parent child Browse files
Show More
@@ -1,815 +1,817 b''
1 # context.py - changeset and file context objects for mercurial
1 # context.py - changeset and file context objects for mercurial
2 #
2 #
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 from node import nullid, nullrev, short, hex
8 from node import nullid, nullrev, short, hex
9 from i18n import _
9 from i18n import _
10 import ancestor, bdiff, error, util, subrepo
10 import ancestor, bdiff, error, util, subrepo
11 import os, errno
11 import os, errno
12
12
13 propertycache = util.propertycache
13 propertycache = util.propertycache
14
14
15 class changectx(object):
15 class changectx(object):
16 """A changecontext object makes access to data related to a particular
16 """A changecontext object makes access to data related to a particular
17 changeset convenient."""
17 changeset convenient."""
18 def __init__(self, repo, changeid=''):
18 def __init__(self, repo, changeid=''):
19 """changeid is a revision number, node, or tag"""
19 """changeid is a revision number, node, or tag"""
20 if changeid == '':
20 if changeid == '':
21 changeid = '.'
21 changeid = '.'
22 self._repo = repo
22 self._repo = repo
23 if isinstance(changeid, (long, int)):
23 if isinstance(changeid, (long, int)):
24 self._rev = changeid
24 self._rev = changeid
25 self._node = self._repo.changelog.node(changeid)
25 self._node = self._repo.changelog.node(changeid)
26 else:
26 else:
27 self._node = self._repo.lookup(changeid)
27 self._node = self._repo.lookup(changeid)
28 self._rev = self._repo.changelog.rev(self._node)
28 self._rev = self._repo.changelog.rev(self._node)
29
29
30 def __str__(self):
30 def __str__(self):
31 return short(self.node())
31 return short(self.node())
32
32
33 def __int__(self):
33 def __int__(self):
34 return self.rev()
34 return self.rev()
35
35
36 def __repr__(self):
36 def __repr__(self):
37 return "<changectx %s>" % str(self)
37 return "<changectx %s>" % str(self)
38
38
39 def __hash__(self):
39 def __hash__(self):
40 try:
40 try:
41 return hash(self._rev)
41 return hash(self._rev)
42 except AttributeError:
42 except AttributeError:
43 return id(self)
43 return id(self)
44
44
45 def __eq__(self, other):
45 def __eq__(self, other):
46 try:
46 try:
47 return self._rev == other._rev
47 return self._rev == other._rev
48 except AttributeError:
48 except AttributeError:
49 return False
49 return False
50
50
51 def __ne__(self, other):
51 def __ne__(self, other):
52 return not (self == other)
52 return not (self == other)
53
53
54 def __nonzero__(self):
54 def __nonzero__(self):
55 return self._rev != nullrev
55 return self._rev != nullrev
56
56
57 @propertycache
57 @propertycache
58 def _changeset(self):
58 def _changeset(self):
59 return self._repo.changelog.read(self.node())
59 return self._repo.changelog.read(self.node())
60
60
61 @propertycache
61 @propertycache
62 def _manifest(self):
62 def _manifest(self):
63 return self._repo.manifest.read(self._changeset[0])
63 return self._repo.manifest.read(self._changeset[0])
64
64
65 @propertycache
65 @propertycache
66 def _manifestdelta(self):
66 def _manifestdelta(self):
67 return self._repo.manifest.readdelta(self._changeset[0])
67 return self._repo.manifest.readdelta(self._changeset[0])
68
68
69 @propertycache
69 @propertycache
70 def _parents(self):
70 def _parents(self):
71 p = self._repo.changelog.parentrevs(self._rev)
71 p = self._repo.changelog.parentrevs(self._rev)
72 if p[1] == nullrev:
72 if p[1] == nullrev:
73 p = p[:-1]
73 p = p[:-1]
74 return [changectx(self._repo, x) for x in p]
74 return [changectx(self._repo, x) for x in p]
75
75
76 @propertycache
76 @propertycache
77 def substate(self):
77 def substate(self):
78 return subrepo.state(self)
78 return subrepo.state(self)
79
79
80 def __contains__(self, key):
80 def __contains__(self, key):
81 return key in self._manifest
81 return key in self._manifest
82
82
83 def __getitem__(self, key):
83 def __getitem__(self, key):
84 return self.filectx(key)
84 return self.filectx(key)
85
85
86 def __iter__(self):
86 def __iter__(self):
87 for f in sorted(self._manifest):
87 for f in sorted(self._manifest):
88 yield f
88 yield f
89
89
90 def changeset(self): return self._changeset
90 def changeset(self): return self._changeset
91 def manifest(self): return self._manifest
91 def manifest(self): return self._manifest
92 def manifestnode(self): return self._changeset[0]
92 def manifestnode(self): return self._changeset[0]
93
93
94 def rev(self): return self._rev
94 def rev(self): return self._rev
95 def node(self): return self._node
95 def node(self): return self._node
96 def hex(self): return hex(self._node)
96 def hex(self): return hex(self._node)
97 def user(self): return self._changeset[1]
97 def user(self): return self._changeset[1]
98 def date(self): return self._changeset[2]
98 def date(self): return self._changeset[2]
99 def files(self): return self._changeset[3]
99 def files(self): return self._changeset[3]
100 def description(self): return self._changeset[4]
100 def description(self): return self._changeset[4]
101 def branch(self): return self._changeset[5].get("branch")
101 def branch(self): return self._changeset[5].get("branch")
102 def extra(self): return self._changeset[5]
102 def extra(self): return self._changeset[5]
103 def tags(self): return self._repo.nodetags(self._node)
103 def tags(self): return self._repo.nodetags(self._node)
104
104
105 def parents(self):
105 def parents(self):
106 """return contexts for each parent changeset"""
106 """return contexts for each parent changeset"""
107 return self._parents
107 return self._parents
108
108
109 def p1(self):
109 def p1(self):
110 return self._parents[0]
110 return self._parents[0]
111
111
112 def p2(self):
112 def p2(self):
113 if len(self._parents) == 2:
113 if len(self._parents) == 2:
114 return self._parents[1]
114 return self._parents[1]
115 return changectx(self._repo, -1)
115 return changectx(self._repo, -1)
116
116
117 def children(self):
117 def children(self):
118 """return contexts for each child changeset"""
118 """return contexts for each child changeset"""
119 c = self._repo.changelog.children(self._node)
119 c = self._repo.changelog.children(self._node)
120 return [changectx(self._repo, x) for x in c]
120 return [changectx(self._repo, x) for x in c]
121
121
122 def ancestors(self):
122 def ancestors(self):
123 for a in self._repo.changelog.ancestors(self._rev):
123 for a in self._repo.changelog.ancestors(self._rev):
124 yield changectx(self._repo, a)
124 yield changectx(self._repo, a)
125
125
126 def descendants(self):
126 def descendants(self):
127 for d in self._repo.changelog.descendants(self._rev):
127 for d in self._repo.changelog.descendants(self._rev):
128 yield changectx(self._repo, d)
128 yield changectx(self._repo, d)
129
129
130 def _fileinfo(self, path):
130 def _fileinfo(self, path):
131 if '_manifest' in self.__dict__:
131 if '_manifest' in self.__dict__:
132 try:
132 try:
133 return self._manifest[path], self._manifest.flags(path)
133 return self._manifest[path], self._manifest.flags(path)
134 except KeyError:
134 except KeyError:
135 raise error.LookupError(self._node, path,
135 raise error.LookupError(self._node, path,
136 _('not found in manifest'))
136 _('not found in manifest'))
137 if '_manifestdelta' in self.__dict__ or path in self.files():
137 if '_manifestdelta' in self.__dict__ or path in self.files():
138 if path in self._manifestdelta:
138 if path in self._manifestdelta:
139 return self._manifestdelta[path], self._manifestdelta.flags(path)
139 return self._manifestdelta[path], self._manifestdelta.flags(path)
140 node, flag = self._repo.manifest.find(self._changeset[0], path)
140 node, flag = self._repo.manifest.find(self._changeset[0], path)
141 if not node:
141 if not node:
142 raise error.LookupError(self._node, path,
142 raise error.LookupError(self._node, path,
143 _('not found in manifest'))
143 _('not found in manifest'))
144
144
145 return node, flag
145 return node, flag
146
146
147 def filenode(self, path):
147 def filenode(self, path):
148 return self._fileinfo(path)[0]
148 return self._fileinfo(path)[0]
149
149
150 def flags(self, path):
150 def flags(self, path):
151 try:
151 try:
152 return self._fileinfo(path)[1]
152 return self._fileinfo(path)[1]
153 except error.LookupError:
153 except error.LookupError:
154 return ''
154 return ''
155
155
156 def filectx(self, path, fileid=None, filelog=None):
156 def filectx(self, path, fileid=None, filelog=None):
157 """get a file context from this changeset"""
157 """get a file context from this changeset"""
158 if fileid is None:
158 if fileid is None:
159 fileid = self.filenode(path)
159 fileid = self.filenode(path)
160 return filectx(self._repo, path, fileid=fileid,
160 return filectx(self._repo, path, fileid=fileid,
161 changectx=self, filelog=filelog)
161 changectx=self, filelog=filelog)
162
162
163 def ancestor(self, c2):
163 def ancestor(self, c2):
164 """
164 """
165 return the ancestor context of self and c2
165 return the ancestor context of self and c2
166 """
166 """
167 n = self._repo.changelog.ancestor(self._node, c2._node)
167 n = self._repo.changelog.ancestor(self._node, c2._node)
168 return changectx(self._repo, n)
168 return changectx(self._repo, n)
169
169
170 def walk(self, match):
170 def walk(self, match):
171 fset = set(match.files())
171 fset = set(match.files())
172 # for dirstate.walk, files=['.'] means "walk the whole tree".
172 # for dirstate.walk, files=['.'] means "walk the whole tree".
173 # follow that here, too
173 # follow that here, too
174 fset.discard('.')
174 fset.discard('.')
175 for fn in self:
175 for fn in self:
176 for ffn in fset:
176 for ffn in fset:
177 # match if the file is the exact name or a directory
177 # match if the file is the exact name or a directory
178 if ffn == fn or fn.startswith("%s/" % ffn):
178 if ffn == fn or fn.startswith("%s/" % ffn):
179 fset.remove(ffn)
179 fset.remove(ffn)
180 break
180 break
181 if match(fn):
181 if match(fn):
182 yield fn
182 yield fn
183 for fn in sorted(fset):
183 for fn in sorted(fset):
184 if match.bad(fn, 'No such file in rev ' + str(self)) and match(fn):
184 if match.bad(fn, 'No such file in rev ' + str(self)) and match(fn):
185 yield fn
185 yield fn
186
186
187 def sub(self, path):
187 def sub(self, path):
188 return subrepo.subrepo(self, path)
188 return subrepo.subrepo(self, path)
189
189
190 class filectx(object):
190 class filectx(object):
191 """A filecontext object makes access to data related to a particular
191 """A filecontext object makes access to data related to a particular
192 filerevision convenient."""
192 filerevision convenient."""
193 def __init__(self, repo, path, changeid=None, fileid=None,
193 def __init__(self, repo, path, changeid=None, fileid=None,
194 filelog=None, changectx=None):
194 filelog=None, changectx=None):
195 """changeid can be a changeset revision, node, or tag.
195 """changeid can be a changeset revision, node, or tag.
196 fileid can be a file revision or node."""
196 fileid can be a file revision or node."""
197 self._repo = repo
197 self._repo = repo
198 self._path = path
198 self._path = path
199
199
200 assert (changeid is not None
200 assert (changeid is not None
201 or fileid is not None
201 or fileid is not None
202 or changectx is not None)
202 or changectx is not None), \
203 ("bad args: changeid=%r, fileid=%r, changectx=%r"
204 % (changeid, fileid, changectx))
203
205
204 if filelog:
206 if filelog:
205 self._filelog = filelog
207 self._filelog = filelog
206
208
207 if changeid is not None:
209 if changeid is not None:
208 self._changeid = changeid
210 self._changeid = changeid
209 if changectx is not None:
211 if changectx is not None:
210 self._changectx = changectx
212 self._changectx = changectx
211 if fileid is not None:
213 if fileid is not None:
212 self._fileid = fileid
214 self._fileid = fileid
213
215
214 @propertycache
216 @propertycache
215 def _changectx(self):
217 def _changectx(self):
216 return changectx(self._repo, self._changeid)
218 return changectx(self._repo, self._changeid)
217
219
218 @propertycache
220 @propertycache
219 def _filelog(self):
221 def _filelog(self):
220 return self._repo.file(self._path)
222 return self._repo.file(self._path)
221
223
222 @propertycache
224 @propertycache
223 def _changeid(self):
225 def _changeid(self):
224 if '_changectx' in self.__dict__:
226 if '_changectx' in self.__dict__:
225 return self._changectx.rev()
227 return self._changectx.rev()
226 else:
228 else:
227 return self._filelog.linkrev(self._filerev)
229 return self._filelog.linkrev(self._filerev)
228
230
229 @propertycache
231 @propertycache
230 def _filenode(self):
232 def _filenode(self):
231 if '_fileid' in self.__dict__:
233 if '_fileid' in self.__dict__:
232 return self._filelog.lookup(self._fileid)
234 return self._filelog.lookup(self._fileid)
233 else:
235 else:
234 return self._changectx.filenode(self._path)
236 return self._changectx.filenode(self._path)
235
237
236 @propertycache
238 @propertycache
237 def _filerev(self):
239 def _filerev(self):
238 return self._filelog.rev(self._filenode)
240 return self._filelog.rev(self._filenode)
239
241
240 @propertycache
242 @propertycache
241 def _repopath(self):
243 def _repopath(self):
242 return self._path
244 return self._path
243
245
244 def __nonzero__(self):
246 def __nonzero__(self):
245 try:
247 try:
246 self._filenode
248 self._filenode
247 return True
249 return True
248 except error.LookupError:
250 except error.LookupError:
249 # file is missing
251 # file is missing
250 return False
252 return False
251
253
252 def __str__(self):
254 def __str__(self):
253 return "%s@%s" % (self.path(), short(self.node()))
255 return "%s@%s" % (self.path(), short(self.node()))
254
256
255 def __repr__(self):
257 def __repr__(self):
256 return "<filectx %s>" % str(self)
258 return "<filectx %s>" % str(self)
257
259
258 def __hash__(self):
260 def __hash__(self):
259 try:
261 try:
260 return hash((self._path, self._fileid))
262 return hash((self._path, self._fileid))
261 except AttributeError:
263 except AttributeError:
262 return id(self)
264 return id(self)
263
265
264 def __eq__(self, other):
266 def __eq__(self, other):
265 try:
267 try:
266 return (self._path == other._path
268 return (self._path == other._path
267 and self._fileid == other._fileid)
269 and self._fileid == other._fileid)
268 except AttributeError:
270 except AttributeError:
269 return False
271 return False
270
272
271 def __ne__(self, other):
273 def __ne__(self, other):
272 return not (self == other)
274 return not (self == other)
273
275
274 def filectx(self, fileid):
276 def filectx(self, fileid):
275 '''opens an arbitrary revision of the file without
277 '''opens an arbitrary revision of the file without
276 opening a new filelog'''
278 opening a new filelog'''
277 return filectx(self._repo, self._path, fileid=fileid,
279 return filectx(self._repo, self._path, fileid=fileid,
278 filelog=self._filelog)
280 filelog=self._filelog)
279
281
280 def filerev(self): return self._filerev
282 def filerev(self): return self._filerev
281 def filenode(self): return self._filenode
283 def filenode(self): return self._filenode
282 def flags(self): return self._changectx.flags(self._path)
284 def flags(self): return self._changectx.flags(self._path)
283 def filelog(self): return self._filelog
285 def filelog(self): return self._filelog
284
286
285 def rev(self):
287 def rev(self):
286 if '_changectx' in self.__dict__:
288 if '_changectx' in self.__dict__:
287 return self._changectx.rev()
289 return self._changectx.rev()
288 if '_changeid' in self.__dict__:
290 if '_changeid' in self.__dict__:
289 return self._changectx.rev()
291 return self._changectx.rev()
290 return self._filelog.linkrev(self._filerev)
292 return self._filelog.linkrev(self._filerev)
291
293
292 def linkrev(self): return self._filelog.linkrev(self._filerev)
294 def linkrev(self): return self._filelog.linkrev(self._filerev)
293 def node(self): return self._changectx.node()
295 def node(self): return self._changectx.node()
294 def user(self): return self._changectx.user()
296 def user(self): return self._changectx.user()
295 def date(self): return self._changectx.date()
297 def date(self): return self._changectx.date()
296 def files(self): return self._changectx.files()
298 def files(self): return self._changectx.files()
297 def description(self): return self._changectx.description()
299 def description(self): return self._changectx.description()
298 def branch(self): return self._changectx.branch()
300 def branch(self): return self._changectx.branch()
299 def manifest(self): return self._changectx.manifest()
301 def manifest(self): return self._changectx.manifest()
300 def changectx(self): return self._changectx
302 def changectx(self): return self._changectx
301
303
302 def data(self): return self._filelog.read(self._filenode)
304 def data(self): return self._filelog.read(self._filenode)
303 def path(self): return self._path
305 def path(self): return self._path
304 def size(self): return self._filelog.size(self._filerev)
306 def size(self): return self._filelog.size(self._filerev)
305
307
306 def cmp(self, text): return self._filelog.cmp(self._filenode, text)
308 def cmp(self, text): return self._filelog.cmp(self._filenode, text)
307
309
308 def renamed(self):
310 def renamed(self):
309 """check if file was actually renamed in this changeset revision
311 """check if file was actually renamed in this changeset revision
310
312
311 If rename logged in file revision, we report copy for changeset only
313 If rename logged in file revision, we report copy for changeset only
312 if file revisions linkrev points back to the changeset in question
314 if file revisions linkrev points back to the changeset in question
313 or both changeset parents contain different file revisions.
315 or both changeset parents contain different file revisions.
314 """
316 """
315
317
316 renamed = self._filelog.renamed(self._filenode)
318 renamed = self._filelog.renamed(self._filenode)
317 if not renamed:
319 if not renamed:
318 return renamed
320 return renamed
319
321
320 if self.rev() == self.linkrev():
322 if self.rev() == self.linkrev():
321 return renamed
323 return renamed
322
324
323 name = self.path()
325 name = self.path()
324 fnode = self._filenode
326 fnode = self._filenode
325 for p in self._changectx.parents():
327 for p in self._changectx.parents():
326 try:
328 try:
327 if fnode == p.filenode(name):
329 if fnode == p.filenode(name):
328 return None
330 return None
329 except error.LookupError:
331 except error.LookupError:
330 pass
332 pass
331 return renamed
333 return renamed
332
334
333 def parents(self):
335 def parents(self):
334 p = self._path
336 p = self._path
335 fl = self._filelog
337 fl = self._filelog
336 pl = [(p, n, fl) for n in self._filelog.parents(self._filenode)]
338 pl = [(p, n, fl) for n in self._filelog.parents(self._filenode)]
337
339
338 r = self._filelog.renamed(self._filenode)
340 r = self._filelog.renamed(self._filenode)
339 if r:
341 if r:
340 pl[0] = (r[0], r[1], None)
342 pl[0] = (r[0], r[1], None)
341
343
342 return [filectx(self._repo, p, fileid=n, filelog=l)
344 return [filectx(self._repo, p, fileid=n, filelog=l)
343 for p,n,l in pl if n != nullid]
345 for p,n,l in pl if n != nullid]
344
346
345 def children(self):
347 def children(self):
346 # hard for renames
348 # hard for renames
347 c = self._filelog.children(self._filenode)
349 c = self._filelog.children(self._filenode)
348 return [filectx(self._repo, self._path, fileid=x,
350 return [filectx(self._repo, self._path, fileid=x,
349 filelog=self._filelog) for x in c]
351 filelog=self._filelog) for x in c]
350
352
351 def annotate(self, follow=False, linenumber=None):
353 def annotate(self, follow=False, linenumber=None):
352 '''returns a list of tuples of (ctx, line) for each line
354 '''returns a list of tuples of (ctx, line) for each line
353 in the file, where ctx is the filectx of the node where
355 in the file, where ctx is the filectx of the node where
354 that line was last changed.
356 that line was last changed.
355 This returns tuples of ((ctx, linenumber), line) for each line,
357 This returns tuples of ((ctx, linenumber), line) for each line,
356 if "linenumber" parameter is NOT "None".
358 if "linenumber" parameter is NOT "None".
357 In such tuples, linenumber means one at the first appearance
359 In such tuples, linenumber means one at the first appearance
358 in the managed file.
360 in the managed file.
359 To reduce annotation cost,
361 To reduce annotation cost,
360 this returns fixed value(False is used) as linenumber,
362 this returns fixed value(False is used) as linenumber,
361 if "linenumber" parameter is "False".'''
363 if "linenumber" parameter is "False".'''
362
364
363 def decorate_compat(text, rev):
365 def decorate_compat(text, rev):
364 return ([rev] * len(text.splitlines()), text)
366 return ([rev] * len(text.splitlines()), text)
365
367
366 def without_linenumber(text, rev):
368 def without_linenumber(text, rev):
367 return ([(rev, False)] * len(text.splitlines()), text)
369 return ([(rev, False)] * len(text.splitlines()), text)
368
370
369 def with_linenumber(text, rev):
371 def with_linenumber(text, rev):
370 size = len(text.splitlines())
372 size = len(text.splitlines())
371 return ([(rev, i) for i in xrange(1, size + 1)], text)
373 return ([(rev, i) for i in xrange(1, size + 1)], text)
372
374
373 decorate = (((linenumber is None) and decorate_compat) or
375 decorate = (((linenumber is None) and decorate_compat) or
374 (linenumber and with_linenumber) or
376 (linenumber and with_linenumber) or
375 without_linenumber)
377 without_linenumber)
376
378
377 def pair(parent, child):
379 def pair(parent, child):
378 for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]):
380 for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]):
379 child[0][b1:b2] = parent[0][a1:a2]
381 child[0][b1:b2] = parent[0][a1:a2]
380 return child
382 return child
381
383
382 getlog = util.cachefunc(lambda x: self._repo.file(x))
384 getlog = util.cachefunc(lambda x: self._repo.file(x))
383 def getctx(path, fileid):
385 def getctx(path, fileid):
384 log = path == self._path and self._filelog or getlog(path)
386 log = path == self._path and self._filelog or getlog(path)
385 return filectx(self._repo, path, fileid=fileid, filelog=log)
387 return filectx(self._repo, path, fileid=fileid, filelog=log)
386 getctx = util.cachefunc(getctx)
388 getctx = util.cachefunc(getctx)
387
389
388 def parents(f):
390 def parents(f):
389 # we want to reuse filectx objects as much as possible
391 # we want to reuse filectx objects as much as possible
390 p = f._path
392 p = f._path
391 if f._filerev is None: # working dir
393 if f._filerev is None: # working dir
392 pl = [(n.path(), n.filerev()) for n in f.parents()]
394 pl = [(n.path(), n.filerev()) for n in f.parents()]
393 else:
395 else:
394 pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)]
396 pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)]
395
397
396 if follow:
398 if follow:
397 r = f.renamed()
399 r = f.renamed()
398 if r:
400 if r:
399 pl[0] = (r[0], getlog(r[0]).rev(r[1]))
401 pl[0] = (r[0], getlog(r[0]).rev(r[1]))
400
402
401 return [getctx(p, n) for p, n in pl if n != nullrev]
403 return [getctx(p, n) for p, n in pl if n != nullrev]
402
404
403 # use linkrev to find the first changeset where self appeared
405 # use linkrev to find the first changeset where self appeared
404 if self.rev() != self.linkrev():
406 if self.rev() != self.linkrev():
405 base = self.filectx(self.filerev())
407 base = self.filectx(self.filerev())
406 else:
408 else:
407 base = self
409 base = self
408
410
409 # find all ancestors
411 # find all ancestors
410 needed = {base: 1}
412 needed = {base: 1}
411 visit = [base]
413 visit = [base]
412 files = [base._path]
414 files = [base._path]
413 while visit:
415 while visit:
414 f = visit.pop(0)
416 f = visit.pop(0)
415 for p in parents(f):
417 for p in parents(f):
416 if p not in needed:
418 if p not in needed:
417 needed[p] = 1
419 needed[p] = 1
418 visit.append(p)
420 visit.append(p)
419 if p._path not in files:
421 if p._path not in files:
420 files.append(p._path)
422 files.append(p._path)
421 else:
423 else:
422 # count how many times we'll use this
424 # count how many times we'll use this
423 needed[p] += 1
425 needed[p] += 1
424
426
425 # sort by revision (per file) which is a topological order
427 # sort by revision (per file) which is a topological order
426 visit = []
428 visit = []
427 for f in files:
429 for f in files:
428 fn = [(n.rev(), n) for n in needed if n._path == f]
430 fn = [(n.rev(), n) for n in needed if n._path == f]
429 visit.extend(fn)
431 visit.extend(fn)
430
432
431 hist = {}
433 hist = {}
432 for r, f in sorted(visit):
434 for r, f in sorted(visit):
433 curr = decorate(f.data(), f)
435 curr = decorate(f.data(), f)
434 for p in parents(f):
436 for p in parents(f):
435 if p != nullid:
437 if p != nullid:
436 curr = pair(hist[p], curr)
438 curr = pair(hist[p], curr)
437 # trim the history of unneeded revs
439 # trim the history of unneeded revs
438 needed[p] -= 1
440 needed[p] -= 1
439 if not needed[p]:
441 if not needed[p]:
440 del hist[p]
442 del hist[p]
441 hist[f] = curr
443 hist[f] = curr
442
444
443 return zip(hist[f][0], hist[f][1].splitlines(1))
445 return zip(hist[f][0], hist[f][1].splitlines(1))
444
446
445 def ancestor(self, fc2):
447 def ancestor(self, fc2):
446 """
448 """
447 find the common ancestor file context, if any, of self, and fc2
449 find the common ancestor file context, if any, of self, and fc2
448 """
450 """
449
451
450 acache = {}
452 acache = {}
451
453
452 # prime the ancestor cache for the working directory
454 # prime the ancestor cache for the working directory
453 for c in (self, fc2):
455 for c in (self, fc2):
454 if c._filerev is None:
456 if c._filerev is None:
455 pl = [(n.path(), n.filenode()) for n in c.parents()]
457 pl = [(n.path(), n.filenode()) for n in c.parents()]
456 acache[(c._path, None)] = pl
458 acache[(c._path, None)] = pl
457
459
458 flcache = {self._repopath:self._filelog, fc2._repopath:fc2._filelog}
460 flcache = {self._repopath:self._filelog, fc2._repopath:fc2._filelog}
459 def parents(vertex):
461 def parents(vertex):
460 if vertex in acache:
462 if vertex in acache:
461 return acache[vertex]
463 return acache[vertex]
462 f, n = vertex
464 f, n = vertex
463 if f not in flcache:
465 if f not in flcache:
464 flcache[f] = self._repo.file(f)
466 flcache[f] = self._repo.file(f)
465 fl = flcache[f]
467 fl = flcache[f]
466 pl = [(f, p) for p in fl.parents(n) if p != nullid]
468 pl = [(f, p) for p in fl.parents(n) if p != nullid]
467 re = fl.renamed(n)
469 re = fl.renamed(n)
468 if re:
470 if re:
469 pl.append(re)
471 pl.append(re)
470 acache[vertex] = pl
472 acache[vertex] = pl
471 return pl
473 return pl
472
474
473 a, b = (self._path, self._filenode), (fc2._path, fc2._filenode)
475 a, b = (self._path, self._filenode), (fc2._path, fc2._filenode)
474 v = ancestor.ancestor(a, b, parents)
476 v = ancestor.ancestor(a, b, parents)
475 if v:
477 if v:
476 f, n = v
478 f, n = v
477 return filectx(self._repo, f, fileid=n, filelog=flcache[f])
479 return filectx(self._repo, f, fileid=n, filelog=flcache[f])
478
480
479 return None
481 return None
480
482
481 class workingctx(changectx):
483 class workingctx(changectx):
482 """A workingctx object makes access to data related to
484 """A workingctx object makes access to data related to
483 the current working directory convenient.
485 the current working directory convenient.
484 parents - a pair of parent nodeids, or None to use the dirstate.
486 parents - a pair of parent nodeids, or None to use the dirstate.
485 date - any valid date string or (unixtime, offset), or None.
487 date - any valid date string or (unixtime, offset), or None.
486 user - username string, or None.
488 user - username string, or None.
487 extra - a dictionary of extra values, or None.
489 extra - a dictionary of extra values, or None.
488 changes - a list of file lists as returned by localrepo.status()
490 changes - a list of file lists as returned by localrepo.status()
489 or None to use the repository status.
491 or None to use the repository status.
490 """
492 """
491 def __init__(self, repo, parents=None, text="", user=None, date=None,
493 def __init__(self, repo, parents=None, text="", user=None, date=None,
492 extra=None, changes=None):
494 extra=None, changes=None):
493 self._repo = repo
495 self._repo = repo
494 self._rev = None
496 self._rev = None
495 self._node = None
497 self._node = None
496 self._text = text
498 self._text = text
497 if date:
499 if date:
498 self._date = util.parsedate(date)
500 self._date = util.parsedate(date)
499 if user:
501 if user:
500 self._user = user
502 self._user = user
501 if parents:
503 if parents:
502 self._parents = [changectx(self._repo, p) for p in parents]
504 self._parents = [changectx(self._repo, p) for p in parents]
503 if changes:
505 if changes:
504 self._status = list(changes)
506 self._status = list(changes)
505
507
506 self._extra = {}
508 self._extra = {}
507 if extra:
509 if extra:
508 self._extra = extra.copy()
510 self._extra = extra.copy()
509 if 'branch' not in self._extra:
511 if 'branch' not in self._extra:
510 branch = self._repo.dirstate.branch()
512 branch = self._repo.dirstate.branch()
511 try:
513 try:
512 branch = branch.decode('UTF-8').encode('UTF-8')
514 branch = branch.decode('UTF-8').encode('UTF-8')
513 except UnicodeDecodeError:
515 except UnicodeDecodeError:
514 raise util.Abort(_('branch name not in UTF-8!'))
516 raise util.Abort(_('branch name not in UTF-8!'))
515 self._extra['branch'] = branch
517 self._extra['branch'] = branch
516 if self._extra['branch'] == '':
518 if self._extra['branch'] == '':
517 self._extra['branch'] = 'default'
519 self._extra['branch'] = 'default'
518
520
519 def __str__(self):
521 def __str__(self):
520 return str(self._parents[0]) + "+"
522 return str(self._parents[0]) + "+"
521
523
522 def __nonzero__(self):
524 def __nonzero__(self):
523 return True
525 return True
524
526
525 def __contains__(self, key):
527 def __contains__(self, key):
526 return self._repo.dirstate[key] not in "?r"
528 return self._repo.dirstate[key] not in "?r"
527
529
528 @propertycache
530 @propertycache
529 def _manifest(self):
531 def _manifest(self):
530 """generate a manifest corresponding to the working directory"""
532 """generate a manifest corresponding to the working directory"""
531
533
532 man = self._parents[0].manifest().copy()
534 man = self._parents[0].manifest().copy()
533 copied = self._repo.dirstate.copies()
535 copied = self._repo.dirstate.copies()
534 cf = lambda x: man.flags(copied.get(x, x))
536 cf = lambda x: man.flags(copied.get(x, x))
535 ff = self._repo.dirstate.flagfunc(cf)
537 ff = self._repo.dirstate.flagfunc(cf)
536 modified, added, removed, deleted, unknown = self._status[:5]
538 modified, added, removed, deleted, unknown = self._status[:5]
537 for i, l in (("a", added), ("m", modified), ("u", unknown)):
539 for i, l in (("a", added), ("m", modified), ("u", unknown)):
538 for f in l:
540 for f in l:
539 man[f] = man.get(copied.get(f, f), nullid) + i
541 man[f] = man.get(copied.get(f, f), nullid) + i
540 try:
542 try:
541 man.set(f, ff(f))
543 man.set(f, ff(f))
542 except OSError:
544 except OSError:
543 pass
545 pass
544
546
545 for f in deleted + removed:
547 for f in deleted + removed:
546 if f in man:
548 if f in man:
547 del man[f]
549 del man[f]
548
550
549 return man
551 return man
550
552
551 @propertycache
553 @propertycache
552 def _status(self):
554 def _status(self):
553 return self._repo.status(unknown=True)
555 return self._repo.status(unknown=True)
554
556
555 @propertycache
557 @propertycache
556 def _user(self):
558 def _user(self):
557 return self._repo.ui.username()
559 return self._repo.ui.username()
558
560
559 @propertycache
561 @propertycache
560 def _date(self):
562 def _date(self):
561 return util.makedate()
563 return util.makedate()
562
564
563 @propertycache
565 @propertycache
564 def _parents(self):
566 def _parents(self):
565 p = self._repo.dirstate.parents()
567 p = self._repo.dirstate.parents()
566 if p[1] == nullid:
568 if p[1] == nullid:
567 p = p[:-1]
569 p = p[:-1]
568 self._parents = [changectx(self._repo, x) for x in p]
570 self._parents = [changectx(self._repo, x) for x in p]
569 return self._parents
571 return self._parents
570
572
571 def manifest(self): return self._manifest
573 def manifest(self): return self._manifest
572
574
573 def user(self): return self._user or self._repo.ui.username()
575 def user(self): return self._user or self._repo.ui.username()
574 def date(self): return self._date
576 def date(self): return self._date
575 def description(self): return self._text
577 def description(self): return self._text
576 def files(self):
578 def files(self):
577 return sorted(self._status[0] + self._status[1] + self._status[2])
579 return sorted(self._status[0] + self._status[1] + self._status[2])
578
580
579 def modified(self): return self._status[0]
581 def modified(self): return self._status[0]
580 def added(self): return self._status[1]
582 def added(self): return self._status[1]
581 def removed(self): return self._status[2]
583 def removed(self): return self._status[2]
582 def deleted(self): return self._status[3]
584 def deleted(self): return self._status[3]
583 def unknown(self): return self._status[4]
585 def unknown(self): return self._status[4]
584 def clean(self): return self._status[5]
586 def clean(self): return self._status[5]
585 def branch(self): return self._extra['branch']
587 def branch(self): return self._extra['branch']
586 def extra(self): return self._extra
588 def extra(self): return self._extra
587
589
588 def tags(self):
590 def tags(self):
589 t = []
591 t = []
590 [t.extend(p.tags()) for p in self.parents()]
592 [t.extend(p.tags()) for p in self.parents()]
591 return t
593 return t
592
594
593 def children(self):
595 def children(self):
594 return []
596 return []
595
597
596 def flags(self, path):
598 def flags(self, path):
597 if '_manifest' in self.__dict__:
599 if '_manifest' in self.__dict__:
598 try:
600 try:
599 return self._manifest.flags(path)
601 return self._manifest.flags(path)
600 except KeyError:
602 except KeyError:
601 return ''
603 return ''
602
604
603 pnode = self._parents[0].changeset()[0]
605 pnode = self._parents[0].changeset()[0]
604 orig = self._repo.dirstate.copies().get(path, path)
606 orig = self._repo.dirstate.copies().get(path, path)
605 node, flag = self._repo.manifest.find(pnode, orig)
607 node, flag = self._repo.manifest.find(pnode, orig)
606 try:
608 try:
607 ff = self._repo.dirstate.flagfunc(lambda x: flag or '')
609 ff = self._repo.dirstate.flagfunc(lambda x: flag or '')
608 return ff(path)
610 return ff(path)
609 except OSError:
611 except OSError:
610 pass
612 pass
611
613
612 if not node or path in self.deleted() or path in self.removed():
614 if not node or path in self.deleted() or path in self.removed():
613 return ''
615 return ''
614 return flag
616 return flag
615
617
616 def filectx(self, path, filelog=None):
618 def filectx(self, path, filelog=None):
617 """get a file context from the working directory"""
619 """get a file context from the working directory"""
618 return workingfilectx(self._repo, path, workingctx=self,
620 return workingfilectx(self._repo, path, workingctx=self,
619 filelog=filelog)
621 filelog=filelog)
620
622
621 def ancestor(self, c2):
623 def ancestor(self, c2):
622 """return the ancestor context of self and c2"""
624 """return the ancestor context of self and c2"""
623 return self._parents[0].ancestor(c2) # punt on two parents for now
625 return self._parents[0].ancestor(c2) # punt on two parents for now
624
626
625 def walk(self, match):
627 def walk(self, match):
626 return sorted(self._repo.dirstate.walk(match, True, False))
628 return sorted(self._repo.dirstate.walk(match, True, False))
627
629
628 def dirty(self, missing=False):
630 def dirty(self, missing=False):
629 "check whether a working directory is modified"
631 "check whether a working directory is modified"
630
632
631 return (self.p2() or self.branch() != self.p1().branch() or
633 return (self.p2() or self.branch() != self.p1().branch() or
632 self.modified() or self.added() or self.removed() or
634 self.modified() or self.added() or self.removed() or
633 (missing and self.deleted()))
635 (missing and self.deleted()))
634
636
635 class workingfilectx(filectx):
637 class workingfilectx(filectx):
636 """A workingfilectx object makes access to data related to a particular
638 """A workingfilectx object makes access to data related to a particular
637 file in the working directory convenient."""
639 file in the working directory convenient."""
638 def __init__(self, repo, path, filelog=None, workingctx=None):
640 def __init__(self, repo, path, filelog=None, workingctx=None):
639 """changeid can be a changeset revision, node, or tag.
641 """changeid can be a changeset revision, node, or tag.
640 fileid can be a file revision or node."""
642 fileid can be a file revision or node."""
641 self._repo = repo
643 self._repo = repo
642 self._path = path
644 self._path = path
643 self._changeid = None
645 self._changeid = None
644 self._filerev = self._filenode = None
646 self._filerev = self._filenode = None
645
647
646 if filelog:
648 if filelog:
647 self._filelog = filelog
649 self._filelog = filelog
648 if workingctx:
650 if workingctx:
649 self._changectx = workingctx
651 self._changectx = workingctx
650
652
651 @propertycache
653 @propertycache
652 def _changectx(self):
654 def _changectx(self):
653 return workingctx(self._repo)
655 return workingctx(self._repo)
654
656
655 def __nonzero__(self):
657 def __nonzero__(self):
656 return True
658 return True
657
659
658 def __str__(self):
660 def __str__(self):
659 return "%s@%s" % (self.path(), self._changectx)
661 return "%s@%s" % (self.path(), self._changectx)
660
662
661 def data(self): return self._repo.wread(self._path)
663 def data(self): return self._repo.wread(self._path)
662 def renamed(self):
664 def renamed(self):
663 rp = self._repo.dirstate.copied(self._path)
665 rp = self._repo.dirstate.copied(self._path)
664 if not rp:
666 if not rp:
665 return None
667 return None
666 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
668 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
667
669
668 def parents(self):
670 def parents(self):
669 '''return parent filectxs, following copies if necessary'''
671 '''return parent filectxs, following copies if necessary'''
670 def filenode(ctx, path):
672 def filenode(ctx, path):
671 return ctx._manifest.get(path, nullid)
673 return ctx._manifest.get(path, nullid)
672
674
673 path = self._path
675 path = self._path
674 fl = self._filelog
676 fl = self._filelog
675 pcl = self._changectx._parents
677 pcl = self._changectx._parents
676 renamed = self.renamed()
678 renamed = self.renamed()
677
679
678 if renamed:
680 if renamed:
679 pl = [renamed + (None,)]
681 pl = [renamed + (None,)]
680 else:
682 else:
681 pl = [(path, filenode(pcl[0], path), fl)]
683 pl = [(path, filenode(pcl[0], path), fl)]
682
684
683 for pc in pcl[1:]:
685 for pc in pcl[1:]:
684 pl.append((path, filenode(pc, path), fl))
686 pl.append((path, filenode(pc, path), fl))
685
687
686 return [filectx(self._repo, p, fileid=n, filelog=l)
688 return [filectx(self._repo, p, fileid=n, filelog=l)
687 for p,n,l in pl if n != nullid]
689 for p,n,l in pl if n != nullid]
688
690
689 def children(self):
691 def children(self):
690 return []
692 return []
691
693
692 def size(self): return os.stat(self._repo.wjoin(self._path)).st_size
694 def size(self): return os.stat(self._repo.wjoin(self._path)).st_size
693 def date(self):
695 def date(self):
694 t, tz = self._changectx.date()
696 t, tz = self._changectx.date()
695 try:
697 try:
696 return (int(os.lstat(self._repo.wjoin(self._path)).st_mtime), tz)
698 return (int(os.lstat(self._repo.wjoin(self._path)).st_mtime), tz)
697 except OSError, err:
699 except OSError, err:
698 if err.errno != errno.ENOENT: raise
700 if err.errno != errno.ENOENT: raise
699 return (t, tz)
701 return (t, tz)
700
702
701 def cmp(self, text): return self._repo.wread(self._path) == text
703 def cmp(self, text): return self._repo.wread(self._path) == text
702
704
703 class memctx(object):
705 class memctx(object):
704 """Use memctx to perform in-memory commits via localrepo.commitctx().
706 """Use memctx to perform in-memory commits via localrepo.commitctx().
705
707
706 Revision information is supplied at initialization time while
708 Revision information is supplied at initialization time while
707 related files data and is made available through a callback
709 related files data and is made available through a callback
708 mechanism. 'repo' is the current localrepo, 'parents' is a
710 mechanism. 'repo' is the current localrepo, 'parents' is a
709 sequence of two parent revisions identifiers (pass None for every
711 sequence of two parent revisions identifiers (pass None for every
710 missing parent), 'text' is the commit message and 'files' lists
712 missing parent), 'text' is the commit message and 'files' lists
711 names of files touched by the revision (normalized and relative to
713 names of files touched by the revision (normalized and relative to
712 repository root).
714 repository root).
713
715
714 filectxfn(repo, memctx, path) is a callable receiving the
716 filectxfn(repo, memctx, path) is a callable receiving the
715 repository, the current memctx object and the normalized path of
717 repository, the current memctx object and the normalized path of
716 requested file, relative to repository root. It is fired by the
718 requested file, relative to repository root. It is fired by the
717 commit function for every file in 'files', but calls order is
719 commit function for every file in 'files', but calls order is
718 undefined. If the file is available in the revision being
720 undefined. If the file is available in the revision being
719 committed (updated or added), filectxfn returns a memfilectx
721 committed (updated or added), filectxfn returns a memfilectx
720 object. If the file was removed, filectxfn raises an
722 object. If the file was removed, filectxfn raises an
721 IOError. Moved files are represented by marking the source file
723 IOError. Moved files are represented by marking the source file
722 removed and the new file added with copy information (see
724 removed and the new file added with copy information (see
723 memfilectx).
725 memfilectx).
724
726
725 user receives the committer name and defaults to current
727 user receives the committer name and defaults to current
726 repository username, date is the commit date in any format
728 repository username, date is the commit date in any format
727 supported by util.parsedate() and defaults to current date, extra
729 supported by util.parsedate() and defaults to current date, extra
728 is a dictionary of metadata or is left empty.
730 is a dictionary of metadata or is left empty.
729 """
731 """
730 def __init__(self, repo, parents, text, files, filectxfn, user=None,
732 def __init__(self, repo, parents, text, files, filectxfn, user=None,
731 date=None, extra=None):
733 date=None, extra=None):
732 self._repo = repo
734 self._repo = repo
733 self._rev = None
735 self._rev = None
734 self._node = None
736 self._node = None
735 self._text = text
737 self._text = text
736 self._date = date and util.parsedate(date) or util.makedate()
738 self._date = date and util.parsedate(date) or util.makedate()
737 self._user = user
739 self._user = user
738 parents = [(p or nullid) for p in parents]
740 parents = [(p or nullid) for p in parents]
739 p1, p2 = parents
741 p1, p2 = parents
740 self._parents = [changectx(self._repo, p) for p in (p1, p2)]
742 self._parents = [changectx(self._repo, p) for p in (p1, p2)]
741 files = sorted(set(files))
743 files = sorted(set(files))
742 self._status = [files, [], [], [], []]
744 self._status = [files, [], [], [], []]
743 self._filectxfn = filectxfn
745 self._filectxfn = filectxfn
744
746
745 self._extra = extra and extra.copy() or {}
747 self._extra = extra and extra.copy() or {}
746 if 'branch' not in self._extra:
748 if 'branch' not in self._extra:
747 self._extra['branch'] = 'default'
749 self._extra['branch'] = 'default'
748 elif self._extra.get('branch') == '':
750 elif self._extra.get('branch') == '':
749 self._extra['branch'] = 'default'
751 self._extra['branch'] = 'default'
750
752
751 def __str__(self):
753 def __str__(self):
752 return str(self._parents[0]) + "+"
754 return str(self._parents[0]) + "+"
753
755
754 def __int__(self):
756 def __int__(self):
755 return self._rev
757 return self._rev
756
758
757 def __nonzero__(self):
759 def __nonzero__(self):
758 return True
760 return True
759
761
760 def __getitem__(self, key):
762 def __getitem__(self, key):
761 return self.filectx(key)
763 return self.filectx(key)
762
764
763 def p1(self): return self._parents[0]
765 def p1(self): return self._parents[0]
764 def p2(self): return self._parents[1]
766 def p2(self): return self._parents[1]
765
767
766 def user(self): return self._user or self._repo.ui.username()
768 def user(self): return self._user or self._repo.ui.username()
767 def date(self): return self._date
769 def date(self): return self._date
768 def description(self): return self._text
770 def description(self): return self._text
769 def files(self): return self.modified()
771 def files(self): return self.modified()
770 def modified(self): return self._status[0]
772 def modified(self): return self._status[0]
771 def added(self): return self._status[1]
773 def added(self): return self._status[1]
772 def removed(self): return self._status[2]
774 def removed(self): return self._status[2]
773 def deleted(self): return self._status[3]
775 def deleted(self): return self._status[3]
774 def unknown(self): return self._status[4]
776 def unknown(self): return self._status[4]
775 def clean(self): return self._status[5]
777 def clean(self): return self._status[5]
776 def branch(self): return self._extra['branch']
778 def branch(self): return self._extra['branch']
777 def extra(self): return self._extra
779 def extra(self): return self._extra
778 def flags(self, f): return self[f].flags()
780 def flags(self, f): return self[f].flags()
779
781
780 def parents(self):
782 def parents(self):
781 """return contexts for each parent changeset"""
783 """return contexts for each parent changeset"""
782 return self._parents
784 return self._parents
783
785
784 def filectx(self, path, filelog=None):
786 def filectx(self, path, filelog=None):
785 """get a file context from the working directory"""
787 """get a file context from the working directory"""
786 return self._filectxfn(self._repo, self, path)
788 return self._filectxfn(self._repo, self, path)
787
789
788 class memfilectx(object):
790 class memfilectx(object):
789 """memfilectx represents an in-memory file to commit.
791 """memfilectx represents an in-memory file to commit.
790
792
791 See memctx for more details.
793 See memctx for more details.
792 """
794 """
793 def __init__(self, path, data, islink, isexec, copied):
795 def __init__(self, path, data, islink, isexec, copied):
794 """
796 """
795 path is the normalized file path relative to repository root.
797 path is the normalized file path relative to repository root.
796 data is the file content as a string.
798 data is the file content as a string.
797 islink is True if the file is a symbolic link.
799 islink is True if the file is a symbolic link.
798 isexec is True if the file is executable.
800 isexec is True if the file is executable.
799 copied is the source file path if current file was copied in the
801 copied is the source file path if current file was copied in the
800 revision being committed, or None."""
802 revision being committed, or None."""
801 self._path = path
803 self._path = path
802 self._data = data
804 self._data = data
803 self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
805 self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
804 self._copied = None
806 self._copied = None
805 if copied:
807 if copied:
806 self._copied = (copied, nullid)
808 self._copied = (copied, nullid)
807
809
808 def __nonzero__(self): return True
810 def __nonzero__(self): return True
809 def __str__(self): return "%s@%s" % (self.path(), self._changectx)
811 def __str__(self): return "%s@%s" % (self.path(), self._changectx)
810 def path(self): return self._path
812 def path(self): return self._path
811 def data(self): return self._data
813 def data(self): return self._data
812 def flags(self): return self._flags
814 def flags(self): return self._flags
813 def isexec(self): return 'x' in self._flags
815 def isexec(self): return 'x' in self._flags
814 def islink(self): return 'l' in self._flags
816 def islink(self): return 'l' in self._flags
815 def renamed(self): return self._copied
817 def renamed(self): return self._copied
General Comments 0
You need to be logged in to leave comments. Login now