##// END OF EJS Templates
git: implement some changelog methods...
Romain DEP. -
r45364:3e09d22a default
parent child Browse files
Show More
@@ -1,469 +1,523 b''
1 1 from __future__ import absolute_import
2 2
3 3 from mercurial.i18n import _
4 4
5 5 from mercurial import (
6 6 ancestor,
7 7 changelog as hgchangelog,
8 8 dagop,
9 9 encoding,
10 10 error,
11 11 manifest,
12 12 node as nodemod,
13 13 pycompat,
14 14 )
15 15 from mercurial.interfaces import (
16 16 repository,
17 17 util as interfaceutil,
18 18 )
19 19 from mercurial.utils import stringutil
20 20 from . import (
21 21 gitutil,
22 22 index,
23 23 manifest as gitmanifest,
24 24 )
25 25
26 26 pygit2 = gitutil.get_pygit2()
27 27
28 28
29 29 class baselog(object): # revlog.revlog):
30 30 """Common implementations between changelog and manifestlog."""
31 31
32 32 def __init__(self, gr, db):
33 33 self.gitrepo = gr
34 34 self._db = db
35 35
36 36 def __len__(self):
37 37 return int(
38 38 self._db.execute('SELECT COUNT(*) FROM changelog').fetchone()[0]
39 39 )
40 40
41 41 def rev(self, n):
42 42 if n == nodemod.nullid:
43 43 return -1
44 44 t = self._db.execute(
45 45 'SELECT rev FROM changelog WHERE node = ?', (gitutil.togitnode(n),)
46 46 ).fetchone()
47 47 if t is None:
48 48 raise error.LookupError(n, b'00changelog.i', _(b'no node %d'))
49 49 return t[0]
50 50
51 51 def node(self, r):
52 52 if r == nodemod.nullrev:
53 53 return nodemod.nullid
54 54 t = self._db.execute(
55 55 'SELECT node FROM changelog WHERE rev = ?', (r,)
56 56 ).fetchone()
57 57 if t is None:
58 58 raise error.LookupError(r, b'00changelog.i', _(b'no node'))
59 59 return nodemod.bin(t[0])
60 60
61 61 def hasnode(self, n):
62 62 t = self._db.execute(
63 63 'SELECT node FROM changelog WHERE node = ?', (n,)
64 64 ).fetchone()
65 65 return t is not None
66 66
67 67
68 68 class baselogindex(object):
69 69 def __init__(self, log):
70 70 self._log = log
71 71
72 72 def has_node(self, n):
73 73 return self._log.rev(n) != -1
74 74
75 75 def __len__(self):
76 76 return len(self._log)
77 77
78 78 def __getitem__(self, idx):
79 79 p1rev, p2rev = self._log.parentrevs(idx)
80 80 # TODO: it's messy that the index leaks so far out of the
81 81 # storage layer that we have to implement things like reading
82 82 # this raw tuple, which exposes revlog internals.
83 83 return (
84 84 # Pretend offset is just the index, since we don't really care.
85 85 idx,
86 86 # Same with lengths
87 87 idx, # length
88 88 idx, # rawsize
89 89 -1, # delta base
90 90 idx, # linkrev TODO is this right?
91 91 p1rev,
92 92 p2rev,
93 93 self._log.node(idx),
94 94 )
95 95
96 96
97 97 # TODO: an interface for the changelog type?
98 98 class changelog(baselog):
99 99 def __contains__(self, rev):
100 100 try:
101 101 self.node(rev)
102 102 return True
103 103 except error.LookupError:
104 104 return False
105 105
106 106 def __iter__(self):
107 107 return iter(pycompat.xrange(len(self)))
108 108
109 109 @property
110 110 def filteredrevs(self):
111 111 # TODO: we should probably add a refs/hg/ namespace for hidden
112 112 # heads etc, but that's an idea for later.
113 113 return set()
114 114
115 115 @property
116 116 def index(self):
117 117 return baselogindex(self)
118 118
119 119 @property
120 120 def nodemap(self):
121 121 r = {
122 122 nodemod.bin(v[0]): v[1]
123 123 for v in self._db.execute('SELECT node, rev FROM changelog')
124 124 }
125 125 r[nodemod.nullid] = nodemod.nullrev
126 126 return r
127 127
128 128 def tip(self):
129 129 t = self._db.execute(
130 130 'SELECT node FROM changelog ORDER BY rev DESC LIMIT 1'
131 131 ).fetchone()
132 132 if t:
133 133 return nodemod.bin(t[0])
134 134 return nodemod.nullid
135 135
136 136 def revs(self, start=0, stop=None):
137 137 if stop is None:
138 138 stop = self.tip()
139 139 t = self._db.execute(
140 140 'SELECT rev FROM changelog '
141 141 'WHERE rev >= ? AND rev <= ? '
142 142 'ORDER BY REV ASC',
143 143 (start, stop),
144 144 )
145 145 return (int(r[0]) for r in t)
146 146
147 147 def _partialmatch(self, id):
148 148 if nodemod.wdirhex.startswith(id):
149 149 raise error.WdirUnsupported
150 150 candidates = [
151 151 nodemod.bin(x[0])
152 152 for x in self._db.execute(
153 153 'SELECT node FROM changelog WHERE node LIKE ?', (id + b'%',)
154 154 )
155 155 ]
156 156 if nodemod.nullhex.startswith(id):
157 157 candidates.append(nodemod.nullid)
158 158 if len(candidates) > 1:
159 159 raise error.AmbiguousPrefixLookupError(
160 160 id, b'00changelog.i', _(b'ambiguous identifier')
161 161 )
162 162 if candidates:
163 163 return candidates[0]
164 164 return None
165 165
166 166 def flags(self, rev):
167 167 return 0
168 168
169 169 def shortest(self, node, minlength=1):
170 170 nodehex = nodemod.hex(node)
171 171 for attempt in pycompat.xrange(minlength, len(nodehex) + 1):
172 172 candidate = nodehex[:attempt]
173 173 matches = int(
174 174 self._db.execute(
175 175 'SELECT COUNT(*) FROM changelog WHERE node LIKE ?',
176 176 (pycompat.sysstr(candidate + b'%'),),
177 177 ).fetchone()[0]
178 178 )
179 179 if matches == 1:
180 180 return candidate
181 181 return nodehex
182 182
183 183 def headrevs(self, revs=None):
184 184 realheads = [
185 185 int(x[0])
186 186 for x in self._db.execute(
187 187 'SELECT rev FROM changelog '
188 188 'INNER JOIN heads ON changelog.node = heads.node'
189 189 )
190 190 ]
191 191 if revs:
192 192 return sorted([r for r in revs if r in realheads])
193 193 return sorted(realheads)
194 194
195 195 def changelogrevision(self, nodeorrev):
196 196 # Ensure we have a node id
197 197 if isinstance(nodeorrev, int):
198 198 n = self.node(nodeorrev)
199 199 else:
200 200 n = nodeorrev
201 201 # handle looking up nullid
202 202 if n == nodemod.nullid:
203 203 return hgchangelog._changelogrevision(extra={})
204 204 hn = gitutil.togitnode(n)
205 205 # We've got a real commit!
206 206 files = [
207 207 r[0]
208 208 for r in self._db.execute(
209 209 'SELECT filename FROM changedfiles '
210 210 'WHERE node = ? and filenode != ?',
211 211 (hn, gitutil.nullgit),
212 212 )
213 213 ]
214 214 filesremoved = [
215 215 r[0]
216 216 for r in self._db.execute(
217 217 'SELECT filename FROM changedfiles '
218 218 'WHERE node = ? and filenode = ?',
219 219 (hn, nodemod.nullhex),
220 220 )
221 221 ]
222 222 c = self.gitrepo[hn]
223 223 return hgchangelog._changelogrevision(
224 224 manifest=n, # pretend manifest the same as the commit node
225 225 user=b'%s <%s>'
226 226 % (c.author.name.encode('utf8'), c.author.email.encode('utf8')),
227 227 date=(c.author.time, -c.author.offset * 60),
228 228 files=files,
229 229 # TODO filesadded in the index
230 230 filesremoved=filesremoved,
231 231 description=c.message.encode('utf8'),
232 232 # TODO do we want to handle extra? how?
233 233 extra={b'branch': b'default'},
234 234 )
235 235
236 236 def ancestors(self, revs, stoprev=0, inclusive=False):
237 237 revs = list(revs)
238 238 tip = self.rev(self.tip())
239 239 for r in revs:
240 240 if r > tip:
241 241 raise IndexError(b'Invalid rev %r' % r)
242 242 return ancestor.lazyancestors(
243 243 self.parentrevs, revs, stoprev=stoprev, inclusive=inclusive
244 244 )
245 245
246 246 # Cleanup opportunity: this is *identical* to the revlog.py version
247 247 def descendants(self, revs):
248 248 return dagop.descendantrevs(revs, self.revs, self.parentrevs)
249 249
250 def incrementalmissingrevs(self, common=None):
251 """Return an object that can be used to incrementally compute the
252 revision numbers of the ancestors of arbitrary sets that are not
253 ancestors of common. This is an ancestor.incrementalmissingancestors
254 object.
255
256 'common' is a list of revision numbers. If common is not supplied, uses
257 nullrev.
258 """
259 if common is None:
260 common = [nodemod.nullrev]
261
262 return ancestor.incrementalmissingancestors(self.parentrevs, common)
263
264 def findmissing(self, common=None, heads=None):
265 """Return the ancestors of heads that are not ancestors of common.
266
267 More specifically, return a list of nodes N such that every N
268 satisfies the following constraints:
269
270 1. N is an ancestor of some node in 'heads'
271 2. N is not an ancestor of any node in 'common'
272
273 The list is sorted by revision number, meaning it is
274 topologically sorted.
275
276 'heads' and 'common' are both lists of node IDs. If heads is
277 not supplied, uses all of the revlog's heads. If common is not
278 supplied, uses nullid."""
279 if common is None:
280 common = [nodemod.nullid]
281 if heads is None:
282 heads = self.heads()
283
284 common = [self.rev(n) for n in common]
285 heads = [self.rev(n) for n in heads]
286
287 inc = self.incrementalmissingrevs(common=common)
288 return [self.node(r) for r in inc.missingancestors(heads)]
289
290 def children(self, node):
291 """find the children of a given node"""
292 c = []
293 p = self.rev(node)
294 for r in self.revs(start=p + 1):
295 prevs = [pr for pr in self.parentrevs(r) if pr != nodemod.nullrev]
296 if prevs:
297 for pr in prevs:
298 if pr == p:
299 c.append(self.node(r))
300 elif p == nodemod.nullrev:
301 c.append(self.node(r))
302 return c
303
250 304 def reachableroots(self, minroot, heads, roots, includepath=False):
251 305 return dagop._reachablerootspure(
252 306 self.parentrevs, minroot, roots, heads, includepath
253 307 )
254 308
255 309 # Cleanup opportunity: this is *identical* to the revlog.py version
256 310 def isancestor(self, a, b):
257 311 a, b = self.rev(a), self.rev(b)
258 312 return self.isancestorrev(a, b)
259 313
260 314 # Cleanup opportunity: this is *identical* to the revlog.py version
261 315 def isancestorrev(self, a, b):
262 316 if a == nodemod.nullrev:
263 317 return True
264 318 elif a == b:
265 319 return True
266 320 elif a > b:
267 321 return False
268 322 return bool(self.reachableroots(a, [b], [a], includepath=False))
269 323
270 324 def parentrevs(self, rev):
271 325 n = self.node(rev)
272 326 hn = gitutil.togitnode(n)
273 327 if hn != gitutil.nullgit:
274 328 c = self.gitrepo[hn]
275 329 else:
276 330 return nodemod.nullrev, nodemod.nullrev
277 331 p1 = p2 = nodemod.nullrev
278 332 if c.parents:
279 333 p1 = self.rev(c.parents[0].id.raw)
280 334 if len(c.parents) > 2:
281 335 raise error.Abort(b'TODO octopus merge handling')
282 336 if len(c.parents) == 2:
283 337 p2 = self.rev(c.parents[1].id.raw)
284 338 return p1, p2
285 339
286 340 # Private method is used at least by the tags code.
287 341 _uncheckedparentrevs = parentrevs
288 342
289 343 def commonancestorsheads(self, a, b):
290 344 # TODO the revlog verson of this has a C path, so we probably
291 345 # need to optimize this...
292 346 a, b = self.rev(a), self.rev(b)
293 347 return [
294 348 self.node(n)
295 349 for n in ancestor.commonancestorsheads(self.parentrevs, a, b)
296 350 ]
297 351
298 352 def branchinfo(self, rev):
299 353 """Git doesn't do named branches, so just put everything on default."""
300 354 return b'default', False
301 355
302 356 def delayupdate(self, tr):
303 357 # TODO: I think we can elide this because we're just dropping
304 358 # an object in the git repo?
305 359 pass
306 360
307 361 def add(
308 362 self,
309 363 manifest,
310 364 files,
311 365 desc,
312 366 transaction,
313 367 p1,
314 368 p2,
315 369 user,
316 370 date=None,
317 371 extra=None,
318 372 p1copies=None,
319 373 p2copies=None,
320 374 filesadded=None,
321 375 filesremoved=None,
322 376 ):
323 377 parents = []
324 378 hp1, hp2 = gitutil.togitnode(p1), gitutil.togitnode(p2)
325 379 if p1 != nodemod.nullid:
326 380 parents.append(hp1)
327 381 if p2 and p2 != nodemod.nullid:
328 382 parents.append(hp2)
329 383 assert date is not None
330 384 timestamp, tz = date
331 385 sig = pygit2.Signature(
332 386 encoding.unifromlocal(stringutil.person(user)),
333 387 encoding.unifromlocal(stringutil.email(user)),
334 388 timestamp,
335 389 -(tz // 60),
336 390 )
337 391 oid = self.gitrepo.create_commit(
338 392 None, sig, sig, desc, gitutil.togitnode(manifest), parents
339 393 )
340 394 # Set up an internal reference to force the commit into the
341 395 # changelog. Hypothetically, we could even use this refs/hg/
342 396 # namespace to allow for anonymous heads on git repos, which
343 397 # would be neat.
344 398 self.gitrepo.references.create(
345 399 'refs/hg/internal/latest-commit', oid, force=True
346 400 )
347 401 # Reindex now to pick up changes. We omit the progress
348 402 # callback because this will be very quick.
349 403 index._index_repo(self.gitrepo, self._db)
350 404 return oid.raw
351 405
352 406
353 407 class manifestlog(baselog):
354 408 def __getitem__(self, node):
355 409 return self.get(b'', node)
356 410
357 411 def get(self, relpath, node):
358 412 if node == nodemod.nullid:
359 413 # TODO: this should almost certainly be a memgittreemanifestctx
360 414 return manifest.memtreemanifestctx(self, relpath)
361 415 commit = self.gitrepo[gitutil.togitnode(node)]
362 416 t = commit.tree
363 417 if relpath:
364 418 parts = relpath.split(b'/')
365 419 for p in parts:
366 420 te = t[p]
367 421 t = self.gitrepo[te.id]
368 422 return gitmanifest.gittreemanifestctx(self.gitrepo, t)
369 423
370 424
371 425 @interfaceutil.implementer(repository.ifilestorage)
372 426 class filelog(baselog):
373 427 def __init__(self, gr, db, path):
374 428 super(filelog, self).__init__(gr, db)
375 429 assert isinstance(path, bytes)
376 430 self.path = path
377 431
378 432 def read(self, node):
379 433 if node == nodemod.nullid:
380 434 return b''
381 435 return self.gitrepo[gitutil.togitnode(node)].data
382 436
383 437 def lookup(self, node):
384 438 if len(node) not in (20, 40):
385 439 node = int(node)
386 440 if isinstance(node, int):
387 441 assert False, b'todo revnums for nodes'
388 442 if len(node) == 40:
389 443 node = nodemod.bin(node)
390 444 hnode = gitutil.togitnode(node)
391 445 if hnode in self.gitrepo:
392 446 return node
393 447 raise error.LookupError(self.path, node, _(b'no match found'))
394 448
395 449 def cmp(self, node, text):
396 450 """Returns True if text is different than content at `node`."""
397 451 return self.read(node) != text
398 452
399 453 def add(self, text, meta, transaction, link, p1=None, p2=None):
400 454 assert not meta # Should we even try to handle this?
401 455 return self.gitrepo.create_blob(text).raw
402 456
403 457 def __iter__(self):
404 458 for clrev in self._db.execute(
405 459 '''
406 460 SELECT rev FROM changelog
407 461 INNER JOIN changedfiles ON changelog.node = changedfiles.node
408 462 WHERE changedfiles.filename = ? AND changedfiles.filenode != ?
409 463 ''',
410 464 (pycompat.fsdecode(self.path), gitutil.nullgit),
411 465 ):
412 466 yield clrev[0]
413 467
414 468 def linkrev(self, fr):
415 469 return fr
416 470
417 471 def rev(self, node):
418 472 row = self._db.execute(
419 473 '''
420 474 SELECT rev FROM changelog
421 475 INNER JOIN changedfiles ON changelog.node = changedfiles.node
422 476 WHERE changedfiles.filename = ? AND changedfiles.filenode = ?''',
423 477 (pycompat.fsdecode(self.path), gitutil.togitnode(node)),
424 478 ).fetchone()
425 479 if row is None:
426 480 raise error.LookupError(self.path, node, _(b'no such node'))
427 481 return int(row[0])
428 482
429 483 def node(self, rev):
430 484 maybe = self._db.execute(
431 485 '''SELECT filenode FROM changedfiles
432 486 INNER JOIN changelog ON changelog.node = changedfiles.node
433 487 WHERE changelog.rev = ? AND filename = ?
434 488 ''',
435 489 (rev, pycompat.fsdecode(self.path)),
436 490 ).fetchone()
437 491 if maybe is None:
438 492 raise IndexError('gitlog %r out of range %d' % (self.path, rev))
439 493 return nodemod.bin(maybe[0])
440 494
441 495 def parents(self, node):
442 496 gn = gitutil.togitnode(node)
443 497 gp = pycompat.fsdecode(self.path)
444 498 ps = []
445 499 for p in self._db.execute(
446 500 '''SELECT p1filenode, p2filenode FROM changedfiles
447 501 WHERE filenode = ? AND filename = ?
448 502 ''',
449 503 (gn, gp),
450 504 ).fetchone():
451 505 if p is None:
452 506 commit = self._db.execute(
453 507 "SELECT node FROM changedfiles "
454 508 "WHERE filenode = ? AND filename = ?",
455 509 (gn, gp),
456 510 ).fetchone()[0]
457 511 # This filelog is missing some data. Build the
458 512 # filelog, then recurse (which will always find data).
459 513 if pycompat.ispy3:
460 514 commit = commit.decode('ascii')
461 515 index.fill_in_filelog(self.gitrepo, self._db, commit, gp, gn)
462 516 return self.parents(node)
463 517 else:
464 518 ps.append(nodemod.bin(p))
465 519 return ps
466 520
467 521 def renamed(self, node):
468 522 # TODO: renames/copies
469 523 return False
General Comments 0
You need to be logged in to leave comments. Login now